@nikala-ui/core 0.9.10 → 0.9.11
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 +1 -1
- package/package.json +2 -2
- package/registry/combobox.json +18 -0
- package/registry/hover-card.json +18 -0
- package/registry/index.json +43 -0
- package/registry/pin-input.json +17 -0
- package/registry/slider.json +18 -0
- package/src/registry/components/ui/combobox.tsx +295 -0
- package/src/registry/components/ui/hover-card.tsx +105 -0
- package/src/registry/components/ui/pin-input.tsx +198 -0
- package/src/registry/components/ui/slider.tsx +153 -0
- package/src/registry/metadata.ts +20 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Core design system components, providers, and registry manifests for **Nikala UI
|
|
|
4
4
|
|
|
5
5
|
Honoring the iconic Georgian painter **Niko Pirosmani (Nikala)**.
|
|
6
6
|
|
|
7
|
-
Official Documentation & Interactive Demos: [nikala.
|
|
7
|
+
Official Documentation & Interactive Demos: [nikala.dev](https://nikala.dev)
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nikala-ui/core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11",
|
|
4
4
|
"description": "Core component definitions, design tokens, and registry for Nikala UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -34,4 +34,4 @@
|
|
|
34
34
|
"@nikala-ui/hooks": "workspace:*",
|
|
35
35
|
"lucide-solid": "^1.28.0"
|
|
36
36
|
}
|
|
37
|
-
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "combobox",
|
|
3
|
+
"title": "Combobox",
|
|
4
|
+
"description": "Searchable autocomplete dropdown with single/multi-selection tags, avatars, group headers, and customizable clear controls.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"@kobalte/core"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "ui/combobox.tsx",
|
|
14
|
+
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as ComboboxPrimitive from \"@kobalte/core/combobox\";\nimport { cn } from \"@/lib/cn\";\n\nexport type ComboboxRootProps<Option = any, OptGroup = any, T extends ValidComponent = \"div\"> =\n ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T> & {\n class?: string;\n children?: JSX.Element;\n triggerMode?: \"input\" | \"focus\" | \"both\" | \"manual\";\n };\n\n/**\n * Root Combobox component providing search, single/multi-selection, and group support.\n * `triggerMode=\"focus\"` or `triggerMode=\"both\"` enables opening dropdown on input click/focus.\n */\nexport const Combobox = <Option = any, OptGroup = any, T extends ValidComponent = \"div\">(\n props: ComboboxRootProps<Option, OptGroup, T>\n) => {\n const [local, rest] = splitProps(props as ComboboxRootProps, [\"class\", \"children\", \"triggerMode\"]);\n\n return (\n <ComboboxPrimitive.Root\n triggerMode={local.triggerMode ?? \"input\"}\n class={cn(\"relative w-full\", local.class)}\n {...(rest as any)}\n >\n {local.children}\n </ComboboxPrimitive.Root>\n );\n};\n\nexport type ComboboxControlProps<Option = any, T extends ValidComponent = \"div\"> =\n ComboboxPrimitive.ComboboxControlProps<Option, T> & {\n class?: string;\n children?: JSX.Element;\n clearable?: boolean;\n onClear?: () => void;\n };\n\n/**\n * Input container for Combobox supporting search input, selected tags, and clear button.\n */\nexport const ComboboxControl = <Option = any, T extends ValidComponent = \"div\">(\n props: ComboboxControlProps<Option, T>\n) => {\n const [local, rest] = splitProps(props as ComboboxControlProps, [\n \"class\",\n \"children\",\n \"clearable\",\n \"onClear\",\n ]);\n\n return (\n <ComboboxPrimitive.Control\n class={cn(\n \"flex min-h-9 w-full flex-wrap items-center justify-between rounded-md border border-input bg-muted px-3 py-1 text-sm shadow-sm transition-colors focus-within:ring-1 focus-within:ring-primary focus-within:border-primary disabled:cursor-not-allowed disabled:opacity-50 gap-1.5 text-foreground\",\n local.class\n )}\n {...(rest as any)}\n >\n <div class=\"flex flex-wrap items-center gap-1.5 flex-1 min-w-0\">\n {local.children}\n </div>\n\n <div class=\"flex items-center gap-1 shrink-0 self-center\">\n {local.clearable && (\n <button\n type=\"button\"\n tabIndex={-1}\n onClick={(e) => {\n e.stopPropagation();\n if (local.onClear) local.onClear();\n }}\n class=\"rounded-sm p-0.5 opacity-60 hover:opacity-100 hover:bg-accent text-foreground transition-opacity focus:outline-none cursor-pointer\"\n aria-label=\"Clear selection\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n )}\n <ComboboxTrigger />\n </div>\n </ComboboxPrimitive.Control>\n );\n};\n\nexport type ComboboxInputProps<T extends ValidComponent = \"input\"> =\n ComboboxPrimitive.ComboboxInputProps<T> & {\n class?: string;\n openOnFocus?: boolean;\n };\n\n/**\n * Search input field embedded within ComboboxControl.\n * Supports `openOnFocus` to automatically trigger the dropdown when focused or clicked.\n */\nexport const ComboboxInput = <T extends ValidComponent = \"input\">(\n props: ComboboxInputProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxInputProps, [\"class\", \"openOnFocus\", \"onFocus\", \"onClick\"]);\n\n return (\n <ComboboxPrimitive.Input\n class={cn(\n \"flex-1 bg-transparent py-1 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed text-foreground min-w-16\",\n local.class\n )}\n onFocus={(e: FocusEvent) => {\n if (typeof local.onFocus === \"function\") local.onFocus(e as any);\n }}\n onClick={(e: MouseEvent) => {\n if (typeof local.onClick === \"function\") local.onClick(e as any);\n }}\n {...(rest as any)}\n />\n );\n};\n\nexport type ComboboxTriggerProps<T extends ValidComponent = \"button\"> =\n ComboboxPrimitive.ComboboxTriggerProps<T> & {\n class?: string;\n };\n\n/**\n * Dropdown chevron trigger icon.\n */\nexport const ComboboxTrigger = <T extends ValidComponent = \"button\">(\n props: ComboboxTriggerProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxTriggerProps, [\"class\"]);\n\n return (\n <ComboboxPrimitive.Trigger\n class={cn(\n \"flex h-4 w-4 items-center justify-center opacity-50 hover:opacity-100 transition-opacity focus:outline-none cursor-pointer\",\n local.class\n )}\n {...(rest as any)}\n >\n <ComboboxPrimitive.Icon\n as=\"svg\"\n class=\"h-4 w-4 stroke-current stroke-2 fill-none\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </ComboboxPrimitive.Icon>\n </ComboboxPrimitive.Trigger>\n );\n};\n\nexport type ComboboxTokenProps<Option = any> = {\n class?: string;\n children?: JSX.Element;\n item?: Option;\n onRemove?: () => void;\n};\n\n/**\n * Selected item tag token pill rendered in multi-select mode.\n */\nexport const ComboboxToken = <Option = any>(props: ComboboxTokenProps<Option>) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\", \"onRemove\"]);\n\n return (\n <span\n class={cn(\n \"inline-flex items-center gap-1.5 rounded-md bg-accent px-2 py-0.5 text-xs font-medium text-accent-foreground border border-border shadow-2xs animate-in fade-in-50\",\n local.class\n )}\n {...rest}\n >\n <span class=\"truncate\">{local.children}</span>\n <button\n type=\"button\"\n tabIndex={-1}\n onClick={(e) => {\n e.stopPropagation();\n if (local.onRemove) local.onRemove();\n }}\n class=\"rounded-xs p-0.5 hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground transition-colors focus:outline-none cursor-pointer\"\n aria-label=\"Remove tag\"\n >\n <svg class=\"h-3 w-3\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </span>\n );\n};\n\nexport type ComboboxContentProps<T extends ValidComponent = \"div\"> =\n ComboboxPrimitive.ComboboxContentProps<T> & {\n class?: string;\n };\n\n/**\n * Portaled popover content listbox container.\n */\nexport const ComboboxContent = <T extends ValidComponent = \"div\">(\n props: ComboboxContentProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxContentProps, [\"class\"]);\n\n return (\n <ComboboxPrimitive.Portal>\n <ComboboxPrimitive.Content\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 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95\",\n local.class\n )}\n {...(rest as any)}\n >\n <ComboboxPrimitive.Listbox class=\"max-h-60 overflow-y-auto p-1 outline-none\" />\n </ComboboxPrimitive.Content>\n </ComboboxPrimitive.Portal>\n );\n};\n\nexport type ComboboxItemProps<T extends ValidComponent = \"li\"> =\n ComboboxPrimitive.ComboboxItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Individual option item inside listbox supporting custom avatars, icons, and titles.\n */\nexport const ComboboxItem = <T extends ValidComponent = \"li\">(\n props: ComboboxItemProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxItemProps, [\"class\", \"children\"]);\n\n return (\n <ComboboxPrimitive.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-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground text-popover-foreground transition-colors\",\n local.class\n )}\n {...rest}\n >\n <ComboboxPrimitive.ItemIndicator class=\"absolute left-2 flex h-4 w-4 items-center justify-center text-primary\">\n <svg class=\"h-4 w-4 fill-none stroke-current stroke-2\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n </ComboboxPrimitive.ItemIndicator>\n <ComboboxPrimitive.ItemLabel class=\"flex items-center gap-2 w-full truncate\">\n {local.children}\n </ComboboxPrimitive.ItemLabel>\n </ComboboxPrimitive.Item>\n );\n};\n\nexport type ComboboxGroupProps<T extends ValidComponent = \"li\"> =\n ComboboxPrimitive.ComboboxSectionProps<T> & {\n class?: string;\n children?: JSX.Element;\n label?: JSX.Element;\n };\n\n/**\n * Group container for organizing related options.\n */\nexport const ComboboxGroup = <T extends ValidComponent = \"li\">(\n props: ComboboxGroupProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxGroupProps, [\"class\", \"children\", \"label\"]);\n\n return (\n <ComboboxPrimitive.Section class={cn(\"px-1 py-1\", local.class)} {...(rest as any)}>\n {local.label && (\n <span class=\"px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider block\">\n {local.label}\n </span>\n )}\n {local.children}\n </ComboboxPrimitive.Section>\n );\n};\n\n/**\n * Empty state notice when no matching search items exist.\n */\nexport const ComboboxEmpty = (props: { class?: string; children?: JSX.Element }) => {\n return (\n <div class={cn(\"py-6 text-center text-sm text-muted-foreground\", props.class)}>\n {props.children || \"No matching items found.\"}\n </div>\n );\n};\n\n/**\n * Hidden native select element for form integrations.\n */\nexport const ComboboxHiddenSelect = ComboboxPrimitive.HiddenSelect;\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hover-card",
|
|
3
|
+
"title": "Hover Card",
|
|
4
|
+
"description": "Profile and link preview popover triggered on hover, 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/hover-card.tsx",
|
|
14
|
+
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as HoverCardPrimitive from \"@kobalte/core/hover-card\";\nimport { cn } from \"@/lib/cn\";\n\nexport type HoverCardProps = HoverCardPrimitive.HoverCardRootProps;\n\n/**\n * Root HoverCard component built on Kobalte primitives.\n * Controls hover open/close delays (default: 200ms / 150ms).\n */\nexport const HoverCard = (props: HoverCardProps) => {\n return (\n <HoverCardPrimitive.Root\n openDelay={200}\n closeDelay={150}\n {...props}\n />\n );\n};\n\nexport type HoverCardTriggerProps<T extends ValidComponent = \"a\"> =\n HoverCardPrimitive.HoverCardTriggerProps<T> &\n JSX.AnchorHTMLAttributes<HTMLAnchorElement> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * HoverCard trigger handle (link, avatar, or text).\n */\nexport const HoverCardTrigger = <T extends ValidComponent = \"a\">(\n props: HoverCardTriggerProps<T>\n) => {\n const [local, rest] = splitProps(props as HoverCardTriggerProps, [\n \"class\",\n \"children\",\n ]);\n\n return (\n <HoverCardPrimitive.Trigger\n class={cn(\n \"inline-flex items-center text-sm font-medium text-foreground underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2\",\n local.class\n )}\n {...(rest as any)}\n >\n {local.children}\n </HoverCardPrimitive.Trigger>\n );\n};\n\nexport type HoverCardContentProps<T extends ValidComponent = \"div\"> =\n HoverCardPrimitive.HoverCardContentProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Floating card preview container rendered in a portal layer.\n * Includes offset spacing from trigger and support for optional HoverCardArrow.\n */\nexport const HoverCardContent = <T extends ValidComponent = \"div\">(\n props: HoverCardContentProps<T>\n) => {\n const [local, rest] = splitProps(props as HoverCardContentProps, [\n \"class\",\n \"children\",\n ]);\n\n return (\n <HoverCardPrimitive.Portal>\n <HoverCardPrimitive.Content\n sideOffset={8}\n class={cn(\n \"z-50 w-80 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-xl outline-none transition-all data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-closed:zoom-out-95 data-expanded:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n local.class\n )}\n {...(rest as any)}\n >\n {local.children}\n </HoverCardPrimitive.Content>\n </HoverCardPrimitive.Portal>\n );\n};\n\nexport type HoverCardArrowProps<T extends ValidComponent = \"div\"> =\n HoverCardPrimitive.HoverCardArrowProps<T> & {\n class?: string;\n };\n\n/**\n * Optional arrow pointer pointing to the trigger.\n */\nexport const HoverCardArrow = <T extends ValidComponent = \"div\">(\n props: HoverCardArrowProps<T>\n) => {\n const [local, rest] = splitProps(props as HoverCardArrowProps, [\"class\"]);\n\n return (\n <HoverCardPrimitive.Arrow\n class={cn(\"fill-popover stroke-border stroke-1\", local.class)}\n {...(rest as any)}\n />\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/registry/index.json
CHANGED
|
@@ -95,6 +95,17 @@
|
|
|
95
95
|
"tailwind-merge"
|
|
96
96
|
]
|
|
97
97
|
},
|
|
98
|
+
{
|
|
99
|
+
"name": "combobox",
|
|
100
|
+
"title": "Combobox",
|
|
101
|
+
"description": "Searchable autocomplete dropdown with single/multi-selection tags, avatars, group headers, and customizable clear controls.",
|
|
102
|
+
"type": "registry:ui",
|
|
103
|
+
"dependencies": [
|
|
104
|
+
"clsx",
|
|
105
|
+
"tailwind-merge",
|
|
106
|
+
"@kobalte/core"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
98
109
|
{
|
|
99
110
|
"name": "command",
|
|
100
111
|
"title": "Command / Command Palette",
|
|
@@ -135,6 +146,17 @@
|
|
|
135
146
|
"@kobalte/core"
|
|
136
147
|
]
|
|
137
148
|
},
|
|
149
|
+
{
|
|
150
|
+
"name": "hover-card",
|
|
151
|
+
"title": "Hover Card",
|
|
152
|
+
"description": "Profile and link preview popover triggered on hover, built on Kobalte primitives.",
|
|
153
|
+
"type": "registry:ui",
|
|
154
|
+
"dependencies": [
|
|
155
|
+
"clsx",
|
|
156
|
+
"tailwind-merge",
|
|
157
|
+
"@kobalte/core"
|
|
158
|
+
]
|
|
159
|
+
},
|
|
138
160
|
{
|
|
139
161
|
"name": "input-group",
|
|
140
162
|
"title": "Input Group",
|
|
@@ -204,6 +226,16 @@
|
|
|
204
226
|
"tailwind-merge"
|
|
205
227
|
]
|
|
206
228
|
},
|
|
229
|
+
{
|
|
230
|
+
"name": "pin-input",
|
|
231
|
+
"title": "Pin Input",
|
|
232
|
+
"description": "Interactive multi-slot PIN/OTP input component for SMS and 2FA authentication verification codes.",
|
|
233
|
+
"type": "registry:ui",
|
|
234
|
+
"dependencies": [
|
|
235
|
+
"clsx",
|
|
236
|
+
"tailwind-merge"
|
|
237
|
+
]
|
|
238
|
+
},
|
|
207
239
|
{
|
|
208
240
|
"name": "popover",
|
|
209
241
|
"title": "Popover",
|
|
@@ -281,6 +313,17 @@
|
|
|
281
313
|
"tailwind-merge"
|
|
282
314
|
]
|
|
283
315
|
},
|
|
316
|
+
{
|
|
317
|
+
"name": "slider",
|
|
318
|
+
"title": "Slider",
|
|
319
|
+
"description": "Numeric range selection slider supporting single/dual thumbs, custom steps, vertical orientation, and formatted value labels, built on Kobalte primitives.",
|
|
320
|
+
"type": "registry:ui",
|
|
321
|
+
"dependencies": [
|
|
322
|
+
"clsx",
|
|
323
|
+
"tailwind-merge",
|
|
324
|
+
"@kobalte/core"
|
|
325
|
+
]
|
|
326
|
+
},
|
|
284
327
|
{
|
|
285
328
|
"name": "switch",
|
|
286
329
|
"title": "Switch",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pin-input",
|
|
3
|
+
"title": "Pin Input",
|
|
4
|
+
"description": "Interactive multi-slot PIN/OTP input component for SMS and 2FA authentication verification codes.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "ui/pin-input.tsx",
|
|
13
|
+
"content": "import {\n createSignal,\n createMemo,\n createContext,\n useContext,\n type JSX,\n} from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport type PinInputType = \"numeric\" | \"alphanumeric\";\n\nexport interface PinInputContextValue {\n value: () => string;\n setValue: (val: string) => void;\n mask: () => boolean;\n disabled: () => boolean;\n length: () => number;\n type: () => PinInputType;\n containerRef: HTMLDivElement | undefined;\n}\n\nconst PinInputContext = createContext<PinInputContextValue>();\n\nexport interface PinInputProps {\n value?: string;\n onValueChange?: (value: string) => void;\n length?: number;\n type?: PinInputType;\n mask?: boolean;\n disabled?: boolean;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Root PinInput component for 4 or 6-digit OTP / verification PIN codes.\n */\nexport const PinInput = (props: PinInputProps) => {\n let containerRef: HTMLDivElement | undefined;\n const [internalValue, setInternalValue] = createSignal(props.value ?? \"\");\n\n const value = () => props.value ?? internalValue();\n const mask = () => props.mask ?? false;\n const disabled = () => props.disabled ?? false;\n const length = () => props.length ?? 6;\n\n const inputType = (): PinInputType => {\n const t = props.type ?? \"numeric\";\n if (t !== \"numeric\" && t !== \"alphanumeric\") {\n return \"numeric\";\n }\n return t;\n };\n\n const handleValueChange = (val: string) => {\n setInternalValue(val);\n props.onValueChange?.(val);\n };\n\n const contextValue: PinInputContextValue = {\n value,\n setValue: handleValueChange,\n mask,\n disabled,\n length,\n type: inputType,\n get containerRef() {\n return containerRef;\n },\n };\n\n return (\n <PinInputContext.Provider value={contextValue}>\n <div ref={containerRef} class={cn(\"flex items-center gap-2\", props.class)}>\n {props.children}\n </div>\n </PinInputContext.Provider>\n );\n};\n\nexport interface PinInputLabelProps {\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Accessible label for PinInput.\n */\nexport const PinInputLabel = (props: PinInputLabelProps) => {\n return (\n <label class={cn(\"text-sm font-medium leading-none text-foreground\", props.class)}>\n {props.children}\n </label>\n );\n};\n\nconst focusIndex = (containerRef: HTMLDivElement | undefined, index: number) => {\n const el = containerRef?.querySelector<HTMLInputElement>(\n `[data-pin-input-index=\"${index}\"]`\n );\n el?.focus();\n el?.select();\n};\n\nexport interface PinInputInputProps {\n index: number;\n class?: string;\n}\n\n/**\n * Individual input slot for a single OTP digit/character.\n */\nexport const PinInputInput = (props: PinInputInputProps) => {\n const ctx = useContext(PinInputContext);\n const digit = createMemo(() => ctx?.value()[props.index] ?? \"\");\n\n const isCharAllowed = (char: string) => {\n if (!ctx || char === \"\") return false;\n return ctx.type() === \"numeric\"\n ? /^[0-9]$/.test(char)\n : /^[a-zA-Z0-9]$/.test(char);\n };\n\n const commitChar = (char: string) => {\n if (!ctx) return;\n const currentArray = ctx.value().split(\"\");\n while (currentArray.length < props.index) currentArray.push(\"\");\n currentArray[props.index] = char;\n ctx.setValue(currentArray.join(\"\"));\n };\n\n const handleInput = (e: InputEvent & { currentTarget: HTMLInputElement }) => {\n if (!ctx || ctx.disabled()) return;\n const val = e.currentTarget.value;\n const rawChar = val ? val[val.length - 1] : \"\";\n\n // Clear digit if input is empty\n if (rawChar === \"\") {\n commitChar(\"\");\n return;\n }\n\n if (!isCharAllowed(rawChar)) {\n e.currentTarget.value = digit();\n return;\n }\n\n commitChar(rawChar);\n\n if (props.index < ctx.length() - 1) {\n focusIndex(ctx.containerRef, props.index + 1);\n }\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!ctx || ctx.disabled()) return;\n if (e.key === \"Backspace\" && digit() === \"\" && props.index > 0) {\n focusIndex(ctx.containerRef, props.index - 1);\n }\n };\n\n const handlePaste = (e: ClipboardEvent & { currentTarget: HTMLInputElement }) => {\n if (!ctx || ctx.disabled()) return;\n e.preventDefault();\n const pasted = e.clipboardData?.getData(\"text\") ?? \"\";\n const chars = pasted.split(\"\").filter(isCharAllowed);\n if (chars.length === 0) return;\n\n const currentArray = ctx.value().split(\"\");\n let lastFilled = props.index;\n for (let i = 0; i < chars.length && props.index + i < ctx.length(); i++) {\n while (currentArray.length < props.index + i) currentArray.push(\"\");\n currentArray[props.index + i] = chars[i];\n lastFilled = props.index + i;\n }\n ctx.setValue(currentArray.join(\"\"));\n focusIndex(ctx.containerRef, Math.min(lastFilled + 1, ctx.length() - 1));\n };\n\n return (\n <input\n type={ctx?.mask() ? \"password\" : \"text\"}\n inputMode={ctx?.type() === \"numeric\" ? \"numeric\" : \"text\"}\n autocomplete=\"one-time-code\"\n maxLength={1}\n disabled={ctx?.disabled()}\n data-pin-input-index={props.index}\n value={digit()}\n onInput={handleInput}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n class={cn(\n \"relative flex h-10 w-10 text-center text-sm font-semibold rounded-md border border-input bg-background shadow-xs transition-all focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50\",\n props.class\n )}\n />\n );\n};\n",
|
|
14
|
+
"type": "registry:ui"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slider",
|
|
3
|
+
"title": "Slider",
|
|
4
|
+
"description": "Numeric range selection slider supporting single/dual thumbs, custom steps, vertical orientation, and formatted value labels, 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/slider.tsx",
|
|
14
|
+
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as SliderPrimitive from \"@kobalte/core/slider\";\nimport { cn } from \"@/lib/cn\";\n\nexport type SliderRootProps<T extends ValidComponent = \"div\"> =\n SliderPrimitive.SliderRootProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Root Slider component built on Kobalte primitives.\n */\nexport const Slider = <T extends ValidComponent = \"div\">(\n props: SliderRootProps<T>\n) => {\n const [local, rest] = splitProps(props as SliderRootProps, [\"class\", \"children\"]);\n\n return (\n <SliderPrimitive.Root\n class={cn(\n \"relative flex w-full touch-none select-none flex-col gap-2 data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:items-center\",\n local.class\n )}\n {...(rest as any)}\n >\n {local.children}\n </SliderPrimitive.Root>\n );\n};\n\nexport type SliderTrackProps<T extends ValidComponent = \"div\"> =\n SliderPrimitive.SliderTrackProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Slider track containing the active range fill and thumbs.\n */\nexport const SliderTrack = <T extends ValidComponent = \"div\">(\n props: SliderTrackProps<T>\n) => {\n const [local, rest] = splitProps(props as SliderTrackProps, [\"class\", \"children\"]);\n\n return (\n <SliderPrimitive.Track\n class={cn(\n \"relative h-2 w-full flex items-center rounded-lg bg-secondary cursor-pointer data-[orientation=vertical]:w-2 data-[orientation=vertical]:h-full data-[orientation=vertical]:flex-col data-[orientation=vertical]:justify-center\",\n local.class\n )}\n {...(rest as any)}\n >\n <SliderFill />\n {local.children}\n </SliderPrimitive.Track>\n );\n};\n\nexport type SliderFillProps<T extends ValidComponent = \"div\"> =\n SliderPrimitive.SliderFillProps<T> & {\n class?: string;\n };\n\n/**\n * Active range indicator fill inside SliderTrack.\n */\nexport const SliderFill = <T extends ValidComponent = \"div\">(\n props: SliderFillProps<T>\n) => {\n const [local, rest] = splitProps(props as SliderFillProps, [\"class\"]);\n\n return (\n <SliderPrimitive.Fill\n class={cn(\n \"absolute rounded-lg bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type SliderThumbProps<T extends ValidComponent = \"span\"> =\n SliderPrimitive.SliderThumbProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Draggable thumb handle for selecting slider values.\n * Positioned centered on Kobalte offset coordinates using -translate-x-1/2 -translate-y-1/2.\n */\nexport const SliderThumb = <T extends ValidComponent = \"span\">(\n props: SliderThumbProps<T>\n) => {\n const [local, rest] = splitProps(props as SliderThumbProps, [\"class\", \"children\"]);\n\n return (\n <SliderPrimitive.Thumb\n class={cn(\n \"block h-5 w-5 rounded-lg border-2 border-primary bg-background shadow-md transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 cursor-grab active:cursor-grabbing z-10 data-[orientation=horizontal]:translate-x-0 data-[orientation=horizontal]:translate-y-0 data-[orientation=vertical]:translate-x-0 data-[orientation=vertical]:translate-y-0\",\n local.class\n )}\n {...(rest as any)}\n >\n <SliderPrimitive.Input />\n {local.children}\n </SliderPrimitive.Thumb>\n );\n};\n\nexport type SliderLabelProps<T extends ValidComponent = \"label\"> =\n SliderPrimitive.SliderLabelProps<T> & {\n class?: string;\n };\n\n/**\n * Accessible label for the Slider component.\n */\nexport const SliderLabel = <T extends ValidComponent = \"label\">(\n props: SliderLabelProps<T>\n) => {\n const [local, rest] = splitProps(props as SliderLabelProps, [\"class\"]);\n\n return (\n <SliderPrimitive.Label\n class={cn(\"text-sm font-medium leading-none text-foreground\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type SliderValueLabelProps<T extends ValidComponent = \"output\"> =\n SliderPrimitive.SliderValueLabelProps<T> & {\n class?: string;\n };\n\n/**\n * Displays current formatted value label for single or dual thumbs.\n */\nexport const SliderValueLabel = <T extends ValidComponent = \"output\">(\n props: SliderValueLabelProps<T>\n) => {\n const [local, rest] = splitProps(props as SliderValueLabelProps, [\"class\"]);\n\n return (\n <SliderPrimitive.ValueLabel\n class={cn(\"text-sm font-medium text-muted-foreground\", local.class)}\n {...(rest as any)}\n />\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { splitProps, type JSX, type ValidComponent } from "solid-js";
|
|
2
|
+
import * as ComboboxPrimitive from "@kobalte/core/combobox";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export type ComboboxRootProps<Option = any, OptGroup = any, T extends ValidComponent = "div"> =
|
|
6
|
+
ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T> & {
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: JSX.Element;
|
|
9
|
+
triggerMode?: "input" | "focus" | "both" | "manual";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Root Combobox component providing search, single/multi-selection, and group support.
|
|
14
|
+
* `triggerMode="focus"` or `triggerMode="both"` enables opening dropdown on input click/focus.
|
|
15
|
+
*/
|
|
16
|
+
export const Combobox = <Option = any, OptGroup = any, T extends ValidComponent = "div">(
|
|
17
|
+
props: ComboboxRootProps<Option, OptGroup, T>
|
|
18
|
+
) => {
|
|
19
|
+
const [local, rest] = splitProps(props as ComboboxRootProps, ["class", "children", "triggerMode"]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<ComboboxPrimitive.Root
|
|
23
|
+
triggerMode={local.triggerMode ?? "input"}
|
|
24
|
+
class={cn("relative w-full", local.class)}
|
|
25
|
+
{...(rest as any)}
|
|
26
|
+
>
|
|
27
|
+
{local.children}
|
|
28
|
+
</ComboboxPrimitive.Root>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ComboboxControlProps<Option = any, T extends ValidComponent = "div"> =
|
|
33
|
+
ComboboxPrimitive.ComboboxControlProps<Option, T> & {
|
|
34
|
+
class?: string;
|
|
35
|
+
children?: JSX.Element;
|
|
36
|
+
clearable?: boolean;
|
|
37
|
+
onClear?: () => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Input container for Combobox supporting search input, selected tags, and clear button.
|
|
42
|
+
*/
|
|
43
|
+
export const ComboboxControl = <Option = any, T extends ValidComponent = "div">(
|
|
44
|
+
props: ComboboxControlProps<Option, T>
|
|
45
|
+
) => {
|
|
46
|
+
const [local, rest] = splitProps(props as ComboboxControlProps, [
|
|
47
|
+
"class",
|
|
48
|
+
"children",
|
|
49
|
+
"clearable",
|
|
50
|
+
"onClear",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ComboboxPrimitive.Control
|
|
55
|
+
class={cn(
|
|
56
|
+
"flex min-h-9 w-full flex-wrap items-center justify-between rounded-md border border-input bg-muted px-3 py-1 text-sm shadow-sm transition-colors focus-within:ring-1 focus-within:ring-primary focus-within:border-primary disabled:cursor-not-allowed disabled:opacity-50 gap-1.5 text-foreground",
|
|
57
|
+
local.class
|
|
58
|
+
)}
|
|
59
|
+
{...(rest as any)}
|
|
60
|
+
>
|
|
61
|
+
<div class="flex flex-wrap items-center gap-1.5 flex-1 min-w-0">
|
|
62
|
+
{local.children}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="flex items-center gap-1 shrink-0 self-center">
|
|
66
|
+
{local.clearable && (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
tabIndex={-1}
|
|
70
|
+
onClick={(e) => {
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
if (local.onClear) local.onClear();
|
|
73
|
+
}}
|
|
74
|
+
class="rounded-sm p-0.5 opacity-60 hover:opacity-100 hover:bg-accent text-foreground transition-opacity focus:outline-none cursor-pointer"
|
|
75
|
+
aria-label="Clear selection"
|
|
76
|
+
>
|
|
77
|
+
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
78
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
79
|
+
</svg>
|
|
80
|
+
</button>
|
|
81
|
+
)}
|
|
82
|
+
<ComboboxTrigger />
|
|
83
|
+
</div>
|
|
84
|
+
</ComboboxPrimitive.Control>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type ComboboxInputProps<T extends ValidComponent = "input"> =
|
|
89
|
+
ComboboxPrimitive.ComboboxInputProps<T> & {
|
|
90
|
+
class?: string;
|
|
91
|
+
openOnFocus?: boolean;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Search input field embedded within ComboboxControl.
|
|
96
|
+
* Supports `openOnFocus` to automatically trigger the dropdown when focused or clicked.
|
|
97
|
+
*/
|
|
98
|
+
export const ComboboxInput = <T extends ValidComponent = "input">(
|
|
99
|
+
props: ComboboxInputProps<T>
|
|
100
|
+
) => {
|
|
101
|
+
const [local, rest] = splitProps(props as ComboboxInputProps, ["class", "openOnFocus", "onFocus", "onClick"]);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<ComboboxPrimitive.Input
|
|
105
|
+
class={cn(
|
|
106
|
+
"flex-1 bg-transparent py-1 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed text-foreground min-w-16",
|
|
107
|
+
local.class
|
|
108
|
+
)}
|
|
109
|
+
onFocus={(e: FocusEvent) => {
|
|
110
|
+
if (typeof local.onFocus === "function") local.onFocus(e as any);
|
|
111
|
+
}}
|
|
112
|
+
onClick={(e: MouseEvent) => {
|
|
113
|
+
if (typeof local.onClick === "function") local.onClick(e as any);
|
|
114
|
+
}}
|
|
115
|
+
{...(rest as any)}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type ComboboxTriggerProps<T extends ValidComponent = "button"> =
|
|
121
|
+
ComboboxPrimitive.ComboboxTriggerProps<T> & {
|
|
122
|
+
class?: string;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Dropdown chevron trigger icon.
|
|
127
|
+
*/
|
|
128
|
+
export const ComboboxTrigger = <T extends ValidComponent = "button">(
|
|
129
|
+
props: ComboboxTriggerProps<T>
|
|
130
|
+
) => {
|
|
131
|
+
const [local, rest] = splitProps(props as ComboboxTriggerProps, ["class"]);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<ComboboxPrimitive.Trigger
|
|
135
|
+
class={cn(
|
|
136
|
+
"flex h-4 w-4 items-center justify-center opacity-50 hover:opacity-100 transition-opacity focus:outline-none cursor-pointer",
|
|
137
|
+
local.class
|
|
138
|
+
)}
|
|
139
|
+
{...(rest as any)}
|
|
140
|
+
>
|
|
141
|
+
<ComboboxPrimitive.Icon
|
|
142
|
+
as="svg"
|
|
143
|
+
class="h-4 w-4 stroke-current stroke-2 fill-none"
|
|
144
|
+
viewBox="0 0 24 24"
|
|
145
|
+
>
|
|
146
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
147
|
+
</ComboboxPrimitive.Icon>
|
|
148
|
+
</ComboboxPrimitive.Trigger>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type ComboboxTokenProps<Option = any> = {
|
|
153
|
+
class?: string;
|
|
154
|
+
children?: JSX.Element;
|
|
155
|
+
item?: Option;
|
|
156
|
+
onRemove?: () => void;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Selected item tag token pill rendered in multi-select mode.
|
|
161
|
+
*/
|
|
162
|
+
export const ComboboxToken = <Option = any>(props: ComboboxTokenProps<Option>) => {
|
|
163
|
+
const [local, rest] = splitProps(props, ["class", "children", "onRemove"]);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<span
|
|
167
|
+
class={cn(
|
|
168
|
+
"inline-flex items-center gap-1.5 rounded-md bg-accent px-2 py-0.5 text-xs font-medium text-accent-foreground border border-border shadow-2xs animate-in fade-in-50",
|
|
169
|
+
local.class
|
|
170
|
+
)}
|
|
171
|
+
{...rest}
|
|
172
|
+
>
|
|
173
|
+
<span class="truncate">{local.children}</span>
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
tabIndex={-1}
|
|
177
|
+
onClick={(e) => {
|
|
178
|
+
e.stopPropagation();
|
|
179
|
+
if (local.onRemove) local.onRemove();
|
|
180
|
+
}}
|
|
181
|
+
class="rounded-xs p-0.5 hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground transition-colors focus:outline-none cursor-pointer"
|
|
182
|
+
aria-label="Remove tag"
|
|
183
|
+
>
|
|
184
|
+
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
185
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
186
|
+
</svg>
|
|
187
|
+
</button>
|
|
188
|
+
</span>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export type ComboboxContentProps<T extends ValidComponent = "div"> =
|
|
193
|
+
ComboboxPrimitive.ComboboxContentProps<T> & {
|
|
194
|
+
class?: string;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Portaled popover content listbox container.
|
|
199
|
+
*/
|
|
200
|
+
export const ComboboxContent = <T extends ValidComponent = "div">(
|
|
201
|
+
props: ComboboxContentProps<T>
|
|
202
|
+
) => {
|
|
203
|
+
const [local, rest] = splitProps(props as ComboboxContentProps, ["class"]);
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<ComboboxPrimitive.Portal>
|
|
207
|
+
<ComboboxPrimitive.Content
|
|
208
|
+
class={cn(
|
|
209
|
+
"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 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
|
210
|
+
local.class
|
|
211
|
+
)}
|
|
212
|
+
{...(rest as any)}
|
|
213
|
+
>
|
|
214
|
+
<ComboboxPrimitive.Listbox class="max-h-60 overflow-y-auto p-1 outline-none" />
|
|
215
|
+
</ComboboxPrimitive.Content>
|
|
216
|
+
</ComboboxPrimitive.Portal>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export type ComboboxItemProps<T extends ValidComponent = "li"> =
|
|
221
|
+
ComboboxPrimitive.ComboboxItemProps<T> & {
|
|
222
|
+
class?: string;
|
|
223
|
+
children?: JSX.Element;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Individual option item inside listbox supporting custom avatars, icons, and titles.
|
|
228
|
+
*/
|
|
229
|
+
export const ComboboxItem = <T extends ValidComponent = "li">(
|
|
230
|
+
props: ComboboxItemProps<T>
|
|
231
|
+
) => {
|
|
232
|
+
const [local, rest] = splitProps(props as ComboboxItemProps, ["class", "children"]);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<ComboboxPrimitive.Item
|
|
236
|
+
class={cn(
|
|
237
|
+
"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-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground text-popover-foreground transition-colors",
|
|
238
|
+
local.class
|
|
239
|
+
)}
|
|
240
|
+
{...rest}
|
|
241
|
+
>
|
|
242
|
+
<ComboboxPrimitive.ItemIndicator class="absolute left-2 flex h-4 w-4 items-center justify-center text-primary">
|
|
243
|
+
<svg class="h-4 w-4 fill-none stroke-current stroke-2" viewBox="0 0 24 24">
|
|
244
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
245
|
+
</svg>
|
|
246
|
+
</ComboboxPrimitive.ItemIndicator>
|
|
247
|
+
<ComboboxPrimitive.ItemLabel class="flex items-center gap-2 w-full truncate">
|
|
248
|
+
{local.children}
|
|
249
|
+
</ComboboxPrimitive.ItemLabel>
|
|
250
|
+
</ComboboxPrimitive.Item>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export type ComboboxGroupProps<T extends ValidComponent = "li"> =
|
|
255
|
+
ComboboxPrimitive.ComboboxSectionProps<T> & {
|
|
256
|
+
class?: string;
|
|
257
|
+
children?: JSX.Element;
|
|
258
|
+
label?: JSX.Element;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Group container for organizing related options.
|
|
263
|
+
*/
|
|
264
|
+
export const ComboboxGroup = <T extends ValidComponent = "li">(
|
|
265
|
+
props: ComboboxGroupProps<T>
|
|
266
|
+
) => {
|
|
267
|
+
const [local, rest] = splitProps(props as ComboboxGroupProps, ["class", "children", "label"]);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<ComboboxPrimitive.Section class={cn("px-1 py-1", local.class)} {...(rest as any)}>
|
|
271
|
+
{local.label && (
|
|
272
|
+
<span class="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider block">
|
|
273
|
+
{local.label}
|
|
274
|
+
</span>
|
|
275
|
+
)}
|
|
276
|
+
{local.children}
|
|
277
|
+
</ComboboxPrimitive.Section>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Empty state notice when no matching search items exist.
|
|
283
|
+
*/
|
|
284
|
+
export const ComboboxEmpty = (props: { class?: string; children?: JSX.Element }) => {
|
|
285
|
+
return (
|
|
286
|
+
<div class={cn("py-6 text-center text-sm text-muted-foreground", props.class)}>
|
|
287
|
+
{props.children || "No matching items found."}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Hidden native select element for form integrations.
|
|
294
|
+
*/
|
|
295
|
+
export const ComboboxHiddenSelect = ComboboxPrimitive.HiddenSelect;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { splitProps, type JSX, type ValidComponent } from "solid-js";
|
|
2
|
+
import * as HoverCardPrimitive from "@kobalte/core/hover-card";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export type HoverCardProps = HoverCardPrimitive.HoverCardRootProps;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Root HoverCard component built on Kobalte primitives.
|
|
9
|
+
* Controls hover open/close delays (default: 200ms / 150ms).
|
|
10
|
+
*/
|
|
11
|
+
export const HoverCard = (props: HoverCardProps) => {
|
|
12
|
+
return (
|
|
13
|
+
<HoverCardPrimitive.Root
|
|
14
|
+
openDelay={200}
|
|
15
|
+
closeDelay={150}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type HoverCardTriggerProps<T extends ValidComponent = "a"> =
|
|
22
|
+
HoverCardPrimitive.HoverCardTriggerProps<T> &
|
|
23
|
+
JSX.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
24
|
+
class?: string;
|
|
25
|
+
children?: JSX.Element;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* HoverCard trigger handle (link, avatar, or text).
|
|
30
|
+
*/
|
|
31
|
+
export const HoverCardTrigger = <T extends ValidComponent = "a">(
|
|
32
|
+
props: HoverCardTriggerProps<T>
|
|
33
|
+
) => {
|
|
34
|
+
const [local, rest] = splitProps(props as HoverCardTriggerProps, [
|
|
35
|
+
"class",
|
|
36
|
+
"children",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<HoverCardPrimitive.Trigger
|
|
41
|
+
class={cn(
|
|
42
|
+
"inline-flex items-center text-sm font-medium text-foreground underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
|
|
43
|
+
local.class
|
|
44
|
+
)}
|
|
45
|
+
{...(rest as any)}
|
|
46
|
+
>
|
|
47
|
+
{local.children}
|
|
48
|
+
</HoverCardPrimitive.Trigger>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type HoverCardContentProps<T extends ValidComponent = "div"> =
|
|
53
|
+
HoverCardPrimitive.HoverCardContentProps<T> & {
|
|
54
|
+
class?: string;
|
|
55
|
+
children?: JSX.Element;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Floating card preview container rendered in a portal layer.
|
|
60
|
+
* Includes offset spacing from trigger and support for optional HoverCardArrow.
|
|
61
|
+
*/
|
|
62
|
+
export const HoverCardContent = <T extends ValidComponent = "div">(
|
|
63
|
+
props: HoverCardContentProps<T>
|
|
64
|
+
) => {
|
|
65
|
+
const [local, rest] = splitProps(props as HoverCardContentProps, [
|
|
66
|
+
"class",
|
|
67
|
+
"children",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<HoverCardPrimitive.Portal>
|
|
72
|
+
<HoverCardPrimitive.Content
|
|
73
|
+
sideOffset={8}
|
|
74
|
+
class={cn(
|
|
75
|
+
"z-50 w-80 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-xl outline-none transition-all data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-closed:zoom-out-95 data-expanded:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
76
|
+
local.class
|
|
77
|
+
)}
|
|
78
|
+
{...(rest as any)}
|
|
79
|
+
>
|
|
80
|
+
{local.children}
|
|
81
|
+
</HoverCardPrimitive.Content>
|
|
82
|
+
</HoverCardPrimitive.Portal>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type HoverCardArrowProps<T extends ValidComponent = "div"> =
|
|
87
|
+
HoverCardPrimitive.HoverCardArrowProps<T> & {
|
|
88
|
+
class?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Optional arrow pointer pointing to the trigger.
|
|
93
|
+
*/
|
|
94
|
+
export const HoverCardArrow = <T extends ValidComponent = "div">(
|
|
95
|
+
props: HoverCardArrowProps<T>
|
|
96
|
+
) => {
|
|
97
|
+
const [local, rest] = splitProps(props as HoverCardArrowProps, ["class"]);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<HoverCardPrimitive.Arrow
|
|
101
|
+
class={cn("fill-popover stroke-border stroke-1", local.class)}
|
|
102
|
+
{...(rest as any)}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSignal,
|
|
3
|
+
createMemo,
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
type JSX,
|
|
7
|
+
} from "solid-js";
|
|
8
|
+
import { cn } from "@/lib/cn";
|
|
9
|
+
|
|
10
|
+
export type PinInputType = "numeric" | "alphanumeric";
|
|
11
|
+
|
|
12
|
+
export interface PinInputContextValue {
|
|
13
|
+
value: () => string;
|
|
14
|
+
setValue: (val: string) => void;
|
|
15
|
+
mask: () => boolean;
|
|
16
|
+
disabled: () => boolean;
|
|
17
|
+
length: () => number;
|
|
18
|
+
type: () => PinInputType;
|
|
19
|
+
containerRef: HTMLDivElement | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PinInputContext = createContext<PinInputContextValue>();
|
|
23
|
+
|
|
24
|
+
export interface PinInputProps {
|
|
25
|
+
value?: string;
|
|
26
|
+
onValueChange?: (value: string) => void;
|
|
27
|
+
length?: number;
|
|
28
|
+
type?: PinInputType;
|
|
29
|
+
mask?: boolean;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
class?: string;
|
|
32
|
+
children?: JSX.Element;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Root PinInput component for 4 or 6-digit OTP / verification PIN codes.
|
|
37
|
+
*/
|
|
38
|
+
export const PinInput = (props: PinInputProps) => {
|
|
39
|
+
let containerRef: HTMLDivElement | undefined;
|
|
40
|
+
const [internalValue, setInternalValue] = createSignal(props.value ?? "");
|
|
41
|
+
|
|
42
|
+
const value = () => props.value ?? internalValue();
|
|
43
|
+
const mask = () => props.mask ?? false;
|
|
44
|
+
const disabled = () => props.disabled ?? false;
|
|
45
|
+
const length = () => props.length ?? 6;
|
|
46
|
+
|
|
47
|
+
const inputType = (): PinInputType => {
|
|
48
|
+
const t = props.type ?? "numeric";
|
|
49
|
+
if (t !== "numeric" && t !== "alphanumeric") {
|
|
50
|
+
return "numeric";
|
|
51
|
+
}
|
|
52
|
+
return t;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleValueChange = (val: string) => {
|
|
56
|
+
setInternalValue(val);
|
|
57
|
+
props.onValueChange?.(val);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const contextValue: PinInputContextValue = {
|
|
61
|
+
value,
|
|
62
|
+
setValue: handleValueChange,
|
|
63
|
+
mask,
|
|
64
|
+
disabled,
|
|
65
|
+
length,
|
|
66
|
+
type: inputType,
|
|
67
|
+
get containerRef() {
|
|
68
|
+
return containerRef;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<PinInputContext.Provider value={contextValue}>
|
|
74
|
+
<div ref={containerRef} class={cn("flex items-center gap-2", props.class)}>
|
|
75
|
+
{props.children}
|
|
76
|
+
</div>
|
|
77
|
+
</PinInputContext.Provider>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export interface PinInputLabelProps {
|
|
82
|
+
class?: string;
|
|
83
|
+
children?: JSX.Element;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Accessible label for PinInput.
|
|
88
|
+
*/
|
|
89
|
+
export const PinInputLabel = (props: PinInputLabelProps) => {
|
|
90
|
+
return (
|
|
91
|
+
<label class={cn("text-sm font-medium leading-none text-foreground", props.class)}>
|
|
92
|
+
{props.children}
|
|
93
|
+
</label>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const focusIndex = (containerRef: HTMLDivElement | undefined, index: number) => {
|
|
98
|
+
const el = containerRef?.querySelector<HTMLInputElement>(
|
|
99
|
+
`[data-pin-input-index="${index}"]`
|
|
100
|
+
);
|
|
101
|
+
el?.focus();
|
|
102
|
+
el?.select();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export interface PinInputInputProps {
|
|
106
|
+
index: number;
|
|
107
|
+
class?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Individual input slot for a single OTP digit/character.
|
|
112
|
+
*/
|
|
113
|
+
export const PinInputInput = (props: PinInputInputProps) => {
|
|
114
|
+
const ctx = useContext(PinInputContext);
|
|
115
|
+
const digit = createMemo(() => ctx?.value()[props.index] ?? "");
|
|
116
|
+
|
|
117
|
+
const isCharAllowed = (char: string) => {
|
|
118
|
+
if (!ctx || char === "") return false;
|
|
119
|
+
return ctx.type() === "numeric"
|
|
120
|
+
? /^[0-9]$/.test(char)
|
|
121
|
+
: /^[a-zA-Z0-9]$/.test(char);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const commitChar = (char: string) => {
|
|
125
|
+
if (!ctx) return;
|
|
126
|
+
const currentArray = ctx.value().split("");
|
|
127
|
+
while (currentArray.length < props.index) currentArray.push("");
|
|
128
|
+
currentArray[props.index] = char;
|
|
129
|
+
ctx.setValue(currentArray.join(""));
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleInput = (e: InputEvent & { currentTarget: HTMLInputElement }) => {
|
|
133
|
+
if (!ctx || ctx.disabled()) return;
|
|
134
|
+
const val = e.currentTarget.value;
|
|
135
|
+
const rawChar = val ? val[val.length - 1] : "";
|
|
136
|
+
|
|
137
|
+
// Clear digit if input is empty
|
|
138
|
+
if (rawChar === "") {
|
|
139
|
+
commitChar("");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!isCharAllowed(rawChar)) {
|
|
144
|
+
e.currentTarget.value = digit();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
commitChar(rawChar);
|
|
149
|
+
|
|
150
|
+
if (props.index < ctx.length() - 1) {
|
|
151
|
+
focusIndex(ctx.containerRef, props.index + 1);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
156
|
+
if (!ctx || ctx.disabled()) return;
|
|
157
|
+
if (e.key === "Backspace" && digit() === "" && props.index > 0) {
|
|
158
|
+
focusIndex(ctx.containerRef, props.index - 1);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handlePaste = (e: ClipboardEvent & { currentTarget: HTMLInputElement }) => {
|
|
163
|
+
if (!ctx || ctx.disabled()) return;
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
const pasted = e.clipboardData?.getData("text") ?? "";
|
|
166
|
+
const chars = pasted.split("").filter(isCharAllowed);
|
|
167
|
+
if (chars.length === 0) return;
|
|
168
|
+
|
|
169
|
+
const currentArray = ctx.value().split("");
|
|
170
|
+
let lastFilled = props.index;
|
|
171
|
+
for (let i = 0; i < chars.length && props.index + i < ctx.length(); i++) {
|
|
172
|
+
while (currentArray.length < props.index + i) currentArray.push("");
|
|
173
|
+
currentArray[props.index + i] = chars[i];
|
|
174
|
+
lastFilled = props.index + i;
|
|
175
|
+
}
|
|
176
|
+
ctx.setValue(currentArray.join(""));
|
|
177
|
+
focusIndex(ctx.containerRef, Math.min(lastFilled + 1, ctx.length() - 1));
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<input
|
|
182
|
+
type={ctx?.mask() ? "password" : "text"}
|
|
183
|
+
inputMode={ctx?.type() === "numeric" ? "numeric" : "text"}
|
|
184
|
+
autocomplete="one-time-code"
|
|
185
|
+
maxLength={1}
|
|
186
|
+
disabled={ctx?.disabled()}
|
|
187
|
+
data-pin-input-index={props.index}
|
|
188
|
+
value={digit()}
|
|
189
|
+
onInput={handleInput}
|
|
190
|
+
onKeyDown={handleKeyDown}
|
|
191
|
+
onPaste={handlePaste}
|
|
192
|
+
class={cn(
|
|
193
|
+
"relative flex h-10 w-10 text-center text-sm font-semibold rounded-md border border-input bg-background shadow-xs transition-all focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50",
|
|
194
|
+
props.class
|
|
195
|
+
)}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { splitProps, type JSX, type ValidComponent } from "solid-js";
|
|
2
|
+
import * as SliderPrimitive from "@kobalte/core/slider";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export type SliderRootProps<T extends ValidComponent = "div"> =
|
|
6
|
+
SliderPrimitive.SliderRootProps<T> & {
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: JSX.Element;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Root Slider component built on Kobalte primitives.
|
|
13
|
+
*/
|
|
14
|
+
export const Slider = <T extends ValidComponent = "div">(
|
|
15
|
+
props: SliderRootProps<T>
|
|
16
|
+
) => {
|
|
17
|
+
const [local, rest] = splitProps(props as SliderRootProps, ["class", "children"]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<SliderPrimitive.Root
|
|
21
|
+
class={cn(
|
|
22
|
+
"relative flex w-full touch-none select-none flex-col gap-2 data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:items-center",
|
|
23
|
+
local.class
|
|
24
|
+
)}
|
|
25
|
+
{...(rest as any)}
|
|
26
|
+
>
|
|
27
|
+
{local.children}
|
|
28
|
+
</SliderPrimitive.Root>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type SliderTrackProps<T extends ValidComponent = "div"> =
|
|
33
|
+
SliderPrimitive.SliderTrackProps<T> & {
|
|
34
|
+
class?: string;
|
|
35
|
+
children?: JSX.Element;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Slider track containing the active range fill and thumbs.
|
|
40
|
+
*/
|
|
41
|
+
export const SliderTrack = <T extends ValidComponent = "div">(
|
|
42
|
+
props: SliderTrackProps<T>
|
|
43
|
+
) => {
|
|
44
|
+
const [local, rest] = splitProps(props as SliderTrackProps, ["class", "children"]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<SliderPrimitive.Track
|
|
48
|
+
class={cn(
|
|
49
|
+
"relative h-2 w-full flex items-center rounded-lg bg-secondary cursor-pointer data-[orientation=vertical]:w-2 data-[orientation=vertical]:h-full data-[orientation=vertical]:flex-col data-[orientation=vertical]:justify-center",
|
|
50
|
+
local.class
|
|
51
|
+
)}
|
|
52
|
+
{...(rest as any)}
|
|
53
|
+
>
|
|
54
|
+
<SliderFill />
|
|
55
|
+
{local.children}
|
|
56
|
+
</SliderPrimitive.Track>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type SliderFillProps<T extends ValidComponent = "div"> =
|
|
61
|
+
SliderPrimitive.SliderFillProps<T> & {
|
|
62
|
+
class?: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Active range indicator fill inside SliderTrack.
|
|
67
|
+
*/
|
|
68
|
+
export const SliderFill = <T extends ValidComponent = "div">(
|
|
69
|
+
props: SliderFillProps<T>
|
|
70
|
+
) => {
|
|
71
|
+
const [local, rest] = splitProps(props as SliderFillProps, ["class"]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<SliderPrimitive.Fill
|
|
75
|
+
class={cn(
|
|
76
|
+
"absolute rounded-lg bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
|
77
|
+
local.class
|
|
78
|
+
)}
|
|
79
|
+
{...(rest as any)}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type SliderThumbProps<T extends ValidComponent = "span"> =
|
|
85
|
+
SliderPrimitive.SliderThumbProps<T> & {
|
|
86
|
+
class?: string;
|
|
87
|
+
children?: JSX.Element;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Draggable thumb handle for selecting slider values.
|
|
92
|
+
* Positioned centered on Kobalte offset coordinates using -translate-x-1/2 -translate-y-1/2.
|
|
93
|
+
*/
|
|
94
|
+
export const SliderThumb = <T extends ValidComponent = "span">(
|
|
95
|
+
props: SliderThumbProps<T>
|
|
96
|
+
) => {
|
|
97
|
+
const [local, rest] = splitProps(props as SliderThumbProps, ["class", "children"]);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<SliderPrimitive.Thumb
|
|
101
|
+
class={cn(
|
|
102
|
+
"block h-5 w-5 rounded-lg border-2 border-primary bg-background shadow-md transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 cursor-grab active:cursor-grabbing z-10 data-[orientation=horizontal]:translate-x-0 data-[orientation=horizontal]:translate-y-0 data-[orientation=vertical]:translate-x-0 data-[orientation=vertical]:translate-y-0",
|
|
103
|
+
local.class
|
|
104
|
+
)}
|
|
105
|
+
{...(rest as any)}
|
|
106
|
+
>
|
|
107
|
+
<SliderPrimitive.Input />
|
|
108
|
+
{local.children}
|
|
109
|
+
</SliderPrimitive.Thumb>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type SliderLabelProps<T extends ValidComponent = "label"> =
|
|
114
|
+
SliderPrimitive.SliderLabelProps<T> & {
|
|
115
|
+
class?: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Accessible label for the Slider component.
|
|
120
|
+
*/
|
|
121
|
+
export const SliderLabel = <T extends ValidComponent = "label">(
|
|
122
|
+
props: SliderLabelProps<T>
|
|
123
|
+
) => {
|
|
124
|
+
const [local, rest] = splitProps(props as SliderLabelProps, ["class"]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<SliderPrimitive.Label
|
|
128
|
+
class={cn("text-sm font-medium leading-none text-foreground", local.class)}
|
|
129
|
+
{...(rest as any)}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type SliderValueLabelProps<T extends ValidComponent = "output"> =
|
|
135
|
+
SliderPrimitive.SliderValueLabelProps<T> & {
|
|
136
|
+
class?: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Displays current formatted value label for single or dual thumbs.
|
|
141
|
+
*/
|
|
142
|
+
export const SliderValueLabel = <T extends ValidComponent = "output">(
|
|
143
|
+
props: SliderValueLabelProps<T>
|
|
144
|
+
) => {
|
|
145
|
+
const [local, rest] = splitProps(props as SliderValueLabelProps, ["class"]);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<SliderPrimitive.ValueLabel
|
|
149
|
+
class={cn("text-sm font-medium text-muted-foreground", local.class)}
|
|
150
|
+
{...(rest as any)}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
};
|
package/src/registry/metadata.ts
CHANGED
|
@@ -75,6 +75,11 @@ export const COMPONENT_METADATA: Record<string, ComponentMeta> = {
|
|
|
75
75
|
description: "Displays a list of options for the user to pick from, built on Kobalte primitives.",
|
|
76
76
|
dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
|
|
77
77
|
},
|
|
78
|
+
combobox: {
|
|
79
|
+
title: "Combobox",
|
|
80
|
+
description: "Searchable autocomplete dropdown with single/multi-selection tags, avatars, group headers, and customizable clear controls.",
|
|
81
|
+
dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
|
|
82
|
+
},
|
|
78
83
|
tabs: {
|
|
79
84
|
title: "Tabs",
|
|
80
85
|
description: "A set of layered sections of content displayed one at a time.",
|
|
@@ -169,6 +174,21 @@ export const COMPONENT_METADATA: Record<string, ComponentMeta> = {
|
|
|
169
174
|
description: "Displays an indicator showing the completion progress of a task or media playback, built on Kobalte primitives.",
|
|
170
175
|
dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
|
|
171
176
|
},
|
|
177
|
+
slider: {
|
|
178
|
+
title: "Slider",
|
|
179
|
+
description: "Numeric range selection slider supporting single/dual thumbs, custom steps, vertical orientation, and formatted value labels, built on Kobalte primitives.",
|
|
180
|
+
dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
|
|
181
|
+
},
|
|
182
|
+
"pin-input": {
|
|
183
|
+
title: "Pin Input",
|
|
184
|
+
description: "Interactive multi-slot PIN/OTP input component for SMS and 2FA authentication verification codes.",
|
|
185
|
+
dependencies: ["clsx", "tailwind-merge"],
|
|
186
|
+
},
|
|
187
|
+
"hover-card": {
|
|
188
|
+
title: "Hover Card",
|
|
189
|
+
description: "Profile and link preview popover triggered on hover, built on Kobalte primitives.",
|
|
190
|
+
dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
|
|
191
|
+
},
|
|
172
192
|
};
|
|
173
193
|
|
|
174
194
|
/**
|