@nikala-ui/core 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +97 -20
  3. package/package.json +43 -2
  4. package/registry/api-table.json +21 -0
  5. package/registry/banner.json +1 -1
  6. package/registry/button.json +1 -1
  7. package/registry/callout.json +19 -0
  8. package/registry/checkbox.json +4 -1
  9. package/registry/code-block.json +26 -0
  10. package/registry/code-group.json +20 -0
  11. package/registry/combobox.json +1 -1
  12. package/registry/command.json +3 -2
  13. package/registry/component-viewer.json +25 -0
  14. package/registry/container.json +18 -0
  15. package/registry/context-menu.json +1 -1
  16. package/registry/create-app-updater.json +13 -0
  17. package/registry/create-document-tabs.json +13 -0
  18. package/registry/create-global-shortcut.json +13 -0
  19. package/registry/create-tauri-window.json +13 -0
  20. package/registry/create-tiptap-editor.json +34 -0
  21. package/registry/dialog.json +1 -1
  22. package/registry/dropdown-menu.json +3 -2
  23. package/registry/field.json +1 -1
  24. package/registry/file-tree.json +21 -0
  25. package/registry/footer.json +1 -1
  26. package/registry/form-message.json +3 -2
  27. package/registry/icon-button.json +1 -1
  28. package/registry/index.json +336 -5
  29. package/registry/navbar.json +1 -1
  30. package/registry/navigation-menu.json +1 -1
  31. package/registry/number-input.json +1 -1
  32. package/registry/package-manager-tabs.json +24 -0
  33. package/registry/pager.json +21 -0
  34. package/registry/popover.json +4 -1
  35. package/registry/resizable.json +1 -1
  36. package/registry/rich-text-editor.json +95 -0
  37. package/registry/scroll-area.json +1 -1
  38. package/registry/section-heading.json +21 -0
  39. package/registry/select.json +3 -2
  40. package/registry/sheet.json +1 -1
  41. package/registry/sidebar.json +1 -1
  42. package/registry/status.json +1 -1
  43. package/registry/steps.json +20 -0
  44. package/registry/switch.json +4 -1
  45. package/registry/table-of-contents.json +23 -0
  46. package/registry/tabs.json +4 -1
  47. package/registry/theme-manager.json +6 -5
  48. package/registry/titlebar-tabs.json +24 -0
  49. package/registry/titlebar.json +26 -0
  50. package/registry/toggle-group.json +1 -1
  51. package/registry/updater-modal.json +26 -0
  52. package/src/index.ts +48 -0
  53. package/src/registry/components/ui/api-table.tsx +117 -0
  54. package/src/registry/components/ui/banner.tsx +0 -2
  55. package/src/registry/components/ui/button.tsx +1 -1
  56. package/src/registry/components/ui/callout.tsx +137 -0
  57. package/src/registry/components/ui/checkbox.tsx +1 -1
  58. package/src/registry/components/ui/code-block.tsx +326 -0
  59. package/src/registry/components/ui/code-group.tsx +105 -0
  60. package/src/registry/components/ui/combobox.tsx +83 -18
  61. package/src/registry/components/ui/command.tsx +110 -106
  62. package/src/registry/components/ui/component-viewer.tsx +363 -0
  63. package/src/registry/components/ui/container.tsx +45 -0
  64. package/src/registry/components/ui/context-menu.tsx +67 -22
  65. package/src/registry/components/ui/dialog.tsx +42 -32
  66. package/src/registry/components/ui/dropdown-menu.tsx +69 -31
  67. package/src/registry/components/ui/field.tsx +7 -3
  68. package/src/registry/components/ui/file-tree.tsx +181 -0
  69. package/src/registry/components/ui/footer.tsx +1 -1
  70. package/src/registry/components/ui/form-message.tsx +2 -2
  71. package/src/registry/components/ui/icon-button.tsx +1 -1
  72. package/src/registry/components/ui/navbar.tsx +18 -11
  73. package/src/registry/components/ui/navigation-menu.tsx +2 -2
  74. package/src/registry/components/ui/number-input.tsx +3 -3
  75. package/src/registry/components/ui/package-manager-tabs.tsx +226 -0
  76. package/src/registry/components/ui/pager.tsx +102 -0
  77. package/src/registry/components/ui/popover.tsx +1 -1
  78. package/src/registry/components/ui/resizable.tsx +3 -1
  79. package/src/registry/components/ui/rich-text-editor/bubble-menu.tsx +236 -0
  80. package/src/registry/components/ui/rich-text-editor/editor.tsx +308 -0
  81. package/src/registry/components/ui/rich-text-editor/extensions.ts +116 -0
  82. package/src/registry/components/ui/rich-text-editor/footer.tsx +36 -0
  83. package/src/registry/components/ui/rich-text-editor/image-dialog.tsx +75 -0
  84. package/src/registry/components/ui/rich-text-editor/index.ts +11 -0
  85. package/src/registry/components/ui/rich-text-editor/link-dialog.tsx +48 -0
  86. package/src/registry/components/ui/rich-text-editor/markdown.ts +170 -0
  87. package/src/registry/components/ui/rich-text-editor/slash-command.tsx +301 -0
  88. package/src/registry/components/ui/rich-text-editor/table-controls.tsx +68 -0
  89. package/src/registry/components/ui/rich-text-editor/toolbar.tsx +465 -0
  90. package/src/registry/components/ui/rich-text-editor/use-editor.ts +231 -0
  91. package/src/registry/components/ui/scroll-area.tsx +12 -2
  92. package/src/registry/components/ui/section-heading.tsx +108 -0
  93. package/src/registry/components/ui/select.tsx +16 -19
  94. package/src/registry/components/ui/sheet.tsx +58 -53
  95. package/src/registry/components/ui/sidebar.tsx +4 -4
  96. package/src/registry/components/ui/status.tsx +7 -5
  97. package/src/registry/components/ui/steps.tsx +198 -0
  98. package/src/registry/components/ui/switch.tsx +1 -1
  99. package/src/registry/components/ui/table-of-contents.tsx +131 -0
  100. package/src/registry/components/ui/tabs.tsx +1 -1
  101. package/src/registry/components/ui/theme-toggle.tsx +175 -139
  102. package/src/registry/components/ui/titlebar-tabs.tsx +773 -0
  103. package/src/registry/components/ui/titlebar.tsx +468 -0
  104. package/src/registry/components/ui/toggle-group.tsx +1 -1
  105. package/src/registry/components/ui/updater-modal.tsx +254 -0
  106. package/src/registry/metadata.ts +167 -6
  107. package/src/registry/providers/theme-provider.tsx +16 -8
  108. package/src/registry/providers/theme-script.tsx +29 -9
  109. package/src/registry/providers/theme-transitions.ts +81 -50
@@ -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, XCircle, CircleX } from \"lucide-solid\";\nimport * as ContextMenuPrimitive from \"@kobalte/core/context-menu\";\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 reorderable?: boolean;\n enableContextMenu?: boolean;\n draggedTabId: Accessor<string | null>;\n dropTargetId: Accessor<string | null>;\n dropPosition: Accessor<\"left\" | \"right\" | null>;\n setDragState: (draggedId: string | null, targetId: string | null, position: \"left\" | \"right\" | null) => void;\n handleDropReorder: (sourceId: string, targetId: string, position: \"left\" | \"right\") => void;\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 /** Whether tabs can be reordered via Drag & Drop. Defaults to true when manager is present. */\n reorderable?: boolean;\n /** Whether right-click context menu is enabled on tabs and empty space. Defaults to true. */\n enableContextMenu?: boolean;\n /** Custom render function for the empty tabstrip context menu. */\n renderEmptyContextMenu?: () => JSX.Element;\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 \"reorderable\",\n \"enableContextMenu\",\n \"renderEmptyContextMenu\",\n \"variant\",\n \"class\",\n \"children\",\n ]);\n\n let scrollContainerRef: HTMLDivElement | undefined;\n\n const [internalValue, setInternalValue] = createSignal(local.defaultValue || local.value);\n const [draggedTabId, setDraggedTabId] = createSignal<string | null>(null);\n const [dropTargetId, setDropTargetId] = createSignal<string | null>(null);\n const [dropPosition, setDropPosition] = createSignal<\"left\" | \"right\" | null>(null);\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 const setDragState = (\n draggedId: string | null,\n targetId: string | null,\n pos: \"left\" | \"right\" | null\n ) => {\n setDraggedTabId(draggedId);\n setDropTargetId(targetId);\n setDropPosition(pos);\n };\n\n const handleDropReorder = (sourceId: string, targetId: string, pos: \"left\" | \"right\") => {\n if (!local.manager) return;\n const list = local.manager.tabs();\n const fromIndex = list.findIndex((t) => t.id === sourceId);\n const targetIndex = list.findIndex((t) => t.id === targetId);\n if (fromIndex === -1 || targetIndex === -1 || fromIndex === targetIndex) return;\n\n let finalIndex = targetIndex;\n if (fromIndex < targetIndex && pos === \"left\") {\n finalIndex = targetIndex - 1;\n } else if (fromIndex > targetIndex && pos === \"right\") {\n finalIndex = targetIndex + 1;\n }\n\n if (fromIndex !== finalIndex && finalIndex >= 0 && finalIndex < list.length) {\n local.manager.reorderTabs(fromIndex, finalIndex);\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 reorderable: local.reorderable ?? Boolean(local.manager),\n enableContextMenu: local.enableContextMenu ?? true,\n draggedTabId,\n dropTargetId,\n dropPosition,\n setDragState,\n handleDropReorder,\n };\n\n const hasEmptyContextMenu = () =>\n (local.enableContextMenu ?? true) &&\n (Boolean(local.onAddTab) || Boolean(local.manager) || Boolean(local.renderEmptyContextMenu));\n\n const renderNavContent = () => (\n <nav\n aria-label=\"Window Tabs\"\n class={cn(\n \"flex flex-1 h-full overflow-hidden z-10 min-w-0 pointer-events-auto [app-region:drag] [-webkit-app-region: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 [app-region:drag] [-webkit-app-region:drag]\",\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 );\n\n return (\n <TitlebarTabsContext.Provider value={contextValue}>\n <Show when={hasEmptyContextMenu()} fallback={renderNavContent()}>\n <ContextMenuPrimitive.Root>\n <ContextMenuPrimitive.Trigger\n as=\"div\"\n style={(\"-webkit-app-region: no-drag; app-region: no-drag; outline: none !important; -webkit-focus-ring-color: transparent !important;\") as any}\n class=\"flex flex-1 h-full min-w-0 outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0\"\n >\n {renderNavContent()}\n </ContextMenuPrimitive.Trigger>\n <ContextMenuPrimitive.Portal>\n <ContextMenuPrimitive.Content\n data-no-drag\n data-tauri-drag-region=\"false\"\n style=\"-webkit-app-region: no-drag; app-region: no-drag; outline: none !important; -webkit-focus-ring-color: transparent !important;\"\n onMouseDown={(e) => e.stopPropagation()}\n onPointerDown={(e) => e.stopPropagation()}\n class={cn(\n \"z-50 min-w-[160px] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md\",\n \"animate-in fade-in-80 slide-in-from-top-1 text-xs pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\",\n \"outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0\"\n )}\n >\n <Show\n when={local.renderEmptyContextMenu}\n fallback={\n <>\n <Show when={local.onAddTab}>\n <ContextMenuPrimitive.Item\n data-no-drag\n data-tauri-drag-region=\"false\"\n style=\"-webkit-app-region: no-drag; app-region: no-drag;\"\n onMouseDown={(e) => e.stopPropagation()}\n onPointerDown={(e) => e.stopPropagation()}\n onSelect={() => {\n setTimeout(() => local.onAddTab?.(), 0);\n }}\n class=\"relative flex cursor-pointer select-none items-center justify-between gap-2 rounded-xs px-2 py-1.5 text-xs outline-none transition-colors hover:bg-accent hover:text-accent-foreground pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\"\n >\n <span class=\"flex items-center gap-2\">\n <Plus class=\"size-3.5 text-muted-foreground\" />\n New Tab\n </span>\n <span class=\"ml-auto text-[10px] text-muted-foreground font-mono\">Ctrl+T</span>\n </ContextMenuPrimitive.Item>\n </Show>\n\n <Show when={local.manager && local.manager.tabs().length > 0}>\n <Show when={local.onAddTab}>\n <ContextMenuPrimitive.Separator class=\"my-1 h-px bg-border/60\" />\n </Show>\n <ContextMenuPrimitive.Item\n data-no-drag\n data-tauri-drag-region=\"false\"\n style=\"-webkit-app-region: no-drag; app-region: no-drag;\"\n onMouseDown={(e) => e.stopPropagation()}\n onPointerDown={(e) => e.stopPropagation()}\n onSelect={() => {\n setTimeout(() => local.manager?.closeAll(), 0);\n }}\n class=\"relative flex cursor-pointer select-none items-center gap-2 rounded-xs px-2 py-1.5 text-xs outline-none transition-colors hover:bg-accent hover:text-accent-foreground pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\"\n >\n <CircleX class=\"size-3.5 text-muted-foreground\" />\n <span>Close All Tabs</span>\n </ContextMenuPrimitive.Item>\n </Show>\n </>\n }\n >\n {local.renderEmptyContextMenu!()}\n </Show>\n </ContextMenuPrimitive.Content>\n </ContextMenuPrimitive.Portal>\n </ContextMenuPrimitive.Root>\n </Show>\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 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 [app-region:drag] [-webkit-app-region:drag]\",\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 reorderable?: boolean;\n /** Whether right-click context menu is enabled on this tab. Defaults to true. */\n enableContextMenu?: boolean;\n /** Custom render function for context menu content. */\n renderContextMenu?: (tab: TabItem<any>) => JSX.Element;\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 \"reorderable\",\n \"enableContextMenu\",\n \"renderContextMenu\",\n \"onClose\",\n \"class\",\n \"style\",\n \"children\",\n \"onClick\",\n \"onKeyDown\",\n \"onDragStart\",\n \"onDragOver\",\n \"onDragLeave\",\n \"onDragEnd\",\n \"onDrop\",\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 const isDragging = () => ctx.draggedTabId() === tabId();\n const isDropTarget = () => ctx.dropTargetId() === tabId() && ctx.draggedTabId() !== tabId();\n const dropPosition = () => (isDropTarget() ? ctx.dropPosition() : null);\n\n const canDrag = () => !isPinned() && (local.reorderable ?? ctx.reorderable ?? true);\n const hasContextMenu = () =>\n (local.enableContextMenu ?? ctx.enableContextMenu ?? true) &&\n (Boolean(ctx.manager) || Boolean(local.renderContextMenu));\n\n // Scroll active tab horizontally inside its tablist container\n createEffect(() => {\n if (typeof window !== \"undefined\" && isActive() && tabRef && tabRef.parentElement) {\n try {\n const container = tabRef.parentElement;\n const tabLeft = tabRef.offsetLeft;\n const tabRight = tabLeft + tabRef.offsetWidth;\n const containerLeft = container.scrollLeft;\n const containerRight = containerLeft + container.clientWidth;\n\n if (tabLeft < containerLeft) {\n container.scrollTo({ left: tabLeft, behavior: \"smooth\" });\n } else if (tabRight > containerRight) {\n container.scrollTo({ left: tabRight - container.clientWidth, behavior: \"smooth\" });\n }\n } catch {\n // Safe fallback\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 const handleDragStart: JSX.EventHandlerUnion<HTMLDivElement, DragEvent> = (e) => {\n if (!canDrag()) {\n e.preventDefault();\n return;\n }\n if (e.dataTransfer) {\n e.dataTransfer.setData(\"text/plain\", tabId());\n e.dataTransfer.effectAllowed = \"move\";\n }\n ctx.setDragState(tabId(), null, null);\n if (typeof local.onDragStart === \"function\") {\n (local.onDragStart as any)(e);\n }\n };\n\n const handleDragOver: JSX.EventHandlerUnion<HTMLDivElement, DragEvent> = (e) => {\n const currentDragged = ctx.draggedTabId();\n if (!currentDragged || currentDragged === tabId() || isPinned()) return;\n\n e.preventDefault();\n if (e.dataTransfer) {\n e.dataTransfer.dropEffect = \"move\";\n }\n\n if (tabRef) {\n const rect = tabRef.getBoundingClientRect();\n const midpoint = rect.left + rect.width / 2;\n const pos: \"left\" | \"right\" = e.clientX < midpoint ? \"left\" : \"right\";\n ctx.setDragState(currentDragged, tabId(), pos);\n }\n\n if (typeof local.onDragOver === \"function\") {\n (local.onDragOver as any)(e);\n }\n };\n\n const handleDragLeave: JSX.EventHandlerUnion<HTMLDivElement, DragEvent> = (e) => {\n if (tabRef && !tabRef.contains(e.relatedTarget as Node)) {\n if (ctx.dropTargetId() === tabId()) {\n ctx.setDragState(ctx.draggedTabId(), null, null);\n }\n }\n if (typeof local.onDragLeave === \"function\") {\n (local.onDragLeave as any)(e);\n }\n };\n\n const handleDragEnd: JSX.EventHandlerUnion<HTMLDivElement, DragEvent> = (e) => {\n ctx.setDragState(null, null, null);\n if (typeof local.onDragEnd === \"function\") {\n (local.onDragEnd as any)(e);\n }\n };\n\n const handleDrop: JSX.EventHandlerUnion<HTMLDivElement, DragEvent> = (e) => {\n e.preventDefault();\n const sourceId = e.dataTransfer?.getData(\"text/plain\") || ctx.draggedTabId();\n const pos = ctx.dropPosition();\n if (sourceId && pos && sourceId !== tabId()) {\n ctx.handleDropReorder(sourceId, tabId(), pos);\n }\n ctx.setDragState(null, null, null);\n if (typeof local.onDrop === \"function\") {\n (local.onDrop as any)(e);\n }\n };\n\n const renderTabElement = () => (\n <div\n ref={tabRef}\n role=\"tab\"\n tabindex=\"0\"\n aria-selected={isActive()}\n draggable={canDrag()}\n onDragStart={handleDragStart}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDragEnd={handleDragEnd}\n onDrop={handleDrop}\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 data-active={isActive() ? \"true\" : \"false\"}\n data-dragging={isDragging() ? \"true\" : undefined}\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 isDragging() && \"opacity-40 scale-95\",\n dropPosition() === \"left\" &&\n \"before:absolute before:left-0 before:top-1 before:bottom-1 before:w-0.5 before:bg-primary before:z-30 before:rouded-lg\",\n dropPosition() === \"right\" &&\n \"after:absolute after:right-0 after:top-1 after:bottom-1 after:w-0.5 after:bg-primary after:z-30 after:rouded-lg\",\n \"cursor-pointer outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [app-region:no-drag] [-webkit-app-region:no-drag] pointer-events-auto transition-all duration-150\",\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 outline-none ring-0 focus:ring-0 focus-visible:ring-0\"\n >\n <Show\n when={isDirty()}\n fallback={<X class=\"size-3 opacity-60 hover:opacity-100\" />}\n >\n <span class=\"size-1.5 rouded-lg 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 return (\n <Show when={hasContextMenu()} fallback={renderTabElement()}>\n <ContextMenuPrimitive.Root>\n <ContextMenuPrimitive.Trigger\n as=\"div\"\n data-no-drag\n data-tauri-drag-region=\"false\"\n style={(\"-webkit-app-region: no-drag; app-region: no-drag; outline: none !important; -webkit-focus-ring-color: transparent !important;\" + (typeof local.style === \"string\" ? ` ${local.style}` : \"\")) as any}\n class=\"h-full flex items-center shrink-0 pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag] outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0\"\n >\n {renderTabElement()}\n </ContextMenuPrimitive.Trigger>\n <ContextMenuPrimitive.Portal>\n <ContextMenuPrimitive.Content\n data-no-drag\n data-tauri-drag-region=\"false\"\n style=\"-webkit-app-region: no-drag; app-region: no-drag; outline: none !important; -webkit-focus-ring-color: transparent !important;\"\n onMouseDown={(e) => e.stopPropagation()}\n onPointerDown={(e) => e.stopPropagation()}\n class={cn(\n \"z-50 min-w-[170px] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md\",\n \"animate-in fade-in-80 slide-in-from-top-1 text-xs pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\",\n \"outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0\"\n )}\n >\n <Show\n when={local.renderContextMenu}\n fallback={\n <>\n <Show when={ctx.manager}>\n <ContextMenuPrimitive.Item\n data-no-drag\n data-tauri-drag-region=\"false\"\n style=\"-webkit-app-region: no-drag; app-region: no-drag;\"\n onMouseDown={(e) => e.stopPropagation()}\n onPointerDown={(e) => e.stopPropagation()}\n onSelect={() => {\n setTimeout(() => ctx.manager?.togglePin(tabId()), 0);\n }}\n class=\"relative flex cursor-pointer select-none items-center gap-2 rounded-xs px-2 py-1.5 text-xs outline-none transition-colors hover:bg-accent hover:text-accent-foreground pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\"\n >\n <Pin class=\"size-3.5 text-muted-foreground\" />\n <span>{isPinned() ? \"Unpin Tab\" : \"Pin Tab\"}</span>\n </ContextMenuPrimitive.Item>\n <ContextMenuPrimitive.Separator class=\"my-1 h-px bg-border/60\" />\n </Show>\n\n <ContextMenuPrimitive.Item\n data-no-drag\n data-tauri-drag-region=\"false\"\n style=\"-webkit-app-region: no-drag; app-region: no-drag;\"\n onMouseDown={(e) => e.stopPropagation()}\n onPointerDown={(e) => e.stopPropagation()}\n disabled={isPinned() || !closable()}\n onSelect={() => {\n setTimeout(() => {\n if (local.onClose) local.onClose(new MouseEvent(\"click\"));\n else ctx.manager?.closeTab(tabId());\n }, 0);\n }}\n class=\"relative flex cursor-pointer select-none items-center justify-between gap-2 rounded-xs px-2 py-1.5 text-xs outline-none transition-colors hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\"\n >\n <span class=\"flex items-center gap-2\">\n <X class=\"size-3.5 text-muted-foreground\" />\n Close Tab\n </span>\n <span class=\"ml-auto text-[10px] text-muted-foreground font-mono\">Ctrl+W</span>\n </ContextMenuPrimitive.Item>\n\n <Show when={ctx.manager && ctx.manager.tabs().length > 1}>\n <ContextMenuPrimitive.Item\n data-no-drag\n data-tauri-drag-region=\"false\"\n style=\"-webkit-app-region: no-drag; app-region: no-drag;\"\n onMouseDown={(e) => e.stopPropagation()}\n onPointerDown={(e) => e.stopPropagation()}\n onSelect={() => {\n setTimeout(() => ctx.manager?.closeOthers(tabId()), 0);\n }}\n class=\"relative flex cursor-pointer select-none items-center gap-2 rounded-xs px-2 py-1.5 text-xs outline-none transition-colors hover:bg-accent hover:text-accent-foreground pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\"\n >\n <CircleX class=\"size-3.5 text-muted-foreground\" />\n Close Other Tabs\n </ContextMenuPrimitive.Item>\n <ContextMenuPrimitive.Item\n data-no-drag\n data-tauri-drag-region=\"false\"\n style=\"-webkit-app-region: no-drag; app-region: no-drag;\"\n onMouseDown={(e) => e.stopPropagation()}\n onPointerDown={(e) => e.stopPropagation()}\n onSelect={() => {\n setTimeout(() => ctx.manager?.closeAll(), 0);\n }}\n class=\"relative flex cursor-pointer select-none items-center gap-2 rounded-xs px-2 py-1.5 text-xs outline-none transition-colors hover:bg-accent hover:text-accent-foreground pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\"\n >\n <CircleX class=\"size-3.5 text-muted-foreground\" />\n <span>Close All Tabs</span>\n </ContextMenuPrimitive.Item>\n </Show>\n </>\n }\n >\n {local.renderContextMenu!(local.tab || { id: tabId(), title: title() })}\n </Show>\n </ContextMenuPrimitive.Content>\n </ContextMenuPrimitive.Portal>\n </ContextMenuPrimitive.Root>\n </Show>\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\", \"style\"]);\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=\"shrink-0 flex items-center z-20 pointer-events-auto [app-region:no-drag] [-webkit-app-region:no-drag]\"\n >\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n data-no-drag\n data-tauri-drag-region=\"false\"\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 </div>\n );\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\nexport interface 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 destroy?: () => 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 \"onMouseDown\",\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 const destroy = () => windowCtrl.destroy();\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 if (e.detail === 2 && local.doubleClickToMaximize !== false) {\n // Double-click detected on mousedown detail - toggles maximize immediately without initiating a drag\n toggleMaximize();\n } else if (e.detail === 1) {\n // Single-click: initiate window drag\n windowCtrl.startDragging(e);\n }\n }\n }\n if (typeof local.onMouseDown === \"function\") {\n (local.onMouseDown as any)(e);\n }\n };\n\n let lastDblClickTime = 0;\n const handleDoubleClick: JSX.EventHandlerUnion<HTMLElement, MouseEvent> = (e) => {\n const now = Date.now();\n if (now - lastDblClickTime < 350) {\n return; // Deduplicate rapid triggers\n }\n lastDblClickTime = now;\n\n if (local.doubleClickToMaximize !== false) {\n // Don't maximize if user double-clicked an interactive control\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 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 destroy,\n };\n\n return (\n <TitlebarContext.Provider value={contextValue}>\n <header\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 or detected OS. */\n platform?: \"macos\" | \"windows\";\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}\n\nexport const TitlebarControls: Component<TitlebarControlsProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"platform\",\n \"class\",\n \"style\",\n \"isMaximized\",\n \"onMinimize\",\n \"onToggleMaximize\",\n \"onClose\",\n ]);\n const ctx = useContext(TitlebarContext);\n let fallbackCtrl: ReturnType<typeof createTauriWindow> | null = null;\n if (!ctx) {\n fallbackCtrl = createTauriWindow();\n }\n\n const activePlatform = createMemo<\"macos\" | \"windows\">(() => {\n if (local.platform) return local.platform;\n if (ctx?.platform) return ctx.platform();\n const detected = detectPlatform();\n return detected === \"macos\" ? \"macos\" : \"windows\";\n });\n\n const isMaximized = () => {\n if (local.isMaximized !== undefined) return local.isMaximized;\n if (ctx?.isMaximized) return ctx.isMaximized();\n return fallbackCtrl ? fallbackCtrl.isMaximized() : false;\n };\n\n const handleMinimize = (e: MouseEvent) => {\n e.stopPropagation();\n if (local.onMinimize) return local.onMinimize();\n if (ctx?.minimize) return ctx.minimize();\n return fallbackCtrl?.minimize();\n };\n\n const handleToggleMaximize = (e: MouseEvent) => {\n e.stopPropagation();\n if (local.onToggleMaximize) return local.onToggleMaximize();\n if (ctx?.toggleMaximize) return ctx.toggleMaximize();\n return fallbackCtrl?.toggleMaximize();\n };\n\n const handleClose = (e: MouseEvent) => {\n e.stopPropagation();\n if (local.onClose) return local.onClose();\n if (ctx?.close) return ctx.close();\n return fallbackCtrl?.close();\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 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={handleMinimize}\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={handleToggleMaximize}\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={isMaximized() ? \"Restore Window\" : \"Maximize Window\"}\n >\n <Show\n when={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={handleClose}\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={handleClose}\n class=\"flex size-3 items-center justify-center rouded-lg 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={handleMinimize}\n class=\"flex size-3 items-center justify-center rouded-lg 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={handleToggleMaximize}\n class=\"flex size-3 items-center justify-center rouded-lg 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.2\"\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 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\n",
23
+ "type": "registry:ui"
24
+ }
25
+ ]
26
+ }
@@ -14,7 +14,7 @@
14
14
  "files": [
15
15
  {
16
16
  "path": "ui/toggle-group.tsx",
17
- "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 { createControllableSignal } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport const toggleGroupItemVariants = cva(\n \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline:\n \"border border-border bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground\",\n },\n size: {\n default: \"h-9 px-3 min-w-9\",\n sm: \"h-8 px-2 text-xs min-w-8\",\n lg: \"h-10 px-3 min-w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\ninterface ToggleGroupContextValue {\n type: \"single\" | \"multiple\";\n value: Accessor<any>;\n onItemSelect: (itemValue: string) => void;\n variant: Accessor<\"default\" | \"outline\">;\n size: Accessor<\"default\" | \"sm\" | \"lg\">;\n disabled: Accessor<boolean>;\n}\n\nconst ToggleGroupContext = createContext<ToggleGroupContextValue>();\n\nexport interface ToggleGroupProps<T extends \"single\" | \"multiple\" = \"single\">\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onChange\">,\n VariantProps<typeof toggleGroupItemVariants> {\n /** Mode of selection: single choice or multiple choices */\n type?: T;\n /** Controlled value: string for 'single', string[] for 'multiple' */\n value?: T extends \"multiple\" ? string[] : string;\n /** Uncontrolled default value */\n defaultValue?: T extends \"multiple\" ? string[] : string;\n /** Callback fired when selection changes */\n onChange?: (value: T extends \"multiple\" ? string[] : string) => void;\n /** Layout orientation */\n orientation?: \"horizontal\" | \"vertical\";\n /** Disables all buttons in the group */\n disabled?: boolean;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Root container for grouping connected toggle items with shared single/multiple selection state.\n */\nexport const ToggleGroup = <T extends \"single\" | \"multiple\" = \"single\">(\n props: ToggleGroupProps<T>\n) => {\n const [local, rest] = splitProps(props, [\n \"type\",\n \"value\",\n \"defaultValue\",\n \"onChange\",\n \"orientation\",\n \"variant\",\n \"size\",\n \"disabled\",\n \"class\",\n \"children\",\n ]);\n\n const type = () => local.type ?? (\"single\" as T);\n const variant = () => local.variant ?? \"default\";\n const size = () => local.size ?? \"default\";\n const disabled = () => local.disabled ?? false;\n const orientation = () => local.orientation ?? \"horizontal\";\n\n const [currentValue, setCurrentValue] = createControllableSignal<any>({\n value: () => local.value,\n defaultValue: local.defaultValue ?? (type() === \"multiple\" ? [] : undefined),\n onChange: (val) => local.onChange?.(val),\n });\n\n const onItemSelect = (itemValue: string) => {\n if (disabled()) return;\n\n if (type() === \"multiple\") {\n const currentList = Array.isArray(currentValue()) ? currentValue() : [];\n if (currentList.includes(itemValue)) {\n setCurrentValue(currentList.filter((v: string) => v !== itemValue));\n } else {\n setCurrentValue([...currentList, itemValue]);\n }\n } else {\n const current = currentValue();\n if (current === itemValue) {\n setCurrentValue(undefined);\n } else {\n setCurrentValue(itemValue);\n }\n }\n };\n\n const contextValue: ToggleGroupContextValue = {\n type: type(),\n value: currentValue,\n onItemSelect,\n variant,\n size,\n disabled,\n };\n\n return (\n <ToggleGroupContext.Provider value={contextValue}>\n <div\n role=\"group\"\n data-orientation={orientation()}\n class={cn(\n \"flex items-center justify-center gap-1 rounded-lg\",\n orientation() === \"vertical\" ? \"flex-col\" : \"flex-row\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </ToggleGroupContext.Provider>\n );\n};\n\nexport interface ToggleGroupItemProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof toggleGroupItemVariants> {\n /** Unique value representing this toggle item within the group */\n value: string;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Individual toggle button item within a ToggleGroup.\n */\nexport const ToggleGroupItem: Component<ToggleGroupItemProps> = (props) => {\n const context = useContext(ToggleGroupContext);\n\n if (!context) {\n throw new Error(\"ToggleGroupItem must be used within a ToggleGroup\");\n }\n\n const [local, rest] = splitProps(props, [\n \"value\",\n \"variant\",\n \"size\",\n \"disabled\",\n \"class\",\n \"children\",\n ]);\n\n const isSelected = () => {\n const groupValue = context.value();\n if (context.type === \"multiple\") {\n return Array.isArray(groupValue) && groupValue.includes(local.value);\n }\n return groupValue === local.value;\n };\n\n const isDisabled = () => local.disabled || context.disabled();\n const itemVariant = () => local.variant || context.variant();\n const itemSize = () => local.size || context.size();\n\n return (\n <button\n type=\"button\"\n role={context.type === \"single\" ? \"radio\" : \"checkbox\"}\n aria-checked={isSelected()}\n data-state={isSelected() ? \"on\" : \"off\"}\n disabled={isDisabled()}\n onClick={() => context.onItemSelect(local.value)}\n class={cn(\n toggleGroupItemVariants({\n variant: itemVariant(),\n size: itemSize(),\n }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n",
17
+ "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 { createControllableSignal } from \"@/hooks/create-controllable-signal\";\nimport { cn } from \"@/lib/cn\";\n\nexport const toggleGroupItemVariants = cva(\n \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline:\n \"border border-border bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground\",\n },\n size: {\n default: \"h-9 px-3 min-w-9\",\n sm: \"h-8 px-2 text-xs min-w-8\",\n lg: \"h-10 px-3 min-w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\ninterface ToggleGroupContextValue {\n type: \"single\" | \"multiple\";\n value: Accessor<any>;\n onItemSelect: (itemValue: string) => void;\n variant: Accessor<\"default\" | \"outline\">;\n size: Accessor<\"default\" | \"sm\" | \"lg\">;\n disabled: Accessor<boolean>;\n}\n\nconst ToggleGroupContext = createContext<ToggleGroupContextValue>();\n\nexport interface ToggleGroupProps<T extends \"single\" | \"multiple\" = \"single\">\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onChange\">,\n VariantProps<typeof toggleGroupItemVariants> {\n /** Mode of selection: single choice or multiple choices */\n type?: T;\n /** Controlled value: string for 'single', string[] for 'multiple' */\n value?: T extends \"multiple\" ? string[] : string;\n /** Uncontrolled default value */\n defaultValue?: T extends \"multiple\" ? string[] : string;\n /** Callback fired when selection changes */\n onChange?: (value: T extends \"multiple\" ? string[] : string) => void;\n /** Layout orientation */\n orientation?: \"horizontal\" | \"vertical\";\n /** Disables all buttons in the group */\n disabled?: boolean;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Root container for grouping connected toggle items with shared single/multiple selection state.\n */\nexport const ToggleGroup = <T extends \"single\" | \"multiple\" = \"single\">(\n props: ToggleGroupProps<T>\n) => {\n const [local, rest] = splitProps(props, [\n \"type\",\n \"value\",\n \"defaultValue\",\n \"onChange\",\n \"orientation\",\n \"variant\",\n \"size\",\n \"disabled\",\n \"class\",\n \"children\",\n ]);\n\n const type = () => local.type ?? (\"single\" as T);\n const variant = () => local.variant ?? \"default\";\n const size = () => local.size ?? \"default\";\n const disabled = () => local.disabled ?? false;\n const orientation = () => local.orientation ?? \"horizontal\";\n\n const [currentValue, setCurrentValue] = createControllableSignal<any>({\n value: () => local.value,\n defaultValue: local.defaultValue ?? (type() === \"multiple\" ? [] : undefined),\n onChange: (val) => local.onChange?.(val),\n });\n\n const onItemSelect = (itemValue: string) => {\n if (disabled()) return;\n\n if (type() === \"multiple\") {\n const currentList = Array.isArray(currentValue()) ? currentValue() : [];\n if (currentList.includes(itemValue)) {\n setCurrentValue(currentList.filter((v: string) => v !== itemValue));\n } else {\n setCurrentValue([...currentList, itemValue]);\n }\n } else {\n const current = currentValue();\n if (current === itemValue) {\n setCurrentValue(undefined);\n } else {\n setCurrentValue(itemValue);\n }\n }\n };\n\n const contextValue: ToggleGroupContextValue = {\n type: type(),\n value: currentValue,\n onItemSelect,\n variant,\n size,\n disabled,\n };\n\n return (\n <ToggleGroupContext.Provider value={contextValue}>\n <div\n role=\"group\"\n data-orientation={orientation()}\n class={cn(\n \"flex items-center justify-center gap-1 rounded-lg\",\n orientation() === \"vertical\" ? \"flex-col\" : \"flex-row\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </ToggleGroupContext.Provider>\n );\n};\n\nexport interface ToggleGroupItemProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof toggleGroupItemVariants> {\n /** Unique value representing this toggle item within the group */\n value: string;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Individual toggle button item within a ToggleGroup.\n */\nexport const ToggleGroupItem: Component<ToggleGroupItemProps> = (props) => {\n const context = useContext(ToggleGroupContext);\n\n if (!context) {\n throw new Error(\"ToggleGroupItem must be used within a ToggleGroup\");\n }\n\n const [local, rest] = splitProps(props, [\n \"value\",\n \"variant\",\n \"size\",\n \"disabled\",\n \"class\",\n \"children\",\n ]);\n\n const isSelected = () => {\n const groupValue = context.value();\n if (context.type === \"multiple\") {\n return Array.isArray(groupValue) && groupValue.includes(local.value);\n }\n return groupValue === local.value;\n };\n\n const isDisabled = () => local.disabled || context.disabled();\n const itemVariant = () => local.variant || context.variant();\n const itemSize = () => local.size || context.size();\n\n return (\n <button\n type=\"button\"\n role={context.type === \"single\" ? \"radio\" : \"checkbox\"}\n aria-checked={isSelected()}\n data-state={isSelected() ? \"on\" : \"off\"}\n disabled={isDisabled()}\n onClick={() => context.onItemSelect(local.value)}\n class={cn(\n toggleGroupItemVariants({\n variant: itemVariant(),\n size: itemSize(),\n }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n",
18
18
  "type": "registry:ui"
19
19
  }
20
20
  ]
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "updater-modal",
3
+ "title": "Updater Modal",
4
+ "description": "An automated application auto-updater dialog for Tauri v2 with release notes preview, download progress bar, and relaunch actions.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "lucide-solid"
10
+ ],
11
+ "registryDependencies": [
12
+ "dialog",
13
+ "button",
14
+ "badge",
15
+ "progress",
16
+ "alert",
17
+ "create-app-updater"
18
+ ],
19
+ "files": [
20
+ {
21
+ "path": "ui/updater-modal.tsx",
22
+ "content": "import {\n Show,\n splitProps,\n type Component,\n type ParentComponent,\n type JSX,\n} from \"solid-js\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { Alert, AlertTitle, AlertDescription } from \"@/components/ui/alert\";\nimport type {\n CreateAppUpdaterReturn,\n UpdateManifestInfo,\n UpdateProgressInfo,\n} from \"@/hooks/create-app-updater\";\nimport {\n Download,\n RotateCcw,\n CheckCircle2,\n AlertTriangle,\n ArrowRight,\n Package,\n} from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nfunction formatBytes(bytes: number): string {\n if (!bytes || bytes === 0) return \"0 MB\";\n const mb = bytes / (1024 * 1024);\n return `${mb.toFixed(1)} MB`;\n}\n\n/* --- 1. Sub-components for Clean Modularity & Zero Repetition --- */\n\nexport interface UpdaterHeaderProps {\n appName: string;\n status: string;\n info?: UpdateManifestInfo | null;\n}\n\nexport const UpdaterHeader: Component<UpdaterHeaderProps> = (props) => (\n <DialogHeader class=\"space-y-1.5 text-left\">\n <div class=\"flex items-center justify-between\">\n <div class=\"size-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center\">\n <Package class=\"size-4\" />\n </div>\n <Show when={props.info?.version}>\n <div class=\"flex items-center gap-1.5 text-xs font-mono\">\n <span class=\"text-muted-foreground\">{props.info?.currentVersion || \"v1.0.0\"}</span>\n <ArrowRight class=\"size-3 text-muted-foreground\" />\n <Badge variant=\"default\" class=\"text-xs px-1.5 py-0 bg-primary text-primary-foreground font-semibold\">\n {props.info?.version}\n </Badge>\n </div>\n </Show>\n </div>\n\n <DialogTitle class=\"text-base font-bold tracking-tight text-foreground\">\n {props.status === \"downloaded\"\n ? \"Update Ready to Install\"\n : props.status === \"downloading\"\n ? \"Downloading Update...\"\n : props.status === \"up-to-date\"\n ? \"Up to Date\"\n : `New Update Available for ${props.appName}`}\n </DialogTitle>\n\n <DialogDescription class=\"text-xs text-muted-foreground leading-relaxed\">\n {props.status === \"downloaded\"\n ? \"The update has been downloaded and verified. Restart to apply changes.\"\n : props.status === \"downloading\"\n ? \"Please wait while update packages are being downloaded.\"\n : props.status === \"up-to-date\"\n ? `${props.appName} is running on the latest version.`\n : \"A new version with features and bug fixes is ready to install.\"}\n </DialogDescription>\n </DialogHeader>\n);\n\nexport interface UpdaterReleaseNotesProps {\n body?: string;\n date?: string;\n}\n\nexport const UpdaterReleaseNotes: Component<UpdaterReleaseNotesProps> = (props) => (\n <div class=\"space-y-1\">\n <div class=\"flex items-center justify-between text-xs text-muted-foreground font-medium\">\n <span>Release Notes</span>\n <Show when={props.date}>\n <span class=\"text-[11px] font-mono\">{props.date}</span>\n </Show>\n </div>\n <pre class=\"max-h-28 overflow-y-auto rounded-md border border-border bg-black/5 dark:bg-black/50 p-2.5 text-xs text-foreground font-mono whitespace-pre-wrap leading-relaxed select-text shadow-inner\">\n {props.body}\n </pre>\n </div>\n);\n\nexport interface UpdaterProgressBarProps {\n progress: UpdateProgressInfo;\n}\n\nexport const UpdaterProgressBar: Component<UpdaterProgressBarProps> = (props) => (\n <div class=\"space-y-1.5 py-1\">\n <div class=\"flex items-center justify-between text-xs font-mono\">\n <span class=\"text-muted-foreground font-sans\">Progress</span>\n <span class=\"font-semibold text-primary\">{props.progress.percentage}%</span>\n </div>\n <Progress value={props.progress.percentage} class=\"h-1.5\" />\n <div class=\"flex items-center justify-between text-[11px] font-mono text-muted-foreground\">\n <span>{formatBytes(props.progress.downloaded)}</span>\n <span>{formatBytes(props.progress.total)}</span>\n </div>\n </div>\n);\n\n/* --- 2. Main UpdaterModal Compound Component --- */\n\nexport interface UpdaterModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n updater: CreateAppUpdaterReturn;\n appName?: string;\n class?: string;\n}\n\nexport const UpdaterModal: Component<UpdaterModalProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"open\",\n \"onOpenChange\",\n \"updater\",\n \"appName\",\n \"class\",\n ]);\n\n const appName = () => local.appName || \"Nikala Desktop\";\n const status = () => local.updater.status();\n const info = () => local.updater.updateInfo();\n const progress = () => local.updater.progress();\n const error = () => local.updater.error();\n\n return (\n <Dialog open={local.open} onOpenChange={local.onOpenChange}>\n <DialogContent class={cn(\"sm:max-w-sm\", local.class)}>\n {/* Header */}\n <UpdaterHeader\n appName={appName()}\n status={status()}\n info={info()}\n />\n\n {/* Error Alert */}\n <Show when={error()}>\n <Alert variant=\"destructive\" class=\"py-2 text-xs\">\n <AlertTriangle class=\"size-3.5\" />\n <AlertTitle class=\"font-semibold text-xs\">Update Failed</AlertTitle>\n <AlertDescription class=\"text-[11px]\">{error()}</AlertDescription>\n </Alert>\n </Show>\n\n {/* Release Notes */}\n <Show when={info()?.body && (status() === \"available\" || status() === \"downloading\")}>\n <UpdaterReleaseNotes body={info()?.body} date={info()?.date} />\n </Show>\n\n {/* Progress Bar */}\n <Show when={status() === \"downloading\"}>\n <UpdaterProgressBar progress={progress()} />\n </Show>\n\n {/* Success Callout */}\n <Show when={status() === \"downloaded\"}>\n <div class=\"flex items-center gap-2.5 p-2.5 rounded-md border border-primary/30 bg-primary/5 text-xs text-foreground\">\n <CheckCircle2 class=\"size-4 text-primary shrink-0\" />\n <p class=\"font-medium text-foreground text-xs\">Package downloaded & verified.</p>\n </div>\n </Show>\n\n {/* Actions Footer */}\n <DialogFooter class=\"flex-row items-center justify-end gap-1.5 pt-1\">\n <Show when={status() === \"available\" || status() === \"error\"}>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => local.onOpenChange(false)}\n class=\"text-xs h-7.5 px-3 cursor-pointer\"\n >\n Later\n </Button>\n <Button\n variant=\"default\"\n size=\"sm\"\n onClick={() => local.updater.downloadAndInstall()}\n class=\"text-xs h-7.5 px-3 gap-1.5 cursor-pointer font-medium\"\n >\n <Download class=\"size-3.5\" />\n <span>Update Now</span>\n </Button>\n </Show>\n\n <Show when={status() === \"downloading\"}>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled\n class=\"text-xs h-7.5 w-full opacity-70 justify-center\"\n >\n Downloading ({progress().percentage}%)\n </Button>\n </Show>\n\n <Show when={status() === \"downloaded\"}>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => local.onOpenChange(false)}\n class=\"text-xs h-7.5 px-3 cursor-pointer\"\n >\n Later\n </Button>\n <Button\n variant=\"default\"\n size=\"sm\"\n onClick={() => local.updater.relaunch()}\n class=\"text-xs h-7.5 px-3 gap-1.5 cursor-pointer font-semibold shadow-xs\"\n >\n <RotateCcw class=\"size-3.5\" />\n <span>Relaunch</span>\n </Button>\n </Show>\n\n <Show when={status() === \"up-to-date\" || status() === \"idle\"}>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => local.onOpenChange(false)}\n class=\"text-xs h-7.5 px-3 cursor-pointer\"\n >\n Close\n </Button>\n </Show>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n};\n",
23
+ "type": "registry:ui"
24
+ }
25
+ ]
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ // packages/core/src/index.ts
2
+ export * from "./registry/index.js";
3
+ export * from "./lib/cn.js";
4
+
5
+ // Providers & Theme
6
+ export * from "./registry/providers/theme-provider.jsx";
7
+ export * from "./registry/providers/theme-transitions.js";
8
+ export * from "./registry/providers/theme-script.jsx";
9
+
10
+ // UI Primitives & Components
11
+ export * from "./registry/components/ui/accordion.jsx";
12
+ export * from "./registry/components/ui/alert.jsx";
13
+ export * from "./registry/components/ui/api-table.jsx";
14
+ export * from "./registry/components/ui/aspect-ratio.jsx";
15
+ export * from "./registry/components/ui/avatar.jsx";
16
+ export * from "./registry/components/ui/badge.jsx";
17
+ export * from "./registry/components/ui/banner.jsx";
18
+ export * from "./registry/components/ui/breadcrumb.jsx";
19
+ export * from "./registry/components/ui/bubble.jsx";
20
+ export * from "./registry/components/ui/button.jsx";
21
+ export * from "./registry/components/ui/callout.jsx";
22
+ export * from "./registry/components/ui/card.jsx";
23
+ export * from "./registry/components/ui/code-block.jsx";
24
+ export * from "./registry/components/ui/code-group.jsx";
25
+ export * from "./registry/components/ui/component-viewer.jsx";
26
+ export * from "./registry/components/ui/collapsible.jsx";
27
+ export * from "./registry/components/ui/command.jsx";
28
+ export * from "./registry/components/ui/container.jsx";
29
+ export * from "./registry/components/ui/dialog.jsx";
30
+ export * from "./registry/components/ui/file-tree.jsx";
31
+ export * from "./registry/components/ui/kbd.jsx";
32
+ export * from "./registry/components/ui/list.jsx";
33
+ export * from "./registry/components/ui/navbar.jsx";
34
+ export * from "./registry/components/ui/pager.jsx";
35
+ export * from "./registry/components/ui/pagination.jsx";
36
+ export * from "./registry/components/ui/package-manager-tabs.jsx";
37
+ export * from "./registry/components/ui/scroll-area.jsx";
38
+ export * from "./registry/components/ui/section-heading.jsx";
39
+ export * from "./registry/components/ui/separator.jsx";
40
+ export * from "./registry/components/ui/sheet.jsx";
41
+ export * from "./registry/components/ui/sidebar.jsx";
42
+ export * from "./registry/components/ui/spinner.jsx";
43
+ export * from "./registry/components/ui/steps.jsx";
44
+ export * from "./registry/components/ui/table-of-contents.jsx";
45
+ export * from "./registry/components/ui/table.jsx";
46
+ export * from "./registry/components/ui/tabs.jsx";
47
+ export * from "./registry/components/ui/theme-toggle.jsx";
48
+ export * from "./registry/components/ui/tooltip.jsx";
@@ -0,0 +1,117 @@
1
+ import {
2
+ splitProps,
3
+ For,
4
+ Show,
5
+ type JSX,
6
+ type Component,
7
+ } from "solid-js";
8
+ import { SectionHeading } from "@/components/ui/section-heading";
9
+ import {
10
+ Table,
11
+ TableHeader,
12
+ TableBody,
13
+ TableRow,
14
+ TableHead,
15
+ TableCell,
16
+ } from "@/components/ui/table";
17
+ import { cn } from "@/lib/cn";
18
+
19
+ export interface ApiTableItem {
20
+ /** Property, attribute, or method name */
21
+ prop: string;
22
+ /** TypeScript type definition string */
23
+ type: string;
24
+ /** Default value if optional */
25
+ default?: string;
26
+ /** Detailed description of purpose and usage */
27
+ description: string;
28
+ /** Whether the property is strictly required */
29
+ required?: boolean;
30
+ }
31
+
32
+ export interface ApiTableProps extends JSX.HTMLAttributes<HTMLDivElement> {
33
+ /** Title header for this API section (e.g. component or interface name) */
34
+ title?: string;
35
+ /** Subtitle or explanatory note */
36
+ description?: string;
37
+ /** Badge label next to the title (defaults to "Props") */
38
+ badge?: string;
39
+ /** List of API properties to render in the table */
40
+ items: ApiTableItem[];
41
+ class?: string;
42
+ }
43
+
44
+ /**
45
+ * Clean, structured API reference table component composed from Table and Badge primitives.
46
+ */
47
+ export const ApiTable: Component<ApiTableProps> = (props) => {
48
+ const [local, rest] = splitProps(props, [
49
+ "title",
50
+ "description",
51
+ "badge",
52
+ "items",
53
+ "class",
54
+ ]);
55
+
56
+ return (
57
+ <div class={cn("my-6 space-y-3", local.class)} {...rest}>
58
+ {/* Header */}
59
+ <Show when={local.title}>
60
+ <SectionHeading
61
+ variant="compact"
62
+ title={local.title!}
63
+ badge={local.badge || "Props"}
64
+ description={local.description}
65
+ />
66
+ </Show>
67
+
68
+ {/* Responsive Table Container Composed from Nikala Table Primitives */}
69
+ <div class="rounded-lg border border-border bg-card/50 shadow-2xs overflow-hidden">
70
+ <Table class="text-xs">
71
+ <TableHeader class="bg-muted/40 font-mono text-[11px] uppercase tracking-wider text-muted-foreground select-none">
72
+ <TableRow class="hover:bg-transparent">
73
+ <TableHead class="h-9 px-4 font-semibold">Prop</TableHead>
74
+ <TableHead class="h-9 px-4 font-semibold">Type</TableHead>
75
+ <TableHead class="h-9 px-4 font-semibold">Default</TableHead>
76
+ <TableHead class="h-9 px-4 font-semibold">Description</TableHead>
77
+ </TableRow>
78
+ </TableHeader>
79
+ <TableBody class="font-mono text-xs">
80
+ <For each={local.items}>
81
+ {(item) => (
82
+ <TableRow class="transition-colors hover:bg-muted/20">
83
+ <TableCell class="px-4 py-3 font-semibold text-primary">
84
+ <span class="inline-flex items-center gap-1">
85
+ {item.prop}
86
+ <Show when={item.required}>
87
+ <span class="text-rose-500 font-bold" title="Required">*</span>
88
+ </Show>
89
+ </span>
90
+ </TableCell>
91
+ <TableCell class="px-4 py-3 text-muted-foreground">
92
+ <code class="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[11px] text-foreground">
93
+ {item.type}
94
+ </code>
95
+ </TableCell>
96
+ <TableCell class="px-4 py-3 text-muted-foreground">
97
+ <Show
98
+ when={item.default}
99
+ fallback={<span class="text-muted-foreground/40 font-mono">-</span>}
100
+ >
101
+ <code class="rounded-md bg-muted/60 px-1 py-0.5 font-mono text-[11px]">
102
+ {item.default}
103
+ </code>
104
+ </Show>
105
+ </TableCell>
106
+ <TableCell class="px-4 py-3 font-sans font-normal leading-relaxed text-muted-foreground">
107
+ {item.description}
108
+ </TableCell>
109
+ </TableRow>
110
+ )}
111
+ </For>
112
+ </TableBody>
113
+ </Table>
114
+ </div>
115
+ </div>
116
+ );
117
+ };
@@ -28,8 +28,6 @@ export const bannerVariants = cva(
28
28
  "bg-emerald-500/15 text-emerald-900 border-b border-emerald-500/20 dark:text-emerald-200",
29
29
  destructive:
30
30
  "bg-rose-500/15 text-rose-900 border-b border-rose-500/20 dark:text-rose-200",
31
- pirosmani:
32
- "bg-[#722f37] text-white border-b border-[#8a3943] shadow-sm",
33
31
  },
34
32
  sticky: {
35
33
  true: "sticky top-0 z-50 backdrop-blur-sm",
@@ -1,7 +1,7 @@
1
1
  import { Show, splitProps, type Component, type JSX } from "solid-js";
2
2
  import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { cn } from "@/lib/cn";
4
- import { Spinner } from "./spinner";
4
+ import { Spinner } from "@/components/ui/spinner";
5
5
 
6
6
  /**
7
7
  * Class variance authority configuration for button styling variants and sizes.
@@ -0,0 +1,137 @@
1
+ import {
2
+ splitProps,
3
+ Show,
4
+ type Component,
5
+ type JSX,
6
+ type ParentComponent,
7
+ } from "solid-js";
8
+ import { Dynamic } from "solid-js/web";
9
+ import { cva, type VariantProps } from "class-variance-authority";
10
+ import {
11
+ Info,
12
+ Lightbulb,
13
+ AlertTriangle,
14
+ AlertCircle,
15
+ CheckCircle2,
16
+ Bookmark,
17
+ } from "lucide-solid";
18
+ import { cn } from "@/lib/cn";
19
+
20
+ export const calloutVariants = cva(
21
+ "relative w-full rounded-lg border p-4 text-sm leading-relaxed transition-colors",
22
+ {
23
+ variants: {
24
+ variant: {
25
+ note: "border-border/80 bg-muted/50 text-foreground [&>svg]:text-foreground",
26
+ info: "border-sky-500/30 bg-sky-500/10 text-sky-950 dark:text-sky-100 [&>svg]:text-sky-600 dark:[&>svg]:text-sky-400",
27
+ tip: "border-emerald-500/30 bg-emerald-500/10 text-emerald-950 dark:text-emerald-100 [&>svg]:text-emerald-600 dark:[&>svg]:text-emerald-400",
28
+ warning: "border-amber-500/30 bg-amber-500/10 text-amber-950 dark:text-amber-100 [&>svg]:text-amber-600 dark:[&>svg]:text-amber-400",
29
+ danger: "border-destructive/30 bg-destructive/10 text-destructive dark:text-red-200 [&>svg]:text-destructive",
30
+ success: "border-emerald-500/30 bg-emerald-500/10 text-emerald-950 dark:text-emerald-100 [&>svg]:text-emerald-600 dark:[&>svg]:text-emerald-400",
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: "note",
35
+ },
36
+ }
37
+ );
38
+
39
+ const defaultIcons = {
40
+ note: Bookmark,
41
+ info: Info,
42
+ tip: Lightbulb,
43
+ warning: AlertTriangle,
44
+ danger: AlertCircle,
45
+ success: CheckCircle2,
46
+ };
47
+
48
+ export interface CalloutProps
49
+ extends JSX.HTMLAttributes<HTMLDivElement>,
50
+ VariantProps<typeof calloutVariants> {
51
+ title?: string;
52
+ type?: VariantProps<typeof calloutVariants>["variant"];
53
+ icon?: Component<{ class?: string }> | false;
54
+ class?: string;
55
+ }
56
+
57
+ export const Callout: ParentComponent<CalloutProps> = (props) => {
58
+ const [local, rest] = splitProps(props, [
59
+ "variant",
60
+ "type",
61
+ "title",
62
+ "icon",
63
+ "class",
64
+ "children",
65
+ ]);
66
+
67
+ const variant = () => local.variant || local.type || "note";
68
+
69
+ const IconComponent = () => {
70
+ if (local.icon === false) return null;
71
+ if (local.icon) return local.icon;
72
+ return defaultIcons[variant()] || Info;
73
+ };
74
+
75
+ return (
76
+ <div
77
+ role="region"
78
+ aria-label={local.title || `${variant()} callout`}
79
+ class={cn(
80
+ calloutVariants({ variant: variant() }),
81
+ "flex gap-3.5 items-start",
82
+ local.class
83
+ )}
84
+ {...rest}
85
+ >
86
+ <Show when={IconComponent()}>
87
+ {(Icon) => (
88
+ <span class="inline-flex shrink-0 items-center justify-center mt-0.5 select-none">
89
+ <Dynamic component={Icon()} class="size-4.5 shrink-0" />
90
+ </span>
91
+ )}
92
+ </Show>
93
+ <div class="flex-1 space-y-1 min-w-0">
94
+ <Show when={local.title}>
95
+ <h5 class="font-semibold text-sm leading-none tracking-tight">
96
+ {local.title}
97
+ </h5>
98
+ </Show>
99
+ <div class="text-sm opacity-90 leading-relaxed break-words">
100
+ {local.children}
101
+ </div>
102
+ </div>
103
+ </div>
104
+ );
105
+ };
106
+
107
+ export interface CalloutTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {
108
+ class?: string;
109
+ }
110
+
111
+ export const CalloutTitle: ParentComponent<CalloutTitleProps> = (props) => {
112
+ const [local, rest] = splitProps(props, ["class", "children"]);
113
+ return (
114
+ <h5
115
+ class={cn("font-semibold text-sm leading-none tracking-tight mb-1", local.class)}
116
+ {...rest}
117
+ >
118
+ {local.children}
119
+ </h5>
120
+ );
121
+ };
122
+
123
+ export interface CalloutDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {
124
+ class?: string;
125
+ }
126
+
127
+ export const CalloutDescription: ParentComponent<CalloutDescriptionProps> = (props) => {
128
+ const [local, rest] = splitProps(props, ["class", "children"]);
129
+ return (
130
+ <div
131
+ class={cn("text-sm opacity-90 leading-relaxed", local.class)}
132
+ {...rest}
133
+ >
134
+ {local.children}
135
+ </div>
136
+ );
137
+ };
@@ -1,5 +1,5 @@
1
1
  import { splitProps, Show, type Component, type JSX } from "solid-js";
2
- import { createControllableSignal } from "@nikala-ui/hooks";
2
+ import { createControllableSignal } from "@/hooks/create-controllable-signal";
3
3
  import { cn } from "@/lib/cn";
4
4
 
5
5
  export interface CheckboxProps