@nikala-ui/core 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +43 -2
- package/registry/api-table.json +21 -0
- package/registry/banner.json +1 -1
- package/registry/button.json +1 -1
- package/registry/callout.json +19 -0
- package/registry/checkbox.json +4 -1
- package/registry/code-block.json +26 -0
- package/registry/code-group.json +20 -0
- package/registry/combobox.json +1 -1
- package/registry/command.json +3 -2
- package/registry/component-viewer.json +25 -0
- package/registry/container.json +18 -0
- package/registry/context-menu.json +1 -1
- package/registry/create-app-updater.json +13 -0
- package/registry/create-document-tabs.json +13 -0
- package/registry/create-global-shortcut.json +13 -0
- package/registry/create-tauri-window.json +13 -0
- package/registry/create-tiptap-editor.json +34 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +3 -2
- package/registry/field.json +1 -1
- package/registry/file-tree.json +21 -0
- package/registry/footer.json +1 -1
- package/registry/form-message.json +3 -2
- package/registry/icon-button.json +1 -1
- package/registry/index.json +336 -5
- package/registry/navbar.json +1 -1
- package/registry/navigation-menu.json +1 -1
- package/registry/number-input.json +1 -1
- package/registry/package-manager-tabs.json +24 -0
- package/registry/pager.json +21 -0
- package/registry/popover.json +4 -1
- package/registry/resizable.json +1 -1
- package/registry/rich-text-editor.json +95 -0
- package/registry/scroll-area.json +1 -1
- package/registry/section-heading.json +21 -0
- package/registry/select.json +3 -2
- package/registry/sheet.json +1 -1
- package/registry/sidebar.json +1 -1
- package/registry/status.json +1 -1
- package/registry/steps.json +20 -0
- package/registry/switch.json +4 -1
- package/registry/table-of-contents.json +23 -0
- package/registry/tabs.json +4 -1
- package/registry/theme-manager.json +6 -5
- package/registry/titlebar-tabs.json +24 -0
- package/registry/titlebar.json +26 -0
- package/registry/toggle-group.json +1 -1
- package/registry/updater-modal.json +26 -0
- package/src/index.ts +48 -0
- package/src/registry/components/ui/api-table.tsx +117 -0
- package/src/registry/components/ui/banner.tsx +0 -2
- package/src/registry/components/ui/button.tsx +1 -1
- package/src/registry/components/ui/callout.tsx +137 -0
- package/src/registry/components/ui/checkbox.tsx +1 -1
- package/src/registry/components/ui/code-block.tsx +326 -0
- package/src/registry/components/ui/code-group.tsx +105 -0
- package/src/registry/components/ui/combobox.tsx +83 -18
- package/src/registry/components/ui/command.tsx +110 -106
- package/src/registry/components/ui/component-viewer.tsx +363 -0
- package/src/registry/components/ui/container.tsx +45 -0
- package/src/registry/components/ui/context-menu.tsx +67 -22
- package/src/registry/components/ui/dialog.tsx +42 -32
- package/src/registry/components/ui/dropdown-menu.tsx +69 -31
- package/src/registry/components/ui/field.tsx +7 -3
- package/src/registry/components/ui/file-tree.tsx +181 -0
- package/src/registry/components/ui/footer.tsx +1 -1
- package/src/registry/components/ui/form-message.tsx +2 -2
- package/src/registry/components/ui/icon-button.tsx +1 -1
- package/src/registry/components/ui/navbar.tsx +18 -11
- package/src/registry/components/ui/navigation-menu.tsx +2 -2
- package/src/registry/components/ui/number-input.tsx +3 -3
- package/src/registry/components/ui/package-manager-tabs.tsx +226 -0
- package/src/registry/components/ui/pager.tsx +102 -0
- package/src/registry/components/ui/popover.tsx +1 -1
- package/src/registry/components/ui/resizable.tsx +3 -1
- package/src/registry/components/ui/rich-text-editor/bubble-menu.tsx +236 -0
- package/src/registry/components/ui/rich-text-editor/editor.tsx +308 -0
- package/src/registry/components/ui/rich-text-editor/extensions.ts +116 -0
- package/src/registry/components/ui/rich-text-editor/footer.tsx +36 -0
- package/src/registry/components/ui/rich-text-editor/image-dialog.tsx +75 -0
- package/src/registry/components/ui/rich-text-editor/index.ts +11 -0
- package/src/registry/components/ui/rich-text-editor/link-dialog.tsx +48 -0
- package/src/registry/components/ui/rich-text-editor/markdown.ts +170 -0
- package/src/registry/components/ui/rich-text-editor/slash-command.tsx +301 -0
- package/src/registry/components/ui/rich-text-editor/table-controls.tsx +68 -0
- package/src/registry/components/ui/rich-text-editor/toolbar.tsx +465 -0
- package/src/registry/components/ui/rich-text-editor/use-editor.ts +231 -0
- package/src/registry/components/ui/scroll-area.tsx +12 -2
- package/src/registry/components/ui/section-heading.tsx +108 -0
- package/src/registry/components/ui/select.tsx +16 -19
- package/src/registry/components/ui/sheet.tsx +58 -53
- package/src/registry/components/ui/sidebar.tsx +4 -4
- package/src/registry/components/ui/status.tsx +7 -5
- package/src/registry/components/ui/steps.tsx +198 -0
- package/src/registry/components/ui/switch.tsx +1 -1
- package/src/registry/components/ui/table-of-contents.tsx +131 -0
- package/src/registry/components/ui/tabs.tsx +1 -1
- package/src/registry/components/ui/theme-toggle.tsx +175 -139
- package/src/registry/components/ui/titlebar-tabs.tsx +773 -0
- package/src/registry/components/ui/titlebar.tsx +468 -0
- package/src/registry/components/ui/toggle-group.tsx +1 -1
- package/src/registry/components/ui/updater-modal.tsx +254 -0
- package/src/registry/metadata.ts +167 -6
- package/src/registry/providers/theme-provider.tsx +16 -8
- package/src/registry/providers/theme-script.tsx +29 -9
- package/src/registry/providers/theme-transitions.ts +81 -50
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-app-updater",
|
|
3
|
+
"title": "createAppUpdater",
|
|
4
|
+
"description": "SolidJS reactive primitive for controlling and observing Tauri v2 application updates",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "hooks/create-app-updater.ts",
|
|
9
|
+
"content": "import { createSignal, onMount, type Accessor } from \"solid-js\";\nimport { isTauriEnvironment } from \"./create-tauri-window\";\n\nexport type UpdaterStatus =\n | \"idle\"\n | \"checking\"\n | \"available\"\n | \"up-to-date\"\n | \"downloading\"\n | \"downloaded\"\n | \"error\";\n\nexport interface UpdateManifestInfo {\n version: string;\n currentVersion?: string;\n date?: string;\n body?: string;\n}\n\nexport interface UpdateProgressInfo {\n downloaded: number;\n total: number;\n percentage: number;\n}\n\nexport interface CreateAppUpdaterOptions {\n /** Automatically check for updates on mount. Defaults to false. */\n autoCheck?: boolean;\n /** Current application version fallback if not detected. */\n currentVersion?: string;\n /** Callback fired when an update is available. */\n onUpdateAvailable?: (info: UpdateManifestInfo) => void;\n /** Callback fired when download finishes. */\n onDownloadFinished?: () => void;\n /** Callback fired on update check or download error. */\n onError?: (error: Error | string) => void;\n}\n\nexport interface CreateAppUpdaterReturn {\n /** Current lifecycle status of the updater. */\n status: Accessor<UpdaterStatus>;\n /** Update metadata (new version, release notes, release date). */\n updateInfo: Accessor<UpdateManifestInfo | null>;\n /** Download progress (bytes downloaded, total bytes, percentage). */\n progress: Accessor<UpdateProgressInfo>;\n /** Error message if an operation failed. */\n error: Accessor<string | null>;\n /** Whether an update is currently being checked or downloaded. */\n isLoading: Accessor<boolean>;\n /** Check update endpoint for a new release. */\n checkForUpdates: () => Promise<boolean>;\n /** Download and install the available update. */\n downloadAndInstall: () => Promise<void>;\n /** Restart application to apply the downloaded update. */\n relaunch: () => Promise<void>;\n /** Dismiss current update prompt or clear error state. */\n dismiss: () => void;\n /** Simulate an update cycle in web / playground preview environments. */\n simulateUpdate: (mockInfo?: Partial<UpdateManifestInfo>) => void;\n}\n\n/**\n * SolidJS reactive primitive for controlling and observing Tauri v2 application updates.\n * Integrates with @tauri-apps/plugin-updater with full web simulation support.\n */\nexport function createAppUpdater(\n options: CreateAppUpdaterOptions = {}\n): CreateAppUpdaterReturn {\n const [status, setStatus] = createSignal<UpdaterStatus>(\"idle\");\n const [updateInfo, setUpdateInfo] = createSignal<UpdateManifestInfo | null>(null);\n const [progress, setProgress] = createSignal<UpdateProgressInfo>({\n downloaded: 0,\n total: 0,\n percentage: 0,\n });\n const [error, setError] = createSignal<string | null>(null);\n\n let activeTauriUpdate: any = null;\n let simulatedTimer: any = null;\n\n const getTauriUpdaterPlugin = async () => {\n if (typeof window === \"undefined\") return null;\n try {\n if ((window as any).__TAURI__?.updater) {\n return (window as any).__TAURI__.updater;\n }\n const moduleName = \"@tauri-apps/plugin-updater\";\n // @ts-ignore - Optional runtime dependency in Tauri apps\n const plugin = await import(/* @vite-ignore */ moduleName).catch(() => null);\n return plugin;\n } catch {\n return null;\n }\n };\n\n const getTauriProcessPlugin = async () => {\n if (typeof window === \"undefined\") return null;\n try {\n if ((window as any).__TAURI__?.process) {\n return (window as any).__TAURI__.process;\n }\n const moduleName = \"@tauri-apps/plugin-process\";\n // @ts-ignore - Optional runtime dependency in Tauri apps\n const plugin = await import(/* @vite-ignore */ moduleName).catch(() => null);\n return plugin;\n } catch {\n return null;\n }\n };\n\n const checkForUpdates = async (): Promise<boolean> => {\n setStatus(\"checking\");\n setError(null);\n\n if (isTauriEnvironment()) {\n try {\n const updaterPlugin = await getTauriUpdaterPlugin();\n if (updaterPlugin?.check) {\n const update = await updaterPlugin.check();\n if (update?.available) {\n activeTauriUpdate = update;\n const info: UpdateManifestInfo = {\n version: update.version,\n currentVersion: update.currentVersion || options.currentVersion || \"v1.0.0\",\n date: update.date,\n body: update.body || \"Bug fixes and performance enhancements.\",\n };\n setUpdateInfo(info);\n setStatus(\"available\");\n options.onUpdateAvailable?.(info);\n return true;\n } else {\n setStatus(\"up-to-date\");\n return false;\n }\n }\n } catch (err: any) {\n const errMsg = err?.message || String(err);\n setError(errMsg);\n setStatus(\"error\");\n options.onError?.(errMsg);\n return false;\n }\n }\n\n // In web preview / non-tauri mode, simulate checking\n await new Promise((resolve) => setTimeout(resolve, 800));\n setStatus(\"up-to-date\");\n return false;\n };\n\n const downloadAndInstall = async (): Promise<void> => {\n if (status() !== \"available\" && status() !== \"error\") return;\n setStatus(\"downloading\");\n setError(null);\n setProgress({ downloaded: 0, total: 100, percentage: 0 });\n\n if (isTauriEnvironment() && activeTauriUpdate?.downloadAndInstall) {\n try {\n let downloadedBytes = 0;\n let contentLength = 0;\n\n await activeTauriUpdate.downloadAndInstall((event: any) => {\n if (event.event === \"Started\") {\n contentLength = event.data?.contentLength || 100;\n } else if (event.event === \"Progress\") {\n downloadedBytes += event.data?.chunkLength || 0;\n const pct = contentLength > 0 ? Math.min(100, Math.round((downloadedBytes / contentLength) * 100)) : 50;\n setProgress({\n downloaded: downloadedBytes,\n total: contentLength,\n percentage: pct,\n });\n } else if (event.event === \"Finished\") {\n setProgress({\n downloaded: contentLength,\n total: contentLength,\n percentage: 100,\n });\n }\n });\n\n setStatus(\"downloaded\");\n options.onDownloadFinished?.();\n return;\n } catch (err: any) {\n const errMsg = err?.message || String(err);\n setError(errMsg);\n setStatus(\"error\");\n options.onError?.(errMsg);\n return;\n }\n }\n\n // Web simulation mode with smooth progress step\n let currentPct = 0;\n simulatedTimer = setInterval(() => {\n currentPct += 15;\n if (currentPct >= 100) {\n clearInterval(simulatedTimer);\n setProgress({ downloaded: 45.8 * 1024 * 1024, total: 45.8 * 1024 * 1024, percentage: 100 });\n setStatus(\"downloaded\");\n options.onDownloadFinished?.();\n } else {\n const downloaded = (45.8 * 1024 * 1024 * currentPct) / 100;\n setProgress({ downloaded, total: 45.8 * 1024 * 1024, percentage: currentPct });\n }\n }, 250);\n };\n\n const relaunch = async (): Promise<void> => {\n if (isTauriEnvironment()) {\n try {\n const processPlugin = await getTauriProcessPlugin();\n if (processPlugin?.relaunch) {\n await processPlugin.relaunch();\n return;\n }\n } catch {\n // Fallback\n }\n }\n\n if (typeof window !== \"undefined\") {\n window.location.reload();\n }\n };\n\n const dismiss = (): void => {\n if (simulatedTimer) clearInterval(simulatedTimer);\n setStatus(\"idle\");\n setError(null);\n };\n\n const simulateUpdate = (mockInfo?: Partial<UpdateManifestInfo>): void => {\n if (simulatedTimer) clearInterval(simulatedTimer);\n const info: UpdateManifestInfo = {\n version: mockInfo?.version || \"v1.2.0\",\n currentVersion: mockInfo?.currentVersion || options.currentVersion || \"v1.0.0\",\n date: mockInfo?.date || new Date().toISOString().split(\"T\")[0],\n body:\n mockInfo?.body ||\n \"### What's New in v1.2.0\\n- Added native Window Titlebar tabs\\n- Enhanced Tauri v2 capability security\\n- Optimized SolidJS signal reactivity\\n- Fixed titlebar drag region jitter on Linux\",\n };\n setUpdateInfo(info);\n setStatus(\"available\");\n setError(null);\n setProgress({ downloaded: 0, total: 100, percentage: 0 });\n options.onUpdateAvailable?.(info);\n };\n\n onMount(() => {\n if (options.autoCheck) {\n checkForUpdates();\n }\n });\n\n const isLoading = () => status() === \"checking\" || status() === \"downloading\";\n\n return {\n status,\n updateInfo,\n progress,\n error,\n isLoading,\n checkForUpdates,\n downloadAndInstall,\n relaunch,\n dismiss,\n simulateUpdate,\n };\n}\n",
|
|
10
|
+
"type": "registry:hook"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-document-tabs",
|
|
3
|
+
"title": "createDocumentTabs",
|
|
4
|
+
"description": "SolidJS reactive primitive for managing multi-document tabs, editor buffers, and browser tab stacks with adjacent activation and pinned state",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "hooks/create-document-tabs.ts",
|
|
9
|
+
"content": "import {\n createSignal,\n createMemo,\n onMount,\n onCleanup,\n type Accessor,\n} from \"solid-js\";\n\nexport interface TabItem<T = any> {\n id: string;\n title: string;\n icon?: any;\n isDirty?: boolean;\n isPinned?: boolean;\n closable?: boolean;\n data?: T;\n}\n\nexport interface CreateDocumentTabsOptions<T = any> {\n /** Initial array of tab items. */\n initialTabs?: TabItem<T>[];\n /** Default active tab id. Defaults to first tab if available. */\n defaultActiveId?: string;\n /** Whether to enable keyboard shortcuts (Ctrl/Cmd+W to close active, Ctrl/Cmd+T for new tab). */\n enableKeybindings?: boolean;\n /** Callback triggered when the active tab changes. */\n onTabChange?: (tab: TabItem<T> | undefined) => void;\n /** Callback triggered before or when a tab closes. Return false to prevent closing. */\n onTabClose?: (tab: TabItem<T>) => boolean | void;\n /** Callback triggered when a new tab is created. */\n onTabAdd?: (tab: TabItem<T>) => void;\n}\n\nexport interface CreateDocumentTabsReturn<T = any> {\n /** All tabs list. */\n tabs: Accessor<TabItem<T>[]>;\n /** Currently active tab ID. */\n activeTabId: Accessor<string>;\n /** Currently active tab object. */\n activeTab: Accessor<TabItem<T> | undefined>;\n /** Pinned tabs only. */\n pinnedTabs: Accessor<TabItem<T>[]>;\n /** Standard unpinned tabs. */\n unpinnedTabs: Accessor<TabItem<T>[]>;\n /** Whether any open tab has unsaved changes (isDirty). */\n hasDirtyTabs: Accessor<boolean>;\n /** Count of total open tabs. */\n count: Accessor<number>;\n /** Add a new tab to the list. */\n addTab: (tab: TabItem<T>, activate?: boolean) => void;\n /** Close a tab by ID with auto-switching to adjacent tab. */\n closeTab: (id: string) => boolean;\n /** Set the active tab. */\n setActiveTab: (id: string) => void;\n /** Mark or unmark a tab as dirty (unsaved changes). */\n markDirty: (id: string, isDirty?: boolean) => void;\n /** Toggle pinned state of a tab. */\n togglePin: (id: string) => void;\n /** Close all tabs except the specified one. */\n closeOthers: (id: string) => void;\n /** Close all open unpinned tabs. */\n closeAll: (includePinned?: boolean) => void;\n /** Switch to the next tab in sequence. */\n nextTab: () => void;\n /** Switch to the previous tab in sequence. */\n prevTab: () => void;\n /** Reorder tabs from one position to another. */\n reorderTabs: (fromIndex: number, toIndex: number) => void;\n}\n\nfunction findLastPinnedIndex<T>(items: TabItem<T>[]): number {\n for (let i = items.length - 1; i >= 0; i--) {\n if (items[i].isPinned) return i;\n }\n return -1;\n}\n\n/**\n * SolidJS reactive primitive for managing multi-document tabs, editor buffers, and browser tab states.\n */\nexport function createDocumentTabs<T = any>(\n options: CreateDocumentTabsOptions<T> = {}\n): CreateDocumentTabsReturn<T> {\n const initial = options.initialTabs || [];\n const [tabs, setTabs] = createSignal<TabItem<T>[]>(initial);\n const [activeTabId, setActiveTabId] = createSignal<string>(\n options.defaultActiveId || (initial.length > 0 ? initial[0].id : \"\")\n );\n\n const activeTab = createMemo(() => tabs().find((t) => t.id === activeTabId()));\n const pinnedTabs = createMemo(() => tabs().filter((t) => t.isPinned));\n const unpinnedTabs = createMemo(() => tabs().filter((t) => !t.isPinned));\n const hasDirtyTabs = createMemo(() => tabs().some((t) => t.isDirty));\n const count = createMemo(() => tabs().length);\n\n const setActive = (id: string) => {\n const target = tabs().find((t) => t.id === id);\n if (target) {\n setActiveTabId(id);\n options.onTabChange?.(target);\n }\n };\n\n const addTab = (tab: TabItem<T>, activate = true) => {\n const existing = tabs().find((t) => t.id === tab.id);\n if (existing) {\n if (activate) setActive(tab.id);\n return;\n }\n\n setTabs((prev) => {\n if (tab.isPinned) {\n const lastPinnedIdx = findLastPinnedIndex(prev);\n if (lastPinnedIdx === -1) return [tab, ...prev];\n const next = [...prev];\n next.splice(lastPinnedIdx + 1, 0, tab);\n return next;\n }\n return [...prev, tab];\n });\n\n if (activate) {\n setActiveTabId(tab.id);\n options.onTabChange?.(tab);\n }\n options.onTabAdd?.(tab);\n };\n\n const closeTab = (id: string): boolean => {\n const currentTabs = tabs();\n const tabToClose = currentTabs.find((t) => t.id === id);\n if (!tabToClose) return false;\n\n if (options.onTabClose && options.onTabClose(tabToClose) === false) {\n return false;\n }\n\n const closeIdx = currentTabs.findIndex((t) => t.id === id);\n const nextTabs = currentTabs.filter((t) => t.id !== id);\n setTabs(nextTabs);\n\n if (activeTabId() === id) {\n if (nextTabs.length === 0) {\n setActiveTabId(\"\");\n options.onTabChange?.(undefined);\n } else {\n const nextIdx = Math.min(closeIdx, nextTabs.length - 1);\n const nextActive = nextTabs[nextIdx];\n setActiveTabId(nextActive.id);\n options.onTabChange?.(nextActive);\n }\n }\n return true;\n };\n\n const markDirty = (id: string, isDirty = true) => {\n setTabs((prev) =>\n prev.map((t) => (t.id === id ? { ...t, isDirty } : t))\n );\n };\n\n const togglePin = (id: string) => {\n setTabs((prev) => {\n const target = prev.find((t) => t.id === id);\n if (!target) return prev;\n const updated = { ...target, isPinned: !target.isPinned };\n const remaining = prev.filter((t) => t.id !== id);\n\n if (updated.isPinned) {\n const lastPinnedIdx = findLastPinnedIndex(remaining);\n if (lastPinnedIdx === -1) return [updated, ...remaining];\n remaining.splice(lastPinnedIdx + 1, 0, updated);\n return remaining;\n }\n return [...remaining, updated];\n });\n };\n\n const closeOthers = (id: string) => {\n setTabs((prev) => prev.filter((t) => t.id === id || t.isPinned));\n setActive(id);\n };\n\n const closeAll = (includePinned = false) => {\n if (includePinned) {\n setTabs([]);\n setActiveTabId(\"\");\n options.onTabChange?.(undefined);\n } else {\n const pinned = tabs().filter((t) => t.isPinned);\n setTabs(pinned);\n if (pinned.length > 0) {\n setActiveTabId(pinned[0].id);\n options.onTabChange?.(pinned[0]);\n } else {\n setActiveTabId(\"\");\n options.onTabChange?.(undefined);\n }\n }\n };\n\n const nextTab = () => {\n const list = tabs();\n if (list.length <= 1) return;\n const currentIdx = list.findIndex((t) => t.id === activeTabId());\n const nextIdx = (currentIdx + 1) % list.length;\n setActive(list[nextIdx].id);\n };\n\n const prevTab = () => {\n const list = tabs();\n if (list.length <= 1) return;\n const currentIdx = list.findIndex((t) => t.id === activeTabId());\n const prevIdx = (currentIdx - 1 + list.length) % list.length;\n setActive(list[prevIdx].id);\n };\n\n const reorderTabs = (fromIndex: number, toIndex: number) => {\n setTabs((prev) => {\n if (fromIndex < 0 || fromIndex >= prev.length || toIndex < 0 || toIndex >= prev.length) {\n return prev;\n }\n const clone = [...prev];\n const [moved] = clone.splice(fromIndex, 1);\n clone.splice(toIndex, 0, moved);\n return clone;\n });\n };\n\n onMount(() => {\n if (!options.enableKeybindings || typeof window === \"undefined\") return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n const isCmdOrCtrl = e.metaKey || e.ctrlKey;\n if (isCmdOrCtrl && (e.key === \"w\" || e.key === \"W\")) {\n const active = activeTabId();\n if (active) {\n e.preventDefault();\n closeTab(active);\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n onCleanup(() => window.removeEventListener(\"keydown\", handleKeyDown));\n });\n\n return {\n tabs,\n activeTabId,\n activeTab,\n pinnedTabs,\n unpinnedTabs,\n hasDirtyTabs,\n count,\n addTab,\n closeTab,\n setActiveTab: setActive,\n markDirty,\n togglePin,\n closeOthers,\n closeAll,\n nextTab,\n prevTab,\n reorderTabs,\n };\n}\n",
|
|
10
|
+
"type": "registry:hook"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-global-shortcut",
|
|
3
|
+
"title": "createGlobalShortcut",
|
|
4
|
+
"description": "SolidJS reactive primitive for registering OS-level global keyboard shortcuts via Tauri v2 plugin",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "hooks/create-global-shortcut.ts",
|
|
9
|
+
"content": "import { onMount, onCleanup } from \"solid-js\";\nimport { isTauriEnvironment } from \"./create-tauri-window\";\n\nexport interface CreateGlobalShortcutOptions {\n /** Shortcut key combination, e.g. 'CommandOrControl+Shift+P' or 'Alt+Space'. */\n shortcut: string | string[];\n /** Handler invoked when global shortcut is triggered. */\n onTrigger: (shortcut: string) => void;\n /** Automatically register shortcut on mount. Defaults to true. */\n autoRegister?: boolean;\n}\n\nexport interface CreateGlobalShortcutReturn {\n /** Register the configured global shortcut with Tauri / Browser. */\n register: () => Promise<boolean>;\n /** Unregister the shortcut. */\n unregister: () => Promise<void>;\n /** Indicates whether the shortcut is currently registered. */\n isRegistered: () => boolean;\n}\n\n/**\n * SolidJS reactive primitive for registering native OS global hotkeys via Tauri v2 global shortcut plugin,\n * with automatic fallback to browser DOM keyboard listeners.\n */\nexport function createGlobalShortcut(\n options: CreateGlobalShortcutOptions\n): CreateGlobalShortcutReturn {\n let registered = false;\n const shortcuts = Array.isArray(options.shortcut) ? options.shortcut : [options.shortcut];\n\n const getTauriShortcutPlugin = async () => {\n if (typeof window === \"undefined\") return null;\n try {\n if ((window as any).__TAURI__?.globalShortcut) {\n return (window as any).__TAURI__.globalShortcut;\n }\n // Variable prevents Vite dev server from static module resolution\n const moduleName = \"@tauri-apps/plugin-global-shortcut\";\n // @ts-ignore - Optional runtime dependency in Tauri apps\n const plugin = await import(/* @vite-ignore */ moduleName).catch(() => null);\n return plugin;\n } catch {\n return null;\n }\n };\n\n const normalizeKey = (k: string) => {\n const key = k.toLowerCase().trim();\n if (key === \" \" || key === \"space\" || key === \"spacebar\") return \"space\";\n if (key === \"esc\" || key === \"escape\") return \"escape\";\n if (key === \"return\" || key === \"enter\") return \"enter\";\n return key;\n };\n\n const handleBrowserKeydown = (e: KeyboardEvent) => {\n const eventKey = normalizeKey(e.key);\n const eventCode = normalizeKey(e.code.replace(/^Key|^Digit/, \"\"));\n\n for (const keyCombo of shortcuts) {\n const parts = keyCombo.toLowerCase().split(\"+\");\n const hasCtrlOrCmd = parts.includes(\"commandorcontrol\") || parts.includes(\"ctrl\") || parts.includes(\"cmd\");\n const hasShift = parts.includes(\"shift\");\n const hasAlt = parts.includes(\"alt\");\n const mainKey = parts.find((p) => ![\"commandorcontrol\", \"ctrl\", \"cmd\", \"shift\", \"alt\"].includes(p));\n\n const ctrlMatch = hasCtrlOrCmd ? e.ctrlKey || e.metaKey : !e.ctrlKey && !e.metaKey;\n const shiftMatch = hasShift ? e.shiftKey : !e.shiftKey;\n const altMatch = hasAlt ? e.altKey : !e.altKey;\n const keyMatch = mainKey\n ? eventKey === normalizeKey(mainKey) || eventCode === normalizeKey(mainKey)\n : false;\n\n if (ctrlMatch && shiftMatch && altMatch && keyMatch) {\n e.preventDefault();\n options.onTrigger(keyCombo);\n break;\n }\n }\n };\n\n const register = async (): Promise<boolean> => {\n if (typeof window === \"undefined\") return false;\n\n if (isTauriEnvironment()) {\n const plugin = await getTauriShortcutPlugin();\n if (plugin?.register) {\n try {\n for (const s of shortcuts) {\n await plugin.register(s, () => options.onTrigger(s));\n }\n registered = true;\n return true;\n } catch {\n // Fallback to web listener\n }\n }\n }\n\n window.addEventListener(\"keydown\", handleBrowserKeydown);\n registered = true;\n return true;\n };\n\n const unregister = async (): Promise<void> => {\n if (typeof window === \"undefined\") return;\n\n if (isTauriEnvironment()) {\n const plugin = await getTauriShortcutPlugin();\n if (plugin?.unregister) {\n try {\n for (const s of shortcuts) {\n await plugin.unregister(s);\n }\n } catch {\n // Handled\n }\n }\n }\n\n window.removeEventListener(\"keydown\", handleBrowserKeydown);\n registered = false;\n };\n\n onMount(() => {\n if (options.autoRegister !== false) {\n register();\n }\n });\n\n onCleanup(() => {\n unregister();\n });\n\n return {\n register,\n unregister,\n isRegistered: () => registered,\n };\n}\n",
|
|
10
|
+
"type": "registry:hook"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-tauri-window",
|
|
3
|
+
"title": "createTauriWindow",
|
|
4
|
+
"description": "SolidJS reactive primitive for interacting with and controlling Tauri v2 application window states",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "hooks/create-tauri-window.ts",
|
|
9
|
+
"content": "import { createSignal, onMount, onCleanup, type Accessor } from \"solid-js\";\n\nexport type TauriPlatform = \"macos\" | \"windows\" | \"linux\" | \"web\";\n\nexport interface CreateTauriWindowOptions {\n /** Initial fallback platform if not detected automatically. */\n defaultPlatform?: TauriPlatform;\n /** Listen to window resize and focus events. Defaults to true. */\n listenEvents?: boolean;\n}\n\nexport interface CreateTauriWindowReturn {\n /** Signal indicating whether the application is running inside a Tauri runtime. */\n isTauri: Accessor<boolean>;\n /** Detected host platform ('macos' | 'windows' | 'linux' | 'web'). */\n platform: Accessor<TauriPlatform>;\n /** Signal indicating whether the window is currently maximized. */\n isMaximized: Accessor<boolean>;\n /** Signal indicating whether the window is currently minimized. */\n isMinimized: Accessor<boolean>;\n /** Signal indicating whether the window is currently in fullscreen mode. */\n isFullscreen: Accessor<boolean>;\n /** Signal indicating whether the window is currently focused. */\n isFocused: Accessor<boolean>;\n /** Minimize the application window. */\n minimize: () => Promise<void>;\n /** Maximize the application window. */\n maximize: () => Promise<void>;\n /** Unmaximize the application window. */\n unmaximize: () => Promise<void>;\n /** Toggle between maximized and normal window states. */\n toggleMaximize: () => Promise<void>;\n /** Close the application window or exit process if main window. */\n close: () => Promise<void>;\n /** Forcefully destroy/close the application window. */\n destroy: () => Promise<void>;\n /** Toggle fullscreen state. */\n setFullscreen: (fullscreen: boolean) => Promise<void>;\n /** Start dragging the window. Can be bound to mousedown event or custom titlebar headers. */\n startDragging: (e?: MouseEvent) => Promise<void>;\n}\n\n/**\n * Helper to detect current operating system in browser or desktop environments.\n */\nexport function detectPlatform(): TauriPlatform {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return \"web\";\n }\n\n const userAgent = navigator.userAgent.toLowerCase();\n const platform =\n (navigator as any).userAgentData?.platform?.toLowerCase() ||\n navigator.platform?.toLowerCase() ||\n \"\";\n\n if (platform.includes(\"mac\") || userAgent.includes(\"macintosh\") || userAgent.includes(\"mac os x\")) {\n return \"macos\";\n }\n if (platform.includes(\"win\") || userAgent.includes(\"windows\")) {\n return \"windows\";\n }\n if (platform.includes(\"linux\") || userAgent.includes(\"linux\")) {\n return \"linux\";\n }\n\n return \"web\";\n}\n\n/**\n * Checks whether the current environment is running inside Tauri v1 or v2.\n */\nexport function isTauriEnvironment(): boolean {\n if (typeof window === \"undefined\") return false;\n const win = window as any;\n return Boolean(\n win.__TAURI_INTERNALS__ ||\n win.__TAURI__ ||\n win.__TAURI_PATTERN__ ||\n win.__TAURI_METADATA__ ||\n win.__TAURI_IPC__ ||\n win.__TAURI_WINDOW__\n );\n}\n\n/**\n * Checks whether the current environment is running inside Electron.\n */\nexport function isElectronEnvironment(): boolean {\n if (typeof window === \"undefined\") return false;\n const win = window as any;\n return Boolean(\n win.electron ||\n win.electronAPI ||\n win.ipcRenderer ||\n win.process?.versions?.electron ||\n (typeof navigator !== \"undefined\" && navigator.userAgent?.toLowerCase().includes(\"electron\"))\n );\n}\n\n/**\n * Direct low-level IPC invocation across Tauri v2 (__TAURI_INTERNALS__) and Tauri v1.\n */\nasync function invokeTauri(\n v2Cmd: string,\n v2Args?: Record<string, any>,\n v1CmdType?: string,\n v1Data?: any\n): Promise<any> {\n if (typeof window === \"undefined\") return undefined;\n const win = window as any;\n\n // 1. Try Tauri v2 direct IPC via injected __TAURI_INTERNALS__ or core.invoke\n const invokeV2 = win.__TAURI_INTERNALS__?.invoke || win.__TAURI__?.core?.invoke;\n if (typeof invokeV2 === \"function\") {\n try {\n return await invokeV2(`plugin:window|${v2Cmd}`, v2Args || {});\n } catch {\n // Fallback to secondary invoke mechanisms\n }\n }\n\n // 2. Try Tauri v1 / generic invoke\n const invokeV1 = win.__TAURI__?.invoke || win.__TAURI_INVOKE__;\n if (typeof invokeV1 === \"function\") {\n try {\n if (v1CmdType) {\n return await invokeV1(\"tauri\", {\n __tauriModule: \"Window\",\n message: {\n cmd: \"manage\",\n data: {\n cmd: {\n type: v1CmdType,\n ...(v1Data !== undefined ? { data: v1Data } : {}),\n },\n },\n },\n });\n }\n return await invokeV1(v2Cmd, v2Args);\n } catch {\n // Fallback\n }\n }\n\n return undefined;\n}\n\n/**\n * Helper to send Electron IPC messages if running in an Electron shell.\n */\nfunction sendElectronMessage(channel: string, ...args: any[]): boolean {\n if (typeof window === \"undefined\") return false;\n const win = window as any;\n\n try {\n if (win.electronAPI && typeof win.electronAPI[channel] === \"function\") {\n win.electronAPI[channel](...args);\n return true;\n }\n if (win.electron?.ipcRenderer?.send) {\n win.electron.ipcRenderer.send(channel, ...args);\n return true;\n }\n if (win.ipcRenderer?.send) {\n win.ipcRenderer.send(channel, ...args);\n return true;\n }\n if (typeof win.require === \"function\") {\n const electron = win.require(\"electron\");\n if (electron?.ipcRenderer?.send) {\n electron.ipcRenderer.send(channel, ...args);\n return true;\n }\n }\n } catch {\n // Ignored\n }\n return false;\n}\n\n/**\n * SolidJS reactive primitive for controlling and observing desktop window states.\n * Supports Tauri v2, Tauri v1, Electron, and browser preview environments.\n */\nexport function createTauriWindow(\n options: CreateTauriWindowOptions = {}\n): CreateTauriWindowReturn {\n const [isTauri, setIsTauri] = createSignal<boolean>(false);\n const [platform, setPlatform] = createSignal<TauriPlatform>(options.defaultPlatform || \"web\");\n const [isMaximized, setIsMaximized] = createSignal<boolean>(false);\n const [isMinimized, setIsMinimized] = createSignal<boolean>(false);\n const [isFullscreen, setIsFullscreen] = createSignal<boolean>(false);\n const [isFocused, setIsFocused] = createSignal<boolean>(true);\n\n let unlistenResize: (() => void) | null = null;\n let unlistenFocus: (() => void) | null = null;\n\n // Retrieve Tauri Window instance safely\n const getTauriWindow = async () => {\n if (typeof window === \"undefined\") return null;\n const win = window as any;\n try {\n if (win.__TAURI__?.window?.getCurrentWindow) {\n return win.__TAURI__.window.getCurrentWindow();\n }\n if (win.__TAURI__?.window?.appWindow) {\n return win.__TAURI__.window.appWindow;\n }\n if (win.__TAURI_WINDOW__?.getCurrentWindow) {\n return win.__TAURI_WINDOW__.getCurrentWindow();\n }\n if (win.__TAURI_WINDOW__?.appWindow) {\n return win.__TAURI_WINDOW__.appWindow;\n }\n // Dynamic import if bundled by user's build setup\n const moduleName = \"@tauri-apps/api/window\";\n // @ts-ignore - Optional runtime dependency in Tauri apps\n const tauriApi = await import(/* @vite-ignore */ moduleName).catch(() => null);\n if (tauriApi?.getCurrentWindow) {\n return tauriApi.getCurrentWindow();\n }\n if (tauriApi?.appWindow) {\n return tauriApi.appWindow;\n }\n } catch {\n // Running in standard web mode\n }\n return null;\n };\n\n const updateWindowStates = async () => {\n if (typeof window === \"undefined\") return;\n\n const tauriWin = await getTauriWindow();\n if (tauriWin) {\n setIsTauri(true);\n try {\n const [max, min, full, foc] = await Promise.all([\n typeof tauriWin.isMaximized === \"function\" ? tauriWin.isMaximized() : false,\n typeof tauriWin.isMinimized === \"function\" ? tauriWin.isMinimized() : false,\n typeof tauriWin.isFullscreen === \"function\" ? tauriWin.isFullscreen() : false,\n typeof tauriWin.isFocused === \"function\" ? tauriWin.isFocused() : true,\n ]);\n setIsMaximized(Boolean(max));\n setIsMinimized(Boolean(min));\n setIsFullscreen(Boolean(full));\n setIsFocused(Boolean(foc));\n return;\n } catch {\n // Fallback gracefully\n }\n }\n\n if (isTauriEnvironment()) {\n setIsTauri(true);\n try {\n const max = await invokeTauri(\"is_maximized\", {}, \"isMaximized\");\n if (typeof max === \"boolean\") setIsMaximized(max);\n const min = await invokeTauri(\"is_minimized\", {}, \"isMinimized\");\n if (typeof min === \"boolean\") setIsMinimized(min);\n const full = await invokeTauri(\"is_fullscreen\", {}, \"isFullscreen\");\n if (typeof full === \"boolean\") setIsFullscreen(full);\n const foc = await invokeTauri(\"is_focused\", {}, \"isFocused\");\n if (typeof foc === \"boolean\") setIsFocused(foc);\n return;\n } catch {\n // Fallback\n }\n }\n\n if (!isElectronEnvironment()) {\n setIsTauri(false);\n }\n if (typeof document !== \"undefined\") {\n setIsFullscreen(Boolean(document.fullscreenElement));\n }\n };\n\n onMount(async () => {\n if (typeof window === \"undefined\") return;\n\n setPlatform(detectPlatform());\n setIsTauri(isTauriEnvironment());\n await updateWindowStates();\n\n if (options.listenEvents !== false) {\n const handleWindowFocus = () => setIsFocused(true);\n const handleWindowBlur = () => setIsFocused(false);\n const handleFullscreenChange = () => setIsFullscreen(Boolean(document.fullscreenElement));\n const handleResize = () => {\n updateWindowStates();\n };\n\n window.addEventListener(\"focus\", handleWindowFocus);\n window.addEventListener(\"blur\", handleWindowBlur);\n window.addEventListener(\"resize\", handleResize);\n document.addEventListener(\"fullscreenchange\", handleFullscreenChange);\n\n const tauriWin = await getTauriWindow();\n if (tauriWin?.onResized) {\n try {\n unlistenResize = await tauriWin.onResized(() => updateWindowStates());\n unlistenFocus = await tauriWin.onFocusChanged(({ payload }: { payload: boolean }) => {\n setIsFocused(payload);\n });\n } catch {\n // Ignored in non-tauri contexts\n }\n }\n\n onCleanup(() => {\n window.removeEventListener(\"focus\", handleWindowFocus);\n window.removeEventListener(\"blur\", handleWindowBlur);\n window.removeEventListener(\"resize\", handleResize);\n document.removeEventListener(\"fullscreenchange\", handleFullscreenChange);\n if (unlistenResize) unlistenResize();\n if (unlistenFocus) unlistenFocus();\n });\n }\n });\n\n const minimize = async (): Promise<void> => {\n let executed = false;\n const tauriWin = await getTauriWindow();\n if (tauriWin?.minimize) {\n try {\n await tauriWin.minimize();\n executed = true;\n } catch {}\n }\n if (!executed && isTauriEnvironment()) {\n try {\n await invokeTauri(\"minimize\", {}, \"minimize\");\n executed = true;\n } catch {}\n }\n if (!executed && isElectronEnvironment()) {\n executed =\n sendElectronMessage(\"window-minimize\") ||\n sendElectronMessage(\"minimize\") ||\n sendElectronMessage(\"windowMinimize\");\n }\n\n setIsMinimized(true);\n setIsMaximized(false);\n };\n\n const maximize = async (): Promise<void> => {\n let executed = false;\n const tauriWin = await getTauriWindow();\n if (tauriWin?.maximize) {\n try {\n await tauriWin.maximize();\n executed = true;\n } catch {}\n }\n if (!executed && isTauriEnvironment()) {\n try {\n await invokeTauri(\"maximize\", {}, \"maximize\");\n executed = true;\n } catch {}\n }\n if (!executed && isElectronEnvironment()) {\n executed =\n sendElectronMessage(\"window-maximize\") ||\n sendElectronMessage(\"maximize\") ||\n sendElectronMessage(\"windowMaximize\");\n }\n\n setIsMaximized(true);\n setIsMinimized(false);\n };\n\n const unmaximize = async (): Promise<void> => {\n let executed = false;\n const tauriWin = await getTauriWindow();\n if (tauriWin?.unmaximize) {\n try {\n await tauriWin.unmaximize();\n executed = true;\n } catch {}\n }\n if (!executed && isTauriEnvironment()) {\n try {\n await invokeTauri(\"unmaximize\", {}, \"unmaximize\");\n executed = true;\n } catch {}\n }\n if (!executed && isElectronEnvironment()) {\n executed =\n sendElectronMessage(\"window-unmaximize\") ||\n sendElectronMessage(\"unmaximize\") ||\n sendElectronMessage(\"windowRestore\") ||\n sendElectronMessage(\"restore\");\n }\n\n setIsMaximized(false);\n };\n\n let lastToggleTime = 0;\n const toggleMaximize = async (): Promise<void> => {\n const now = Date.now();\n if (now - lastToggleTime < 350) {\n return; // Ignore duplicate rapid triggers within 350ms window\n }\n lastToggleTime = now;\n\n let executed = false;\n const tauriWin = await getTauriWindow();\n if (tauriWin) {\n try {\n if (typeof tauriWin.isMaximized === \"function\") {\n const isMax = await tauriWin.isMaximized();\n if (isMax && typeof tauriWin.unmaximize === \"function\") {\n await tauriWin.unmaximize();\n setIsMaximized(false);\n executed = true;\n } else if (!isMax && typeof tauriWin.maximize === \"function\") {\n await tauriWin.maximize();\n setIsMaximized(true);\n executed = true;\n }\n }\n if (!executed && typeof tauriWin.toggleMaximize === \"function\") {\n await tauriWin.toggleMaximize();\n executed = true;\n }\n } catch {}\n }\n\n if (!executed && isTauriEnvironment()) {\n try {\n const isMax = await invokeTauri(\"is_maximized\", {}, \"isMaximized\");\n if (typeof isMax === \"boolean\") {\n if (isMax) {\n await invokeTauri(\"unmaximize\", {}, \"unmaximize\");\n setIsMaximized(false);\n } else {\n await invokeTauri(\"maximize\", {}, \"maximize\");\n setIsMaximized(true);\n }\n executed = true;\n } else {\n await invokeTauri(\"toggle_maximize\", {}, \"toggleMaximize\");\n executed = true;\n }\n } catch {}\n }\n\n if (!executed && isElectronEnvironment()) {\n executed =\n sendElectronMessage(\"window-toggle-maximize\") ||\n sendElectronMessage(\"toggle-maximize\") ||\n sendElectronMessage(\"toggleMaximize\");\n }\n\n if (!executed) {\n setIsMaximized((prev) => !prev);\n }\n setIsMinimized(false);\n };\n\n const close = async (): Promise<void> => {\n let closed = false;\n const win = typeof window !== \"undefined\" ? (window as any) : null;\n const tauriWin = await getTauriWindow();\n\n // 1. Try Tauri Window API close\n if (tauriWin?.close) {\n try {\n await tauriWin.close();\n closed = true;\n } catch {}\n }\n\n // 2. Try Tauri IPC window close\n if (!closed && isTauriEnvironment()) {\n try {\n await invokeTauri(\"close\", {}, \"close\");\n closed = true;\n } catch {}\n }\n\n // 3. Try Tauri destroy fallback if close did not succeed\n if (!closed && tauriWin?.destroy) {\n try {\n await tauriWin.destroy();\n closed = true;\n } catch {}\n }\n if (!closed && isTauriEnvironment()) {\n try {\n await invokeTauri(\"destroy\", {}, \"destroy\");\n closed = true;\n } catch {}\n }\n\n // 4. Try Tauri process exit if in Tauri environment\n if (!closed && isTauriEnvironment()) {\n try {\n const invokeProcess = win?.__TAURI_INTERNALS__?.invoke || win?.__TAURI__?.core?.invoke;\n if (typeof invokeProcess === \"function\") {\n await invokeProcess(\"plugin:process|exit\", { code: 0 });\n closed = true;\n } else if (win?.__TAURI__?.process?.exit) {\n await win.__TAURI__.process.exit(0);\n closed = true;\n }\n } catch {}\n }\n\n // 5. Try Electron window close / app quit\n if (!closed && isElectronEnvironment()) {\n closed =\n sendElectronMessage(\"window-close\") ||\n sendElectronMessage(\"close\") ||\n sendElectronMessage(\"app-close\") ||\n sendElectronMessage(\"quit\");\n }\n\n // 6. Browser fallback & custom event dispatch\n if (typeof window !== \"undefined\") {\n try {\n window.dispatchEvent(new CustomEvent(\"nikala:window-close\"));\n } catch {}\n\n try {\n window.close();\n } catch {}\n }\n };\n\n const destroy = async (): Promise<void> => {\n const tauriWin = await getTauriWindow();\n if (tauriWin?.destroy) {\n try {\n await tauriWin.destroy();\n return;\n } catch {}\n }\n if (isTauriEnvironment()) {\n try {\n await invokeTauri(\"destroy\", {}, \"destroy\");\n return;\n } catch {}\n }\n await close();\n };\n\n const setFullscreen = async (fullscreen: boolean): Promise<void> => {\n let executed = false;\n const tauriWin = await getTauriWindow();\n if (tauriWin?.setFullscreen) {\n try {\n await tauriWin.setFullscreen(fullscreen);\n executed = true;\n } catch {}\n }\n if (!executed && isTauriEnvironment()) {\n try {\n await invokeTauri(\"set_fullscreen\", { value: fullscreen }, \"setFullscreen\", fullscreen);\n executed = true;\n } catch {}\n }\n if (!executed && isElectronEnvironment()) {\n executed =\n sendElectronMessage(\"window-set-fullscreen\", fullscreen) ||\n sendElectronMessage(\"set-fullscreen\", fullscreen);\n }\n\n if (typeof document !== \"undefined\") {\n if (fullscreen && document.documentElement.requestFullscreen) {\n try {\n await document.documentElement.requestFullscreen();\n } catch {}\n } else if (!fullscreen && document.fullscreenElement && document.exitFullscreen) {\n try {\n await document.exitFullscreen();\n } catch {}\n }\n }\n\n setIsFullscreen(fullscreen);\n };\n\n const startDragging = async (e?: MouseEvent): Promise<void> => {\n if (e) {\n if (e.button !== 0) return; // Only primary mouse button starts drag\n if (e.detail > 1) return; // Prevent drag initialization on double-click gesture\n }\n let dragged = false;\n const tauriWin = await getTauriWindow();\n if (tauriWin?.startDragging) {\n try {\n await tauriWin.startDragging();\n dragged = true;\n } catch {}\n }\n if (!dragged && isTauriEnvironment()) {\n try {\n await invokeTauri(\"start_dragging\", {}, \"startDragging\");\n dragged = true;\n } catch {}\n }\n if (!dragged && isElectronEnvironment()) {\n sendElectronMessage(\"window-drag\");\n }\n };\n\n return {\n isTauri,\n platform,\n isMaximized,\n isMinimized,\n isFullscreen,\n isFocused,\n minimize,\n maximize,\n unmaximize,\n toggleMaximize,\n close,\n destroy,\n setFullscreen,\n startDragging,\n };\n}\n",
|
|
10
|
+
"type": "registry:hook"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-tiptap-editor",
|
|
3
|
+
"title": "createTiptapEditor",
|
|
4
|
+
"description": "Fine-grained reactive SolidJS primitive for managing Tiptap rich text editor instances, signals, and formatting commands.",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@tiptap/core",
|
|
8
|
+
"@tiptap/starter-kit",
|
|
9
|
+
"@tiptap/extension-link",
|
|
10
|
+
"@tiptap/extension-image",
|
|
11
|
+
"@tiptap/extension-placeholder",
|
|
12
|
+
"@tiptap/extension-task-list",
|
|
13
|
+
"@tiptap/extension-task-item",
|
|
14
|
+
"@tiptap/extension-table",
|
|
15
|
+
"@tiptap/extension-table-row",
|
|
16
|
+
"@tiptap/extension-table-cell",
|
|
17
|
+
"@tiptap/extension-table-header",
|
|
18
|
+
"@tiptap/extension-underline",
|
|
19
|
+
"@tiptap/extension-text-align",
|
|
20
|
+
"@tiptap/extension-highlight",
|
|
21
|
+
"@tiptap/extension-character-count",
|
|
22
|
+
"@tiptap/extension-subscript",
|
|
23
|
+
"@tiptap/extension-superscript",
|
|
24
|
+
"@tiptap/extension-typography",
|
|
25
|
+
"tiptap-markdown"
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
{
|
|
29
|
+
"path": "hooks/create-tiptap-editor.ts",
|
|
30
|
+
"content": "import {\n createSignal,\n onMount,\n onCleanup,\n type Accessor,\n} from \"solid-js\";\nimport { Editor, type EditorOptions, type Extension } from \"@tiptap/core\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport Placeholder from \"@tiptap/extension-placeholder\";\nimport Link from \"@tiptap/extension-link\";\nimport Image from \"@tiptap/extension-image\";\nimport TaskList from \"@tiptap/extension-task-list\";\nimport TaskItem from \"@tiptap/extension-task-item\";\nimport { Table, TableRow, TableCell, TableHeader } from \"@tiptap/extension-table\";\nimport Underline from \"@tiptap/extension-underline\";\nimport TextAlign from \"@tiptap/extension-text-align\";\nimport Highlight from \"@tiptap/extension-highlight\";\nimport CharacterCount from \"@tiptap/extension-character-count\";\nimport Subscript from \"@tiptap/extension-subscript\";\nimport Superscript from \"@tiptap/extension-superscript\";\nimport Typography from \"@tiptap/extension-typography\";\nimport { Markdown } from \"tiptap-markdown\";\n\nexport interface CreateTiptapEditorOptions\n extends Partial<Omit<EditorOptions, \"element\">> {\n placeholder?: string;\n characterLimit?: number;\n}\n\nexport interface CreateTiptapEditorReturn {\n editor: Accessor<Editor | null>;\n html: Accessor<string>;\n text: Accessor<string>;\n markdown: Accessor<string>;\n isEmpty: Accessor<boolean>;\n characterCount: Accessor<number>;\n wordCount: Accessor<number>;\n canUndo: Accessor<boolean>;\n canRedo: Accessor<boolean>;\n mount: (element: HTMLElement) => void;\n destroy: () => void;\n isActive: (name: string | Record<string, any>, attributes?: Record<string, any>) => boolean;\n toggleBold: () => void;\n toggleItalic: () => void;\n toggleUnderline: () => void;\n toggleStrike: () => void;\n toggleCode: () => void;\n toggleHighlight: (color?: string) => void;\n setHeading: (level: 1 | 2 | 3 | 4 | 5 | 6) => void;\n setParagraph: () => void;\n toggleBulletList: () => void;\n toggleOrderedList: () => void;\n toggleTaskList: () => void;\n toggleBlockquote: () => void;\n toggleCodeBlock: () => void;\n setTextAlign: (alignment: \"left\" | \"center\" | \"right\" | \"justify\") => void;\n setLink: (options: string | { href: string; target?: string }) => void;\n unsetLink: () => void;\n setImage: (options: string | { src: string; alt?: string; title?: string }) => void;\n insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => void;\n insertHorizontalRule: () => void;\n clearContent: () => void;\n setContent: (content: string) => void;\n setEditable: (editable: boolean) => void;\n undo: () => void;\n redo: () => void;\n}\n\nexport function createTiptapEditor(\n options: CreateTiptapEditorOptions = {}\n): CreateTiptapEditorReturn {\n const [editorInstance, setEditorInstance] = createSignal<Editor | null>(null);\n const [html, setHtml] = createSignal<string>(\"\");\n const [text, setText] = createSignal<string>(\"\");\n const [markdown, setMarkdown] = createSignal<string>(\"\");\n const [isEmpty, setIsEmpty] = createSignal<boolean>(true);\n const [characterCount, setCharacterCount] = createSignal<number>(0);\n const [wordCount, setWordCount] = createSignal<number>(0);\n const [canUndo, setCanUndo] = createSignal<boolean>(false);\n const [canRedo, setCanRedo] = createSignal<boolean>(false);\n const [transactionVersion, setTransactionVersion] = createSignal<number>(0);\n\n let targetElement: HTMLElement | null = null;\n\n const updateStats = (ed: Editor) => {\n const currentHtml = ed.getHTML();\n const currentText = ed.getText();\n const currentMd = (ed.storage as any).markdown?.getMarkdown?.() || \"\";\n setHtml(currentHtml);\n setText(currentText);\n setMarkdown(currentMd);\n setIsEmpty(ed.isEmpty);\n setCharacterCount(ed.storage.characterCount?.characters?.() ?? currentText.length);\n setWordCount(ed.storage.characterCount?.words?.() ?? currentText.split(/\\s+/).filter(Boolean).length);\n setCanUndo(ed.can().undo());\n setCanRedo(ed.can().redo());\n setTransactionVersion((v) => v + 1);\n };\n\n const defaultExtensions: Extension[] = [\n StarterKit.configure({\n heading: {\n levels: [1, 2, 3, 4, 5, 6],\n HTMLAttributes: { class: \"font-heading tracking-tight\" },\n },\n bulletList: {\n HTMLAttributes: { class: \"list-disc pl-6 my-3 space-y-1\" },\n },\n orderedList: {\n HTMLAttributes: { class: \"list-decimal pl-6 my-3 space-y-1\" },\n },\n codeBlock: {\n HTMLAttributes: {\n class: \"rounded-lg bg-muted p-4 font-mono text-xs border border-border my-3 overflow-x-auto text-foreground\",\n },\n },\n code: {\n HTMLAttributes: {\n class: \"rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground font-semibold border border-border/50\",\n },\n },\n blockquote: {\n HTMLAttributes: {\n class: \"border-l-4 border-primary pl-4 italic text-muted-foreground my-3\",\n },\n },\n }) as Extension,\n Placeholder.configure({\n placeholder: options.placeholder || \"Write something rich or type '/' for commands...\",\n emptyEditorClass: \"is-editor-empty\",\n }) as Extension,\n Underline as Extension,\n Subscript as Extension,\n Superscript as Extension,\n Typography as Extension,\n Highlight.configure({\n multicolor: true,\n HTMLAttributes: { class: \"rounded px-1 py-0.5\" },\n }) as Extension,\n TextAlign.configure({ types: [\"heading\", \"paragraph\"] }) as Extension,\n Link.configure({\n openOnClick: false,\n HTMLAttributes: {\n class: \"text-primary font-medium underline underline-offset-4 hover:opacity-80 transition-opacity cursor-pointer\",\n },\n }) as Extension,\n Image.configure({\n HTMLAttributes: {\n class: \"rounded-lg max-w-full h-auto my-4 border border-border shadow-xs\",\n },\n }) as Extension,\n TaskList.configure({\n HTMLAttributes: { class: \"list-none pl-0 my-3 space-y-2\" },\n }) as Extension,\n TaskItem.configure({\n nested: true,\n HTMLAttributes: { class: \"flex items-start gap-2.5 my-1\" },\n }) as Extension,\n Table.configure({\n resizable: true,\n HTMLAttributes: {\n class: \"border-collapse table-auto w-full my-4 border border-border rounded-lg overflow-hidden text-sm\",\n },\n }) as Extension,\n TableRow as Extension,\n TableHeader.configure({\n HTMLAttributes: { class: \"border border-border bg-muted/70 px-3 py-2 font-semibold text-left text-xs\" },\n }) as Extension,\n TableCell.configure({\n HTMLAttributes: { class: \"border border-border px-3 py-2 text-xs\" },\n }) as Extension,\n CharacterCount.configure({ limit: options.characterLimit }) as Extension,\n Markdown.configure({\n html: true,\n tightLists: true,\n bulletListMarker: \"-\",\n linkify: true,\n breaks: false,\n transformPastedText: true,\n transformCopiedText: true,\n }) as Extension,\n ];\n\n const initEditor = (el: HTMLElement) => {\n if (typeof window === \"undefined\") return;\n\n const extensions = [...defaultExtensions, ...(options.extensions || [])];\n\n const editor = new Editor({\n element: el,\n content: options.content,\n editable: options.editable ?? true,\n autofocus: options.autofocus ?? false,\n extensions,\n editorProps: {\n attributes: {\n class:\n \"focus:outline-hidden min-h-[220px] w-full max-w-none text-foreground leading-relaxed text-sm \" +\n \"[&_h1]:text-3xl [&_h1]:font-bold [&_h1]:tracking-tight [&_h1]:mt-6 [&_h1]:mb-3 \" +\n \"[&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:tracking-tight [&_h2]:mt-5 [&_h2]:mb-2.5 \" +\n \"[&_h3]:text-xl [&_h3]:font-semibold [&_h3]:mt-4 [&_h3]:mb-2 \" +\n \"[&_h4]:text-lg [&_h4]:font-semibold [&_h4]:mt-3 [&_h4]:mb-1.5 \" +\n \"[&_p]:my-2 [&_p]:leading-7 \" +\n \"[&_ul]:list-disc [&_ul]:pl-6 [&_ul]:my-3 [&_ul]:space-y-1 \" +\n \"[&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:my-3 [&_ol]:space-y-1 \" +\n \"[&_li]:my-0.5 \" +\n \"[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-0 [&_ul[data-type=taskList]]:my-3 [&_ul[data-type=taskList]]:space-y-2 \" +\n \"[&_ul[data-type=taskList]_li]:flex [&_ul[data-type=taskList]_li]:items-start [&_ul[data-type=taskList]_li]:gap-2.5 \" +\n \"[&_ul[data-type=taskList]_input[type=checkbox]]:mt-1 [&_ul[data-type=taskList]_input[type=checkbox]]:h-4 [&_ul[data-type=taskList]_input[type=checkbox]]:w-4 [&_ul[data-type=taskList]_input[type=checkbox]]:rounded [&_ul[data-type=taskList]_input[type=checkbox]]:border-border [&_ul[data-type=taskList]_input[type=checkbox]]:accent-primary [&_ul[data-type=taskList]_input[type=checkbox]]:cursor-pointer \" +\n \"[&_mark]:text-foreground [&_mark]:dark:text-background [&_mark]:font-medium [&_mark]:rounded-xs [&_mark]:px-1 [&_mark]:py-0.5 \" +\n \"[&_.is-editor-empty:first-child::before]:content-[attr(data-placeholder)] [&_.is-editor-empty:first-child::before]:text-muted-foreground [&_.is-editor-empty:first-child::before]:float-left [&_.is-editor-empty:first-child::before]:pointer-events-none [&_.is-editor-empty:first-child::before]:h-0\",\n },\n ...options.editorProps,\n },\n onUpdate: (props) => {\n updateStats(props.editor);\n options.onUpdate?.(props);\n },\n onSelectionUpdate: (props) => {\n updateStats(props.editor);\n options.onSelectionUpdate?.(props);\n },\n onTransaction: (props) => {\n updateStats(props.editor);\n options.onTransaction?.(props);\n },\n onFocus: (args) => options.onFocus?.(args),\n onBlur: (args) => options.onBlur?.(args),\n });\n\n setEditorInstance(editor);\n updateStats(editor);\n };\n\n const mount = (element: HTMLElement) => {\n targetElement = element;\n initEditor(element);\n };\n\n const destroy = () => {\n editorInstance()?.destroy();\n setEditorInstance(null);\n };\n\n onCleanup(() => {\n destroy();\n });\n\n const isActive = (name: string | Record<string, any>, attributes?: Record<string, any>) => {\n transactionVersion();\n const ed = editorInstance();\n if (!ed) return false;\n if (typeof name === \"string\") {\n return ed.isActive(name, attributes);\n }\n return ed.isActive(name);\n };\n\n return {\n editor: editorInstance,\n html,\n text,\n markdown,\n isEmpty,\n characterCount,\n wordCount,\n canUndo,\n canRedo,\n mount,\n destroy,\n isActive,\n toggleBold: () => editorInstance()?.chain().focus().toggleBold().run(),\n toggleItalic: () => editorInstance()?.chain().focus().toggleItalic().run(),\n toggleUnderline: () => editorInstance()?.chain().focus().toggleUnderline().run(),\n toggleStrike: () => editorInstance()?.chain().focus().toggleStrike().run(),\n toggleCode: () => editorInstance()?.chain().focus().toggleCode().run(),\n toggleHighlight: (color) => {\n const ed = editorInstance();\n if (!ed) return;\n if (color) {\n ed.chain().focus().toggleHighlight({ color }).run();\n } else {\n ed.chain().focus().toggleHighlight().run();\n }\n },\n setHeading: (level) => editorInstance()?.chain().focus().toggleHeading({ level }).run(),\n setParagraph: () => editorInstance()?.chain().focus().setParagraph().run(),\n toggleBulletList: () => editorInstance()?.chain().focus().toggleBulletList().run(),\n toggleOrderedList: () => editorInstance()?.chain().focus().toggleOrderedList().run(),\n toggleTaskList: () => editorInstance()?.chain().focus().toggleTaskList().run(),\n toggleBlockquote: () => editorInstance()?.chain().focus().toggleBlockquote().run(),\n toggleCodeBlock: () => editorInstance()?.chain().focus().toggleCodeBlock().run(),\n setTextAlign: (alignment) => editorInstance()?.chain().focus().setTextAlign(alignment).run(),\n setLink: (opts) => {\n const ed = editorInstance();\n if (!ed) return;\n if (typeof opts === \"string\") {\n ed.chain().focus().setLink({ href: opts, target: \"_blank\" }).run();\n } else {\n ed.chain().focus().setLink({ href: opts.href, target: opts.target ?? \"_blank\" }).run();\n }\n },\n unsetLink: () => editorInstance()?.chain().focus().unsetLink().run(),\n setImage: (opts) => {\n const ed = editorInstance();\n if (!ed) return;\n if (typeof opts === \"string\") {\n ed.chain().focus().setImage({ src: opts }).run();\n } else {\n ed.chain().focus().setImage(opts).run();\n }\n },\n insertTable: (opts) =>\n editorInstance()?.chain().focus().insertTable({\n rows: opts?.rows ?? 3,\n cols: opts?.cols ?? 3,\n withHeaderRow: opts?.withHeaderRow ?? true,\n }).run(),\n insertHorizontalRule: () => editorInstance()?.chain().focus().setHorizontalRule().run(),\n clearContent: () => editorInstance()?.chain().focus().clearContent().run(),\n setContent: (c) => editorInstance()?.commands.setContent(c),\n setEditable: (editable: boolean) => editorInstance()?.setEditable(editable),\n undo: () => editorInstance()?.chain().focus().undo().run(),\n redo: () => editorInstance()?.chain().focus().redo().run(),\n };\n}\n",
|
|
31
|
+
"type": "registry:hook"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
package/registry/dialog.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "ui/dialog.tsx",
|
|
17
|
-
"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 { ScrollArea } from \"
|
|
17
|
+
"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 { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\nexport type DialogRootProps = DialogPrimitive.DialogRootProps;\n\n/**\n * Root Dialog component managing modal open state.\n */\nexport const Dialog: Component<DialogRootProps> = (props) => {\n return <DialogPrimitive.Root {...props} />;\n};\n\nexport const DialogTrigger = DialogPrimitive.Trigger;\n\nexport type DialogOverlayProps<T extends ValidComponent = \"div\"> =\n DialogPrimitive.DialogOverlayProps<T> & {\n /** Whether to apply background blur effect (default: true) */\n blur?: boolean;\n class?: string;\n };\n\n/**\n * Backdrop overlay wrapper with optional backdrop blur styling.\n */\nexport const DialogOverlay = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DialogOverlayProps<T>>\n) => {\n const [local, rest] = splitProps(props as DialogOverlayProps, [\"class\", \"blur\"]);\n\n return (\n <DialogPrimitive.Overlay\n class={cn(\n \"fixed inset-0 z-50 bg-black/80 data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\",\n local.blur !== false && \"backdrop-blur-sm\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type DialogContentProps<T extends ValidComponent = \"div\"> =\n DialogPrimitive.DialogContentProps<T> & {\n /** Whether to show the top-right close (X) button (default: true) */\n showCloseButton?: boolean;\n /** Whether clicking outside closes the dialog (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 dialog popup window housing header, content, and footer.\n */\nexport const DialogContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DialogContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as DialogContentProps, [\n \"class\",\n \"children\",\n \"showCloseButton\",\n \"closeOnOutsideClick\",\n \"blur\",\n \"onPointerDownOutside\",\n \"onInteractOutside\",\n ]);\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 {/* Forward blur prop to DialogOverlay */}\n <DialogOverlay blur={local.blur} />\n <DialogPrimitive.Content\n onPointerDownOutside={handlePointerDownOutside}\n onInteractOutside={handleInteractOutside}\n class={cn(\n \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border border-border bg-popover shadow-lg duration-200 data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-closed:zoom-out-95 data-expanded:zoom-in-95 data-[closed]:slide-out-to-left-1/2 data-[closed]:slide-out-to-top-[48%] data-[expanded]:slide-in-from-left-1/2 data-[expanded]:slide-in-from-top-[48%] sm:rounded-lg text-card-foreground overflow-hidden max-h-[80vh]\",\n local.class\n )}\n {...(rest as any)}\n >\n <ScrollArea class=\"max-h-[80vh] 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 data-expanded:bg-accent data-expanded:text-muted-foreground cursor-pointer z-50\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n <span class=\"sr-only\">Close</span>\n </DialogPrimitive.CloseButton>\n </Show>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n};\n\nexport interface DialogHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Dialog top header container.\n */\nexport const DialogHeader: Component<DialogHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"flex flex-col space-y-1.5 text-center sm:text-left\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface DialogFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Dialog bottom action buttons area.\n */\nexport const DialogFooter: Component<DialogFooterProps> = (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 DialogTitleProps<T extends ValidComponent = \"h2\"> =\n DialogPrimitive.DialogTitleProps<T> & {\n class?: string;\n };\n\n/**\n * Dialog title heading.\n */\nexport const DialogTitle = <T extends ValidComponent = \"h2\">(\n props: PolymorphicProps<T, DialogTitleProps<T>>\n) => {\n const [local, rest] = splitProps(props as DialogTitleProps, [\"class\"]);\n\n return (\n <DialogPrimitive.Title\n class={cn(\"text-lg font-semibold leading-none tracking-tight text-foreground\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type DialogDescriptionProps<T extends ValidComponent = \"p\"> =\n DialogPrimitive.DialogDescriptionProps<T> & {\n class?: string;\n };\n\n/**\n * Dialog subtitle or description text.\n */\nexport const DialogDescription = <T extends ValidComponent = \"p\">(\n props: PolymorphicProps<T, DialogDescriptionProps<T>>\n) => {\n const [local, rest] = splitProps(props as DialogDescriptionProps, [\"class\"]);\n\n return (\n <DialogPrimitive.Description\n class={cn(\"text-sm text-muted-foreground\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport const DialogClose = DialogPrimitive.CloseButton;",
|
|
18
18
|
"type": "registry:ui"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
"@kobalte/core"
|
|
10
10
|
],
|
|
11
11
|
"registryDependencies": [
|
|
12
|
-
"scroll-area"
|
|
12
|
+
"scroll-area",
|
|
13
|
+
"create-click-outside"
|
|
13
14
|
],
|
|
14
15
|
"files": [
|
|
15
16
|
{
|
|
16
17
|
"path": "ui/dropdown-menu.tsx",
|
|
17
|
-
"content": "import { splitProps, type Component, type JSX, type ValidComponent } from \"solid-js\";\nimport * as DropdownMenuPrimitive from \"@kobalte/core/dropdown-menu\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"@nikala-ui/hooks\";\nimport { ScrollArea } from \"./scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\nexport type DropdownMenuRootProps = Omit<\n DropdownMenuPrimitive.DropdownMenuRootProps,\n \"placement\"\n> & {\n placement?:\n | \"top\"\n | \"top-start\"\n | \"top-end\"\n | \"right\"\n | \"right-start\"\n | \"right-end\"\n | \"bottom\"\n | \"bottom-start\"\n | \"bottom-end\"\n | \"left\"\n | \"left-start\"\n | \"left-end\";\n};\n\nexport const DropdownMenu: Component<DropdownMenuRootProps> = (props) => {\n return <DropdownMenuPrimitive.Root {...props} />;\n};\n\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\nexport const DropdownMenuSub = DropdownMenuPrimitive.Sub;\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nexport type DropdownMenuSubTriggerProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuSubTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n inset?: boolean;\n };\n\nexport const DropdownMenuSubTrigger = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuSubTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSubTriggerProps, [\"class\", \"children\", \"inset\"]);\n\n return (\n <DropdownMenuPrimitive.SubTrigger\n class={cn(\n \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent text-foreground\",\n local.inset && \"pl-8\",\n local.class\n )}\n {...(rest as any)}\n >\n {local.children}\n <svg class=\"ml-auto h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </DropdownMenuPrimitive.SubTrigger>\n );\n};\n\nexport type DropdownMenuSubContentProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuSubContentProps<T> & {\n class?: string;\n };\n\nexport const DropdownMenuSubContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuSubContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSubContentProps, [\"class\"]);\n\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.SubContent\n class={cn(\n \"z-50 min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\",\n local.class\n )}\n {...(rest as any)}\n />\n </DropdownMenuPrimitive.Portal>\n );\n};\n\nexport type DropdownMenuContentProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuContentProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const DropdownMenuContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuContentProps, [\"class\", \"children\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof (props as any).onInteractOutside === \"function\") {\n (props as any).onInteractOutside(e);\n }\n },\n });\n\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={(el) => {\n contentRef = el;\n if (typeof (props as any).ref === \"function\") (props as any).ref(el);\n }}\n {...(rest as any)}\n sideOffset={(rest as any).sideOffset ?? 8}\n class={cn(\n \"z-50 min-w-32 overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 max-h-72 flex flex-col mt-1\",\n local.class\n )}\n >\n <ScrollArea class=\"max-h-72 w-full rounded-[inherit]\">\n <div class=\"p-1\">\n {local.children}\n </div>\n </ScrollArea>\n </DropdownMenuPrimitive.Content>\n </DropdownMenuPrimitive.Portal>\n );\n};\n\nexport type DropdownMenuItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuItemProps<T> & {\n class?: string;\n inset?: boolean;\n };\n\nexport const DropdownMenuItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuItemProps, [\"class\", \"inset\"]);\n\n return (\n <DropdownMenuPrimitive.Item\n class={cn(\n \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 text-foreground\",\n local.inset && \"pl-8\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type DropdownMenuCheckboxItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuCheckboxItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n checked?: boolean;\n };\n\nexport const DropdownMenuCheckboxItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuCheckboxItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuCheckboxItemProps, [\"class\", \"children\", \"checked\"]);\n\n return (\n <DropdownMenuPrimitive.CheckboxItem\n checked={local.checked}\n class={cn(\n \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 text-foreground\",\n local.class\n )}\n {...(rest as any)}\n >\n <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n};\n\nexport type DropdownMenuRadioItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuRadioItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const DropdownMenuRadioItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuRadioItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuRadioItemProps, [\"class\", \"children\"]);\n\n return (\n <DropdownMenuPrimitive.RadioItem\n class={cn(\n \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 text-foreground\",\n local.class\n )}\n {...(rest as any)}\n >\n <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <svg class=\"h-2 w-2 fill-current\" viewBox=\"0 0 8 8\">\n <circle cx=\"4\" cy=\"4\" r=\"3\" />\n </svg>\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </DropdownMenuPrimitive.RadioItem>\n );\n};\n\nexport interface DropdownMenuLabelProps {\n class?: string;\n inset?: boolean;\n children?: JSX.Element;\n}\n\nexport const DropdownMenuLabel: Component<DropdownMenuLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"inset\", \"children\"]);\n\n return (\n <DropdownMenuPrimitive.GroupLabel\n class={cn(\"px-2 py-1.5 text-sm font-semibold text-foreground\", local.inset && \"pl-8\", local.class)}\n {...rest}\n >\n {local.children}\n </DropdownMenuPrimitive.GroupLabel>\n );\n};\n\nexport const DropdownMenuSeparator: Component<{ class?: string }> = (props) => {\n return (\n <DropdownMenuPrimitive.Separator\n class={cn(\"-mx-1 my-1 h-px bg-border\", props.class)}\n />\n );\n};\n\nexport interface DropdownMenuShortcutProps {\n class?: string;\n children?: JSX.Element;\n}\n\nexport const DropdownMenuShortcut: Component<DropdownMenuShortcutProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <span class={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", local.class)} {...rest}>\n {local.children}\n </span>\n );\n};",
|
|
18
|
+
"content": "import { splitProps, type Component, type JSX, type ValidComponent, createContext, useContext } from \"solid-js\";\nimport * as DropdownMenuPrimitive from \"@kobalte/core/dropdown-menu\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"@/hooks/create-click-outside\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\nconst DropdownGroupContext = createContext<boolean>(false);\n\nexport type DropdownMenuRootProps = Omit<\n DropdownMenuPrimitive.DropdownMenuRootProps,\n \"placement\"\n> & {\n placement?:\n | \"top\"\n | \"top-start\"\n | \"top-end\"\n | \"right\"\n | \"right-start\"\n | \"right-end\"\n | \"bottom\"\n | \"bottom-start\"\n | \"bottom-end\"\n | \"left\"\n | \"left-start\"\n | \"left-end\";\n};\n\nexport const DropdownMenu: Component<DropdownMenuRootProps> = (props) => {\n return <DropdownMenuPrimitive.Root {...props} />;\n};\n\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nexport const DropdownMenuGroup: Component<DropdownMenuPrimitive.DropdownMenuGroupProps> = (props) => {\n return (\n <DropdownGroupContext.Provider value={true}>\n <DropdownMenuPrimitive.Group {...props} />\n </DropdownGroupContext.Provider>\n );\n};\n\nexport const DropdownMenuSub = DropdownMenuPrimitive.Sub;\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nexport type DropdownMenuSubTriggerProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuSubTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n inset?: boolean;\n };\n\nexport const DropdownMenuSubTrigger = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuSubTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSubTriggerProps, [\"class\", \"children\", \"inset\"]);\n\n return (\n <DropdownMenuPrimitive.SubTrigger\n class={cn(\n \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent text-foreground\",\n local.inset && \"pl-8\",\n local.class\n )}\n {...(rest as any)}\n >\n {local.children}\n <svg class=\"ml-auto h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </DropdownMenuPrimitive.SubTrigger>\n );\n};\n\nexport type DropdownMenuSubContentProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuSubContentProps<T> & {\n class?: string;\n };\n\nexport const DropdownMenuSubContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuSubContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSubContentProps, [\"class\"]);\n\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.SubContent\n class={cn(\n \"z-50 min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\",\n local.class\n )}\n {...(rest as any)}\n />\n </DropdownMenuPrimitive.Portal>\n );\n};\n\nexport type DropdownMenuContentProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuContentProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const DropdownMenuContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuContentProps, [\"class\", \"children\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof (props as any).onInteractOutside === \"function\") {\n (props as any).onInteractOutside(e);\n }\n },\n });\n\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={(el) => {\n contentRef = el;\n if (typeof (props as any).ref === \"function\") (props as any).ref(el);\n }}\n {...(rest as any)}\n sideOffset={(rest as any).sideOffset ?? 8}\n class={cn(\n \"z-50 min-w-32 overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 max-h-72 flex flex-col mt-1\",\n local.class\n )}\n >\n <ScrollArea class=\"max-h-72 w-full\">\n <div class=\"p-1\">\n {local.children}\n </div>\n </ScrollArea>\n </DropdownMenuPrimitive.Content>\n </DropdownMenuPrimitive.Portal>\n );\n};\n\nexport type DropdownMenuItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuItemProps<T> & {\n class?: string;\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n };\n\nexport const DropdownMenuItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuItemProps, [\n \"class\",\n \"inset\",\n \"variant\",\n ]);\n\n return (\n <DropdownMenuPrimitive.Item\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n local.inset && \"pl-8\",\n local.variant === \"destructive\" &&\n \"text-destructive focus:bg-destructive/15 focus:text-destructive\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type DropdownMenuCheckboxItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuCheckboxItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const DropdownMenuCheckboxItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuCheckboxItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuCheckboxItemProps, [\"class\", \"children\"]);\n\n return (\n <DropdownMenuPrimitive.CheckboxItem\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n local.class\n )}\n {...(rest as any)}\n >\n <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <svg class=\"h-4 w-4 fill-none stroke-current stroke-2\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n};\n\nexport type DropdownMenuRadioItemProps<T extends ValidComponent = \"div\"> =\n DropdownMenuPrimitive.DropdownMenuRadioItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const DropdownMenuRadioItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, DropdownMenuRadioItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuRadioItemProps, [\"class\", \"children\"]);\n\n return (\n <DropdownMenuPrimitive.RadioItem\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n local.class\n )}\n {...(rest as any)}\n >\n <span class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <span class=\"h-2 w-2 rounded-lg bg-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </DropdownMenuPrimitive.RadioItem>\n );\n};\n\nexport const DropdownMenuGroupLabel = DropdownMenuPrimitive.GroupLabel;\n\nexport interface DropdownMenuLabelProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n inset?: boolean;\n children?: JSX.Element;\n}\n\nexport const DropdownMenuLabel: Component<DropdownMenuLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"inset\", \"children\"]);\n const isInsideGroup = useContext(DropdownGroupContext);\n\n const classes = () =>\n cn(\n \"px-2 py-1.5 text-sm font-semibold text-foreground\",\n local.inset && \"pl-8\",\n local.class\n );\n\n return (\n <>\n {isInsideGroup ? (\n <DropdownMenuPrimitive.GroupLabel class={classes()} {...(rest as any)}>\n {local.children}\n </DropdownMenuPrimitive.GroupLabel>\n ) : (\n <div class={classes()} {...rest}>\n {local.children}\n </div>\n )}\n </>\n );\n};\n\nexport type DropdownMenuSeparatorProps<T extends ValidComponent = \"hr\"> =\n DropdownMenuPrimitive.DropdownMenuSeparatorProps<T> & {\n class?: string;\n };\n\nexport const DropdownMenuSeparator = <T extends ValidComponent = \"hr\">(\n props: PolymorphicProps<T, DropdownMenuSeparatorProps<T>>\n) => {\n const [local, rest] = splitProps(props as DropdownMenuSeparatorProps, [\"class\"]);\n\n return (\n <DropdownMenuPrimitive.Separator\n class={cn(\"-mx-1 my-1 h-px bg-border\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport interface DropdownMenuShortcutProps extends JSX.HTMLAttributes<HTMLSpanElement> {\n class?: string;\n}\n\nexport const DropdownMenuShortcut: Component<DropdownMenuShortcutProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <span\n class={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};",
|
|
18
19
|
"type": "registry:ui"
|
|
19
20
|
}
|
|
20
21
|
]
|
package/registry/field.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
{
|
|
15
15
|
"path": "ui/field.tsx",
|
|
16
|
-
"content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\nimport { Label, type LabelProps } from \"
|
|
16
|
+
"content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\nimport { Label, type LabelProps } from \"@/components/ui/label\";\n\nexport interface FieldProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/** A consistent layout wrapper for labels, controls, descriptions, and errors. */\nexport const Field: Component<FieldProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"grid w-full gap-1.5\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n\nexport interface FieldLabelProps extends JSX.LabelHTMLAttributes<HTMLLabelElement>, LabelProps {\n class?: string;\n for?: string;\n children?: JSX.Element;\n}\n\nexport const FieldLabel: Component<FieldLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <Label class={cn(\"text-sm font-medium\", local.class)} {...rest}>\n {local.children}\n </Label>\n );\n};\n\nexport interface FieldDescriptionProps\n extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const FieldDescription: Component<FieldDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p\n class={cn(\"text-sm text-muted-foreground\", local.class)}\n {...rest}\n >\n {local.children}\n </p>\n );\n};\n\nexport interface FieldErrorProps\n extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const FieldError: Component<FieldErrorProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p\n role=\"alert\"\n class={cn(\"text-sm font-medium text-destructive\", local.class)}\n {...rest}\n >\n {local.children}\n </p>\n );\n};\n",
|
|
17
17
|
"type": "registry:ui"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "file-tree",
|
|
3
|
+
"title": "File Tree",
|
|
4
|
+
"description": "An interactive hierarchical directory file tree component built on Collapsible.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"lucide-solid"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"collapsible"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "ui/file-tree.tsx",
|
|
17
|
+
"content": "import {\n createSignal,\n splitProps,\n Show,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport {\n Folder,\n FolderOpen,\n File,\n FileCode,\n FileJson,\n FileText,\n ChevronRight,\n} from \"lucide-solid\";\nimport {\n Collapsible,\n CollapsibleTrigger,\n CollapsibleContent,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface FileTreeProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Root FileTree container for displaying project directory hierarchies.\n */\nexport const FileTree: ParentComponent<FileTreeProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n return (\n <div\n class={cn(\n \"my-4 w-full rounded-lg border border-border bg-card/60 p-3 font-mono text-sm shadow-2xs select-none\",\n local.class\n )}\n {...rest}\n >\n <div class=\"space-y-0.5\">{local.children}</div>\n </div>\n );\n};\n\nexport interface FileTreeFolderProps {\n /** Folder name */\n name: string;\n /** Initial open state of the folder (default: true) */\n defaultOpen?: boolean;\n /** Controlled open state */\n open?: boolean;\n /** Callback fired when open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Custom folder icon override */\n icon?: JSX.Element;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Collapsible folder node in the FileTree.\n */\nexport const FileTreeFolder: ParentComponent<FileTreeFolderProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"name\",\n \"defaultOpen\",\n \"open\",\n \"onOpenChange\",\n \"icon\",\n \"class\",\n \"children\",\n ]);\n\n const [isOpen, setIsOpen] = createSignal(local.defaultOpen ?? true);\n\n const openState = () => (local.open !== undefined ? local.open : isOpen());\n\n const handleToggle = (next: boolean) => {\n if (local.open === undefined) {\n setIsOpen(next);\n }\n local.onOpenChange?.(next);\n };\n\n return (\n <Collapsible\n open={openState()}\n onOpenChange={handleToggle}\n class={cn(\"w-full\", local.class)}\n {...rest}\n >\n <CollapsibleTrigger class=\"group flex w-full items-center justify-start gap-1.5 rounded-md px-1.5 py-1 text-xs text-foreground transition-colors hover:bg-muted/70 cursor-pointer\">\n <ChevronRight\n class={cn(\n \"size-3.5 shrink-0 text-muted-foreground transition-transform duration-200\",\n openState() && \"rotate-90 text-foreground\"\n )}\n />\n <Show\n when={local.icon}\n fallback={\n <Show\n when={openState()}\n fallback={<Folder class=\"size-3.5 shrink-0 text-amber-500/90 dark:text-amber-400\" />}\n >\n <FolderOpen class=\"size-3.5 shrink-0 text-amber-500 dark:text-amber-400\" />\n </Show>\n }\n >\n {local.icon}\n </Show>\n <span class=\"font-medium text-foreground tracking-tight\">{local.name}</span>\n </CollapsibleTrigger>\n\n <CollapsibleContent class=\"border-l border-border/60 ml-3.5 pl-3 pt-0.5 space-y-0.5\">\n {local.children}\n </CollapsibleContent>\n </Collapsible>\n );\n};\n\nexport interface FileTreeFileProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** File name */\n name: string;\n /** Custom file icon override */\n icon?: JSX.Element;\n class?: string;\n}\n\n/**\n * Returns a smart default Lucide icon based on file extension.\n */\nfunction getFileIcon(filename: string): JSX.Element {\n const ext = filename.split(\".\").pop()?.toLowerCase();\n switch (ext) {\n case \"tsx\":\n case \"jsx\":\n case \"ts\":\n case \"js\":\n case \"mjs\":\n case \"rs\":\n case \"py\":\n case \"go\":\n return <FileCode class=\"size-3.5 shrink-0 text-sky-500 dark:text-sky-400\" />;\n case \"json\":\n return <FileJson class=\"size-3.5 shrink-0 text-amber-500 dark:text-amber-400\" />;\n case \"md\":\n case \"mdx\":\n case \"txt\":\n return <FileText class=\"size-3.5 shrink-0 text-emerald-500 dark:text-emerald-400\" />;\n default:\n return <File class=\"size-3.5 shrink-0 text-muted-foreground\" />;\n }\n}\n\n/**\n * Leaf file item inside a FileTree.\n */\nexport const FileTreeFile: ParentComponent<FileTreeFileProps> = (props) => {\n const [local, rest] = splitProps(props, [\"name\", \"icon\", \"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"flex w-full items-center justify-start gap-1.5 rounded-md px-1.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground cursor-default\",\n local.class\n )}\n {...rest}\n >\n <span class=\"size-3.5 shrink-0\" />\n <Show when={local.icon} fallback={getFileIcon(local.name)}>\n {local.icon}\n </Show>\n <span class=\"tracking-tight\">{local.name}</span>\n <Show when={local.children}>\n <div class=\"ml-auto\">{local.children}</div>\n </Show>\n </div>\n );\n};\n",
|
|
18
|
+
"type": "registry:ui"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/registry/footer.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "ui/footer.tsx",
|
|
14
|
-
"content": "import { splitProps, type JSX, type ParentComponent } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- Root Footer --- */\nexport const footerVariants = cva(\"w-full transition-colors\", {\n variants: {\n variant: {\n default: \"bg-background border-t border-border/60 text-foreground\",\n muted: \"bg-muted/40 border-t border-border text-foreground\",\n bordered: \"bg-background border-t-2 border-border text-foreground\",\n floating: \"my-8 rounded-lg border border-border/80 bg-card text-card-foreground shadow-xs\",\n transparent: \"bg-transparent border-transparent text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n});\n\nexport interface FooterProps\n extends JSX.HTMLAttributes<HTMLElement>,\n VariantProps<typeof footerVariants> {\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"full\";\n}\n\nexport const Footer: ParentComponent<FooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"maxWidth\", \"children\"]);\n\n const maxWidthClass = () => {\n switch (local.maxWidth) {\n case \"sm\":\n return \"max-w-screen-sm\";\n case \"md\":\n return \"max-w-screen-md\";\n case \"lg\":\n return \"max-w-screen-lg\";\n case \"xl\":\n return \"max-w-screen-xl\";\n case \"full\":\n return \"max-w-full\";\n case \"2xl\":\n default:\n return \"max-w-screen-2xl\";\n }\n };\n\n return (\n <footer\n class={cn(\n footerVariants({ variant: local.variant }),\n local.variant === \"floating\" && cn(\"mx-auto\", maxWidthClass()),\n local.class\n )}\n {...rest}\n >\n <div class={cn(\"w-full\", local.variant !== \"floating\" && cn(\"mx-auto\", maxWidthClass()))}>\n {local.children}\n </div>\n </footer>\n );\n};\n\n/* --- Footer Container --- */\nexport interface FooterContainerProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterContainer: ParentComponent<FooterContainerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"px-4 sm:px-6 lg:px-8 py-10 sm:py-12 md:py-16 w-full\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Content (Columns Grid / Layout) --- */\nexport interface FooterContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterContent: ParentComponent<FooterContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8 lg:gap-12\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Column --- */\nexport interface FooterColumnProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterColumn: ParentComponent<FooterColumnProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"flex flex-col space-y-3\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Column Title --- */\nexport interface FooterColumnTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {\n class?: string;\n}\n\nexport const FooterColumnTitle: ParentComponent<FooterColumnTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <h4\n class={cn(\n \"text-xs font-semibold uppercase tracking-wider text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </h4>\n );\n};\n\n/* --- Footer Column List --- */\nexport interface FooterColumnListProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const FooterColumnList: ParentComponent<FooterColumnListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul class={cn(\"space-y-2 list-none p-0 m-0\", local.class)} {...rest}>\n {local.children}\n </ul>\n );\n};\n\n/* --- Footer Link --- */\nexport interface FooterLinkProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n class?: string;\n}\n\nexport const FooterLink: ParentComponent<FooterLinkProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li>\n <a\n class={cn(\n \"inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n </li>\n );\n};\n\n/* --- Footer Brand / Info Section --- */\nexport interface FooterBrandProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterBrand: ParentComponent<FooterBrandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"col-span-2 sm:col-span-2 md:col-span-4 lg:col-span-2 flex flex-col space-y-3 mb-4 lg:mb-0\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Bottom (Copyright and Secondary Links Bar) --- */\nexport interface FooterBottomProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterBottom: ParentComponent<FooterBottomProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"mt-12 sm:mt-16 pt-8 border-t border-border/50 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-muted-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Copyright Text --- */\nexport interface FooterCopyrightProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const FooterCopyright: ParentComponent<FooterCopyrightProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p class={cn(\"text-xs text-muted-foreground leading-relaxed\", local.class)} {...rest}>\n {local.children}\n </p>\n );\n};\n\n/* --- Footer Social Links Container --- */\nexport interface FooterSocialsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterSocials: ParentComponent<FooterSocialsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"flex items-center gap-3 text-muted-foreground\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n",
|
|
14
|
+
"content": "import { splitProps, type JSX, type ParentComponent, type Component } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- Root Footer --- */\nexport const footerVariants = cva(\"w-full transition-colors\", {\n variants: {\n variant: {\n default: \"bg-background border-t border-border/60 text-foreground\",\n muted: \"bg-muted/40 border-t border-border text-foreground\",\n bordered: \"bg-background border-t-2 border-border text-foreground\",\n floating: \"my-8 rounded-lg border border-border/80 bg-card text-card-foreground shadow-xs\",\n transparent: \"bg-transparent border-transparent text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n});\n\nexport interface FooterProps\n extends JSX.HTMLAttributes<HTMLElement>,\n VariantProps<typeof footerVariants> {\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"full\";\n}\n\nexport const Footer: ParentComponent<FooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"maxWidth\", \"children\"]);\n\n const maxWidthClass = () => {\n switch (local.maxWidth) {\n case \"sm\":\n return \"max-w-screen-sm\";\n case \"md\":\n return \"max-w-screen-md\";\n case \"lg\":\n return \"max-w-screen-lg\";\n case \"xl\":\n return \"max-w-screen-xl\";\n case \"full\":\n return \"max-w-full\";\n case \"2xl\":\n default:\n return \"max-w-screen-2xl\";\n }\n };\n\n return (\n <footer\n class={cn(\n footerVariants({ variant: local.variant }),\n local.variant === \"floating\" && cn(\"mx-auto\", maxWidthClass()),\n local.class\n )}\n {...rest}\n >\n <div class={cn(\"w-full\", local.variant !== \"floating\" && cn(\"mx-auto\", maxWidthClass()))}>\n {local.children}\n </div>\n </footer>\n );\n};\n\n/* --- Footer Container --- */\nexport interface FooterContainerProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterContainer: ParentComponent<FooterContainerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"px-4 sm:px-6 lg:px-8 py-10 sm:py-12 md:py-16 w-full\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Content (Columns Grid / Layout) --- */\nexport interface FooterContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterContent: ParentComponent<FooterContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8 lg:gap-12\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Column --- */\nexport interface FooterColumnProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterColumn: ParentComponent<FooterColumnProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"flex flex-col space-y-3\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Column Title --- */\nexport interface FooterColumnTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {\n class?: string;\n}\n\nexport const FooterColumnTitle: ParentComponent<FooterColumnTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <h4\n class={cn(\n \"text-xs font-semibold uppercase tracking-wider text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </h4>\n );\n};\n\n/* --- Footer Column List --- */\nexport interface FooterColumnListProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const FooterColumnList: ParentComponent<FooterColumnListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul class={cn(\"space-y-2 list-none p-0 m-0\", local.class)} {...rest}>\n {local.children}\n </ul>\n );\n};\n\n/* --- Footer Link --- */\nexport interface FooterLinkProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n class?: string;\n}\n\nexport const FooterLink: ParentComponent<FooterLinkProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li>\n <a\n class={cn(\n \"inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n </li>\n );\n};\n\n/* --- Footer Brand / Info Section --- */\nexport interface FooterBrandProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterBrand: ParentComponent<FooterBrandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"col-span-2 sm:col-span-2 md:col-span-4 lg:col-span-2 flex flex-col space-y-3 mb-4 lg:mb-0\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Bottom (Copyright and Secondary Links Bar) --- */\nexport interface FooterBottomProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterBottom: ParentComponent<FooterBottomProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"mt-12 sm:mt-16 pt-8 border-t border-border/50 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-muted-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Copyright Text --- */\nexport interface FooterCopyrightProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const FooterCopyright: ParentComponent<FooterCopyrightProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p class={cn(\"text-xs text-muted-foreground leading-relaxed\", local.class)} {...rest}>\n {local.children}\n </p>\n );\n};\n\n/* --- Footer Social Links Container --- */\nexport interface FooterSocialsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterSocials: ParentComponent<FooterSocialsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"flex items-center gap-3 text-muted-foreground\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n",
|
|
15
15
|
"type": "registry:ui"
|
|
16
16
|
}
|
|
17
17
|
]
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
"description": "Validation message helper connected to createForm errors and touched state.",
|
|
5
5
|
"type": "registry:ui",
|
|
6
6
|
"registryDependencies": [
|
|
7
|
-
"field"
|
|
7
|
+
"field",
|
|
8
|
+
"create-form"
|
|
8
9
|
],
|
|
9
10
|
"files": [
|
|
10
11
|
{
|
|
11
12
|
"path": "ui/form-message.tsx",
|
|
12
|
-
"content": "import { Show, splitProps, type JSX } from \"solid-js\";\nimport type { CreateFormReturn } from \"
|
|
13
|
+
"content": "import { Show, splitProps, type JSX } from \"solid-js\";\nimport type { CreateFormReturn } from \"@/hooks/create-form\";\nimport { FieldError, type FieldErrorProps } from \"@/components/ui/field\";\n\ntype FormState<T extends Record<string, any>> = Pick<\n CreateFormReturn<T>,\n \"errors\" | \"touched\"\n>;\n\nexport interface FormMessageProps<\n T extends Record<string, any>,\n K extends keyof T,\n> extends Omit<FieldErrorProps, \"children\"> {\n form: FormState<T>;\n name: K;\n /** Show the message even before the field has been touched. */\n showUntouched?: boolean;\n}\n\n/** Displays a field's validation error from createForm when it should be visible. */\nexport function FormMessage<\n T extends Record<string, any>,\n K extends keyof T,\n>(props: FormMessageProps<T, K>): JSX.Element {\n const [local, rest] = splitProps(props, [\"form\", \"name\", \"showUntouched\", \"class\"]);\n const error = () => local.form.errors()[local.name] as string | undefined;\n const touched = () => local.form.touched()[local.name] === true;\n\n return (\n <Show when={error() && (local.showUntouched || touched())}>\n <FieldError class={local.class} {...rest}>\n {error()}\n </FieldError>\n </Show>\n );\n}\n",
|
|
13
14
|
"type": "registry:ui"
|
|
14
15
|
}
|
|
15
16
|
]
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
{
|
|
15
15
|
"path": "ui/icon-button.tsx",
|
|
16
|
-
"content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { Button, type ButtonProps } from \"
|
|
16
|
+
"content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { Button, type ButtonProps } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface IconButtonProps\n extends Omit<ButtonProps, \"size\" | \"children\"> {\n /** Accessible label for the icon-only button. */\n label: string;\n size?: \"sm\" | \"default\" | \"lg\";\n children?: JSX.Element;\n}\n\n/** An accessible square button intended for a single icon action. */\nexport const IconButton: Component<IconButtonProps> = (props) => {\n const [local, rest] = splitProps(props, [\"label\", \"size\", \"class\", \"children\"]);\n\n return (\n <Button\n size=\"icon\"\n aria-label={local.label}\n class={cn(\n {\n \"size-8\": local.size === \"sm\",\n \"size-9\": !local.size || local.size === \"default\",\n \"size-10\": local.size === \"lg\",\n },\n local.class\n )}\n {...rest}\n >\n {local.children}\n </Button>\n );\n};\n",
|
|
17
17
|
"type": "registry:ui"
|
|
18
18
|
}
|
|
19
19
|
]
|