ajo-ui 0.1.0

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.
Files changed (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +126 -0
  3. package/dist/accordion.js +130 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/calendar.js +2 -0
  6. package/dist/carousel.js +239 -0
  7. package/dist/chart.js +790 -0
  8. package/dist/checkbox-group.js +70 -0
  9. package/dist/checkbox.js +77 -0
  10. package/dist/chunks/bar-CVafh6C1.js +99 -0
  11. package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
  12. package/dist/chunks/collection-DtRB63U4.js +111 -0
  13. package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
  14. package/dist/chunks/menu-B45IyHHC.js +704 -0
  15. package/dist/chunks/native-BJdhd9XJ.js +20 -0
  16. package/dist/chunks/popup-C8Bb3l_g.js +459 -0
  17. package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
  18. package/dist/chunks/position-D6_i_SRn.js +434 -0
  19. package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
  20. package/dist/collapsible.js +106 -0
  21. package/dist/command.js +263 -0
  22. package/dist/context-menu.js +112 -0
  23. package/dist/data-table.js +5 -0
  24. package/dist/dialog.js +207 -0
  25. package/dist/direction.js +22 -0
  26. package/dist/drawer.js +139 -0
  27. package/dist/field.js +26 -0
  28. package/dist/index.js +38 -0
  29. package/dist/input-date.js +994 -0
  30. package/dist/input-group.js +52 -0
  31. package/dist/input-otp.js +179 -0
  32. package/dist/menu.js +2 -0
  33. package/dist/menubar.js +236 -0
  34. package/dist/message-scroller.js +446 -0
  35. package/dist/navigation-menu.js +330 -0
  36. package/dist/popover.js +307 -0
  37. package/dist/progress.js +39 -0
  38. package/dist/radio-group.js +107 -0
  39. package/dist/resizable.js +172 -0
  40. package/dist/select.js +961 -0
  41. package/dist/sidebar.js +343 -0
  42. package/dist/slider.js +259 -0
  43. package/dist/switch.js +53 -0
  44. package/dist/tabs.js +182 -0
  45. package/dist/toast.js +492 -0
  46. package/dist/toggle-group.js +111 -0
  47. package/dist/toggle.js +52 -0
  48. package/dist/toolbar.js +127 -0
  49. package/dist/tooltip.js +196 -0
  50. package/dist/utils.js +104 -0
  51. package/dist/virtual-list.js +2 -0
  52. package/package.json +250 -0
  53. package/src/accordion.tsx +261 -0
  54. package/src/availability.ts +261 -0
  55. package/src/avatar.tsx +99 -0
  56. package/src/bar.ts +156 -0
  57. package/src/calendar.tsx +1441 -0
  58. package/src/carousel.tsx +424 -0
  59. package/src/chart.tsx +1194 -0
  60. package/src/checkbox-group.tsx +132 -0
  61. package/src/checkbox.tsx +130 -0
  62. package/src/collapsible.tsx +188 -0
  63. package/src/collection.ts +154 -0
  64. package/src/command.tsx +511 -0
  65. package/src/context-menu.tsx +233 -0
  66. package/src/data-table-contract.ts +143 -0
  67. package/src/data-table-model.ts +760 -0
  68. package/src/data-table.tsx +475 -0
  69. package/src/dialog.tsx +393 -0
  70. package/src/direction.tsx +45 -0
  71. package/src/drawer.tsx +251 -0
  72. package/src/field.tsx +61 -0
  73. package/src/index.ts +37 -0
  74. package/src/input-date.tsx +1539 -0
  75. package/src/input-group.tsx +142 -0
  76. package/src/input-otp.tsx +324 -0
  77. package/src/menu-cluster.ts +124 -0
  78. package/src/menu.tsx +1095 -0
  79. package/src/menubar.tsx +459 -0
  80. package/src/message-scroller.tsx +732 -0
  81. package/src/native.ts +26 -0
  82. package/src/navigation-menu.tsx +578 -0
  83. package/src/popover.tsx +519 -0
  84. package/src/popup-surface.tsx +31 -0
  85. package/src/popup.ts +569 -0
  86. package/src/position.ts +523 -0
  87. package/src/progress.tsx +70 -0
  88. package/src/radio-group.tsx +186 -0
  89. package/src/resizable.tsx +310 -0
  90. package/src/segments.ts +922 -0
  91. package/src/select.tsx +1501 -0
  92. package/src/sidebar.tsx +683 -0
  93. package/src/slider.tsx +424 -0
  94. package/src/switch.tsx +104 -0
  95. package/src/tabs.tsx +314 -0
  96. package/src/toast.tsx +923 -0
  97. package/src/toggle-group.tsx +249 -0
  98. package/src/toggle.tsx +91 -0
  99. package/src/toolbar.tsx +212 -0
  100. package/src/tooltip.tsx +359 -0
  101. package/src/utils.ts +204 -0
  102. package/src/virtual-list.tsx +205 -0
  103. package/src/virtual.ts +385 -0
@@ -0,0 +1,511 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import { announce, callHandler, controlled, dom, id, listen, roving, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+ import type { FixedArgs, OmitArg } from './utils'
5
+ import { defaultResultsLabel, flag, matchesTokens, resolveFilter, text } from './utils'
6
+ import { collection } from './collection'
7
+ import {
8
+ Dialog,
9
+ DialogClose,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogTitle,
13
+ type DialogContentArgs,
14
+ } from './dialog'
15
+
16
+ /** Predicate used to match a command item against the current search. */
17
+ export type CommandFilter = (value: string, search: string, keywords: string[]) => boolean
18
+
19
+ /** Arguments for the searchable Command collection root. */
20
+ export type CommandArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'onchange'> & {
21
+ /** Controlled selected item value. */
22
+ value?: string
23
+ /** Initial selected item value for uncontrolled usage. */
24
+ defaultValue?: string
25
+ /** Controlled search query. */
26
+ search?: string
27
+ /** Initial search query for uncontrolled usage. */
28
+ defaultSearch?: string
29
+ /** Disable every command item and input. */
30
+ disabled?: boolean
31
+ /** Item filter; null disables internal filtering for externally driven lists. */
32
+ filter?: CommandFilter | null
33
+ /** Wrap keyboard navigation at the ends. Default: true. */
34
+ loop?: boolean
35
+ /** Called whenever the selected item value changes. */
36
+ onValueChange?: (value: string, event?: Event) => void
37
+ /** Called whenever the search query changes. */
38
+ onSearchChange?: (search: string, event?: Event) => void
39
+ /** Screen-reader message for filtered result counts. */
40
+ resultsLabel?: (count: number) => string
41
+ /** Additional UnoCSS classes. */
42
+ class?: string
43
+ }> & FixedArgs<'onchange'>
44
+
45
+ /** Arguments for a Dialog that owns a Command surface. */
46
+ export type CommandDialogArgs = WithChildren<OmitArg<IntrinsicElements['dialog'], 'open'> & {
47
+ /** Controlled dialog open state. */
48
+ open?: boolean
49
+ /** Initial open state for uncontrolled usage. */
50
+ defaultOpen?: boolean
51
+ /** Called whenever the dialog opens or closes. */
52
+ onOpenChange?: (open: boolean, event?: Event) => void
53
+ /** Called when Escape requests dialog close. Prevent default to keep it open. */
54
+ onEscapeKeyDown?: DialogContentArgs['onEscapeKeyDown']
55
+ /** Called when the native backdrop is clicked. Prevent default to keep it open. */
56
+ onPointerDownOutside?: DialogContentArgs['onPointerDownOutside']
57
+ /** Accessible dialog title. */
58
+ title?: string
59
+ /** Accessible dialog description. */
60
+ description?: string
61
+ /** Show the default close button. */
62
+ showCloseButton?: boolean
63
+ /** Accessible label for the default close button. */
64
+ closeLabel?: string
65
+ /** Additional UnoCSS classes for dialog content. */
66
+ class?: string
67
+ commandClass?: string
68
+ closeClass?: string
69
+ closeIconClass?: string
70
+ descriptionClass?: string
71
+ titleClass?: string
72
+ }>
73
+
74
+ /** Arguments for the search input bound to a Command root. */
75
+ export type CommandInputArgs = OmitArg<IntrinsicElements['input'], 'onchange'> & {
76
+ /** Controlled input value. Prefer Command `search` for root-level control. */
77
+ value?: string
78
+ /** Called whenever the input value changes. */
79
+ onValueChange?: (value: string, event: Event) => void
80
+ /** Additional UnoCSS classes. */
81
+ class?: string
82
+ iconClass?: string
83
+ wrapperClass?: string
84
+ } & FixedArgs<'onchange'>
85
+
86
+ /** Arguments for the Command listbox container. */
87
+ export type CommandListArgs = WithChildren<IntrinsicElements['div'] & {
88
+ /** Additional UnoCSS classes. */
89
+ class?: string
90
+ }>
91
+
92
+ /** Arguments for content shown when no command item matches. */
93
+ export type CommandEmptyArgs = WithChildren<IntrinsicElements['div'] & {
94
+ /** Additional UnoCSS classes. */
95
+ class?: string
96
+ }>
97
+
98
+ /** Arguments for a labelled group of command items. */
99
+ export type CommandGroupArgs = WithChildren<IntrinsicElements['div'] & {
100
+ /** Group heading. */
101
+ heading?: string
102
+ /** Keep the group mounted and visible even when all child items are filtered out. */
103
+ forceMount?: boolean
104
+ /** Additional UnoCSS classes. */
105
+ class?: string
106
+ headingClass?: string
107
+ }>
108
+
109
+ /** Arguments for a visual separator between command groups. */
110
+ export type CommandSeparatorArgs = IntrinsicElements['div'] & {
111
+ /** Additional UnoCSS classes. */
112
+ class?: string
113
+ }
114
+
115
+ /** Arguments for one selectable and filterable command item. */
116
+ export type CommandItemArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'value'> & {
117
+ /** Stable value used for filtering, selection, and onSelect. */
118
+ value?: string
119
+ /** Extra searchable terms. */
120
+ keywords?: string[]
121
+ /** Keep mounted and visible even when it does not match the search query. */
122
+ forceMount?: boolean
123
+ /** Disable activation. */
124
+ disabled?: boolean
125
+ /** Called when this command item is selected by click or Enter. */
126
+ onSelect?: (value: string, event: Event) => void
127
+ /** Additional UnoCSS classes. */
128
+ class?: string
129
+ }>
130
+
131
+ /** Arguments for shortcut text displayed beside a command item. */
132
+ export type CommandShortcutArgs = WithChildren<IntrinsicElements['span'] & {
133
+ /** Additional UnoCSS classes. */
134
+ class?: string
135
+ }>
136
+
137
+ type CommandContextValue = {
138
+ activeId: string
139
+ disabled: boolean
140
+ itemId: (value: string) => string
141
+ listId: string
142
+ matches: (value: string, keywords?: string[]) => boolean
143
+ search: string
144
+ setValue: (value: string, event?: Event) => void
145
+ setSearch: (search: string, event: Event) => void
146
+ value: string
147
+ }
148
+
149
+ const CommandContext = context<CommandContextValue | null>(null)
150
+
151
+ const defaultFilter: CommandFilter = (value, search, keywords) =>
152
+ matchesTokens(search, [value, ...keywords].join(' '))
153
+
154
+ const commandItems = collection('command')
155
+
156
+ const CommandRoot: Stateful<CommandArgs> = function* ({ defaultSearch, defaultValue, search, value }) {
157
+ const commandId = id('command')
158
+ let announceResults = false
159
+ let disabled = false
160
+ let lastResultCount = -1
161
+ let loop = true
162
+ let onSearchChange: CommandArgs['onSearchChange']
163
+ let onValueChange: CommandArgs['onValueChange']
164
+ let resultsLabel: CommandArgs['resultsLabel']
165
+ const valueState = controlled<string>(this, {
166
+ fallback: String(value ?? defaultValue ?? ''),
167
+ onChange: (next, event) => onValueChange?.(next, event),
168
+ })
169
+ const searchState = controlled<string>(this, {
170
+ fallback: String(search ?? defaultSearch ?? ''),
171
+ onChange: (next, event) => onSearchChange?.(next, event),
172
+ })
173
+ const live = announce(this)
174
+
175
+ const itemId = (itemValue: string) => `${commandId}-item-${encodeURIComponent(itemValue)}`
176
+
177
+ const setValue = (next: string, event?: Event) => {
178
+ if (next === valueState.value) return
179
+ valueState.set(next, event)
180
+ }
181
+
182
+ const setSearch = (next: string, event: Event) => {
183
+ if (next === searchState.value) return
184
+ searchState.set(next, event)
185
+ announceResults = true
186
+ }
187
+
188
+ const nav = roving(this, {
189
+ items: () => commandItems.items(this),
190
+ loop: () => loop,
191
+ current: () => commandItems.items(this).find(item => item.dataset.value === valueState.value),
192
+ onMove: (target, event) => {
193
+ setValue(target.dataset.value ?? '', event)
194
+ target.scrollIntoView({ block: 'nearest' })
195
+ },
196
+ })
197
+
198
+ listen(this, 'keydown', (event: KeyboardEvent) => {
199
+ const target = event.target as HTMLElement | null
200
+ if (!target?.closest('[data-slot="command"]')) return
201
+
202
+ if (nav.handle(event)) return
203
+
204
+ if (event.key === 'Enter') {
205
+ // Visible items only: a highlight the filter hid must never commit.
206
+ const item = commandItems.items(this).find(candidate => candidate.dataset.value === valueState.value)
207
+ if (!item) return
208
+ event.preventDefault()
209
+ item.click()
210
+ } else if (event.key === 'Escape') {
211
+ if (!searchState.value) return
212
+ event.preventDefault()
213
+ setSearch('', event)
214
+ }
215
+ })
216
+
217
+ for (const args of this) {
218
+ disabled = Boolean(args.disabled)
219
+ loop = args.loop !== false
220
+ onSearchChange = args.onSearchChange
221
+ onValueChange = args.onValueChange
222
+ resultsLabel = args.resultsLabel
223
+ searchState.sync(args.search != null ? String(args.search) : undefined)
224
+ valueState.sync(args.value != null ? String(args.value) : undefined)
225
+
226
+ const filter = resolveFilter(args.filter, defaultFilter)
227
+ const matches = (itemValue: string, keywords: string[] = []) =>
228
+ filter(itemValue, searchState.value, keywords)
229
+
230
+ CommandContext({
231
+ activeId: valueState.value ? itemId(valueState.value) : '',
232
+ disabled,
233
+ itemId,
234
+ listId: `${commandId}-list`,
235
+ matches,
236
+ search: searchState.value,
237
+ setValue,
238
+ setSearch,
239
+ value: valueState.value,
240
+ })
241
+
242
+ // The render owns highlight attrs; this microtask only sweeps structure
243
+ // and clears staleness the filter introduced.
244
+ if (dom(this)) queueMicrotask(() => {
245
+ const visible = commandItems.sweep(this)
246
+
247
+ // Selection follows highlight: an uncontrolled value the filter hid
248
+ // re-seats on the first visible item.
249
+ if (!valueState.controlled) {
250
+ const next = visible.find(item => item.dataset.value === valueState.value) ?? visible[0]
251
+ const nextValue = next?.dataset.value ?? ''
252
+ if (nextValue !== valueState.value) valueState.init(nextValue)
253
+ }
254
+
255
+ if (announceResults && visible.length !== lastResultCount) {
256
+ live.polite((resultsLabel ?? defaultResultsLabel)(visible.length))
257
+ }
258
+ lastResultCount = visible.length
259
+ announceResults = false
260
+ })
261
+
262
+ yield <>{args.children}</>
263
+ }
264
+ }
265
+
266
+
267
+ /** Searchable command menu. */
268
+ const Command: Stateless<CommandArgs> = ({
269
+ children,
270
+ class: classes,
271
+ defaultSearch,
272
+ defaultValue,
273
+ disabled,
274
+ filter,
275
+ loop,
276
+ onSearchChange,
277
+ onValueChange,
278
+ resultsLabel,
279
+ search,
280
+ value,
281
+ ...attrs
282
+ }) => (
283
+ <CommandRoot
284
+ {...rootAttrs(attrs)}
285
+ defaultSearch={defaultSearch}
286
+ defaultValue={defaultValue}
287
+ disabled={disabled}
288
+ filter={filter}
289
+ loop={loop}
290
+ onSearchChange={onSearchChange}
291
+ onValueChange={onValueChange}
292
+ resultsLabel={resultsLabel}
293
+ search={search}
294
+ value={value}
295
+ attr:class={classes}
296
+ attr:data-slot="command"
297
+ >
298
+ {children}
299
+ </CommandRoot>
300
+ )
301
+
302
+ /** Native dialog wrapper for a Command palette. */
303
+ const CommandDialog: Stateless<CommandDialogArgs> = ({
304
+ children,
305
+ class: classes,
306
+ defaultOpen,
307
+ description,
308
+ commandClass,
309
+ onOpenChange,
310
+ open,
311
+ showCloseButton,
312
+ closeClass,
313
+ closeIconClass,
314
+ closeLabel = 'Close',
315
+ descriptionClass,
316
+ onEscapeKeyDown,
317
+ title,
318
+ titleClass,
319
+ onPointerDownOutside,
320
+ ...attrs
321
+ }) => (
322
+ <Dialog
323
+ defaultOpen={defaultOpen}
324
+ onOpenChange={onOpenChange}
325
+ open={open}
326
+ class="contents"
327
+ >
328
+ <DialogContent
329
+ {...attrs}
330
+ class={classes}
331
+ data-slot="command-dialog"
332
+ onEscapeKeyDown={onEscapeKeyDown}
333
+ onPointerDownOutside={onPointerDownOutside}
334
+ >
335
+ <div class={titleClass}>
336
+ <DialogTitle>{title ?? 'Command Palette'}</DialogTitle>
337
+ <DialogDescription class={descriptionClass}>
338
+ {description ?? 'Search for a command to run...'}
339
+ </DialogDescription>
340
+ </div>
341
+ {showCloseButton !== false ? (
342
+ <DialogClose aria-label={closeLabel} class={closeClass}>
343
+ <span aria-hidden="true" class={closeIconClass} />
344
+ </DialogClose>
345
+ ) : null}
346
+ <Command class={commandClass}>
347
+ {children}
348
+ </Command>
349
+ </DialogContent>
350
+ </Dialog>
351
+ )
352
+
353
+ /** Search input for a Command menu. */
354
+ const CommandInput: Stateless<CommandInputArgs> = ({
355
+ class: classes,
356
+ disabled,
357
+ iconClass,
358
+ onValueChange,
359
+ placeholder = 'Type a command or search...',
360
+ type: _type,
361
+ value,
362
+ wrapperClass,
363
+ 'set:oninput': onInput,
364
+ ...attrs
365
+ }) => {
366
+ const command = CommandContext()
367
+ const disabledFlag = Boolean(disabled ?? command?.disabled)
368
+ const shown = value == null ? command?.search ?? '' : String(value)
369
+
370
+ return (
371
+ <div class={wrapperClass} data-slot="command-input-wrapper">
372
+ <span aria-hidden="true" class={iconClass} />
373
+ <input
374
+ {...attrs}
375
+ aria-activedescendant={command?.activeId || undefined}
376
+ aria-autocomplete="list"
377
+ aria-controls={command?.listId}
378
+ aria-expanded="true"
379
+ class={classes}
380
+ data-slot="command-input"
381
+ disabled={disabledFlag}
382
+ placeholder={placeholder}
383
+ role="combobox"
384
+ set:oninput={(event: Event) => {
385
+ const next = (event.target as HTMLInputElement).value
386
+ callHandler(onInput, event)
387
+ if (event.defaultPrevented) return
388
+ onValueChange?.(next, event)
389
+ command?.setSearch(next, event)
390
+ }}
391
+ set:value={shown}
392
+ type="search"
393
+ />
394
+ </div>
395
+ )
396
+ }
397
+
398
+ /** Scrollable list for command options. */
399
+ const CommandList: Stateless<CommandListArgs> = ({ children, class: classes, ...attrs }) => {
400
+ const command = CommandContext()
401
+
402
+ return (
403
+ <div
404
+ {...attrs}
405
+ class={classes}
406
+ data-slot="command-list"
407
+ id={command?.listId}
408
+ role="listbox"
409
+ >
410
+ {children}
411
+ </div>
412
+ )
413
+ }
414
+
415
+ /** Empty state shown when filtering hides every command item. */
416
+ const CommandEmpty: Stateless<CommandEmptyArgs> = ({ children, class: classes, ...attrs }) => (
417
+ <div {...attrs} class={classes} data-slot="command-empty">
418
+ {children}
419
+ </div>
420
+ )
421
+
422
+ /** Group of related command items. */
423
+ const CommandGroup: Stateless<CommandGroupArgs> = ({
424
+ children,
425
+ class: classes,
426
+ forceMount,
427
+ heading,
428
+ headingClass,
429
+ ...attrs
430
+ }) => (
431
+ <div
432
+ {...attrs}
433
+ class={classes}
434
+ data-force-mount={flag(forceMount)}
435
+ data-slot="command-group"
436
+ role="group"
437
+ >
438
+ {heading ? <div class={headingClass} data-slot="command-group-heading">{heading}</div> : null}
439
+ {children}
440
+ </div>
441
+ )
442
+
443
+ /** Visual separator between command groups. */
444
+ const CommandSeparator: Stateless<CommandSeparatorArgs> = ({ class: classes, ...attrs }) => (
445
+ <div {...attrs} class={classes} data-slot="command-separator" role="separator" />
446
+ )
447
+
448
+ /** Selectable command option. */
449
+ const CommandItem: Stateless<CommandItemArgs> = ({
450
+ children,
451
+ class: classes,
452
+ disabled,
453
+ forceMount,
454
+ id: idArg,
455
+ keywords = [],
456
+ onSelect,
457
+ value,
458
+ ...attrs
459
+ }) => {
460
+ const command = CommandContext()
461
+ const itemValue = String(value ?? text(children))
462
+ const disabledFlag = Boolean(disabled ?? command?.disabled)
463
+ const hidden = !forceMount && command ? !command.matches(itemValue, keywords) : false
464
+ const highlighted = command?.value === itemValue
465
+
466
+ return (
467
+ <div
468
+ {...attrs}
469
+ {...commandItems.attrs({ disabled: disabledFlag, value: itemValue })}
470
+ aria-disabled={flag(disabledFlag)}
471
+ aria-selected={highlighted ? 'true' : 'false'}
472
+ class={classes}
473
+ data-highlighted={flag(highlighted)}
474
+ data-slot="command-item"
475
+ hidden={hidden || undefined}
476
+ id={idArg ?? command?.itemId(itemValue)}
477
+ role="option"
478
+ set:onclick={(event: Event) => {
479
+ if (disabledFlag) return
480
+ onSelect?.(itemValue, event)
481
+ if (event.defaultPrevented) return
482
+ command?.setValue(itemValue, event)
483
+ }}
484
+ set:onpointermove={(event: Event) => {
485
+ if (disabledFlag) return
486
+ command?.setValue(itemValue, event)
487
+ }}
488
+ >
489
+ {children}
490
+ </div>
491
+ )
492
+ }
493
+
494
+ /** Right-aligned shortcut hint inside a CommandItem. */
495
+ const CommandShortcut: Stateless<CommandShortcutArgs> = ({ children, class: classes, ...attrs }) => (
496
+ <span {...attrs} class={classes} data-slot="command-shortcut">
497
+ {children}
498
+ </span>
499
+ )
500
+
501
+ export {
502
+ Command,
503
+ CommandDialog,
504
+ CommandEmpty,
505
+ CommandGroup,
506
+ CommandInput,
507
+ CommandItem,
508
+ CommandList,
509
+ CommandSeparator,
510
+ CommandShortcut,
511
+ }