feat: Add quick settings menu with theme switcher and other changes (#673)
Co-authored-by: Mo Bitar <mo@standardnotes.org> Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 18C8.94943 18 7.90914 17.7931 6.93853 17.391C5.96793 16.989 5.08601 16.3997 4.34315 15.6569C2.84285 14.1566 2 12.1217 2 10C2 7.87827 2.84285 5.84344 4.34315 4.34315C5.84344 2.84285 7.87827 2 10 2C14.4 2 18 5.2 18 9.2C18 10.473 17.4943 11.6939 16.5941 12.5941C15.6939 13.4943 14.473 14 13.2 14H11.76C11.52 14 11.36 14.16 11.36 14.4C11.36 14.48 11.44 14.56 11.44 14.64C11.76 15.04 11.92 15.52 11.92 16C12 17.12 11.12 18 10 18ZM10 3.6C8.30261 3.6 6.67475 4.27428 5.47452 5.47452C4.27428 6.67475 3.6 8.30261 3.6 10C3.6 11.6974 4.27428 13.3253 5.47452 14.5255C6.67475 15.7257 8.30261 16.4 10 16.4C10.24 16.4 10.4 16.24 10.4 16C10.4 15.84 10.32 15.76 10.32 15.68C10 15.28 9.84 14.88 9.84 14.4C9.84 13.28 10.72 12.4 11.84 12.4H13.2C14.0487 12.4 14.8626 12.0629 15.4627 11.4627C16.0629 10.8626 16.4 10.0487 16.4 9.2C16.4 6.08 13.52 3.6 10 3.6ZM5.6 8.4C6.24 8.4 6.8 8.96 6.8 9.6C6.8 10.24 6.24 10.8 5.6 10.8C4.96 10.8 4.4 10.24 4.4 9.6C4.4 8.96 4.96 8.4 5.6 8.4ZM8 5.2C8.64 5.2 9.2 5.76 9.2 6.4C9.2 7.04 8.64 7.6 8 7.6C7.36 7.6 6.8 7.04 6.8 6.4C6.8 5.76 7.36 5.2 8 5.2ZM12 5.2C12.64 5.2 13.2 5.76 13.2 6.4C13.2 7.04 12.64 7.6 12 7.6C11.36 7.6 10.8 7.04 10.8 6.4C10.8 5.76 11.36 5.2 12 5.2ZM14.4 8.4C15.04 8.4 15.6 8.96 15.6 9.6C15.6 10.24 15.04 10.8 14.4 10.8C13.76 10.8 13.2 10.24 13.2 9.6C13.2 8.96 13.76 8.4 14.4 8.4Z" fill="#72767E"/>
|
||||
<path d="M10 18C8.94943 18 7.90914 17.7931 6.93853 17.391C5.96793 16.989 5.08601 16.3997 4.34315 15.6569C2.84285 14.1566 2 12.1217 2 10C2 7.87827 2.84285 5.84344 4.34315 4.34315C5.84344 2.84285 7.87827 2 10 2C14.4 2 18 5.2 18 9.2C18 10.473 17.4943 11.6939 16.5941 12.5941C15.6939 13.4943 14.473 14 13.2 14H11.76C11.52 14 11.36 14.16 11.36 14.4C11.36 14.48 11.44 14.56 11.44 14.64C11.76 15.04 11.92 15.52 11.92 16C12 17.12 11.12 18 10 18ZM10 3.6C8.30261 3.6 6.67475 4.27428 5.47452 5.47452C4.27428 6.67475 3.6 8.30261 3.6 10C3.6 11.6974 4.27428 13.3253 5.47452 14.5255C6.67475 15.7257 8.30261 16.4 10 16.4C10.24 16.4 10.4 16.24 10.4 16C10.4 15.84 10.32 15.76 10.32 15.68C10 15.28 9.84 14.88 9.84 14.4C9.84 13.28 10.72 12.4 11.84 12.4H13.2C14.0487 12.4 14.8626 12.0629 15.4627 11.4627C16.0629 10.8626 16.4 10.0487 16.4 9.2C16.4 6.08 13.52 3.6 10 3.6ZM5.6 8.4C6.24 8.4 6.8 8.96 6.8 9.6C6.8 10.24 6.24 10.8 5.6 10.8C4.96 10.8 4.4 10.24 4.4 9.6C4.4 8.96 4.96 8.4 5.6 8.4ZM8 5.2C8.64 5.2 9.2 5.76 9.2 6.4C9.2 7.04 8.64 7.6 8 7.6C7.36 7.6 6.8 7.04 6.8 6.4C6.8 5.76 7.36 5.2 8 5.2ZM12 5.2C12.64 5.2 13.2 5.76 13.2 6.4C13.2 7.04 12.64 7.6 12 7.6C11.36 7.6 10.8 7.04 10.8 6.4C10.8 5.76 11.36 5.2 12 5.2ZM14.4 8.4C15.04 8.4 15.6 8.96 15.6 9.6C15.6 10.24 15.04 10.8 14.4 10.8C13.76 10.8 13.2 10.24 13.2 9.6C13.2 8.96 13.76 8.4 14.4 8.4Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -64,6 +64,7 @@ import { IconDirective } from './components/Icon';
|
||||
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
|
||||
import { PreferencesDirective } from './preferences';
|
||||
import { AppVersion, IsWebPlatform } from '@/version';
|
||||
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
/**
|
||||
@@ -154,6 +155,7 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('syncResolutionMenu', () => new SyncResolutionMenu())
|
||||
.directive('sessionsModal', SessionsModalDirective)
|
||||
.directive('accountMenu', AccountMenuDirective)
|
||||
.directive('quickSettingsMenu', QuickSettingsMenuDirective)
|
||||
.directive('noAccountWarning', NoAccountWarningDirective)
|
||||
.directive('protectedNotePanel', NoProtectionsdNoteWarningDirective)
|
||||
.directive('searchOptions', SearchOptionsDirective)
|
||||
|
||||
@@ -39,7 +39,7 @@ export const AdvancedOptions: FunctionComponent<Props> = observer(
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="sn-dropdown-item font-bold"
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none font-bold"
|
||||
onClick={toggleShowAdvanced}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { StateUpdater, useRef, useState } from 'preact/hooks';
|
||||
import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { Button } from '../Button';
|
||||
import { Checkbox } from '../Checkbox';
|
||||
@@ -31,6 +31,10 @@ export const ConfirmPassword: FunctionComponent<Props> = observer(
|
||||
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
useEffect(() => {
|
||||
passwordInputRef?.current?.focus();
|
||||
}, []);
|
||||
|
||||
const handlePasswordChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setConfirmPassword(e.target.value);
|
||||
|
||||
@@ -5,9 +5,10 @@ import { Icon } from '../Icon';
|
||||
import { formatLastSyncDate } from '@/preferences/panes/account/Sync';
|
||||
import { SyncQueueStrategy } from '@standardnotes/snjs';
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import { AppVersion } from '@/version';
|
||||
|
||||
type Props = {
|
||||
@@ -25,6 +26,9 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
const [lastSyncDate, setLastSyncDate] = useState(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
const [currentFocusedIndex, setCurrentFocusedIndex] = useState(0);
|
||||
|
||||
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
@@ -53,9 +57,49 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
|
||||
const user = application.getUser();
|
||||
|
||||
const accountMenuRef = useRef<HTMLDivElement>();
|
||||
|
||||
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = (
|
||||
event
|
||||
) => {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
setCurrentFocusedIndex((index) => {
|
||||
console.log(index, buttonRefs.current.length);
|
||||
if (index + 1 < buttonRefs.current.length) {
|
||||
return index + 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
setCurrentFocusedIndex((index) => {
|
||||
if (index - 1 > -1) {
|
||||
return index - 1;
|
||||
} else {
|
||||
return buttonRefs.current.length - 1;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (buttonRefs.current[currentFocusedIndex]) {
|
||||
buttonRefs.current[currentFocusedIndex]?.focus();
|
||||
}
|
||||
}, [currentFocusedIndex]);
|
||||
|
||||
const pushRefToArray = (ref: HTMLButtonElement | null) => {
|
||||
if (ref && !buttonRefs.current.includes(ref)) {
|
||||
buttonRefs.current.push(ref);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-3 mt-1 mb-2">
|
||||
<div ref={accountMenuRef} onKeyDown={handleKeyDown}>
|
||||
<div className="flex items-center justify-between px-3 mt-1 mb-3">
|
||||
<div className="sn-account-menu-headline">Account</div>
|
||||
<div className="flex cursor-pointer" onClick={closeMenu}>
|
||||
<Icon type="close" className="color-grey-1" />
|
||||
@@ -105,7 +149,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
{user ? (
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
ref={pushRefToArray}
|
||||
onClick={() => {
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
appState.preferences.setCurrentPane('account');
|
||||
@@ -118,7 +163,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
ref={pushRefToArray}
|
||||
onClick={() => {
|
||||
setMenuPane(AccountMenuPane.Register);
|
||||
}}
|
||||
@@ -127,7 +173,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
Create free account
|
||||
</button>
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
ref={pushRefToArray}
|
||||
onClick={() => {
|
||||
setMenuPane(AccountMenuPane.SignIn);
|
||||
}}
|
||||
@@ -138,7 +185,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="sn-dropdown-item justify-between"
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
ref={pushRefToArray}
|
||||
onClick={() => {
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
appState.preferences.setCurrentPane('help-feedback');
|
||||
@@ -155,7 +203,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
<>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
ref={pushRefToArray}
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true);
|
||||
}}
|
||||
@@ -165,7 +214,7 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { SignInPane } from './SignIn';
|
||||
import { CreateAccount } from './CreateAccount';
|
||||
import { ConfirmSignoutContainer } from '../ConfirmSignoutModal';
|
||||
import { ConfirmPassword } from './ConfirmPassword';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
|
||||
export enum AccountMenuPane {
|
||||
GeneralMenu,
|
||||
@@ -87,14 +88,31 @@ const AccountMenu: FunctionComponent<Props> = observer(
|
||||
closeAccountMenu,
|
||||
} = appState.accountMenu;
|
||||
|
||||
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = (
|
||||
event
|
||||
) => {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
if (currentPane === AccountMenuPane.GeneralMenu) {
|
||||
closeAccountMenu();
|
||||
} else if (currentPane === AccountMenuPane.ConfirmPassword) {
|
||||
setCurrentPane(AccountMenuPane.Register);
|
||||
} else {
|
||||
setCurrentPane(AccountMenuPane.GeneralMenu);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sn-component">
|
||||
<div
|
||||
className={`sn-account-menu sn-dropdown ${
|
||||
className={`sn-menu-border sn-account-menu sn-dropdown ${
|
||||
shouldAnimateCloseMenu
|
||||
? 'slide-up-animation'
|
||||
: 'sn-dropdown--animated'
|
||||
} min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto absolute`}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<MenuPaneSelector
|
||||
appState={appState}
|
||||
|
||||
315
app/assets/javascripts/components/QuickSettingsMenu.tsx
Normal file
315
app/assets/javascripts/components/QuickSettingsMenu.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import { ContentType, SNTheme } from '@standardnotes/snjs';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import { Icon } from './Icon';
|
||||
import { toDirective, useCloseOnBlur } from './utils';
|
||||
|
||||
const MENU_CLASSNAME =
|
||||
'sn-menu-border sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto';
|
||||
|
||||
type ThemeButtonProps = {
|
||||
theme: SNTheme;
|
||||
application: WebApplication;
|
||||
onBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
};
|
||||
|
||||
type MenuProps = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
const ThemeButton: FunctionComponent<ThemeButtonProps> = ({
|
||||
application,
|
||||
theme,
|
||||
onBlur,
|
||||
}) => {
|
||||
const toggleTheme = () => {
|
||||
if (theme.isLayerable() || !theme.active) {
|
||||
application.toggleComponent(theme);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={toggleTheme}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{theme.isLayerable() ? (
|
||||
theme.active ? (
|
||||
<Icon type="check" className="color-info mr-2" />
|
||||
) : null
|
||||
) : (
|
||||
<div
|
||||
className={`pseudo-radio-btn ${
|
||||
theme.active ? 'pseudo-radio-btn--checked' : ''
|
||||
} mr-2`}
|
||||
></div>
|
||||
)}
|
||||
<span className={theme.active ? 'font-semibold' : undefined}>
|
||||
{theme.package_info.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="w-5 h-5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: theme.package_info?.dock_icon?.background_color,
|
||||
}}
|
||||
></div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
({ application, appState }) => {
|
||||
const { closeQuickSettingsMenu, shouldAnimateCloseMenu } =
|
||||
appState.quickSettingsMenu;
|
||||
const [themes, setThemes] = useState<SNTheme[]>([]);
|
||||
const [themesMenuOpen, setThemesMenuOpen] = useState(false);
|
||||
const [themesMenuPosition, setThemesMenuPosition] = useState({});
|
||||
const [defaultThemeOn, setDefaultThemeOn] = useState(false);
|
||||
|
||||
const themesMenuRef = useRef<HTMLDivElement>();
|
||||
const themesButtonRef = useRef<HTMLButtonElement>();
|
||||
const prefsButtonRef = useRef<HTMLButtonElement>();
|
||||
const quickSettingsMenuRef = useRef<HTMLDivElement>();
|
||||
const defaultThemeButtonRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const reloadThemes = useCallback(() => {
|
||||
application.streamItems(ContentType.Theme, () => {
|
||||
const themes = application.getDisplayableItems(
|
||||
ContentType.Theme
|
||||
) as SNTheme[];
|
||||
setThemes(
|
||||
themes.sort((a, b) => {
|
||||
const aIsLayerable = a.isLayerable();
|
||||
const bIsLayerable = b.isLayerable();
|
||||
|
||||
if (aIsLayerable && !bIsLayerable) {
|
||||
return 1;
|
||||
} else if (!aIsLayerable && bIsLayerable) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.package_info.name.toLowerCase() <
|
||||
b.package_info.name.toLowerCase()
|
||||
? -1
|
||||
: 1;
|
||||
}
|
||||
})
|
||||
);
|
||||
setDefaultThemeOn(
|
||||
!themes.find((theme) => theme.active && !theme.isLayerable())
|
||||
);
|
||||
});
|
||||
}, [application]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadThemes();
|
||||
}, [reloadThemes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (themesMenuOpen) {
|
||||
defaultThemeButtonRef.current.focus();
|
||||
}
|
||||
}, [themesMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
prefsButtonRef.current.focus();
|
||||
}, []);
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(themesMenuRef, setThemesMenuOpen);
|
||||
|
||||
const toggleThemesMenu = () => {
|
||||
if (!themesMenuOpen) {
|
||||
const themesButtonRect =
|
||||
themesButtonRef.current.getBoundingClientRect();
|
||||
setThemesMenuPosition({
|
||||
left: themesButtonRect.right,
|
||||
bottom:
|
||||
document.documentElement.clientHeight - themesButtonRect.bottom,
|
||||
});
|
||||
setThemesMenuOpen(true);
|
||||
} else {
|
||||
setThemesMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openPreferences = () => {
|
||||
closeQuickSettingsMenu();
|
||||
appState.preferences.openPreferences();
|
||||
};
|
||||
|
||||
const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (
|
||||
event
|
||||
) => {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
setThemesMenuOpen(false);
|
||||
themesButtonRef.current.focus();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (!themesMenuOpen) {
|
||||
toggleThemesMenu();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> =
|
||||
(event) => {
|
||||
const items: NodeListOf<HTMLButtonElement> =
|
||||
quickSettingsMenuRef.current.querySelectorAll(':scope > button');
|
||||
const currentFocusedIndex = Array.from(items).findIndex(
|
||||
(btn) => btn === document.activeElement
|
||||
);
|
||||
|
||||
if (!themesMenuOpen) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
closeQuickSettingsMenu();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (items[currentFocusedIndex + 1]) {
|
||||
items[currentFocusedIndex + 1].focus();
|
||||
} else {
|
||||
items[0].focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (items[currentFocusedIndex - 1]) {
|
||||
items[currentFocusedIndex - 1].focus();
|
||||
} else {
|
||||
items[items.length - 1].focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (
|
||||
event
|
||||
) => {
|
||||
const themes = themesMenuRef.current.querySelectorAll('button');
|
||||
const currentFocusedIndex = Array.from(themes).findIndex(
|
||||
(themeBtn) => themeBtn === document.activeElement
|
||||
);
|
||||
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
case 'ArrowLeft':
|
||||
event.stopPropagation();
|
||||
setThemesMenuOpen(false);
|
||||
themesButtonRef.current.focus();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (themes[currentFocusedIndex + 1]) {
|
||||
themes[currentFocusedIndex + 1].focus();
|
||||
} else {
|
||||
themes[0].focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (themes[currentFocusedIndex - 1]) {
|
||||
themes[currentFocusedIndex - 1].focus();
|
||||
} else {
|
||||
themes[themes.length - 1].focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDefaultTheme = () => {
|
||||
const activeTheme = themes.find(
|
||||
(theme) => theme.active && !theme.isLayerable()
|
||||
);
|
||||
if (activeTheme) application.toggleComponent(activeTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sn-component">
|
||||
<div
|
||||
className={`sn-quick-settings-menu absolute ${MENU_CLASSNAME} ${
|
||||
shouldAnimateCloseMenu
|
||||
? 'slide-up-animation'
|
||||
: 'sn-dropdown--animated'
|
||||
}`}
|
||||
ref={quickSettingsMenuRef}
|
||||
onKeyDown={handleQuickSettingsKeyDown}
|
||||
>
|
||||
<div className="px-3 mt-1 mb-2 font-semibold color-text uppercase">
|
||||
Quick Settings
|
||||
</div>
|
||||
<Disclosure open={themesMenuOpen} onChange={toggleThemesMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={handleBtnKeyDown}
|
||||
onBlur={closeOnBlur}
|
||||
ref={themesButtonRef}
|
||||
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="themes" className="color-neutral mr-2" />
|
||||
Themes
|
||||
</div>
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onBlur={closeOnBlur}
|
||||
ref={themesMenuRef}
|
||||
onKeyDown={handlePanelKeyDown}
|
||||
style={{
|
||||
...themesMenuPosition,
|
||||
}}
|
||||
className={`${MENU_CLASSNAME} fixed sn-dropdown--animated`}
|
||||
>
|
||||
<div className="px-3 my-1 font-semibold color-text uppercase">
|
||||
Themes
|
||||
</div>
|
||||
<button
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={toggleDefaultTheme}
|
||||
onBlur={closeOnBlur}
|
||||
ref={defaultThemeButtonRef}
|
||||
>
|
||||
<div
|
||||
className={`pseudo-radio-btn ${
|
||||
defaultThemeOn ? 'pseudo-radio-btn--checked' : ''
|
||||
} mr-2`}
|
||||
></div>
|
||||
Default
|
||||
</button>
|
||||
{themes.map((theme) => (
|
||||
<ThemeButton
|
||||
theme={theme}
|
||||
application={application}
|
||||
key={theme.uuid}
|
||||
onBlur={closeOnBlur}
|
||||
/>
|
||||
))}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<button
|
||||
class="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={openPreferences}
|
||||
ref={prefsButtonRef}
|
||||
>
|
||||
<Icon type="more" className="color-neutral mr-2" />
|
||||
Open Preferences
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const QuickSettingsMenuDirective =
|
||||
toDirective<MenuProps>(QuickSettingsMenu);
|
||||
@@ -52,6 +52,7 @@ export class AccountMenuState {
|
||||
shouldAnimateCloseMenu: observable,
|
||||
|
||||
setShow: action,
|
||||
setShouldAnimateClose: action,
|
||||
toggleShow: action,
|
||||
setSigningOut: action,
|
||||
setIsEncryptionEnabled: action,
|
||||
@@ -95,11 +96,15 @@ export class AccountMenuState {
|
||||
this.show = show;
|
||||
};
|
||||
|
||||
setShouldAnimateClose = (shouldAnimateCloseMenu: boolean): void => {
|
||||
this.shouldAnimateCloseMenu = shouldAnimateCloseMenu;
|
||||
};
|
||||
|
||||
closeAccountMenu = (): void => {
|
||||
this.shouldAnimateCloseMenu = true;
|
||||
this.setShouldAnimateClose(true);
|
||||
setTimeout(() => {
|
||||
this.setShow(false);
|
||||
this.shouldAnimateCloseMenu = false;
|
||||
this.setShouldAnimateClose(false);
|
||||
this.setCurrentPane(AccountMenuPane.GeneralMenu);
|
||||
}, 150);
|
||||
};
|
||||
@@ -137,7 +142,11 @@ export class AccountMenuState {
|
||||
};
|
||||
|
||||
toggleShow = (): void => {
|
||||
this.show = !this.show;
|
||||
if (this.show) {
|
||||
this.closeAccountMenu();
|
||||
} else {
|
||||
this.setShow(true);
|
||||
}
|
||||
};
|
||||
|
||||
setOtherSessionsSignOut = (otherSessionsSignOut: boolean): void => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { NotesState } from './notes_state';
|
||||
import { TagsState } from './tags_state';
|
||||
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
|
||||
import { PreferencesState } from './preferences_state';
|
||||
import { QuickSettingsState } from './quick_settings_state';
|
||||
|
||||
export enum AppStateEvent {
|
||||
TagChanged,
|
||||
@@ -62,6 +63,7 @@ export class AppState {
|
||||
onVisibilityChange: any;
|
||||
selectedTag?: SNTag;
|
||||
showBetaWarning: boolean;
|
||||
readonly quickSettingsMenu = new QuickSettingsState();
|
||||
readonly accountMenu: AccountMenuState;
|
||||
readonly actionsMenu = new ActionsMenuState();
|
||||
readonly preferences = new PreferencesState();
|
||||
@@ -105,7 +107,7 @@ export class AppState {
|
||||
);
|
||||
this.accountMenu = new AccountMenuState(
|
||||
application,
|
||||
this.appEventObserverRemovers,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.searchOptions = new SearchOptionsState(
|
||||
application,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
|
||||
export class QuickSettingsState {
|
||||
open = false;
|
||||
shouldAnimateCloseMenu = false;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
open: observable,
|
||||
shouldAnimateCloseMenu: observable,
|
||||
|
||||
setOpen: action,
|
||||
setShouldAnimateCloseMenu: action,
|
||||
toggle: action,
|
||||
closeQuickSettingsMenu: action,
|
||||
});
|
||||
}
|
||||
|
||||
setOpen = (open: boolean): void => {
|
||||
this.open = open;
|
||||
};
|
||||
|
||||
setShouldAnimateCloseMenu = (shouldAnimate: boolean): void => {
|
||||
this.shouldAnimateCloseMenu = shouldAnimate;
|
||||
};
|
||||
|
||||
toggle = (): void => {
|
||||
if (this.open) {
|
||||
this.closeQuickSettingsMenu();
|
||||
} else {
|
||||
this.setOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
closeQuickSettingsMenu = (): void => {
|
||||
this.setShouldAnimateCloseMenu(true);
|
||||
setTimeout(() => {
|
||||
this.setOpen(false);
|
||||
this.setShouldAnimateCloseMenu(false);
|
||||
}, 150);
|
||||
};
|
||||
}
|
||||
@@ -23,14 +23,22 @@
|
||||
ng-if='ctrl.showAccountMenu',
|
||||
)
|
||||
.sk-app-bar-item.ml-0-important(
|
||||
ng-click='ctrl.clickPreferences()'
|
||||
click-outside='ctrl.clickOutsideQuickSettingsMenu()',
|
||||
is-open='ctrl.showQuickSettingsMenu',
|
||||
ng-click='ctrl.quickSettingsPressed()'
|
||||
)
|
||||
.w-8.h-full.flex.items-center.justify-center.cursor-pointer
|
||||
.h-5
|
||||
icon(
|
||||
type="tune"
|
||||
class-name="rounded hover:color-info"
|
||||
ng-class="{'color-info': ctrl.showQuickSettingsMenu}"
|
||||
)
|
||||
quick-settings-menu(
|
||||
ng-click='$event.stopPropagation()',
|
||||
app-state='ctrl.appState'
|
||||
application='ctrl.application'
|
||||
ng-if='ctrl.showQuickSettingsMenu',)
|
||||
.sk-app-bar-item
|
||||
a.no-decoration.sk-label.title(
|
||||
href='https://standardnotes.com/help',
|
||||
|
||||
@@ -64,6 +64,7 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
public user?: any;
|
||||
private offline = true;
|
||||
public showAccountMenu = false;
|
||||
public showQuickSettingsMenu = false;
|
||||
private didCheckForOffline = false;
|
||||
private queueExtReload = false;
|
||||
private reloadInProgress = false;
|
||||
@@ -115,6 +116,7 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
this.autorun(() => {
|
||||
const showBetaWarning = this.appState.showBetaWarning;
|
||||
this.showAccountMenu = this.appState.accountMenu.show;
|
||||
this.showQuickSettingsMenu = this.appState.quickSettingsMenu.open;
|
||||
this.setState({
|
||||
showBetaWarning: showBetaWarning,
|
||||
showDataUpgrade: !showBetaWarning,
|
||||
@@ -449,10 +451,21 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
}
|
||||
|
||||
accountMenuPressed() {
|
||||
this.appState.quickSettingsMenu.closeQuickSettingsMenu();
|
||||
this.appState.accountMenu.toggleShow();
|
||||
this.closeAllRooms();
|
||||
}
|
||||
|
||||
quickSettingsPressed() {
|
||||
this.appState.accountMenu.closeAccountMenu();
|
||||
if (this.themesWithIcons.length > 0) {
|
||||
this.appState.quickSettingsMenu.toggle();
|
||||
} else {
|
||||
this.appState.preferences.openPreferences();
|
||||
}
|
||||
this.closeAllRooms();
|
||||
}
|
||||
|
||||
toggleSyncResolutionMenu() {
|
||||
this.showSyncResolution = !this.showSyncResolution;
|
||||
}
|
||||
@@ -476,22 +489,7 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
}
|
||||
|
||||
reloadDockShortcuts() {
|
||||
const shortcuts = [];
|
||||
for (const theme of this.themesWithIcons) {
|
||||
if (!theme.package_info) {
|
||||
continue;
|
||||
}
|
||||
const name = theme.package_info.name;
|
||||
const icon = theme.package_info.dock_icon;
|
||||
if (!icon) {
|
||||
continue;
|
||||
}
|
||||
shortcuts.push({
|
||||
name: name,
|
||||
component: theme,
|
||||
icon: icon,
|
||||
} as DockShortcut);
|
||||
}
|
||||
const shortcuts: DockShortcut[] = [];
|
||||
this.setState({
|
||||
dockShortcuts: shortcuts.sort((a, b) => {
|
||||
/** Circles first, then images */
|
||||
@@ -556,8 +554,8 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
this.appState.accountMenu.closeAccountMenu();
|
||||
}
|
||||
|
||||
clickPreferences() {
|
||||
this.appState.preferences.openPreferences();
|
||||
clickOutsideQuickSettingsMenu() {
|
||||
this.appState.quickSettingsMenu.closeQuickSettingsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,20 @@
|
||||
max-height: calc(85vh - 90px);
|
||||
}
|
||||
|
||||
.sn-account-menu {
|
||||
.sn-account-menu,
|
||||
.sn-quick-settings-menu {
|
||||
z-index: $z-index-footer-bar-item-panel;
|
||||
@extend .bottom-100;
|
||||
@extend .left-0;
|
||||
@extend .cursor-auto;
|
||||
}
|
||||
|
||||
.sn-menu-border {
|
||||
@extend .border-1;
|
||||
@extend .border-solid;
|
||||
@extend .border-gray-300;
|
||||
}
|
||||
|
||||
.sn-account-menu-headline {
|
||||
@extend .sk-h2;
|
||||
@extend .sk-bold;
|
||||
|
||||
@@ -384,3 +384,41 @@
|
||||
.cursor-auto {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.top-1\/2 {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.left-1\/2 {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.-translate-1\/2 {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.pseudo-radio-btn {
|
||||
@extend .w-4;
|
||||
@extend .h-4;
|
||||
@extend .border-2;
|
||||
@extend .border-solid;
|
||||
@extend .border-info;
|
||||
@extend .rounded-full;
|
||||
@extend .relative;
|
||||
}
|
||||
|
||||
.pseudo-radio-btn--checked::after {
|
||||
content: '';
|
||||
@extend .bg-info;
|
||||
@extend .absolute;
|
||||
@extend .top-1\/2;
|
||||
@extend .left-1\/2;
|
||||
@extend .-translate-1\/2;
|
||||
@extend .w-2;
|
||||
@extend .h-2;
|
||||
@extend .rounded-full;
|
||||
}
|
||||
|
||||
.focus\:bg-info-backdrop:focus {
|
||||
background-color: var(--sn-stylekit-info-backdrop-color);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user