@nikala-ui/core 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +97 -20
- package/package.json +43 -2
- package/registry/api-table.json +21 -0
- package/registry/banner.json +1 -1
- package/registry/button.json +1 -1
- package/registry/callout.json +19 -0
- package/registry/checkbox.json +4 -1
- package/registry/code-block.json +26 -0
- package/registry/code-group.json +20 -0
- package/registry/combobox.json +1 -1
- package/registry/command.json +3 -2
- package/registry/component-viewer.json +25 -0
- package/registry/container.json +18 -0
- package/registry/context-menu.json +1 -1
- package/registry/create-app-updater.json +13 -0
- package/registry/create-document-tabs.json +13 -0
- package/registry/create-global-shortcut.json +13 -0
- package/registry/create-tauri-window.json +13 -0
- package/registry/create-tiptap-editor.json +34 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +3 -2
- package/registry/field.json +1 -1
- package/registry/file-tree.json +21 -0
- package/registry/footer.json +1 -1
- package/registry/form-message.json +3 -2
- package/registry/icon-button.json +1 -1
- package/registry/index.json +336 -5
- package/registry/navbar.json +1 -1
- package/registry/navigation-menu.json +1 -1
- package/registry/number-input.json +1 -1
- package/registry/package-manager-tabs.json +24 -0
- package/registry/pager.json +21 -0
- package/registry/popover.json +4 -1
- package/registry/resizable.json +1 -1
- package/registry/rich-text-editor.json +95 -0
- package/registry/scroll-area.json +1 -1
- package/registry/section-heading.json +21 -0
- package/registry/select.json +3 -2
- package/registry/sheet.json +1 -1
- package/registry/sidebar.json +1 -1
- package/registry/status.json +1 -1
- package/registry/steps.json +20 -0
- package/registry/switch.json +4 -1
- package/registry/table-of-contents.json +23 -0
- package/registry/tabs.json +4 -1
- package/registry/theme-manager.json +6 -5
- package/registry/titlebar-tabs.json +24 -0
- package/registry/titlebar.json +26 -0
- package/registry/toggle-group.json +1 -1
- package/registry/updater-modal.json +26 -0
- package/src/index.ts +48 -0
- package/src/registry/components/ui/api-table.tsx +117 -0
- package/src/registry/components/ui/banner.tsx +0 -2
- package/src/registry/components/ui/button.tsx +1 -1
- package/src/registry/components/ui/callout.tsx +137 -0
- package/src/registry/components/ui/checkbox.tsx +1 -1
- package/src/registry/components/ui/code-block.tsx +326 -0
- package/src/registry/components/ui/code-group.tsx +105 -0
- package/src/registry/components/ui/combobox.tsx +83 -18
- package/src/registry/components/ui/command.tsx +110 -106
- package/src/registry/components/ui/component-viewer.tsx +363 -0
- package/src/registry/components/ui/container.tsx +45 -0
- package/src/registry/components/ui/context-menu.tsx +67 -22
- package/src/registry/components/ui/dialog.tsx +42 -32
- package/src/registry/components/ui/dropdown-menu.tsx +69 -31
- package/src/registry/components/ui/field.tsx +7 -3
- package/src/registry/components/ui/file-tree.tsx +181 -0
- package/src/registry/components/ui/footer.tsx +1 -1
- package/src/registry/components/ui/form-message.tsx +2 -2
- package/src/registry/components/ui/icon-button.tsx +1 -1
- package/src/registry/components/ui/navbar.tsx +18 -11
- package/src/registry/components/ui/navigation-menu.tsx +2 -2
- package/src/registry/components/ui/number-input.tsx +3 -3
- package/src/registry/components/ui/package-manager-tabs.tsx +226 -0
- package/src/registry/components/ui/pager.tsx +102 -0
- package/src/registry/components/ui/popover.tsx +1 -1
- package/src/registry/components/ui/resizable.tsx +3 -1
- package/src/registry/components/ui/rich-text-editor/bubble-menu.tsx +236 -0
- package/src/registry/components/ui/rich-text-editor/editor.tsx +308 -0
- package/src/registry/components/ui/rich-text-editor/extensions.ts +116 -0
- package/src/registry/components/ui/rich-text-editor/footer.tsx +36 -0
- package/src/registry/components/ui/rich-text-editor/image-dialog.tsx +75 -0
- package/src/registry/components/ui/rich-text-editor/index.ts +11 -0
- package/src/registry/components/ui/rich-text-editor/link-dialog.tsx +48 -0
- package/src/registry/components/ui/rich-text-editor/markdown.ts +170 -0
- package/src/registry/components/ui/rich-text-editor/slash-command.tsx +301 -0
- package/src/registry/components/ui/rich-text-editor/table-controls.tsx +68 -0
- package/src/registry/components/ui/rich-text-editor/toolbar.tsx +465 -0
- package/src/registry/components/ui/rich-text-editor/use-editor.ts +231 -0
- package/src/registry/components/ui/scroll-area.tsx +12 -2
- package/src/registry/components/ui/section-heading.tsx +108 -0
- package/src/registry/components/ui/select.tsx +16 -19
- package/src/registry/components/ui/sheet.tsx +58 -53
- package/src/registry/components/ui/sidebar.tsx +4 -4
- package/src/registry/components/ui/status.tsx +7 -5
- package/src/registry/components/ui/steps.tsx +198 -0
- package/src/registry/components/ui/switch.tsx +1 -1
- package/src/registry/components/ui/table-of-contents.tsx +131 -0
- package/src/registry/components/ui/tabs.tsx +1 -1
- package/src/registry/components/ui/theme-toggle.tsx +175 -139
- package/src/registry/components/ui/titlebar-tabs.tsx +773 -0
- package/src/registry/components/ui/titlebar.tsx +468 -0
- package/src/registry/components/ui/toggle-group.tsx +1 -1
- package/src/registry/components/ui/updater-modal.tsx +254 -0
- package/src/registry/metadata.ts +167 -6
- package/src/registry/providers/theme-provider.tsx +16 -8
- package/src/registry/providers/theme-script.tsx +29 -9
- package/src/registry/providers/theme-transitions.ts +81 -50
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Giorgi Magradze
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO QUALITY AND FITNESS FOR A PARTICULAR
|
|
17
|
+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
18
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
19
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,41 +1,118 @@
|
|
|
1
1
|
# @nikala-ui/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@nikala-ui/core` is the internal source registry for Nikala UI. It contains the SolidJS component sources, reactive primitive manifests, application blocks, providers, metadata, and generated registry JSON used to build and publish the Nikala registry.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Nikala UI is a copy-paste component system built for SolidJS and Tailwind CSS v4. The package honors the work of Georgian painter Niko Pirosmani (Nikala).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Documentation and interactive examples: [nikala.dev](https://nikala.dev)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Role of the package
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The core package is not a standalone application dependency. It is the canonical registry source used inside the Nikala UI monorepo, by registry generation, by the official documentation site, and by release tooling.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
End users should not install or import `@nikala-ui/core` directly. The public entry point for adding Nikala UI to an application is `@nikala-ui/cli`.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The normal consumer workflow is:
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
```bash
|
|
18
|
+
bunx @nikala-ui/cli init
|
|
19
|
+
bunx @nikala-ui/cli add button dialog
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This copies editable files into the local project, usually under:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
src/components/ui/
|
|
26
|
+
src/hooks/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Applications own the copied source files. They do not import `@nikala-ui/core` at runtime to use the generated components.
|
|
30
|
+
|
|
31
|
+
## Registry contents
|
|
32
|
+
|
|
33
|
+
The registry contains three item types:
|
|
34
|
+
|
|
35
|
+
- `registry:ui` for SolidJS UI components.
|
|
36
|
+
- `registry:hook` for reactive SolidJS primitives.
|
|
37
|
+
- `registry:block` for ready-to-use application and marketing sections.
|
|
38
|
+
|
|
39
|
+
Each registry item is represented by a JSON manifest in `registry/`. A manifest contains the item metadata, source files, internal registry dependencies, and required npm dependencies.
|
|
40
|
+
|
|
41
|
+
The package also includes the source snapshot used to generate those manifests:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
packages/core/
|
|
45
|
+
├── registry/
|
|
46
|
+
│ ├── index.json
|
|
47
|
+
│ └── *.json
|
|
48
|
+
├── src/
|
|
49
|
+
│ ├── index.ts
|
|
50
|
+
│ ├── index.css
|
|
51
|
+
│ ├── lib/
|
|
52
|
+
│ └── registry/
|
|
53
|
+
│ ├── components/ui/
|
|
54
|
+
│ ├── blocks/
|
|
55
|
+
│ ├── providers/
|
|
56
|
+
│ ├── index.ts
|
|
57
|
+
│ └── metadata.ts
|
|
58
|
+
└── scripts/
|
|
59
|
+
└── build-registry.ts
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Registry manifests
|
|
63
|
+
|
|
64
|
+
The central registry index is available at:
|
|
18
65
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
66
|
+
```text
|
|
67
|
+
registry/index.json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Individual items can be inspected directly:
|
|
23
71
|
|
|
24
|
-
|
|
72
|
+
```text
|
|
73
|
+
registry/button.json
|
|
74
|
+
registry/create-clipboard.json
|
|
75
|
+
registry/hero-01.json
|
|
76
|
+
```
|
|
25
77
|
|
|
26
|
-
|
|
78
|
+
The manifest format allows the CLI to install only the requested item and its transitive registry dependencies. For example, a component can declare another component or hook in `registryDependencies`, while external packages are listed separately as npm dependencies.
|
|
27
79
|
|
|
28
|
-
|
|
80
|
+
## Source ownership
|
|
81
|
+
|
|
82
|
+
Source files are written for local ownership rather than as an opaque runtime UI library. After installation, developers can customize component behavior, markup, accessibility details, and styles directly in their project.
|
|
83
|
+
|
|
84
|
+
Generated sources follow Nikala UI conventions:
|
|
85
|
+
|
|
86
|
+
- SolidJS props are handled with `splitProps` to preserve reactivity.
|
|
87
|
+
- Components use semantic Tailwind CSS v4 design tokens.
|
|
88
|
+
- Browser-only behavior is guarded for SSR environments.
|
|
89
|
+
- Kobalte and Corvu are used where headless behavior and accessibility primitives are required.
|
|
90
|
+
- Components use local aliases such as `@/components/ui/*`, `@/hooks/*`, and `@/lib/*`.
|
|
91
|
+
|
|
92
|
+
## Registry generation
|
|
93
|
+
|
|
94
|
+
When component, hook, block, or metadata sources change, regenerate the manifests from the monorepo root:
|
|
29
95
|
|
|
30
96
|
```bash
|
|
31
|
-
|
|
97
|
+
bun run build:registry
|
|
32
98
|
```
|
|
33
99
|
|
|
34
|
-
|
|
100
|
+
This runs:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd packages/core
|
|
104
|
+
bun run build:registry
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The generator scans the source directories, creates item manifests, writes `registry/index.json`, and synchronizes the official web application's copied sources when that workspace is present.
|
|
108
|
+
|
|
109
|
+
The registry must be regenerated before publishing changes to the core package. CI verifies that generated registry files are synchronized with the source tree.
|
|
110
|
+
|
|
111
|
+
## Related packages
|
|
35
112
|
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
113
|
+
- `@nikala-ui/cli` installs and updates registry items in a consuming project.
|
|
114
|
+
- `@nikala-ui/hooks` publishes the standalone reactive primitives package.
|
|
115
|
+
- `@nikala-ui/docs` bundles registry and source snapshots to initialize independent documentation projects without requiring `@nikala-ui/core` at runtime.
|
|
39
116
|
|
|
40
117
|
## License
|
|
41
118
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nikala-ui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Core component definitions, design tokens, and registry for Nikala UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -8,6 +8,27 @@
|
|
|
8
8
|
"registry",
|
|
9
9
|
"src"
|
|
10
10
|
],
|
|
11
|
+
"main": "./src/index.ts",
|
|
12
|
+
"module": "./src/index.ts",
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.ts",
|
|
17
|
+
"import": "./src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"./ui/*": {
|
|
20
|
+
"types": "./src/registry/components/ui/*.tsx",
|
|
21
|
+
"import": "./src/registry/components/ui/*.tsx"
|
|
22
|
+
},
|
|
23
|
+
"./lib/*": {
|
|
24
|
+
"types": "./src/lib/*.ts",
|
|
25
|
+
"import": "./src/lib/*.ts"
|
|
26
|
+
},
|
|
27
|
+
"./providers/*": {
|
|
28
|
+
"types": "./src/registry/providers/*.tsx",
|
|
29
|
+
"import": "./src/registry/providers/*.tsx"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
11
32
|
"publishConfig": {
|
|
12
33
|
"access": "public"
|
|
13
34
|
},
|
|
@@ -32,6 +53,26 @@
|
|
|
32
53
|
},
|
|
33
54
|
"dependencies": {
|
|
34
55
|
"@nikala-ui/hooks": "workspace:*",
|
|
35
|
-
"
|
|
56
|
+
"@tiptap/core": "^3.31.0",
|
|
57
|
+
"@tiptap/extension-character-count": "^3.31.0",
|
|
58
|
+
"@tiptap/extension-highlight": "^3.31.0",
|
|
59
|
+
"@tiptap/extension-image": "^3.31.0",
|
|
60
|
+
"@tiptap/extension-link": "^3.31.0",
|
|
61
|
+
"@tiptap/extension-placeholder": "^3.31.0",
|
|
62
|
+
"@tiptap/extension-subscript": "^3.31.0",
|
|
63
|
+
"@tiptap/extension-superscript": "^3.31.0",
|
|
64
|
+
"@tiptap/extension-table": "^3.31.0",
|
|
65
|
+
"@tiptap/extension-table-cell": "^3.31.0",
|
|
66
|
+
"@tiptap/extension-table-header": "^3.31.0",
|
|
67
|
+
"@tiptap/extension-table-row": "^3.31.0",
|
|
68
|
+
"@tiptap/extension-task-item": "^3.31.0",
|
|
69
|
+
"@tiptap/extension-task-list": "^3.31.0",
|
|
70
|
+
"@tiptap/extension-text-align": "^3.31.0",
|
|
71
|
+
"@tiptap/extension-typography": "^3.31.0",
|
|
72
|
+
"@tiptap/extension-underline": "^3.31.0",
|
|
73
|
+
"@tiptap/starter-kit": "^3.31.0",
|
|
74
|
+
"lucide-solid": "^1.28.0",
|
|
75
|
+
"shiki": "^4.4.3",
|
|
76
|
+
"tiptap-markdown": "^0.9.0"
|
|
36
77
|
}
|
|
37
78
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api-table",
|
|
3
|
+
"title": "API Table",
|
|
4
|
+
"description": "A structured, clean API reference table component for documenting props, options, and events.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"table",
|
|
12
|
+
"section-heading"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "ui/api-table.tsx",
|
|
17
|
+
"content": "import {\n splitProps,\n For,\n Show,\n type JSX,\n type Component,\n} from \"solid-js\";\nimport { SectionHeading } from \"@/components/ui/section-heading\";\nimport {\n Table,\n TableHeader,\n TableBody,\n TableRow,\n TableHead,\n TableCell,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ApiTableItem {\n /** Property, attribute, or method name */\n prop: string;\n /** TypeScript type definition string */\n type: string;\n /** Default value if optional */\n default?: string;\n /** Detailed description of purpose and usage */\n description: string;\n /** Whether the property is strictly required */\n required?: boolean;\n}\n\nexport interface ApiTableProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Title header for this API section (e.g. component or interface name) */\n title?: string;\n /** Subtitle or explanatory note */\n description?: string;\n /** Badge label next to the title (defaults to \"Props\") */\n badge?: string;\n /** List of API properties to render in the table */\n items: ApiTableItem[];\n class?: string;\n}\n\n/**\n * Clean, structured API reference table component composed from Table and Badge primitives.\n */\nexport const ApiTable: Component<ApiTableProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"title\",\n \"description\",\n \"badge\",\n \"items\",\n \"class\",\n ]);\n\n return (\n <div class={cn(\"my-6 space-y-3\", local.class)} {...rest}>\n {/* Header */}\n <Show when={local.title}>\n <SectionHeading\n variant=\"compact\"\n title={local.title!}\n badge={local.badge || \"Props\"}\n description={local.description}\n />\n </Show>\n\n {/* Responsive Table Container Composed from Nikala Table Primitives */}\n <div class=\"rounded-lg border border-border bg-card/50 shadow-2xs overflow-hidden\">\n <Table class=\"text-xs\">\n <TableHeader class=\"bg-muted/40 font-mono text-[11px] uppercase tracking-wider text-muted-foreground select-none\">\n <TableRow class=\"hover:bg-transparent\">\n <TableHead class=\"h-9 px-4 font-semibold\">Prop</TableHead>\n <TableHead class=\"h-9 px-4 font-semibold\">Type</TableHead>\n <TableHead class=\"h-9 px-4 font-semibold\">Default</TableHead>\n <TableHead class=\"h-9 px-4 font-semibold\">Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody class=\"font-mono text-xs\">\n <For each={local.items}>\n {(item) => (\n <TableRow class=\"transition-colors hover:bg-muted/20\">\n <TableCell class=\"px-4 py-3 font-semibold text-primary\">\n <span class=\"inline-flex items-center gap-1\">\n {item.prop}\n <Show when={item.required}>\n <span class=\"text-rose-500 font-bold\" title=\"Required\">*</span>\n </Show>\n </span>\n </TableCell>\n <TableCell class=\"px-4 py-3 text-muted-foreground\">\n <code class=\"rounded-md bg-muted px-1.5 py-0.5 font-mono text-[11px] text-foreground\">\n {item.type}\n </code>\n </TableCell>\n <TableCell class=\"px-4 py-3 text-muted-foreground\">\n <Show\n when={item.default}\n fallback={<span class=\"text-muted-foreground/40 font-mono\">-</span>}\n >\n <code class=\"rounded-md bg-muted/60 px-1 py-0.5 font-mono text-[11px]\">\n {item.default}\n </code>\n </Show>\n </TableCell>\n <TableCell class=\"px-4 py-3 font-sans font-normal leading-relaxed text-muted-foreground\">\n {item.description}\n </TableCell>\n </TableRow>\n )}\n </For>\n </TableBody>\n </Table>\n </div>\n </div>\n );\n};\n",
|
|
18
|
+
"type": "registry:ui"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
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
|
]
|
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
|
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "callout",
|
|
3
|
+
"title": "Callout",
|
|
4
|
+
"description": "A semantic callout and alert block with status variants, icons, and titles for documentation and notices.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority",
|
|
10
|
+
"lucide-solid"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "ui/callout.tsx",
|
|
15
|
+
"content": "import {\n splitProps,\n Show,\n type Component,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport { Dynamic } from \"solid-js/web\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n Info,\n Lightbulb,\n AlertTriangle,\n AlertCircle,\n CheckCircle2,\n Bookmark,\n} from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport const calloutVariants = cva(\n \"relative w-full rounded-lg border p-4 text-sm leading-relaxed transition-colors\",\n {\n variants: {\n variant: {\n note: \"border-border/80 bg-muted/50 text-foreground [&>svg]:text-foreground\",\n info: \"border-sky-500/30 bg-sky-500/10 text-sky-950 dark:text-sky-100 [&>svg]:text-sky-600 dark:[&>svg]:text-sky-400\",\n tip: \"border-emerald-500/30 bg-emerald-500/10 text-emerald-950 dark:text-emerald-100 [&>svg]:text-emerald-600 dark:[&>svg]:text-emerald-400\",\n warning: \"border-amber-500/30 bg-amber-500/10 text-amber-950 dark:text-amber-100 [&>svg]:text-amber-600 dark:[&>svg]:text-amber-400\",\n danger: \"border-destructive/30 bg-destructive/10 text-destructive dark:text-red-200 [&>svg]:text-destructive\",\n success: \"border-emerald-500/30 bg-emerald-500/10 text-emerald-950 dark:text-emerald-100 [&>svg]:text-emerald-600 dark:[&>svg]:text-emerald-400\",\n },\n },\n defaultVariants: {\n variant: \"note\",\n },\n }\n);\n\nconst defaultIcons = {\n note: Bookmark,\n info: Info,\n tip: Lightbulb,\n warning: AlertTriangle,\n danger: AlertCircle,\n success: CheckCircle2,\n};\n\nexport interface CalloutProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof calloutVariants> {\n title?: string;\n type?: VariantProps<typeof calloutVariants>[\"variant\"];\n icon?: Component<{ class?: string }> | false;\n class?: string;\n}\n\nexport const Callout: ParentComponent<CalloutProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"type\",\n \"title\",\n \"icon\",\n \"class\",\n \"children\",\n ]);\n\n const variant = () => local.variant || local.type || \"note\";\n\n const IconComponent = () => {\n if (local.icon === false) return null;\n if (local.icon) return local.icon;\n return defaultIcons[variant()] || Info;\n };\n\n return (\n <div\n role=\"region\"\n aria-label={local.title || `${variant()} callout`}\n class={cn(\n calloutVariants({ variant: variant() }),\n \"flex gap-3.5 items-start\",\n local.class\n )}\n {...rest}\n >\n <Show when={IconComponent()}>\n {(Icon) => (\n <span class=\"inline-flex shrink-0 items-center justify-center mt-0.5 select-none\">\n <Dynamic component={Icon()} class=\"size-4.5 shrink-0\" />\n </span>\n )}\n </Show>\n <div class=\"flex-1 space-y-1 min-w-0\">\n <Show when={local.title}>\n <h5 class=\"font-semibold text-sm leading-none tracking-tight\">\n {local.title}\n </h5>\n </Show>\n <div class=\"text-sm opacity-90 leading-relaxed break-words\">\n {local.children}\n </div>\n </div>\n </div>\n );\n};\n\nexport interface CalloutTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {\n class?: string;\n}\n\nexport const CalloutTitle: ParentComponent<CalloutTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n return (\n <h5\n class={cn(\"font-semibold text-sm leading-none tracking-tight mb-1\", local.class)}\n {...rest}\n >\n {local.children}\n </h5>\n );\n};\n\nexport interface CalloutDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const CalloutDescription: ParentComponent<CalloutDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n return (\n <div\n class={cn(\"text-sm opacity-90 leading-relaxed\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
|
|
16
|
+
"type": "registry:ui"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
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
|
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "code-block",
|
|
3
|
+
"title": "Code Block",
|
|
4
|
+
"description": "A code block container with filename header, language indicator, and interactive copy to clipboard button.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"lucide-solid",
|
|
10
|
+
"shiki"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"button",
|
|
14
|
+
"badge",
|
|
15
|
+
"tooltip",
|
|
16
|
+
"tabs",
|
|
17
|
+
"create-clipboard"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
{
|
|
21
|
+
"path": "ui/code-block.tsx",
|
|
22
|
+
"content": "import {\n createSignal,\n createEffect,\n createMemo,\n splitProps,\n For,\n Show,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport { Check, Copy } from \"lucide-solid\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Tooltip, TooltipTrigger, TooltipContent } from \"@/components/ui/tooltip\";\nimport { createClipboard } from \"@/hooks/create-clipboard\";\nimport { cn } from \"@/lib/cn\";\n\nexport type CodeBlockPackageManager = \"bunx\" | \"npx\" | \"pnpm\" | \"yarn\";\n\nexport interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** The raw source code text to display and copy */\n code?: string;\n /** File name to display in the header bar (e.g. \"App.tsx\", \"main.rs\") */\n filename?: string;\n /** Programming language badge (e.g. \"tsx\", \"rust\", \"bash\") */\n language?: string;\n /** Alias for language */\n lang?: string;\n /** Whether the code snippet is an executable CLI command */\n isCli?: boolean;\n /** Shows the interactive package manager runner tabs (bunx, npx, pnpm, yarn) */\n showPmSwitcher?: boolean;\n /** List of package manager runners to show */\n managers?: CodeBlockPackageManager[];\n /** Whether the copy button is enabled. Defaults to true. */\n copyable?: boolean;\n class?: string;\n}\n\nlet shikiHighlighterPromise: Promise<any> | null = null;\n\nasync function getShikiHighlighter() {\n if (typeof window === \"undefined\") return null;\n if (!shikiHighlighterPromise) {\n try {\n const { createHighlighter } = await import(\"shiki\");\n shikiHighlighterPromise = createHighlighter({\n themes: [\"github-dark\", \"github-light\"],\n langs: [\n \"typescript\",\n \"javascript\",\n \"tsx\",\n \"jsx\",\n \"bash\",\n \"json\",\n \"css\",\n \"html\",\n \"rust\",\n ],\n });\n } catch (e) {\n console.warn(\"Failed to load Shiki highlighter\", e);\n return null;\n }\n }\n return shikiHighlighterPromise;\n}\n\nfunction highlightCliCommand(code: string): string {\n const trimmed = code.trim();\n const parts = trimmed.split(/\\s+/);\n if (parts.length === 0) return code;\n\n const isRunner = [\"bunx\", \"npx\", \"pnpm\", \"yarn\", \"bun\", \"npm\", \"cargo\", \"deno\"].includes(parts[0]);\n if (!isRunner) return `<span class=\"text-foreground\">${escapeHtml(trimmed)}</span>`;\n\n return parts\n .map((part, index) => {\n if (index === 0) {\n return `<span class=\"text-amber-500 dark:text-amber-400 font-bold\">${escapeHtml(part)}</span>`;\n }\n if (part.startsWith(\"-\")) {\n return `<span class=\"text-purple-400 dark:text-purple-300\">${escapeHtml(part)}</span>`;\n }\n if (part.startsWith(\"@\")) {\n return `<span class=\"text-emerald-400 dark:text-emerald-300 font-medium\">${escapeHtml(part)}</span>`;\n }\n if (index === 1 && [\"add\", \"install\", \"init\", \"create\", \"run\"].includes(part)) {\n return `<span class=\"text-sky-500 dark:text-sky-400 font-medium\">${escapeHtml(part)}</span>`;\n }\n return `<span class=\"text-foreground/90\">${escapeHtml(part)}</span>`;\n })\n .join(\" \");\n}\n\nfunction transformCommandForPm(cmd: string, pm: CodeBlockPackageManager): string {\n const trimmed = cmd.trim();\n const runners = [\"bunx\", \"npx\", \"pnpm dlx\", \"pnpm\", \"yarn dlx\", \"yarn\", \"bun\", \"npm\"];\n\n let base = trimmed;\n for (const r of runners) {\n if (trimmed.startsWith(r)) {\n base = trimmed.slice(r.length).trim();\n break;\n }\n }\n\n switch (pm) {\n case \"bunx\":\n return `bunx ${base}`;\n case \"npx\":\n return `npx ${base}`;\n case \"pnpm\":\n return `pnpm dlx ${base}`;\n case \"yarn\":\n return `yarn dlx ${base}`;\n default:\n return `${pm} ${base}`;\n }\n}\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nexport const CodeBlock: ParentComponent<CodeBlockProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"id\",\n \"code\",\n \"filename\",\n \"language\",\n \"lang\",\n \"isCli\",\n \"showPmSwitcher\",\n \"managers\",\n \"copyable\",\n \"class\",\n \"children\",\n ]);\n\n const clipboard = createClipboard();\n const [activePm, setActivePm] = createSignal<CodeBlockPackageManager>(\"bunx\");\n const [highlightedHtml, setHighlightedHtml] = createSignal(\"\");\n\n const effectiveLang = () => (local.lang || local.language || \"tsx\").toLowerCase();\n const copyable = () => local.copyable ?? true;\n const defaultManagers: CodeBlockPackageManager[] = [\"bunx\", \"npx\", \"pnpm\", \"yarn\"];\n const managersList = () => local.managers || defaultManagers;\n\n const isBashCli = () => {\n const l = effectiveLang();\n return l === \"bash\" || l === \"sh\" || l === \"shell\" || Boolean(local.isCli);\n };\n\n const cleanCode = createMemo(() => {\n const raw = (local.code || \"\").trim();\n if (local.isCli || local.showPmSwitcher) {\n return transformCommandForPm(raw, activePm());\n }\n return raw;\n });\n\n createEffect(() => {\n const codeStr = cleanCode();\n const lang = effectiveLang();\n\n if (isBashCli()) {\n setHighlightedHtml(highlightCliCommand(codeStr));\n return;\n }\n\n if (!codeStr) {\n setHighlightedHtml(\"\");\n return;\n }\n\n // Attempt Shiki highlighting\n getShikiHighlighter().then((highlighter) => {\n if (highlighter) {\n try {\n const html = highlighter.codeToHtml(codeStr, {\n lang: lang === \"js\" ? \"javascript\" : lang === \"ts\" ? \"typescript\" : lang,\n themes: {\n light: \"github-light\",\n dark: \"github-dark\",\n },\n });\n setHighlightedHtml(html);\n return;\n } catch (err) {\n console.warn(`Shiki failed for language: ${lang}`, err);\n }\n }\n // Fallback\n setHighlightedHtml(`<pre class=\"text-foreground/90\"><code>${escapeHtml(codeStr)}</code></pre>`);\n });\n });\n\n const handleCopy = async () => {\n let textToCopy = cleanCode();\n if (!textToCopy && typeof window !== \"undefined\") {\n const target = document.querySelector(`[data-code-block-id=\"${local.id}\"]`);\n if (target) textToCopy = target.textContent || \"\";\n }\n\n if (!textToCopy) return;\n await clipboard.copy(textToCopy);\n };\n\n const shouldShowSwitcher = () => Boolean(local.showPmSwitcher || local.isCli);\n const hasHeader = () => Boolean(shouldShowSwitcher() || local.filename || effectiveLang());\n\n return (\n <div\n class={cn(\n \"group relative my-4 w-full overflow-hidden rounded-lg border border-border bg-muted/40 font-mono text-sm shadow-2xs\",\n local.class\n )}\n {...rest}\n >\n {/* Code Header Bar */}\n <Show when={hasHeader()}>\n <div class=\"flex h-9 items-center justify-between border-b border-border/70 bg-muted/70 px-3.5 text-xs text-muted-foreground select-none\">\n <div class=\"flex items-center gap-2\">\n <Show when={shouldShowSwitcher()}>\n <Tabs\n value={activePm()}\n onChange={(val) => setActivePm(val as CodeBlockPackageManager)}\n class=\"w-auto flex-none\"\n >\n <TabsList class=\"w-auto inline-flex h-auto bg-background/60 p-0.5 border border-border/50 gap-0.5 rounded-md font-mono\">\n <For each={managersList()}>\n {(pm) => (\n <TabsTrigger\n value={pm}\n class=\"rounded-xs px-2 py-0.5 text-[11px] font-mono transition-colors cursor-pointer data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:font-semibold data-[state=active]:shadow-2xs\"\n >\n {pm}\n </TabsTrigger>\n )}\n </For>\n </TabsList>\n </Tabs>\n </Show>\n\n <Show when={local.filename}>\n <span class=\"font-medium text-foreground tracking-tight\">{local.filename}</span>\n </Show>\n\n <Show when={effectiveLang() && !shouldShowSwitcher() && !local.filename}>\n <Badge variant=\"outline\" class=\"uppercase text-[10px] px-1.5 py-0 h-4 font-mono font-semibold\">\n {effectiveLang()}\n </Badge>\n </Show>\n </div>\n\n <Show when={copyable() && (local.code || cleanCode())}>\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleCopy}\n aria-label={clipboard.copied() ? \"Copied code\" : \"Copy code\"}\n class=\"size-6 p-0 rounded-xs text-muted-foreground hover:bg-background hover:text-foreground cursor-pointer\"\n >\n <Show when={clipboard.copied()} fallback={<Copy class=\"size-3.5\" />}>\n <Check class=\"size-3.5 text-emerald-500 dark:text-emerald-400\" />\n </Show>\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">\n {clipboard.copied() ? \"Copied!\" : \"Copy code\"}\n </TooltipContent>\n </Tooltip>\n </Show>\n </div>\n </Show>\n\n {/* Floating Copy Button (if no header bar is present) */}\n <Show when={!hasHeader() && copyable() && (local.code || cleanCode())}>\n <div class=\"absolute right-2.5 top-2.5 z-10 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100\">\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"outline\"\n size=\"icon\"\n onClick={handleCopy}\n aria-label={clipboard.copied() ? \"Copied code\" : \"Copy code\"}\n class=\"size-7 rounded-md border-border/80 bg-background/90 text-muted-foreground backdrop-blur-xs hover:bg-muted hover:text-foreground cursor-pointer shadow-2xs\"\n >\n <Show when={clipboard.copied()} fallback={<Copy class=\"size-3.5\" />}>\n <Check class=\"size-3.5 text-emerald-500 dark:text-emerald-400\" />\n </Show>\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">\n {clipboard.copied() ? \"Copied!\" : \"Copy code\"}\n </TooltipContent>\n </Tooltip>\n </div>\n </Show>\n\n {/* Code Container */}\n <div class=\"overflow-x-auto p-4 text-[13px] leading-relaxed [scrollbar-width:thin]\">\n <Show\n when={local.children}\n fallback={\n <div\n class=\"font-mono text-sm leading-relaxed overflow-x-auto [&>pre]:!bg-transparent [&>pre]:!p-0 [&>pre]:!m-0 [&>pre]:!border-none\"\n innerHTML={highlightedHtml() || `<pre class=\"text-foreground/90\"><code>${escapeHtml(cleanCode())}</code></pre>`}\n />\n }\n >\n <pre class=\"shiki nikala-code-block m-0 overflow-x-auto bg-transparent p-0 font-mono text-xs\">\n {local.children}\n </pre>\n </Show>\n </div>\n </div>\n );\n};\n",
|
|
23
|
+
"type": "registry:ui"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "code-group",
|
|
3
|
+
"title": "Code Group",
|
|
4
|
+
"description": "A compound tabbed code container for organizing multiple files, languages, and snippets built on Tabs.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"tabs"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "ui/code-group.tsx",
|
|
16
|
+
"content": "import {\n splitProps,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport {\n Tabs,\n TabsList,\n TabsTrigger,\n TabsContent,\n} from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface CodeGroupProps\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** Controlled active tab value */\n value?: string;\n /** Default active tab value */\n defaultValue?: string;\n /** Callback fired when active tab changes */\n onChange?: (value: string) => void;\n class?: string;\n}\n\n/**\n * Tabbed code container composed directly from Nikala UI Tabs primitives.\n */\nexport const CodeGroup: ParentComponent<CodeGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"defaultValue\",\n \"onChange\",\n \"class\",\n \"children\",\n ]);\n\n return (\n <Tabs\n value={local.value}\n defaultValue={local.defaultValue}\n onChange={local.onChange}\n class={cn(\n \"my-4 w-full gap-0 overflow-hidden rounded-lg border border-border bg-muted/40 font-mono text-sm shadow-2xs\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </Tabs>\n );\n};\n\nexport interface CodeGroupListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CodeGroupList: ParentComponent<CodeGroupListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n return (\n <div class={cn(\"flex h-9 items-center justify-between border-b border-border/70 bg-muted/70 px-3 select-none\", local.class)}>\n <TabsList class=\"h-auto bg-background/60 p-0.5 border border-border/50 gap-0.5 rounded-md\" {...rest}>\n {local.children}\n </TabsList>\n </div>\n );\n};\n\nexport interface CodeGroupTriggerProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n value: string;\n class?: string;\n}\n\nexport const CodeGroupTrigger: ParentComponent<CodeGroupTriggerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"value\", \"class\", \"children\"]);\n return (\n <TabsTrigger\n value={local.value}\n class={cn(\n \"rounded-xs px-2 py-0.5 text-[11px] font-mono transition-colors cursor-pointer data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:font-semibold data-[state=active]:shadow-2xs\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </TabsTrigger>\n );\n};\n\nexport interface CodeGroupContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n value: string;\n class?: string;\n}\n\nexport const CodeGroupContent: ParentComponent<CodeGroupContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"value\", \"class\", \"children\"]);\n return (\n <TabsContent\n value={local.value}\n class={cn(\"p-4 text-[13px] leading-relaxed overflow-x-auto m-0 focus-visible:outline-none\", local.class)}\n {...rest}\n >\n {local.children}\n </TabsContent>\n );\n};\n",
|
|
17
|
+
"type": "registry:ui"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
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 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 description?: string;\n onSelect?: () => void;\n shouldFilter?: boolean;\n}\n\nexport const CommandItem: Component<CommandItemProps> = (props) => {\n let itemRef: HTMLDivElement | undefined;\n const [local, rest] = splitProps(props, [\n \"title\",\n \"subtitle\",\n \"description\",\n \"keywords\",\n \"onSelect\",\n \"onClick\",\n \"shouldFilter\",\n \"class\",\n ]);\n const { search } = useCommand();\n\n /* Auto filter item based on search query */\n const matchesSearch = () => {\n if (local.shouldFilter === false) return true;\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 descMatch = local.description?.toLowerCase().includes(query);\n const keywordMatch = local.keywords?.some((k) =>\n k.toLowerCase().includes(query)\n );\n\n return Boolean(titleMatch || subtitleMatch || descMatch || 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 || local.description}\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 children?: JSX.Element;\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 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\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\">Esc</Kbd>\n <span>Close</span>\n </span>\n </div>\n\n {local.children}\n </div>\n );\n};",
|
|
23
24
|
"type": "registry:ui"
|
|
24
25
|
}
|
|
25
26
|
]
|