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.
- package/AGENTS.md +308 -29
- package/README.md +55 -13
- package/package.json +1 -1
- package/scripts/lib/host-config.example.sh +1 -1
- package/scripts/publish-package.sh +41 -0
- package/scripts/verify-prototype-preview-states.mjs +57 -0
- package/src/components/platform-ui/controls-panel-select.tsx +4 -3
- package/src/components/platform-ui/prototype-tool-dialog.tsx +117 -0
- package/src/components/platform-ui/sidebar.tsx +34 -16
- package/src/components/prototype-gallery-client.tsx +77 -37
- package/src/components/prototypes/design-exploration-variant-preview-shell.tsx +43 -0
- package/src/components/prototypes/prototype-brief-field.tsx +33 -0
- package/src/components/prototypes/prototype-change-log-panel.tsx +4 -0
- package/src/components/prototypes/prototype-comment-storage-empty-state.tsx +41 -0
- package/src/components/prototypes/prototype-create-exploration-modal.tsx +41 -64
- package/src/components/prototypes/prototype-create-prototype-modal.tsx +102 -0
- package/src/components/prototypes/prototype-create-state-modal.tsx +31 -70
- package/src/components/prototypes/prototype-default-state-map-page.tsx +4 -4
- package/src/components/prototypes/prototype-design-brief-modal.tsx +10 -10
- package/src/components/prototypes/prototype-design-brief-panel.tsx +261 -231
- package/src/components/prototypes/prototype-floating-pill.module.scss +16 -9
- package/src/components/prototypes/prototype-mobbin-gallery.tsx +18 -2
- package/src/components/prototypes/prototype-review-sidebar.tsx +11 -28
- package/src/components/prototypes/prototype-spec-panel-content.tsx +5 -5
- package/src/components/prototypes/prototype-spec-panel-empty-state.tsx +53 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +20 -47
- package/src/components/prototypes/prototype-state-canvas-overlay.tsx +4 -42
- package/src/components/prototypes/prototype-variant-explorer.tsx +32 -78
- package/src/components/prototypes/prototype-vercel-preview-modal.tsx +4 -3
- package/src/components/prototypes/state-map-annotation-tooltip.module.scss +7 -7
- package/src/components/shell/prototype-component-library-empty-state.tsx +59 -0
- package/src/components/shell/prototype-gallery-nav.tsx +124 -0
- package/src/components/shell/prototype-gallery-shell.tsx +45 -13
- package/src/components/shell/prototype-link-source-modal.tsx +100 -0
- package/src/components/ui/button.tsx +3 -1
- package/src/components/ui/dialog.tsx +43 -4
- package/src/config/create-prototype-component-library-page.tsx +74 -0
- package/src/config/create-prototype-gallery-page.tsx +26 -23
- package/src/config/create-prototype-host-app.tsx +3 -15
- package/src/config/create-prototype-site-layout.tsx +14 -1
- package/src/config/create-prototype-state-map-page.tsx +44 -0
- package/src/config/prototype-site-layout-client.tsx +20 -6
- package/src/config/with-prototype.ts +21 -0
- package/src/index.ts +54 -10
- package/src/lib/copy-text-with-attachments.ts +35 -0
- package/src/lib/format-source-directory-name.ts +12 -0
- package/src/lib/platform-ui/sidebar-interactive-cursor.ts +19 -0
- package/src/lib/pr-split/build-pr-split-plan-prompt.ts +81 -0
- package/src/lib/pr-split/pr-split-highlight.ts +3 -0
- package/src/lib/prototype-comments/core/annotation-target.ts +12 -3
- package/src/lib/prototype-comments/core/comment-capture-blocked.ts +24 -0
- package/src/lib/prototype-comments/core/screenshot.ts +68 -31
- package/src/lib/prototype-comments/core/storage.ts +3 -6
- package/src/lib/prototype-comments/core/types.ts +2 -0
- package/src/lib/prototype-comments/hooks/useLayoutShift.ts +9 -2
- package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +45 -10
- package/src/lib/prototype-comments/react/CommentProvider.tsx +19 -0
- package/src/lib/prototype-comments/ui/CommentModeToggleButton.tsx +34 -0
- package/src/lib/prototype-comments/ui/CommentsGrid.tsx +8 -16
- package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +5 -19
- package/src/lib/prototype-comments/ui/capture-styles.module.scss +5 -1
- package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +0 -7
- package/src/lib/prototypes/build-design-exploration-renderers.ts +44 -0
- package/src/lib/prototypes/comment-storage-setup-prompt.ts +124 -0
- package/src/lib/prototypes/create-prototype-prompt.ts +96 -0
- package/src/lib/prototypes/create-prototype-registry.ts +5 -0
- package/src/lib/prototypes/design-exploration-types.ts +261 -10
- package/src/lib/prototypes/link-source-prompt.ts +93 -0
- package/src/lib/prototypes/populate-component-library-prompt.ts +64 -0
- package/src/lib/prototypes/prototype-config-types.ts +2 -0
- package/src/lib/prototypes/prototype-layout-classes.ts +19 -0
- package/src/lib/prototypes/prototype-preview-state-registry.ts +5 -2
- package/src/lib/prototypes/prototype-review-context.tsx +4 -4
- package/src/lib/prototypes/prototype-state-canvas-constants.ts +1 -1
- package/src/lib/prototypes/prototype-state-canvas-types.ts +0 -2
- package/src/lib/prototypes/prototype-storage-status.ts +31 -0
- package/src/lib/prototypes/sort-prototypes-by-creation-date.ts +43 -0
- package/src/lib/prototypes/use-prototype-tool-theme.tsx +1 -1
- package/src/lib/prototypes/use-toolbar-hover-panel.ts +12 -1
- package/src/lib/prototypes/variant-set-lucide-icon.tsx +24 -0
- package/src/lib/tool-portal.ts +24 -0
- package/src/lib/use-copy-to-clipboard.ts +7 -13
- package/src/server/create-host-api-route.ts +7 -16
- package/src/server/create-prototype-api-routes.ts +0 -115
- package/src/server/prototype-storage-status-route.ts +29 -0
- package/src/server.ts +0 -8
- package/src/styles/globals.css +359 -71
- package/src/lib/prototypes/design-exploration-default-storage.ts +0 -68
- package/src/lib/prototypes/design-exploration-default.ts +0 -51
- package/src/lib/prototypes/use-design-exploration-default.ts +0 -114
- 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
|
-
/**
|
|
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
|
|
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
|
-
"
|
|
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).
|
|
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<
|
|
216
|
-
|
|
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
|
+
}
|