feat: ability to favorite tags + customize icon (#1858)

This commit is contained in:
Mo
2022-10-21 11:11:31 -05:00
committed by GitHub
parent 3b048a31aa
commit cbd0063926
38 changed files with 568 additions and 262 deletions

View File

@@ -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<Props> = ({ 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 (
<label
className={classNames(
'fill-current',
'text-center',
EmojiSize[size],
EmojiContainerDimensions[size],
EmojiOffset[size],
className,
)}
>
{type}
</label>
)
}
return (
<IconComponent
className={`${dimensions} fill-current ${className}`}
className={`${ContainerDimensions[size]} fill-current ${className}`}
role="img"
{...(ariaLabel ? { 'aria-label': ariaLabel } : { 'aria-hidden': true })}
/>

View File

@@ -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,
}

View File

@@ -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<HTMLInputElement>(null)
const [emojiInputFocused, setEmojiInputFocused] = useState(true)
const [currentType, setCurrentType] = useState<IconPickerType>(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 (
<button
className={`relative mr-2 cursor-pointer border-0 bg-default pb-1.5 text-sm focus:shadow-none ${
isSelected ? 'font-medium text-info' : 'text-text'
}`}
onClick={() => {
selectTab(type)
}}
>
{label}
</button>
)
}
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 (
<div className={`flex h-full flex-grow flex-col overflow-auto ${className}`}>
<div className="flex">
<TabButton label="Icon" type={'icon'} />
<TabButton label="Emoji" type={'emoji'} />
<TabButton label="Reset" type={'reset'} />
</div>
<div className={'mt-2 h-full min-h-0 overflow-auto'}>
{currentType === 'icon' && (
<Dropdown
id="change-tag-icon-dropdown"
label="Change the icon for a tag"
items={iconOptions}
value={selectedValue}
onChange={handleIconChange}
/>
)}
{currentType === 'emoji' && (
<>
<div>
<input
ref={emojiInputRef}
autoComplete="off"
autoFocus={emojiInputFocused}
className="w-full flex-grow rounded border border-solid border-passive-3 bg-default px-2 py-1 text-base font-bold text-text focus:shadow-none focus:outline-none"
type="text"
value={emojiInputValue}
onChange={({ target: input }) => handleEmojiChange((input as HTMLInputElement)?.value)}
/>
</div>
<div className="mt-2 text-xs text-passive-0">
Use your keyboard to enter or paste in an emoji character.
</div>
{isMacOS && (
<div className="mt-2 text-xs text-passive-0">On macOS: + + Space bar to bring up emoji picker.</div>
)}
{isWindows && (
<div className="mt-2 text-xs text-passive-0">On Windows: Windows key + . to bring up emoji picker.</div>
)}
</>
)}
</div>
</div>
)
}
export default IconPicker

View File

@@ -0,0 +1 @@
export type IconPickerType = 'icon' | 'emoji'