proto-plugin 0.1.0 → 0.1.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.
Files changed (91) hide show
  1. package/AGENTS.md +308 -29
  2. package/README.md +55 -13
  3. package/package.json +1 -1
  4. package/scripts/lib/host-config.example.sh +1 -1
  5. package/scripts/publish-package.sh +41 -0
  6. package/scripts/verify-prototype-preview-states.mjs +57 -0
  7. package/src/components/platform-ui/controls-panel-select.tsx +4 -3
  8. package/src/components/platform-ui/prototype-tool-dialog.tsx +117 -0
  9. package/src/components/platform-ui/sidebar.tsx +34 -16
  10. package/src/components/prototype-gallery-client.tsx +77 -37
  11. package/src/components/prototypes/design-exploration-variant-preview-shell.tsx +43 -0
  12. package/src/components/prototypes/prototype-brief-field.tsx +33 -0
  13. package/src/components/prototypes/prototype-change-log-panel.tsx +4 -0
  14. package/src/components/prototypes/prototype-comment-storage-empty-state.tsx +41 -0
  15. package/src/components/prototypes/prototype-create-exploration-modal.tsx +41 -64
  16. package/src/components/prototypes/prototype-create-prototype-modal.tsx +102 -0
  17. package/src/components/prototypes/prototype-create-state-modal.tsx +31 -70
  18. package/src/components/prototypes/prototype-default-state-map-page.tsx +4 -4
  19. package/src/components/prototypes/prototype-design-brief-modal.tsx +10 -10
  20. package/src/components/prototypes/prototype-design-brief-panel.tsx +261 -231
  21. package/src/components/prototypes/prototype-floating-pill.module.scss +16 -9
  22. package/src/components/prototypes/prototype-mobbin-gallery.tsx +18 -2
  23. package/src/components/prototypes/prototype-review-sidebar.tsx +11 -28
  24. package/src/components/prototypes/prototype-spec-panel-content.tsx +5 -5
  25. package/src/components/prototypes/prototype-spec-panel-empty-state.tsx +53 -0
  26. package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +20 -47
  27. package/src/components/prototypes/prototype-state-canvas-overlay.tsx +4 -42
  28. package/src/components/prototypes/prototype-variant-explorer.tsx +32 -78
  29. package/src/components/prototypes/prototype-vercel-preview-modal.tsx +4 -3
  30. package/src/components/prototypes/state-map-annotation-tooltip.module.scss +7 -7
  31. package/src/components/shell/prototype-component-library-empty-state.tsx +59 -0
  32. package/src/components/shell/prototype-gallery-nav.tsx +124 -0
  33. package/src/components/shell/prototype-gallery-shell.tsx +45 -13
  34. package/src/components/shell/prototype-link-source-modal.tsx +100 -0
  35. package/src/components/ui/button.tsx +3 -1
  36. package/src/components/ui/dialog.tsx +43 -4
  37. package/src/config/create-prototype-component-library-page.tsx +74 -0
  38. package/src/config/create-prototype-gallery-page.tsx +26 -23
  39. package/src/config/create-prototype-host-app.tsx +3 -15
  40. package/src/config/create-prototype-site-layout.tsx +14 -1
  41. package/src/config/create-prototype-state-map-page.tsx +44 -0
  42. package/src/config/prototype-site-layout-client.tsx +20 -6
  43. package/src/config/with-prototype.ts +21 -0
  44. package/src/index.ts +54 -10
  45. package/src/lib/copy-text-with-attachments.ts +35 -0
  46. package/src/lib/format-source-directory-name.ts +12 -0
  47. package/src/lib/platform-ui/sidebar-interactive-cursor.ts +19 -0
  48. package/src/lib/pr-split/build-pr-split-plan-prompt.ts +81 -0
  49. package/src/lib/pr-split/pr-split-highlight.ts +3 -0
  50. package/src/lib/prototype-comments/core/annotation-target.ts +12 -3
  51. package/src/lib/prototype-comments/core/comment-capture-blocked.ts +24 -0
  52. package/src/lib/prototype-comments/core/screenshot.ts +68 -31
  53. package/src/lib/prototype-comments/core/storage.ts +3 -6
  54. package/src/lib/prototype-comments/core/types.ts +2 -0
  55. package/src/lib/prototype-comments/hooks/useLayoutShift.ts +9 -2
  56. package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +45 -10
  57. package/src/lib/prototype-comments/react/CommentProvider.tsx +19 -0
  58. package/src/lib/prototype-comments/ui/CommentModeToggleButton.tsx +34 -0
  59. package/src/lib/prototype-comments/ui/CommentsGrid.tsx +8 -16
  60. package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +5 -19
  61. package/src/lib/prototype-comments/ui/capture-styles.module.scss +5 -1
  62. package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +0 -7
  63. package/src/lib/prototypes/build-design-exploration-renderers.ts +44 -0
  64. package/src/lib/prototypes/comment-storage-setup-prompt.ts +124 -0
  65. package/src/lib/prototypes/create-prototype-prompt.ts +96 -0
  66. package/src/lib/prototypes/create-prototype-registry.ts +5 -0
  67. package/src/lib/prototypes/design-exploration-types.ts +261 -10
  68. package/src/lib/prototypes/link-source-prompt.ts +93 -0
  69. package/src/lib/prototypes/populate-component-library-prompt.ts +64 -0
  70. package/src/lib/prototypes/prototype-config-types.ts +2 -0
  71. package/src/lib/prototypes/prototype-layout-classes.ts +19 -0
  72. package/src/lib/prototypes/prototype-preview-state-registry.ts +5 -2
  73. package/src/lib/prototypes/prototype-review-context.tsx +4 -4
  74. package/src/lib/prototypes/prototype-state-canvas-constants.ts +1 -1
  75. package/src/lib/prototypes/prototype-state-canvas-types.ts +0 -2
  76. package/src/lib/prototypes/prototype-storage-status.ts +31 -0
  77. package/src/lib/prototypes/sort-prototypes-by-creation-date.ts +43 -0
  78. package/src/lib/prototypes/use-prototype-tool-theme.tsx +1 -1
  79. package/src/lib/prototypes/use-toolbar-hover-panel.ts +12 -1
  80. package/src/lib/prototypes/variant-set-lucide-icon.tsx +24 -0
  81. package/src/lib/tool-portal.ts +24 -0
  82. package/src/lib/use-copy-to-clipboard.ts +7 -13
  83. package/src/server/create-host-api-route.ts +7 -16
  84. package/src/server/create-prototype-api-routes.ts +0 -115
  85. package/src/server/prototype-storage-status-route.ts +29 -0
  86. package/src/server.ts +0 -8
  87. package/src/styles/globals.css +359 -71
  88. package/src/lib/prototypes/design-exploration-default-storage.ts +0 -68
  89. package/src/lib/prototypes/design-exploration-default.ts +0 -51
  90. package/src/lib/prototypes/use-design-exploration-default.ts +0 -114
  91. package/src/server/redis/prototype-design-exploration-default.ts +0 -32
@@ -1,14 +1,16 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
+ export type DesignExplorationRationale = {
4
+ good: string;
5
+ bad: string;
6
+ };
7
+
3
8
  export type DesignExplorationVariantOption<TVariant extends string> = {
4
9
  value: TVariant;
5
10
  label: string;
6
11
  hint?: string;
7
- };
8
-
9
- export type DesignExplorationRationale = {
10
- good: string;
11
- bad: string;
12
+ /** One-line pros/cons shown under the variant title in the design brief. */
13
+ rationale?: DesignExplorationRationale;
12
14
  };
13
15
 
14
16
  export type DesignExplorationBrief = {
@@ -47,26 +49,82 @@ export type DesignExplorationVariantsSection = {
47
49
  description?: string;
48
50
  };
49
51
 
50
- /** Persists to KV via `storageKeyPrefix` use `useDesignExplorationDefault`. */
52
+ /** Pre-exploration UI always pinned at the bottom of the variant list. */
53
+ export type DesignExplorationBaselineOption<TVariant extends string> = {
54
+ value: TVariant;
55
+ /** Shown in the brief and tabs. Default: "Original". */
56
+ label?: string;
57
+ hint?: string;
58
+ rationale?: DesignExplorationRationale;
59
+ };
60
+
51
61
  export type DesignExplorationConfig<TVariant extends string> = {
52
62
  componentIdPrefix: string;
53
63
  variantTabsIdPrefix: string;
54
64
  storageKeyPrefix: string;
55
65
  variant: TVariant;
56
66
  onVariantChange: (variant: TVariant) => void;
67
+ /** Layout directions being explored — newest first. Excludes baseline. */
57
68
  options: DesignExplorationVariantOption<TVariant>[];
58
- rationale: Record<TVariant, DesignExplorationRationale>;
59
69
  renderers: Record<TVariant, () => ReactNode>;
60
70
  brief: DesignExplorationBrief;
61
71
  context?: DesignExplorationContext;
62
72
  mobbin?: DesignExplorationMobbin;
63
73
  variantsSection?: DesignExplorationVariantsSection;
74
+ /**
75
+ * UI state before this exploration was added. Rendered last in the brief
76
+ * and tabs; deduped from `options` when the same value appears there.
77
+ */
78
+ baseline: DesignExplorationBaselineOption<TVariant>;
79
+ /**
80
+ * Default variant stored in the codebase (e.g. `DEFAULT_*_VARIANT`). Marks
81
+ * the "Default" option in the design brief. Falls back to the first option.
82
+ */
83
+ defaultVariant?: TVariant;
64
84
  /** Label for prev/next variant tab buttons. Default: "variant". */
65
85
  variantTabAriaLabel?: string;
66
- /** Path to design exploration config — included in reset-brief copy prompt. */
86
+ /** Path to design exploration config — included in copy prompts. */
67
87
  briefConfigFilePath?: string;
68
88
  };
69
89
 
90
+ export const DESIGN_EXPLORATION_BASELINE_LABEL = "Original";
91
+
92
+ export function resolveDesignExplorationBaselineOption<
93
+ TVariant extends string,
94
+ >(
95
+ baseline: DesignExplorationBaselineOption<TVariant>,
96
+ ): DesignExplorationVariantOption<TVariant> {
97
+ return {
98
+ value: baseline.value,
99
+ label: baseline.label ?? DESIGN_EXPLORATION_BASELINE_LABEL,
100
+ hint: baseline.hint,
101
+ rationale: baseline.rationale,
102
+ };
103
+ }
104
+
105
+ /** Exploration options only — baseline value removed when duplicated. */
106
+ export function getDesignExplorationVariantOptions<
107
+ TVariant extends string,
108
+ >(
109
+ options: DesignExplorationVariantOption<TVariant>[],
110
+ baseline: DesignExplorationBaselineOption<TVariant>,
111
+ ): DesignExplorationVariantOption<TVariant>[] {
112
+ return options.filter((option) => option.value !== baseline.value);
113
+ }
114
+
115
+ /** Exploration options plus baseline pinned at the bottom (for tabs / cycling). */
116
+ export function getDesignExplorationDisplayOptions<
117
+ TVariant extends string,
118
+ >(
119
+ options: DesignExplorationVariantOption<TVariant>[],
120
+ baseline: DesignExplorationBaselineOption<TVariant>,
121
+ ): DesignExplorationVariantOption<TVariant>[] {
122
+ return [
123
+ ...getDesignExplorationVariantOptions(options, baseline),
124
+ resolveDesignExplorationBaselineOption(baseline),
125
+ ];
126
+ }
127
+
70
128
  /** Plain-text copy of the overview fields as shown in the design brief modal. */
71
129
  export function buildBriefOverviewCopyText(
72
130
  title: string,
@@ -84,11 +142,13 @@ export function buildCreateDesignExplorationPrompt({
84
142
  existingExplorations = [],
85
143
  origin = "http://localhost:3003",
86
144
  briefText,
145
+ includeMobbin = true,
87
146
  }: {
88
147
  slug: string;
89
148
  existingExplorations?: Array<{ id: string; label: string }>;
90
149
  origin?: string;
91
150
  briefText?: string;
151
+ includeMobbin?: boolean;
92
152
  }): string {
93
153
  const prototypeUrl = `${origin}/prototypes/${slug}`;
94
154
  const variantSetsRegistrar = `src/prototypes/${slug}/_components/*-variant-sets.tsx`;
@@ -122,6 +182,27 @@ export function buildCreateDesignExplorationPrompt({
122
182
  "## Prototype",
123
183
  `- Slug: \`${slug}\``,
124
184
  `- Preview: ${prototypeUrl}`,
185
+ );
186
+
187
+ if (includeMobbin) {
188
+ lines.push(
189
+ "",
190
+ "## Mobbin references",
191
+ "Before building variants, pull real-world reference screens from Mobbin. Use the Mobbin MCP `search_screens` tool when available; otherwise search on mobbin.com.",
192
+ "",
193
+ "1. Search for **3–5 screens** that match the brief and the distinct layout directions you plan to explore. Prefer specific UI descriptions (e.g. \"settings page with profile photo and bio field\") over vague style words.",
194
+ "2. Pick references that clarify layout, hierarchy, and copy patterns — not just visual polish.",
195
+ "3. In `*-design-exploration-config.tsx`, add a `mobbin` block:",
196
+ " - `references`: `{ id, appName, imageUrl, mobbinUrl, relevance, variantHint? }[]` — use each screen UUID as `id`",
197
+ " - `imagePathForReference`: `(id) => \\`/prototypes/mobbin-references/${id}.webp\\``",
198
+ "4. Register `mobbin-inspiration-gallery` and `mobbin-inspiration-gallery.` in `component-ids.ts` (or pass a custom `mobbinGalleryId` to `PrototypeVariantExplorer`).",
199
+ "5. Download local assets: `pnpm download-mobbin-references -- --ids=<comma-separated-ids>`",
200
+ "",
201
+ "Reference: `src/prototypes/proto-partner-page/_components/invite-copy-design-exploration-config.tsx`.",
202
+ );
203
+ }
204
+
205
+ lines.push(
125
206
  "",
126
207
  "## Instructions",
127
208
  "Follow AGENTS.md → Design exploration. Do not register a new prototype slug — add a design exploration variant set on this page.",
@@ -129,7 +210,12 @@ export function buildCreateDesignExplorationPrompt({
129
210
  "1. Add `{ id, label, previewMode? }` to the variant sets registrar (e.g. `" +
130
211
  variantSetsRegistrar +
131
212
  "`).",
132
- "2. Create `*-design-exploration-config.tsx` with options, rationale, renderers, and brief defaults.",
213
+ " - Give this exploration a **distinct Lucide icon** in the review sidebar: add a `{ test, icon }` rule to `packages/prototype/src/lib/prototypes/variant-set-lucide-icon.tsx` that matches its id or label. Place the rule **before** broad matchers (e.g. `/mobile/i`) so it wins. Pick an icon that fits the topic and avoid reusing icons already assigned to other explorations on this prototype — do not leave new explorations on the default `LayoutGrid` fallback or the generic `Smartphone` icon shared by unrelated mobile explorations.",
214
+ `2. Create \`*-design-exploration-config.tsx\` with options, renderers, brief defaults, baseline,${includeMobbin ? " and the Mobbin references above" : ""}.`,
215
+ " - `baseline`: the UI before this exploration existed — `{ value, label?, hint? }`, pinned at the bottom of the variant list.",
216
+ " - `options`: layout directions to compare — exclude the baseline value when possible.",
217
+ " - Build renderers with `buildDesignExplorationRenderers(options, (variant) => …, baseline)` — one preview per option plus baseline.",
218
+ " - Wrap overlay-style variants in `DesignExplorationVariantPreviewShell` with `layout=\"overlay\"` for sidebar previews.",
133
219
  "3. Create `*-variant-toggle.tsx` mounting `PrototypeVariantExplorer` with matching `variantSet={{ id, label }}`.",
134
220
  "4. Register static ids + any `dynamicPrefixes` in `src/prototypes/" +
135
221
  slug +
@@ -149,11 +235,13 @@ export function buildCreateDesignExplorationCopyText({
149
235
  existingExplorations = [],
150
236
  origin = "http://localhost:3003",
151
237
  briefText,
238
+ includeMobbin = true,
152
239
  }: {
153
240
  slug: string;
154
241
  existingExplorations?: Array<{ id: string; label: string }>;
155
242
  origin?: string;
156
243
  briefText: string;
244
+ includeMobbin?: boolean;
157
245
  }): string {
158
246
  const trimmedBrief = briefText.trim();
159
247
  const prompt = buildCreateDesignExplorationPrompt({
@@ -161,6 +249,141 @@ export function buildCreateDesignExplorationCopyText({
161
249
  existingExplorations,
162
250
  origin,
163
251
  briefText: trimmedBrief,
252
+ includeMobbin,
253
+ });
254
+
255
+ return `${trimmedBrief}\n\n---\n\n${prompt}`;
256
+ }
257
+
258
+ export function buildMoreDesignExplorationVariantsPrompt({
259
+ slug,
260
+ explorationLabel,
261
+ explorationId,
262
+ options,
263
+ brief,
264
+ configFilePath,
265
+ origin = "http://localhost:3003",
266
+ briefText,
267
+ multipleVersions = true,
268
+ }: {
269
+ slug: string;
270
+ explorationLabel: string;
271
+ explorationId?: string;
272
+ options: Array<{ value: string; label: string; hint?: string }>;
273
+ brief: DesignExplorationBrief;
274
+ configFilePath?: string;
275
+ origin?: string;
276
+ briefText?: string;
277
+ /** When false, prompt asks for one targeted derivative variant. */
278
+ multipleVersions?: boolean;
279
+ }): string {
280
+ const prototypeUrl = `${origin}/prototypes/${slug}`;
281
+ const configTarget = configFilePath ?? "the design exploration config file";
282
+ const trimmedBrief = briefText?.trim() ?? "";
283
+
284
+ const lines = [
285
+ `Add new layout variants to the "${explorationLabel}" design exploration on the "${slug}" prototype.`,
286
+ "",
287
+ "## Brief",
288
+ ];
289
+
290
+ if (trimmedBrief) {
291
+ lines.push(trimmedBrief);
292
+ } else if (multipleVersions) {
293
+ lines.push(
294
+ "Describe the new directions to explore:",
295
+ "- What to try:",
296
+ "- Target count: 3–5 new variants",
297
+ );
298
+ } else {
299
+ lines.push(
300
+ "Describe the variation to explore:",
301
+ "- What to try:",
302
+ "- Target count: 1 new variant",
303
+ );
304
+ }
305
+
306
+ lines.push(
307
+ "",
308
+ "## Exploration context",
309
+ `- Label: ${explorationLabel}`,
310
+ );
311
+
312
+ if (explorationId) {
313
+ lines.push(`- Variant set id: \`${explorationId}\``);
314
+ }
315
+
316
+ lines.push(
317
+ `- Overview title: ${brief.titleDefault}`,
318
+ `- Overview description: ${brief.descriptionDefault}`,
319
+ "",
320
+ "## Existing variants",
321
+ );
322
+
323
+ for (const option of options) {
324
+ const hint = option.hint ? ` — ${option.hint}` : "";
325
+ lines.push(`- ${option.label} (\`${option.value}\`)${hint}`);
326
+ }
327
+
328
+ lines.push(
329
+ "",
330
+ "## Prototype",
331
+ `- Slug: \`${slug}\``,
332
+ `- Config: \`${configTarget}\``,
333
+ `- Preview: ${prototypeUrl}`,
334
+ "",
335
+ "## Instructions",
336
+ "Follow AGENTS.md → Design exploration → Iterating on variants.",
337
+ "",
338
+ multipleVersions
339
+ ? "Supply **3–5 new variant options** unless the brief specifies a different count."
340
+ : "Add **one new derivative variant** unless the brief specifies otherwise.",
341
+ "",
342
+ multipleVersions
343
+ ? "When modifying an existing option, add **new variants** (do not replace in place)."
344
+ : "When modifying an existing option, add a **single new variant** (do not replace in place). Keep all existing variants.",
345
+ "",
346
+ "1. Prepend new entries to the top of `*_VARIANT_OPTIONS` so newest explorations appear first.",
347
+ "2. Add matching renderers via `buildDesignExplorationRenderers` and any new component ids.",
348
+ "3. Keep existing variants unless the brief explicitly asks to remove one.",
349
+ "4. Run `pnpm verify:prototype-ids`.",
350
+ );
351
+
352
+ return lines.join("\n");
353
+ }
354
+
355
+ export function buildMoreDesignExplorationVariantsCopyText({
356
+ slug,
357
+ explorationLabel,
358
+ explorationId,
359
+ options,
360
+ brief,
361
+ configFilePath,
362
+ origin = "http://localhost:3003",
363
+ briefText,
364
+ multipleVersions = true,
365
+ }: {
366
+ slug: string;
367
+ explorationLabel: string;
368
+ explorationId?: string;
369
+ options: Array<{ value: string; label: string; hint?: string }>;
370
+ brief: DesignExplorationBrief;
371
+ configFilePath?: string;
372
+ origin?: string;
373
+ briefText: string;
374
+ multipleVersions?: boolean;
375
+ }): string {
376
+ const trimmedBrief = briefText.trim();
377
+ const prompt = buildMoreDesignExplorationVariantsPrompt({
378
+ slug,
379
+ explorationLabel,
380
+ explorationId,
381
+ options,
382
+ brief,
383
+ configFilePath,
384
+ origin,
385
+ briefText: trimmedBrief,
386
+ multipleVersions,
164
387
  });
165
388
 
166
389
  return `${trimmedBrief}\n\n---\n\n${prompt}`;
@@ -187,9 +410,37 @@ export function buildResetBriefDefaultsPrompt({
187
410
  ].join("\n");
188
411
  }
189
412
 
413
+ /**
414
+ * Prompt that asks an agent to set the codebase-stored default variant. Copied
415
+ * by the "Make default" control in the design brief — the default lives in the
416
+ * config file (e.g. `DEFAULT_*_VARIANT`), not in KV storage.
417
+ */
418
+ export function buildSetDefaultVariantPrompt({
419
+ variantLabel,
420
+ variantValue,
421
+ configFilePath,
422
+ }: {
423
+ variantLabel: string;
424
+ variantValue: string;
425
+ configFilePath?: string;
426
+ }): string {
427
+ const target = configFilePath ?? "the design exploration config file";
428
+
429
+ return [
430
+ `Make "${variantLabel}" the default design exploration variant.`,
431
+ "",
432
+ `In ${target}, update the exported default variant constant (e.g. \`DEFAULT_*_VARIANT\`) so it equals:`,
433
+ "",
434
+ variantValue,
435
+ "",
436
+ 'This constant is the codebase-stored default the prototype renders first, and the option marked "Default" in the design brief. Keep it as one of the values in the variant options list.',
437
+ ].join("\n");
438
+ }
439
+
190
440
  /**
191
441
  * Prepend new entries to the top of *_VARIANT_OPTIONS so they appear first in the
192
- * design-brief scroll view (see AGENTS.md). Default variant is stored in KV.
442
+ * design-brief scroll view (see AGENTS.md). The default variant is stored in the
443
+ * codebase config (`defaultVariant`), not KV.
193
444
  */
194
445
  export function getAdjacentDesignExplorationVariant<TVariant extends string>(
195
446
  options: DesignExplorationVariantOption<TVariant>[],
@@ -0,0 +1,93 @@
1
+ export function buildLinkSourcePrompt({
2
+ sourceInput,
3
+ description,
4
+ currentSourcePath,
5
+ referencePaths = [],
6
+ }: {
7
+ sourceInput?: string;
8
+ description?: string;
9
+ currentSourcePath?: string;
10
+ referencePaths?: string[];
11
+ } = {}): string {
12
+ const trimmedInput = sourceInput?.trim();
13
+ const trimmedDescription = description?.trim();
14
+ const trimmedCurrent = currentSourcePath?.trim();
15
+ const isUpdate = Boolean(trimmedCurrent);
16
+
17
+ const lines = [
18
+ isUpdate
19
+ ? "Update the linked source application for this prototype host app."
20
+ : "Link the source application for this prototype host app.",
21
+ "",
22
+ "## Goal",
23
+ "Set `SOURCE_PATH` in `.env.local` and run `pnpm link-source` so the `source/` symlink points at the real product repo.",
24
+ ];
25
+
26
+ if (trimmedCurrent) {
27
+ lines.push("", "## Current source", `- \`SOURCE_PATH\`: \`${trimmedCurrent}\``);
28
+ }
29
+
30
+ lines.push("", "## Desired source");
31
+
32
+ if (trimmedInput) {
33
+ lines.push(`- ${trimmedInput}`);
34
+ lines.push(
35
+ "",
36
+ "Resolve this to a filesystem path (folder name, relative path like `../cal.diy`, or absolute path) before writing `.env.local`.",
37
+ );
38
+ } else {
39
+ lines.push("- (Specify folder name, path, or description above before copying.)");
40
+ }
41
+
42
+ lines.push(
43
+ "",
44
+ "## Clarification",
45
+ "If the input is vague, matches multiple folders on disk, or you cannot confidently identify the correct repo from the description and filesystem, ask the user a clarifying question before changing anything. Do not guess — confirm the exact path first.",
46
+ );
47
+
48
+ if (trimmedDescription) {
49
+ lines.push("", "## Notes", trimmedDescription);
50
+ }
51
+
52
+ if (referencePaths.length > 0) {
53
+ lines.push(
54
+ "",
55
+ "## Reference screenshots",
56
+ "Use these files for additional context:",
57
+ );
58
+ for (const referencePath of referencePaths) {
59
+ lines.push(`- \`${referencePath}\``);
60
+ }
61
+ }
62
+
63
+ lines.push(
64
+ "",
65
+ "## Steps",
66
+ "1. Confirm the correct source path. If it is still unclear after checking the description and filesystem, ask the user before proceeding.",
67
+ "2. Set or update `SOURCE_PATH` in `.env.local` at the host repo root (see `.env.example`).",
68
+ "3. Run `pnpm link-source` from the host repo to create or refresh the `source/` symlink.",
69
+ "4. Restart the dev server if it is already running so env changes are picked up.",
70
+ "5. Confirm the gallery sidebar footer shows the correct source directory name.",
71
+ );
72
+
73
+ return lines.join("\n");
74
+ }
75
+
76
+ export function buildLinkSourceCopyText({
77
+ sourcePath,
78
+ description,
79
+ currentSourcePath,
80
+ referencePaths = [],
81
+ }: {
82
+ sourcePath?: string;
83
+ description?: string;
84
+ currentSourcePath?: string;
85
+ referencePaths?: string[];
86
+ } = {}): string {
87
+ return buildLinkSourcePrompt({
88
+ sourceInput: sourcePath,
89
+ description,
90
+ currentSourcePath,
91
+ referencePaths,
92
+ });
93
+ }
@@ -0,0 +1,64 @@
1
+ const DEFAULT_SYNC_CONFIG_PATH = "prototype.sync.config.sh";
2
+ const DEFAULT_COMPONENT_LIBRARY_PAGE_PATH = "src/app/component-library/page.tsx";
3
+
4
+ export function buildPopulateComponentLibraryPrompt({
5
+ sourcePath,
6
+ syncConfigPath = DEFAULT_SYNC_CONFIG_PATH,
7
+ componentLibraryPagePath = DEFAULT_COMPONENT_LIBRARY_PAGE_PATH,
8
+ origin = "http://localhost:3003",
9
+ }: {
10
+ sourcePath?: string;
11
+ syncConfigPath?: string;
12
+ componentLibraryPagePath?: string;
13
+ origin?: string;
14
+ } = {}): string {
15
+ const sourceLine = sourcePath?.trim()
16
+ ? `- Source path: \`${sourcePath.trim()}\` (also readable via the \`source/\` symlink after \`pnpm link-source\`)`
17
+ : "- Source: set `SOURCE_PATH` in `.env.local`, run `pnpm link-source`, then read via the `source/` symlink";
18
+
19
+ const lines = [
20
+ "Populate the Component Library for this prototype host app.",
21
+ "",
22
+ "## Goal",
23
+ "The Component Library page is empty. Inspect the linked source application and extract all base styles and UI building blocks into this host repo.",
24
+ "",
25
+ "**Be generous — when in doubt, port more rather than less.** Prefer syncing whole directories and shared primitives over cherry-picking a minimal subset.",
26
+ "",
27
+ "## Source",
28
+ sourceLine,
29
+ "- Read the source app thoroughly before deciding what to skip.",
30
+ "",
31
+ "## What to extract (prioritize breadth)",
32
+ "1. Design tokens and global styles (`globals.css`, fonts, CSS variables, theme layers)",
33
+ "2. All shadcn/base UI primitives under `src/components/ui/`",
34
+ "3. Shared layout primitives, form patterns, badges, cards, tables, dialogs, and other reusable chrome",
35
+ "4. Shared utilities used by UI (`src/lib/utils.ts`, `cn` helpers, formatters, small hooks)",
36
+ "5. Composite components reused across multiple product surfaces",
37
+ "6. If the source organizes design-system pieces elsewhere (monorepo packages, shared folders), trace what product pages actually import and replicate those pieces here",
38
+ "",
39
+ "## How to sync",
40
+ "1. Ensure `SOURCE_PATH` is set in `.env.local` and run `pnpm link-source`.",
41
+ `2. Update \`${syncConfigPath}\` — add paths to \`SYNC_FILES\`, \`SYNC_DIRS\`, and/or \`SYNC_GLOBS\`. Prefer syncing whole directories (e.g. \`src/components/ui\`) over individual files.`,
42
+ "3. Run `pnpm sync-from-source`.",
43
+ "4. Re-apply any entries listed in `SYNC_LOCAL_EXTENSIONS` if this host customizes beyond source.",
44
+ "",
45
+ "## Component Library page",
46
+ `After syncing, wire live previews on \`${componentLibraryPagePath}\` via \`createPrototypeComponentLibraryPage\` so each primitive and composite is visible with common variants and states.`,
47
+ "",
48
+ `Gallery: ${origin}/component-library`,
49
+ "",
50
+ "## Rules",
51
+ "- Follow AGENTS.md — no runtime imports from `source/`; replicate into the host tree.",
52
+ "- Preserve semantic tokens from `globals.css`; avoid hardcoded hex.",
53
+ "- Confirm components render correctly on `/component-library` before finishing.",
54
+ ];
55
+
56
+ return lines.join("\n");
57
+ }
58
+
59
+ export function buildPopulateComponentLibraryCopyText(
60
+ options: Parameters<typeof buildPopulateComponentLibraryPrompt>[0] = {},
61
+ ): string {
62
+ const prompt = buildPopulateComponentLibraryPrompt(options);
63
+ return prompt;
64
+ }
@@ -9,6 +9,8 @@ export type PrototypeDefinition = {
9
9
  screenshot: string;
10
10
  component: ComponentType;
11
11
  componentRegistry: PrototypeComponentRegistry;
12
+ /** Full-page state map at `/prototypes/<slug>/states`. Omit to show the default empty map. */
13
+ stateMapComponent?: ComponentType;
12
14
  };
13
15
 
14
16
  export type PrototypeExtraRoute = {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Canonical Tailwind fill classes for prototype pages inside #prototype-viewport.
3
+ * The preview shell only auto-sizes direct children of .viewportPage — every nested
4
+ * layer must repeat this chain or content pins to a corner and clips.
5
+ *
6
+ * @see packages/prototype/AGENTS.md — "Prototype viewport fill"
7
+ */
8
+
9
+ /** `PrototypeComponent` with id="scroll-container" */
10
+ export const PROTOTYPE_SCROLL_CONTAINER_CLASS =
11
+ "flex h-full min-h-0 flex-1 flex-col overflow-hidden";
12
+
13
+ /** `PrototypeComponent` with id="page" */
14
+ export const PROTOTYPE_PAGE_CLASS =
15
+ "relative flex h-full min-h-0 flex-1 flex-col overflow-hidden";
16
+
17
+ /** Step/view roots, AnimatePresence wrappers, motion.div page shells */
18
+ export const PROTOTYPE_VIEW_SHELL_CLASS =
19
+ "flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden";
@@ -212,8 +212,11 @@ export function buildPreviewStateCanvasLayout<T extends string>(
212
212
  };
213
213
  }
214
214
 
215
- function resolvePreviewStateRegistryLayout<TPrimary extends string>(
216
- registry: PrototypePreviewStateRegistry<TPrimary>,
215
+ function resolvePreviewStateRegistryLayout<
216
+ TPrimary extends string,
217
+ TVariant extends string = never,
218
+ >(
219
+ registry: PrototypePreviewStateRegistry<TPrimary, TVariant>,
217
220
  ): {
218
221
  states: ResolvedPrototypePreviewStateDefinition<TPrimary>[];
219
222
  sections: PrototypeStateCanvasSection[];
@@ -732,8 +732,8 @@ export function PrototypeReviewProvider({
732
732
  showProdReference,
733
733
  ]);
734
734
 
735
- const value = useMemo(
736
- () => ({
735
+ const value = useMemo(() => {
736
+ return {
737
737
  slug,
738
738
  open,
739
739
  sidebarPanel,
@@ -806,8 +806,8 @@ export function PrototypeReviewProvider({
806
806
  viewportLayout,
807
807
  setViewportLayout,
808
808
  toggleViewportLayout,
809
- }),
810
- [
809
+ };
810
+ }, [
811
811
  slug,
812
812
  open,
813
813
  sidebarPanel,
@@ -85,7 +85,7 @@ export function getStateCanvasNodeHeight(
85
85
  const count = callouts?.length ?? 0;
86
86
  if (count === 0) return bodyHeight;
87
87
 
88
- const calloutsHeight = callouts.reduce((sum, callout, index) => {
88
+ const calloutsHeight = (callouts ?? []).reduce((sum, callout, index) => {
89
89
  const gap = index > 0 ? PROTOTYPE_STATE_NODE_CALLOUT_GAP : 0;
90
90
  return sum + gap + getStateCanvasCalloutHeight(callout);
91
91
  }, 0);
@@ -75,8 +75,6 @@ export type StateMapHighlightRect = {
75
75
  };
76
76
 
77
77
  export type PrototypeStateCanvasConfig<T extends string = string> = {
78
- /** When omitted, no node appears selected — the map is a stateless picker. */
79
- activeStateId?: T;
80
78
  onStateSelect: (id: T) => void;
81
79
  nodes: PrototypeStateCanvasNode<T>[];
82
80
  edges: PrototypeStateCanvasEdge<T>[];
@@ -0,0 +1,31 @@
1
+ export type PrototypeStorageStatusSlice = {
2
+ configured: boolean;
3
+ missing: string[];
4
+ };
5
+
6
+ export type PrototypeStorageStatus = {
7
+ comments: PrototypeStorageStatusSlice;
8
+ gallery: {
9
+ kvConfigured: boolean;
10
+ storageConfigured: boolean;
11
+ missing: string[];
12
+ };
13
+ };
14
+
15
+ export const PROTOTYPE_STORAGE_STATUS_PATH = "/api/prototypes/storage-status";
16
+
17
+ export async function fetchPrototypeStorageStatus(): Promise<PrototypeStorageStatus> {
18
+ const response = await fetch(PROTOTYPE_STORAGE_STATUS_PATH);
19
+
20
+ if (!response.ok) {
21
+ throw new Error("Failed to check prototype storage status.");
22
+ }
23
+
24
+ return (await response.json()) as PrototypeStorageStatus;
25
+ }
26
+
27
+ export function isPrototypeStorageFullyConfigured(
28
+ status: PrototypeStorageStatus,
29
+ ): boolean {
30
+ return status.comments.configured && status.gallery.storageConfigured;
31
+ }
@@ -0,0 +1,43 @@
1
+ import { stat } from "fs/promises";
2
+ import path from "path";
3
+
4
+ import type { PrototypeMetadata } from "./prototype-config-types";
5
+
6
+ async function getPrototypeCreationMs(
7
+ slug: string,
8
+ configIndex: number,
9
+ ): Promise<number> {
10
+ try {
11
+ const stats = await stat(
12
+ path.join(process.cwd(), "src/prototypes", slug),
13
+ );
14
+ if (stats.birthtimeMs > 0) return stats.birthtimeMs;
15
+ } catch {
16
+ // Prototype directory may not exist in some host layouts.
17
+ }
18
+
19
+ // Config order is append-only — use registration index when birthtime is unavailable.
20
+ return configIndex;
21
+ }
22
+
23
+ export async function sortPrototypesByCreationDate(
24
+ prototypes: PrototypeMetadata[],
25
+ ): Promise<PrototypeMetadata[]> {
26
+ const creationMsBySlug = new Map<string, number>();
27
+
28
+ await Promise.all(
29
+ prototypes.map(async (prototype, configIndex) => {
30
+ creationMsBySlug.set(
31
+ prototype.slug,
32
+ await getPrototypeCreationMs(prototype.slug, configIndex),
33
+ );
34
+ }),
35
+ );
36
+
37
+ return [...prototypes].sort((a, b) => {
38
+ const creationDiff =
39
+ (creationMsBySlug.get(b.slug) ?? 0) - (creationMsBySlug.get(a.slug) ?? 0);
40
+ if (creationDiff !== 0) return creationDiff;
41
+ return a.title.localeCompare(b.title);
42
+ });
43
+ }