chore: make mobile menu ui more native-like (#2594)

This commit is contained in:
Aman Harwara
2023-10-18 21:31:28 +05:30
committed by GitHub
parent 6a40f611f6
commit 2e3ac3ce57
32 changed files with 615 additions and 584 deletions

View File

@@ -77,7 +77,7 @@ const AddTagOption: FunctionComponent<Props> = ({
className="py-2"
overrideZIndex="z-modal"
>
<Menu a11yLabel="Tag selection menu" isOpen={isOpen}>
<Menu a11yLabel="Tag selection menu" isOpen={isOpen} className="!px-0">
{navigationController.tags.map((tag) => (
<MenuItem
key={tag.uuid}

View File

@@ -60,7 +60,7 @@ const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
title="Change note type"
align="start"
anchorElement={buttonRef}
className="pt-2 md:pt-0"
className="md:pb-1"
open={isOpen}
side="right"
togglePopover={toggleMenu}

View File

@@ -1,10 +1,11 @@
import { WebApplication } from '@/Application/WebApplication'
import { Action, SNNote } from '@standardnotes/snjs'
import { Fragment, useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import { ListedMenuGroup } from './ListedMenuGroup'
import ListedMenuItem from './ListedMenuItem'
import Spinner from '@/Components/Spinner/Spinner'
import MenuSection from '@/Components/Menu/MenuSection'
type ListedActionsMenuProps = {
application: WebApplication
@@ -125,15 +126,15 @@ const ListedActionsMenu = ({ application, note }: ListedActionsMenuProps) => {
)}
{!isFetchingAccounts && menuGroups.length ? (
<>
{menuGroups.map((group, index) => (
<Fragment key={group.account.authorId}>
<div
className={`text-input flex w-full items-center border-y border-solid border-border px-2.5 py-2 font-semibold text-text ${
index === 0 ? 'mb-1 border-t-0' : 'my-1'
}`}
>
<Icon type="notes" className="mr-2 text-info" /> {group.name}
</div>
{menuGroups.map((group) => (
<MenuSection
key={group.account.authorId}
title={
<div className="flex items-center">
<Icon type="notes" className="mr-2 text-info" /> {group.name}
</div>
}
>
{group.actions.length ? (
group.actions.map((action) => (
<ListedMenuItem
@@ -148,7 +149,7 @@ const ListedActionsMenu = ({ application, note }: ListedActionsMenuProps) => {
) : (
<div className="select-none px-3 py-2 text-sm text-passive-0">No actions available</div>
)}
</Fragment>
</MenuSection>
))}
</>
) : null}

View File

@@ -54,7 +54,7 @@ const ListedActionsOption: FunctionComponent<Props> = ({ application, note, icon
open={isOpen}
side="right"
align="end"
className="pt-2 md:pt-0"
className="px-4 md:px-0 md:pt-0"
>
<ListedActionsMenu application={application} note={note} />
</Popover>

View File

@@ -18,7 +18,6 @@ import ListedActionsOption from './Listed/ListedActionsOption'
import AddTagOption from './AddTagOption'
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
import { NotesOptionsProps } from './NotesOptionsProps'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { AppPaneId } from '../Panes/AppPaneMetadata'
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
@@ -42,6 +41,7 @@ import { MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption'
import Menu from '../Menu/Menu'
import Popover from '../Popover/Popover'
import MenuSection from '../Menu/MenuSection'
const iconSize = MenuItemIconSize
const iconClassDanger = `text-danger mr-2 ${iconSize}`
@@ -230,271 +230,282 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
<>
{notes.length === 1 && (
<>
<MenuItem onClick={openRevisionHistoryModal}>
<Icon type="history" className={iconClass} />
Note history
{historyShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={historyShortcut} />}
</MenuItem>
<HorizontalSeparator classes="my-2" />
<MenuItem onClick={toggleLineWidthModal} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="line-width" className={iconClass} />
Editor width
{editorWidthShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={editorWidthShortcut} />}
</MenuItem>
<MenuSection>
<MenuItem onClick={openRevisionHistoryModal}>
<Icon type="history" className={iconClass} />
Note history
{historyShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={historyShortcut} />}
</MenuItem>
</MenuSection>
<MenuSection>
<MenuItem onClick={toggleLineWidthModal} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="line-width" className={iconClass} />
Editor width
{editorWidthShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={editorWidthShortcut} />}
</MenuItem>
</MenuSection>
</>
)}
<MenuSwitchButtonItem
checked={locked}
onChange={(locked) => {
application.notesController.setLockSelectedNotes(locked)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="pencil-off" className={iconClass} />
Prevent editing
</MenuSwitchButtonItem>
<MenuSwitchButtonItem
checked={!hidePreviews}
onChange={(hidePreviews) => {
application.notesController.setHideSelectedNotePreviews(!hidePreviews)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="rich-text" className={iconClass} />
Show preview
</MenuSwitchButtonItem>
<MenuSwitchButtonItem
checked={protect}
onChange={(protect) => {
application.notesController.setProtectSelectedNotes(protect).catch(console.error)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="lock" className={iconClass} />
Password protect
</MenuSwitchButtonItem>
<MenuSection>
<MenuSwitchButtonItem
checked={locked}
onChange={(locked) => {
application.notesController.setLockSelectedNotes(locked)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="pencil-off" className={iconClass} />
Prevent editing
</MenuSwitchButtonItem>
<MenuSwitchButtonItem
checked={!hidePreviews}
onChange={(hidePreviews) => {
application.notesController.setHideSelectedNotePreviews(!hidePreviews)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="rich-text" className={iconClass} />
Show preview
</MenuSwitchButtonItem>
<MenuSwitchButtonItem
checked={protect}
onChange={(protect) => {
application.notesController.setProtectSelectedNotes(protect).catch(console.error)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="lock" className={iconClass} />
Password protect
</MenuSwitchButtonItem>
</MenuSection>
{notes.length === 1 && (
<>
<HorizontalSeparator classes="my-2" />
<MenuSection>
<ChangeEditorOption
iconClassName={iconClass}
application={application}
note={notes[0]}
disabled={areSomeNotesInReadonlySharedVault}
/>
</>
)}
<HorizontalSeparator classes="my-2" />
{application.featuresController.isVaultsEnabled() && (
<AddToVaultMenuOption iconClassName={iconClass} items={notes} disabled={!hasAdminPermissionForAllSharedNotes} />
</MenuSection>
)}
{application.navigationController.tagsCount > 0 && (
<AddTagOption
iconClassName={iconClass}
navigationController={application.navigationController}
selectedItems={notes}
linkingController={application.linkingController}
disabled={areSomeNotesInReadonlySharedVault}
/>
)}
<MenuItem
onClick={() => {
application.notesController.setStarSelectedNotes(!starred)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="star" className={iconClass} />
{starred ? 'Unstar' : 'Star'}
{starShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={starShortcut} />}
</MenuItem>
<MenuSection>
{application.featuresController.isVaultsEnabled() && (
<AddToVaultMenuOption
iconClassName={iconClass}
items={notes}
disabled={!hasAdminPermissionForAllSharedNotes}
/>
)}
{unpinned && (
{application.navigationController.tagsCount > 0 && (
<AddTagOption
iconClassName={iconClass}
navigationController={application.navigationController}
selectedItems={notes}
linkingController={application.linkingController}
disabled={areSomeNotesInReadonlySharedVault}
/>
)}
<MenuItem
onClick={() => {
application.notesController.setPinSelectedNotes(true)
application.notesController.setStarSelectedNotes(!starred)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="pin" className={iconClass} />
Pin to top
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
<Icon type="star" className={iconClass} />
{starred ? 'Unstar' : 'Star'}
{starShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={starShortcut} />}
</MenuItem>
)}
{pinned && (
<MenuItem
onClick={() => {
application.notesController.setPinSelectedNotes(false)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="unpin" className={iconClass} />
Unpin
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
</MenuItem>
)}
{isOnlySuperNoteSelected ? (
<>
<MenuItem
ref={superExportButtonRef}
onClick={() => {
setIsSuperExportMenuOpen((open) => !open)
}}
>
<div className="flex items-center">
<Icon type="download" className={iconClass} />
Export
</div>
<Icon type="chevron-right" className="ml-auto text-neutral" />
</MenuItem>
<Popover
title="Export note"
side="left"
align="start"
open={isSuperExportMenuOpen}
anchorElement={superExportButtonRef.current}
togglePopover={() => {
setIsSuperExportMenuOpen(!isSuperExportMenuOpen)
}}
className="py-1"
>
<Menu a11yLabel={'Super note export menu'} isOpen={isSuperExportMenuOpen}>
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_JSON, notes[0].title)}>
<Icon type="code" className={iconClass} />
Export as JSON
</MenuItem>
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_MARKDOWN, notes[0].title)}>
<Icon type="markdown" className={iconClass} />
Export as Markdown
</MenuItem>
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_HTML, notes[0].title)}>
<Icon type="rich-text" className={iconClass} />
Export as HTML
</MenuItem>
</Menu>
</Popover>
</>
) : (
<>
{unpinned && (
<MenuItem
onClick={() => {
if (application.isNativeMobileWeb()) {
void shareSelectedNotes(application, notes)
} else {
const hasSuperNote = notes.some((note) => note.noteType === NoteType.Super)
if (hasSuperNote) {
setShowExportSuperModal(true)
return
}
void downloadSelectedItems()
}
}}
>
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
{application.platform === Platform.Android ? 'Share' : 'Export'}
</MenuItem>
{application.platform === Platform.Android && (
<MenuItem onClick={() => downloadSelectedNotesOnAndroid(application, notes)}>
<Icon type="download" className={iconClass} />
Export
</MenuItem>
)}
</>
)}
<MenuItem onClick={duplicateSelectedItems} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="copy" className={iconClass} />
Duplicate
</MenuItem>
{unarchived && (
<MenuItem
onClick={async () => {
await application.notesController.setArchiveSelectedNotes(true).catch(console.error)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="archive" className={iconClassWarning} />
<span className="text-warning">Archive</span>
</MenuItem>
)}
{archived && (
<MenuItem
onClick={async () => {
await application.notesController.setArchiveSelectedNotes(false).catch(console.error)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="unarchive" className={iconClassWarning} />
<span className="text-warning">Unarchive</span>
</MenuItem>
)}
{notTrashed &&
(altKeyDown ? (
<MenuItem
disabled={areSomeNotesInReadonlySharedVault}
onClick={async () => {
await application.notesController.deleteNotesPermanently()
closeMenuAndToggleNotesList()
}}
>
<Icon type="close" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</MenuItem>
) : (
<MenuItem
onClick={async () => {
await application.notesController.setTrashSelectedNotes(true)
closeMenuAndToggleNotesList()
application.notesController.setPinSelectedNotes(true)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="trash" className={iconClassDanger} />
<span className="text-danger">Move to trash</span>
<Icon type="pin" className={iconClass} />
Pin to top
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
</MenuItem>
))}
{trashed && (
<>
)}
{pinned && (
<MenuItem
onClick={async () => {
await application.notesController.setTrashSelectedNotes(false)
closeMenuAndToggleNotesList()
onClick={() => {
application.notesController.setPinSelectedNotes(false)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="restore" className={iconClassSuccess} />
<span className="text-success">Restore</span>
<Icon type="unpin" className={iconClass} />
Unpin
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
</MenuItem>
<MenuItem
disabled={areSomeNotesInReadonlySharedVault}
onClick={async () => {
await application.notesController.deleteNotesPermanently()
closeMenuAndToggleNotesList()
}}
>
<Icon type="close" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</MenuItem>
<MenuItem
onClick={async () => {
await application.notesController.emptyTrash()
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<div className="flex items-start">
<Icon type="trash-sweep" className="mr-2 text-danger" />
<div className="flex-row">
<div className="text-danger">Empty Trash</div>
<div className="text-xs">{application.notesController.trashedNotesCount} notes in Trash</div>
)}
{isOnlySuperNoteSelected ? (
<>
<MenuItem
ref={superExportButtonRef}
onClick={() => {
setIsSuperExportMenuOpen((open) => !open)
}}
>
<div className="flex items-center">
<Icon type="download" className={iconClass} />
Export
</div>
</div>
<Icon type="chevron-right" className="ml-auto text-neutral" />
</MenuItem>
<Popover
title="Export note"
side="left"
align="start"
open={isSuperExportMenuOpen}
anchorElement={superExportButtonRef.current}
togglePopover={() => {
setIsSuperExportMenuOpen(!isSuperExportMenuOpen)
}}
className="md:py-1"
>
<Menu a11yLabel={'Super note export menu'} isOpen={isSuperExportMenuOpen}>
<MenuSection>
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_JSON, notes[0].title)}>
<Icon type="code" className={iconClass} />
Export as JSON
</MenuItem>
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_MARKDOWN, notes[0].title)}>
<Icon type="markdown" className={iconClass} />
Export as Markdown
</MenuItem>
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_HTML, notes[0].title)}>
<Icon type="rich-text" className={iconClass} />
Export as HTML
</MenuItem>
</MenuSection>
</Menu>
</Popover>
</>
) : (
<>
<MenuItem
onClick={() => {
if (application.isNativeMobileWeb()) {
void shareSelectedNotes(application, notes)
} else {
const hasSuperNote = notes.some((note) => note.noteType === NoteType.Super)
if (hasSuperNote) {
setShowExportSuperModal(true)
return
}
void downloadSelectedItems()
}
}}
>
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
{application.platform === Platform.Android ? 'Share' : 'Export'}
</MenuItem>
{application.platform === Platform.Android && (
<MenuItem onClick={() => downloadSelectedNotesOnAndroid(application, notes)}>
<Icon type="download" className={iconClass} />
Export
</MenuItem>
)}
</>
)}
<MenuItem onClick={duplicateSelectedItems} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="copy" className={iconClass} />
Duplicate
</MenuItem>
{unarchived && (
<MenuItem
onClick={async () => {
await application.notesController.setArchiveSelectedNotes(true).catch(console.error)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="archive" className={iconClassWarning} />
<span className="text-warning">Archive</span>
</MenuItem>
</>
)}
)}
{archived && (
<MenuItem
onClick={async () => {
await application.notesController.setArchiveSelectedNotes(false).catch(console.error)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="unarchive" className={iconClassWarning} />
<span className="text-warning">Unarchive</span>
</MenuItem>
)}
{notTrashed &&
(altKeyDown ? (
<MenuItem
disabled={areSomeNotesInReadonlySharedVault}
onClick={async () => {
await application.notesController.deleteNotesPermanently()
closeMenuAndToggleNotesList()
}}
>
<Icon type="close" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</MenuItem>
) : (
<MenuItem
onClick={async () => {
await application.notesController.setTrashSelectedNotes(true)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="trash" className={iconClassDanger} />
<span className="text-danger">Move to trash</span>
</MenuItem>
))}
{trashed && (
<>
<MenuItem
onClick={async () => {
await application.notesController.setTrashSelectedNotes(false)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="restore" className={iconClassSuccess} />
<span className="text-success">Restore</span>
</MenuItem>
<MenuItem
disabled={areSomeNotesInReadonlySharedVault}
onClick={async () => {
await application.notesController.deleteNotesPermanently()
closeMenuAndToggleNotesList()
}}
>
<Icon type="close" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</MenuItem>
<MenuItem
onClick={async () => {
await application.notesController.emptyTrash()
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<div className="flex items-start">
<Icon type="trash-sweep" className="mr-2 text-danger" />
<div className="flex-row">
<div className="text-danger">Empty Trash</div>
<div className="text-xs">{application.notesController.trashedNotesCount} notes in Trash</div>
</div>
</div>
</MenuItem>
</>
)}
</MenuSection>
{notes.length === 1 ? (
<>
@@ -507,25 +518,22 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
)}
{!areSomeNotesInSharedVault && (
<>
<HorizontalSeparator classes="my-2" />
<MenuSection>
<ListedActionsOption iconClassName={iconClass} application={application} note={notes[0]} />
</>
</MenuSection>
)}
<HorizontalSeparator classes="my-2" />
{editorForNote && (
<SpellcheckOptions
editorForNote={editorForNote}
notesController={application.notesController}
note={notes[0]}
disabled={areSomeNotesInReadonlySharedVault}
/>
<MenuSection>
<SpellcheckOptions
editorForNote={editorForNote}
notesController={application.notesController}
note={notes[0]}
disabled={areSomeNotesInReadonlySharedVault}
/>
</MenuSection>
)}
<HorizontalSeparator classes="my-2" />
<NoteAttributes className="mb-2" application={application} note={notes[0]} />
<NoteSizeWarning note={notes[0]} />

View File

@@ -37,7 +37,7 @@ const NotesOptionsPanel = ({ notesController, onClickPreprocessing }: Props) =>
togglePopover={toggleMenu}
anchorElement={buttonRef}
open={isOpen}
className="select-none pt-2"
className="select-none"
>
<Menu a11yLabel="Note options menu" isOpen={isOpen}>
<NotesOptions

View File

@@ -3,8 +3,8 @@ import { PlatformedKeyboardShortcut } from '@standardnotes/ui-services'
import Icon from '../Icon/Icon'
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
import MenuItem from '../Menu/MenuItem'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { iconClass } from './ClassNames'
import MenuSection from '../Menu/MenuSection'
type Props = {
note: SNNote
@@ -14,17 +14,13 @@ type Props = {
const SuperNoteOptions = ({ markdownShortcut, enableSuperMarkdownPreview }: Props) => {
return (
<>
<HorizontalSeparator classes="my-2" />
<div className="my-1 px-3 text-base font-semibold uppercase text-text lg:text-xs">Super</div>
<MenuSection>
<MenuItem onClick={enableSuperMarkdownPreview}>
<Icon type="markdown" className={iconClass} />
Show Markdown
{markdownShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={markdownShortcut} />}
</MenuItem>
</>
</MenuSection>
)
}