From f3cd1c2f8c93253fcfc7ba3f9ef76a3de5ef8743 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 23 Apr 2025 17:58:04 -0500 Subject: [PATCH 01/22] Add tooltips to basic view buttons --- src/admin/AdminAppMenu.tsx | 2 +- src/app/ThemeSwitcher.tsx | 6 ++-- src/app/ViewSwitcher.tsx | 23 +++++++++--- src/components/SwitcherItem.tsx | 35 ++++++++++++------- src/components/primitives/KeyCommand.tsx | 29 +++++++++++++++ .../primitives/TooltipPrimitive.tsx | 24 +++++++++++-- 6 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 src/components/primitives/KeyCommand.tsx diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index c6a09b40..8a19e63d 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -220,7 +220,7 @@ export default function AdminAppMenu({ } align="start" sideOffset={12} - alignOffset={-85} + alignOffset={-93} onOpen={refreshAdminData} className={clsx( 'border-medium', diff --git a/src/app/ThemeSwitcher.tsx b/src/app/ThemeSwitcher.tsx index 861a26bc..6ef0d021 100644 --- a/src/app/ThemeSwitcher.tsx +++ b/src/app/ThemeSwitcher.tsx @@ -22,22 +22,22 @@ export default function ThemeSwitcher () { return ( } onClick={() => setTheme('system')} active={theme === 'system'} + tooltip={{ content: 'System' }} /> } onClick={() => setTheme('light')} active={theme === 'light'} + tooltip={{ content: 'Light Mode' }} /> } onClick={() => setTheme('dark')} active={theme === 'dark'} + tooltip={{ content: 'Dark Mode' }} /> ); diff --git a/src/app/ViewSwitcher.tsx b/src/app/ViewSwitcher.tsx index 21f3322e..1d8db547 100644 --- a/src/app/ViewSwitcher.tsx +++ b/src/app/ViewSwitcher.tsx @@ -30,17 +30,25 @@ export default function ViewSwitcher({ const renderItemFeed = } + icon={} href={PATH_FEED_INFERRED} active={currentSelection === 'feed'} + tooltip={{ + content: 'Feed', + keyCommand: 'F', + }} noPadding />; const renderItemGrid = } + icon={} href={PATH_GRID_INFERRED} active={currentSelection === 'grid'} + tooltip={{ + content: 'Grid', + keyCommand: 'G', + }} noPadding />; @@ -58,18 +66,25 @@ export default function ViewSwitcher({ icon={} isInteractive={false} noPadding + tooltip={{ content: 'Admin Menu' }} />} {isUserSignedIn && } noPadding + tooltip={{ content: 'Admin Menu' }} />} } + title="Search" + icon={} onClick={() => setIsCommandKOpen?.(true)} + tooltip={{ + content: 'Search', + keyCommand: 'K', + keyCommandModifier: '⌘', + }} /> diff --git a/src/components/SwitcherItem.tsx b/src/components/SwitcherItem.tsx index 347a5475..3770cadc 100644 --- a/src/components/SwitcherItem.tsx +++ b/src/components/SwitcherItem.tsx @@ -1,8 +1,9 @@ import { clsx } from 'clsx/lite'; import { SHOULD_PREFETCH_ALL_LINKS } from '@/app/config'; -import { ReactNode } from 'react'; +import { ComponentProps, ReactNode } from 'react'; import Spinner from './Spinner'; import LinkWithIconLoader from './LinkWithIconLoader'; +import Tooltip from './Tooltip'; export default function SwitcherItem({ icon, @@ -14,6 +15,7 @@ export default function SwitcherItem({ isInteractive = true, noPadding, prefetch = SHOULD_PREFETCH_ALL_LINKS, + tooltip, }: { icon: ReactNode title?: string @@ -24,6 +26,7 @@ export default function SwitcherItem({ isInteractive?: boolean noPadding?: boolean prefetch?: boolean + tooltip?: ComponentProps }) { const className = clsx( 'flex items-center justify-center', @@ -50,18 +53,24 @@ export default function SwitcherItem({ {icon} ; + const content = href + ? , + }} /> + :
+ {renderIcon()} +
; + return ( - href - ? , - }} /> - :
- {renderIcon()} -
+ tooltip + ? + {content} + + : content ); }; diff --git a/src/components/primitives/KeyCommand.tsx b/src/components/primitives/KeyCommand.tsx new file mode 100644 index 00000000..290563f0 --- /dev/null +++ b/src/components/primitives/KeyCommand.tsx @@ -0,0 +1,29 @@ +import clsx from 'clsx/lite'; +import { useMemo } from 'react'; + +export default function KeyCommand({ + children, + modifier, + className, +}: { + children: string + modifier?: '⌘' | '⌥' | '⇧' | '⌃' | '⏎' + className?: string +}) { + const keys = useMemo(() => + modifier ? [modifier, ...children] : [...children], + [modifier, children]); + + return ( + + {keys.map((key) => ( + + {key} + + ))} + + ); +} diff --git a/src/components/primitives/TooltipPrimitive.tsx b/src/components/primitives/TooltipPrimitive.tsx index 02d19728..ddcd86d5 100644 --- a/src/components/primitives/TooltipPrimitive.tsx +++ b/src/components/primitives/TooltipPrimitive.tsx @@ -6,22 +6,30 @@ import MenuSurface from './MenuSurface'; import useSupportsHover from '@/utility/useSupportsHover'; import clsx from 'clsx/lite'; import useClickInsideOutside from '@/utility/useClickInsideOutside'; - +import KeyCommand from './KeyCommand'; export default function TooltipPrimitive({ - content, + content: contentProp, className, classNameTrigger: classNameTriggerProp, sideOffset = 10, + delayDuration = 100, + skipDelayDuration = 300, supportMobile, color, + keyCommand, + keyCommandModifier, children, }: { content?: ReactNode className?: string classNameTrigger?: string sideOffset?: number + delayDuration?: number + skipDelayDuration?: number supportMobile?: boolean color?: ComponentProps['color'] + keyCommand?: string + keyCommandModifier?: ComponentProps['modifier'] children: ReactNode }) { const refTrigger = useRef(null); @@ -45,8 +53,18 @@ export default function TooltipPrimitive({ classNameTriggerProp, ); + const content = keyCommand + ?
+ {contentProp} + {' '} + + {keyCommand} + +
+ : contentProp; + return ( - + {includeButton From 99a3fb7ad28acb52960b322b98e70e2df3852a89 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Thu, 24 Apr 2025 09:18:27 -0500 Subject: [PATCH 02/22] Refine tooltip/more menu interactions --- src/admin/AdminAppMenu.tsx | 27 +++++++++++++--------- src/admin/AdminPhotoMenu.tsx | 10 ++++---- src/app/ViewSwitcher.tsx | 10 ++++++-- src/components/SwitcherItem.tsx | 8 ++++--- src/components/more/MoreMenu.tsx | 26 ++++++++++++++------- src/components/more/MoreMenuItem.tsx | 5 ++-- src/components/primitives/LoaderButton.tsx | 3 +++ 7 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index 8a19e63d..201b77c4 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -34,10 +34,14 @@ import MoreMenuItem from '@/components/more/MoreMenuItem'; export default function AdminAppMenu({ active, animateMenuClose, + isOpen, + setIsOpen, className, }: { active?: boolean animateMenuClose?: boolean + isOpen?: boolean + setIsOpen?: (isOpen: boolean) => void className?: string }) { const { @@ -80,7 +84,7 @@ export default function AdminAppMenu({ annotation: `${uploadsCount}`, icon: , href: PATH_ADMIN_UPLOADS, }); @@ -93,13 +97,13 @@ export default function AdminAppMenu({ {photosCountNeedSync} , icon: , href: PATH_ADMIN_PHOTOS_UPDATES, }); @@ -112,7 +116,7 @@ export default function AdminAppMenu({ }, icon: , href: PATH_ADMIN_PHOTOS, }); @@ -123,7 +127,7 @@ export default function AdminAppMenu({ annotation: `${tagsCount}`, icon: , href: PATH_ADMIN_TAGS, }); @@ -134,7 +138,7 @@ export default function AdminAppMenu({ annotation: `${recipesCount}`, icon: , href: PATH_ADMIN_RECIPES, }); @@ -147,7 +151,7 @@ export default function AdminAppMenu({ icon: isSelecting ? : , href: showAppInsightsLink ? PATH_ADMIN_INSIGHTS @@ -189,6 +193,7 @@ export default function AdminAppMenu({ return ( {!hasAdminData && isLoadingAdminData @@ -226,7 +231,7 @@ export default function AdminAppMenu({ 'border-medium', className, )} - buttonClassName={clsx( + classNameButton={clsx( 'p-0!', 'w-full h-full', 'flex items-center justify-center', @@ -237,7 +242,7 @@ export default function AdminAppMenu({ ? 'text-black dark:text-white' : 'text-gray-400 dark:text-gray-600', )} - buttonClassNameOpen={clsx( + classNameButtonOpen={clsx( 'bg-dim text-main!', '[&>*>*]:translate-y-[6px]', !animateMenuClose && '[&>*>*]:duration-300', diff --git a/src/admin/AdminPhotoMenu.tsx b/src/admin/AdminPhotoMenu.tsx index 3d3b7822..feeead60 100644 --- a/src/admin/AdminPhotoMenu.tsx +++ b/src/admin/AdminPhotoMenu.tsx @@ -48,7 +48,7 @@ export default function AdminPhotoMenu({ label: 'Edit', icon: , href: pathForAdminPhotoEdit(photo.id), }]; @@ -57,7 +57,7 @@ export default function AdminPhotoMenu({ label: isFav ? 'Unfavorite' : 'Favorite', icon: , action: () => toggleFavoritePhotoAction( @@ -70,7 +70,7 @@ export default function AdminPhotoMenu({ label: 'Download', icon: , href: photo.url, hrefDownloadName: downloadFileNameForPhoto(photo), @@ -86,7 +86,9 @@ export default function AdminPhotoMenu({ size="small" />} , - icon: , + icon: , action: () => syncPhotoAction(photo.id) .then(() => revalidatePhoto?.(photo.id)), }); diff --git a/src/app/ViewSwitcher.tsx b/src/app/ViewSwitcher.tsx index 1d8db547..052c0225 100644 --- a/src/app/ViewSwitcher.tsx +++ b/src/app/ViewSwitcher.tsx @@ -12,6 +12,7 @@ import { GRID_HOMEPAGE_ENABLED } from './config'; import AdminAppMenu from '@/admin/AdminAppMenu'; import Spinner from '@/components/Spinner'; import clsx from 'clsx/lite'; +import { useState } from 'react'; export type SwitcherSelection = 'feed' | 'grid' | 'admin'; @@ -28,6 +29,8 @@ export default function ViewSwitcher({ setIsCommandKOpen, } = useAppState(); + const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false); + const renderItemFeed = } @@ -70,9 +73,12 @@ export default function ViewSwitcher({ />} {isUserSignedIn && } - noPadding + icon={} tooltip={{ content: 'Admin Menu' }} + noPadding />} diff --git a/src/components/SwitcherItem.tsx b/src/components/SwitcherItem.tsx index 3770cadc..702a4c51 100644 --- a/src/components/SwitcherItem.tsx +++ b/src/components/SwitcherItem.tsx @@ -30,8 +30,7 @@ export default function SwitcherItem({ }) { const className = clsx( 'flex items-center justify-center', - 'w-[42px] h-full', - 'py-0.5 px-1.5', + 'w-[42px] h-[28px]', isInteractive && 'cursor-pointer', isInteractive && 'hover:bg-gray-100/60 active:bg-gray-100', isInteractive && 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900', @@ -68,7 +67,10 @@ export default function SwitcherItem({ return ( tooltip - ? + ? {content} : content diff --git a/src/components/more/MoreMenu.tsx b/src/components/more/MoreMenu.tsx index 0afded48..4cb4c4f9 100644 --- a/src/components/more/MoreMenu.tsx +++ b/src/components/more/MoreMenu.tsx @@ -15,12 +15,14 @@ export default function MoreMenu({ icon, header, className, - buttonClassName, - buttonClassNameOpen, + classNameButton, + classNameButtonOpen, ariaLabel, align = 'end', // Prevent errant clicks from trigger being too close to menu sideOffset = 6, + isOpen: isOpenProp, + setIsOpen: setIsOpenProp, onOpen, ...props }: { @@ -28,12 +30,17 @@ export default function MoreMenu({ icon?: ReactNode header?: ReactNode className?: string - buttonClassName?: string - buttonClassNameOpen?: string + classNameButton?: string + classNameButtonOpen?: string ariaLabel: string + isOpen?: boolean + setIsOpen?: (isOpen: boolean) => void onOpen?: () => void } & ComponentProps){ - const [isOpen, setIsOpen] = useState(false); + const [isOpenInternal, setIsOpenInternal] = useState(isOpenProp ?? false); + + const isOpen = isOpenProp ?? isOpenInternal; + const setIsOpen = setIsOpenProp ?? setIsOpenInternal; const dismissMenu = useCallback(() => { setIsOpen(false); @@ -44,7 +51,10 @@ export default function MoreMenu({ }, [isOpen, onOpen]); return ( - +