@nikala-ui/core 0.10.1 → 0.11.0-nightly.b01fa01

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 (106) hide show
  1. package/package.json +1 -1
  2. package/registry/banner.json +1 -1
  3. package/registry/bubble.json +18 -0
  4. package/registry/button-group.json +20 -0
  5. package/registry/button.json +1 -1
  6. package/registry/checkbox.json +4 -1
  7. package/registry/combobox.json +1 -1
  8. package/registry/command.json +3 -2
  9. package/registry/context-menu.json +1 -1
  10. package/registry/create-app-updater.json +13 -0
  11. package/registry/create-chat-scroll.json +13 -0
  12. package/registry/create-document-tabs.json +13 -0
  13. package/registry/create-drop-zone.json +13 -0
  14. package/registry/create-global-shortcut.json +13 -0
  15. package/registry/create-pagination.json +13 -0
  16. package/registry/create-tauri-window.json +13 -0
  17. package/registry/dialog.json +1 -1
  18. package/registry/dropdown-menu.json +3 -2
  19. package/registry/dropzone.json +21 -0
  20. package/registry/field.json +1 -1
  21. package/registry/footer.json +18 -0
  22. package/registry/forgot-password-01.json +28 -0
  23. package/registry/form-message.json +3 -2
  24. package/registry/hero-01.json +22 -0
  25. package/registry/icon-button.json +1 -1
  26. package/registry/index.json +432 -5
  27. package/registry/login-01.json +28 -0
  28. package/registry/marker.json +17 -0
  29. package/registry/marquee.json +17 -0
  30. package/registry/message.json +17 -0
  31. package/registry/navbar.json +19 -0
  32. package/registry/navigation-menu.json +22 -0
  33. package/registry/number-input.json +1 -1
  34. package/registry/otp-verification-01.json +26 -0
  35. package/registry/pagination.json +22 -0
  36. package/registry/popover.json +4 -1
  37. package/registry/rating.json +19 -0
  38. package/registry/register-01.json +31 -0
  39. package/registry/resizable.json +1 -1
  40. package/registry/review-card.json +23 -0
  41. package/registry/scroll-area.json +1 -1
  42. package/registry/select.json +3 -2
  43. package/registry/sheet.json +1 -1
  44. package/registry/sidebar.json +24 -0
  45. package/registry/spinner.json +1 -1
  46. package/registry/stat.json +19 -0
  47. package/registry/status.json +1 -1
  48. package/registry/switch.json +4 -1
  49. package/registry/table.json +17 -0
  50. package/registry/tabs.json +4 -1
  51. package/registry/theme-manager.json +4 -3
  52. package/registry/timeline.json +18 -0
  53. package/registry/titlebar-tabs.json +24 -0
  54. package/registry/titlebar.json +26 -0
  55. package/registry/toggle-group.json +21 -0
  56. package/registry/updater-modal.json +26 -0
  57. package/src/registry/blocks/forgot-password-01.tsx +141 -0
  58. package/src/registry/blocks/hero-01.tsx +28 -0
  59. package/src/registry/blocks/login-01.tsx +231 -0
  60. package/src/registry/blocks/otp-verification-01.tsx +170 -0
  61. package/src/registry/blocks/register-01.tsx +318 -0
  62. package/src/registry/components/ui/banner.tsx +0 -2
  63. package/src/registry/components/ui/bubble.tsx +142 -0
  64. package/src/registry/components/ui/button-group.tsx +42 -0
  65. package/src/registry/components/ui/button.tsx +1 -1
  66. package/src/registry/components/ui/checkbox.tsx +1 -1
  67. package/src/registry/components/ui/combobox.tsx +83 -18
  68. package/src/registry/components/ui/command.tsx +101 -105
  69. package/src/registry/components/ui/context-menu.tsx +67 -22
  70. package/src/registry/components/ui/dialog.tsx +42 -32
  71. package/src/registry/components/ui/dropdown-menu.tsx +69 -31
  72. package/src/registry/components/ui/dropzone.tsx +189 -0
  73. package/src/registry/components/ui/field.tsx +7 -3
  74. package/src/registry/components/ui/footer.tsx +247 -0
  75. package/src/registry/components/ui/form-message.tsx +2 -2
  76. package/src/registry/components/ui/icon-button.tsx +1 -1
  77. package/src/registry/components/ui/marker.tsx +102 -0
  78. package/src/registry/components/ui/marquee.tsx +118 -0
  79. package/src/registry/components/ui/message.tsx +168 -0
  80. package/src/registry/components/ui/navbar.tsx +375 -0
  81. package/src/registry/components/ui/navigation-menu.tsx +358 -0
  82. package/src/registry/components/ui/number-input.tsx +3 -3
  83. package/src/registry/components/ui/pagination.tsx +258 -0
  84. package/src/registry/components/ui/popover.tsx +1 -1
  85. package/src/registry/components/ui/rating.tsx +185 -0
  86. package/src/registry/components/ui/resizable.tsx +3 -1
  87. package/src/registry/components/ui/review-card.tsx +195 -0
  88. package/src/registry/components/ui/scroll-area.tsx +13 -3
  89. package/src/registry/components/ui/select.tsx +16 -19
  90. package/src/registry/components/ui/sheet.tsx +58 -53
  91. package/src/registry/components/ui/sidebar.tsx +692 -0
  92. package/src/registry/components/ui/spinner.tsx +1 -1
  93. package/src/registry/components/ui/stat.tsx +245 -0
  94. package/src/registry/components/ui/status.tsx +7 -5
  95. package/src/registry/components/ui/switch.tsx +1 -1
  96. package/src/registry/components/ui/table.tsx +160 -0
  97. package/src/registry/components/ui/tabs.tsx +1 -1
  98. package/src/registry/components/ui/theme-toggle.tsx +80 -79
  99. package/src/registry/components/ui/timeline.tsx +350 -0
  100. package/src/registry/components/ui/titlebar-tabs.tsx +412 -0
  101. package/src/registry/components/ui/titlebar.tsx +399 -0
  102. package/src/registry/components/ui/toggle-group.tsx +203 -0
  103. package/src/registry/components/ui/updater-modal.tsx +254 -0
  104. package/src/registry/index.ts +3 -3
  105. package/src/registry/metadata.ts +184 -5
  106. 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
  ]
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "sidebar",
3
+ "title": "Sidebar",
4
+ "description": "A composable, collapsible, and accessible application sidebar navigation suite with icon mode, mobile drawer, and keyboard shortcuts.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority",
10
+ "lucide-solid"
11
+ ],
12
+ "registryDependencies": [
13
+ "tooltip",
14
+ "skeleton",
15
+ "separator"
16
+ ],
17
+ "files": [
18
+ {
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 \"@/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
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "ui/spinner.tsx",
14
- "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const spinnerVariants = cva(\n \"inline-block animate-spin rounded-full border-2 border-current border-r-transparent text-primary\",\n {\n variants: {\n size: {\n sm: \"size-3\",\n default: \"size-4\",\n lg: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n);\n\nexport interface SpinnerProps\n extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, \"role\">,\n VariantProps<typeof spinnerVariants> {\n /** Accessible text announced while the spinner is active. */\n label?: string;\n class?: string;\n}\n\n/** A compact, accessible loading indicator for async UI states. */\nexport const Spinner: Component<SpinnerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"size\", \"label\", \"class\"]);\n\n return (\n <span\n role=\"status\"\n aria-label={local.label || \"Loading\"}\n class={cn(spinnerVariants({ size: local.size }), local.class)}\n {...rest}\n />\n );\n};\n",
14
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const spinnerVariants = cva(\n \"inline-block animate-spin rounded-lg border-2 border-current border-r-transparent text-primary\",\n {\n variants: {\n size: {\n sm: \"size-3\",\n default: \"size-4\",\n lg: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n);\n\nexport interface SpinnerProps\n extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, \"role\">,\n VariantProps<typeof spinnerVariants> {\n /** Accessible text announced while the spinner is active. */\n label?: string;\n class?: string;\n}\n\n/** A compact, accessible loading indicator for async UI states. */\nexport const Spinner: Component<SpinnerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"size\", \"label\", \"class\"]);\n\n return (\n <span\n role=\"status\"\n aria-label={local.label || \"Loading\"}\n class={cn(spinnerVariants({ size: local.size }), local.class)}\n {...rest}\n />\n );\n};\n",
15
15
  "type": "registry:ui"
16
16
  }
17
17
  ]
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "stat",
3
+ "title": "Stat",
4
+ "description": "Display key performance indicators, statistics, financial data, and metrics with trends and icons.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority",
10
+ "lucide-solid"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "ui/stat.tsx",
15
+ "content": "import {\n splitProps,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { TrendingUp, TrendingDown, Minus } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- 1. Stat Group (Container Grid) --- */\nexport interface StatGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n columns?: 1 | 2 | 3 | 4 | 5;\n}\n\nexport const StatGroup: ParentComponent<StatGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"columns\", \"children\"]);\n\n const columnClass = () => {\n switch (local.columns) {\n case 1:\n return \"grid-cols-1\";\n case 2:\n return \"grid-cols-1 sm:grid-cols-2\";\n case 3:\n return \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\";\n case 5:\n return \"grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5\";\n case 4:\n default:\n return \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-4\";\n }\n };\n\n return (\n <div\n class={cn(\"grid gap-4 sm:gap-5 w-full\", columnClass(), local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 2. Stat Root Card --- */\nexport const statVariants = cva(\n \"relative flex flex-col justify-between rounded-lg transition-all duration-200\",\n {\n variants: {\n variant: {\n default: \"border border-border bg-card text-card-foreground p-4 sm:p-5 shadow-2xs space-y-2\",\n flat: \"bg-muted/40 text-foreground p-4 sm:p-5 space-y-2\",\n bordered: \"border-2 border-border bg-background text-foreground p-4 sm:p-5 space-y-2\",\n ghost: \"bg-transparent text-foreground p-2 space-y-1\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\nexport interface StatProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof statVariants> {\n class?: string;\n}\n\nexport const Stat: ParentComponent<StatProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"children\"]);\n\n return (\n <div\n class={cn(statVariants({ variant: local.variant }), local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 3. Stat Header (Top Row) --- */\nexport interface StatHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const StatHeader: ParentComponent<StatHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"flex items-center justify-between gap-2\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 4. Stat Label / Title --- */\nexport interface StatLabelProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const StatLabel: ParentComponent<StatLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p\n class={cn(\n \"text-xs sm:text-sm font-medium text-muted-foreground leading-tight tracking-tight\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </p>\n );\n};\n\n/* --- 5. Stat Icon Wrapper --- */\nexport interface StatIconProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const StatIcon: ParentComponent<StatIconProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"flex size-4 sm:size-4.5 shrink-0 items-center justify-center text-muted-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 6. Stat Value (Big Number) --- */\nexport interface StatValueProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const StatValue: ParentComponent<StatValueProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"text-2xl sm:text-3xl font-bold tracking-tight text-foreground flex items-baseline gap-1 my-0.5\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 7. Stat Unit / Currency (Prefix or Suffix) --- */\nexport interface StatUnitProps extends JSX.HTMLAttributes<HTMLSpanElement> {\n class?: string;\n}\n\nexport const StatUnit: ParentComponent<StatUnitProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <span\n class={cn(\"text-lg sm:text-xl font-semibold text-muted-foreground self-baseline\", local.class)}\n {...rest}\n >\n {local.children}\n </span>\n );\n};\n\n/* --- 8. Stat Trend (Growth / Decline Indicator - No Background) --- */\nexport const statTrendVariants = cva(\n \"inline-flex items-center gap-1 text-xs font-semibold select-none shrink-0 tracking-tight\",\n {\n variants: {\n type: {\n up: \"text-emerald-600 dark:text-emerald-400\",\n down: \"text-red-600 dark:text-red-400\",\n neutral: \"text-muted-foreground\",\n },\n },\n defaultVariants: {\n type: \"up\",\n },\n }\n);\n\nexport interface StatTrendProps\n extends JSX.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof statTrendVariants> {\n class?: string;\n hideIcon?: boolean;\n}\n\nexport const StatTrend: ParentComponent<StatTrendProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"type\", \"hideIcon\", \"children\"]);\n\n const Icon = () => {\n if (local.hideIcon) return null;\n if (local.type === \"down\") return <TrendingDown class=\"size-3.5 stroke-[2.5]\" />;\n if (local.type === \"neutral\") return <Minus class=\"size-3.5 stroke-[2.5]\" />;\n return <TrendingUp class=\"size-3.5 stroke-[2.5]\" />;\n };\n\n return (\n <span\n class={cn(statTrendVariants({ type: local.type }), local.class)}\n {...rest}\n >\n <Icon />\n <span>{local.children}</span>\n </span>\n );\n};\n\n/* --- 9. Stat Help Text / Bottom Row (Between Alignment) --- */\nexport interface StatHelpTextProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const StatHelpText: ParentComponent<StatHelpTextProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between gap-2 text-xs text-muted-foreground pt-1\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
16
+ "type": "registry:ui"
17
+ }
18
+ ]
19
+ }
@@ -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
  ]
@@ -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,17 @@
1
+ {
2
+ "name": "table",
3
+ "title": "Table",
4
+ "description": "A responsive and accessible data table component with headers, rows, cells, and footer summaries.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "ui/table.tsx",
13
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface TableProps extends JSX.HTMLAttributes<HTMLTableElement> {\n class?: string;\n}\n\n/**\n * Root Table container component wrapped in a responsive scroll container.\n */\nexport const Table: Component<TableProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div class=\"relative w-full overflow-auto\">\n <table\n class={cn(\"w-full caption-bottom text-sm\", local.class)}\n {...rest}\n />\n </div>\n );\n};\n\nexport interface TableHeaderProps extends JSX.HTMLAttributes<HTMLTableSectionElement> {\n class?: string;\n}\n\n/**\n * Header section wrapper for the Table component.\n */\nexport const TableHeader: Component<TableHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <thead\n class={cn(\"[&_tr]:border-b border-border\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface TableBodyProps extends JSX.HTMLAttributes<HTMLTableSectionElement> {\n class?: string;\n}\n\n/**\n * Main body section wrapper for the Table component.\n */\nexport const TableBody: Component<TableBodyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <tbody\n class={cn(\"[&_tr:last-child]:border-0\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface TableFooterProps extends JSX.HTMLAttributes<HTMLTableSectionElement> {\n class?: string;\n}\n\n/**\n * Footer section wrapper for the Table component.\n */\nexport const TableFooter: Component<TableFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <tfoot\n class={cn(\n \"border-t border-border bg-muted/50 font-medium [&>tr]:last:border-b-0\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface TableRowProps extends JSX.HTMLAttributes<HTMLTableRowElement> {\n class?: string;\n}\n\n/**\n * Table row component with hover highlight states.\n */\nexport const TableRow: Component<TableRowProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <tr\n class={cn(\n \"border-b border-border transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface TableHeadProps extends JSX.ThHTMLAttributes<HTMLTableCellElement> {\n class?: string;\n}\n\n/**\n * Header cell component for table columns.\n */\nexport const TableHead: Component<TableHeadProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <th\n class={cn(\n \"h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface TableCellProps extends JSX.TdHTMLAttributes<HTMLTableCellElement> {\n class?: string;\n}\n\n/**\n * Standard data cell component for table rows.\n */\nexport const TableCell: Component<TableCellProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <td\n class={cn(\n \"p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface TableCaptionProps extends JSX.HTMLAttributes<HTMLTableCaptionElement> {\n class?: string;\n}\n\n/**\n * Accessible table caption for describing table contents.\n */\nexport const TableCaption: Component<TableCaptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <caption\n class={cn(\"mt-4 text-xs text-muted-foreground pb-2\", local.class)}\n {...rest}\n />\n );\n};\n",
14
+ "type": "registry:ui"
15
+ }
16
+ ]
17
+ }
@@ -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,7 +11,8 @@
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
  {
@@ -21,7 +22,7 @@
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
  {
@@ -31,7 +32,7 @@
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 } 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 DropdownMenuGroup,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ThemeToggleProps {\n /** Display mode: \"mini\" for compact dropdown, \"max\" for full customizer panel (default: \"mini\") */\n mode?: \"mini\" | \"max\";\n /** Transition animation effect when changing themes (\"none\", \"circular\", \"fade\") */\n effect?: ThemeEffect;\n class?: string;\n}\n\nconst ACCENT_OPTIONS: { name: AccentColor; label: string; color: string }[] = [\n { name: \"amber\", label: \"Amber\", color: \"bg-[#d97706]\" },\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: \"zinc\", label: \"Zinc\", color: \"bg-[#18181b]\" },\n];\n\nconst RADIUS_OPTIONS: { value: Radius; label: string }[] = [\n { value: \"0\", label: \"0\" },\n { value: \"0.3\", label: \"0.3\" },\n { value: \"0.5\", label: \"0.5\" },\n { value: \"0.75\", label: \"0.75\" },\n { value: \"1.0\", label: \"1.0\" },\n];\n\n/**\n * Interactive UI theme switcher supporting mini/max modes and View Transition animations.\n */\nexport const ThemeToggle: Component<ThemeToggleProps> = (props) => {\n const [local] = splitProps(props, [\"mode\", \"effect\", \"class\"]);\n const { theme, setTheme, accent, setAccent, radius, setRadius } = useTheme();\n\n const mode = () => local.mode || \"mini\";\n const effect = () => local.effect || \"none\";\n\n /* Reactive accessor determining whether dark mode is active */\n const isDarkMode = () => {\n const currentTheme = theme();\n if (currentTheme === \"dark\") return true;\n if (currentTheme === \"light\") return false;\n 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 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 <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 ? \"ring-2 ring-primary ring-offset-2 ring-offset-background scale-110\" : \"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 );\n};",
35
36
  "type": "registry:ui"
36
37
  }
37
38
  ]
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "timeline",
3
+ "title": "Timeline",
4
+ "description": "A responsive chronological display for event streams, activity logs, order tracking, and multi-step workflows.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "ui/timeline.tsx",
14
+ "content": "import {\n createContext,\n useContext,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- Timeline Context --- */\ninterface TimelineContextValue {\n orientation: Accessor<\"vertical\" | \"horizontal\">;\n align: Accessor<\"left\" | \"right\" | \"alternate\">;\n size: Accessor<\"sm\" | \"default\" | \"lg\">;\n}\n\nconst TimelineContext = createContext<TimelineContextValue>();\n\nexport interface TimelineProps extends JSX.HTMLAttributes<HTMLOListElement> {\n /** Layout orientation of the timeline */\n orientation?: \"vertical\" | \"horizontal\";\n /** Alignment of content relative to the timeline line */\n align?: \"left\" | \"right\" | \"alternate\";\n /** Sizing of dots and spacing */\n size?: \"sm\" | \"default\" | \"lg\";\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Root container for chronological timeline event sequences.\n */\nexport const Timeline: Component<TimelineProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"orientation\",\n \"align\",\n \"size\",\n \"class\",\n \"children\",\n ]);\n\n const orientation = () => local.orientation ?? \"vertical\";\n const align = () => local.align ?? \"left\";\n const size = () => local.size ?? \"default\";\n\n const contextValue: TimelineContextValue = {\n orientation,\n align,\n size,\n };\n\n return (\n <TimelineContext.Provider value={contextValue}>\n <ol\n role=\"list\"\n data-orientation={orientation()}\n data-align={align()}\n class={cn(\n \"relative flex\",\n orientation() === \"vertical\"\n ? \"flex-col w-full\"\n : \"flex-row w-full items-start\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </ol>\n </TimelineContext.Provider>\n );\n};\n\n/* --- Timeline Item --- */\nexport interface TimelineItemProps extends JSX.HTMLAttributes<HTMLLIElement> {\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Individual event row container in the timeline.\n */\nexport const TimelineItem: Component<TimelineItemProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n const align = () => context?.align() ?? \"left\";\n\n return (\n <li\n class={cn(\n \"group relative flex\",\n isVertical()\n ? \"min-h-[3.5rem] w-full items-start\"\n : \"flex-1 min-w-0 flex-col items-start\",\n isVertical() && align() === \"right\" && \"flex-row-reverse\",\n isVertical() && align() === \"alternate\" && \"[&:nth-child(even)]:flex-row-reverse\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </li>\n );\n};\n\n/* --- Timeline Separator (Dot + Connector wrapper) --- */\nexport interface TimelineSeparatorProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const TimelineSeparator: Component<TimelineSeparatorProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n\n return (\n <div\n aria-hidden=\"true\"\n class={cn(\n \"flex shrink-0 relative\",\n isVertical()\n ? \"flex-col items-center self-stretch\"\n : \"flex-row items-center w-full\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Timeline Point / Dot --- */\nexport const timelinePointVariants = cva(\n \"relative z-10 flex shrink-0 items-center justify-center rounded-lg font-medium transition-all shadow-xs\",\n {\n variants: {\n variant: {\n default: \"border-2 bg-background\",\n solid: \"text-primary-foreground\",\n subtle: \"bg-muted text-muted-foreground\",\n outline: \"border-2 border-border bg-card text-foreground\",\n },\n status: {\n default: \"border-border text-foreground bg-card\",\n primary: \"border-primary bg-primary text-primary-foreground\",\n success: \"border-emerald-500 bg-emerald-500 text-white dark:border-emerald-400 dark:bg-emerald-400\",\n warning: \"border-amber-500 bg-amber-500 text-white dark:border-amber-400 dark:bg-amber-400\",\n destructive: \"border-destructive bg-destructive text-destructive-foreground\",\n muted: \"border-border/60 bg-muted text-muted-foreground\",\n },\n size: {\n sm: \"size-5 text-[10px] [&_svg]:size-3\",\n default: \"size-8 text-xs [&_svg]:size-4\",\n lg: \"size-10 text-sm [&_svg]:size-5\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n status: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport interface TimelinePointProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof timelinePointVariants> {\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Status indicator node or icon container in the timeline separator.\n */\nexport const TimelinePoint: Component<TimelinePointProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"status\",\n \"size\",\n \"class\",\n \"children\",\n ]);\n\n const size = () => local.size || context?.size() || \"default\";\n\n return (\n <div\n class={cn(\n timelinePointVariants({\n variant: local.variant,\n status: local.status,\n size: size(),\n }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Timeline Connector (Line track) --- */\nexport interface TimelineConnectorProps extends JSX.HTMLAttributes<HTMLDivElement> {\n dashed?: boolean;\n class?: string;\n}\n\n/**\n * Line track connecting sequential timeline items.\n */\nexport const TimelineConnector: Component<TimelineConnectorProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"dashed\", \"class\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n\n return (\n <div\n aria-hidden=\"true\"\n class={cn(\n \"transition-colors group-last:hidden\",\n isVertical()\n ? \"w-0.5 flex-1 min-h-6 my-1 bg-border\"\n : \"h-0.5 flex-1 min-w-4 mx-2 bg-border\",\n local.dashed && (\n isVertical()\n ? \"border-l-2 border-dashed border-border bg-transparent w-0\"\n : \"border-t-2 border-dashed border-border bg-transparent h-0\"\n ),\n local.class\n )}\n {...rest}\n />\n );\n};\n\n/* --- Timeline Content (Event Details) --- */\nexport interface TimelineContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Primary container holding the description, title, and body for a timeline event.\n */\nexport const TimelineContent: Component<TimelineContentProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"class\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n const align = () => context?.align() ?? \"left\";\n\n return (\n <div\n class={cn(\n \"flex flex-col\",\n isVertical() ? \"flex-1 pb-6 pt-0.5\" : \"pt-2 pr-2 text-left\",\n isVertical() && align() === \"left\" && \"text-left pl-3.5 pr-0\",\n isVertical() && align() === \"right\" && \"text-right pr-3.5 pl-0\",\n isVertical() && align() === \"alternate\" && \"text-left pl-3.5 group-even:text-right group-even:pr-3.5 group-even:pl-0\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\n/* --- Timeline Opposite Content --- */\nexport interface TimelineOppositeContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Content placed on the opposite side of the timeline separator (e.g. timestamps in alternate layouts).\n */\nexport const TimelineOppositeContent: Component<TimelineOppositeContentProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"class\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n const align = () => context?.align() ?? \"left\";\n\n return (\n <div\n class={cn(\n \"flex flex-col text-xs text-muted-foreground\",\n align() !== \"alternate\" && \"hidden\",\n isVertical()\n ? \"flex-1 pb-6 pt-1 text-right pr-3.5 group-even:text-left group-even:pl-3.5 group-even:pr-0\"\n : \"pb-1 pr-2\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\n/* --- Timeline Title --- */\nexport interface TimelineTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {\n class?: string;\n}\n\nexport const TimelineTitle: Component<TimelineTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <h4\n class={cn(\"text-sm font-semibold tracking-tight text-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\n/* --- Timeline Description --- */\nexport interface TimelineDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const TimelineDescription: Component<TimelineDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <p\n class={cn(\"mt-1 text-sm text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\n/* --- Timeline Time --- */\nexport interface TimelineTimeProps extends JSX.HTMLAttributes<HTMLTimeElement> {\n class?: string;\n}\n\nexport const TimelineTime: Component<TimelineTimeProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <time\n class={cn(\"text-xs font-medium text-muted-foreground/80\", local.class)}\n {...rest}\n />\n );\n};\n",
15
+ "type": "registry:ui"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "titlebar-tabs",
3
+ "title": "Titlebar Tabs",
4
+ "description": "Native draggable tab bar integrated directly inside desktop titlebars for multi-document applications.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority",
10
+ "lucide-solid"
11
+ ],
12
+ "registryDependencies": [
13
+ "button",
14
+ "tooltip",
15
+ "create-document-tabs"
16
+ ],
17
+ "files": [
18
+ {
19
+ "path": "ui/titlebar-tabs.tsx",
20
+ "content": "import {\n createContext,\n useContext,\n splitProps,\n createSignal,\n createEffect,\n onMount,\n onCleanup,\n Show,\n For,\n type Component,\n type JSX,\n type ParentComponent,\n type Accessor,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Plus, X, Pin } from \"lucide-solid\";\nimport { Button } from \"@/components/ui/button\";\nimport { Tooltip, TooltipTrigger, TooltipContent } from \"@/components/ui/tooltip\";\nimport {\n type CreateDocumentTabsReturn,\n type TabItem,\n} from \"@/hooks/create-document-tabs\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- 1. Context & Variants --- */\n\nexport type TitlebarTabsVariant = \"editor\" | \"chrome\" | \"pills\";\n\ninterface TitlebarTabsContextValue {\n value: Accessor<string | undefined>;\n setValue: (value: string) => void;\n variant: Accessor<TitlebarTabsVariant>;\n manager?: CreateDocumentTabsReturn<any>;\n}\n\nconst TitlebarTabsContext = createContext<TitlebarTabsContextValue>();\n\nexport function useTitlebarTabs() {\n const ctx = useContext(TitlebarTabsContext);\n if (!ctx) {\n throw new Error(\"useTitlebarTabs must be used within a <TitlebarTabs /> container\");\n }\n return ctx;\n}\n\nexport const titlebarTabVariants = cva(\n \"group relative inline-flex items-center gap-1.5 px-3 text-xs font-medium select-none cursor-pointer transition-colors shrink-0 max-w-[180px] min-w-[80px]\",\n {\n variants: {\n variant: {\n editor:\n \"h-full py-1 border-r border-border/40 border-t-2 border-t-transparent hover:bg-muted/40 data-[active=true]:bg-background data-[active=true]:border-t-primary data-[active=true]:text-foreground data-[active=false]:text-muted-foreground\",\n chrome:\n \"h-[calc(100%-2px)] py-1 rounded-t-lg border-t border-x border-transparent hover:bg-muted/60 data-[active=true]:border-border/70 data-[active=true]:bg-background data-[active=true]:text-foreground data-[active=true]:-mb-px data-[active=true]:shadow-xs data-[active=false]:text-muted-foreground\",\n pills:\n \"h-7 py-0.5 rounded-md mx-0.5 hover:bg-muted/50 data-[active=true]:bg-primary/10 data-[active=true]:text-primary data-[active=true]:font-semibold data-[active=false]:text-muted-foreground\",\n },\n },\n defaultVariants: {\n variant: \"editor\",\n },\n }\n);\n\n/* --- 2. TitlebarTabs Root Container --- */\n\nexport interface TitlebarTabsProps extends JSX.HTMLAttributes<HTMLElement> {\n /** Reactive tabs manager instance from createDocumentTabs(). */\n manager?: CreateDocumentTabsReturn<any>;\n /** Controlled active tab identifier value (when not using manager). */\n value?: string;\n /** Uncontrolled default active tab identifier value. */\n defaultValue?: string;\n /** Callback triggered when active tab changes. */\n onValueChange?: (value: string) => void;\n /** Callback triggered when the \"+\" add tab button is clicked. */\n onAddTab?: () => void;\n /** Visual chrome styling variant. */\n variant?: TitlebarTabsVariant;\n class?: string;\n}\n\nexport const TitlebarTabs: ParentComponent<TitlebarTabsProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"manager\",\n \"value\",\n \"defaultValue\",\n \"onValueChange\",\n \"onAddTab\",\n \"variant\",\n \"class\",\n \"children\",\n ]);\n\n let scrollContainerRef: HTMLDivElement | undefined;\n\n const [internalValue, setInternalValue] = createSignal(local.defaultValue || local.value);\n\n const activeValue = () => {\n if (local.manager) return local.manager.activeTabId();\n return local.value !== undefined ? local.value : internalValue();\n };\n\n const variant = () => local.variant || \"editor\";\n\n const setValue = (val: string) => {\n if (local.manager) {\n local.manager.setActiveTab(val);\n } else {\n setInternalValue(val);\n local.onValueChange?.(val);\n }\n };\n\n // Mouse wheel horizontal scroll listener (converts vertical wheel deltaY to horizontal scroll)\n onMount(() => {\n if (!scrollContainerRef) return;\n\n const handleWheel = (e: WheelEvent) => {\n if (e.deltaY !== 0) {\n e.preventDefault();\n scrollContainerRef!.scrollLeft += e.deltaY;\n }\n };\n\n scrollContainerRef.addEventListener(\"wheel\", handleWheel, { passive: false });\n onCleanup(() => {\n scrollContainerRef?.removeEventListener(\"wheel\", handleWheel);\n });\n });\n\n // Auto-scroll to end when tabs are added\n createEffect(() => {\n if (local.manager?.count() && scrollContainerRef) {\n scrollContainerRef.scrollTo({\n left: scrollContainerRef.scrollWidth,\n behavior: \"smooth\",\n });\n }\n });\n\n const contextValue: TitlebarTabsContextValue = {\n value: activeValue,\n setValue,\n variant,\n manager: local.manager,\n };\n\n return (\n <TitlebarTabsContext.Provider value={contextValue}>\n <nav\n aria-label=\"Window Tabs\"\n data-no-drag\n data-tauri-drag-region=\"false\"\n style={\"-webkit-app-region: no-drag; app-region: no-drag;\" as any}\n class={cn(\n \"flex flex-1 h-full overflow-hidden z-10 min-w-0 pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\",\n variant() === \"chrome\" ? \"items-end\" : \"items-center\",\n local.class\n )}\n {...rest}\n >\n <Show\n when={local.children}\n fallback={\n /* Automatic Declarative Mode: Tab List with Mouse Wheel Scroll + Anchored Add Button */\n <>\n <div\n ref={scrollContainerRef}\n class={cn(\n \"flex h-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] gap-0 items-center min-w-0 flex-1 scroll-smooth\",\n variant() === \"chrome\" && \"items-end pt-1.5\"\n )}\n >\n <Show when={local.manager}>\n <For each={local.manager!.tabs()}>\n {(tab) => <TitlebarTab tab={tab} />}\n </For>\n </Show>\n </div>\n\n {/* Anchored Add Tab Button (Always visible outside scroll container) */}\n <Show when={local.onAddTab}>\n <div class={cn(\"shrink-0 px-1 z-20 flex items-center\", variant() === \"chrome\" ? \"mb-1\" : \"my-auto\")}>\n <TitlebarTabAddButton onClick={local.onAddTab} />\n </div>\n </Show>\n </>\n }\n >\n {local.children}\n </Show>\n </nav>\n </TitlebarTabsContext.Provider>\n );\n};\n\n/* --- 3. TitlebarTabList --- */\n\nexport interface TitlebarTabListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const TitlebarTabList: ParentComponent<TitlebarTabListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const ctx = useTitlebarTabs();\n let listRef: HTMLDivElement | undefined;\n\n onMount(() => {\n if (!listRef) return;\n const handleWheel = (e: WheelEvent) => {\n if (e.deltaY !== 0) {\n e.preventDefault();\n listRef!.scrollLeft += e.deltaY;\n }\n };\n listRef.addEventListener(\"wheel\", handleWheel, { passive: false });\n onCleanup(() => listRef?.removeEventListener(\"wheel\", handleWheel));\n });\n\n return (\n <div\n ref={listRef}\n data-no-drag\n class={cn(\n \"flex h-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] gap-0 items-center min-w-0 flex-1 scroll-smooth\",\n ctx.variant() === \"chrome\" && \"items-end pt-1.5\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 4. TitlebarTab Individual Item --- */\n\nexport interface TitlebarTabProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onClose\"> {\n /** Tab data object from createDocumentTabs(). Automatically wires all properties. */\n tab?: TabItem<any>;\n value?: string;\n isDirty?: boolean;\n isPinned?: boolean;\n closable?: boolean;\n onClose?: (e: MouseEvent) => void;\n class?: string;\n}\n\nexport const TitlebarTab: ParentComponent<TitlebarTabProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"tab\",\n \"value\",\n \"isDirty\",\n \"isPinned\",\n \"closable\",\n \"onClose\",\n \"class\",\n \"children\",\n \"onClick\",\n \"onKeyDown\",\n ]);\n\n const ctx = useTitlebarTabs();\n let tabRef: HTMLDivElement | undefined;\n\n const tabId = () => local.tab?.id || local.value || \"\";\n const isPinned = () => local.tab?.isPinned ?? local.isPinned ?? false;\n const isDirty = () => local.tab?.isDirty ?? local.isDirty ?? false;\n const closable = () => local.tab?.closable ?? local.closable ?? true;\n const title = () => local.tab?.title || \"\";\n const IconComp = () => local.tab?.icon;\n\n const isActive = () => ctx.value() === tabId();\n\n // Scroll active tab into view when selected safely\n createEffect(() => {\n if (typeof window !== \"undefined\" && isActive() && tabRef) {\n try {\n tabRef.scrollIntoView({\n behavior: \"smooth\",\n block: \"nearest\",\n inline: \"nearest\",\n });\n } catch {\n // Safe fallback in SSR or testing environments\n }\n }\n });\n\n const handleClick = (e: MouseEvent) => {\n ctx.setValue(tabId());\n if (typeof local.onClick === \"function\") {\n (local.onClick as any)(e);\n }\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n ctx.setValue(tabId());\n }\n if (typeof (local as any).onKeyDown === \"function\") {\n ((local as any).onKeyDown)(e);\n }\n };\n\n const handleClose = (e: MouseEvent) => {\n e.stopPropagation();\n if (local.onClose) {\n local.onClose(e);\n } else if (ctx.manager) {\n ctx.manager.closeTab(tabId());\n }\n };\n\n return (\n <div\n ref={tabRef}\n role=\"tab\"\n tabindex=\"0\"\n aria-selected={isActive()}\n data-no-drag\n data-active={isActive() ? \"true\" : \"false\"}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n class={cn(\n titlebarTabVariants({ variant: ctx.variant() }),\n isPinned() && \"min-w-fit px-2 max-w-fit\",\n \"cursor-pointer outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n local.class\n )}\n {...rest}\n >\n {/* Tab Icon & Title Slot */}\n <span class=\"flex items-center gap-1.5 truncate flex-1 pointer-events-none\">\n <Show when={IconComp()}>\n {(() => {\n const Comp = IconComp()!;\n return <Comp class=\"size-3.5 shrink-0 text-primary\" />;\n })()}\n </Show>\n <Show when={local.children} fallback={<span class=\"truncate\">{title()}</span>}>\n {local.children}\n </Show>\n </span>\n\n {/* Pinned Tab Badge */}\n <Show when={isPinned()}>\n <Pin class=\"size-2.5 text-muted-foreground rotate-45 shrink-0\" />\n </Show>\n\n {/* Close Button / Dirty Dot Indicator using Nikala UI Button */}\n <Show when={!isPinned() && closable()}>\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleClose}\n aria-label={`Close ${tabId()} tab`}\n class=\"size-4 p-0 rounded-xs text-muted-foreground hover:bg-muted hover:text-foreground shrink-0 ml-0.5 cursor-pointer\"\n >\n <Show\n when={isDirty()}\n fallback={<X class=\"size-3 opacity-60 hover:opacity-100\" />}\n >\n <span class=\"size-1.5 rounded-full bg-primary group-hover:hidden\" />\n <X class=\"size-3 hidden group-hover:block\" />\n </Show>\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">\n Close tab (Ctrl+W)\n </TooltipContent>\n </Tooltip>\n </Show>\n </div>\n );\n};\n\n/* --- 5. TitlebarTabAddButton using Nikala UI Button & Tooltip --- */\n\nexport interface TitlebarTabAddButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const TitlebarTabAddButton: Component<TitlebarTabAddButtonProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n data-no-drag\n aria-label=\"Add new tab\"\n class={cn(\n \"size-6 rounded-md text-muted-foreground hover:bg-muted/80 hover:text-foreground cursor-pointer shrink-0\",\n local.class\n )}\n {...rest}\n >\n <Plus class=\"size-3.5\" />\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">\n New tab (Ctrl+T)\n </TooltipContent>\n </Tooltip>\n );\n};\n",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "titlebar",
3
+ "title": "Titlebar",
4
+ "description": "A native-feeling custom titlebar for frameless desktop windows supporting macOS Traffic Lights and Windows 11 controls.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority",
10
+ "lucide-solid"
11
+ ],
12
+ "registryDependencies": [
13
+ "titlebar-tabs",
14
+ "button",
15
+ "tooltip",
16
+ "create-tauri-window",
17
+ "create-document-tabs"
18
+ ],
19
+ "files": [
20
+ {
21
+ "path": "ui/titlebar.tsx",
22
+ "content": "import {\n createContext,\n useContext,\n splitProps,\n createMemo,\n Show,\n type Component,\n type JSX,\n type ParentComponent,\n type Accessor,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { createTauriWindow, type TauriPlatform, detectPlatform } from \"@/hooks/create-tauri-window\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- 1. Context & Types --- */\nexport type TitlebarPlatform = \"macos\" | \"windows\" | \"auto\";\n\ninterface TitlebarContextValue {\n platform: Accessor<\"macos\" | \"windows\">;\n isMaximized: Accessor<boolean>;\n minimize: () => Promise<void> | void;\n toggleMaximize: () => Promise<void> | void;\n close: () => Promise<void> | void;\n}\n\nconst TitlebarContext = createContext<TitlebarContextValue>();\n\nexport function useTitlebar() {\n const context = useContext(TitlebarContext);\n if (!context) {\n throw new Error(\"useTitlebar must be used within a <Titlebar />\");\n }\n return context;\n}\n\n/* --- 2. Titlebar Variants --- */\nexport const titlebarVariants = cva(\n \"relative flex w-full select-none items-center justify-between border-b transition-colors shrink-0 sticky top-0 z-50\",\n {\n variants: {\n variant: {\n default: \"border-border/50 bg-background text-foreground\",\n translucent: \"border-border/40 bg-background/80 backdrop-blur-md text-foreground\",\n floating: \"m-2 rounded-lg border border-border bg-card shadow-sm text-card-foreground\",\n transparent: \"border-transparent bg-transparent text-foreground\",\n },\n size: {\n sm: \"h-8 text-xs px-2.5\",\n default: \"h-9 text-xs px-3\",\n lg: \"h-11 text-sm px-4\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\n/* --- 3. Root Titlebar Component --- */\nexport interface TitlebarProps\n extends JSX.HTMLAttributes<HTMLElement>,\n VariantProps<typeof titlebarVariants> {\n /** Explicit platform style, or 'auto' to detect host OS. */\n platform?: TitlebarPlatform;\n /** Optional controlled maximize state override. */\n isMaximized?: boolean;\n /** Optional minimize callback override. */\n onMinimize?: () => void | Promise<void>;\n /** Optional maximize callback override. */\n onToggleMaximize?: () => void | Promise<void>;\n /** Optional close callback override. */\n onClose?: () => void | Promise<void>;\n /** Whether double-clicking the titlebar toggles window maximize state. Defaults to true. */\n doubleClickToMaximize?: boolean;\n}\n\nexport const Titlebar: ParentComponent<TitlebarProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"platform\",\n \"isMaximized\",\n \"onMinimize\",\n \"onToggleMaximize\",\n \"onClose\",\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n \"doubleClickToMaximize\",\n \"onDblClick\",\n ]);\n\n const windowCtrl = createTauriWindow();\n\n const resolvedPlatform = createMemo<\"macos\" | \"windows\">(() => {\n if (local.platform && local.platform !== \"auto\") {\n return local.platform;\n }\n const detected = detectPlatform();\n return detected === \"macos\" ? \"macos\" : \"windows\";\n });\n\n const isMaximized = () =>\n local.isMaximized !== undefined ? local.isMaximized : windowCtrl.isMaximized();\n\n const minimize = () => (local.onMinimize ? local.onMinimize() : windowCtrl.minimize());\n const toggleMaximize = () =>\n local.onToggleMaximize ? local.onToggleMaximize() : windowCtrl.toggleMaximize();\n const close = () => (local.onClose ? local.onClose() : windowCtrl.close());\n\n const handleMouseDown: JSX.EventHandlerUnion<HTMLElement, MouseEvent> = (e) => {\n if (e.button === 0) {\n const target = e.target as HTMLElement;\n if (!target.closest(\"button, a, input, select, textarea, [data-no-drag], [data-tauri-drag-region='false']\")) {\n windowCtrl.startDragging(e);\n }\n }\n if (typeof (local as any).onMouseDown === \"function\") {\n ((local as any).onMouseDown)(e);\n }\n };\n\n const handleDoubleClick: JSX.EventHandlerUnion<HTMLElement, MouseEvent> = (e) => {\n if (local.doubleClickToMaximize !== false) {\n // Don't maximize if user double-clicked an interactive button\n const target = e.target as HTMLElement;\n if (!target.closest(\"button, [data-no-drag], [data-tauri-drag-region='false']\")) {\n toggleMaximize();\n }\n }\n if (typeof local.onDblClick === \"function\") {\n (local.onDblClick as any)(e);\n }\n };\n\n const contextValue: TitlebarContextValue = {\n platform: resolvedPlatform,\n isMaximized,\n minimize,\n toggleMaximize,\n close,\n };\n\n return (\n <TitlebarContext.Provider value={contextValue}>\n <header\n data-tauri-drag-region\n onMouseDown={handleMouseDown}\n onDblClick={handleDoubleClick}\n class={cn(\n titlebarVariants({ variant: local.variant, size: local.size }),\n \"select-none cursor-default\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </header>\n </TitlebarContext.Provider>\n );\n};\n\n/* --- 4. Subcomponents --- */\n\n/**\n * Traffic light buttons for macOS or Fluent control buttons for Windows.\n */\nexport interface TitlebarControlsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Optional override platform style. Defaults to Titlebar context. */\n platform?: \"macos\" | \"windows\";\n}\n\nexport const TitlebarControls: Component<TitlebarControlsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"platform\", \"class\", \"style\"]);\n const ctx = useTitlebar();\n const activePlatform = () => local.platform || ctx.platform();\n\n return (\n <div\n data-no-drag\n data-tauri-drag-region=\"false\"\n style={(\"-webkit-app-region: no-drag; app-region: no-drag;\" + (typeof local.style === \"string\" ? ` ${local.style}` : \"\")) as any}\n class={cn(\n \"flex items-center z-10 self-stretch shrink-0 pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\",\n activePlatform() === \"macos\"\n ? \"gap-2 px-1 order-first\"\n : \"gap-0 -mr-3 h-full order-last ml-auto\",\n local.class\n )}\n {...rest}\n >\n <Show\n when={activePlatform() === \"macos\"}\n fallback={\n /* Windows 11 Fluent Window Controls */\n <div class=\"flex items-center h-full self-stretch\">\n {/* Minimize */}\n <button\n type=\"button\"\n onClick={() => ctx.minimize()}\n class=\"inline-flex h-full w-11 items-center justify-center text-muted-foreground hover:bg-muted hover:text-foreground transition-colors cursor-pointer\"\n aria-label=\"Minimize Window\"\n >\n <svg class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 12 12\">\n <path stroke=\"currentColor\" stroke-width=\"1\" d=\"M1.5 6h9\" />\n </svg>\n </button>\n\n {/* Maximize / Restore */}\n <button\n type=\"button\"\n onClick={() => ctx.toggleMaximize()}\n class=\"inline-flex h-full w-11 items-center justify-center text-muted-foreground hover:bg-muted hover:text-foreground transition-colors cursor-pointer\"\n aria-label={ctx.isMaximized() ? \"Restore Window\" : \"Maximize Window\"}\n >\n <Show\n when={ctx.isMaximized()}\n fallback={\n <svg class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 12 12\">\n <rect width=\"8\" height=\"8\" x=\"2\" y=\"2\" stroke=\"currentColor\" stroke-width=\"1\" rx=\"0.5\" />\n </svg>\n }\n >\n <svg class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 12 12\">\n <path stroke=\"currentColor\" stroke-width=\"1\" d=\"M3.5 3.5v-1.5h6v6h-1.5M2 4.5h6v6h-6z\" />\n </svg>\n </Show>\n </button>\n\n {/* Close */}\n <button\n type=\"button\"\n onClick={() => ctx.close()}\n class=\"inline-flex h-full w-11 items-center justify-center text-muted-foreground hover:bg-destructive hover:text-destructive-foreground transition-colors cursor-pointer\"\n aria-label=\"Close Window\"\n >\n <svg class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 12 12\">\n <path stroke=\"currentColor\" stroke-width=\"1\" d=\"M2.5 2.5l7 7M9.5 2.5l-7 7\" />\n </svg>\n </button>\n </div>\n }\n >\n {/* macOS Traffic Lights */}\n <div class=\"group/traffic flex items-center gap-2\">\n {/* Close */}\n <button\n type=\"button\"\n onClick={() => ctx.close()}\n class=\"flex size-3 items-center justify-center rounded-full bg-[#ff5f57] border border-[#e0443e] cursor-pointer\"\n aria-label=\"Close Window\"\n >\n <svg\n class=\"size-1.5 text-[#4c0000] opacity-0 group-hover/traffic:opacity-100 transition-opacity\"\n viewBox=\"0 0 6 6\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.2\"\n >\n <path d=\"M1 1l4 4M5 1L1 5\" />\n </svg>\n </button>\n\n {/* Minimize */}\n <button\n type=\"button\"\n onClick={() => ctx.minimize()}\n class=\"flex size-3 items-center justify-center rounded-full bg-[#febc2e] border border-[#d89e24] cursor-pointer\"\n aria-label=\"Minimize Window\"\n >\n <svg\n class=\"size-1.5 text-[#5c3e00] opacity-0 group-hover/traffic:opacity-100 transition-opacity\"\n viewBox=\"0 0 6 6\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.2\"\n >\n <path d=\"M1 3h4\" />\n </svg>\n </button>\n\n {/* Maximize */}\n <button\n type=\"button\"\n onClick={() => ctx.toggleMaximize()}\n class=\"flex size-3 items-center justify-center rounded-full bg-[#28c840] border border-[#1aab29] cursor-pointer\"\n aria-label=\"Maximize Window\"\n >\n <svg\n class=\"size-1.5 text-[#003d07] opacity-0 group-hover/traffic:opacity-100 transition-opacity\"\n viewBox=\"0 0 6 6\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1\"\n >\n <path d=\"M1 4.5l3.5-3.5M4.5 1h-3M4.5 1v3\" />\n </svg>\n </button>\n </div>\n </Show>\n </div>\n );\n};\n\n/**\n * Title text container. Placed naturally in JSX flow by default, or centered with align=\"center\".\n */\nexport interface TitlebarTitleProps extends JSX.HTMLAttributes<HTMLDivElement> {\n align?: \"center\" | \"start\";\n}\n\nexport const TitlebarTitle: ParentComponent<TitlebarTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"align\", \"class\", \"children\"]);\n\n const isCentered = () => local.align === \"center\";\n\n return (\n <div\n data-tauri-drag-region\n class={cn(\n \"flex items-center gap-2 font-medium truncate text-xs text-foreground\",\n isCentered()\n ? \"absolute left-1/2 -translate-x-1/2 pointer-events-none text-center justify-center max-w-[calc(100%-160px)]\"\n : \"shrink-0\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/**\n * App icon slot within Titlebar.\n */\nexport const TitlebarIcon: ParentComponent<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n return (\n <div\n data-no-drag\n data-tauri-drag-region=\"false\"\n class={cn(\n \"flex shrink-0 items-center justify-center size-4 text-muted-foreground pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/**\n * Action buttons container (Search, Settings, ThemeToggle, Navigation) placed in the titlebar.\n * Supports start, end, center, or natural flow alignment.\n */\nexport interface TitlebarActionsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Alignment of the actions container within the titlebar. Defaults to 'end'. */\n align?: \"start\" | \"end\" | \"center\" | \"none\";\n}\n\nexport const TitlebarActions: ParentComponent<TitlebarActionsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"align\", \"class\", \"children\", \"style\"]);\n\n const alignClass = () => {\n switch (local.align) {\n case \"start\":\n return \"mr-auto\";\n case \"center\":\n return \"mx-auto\";\n case \"none\":\n return \"\";\n case \"end\":\n default:\n return \"ml-auto\";\n }\n };\n\n return (\n <div\n data-no-drag\n data-tauri-drag-region=\"false\"\n style={(\"-webkit-app-region: no-drag; app-region: no-drag;\" + (typeof local.style === \"string\" ? ` ${local.style}` : \"\")) as any}\n class={cn(\n \"flex items-center gap-1 shrink-0 z-10 px-1 pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\",\n alignClass(),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 6. Re-export TitlebarTabs Suite --- */\nexport * from \"./titlebar-tabs\";\n",
23
+ "type": "registry:ui"
24
+ }
25
+ ]
26
+ }