@nikala-ui/core 0.11.0 → 0.12.1

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.
Files changed (109) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +97 -20
  3. package/package.json +43 -2
  4. package/registry/api-table.json +21 -0
  5. package/registry/banner.json +1 -1
  6. package/registry/button.json +1 -1
  7. package/registry/callout.json +19 -0
  8. package/registry/checkbox.json +4 -1
  9. package/registry/code-block.json +26 -0
  10. package/registry/code-group.json +20 -0
  11. package/registry/combobox.json +1 -1
  12. package/registry/command.json +3 -2
  13. package/registry/component-viewer.json +25 -0
  14. package/registry/container.json +18 -0
  15. package/registry/context-menu.json +1 -1
  16. package/registry/create-app-updater.json +13 -0
  17. package/registry/create-document-tabs.json +13 -0
  18. package/registry/create-global-shortcut.json +13 -0
  19. package/registry/create-tauri-window.json +13 -0
  20. package/registry/create-tiptap-editor.json +34 -0
  21. package/registry/dialog.json +1 -1
  22. package/registry/dropdown-menu.json +3 -2
  23. package/registry/field.json +1 -1
  24. package/registry/file-tree.json +21 -0
  25. package/registry/footer.json +1 -1
  26. package/registry/form-message.json +3 -2
  27. package/registry/icon-button.json +1 -1
  28. package/registry/index.json +336 -5
  29. package/registry/navbar.json +1 -1
  30. package/registry/navigation-menu.json +1 -1
  31. package/registry/number-input.json +1 -1
  32. package/registry/package-manager-tabs.json +24 -0
  33. package/registry/pager.json +21 -0
  34. package/registry/popover.json +4 -1
  35. package/registry/resizable.json +1 -1
  36. package/registry/rich-text-editor.json +95 -0
  37. package/registry/scroll-area.json +1 -1
  38. package/registry/section-heading.json +21 -0
  39. package/registry/select.json +3 -2
  40. package/registry/sheet.json +1 -1
  41. package/registry/sidebar.json +1 -1
  42. package/registry/status.json +1 -1
  43. package/registry/steps.json +20 -0
  44. package/registry/switch.json +4 -1
  45. package/registry/table-of-contents.json +23 -0
  46. package/registry/tabs.json +4 -1
  47. package/registry/theme-manager.json +6 -5
  48. package/registry/titlebar-tabs.json +24 -0
  49. package/registry/titlebar.json +26 -0
  50. package/registry/toggle-group.json +1 -1
  51. package/registry/updater-modal.json +26 -0
  52. package/src/index.ts +48 -0
  53. package/src/registry/components/ui/api-table.tsx +117 -0
  54. package/src/registry/components/ui/banner.tsx +0 -2
  55. package/src/registry/components/ui/button.tsx +1 -1
  56. package/src/registry/components/ui/callout.tsx +137 -0
  57. package/src/registry/components/ui/checkbox.tsx +1 -1
  58. package/src/registry/components/ui/code-block.tsx +326 -0
  59. package/src/registry/components/ui/code-group.tsx +105 -0
  60. package/src/registry/components/ui/combobox.tsx +83 -18
  61. package/src/registry/components/ui/command.tsx +110 -106
  62. package/src/registry/components/ui/component-viewer.tsx +363 -0
  63. package/src/registry/components/ui/container.tsx +45 -0
  64. package/src/registry/components/ui/context-menu.tsx +67 -22
  65. package/src/registry/components/ui/dialog.tsx +42 -32
  66. package/src/registry/components/ui/dropdown-menu.tsx +69 -31
  67. package/src/registry/components/ui/field.tsx +7 -3
  68. package/src/registry/components/ui/file-tree.tsx +181 -0
  69. package/src/registry/components/ui/footer.tsx +1 -1
  70. package/src/registry/components/ui/form-message.tsx +2 -2
  71. package/src/registry/components/ui/icon-button.tsx +1 -1
  72. package/src/registry/components/ui/navbar.tsx +18 -11
  73. package/src/registry/components/ui/navigation-menu.tsx +2 -2
  74. package/src/registry/components/ui/number-input.tsx +3 -3
  75. package/src/registry/components/ui/package-manager-tabs.tsx +226 -0
  76. package/src/registry/components/ui/pager.tsx +102 -0
  77. package/src/registry/components/ui/popover.tsx +1 -1
  78. package/src/registry/components/ui/resizable.tsx +3 -1
  79. package/src/registry/components/ui/rich-text-editor/bubble-menu.tsx +236 -0
  80. package/src/registry/components/ui/rich-text-editor/editor.tsx +308 -0
  81. package/src/registry/components/ui/rich-text-editor/extensions.ts +116 -0
  82. package/src/registry/components/ui/rich-text-editor/footer.tsx +36 -0
  83. package/src/registry/components/ui/rich-text-editor/image-dialog.tsx +75 -0
  84. package/src/registry/components/ui/rich-text-editor/index.ts +11 -0
  85. package/src/registry/components/ui/rich-text-editor/link-dialog.tsx +48 -0
  86. package/src/registry/components/ui/rich-text-editor/markdown.ts +170 -0
  87. package/src/registry/components/ui/rich-text-editor/slash-command.tsx +301 -0
  88. package/src/registry/components/ui/rich-text-editor/table-controls.tsx +68 -0
  89. package/src/registry/components/ui/rich-text-editor/toolbar.tsx +465 -0
  90. package/src/registry/components/ui/rich-text-editor/use-editor.ts +231 -0
  91. package/src/registry/components/ui/scroll-area.tsx +12 -2
  92. package/src/registry/components/ui/section-heading.tsx +108 -0
  93. package/src/registry/components/ui/select.tsx +16 -19
  94. package/src/registry/components/ui/sheet.tsx +58 -53
  95. package/src/registry/components/ui/sidebar.tsx +4 -4
  96. package/src/registry/components/ui/status.tsx +7 -5
  97. package/src/registry/components/ui/steps.tsx +198 -0
  98. package/src/registry/components/ui/switch.tsx +1 -1
  99. package/src/registry/components/ui/table-of-contents.tsx +131 -0
  100. package/src/registry/components/ui/tabs.tsx +1 -1
  101. package/src/registry/components/ui/theme-toggle.tsx +175 -139
  102. package/src/registry/components/ui/titlebar-tabs.tsx +773 -0
  103. package/src/registry/components/ui/titlebar.tsx +468 -0
  104. package/src/registry/components/ui/toggle-group.tsx +1 -1
  105. package/src/registry/components/ui/updater-modal.tsx +254 -0
  106. package/src/registry/metadata.ts +167 -6
  107. package/src/registry/providers/theme-provider.tsx +16 -8
  108. package/src/registry/providers/theme-script.tsx +29 -9
  109. package/src/registry/providers/theme-transitions.ts +81 -50
@@ -15,7 +15,7 @@
15
15
  "files": [
16
16
  {
17
17
  "path": "ui/sheet.tsx",
18
- "content": "import { splitProps, type Component, type JSX, type ValidComponent, Show } from \"solid-js\";\nimport * as DialogPrimitive from \"@kobalte/core/dialog\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-solid\";\nimport { ScrollArea } from \"./scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\n// Global CSS Keyframe Animations specifically designed for Kobalte animationend DOM events\nconst sheetStyles = `\n@keyframes sheet-slide-in-left { from { transform: translateX(-100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-left { from { transform: translateX(0); } to { transform: translateX(-100%); } }\n@keyframes sheet-slide-in-right { from { transform: translateX(100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-right { from { transform: translateX(0); } to { transform: translateX(100%); } }\n@keyframes sheet-slide-in-top { from { transform: translateY(-100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-top { from { transform: translateY(0); } to { transform: translateY(-100%); } }\n@keyframes sheet-slide-in-bottom { from { transform: translateY(100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-bottom { from { transform: translateY(0); } to { transform: translateY(-100%); } }\n@keyframes sheet-fade-in { from { opacity: 0; } to { opacity: 1; } }\n@keyframes sheet-fade-out { from { opacity: 1; } to { opacity: 0; } }\n`;\n\n/**\n * CVA variants for CSS keyframe slide animations across 4 edges.\n */\nexport const sheetVariants = cva(\n \"fixed z-50 gap-4 bg-card p-6 text-card-foreground shadow-lg\",\n {\n variants: {\n side: {\n top: \"inset-x-0 top-0 border-b data-[expanded]:animate-[sheet-slide-in-top_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-top_300ms_ease-in-out]\",\n bottom:\n \"inset-x-0 bottom-0 border-t data-[expanded]:animate-[sheet-slide-in-bottom_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-bottom_300ms_ease-in-out]\",\n left: \"inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-left_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-left_300ms_ease-in-out]\",\n right:\n \"inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-right_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-right_300ms_ease-in-out]\",\n },\n },\n defaultVariants: {\n side: \"right\",\n },\n }\n);\n\nexport const Sheet = DialogPrimitive.Root;\nexport const SheetTrigger = DialogPrimitive.Trigger;\nexport const SheetClose = DialogPrimitive.CloseButton;\nexport const SheetPortal = DialogPrimitive.Portal;\n\nexport type SheetOverlayProps<T extends ValidComponent = \"div\"> =\n DialogPrimitive.DialogOverlayProps<T> & {\n /** Whether to apply backdrop blur (default: true) */\n blur?: boolean;\n class?: string;\n };\n\n/**\n * Fullscreen dark backdrop with fade keyframe animation.\n */\nexport const SheetOverlay = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SheetOverlayProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetOverlayProps, [\"class\", \"blur\"]);\n\n return (\n <DialogPrimitive.Overlay\n class={cn(\n \"fixed inset-0 z-50 transition-all duration-200 data-[expanded]:animate-[sheet-fade-in_300ms_ease-in-out] data-[closed]:animate-[sheet-fade-out_300ms_ease-in-out]\",\n local.blur !== false ? \"bg-black/80 backdrop-blur-sm\" : \"bg-black/80\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type SheetContentProps<T extends ValidComponent = \"div\"> =\n DialogPrimitive.DialogContentProps<T> &\n VariantProps<typeof sheetVariants> & {\n side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n showCloseButton?: boolean;\n closeOnOutsideClick?: boolean;\n blur?: boolean;\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Slide-out panel container with ScrollArea and animation support.\n */\nexport const SheetContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SheetContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetContentProps, [\n \"class\",\n \"children\",\n \"side\",\n \"showCloseButton\",\n \"closeOnOutsideClick\",\n \"blur\",\n \"onPointerDownOutside\",\n \"onInteractOutside\",\n ]);\n\n const side = () => local.side || \"right\";\n\n const handlePointerDownOutside = (e: Event) => {\n if (local.closeOnOutsideClick === false) {\n e.preventDefault();\n }\n if (typeof local.onPointerDownOutside === \"function\") {\n local.onPointerDownOutside(e as any);\n }\n };\n\n const handleInteractOutside = (e: Event) => {\n if (local.closeOnOutsideClick === false) {\n e.preventDefault();\n }\n if (typeof local.onInteractOutside === \"function\") {\n local.onInteractOutside(e as any);\n }\n };\n\n return (\n <SheetPortal>\n <style>{sheetStyles}</style>\n <SheetOverlay blur={local.blur} />\n <DialogPrimitive.Content\n onPointerDownOutside={handlePointerDownOutside}\n onInteractOutside={handleInteractOutside}\n class={cn(sheetVariants({ side: side() }), \"p-0\", local.class)}\n {...(rest as any)}\n >\n <ScrollArea class=\"h-full w-full\">\n <div class=\"p-6 space-y-4\">\n {local.children}\n </div>\n </ScrollArea>\n <Show when={local.showCloseButton !== false}>\n <DialogPrimitive.CloseButton class=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none cursor-pointer z-50\">\n <X class=\"h-4 w-4\" />\n <span class=\"sr-only\">Close</span>\n </DialogPrimitive.CloseButton>\n </Show>\n </DialogPrimitive.Content>\n </SheetPortal>\n );\n};\n\nexport interface SheetHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SheetHeader: Component<SheetHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"flex flex-col space-y-2 text-center sm:text-left\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface SheetFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SheetFooter: Component<SheetFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-4\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface SheetTitleProps {\n class?: string;\n children?: JSX.Element;\n}\n\nexport const SheetTitle: Component<SheetTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <DialogPrimitive.Title\n class={cn(\"text-lg font-semibold text-foreground\", local.class)}\n {...rest}\n >\n {local.children}\n </DialogPrimitive.Title>\n );\n};\n\nexport interface SheetDescriptionProps {\n class?: string;\n children?: JSX.Element;\n}\n\nexport const SheetDescription: Component<SheetDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <DialogPrimitive.Description\n class={cn(\"text-sm text-muted-foreground\", local.class)}\n {...rest}\n >\n {local.children}\n </DialogPrimitive.Description>\n );\n};",
18
+ "content": "import { splitProps, type Component, type JSX, type ValidComponent, Show } from \"solid-js\";\nimport * as DialogPrimitive from \"@kobalte/core/dialog\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-solid\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\n// Global CSS Keyframe Animations specifically designed for Kobalte animationend DOM events\nconst sheetStyles = `\n@keyframes sheet-slide-in-left { from { transform: translateX(-100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-left { from { transform: translateX(0); } to { transform: translateX(-100%); } }\n@keyframes sheet-slide-in-right { from { transform: translateX(100%); } to { transform: translateX(0); } }\n@keyframes sheet-slide-out-right { from { transform: translateX(0); } to { transform: translateX(100%); } }\n@keyframes sheet-slide-in-top { from { transform: translateY(-100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-top { from { transform: translateY(0); } to { transform: translateY(-100%); } }\n@keyframes sheet-slide-in-bottom { from { transform: translateY(100%); } to { transform: translateY(0); } }\n@keyframes sheet-slide-out-bottom { from { transform: translateY(0); } to { transform: translateY(100%); } }\n@keyframes sheet-fade-in { from { opacity: 0; } to { opacity: 1; } }\n@keyframes sheet-fade-out { from { opacity: 1; } to { opacity: 0; } }\n`;\n\n/**\n * CVA variants for CSS keyframe slide animations across 4 edges.\n */\nexport const sheetVariants = cva(\n \"fixed z-50 gap-4 bg-card p-6 text-card-foreground shadow-lg\",\n {\n variants: {\n side: {\n left: \"fixed top-0 bottom-0 left-0 w-3/4 border-r border-border sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-left_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-left_300ms_ease-in-out]\",\n right: \"fixed top-0 bottom-0 right-0 w-3/4 border-l border-border sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-right_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-right_300ms_ease-in-out]\",\n top: \"fixed top-0 left-0 right-0 border-b border-border data-[expanded]:animate-[sheet-slide-in-top_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-top_300ms_ease-in-out]\",\n bottom: \"fixed bottom-0 left-0 right-0 border-t border-border data-[expanded]:animate-[sheet-slide-in-bottom_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-bottom_300ms_ease-in-out]\",\n },\n },\n defaultVariants: {\n side: \"right\",\n },\n }\n);\n\nexport type SheetRootProps = DialogPrimitive.DialogRootProps;\n\nexport const Sheet: Component<SheetRootProps> = (props) => {\n return <DialogPrimitive.Root {...props} />;\n};\n\nexport const SheetTrigger = DialogPrimitive.Trigger;\nexport const SheetClose = DialogPrimitive.CloseButton;\n\nexport interface SheetOverlayProps<T extends ValidComponent = \"div\"> {\n /** Whether to apply background blur effect or keep transparent (default: true) */\n blur?: boolean;\n class?: string;\n}\n\n/**\n * Backdrop overlay wrapper with fade-in and fade-out animations.\n */\nexport const SheetOverlay = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SheetOverlayProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetOverlayProps, [\"class\", \"blur\"]);\n\n return (\n <DialogPrimitive.Overlay\n class={cn(\n \"fixed inset-0 z-50 data-[expanded]:animate-[sheet-fade-in_300ms_ease-in-out] data-[closed]:animate-[sheet-fade-out_300ms_ease-in-out]\",\n local.blur !== false ? \"bg-black/80 backdrop-blur-sm\" : \"bg-transparent\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type SheetContentProps<T extends ValidComponent = \"div\"> =\n DialogPrimitive.DialogContentProps<T> &\n VariantProps<typeof sheetVariants> & {\n /** Direction from which the sheet slides out: top, bottom, left, right */\n side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n /** Whether to display the top-right close (X) button (default: true) */\n showCloseButton?: boolean;\n /** Whether clicking outside closes the sheet (default: true) */\n closeOnOutsideClick?: boolean;\n /** Whether to apply background backdrop blur (default: true) */\n blur?: boolean;\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Main sheet container with smooth CSS keyframe slide animations.\n */\nexport const SheetContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SheetContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetContentProps, [\n \"side\",\n \"showCloseButton\",\n \"closeOnOutsideClick\",\n \"blur\",\n \"class\",\n \"children\",\n \"onPointerDownOutside\",\n \"onInteractOutside\",\n ]);\n\n const side = () => local.side || \"right\";\n\n const handlePointerDownOutside = (e: Event) => {\n if (local.closeOnOutsideClick === false) {\n e.preventDefault();\n }\n if (typeof local.onPointerDownOutside === \"function\") {\n local.onPointerDownOutside(e as any);\n }\n };\n\n const handleInteractOutside = (e: Event) => {\n if (local.closeOnOutsideClick === false) {\n e.preventDefault();\n }\n if (typeof local.onInteractOutside === \"function\") {\n local.onInteractOutside(e as any);\n }\n };\n\n return (\n <DialogPrimitive.Portal>\n <style>{sheetStyles}</style>\n <SheetOverlay blur={local.blur} />\n <DialogPrimitive.Content\n onPointerDownOutside={handlePointerDownOutside}\n onInteractOutside={handleInteractOutside}\n class={cn(sheetVariants({ side: side() }), \"p-0\", local.class)}\n {...(rest as any)}\n >\n <ScrollArea class=\"h-full w-full\">\n <div class=\"p-6 space-y-4\">\n {local.children}\n </div>\n </ScrollArea>\n <Show when={local.showCloseButton !== false}>\n <DialogPrimitive.CloseButton class=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none cursor-pointer z-50\">\n <X class=\"h-4 w-4\" />\n <span class=\"sr-only\">Close</span>\n </DialogPrimitive.CloseButton>\n </Show>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n};\n\nexport interface SheetHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SheetHeader: Component<SheetHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"flex flex-col space-y-2 text-center sm:text-left\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface SheetFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SheetFooter: Component<SheetFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", local.class)}\n {...rest}\n />\n );\n};\n\nexport type SheetTitleProps<T extends ValidComponent = \"h2\"> =\n DialogPrimitive.DialogTitleProps<T> & {\n class?: string;\n };\n\nexport const SheetTitle = <T extends ValidComponent = \"h2\">(\n props: PolymorphicProps<T, SheetTitleProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetTitleProps, [\"class\"]);\n\n return (\n <DialogPrimitive.Title\n class={cn(\"text-lg font-semibold text-foreground\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type SheetDescriptionProps<T extends ValidComponent = \"p\"> =\n DialogPrimitive.DialogDescriptionProps<T> & {\n class?: string;\n };\n\nexport const SheetDescription = <T extends ValidComponent = \"p\">(\n props: PolymorphicProps<T, SheetDescriptionProps<T>>\n) => {\n const [local, rest] = splitProps(props as SheetDescriptionProps, [\"class\"]);\n\n return (\n <DialogPrimitive.Description\n class={cn(\"text-sm text-muted-foreground\", local.class)}\n {...(rest as any)}\n />\n );\n};",
19
19
  "type": "registry:ui"
20
20
  }
21
21
  ]
@@ -17,7 +17,7 @@
17
17
  "files": [
18
18
  {
19
19
  "path": "ui/sidebar.tsx",
20
- "content": "import {\n createContext,\n createSignal,\n createMemo,\n createEffect,\n useContext,\n splitProps,\n onMount,\n onCleanup,\n type Component,\n type JSX,\n type ParentComponent,\n type Accessor,\n Show,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { PanelLeft, PanelRight } from \"lucide-solid\";\nimport { Tooltip, TooltipTrigger, TooltipContent } from \"./tooltip\";\nimport { Skeleton } from \"./skeleton\";\nimport { Separator } from \"./separator\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- Constants --- */\nconst SIDEBAR_COOKIE_NAME = \"nikala_sidebar_state\";\nconst SIDEBAR_WIDTH = \"17rem\";\nconst SIDEBAR_WIDTH_ICON = \"3.5rem\";\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\n\n/* --- 1. Context & Types --- */\nexport type SidebarState = \"expanded\" | \"collapsed\";\nexport type SidebarSide = \"left\" | \"right\";\n\nexport interface SidebarContextValue {\n state: Accessor<SidebarState>;\n open: Accessor<boolean>;\n setOpen: (open: boolean) => void;\n openMobile: Accessor<boolean>;\n setOpenMobile: (open: boolean) => void;\n isMobile: Accessor<boolean>;\n toggleSidebar: () => void;\n side: Accessor<SidebarSide>;\n setSide: (side: SidebarSide) => void;\n}\n\nconst SidebarContext = createContext<SidebarContextValue>();\n\nconst fallbackSidebarContext: SidebarContextValue = {\n state: () => \"expanded\",\n open: () => true,\n setOpen: () => {},\n openMobile: () => false,\n setOpenMobile: () => {},\n isMobile: () => false,\n toggleSidebar: () => {},\n side: () => \"left\",\n setSide: () => {},\n};\n\nexport function useSidebar() {\n const context = useContext(SidebarContext);\n return context ?? fallbackSidebarContext;\n}\n\n/* --- 2. SidebarProvider --- */\nexport interface SidebarProviderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n defaultOpen?: boolean;\n open?: Accessor<boolean>;\n onOpenChange?: (open: boolean) => void;\n side?: SidebarSide;\n class?: string;\n style?: JSX.CSSProperties;\n}\n\nexport const SidebarProvider: ParentComponent<SidebarProviderProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"defaultOpen\",\n \"open\",\n \"onOpenChange\",\n \"side\",\n \"class\",\n \"style\",\n \"children\",\n ]);\n\n const [internalOpen, setInternalOpen] = createSignal<boolean>(local.defaultOpen ?? true);\n const [openMobile, setOpenMobile] = createSignal<boolean>(false);\n const [isMobile, setIsMobile] = createSignal<boolean>(false);\n const [side, setSide] = createSignal<SidebarSide>(local.side ?? \"left\");\n\n createEffect(() => {\n if (local.side) {\n setSide(local.side);\n }\n });\n\n const open = () => (local.open !== undefined ? local.open() : internalOpen());\n\n const setOpen = (value: boolean) => {\n if (local.open === undefined) {\n setInternalOpen(value);\n }\n local.onOpenChange?.(value);\n\n if (typeof document !== \"undefined\") {\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${value}; path=/; max-age=${60 * 60 * 24 * 7}`;\n }\n };\n\n const toggleSidebar = () => {\n setOpen(!open());\n };\n\n const state = createMemo<SidebarState>(() => (open() ? \"expanded\" : \"collapsed\"));\n\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const checkMobile = () => {\n setIsMobile(window.innerWidth < 768);\n };\n\n checkMobile();\n window.addEventListener(\"resize\", checkMobile, { passive: true });\n\n // Keyboard shortcut (⌘B / Ctrl+B)\n const handleKeyDown = (e: KeyboardEvent) => {\n if (\n (e.metaKey || e.ctrlKey) &&\n e.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT &&\n !e.defaultPrevented\n ) {\n e.preventDefault();\n toggleSidebar();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n\n onCleanup(() => {\n window.removeEventListener(\"resize\", checkMobile);\n window.removeEventListener(\"keydown\", handleKeyDown);\n });\n });\n\n const contextValue: SidebarContextValue = {\n state,\n open,\n setOpen,\n openMobile,\n setOpenMobile,\n isMobile,\n toggleSidebar,\n side,\n setSide,\n };\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <div\n style={{\n \"--sidebar-width\": SIDEBAR_WIDTH,\n \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n ...(typeof local.style === \"object\" ? local.style : {}),\n } as JSX.CSSProperties}\n class={cn(\n \"group/sidebar-wrapper relative flex w-full max-w-full\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </SidebarContext.Provider>\n );\n};\n\n/* --- 3. Sidebar Root --- */\nexport interface SidebarProps extends JSX.HTMLAttributes<HTMLDivElement> {\n side?: SidebarSide;\n variant?: \"sidebar\" | \"floating\" | \"inset\";\n collapsible?: \"offcanvas\" | \"icon\" | \"none\";\n class?: string;\n}\n\nexport const Sidebar: ParentComponent<SidebarProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"side\",\n \"variant\",\n \"collapsible\",\n \"class\",\n \"children\",\n ]);\n\n const sidebar = useSidebar();\n const side = () => local.side ?? sidebar.side();\n const variant = () => local.variant ?? \"sidebar\";\n const collapsible = () => local.collapsible ?? \"icon\";\n\n createEffect(() => {\n if (local.side) {\n sidebar.setSide(local.side);\n }\n });\n\n const isCollapsed = () => sidebar.state() === \"collapsed\" && collapsible() !== \"none\";\n\n return (\n <div\n data-state={isCollapsed() ? \"collapsed\" : \"expanded\"}\n data-collapsible={isCollapsed() ? collapsible() : \"\"}\n data-variant={variant()}\n data-side={side()}\n class={cn(\n \"group relative flex flex-col bg-card text-card-foreground transition-all duration-200 ease-in-out shrink-0\",\n !isCollapsed()\n ? \"w-[var(--sidebar-width,17rem)]\"\n : collapsible() === \"icon\"\n ? \"w-[var(--sidebar-width-icon,3.5rem)]\"\n : \"w-0 overflow-hidden opacity-0 border-none\",\n // Side borders\n variant() === \"sidebar\" && (side() === \"left\" ? \"border-r border-border\" : \"border-l border-border\"),\n // Floating variant\n variant() === \"floating\" && \"m-2 rounded-lg border border-border shadow-md\",\n // Inset variant\n variant() === \"inset\" && \"m-2 rounded-lg border border-border/80 bg-card shadow-2xs\",\n local.class\n )}\n {...rest}\n >\n <div\n data-sidebar=\"sidebar\"\n class=\"flex h-full w-full flex-col overflow-hidden\"\n >\n {local.children}\n </div>\n </div>\n );\n};\n\n/* --- 4. SidebarTrigger --- */\nexport interface SidebarTriggerProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const SidebarTrigger: Component<SidebarTriggerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"onClick\"]);\n const { toggleSidebar, side } = useSidebar();\n\n return (\n <button\n type=\"button\"\n aria-label=\"Toggle Sidebar\"\n onClick={(e) => {\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n toggleSidebar();\n }}\n class={cn(\n \"inline-flex size-7 items-center justify-center rounded-md border border-border/60 bg-background text-foreground hover:bg-muted focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring cursor-pointer transition-colors shadow-2xs\",\n local.class\n )}\n {...rest}\n >\n <Show when={side() === \"right\"} fallback={<PanelLeft class=\"size-4\" />}>\n <PanelRight class=\"size-4\" />\n </Show>\n <span class=\"sr-only\">Toggle Sidebar</span>\n </button>\n );\n};\n\n/* --- 5. SidebarRail --- */\nexport interface SidebarRailProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const SidebarRail: Component<SidebarRailProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n const { toggleSidebar } = useSidebar();\n\n return (\n <button\n type=\"button\"\n data-sidebar=\"rail\"\n aria-label=\"Toggle Sidebar Rail\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n class={cn(\n \"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex cursor-w-resize\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\n/* --- 6. SidebarInset --- */\nexport interface SidebarInsetProps extends JSX.HTMLAttributes<HTMLElement> {\n class?: string;\n}\n\nexport const SidebarInset: ParentComponent<SidebarInsetProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <main\n class={cn(\n \"relative flex min-h-0 flex-1 flex-col bg-background\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </main>\n );\n};\n\n/* --- 7. Sidebar Structural Sections --- */\nexport interface SidebarSectionProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SidebarHeader: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"header\"\n class={cn(\"flex flex-col gap-2 p-3 border-b border-border/40 shrink-0 group-data-[collapsible=icon]:p-2 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport const SidebarFooter: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"footer\"\n class={cn(\"flex flex-col gap-2 p-3 mt-auto border-t border-border/40 shrink-0 group-data-[collapsible=icon]:p-2 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport const SidebarSeparator: Component<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <Separator\n data-sidebar=\"separator\"\n class={cn(\"mx-2 w-auto bg-border/50 my-1 group-data-[collapsible=icon]:mx-1\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const SidebarContent: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"content\"\n class={cn(\n \"flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto p-2 group-data-[collapsible=icon]:p-1 group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon]:items-center\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 8. Sidebar Groups --- */\nexport const SidebarGroup: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"group\"\n class={cn(\"relative flex w-full min-w-0 flex-col p-1 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface SidebarGroupLabelProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SidebarGroupLabel: ParentComponent<SidebarGroupLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"group-label\"\n class={cn(\n \"flex h-7 shrink-0 items-center rounded-md px-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground outline-hidden transition-all duration-200 ease-linear group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface SidebarGroupActionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const SidebarGroupAction: ParentComponent<SidebarGroupActionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n data-sidebar=\"group-action\"\n class={cn(\n \"absolute right-3 top-3 flex size-5 items-center justify-center rounded-md p-0 text-muted-foreground outline-hidden transition-transform hover:bg-muted hover:text-foreground cursor-pointer group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n\nexport const SidebarGroupContent: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"group-content\"\n class={cn(\"w-full text-sm\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 9. Sidebar Menu --- */\nexport interface SidebarMenuProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const SidebarMenu: ParentComponent<SidebarMenuProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul\n data-sidebar=\"menu\"\n class={cn(\"flex w-full min-w-0 flex-col gap-1 list-none p-0 m-0 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </ul>\n );\n};\n\nexport interface SidebarMenuItemProps extends JSX.HTMLAttributes<HTMLLIElement> {\n class?: string;\n}\n\nexport const SidebarMenuItem: ParentComponent<SidebarMenuItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li\n data-sidebar=\"menu-item\"\n class={cn(\"group/menu-item relative flex w-full justify-center\", local.class)}\n {...rest}\n >\n {local.children}\n </li>\n );\n};\n\n/* --- 10. SidebarMenuButton --- */\nexport const sidebarMenuButtonVariants = cva(\n \"peer/menu-button flex w-full items-center gap-2.5 overflow-hidden rounded-md p-2 text-left text-sm font-medium outline-hidden ring-sidebar-ring transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 active:bg-muted disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-muted data-[active=true]:font-semibold data-[active=true]:text-foreground group-data-[collapsible=icon]:size-9 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:[&>span]:hidden cursor-pointer select-none\",\n {\n variants: {\n variant: {\n default: \"hover:bg-muted hover:text-foreground\",\n outline: \"bg-background shadow-2xs hover:bg-muted hover:text-foreground border border-border\",\n },\n size: {\n default: \"h-8 text-sm\",\n sm: \"h-7 text-xs\",\n lg: \"h-10 text-sm\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport interface SidebarMenuButtonProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof sidebarMenuButtonVariants> {\n isActive?: boolean;\n tooltip?: string;\n class?: string;\n}\n\nexport const SidebarMenuButton: ParentComponent<SidebarMenuButtonProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"isActive\",\n \"tooltip\",\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n ]);\n\n const sidebar = useSidebar();\n const isCollapsed = () => sidebar.state() === \"collapsed\";\n const tooltipPlacement = () => (sidebar.side() === \"right\" ? \"left\" : \"right\");\n\n const buttonElement = () => (\n <button\n type=\"button\"\n data-sidebar=\"menu-button\"\n data-size={local.size}\n data-active={local.isActive ? \"true\" : \"false\"}\n class={cn(\n sidebarMenuButtonVariants({ variant: local.variant, size: local.size }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n\n return (\n <Show\n when={local.tooltip && isCollapsed() && !sidebar.isMobile()}\n fallback={buttonElement()}\n >\n <Tooltip placement={tooltipPlacement()} gutter={8} openDelay={100}>\n <TooltipTrigger as=\"div\" class=\"w-full flex items-center justify-center\">\n {buttonElement()}\n </TooltipTrigger>\n <TooltipContent>\n {local.tooltip}\n </TooltipContent>\n </Tooltip>\n </Show>\n );\n};\n\n/* --- 11. SidebarMenuAction & Badge --- */\nexport interface SidebarMenuActionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n showOnHover?: boolean;\n class?: string;\n}\n\nexport const SidebarMenuAction: ParentComponent<SidebarMenuActionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"showOnHover\", \"class\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n data-sidebar=\"menu-action\"\n class={cn(\n \"absolute right-1 top-1.5 flex size-5 items-center justify-center rounded-md p-0 text-muted-foreground outline-hidden transition-transform hover:bg-muted hover:text-foreground cursor-pointer group-data-[collapsible=icon]:hidden\",\n local.showOnHover && \"opacity-0 group-hover/menu-item:opacity-100 transition-opacity\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n\nexport interface SidebarMenuBadgeProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SidebarMenuBadge: ParentComponent<SidebarMenuBadgeProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"menu-badge\"\n class={cn(\n \"pointer-events-none absolute right-1.5 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1.5 text-[11px] font-medium tabular-nums bg-muted text-muted-foreground group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface SidebarMenuSkeletonProps extends JSX.HTMLAttributes<HTMLDivElement> {\n showIcon?: boolean;\n class?: string;\n}\n\nexport const SidebarMenuSkeleton: Component<SidebarMenuSkeletonProps> = (props) => {\n const [local, rest] = splitProps(props, [\"showIcon\", \"class\"]);\n\n return (\n <div\n data-sidebar=\"menu-skeleton\"\n class={cn(\"flex h-8 items-center gap-2 rounded-md px-2 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:justify-center\", local.class)}\n {...rest}\n >\n <Show when={local.showIcon ?? true}>\n <Skeleton class=\"size-4 rounded-md shrink-0\" />\n </Show>\n <Skeleton class=\"h-4 flex-1 max-w-[80%] group-data-[collapsible=icon]:hidden\" />\n </div>\n );\n};\n\n/* --- 12. Nested Submenus --- */\nexport interface SidebarMenuSubProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const SidebarMenuSub: ParentComponent<SidebarMenuSubProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul\n data-sidebar=\"menu-sub\"\n class={cn(\n \"mx-3.5 flex min-w-0 flex-col gap-1 border-l border-border/60 px-2.5 py-0.5 list-none group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </ul>\n );\n};\n\nexport const SidebarMenuSubItem: ParentComponent<SidebarMenuItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li class={cn(\"relative\", local.class)} {...rest}>\n {local.children}\n </li>\n );\n};\n\nexport interface SidebarMenuSubButtonProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n isActive?: boolean;\n size?: \"sm\" | \"md\";\n class?: string;\n}\n\nexport const SidebarMenuSubButton: ParentComponent<SidebarMenuSubButtonProps> = (props) => {\n const [local, rest] = splitProps(props, [\"isActive\", \"size\", \"class\", \"children\"]);\n\n return (\n <a\n data-sidebar=\"menu-sub-button\"\n data-size={local.size ?? \"md\"}\n data-active={local.isActive ? \"true\" : \"false\"}\n class={cn(\n \"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-xs text-muted-foreground outline-hidden hover:bg-muted hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring data-[active=true]:bg-muted data-[active=true]:font-medium data-[active=true]:text-foreground\",\n local.size === \"sm\" && \"text-[11px]\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n );\n};\n",
20
+ "content": "import {\n createContext,\n createSignal,\n createMemo,\n createEffect,\n useContext,\n splitProps,\n onMount,\n onCleanup,\n type Component,\n type JSX,\n type ParentComponent,\n type Accessor,\n Show,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { PanelLeft, PanelRight } from \"lucide-solid\";\nimport { Tooltip, TooltipTrigger, TooltipContent } from \"@/components/ui/tooltip\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- Constants --- */\nconst SIDEBAR_COOKIE_NAME = \"nikala_sidebar_state\";\nconst SIDEBAR_WIDTH = \"17rem\";\nconst SIDEBAR_WIDTH_ICON = \"3.5rem\";\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\n\n/* --- 1. Context & Types --- */\nexport type SidebarState = \"expanded\" | \"collapsed\";\nexport type SidebarSide = \"left\" | \"right\";\n\nexport interface SidebarContextValue {\n state: Accessor<SidebarState>;\n open: Accessor<boolean>;\n setOpen: (open: boolean) => void;\n openMobile: Accessor<boolean>;\n setOpenMobile: (open: boolean) => void;\n isMobile: Accessor<boolean>;\n toggleSidebar: () => void;\n side: Accessor<SidebarSide>;\n setSide: (side: SidebarSide) => void;\n}\n\nconst SidebarContext = createContext<SidebarContextValue>();\n\nconst fallbackSidebarContext: SidebarContextValue = {\n state: () => \"expanded\",\n open: () => true,\n setOpen: () => {},\n openMobile: () => false,\n setOpenMobile: () => {},\n isMobile: () => false,\n toggleSidebar: () => {},\n side: () => \"left\",\n setSide: () => {},\n};\n\nexport function useSidebar() {\n const context = useContext(SidebarContext);\n return context ?? fallbackSidebarContext;\n}\n\n/* --- 2. SidebarProvider --- */\nexport interface SidebarProviderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n defaultOpen?: boolean;\n open?: Accessor<boolean>;\n onOpenChange?: (open: boolean) => void;\n side?: SidebarSide;\n class?: string;\n style?: JSX.CSSProperties;\n}\n\nexport const SidebarProvider: ParentComponent<SidebarProviderProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"defaultOpen\",\n \"open\",\n \"onOpenChange\",\n \"side\",\n \"class\",\n \"style\",\n \"children\",\n ]);\n\n const [internalOpen, setInternalOpen] = createSignal<boolean>(local.defaultOpen ?? true);\n const [openMobile, setOpenMobile] = createSignal<boolean>(false);\n const [isMobile, setIsMobile] = createSignal<boolean>(false);\n const [side, setSide] = createSignal<SidebarSide>(local.side ?? \"left\");\n\n createEffect(() => {\n if (local.side) {\n setSide(local.side);\n }\n });\n\n const open = () => (local.open !== undefined ? local.open() : internalOpen());\n\n const setOpen = (value: boolean) => {\n if (local.open === undefined) {\n setInternalOpen(value);\n }\n local.onOpenChange?.(value);\n\n if (typeof document !== \"undefined\") {\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${value}; path=/; max-age=${60 * 60 * 24 * 7}`;\n }\n };\n\n const toggleSidebar = () => {\n setOpen(!open());\n };\n\n const state = createMemo<SidebarState>(() => (open() ? \"expanded\" : \"collapsed\"));\n\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const checkMobile = () => {\n setIsMobile(window.innerWidth < 768);\n };\n\n checkMobile();\n window.addEventListener(\"resize\", checkMobile, { passive: true });\n\n // Keyboard shortcut (⌘B / Ctrl+B)\n const handleKeyDown = (e: KeyboardEvent) => {\n if (\n (e.metaKey || e.ctrlKey) &&\n e.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT &&\n !e.defaultPrevented\n ) {\n e.preventDefault();\n toggleSidebar();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n\n onCleanup(() => {\n window.removeEventListener(\"resize\", checkMobile);\n window.removeEventListener(\"keydown\", handleKeyDown);\n });\n });\n\n const contextValue: SidebarContextValue = {\n state,\n open,\n setOpen,\n openMobile,\n setOpenMobile,\n isMobile,\n toggleSidebar,\n side,\n setSide,\n };\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <div\n style={{\n \"--sidebar-width\": SIDEBAR_WIDTH,\n \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n ...(typeof local.style === \"object\" ? local.style : {}),\n } as JSX.CSSProperties}\n class={cn(\n \"group/sidebar-wrapper relative flex w-full max-w-full\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </SidebarContext.Provider>\n );\n};\n\n/* --- 3. Sidebar Root --- */\nexport interface SidebarProps extends JSX.HTMLAttributes<HTMLDivElement> {\n side?: SidebarSide;\n variant?: \"sidebar\" | \"floating\" | \"inset\";\n collapsible?: \"offcanvas\" | \"icon\" | \"none\";\n class?: string;\n}\n\nexport const Sidebar: ParentComponent<SidebarProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"side\",\n \"variant\",\n \"collapsible\",\n \"class\",\n \"children\",\n ]);\n\n const sidebar = useSidebar();\n const side = () => local.side ?? sidebar.side();\n const variant = () => local.variant ?? \"sidebar\";\n const collapsible = () => local.collapsible ?? \"icon\";\n\n createEffect(() => {\n if (local.side) {\n sidebar.setSide(local.side);\n }\n });\n\n const isCollapsed = () => sidebar.state() === \"collapsed\" && collapsible() !== \"none\";\n\n return (\n <div\n data-state={isCollapsed() ? \"collapsed\" : \"expanded\"}\n data-collapsible={isCollapsed() ? collapsible() : \"\"}\n data-variant={variant()}\n data-side={side()}\n class={cn(\n \"group relative flex flex-col bg-card text-card-foreground transition-[width,opacity] duration-200 ease-in-out shrink-0\",\n !isCollapsed()\n ? \"w-[var(--sidebar-width,17rem)]\"\n : collapsible() === \"icon\"\n ? \"w-[var(--sidebar-width-icon,3.5rem)]\"\n : \"w-0 overflow-hidden opacity-0 border-none\",\n // Side borders\n variant() === \"sidebar\" && (side() === \"left\" ? \"border-r border-border\" : \"border-l border-border\"),\n // Floating variant\n variant() === \"floating\" && \"m-2 rounded-lg border border-border shadow-md\",\n // Inset variant\n variant() === \"inset\" && \"m-2 rounded-lg border border-border/80 bg-card shadow-2xs\",\n local.class\n )}\n {...rest}\n >\n <div\n data-sidebar=\"sidebar\"\n class=\"flex h-full w-full flex-col overflow-hidden\"\n >\n {local.children}\n </div>\n </div>\n );\n};\n\n/* --- 4. SidebarTrigger --- */\nexport interface SidebarTriggerProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const SidebarTrigger: Component<SidebarTriggerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"onClick\"]);\n const { toggleSidebar, side } = useSidebar();\n\n return (\n <button\n type=\"button\"\n aria-label=\"Toggle Sidebar\"\n onClick={(e) => {\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n toggleSidebar();\n }}\n class={cn(\n \"inline-flex size-7 items-center justify-center rounded-md border border-border/60 bg-background text-foreground hover:bg-muted focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring cursor-pointer transition-colors shadow-2xs\",\n local.class\n )}\n {...rest}\n >\n <Show when={side() === \"right\"} fallback={<PanelLeft class=\"size-4\" />}>\n <PanelRight class=\"size-4\" />\n </Show>\n <span class=\"sr-only\">Toggle Sidebar</span>\n </button>\n );\n};\n\n/* --- 5. SidebarRail --- */\nexport interface SidebarRailProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const SidebarRail: Component<SidebarRailProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n const { toggleSidebar } = useSidebar();\n\n return (\n <button\n type=\"button\"\n data-sidebar=\"rail\"\n aria-label=\"Toggle Sidebar Rail\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n class={cn(\n \"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex cursor-w-resize\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\n/* --- 6. SidebarInset --- */\nexport interface SidebarInsetProps extends JSX.HTMLAttributes<HTMLElement> {\n class?: string;\n}\n\nexport const SidebarInset: ParentComponent<SidebarInsetProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <main\n class={cn(\n \"relative flex min-h-0 flex-1 flex-col bg-background\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </main>\n );\n};\n\n/* --- 7. Sidebar Structural Sections --- */\nexport interface SidebarSectionProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SidebarHeader: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"header\"\n class={cn(\"flex flex-col gap-2 p-3 border-b border-border/40 shrink-0 group-data-[collapsible=icon]:p-2 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport const SidebarFooter: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"footer\"\n class={cn(\"flex flex-col gap-2 p-3 mt-auto border-t border-border/40 shrink-0 group-data-[collapsible=icon]:p-2 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport const SidebarSeparator: Component<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <Separator\n data-sidebar=\"separator\"\n class={cn(\"mx-2 w-auto bg-border/50 my-1 group-data-[collapsible=icon]:mx-1\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const SidebarContent: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"content\"\n class={cn(\n \"flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto p-2 group-data-[collapsible=icon]:p-1 group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon]:items-center\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 8. Sidebar Groups --- */\nexport const SidebarGroup: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"group\"\n class={cn(\"relative flex w-full min-w-0 flex-col p-1 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface SidebarGroupLabelProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SidebarGroupLabel: ParentComponent<SidebarGroupLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"group-label\"\n class={cn(\n \"flex h-7 shrink-0 items-center rounded-md px-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground outline-hidden transition-all duration-200 ease-linear group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface SidebarGroupActionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const SidebarGroupAction: ParentComponent<SidebarGroupActionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n data-sidebar=\"group-action\"\n class={cn(\n \"absolute right-3 top-3 flex size-5 items-center justify-center rounded-md p-0 text-muted-foreground outline-hidden transition-transform hover:bg-muted hover:text-foreground cursor-pointer group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n\nexport const SidebarGroupContent: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"group-content\"\n class={cn(\"w-full text-sm\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 9. Sidebar Menu --- */\nexport interface SidebarMenuProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const SidebarMenu: ParentComponent<SidebarMenuProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul\n data-sidebar=\"menu\"\n class={cn(\"flex w-full min-w-0 flex-col gap-1 list-none p-0 m-0 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </ul>\n );\n};\n\nexport interface SidebarMenuItemProps extends JSX.HTMLAttributes<HTMLLIElement> {\n class?: string;\n}\n\nexport const SidebarMenuItem: ParentComponent<SidebarMenuItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li\n data-sidebar=\"menu-item\"\n class={cn(\"group/menu-item relative flex w-full justify-center\", local.class)}\n {...rest}\n >\n {local.children}\n </li>\n );\n};\n\n/* --- 10. SidebarMenuButton --- */\nexport const sidebarMenuButtonVariants = cva(\n \"peer/menu-button flex w-full items-center gap-2.5 overflow-hidden rounded-md p-2 text-left text-sm font-medium outline-hidden ring-sidebar-ring transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 active:bg-muted disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-muted data-[active=true]:font-semibold data-[active=true]:text-foreground group-data-[collapsible=icon]:size-9 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:[&>span]:hidden cursor-pointer select-none\",\n {\n variants: {\n variant: {\n default: \"hover:bg-muted hover:text-foreground\",\n outline: \"bg-background shadow-2xs hover:bg-muted hover:text-foreground border border-border\",\n },\n size: {\n default: \"h-8 text-sm\",\n sm: \"h-7 text-xs\",\n lg: \"h-10 text-sm\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport interface SidebarMenuButtonProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof sidebarMenuButtonVariants> {\n isActive?: boolean;\n tooltip?: string;\n class?: string;\n}\n\nexport const SidebarMenuButton: ParentComponent<SidebarMenuButtonProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"isActive\",\n \"tooltip\",\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n ]);\n\n const sidebar = useSidebar();\n const isCollapsed = () => sidebar.state() === \"collapsed\";\n const tooltipPlacement = () => (sidebar.side() === \"right\" ? \"left\" : \"right\");\n\n const buttonElement = () => (\n <button\n type=\"button\"\n data-sidebar=\"menu-button\"\n data-size={local.size}\n data-active={local.isActive ? \"true\" : \"false\"}\n class={cn(\n sidebarMenuButtonVariants({ variant: local.variant, size: local.size }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n\n return (\n <Show\n when={local.tooltip && isCollapsed() && !sidebar.isMobile()}\n fallback={buttonElement()}\n >\n <Tooltip placement={tooltipPlacement()} gutter={8} openDelay={100}>\n <TooltipTrigger as=\"div\" class=\"w-full flex items-center justify-center\">\n {buttonElement()}\n </TooltipTrigger>\n <TooltipContent>\n {local.tooltip}\n </TooltipContent>\n </Tooltip>\n </Show>\n );\n};\n\n/* --- 11. SidebarMenuAction & Badge --- */\nexport interface SidebarMenuActionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n showOnHover?: boolean;\n class?: string;\n}\n\nexport const SidebarMenuAction: ParentComponent<SidebarMenuActionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"showOnHover\", \"class\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n data-sidebar=\"menu-action\"\n class={cn(\n \"absolute right-1 top-1.5 flex size-5 items-center justify-center rounded-md p-0 text-muted-foreground outline-hidden transition-transform hover:bg-muted hover:text-foreground cursor-pointer group-data-[collapsible=icon]:hidden\",\n local.showOnHover && \"opacity-0 group-hover/menu-item:opacity-100 transition-opacity\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n\nexport interface SidebarMenuBadgeProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SidebarMenuBadge: ParentComponent<SidebarMenuBadgeProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"menu-badge\"\n class={cn(\n \"pointer-events-none absolute right-1.5 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1.5 text-[11px] font-medium tabular-nums bg-muted text-muted-foreground group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface SidebarMenuSkeletonProps extends JSX.HTMLAttributes<HTMLDivElement> {\n showIcon?: boolean;\n class?: string;\n}\n\nexport const SidebarMenuSkeleton: Component<SidebarMenuSkeletonProps> = (props) => {\n const [local, rest] = splitProps(props, [\"showIcon\", \"class\"]);\n\n return (\n <div\n data-sidebar=\"menu-skeleton\"\n class={cn(\"flex h-8 items-center gap-2 rounded-md px-2 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:justify-center\", local.class)}\n {...rest}\n >\n <Show when={local.showIcon ?? true}>\n <Skeleton class=\"size-4 rounded-md shrink-0\" />\n </Show>\n <Skeleton class=\"h-4 flex-1 max-w-[80%] group-data-[collapsible=icon]:hidden\" />\n </div>\n );\n};\n\n/* --- 12. Nested Submenus --- */\nexport interface SidebarMenuSubProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const SidebarMenuSub: ParentComponent<SidebarMenuSubProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul\n data-sidebar=\"menu-sub\"\n class={cn(\n \"mx-3.5 flex min-w-0 flex-col gap-1 border-l border-border/60 px-2.5 py-0.5 list-none group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </ul>\n );\n};\n\nexport const SidebarMenuSubItem: ParentComponent<SidebarMenuItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li class={cn(\"relative\", local.class)} {...rest}>\n {local.children}\n </li>\n );\n};\n\nexport interface SidebarMenuSubButtonProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n isActive?: boolean;\n size?: \"sm\" | \"md\";\n class?: string;\n}\n\nexport const SidebarMenuSubButton: ParentComponent<SidebarMenuSubButtonProps> = (props) => {\n const [local, rest] = splitProps(props, [\"isActive\", \"size\", \"class\", \"children\"]);\n\n return (\n <a\n data-sidebar=\"menu-sub-button\"\n data-size={local.size ?? \"md\"}\n data-active={local.isActive ? \"true\" : \"false\"}\n class={cn(\n \"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-xs text-muted-foreground outline-hidden hover:bg-muted hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring data-[active=true]:bg-muted data-[active=true]:font-medium data-[active=true]:text-foreground\",\n local.size === \"sm\" && \"text-[11px]\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n );\n};\n",
21
21
  "type": "registry:ui"
22
22
  }
23
23
  ]
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "ui/status.tsx",
14
- "content": "import { Show, splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const statusVariants = cva(\n \"inline-flex items-center gap-2 text-sm font-medium text-foreground\",\n {\n variants: {\n variant: {\n neutral: \"text-muted-foreground\",\n success: \"text-emerald-700 dark:text-emerald-400\",\n warning: \"text-amber-700 dark:text-amber-400\",\n error: \"text-destructive\",\n info: \"text-blue-700 dark:text-blue-400\",\n },\n size: {\n sm: \"text-xs gap-1.5\",\n default: \"text-sm gap-2\",\n },\n bordered: {\n true: \"rounded-md border px-2 py-1\",\n false: \"\",\n },\n borderVariant: {\n neutral: \"border-muted-foreground/30\",\n success: \"border-emerald-500/30\",\n warning: \"border-amber-500/30\",\n error: \"border-destructive/30\",\n info: \"border-blue-500/30\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n size: \"default\",\n bordered: false,\n borderVariant: \"neutral\",\n },\n }\n);\n\nexport const statusDotVariants = cva(\"size-2 shrink-0 rounded-lg\", {\n variants: {\n variant: {\n neutral: \"bg-muted-foreground\",\n success: \"bg-emerald-500\",\n warning: \"bg-amber-500\",\n error: \"bg-destructive\",\n info: \"bg-blue-500\",\n },\n size: {\n sm: \"size-1.5\",\n default: \"size-2\",\n },\n animation: {\n none: \"\",\n pulse: \"animate-pulse\",\n ping: \"\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n size: \"default\",\n animation: \"none\",\n },\n});\n\nexport interface StatusProps\n extends JSX.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof statusVariants> {\n class?: string;\n animation?: \"none\" | \"pulse\" | \"ping\";\n}\n\n/** A compact status indicator combining a semantic color dot and label. */\nexport const Status: Component<StatusProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n \"animation\",\n \"bordered\",\n ]);\n\n return (\n <span\n role=\"status\"\n class={cn(\n statusVariants({\n variant: local.variant,\n size: local.size,\n bordered: local.bordered,\n borderVariant: local.variant,\n }),\n local.class\n )}\n {...rest}\n >\n <span class=\"relative flex shrink-0\" aria-hidden=\"true\">\n <Show when={local.animation === \"ping\"}>\n <span\n class={cn(\n \"absolute inline-flex size-full animate-ping rounded-lg opacity-75\",\n statusDotVariants({ variant: local.variant, size: local.size })\n )}\n />\n </Show>\n <span\n class={statusDotVariants({\n variant: local.variant,\n size: local.size,\n animation: local.animation === \"ping\" ? \"none\" : local.animation,\n })}\n />\n </span>\n {local.children}\n </span>\n );\n};\n",
14
+ "content": "import { Show, splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const statusVariants = cva(\n \"inline-flex items-center gap-2 text-sm font-medium text-foreground\",\n {\n variants: {\n variant: {\n neutral: \"text-muted-foreground\",\n success: \"text-emerald-700 dark:text-emerald-400\",\n warning: \"text-amber-700 dark:text-amber-400\",\n error: \"text-destructive\",\n info: \"text-blue-700 dark:text-blue-400\",\n },\n size: {\n sm: \"text-xs gap-1.5\",\n default: \"text-sm gap-2\",\n },\n bordered: {\n true: \"rounded-md border px-2 py-1\",\n false: \"\",\n },\n borderVariant: {\n neutral: \"border-muted-foreground/30\",\n success: \"border-emerald-500/30\",\n warning: \"border-amber-500/30\",\n error: \"border-destructive/30\",\n info: \"border-blue-500/30\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n size: \"default\",\n bordered: false,\n borderVariant: \"neutral\",\n },\n }\n);\n\nexport const statusDotVariants = cva(\"size-2 shrink-0 rounded-lg\", {\n variants: {\n variant: {\n neutral: \"bg-muted-foreground\",\n success: \"bg-emerald-500\",\n warning: \"bg-amber-500\",\n error: \"bg-destructive\",\n info: \"bg-blue-500\",\n },\n size: {\n sm: \"size-1.5\",\n default: \"size-2\",\n },\n animation: {\n none: \"\",\n pulse: \"animate-pulse\",\n ping: \"\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n size: \"default\",\n animation: \"none\",\n },\n});\n\nexport interface StatusProps\n extends JSX.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof statusVariants> {\n class?: string;\n animation?: \"none\" | \"pulse\" | \"ping\";\n}\n\n/** A compact status indicator combining a semantic color dot and label. */\nexport const Status: Component<StatusProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n \"animation\",\n \"bordered\",\n ]);\n\n return (\n <span\n role=\"status\"\n class={cn(\n statusVariants({\n variant: local.variant,\n size: local.size,\n bordered: local.bordered,\n borderVariant: local.variant,\n }),\n local.class\n )}\n {...rest}\n >\n <span class=\"relative flex shrink-0\" aria-hidden=\"true\">\n <Show when={local.animation === \"ping\"}>\n <span\n class={cn(\n \"absolute inline-flex size-full animate-ping rounded-lg opacity-75\",\n statusDotVariants({ variant: local.variant, size: local.size })\n )}\n />\n </Show>\n <span\n class={cn(\n statusDotVariants({\n variant: local.variant,\n size: local.size,\n animation: local.animation === \"ping\" ? \"none\" : local.animation,\n })\n )}\n />\n </span>\n {local.children}\n </span>\n );\n};\n",
15
15
  "type": "registry:ui"
16
16
  }
17
17
  ]
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "steps",
3
+ "title": "Steps",
4
+ "description": "A vertical multi-step instruction container with connecting progress line and numbered step indicators.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "ui/steps.tsx",
16
+ "content": "import {\n createContext,\n useContext,\n splitProps,\n Show,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport { Badge, type BadgeProps } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/cn\";\n\nexport type StepVariant =\n | \"default\"\n | \"primary\"\n | \"info\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"secondary\"\n | \"outline\"\n | \"destructive\"\n | \"purple\";\n\nfunction toBadgeVariant(variant?: StepVariant): NonNullable<BadgeProps[\"variant\"]> {\n switch (variant) {\n case \"primary\":\n return \"default\";\n case \"danger\":\n return \"destructive\";\n case \"default\":\n return \"outline\";\n case \"info\":\n return \"info\";\n case \"success\":\n return \"success\";\n case \"warning\":\n return \"warning\";\n case \"secondary\":\n return \"secondary\";\n default:\n return \"outline\";\n }\n}\nexport type StepOrientation = \"horizontal\" | \"vertical\";\n\ninterface StepsContextValue {\n variant?: StepVariant;\n orientation?: () => StepOrientation;\n}\n\nconst StepsContext = createContext<StepsContextValue>();\n\nexport interface StepsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Layout orientation: vertical (default) or horizontal */\n orientation?: StepOrientation;\n /** Default color variant for all steps in this container */\n variant?: StepVariant;\n class?: string;\n}\n\n/**\n * Multi-step instruction and stepper container supporting vertical and horizontal layouts.\n */\nexport const Steps: ParentComponent<StepsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"orientation\", \"variant\", \"class\", \"children\"]);\n\n const orientation = () => local.orientation || \"vertical\";\n\n return (\n <StepsContext.Provider value={{ variant: local.variant, orientation }}>\n <div\n class={cn(\n \"relative my-6 [counter-reset:step]\",\n orientation() === \"horizontal\"\n ? \"flex flex-row items-start justify-between w-full\"\n : \"ml-4 border-l border-border/80 pl-8 space-y-8\",\n local.class\n )}\n {...rest}\n >\n {/* Single continuous horizontal rail in parent container */}\n <Show when={orientation() === \"horizontal\"}>\n <div class=\"absolute left-8 right-8 top-3.5 h-0.5 bg-border/80 z-0\" />\n </Show>\n\n {local.children}\n </div>\n </StepsContext.Provider>\n );\n};\n\nexport interface StepProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Optional custom step number, icon, or label to override automatic CSS counter */\n step?: string | number | JSX.Element;\n /** Optional step title rendered at the top of the step */\n title?: string;\n /** Step color variant */\n variant?: StepVariant;\n class?: string;\n}\n\nexport const Step: ParentComponent<StepProps> = (props) => {\n const [local, rest] = splitProps(props, [\"step\", \"title\", \"variant\", \"class\", \"children\"]);\n const context = useContext(StepsContext);\n\n const effectiveVariant = () => local.variant || context?.variant || \"default\";\n const isHorizontal = () => context?.orientation?.() === \"horizontal\";\n\n return (\n <Show\n when={isHorizontal()}\n fallback={\n <div class={cn(\"relative\", local.class)} {...rest}>\n {/* Numbered Step Badge with geometric center on left vertical border line */}\n <Badge\n variant={toBadgeVariant(effectiveVariant())}\n class={cn(\n \"absolute -left-[2.875rem] top-0 size-7 p-0 flex items-center justify-center rounded-lg font-mono text-xs font-semibold shadow-2xs select-none\",\n !local.step && \"[counter-increment:step] before:content-[counter(step)]\"\n )}\n >\n <Show when={local.step}>\n {local.step}\n </Show>\n </Badge>\n\n {/* Step Title Header */}\n <Show when={local.title}>\n <h4 class=\"font-semibold text-base tracking-tight text-foreground mb-2\">\n {local.title}\n </h4>\n </Show>\n\n {/* Step Body Content */}\n <div class=\"text-sm leading-relaxed text-muted-foreground space-y-3\">\n {local.children}\n </div>\n </div>\n }\n >\n <div class={cn(\"flex-1 flex flex-col items-center text-center relative z-10 px-2\", local.class)} {...rest}>\n {/* Step Badge sitting on top of continuous parent rail */}\n <Badge\n variant={toBadgeVariant(effectiveVariant())}\n class={cn(\n \"mb-2.5 size-7 p-0 flex items-center justify-center rounded-lg font-mono text-xs font-semibold shadow-2xs select-none\",\n !local.step && \"[counter-increment:step] before:content-[counter(step)]\"\n )}\n >\n <Show when={local.step}>\n {local.step}\n </Show>\n </Badge>\n\n {/* Step Title Header */}\n <Show when={local.title}>\n <h4 class=\"font-semibold text-sm tracking-tight text-foreground mb-1\">\n {local.title}\n </h4>\n </Show>\n\n {/* Step Body Content */}\n <div class=\"text-xs leading-relaxed text-muted-foreground\">\n {local.children}\n </div>\n </div>\n </Show>\n );\n};\n\nexport interface StepTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {\n class?: string;\n}\n\nexport const StepTitle: ParentComponent<StepTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n return (\n <h4\n class={cn(\"font-semibold text-base tracking-tight text-foreground mb-2\", local.class)}\n {...rest}\n >\n {local.children}\n </h4>\n );\n};\n\nexport interface StepDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const StepDescription: ParentComponent<StepDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n return (\n <div class={cn(\"text-sm leading-relaxed text-muted-foreground\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n",
17
+ "type": "registry:ui"
18
+ }
19
+ ]
20
+ }
@@ -7,10 +7,13 @@
7
7
  "clsx",
8
8
  "tailwind-merge"
9
9
  ],
10
+ "registryDependencies": [
11
+ "create-controllable-signal"
12
+ ],
10
13
  "files": [
11
14
  {
12
15
  "path": "ui/switch.tsx",
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={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-muted\",\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};",
16
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { createControllableSignal } from \"@/hooks/create-controllable-signal\";\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={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-muted\",\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
17
  "type": "registry:ui"
15
18
  }
16
19
  ]
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "table-of-contents",
3
+ "title": "Table of Contents",
4
+ "description": "An accessible heading navigation tree with ScrollSpy tracking and smooth scrolling, composed of ScrollArea and List.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority"
10
+ ],
11
+ "registryDependencies": [
12
+ "scroll-area",
13
+ "list",
14
+ "create-scroll-position"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "ui/table-of-contents.tsx",
19
+ "content": "// src/components/ui/table-of-contents.tsx\nimport {\n splitProps,\n createSignal,\n createEffect,\n onMount,\n onCleanup,\n For,\n Show,\n type Component,\n type JSX,\n} from \"solid-js\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { List, ListHeader, ListItem } from \"@/components/ui/list\";\nimport { cn } from \"@/lib/cn\";\nimport { createScrollPosition } from \"@/hooks/create-scroll-position\";\n\nexport interface TocItem {\n id: string;\n text: string;\n depth: number;\n}\n\nexport interface TableOfContentsProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"title\"> {\n items: TocItem[];\n title?: string | false;\n activeId?: string;\n onItemClick?: (id: string) => void;\n class?: string;\n}\n\n/**\n * Nikala UI TableOfContents Component.\n * Pure composition of ScrollArea, ListHeader, List, and ListItem with active ScrollSpy tracking.\n */\nexport const TableOfContents: Component<TableOfContentsProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"items\",\n \"title\",\n \"activeId\",\n \"onItemClick\",\n \"class\",\n ]);\n\n const [activeId, setActiveId] = createSignal<string>(local.activeId || \"\");\n const scrollPosition = createScrollPosition();\n\n onMount(() => {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n const headings = (local.items || [])\n .map((item) => document.getElementById(item.id))\n .filter((element): element is HTMLElement => Boolean(element));\n\n const updateActiveHeading = () => {\n if (headings.length === 0) return;\n\n const atPageBottom = scrollPosition.isAtBottom();\n if (atPageBottom) {\n setActiveId(headings[headings.length - 1].id);\n return;\n }\n\n const heading = [...headings]\n .reverse()\n .find((element) => element.getBoundingClientRect().top <= 120);\n\n setActiveId(heading?.id || headings[0].id);\n };\n\n createEffect(() => {\n scrollPosition.y();\n scrollPosition.isAtBottom();\n updateActiveHeading();\n });\n\n window.addEventListener(\"resize\", updateActiveHeading);\n onCleanup(() => window.removeEventListener(\"resize\", updateActiveHeading));\n });\n\n const handleClick = (e: MouseEvent, id: string) => {\n if (typeof document !== \"undefined\") {\n const el = document.getElementById(id);\n if (el) {\n e.preventDefault();\n el.scrollIntoView({ behavior: \"smooth\" });\n setActiveId(id);\n if (typeof window !== \"undefined\") {\n window.history.pushState(null, \"\", `#${id}`);\n }\n }\n }\n local.onItemClick?.(id);\n };\n\n return (\n <ScrollArea class={cn(\"h-full py-2\", local.class)} {...rest}>\n <Show when={local.title !== false}>\n <ListHeader\n title={typeof local.title === \"string\" ? local.title : \"On this page\"}\n class=\"px-3 pb-1\"\n />\n </Show>\n\n <List class=\"gap-0.5 p-1\">\n <For each={local.items}>\n {(item) => (\n <ListItem\n title={item.text}\n href={`#${item.id}`}\n active={activeId() === item.id}\n size=\"sm\"\n hoverVariant=\"muted\"\n class={cn(\n \"text-xs transition-colors py-1 h-auto min-h-0\",\n item.depth === 3 && \"pl-5 text-muted-foreground/80\",\n item.depth > 3 && \"pl-7 text-muted-foreground/60\",\n activeId() === item.id\n ? \"text-primary font-medium bg-primary/10\"\n : \"text-muted-foreground hover:text-foreground\"\n )}\n onClick={(e: MouseEvent) => handleClick(e, item.id)}\n />\n )}\n </For>\n </List>\n </ScrollArea>\n );\n};\n\nexport const Toc = TableOfContents;\n",
20
+ "type": "registry:ui"
21
+ }
22
+ ]
23
+ }
@@ -7,10 +7,13 @@
7
7
  "clsx",
8
8
  "tailwind-merge"
9
9
  ],
10
+ "registryDependencies": [
11
+ "create-controllable-signal"
12
+ ],
10
13
  "files": [
11
14
  {
12
15
  "path": "ui/tabs.tsx",
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-accent data-[state=active]:border data-[state=active]:border-border 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};",
16
+ "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 \"@/hooks/create-controllable-signal\";\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-accent data-[state=active]:border data-[state=active]:border-border 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
17
  "type": "registry:ui"
15
18
  }
16
19
  ]
@@ -11,27 +11,28 @@
11
11
  ],
12
12
  "registryDependencies": [
13
13
  "button",
14
- "dropdown-menu"
14
+ "dropdown-menu",
15
+ "create-color-mode"
15
16
  ],
16
17
  "files": [
17
18
  {
18
19
  "path": "providers/theme-provider.tsx",
19
- "content": "import {\n createContext,\n createEffect,\n createSignal,\n onCleanup,\n onMount,\n useContext,\n type ParentComponent,\n type Accessor,\n} from \"solid-js\";\n\nexport type Theme = \"light\" | \"dark\" | \"system\";\nexport type AccentColor = \"wine\" | \"violet\" | \"sky\" | \"emerald\" | \"rose\" | \"amber\" | \"zinc\";\nexport type Radius = \"0\" | \"0.3\" | \"0.5\" | \"0.75\" | \"1.0\";\n\nexport interface ThemeProviderProps {\n /** Initial default theme mode if no saved preference is found in localStorage */\n defaultTheme?: Theme;\n /** Initial default accent color override if needed */\n defaultAccent?: AccentColor;\n /** Initial default border radius override if needed */\n defaultRadius?: Radius;\n /** Key used to store theme preferences in localStorage */\n storageKey?: string;\n}\n\nconst ACCENT_COLORS: Record<\n AccentColor,\n { light: string; dark: string; lightFg: string; darkFg: string }\n> = {\n wine: { light: \"#722f37\", dark: \"#9e3b47\", lightFg: \"#ffffff\", darkFg: \"#ffffff\" },\n violet: { light: \"#7c3aed\", dark: \"#8b5cf6\", lightFg: \"#ffffff\", darkFg: \"#ffffff\" },\n sky: { light: \"#0284c7\", dark: \"#38bdf8\", lightFg: \"#ffffff\", darkFg: \"#0f172a\" },\n emerald: { light: \"#059669\", dark: \"#34d399\", lightFg: \"#ffffff\", darkFg: \"#052e16\" },\n rose: { light: \"#e11d48\", dark: \"#fb7185\", lightFg: \"#ffffff\", darkFg: \"#ffffff\" },\n amber: { light: \"#d97706\", dark: \"#fbbf24\", lightFg: \"#ffffff\", darkFg: \"#111827\" },\n zinc: { light: \"#18181b\", dark: \"#fafafa\", lightFg: \"#fafafa\", darkFg: \"#18181b\" },\n};\n\ninterface ThemeProviderContextValue {\n theme: Accessor<Theme>;\n setTheme: (theme: Theme) => void;\n accent: Accessor<AccentColor | undefined>;\n setAccent: (accent: AccentColor) => void;\n radius: Accessor<Radius | undefined>;\n setRadius: (radius: Radius) => void;\n}\n\nconst ThemeProviderContext = createContext<ThemeProviderContextValue>();\n\n/**\n * Context provider managing application theme state (light/dark/system), accent colors, and border radius.\n */\nexport const ThemeProvider: ParentComponent<ThemeProviderProps> = (props) => {\n const storageKey = props.storageKey || \"nikala-theme\";\n const defaultTheme = props.defaultTheme || \"system\";\n\n // Safely read saved values from localStorage without forcing unnecessary fallbacks\n const getInitialTheme = (): Theme => {\n if (typeof window === \"undefined\") return defaultTheme;\n try {\n const saved = localStorage.getItem(`${storageKey}-mode`);\n if (saved === \"light\" || saved === \"dark\" || saved === \"system\") return saved;\n } catch { }\n return defaultTheme;\n };\n\n const getInitialAccent = (): AccentColor | undefined => {\n if (typeof window === \"undefined\") return props.defaultAccent;\n try {\n const saved = localStorage.getItem(`${storageKey}-accent`);\n if (saved && ACCENT_COLORS[saved as AccentColor]) return saved as AccentColor;\n } catch { }\n return props.defaultAccent;\n };\n\n const getInitialRadius = (): Radius | undefined => {\n if (typeof window === \"undefined\") return props.defaultRadius;\n try {\n const saved = localStorage.getItem(`${storageKey}-radius`);\n if (saved) return saved as Radius;\n } catch { }\n return props.defaultRadius;\n };\n\n const [theme, setThemeSignal] = createSignal<Theme>(getInitialTheme());\n const [accent, setAccentSignal] = createSignal<AccentColor | undefined>(getInitialAccent());\n const [radius, setRadiusSignal] = createSignal<Radius | undefined>(getInitialRadius());\n\n // Applies classes and CSS custom properties when custom overrides are explicitly set\n const applyTheme = (\n targetTheme: Theme,\n currentAccent: AccentColor | undefined,\n currentRadius: Radius | undefined\n ) => {\n if (typeof window === \"undefined\") return;\n\n const root = document.documentElement;\n root.classList.remove(\"light\", \"dark\");\n\n let resolvedDark = false;\n if (targetTheme === \"system\") {\n resolvedDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n root.classList.add(resolvedDark ? \"dark\" : \"light\");\n } else {\n resolvedDark = targetTheme === \"dark\";\n root.classList.add(targetTheme);\n }\n\n // Override --primary CSS variables ONLY if explicitly chosen\n if (currentAccent && ACCENT_COLORS[currentAccent]) {\n const accentData = ACCENT_COLORS[currentAccent];\n const primaryHex = resolvedDark ? accentData.dark : accentData.light;\n const primaryFgHex = resolvedDark ? accentData.darkFg : accentData.lightFg;\n\n root.style.setProperty(\"--primary\", primaryHex);\n root.style.setProperty(\"--primary-foreground\", primaryFgHex);\n }\n\n // Override --radius CSS variable ONLY if explicitly chosen\n if (currentRadius) {\n root.style.setProperty(\"--radius\", `${currentRadius}rem`);\n }\n };\n\n // Reactively apply theme updates and store preferences in localStorage\n createEffect(() => {\n const t = theme();\n const a = accent();\n const r = radius();\n\n applyTheme(t, a, r);\n\n if (typeof window !== \"undefined\") {\n try {\n localStorage.setItem(`${storageKey}-mode`, t);\n if (a) localStorage.setItem(`${storageKey}-accent`, a);\n if (r) localStorage.setItem(`${storageKey}-radius`, r);\n } catch { }\n }\n });\n\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n const handleSystemChange = (e: MediaQueryListEvent) => {\n if (theme() === \"system\") {\n const root = document.documentElement;\n root.classList.remove(\"light\", \"dark\");\n root.classList.add(e.matches ? \"dark\" : \"light\");\n\n const currentAccent = accent();\n if (currentAccent && ACCENT_COLORS[currentAccent]) {\n const accentData = ACCENT_COLORS[currentAccent];\n const primaryHex = e.matches ? accentData.dark : accentData.light;\n const primaryFgHex = e.matches ? accentData.darkFg : accentData.lightFg;\n\n root.style.setProperty(\"--primary\", primaryHex);\n root.style.setProperty(\"--primary-foreground\", primaryFgHex);\n }\n }\n };\n\n if (mediaQuery.addEventListener) {\n mediaQuery.addEventListener(\"change\", handleSystemChange);\n } else if (\"addListener\" in mediaQuery) {\n (mediaQuery as any).addListener(handleSystemChange);\n }\n\n onCleanup(() => {\n if (mediaQuery.removeEventListener) {\n mediaQuery.removeEventListener(\"change\", handleSystemChange);\n } else if (\"removeListener\" in mediaQuery) {\n (mediaQuery as any).removeListener(handleSystemChange);\n }\n });\n });\n\n const value: ThemeProviderContextValue = {\n theme,\n setTheme: (newTheme: Theme) => setThemeSignal(newTheme),\n accent,\n setAccent: (newAccent: AccentColor) => setAccentSignal(newAccent),\n radius,\n setRadius: (newRadius: Radius) => setRadiusSignal(newRadius),\n };\n\n return (\n <ThemeProviderContext.Provider value={value}>\n {props.children}\n </ThemeProviderContext.Provider>\n );\n};\n\n/**\n * Accesses Nikala UI theme state, accent colors, border radius, and update functions.\n */\nexport function useTheme(): ThemeProviderContextValue {\n const context = useContext(ThemeProviderContext);\n if (!context) {\n throw new Error(\"useTheme must be used within a ThemeProvider\");\n }\n return context;\n}\n\nexport { ThemeScript, type ThemeScriptProps } from \"./theme-script\";",
20
+ "content": "import {\n createContext,\n createEffect,\n createSignal,\n onCleanup,\n onMount,\n useContext,\n type ParentComponent,\n type Accessor,\n} from \"solid-js\";\n\nexport type Theme = \"light\" | \"dark\" | \"system\";\nexport type AccentColor = \"yellow\" | \"red\" | \"violet\" | \"sky\" | \"emerald\" | \"zinc\";\nexport type Radius = \"0\" | \"0.3\" | \"0.5\" | \"0.75\" | \"1.0\";\n\nexport interface ThemeProviderProps {\n /** Initial default theme mode if no saved preference is found in localStorage */\n defaultTheme?: Theme;\n /** Initial default accent color override if needed */\n defaultAccent?: AccentColor;\n /** Initial default border radius override if needed */\n defaultRadius?: Radius;\n /** Key used to store theme preferences in localStorage */\n storageKey?: string;\n}\n\nconst ACCENT_COLORS: Record<\n AccentColor,\n { light: string; dark: string; lightFg: string; darkFg: string }\n> = {\n yellow: { light: \"oklch(0.795 0.184 86.047)\", dark: \"oklch(0.852 0.199 91.936)\", lightFg: \"oklch(0.145 0 0)\", darkFg: \"oklch(0.145 0 0)\" },\n red: { light: \"oklch(0.577 0.245 27.325)\", dark: \"oklch(0.637 0.237 25.331)\", lightFg: \"oklch(0.985 0 0)\", darkFg: \"oklch(0.985 0 0)\" },\n violet: { light: \"oklch(0.541 0.281 293.009)\", dark: \"oklch(0.606 0.25 292.717)\", lightFg: \"oklch(0.985 0 0)\", darkFg: \"oklch(0.985 0 0)\" },\n sky: { light: \"oklch(0.588 0.158 241.966)\", dark: \"oklch(0.672 0.154 238.29)\", lightFg: \"oklch(0.985 0 0)\", darkFg: \"oklch(0.145 0 0)\" },\n emerald: { light: \"oklch(0.596 0.145 163.225)\", dark: \"oklch(0.696 0.17 162.48)\", lightFg: \"oklch(0.985 0 0)\", darkFg: \"oklch(0.145 0 0)\" },\n zinc: { light: \"oklch(0.205 0 0)\", dark: \"oklch(0.985 0 0)\", lightFg: \"oklch(0.985 0 0)\", darkFg: \"oklch(0.205 0 0)\" },\n};\n\ninterface ThemeProviderContextValue {\n theme: Accessor<Theme>;\n setTheme: (theme: Theme) => void;\n accent: Accessor<AccentColor | undefined>;\n setAccent: (accent: AccentColor) => void;\n radius: Accessor<Radius | undefined>;\n setRadius: (radius: Radius) => void;\n}\n\nconst ThemeProviderContext = createContext<ThemeProviderContextValue>();\n\n/**\n * Context provider managing application theme state (light/dark/system), accent colors, and border radius.\n */\nexport const ThemeProvider: ParentComponent<ThemeProviderProps> = (props) => {\n const storageKey = props.storageKey || \"nikala-theme\";\n const defaultTheme = props.defaultTheme || \"system\";\n\n // Safely read saved values from localStorage without forcing unnecessary fallbacks\n const getInitialTheme = (): Theme => {\n if (typeof window === \"undefined\") return defaultTheme;\n try {\n const saved = localStorage.getItem(`${storageKey}-mode`);\n if (saved === \"light\" || saved === \"dark\" || saved === \"system\") return saved;\n } catch { }\n return defaultTheme;\n };\n\n const getInitialAccent = (): AccentColor | undefined => {\n if (typeof window === \"undefined\") return props.defaultAccent;\n try {\n const saved = localStorage.getItem(`${storageKey}-accent`);\n if (saved && ACCENT_COLORS[saved as AccentColor]) return saved as AccentColor;\n } catch { }\n return props.defaultAccent;\n };\n\n const getInitialRadius = (): Radius | undefined => {\n if (typeof window === \"undefined\") return props.defaultRadius;\n try {\n const saved = localStorage.getItem(`${storageKey}-radius`);\n if (saved) return saved as Radius;\n } catch { }\n return props.defaultRadius;\n };\n\n const [theme, setThemeSignal] = createSignal<Theme>(getInitialTheme());\n const [accent, setAccentSignal] = createSignal<AccentColor | undefined>(getInitialAccent());\n const [radius, setRadiusSignal] = createSignal<Radius | undefined>(getInitialRadius());\n\n // Applies classes and CSS custom properties when custom overrides are explicitly set\n const applyTheme = (\n targetTheme: Theme,\n currentAccent: AccentColor | undefined,\n currentRadius: Radius | undefined\n ) => {\n if (typeof window === \"undefined\") return;\n\n const root = document.documentElement;\n root.classList.remove(\"light\", \"dark\");\n\n let resolvedDark = false;\n if (targetTheme === \"system\") {\n resolvedDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n root.classList.add(resolvedDark ? \"dark\" : \"light\");\n } else {\n resolvedDark = targetTheme === \"dark\";\n root.classList.add(targetTheme);\n }\n root.style.colorScheme = resolvedDark ? \"dark\" : \"light\";\n\n // Override --primary CSS variables ONLY if explicitly chosen\n if (currentAccent && ACCENT_COLORS[currentAccent]) {\n const accentData = ACCENT_COLORS[currentAccent];\n const primaryHex = resolvedDark ? accentData.dark : accentData.light;\n const primaryFgHex = resolvedDark ? accentData.darkFg : accentData.lightFg;\n\n root.style.setProperty(\"--primary\", primaryHex);\n root.style.setProperty(\"--primary-foreground\", primaryFgHex);\n } else {\n root.style.removeProperty(\"--primary\");\n root.style.removeProperty(\"--primary-foreground\");\n }\n\n // Override --radius CSS variable ONLY if explicitly chosen\n if (currentRadius) {\n root.style.setProperty(\"--radius\", `${currentRadius}rem`);\n } else {\n root.style.removeProperty(\"--radius\");\n }\n };\n\n // Reactively apply theme updates and store preferences in localStorage\n createEffect(() => {\n const t = theme();\n const a = accent();\n const r = radius();\n\n applyTheme(t, a, r);\n\n if (typeof window !== \"undefined\") {\n try {\n localStorage.setItem(`${storageKey}-mode`, t);\n if (a) localStorage.setItem(`${storageKey}-accent`, a);\n else localStorage.removeItem(`${storageKey}-accent`);\n if (r) localStorage.setItem(`${storageKey}-radius`, r);\n else localStorage.removeItem(`${storageKey}-radius`);\n } catch { }\n }\n });\n\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n const handleSystemChange = (e: MediaQueryListEvent) => {\n if (theme() === \"system\") {\n const root = document.documentElement;\n root.classList.remove(\"light\", \"dark\");\n root.classList.add(e.matches ? \"dark\" : \"light\");\n root.style.colorScheme = e.matches ? \"dark\" : \"light\";\n\n const currentAccent = accent();\n if (currentAccent && ACCENT_COLORS[currentAccent]) {\n const accentData = ACCENT_COLORS[currentAccent];\n const primaryHex = e.matches ? accentData.dark : accentData.light;\n const primaryFgHex = e.matches ? accentData.darkFg : accentData.lightFg;\n\n root.style.setProperty(\"--primary\", primaryHex);\n root.style.setProperty(\"--primary-foreground\", primaryFgHex);\n }\n }\n };\n\n if (mediaQuery.addEventListener) {\n mediaQuery.addEventListener(\"change\", handleSystemChange);\n } else if (\"addListener\" in mediaQuery) {\n (mediaQuery as any).addListener(handleSystemChange);\n }\n\n onCleanup(() => {\n if (mediaQuery.removeEventListener) {\n mediaQuery.removeEventListener(\"change\", handleSystemChange);\n } else if (\"removeListener\" in mediaQuery) {\n (mediaQuery as any).removeListener(handleSystemChange);\n }\n });\n });\n\n const value: ThemeProviderContextValue = {\n theme,\n setTheme: (newTheme: Theme) => setThemeSignal(newTheme),\n accent,\n setAccent: (newAccent: AccentColor) => setAccentSignal(newAccent),\n radius,\n setRadius: (newRadius: Radius) => setRadiusSignal(newRadius),\n };\n\n return (\n <ThemeProviderContext.Provider value={value}>\n {props.children}\n </ThemeProviderContext.Provider>\n );\n};\n\n/**\n * Accesses Nikala UI theme state, accent colors, border radius, and update functions.\n */\nexport function useTheme(): ThemeProviderContextValue {\n const context = useContext(ThemeProviderContext);\n if (!context) {\n throw new Error(\"useTheme must be used within a ThemeProvider\");\n }\n return context;\n}\n\nexport { ThemeScript, type ThemeScriptProps } from \"./theme-script\";",
20
21
  "type": "registry:ui"
21
22
  },
22
23
  {
23
24
  "path": "providers/theme-transitions.ts",
24
- "content": "export type ThemeEffect = \"none\" | \"circular\" | \"fade\";\n\n/**\n * Injects required CSS view-transition pseudo-element styles to prevent browser mix-blend artifacts.\n */\nfunction ensureTransitionStyles() {\n if (typeof document === \"undefined\") return;\n const styleId = \"nikala-view-transition-styles\";\n if (!document.getElementById(styleId)) {\n const style = document.createElement(\"style\");\n style.id = styleId;\n style.textContent = `\n ::view-transition-old(root),\n ::view-transition-new(root) {\n animation: none;\n mix-blend-mode: normal;\n }\n ::view-transition-old(root) {\n z-index: 1;\n }\n ::view-transition-new(root) {\n z-index: 9999;\n }\n `;\n document.head.appendChild(style);\n }\n}\n\n/**\n * Executes a theme change with the specified transition effect using the Web View Transitions API.\n *\n * @param effect - Desired transition animation (\"none\", \"circular\", \"fade\")\n * @param event - Mouse or Pointer event to calculate transition center coordinates\n * @param updateThemeCallback - Callback function performing the actual theme state change\n */\nexport function runThemeTransition(\n effect: ThemeEffect = \"none\",\n event: MouseEvent | undefined,\n updateThemeCallback: () => void\n) {\n // Safe fallback if View Transitions API is unsupported or user prefers reduced motion\n if (\n effect === \"none\" ||\n typeof document === \"undefined\" ||\n !(document as any).startViewTransition ||\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n ) {\n updateThemeCallback();\n return;\n }\n\n ensureTransitionStyles();\n\n // Circular expanding ripple transition originating from click coordinates\n if (effect === \"circular\" && event) {\n const x = event.clientX;\n const y = event.clientY;\n\n const endRadius = Math.hypot(\n Math.max(x, window.innerWidth - x),\n Math.max(y, window.innerHeight - y)\n );\n\n const transition = (document as any).startViewTransition(() => {\n updateThemeCallback();\n });\n\n transition.ready.then(() => {\n document.documentElement.animate(\n {\n clipPath: [\n `circle(0px at ${x}px ${y}px)`,\n `circle(${endRadius}px at ${x}px ${y}px)`,\n ],\n },\n {\n duration: 500,\n easing: \"ease-in-out\",\n pseudoElement: \"::view-transition-new(root)\",\n }\n );\n });\n return;\n }\n\n // Smooth opacity fade view transition\n if (effect === \"fade\") {\n const transition = (document as any).startViewTransition(() => {\n updateThemeCallback();\n });\n\n transition.ready.then(() => {\n document.documentElement.animate(\n {\n opacity: [0, 1],\n },\n {\n duration: 350,\n easing: \"ease-in-out\",\n pseudoElement: \"::view-transition-new(root)\",\n }\n );\n });\n return;\n }\n\n updateThemeCallback();\n}",
25
+ "content": "export type ThemeEffect = \"none\" | \"circular\" | \"fade\";\n\n/**\n * Injects required CSS view-transition pseudo-element styles to prevent browser mix-blend artifacts.\n */\nfunction ensureTransitionStyles() {\n if (typeof document === \"undefined\") return;\n const styleId = \"nikala-view-transition-styles\";\n if (!document.getElementById(styleId)) {\n const style = document.createElement(\"style\");\n style.id = styleId;\n style.textContent = `\n ::view-transition-old(root),\n ::view-transition-new(root) {\n animation: none;\n mix-blend-mode: normal;\n }\n ::view-transition-old(root) {\n z-index: 1;\n }\n ::view-transition-new(root) {\n z-index: 9999;\n }\n `;\n document.head.appendChild(style);\n }\n}\n\n/**\n * Checks whether the current environment is Linux WebKitGTK (e.g. Tauri on Linux)\n * where CSS View Transitions pseudo-element animations trigger known WebProcess crashes.\n */\nfunction isLinuxWebKit(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const ua = navigator.userAgent || \"\";\n const isLinux = /linux/i.test(ua);\n const isWebKit = /applewebkit/i.test(ua);\n const isChrome = /chrome|chromium|edg/i.test(ua);\n const isTauri = typeof window !== \"undefined\" && typeof (window as any).__TAURI_INTERNALS__ !== \"undefined\";\n\n return isLinux && ((isWebKit && !isChrome) || isTauri);\n}\n\n/**\n * Executes a theme change with the specified transition effect using the Web View Transitions API.\n * Supported on modern browsers, Windows (Tauri WebView2), and macOS (Tauri WKWebView).\n * Automatically falls back to safe instant transitions on Linux WebKitGTK.\n *\n * @param effect - Desired transition animation (\"none\", \"circular\", \"fade\")\n * @param event - Mouse or Pointer event to calculate transition center coordinates\n * @param updateThemeCallback - Callback function performing the actual theme state change\n */\nexport function runThemeTransition(\n effect: ThemeEffect = \"none\",\n event: MouseEvent | undefined,\n updateThemeCallback: () => void\n) {\n // Safe fallback if View Transitions API is unsupported, reduced motion is preferred,\n // or on Linux WebKitGTK where pseudo-element clipping transitions trigger WebProcess faults\n if (\n effect === \"none\" ||\n typeof document === \"undefined\" ||\n !(document as any).startViewTransition ||\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ||\n isLinuxWebKit()\n ) {\n updateThemeCallback();\n return;\n }\n\n ensureTransitionStyles();\n\n try {\n // Circular expanding ripple transition originating from click coordinates\n if (effect === \"circular\" && event) {\n const x = event.clientX;\n const y = event.clientY;\n\n const endRadius = Math.hypot(\n Math.max(x, window.innerWidth - x),\n Math.max(y, window.innerHeight - y)\n );\n\n const transition = (document as any).startViewTransition(() => {\n updateThemeCallback();\n });\n\n transition.ready\n ?.then(() => {\n document.documentElement.animate(\n {\n clipPath: [\n `circle(0px at ${x}px ${y}px)`,\n `circle(${endRadius}px at ${x}px ${y}px)`,\n ],\n },\n {\n duration: 500,\n easing: \"ease-in-out\",\n pseudoElement: \"::view-transition-new(root)\",\n }\n );\n })\n ?.catch(() => {\n // Graceful fallback\n });\n return;\n }\n\n // Smooth opacity fade view transition\n if (effect === \"fade\") {\n const transition = (document as any).startViewTransition(() => {\n updateThemeCallback();\n });\n\n transition.ready\n ?.then(() => {\n document.documentElement.animate(\n {\n opacity: [0, 1],\n },\n {\n duration: 350,\n easing: \"ease-in-out\",\n pseudoElement: \"::view-transition-new(root)\",\n }\n );\n })\n ?.catch(() => {\n // Graceful fallback\n });\n return;\n }\n\n updateThemeCallback();\n } catch {\n updateThemeCallback();\n }\n}",
25
26
  "type": "registry:ui"
26
27
  },
27
28
  {
28
29
  "path": "providers/theme-script.tsx",
29
- "content": "import { type Component } from \"solid-js\";\n\nexport interface ThemeScriptProps {\n /** Storage key namespace used in localStorage (default: \"nikala-theme\") */\n storageKey?: string;\n /** Initial default theme mode if no saved preference exists (default: \"system\") */\n defaultTheme?: \"light\" | \"dark\" | \"system\";\n /** Initial default primary accent color if no saved preference exists */\n defaultAccent?: string;\n /** Initial default border radius if no saved preference exists */\n defaultRadius?: string;\n}\n\n/**\n * Pre-hydration inline script executed synchronously before DOM paint to prevent theme flickering (anti-FOUC).\n */\nexport const ThemeScript: Component<ThemeScriptProps> = (props) => {\n const key = props.storageKey || \"nikala-theme\";\n const defTheme = props.defaultTheme || \"system\";\n const defAccent = props.defaultAccent || \"\";\n const defRadius = props.defaultRadius || \"\";\n\n const scriptText = `(function(){try{\n var key = '${key}';\n var mode = localStorage.getItem(key + '-mode') || '${defTheme}';\n var accent = localStorage.getItem(key + '-accent') || '${defAccent}';\n var radius = localStorage.getItem(key + '-radius') || '${defRadius}';\n\n var isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n var resolvedDark = mode === 'dark' || (mode === 'system' && isDark);\n\n var root = document.documentElement;\n root.classList.remove('light', 'dark');\n root.classList.add(resolvedDark ? 'dark' : 'light');\n root.style.colorScheme = resolvedDark ? 'dark' : 'light';\n\n var colorMap = {\n wine: resolvedDark ? '#9e3b47' : '#722f37',\n violet: resolvedDark ? '#8b5cf6' : '#7c3aed',\n sky: resolvedDark ? '#38bdf8' : '#0284c7',\n emerald: resolvedDark ? '#34d399' : '#059669',\n rose: resolvedDark ? '#fb7185' : '#e11d48',\n amber: resolvedDark ? '#fbbf24' : '#d97706',\n zinc: resolvedDark ? '#fafafa' : '#18181b'\n };\n\n if (accent && colorMap[accent]) {\n root.style.setProperty('--primary', colorMap[accent]);\n }\n\n if (radius) {\n var radVal = radius.endsWith('rem') ? radius : radius + 'rem';\n root.style.setProperty('--radius', radVal);\n }\n}catch(e){}})();`;\n\n return <script innerHTML={scriptText} />;\n};",
30
+ "content": "import { isServer } from \"solid-js/web\";\nimport type { Component } from \"solid-js\";\n\nexport interface ThemeScriptProps {\n /** Storage key namespace used in localStorage (default: \"nikala-theme\") */\n storageKey?: string;\n /** Initial default theme mode if no saved preference exists (default: \"system\") */\n defaultTheme?: \"light\" | \"dark\" | \"system\";\n /** Initial default primary accent color if no saved preference exists */\n defaultAccent?: string;\n /** Initial default border radius if no saved preference exists */\n defaultRadius?: string;\n}\n\n/**\n * Pre-hydration inline script executed synchronously before DOM paint to prevent theme flickering (anti-FOUC).\n */\nexport const ThemeScript: Component<ThemeScriptProps> = (props) => {\n if (!isServer) return null;\n const key = props.storageKey || \"nikala-theme\";\n const defTheme = props.defaultTheme || \"system\";\n const defAccent = props.defaultAccent || \"\";\n const defRadius = props.defaultRadius || \"\";\n\n const scriptText = `(function(){try{\n var key = '${key}';\n var mode = localStorage.getItem(key + '-mode') || '${defTheme}';\n var accent = localStorage.getItem(key + '-accent') || '${defAccent}';\n var radius = localStorage.getItem(key + '-radius') || '${defRadius}';\n\n var isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n var resolvedDark = mode === 'dark' || (mode === 'system' && isDark);\n\n var root = document.documentElement;\n root.classList.remove('light', 'dark');\n root.classList.add(resolvedDark ? 'dark' : 'light');\n root.style.colorScheme = resolvedDark ? 'dark' : 'light';\n\n var colorMap = {\n yellow: {\n primary: resolvedDark ? 'oklch(0.852 0.199 91.936)' : 'oklch(0.795 0.184 86.047)',\n foreground: 'oklch(0.145 0 0)'\n },\n red: {\n primary: resolvedDark ? 'oklch(0.637 0.237 25.331)' : 'oklch(0.577 0.245 27.325)',\n foreground: 'oklch(0.985 0 0)'\n },\n violet: {\n primary: resolvedDark ? 'oklch(0.606 0.25 292.717)' : 'oklch(0.541 0.281 293.009)',\n foreground: 'oklch(0.985 0 0)'\n },\n sky: {\n primary: resolvedDark ? 'oklch(0.672 0.154 238.29)' : 'oklch(0.588 0.158 241.966)',\n foreground: resolvedDark ? 'oklch(0.145 0 0)' : 'oklch(0.985 0 0)'\n },\n emerald: {\n primary: resolvedDark ? 'oklch(0.696 0.17 162.48)' : 'oklch(0.596 0.145 163.225)',\n foreground: resolvedDark ? 'oklch(0.145 0 0)' : 'oklch(0.985 0 0)'\n },\n zinc: {\n primary: resolvedDark ? 'oklch(0.985 0 0)' : 'oklch(0.205 0 0)',\n foreground: resolvedDark ? 'oklch(0.205 0 0)' : 'oklch(0.985 0 0)'\n }\n };\n\n if (accent && colorMap[accent]) {\n root.style.setProperty('--primary', colorMap[accent].primary);\n root.style.setProperty('--primary-foreground', colorMap[accent].foreground);\n }\n\n if (radius) {\n var radVal = radius.endsWith('rem') ? radius : radius + 'rem';\n root.style.setProperty('--radius', radVal);\n }\n}catch(e){}})();`;\n\n return <script innerHTML={scriptText} />;\n};",
30
31
  "type": "registry:ui"
31
32
  },
32
33
  {
33
34
  "path": "ui/theme-toggle.tsx",
34
- "content": "import { splitProps, type Component, For, Show } from \"solid-js\";\nimport { Sun, Moon, Monitor } from \"lucide-solid\";\nimport {\n useTheme,\n type AccentColor,\n type Radius,\n type Theme,\n} from \"../../providers/theme-provider\";\nimport {\n runThemeTransition,\n type ThemeEffect,\n} from \"../../providers/theme-transitions\";\nimport { Button } from \"./button\";\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\";\nimport { cn } from \"@/lib/cn\";\n\nimport { createColorMode } from \"@nikala-ui/hooks\";\n\nexport interface ThemeToggleProps {\n /** Display mode: \"mini\" for compact dropdown, \"max\" for full customizer panel (default: \"mini\") */\n mode?: \"mini\" | \"max\";\n /** Transition animation effect when changing themes (\"none\", \"circular\", \"fade\") */\n effect?: ThemeEffect;\n class?: string;\n}\n\nconst ACCENT_OPTIONS: { name: AccentColor; label: string; color: string }[] = [\n { name: \"wine\", label: \"Wine\", color: \"bg-[#722f37]\" },\n { name: \"violet\", label: \"Violet\", color: \"bg-[#7c3aed]\" },\n { name: \"sky\", label: \"Sky\", color: \"bg-[#0284c7]\" },\n { name: \"emerald\", label: \"Emerald\", color: \"bg-[#059669]\" },\n { name: \"rose\", label: \"Rose\", color: \"bg-[#e11d48]\" },\n { name: \"amber\", label: \"Amber\", color: \"bg-[#d97706]\" },\n { name: \"zinc\", label: \"Zinc\", color: \"bg-[#18181b]\" },\n];\n\nconst RADIUS_OPTIONS: { value: Radius; label: string }[] = [\n { value: \"0\", label: \"0\" },\n { value: \"0.3\", label: \"0.3\" },\n { value: \"0.5\", label: \"0.5\" },\n { value: \"0.75\", label: \"0.75\" },\n { value: \"1.0\", label: \"1.0\" },\n];\n\n/**\n * Interactive UI theme switcher supporting mini/max modes and View Transition animations.\n */\nexport const ThemeToggle: Component<ThemeToggleProps> = (props) => {\n const [local] = splitProps(props, [\"mode\", \"effect\", \"class\"]);\n const { theme, setTheme, accent, setAccent, radius, setRadius } = useTheme();\n\n const colorMode = createColorMode({\n initialValue: theme(),\n storageKey: \"nikala-theme-mode\",\n });\n\n const mode = () => local.mode || \"mini\";\n const effect = () => local.effect || \"none\";\n\n /* Reactive accessor determining whether dark mode is active via createColorMode hook */\n const isDarkMode = () => {\n const currentTheme = theme();\n if (currentTheme === \"dark\") return true;\n if (currentTheme === \"light\") return false;\n return colorMode.isDark();\n };\n\n const changeThemeWithEffect = (newTheme: Theme, e: MouseEvent) => {\n runThemeTransition(effect(), e, () => {\n setTheme(newTheme);\n colorMode.setMode(newTheme);\n });\n };\n\n return (\n <DropdownMenu placement=\"bottom-end\">\n <DropdownMenuTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n class={cn(\"relative h-9 w-9 cursor-pointer\", local.class)}\n >\n {/* Reactive Sun / Moon Icon Toggle */}\n <Show\n when={isDarkMode()}\n fallback={<Sun class=\"h-4 w-4 text-foreground transition-transform\" />}\n >\n <Moon class=\"h-4 w-4 text-foreground transition-transform\" />\n </Show>\n\n <span class=\"sr-only\">Toggle theme</span>\n </DropdownMenuTrigger>\n\n <Show\n when={mode() === \"max\"}\n fallback={\n /* Mini Mode: Compact Dropdown */\n <DropdownMenuContent>\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}>\n <Sun class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Light\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}>\n <Moon class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Dark\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}>\n <Monitor class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n System\n </DropdownMenuItem>\n </DropdownMenuContent>\n }\n >\n {/* Max Mode: Full Theme Customizer Panel */}\n <DropdownMenuContent class=\"w-64\">\n <div class=\"p-2 space-y-2\">\n <DropdownMenuLabel class=\"px-0 pt-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Theme Mode\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-3 gap-1 my-1.5\">\n <Button\n variant={theme() === \"light\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Light\n </Button>\n <Button\n variant={theme() === \"dark\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Dark\n </Button>\n <Button\n variant={theme() === \"system\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n System\n </Button>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Brand Accent Color\n </DropdownMenuLabel>\n <div class=\"flex flex-wrap gap-1.5 my-1.5\">\n <For each={ACCENT_OPTIONS}>\n {(opt) => (\n <button\n type=\"button\"\n title={opt.label}\n onClick={() => setAccent(opt.name)}\n class={cn(\n \"h-6 w-6 rounded-md transition-all cursor-pointer border border-border flex items-center justify-center\",\n opt.color,\n accent() === opt.name ? \"ring-2 ring-primary ring-offset-2 ring-offset-background scale-110\" : \"hover:scale-105\"\n )}\n />\n )}\n </For>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Border Radius\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-5 gap-1 my-1.5\">\n <For each={RADIUS_OPTIONS}>\n {(r) => (\n <Button\n variant={radius() === r.value ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={() => setRadius(r.value)}\n class=\"h-7 text-xs px-1 cursor-pointer\"\n >\n {r.label}\n </Button>\n )}\n </For>\n </div>\n </div>\n </DropdownMenuContent>\n </Show>\n </DropdownMenu>\n );\n};",
35
+ "content": "import { splitProps, type Component, For, Show, type JSX } 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, type ButtonProps } from \"./button\";\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuGroup,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ThemeToggleProps\n extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\"> {\n /** Display mode: \"button\" (direct 1-click toggle), \"mini\" (compact dropdown), \"max\" (full customizer panel). Defaults to \"mini\". */\n mode?: \"button\" | \"mini\" | \"max\";\n /** Transition animation effect when changing themes (\"none\", \"circular\", \"fade\") */\n effect?: ThemeEffect;\n /** Button visual style variant when rendered as a button or dropdown trigger */\n variant?: ButtonProps[\"variant\"];\n /** Button size */\n size?: ButtonProps[\"size\"];\n class?: string;\n}\n\nconst ACCENT_OPTIONS: { name: AccentColor; label: string; color: string }[] = [\n { name: \"yellow\", label: \"Yellow\", color: \"bg-yellow-500\" },\n { name: \"red\", label: \"Red\", color: \"bg-red-500\" },\n { name: \"violet\", label: \"Violet\", color: \"bg-violet-500\" },\n { name: \"sky\", label: \"Sky\", color: \"bg-sky-500\" },\n { name: \"emerald\", label: \"Emerald\", color: \"bg-emerald-500\" },\n { name: \"zinc\", label: \"Zinc\", color: \"bg-zinc-800 dark:bg-zinc-200\" },\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 button, mini (dropdown), and max (customizer panel) modes with View Transition animations.\n */\nexport const ThemeToggle: Component<ThemeToggleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"mode\", \"effect\", \"variant\", \"size\", \"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 if (typeof window !== \"undefined\") {\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n }\n return false;\n };\n\n const changeThemeWithEffect = (newTheme: Theme, e: MouseEvent) => {\n runThemeTransition(effect(), e, () => {\n setTheme(newTheme);\n });\n };\n\n const handleToggleClick = (e: MouseEvent) => {\n const nextTheme: Theme = isDarkMode() ? \"light\" : \"dark\";\n changeThemeWithEffect(nextTheme, e);\n };\n\n return (\n <Show\n when={mode() !== \"button\"}\n fallback={\n /* Button Mode: Direct 1-click Dark/Light switcher without dropdown */\n <Button\n variant={local.variant || \"ghost\"}\n size={local.size || \"icon\"}\n onClick={handleToggleClick}\n class={cn(\"relative cursor-pointer\", local.class)}\n aria-label=\"Toggle theme\"\n {...rest}\n >\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 <span class=\"sr-only\">Toggle theme</span>\n </Button>\n }\n >\n <DropdownMenu placement=\"bottom-end\">\n <DropdownMenuTrigger\n as={Button}\n variant={local.variant || \"ghost\"}\n size={local.size || \"icon\"}\n class={cn(\"relative cursor-pointer\", local.class)}\n {...rest}\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 <span class=\"sr-only\">Toggle theme</span>\n </DropdownMenuTrigger>\n\n <Show\n when={mode() === \"max\"}\n fallback={\n /* Mini Mode: Compact Dropdown */\n <DropdownMenuContent>\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}>\n <Sun class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Light\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}>\n <Moon class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Dark\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}>\n <Monitor class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n System\n </DropdownMenuItem>\n </DropdownMenuContent>\n }\n >\n {/* Max Mode: Full Theme Customizer Panel */}\n <DropdownMenuContent class=\"w-64\">\n <div class=\"p-2 space-y-2\">\n <DropdownMenuGroup>\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 </DropdownMenuGroup>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuGroup>\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\n ? \"ring-2 ring-primary ring-offset-2 ring-offset-background scale-110\"\n : \"hover:scale-105\"\n )}\n />\n )}\n </For>\n </div>\n </DropdownMenuGroup>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuGroup>\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 </DropdownMenuGroup>\n </div>\n </DropdownMenuContent>\n </Show>\n </DropdownMenu>\n </Show>\n );\n};",
35
36
  "type": "registry:ui"
36
37
  }
37
38
  ]