@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.
- package/README.md +166 -0
- package/package.json +67 -0
- package/src/components/actions/Button.tsx +612 -0
- package/src/components/actions/ButtonGroup.tsx +728 -0
- package/src/components/actions/Copy.tsx +98 -0
- package/src/components/actions/DarkModeToggle.tsx +80 -0
- package/src/components/actions/Link.tsx +37 -0
- package/src/components/actions/index.ts +19 -0
- package/src/components/actions/useCopyToClipboard.ts +90 -0
- package/src/components/charts/Chart.tsx +331 -0
- package/src/components/charts/Sparkline.tsx +156 -0
- package/src/components/charts/index.ts +13 -0
- package/src/components/data-display/Avatar.tsx +208 -0
- package/src/components/data-display/AvatarGroup.tsx +228 -0
- package/src/components/data-display/Badge.tsx +70 -0
- package/src/components/data-display/Carousel.tsx +214 -0
- package/src/components/data-display/ColorSwatch.tsx +56 -0
- package/src/components/data-display/DataTable.tsx +886 -0
- package/src/components/data-display/EmptyState.tsx +61 -0
- package/src/components/data-display/Image.tsx +277 -0
- package/src/components/data-display/Kbd.tsx +114 -0
- package/src/components/data-display/Persona.tsx +78 -0
- package/src/components/data-display/StatCard.tsx +338 -0
- package/src/components/data-display/Table.tsx +147 -0
- package/src/components/data-display/Tag.tsx +91 -0
- package/src/components/data-display/Timeline.tsx +200 -0
- package/src/components/data-display/TreeView.tsx +172 -0
- package/src/components/data-display/Video.tsx +95 -0
- package/src/components/data-display/avatar-utils.ts +32 -0
- package/src/components/data-display/index.ts +81 -0
- package/src/components/feedback/Loading.tsx +159 -0
- package/src/components/feedback/Progress.tsx +321 -0
- package/src/components/feedback/Skeleton.tsx +62 -0
- package/src/components/feedback/SkeletonBlocks.tsx +222 -0
- package/src/components/feedback/Toast.tsx +648 -0
- package/src/components/feedback/index.ts +44 -0
- package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
- package/src/components/feedback/password/password-strength.ts +115 -0
- package/src/components/feedback/password/password-validation-data.ts +66 -0
- package/src/components/feedback/password/password-validation.ts +93 -0
- package/src/components/forms/Autocomplete.tsx +268 -0
- package/src/components/forms/Checkbox.tsx +155 -0
- package/src/components/forms/CodeInput.tsx +237 -0
- package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
- package/src/components/forms/ColorPicker/color-utils.ts +75 -0
- package/src/components/forms/ColorPicker/index.ts +2 -0
- package/src/components/forms/DatePicker.tsx +516 -0
- package/src/components/forms/DateRangePicker.tsx +464 -0
- package/src/components/forms/FieldPicker.tsx +64 -0
- package/src/components/forms/FileUpload.tsx +614 -0
- package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
- package/src/components/forms/FilterBuilder.tsx +16 -0
- package/src/components/forms/FilterRuleRow.tsx +68 -0
- package/src/components/forms/Input.tsx +200 -0
- package/src/components/forms/MultiSelect.tsx +361 -0
- package/src/components/forms/NumberField.tsx +145 -0
- package/src/components/forms/RadioGroup.tsx +135 -0
- package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
- package/src/components/forms/ReorderableList.tsx +163 -0
- package/src/components/forms/Select.tsx +268 -0
- package/src/components/forms/Slider.tsx +260 -0
- package/src/components/forms/Switch.tsx +135 -0
- package/src/components/forms/TextArea.tsx +202 -0
- package/src/components/forms/ViewCustomizer.tsx +44 -0
- package/src/components/forms/index.ts +43 -0
- package/src/components/layout/Accordion.tsx +110 -0
- package/src/components/layout/Alert.tsx +156 -0
- package/src/components/layout/BlockQuote.tsx +70 -0
- package/src/components/layout/Card.tsx +166 -0
- package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
- package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
- package/src/components/layout/CodeBlock/prism.ts +81 -0
- package/src/components/layout/Collapsible.tsx +84 -0
- package/src/components/layout/Container.tsx +55 -0
- package/src/components/layout/Divider.tsx +64 -0
- package/src/components/layout/Form.tsx +39 -0
- package/src/components/layout/FormActions.tsx +50 -0
- package/src/components/layout/Grid.tsx +53 -0
- package/src/components/layout/PageHeading.tsx +46 -0
- package/src/components/layout/PromptWithAction.tsx +49 -0
- package/src/components/layout/Section.tsx +60 -0
- package/src/components/layout/TablePanel.tsx +24 -0
- package/src/components/layout/TableView/TableView.tsx +1018 -0
- package/src/components/layout/TableView/index.ts +3 -0
- package/src/components/layout/TableView/types.ts +51 -0
- package/src/components/layout/WizardStep.tsx +40 -0
- package/src/components/layout/WizardStepper.tsx +173 -0
- package/src/components/layout/index.ts +96 -0
- package/src/components/navigation/Breadcrumbs.tsx +66 -0
- package/src/components/navigation/DropdownMenu.tsx +86 -0
- package/src/components/navigation/MegaMenu.tsx +480 -0
- package/src/components/navigation/NavigationMenu.tsx +305 -0
- package/src/components/navigation/Pagination.tsx +298 -0
- package/src/components/navigation/Sidebar.tsx +280 -0
- package/src/components/navigation/Tabs.tsx +122 -0
- package/src/components/navigation/ViewSwitcher.tsx +314 -0
- package/src/components/navigation/index.ts +66 -0
- package/src/components/overlays/AlertDialog.tsx +174 -0
- package/src/components/overlays/ContextMenu.tsx +65 -0
- package/src/components/overlays/Dialog.tsx +279 -0
- package/src/components/overlays/Drawer.tsx +370 -0
- package/src/components/overlays/HoverCard.tsx +107 -0
- package/src/components/overlays/Popover.tsx +73 -0
- package/src/components/overlays/Tooltip.tsx +31 -0
- package/src/components/overlays/index.ts +71 -0
- package/src/components/typography/Code.tsx +72 -0
- package/src/components/typography/Icon.tsx +36 -0
- package/src/components/typography/index.ts +10 -0
- package/src/env.d.ts +9 -0
- package/src/index.ts +13 -0
- package/src/styles/theme.css +226 -0
- package/src/types/avatar-types.ts +11 -0
- package/src/types/filter-types.ts +35 -0
- package/src/utilities/classNames.ts +6 -0
- package/src/utilities/componentSize.ts +46 -0
- package/src/utilities/i18n.tsx +60 -0
- package/src/utilities/mergeRefs.ts +12 -0
- 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
|
+
}
|