inai-react-components 1.6.2 → 2.0.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.
@@ -3,11 +3,13 @@ import path from "node:path";
3
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
6
+ import { spawn } from "node:child_process";
6
7
  import { runList } from "./list.js";
7
8
  import { runDiff } from "./diff.js";
8
9
  import { runAdd } from "./add.js";
9
10
  import { runUpdate } from "./update.js";
10
11
  import { runStatus } from "./status.js";
12
+ import { runThemeFromHex } from "./theme.js";
11
13
  import { THEMES } from "../utils/themes.js";
12
14
  import { resolveRegistryDir } from "../utils/registry-resolver.js";
13
15
  import { readRegistryJson } from "./status.js";
@@ -75,6 +77,39 @@ export async function handleViewComponent(args, cwd) {
75
77
  }
76
78
  return textResult(sections.join("\n"));
77
79
  }
80
+ /**
81
+ * Read the prop schema for a component from the registry's generated
82
+ * `props-data.json` (produced by `scripts/extract-props.ts`). Returns
83
+ * the schema as JSON text so AI assistants can validate props before
84
+ * generating JSX.
85
+ */
86
+ export async function handleViewComponentSchema(args, cwd) {
87
+ const name = args?.name;
88
+ if (!name)
89
+ return textResult("Error: 'name' argument is required.", true);
90
+ const registryDir = resolveRegistryDir(cwd);
91
+ const propsDataPath = path.join(registryDir, "apps", "docs", "src", "generated", "props-data.json");
92
+ if (!fs.existsSync(propsDataPath)) {
93
+ return textResult(`Error: props-data.json not found at ${propsDataPath}. Run \`pnpm run extract-props\` in the registry repo to generate it.`, true);
94
+ }
95
+ let propsData;
96
+ try {
97
+ propsData = JSON.parse(fs.readFileSync(propsDataPath, "utf-8"));
98
+ }
99
+ catch (e) {
100
+ const msg = e instanceof Error ? e.message : String(e);
101
+ return textResult(`Error: props-data.json is malformed: ${msg}`, true);
102
+ }
103
+ const componentSchema = propsData[name];
104
+ if (!componentSchema) {
105
+ const available = Object.keys(propsData).slice(0, 20).join(", ");
106
+ return textResult(`Error: No schema found for "${name}". First 20 available: ${available}, ...`, true);
107
+ }
108
+ return textResult(JSON.stringify({
109
+ name,
110
+ props: componentSchema.props,
111
+ }, null, 2));
112
+ }
78
113
  export async function handleAddComponent(args, cwd) {
79
114
  const name = args?.name;
80
115
  if (!name)
@@ -158,6 +193,351 @@ export async function handleStatus(args, cwd) {
158
193
  const output = await runStatus(cwd, { json: args?.json ?? true });
159
194
  return textResult(output);
160
195
  }
196
+ function findWorkspaceRoot(cwd) {
197
+ let dir = cwd;
198
+ for (let i = 0; i < 8; i++) {
199
+ if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml")))
200
+ return dir;
201
+ const parent = path.dirname(dir);
202
+ if (parent === dir)
203
+ break;
204
+ dir = parent;
205
+ }
206
+ return cwd;
207
+ }
208
+ export function parseMoonshotsFile(sourceText) {
209
+ const results = [];
210
+ // Each moonshot entry is a top-level object literal inside the `moonshots`
211
+ // array. We split on the `{ id: N,` header then read the key/value lines.
212
+ const blocks = sourceText.split(/\{\s*id:\s*/).slice(1);
213
+ for (const block of blocks) {
214
+ const idMatch = block.match(/^(\d+)\s*,/);
215
+ if (!idMatch)
216
+ continue;
217
+ const id = Number.parseInt(idMatch[1], 10);
218
+ const slug = block.match(/slug:\s*"([^"]+)"/)?.[1] ?? "";
219
+ const title = block.match(/title:\s*"([^"]+)"/)?.[1] ?? "";
220
+ const description = block.match(/description:\s*\n?\s*"([^"]+)"/)?.[1] ?? "";
221
+ const status = block.match(/status:\s*"([^"]+)"/)?.[1] ?? "";
222
+ const category = block.match(/category:\s*"([^"]+)"/)?.[1];
223
+ const demoPath = block.match(/demoPath:\s*"([^"]+)"/)?.[1];
224
+ const reducedMotionStrategy = block.match(/reducedMotionStrategy:\s*\n?\s*"([^"]+)"/)?.[1];
225
+ const componentsRaw = block.match(/components:\s*\[([^\]]*)\]/)?.[1] ?? "";
226
+ const components = componentsRaw
227
+ .split(",")
228
+ .map((s) => s.trim().replace(/^"|"$/g, ""))
229
+ .filter(Boolean);
230
+ results.push({
231
+ id,
232
+ slug,
233
+ title,
234
+ description,
235
+ status,
236
+ components,
237
+ ...(category ? { category } : {}),
238
+ ...(demoPath ? { demoPath } : {}),
239
+ ...(reducedMotionStrategy ? { reducedMotionStrategy } : {}),
240
+ });
241
+ }
242
+ return results;
243
+ }
244
+ export async function handleListMoonshots(args, cwd) {
245
+ const root = findWorkspaceRoot(cwd);
246
+ const moonshotsPath = path.join(root, "packages/ui/src/lib/moonshots.ts");
247
+ if (!fs.existsSync(moonshotsPath)) {
248
+ return textResult(`Error: moonshots.ts not found at ${moonshotsPath}. Run from inside the InAI UI workspace.`, true);
249
+ }
250
+ const src = fs.readFileSync(moonshotsPath, "utf-8");
251
+ let moonshots = parseMoonshotsFile(src);
252
+ if (args?.status) {
253
+ moonshots = moonshots.filter((m) => m.status === args.status);
254
+ }
255
+ if (args?.category) {
256
+ moonshots = moonshots.filter((m) => m.category === args.category);
257
+ }
258
+ return textResult(JSON.stringify(moonshots, null, 2));
259
+ }
260
+ /**
261
+ * Ranks blocks by keyword overlap between the user's brief and each
262
+ * block's name + description. Intentionally simple — the point is to
263
+ * surface 5–8 relevant candidates the agent can read in full via
264
+ * `view_component`, not to replace a proper embedding search.
265
+ */
266
+ function scoreBlock(query, text) {
267
+ const terms = query
268
+ .toLowerCase()
269
+ .split(/[^a-z0-9]+/)
270
+ .filter((t) => t.length >= 3);
271
+ if (terms.length === 0)
272
+ return 0;
273
+ const haystack = text.toLowerCase();
274
+ let score = 0;
275
+ for (const term of terms) {
276
+ if (haystack.includes(term))
277
+ score += 1;
278
+ // Bonus for exact word match.
279
+ const wordRegex = new RegExp(`\\b${term}\\b`);
280
+ if (wordRegex.test(haystack))
281
+ score += 1;
282
+ }
283
+ return score;
284
+ }
285
+ /**
286
+ * Pascalize a kebab/snake block slug so we can use it as an import
287
+ * identifier in the generated file (e.g. `signature-pricing-table-morph`
288
+ * → `SignaturePricingTableMorph`).
289
+ */
290
+ function pascalizeSlug(slug) {
291
+ return slug
292
+ .split(/[-_]/)
293
+ .filter(Boolean)
294
+ .map((part) => part[0].toUpperCase() + part.slice(1))
295
+ .join("");
296
+ }
297
+ /**
298
+ * Given a brief and a target path, compose a React `.tsx` page that
299
+ * imports the top-matching blocks from the local registry and renders
300
+ * them in order. Used by the MCP `scaffold_page` tool so an agent can
301
+ * drop a proposal page into the current project during a live client
302
+ * demo without hand-typing import boilerplate.
303
+ *
304
+ * Exported for unit tests; the CLI command wraps it.
305
+ */
306
+ export function scaffoldPageSource(brief, blocks) {
307
+ const lines = [
308
+ `// Generated by InAI UI MCP scaffold_page for brief:`,
309
+ `// "${brief.replace(/\n/g, " ")}"`,
310
+ `// Edit freely — the generator does not write this file again unless`,
311
+ `// you delete it.`,
312
+ ``,
313
+ ];
314
+ for (const block of blocks) {
315
+ const pascal = pascalizeSlug(block.name);
316
+ lines.push(`import ${pascal} from "./blocks/${block.name}/page";`);
317
+ }
318
+ lines.push("");
319
+ lines.push("export default function GeneratedPage() {");
320
+ lines.push(" return (");
321
+ lines.push(` <div className="flex flex-col">`);
322
+ for (const block of blocks) {
323
+ const pascal = pascalizeSlug(block.name);
324
+ lines.push(` <${pascal} />`);
325
+ }
326
+ lines.push(" </div>");
327
+ lines.push(" );");
328
+ lines.push("}");
329
+ lines.push("");
330
+ return lines.join("\n");
331
+ }
332
+ /**
333
+ * Wrap the CLI's `runThemeFromHex` so an agent can generate a layered
334
+ * brand override from a single hex value during a live demo. Writes
335
+ * the CSS layer under `packages/tokens/src/themes/<name>.css` (when
336
+ * invoked from the monorepo root) or a project-local themes dir.
337
+ */
338
+ export async function handleGenerateTheme(args, cwd) {
339
+ const name = args?.name?.trim();
340
+ const hex = args?.hex?.trim();
341
+ if (!name) {
342
+ return textResult("Error: 'name' argument is required.", true);
343
+ }
344
+ if (!hex) {
345
+ return textResult("Error: 'hex' argument is required.", true);
346
+ }
347
+ const root = findWorkspaceRoot(cwd);
348
+ const result = await runThemeFromHex({ name, hex }, root);
349
+ if (!result.success) {
350
+ return textResult(result.message, true);
351
+ }
352
+ return textResult([
353
+ `✓ ${result.message}`,
354
+ "",
355
+ "Layer it on top of a base theme:",
356
+ "```css",
357
+ `@import "@company/tokens/themes/ultimate.css";`,
358
+ `@import "@company/tokens/themes/${name}.css";`,
359
+ "```",
360
+ "",
361
+ `Then set \`data-theme="${name}"\` on <html> (or use the live theme switcher dock) to activate it.`,
362
+ ].join("\n"));
363
+ }
364
+ /**
365
+ * Seed a state file that the docs app's `LiveThemeSwitcher` picks up on
366
+ * boot, so an agent can say "use the brand theme we just generated" and
367
+ * the next `pnpm --filter docs dev` run opens already themed.
368
+ *
369
+ * Implementation: writes `{ theme: "<slug>" }` JSON to `.inai/state.json`
370
+ * in the current project (matches the convention used by other CLI
371
+ * commands). Non-destructive: merges with existing state if present.
372
+ */
373
+ export async function handleApplyTheme(args, cwd) {
374
+ const name = args?.name?.trim();
375
+ if (!name) {
376
+ return textResult("Error: 'name' argument is required.", true);
377
+ }
378
+ const stateDir = path.join(cwd, ".inai");
379
+ const stateFile = path.join(stateDir, "state.json");
380
+ fs.mkdirSync(stateDir, { recursive: true });
381
+ let existing = {};
382
+ if (fs.existsSync(stateFile)) {
383
+ try {
384
+ existing = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
385
+ }
386
+ catch {
387
+ // Corrupt state file — rewrite from scratch.
388
+ existing = {};
389
+ }
390
+ }
391
+ const merged = { ...existing, theme: name, updatedAt: new Date().toISOString() };
392
+ fs.writeFileSync(stateFile, JSON.stringify(merged, null, 2));
393
+ return textResult([
394
+ `✓ Applied theme "${name}".`,
395
+ "",
396
+ `State file: \`${path.relative(cwd, stateFile) || stateFile}\``,
397
+ "",
398
+ "Next steps:",
399
+ "1. Reload the docs app — the pitch mode shell reads this state on mount.",
400
+ "2. Or open the live theme switcher dock (bottom-right FAB) and click the preset — that also sets the active theme without a reload.",
401
+ ].join("\n"));
402
+ }
403
+ /**
404
+ * Spawn `scripts/capture-moonshots.mjs` for the agent. Streams stdout
405
+ * + stderr back through the MCP result. Intended for the case where
406
+ * the team wants fresh video loops without dropping to the terminal.
407
+ */
408
+ export async function handleCaptureMoonshots(args, cwd) {
409
+ const root = findWorkspaceRoot(cwd);
410
+ const scriptPath = path.join(root, "scripts/capture-moonshots.mjs");
411
+ if (!fs.existsSync(scriptPath)) {
412
+ return textResult(`Error: capture script not found at ${scriptPath}. Run from inside the InAI UI workspace.`, true);
413
+ }
414
+ const extra = [];
415
+ if (args?.slug)
416
+ extra.push(`--slug=${args.slug}`);
417
+ if (args?.force)
418
+ extra.push("--force");
419
+ return await new Promise((resolve) => {
420
+ const proc = spawn("node", [scriptPath, ...extra], {
421
+ cwd: root,
422
+ env: { ...process.env },
423
+ });
424
+ let out = "";
425
+ let err = "";
426
+ proc.stdout.on("data", (d) => {
427
+ out += d.toString();
428
+ });
429
+ proc.stderr.on("data", (d) => {
430
+ err += d.toString();
431
+ });
432
+ proc.on("close", (code) => {
433
+ const body = [
434
+ `\`node scripts/capture-moonshots.mjs ${extra.join(" ")}\` exited with code ${code ?? "?"}.`,
435
+ "",
436
+ "```",
437
+ out.trim(),
438
+ err.trim() ? `\n[stderr]\n${err.trim()}` : "",
439
+ "```",
440
+ ].join("\n");
441
+ resolve(textResult(body, (code ?? 0) !== 0));
442
+ });
443
+ proc.on("error", (e) => {
444
+ resolve(textResult(`Spawn failed: ${e.message}`, true));
445
+ });
446
+ });
447
+ }
448
+ export async function handleScaffoldPage(args, cwd) {
449
+ const brief = args?.brief?.trim();
450
+ if (!brief) {
451
+ return textResult("Error: 'brief' argument is required.", true);
452
+ }
453
+ const targetRel = args?.target_path?.trim();
454
+ if (!targetRel) {
455
+ return textResult("Error: 'target_path' argument is required (relative to the current project).", true);
456
+ }
457
+ const limit = Math.max(1, Math.min(args?.limit ?? 6, 12));
458
+ const registryDir = resolveRegistryDir(cwd);
459
+ const registryPath = path.join(registryDir, "registry.json");
460
+ const registry = readRegistryJson(registryPath);
461
+ if (!registry) {
462
+ return textResult(`Error: Registry not found at ${registryPath}.`, true);
463
+ }
464
+ const candidates = [...registry.blocks, ...registry.templates];
465
+ const scored = candidates
466
+ .map((item) => {
467
+ const combined = `${item.name} ${item.description}`;
468
+ return { item, score: scoreBlock(brief, combined) };
469
+ })
470
+ .filter((s) => s.score > 0)
471
+ .sort((a, b) => b.score - a.score)
472
+ .slice(0, limit);
473
+ if (scored.length === 0) {
474
+ return textResult(`No blocks or templates matched brief "${brief}". Nothing scaffolded.`, true);
475
+ }
476
+ const targetAbs = path.resolve(cwd, targetRel);
477
+ if (fs.existsSync(targetAbs)) {
478
+ return textResult(`Error: target file already exists at ${targetAbs}. Remove it or pick a new path.`, true);
479
+ }
480
+ const source = scaffoldPageSource(brief, scored.map((s) => ({
481
+ name: s.item.name,
482
+ type: s.item.type,
483
+ description: s.item.description,
484
+ })));
485
+ fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
486
+ fs.writeFileSync(targetAbs, source, "utf-8");
487
+ const lines = [
488
+ `# Scaffolded page at ${targetRel}`,
489
+ "",
490
+ `**Brief**: ${brief}`,
491
+ "",
492
+ `Composed ${scored.length} block${scored.length === 1 ? "" : "s"}:`,
493
+ "",
494
+ ];
495
+ for (const { item, score } of scored) {
496
+ lines.push(`- \`${item.name}\` (${item.type}, score ${score}) — ${item.description}`);
497
+ }
498
+ lines.push("");
499
+ lines.push("Next steps:");
500
+ lines.push("1. Make sure each referenced block is installed in your project (run `inai-ui add <slug>` for any that aren't yet).");
501
+ lines.push("2. Import the generated page from your router / entry point.");
502
+ lines.push("3. Theme it with the live theme switcher or `inai-ui theme from-hex` to lock in the client's brand.");
503
+ return textResult(lines.join("\n"));
504
+ }
505
+ export async function handleSuggestBlocks(args, cwd) {
506
+ const brief = args?.brief?.trim();
507
+ if (!brief) {
508
+ return textResult("Error: 'brief' argument is required.", true);
509
+ }
510
+ const limit = Math.max(1, Math.min(args?.limit ?? 6, 20));
511
+ const registryDir = resolveRegistryDir(cwd);
512
+ const registryPath = path.join(registryDir, "registry.json");
513
+ const registry = readRegistryJson(registryPath);
514
+ if (!registry) {
515
+ return textResult(`Error: Registry not found at ${registryPath}.`, true);
516
+ }
517
+ const candidates = [...registry.blocks, ...registry.templates];
518
+ const scored = candidates
519
+ .map((item) => {
520
+ const combined = `${item.name} ${item.description}`;
521
+ return { item, score: scoreBlock(brief, combined) };
522
+ })
523
+ .filter((s) => s.score > 0)
524
+ .sort((a, b) => b.score - a.score)
525
+ .slice(0, limit);
526
+ if (scored.length === 0) {
527
+ return textResult(`No blocks or templates matched brief "${brief}". Try broader keywords.`);
528
+ }
529
+ const lines = [
530
+ `# ${scored.length} suggestion${scored.length === 1 ? "" : "s"} for: ${brief}`,
531
+ "",
532
+ ];
533
+ for (const { item, score } of scored) {
534
+ lines.push(`## ${item.name} (score ${score})`);
535
+ lines.push(`- type: \`${item.type}\``);
536
+ lines.push(`- ${item.description}`);
537
+ lines.push("");
538
+ }
539
+ return textResult(lines.join("\n"));
540
+ }
161
541
  // ─── MCP server bootstrap ────────────────────────────────────────────
162
542
  export const MCP_TOOLS = [
163
543
  {
@@ -184,6 +564,17 @@ export const MCP_TOOLS = [
184
564
  required: ["name"],
185
565
  },
186
566
  },
567
+ {
568
+ name: "view_component_schema",
569
+ description: "Return the prop schema (name, type, required, description, default) for a component. Use before generating JSX so prop names, variants and types are correct without reading the source.",
570
+ inputSchema: {
571
+ type: "object",
572
+ properties: {
573
+ name: { type: "string", description: "Component name" },
574
+ },
575
+ required: ["name"],
576
+ },
577
+ },
187
578
  {
188
579
  name: "add_component",
189
580
  description: "Install a component (and its transitive registry dependencies) into the current project.",
@@ -236,6 +627,112 @@ export const MCP_TOOLS = [
236
627
  },
237
628
  },
238
629
  },
630
+ {
631
+ name: "list_moonshots",
632
+ description: "List the creative signature moonshots registered in InAI UI (kinetic typography, gradient mesh, magnetic cursor, decrypt text, etc.). Filter by status ('stable' | 'experimental') and/or category ('interaction' | 'typography' | 'layout' | 'dataviz' | 'color' | 'ambient'). Returns JSON metadata including reduced-motion strategy so the agent can choose moonshots responsibly.",
633
+ inputSchema: {
634
+ type: "object",
635
+ properties: {
636
+ status: {
637
+ type: "string",
638
+ description: "Filter by maturity: 'stable' (default returns all).",
639
+ },
640
+ category: {
641
+ type: "string",
642
+ description: "Filter by thematic category: interaction, typography, layout, dataviz, color, ambient.",
643
+ },
644
+ },
645
+ },
646
+ },
647
+ {
648
+ name: "suggest_blocks",
649
+ description: "Given a free-text brief (e.g. 'fintech dashboard with live KPIs and dark hero'), rank the blocks and templates in registry.json by keyword overlap and return the top matches. Use this to scaffold a client proposal page without reading the entire registry.",
650
+ inputSchema: {
651
+ type: "object",
652
+ properties: {
653
+ brief: {
654
+ type: "string",
655
+ description: "Short description of the desired page or section.",
656
+ },
657
+ limit: {
658
+ type: "number",
659
+ description: "Max number of suggestions. Defaults to 6.",
660
+ },
661
+ },
662
+ required: ["brief"],
663
+ },
664
+ },
665
+ {
666
+ name: "scaffold_page",
667
+ description: "Compose a fresh React .tsx page that imports and renders the top blocks matching a brief. Writes the file to `target_path` (relative to cwd) and returns a Markdown summary. Pair with `suggest_blocks` if the caller wants to preview the picks before writing anything.",
668
+ inputSchema: {
669
+ type: "object",
670
+ properties: {
671
+ brief: {
672
+ type: "string",
673
+ description: "Short description of the desired page (e.g. 'fintech landing with pricing and stats').",
674
+ },
675
+ target_path: {
676
+ type: "string",
677
+ description: "Relative path where the generated .tsx file will be written (e.g. 'src/pages/client-x-landing.tsx'). Must not exist yet.",
678
+ },
679
+ limit: {
680
+ type: "number",
681
+ description: "Max number of blocks to compose. Defaults to 6, capped at 12.",
682
+ },
683
+ },
684
+ required: ["brief", "target_path"],
685
+ },
686
+ },
687
+ {
688
+ name: "generate_theme",
689
+ description: "Generate a brand override theme from a single hex color. Uses the same hex → OKLCH pipeline as ThemeStudio and the live theme switcher, and writes a layered `.css` file under `packages/tokens/src/themes/` that should be imported after a base theme.",
690
+ inputSchema: {
691
+ type: "object",
692
+ properties: {
693
+ name: {
694
+ type: "string",
695
+ description: "Theme slug, used as the CSS filename (e.g. 'client-orbis').",
696
+ },
697
+ hex: {
698
+ type: "string",
699
+ description: "Brand hex color, e.g. '#6b46c1'.",
700
+ },
701
+ },
702
+ required: ["name", "hex"],
703
+ },
704
+ },
705
+ {
706
+ name: "apply_theme",
707
+ description: "Seed `.inai/state.json` with the active theme slug so the next docs app boot picks it up automatically. Non-destructive: merges with existing state. The live theme switcher also honours this state on mount.",
708
+ inputSchema: {
709
+ type: "object",
710
+ properties: {
711
+ name: {
712
+ type: "string",
713
+ description: "Theme slug to activate (must already exist).",
714
+ },
715
+ },
716
+ required: ["name"],
717
+ },
718
+ },
719
+ {
720
+ name: "capture_moonshots",
721
+ description: "Run `scripts/capture-moonshots.mjs` to regenerate the WebM loops consumed by the gallery. Accepts an optional `slug` filter and a `force` flag to re-record already-captured moonshots. Returns the script's stdout/stderr.",
722
+ inputSchema: {
723
+ type: "object",
724
+ properties: {
725
+ slug: {
726
+ type: "string",
727
+ description: "Optional moonshot slug to record in isolation (e.g. 'gradient-mesh-live').",
728
+ },
729
+ force: {
730
+ type: "boolean",
731
+ description: "Re-capture slugs that already have a video. Defaults to false.",
732
+ },
733
+ },
734
+ },
735
+ },
239
736
  ];
240
737
  export async function runMcp(cwd = process.cwd()) {
241
738
  const server = new Server({ name: "inai-ui-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
@@ -251,6 +748,8 @@ export async function runMcp(cwd = process.cwd()) {
251
748
  return await handleListComponents(args, cwd);
252
749
  case "view_component":
253
750
  return await handleViewComponent(args, cwd);
751
+ case "view_component_schema":
752
+ return await handleViewComponentSchema(args, cwd);
254
753
  case "add_component":
255
754
  return await handleAddComponent(args, cwd);
256
755
  case "diff_component":
@@ -261,6 +760,18 @@ export async function runMcp(cwd = process.cwd()) {
261
760
  return await handleListThemes();
262
761
  case "project_status":
263
762
  return await handleStatus(args, cwd);
763
+ case "list_moonshots":
764
+ return await handleListMoonshots(args, cwd);
765
+ case "suggest_blocks":
766
+ return await handleSuggestBlocks(args, cwd);
767
+ case "scaffold_page":
768
+ return await handleScaffoldPage(args, cwd);
769
+ case "generate_theme":
770
+ return await handleGenerateTheme(args, cwd);
771
+ case "apply_theme":
772
+ return await handleApplyTheme(args, cwd);
773
+ case "capture_moonshots":
774
+ return await handleCaptureMoonshots(args, cwd);
264
775
  default:
265
776
  return {
266
777
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
@@ -53,6 +53,13 @@ export interface RegistryJson {
53
53
  components: RegistryComponent[];
54
54
  blocks: RegistryComponent[];
55
55
  templates: RegistryComponent[];
56
+ /**
57
+ * Installable theme presets — each one is a plain `.css` file that the
58
+ * CLI can drop into a consumer project's styles tree and wire into the
59
+ * user's Tailwind v4 `@theme` layer. Optional for backward-compat with
60
+ * pre-2.0 registries; v2.0+ registries always populate this.
61
+ */
62
+ themes?: RegistryTheme[];
56
63
  }
57
64
  export interface RegistryComponent {
58
65
  name: string;
@@ -64,12 +71,35 @@ export interface RegistryComponent {
64
71
  tokenUsage: string[];
65
72
  tanstackCompatibility: Record<string, boolean>;
66
73
  fieldWrappers?: string[];
74
+ /**
75
+ * Thematic category used by `inai-ui list --category <name>`. Optional
76
+ * because older registry snapshots may not carry it. Common values:
77
+ * layout, data-display, feedback, overlay, navigation, forms, data-entry,
78
+ * misc, typography, dataviz.
79
+ */
80
+ category?: string;
81
+ /**
82
+ * Tags used for fuzzy discovery by `inai-ui list`/`add` search. Optional
83
+ * because older registry snapshots may not carry them.
84
+ */
85
+ tags?: string[];
67
86
  /**
68
87
  * Names of other registry components this component depends on. Used by
69
88
  * `inai-ui add` to install transitive deps automatically. Defaults to [].
70
89
  */
71
90
  registryDependencies?: string[];
72
91
  }
92
+ export interface RegistryTheme {
93
+ /** Slug used with `inai-ui add theme <name>`. Matches the CSS selector. */
94
+ name: string;
95
+ type: "theme";
96
+ description: string;
97
+ /** "brand" | "shadcn" | "creative" — drives galleries and grouping. */
98
+ category: "brand" | "shadcn" | "creative";
99
+ /** Single `.css` file in `packages/tokens/src/themes/`. */
100
+ files: string[];
101
+ tags: string[];
102
+ }
73
103
  export declare function readComponentsJson(rootDir: string): ComponentsJsonFile | null;
74
104
  export declare function readRegistryJson(registryPath: string): RegistryJson | null;
75
105
  export type ComponentHealth = "HEALTHY" | "DRIFT" | "OUTDATED" | "MISSING";
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,12 @@ export interface ThemeCreateOptions {
2
2
  name: string;
3
3
  base: string;
4
4
  }
5
+ export interface ThemeFromHexOptions {
6
+ /** Theme slug used as filename. */
7
+ name: string;
8
+ /** Brand hex string, e.g. "#6b46c1". */
9
+ hex: string;
10
+ }
5
11
  export declare function resolveThemesDir(rootDir: string): string;
6
12
  export declare function readBaseTheme(themesDir: string, baseName: string): string | null;
7
13
  export declare function generateThemeCss(baseCss: string, newName: string, baseName: string): string;
@@ -10,5 +16,31 @@ export declare function runThemeCreate(options: ThemeCreateOptions, rootDir: str
10
16
  filePath: string;
11
17
  message: string;
12
18
  }>;
19
+ /**
20
+ * Convert an sRGB hex string to approximate OKLCH. Uses the same
21
+ * pipeline as `packages/ui/src/lib/color-utils.ts` so themes generated
22
+ * by the CLI match what ThemeStudio and the live theme switcher pick.
23
+ *
24
+ * Kept self-contained so the CLI has no runtime dep on the UI package.
25
+ */
26
+ export interface OklchTriple {
27
+ l: number;
28
+ c: number;
29
+ h: number;
30
+ }
31
+ export declare function hexToOklch(hex: string): OklchTriple;
32
+ /**
33
+ * Produce a minimal CSS theme layer that overrides the brand-centric
34
+ * token slots. It is meant to be stacked on top of an existing theme
35
+ * (e.g. by `@import`-ing this file after `ultimate.css`) so the rest of
36
+ * the scheme stays coherent and accessible.
37
+ */
38
+ export declare function generateThemeFromHexCss(name: string, hex: string, oklch: OklchTriple): string;
39
+ export declare function runThemeFromHex(options: ThemeFromHexOptions, rootDir: string): Promise<{
40
+ success: boolean;
41
+ filePath: string;
42
+ message: string;
43
+ }>;
44
+ export declare function themeFromHexCommand(options: ThemeFromHexOptions): Promise<void>;
13
45
  export declare function themeCreateCommand(options: ThemeCreateOptions): Promise<void>;
14
46
  //# sourceMappingURL=theme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/commands/theme.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMhF;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgB3F;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDlE;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBnF"}
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/commands/theme.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMhF;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgB3F;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDlE;AAID;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAOD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAwBnD;AASD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,WAAW,GACjB,MAAM,CAkBR;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BlE;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBrF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBnF"}