@nikala-ui/core 0.9.10 → 0.9.11-nightly.78f0a94
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/aspect-ratio.json +17 -0
- package/registry/collapsible.json +18 -0
- package/registry/combobox.json +18 -0
- package/registry/command.json +1 -1
- package/registry/hover-card.json +18 -0
- package/registry/index.json +64 -0
- package/registry/pin-input.json +17 -0
- package/registry/slider.json +18 -0
- package/src/registry/components/ui/aspect-ratio.tsx +31 -0
- package/src/registry/components/ui/collapsible.tsx +73 -0
- package/src/registry/components/ui/combobox.tsx +295 -0
- package/src/registry/components/ui/command.tsx +90 -7
- 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 +30 -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-nightly.78f0a94",
|
|
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,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aspect-ratio",
|
|
3
|
+
"title": "Aspect Ratio",
|
|
4
|
+
"description": "Displays content within a specific aspect ratio using CSS aspect-ratio while preventing layout shifts.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "ui/aspect-ratio.tsx",
|
|
13
|
+
"content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface AspectRatioProps extends JSX.HTMLAttributes<HTMLDivElement> {\n ratio?: number;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Nikala UI AspectRatio Component.\n * Displays content within a specific aspect ratio (e.g. 16/9, 4/3, 1/1) using CSS aspect-ratio.\n */\nexport const AspectRatio: Component<AspectRatioProps> = (props) => {\n const [local, rest] = splitProps(props, [\"ratio\", \"class\", \"children\", \"style\"]);\n\n const computedRatio = () => (local.ratio !== undefined ? local.ratio : 16 / 9);\n\n return (\n <div\n class={cn(\"relative w-full overflow-hidden\", local.class)}\n style={{\n \"aspect-ratio\": `${computedRatio()}`,\n ...(typeof local.style === \"object\" ? local.style : {}),\n }}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
|
|
14
|
+
"type": "registry:ui"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "collapsible",
|
|
3
|
+
"title": "Collapsible",
|
|
4
|
+
"description": "An interactive component that expands and collapses content panels with smooth height animations, 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/collapsible.tsx",
|
|
14
|
+
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as CollapsiblePrimitive from \"@kobalte/core/collapsible\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cn } from \"@/lib/cn\";\n\nexport type CollapsibleRootProps<T extends ValidComponent = \"div\"> =\n CollapsiblePrimitive.CollapsibleRootProps<T> & {\n class?: string;\n };\n\n/**\n * Root Collapsible component built on Kobalte primitives.\n */\nexport const Collapsible = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, CollapsibleRootProps<T>>\n) => {\n const [local, rest] = splitProps(props as CollapsibleRootProps, [\"class\"]);\n return (\n <CollapsiblePrimitive.Root\n class={cn(\"w-full\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type CollapsibleTriggerProps<T extends ValidComponent = \"button\"> =\n CollapsiblePrimitive.CollapsibleTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Trigger element that toggles the Collapsible open/closed state.\n */\nexport const CollapsibleTrigger = <T extends ValidComponent = \"button\">(\n props: PolymorphicProps<T, CollapsibleTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as CollapsibleTriggerProps, [\"class\", \"children\"]);\n return (\n <CollapsiblePrimitive.Trigger\n class={cn(\"flex w-full items-center justify-between cursor-pointer\", local.class)}\n {...rest}\n >\n {local.children}\n </CollapsiblePrimitive.Trigger>\n );\n};\n\nexport type CollapsibleContentProps<T extends ValidComponent = \"div\"> =\n CollapsiblePrimitive.CollapsibleContentProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Collapsible content panel revealed when opened.\n */\nexport const CollapsibleContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, CollapsibleContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as CollapsibleContentProps, [\"class\", \"children\"]);\n return (\n <CollapsiblePrimitive.Content\n class={cn(\n \"overflow-hidden transition-all data-expanded:animate-collapsible-down data-closed:animate-collapsible-up\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </CollapsiblePrimitive.Content>\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -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
|
+
}
|
package/registry/command.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
{
|
|
20
20
|
"path": "ui/command.tsx",
|
|
21
|
-
"content": "import {\n createContext,\n useContext,\n createSignal,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createKeybindings } from \"@nikala-ui/hooks\";\nimport { Dialog } from \"@kobalte/core/dialog\";\nimport { Search, ArrowUp, ArrowDown, CornerDownLeft } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\nimport { Kbd, KbdGroup } from \"../ui/kbd\";\nimport { InputGroup, InputGroupInput, InputGroupAddon } from \"../ui/input-group\";\nimport { List, ListGroup, ListHeader, ListItem, type ListItemProps } from \"../ui/list\";\n\n/* --- Command Context State --- */\ninterface CommandContextValue {\n search: Accessor<string>;\n setSearch: (value: string) => void;\n}\n\nconst CommandContext = createContext<CommandContextValue>();\n\nexport const useCommand = () => {\n const ctx = useContext(CommandContext);\n if (!ctx) {\n throw new Error(\"useCommand must be used within a <Command />\");\n }\n return ctx;\n};\n\n/* --- Root Command Container --- */\nexport interface CommandProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const Command: Component<CommandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const [search, setSearch] = createSignal(\"\");\n\n return (\n <CommandContext.Provider value={{ search, setSearch }}>\n <div\n class={cn(\n \"flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </CommandContext.Provider>\n );\n};\n\n/* --- Command Dialog (Modal) --- */\nexport interface CommandDialogProps {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n enableHotkey?: boolean;\n children?: JSX.Element;\n class?: string;\n}\n\nexport const CommandDialog: Component<CommandDialogProps> = (props) => {\n const [internalOpen, setInternalOpen] = createSignal(false);\n\n const isOpen = () => (props.open !== undefined ? props.open : internalOpen());\n const setOpen = (val: boolean) => {\n if (props.open === undefined) setInternalOpen(val);\n if (typeof props.onOpenChange === \"function\") props.onOpenChange(val);\n };\n\n /* Listen for global Ctrl+K / Cmd+K hotkeys */\n createKeybindings(\n [\n {\n key: [\"meta+k\", \"ctrl+k\"],\n handler: () => setOpen(!isOpen()),\n preventDefault: true,\n },\n ],\n {\n enabled: () => props.enableHotkey !== false,\n }\n );\n\n return (\n <Dialog open={isOpen()} onOpenChange={setOpen}>\n <Dialog.Portal>\n <Dialog.Overlay class=\"fixed inset-0 z-50 bg-black/60 backdrop-blur-xs data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\" />\n <div class=\"fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4\">\n <Dialog.Content class=\"w-full max-w-xl rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl outline-none 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\">\n <Command class={props.class}>{props.children}</Command>\n </Dialog.Content>\n </div>\n </Dialog.Portal>\n </Dialog>\n );\n};\n\n/* --- Command Input --- */\nexport interface CommandInputProps\n extends JSX.InputHTMLAttributes<HTMLInputElement> {\n class?: string;\n}\n\nexport const CommandInput: Component<CommandInputProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"value\", \"onInput\"]);\n const { search, setSearch } = useCommand();\n\n return (\n <InputGroup class=\"border-0 border-b border-border rounded-none bg-transparent px-3 py-1 shadow-none focus-within:ring-0 focus-within:border-border\">\n <InputGroupAddon align=\"inline-start\">\n <Search class=\"w-4 h-4 text-muted-foreground\" />\n </InputGroupAddon>\n\n <InputGroupInput\n value={search()}\n onInput={(e) => {\n setSearch(e.currentTarget.value);\n if (typeof local.onInput === \"function\") {\n local.onInput(e);\n }\n }}\n placeholder=\"Type a command or search...\"\n class=\"h-11 text-base sm:text-sm font-medium\"\n {...rest}\n />\n </InputGroup>\n );\n};\n\n/* --- Command List Container --- */\nexport interface CommandListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandList: Component<CommandListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"max-h-82.5 overflow-y-auto p-1.5 scrollbar-thin\", local.class)}\n {...rest}\n >\n <List>{local.children}</List>\n </div>\n );\n};\n\n/* --- Command Empty Block --- */\nexport interface CommandEmptyProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandEmpty: Component<CommandEmptyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { search } = useCommand();\n\n return (\n <Show when={search().trim().length > 0}>\n <div\n class={cn(\n \"py-8 text-center text-sm text-muted-foreground font-medium select-none\",\n local.class\n )}\n {...rest}\n >\n {local.children || `No results found for \"${search()}\".`}\n </div>\n </Show>\n );\n};\n\n/* --- Command Group --- */\nexport interface CommandGroupProps {\n heading: string;\n children?: JSX.Element;\n class?: string;\n}\n\nexport const CommandGroup: Component<CommandGroupProps> = (props) => {\n return (\n <ListGroup class={props.class}>\n <ListHeader title={props.heading} />\n {props.children}\n </ListGroup>\n );\n};\n\n/* --- Command Item --- */\nexport interface CommandItemProps extends ListItemProps {\n keywords?: string[];\n onSelect?: () => void;\n}\n\nexport const CommandItem: Component<CommandItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"title\",\n \"subtitle\",\n \"keywords\",\n \"onSelect\",\n \"onClick\",\n \"class\",\n ]);\n const { search } = useCommand();\n\n /* Auto fuzzy-filter item based on search query */\n const matchesSearch = () => {\n const query = search().toLowerCase().trim();\n if (!query) return true;\n\n const titleMatch = local.title?.toLowerCase().includes(query);\n const subtitleMatch = local.subtitle?.toLowerCase().includes(query);\n const keywordMatch = local.keywords?.some((k) =>\n k.toLowerCase().includes(query)\n );\n\n return Boolean(titleMatch || subtitleMatch || keywordMatch);\n };\n\n const handleClick = (e: MouseEvent) => {\n if (typeof local.onSelect === \"function\") {\n local.onSelect();\n }\n if (typeof local.onClick === \"function\") {\n local.onClick(e as any);\n }\n };\n\n return (\n <Show when={matchesSearch()}>\n <ListItem\n title={local.title}\n subtitle={local.subtitle}\n onClick={handleClick}\n class={local.class}\n {...rest}\n />\n </Show>\n );\n};\n\n/* --- Command Footer --- */\nexport interface CommandFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandFooter: Component<CommandFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between border-t border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground select-none\",\n local.class\n )}\n {...rest}\n >\n <div class=\"flex items-center gap-3\">\n <span class=\"flex items-center gap-1\">\n <KbdGroup>\n <Kbd size=\"sm\"><ArrowUp class=\"w-2.5 h-2.5\" /></Kbd>\n <Kbd size=\"sm\"><ArrowDown class=\"w-2.5 h-2.5\" /></Kbd>\n </KbdGroup>\n <span>Navigate</span>\n </span>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\"><CornerDownLeft class=\"w-2.5 h-2.5\" /></Kbd>\n <span>Select</span>\n </span>\n </div>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\">Esc</Kbd>\n <span>Close</span>\n </span>\n </div>\n );\n};",
|
|
21
|
+
"content": "import {\n createContext,\n useContext,\n createSignal,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createKeybindings } from \"@nikala-ui/hooks\";\nimport { Dialog } from \"@kobalte/core/dialog\";\nimport { Search, ArrowUp, ArrowDown, CornerDownLeft } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\nimport { Kbd, KbdGroup } from \"../ui/kbd\";\nimport { InputGroup, InputGroupInput, InputGroupAddon } from \"../ui/input-group\";\nimport { List, ListGroup, ListHeader, ListItem, type ListItemProps } from \"../ui/list\";\n\n/* --- Command Context State --- */\ninterface CommandContextValue {\n search: Accessor<string>;\n setSearch: (value: string) => void;\n activeIndex: Accessor<number>;\n setActiveIndex: (fn: (prev: number) => number) => void;\n registerItemIndex: (element: HTMLElement) => number;\n onItemSelect: (index: number, action?: () => void) => void;\n}\n\nconst CommandContext = createContext<CommandContextValue>();\n\nexport const useCommand = () => {\n const ctx = useContext(CommandContext);\n if (!ctx) {\n throw new Error(\"useCommand must be used within a <Command />\");\n }\n return ctx;\n};\n\n/* --- Root Command Container --- */\nexport interface CommandProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"children\"> {\n class?: string;\n children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);\n}\n\nexport const Command: Component<CommandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const [search, setSearch] = createSignal(\"\");\n const [activeIndex, setActiveIndex] = createSignal(0);\n\n let containerRef: HTMLDivElement | undefined;\n let itemsList: HTMLElement[] = [];\n\n const registerItemIndex = (element: HTMLElement) => {\n if (!itemsList.includes(element)) {\n itemsList.push(element);\n }\n return itemsList.indexOf(element);\n };\n\n const onItemSelect = (index: number, action?: () => void) => {\n setActiveIndex(index);\n if (action) action();\n };\n\n const getVisibleItems = () => {\n if (!containerRef) return [];\n return Array.from(containerRef.querySelectorAll<HTMLElement>(\"[data-command-item]:not(.hidden)\"));\n };\n\n const updateActiveAttribute = (index: number) => {\n const visible = getVisibleItems();\n visible.forEach((el, idx) => {\n if (idx === index) {\n el.setAttribute(\"aria-selected\", \"true\");\n el.scrollIntoView({ block: \"nearest\" });\n } else {\n el.removeAttribute(\"aria-selected\");\n }\n });\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n const visible = getVisibleItems();\n if (visible.length === 0) return;\n\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n const next = (activeIndex() + 1) % visible.length;\n setActiveIndex(next);\n updateActiveAttribute(next);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n const prev = (activeIndex() - 1 + visible.length) % visible.length;\n setActiveIndex(prev);\n updateActiveAttribute(prev);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n const current = visible[activeIndex()];\n if (current) {\n current.click();\n }\n }\n };\n\n const ctx: CommandContextValue = {\n search,\n setSearch: (val) => {\n setSearch(val);\n setActiveIndex(0);\n },\n activeIndex,\n setActiveIndex: (fn) => setActiveIndex(fn),\n registerItemIndex,\n onItemSelect,\n };\n\n return (\n <CommandContext.Provider value={ctx}>\n <div\n ref={containerRef}\n onKeyDown={handleKeyDown}\n class={cn(\n \"flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md outline-none\",\n local.class\n )}\n tabIndex={0}\n {...rest}\n >\n {typeof local.children === \"function\" ? (local.children as any)(ctx) : local.children}\n </div>\n </CommandContext.Provider>\n );\n};\n\n/* --- Command Dialog (Modal) --- */\nexport interface CommandDialogProps {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n enableHotkey?: boolean;\n children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);\n class?: string;\n}\n\nexport const CommandDialog: Component<CommandDialogProps> = (props) => {\n const [internalOpen, setInternalOpen] = createSignal(false);\n\n const isOpen = () => (props.open !== undefined ? props.open : internalOpen());\n const setOpen = (val: boolean) => {\n if (props.open === undefined) setInternalOpen(val);\n if (typeof props.onOpenChange === \"function\") props.onOpenChange(val);\n };\n\n /* Listen for global Ctrl+K / Cmd+K hotkeys */\n createKeybindings(\n [\n {\n key: [\"meta+k\", \"ctrl+k\"],\n handler: () => setOpen(!isOpen()),\n preventDefault: true,\n },\n ],\n {\n enabled: () => props.enableHotkey !== false,\n }\n );\n\n return (\n <Dialog open={isOpen()} onOpenChange={setOpen}>\n <Dialog.Portal>\n <Dialog.Overlay class=\"fixed inset-0 z-50 bg-black/60 backdrop-blur-xs data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\" />\n <div class=\"fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4\">\n <Dialog.Content class=\"w-full max-w-xl rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl outline-none 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\">\n <Command class={props.class}>{props.children}</Command>\n </Dialog.Content>\n </div>\n </Dialog.Portal>\n </Dialog>\n );\n};\n\n/* --- Command Input --- */\nexport interface CommandInputProps\n extends JSX.InputHTMLAttributes<HTMLInputElement> {\n class?: string;\n}\n\nexport const CommandInput: Component<CommandInputProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"value\", \"onInput\"]);\n const { search, setSearch } = useCommand();\n\n return (\n <InputGroup class=\"border-0 border-b border-border rounded-none bg-transparent px-3 py-1 shadow-none focus-within:ring-0 focus-within:border-border\">\n <InputGroupAddon align=\"inline-start\">\n <Search class=\"w-4 h-4 text-muted-foreground\" />\n </InputGroupAddon>\n\n <InputGroupInput\n ref={(el) => setTimeout(() => el.focus(), 50)}\n value={search()}\n onInput={(e) => {\n setSearch(e.currentTarget.value);\n if (typeof local.onInput === \"function\") {\n local.onInput(e);\n }\n }}\n placeholder=\"Type a command or search...\"\n class=\"h-11 text-base sm:text-sm font-medium\"\n {...rest}\n />\n </InputGroup>\n );\n};\n\n/* --- Command List Container --- */\nexport interface CommandListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandList: Component<CommandListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"max-h-82.5 overflow-y-auto p-1.5 scrollbar-thin\", local.class)}\n {...rest}\n >\n <List>{local.children}</List>\n </div>\n );\n};\n\n/* --- Command Empty Block --- */\nexport interface CommandEmptyProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandEmpty: Component<CommandEmptyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { search } = useCommand();\n\n return (\n <Show when={search().trim().length > 0}>\n <div\n class={cn(\n \"py-8 text-center text-sm text-muted-foreground font-medium select-none\",\n local.class\n )}\n {...rest}\n >\n {local.children || `No results found for \"${search()}\".`}\n </div>\n </Show>\n );\n};\n\n/* --- Command Group --- */\nexport interface CommandGroupProps {\n heading: string;\n children?: JSX.Element;\n class?: string;\n}\n\nexport const CommandGroup: Component<CommandGroupProps> = (props) => {\n return (\n <ListGroup class={props.class}>\n <ListHeader title={props.heading} />\n {props.children}\n </ListGroup>\n );\n};\n\n/* --- Command Item --- */\nexport interface CommandItemProps extends ListItemProps {\n keywords?: string[];\n onSelect?: () => void;\n}\n\nexport const CommandItem: Component<CommandItemProps> = (props) => {\n let itemRef: HTMLDivElement | undefined;\n const [local, rest] = splitProps(props, [\n \"title\",\n \"subtitle\",\n \"keywords\",\n \"onSelect\",\n \"onClick\",\n \"class\",\n ]);\n const { search, activeIndex } = useCommand();\n\n /* Auto fuzzy-filter item based on search query */\n const matchesSearch = () => {\n const query = search().toLowerCase().trim();\n if (!query) return true;\n\n const titleMatch = local.title?.toLowerCase().includes(query);\n const subtitleMatch = local.subtitle?.toLowerCase().includes(query);\n const keywordMatch = local.keywords?.some((k) =>\n k.toLowerCase().includes(query)\n );\n\n return Boolean(titleMatch || subtitleMatch || keywordMatch);\n };\n\n const handleClick = (e: MouseEvent) => {\n if (typeof local.onSelect === \"function\") {\n local.onSelect();\n }\n if (typeof local.onClick === \"function\") {\n local.onClick(e as any);\n }\n };\n\n return (\n <Show when={matchesSearch()}>\n <ListItem\n ref={itemRef}\n data-command-item=\"true\"\n title={local.title}\n subtitle={local.subtitle}\n onClick={handleClick}\n class={cn(\n \"aria-selected:bg-accent aria-selected:text-accent-foreground cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground\",\n local.class\n )}\n {...rest}\n />\n </Show>\n );\n};\n\n/* --- Command Footer --- */\nexport interface CommandFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandFooter: Component<CommandFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between border-t border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground select-none\",\n local.class\n )}\n {...rest}\n >\n <div class=\"flex items-center gap-3\">\n <span class=\"flex items-center gap-1\">\n <KbdGroup>\n <Kbd size=\"sm\"><ArrowUp class=\"w-2.5 h-2.5\" /></Kbd>\n <Kbd size=\"sm\"><ArrowDown class=\"w-2.5 h-2.5\" /></Kbd>\n </KbdGroup>\n <span>Navigate</span>\n </span>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\"><CornerDownLeft class=\"w-2.5 h-2.5\" /></Kbd>\n <span>Select</span>\n </span>\n </div>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\">Esc</Kbd>\n <span>Close</span>\n </span>\n </div>\n );\n};",
|
|
22
22
|
"type": "registry:ui"
|
|
23
23
|
}
|
|
24
24
|
]
|
|
@@ -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
|
@@ -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",
|
|
@@ -95,6 +105,28 @@
|
|
|
95
105
|
"tailwind-merge"
|
|
96
106
|
]
|
|
97
107
|
},
|
|
108
|
+
{
|
|
109
|
+
"name": "collapsible",
|
|
110
|
+
"title": "Collapsible",
|
|
111
|
+
"description": "An interactive component that expands and collapses content panels with smooth height animations, built on Kobalte primitives.",
|
|
112
|
+
"type": "registry:ui",
|
|
113
|
+
"dependencies": [
|
|
114
|
+
"clsx",
|
|
115
|
+
"tailwind-merge",
|
|
116
|
+
"@kobalte/core"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "combobox",
|
|
121
|
+
"title": "Combobox",
|
|
122
|
+
"description": "Searchable autocomplete dropdown with single/multi-selection tags, avatars, group headers, and customizable clear controls.",
|
|
123
|
+
"type": "registry:ui",
|
|
124
|
+
"dependencies": [
|
|
125
|
+
"clsx",
|
|
126
|
+
"tailwind-merge",
|
|
127
|
+
"@kobalte/core"
|
|
128
|
+
]
|
|
129
|
+
},
|
|
98
130
|
{
|
|
99
131
|
"name": "command",
|
|
100
132
|
"title": "Command / Command Palette",
|
|
@@ -135,6 +167,17 @@
|
|
|
135
167
|
"@kobalte/core"
|
|
136
168
|
]
|
|
137
169
|
},
|
|
170
|
+
{
|
|
171
|
+
"name": "hover-card",
|
|
172
|
+
"title": "Hover Card",
|
|
173
|
+
"description": "Profile and link preview popover triggered on hover, built on Kobalte primitives.",
|
|
174
|
+
"type": "registry:ui",
|
|
175
|
+
"dependencies": [
|
|
176
|
+
"clsx",
|
|
177
|
+
"tailwind-merge",
|
|
178
|
+
"@kobalte/core"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
138
181
|
{
|
|
139
182
|
"name": "input-group",
|
|
140
183
|
"title": "Input Group",
|
|
@@ -204,6 +247,16 @@
|
|
|
204
247
|
"tailwind-merge"
|
|
205
248
|
]
|
|
206
249
|
},
|
|
250
|
+
{
|
|
251
|
+
"name": "pin-input",
|
|
252
|
+
"title": "Pin Input",
|
|
253
|
+
"description": "Interactive multi-slot PIN/OTP input component for SMS and 2FA authentication verification codes.",
|
|
254
|
+
"type": "registry:ui",
|
|
255
|
+
"dependencies": [
|
|
256
|
+
"clsx",
|
|
257
|
+
"tailwind-merge"
|
|
258
|
+
]
|
|
259
|
+
},
|
|
207
260
|
{
|
|
208
261
|
"name": "popover",
|
|
209
262
|
"title": "Popover",
|
|
@@ -281,6 +334,17 @@
|
|
|
281
334
|
"tailwind-merge"
|
|
282
335
|
]
|
|
283
336
|
},
|
|
337
|
+
{
|
|
338
|
+
"name": "slider",
|
|
339
|
+
"title": "Slider",
|
|
340
|
+
"description": "Numeric range selection slider supporting single/dual thumbs, custom steps, vertical orientation, and formatted value labels, built on Kobalte primitives.",
|
|
341
|
+
"type": "registry:ui",
|
|
342
|
+
"dependencies": [
|
|
343
|
+
"clsx",
|
|
344
|
+
"tailwind-merge",
|
|
345
|
+
"@kobalte/core"
|
|
346
|
+
]
|
|
347
|
+
},
|
|
284
348
|
{
|
|
285
349
|
"name": "switch",
|
|
286
350
|
"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,31 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface AspectRatioProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
ratio?: number;
|
|
6
|
+
class?: string;
|
|
7
|
+
children?: JSX.Element;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Nikala UI AspectRatio Component.
|
|
12
|
+
* Displays content within a specific aspect ratio (e.g. 16/9, 4/3, 1/1) using CSS aspect-ratio.
|
|
13
|
+
*/
|
|
14
|
+
export const AspectRatio: Component<AspectRatioProps> = (props) => {
|
|
15
|
+
const [local, rest] = splitProps(props, ["ratio", "class", "children", "style"]);
|
|
16
|
+
|
|
17
|
+
const computedRatio = () => (local.ratio !== undefined ? local.ratio : 16 / 9);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
class={cn("relative w-full overflow-hidden", local.class)}
|
|
22
|
+
style={{
|
|
23
|
+
"aspect-ratio": `${computedRatio()}`,
|
|
24
|
+
...(typeof local.style === "object" ? local.style : {}),
|
|
25
|
+
}}
|
|
26
|
+
{...rest}
|
|
27
|
+
>
|
|
28
|
+
{local.children}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { splitProps, type JSX, type ValidComponent } from "solid-js";
|
|
2
|
+
import * as CollapsiblePrimitive from "@kobalte/core/collapsible";
|
|
3
|
+
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
export type CollapsibleRootProps<T extends ValidComponent = "div"> =
|
|
7
|
+
CollapsiblePrimitive.CollapsibleRootProps<T> & {
|
|
8
|
+
class?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Root Collapsible component built on Kobalte primitives.
|
|
13
|
+
*/
|
|
14
|
+
export const Collapsible = <T extends ValidComponent = "div">(
|
|
15
|
+
props: PolymorphicProps<T, CollapsibleRootProps<T>>
|
|
16
|
+
) => {
|
|
17
|
+
const [local, rest] = splitProps(props as CollapsibleRootProps, ["class"]);
|
|
18
|
+
return (
|
|
19
|
+
<CollapsiblePrimitive.Root
|
|
20
|
+
class={cn("w-full", local.class)}
|
|
21
|
+
{...(rest as any)}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type CollapsibleTriggerProps<T extends ValidComponent = "button"> =
|
|
27
|
+
CollapsiblePrimitive.CollapsibleTriggerProps<T> & {
|
|
28
|
+
class?: string;
|
|
29
|
+
children?: JSX.Element;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Trigger element that toggles the Collapsible open/closed state.
|
|
34
|
+
*/
|
|
35
|
+
export const CollapsibleTrigger = <T extends ValidComponent = "button">(
|
|
36
|
+
props: PolymorphicProps<T, CollapsibleTriggerProps<T>>
|
|
37
|
+
) => {
|
|
38
|
+
const [local, rest] = splitProps(props as CollapsibleTriggerProps, ["class", "children"]);
|
|
39
|
+
return (
|
|
40
|
+
<CollapsiblePrimitive.Trigger
|
|
41
|
+
class={cn("flex w-full items-center justify-between cursor-pointer", local.class)}
|
|
42
|
+
{...rest}
|
|
43
|
+
>
|
|
44
|
+
{local.children}
|
|
45
|
+
</CollapsiblePrimitive.Trigger>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type CollapsibleContentProps<T extends ValidComponent = "div"> =
|
|
50
|
+
CollapsiblePrimitive.CollapsibleContentProps<T> & {
|
|
51
|
+
class?: string;
|
|
52
|
+
children?: JSX.Element;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Collapsible content panel revealed when opened.
|
|
57
|
+
*/
|
|
58
|
+
export const CollapsibleContent = <T extends ValidComponent = "div">(
|
|
59
|
+
props: PolymorphicProps<T, CollapsibleContentProps<T>>
|
|
60
|
+
) => {
|
|
61
|
+
const [local, rest] = splitProps(props as CollapsibleContentProps, ["class", "children"]);
|
|
62
|
+
return (
|
|
63
|
+
<CollapsiblePrimitive.Content
|
|
64
|
+
class={cn(
|
|
65
|
+
"overflow-hidden transition-all data-expanded:animate-collapsible-down data-closed:animate-collapsible-up",
|
|
66
|
+
local.class
|
|
67
|
+
)}
|
|
68
|
+
{...rest}
|
|
69
|
+
>
|
|
70
|
+
{local.children}
|
|
71
|
+
</CollapsiblePrimitive.Content>
|
|
72
|
+
);
|
|
73
|
+
};
|