Merge branch 'develop' into feature/account-menu-react
This commit is contained in:
@@ -13,41 +13,60 @@ import PasswordIcon from '../../icons/ic-textbox-password.svg';
|
||||
import TrashSweepIcon from '../../icons/ic-trash-sweep.svg';
|
||||
import MoreIcon from '../../icons/ic-more.svg';
|
||||
import TuneIcon from '../../icons/ic-tune.svg';
|
||||
|
||||
import AccessibilityIcon from '../../icons/ic-accessibility.svg';
|
||||
import HelpIcon from '../../icons/ic-help.svg';
|
||||
import KeyboardIcon from '../../icons/ic-keyboard.svg';
|
||||
import ListedIcon from '../../icons/ic-listed.svg';
|
||||
import SecurityIcon from '../../icons/ic-security.svg';
|
||||
import SettingsIcon from '../../icons/ic-settings.svg';
|
||||
import StarIcon from '../../icons/ic-star.svg';
|
||||
import ThemesIcon from '../../icons/ic-themes.svg';
|
||||
import UserIcon from '../../icons/ic-user.svg';
|
||||
|
||||
import { toDirective } from './utils';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
const ICONS = {
|
||||
'pencil-off': PencilOffIcon,
|
||||
'rich-text': RichTextIcon,
|
||||
'trash': TrashIcon,
|
||||
'pin': PinIcon,
|
||||
'unpin': UnpinIcon,
|
||||
'archive': ArchiveIcon,
|
||||
'unarchive': UnarchiveIcon,
|
||||
'hashtag': HashtagIcon,
|
||||
trash: TrashIcon,
|
||||
pin: PinIcon,
|
||||
unpin: UnpinIcon,
|
||||
archive: ArchiveIcon,
|
||||
unarchive: UnarchiveIcon,
|
||||
hashtag: HashtagIcon,
|
||||
'chevron-right': ChevronRightIcon,
|
||||
'restore': RestoreIcon,
|
||||
'close': CloseIcon,
|
||||
'password': PasswordIcon,
|
||||
restore: RestoreIcon,
|
||||
close: CloseIcon,
|
||||
password: PasswordIcon,
|
||||
'trash-sweep': TrashSweepIcon,
|
||||
'more': MoreIcon,
|
||||
'tune': TuneIcon,
|
||||
more: MoreIcon,
|
||||
tune: TuneIcon,
|
||||
accessibility: AccessibilityIcon,
|
||||
help: HelpIcon,
|
||||
keyboard: KeyboardIcon,
|
||||
listed: ListedIcon,
|
||||
security: SecurityIcon,
|
||||
settings: SettingsIcon,
|
||||
star: StarIcon,
|
||||
themes: ThemesIcon,
|
||||
user: UserIcon,
|
||||
};
|
||||
|
||||
export type IconType = keyof typeof ICONS;
|
||||
|
||||
type Props = {
|
||||
type: keyof (typeof ICONS);
|
||||
className: string;
|
||||
}
|
||||
type: IconType;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Icon: FunctionalComponent<Props> = ({ type, className }) => {
|
||||
const IconComponent = ICONS[type];
|
||||
return <IconComponent className={`sn-icon ${className}`} />;
|
||||
};
|
||||
|
||||
export const IconDirective = toDirective<Props>(
|
||||
Icon,
|
||||
{
|
||||
type: '@',
|
||||
className: '@',
|
||||
}
|
||||
);
|
||||
export const IconDirective = toDirective<Props>(Icon, {
|
||||
type: '@',
|
||||
className: '@',
|
||||
});
|
||||
|
||||
53
app/assets/javascripts/components/IconButton.tsx
Normal file
53
app/assets/javascripts/components/IconButton.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { Icon, IconType } from './Icon';
|
||||
|
||||
const ICON_BUTTON_TYPES: {
|
||||
[type: string]: { className: string };
|
||||
} = {
|
||||
normal: {
|
||||
className: '',
|
||||
},
|
||||
primary: {
|
||||
className: 'info',
|
||||
},
|
||||
};
|
||||
|
||||
export type IconButtonType = keyof typeof ICON_BUTTON_TYPES;
|
||||
|
||||
interface IconButtonProps {
|
||||
/**
|
||||
* onClick - preventDefault is handled within the component
|
||||
*/
|
||||
onClick: () => void;
|
||||
|
||||
type: IconButtonType;
|
||||
|
||||
className?: string;
|
||||
|
||||
iconType: IconType;
|
||||
}
|
||||
|
||||
/**
|
||||
* CircleButton component with an icon for SPA
|
||||
* preventDefault is already handled within the component
|
||||
*/
|
||||
export const IconButton: FunctionComponent<IconButtonProps> = ({
|
||||
onClick,
|
||||
type,
|
||||
className,
|
||||
iconType,
|
||||
}) => {
|
||||
const click = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
};
|
||||
const typeProps = ICON_BUTTON_TYPES[type];
|
||||
return (
|
||||
<button
|
||||
className={`sn-icon-button ${typeProps.className} ${className ?? ''}`}
|
||||
onClick={click}
|
||||
>
|
||||
<Icon type={iconType} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
23
app/assets/javascripts/components/PreferencesMenuItem.tsx
Normal file
23
app/assets/javascripts/components/PreferencesMenuItem.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Icon, IconType } from '@/components/Icon';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
interface PreferencesMenuItemProps {
|
||||
iconType: IconType;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const PreferencesMenuItem: FunctionComponent<PreferencesMenuItemProps> =
|
||||
({ iconType, label, selected, onClick }) => (
|
||||
<div
|
||||
className={`preferences-menu-item ${selected ? 'selected' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Icon className="icon" type={iconType} />
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
13
app/assets/javascripts/components/TitleBar.tsx
Normal file
13
app/assets/javascripts/components/TitleBar.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const TitleBar: FunctionComponent<{ className?: string }> = ({
|
||||
children,
|
||||
className,
|
||||
}) => <div className={`sn-titlebar ${className ?? ''}`}>{children}</div>;
|
||||
|
||||
export const Title: FunctionComponent<{ className?: string }> = ({
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return <div className={`sn-title ${className ?? ''}`}>{children}</div>;
|
||||
};
|
||||
28
app/assets/javascripts/components/preferences/content.tsx
Normal file
28
app/assets/javascripts/components/preferences/content.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
export const Title: FunctionalComponent = ({ children }) => (
|
||||
<h2 className="text-base m-0 mb-3">{children}</h2>
|
||||
);
|
||||
|
||||
export const Subtitle: FunctionalComponent = ({ children }) => (
|
||||
<h4 className="font-medium text-sm m-0 mb-1">{children}</h4>
|
||||
);
|
||||
|
||||
export const Text: FunctionalComponent = ({ children }) => (
|
||||
<p className="text-xs">{children}</p>
|
||||
);
|
||||
|
||||
export const Button: FunctionalComponent<{ label: string; link: string }> = ({
|
||||
label,
|
||||
link,
|
||||
}) => (
|
||||
<a
|
||||
target="_blank"
|
||||
className="block bg-default color-text rounded border-solid border-1
|
||||
border-gray-300 px-4 py-2 font-bold text-sm fit-content mt-3
|
||||
focus:bg-contrast hover:bg-contrast "
|
||||
href={link}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
@@ -0,0 +1,92 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { PreferencesGroup, PreferencesPane, PreferencesSegment } from './pane';
|
||||
import { Title, Subtitle, Text, Button } from './content';
|
||||
|
||||
export const HelpAndFeedback: FunctionalComponent = () => (
|
||||
<PreferencesPane>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Frequently asked questions</Title>
|
||||
<Subtitle>Who can read my private notes?</Subtitle>
|
||||
<Text>
|
||||
Quite simply: no one but you. Not us, not your ISP, not a hacker, and
|
||||
not a government agency. As long as you keep your password safe, and
|
||||
your password is reasonably strong, then you are the only person in
|
||||
the world with the ability to decrypt your notes. For more on how we
|
||||
handle your privacy and security, check out our easy to read{' '}
|
||||
<a target="_blank" href="https://standardnotes.com/privacy">
|
||||
Privacy Manifesto.
|
||||
</a>
|
||||
</Text>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
<Subtitle>Can I collaborate with others on a note?</Subtitle>
|
||||
<Text>
|
||||
Because of our encrypted architecture, Standard Notes does not
|
||||
currently provide a real-time collaboration solution. Multiple users
|
||||
can share the same account however, but editing at the same time may
|
||||
result in sync conflicts, which may result in the duplication of
|
||||
notes.
|
||||
</Text>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
<Subtitle>Can I use Standard Notes totally offline?</Subtitle>
|
||||
<Text>
|
||||
Standard Notes can be used totally offline without an account, and
|
||||
without an internet connection. You can find{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://standardnotes.com/help/59/can-i-use-standard-notes-totally-offline"
|
||||
>
|
||||
more details here.
|
||||
</a>
|
||||
</Text>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
<Subtitle>Can’t find your question here?</Subtitle>
|
||||
<Button label="Open FAQ" link="https://standardnotes.com/help" />
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Community forum</Title>
|
||||
<Text>
|
||||
If you have an issue, found a bug or want to suggest a feature, you
|
||||
can browse or post to the forum. It’s recommended for non-account
|
||||
related issues. Please read our{' '}
|
||||
<a target="_blank" href="https://standardnotes.com/longevity/">
|
||||
Longevity statement
|
||||
</a>{' '}
|
||||
before advocating for a feature request.
|
||||
</Text>
|
||||
<Button
|
||||
label="Go to the forum"
|
||||
link="https://forum.standardnotes.org/"
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Slack group</Title>
|
||||
<Text>
|
||||
Want to meet other passionate note-takers and privacy enthusiasts?
|
||||
Want to share your feedback with us? Join the Standard Notes Slack
|
||||
group for discussions on security, themes, editors and more.
|
||||
</Text>
|
||||
<Button
|
||||
link="https://standardnotes.com/slack"
|
||||
label="Join our Slack group"
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Account related issue?</Title>
|
||||
<Text>
|
||||
Send an email to help@standardnotes.org and we’ll sort it out.
|
||||
</Text>
|
||||
<Button link="mailto: help@standardnotes.org" label="Email us" />
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</PreferencesPane>
|
||||
);
|
||||
22
app/assets/javascripts/components/preferences/index.tsx
Normal file
22
app/assets/javascripts/components/preferences/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
import { toDirective } from '../utils';
|
||||
import { PreferencesView } from './view';
|
||||
|
||||
interface WrapperProps {
|
||||
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
|
||||
}
|
||||
|
||||
const PreferencesViewWrapper: FunctionComponent<WrapperProps> = observer(
|
||||
({ appState }) => {
|
||||
if (!appState.preferences.isOpen) return null;
|
||||
return (
|
||||
<PreferencesView close={() => appState.preferences.closePreferences()} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const PreferencesDirective = toDirective<WrapperProps>(
|
||||
PreferencesViewWrapper
|
||||
);
|
||||
23
app/assets/javascripts/components/preferences/menu.tsx
Normal file
23
app/assets/javascripts/components/preferences/menu.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PreferencesMenuItem } from '../PreferencesMenuItem';
|
||||
import { Preferences } from './preferences';
|
||||
|
||||
interface PreferencesMenuProps {
|
||||
preferences: Preferences;
|
||||
}
|
||||
|
||||
export const PreferencesMenu: FunctionComponent<PreferencesMenuProps> =
|
||||
observer(({ preferences }) => (
|
||||
<div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6">
|
||||
{preferences.items.map((pref) => (
|
||||
<PreferencesMenuItem
|
||||
key={pref.id}
|
||||
iconType={pref.icon}
|
||||
label={pref.label}
|
||||
selected={pref.selected}
|
||||
onClick={() => preferences.selectItem(pref.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
33
app/assets/javascripts/components/preferences/pane.tsx
Normal file
33
app/assets/javascripts/components/preferences/pane.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
const HorizontalLine: FunctionalComponent<{ index: number; length: number }> =
|
||||
({ index, length }) =>
|
||||
index < length - 1 ? (
|
||||
<hr className="h-1px w-full bg-border no-border" />
|
||||
) : null;
|
||||
|
||||
export const PreferencesSegment: FunctionalComponent = ({ children }) => (
|
||||
<div>{children}</div>
|
||||
);
|
||||
|
||||
export const PreferencesGroup: FunctionalComponent = ({ children }) => (
|
||||
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col gap-2">
|
||||
{!Array.isArray(children)
|
||||
? children
|
||||
: children.map((c, i, arr) => (
|
||||
<>
|
||||
{c}
|
||||
<HorizontalLine index={i} length={arr.length} />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const PreferencesPane: FunctionalComponent = ({ children }) => (
|
||||
<div className="preferences-pane flex-grow flex flex-row overflow-y-auto min-h-0">
|
||||
<div className="flex-grow flex flex-col py-6 items-center">
|
||||
<div className="max-w-124 flex flex-col gap-3">{children}</div>
|
||||
</div>
|
||||
<div className="flex-basis-55 flex-shrink-max" />
|
||||
</div>
|
||||
);
|
||||
55
app/assets/javascripts/components/preferences/preferences.ts
Normal file
55
app/assets/javascripts/components/preferences/preferences.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { IconType } from '@/components/Icon';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
|
||||
interface PreferenceItem {
|
||||
icon: IconType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface PreferenceListItem extends PreferenceItem {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const predefinedItems: PreferenceItem[] = [
|
||||
{ label: 'General', icon: 'settings' },
|
||||
{ label: 'Account', icon: 'user' },
|
||||
{ label: 'Appearance', icon: 'themes' },
|
||||
{ label: 'Security', icon: 'security' },
|
||||
{ label: 'Listed', icon: 'listed' },
|
||||
{ label: 'Shortcuts', icon: 'keyboard' },
|
||||
{ label: 'Accessibility', icon: 'accessibility' },
|
||||
{ label: 'Get a free month', icon: 'star' },
|
||||
{ label: 'Help & feedback', icon: 'help' },
|
||||
];
|
||||
|
||||
export class Preferences {
|
||||
private readonly _items: PreferenceListItem[];
|
||||
private _selectedId = 0;
|
||||
|
||||
constructor(items: PreferenceItem[] = predefinedItems) {
|
||||
makeObservable<Preferences, '_selectedId'>(this, {
|
||||
_selectedId: observable,
|
||||
selectedItem: computed,
|
||||
items: computed,
|
||||
selectItem: action,
|
||||
});
|
||||
|
||||
this._items = items.map((p, idx) => ({ ...p, id: idx }));
|
||||
this._selectedId = this._items[0].id;
|
||||
}
|
||||
|
||||
selectItem(id: number) {
|
||||
this._selectedId = id;
|
||||
}
|
||||
|
||||
get items(): (PreferenceListItem & { selected: boolean })[] {
|
||||
return this._items.map((p) => ({
|
||||
...p,
|
||||
selected: p.id === this._selectedId,
|
||||
}));
|
||||
}
|
||||
|
||||
get selectedItem(): PreferenceListItem {
|
||||
return this._items.find((item) => item.id === this._selectedId)!;
|
||||
}
|
||||
}
|
||||
45
app/assets/javascripts/components/preferences/view.tsx
Normal file
45
app/assets/javascripts/components/preferences/view.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
import { TitleBar, Title } from '@/components/TitleBar';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { Preferences } from './preferences';
|
||||
import { PreferencesMenu } from './menu';
|
||||
import { HelpAndFeedback } from './help-feedback';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface PreferencesViewProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export const PreferencesCanvas: FunctionComponent<{
|
||||
preferences: Preferences;
|
||||
}> = observer(({ preferences: prefs }) => (
|
||||
<div className="flex flex-row flex-grow min-h-0 justify-between">
|
||||
<PreferencesMenu preferences={prefs}></PreferencesMenu>
|
||||
{/* Temporary selector until a full solution is implemented */}
|
||||
{prefs.selectedItem.label === 'Help & feedback' ? (
|
||||
<HelpAndFeedback />
|
||||
) : null}
|
||||
</div>
|
||||
));
|
||||
|
||||
export const PreferencesView: FunctionComponent<PreferencesViewProps> =
|
||||
observer(({ close }) => {
|
||||
const prefs = new Preferences();
|
||||
return (
|
||||
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
|
||||
<TitleBar className="items-center justify-between">
|
||||
{/* div is added so flex justify-between can center the title */}
|
||||
<div className="h-8 w-8" />
|
||||
<Title className="text-lg">Your preferences for Standard Notes</Title>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
type="normal"
|
||||
iconType="close"
|
||||
/>
|
||||
</TitleBar>
|
||||
<PreferencesCanvas preferences={prefs} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user