proto-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/AGENTS.md +350 -0
  2. package/README.md +279 -0
  3. package/components.json +18 -0
  4. package/package.json +73 -0
  5. package/scripts/download-mobbin-reference-images.mjs +112 -0
  6. package/scripts/lib/host-config.example.sh +48 -0
  7. package/scripts/lib/host-config.sh +25 -0
  8. package/scripts/lib/resolve-host-root.mjs +18 -0
  9. package/scripts/lib/source-path.sh +49 -0
  10. package/scripts/link-source-db.sh +128 -0
  11. package/scripts/link-source.sh +40 -0
  12. package/scripts/publish-package.sh +147 -0
  13. package/scripts/share-prototype.sh +279 -0
  14. package/scripts/sync-from-source.sh +74 -0
  15. package/scripts/verify-prototype-component-ids.mjs +151 -0
  16. package/scripts/verify-prototype-preview-states.mjs +135 -0
  17. package/src/app.ts +4 -0
  18. package/src/components/platform-ui/code-block.tsx +21 -0
  19. package/src/components/platform-ui/controls-panel-options.tsx +71 -0
  20. package/src/components/platform-ui/controls-panel-select.tsx +438 -0
  21. package/src/components/platform-ui/empty-state.tsx +15 -0
  22. package/src/components/platform-ui/icon-button.tsx +18 -0
  23. package/src/components/platform-ui/panel-select.tsx +123 -0
  24. package/src/components/platform-ui/panel.tsx +97 -0
  25. package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
  26. package/src/components/platform-ui/sidebar.tsx +293 -0
  27. package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
  28. package/src/components/platform-ui/toolbar.tsx +37 -0
  29. package/src/components/prototype-gallery-client.tsx +57 -0
  30. package/src/components/prototype-provider.tsx +20 -0
  31. package/src/components/prototypes/mini-pill-label.tsx +37 -0
  32. package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
  33. package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
  34. package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
  35. package/src/components/prototypes/prototype-control.tsx +31 -0
  36. package/src/components/prototypes/prototype-controls.tsx +553 -0
  37. package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
  38. package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
  39. package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
  40. package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
  41. package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
  42. package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
  43. package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
  44. package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
  45. package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
  46. package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
  47. package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
  48. package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
  49. package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
  50. package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
  51. package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
  52. package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
  53. package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
  54. package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
  55. package/src/components/prototypes/prototype-share-mode.tsx +206 -0
  56. package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
  57. package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
  58. package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
  59. package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
  60. package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
  61. package/src/components/prototypes/prototype-target.tsx +120 -0
  62. package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
  63. package/src/components/prototypes/prototype-tweaks.tsx +48 -0
  64. package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
  65. package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
  66. package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
  67. package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
  68. package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
  69. package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
  70. package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
  71. package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
  72. package/src/components/shell/prototype-gallery-shell.tsx +79 -0
  73. package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
  74. package/src/components/shell/prototype-shell.module.scss +194 -0
  75. package/src/components/shell/prototype-shell.tsx +160 -0
  76. package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
  77. package/src/components/ui/button.tsx +58 -0
  78. package/src/components/ui/dialog.module.scss +81 -0
  79. package/src/components/ui/dialog.tsx +127 -0
  80. package/src/components/ui/dropdown-menu.tsx +200 -0
  81. package/src/components/ui/input.tsx +22 -0
  82. package/src/components/ui/label.tsx +26 -0
  83. package/src/components/ui/option-select.tsx +81 -0
  84. package/src/components/ui/scroll-area.tsx +48 -0
  85. package/src/components/ui/separator.tsx +31 -0
  86. package/src/components/ui/slider.tsx +28 -0
  87. package/src/components/ui/switch.tsx +29 -0
  88. package/src/components/ui/tabs.tsx +55 -0
  89. package/src/components/ui/toggle-group.tsx +80 -0
  90. package/src/components/ui/tooltip.tsx +32 -0
  91. package/src/config/create-prototype-gallery-page.tsx +65 -0
  92. package/src/config/create-prototype-host-app.tsx +121 -0
  93. package/src/config/create-prototype-page.tsx +41 -0
  94. package/src/config/create-prototype-site-layout.tsx +28 -0
  95. package/src/config/define-prototype-config.ts +7 -0
  96. package/src/config/prototype-icon.tsx +25 -0
  97. package/src/config/prototype-site-layout-client.tsx +35 -0
  98. package/src/config/with-prototype.ts +20 -0
  99. package/src/index.ts +303 -0
  100. package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
  101. package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
  102. package/src/lib/pr-split/pr-split-highlight.ts +6 -0
  103. package/src/lib/pr-split/pr-split-types.ts +58 -0
  104. package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
  105. package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
  106. package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
  107. package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
  108. package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
  109. package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
  110. package/src/lib/prototype-comments/core/capture.ts +11 -0
  111. package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
  112. package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
  113. package/src/lib/prototype-comments/core/element-identification.ts +615 -0
  114. package/src/lib/prototype-comments/core/export.ts +35 -0
  115. package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
  116. package/src/lib/prototype-comments/core/identify-element.ts +76 -0
  117. package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
  118. package/src/lib/prototype-comments/core/react-detection.ts +704 -0
  119. package/src/lib/prototype-comments/core/restore.ts +7 -0
  120. package/src/lib/prototype-comments/core/screenshot.ts +428 -0
  121. package/src/lib/prototype-comments/core/source-location.ts +906 -0
  122. package/src/lib/prototype-comments/core/storage.ts +80 -0
  123. package/src/lib/prototype-comments/core/types.ts +79 -0
  124. package/src/lib/prototype-comments/core/validation.ts +17 -0
  125. package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
  126. package/src/lib/prototype-comments/index.ts +58 -0
  127. package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
  128. package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
  129. package/src/lib/prototype-comments/scss.d.ts +10 -0
  130. package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
  131. package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
  132. package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
  133. package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
  134. package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
  135. package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
  136. package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
  137. package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
  138. package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
  139. package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
  140. package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
  141. package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
  142. package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
  143. package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
  144. package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
  145. package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
  146. package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
  147. package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
  148. package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
  149. package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
  150. package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
  151. package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
  152. package/src/lib/prototypes/changelog-meta.ts +95 -0
  153. package/src/lib/prototypes/create-prototype-registry.ts +74 -0
  154. package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
  155. package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
  156. package/src/lib/prototypes/design-exploration-default.ts +51 -0
  157. package/src/lib/prototypes/design-exploration-types.ts +203 -0
  158. package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
  159. package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
  160. package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
  161. package/src/lib/prototypes/prototype-component-registry.ts +40 -0
  162. package/src/lib/prototypes/prototype-config-types.ts +34 -0
  163. package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
  164. package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
  165. package/src/lib/prototypes/prototype-review-context.tsx +905 -0
  166. package/src/lib/prototypes/prototype-review-url.ts +134 -0
  167. package/src/lib/prototypes/prototype-share-link.ts +175 -0
  168. package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
  169. package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
  170. package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
  171. package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
  172. package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
  173. package/src/lib/prototypes/reference-docs.ts +137 -0
  174. package/src/lib/prototypes/screenshot-manifest.ts +33 -0
  175. package/src/lib/prototypes/screenshot-src.ts +7 -0
  176. package/src/lib/prototypes/share-command.ts +8 -0
  177. package/src/lib/prototypes/state-map-annotation.ts +70 -0
  178. package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
  179. package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
  180. package/src/lib/prototypes/use-prototype-comments.ts +63 -0
  181. package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
  182. package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
  183. package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
  184. package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
  185. package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
  186. package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
  187. package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
  188. package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
  189. package/src/lib/tool-portal.ts +33 -0
  190. package/src/lib/use-copy-to-clipboard.ts +81 -0
  191. package/src/lib/utils.ts +6 -0
  192. package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
  193. package/src/server/create-host-api-route.ts +100 -0
  194. package/src/server/create-prototype-api-routes.ts +428 -0
  195. package/src/server/gallery/create-gallery-api.ts +56 -0
  196. package/src/server/gallery/folders-route.ts +156 -0
  197. package/src/server/gallery/gallery-env.ts +39 -0
  198. package/src/server/gallery/gallery-route.ts +150 -0
  199. package/src/server/gallery/gallery-types.ts +15 -0
  200. package/src/server/gallery/image-route.ts +59 -0
  201. package/src/server/gallery/item-route.ts +92 -0
  202. package/src/server/pr-vercel-preview-route.ts +162 -0
  203. package/src/server/redis/client.ts +55 -0
  204. package/src/server/redis/prototype-changelog-meta.ts +31 -0
  205. package/src/server/redis/prototype-comments.ts +92 -0
  206. package/src/server/redis/prototype-design-exploration-default.ts +32 -0
  207. package/src/server.ts +51 -0
  208. package/src/styles/globals.css +624 -0
  209. package/tsconfig.json +19 -0
@@ -0,0 +1,443 @@
1
+ import type { ControlsPanelSelectEntry } from "@prototype/components/platform-ui/controls-panel-select";
2
+ import {
3
+ getStateCanvasNodeHeight,
4
+ PROTOTYPE_STATE_CANVAS_PADDING,
5
+ PROTOTYPE_STATE_NODE_ROW_GAP,
6
+ PROTOTYPE_STATE_NODE_WIDTH,
7
+ PROTOTYPE_STATE_SECTION_TO_NODES,
8
+ stateCanvasColumnX,
9
+ } from "@prototype/lib/prototypes/prototype-state-canvas-constants";
10
+ import type {
11
+ PrototypeStateCanvasEdge,
12
+ PrototypeStateCanvasNode,
13
+ PrototypeStateCanvasSection,
14
+ } from "@prototype/lib/prototypes/prototype-state-canvas-types";
15
+
16
+ export type PrototypePreviewStateVariant = {
17
+ id: string;
18
+ label: string;
19
+ hint?: string;
20
+ /** Planning note for this variant (shown on the state map). */
21
+ annotation?: string;
22
+ /** Maps bullet index to wireframe highlight region keys. */
23
+ highlightRegions?: readonly string[];
24
+ };
25
+
26
+ /** State entry in a preview-state registry — layout is computed from `canvasLayout`. */
27
+ export type PrototypePreviewStateDefinition<T extends string = string> = {
28
+ id: T;
29
+ label: string;
30
+ variants?: readonly PrototypePreviewStateVariant[];
31
+ separatorBefore?: boolean;
32
+ /** Planning note — what this screen should do, triggers, open questions. */
33
+ annotation?: string;
34
+ /** Maps bullet index to wireframe highlight region keys. */
35
+ highlightRegions?: readonly string[];
36
+ };
37
+
38
+ /** Resolved state with computed canvas coordinates (output of `definePreviewStateRegistry`). */
39
+ export type ResolvedPrototypePreviewStateDefinition<T extends string = string> =
40
+ PrototypePreviewStateDefinition<T> & {
41
+ layout: {
42
+ x: number;
43
+ y: number;
44
+ };
45
+ };
46
+
47
+ export type PrototypePreviewStateSection = PrototypeStateCanvasSection;
48
+
49
+ export type PrototypePreviewStateEdge<T extends string = string> = {
50
+ from: T;
51
+ to: T;
52
+ /** Dashed lines for alternate / empty-state branches. */
53
+ dashed?: boolean;
54
+ };
55
+
56
+ /** State id, or `{ id, column }` when a row skips leading columns. */
57
+ export type PreviewStateCanvasRowStatePlacement<T extends string = string> =
58
+ | T
59
+ | {
60
+ id: T;
61
+ column: number;
62
+ };
63
+
64
+ export type PreviewStateCanvasRowPlacement<T extends string = string> = {
65
+ section?: {
66
+ label: string;
67
+ annotation?: string;
68
+ };
69
+ /** First column index when `states` is a list of ids (default `0`). */
70
+ startColumn?: number;
71
+ states: readonly PreviewStateCanvasRowStatePlacement<T>[];
72
+ };
73
+
74
+ /**
75
+ * Grid placement for state map nodes. Layout is computed from `rows`; do not set x/y manually.
76
+ *
77
+ * **Edge routing:** Connectors draw straight or elbow paths between node anchors and do not
78
+ * route around intervening nodes. When two states are linked by an `edges` entry, place them
79
+ * so no other state sits on the path between them — e.g. adjacent columns on the same row, or
80
+ * offset columns so vertical edges do not pass through unrelated cards in the same column.
81
+ */
82
+ export type PrototypePreviewStateCanvasLayoutSpec<
83
+ T extends string = string,
84
+ > = {
85
+ rows: readonly PreviewStateCanvasRowPlacement<T>[];
86
+ options?: {
87
+ sectionToNodes?: number;
88
+ rowGap?: number;
89
+ canvasPadding?: number;
90
+ bottomPadding?: number;
91
+ sectionLabelOffset?: number;
92
+ };
93
+ };
94
+
95
+ export type PreviewStateCanvasLayout = {
96
+ layouts: Map<string, { x: number; y: number }>;
97
+ sections: PrototypeStateCanvasSection[];
98
+ canvasWidth: number;
99
+ canvasHeight: number;
100
+ };
101
+
102
+ export type PrototypePreviewStateRegistry<
103
+ TPrimary extends string = string,
104
+ TVariant extends string = never,
105
+ > = {
106
+ canvasLayout: PrototypePreviewStateCanvasLayoutSpec<TPrimary>;
107
+ states: readonly PrototypePreviewStateDefinition<TPrimary>[];
108
+ edges?: readonly PrototypePreviewStateEdge<TPrimary | TVariant>[];
109
+ };
110
+
111
+ export type DefinedPreviewStateRegistry<
112
+ TPrimary extends string,
113
+ TVariant extends string = never,
114
+ > = Omit<PrototypePreviewStateRegistry<TPrimary, TVariant>, "states"> & {
115
+ states: readonly ResolvedPrototypePreviewStateDefinition<TPrimary>[];
116
+ pickerOptions: ControlsPanelSelectEntry<TPrimary>[];
117
+ canvasNodes: PrototypeStateCanvasNode<TPrimary | TVariant>[];
118
+ canvasEdges: PrototypeStateCanvasEdge<TPrimary | TVariant>[];
119
+ canvasSections: PrototypeStateCanvasSection[];
120
+ canvasSize: { width: number; height: number };
121
+ previewStates: Array<{ id: string; label: string }>;
122
+ };
123
+
124
+ export function normalizePreviewStateCanvasRowStates<T extends string>(
125
+ states: readonly PreviewStateCanvasRowStatePlacement<T>[],
126
+ startColumn = 0,
127
+ ): Array<{ id: T; column: number }> {
128
+ return states.map((entry, index) =>
129
+ typeof entry === "string"
130
+ ? { id: entry, column: startColumn + index }
131
+ : entry,
132
+ );
133
+ }
134
+
135
+ export function getPreviewStateNodeHeight(
136
+ state: Pick<PrototypePreviewStateDefinition, "variants">,
137
+ ): number {
138
+ return getStateCanvasNodeHeight(state.variants);
139
+ }
140
+
141
+ /** Stack canvas rows using each row's tallest node (including nested states). */
142
+ export function buildPreviewStateCanvasLayout<T extends string>(
143
+ rows: readonly PreviewStateCanvasRowPlacement<T>[],
144
+ stateDefinitions: ReadonlyArray<
145
+ Pick<PrototypePreviewStateDefinition<T>, "id" | "variants">
146
+ >,
147
+ options?: PrototypePreviewStateCanvasLayoutSpec<T>["options"],
148
+ ): PreviewStateCanvasLayout {
149
+ const sectionToNodes =
150
+ options?.sectionToNodes ?? PROTOTYPE_STATE_SECTION_TO_NODES;
151
+ const rowGap = options?.rowGap ?? PROTOTYPE_STATE_NODE_ROW_GAP;
152
+ const canvasPadding = options?.canvasPadding ?? PROTOTYPE_STATE_CANVAS_PADDING;
153
+ const bottomPadding = options?.bottomPadding ?? canvasPadding;
154
+ const sectionLabelOffset = options?.sectionLabelOffset ?? 8;
155
+
156
+ const stateById = new Map(
157
+ stateDefinitions.map((state) => [state.id, state]),
158
+ );
159
+ const layouts = new Map<string, { x: number; y: number }>();
160
+ const sections: PrototypeStateCanvasSection[] = [];
161
+
162
+ let cursorY = canvasPadding - sectionLabelOffset;
163
+ let maxColumn = 0;
164
+ let maxBottom = 0;
165
+
166
+ for (const row of rows) {
167
+ let nodesY = cursorY;
168
+ const rowStates = normalizePreviewStateCanvasRowStates(
169
+ row.states,
170
+ row.startColumn ?? 0,
171
+ );
172
+
173
+ if (row.section) {
174
+ sections.push({
175
+ label: row.section.label,
176
+ y: cursorY,
177
+ annotation: row.section.annotation,
178
+ });
179
+ nodesY = cursorY + sectionToNodes;
180
+ }
181
+
182
+ let rowMaxHeight = 0;
183
+
184
+ for (const placement of rowStates) {
185
+ const state = stateById.get(placement.id);
186
+ if (!state) {
187
+ throw new Error(
188
+ `Preview state canvas layout: unknown state id "${placement.id}".`,
189
+ );
190
+ }
191
+
192
+ const height = getPreviewStateNodeHeight(state);
193
+ rowMaxHeight = Math.max(rowMaxHeight, height);
194
+ maxColumn = Math.max(maxColumn, placement.column);
195
+ layouts.set(placement.id, {
196
+ x: stateCanvasColumnX(placement.column),
197
+ y: nodesY,
198
+ });
199
+ }
200
+
201
+ const rowBottom = nodesY + rowMaxHeight;
202
+ maxBottom = Math.max(maxBottom, rowBottom);
203
+ cursorY = rowBottom + rowGap;
204
+ }
205
+
206
+ return {
207
+ layouts,
208
+ sections,
209
+ canvasWidth:
210
+ stateCanvasColumnX(maxColumn) + PROTOTYPE_STATE_NODE_WIDTH + canvasPadding,
211
+ canvasHeight: maxBottom + bottomPadding,
212
+ };
213
+ }
214
+
215
+ function resolvePreviewStateRegistryLayout<TPrimary extends string>(
216
+ registry: PrototypePreviewStateRegistry<TPrimary>,
217
+ ): {
218
+ states: ResolvedPrototypePreviewStateDefinition<TPrimary>[];
219
+ sections: PrototypeStateCanvasSection[];
220
+ canvasSize: { width: number; height: number };
221
+ } {
222
+ const layout = buildPreviewStateCanvasLayout(
223
+ registry.canvasLayout.rows,
224
+ registry.states,
225
+ registry.canvasLayout.options,
226
+ );
227
+
228
+ const placedIds = new Set(layout.layouts.keys());
229
+
230
+ for (const state of registry.states) {
231
+ if (!placedIds.has(state.id)) {
232
+ throw new Error(
233
+ `Preview state registry: state "${state.id}" is missing from canvasLayout.`,
234
+ );
235
+ }
236
+ }
237
+
238
+ for (const placedId of placedIds) {
239
+ if (!registry.states.some((state) => state.id === placedId)) {
240
+ throw new Error(
241
+ `Preview state registry: canvasLayout references unknown state "${placedId}".`,
242
+ );
243
+ }
244
+ }
245
+
246
+ return {
247
+ states: registry.states.map((state) => ({
248
+ ...state,
249
+ layout: layout.layouts.get(state.id)!,
250
+ })),
251
+ sections: layout.sections,
252
+ canvasSize: {
253
+ width: layout.canvasWidth,
254
+ height: layout.canvasHeight,
255
+ },
256
+ };
257
+ }
258
+
259
+ export function definePreviewStateRegistry<
260
+ TPrimary extends string,
261
+ TVariant extends string = never,
262
+ >(
263
+ registry: PrototypePreviewStateRegistry<TPrimary, TVariant>,
264
+ ): DefinedPreviewStateRegistry<TPrimary, TVariant> {
265
+ const resolved = resolvePreviewStateRegistryLayout(registry);
266
+
267
+ const pickerOptions = buildPreviewStatePickerOptions(registry);
268
+ const canvasNodes = buildPreviewStateCanvasNodes(resolved.states);
269
+ const canvasEdges = buildPreviewStateCanvasEdges(registry);
270
+ const previewStates = flattenPreviewStateLabels(registry);
271
+
272
+ assertPreviewStateRegistryParity(registry, resolved.states, {
273
+ pickerOptions,
274
+ canvasNodes,
275
+ });
276
+
277
+ return {
278
+ ...registry,
279
+ states: resolved.states,
280
+ pickerOptions,
281
+ canvasNodes,
282
+ canvasEdges,
283
+ canvasSections: resolved.sections,
284
+ canvasSize: resolved.canvasSize,
285
+ previewStates,
286
+ };
287
+ }
288
+
289
+ export function buildPreviewStatePickerOptions<TPrimary extends string>(
290
+ registry: Pick<PrototypePreviewStateRegistry<TPrimary>, "states">,
291
+ ): ControlsPanelSelectEntry<TPrimary>[] {
292
+ const options: ControlsPanelSelectEntry<TPrimary>[] = [];
293
+
294
+ for (const state of registry.states) {
295
+ if (state.separatorBefore) {
296
+ options.push({ type: "separator" });
297
+ }
298
+
299
+ if (state.variants && state.variants.length > 0) {
300
+ options.push({
301
+ type: "submenu",
302
+ value: state.id,
303
+ label: state.label,
304
+ options: state.variants.map((variant) => ({
305
+ value: variant.id,
306
+ label: variant.label,
307
+ })),
308
+ });
309
+ continue;
310
+ }
311
+
312
+ options.push({
313
+ value: state.id,
314
+ label: state.label,
315
+ });
316
+ }
317
+
318
+ return options;
319
+ }
320
+
321
+ export function buildPreviewStateCanvasNodes<
322
+ TPrimary extends string,
323
+ TVariant extends string = never,
324
+ >(
325
+ states: readonly ResolvedPrototypePreviewStateDefinition<TPrimary>[],
326
+ ): PrototypeStateCanvasNode<TPrimary | TVariant>[] {
327
+ return states.map((state) => ({
328
+ id: state.id,
329
+ label: state.label,
330
+ x: state.layout.x,
331
+ y: state.layout.y,
332
+ annotation: state.annotation,
333
+ highlightRegions: state.highlightRegions,
334
+ callouts: state.variants?.map((variant) => ({
335
+ id: variant.id as TPrimary | TVariant,
336
+ label: variant.label,
337
+ hint: variant.hint,
338
+ annotation: variant.annotation,
339
+ highlightRegions: variant.highlightRegions,
340
+ })),
341
+ }));
342
+ }
343
+
344
+ export function buildPreviewStateCanvasEdges<
345
+ TPrimary extends string,
346
+ TVariant extends string = never,
347
+ >(
348
+ registry: Pick<PrototypePreviewStateRegistry<TPrimary, TVariant>, "edges">,
349
+ ): PrototypeStateCanvasEdge<TPrimary | TVariant>[] {
350
+ return [...(registry.edges ?? [])];
351
+ }
352
+
353
+ export function flattenPreviewStateLabels<TPrimary extends string>(
354
+ registry: Pick<PrototypePreviewStateRegistry<TPrimary>, "states">,
355
+ ): Array<{ id: string; label: string }> {
356
+ const states: Array<{ id: string; label: string }> = [];
357
+
358
+ for (const state of registry.states) {
359
+ states.push({ id: state.id, label: state.label });
360
+ for (const variant of state.variants ?? []) {
361
+ states.push({ id: variant.id, label: variant.label });
362
+ }
363
+ }
364
+
365
+ return states;
366
+ }
367
+
368
+ function assertPreviewStateRegistryParity<TPrimary extends string>(
369
+ registry: Pick<PrototypePreviewStateRegistry<TPrimary>, "states">,
370
+ resolvedStates: readonly ResolvedPrototypePreviewStateDefinition<TPrimary>[],
371
+ derived: {
372
+ pickerOptions: ControlsPanelSelectEntry<TPrimary>[];
373
+ canvasNodes: PrototypeStateCanvasNode<TPrimary>[];
374
+ },
375
+ ): void {
376
+ if (registry.states.length !== derived.canvasNodes.length) {
377
+ throw new Error(
378
+ `Preview state registry mismatch: ${registry.states.length} definitions but ${derived.canvasNodes.length} canvas nodes.`,
379
+ );
380
+ }
381
+
382
+ for (let index = 0; index < registry.states.length; index += 1) {
383
+ const state = registry.states[index]!;
384
+ const node = derived.canvasNodes[index]!;
385
+ const resolved = resolvedStates[index]!;
386
+
387
+ if (state.id !== node.id) {
388
+ throw new Error(
389
+ `Preview state registry mismatch at index ${index}: definition id "${state.id}" !== canvas node id "${node.id}".`,
390
+ );
391
+ }
392
+
393
+ if (state.label !== node.label) {
394
+ throw new Error(
395
+ `Preview state registry mismatch for "${state.id}": picker label "${state.label}" !== canvas label "${node.label}".`,
396
+ );
397
+ }
398
+
399
+ if (
400
+ resolved.layout.x !== node.x ||
401
+ resolved.layout.y !== node.y
402
+ ) {
403
+ throw new Error(
404
+ `Preview state registry mismatch for "${state.id}": resolved layout does not match canvas node position.`,
405
+ );
406
+ }
407
+
408
+ const variantCount = state.variants?.length ?? 0;
409
+ const calloutCount = node.callouts?.length ?? 0;
410
+ if (variantCount !== calloutCount) {
411
+ throw new Error(
412
+ `Preview state registry mismatch for "${state.id}": ${variantCount} picker variants !== ${calloutCount} canvas callouts.`,
413
+ );
414
+ }
415
+
416
+ for (let variantIndex = 0; variantIndex < variantCount; variantIndex += 1) {
417
+ const variant = state.variants![variantIndex]!;
418
+ const callout = node.callouts![variantIndex]!;
419
+
420
+ if (variant.id !== callout.id) {
421
+ throw new Error(
422
+ `Preview state registry mismatch for "${state.id}" variant ${variantIndex}: picker id "${variant.id}" !== canvas callout id "${callout.id}".`,
423
+ );
424
+ }
425
+
426
+ if (variant.label !== callout.label) {
427
+ throw new Error(
428
+ `Preview state registry mismatch for "${state.id}" variant "${variant.id}": picker label "${variant.label}" !== canvas callout label "${callout.label}".`,
429
+ );
430
+ }
431
+ }
432
+ }
433
+
434
+ const pickerStateCount = derived.pickerOptions.filter(
435
+ (entry) => !("type" in entry && entry.type === "separator"),
436
+ ).length;
437
+
438
+ if (pickerStateCount !== registry.states.length) {
439
+ throw new Error(
440
+ `Preview state registry mismatch: ${registry.states.length} definitions but ${pickerStateCount} picker entries.`,
441
+ );
442
+ }
443
+ }
@@ -0,0 +1,89 @@
1
+ export type PrototypePreviewState = {
2
+ id: string;
3
+ label: string;
4
+ };
5
+
6
+ export function buildCreatePreviewStatePrompt({
7
+ slug,
8
+ existingStates = [],
9
+ origin = "http://localhost:3003",
10
+ briefText,
11
+ }: {
12
+ slug: string;
13
+ existingStates?: PrototypePreviewState[];
14
+ origin?: string;
15
+ briefText?: string;
16
+ }): string {
17
+ const prototypeUrl = `${origin}/prototypes/${slug}`;
18
+ const registryFile = `src/prototypes/${slug}/**/*-preview-states.ts`;
19
+ const trimmedBrief = briefText?.trim() ?? "";
20
+
21
+ const lines = [
22
+ `Add a new preview state to the "${slug}" prototype.`,
23
+ "",
24
+ "## Brief",
25
+ ];
26
+
27
+ if (trimmedBrief) {
28
+ lines.push(trimmedBrief);
29
+ } else {
30
+ lines.push(
31
+ "Describe the UI state to add:",
32
+ "- State id (kebab-case):",
33
+ "- Label (shown in the states menu):",
34
+ "- What should the page show:",
35
+ );
36
+ }
37
+
38
+ if (existingStates.length > 0) {
39
+ lines.push("", "## Existing states on this prototype");
40
+ for (const state of existingStates) {
41
+ lines.push(`- ${state.label} (\`${state.id}\`)`);
42
+ }
43
+ }
44
+
45
+ lines.push(
46
+ "",
47
+ "## Prototype",
48
+ `- Slug: \`${slug}\``,
49
+ `- Preview: ${prototypeUrl}`,
50
+ "",
51
+ "## Instructions",
52
+ "Add a preview state on this prototype page — do not register a new prototype slug.",
53
+ "",
54
+ "1. Add the state to `${registryFile}` using `definePreviewStateRegistry` — one entry drives both the state picker and state map. Place it in `canvasLayout.rows` (no manual x/y); nested options go in `variants`. When adding or moving states, keep `edges` readable: connected states must not share a column (or sit on the line) with an unrelated state between them — edge paths do not route around intervening nodes.",
55
+ "2. Extend the preview mode / live-state type if needed (e.g. in `use-mock-*-data.ts` or the page module).",
56
+ "3. Wire mock data / fixtures so the new mode renders the correct UI.",
57
+ "4. For nested options (e.g. billing plans), add a `variants` array on the registry entry — it becomes a picker submenu and map callouts automatically.",
58
+ "5. Sync URL params / liveState restore if the page persists preview mode in the query string.",
59
+ "6. Open the prototype in dev — `PrototypeStateScreenshotCapture` saves a thumbnail under `public/prototypes/screenshots/<slug>/states/`.",
60
+ "7. Run `pnpm verify:prototype-ids` if you add new UI components.",
61
+ "8. Run `pnpm verify:prototype-preview-states` to confirm picker/map parity.",
62
+ "",
63
+ "Reference: `src/prototypes/example-feature/_components/list-preview-states.ts`.",
64
+ );
65
+
66
+ return lines.join("\n");
67
+ }
68
+
69
+ export function buildCreatePreviewStateCopyText({
70
+ slug,
71
+ existingStates = [],
72
+ origin = "http://localhost:3003",
73
+ briefText,
74
+ }: {
75
+ slug: string;
76
+ existingStates?: PrototypePreviewState[];
77
+ origin?: string;
78
+ briefText: string;
79
+ }): string {
80
+ const trimmedBrief = briefText.trim();
81
+ const prompt = buildCreatePreviewStatePrompt({
82
+ slug,
83
+ existingStates,
84
+ origin,
85
+ briefText: trimmedBrief,
86
+ });
87
+
88
+ return `${trimmedBrief}\n\n---\n\n${prompt}`;
89
+ }