diff --git a/packages/models/package.json b/packages/models/package.json index 92ac061f6..7967ce9ba 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -25,6 +25,7 @@ "@types/jest": "^28.1.5", "@types/lodash": "^4.14.182", "@typescript-eslint/eslint-plugin": "^5.30.0", + "eslint": "*", "eslint-plugin-prettier": "*", "jest": "^28.1.2", "ts-jest": "^28.0.5", diff --git a/packages/models/src/Domain/Syncable/SmartView/SmartView.ts b/packages/models/src/Domain/Syncable/SmartView/SmartView.ts index 984349660..315c12e43 100644 --- a/packages/models/src/Domain/Syncable/SmartView/SmartView.ts +++ b/packages/models/src/Domain/Syncable/SmartView/SmartView.ts @@ -1,41 +1,33 @@ import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' -import { PredicateInterface, PredicateJsonForm } from '../../Runtime/Predicate/Interface' +import { PredicateInterface } from '../../Runtime/Predicate/Interface' import { predicateFromJson } from '../../Runtime/Predicate/Generators' -import { ItemContent } from '../../Abstract/Content/ItemContent' import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { SystemViewId } from './SystemViewId' +import { EmojiString, IconType } from '../../Utilities/Icon/IconType' +import { SmartViewDefaultIconName, systemViewIcon } from './SmartViewIcons' +import { SmartViewContent } from './SmartViewContent' export const SMART_TAG_DSL_PREFIX = '![' -export enum SystemViewId { - AllNotes = 'all-notes', - Files = 'files', - ArchivedNotes = 'archived-notes', - TrashedNotes = 'trashed-notes', - UntaggedNotes = 'untagged-notes', - StarredNotes = 'starred-notes', -} - -export interface SmartViewContent extends ItemContent { - title: string - predicate: PredicateJsonForm -} - export function isSystemView(view: SmartView): boolean { return Object.values(SystemViewId).includes(view.uuid as SystemViewId) } -/** - * A tag that defines a predicate that consumers can use - * to retrieve a dynamic list of items. - */ export class SmartView extends DecryptedItem { public readonly predicate!: PredicateInterface public readonly title: string + public readonly iconString: IconType | EmojiString constructor(payload: DecryptedPayloadInterface) { super(payload) this.title = String(this.content.title || '') + if (isSystemView(this)) { + this.iconString = systemViewIcon(this.uuid as SystemViewId) + } else { + this.iconString = this.payload.content.iconString || SmartViewDefaultIconName + } + try { this.predicate = this.content.predicate && predicateFromJson(this.content.predicate) } catch (error) { diff --git a/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts b/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts index 3052db51d..a16510ca3 100644 --- a/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts +++ b/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts @@ -1,6 +1,8 @@ import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload' import { SNNote } from '../Note/Note' -import { SmartViewContent, SmartView, SystemViewId } from './SmartView' +import { SmartView } from './SmartView' +import { SmartViewContent } from './SmartViewContent' +import { SystemViewId } from './SystemViewId' import { ItemWithTags } from '../../Runtime/Display/Search/ItemWithTags' import { ContentType } from '@standardnotes/common' import { FillItemContent } from '../../Abstract/Content/ItemContent' diff --git a/packages/models/src/Domain/Syncable/SmartView/SmartViewContent.ts b/packages/models/src/Domain/Syncable/SmartView/SmartViewContent.ts new file mode 100644 index 000000000..f445681fa --- /dev/null +++ b/packages/models/src/Domain/Syncable/SmartView/SmartViewContent.ts @@ -0,0 +1,6 @@ +import { TagContent } from './../Tag/TagContent' +import { PredicateJsonForm } from '../../Runtime/Predicate/Interface' + +export interface SmartViewContent extends TagContent { + predicate: PredicateJsonForm +} diff --git a/packages/models/src/Domain/Syncable/SmartView/SmartViewIcons.ts b/packages/models/src/Domain/Syncable/SmartView/SmartViewIcons.ts new file mode 100644 index 000000000..32a6b51cd --- /dev/null +++ b/packages/models/src/Domain/Syncable/SmartView/SmartViewIcons.ts @@ -0,0 +1,17 @@ +import { SystemViewId } from './SystemViewId' +import { IconType } from '../../Utilities/Icon/IconType' + +export const SmartViewIcons: Record = { + [SystemViewId.AllNotes]: 'notes', + [SystemViewId.Files]: 'folder', + [SystemViewId.ArchivedNotes]: 'archive', + [SystemViewId.TrashedNotes]: 'trash', + [SystemViewId.UntaggedNotes]: 'hashtag-off', + [SystemViewId.StarredNotes]: 'star-filled', +} + +export function systemViewIcon(id: SystemViewId): IconType { + return SmartViewIcons[id] +} + +export const SmartViewDefaultIconName: IconType = 'restore' diff --git a/packages/models/src/Domain/Syncable/SmartView/SystemViewId.ts b/packages/models/src/Domain/Syncable/SmartView/SystemViewId.ts new file mode 100644 index 000000000..913e1a10e --- /dev/null +++ b/packages/models/src/Domain/Syncable/SmartView/SystemViewId.ts @@ -0,0 +1,8 @@ +export enum SystemViewId { + AllNotes = 'all-notes', + Files = 'files', + ArchivedNotes = 'archived-notes', + TrashedNotes = 'trashed-notes', + UntaggedNotes = 'untagged-notes', + StarredNotes = 'starred-notes', +} diff --git a/packages/models/src/Domain/Syncable/SmartView/index.ts b/packages/models/src/Domain/Syncable/SmartView/index.ts index 8692368bf..bf1dfdd5f 100644 --- a/packages/models/src/Domain/Syncable/SmartView/index.ts +++ b/packages/models/src/Domain/Syncable/SmartView/index.ts @@ -1,2 +1,4 @@ export * from './SmartView' export * from './SmartViewBuilder' +export * from './SystemViewId' +export * from './SmartViewContent' diff --git a/packages/models/src/Domain/Syncable/Tag/Tag.spec.ts b/packages/models/src/Domain/Syncable/Tag/Tag.spec.ts index ed5ab63b8..36d8cd401 100644 --- a/packages/models/src/Domain/Syncable/Tag/Tag.spec.ts +++ b/packages/models/src/Domain/Syncable/Tag/Tag.spec.ts @@ -1,10 +1,11 @@ import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource' import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload' -import { SNTag, TagContent } from './Tag' +import { SNTag } from './Tag' import { ContentType } from '@standardnotes/common' import { FillItemContent } from '../../Abstract/Content/ItemContent' import { ContentReference } from '../../Abstract/Reference/ContentReference' import { PayloadTimestampDefaults } from '../../Abstract/Payload' +import { TagContent } from './TagContent' const randUuid = () => String(Math.random()) diff --git a/packages/models/src/Domain/Syncable/Tag/Tag.ts b/packages/models/src/Domain/Syncable/Tag/Tag.ts index 062c0786e..3febe4418 100644 --- a/packages/models/src/Domain/Syncable/Tag/Tag.ts +++ b/packages/models/src/Domain/Syncable/Tag/Tag.ts @@ -1,32 +1,28 @@ +import { VectorIconNameOrEmoji, IconType } from './../../Utilities/Icon/IconType' import { ContentType, Uuid } from '@standardnotes/common' import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface' -import { ItemContent } from '../../Abstract/Content/ItemContent' import { ContentReference } from '../../Abstract/Reference/ContentReference' import { isTagToParentTagReference } from '../../Abstract/Reference/Functions' import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { TagContent, TagContentSpecialized } from './TagContent' export const TagFolderDelimitter = '.' -interface TagInterface { - title: string - expanded: boolean -} - -export type TagContent = TagInterface & ItemContent +export const DefaultTagIconName: IconType = 'hashtag' export const isTag = (x: ItemInterface): x is SNTag => x.content_type === ContentType.Tag -export class SNTag extends DecryptedItem implements TagInterface { +export class SNTag extends DecryptedItem implements TagContentSpecialized { public readonly title: string - - /** Whether to render child tags in view hierarchy. Opposite of collapsed. */ + public readonly iconString: VectorIconNameOrEmoji public readonly expanded: boolean constructor(payload: DecryptedPayloadInterface) { super(payload) this.title = this.payload.content.title || '' this.expanded = this.payload.content.expanded != undefined ? this.payload.content.expanded : true + this.iconString = this.payload.content.iconString || DefaultTagIconName } get noteReferences(): ContentReference[] { diff --git a/packages/models/src/Domain/Syncable/Tag/TagContent.ts b/packages/models/src/Domain/Syncable/Tag/TagContent.ts new file mode 100644 index 000000000..43ca3ce0d --- /dev/null +++ b/packages/models/src/Domain/Syncable/Tag/TagContent.ts @@ -0,0 +1,11 @@ +import { IconType } from './../../Utilities/Icon/IconType' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { EmojiString } from '../../Utilities/Icon/IconType' + +export interface TagContentSpecialized { + title: string + expanded: boolean + iconString: IconType | EmojiString +} + +export type TagContent = TagContentSpecialized & ItemContent diff --git a/packages/models/src/Domain/Syncable/Tag/TagMutator.ts b/packages/models/src/Domain/Syncable/Tag/TagMutator.ts index ae493461f..a1f8c7ab0 100644 --- a/packages/models/src/Domain/Syncable/Tag/TagMutator.ts +++ b/packages/models/src/Domain/Syncable/Tag/TagMutator.ts @@ -1,5 +1,6 @@ import { ContentType } from '@standardnotes/common' -import { TagContent, SNTag } from './Tag' +import { SNTag } from './Tag' +import { TagContent } from './TagContent' import { FileItem } from '../File' import { SNNote } from '../Note' import { isTagToParentTagReference } from '../../Abstract/Reference/Functions' @@ -17,6 +18,10 @@ export class TagMutator extends DecryptedItemMutator { this.mutableContent.expanded = expanded } + set iconString(iconString: string) { + this.mutableContent.iconString = iconString + } + public makeChildOf(tag: SNTag): void { const references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref)) diff --git a/packages/models/src/Domain/Syncable/Tag/index.ts b/packages/models/src/Domain/Syncable/Tag/index.ts index 339182ba6..579aa9788 100644 --- a/packages/models/src/Domain/Syncable/Tag/index.ts +++ b/packages/models/src/Domain/Syncable/Tag/index.ts @@ -1,2 +1,3 @@ export * from './Tag' export * from './TagMutator' +export * from './TagContent' diff --git a/packages/snjs/lib/Types/IconType.ts b/packages/models/src/Domain/Utilities/Icon/IconType.ts similarity index 97% rename from packages/snjs/lib/Types/IconType.ts rename to packages/models/src/Domain/Utilities/Icon/IconType.ts index 43fd117c3..b0ef66e80 100644 --- a/packages/snjs/lib/Types/IconType.ts +++ b/packages/models/src/Domain/Utilities/Icon/IconType.ts @@ -1,3 +1,7 @@ +export type VectorIconNameOrEmoji = EmojiString | IconType + +export type EmojiString = string + export type IconType = | 'accessibility' | 'account-card-details-outline' diff --git a/packages/models/src/Domain/Utilities/Test/SpecUtils.ts b/packages/models/src/Domain/Utilities/Test/SpecUtils.ts index 9b7f7de66..cd98410df 100644 --- a/packages/models/src/Domain/Utilities/Test/SpecUtils.ts +++ b/packages/models/src/Domain/Utilities/Test/SpecUtils.ts @@ -1,4 +1,4 @@ -import { TagContent } from './../../Syncable/Tag/Tag' +import { TagContent } from './../../Syncable/Tag/TagContent' import { ContentType } from '@standardnotes/common' import { FillItemContent, ItemContent } from '../../Abstract/Content/ItemContent' import { DecryptedPayload, PayloadSource, PayloadTimestampDefaults } from '../../Abstract/Payload' diff --git a/packages/models/src/Domain/index.ts b/packages/models/src/Domain/index.ts index 5e4492cef..f10ab63a7 100644 --- a/packages/models/src/Domain/index.ts +++ b/packages/models/src/Domain/index.ts @@ -1,7 +1,7 @@ export * from './Abstract/Component/ActionObserver' -export * from './Abstract/Component/ComponentViewerEvent' -export * from './Abstract/Component/ComponentMessage' export * from './Abstract/Component/ComponentEventObserver' +export * from './Abstract/Component/ComponentMessage' +export * from './Abstract/Component/ComponentViewerEvent' export * from './Abstract/Component/IncomingComponentItemPayload' export * from './Abstract/Component/KeyboardModifier' export * from './Abstract/Component/MessageData' @@ -43,9 +43,9 @@ export * from './Runtime/Collection/Payload/ImmutablePayloadCollection' export * from './Runtime/Collection/Payload/PayloadCollection' export * from './Runtime/Deltas' export * from './Runtime/DirtyCounter/DirtyCounter' +export * from './Runtime/Display' export * from './Runtime/Display/ItemDisplayController' export * from './Runtime/Display/Types' -export * from './Runtime/Display' export * from './Runtime/History' export * from './Runtime/Index/ItemDelta' export * from './Runtime/Index/SNIndex' @@ -70,6 +70,7 @@ export * from './Syncable/SmartView' export * from './Syncable/Tag' export * from './Syncable/Theme' export * from './Syncable/UserPrefs' +export * from './Utilities/Icon/IconType' export * from './Utilities/Item/FindItem' export * from './Utilities/Item/ItemContentsDiffer' export * from './Utilities/Item/ItemContentsEqual' diff --git a/packages/snjs/lib/Client/IconsController.ts b/packages/snjs/lib/Client/IconsController.ts index 9e284537d..d46a09b7d 100644 --- a/packages/snjs/lib/Client/IconsController.ts +++ b/packages/snjs/lib/Client/IconsController.ts @@ -1,5 +1,5 @@ import { NoteType } from '@standardnotes/features' -import { IconType } from '@Lib/Types/IconType' +import { IconType } from '@standardnotes/models' export class IconsController { getIconForFileType(type: string): IconType { diff --git a/packages/snjs/lib/Types/index.ts b/packages/snjs/lib/Types/index.ts index 3dc5e03b8..88907d96b 100644 --- a/packages/snjs/lib/Types/index.ts +++ b/packages/snjs/lib/Types/index.ts @@ -1,3 +1,2 @@ export * from './ApplicationEventPayload' -export * from './IconType' export * from './UuidString' diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index f5cca3dd2..abfd07102 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -111,6 +111,10 @@ const ContentListView: FunctionComponent = ({ const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController + const { selected: selectedTag } = navigationController + + const icon = selectedTag?.iconString + const isFilesSmartView = useMemo( () => navigationController.selected?.uuid === SystemViewId.Files, [navigationController.selected?.uuid], @@ -259,6 +263,7 @@ const ContentListView: FunctionComponent = ({ void isFilesSmartView: boolean @@ -23,6 +25,7 @@ type Props = { const ContentListHeader = ({ application, panelTitle, + icon, addButtonLabel, addNewItem, isFilesSmartView, @@ -41,8 +44,19 @@ const ContentListHeader = ({
-
{panelTitle}
- {optionsSubtitle &&
{optionsSubtitle}
} +
+ {icon && ( + + )} +
+
{panelTitle}
+ {optionsSubtitle &&
{optionsSubtitle}
} +
+
diff --git a/packages/web/src/javascripts/Components/ContentListView/ListItemTags.tsx b/packages/web/src/javascripts/Components/ContentListView/ListItemTags.tsx index ae5187241..b6f51877f 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ListItemTags.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ListItemTags.tsx @@ -19,7 +19,7 @@ const ListItemTags: FunctionComponent = ({ hideTags, tags }) => { className="inline-flex items-center rounded-sm bg-passive-4-opacity-variant py-1 px-1.5 text-foreground" key={tag.uuid} > - + {tag.title} ))} diff --git a/packages/web/src/javascripts/Components/ContentListView/Types/DisplayableListItemProps.ts b/packages/web/src/javascripts/Components/ContentListView/Types/DisplayableListItemProps.ts index e0d04d994..49f1e57ea 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Types/DisplayableListItemProps.ts +++ b/packages/web/src/javascripts/Components/ContentListView/Types/DisplayableListItemProps.ts @@ -5,5 +5,6 @@ export type DisplayableListItemProps = AbstractListItemProps & { tags: { uuid: SNTag['uuid'] title: SNTag['title'] + iconString: SNTag['iconString'] }[] } diff --git a/packages/web/src/javascripts/Components/FilePreview/getFileIconComponent.tsx b/packages/web/src/javascripts/Components/FilePreview/getFileIconComponent.tsx index bd1e0e867..eb1443582 100644 --- a/packages/web/src/javascripts/Components/FilePreview/getFileIconComponent.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/getFileIconComponent.tsx @@ -1,7 +1,7 @@ -import { ICONS } from '@/Components/Icon/Icon' +import { IconNameToSvgMapping } from '@/Components/Icon/IconNameToSvgMapping' export const getFileIconComponent = (iconType: string, className: string) => { - const IconComponent = ICONS[iconType as keyof typeof ICONS] + const IconComponent = IconNameToSvgMapping[iconType as keyof typeof IconNameToSvgMapping] return } diff --git a/packages/web/src/javascripts/Components/Icon/Icon.tsx b/packages/web/src/javascripts/Components/Icon/Icon.tsx index 48bc7e557..e5bd779c5 100644 --- a/packages/web/src/javascripts/Components/Icon/Icon.tsx +++ b/packages/web/src/javascripts/Components/Icon/Icon.tsx @@ -1,141 +1,77 @@ -import { FunctionComponent, useMemo } from 'react' -import { IconType } from '@standardnotes/snjs' -import * as icons from '@standardnotes/icons' - -export const ICONS = { - 'account-circle': icons.AccountCircleIcon, - 'arrow-left': icons.ArrowLeftIcon, - 'arrow-right': icons.ArrowRightIcon, - 'arrows-sort-down': icons.ArrowsSortDownIcon, - 'arrows-sort-up': icons.ArrowsSortUpIcon, - 'attachment-file': icons.AttachmentFileIcon, - 'check-bold': icons.CheckBoldIcon, - 'check-circle': icons.CheckCircleIcon, - 'chevron-down': icons.ChevronDownIcon, - 'chevron-left': icons.ChevronLeftIcon, - 'chevron-right': icons.ChevronRightIcon, - 'clear-circle-filled': icons.ClearCircleFilledIcon, - 'cloud-off': icons.CloudOffIcon, - 'diamond-filled': icons.DiamondFilledIcon, - 'eye-off': icons.EyeOffIcon, - 'file-doc': icons.FileDocIcon, - 'file-image': icons.FileImageIcon, - 'file-mov': icons.FileMovIcon, - 'file-music': icons.FileMusicIcon, - 'file-other': icons.FileOtherIcon, - 'file-pdf': icons.FilePdfIcon, - 'file-ppt': icons.FilePptIcon, - 'file-xls': icons.FileXlsIcon, - 'file-zip': icons.FileZipIcon, - 'hashtag-off': icons.HashtagOffIcon, - 'link-off': icons.LinkOffIcon, - 'list-bulleted': icons.ListBulleted, - 'lock-filled': icons.LockFilledIcon, - 'menu-arrow-down-alt': icons.MenuArrowDownAlt, - 'menu-arrow-down': icons.MenuArrowDownIcon, - 'menu-arrow-right': icons.MenuArrowRightIcon, - 'menu-close': icons.MenuCloseIcon, - 'menu-variant': icons.MenuVariantIcon, - 'notes-filled': icons.NotesFilledIcon, - 'pencil-filled': icons.PencilFilledIcon, - 'pencil-off': icons.PencilOffIcon, - 'pin-filled': icons.PinFilledIcon, - 'plain-text': icons.PlainTextIcon, - 'premium-feature': icons.PremiumFeatureIcon, - 'rich-text': icons.RichTextIcon, - 'sort-descending': icons.SortDescendingIcon, - 'star-circle-filled': icons.StarCircleFilled, - 'star-filled': icons.StarFilledIcon, - 'star-variant-filled': icons.StarVariantFilledIcon, - 'trash-filled': icons.TrashFilledIcon, - 'trash-sweep': icons.TrashSweepIcon, - 'user-add': icons.UserAddIcon, - 'user-switch': icons.UserSwitch, - accessibility: icons.AccessibilityIcon, - add: icons.AddIcon, - archive: icons.ArchiveIcon, - asterisk: icons.AsteriskIcon, - authenticator: icons.AuthenticatorIcon, - check: icons.CheckIcon, - close: icons.CloseIcon, - code: icons.CodeIcon, - copy: icons.CopyIcon, - dashboard: icons.DashboardIcon, - diamond: icons.DiamondIcon, - download: icons.DownloadIcon, - editor: icons.EditorIcon, - email: icons.EmailIcon, - eye: icons.EyeIcon, - file: icons.FileIcon, - folder: icons.FolderIcon, - hashtag: icons.HashtagIcon, - help: icons.HelpIcon, - history: icons.HistoryIcon, - info: icons.InfoIcon, - keyboard: icons.KeyboardIcon, - link: icons.LinkIcon, - listed: icons.ListedIcon, - lock: icons.LockIcon, - markdown: icons.MarkdownIcon, - more: icons.MoreIcon, - notes: icons.NotesIcon, - password: icons.PasswordIcon, - pencil: icons.PencilIcon, - pin: icons.PinIcon, - restore: icons.RestoreIcon, - search: icons.SearchIcon, - security: icons.SecurityIcon, - server: icons.ServerIcon, - settings: icons.SettingsIcon, - share: icons.ShareIcon, - signIn: icons.SignInIcon, - signOut: icons.SignOutIcon, - spreadsheets: icons.SpreadsheetsIcon, - star: icons.StarIcon, - subtract: icons.SubtractIcon, - sync: icons.SyncIcon, - tasks: icons.TasksIcon, - themes: icons.ThemesIcon, - trash: icons.TrashIcon, - tune: icons.TuneIcon, - unarchive: icons.UnarchiveIcon, - unpin: icons.UnpinIcon, - user: icons.UserIcon, - view: icons.ViewIcon, - warning: icons.WarningIcon, - window: icons.WindowIcon, -} +import { FunctionComponent } from 'react' +import { VectorIconNameOrEmoji } from '@standardnotes/snjs' +import { IconNameToSvgMapping } from './IconNameToSvgMapping' +import { classNames } from '@/Utils/ConcatenateClassNames' type Props = { - type: IconType + type: VectorIconNameOrEmoji className?: string ariaLabel?: string - size?: 'small' | 'medium' | 'normal' | 'custom' + size?: 'small' | 'medium' | 'normal' | 'large' | 'custom' +} + +const ContainerDimensions = { + small: 'w-3.5 h-3.5', + medium: 'w-4 h-4', + normal: 'w-5 h-5', + large: 'w-6 h-6', + custom: '', +} + +const EmojiContainerDimensions = { + small: 'w-4 h-4', + medium: 'w-5 h-5', + normal: 'w-5 h-5', + large: 'w-7 h-6', + custom: '', +} + +const EmojiOffset = { + small: '', + medium: '', + normal: '-mt-0.5', + large: '', + custom: '', +} + +const EmojiSize = { + small: 'text-xs', + medium: 'text-sm', + normal: 'text-base', + large: 'text-lg', + custom: '', +} + +const getIconComponent = (type: VectorIconNameOrEmoji) => { + return IconNameToSvgMapping[type as keyof typeof IconNameToSvgMapping] +} + +export const isIconEmoji = (type: VectorIconNameOrEmoji): boolean => { + return getIconComponent(type) == undefined } const Icon: FunctionComponent = ({ type, className = '', ariaLabel, size = 'normal' }) => { - const IconComponent = ICONS[type as keyof typeof ICONS] - - const dimensions = useMemo(() => { - switch (size) { - case 'small': - return 'w-3.5 h-3.5' - case 'medium': - return 'w-4 h-4' - case 'custom': - return '' - default: - return 'w-5 h-5' - } - }, [size]) - + const IconComponent = getIconComponent(type) if (!IconComponent) { - return null + return ( + + ) } return ( diff --git a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx new file mode 100644 index 000000000..11c04af66 --- /dev/null +++ b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx @@ -0,0 +1,106 @@ +import * as icons from '@standardnotes/icons' + +export const IconNameToSvgMapping = { + 'account-circle': icons.AccountCircleIcon, + 'arrow-left': icons.ArrowLeftIcon, + 'arrow-right': icons.ArrowRightIcon, + 'arrows-sort-down': icons.ArrowsSortDownIcon, + 'arrows-sort-up': icons.ArrowsSortUpIcon, + 'attachment-file': icons.AttachmentFileIcon, + 'check-bold': icons.CheckBoldIcon, + 'check-circle': icons.CheckCircleIcon, + 'chevron-down': icons.ChevronDownIcon, + 'chevron-left': icons.ChevronLeftIcon, + 'chevron-right': icons.ChevronRightIcon, + 'clear-circle-filled': icons.ClearCircleFilledIcon, + 'cloud-off': icons.CloudOffIcon, + 'diamond-filled': icons.DiamondFilledIcon, + 'eye-off': icons.EyeOffIcon, + 'file-doc': icons.FileDocIcon, + 'file-image': icons.FileImageIcon, + 'file-mov': icons.FileMovIcon, + 'file-music': icons.FileMusicIcon, + 'file-other': icons.FileOtherIcon, + 'file-pdf': icons.FilePdfIcon, + 'file-ppt': icons.FilePptIcon, + 'file-xls': icons.FileXlsIcon, + 'file-zip': icons.FileZipIcon, + 'hashtag-off': icons.HashtagOffIcon, + 'link-off': icons.LinkOffIcon, + 'list-bulleted': icons.ListBulleted, + 'lock-filled': icons.LockFilledIcon, + 'menu-arrow-down-alt': icons.MenuArrowDownAlt, + 'menu-arrow-down': icons.MenuArrowDownIcon, + 'menu-arrow-right': icons.MenuArrowRightIcon, + 'menu-close': icons.MenuCloseIcon, + 'menu-variant': icons.MenuVariantIcon, + 'notes-filled': icons.NotesFilledIcon, + 'pencil-filled': icons.PencilFilledIcon, + 'pencil-off': icons.PencilOffIcon, + 'pin-filled': icons.PinFilledIcon, + 'plain-text': icons.PlainTextIcon, + 'premium-feature': icons.PremiumFeatureIcon, + 'rich-text': icons.RichTextIcon, + 'sort-descending': icons.SortDescendingIcon, + 'star-circle-filled': icons.StarCircleFilled, + 'star-filled': icons.StarFilledIcon, + 'star-variant-filled': icons.StarVariantFilledIcon, + 'trash-filled': icons.TrashFilledIcon, + 'trash-sweep': icons.TrashSweepIcon, + 'user-add': icons.UserAddIcon, + 'user-switch': icons.UserSwitch, + 'fullscreen-exit': icons.FullscreenExitIcon, + accessibility: icons.AccessibilityIcon, + add: icons.AddIcon, + archive: icons.ArchiveIcon, + asterisk: icons.AsteriskIcon, + authenticator: icons.AuthenticatorIcon, + check: icons.CheckIcon, + close: icons.CloseIcon, + code: icons.CodeIcon, + copy: icons.CopyIcon, + dashboard: icons.DashboardIcon, + diamond: icons.DiamondIcon, + download: icons.DownloadIcon, + editor: icons.EditorIcon, + email: icons.EmailIcon, + eye: icons.EyeIcon, + file: icons.FileIcon, + folder: icons.FolderIcon, + hashtag: icons.HashtagIcon, + help: icons.HelpIcon, + history: icons.HistoryIcon, + info: icons.InfoIcon, + keyboard: icons.KeyboardIcon, + link: icons.LinkIcon, + listed: icons.ListedIcon, + lock: icons.LockIcon, + markdown: icons.MarkdownIcon, + more: icons.MoreIcon, + notes: icons.NotesIcon, + password: icons.PasswordIcon, + pencil: icons.PencilIcon, + pin: icons.PinIcon, + restore: icons.RestoreIcon, + search: icons.SearchIcon, + security: icons.SecurityIcon, + server: icons.ServerIcon, + settings: icons.SettingsIcon, + share: icons.ShareIcon, + signIn: icons.SignInIcon, + signOut: icons.SignOutIcon, + spreadsheets: icons.SpreadsheetsIcon, + star: icons.StarIcon, + subtract: icons.SubtractIcon, + sync: icons.SyncIcon, + tasks: icons.TasksIcon, + themes: icons.ThemesIcon, + trash: icons.TrashIcon, + tune: icons.TuneIcon, + unarchive: icons.UnarchiveIcon, + unpin: icons.UnpinIcon, + user: icons.UserIcon, + view: icons.ViewIcon, + warning: icons.WarningIcon, + window: icons.WindowIcon, +} diff --git a/packages/web/src/javascripts/Components/Icon/IconPicker.tsx b/packages/web/src/javascripts/Components/Icon/IconPicker.tsx new file mode 100644 index 000000000..b542fc913 --- /dev/null +++ b/packages/web/src/javascripts/Components/Icon/IconPicker.tsx @@ -0,0 +1,131 @@ +import { EmojiString, Platform, VectorIconNameOrEmoji } from '@standardnotes/snjs' +import { FunctionComponent, useMemo, useRef, useState } from 'react' +import Dropdown from '../Dropdown/Dropdown' +import { DropdownItem } from '../Dropdown/DropdownItem' +import { isIconEmoji } from './Icon' +import { IconNameToSvgMapping } from './IconNameToSvgMapping' +import { IconPickerType } from './IconPickerType' + +type Props = { + selectedValue: VectorIconNameOrEmoji + onIconChange: (value?: string) => void + platform: Platform + className?: string +} + +const IconPicker = ({ selectedValue, onIconChange, platform, className }: Props) => { + const iconOptions = useMemo( + () => + [...Object.keys(IconNameToSvgMapping)].map( + (value) => + ({ + label: value, + value: value, + icon: value, + } as DropdownItem), + ), + [], + ) + + const isSelectedEmoji = isIconEmoji(selectedValue) + const isMacOS = platform === Platform.MacWeb || platform === Platform.MacDesktop + const isWindows = platform === Platform.WindowsWeb || platform === Platform.WindowsDesktop + + const emojiInputRef = useRef(null) + const [emojiInputFocused, setEmojiInputFocused] = useState(true) + const [currentType, setCurrentType] = useState(isSelectedEmoji ? 'emoji' : 'icon') + const [emojiInputValue, setEmojiInputValue] = useState(isSelectedEmoji ? selectedValue : '') + + const selectTab = (type: IconPickerType | 'reset') => { + if (type === 'reset') { + onIconChange(undefined) + setEmojiInputValue('') + } else { + setCurrentType(type) + } + } + + const TabButton: FunctionComponent<{ + label: string + type: IconPickerType | 'reset' + }> = ({ type, label }) => { + const isSelected = currentType === type + + return ( + + ) + } + + const handleIconChange = (value: string) => { + onIconChange(value) + } + + const handleEmojiChange = (value: EmojiString) => { + setEmojiInputValue(value) + + const emojiLength = [...value].length + if (emojiLength === 1) { + onIconChange(value) + emojiInputRef.current?.blur() + setEmojiInputFocused(false) + } else { + setEmojiInputFocused(true) + } + } + + return ( +
+
+ + + +
+
+ {currentType === 'icon' && ( + + )} + {currentType === 'emoji' && ( + <> +
+ handleEmojiChange((input as HTMLInputElement)?.value)} + /> +
+
+ Use your keyboard to enter or paste in an emoji character. +
+ {isMacOS && ( +
On macOS: ⌘ + ⌃ + Space bar to bring up emoji picker.
+ )} + {isWindows && ( +
On Windows: Windows key + . to bring up emoji picker.
+ )} + + )} +
+
+ ) +} + +export default IconPicker diff --git a/packages/web/src/javascripts/Components/Icon/IconPickerType.ts b/packages/web/src/javascripts/Components/Icon/IconPickerType.ts new file mode 100644 index 000000000..ba7447c44 --- /dev/null +++ b/packages/web/src/javascripts/Components/Icon/IconPickerType.ts @@ -0,0 +1 @@ +export type IconPickerType = 'icon' | 'emoji' diff --git a/packages/web/src/javascripts/Components/Menu/MenuItem.tsx b/packages/web/src/javascripts/Components/Menu/MenuItem.tsx index 6743e23d0..251e8c4d2 100644 --- a/packages/web/src/javascripts/Components/Menu/MenuItem.tsx +++ b/packages/web/src/javascripts/Components/Menu/MenuItem.tsx @@ -6,6 +6,7 @@ import { IconType } from '@standardnotes/snjs' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { MenuItemType } from './MenuItemType' import RadioIndicator from '../Radio/RadioIndicator' +import { classNames } from '@/Utils/ConcatenateClassNames' type MenuItemProps = { children: ReactNode @@ -40,7 +41,10 @@ const MenuItem = forwardRef(