@nikala-ui/core 0.7.0 → 0.8.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.0",
3
+ "version": "0.8.0",
4
4
  "description": "Core component definitions, design tokens, and registry for Nikala UI",
5
5
  "type": "module",
6
6
  "private": false,
@@ -11,6 +11,11 @@
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/nikala-ui/ui.git",
17
+ "directory": "packages/core"
18
+ },
14
19
  "scripts": {
15
20
  "build:registry": "bun scripts/build-registry.ts"
16
21
  },
@@ -24,5 +29,9 @@
24
29
  "picocolors": "^1.1.1",
25
30
  "solid-js": "^1.9.14",
26
31
  "tailwind-merge": "^3.6.0"
32
+ },
33
+ "dependencies": {
34
+ "@nikala-ui/hooks": "workspace:*",
35
+ "lucide-solid": "^1.28.0"
27
36
  }
28
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
  ]
@@ -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,18 @@
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
+ },
197
209
  {
198
210
  "name": "radio-group",
199
211
  "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
+ }
@@ -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}
@@ -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
+ };
@@ -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,9 @@ 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
+ },
162
167
  };