@choice-ui/command 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,571 @@
1
+ # Command
2
+
3
+ A sophisticated, enterprise-grade command palette component that provides fast search, keyboard navigation, and flexible content organization. Built with performance, accessibility, and extensibility at its core.
4
+
5
+ ## Import
6
+
7
+ ```tsx
8
+ import { Command } from "@choice-ui/react"
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Advanced Fuzzy Search** - Multi-factor scoring with prefix, word boundary, and substring matching
14
+ - **Complete Keyboard Navigation** - Arrow keys, vim bindings, group navigation, and meta shortcuts
15
+ - **Compound Architecture** - 10+ sub-components for maximum flexibility
16
+ - **Dialog Integration** - Built-in modal mode with focus management
17
+ - **Tab Filtering** - Category-based filtering with search preservation
18
+ - **Async Support** - Loading states, error handling, and progressive enhancement
19
+ - **Virtual Scrolling** - High-performance rendering of large datasets
20
+ - **Custom Filtering** - Pluggable filter functions with scoring interface
21
+ - **Accessibility First** - ARIA compliance, screen reader support, and keyboard-only navigation
22
+ - **Performance Optimized** - Memoization, selective re-renders, and efficient DOM manipulation
23
+
24
+ ## Architecture
25
+
26
+ ### Core Components
27
+
28
+ - `Command` - Root container with state management
29
+ - `Command.Input` - Search input with autocomplete
30
+ - `Command.List` - Virtualized scrollable container
31
+ - `Command.Item` - Individual selectable items
32
+ - `Command.Group` - Categorization with headings
33
+ - `Command.Empty` - No results state
34
+ - `Command.Loading` - Async loading indicator
35
+ - `Command.Divider` - Visual separators
36
+ - `Command.Footer` - Action bar/status area
37
+ - `Command.Tabs` - Integrated filtering tabs
38
+ - `Command.Value` - Display component for values
39
+
40
+ ## Usage
41
+
42
+ ### Basic Structure
43
+
44
+ ```tsx
45
+ <Command>
46
+ <Command.Input placeholder="Type a command..." />
47
+ <Command.List>
48
+ <Command.Empty>No results found.</Command.Empty>
49
+ <Command.Group heading="Actions">
50
+ <Command.Item>New File</Command.Item>
51
+ <Command.Item>Open File</Command.Item>
52
+ </Command.Group>
53
+ </Command.List>
54
+ </Command>
55
+ ```
56
+
57
+ ### With Rich Content
58
+
59
+ ```tsx
60
+ <Command>
61
+ <Command.Input placeholder="Search..." />
62
+ <Command.List>
63
+ <Command.Group heading="Files">
64
+ <Command.Item
65
+ value="new-file"
66
+ prefixElement={<FileIcon />}
67
+ suffixElement={<Badge>New</Badge>}
68
+ shortcut={{ keys: "N", modifier: "command" }}
69
+ >
70
+ <Command.Value>New File</Command.Value>
71
+ </Command.Item>
72
+ </Command.Group>
73
+ </Command.List>
74
+ </Command>
75
+ ```
76
+
77
+ ### Dialog Mode
78
+
79
+ ```tsx
80
+ const [open, setOpen] = useState(false)
81
+
82
+ <Dialog open={open} onOpenChange={setOpen}>
83
+ <Dialog.Content>
84
+ <Command loop size="large">
85
+ <Command.Input placeholder="Search commands..." />
86
+ <Command.List>
87
+ <Command.Item onSelect={() => setOpen(false)}>
88
+ Action 1
89
+ </Command.Item>
90
+ </Command.List>
91
+ </Command>
92
+ </Dialog.Content>
93
+ </Dialog>
94
+ ```
95
+
96
+ ### With Tabs
97
+
98
+ ```tsx
99
+ <Command>
100
+ <Command.Input />
101
+ <Command.Tabs
102
+ value={activeTab}
103
+ onChange={setActiveTab}
104
+ >
105
+ <Command.TabItem value="all">All</Command.TabItem>
106
+ <Command.TabItem value="files">Files</Command.TabItem>
107
+ <Command.TabItem value="actions">Actions</Command.TabItem>
108
+ </Command.Tabs>
109
+ <Command.List>{/* Items filtered by active tab */}</Command.List>
110
+ </Command>
111
+ ```
112
+
113
+ ### Controlled State
114
+
115
+ ```tsx
116
+ const [value, setValue] = useState("")
117
+ const [search, setSearch] = useState("")
118
+
119
+ <Command value={value} onChange={setValue}>
120
+ <Command.Input
121
+ value={search}
122
+ onChange={setSearch}
123
+ />
124
+ <Command.List>
125
+ <Command.Item value="item1">Item 1</Command.Item>
126
+ </Command.List>
127
+ </Command>
128
+ ```
129
+
130
+ ### Async Loading
131
+
132
+ ```tsx
133
+ <Command>
134
+ <Command.Input />
135
+ <Command.List>
136
+ {loading && <Command.Loading>Fetching results...</Command.Loading>}
137
+
138
+ {error && <Command.Empty>Error: {error.message}</Command.Empty>}
139
+
140
+ {data?.map((item) => (
141
+ <Command.Item
142
+ key={item.id}
143
+ value={item.id}
144
+ >
145
+ {item.name}
146
+ </Command.Item>
147
+ ))}
148
+ </Command.List>
149
+ </Command>
150
+ ```
151
+
152
+ ### Custom Filtering
153
+
154
+ ```tsx
155
+ const customFilter = (value: string, search: string) => {
156
+ if (!search) return 1
157
+ // Custom scoring logic
158
+ return value.toLowerCase().includes(search.toLowerCase()) ? 0.8 : 0
159
+ }
160
+
161
+ ;<Command filter={customFilter}>
162
+ <Command.Input />
163
+ <Command.List>
164
+ <Command.Item>Custom filtered item</Command.Item>
165
+ </Command.List>
166
+ </Command>
167
+ ```
168
+
169
+ ### With Keywords
170
+
171
+ ```tsx
172
+ <Command.Item
173
+ value="javascript-file"
174
+ keywords={["js", "script", "code", "typescript"]}
175
+ >
176
+ app.js
177
+ </Command.Item>
178
+ ```
179
+
180
+ ## Props
181
+
182
+ ### Command (Root)
183
+
184
+ ```ts
185
+ interface CommandProps {
186
+ /** Controlled selected value */
187
+ value?: string
188
+
189
+ /** Default selected value (uncontrolled) */
190
+ defaultValue?: string
191
+
192
+ /** Selection change handler */
193
+ onChange?: (value: string) => void
194
+
195
+ /** Custom filter function */
196
+ filter?: (value: string, search: string, keywords?: string[]) => number
197
+
198
+ /** Enable/disable automatic filtering */
199
+ shouldFilter?: boolean
200
+
201
+ /** Enable wraparound navigation at boundaries */
202
+ loop?: boolean
203
+
204
+ /** Disable mouse selection (keyboard only) */
205
+ disablePointerSelection?: boolean
206
+
207
+ /** Enable vim-style navigation (Ctrl+N/J/P/K) */
208
+ vimBindings?: boolean
209
+
210
+ /** Size variant */
211
+ size?: "default" | "large"
212
+
213
+ /** Theme variant */
214
+ variant?: "default" | "dark"
215
+
216
+ /** Screen reader label */
217
+ label?: string
218
+
219
+ /** Key handler for global shortcuts */
220
+ onKeyDown?: (event: React.KeyboardEvent) => void
221
+ }
222
+ ```
223
+
224
+ ### Command.Item
225
+
226
+ ```ts
227
+ interface CommandItemProps {
228
+ /** Value for selection and filtering */
229
+ value?: string
230
+
231
+ /** Additional search keywords */
232
+ keywords?: string[]
233
+
234
+ /** Leading icon or element */
235
+ prefixElement?: ReactNode
236
+
237
+ /** Trailing element */
238
+ suffixElement?: ReactNode
239
+
240
+ /** Keyboard shortcut display */
241
+ shortcut?: {
242
+ keys?: ReactNode
243
+ modifier?: KbdKey | KbdKey[]
244
+ }
245
+
246
+ /** Disable item selection */
247
+ disabled?: boolean
248
+
249
+ /** Always render (skip filtering) */
250
+ forceMount?: boolean
251
+
252
+ /** Selection callback */
253
+ onSelect?: (value: string) => void
254
+ }
255
+ ```
256
+
257
+ ### Command.Group
258
+
259
+ ```ts
260
+ interface CommandGroupProps {
261
+ /** Group heading */
262
+ heading?: ReactNode
263
+
264
+ /** Group identifier */
265
+ value?: string
266
+
267
+ /** Always render (skip filtering) */
268
+ forceMount?: boolean
269
+ }
270
+ ```
271
+
272
+ ### Command.Input
273
+
274
+ ```ts
275
+ interface CommandInputProps extends InputProps {
276
+ /** Controlled search value */
277
+ value?: string
278
+
279
+ /** Search change handler */
280
+ onChange?: (search: string) => void
281
+
282
+ /** Leading element */
283
+ prefixElement?: ReactNode
284
+
285
+ /** Trailing element */
286
+ suffixElement?: ReactNode
287
+ }
288
+ ```
289
+
290
+ ### Command.Tabs
291
+
292
+ ```ts
293
+ interface CommandTabsProps {
294
+ /** Active tab value */
295
+ value?: string
296
+
297
+ /** Tab change handler */
298
+ onChange?: (value: string) => void
299
+ }
300
+ ```
301
+
302
+ ## Keyboard Navigation
303
+
304
+ ### Basic Navigation
305
+
306
+ - `↑` `↓` - Navigate between items
307
+ - `Enter` - Select current item
308
+ - `Home` - Jump to first item
309
+ - `End` - Jump to last item
310
+
311
+ ### Vim Bindings (optional)
312
+
313
+ - `Ctrl+J` - Next item (same as ↓)
314
+ - `Ctrl+K` - Previous item (same as ↑)
315
+ - `Ctrl+N` - Next item
316
+ - `Ctrl+P` - Previous item
317
+
318
+ ### Advanced Navigation
319
+
320
+ - `Alt+↑` `Alt+↓` - Navigate between groups
321
+ - `Cmd+↑` `Cmd+↓` - Jump to first/last item (Mac)
322
+ - `←` `→` - Switch tabs (when tabs are present)
323
+
324
+ ### IME Support
325
+
326
+ - Full support for Chinese, Japanese, Korean input methods
327
+ - Composition events handled correctly
328
+ - No interference with typing flow
329
+
330
+ ## Search Algorithm
331
+
332
+ The component uses a sophisticated fuzzy search algorithm that scores matches based on:
333
+
334
+ 1. **Exact Match** (1.0) - Perfect string match
335
+ 2. **Prefix Match** (0.9) - Search term at start of value
336
+ 3. **Word Boundary** (0.8) - Search term at start of any word
337
+ 4. **Keyword Match** (0.7) - Match in associated keywords
338
+ 5. **Substring Match** (0.6) - Search term anywhere in value
339
+ 6. **Fuzzy Match** (0.1-0.5) - Character sequence matching
340
+
341
+ ### Scoring Factors
342
+
343
+ - Case sensitivity bonus
344
+ - Distance between matched characters
345
+ - Match position weighting
346
+ - Keyword alias support
347
+
348
+ ## Advanced Usage
349
+
350
+ ### Conditional Items
351
+
352
+ ```tsx
353
+ const ConditionalItem = ({ children, ...props }) => {
354
+ const search = useCommandState((state) => state.search)
355
+ if (!search) return null
356
+ return <Command.Item {...props}>{children}</Command.Item>
357
+ }
358
+ ```
359
+
360
+ ### Nested Navigation
361
+
362
+ ```tsx
363
+ const [pages, setPages] = useState([])
364
+ const page = pages[pages.length - 1]
365
+
366
+ <Command onKeyDown={e => {
367
+ if (e.key === 'Escape') {
368
+ setPages(pages => pages.slice(0, -1))
369
+ }
370
+ }}>
371
+ {!page && (
372
+ <Command.Item onSelect={() => setPages([...pages, 'projects'])}>
373
+ Browse projects...
374
+ </Command.Item>
375
+ )}
376
+
377
+ {page === 'projects' && (
378
+ <Command.Group heading="Projects">
379
+ <Command.Item>Project A</Command.Item>
380
+ </Command.Group>
381
+ )}
382
+ </Command>
383
+ ```
384
+
385
+ ### Performance with Large Datasets
386
+
387
+ ```tsx
388
+ // For 1000+ items
389
+ <Command>
390
+ <Command.Input />
391
+ <Command.List className="max-h-64">
392
+ {" "}
393
+ {/* Fixed height enables virtualization */}
394
+ {largeDataset.map((item) => (
395
+ <Command.Item
396
+ key={item.id}
397
+ value={`${item.name} ${item.description}`}
398
+ >
399
+ {item.name}
400
+ </Command.Item>
401
+ ))}
402
+ </Command.List>
403
+ </Command>
404
+ ```
405
+
406
+ ## State Management
407
+
408
+ ### Internal State Structure
409
+
410
+ ```ts
411
+ interface CommandState {
412
+ search: string // Current search query
413
+ value: string // Selected item value
414
+ selectedItemId: string // DOM id of selected item
415
+ filtered: {
416
+ count: number // Number of visible items
417
+ items: Map<string, number> // Item scores
418
+ groups: Set<string> // Visible groups
419
+ }
420
+ }
421
+ ```
422
+
423
+ ### Custom Hooks
424
+
425
+ ```tsx
426
+ import { useCommandState } from "./hooks"
427
+
428
+ function MyComponent() {
429
+ const search = useCommandState((state) => state.search)
430
+ const selectedValue = useCommandState((state) => state.value)
431
+
432
+ // Component logic
433
+ }
434
+ ```
435
+
436
+ ## Best Practices
437
+
438
+ ### Performance
439
+
440
+ - Use fixed heights on Command.List for virtual scrolling
441
+ - Implement custom filter functions for complex logic
442
+ - Memoize expensive item content
443
+ - Use `forceMount` sparingly
444
+
445
+ ### Accessibility
446
+
447
+ - Provide meaningful `value` props for all items
448
+ - Use semantic group headings
449
+ - Include keyboard shortcuts in UI
450
+ - Test with screen readers
451
+
452
+ ### UX Guidelines
453
+
454
+ - Keep search responsive (< 100ms)
455
+ - Show loading states for async operations
456
+ - Provide empty states with helpful messages
457
+ - Use consistent iconography and spacing
458
+
459
+ ### Search Optimization
460
+
461
+ - Include relevant keywords for better matching
462
+ - Use descriptive values that users would expect
463
+ - Consider abbreviations and acronyms
464
+ - Test search with real user queries
465
+
466
+ ## Styling
467
+
468
+ The component uses Tailwind Variants with comprehensive slots:
469
+
470
+ - `root` - Main container
471
+ - `input` - Search input styling
472
+ - `list` - Scrollable list container
473
+ - `item` - Individual items
474
+ - `group` - Group containers
475
+ - `heading` - Group headings
476
+ - `empty` - Empty state
477
+ - `loading` - Loading state
478
+
479
+ Customize with className overrides or modify the theme configuration.
480
+
481
+ ## Examples
482
+
483
+ ### Command Palette
484
+
485
+ ```tsx
486
+ function AppCommandPalette() {
487
+ const [open, setOpen] = useState(false)
488
+
489
+ useEffect(() => {
490
+ const down = (e) => {
491
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
492
+ e.preventDefault()
493
+ setOpen(true)
494
+ }
495
+ }
496
+ document.addEventListener("keydown", down)
497
+ return () => document.removeEventListener("keydown", down)
498
+ }, [])
499
+
500
+ return (
501
+ <Dialog
502
+ open={open}
503
+ onOpenChange={setOpen}
504
+ >
505
+ <Dialog.Content>
506
+ <Command
507
+ loop
508
+ vimBindings
509
+ >
510
+ <Command.Input placeholder="Type a command..." />
511
+ <Command.List>
512
+ <Command.Group heading="File">
513
+ <Command.Item onSelect={() => newFile()}>
514
+ New File
515
+ <Kbd keys="command">N</Kbd>
516
+ </Command.Item>
517
+ </Command.Group>
518
+ </Command.List>
519
+ </Command>
520
+ </Dialog.Content>
521
+ </Dialog>
522
+ )
523
+ }
524
+ ```
525
+
526
+ ### File Browser
527
+
528
+ ```tsx
529
+ function FileBrowser({ files }) {
530
+ const [search, setSearch] = useState("")
531
+
532
+ return (
533
+ <Command shouldFilter={false}>
534
+ <Command.Input
535
+ value={search}
536
+ onChange={setSearch}
537
+ placeholder="Search files..."
538
+ />
539
+ <Command.List>
540
+ <Command.Group heading="Recent Files">
541
+ {files
542
+ .filter((file) => file.name.includes(search))
543
+ .map((file) => (
544
+ <Command.Item
545
+ key={file.id}
546
+ value={file.id}
547
+ >
548
+ <FileIcon type={file.type} />
549
+ <div>
550
+ <div>{file.name}</div>
551
+ <div className="text-body-small text-gray-500">
552
+ {file.size} • {file.modified}
553
+ </div>
554
+ </div>
555
+ </Command.Item>
556
+ ))}
557
+ </Command.Group>
558
+ </Command.List>
559
+ </Command>
560
+ )
561
+ }
562
+ ```
563
+
564
+ ## Technical Notes
565
+
566
+ - Built on React 18+ with concurrent features
567
+ - Uses `useSyncExternalStore` for optimal performance
568
+ - Implements proper focus management and restoration
569
+ - ResizeObserver integration for responsive behavior
570
+ - Supports server-side rendering with hydration safety
571
+ - Extensive TypeScript coverage with strict types