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,120 @@
1
+ "use client";
2
+
3
+ import {
4
+ Dialog,
5
+ DialogClose,
6
+ DialogContent,
7
+ DialogTitle,
8
+ } from "@prototype/components/ui/dialog";
9
+ import { ExternalLink, Loader2, X } from "lucide-react";
10
+ import { useEffect, useState } from "react";
11
+
12
+ const MODAL_CHROME_BUTTON_CLASS =
13
+ "text-muted-foreground hover:text-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-sm border-0 bg-transparent p-0 opacity-70 transition-opacity hover:opacity-100 outline-none focus:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-40 disabled:hover:opacity-40 disabled:hover:text-muted-foreground motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
14
+
15
+ export type PrototypeVercelPreviewModalProps = {
16
+ open: boolean;
17
+ onOpenChange: (open: boolean) => void;
18
+ title: string;
19
+ previewUrl: string | null;
20
+ inspectorUrl?: string | null;
21
+ loading?: boolean;
22
+ error?: string | null;
23
+ };
24
+
25
+ export function PrototypeVercelPreviewModal({
26
+ open,
27
+ onOpenChange,
28
+ title,
29
+ previewUrl,
30
+ inspectorUrl,
31
+ loading = false,
32
+ error = null,
33
+ }: PrototypeVercelPreviewModalProps) {
34
+ const [iframeLoaded, setIframeLoaded] = useState(false);
35
+
36
+ useEffect(() => {
37
+ if (!open) {
38
+ setIframeLoaded(false);
39
+ }
40
+ }, [open, previewUrl]);
41
+
42
+ const showIframe = Boolean(previewUrl) && !loading && !error;
43
+
44
+ return (
45
+ <Dialog open={open} onOpenChange={onOpenChange}>
46
+ <DialogContent
47
+ showCloseButton={false}
48
+ className="flex h-[min(92svh,calc(100%-2rem))] max-h-[min(92svh,calc(100%-2rem))] w-[min(96rem,calc(100vw-2rem))] max-w-[min(96rem,calc(100vw-2rem))] flex-col gap-0 overflow-hidden p-0 sm:max-w-[min(96rem,calc(100vw-2rem))]"
49
+ >
50
+ <div className="flex items-center justify-between gap-3 border-b border-border px-4 py-3">
51
+ <DialogTitle className="min-w-0 truncate text-sm font-medium">
52
+ {title}
53
+ </DialogTitle>
54
+ <div className="flex shrink-0 items-center gap-0.5">
55
+ {previewUrl ? (
56
+ <a
57
+ href={previewUrl}
58
+ target="_blank"
59
+ rel="noreferrer"
60
+ className={MODAL_CHROME_BUTTON_CLASS}
61
+ aria-label="Open preview in new tab"
62
+ >
63
+ <ExternalLink />
64
+ </a>
65
+ ) : null}
66
+ {inspectorUrl ? (
67
+ <a
68
+ href={inspectorUrl}
69
+ target="_blank"
70
+ rel="noreferrer"
71
+ className={MODAL_CHROME_BUTTON_CLASS}
72
+ aria-label="Open Vercel inspector"
73
+ >
74
+ <span className="text-[10px] font-medium uppercase tracking-wide">
75
+ Vercel
76
+ </span>
77
+ </a>
78
+ ) : null}
79
+ <DialogClose className={MODAL_CHROME_BUTTON_CLASS}>
80
+ <X />
81
+ <span className="sr-only">Close</span>
82
+ </DialogClose>
83
+ </div>
84
+ </div>
85
+
86
+ <div className="relative min-h-0 flex-1 bg-[var(--bg-subtle)]">
87
+ {loading ? (
88
+ <div className="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
89
+ <Loader2 className="size-4 animate-spin" />
90
+ Loading preview…
91
+ </div>
92
+ ) : null}
93
+
94
+ {!loading && error ? (
95
+ <div className="flex h-full items-center justify-center px-6 text-center text-sm text-muted-foreground">
96
+ {error}
97
+ </div>
98
+ ) : null}
99
+
100
+ {showIframe ? (
101
+ <>
102
+ {!iframeLoaded ? (
103
+ <div className="absolute inset-0 flex items-center justify-center gap-2 text-sm text-muted-foreground">
104
+ <Loader2 className="size-4 animate-spin" />
105
+ Loading deployment…
106
+ </div>
107
+ ) : null}
108
+ <iframe
109
+ title={title}
110
+ src={previewUrl ?? undefined}
111
+ className="h-full w-full border-0 bg-background"
112
+ onLoad={() => setIframeLoaded(true)}
113
+ />
114
+ </>
115
+ ) : null}
116
+ </div>
117
+ </DialogContent>
118
+ </Dialog>
119
+ );
120
+ }
@@ -0,0 +1,89 @@
1
+ $wireframe-bg: #ffffff;
2
+ $wireframe-border: #edebeb;
3
+ $enter-duration: 0.12s;
4
+ $enter-ease: cubic-bezier(0.215, 0.61, 0.355, 1);
5
+
6
+ .tooltip {
7
+ position: absolute;
8
+ z-index: 2;
9
+ width: 220px;
10
+ padding: 0.625rem 0.75rem;
11
+ border: 1px solid $wireframe-border;
12
+ border-radius: 0.5rem;
13
+ background-color: $wireframe-bg;
14
+ pointer-events: none;
15
+ opacity: 0;
16
+ transform: translateX(-4px);
17
+ transition:
18
+ opacity $enter-duration $enter-ease,
19
+ transform $enter-duration $enter-ease;
20
+
21
+ @media (prefers-reduced-motion: reduce) {
22
+ transform: none;
23
+ transition: opacity 0.08s ease;
24
+ }
25
+ }
26
+
27
+ .tooltipVisible {
28
+ opacity: 1;
29
+ transform: translateY(0);
30
+ pointer-events: auto;
31
+
32
+ @media (prefers-reduced-motion: reduce) {
33
+ transform: none;
34
+ }
35
+ }
36
+
37
+ .lead {
38
+ margin: 0;
39
+ font-size: 0.6875rem;
40
+ line-height: 1.45;
41
+ color: var(--color-gray-500);
42
+ white-space: pre-wrap;
43
+ }
44
+
45
+ .bulletList {
46
+ margin: 0.4375rem 0 0;
47
+ padding: 0;
48
+ list-style: none;
49
+ }
50
+
51
+ .bulletItem {
52
+ position: relative;
53
+ margin: 0;
54
+ padding: 0.1875rem 0 0.1875rem 0.875rem;
55
+ font-size: 0.6875rem;
56
+ line-height: 1.45;
57
+ color: var(--color-gray-500);
58
+
59
+ &::before {
60
+ content: "•";
61
+ position: absolute;
62
+ left: 0;
63
+ color: var(--color-gray-400);
64
+ }
65
+
66
+ & + & {
67
+ margin-top: 0.0625rem;
68
+ }
69
+ }
70
+
71
+ .bulletItemInteractive {
72
+ margin-left: -0.375rem;
73
+ padding-left: 1.25rem;
74
+ border-radius: 0.25rem;
75
+ cursor: pointer;
76
+ transition: background-color 0.2s ease, color 0.2s ease;
77
+
78
+ @media (hover: hover) and (pointer: fine) {
79
+ &:hover {
80
+ background: color-mix(in srgb, var(--tool-chrome-blue) 6%, $wireframe-bg);
81
+ color: var(--color-gray-700);
82
+ }
83
+ }
84
+ }
85
+
86
+ .bulletItemActive {
87
+ background: color-mix(in srgb, var(--tool-chrome-blue) 8%, $wireframe-bg);
88
+ color: var(--color-gray-700);
89
+ }
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import type { PrototypeStateCanvasNode } from "@prototype/lib/prototypes/prototype-state-canvas-types";
4
+ import {
5
+ getStateMapAnnotationTitleTop,
6
+ PROTOTYPE_STATE_NODE_WIDTH,
7
+ } from "@prototype/lib/prototypes/prototype-state-canvas-constants";
8
+ import {
9
+ parseStateMapAnnotation,
10
+ resolveBulletHighlightId,
11
+ stateMapAnnotationHasInteractiveBullets,
12
+ } from "@prototype/lib/prototypes/state-map-annotation";
13
+ import { cn } from "@prototype/lib/utils";
14
+ import { useEffect } from "react";
15
+
16
+ import styles from "./state-map-annotation-tooltip.module.scss";
17
+
18
+ const TOOLTIP_GAP = 10;
19
+
20
+ export type StateMapAnnotationTarget = {
21
+ id: string;
22
+ label: string;
23
+ parentLabel?: string;
24
+ annotation: string;
25
+ highlightRegions?: readonly string[];
26
+ };
27
+
28
+ export type StateMapAnnotationAnchor = {
29
+ x: number;
30
+ y: number;
31
+ width: number;
32
+ height: number;
33
+ };
34
+
35
+ type StateMapAnnotationTooltipProps = {
36
+ target: StateMapAnnotationTarget;
37
+ anchor: StateMapAnnotationAnchor;
38
+ visible: boolean;
39
+ activeHighlightId: string | null;
40
+ onHighlightChange: (highlightId: string | null) => void;
41
+ onDismiss: () => void;
42
+ onKeepOpen: () => void;
43
+ };
44
+
45
+ /** Anchor beside the hovered state or nested-state title row. */
46
+ export function getStateMapAnnotationAnchor<T extends string>(
47
+ node: PrototypeStateCanvasNode<T>,
48
+ targetId: string,
49
+ ): StateMapAnnotationAnchor {
50
+ return {
51
+ x: node.x,
52
+ y: getStateMapAnnotationTitleTop(
53
+ node.y,
54
+ targetId,
55
+ node.id,
56
+ node.callouts,
57
+ ),
58
+ width: PROTOTYPE_STATE_NODE_WIDTH,
59
+ height: 0,
60
+ };
61
+ }
62
+
63
+ function computeCanvasTooltipPosition(
64
+ anchor: StateMapAnnotationAnchor,
65
+ ): { left: number; top: number } {
66
+ return {
67
+ left: anchor.x + anchor.width + TOOLTIP_GAP,
68
+ top: anchor.y,
69
+ };
70
+ }
71
+
72
+ export function StateMapAnnotationTooltip({
73
+ target,
74
+ anchor,
75
+ visible,
76
+ activeHighlightId,
77
+ onHighlightChange,
78
+ onDismiss,
79
+ onKeepOpen,
80
+ }: StateMapAnnotationTooltipProps) {
81
+ const position = computeCanvasTooltipPosition(anchor);
82
+ const parsed = parseStateMapAnnotation(target.annotation);
83
+ const hasInteractiveBullets = stateMapAnnotationHasInteractiveBullets(
84
+ target.annotation,
85
+ target.highlightRegions,
86
+ );
87
+
88
+ useEffect(() => {
89
+ onHighlightChange(null);
90
+ }, [target.id, onHighlightChange]);
91
+
92
+ return (
93
+ <div
94
+ className={cn(styles.tooltip, visible && styles.tooltipVisible)}
95
+ role="tooltip"
96
+ style={{ left: position.left, top: position.top }}
97
+ onMouseEnter={onKeepOpen}
98
+ onMouseLeave={onDismiss}
99
+ onPointerDown={(event) => event.stopPropagation()}
100
+ >
101
+ {parsed.lead ? <p className={styles.lead}>{parsed.lead}</p> : null}
102
+ {parsed.bullets.length > 0 ? (
103
+ <ul className={styles.bulletList}>
104
+ {parsed.bullets.map((bullet) => {
105
+ const highlightId = resolveBulletHighlightId(
106
+ bullet,
107
+ target.highlightRegions,
108
+ );
109
+ const isInteractive = hasInteractiveBullets && highlightId != null;
110
+ return (
111
+ <li
112
+ key={bullet.index}
113
+ className={cn(
114
+ styles.bulletItem,
115
+ isInteractive && styles.bulletItemInteractive,
116
+ isInteractive &&
117
+ activeHighlightId === highlightId &&
118
+ styles.bulletItemActive,
119
+ )}
120
+ onMouseEnter={
121
+ isInteractive
122
+ ? () => onHighlightChange(highlightId)
123
+ : undefined
124
+ }
125
+ onMouseLeave={
126
+ isInteractive ? () => onHighlightChange(null) : undefined
127
+ }
128
+ >
129
+ {bullet.text}
130
+ </li>
131
+ );
132
+ })}
133
+ </ul>
134
+ ) : null}
135
+ </div>
136
+ );
137
+ }
@@ -0,0 +1,25 @@
1
+ .region {
2
+ border-radius: inherit;
3
+ transition:
4
+ background-color 0.2s ease-out,
5
+ box-shadow 0.2s ease-out;
6
+ }
7
+
8
+ .regionActive {
9
+ background: color-mix(in srgb, var(--prototype-comment-color-blue, #3b82f6) 8%, transparent);
10
+ box-shadow: inset 0 0 0 2px
11
+ color-mix(in srgb, var(--prototype-comment-color-blue, #3b82f6) 55%, transparent);
12
+ }
13
+
14
+ .previewOverlay {
15
+ position: absolute;
16
+ z-index: 2;
17
+ border: 2px solid;
18
+ border-radius: 0.25rem;
19
+ pointer-events: none;
20
+ transition: opacity 0.2s ease-out;
21
+
22
+ @media (prefers-reduced-motion: reduce) {
23
+ transition: none;
24
+ }
25
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import {
4
+ PR_TARGET_HIGHLIGHT_BORDER,
5
+ PR_TARGET_HIGHLIGHT_FILL,
6
+ } from "@prototype/lib/pr-split/pr-split-highlight";
7
+ import type { StateMapHighlightRect } from "@prototype/lib/prototypes/prototype-state-canvas-types";
8
+ import { cn } from "@prototype/lib/utils";
9
+ import {
10
+ createContext,
11
+ useContext,
12
+ type CSSProperties,
13
+ type ReactNode,
14
+ } from "react";
15
+
16
+ import styles from "./state-map-wireframe-highlight.module.scss";
17
+
18
+ const StateMapHighlightContext = createContext<string | null>(null);
19
+
20
+ export function StateMapHighlightProvider({
21
+ highlightId,
22
+ children,
23
+ }: {
24
+ highlightId: string | null;
25
+ children: ReactNode;
26
+ }) {
27
+ return (
28
+ <StateMapHighlightContext.Provider value={highlightId}>
29
+ {children}
30
+ </StateMapHighlightContext.Provider>
31
+ );
32
+ }
33
+
34
+ export function useStateMapHighlight(): string | null {
35
+ return useContext(StateMapHighlightContext);
36
+ }
37
+
38
+ export function StateMapHighlightRegion({
39
+ id,
40
+ children,
41
+ className,
42
+ }: {
43
+ id: string;
44
+ children: ReactNode;
45
+ className?: string;
46
+ }) {
47
+ const activeHighlightId = useStateMapHighlight();
48
+ const isActive = activeHighlightId === id;
49
+
50
+ return (
51
+ <div
52
+ data-state-map-highlight={id}
53
+ className={cn(styles.region, isActive && styles.regionActive, className)}
54
+ >
55
+ {children}
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export function StateMapPreviewHighlightOverlay({
61
+ rect,
62
+ }: {
63
+ rect: StateMapHighlightRect;
64
+ }) {
65
+ const style: CSSProperties = {
66
+ top: `${rect.top}%`,
67
+ left: `${rect.left}%`,
68
+ width: `${rect.width}%`,
69
+ height: `${rect.height}%`,
70
+ borderColor: PR_TARGET_HIGHLIGHT_BORDER,
71
+ backgroundColor: PR_TARGET_HIGHLIGHT_FILL,
72
+ };
73
+
74
+ return (
75
+ <div
76
+ aria-hidden
77
+ className={styles.previewOverlay}
78
+ style={style}
79
+ data-state-map-highlight-overlay
80
+ />
81
+ );
82
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { PrototypeProvider } from "@prototype/components/prototype-provider";
4
+ import { cn } from "@prototype/lib/utils";
5
+ import type { ReactNode } from "react";
6
+
7
+ type PrototypeGalleryShellProps = {
8
+ children: ReactNode;
9
+ footer?: ReactNode;
10
+ };
11
+
12
+ export function PrototypeGalleryShell({
13
+ children,
14
+ footer,
15
+ }: PrototypeGalleryShellProps) {
16
+ return (
17
+ <PrototypeProvider className="bg-[var(--bg-ground)] flex h-svh w-full flex-col overflow-hidden">
18
+ {footer ? (
19
+ <div className="border-border flex shrink-0 items-center justify-end border-b px-10 py-3 lg:px-16">
20
+ {footer}
21
+ </div>
22
+ ) : null}
23
+
24
+ <main className="bg-background min-h-0 flex-1 overflow-y-auto">
25
+ {children}
26
+ </main>
27
+ </PrototypeProvider>
28
+ );
29
+ }
30
+
31
+ type PrototypeGalleryHeaderProps = {
32
+ eyebrow?: string;
33
+ title?: string;
34
+ description?: string;
35
+ className?: string;
36
+ };
37
+
38
+ export function PrototypeGalleryHeader({
39
+ eyebrow,
40
+ title = "Prototypes",
41
+ description = "UI experiments and concept explorations. Select a prototype to open it.",
42
+ className,
43
+ }: PrototypeGalleryHeaderProps) {
44
+ return (
45
+ <div
46
+ className={cn(
47
+ "border-border -mx-10 mb-8 border-b px-10 py-6 lg:-mx-16 lg:px-16",
48
+ className,
49
+ )}
50
+ >
51
+ {eyebrow ? (
52
+ <p className="text-xs text-muted-foreground">{eyebrow}</p>
53
+ ) : null}
54
+ <h1 className="text-foreground mt-0 text-2xl font-medium tracking-tight">
55
+ {title}
56
+ </h1>
57
+ <p className="text-muted-foreground mt-2 max-w-2xl text-sm leading-relaxed">
58
+ {description}
59
+ </p>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ export function PrototypeGalleryPage({
65
+ children,
66
+ footer,
67
+ header,
68
+ }: PrototypeGalleryShellProps & {
69
+ header?: ReactNode;
70
+ }) {
71
+ return (
72
+ <PrototypeGalleryShell footer={footer}>
73
+ <div className="px-10 lg:px-16">
74
+ {header ?? <PrototypeGalleryHeader />}
75
+ {children}
76
+ </div>
77
+ </PrototypeGalleryShell>
78
+ );
79
+ }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { capturePrototypeViewportPng } from "@prototype/lib/prototypes/capture-prototype-viewport";
4
+ import { useEffect } from "react";
5
+
6
+ type PrototypeScreenshotCaptureProps = {
7
+ slug: string;
8
+ };
9
+
10
+ export function PrototypeScreenshotCapture({
11
+ slug,
12
+ }: PrototypeScreenshotCaptureProps) {
13
+ useEffect(() => {
14
+ if (process.env.NODE_ENV !== "development") return;
15
+
16
+ let cancelled = false;
17
+
18
+ const capture = async () => {
19
+ const dataUrl = await capturePrototypeViewportPng();
20
+ if (cancelled || !dataUrl) return;
21
+
22
+ try {
23
+ const response = await fetch(`/api/prototypes/${slug}/screenshot`, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify({ image: dataUrl }),
27
+ });
28
+
29
+ if (!response.ok) {
30
+ throw new Error("Failed to save screenshot");
31
+ }
32
+ } catch (error) {
33
+ console.warn("[prototype-screenshot]", error);
34
+ }
35
+ };
36
+
37
+ void capture();
38
+
39
+ return () => {
40
+ cancelled = true;
41
+ };
42
+ }, [slug]);
43
+
44
+ return null;
45
+ }