@nikala-ui/core 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +43 -2
- package/registry/api-table.json +21 -0
- package/registry/banner.json +1 -1
- package/registry/button.json +1 -1
- package/registry/callout.json +19 -0
- package/registry/checkbox.json +4 -1
- package/registry/code-block.json +26 -0
- package/registry/code-group.json +20 -0
- package/registry/combobox.json +1 -1
- package/registry/command.json +3 -2
- package/registry/component-viewer.json +25 -0
- package/registry/container.json +18 -0
- package/registry/context-menu.json +1 -1
- package/registry/create-app-updater.json +13 -0
- package/registry/create-document-tabs.json +13 -0
- package/registry/create-global-shortcut.json +13 -0
- package/registry/create-tauri-window.json +13 -0
- package/registry/create-tiptap-editor.json +34 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +3 -2
- package/registry/field.json +1 -1
- package/registry/file-tree.json +21 -0
- package/registry/footer.json +1 -1
- package/registry/form-message.json +3 -2
- package/registry/icon-button.json +1 -1
- package/registry/index.json +336 -5
- package/registry/navbar.json +1 -1
- package/registry/navigation-menu.json +1 -1
- package/registry/number-input.json +1 -1
- package/registry/package-manager-tabs.json +24 -0
- package/registry/pager.json +21 -0
- package/registry/popover.json +4 -1
- package/registry/resizable.json +1 -1
- package/registry/rich-text-editor.json +95 -0
- package/registry/scroll-area.json +1 -1
- package/registry/section-heading.json +21 -0
- package/registry/select.json +3 -2
- package/registry/sheet.json +1 -1
- package/registry/sidebar.json +1 -1
- package/registry/status.json +1 -1
- package/registry/steps.json +20 -0
- package/registry/switch.json +4 -1
- package/registry/table-of-contents.json +23 -0
- package/registry/tabs.json +4 -1
- package/registry/theme-manager.json +6 -5
- package/registry/titlebar-tabs.json +24 -0
- package/registry/titlebar.json +26 -0
- package/registry/toggle-group.json +1 -1
- package/registry/updater-modal.json +26 -0
- package/src/index.ts +48 -0
- package/src/registry/components/ui/api-table.tsx +117 -0
- package/src/registry/components/ui/banner.tsx +0 -2
- package/src/registry/components/ui/button.tsx +1 -1
- package/src/registry/components/ui/callout.tsx +137 -0
- package/src/registry/components/ui/checkbox.tsx +1 -1
- package/src/registry/components/ui/code-block.tsx +326 -0
- package/src/registry/components/ui/code-group.tsx +105 -0
- package/src/registry/components/ui/combobox.tsx +83 -18
- package/src/registry/components/ui/command.tsx +110 -106
- package/src/registry/components/ui/component-viewer.tsx +363 -0
- package/src/registry/components/ui/container.tsx +45 -0
- package/src/registry/components/ui/context-menu.tsx +67 -22
- package/src/registry/components/ui/dialog.tsx +42 -32
- package/src/registry/components/ui/dropdown-menu.tsx +69 -31
- package/src/registry/components/ui/field.tsx +7 -3
- package/src/registry/components/ui/file-tree.tsx +181 -0
- package/src/registry/components/ui/footer.tsx +1 -1
- package/src/registry/components/ui/form-message.tsx +2 -2
- package/src/registry/components/ui/icon-button.tsx +1 -1
- package/src/registry/components/ui/navbar.tsx +18 -11
- package/src/registry/components/ui/navigation-menu.tsx +2 -2
- package/src/registry/components/ui/number-input.tsx +3 -3
- package/src/registry/components/ui/package-manager-tabs.tsx +226 -0
- package/src/registry/components/ui/pager.tsx +102 -0
- package/src/registry/components/ui/popover.tsx +1 -1
- package/src/registry/components/ui/resizable.tsx +3 -1
- package/src/registry/components/ui/rich-text-editor/bubble-menu.tsx +236 -0
- package/src/registry/components/ui/rich-text-editor/editor.tsx +308 -0
- package/src/registry/components/ui/rich-text-editor/extensions.ts +116 -0
- package/src/registry/components/ui/rich-text-editor/footer.tsx +36 -0
- package/src/registry/components/ui/rich-text-editor/image-dialog.tsx +75 -0
- package/src/registry/components/ui/rich-text-editor/index.ts +11 -0
- package/src/registry/components/ui/rich-text-editor/link-dialog.tsx +48 -0
- package/src/registry/components/ui/rich-text-editor/markdown.ts +170 -0
- package/src/registry/components/ui/rich-text-editor/slash-command.tsx +301 -0
- package/src/registry/components/ui/rich-text-editor/table-controls.tsx +68 -0
- package/src/registry/components/ui/rich-text-editor/toolbar.tsx +465 -0
- package/src/registry/components/ui/rich-text-editor/use-editor.ts +231 -0
- package/src/registry/components/ui/scroll-area.tsx +12 -2
- package/src/registry/components/ui/section-heading.tsx +108 -0
- package/src/registry/components/ui/select.tsx +16 -19
- package/src/registry/components/ui/sheet.tsx +58 -53
- package/src/registry/components/ui/sidebar.tsx +4 -4
- package/src/registry/components/ui/status.tsx +7 -5
- package/src/registry/components/ui/steps.tsx +198 -0
- package/src/registry/components/ui/switch.tsx +1 -1
- package/src/registry/components/ui/table-of-contents.tsx +131 -0
- package/src/registry/components/ui/tabs.tsx +1 -1
- package/src/registry/components/ui/theme-toggle.tsx +175 -139
- package/src/registry/components/ui/titlebar-tabs.tsx +773 -0
- package/src/registry/components/ui/titlebar.tsx +468 -0
- package/src/registry/components/ui/toggle-group.tsx +1 -1
- package/src/registry/components/ui/updater-modal.tsx +254 -0
- package/src/registry/metadata.ts +167 -6
- package/src/registry/providers/theme-provider.tsx +16 -8
- package/src/registry/providers/theme-script.tsx +29 -9
- package/src/registry/providers/theme-transitions.ts +81 -50
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSignal,
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
splitProps,
|
|
6
|
+
For,
|
|
7
|
+
Show,
|
|
8
|
+
type JSX,
|
|
9
|
+
type ParentComponent,
|
|
10
|
+
} from "solid-js";
|
|
11
|
+
import { Check, Copy } from "lucide-solid";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { Badge } from "@/components/ui/badge";
|
|
14
|
+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
15
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
|
16
|
+
import { createClipboard } from "@/hooks/create-clipboard";
|
|
17
|
+
import { cn } from "@/lib/cn";
|
|
18
|
+
|
|
19
|
+
export type CodeBlockPackageManager = "bunx" | "npx" | "pnpm" | "yarn";
|
|
20
|
+
|
|
21
|
+
export interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
22
|
+
/** The raw source code text to display and copy */
|
|
23
|
+
code?: string;
|
|
24
|
+
/** File name to display in the header bar (e.g. "App.tsx", "main.rs") */
|
|
25
|
+
filename?: string;
|
|
26
|
+
/** Programming language badge (e.g. "tsx", "rust", "bash") */
|
|
27
|
+
language?: string;
|
|
28
|
+
/** Alias for language */
|
|
29
|
+
lang?: string;
|
|
30
|
+
/** Whether the code snippet is an executable CLI command */
|
|
31
|
+
isCli?: boolean;
|
|
32
|
+
/** Shows the interactive package manager runner tabs (bunx, npx, pnpm, yarn) */
|
|
33
|
+
showPmSwitcher?: boolean;
|
|
34
|
+
/** List of package manager runners to show */
|
|
35
|
+
managers?: CodeBlockPackageManager[];
|
|
36
|
+
/** Whether the copy button is enabled. Defaults to true. */
|
|
37
|
+
copyable?: boolean;
|
|
38
|
+
class?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let shikiHighlighterPromise: Promise<any> | null = null;
|
|
42
|
+
|
|
43
|
+
async function getShikiHighlighter() {
|
|
44
|
+
if (typeof window === "undefined") return null;
|
|
45
|
+
if (!shikiHighlighterPromise) {
|
|
46
|
+
try {
|
|
47
|
+
const { createHighlighter } = await import("shiki");
|
|
48
|
+
shikiHighlighterPromise = createHighlighter({
|
|
49
|
+
themes: ["github-dark", "github-light"],
|
|
50
|
+
langs: [
|
|
51
|
+
"typescript",
|
|
52
|
+
"javascript",
|
|
53
|
+
"tsx",
|
|
54
|
+
"jsx",
|
|
55
|
+
"bash",
|
|
56
|
+
"json",
|
|
57
|
+
"css",
|
|
58
|
+
"html",
|
|
59
|
+
"rust",
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.warn("Failed to load Shiki highlighter", e);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return shikiHighlighterPromise;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function highlightCliCommand(code: string): string {
|
|
71
|
+
const trimmed = code.trim();
|
|
72
|
+
const parts = trimmed.split(/\s+/);
|
|
73
|
+
if (parts.length === 0) return code;
|
|
74
|
+
|
|
75
|
+
const isRunner = ["bunx", "npx", "pnpm", "yarn", "bun", "npm", "cargo", "deno"].includes(parts[0]);
|
|
76
|
+
if (!isRunner) return `<span class="text-foreground">${escapeHtml(trimmed)}</span>`;
|
|
77
|
+
|
|
78
|
+
return parts
|
|
79
|
+
.map((part, index) => {
|
|
80
|
+
if (index === 0) {
|
|
81
|
+
return `<span class="text-amber-500 dark:text-amber-400 font-bold">${escapeHtml(part)}</span>`;
|
|
82
|
+
}
|
|
83
|
+
if (part.startsWith("-")) {
|
|
84
|
+
return `<span class="text-purple-400 dark:text-purple-300">${escapeHtml(part)}</span>`;
|
|
85
|
+
}
|
|
86
|
+
if (part.startsWith("@")) {
|
|
87
|
+
return `<span class="text-emerald-400 dark:text-emerald-300 font-medium">${escapeHtml(part)}</span>`;
|
|
88
|
+
}
|
|
89
|
+
if (index === 1 && ["add", "install", "init", "create", "run"].includes(part)) {
|
|
90
|
+
return `<span class="text-sky-500 dark:text-sky-400 font-medium">${escapeHtml(part)}</span>`;
|
|
91
|
+
}
|
|
92
|
+
return `<span class="text-foreground/90">${escapeHtml(part)}</span>`;
|
|
93
|
+
})
|
|
94
|
+
.join(" ");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function transformCommandForPm(cmd: string, pm: CodeBlockPackageManager): string {
|
|
98
|
+
const trimmed = cmd.trim();
|
|
99
|
+
const runners = ["bunx", "npx", "pnpm dlx", "pnpm", "yarn dlx", "yarn", "bun", "npm"];
|
|
100
|
+
|
|
101
|
+
let base = trimmed;
|
|
102
|
+
for (const r of runners) {
|
|
103
|
+
if (trimmed.startsWith(r)) {
|
|
104
|
+
base = trimmed.slice(r.length).trim();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
switch (pm) {
|
|
110
|
+
case "bunx":
|
|
111
|
+
return `bunx ${base}`;
|
|
112
|
+
case "npx":
|
|
113
|
+
return `npx ${base}`;
|
|
114
|
+
case "pnpm":
|
|
115
|
+
return `pnpm dlx ${base}`;
|
|
116
|
+
case "yarn":
|
|
117
|
+
return `yarn dlx ${base}`;
|
|
118
|
+
default:
|
|
119
|
+
return `${pm} ${base}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function escapeHtml(str: string): string {
|
|
124
|
+
return str
|
|
125
|
+
.replace(/&/g, "&")
|
|
126
|
+
.replace(/</g, "<")
|
|
127
|
+
.replace(/>/g, ">")
|
|
128
|
+
.replace(/"/g, """)
|
|
129
|
+
.replace(/'/g, "'");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const CodeBlock: ParentComponent<CodeBlockProps> = (props) => {
|
|
133
|
+
const [local, rest] = splitProps(props, [
|
|
134
|
+
"id",
|
|
135
|
+
"code",
|
|
136
|
+
"filename",
|
|
137
|
+
"language",
|
|
138
|
+
"lang",
|
|
139
|
+
"isCli",
|
|
140
|
+
"showPmSwitcher",
|
|
141
|
+
"managers",
|
|
142
|
+
"copyable",
|
|
143
|
+
"class",
|
|
144
|
+
"children",
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const clipboard = createClipboard();
|
|
148
|
+
const [activePm, setActivePm] = createSignal<CodeBlockPackageManager>("bunx");
|
|
149
|
+
const [highlightedHtml, setHighlightedHtml] = createSignal("");
|
|
150
|
+
|
|
151
|
+
const effectiveLang = () => (local.lang || local.language || "tsx").toLowerCase();
|
|
152
|
+
const copyable = () => local.copyable ?? true;
|
|
153
|
+
const defaultManagers: CodeBlockPackageManager[] = ["bunx", "npx", "pnpm", "yarn"];
|
|
154
|
+
const managersList = () => local.managers || defaultManagers;
|
|
155
|
+
|
|
156
|
+
const isBashCli = () => {
|
|
157
|
+
const l = effectiveLang();
|
|
158
|
+
return l === "bash" || l === "sh" || l === "shell" || Boolean(local.isCli);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const cleanCode = createMemo(() => {
|
|
162
|
+
const raw = (local.code || "").trim();
|
|
163
|
+
if (local.isCli || local.showPmSwitcher) {
|
|
164
|
+
return transformCommandForPm(raw, activePm());
|
|
165
|
+
}
|
|
166
|
+
return raw;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
createEffect(() => {
|
|
170
|
+
const codeStr = cleanCode();
|
|
171
|
+
const lang = effectiveLang();
|
|
172
|
+
|
|
173
|
+
if (isBashCli()) {
|
|
174
|
+
setHighlightedHtml(highlightCliCommand(codeStr));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!codeStr) {
|
|
179
|
+
setHighlightedHtml("");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Attempt Shiki highlighting
|
|
184
|
+
getShikiHighlighter().then((highlighter) => {
|
|
185
|
+
if (highlighter) {
|
|
186
|
+
try {
|
|
187
|
+
const html = highlighter.codeToHtml(codeStr, {
|
|
188
|
+
lang: lang === "js" ? "javascript" : lang === "ts" ? "typescript" : lang,
|
|
189
|
+
themes: {
|
|
190
|
+
light: "github-light",
|
|
191
|
+
dark: "github-dark",
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
setHighlightedHtml(html);
|
|
195
|
+
return;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.warn(`Shiki failed for language: ${lang}`, err);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Fallback
|
|
201
|
+
setHighlightedHtml(`<pre class="text-foreground/90"><code>${escapeHtml(codeStr)}</code></pre>`);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const handleCopy = async () => {
|
|
206
|
+
let textToCopy = cleanCode();
|
|
207
|
+
if (!textToCopy && typeof window !== "undefined") {
|
|
208
|
+
const target = document.querySelector(`[data-code-block-id="${local.id}"]`);
|
|
209
|
+
if (target) textToCopy = target.textContent || "";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!textToCopy) return;
|
|
213
|
+
await clipboard.copy(textToCopy);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const shouldShowSwitcher = () => Boolean(local.showPmSwitcher || local.isCli);
|
|
217
|
+
const hasHeader = () => Boolean(shouldShowSwitcher() || local.filename || effectiveLang());
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div
|
|
221
|
+
class={cn(
|
|
222
|
+
"group relative my-4 w-full overflow-hidden rounded-lg border border-border bg-muted/40 font-mono text-sm shadow-2xs",
|
|
223
|
+
local.class
|
|
224
|
+
)}
|
|
225
|
+
{...rest}
|
|
226
|
+
>
|
|
227
|
+
{/* Code Header Bar */}
|
|
228
|
+
<Show when={hasHeader()}>
|
|
229
|
+
<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">
|
|
230
|
+
<div class="flex items-center gap-2">
|
|
231
|
+
<Show when={shouldShowSwitcher()}>
|
|
232
|
+
<Tabs
|
|
233
|
+
value={activePm()}
|
|
234
|
+
onChange={(val) => setActivePm(val as CodeBlockPackageManager)}
|
|
235
|
+
class="w-auto flex-none"
|
|
236
|
+
>
|
|
237
|
+
<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">
|
|
238
|
+
<For each={managersList()}>
|
|
239
|
+
{(pm) => (
|
|
240
|
+
<TabsTrigger
|
|
241
|
+
value={pm}
|
|
242
|
+
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"
|
|
243
|
+
>
|
|
244
|
+
{pm}
|
|
245
|
+
</TabsTrigger>
|
|
246
|
+
)}
|
|
247
|
+
</For>
|
|
248
|
+
</TabsList>
|
|
249
|
+
</Tabs>
|
|
250
|
+
</Show>
|
|
251
|
+
|
|
252
|
+
<Show when={local.filename}>
|
|
253
|
+
<span class="font-medium text-foreground tracking-tight">{local.filename}</span>
|
|
254
|
+
</Show>
|
|
255
|
+
|
|
256
|
+
<Show when={effectiveLang() && !shouldShowSwitcher() && !local.filename}>
|
|
257
|
+
<Badge variant="outline" class="uppercase text-[10px] px-1.5 py-0 h-4 font-mono font-semibold">
|
|
258
|
+
{effectiveLang()}
|
|
259
|
+
</Badge>
|
|
260
|
+
</Show>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<Show when={copyable() && (local.code || cleanCode())}>
|
|
264
|
+
<Tooltip>
|
|
265
|
+
<TooltipTrigger
|
|
266
|
+
as={Button}
|
|
267
|
+
variant="ghost"
|
|
268
|
+
size="icon"
|
|
269
|
+
onClick={handleCopy}
|
|
270
|
+
aria-label={clipboard.copied() ? "Copied code" : "Copy code"}
|
|
271
|
+
class="size-6 p-0 rounded-xs text-muted-foreground hover:bg-background hover:text-foreground cursor-pointer"
|
|
272
|
+
>
|
|
273
|
+
<Show when={clipboard.copied()} fallback={<Copy class="size-3.5" />}>
|
|
274
|
+
<Check class="size-3.5 text-emerald-500 dark:text-emerald-400" />
|
|
275
|
+
</Show>
|
|
276
|
+
</TooltipTrigger>
|
|
277
|
+
<TooltipContent class="text-[10px] py-1 px-2">
|
|
278
|
+
{clipboard.copied() ? "Copied!" : "Copy code"}
|
|
279
|
+
</TooltipContent>
|
|
280
|
+
</Tooltip>
|
|
281
|
+
</Show>
|
|
282
|
+
</div>
|
|
283
|
+
</Show>
|
|
284
|
+
|
|
285
|
+
{/* Floating Copy Button (if no header bar is present) */}
|
|
286
|
+
<Show when={!hasHeader() && copyable() && (local.code || cleanCode())}>
|
|
287
|
+
<div class="absolute right-2.5 top-2.5 z-10 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">
|
|
288
|
+
<Tooltip>
|
|
289
|
+
<TooltipTrigger
|
|
290
|
+
as={Button}
|
|
291
|
+
variant="outline"
|
|
292
|
+
size="icon"
|
|
293
|
+
onClick={handleCopy}
|
|
294
|
+
aria-label={clipboard.copied() ? "Copied code" : "Copy code"}
|
|
295
|
+
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"
|
|
296
|
+
>
|
|
297
|
+
<Show when={clipboard.copied()} fallback={<Copy class="size-3.5" />}>
|
|
298
|
+
<Check class="size-3.5 text-emerald-500 dark:text-emerald-400" />
|
|
299
|
+
</Show>
|
|
300
|
+
</TooltipTrigger>
|
|
301
|
+
<TooltipContent class="text-[10px] py-1 px-2">
|
|
302
|
+
{clipboard.copied() ? "Copied!" : "Copy code"}
|
|
303
|
+
</TooltipContent>
|
|
304
|
+
</Tooltip>
|
|
305
|
+
</div>
|
|
306
|
+
</Show>
|
|
307
|
+
|
|
308
|
+
{/* Code Container */}
|
|
309
|
+
<div class="overflow-x-auto p-4 text-[13px] leading-relaxed [scrollbar-width:thin]">
|
|
310
|
+
<Show
|
|
311
|
+
when={local.children}
|
|
312
|
+
fallback={
|
|
313
|
+
<div
|
|
314
|
+
class="font-mono text-sm leading-relaxed overflow-x-auto [&>pre]:!bg-transparent [&>pre]:!p-0 [&>pre]:!m-0 [&>pre]:!border-none"
|
|
315
|
+
innerHTML={highlightedHtml() || `<pre class="text-foreground/90"><code>${escapeHtml(cleanCode())}</code></pre>`}
|
|
316
|
+
/>
|
|
317
|
+
}
|
|
318
|
+
>
|
|
319
|
+
<pre class="shiki nikala-code-block m-0 overflow-x-auto bg-transparent p-0 font-mono text-xs">
|
|
320
|
+
{local.children}
|
|
321
|
+
</pre>
|
|
322
|
+
</Show>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
splitProps,
|
|
3
|
+
type JSX,
|
|
4
|
+
type ParentComponent,
|
|
5
|
+
} from "solid-js";
|
|
6
|
+
import {
|
|
7
|
+
Tabs,
|
|
8
|
+
TabsList,
|
|
9
|
+
TabsTrigger,
|
|
10
|
+
TabsContent,
|
|
11
|
+
} from "@/components/ui/tabs";
|
|
12
|
+
import { cn } from "@/lib/cn";
|
|
13
|
+
|
|
14
|
+
export interface CodeGroupProps
|
|
15
|
+
extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
16
|
+
/** Controlled active tab value */
|
|
17
|
+
value?: string;
|
|
18
|
+
/** Default active tab value */
|
|
19
|
+
defaultValue?: string;
|
|
20
|
+
/** Callback fired when active tab changes */
|
|
21
|
+
onChange?: (value: string) => void;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tabbed code container composed directly from Nikala UI Tabs primitives.
|
|
27
|
+
*/
|
|
28
|
+
export const CodeGroup: ParentComponent<CodeGroupProps> = (props) => {
|
|
29
|
+
const [local, rest] = splitProps(props, [
|
|
30
|
+
"value",
|
|
31
|
+
"defaultValue",
|
|
32
|
+
"onChange",
|
|
33
|
+
"class",
|
|
34
|
+
"children",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Tabs
|
|
39
|
+
value={local.value}
|
|
40
|
+
defaultValue={local.defaultValue}
|
|
41
|
+
onChange={local.onChange}
|
|
42
|
+
class={cn(
|
|
43
|
+
"my-4 w-full gap-0 overflow-hidden rounded-lg border border-border bg-muted/40 font-mono text-sm shadow-2xs",
|
|
44
|
+
local.class
|
|
45
|
+
)}
|
|
46
|
+
{...rest}
|
|
47
|
+
>
|
|
48
|
+
{local.children}
|
|
49
|
+
</Tabs>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export interface CodeGroupListProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
54
|
+
class?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const CodeGroupList: ParentComponent<CodeGroupListProps> = (props) => {
|
|
58
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
59
|
+
return (
|
|
60
|
+
<div class={cn("flex h-9 items-center justify-between border-b border-border/70 bg-muted/70 px-3 select-none", local.class)}>
|
|
61
|
+
<TabsList class="h-auto bg-background/60 p-0.5 border border-border/50 gap-0.5 rounded-md" {...rest}>
|
|
62
|
+
{local.children}
|
|
63
|
+
</TabsList>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface CodeGroupTriggerProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
69
|
+
value: string;
|
|
70
|
+
class?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const CodeGroupTrigger: ParentComponent<CodeGroupTriggerProps> = (props) => {
|
|
74
|
+
const [local, rest] = splitProps(props, ["value", "class", "children"]);
|
|
75
|
+
return (
|
|
76
|
+
<TabsTrigger
|
|
77
|
+
value={local.value}
|
|
78
|
+
class={cn(
|
|
79
|
+
"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",
|
|
80
|
+
local.class
|
|
81
|
+
)}
|
|
82
|
+
{...rest}
|
|
83
|
+
>
|
|
84
|
+
{local.children}
|
|
85
|
+
</TabsTrigger>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export interface CodeGroupContentProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
90
|
+
value: string;
|
|
91
|
+
class?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const CodeGroupContent: ParentComponent<CodeGroupContentProps> = (props) => {
|
|
95
|
+
const [local, rest] = splitProps(props, ["value", "class", "children"]);
|
|
96
|
+
return (
|
|
97
|
+
<TabsContent
|
|
98
|
+
value={local.value}
|
|
99
|
+
class={cn("p-4 text-[13px] leading-relaxed overflow-x-auto m-0 focus-visible:outline-none", local.class)}
|
|
100
|
+
{...rest}
|
|
101
|
+
>
|
|
102
|
+
{local.children}
|
|
103
|
+
</TabsContent>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
@@ -1,44 +1,88 @@
|
|
|
1
1
|
import { splitProps, type JSX, type ValidComponent } from "solid-js";
|
|
2
2
|
import { Check, ChevronDown, X } from "lucide-solid";
|
|
3
|
-
import { ScrollArea } from "
|
|
3
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
4
4
|
import * as ComboboxPrimitive from "@kobalte/core/combobox";
|
|
5
5
|
import { cn } from "@/lib/cn";
|
|
6
6
|
|
|
7
7
|
export type ComboboxRootProps<Option = any, OptGroup = any, T extends ValidComponent = "div"> =
|
|
8
|
-
ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T
|
|
8
|
+
ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T> & {
|
|
9
|
+
class?: string;
|
|
10
|
+
children?: JSX.Element;
|
|
11
|
+
triggerMode?: "input" | "focus" | "both" | "manual";
|
|
12
|
+
};
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
|
-
* Root Combobox
|
|
15
|
+
* Root Combobox component providing search, single/multi-selection, and group support.
|
|
16
|
+
* `triggerMode="focus"` or `triggerMode="both"` enables opening dropdown on input click/focus.
|
|
12
17
|
*/
|
|
13
18
|
export const Combobox = <Option = any, OptGroup = any, T extends ValidComponent = "div">(
|
|
14
19
|
props: ComboboxRootProps<Option, OptGroup, T>
|
|
15
20
|
) => {
|
|
16
|
-
|
|
21
|
+
const [local, rest] = splitProps(props as ComboboxRootProps, ["class", "children", "triggerMode"]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<ComboboxPrimitive.Root
|
|
25
|
+
triggerMode={local.triggerMode ?? "input"}
|
|
26
|
+
class={cn("relative w-full", local.class)}
|
|
27
|
+
{...(rest as any)}
|
|
28
|
+
>
|
|
29
|
+
{local.children}
|
|
30
|
+
</ComboboxPrimitive.Root>
|
|
31
|
+
);
|
|
17
32
|
};
|
|
18
33
|
|
|
19
34
|
export type ComboboxControlProps<Option = any, T extends ValidComponent = "div"> =
|
|
20
35
|
ComboboxPrimitive.ComboboxControlProps<Option, T> & {
|
|
21
36
|
class?: string;
|
|
22
37
|
children?: JSX.Element;
|
|
38
|
+
clearable?: boolean;
|
|
39
|
+
onClear?: () => void;
|
|
23
40
|
};
|
|
24
41
|
|
|
25
42
|
/**
|
|
26
|
-
* Input container
|
|
43
|
+
* Input container for Combobox supporting search input, selected tags, and clear button.
|
|
27
44
|
*/
|
|
28
45
|
export const ComboboxControl = <Option = any, T extends ValidComponent = "div">(
|
|
29
46
|
props: ComboboxControlProps<Option, T>
|
|
30
47
|
) => {
|
|
31
|
-
const [local, rest] = splitProps(props as ComboboxControlProps, [
|
|
48
|
+
const [local, rest] = splitProps(props as ComboboxControlProps, [
|
|
49
|
+
"class",
|
|
50
|
+
"children",
|
|
51
|
+
"clearable",
|
|
52
|
+
"onClear",
|
|
53
|
+
]);
|
|
32
54
|
|
|
33
55
|
return (
|
|
34
56
|
<ComboboxPrimitive.Control
|
|
35
57
|
class={cn(
|
|
36
|
-
"flex min-h-9 w-full flex-wrap items-center
|
|
58
|
+
"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",
|
|
37
59
|
local.class
|
|
38
60
|
)}
|
|
39
61
|
{...(rest as any)}
|
|
40
62
|
>
|
|
41
|
-
|
|
63
|
+
<div class="flex flex-wrap items-center gap-1.5 flex-1 min-w-0">
|
|
64
|
+
{local.children}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="flex items-center gap-1 shrink-0 self-center">
|
|
68
|
+
{local.clearable && (
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
tabIndex={-1}
|
|
72
|
+
onClick={(e) => {
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
if (local.onClear) local.onClear();
|
|
75
|
+
}}
|
|
76
|
+
class="rounded-sm p-0.5 opacity-60 hover:opacity-100 hover:bg-accent text-foreground transition-opacity focus:outline-none cursor-pointer"
|
|
77
|
+
aria-label="Clear selection"
|
|
78
|
+
>
|
|
79
|
+
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
80
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
81
|
+
</svg>
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
84
|
+
<ComboboxTrigger />
|
|
85
|
+
</div>
|
|
42
86
|
</ComboboxPrimitive.Control>
|
|
43
87
|
);
|
|
44
88
|
};
|
|
@@ -50,7 +94,8 @@ export type ComboboxInputProps<T extends ValidComponent = "input"> =
|
|
|
50
94
|
};
|
|
51
95
|
|
|
52
96
|
/**
|
|
53
|
-
*
|
|
97
|
+
* Search input field embedded within ComboboxControl.
|
|
98
|
+
* Supports `openOnFocus` to automatically trigger the dropdown when focused or clicked.
|
|
54
99
|
*/
|
|
55
100
|
export const ComboboxInput = <T extends ValidComponent = "input">(
|
|
56
101
|
props: ComboboxInputProps<T>
|
|
@@ -135,10 +180,12 @@ export const ComboboxToken = <Option = any>(props: ComboboxTokenProps<Option>) =
|
|
|
135
180
|
e.stopPropagation();
|
|
136
181
|
if (local.onRemove) local.onRemove();
|
|
137
182
|
}}
|
|
138
|
-
class="rounded-xs
|
|
183
|
+
class="rounded-xs p-0.5 hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground transition-colors focus:outline-none cursor-pointer"
|
|
184
|
+
aria-label="Remove tag"
|
|
139
185
|
>
|
|
140
|
-
<
|
|
141
|
-
|
|
186
|
+
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
187
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
188
|
+
</svg>
|
|
142
189
|
</button>
|
|
143
190
|
</span>
|
|
144
191
|
);
|
|
@@ -191,17 +238,19 @@ export const ComboboxItem = <T extends ValidComponent = "li">(
|
|
|
191
238
|
return (
|
|
192
239
|
<ComboboxPrimitive.Item
|
|
193
240
|
class={cn(
|
|
194
|
-
"relative flex w-full cursor-pointer select-none items-center
|
|
241
|
+
"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",
|
|
195
242
|
local.class
|
|
196
243
|
)}
|
|
197
|
-
{...
|
|
244
|
+
{...rest}
|
|
198
245
|
>
|
|
199
|
-
<ComboboxPrimitive.
|
|
246
|
+
<ComboboxPrimitive.ItemIndicator class="absolute left-2 flex h-4 w-4 items-center justify-center text-primary">
|
|
247
|
+
<svg class="h-4 w-4 fill-none stroke-current stroke-2" viewBox="0 0 24 24">
|
|
248
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
249
|
+
</svg>
|
|
250
|
+
</ComboboxPrimitive.ItemIndicator>
|
|
251
|
+
<ComboboxPrimitive.ItemLabel class="flex items-center gap-2 w-full truncate">
|
|
200
252
|
{local.children}
|
|
201
253
|
</ComboboxPrimitive.ItemLabel>
|
|
202
|
-
<ComboboxPrimitive.ItemIndicator class="ml-2 flex h-4 w-4 items-center justify-center text-primary">
|
|
203
|
-
<Check class="h-4 w-4 stroke-2" />
|
|
204
|
-
</ComboboxPrimitive.ItemIndicator>
|
|
205
254
|
</ComboboxPrimitive.Item>
|
|
206
255
|
);
|
|
207
256
|
};
|
|
@@ -232,3 +281,19 @@ export const ComboboxGroup = <T extends ValidComponent = "li">(
|
|
|
232
281
|
</ComboboxPrimitive.Section>
|
|
233
282
|
);
|
|
234
283
|
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Empty state notice when no matching search items exist.
|
|
287
|
+
*/
|
|
288
|
+
export const ComboboxEmpty = (props: { class?: string; children?: JSX.Element }) => {
|
|
289
|
+
return (
|
|
290
|
+
<div class={cn("py-6 text-center text-sm text-muted-foreground", props.class)}>
|
|
291
|
+
{props.children || "No matching items found."}
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Hidden native select element for form integrations.
|
|
298
|
+
*/
|
|
299
|
+
export const ComboboxHiddenSelect = ComboboxPrimitive.HiddenSelect;
|