@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,728 @@
|
|
|
1
|
+
import {
|
|
2
|
+
|
|
3
|
+
type JSX,
|
|
4
|
+
|
|
5
|
+
children,
|
|
6
|
+
|
|
7
|
+
createContext,
|
|
8
|
+
|
|
9
|
+
createEffect,
|
|
10
|
+
|
|
11
|
+
createSignal,
|
|
12
|
+
|
|
13
|
+
splitProps,
|
|
14
|
+
|
|
15
|
+
useContext,
|
|
16
|
+
|
|
17
|
+
For,
|
|
18
|
+
|
|
19
|
+
} from 'solid-js'
|
|
20
|
+
|
|
21
|
+
import { DropdownMenu as KobalteDropdownMenu, type DropdownMenuRootProps as KobalteDropdownMenuRootProps } from '@kobalte/core/dropdown-menu'
|
|
22
|
+
|
|
23
|
+
import { ToggleGroup as KobalteToggleGroup } from '@kobalte/core/toggle-group'
|
|
24
|
+
|
|
25
|
+
import { ChevronDown } from 'lucide-solid'
|
|
26
|
+
|
|
27
|
+
import { cn } from '../../utilities/classNames'
|
|
28
|
+
|
|
29
|
+
import { Button } from './Button'
|
|
30
|
+
|
|
31
|
+
import type { ButtonProps, ButtonSize, ButtonVariant } from './Button'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export interface ToggleGroupOption {
|
|
36
|
+
|
|
37
|
+
/** Value passed to `onChange` when this option is selected. */
|
|
38
|
+
|
|
39
|
+
value: string
|
|
40
|
+
|
|
41
|
+
/** Display text rendered inside the toggle item. */
|
|
42
|
+
|
|
43
|
+
label: string
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/* open/onOpenChange are Partial<Pick<...>> rather than declared directly so that
|
|
50
|
+
|
|
51
|
+
the types stay in sync with Kobalte's DropdownMenuRootProps if the API changes. */
|
|
52
|
+
|
|
53
|
+
type ButtonGroupPropsBase = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onChange'> &
|
|
54
|
+
|
|
55
|
+
Partial<Pick<KobalteDropdownMenuRootProps, 'open' | 'onOpenChange'>> & {
|
|
56
|
+
|
|
57
|
+
class?: string
|
|
58
|
+
|
|
59
|
+
children?: JSX.Element
|
|
60
|
+
|
|
61
|
+
/** Enable split-button mode (main action + dropdown trigger). */
|
|
62
|
+
|
|
63
|
+
split?: boolean
|
|
64
|
+
|
|
65
|
+
/** Button size inherited by split-mode children. Default: "md" */
|
|
66
|
+
|
|
67
|
+
size?: ButtonSize
|
|
68
|
+
|
|
69
|
+
/** Button variant inherited by split-mode children. Default: "primary" */
|
|
70
|
+
|
|
71
|
+
variant?: ButtonVariant
|
|
72
|
+
|
|
73
|
+
/** Use filled-variant child dividers (white/20 instead of ink borders). */
|
|
74
|
+
|
|
75
|
+
filled?: boolean
|
|
76
|
+
|
|
77
|
+
/** Toggle-group mode: array of selectable options. Requires `value` and `onChange`. */
|
|
78
|
+
|
|
79
|
+
options?: ToggleGroupOption[]
|
|
80
|
+
|
|
81
|
+
/** Toggle-group layout direction. Default: "horizontal" */
|
|
82
|
+
|
|
83
|
+
orientation?: 'horizontal' | 'vertical'
|
|
84
|
+
|
|
85
|
+
/** Disable all buttons in the group. */
|
|
86
|
+
|
|
87
|
+
disabled?: boolean
|
|
88
|
+
|
|
89
|
+
/** Split mode: aria-label for the role="group" wrapper. Default: "Split button" */
|
|
90
|
+
|
|
91
|
+
splitButtonAriaLabel?: string
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
export type ButtonGroupProps =
|
|
98
|
+
|
|
99
|
+
| (ButtonGroupPropsBase & { multiple?: false; value?: string; onChange?: (value: string) => void })
|
|
100
|
+
|
|
101
|
+
| (ButtonGroupPropsBase & { multiple: true; value?: string[]; onChange?: (value: string[]) => void })
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
const ButtonGroupSplitContext = createContext<{ size: ButtonSize; variant: ButtonVariant; disabled: boolean }>({
|
|
106
|
+
|
|
107
|
+
size: 'md',
|
|
108
|
+
|
|
109
|
+
variant: 'primary',
|
|
110
|
+
|
|
111
|
+
disabled: false,
|
|
112
|
+
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
/* Match Button iconOnlySizes so split trigger is same height as main button. */
|
|
118
|
+
|
|
119
|
+
const splitTriggerSizes: Record<ButtonSize, string> = {
|
|
120
|
+
|
|
121
|
+
xs: 'h-8 w-8 p-0',
|
|
122
|
+
|
|
123
|
+
sm: 'h-10 w-10 p-0',
|
|
124
|
+
|
|
125
|
+
md: 'h-11 w-11 p-0',
|
|
126
|
+
|
|
127
|
+
lg: 'h-14 w-14 p-0',
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
/* Split trigger variant styles. These intentionally mirror buttonVariants in Button.tsx
|
|
134
|
+
|
|
135
|
+
for the subset of properties relevant to the dropdown chevron. If a variant's hover/active
|
|
136
|
+
|
|
137
|
+
colors change in Button, update the corresponding entry here as well. */
|
|
138
|
+
|
|
139
|
+
const splitTriggerVariants: Record<ButtonVariant, string> = {
|
|
140
|
+
|
|
141
|
+
primary:
|
|
142
|
+
|
|
143
|
+
'bg-primary-500 text-white border-white/20 hover:bg-primary-600 active:bg-primary-700',
|
|
144
|
+
|
|
145
|
+
'primary-outline':
|
|
146
|
+
|
|
147
|
+
'bg-transparent text-primary-500 border-surface-border hover:bg-primary-500/10 active:bg-primary-500/20',
|
|
148
|
+
|
|
149
|
+
secondary:
|
|
150
|
+
|
|
151
|
+
'bg-ink-200 text-ink-800 border-ink-300 hover:bg-ink-300 dark:hover:bg-ink-600',
|
|
152
|
+
|
|
153
|
+
outlined:
|
|
154
|
+
|
|
155
|
+
'bg-transparent text-primary-500 border-surface-border hover:bg-primary-500/10 active:bg-primary-500/20',
|
|
156
|
+
|
|
157
|
+
ghost:
|
|
158
|
+
|
|
159
|
+
'bg-transparent text-primary-500 border-surface-border hover:bg-primary-500/10 active:bg-primary-500/20',
|
|
160
|
+
|
|
161
|
+
link: 'bg-transparent text-primary-500 border-surface-border hover:bg-primary-500/10',
|
|
162
|
+
|
|
163
|
+
danger:
|
|
164
|
+
|
|
165
|
+
'bg-danger-500 text-white border-white/20 hover:bg-danger-600 active:bg-danger-700',
|
|
166
|
+
|
|
167
|
+
'danger-outline':
|
|
168
|
+
|
|
169
|
+
'bg-transparent text-danger-500 border-surface-border hover:bg-danger-500/10 active:bg-danger-500/20',
|
|
170
|
+
|
|
171
|
+
'danger-link':
|
|
172
|
+
|
|
173
|
+
'bg-transparent text-danger-500 border-surface-border hover:bg-danger-500/10',
|
|
174
|
+
|
|
175
|
+
success:
|
|
176
|
+
|
|
177
|
+
'bg-success-500 text-white border-white/20 hover:bg-success-600 active:bg-success-700',
|
|
178
|
+
|
|
179
|
+
'success-outline':
|
|
180
|
+
|
|
181
|
+
'bg-transparent text-success-500 border-surface-border hover:bg-success-500/10 active:bg-success-500/20',
|
|
182
|
+
|
|
183
|
+
warning:
|
|
184
|
+
|
|
185
|
+
'bg-warning-500 text-white border-white/20 hover:bg-warning-600 active:bg-warning-700',
|
|
186
|
+
|
|
187
|
+
'warning-outline':
|
|
188
|
+
|
|
189
|
+
'bg-transparent text-warning-500 border-surface-border hover:bg-warning-500/10 active:bg-warning-500/20',
|
|
190
|
+
|
|
191
|
+
info:
|
|
192
|
+
|
|
193
|
+
'bg-sky-600 text-white border-white/20 hover:bg-sky-700 active:bg-sky-800 dark:bg-sky-500 dark:hover:bg-sky-600 dark:active:bg-sky-700',
|
|
194
|
+
|
|
195
|
+
'info-outline':
|
|
196
|
+
|
|
197
|
+
'bg-transparent text-sky-700 border-surface-border hover:bg-sky-500/10 active:bg-sky-500/20 dark:text-sky-300 dark:hover:bg-sky-400/10 dark:active:bg-sky-400/20',
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
/* Slot pattern for split-button menu content. ButtonGroup.Menu returns a plain
|
|
204
|
+
|
|
205
|
+
object (not a component); the root detects it via the symbol and calls
|
|
206
|
+
|
|
207
|
+
render() when rendering the dropdown. Opaque to Solid's children() so the
|
|
208
|
+
|
|
209
|
+
menu render function isn't invoked during child resolution. */
|
|
210
|
+
|
|
211
|
+
const BUTTON_GROUP_MENU_SYMBOL = Symbol.for('ButtonGroup.Menu')
|
|
212
|
+
|
|
213
|
+
export interface ButtonGroupMenuSlot {
|
|
214
|
+
|
|
215
|
+
[BUTTON_GROUP_MENU_SYMBOL]: true
|
|
216
|
+
|
|
217
|
+
render: () => JSX.Element
|
|
218
|
+
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
function isMenuSlot(v: unknown): v is ButtonGroupMenuSlot {
|
|
224
|
+
|
|
225
|
+
return typeof v === 'object' && v !== null && BUTTON_GROUP_MENU_SYMBOL in v
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
/** Resolve a menu slot, render function, or plain JSX element. */
|
|
232
|
+
|
|
233
|
+
function resolveSlot(value: unknown): JSX.Element {
|
|
234
|
+
|
|
235
|
+
if (isMenuSlot(value)) return value.render()
|
|
236
|
+
|
|
237
|
+
if (typeof value === 'function') return (value as () => JSX.Element)()
|
|
238
|
+
|
|
239
|
+
return value as JSX.Element
|
|
240
|
+
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
function ButtonGroupMenuRenderer(props: { content: unknown }) {
|
|
246
|
+
|
|
247
|
+
return <>{resolveSlot(props.content)}</>
|
|
248
|
+
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
const groupBaseClasses =
|
|
254
|
+
|
|
255
|
+
'inline-flex rounded-lg border border-surface-border overflow-hidden [&>*]:!shadow-none'
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
const groupChildClasses =
|
|
260
|
+
|
|
261
|
+
'[&>*]:!rounded-none [&>*]:!border-0 [&>*]:!border-r [&>*]:!border-surface-border [&>*:last-child]:!border-r-0 [&>*:first-child]:!rounded-l-lg [&>*:last-child]:!rounded-r-lg'
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
const groupChildClassesFilled =
|
|
266
|
+
|
|
267
|
+
'[&>*]:!rounded-none [&>*]:!border-0 [&>*]:!border-r [&>*]:!border-white/20 [&>*:last-child]:!border-r-0 [&>*:first-child]:!rounded-l-lg [&>*:last-child]:!rounded-r-lg'
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
const groupChildClassesVertical =
|
|
272
|
+
|
|
273
|
+
'flex-col [&>*]:!rounded-none [&>*]:!border-0 [&>*]:!border-b [&>*]:!border-surface-border [&>*:last-child]:!border-b-0 [&>*:first-child]:!rounded-t-lg [&>*:last-child]:!rounded-b-lg'
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
const toggleItemClass = cn(
|
|
278
|
+
|
|
279
|
+
'inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors outline-none',
|
|
280
|
+
|
|
281
|
+
'bg-transparent text-ink-700 hover:bg-surface-overlay',
|
|
282
|
+
|
|
283
|
+
'data-[pressed]:bg-primary-500 data-[pressed]:text-white',
|
|
284
|
+
|
|
285
|
+
'data-[pressed]:hover:bg-primary-600 data-[pressed]:hover:text-white',
|
|
286
|
+
|
|
287
|
+
'data-[pressed]:!border-white/20',
|
|
288
|
+
|
|
289
|
+
'focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-inset',
|
|
290
|
+
|
|
291
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
292
|
+
|
|
293
|
+
'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed'
|
|
294
|
+
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
|
|
301
|
+
* Groups buttons into a single visual unit. Supports three modes:
|
|
302
|
+
|
|
303
|
+
* default (inline group), split (primary action + dropdown), and
|
|
304
|
+
|
|
305
|
+
* toggle (single/multi-select option bar via `options`).
|
|
306
|
+
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
export function ButtonGroupRoot(props: ButtonGroupProps) {
|
|
310
|
+
|
|
311
|
+
const [local, others] = splitProps(props, [
|
|
312
|
+
|
|
313
|
+
'class',
|
|
314
|
+
|
|
315
|
+
'children',
|
|
316
|
+
|
|
317
|
+
'split',
|
|
318
|
+
|
|
319
|
+
'size',
|
|
320
|
+
|
|
321
|
+
'variant',
|
|
322
|
+
|
|
323
|
+
'filled',
|
|
324
|
+
|
|
325
|
+
'options',
|
|
326
|
+
|
|
327
|
+
'value',
|
|
328
|
+
|
|
329
|
+
'onChange',
|
|
330
|
+
|
|
331
|
+
'multiple',
|
|
332
|
+
|
|
333
|
+
'orientation',
|
|
334
|
+
|
|
335
|
+
'disabled',
|
|
336
|
+
|
|
337
|
+
'splitButtonAriaLabel',
|
|
338
|
+
|
|
339
|
+
'open',
|
|
340
|
+
|
|
341
|
+
'onOpenChange',
|
|
342
|
+
|
|
343
|
+
])
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
// Mode determined once at creation. Switching after mount is not supported.
|
|
348
|
+
|
|
349
|
+
const mode: 'toggle' | 'split' | 'default' =
|
|
350
|
+
|
|
351
|
+
local.options != null ? 'toggle'
|
|
352
|
+
|
|
353
|
+
: local.split === true ? 'split'
|
|
354
|
+
|
|
355
|
+
: 'default'
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
// All reactive primitives unconditional — Solid tracks ownership by call
|
|
360
|
+
|
|
361
|
+
// order, so conditional creation can cause issues with HMR / Suspense.
|
|
362
|
+
|
|
363
|
+
const [internalOpen, setInternalOpen] = createSignal(false)
|
|
364
|
+
|
|
365
|
+
const isControlled = () => local.open !== undefined
|
|
366
|
+
|
|
367
|
+
const open = () => (isControlled() ? (local.open as boolean) : internalOpen())
|
|
368
|
+
|
|
369
|
+
const setOpen = (next: boolean) => {
|
|
370
|
+
|
|
371
|
+
if (local.disabled && next) return
|
|
372
|
+
|
|
373
|
+
if (!isControlled()) setInternalOpen(next)
|
|
374
|
+
|
|
375
|
+
local.onOpenChange?.(next)
|
|
376
|
+
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// children() resolves slot objects returned by ButtonGroupMenu. The slot
|
|
380
|
+
|
|
381
|
+
// pattern relies on Solid's children() returning raw component return values
|
|
382
|
+
|
|
383
|
+
// (including non-DOM objects) — this is documented Solid behavior but worth
|
|
384
|
+
|
|
385
|
+
// noting for future Solid version upgrades.
|
|
386
|
+
|
|
387
|
+
const resolved = children(() => local.children)
|
|
388
|
+
|
|
389
|
+
const list = () => {
|
|
390
|
+
|
|
391
|
+
const c = resolved()
|
|
392
|
+
|
|
393
|
+
return Array.isArray(c) ? c : c ? [c] : []
|
|
394
|
+
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const main = () => list()[0]
|
|
398
|
+
|
|
399
|
+
const menuContent = () => list()[1]
|
|
400
|
+
|
|
401
|
+
const size = () => (local.size ?? 'md') as ButtonSize
|
|
402
|
+
|
|
403
|
+
const variant = () => (local.variant ?? 'primary') as ButtonVariant
|
|
404
|
+
|
|
405
|
+
const toggleOrientation = () => local.orientation ?? 'horizontal'
|
|
406
|
+
|
|
407
|
+
const toggleRootClass = () =>
|
|
408
|
+
|
|
409
|
+
cn(
|
|
410
|
+
|
|
411
|
+
groupBaseClasses,
|
|
412
|
+
|
|
413
|
+
toggleOrientation() === 'vertical' ? groupChildClassesVertical : groupChildClasses,
|
|
414
|
+
|
|
415
|
+
local.class
|
|
416
|
+
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
if (import.meta.env.DEV) {
|
|
422
|
+
|
|
423
|
+
createEffect(() => {
|
|
424
|
+
|
|
425
|
+
if (mode === 'split' && list().length < 2) {
|
|
426
|
+
|
|
427
|
+
console.warn(
|
|
428
|
+
|
|
429
|
+
'ButtonGroup: split mode expects <ButtonGroup.Main> and <ButtonGroup.Menu> as children.'
|
|
430
|
+
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
if (mode === 'toggle') {
|
|
442
|
+
|
|
443
|
+
const toggleChildren = () => (
|
|
444
|
+
|
|
445
|
+
<For each={local.options}>
|
|
446
|
+
|
|
447
|
+
{(opt) => (
|
|
448
|
+
|
|
449
|
+
<KobalteToggleGroup.Item value={opt.value} aria-label={opt.label} class={toggleItemClass}>
|
|
450
|
+
|
|
451
|
+
{opt.label}
|
|
452
|
+
|
|
453
|
+
</KobalteToggleGroup.Item>
|
|
454
|
+
|
|
455
|
+
)}
|
|
456
|
+
|
|
457
|
+
</For>
|
|
458
|
+
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
if (local.multiple) {
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
|
|
465
|
+
<KobalteToggleGroup
|
|
466
|
+
|
|
467
|
+
value={(local.value as string[] | undefined) ?? []}
|
|
468
|
+
|
|
469
|
+
onChange={local.onChange as (v: string[]) => void}
|
|
470
|
+
|
|
471
|
+
multiple
|
|
472
|
+
|
|
473
|
+
orientation={toggleOrientation()}
|
|
474
|
+
|
|
475
|
+
disabled={local.disabled}
|
|
476
|
+
|
|
477
|
+
class={toggleRootClass()}
|
|
478
|
+
|
|
479
|
+
>
|
|
480
|
+
|
|
481
|
+
{toggleChildren()}
|
|
482
|
+
|
|
483
|
+
</KobalteToggleGroup>
|
|
484
|
+
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
|
|
491
|
+
<KobalteToggleGroup
|
|
492
|
+
|
|
493
|
+
value={(local.value as string | undefined) ?? null}
|
|
494
|
+
|
|
495
|
+
onChange={local.onChange as (v: string | null) => void}
|
|
496
|
+
|
|
497
|
+
orientation={toggleOrientation()}
|
|
498
|
+
|
|
499
|
+
disabled={local.disabled}
|
|
500
|
+
|
|
501
|
+
class={toggleRootClass()}
|
|
502
|
+
|
|
503
|
+
>
|
|
504
|
+
|
|
505
|
+
{toggleChildren()}
|
|
506
|
+
|
|
507
|
+
</KobalteToggleGroup>
|
|
508
|
+
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
if (mode === 'split') {
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
|
|
519
|
+
<ButtonGroupSplitContext.Provider
|
|
520
|
+
|
|
521
|
+
value={{ get size() { return size() }, get variant() { return variant() }, get disabled() { return !!local.disabled } }}
|
|
522
|
+
|
|
523
|
+
>
|
|
524
|
+
|
|
525
|
+
<KobalteDropdownMenu open={open()} onOpenChange={setOpen}>
|
|
526
|
+
|
|
527
|
+
<div
|
|
528
|
+
|
|
529
|
+
data-torchui="button-group"
|
|
530
|
+
|
|
531
|
+
role="group"
|
|
532
|
+
|
|
533
|
+
aria-label={local.splitButtonAriaLabel ?? 'Split button'}
|
|
534
|
+
|
|
535
|
+
class={cn(groupBaseClasses, local.class)}
|
|
536
|
+
|
|
537
|
+
{...others}
|
|
538
|
+
|
|
539
|
+
>
|
|
540
|
+
|
|
541
|
+
{main()}
|
|
542
|
+
|
|
543
|
+
<KobalteDropdownMenu.Trigger
|
|
544
|
+
|
|
545
|
+
as="button"
|
|
546
|
+
|
|
547
|
+
type="button"
|
|
548
|
+
|
|
549
|
+
disabled={local.disabled}
|
|
550
|
+
|
|
551
|
+
class={cn(
|
|
552
|
+
|
|
553
|
+
'inline-flex shrink-0 items-center justify-center rounded-none border-l',
|
|
554
|
+
|
|
555
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-500',
|
|
556
|
+
|
|
557
|
+
splitTriggerVariants[variant()],
|
|
558
|
+
|
|
559
|
+
splitTriggerSizes[size()],
|
|
560
|
+
|
|
561
|
+
local.disabled && 'opacity-50 cursor-not-allowed'
|
|
562
|
+
|
|
563
|
+
)}
|
|
564
|
+
|
|
565
|
+
aria-label="Open menu"
|
|
566
|
+
|
|
567
|
+
>
|
|
568
|
+
|
|
569
|
+
<ChevronDown class="h-4 w-4" />
|
|
570
|
+
|
|
571
|
+
</KobalteDropdownMenu.Trigger>
|
|
572
|
+
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
<KobalteDropdownMenu.Portal>
|
|
576
|
+
|
|
577
|
+
<KobalteDropdownMenu.Content
|
|
578
|
+
|
|
579
|
+
class={cn(
|
|
580
|
+
|
|
581
|
+
'z-50 min-w-[160px] rounded-lg border border-surface-border bg-surface-raised p-1 shadow-lg'
|
|
582
|
+
|
|
583
|
+
)}
|
|
584
|
+
|
|
585
|
+
>
|
|
586
|
+
|
|
587
|
+
<ButtonGroupMenuRenderer content={menuContent()} />
|
|
588
|
+
|
|
589
|
+
</KobalteDropdownMenu.Content>
|
|
590
|
+
|
|
591
|
+
</KobalteDropdownMenu.Portal>
|
|
592
|
+
|
|
593
|
+
</KobalteDropdownMenu>
|
|
594
|
+
|
|
595
|
+
</ButtonGroupSplitContext.Provider>
|
|
596
|
+
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
const childClasses = () =>
|
|
604
|
+
|
|
605
|
+
local.filled ? groupChildClassesFilled : groupChildClasses
|
|
606
|
+
|
|
607
|
+
return (
|
|
608
|
+
|
|
609
|
+
<div
|
|
610
|
+
|
|
611
|
+
data-torchui="button-group"
|
|
612
|
+
|
|
613
|
+
role="group"
|
|
614
|
+
|
|
615
|
+
class={cn(groupBaseClasses, childClasses(), local.class)}
|
|
616
|
+
|
|
617
|
+
{...others}
|
|
618
|
+
|
|
619
|
+
>
|
|
620
|
+
|
|
621
|
+
{local.children}
|
|
622
|
+
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
export interface ButtonGroupMainProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
632
|
+
|
|
633
|
+
/** Override the variant inherited from ButtonGroup. */
|
|
634
|
+
|
|
635
|
+
variant?: ButtonVariant
|
|
636
|
+
|
|
637
|
+
/** Override the size inherited from ButtonGroup. */
|
|
638
|
+
|
|
639
|
+
size?: ButtonSize
|
|
640
|
+
|
|
641
|
+
class?: string
|
|
642
|
+
|
|
643
|
+
children?: JSX.Element
|
|
644
|
+
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
/** Primary action button in split mode. Inherits size/variant from the parent ButtonGroup. */
|
|
650
|
+
|
|
651
|
+
export function ButtonGroupMain(props: ButtonGroupMainProps) {
|
|
652
|
+
|
|
653
|
+
const ctx = useContext(ButtonGroupSplitContext)
|
|
654
|
+
|
|
655
|
+
// onChange must be extracted here to prevent it from spreading into Button via
|
|
656
|
+
|
|
657
|
+
// {...others}. Button treats the presence of onChange + pressed as a signal to
|
|
658
|
+
|
|
659
|
+
// activate toggle mode, so leaking onChange would cause unintended behavior.
|
|
660
|
+
|
|
661
|
+
const [local, others] = splitProps(props, ['variant', 'size', 'class', 'children', 'onChange'])
|
|
662
|
+
|
|
663
|
+
return (
|
|
664
|
+
|
|
665
|
+
<Button
|
|
666
|
+
|
|
667
|
+
variant={local.variant ?? ctx.variant}
|
|
668
|
+
|
|
669
|
+
size={local.size ?? ctx.size}
|
|
670
|
+
|
|
671
|
+
disabled={others.disabled ?? ctx.disabled}
|
|
672
|
+
|
|
673
|
+
disableElevation
|
|
674
|
+
|
|
675
|
+
class={cn('!rounded-r-none !border-r-0', local.class)}
|
|
676
|
+
|
|
677
|
+
{...(others as ButtonProps)}
|
|
678
|
+
|
|
679
|
+
>
|
|
680
|
+
|
|
681
|
+
{local.children}
|
|
682
|
+
|
|
683
|
+
</Button>
|
|
684
|
+
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
export interface ButtonGroupMenuProps {
|
|
692
|
+
|
|
693
|
+
/** Dropdown content rendered when the split trigger is clicked. Accepts JSX or a render function. */
|
|
694
|
+
|
|
695
|
+
children?: JSX.Element | (() => JSX.Element)
|
|
696
|
+
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
/** Returns a plain object (slot), not JSX — used as second child of ButtonGroup when split. Cast to JSX.Element so TS accepts <ButtonGroup.Menu> in JSX. */
|
|
702
|
+
|
|
703
|
+
export function ButtonGroupMenu(props: ButtonGroupMenuProps): JSX.Element {
|
|
704
|
+
|
|
705
|
+
const child = props.children
|
|
706
|
+
|
|
707
|
+
const render =
|
|
708
|
+
|
|
709
|
+
typeof child === 'function'
|
|
710
|
+
|
|
711
|
+
? (child as () => JSX.Element)
|
|
712
|
+
|
|
713
|
+
: () => (child as JSX.Element) ?? null
|
|
714
|
+
|
|
715
|
+
return { [BUTTON_GROUP_MENU_SYMBOL]: true, render } as unknown as JSX.Element
|
|
716
|
+
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
export const ButtonGroup = Object.assign(ButtonGroupRoot, {
|
|
722
|
+
|
|
723
|
+
Main: ButtonGroupMain,
|
|
724
|
+
|
|
725
|
+
Menu: ButtonGroupMenu,
|
|
726
|
+
|
|
727
|
+
})
|
|
728
|
+
|