nimbus-docs 0.0.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["addCommand","addCommand"],"sources":["../../src/cli/_registry.generated.ts","../../src/cli/pm.ts","../../src/cli/component.ts","../../src/cli/dotenv.ts","../../src/cli/resolver.ts","../../src/cli/feature.ts","../../src/cli/index.ts"],"sourcesContent":["/**\n * Bundled registry index — auto-generated by\n * apps/www/scripts/generate-registry.ts. Do not edit by hand.\n *\n * Re-run via: pnpm --filter @nimbus/www generate-registry\n */\n\nexport type RegistryEntryType =\n | \"registry:ui\"\n | \"registry:lib\"\n | \"registry:feature\";\n\nexport interface RegistryIndexEntry {\n name: string;\n type: RegistryEntryType;\n title: string;\n description: string;\n}\n\nexport interface BundledIndex {\n version: 1;\n items: Record<string, RegistryIndexEntry>;\n}\n\nexport const REGISTRY_BASE_URL = \"https://nimbus-docs.com/registry\";\n\nexport const BUNDLED_INDEX: BundledIndex = {\n \"version\": 1,\n \"items\": {\n \"cn\": {\n \"name\": \"cn\",\n \"type\": \"registry:lib\",\n \"title\": \"cn\",\n \"description\": \"Tailwind-aware className merger built on clsx + tailwind-merge.\"\n },\n \"accordion\": {\n \"name\": \"accordion\",\n \"type\": \"registry:ui\",\n \"title\": \"Accordion\",\n \"description\": \"Vertically stacked collapsible sections.\"\n },\n \"aside\": {\n \"name\": \"aside\",\n \"type\": \"registry:ui\",\n \"title\": \"Aside\",\n \"description\": \"Generic boxed callout. Building block for Callout, Note, Warning.\"\n },\n \"badge\": {\n \"name\": \"badge\",\n \"type\": \"registry:ui\",\n \"title\": \"Badge\",\n \"description\": \"Small status / category pill.\"\n },\n \"banner\": {\n \"name\": \"banner\",\n \"type\": \"registry:ui\",\n \"title\": \"Banner\",\n \"description\": \"Site-wide dismissible announcement bar.\"\n },\n \"breadcrumbs\": {\n \"name\": \"breadcrumbs\",\n \"type\": \"registry:ui\",\n \"title\": \"Breadcrumbs\",\n \"description\": \"Page-context navigation crumbs.\"\n },\n \"callout\": {\n \"name\": \"callout\",\n \"type\": \"registry:ui\",\n \"title\": \"Callout\",\n \"description\": \"Inline note / tip / warning / danger / info card.\"\n },\n \"card\": {\n \"name\": \"card\",\n \"type\": \"registry:ui\",\n \"title\": \"Card\",\n \"description\": \"Generic content card with optional title and footer.\"\n },\n \"card-grid\": {\n \"name\": \"card-grid\",\n \"type\": \"registry:ui\",\n \"title\": \"CardGrid\",\n \"description\": \"Responsive grid layout for cards.\"\n },\n \"code\": {\n \"name\": \"code\",\n \"type\": \"registry:ui\",\n \"title\": \"Code\",\n \"description\": \"Inline / block code wrapper. Re-exports Astro's built-in <Code> (Shiki).\"\n },\n \"code-group\": {\n \"name\": \"code-group\",\n \"type\": \"registry:ui\",\n \"title\": \"CodeGroup\",\n \"description\": \"Tabbed group of code blocks.\"\n },\n \"collapsible\": {\n \"name\": \"collapsible\",\n \"type\": \"registry:ui\",\n \"title\": \"Collapsible\",\n \"description\": \"Headless show/hide primitive — building block for Accordion and Sidebar groups.\"\n },\n \"dialog\": {\n \"name\": \"dialog\",\n \"type\": \"registry:ui\",\n \"title\": \"Dialog\",\n \"description\": \"Modal dialog with focus-trap and body-scroll lock.\"\n },\n \"embed\": {\n \"name\": \"embed\",\n \"type\": \"registry:ui\",\n \"title\": \"Embed\",\n \"description\": \"Responsive iframe / video / external content wrapper.\"\n },\n \"file-tree\": {\n \"name\": \"file-tree\",\n \"type\": \"registry:ui\",\n \"title\": \"FileTree\",\n \"description\": \"Render a directory tree as nested markup.\"\n },\n \"frame\": {\n \"name\": \"frame\",\n \"type\": \"registry:ui\",\n \"title\": \"Frame\",\n \"description\": \"Decorative outer frame for screenshots and demos.\"\n },\n \"layer-card\": {\n \"name\": \"layer-card\",\n \"type\": \"registry:ui\",\n \"title\": \"LayerCard\",\n \"description\": \"Stacked-card container with sticky header. Base for CodeGroup and PackageManagers.\"\n },\n \"link-button\": {\n \"name\": \"link-button\",\n \"type\": \"registry:ui\",\n \"title\": \"LinkButton\",\n \"description\": \"Anchor styled as a button.\"\n },\n \"link-card\": {\n \"name\": \"link-card\",\n \"type\": \"registry:ui\",\n \"title\": \"LinkCard\",\n \"description\": \"Card whose entire surface is a link.\"\n },\n \"package-managers\": {\n \"name\": \"package-managers\",\n \"type\": \"registry:ui\",\n \"title\": \"PackageManagers\",\n \"description\": \"Tabbed install command block translated across npm / pnpm / yarn / bun.\"\n },\n \"pagination\": {\n \"name\": \"pagination\",\n \"type\": \"registry:ui\",\n \"title\": \"Pagination\",\n \"description\": \"Prev / next page navigation.\"\n },\n \"popover\": {\n \"name\": \"popover\",\n \"type\": \"registry:ui\",\n \"title\": \"Popover\",\n \"description\": \"Floating panel anchored to a trigger element.\"\n },\n \"search\": {\n \"name\": \"search\",\n \"type\": \"registry:ui\",\n \"title\": \"Search\",\n \"description\": \"Command-palette search dialog with a provider seam. Defaults to Pagefind.\"\n },\n \"sidebar\": {\n \"name\": \"sidebar\",\n \"type\": \"registry:ui\",\n \"title\": \"Sidebar\",\n \"description\": \"Docs sidebar with nested groups and active-link tracking.\"\n },\n \"steps\": {\n \"name\": \"steps\",\n \"type\": \"registry:ui\",\n \"title\": \"Steps\",\n \"description\": \"Numbered ordered-list with vertical connectors.\"\n },\n \"tabs\": {\n \"name\": \"tabs\",\n \"type\": \"registry:ui\",\n \"title\": \"Tabs\",\n \"description\": \"Tabbed content panels (manual + Starlight-compatible modes).\"\n },\n \"theme-toggle\": {\n \"name\": \"theme-toggle\",\n \"type\": \"registry:ui\",\n \"title\": \"ThemeToggle\",\n \"description\": \"Light / dark theme switcher button.\"\n },\n \"toc\": {\n \"name\": \"toc\",\n \"type\": \"registry:ui\",\n \"title\": \"TOC\",\n \"description\": \"On-page table of contents with active-heading tracking.\"\n },\n \"404-page\": {\n \"name\": \"404-page\",\n \"type\": \"registry:feature\",\n \"title\": \"Custom 404 page\",\n \"description\": \"Generate a brand-matched 404 page for the docs site.\"\n },\n \"ai-native\": {\n \"name\": \"ai-native\",\n \"type\": \"registry:feature\",\n \"title\": \"AI-native static surface\",\n \"description\": \"Add llms.txt, markdown variants, robots.txt, and an AgentDirective to a Nimbus docs site.\"\n },\n \"pagefind-search\": {\n \"name\": \"pagefind-search\",\n \"type\": \"registry:feature\",\n \"title\": \"Pagefind search\",\n \"description\": \"Add static Pagefind indexing and the Nimbus search dialog to an existing docs site.\"\n }\n }\n};\n","/**\n * Package-manager detection + install command helpers.\n *\n * Detection prefers lockfile presence in the user's cwd, then falls back\n * to the `npm_config_user_agent` env var the active package manager sets\n * when invoking the CLI. Finally falls back to `npm`.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport type PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n\nconst LOCKFILES: ReadonlyArray<readonly [string, PackageManager]> = [\n [\"pnpm-lock.yaml\", \"pnpm\"],\n [\"yarn.lock\", \"yarn\"],\n [\"bun.lockb\", \"bun\"],\n [\"bun.lock\", \"bun\"],\n [\"package-lock.json\", \"npm\"],\n];\n\nexport function detectPackageManager(cwd: string): PackageManager {\n for (const [lockfile, pm] of LOCKFILES) {\n if (existsSync(join(cwd, lockfile))) return pm;\n }\n const ua = process.env.npm_config_user_agent ?? \"\";\n if (ua.startsWith(\"pnpm\")) return \"pnpm\";\n if (ua.startsWith(\"yarn\")) return \"yarn\";\n if (ua.startsWith(\"bun\")) return \"bun\";\n return \"npm\";\n}\n\n/**\n * Command + args to install one or more new npm deps. Each PM picks the\n * verb that both adds to package.json AND installs:\n *\n * npm install <deps...>\n * pnpm add <deps...>\n * yarn add <deps...>\n * bun add <deps...>\n */\nexport function addCommand(\n pm: PackageManager,\n deps: string[],\n): { bin: string; args: string[] } {\n if (deps.length === 0) {\n throw new Error(\"addCommand called with empty deps\");\n }\n switch (pm) {\n case \"npm\":\n return { bin: \"npm\", args: [\"install\", ...deps] };\n case \"pnpm\":\n return { bin: \"pnpm\", args: [\"add\", ...deps] };\n case \"yarn\":\n return { bin: \"yarn\", args: [\"add\", ...deps] };\n case \"bun\":\n return { bin: \"bun\", args: [\"add\", ...deps] };\n }\n}\n","/**\n * Component / utility installer.\n *\n * Walks the resolved list of items, writes each file (per-file overwrite\n * prompt on conflict), then collects all npm `dependencies` across the\n * tree and runs `<pm> add` once for the dedup'd set.\n *\n * File destination: `<cwd>/src/<path>`. The `path` field already encodes\n * the directory layout (e.g. `components/ui/dialog/Dialog.astro`).\n */\n\nimport { spawn } from \"node:child_process\";\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join, relative } from \"node:path\";\n\nimport * as p from \"@clack/prompts\";\n\nimport { addCommand, detectPackageManager } from \"./pm.js\";\nimport type { ComponentItem } from \"./resolver.js\";\n\nexport interface InstallOptions {\n /** User's project root. */\n cwd: string;\n /** Skip overwrite prompts; assume \"overwrite\" on every conflict. */\n yes: boolean;\n}\n\nexport interface InstallReport {\n /** Individual files actually written. */\n written: string[];\n /** Registry slugs skipped wholesale by the user. */\n skipped: string[];\n npmDepsInstalled: string[];\n}\n\nexport async function installComponents(\n items: ComponentItem[],\n options: InstallOptions,\n): Promise<InstallReport> {\n const report: InstallReport = {\n written: [],\n skipped: [],\n npmDepsInstalled: [],\n };\n\n // ---- 1. Write files — atomic per registry item -------------------------\n //\n // Each item (e.g. `dialog`) is treated as an indivisible unit: when any\n // of its files conflict, we prompt once for the whole slug. Letting users\n // overwrite Dialog.astro while keeping DialogContent.astro is a footgun\n // — components are cohesive and meant to evolve together.\n const srcDir = join(options.cwd, \"src\");\n\n for (const item of items) {\n const filePlans = item.files.map((file) => {\n const targetAbs = join(srcDir, file.path);\n return {\n targetAbs,\n targetRel: relative(options.cwd, targetAbs),\n content: file.content,\n exists: existsSync(targetAbs),\n };\n });\n\n const conflicts = filePlans.filter((f) => f.exists);\n\n if (conflicts.length > 0 && !options.yes) {\n const total = filePlans.length;\n const message =\n conflicts.length === total\n ? `${item.name} is already installed (${total} file${total === 1 ? \"\" : \"s\"}). Overwrite?`\n : `${item.name} is partially installed (${conflicts.length} of ${total} file${total === 1 ? \"\" : \"s\"} present). Overwrite all?`;\n\n const choice = await p.select({\n message,\n options: [\n { value: \"overwrite\", label: \"Overwrite — replace existing files\" },\n { value: \"skip\", label: \"Skip — leave files as-is\" },\n { value: \"cancel\", label: \"Cancel install\" },\n ],\n initialValue: \"overwrite\",\n });\n\n if (p.isCancel(choice) || choice === \"cancel\") {\n p.cancel(\"Cancelled.\");\n process.exit(0);\n }\n if (choice === \"skip\") {\n report.skipped.push(item.name);\n continue;\n }\n }\n\n // Either no conflicts, --yes, or user chose overwrite. Write every file.\n for (const plan of filePlans) {\n mkdirSync(dirname(plan.targetAbs), { recursive: true });\n writeFileSync(plan.targetAbs, plan.content);\n report.written.push(plan.targetRel);\n }\n }\n\n // ---- 2. Install missing npm deps ---------------------------------------\n const allDeps = new Set<string>();\n for (const item of items) {\n for (const dep of item.dependencies) allDeps.add(dep);\n }\n\n if (allDeps.size > 0) {\n const newDeps = filterAlreadyInstalled(options.cwd, [...allDeps]);\n if (newDeps.length > 0) {\n const pm = detectPackageManager(options.cwd);\n const { bin, args } = addCommand(pm, newDeps);\n const spinner = p.spinner();\n spinner.start(`${pm} add ${newDeps.join(\" \")}`);\n try {\n await runCommand(bin, args, options.cwd);\n spinner.stop(\n `Installed ${newDeps.length} dep${newDeps.length === 1 ? \"\" : \"s\"}.`,\n );\n report.npmDepsInstalled = newDeps;\n } catch (err) {\n spinner.stop(\"Dependency install failed.\");\n p.log.warn(\n `Could not install ${newDeps.join(\", \")}. Run \\`${bin} ${args.join(\" \")}\\` manually.`,\n );\n }\n }\n }\n\n return report;\n}\n\n/**\n * Filter out deps already present in `dependencies` or `devDependencies`\n * of the user's package.json. If package.json is missing, returns all.\n */\nfunction filterAlreadyInstalled(cwd: string, deps: string[]): string[] {\n const pkgPath = join(cwd, \"package.json\");\n if (!existsSync(pkgPath)) return deps;\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const installed = new Set([\n ...Object.keys(pkg.dependencies ?? {}),\n ...Object.keys(pkg.devDependencies ?? {}),\n ]);\n return deps.filter((d) => !installed.has(d));\n } catch {\n return deps;\n }\n}\n\nfunction runCommand(\n bin: string,\n args: string[],\n cwd: string,\n): Promise<void> {\n return new Promise((resolveP, rejectP) => {\n const child = spawn(bin, args, {\n cwd,\n stdio: [\"ignore\", \"ignore\", \"inherit\"],\n });\n child.on(\"close\", (code) =>\n code === 0\n ? resolveP()\n : rejectP(new Error(`${bin} ${args.join(\" \")} exited ${code}`)),\n );\n child.on(\"error\", rejectP);\n });\n}\n","/**\n * Tiny .env loader — no dependency.\n *\n * Reads `.env` from the user's cwd at CLI startup and sets any KEY=VALUE\n * pairs into `process.env` IF the variable isn't already set (so a shell-\n * provided env always wins over the file). Supports the basic cases:\n *\n * KEY=value\n * KEY=\"quoted value\"\n * KEY='quoted value'\n * # comments\n *\n * Used so `examples/local/.env` can carry `NIMBUS_REGISTRY_URL=...` without\n * the user having to prefix every CLI invocation.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport function loadDotenv(cwd: string): void {\n const path = join(cwd, \".env\");\n if (!existsSync(path)) return;\n\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch {\n return;\n }\n\n for (const rawLine of raw.split(/\\r?\\n/)) {\n const line = rawLine.trim();\n if (!line || line.startsWith(\"#\")) continue;\n\n const eq = line.indexOf(\"=\");\n if (eq <= 0) continue;\n\n const key = line.slice(0, eq).trim();\n if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) continue;\n\n let value = line.slice(eq + 1).trim();\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n}\n","/**\n * Registry resolver.\n *\n * Two entry points:\n *\n * - `resolveComponentTree(slug)` walks `registryDependencies` transitively\n * and returns a flat ordered list of components/utilities to install\n * (dependencies first, root last). Cycles are detected as repeated\n * visits and skipped.\n *\n * - `fetchFeatureMarkdown(slug)` returns the raw markdown for an\n * agent-handoff feature; the caller decides what to do with it.\n *\n * The base URL for hosted artifacts is read from the bundled index, with\n * an `NIMBUS_REGISTRY_URL` env override for local development.\n */\n\nimport {\n BUNDLED_INDEX,\n REGISTRY_BASE_URL,\n type RegistryIndexEntry,\n} from \"./_registry.generated.js\";\n\nexport interface RegistryFile {\n path: string;\n content: string;\n}\n\nexport interface ComponentItem {\n name: string;\n type: \"registry:ui\" | \"registry:lib\";\n title: string;\n description: string;\n dependencies: string[];\n registryDependencies: string[];\n files: RegistryFile[];\n}\n\n/**\n * Read the registry base URL on every call so `.env` files loaded after\n * module-import time (see cli/dotenv.ts) are picked up. The cost is\n * negligible — string interpolation of an env var.\n */\nfunction getBaseUrl(): string {\n return (process.env.NIMBUS_REGISTRY_URL ?? REGISTRY_BASE_URL).replace(\n /\\/$/,\n \"\",\n );\n}\n\n// ---------------------------------------------------------------------------\n// Index lookup (offline — no network)\n// ---------------------------------------------------------------------------\n\nexport function getIndexEntry(slug: string): RegistryIndexEntry | undefined {\n return BUNDLED_INDEX.items[slug];\n}\n\nexport function listEntries(filter?: {\n type?: RegistryIndexEntry[\"type\"];\n}): RegistryIndexEntry[] {\n const all = Object.values(BUNDLED_INDEX.items);\n if (!filter?.type) return all;\n return all.filter((e) => e.type === filter.type);\n}\n\n// ---------------------------------------------------------------------------\n// Network: component JSON + feature markdown\n// ---------------------------------------------------------------------------\n\nasync function httpGet(url: string): Promise<Response> {\n let res: Response;\n try {\n res = await fetch(url);\n } catch (err) {\n const cause = (err as Error).message;\n throw new Error(\n `Could not reach the registry at ${url}.\\n` +\n ` Underlying error: ${cause}\\n\\n` +\n ` Things to try:\\n` +\n ` - Is the registry server running? Start it with \\`pnpm local\\` (in the monorepo root).\\n` +\n ` - Override the URL: NIMBUS_REGISTRY_URL=https://example.com nimbus add ...\\n` +\n ` - Check the value in your project's .env file.`,\n );\n }\n if (!res.ok) {\n throw new Error(\n `Registry returned ${res.status} ${res.statusText} for ${url}. ` +\n `The server is up but doesn't know about this slug — check \\`nimbus list\\` for valid names.`,\n );\n }\n return res;\n}\n\nexport async function fetchComponent(slug: string): Promise<ComponentItem> {\n const res = await httpGet(`${getBaseUrl()}/components/${slug}.json`);\n return (await res.json()) as ComponentItem;\n}\n\nexport async function fetchFeatureMarkdown(slug: string): Promise<string> {\n const res = await httpGet(`${getBaseUrl()}/features/${slug}.md`);\n return await res.text();\n}\n\n// ---------------------------------------------------------------------------\n// Transitive dep resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Depth-first walk of registryDependencies. Returns items in install order\n * (deps before dependents), deduplicated by slug.\n */\nexport async function resolveComponentTree(\n rootSlug: string,\n): Promise<ComponentItem[]> {\n const visited = new Set<string>();\n const ordered: ComponentItem[] = [];\n\n async function visit(slug: string): Promise<void> {\n if (visited.has(slug)) return;\n visited.add(slug);\n\n const item = await fetchComponent(slug);\n\n // Walk deps first so they're earlier in the install order.\n for (const dep of item.registryDependencies) {\n await visit(dep);\n }\n\n ordered.push(item);\n }\n\n await visit(rootSlug);\n return ordered;\n}\n","/**\n * Feature installer — Flue-style agent-handoff.\n *\n * Same rule Flue uses for `flue add`: if `--print` is set OR\n * `determineAgent()` says the CLI is running inside a known coding agent,\n * the markdown is piped to stdout for the agent to consume. Otherwise we\n * print human-friendly instructions on stderr telling the user exactly\n * how to pipe the output to their agent of choice.\n *\n * No picker, no clipboard mode — the printed pipe commands cover both.\n */\n\nimport { determineAgent } from \"@vercel/detect-agent\";\n\nimport { fetchFeatureMarkdown } from \"./resolver.js\";\n\nexport interface FeatureInstallOptions {\n /** Force markdown to stdout regardless of agent detection. */\n print: boolean;\n}\n\nexport async function installFeature(\n slug: string,\n options: FeatureInstallOptions,\n): Promise<void> {\n const markdown = await fetchFeatureMarkdown(slug);\n\n // Same predicate as Flue: explicit --print, or detection says we're\n // running inside a known agent (which captures our stdout).\n const detected = await determineAgent().catch(() => ({\n isAgent: false as const,\n }));\n const isAgentMode = options.print || detected.isAgent === true;\n\n if (isAgentMode) {\n process.stdout.write(markdown);\n if (!markdown.endsWith(\"\\n\")) process.stdout.write(\"\\n\");\n return;\n }\n\n printHumanInstructions(slug);\n}\n\n/**\n * Stderr-only. We don't put this on stdout because if the user pipes our\n * output anywhere by accident, only the markdown should reach the agent.\n *\n * Formatting mirrors Flue's `printHumanInstructions` 1:1 — agents listed\n * with a blank line between the \"first-tier\" CLIs (claude/codex/cursor-agent)\n * and the rest (opencode/pi).\n */\nfunction printHumanInstructions(slug: string): void {\n const cmd = `nimbus add ${slug}`;\n const stream = process.stderr;\n stream.write(`${cmd}\\n\\n`);\n stream.write(\"To install this feature, pipe it to your coding agent:\\n\\n\");\n stream.write(` ${cmd} --print | claude\\n`);\n stream.write(` ${cmd} --print | codex\\n`);\n stream.write(` ${cmd} --print | cursor-agent\\n\\n`);\n stream.write(` ${cmd} --print | opencode\\n`);\n stream.write(` ${cmd} --print | pi\\n`);\n stream.write(\"Or paste this prompt into any agent:\\n\\n\");\n stream.write(` Run \"${cmd} --print\" and follow the instructions.\\n`);\n}\n","#!/usr/bin/env node\n\n/**\n * `nimbus` CLI entry.\n *\n * Surface:\n *\n * nimbus → list (table of installable items)\n * nimbus list → list\n * nimbus list --type ui|lib|feature\n * nimbus add → list\n * nimbus add <slug> → install (component path or feature path)\n * nimbus add <slug> --yes → component: skip overwrite prompts\n * nimbus add <slug> --print → feature: print markdown to stdout (skip detect)\n *\n * Feature behavior mirrors Flue's `add` command: print markdown to stdout\n * iff `--print` OR an agent is detected; otherwise print human-friendly\n * pipe instructions to stderr.\n *\n * The bundled index makes `list` (and `add` with no slug) work offline.\n * Per-item content is fetched from `REGISTRY_BASE_URL` only when actually\n * installing a slug — override via `NIMBUS_REGISTRY_URL` for local dev.\n */\n\nimport mri from \"mri\";\nimport * as p from \"@clack/prompts\";\n\nimport { BUNDLED_INDEX } from \"./_registry.generated.js\";\nimport { installComponents } from \"./component.js\";\nimport { loadDotenv } from \"./dotenv.js\";\nimport { installFeature } from \"./feature.js\";\nimport {\n getIndexEntry,\n listEntries,\n resolveComponentTree,\n} from \"./resolver.js\";\n\n// Load .env from the user's cwd so per-project NIMBUS_REGISTRY_URL (and\n// any future env vars) work without shell prefixes. Shell-provided vars\n// always win (loadDotenv only sets undefined keys).\nloadDotenv(process.cwd());\n\ndeclare const __APP_VERSION__: string;\n\ninterface CliArgs {\n _: string[];\n yes: boolean;\n print: boolean;\n help: boolean;\n version: boolean;\n type?: string;\n}\n\nconst HELP = `\n Usage: nimbus <command> [args]\n\n Commands:\n list [--type ui|lib|feature] List available registry items\n add Same as \\`list\\`\n add <slug> Install a component or hand off a feature\n\n Flags:\n --yes, -y Component: overwrite conflicts without prompting\n --print Feature: print markdown to stdout (skip agent detect)\n --type <ui|lib|feature> \\`list\\`: filter by type\n --help, -h\n --version, -v\n\n Examples:\n nimbus add dialog # component: resolve + install\n nimbus add 404-page # feature: detect agent or print\n # pipe instructions for humans\n nimbus add 404-page --print | claude # explicit pipe to claude\n nimbus add 404-page --print | codex # …or any other agent\n`;\n\nasync function main(): Promise<void> {\n const args = mri(process.argv.slice(2), {\n boolean: [\"yes\", \"print\", \"help\", \"version\"],\n string: [\"type\"],\n alias: { y: \"yes\", h: \"help\", v: \"version\" },\n }) as unknown as CliArgs;\n\n if (args.help) {\n process.stdout.write(HELP);\n return;\n }\n if (args.version) {\n process.stdout.write(`${__APP_VERSION__}\\n`);\n return;\n }\n\n const [command, slug] = args._;\n\n if (command === \"list\" || (command === \"add\" && !slug) || !command) {\n listCommand(args.type);\n return;\n }\n\n if (command === \"add\") {\n await addCommand(slug!, {\n yes: args.yes,\n print: args.print,\n });\n return;\n }\n\n p.log.error(`Unknown command: \\`${command}\\`. Try \\`nimbus --help\\`.`);\n process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// `nimbus list`\n// ---------------------------------------------------------------------------\n\nfunction listCommand(typeFilter: string | undefined): void {\n const typeMap: Record<string, \"registry:ui\" | \"registry:lib\" | \"registry:feature\"> = {\n ui: \"registry:ui\",\n lib: \"registry:lib\",\n feature: \"registry:feature\",\n };\n\n const filter =\n typeFilter && typeFilter in typeMap\n ? { type: typeMap[typeFilter] }\n : undefined;\n\n if (typeFilter && !(typeFilter in typeMap)) {\n p.log.error(\n `Unknown --type \"${typeFilter}\". Valid: ui, lib, feature.`,\n );\n process.exit(1);\n }\n\n const items = listEntries(filter);\n if (items.length === 0) {\n p.log.info(\"No items match the filter.\");\n return;\n }\n\n // Group by type for readability.\n const grouped: Record<string, typeof items> = {\n \"registry:ui\": [],\n \"registry:lib\": [],\n \"registry:feature\": [],\n };\n for (const item of items) grouped[item.type]!.push(item);\n\n const labels: Record<string, string> = {\n \"registry:ui\": \"Components\",\n \"registry:lib\": \"Utilities\",\n \"registry:feature\": \"Features\",\n };\n const widths = items.reduce(\n (m, i) => Math.max(m, i.name.length),\n 0,\n );\n\n process.stdout.write(\"\\n\");\n for (const [type, label] of Object.entries(labels)) {\n const group = grouped[type];\n if (!group || group.length === 0) continue;\n process.stdout.write(` ${label}\\n`);\n for (const item of group) {\n process.stdout.write(\n ` ${item.name.padEnd(widths + 2)}${item.description}\\n`,\n );\n }\n process.stdout.write(\"\\n\");\n }\n process.stdout.write(\n ` Install: nimbus add <name> · ${items.length} item${items.length === 1 ? \"\" : \"s\"}\\n\\n`,\n );\n}\n\n// ---------------------------------------------------------------------------\n// `nimbus add <slug>`\n// ---------------------------------------------------------------------------\n\nasync function addCommand(\n slug: string,\n flags: { yes: boolean; print: boolean },\n): Promise<void> {\n const entry = getIndexEntry(slug);\n if (!entry) {\n p.log.error(\n `Unknown registry item: \\`${slug}\\`. Try \\`nimbus list\\` to see what's available.`,\n );\n process.exit(1);\n }\n\n if (entry.type === \"registry:feature\") {\n await installFeature(slug, { print: flags.print });\n return;\n }\n\n // Component / utility path.\n p.intro(`nimbus add ${slug}`);\n p.log.info(`${entry.title} — ${entry.description}`);\n\n const spinner = p.spinner();\n spinner.start(\"Resolving dependencies\");\n let items;\n try {\n items = await resolveComponentTree(slug);\n spinner.stop(\n `Resolved ${items.length} item${items.length === 1 ? \"\" : \"s\"}.`,\n );\n } catch (err) {\n spinner.stop(\"Failed to resolve.\");\n p.log.error((err as Error).message);\n process.exit(1);\n }\n\n if (items.length > 1) {\n p.log.message(\n \"Install order:\\n \" + items.map((i) => i.name).join(\" → \"),\n );\n }\n\n const report = await installComponents(items, {\n cwd: process.cwd(),\n yes: flags.yes,\n });\n\n const lines: string[] = [];\n if (report.written.length > 0) {\n lines.push(`✓ Wrote ${report.written.length} file${report.written.length === 1 ? \"\" : \"s\"}`);\n }\n if (report.skipped.length > 0) {\n lines.push(`↷ Skipped: ${report.skipped.join(\", \")}`);\n }\n if (report.npmDepsInstalled.length > 0) {\n lines.push(\n `+ Installed ${report.npmDepsInstalled.length} npm dep${report.npmDepsInstalled.length === 1 ? \"\" : \"s\"}: ${report.npmDepsInstalled.join(\", \")}`,\n );\n }\n\n if (lines.length === 0) {\n p.outro(\"Nothing to do.\");\n } else {\n p.outro(lines.join(\"\\n\"));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Entrypoint\n// ---------------------------------------------------------------------------\n\nmain().catch((err) => {\n p.log.error(`${(err as Error).message}`);\n process.exit(1);\n});\n\n// Tell TS BUNDLED_INDEX is used (so no `verbatimModuleSyntax` warning).\nvoid BUNDLED_INDEX;\n"],"mappings":"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"}
@@ -0,0 +1,167 @@
1
+ //#region src/client/mount.d.ts
2
+ /**
3
+ * mount.ts — Discover, mount, and unmount component instances.
4
+ *
5
+ * The single entry point used by every `*.client.ts` to wire its component.
6
+ * Handles three concerns:
7
+ *
8
+ * 1. Initial discovery — finds elements matching `selector` and calls `init`
9
+ * on each, storing the returned teardown.
10
+ * 2. View transitions — on `astro:before-swap`, runs every teardown so
11
+ * document/window listeners come down before the DOM is replaced.
12
+ * 3. Re-mount — on `astro:page-load`, re-runs discovery against the new DOM.
13
+ *
14
+ * The init function receives the root element and returns a `destroy()`
15
+ * function. The root element is the keying mechanism — calling mount again
16
+ * against an already-tracked element is a no-op.
17
+ *
18
+ * Usage:
19
+ *
20
+ * mount("[data-nb-collapsible]", (root) => {
21
+ * const disclosure = makeDisclosure({ ... });
22
+ * return () => disclosure.destroy();
23
+ * });
24
+ */
25
+ type Init = (root: HTMLElement) => () => void;
26
+ declare function mount(selector: string, init: Init): void;
27
+ //#endregion
28
+ //#region src/client/disclosure.d.ts
29
+ /**
30
+ * disclosure.ts — Open/close state with ARIA wiring.
31
+ *
32
+ * The shared module for any "click trigger, reveals content" pattern.
33
+ * Owns:
34
+ * - open/closed state (in-memory + reflected as `data-nb-state` on both
35
+ * trigger and content)
36
+ * - ARIA: `aria-expanded` on trigger, `aria-controls` linking to content
37
+ * - Click handler on the trigger
38
+ *
39
+ * Animation is intentionally out of scope — CSS targets the `data-nb-state`
40
+ * attribute and runs whatever transition the component wants. Returning
41
+ * a teardown means the caller can clean up on unmount.
42
+ *
43
+ * Used by: Collapsible, and any future Accordion / Sidebar group /
44
+ * dismissable Banner that wants the standard disclosure semantics.
45
+ */
46
+ interface DisclosureOptions {
47
+ /** The element users click to toggle. Usually a `<button>`. */
48
+ trigger: HTMLElement;
49
+ /** The element that's shown/hidden. Gets `id` + `data-nb-state`. */
50
+ content: HTMLElement;
51
+ /** Initial open state. Default `false`. */
52
+ defaultOpen?: boolean;
53
+ /** Called whenever open changes. */
54
+ onOpenChange?: (open: boolean) => void;
55
+ }
56
+ interface DisclosureInstance {
57
+ open(): void;
58
+ close(): void;
59
+ toggle(): void;
60
+ isOpen(): boolean;
61
+ destroy(): void;
62
+ }
63
+ declare function makeDisclosure(opts: DisclosureOptions): DisclosureInstance;
64
+ //#endregion
65
+ //#region src/client/tabs-controller.d.ts
66
+ /**
67
+ * Shared tab activation primitive.
68
+ *
69
+ * Handles aria-selected, panel visibility, roving tabindex,
70
+ * keyboard navigation, sliding indicator, and cross-instance sync.
71
+ *
72
+ * Used by: Tabs.astro, PackageManagers.astro
73
+ */
74
+ interface TabsConfig {
75
+ /** Root container holding tabs + panels */
76
+ container: Element;
77
+ /** CSS selector for tab trigger buttons */
78
+ tabSelector: string;
79
+ /** CSS selector for tab panels */
80
+ panelSelector: string;
81
+ /** Optional sliding indicator element */
82
+ indicator?: HTMLElement | null;
83
+ /** Enable roving tabindex + arrow key navigation (default: true) */
84
+ rovingTabindex?: boolean;
85
+ /** Cross-instance persistence config */
86
+ sync?: {
87
+ key: string;
88
+ storage?: "local" | "session"; /** Extract sync label from a tab element. Default: textContent.trim() */
89
+ getLabel?: (tab: HTMLElement) => string;
90
+ };
91
+ /** Called after a tab is activated */
92
+ onActivate?: (index: number) => void;
93
+ }
94
+ interface TabsInstance {
95
+ activate(index: number, options?: {
96
+ emitSync?: boolean;
97
+ }): void;
98
+ readonly currentIndex: number;
99
+ destroy(): void;
100
+ }
101
+ declare function initTabs(config: TabsConfig): TabsInstance;
102
+ //#endregion
103
+ //#region src/client/scroll-lock.d.ts
104
+ /**
105
+ * scroll-lock.ts — Body scroll lock with scrollbar-width compensation.
106
+ *
107
+ * Prevents background scrolling when a modal/overlay is open.
108
+ * Compensates for the scrollbar disappearing to avoid layout shift
109
+ * (visible on Windows/Linux where scrollbars have width).
110
+ *
111
+ * Uses a data attribute + CSS for the overflow lock, and inline
112
+ * paddingRight for the scrollbar compensation.
113
+ *
114
+ * Used by: Dialog (and any future overlay primitive).
115
+ */
116
+ declare function lockScroll(): void;
117
+ declare function unlockScroll(): void;
118
+ //#endregion
119
+ //#region src/client/ids.d.ts
120
+ /**
121
+ * Generate a unique ID with a prefix, scoped to the page.
122
+ *
123
+ * Used to build ARIA relationships (`aria-controls` / `aria-labelledby`)
124
+ * between elements that don't have a stable author-provided id.
125
+ */
126
+ declare function generateId(prefix: string): string;
127
+ //#endregion
128
+ //#region src/client/dom.d.ts
129
+ /**
130
+ * Shared DOM constants for client-side interaction primitives.
131
+ */
132
+ /** CSS selector for focusable elements within a container. */
133
+ declare const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([disabled]):not([tabindex=\"-1\"])";
134
+ //#endregion
135
+ //#region src/client/code-copy.d.ts
136
+ /**
137
+ * code-copy.ts — Injects a Nimbus-styled copy button into every Shiki
138
+ * code block rendered by Astro's built-in `<Code>` and fenced
139
+ * code blocks in MDX.
140
+ *
141
+ * Astro emits `<pre class="astro-code shiki ...">` for syntax-highlighted
142
+ * blocks. We attach a copy button positioned in the top-right corner.
143
+ *
144
+ * Page-wide enhancement, not tied to a single component. Call `codeCopy()`
145
+ * once (e.g. from BaseLayout). Uses `mount` for lifecycle so the buttons
146
+ * are torn down on view transitions and re-added against the new DOM.
147
+ *
148
+ * The original code text comes from the `<code>` element's textContent —
149
+ * Shiki's wrapper spans flatten down to the raw source.
150
+ *
151
+ * Code blocks rendered inside [data-cg-row] (CodeGroup) are skipped
152
+ * because CodeGroup brings its own copy button per panel.
153
+ */
154
+ /** Wire copy buttons onto all Shiki code blocks on the page. Call once. */
155
+ declare function codeCopy(): void;
156
+ //#endregion
157
+ //#region src/client/heading-anchors.d.ts
158
+ /** Add hoverable self-links to markdown headings with ids. */
159
+ /**
160
+ * Add hoverable `#` self-links to all `h2`–`h4` headings in `.docs-content`.
161
+ * Re-runs on `astro:page-load` for View Transitions. Call once (e.g. from
162
+ * BaseLayout).
163
+ */
164
+ declare function headingAnchors(): void;
165
+ //#endregion
166
+ export { type DisclosureInstance, type DisclosureOptions, FOCUSABLE, type TabsConfig, type TabsInstance, codeCopy, generateId, headingAnchors, initTabs, lockScroll, makeDisclosure, mount, unlockScroll };
167
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../src/client/mount.ts","../src/client/disclosure.ts","../src/client/tabs-controller.ts","../src/client/scroll-lock.ts","../src/client/ids.ts","../src/client/dom.ts","../src/client/code-copy.ts","../src/client/heading-anchors.ts"],"mappings":";;;;;;;AA0BA;;;;;;;;;;;;ACNA;;;;;KDIK,IAAA,IAAQ,IAAA,EAAM,WAAA;AAAA,iBAEH,KAAA,CAAM,QAAA,UAAkB,IAAA,EAAM,IAAA;;;;;;;;;AAA9C;;;;;;;;;;;UCNiB,iBAAA;EAAA;EAEf,OAAA,EAAS,WAAA;;EAET,OAAA,EAAS,WAAA;EAFT;EAIA,WAAA;EAFA;EAIA,YAAA,IAAgB,IAAA;AAAA;AAAA,UAGD,kBAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;EACA,MAAA;EACA,OAAA;AAAA;AAAA,iBAGc,cAAA,CAAe,IAAA,EAAM,iBAAA,GAAoB,kBAAA;;;;;;;;;ADbzD;;UEfiB,UAAA;EFeiC;EEbhD,SAAA,EAAW,OAAA;EFaiC;EEX5C,WAAA;EFWgD;EEThD,aAAA;;EAEA,SAAA,GAAY,WAAA;;EAEZ,cAAA;EDDgC;ECGhC,IAAA;IACE,GAAA;IACA,OAAA,wBDHO;ICKP,QAAA,IAAY,GAAA,EAAK,WAAA;EAAA;EDDnB;ECIA,UAAA,IAAc,KAAA;AAAA;AAAA,UAGC,YAAA;EACf,QAAA,CAAS,KAAA,UAAe,OAAA;IAAY,QAAA;EAAA;EAAA,SAC3B,YAAA;EACT,OAAA;AAAA;AAAA,iBAGc,QAAA,CAAS,MAAA,EAAQ,UAAA,GAAa,YAAA;;;;;;;;;AFb9C;;;;;;iBGRgB,UAAA,CAAA;AAAA,iBAaA,YAAA,CAAA;;;;;;;;;iBCtBA,UAAA,CAAW,MAAA;;;;;;;cCJd,SAAA;;;;;;;;;ALqBb;;;;;;;;;;;;ACNA;AAAA,iBK6DgB,QAAA,CAAA;;;;;;;;;iBC5DA,cAAA,CAAA"}
package/dist/client.js ADDED
@@ -0,0 +1,367 @@
1
+ //#region src/client/mount.ts
2
+ function mount(selector, init) {
3
+ const instances = /* @__PURE__ */ new Map();
4
+ function setup() {
5
+ document.querySelectorAll(selector).forEach((el) => {
6
+ if (instances.has(el)) return;
7
+ instances.set(el, init(el));
8
+ });
9
+ }
10
+ function teardown() {
11
+ instances.forEach((destroy) => destroy());
12
+ instances.clear();
13
+ }
14
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", setup, { once: true });
15
+ else setup();
16
+ document.addEventListener("astro:before-swap", teardown);
17
+ document.addEventListener("astro:page-load", setup);
18
+ }
19
+
20
+ //#endregion
21
+ //#region src/client/ids.ts
22
+ /**
23
+ * Generate a unique ID with a prefix, scoped to the page.
24
+ *
25
+ * Used to build ARIA relationships (`aria-controls` / `aria-labelledby`)
26
+ * between elements that don't have a stable author-provided id.
27
+ */
28
+ let counter = 0;
29
+ function generateId(prefix) {
30
+ counter += 1;
31
+ return `${prefix}-${counter}`;
32
+ }
33
+
34
+ //#endregion
35
+ //#region src/client/disclosure.ts
36
+ /**
37
+ * disclosure.ts — Open/close state with ARIA wiring.
38
+ *
39
+ * The shared module for any "click trigger, reveals content" pattern.
40
+ * Owns:
41
+ * - open/closed state (in-memory + reflected as `data-nb-state` on both
42
+ * trigger and content)
43
+ * - ARIA: `aria-expanded` on trigger, `aria-controls` linking to content
44
+ * - Click handler on the trigger
45
+ *
46
+ * Animation is intentionally out of scope — CSS targets the `data-nb-state`
47
+ * attribute and runs whatever transition the component wants. Returning
48
+ * a teardown means the caller can clean up on unmount.
49
+ *
50
+ * Used by: Collapsible, and any future Accordion / Sidebar group /
51
+ * dismissable Banner that wants the standard disclosure semantics.
52
+ */
53
+ function makeDisclosure(opts) {
54
+ const { trigger, content, defaultOpen = false, onOpenChange } = opts;
55
+ let open = defaultOpen;
56
+ if (!content.id) content.id = generateId("nb-disclosure");
57
+ trigger.setAttribute("aria-controls", content.id);
58
+ function syncState() {
59
+ const state = open ? "open" : "closed";
60
+ trigger.setAttribute("data-nb-state", state);
61
+ content.setAttribute("data-nb-state", state);
62
+ trigger.setAttribute("aria-expanded", String(open));
63
+ }
64
+ function setOpen(value) {
65
+ if (open === value) return;
66
+ open = value;
67
+ syncState();
68
+ onOpenChange?.(value);
69
+ }
70
+ function handleClick(e) {
71
+ e.preventDefault();
72
+ setOpen(!open);
73
+ }
74
+ syncState();
75
+ trigger.addEventListener("click", handleClick);
76
+ return {
77
+ open() {
78
+ setOpen(true);
79
+ },
80
+ close() {
81
+ setOpen(false);
82
+ },
83
+ toggle() {
84
+ setOpen(!open);
85
+ },
86
+ isOpen() {
87
+ return open;
88
+ },
89
+ destroy() {
90
+ trigger.removeEventListener("click", handleClick);
91
+ }
92
+ };
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/client/dom.ts
97
+ /**
98
+ * Shared DOM constants for client-side interaction primitives.
99
+ */
100
+ /** CSS selector for focusable elements within a container. */
101
+ const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([disabled]):not([tabindex=\"-1\"])";
102
+
103
+ //#endregion
104
+ //#region src/client/tabs-controller.ts
105
+ /**
106
+ * Shared tab activation primitive.
107
+ *
108
+ * Handles aria-selected, panel visibility, roving tabindex,
109
+ * keyboard navigation, sliding indicator, and cross-instance sync.
110
+ *
111
+ * Used by: Tabs.astro, PackageManagers.astro
112
+ */
113
+ function initTabs(config) {
114
+ const { container, tabSelector, panelSelector, indicator = null, rovingTabindex = true, sync, onActivate } = config;
115
+ const tabs = Array.from(container.querySelectorAll(tabSelector));
116
+ const panels = Array.from(container.querySelectorAll(panelSelector));
117
+ const tablist = container.querySelector("[role=tablist]") ?? container;
118
+ let currentIndex = -1;
119
+ let isInitialActivation = true;
120
+ function getStorage(kind) {
121
+ try {
122
+ return kind === "session" ? sessionStorage : localStorage;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+ function getLabel(tab) {
128
+ return sync?.getLabel?.(tab) ?? tab.textContent?.trim() ?? "";
129
+ }
130
+ function updateIndicator(index) {
131
+ if (!indicator || !tabs[index]) return;
132
+ indicator.style.left = `${tabs[index].offsetLeft}px`;
133
+ indicator.style.width = `${tabs[index].offsetWidth}px`;
134
+ }
135
+ function activate(index, options) {
136
+ const emitSync = options?.emitSync ?? true;
137
+ if (index === currentIndex) return;
138
+ const scrollBefore = container.getBoundingClientRect().top;
139
+ currentIndex = index;
140
+ tabs.forEach((tab, i) => {
141
+ const active = i === index;
142
+ tab.setAttribute("aria-selected", String(active));
143
+ if (rovingTabindex) tab.setAttribute("tabindex", active ? "0" : "-1");
144
+ });
145
+ panels.forEach((panel, i) => {
146
+ const visible = i === index;
147
+ panel.hidden = !visible;
148
+ if (visible) if (!(panel.querySelector(FOCUSABLE) !== null)) panel.setAttribute("tabindex", "0");
149
+ else panel.removeAttribute("tabindex");
150
+ });
151
+ updateIndicator(index);
152
+ onActivate?.(index);
153
+ if (emitSync && !isInitialActivation) {
154
+ const delta = container.getBoundingClientRect().top - scrollBefore;
155
+ if (Math.abs(delta) > 1) window.scrollTo({
156
+ top: window.scrollY + delta,
157
+ behavior: "instant"
158
+ });
159
+ }
160
+ isInitialActivation = false;
161
+ if (sync && emitSync) {
162
+ const label = getLabel(tabs[index]);
163
+ getStorage(sync.storage === "session" ? "session" : "local")?.setItem(sync.key, label);
164
+ window.dispatchEvent(new CustomEvent("ui-tab-sync", { detail: {
165
+ key: sync.key,
166
+ label,
167
+ origin: container
168
+ } }));
169
+ }
170
+ }
171
+ let initialIndex = 0;
172
+ if (sync) {
173
+ const saved = getStorage(sync.storage === "session" ? "session" : "local")?.getItem(sync.key);
174
+ if (saved) {
175
+ const idx = tabs.findIndex((t) => getLabel(t) === saved);
176
+ if (idx >= 0) initialIndex = idx;
177
+ }
178
+ }
179
+ function handleClick(e) {
180
+ const target = e.target.closest(tabSelector);
181
+ if (!target) return;
182
+ const idx = tabs.indexOf(target);
183
+ if (idx >= 0) {
184
+ activate(idx);
185
+ if (rovingTabindex) target.focus();
186
+ }
187
+ }
188
+ function handleKeydown(e) {
189
+ if (!rovingTabindex) return;
190
+ const ci = tabs.indexOf(e.target);
191
+ if (ci < 0) return;
192
+ let next;
193
+ switch (e.key) {
194
+ case "ArrowRight":
195
+ next = ci + 1;
196
+ break;
197
+ case "ArrowLeft":
198
+ next = ci - 1;
199
+ break;
200
+ case "Home":
201
+ next = 0;
202
+ break;
203
+ case "End":
204
+ next = tabs.length - 1;
205
+ break;
206
+ default: return;
207
+ }
208
+ if (!tabs[next]) return;
209
+ e.preventDefault();
210
+ activate(next);
211
+ tabs[next].focus();
212
+ }
213
+ function handleSync(e) {
214
+ const detail = e.detail;
215
+ if (detail.key === sync?.key && detail.origin !== container) {
216
+ const idx = tabs.findIndex((t) => getLabel(t) === detail.label);
217
+ if (idx >= 0) activate(idx, { emitSync: false });
218
+ }
219
+ }
220
+ tablist.addEventListener("click", handleClick);
221
+ tablist.addEventListener("keydown", handleKeydown);
222
+ if (sync) window.addEventListener("ui-tab-sync", handleSync);
223
+ if (indicator) indicator.style.transition = "none";
224
+ activate(initialIndex);
225
+ if (indicator) {
226
+ indicator.offsetHeight;
227
+ indicator.style.transition = "";
228
+ }
229
+ return {
230
+ activate,
231
+ get currentIndex() {
232
+ return currentIndex;
233
+ },
234
+ destroy() {
235
+ tablist.removeEventListener("click", handleClick);
236
+ tablist.removeEventListener("keydown", handleKeydown);
237
+ if (sync) window.removeEventListener("ui-tab-sync", handleSync);
238
+ }
239
+ };
240
+ }
241
+
242
+ //#endregion
243
+ //#region src/client/scroll-lock.ts
244
+ /**
245
+ * scroll-lock.ts — Body scroll lock with scrollbar-width compensation.
246
+ *
247
+ * Prevents background scrolling when a modal/overlay is open.
248
+ * Compensates for the scrollbar disappearing to avoid layout shift
249
+ * (visible on Windows/Linux where scrollbars have width).
250
+ *
251
+ * Uses a data attribute + CSS for the overflow lock, and inline
252
+ * paddingRight for the scrollbar compensation.
253
+ *
254
+ * Used by: Dialog (and any future overlay primitive).
255
+ */
256
+ const ATTR = "data-scroll-locked";
257
+ let lockCount = 0;
258
+ let savedPaddingRight = "";
259
+ function lockScroll() {
260
+ lockCount++;
261
+ if (lockCount > 1) return;
262
+ const scrollbarW = window.innerWidth - document.documentElement.clientWidth;
263
+ savedPaddingRight = document.body.style.paddingRight;
264
+ document.body.setAttribute(ATTR, "");
265
+ if (scrollbarW > 0) document.body.style.paddingRight = `${scrollbarW}px`;
266
+ }
267
+ function unlockScroll() {
268
+ if (lockCount === 0) return;
269
+ lockCount--;
270
+ if (lockCount > 0) return;
271
+ document.body.removeAttribute(ATTR);
272
+ document.body.style.paddingRight = savedPaddingRight;
273
+ }
274
+
275
+ //#endregion
276
+ //#region src/client/code-copy.ts
277
+ /**
278
+ * code-copy.ts — Injects a Nimbus-styled copy button into every Shiki
279
+ * code block rendered by Astro's built-in `<Code>` and fenced
280
+ * code blocks in MDX.
281
+ *
282
+ * Astro emits `<pre class="astro-code shiki ...">` for syntax-highlighted
283
+ * blocks. We attach a copy button positioned in the top-right corner.
284
+ *
285
+ * Page-wide enhancement, not tied to a single component. Call `codeCopy()`
286
+ * once (e.g. from BaseLayout). Uses `mount` for lifecycle so the buttons
287
+ * are torn down on view transitions and re-added against the new DOM.
288
+ *
289
+ * The original code text comes from the `<code>` element's textContent —
290
+ * Shiki's wrapper spans flatten down to the raw source.
291
+ *
292
+ * Code blocks rendered inside [data-cg-row] (CodeGroup) are skipped
293
+ * because CodeGroup brings its own copy button per panel.
294
+ */
295
+ const COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"/></svg>`;
296
+ const CHECK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34Z"/></svg>`;
297
+ function getCodeText(pre) {
298
+ return pre.querySelector("code")?.textContent ?? pre.textContent ?? "";
299
+ }
300
+ function initCodeCopy(pre) {
301
+ if (pre.closest("[data-cg-row]")) return () => {};
302
+ const btn = document.createElement("button");
303
+ btn.type = "button";
304
+ btn.className = "nb-code-copy";
305
+ btn.setAttribute("aria-label", "Copy code to clipboard");
306
+ btn.innerHTML = COPY_ICON;
307
+ let resetTimer;
308
+ async function handleClick() {
309
+ const text = getCodeText(pre);
310
+ if (!text) return;
311
+ try {
312
+ await navigator.clipboard.writeText(text);
313
+ } catch {
314
+ return;
315
+ }
316
+ btn.innerHTML = CHECK_ICON;
317
+ btn.dataset.state = "copied";
318
+ btn.setAttribute("aria-label", "Copied!");
319
+ if (resetTimer) window.clearTimeout(resetTimer);
320
+ resetTimer = window.setTimeout(() => {
321
+ btn.innerHTML = COPY_ICON;
322
+ delete btn.dataset.state;
323
+ btn.setAttribute("aria-label", "Copy code to clipboard");
324
+ }, 1500);
325
+ }
326
+ btn.addEventListener("click", handleClick);
327
+ if (getComputedStyle(pre).position === "static") pre.style.position = "relative";
328
+ pre.appendChild(btn);
329
+ return () => {
330
+ if (resetTimer) window.clearTimeout(resetTimer);
331
+ btn.removeEventListener("click", handleClick);
332
+ btn.remove();
333
+ };
334
+ }
335
+ /** Wire copy buttons onto all Shiki code blocks on the page. Call once. */
336
+ function codeCopy() {
337
+ mount("pre.astro-code", initCodeCopy);
338
+ }
339
+
340
+ //#endregion
341
+ //#region src/client/heading-anchors.ts
342
+ /** Add hoverable self-links to markdown headings with ids. */
343
+ function applyHeadingAnchors() {
344
+ document.querySelectorAll(".docs-content :is(h2, h3, h4)[id]").forEach((heading) => {
345
+ if (heading.hasAttribute("data-heading-anchor-ready")) return;
346
+ heading.setAttribute("data-heading-anchor-ready", "true");
347
+ const link = document.createElement("a");
348
+ link.href = `#${heading.id}`;
349
+ link.className = "heading-anchor";
350
+ link.setAttribute("aria-label", `Link to ${heading.textContent?.trim() ?? "section"}`);
351
+ link.textContent = "#";
352
+ heading.appendChild(link);
353
+ });
354
+ }
355
+ /**
356
+ * Add hoverable `#` self-links to all `h2`–`h4` headings in `.docs-content`.
357
+ * Re-runs on `astro:page-load` for View Transitions. Call once (e.g. from
358
+ * BaseLayout).
359
+ */
360
+ function headingAnchors() {
361
+ applyHeadingAnchors();
362
+ document.addEventListener("astro:page-load", applyHeadingAnchors);
363
+ }
364
+
365
+ //#endregion
366
+ export { FOCUSABLE, codeCopy, generateId, headingAnchors, initTabs, lockScroll, makeDisclosure, mount, unlockScroll };
367
+ //# sourceMappingURL=client.js.map