From 7dd4a605952dc50f7c183ada225ae36808334c3b Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Tue, 4 Jan 2022 17:29:10 +0530 Subject: [PATCH] feat: New notes list design (#780) --- app/assets/icons/ic-authenticator.svg | 7 +- app/assets/icons/ic-code.svg | 7 +- app/assets/icons/ic-lock-filled.svg | 4 + app/assets/icons/ic-markdown.svg | 7 +- app/assets/icons/ic-pin-filled.svg | 3 + app/assets/icons/ic-spreadsheets.svg | 7 +- app/assets/icons/ic-tasks.svg | 7 +- app/assets/icons/ic-text-paragraph.svg | 7 +- app/assets/icons/ic-text-rich.svg | 5 +- app/assets/icons/ic-trash-filled.svg | 4 + .../javascripts/components/Dropdown.tsx | 18 +- app/assets/javascripts/components/Icon.tsx | 21 +- .../javascripts/components/NotesList.tsx | 32 ++-- .../javascripts/components/NotesListItem.tsx | 179 +++++++++++------- .../javascripts/components/NotesView.tsx | 1 + .../panes/general-segments/Defaults.tsx | 27 +-- app/assets/stylesheets/_notes.scss | 99 +++++++--- app/assets/stylesheets/_sn.scss | 6 + package.json | 2 +- yarn.lock | 8 +- 20 files changed, 296 insertions(+), 155 deletions(-) create mode 100644 app/assets/icons/ic-lock-filled.svg create mode 100644 app/assets/icons/ic-pin-filled.svg create mode 100644 app/assets/icons/ic-trash-filled.svg diff --git a/app/assets/icons/ic-authenticator.svg b/app/assets/icons/ic-authenticator.svg index 9a119391933..e8dd720cfe9 100644 --- a/app/assets/icons/ic-authenticator.svg +++ b/app/assets/icons/ic-authenticator.svg @@ -1,3 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/app/assets/icons/ic-code.svg b/app/assets/icons/ic-code.svg index 4a871e270e9..79df4be8db6 100644 --- a/app/assets/icons/ic-code.svg +++ b/app/assets/icons/ic-code.svg @@ -1,3 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/app/assets/icons/ic-lock-filled.svg b/app/assets/icons/ic-lock-filled.svg new file mode 100644 index 00000000000..a71db279476 --- /dev/null +++ b/app/assets/icons/ic-lock-filled.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/app/assets/icons/ic-markdown.svg b/app/assets/icons/ic-markdown.svg index bceed54b39f..1efac587636 100644 --- a/app/assets/icons/ic-markdown.svg +++ b/app/assets/icons/ic-markdown.svg @@ -1,3 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/app/assets/icons/ic-pin-filled.svg b/app/assets/icons/ic-pin-filled.svg new file mode 100644 index 00000000000..4e5ae92a5e6 --- /dev/null +++ b/app/assets/icons/ic-pin-filled.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/assets/icons/ic-spreadsheets.svg b/app/assets/icons/ic-spreadsheets.svg index 70f175be23d..2566d69bb99 100644 --- a/app/assets/icons/ic-spreadsheets.svg +++ b/app/assets/icons/ic-spreadsheets.svg @@ -1,3 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/app/assets/icons/ic-tasks.svg b/app/assets/icons/ic-tasks.svg index 0f8ef05875c..c6b89554fc1 100644 --- a/app/assets/icons/ic-tasks.svg +++ b/app/assets/icons/ic-tasks.svg @@ -1,3 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/app/assets/icons/ic-text-paragraph.svg b/app/assets/icons/ic-text-paragraph.svg index 4f43cdc0c92..376e8ad4648 100644 --- a/app/assets/icons/ic-text-paragraph.svg +++ b/app/assets/icons/ic-text-paragraph.svg @@ -1,4 +1,3 @@ - - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/assets/icons/ic-text-rich.svg b/app/assets/icons/ic-text-rich.svg index 87f57dd41fd..d895ca8c47c 100644 --- a/app/assets/icons/ic-text-rich.svg +++ b/app/assets/icons/ic-text-rich.svg @@ -1,3 +1,4 @@ - - + + \ No newline at end of file diff --git a/app/assets/icons/ic-trash-filled.svg b/app/assets/icons/ic-trash-filled.svg new file mode 100644 index 00000000000..63f9575bf2e --- /dev/null +++ b/app/assets/icons/ic-trash-filled.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx index e06994baece..5ec2e293284 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -13,6 +13,7 @@ import { useState } from 'preact/hooks'; export type DropdownItem = { icon?: IconType; + iconClassName?: string; label: string; value: string; }; @@ -25,10 +26,7 @@ type DropdownProps = { onChange: (value: string) => void; }; -type ListboxButtonProps = { - icon?: IconType; - value: string | null; - label: string; +type ListboxButtonProps = DropdownItem & { isExpanded: boolean; }; @@ -36,12 +34,13 @@ const CustomDropdownButton: FunctionComponent = ({ label, isExpanded, icon, + iconClassName = '', }) => ( <>
{icon ? (
- +
) : null}
{label}
@@ -85,11 +84,13 @@ export const Dropdown: FunctionComponent = ({ children={({ value, label, isExpanded }) => { const current = items.find((item) => item.value === value); const icon = current ? current?.icon : null; + const iconClassName = current ? current?.iconClassName : null; return CustomDropdownButton({ - value, + value: value ? value : label.toLowerCase(), label, isExpanded, ...(icon ? { icon } : null), + ...(iconClassName ? { iconClassName } : null), }); }} /> @@ -104,7 +105,10 @@ export const Dropdown: FunctionComponent = ({ > {item.icon ? (
- +
) : null}
{item.label}
diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index 96eee992f8e..bb82794cf1f 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -3,7 +3,9 @@ import PencilOffIcon from '../../icons/ic-pencil-off.svg'; import PlainTextIcon from '../../icons/ic-text-paragraph.svg'; import RichTextIcon from '../../icons/ic-text-rich.svg'; import TrashIcon from '../../icons/ic-trash.svg'; +import TrashFilledIcon from '../../icons/ic-trash-filled.svg'; import PinIcon from '../../icons/ic-pin.svg'; +import PinFilledIcon from '../../icons/ic-pin-filled.svg'; import UnpinIcon from '../../icons/ic-pin-off.svg'; import ArchiveIcon from '../../icons/ic-archive.svg'; import UnarchiveIcon from '../../icons/ic-unarchive.svg'; @@ -52,6 +54,7 @@ import ServerIcon from '../../icons/ic-server.svg'; import EyeIcon from '../../icons/ic-eye.svg'; import EyeOffIcon from '../../icons/ic-eye-off.svg'; import LockIcon from '../../icons/ic-lock.svg'; +import LockFilledIcon from '../../icons/ic-lock-filled.svg'; import ArrowsSortUpIcon from '../../icons/ic-arrows-sort-up.svg'; import ArrowsSortDownIcon from '../../icons/ic-arrows-sort-down.svg'; import WindowIcon from '../../icons/ic-window.svg'; @@ -69,6 +72,7 @@ const ICONS = { 'arrows-sort-up': ArrowsSortUpIcon, 'arrows-sort-down': ArrowsSortDownIcon, lock: LockIcon, + 'lock-filled': LockFilledIcon, eye: EyeIcon, 'eye-off': EyeOffIcon, server: ServerIcon, @@ -89,7 +93,9 @@ const ICONS = { spreadsheets: SpreadsheetsIcon, tasks: TasksIcon, trash: TrashIcon, + 'trash-filled': TrashFilledIcon, pin: PinIcon, + 'pin-filled': PinFilledIcon, unpin: UnpinIcon, archive: ArchiveIcon, unarchive: UnarchiveIcon, @@ -130,11 +136,22 @@ export type IconType = keyof typeof ICONS; type Props = { type: IconType; className?: string; + ariaLabel?: string; }; -export const Icon: FunctionalComponent = ({ type, className = '' }) => { +export const Icon: FunctionalComponent = ({ + type, + className = '', + ariaLabel, +}) => { const IconComponent = ICONS[type]; - return ; + return ( + + ); }; export const IconDirective = toDirective(Icon, { diff --git a/app/assets/javascripts/components/NotesList.tsx b/app/assets/javascripts/components/NotesList.tsx index e443393aa56..4b505bb581b 100644 --- a/app/assets/javascripts/components/NotesList.tsx +++ b/app/assets/javascripts/components/NotesList.tsx @@ -1,3 +1,4 @@ +import { WebApplication } from '@/ui_models/application'; import { KeyboardKey } from '@/services/ioService'; import { AppState } from '@/ui_models/app_state'; import { DisplayOptions } from '@/ui_models/app_state/notes_view_state'; @@ -7,6 +8,7 @@ import { FunctionComponent } from 'preact'; import { NotesListItem } from './NotesListItem'; type Props = { + application: WebApplication; appState: AppState; notes: SNNote[]; selectedNotes: Record; @@ -18,23 +20,30 @@ const FOCUSABLE_BUT_NOT_TABBABLE = -1; const NOTES_LIST_SCROLL_THRESHOLD = 200; export const NotesList: FunctionComponent = observer( - ({ appState, notes, selectedNotes, displayOptions, paginate }) => { + ({ + application, + appState, + notes, + selectedNotes, + displayOptions, + paginate, + }) => { const { selectPreviousNote, selectNextNote } = appState.notesView; const { hideTags, hideDate, hideNotePreview, sortBy } = displayOptions; - const tagsStringForNote = (note: SNNote): string => { + const tagsForNote = (note: SNNote): string[] => { if (hideTags) { - return ''; + return []; } const selectedTag = appState.selectedTag; if (!selectedTag) { - return ''; + return []; } const tags = appState.getNoteTags(note); if (!selectedTag.isSmartTag && tags.length === 1) { - return ''; + return []; } - return tags.map((tag) => `#${tag.title}`).join(' '); + return tags.map((tag) => tag.title); }; const openNoteContextMenu = (posX: number, posY: number) => { @@ -46,11 +55,9 @@ export const NotesList: FunctionComponent = observer( appState.notes.setContextMenuOpen(true); }; - const onContextMenu = async (note: SNNote, posX: number, posY: number) => { - await appState.notes.selectNote(note.uuid, true); - if (selectedNotes[note.uuid]) { - openNoteContextMenu(posX, posY); - } + const onContextMenu = (note: SNNote, posX: number, posY: number) => { + appState.notes.selectNote(note.uuid, true); + openNoteContextMenu(posX, posY); }; const onScroll = (e: Event) => { @@ -84,9 +91,10 @@ export const NotesList: FunctionComponent = observer( > {notes.map((note) => ( { const flags = [] as NoteFlag[]; - if (note.pinned) { - flags.push({ - text: 'Pinned', - class: 'info', - }); - } - if (note.archived) { - flags.push({ - text: 'Archived', - class: 'warning', - }); - } - if (note.locked) { - flags.push({ - text: 'Editing Disabled', - class: 'neutral', - }); - } - if (note.trashed) { - flags.push({ - text: 'Deleted', - class: 'danger', - }); - } if (note.conflictOf) { flags.push({ text: 'Conflicted Copy', @@ -77,6 +57,7 @@ const flagsForNote = (note: SNNote) => { }; export const NotesListItem: FunctionComponent = ({ + application, hideDate, hidePreview, hideTags, @@ -89,6 +70,9 @@ export const NotesListItem: FunctionComponent = ({ }) => { const flags = flagsForNote(note); const showModifiedDate = sortedBy === CollectionSort.UpdatedAt; + const editorForNote = application.componentManager.editorForNote(note); + const editorName = editorForNote?.name ?? 'Plain editor'; + const [icon, tint] = getIconAndTintForEditor(editorForNote?.identifier); return (
= ({ onClick={onClick} onContextMenu={onContextMenu} > - {flags && flags.length > 0 ? ( -
- {flags.map((flag) => ( -
-
{flag.text}
-
- ))} -
- ) : null} -
{note.title}
- {!hidePreview && !note.hidePreview && !note.protected ? ( -
- {note.preview_html ? ( -
- ) : null} - {!note.preview_html && note.preview_plain ? ( -
{note.preview_plain}
- ) : null} - {!note.preview_html && !note.preview_plain ? ( -
{note.text}
- ) : null} -
- ) : null} - {!hideDate || note.protected ? ( -
- {note.protected ? ( - Protected {hideDate ? '' : ' • '} - ) : null} - {!hideDate && showModifiedDate ? ( - Modified {note.updatedAtString || 'Now'} - ) : null} - {!hideDate && !showModifiedDate ? ( - {note.createdAtString || 'Now'} - ) : null} -
- ) : null} - {!hideTags && ( -
-
{tags}
+
+ +
+
+
+
{note.title}
+
+ {note.locked && ( + + + + )} + {note.trashed && ( + + + + )} + {note.archived && ( + + + + )} + {note.pinned && ( + + + + )} +
- )} + {!hidePreview && !note.hidePreview && !note.protected && ( +
+ {note.preview_html && ( +
+ )} + {!note.preview_html && note.preview_plain && ( +
{note.preview_plain}
+ )} + {!note.preview_html && !note.preview_plain && note.text && ( +
{note.text}
+ )} +
+ )} + {!hideDate || note.protected ? ( +
+ {note.protected && Protected {hideDate ? '' : ' • '}} + {!hideDate && showModifiedDate && ( + Modified {note.updatedAtString || 'Now'} + )} + {!hideDate && !showModifiedDate && ( + {note.createdAtString || 'Now'} + )} +
+ ) : null} + {!hideTags && tags.length ? ( +
+ {tags.map((tag) => ( + + + {tag} + + ))} +
+ ) : null} + {flags.length ? ( +
+ {flags.map((flag) => ( +
+
{flag.text}
+
+ ))} +
+ ) : null} +
); }; diff --git a/app/assets/javascripts/components/NotesView.tsx b/app/assets/javascripts/components/NotesView.tsx index 004a54e1dfd..6c1b8e0df96 100644 --- a/app/assets/javascripts/components/NotesView.tsx +++ b/app/assets/javascripts/components/NotesView.tsx @@ -230,6 +230,7 @@ const NotesView: FunctionComponent = observer( { +export const getIconAndTintForEditor = ( + identifier: FeatureIdentifier | undefined +): [IconType, number] => { switch (identifier) { case FeatureIdentifier.BoldEditor: case FeatureIdentifier.PlusEditor: - return 'rich-text'; + return ['rich-text', 1]; case FeatureIdentifier.MarkdownBasicEditor: case FeatureIdentifier.MarkdownMathEditor: case FeatureIdentifier.MarkdownMinimistEditor: case FeatureIdentifier.MarkdownProEditor: - return 'markdown'; + return ['markdown', 2]; case FeatureIdentifier.TokenVaultEditor: - return 'authenticator'; + return ['authenticator', 6]; case FeatureIdentifier.SheetsEditor: - return 'spreadsheets'; + return ['spreadsheets', 5]; case FeatureIdentifier.TaskEditor: - return 'tasks'; + return ['tasks', 3]; case FeatureIdentifier.CodeEditor: - return 'code'; + return ['code', 4]; + default: + return ['plain-text', 1]; } - return null; }; const makeEditorDefault = ( @@ -91,17 +92,19 @@ export const Defaults: FunctionComponent = ({ application }) => { .componentsForArea(ComponentArea.Editor) .map((editor): EditorOption => { const identifier = editor.package_info.identifier; - const iconType = getEditorIconType(identifier); + const [iconType, tint] = getIconAndTintForEditor(identifier); return { label: editor.name, value: identifier, ...(iconType ? { icon: iconType } : null), + ...(tint ? { iconClassName: `color-accessory-tint-${tint}` } : null), }; }) .concat([ { icon: 'plain-text', + iconClassName: `color-accessory-tint-1`, label: 'Plain Editor', value: 'plain-editor', }, diff --git a/app/assets/stylesheets/_notes.scss b/app/assets/stylesheets/_notes.scss index 6ec93780457..cf4a3a36542 100644 --- a/app/assets/stylesheets/_notes.scss +++ b/app/assets/stylesheets/_notes.scss @@ -123,42 +123,95 @@ notes-view { } .note { + display: flex; + align-items: stretch; + width: 100%; - padding: 15px; - border-bottom: 1px solid var(--sn-stylekit-border-color); cursor: pointer; - > .name { - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; + &:hover { + background-color: var(--sn-stylekit-grey-5); } - > .bottom-info { - font-size: 12px; - margin-top: 4px; + .icon { + display: flex; + flex-flow: column; + align-items: center; + justify-content: space-between; + padding: 0.9rem; + padding-right: 0.75rem; + margin-right: 0; + } + + .meta { + flex-grow: 1; + min-width: 0; + padding: 0.9rem; + padding-left: 0; + border-bottom: 1px solid var(--sn-stylekit-border-color); + + .name { + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 600; + font-size: 1rem; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + } + + .flag-icons { + &, + & > * { + display: flex; + align-items: center; + } + + & > * + * { + margin-left: 0.375rem; + } + } + + .bottom-info { + font-size: 12px; + line-height: 1.4; + margin-top: 0.25rem; + } } .tags-string { - margin-top: 4px; - font-size: 12px; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.345rem; + font-size: 0.725rem; + + .tag { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.375rem 0.25rem 0.325rem; + background-color: var(--sn-stylekit-grey-4-opacity-variant); + border-radius: 0.125rem; + } } .note-preview { font-size: var(--sn-stylekit-font-size-h3); - margin-top: 2px; - overflow: hidden; text-overflow: ellipsis; + & > * { + margin-top: 0.15rem; + } + .default-preview, .plain-preview { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; /* number of lines to show */ - $line-height: 18px; - line-height: $line-height; /* fallback */ - max-height: calc(#{$line-height} * 1); /* fallback */ + line-height: 1.3; + overflow: hidden; } .html-preview { @@ -175,8 +228,7 @@ notes-view { display: flex; flex-direction: row; align-items: center; - margin-bottom: 8px; - margin-top: -4px; + margin-top: 0.125rem; .flag { padding: 4px; @@ -238,13 +290,8 @@ notes-view { } &.selected { - background-color: var(--sn-stylekit-info-color); - color: var(--sn-stylekit-info-contrast-color); - - .note-flags .flag { - background-color: var(--sn-stylekit-info-contrast-color); - color: var(--sn-stylekit-info-color); - } + background-color: var(--sn-stylekit-grey-5); + border-left: 2px solid var(--sn-stylekit-info-color); progress { background-color: var(--sn-stylekit-secondary-foreground-color); @@ -255,7 +302,7 @@ notes-view { } &::-webkit-progress-value { - background-color: var(--sn-stylekit-secondary-background-color); + background-color: var(--sn-stylekit-info-color); } &::-moz-progress-bar { diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 5ba5161c0b7..48198c039ff 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -40,6 +40,11 @@ @extend .h-3\.5; @extend .w-3\.5; } + + &.sn-icon--mid { + @extend .w-4; + @extend .h-4; + } } .sn-dropdown { @@ -777,6 +782,7 @@ } &:hover { + background-color: var(--sn-stylekit-contrast-background-color) !important; @extend .color-info; @extend .border-info; } diff --git a/package.json b/package.json index 087a79eed8d..fef47586fc0 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "pug-loader": "^2.4.0", "sass-loader": "^12.2.0", "serve-static": "^1.14.1", - "sn-stylekit": "5.2.20", + "sn-stylekit": "5.2.21", "svg-jest": "^1.0.1", "ts-jest": "^27.0.7", "ts-loader": "^9.2.6", diff --git a/yarn.lock b/yarn.lock index 08588f4fb97..3a4040c304c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9264,10 +9264,10 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" -sn-stylekit@5.2.20: - version "5.2.20" - resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.20.tgz#c18f40ff3aaf4c59af89152439a8efbdde35f2dd" - integrity sha512-JymHBiZOzQPfCqHYgnVPSA2PwJqiKR268qqQoEMqI85MMAWSG3WYzuKEbd0LgfIQAKLElCxJjeZkrhejyRg+2A== +sn-stylekit@5.2.21: + version "5.2.21" + resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.21.tgz#5aec6c329949bda64a1e3c563ee594b141295d27" + integrity sha512-rjlgo42A/kx+M4iY7HYRpnQyp4dLb2HQpEMHz+CYumOzTf/lsRy0Up5HI1haNK4/JMmpq36Eb/7BMDmvLpdXnQ== dependencies: "@reach/listbox" "^0.15.0" "@reach/menu-button" "^0.15.1"