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,134 @@
1
+ export type PrototypeReviewSidebarPanel =
2
+ | "comments"
3
+ | "variants"
4
+ | "change-log"
5
+ | "spec";
6
+
7
+ export type PrototypeReviewTab = "tweaks" | "comments";
8
+
9
+ export const REVIEW_PANEL_PARAM = "reviewPanel";
10
+ export const REVIEW_VARIANT_SET_PARAM = "variantSet";
11
+ export const REVIEW_TAB_PARAM = "reviewTab";
12
+ export const REVIEW_TWEAKS_PARAM = "tweaks";
13
+ export const REVIEW_SETTINGS_PARAM = "settings";
14
+ export const REVIEW_PROD_REFERENCE_PARAM = "prodReference";
15
+
16
+ export const REVIEW_URL_PARAM_KEYS = [
17
+ REVIEW_PANEL_PARAM,
18
+ REVIEW_VARIANT_SET_PARAM,
19
+ REVIEW_TAB_PARAM,
20
+ REVIEW_TWEAKS_PARAM,
21
+ REVIEW_SETTINGS_PARAM,
22
+ REVIEW_PROD_REFERENCE_PARAM,
23
+ ] as const;
24
+
25
+ const VALID_REVIEW_PANELS = new Set<PrototypeReviewSidebarPanel>([
26
+ "comments",
27
+ "variants",
28
+ "change-log",
29
+ "spec",
30
+ ]);
31
+
32
+ const VALID_REVIEW_TABS = new Set<PrototypeReviewTab>(["tweaks", "comments"]);
33
+
34
+ export type PrototypeReviewUrlState = {
35
+ panel: PrototypeReviewSidebarPanel | null;
36
+ variantSetId: string | null;
37
+ activeTab: PrototypeReviewTab;
38
+ tweaksOpen: boolean;
39
+ settingsOpen: boolean;
40
+ prodReference: boolean;
41
+ };
42
+
43
+ export function hasReviewUrlParams(searchParams: URLSearchParams): boolean {
44
+ return REVIEW_URL_PARAM_KEYS.some((key) => searchParams.has(key));
45
+ }
46
+
47
+ export function parseReviewStateFromSearchParams(
48
+ searchParams: URLSearchParams,
49
+ ): PrototypeReviewUrlState {
50
+ const panelRaw = searchParams.get(REVIEW_PANEL_PARAM);
51
+ const panel =
52
+ panelRaw && VALID_REVIEW_PANELS.has(panelRaw as PrototypeReviewSidebarPanel)
53
+ ? (panelRaw as PrototypeReviewSidebarPanel)
54
+ : null;
55
+
56
+ const variantSetRaw = searchParams.get(REVIEW_VARIANT_SET_PARAM)?.trim();
57
+ const variantSetId = variantSetRaw ? variantSetRaw : null;
58
+
59
+ const tabRaw = searchParams.get(REVIEW_TAB_PARAM);
60
+ const activeTab =
61
+ tabRaw && VALID_REVIEW_TABS.has(tabRaw as PrototypeReviewTab)
62
+ ? (tabRaw as PrototypeReviewTab)
63
+ : "tweaks";
64
+
65
+ return {
66
+ panel,
67
+ variantSetId,
68
+ activeTab,
69
+ tweaksOpen: searchParams.get(REVIEW_TWEAKS_PARAM) === "1",
70
+ settingsOpen: searchParams.get(REVIEW_SETTINGS_PARAM) === "1",
71
+ prodReference: searchParams.get(REVIEW_PROD_REFERENCE_PARAM) === "1",
72
+ };
73
+ }
74
+
75
+ export function writeReviewStateToSearchParams(
76
+ searchParams: URLSearchParams,
77
+ state: {
78
+ open: boolean;
79
+ sidebarPanel: PrototypeReviewSidebarPanel | null;
80
+ activeVariantSetId: string | null;
81
+ activeTab: PrototypeReviewTab;
82
+ tweaksOpen: boolean;
83
+ settingsOpen: boolean;
84
+ prodReferenceAvailable: boolean;
85
+ showProdReference: boolean;
86
+ },
87
+ ): void {
88
+ for (const key of REVIEW_URL_PARAM_KEYS) {
89
+ searchParams.delete(key);
90
+ }
91
+
92
+ if (state.tweaksOpen) {
93
+ searchParams.set(REVIEW_TWEAKS_PARAM, "1");
94
+ }
95
+
96
+ if (state.settingsOpen) {
97
+ searchParams.set(REVIEW_SETTINGS_PARAM, "1");
98
+ }
99
+
100
+ if (state.prodReferenceAvailable && state.showProdReference) {
101
+ searchParams.set(REVIEW_PROD_REFERENCE_PARAM, "1");
102
+ }
103
+
104
+ if (!state.open || !state.sidebarPanel) {
105
+ return;
106
+ }
107
+
108
+ searchParams.set(REVIEW_PANEL_PARAM, state.sidebarPanel);
109
+
110
+ if (state.sidebarPanel === "variants" && state.activeVariantSetId) {
111
+ searchParams.set(REVIEW_VARIANT_SET_PARAM, state.activeVariantSetId);
112
+ }
113
+
114
+ if (state.sidebarPanel === "comments" && state.activeTab === "comments") {
115
+ searchParams.set(REVIEW_TAB_PARAM, "comments");
116
+ }
117
+ }
118
+
119
+ export function syncReviewStateToUrl(state: {
120
+ open: boolean;
121
+ sidebarPanel: PrototypeReviewSidebarPanel | null;
122
+ activeVariantSetId: string | null;
123
+ activeTab: PrototypeReviewTab;
124
+ tweaksOpen: boolean;
125
+ settingsOpen: boolean;
126
+ prodReferenceAvailable: boolean;
127
+ showProdReference: boolean;
128
+ }): void {
129
+ if (typeof window === "undefined") return;
130
+
131
+ const url = new URL(window.location.href);
132
+ writeReviewStateToSearchParams(url.searchParams, state);
133
+ window.history.replaceState(null, "", url);
134
+ }
@@ -0,0 +1,175 @@
1
+ import { resolveAnnotationTargetById } from "@prototype/lib/prototype-comments/core/annotation-target";
2
+
3
+ export const SHARE_TARGET_PARAM = "shareTarget";
4
+ export const SHARE_STATE_PARAM = "shareState";
5
+ export const SHARE_COMMENT_PARAM = "shareComment";
6
+
7
+ function toBase64Url(value: string): string {
8
+ const bytes = new TextEncoder().encode(value);
9
+ let binary = "";
10
+ for (const byte of bytes) {
11
+ binary += String.fromCharCode(byte);
12
+ }
13
+ return btoa(binary)
14
+ .replace(/\+/g, "-")
15
+ .replace(/\//g, "_")
16
+ .replace(/=+$/, "");
17
+ }
18
+
19
+ function fromBase64Url(value: string): string {
20
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/");
21
+ const padLength = (4 - (padded.length % 4)) % 4;
22
+ const binary = atob(padded + "=".repeat(padLength));
23
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
24
+ return new TextDecoder().decode(bytes);
25
+ }
26
+
27
+ export function encodeShareState(state: unknown): string {
28
+ return toBase64Url(JSON.stringify(state));
29
+ }
30
+
31
+ export function decodeShareState<T = unknown>(encoded: string): T {
32
+ return JSON.parse(fromBase64Url(encoded)) as T;
33
+ }
34
+
35
+ export type ShareLinkParams = {
36
+ targetId: string;
37
+ state: unknown | null;
38
+ };
39
+
40
+ export function readShareLinkParams(
41
+ searchParams: URLSearchParams,
42
+ ): ShareLinkParams | null {
43
+ const targetId = searchParams.get(SHARE_TARGET_PARAM);
44
+ if (!targetId) return null;
45
+
46
+ const stateParam = searchParams.get(SHARE_STATE_PARAM);
47
+ if (!stateParam) {
48
+ return { targetId, state: null };
49
+ }
50
+
51
+ try {
52
+ return { targetId, state: decodeShareState(stateParam) };
53
+ } catch {
54
+ return { targetId, state: null };
55
+ }
56
+ }
57
+
58
+ export function readShareCommentParam(
59
+ searchParams: URLSearchParams,
60
+ ): string | null {
61
+ const commentId = searchParams.get(SHARE_COMMENT_PARAM);
62
+ return commentId?.trim() ? commentId : null;
63
+ }
64
+
65
+ export function pendingCommentRestoreKey(slug: string): string {
66
+ return `prototype-share-comment:${slug}`;
67
+ }
68
+
69
+ export function stashPendingCommentRestore(slug: string, commentId: string): void {
70
+ if (typeof window === "undefined") return;
71
+
72
+ try {
73
+ sessionStorage.setItem(pendingCommentRestoreKey(slug), commentId);
74
+ } catch {
75
+ // Ignore sessionStorage write failures in prototype preview.
76
+ }
77
+ }
78
+
79
+ export function readPendingCommentRestore(slug: string): string | null {
80
+ if (typeof window === "undefined") return null;
81
+
82
+ try {
83
+ const value = sessionStorage.getItem(pendingCommentRestoreKey(slug));
84
+ return value?.trim() ? value : null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ export function clearPendingCommentRestore(slug: string): void {
91
+ if (typeof window === "undefined") return;
92
+
93
+ try {
94
+ sessionStorage.removeItem(pendingCommentRestoreKey(slug));
95
+ } catch {
96
+ // Ignore sessionStorage write failures in prototype preview.
97
+ }
98
+ }
99
+
100
+ export function hasPendingCommentRestore(slug: string): boolean {
101
+ return readPendingCommentRestore(slug) != null;
102
+ }
103
+
104
+ /** URL param first; falls back to session stash so restore survives URL rewrites. */
105
+ export function resolveShareCommentId(
106
+ searchParams: URLSearchParams,
107
+ slug: string | null,
108
+ ): string | null {
109
+ const fromUrl = readShareCommentParam(searchParams);
110
+ if (fromUrl) {
111
+ if (slug) stashPendingCommentRestore(slug, fromUrl);
112
+ return fromUrl;
113
+ }
114
+
115
+ return slug ? readPendingCommentRestore(slug) : null;
116
+ }
117
+
118
+ export function buildComponentShareLink(
119
+ targetId: string,
120
+ liveState: unknown,
121
+ options?: { commentId?: string | null },
122
+ ): string {
123
+ const url = new URL(window.location.href);
124
+ url.searchParams.set(SHARE_TARGET_PARAM, targetId);
125
+ url.searchParams.set(SHARE_STATE_PARAM, encodeShareState(liveState));
126
+ if (options?.commentId) {
127
+ url.searchParams.set(SHARE_COMMENT_PARAM, options.commentId);
128
+ }
129
+ return url.toString();
130
+ }
131
+
132
+ export function buildCommentShareLink(commentId: string): string {
133
+ const url = new URL(window.location.href);
134
+ url.searchParams.set(SHARE_COMMENT_PARAM, commentId);
135
+ return url.toString();
136
+ }
137
+
138
+ export function syncShareCommentParam(commentId: string | null): void {
139
+ const url = new URL(window.location.href);
140
+ if (commentId) {
141
+ if (url.searchParams.get(SHARE_COMMENT_PARAM) === commentId) return;
142
+ url.searchParams.set(SHARE_COMMENT_PARAM, commentId);
143
+ } else if (!url.searchParams.has(SHARE_COMMENT_PARAM)) {
144
+ return;
145
+ } else {
146
+ url.searchParams.delete(SHARE_COMMENT_PARAM);
147
+ }
148
+ window.history.replaceState(null, "", url);
149
+ }
150
+
151
+ export function clearShareCommentParam(url: URL): void {
152
+ url.searchParams.delete(SHARE_COMMENT_PARAM);
153
+ }
154
+
155
+ /** Removes component share params only; keeps `shareComment` for comment deep-link restore. */
156
+ export function clearShareComponentParams(url: URL): void {
157
+ url.searchParams.delete(SHARE_TARGET_PARAM);
158
+ url.searchParams.delete(SHARE_STATE_PARAM);
159
+ }
160
+
161
+ export function clearShareLinkParams(url: URL): void {
162
+ clearShareComponentParams(url);
163
+ clearShareCommentParam(url);
164
+ }
165
+
166
+ export function scrollPrototypeTargetIntoView(targetId: string): void {
167
+ const element = resolveAnnotationTargetById(targetId);
168
+ if (!element) return;
169
+
170
+ window.requestAnimationFrame(() => {
171
+ window.requestAnimationFrame(() => {
172
+ element.scrollIntoView({ behavior: "smooth", block: "center" });
173
+ });
174
+ });
175
+ }
@@ -0,0 +1,118 @@
1
+ /** Layout width of a state-map node card (CSS px). */
2
+ export const PROTOTYPE_STATE_NODE_WIDTH = 220;
3
+
4
+ /** Preview image area height inside a state-map node (CSS px). */
5
+ export const PROTOTYPE_STATE_NODE_PREVIEW_HEIGHT = 132;
6
+
7
+ /** Label row height below the wireframe preview (CSS px). */
8
+ export const PROTOTYPE_STATE_NODE_LABEL_HEIGHT = 36;
9
+
10
+ /** Single nested-state (callout) row height (CSS px). */
11
+ export const PROTOTYPE_STATE_NODE_CALLOUT_HEIGHT = 28;
12
+
13
+ /** Nested-state row height when a hint label is shown (CSS px). */
14
+ export const PROTOTYPE_STATE_NODE_CALLOUT_WITH_HINT_HEIGHT = 36;
15
+
16
+ /** Gap between nested-state rows (CSS px). */
17
+ export const PROTOTYPE_STATE_NODE_CALLOUT_GAP = 4;
18
+
19
+ /** Padding around the nested-state tray (CSS px). */
20
+ export const PROTOTYPE_STATE_NODE_CALLOUT_PADDING = 8;
21
+
22
+ /** Horizontal gap between state-map node columns (CSS px). */
23
+ export const PROTOTYPE_STATE_NODE_COLUMN_GAP = 56;
24
+
25
+ /** Vertical gap between a section label and the nodes below it (CSS px). */
26
+ export const PROTOTYPE_STATE_SECTION_TO_NODES = 40;
27
+
28
+ /** Vertical gap between rows of nodes (CSS px). */
29
+ export const PROTOTYPE_STATE_NODE_ROW_GAP = 48;
30
+
31
+ /** Padding from the canvas origin (CSS px). */
32
+ export const PROTOTYPE_STATE_CANVAS_PADDING = 48;
33
+
34
+ export type StateCanvasCalloutDimensions = {
35
+ hint?: string;
36
+ };
37
+
38
+ export function getStateCanvasCalloutHeight(
39
+ callout: StateCanvasCalloutDimensions,
40
+ ): number {
41
+ return callout.hint
42
+ ? PROTOTYPE_STATE_NODE_CALLOUT_WITH_HINT_HEIGHT
43
+ : PROTOTYPE_STATE_NODE_CALLOUT_HEIGHT;
44
+ }
45
+
46
+ /** Preview + label area height (excludes nested-state callouts). */
47
+ export function getStateCanvasNodeBodyHeight(): number {
48
+ return PROTOTYPE_STATE_NODE_PREVIEW_HEIGHT + PROTOTYPE_STATE_NODE_LABEL_HEIGHT;
49
+ }
50
+
51
+ /** Canvas Y of the title row for a state or nested-state annotation target. */
52
+ export function getStateMapAnnotationTitleTop(
53
+ nodeY: number,
54
+ targetId: string,
55
+ nodeId: string,
56
+ callouts?: readonly (StateCanvasCalloutDimensions & { id: string })[],
57
+ ): number {
58
+ if (targetId === nodeId) {
59
+ return nodeY + PROTOTYPE_STATE_NODE_PREVIEW_HEIGHT;
60
+ }
61
+
62
+ if (!callouts?.length) {
63
+ return nodeY + PROTOTYPE_STATE_NODE_PREVIEW_HEIGHT;
64
+ }
65
+
66
+ let offset =
67
+ getStateCanvasNodeBodyHeight() + PROTOTYPE_STATE_NODE_CALLOUT_PADDING;
68
+
69
+ for (const callout of callouts) {
70
+ if (callout.id === targetId) {
71
+ return nodeY + offset;
72
+ }
73
+ offset +=
74
+ getStateCanvasCalloutHeight(callout) + PROTOTYPE_STATE_NODE_CALLOUT_GAP;
75
+ }
76
+
77
+ return nodeY + PROTOTYPE_STATE_NODE_PREVIEW_HEIGHT;
78
+ }
79
+
80
+ /** Total rendered height of a state-map node, including nested states. */
81
+ export function getStateCanvasNodeHeight(
82
+ callouts?: readonly StateCanvasCalloutDimensions[],
83
+ ): number {
84
+ const bodyHeight = getStateCanvasNodeBodyHeight();
85
+ const count = callouts?.length ?? 0;
86
+ if (count === 0) return bodyHeight;
87
+
88
+ const calloutsHeight = callouts.reduce((sum, callout, index) => {
89
+ const gap = index > 0 ? PROTOTYPE_STATE_NODE_CALLOUT_GAP : 0;
90
+ return sum + gap + getStateCanvasCalloutHeight(callout);
91
+ }, 0);
92
+
93
+ return bodyHeight + PROTOTYPE_STATE_NODE_CALLOUT_PADDING * 2 + calloutsHeight;
94
+ }
95
+
96
+ /** X coordinate for a zero-based column index. */
97
+ export function stateCanvasColumnX(columnIndex: number): number {
98
+ return (
99
+ PROTOTYPE_STATE_CANVAS_PADDING +
100
+ columnIndex * (PROTOTYPE_STATE_NODE_WIDTH + PROTOTYPE_STATE_NODE_COLUMN_GAP)
101
+ );
102
+ }
103
+
104
+ /** Maximum canvas zoom — thumbnails are generated for this scale. */
105
+ export const PROTOTYPE_STATE_CANVAS_MAX_SCALE = 4.5;
106
+
107
+ /** Device pixel ratio used when rasterizing state thumbnails. */
108
+ export const PROTOTYPE_STATE_SCREENSHOT_THUMB_PIXEL_RATIO = 2;
109
+
110
+ export function getStateScreenshotThumbSize(): { width: number; height: number } {
111
+ const scale =
112
+ PROTOTYPE_STATE_CANVAS_MAX_SCALE * PROTOTYPE_STATE_SCREENSHOT_THUMB_PIXEL_RATIO;
113
+
114
+ return {
115
+ width: Math.round(PROTOTYPE_STATE_NODE_WIDTH * scale),
116
+ height: Math.round(PROTOTYPE_STATE_NODE_PREVIEW_HEIGHT * scale),
117
+ };
118
+ }
@@ -0,0 +1,159 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type PrototypeStateCanvasNodeCallout<T extends string = string> = {
4
+ id: T;
5
+ label: string;
6
+ hint?: string;
7
+ /** Planning note — what this variant should do or communicate. */
8
+ annotation?: string;
9
+ /** Maps bullet index (or @suffix id) to wireframe highlight region keys. */
10
+ highlightRegions?: readonly string[];
11
+ };
12
+
13
+ export type PrototypeStateCanvasNode<T extends string = string> = {
14
+ id: T;
15
+ label: string;
16
+ /** Canvas coordinates in px from the flowchart origin. */
17
+ x: number;
18
+ y: number;
19
+ /** Nested options rendered below the node — not separate canvas nodes. */
20
+ callouts?: PrototypeStateCanvasNodeCallout<T>[];
21
+ /** Planning note — what this screen should do, triggers, open questions. */
22
+ annotation?: string;
23
+ /** Maps bullet index (or @suffix id) to wireframe highlight region keys. */
24
+ highlightRegions?: readonly string[];
25
+ };
26
+
27
+ export type PrototypeStateCanvasEdge<T extends string = string> = {
28
+ from: T;
29
+ to: T;
30
+ /** Dashed lines for alternate / empty-state branches. */
31
+ dashed?: boolean;
32
+ };
33
+
34
+ export type PrototypeStateCanvasSection = {
35
+ label: string;
36
+ /** Top offset of the section label in canvas px. */
37
+ y: number;
38
+ /** Group-level planning note for this region of the map. */
39
+ annotation?: string;
40
+ };
41
+
42
+ export const PROTOTYPE_STATE_MAP_RETURN_TO_PARAM = "returnTo";
43
+
44
+ export function getDefaultPrototypeStateMapPath(slug: string): string {
45
+ return `/prototypes/${slug}/states`;
46
+ }
47
+
48
+ /** Relative path (including query) to restore when leaving the map without picking a state. */
49
+ export function parseStateMapReturnTo(
50
+ searchParams: URLSearchParams,
51
+ ): string | null {
52
+ const returnTo = searchParams.get(PROTOTYPE_STATE_MAP_RETURN_TO_PARAM);
53
+ if (!returnTo?.startsWith("/") || returnTo.startsWith("//")) {
54
+ return null;
55
+ }
56
+ return returnTo;
57
+ }
58
+
59
+ export function buildStateMapHref(
60
+ pagePath: string,
61
+ returnTo: string,
62
+ searchParams?: URLSearchParams,
63
+ ): string {
64
+ const params = new URLSearchParams(searchParams?.toString() ?? "");
65
+ params.set(PROTOTYPE_STATE_MAP_RETURN_TO_PARAM, returnTo);
66
+ const query = params.toString();
67
+ return query ? `${pagePath}?${query}` : pagePath;
68
+ }
69
+
70
+ export type StateMapHighlightRect = {
71
+ top: number;
72
+ left: number;
73
+ width: number;
74
+ height: number;
75
+ };
76
+
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
+ onStateSelect: (id: T) => void;
81
+ nodes: PrototypeStateCanvasNode<T>[];
82
+ edges: PrototypeStateCanvasEdge<T>[];
83
+ sections?: PrototypeStateCanvasSection[];
84
+ renderWireframe: (nodeId: T) => ReactNode;
85
+ /** Percent-based overlay regions for screenshot previews (0–100). */
86
+ getHighlightRegions?: (
87
+ nodeId: T,
88
+ ) => Partial<Record<string, StateMapHighlightRect>> | undefined;
89
+ /** Route for the full-page state map (toolbar link target). */
90
+ pagePath?: string;
91
+ /** Total scrollable canvas size. Defaults derived from node bounds. */
92
+ canvasWidth?: number;
93
+ canvasHeight?: number;
94
+ };
95
+
96
+ export type PrototypeStateMapAnnotationEntry = {
97
+ id: string;
98
+ label: string;
99
+ annotation: string;
100
+ kind: "section" | "state" | "variant";
101
+ parentLabel?: string;
102
+ };
103
+
104
+ export function stateMapHasAnnotations<T extends string>(
105
+ config: Pick<PrototypeStateCanvasConfig<T>, "nodes" | "sections">,
106
+ ): boolean {
107
+ if (config.sections?.some((section) => section.annotation?.trim())) {
108
+ return true;
109
+ }
110
+
111
+ return config.nodes.some((node) => {
112
+ if (node.annotation?.trim()) return true;
113
+ return node.callouts?.some((callout) => callout.annotation?.trim()) ?? false;
114
+ });
115
+ }
116
+
117
+ export function buildStateMapAnnotationEntries<T extends string>(
118
+ nodes: readonly PrototypeStateCanvasNode<T>[],
119
+ sections?: readonly PrototypeStateCanvasSection[],
120
+ ): PrototypeStateMapAnnotationEntry[] {
121
+ const entries: PrototypeStateMapAnnotationEntry[] = [];
122
+
123
+ for (const section of sections ?? []) {
124
+ const annotation = section.annotation?.trim();
125
+ if (!annotation) continue;
126
+ entries.push({
127
+ id: `section:${section.label}`,
128
+ label: section.label,
129
+ annotation,
130
+ kind: "section",
131
+ });
132
+ }
133
+
134
+ for (const node of nodes) {
135
+ const nodeAnnotation = node.annotation?.trim();
136
+ if (nodeAnnotation) {
137
+ entries.push({
138
+ id: node.id,
139
+ label: node.label,
140
+ annotation: nodeAnnotation,
141
+ kind: "state",
142
+ });
143
+ }
144
+
145
+ for (const callout of node.callouts ?? []) {
146
+ const calloutAnnotation = callout.annotation?.trim();
147
+ if (!calloutAnnotation) continue;
148
+ entries.push({
149
+ id: callout.id,
150
+ label: callout.label,
151
+ annotation: calloutAnnotation,
152
+ kind: "variant",
153
+ parentLabel: node.label,
154
+ });
155
+ }
156
+ }
157
+
158
+ return entries;
159
+ }
@@ -0,0 +1,39 @@
1
+ export const PROTOTYPE_STATE_SCREENSHOT_CAPTURED_EVENT =
2
+ "prototype-state-screenshot-captured";
3
+
4
+ export type PrototypeStateScreenshotCapturedDetail = {
5
+ slug: string;
6
+ stateId: string;
7
+ version: number;
8
+ };
9
+
10
+ export function sanitizeStateIdForFilename(stateId: string): string {
11
+ return stateId.replace(/:/g, "--");
12
+ }
13
+
14
+ export function stateScreenshotManifestKey(slug: string, stateId: string): string {
15
+ return `${slug}/states/${sanitizeStateIdForFilename(stateId)}`;
16
+ }
17
+
18
+ export function stateScreenshotPublicPath(slug: string, stateId: string): string {
19
+ return `/prototypes/screenshots/${slug}/states/${sanitizeStateIdForFilename(stateId)}.png`;
20
+ }
21
+
22
+ export function stateScreenshotSrc(
23
+ slug: string,
24
+ stateId: string,
25
+ version: number | undefined,
26
+ ): string {
27
+ const basePath = stateScreenshotPublicPath(slug, stateId);
28
+ if (!version) return basePath;
29
+ return `${basePath}?v=${version}`;
30
+ }
31
+
32
+ export function dispatchPrototypeStateScreenshotCaptured(
33
+ detail: PrototypeStateScreenshotCapturedDetail,
34
+ ): void {
35
+ if (typeof window === "undefined") return;
36
+ window.dispatchEvent(
37
+ new CustomEvent(PROTOTYPE_STATE_SCREENSHOT_CAPTURED_EVENT, { detail }),
38
+ );
39
+ }