@torch-ui/solid 0.1.3

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 (118) hide show
  1. package/README.md +166 -0
  2. package/package.json +67 -0
  3. package/src/components/actions/Button.tsx +612 -0
  4. package/src/components/actions/ButtonGroup.tsx +728 -0
  5. package/src/components/actions/Copy.tsx +98 -0
  6. package/src/components/actions/DarkModeToggle.tsx +80 -0
  7. package/src/components/actions/Link.tsx +37 -0
  8. package/src/components/actions/index.ts +19 -0
  9. package/src/components/actions/useCopyToClipboard.ts +90 -0
  10. package/src/components/charts/Chart.tsx +331 -0
  11. package/src/components/charts/Sparkline.tsx +156 -0
  12. package/src/components/charts/index.ts +13 -0
  13. package/src/components/data-display/Avatar.tsx +208 -0
  14. package/src/components/data-display/AvatarGroup.tsx +228 -0
  15. package/src/components/data-display/Badge.tsx +70 -0
  16. package/src/components/data-display/Carousel.tsx +214 -0
  17. package/src/components/data-display/ColorSwatch.tsx +56 -0
  18. package/src/components/data-display/DataTable.tsx +886 -0
  19. package/src/components/data-display/EmptyState.tsx +61 -0
  20. package/src/components/data-display/Image.tsx +277 -0
  21. package/src/components/data-display/Kbd.tsx +114 -0
  22. package/src/components/data-display/Persona.tsx +78 -0
  23. package/src/components/data-display/StatCard.tsx +338 -0
  24. package/src/components/data-display/Table.tsx +147 -0
  25. package/src/components/data-display/Tag.tsx +91 -0
  26. package/src/components/data-display/Timeline.tsx +200 -0
  27. package/src/components/data-display/TreeView.tsx +172 -0
  28. package/src/components/data-display/Video.tsx +95 -0
  29. package/src/components/data-display/avatar-utils.ts +32 -0
  30. package/src/components/data-display/index.ts +81 -0
  31. package/src/components/feedback/Loading.tsx +159 -0
  32. package/src/components/feedback/Progress.tsx +321 -0
  33. package/src/components/feedback/Skeleton.tsx +62 -0
  34. package/src/components/feedback/SkeletonBlocks.tsx +222 -0
  35. package/src/components/feedback/Toast.tsx +648 -0
  36. package/src/components/feedback/index.ts +44 -0
  37. package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
  38. package/src/components/feedback/password/password-strength.ts +115 -0
  39. package/src/components/feedback/password/password-validation-data.ts +66 -0
  40. package/src/components/feedback/password/password-validation.ts +93 -0
  41. package/src/components/forms/Autocomplete.tsx +268 -0
  42. package/src/components/forms/Checkbox.tsx +155 -0
  43. package/src/components/forms/CodeInput.tsx +237 -0
  44. package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
  45. package/src/components/forms/ColorPicker/color-utils.ts +75 -0
  46. package/src/components/forms/ColorPicker/index.ts +2 -0
  47. package/src/components/forms/DatePicker.tsx +516 -0
  48. package/src/components/forms/DateRangePicker.tsx +464 -0
  49. package/src/components/forms/FieldPicker.tsx +64 -0
  50. package/src/components/forms/FileUpload.tsx +614 -0
  51. package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
  52. package/src/components/forms/FilterBuilder.tsx +16 -0
  53. package/src/components/forms/FilterRuleRow.tsx +68 -0
  54. package/src/components/forms/Input.tsx +200 -0
  55. package/src/components/forms/MultiSelect.tsx +361 -0
  56. package/src/components/forms/NumberField.tsx +145 -0
  57. package/src/components/forms/RadioGroup.tsx +135 -0
  58. package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
  59. package/src/components/forms/ReorderableList.tsx +163 -0
  60. package/src/components/forms/Select.tsx +268 -0
  61. package/src/components/forms/Slider.tsx +260 -0
  62. package/src/components/forms/Switch.tsx +135 -0
  63. package/src/components/forms/TextArea.tsx +202 -0
  64. package/src/components/forms/ViewCustomizer.tsx +44 -0
  65. package/src/components/forms/index.ts +43 -0
  66. package/src/components/layout/Accordion.tsx +110 -0
  67. package/src/components/layout/Alert.tsx +156 -0
  68. package/src/components/layout/BlockQuote.tsx +70 -0
  69. package/src/components/layout/Card.tsx +166 -0
  70. package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
  71. package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
  72. package/src/components/layout/CodeBlock/prism.ts +81 -0
  73. package/src/components/layout/Collapsible.tsx +84 -0
  74. package/src/components/layout/Container.tsx +55 -0
  75. package/src/components/layout/Divider.tsx +64 -0
  76. package/src/components/layout/Form.tsx +39 -0
  77. package/src/components/layout/FormActions.tsx +50 -0
  78. package/src/components/layout/Grid.tsx +53 -0
  79. package/src/components/layout/PageHeading.tsx +46 -0
  80. package/src/components/layout/PromptWithAction.tsx +49 -0
  81. package/src/components/layout/Section.tsx +60 -0
  82. package/src/components/layout/TablePanel.tsx +24 -0
  83. package/src/components/layout/TableView/TableView.tsx +1018 -0
  84. package/src/components/layout/TableView/index.ts +3 -0
  85. package/src/components/layout/TableView/types.ts +51 -0
  86. package/src/components/layout/WizardStep.tsx +40 -0
  87. package/src/components/layout/WizardStepper.tsx +173 -0
  88. package/src/components/layout/index.ts +96 -0
  89. package/src/components/navigation/Breadcrumbs.tsx +66 -0
  90. package/src/components/navigation/DropdownMenu.tsx +86 -0
  91. package/src/components/navigation/MegaMenu.tsx +480 -0
  92. package/src/components/navigation/NavigationMenu.tsx +305 -0
  93. package/src/components/navigation/Pagination.tsx +298 -0
  94. package/src/components/navigation/Sidebar.tsx +280 -0
  95. package/src/components/navigation/Tabs.tsx +122 -0
  96. package/src/components/navigation/ViewSwitcher.tsx +314 -0
  97. package/src/components/navigation/index.ts +66 -0
  98. package/src/components/overlays/AlertDialog.tsx +174 -0
  99. package/src/components/overlays/ContextMenu.tsx +65 -0
  100. package/src/components/overlays/Dialog.tsx +279 -0
  101. package/src/components/overlays/Drawer.tsx +370 -0
  102. package/src/components/overlays/HoverCard.tsx +107 -0
  103. package/src/components/overlays/Popover.tsx +73 -0
  104. package/src/components/overlays/Tooltip.tsx +31 -0
  105. package/src/components/overlays/index.ts +71 -0
  106. package/src/components/typography/Code.tsx +72 -0
  107. package/src/components/typography/Icon.tsx +36 -0
  108. package/src/components/typography/index.ts +10 -0
  109. package/src/env.d.ts +9 -0
  110. package/src/index.ts +13 -0
  111. package/src/styles/theme.css +226 -0
  112. package/src/types/avatar-types.ts +11 -0
  113. package/src/types/filter-types.ts +35 -0
  114. package/src/utilities/classNames.ts +6 -0
  115. package/src/utilities/componentSize.ts +46 -0
  116. package/src/utilities/i18n.tsx +60 -0
  117. package/src/utilities/mergeRefs.ts +12 -0
  118. package/src/utilities/relativeDateDefault.ts +14 -0
@@ -0,0 +1,159 @@
1
+ import { type JSX, Show, Switch, Match, splitProps } from 'solid-js'
2
+ import { Loader2, LoaderCircle } from 'lucide-solid'
3
+ import { cn } from '../../utilities/classNames'
4
+ import {
5
+ SkeletonCard,
6
+ SkeletonTable,
7
+ SkeletonSection,
8
+ SkeletonHeading,
9
+ SkeletonForm,
10
+ SkeletonNavBlock,
11
+ } from './SkeletonBlocks'
12
+
13
+ export type LoadingVariant =
14
+ | 'spinner'
15
+ | 'dashboard'
16
+ | 'tablePage'
17
+ | 'admin'
18
+ | 'generic'
19
+
20
+ export interface LoadingProps extends JSX.HTMLAttributes<HTMLDivElement> {
21
+ /** Spinner (default) or skeleton layout for full-page loading. Omit for spinner. */
22
+ variant?: LoadingVariant
23
+ /** For spinner: message (default "Loading…"). Omit or set iconOnly for no message. */
24
+ message?: string
25
+ /** For spinner: when true, show only the spinner (no message). */
26
+ iconOnly?: boolean
27
+ /** For spinner: size of the icon. Ignored when icon is provided. */
28
+ size?: 'sm' | 'md' | 'lg'
29
+ /** For spinner: custom icon element (e.g. from lucide-solid). Add animate-spin and size classes. */
30
+ icon?: JSX.Element
31
+ /** For spinner: minimum height (default 200px when not iconOnly). */
32
+ minHeight?: string | number
33
+ }
34
+
35
+ /** Single loading component: spinner (for Suspense/panels) or skeleton layout (for full-page). */
36
+ export function Loading(props: LoadingProps) {
37
+ const [local, others] = splitProps(props, [
38
+ 'variant',
39
+ 'class',
40
+ 'message',
41
+ 'iconOnly',
42
+ 'size',
43
+ 'icon',
44
+ 'minHeight',
45
+ 'aria-label',
46
+ ])
47
+
48
+ const variant = () => local.variant ?? 'spinner'
49
+ if (variant() === 'spinner') {
50
+ const iconOnly = () => local.iconOnly === true
51
+ const size = () => local.size ?? 'md'
52
+ const sizeClasses = () =>
53
+ size() === 'sm' ? 'h-4 w-4' : size() === 'lg' ? 'h-6 w-6' : 'h-5 w-5'
54
+ const minHeight = () =>
55
+ local.minHeight != null
56
+ ? typeof local.minHeight === 'number'
57
+ ? `${local.minHeight}px`
58
+ : local.minHeight
59
+ : iconOnly()
60
+ ? undefined
61
+ : '200px'
62
+ const defaultIcon = () => (
63
+ <LoaderCircle
64
+ class={cn('shrink-0 animate-spin text-ink-400', sizeClasses())}
65
+ aria-hidden="true"
66
+ />
67
+ )
68
+ const resolvedIcon = () => local.icon ?? defaultIcon()
69
+ const label = () => local['aria-label'] ?? (iconOnly() ? (local.message ?? 'Loading') : undefined)
70
+ return (
71
+ <div
72
+ {...others}
73
+ class={cn('flex items-center justify-center gap-2', local.class)}
74
+ style={minHeight() ? { 'min-height': minHeight() } : undefined}
75
+ role="status"
76
+ aria-live="polite"
77
+ aria-label={label()}
78
+ >
79
+ <span aria-hidden="true">{resolvedIcon()}</span>
80
+ <Show when={!iconOnly()}>
81
+ <span
82
+ class="text-sm text-ink-500"
83
+ aria-hidden={local['aria-label'] ? 'true' : undefined}
84
+ >
85
+ {local.message ?? 'Loading…'}
86
+ </span>
87
+ </Show>
88
+ </div>
89
+ )
90
+ }
91
+
92
+ // Skeleton layout: composed from SkeletonBlocks that map to Card, Table, Section, etc.
93
+ return (
94
+ <div {...others} class={cn(local.class)} role="status" aria-live="polite" aria-atomic="true" aria-label="Loading">
95
+ <Switch>
96
+ <Match when={variant() === 'dashboard'}><DashboardSkeletonLayout /></Match>
97
+ <Match when={variant() === 'tablePage'}><TablePageSkeletonLayout /></Match>
98
+ <Match when={variant() === 'admin'}><AdminSkeletonLayout /></Match>
99
+ <Match when={true}><GenericSkeletonLayout /></Match>
100
+ </Switch>
101
+ </div>
102
+ )
103
+ }
104
+
105
+ function DashboardSkeletonLayout() {
106
+ return (
107
+ <div class="space-y-8">
108
+ <SkeletonHeading />
109
+ <div class="grid gap-6 sm:grid-cols-2">
110
+ <SkeletonCard header bodyLines={2} />
111
+ <SkeletonCard header bodyLines={2} />
112
+ </div>
113
+ </div>
114
+ )
115
+ }
116
+
117
+ function TablePageSkeletonLayout() {
118
+ return (
119
+ <div class="space-y-6">
120
+ <div class="mb-6">
121
+ <SkeletonHeading />
122
+ </div>
123
+ <div class="mb-6 flex flex-wrap items-center justify-between gap-4">
124
+ <div class="flex min-w-0 max-w-2xl flex-1 items-center gap-4">
125
+ <div class="h-10 min-w-0 flex-1 rounded-lg bg-surface-overlay animate-pulse" />
126
+ <div class="h-10 w-28 rounded-lg bg-surface-overlay animate-pulse" />
127
+ </div>
128
+ <div class="flex items-center gap-2">
129
+ <div class="h-10 w-20 rounded-lg bg-surface-overlay animate-pulse" />
130
+ <div class="h-10 w-28 rounded-lg bg-surface-overlay animate-pulse" />
131
+ </div>
132
+ </div>
133
+ <SkeletonTable rows={6} columns={5} />
134
+ </div>
135
+ )
136
+ }
137
+
138
+ function AdminSkeletonLayout() {
139
+ return (
140
+ <div class="flex gap-8">
141
+ <aside class="w-64 flex-shrink-0 space-y-6">
142
+ <SkeletonNavBlock items={3} />
143
+ <SkeletonNavBlock items={5} />
144
+ </aside>
145
+ <div class="min-w-0 flex-1 space-y-6">
146
+ <SkeletonHeading />
147
+ <SkeletonForm fields={2} buttons={2} />
148
+ </div>
149
+ </div>
150
+ )
151
+ }
152
+
153
+ function GenericSkeletonLayout() {
154
+ return (
155
+ <div class="space-y-6">
156
+ <SkeletonSection description content contentLines={3} />
157
+ </div>
158
+ )
159
+ }
@@ -0,0 +1,321 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { createEffect, createMemo, For, Show, splitProps } from 'solid-js'
3
+ import { Progress as KobalteProgress } from '@kobalte/core/progress'
4
+ import { cn } from '../../utilities/classNames'
5
+
6
+ export type ProgressSize = 'sm' | 'md' | 'lg'
7
+ export type ProgressColor =
8
+ | 'default'
9
+ | 'primary'
10
+ | 'secondary'
11
+ | 'success'
12
+ | 'warning'
13
+ | 'danger'
14
+ export type ProgressRadius = 'none' | 'sm' | 'md' | 'lg' | 'full'
15
+
16
+ export interface ProgressClassNames {
17
+ base?: string
18
+ labelWrapper?: string
19
+ label?: string
20
+ track?: string
21
+ value?: string
22
+ indicator?: string
23
+ }
24
+
25
+ export interface ProgressProps {
26
+ /** Current value (between minValue and maxValue) */
27
+ value?: number
28
+ /** Minimum value */
29
+ minValue?: number
30
+ /** Maximum value */
31
+ maxValue?: number
32
+ /** Label above the bar; also used for aria-label when aria-label not set and label is a string */
33
+ label?: JSX.Element | string
34
+ /** Custom value label (e.g. "3/10"). When not set, value is shown as percentage if showValueLabel. */
35
+ valueLabel?: JSX.Element
36
+ /** Size of the track */
37
+ size?: ProgressSize
38
+ /** Color of the indicator */
39
+ color?: ProgressColor
40
+ /** Track and indicator radius */
41
+ radius?: ProgressRadius
42
+ /** Intl.NumberFormat options for default value display (e.g. { style: 'percent' }) */
43
+ formatOptions?: Intl.NumberFormatOptions
44
+ /** Whether to show the value label (default true when determinate and no valueLabel) */
45
+ showValueLabel?: boolean
46
+ /** Indeterminate animation when total progress is unknown */
47
+ isIndeterminate?: boolean
48
+ /** Striped indicator */
49
+ isStriped?: boolean
50
+ /** Disabled state */
51
+ isDisabled?: boolean
52
+ /** Disable fill animation */
53
+ disableAnimation?: boolean
54
+ /** When set, render as segmented bar (e.g. password strength) */
55
+ segments?: number
56
+ /** Animate from 0 to 100 over this duration (ms). Use for indeterminate-duration progress. */
57
+ durationMs?: number
58
+ /** Accessible label (required when label prop is not provided) */
59
+ 'aria-label'?: string
60
+ 'aria-labelledby'?: string
61
+ 'aria-describedby'?: string
62
+ 'aria-valuetext'?: string
63
+ 'aria-valuenow'?: number
64
+ 'aria-valuemin'?: number
65
+ 'aria-valuemax'?: number
66
+ /** Slot class overrides */
67
+ classNames?: ProgressClassNames
68
+ class?: string
69
+ trackClass?: string
70
+ fillClass?: string
71
+ }
72
+
73
+ const ANIMATION_NAME = 'torchui-progress-fill'
74
+ const INDETERMINATE_NAME = 'torchui-progress-indeterminate'
75
+
76
+ const SIZE_CLASSES: Record<ProgressSize, string> = {
77
+ sm: 'h-1',
78
+ md: 'h-1.5',
79
+ lg: 'h-2',
80
+ }
81
+
82
+ const RADIUS_CLASSES: Record<ProgressRadius, string> = {
83
+ none: 'rounded-none',
84
+ sm: 'rounded-sm',
85
+ md: 'rounded-md',
86
+ lg: 'rounded-lg',
87
+ full: 'rounded-full',
88
+ }
89
+
90
+ const EDGE_RADIUS_CLASSES: Record<ProgressRadius, { left: string; right: string }> = {
91
+ none: { left: 'rounded-l-none', right: 'rounded-r-none' },
92
+ sm: { left: 'rounded-l-sm', right: 'rounded-r-sm' },
93
+ md: { left: 'rounded-l-md', right: 'rounded-r-md' },
94
+ lg: { left: 'rounded-l-lg', right: 'rounded-r-lg' },
95
+ full: { left: 'rounded-l-full', right: 'rounded-r-full' },
96
+ }
97
+
98
+ const COLOR_CLASSES: Record<ProgressColor, string> = {
99
+ default: 'bg-ink-500',
100
+ primary: 'bg-primary-500',
101
+ secondary: 'bg-secondary-500',
102
+ success: 'bg-success-500',
103
+ warning: 'bg-warning-500',
104
+ danger: 'bg-danger-500',
105
+ }
106
+
107
+ function getPercent(value: number, min: number, max: number): number {
108
+ if (min >= max) {
109
+ if (import.meta.env.DEV && min > max) {
110
+ console.warn(`Progress: minValue (${min}) is greater than maxValue (${max}).`)
111
+ }
112
+ return 0
113
+ }
114
+ return Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100))
115
+ }
116
+
117
+ /**
118
+ * Progress bar. Supports determinate (value), indeterminate, segmented, or duration-based fill.
119
+ * Aligned with common Progress APIs (label, size, color, radius, valueLabel, isIndeterminate, etc.).
120
+ */
121
+ export function Progress(props: ProgressProps): JSX.Element {
122
+ const [local] = splitProps(props, [
123
+ 'value',
124
+ 'minValue',
125
+ 'maxValue',
126
+ 'label',
127
+ 'valueLabel',
128
+ 'size',
129
+ 'color',
130
+ 'radius',
131
+ 'formatOptions',
132
+ 'showValueLabel',
133
+ 'isIndeterminate',
134
+ 'isStriped',
135
+ 'isDisabled',
136
+ 'disableAnimation',
137
+ 'segments',
138
+ 'durationMs',
139
+ 'aria-label',
140
+ 'aria-labelledby',
141
+ 'aria-describedby',
142
+ 'aria-valuetext',
143
+ 'aria-valuenow',
144
+ 'aria-valuemin',
145
+ 'aria-valuemax',
146
+ 'classNames',
147
+ 'class',
148
+ 'trackClass',
149
+ 'fillClass',
150
+ ])
151
+
152
+ const min = () => local.minValue ?? 0
153
+ const max = () => local.maxValue ?? 100
154
+ const rawValue = () => local.value ?? 0
155
+ const percent = () => getPercent(rawValue(), min(), max())
156
+ const segments = () => local.segments
157
+ const durationMs = () => local.durationMs
158
+ const size = () => local.size ?? 'md'
159
+ const color = () => local.color ?? 'primary'
160
+ const radius = () => local.radius ?? 'full'
161
+ const isIndeterminate = () => local.isIndeterminate === true
162
+ const showValue = () =>
163
+ local.showValueLabel !== false &&
164
+ local.valueLabel == null &&
165
+ durationMs() == null &&
166
+ !isIndeterminate()
167
+ const ariaLabel = () => local['aria-label'] ?? (typeof local.label === 'string' ? local.label : undefined)
168
+ let warnedLabel = false
169
+ createEffect(() => {
170
+ if (import.meta.env.DEV && !warnedLabel && ariaLabel() == null && local['aria-labelledby'] == null) {
171
+ warnedLabel = true
172
+ console.warn('Progress: provide aria-label, aria-labelledby, or a string label prop for an accessible progress bar.')
173
+ }
174
+ })
175
+ const formatter = createMemo(() =>
176
+ new Intl.NumberFormat(undefined, local.formatOptions ?? { style: 'percent', maximumFractionDigits: 0 })
177
+ )
178
+ const valueDisplay = () => {
179
+ if (local.valueLabel != null) return local.valueLabel
180
+ if (!showValue()) return null
181
+ return formatter().format(percent() / 100)
182
+ }
183
+ const segmentIndexes = createMemo(() =>
184
+ segments() != null ? Array.from({ length: segments()! }, (_, i) => i) : []
185
+ )
186
+ const classNames = () => local.classNames ?? {}
187
+
188
+ return (
189
+ <KobalteProgress
190
+ value={isIndeterminate() || durationMs() != null ? undefined : rawValue()}
191
+ minValue={min()}
192
+ maxValue={max()}
193
+ indeterminate={isIndeterminate() || durationMs() != null}
194
+ getValueLabel={({ value, min, max }) => {
195
+ if (local['aria-valuetext']) return local['aria-valuetext']
196
+ const pct = getPercent(value, min, max)
197
+ return formatter().format(pct / 100)
198
+ }}
199
+ class={cn('w-full', local.class, classNames().base)}
200
+ aria-label={ariaLabel()}
201
+ aria-labelledby={local['aria-labelledby']}
202
+ aria-describedby={local['aria-describedby']}
203
+ aria-disabled={local.isDisabled ? true : undefined}
204
+ >
205
+ {/* Style tags are injected per instance — CSS rules are idempotent so
206
+ duplicates are harmless, just slightly wasteful if many Progress bars
207
+ are on screen simultaneously. */}
208
+ <Show when={durationMs() != null || isIndeterminate()}>
209
+ <style>
210
+ {`@keyframes ${ANIMATION_NAME} { to { width: 100%; } }
211
+ @keyframes ${INDETERMINATE_NAME} {
212
+ 0% { transform: translateX(-100%); }
213
+ 100% { transform: translateX(400%); }
214
+ }`}
215
+ </style>
216
+ </Show>
217
+ <Show when={local.isStriped}>
218
+ <style>
219
+ {`.torchui-progress-stripes {
220
+ position: absolute;
221
+ inset: 0;
222
+ pointer-events: none;
223
+ background-image: repeating-linear-gradient(
224
+ -45deg,
225
+ transparent 0,
226
+ transparent 10px,
227
+ rgba(255,255,255,.25) 10px,
228
+ rgba(255,255,255,.25) 20px
229
+ );
230
+ border-radius: inherit;
231
+ }`}
232
+ </style>
233
+ </Show>
234
+ <Show when={local.label != null || valueDisplay() != null}>
235
+ <div
236
+ class={cn(
237
+ 'flex items-center justify-between gap-2 mb-1',
238
+ classNames().labelWrapper
239
+ )}
240
+ >
241
+ <Show when={local.label != null}>
242
+ <KobalteProgress.Label class={cn('text-sm font-medium text-ink-700', classNames().label)}>
243
+ {local.label}
244
+ </KobalteProgress.Label>
245
+ </Show>
246
+ <Show when={valueDisplay() != null}>
247
+ <KobalteProgress.ValueLabel class={cn('text-sm text-ink-600', classNames().value)}>
248
+ {valueDisplay()}
249
+ </KobalteProgress.ValueLabel>
250
+ </Show>
251
+ </div>
252
+ </Show>
253
+ <KobalteProgress.Track
254
+ class={cn(
255
+ 'w-full overflow-hidden bg-surface-dim',
256
+ SIZE_CLASSES[size()],
257
+ RADIUS_CLASSES[radius()],
258
+ local.trackClass,
259
+ classNames().track
260
+ )}
261
+ >
262
+ {segments() != null ? (
263
+ <div class="h-full w-full flex gap-0.5">
264
+ <For each={segmentIndexes()}>
265
+ {(i) => {
266
+ const count = segments()!
267
+ const filled = () => (percent() / 100) * count > i
268
+ const segRadius = () =>
269
+ i === 0 ? EDGE_RADIUS_CLASSES[radius()].left
270
+ : i === count - 1 ? EDGE_RADIUS_CLASSES[radius()].right
271
+ : 'rounded-none'
272
+ return (
273
+ <div
274
+ class={cn(
275
+ 'h-full flex-1 transition-colors duration-200',
276
+ segRadius(),
277
+ filled()
278
+ ? local.fillClass ?? COLOR_CLASSES[color()]
279
+ : 'bg-surface-dim',
280
+ classNames().indicator
281
+ )}
282
+ />
283
+ )
284
+ }}
285
+ </For>
286
+ </div>
287
+ ) : (
288
+ <KobalteProgress.Fill
289
+ class={cn(
290
+ 'h-full transition-[width] ease-linear relative',
291
+ RADIUS_CLASSES[radius()],
292
+ !durationMs() && !isIndeterminate() && !local.disableAnimation && 'duration-200',
293
+ local.fillClass ?? COLOR_CLASSES[color()],
294
+ classNames().indicator
295
+ )}
296
+ style={
297
+ isIndeterminate()
298
+ ? {
299
+ width: '25%',
300
+ animation: local.disableAnimation
301
+ ? undefined
302
+ : `${INDETERMINATE_NAME} 1.5s ease-in-out infinite`,
303
+ }
304
+ : durationMs() != null
305
+ ? {
306
+ width: '0%',
307
+ animation: `${ANIMATION_NAME} ${durationMs()}ms linear forwards`,
308
+ }
309
+ : { width: 'var(--kb-progress-fill-width)' }
310
+ }
311
+ >
312
+ <Show when={local.isStriped}>
313
+ <div class="torchui-progress-stripes" aria-hidden="true" />
314
+ </Show>
315
+ </KobalteProgress.Fill>
316
+ )}
317
+ </KobalteProgress.Track>
318
+ </KobalteProgress>
319
+ )
320
+ }
321
+
@@ -0,0 +1,62 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { splitProps } from 'solid-js'
3
+ import { cn } from '../../utilities/classNames'
4
+
5
+ const STANDALONE_CLASS =
6
+ 'inline-block bg-ink-200 animate-pulse'
7
+
8
+ export interface SkeletonProps {
9
+ /** Extra class for the wrapper (or the standalone block). */
10
+ class?: string
11
+ /** Use "full" for circular (e.g. avatar), "lg" for large radius, or omit for default. Matches the wrapped child's shape when you pass the same as the child (e.g. round="full" for rounded-full). */
12
+ round?: 'full' | 'lg' | 'md' | 'sm' | 'none'
13
+ /** Use block layout instead of inline-block. Needed when wrapping children that depend on parent width (w-full, flex-1, %-based). Default: false. */
14
+ block?: boolean
15
+ /** Wrap content to take its shape; omit for a standalone block you size with class.
16
+ * Note: wrap mode works best with intrinsic-size children. For layout-dependent children
17
+ * (w-full, flex-1, %-widths), set block={true} so the wrapper participates in flow layout.
18
+ * Skeleton is always aria-hidden — pair with a Loading status region for screen reader announcements. */
19
+ children?: JSX.Element
20
+ }
21
+
22
+ const ROUND_CLASS: Record<NonNullable<SkeletonProps['round']>, string> = {
23
+ full: 'rounded-full',
24
+ lg: 'rounded-lg',
25
+ md: 'rounded-md',
26
+ sm: 'rounded-sm',
27
+ none: 'rounded-none',
28
+ }
29
+
30
+ /**
31
+ * Skeleton takes the shape of its children by default (wrap mode), or use as a standalone
32
+ * block by omitting children and sizing with class. Use the round prop to match your
33
+ * child (e.g. round="full" for avatars so the skeleton is a circle).
34
+ */
35
+ export function Skeleton(props: SkeletonProps): JSX.Element {
36
+ const [local] = splitProps(props, ['class', 'round', 'block', 'children'])
37
+
38
+ const roundClass = local.round ? ROUND_CLASS[local.round] : 'rounded'
39
+
40
+ if (local.children == null) {
41
+ return (
42
+ <div
43
+ class={cn(STANDALONE_CLASS, roundClass, local.class)}
44
+ aria-hidden="true"
45
+ />
46
+ )
47
+ }
48
+
49
+ return (
50
+ <div
51
+ class={cn('relative', local.block ? 'block' : 'inline-block', roundClass, local.class)}
52
+ aria-hidden="true"
53
+ >
54
+ <div class="invisible">
55
+ {local.children}
56
+ </div>
57
+ <div
58
+ class={cn('absolute inset-0', roundClass, STANDALONE_CLASS)}
59
+ />
60
+ </div>
61
+ )
62
+ }