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
package/src/tabs.tsx ADDED
@@ -0,0 +1,314 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import { callHandler, callRef, controlled, dom, id, indicator, listen, overflow, roving, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+ import { DirectionContext } from './direction'
5
+ import type { FixedArgs, OmitArg } from './utils'
6
+
7
+ /** Layout and keyboard-navigation axis of a tab list. */
8
+ export type TabsOrientation = 'horizontal' | 'vertical'
9
+ /** Whether moving focus or explicit activation selects a tab. */
10
+ export type TabsActivationMode = 'automatic' | 'manual'
11
+
12
+ /** Props for the tabs root and its controlled selection. */
13
+ export type TabsArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'defaultValue' | 'dir' | 'onchange'> & {
14
+ /** Controlled selected tab value. */
15
+ value?: string
16
+ /** Initial selected tab value for uncontrolled usage. */
17
+ defaultValue?: string
18
+ /** Called whenever the selected tab changes. */
19
+ onValueChange?: (value: string, event?: Event) => void
20
+ /** Tab activation mode. Automatic follows focus; manual waits for Enter/Space or click. */
21
+ activationMode?: TabsActivationMode
22
+ /** Text direction for horizontal arrow-key navigation. Defaults to the nearest DirectionProvider. */
23
+ dir?: 'ltr' | 'rtl'
24
+ /** Allow arrow-key focus to wrap at the ends. */
25
+ loop?: boolean
26
+ /** Tablist orientation. */
27
+ orientation?: TabsOrientation
28
+ }> & FixedArgs<'onchange'>
29
+
30
+ /** Props for the container that owns tab triggers. */
31
+ export type TabsListArgs = WithChildren<IntrinsicElements['div']>
32
+
33
+ /** Props for a button that activates one tab value. */
34
+ export type TabsTriggerArgs = WithChildren<IntrinsicElements['button'] & {
35
+ /** Tab value controlled by this trigger. */
36
+ value: string
37
+ }>
38
+
39
+ /** Props for the panel associated with one tab value. */
40
+ export type TabsContentArgs = WithChildren<IntrinsicElements['div'] & {
41
+ /** Tab value that owns this panel. */
42
+ value: string
43
+ /** Keep the panel mounted while inactive. */
44
+ forceMount?: boolean
45
+ }>
46
+
47
+ type TabsRootArgs = WithChildren<{
48
+ activationMode: TabsActivationMode
49
+ defaultValue?: string
50
+ dir: 'ltr' | 'rtl'
51
+ loop: boolean
52
+ onValueChange?: (value: string, event?: Event) => void
53
+ orientation: TabsOrientation
54
+ value?: string
55
+ }>
56
+
57
+ type TabsContextValue = {
58
+ activationMode: TabsActivationMode
59
+ contentId: (value: string) => string
60
+ dir: 'ltr' | 'rtl'
61
+ loop: boolean
62
+ orientation: TabsOrientation
63
+ selected: (value: string) => boolean
64
+ setList: (element: HTMLElement | null) => void
65
+ setValue: (value: string, event?: Event) => void
66
+ triggerId: (value: string) => string
67
+ value: string
68
+ }
69
+
70
+ const TabsContext = context<TabsContextValue | null>(null)
71
+
72
+ const state = (active: boolean) => active ? 'active' : 'inactive'
73
+
74
+ const tabs = (root: HTMLElement) =>
75
+ Array.from(root.querySelectorAll<HTMLButtonElement>('button[data-slot="tabs-trigger"]'))
76
+ .filter(button => !button.disabled && button.offsetParent !== null)
77
+
78
+ const TabsRoot: Stateful<TabsRootArgs> = function* ({ defaultValue, value }) {
79
+ const rootId = id('tabs')
80
+ let activationMode: TabsActivationMode = 'automatic'
81
+ let dir: 'ltr' | 'rtl' = 'ltr'
82
+ let loop = true
83
+ let onValueChange: TabsRootArgs['onValueChange']
84
+ let orientation: TabsOrientation = 'horizontal'
85
+ const state = controlled<string>(this, {
86
+ fallback: String(value ?? defaultValue ?? ''),
87
+ onChange: (next, event) => onValueChange?.(next, event),
88
+ })
89
+
90
+ const triggerId = (value: string) => `${rootId}-trigger-${encodeURIComponent(value)}`
91
+ const contentId = (value: string) => `${rootId}-content-${encodeURIComponent(value)}`
92
+ const selected = (value: string) => state.value === value
93
+
94
+ const setValue = (next: string, event?: Event) => {
95
+ if (!next || next === state.value) return
96
+ state.set(next, event)
97
+ }
98
+
99
+ const ensureValue = () => {
100
+ if (state.value || state.controlled || !dom(this)) return
101
+ const first = tabs(this)[0]
102
+ if (first?.value) state.init(first.value)
103
+ }
104
+
105
+ const nav = roving(this, {
106
+ items: () => tabs(this),
107
+ orientation: () => orientation,
108
+ dir: () => dir,
109
+ loop: () => loop,
110
+ onMove: (target, event) => {
111
+ target.focus()
112
+ target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
113
+ if (activationMode === 'automatic') setValue(String((target as HTMLButtonElement).value), event)
114
+ },
115
+ })
116
+
117
+ // Overflow stamps on the tablist so themes can scroll it and fade the
118
+ // overflowing edges (data-overflow-x/-y pair with the mask preflight).
119
+ let list: HTMLElement | null = null
120
+ const edges = overflow(this, { target: () => list })
121
+ // Indicator variables on the tablist let themes glide an active marker
122
+ // between triggers instead of restyling each one.
123
+ const mark = indicator(this, {
124
+ target: () => list,
125
+ of: container => container.querySelector<HTMLElement>('button[data-slot="tabs-trigger"][data-state="active"]'),
126
+ })
127
+ const setList = (element: HTMLElement | null) => {
128
+ list = element
129
+ edges.sync()
130
+ mark.sync()
131
+ }
132
+
133
+ listen(this, 'keydown', (event: KeyboardEvent) => {
134
+ const target = event.target as HTMLElement | null
135
+ if (!target?.matches('button[data-slot="tabs-trigger"]')) return
136
+
137
+ if (nav.handle(event)) return
138
+
139
+ if (activationMode === 'manual' && (event.key === 'Enter' || event.key === ' ')) {
140
+ event.preventDefault()
141
+ setValue(String((target as HTMLButtonElement).value), event)
142
+ }
143
+ })
144
+
145
+ for (const args of this) {
146
+ activationMode = args.activationMode
147
+ dir = args.dir
148
+ loop = args.loop
149
+ onValueChange = args.onValueChange
150
+ orientation = args.orientation
151
+ state.sync(args.value != null ? String(args.value) : undefined)
152
+
153
+ TabsContext({
154
+ activationMode,
155
+ contentId,
156
+ dir,
157
+ loop,
158
+ orientation,
159
+ selected,
160
+ setList,
161
+ setValue,
162
+ triggerId,
163
+ value: state.value,
164
+ })
165
+
166
+ edges.sync()
167
+ mark.sync()
168
+ queueMicrotask(ensureValue)
169
+ yield <>{args.children}</>
170
+ }
171
+ }
172
+
173
+
174
+ /** Unstyled root provider for tab state. */
175
+ const Tabs: Stateless<TabsArgs> = ({
176
+ activationMode = 'automatic',
177
+ children,
178
+ defaultValue,
179
+ dir,
180
+ loop = true,
181
+ onValueChange,
182
+ orientation = 'horizontal',
183
+ value,
184
+ ...attrs
185
+ }) => {
186
+ const resolvedDir = dir ?? DirectionContext()
187
+
188
+ return (
189
+ <TabsRoot
190
+ {...rootAttrs(attrs)}
191
+ activationMode={activationMode}
192
+ defaultValue={defaultValue}
193
+ dir={resolvedDir}
194
+ loop={loop}
195
+ onValueChange={onValueChange}
196
+ orientation={orientation}
197
+ value={value}
198
+ attr:data-orientation={orientation}
199
+ attr:data-slot="tabs"
200
+ attr:dir={resolvedDir}
201
+ >
202
+ {children}
203
+ </TabsRoot>
204
+ )
205
+ }
206
+
207
+ /** Unstyled container for tab triggers. */
208
+ const TabsList: Stateless<TabsListArgs> = ({
209
+ children,
210
+ ref,
211
+ role = 'tablist',
212
+ ...attrs
213
+ }) => {
214
+ const context = TabsContext()
215
+
216
+ return (
217
+ <div
218
+ {...attrs}
219
+ aria-orientation={context?.orientation}
220
+ data-orientation={context?.orientation}
221
+ data-slot="tabs-list"
222
+ ref={(element: HTMLDivElement | null) => {
223
+ context?.setList(element)
224
+ callRef(ref, element)
225
+ }}
226
+ role={role}
227
+ >
228
+ {children}
229
+ </div>
230
+ )
231
+ }
232
+
233
+ /** Unstyled button that activates a tab panel. */
234
+ const TabsTrigger: Stateless<TabsTriggerArgs> = ({
235
+ children,
236
+ disabled,
237
+ id,
238
+ 'set:onclick': onClick,
239
+ 'set:onfocus': onFocus,
240
+ type = 'button',
241
+ value,
242
+ ...attrs
243
+ }) => {
244
+ const context = TabsContext()
245
+ const tabValue = String(value)
246
+ const active = Boolean(context?.selected(tabValue))
247
+ const disabledFlag = Boolean(disabled)
248
+ const triggerId = id ?? context?.triggerId(tabValue)
249
+
250
+ return (
251
+ <button
252
+ {...attrs}
253
+ aria-controls={context?.contentId(tabValue)}
254
+ aria-selected={active ? 'true' : 'false'}
255
+ data-slot="tabs-trigger"
256
+ data-state={state(active)}
257
+ data-value={tabValue}
258
+ disabled={disabledFlag}
259
+ id={triggerId}
260
+ role="tab"
261
+ set:onclick={(event: Event) => {
262
+ callHandler(onClick, event)
263
+ if (event.defaultPrevented || disabledFlag) return
264
+ context?.setValue(tabValue, event)
265
+ // A partially visible tab in an overflowing list scrolls fully
266
+ // into view on activation; scroll-padding keeps it clear of
267
+ // the theme's edge fade. No-op while the list fits.
268
+ ;(event.currentTarget as HTMLElement).scrollIntoView({ block: 'nearest', inline: 'nearest' })
269
+ }}
270
+ set:onfocus={(event: FocusEvent) => {
271
+ callHandler(onFocus, event)
272
+ if (event.defaultPrevented || disabledFlag || active || context?.activationMode !== 'automatic') return
273
+ context?.setValue(tabValue, event)
274
+ }}
275
+ tabindex={active ? 0 : -1}
276
+ type={type}
277
+ value={tabValue}
278
+ >
279
+ {children}
280
+ </button>
281
+ )
282
+ }
283
+
284
+ /** Unstyled panel displayed by its matching tab trigger. */
285
+ const TabsContent: Stateless<TabsContentArgs> = ({
286
+ children,
287
+ forceMount,
288
+ value,
289
+ ...attrs
290
+ }) => {
291
+ const context = TabsContext()
292
+ const tabValue = String(value)
293
+ const active = Boolean(context?.selected(tabValue))
294
+
295
+ if (!active && !forceMount) return null
296
+
297
+ return (
298
+ <div
299
+ {...attrs}
300
+ aria-labelledby={context?.triggerId(tabValue)}
301
+ data-slot="tabs-content"
302
+ data-state={state(active)}
303
+ data-value={tabValue}
304
+ hidden={active ? undefined : true}
305
+ id={context?.contentId(tabValue)}
306
+ role="tabpanel"
307
+ tabindex={0}
308
+ >
309
+ {children}
310
+ </div>
311
+ )
312
+ }
313
+
314
+ export { Tabs, TabsContent, TabsList, TabsTrigger }