@proyecto-viviana/solidaria-components 0.2.2 → 0.2.4

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 (64) hide show
  1. package/dist/Color.d.ts +2 -6
  2. package/dist/Color.d.ts.map +1 -1
  3. package/dist/ComboBox.d.ts +3 -3
  4. package/dist/ComboBox.d.ts.map +1 -1
  5. package/dist/GridList.d.ts +2 -2
  6. package/dist/GridList.d.ts.map +1 -1
  7. package/dist/ListBox.d.ts +5 -5
  8. package/dist/ListBox.d.ts.map +1 -1
  9. package/dist/Menu.d.ts +3 -3
  10. package/dist/Menu.d.ts.map +1 -1
  11. package/dist/Select.d.ts +3 -3
  12. package/dist/Select.d.ts.map +1 -1
  13. package/dist/Table.d.ts +2 -2
  14. package/dist/Table.d.ts.map +1 -1
  15. package/dist/Tabs.d.ts +1 -1
  16. package/dist/Tabs.d.ts.map +1 -1
  17. package/dist/index.js +56 -56
  18. package/dist/index.js.map +2 -2
  19. package/dist/index.ssr.js +56 -56
  20. package/dist/index.ssr.js.map +2 -2
  21. package/package.json +10 -8
  22. package/src/Autocomplete.tsx +174 -0
  23. package/src/Breadcrumbs.tsx +264 -0
  24. package/src/Button.tsx +238 -0
  25. package/src/Calendar.tsx +471 -0
  26. package/src/Checkbox.tsx +387 -0
  27. package/src/Color.tsx +1370 -0
  28. package/src/ComboBox.tsx +824 -0
  29. package/src/DateField.tsx +337 -0
  30. package/src/DatePicker.tsx +367 -0
  31. package/src/Dialog.tsx +262 -0
  32. package/src/Disclosure.tsx +439 -0
  33. package/src/GridList.tsx +511 -0
  34. package/src/Landmark.tsx +203 -0
  35. package/src/Link.tsx +201 -0
  36. package/src/ListBox.tsx +346 -0
  37. package/src/Menu.tsx +544 -0
  38. package/src/Meter.tsx +157 -0
  39. package/src/Modal.tsx +433 -0
  40. package/src/NumberField.tsx +542 -0
  41. package/src/Popover.tsx +540 -0
  42. package/src/ProgressBar.tsx +162 -0
  43. package/src/RadioGroup.tsx +356 -0
  44. package/src/RangeCalendar.tsx +462 -0
  45. package/src/SearchField.tsx +479 -0
  46. package/src/Select.tsx +734 -0
  47. package/src/Separator.tsx +130 -0
  48. package/src/Slider.tsx +500 -0
  49. package/src/Switch.tsx +213 -0
  50. package/src/Table.tsx +857 -0
  51. package/src/Tabs.tsx +552 -0
  52. package/src/TagGroup.tsx +421 -0
  53. package/src/TextField.tsx +271 -0
  54. package/src/TimeField.tsx +455 -0
  55. package/src/Toast.tsx +503 -0
  56. package/src/Toolbar.tsx +160 -0
  57. package/src/Tooltip.tsx +423 -0
  58. package/src/Tree.tsx +551 -0
  59. package/src/VisuallyHidden.tsx +60 -0
  60. package/src/contexts.ts +74 -0
  61. package/src/index.ts +620 -0
  62. package/src/utils.tsx +329 -0
  63. package/dist/index.jsx +0 -9056
  64. package/dist/index.jsx.map +0 -7
@@ -0,0 +1,540 @@
1
+ /**
2
+ * Popover component for solidaria-components
3
+ *
4
+ * A headless popover component that positions relative to a trigger element.
5
+ * Port of react-aria-components Popover.
6
+ */
7
+
8
+ import {
9
+ type JSX,
10
+ createContext,
11
+ createMemo,
12
+ createSignal,
13
+ createUniqueId,
14
+ splitProps,
15
+ useContext,
16
+ Show,
17
+ createEffect,
18
+ onCleanup,
19
+ } from 'solid-js'
20
+ import { Portal, isServer } from 'solid-js/web'
21
+ import {
22
+ createOverlayTrigger,
23
+ FocusScope,
24
+ type Placement,
25
+ type PlacementAxis,
26
+ } from '@proyecto-viviana/solidaria'
27
+ import { createOverlayTriggerState } from '@proyecto-viviana/solid-stately'
28
+ import {
29
+ type RenderChildren,
30
+ type ClassNameOrFunction,
31
+ type StyleOrFunction,
32
+ type SlotProps,
33
+ useRenderProps,
34
+ filterDOMProps,
35
+ dataAttr,
36
+ } from './utils'
37
+ import { PopoverTriggerContext } from './contexts'
38
+
39
+ // ============================================
40
+ // TYPES
41
+ // ============================================
42
+
43
+ export interface PopoverRenderProps {
44
+ /**
45
+ * The name of the component that triggered the popover.
46
+ */
47
+ trigger: string | null
48
+ /**
49
+ * The placement of the popover relative to the trigger.
50
+ */
51
+ placement: PlacementAxis | null
52
+ /**
53
+ * Whether the popover is currently entering (for animations).
54
+ */
55
+ isEntering: boolean
56
+ /**
57
+ * Whether the popover is currently exiting (for animations).
58
+ */
59
+ isExiting: boolean
60
+ }
61
+
62
+ export interface PopoverProps extends SlotProps {
63
+ /** The children of the component - can be JSX or render function. */
64
+ children?: RenderChildren<PopoverRenderProps>
65
+ /** The CSS className for the element. */
66
+ class?: ClassNameOrFunction<PopoverRenderProps>
67
+ /** The inline style for the element. */
68
+ style?: StyleOrFunction<PopoverRenderProps>
69
+ /**
70
+ * The name of the component that triggered the popover.
71
+ */
72
+ trigger?: string
73
+ /**
74
+ * The ref for the element which the popover positions itself with respect to.
75
+ * Required when used standalone (not within a trigger component).
76
+ */
77
+ triggerRef?: () => Element | null
78
+ /**
79
+ * The placement of the element with respect to its anchor element.
80
+ * @default 'bottom'
81
+ */
82
+ placement?: Placement
83
+ /**
84
+ * The placement padding that should be applied between the element and its
85
+ * surrounding container.
86
+ * @default 12
87
+ */
88
+ containerPadding?: number
89
+ /**
90
+ * The additional offset applied along the main axis between the element and its
91
+ * anchor element.
92
+ * @default 8
93
+ */
94
+ offset?: number
95
+ /**
96
+ * The additional offset applied along the cross axis between the element and its
97
+ * anchor element.
98
+ * @default 0
99
+ */
100
+ crossOffset?: number
101
+ /**
102
+ * Whether the element should flip its orientation when there is insufficient room.
103
+ * @default true
104
+ */
105
+ shouldFlip?: boolean
106
+ /**
107
+ * Whether the popover is non-modal (allows interaction outside).
108
+ */
109
+ isNonModal?: boolean
110
+ /**
111
+ * Whether pressing Escape to close should be disabled.
112
+ */
113
+ isKeyboardDismissDisabled?: boolean
114
+ /**
115
+ * Filter for which outside interactions should close the popover.
116
+ */
117
+ shouldCloseOnInteractOutside?: (element: Element) => boolean
118
+ /** Whether the popover is open (controlled). */
119
+ isOpen?: boolean
120
+ /** Whether the popover opens by default (uncontrolled). */
121
+ defaultOpen?: boolean
122
+ /** Handler called when the popover's open state changes. */
123
+ onOpenChange?: (isOpen: boolean) => void
124
+ /** Whether the popover is entering (for animations). */
125
+ isEntering?: boolean
126
+ /** Whether the popover is exiting (for animations). */
127
+ isExiting?: boolean
128
+ }
129
+
130
+ export interface PopoverTriggerProps {
131
+ /** The children - should include a trigger and popover content. */
132
+ children: JSX.Element
133
+ /** Whether the popover is open (controlled). */
134
+ isOpen?: boolean
135
+ /** Whether the popover is open by default (uncontrolled). */
136
+ defaultOpen?: boolean
137
+ /** Callback when open state changes. */
138
+ onOpenChange?: (isOpen: boolean) => void
139
+ }
140
+
141
+ // ============================================
142
+ // CONTEXTS
143
+ // ============================================
144
+
145
+ // Re-export from shared contexts
146
+ export { PopoverTriggerContext, usePopoverTrigger, type PopoverTriggerContextValue } from './contexts'
147
+
148
+ // Internal context for placement
149
+ export const PopoverContext = createContext<{ placement: () => PlacementAxis | null } | null>(null)
150
+
151
+ // ============================================
152
+ // POPOVER TRIGGER COMPONENT
153
+ // ============================================
154
+
155
+ /**
156
+ * A PopoverTrigger opens a popover when a trigger element is pressed.
157
+ * Children should include a trigger element (e.g. Button) and the Popover.
158
+ */
159
+ export function PopoverTrigger(props: PopoverTriggerProps): JSX.Element {
160
+ const [local] = splitProps(props, ['isOpen', 'defaultOpen', 'onOpenChange'])
161
+
162
+ // Create overlay trigger state
163
+ const state = createOverlayTriggerState({
164
+ get isOpen() {
165
+ return local.isOpen
166
+ },
167
+ get defaultOpen() {
168
+ return local.defaultOpen
169
+ },
170
+ onOpenChange: local.onOpenChange,
171
+ })
172
+
173
+ // Ref for the trigger element
174
+ let triggerRef: HTMLElement | null = null
175
+ const triggerId = createUniqueId()
176
+
177
+ // Create overlay trigger (for side effects like scroll close)
178
+ createOverlayTrigger(
179
+ { type: 'dialog' },
180
+ state,
181
+ () => triggerRef
182
+ )
183
+
184
+ // Track if triggerRef has been set (to prevent buttons inside Popover from overwriting it)
185
+ let triggerRefSet = false
186
+
187
+ // Context value
188
+ const contextValue = createMemo(() => ({
189
+ state: {
190
+ isOpen: () => state.isOpen(),
191
+ open: () => state.open(),
192
+ close: () => state.close(),
193
+ toggle: () => state.toggle(),
194
+ },
195
+ triggerRef: () => {
196
+ return triggerRef
197
+ },
198
+ setTriggerRef: (el: HTMLElement | null) => {
199
+ // Only set the trigger ref once - the first button to register is the actual trigger
200
+ // This prevents buttons inside the Popover content from overwriting the trigger ref
201
+ if (!triggerRefSet && el) {
202
+ triggerRef = el
203
+ triggerRefSet = true
204
+ }
205
+ },
206
+ triggerId,
207
+ trigger: 'PopoverTrigger',
208
+ }))
209
+
210
+ // Just render children inside the provider - the Button component will detect
211
+ // the PopoverTriggerContext and handle toggling. The Popover component will
212
+ // detect the context and use it for open state.
213
+ //
214
+ // IMPORTANT: In SolidJS, JSX children are lazily evaluated when they're part
215
+ // of JSX expression. So {props.children} here means the children (Button, Popover)
216
+ // will be evaluated inside the Provider context.
217
+ return (
218
+ <PopoverTriggerContext.Provider value={contextValue()}>
219
+ {props.children}
220
+ </PopoverTriggerContext.Provider>
221
+ )
222
+ }
223
+
224
+ // ============================================
225
+ // POPOVER COMPONENT
226
+ // ============================================
227
+
228
+ /**
229
+ * A popover is an overlay element positioned relative to a trigger.
230
+ */
231
+ export function Popover(props: PopoverProps): JSX.Element {
232
+ if (isServer) {
233
+ // On the server, return null - popovers should not render during SSR
234
+ return null as unknown as JSX.Element
235
+ }
236
+
237
+ const [local, rest] = splitProps(props, [
238
+ 'class',
239
+ 'style',
240
+ 'trigger',
241
+ 'triggerRef',
242
+ 'placement',
243
+ 'containerPadding',
244
+ 'offset',
245
+ 'crossOffset',
246
+ 'shouldFlip',
247
+ 'isNonModal',
248
+ 'isKeyboardDismissDisabled',
249
+ 'shouldCloseOnInteractOutside',
250
+ 'isOpen',
251
+ 'defaultOpen',
252
+ 'onOpenChange',
253
+ 'isEntering',
254
+ 'isExiting',
255
+ ])
256
+
257
+ let popoverRef!: HTMLDivElement
258
+
259
+ // Get trigger context if available
260
+ const triggerContext = useContext(PopoverTriggerContext)
261
+
262
+ // Internal state for uncontrolled mode
263
+ const [internalOpen, setInternalOpen] = createSignal(local.defaultOpen ?? false)
264
+
265
+ // Determine if open
266
+ const isOpen = (): boolean => {
267
+ if (local.isOpen !== undefined) return local.isOpen
268
+ if (triggerContext) {
269
+ return triggerContext.state.isOpen()
270
+ }
271
+ return internalOpen()
272
+ }
273
+
274
+ const close = () => {
275
+ if (local.isOpen !== undefined) {
276
+ local.onOpenChange?.(false)
277
+ } else if (triggerContext) {
278
+ triggerContext.state.close()
279
+ local.onOpenChange?.(false)
280
+ } else {
281
+ setInternalOpen(false)
282
+ local.onOpenChange?.(false)
283
+ }
284
+ }
285
+
286
+ // Get trigger ref
287
+ const getTriggerRef = () => {
288
+ if (local.triggerRef) return local.triggerRef()
289
+ if (triggerContext) return triggerContext.triggerRef()
290
+ return null
291
+ }
292
+
293
+ // Signal to track placement after positioning
294
+ const [placement, setPlacement] = createSignal<PlacementAxis | null>(null)
295
+ // Signal to track position styles
296
+ // Start with visibility hidden, then show after positioning
297
+ const [positionStyles, setPositionStyles] = createSignal({
298
+ top: '0px',
299
+ left: '0px',
300
+ visibility: 'hidden' as 'hidden' | 'visible',
301
+ })
302
+
303
+ // Handle keyboard dismiss (Escape key)
304
+ createEffect(() => {
305
+ if (!isOpen()) return
306
+ if (local.isKeyboardDismissDisabled) return
307
+
308
+ const handleKeyDown = (e: KeyboardEvent) => {
309
+ if (e.key === 'Escape') {
310
+ e.preventDefault()
311
+ e.stopPropagation()
312
+ close()
313
+ }
314
+ }
315
+
316
+ document.addEventListener('keydown', handleKeyDown)
317
+ onCleanup(() => document.removeEventListener('keydown', handleKeyDown))
318
+ })
319
+
320
+ // Handle click outside to dismiss popover
321
+ createEffect(() => {
322
+ if (!isOpen()) return
323
+ if (local.isNonModal) return // Non-modal popovers don't auto-dismiss on outside click
324
+
325
+ const handleClickOutside = (e: MouseEvent) => {
326
+ const target = e.target as Element
327
+ const trigger = getTriggerRef()
328
+
329
+ // Don't close if clicking inside the popover
330
+ if (popoverRef && popoverRef.contains(target)) {
331
+ return
332
+ }
333
+
334
+ // Don't close if clicking the trigger (it will toggle)
335
+ if (trigger && trigger.contains(target)) {
336
+ return
337
+ }
338
+
339
+ // Check custom filter
340
+ if (local.shouldCloseOnInteractOutside && !local.shouldCloseOnInteractOutside(target)) {
341
+ return
342
+ }
343
+
344
+ close()
345
+ }
346
+
347
+ // Use capture phase to catch clicks before they bubble
348
+ // Small delay to avoid closing on the same click that opened it
349
+ const timeoutId = setTimeout(() => {
350
+ document.addEventListener('mousedown', handleClickOutside, true)
351
+ }, 0)
352
+
353
+ onCleanup(() => {
354
+ clearTimeout(timeoutId)
355
+ document.removeEventListener('mousedown', handleClickOutside, true)
356
+ })
357
+ })
358
+
359
+ // Calculate position based on trigger element
360
+ // Using position: fixed so we use viewport coordinates directly from getBoundingClientRect
361
+ const updatePosition = () => {
362
+ const trigger = getTriggerRef()
363
+ const popover = popoverRef
364
+ if (!trigger || !popover) return
365
+
366
+ const triggerRect = trigger.getBoundingClientRect()
367
+ // Use offsetWidth/offsetHeight which are more reliable than getBoundingClientRect
368
+ // when the element might be positioned off-screen initially
369
+ const popoverWidth = popover.offsetWidth
370
+ const popoverHeight = popover.offsetHeight
371
+ const offset = local.offset ?? 8
372
+
373
+ let top = 0
374
+ let left = 0
375
+ const placementValue = local.placement ?? 'bottom'
376
+
377
+ // Using viewport coordinates for position: fixed
378
+ switch (placementValue) {
379
+ case 'top':
380
+ case 'top start':
381
+ case 'top end':
382
+ top = triggerRect.top - popoverHeight - offset
383
+ left = triggerRect.left + (triggerRect.width - popoverWidth) / 2
384
+ setPlacement('top')
385
+ break
386
+ case 'bottom':
387
+ case 'bottom start':
388
+ case 'bottom end':
389
+ top = triggerRect.bottom + offset
390
+ left = triggerRect.left + (triggerRect.width - popoverWidth) / 2
391
+ setPlacement('bottom')
392
+ break
393
+ case 'left':
394
+ case 'left top':
395
+ case 'left bottom':
396
+ top = triggerRect.top + (triggerRect.height - popoverHeight) / 2
397
+ left = triggerRect.left - popoverWidth - offset
398
+ setPlacement('left')
399
+ break
400
+ case 'right':
401
+ case 'right top':
402
+ case 'right bottom':
403
+ top = triggerRect.top + (triggerRect.height - popoverHeight) / 2
404
+ left = triggerRect.right + offset
405
+ setPlacement('right')
406
+ break
407
+ default:
408
+ top = triggerRect.bottom + offset
409
+ left = triggerRect.left + (triggerRect.width - popoverWidth) / 2
410
+ setPlacement('bottom')
411
+ }
412
+
413
+ setPositionStyles({
414
+ top: `${top}px`,
415
+ left: `${left}px`,
416
+ visibility: 'visible',
417
+ })
418
+ }
419
+
420
+ // Set up positioning effect - runs when open and trigger ref is available
421
+ createEffect(() => {
422
+ if (!isOpen()) return
423
+
424
+ const triggerElement = getTriggerRef()
425
+ if (!triggerElement) return
426
+
427
+ // Initial position calculation - use requestAnimationFrame to ensure
428
+ // the element is rendered and has proper dimensions
429
+ requestAnimationFrame(() => {
430
+ updatePosition()
431
+ })
432
+
433
+ // Update on scroll/resize
434
+ window.addEventListener('scroll', updatePosition, true)
435
+ window.addEventListener('resize', updatePosition)
436
+
437
+ onCleanup(() => {
438
+ window.removeEventListener('scroll', updatePosition, true)
439
+ window.removeEventListener('resize', updatePosition)
440
+ })
441
+ })
442
+
443
+ // Render props values
444
+ const renderValues = createMemo<PopoverRenderProps>(() => ({
445
+ trigger: local.trigger ?? triggerContext?.trigger ?? null,
446
+ placement: placement(),
447
+ isEntering: local.isEntering ?? false,
448
+ isExiting: local.isExiting ?? false,
449
+ }))
450
+
451
+ // Resolve render props
452
+ const renderProps = useRenderProps(
453
+ {
454
+ children: props.children,
455
+ class: local.class,
456
+ style: local.style,
457
+ defaultClassName: 'solidaria-Popover',
458
+ },
459
+ renderValues
460
+ )
461
+
462
+ // Filter DOM props
463
+ const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }))
464
+
465
+ // Check if we should render with dialog role
466
+ const shouldBeDialog = () => !local.isNonModal
467
+
468
+ return (
469
+ <Show when={isOpen() || local.isExiting}>
470
+ <Portal>
471
+ <PopoverContext.Provider value={{ placement: () => placement() }}>
472
+ <FocusScope contain={shouldBeDialog()} restoreFocus autoFocus>
473
+ <div
474
+ {...domProps()}
475
+ ref={popoverRef}
476
+ role={shouldBeDialog() ? 'dialog' : undefined}
477
+ tabIndex={shouldBeDialog() ? -1 : undefined}
478
+ class={renderProps.class()}
479
+ style={{
480
+ position: 'fixed',
481
+ 'z-index': 100000,
482
+ ...positionStyles(),
483
+ ...renderProps.style(),
484
+ }}
485
+ data-trigger={local.trigger ?? triggerContext?.trigger}
486
+ data-placement={placement()}
487
+ data-entering={dataAttr(local.isEntering)}
488
+ data-exiting={dataAttr(local.isExiting)}
489
+ >
490
+ {renderProps.renderChildren()}
491
+ </div>
492
+ </FocusScope>
493
+ </PopoverContext.Provider>
494
+ </Portal>
495
+ </Show>
496
+ )
497
+ }
498
+
499
+ // ============================================
500
+ // OVERLAY ARROW COMPONENT
501
+ // ============================================
502
+
503
+ export interface OverlayArrowProps {
504
+ /** The children - should be an SVG or element for the arrow. */
505
+ children?: JSX.Element | ((placement: PlacementAxis | null) => JSX.Element)
506
+ /** The CSS className. */
507
+ class?: string
508
+ /** The inline style. */
509
+ style?: JSX.CSSProperties
510
+ }
511
+
512
+ /**
513
+ * An arrow element that points towards the trigger.
514
+ */
515
+ export function OverlayArrow(props: OverlayArrowProps): JSX.Element {
516
+ const popoverContext = useContext(PopoverContext)
517
+ const placement = () => popoverContext?.placement() ?? null
518
+
519
+ const resolveChildren = () => {
520
+ const children = props.children
521
+ if (typeof children === 'function') {
522
+ return children(placement())
523
+ }
524
+ return children
525
+ }
526
+
527
+ return (
528
+ <div
529
+ class={props.class}
530
+ style={props.style}
531
+ data-placement={placement()}
532
+ aria-hidden="true"
533
+ role="presentation"
534
+ >
535
+ {resolveChildren()}
536
+ </div>
537
+ )
538
+ }
539
+
540
+ export default Popover
@@ -0,0 +1,162 @@
1
+ /**
2
+ * ProgressBar component for solidaria-components
3
+ *
4
+ * Pre-wired headless progress bar component that combines aria hooks.
5
+ * Port of react-aria-components/src/ProgressBar.tsx
6
+ */
7
+
8
+ import {
9
+ type JSX,
10
+ type ParentProps,
11
+ createContext,
12
+ createMemo,
13
+ splitProps,
14
+ } from 'solid-js';
15
+ import {
16
+ createProgressBar,
17
+ type AriaProgressBarProps,
18
+ } from '@proyecto-viviana/solidaria';
19
+ import {
20
+ type RenderChildren,
21
+ type ClassNameOrFunction,
22
+ type StyleOrFunction,
23
+ type SlotProps,
24
+ useRenderProps,
25
+ filterDOMProps,
26
+ } from './utils';
27
+
28
+ // ============================================
29
+ // TYPES
30
+ // ============================================
31
+
32
+ export interface ProgressBarRenderProps {
33
+ /** The value as a percentage between the minimum and maximum (0-100). */
34
+ percentage: number | undefined;
35
+ /** A formatted version of the value. */
36
+ valueText: string | undefined;
37
+ /** Whether the progress bar is indeterminate. */
38
+ isIndeterminate: boolean;
39
+ }
40
+
41
+ export interface ProgressBarProps
42
+ extends AriaProgressBarProps,
43
+ SlotProps {
44
+ /** The children of the component. A function may be provided to receive render props. */
45
+ children?: RenderChildren<ProgressBarRenderProps>;
46
+ /** The CSS className for the element. */
47
+ class?: ClassNameOrFunction<ProgressBarRenderProps>;
48
+ /** The inline style for the element. */
49
+ style?: StyleOrFunction<ProgressBarRenderProps>;
50
+ }
51
+
52
+ // ============================================
53
+ // CONTEXT
54
+ // ============================================
55
+
56
+ export const ProgressBarContext = createContext<ProgressBarProps | null>(null);
57
+
58
+ // ============================================
59
+ // UTILITIES
60
+ // ============================================
61
+
62
+ function clamp(value: number, min: number, max: number): number {
63
+ return Math.min(Math.max(value, min), max);
64
+ }
65
+
66
+ // ============================================
67
+ // PROGRESSBAR COMPONENT
68
+ // ============================================
69
+
70
+ /**
71
+ * Progress bars show either determinate or indeterminate progress of an operation
72
+ * over time.
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * <ProgressBar value={50}>
77
+ * {({ percentage, valueText }) => (
78
+ * <>
79
+ * <Label>Loading...</Label>
80
+ * <span>{valueText}</span>
81
+ * <div class="bar" style={{ width: `${percentage}%` }} />
82
+ * </>
83
+ * )}
84
+ * </ProgressBar>
85
+ * ```
86
+ */
87
+ export function ProgressBar(props: ParentProps<ProgressBarProps>): JSX.Element {
88
+ const [local, ariaProps] = splitProps(props, [
89
+ 'children',
90
+ 'class',
91
+ 'style',
92
+ 'slot',
93
+ ]);
94
+
95
+ // Get values for calculations
96
+ const value = () => ariaProps.value ?? 0;
97
+ const minValue = () => ariaProps.minValue ?? 0;
98
+ const maxValue = () => ariaProps.maxValue ?? 100;
99
+ const isIndeterminate = () => ariaProps.isIndeterminate ?? false;
100
+
101
+ // Create progress bar aria props
102
+ const progressAria = createProgressBar({
103
+ get value() { return ariaProps.value; },
104
+ get minValue() { return ariaProps.minValue; },
105
+ get maxValue() { return ariaProps.maxValue; },
106
+ get valueLabel() { return ariaProps.valueLabel; },
107
+ get isIndeterminate() { return ariaProps.isIndeterminate; },
108
+ get formatOptions() { return ariaProps.formatOptions; },
109
+ get label() { return ariaProps.label; },
110
+ get 'aria-label'() { return ariaProps['aria-label']; },
111
+ get 'aria-labelledby'() { return ariaProps['aria-labelledby']; },
112
+ get 'aria-describedby'() { return ariaProps['aria-describedby']; },
113
+ get 'aria-details'() { return ariaProps['aria-details']; },
114
+ });
115
+
116
+ // Calculate percentage
117
+ const percentage = createMemo(() => {
118
+ if (isIndeterminate()) {
119
+ return undefined;
120
+ }
121
+ const clampedValue = clamp(value(), minValue(), maxValue());
122
+ return ((clampedValue - minValue()) / (maxValue() - minValue())) * 100;
123
+ });
124
+
125
+ // Get value text from aria props
126
+ const valueText = createMemo(() => {
127
+ return progressAria.progressBarProps['aria-valuetext'] as string | undefined;
128
+ });
129
+
130
+ // Render props values
131
+ const renderValues = createMemo<ProgressBarRenderProps>(() => ({
132
+ percentage: percentage(),
133
+ valueText: valueText(),
134
+ isIndeterminate: isIndeterminate(),
135
+ }));
136
+
137
+ // Resolve render props
138
+ const renderProps = useRenderProps(
139
+ {
140
+ children: props.children,
141
+ class: local.class,
142
+ style: local.style,
143
+ defaultClassName: 'solidaria-ProgressBar',
144
+ },
145
+ renderValues
146
+ );
147
+
148
+ // Filter DOM props
149
+ const domProps = createMemo(() => filterDOMProps(ariaProps, { global: true }));
150
+
151
+ return (
152
+ <div
153
+ {...domProps()}
154
+ {...progressAria.progressBarProps}
155
+ class={renderProps.class()}
156
+ style={renderProps.style()}
157
+ slot={local.slot}
158
+ >
159
+ {renderProps.renderChildren()}
160
+ </div>
161
+ );
162
+ }