diff --git a/.Jules/palette.md b/.Jules/palette.md index dd0197d..1924d9c 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -2,6 +2,10 @@ **Learning:** Adding shadcn/ui `Tooltip` components to improve icon-only button accessibility requires wrapping the component tree in `TooltipProvider`. While this is often handled at the root in the main app, it must be explicitly included in unit tests that render these components to avoid runtime errors. **Action:** When adding tooltips to components, ensure related tests are updated to include a `TooltipProvider` in the render wrapper. +## 2025-05-16 - [Keyboard Shortcuts for Search Accessibility] +**Learning:** Adding a global keyboard shortcut (like '/') to focus the search input significantly improves the experience for keyboard-centric users. Abstracting this logic into a custom hook ensures consistency across different views (App Catalog, My Packages) and simplifies component code. +**Action:** Use the `useSearchShortcut` hook for any new search-heavy views and provide a visual hint (like a `` tag) to discover the shortcut. + ## 2025-05-16 - [Accessible Selection Pattern] **Learning:** Custom selection indicators (like those built with `div` and icons) often lack keyboard accessibility and screen reader support. Replacing these with a hidden but semantic `` that overlays the custom visual maintains the design while providing native accessibility features (tab focus, space/enter toggle, and state announcement). For "select all" headers, ensure the native `indeterminate` property is set via a `ref` so screen readers accurately announce the partial selection state. **Action:** Use hidden native checkboxes for custom selection UIs and manage `indeterminate` state via React refs for header checkboxes. diff --git a/src/components/AppCatalog.tsx b/src/components/AppCatalog.tsx index f4b3c3c..a40f5fd 100644 --- a/src/components/AppCatalog.tsx +++ b/src/components/AppCatalog.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useSearchShortcut } from '@/hooks/useSearchShortcut'; import { Search, Loader2, @@ -18,7 +19,8 @@ import { Package, PlusCircle, Info, - FileText + FileText, + X } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; @@ -52,6 +54,7 @@ const getCategoryIcon = (category: string) => { export function AppCatalog({ onSelect, onBulkSelect }: AppCatalogProps) { const { addConfigs } = usePackage(); const [search, setSearch] = useState(''); + const searchInputRef = useSearchShortcut(); const [selectedCategory, setSelectedCategory] = useState('All'); const [downloading, setDownloading] = useState(null); const [bulkDownloading, setBulkDownloading] = useState(false); @@ -465,11 +468,27 @@ export function AppCatalog({ onSelect, onBulkSelect }: AppCatalogProps) {
setSearch(e.target.value)} /> + {search ? ( + + ) : ( +
+ + / + +
+ )}
diff --git a/src/components/MyPackages.tsx b/src/components/MyPackages.tsx index 2544087..45134aa 100644 --- a/src/components/MyPackages.tsx +++ b/src/components/MyPackages.tsx @@ -5,7 +5,8 @@ import { usePackage } from '@/contexts/PackageContext'; import { exportConfig, importConfig } from '@/lib/package-config'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Input } from '@/components/ui/input'; -import { useState, useRef, useEffect } from 'react'; +import { useState } from 'react'; +import { useSearchShortcut } from '@/hooks/useSearchShortcut'; import { cn } from '@/lib/utils'; import type { PackageConfig } from '@/lib/package-config'; @@ -25,6 +26,7 @@ export function MyPackages({ onEdit }: MyPackagesProps) { } = usePackage(); const [search, setSearch] = useState(''); + const searchInputRef = useSearchShortcut(); const [viewMode, setViewMode] = useState<'grid' | 'table'>('table'); const [selectedIds, setSelectedIds] = useState>(new Set()); const headerCheckboxRef = useRef(null); @@ -105,11 +107,27 @@ export function MyPackages({ onEdit }: MyPackagesProps) {
setSearch(e.target.value)} /> + {search ? ( + + ) : ( +
+ + / + +
+ )}
diff --git a/src/hooks/useSearchShortcut.ts b/src/hooks/useSearchShortcut.ts new file mode 100644 index 0000000..6a3cbd8 --- /dev/null +++ b/src/hooks/useSearchShortcut.ts @@ -0,0 +1,16 @@ +import { useEffect, useRef } from 'react'; + +export function useSearchShortcut() { + const ref = useRef(null); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) { + e.preventDefault(); + ref.current?.focus(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + return ref; +}