@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,268 @@
|
|
|
1
|
+
import { createSignal, createUniqueId, splitProps, Show, type JSX } from 'solid-js'
|
|
2
|
+
import { Check, ChevronDown } from 'lucide-solid'
|
|
3
|
+
import { Select as KobalteSelect } from '@kobalte/core/select'
|
|
4
|
+
import { cn } from '../../utilities/classNames'
|
|
5
|
+
import { type ComponentSize, inputSizeConfig } from '../../utilities/componentSize'
|
|
6
|
+
|
|
7
|
+
export interface SelectOption {
|
|
8
|
+
value: string
|
|
9
|
+
label: string
|
|
10
|
+
/** Optional icon shown before the label. */
|
|
11
|
+
icon?: JSX.Element
|
|
12
|
+
/** Optional hex or CSS color for a status dot (e.g. #22c55e). When set, a small colored dot is shown before the label; useful for status/state fields. */
|
|
13
|
+
color?: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function statusColorStyle(color: string | null | undefined): string | undefined {
|
|
17
|
+
if (color == null || color === '') return undefined
|
|
18
|
+
const t = color.trim()
|
|
19
|
+
if (t.startsWith('#') || t.startsWith('rgb') || /^[a-z]+$/i.test(t)) return t
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function StatusDot(props: { color?: string | null }) {
|
|
24
|
+
const style = () => {
|
|
25
|
+
const c = statusColorStyle(props.color)
|
|
26
|
+
return c ? { 'background-color': c } : undefined
|
|
27
|
+
}
|
|
28
|
+
return (
|
|
29
|
+
<span
|
|
30
|
+
class="size-2.5 shrink-0 rounded-full border border-ink-200/80/80"
|
|
31
|
+
classList={{ 'bg-ink-400': !statusColorStyle(props.color) }}
|
|
32
|
+
style={style()}
|
|
33
|
+
aria-hidden
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SelectProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
39
|
+
label?: string
|
|
40
|
+
error?: string
|
|
41
|
+
helperText?: string
|
|
42
|
+
/** When true, never render label row or error/helper text (control only). */
|
|
43
|
+
bare?: boolean
|
|
44
|
+
required?: boolean
|
|
45
|
+
/** When true, show "optional" on the label row when not required. Default false. */
|
|
46
|
+
optional?: boolean
|
|
47
|
+
options: SelectOption[]
|
|
48
|
+
placeholder?: string
|
|
49
|
+
value?: string
|
|
50
|
+
onValueChange?: (value: string) => void
|
|
51
|
+
onErrorClear?: () => void
|
|
52
|
+
disabled?: boolean
|
|
53
|
+
/** Applied to the root wrapper (label + control + helper/error). Use for layout (e.g. max-w-sm). */
|
|
54
|
+
class?: string
|
|
55
|
+
/** Applied to the trigger element for height/sizing (e.g. min-h-[50px] h-[50px]). */
|
|
56
|
+
triggerClass?: string
|
|
57
|
+
/** Input size. Controls height, text size, and padding. Default: md (36px). */
|
|
58
|
+
size?: ComponentSize
|
|
59
|
+
/** When true, show a search input in the dropdown to filter options by label. Default false. */
|
|
60
|
+
searchable?: boolean
|
|
61
|
+
/** Ref forwarded to the root wrapper div. */
|
|
62
|
+
ref?: (el: HTMLDivElement) => void
|
|
63
|
+
/** Id for the root wrapper (e.g. for aria-labelledby / label for). */
|
|
64
|
+
id?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Select = (props: SelectProps) => {
|
|
68
|
+
const [local, others] = splitProps(props, [
|
|
69
|
+
'label',
|
|
70
|
+
'error',
|
|
71
|
+
'helperText',
|
|
72
|
+
'bare',
|
|
73
|
+
'required',
|
|
74
|
+
'optional',
|
|
75
|
+
'options',
|
|
76
|
+
'placeholder',
|
|
77
|
+
'value',
|
|
78
|
+
'onValueChange',
|
|
79
|
+
'onErrorClear',
|
|
80
|
+
'disabled',
|
|
81
|
+
'class',
|
|
82
|
+
'triggerClass',
|
|
83
|
+
'size',
|
|
84
|
+
'searchable',
|
|
85
|
+
'ref',
|
|
86
|
+
'id',
|
|
87
|
+
])
|
|
88
|
+
const [searchQuery, setSearchQuery] = createSignal('')
|
|
89
|
+
const sc = () => inputSizeConfig[local.size ?? 'md']
|
|
90
|
+
|
|
91
|
+
const hasError = () => !!local.error
|
|
92
|
+
const uid = createUniqueId()
|
|
93
|
+
const helperId = () => (!local.bare && local.helperText ? `select-${uid}-help` : undefined)
|
|
94
|
+
const errorId = () => (!local.bare && local.error ? `select-${uid}-error` : undefined)
|
|
95
|
+
const describedBy = () => [helperId(), errorId()].filter(Boolean).join(' ') || undefined
|
|
96
|
+
const selectedOption = () =>
|
|
97
|
+
local.value != null && local.value !== ''
|
|
98
|
+
? local.options.find((opt) => opt.value === local.value)
|
|
99
|
+
: undefined
|
|
100
|
+
|
|
101
|
+
const filteredOptions = (): SelectOption[] => {
|
|
102
|
+
if (!local.searchable) return local.options
|
|
103
|
+
const q = searchQuery().trim().toLowerCase()
|
|
104
|
+
if (!q) return local.options
|
|
105
|
+
const selected = selectedOption()
|
|
106
|
+
const filtered = local.options.filter((o) => o.label.toLowerCase().includes(q))
|
|
107
|
+
if (selected && !filtered.some((o) => o.value === selected.value))
|
|
108
|
+
return [selected, ...filtered]
|
|
109
|
+
return filtered
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const handleChange = (option: SelectOption | null) => {
|
|
113
|
+
if (local.error && local.onErrorClear) {
|
|
114
|
+
local.onErrorClear()
|
|
115
|
+
}
|
|
116
|
+
local.onValueChange?.(option ? option.value : '')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let searchInputRef: HTMLInputElement | undefined
|
|
120
|
+
const handleOpenChange = (open: boolean) => {
|
|
121
|
+
if (!open) setSearchQuery('')
|
|
122
|
+
else if (local.searchable) {
|
|
123
|
+
requestAnimationFrame(() => searchInputRef?.focus())
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div ref={local.ref} id={local.id} class={cn('w-full', local.class)} {...others}>
|
|
129
|
+
<KobalteSelect<SelectOption>
|
|
130
|
+
value={selectedOption() ?? undefined}
|
|
131
|
+
onChange={handleChange}
|
|
132
|
+
options={filteredOptions()}
|
|
133
|
+
onOpenChange={handleOpenChange}
|
|
134
|
+
optionValue="value"
|
|
135
|
+
optionTextValue="label"
|
|
136
|
+
placeholder={local.placeholder || 'Select an option'}
|
|
137
|
+
disabled={local.disabled}
|
|
138
|
+
validationState={hasError() ? 'invalid' : undefined}
|
|
139
|
+
closeOnSelection={true}
|
|
140
|
+
itemComponent={(itemProps) => (
|
|
141
|
+
<KobalteSelect.Item
|
|
142
|
+
item={itemProps.item}
|
|
143
|
+
class="relative flex items-center justify-between px-4 py-2.5 text-sm cursor-pointer outline-none text-ink-900 data-[highlighted]:bg-primary-50 data-[highlighted]:text-primary-900 dark:data-[highlighted]:bg-primary-500/20 dark:data-[highlighted]:text-primary-200 data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed"
|
|
144
|
+
>
|
|
145
|
+
<KobalteSelect.ItemLabel class="flex-1">
|
|
146
|
+
<span class="flex min-w-0 items-center gap-2">
|
|
147
|
+
<Show when={statusColorStyle(itemProps.item.rawValue.color)}>
|
|
148
|
+
<StatusDot color={itemProps.item.rawValue.color} />
|
|
149
|
+
</Show>
|
|
150
|
+
<Show when={itemProps.item.rawValue.icon}>
|
|
151
|
+
<span class="flex-shrink-0 text-ink-500">{itemProps.item.rawValue.icon}</span>
|
|
152
|
+
</Show>
|
|
153
|
+
<span class="min-w-0 truncate">{itemProps.item.rawValue.label}</span>
|
|
154
|
+
</span>
|
|
155
|
+
</KobalteSelect.ItemLabel>
|
|
156
|
+
<KobalteSelect.ItemIndicator class="inline-flex items-center">
|
|
157
|
+
<Check class="w-4 h-4 text-primary-500" />
|
|
158
|
+
</KobalteSelect.ItemIndicator>
|
|
159
|
+
</KobalteSelect.Item>
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<Show when={!local.bare && local.label}>
|
|
163
|
+
<div class="flex items-center justify-between mb-2">
|
|
164
|
+
<KobalteSelect.Label class="block text-md font-medium text-ink-700">
|
|
165
|
+
{local.label}
|
|
166
|
+
<Show when={local.required}>
|
|
167
|
+
<span class="ml-0.5 text-danger-500 dark:text-danger-400" aria-hidden="true">*</span>
|
|
168
|
+
</Show>
|
|
169
|
+
</KobalteSelect.Label>
|
|
170
|
+
<Show when={local.label && !local.required && local.optional}>
|
|
171
|
+
<span class="text-xs text-ink-500">optional</span>
|
|
172
|
+
</Show>
|
|
173
|
+
</div>
|
|
174
|
+
</Show>
|
|
175
|
+
|
|
176
|
+
<div
|
|
177
|
+
class={cn(
|
|
178
|
+
'w-full flex flex-col min-h-0 rounded-lg border transition-all overflow-hidden',
|
|
179
|
+
sc().h,
|
|
180
|
+
hasError()
|
|
181
|
+
? 'border-danger-500 focus-within:ring-2 focus-within:ring-danger-500 focus-within:border-transparent'
|
|
182
|
+
: 'border-ink-300 focus-within:ring-2 focus-within:ring-primary-500 focus-within:border-transparent',
|
|
183
|
+
'bg-surface-raised'
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<KobalteSelect.Trigger
|
|
187
|
+
aria-invalid={hasError() ? 'true' : undefined}
|
|
188
|
+
aria-describedby={describedBy()}
|
|
189
|
+
aria-errormessage={hasError() ? errorId() : undefined}
|
|
190
|
+
class={cn(
|
|
191
|
+
'w-full h-full min-h-0 min-w-0 flex items-center gap-1 rounded-lg transition-all outline-none bg-transparent text-ink-900 text-left border-0 focus:ring-0',
|
|
192
|
+
sc().py, sc().text, sc().pl, sc().pr,
|
|
193
|
+
hasError()
|
|
194
|
+
? 'focus:border-transparent'
|
|
195
|
+
: '',
|
|
196
|
+
'disabled:bg-surface-base disabled:text-ink-500 dark:disabled:text-ink-500 disabled:cursor-not-allowed',
|
|
197
|
+
'data-[placeholder-shown]:text-ink-400 dark:data-[placeholder-shown]:text-ink-500',
|
|
198
|
+
local.triggerClass
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
<KobalteSelect.Value<SelectOption> class="min-w-0 flex-1 truncate text-left basis-0">
|
|
202
|
+
{(state) => {
|
|
203
|
+
const opt = state.selectedOption()
|
|
204
|
+
if (!opt) return <span class="truncate">{local.placeholder || 'Select an option'}</span>
|
|
205
|
+
return (
|
|
206
|
+
<span class="min-w-0 flex-1 truncate text-left flex items-center gap-2">
|
|
207
|
+
<Show when={statusColorStyle(opt.color)}>
|
|
208
|
+
<StatusDot color={opt.color} />
|
|
209
|
+
</Show>
|
|
210
|
+
<Show when={opt.icon}>
|
|
211
|
+
<span class="shrink-0 text-ink-500">{opt.icon}</span>
|
|
212
|
+
</Show>
|
|
213
|
+
<span class="min-w-0 truncate">{opt.label}</span>
|
|
214
|
+
</span>
|
|
215
|
+
)
|
|
216
|
+
}}
|
|
217
|
+
</KobalteSelect.Value>
|
|
218
|
+
<KobalteSelect.Icon class="inline-flex shrink-0 w-4 items-center justify-center text-ink-400">
|
|
219
|
+
<ChevronDown class="h-3.5 w-3.5" aria-hidden="true" />
|
|
220
|
+
</KobalteSelect.Icon>
|
|
221
|
+
</KobalteSelect.Trigger>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<Show when={!local.bare && !hasError() && local.helperText}>
|
|
225
|
+
<KobalteSelect.Description id={helperId()} class="mt-2 text-sm text-ink-500">
|
|
226
|
+
{local.helperText}
|
|
227
|
+
</KobalteSelect.Description>
|
|
228
|
+
</Show>
|
|
229
|
+
|
|
230
|
+
<Show when={!local.bare && hasError()}>
|
|
231
|
+
<KobalteSelect.ErrorMessage id={errorId()} class="mt-2 text-sm text-danger-600">
|
|
232
|
+
{local.error}
|
|
233
|
+
</KobalteSelect.ErrorMessage>
|
|
234
|
+
</Show>
|
|
235
|
+
|
|
236
|
+
<KobalteSelect.Portal>
|
|
237
|
+
<KobalteSelect.Content
|
|
238
|
+
class={cn(
|
|
239
|
+
'bg-surface-raised rounded-lg border border-surface-border shadow-lg mt-2 z-[100] flex flex-col max-h-60',
|
|
240
|
+
local.searchable ? 'py-0 overflow-hidden' : 'py-1 overflow-auto',
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
<Show when={local.searchable}>
|
|
244
|
+
<div
|
|
245
|
+
class="shrink-0 border-b border-surface-border p-2"
|
|
246
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
247
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
248
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
249
|
+
>
|
|
250
|
+
<input
|
|
251
|
+
ref={(el) => (searchInputRef = el)}
|
|
252
|
+
type="text"
|
|
253
|
+
value={searchQuery()}
|
|
254
|
+
onInput={(e) => setSearchQuery(e.currentTarget.value)}
|
|
255
|
+
placeholder="Search..."
|
|
256
|
+
class="h-9 w-full rounded-md border border-surface-border bg-surface-raised px-3 py-1.5 text-sm text-ink-900 placeholder:text-ink-400 dark:placeholder:text-ink-500 outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
</Show>
|
|
260
|
+
<div class={cn('outline-none min-h-0', local.searchable && 'flex-1 overflow-auto py-1')}>
|
|
261
|
+
<KobalteSelect.Listbox class="outline-none" />
|
|
262
|
+
</div>
|
|
263
|
+
</KobalteSelect.Content>
|
|
264
|
+
</KobalteSelect.Portal>
|
|
265
|
+
</KobalteSelect>
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { splitProps, Show, For } from 'solid-js'
|
|
3
|
+
import { Slider as KobalteSlider } from '@kobalte/core/slider'
|
|
4
|
+
import { cn } from '../../utilities/classNames'
|
|
5
|
+
|
|
6
|
+
export interface SliderProps {
|
|
7
|
+
/** Label for the slider. */
|
|
8
|
+
label?: string
|
|
9
|
+
/** Optional description. */
|
|
10
|
+
description?: string
|
|
11
|
+
/** Error message and invalid styling. */
|
|
12
|
+
error?: string
|
|
13
|
+
/** Controlled value(s). Single thumb: [number], range: [min, max]. */
|
|
14
|
+
value?: number[]
|
|
15
|
+
/** Default value(s) when uncontrolled. */
|
|
16
|
+
defaultValue?: number[]
|
|
17
|
+
/** Called when value changes. */
|
|
18
|
+
onChange?: (value: number[]) => void
|
|
19
|
+
/** Called when user finishes dragging. */
|
|
20
|
+
onChangeEnd?: (value: number[]) => void
|
|
21
|
+
/** Minimum value. Default 0. */
|
|
22
|
+
minValue?: number
|
|
23
|
+
/** Maximum value. Default 100. */
|
|
24
|
+
maxValue?: number
|
|
25
|
+
/** Step. Default 1. */
|
|
26
|
+
step?: number
|
|
27
|
+
/** Minimum steps between thumbs (for range). */
|
|
28
|
+
minStepsBetweenThumbs?: number
|
|
29
|
+
/** Custom accessible value label. */
|
|
30
|
+
getValueLabel?: (params: { values: number[] }) => string
|
|
31
|
+
/** Orientation. */
|
|
32
|
+
orientation?: 'horizontal' | 'vertical'
|
|
33
|
+
/** Content before the track (e.g. icon or min label). */
|
|
34
|
+
startContent?: JSX.Element
|
|
35
|
+
/** Content after the track (e.g. icon or max label). */
|
|
36
|
+
endContent?: JSX.Element
|
|
37
|
+
/** Track thickness (height for horizontal, width for vertical). Default sm. Length is controlled by container. */
|
|
38
|
+
size?: 'sm' | 'md' | 'lg'
|
|
39
|
+
/** Track and thumb color. Default primary (theme). */
|
|
40
|
+
color?: 'primary' | 'indigo' | 'rose'
|
|
41
|
+
/** Optional marks (e.g. [0, 25, 50, 75, 100]) shown below the track, aligned with step positions. */
|
|
42
|
+
marks?: number[]
|
|
43
|
+
/** Disabled. */
|
|
44
|
+
disabled?: boolean
|
|
45
|
+
/** Name for form submission. */
|
|
46
|
+
name?: string
|
|
47
|
+
/** Root class. */
|
|
48
|
+
class?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function Slider(props: SliderProps) {
|
|
52
|
+
const [local, others] = splitProps(props, [
|
|
53
|
+
'label',
|
|
54
|
+
'description',
|
|
55
|
+
'error',
|
|
56
|
+
'value',
|
|
57
|
+
'defaultValue',
|
|
58
|
+
'onChange',
|
|
59
|
+
'onChangeEnd',
|
|
60
|
+
'minValue',
|
|
61
|
+
'maxValue',
|
|
62
|
+
'step',
|
|
63
|
+
'minStepsBetweenThumbs',
|
|
64
|
+
'getValueLabel',
|
|
65
|
+
'orientation',
|
|
66
|
+
'startContent',
|
|
67
|
+
'endContent',
|
|
68
|
+
'size',
|
|
69
|
+
'color',
|
|
70
|
+
'marks',
|
|
71
|
+
'disabled',
|
|
72
|
+
'name',
|
|
73
|
+
'class',
|
|
74
|
+
])
|
|
75
|
+
const hasError = () => !!local.error
|
|
76
|
+
const thumbCount = () => {
|
|
77
|
+
const v = local.value ?? local.defaultValue ?? [50]
|
|
78
|
+
return Math.max(1, Math.min(v.length, 2))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const orientation = () => local.orientation ?? 'horizontal'
|
|
82
|
+
const isHorizontal = () => orientation() === 'horizontal'
|
|
83
|
+
const size = () => local.size ?? 'sm'
|
|
84
|
+
|
|
85
|
+
const trackSizeClass = () =>
|
|
86
|
+
isHorizontal()
|
|
87
|
+
? size() === 'sm'
|
|
88
|
+
? 'h-2'
|
|
89
|
+
: size() === 'lg'
|
|
90
|
+
? 'h-4'
|
|
91
|
+
: 'h-3'
|
|
92
|
+
: size() === 'sm'
|
|
93
|
+
? 'w-2'
|
|
94
|
+
: size() === 'lg'
|
|
95
|
+
? 'w-4'
|
|
96
|
+
: 'w-3'
|
|
97
|
+
const thumbSizeClass = () =>
|
|
98
|
+
size() === 'sm' ? 'h-4 w-4' : size() === 'lg' ? 'h-6 w-6' : 'h-5 w-5'
|
|
99
|
+
|
|
100
|
+
const color = () => local.color ?? 'primary'
|
|
101
|
+
const trackBgClass = () =>
|
|
102
|
+
color() === 'indigo'
|
|
103
|
+
? 'bg-indigo-300/80 dark:bg-indigo-500/40'
|
|
104
|
+
: color() === 'rose'
|
|
105
|
+
? 'bg-rose-300/80 dark:bg-rose-500/40'
|
|
106
|
+
: 'bg-primary-300/80 dark:bg-primary-500/40'
|
|
107
|
+
const fillThumbClass = () =>
|
|
108
|
+
color() === 'indigo'
|
|
109
|
+
? 'bg-indigo-500 dark:bg-indigo-400'
|
|
110
|
+
: color() === 'rose'
|
|
111
|
+
? 'bg-rose-500 dark:bg-rose-400'
|
|
112
|
+
: 'bg-primary-500 dark:bg-primary-400'
|
|
113
|
+
const focusRingClass = () =>
|
|
114
|
+
color() === 'indigo'
|
|
115
|
+
? 'focus-visible:ring-indigo-400 dark:focus-visible:ring-indigo-300'
|
|
116
|
+
: color() === 'rose'
|
|
117
|
+
? 'focus-visible:ring-rose-400 dark:focus-visible:ring-rose-300'
|
|
118
|
+
: 'focus-visible:ring-primary-400 dark:focus-visible:ring-primary-300'
|
|
119
|
+
|
|
120
|
+
const thumbCenterStyle = (): Record<string, string> =>
|
|
121
|
+
isHorizontal()
|
|
122
|
+
? { top: '50%', transform: 'translate(-50%, -50%)' }
|
|
123
|
+
: { left: '50%', transform: 'translate(-50%, -50%)' }
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<KobalteSlider
|
|
127
|
+
value={local.value}
|
|
128
|
+
defaultValue={local.defaultValue ?? [50]}
|
|
129
|
+
onChange={local.onChange}
|
|
130
|
+
onChangeEnd={local.onChangeEnd}
|
|
131
|
+
minValue={local.minValue ?? 0}
|
|
132
|
+
maxValue={local.maxValue ?? 100}
|
|
133
|
+
step={local.step ?? 1}
|
|
134
|
+
minStepsBetweenThumbs={local.minStepsBetweenThumbs}
|
|
135
|
+
getValueLabel={local.getValueLabel}
|
|
136
|
+
orientation={orientation()}
|
|
137
|
+
disabled={local.disabled}
|
|
138
|
+
name={local.name}
|
|
139
|
+
validationState={hasError() ? 'invalid' : undefined}
|
|
140
|
+
class={cn(
|
|
141
|
+
'group/slider flex flex-col gap-1.5',
|
|
142
|
+
isHorizontal() ? 'w-full' : 'h-full w-fit min-h-0 flex-col items-center',
|
|
143
|
+
local.disabled && 'is-disabled',
|
|
144
|
+
local.class
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
<Show when={local.label && isHorizontal()}>
|
|
148
|
+
<div class="flex items-center justify-between gap-2 min-w-0">
|
|
149
|
+
<KobalteSlider.Label
|
|
150
|
+
class={cn(
|
|
151
|
+
'text-sm font-medium text-ink-700 shrink-0',
|
|
152
|
+
hasError() && 'text-danger-600 dark:text-danger-400'
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
{local.label}
|
|
156
|
+
</KobalteSlider.Label>
|
|
157
|
+
<KobalteSlider.ValueLabel class="text-sm text-ink-500 shrink-0 min-w-[2.5rem] text-right tabular-nums" />
|
|
158
|
+
</div>
|
|
159
|
+
</Show>
|
|
160
|
+
<Show when={local.label && !isHorizontal()}>
|
|
161
|
+
<KobalteSlider.Label
|
|
162
|
+
class={cn(
|
|
163
|
+
'text-sm font-medium text-ink-700',
|
|
164
|
+
hasError() && 'text-danger-600 dark:text-danger-400'
|
|
165
|
+
)}
|
|
166
|
+
>
|
|
167
|
+
{local.label}
|
|
168
|
+
</KobalteSlider.Label>
|
|
169
|
+
</Show>
|
|
170
|
+
<div
|
|
171
|
+
class={cn(
|
|
172
|
+
'flex min-w-0',
|
|
173
|
+
isHorizontal() ? 'w-full flex-row items-center gap-2' : 'h-full min-h-0 w-fit flex-col items-center gap-2'
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
<Show when={local.startContent}>
|
|
177
|
+
<div class="shrink-0 text-ink-500 [&>svg]:h-4 [&>svg]:w-4">
|
|
178
|
+
{local.startContent}
|
|
179
|
+
</div>
|
|
180
|
+
</Show>
|
|
181
|
+
<div class={cn('flex min-w-0 flex-col gap-1', isHorizontal() ? 'flex-1' : 'min-h-0 flex-1')}>
|
|
182
|
+
<KobalteSlider.Track
|
|
183
|
+
class={cn(
|
|
184
|
+
'relative shrink-0 overflow-visible rounded-full transition-colors',
|
|
185
|
+
trackBgClass(),
|
|
186
|
+
'group-[.is-disabled]/slider:bg-ink-100 dark:group-[.is-disabled]/slider:bg-ink-800',
|
|
187
|
+
trackSizeClass(),
|
|
188
|
+
isHorizontal() ? 'w-full min-w-0' : 'h-full min-h-0'
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
<KobalteSlider.Fill
|
|
192
|
+
class={cn(
|
|
193
|
+
'absolute rounded-full transition-colors',
|
|
194
|
+
isHorizontal() ? 'inset-y-0 left-0' : 'inset-x-0',
|
|
195
|
+
fillThumbClass(),
|
|
196
|
+
'group-[.is-disabled]/slider:bg-ink-300 dark:group-[.is-disabled]/slider:bg-ink-600'
|
|
197
|
+
)}
|
|
198
|
+
/>
|
|
199
|
+
<For each={Array.from({ length: thumbCount() })}>
|
|
200
|
+
{() => (
|
|
201
|
+
<KobalteSlider.Thumb
|
|
202
|
+
style={thumbCenterStyle()}
|
|
203
|
+
class={cn(
|
|
204
|
+
'relative z-10 block rounded-full border-0 outline-none transition cursor-pointer touch-none',
|
|
205
|
+
thumbSizeClass(),
|
|
206
|
+
fillThumbClass(),
|
|
207
|
+
'shadow-[0_1px_3px_rgba(0,0,0,0.12)] dark:shadow-[0_1px_3px_rgba(0,0,0,0.4)]',
|
|
208
|
+
'ring-0 hover:ring-0 focus-visible:ring-2 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-ink-900',
|
|
209
|
+
focusRingClass(),
|
|
210
|
+
'group-[.is-disabled]/slider:bg-ink-300 dark:group-[.is-disabled]/slider:bg-ink-600 group-[.is-disabled]/slider:cursor-not-allowed group-[.is-disabled]/slider:shadow-none'
|
|
211
|
+
)}
|
|
212
|
+
>
|
|
213
|
+
<KobalteSlider.Input />
|
|
214
|
+
</KobalteSlider.Thumb>
|
|
215
|
+
)}
|
|
216
|
+
</For>
|
|
217
|
+
</KobalteSlider.Track>
|
|
218
|
+
<Show when={local.marks && local.marks.length > 0 && isHorizontal()}>
|
|
219
|
+
<div class="relative min-h-[1.25rem] w-full min-w-0 pt-0.5 text-xs text-ink-500">
|
|
220
|
+
<For each={local.marks!}>
|
|
221
|
+
{(m) => {
|
|
222
|
+
const min = local.minValue ?? 0
|
|
223
|
+
const max = local.maxValue ?? 100
|
|
224
|
+
const range = max - min
|
|
225
|
+
const pct = range === 0 ? 0 : ((m - min) / range) * 100
|
|
226
|
+
return (
|
|
227
|
+
<span
|
|
228
|
+
class="absolute -translate-x-1/2"
|
|
229
|
+
style={{ left: `${pct}%` }}
|
|
230
|
+
>
|
|
231
|
+
{m}
|
|
232
|
+
</span>
|
|
233
|
+
)
|
|
234
|
+
}}
|
|
235
|
+
</For>
|
|
236
|
+
</div>
|
|
237
|
+
</Show>
|
|
238
|
+
</div>
|
|
239
|
+
<Show when={local.endContent}>
|
|
240
|
+
<div class="shrink-0 text-ink-500 [&>svg]:h-4 [&>svg]:w-4">
|
|
241
|
+
{local.endContent}
|
|
242
|
+
</div>
|
|
243
|
+
</Show>
|
|
244
|
+
</div>
|
|
245
|
+
<Show when={local.label && !isHorizontal()}>
|
|
246
|
+
<KobalteSlider.ValueLabel class="text-sm text-ink-500 text-center tabular-nums" />
|
|
247
|
+
</Show>
|
|
248
|
+
<Show when={local.description}>
|
|
249
|
+
<KobalteSlider.Description class="mt-1 text-xs text-ink-500">
|
|
250
|
+
{local.description}
|
|
251
|
+
</KobalteSlider.Description>
|
|
252
|
+
</Show>
|
|
253
|
+
<Show when={local.error}>
|
|
254
|
+
<KobalteSlider.ErrorMessage class="mt-1 text-xs text-danger-600 dark:text-danger-400">
|
|
255
|
+
{local.error}
|
|
256
|
+
</KobalteSlider.ErrorMessage>
|
|
257
|
+
</Show>
|
|
258
|
+
</KobalteSlider>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { splitProps, Show } from 'solid-js'
|
|
3
|
+
import { Switch as KobalteSwitch } from '@kobalte/core/switch'
|
|
4
|
+
import { cn } from '../../utilities/classNames'
|
|
5
|
+
|
|
6
|
+
export interface SwitchProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
|
|
7
|
+
/** Label text. */
|
|
8
|
+
label?: string
|
|
9
|
+
/** Optional description below the label. */
|
|
10
|
+
description?: string
|
|
11
|
+
/** Error message and invalid styling. */
|
|
12
|
+
error?: string
|
|
13
|
+
/** When true, show required indicator on label. */
|
|
14
|
+
required?: boolean
|
|
15
|
+
/** Controlled checked state. */
|
|
16
|
+
checked?: boolean
|
|
17
|
+
/** Default checked (uncontrolled). */
|
|
18
|
+
defaultChecked?: boolean
|
|
19
|
+
/** Called when checked state changes. */
|
|
20
|
+
onChange?: (checked: boolean) => void
|
|
21
|
+
/** Disables the switch. */
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
/** For form submission when checked (e.g. "on"). */
|
|
24
|
+
value?: string
|
|
25
|
+
/** Form name. */
|
|
26
|
+
name?: string
|
|
27
|
+
/** Additional class for the root wrapper. */
|
|
28
|
+
class?: string
|
|
29
|
+
/** Additional class for the control (track). */
|
|
30
|
+
controlClass?: string
|
|
31
|
+
/** Ref forwarded to the root wrapper div. */
|
|
32
|
+
ref?: (el: HTMLDivElement) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Switch(props: SwitchProps) {
|
|
36
|
+
const [local, others] = splitProps(props, [
|
|
37
|
+
'label',
|
|
38
|
+
'description',
|
|
39
|
+
'error',
|
|
40
|
+
'required',
|
|
41
|
+
'checked',
|
|
42
|
+
'defaultChecked',
|
|
43
|
+
'onChange',
|
|
44
|
+
'disabled',
|
|
45
|
+
'value',
|
|
46
|
+
'name',
|
|
47
|
+
'class',
|
|
48
|
+
'controlClass',
|
|
49
|
+
'ref',
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
const hasError = () => !!local.error
|
|
53
|
+
|
|
54
|
+
// If there's no visible label, the switch still needs a name.
|
|
55
|
+
const a11yNameOk = () =>
|
|
56
|
+
local.label != null ||
|
|
57
|
+
(others['aria-label'] as string | undefined) != null ||
|
|
58
|
+
(others['aria-labelledby'] as string | undefined) != null ||
|
|
59
|
+
(others['title'] as string | undefined) != null
|
|
60
|
+
|
|
61
|
+
if (import.meta.env.DEV && !a11yNameOk()) {
|
|
62
|
+
console.warn('Switch: when label is omitted, provide aria-label, aria-labelledby, or title for accessibility.')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Forward "naming" attributes to the actual switch root (not just the wrapper div).
|
|
66
|
+
const switchA11yProps = () => ({
|
|
67
|
+
'aria-label': local.label ? undefined : (others['aria-label'] as string | undefined),
|
|
68
|
+
'aria-labelledby': local.label ? undefined : (others['aria-labelledby'] as string | undefined),
|
|
69
|
+
title: local.label ? undefined : (others['title'] as string | undefined),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div ref={local.ref} class={cn('w-full', local.class)} {...others}>
|
|
74
|
+
<KobalteSwitch
|
|
75
|
+
{...switchA11yProps()}
|
|
76
|
+
checked={local.checked}
|
|
77
|
+
defaultChecked={local.defaultChecked}
|
|
78
|
+
onChange={local.onChange}
|
|
79
|
+
disabled={local.disabled}
|
|
80
|
+
validationState={hasError() ? 'invalid' : undefined}
|
|
81
|
+
name={local.name}
|
|
82
|
+
value={local.value ?? 'on'}
|
|
83
|
+
>
|
|
84
|
+
<div class={cn(local.label ? 'flex items-center justify-between gap-3 mb-1.5' : 'inline-flex items-center gap-2')}>
|
|
85
|
+
<Show when={local.label}>
|
|
86
|
+
<KobalteSwitch.Label
|
|
87
|
+
class={cn(
|
|
88
|
+
'text-sm font-medium cursor-pointer select-none',
|
|
89
|
+
hasError() ? 'text-danger-600' : 'text-ink-700',
|
|
90
|
+
local.disabled && 'opacity-50 cursor-not-allowed'
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{local.label}
|
|
94
|
+
<Show when={local.required}>
|
|
95
|
+
<span class="text-danger-500 ml-0.5" aria-hidden="true">*</span>
|
|
96
|
+
</Show>
|
|
97
|
+
</KobalteSwitch.Label>
|
|
98
|
+
</Show>
|
|
99
|
+
|
|
100
|
+
<KobalteSwitch.Input class="sr-only" />
|
|
101
|
+
|
|
102
|
+
<KobalteSwitch.Control
|
|
103
|
+
class={cn(
|
|
104
|
+
'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors',
|
|
105
|
+
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-surface-base',
|
|
106
|
+
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
|
107
|
+
'bg-ink-200 data-[checked]:bg-primary-500',
|
|
108
|
+
hasError() && 'border-danger-500 focus:ring-danger-500',
|
|
109
|
+
local.controlClass
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
<KobalteSwitch.Thumb
|
|
113
|
+
class={cn(
|
|
114
|
+
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition-transform',
|
|
115
|
+
'translate-x-0.5 data-[checked]:translate-x-5'
|
|
116
|
+
)}
|
|
117
|
+
/>
|
|
118
|
+
</KobalteSwitch.Control>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<Show when={local.description && !hasError()}>
|
|
122
|
+
<KobalteSwitch.Description class="mt-1.5 text-sm text-ink-500">
|
|
123
|
+
{local.description}
|
|
124
|
+
</KobalteSwitch.Description>
|
|
125
|
+
</Show>
|
|
126
|
+
|
|
127
|
+
<Show when={hasError()}>
|
|
128
|
+
<KobalteSwitch.ErrorMessage class="mt-1.5 text-sm text-danger-600">
|
|
129
|
+
{local.error}
|
|
130
|
+
</KobalteSwitch.ErrorMessage>
|
|
131
|
+
</Show>
|
|
132
|
+
</KobalteSwitch>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|