@nikala-ui/core 0.10.1 → 0.11.0-nightly.b01fa01
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry/banner.json +1 -1
- package/registry/bubble.json +18 -0
- package/registry/button-group.json +20 -0
- package/registry/button.json +1 -1
- package/registry/checkbox.json +4 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +3 -2
- package/registry/context-menu.json +1 -1
- package/registry/create-app-updater.json +13 -0
- package/registry/create-chat-scroll.json +13 -0
- package/registry/create-document-tabs.json +13 -0
- package/registry/create-drop-zone.json +13 -0
- package/registry/create-global-shortcut.json +13 -0
- package/registry/create-pagination.json +13 -0
- package/registry/create-tauri-window.json +13 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +3 -2
- package/registry/dropzone.json +21 -0
- package/registry/field.json +1 -1
- package/registry/footer.json +18 -0
- package/registry/forgot-password-01.json +28 -0
- package/registry/form-message.json +3 -2
- package/registry/hero-01.json +22 -0
- package/registry/icon-button.json +1 -1
- package/registry/index.json +432 -5
- package/registry/login-01.json +28 -0
- package/registry/marker.json +17 -0
- package/registry/marquee.json +17 -0
- package/registry/message.json +17 -0
- package/registry/navbar.json +19 -0
- package/registry/navigation-menu.json +22 -0
- package/registry/number-input.json +1 -1
- package/registry/otp-verification-01.json +26 -0
- package/registry/pagination.json +22 -0
- package/registry/popover.json +4 -1
- package/registry/rating.json +19 -0
- package/registry/register-01.json +31 -0
- package/registry/resizable.json +1 -1
- package/registry/review-card.json +23 -0
- package/registry/scroll-area.json +1 -1
- package/registry/select.json +3 -2
- package/registry/sheet.json +1 -1
- package/registry/sidebar.json +24 -0
- package/registry/spinner.json +1 -1
- package/registry/stat.json +19 -0
- package/registry/status.json +1 -1
- package/registry/switch.json +4 -1
- package/registry/table.json +17 -0
- package/registry/tabs.json +4 -1
- package/registry/theme-manager.json +4 -3
- package/registry/timeline.json +18 -0
- package/registry/titlebar-tabs.json +24 -0
- package/registry/titlebar.json +26 -0
- package/registry/toggle-group.json +21 -0
- package/registry/updater-modal.json +26 -0
- package/src/registry/blocks/forgot-password-01.tsx +141 -0
- package/src/registry/blocks/hero-01.tsx +28 -0
- package/src/registry/blocks/login-01.tsx +231 -0
- package/src/registry/blocks/otp-verification-01.tsx +170 -0
- package/src/registry/blocks/register-01.tsx +318 -0
- package/src/registry/components/ui/banner.tsx +0 -2
- package/src/registry/components/ui/bubble.tsx +142 -0
- package/src/registry/components/ui/button-group.tsx +42 -0
- package/src/registry/components/ui/button.tsx +1 -1
- package/src/registry/components/ui/checkbox.tsx +1 -1
- package/src/registry/components/ui/combobox.tsx +83 -18
- package/src/registry/components/ui/command.tsx +101 -105
- 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/dropzone.tsx +189 -0
- package/src/registry/components/ui/field.tsx +7 -3
- package/src/registry/components/ui/footer.tsx +247 -0
- 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/marker.tsx +102 -0
- package/src/registry/components/ui/marquee.tsx +118 -0
- package/src/registry/components/ui/message.tsx +168 -0
- package/src/registry/components/ui/navbar.tsx +375 -0
- package/src/registry/components/ui/navigation-menu.tsx +358 -0
- package/src/registry/components/ui/number-input.tsx +3 -3
- package/src/registry/components/ui/pagination.tsx +258 -0
- package/src/registry/components/ui/popover.tsx +1 -1
- package/src/registry/components/ui/rating.tsx +185 -0
- package/src/registry/components/ui/resizable.tsx +3 -1
- package/src/registry/components/ui/review-card.tsx +195 -0
- package/src/registry/components/ui/scroll-area.tsx +13 -3
- 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 +692 -0
- package/src/registry/components/ui/spinner.tsx +1 -1
- package/src/registry/components/ui/stat.tsx +245 -0
- package/src/registry/components/ui/status.tsx +7 -5
- package/src/registry/components/ui/switch.tsx +1 -1
- package/src/registry/components/ui/table.tsx +160 -0
- package/src/registry/components/ui/tabs.tsx +1 -1
- package/src/registry/components/ui/theme-toggle.tsx +80 -79
- package/src/registry/components/ui/timeline.tsx +350 -0
- package/src/registry/components/ui/titlebar-tabs.tsx +412 -0
- package/src/registry/components/ui/titlebar.tsx +399 -0
- package/src/registry/components/ui/toggle-group.tsx +203 -0
- package/src/registry/components/ui/updater-modal.tsx +254 -0
- package/src/registry/index.ts +3 -3
- package/src/registry/metadata.ts +184 -5
- package/src/registry/providers/theme-transitions.ts +81 -50
package/package.json
CHANGED
package/registry/banner.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
{
|
|
14
14
|
"path": "ui/banner.tsx",
|
|
15
|
-
"content": "// src/components/ui/banner.tsx\nimport {\n createSignal,\n onMount,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n} from \"solid-js\";\nimport { Dynamic } from \"solid-js/web\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Info, AlertTriangle, CheckCircle, AlertCircle, X } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport const bannerVariants = cva(\n \"relative flex w-full items-center justify-between gap-3 px-4 py-2.5 text-sm transition-all duration-300 ease-in-out\",\n {\n variants: {\n variant: {\n default:\n \"bg-zinc-900 text-zinc-50 dark:bg-zinc-100 dark:text-zinc-900\",\n warning:\n \"bg-amber-500/15 text-amber-900 border-b border-amber-500/20 dark:text-amber-200\",\n info:\n \"bg-sky-500/15 text-sky-900 border-b border-sky-500/20 dark:text-sky-200\",\n success:\n \"bg-emerald-500/15 text-emerald-900 border-b border-emerald-500/20 dark:text-emerald-200\",\n destructive:\n \"bg-rose-500/15 text-rose-900 border-b border-rose-500/20 dark:text-rose-200\",\n
|
|
15
|
+
"content": "// src/components/ui/banner.tsx\nimport {\n createSignal,\n onMount,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n} from \"solid-js\";\nimport { Dynamic } from \"solid-js/web\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Info, AlertTriangle, CheckCircle, AlertCircle, X } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport const bannerVariants = cva(\n \"relative flex w-full items-center justify-between gap-3 px-4 py-2.5 text-sm transition-all duration-300 ease-in-out\",\n {\n variants: {\n variant: {\n default:\n \"bg-zinc-900 text-zinc-50 dark:bg-zinc-100 dark:text-zinc-900\",\n warning:\n \"bg-amber-500/15 text-amber-900 border-b border-amber-500/20 dark:text-amber-200\",\n info:\n \"bg-sky-500/15 text-sky-900 border-b border-sky-500/20 dark:text-sky-200\",\n success:\n \"bg-emerald-500/15 text-emerald-900 border-b border-emerald-500/20 dark:text-emerald-200\",\n destructive:\n \"bg-rose-500/15 text-rose-900 border-b border-rose-500/20 dark:text-rose-200\",\n },\n sticky: {\n true: \"sticky top-0 z-50 backdrop-blur-sm\",\n false: \"relative\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n sticky: false,\n },\n }\n);\n\nexport interface BannerProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof bannerVariants> {\n dismissible?: boolean;\n autoHideDelay?: number;\n storageKey?: string;\n showIcon?: boolean;\n icon?: Component<{ class?: string }>;\n link?: string;\n linkText?: string;\n linkTarget?: string;\n onDismiss?: () => void;\n class?: string;\n}\n\n/**\n * Nikala UI Banner Component.\n */\nexport const Banner: Component<BannerProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"sticky\",\n \"dismissible\",\n \"autoHideDelay\",\n \"storageKey\",\n \"showIcon\",\n \"icon\",\n \"link\",\n \"linkText\",\n \"linkTarget\",\n \"onDismiss\",\n \"class\",\n \"children\",\n ]);\n\n const [visible, setVisible] = createSignal(true);\n let timerId: ReturnType<typeof setTimeout> | undefined;\n\n onMount(() => {\n if (local.storageKey) {\n try {\n const isDismissed = localStorage.getItem(local.storageKey);\n if (isDismissed === \"true\") {\n setVisible(false);\n return;\n }\n } catch (e) { }\n }\n\n if (local.autoHideDelay && local.autoHideDelay > 0) {\n timerId = setTimeout(() => {\n handleDismiss();\n }, local.autoHideDelay);\n }\n });\n\n onCleanup(() => {\n if (timerId) clearTimeout(timerId);\n });\n\n const handleDismiss = () => {\n setVisible(false);\n\n if (local.storageKey) {\n try {\n localStorage.setItem(local.storageKey, \"true\");\n } catch (e) {}\n }\n\n if (typeof local.onDismiss === \"function\") {\n local.onDismiss();\n }\n };\n\n /* Default icon selector based on banner variant */\n const getDefaultIcon = () => {\n switch (local.variant) {\n case \"warning\":\n return AlertTriangle;\n case \"success\":\n return CheckCircle;\n case \"destructive\":\n return AlertCircle;\n default:\n return Info;\n }\n };\n\n const activeIcon = () => local.icon || getDefaultIcon();\n const shouldShowIcon = () => local.showIcon !== false;\n const shouldBeDismissible = () => local.dismissible !== false;\n\n return (\n <Show when={visible()}>\n <div\n class={cn(\n bannerVariants({ variant: local.variant, sticky: local.sticky }),\n local.class\n )}\n {...rest}\n >\n <div class=\"flex flex-1 items-center justify-center gap-2 text-center sm:text-left\">\n {/* Dynamic Lucide Icon */}\n <Show when={shouldShowIcon()}>\n <span class=\"inline-flex shrink-0 items-center justify-center\">\n <Dynamic component={activeIcon()} class=\"h-4 w-4 opacity-80\" />\n </span>\n </Show>\n\n {/* Text Content */}\n <div class=\"flex-1 text-xs font-medium sm:text-sm\">\n {local.children}\n </div>\n\n {/* Action Link */}\n <Show when={local.link}>\n <div class=\"shrink-0\">\n <a\n href={local.link}\n target={local.linkTarget || \"_blank\"}\n rel=\"noreferrer\"\n class=\"underline font-semibold hover:opacity-80 transition-opacity\"\n >\n {local.linkText || \"Learn more\"}\n </a>\n </div>\n </Show>\n </div>\n\n {/* Close Button */}\n <Show when={shouldBeDismissible()}>\n <button\n type=\"button\"\n onClick={handleDismiss}\n class=\"shrink-0 rounded-md p-1 opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring\"\n aria-label=\"Dismiss banner\"\n >\n <X class=\"h-4 w-4\" />\n </button>\n </Show>\n </div>\n </Show>\n );\n};",
|
|
16
16
|
"type": "registry:ui"
|
|
17
17
|
}
|
|
18
18
|
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bubble",
|
|
3
|
+
"title": "Bubble",
|
|
4
|
+
"description": "Chat message bubble container supporting variants, grouped consecutive bubbles, and emoji reactions.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "ui/bubble.tsx",
|
|
14
|
+
"content": "import {\n splitProps,\n type Component,\n type JSX,\n type ParentComponent,\n useContext,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- 1. Bubble Variants --- */\nexport const bubbleVariants = cva(\n \"relative max-w-full rounded-lg transition-colors break-words text-left select-text\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground shadow-2xs\",\n muted: \"bg-muted text-foreground border border-border/50 shadow-2xs\",\n outline: \"border border-border bg-background text-foreground shadow-2xs\",\n ghost: \"bg-transparent text-foreground\",\n },\n size: {\n sm: \"px-3 py-1.5 text-xs\",\n default: \"px-4 py-2.5 text-sm\",\n lg: \"px-5 py-3 text-base\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\n/* --- 2. BubbleGroup --- */\nexport interface BubbleGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const BubbleGroup: ParentComponent<BubbleGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"flex flex-col gap-1 w-full\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 3. Bubble --- */\nexport interface BubbleProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof bubbleVariants> {\n class?: string;\n}\n\nexport const Bubble: ParentComponent<BubbleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"variant\", \"size\", \"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n bubbleVariants({\n variant: local.variant,\n size: local.size,\n }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 4. BubbleContent --- */\nexport interface BubbleContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const BubbleContent: ParentComponent<BubbleContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"leading-relaxed\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 5. BubbleReactions --- */\nexport interface BubbleReactionsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const BubbleReactions: ParentComponent<BubbleReactionsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"flex items-center gap-1.5 pt-2 mt-2 border-t border-border/30 z-10 flex-wrap\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 6. BubbleReaction --- */\nexport interface BubbleReactionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n active?: boolean;\n count?: number;\n class?: string;\n}\n\nexport const BubbleReaction: ParentComponent<BubbleReactionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"active\", \"count\", \"class\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n data-active={local.active ? \"true\" : \"false\"}\n class={cn(\n \"inline-flex items-center gap-1.5 rounded-md border border-border/70 bg-background/80 px-2 py-1 text-xs text-foreground shadow-2xs transition-all hover:bg-accent hover:border-primary/40 cursor-pointer select-none\",\n local.active && \"border-primary/40 bg-primary/10 text-primary font-medium\",\n local.class\n )}\n {...rest}\n >\n <span class=\"text-xs leading-none\">{local.children}</span>\n {local.count !== undefined && (\n <span class=\"text-[11px] font-mono leading-none text-muted-foreground\">{local.count}</span>\n )}\n </button>\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "button-group",
|
|
3
|
+
"title": "Button Group",
|
|
4
|
+
"description": "Groups related buttons into a connected horizontal or vertical control.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"button"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "ui/button-group.tsx",
|
|
16
|
+
"content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ButtonGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Controls whether grouped buttons are arranged in a row or column. */\n orientation?: \"horizontal\" | \"vertical\";\n class?: string;\n}\n\n/**\n * Groups adjacent buttons into a connected control with shared borders and radii.\n *\n * ButtonGroup is intentionally presentational. Use Button for individual actions\n * and compose the group with the same reactive state as the surrounding feature.\n */\nexport const ButtonGroup: Component<ButtonGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"orientation\",\n \"class\",\n \"children\",\n ]);\n\n const orientation = () => local.orientation ?? \"horizontal\";\n\n return (\n <div\n role=\"group\"\n data-orientation={orientation()}\n class={cn(\n \"isolate inline-flex\",\n orientation() === \"horizontal\"\n ? \"flex-row [&>button:not(:first-child)]:-ml-px [&>button:not(:first-child)]:rounded-l-none [&>button:not(:last-child)]:rounded-r-none\"\n : \"flex-col [&>button:not(:first-child)]:-mt-px [&>button:not(:first-child)]:rounded-t-none [&>button:not(:last-child)]:rounded-b-none\",\n \"[&>button:focus-visible]:z-10\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
|
|
17
|
+
"type": "registry:ui"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
package/registry/button.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "ui/button.tsx",
|
|
17
|
-
"content": "import { Show, splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\nimport { Spinner } from \"
|
|
17
|
+
"content": "import { Show, splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\nimport { Spinner } from \"@/components/ui/spinner\";\n\n/**\n * Class variance authority configuration for button styling variants and sizes.\n */\nexport const buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground shadow hover:bg-primary/90 border border-primary/50\",\n destructive:\n \"bg-red-600 text-zinc-50 shadow-sm hover:bg-red-600/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90\",\n outline:\n \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80 border border-border\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n success: \"*:bg-green-600 text-zinc-50 shadow-sm hover:bg-green-600/90 dark:bg-green-900 dark:text-zinc-50 dark:hover:bg-green-900/90\",\n warning: \"*:bg-yellow-600 text-zinc-50 shadow-sm hover:bg-yellow-600/90 dark:bg-yellow-900 dark:text-zinc-50 dark:hover:bg-yellow-900/90\",\n info: \"*:bg-blue-600 text-zinc-50 shadow-sm hover:bg-blue-600/90 dark:bg-blue-900 dark:text-zinc-50 dark:hover:bg-blue-900/90\",\n },\n size: {\n xs: \"h-6 rounded-md px-2 text-xs\",\n default: \"h-9 px-4 py-2\",\n sm: \"h-8 rounded-md px-3 text-xs\",\n lg: \"h-10 rounded-md px-8\",\n icon: \"h-9 w-9\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\n/**\n * Props interface for the Button component extending standard HTML button attributes.\n */\nexport interface ButtonProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n /** Shows a loading spinner and prevents repeated clicks while active. */\n loading?: boolean;\n class?: string;\n}\n\n/**\n * Nikala UI Button component built for SolidJS with Tailwind CSS v4 styling.\n */\nexport const Button: Component<ButtonProps> = (props) => {\n // Use splitProps to preserve SolidJS reactivity for destructured props\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n \"loading\",\n \"disabled\",\n ]);\n\n return (\n <button\n class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}\n disabled={local.disabled || local.loading}\n aria-busy={local.loading ? \"true\" : undefined}\n {...rest}\n >\n <Show when={local.loading}>\n <Spinner size=\"sm\" class=\"text-current\" />\n </Show>\n {local.children}\n </button>\n );\n};\n",
|
|
18
18
|
"type": "registry:ui"
|
|
19
19
|
}
|
|
20
20
|
]
|
package/registry/checkbox.json
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
"clsx",
|
|
8
8
|
"tailwind-merge"
|
|
9
9
|
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"create-controllable-signal"
|
|
12
|
+
],
|
|
10
13
|
"files": [
|
|
11
14
|
{
|
|
12
15
|
"path": "ui/checkbox.tsx",
|
|
13
|
-
"content": "import { splitProps, Show, type Component, type JSX } from \"solid-js\";\nimport { createControllableSignal } from \"
|
|
16
|
+
"content": "import { splitProps, Show, type Component, type JSX } from \"solid-js\";\nimport { createControllableSignal } from \"@/hooks/create-controllable-signal\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface CheckboxProps\n extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\"> {\n /** Controlled checked state */\n checked?: boolean;\n /** Uncontrolled default checked state */\n defaultChecked?: boolean;\n /** Event handler triggered when checked state changes */\n onChange?: (checked: boolean) => void;\n class?: string;\n}\n\n/**\n * Nikala UI Checkbox component built for SolidJS with Tailwind CSS v4 styling.\n */\nexport const Checkbox: Component<CheckboxProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"checked\",\n \"defaultChecked\",\n \"onChange\",\n \"disabled\",\n \"class\",\n \"onClick\",\n ]);\n\n const [isChecked, setIsChecked] = createControllableSignal({\n value: () => local.checked,\n defaultValue: local.defaultChecked ?? false,\n onChange: (val) => local.onChange?.(val),\n });\n\n const toggle = (\n e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }\n ) => {\n if (local.disabled) return;\n setIsChecked(!isChecked());\n\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n };\n\n return (\n <button\n type=\"button\"\n role=\"checkbox\"\n aria-checked={isChecked()}\n data-state={isChecked() ? \"checked\" : \"unchecked\"}\n disabled={local.disabled}\n onClick={toggle}\n class={cn(\n \"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground flex items-center justify-center transition-colors\",\n local.class\n )}\n {...rest}\n >\n <Show when={isChecked()}>\n <svg\n class=\"h-3.5 w-3.5 fill-none stroke-current stroke-[3]\"\n viewBox=\"0 0 24 24\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </Show>\n </button>\n );\n};",
|
|
14
17
|
"type": "registry:ui"
|
|
15
18
|
}
|
|
16
19
|
]
|
package/registry/combobox.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "ui/combobox.tsx",
|
|
17
|
-
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport { Check, ChevronDown, X } from \"lucide-solid\";\nimport { ScrollArea } from \"./scroll-area\";\nimport * as ComboboxPrimitive from \"@kobalte/core/combobox\";\nimport { cn } from \"@/lib/cn\";\n\nexport type ComboboxRootProps<Option = any, OptGroup = any, T extends ValidComponent = \"div\"> =\n ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T>;\n\n/**\n * Root Combobox primitive component wrapper.\n */\nexport const Combobox = <Option = any, OptGroup = any, T extends ValidComponent = \"div\">(\n props: ComboboxRootProps<Option, OptGroup, T>\n) => {\n return <ComboboxPrimitive.Root {...props} />;\n};\n\nexport type ComboboxControlProps<Option = any, T extends ValidComponent = \"div\"> =\n ComboboxPrimitive.ComboboxControlProps<Option, T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Input container box supporting single or multi-select tokens.\n */\nexport const ComboboxControl = <Option = any, T extends ValidComponent = \"div\">(\n props: ComboboxControlProps<Option, T>\n) => {\n const [local, rest] = splitProps(props as ComboboxControlProps, [\"class\", \"children\"]);\n\n return (\n <ComboboxPrimitive.Control\n class={cn(\n \"flex min-h-9 w-full flex-wrap items-center gap-1.5 rounded-md border border-input bg-muted px-3 py-1.5 text-sm shadow-2xs ring-offset-background focus-within:ring-1 focus-within:ring-primary focus-within:border-primary disabled:cursor-not-allowed disabled:opacity-50 text-foreground cursor-text transition-colors\",\n local.class\n )}\n {...(rest as any)}\n >\n {local.children}\n </ComboboxPrimitive.Control>\n );\n};\n\nexport type ComboboxInputProps<T extends ValidComponent = \"input\"> =\n ComboboxPrimitive.ComboboxInputProps<T> & {\n class?: string;\n openOnFocus?: boolean;\n };\n\n/**\n * Filter search input field.\n */\nexport const ComboboxInput = <T extends ValidComponent = \"input\">(\n props: ComboboxInputProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxInputProps, [\"class\", \"openOnFocus\", \"onFocus\", \"onClick\"]);\n\n return (\n <ComboboxPrimitive.Input\n class={cn(\n \"flex-1 bg-transparent py-1 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed text-foreground min-w-16\",\n local.class\n )}\n onFocus={(e: FocusEvent) => {\n if (typeof local.onFocus === \"function\") local.onFocus(e as any);\n }}\n onClick={(e: MouseEvent) => {\n if (typeof local.onClick === \"function\") local.onClick(e as any);\n }}\n {...(rest as any)}\n />\n );\n};\n\nexport type ComboboxTriggerProps<T extends ValidComponent = \"button\"> =\n ComboboxPrimitive.ComboboxTriggerProps<T> & {\n class?: string;\n };\n\n/**\n * Dropdown chevron trigger icon.\n */\nexport const ComboboxTrigger = <T extends ValidComponent = \"button\">(\n props: ComboboxTriggerProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxTriggerProps, [\"class\"]);\n\n return (\n <ComboboxPrimitive.Trigger\n class={cn(\n \"flex h-4 w-4 items-center justify-center opacity-50 hover:opacity-100 transition-opacity focus:outline-none cursor-pointer\",\n local.class\n )}\n {...(rest as any)}\n >\n <ComboboxPrimitive.Icon\n as=\"svg\"\n class=\"h-4 w-4 stroke-current stroke-2 fill-none\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </ComboboxPrimitive.Icon>\n </ComboboxPrimitive.Trigger>\n );\n};\n\nexport type ComboboxTokenProps<Option = any> = {\n class?: string;\n children?: JSX.Element;\n item?: Option;\n onRemove?: () => void;\n};\n\n/**\n * Selected item tag token pill rendered in multi-select mode.\n */\nexport const ComboboxToken = <Option = any>(props: ComboboxTokenProps<Option>) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\", \"onRemove\"]);\n\n return (\n <span\n class={cn(\n \"inline-flex items-center gap-1.5 rounded-md bg-accent px-2 py-0.5 text-xs font-medium text-accent-foreground border border-border shadow-2xs animate-in fade-in-50\",\n local.class\n )}\n {...rest}\n >\n <span class=\"truncate\">{local.children}</span>\n <button\n type=\"button\"\n tabIndex={-1}\n onClick={(e) => {\n e.stopPropagation();\n if (local.onRemove) local.onRemove();\n }}\n class=\"rounded-xs opacity-70 hover:opacity-100 focus:outline-none cursor-pointer text-muted-foreground hover:text-foreground\"\n >\n <X class=\"h-3 w-3\" />\n <span class=\"sr-only\">Remove</span>\n </button>\n </span>\n );\n};\n\nexport type ComboboxContentProps<T extends ValidComponent = \"div\"> =\n ComboboxPrimitive.ComboboxContentProps<T> & {\n class?: string;\n };\n\n/**\n * Portaled popover content listbox container.\n */\nexport const ComboboxContent = <T extends ValidComponent = \"div\">(\n props: ComboboxContentProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxContentProps, [\"class\"]);\n\n return (\n <ComboboxPrimitive.Portal>\n <ComboboxPrimitive.Content\n class={cn(\n \"relative z-50 min-w-8rem overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 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 max-h-60\",\n local.class\n )}\n {...(rest as any)}\n >\n <ScrollArea class=\"max-h-60 w-full\">\n <ComboboxPrimitive.Listbox class=\"p-1 outline-none\" />\n </ScrollArea>\n </ComboboxPrimitive.Content>\n </ComboboxPrimitive.Portal>\n );\n};\n\nexport type ComboboxItemProps<T extends ValidComponent = \"li\"> =\n ComboboxPrimitive.ComboboxItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Individual option item inside listbox supporting custom avatars, icons, and titles.\n */\nexport const ComboboxItem = <T extends ValidComponent = \"li\">(\n props: ComboboxItemProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxItemProps, [\"class\", \"children\"]);\n\n return (\n <ComboboxPrimitive.Item\n class={cn(\n \"relative flex w-full cursor-pointer select-none items-center justify-between rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50 text-foreground transition-colors\",\n local.class\n )}\n {...(rest as any)}\n >\n <ComboboxPrimitive.ItemLabel class=\"flex-1 truncate\">\n {local.children}\n </ComboboxPrimitive.ItemLabel>\n <ComboboxPrimitive.ItemIndicator class=\"ml-2 flex h-4 w-4 items-center justify-center text-primary\">\n <Check class=\"h-4 w-4 stroke-2\" />\n </ComboboxPrimitive.ItemIndicator>\n </ComboboxPrimitive.Item>\n );\n};\n\nexport type ComboboxGroupProps<T extends ValidComponent = \"li\"> =\n ComboboxPrimitive.ComboboxSectionProps<T> & {\n class?: string;\n children?: JSX.Element;\n label?: JSX.Element;\n };\n\n/**\n * Group container for organizing related options.\n */\nexport const ComboboxGroup = <T extends ValidComponent = \"li\">(\n props: ComboboxGroupProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxGroupProps, [\"class\", \"children\", \"label\"]);\n\n return (\n <ComboboxPrimitive.Section class={cn(\"px-1 py-1\", local.class)} {...(rest as any)}>\n {local.label && (\n <span class=\"px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider block\">\n {local.label}\n </span>\n )}\n {local.children}\n </ComboboxPrimitive.Section>\n );\n};\n",
|
|
17
|
+
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport { Check, ChevronDown, X } from \"lucide-solid\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport * as ComboboxPrimitive from \"@kobalte/core/combobox\";\nimport { cn } from \"@/lib/cn\";\n\nexport type ComboboxRootProps<Option = any, OptGroup = any, T extends ValidComponent = \"div\"> =\n ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T> & {\n class?: string;\n children?: JSX.Element;\n triggerMode?: \"input\" | \"focus\" | \"both\" | \"manual\";\n };\n\n/**\n * Root Combobox component providing search, single/multi-selection, and group support.\n * `triggerMode=\"focus\"` or `triggerMode=\"both\"` enables opening dropdown on input click/focus.\n */\nexport const Combobox = <Option = any, OptGroup = any, T extends ValidComponent = \"div\">(\n props: ComboboxRootProps<Option, OptGroup, T>\n) => {\n const [local, rest] = splitProps(props as ComboboxRootProps, [\"class\", \"children\", \"triggerMode\"]);\n\n return (\n <ComboboxPrimitive.Root\n triggerMode={local.triggerMode ?? \"input\"}\n class={cn(\"relative w-full\", local.class)}\n {...(rest as any)}\n >\n {local.children}\n </ComboboxPrimitive.Root>\n );\n};\n\nexport type ComboboxControlProps<Option = any, T extends ValidComponent = \"div\"> =\n ComboboxPrimitive.ComboboxControlProps<Option, T> & {\n class?: string;\n children?: JSX.Element;\n clearable?: boolean;\n onClear?: () => void;\n };\n\n/**\n * Input container for Combobox supporting search input, selected tags, and clear button.\n */\nexport const ComboboxControl = <Option = any, T extends ValidComponent = \"div\">(\n props: ComboboxControlProps<Option, T>\n) => {\n const [local, rest] = splitProps(props as ComboboxControlProps, [\n \"class\",\n \"children\",\n \"clearable\",\n \"onClear\",\n ]);\n\n return (\n <ComboboxPrimitive.Control\n class={cn(\n \"flex min-h-9 w-full flex-wrap items-center justify-between rounded-md border border-input bg-muted px-3 py-1 text-sm shadow-sm transition-colors focus-within:ring-1 focus-within:ring-primary focus-within:border-primary disabled:cursor-not-allowed disabled:opacity-50 gap-1.5 text-foreground\",\n local.class\n )}\n {...(rest as any)}\n >\n <div class=\"flex flex-wrap items-center gap-1.5 flex-1 min-w-0\">\n {local.children}\n </div>\n\n <div class=\"flex items-center gap-1 shrink-0 self-center\">\n {local.clearable && (\n <button\n type=\"button\"\n tabIndex={-1}\n onClick={(e) => {\n e.stopPropagation();\n if (local.onClear) local.onClear();\n }}\n class=\"rounded-sm p-0.5 opacity-60 hover:opacity-100 hover:bg-accent text-foreground transition-opacity focus:outline-none cursor-pointer\"\n aria-label=\"Clear selection\"\n >\n <svg class=\"h-3.5 w-3.5\" 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 </button>\n )}\n <ComboboxTrigger />\n </div>\n </ComboboxPrimitive.Control>\n );\n};\n\nexport type ComboboxInputProps<T extends ValidComponent = \"input\"> =\n ComboboxPrimitive.ComboboxInputProps<T> & {\n class?: string;\n openOnFocus?: boolean;\n };\n\n/**\n * Search input field embedded within ComboboxControl.\n * Supports `openOnFocus` to automatically trigger the dropdown when focused or clicked.\n */\nexport const ComboboxInput = <T extends ValidComponent = \"input\">(\n props: ComboboxInputProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxInputProps, [\"class\", \"openOnFocus\", \"onFocus\", \"onClick\"]);\n\n return (\n <ComboboxPrimitive.Input\n class={cn(\n \"flex-1 bg-transparent py-1 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed text-foreground min-w-16\",\n local.class\n )}\n onFocus={(e: FocusEvent) => {\n if (typeof local.onFocus === \"function\") local.onFocus(e as any);\n }}\n onClick={(e: MouseEvent) => {\n if (typeof local.onClick === \"function\") local.onClick(e as any);\n }}\n {...(rest as any)}\n />\n );\n};\n\nexport type ComboboxTriggerProps<T extends ValidComponent = \"button\"> =\n ComboboxPrimitive.ComboboxTriggerProps<T> & {\n class?: string;\n };\n\n/**\n * Dropdown chevron trigger icon.\n */\nexport const ComboboxTrigger = <T extends ValidComponent = \"button\">(\n props: ComboboxTriggerProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxTriggerProps, [\"class\"]);\n\n return (\n <ComboboxPrimitive.Trigger\n class={cn(\n \"flex h-4 w-4 items-center justify-center opacity-50 hover:opacity-100 transition-opacity focus:outline-none cursor-pointer\",\n local.class\n )}\n {...(rest as any)}\n >\n <ComboboxPrimitive.Icon\n as=\"svg\"\n class=\"h-4 w-4 stroke-current stroke-2 fill-none\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </ComboboxPrimitive.Icon>\n </ComboboxPrimitive.Trigger>\n );\n};\n\nexport type ComboboxTokenProps<Option = any> = {\n class?: string;\n children?: JSX.Element;\n item?: Option;\n onRemove?: () => void;\n};\n\n/**\n * Selected item tag token pill rendered in multi-select mode.\n */\nexport const ComboboxToken = <Option = any>(props: ComboboxTokenProps<Option>) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\", \"onRemove\"]);\n\n return (\n <span\n class={cn(\n \"inline-flex items-center gap-1.5 rounded-md bg-accent px-2 py-0.5 text-xs font-medium text-accent-foreground border border-border shadow-2xs animate-in fade-in-50\",\n local.class\n )}\n {...rest}\n >\n <span class=\"truncate\">{local.children}</span>\n <button\n type=\"button\"\n tabIndex={-1}\n onClick={(e) => {\n e.stopPropagation();\n if (local.onRemove) local.onRemove();\n }}\n class=\"rounded-xs p-0.5 hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground transition-colors focus:outline-none cursor-pointer\"\n aria-label=\"Remove tag\"\n >\n <svg class=\"h-3 w-3\" 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 </button>\n </span>\n );\n};\n\nexport type ComboboxContentProps<T extends ValidComponent = \"div\"> =\n ComboboxPrimitive.ComboboxContentProps<T> & {\n class?: string;\n };\n\n/**\n * Portaled popover content listbox container.\n */\nexport const ComboboxContent = <T extends ValidComponent = \"div\">(\n props: ComboboxContentProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxContentProps, [\"class\"]);\n\n return (\n <ComboboxPrimitive.Portal>\n <ComboboxPrimitive.Content\n class={cn(\n \"relative z-50 min-w-8rem overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 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 max-h-60\",\n local.class\n )}\n {...(rest as any)}\n >\n <ScrollArea class=\"max-h-60 w-full\">\n <ComboboxPrimitive.Listbox class=\"p-1 outline-none\" />\n </ScrollArea>\n </ComboboxPrimitive.Content>\n </ComboboxPrimitive.Portal>\n );\n};\n\nexport type ComboboxItemProps<T extends ValidComponent = \"li\"> =\n ComboboxPrimitive.ComboboxItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Individual option item inside listbox supporting custom avatars, icons, and titles.\n */\nexport const ComboboxItem = <T extends ValidComponent = \"li\">(\n props: ComboboxItemProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxItemProps, [\"class\", \"children\"]);\n\n return (\n <ComboboxPrimitive.Item\n class={cn(\n \"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground text-popover-foreground transition-colors\",\n local.class\n )}\n {...rest}\n >\n <ComboboxPrimitive.ItemIndicator class=\"absolute left-2 flex h-4 w-4 items-center justify-center text-primary\">\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 </ComboboxPrimitive.ItemIndicator>\n <ComboboxPrimitive.ItemLabel class=\"flex items-center gap-2 w-full truncate\">\n {local.children}\n </ComboboxPrimitive.ItemLabel>\n </ComboboxPrimitive.Item>\n );\n};\n\nexport type ComboboxGroupProps<T extends ValidComponent = \"li\"> =\n ComboboxPrimitive.ComboboxSectionProps<T> & {\n class?: string;\n children?: JSX.Element;\n label?: JSX.Element;\n };\n\n/**\n * Group container for organizing related options.\n */\nexport const ComboboxGroup = <T extends ValidComponent = \"li\">(\n props: ComboboxGroupProps<T>\n) => {\n const [local, rest] = splitProps(props as ComboboxGroupProps, [\"class\", \"children\", \"label\"]);\n\n return (\n <ComboboxPrimitive.Section class={cn(\"px-1 py-1\", local.class)} {...(rest as any)}>\n {local.label && (\n <span class=\"px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider block\">\n {local.label}\n </span>\n )}\n {local.children}\n </ComboboxPrimitive.Section>\n );\n};\n\n/**\n * Empty state notice when no matching search items exist.\n */\nexport const ComboboxEmpty = (props: { class?: string; children?: JSX.Element }) => {\n return (\n <div class={cn(\"py-6 text-center text-sm text-muted-foreground\", props.class)}>\n {props.children || \"No matching items found.\"}\n </div>\n );\n};\n\n/**\n * Hidden native select element for form integrations.\n */\nexport const ComboboxHiddenSelect = ComboboxPrimitive.HiddenSelect;\n",
|
|
18
18
|
"type": "registry:ui"
|
|
19
19
|
}
|
|
20
20
|
]
|
package/registry/command.json
CHANGED
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
"kbd",
|
|
15
15
|
"input-group",
|
|
16
16
|
"list",
|
|
17
|
-
"scroll-area"
|
|
17
|
+
"scroll-area",
|
|
18
|
+
"create-keybindings"
|
|
18
19
|
],
|
|
19
20
|
"files": [
|
|
20
21
|
{
|
|
21
22
|
"path": "ui/command.tsx",
|
|
22
|
-
"content": "import {\n createContext,\n useContext,\n createSignal,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createKeybindings } from \"@nikala-ui/hooks\";\nimport { Search, ArrowUp, ArrowDown, CornerDownLeft } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\nimport { Kbd, KbdGroup } from \"./kbd\";\nimport { InputGroup, InputGroupInput, InputGroupAddon } from \"./input-group\";\nimport { List, ListGroup, ListHeader, ListItem, type ListItemProps } from \"./list\";\nimport { Dialog, DialogOverlay, DialogContent } from \"./dialog\";\nimport { ScrollArea } from \"./scroll-area\";\n\n/* --- Command Context State --- */\ninterface CommandContextValue {\n search: Accessor<string>;\n setSearch: (value: string) => void;\n activeIndex: Accessor<number>;\n setActiveIndex: (fn: (prev: number) => number) => void;\n registerItemIndex: () => number;\n onItemSelect: (fn: () => void) => void;\n}\n\nconst CommandContext = createContext<CommandContextValue>();\n\nexport const useCommand = () => {\n const ctx = useContext(CommandContext);\n if (!ctx) {\n throw new Error(\"useCommand must be used within a <Command /> root component.\");\n }\n return ctx;\n};\n\n/* --- Command Root --- */\nexport interface CommandProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"children\"> {\n class?: string;\n children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);\n}\n\nexport const Command: Component<CommandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const [search, setSearch] = createSignal(\"\");\n const [activeIndex, setActiveIndex] = createSignal(0);\n let itemCounter = 0;\n const itemCallbacks: Record<number, () => void> = {};\n let containerRef: HTMLDivElement | undefined;\n\n const registerItemIndex = () => {\n const idx = itemCounter;\n itemCounter += 1;\n return idx;\n };\n\n const onItemSelect = (fn: () => void) => {\n itemCallbacks[activeIndex()] = fn;\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((prev) => Math.min(prev + 1, Math.max(0, itemCounter - 1)));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((prev) => Math.max(prev - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n const fn = itemCallbacks[activeIndex()];\n if (fn) fn();\n const current = containerRef?.querySelector('[data-command-active=\"true\"]') as HTMLElement;\n if (current) {\n current.click();\n }\n }\n };\n\n const ctx: CommandContextValue = {\n search,\n setSearch: (val) => {\n setSearch(val);\n setActiveIndex(0);\n },\n activeIndex,\n setActiveIndex: (fn) => setActiveIndex(fn),\n registerItemIndex,\n onItemSelect,\n };\n\n return (\n <CommandContext.Provider value={ctx}>\n <div\n ref={containerRef}\n onKeyDown={handleKeyDown}\n class={cn(\n \"flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md outline-none\",\n local.class\n )}\n tabIndex={0}\n {...rest}\n >\n {typeof local.children === \"function\" ? (local.children as any)(ctx) : local.children}\n </div>\n </CommandContext.Provider>\n );\n};\n\n/* --- Command Dialog (Modal) --- */\nexport interface CommandDialogProps {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n enableHotkey?: boolean;\n children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);\n class?: string;\n}\n\nexport const CommandDialog: Component<CommandDialogProps> = (props) => {\n const [internalOpen, setInternalOpen] = createSignal(false);\n\n const isOpen = () => (props.open !== undefined ? props.open : internalOpen());\n const setOpen = (val: boolean) => {\n if (props.open === undefined) setInternalOpen(val);\n if (typeof props.onOpenChange === \"function\") props.onOpenChange(val);\n };\n\n /* Listen for global Ctrl+K / Cmd+K hotkeys */\n createKeybindings(\n [\n {\n key: [\"meta+k\", \"ctrl+k\"],\n handler: () => setOpen(!isOpen()),\n preventDefault: true,\n },\n ],\n {\n enabled: () => props.enableHotkey !== false,\n }\n );\n\n return (\n <Dialog open={isOpen()} onOpenChange={setOpen}>\n <DialogOverlay class=\"fixed inset-0 z-50 bg-black/60 backdrop-blur-xs data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\" />\n <div class=\"fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4\">\n <DialogContent class=\"w-full max-w-xl rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl outline-none data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-closed:zoom-out-95 data-expanded:zoom-in-95 p-0\">\n <Command class={props.class}>{props.children}</Command>\n </DialogContent>\n </div>\n </Dialog>\n );\n};\n\n/* --- Command Input --- */\nexport interface CommandInputProps\n extends JSX.InputHTMLAttributes<HTMLInputElement> {\n class?: string;\n}\n\nexport const CommandInput: Component<CommandInputProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"value\", \"onInput\"]);\n const { search, setSearch } = useCommand();\n\n return (\n <InputGroup class=\"border-0 border-b border-border rounded-none bg-transparent px-3 py-1 shadow-none focus-within:ring-0 focus-within:border-border\">\n <InputGroupAddon align=\"inline-start\">\n <Search class=\"w-4 h-4 text-muted-foreground\" />\n </InputGroupAddon>\n\n <InputGroupInput\n ref={(el) => setTimeout(() => el.focus(), 50)}\n value={search()}\n onInput={(e) => {\n setSearch(e.currentTarget.value);\n if (typeof local.onInput === \"function\") {\n local.onInput(e);\n }\n }}\n placeholder=\"Type a command or search...\"\n class=\"h-11 text-base sm:text-sm font-medium\"\n {...rest}\n />\n </InputGroup>\n );\n};\n\n/* --- Command List Container --- */\nexport interface CommandListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandList: Component<CommandListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ScrollArea\n class={cn(\"max-h-82.5 p-1.5\", local.class)}\n {...rest}\n >\n <List>{local.children}</List>\n </ScrollArea>\n );\n};\n\n/* --- Command Empty Block --- */\nexport interface CommandEmptyProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandEmpty: Component<CommandEmptyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { search } = useCommand();\n\n return (\n <Show when={search().trim().length > 0}>\n <div\n class={cn(\n \"py-8 text-center text-sm text-muted-foreground font-medium select-none\",\n local.class\n )}\n {...rest}\n >\n {local.children || `No results found for \"${search()}\".`}\n </div>\n </Show>\n );\n};\n\n/* --- Command Group --- */\nexport interface CommandGroupProps {\n heading: string;\n children?: JSX.Element;\n class?: string;\n}\n\nexport const CommandGroup: Component<CommandGroupProps> = (props) => {\n return (\n <ListGroup class={props.class}>\n <ListHeader title={props.heading} />\n {props.children}\n </ListGroup>\n );\n};\n\n/* --- Command Item --- */\nexport interface CommandItemProps extends ListItemProps {\n keywords?: string[];\n onSelect?: () => void;\n}\n\nexport const CommandItem: Component<CommandItemProps> = (props) => {\n let itemRef: HTMLDivElement | undefined;\n const [local, rest] = splitProps(props, [\n \"title\",\n \"subtitle\",\n \"keywords\",\n \"disabled\",\n \"onSelect\",\n \"class\",\n \"children\",\n ]);\n\n const ctx = useCommand();\n const index = ctx.registerItemIndex();\n\n const isActive = () => ctx.activeIndex() === index;\n\n /* Automatic scroll into view when navigating with Arrow keys */\n const scrollIntoViewIfNeeded = () => {\n if (isActive() && itemRef) {\n itemRef.scrollIntoView({ block: \"nearest\", behavior: \"smooth\" });\n }\n };\n\n const isVisible = () => {\n const query = ctx.search().toLowerCase().trim();\n if (!query) return true;\n\n const titleStr = typeof local.title === \"string\" ? local.title.toLowerCase() : \"\";\n const subStr = typeof local.subtitle === \"string\" ? local.subtitle.toLowerCase() : \"\";\n\n if (titleStr.includes(query) || subStr.includes(query)) return true;\n\n if (local.keywords) {\n return local.keywords.some((k) => k.toLowerCase().includes(query));\n }\n\n return false;\n };\n\n const handleSelect = () => {\n if (local.disabled) return;\n if (local.onSelect) local.onSelect();\n };\n\n return (\n <Show when={isVisible()}>\n <ListItem\n ref={(el) => {\n itemRef = el;\n scrollIntoViewIfNeeded();\n }}\n title={local.title}\n subtitle={local.subtitle}\n data-command-active={isActive() ? \"true\" : \"false\"}\n class={cn(\n \"relative flex cursor-pointer select-none items-center rounded-md px-2 py-1.5 text-sm outline-none transition-colors\",\n isActive()\n ? \"bg-accent text-accent-foreground font-medium\"\n : \"text-popover-foreground hover:bg-muted/50\",\n local.disabled && \"pointer-events-none opacity-50\",\n local.class\n )}\n onClick={handleSelect}\n onMouseEnter={() => ctx.setActiveIndex(() => index)}\n {...rest}\n >\n {local.children}\n </ListItem>\n </Show>\n );\n};\n\n/* --- Command Footer Indicator Bar --- */\nexport interface CommandFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandFooter: Component<CommandFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between border-t border-border px-3 py-2 text-xs text-muted-foreground bg-muted/40\",\n local.class\n )}\n {...rest}\n >\n {local.children || (\n <>\n <div class=\"flex items-center gap-2\">\n <span class=\"inline-flex items-center gap-1\">\n <KbdGroup>\n <Kbd size=\"sm\">\n <ArrowUp class=\"w-2.5 h-2.5\" />\n </Kbd>\n <Kbd size=\"sm\">\n <ArrowDown class=\"w-2.5 h-2.5\" />\n </Kbd>\n </KbdGroup>\n <span>Navigate</span>\n </span>\n\n <span class=\"inline-flex items-center gap-1\">\n <Kbd size=\"sm\">\n <CornerDownLeft class=\"w-2.5 h-2.5\" />\n </Kbd>\n <span>Select</span>\n </span>\n </div>\n\n <span class=\"inline-flex items-center gap-1\">\n <Kbd size=\"sm\">ESC</Kbd>\n <span>Close</span>\n </span>\n </>\n )}\n </div>\n );\n};",
|
|
23
|
+
"content": "import {\n createContext,\n useContext,\n createSignal,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createKeybindings } from \"@/hooks/create-keybindings\";\nimport { Dialog } from \"@kobalte/core/dialog\";\nimport { Search, ArrowUp, ArrowDown, CornerDownLeft } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\nimport { Kbd, KbdGroup } from \"../ui/kbd\";\nimport { InputGroup, InputGroupInput, InputGroupAddon } from \"../ui/input-group\";\nimport { List, ListGroup, ListHeader, ListItem, type ListItemProps } from \"../ui/list\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\n\n/* --- Command Context State --- */\ninterface CommandContextValue {\n search: Accessor<string>;\n setSearch: (value: string) => void;\n activeIndex: Accessor<number>;\n setActiveIndex: (fn: (prev: number) => number) => void;\n registerItemIndex: (element: HTMLElement) => number;\n onItemSelect: (index: number, action?: () => void) => void;\n}\n\nconst CommandContext = createContext<CommandContextValue>();\n\nexport const useCommand = () => {\n const ctx = useContext(CommandContext);\n if (!ctx) {\n throw new Error(\"useCommand must be used within a <Command />\");\n }\n return ctx;\n};\n\n/* --- Root Command Container --- */\nexport interface CommandProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"children\"> {\n class?: string;\n children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);\n}\n\nexport const Command: Component<CommandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const [search, setSearch] = createSignal(\"\");\n const [activeIndex, setActiveIndex] = createSignal(0);\n\n let containerRef: HTMLDivElement | undefined;\n let itemsList: HTMLElement[] = [];\n\n const registerItemIndex = (element: HTMLElement) => {\n if (!itemsList.includes(element)) {\n itemsList.push(element);\n }\n return itemsList.indexOf(element);\n };\n\n const onItemSelect = (index: number, action?: () => void) => {\n setActiveIndex(index);\n if (action) action();\n };\n\n const getVisibleItems = () => {\n if (!containerRef) return [];\n return Array.from(containerRef.querySelectorAll<HTMLElement>(\"[data-command-item]:not(.hidden)\"));\n };\n\n const updateActiveAttribute = (index: number) => {\n const visible = getVisibleItems();\n visible.forEach((el, idx) => {\n if (idx === index) {\n el.setAttribute(\"aria-selected\", \"true\");\n el.scrollIntoView({ block: \"nearest\" });\n } else {\n el.removeAttribute(\"aria-selected\");\n }\n });\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n const visible = getVisibleItems();\n if (visible.length === 0) return;\n\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n const next = (activeIndex() + 1) % visible.length;\n setActiveIndex(next);\n updateActiveAttribute(next);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n const prev = (activeIndex() - 1 + visible.length) % visible.length;\n setActiveIndex(prev);\n updateActiveAttribute(prev);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n const current = visible[activeIndex()];\n if (current) {\n current.click();\n }\n }\n };\n\n const ctx: CommandContextValue = {\n search,\n setSearch: (val) => {\n setSearch(val);\n setActiveIndex(0);\n },\n activeIndex,\n setActiveIndex: (fn) => setActiveIndex(fn),\n registerItemIndex,\n onItemSelect,\n };\n\n return (\n <CommandContext.Provider value={ctx}>\n <div\n ref={containerRef}\n onKeyDown={handleKeyDown}\n class={cn(\n \"flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md outline-none\",\n local.class\n )}\n tabIndex={0}\n {...rest}\n >\n {typeof local.children === \"function\" ? (local.children as any)(ctx) : local.children}\n </div>\n </CommandContext.Provider>\n );\n};\n\n/* --- Command Dialog (Modal) --- */\nexport interface CommandDialogProps {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n enableHotkey?: boolean;\n children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);\n class?: string;\n}\n\nexport const CommandDialog: Component<CommandDialogProps> = (props) => {\n const [internalOpen, setInternalOpen] = createSignal(false);\n\n const isOpen = () => (props.open !== undefined ? props.open : internalOpen());\n const setOpen = (val: boolean) => {\n if (props.open === undefined) setInternalOpen(val);\n if (typeof props.onOpenChange === \"function\") props.onOpenChange(val);\n };\n\n /* Listen for global Ctrl+K / Cmd+K hotkeys */\n createKeybindings(\n [\n {\n key: [\"meta+k\", \"ctrl+k\"],\n handler: () => setOpen(!isOpen()),\n preventDefault: true,\n },\n ],\n {\n enabled: () => props.enableHotkey !== false,\n }\n );\n\n return (\n <Dialog open={isOpen()} onOpenChange={setOpen}>\n <Dialog.Portal>\n <Dialog.Overlay class=\"fixed inset-0 z-50 bg-black/60 backdrop-blur-xs data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\" />\n <div class=\"fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4\">\n <Dialog.Content class=\"w-full max-w-xl rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl outline-none data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-closed:zoom-out-95 data-expanded:zoom-in-95\">\n <Command class={props.class}>{props.children}</Command>\n </Dialog.Content>\n </div>\n </Dialog.Portal>\n </Dialog>\n );\n};\n\n/* --- Command Input --- */\nexport interface CommandInputProps\n extends JSX.InputHTMLAttributes<HTMLInputElement> {\n class?: string;\n}\n\nexport const CommandInput: Component<CommandInputProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"value\", \"onInput\"]);\n const { search, setSearch } = useCommand();\n\n return (\n <InputGroup class=\"border-0 border-b border-border rounded-none bg-transparent px-3 py-1 shadow-none focus-within:ring-0 focus-within:border-border\">\n <InputGroupAddon align=\"inline-start\">\n <Search class=\"w-4 h-4 text-muted-foreground\" />\n </InputGroupAddon>\n\n <InputGroupInput\n ref={(el) => setTimeout(() => el.focus(), 50)}\n value={search()}\n onInput={(e) => {\n setSearch(e.currentTarget.value);\n if (typeof local.onInput === \"function\") {\n local.onInput(e);\n }\n }}\n placeholder=\"Type a command or search...\"\n class=\"h-11 text-base sm:text-sm font-medium\"\n {...rest}\n />\n </InputGroup>\n );\n};\n\n/* --- Command List Container --- */\nexport interface CommandListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandList: Component<CommandListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ScrollArea\n class={cn(\"max-h-82.5 p-1.5\", local.class)}\n {...rest}\n >\n <List>{local.children}</List>\n </ScrollArea>\n );\n};\n\n/* --- Command Empty Block --- */\nexport interface CommandEmptyProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandEmpty: Component<CommandEmptyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { search } = useCommand();\n\n return (\n <Show when={search().trim().length > 0}>\n <div\n class={cn(\n \"py-8 text-center text-sm text-muted-foreground font-medium select-none\",\n local.class\n )}\n {...rest}\n >\n {local.children || `No results found for \"${search()}\".`}\n </div>\n </Show>\n );\n};\n\n/* --- Command Group --- */\nexport interface CommandGroupProps {\n heading: string;\n children?: JSX.Element;\n class?: string;\n}\n\nexport const CommandGroup: Component<CommandGroupProps> = (props) => {\n return (\n <ListGroup class={props.class}>\n <ListHeader title={props.heading} />\n {props.children}\n </ListGroup>\n );\n};\n\n/* --- Command Item --- */\nexport interface CommandItemProps extends ListItemProps {\n keywords?: string[];\n onSelect?: () => void;\n}\n\nexport const CommandItem: Component<CommandItemProps> = (props) => {\n let itemRef: HTMLDivElement | undefined;\n const [local, rest] = splitProps(props, [\n \"title\",\n \"subtitle\",\n \"keywords\",\n \"onSelect\",\n \"onClick\",\n \"class\",\n ]);\n const { search, activeIndex } = useCommand();\n\n /* Auto fuzzy-filter item based on search query */\n const matchesSearch = () => {\n const query = search().toLowerCase().trim();\n if (!query) return true;\n\n const titleMatch = local.title?.toLowerCase().includes(query);\n const subtitleMatch = local.subtitle?.toLowerCase().includes(query);\n const keywordMatch = local.keywords?.some((k) =>\n k.toLowerCase().includes(query)\n );\n\n return Boolean(titleMatch || subtitleMatch || keywordMatch);\n };\n\n const handleClick = (e: MouseEvent) => {\n if (typeof local.onSelect === \"function\") {\n local.onSelect();\n }\n if (typeof local.onClick === \"function\") {\n local.onClick(e as any);\n }\n };\n\n return (\n <Show when={matchesSearch()}>\n <ListItem\n ref={itemRef}\n data-command-item=\"true\"\n title={local.title}\n subtitle={local.subtitle}\n onClick={handleClick}\n class={cn(\n \"aria-selected:bg-accent aria-selected:text-accent-foreground cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground\",\n local.class\n )}\n {...rest}\n />\n </Show>\n );\n};\n\n/* --- Command Footer --- */\nexport interface CommandFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandFooter: Component<CommandFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between border-t border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground select-none\",\n local.class\n )}\n {...rest}\n >\n <div class=\"flex items-center gap-3\">\n <span class=\"flex items-center gap-1\">\n <KbdGroup>\n <Kbd size=\"sm\"><ArrowUp class=\"w-2.5 h-2.5\" /></Kbd>\n <Kbd size=\"sm\"><ArrowDown class=\"w-2.5 h-2.5\" /></Kbd>\n </KbdGroup>\n <span>Navigate</span>\n </span>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\"><CornerDownLeft class=\"w-2.5 h-2.5\" /></Kbd>\n <span>Select</span>\n </span>\n </div>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\">Esc</Kbd>\n <span>Close</span>\n </span>\n </div>\n );\n};",
|
|
23
24
|
"type": "registry:ui"
|
|
24
25
|
}
|
|
25
26
|
]
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
{
|
|
19
19
|
"path": "ui/context-menu.tsx",
|
|
20
|
-
"content": "import { splitProps, type Component, type JSX, type ValidComponent } from \"solid-js\";\nimport * as ContextMenuPrimitive from \"@kobalte/core/context-menu\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"
|
|
20
|
+
"content": "import { splitProps, type Component, type JSX, type ValidComponent } from \"solid-js\";\nimport * as ContextMenuPrimitive from \"@kobalte/core/context-menu\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"@/hooks/create-click-outside\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Kbd } from \"@/components/ui/kbd\";\nimport { cn } from \"@/lib/cn\";\n\nexport const ContextMenu = ContextMenuPrimitive.Root;\nexport const ContextMenuTrigger = ContextMenuPrimitive.Trigger;\nexport const ContextMenuGroup = ContextMenuPrimitive.Group;\nexport const ContextMenuPortal = ContextMenuPrimitive.Portal;\nexport const ContextMenuSub = ContextMenuPrimitive.Sub;\nexport const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;\n\nexport type ContextMenuSubTriggerProps<T extends ValidComponent = \"div\"> =\n ContextMenuPrimitive.ContextMenuSubTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n inset?: boolean;\n };\n\nexport const ContextMenuSubTrigger = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, ContextMenuSubTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as ContextMenuSubTriggerProps, [\"class\", \"children\", \"inset\"]);\n\n return (\n <ContextMenuPrimitive.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 </ContextMenuPrimitive.SubTrigger>\n );\n};\n\nexport type ContextMenuSubContentProps<T extends ValidComponent = \"div\"> =\n ContextMenuPrimitive.ContextMenuSubContentProps<T> & {\n class?: string;\n };\n\nexport const ContextMenuSubContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, ContextMenuSubContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as ContextMenuSubContentProps, [\"class\"]);\n\n return (\n <ContextMenuPrimitive.Portal>\n <ContextMenuPrimitive.SubContent\n class={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md transition-all animate-in fade-in-80 slide-in-from-top-1\",\n local.class\n )}\n {...(rest as any)}\n />\n </ContextMenuPrimitive.Portal>\n );\n};\n\nexport type ContextMenuContentProps<T extends ValidComponent = \"div\"> =\n ContextMenuPrimitive.ContextMenuContentProps<T> & {\n class?: string;\n ref?: (el: HTMLElement) => void;\n children?: JSX.Element;\n };\n\nexport const ContextMenuContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, ContextMenuContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as ContextMenuContentProps, [\"class\", \"ref\", \"children\"]);\n let menuRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => menuRef,\n onInteractOutside: () => {\n // Automatic portal dismiss on outside click\n },\n });\n\n return (\n <ContextMenuPrimitive.Portal>\n <ContextMenuPrimitive.Content\n ref={(el) => {\n menuRef = el;\n if (typeof local.ref === \"function\") local.ref(el);\n }}\n class={cn(\n \"z-50 min-w-[8rem] rounded-md border border-border bg-popover text-popover-foreground shadow-md transition-all animate-in fade-in-80 slide-in-from-top-1 max-h-72 flex flex-col\",\n local.class\n )}\n {...(rest as any)}\n >\n <ScrollArea class=\"max-h-72 w-full rounded-[inherit]\">\n <div class=\"p-1\">\n {local.children}\n </div>\n </ScrollArea>\n </ContextMenuPrimitive.Content>\n </ContextMenuPrimitive.Portal>\n );\n};\n\nexport type ContextMenuItemProps<T extends ValidComponent = \"div\"> =\n ContextMenuPrimitive.ContextMenuItemProps<T> & {\n class?: string;\n inset?: boolean;\n };\n\nexport const ContextMenuItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, ContextMenuItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as ContextMenuItemProps, [\"class\", \"inset\"]);\n\n return (\n <ContextMenuPrimitive.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\",\n local.inset && \"pl-8\",\n local.class\n )}\n {...(rest as any)}\n />\n );\n};\n\nexport type ContextMenuCheckboxItemProps<T extends ValidComponent = \"div\"> =\n ContextMenuPrimitive.ContextMenuCheckboxItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n checked?: boolean;\n };\n\nexport const ContextMenuCheckboxItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, ContextMenuCheckboxItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as ContextMenuCheckboxItemProps, [\"class\", \"children\", \"checked\"]);\n\n return (\n <ContextMenuPrimitive.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\",\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 <ContextMenuPrimitive.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 </ContextMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </ContextMenuPrimitive.CheckboxItem>\n );\n};\n\nexport type ContextMenuRadioItemProps<T extends ValidComponent = \"div\"> =\n ContextMenuPrimitive.ContextMenuRadioItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\nexport const ContextMenuRadioItem = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, ContextMenuRadioItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as ContextMenuRadioItemProps, [\"class\", \"children\"]);\n\n return (\n <ContextMenuPrimitive.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\",\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 <ContextMenuPrimitive.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 </ContextMenuPrimitive.ItemIndicator>\n </span>\n {local.children}\n </ContextMenuPrimitive.RadioItem>\n );\n};\n\nexport interface ContextMenuLabelProps {\n class?: string;\n inset?: boolean;\n children?: JSX.Element;\n}\n\nexport const ContextMenuLabel: Component<ContextMenuLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"inset\", \"children\"]);\n\n return (\n <ContextMenuPrimitive.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 </ContextMenuPrimitive.GroupLabel>\n );\n};\n\nexport const ContextMenuSeparator: Component<{ class?: string }> = (props) => {\n return <Separator class={cn(\"-mx-1 my-1\", props.class)} />;\n};\n\nexport interface ContextMenuShortcutProps {\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ContextMenuShortcut: Component<ContextMenuShortcutProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <span class={cn(\"ml-auto pl-4 text-xs tracking-widest text-muted-foreground\", local.class)} {...rest}>\n <Kbd size=\"sm\" variant=\"outline\">\n {local.children}\n </Kbd>\n </span>\n );\n};\n",
|
|
21
21
|
"type": "registry:ui"
|
|
22
22
|
}
|
|
23
23
|
]
|
|
@@ -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-chat-scroll",
|
|
3
|
+
"title": "createChatScroll",
|
|
4
|
+
"description": "SolidJS reactive primitive for automated chat container scrolling with manual scroll-up detection",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "hooks/create-chat-scroll.ts",
|
|
9
|
+
"content": "import { createSignal, createEffect, onCleanup, onMount, type Accessor } from \"solid-js\";\n\nexport interface CreateChatScrollOptions {\n /** Target scrollable container element or accessor */\n target: HTMLElement | Accessor<HTMLElement | undefined>;\n /** Dependency accessor (e.g. messages length or content signal) that triggers auto-scroll when changed */\n trigger?: Accessor<any>;\n /** Threshold in pixels from bottom to consider the container \"at bottom\". Defaults to 40. */\n threshold?: number;\n /** Whether auto-scroll is enabled. Defaults to true. */\n enabled?: boolean | Accessor<boolean>;\n /** Scroll behavior: \"smooth\" or \"auto\". Defaults to \"smooth\". */\n behavior?: ScrollBehavior;\n}\n\nexport interface CreateChatScrollReturn {\n /** Accessor indicating whether the container is currently scrolled to the bottom */\n isAtBottom: Accessor<boolean>;\n /** Accessor indicating whether user has manually scrolled up away from bottom */\n isScrolledUp: Accessor<boolean>;\n /** Programmatically scroll container directly to the bottom */\n scrollToBottom: (options?: { smooth?: boolean }) => void;\n}\n\n/**\n * SolidJS reactive primitive for chat and streaming message auto-scrolling with user scroll detection.\n *\n * @param options Chat scroll configuration options.\n */\nexport function createChatScroll(options: CreateChatScrollOptions): CreateChatScrollReturn {\n const [isAtBottom, setIsAtBottom] = createSignal<boolean>(true);\n const isScrolledUp = () => !isAtBottom();\n\n const getElement = (): HTMLElement | undefined => {\n if (typeof options.target === \"function\") {\n return (options.target as Accessor<HTMLElement | undefined>)();\n }\n return options.target;\n };\n\n const isEnabled = () => {\n if (typeof options.enabled === \"function\") {\n return (options.enabled as Accessor<boolean>)();\n }\n return options.enabled ?? true;\n };\n\n const threshold = options.threshold ?? 40;\n\n const checkIfAtBottom = () => {\n const el = getElement();\n if (!el) return true;\n const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;\n return distanceToBottom <= threshold;\n };\n\n const scrollToBottom = (opts?: { smooth?: boolean }) => {\n const el = getElement();\n if (!el) return;\n\n const useSmooth = opts?.smooth ?? (options.behavior === \"smooth\" || options.behavior === undefined);\n\n el.scrollTo({\n top: el.scrollHeight,\n behavior: useSmooth ? \"smooth\" : \"auto\",\n });\n setIsAtBottom(true);\n };\n\n const handleScroll = () => {\n const atBottom = checkIfAtBottom();\n setIsAtBottom(atBottom);\n };\n\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const el = getElement();\n if (el) {\n el.addEventListener(\"scroll\", handleScroll, { passive: true });\n setIsAtBottom(checkIfAtBottom());\n }\n });\n\n onCleanup(() => {\n if (typeof window === \"undefined\") return;\n const el = getElement();\n if (el) {\n el.removeEventListener(\"scroll\", handleScroll);\n }\n });\n\n // Watch trigger dependencies (e.g. messages length or stream tokens)\n if (options.trigger) {\n createEffect(() => {\n // Track trigger dependency\n options.trigger!();\n\n if (isEnabled() && isAtBottom()) {\n // Run after microtask/DOM paint\n setTimeout(() => {\n scrollToBottom({ smooth: true });\n }, 10);\n }\n });\n }\n\n return {\n isAtBottom,\n isScrolledUp,\n scrollToBottom,\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-drop-zone",
|
|
3
|
+
"title": "createDropZone",
|
|
4
|
+
"description": "SolidJS reactive primitive for file drag & drop operations, validation, and file chooser dialogs",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "hooks/create-drop-zone.ts",
|
|
9
|
+
"content": "import { createSignal, onCleanup, onMount, type Accessor } from \"solid-js\";\n\nexport interface FileRejection {\n file: File;\n errors: Array<{\n code: \"file-invalid-type\" | \"file-too-large\" | \"file-too-small\" | \"too-many-files\";\n message: string;\n }>;\n}\n\nexport interface CreateDropZoneOptions {\n /** Accepted file types: MIME types (e.g. \"image/*\", \"application/pdf\") or extensions (e.g. \".png\", \".jpg\") */\n accept?: string | string[];\n /** Maximum number of files allowed */\n maxFiles?: number;\n /** Maximum file size in bytes */\n maxSize?: number;\n /** Minimum file size in bytes */\n minSize?: number;\n /** Whether multiple files are allowed. Defaults to true */\n multiple?: boolean;\n /** Whether the drop zone is disabled */\n disabled?: boolean | Accessor<boolean>;\n /** Prevents browser default behavior of opening files dropped outside dropzone. Defaults to true */\n preventDropOnDocument?: boolean;\n /** Callback fired when valid files are dropped */\n onDrop?: (files: File[], event: DragEvent) => void;\n /** Callback fired when some or all files fail validation */\n onDropRejected?: (rejectedFiles: FileRejection[], event: DragEvent) => void;\n /** Callback fired when drag enters the dropzone */\n onDragEnter?: (event: DragEvent) => void;\n /** Callback fired when drag leaves the dropzone */\n onDragLeave?: (event: DragEvent) => void;\n /** Callback fired when dragging over the dropzone */\n onDragOver?: (event: DragEvent) => void;\n /** Callback fired whenever accepted files list changes */\n onFilesChanged?: (files: File[]) => void;\n}\n\nexport interface CreateDropZoneReturn {\n /** Whether drag operation is currently active over the target drop zone */\n isOver: Accessor<boolean>;\n /** Whether files are currently being dragged anywhere on the window */\n isDragging: Accessor<boolean>;\n /** Currently accepted dropped files */\n files: Accessor<File[]>;\n /** Currently rejected files with error details */\n rejectedFiles: Accessor<FileRejection[]>;\n /** Clear all accepted and rejected files */\n clear: () => void;\n /** Programmatically set files (e.g. from an <input type=\"file\" /> change event) */\n setFiles: (files: File[]) => void;\n /** Programmatically open the native browser file selector dialog */\n openFileDialog: () => void;\n /** Ref callback to attach to target DOM element */\n ref: (el: HTMLElement) => void;\n /** Event handler props to spread directly onto target JSX element */\n props: {\n onDragEnter: (e: DragEvent) => void;\n onDragLeave: (e: DragEvent) => void;\n onDragOver: (e: DragEvent) => void;\n onDrop: (e: DragEvent) => void;\n };\n}\n\nfunction matchesAccept(file: File, acceptList: string[]): boolean {\n if (acceptList.length === 0) return true;\n const fileName = file.name.toLowerCase();\n const fileType = file.type.toLowerCase();\n\n return acceptList.some((pattern) => {\n const p = pattern.trim().toLowerCase();\n if (p.startsWith(\".\")) {\n return fileName.endsWith(p);\n }\n if (p.endsWith(\"/*\")) {\n const typePrefix = p.slice(0, -2);\n return fileType.startsWith(typePrefix + \"/\");\n }\n return fileType === p;\n });\n}\n\nfunction validateFiles(\n incomingFiles: File[],\n options: CreateDropZoneOptions\n): { accepted: File[]; rejected: FileRejection[] } {\n const acceptList = options.accept\n ? (Array.isArray(options.accept) ? options.accept : options.accept.split(\",\"))\n .map((s) => s.trim())\n .filter(Boolean)\n : [];\n\n const maxFiles = options.maxFiles ?? (options.multiple === false ? 1 : Infinity);\n const maxSize = options.maxSize;\n const minSize = options.minSize;\n\n const accepted: File[] = [];\n const rejected: FileRejection[] = [];\n\n incomingFiles.forEach((file, index) => {\n const errors: FileRejection[\"errors\"] = [];\n\n if (index >= maxFiles) {\n errors.push({\n code: \"too-many-files\",\n message: `Maximum allowed files is ${maxFiles}.`,\n });\n }\n\n if (acceptList.length > 0 && !matchesAccept(file, acceptList)) {\n errors.push({\n code: \"file-invalid-type\",\n message: `File type \"${file.type || file.name.split(\".\").pop()}\" is not allowed.`,\n });\n }\n\n if (maxSize !== undefined && file.size > maxSize) {\n errors.push({\n code: \"file-too-large\",\n message: `File size exceeds ${(maxSize / (1024 * 1024)).toFixed(1)}MB limit.`,\n });\n }\n\n if (minSize !== undefined && file.size < minSize) {\n errors.push({\n code: \"file-too-small\",\n message: `File size is below ${(minSize / 1024).toFixed(1)}KB limit.`,\n });\n }\n\n if (errors.length > 0) {\n rejected.push({ file, errors });\n } else {\n accepted.push(file);\n }\n });\n\n return { accepted, rejected };\n}\n\n/**\n * SolidJS reactive primitive for managing file drag & drop zones with validation and file dialog support.\n *\n * @param options Configuration options for file acceptance, size limits, and callbacks.\n */\nexport function createDropZone(options: CreateDropZoneOptions = {}): CreateDropZoneReturn {\n const [isOver, setIsOver] = createSignal(false);\n const [isDragging, setIsDragging] = createSignal(false);\n const [files, setFilesInternal] = createSignal<File[]>([]);\n const [rejectedFiles, setRejectedFilesInternal] = createSignal<FileRejection[]>([]);\n\n let dragCounter = 0;\n let windowDragCounter = 0;\n let targetElement: HTMLElement | null = null;\n\n const isDisabled = () => {\n if (typeof options.disabled === \"function\") {\n return (options.disabled as Accessor<boolean>)();\n }\n return options.disabled ?? false;\n };\n\n const processFiles = (incomingFiles: File[], event: DragEvent) => {\n const { accepted, rejected } = validateFiles(incomingFiles, options);\n\n setFilesInternal(accepted);\n setRejectedFilesInternal(rejected);\n\n if (accepted.length > 0) {\n options.onDrop?.(accepted, event);\n options.onFilesChanged?.(accepted);\n }\n\n if (rejected.length > 0) {\n options.onDropRejected?.(rejected, event);\n }\n };\n\n const onDragEnter = (e: DragEvent) => {\n if (isDisabled()) return;\n e.preventDefault();\n dragCounter++;\n if (dragCounter === 1) {\n setIsOver(true);\n options.onDragEnter?.(e);\n }\n };\n\n const onDragOver = (e: DragEvent) => {\n if (isDisabled()) return;\n e.preventDefault();\n if (e.dataTransfer) {\n e.dataTransfer.dropEffect = \"copy\";\n }\n options.onDragOver?.(e);\n };\n\n const onDragLeave = (e: DragEvent) => {\n if (isDisabled()) return;\n e.preventDefault();\n dragCounter--;\n if (dragCounter <= 0) {\n dragCounter = 0;\n setIsOver(false);\n options.onDragLeave?.(e);\n }\n };\n\n const onDrop = (e: DragEvent) => {\n if (isDisabled()) return;\n e.preventDefault();\n dragCounter = 0;\n setIsOver(false);\n\n if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {\n processFiles(Array.from(e.dataTransfer.files), e);\n }\n };\n\n const clear = () => {\n setFilesInternal([]);\n setRejectedFilesInternal([]);\n options.onFilesChanged?.([]);\n };\n\n const setFiles = (newFiles: File[]) => {\n const dummyEvent = new Event(\"drop\") as unknown as DragEvent;\n processFiles(newFiles, dummyEvent);\n };\n\n const openFileDialog = () => {\n if (typeof document === \"undefined\" || isDisabled()) return;\n const input = document.createElement(\"input\");\n input.type = \"file\";\n if (options.multiple !== false && (options.maxFiles === undefined || options.maxFiles > 1)) {\n input.multiple = true;\n }\n if (options.accept) {\n input.accept = Array.isArray(options.accept) ? options.accept.join(\",\") : options.accept;\n }\n input.onchange = (e) => {\n const target = e.target as HTMLInputElement;\n if (target.files && target.files.length > 0) {\n processFiles(Array.from(target.files), e as unknown as DragEvent);\n }\n };\n input.click();\n };\n\n const ref = (el: HTMLElement) => {\n targetElement = el;\n };\n\n // Window-level drag detection and document drop prevention\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const handleWindowDragEnter = (e: DragEvent) => {\n windowDragCounter++;\n if (windowDragCounter === 1) {\n setIsDragging(true);\n }\n };\n\n const handleWindowDragLeave = (e: DragEvent) => {\n windowDragCounter--;\n if (windowDragCounter <= 0) {\n windowDragCounter = 0;\n setIsDragging(false);\n }\n };\n\n const handleWindowDrop = (e: DragEvent) => {\n windowDragCounter = 0;\n setIsDragging(false);\n if (options.preventDropOnDocument !== false) {\n e.preventDefault();\n }\n };\n\n const handleWindowDragOver = (e: DragEvent) => {\n if (options.preventDropOnDocument !== false) {\n e.preventDefault();\n }\n };\n\n window.addEventListener(\"dragenter\", handleWindowDragEnter);\n window.addEventListener(\"dragleave\", handleWindowDragLeave);\n window.addEventListener(\"dragover\", handleWindowDragOver);\n window.addEventListener(\"drop\", handleWindowDrop);\n\n onCleanup(() => {\n window.removeEventListener(\"dragenter\", handleWindowDragEnter);\n window.removeEventListener(\"dragleave\", handleWindowDragLeave);\n window.removeEventListener(\"dragover\", handleWindowDragOver);\n window.removeEventListener(\"drop\", handleWindowDrop);\n });\n });\n\n return {\n isOver,\n isDragging,\n files,\n rejectedFiles,\n clear,\n setFiles,\n openFileDialog,\n ref,\n props: {\n onDragEnter,\n onDragLeave,\n onDragOver,\n onDrop,\n },\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-pagination",
|
|
3
|
+
"title": "createPagination",
|
|
4
|
+
"description": "SolidJS reactive primitive for computing pagination state, dynamic page ranges with ellipses, and navigation controls",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "hooks/create-pagination.ts",
|
|
9
|
+
"content": "import { createSignal, createMemo, type Accessor } from \"solid-js\";\n\nexport interface CreatePaginationOptions {\n /** Total number of items across all pages, or total count. */\n count?: number | Accessor<number>;\n /** Explicit total number of pages. If provided, overrides count / pageSize calculation. */\n totalPages?: number | Accessor<number>;\n /** Controlled active page number (1-indexed). */\n page?: number | Accessor<number>;\n /** Default active page number for uncontrolled state. Defaults to 1. */\n defaultPage?: number;\n /** Number of items per page. Defaults to 10. */\n pageSize?: number | Accessor<number>;\n /** Number of sibling page buttons visible on each side of the current active page. Defaults to 1. */\n siblingCount?: number | Accessor<number>;\n /** Number of boundary pages visible at the beginning and end. Defaults to 1. */\n boundaries?: number | Accessor<number>;\n /** Callback fired whenever the active page changes. */\n onChange?: (page: number) => void;\n}\n\nexport interface CreatePaginationReturn {\n /** Accessor returning the current active page number (1-indexed). */\n page: Accessor<number>;\n /** Accessor returning the calculated total number of pages. */\n totalPages: Accessor<number>;\n /** Accessor returning the active page size. */\n pageSize: Accessor<number>;\n /** Accessor returning an array of page numbers and \"ellipsis\" strings. */\n range: Accessor<(number | \"ellipsis\")[]>;\n /** Function to programmatically change to a specific page number. */\n setPage: (page: number) => void;\n /** Function to navigate to the next page. */\n next: () => void;\n /** Function to navigate to the previous page. */\n previous: () => void;\n /** Function to navigate to the first page (1). */\n first: () => void;\n /** Function to navigate to the last page (totalPages). */\n last: () => void;\n /** Accessor indicating whether a next page exists. */\n hasNext: Accessor<boolean>;\n /** Accessor indicating whether a previous page exists. */\n hasPrevious: Accessor<boolean>;\n /** 1-based start index of items on the current page (e.g. 1 for page 1 with pageSize 10). */\n startIndex: Accessor<number>;\n /** 1-based end index of items on the current page (e.g. 10 for page 1 with pageSize 10). */\n endIndex: Accessor<number>;\n}\n\n/**\n * SolidJS reactive primitive for computing pagination state, dynamic page range with ellipses, and navigation helpers.\n *\n * @param options Pagination configuration options.\n */\nexport function createPagination(options: CreatePaginationOptions = {}): CreatePaginationReturn {\n const getCount = () => {\n const raw = typeof options.count === \"function\" ? options.count() : options.count ?? 0;\n return Math.max(0, raw);\n };\n\n const getPageSize = () => {\n const raw = typeof options.pageSize === \"function\" ? options.pageSize() : options.pageSize ?? 10;\n return Math.max(1, raw);\n };\n\n const getExplicitTotalPages = () => {\n const raw = typeof options.totalPages === \"function\" ? options.totalPages() : options.totalPages;\n return raw !== undefined ? Math.max(1, raw) : undefined;\n };\n\n const getSiblingCount = () => {\n const raw = typeof options.siblingCount === \"function\" ? options.siblingCount() : options.siblingCount ?? 1;\n return Math.max(0, raw);\n };\n\n const getBoundaries = () => {\n const raw = typeof options.boundaries === \"function\" ? options.boundaries() : options.boundaries ?? 1;\n return Math.max(0, raw);\n };\n\n const [internalPage, setInternalPage] = createSignal<number>(Math.max(1, options.defaultPage ?? 1));\n\n const totalPages = createMemo<number>(() => {\n const explicit = getExplicitTotalPages();\n if (explicit !== undefined) {\n return Math.max(1, explicit);\n }\n const count = getCount();\n const size = getPageSize();\n return Math.max(1, Math.ceil(count / size));\n });\n\n const rawPage = () => {\n if (typeof options.page === \"function\") {\n return options.page();\n }\n if (typeof options.page === \"number\") {\n return options.page;\n }\n return internalPage();\n };\n\n const page = createMemo<number>(() => {\n const p = rawPage();\n const max = totalPages();\n if (p < 1) return 1;\n if (p > max) return max;\n return p;\n });\n\n const setPage = (nextPage: number) => {\n const max = totalPages();\n const clamped = Math.max(1, Math.min(nextPage, max));\n if (typeof options.page !== \"function\" && typeof options.page !== \"number\") {\n setInternalPage(clamped);\n }\n options.onChange?.(clamped);\n };\n\n const next = () => setPage(page() + 1);\n const previous = () => setPage(page() - 1);\n const first = () => setPage(1);\n const last = () => setPage(totalPages());\n\n const hasNext = createMemo(() => page() < totalPages());\n const hasPrevious = createMemo(() => page() > 1);\n\n const startIndex = createMemo(() => {\n const count = getCount();\n if (count === 0 && getExplicitTotalPages() === undefined) return 0;\n return (page() - 1) * getPageSize() + 1;\n });\n\n const endIndex = createMemo(() => {\n const count = getCount();\n const calculated = page() * getPageSize();\n if (count > 0) {\n return Math.min(calculated, count);\n }\n return calculated;\n });\n\n const range = createMemo<(number | \"ellipsis\")[]>(() => {\n const total = totalPages();\n const current = page();\n const siblings = getSiblingCount();\n const boundaries = getBoundaries();\n\n if (total <= 1) {\n return [1];\n }\n\n const pagesSet = new Set<number>();\n\n // 1. Boundary pages at the start\n for (let i = 1; i <= Math.min(boundaries, total); i++) {\n pagesSet.add(i);\n }\n\n // 2. Sibling pages around current page\n const leftSibling = Math.max(1, current - siblings);\n const rightSibling = Math.min(total, current + siblings);\n for (let i = leftSibling; i <= rightSibling; i++) {\n pagesSet.add(i);\n }\n\n // 3. Boundary pages at the end\n for (let i = Math.max(1, total - boundaries + 1); i <= total; i++) {\n pagesSet.add(i);\n }\n\n const sortedPages = Array.from(pagesSet).sort((a, b) => a - b);\n const result: (number | \"ellipsis\")[] = [];\n\n for (let i = 0; i < sortedPages.length; i++) {\n const currentPageNum = sortedPages[i];\n if (i > 0) {\n const prevPageNum = sortedPages[i - 1];\n const gap = currentPageNum - prevPageNum;\n if (gap === 2) {\n result.push(prevPageNum + 1);\n } else if (gap > 2) {\n result.push(\"ellipsis\");\n }\n }\n result.push(currentPageNum);\n }\n\n return result;\n });\n\n return {\n page,\n totalPages,\n pageSize: getPageSize,\n range,\n setPage,\n next,\n previous,\n first,\n last,\n hasNext,\n hasPrevious,\n startIndex,\n endIndex,\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. */\n close: () => 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 Tauri 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 = (navigator as any).userAgentData?.platform?.toLowerCase() || navigator.platform?.toLowerCase() || \"\";\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 return (\n \"__TAURI_INTERNALS__\" in window ||\n \"__TAURI__\" in window ||\n \"__TAURI_PATTERN__\" in window ||\n Boolean((window as any).__TAURI_METADATA__)\n );\n}\n\n/**\n * SolidJS reactive primitive for controlling and observing Tauri desktop window states.\n * Provides seamless fallbacks for web environments and interactive previews.\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 try {\n if ((window as any).__TAURI__?.window?.getCurrentWindow) {\n return (window as any).__TAURI__.window.getCurrentWindow();\n }\n if ((window as any).__TAURI__?.window?.appWindow) {\n return (window as any).__TAURI__.window.appWindow;\n }\n // Variable prevents Vite dev server from static module resolution\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 } 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 tauriWin.isMaximized ? tauriWin.isMaximized() : false,\n tauriWin.isMinimized ? tauriWin.isMinimized() : false,\n tauriWin.isFullscreen ? tauriWin.isFullscreen() : false,\n tauriWin.isFocused ? tauriWin.isFocused() : true,\n ]);\n setIsMaximized(Boolean(max));\n setIsMinimized(Boolean(min));\n setIsFullscreen(Boolean(full));\n setIsFocused(Boolean(foc));\n } catch {\n // Fallback gracefully\n }\n } else {\n setIsTauri(false);\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\n window.addEventListener(\"focus\", handleWindowFocus);\n window.addEventListener(\"blur\", handleWindowBlur);\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 document.removeEventListener(\"fullscreenchange\", handleFullscreenChange);\n if (unlistenResize) unlistenResize();\n if (unlistenFocus) unlistenFocus();\n });\n }\n });\n\n const minimize = async (): Promise<void> => {\n const tauriWin = await getTauriWindow();\n if (tauriWin?.minimize) {\n await tauriWin.minimize();\n setIsMinimized(true);\n } else {\n setIsMinimized((prev) => !prev);\n if (!isMinimized()) {\n setIsMaximized(false);\n }\n }\n };\n\n const maximize = async (): Promise<void> => {\n const tauriWin = await getTauriWindow();\n if (tauriWin?.maximize) {\n await tauriWin.maximize();\n setIsMaximized(true);\n setIsMinimized(false);\n } else {\n setIsMaximized(true);\n setIsMinimized(false);\n }\n };\n\n const unmaximize = async (): Promise<void> => {\n const tauriWin = await getTauriWindow();\n if (tauriWin?.unmaximize) {\n await tauriWin.unmaximize();\n setIsMaximized(false);\n } else {\n setIsMaximized(false);\n }\n };\n\n const toggleMaximize = async (): Promise<void> => {\n const tauriWin = await getTauriWindow();\n if (tauriWin?.toggleMaximize) {\n await tauriWin.toggleMaximize();\n setIsMaximized((prev) => !prev);\n setIsMinimized(false);\n } else {\n setIsMaximized((prev) => !prev);\n setIsMinimized(false);\n }\n };\n\n const close = async (): Promise<void> => {\n const tauriWin = await getTauriWindow();\n if (tauriWin?.close) {\n await tauriWin.close();\n } else if (typeof window !== \"undefined\") {\n window.close();\n }\n };\n\n const setFullscreen = async (fullscreen: boolean): Promise<void> => {\n const tauriWin = await getTauriWindow();\n if (tauriWin?.setFullscreen) {\n await tauriWin.setFullscreen(fullscreen);\n setIsFullscreen(fullscreen);\n } else if (typeof document !== \"undefined\") {\n if (fullscreen && document.documentElement.requestFullscreen) {\n await document.documentElement.requestFullscreen();\n } else if (!fullscreen && document.fullscreenElement && document.exitFullscreen) {\n await document.exitFullscreen();\n }\n setIsFullscreen(fullscreen);\n }\n };\n\n const startDragging = async (e?: MouseEvent): Promise<void> => {\n if (e && e.button !== 0) return; // Only primary mouse button starts drag\n try {\n const tauriWin = await getTauriWindow();\n if (tauriWin?.startDragging) {\n await tauriWin.startDragging();\n }\n } catch {\n // Silently fall back if Tauri capability is not yet enabled\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 setFullscreen,\n startDragging,\n };\n}\n",
|
|
10
|
+
"type": "registry:hook"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|