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:
Aman Harwara
2021-10-19 09:58:46 +05:30
committed by GitHub
parent bbeab4f623
commit c8dc07d42b
14 changed files with 529 additions and 37 deletions

View File

@@ -1,3 +1,3 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <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> </svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -64,6 +64,7 @@ import { IconDirective } from './components/Icon';
import { NoteTagsContainerDirective } from './components/NoteTagsContainer'; import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
import { PreferencesDirective } from './preferences'; import { PreferencesDirective } from './preferences';
import { AppVersion, IsWebPlatform } from '@/version'; import { AppVersion, IsWebPlatform } from '@/version';
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
function reloadHiddenFirefoxTab(): boolean { function reloadHiddenFirefoxTab(): boolean {
/** /**
@@ -154,6 +155,7 @@ const startApplication: StartApplication = async function startApplication(
.directive('syncResolutionMenu', () => new SyncResolutionMenu()) .directive('syncResolutionMenu', () => new SyncResolutionMenu())
.directive('sessionsModal', SessionsModalDirective) .directive('sessionsModal', SessionsModalDirective)
.directive('accountMenu', AccountMenuDirective) .directive('accountMenu', AccountMenuDirective)
.directive('quickSettingsMenu', QuickSettingsMenuDirective)
.directive('noAccountWarning', NoAccountWarningDirective) .directive('noAccountWarning', NoAccountWarningDirective)
.directive('protectedNotePanel', NoProtectionsdNoteWarningDirective) .directive('protectedNotePanel', NoProtectionsdNoteWarningDirective)
.directive('searchOptions', SearchOptionsDirective) .directive('searchOptions', SearchOptionsDirective)

View File

@@ -39,7 +39,7 @@ export const AdvancedOptions: FunctionComponent<Props> = observer(
return ( return (
<> <>
<button <button
className="sn-dropdown-item font-bold" className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none font-bold"
onClick={toggleShowAdvanced} onClick={toggleShowAdvanced}
> >
<div className="flex items-center"> <div className="flex items-center">

View File

@@ -3,7 +3,7 @@ import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact'; import { FunctionComponent } from 'preact';
import { StateUpdater, useRef, useState } from 'preact/hooks'; import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks';
import { AccountMenuPane } from '.'; import { AccountMenuPane } from '.';
import { Button } from '../Button'; import { Button } from '../Button';
import { Checkbox } from '../Checkbox'; import { Checkbox } from '../Checkbox';
@@ -31,6 +31,10 @@ export const ConfirmPassword: FunctionComponent<Props> = observer(
const passwordInputRef = useRef<HTMLInputElement>(); const passwordInputRef = useRef<HTMLInputElement>();
useEffect(() => {
passwordInputRef?.current?.focus();
}, []);
const handlePasswordChange = (e: Event) => { const handlePasswordChange = (e: Event) => {
if (e.target instanceof HTMLInputElement) { if (e.target instanceof HTMLInputElement) {
setConfirmPassword(e.target.value); setConfirmPassword(e.target.value);

View File

@@ -5,9 +5,10 @@ import { Icon } from '../Icon';
import { formatLastSyncDate } from '@/preferences/panes/account/Sync'; import { formatLastSyncDate } from '@/preferences/panes/account/Sync';
import { SyncQueueStrategy } from '@standardnotes/snjs'; import { SyncQueueStrategy } from '@standardnotes/snjs';
import { STRING_GENERIC_SYNC_ERROR } from '@/strings'; import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
import { useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import { AccountMenuPane } from '.'; import { AccountMenuPane } from '.';
import { FunctionComponent } from 'preact'; import { FunctionComponent } from 'preact';
import { JSXInternal } from 'preact/src/jsx';
import { AppVersion } from '@/version'; import { AppVersion } from '@/version';
type Props = { type Props = {
@@ -25,6 +26,9 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
const [lastSyncDate, setLastSyncDate] = useState( const [lastSyncDate, setLastSyncDate] = useState(
formatLastSyncDate(application.getLastSyncDate() as Date) formatLastSyncDate(application.getLastSyncDate() as Date)
); );
const [currentFocusedIndex, setCurrentFocusedIndex] = useState(0);
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
const doSynchronization = async () => { const doSynchronization = async () => {
setIsSyncingInProgress(true); setIsSyncingInProgress(true);
@@ -53,9 +57,49 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
const user = application.getUser(); 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 ( return (
<> <div ref={accountMenuRef} onKeyDown={handleKeyDown}>
<div className="flex items-center justify-between px-3 mt-1 mb-2"> <div className="flex items-center justify-between px-3 mt-1 mb-3">
<div className="sn-account-menu-headline">Account</div> <div className="sn-account-menu-headline">Account</div>
<div className="flex cursor-pointer" onClick={closeMenu}> <div className="flex cursor-pointer" onClick={closeMenu}>
<Icon type="close" className="color-grey-1" /> <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> <div className="h-1px my-2 bg-border"></div>
{user ? ( {user ? (
<button <button
className="sn-dropdown-item" className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
ref={pushRefToArray}
onClick={() => { onClick={() => {
appState.accountMenu.closeAccountMenu(); appState.accountMenu.closeAccountMenu();
appState.preferences.setCurrentPane('account'); appState.preferences.setCurrentPane('account');
@@ -118,7 +163,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
) : ( ) : (
<> <>
<button <button
className="sn-dropdown-item" className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
ref={pushRefToArray}
onClick={() => { onClick={() => {
setMenuPane(AccountMenuPane.Register); setMenuPane(AccountMenuPane.Register);
}} }}
@@ -127,7 +173,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
Create free account Create free account
</button> </button>
<button <button
className="sn-dropdown-item" className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
ref={pushRefToArray}
onClick={() => { onClick={() => {
setMenuPane(AccountMenuPane.SignIn); setMenuPane(AccountMenuPane.SignIn);
}} }}
@@ -138,7 +185,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
</> </>
)} )}
<button <button
className="sn-dropdown-item justify-between" className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
ref={pushRefToArray}
onClick={() => { onClick={() => {
appState.accountMenu.closeAccountMenu(); appState.accountMenu.closeAccountMenu();
appState.preferences.setCurrentPane('help-feedback'); appState.preferences.setCurrentPane('help-feedback');
@@ -155,7 +203,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
<> <>
<div className="h-1px my-2 bg-border"></div> <div className="h-1px my-2 bg-border"></div>
<button <button
className="sn-dropdown-item" className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
ref={pushRefToArray}
onClick={() => { onClick={() => {
appState.accountMenu.setSigningOut(true); appState.accountMenu.setSigningOut(true);
}} }}
@@ -165,7 +214,7 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
</button> </button>
</> </>
) : null} ) : null}
</> </div>
); );
} }
); );

View File

@@ -9,6 +9,7 @@ import { SignInPane } from './SignIn';
import { CreateAccount } from './CreateAccount'; import { CreateAccount } from './CreateAccount';
import { ConfirmSignoutContainer } from '../ConfirmSignoutModal'; import { ConfirmSignoutContainer } from '../ConfirmSignoutModal';
import { ConfirmPassword } from './ConfirmPassword'; import { ConfirmPassword } from './ConfirmPassword';
import { JSXInternal } from 'preact/src/jsx';
export enum AccountMenuPane { export enum AccountMenuPane {
GeneralMenu, GeneralMenu,
@@ -87,14 +88,31 @@ const AccountMenu: FunctionComponent<Props> = observer(
closeAccountMenu, closeAccountMenu,
} = appState.accountMenu; } = 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 ( return (
<div className="sn-component"> <div className="sn-component">
<div <div
className={`sn-account-menu sn-dropdown ${ className={`sn-menu-border sn-account-menu sn-dropdown ${
shouldAnimateCloseMenu shouldAnimateCloseMenu
? 'slide-up-animation' ? 'slide-up-animation'
: 'sn-dropdown--animated' : 'sn-dropdown--animated'
} min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto absolute`} } min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto absolute`}
onKeyDown={handleKeyDown}
> >
<MenuPaneSelector <MenuPaneSelector
appState={appState} appState={appState}

View 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);

View File

@@ -52,6 +52,7 @@ export class AccountMenuState {
shouldAnimateCloseMenu: observable, shouldAnimateCloseMenu: observable,
setShow: action, setShow: action,
setShouldAnimateClose: action,
toggleShow: action, toggleShow: action,
setSigningOut: action, setSigningOut: action,
setIsEncryptionEnabled: action, setIsEncryptionEnabled: action,
@@ -95,11 +96,15 @@ export class AccountMenuState {
this.show = show; this.show = show;
}; };
setShouldAnimateClose = (shouldAnimateCloseMenu: boolean): void => {
this.shouldAnimateCloseMenu = shouldAnimateCloseMenu;
};
closeAccountMenu = (): void => { closeAccountMenu = (): void => {
this.shouldAnimateCloseMenu = true; this.setShouldAnimateClose(true);
setTimeout(() => { setTimeout(() => {
this.setShow(false); this.setShow(false);
this.shouldAnimateCloseMenu = false; this.setShouldAnimateClose(false);
this.setCurrentPane(AccountMenuPane.GeneralMenu); this.setCurrentPane(AccountMenuPane.GeneralMenu);
}, 150); }, 150);
}; };
@@ -137,7 +142,11 @@ export class AccountMenuState {
}; };
toggleShow = (): void => { toggleShow = (): void => {
this.show = !this.show; if (this.show) {
this.closeAccountMenu();
} else {
this.setShow(true);
}
}; };
setOtherSessionsSignOut = (otherSessionsSignOut: boolean): void => { setOtherSessionsSignOut = (otherSessionsSignOut: boolean): void => {

View File

@@ -23,6 +23,7 @@ import { NotesState } from './notes_state';
import { TagsState } from './tags_state'; import { TagsState } from './tags_state';
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state'; import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
import { PreferencesState } from './preferences_state'; import { PreferencesState } from './preferences_state';
import { QuickSettingsState } from './quick_settings_state';
export enum AppStateEvent { export enum AppStateEvent {
TagChanged, TagChanged,
@@ -62,6 +63,7 @@ export class AppState {
onVisibilityChange: any; onVisibilityChange: any;
selectedTag?: SNTag; selectedTag?: SNTag;
showBetaWarning: boolean; showBetaWarning: boolean;
readonly quickSettingsMenu = new QuickSettingsState();
readonly accountMenu: AccountMenuState; readonly accountMenu: AccountMenuState;
readonly actionsMenu = new ActionsMenuState(); readonly actionsMenu = new ActionsMenuState();
readonly preferences = new PreferencesState(); readonly preferences = new PreferencesState();
@@ -105,7 +107,7 @@ export class AppState {
); );
this.accountMenu = new AccountMenuState( this.accountMenu = new AccountMenuState(
application, application,
this.appEventObserverRemovers, this.appEventObserverRemovers
); );
this.searchOptions = new SearchOptionsState( this.searchOptions = new SearchOptionsState(
application, application,

View File

@@ -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);
};
}

View File

@@ -23,14 +23,22 @@
ng-if='ctrl.showAccountMenu', ng-if='ctrl.showAccountMenu',
) )
.sk-app-bar-item.ml-0-important( .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 .w-8.h-full.flex.items-center.justify-center.cursor-pointer
.h-5 .h-5
icon( icon(
type="tune" type="tune"
class-name="rounded hover:color-info" 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 .sk-app-bar-item
a.no-decoration.sk-label.title( a.no-decoration.sk-label.title(
href='https://standardnotes.com/help', href='https://standardnotes.com/help',

View File

@@ -64,6 +64,7 @@ class FooterViewCtrl extends PureViewCtrl<
public user?: any; public user?: any;
private offline = true; private offline = true;
public showAccountMenu = false; public showAccountMenu = false;
public showQuickSettingsMenu = false;
private didCheckForOffline = false; private didCheckForOffline = false;
private queueExtReload = false; private queueExtReload = false;
private reloadInProgress = false; private reloadInProgress = false;
@@ -115,6 +116,7 @@ class FooterViewCtrl extends PureViewCtrl<
this.autorun(() => { this.autorun(() => {
const showBetaWarning = this.appState.showBetaWarning; const showBetaWarning = this.appState.showBetaWarning;
this.showAccountMenu = this.appState.accountMenu.show; this.showAccountMenu = this.appState.accountMenu.show;
this.showQuickSettingsMenu = this.appState.quickSettingsMenu.open;
this.setState({ this.setState({
showBetaWarning: showBetaWarning, showBetaWarning: showBetaWarning,
showDataUpgrade: !showBetaWarning, showDataUpgrade: !showBetaWarning,
@@ -449,10 +451,21 @@ class FooterViewCtrl extends PureViewCtrl<
} }
accountMenuPressed() { accountMenuPressed() {
this.appState.quickSettingsMenu.closeQuickSettingsMenu();
this.appState.accountMenu.toggleShow(); this.appState.accountMenu.toggleShow();
this.closeAllRooms(); this.closeAllRooms();
} }
quickSettingsPressed() {
this.appState.accountMenu.closeAccountMenu();
if (this.themesWithIcons.length > 0) {
this.appState.quickSettingsMenu.toggle();
} else {
this.appState.preferences.openPreferences();
}
this.closeAllRooms();
}
toggleSyncResolutionMenu() { toggleSyncResolutionMenu() {
this.showSyncResolution = !this.showSyncResolution; this.showSyncResolution = !this.showSyncResolution;
} }
@@ -476,22 +489,7 @@ class FooterViewCtrl extends PureViewCtrl<
} }
reloadDockShortcuts() { reloadDockShortcuts() {
const shortcuts = []; const shortcuts: DockShortcut[] = [];
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);
}
this.setState({ this.setState({
dockShortcuts: shortcuts.sort((a, b) => { dockShortcuts: shortcuts.sort((a, b) => {
/** Circles first, then images */ /** Circles first, then images */
@@ -556,8 +554,8 @@ class FooterViewCtrl extends PureViewCtrl<
this.appState.accountMenu.closeAccountMenu(); this.appState.accountMenu.closeAccountMenu();
} }
clickPreferences() { clickOutsideQuickSettingsMenu() {
this.appState.preferences.openPreferences(); this.appState.quickSettingsMenu.closeQuickSettingsMenu();
} }
} }

View File

@@ -17,13 +17,20 @@
max-height: calc(85vh - 90px); max-height: calc(85vh - 90px);
} }
.sn-account-menu { .sn-account-menu,
.sn-quick-settings-menu {
z-index: $z-index-footer-bar-item-panel; z-index: $z-index-footer-bar-item-panel;
@extend .bottom-100; @extend .bottom-100;
@extend .left-0; @extend .left-0;
@extend .cursor-auto; @extend .cursor-auto;
} }
.sn-menu-border {
@extend .border-1;
@extend .border-solid;
@extend .border-gray-300;
}
.sn-account-menu-headline { .sn-account-menu-headline {
@extend .sk-h2; @extend .sk-h2;
@extend .sk-bold; @extend .sk-bold;

View File

@@ -384,3 +384,41 @@
.cursor-auto { .cursor-auto {
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);
}