@nikala-ui/core 0.9.11 → 0.10.0
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/package.json +1 -1
- package/registry/aspect-ratio.json +17 -0
- package/registry/button.json +4 -1
- package/registry/collapsible.json +18 -0
- package/registry/combobox.json +4 -1
- package/registry/command.json +3 -2
- package/registry/context-menu.json +24 -0
- package/registry/create-form.json +1 -1
- package/registry/dialog.json +4 -1
- package/registry/dropdown-menu.json +4 -1
- package/registry/empty.json +17 -0
- package/registry/field.json +20 -0
- package/registry/form-message.json +16 -0
- package/registry/form.json +17 -0
- package/registry/icon-button.json +20 -0
- package/registry/index.json +191 -1
- package/registry/number-input.json +24 -0
- package/registry/resizable.json +21 -0
- package/registry/scroll-area.json +21 -0
- package/registry/select.json +4 -1
- package/registry/sheet.json +4 -1
- package/registry/spinner.json +18 -0
- package/registry/status.json +18 -0
- package/registry/theme-manager.json +1 -1
- package/registry/toggle.json +18 -0
- package/src/registry/components/ui/aspect-ratio.tsx +31 -0
- package/src/registry/components/ui/button.tsx +18 -3
- package/src/registry/components/ui/collapsible.tsx +73 -0
- package/src/registry/components/ui/combobox.tsx +23 -84
- package/src/registry/components/ui/command.tsx +154 -66
- package/src/registry/components/ui/context-menu.tsx +192 -0
- package/src/registry/components/ui/dialog.tsx +39 -44
- package/src/registry/components/ui/dropdown-menu.tsx +40 -45
- package/src/registry/components/ui/empty.tsx +83 -0
- package/src/registry/components/ui/field.tsx +67 -0
- package/src/registry/components/ui/form-message.tsx +36 -0
- package/src/registry/components/ui/form.tsx +21 -0
- package/src/registry/components/ui/icon-button.tsx +34 -0
- package/src/registry/components/ui/number-input.tsx +150 -0
- package/src/registry/components/ui/resizable.tsx +193 -0
- package/src/registry/components/ui/scroll-area.tsx +218 -0
- package/src/registry/components/ui/select.tsx +22 -16
- package/src/registry/components/ui/sheet.tsx +62 -63
- package/src/registry/components/ui/spinner.tsx +41 -0
- package/src/registry/components/ui/status.tsx +119 -0
- package/src/registry/components/ui/theme-toggle.tsx +6 -4
- package/src/registry/components/ui/toggle.tsx +101 -0
- package/src/registry/metadata.ts +84 -2
package/registry/index.json
CHANGED
|
@@ -21,6 +21,16 @@
|
|
|
21
21
|
"class-variance-authority"
|
|
22
22
|
]
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
"name": "aspect-ratio",
|
|
26
|
+
"title": "Aspect Ratio",
|
|
27
|
+
"description": "Displays content within a specific aspect ratio using CSS aspect-ratio while preventing layout shifts.",
|
|
28
|
+
"type": "registry:ui",
|
|
29
|
+
"dependencies": [
|
|
30
|
+
"clsx",
|
|
31
|
+
"tailwind-merge"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
24
34
|
{
|
|
25
35
|
"name": "avatar",
|
|
26
36
|
"title": "Avatar",
|
|
@@ -73,6 +83,9 @@
|
|
|
73
83
|
"clsx",
|
|
74
84
|
"tailwind-merge",
|
|
75
85
|
"class-variance-authority"
|
|
86
|
+
],
|
|
87
|
+
"registryDependencies": [
|
|
88
|
+
"spinner"
|
|
76
89
|
]
|
|
77
90
|
},
|
|
78
91
|
{
|
|
@@ -95,6 +108,17 @@
|
|
|
95
108
|
"tailwind-merge"
|
|
96
109
|
]
|
|
97
110
|
},
|
|
111
|
+
{
|
|
112
|
+
"name": "collapsible",
|
|
113
|
+
"title": "Collapsible",
|
|
114
|
+
"description": "An interactive component that expands and collapses content panels with smooth height animations, built on Kobalte primitives.",
|
|
115
|
+
"type": "registry:ui",
|
|
116
|
+
"dependencies": [
|
|
117
|
+
"clsx",
|
|
118
|
+
"tailwind-merge",
|
|
119
|
+
"@kobalte/core"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
98
122
|
{
|
|
99
123
|
"name": "combobox",
|
|
100
124
|
"title": "Combobox",
|
|
@@ -104,6 +128,9 @@
|
|
|
104
128
|
"clsx",
|
|
105
129
|
"tailwind-merge",
|
|
106
130
|
"@kobalte/core"
|
|
131
|
+
],
|
|
132
|
+
"registryDependencies": [
|
|
133
|
+
"scroll-area"
|
|
107
134
|
]
|
|
108
135
|
},
|
|
109
136
|
{
|
|
@@ -121,7 +148,25 @@
|
|
|
121
148
|
"registryDependencies": [
|
|
122
149
|
"kbd",
|
|
123
150
|
"input-group",
|
|
124
|
-
"list"
|
|
151
|
+
"list",
|
|
152
|
+
"scroll-area"
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "context-menu",
|
|
157
|
+
"title": "Context Menu",
|
|
158
|
+
"description": "Displays a contextual popup menu triggered by right-clicking target areas, built on Kobalte primitives.",
|
|
159
|
+
"type": "registry:ui",
|
|
160
|
+
"dependencies": [
|
|
161
|
+
"clsx",
|
|
162
|
+
"tailwind-merge",
|
|
163
|
+
"@kobalte/core"
|
|
164
|
+
],
|
|
165
|
+
"registryDependencies": [
|
|
166
|
+
"separator",
|
|
167
|
+
"kbd",
|
|
168
|
+
"create-click-outside",
|
|
169
|
+
"scroll-area"
|
|
125
170
|
]
|
|
126
171
|
},
|
|
127
172
|
{
|
|
@@ -133,6 +178,9 @@
|
|
|
133
178
|
"clsx",
|
|
134
179
|
"tailwind-merge",
|
|
135
180
|
"@kobalte/core"
|
|
181
|
+
],
|
|
182
|
+
"registryDependencies": [
|
|
183
|
+
"scroll-area"
|
|
136
184
|
]
|
|
137
185
|
},
|
|
138
186
|
{
|
|
@@ -144,6 +192,51 @@
|
|
|
144
192
|
"clsx",
|
|
145
193
|
"tailwind-merge",
|
|
146
194
|
"@kobalte/core"
|
|
195
|
+
],
|
|
196
|
+
"registryDependencies": [
|
|
197
|
+
"scroll-area"
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "empty",
|
|
202
|
+
"title": "Empty",
|
|
203
|
+
"description": "A compound empty-state layout for collections, search results, and initial application states.",
|
|
204
|
+
"type": "registry:ui",
|
|
205
|
+
"dependencies": [
|
|
206
|
+
"clsx",
|
|
207
|
+
"tailwind-merge"
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "field",
|
|
212
|
+
"title": "Field",
|
|
213
|
+
"description": "A consistent form field layout for labels, descriptions, and validation errors.",
|
|
214
|
+
"type": "registry:ui",
|
|
215
|
+
"dependencies": [
|
|
216
|
+
"clsx",
|
|
217
|
+
"tailwind-merge"
|
|
218
|
+
],
|
|
219
|
+
"registryDependencies": [
|
|
220
|
+
"label"
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "form-message",
|
|
225
|
+
"title": "Form Message",
|
|
226
|
+
"description": "Validation message helper connected to createForm errors and touched state.",
|
|
227
|
+
"type": "registry:ui",
|
|
228
|
+
"registryDependencies": [
|
|
229
|
+
"field"
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"name": "form",
|
|
234
|
+
"title": "Form",
|
|
235
|
+
"description": "A semantic form layout wrapper designed to work with the createForm hook.",
|
|
236
|
+
"type": "registry:ui",
|
|
237
|
+
"dependencies": [
|
|
238
|
+
"clsx",
|
|
239
|
+
"tailwind-merge"
|
|
147
240
|
]
|
|
148
241
|
},
|
|
149
242
|
{
|
|
@@ -157,6 +250,19 @@
|
|
|
157
250
|
"@kobalte/core"
|
|
158
251
|
]
|
|
159
252
|
},
|
|
253
|
+
{
|
|
254
|
+
"name": "icon-button",
|
|
255
|
+
"title": "Icon Button",
|
|
256
|
+
"description": "An accessible square button intended for a single icon action.",
|
|
257
|
+
"type": "registry:ui",
|
|
258
|
+
"dependencies": [
|
|
259
|
+
"clsx",
|
|
260
|
+
"tailwind-merge"
|
|
261
|
+
],
|
|
262
|
+
"registryDependencies": [
|
|
263
|
+
"button"
|
|
264
|
+
]
|
|
265
|
+
},
|
|
160
266
|
{
|
|
161
267
|
"name": "input-group",
|
|
162
268
|
"title": "Input Group",
|
|
@@ -226,6 +332,23 @@
|
|
|
226
332
|
"tailwind-merge"
|
|
227
333
|
]
|
|
228
334
|
},
|
|
335
|
+
{
|
|
336
|
+
"name": "number-input",
|
|
337
|
+
"title": "Number Input",
|
|
338
|
+
"description": "A numeric stepper input component supporting min, max, step, negative values, and long-press auto-repeat, built on Kobalte primitives.",
|
|
339
|
+
"type": "registry:ui",
|
|
340
|
+
"dependencies": [
|
|
341
|
+
"clsx",
|
|
342
|
+
"tailwind-merge",
|
|
343
|
+
"@kobalte/core",
|
|
344
|
+
"lucide-solid"
|
|
345
|
+
],
|
|
346
|
+
"registryDependencies": [
|
|
347
|
+
"input",
|
|
348
|
+
"button",
|
|
349
|
+
"create-long-press"
|
|
350
|
+
]
|
|
351
|
+
},
|
|
229
352
|
{
|
|
230
353
|
"name": "pin-input",
|
|
231
354
|
"title": "Pin Input",
|
|
@@ -270,6 +393,34 @@
|
|
|
270
393
|
"@kobalte/core"
|
|
271
394
|
]
|
|
272
395
|
},
|
|
396
|
+
{
|
|
397
|
+
"name": "resizable",
|
|
398
|
+
"title": "Resizable",
|
|
399
|
+
"description": "Accessible resizable panel layout component supporting drag-to-resize handles.",
|
|
400
|
+
"type": "registry:ui",
|
|
401
|
+
"dependencies": [
|
|
402
|
+
"clsx",
|
|
403
|
+
"tailwind-merge",
|
|
404
|
+
"lucide-solid"
|
|
405
|
+
],
|
|
406
|
+
"registryDependencies": [
|
|
407
|
+
"create-resize-observer"
|
|
408
|
+
]
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"name": "scroll-area",
|
|
412
|
+
"title": "Scroll Area",
|
|
413
|
+
"description": "Augments native scroll functionality with custom styled scrollbars and reactive scroll tracking.",
|
|
414
|
+
"type": "registry:ui",
|
|
415
|
+
"dependencies": [
|
|
416
|
+
"clsx",
|
|
417
|
+
"tailwind-merge"
|
|
418
|
+
],
|
|
419
|
+
"registryDependencies": [
|
|
420
|
+
"create-scroll-position",
|
|
421
|
+
"create-resize-observer"
|
|
422
|
+
]
|
|
423
|
+
},
|
|
273
424
|
{
|
|
274
425
|
"name": "select",
|
|
275
426
|
"title": "Select",
|
|
@@ -279,6 +430,9 @@
|
|
|
279
430
|
"clsx",
|
|
280
431
|
"tailwind-merge",
|
|
281
432
|
"@kobalte/core"
|
|
433
|
+
],
|
|
434
|
+
"registryDependencies": [
|
|
435
|
+
"scroll-area"
|
|
282
436
|
]
|
|
283
437
|
},
|
|
284
438
|
{
|
|
@@ -301,6 +455,9 @@
|
|
|
301
455
|
"tailwind-merge",
|
|
302
456
|
"class-variance-authority",
|
|
303
457
|
"@kobalte/core"
|
|
458
|
+
],
|
|
459
|
+
"registryDependencies": [
|
|
460
|
+
"scroll-area"
|
|
304
461
|
]
|
|
305
462
|
},
|
|
306
463
|
{
|
|
@@ -324,6 +481,28 @@
|
|
|
324
481
|
"@kobalte/core"
|
|
325
482
|
]
|
|
326
483
|
},
|
|
484
|
+
{
|
|
485
|
+
"name": "spinner",
|
|
486
|
+
"title": "Spinner",
|
|
487
|
+
"description": "An accessible animated loading indicator for asynchronous UI states.",
|
|
488
|
+
"type": "registry:ui",
|
|
489
|
+
"dependencies": [
|
|
490
|
+
"clsx",
|
|
491
|
+
"tailwind-merge",
|
|
492
|
+
"class-variance-authority"
|
|
493
|
+
]
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"name": "status",
|
|
497
|
+
"title": "Status",
|
|
498
|
+
"description": "A compact semantic status indicator with a color dot and label.",
|
|
499
|
+
"type": "registry:ui",
|
|
500
|
+
"dependencies": [
|
|
501
|
+
"clsx",
|
|
502
|
+
"tailwind-merge",
|
|
503
|
+
"class-variance-authority"
|
|
504
|
+
]
|
|
505
|
+
},
|
|
327
506
|
{
|
|
328
507
|
"name": "switch",
|
|
329
508
|
"title": "Switch",
|
|
@@ -383,6 +562,17 @@
|
|
|
383
562
|
"@kobalte/core"
|
|
384
563
|
]
|
|
385
564
|
},
|
|
565
|
+
{
|
|
566
|
+
"name": "toggle",
|
|
567
|
+
"title": "Toggle",
|
|
568
|
+
"description": "A two-state interactive button component built on Kobalte primitives.",
|
|
569
|
+
"type": "registry:ui",
|
|
570
|
+
"dependencies": [
|
|
571
|
+
"clsx",
|
|
572
|
+
"tailwind-merge",
|
|
573
|
+
"@kobalte/core"
|
|
574
|
+
]
|
|
575
|
+
},
|
|
386
576
|
{
|
|
387
577
|
"name": "tooltip",
|
|
388
578
|
"title": "Tooltip",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "number-input",
|
|
3
|
+
"title": "Number Input",
|
|
4
|
+
"description": "A numeric stepper input component supporting min, max, step, negative values, and long-press auto-repeat, built on Kobalte primitives.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"@kobalte/core",
|
|
10
|
+
"lucide-solid"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"input",
|
|
14
|
+
"button",
|
|
15
|
+
"create-long-press"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
{
|
|
19
|
+
"path": "ui/number-input.tsx",
|
|
20
|
+
"content": "import { createSignal, onCleanup, splitProps, type Component, type JSX } from \"solid-js\";\nimport { NumberField as KobalteNumberField } from \"@kobalte/core/number-field\";\nimport { Plus, Minus } from \"lucide-solid\";\nimport { Input } from \"./input\";\nimport { Button } from \"./button\";\nimport { createLongPress } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface NumberInputProps {\n value?: number;\n defaultValue?: number;\n onValueChange?: (value: number) => void;\n minValue?: number;\n maxValue?: number;\n step?: number;\n allowNegative?: boolean;\n disabled?: boolean;\n readOnly?: boolean;\n class?: string;\n id?: string;\n}\n\nexport const NumberInput: Component<NumberInputProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"defaultValue\",\n \"onValueChange\",\n \"minValue\",\n \"maxValue\",\n \"step\",\n \"allowNegative\",\n \"disabled\",\n \"readOnly\",\n \"class\",\n \"id\",\n ]);\n\n const [internalVal, setInternalVal] = createSignal<number>(\n local.value !== undefined\n ? local.value\n : local.defaultValue !== undefined\n ? local.defaultValue\n : 0\n );\n\n let autoRepeatInterval: ReturnType<typeof setInterval> | undefined;\n\n const currentVal = () => (local.value !== undefined ? local.value : internalVal());\n\n const effectiveMin = () => {\n if (local.minValue !== undefined) return local.minValue;\n return local.allowNegative ? -Infinity : 0;\n };\n\n const effectiveMax = () => {\n if (local.maxValue !== undefined) return local.maxValue;\n return Infinity;\n };\n\n const handleValueChange = (val: number) => {\n if (isNaN(val)) return;\n const clamped = Math.max(effectiveMin(), Math.min(effectiveMax(), val));\n setInternalVal(clamped);\n local.onValueChange?.(clamped);\n };\n\n const stopAutoRepeat = () => {\n if (autoRepeatInterval) {\n clearInterval(autoRepeatInterval);\n autoRepeatInterval = undefined;\n }\n };\n\n const startAutoRepeat = (direction: 1 | -1) => {\n if (local.disabled || local.readOnly) return;\n stopAutoRepeat();\n const stepAmount = local.step || 1;\n autoRepeatInterval = setInterval(() => {\n handleValueChange(currentVal() + direction * stepAmount);\n }, 75);\n };\n\n /* Nikala UI createLongPress hook for Increment Trigger */\n const incrementLongPress = createLongPress(\n () => {\n startAutoRepeat(1);\n },\n {\n threshold: 300,\n onCancel: stopAutoRepeat,\n }\n );\n\n /* Nikala UI createLongPress hook for Decrement Trigger */\n const decrementLongPress = createLongPress(\n () => {\n startAutoRepeat(-1);\n },\n {\n threshold: 300,\n onCancel: stopAutoRepeat,\n }\n );\n\n onCleanup(() => stopAutoRepeat());\n\n return (\n <KobalteNumberField\n value={currentVal()}\n onRawValueChange={handleValueChange}\n minValue={effectiveMin()}\n maxValue={effectiveMax()}\n step={local.step || 1}\n disabled={local.disabled}\n readOnly={local.readOnly}\n id={local.id}\n class={cn(\"relative flex items-center max-w-[160px]\", local.class)}\n {...rest}\n >\n <KobalteNumberField.Input\n as={Input}\n class=\"pr-16 text-center font-mono focus-visible:ring-1\"\n />\n\n <div class=\"absolute right-1 flex items-center gap-0.5\">\n <KobalteNumberField.DecrementTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n class=\"h-7 w-7 rounded-sm p-0 text-muted-foreground hover:text-foreground select-none\"\n aria-label=\"Decrement value\"\n {...decrementLongPress.props}\n >\n <Minus class=\"h-3.5 w-3.5\" />\n </KobalteNumberField.DecrementTrigger>\n\n <KobalteNumberField.IncrementTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n class=\"h-7 w-7 rounded-sm p-0 text-muted-foreground hover:text-foreground select-none\"\n aria-label=\"Increment value\"\n {...incrementLongPress.props}\n >\n <Plus class=\"h-3.5 w-3.5\" />\n </KobalteNumberField.IncrementTrigger>\n </div>\n </KobalteNumberField>\n );\n};\n",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resizable",
|
|
3
|
+
"title": "Resizable",
|
|
4
|
+
"description": "Accessible resizable panel layout component supporting drag-to-resize handles.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"lucide-solid"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"create-resize-observer"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "ui/resizable.tsx",
|
|
17
|
+
"content": "import {\n createSignal,\n createContext,\n useContext,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createElementSize } from \"@nikala-ui/hooks\";\nimport { GripVertical, GripHorizontal } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ResizableContextValue {\n orientation: Accessor<\"horizontal\" | \"vertical\">;\n registerPanel: (id: string, initialSizes: number) => void;\n sizes: Accessor<Record<string, number>>;\n startDragging: (handleIndex: number, event: PointerEvent) => void;\n containerRef: () => HTMLDivElement | undefined;\n}\n\nconst ResizableContext = createContext<ResizableContextValue>();\n\nexport interface ResizableGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n orientation?: \"horizontal\" | \"vertical\";\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ResizableGroup: Component<ResizableGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\"orientation\", \"class\", \"children\"]);\n const orientation = () => local.orientation || \"horizontal\";\n let containerEl: HTMLDivElement | undefined;\n\n const [panelOrder, setPanelOrder] = createSignal<string[]>([]);\n const [sizes, setSizes] = createSignal<Record<string, number>>({});\n\n const registerPanel = (id: string, initialSize: number) => {\n setPanelOrder((prev) => (prev.includes(id) ? prev : [...prev, id]));\n setSizes((prev) => (prev[id] !== undefined ? prev : { ...prev, [id]: initialSize }));\n };\n\n const startDragging = (handleIndex: number, event: PointerEvent) => {\n if (!containerEl) return;\n event.preventDefault();\n\n const order = panelOrder();\n if (handleIndex < 0 || handleIndex >= order.length - 1) return;\n\n const leftId = order[handleIndex];\n const rightId = order[handleIndex + 1];\n\n const isHoriz = orientation() === \"horizontal\";\n const startPos = isHoriz ? event.clientX : event.clientY;\n const rect = containerEl.getBoundingClientRect();\n const totalPx = isHoriz ? rect.width : rect.height;\n\n const startLeftPct = sizes()[leftId] ?? 50;\n const startRightPct = sizes()[rightId] ?? 50;\n\n const onPointerMove = (e: PointerEvent) => {\n const currentPos = isHoriz ? e.clientX : e.clientY;\n const deltaPx = currentPos - startPos;\n const deltaPct = (deltaPx / totalPx) * 100;\n\n let newLeft = startLeftPct + deltaPct;\n let newRight = startRightPct - deltaPct;\n\n if (newLeft < 10) {\n newLeft = 10;\n newRight = startLeftPct + startRightPct - 10;\n } else if (newRight < 10) {\n newRight = 10;\n newLeft = startLeftPct + startRightPct - 10;\n }\n\n setSizes((prev) => ({\n ...prev,\n [leftId]: newLeft,\n [rightId]: newRight,\n }));\n };\n\n const onPointerUp = () => {\n window.removeEventListener(\"pointermove\", onPointerMove);\n window.removeEventListener(\"pointerup\", onPointerUp);\n };\n\n window.addEventListener(\"pointermove\", onPointerMove);\n window.addEventListener(\"pointerup\", onPointerUp);\n };\n\n return (\n <ResizableContext.Provider\n value={{\n orientation,\n registerPanel,\n sizes,\n startDragging,\n containerRef: () => containerEl,\n }}\n >\n <div\n ref={containerEl}\n class={cn(\n \"flex h-full w-full overflow-hidden rounded-lg border border-border bg-background\",\n orientation() === \"vertical\" ? \"flex-col\" : \"flex-row\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </ResizableContext.Provider>\n );\n};\n\nexport interface ResizablePanelProps extends JSX.HTMLAttributes<HTMLDivElement> {\n id: string;\n initialSize?: number;\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ResizablePanel: Component<ResizablePanelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"id\", \"initialSize\", \"class\", \"children\"]);\n const ctx = useContext(ResizableContext);\n\n if (!ctx) {\n throw new Error(\"ResizablePanel must be used within a ResizableGroup\");\n }\n\n ctx.registerPanel(local.id, local.initialSize ?? 50);\n\n const currentPct = () => ctx.sizes()[local.id] ?? local.initialSize ?? 50;\n\n const containerSize = createElementSize(() => ctx.containerRef());\n\n return (\n <div\n class={cn(\"overflow-auto transition-[flex-basis] duration-75\", local.class)}\n style={{\n \"flex-basis\": `${currentPct()}%`,\n \"flex-grow\": 0,\n \"flex-shrink\": 0,\n }}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface ResizableHandleProps extends JSX.HTMLAttributes<HTMLDivElement> {\n handleIndex: number;\n withHandle?: boolean;\n class?: string;\n}\n\nexport const ResizableHandle: Component<ResizableHandleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"handleIndex\", \"withHandle\", \"class\"]);\n const ctx = useContext(ResizableContext);\n\n if (!ctx) {\n throw new Error(\"ResizableHandle must be used within a ResizableGroup\");\n }\n\n const isHoriz = () => ctx.orientation() === \"horizontal\";\n\n return (\n <div\n role=\"separator\"\n tabIndex={0}\n class={cn(\n \"relative flex select-none items-center justify-center bg-border transition-colors hover:bg-primary/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-col-resize\",\n isHoriz() ? \"h-full w-1.5 cursor-col-resize\" : \"h-1.5 w-full cursor-row-resize\",\n local.class\n )}\n onPointerDown={(e) => ctx.startDragging(local.handleIndex, e)}\n {...rest}\n >\n {local.withHandle && (\n <div class=\"z-10 flex h-4 w-3 items-center justify-center rounded-xs border border-border bg-muted shadow-2xs\">\n {isHoriz() ? (\n <GripVertical class=\"h-2.5 w-2.5 text-muted-foreground\" />\n ) : (\n <GripHorizontal class=\"h-2.5 w-2.5 text-muted-foreground\" />\n )}\n </div>\n )}\n </div>\n );\n};\n",
|
|
18
|
+
"type": "registry:ui"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scroll-area",
|
|
3
|
+
"title": "Scroll Area",
|
|
4
|
+
"description": "Augments native scroll functionality with custom styled scrollbars and reactive scroll tracking.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"create-scroll-position",
|
|
12
|
+
"create-resize-observer"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "ui/scroll-area.tsx",
|
|
17
|
+
"content": "import {\n createSignal,\n createEffect,\n onCleanup,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createScrollPosition, createElementSize } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ScrollAreaProps extends JSX.HTMLAttributes<HTMLDivElement> {\n orientation?: \"vertical\" | \"horizontal\" | \"both\";\n scrollHideDelay?: number;\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ScrollArea: Component<ScrollAreaProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"orientation\",\n \"scrollHideDelay\",\n \"class\",\n \"children\",\n ]);\n\n const orientation = () => local.orientation || \"vertical\";\n let viewportRef: HTMLDivElement | undefined;\n let verticalTrackRef: HTMLDivElement | undefined;\n let horizontalTrackRef: HTMLDivElement | undefined;\n\n const scrollPos = createScrollPosition({\n target: () => viewportRef,\n });\n\n const viewportSize = createElementSize(() => viewportRef);\n\n const [thumbHeight, setThumbHeight] = createSignal(0);\n const [thumbTop, setThumbTop] = createSignal(0);\n const [thumbWidth, setThumbWidth] = createSignal(0);\n const [thumbLeft, setThumbLeft] = createSignal(0);\n const [isDragging, setIsDragging] = createSignal(false);\n\n const updateThumbMetrics = () => {\n if (!viewportRef) return;\n\n const scrollHeight = viewportRef.scrollHeight;\n const clientHeight = viewportRef.clientHeight;\n const scrollWidth = viewportRef.scrollWidth;\n const clientWidth = viewportRef.clientWidth;\n\n const trackHeight = verticalTrackRef ? verticalTrackRef.clientHeight - 4 : clientHeight - 4;\n const trackWidth = horizontalTrackRef ? horizontalTrackRef.clientWidth - 4 : clientWidth - 4;\n\n if (scrollHeight > clientHeight && clientHeight > 0) {\n const vRatio = clientHeight / scrollHeight;\n const calculatedHeight = Math.max(vRatio * trackHeight, 20);\n const maxTop = trackHeight - calculatedHeight;\n const topPct = scrollPos.y() / (scrollHeight - clientHeight);\n setThumbHeight(calculatedHeight);\n setThumbTop(topPct * maxTop);\n } else {\n setThumbHeight(0);\n }\n\n if (scrollWidth > clientWidth && clientWidth > 0) {\n const hRatio = clientWidth / scrollWidth;\n const calculatedWidth = Math.max(hRatio * trackWidth, 20);\n const maxLeft = trackWidth - calculatedWidth;\n const leftPct = scrollPos.x() / (scrollWidth - clientWidth);\n setThumbWidth(calculatedWidth);\n setThumbLeft(leftPct * maxLeft);\n } else {\n setThumbWidth(0);\n }\n };\n\n createEffect(() => {\n viewportSize.width();\n viewportSize.height();\n scrollPos.x();\n scrollPos.y();\n updateThumbMetrics();\n });\n\n const handleVerticalThumbPointerDown = (e: PointerEvent) => {\n if (!viewportRef || !verticalTrackRef) return;\n e.preventDefault();\n e.stopPropagation();\n\n setIsDragging(true);\n const startY = e.clientY;\n const startScrollTop = viewportRef.scrollTop;\n const scrollHeight = viewportRef.scrollHeight;\n const clientHeight = viewportRef.clientHeight;\n const trackHeight = verticalTrackRef.clientHeight - 4;\n\n const maxScrollTop = scrollHeight - clientHeight;\n const maxThumbTop = trackHeight - thumbHeight();\n const ratio = maxThumbTop > 0 ? maxScrollTop / maxThumbTop : 0;\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n const deltaY = moveEvent.clientY - startY;\n viewportRef!.scrollTop = Math.max(0, Math.min(maxScrollTop, startScrollTop + deltaY * ratio));\n };\n\n const onPointerUp = () => {\n setIsDragging(false);\n window.removeEventListener(\"pointermove\", onPointerMove);\n window.removeEventListener(\"pointerup\", onPointerUp);\n };\n\n window.addEventListener(\"pointermove\", onPointerMove);\n window.addEventListener(\"pointerup\", onPointerUp);\n };\n\n const handleHorizontalThumbPointerDown = (e: PointerEvent) => {\n if (!viewportRef || !horizontalTrackRef) return;\n e.preventDefault();\n e.stopPropagation();\n\n setIsDragging(true);\n const startX = e.clientX;\n const startScrollLeft = viewportRef.scrollLeft;\n const scrollWidth = viewportRef.scrollWidth;\n const clientWidth = viewportRef.clientWidth;\n const trackWidth = horizontalTrackRef.clientWidth - 4;\n\n const maxScrollLeft = scrollWidth - clientWidth;\n const maxThumbLeft = trackWidth - thumbWidth();\n const ratio = maxThumbLeft > 0 ? maxScrollLeft / maxThumbLeft : 0;\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n const deltaX = moveEvent.clientX - startX;\n viewportRef!.scrollLeft = Math.max(0, Math.min(maxScrollLeft, startScrollLeft + deltaX * ratio));\n };\n\n const onPointerUp = () => {\n setIsDragging(false);\n window.removeEventListener(\"pointermove\", onPointerMove);\n window.removeEventListener(\"pointerup\", onPointerUp);\n };\n\n window.addEventListener(\"pointermove\", onPointerMove);\n window.addEventListener(\"pointerup\", onPointerUp);\n };\n\n const handleWheel = (e: WheelEvent) => {\n if (orientation() === \"horizontal\" && viewportRef) {\n if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {\n e.preventDefault();\n viewportRef.scrollLeft += e.deltaY;\n }\n }\n };\n\n return (\n <div\n class={cn(\"relative overflow-hidden group/scroll-area\", local.class)}\n onWheel={handleWheel}\n {...rest}\n >\n <div\n ref={viewportRef}\n class=\"h-full w-full overflow-auto scrollbar-none rounded-[inherit]\"\n style={{\n \"scrollbar-width\": \"none\",\n \"-ms-overflow-style\": \"none\",\n }}\n >\n {local.children}\n </div>\n\n {(orientation() === \"vertical\" || orientation() === \"both\") && thumbHeight() > 0 && (\n <div\n ref={verticalTrackRef}\n class={cn(\n \"absolute right-0 top-0 bottom-0 w-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none\",\n scrollPos.isScrolling() || isDragging()\n ? \"opacity-100\"\n : \"opacity-0 group-hover/scroll-area:opacity-100\"\n )}\n >\n <div\n class=\"w-1.5 rounded-full bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto\"\n style={{\n height: `${thumbHeight()}px`,\n transform: `translateY(${thumbTop()}px)`,\n }}\n onPointerDown={handleVerticalThumbPointerDown}\n />\n </div>\n )}\n\n {(orientation() === \"horizontal\" || orientation() === \"both\") && thumbWidth() > 0 && (\n <div\n ref={horizontalTrackRef}\n class={cn(\n \"absolute bottom-0 left-0 right-0 h-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none\",\n scrollPos.isScrolling() || isDragging()\n ? \"opacity-100\"\n : \"opacity-0 group-hover/scroll-area:opacity-100\"\n )}\n >\n <div\n class=\"h-1.5 rounded-full bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto\"\n style={{\n width: `${thumbWidth()}px`,\n transform: `translateX(${thumbLeft()}px)`,\n }}\n onPointerDown={handleHorizontalThumbPointerDown}\n />\n </div>\n )}\n </div>\n );\n};\n",
|
|
18
|
+
"type": "registry:ui"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/registry/select.json
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
"tailwind-merge",
|
|
9
9
|
"@kobalte/core"
|
|
10
10
|
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"scroll-area"
|
|
13
|
+
],
|
|
11
14
|
"files": [
|
|
12
15
|
{
|
|
13
16
|
"path": "ui/select.tsx",
|
|
14
|
-
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as SelectPrimitive from \"@kobalte/core/select\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport type SelectRootProps<Option = any, OptGroup = any, T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectRootProps<Option, OptGroup, T
|
|
17
|
+
"content": "import { splitProps, type Component, type JSX, type ValidComponent } from \"solid-js\";\nimport * as SelectPrimitive from \"@kobalte/core/select\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"@nikala-ui/hooks\";\nimport { ScrollArea } from \"./scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\nexport type SelectRootProps<Option = any, OptGroup = any, T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectRootProps<Option, OptGroup, T>;\n\n/**\n * Root Select component wrapper built on Kobalte primitives.\n */\nexport const Select = <Option = any, OptGroup = any, T extends ValidComponent = \"div\">(\n props: SelectRootProps<Option, OptGroup, T>\n) => {\n return <SelectPrimitive.Root {...props} />;\n};\n\nexport type SelectTriggerProps<T extends ValidComponent = \"button\"> =\n SelectPrimitive.SelectTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Trigger button opening the Select options list.\n */\nexport const SelectTrigger = <T extends ValidComponent = \"button\">(\n props: PolymorphicProps<T, SelectTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectTriggerProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Trigger\n class={cn(\n \"flex h-9 w-full items-center justify-between rounded-md border border-input bg-muted px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n <SelectPrimitive.Icon\n as=\"svg\"\n class=\"h-4 w-4 opacity-50 transition-transform\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n};\n\nexport type SelectValueProps<Option, T extends ValidComponent = \"span\"> =\n SelectPrimitive.SelectValueProps<Option, T> & {\n class?: string;\n };\n\n/**\n * Renders the currently selected option text or placeholder.\n */\nexport const SelectValue = <Option = any, T extends ValidComponent = \"span\">(\n props: PolymorphicProps<T, SelectValueProps<Option, T>>\n) => {\n const [local, rest] = splitProps(props as SelectValueProps<Option>, [\"class\"]);\n\n return (\n <SelectPrimitive.Value\n class={cn(\"block truncate\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type SelectContentProps<T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectContentProps<T> & {\n class?: string;\n };\n\n/**\n * Portaled overlay container rendering the listbox options.\n */\nexport const SelectContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SelectContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectContentProps, [\"class\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof (props as any).onInteractOutside === \"function\") {\n (props as any).onInteractOutside(e);\n }\n },\n });\n\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={(el) => {\n contentRef = el;\n if (typeof (props as any).ref === \"function\") (props as any).ref(el);\n }}\n class={cn(\n \"relative z-50 min-w-8rem overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 max-h-60\",\n local.class\n )}\n {...rest}\n >\n <ScrollArea class=\"max-h-60 w-full\">\n <SelectPrimitive.Listbox class=\"p-1 outline-none\" />\n </ScrollArea>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n};\n\nexport type SelectItemProps<T extends ValidComponent = \"li\"> =\n SelectPrimitive.SelectItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Individual option item choice inside SelectContent.\n */\nexport const SelectItem = <T extends ValidComponent = \"li\">(\n props: PolymorphicProps<T, SelectItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectItemProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Item\n class={cn(\n \"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50 text-foreground\",\n local.class\n )}\n {...rest}\n >\n <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <SelectPrimitive.ItemIndicator\n as=\"svg\"\n class=\"h-4 w-4\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>\n </SelectPrimitive.Item>\n );\n};",
|
|
15
18
|
"type": "registry:ui"
|
|
16
19
|
}
|
|
17
20
|
]
|
package/registry/sheet.json
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
"class-variance-authority",
|
|
10
10
|
"@kobalte/core"
|
|
11
11
|
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"scroll-area"
|
|
14
|
+
],
|
|
12
15
|
"files": [
|
|
13
16
|
{
|
|
14
17
|
"path": "ui/sheet.tsx",
|
|
15
|
-
"content": "import { splitProps, type Component, type JSX, type ValidComponent, Show } from \"solid-js\";\nimport * as DialogPrimitive from \"@kobalte/core/dialog\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\n// Global CSS Keyframe Animations specifically designed for Kobalte animationend DOM events\nconst sheetStyles = `\n@keyframes sheet-slide-in-left { from { transform: translateX(-100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-left { from { transform: translateX(0); } to { transform: translateX(-100%); } }\n@keyframes sheet-slide-in-right { from { transform: translateX(100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-right { from { transform: translateX(0); } to { transform: translateX(100%); } }\n@keyframes sheet-slide-in-top { from { transform: translateY(-100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-top { from { transform: translateY(0); } to { transform: translateY(-100%); } }\n@keyframes sheet-slide-in-bottom { from { transform: translateY(100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-bottom { from { transform: translateY(0); } to { transform: translateY(100%); } }\n@keyframes sheet-fade-in { from { opacity: 0; } to { opacity: 1; } }\n@keyframes sheet-fade-out { from { opacity: 1; } to { opacity: 0; } }\n`;\n\n/**\n * CVA variants for CSS keyframe slide animations across 4 edges.\n */\nexport const sheetVariants = cva(\n \"fixed z-50 gap-4 bg-card p-6 text-card-foreground shadow-lg\",\n {\n variants: {\n side: {\n
|
|
18
|
+
"content": "import { splitProps, type Component, type JSX, type ValidComponent, Show } from \"solid-js\";\nimport * as DialogPrimitive from \"@kobalte/core/dialog\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-solid\";\nimport { ScrollArea } from \"./scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\n// Global CSS Keyframe Animations specifically designed for Kobalte animationend DOM events\nconst sheetStyles = `\n@keyframes sheet-slide-in-left { from { transform: translateX(-100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-left { from { transform: translateX(0); } to { transform: translateX(-100%); } }\n@keyframes sheet-slide-in-right { from { transform: translateX(100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-right { from { transform: translateX(0); } to { transform: translateX(100%); } }\n@keyframes sheet-slide-in-top { from { transform: translateY(-100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-top { from { transform: translateY(0); } to { transform: translateY(-100%); } }\n@keyframes sheet-slide-in-bottom { from { transform: translateY(100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-bottom { from { transform: translateY(0); } to { transform: translateY(-100%); } }\n@keyframes sheet-fade-in { from { opacity: 0; } to { opacity: 1; } }\n@keyframes sheet-fade-out { from { opacity: 1; } to { opacity: 0; } }\n`;\n\n/**\n * CVA variants for CSS keyframe slide animations across 4 edges.\n */\nexport const sheetVariants = cva(\n \"fixed z-50 gap-4 bg-card p-6 text-card-foreground shadow-lg\",\n {\n variants: {\n side: {\n top: \"inset-x-0 top-0 border-b data-[expanded]:animate-[sheet-slide-in-top_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-top_300ms_ease-in-out]\",\n bottom:\n \"inset-x-0 bottom-0 border-t data-[expanded]:animate-[sheet-slide-in-bottom_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-bottom_300ms_ease-in-out]\",\n left: \"inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-left_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-left_300ms_ease-in-out]\",\n right:\n \"inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-right_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-right_300ms_ease-in-out]\",\n },\n },\n defaultVariants: {\n side: \"right\",\n },\n }\n);\n\nexport const Sheet = DialogPrimitive.Root;\nexport const SheetTrigger = DialogPrimitive.Trigger;\nexport const SheetClose = DialogPrimitive.CloseButton;\nexport const SheetPortal = DialogPrimitive.Portal;\n\nexport type SheetOverlayProps<T extends ValidComponent = \"div\"> =\n DialogPrimitive.DialogOverlayProps<T> & {\n /** Whether to apply backdrop blur (default: true) */\n blur?: boolean;\n class?: string;\n };\n\n/**\n * Fullscreen dark backdrop with fade keyframe animation.\n */\nexport const SheetOverlay = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SheetOverlayProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetOverlayProps, [\"class\", \"blur\"]);\n\n return (\n <DialogPrimitive.Overlay\n class={cn(\n \"fixed inset-0 z-50 transition-all duration-200 data-[expanded]:animate-[sheet-fade-in_300ms_ease-in-out] data-[closed]:animate-[sheet-fade-out_300ms_ease-in-out]\",\n local.blur !== false ? \"bg-black/80 backdrop-blur-sm\" : \"bg-black/80\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type SheetContentProps<T extends ValidComponent = \"div\"> =\n DialogPrimitive.DialogContentProps<T> &\n VariantProps<typeof sheetVariants> & {\n side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n showCloseButton?: boolean;\n closeOnOutsideClick?: boolean;\n blur?: boolean;\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Slide-out panel container with ScrollArea and animation support.\n */\nexport const SheetContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SheetContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetContentProps, [\n \"class\",\n \"children\",\n \"side\",\n \"showCloseButton\",\n \"closeOnOutsideClick\",\n \"blur\",\n \"onPointerDownOutside\",\n \"onInteractOutside\",\n ]);\n\n const side = () => local.side || \"right\";\n\n const handlePointerDownOutside = (e: Event) => {\n if (local.closeOnOutsideClick === false) {\n e.preventDefault();\n }\n if (typeof local.onPointerDownOutside === \"function\") {\n local.onPointerDownOutside(e as any);\n }\n };\n\n const handleInteractOutside = (e: Event) => {\n if (local.closeOnOutsideClick === false) {\n e.preventDefault();\n }\n if (typeof local.onInteractOutside === \"function\") {\n local.onInteractOutside(e as any);\n }\n };\n\n return (\n <SheetPortal>\n <style>{sheetStyles}</style>\n <SheetOverlay blur={local.blur} />\n <DialogPrimitive.Content\n onPointerDownOutside={handlePointerDownOutside}\n onInteractOutside={handleInteractOutside}\n class={cn(sheetVariants({ side: side() }), \"p-0\", local.class)}\n {...(rest as any)}\n >\n <ScrollArea class=\"h-full w-full\">\n <div class=\"p-6 space-y-4\">\n {local.children}\n </div>\n </ScrollArea>\n <Show when={local.showCloseButton !== false}>\n <DialogPrimitive.CloseButton class=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none cursor-pointer z-50\">\n <X class=\"h-4 w-4\" />\n <span class=\"sr-only\">Close</span>\n </DialogPrimitive.CloseButton>\n </Show>\n </DialogPrimitive.Content>\n </SheetPortal>\n );\n};\n\nexport interface SheetHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SheetHeader: Component<SheetHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"flex flex-col space-y-2 text-center sm:text-left\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface SheetFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SheetFooter: Component<SheetFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-4\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface SheetTitleProps {\n class?: string;\n children?: JSX.Element;\n}\n\nexport const SheetTitle: Component<SheetTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <DialogPrimitive.Title\n class={cn(\"text-lg font-semibold text-foreground\", local.class)}\n {...rest}\n >\n {local.children}\n </DialogPrimitive.Title>\n );\n};\n\nexport interface SheetDescriptionProps {\n class?: string;\n children?: JSX.Element;\n}\n\nexport const SheetDescription: Component<SheetDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <DialogPrimitive.Description\n class={cn(\"text-sm text-muted-foreground\", local.class)}\n {...rest}\n >\n {local.children}\n </DialogPrimitive.Description>\n );\n};",
|
|
16
19
|
"type": "registry:ui"
|
|
17
20
|
}
|
|
18
21
|
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spinner",
|
|
3
|
+
"title": "Spinner",
|
|
4
|
+
"description": "An accessible animated loading indicator for asynchronous UI states.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "ui/spinner.tsx",
|
|
14
|
+
"content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const spinnerVariants = cva(\n \"inline-block animate-spin rounded-full border-2 border-current border-r-transparent text-primary\",\n {\n variants: {\n size: {\n sm: \"size-3\",\n default: \"size-4\",\n lg: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n);\n\nexport interface SpinnerProps\n extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, \"role\">,\n VariantProps<typeof spinnerVariants> {\n /** Accessible text announced while the spinner is active. */\n label?: string;\n class?: string;\n}\n\n/** A compact, accessible loading indicator for async UI states. */\nexport const Spinner: Component<SpinnerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"size\", \"label\", \"class\"]);\n\n return (\n <span\n role=\"status\"\n aria-label={local.label || \"Loading\"}\n class={cn(spinnerVariants({ size: local.size }), local.class)}\n {...rest}\n />\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "status",
|
|
3
|
+
"title": "Status",
|
|
4
|
+
"description": "A compact semantic status indicator with a color dot and label.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "ui/status.tsx",
|
|
14
|
+
"content": "import { Show, splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const statusVariants = cva(\n \"inline-flex items-center gap-2 text-sm font-medium text-foreground\",\n {\n variants: {\n variant: {\n neutral: \"text-muted-foreground\",\n success: \"text-emerald-700 dark:text-emerald-400\",\n warning: \"text-amber-700 dark:text-amber-400\",\n error: \"text-destructive\",\n info: \"text-blue-700 dark:text-blue-400\",\n },\n size: {\n sm: \"text-xs gap-1.5\",\n default: \"text-sm gap-2\",\n },\n bordered: {\n true: \"rounded-md border px-2 py-1\",\n false: \"\",\n },\n borderVariant: {\n neutral: \"border-muted-foreground/30\",\n success: \"border-emerald-500/30\",\n warning: \"border-amber-500/30\",\n error: \"border-destructive/30\",\n info: \"border-blue-500/30\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n size: \"default\",\n bordered: false,\n borderVariant: \"neutral\",\n },\n }\n);\n\nexport const statusDotVariants = cva(\"size-2 shrink-0 rounded-lg\", {\n variants: {\n variant: {\n neutral: \"bg-muted-foreground\",\n success: \"bg-emerald-500\",\n warning: \"bg-amber-500\",\n error: \"bg-destructive\",\n info: \"bg-blue-500\",\n },\n size: {\n sm: \"size-1.5\",\n default: \"size-2\",\n },\n animation: {\n none: \"\",\n pulse: \"animate-pulse\",\n ping: \"\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n size: \"default\",\n animation: \"none\",\n },\n});\n\nexport interface StatusProps\n extends JSX.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof statusVariants> {\n class?: string;\n animation?: \"none\" | \"pulse\" | \"ping\";\n}\n\n/** A compact status indicator combining a semantic color dot and label. */\nexport const Status: Component<StatusProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n \"animation\",\n \"bordered\",\n ]);\n\n return (\n <span\n role=\"status\"\n class={cn(\n statusVariants({\n variant: local.variant,\n size: local.size,\n bordered: local.bordered,\n borderVariant: local.variant,\n }),\n local.class\n )}\n {...rest}\n >\n <span class=\"relative flex shrink-0\" aria-hidden=\"true\">\n <Show when={local.animation === \"ping\"}>\n <span\n class={cn(\n \"absolute inline-flex size-full animate-ping rounded-lg opacity-75\",\n statusDotVariants({ variant: local.variant, size: local.size })\n )}\n />\n </Show>\n <span\n class={statusDotVariants({\n variant: local.variant,\n size: local.size,\n animation: local.animation === \"ping\" ? \"none\" : local.animation,\n })}\n />\n </span>\n {local.children}\n </span>\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"path": "ui/theme-toggle.tsx",
|
|
34
|
-
"content": "import { splitProps, type Component, For, Show } from \"solid-js\";\nimport { Sun, Moon, Monitor } from \"lucide-solid\";\nimport {\n useTheme,\n type AccentColor,\n type Radius,\n type Theme,\n} from \"../../providers/theme-provider\";\nimport {\n runThemeTransition,\n type ThemeEffect,\n} from \"../../providers/theme-transitions\";\nimport { Button } from \"./button\";\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\";\nimport { cn } from \"@/lib/cn\";\n\nimport { createColorMode } from \"@nikala-ui/hooks\";\n\nexport interface ThemeToggleProps {\n /** Display mode: \"mini\" for compact dropdown, \"max\" for full customizer panel (default: \"mini\") */\n mode?: \"mini\" | \"max\";\n /** Transition animation effect when changing themes (\"none\", \"circular\", \"fade\") */\n effect?: ThemeEffect;\n class?: string;\n}\n\nconst ACCENT_OPTIONS: { name: AccentColor; label: string; color: string }[] = [\n { name: \"wine\", label: \"Wine\", color: \"bg-[#722f37]\" },\n { name: \"violet\", label: \"Violet\", color: \"bg-[#7c3aed]\" },\n { name: \"sky\", label: \"Sky\", color: \"bg-[#0284c7]\" },\n { name: \"emerald\", label: \"Emerald\", color: \"bg-[#059669]\" },\n { name: \"rose\", label: \"Rose\", color: \"bg-[#e11d48]\" },\n { name: \"amber\", label: \"Amber\", color: \"bg-[#d97706]\" },\n { name: \"zinc\", label: \"Zinc\", color: \"bg-[#18181b]\" },\n];\n\nconst RADIUS_OPTIONS: { value: Radius; label: string }[] = [\n { value: \"0\", label: \"0\" },\n { value: \"0.3\", label: \"0.3\" },\n { value: \"0.5\", label: \"0.5\" },\n { value: \"0.75\", label: \"0.75\" },\n { value: \"1.0\", label: \"1.0\" },\n];\n\n/**\n * Interactive UI theme switcher supporting mini/max modes and View Transition animations.\n */\nexport const ThemeToggle: Component<ThemeToggleProps> = (props) => {\n const [local] = splitProps(props, [\"mode\", \"effect\", \"class\"]);\n const { theme, setTheme, accent, setAccent, radius, setRadius } = useTheme();\n\n const colorMode = createColorMode({\n initialValue: theme(),\n storageKey: \"nikala-theme-mode\",\n });\n\n const mode = () => local.mode || \"mini\";\n const effect = () => local.effect || \"none\";\n\n /* Reactive accessor determining whether dark mode is active via createColorMode hook */\n const isDarkMode = () => {\n const currentTheme = theme();\n if (currentTheme === \"dark\") return true;\n if (currentTheme === \"light\") return false;\n return colorMode.isDark();\n };\n\n const changeThemeWithEffect = (newTheme: Theme, e: MouseEvent) => {\n runThemeTransition(effect(), e, () => {\n setTheme(newTheme);\n colorMode.setMode(newTheme);\n });\n };\n\n return (\n <DropdownMenu placement=\"bottom-end\">\n <DropdownMenuTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n class={cn(\"relative h-9 w-9 cursor-pointer\", local.class)}\n >\n {/* Reactive Sun / Moon Icon Toggle */}\n <Show\n when={isDarkMode()}\n fallback={<Sun class=\"h-4 w-4 text-foreground transition-transform\" />}\n >\n <Moon class=\"h-4 w-4 text-foreground transition-transform\" />\n </Show>\n\n <span class=\"sr-only\">Toggle theme</span>\n </DropdownMenuTrigger>\n\n <Show\n when={mode() === \"max\"}\n fallback={\n /* Mini Mode: Compact Dropdown */\n <DropdownMenuContent>\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}>\n <Sun class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Light\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}>\n <Moon class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Dark\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}>\n <Monitor class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n System\n </DropdownMenuItem>\n </DropdownMenuContent>\n }\n >\n {/* Max Mode: Full Theme Customizer Panel */}\n <DropdownMenuContent class=\"w-64 p-
|
|
34
|
+
"content": "import { splitProps, type Component, For, Show } from \"solid-js\";\nimport { Sun, Moon, Monitor } from \"lucide-solid\";\nimport {\n useTheme,\n type AccentColor,\n type Radius,\n type Theme,\n} from \"../../providers/theme-provider\";\nimport {\n runThemeTransition,\n type ThemeEffect,\n} from \"../../providers/theme-transitions\";\nimport { Button } from \"./button\";\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\";\nimport { cn } from \"@/lib/cn\";\n\nimport { createColorMode } from \"@nikala-ui/hooks\";\n\nexport interface ThemeToggleProps {\n /** Display mode: \"mini\" for compact dropdown, \"max\" for full customizer panel (default: \"mini\") */\n mode?: \"mini\" | \"max\";\n /** Transition animation effect when changing themes (\"none\", \"circular\", \"fade\") */\n effect?: ThemeEffect;\n class?: string;\n}\n\nconst ACCENT_OPTIONS: { name: AccentColor; label: string; color: string }[] = [\n { name: \"wine\", label: \"Wine\", color: \"bg-[#722f37]\" },\n { name: \"violet\", label: \"Violet\", color: \"bg-[#7c3aed]\" },\n { name: \"sky\", label: \"Sky\", color: \"bg-[#0284c7]\" },\n { name: \"emerald\", label: \"Emerald\", color: \"bg-[#059669]\" },\n { name: \"rose\", label: \"Rose\", color: \"bg-[#e11d48]\" },\n { name: \"amber\", label: \"Amber\", color: \"bg-[#d97706]\" },\n { name: \"zinc\", label: \"Zinc\", color: \"bg-[#18181b]\" },\n];\n\nconst RADIUS_OPTIONS: { value: Radius; label: string }[] = [\n { value: \"0\", label: \"0\" },\n { value: \"0.3\", label: \"0.3\" },\n { value: \"0.5\", label: \"0.5\" },\n { value: \"0.75\", label: \"0.75\" },\n { value: \"1.0\", label: \"1.0\" },\n];\n\n/**\n * Interactive UI theme switcher supporting mini/max modes and View Transition animations.\n */\nexport const ThemeToggle: Component<ThemeToggleProps> = (props) => {\n const [local] = splitProps(props, [\"mode\", \"effect\", \"class\"]);\n const { theme, setTheme, accent, setAccent, radius, setRadius } = useTheme();\n\n const colorMode = createColorMode({\n initialValue: theme(),\n storageKey: \"nikala-theme-mode\",\n });\n\n const mode = () => local.mode || \"mini\";\n const effect = () => local.effect || \"none\";\n\n /* Reactive accessor determining whether dark mode is active via createColorMode hook */\n const isDarkMode = () => {\n const currentTheme = theme();\n if (currentTheme === \"dark\") return true;\n if (currentTheme === \"light\") return false;\n return colorMode.isDark();\n };\n\n const changeThemeWithEffect = (newTheme: Theme, e: MouseEvent) => {\n runThemeTransition(effect(), e, () => {\n setTheme(newTheme);\n colorMode.setMode(newTheme);\n });\n };\n\n return (\n <DropdownMenu placement=\"bottom-end\">\n <DropdownMenuTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n class={cn(\"relative h-9 w-9 cursor-pointer\", local.class)}\n >\n {/* Reactive Sun / Moon Icon Toggle */}\n <Show\n when={isDarkMode()}\n fallback={<Sun class=\"h-4 w-4 text-foreground transition-transform\" />}\n >\n <Moon class=\"h-4 w-4 text-foreground transition-transform\" />\n </Show>\n\n <span class=\"sr-only\">Toggle theme</span>\n </DropdownMenuTrigger>\n\n <Show\n when={mode() === \"max\"}\n fallback={\n /* Mini Mode: Compact Dropdown */\n <DropdownMenuContent>\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}>\n <Sun class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Light\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}>\n <Moon class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Dark\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}>\n <Monitor class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n System\n </DropdownMenuItem>\n </DropdownMenuContent>\n }\n >\n {/* Max Mode: Full Theme Customizer Panel */}\n <DropdownMenuContent class=\"w-64\">\n <div class=\"p-2 space-y-2\">\n <DropdownMenuLabel class=\"px-0 pt-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Theme Mode\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-3 gap-1 my-1.5\">\n <Button\n variant={theme() === \"light\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Light\n </Button>\n <Button\n variant={theme() === \"dark\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Dark\n </Button>\n <Button\n variant={theme() === \"system\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n System\n </Button>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Brand Accent Color\n </DropdownMenuLabel>\n <div class=\"flex flex-wrap gap-1.5 my-1.5\">\n <For each={ACCENT_OPTIONS}>\n {(opt) => (\n <button\n type=\"button\"\n title={opt.label}\n onClick={() => setAccent(opt.name)}\n class={cn(\n \"h-6 w-6 rounded-md transition-all cursor-pointer border border-border flex items-center justify-center\",\n opt.color,\n accent() === opt.name ? \"ring-2 ring-primary ring-offset-2 ring-offset-background scale-110\" : \"hover:scale-105\"\n )}\n />\n )}\n </For>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Border Radius\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-5 gap-1 my-1.5\">\n <For each={RADIUS_OPTIONS}>\n {(r) => (\n <Button\n variant={radius() === r.value ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={() => setRadius(r.value)}\n class=\"h-7 text-xs px-1 cursor-pointer\"\n >\n {r.label}\n </Button>\n )}\n </For>\n </div>\n </div>\n </DropdownMenuContent>\n </Show>\n </DropdownMenu>\n );\n};",
|
|
35
35
|
"type": "registry:ui"
|
|
36
36
|
}
|
|
37
37
|
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "toggle",
|
|
3
|
+
"title": "Toggle",
|
|
4
|
+
"description": "A two-state interactive button component built on Kobalte primitives.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"@kobalte/core"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "ui/toggle.tsx",
|
|
14
|
+
"content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { ToggleButton as KobalteToggle } from \"@kobalte/core/toggle-button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const toggleVariants = cva(\n \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[pressed]:bg-accent data-[pressed]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline:\n \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground data-[pressed]:bg-accent data-[pressed]:text-accent-foreground\",\n },\n size: {\n default: \"h-9 px-3 min-w-9\",\n sm: \"h-8 px-2 text-xs min-w-8\",\n lg: \"h-10 px-3 min-w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport interface ToggleProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof toggleVariants> {\n pressed?: boolean;\n defaultPressed?: boolean;\n onPressedChange?: (pressed: boolean) => void;\n disabled?: boolean;\n class?: string;\n children?: JSX.Element;\n}\n\nexport const Toggle: Component<ToggleProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n \"pressed\",\n \"defaultPressed\",\n \"onPressedChange\",\n \"disabled\",\n \"onChange\",\n ]);\n\n return (\n <KobalteToggle\n isPressed={local.pressed}\n defaultIsPressed={local.defaultPressed}\n onChange={local.onPressedChange}\n disabled={local.disabled}\n class={cn(\n toggleVariants({ variant: local.variant, size: local.size }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </KobalteToggle>\n );\n};\n\nexport interface ToggleGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n type?: \"single\" | \"multiple\";\n value?: string | string[];\n defaultValue?: string | string[];\n onChange?: (value: any) => void;\n disabled?: boolean;\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ToggleGroup: Component<ToggleGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"type\",\n \"value\",\n \"defaultValue\",\n \"onChange\",\n \"disabled\",\n \"class\",\n \"children\",\n ]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-center gap-1 rounded-md border border-border/40 p-1 bg-background\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|