Skip to content
This repository was archived by the owner on Feb 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<kbd>` 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 `<input type="checkbox">` 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.
21 changes: 20 additions & 1 deletion src/components/AppCatalog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useSearchShortcut } from '@/hooks/useSearchShortcut';
import {
Search,
Loader2,
Expand All @@ -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';
Expand Down Expand Up @@ -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<string | null>(null);
const [bulkDownloading, setBulkDownloading] = useState(false);
Expand Down Expand Up @@ -465,11 +468,27 @@ export function AppCatalog({ onSelect, onBulkSelect }: AppCatalogProps) {
<div className="relative flex-1 group">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground group-focus-within:text-primary transition-all duration-300" />
<Input
ref={searchInputRef}
placeholder="Search apps, vendors, categories..."
className="pl-12 h-14 bg-muted/40 border-border/40 focus:bg-background transition-all duration-300 rounded-2xl ring-offset-background text-base shadow-inner focus:ring-2 focus:ring-primary/20"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{search ? (
<button
onClick={() => setSearch('')}
className="absolute right-4 top-1/2 -translate-y-1/2 p-1.5 rounded-xl hover:bg-muted text-muted-foreground hover:text-foreground transition-all active:scale-90"
aria-label="Clear search"
>
<X className="h-5 w-5" />
</button>
) : (
<div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none hidden md:flex items-center gap-1">
<kbd className="h-6 px-2 rounded-lg border border-border/60 bg-background/50 text-[10px] font-black text-muted-foreground/60 flex items-center justify-center shadow-sm">
/
</kbd>
</div>
)}
</div>

<div className="flex items-center bg-muted/40 p-1.5 rounded-2xl border border-border/40 shadow-inner">
Expand Down
20 changes: 19 additions & 1 deletion src/components/MyPackages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<Set<string>>(new Set());
const headerCheckboxRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -105,11 +107,27 @@ export function MyPackages({ onEdit }: MyPackagesProps) {
<div className="relative group w-full md:max-w-md">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground group-focus-within:text-primary transition-colors" />
<Input
ref={searchInputRef}
placeholder="Search your packages, vendors..."
className="pl-12 h-14 bg-muted/40 border-border/40 focus:bg-background transition-all rounded-2xl text-base shadow-inner focus:ring-2 focus:ring-primary/20"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{search ? (
<button
onClick={() => setSearch('')}
className="absolute right-4 top-1/2 -translate-y-1/2 p-1.5 rounded-xl hover:bg-muted text-muted-foreground hover:text-foreground transition-all active:scale-90"
aria-label="Clear search"
>
<X className="h-5 w-5" />
</button>
) : (
<div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none hidden md:flex items-center gap-1">
<kbd className="h-6 px-2 rounded-lg border border-border/60 bg-background/50 text-[10px] font-black text-muted-foreground/60 flex items-center justify-center shadow-sm">
/
</kbd>
</div>
)}
</div>

<div className="flex items-center bg-muted/40 p-1.5 rounded-2xl border border-border/40 shadow-inner">
Expand Down
16 changes: 16 additions & 0 deletions src/hooks/useSearchShortcut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useRef } from 'react';

export function useSearchShortcut() {
const ref = useRef<HTMLInputElement>(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;
}