@nikala-ui/core 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nikala-ui/core",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Core component definitions, design tokens, and registry for Nikala UI",
5
5
  "type": "module",
6
6
  "private": false,
@@ -29,5 +29,9 @@
29
29
  "picocolors": "^1.1.1",
30
30
  "solid-js": "^1.9.14",
31
31
  "tailwind-merge": "^3.6.0"
32
+ },
33
+ "dependencies": {
34
+ "@nikala-ui/hooks": "workspace:*",
35
+ "lucide-solid": "^1.28.0"
32
36
  }
33
37
  }
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "ui/accordion.tsx",
14
- "content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as AccordionPrimitive from \"@kobalte/core/accordion\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cn } from \"@/lib/cn\";\n\nexport type AccordionRootProps<T extends ValidComponent = \"div\"> = Omit<\n AccordionPrimitive.AccordionRootProps<T>,\n \"value\" | \"defaultValue\"\n> & {\n /** Accordion mode: \"single\" allows one open item, \"multiple\" allows many */\n type?: \"single\" | \"multiple\";\n /** Controlled value (single string or array of strings) */\n value?: string | string[];\n /** Default initial value (single string or array of strings) */\n defaultValue?: string | string[];\n class?: string;\n};\n\n/**\n * Root Accordion component built on top of Kobalte headless primitives.\n */\nexport const Accordion = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, AccordionRootProps<T>>\n) => {\n const [local, rest] = splitProps(props as AccordionRootProps, [\n \"class\",\n \"type\",\n \"multiple\",\n ]);\n\n // Support both `type=\"multiple\"` and `multiple={true}`\n const isMultiple = () => local.multiple ?? local.type === \"multiple\";\n\n return (\n <AccordionPrimitive.Root\n multiple={isMultiple()}\n class={cn(\"w-full divide-y divide-border\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type AccordionItemProps<T extends ValidComponent = \"div\"> =\n AccordionPrimitive.AccordionItemProps<T> & {\n class?: string;\n value: string;\n };\n\n/**\n * Individual Accordion section item wrapper.\n */\nexport const AccordionItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, AccordionItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as AccordionItemProps, [\"class\"]);\n\n return (\n <AccordionPrimitive.Item\n class={cn(\"border-b border-border\", local.class)}\n {...rest}\n />\n );\n};\n\nexport type AccordionTriggerProps<T extends ValidComponent = \"button\"> =\n AccordionPrimitive.AccordionTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Header trigger button toggling the expansion of an AccordionItem.\n */\nexport const AccordionTrigger = <T extends ValidComponent = \"button\">(\n props: PolymorphicProps<T, AccordionTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as AccordionTriggerProps, [\"class\", \"children\"]);\n\n return (\n <AccordionPrimitive.Header class=\"flex\">\n <AccordionPrimitive.Trigger\n class={cn(\n \"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-expanded]>svg]:rotate-180 cursor-pointer text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n <svg\n class=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </svg>\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n );\n};\n\nexport type AccordionContentProps<T extends ValidComponent = \"div\"> =\n AccordionPrimitive.AccordionContentProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Collapsible content panel revealed when the associated AccordionItem is open.\n */\nexport const AccordionContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, AccordionContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as AccordionContentProps, [\"class\", \"children\"]);\n\n return (\n <AccordionPrimitive.Content\n class={cn(\n \"overflow-hidden text-sm text-muted-foreground transition-all\",\n local.class\n )}\n {...rest}\n >\n <div class=\"pb-4 pt-0\">{local.children}</div>\n </AccordionPrimitive.Content>\n );\n};",
14
+ "content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as AccordionPrimitive from \"@kobalte/core/accordion\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cn } from \"@/lib/cn\";\n\nexport type AccordionRootProps<T extends ValidComponent = \"div\"> = Omit<\n AccordionPrimitive.AccordionRootProps<T>,\n \"value\" | \"defaultValue\"\n> & {\n /** Accordion mode: \"single\" allows one open item, \"multiple\" allows many */\n type?: \"single\" | \"multiple\";\n /** Controlled value (single string or array of strings) */\n value?: string | string[];\n /** Default initial value (single string or array of strings) */\n defaultValue?: string | string[];\n class?: string;\n};\n\n/**\n * Root Accordion component built on top of Kobalte headless primitives.\n */\nexport const Accordion = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, AccordionRootProps<T>>\n) => {\n const [local, rest] = splitProps(props as AccordionRootProps, [\n \"class\",\n \"type\",\n \"multiple\",\n \"value\",\n \"defaultValue\",\n ]);\n\n // Support both `type=\"multiple\"` and `multiple={true}`\n const isMultiple = () => local.multiple ?? local.type === \"multiple\";\n\n return (\n <AccordionPrimitive.Root\n multiple={isMultiple()}\n value={local.value}\n defaultValue={local.defaultValue}\n class={cn(\"w-full divide-y divide-border\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type AccordionItemProps<T extends ValidComponent = \"div\"> =\n AccordionPrimitive.AccordionItemProps<T> & {\n class?: string;\n value: string;\n };\n\n/**\n * Individual Accordion section item wrapper.\n */\nexport const AccordionItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, AccordionItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as AccordionItemProps, [\"class\"]);\n\n return (\n <AccordionPrimitive.Item\n class={cn(\"border-b border-border\", local.class)}\n {...rest}\n />\n );\n};\n\nexport type AccordionTriggerProps<T extends ValidComponent = \"button\"> =\n AccordionPrimitive.AccordionTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Header trigger button toggling the expansion of an AccordionItem.\n */\nexport const AccordionTrigger = <T extends ValidComponent = \"button\">(\n props: PolymorphicProps<T, AccordionTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as AccordionTriggerProps, [\"class\", \"children\"]);\n\n return (\n <AccordionPrimitive.Header class=\"flex\">\n <AccordionPrimitive.Trigger\n class={cn(\n \"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-expanded]>svg]:rotate-180 cursor-pointer text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n <svg\n class=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </svg>\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n );\n};\n\nexport type AccordionContentProps<T extends ValidComponent = \"div\"> =\n AccordionPrimitive.AccordionContentProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Collapsible content panel revealed when the associated AccordionItem is open.\n */\nexport const AccordionContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, AccordionContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as AccordionContentProps, [\"class\", \"children\"]);\n\n return (\n <AccordionPrimitive.Content\n class={cn(\n \"overflow-hidden text-sm text-muted-foreground transition-all\",\n local.class\n )}\n {...rest}\n >\n <div class=\"pb-4 pt-0\">{local.children}</div>\n </AccordionPrimitive.Content>\n );\n};",
15
15
  "type": "registry:ui"
16
16
  }
17
17
  ]
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "ui/checkbox.tsx",
13
- "content": "import { createSignal, splitProps, Show, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface CheckboxProps\n extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\"> {\n /** Controlled checked state */\n checked?: boolean;\n /** Uncontrolled default checked state */\n defaultChecked?: boolean;\n /** Event handler triggered when checked state changes */\n onChange?: (checked: boolean) => void;\n class?: string;\n}\n\n/**\n * Nikala UI Checkbox component built for SolidJS with Tailwind CSS v4 styling.\n */\nexport const Checkbox: Component<CheckboxProps> = (props) => {\n // Use splitProps to preserve SolidJS reactivity\n const [local, rest] = splitProps(props, [\n \"checked\",\n \"defaultChecked\",\n \"onChange\",\n \"disabled\",\n \"class\",\n \"onClick\",\n ]);\n\n // Internal state for uncontrolled mode\n const [internalChecked, setInternalChecked] = createSignal(\n local.defaultChecked ?? false\n );\n\n // Computed checked state\n const isChecked = () =>\n local.checked !== undefined ? local.checked : internalChecked();\n\n const toggle = (\n e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }\n ) => {\n if (local.disabled) return;\n\n const nextState = !isChecked();\n if (local.checked === undefined) {\n setInternalChecked(nextState);\n }\n\n if (typeof local.onChange === \"function\") {\n local.onChange(nextState);\n }\n\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n };\n\n return (\n <button\n type=\"button\"\n role=\"checkbox\"\n aria-checked={isChecked()}\n data-state={isChecked() ? \"checked\" : \"unchecked\"}\n disabled={local.disabled}\n onClick={toggle}\n class={cn(\n \"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground flex items-center justify-center transition-colors\",\n local.class\n )}\n {...rest}\n >\n <Show when={isChecked()}>\n <svg\n class=\"h-3.5 w-3.5 fill-none stroke-current stroke-[3]\"\n viewBox=\"0 0 24 24\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </Show>\n </button>\n );\n};",
13
+ "content": "import { splitProps, Show, type Component, type JSX } from \"solid-js\";\nimport { createControllableSignal } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface CheckboxProps\n extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\"> {\n /** Controlled checked state */\n checked?: boolean;\n /** Uncontrolled default checked state */\n defaultChecked?: boolean;\n /** Event handler triggered when checked state changes */\n onChange?: (checked: boolean) => void;\n class?: string;\n}\n\n/**\n * Nikala UI Checkbox component built for SolidJS with Tailwind CSS v4 styling.\n */\nexport const Checkbox: Component<CheckboxProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"checked\",\n \"defaultChecked\",\n \"onChange\",\n \"disabled\",\n \"class\",\n \"onClick\",\n ]);\n\n const [isChecked, setIsChecked] = createControllableSignal({\n value: () => local.checked,\n defaultValue: local.defaultChecked ?? false,\n onChange: (val) => local.onChange?.(val),\n });\n\n const toggle = (\n e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }\n ) => {\n if (local.disabled) return;\n setIsChecked(!isChecked());\n\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n };\n\n return (\n <button\n type=\"button\"\n role=\"checkbox\"\n aria-checked={Boolean(isChecked())}\n data-state={isChecked() ? \"checked\" : \"unchecked\"}\n disabled={local.disabled}\n onClick={toggle}\n class={cn(\n \"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground flex items-center justify-center transition-colors\",\n local.class\n )}\n {...rest}\n >\n <Show when={isChecked()}>\n <svg\n class=\"h-3.5 w-3.5 fill-none stroke-current stroke-[3]\"\n viewBox=\"0 0 24 24\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </Show>\n </button>\n );\n};",
14
14
  "type": "registry:ui"
15
15
  }
16
16
  ]
@@ -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 onMount,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\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-xl 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 onMount(() => {\n if (props.enableHotkey !== false) {\n const handleKeyDown = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"k\") {\n e.preventDefault();\n setOpen(!isOpen());\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown);\n onCleanup(() => window.removeEventListener(\"keydown\", handleKeyDown));\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-xl 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-[330px] 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 onMount,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\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 onMount(() => {\n if (props.enableHotkey !== false) {\n const handleKeyDown = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"k\") {\n e.preventDefault();\n setOpen(!isOpen());\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown);\n onCleanup(() => window.removeEventListener(\"keydown\", handleKeyDown));\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-[330px] 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};",
22
22
  "type": "registry:ui"
23
23
  }
24
24
  ]
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "ui/dropdown-menu.tsx",
14
- "content": "import { splitProps, type Component, type JSX, type ValidComponent } from \"solid-js\";\nimport * as DropdownMenuPrimitive from \"@kobalte/core/dropdown-menu\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cn } from \"@/lib/cn\";\n\nexport type DropdownMenuRootProps = Omit<\n DropdownMenuPrimitive.DropdownMenuRootProps,\n \"placement\"\n> & {\n placement?:\n | \"top\"\n | \"top-start\"\n | \"top-end\"\n | \"right\"\n | \"right-start\"\n | \"right-end\"\n | \"bottom\"\n | \"bottom-start\"\n | \"bottom-end\"\n | \"left\"\n | \"left-start\"\n | \"left-end\";\n};\n\nexport const DropdownMenu: Component<DropdownMenuRootProps> = (props) => {\n return <DropdownMenuPrimitive.Root {...props} />;\n};\n\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\nexport const DropdownMenuSub = DropdownMenuPrimitive.Sub;\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nexport type DropdownMenuSubTriggerProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuSubTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n inset?: boolean;\n };\n\nexport const DropdownMenuSubTrigger = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuSubTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSubTriggerProps, [\"class\", \"children\", \"inset\"]);\n\n return (\n <DropdownMenuPrimitive.SubTrigger\n class={cn(\n \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent text-foreground\",\n local.inset && \"pl-8\",\n local.class\n )}\n {...(rest as any)}\n >\n {local.children}\n <svg class=\"ml-auto h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </DropdownMenuPrimitive.SubTrigger>\n );\n};\n\nexport type DropdownMenuSubContentProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuSubContentProps<T> & {\n class?: string;\n };\n\nexport const DropdownMenuSubContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuSubContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSubContentProps, [\"class\"]);\n\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.SubContent\n class={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\",\n local.class\n )}\n {...(rest as any)}\n />\n </DropdownMenuPrimitive.Portal>\n );\n};\n\nexport type DropdownMenuContentProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuContentProps<T> & {\n class?: string;\n };\n\nexport const DropdownMenuContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuContentProps, [\"class\"]);\n\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n class={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\",\n local.class\n )}\n {...(rest as any)}\n />\n </DropdownMenuPrimitive.Portal>\n );\n};\n\nexport type DropdownMenuItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuItemProps<T> & {\n class?: string;\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n };\n\nexport const DropdownMenuItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuItemProps, [\n \"class\",\n \"inset\",\n \"variant\",\n ]);\n\n return (\n <DropdownMenuPrimitive.Item\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n local.inset && \"pl-8\",\n local.variant === \"destructive\" &&\n \"text-destructive focus:bg-destructive/15 focus:text-destructive\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type DropdownMenuCheckboxItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuCheckboxItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const DropdownMenuCheckboxItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuCheckboxItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuCheckboxItemProps, [\"class\", \"children\"]);\n\n return (\n <DropdownMenuPrimitive.CheckboxItem\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n local.class\n )}\n {...(rest as any)}\n >\n <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\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 </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n};\n\nexport type DropdownMenuRadioItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuRadioItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const DropdownMenuRadioItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuRadioItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuRadioItemProps, [\"class\", \"children\"]);\n\n return (\n <DropdownMenuPrimitive.RadioItem\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n local.class\n )}\n {...(rest as any)}\n >\n <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <span class=\"h-2 w-2 rounded-full bg-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </DropdownMenuPrimitive.RadioItem>\n );\n};\n\nexport interface DropdownMenuLabelProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n inset?: boolean;\n}\n\nexport const DropdownMenuLabel: Component<DropdownMenuLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"inset\"]);\n\n return (\n <div\n class={cn(\n \"px-2 py-1.5 text-sm font-semibold text-foreground\",\n local.inset && \"pl-8\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport type DropdownMenuSeparatorProps<T extends ValidComponent = \"hr\"> =\n DropdownMenuPrimitive.DropdownMenuSeparatorProps<T> & {\n class?: string;\n };\n\nexport const DropdownMenuSeparator = <T extends ValidComponent = \"hr\">(\n props: PolymorphicProps<T, DropdownMenuSeparatorProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSeparatorProps, [\"class\"]);\n\n return (\n <DropdownMenuPrimitive.Separator\n class={cn(\"-mx-1 my-1 h-px bg-border\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport interface DropdownMenuShortcutProps extends JSX.HTMLAttributes<HTMLSpanElement> {\n class?: string;\n}\n\nexport const DropdownMenuShortcut: Component<DropdownMenuShortcutProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <span\n class={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};",
14
+ "content": "import { splitProps, type Component, type JSX, type ValidComponent } from \"solid-js\";\nimport * as DropdownMenuPrimitive from \"@kobalte/core/dropdown-menu\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport type DropdownMenuRootProps = Omit<\n DropdownMenuPrimitive.DropdownMenuRootProps,\n \"placement\"\n> & {\n placement?:\n | \"top\"\n | \"top-start\"\n | \"top-end\"\n | \"right\"\n | \"right-start\"\n | \"right-end\"\n | \"bottom\"\n | \"bottom-start\"\n | \"bottom-end\"\n | \"left\"\n | \"left-start\"\n | \"left-end\";\n};\n\nexport const DropdownMenu: Component<DropdownMenuRootProps> = (props) => {\n return <DropdownMenuPrimitive.Root {...props} />;\n};\n\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\nexport const DropdownMenuSub = DropdownMenuPrimitive.Sub;\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nexport type DropdownMenuSubTriggerProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuSubTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n inset?: boolean;\n };\n\nexport const DropdownMenuSubTrigger = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuSubTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSubTriggerProps, [\"class\", \"children\", \"inset\"]);\n\n return (\n <DropdownMenuPrimitive.SubTrigger\n class={cn(\n \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent text-foreground\",\n local.inset && \"pl-8\",\n local.class\n )}\n {...(rest as any)}\n >\n {local.children}\n <svg class=\"ml-auto h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </DropdownMenuPrimitive.SubTrigger>\n );\n};\n\nexport type DropdownMenuSubContentProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuSubContentProps<T> & {\n class?: string;\n };\n\nexport const DropdownMenuSubContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuSubContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSubContentProps, [\"class\"]);\n\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.SubContent\n class={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\",\n local.class\n )}\n {...(rest as any)}\n />\n </DropdownMenuPrimitive.Portal>\n );\n};\n\nexport type DropdownMenuContentProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuContentProps<T> & {\n class?: string;\n };\n\nexport const DropdownMenuContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuContentProps, [\"class\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof (props as any).onInteractOutside === \"function\") {\n (props as any).onInteractOutside(e);\n }\n },\n });\n\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={(el) => {\n contentRef = el;\n if (typeof (props as any).ref === \"function\") (props as any).ref(el);\n }}\n class={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\",\n local.class\n )}\n {...(rest as any)}\n />\n </DropdownMenuPrimitive.Portal>\n );\n};\n\nexport type DropdownMenuItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuItemProps<T> & {\n class?: string;\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n };\n\nexport const DropdownMenuItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuItemProps, [\n \"class\",\n \"inset\",\n \"variant\",\n ]);\n\n return (\n <DropdownMenuPrimitive.Item\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n local.inset && \"pl-8\",\n local.variant === \"destructive\" &&\n \"text-destructive focus:bg-destructive/15 focus:text-destructive\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type DropdownMenuCheckboxItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuCheckboxItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const DropdownMenuCheckboxItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuCheckboxItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuCheckboxItemProps, [\"class\", \"children\"]);\n\n return (\n <DropdownMenuPrimitive.CheckboxItem\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n local.class\n )}\n {...(rest as any)}\n >\n <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\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 </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n};\n\nexport type DropdownMenuRadioItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuRadioItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const DropdownMenuRadioItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuRadioItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuRadioItemProps, [\"class\", \"children\"]);\n\n return (\n <DropdownMenuPrimitive.RadioItem\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n local.class\n )}\n {...(rest as any)}\n >\n <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <span class=\"h-2 w-2 rounded-full bg-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </DropdownMenuPrimitive.RadioItem>\n );\n};\n\nexport interface DropdownMenuLabelProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n inset?: boolean;\n}\n\nexport const DropdownMenuLabel: Component<DropdownMenuLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"inset\"]);\n\n return (\n <div\n class={cn(\n \"px-2 py-1.5 text-sm font-semibold text-foreground\",\n local.inset && \"pl-8\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport type DropdownMenuSeparatorProps<T extends ValidComponent = \"hr\"> =\n DropdownMenuPrimitive.DropdownMenuSeparatorProps<T> & {\n class?: string;\n };\n\nexport const DropdownMenuSeparator = <T extends ValidComponent = \"hr\">(\n props: PolymorphicProps<T, DropdownMenuSeparatorProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSeparatorProps, [\"class\"]);\n\n return (\n <DropdownMenuPrimitive.Separator\n class={cn(\"-mx-1 my-1 h-px bg-border\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport interface DropdownMenuShortcutProps extends JSX.HTMLAttributes<HTMLSpanElement> {\n class?: string;\n}\n\nexport const DropdownMenuShortcut: Component<DropdownMenuShortcutProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <span\n class={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};",
15
15
  "type": "registry:ui"
16
16
  }
17
17
  ]
@@ -194,6 +194,29 @@
194
194
  "@kobalte/core"
195
195
  ]
196
196
  },
197
+ {
198
+ "name": "popover",
199
+ "title": "Popover",
200
+ "description": "Displays rich content in a portal layer triggered by a button, built on Kobalte primitives.",
201
+ "type": "registry:ui",
202
+ "dependencies": [
203
+ "clsx",
204
+ "tailwind-merge",
205
+ "@kobalte/core",
206
+ "lucide-solid"
207
+ ]
208
+ },
209
+ {
210
+ "name": "progress",
211
+ "title": "Progress",
212
+ "description": "Displays an indicator showing the completion progress of a task or media playback, built on Kobalte primitives.",
213
+ "type": "registry:ui",
214
+ "dependencies": [
215
+ "clsx",
216
+ "tailwind-merge",
217
+ "@kobalte/core"
218
+ ]
219
+ },
197
220
  {
198
221
  "name": "radio-group",
199
222
  "title": "Radio Group",
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "popover",
3
+ "title": "Popover",
4
+ "description": "Displays rich content in a portal layer triggered by a button, built on Kobalte primitives.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "@kobalte/core",
10
+ "lucide-solid"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "ui/popover.tsx",
15
+ "content": "import { splitProps, type Component, type ComponentProps } from \"solid-js\";\nimport { Popover as KobaltePopover } from \"@kobalte/core/popover\";\nimport { createClickOutside } from \"@nikala-ui/hooks\";\nimport { X } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Popover = KobaltePopover;\n\nexport const PopoverTrigger = KobaltePopover.Trigger;\n\nexport const PopoverArrow: Component<ComponentProps<typeof KobaltePopover.Arrow>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobaltePopover.Arrow\n class={cn(\"fill-popover stroke-border\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const PopoverContent: Component<ComponentProps<typeof KobaltePopover.Content>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\", \"onInteractOutside\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof local.onInteractOutside === \"function\") {\n local.onInteractOutside(e as any);\n }\n },\n });\n\n return (\n <KobaltePopover.Portal>\n <KobaltePopover.Content\n ref={(el) => {\n contentRef = el;\n if (typeof (props as any).ref === \"function\") (props as any).ref(el);\n }}\n class={cn(\n \"z-50 w-72 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-md 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 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}\n >\n {local.children}\n </KobaltePopover.Content>\n </KobaltePopover.Portal>\n );\n};\n\nexport const PopoverTitle: Component<ComponentProps<typeof KobaltePopover.Title>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobaltePopover.Title\n class={cn(\"text-sm font-semibold text-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const PopoverDescription: Component<ComponentProps<typeof KobaltePopover.Description>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobaltePopover.Description\n class={cn(\"text-sm text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const PopoverCloseButton: Component<ComponentProps<typeof KobaltePopover.CloseButton>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <KobaltePopover.CloseButton\n class={cn(\n \"absolute right-2 top-2 rounded-md p-1 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none\",\n local.class\n )}\n {...rest}\n >\n {local.children || <X class=\"h-4 w-4 text-muted-foreground\" />}\n </KobaltePopover.CloseButton>\n );\n};",
16
+ "type": "registry:ui"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "progress",
3
+ "title": "Progress",
4
+ "description": "Displays an indicator showing the completion progress of a task or media playback, 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/progress.tsx",
14
+ "content": "import { splitProps, Show, type Component, type JSX } from \"solid-js\";\nimport { Progress as KobalteProgress } from \"@kobalte/core/progress\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ProgressProps\n extends Omit<\n JSX.HTMLAttributes<HTMLDivElement>,\n \"value\" | \"aria-valuenow\" | \"aria-valuemin\" | \"aria-valuemax\"\n > {\n /** The current numeric progress value. */\n value?: number;\n /** Minimum progress value. Defaults to 0. */\n minValue?: number;\n /** Maximum progress value. Defaults to 100. */\n maxValue?: number;\n /** Label text for accessibility and screen readers. */\n getValueLabel?: (params: { value: number; max: number }) => string;\n /** Custom label element to display above or beside progress bar. */\n label?: JSX.Element;\n /** Optional custom class for root container. */\n class?: string;\n /** Optional custom class for indicator fill bar. */\n indicatorClass?: string;\n}\n\n/**\n * Nikala UI Progress component for showing task completion status or media timeline positions.\n */\nexport const Progress: Component<ProgressProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"minValue\",\n \"maxValue\",\n \"getValueLabel\",\n \"label\",\n \"class\",\n \"indicatorClass\",\n ]);\n\n return (\n <KobalteProgress\n value={local.value ?? 0}\n minValue={local.minValue ?? 0}\n maxValue={local.maxValue ?? 100}\n getValueLabel={local.getValueLabel}\n class={cn(\"flex w-full flex-col gap-1.5\", local.class)}\n {...rest}\n >\n <Show when={local.label}>\n <div class=\"flex justify-between text-xs font-medium text-muted-foreground\">\n <KobalteProgress.Label>{local.label}</KobalteProgress.Label>\n <KobalteProgress.ValueLabel class=\"font-mono text-foreground\" />\n </div>\n </Show>\n\n <KobalteProgress.Track class=\"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\">\n <KobalteProgress.Fill\n class={cn(\n \"h-full w-[var(--kb-progress-fill-width)] bg-primary transition-all duration-300 ease-in-out\",\n local.indicatorClass\n )}\n />\n </KobalteProgress.Track>\n </KobalteProgress>\n );\n};\n",
15
+ "type": "registry:ui"
16
+ }
17
+ ]
18
+ }
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "ui/select.tsx",
14
- "content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as SelectPrimitive from \"@kobalte/core/select\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cn } from \"@/lib/cn\";\n\nexport type SelectRootProps<Option = any, OptGroup = any, T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectRootProps<Option, OptGroup, T> & {\n class?: string;\n };\n\n/**\n * Root Select component built on top of Kobalte headless primitives.\n */\nexport const Select = <Option = any, OptGroup = any, T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SelectRootProps<Option, OptGroup, T>>\n) => {\n const [local, rest] = splitProps(props as SelectRootProps, [\"class\"]);\n\n return <SelectPrimitive.Root class={cn(\"relative w-full\", local.class)} {...(rest as any)} />;\n};\n\nexport type SelectTriggerProps<T extends ValidComponent = \"button\"> =\n SelectPrimitive.SelectTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Trigger button opening the Select options list.\n */\nexport const SelectTrigger = <T extends ValidComponent = \"button\">(\n props: PolymorphicProps<T, SelectTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectTriggerProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Trigger\n class={cn(\n \"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n <SelectPrimitive.Icon\n as=\"svg\"\n class=\"h-4 w-4 opacity-50 transition-transform\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n};\n\nexport type SelectValueProps<Option, T extends ValidComponent = \"span\"> =\n SelectPrimitive.SelectValueProps<Option, T> & {\n class?: string;\n };\n\n/**\n * Renders the currently selected option text or placeholder.\n */\nexport const SelectValue = <Option = any, T extends ValidComponent = \"span\">(\n props: PolymorphicProps<T, SelectValueProps<Option, T>>\n) => {\n const [local, rest] = splitProps(props as SelectValueProps<Option>, [\"class\"]);\n\n return (\n <SelectPrimitive.Value\n class={cn(\"block truncate\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type SelectContentProps<T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectContentProps<T> & {\n class?: string;\n };\n\n/**\n * Portaled overlay container rendering the listbox options.\n */\nexport const SelectContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SelectContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectContentProps, [\"class\"]);\n\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.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\",\n local.class\n )}\n {...rest}\n >\n <SelectPrimitive.Listbox class=\"p-1 outline-none\" />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n};\n\nexport type SelectItemProps<T extends ValidComponent = \"li\"> =\n SelectPrimitive.SelectItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Individual option item choice inside SelectContent.\n */\nexport const SelectItem = <T extends ValidComponent = \"li\">(\n props: PolymorphicProps<T, SelectItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectItemProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Item\n class={cn(\n \"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n local.class\n )}\n {...rest}\n >\n <SelectPrimitive.ItemIndicator class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\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 </SelectPrimitive.ItemIndicator>\n <SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>\n </SelectPrimitive.Item>\n );\n};",
14
+ "content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as SelectPrimitive from \"@kobalte/core/select\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport type SelectRootProps<Option = any, OptGroup = any, T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectRootProps<Option, OptGroup, T> & {\n class?: string;\n };\n\n/**\n * Root Select component built on top of Kobalte headless primitives.\n */\nexport const Select = <Option = any, OptGroup = any, T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SelectRootProps<Option, OptGroup, T>>\n) => {\n const [local, rest] = splitProps(props as SelectRootProps, [\"class\"]);\n\n return <SelectPrimitive.Root class={cn(\"relative w-full\", local.class)} {...(rest as any)} />;\n};\n\nexport type SelectTriggerProps<T extends ValidComponent = \"button\"> =\n SelectPrimitive.SelectTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Trigger button opening the Select options list.\n */\nexport const SelectTrigger = <T extends ValidComponent = \"button\">(\n props: PolymorphicProps<T, SelectTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectTriggerProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Trigger\n class={cn(\n \"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n <SelectPrimitive.Icon\n as=\"svg\"\n class=\"h-4 w-4 opacity-50 transition-transform\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n};\n\nexport type SelectValueProps<Option, T extends ValidComponent = \"span\"> =\n SelectPrimitive.SelectValueProps<Option, T> & {\n class?: string;\n };\n\n/**\n * Renders the currently selected option text or placeholder.\n */\nexport const SelectValue = <Option = any, T extends ValidComponent = \"span\">(\n props: PolymorphicProps<T, SelectValueProps<Option, T>>\n) => {\n const [local, rest] = splitProps(props as SelectValueProps<Option>, [\"class\"]);\n\n return (\n <SelectPrimitive.Value\n class={cn(\"block truncate\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type SelectContentProps<T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectContentProps<T> & {\n class?: string;\n };\n\n/**\n * Portaled overlay container rendering the listbox options.\n */\nexport const SelectContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SelectContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectContentProps, [\"class\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof (props as any).onInteractOutside === \"function\") {\n (props as any).onInteractOutside(e);\n }\n },\n });\n\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={(el) => {\n contentRef = el;\n if (typeof (props as any).ref === \"function\") (props as any).ref(el);\n }}\n class={cn(\n \"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80\",\n local.class\n )}\n {...rest}\n >\n <SelectPrimitive.Listbox class=\"p-1 outline-none\" />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n};\n\nexport type SelectItemProps<T extends ValidComponent = \"li\"> =\n SelectPrimitive.SelectItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Individual option item choice inside SelectContent.\n */\nexport const SelectItem = <T extends ValidComponent = \"li\">(\n props: PolymorphicProps<T, SelectItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectItemProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Item\n class={cn(\n \"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n local.class\n )}\n {...rest}\n >\n <SelectPrimitive.ItemIndicator class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\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 </SelectPrimitive.ItemIndicator>\n <SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>\n </SelectPrimitive.Item>\n );\n};",
15
15
  "type": "registry:ui"
16
16
  }
17
17
  ]
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "ui/switch.tsx",
13
- "content": "import { createSignal, splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface SwitchProps\n extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\"> {\n /** Controlled checked state */\n checked?: boolean;\n /** Uncontrolled default checked state */\n defaultChecked?: boolean;\n /** Event handler triggered when toggle state changes */\n onChange?: (checked: boolean) => void;\n class?: string;\n}\n\n/**\n * Nikala UI Switch component for boolean toggle inputs built for SolidJS with Tailwind CSS v4 styling.\n */\nexport const Switch: Component<SwitchProps> = (props) => {\n // Use splitProps to preserve SolidJS reactivity\n const [local, rest] = splitProps(props, [\n \"checked\",\n \"defaultChecked\",\n \"onChange\",\n \"disabled\",\n \"class\",\n \"onClick\",\n ]);\n\n // Internal state for uncontrolled mode\n const [internalChecked, setInternalChecked] = createSignal(\n local.defaultChecked ?? false\n );\n\n // Computed checked state supporting both controlled and uncontrolled modes\n const isChecked = () =>\n local.checked !== undefined ? local.checked : internalChecked();\n\n const toggle = (\n e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }\n ) => {\n if (local.disabled) return;\n\n const nextState = !isChecked();\n if (local.checked === undefined) {\n setInternalChecked(nextState);\n }\n\n if (typeof local.onChange === \"function\") {\n local.onChange(nextState);\n }\n\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n };\n\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={isChecked()}\n data-state={isChecked() ? \"checked\" : \"unchecked\"}\n disabled={local.disabled}\n onClick={toggle}\n class={cn(\n \"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-lg border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n local.class\n )}\n {...rest}\n >\n <span\n data-state={isChecked() ? \"checked\" : \"unchecked\"}\n class={cn(\n \"pointer-events-none block h-4 w-4 rounded-sm bg-primary-foreground shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0\"\n )}\n />\n </button>\n );\n};",
13
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { createControllableSignal } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface SwitchProps\n extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\"> {\n /** Controlled checked state */\n checked?: boolean;\n /** Uncontrolled default checked state */\n defaultChecked?: boolean;\n /** Event handler triggered when toggle state changes */\n onChange?: (checked: boolean) => void;\n class?: string;\n}\n\n/**\n * Nikala UI Switch component for boolean toggle inputs built for SolidJS with Tailwind CSS v4 styling.\n */\nexport const Switch: Component<SwitchProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"checked\",\n \"defaultChecked\",\n \"onChange\",\n \"disabled\",\n \"class\",\n \"onClick\",\n ]);\n\n const [isChecked, setIsChecked] = createControllableSignal({\n value: () => local.checked,\n defaultValue: local.defaultChecked ?? false,\n onChange: (val) => local.onChange?.(val),\n });\n\n const toggle = (\n e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }\n ) => {\n if (local.disabled) return;\n setIsChecked(!isChecked());\n\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n };\n\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={Boolean(isChecked())}\n data-state={isChecked() ? \"checked\" : \"unchecked\"}\n disabled={local.disabled}\n onClick={toggle}\n class={cn(\n \"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-lg border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n local.class\n )}\n {...rest}\n >\n <span\n data-state={isChecked() ? \"checked\" : \"unchecked\"}\n class={cn(\n \"pointer-events-none block h-4 w-4 rounded-sm bg-primary-foreground shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0\"\n )}\n />\n </button>\n );\n};",
14
14
  "type": "registry:ui"
15
15
  }
16
16
  ]
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "ui/tabs.tsx",
13
- "content": "import {\n createContext,\n useContext,\n createSignal,\n splitProps,\n Show,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\ninterface TabsContextValue {\n value: Accessor<string | undefined>;\n setValue: (value: string) => void;\n orientation: Accessor<\"horizontal\" | \"vertical\">;\n}\n\nconst TabsContext = createContext<TabsContextValue>();\n\nexport interface TabsProps\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** Controlled active tab value */\n value?: string;\n /** Uncontrolled default active tab value */\n defaultValue?: string;\n /** Callback fired when active tab changes */\n onChange?: (value: string) => void;\n /** Layout orientation of tab triggers and content */\n orientation?: \"horizontal\" | \"vertical\";\n class?: string;\n}\n\n/**\n * Root Tabs container component managing active tab context state and orientation.\n */\nexport const Tabs: Component<TabsProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"defaultValue\",\n \"onChange\",\n \"orientation\",\n \"class\",\n \"children\",\n ]);\n\n const [internalValue, setInternalValue] = createSignal(local.defaultValue);\n\n const currentValue = () =>\n local.value !== undefined ? local.value : internalValue();\n\n const orientation = () => local.orientation || \"horizontal\";\n\n const handleSelect = (val: string) => {\n if (local.value === undefined) {\n setInternalValue(val);\n }\n if (typeof local.onChange === \"function\") {\n local.onChange(val);\n }\n };\n\n const contextValue: TabsContextValue = {\n value: currentValue,\n setValue: handleSelect,\n orientation,\n };\n\n return (\n <TabsContext.Provider value={contextValue}>\n <div\n data-orientation={orientation()}\n class={cn(\n \"w-full\",\n orientation() === \"vertical\" ? \"flex flex-row gap-4\" : \"flex flex-col gap-2\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </TabsContext.Provider>\n );\n};\n\nexport interface TabsListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Container wrapper for Tab triggers.\n */\nexport const TabsList: Component<TabsListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const context = useContext(TabsContext);\n\n const isVertical = () => context?.orientation() === \"vertical\";\n\n return (\n <div\n role=\"tablist\"\n aria-orientation={context?.orientation() || \"horizontal\"}\n class={cn(\n \"inline-flex rounded-lg bg-muted p-1 text-muted-foreground\",\n isVertical()\n ? \"flex-col h-auto w-auto items-stretch justify-start\"\n : \"h-9 items-center justify-center\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface TabsTriggerProps\n extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\"> {\n /** Unique value identifier for this tab */\n value: string;\n class?: string;\n}\n\n/**\n * Tab button trigger to activate a specific tab panel.\n */\nexport const TabsTrigger: Component<TabsTriggerProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"disabled\",\n \"class\",\n \"children\",\n \"onClick\",\n ]);\n const context = useContext(TabsContext);\n\n if (!context) {\n throw new Error(\"TabsTrigger must be used within a Tabs component\");\n }\n\n const isSelected = () => context.value() === local.value;\n const isVertical = () => context.orientation() === \"vertical\";\n\n const handleClick = (\n e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }\n ) => {\n if (local.disabled) return;\n context.setValue(local.value);\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n };\n\n return (\n <button\n type=\"button\"\n role=\"tab\"\n aria-selected={isSelected()}\n data-state={isSelected() ? \"active\" : \"inactive\"}\n data-orientation={context.orientation()}\n disabled={local.disabled}\n onClick={handleClick}\n class={cn(\n \"inline-flex items-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm cursor-pointer\",\n isVertical() ? \"justify-start py-1.5\" : \"justify-center\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n\nexport interface TabsContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Value matching the corresponding tab trigger */\n value: string;\n class?: string;\n}\n\n/**\n * Content panel revealed when the associated tab is active.\n */\nexport const TabsContent: Component<TabsContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"value\", \"class\", \"children\"]);\n const context = useContext(TabsContext);\n\n if (!context) {\n throw new Error(\"TabsContent must be used within a Tabs component\");\n }\n\n const isSelected = () => context.value() === local.value;\n const isVertical = () => context.orientation() === \"vertical\";\n\n return (\n <Show when={isSelected()}>\n <div\n role=\"tabpanel\"\n data-state={isSelected() ? \"active\" : \"inactive\"}\n data-orientation={context.orientation()}\n class={cn(\n \"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n isVertical() ? \"flex-1 mt-0\" : \"mt-2\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </Show>\n );\n};",
13
+ "content": "import {\n createContext,\n useContext,\n splitProps,\n Show,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createControllableSignal } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\ninterface TabsContextValue {\n value: Accessor<string | undefined>;\n setValue: (value: string) => void;\n orientation: Accessor<\"horizontal\" | \"vertical\">;\n}\n\nconst TabsContext = createContext<TabsContextValue>();\n\nexport interface TabsProps\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** Controlled active tab value */\n value?: string;\n /** Uncontrolled default active tab value */\n defaultValue?: string;\n /** Callback fired when active tab changes */\n onChange?: (value: string) => void;\n /** Layout orientation of tab triggers and content */\n orientation?: \"horizontal\" | \"vertical\";\n class?: string;\n}\n\n/**\n * Root Tabs container component managing active tab context state and orientation.\n */\nexport const Tabs: Component<TabsProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"defaultValue\",\n \"onChange\",\n \"orientation\",\n \"class\",\n \"children\",\n ]);\n\n const [currentValue, setCurrentValue] = createControllableSignal<string>({\n value: () => local.value,\n defaultValue: local.defaultValue,\n onChange: (val) => local.onChange?.(val),\n });\n\n const orientation = () => local.orientation || \"horizontal\";\n\n const contextValue: TabsContextValue = {\n value: currentValue,\n setValue: (val: string) => setCurrentValue(val),\n orientation,\n };\n\n return (\n <TabsContext.Provider value={contextValue}>\n <div\n data-orientation={orientation()}\n class={cn(\n \"w-full\",\n orientation() === \"vertical\" ? \"flex flex-row gap-4\" : \"flex flex-col gap-2\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </TabsContext.Provider>\n );\n};\n\nexport interface TabsListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Container wrapper for Tab triggers.\n */\nexport const TabsList: Component<TabsListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const context = useContext(TabsContext);\n\n const isVertical = () => context?.orientation() === \"vertical\";\n\n return (\n <div\n role=\"tablist\"\n aria-orientation={context?.orientation() || \"horizontal\"}\n class={cn(\n \"inline-flex rounded-lg bg-muted p-1 text-muted-foreground\",\n isVertical()\n ? \"flex-col h-auto w-auto items-stretch justify-start\"\n : \"h-9 items-center justify-center\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface TabsTriggerProps\n extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\"> {\n /** Unique value identifier for this tab */\n value: string;\n class?: string;\n}\n\n/**\n * Tab button trigger to activate a specific tab panel.\n */\nexport const TabsTrigger: Component<TabsTriggerProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"disabled\",\n \"class\",\n \"children\",\n \"onClick\",\n ]);\n const context = useContext(TabsContext);\n\n if (!context) {\n throw new Error(\"TabsTrigger must be used within a Tabs component\");\n }\n\n const isSelected = () => context.value() === local.value;\n const isVertical = () => context.orientation() === \"vertical\";\n\n const handleClick = (\n e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }\n ) => {\n if (local.disabled) return;\n context.setValue(local.value);\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n };\n\n return (\n <button\n type=\"button\"\n role=\"tab\"\n aria-selected={isSelected()}\n data-state={isSelected() ? \"active\" : \"inactive\"}\n data-orientation={context.orientation()}\n disabled={local.disabled}\n onClick={handleClick}\n class={cn(\n \"inline-flex items-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm cursor-pointer\",\n isVertical() ? \"justify-start py-1.5\" : \"justify-center\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n\nexport interface TabsContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Value matching the corresponding tab trigger */\n value: string;\n class?: string;\n}\n\n/**\n * Content panel revealed when the associated tab is active.\n */\nexport const TabsContent: Component<TabsContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"value\", \"class\", \"children\"]);\n const context = useContext(TabsContext);\n\n if (!context) {\n throw new Error(\"TabsContent must be used within a Tabs component\");\n }\n\n const isSelected = () => context.value() === local.value;\n const isVertical = () => context.orientation() === \"vertical\";\n\n return (\n <Show when={isSelected()}>\n <div\n role=\"tabpanel\"\n data-state={isSelected() ? \"active\" : \"inactive\"}\n data-orientation={context.orientation()}\n class={cn(\n \"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n isVertical() ? \"flex-1 mt-0\" : \"mt-2\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </Show>\n );\n};",
14
14
  "type": "registry:ui"
15
15
  }
16
16
  ]
@@ -31,7 +31,7 @@
31
31
  },
32
32
  {
33
33
  "path": "ui/theme-toggle.tsx",
34
- "content": "import { splitProps, type Component, For, Show } from \"solid-js\";\nimport { Sun, Moon, Monitor } from \"lucide-solid\";\nimport {\n useTheme,\n type AccentColor,\n type Radius,\n type Theme,\n} from \"../../providers/theme-provider\";\nimport {\n runThemeTransition,\n type ThemeEffect,\n} from \"../../providers/theme-transitions\";\nimport { Button } from \"./button\";\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ThemeToggleProps {\n /** Display mode: \"mini\" for compact dropdown, \"max\" for full customizer panel (default: \"mini\") */\n mode?: \"mini\" | \"max\";\n /** Transition animation effect when changing themes (\"none\", \"circular\", \"fade\") */\n effect?: ThemeEffect;\n class?: string;\n}\n\nconst ACCENT_OPTIONS: { name: AccentColor; label: string; color: string }[] = [\n { name: \"wine\", label: \"Wine\", color: \"bg-[#722f37]\" },\n { name: \"violet\", label: \"Violet\", color: \"bg-[#7c3aed]\" },\n { name: \"sky\", label: \"Sky\", color: \"bg-[#0284c7]\" },\n { name: \"emerald\", label: \"Emerald\", color: \"bg-[#059669]\" },\n { name: \"rose\", label: \"Rose\", color: \"bg-[#e11d48]\" },\n { name: \"amber\", label: \"Amber\", color: \"bg-[#d97706]\" },\n { name: \"zinc\", label: \"Zinc\", color: \"bg-[#18181b]\" },\n];\n\nconst RADIUS_OPTIONS: { value: Radius; label: string }[] = [\n { value: \"0\", label: \"0\" },\n { value: \"0.3\", label: \"0.3\" },\n { value: \"0.5\", label: \"0.5\" },\n { value: \"0.75\", label: \"0.75\" },\n { value: \"1.0\", label: \"1.0\" },\n];\n\n/**\n * Interactive UI theme switcher supporting mini/max modes and View Transition animations.\n */\nexport const ThemeToggle: Component<ThemeToggleProps> = (props) => {\n const [local] = splitProps(props, [\"mode\", \"effect\", \"class\"]);\n const { theme, setTheme, accent, setAccent, radius, setRadius } = useTheme();\n\n const mode = () => local.mode || \"mini\";\n const effect = () => local.effect || \"none\";\n\n /* Reactive accessor determining whether dark mode is active */\n const isDarkMode = () => {\n const currentTheme = theme();\n if (currentTheme === \"dark\") return true;\n if (currentTheme === \"light\") return false;\n return (\n typeof window !== \"undefined\" &&\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n );\n };\n\n const changeThemeWithEffect = (newTheme: Theme, e: MouseEvent) => {\n runThemeTransition(effect(), e, () => {\n setTheme(newTheme);\n });\n };\n\n return (\n <DropdownMenu placement=\"bottom-end\">\n <DropdownMenuTrigger\n as={Button}\n variant=\"outline\"\n size=\"icon\"\n class={cn(\"relative h-9 w-9 cursor-pointer\", local.class)}\n >\n {/* Reactive Sun / Moon Icon Toggle */}\n <Show\n when={isDarkMode()}\n fallback={<Sun class=\"h-4 w-4 text-foreground transition-transform\" />}\n >\n <Moon class=\"h-4 w-4 text-foreground transition-transform\" />\n </Show>\n\n <span class=\"sr-only\">Toggle theme</span>\n </DropdownMenuTrigger>\n\n <Show\n when={mode() === \"max\"}\n fallback={\n /* Mini Mode: Compact Dropdown */\n <DropdownMenuContent>\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}>\n <Sun class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Light\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}>\n <Moon class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Dark\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}>\n <Monitor class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n System\n </DropdownMenuItem>\n </DropdownMenuContent>\n }\n >\n {/* Max Mode: Full Theme Customizer Panel */}\n <DropdownMenuContent class=\"w-64 p-3\">\n <DropdownMenuLabel class=\"px-0 pt-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Theme Mode\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-3 gap-1 my-1.5\">\n <Button\n variant={theme() === \"light\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Light\n </Button>\n <Button\n variant={theme() === \"dark\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Dark\n </Button>\n <Button\n variant={theme() === \"system\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n System\n </Button>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Brand Accent Color\n </DropdownMenuLabel>\n <div class=\"flex flex-wrap gap-1.5 my-1.5\">\n <For each={ACCENT_OPTIONS}>\n {(opt) => (\n <button\n type=\"button\"\n title={opt.label}\n onClick={() => setAccent(opt.name)}\n class={cn(\n \"h-6 w-6 rounded-md transition-all cursor-pointer border border-border flex items-center justify-center\",\n opt.color,\n accent() === opt.name ? \"ring-2 ring-primary ring-offset-2 ring-offset-background scale-110\" : \"hover:scale-105\"\n )}\n />\n )}\n </For>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Border Radius\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-5 gap-1 my-1.5\">\n <For each={RADIUS_OPTIONS}>\n {(r) => (\n <Button\n variant={radius() === r.value ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={() => setRadius(r.value)}\n class=\"h-7 text-xs px-1 cursor-pointer\"\n >\n {r.label}\n </Button>\n )}\n </For>\n </div>\n </DropdownMenuContent>\n </Show>\n </DropdownMenu>\n );\n};",
34
+ "content": "import { splitProps, type Component, For, Show } from \"solid-js\";\nimport { Sun, Moon, Monitor } from \"lucide-solid\";\nimport {\n useTheme,\n type AccentColor,\n type Radius,\n type Theme,\n} from \"../../providers/theme-provider\";\nimport {\n runThemeTransition,\n type ThemeEffect,\n} from \"../../providers/theme-transitions\";\nimport { Button } from \"./button\";\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\";\nimport { cn } from \"@/lib/cn\";\n\nimport { createColorMode } from \"@nikala-ui/hooks\";\n\nexport interface ThemeToggleProps {\n /** Display mode: \"mini\" for compact dropdown, \"max\" for full customizer panel (default: \"mini\") */\n mode?: \"mini\" | \"max\";\n /** Transition animation effect when changing themes (\"none\", \"circular\", \"fade\") */\n effect?: ThemeEffect;\n class?: string;\n}\n\nconst ACCENT_OPTIONS: { name: AccentColor; label: string; color: string }[] = [\n { name: \"wine\", label: \"Wine\", color: \"bg-[#722f37]\" },\n { name: \"violet\", label: \"Violet\", color: \"bg-[#7c3aed]\" },\n { name: \"sky\", label: \"Sky\", color: \"bg-[#0284c7]\" },\n { name: \"emerald\", label: \"Emerald\", color: \"bg-[#059669]\" },\n { name: \"rose\", label: \"Rose\", color: \"bg-[#e11d48]\" },\n { name: \"amber\", label: \"Amber\", color: \"bg-[#d97706]\" },\n { name: \"zinc\", label: \"Zinc\", color: \"bg-[#18181b]\" },\n];\n\nconst RADIUS_OPTIONS: { value: Radius; label: string }[] = [\n { value: \"0\", label: \"0\" },\n { value: \"0.3\", label: \"0.3\" },\n { value: \"0.5\", label: \"0.5\" },\n { value: \"0.75\", label: \"0.75\" },\n { value: \"1.0\", label: \"1.0\" },\n];\n\n/**\n * Interactive UI theme switcher supporting mini/max modes and View Transition animations.\n */\nexport const ThemeToggle: Component<ThemeToggleProps> = (props) => {\n const [local] = splitProps(props, [\"mode\", \"effect\", \"class\"]);\n const { theme, setTheme, accent, setAccent, radius, setRadius } = useTheme();\n\n const colorMode = createColorMode({\n initialValue: theme(),\n storageKey: \"nikala-theme-mode\",\n });\n\n const mode = () => local.mode || \"mini\";\n const effect = () => local.effect || \"none\";\n\n /* Reactive accessor determining whether dark mode is active via createColorMode hook */\n const isDarkMode = () => {\n const currentTheme = theme();\n if (currentTheme === \"dark\") return true;\n if (currentTheme === \"light\") return false;\n return colorMode.isDark();\n };\n\n const changeThemeWithEffect = (newTheme: Theme, e: MouseEvent) => {\n runThemeTransition(effect(), e, () => {\n setTheme(newTheme);\n colorMode.setMode(newTheme);\n });\n };\n\n return (\n <DropdownMenu placement=\"bottom-end\">\n <DropdownMenuTrigger\n as={Button}\n variant=\"outline\"\n size=\"icon\"\n class={cn(\"relative h-9 w-9 cursor-pointer\", local.class)}\n >\n {/* Reactive Sun / Moon Icon Toggle */}\n <Show\n when={isDarkMode()}\n fallback={<Sun class=\"h-4 w-4 text-foreground transition-transform\" />}\n >\n <Moon class=\"h-4 w-4 text-foreground transition-transform\" />\n </Show>\n\n <span class=\"sr-only\">Toggle theme</span>\n </DropdownMenuTrigger>\n\n <Show\n when={mode() === \"max\"}\n fallback={\n /* Mini Mode: Compact Dropdown */\n <DropdownMenuContent>\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}>\n <Sun class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Light\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}>\n <Moon class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Dark\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}>\n <Monitor class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n System\n </DropdownMenuItem>\n </DropdownMenuContent>\n }\n >\n {/* Max Mode: Full Theme Customizer Panel */}\n <DropdownMenuContent class=\"w-64 p-3\">\n <DropdownMenuLabel class=\"px-0 pt-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Theme Mode\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-3 gap-1 my-1.5\">\n <Button\n variant={theme() === \"light\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Light\n </Button>\n <Button\n variant={theme() === \"dark\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Dark\n </Button>\n <Button\n variant={theme() === \"system\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n System\n </Button>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Brand Accent Color\n </DropdownMenuLabel>\n <div class=\"flex flex-wrap gap-1.5 my-1.5\">\n <For each={ACCENT_OPTIONS}>\n {(opt) => (\n <button\n type=\"button\"\n title={opt.label}\n onClick={() => setAccent(opt.name)}\n class={cn(\n \"h-6 w-6 rounded-md transition-all cursor-pointer border border-border flex items-center justify-center\",\n opt.color,\n accent() === opt.name ? \"ring-2 ring-primary ring-offset-2 ring-offset-background scale-110\" : \"hover:scale-105\"\n )}\n />\n )}\n </For>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Border Radius\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-5 gap-1 my-1.5\">\n <For each={RADIUS_OPTIONS}>\n {(r) => (\n <Button\n variant={radius() === r.value ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={() => setRadius(r.value)}\n class=\"h-7 text-xs px-1 cursor-pointer\"\n >\n {r.label}\n </Button>\n )}\n </For>\n </div>\n </DropdownMenuContent>\n </Show>\n </DropdownMenu>\n );\n};",
35
35
  "type": "registry:ui"
36
36
  }
37
37
  ]
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "ui/tooltip.tsx",
14
- "content": "import { splitProps, type Component, type ComponentProps } from \"solid-js\";\nimport { Tooltip as KobalteTooltip } from \"@kobalte/core/tooltip\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Tooltip: Component<ComponentProps<typeof KobalteTooltip>> = (props) => {\n return <KobalteTooltip gutter={4} {...props} />;\n};\n\nexport const TooltipTrigger = KobalteTooltip.Trigger;\n\nexport interface TooltipContentProps extends ComponentProps<typeof KobalteTooltip.Content> {\n class?: string;\n}\n\nexport const TooltipContent: Component<TooltipContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobalteTooltip.Portal>\n <KobalteTooltip.Content\n class={cn(\n \"z-50 overflow-hidden rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md transition-all animate-in fade-in-0 zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-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}\n />\n </KobalteTooltip.Portal>\n );\n};",
14
+ "content": "import { splitProps, type Component, type ComponentProps } from \"solid-js\";\nimport { Tooltip as KobalteTooltip } from \"@kobalte/core/tooltip\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Tooltip = KobalteTooltip;\n\nexport const TooltipTrigger = KobalteTooltip.Trigger;\n\nexport const TooltipArrow: Component<ComponentProps<typeof KobalteTooltip.Arrow>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobalteTooltip.Arrow\n size={8}\n class={cn(\"fill-popover stroke-border\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface TooltipContentProps extends ComponentProps<typeof KobalteTooltip.Content> {\n class?: string;\n}\n\nexport const TooltipContent: Component<TooltipContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <KobalteTooltip.Portal>\n <KobalteTooltip.Content\n class={cn(\n \"z-50 rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md transition-all animate-in fade-in-0 zoom-in-95 data-closed:animate-out data-[closed]:fade-out-0 data-closed:zoom-out-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}\n >\n {local.children}\n </KobalteTooltip.Content>\n </KobalteTooltip.Portal>\n );\n};",
15
15
  "type": "registry:ui"
16
16
  }
17
17
  ]
@@ -26,6 +26,8 @@ export const Accordion = <T extends ValidComponent = "div">(
26
26
  "class",
27
27
  "type",
28
28
  "multiple",
29
+ "value",
30
+ "defaultValue",
29
31
  ]);
30
32
 
31
33
  // Support both `type="multiple"` and `multiple={true}`
@@ -34,6 +36,8 @@ export const Accordion = <T extends ValidComponent = "div">(
34
36
  return (
35
37
  <AccordionPrimitive.Root
36
38
  multiple={isMultiple()}
39
+ value={local.value}
40
+ defaultValue={local.defaultValue}
37
41
  class={cn("w-full divide-y divide-border", local.class)}
38
42
  {...(rest as any)}
39
43
  />
@@ -1,4 +1,5 @@
1
- import { createSignal, splitProps, Show, type Component, type JSX } from "solid-js";
1
+ import { splitProps, Show, type Component, type JSX } from "solid-js";
2
+ import { createControllableSignal } from "@nikala-ui/hooks";
2
3
  import { cn } from "@/lib/cn";
3
4
 
4
5
  export interface CheckboxProps
@@ -16,7 +17,6 @@ export interface CheckboxProps
16
17
  * Nikala UI Checkbox component built for SolidJS with Tailwind CSS v4 styling.
17
18
  */
18
19
  export const Checkbox: Component<CheckboxProps> = (props) => {
19
- // Use splitProps to preserve SolidJS reactivity
20
20
  const [local, rest] = splitProps(props, [
21
21
  "checked",
22
22
  "defaultChecked",
@@ -26,28 +26,17 @@ export const Checkbox: Component<CheckboxProps> = (props) => {
26
26
  "onClick",
27
27
  ]);
28
28
 
29
- // Internal state for uncontrolled mode
30
- const [internalChecked, setInternalChecked] = createSignal(
31
- local.defaultChecked ?? false
32
- );
33
-
34
- // Computed checked state
35
- const isChecked = () =>
36
- local.checked !== undefined ? local.checked : internalChecked();
29
+ const [isChecked, setIsChecked] = createControllableSignal({
30
+ value: () => local.checked,
31
+ defaultValue: local.defaultChecked ?? false,
32
+ onChange: (val) => local.onChange?.(val),
33
+ });
37
34
 
38
35
  const toggle = (
39
36
  e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }
40
37
  ) => {
41
38
  if (local.disabled) return;
42
-
43
- const nextState = !isChecked();
44
- if (local.checked === undefined) {
45
- setInternalChecked(nextState);
46
- }
47
-
48
- if (typeof local.onChange === "function") {
49
- local.onChange(nextState);
50
- }
39
+ setIsChecked(!isChecked());
51
40
 
52
41
  if (typeof local.onClick === "function") {
53
42
  local.onClick(e);
@@ -58,7 +47,7 @@ export const Checkbox: Component<CheckboxProps> = (props) => {
58
47
  <button
59
48
  type="button"
60
49
  role="checkbox"
61
- aria-checked={isChecked()}
50
+ aria-checked={Boolean(isChecked())}
62
51
  data-state={isChecked() ? "checked" : "unchecked"}
63
52
  disabled={local.disabled}
64
53
  onClick={toggle}
@@ -46,7 +46,7 @@ export const Command: Component<CommandProps> = (props) => {
46
46
  <CommandContext.Provider value={{ search, setSearch }}>
47
47
  <div
48
48
  class={cn(
49
- "flex flex-col w-full h-full rounded-xl bg-popover text-popover-foreground overflow-hidden border border-border shadow-md",
49
+ "flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md",
50
50
  local.class
51
51
  )}
52
52
  {...rest}
@@ -94,7 +94,7 @@ export const CommandDialog: Component<CommandDialogProps> = (props) => {
94
94
  <Dialog.Portal>
95
95
  <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" />
96
96
  <div class="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4">
97
- <Dialog.Content class="w-full max-w-xl rounded-xl 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">
97
+ <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">
98
98
  <Command class={props.class}>{props.children}</Command>
99
99
  </Dialog.Content>
100
100
  </div>
@@ -1,6 +1,7 @@
1
1
  import { splitProps, type Component, type JSX, type ValidComponent } from "solid-js";
2
2
  import * as DropdownMenuPrimitive from "@kobalte/core/dropdown-menu";
3
3
  import type { PolymorphicProps } from "@kobalte/core/polymorphic";
4
+ import { createClickOutside } from "@nikala-ui/hooks";
4
5
  import { cn } from "@/lib/cn";
5
6
 
6
7
  export type DropdownMenuRootProps = Omit<
@@ -92,10 +93,24 @@ export const DropdownMenuContent = <T extends ValidComponent = "div">(
92
93
  props: PolymorphicProps<T, DropdownMenuContentProps<T>>
93
94
  ) => {
94
95
  const [local, rest] = splitProps(props as DropdownMenuContentProps, ["class"]);
96
+ let contentRef: HTMLElement | undefined;
97
+
98
+ createClickOutside({
99
+ target: () => contentRef,
100
+ onInteractOutside: (e) => {
101
+ if (typeof (props as any).onInteractOutside === "function") {
102
+ (props as any).onInteractOutside(e);
103
+ }
104
+ },
105
+ });
95
106
 
96
107
  return (
97
108
  <DropdownMenuPrimitive.Portal>
98
109
  <DropdownMenuPrimitive.Content
110
+ ref={(el) => {
111
+ contentRef = el;
112
+ if (typeof (props as any).ref === "function") (props as any).ref(el);
113
+ }}
99
114
  class={cn(
100
115
  "z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
101
116
  local.class
@@ -0,0 +1,90 @@
1
+ import { splitProps, type Component, type ComponentProps } from "solid-js";
2
+ import { Popover as KobaltePopover } from "@kobalte/core/popover";
3
+ import { createClickOutside } from "@nikala-ui/hooks";
4
+ import { X } from "lucide-solid";
5
+ import { cn } from "@/lib/cn";
6
+
7
+ export const Popover = KobaltePopover;
8
+
9
+ export const PopoverTrigger = KobaltePopover.Trigger;
10
+
11
+ export const PopoverArrow: Component<ComponentProps<typeof KobaltePopover.Arrow>> = (props) => {
12
+ const [local, rest] = splitProps(props, ["class"]);
13
+
14
+ return (
15
+ <KobaltePopover.Arrow
16
+ class={cn("fill-popover stroke-border", local.class)}
17
+ {...rest}
18
+ />
19
+ );
20
+ };
21
+
22
+ export const PopoverContent: Component<ComponentProps<typeof KobaltePopover.Content>> = (props) => {
23
+ const [local, rest] = splitProps(props, ["class", "children", "onInteractOutside"]);
24
+ let contentRef: HTMLElement | undefined;
25
+
26
+ createClickOutside({
27
+ target: () => contentRef,
28
+ onInteractOutside: (e) => {
29
+ if (typeof local.onInteractOutside === "function") {
30
+ local.onInteractOutside(e as any);
31
+ }
32
+ },
33
+ });
34
+
35
+ return (
36
+ <KobaltePopover.Portal>
37
+ <KobaltePopover.Content
38
+ ref={(el) => {
39
+ contentRef = el;
40
+ if (typeof (props as any).ref === "function") (props as any).ref(el);
41
+ }}
42
+ class={cn(
43
+ "z-50 w-72 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-md 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 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",
44
+ local.class
45
+ )}
46
+ {...rest}
47
+ >
48
+ {local.children}
49
+ </KobaltePopover.Content>
50
+ </KobaltePopover.Portal>
51
+ );
52
+ };
53
+
54
+ export const PopoverTitle: Component<ComponentProps<typeof KobaltePopover.Title>> = (props) => {
55
+ const [local, rest] = splitProps(props, ["class"]);
56
+
57
+ return (
58
+ <KobaltePopover.Title
59
+ class={cn("text-sm font-semibold text-foreground", local.class)}
60
+ {...rest}
61
+ />
62
+ );
63
+ };
64
+
65
+ export const PopoverDescription: Component<ComponentProps<typeof KobaltePopover.Description>> = (props) => {
66
+ const [local, rest] = splitProps(props, ["class"]);
67
+
68
+ return (
69
+ <KobaltePopover.Description
70
+ class={cn("text-sm text-muted-foreground", local.class)}
71
+ {...rest}
72
+ />
73
+ );
74
+ };
75
+
76
+ export const PopoverCloseButton: Component<ComponentProps<typeof KobaltePopover.CloseButton>> = (props) => {
77
+ const [local, rest] = splitProps(props, ["class", "children"]);
78
+
79
+ return (
80
+ <KobaltePopover.CloseButton
81
+ class={cn(
82
+ "absolute right-2 top-2 rounded-md p-1 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none",
83
+ local.class
84
+ )}
85
+ {...rest}
86
+ >
87
+ {local.children || <X class="h-4 w-4 text-muted-foreground" />}
88
+ </KobaltePopover.CloseButton>
89
+ );
90
+ };
@@ -0,0 +1,66 @@
1
+ import { splitProps, Show, type Component, type JSX } from "solid-js";
2
+ import { Progress as KobalteProgress } from "@kobalte/core/progress";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export interface ProgressProps
6
+ extends Omit<
7
+ JSX.HTMLAttributes<HTMLDivElement>,
8
+ "value" | "aria-valuenow" | "aria-valuemin" | "aria-valuemax"
9
+ > {
10
+ /** The current numeric progress value. */
11
+ value?: number;
12
+ /** Minimum progress value. Defaults to 0. */
13
+ minValue?: number;
14
+ /** Maximum progress value. Defaults to 100. */
15
+ maxValue?: number;
16
+ /** Label text for accessibility and screen readers. */
17
+ getValueLabel?: (params: { value: number; max: number }) => string;
18
+ /** Custom label element to display above or beside progress bar. */
19
+ label?: JSX.Element;
20
+ /** Optional custom class for root container. */
21
+ class?: string;
22
+ /** Optional custom class for indicator fill bar. */
23
+ indicatorClass?: string;
24
+ }
25
+
26
+ /**
27
+ * Nikala UI Progress component for showing task completion status or media timeline positions.
28
+ */
29
+ export const Progress: Component<ProgressProps> = (props) => {
30
+ const [local, rest] = splitProps(props, [
31
+ "value",
32
+ "minValue",
33
+ "maxValue",
34
+ "getValueLabel",
35
+ "label",
36
+ "class",
37
+ "indicatorClass",
38
+ ]);
39
+
40
+ return (
41
+ <KobalteProgress
42
+ value={local.value ?? 0}
43
+ minValue={local.minValue ?? 0}
44
+ maxValue={local.maxValue ?? 100}
45
+ getValueLabel={local.getValueLabel}
46
+ class={cn("flex w-full flex-col gap-1.5", local.class)}
47
+ {...rest}
48
+ >
49
+ <Show when={local.label}>
50
+ <div class="flex justify-between text-xs font-medium text-muted-foreground">
51
+ <KobalteProgress.Label>{local.label}</KobalteProgress.Label>
52
+ <KobalteProgress.ValueLabel class="font-mono text-foreground" />
53
+ </div>
54
+ </Show>
55
+
56
+ <KobalteProgress.Track class="relative h-2 w-full overflow-hidden rounded-full bg-primary/20">
57
+ <KobalteProgress.Fill
58
+ class={cn(
59
+ "h-full w-[var(--kb-progress-fill-width)] bg-primary transition-all duration-300 ease-in-out",
60
+ local.indicatorClass
61
+ )}
62
+ />
63
+ </KobalteProgress.Track>
64
+ </KobalteProgress>
65
+ );
66
+ };
@@ -1,6 +1,7 @@
1
1
  import { splitProps, type JSX, type ValidComponent } from "solid-js";
2
2
  import * as SelectPrimitive from "@kobalte/core/select";
3
3
  import type { PolymorphicProps } from "@kobalte/core/polymorphic";
4
+ import { createClickOutside } from "@nikala-ui/hooks";
4
5
  import { cn } from "@/lib/cn";
5
6
 
6
7
  export type SelectRootProps<Option = any, OptGroup = any, T extends ValidComponent = "div"> =
@@ -89,10 +90,24 @@ export const SelectContent = <T extends ValidComponent = "div">(
89
90
  props: PolymorphicProps<T, SelectContentProps<T>>
90
91
  ) => {
91
92
  const [local, rest] = splitProps(props as SelectContentProps, ["class"]);
93
+ let contentRef: HTMLElement | undefined;
94
+
95
+ createClickOutside({
96
+ target: () => contentRef,
97
+ onInteractOutside: (e) => {
98
+ if (typeof (props as any).onInteractOutside === "function") {
99
+ (props as any).onInteractOutside(e);
100
+ }
101
+ },
102
+ });
92
103
 
93
104
  return (
94
105
  <SelectPrimitive.Portal>
95
106
  <SelectPrimitive.Content
107
+ ref={(el) => {
108
+ contentRef = el;
109
+ if (typeof (props as any).ref === "function") (props as any).ref(el);
110
+ }}
96
111
  class={cn(
97
112
  "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",
98
113
  local.class
@@ -1,4 +1,5 @@
1
- import { createSignal, splitProps, type Component, type JSX } from "solid-js";
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { createControllableSignal } from "@nikala-ui/hooks";
2
3
  import { cn } from "@/lib/cn";
3
4
 
4
5
  export interface SwitchProps
@@ -16,7 +17,6 @@ export interface SwitchProps
16
17
  * Nikala UI Switch component for boolean toggle inputs built for SolidJS with Tailwind CSS v4 styling.
17
18
  */
18
19
  export const Switch: Component<SwitchProps> = (props) => {
19
- // Use splitProps to preserve SolidJS reactivity
20
20
  const [local, rest] = splitProps(props, [
21
21
  "checked",
22
22
  "defaultChecked",
@@ -26,28 +26,17 @@ export const Switch: Component<SwitchProps> = (props) => {
26
26
  "onClick",
27
27
  ]);
28
28
 
29
- // Internal state for uncontrolled mode
30
- const [internalChecked, setInternalChecked] = createSignal(
31
- local.defaultChecked ?? false
32
- );
33
-
34
- // Computed checked state supporting both controlled and uncontrolled modes
35
- const isChecked = () =>
36
- local.checked !== undefined ? local.checked : internalChecked();
29
+ const [isChecked, setIsChecked] = createControllableSignal({
30
+ value: () => local.checked,
31
+ defaultValue: local.defaultChecked ?? false,
32
+ onChange: (val) => local.onChange?.(val),
33
+ });
37
34
 
38
35
  const toggle = (
39
36
  e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }
40
37
  ) => {
41
38
  if (local.disabled) return;
42
-
43
- const nextState = !isChecked();
44
- if (local.checked === undefined) {
45
- setInternalChecked(nextState);
46
- }
47
-
48
- if (typeof local.onChange === "function") {
49
- local.onChange(nextState);
50
- }
39
+ setIsChecked(!isChecked());
51
40
 
52
41
  if (typeof local.onClick === "function") {
53
42
  local.onClick(e);
@@ -58,7 +47,7 @@ export const Switch: Component<SwitchProps> = (props) => {
58
47
  <button
59
48
  type="button"
60
49
  role="switch"
61
- aria-checked={isChecked()}
50
+ aria-checked={Boolean(isChecked())}
62
51
  data-state={isChecked() ? "checked" : "unchecked"}
63
52
  disabled={local.disabled}
64
53
  onClick={toggle}
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  createContext,
3
3
  useContext,
4
- createSignal,
5
4
  splitProps,
6
5
  Show,
7
6
  type Component,
8
7
  type JSX,
9
8
  type Accessor,
10
9
  } from "solid-js";
10
+ import { createControllableSignal } from "@nikala-ui/hooks";
11
11
  import { cn } from "@/lib/cn";
12
12
 
13
13
  interface TabsContextValue {
@@ -44,25 +44,17 @@ export const Tabs: Component<TabsProps> = (props) => {
44
44
  "children",
45
45
  ]);
46
46
 
47
- const [internalValue, setInternalValue] = createSignal(local.defaultValue);
48
-
49
- const currentValue = () =>
50
- local.value !== undefined ? local.value : internalValue();
47
+ const [currentValue, setCurrentValue] = createControllableSignal<string>({
48
+ value: () => local.value,
49
+ defaultValue: local.defaultValue,
50
+ onChange: (val) => local.onChange?.(val),
51
+ });
51
52
 
52
53
  const orientation = () => local.orientation || "horizontal";
53
54
 
54
- const handleSelect = (val: string) => {
55
- if (local.value === undefined) {
56
- setInternalValue(val);
57
- }
58
- if (typeof local.onChange === "function") {
59
- local.onChange(val);
60
- }
61
- };
62
-
63
55
  const contextValue: TabsContextValue = {
64
56
  value: currentValue,
65
- setValue: handleSelect,
57
+ setValue: (val: string) => setCurrentValue(val),
66
58
  orientation,
67
59
  };
68
60
 
@@ -21,6 +21,8 @@ import {
21
21
  } from "./dropdown-menu";
22
22
  import { cn } from "@/lib/cn";
23
23
 
24
+ import { createColorMode } from "@nikala-ui/hooks";
25
+
24
26
  export interface ThemeToggleProps {
25
27
  /** Display mode: "mini" for compact dropdown, "max" for full customizer panel (default: "mini") */
26
28
  mode?: "mini" | "max";
@@ -54,23 +56,26 @@ export const ThemeToggle: Component<ThemeToggleProps> = (props) => {
54
56
  const [local] = splitProps(props, ["mode", "effect", "class"]);
55
57
  const { theme, setTheme, accent, setAccent, radius, setRadius } = useTheme();
56
58
 
59
+ const colorMode = createColorMode({
60
+ initialValue: theme(),
61
+ storageKey: "nikala-theme-mode",
62
+ });
63
+
57
64
  const mode = () => local.mode || "mini";
58
65
  const effect = () => local.effect || "none";
59
66
 
60
- /* Reactive accessor determining whether dark mode is active */
67
+ /* Reactive accessor determining whether dark mode is active via createColorMode hook */
61
68
  const isDarkMode = () => {
62
69
  const currentTheme = theme();
63
70
  if (currentTheme === "dark") return true;
64
71
  if (currentTheme === "light") return false;
65
- return (
66
- typeof window !== "undefined" &&
67
- window.matchMedia("(prefers-color-scheme: dark)").matches
68
- );
72
+ return colorMode.isDark();
69
73
  };
70
74
 
71
75
  const changeThemeWithEffect = (newTheme: Theme, e: MouseEvent) => {
72
76
  runThemeTransition(effect(), e, () => {
73
77
  setTheme(newTheme);
78
+ colorMode.setMode(newTheme);
74
79
  });
75
80
  };
76
81
 
@@ -2,28 +2,40 @@ import { splitProps, type Component, type ComponentProps } from "solid-js";
2
2
  import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip";
3
3
  import { cn } from "@/lib/cn";
4
4
 
5
- export const Tooltip: Component<ComponentProps<typeof KobalteTooltip>> = (props) => {
6
- return <KobalteTooltip gutter={4} {...props} />;
7
- };
5
+ export const Tooltip = KobalteTooltip;
8
6
 
9
7
  export const TooltipTrigger = KobalteTooltip.Trigger;
10
8
 
9
+ export const TooltipArrow: Component<ComponentProps<typeof KobalteTooltip.Arrow>> = (props) => {
10
+ const [local, rest] = splitProps(props, ["class"]);
11
+
12
+ return (
13
+ <KobalteTooltip.Arrow
14
+ size={8}
15
+ class={cn("fill-popover stroke-border", local.class)}
16
+ {...rest}
17
+ />
18
+ );
19
+ };
20
+
11
21
  export interface TooltipContentProps extends ComponentProps<typeof KobalteTooltip.Content> {
12
22
  class?: string;
13
23
  }
14
24
 
15
25
  export const TooltipContent: Component<TooltipContentProps> = (props) => {
16
- const [local, rest] = splitProps(props, ["class"]);
26
+ const [local, rest] = splitProps(props, ["class", "children"]);
17
27
 
18
28
  return (
19
29
  <KobalteTooltip.Portal>
20
30
  <KobalteTooltip.Content
21
31
  class={cn(
22
- "z-50 overflow-hidden rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md transition-all animate-in fade-in-0 zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-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",
32
+ "z-50 rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md transition-all animate-in fade-in-0 zoom-in-95 data-closed:animate-out data-[closed]:fade-out-0 data-closed:zoom-out-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",
23
33
  local.class
24
34
  )}
25
35
  {...rest}
26
- />
36
+ >
37
+ {local.children}
38
+ </KobalteTooltip.Content>
27
39
  </KobalteTooltip.Portal>
28
40
  );
29
41
  };
@@ -159,4 +159,14 @@ export const COMPONENT_METADATA: Record<string, ComponentMeta> = {
159
159
  description: "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it, built on Kobalte primitives.",
160
160
  dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
161
161
  },
162
+ popover: {
163
+ title: "Popover",
164
+ description: "Displays rich content in a portal layer triggered by a button, built on Kobalte primitives.",
165
+ dependencies: ["clsx", "tailwind-merge", "@kobalte/core", "lucide-solid"],
166
+ },
167
+ progress: {
168
+ title: "Progress",
169
+ description: "Displays an indicator showing the completion progress of a task or media playback, built on Kobalte primitives.",
170
+ dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
171
+ },
162
172
  };