@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 +571 -0
- package/dist/index.cjs +1309 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +1300 -0
- package/package.json +50 -0
- package/src/command-score.ts +171 -0
- package/src/command.tsx +482 -0
- package/src/components/command-divider.tsx +30 -0
- package/src/components/command-empty.tsx +30 -0
- package/src/components/command-footer.tsx +22 -0
- package/src/components/command-group.tsx +76 -0
- package/src/components/command-input.tsx +66 -0
- package/src/components/command-item.tsx +165 -0
- package/src/components/command-list.tsx +77 -0
- package/src/components/command-loading.tsx +30 -0
- package/src/components/command-tabs.tsx +20 -0
- package/src/components/command-value.tsx +23 -0
- package/src/components/index.ts +10 -0
- package/src/context/command-context.ts +5 -0
- package/src/context/create-command-context.ts +140 -0
- package/src/context/index.ts +2 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-as-ref.ts +12 -0
- package/src/hooks/use-command-state.ts +18 -0
- package/src/hooks/use-command.ts +10 -0
- package/src/hooks/use-schedule-layout-effect.ts +19 -0
- package/src/hooks/use-value.ts +39 -0
- package/src/index.ts +31 -0
- package/src/store/index.ts +1 -0
- package/src/tv.ts +248 -0
- package/src/types.ts +84 -0
- package/src/utils/constants.ts +7 -0
- package/src/utils/dom.ts +19 -0
- package/src/utils/helpers.ts +45 -0
- package/src/utils/index.ts +3 -0
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
|