From 432ace778c4c355be7a1cd7761ec8485a66d9474 Mon Sep 17 00:00:00 2001 From: Richard R Date: Mon, 1 Jun 2026 15:20:28 -0600 Subject: [PATCH] refactor(ui): improve segmented control accessibility and update danger color tokens Update SegmentedControl to support full keyboard navigation and focus management, enhancing accessibility. Replace string indicator in Select with icon, and update button danger variant to use new --danger-strong variable for hover states. Add danger-strong token to Tailwind config and globals. Refine dropzone disabled behavior, adjust focus ring for better contrast, and apply minor UI consistency tweaks across components. --- src/app/(public)/page.tsx | 2 +- src/app/globals.css | 1 + src/components/admin/AdminProvidersPanel.tsx | 1 - src/components/auth/UserMenu.tsx | 4 +- .../documents/DexieMigrationModal.tsx | 2 - .../documents/DocumentHeaderMenu.tsx | 9 +--- src/components/documents/DocumentSettings.tsx | 2 +- src/components/layout/LibraryFrame.tsx | 12 +++--- src/components/ui/button.tsx | 2 +- src/components/ui/dialog.tsx | 2 +- src/components/ui/dropzone.ts | 2 +- src/components/ui/range.tsx | 2 +- src/components/ui/search-field.tsx | 3 +- src/components/ui/segmented-control.tsx | 43 ++++++++++++++++++- src/components/ui/select.tsx | 9 +++- src/components/ui/sidebar-nav.tsx | 2 +- src/components/ui/tokens.ts | 2 +- tailwind.config.ts | 1 + 18 files changed, 67 insertions(+), 34 deletions(-) diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 6c9d1bd..a297501 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -103,7 +103,7 @@ export default async function LandingPage() { ) : ( Sign in )} - Read the docs → + Read the docs →
diff --git a/src/app/globals.css b/src/app/globals.css index ceb7236..284d34c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -220,6 +220,7 @@ html.custom { --accent-strong: color-mix(in srgb, var(--accent) 88%, var(--foreground)); --danger: #dc2626; + --danger-strong: #b91c1c; --danger-wash: color-mix(in srgb, var(--danger) 12%, transparent); --elev-1: 0 1px 2px rgb(var(--shadow-rgb) / 0.10); diff --git a/src/components/admin/AdminProvidersPanel.tsx b/src/components/admin/AdminProvidersPanel.tsx index 8960fe9..55dfc0f 100644 --- a/src/components/admin/AdminProvidersPanel.tsx +++ b/src/components/admin/AdminProvidersPanel.tsx @@ -370,7 +370,6 @@ export function AdminProvidersPanel() { onClick={startCreate} variant="primary" size="icon" - className="h-7 w-7" aria-label="Add provider" title="Add provider" > diff --git a/src/components/auth/UserMenu.tsx b/src/components/auth/UserMenu.tsx index e780d2a..d9e44ea 100644 --- a/src/components/auth/UserMenu.tsx +++ b/src/components/auth/UserMenu.tsx @@ -86,9 +86,7 @@ export function UserMenu({ )} - > - {session.user.email || 'Account'} - + /> ); } diff --git a/src/components/documents/DexieMigrationModal.tsx b/src/components/documents/DexieMigrationModal.tsx index d065adf..df469d1 100644 --- a/src/components/documents/DexieMigrationModal.tsx +++ b/src/components/documents/DexieMigrationModal.tsx @@ -133,8 +133,6 @@ export function DexieMigrationModal({ } }, [loadLocalDexieDocs, onComplete, refreshDocuments]); - if (!isOpen) return null; - return ( { - // We wrap in a handler to stop propagation if needed, - // but ZoomControl buttons handle their own clicks. - // However, Menu might close on click? - // Headless UI Menu closes on click inside MenuItem, but these are just buttons in a div. - // It should NOT close unless we click a MenuItem. - onZoomIncrease(); - }} + onIncrease={onZoomIncrease} onDecrease={onZoomDecrease} min={minZoom} max={maxZoom} diff --git a/src/components/documents/DocumentSettings.tsx b/src/components/documents/DocumentSettings.tsx index 6632e09..07442c2 100644 --- a/src/components/documents/DocumentSettings.tsx +++ b/src/components/documents/DocumentSettings.tsx @@ -203,7 +203,7 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: { {pdf.parseStatus ?? 'pending'} - typeof window !== 'undefined' ? window.matchMedia(NARROW_QUERY).matches : false, - ); + const [isNarrow, setIsNarrow] = useState(false); useEffect(() => { if (typeof window === 'undefined') return; const mq = window.matchMedia(NARROW_QUERY); @@ -48,9 +46,11 @@ export function LibraryFrame({
{children}
{statusBar} - - {sidebar} - + {isNarrow && ( + + {sidebar} + + )} ); } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index b1ca111..d31b6b0 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -18,7 +18,7 @@ export const buttonStyles = variants({ primary: 'bg-accent text-background hover:bg-secondary-accent', secondary: 'border border-line bg-surface text-foreground hover:bg-accent-wash', outline: 'border border-line bg-surface-sunken text-foreground hover:bg-accent-wash hover:text-accent', - danger: 'border border-danger bg-danger text-background hover:bg-danger', + danger: 'border border-danger bg-danger text-background hover:bg-danger-strong hover:border-danger-strong', ghost: 'bg-transparent text-foreground hover:bg-accent-wash hover:text-accent', }, size: { diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index d12917f..4df9dc2 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -135,7 +135,7 @@ export function DrawerFrame({ leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
) { +}: Omit, 'type'>) { const rangeStyle: RangeStyle = { ...style, '--range-progress': `${resolveRangeProgress(props)}%`, diff --git a/src/components/ui/search-field.tsx b/src/components/ui/search-field.tsx index be3b353..6a5b44a 100644 --- a/src/components/ui/search-field.tsx +++ b/src/components/ui/search-field.tsx @@ -1,6 +1,6 @@ import type { InputHTMLAttributes, ReactNode } from 'react'; import { cn } from './cn'; -import { focusRing, motionColors } from './tokens'; +import { motionColors } from './tokens'; export function SearchField({ icon, @@ -16,7 +16,6 @@ export function SearchField({ className={cn( 'flex min-w-0 items-center gap-1.5 rounded-md border border-line bg-surface-sunken px-2 py-1', 'focus-within:border-accent-line focus-within:ring-1 focus-within:ring-accent-line hover:border-accent-line', - focusRing, motionColors, className, )} diff --git a/src/components/ui/segmented-control.tsx b/src/components/ui/segmented-control.tsx index fbfcba1..9033cc1 100644 --- a/src/components/ui/segmented-control.tsx +++ b/src/components/ui/segmented-control.tsx @@ -1,4 +1,6 @@ -import type { ReactNode } from 'react'; +'use client'; + +import { useRef, type KeyboardEvent, type ReactNode } from 'react'; import { cn } from './cn'; import { focusRing, motionColors } from './tokens'; @@ -25,17 +27,54 @@ export function SegmentedControl({ ariaLabel: string; className?: string; }) { + const buttonRefs = useRef>([]); + + const focusOption = (index: number) => { + const option = options[index]; + if (!option) return; + onChange(option.value); + buttonRefs.current[index]?.focus(); + }; + + const handleKeyDown = (event: KeyboardEvent, index: number) => { + switch (event.key) { + case 'ArrowLeft': + case 'ArrowUp': + event.preventDefault(); + focusOption((index - 1 + options.length) % options.length); + break; + case 'ArrowRight': + case 'ArrowDown': + event.preventDefault(); + focusOption((index + 1) % options.length); + break; + case 'Home': + event.preventDefault(); + focusOption(0); + break; + case 'End': + event.preventDefault(); + focusOption(options.length - 1); + break; + default: + break; + } + }; + return (
- {options.map((option) => { + {options.map((option, index) => { const active = value === option.value; return (