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,523 @@
1
+ import {
2
+ arrow as arrowMiddleware,
3
+ autoPlacement,
4
+ autoUpdate,
5
+ computePosition,
6
+ flip,
7
+ hide,
8
+ inline,
9
+ limitShift,
10
+ offset,
11
+ shift,
12
+ size,
13
+ type Middleware,
14
+ type Placement,
15
+ type ReferenceElement,
16
+ } from '@floating-ui/dom'
17
+ import { dom, type Host } from 'ajo-cloves'
18
+ import type { PopupPlacement } from './utils'
19
+
20
+ export type PositionProfile =
21
+ | 'popover'
22
+ | 'tooltip'
23
+ | 'menu'
24
+ | 'submenu'
25
+ | 'select'
26
+ | 'date'
27
+ | 'navigation'
28
+ | 'context'
29
+ | 'menubar'
30
+ | 'chart'
31
+
32
+ /** Raw geometry arguments reserved by the private positioning profiles. */
33
+ export type ReservedPositionArg =
34
+ | 'align'
35
+ | 'alignOffset'
36
+ | 'boundary'
37
+ | 'collisionPadding'
38
+ | 'constrain'
39
+ | 'middleware'
40
+ | 'platform'
41
+ | 'side'
42
+ | 'sideOffset'
43
+ | 'strategy'
44
+
45
+ /** Private real-or-virtual reference shape owned at the Adapter boundary. */
46
+ export type PositionReference = ReferenceElement
47
+
48
+ /** Private zero-area virtual reference shared by point-positioned families. */
49
+ export const pointReference = (
50
+ contextElement: Element | (() => Element),
51
+ point: () => { x: number; y: number },
52
+ ): PositionReference => ({
53
+ get contextElement() {
54
+ return typeof contextElement === 'function' ? contextElement() : contextElement
55
+ },
56
+ getBoundingClientRect: () => {
57
+ const { x, y } = point()
58
+ return { x, y, top: y, right: x, bottom: y, left: x, width: 0, height: 0 }
59
+ },
60
+ })
61
+
62
+ export type PositionElements = {
63
+ reference: PositionReference | null
64
+ floating: HTMLElement | null
65
+ arrow: HTMLElement | null
66
+ }
67
+
68
+ export type PositionOptions = {
69
+ profile: PositionProfile
70
+ elements: () => PositionElements
71
+ placement?: () => PopupPlacement | undefined
72
+ gap?: () => number | undefined
73
+ boundary?: () => Element | null
74
+ /** Optional clipping boundary used only to decide whether the reference is hidden. */
75
+ referenceBoundary?: () => Element | null
76
+ referenceHidden?: (hidden: boolean) => void
77
+ }
78
+
79
+ export type PositionView = {
80
+ /** Starts observation and resolves true after a current first commit. */
81
+ start(): Promise<boolean>
82
+ /** Coalesces a geometry update and resolves true after a current commit. */
83
+ update(): Promise<boolean>
84
+ /** Invalidates pending work and removes the active observation scope. */
85
+ stop(): void
86
+ }
87
+
88
+ type Policy = {
89
+ placement: Placement
90
+ gap: number
91
+ padding: number
92
+ inline?: boolean
93
+ size?: 'both' | 'width'
94
+ hidden?: boolean
95
+ crossAxis?: number
96
+ fallbackAxisSideDirection?: 'end' | 'start'
97
+ fallbackPlacements?: Placement[]
98
+ flipFirst?: boolean
99
+ writer?: 'transform'
100
+ }
101
+
102
+ const policies: Record<PositionProfile, Policy> = {
103
+ popover: { placement: 'bottom', gap: 4, padding: 8, inline: true, size: 'both', hidden: true },
104
+ tooltip: { placement: 'top', gap: 8, padding: 8, inline: true, size: 'width', hidden: true, fallbackAxisSideDirection: 'start' },
105
+ menu: { placement: 'bottom-start', gap: 4, padding: 4, size: 'both', hidden: true },
106
+ submenu: { placement: 'right-start', gap: 4, padding: 4, size: 'both', hidden: true, fallbackPlacements: ['left-start'] },
107
+ select: { placement: 'bottom-start', gap: 6, padding: 8, size: 'both', hidden: true },
108
+ date: { placement: 'bottom-start', gap: 6, padding: 8, size: 'both', hidden: true },
109
+ navigation: { placement: 'bottom', gap: 8, padding: 8, size: 'both', hidden: true },
110
+ context: { placement: 'bottom-start', gap: 2, padding: 4, size: 'both', hidden: true },
111
+ menubar: { placement: 'bottom-start', gap: 8, padding: 4, size: 'both', hidden: true, crossAxis: -4 },
112
+ chart: { placement: 'right', gap: 12, padding: 8, flipFirst: true, writer: 'transform' },
113
+ }
114
+
115
+ const inactive = (): PositionView => ({
116
+ start: async () => false,
117
+ update: async () => false,
118
+ stop() { },
119
+ })
120
+
121
+ const connected = (reference: PositionReference) => {
122
+ if (dom(reference)) return reference.isConnected
123
+ return !reference.contextElement || reference.contextElement.isConnected
124
+ }
125
+
126
+ const finite = (value: number | undefined, fallback: number) =>
127
+ Number.isFinite(value) ? Number(value) : fallback
128
+
129
+ const round = (value: number, target: Element) => {
130
+ const ratio = target.ownerDocument.defaultView?.devicePixelRatio || 1
131
+ return Math.round(value * ratio) / ratio
132
+ }
133
+
134
+ const parts = (placement: Placement) => {
135
+ const [side, align = 'center'] = placement.split('-')
136
+ return { side, align }
137
+ }
138
+
139
+ const overflow = (padding: number, boundary: Element | null) => ({
140
+ padding,
141
+ ...(boundary ? { boundary } : {}),
142
+ })
143
+
144
+ const middleware = (
145
+ policy: Policy,
146
+ preferred: PopupPlacement,
147
+ gap: number,
148
+ boundary: Element | null,
149
+ referenceBoundary: Element | null,
150
+ arrow: HTMLElement | null,
151
+ applySize: (
152
+ availableHeight: number,
153
+ availableWidth: number,
154
+ floating: HTMLElement,
155
+ referenceHeight: number,
156
+ referenceWidth: number,
157
+ ) => void,
158
+ ): Middleware[] => {
159
+ const detect = overflow(policy.padding, boundary)
160
+ const placement = preferred === 'auto' ? policy.placement : preferred
161
+ const aligned = placement.includes('-')
162
+ const move = shift({ ...detect, limiter: limitShift() })
163
+ const fallback = flip({
164
+ ...detect,
165
+ fallbackAxisSideDirection: policy.fallbackAxisSideDirection ?? 'end',
166
+ ...(policy.fallbackPlacements ? { fallbackPlacements: policy.fallbackPlacements } : {}),
167
+ })
168
+ const collision = preferred === 'auto'
169
+ ? [autoPlacement(detect), move]
170
+ : aligned || policy.flipFirst ? [fallback, move] : [move, fallback]
171
+ const result: Middleware[] = []
172
+ // Floating UI requires inline() before offset() so a multiline rect reset
173
+ // recalculates the requested gap against the selected client rect.
174
+ if (policy.inline) result.push(inline())
175
+ result.push(offset({ mainAxis: gap, crossAxis: policy.crossAxis ?? 0 }))
176
+ result.push(...collision)
177
+ if (policy.size) result.push(size({
178
+ ...detect,
179
+ apply({ availableHeight, availableWidth, elements, rects }) {
180
+ applySize(
181
+ availableHeight,
182
+ availableWidth,
183
+ elements.floating,
184
+ rects.reference.height,
185
+ rects.reference.width,
186
+ )
187
+ },
188
+ }))
189
+ if (arrow) result.push(arrowMiddleware({ element: arrow, padding: 8 }))
190
+ if (policy.hidden) {
191
+ result.push(hide(overflow(policy.padding, referenceBoundary ?? boundary)))
192
+ result.push(hide({ ...detect, strategy: 'escaped' }))
193
+ }
194
+ return result
195
+ }
196
+
197
+ const clearArrow = (arrow: HTMLElement | null) => {
198
+ if (!arrow) return
199
+ arrow.style.left = ''
200
+ arrow.style.right = ''
201
+ arrow.style.top = ''
202
+ arrow.style.bottom = ''
203
+ delete arrow.dataset.arrowUncentered
204
+ }
205
+
206
+ const deactivateFloating = (floating: HTMLElement, profile: PositionProfile) => {
207
+ if (profile === 'chart') {
208
+ delete floating.dataset.positioned
209
+ floating.style.transform = ''
210
+ }
211
+ floating.style.willChange = ''
212
+ }
213
+
214
+ const resetFloating = (floating: HTMLElement, profile: PositionProfile) => {
215
+ deactivateFloating(floating, profile)
216
+ floating.style.position = ''
217
+ floating.style.left = ''
218
+ floating.style.top = ''
219
+ floating.style.transformOrigin = ''
220
+ floating.style.boxSizing = ''
221
+ floating.style.maxWidth = ''
222
+ floating.style.maxHeight = ''
223
+ floating.style.removeProperty('--reference-width')
224
+ floating.style.removeProperty('--reference-height')
225
+ floating.style.removeProperty('--available-width')
226
+ floating.style.removeProperty('--available-height')
227
+ floating.style.removeProperty('--popup-arrow-center')
228
+ delete floating.dataset.placement
229
+ delete floating.dataset.side
230
+ delete floating.dataset.align
231
+ delete floating.dataset.referenceHidden
232
+ delete floating.dataset.escaped
233
+ }
234
+
235
+ const sizeState = (element: HTMLElement) => ({
236
+ boxSizing: element.style.boxSizing,
237
+ maxWidth: element.style.maxWidth,
238
+ maxHeight: element.style.maxHeight,
239
+ referenceWidth: element.style.getPropertyValue('--reference-width'),
240
+ referenceHeight: element.style.getPropertyValue('--reference-height'),
241
+ availableWidth: element.style.getPropertyValue('--available-width'),
242
+ availableHeight: element.style.getPropertyValue('--available-height'),
243
+ })
244
+
245
+ const restoreSize = (element: HTMLElement, state: ReturnType<typeof sizeState>) => {
246
+ element.style.boxSizing = state.boxSizing
247
+ element.style.maxWidth = state.maxWidth
248
+ element.style.maxHeight = state.maxHeight
249
+ element.style.setProperty('--reference-width', state.referenceWidth)
250
+ element.style.setProperty('--reference-height', state.referenceHeight)
251
+ element.style.setProperty('--available-width', state.availableWidth)
252
+ element.style.setProperty('--available-height', state.availableHeight)
253
+ }
254
+
255
+ const origin = (target: HTMLElement, side: string, align: string) => {
256
+ const horizontal = side === 'top' || side === 'bottom'
257
+ const rtl = horizontal && target.ownerDocument.defaultView?.getComputedStyle(target).direction === 'rtl'
258
+ const cross = align === 'center' ? '50%'
259
+ : align === 'start' ? rtl ? '100%' : '0%'
260
+ : rtl ? '0%' : '100%'
261
+ return side === 'top' ? `${cross} 100%`
262
+ : side === 'right' ? `0% ${cross}`
263
+ : side === 'bottom' ? `${cross} 0%`
264
+ : `100% ${cross}`
265
+ }
266
+
267
+ /** Private Floating UI Adapter shared by popup families and Chart. */
268
+ export const position = (host: Host, options: PositionOptions): PositionView => {
269
+ if (!dom(host) || !host.ownerDocument.defaultView) return inactive()
270
+
271
+ const policy = policies[options.profile]
272
+ let active = false
273
+ let cleanup: (() => void) | undefined
274
+ let current: PositionElements | undefined
275
+ let generation = 0
276
+ let hidden: boolean | undefined
277
+ let pending: Promise<boolean> | undefined
278
+ let pendingGeneration = -1
279
+ let requested = 0
280
+ let sizeGeneration = -1
281
+ let sizeRestore: (() => void) | undefined
282
+
283
+ const rollbackSize = (scope = sizeGeneration) => {
284
+ if (scope !== sizeGeneration) return
285
+ const restore = sizeRestore
286
+ sizeGeneration = -1
287
+ sizeRestore = undefined
288
+ restore?.()
289
+ }
290
+
291
+ const commitSize = (scope: number) => {
292
+ if (scope !== sizeGeneration) return
293
+ sizeGeneration = -1
294
+ sizeRestore = undefined
295
+ }
296
+
297
+ const stopScope = () => {
298
+ const dispose = cleanup
299
+ active = false
300
+ generation++
301
+ requested++
302
+ cleanup = undefined
303
+ pending = undefined
304
+ pendingGeneration = -1
305
+ rollbackSize()
306
+ if (current?.floating) deactivateFloating(current.floating, options.profile)
307
+ try {
308
+ dispose?.()
309
+ } catch (error) {
310
+ host.throw(error)
311
+ }
312
+ }
313
+
314
+ const valid = (version: number, elements: PositionElements) =>
315
+ active &&
316
+ version === generation &&
317
+ current?.reference === elements.reference &&
318
+ current.floating === elements.floating &&
319
+ current.arrow === elements.arrow &&
320
+ !host.signal.aborted &&
321
+ Boolean(elements.floating?.isConnected) &&
322
+ Boolean(elements.reference && connected(elements.reference))
323
+ const currentRequest = (scope: number, request: number, elements: PositionElements) =>
324
+ request === requested && valid(scope, elements)
325
+
326
+ const commit = (
327
+ elements: PositionElements,
328
+ result: Awaited<ReturnType<typeof computePosition>>,
329
+ ) => {
330
+ const target = elements.floating!
331
+ const { side, align } = parts(result.placement)
332
+ const x = round(result.x, target)
333
+ const y = round(result.y, target)
334
+
335
+ if (policy.writer === 'transform') {
336
+ target.style.position = 'absolute'
337
+ target.style.left = '0px'
338
+ target.style.top = '0px'
339
+ target.style.transform = `translate(${x}px, ${y}px)`
340
+ target.style.willChange = 'transform'
341
+ if (target.dataset.positioned !== 'true') {
342
+ // Snap the first endpoint before Playa enables retarget motion.
343
+ target.getBoundingClientRect()
344
+ target.dataset.positioned = 'true'
345
+ }
346
+ } else {
347
+ target.style.position = 'fixed'
348
+ target.style.left = `${x}px`
349
+ target.style.top = `${y}px`
350
+ }
351
+
352
+ target.dataset.placement = result.placement
353
+ target.dataset.side = side
354
+ target.dataset.align = align
355
+
356
+ const hiddenData = result.middlewareData.hide
357
+ const referenceHidden = Boolean(hiddenData?.referenceHidden)
358
+ const escaped = Boolean(hiddenData?.escaped)
359
+ if (referenceHidden) target.dataset.referenceHidden = 'true'
360
+ else delete target.dataset.referenceHidden
361
+ if (escaped) target.dataset.escaped = 'true'
362
+ else delete target.dataset.escaped
363
+ const notifyHidden = hidden !== referenceHidden
364
+ hidden = referenceHidden
365
+
366
+ const arrow = elements.arrow
367
+ const arrowData = result.middlewareData.arrow
368
+ clearArrow(arrow)
369
+ target.style.removeProperty('--popup-arrow-center')
370
+ if (policy.writer === 'transform') {
371
+ target.style.transformOrigin = ''
372
+ } else if (arrow && arrowData) {
373
+ if (arrowData.x != null) arrow.style.left = `${round(arrowData.x, target)}px`
374
+ if (arrowData.y != null) arrow.style.top = `${round(arrowData.y, target)}px`
375
+ const staticSide = side === 'top' ? 'bottom' : side === 'right' ? 'left' : side === 'bottom' ? 'top' : 'right'
376
+ const arrowSize = side === 'top' || side === 'bottom' ? arrow.offsetHeight : arrow.offsetWidth
377
+ arrow.style[staticSide] = `${round(-arrowSize / 2, target)}px`
378
+ if (arrowData.centerOffset !== 0) arrow.dataset.arrowUncentered = 'true'
379
+
380
+ const center = side === 'top' || side === 'bottom'
381
+ ? `${round((arrowData.x ?? 0) + arrow.offsetWidth / 2, target)}px`
382
+ : `${round((arrowData.y ?? 0) + arrow.offsetHeight / 2, target)}px`
383
+ target.style.setProperty('--popup-arrow-center', center)
384
+ target.style.transformOrigin = side === 'top' ? `${center} bottom`
385
+ : side === 'right' ? `left ${center}`
386
+ : side === 'bottom' ? `${center} top`
387
+ : `right ${center}`
388
+ } else {
389
+ target.style.transformOrigin = origin(target, side, align)
390
+ }
391
+
392
+ return notifyHidden ? referenceHidden : undefined
393
+ }
394
+
395
+ const calculate = async (scope: number, request: number) => {
396
+ const elements = current
397
+ if (!elements?.reference || !elements.floating) return false
398
+ const initialSize = sizeState(elements.floating)
399
+ const preferred = options.placement?.() ?? policy.placement
400
+ const placement = preferred === 'auto' ? policy.placement : preferred
401
+ try {
402
+ const result = await computePosition(elements.reference, elements.floating, {
403
+ placement,
404
+ strategy: policy.writer === 'transform' ? 'absolute' : 'fixed',
405
+ middleware: middleware(
406
+ policy,
407
+ preferred,
408
+ finite(options.gap?.(), policy.gap),
409
+ options.boundary?.() ?? null,
410
+ options.referenceBoundary?.() ?? null,
411
+ elements.arrow,
412
+ (availableHeight, availableWidth, target, referenceHeight, referenceWidth) => {
413
+ if (!currentRequest(scope, request, elements) || target !== elements.floating) return
414
+ if (sizeGeneration !== scope) {
415
+ rollbackSize()
416
+ sizeGeneration = scope
417
+ sizeRestore = () => restoreSize(target, initialSize)
418
+ }
419
+ const height = Math.max(0, availableHeight)
420
+ const width = Math.max(0, availableWidth)
421
+ target.style.boxSizing = 'border-box'
422
+ target.style.maxWidth = `${width}px`
423
+ if (policy.size === 'both') target.style.maxHeight = `${height}px`
424
+ target.style.setProperty('--reference-width', `${referenceWidth}px`)
425
+ target.style.setProperty('--reference-height', `${referenceHeight}px`)
426
+ target.style.setProperty('--available-width', `${width}px`)
427
+ target.style.setProperty('--available-height', `${height}px`)
428
+ },
429
+ ),
430
+ })
431
+ if (!currentRequest(scope, request, elements)) {
432
+ rollbackSize(scope)
433
+ return false
434
+ }
435
+ const referenceHidden = commit(elements, result)
436
+ commitSize(scope)
437
+ if (referenceHidden !== undefined) options.referenceHidden?.(referenceHidden)
438
+ return true
439
+ } catch (error) {
440
+ rollbackSize(scope)
441
+ if (!currentRequest(scope, request, elements)) return false
442
+ throw error
443
+ }
444
+ }
445
+
446
+ const drain = async (scope: number) => {
447
+ let committed = false
448
+ while (active && generation === scope) {
449
+ const expected = requested
450
+ committed = await calculate(scope, expected)
451
+ if (expected === requested) break
452
+ }
453
+ return committed
454
+ }
455
+
456
+ const update = () => {
457
+ if (!active) return Promise.resolve(false)
458
+ requested++
459
+ const scope = generation
460
+ if (pending && pendingGeneration === scope) {
461
+ rollbackSize(scope)
462
+ return pending
463
+ }
464
+ const task = drain(scope)
465
+ pending = task
466
+ pendingGeneration = scope
467
+ const clear = () => {
468
+ if (pending !== task) return
469
+ pending = undefined
470
+ pendingGeneration = -1
471
+ }
472
+ void task.then(clear, clear)
473
+ return task
474
+ }
475
+
476
+ const fail = (error: unknown) => {
477
+ if (host.signal.aborted) return
478
+ queueMicrotask(() => {
479
+ if (!host.signal.aborted) host.throw(error)
480
+ })
481
+ }
482
+
483
+ const stop = () => {
484
+ stopScope()
485
+ hidden = undefined
486
+ }
487
+
488
+ const start = async () => {
489
+ if (host.signal.aborted) {
490
+ stop()
491
+ return false
492
+ }
493
+ const next = options.elements()
494
+ if (!next.reference || !next.floating || !next.floating.isConnected || !connected(next.reference)) {
495
+ stop()
496
+ return false
497
+ }
498
+
499
+ const previous = current
500
+ stopScope()
501
+ if (previous?.arrow !== next.arrow) clearArrow(previous?.arrow ?? null)
502
+ if (previous?.floating && previous.floating !== next.floating) resetFloating(previous.floating, options.profile)
503
+ current = next
504
+ active = true
505
+ hidden = undefined
506
+ requested = 0
507
+
508
+ try {
509
+ let starting = true
510
+ cleanup = autoUpdate(next.reference, next.floating, () => {
511
+ if (!starting) void update().catch(fail)
512
+ })
513
+ starting = false
514
+ return await update()
515
+ } catch (error) {
516
+ stop()
517
+ throw error
518
+ }
519
+ }
520
+
521
+ host.signal.addEventListener('abort', stop, { once: true })
522
+ return { start, stop, update }
523
+ }
@@ -0,0 +1,70 @@
1
+ import type { IntrinsicElements, Stateless } from 'ajo'
2
+ import { clamp } from 'ajo-cloves'
3
+ import type { FixedArgs, OmitArg } from './utils'
4
+ import { toNumber } from './utils'
5
+
6
+ /** Props for a determinate or indeterminate progress bar. */
7
+ export type ProgressArgs = OmitArg<IntrinsicElements['div'], 'children' | 'max' | 'value'> & {
8
+ /** Current progress value. Omit or pass `null` for an indeterminate progress bar. */
9
+ value?: number | null
10
+ /** Maximum progress value. */
11
+ max?: number
12
+ /** Custom accessible value text. */
13
+ getValueLabel?: (value: number, max: number) => string
14
+ /** Classes for the indicator element. */
15
+ indicatorClass?: string
16
+ } & FixedArgs<'children'>
17
+
18
+ const state = (value: number | null, max: number) => {
19
+ if (value == null) return 'indeterminate'
20
+ if (value >= max) return 'complete'
21
+ return 'loading'
22
+ }
23
+
24
+ const percent = (value: number | null, max: number) =>
25
+ value == null || max <= 0 ? 0 : clamp((value / max) * 100, 0, 100)
26
+
27
+ /** Unstyled progress bar with determinate and indeterminate states. */
28
+ const Progress: Stateless<ProgressArgs> = ({
29
+ 'aria-label': ariaLabel,
30
+ 'aria-valuetext': ariaValueText,
31
+ getValueLabel,
32
+ indicatorClass,
33
+ max = 100,
34
+ role = 'progressbar',
35
+ value = null,
36
+ ...attrs
37
+ }) => {
38
+ const maxValue = Math.max(1, toNumber(max, 100))
39
+ const current = value == null ? null : clamp(toNumber(value, 0), 0, maxValue)
40
+ const progress = percent(current, maxValue)
41
+ const progressState = state(current, maxValue)
42
+ const valueText = current == null
43
+ ? ariaValueText
44
+ : ariaValueText ?? getValueLabel?.(current, maxValue)
45
+
46
+ return (
47
+ <div
48
+ {...attrs}
49
+ aria-label={ariaLabel}
50
+ aria-valuemax={maxValue}
51
+ aria-valuemin="0"
52
+ aria-valuenow={current == null ? undefined : current}
53
+ aria-valuetext={valueText}
54
+ data-max={maxValue}
55
+ data-slot="progress"
56
+ data-state={progressState}
57
+ data-value={current == null ? undefined : current}
58
+ role={role}
59
+ >
60
+ <div
61
+ aria-hidden="true"
62
+ class={indicatorClass}
63
+ data-slot="progress-indicator"
64
+ style={current == null ? undefined : `transform:translateX(-${100 - progress}%)`}
65
+ />
66
+ </div>
67
+ )
68
+ }
69
+
70
+ export { Progress }