rivet-design 0.9.5 → 0.9.6

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 (42) hide show
  1. package/dist/mcp/agent-variants/SessionStore.d.ts +10 -2
  2. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  3. package/dist/mcp/agent-variants/SessionStore.js +82 -22
  4. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  5. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +4 -0
  6. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  7. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +430 -35
  8. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  9. package/dist/mcp/agent-variants/contracts.d.ts +389 -129
  10. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  11. package/dist/mcp/agent-variants/contracts.js +90 -36
  12. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  13. package/dist/mcp/agent-variants/createZeroToOneTool.d.ts +32 -13
  14. package/dist/mcp/agent-variants/createZeroToOneTool.d.ts.map +1 -1
  15. package/dist/mcp/agent-variants/createZeroToOneTool.js +1 -1
  16. package/dist/mcp/agent-variants/createZeroToOneTool.js.map +1 -1
  17. package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
  18. package/dist/mcp/agent-variants/tools.js +9 -2
  19. package/dist/mcp/agent-variants/tools.js.map +1 -1
  20. package/dist/routes/agentVariants.d.ts.map +1 -1
  21. package/dist/routes/agentVariants.js +120 -37
  22. package/dist/routes/agentVariants.js.map +1 -1
  23. package/dist/services/VariantHistoryService.d.ts +64 -1
  24. package/dist/services/VariantHistoryService.d.ts.map +1 -1
  25. package/dist/services/VariantHistoryService.js +148 -18
  26. package/dist/services/VariantHistoryService.js.map +1 -1
  27. package/dist/utils/skills/claude-skill.d.ts +1 -1
  28. package/dist/utils/skills/claude-skill.js +1 -1
  29. package/dist/utils/skills/cursor-rules.d.ts +1 -1
  30. package/dist/utils/skills/cursor-rules.js +1 -1
  31. package/dist/utils/skills/describe-motion-protocol.d.ts +11 -0
  32. package/dist/utils/skills/describe-motion-protocol.d.ts.map +1 -0
  33. package/dist/utils/skills/describe-motion-protocol.js +216 -0
  34. package/dist/utils/skills/describe-motion-protocol.js.map +1 -0
  35. package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
  36. package/dist/utils/skills/shared-variants-protocol.js +23 -17
  37. package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/ui/dist/assets/main-BX1XfsOq.css +1 -0
  40. package/src/ui/dist/assets/{main-SuZlKEi0.js → main-CO7W1r28.js} +39 -39
  41. package/src/ui/dist/index.html +2 -2
  42. package/src/ui/dist/assets/main-OdmwI8Od.css +0 -1
@@ -22,6 +22,8 @@ const previewQa_1 = require("./previewQa");
22
22
  const VariantHistoryService_1 = require("../../services/VariantHistoryService");
23
23
  const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
24
24
  const FRESH_DEV_SERVER_HOST = '127.0.0.1';
25
+ const DESIGN_CONTEXT_ROUTE_SEGMENT = 'design-md';
26
+ const DESIGN_CONTEXT_VIEW_SEGMENT = 'view';
25
27
  /**
26
28
  * Allowlist of asset file extensions an agent-planned source may have.
27
29
  * `assetPlan` is sized for large local *assets* (3D models, images,
@@ -39,15 +41,36 @@ const FRESH_DEV_SERVER_HOST = '127.0.0.1';
39
41
  */
40
42
  const ALLOWED_ASSET_EXTENSIONS = new Set([
41
43
  // 3D / models
42
- '.glb', '.gltf', '.obj', '.fbx', '.usdz',
44
+ '.glb',
45
+ '.gltf',
46
+ '.obj',
47
+ '.fbx',
48
+ '.usdz',
43
49
  // images
44
- '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.avif', '.bmp', '.ico',
50
+ '.png',
51
+ '.jpg',
52
+ '.jpeg',
53
+ '.gif',
54
+ '.webp',
55
+ '.svg',
56
+ '.avif',
57
+ '.bmp',
58
+ '.ico',
45
59
  // video
46
- '.mp4', '.webm', '.mov',
60
+ '.mp4',
61
+ '.webm',
62
+ '.mov',
47
63
  // audio
48
- '.mp3', '.wav', '.ogg', '.m4a',
64
+ '.mp3',
65
+ '.wav',
66
+ '.ogg',
67
+ '.m4a',
49
68
  // fonts
50
- '.woff', '.woff2', '.ttf', '.otf', '.eot',
69
+ '.woff',
70
+ '.woff2',
71
+ '.ttf',
72
+ '.otf',
73
+ '.eot',
51
74
  // PDFs
52
75
  '.pdf',
53
76
  ]);
@@ -180,8 +203,7 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
180
203
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source resolved extension '${resolvedExt || '(none)'}' is not on the allowlist (resolved from '${entry.source}').`);
181
204
  }
182
205
  const normalizedDest = path_1.default.normalize(entry.destination);
183
- if (normalizedDest.startsWith('..') ||
184
- path_1.default.isAbsolute(normalizedDest)) {
206
+ if (normalizedDest.startsWith('..') || path_1.default.isAbsolute(normalizedDest)) {
185
207
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.destination must stay inside the worktree, got '${entry.destination}'`);
186
208
  }
187
209
  const absDest = path_1.default.join(worktreePath, normalizedDest);
@@ -306,8 +328,10 @@ class AgentVariantsOrchestrator {
306
328
  const variants = this.getVariants(sessionId);
307
329
  const sessionProjectContext = this.store.getProjectContext(sessionId);
308
330
  const projectContext = toActiveProjectContext(sessionProjectContext);
309
- const destinationPath = projectContext.kind === 'fresh' ? projectContext.workspacePath : undefined;
310
- const artifacts = buildSessionArtifacts(sessionProjectContext);
331
+ const destinationPath = projectContext.kind === 'fresh'
332
+ ? projectContext.workspacePath
333
+ : undefined;
334
+ const artifacts = buildSessionArtifacts(sessionId, sessionProjectContext);
311
335
  return {
312
336
  active: true,
313
337
  sessionId,
@@ -429,7 +453,9 @@ class AgentVariantsOrchestrator {
429
453
  };
430
454
  }
431
455
  }
432
- catch { /* work item may not exist in edge cases */ }
456
+ catch {
457
+ /* work item may not exist in edge cases */
458
+ }
433
459
  }
434
460
  if (!preview && port) {
435
461
  preview = { kind: 'dev_server', port };
@@ -440,7 +466,7 @@ class AgentVariantsOrchestrator {
440
466
  const canView = Boolean(preview) || (isSucceeded && Boolean(port));
441
467
  const canCommit = isSucceeded && !qaFailed;
442
468
  const commitDisabledReason = qaFailed
443
- ? qa?.summary ?? 'Variant failed QA'
469
+ ? (qa?.summary ?? 'Variant failed QA')
444
470
  : 'Wait for a successful variant';
445
471
  return {
446
472
  ...variant,
@@ -486,7 +512,9 @@ class AgentVariantsOrchestrator {
486
512
  }
487
513
  getStaticPreviewHtml(sessionId, workItemId) {
488
514
  // Primary: from the staticPreviews Map populated by handleSucceededReport.
489
- const fromMap = this.resources.get(sessionId)?.staticPreviews.get(workItemId)?.html;
515
+ const fromMap = this.resources
516
+ .get(sessionId)
517
+ ?.staticPreviews.get(workItemId)?.html;
490
518
  if (fromMap)
491
519
  return fromMap;
492
520
  // Fallback: read directly from the work item's stored output — available
@@ -501,6 +529,16 @@ class AgentVariantsOrchestrator {
501
529
  return undefined;
502
530
  }
503
531
  }
532
+ /** Resolve the DESIGN.md markdown behind an artifact link. */
533
+ getDesignContextMarkdown(sessionId, artifactId) {
534
+ const artifact = findDesignContextArtifact(this.store.getProjectContext(sessionId), artifactId);
535
+ return artifact?.content;
536
+ }
537
+ /** Build the raw-plus-rendered DesignMD document for an artifact link. */
538
+ getDesignContextViewerHtml(sessionId, artifactId) {
539
+ const artifact = findDesignContextArtifact(this.store.getProjectContext(sessionId), artifactId);
540
+ return artifact ? buildDesignContextViewerDocument(artifact) : undefined;
541
+ }
504
542
  getStaticPreviewByBriefId(sessionId, briefId) {
505
543
  const resources = this.resources.get(sessionId);
506
544
  if (!resources)
@@ -559,8 +597,12 @@ class AgentVariantsOrchestrator {
559
597
  */
560
598
  async startUnified(args) {
561
599
  const count = args.briefs?.length ?? args.count ?? 4;
562
- const projectContext = args.projectContext ?? { kind: 'existing' };
563
- const sourceContext = projectContext.kind === 'fresh' ? projectContext.sourceContext : undefined;
600
+ const projectContext = args.projectContext ?? {
601
+ kind: 'existing',
602
+ };
603
+ const sourceContext = projectContext.kind === 'fresh'
604
+ ? projectContext.sourceContext
605
+ : undefined;
564
606
  const isSourceGrounded = Boolean(sourceContext?.sourceUrls?.length) ||
565
607
  Boolean(sourceContext?.sourceArtifacts?.length) ||
566
608
  Boolean(sourceContext?.sourceIntent) ||
@@ -835,8 +877,8 @@ class AgentVariantsOrchestrator {
835
877
  }
836
878
  else {
837
879
  try {
838
- historyProjectPath =
839
- (await this.resolveEnv(args.sessionId))?.projectPath;
880
+ historyProjectPath = (await this.resolveEnv(args.sessionId))
881
+ ?.projectPath;
840
882
  }
841
883
  catch {
842
884
  historyProjectPath = undefined;
@@ -869,13 +911,15 @@ class AgentVariantsOrchestrator {
869
911
  }
870
912
  const variantSnapshot = this.getVariants(args.sessionId).find((variant) => variant.workItemId === args.variantId);
871
913
  if (!variantSnapshot || variantSnapshot.actions?.commit?.enabled !== true) {
872
- throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', variantSnapshot?.actions?.commit?.reason ?? 'Variant is not committable');
914
+ throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', variantSnapshot?.actions?.commit?.reason ??
915
+ 'Variant is not committable');
873
916
  }
874
917
  const input = this.store.getWorkItemInput(args.sessionId, args.variantId);
875
918
  const projectContext = this.store.getProjectContext(args.sessionId);
876
919
  let payload;
877
920
  let envelopeDestination;
878
921
  let changedFilesCount;
922
+ let runnablePaths;
879
923
  if (projectContext.kind === 'fresh') {
880
924
  const destinationPath = projectContext.workspacePath;
881
925
  this.assertDestinationAvailable(destinationPath);
@@ -956,7 +1000,7 @@ class AgentVariantsOrchestrator {
956
1000
  // chosen project. Best-effort: failures here log and continue so
957
1001
  // a partial history never blocks the commit handoff.
958
1002
  try {
959
- this.preserveUnchosenVariants({
1003
+ runnablePaths = this.preserveUnchosenVariants({
960
1004
  sessionId: args.sessionId,
961
1005
  chosenVariantId: args.variantId,
962
1006
  destinationPath,
@@ -1052,7 +1096,7 @@ class AgentVariantsOrchestrator {
1052
1096
  diff: record.diff,
1053
1097
  target: input.target,
1054
1098
  changedFilesCount,
1055
- note: 'Variant diff applied to the user\'s working tree (uncommitted).',
1099
+ note: "Variant diff applied to the user's working tree (uncommitted).",
1056
1100
  };
1057
1101
  envelopeDestination = env.projectPath;
1058
1102
  }
@@ -1087,12 +1131,21 @@ class AgentVariantsOrchestrator {
1087
1131
  projectPath: historyProjectPath,
1088
1132
  projectKind: projectContext.kind,
1089
1133
  destinationPath: envelopeDestination,
1134
+ runnablePaths,
1090
1135
  }).catch((err) => {
1091
1136
  log.warn(`persistVariantHistoryAtCommit failed for session ${args.sessionId}`, err);
1092
1137
  });
1093
1138
  const enqueueResult = this.adapter.enqueue(envelope);
1094
1139
  resources.committedVariantIds.add(args.variantId);
1095
- if (this.activeSessionId === args.sessionId) {
1140
+ // For fresh `new-project` commits the committed variant IS the user's
1141
+ // final result — there's no dev server to fall back to. Keep the session
1142
+ // active so SSE keeps pushing snapshots with the chosen variant; the UI's
1143
+ // iframe stays on it instead of unmounting to about:blank (which would
1144
+ // surface a misleading "Preview isn't connected" overlay). For diff /
1145
+ // diff-applied commits the user moves on to their own dev server, so
1146
+ // clearing is correct — that's the original behavior preserved below.
1147
+ if (this.activeSessionId === args.sessionId &&
1148
+ payload.kind !== 'project-created') {
1096
1149
  this.activeSessionId = null;
1097
1150
  }
1098
1151
  const dwellMsFromTerminal = resources.terminalAt
@@ -1558,19 +1611,28 @@ class AgentVariantsOrchestrator {
1558
1611
  const projectContext = this.store.getProjectContext(args.sessionId);
1559
1612
  if (projectContext.kind !== 'existing')
1560
1613
  return;
1561
- let projectPath;
1614
+ let env;
1562
1615
  try {
1563
- const env = await this.resolveEnv(args.sessionId);
1564
- projectPath = env.projectPath;
1616
+ env = await this.resolveEnv(args.sessionId);
1565
1617
  }
1566
1618
  catch (err) {
1567
1619
  log.warn(`persistCompletedExistingVariant: resolveEnv failed for ${args.sessionId}`, err);
1568
1620
  return;
1569
1621
  }
1622
+ const record = this.resources.get(args.sessionId)?.worktrees.get(args.workItemId);
1623
+ let sourceDir;
1624
+ if (env.framework === 'static' && record) {
1625
+ try {
1626
+ sourceDir = await this.worktrees.getProjectCwdInWorktree(record.worktreePath);
1627
+ }
1628
+ catch (err) {
1629
+ log.warn(`persistCompletedExistingVariant: static snapshot path failed for ${args.sessionId}/${args.workItemId}`, err);
1630
+ }
1631
+ }
1570
1632
  const input = this.store.getWorkItemInput(args.sessionId, args.workItemId);
1571
1633
  const sessionPrompt = this.store.getPrompt(args.sessionId);
1572
1634
  await this.variantHistory.persistVariant({
1573
- projectPath,
1635
+ projectPath: env.projectPath,
1574
1636
  sessionId: args.sessionId,
1575
1637
  variantId: args.workItemId,
1576
1638
  label: input.briefLabel,
@@ -1578,6 +1640,10 @@ class AgentVariantsOrchestrator {
1578
1640
  sessionPrompt,
1579
1641
  kind: 'diff',
1580
1642
  diff: args.diff,
1643
+ sourceDir,
1644
+ preview: sourceDir
1645
+ ? { kind: 'static', entryPath: 'index.html' }
1646
+ : { kind: 'none', reason: 'diff_only' },
1581
1647
  changedFilesCount: countDiffFiles(args.diff),
1582
1648
  projectKind: 'existing',
1583
1649
  });
@@ -1634,9 +1700,17 @@ class AgentVariantsOrchestrator {
1634
1700
  // is implementation-detail staging that we copy alongside it. Hardcode
1635
1701
  // the count so the history row matches what `commit_variant` reports
1636
1702
  // (always 1) instead of double-counting the brief.
1637
- const changedFilesCount = staticPreview
1638
- ? 1
1639
- : countWorktreeFiles(sourceDir);
1703
+ const changedFilesCount = staticPreview ? 1 : countWorktreeFiles(sourceDir);
1704
+ const preview = staticPreview
1705
+ ? { kind: 'static', entryPath: 'index.html' }
1706
+ : {
1707
+ kind: 'dev-server',
1708
+ command: {
1709
+ cwd: '.',
1710
+ packageManager: 'npm',
1711
+ script: 'dev',
1712
+ },
1713
+ };
1640
1714
  try {
1641
1715
  await this.variantHistory.persistVariant({
1642
1716
  projectPath: historyProjectPath,
@@ -1647,6 +1721,7 @@ class AgentVariantsOrchestrator {
1647
1721
  sessionPrompt,
1648
1722
  kind: 'project-created',
1649
1723
  sourceDir,
1724
+ preview,
1650
1725
  changedFilesCount,
1651
1726
  projectKind: 'fresh',
1652
1727
  designMarkdown: designArtifact?.markdown,
@@ -1688,6 +1763,7 @@ class AgentVariantsOrchestrator {
1688
1763
  variantId: workItemId,
1689
1764
  status,
1690
1765
  destinationPath: isChosen ? args.destinationPath : undefined,
1766
+ runnablePath: args.runnablePaths?.get(workItemId),
1691
1767
  });
1692
1768
  }
1693
1769
  }
@@ -1807,8 +1883,9 @@ class AgentVariantsOrchestrator {
1807
1883
  */
1808
1884
  preserveUnchosenVariants(args) {
1809
1885
  const resources = this.resources.get(args.sessionId);
1886
+ const runnablePaths = new Map();
1810
1887
  if (!resources)
1811
- return;
1888
+ return runnablePaths;
1812
1889
  const destinationParent = path_1.default.dirname(args.destinationPath);
1813
1890
  const projectSlug = path_1.default.basename(args.destinationPath);
1814
1891
  const historyDir = (0, createProjectArtifacts_1.createVariantsHistoryPath)(destinationParent, projectSlug);
@@ -1827,6 +1904,7 @@ class AgentVariantsOrchestrator {
1827
1904
  const folderName = `${numericPrefix}-${slug}`;
1828
1905
  if (variant.workItemId === args.chosenVariantId) {
1829
1906
  chosenSlug = slug;
1907
+ runnablePaths.set(variant.workItemId, projectSlug);
1830
1908
  manifestEntries.push({
1831
1909
  variantId: variant.workItemId,
1832
1910
  label,
@@ -1892,6 +1970,7 @@ class AgentVariantsOrchestrator {
1892
1970
  // Update the in-memory record so teardown doesn't try to operate on
1893
1971
  // the stale path.
1894
1972
  record.worktreePath = newPath;
1973
+ runnablePaths.set(variant.workItemId, `${path_1.default.basename(historyDir)}/${folderName}`);
1895
1974
  manifestEntries.push({
1896
1975
  variantId: variant.workItemId,
1897
1976
  label,
@@ -1926,6 +2005,7 @@ class AgentVariantsOrchestrator {
1926
2005
  log.warn(`Writing variants history manifest failed for ${historyDir}`, err);
1927
2006
  }
1928
2007
  resources.vitePreservedSiblings = true;
2008
+ return runnablePaths;
1929
2009
  }
1930
2010
  /**
1931
2011
  * Rename `sourceWorktreePath` into `destinationPath`, then replace the
@@ -2323,15 +2403,26 @@ const toActiveProjectContext = (projectContext) => {
2323
2403
  * Resolve the user-facing supporting artifacts for a session.
2324
2404
  *
2325
2405
  * For 0→1 (`fresh`) sessions with a populated `designContext`, each slot is
2326
- * turned into a `design_context` artifact carrying the full DESIGN.md
2327
- * markdown:
2406
+ * turned into a `design_context` artifact that links to Rivet-hosted
2407
+ * DESIGN.md views:
2328
2408
  * - `slug` entries resolve bundled catalog markdown via the design catalog.
2329
2409
  * - `markdown` entries (Agent Browser / inspiration extractor output) carry
2330
- * their stored markdown verbatim.
2410
+ * their stored markdown when the linked route is requested.
2331
2411
  * Slots whose markdown can't be resolved are skipped so the UI never renders
2332
2412
  * a metadata-only DESIGN.md row.
2333
2413
  */
2334
- const buildSessionArtifacts = (projectContext) => {
2414
+ const buildSessionArtifacts = (sessionId, projectContext) => {
2415
+ const artifacts = buildResolvedDesignContextArtifacts(projectContext);
2416
+ return artifacts.map(({ content: _content, ...artifact }) => ({
2417
+ ...artifact,
2418
+ rawUrl: buildDesignContextRawUrl(sessionId, artifact.id),
2419
+ viewUrl: buildDesignContextViewUrl(sessionId, artifact.id),
2420
+ }));
2421
+ };
2422
+ const findDesignContextArtifact = (projectContext, artifactId) => {
2423
+ return buildResolvedDesignContextArtifacts(projectContext).find((artifact) => artifact.id === artifactId);
2424
+ };
2425
+ const buildResolvedDesignContextArtifacts = (projectContext) => {
2335
2426
  if (projectContext.kind !== 'fresh')
2336
2427
  return [];
2337
2428
  const designContext = projectContext.designContext;
@@ -2350,7 +2441,9 @@ const buildSessionArtifacts = (projectContext) => {
2350
2441
  id: `design_context:${slot}:${entry.slug}`,
2351
2442
  kind: 'design_context',
2352
2443
  label: catalogEntry?.name ?? entry.slug,
2353
- ...(catalogEntry?.description ? { summary: catalogEntry.description } : {}),
2444
+ ...(catalogEntry?.description
2445
+ ? { summary: catalogEntry.description }
2446
+ : {}),
2354
2447
  status: 'ready',
2355
2448
  source: 'static',
2356
2449
  contentType: 'text/markdown',
@@ -2382,8 +2475,6 @@ const buildSessionArtifacts = (projectContext) => {
2382
2475
  });
2383
2476
  };
2384
2477
  const addDesignContextArtifact = (artifactsByContent, artifact) => {
2385
- if (!artifact.content)
2386
- return;
2387
2478
  const existing = artifactsByContent.get(artifact.content);
2388
2479
  if (existing) {
2389
2480
  existing.usedByVariantCount += 1;
@@ -2394,6 +2485,310 @@ const addDesignContextArtifact = (artifactsByContent, artifact) => {
2394
2485
  usedByVariantCount: 1,
2395
2486
  });
2396
2487
  };
2488
+ const buildDesignContextRawUrl = (sessionId, artifactId) => {
2489
+ return `/api/variants/${encodeURIComponent(sessionId)}/${DESIGN_CONTEXT_ROUTE_SEGMENT}/${encodeURIComponent(artifactId)}`;
2490
+ };
2491
+ const buildDesignContextViewUrl = (sessionId, artifactId) => {
2492
+ return `${buildDesignContextRawUrl(sessionId, artifactId)}/${DESIGN_CONTEXT_VIEW_SEGMENT}`;
2493
+ };
2494
+ const buildDesignContextViewerDocument = (artifact) => {
2495
+ const title = `${artifact.label} DESIGN.md`;
2496
+ const visualHtml = renderDesignMarkdown(artifact.content);
2497
+ const rawMarkdown = escapeHtml(artifact.content);
2498
+ return `<!doctype html>
2499
+ <html lang="en">
2500
+ <head>
2501
+ <meta charset="utf-8" />
2502
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2503
+ <title>${escapeHtml(title)}</title>
2504
+ <style>
2505
+ :root {
2506
+ color-scheme: light;
2507
+ --ink: #201b16;
2508
+ --muted: #75695e;
2509
+ --paper: #fbf7ef;
2510
+ --panel: #fffdf8;
2511
+ --rule: #eadfcf;
2512
+ --accent: #e45d2f;
2513
+ --accent-soft: #ffe0d3;
2514
+ --code: #2a2521;
2515
+ }
2516
+ * { box-sizing: border-box; }
2517
+ body {
2518
+ margin: 0;
2519
+ min-height: 100vh;
2520
+ background:
2521
+ radial-gradient(circle at top left, rgba(228, 93, 47, 0.18), transparent 36rem),
2522
+ linear-gradient(135deg, #fbf7ef 0%, #f2eadc 100%);
2523
+ color: var(--ink);
2524
+ font-family: ui-serif, Georgia, Cambria, "Times New Roman", serif;
2525
+ }
2526
+ main {
2527
+ width: min(1440px, calc(100vw - 40px));
2528
+ margin: 0 auto;
2529
+ padding: 40px 0;
2530
+ }
2531
+ header {
2532
+ display: flex;
2533
+ align-items: center;
2534
+ justify-content: space-between;
2535
+ gap: 24px;
2536
+ margin-bottom: 24px;
2537
+ border-bottom: 1px solid var(--rule);
2538
+ padding-bottom: 18px;
2539
+ }
2540
+ h1 {
2541
+ margin: 0;
2542
+ max-width: 780px;
2543
+ font-size: clamp(2rem, 5vw, 4.5rem);
2544
+ letter-spacing: -0.06em;
2545
+ line-height: 0.92;
2546
+ }
2547
+ .mode-input {
2548
+ position: absolute;
2549
+ opacity: 0;
2550
+ pointer-events: none;
2551
+ }
2552
+ .mode-toggle {
2553
+ display: inline-flex;
2554
+ flex-shrink: 0;
2555
+ gap: 4px;
2556
+ border: 1px solid var(--rule);
2557
+ border-radius: 999px;
2558
+ background: rgba(255, 253, 248, 0.72);
2559
+ padding: 4px;
2560
+ box-shadow: 0 12px 30px rgba(61, 44, 26, 0.08);
2561
+ }
2562
+ .mode-toggle label {
2563
+ position: relative;
2564
+ cursor: pointer;
2565
+ border-radius: 999px;
2566
+ padding: 8px 14px;
2567
+ color: var(--muted);
2568
+ font: 600 0.74rem/1.2 ui-sans-serif, system-ui, sans-serif;
2569
+ letter-spacing: 0.12em;
2570
+ text-transform: uppercase;
2571
+ transition:
2572
+ background 160ms ease,
2573
+ color 160ms ease;
2574
+ }
2575
+ .mode-toggle label:has(input:checked) {
2576
+ background: var(--ink);
2577
+ color: var(--paper);
2578
+ }
2579
+ .viewer {
2580
+ display: block;
2581
+ }
2582
+ .viewer-panel {
2583
+ display: none;
2584
+ }
2585
+ main:has(#designmd-visual-mode:checked) .visual-panel,
2586
+ main:has(#designmd-raw-mode:checked) .raw-panel {
2587
+ display: block;
2588
+ }
2589
+ .panel {
2590
+ overflow: hidden;
2591
+ border: 1px solid var(--rule);
2592
+ border-radius: 18px;
2593
+ background: color-mix(in srgb, var(--panel) 92%, white);
2594
+ box-shadow: 0 20px 60px rgba(61, 44, 26, 0.12);
2595
+ }
2596
+ .panel-title {
2597
+ display: flex;
2598
+ align-items: center;
2599
+ justify-content: space-between;
2600
+ border-bottom: 1px solid var(--rule);
2601
+ padding: 12px 16px;
2602
+ color: var(--muted);
2603
+ font: 700 0.72rem/1 ui-sans-serif, system-ui, sans-serif;
2604
+ letter-spacing: 0.12em;
2605
+ text-transform: uppercase;
2606
+ }
2607
+ .visual {
2608
+ padding: 24px;
2609
+ }
2610
+ .visual h1,
2611
+ .visual h2,
2612
+ .visual h3 {
2613
+ margin: 1.3em 0 0.45em;
2614
+ letter-spacing: -0.04em;
2615
+ line-height: 1;
2616
+ }
2617
+ .visual h1:first-child,
2618
+ .visual h2:first-child,
2619
+ .visual h3:first-child {
2620
+ margin-top: 0;
2621
+ }
2622
+ .visual h1 { font-size: 2.25rem; }
2623
+ .visual h2 { font-size: 1.6rem; }
2624
+ .visual h3 { font-size: 1.18rem; }
2625
+ .visual p,
2626
+ .visual li,
2627
+ .visual blockquote {
2628
+ color: #3d342c;
2629
+ font-size: 1rem;
2630
+ line-height: 1.62;
2631
+ }
2632
+ .visual ul {
2633
+ display: grid;
2634
+ gap: 8px;
2635
+ padding-left: 1.1rem;
2636
+ }
2637
+ .visual blockquote {
2638
+ margin: 18px 0;
2639
+ border-left: 4px solid var(--accent);
2640
+ padding-left: 14px;
2641
+ color: var(--muted);
2642
+ }
2643
+ .visual code {
2644
+ border-radius: 6px;
2645
+ background: var(--accent-soft);
2646
+ padding: 0.12rem 0.34rem;
2647
+ font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
2648
+ font-size: 0.92em;
2649
+ }
2650
+ pre {
2651
+ margin: 0;
2652
+ max-height: calc(100vh - 190px);
2653
+ overflow: auto;
2654
+ background: #181512;
2655
+ color: #f9ead7;
2656
+ padding: 20px;
2657
+ font: 0.78rem/1.55 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
2658
+ white-space: pre-wrap;
2659
+ word-break: break-word;
2660
+ }
2661
+ @media (max-width: 900px) {
2662
+ main { width: min(100vw - 24px, 760px); padding: 24px 0; }
2663
+ header { align-items: start; flex-direction: column; }
2664
+ pre { max-height: 520px; }
2665
+ }
2666
+ </style>
2667
+ </head>
2668
+ <body>
2669
+ <main>
2670
+ <header>
2671
+ <h1>${escapeHtml(title)}</h1>
2672
+ <div class="mode-toggle" aria-label="DesignMD view mode">
2673
+ <label>
2674
+ <input class="mode-input" type="radio" name="designmd-view-mode" id="designmd-visual-mode" checked />
2675
+ <span>Visual</span>
2676
+ </label>
2677
+ <label>
2678
+ <input class="mode-input" type="radio" name="designmd-view-mode" id="designmd-raw-mode" />
2679
+ <span>Raw</span>
2680
+ </label>
2681
+ </div>
2682
+ </header>
2683
+ <section class="viewer" aria-label="DesignMD artifact">
2684
+ <article class="panel viewer-panel visual-panel" id="designmd-visual">
2685
+ <div class="panel-title"><span>Visual</span><span>Rendered DESIGN.md</span></div>
2686
+ <div class="visual">${visualHtml}</div>
2687
+ </article>
2688
+ <article class="panel viewer-panel raw-panel" id="designmd-raw">
2689
+ <div class="panel-title"><span>Raw</span><span>Markdown source</span></div>
2690
+ <pre>${rawMarkdown}</pre>
2691
+ </article>
2692
+ </section>
2693
+ </main>
2694
+ </body>
2695
+ </html>`;
2696
+ };
2697
+ const renderDesignMarkdown = (markdown) => {
2698
+ const lines = markdown.replace(/\r\n/g, '\n').split('\n');
2699
+ const output = [];
2700
+ const paragraph = [];
2701
+ const listItems = [];
2702
+ const codeLines = [];
2703
+ let isCodeBlock = false;
2704
+ const flushParagraph = () => {
2705
+ if (paragraph.length === 0)
2706
+ return;
2707
+ output.push(`<p>${renderInlineMarkdown(paragraph.join(' '))}</p>`);
2708
+ paragraph.length = 0;
2709
+ };
2710
+ const flushList = () => {
2711
+ if (listItems.length === 0)
2712
+ return;
2713
+ output.push(`<ul>${listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join('')}</ul>`);
2714
+ listItems.length = 0;
2715
+ };
2716
+ const flushCode = () => {
2717
+ if (codeLines.length === 0)
2718
+ return;
2719
+ output.push(`<pre>${escapeHtml(codeLines.join('\n'))}</pre>`);
2720
+ codeLines.length = 0;
2721
+ };
2722
+ lines.forEach((line) => {
2723
+ if (line.trim().startsWith('```')) {
2724
+ if (isCodeBlock) {
2725
+ flushCode();
2726
+ isCodeBlock = false;
2727
+ return;
2728
+ }
2729
+ flushParagraph();
2730
+ flushList();
2731
+ isCodeBlock = true;
2732
+ return;
2733
+ }
2734
+ if (isCodeBlock) {
2735
+ codeLines.push(line);
2736
+ return;
2737
+ }
2738
+ const trimmed = line.trim();
2739
+ if (!trimmed) {
2740
+ flushParagraph();
2741
+ flushList();
2742
+ return;
2743
+ }
2744
+ if (/^---+$/.test(trimmed)) {
2745
+ flushParagraph();
2746
+ flushList();
2747
+ output.push('<hr />');
2748
+ return;
2749
+ }
2750
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
2751
+ if (headingMatch) {
2752
+ flushParagraph();
2753
+ flushList();
2754
+ const level = Math.min(headingMatch[1].length, 3);
2755
+ output.push(`<h${level}>${renderInlineMarkdown(headingMatch[2])}</h${level}>`);
2756
+ return;
2757
+ }
2758
+ const listMatch = trimmed.match(/^[-*]\s+(.+)$/);
2759
+ if (listMatch) {
2760
+ flushParagraph();
2761
+ listItems.push(listMatch[1]);
2762
+ return;
2763
+ }
2764
+ const quoteMatch = trimmed.match(/^>\s?(.+)$/);
2765
+ if (quoteMatch) {
2766
+ flushParagraph();
2767
+ flushList();
2768
+ output.push(`<blockquote>${renderInlineMarkdown(quoteMatch[1])}</blockquote>`);
2769
+ return;
2770
+ }
2771
+ paragraph.push(trimmed);
2772
+ });
2773
+ flushCode();
2774
+ flushParagraph();
2775
+ flushList();
2776
+ return output.join('\n');
2777
+ };
2778
+ const renderInlineMarkdown = (value) => {
2779
+ return escapeHtml(value)
2780
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
2781
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
2782
+ .replace(/\*([^*]+)\*/g, '<em>$1</em>');
2783
+ };
2784
+ const escapeHtml = (value) => {
2785
+ return value
2786
+ .replace(/&/g, '&amp;')
2787
+ .replace(/</g, '&lt;')
2788
+ .replace(/>/g, '&gt;')
2789
+ .replace(/"/g, '&quot;')
2790
+ .replace(/'/g, '&#39;');
2791
+ };
2397
2792
  /**
2398
2793
  * Resolve a per-variant design context entry into the raw DESIGN.md markdown
2399
2794
  * the worktree scaffolder writes, plus a small `designSource` descriptor for