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,424 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import { callHandler, clamp, listen, resize, scrolling, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+
5
+ /** Layout axis used by a Carousel. */
6
+ export type CarouselOrientation =
7
+ | 'horizontal'
8
+ | 'vertical'
9
+
10
+ /** Inline direction used by horizontal Carousel navigation. */
11
+ export type CarouselDirection =
12
+ | 'ltr'
13
+ | 'rtl'
14
+
15
+ /** Behavioral options for the native Carousel controller. */
16
+ export type CarouselOptions = {
17
+ /** Axis hint for carousel options. */
18
+ axis?: 'x' | 'y'
19
+ /** Text direction hint for localized carousels. */
20
+ direction?: CarouselDirection
21
+ /** Loop prev/next navigation at the ends. */
22
+ loop?: boolean
23
+ }
24
+
25
+ /** Lifecycle events emitted by a Carousel controller. */
26
+ export type CarouselEvent =
27
+ | 'reInit'
28
+ | 'select'
29
+
30
+ /** Imperative view of the current Carousel position and controls. */
31
+ export type CarouselApi = {
32
+ canScrollNext: () => boolean
33
+ canScrollPrev: () => boolean
34
+ off: (event: CarouselEvent, listener: () => void) => void
35
+ on: (event: CarouselEvent, listener: () => void) => void
36
+ reInit: () => void
37
+ scrollNext: () => void
38
+ scrollPrev: () => void
39
+ scrollSnapList: () => number[]
40
+ scrollTo: (index: number) => void
41
+ selectedScrollSnap: () => number
42
+ }
43
+
44
+ /** Arguments for the Carousel root. */
45
+ export type CarouselArgs = WithChildren<IntrinsicElements['div'] & {
46
+ /** Orientation of the carousel track. */
47
+ orientation?: CarouselOrientation
48
+ /** Native carousel options. */
49
+ opts?: CarouselOptions
50
+ /** Receives the small native carousel controller. */
51
+ setApi?: (api: CarouselApi) => void
52
+ }>
53
+
54
+ /** Arguments for the Carousel track and viewport composition. */
55
+ export type CarouselContentArgs = WithChildren<IntrinsicElements['div'] & {
56
+ /** Classes for the viewport wrapper. */
57
+ viewportClass?: string
58
+ }>
59
+
60
+ /** Arguments for one snap-aligned Carousel item. */
61
+ export type CarouselItemArgs = WithChildren<IntrinsicElements['div']>
62
+
63
+ /** Shared arguments for previous and next Carousel buttons. */
64
+ export type CarouselButtonArgs = WithChildren<IntrinsicElements['button']>
65
+
66
+ /** Observable state and controls inherited from the nearest Carousel. */
67
+ export type CarouselContextValue = {
68
+ canScrollNext: boolean
69
+ canScrollPrev: boolean
70
+ count: number
71
+ orientation: CarouselOrientation
72
+ scrollNext: () => void
73
+ scrollPrev: () => void
74
+ selected: number
75
+ }
76
+
77
+ type CarouselPartsContextValue = {
78
+ setViewport: (element: HTMLElement | null) => void
79
+ }
80
+
81
+ /** Read the observable state and controls inherited from the nearest Carousel. */
82
+ export const CarouselContext = context<CarouselContextValue | null>(null)
83
+ const CarouselPartsContext = context<CarouselPartsContextValue | null>(null)
84
+
85
+ const carousel = () => {
86
+ const value = CarouselContext()
87
+ if (!value) throw new Error('Carousel parts must be used within a <Carousel />')
88
+ return value
89
+ }
90
+
91
+ const carouselParts = () => {
92
+ const value = CarouselPartsContext()
93
+ if (!value) throw new Error('Carousel parts must be used within a <Carousel />')
94
+ return value
95
+ }
96
+
97
+ const axis = (orientation: CarouselOrientation) =>
98
+ orientation === 'horizontal' ? 'x' : 'y'
99
+
100
+ const alignBlock = (orientation: CarouselOrientation) =>
101
+ orientation === 'vertical' ? 'start' : 'nearest'
102
+
103
+ const alignInline = (orientation: CarouselOrientation) =>
104
+ orientation === 'horizontal' ? 'start' : 'nearest'
105
+
106
+ const itemPosition = (item: HTMLElement, orientation: CarouselOrientation) =>
107
+ orientation === 'horizontal' ? item.offsetLeft : item.offsetTop
108
+
109
+ const viewportPosition = (viewport: HTMLElement, orientation: CarouselOrientation) =>
110
+ orientation === 'horizontal' ? viewport.scrollLeft : viewport.scrollTop
111
+
112
+ const viewportSize = (viewport: HTMLElement, orientation: CarouselOrientation) =>
113
+ orientation === 'horizontal' ? viewport.clientWidth : viewport.clientHeight
114
+
115
+ const itemSize = (item: HTMLElement, orientation: CarouselOrientation) =>
116
+ orientation === 'horizontal' ? item.offsetWidth : item.offsetHeight
117
+
118
+ const CarouselRoot: Stateful<Pick<CarouselArgs, 'children' | 'opts' | 'orientation' | 'setApi'>> = function* () {
119
+ let viewport: HTMLElement | null = null
120
+ let orientation: CarouselOrientation = 'horizontal'
121
+ let loop = false
122
+ let selected = 0
123
+ let count = 0
124
+ let canScrollPrev = false
125
+ let canScrollNext = false
126
+ let apiReceiver: CarouselArgs['setApi']
127
+ const listeners = new Map<CarouselEvent, Set<() => void>>()
128
+
129
+ const items = () => viewport
130
+ ? Array.from(viewport.querySelectorAll<HTMLElement>('[data-slot="carousel-item"]'))
131
+ : []
132
+
133
+ const emit = (event: CarouselEvent) => {
134
+ for (const listener of listeners.get(event) ?? []) listener()
135
+ }
136
+
137
+ const syncScrollability = () => {
138
+ canScrollPrev = Boolean(count) && (loop || selected > 0)
139
+ canScrollNext = Boolean(count) && (loop || selected < count - 1)
140
+ }
141
+
142
+ const sync = () => {
143
+ const nodes = items()
144
+ const previousSelected = selected
145
+ const previousCount = count
146
+ const previousCanScrollPrev = canScrollPrev
147
+ const previousCanScrollNext = canScrollNext
148
+ count = nodes.length
149
+
150
+ if (!viewport || !count) {
151
+ selected = 0
152
+ } else {
153
+ const center = viewportPosition(viewport, orientation) + viewportSize(viewport, orientation) / 2
154
+ let nextSelected = 0
155
+ let distance = Number.POSITIVE_INFINITY
156
+
157
+ nodes.forEach((item, index) => {
158
+ const next = Math.abs(itemPosition(item, orientation) + itemSize(item, orientation) / 2 - center)
159
+ if (next < distance) {
160
+ distance = next
161
+ nextSelected = index
162
+ }
163
+ })
164
+
165
+ selected = nextSelected
166
+ }
167
+ syncScrollability()
168
+
169
+ const changed = previousSelected !== selected
170
+ || previousCount !== count
171
+ || previousCanScrollPrev !== canScrollPrev
172
+ || previousCanScrollNext !== canScrollNext
173
+ if (previousCount !== count) emit('reInit')
174
+ if (previousSelected !== selected) emit('select')
175
+
176
+ return changed
177
+ }
178
+ const invalidate = () => {
179
+ if (sync()) this.next()
180
+ }
181
+
182
+ const scroll = scrolling(this, {
183
+ target: () => viewport,
184
+ onScroll: invalidate,
185
+ })
186
+
187
+ const size = resize(this, {
188
+ target: () => viewport,
189
+ onResize: invalidate,
190
+ })
191
+
192
+ const scrollTo = (index: number) => {
193
+ const nodes = items()
194
+ if (!nodes.length) return
195
+ const target = clamp(index, 0, nodes.length - 1)
196
+ nodes[target]?.scrollIntoView({
197
+ behavior: 'smooth',
198
+ block: alignBlock(orientation),
199
+ inline: alignInline(orientation),
200
+ })
201
+ }
202
+
203
+ const scrollPrev = () => {
204
+ if (!count) return
205
+ scrollTo(selected <= 0 && loop ? count - 1 : selected - 1)
206
+ }
207
+
208
+ const scrollNext = () => {
209
+ if (!count) return
210
+ scrollTo(selected >= count - 1 && loop ? 0 : selected + 1)
211
+ }
212
+
213
+ const api: CarouselApi = {
214
+ canScrollNext: () => canScrollNext,
215
+ canScrollPrev: () => canScrollPrev,
216
+ off: (event, listener) => listeners.get(event)?.delete(listener),
217
+ on: (event, listener) => {
218
+ const set = listeners.get(event) ?? new Set()
219
+ set.add(listener)
220
+ listeners.set(event, set)
221
+ },
222
+ reInit: () => {
223
+ this.next(sync)
224
+ },
225
+ scrollNext,
226
+ scrollPrev,
227
+ scrollSnapList: () => items().map((_, index) => index),
228
+ scrollTo,
229
+ selectedScrollSnap: () => selected,
230
+ }
231
+
232
+ const setViewport = (element: HTMLElement | null) => {
233
+ if (viewport === element) return
234
+
235
+ viewport = element
236
+ scroll.sync()
237
+ size.sync()
238
+ this.next(sync)
239
+ }
240
+ const parts: CarouselPartsContextValue = { setViewport }
241
+
242
+ const keydown = (event: KeyboardEvent) => {
243
+ if (orientation === 'horizontal') {
244
+ if (event.key === 'ArrowLeft') {
245
+ event.preventDefault()
246
+ scrollPrev()
247
+ } else if (event.key === 'ArrowRight') {
248
+ event.preventDefault()
249
+ scrollNext()
250
+ }
251
+ } else if (event.key === 'ArrowUp') {
252
+ event.preventDefault()
253
+ scrollPrev()
254
+ } else if (event.key === 'ArrowDown') {
255
+ event.preventDefault()
256
+ scrollNext()
257
+ }
258
+ }
259
+
260
+ listen(this, 'keydown', keydown)
261
+
262
+ for (const {
263
+ children,
264
+ opts,
265
+ orientation: nextOrientation,
266
+ setApi,
267
+ } of this) {
268
+ orientation = nextOrientation ?? (opts?.axis === 'y' ? 'vertical' : 'horizontal')
269
+ loop = Boolean(opts?.loop)
270
+ syncScrollability()
271
+ if (setApi && setApi !== apiReceiver) {
272
+ apiReceiver = setApi
273
+ setApi(api)
274
+ }
275
+
276
+ scroll.sync()
277
+ size.sync()
278
+
279
+ CarouselContext({
280
+ canScrollNext,
281
+ canScrollPrev,
282
+ count,
283
+ orientation,
284
+ scrollNext,
285
+ scrollPrev,
286
+ selected,
287
+ })
288
+ CarouselPartsContext(parts)
289
+
290
+ yield <>{children}</>
291
+ }
292
+ }
293
+
294
+ /** Unstyled native scroll-snap carousel root. */
295
+ const Carousel: Stateless<CarouselArgs> = ({
296
+ children,
297
+ dir,
298
+ opts,
299
+ orientation,
300
+ role = 'region',
301
+ setApi,
302
+ ...attrs
303
+ }) => (
304
+ <CarouselRoot
305
+ {...rootAttrs(attrs as Record<string, unknown>)}
306
+ opts={opts}
307
+ orientation={orientation}
308
+ setApi={setApi}
309
+ attr:aria-roledescription="carousel"
310
+ attr:data-axis={axis(orientation ?? (opts?.axis === 'y' ? 'vertical' : 'horizontal'))}
311
+ attr:data-slot="carousel"
312
+ attr:dir={opts?.direction ?? dir}
313
+ attr:role={role}
314
+ >
315
+ {children}
316
+ </CarouselRoot>
317
+ )
318
+
319
+ /** Unstyled scroll viewport and track for carousel slides. */
320
+ const CarouselContent: Stateless<CarouselContentArgs> = ({
321
+ children,
322
+ viewportClass,
323
+ ...attrs
324
+ }) => {
325
+ const state = carousel()
326
+ const parts = carouselParts()
327
+
328
+ return (
329
+ <div class={viewportClass} data-slot="carousel-content">
330
+ <div
331
+ {...attrs}
332
+ data-axis={axis(state.orientation)}
333
+ data-slot="carousel-track"
334
+ ref={parts.setViewport}
335
+ >
336
+ {children}
337
+ </div>
338
+ </div>
339
+ )
340
+ }
341
+
342
+ /** Unstyled carousel slide item. */
343
+ const CarouselItem: Stateless<CarouselItemArgs> = ({
344
+ children,
345
+ role = 'group',
346
+ ...attrs
347
+ }) => (
348
+ <div
349
+ {...attrs}
350
+ aria-roledescription="slide"
351
+ data-slot="carousel-item"
352
+ role={role}
353
+ >
354
+ {children}
355
+ </div>
356
+ )
357
+
358
+ /** Unstyled previous slide button. */
359
+ const CarouselPrevious: Stateless<CarouselButtonArgs> = ({
360
+ children,
361
+ 'aria-label': label = 'Previous slide',
362
+ disabled,
363
+ type = 'button',
364
+ 'set:onclick': onClick,
365
+ ...attrs
366
+ }) => {
367
+ const { canScrollPrev, scrollPrev } = carousel()
368
+ const click = (event: MouseEvent) => {
369
+ callHandler(onClick, event)
370
+ if (event.defaultPrevented || disabled || !canScrollPrev) return
371
+ scrollPrev()
372
+ }
373
+
374
+ return (
375
+ <button
376
+ {...attrs}
377
+ aria-label={label}
378
+ data-slot="carousel-previous"
379
+ disabled={disabled || !canScrollPrev}
380
+ set:onclick={click}
381
+ type={type}
382
+ >
383
+ {children}
384
+ </button>
385
+ )
386
+ }
387
+
388
+ /** Unstyled next slide button. */
389
+ const CarouselNext: Stateless<CarouselButtonArgs> = ({
390
+ children,
391
+ 'aria-label': label = 'Next slide',
392
+ disabled,
393
+ type = 'button',
394
+ 'set:onclick': onClick,
395
+ ...attrs
396
+ }) => {
397
+ const { canScrollNext, scrollNext } = carousel()
398
+ const click = (event: MouseEvent) => {
399
+ callHandler(onClick, event)
400
+ if (event.defaultPrevented || disabled || !canScrollNext) return
401
+ scrollNext()
402
+ }
403
+
404
+ return (
405
+ <button
406
+ {...attrs}
407
+ aria-label={label}
408
+ data-slot="carousel-next"
409
+ disabled={disabled || !canScrollNext}
410
+ set:onclick={click}
411
+ type={type}
412
+ >
413
+ {children}
414
+ </button>
415
+ )
416
+ }
417
+
418
+ export {
419
+ Carousel,
420
+ CarouselContent,
421
+ CarouselItem,
422
+ CarouselNext,
423
+ CarouselPrevious,
424
+ }