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,203 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type DesignExplorationVariantOption<TVariant extends string> = {
4
+ value: TVariant;
5
+ label: string;
6
+ hint?: string;
7
+ };
8
+
9
+ export type DesignExplorationRationale = {
10
+ good: string;
11
+ bad: string;
12
+ };
13
+
14
+ export type DesignExplorationBrief = {
15
+ titleDefault: string;
16
+ descriptionDefault: string;
17
+ };
18
+
19
+ /** Open-ended context — prototype defines the JSON shape. */
20
+ export type DesignExplorationContext = {
21
+ label: string;
22
+ data: unknown;
23
+ render?: (data: unknown) => ReactNode;
24
+ defaultExpanded?: boolean;
25
+ /** Stable id suffix for aria-controls on the collapsible panel. */
26
+ panelId?: string;
27
+ };
28
+
29
+ export type MobbinReference = {
30
+ id: string;
31
+ appName: string;
32
+ imageUrl: string;
33
+ mobbinUrl: string;
34
+ relevance: string;
35
+ variantHint?: string;
36
+ };
37
+
38
+ export type DesignExplorationMobbin = {
39
+ references: MobbinReference[];
40
+ title?: string;
41
+ description?: string;
42
+ imagePathForReference?: (id: string) => string;
43
+ };
44
+
45
+ export type DesignExplorationVariantsSection = {
46
+ title?: string;
47
+ description?: string;
48
+ };
49
+
50
+ /** Persists to KV via `storageKeyPrefix` — use `useDesignExplorationDefault`. */
51
+ export type DesignExplorationConfig<TVariant extends string> = {
52
+ componentIdPrefix: string;
53
+ variantTabsIdPrefix: string;
54
+ storageKeyPrefix: string;
55
+ variant: TVariant;
56
+ onVariantChange: (variant: TVariant) => void;
57
+ options: DesignExplorationVariantOption<TVariant>[];
58
+ rationale: Record<TVariant, DesignExplorationRationale>;
59
+ renderers: Record<TVariant, () => ReactNode>;
60
+ brief: DesignExplorationBrief;
61
+ context?: DesignExplorationContext;
62
+ mobbin?: DesignExplorationMobbin;
63
+ variantsSection?: DesignExplorationVariantsSection;
64
+ /** Label for prev/next variant tab buttons. Default: "variant". */
65
+ variantTabAriaLabel?: string;
66
+ /** Path to design exploration config — included in reset-brief copy prompt. */
67
+ briefConfigFilePath?: string;
68
+ };
69
+
70
+ /** Plain-text copy of the overview fields as shown in the design brief modal. */
71
+ export function buildBriefOverviewCopyText(
72
+ title: string,
73
+ description: string,
74
+ ): string {
75
+ const trimmedTitle = title.trim();
76
+ const trimmedDescription = description.trim();
77
+ if (!trimmedTitle) return trimmedDescription;
78
+ if (!trimmedDescription) return trimmedTitle;
79
+ return `${trimmedTitle}\n\n${trimmedDescription}`;
80
+ }
81
+
82
+ export function buildCreateDesignExplorationPrompt({
83
+ slug,
84
+ existingExplorations = [],
85
+ origin = "http://localhost:3003",
86
+ briefText,
87
+ }: {
88
+ slug: string;
89
+ existingExplorations?: Array<{ id: string; label: string }>;
90
+ origin?: string;
91
+ briefText?: string;
92
+ }): string {
93
+ const prototypeUrl = `${origin}/prototypes/${slug}`;
94
+ const variantSetsRegistrar = `src/prototypes/${slug}/_components/*-variant-sets.tsx`;
95
+ const trimmedBrief = briefText?.trim() ?? "";
96
+
97
+ const lines = [
98
+ `Add a new design exploration to the "${slug}" prototype.`,
99
+ "",
100
+ "## Brief",
101
+ ];
102
+
103
+ if (trimmedBrief) {
104
+ lines.push(trimmedBrief);
105
+ } else {
106
+ lines.push(
107
+ "Describe the UI problem to explore and the 2+ directions to compare:",
108
+ "- Problem:",
109
+ "- Variants to explore:",
110
+ );
111
+ }
112
+
113
+ if (existingExplorations.length > 0) {
114
+ lines.push("", "## Existing explorations on this prototype");
115
+ for (const exploration of existingExplorations) {
116
+ lines.push(`- ${exploration.label} (\`${exploration.id}\`)`);
117
+ }
118
+ }
119
+
120
+ lines.push(
121
+ "",
122
+ "## Prototype",
123
+ `- Slug: \`${slug}\``,
124
+ `- Preview: ${prototypeUrl}`,
125
+ "",
126
+ "## Instructions",
127
+ "Follow AGENTS.md → Design exploration. Do not register a new prototype slug — add a design exploration variant set on this page.",
128
+ "",
129
+ "1. Add `{ id, label, previewMode? }` to the variant sets registrar (e.g. `" +
130
+ variantSetsRegistrar +
131
+ "`).",
132
+ "2. Create `*-design-exploration-config.tsx` with options, rationale, renderers, and brief defaults.",
133
+ "3. Create `*-variant-toggle.tsx` mounting `PrototypeVariantExplorer` with matching `variantSet={{ id, label }}`.",
134
+ "4. Register static ids + any `dynamicPrefixes` in `src/prototypes/" +
135
+ slug +
136
+ "/component-ids.ts`.",
137
+ "5. If the exploration needs a specific page state, wire preview mode in mock data / URL params and render the toggle on the page.",
138
+ "6. Prepend new variant options to the top of `*_VARIANT_OPTIONS` so newest explorations appear first.",
139
+ "7. Run `pnpm verify:prototype-ids`.",
140
+ "",
141
+ "Reference: `src/prototypes/example-feature/_components/no-data-design-exploration-config.tsx` + `no-data-variant-toggle.tsx`.",
142
+ );
143
+
144
+ return lines.join("\n");
145
+ }
146
+
147
+ export function buildCreateDesignExplorationCopyText({
148
+ slug,
149
+ existingExplorations = [],
150
+ origin = "http://localhost:3003",
151
+ briefText,
152
+ }: {
153
+ slug: string;
154
+ existingExplorations?: Array<{ id: string; label: string }>;
155
+ origin?: string;
156
+ briefText: string;
157
+ }): string {
158
+ const trimmedBrief = briefText.trim();
159
+ const prompt = buildCreateDesignExplorationPrompt({
160
+ slug,
161
+ existingExplorations,
162
+ origin,
163
+ briefText: trimmedBrief,
164
+ });
165
+
166
+ return `${trimmedBrief}\n\n---\n\n${prompt}`;
167
+ }
168
+
169
+ export function buildResetBriefDefaultsPrompt({
170
+ brief,
171
+ configFilePath,
172
+ }: {
173
+ brief: DesignExplorationBrief;
174
+ configFilePath?: string;
175
+ }): string {
176
+ const target = configFilePath ?? "the design exploration config file";
177
+
178
+ return [
179
+ "Update the design exploration overview defaults.",
180
+ "",
181
+ `In ${target}, set brief.titleDefault and brief.descriptionDefault to:`,
182
+ "",
183
+ `Title: ${brief.titleDefault}`,
184
+ "",
185
+ "Description:",
186
+ brief.descriptionDefault,
187
+ ].join("\n");
188
+ }
189
+
190
+ /**
191
+ * 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.
193
+ */
194
+ export function getAdjacentDesignExplorationVariant<TVariant extends string>(
195
+ options: DesignExplorationVariantOption<TVariant>[],
196
+ current: TVariant,
197
+ direction: "prev" | "next",
198
+ ): TVariant {
199
+ const index = options.findIndex((option) => option.value === current);
200
+ const offset = direction === "prev" ? -1 : 1;
201
+ const nextIndex = (index + offset + options.length) % options.length;
202
+ return options[nextIndex].value;
203
+ }
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useMemo,
8
+ useRef,
9
+ type ReactNode,
10
+ } from "react";
11
+
12
+ import type { AnnotationTargetOptions } from "@prototype/lib/prototype-comments/core/annotation-target";
13
+
14
+ import {
15
+ augmentLiveStateWithReviewState,
16
+ restoreLiveStateWithReviewState,
17
+ type PrototypeCommentReviewStateBridge,
18
+ } from "./prototype-comment-review-state";
19
+
20
+ export type PrototypeCommentHandlers = {
21
+ getLiveState: () => unknown;
22
+ onRestore: (live: unknown) => void;
23
+ resolveTargetOptions?: () => AnnotationTargetOptions;
24
+ };
25
+
26
+ type PrototypeCommentRegistryValue = {
27
+ register: (handlers: PrototypeCommentHandlers) => void;
28
+ unregister: (handlers: PrototypeCommentHandlers) => void;
29
+ registerReviewStateBridge: (
30
+ bridge: PrototypeCommentReviewStateBridge | null,
31
+ ) => void;
32
+ readHandlers: () => PrototypeCommentHandlers | null;
33
+ /** Notify subscribers that the prototype's live state changed. */
34
+ notifyLiveStateChange: () => void;
35
+ /** Subscribe to live-state changes. Returns an unsubscribe function. */
36
+ subscribeLiveStateChange: (listener: () => void) => () => void;
37
+ };
38
+
39
+ const PrototypeCommentRegistryContext =
40
+ createContext<PrototypeCommentRegistryValue | null>(null);
41
+
42
+ export function PrototypeCommentRegistryProvider({
43
+ children,
44
+ }: {
45
+ children: ReactNode;
46
+ }) {
47
+ const handlersRef = useRef<PrototypeCommentHandlers | null>(null);
48
+ const reviewBridgeRef = useRef<PrototypeCommentReviewStateBridge | null>(
49
+ null,
50
+ );
51
+ const liveStateListenersRef = useRef<Set<() => void>>(new Set());
52
+
53
+ const notifyLiveStateChange = useCallback(() => {
54
+ liveStateListenersRef.current.forEach((listener) => listener());
55
+ }, []);
56
+
57
+ const subscribeLiveStateChange = useCallback((listener: () => void) => {
58
+ liveStateListenersRef.current.add(listener);
59
+ return () => {
60
+ liveStateListenersRef.current.delete(listener);
61
+ };
62
+ }, []);
63
+
64
+ const register = useCallback((handlers: PrototypeCommentHandlers) => {
65
+ handlersRef.current = handlers;
66
+ }, []);
67
+
68
+ const unregister = useCallback((handlers: PrototypeCommentHandlers) => {
69
+ if (handlersRef.current === handlers) {
70
+ handlersRef.current = null;
71
+ }
72
+ }, []);
73
+
74
+ const registerReviewStateBridge = useCallback(
75
+ (bridge: PrototypeCommentReviewStateBridge | null) => {
76
+ reviewBridgeRef.current = bridge;
77
+ },
78
+ [],
79
+ );
80
+
81
+ const readHandlers = useCallback((): PrototypeCommentHandlers | null => {
82
+ const prototypeHandlers = handlersRef.current;
83
+ if (!prototypeHandlers) return null;
84
+
85
+ const bridge = reviewBridgeRef.current;
86
+ if (!bridge?.prodReferenceAvailable) {
87
+ return prototypeHandlers;
88
+ }
89
+
90
+ return {
91
+ getLiveState: () =>
92
+ augmentLiveStateWithReviewState(
93
+ prototypeHandlers.getLiveState(),
94
+ bridge.getShowProdReference(),
95
+ ),
96
+ onRestore: (live) => {
97
+ restoreLiveStateWithReviewState(
98
+ live,
99
+ prototypeHandlers.onRestore,
100
+ bridge,
101
+ );
102
+ },
103
+ ...(prototypeHandlers.resolveTargetOptions
104
+ ? {
105
+ resolveTargetOptions: prototypeHandlers.resolveTargetOptions,
106
+ }
107
+ : {}),
108
+ };
109
+ }, []);
110
+
111
+ const value = useMemo(
112
+ () => ({
113
+ register,
114
+ unregister,
115
+ registerReviewStateBridge,
116
+ readHandlers,
117
+ notifyLiveStateChange,
118
+ subscribeLiveStateChange,
119
+ }),
120
+ [
121
+ register,
122
+ unregister,
123
+ registerReviewStateBridge,
124
+ readHandlers,
125
+ notifyLiveStateChange,
126
+ subscribeLiveStateChange,
127
+ ],
128
+ );
129
+
130
+ return (
131
+ <PrototypeCommentRegistryContext.Provider value={value}>
132
+ {children}
133
+ </PrototypeCommentRegistryContext.Provider>
134
+ );
135
+ }
136
+
137
+ export function usePrototypeCommentRegistry(): PrototypeCommentRegistryValue {
138
+ const context = useContext(PrototypeCommentRegistryContext);
139
+ if (!context) {
140
+ throw new Error(
141
+ "usePrototypeCommentRegistry must be used within PrototypeCommentRegistryProvider",
142
+ );
143
+ }
144
+ return context;
145
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { useLayoutEffect, useRef } from "react";
4
+
5
+ import { usePrototypeReviewOptional } from "./prototype-review-context";
6
+ import { usePrototypeCommentRegistry } from "./prototype-comment-registry";
7
+ import type { PrototypeCommentReviewStateBridge } from "./prototype-comment-review-state";
8
+
9
+ /** Registers prod-reference toggle state with the comment capture/restore pipeline. */
10
+ export function PrototypeCommentReviewBridge() {
11
+ const review = usePrototypeReviewOptional();
12
+ const { registerReviewStateBridge } = usePrototypeCommentRegistry();
13
+ const reviewRef = useRef(review);
14
+ reviewRef.current = review;
15
+
16
+ useLayoutEffect(() => {
17
+ const currentReview = reviewRef.current;
18
+ if (!currentReview?.prodReferenceAvailable) {
19
+ registerReviewStateBridge(null);
20
+ return;
21
+ }
22
+
23
+ const bridge: PrototypeCommentReviewStateBridge = {
24
+ prodReferenceAvailable: true,
25
+ getShowProdReference: () => reviewRef.current?.showProdReference ?? false,
26
+ setShowProdReference: (show) => {
27
+ reviewRef.current?.setShowProdReference(show);
28
+ },
29
+ };
30
+
31
+ registerReviewStateBridge(bridge);
32
+ return () => registerReviewStateBridge(null);
33
+ }, [
34
+ registerReviewStateBridge,
35
+ review?.prodReferenceAvailable,
36
+ review?.setShowProdReference,
37
+ ]);
38
+
39
+ return null;
40
+ }
@@ -0,0 +1,57 @@
1
+ export const COMMENT_SHOW_PROD_REFERENCE_KEY = "showProdReference";
2
+
3
+ export type PrototypeCommentReviewStateBridge = {
4
+ prodReferenceAvailable: boolean;
5
+ getShowProdReference: () => boolean;
6
+ setShowProdReference: (show: boolean) => void;
7
+ };
8
+
9
+ export function augmentLiveStateWithReviewState(
10
+ live: unknown,
11
+ showProdReference: boolean,
12
+ ): Record<string, unknown> {
13
+ return {
14
+ ...(typeof live === "object" && live !== null
15
+ ? (live as Record<string, unknown>)
16
+ : {}),
17
+ [COMMENT_SHOW_PROD_REFERENCE_KEY]: showProdReference,
18
+ };
19
+ }
20
+
21
+ function splitPrototypeLiveState(live: unknown): {
22
+ showProdReference: boolean;
23
+ prototypeLive: unknown;
24
+ } {
25
+ if (typeof live !== "object" || live === null) {
26
+ return { showProdReference: false, prototypeLive: live };
27
+ }
28
+
29
+ const state = live as Record<string, unknown>;
30
+ const showProdReference = state[COMMENT_SHOW_PROD_REFERENCE_KEY] === true;
31
+ const { [COMMENT_SHOW_PROD_REFERENCE_KEY]: _removed, ...prototypeLive } =
32
+ state;
33
+
34
+ return { showProdReference, prototypeLive };
35
+ }
36
+
37
+ export function restoreLiveStateWithReviewState(
38
+ live: unknown,
39
+ onRestorePrototypeLive: (live: unknown) => void,
40
+ bridge: PrototypeCommentReviewStateBridge | null,
41
+ ): void {
42
+ const { showProdReference, prototypeLive } = splitPrototypeLiveState(live);
43
+
44
+ if (!bridge?.prodReferenceAvailable) {
45
+ onRestorePrototypeLive(prototypeLive);
46
+ return;
47
+ }
48
+
49
+ if (showProdReference) {
50
+ onRestorePrototypeLive(prototypeLive);
51
+ bridge.setShowProdReference(true);
52
+ return;
53
+ }
54
+
55
+ bridge.setShowProdReference(false);
56
+ onRestorePrototypeLive(prototypeLive);
57
+ }
@@ -0,0 +1,40 @@
1
+ export type PrototypeComponentRegistry = {
2
+ /** Static segment ids (slug is prefixed at runtime). */
3
+ ids: readonly string[];
4
+ /** Allowed dynamic id prefixes, e.g. `mock-table.row.` */
5
+ dynamicPrefixes?: readonly string[];
6
+ };
7
+
8
+ const registries: Record<string, PrototypeComponentRegistry> = {};
9
+
10
+ export function registerPrototypeComponentRegistry(
11
+ slug: string,
12
+ registry: PrototypeComponentRegistry,
13
+ ) {
14
+ registries[slug] = registry;
15
+ }
16
+
17
+ export function getPrototypeComponentRegistry(
18
+ slug: string,
19
+ ): PrototypeComponentRegistry | undefined {
20
+ return registries[slug];
21
+ }
22
+
23
+ export function isAllowedComponentTargetId(
24
+ segmentId: string,
25
+ registry: PrototypeComponentRegistry,
26
+ ): boolean {
27
+ if ((registry.ids as readonly string[]).includes(segmentId)) {
28
+ return true;
29
+ }
30
+
31
+ const prefixes = registry.dynamicPrefixes ?? [];
32
+ return prefixes.some((prefix) => segmentId.startsWith(prefix));
33
+ }
34
+
35
+ /** Call from each prototype's component-ids module. */
36
+ export function definePrototypeComponentRegistry(
37
+ registry: PrototypeComponentRegistry,
38
+ ): PrototypeComponentRegistry {
39
+ return registry;
40
+ }
@@ -0,0 +1,34 @@
1
+ import type { ComponentType } from "react";
2
+
3
+ import type { GalleryConfig } from "@prototype/server/gallery/gallery-types";
4
+ import type { PrototypeComponentRegistry } from "./prototype-component-registry";
5
+
6
+ export type PrototypeDefinition = {
7
+ slug: string;
8
+ title: string;
9
+ screenshot: string;
10
+ component: ComponentType;
11
+ componentRegistry: PrototypeComponentRegistry;
12
+ };
13
+
14
+ export type PrototypeExtraRoute = {
15
+ /** URL segments, e.g. `["prototypes", "example-feature", "states"]` or `["section", "[groupId]", "[itemId]", "view"]`. */
16
+ segments: string[];
17
+ /** When set, wrap the page in `PrototypeShell` for this slug. */
18
+ shellSlug?: string;
19
+ component: ComponentType<Record<string, string>>;
20
+ componentRegistry?: PrototypeComponentRegistry;
21
+ };
22
+
23
+ export type PrototypeMetadata = {
24
+ slug: string;
25
+ title: string;
26
+ screenshot: string;
27
+ };
28
+
29
+ export type PrototypeConfig = {
30
+ prototypes: PrototypeDefinition[];
31
+ extraRoutes?: PrototypeExtraRoute[];
32
+ /** KV + Blob gallery routes at `/api/gallery/*`. Set `false` to disable. */
33
+ gallery?: GalleryConfig | false;
34
+ };