@nikala-ui/core 0.11.0-nightly.b447b5b → 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.
Files changed (32) hide show
  1. package/package.json +23 -1
  2. package/registry/api-table.json +21 -0
  3. package/registry/callout.json +19 -0
  4. package/registry/code-block.json +26 -0
  5. package/registry/code-group.json +20 -0
  6. package/registry/component-viewer.json +25 -0
  7. package/registry/container.json +18 -0
  8. package/registry/file-tree.json +21 -0
  9. package/registry/index.json +175 -0
  10. package/registry/package-manager-tabs.json +24 -0
  11. package/registry/pager.json +21 -0
  12. package/registry/section-heading.json +21 -0
  13. package/registry/steps.json +20 -0
  14. package/registry/table-of-contents.json +23 -0
  15. package/registry/titlebar-tabs.json +1 -1
  16. package/registry/titlebar.json +1 -1
  17. package/src/index.ts +48 -0
  18. package/src/registry/components/ui/api-table.tsx +117 -0
  19. package/src/registry/components/ui/callout.tsx +137 -0
  20. package/src/registry/components/ui/code-block.tsx +326 -0
  21. package/src/registry/components/ui/code-group.tsx +105 -0
  22. package/src/registry/components/ui/component-viewer.tsx +363 -0
  23. package/src/registry/components/ui/container.tsx +45 -0
  24. package/src/registry/components/ui/file-tree.tsx +181 -0
  25. package/src/registry/components/ui/package-manager-tabs.tsx +226 -0
  26. package/src/registry/components/ui/pager.tsx +102 -0
  27. package/src/registry/components/ui/section-heading.tsx +108 -0
  28. package/src/registry/components/ui/steps.tsx +198 -0
  29. package/src/registry/components/ui/table-of-contents.tsx +131 -0
  30. package/src/registry/components/ui/titlebar-tabs.tsx +3 -3
  31. package/src/registry/components/ui/titlebar.tsx +3 -3
  32. package/src/registry/metadata.ts +70 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nikala-ui/core",
3
- "version": "0.11.0-nightly.b447b5b",
3
+ "version": "0.12.0",
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
  },
@@ -51,6 +72,7 @@
51
72
  "@tiptap/extension-underline": "^3.31.0",
52
73
  "@tiptap/starter-kit": "^3.31.0",
53
74
  "lucide-solid": "^1.28.0",
75
+ "shiki": "^4.4.3",
54
76
  "tiptap-markdown": "^0.9.0"
55
77
  }
56
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
+ }
@@ -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
+ }
@@ -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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\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
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "component-viewer",
3
+ "title": "Component Viewer",
4
+ "description": "An interactive preview canvas and code viewer container with responsive viewports, canvas grid, and AI prompts.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "lucide-solid"
10
+ ],
11
+ "registryDependencies": [
12
+ "tabs",
13
+ "button",
14
+ "tooltip",
15
+ "code-block",
16
+ "create-clipboard"
17
+ ],
18
+ "files": [
19
+ {
20
+ "path": "ui/component-viewer.tsx",
21
+ "content": "import {\n createSignal,\n splitProps,\n Show,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport { Portal } from \"solid-js/web\";\nimport {\n Monitor,\n Tablet,\n Smartphone,\n Sparkles,\n RotateCcw,\n Terminal,\n Check,\n Maximize2,\n Minimize2,\n Grid,\n} from \"lucide-solid\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { Button } from \"@/components/ui/button\";\nimport { Tooltip, TooltipTrigger, TooltipContent } from \"@/components/ui/tooltip\";\nimport { CodeBlock } from \"@/components/ui/code-block\";\nimport { createClipboard } from \"@/hooks/create-clipboard\";\nimport { cn } from \"@/lib/cn\";\n\nexport type ComponentViewerViewport = \"desktop\" | \"tablet\" | \"mobile\";\n\nexport interface ComponentViewerProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Name of the component or UI primitive (e.g. \"button\", \"dialog\") */\n name?: string;\n /** Title label displayed in the viewer header */\n title?: string;\n /** Raw TSX/JSX source code to display in the Code tab and copy */\n code: string;\n /** Programming language for syntax highlighting (defaults to \"tsx\") */\n lang?: string;\n /** Alignment of the preview canvas: \"center\" (default), \"start\", or \"end\" */\n align?: \"center\" | \"start\" | \"end\";\n /** Optional CLI command to install this component (e.g. \"bunx @nikala-ui/cli add button\") */\n command?: string;\n /** Initial responsive viewport (default: \"desktop\") */\n defaultViewport?: ComponentViewerViewport;\n /** Whether to allow overflowing elements in the canvas (e.g. popovers, dropdowns) */\n allowOverflow?: boolean;\n /** Enables responsive screen size toggle controls (Desktop, Tablet, Mobile) */\n responsive?: boolean;\n /** Enables canvas grid pattern toggle */\n showGridToggle?: boolean;\n /** Enables a reset button to re-mount the preview canvas */\n reRenderable?: boolean;\n class?: string;\n}\n\n/**\n * Advanced component inspection and preview container with responsive viewport emulation,\n * background grid patterns, live re-mounting, AI context generation, and syntax-highlighted code.\n */\nexport const ComponentViewer: ParentComponent<ComponentViewerProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"name\",\n \"title\",\n \"code\",\n \"lang\",\n \"align\",\n \"command\",\n \"defaultViewport\",\n \"allowOverflow\",\n \"responsive\",\n \"showGridToggle\",\n \"reRenderable\",\n \"class\",\n \"children\",\n ]);\n\n const [viewport, setViewport] = createSignal<ComponentViewerViewport>(\n local.defaultViewport || \"desktop\"\n );\n const [showGrid, setShowGrid] = createSignal(true);\n const [isFullscreen, setIsFullscreen] = createSignal(false);\n const aiClipboard = createClipboard();\n const cmdClipboard = createClipboard();\n const [mounted, setMounted] = createSignal(true);\n\n const cliCommand = () => local.command;\n\n const handleCopyAi = async () => {\n const cmd = cliCommand();\n const prompt = `// ${local.title || local.name || \"Component\"}\n${cmd ? `// Installation: ${cmd}\\n` : \"\"}// Usage Code:\n${local.code}`;\n await aiClipboard.copy(prompt);\n };\n\n const handleCopyCmd = async () => {\n const cmd = cliCommand();\n if (!cmd) return;\n await cmdClipboard.copy(cmd);\n };\n\n const handleReset = () => {\n setMounted(false);\n setTimeout(() => setMounted(true), 20);\n };\n\n const alignmentClass = () => {\n if (local.align === \"start\") return \"items-start justify-start\";\n if (local.align === \"end\") return \"items-end justify-end\";\n return \"items-center justify-center\";\n };\n\n const viewportWidthClass = () => {\n switch (viewport()) {\n case \"mobile\":\n return \"max-w-[375px]\";\n case \"tablet\":\n return \"max-w-[768px]\";\n default:\n return \"max-w-full\";\n }\n };\n\n const renderViewerContent = (isModal = false) => (\n <div\n class={cn(\n \"group relative flex flex-col rounded-lg border border-border bg-card/60 shadow-2xs transition-all w-full\",\n isModal && \"h-full flex-1 bg-background shadow-2xl\",\n !isModal && \"my-6\",\n local.class\n )}\n {...rest}\n >\n <Tabs defaultValue=\"preview\" class=\"relative w-full flex flex-col h-full flex-1 gap-0\">\n {/* Top Header & Toolbar */}\n <div class=\"flex flex-wrap items-center justify-between border-b border-border/70 px-3 py-2 gap-2 bg-muted/30 shrink-0\">\n <div class=\"flex items-center gap-3\">\n <TabsList class=\"h-8 rounded-md bg-muted/60 p-0.5 gap-1\">\n <TabsTrigger value=\"preview\" class=\"h-7 px-3 text-xs cursor-pointer data-[state=active]:bg-background data-[state=active]:shadow-2xs\">\n Preview\n </TabsTrigger>\n <TabsTrigger value=\"code\" class=\"h-7 px-3 text-xs cursor-pointer data-[state=active]:bg-background data-[state=active]:shadow-2xs\">\n Code\n </TabsTrigger>\n </TabsList>\n\n <Show when={local.title}>\n <span class=\"text-xs font-semibold text-foreground tracking-tight hidden sm:inline-block\">\n {local.title}\n </span>\n </Show>\n </div>\n\n {/* Right Action Tools */}\n <div class=\"flex flex-wrap items-center gap-1.5 ml-auto\">\n {/* Responsive Viewport Switcher */}\n <Show when={local.responsive ?? true}>\n <div class=\"flex items-center rounded-md border border-border/60 bg-muted/40 p-0.5 gap-0.5 mr-1\">\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setViewport(\"desktop\")}\n aria-label=\"Desktop viewport\"\n class={cn(\n \"size-6.5 p-0 rounded-sm cursor-pointer text-muted-foreground\",\n viewport() === \"desktop\" && \"bg-background text-foreground shadow-2xs font-semibold\"\n )}\n >\n <Monitor class=\"size-3.5\" />\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">Desktop (100%)</TooltipContent>\n </Tooltip>\n\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setViewport(\"tablet\")}\n aria-label=\"Tablet viewport\"\n class={cn(\n \"size-6.5 p-0 rounded-sm cursor-pointer text-muted-foreground\",\n viewport() === \"tablet\" && \"bg-background text-foreground shadow-2xs font-semibold\"\n )}\n >\n <Tablet class=\"size-3.5\" />\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">Tablet (768px)</TooltipContent>\n </Tooltip>\n\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setViewport(\"mobile\")}\n aria-label=\"Mobile viewport\"\n class={cn(\n \"size-6.5 p-0 rounded-sm cursor-pointer text-muted-foreground\",\n viewport() === \"mobile\" && \"bg-background text-foreground shadow-2xs font-semibold\"\n )}\n >\n <Smartphone class=\"size-3.5\" />\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">Mobile (375px)</TooltipContent>\n </Tooltip>\n </div>\n </Show>\n\n {/* Grid Pattern Toggle */}\n <Show when={local.showGridToggle ?? true}>\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setShowGrid(!showGrid())}\n aria-label=\"Toggle background grid\"\n class={cn(\n \"size-7 rounded-md border border-border/50 text-muted-foreground hover:text-foreground cursor-pointer\",\n showGrid() && \"text-foreground bg-muted/60\"\n )}\n >\n <Grid class=\"size-3.5\" />\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">Toggle Canvas Grid</TooltipContent>\n </Tooltip>\n </Show>\n\n {/* Re-render Reset Canvas */}\n <Show when={local.reRenderable ?? true}>\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleReset}\n aria-label=\"Reset preview canvas\"\n class=\"size-7 rounded-md border border-border/50 text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n <RotateCcw class=\"size-3.5\" />\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">Reset canvas</TooltipContent>\n </Tooltip>\n </Show>\n\n {/* Copy for AI */}\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleCopyAi}\n aria-label=\"Copy context formatted for AI assistants\"\n class=\"h-7 px-2 text-xs text-muted-foreground hover:text-foreground border border-border/50 rounded-md cursor-pointer flex items-center gap-1.5\"\n >\n <Show when={aiClipboard.copied()} fallback={<Sparkles class=\"size-3\" />}>\n <Check class=\"size-3 text-emerald-500\" />\n </Show>\n <span class=\"hidden sm:inline\">{aiClipboard.copied() ? \"Copied!\" : \"Prompt\"}</span>\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">Copy code formatted for AI</TooltipContent>\n </Tooltip>\n\n {/* Copy CLI Installation Command (only when command prop is provided) */}\n <Show when={cliCommand()}>\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleCopyCmd}\n aria-label=\"Copy CLI installation command\"\n class=\"h-7 px-2 text-xs font-mono text-muted-foreground hover:text-foreground border border-border/50 rounded-md cursor-pointer flex items-center gap-1.5\"\n >\n <Show when={cmdClipboard.copied()} fallback={<Terminal class=\"size-3\" />}>\n <Check class=\"size-3 text-emerald-500\" />\n </Show>\n <span class=\"hidden md:inline\">{cmdClipboard.copied() ? \"Copied!\" : cliCommand()}</span>\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">Copy CLI command</TooltipContent>\n </Tooltip>\n </Show>\n\n {/* Fullscreen Expand Button */}\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setIsFullscreen(!isFullscreen())}\n aria-label={isFullscreen() ? \"Exit fullscreen\" : \"Expand to fullscreen\"}\n class=\"size-7 rounded-md border border-border/50 text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n <Show when={isFullscreen()} fallback={<Maximize2 class=\"size-3.5\" />}>\n <Minimize2 class=\"size-3.5 text-primary\" />\n </Show>\n </TooltipTrigger>\n <TooltipContent class=\"text-[10px] py-1 px-2\">\n {isFullscreen() ? \"Exit Fullscreen\" : \"Fullscreen Preview\"}\n </TooltipContent>\n </Tooltip>\n </div>\n </div>\n\n {/* Live Preview Canvas Content */}\n <TabsContent\n value=\"preview\"\n class={cn(\n \"relative flex-1 p-4 sm:p-6 md:p-10 flex flex-col items-center justify-center transition-all\",\n isModal ? \"min-h-[450px] flex-1\" : \"min-h-[280px]\",\n showGrid() &&\n \"bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#27272a_1px,transparent_1px)] [background-size:16px_16px]\",\n local.allowOverflow ? \"overflow-visible\" : \"overflow-x-auto\"\n )}\n >\n <div\n class={cn(\n \"w-full transition-all duration-300 flex\",\n viewportWidthClass(),\n alignmentClass(),\n viewport() !== \"desktop\" &&\n \"rounded-lg border border-dashed border-border/80 p-4 bg-background/80 shadow-xs\"\n )}\n >\n <Show when={mounted()}>\n {local.children}\n </Show>\n </div>\n </TabsContent>\n\n {/* Source Code View Content */}\n <TabsContent value=\"code\" class={cn(\"p-0 m-0\", isModal && \"flex-1 overflow-auto\")}>\n <CodeBlock\n code={local.code}\n lang={local.lang || \"tsx\"}\n filename={local.name ? `${local.name}.tsx` : undefined}\n class=\"my-0 border-0 rounded-t-none\"\n />\n </TabsContent>\n </Tabs>\n </div>\n );\n\n return (\n <>\n <Show when={!isFullscreen()}>\n {renderViewerContent(false)}\n </Show>\n\n {/* Fullscreen Portal Modal */}\n <Show when={isFullscreen()}>\n <Portal mount={typeof document !== \"undefined\" ? document.body : undefined}>\n <div class=\"fixed inset-0 z-[9999] flex flex-col bg-background/95 backdrop-blur-md p-4 sm:p-8 animate-in fade-in duration-200\">\n {renderViewerContent(true)}\n </div>\n </Portal>\n </Show>\n </>\n );\n};\n",
22
+ "type": "registry:ui"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "container",
3
+ "title": "Container",
4
+ "description": "A responsive layout container constraining maximum width with semantic padding tokens.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "ui/container.tsx",
14
+ "content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport { Dynamic } from \"solid-js/web\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const containerVariants = cva(\"w-full mx-auto px-4 sm:px-6 lg:px-8\", {\n variants: {\n size: {\n sm: \"max-w-screen-sm\",\n md: \"max-w-screen-md\",\n lg: \"max-w-screen-lg\",\n xl: \"max-w-screen-xl\",\n \"2xl\": \"max-w-screen-2xl\",\n full: \"max-w-full\",\n },\n },\n defaultVariants: {\n size: \"2xl\",\n },\n});\n\nexport interface ContainerProps<T extends ValidComponent = \"div\">\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof containerVariants> {\n /** Underlying HTML element or Solid component tag (defaults to \"div\") */\n as?: T;\n class?: string;\n}\n\n/**\n * Responsive container primitive constraining max-width with semantic padding tokens.\n */\nexport const Container = <T extends ValidComponent = \"div\">(props: ContainerProps<T>) => {\n const [local, rest] = splitProps(props as ContainerProps, [\"as\", \"size\", \"class\", \"children\"]);\n\n return (\n <Dynamic\n component={local.as || \"div\"}\n class={cn(containerVariants({ size: local.size }), local.class)}\n {...rest}\n >\n {local.children}\n </Dynamic>\n );\n};\n",
15
+ "type": "registry:ui"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "file-tree",
3
+ "title": "File Tree",
4
+ "description": "An interactive hierarchical directory file tree component built on Collapsible.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "lucide-solid"
10
+ ],
11
+ "registryDependencies": [
12
+ "collapsible"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "ui/file-tree.tsx",
17
+ "content": "import {\n createSignal,\n splitProps,\n Show,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport {\n Folder,\n FolderOpen,\n File,\n FileCode,\n FileJson,\n FileText,\n ChevronRight,\n} from \"lucide-solid\";\nimport {\n Collapsible,\n CollapsibleTrigger,\n CollapsibleContent,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface FileTreeProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Root FileTree container for displaying project directory hierarchies.\n */\nexport const FileTree: ParentComponent<FileTreeProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n return (\n <div\n class={cn(\n \"my-4 w-full rounded-lg border border-border bg-card/60 p-3 font-mono text-sm shadow-2xs select-none\",\n local.class\n )}\n {...rest}\n >\n <div class=\"space-y-0.5\">{local.children}</div>\n </div>\n );\n};\n\nexport interface FileTreeFolderProps {\n /** Folder name */\n name: string;\n /** Initial open state of the folder (default: true) */\n defaultOpen?: boolean;\n /** Controlled open state */\n open?: boolean;\n /** Callback fired when open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Custom folder icon override */\n icon?: JSX.Element;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Collapsible folder node in the FileTree.\n */\nexport const FileTreeFolder: ParentComponent<FileTreeFolderProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"name\",\n \"defaultOpen\",\n \"open\",\n \"onOpenChange\",\n \"icon\",\n \"class\",\n \"children\",\n ]);\n\n const [isOpen, setIsOpen] = createSignal(local.defaultOpen ?? true);\n\n const openState = () => (local.open !== undefined ? local.open : isOpen());\n\n const handleToggle = (next: boolean) => {\n if (local.open === undefined) {\n setIsOpen(next);\n }\n local.onOpenChange?.(next);\n };\n\n return (\n <Collapsible\n open={openState()}\n onOpenChange={handleToggle}\n class={cn(\"w-full\", local.class)}\n {...rest}\n >\n <CollapsibleTrigger class=\"group flex w-full items-center justify-start gap-1.5 rounded-md px-1.5 py-1 text-xs text-foreground transition-colors hover:bg-muted/70 cursor-pointer\">\n <ChevronRight\n class={cn(\n \"size-3.5 shrink-0 text-muted-foreground transition-transform duration-200\",\n openState() && \"rotate-90 text-foreground\"\n )}\n />\n <Show\n when={local.icon}\n fallback={\n <Show\n when={openState()}\n fallback={<Folder class=\"size-3.5 shrink-0 text-amber-500/90 dark:text-amber-400\" />}\n >\n <FolderOpen class=\"size-3.5 shrink-0 text-amber-500 dark:text-amber-400\" />\n </Show>\n }\n >\n {local.icon}\n </Show>\n <span class=\"font-medium text-foreground tracking-tight\">{local.name}</span>\n </CollapsibleTrigger>\n\n <CollapsibleContent class=\"border-l border-border/60 ml-3.5 pl-3 pt-0.5 space-y-0.5\">\n {local.children}\n </CollapsibleContent>\n </Collapsible>\n );\n};\n\nexport interface FileTreeFileProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** File name */\n name: string;\n /** Custom file icon override */\n icon?: JSX.Element;\n class?: string;\n}\n\n/**\n * Returns a smart default Lucide icon based on file extension.\n */\nfunction getFileIcon(filename: string): JSX.Element {\n const ext = filename.split(\".\").pop()?.toLowerCase();\n switch (ext) {\n case \"tsx\":\n case \"jsx\":\n case \"ts\":\n case \"js\":\n case \"mjs\":\n case \"rs\":\n case \"py\":\n case \"go\":\n return <FileCode class=\"size-3.5 shrink-0 text-sky-500 dark:text-sky-400\" />;\n case \"json\":\n return <FileJson class=\"size-3.5 shrink-0 text-amber-500 dark:text-amber-400\" />;\n case \"md\":\n case \"mdx\":\n case \"txt\":\n return <FileText class=\"size-3.5 shrink-0 text-emerald-500 dark:text-emerald-400\" />;\n default:\n return <File class=\"size-3.5 shrink-0 text-muted-foreground\" />;\n }\n}\n\n/**\n * Leaf file item inside a FileTree.\n */\nexport const FileTreeFile: ParentComponent<FileTreeFileProps> = (props) => {\n const [local, rest] = splitProps(props, [\"name\", \"icon\", \"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"flex w-full items-center justify-start gap-1.5 rounded-md px-1.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground cursor-default\",\n local.class\n )}\n {...rest}\n >\n <span class=\"size-3.5 shrink-0\" />\n <Show when={local.icon} fallback={getFileIcon(local.name)}>\n {local.icon}\n </Show>\n <span class=\"tracking-tight\">{local.name}</span>\n <Show when={local.children}>\n <div class=\"ml-auto\">{local.children}</div>\n </Show>\n </div>\n );\n};\n",
18
+ "type": "registry:ui"
19
+ }
20
+ ]
21
+ }
@@ -21,6 +21,20 @@
21
21
  "class-variance-authority"
22
22
  ]
23
23
  },
24
+ {
25
+ "name": "api-table",
26
+ "title": "API Table",
27
+ "description": "A structured, clean API reference table component for documenting props, options, and events.",
28
+ "type": "registry:ui",
29
+ "dependencies": [
30
+ "clsx",
31
+ "tailwind-merge"
32
+ ],
33
+ "registryDependencies": [
34
+ "table",
35
+ "section-heading"
36
+ ]
37
+ },
24
38
  {
25
39
  "name": "aspect-ratio",
26
40
  "title": "Aspect Ratio",
@@ -112,6 +126,18 @@
112
126
  "spinner"
113
127
  ]
114
128
  },
129
+ {
130
+ "name": "callout",
131
+ "title": "Callout",
132
+ "description": "A semantic callout and alert block with status variants, icons, and titles for documentation and notices.",
133
+ "type": "registry:ui",
134
+ "dependencies": [
135
+ "clsx",
136
+ "tailwind-merge",
137
+ "class-variance-authority",
138
+ "lucide-solid"
139
+ ]
140
+ },
115
141
  {
116
142
  "name": "card",
117
143
  "title": "Card",
@@ -135,6 +161,38 @@
135
161
  "create-controllable-signal"
136
162
  ]
137
163
  },
164
+ {
165
+ "name": "code-block",
166
+ "title": "Code Block",
167
+ "description": "A code block container with filename header, language indicator, and interactive copy to clipboard button.",
168
+ "type": "registry:ui",
169
+ "dependencies": [
170
+ "clsx",
171
+ "tailwind-merge",
172
+ "lucide-solid",
173
+ "shiki"
174
+ ],
175
+ "registryDependencies": [
176
+ "button",
177
+ "badge",
178
+ "tooltip",
179
+ "tabs",
180
+ "create-clipboard"
181
+ ]
182
+ },
183
+ {
184
+ "name": "code-group",
185
+ "title": "Code Group",
186
+ "description": "A compound tabbed code container for organizing multiple files, languages, and snippets built on Tabs.",
187
+ "type": "registry:ui",
188
+ "dependencies": [
189
+ "clsx",
190
+ "tailwind-merge"
191
+ ],
192
+ "registryDependencies": [
193
+ "tabs"
194
+ ]
195
+ },
138
196
  {
139
197
  "name": "collapsible",
140
198
  "title": "Collapsible",
@@ -180,6 +238,35 @@
180
238
  "create-keybindings"
181
239
  ]
182
240
  },
241
+ {
242
+ "name": "component-viewer",
243
+ "title": "Component Viewer",
244
+ "description": "An interactive preview canvas and code viewer container with responsive viewports, canvas grid, and AI prompts.",
245
+ "type": "registry:ui",
246
+ "dependencies": [
247
+ "clsx",
248
+ "tailwind-merge",
249
+ "lucide-solid"
250
+ ],
251
+ "registryDependencies": [
252
+ "tabs",
253
+ "button",
254
+ "tooltip",
255
+ "code-block",
256
+ "create-clipboard"
257
+ ]
258
+ },
259
+ {
260
+ "name": "container",
261
+ "title": "Container",
262
+ "description": "A responsive layout container constraining maximum width with semantic padding tokens.",
263
+ "type": "registry:ui",
264
+ "dependencies": [
265
+ "clsx",
266
+ "tailwind-merge",
267
+ "class-variance-authority"
268
+ ]
269
+ },
183
270
  {
184
271
  "name": "context-menu",
185
272
  "title": "Context Menu",
@@ -263,6 +350,20 @@
263
350
  "label"
264
351
  ]
265
352
  },
353
+ {
354
+ "name": "file-tree",
355
+ "title": "File Tree",
356
+ "description": "An interactive hierarchical directory file tree component built on Collapsible.",
357
+ "type": "registry:ui",
358
+ "dependencies": [
359
+ "clsx",
360
+ "tailwind-merge",
361
+ "lucide-solid"
362
+ ],
363
+ "registryDependencies": [
364
+ "collapsible"
365
+ ]
366
+ },
266
367
  {
267
368
  "name": "footer",
268
369
  "title": "Footer",
@@ -461,6 +562,37 @@
461
562
  "create-long-press"
462
563
  ]
463
564
  },
565
+ {
566
+ "name": "package-manager-tabs",
567
+ "title": "Package Manager Tabs",
568
+ "description": "An interactive command tab bar supporting customizable package managers (bun, pnpm, npm, yarn, deno, bunx, npx) with copy functionality.",
569
+ "type": "registry:ui",
570
+ "dependencies": [
571
+ "clsx",
572
+ "tailwind-merge",
573
+ "lucide-solid"
574
+ ],
575
+ "registryDependencies": [
576
+ "button",
577
+ "tooltip",
578
+ "tabs",
579
+ "create-clipboard"
580
+ ]
581
+ },
582
+ {
583
+ "name": "pager",
584
+ "title": "Pager",
585
+ "description": "Previous and next article navigation links with card previews for documentation and blog layouts.",
586
+ "type": "registry:ui",
587
+ "dependencies": [
588
+ "clsx",
589
+ "tailwind-merge",
590
+ "lucide-solid"
591
+ ],
592
+ "registryDependencies": [
593
+ "card"
594
+ ]
595
+ },
464
596
  {
465
597
  "name": "pagination",
466
598
  "title": "Pagination",
@@ -612,6 +744,20 @@
612
744
  "create-resize-observer"
613
745
  ]
614
746
  },
747
+ {
748
+ "name": "section-heading",
749
+ "title": "Section Heading",
750
+ "description": "A reusable heading block with title, optional badge, and description. Supports page and section variants.",
751
+ "type": "registry:ui",
752
+ "dependencies": [
753
+ "clsx",
754
+ "tailwind-merge",
755
+ "class-variance-authority"
756
+ ],
757
+ "registryDependencies": [
758
+ "badge"
759
+ ]
760
+ },
615
761
  {
616
762
  "name": "select",
617
763
  "title": "Select",
@@ -724,6 +870,19 @@
724
870
  "class-variance-authority"
725
871
  ]
726
872
  },
873
+ {
874
+ "name": "steps",
875
+ "title": "Steps",
876
+ "description": "A vertical multi-step instruction container with connecting progress line and numbered step indicators.",
877
+ "type": "registry:ui",
878
+ "dependencies": [
879
+ "clsx",
880
+ "tailwind-merge"
881
+ ],
882
+ "registryDependencies": [
883
+ "badge"
884
+ ]
885
+ },
727
886
  {
728
887
  "name": "switch",
729
888
  "title": "Switch",
@@ -737,6 +896,22 @@
737
896
  "create-controllable-signal"
738
897
  ]
739
898
  },
899
+ {
900
+ "name": "table-of-contents",
901
+ "title": "Table of Contents",
902
+ "description": "An accessible heading navigation tree with ScrollSpy tracking and smooth scrolling, composed of ScrollArea and List.",
903
+ "type": "registry:ui",
904
+ "dependencies": [
905
+ "clsx",
906
+ "tailwind-merge",
907
+ "class-variance-authority"
908
+ ],
909
+ "registryDependencies": [
910
+ "scroll-area",
911
+ "list",
912
+ "create-scroll-position"
913
+ ]
914
+ },
740
915
  {
741
916
  "name": "table",
742
917
  "title": "Table",
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "package-manager-tabs",
3
+ "title": "Package Manager Tabs",
4
+ "description": "An interactive command tab bar supporting customizable package managers (bun, pnpm, npm, yarn, deno, bunx, npx) with copy functionality.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "lucide-solid"
10
+ ],
11
+ "registryDependencies": [
12
+ "button",
13
+ "tooltip",
14
+ "tabs",
15
+ "create-clipboard"
16
+ ],
17
+ "files": [
18
+ {
19
+ "path": "ui/package-manager-tabs.tsx",
20
+ "content": "import {\n createSignal,\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 { 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 DefaultPackageManager = \"bun\" | \"pnpm\" | \"npm\" | \"yarn\" | \"bunx\" | \"npx\" | \"deno\";\n\nexport interface PackageManagerTabsProps\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** List of package managers to show in the switcher tabs */\n managers?: string[];\n /** Single CLI command template (e.g. \"add @nikala-ui/core\", \"create @nikala-ui/docs my-app\") */\n command?: string;\n /** Explicit map of custom commands per manager (e.g. { bun: \"bun add foo\", npm: \"npm i foo\" }) */\n commands?: Record<string, string>;\n /** Selected manager (controlled) */\n value?: string;\n /** Initial selected manager (uncontrolled) */\n defaultValue?: string;\n /** Callback fired when the active package manager changes */\n onChange?: (manager: string) => void;\n /** Whether to show the copy button in the header */\n copyable?: boolean;\n class?: string;\n}\n\n/**\n * Automatically formats a command string for a specific package manager.\n */\nfunction resolvePmCommand(manager: string, command: string): string {\n const trimmed = command.trim();\n const pm = manager.toLowerCase();\n\n // 1. \"create\" command\n const createMatch = trimmed.match(/^(?:bun|npm|npx|pnpm|yarn|deno)?\\s*create\\s+(.*)$/);\n if (createMatch) {\n const args = createMatch[1];\n if (pm === \"bun\" || pm === \"bunx\") return `bun create ${args}`;\n if (pm === \"pnpm\") return `pnpm create ${args}`;\n if (pm === \"yarn\") return `yarn create ${args}`;\n if (pm === \"deno\") return `deno run -A npm:create-${args}`;\n return `npm create ${args}`;\n }\n\n // 2. \"add\" / \"install\" command\n const addMatch = trimmed.match(/^(?:bun\\s+add|npm\\s+i|npm\\s+install|pnpm\\s+add|yarn\\s+add|deno\\s+add|add|install|i)\\s+(.*)$/);\n if (addMatch) {\n const args = addMatch[1];\n if (pm === \"npm\") return `npm i ${args}`;\n if (pm === \"pnpm\") return `pnpm add ${args}`;\n if (pm === \"yarn\") return `yarn add ${args}`;\n if (pm === \"deno\") return `deno add ${args}`;\n return `bun add ${args}`;\n }\n\n // 3. CLI runner (e.g. \"nikala add button\" or \"@nikala-ui/cli add button\")\n const runnerMatch = trimmed.match(/^(?:bunx|npx|pnpm\\s+dlx|yarn\\s+dlx)?\\s*(.*)$/);\n if (runnerMatch) {\n const rest = runnerMatch[1];\n if (pm === \"npx\") return `npx ${rest}`;\n if (pm === \"pnpm\") return `pnpm dlx ${rest}`;\n if (pm === \"yarn\") return `yarn dlx ${rest}`;\n if (pm === \"bun\" || pm === \"bunx\") return `bunx ${rest}`;\n return `${pm} ${rest}`;\n }\n\n return trimmed;\n}\n\nfunction highlightCliCommandText(code: string): string {\n const trimmed = code.trim();\n if (!trimmed) return \"\";\n const parts = trimmed.split(/\\s+/);\n\n let pkgIndex = 1;\n if ((parts[0] === \"pnpm\" || parts[0] === \"yarn\") && parts[1] === \"dlx\") {\n pkgIndex = 2;\n } else if (parts[0] === \"bun\" && (parts[1] === \"create\" || parts[1] === \"add\" || parts[1] === \"run\")) {\n pkgIndex = 2;\n } else if (parts[0] === \"npm\" && (parts[1] === \"create\" || parts[1] === \"run\")) {\n pkgIndex = 2;\n }\n\n const mainPart = parts.slice(0, pkgIndex).join(\" \");\n const restTokens = parts.slice(pkgIndex);\n\n const highlightedRest = restTokens\n .map((token) => {\n if (token.startsWith(\"-\")) {\n return `<span class=\"text-amber-500 dark:text-amber-400 font-medium\">${token}</span>`;\n }\n if ([\"add\", \"create\", \"init\", \"run\", \"dev\", \"build\", \"set\", \"theme\", \"diff\", \"list\"].includes(token)) {\n return `<span class=\"text-sky-500 dark:text-sky-400 font-semibold\">${token}</span>`;\n }\n return `<span class=\"text-violet-500 dark:text-violet-400 font-medium\">${token}</span>`;\n })\n .join(\" \");\n\n return `<span class=\"text-emerald-500 dark:text-emerald-400 font-semibold\">${mainPart}</span>${\n highlightedRest ? \" \" + highlightedRest : \"\"\n }`;\n}\n\nexport const PackageManagerTabs: ParentComponent<PackageManagerTabsProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"managers\",\n \"command\",\n \"commands\",\n \"value\",\n \"defaultValue\",\n \"onChange\",\n \"copyable\",\n \"class\",\n ]);\n\n const defaultManagers = [\"bun\", \"pnpm\", \"npm\", \"yarn\"];\n const managersList = () => local.managers || defaultManagers;\n\n const [internalActive, setInternalActive] = createSignal(\n local.defaultValue || managersList()[0] || \"bun\"\n );\n\n const activeManager = () => local.value ?? internalActive();\n\n const handleSelect = (pm: string) => {\n if (local.value === undefined) {\n setInternalActive(pm);\n }\n local.onChange?.(pm);\n };\n\n const currentCommand = createMemo(() => {\n const pm = activeManager();\n if (local.commands && local.commands[pm]) {\n return local.commands[pm];\n }\n if (local.command) {\n return resolvePmCommand(pm, local.command);\n }\n return \"\";\n });\n\n const clipboard = createClipboard();\n const copyable = () => local.copyable ?? true;\n\n const handleCopy = async () => {\n const text = currentCommand();\n if (!text) return;\n await clipboard.copy(text);\n };\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 {/* Header with Manager Switcher Tabs and Copy Button */}\n <div class=\"flex h-9 items-center justify-between border-b border-border/70 bg-muted/70 px-3.5 select-none\">\n {/* Switcher Tabs */}\n <Tabs\n value={activeManager()}\n onChange={handleSelect}\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\n {/* Copy Button */}\n <Show when={copyable() && currentCommand()}>\n <Tooltip>\n <TooltipTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleCopy}\n aria-label={clipboard.copied() ? \"Copied command\" : \"Copy command\"}\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 command\"}\n </TooltipContent>\n </Tooltip>\n </Show>\n </div>\n\n {/* Command Box Content */}\n <div class=\"overflow-x-auto p-4 text-[13px] leading-relaxed [scrollbar-width:thin]\">\n <pre class=\"font-mono\">\n <code>\n <span class=\"text-sky-500 dark:text-sky-400 font-semibold select-none\">$ </span>\n <span innerHTML={highlightCliCommandText(currentCommand())} />\n </code>\n </pre>\n </div>\n </div>\n );\n};\n",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "pager",
3
+ "title": "Pager",
4
+ "description": "Previous and next article navigation links with card previews for documentation and blog layouts.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "lucide-solid"
10
+ ],
11
+ "registryDependencies": [
12
+ "card"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "ui/pager.tsx",
17
+ "content": "import { splitProps, Show, children, type Component, type JSX } from \"solid-js\";\nimport { ChevronLeft, ChevronRight } from \"lucide-solid\";\nimport {\n Card,\n CardHeader,\n CardTitle,\n CardDescription,\n} from \"@/components/ui/card\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface PagerItem {\n title: string;\n href: string;\n}\n\nexport interface PagerLinkProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n title: string;\n href: string;\n type: \"prev\" | \"next\";\n class?: string;\n}\n\n/**\n * Individual Next or Previous page navigation card.\n */\nexport const PagerLink: Component<PagerLinkProps> = (props) => {\n const [local, rest] = splitProps(props, [\"title\", \"href\", \"type\", \"class\"]);\n\n const isNext = () => local.type === \"next\";\n\n return (\n <a\n href={local.href}\n class={cn(\n \"group block rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n isNext() && \"sm:col-start-2\",\n local.class\n )}\n {...rest}\n >\n <Card class=\"h-full transition-all group-hover:bg-accent/40 group-hover:shadow-2xs\">\n <CardHeader class={cn(\"p-4 flex flex-col space-y-1.5\", isNext() && \"items-end text-right\")}>\n <CardDescription class=\"flex items-center gap-1 text-xs font-medium text-muted-foreground\">\n <Show when={!isNext()}>\n <ChevronLeft class=\"size-3.5 transition-transform group-hover:-translate-x-0.5\" />\n <span>Previous</span>\n </Show>\n <Show when={isNext()}>\n <span>Next</span>\n <ChevronRight class=\"size-3.5 transition-transform group-hover:translate-x-0.5\" />\n </Show>\n </CardDescription>\n <CardTitle class=\"text-sm font-semibold tracking-tight group-hover:text-primary transition-colors line-clamp-1\">\n {local.title}\n </CardTitle>\n </CardHeader>\n </Card>\n </a>\n );\n};\n\nexport interface PagerProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Previous documentation page item */\n prev?: PagerItem | null;\n /** Next documentation page item */\n next?: PagerItem | null;\n class?: string;\n}\n\n/**\n * Documentation Next / Previous article navigation block composed of Nikala UI Card primitives.\n */\nexport const Pager: Component<PagerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"prev\", \"next\", \"class\", \"children\"]);\n const resolved = children(() => local.children);\n\n return (\n <div\n class={cn(\"grid grid-cols-1 gap-4 sm:grid-cols-2 mt-10 pt-6 border-t border-border\", local.class)}\n {...rest}\n >\n <Show\n when={resolved()}\n fallback={\n <>\n <Show\n when={local.prev}\n fallback={<span class=\"hidden sm:block\" />}\n >\n <PagerLink type=\"prev\" title={local.prev!.title} href={local.prev!.href} />\n </Show>\n <Show when={local.next}>\n <PagerLink type=\"next\" title={local.next!.title} href={local.next!.href} />\n </Show>\n </>\n }\n >\n {resolved()}\n </Show>\n </div>\n );\n};\n",
18
+ "type": "registry:ui"
19
+ }
20
+ ]
21
+ }