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,347 @@
1
+ "use client";
2
+
3
+ import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
4
+ import { PrototypeControlGroup } from "@prototype/components/prototypes/prototype-control";
5
+ import { PrototypeDesignBriefModal } from "@prototype/components/prototypes/prototype-design-brief-modal";
6
+ import { PrototypeDesignBriefPanel } from "@prototype/components/prototypes/prototype-design-brief-panel";
7
+ import {
8
+ PrototypeComponent,
9
+ usePrototypeSlug,
10
+ } from "@prototype/components/prototypes/prototype-target";
11
+ import { PrototypeVariantTabs } from "@prototype/components/prototypes/prototype-variant-tabs";
12
+ import {
13
+ buildBriefOverviewCopyText,
14
+ type DesignExplorationConfig,
15
+ } from "@prototype/lib/prototypes/design-exploration-types";
16
+ import { usePrototypeReviewOptional } from "@prototype/lib/prototypes/prototype-review-context";
17
+ import { useDesignExplorationDefault } from "@prototype/lib/prototypes/use-design-exploration-default";
18
+ import { usePersistedLocalString } from "@prototype/lib/prototypes/use-persisted-local-state";
19
+ import { ChevronDown, ChevronUp, LayoutGrid } from "lucide-react";
20
+ import { useEffect, useMemo, useState } from "react";
21
+
22
+ const OVERVIEW_CONTROL_BUTTON_CLASS = "tool-toolbar-button";
23
+
24
+ const OVERVIEW_TOOLBAR_CLASS = "tool-toolbar pointer-events-auto";
25
+
26
+ type PrototypeVariantExplorerProps<TVariant extends string> =
27
+ DesignExplorationConfig<TVariant> & {
28
+ /** Stable id for the Mobbin gallery root (registered in component-ids.ts). */
29
+ mobbinGalleryId?: string;
30
+ /** When false, caller wraps with PrototypeComponent id={componentIdPrefix}. */
31
+ wrapRoot?: boolean;
32
+ /**
33
+ * When true, only registers sidebar content — skips rendering the active
34
+ * variant in the page tree. Use for hidden hosts that exist solely to keep
35
+ * design explorations available in the review sidebar.
36
+ */
37
+ registerOnly?: boolean;
38
+ /** When set, registers this exploration in the review sidebar. */
39
+ variantSet?: {
40
+ id: string;
41
+ label: string;
42
+ };
43
+ };
44
+
45
+ export function PrototypeVariantExplorer<TVariant extends string>({
46
+ componentIdPrefix,
47
+ variantTabsIdPrefix,
48
+ storageKeyPrefix,
49
+ variant,
50
+ onVariantChange,
51
+ options,
52
+ rationale,
53
+ renderers,
54
+ brief,
55
+ context,
56
+ mobbin,
57
+ variantsSection,
58
+ variantTabAriaLabel = "variant",
59
+ mobbinGalleryId = "mobbin-inspiration-gallery",
60
+ wrapRoot = true,
61
+ registerOnly = false,
62
+ briefConfigFilePath,
63
+ variantSet,
64
+ }: PrototypeVariantExplorerProps<TVariant>) {
65
+ const review = usePrototypeReviewOptional();
66
+ const slug = usePrototypeSlug();
67
+ const validValues = useMemo(
68
+ () => options.map((option) => option.value),
69
+ [options],
70
+ );
71
+ const {
72
+ effectiveDefault,
73
+ setDefaultVariant,
74
+ } = useDesignExplorationDefault({
75
+ slug,
76
+ storageKeyPrefix,
77
+ validValues,
78
+ });
79
+ const [notesExpanded, setNotesExpanded] = useState(false);
80
+ const [overviewOpen, setOverviewOpen] = useState(false);
81
+ const { copy: copyOverview, icon: copyIcon } = useCopyToClipboard();
82
+ const { value: overviewTitle } = usePersistedLocalString(
83
+ `${storageKeyPrefix}-overview-title`,
84
+ brief.titleDefault,
85
+ );
86
+ const { value: overviewDescription } = usePersistedLocalString(
87
+ `${storageKeyPrefix}-overview-description`,
88
+ brief.descriptionDefault,
89
+ );
90
+
91
+ const fallbackVariant = options[0]?.value;
92
+ const activeRationale =
93
+ rationale[variant] ?? (fallbackVariant ? rationale[fallbackVariant] : undefined);
94
+ const ActiveVariantRenderer =
95
+ renderers[variant] ?? (fallbackVariant ? renderers[fallbackVariant] : null);
96
+ const hideInlineControls = Boolean(review);
97
+
98
+ const briefPanelProps = useMemo(
99
+ () => ({
100
+ componentIdPrefix,
101
+ galleryId: mobbinGalleryId,
102
+ storageKeyPrefix,
103
+ brief,
104
+ context,
105
+ mobbin,
106
+ variantsSection,
107
+ variant,
108
+ onVariantChange,
109
+ options,
110
+ rationale,
111
+ renderers,
112
+ defaultVariantValue: effectiveDefault,
113
+ onSetDefaultVariant: setDefaultVariant,
114
+ briefConfigFilePath,
115
+ }),
116
+ [
117
+ brief,
118
+ briefConfigFilePath,
119
+ componentIdPrefix,
120
+ context,
121
+ effectiveDefault,
122
+ mobbin,
123
+ mobbinGalleryId,
124
+ onVariantChange,
125
+ options,
126
+ rationale,
127
+ renderers,
128
+ setDefaultVariant,
129
+ storageKeyPrefix,
130
+ variant,
131
+ variantsSection,
132
+ ],
133
+ );
134
+
135
+ const sidebarPanel = useMemo(
136
+ () => (
137
+ <PrototypeDesignBriefPanel
138
+ {...briefPanelProps}
139
+ layout="sidebar"
140
+ className="min-h-0"
141
+ />
142
+ ),
143
+ [briefPanelProps],
144
+ );
145
+
146
+ const sidebarRegistrationKey = useMemo(
147
+ () =>
148
+ [
149
+ variant,
150
+ effectiveDefault,
151
+ overviewTitle,
152
+ overviewDescription,
153
+ options.map((option) => option.value).join(","),
154
+ ].join("|"),
155
+ [variant, effectiveDefault, overviewTitle, overviewDescription, options],
156
+ );
157
+
158
+ const variantSetId = variantSet?.id ?? null;
159
+ const hasReview = review != null;
160
+
161
+ useEffect(() => {
162
+ if (!review) return;
163
+
164
+ if (variantSetId) {
165
+ review.registerVariantSidebarContent(variantSetId, sidebarPanel);
166
+ return;
167
+ }
168
+
169
+ review.registerLegacyVariantSidebarContent(sidebarPanel);
170
+ }, [hasReview, review, sidebarPanel, variantSetId]);
171
+
172
+ useEffect(() => {
173
+ if (!review) return;
174
+
175
+ if (variantSetId) {
176
+ review.updateVariantSidebarContent(variantSetId, sidebarPanel);
177
+ return;
178
+ }
179
+
180
+ review.updateLegacyVariantSidebarContent(sidebarPanel);
181
+ // Gate this effect on `sidebarRegistrationKey` (a serialized snapshot of the
182
+ // meaningful sidebar content), NOT on `review` or `sidebarPanel`:
183
+ // - `review` identity changes every time updateVariant*SidebarContent bumps
184
+ // variantSidebarRevision, and `sidebarPanel` is rebuilt on every render
185
+ // (its config objects are not referentially stable). Depending on either
186
+ // re-triggers this effect, which bumps the revision again -> infinite loop.
187
+ // - The closure still reads the latest `sidebarPanel`, and the key changes
188
+ // whenever the rendered content actually changes, so fresh content is
189
+ // always pushed.
190
+ // eslint-disable-next-line react-hooks/exhaustive-deps
191
+ }, [hasReview, variantSetId, sidebarRegistrationKey]);
192
+
193
+ useEffect(() => {
194
+ if (!review || variantSetId) return;
195
+
196
+ review.setLegacyVariantsAvailable(true);
197
+ return () => review.setLegacyVariantsAvailable(false);
198
+ }, [hasReview, variantSetId]);
199
+
200
+ const controls = useMemo(
201
+ () => (
202
+ <div className="relative">
203
+ <PrototypeControlGroup className="flex justify-end">
204
+ <div className="relative">
205
+ <div className="flex flex-wrap items-center gap-2">
206
+ <PrototypeVariantTabs
207
+ idPrefix={variantTabsIdPrefix}
208
+ options={options}
209
+ value={variant}
210
+ onValueChange={onVariantChange}
211
+ keyboardScopeActive={!overviewOpen}
212
+ variantTabAriaLabel={variantTabAriaLabel}
213
+ defaultVariantValue={effectiveDefault}
214
+ />
215
+
216
+ <div className={OVERVIEW_TOOLBAR_CLASS}>
217
+ <PrototypeComponent id={`${componentIdPrefix}.overview-copy-brief`}>
218
+ <button
219
+ type="button"
220
+ className={OVERVIEW_CONTROL_BUTTON_CLASS}
221
+ onClick={() =>
222
+ copyOverview(
223
+ buildBriefOverviewCopyText(
224
+ overviewTitle.trim() || brief.titleDefault,
225
+ overviewDescription.trim() || brief.descriptionDefault,
226
+ ),
227
+ )
228
+ }
229
+ aria-label="Copy overview"
230
+ >
231
+ {copyIcon}
232
+ </button>
233
+ </PrototypeComponent>
234
+
235
+ <PrototypeComponent id={`${componentIdPrefix}.overview-trigger`}>
236
+ <button
237
+ type="button"
238
+ className={OVERVIEW_CONTROL_BUTTON_CLASS}
239
+ onClick={() => setOverviewOpen(true)}
240
+ aria-label="View all variants"
241
+ >
242
+ <LayoutGrid className="size-3.5" />
243
+ </button>
244
+ </PrototypeComponent>
245
+
246
+ <button
247
+ type="button"
248
+ className={OVERVIEW_CONTROL_BUTTON_CLASS}
249
+ onClick={() => setNotesExpanded((expanded) => !expanded)}
250
+ aria-expanded={notesExpanded}
251
+ aria-controls={`${componentIdPrefix}-notes`}
252
+ aria-label={notesExpanded ? "Hide notes" : "Show notes"}
253
+ >
254
+ {notesExpanded ? (
255
+ <ChevronUp className="size-3.5" />
256
+ ) : (
257
+ <ChevronDown className="size-3.5" />
258
+ )}
259
+ </button>
260
+ </div>
261
+ </div>
262
+
263
+ {notesExpanded && activeRationale ? (
264
+ <PrototypeComponent
265
+ id={`${componentIdPrefix}.rationale`}
266
+ className="border-border bg-muted absolute right-0 top-full z-10 mt-2 max-w-md rounded-lg border px-3 py-2.5 text-left"
267
+ >
268
+ <div
269
+ id={`${componentIdPrefix}-notes`}
270
+ className="flex flex-col gap-2"
271
+ >
272
+ <p className="text-sm leading-relaxed">
273
+ <span className="text-foreground font-medium">Good</span>
274
+ {" · "}
275
+ <span className="text-muted-foreground">
276
+ {activeRationale.good}
277
+ </span>
278
+ </p>
279
+ <p className="text-sm leading-relaxed">
280
+ <span className="text-foreground font-medium">Bad</span>
281
+ {" · "}
282
+ <span className="text-muted-foreground">
283
+ {activeRationale.bad}
284
+ </span>
285
+ </p>
286
+ </div>
287
+ </PrototypeComponent>
288
+ ) : null}
289
+ </div>
290
+ </PrototypeControlGroup>
291
+ </div>
292
+ ),
293
+ [
294
+ activeRationale,
295
+ brief.descriptionDefault,
296
+ brief.titleDefault,
297
+ componentIdPrefix,
298
+ copyIcon,
299
+ copyOverview,
300
+ effectiveDefault,
301
+ notesExpanded,
302
+ onVariantChange,
303
+ options,
304
+ overviewDescription,
305
+ overviewOpen,
306
+ overviewTitle,
307
+ variant,
308
+ variantTabAriaLabel,
309
+ variantTabsIdPrefix,
310
+ ],
311
+ );
312
+
313
+ const inlineControls = wrapRoot ? (
314
+ <PrototypeComponent id={componentIdPrefix}>{controls}</PrototypeComponent>
315
+ ) : (
316
+ controls
317
+ );
318
+
319
+ return (
320
+ <div className="relative">
321
+ {!hideInlineControls ? inlineControls : null}
322
+
323
+ {!hideInlineControls ? (
324
+ <PrototypeDesignBriefModal
325
+ open={overviewOpen}
326
+ onOpenChange={setOverviewOpen}
327
+ componentIdPrefix={componentIdPrefix}
328
+ galleryId={mobbinGalleryId}
329
+ storageKeyPrefix={storageKeyPrefix}
330
+ brief={brief}
331
+ context={context}
332
+ mobbin={mobbin}
333
+ variantsSection={variantsSection}
334
+ variant={variant}
335
+ onVariantChange={onVariantChange}
336
+ options={options}
337
+ rationale={rationale}
338
+ renderers={renderers}
339
+ defaultVariantValue={effectiveDefault}
340
+ onSetDefaultVariant={setDefaultVariant}
341
+ />
342
+ ) : null}
343
+
344
+ {!registerOnly && ActiveVariantRenderer ? ActiveVariantRenderer() : null}
345
+ </div>
346
+ );
347
+ }
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import { Panel, PanelSection } from "@prototype/components/platform-ui/panel";
4
+ import { PrototypeComponent } from "@prototype/components/prototypes/prototype-target";
5
+ import type { DesignExplorationRationale } from "@prototype/lib/prototypes/design-exploration-types";
6
+ import { cn } from "@prototype/lib/utils";
7
+
8
+ type PrototypeVariantRationalePanelProps = {
9
+ componentId: string;
10
+ rationale: DesignExplorationRationale;
11
+ expanded: boolean;
12
+ onExpandedChange: (expanded: boolean) => void;
13
+ layout?: "default" | "sidebar";
14
+ bodyClassName?: string;
15
+ };
16
+
17
+ export function PrototypeVariantRationalePanel({
18
+ componentId,
19
+ rationale,
20
+ expanded,
21
+ onExpandedChange,
22
+ layout = "default",
23
+ bodyClassName,
24
+ }: PrototypeVariantRationalePanelProps) {
25
+ const isSidebar = layout === "sidebar";
26
+
27
+ return (
28
+ <PrototypeComponent id={componentId}>
29
+ <Panel
30
+ className={cn(
31
+ "overflow-hidden rounded-lg bg-muted",
32
+ isSidebar && "rounded-md",
33
+ )}
34
+ >
35
+ <PanelSection
36
+ title="Notes"
37
+ expanded={expanded}
38
+ onExpandedChange={onExpandedChange}
39
+ headerClassName={isSidebar ? "px-2.5 py-2 normal-case" : undefined}
40
+ className="border-t-0"
41
+ >
42
+ <p className={cn("leading-relaxed", bodyClassName)}>
43
+ {rationale.good} {rationale.bad}
44
+ </p>
45
+ </PanelSection>
46
+ </Panel>
47
+ </PrototypeComponent>
48
+ );
49
+ }
@@ -0,0 +1,207 @@
1
+ "use client";
2
+
3
+ import pillStyles from "@prototype/components/prototypes/prototype-floating-pill.module.scss";
4
+ import { PrototypeComponent } from "@prototype/components/prototypes/prototype-target";
5
+ import { Tabs, TabsList, TabsTrigger } from "@prototype/components/ui/tabs";
6
+ import {
7
+ Tooltip,
8
+ TooltipContent,
9
+ TooltipProvider,
10
+ TooltipTrigger,
11
+ } from "@prototype/components/ui/tooltip";
12
+ import { cn } from "@prototype/lib/utils";
13
+ import {
14
+ getAdjacentDesignExplorationVariant,
15
+ type DesignExplorationVariantOption,
16
+ } from "@prototype/lib/prototypes/design-exploration-types";
17
+ import { useCallback, useEffect, useState } from "react";
18
+
19
+ type PrototypeVariantTabsProps<TVariant extends string> = {
20
+ idPrefix: string;
21
+ options: DesignExplorationVariantOption<TVariant>[];
22
+ value: TVariant;
23
+ onValueChange: (value: TVariant) => void;
24
+ /** When true, ArrowLeft/ArrowRight cycle variants (unless a lightbox is open). */
25
+ keyboardScopeActive?: boolean;
26
+ className?: string;
27
+ /** Disambiguates comment targets when multiple tab strips could mount. */
28
+ surface?: "inline" | "modal";
29
+ variantTabAriaLabel?: string;
30
+ /** Shown with a marker when this option is not the active tab. */
31
+ defaultVariantValue?: TVariant;
32
+ /** Dark styling for the review chrome floating popover. */
33
+ appearance?: "light" | "dark";
34
+ /** Tab trigger content — footer chrome uses labels; modals default to index numbers. */
35
+ tabDisplay?: "index" | "label";
36
+ };
37
+
38
+ function getVariantTabsId(idPrefix: string, surface: "inline" | "modal", segment: string) {
39
+ return surface === "modal"
40
+ ? `${idPrefix}.modal.${segment}`
41
+ : `${idPrefix}.${segment}`;
42
+ }
43
+
44
+ function isTypingTarget(target: EventTarget | null) {
45
+ if (!(target instanceof HTMLElement)) return false;
46
+ const tag = target.tagName;
47
+ return tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable;
48
+ }
49
+
50
+ export function PrototypeVariantTabs<TVariant extends string>({
51
+ idPrefix,
52
+ options,
53
+ value,
54
+ onValueChange,
55
+ keyboardScopeActive = true,
56
+ className,
57
+ surface = "inline",
58
+ variantTabAriaLabel = "variant",
59
+ defaultVariantValue,
60
+ appearance = "dark",
61
+ tabDisplay = "index",
62
+ }: PrototypeVariantTabsProps<TVariant>) {
63
+ const [keyboardNavEngaged, setKeyboardNavEngaged] = useState(false);
64
+
65
+ const goPrev = useCallback(() => {
66
+ onValueChange(getAdjacentDesignExplorationVariant(options, value, "prev"));
67
+ }, [onValueChange, options, value]);
68
+
69
+ const goNext = useCallback(() => {
70
+ onValueChange(getAdjacentDesignExplorationVariant(options, value, "next"));
71
+ }, [onValueChange, options, value]);
72
+
73
+ const engageKeyboardNav = useCallback(() => {
74
+ setKeyboardNavEngaged(true);
75
+ }, []);
76
+
77
+ const handleArrowNavigation = useCallback(
78
+ (direction: "prev" | "next") => {
79
+ if (direction === "prev") {
80
+ goPrev();
81
+ } else {
82
+ goNext();
83
+ }
84
+ },
85
+ [goNext, goPrev],
86
+ );
87
+
88
+ useEffect(() => {
89
+ if (!keyboardScopeActive) {
90
+ setKeyboardNavEngaged(false);
91
+ }
92
+ }, [keyboardScopeActive]);
93
+
94
+ useEffect(() => {
95
+ if (!keyboardScopeActive || !keyboardNavEngaged) return;
96
+
97
+ function handleKeyDown(event: globalThis.KeyboardEvent) {
98
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
99
+ if (isTypingTarget(event.target)) return;
100
+ if (document.querySelector("[data-mobbin-lightbox-open]")) return;
101
+
102
+ event.preventDefault();
103
+ handleArrowNavigation(event.key === "ArrowLeft" ? "prev" : "next");
104
+ }
105
+
106
+ document.addEventListener("keydown", handleKeyDown, true);
107
+ return () => document.removeEventListener("keydown", handleKeyDown, true);
108
+ }, [handleArrowNavigation, keyboardNavEngaged, keyboardScopeActive]);
109
+
110
+ const activeIndex = options.findIndex((option) => option.value === value);
111
+ const rootId = surface === "modal" ? `${idPrefix}.modal` : idPrefix;
112
+ const isDark = appearance === "dark";
113
+ const showLabel = tabDisplay === "label";
114
+
115
+ function getTabTriggerContent(
116
+ option: DesignExplorationVariantOption<TVariant>,
117
+ index: number,
118
+ ) {
119
+ return showLabel ? option.label : index + 1;
120
+ }
121
+
122
+ return (
123
+ <PrototypeComponent
124
+ id={rootId}
125
+ className={cn("min-w-0 shrink-0", className)}
126
+ onPointerDown={engageKeyboardNav}
127
+ >
128
+ <Tabs
129
+ value={value}
130
+ onValueChange={(nextValue) => {
131
+ engageKeyboardNav();
132
+ onValueChange(nextValue as TVariant);
133
+ }}
134
+ className="min-w-0"
135
+ >
136
+ <PrototypeComponent
137
+ id={getVariantTabsId(idPrefix, surface, "list")}
138
+ className="min-w-0 overflow-x-auto"
139
+ >
140
+ <TooltipProvider delayDuration={200}>
141
+ <TabsList
142
+ className={cn(
143
+ "inline-flex w-max",
144
+ isDark && pillStyles.variantsTabsList,
145
+ )}
146
+ >
147
+ {options.map((option, index) => {
148
+ const isDefaultOption =
149
+ defaultVariantValue !== undefined &&
150
+ option.value === defaultVariantValue;
151
+ const isDefaultWhenUnselected =
152
+ isDefaultOption && option.value !== value;
153
+ const tabClassName = cn(
154
+ "shrink-0",
155
+ !showLabel && "min-w-8 tabular-nums",
156
+ showLabel && "max-w-[10rem] truncate px-2.5",
157
+ !isDark && !showLabel && "px-2",
158
+ isDefaultWhenUnselected &&
159
+ (isDark
160
+ ? pillStyles.variantsDefaultTab
161
+ : "bg-muted text-foreground"),
162
+ isDark && pillStyles.variantsTab,
163
+ );
164
+ const tabId = `${idPrefix}-tab-${surface}-${option.value}`;
165
+ const defaultTabAriaLabel = `${variantTabAriaLabel} ${index + 1}: ${option.label}, selected`;
166
+
167
+ if (!isDefaultOption) {
168
+ return (
169
+ <TabsTrigger
170
+ key={option.value}
171
+ value={option.value}
172
+ id={tabId}
173
+ aria-label={`${variantTabAriaLabel} ${index + 1}: ${option.label}`}
174
+ className={tabClassName}
175
+ >
176
+ {getTabTriggerContent(option, index)}
177
+ </TabsTrigger>
178
+ );
179
+ }
180
+
181
+ return (
182
+ <Tooltip key={option.value}>
183
+ <TooltipTrigger asChild>
184
+ <TabsTrigger
185
+ value={option.value}
186
+ id={tabId}
187
+ aria-label={defaultTabAriaLabel}
188
+ className={tabClassName}
189
+ >
190
+ {getTabTriggerContent(option, index)}
191
+ </TabsTrigger>
192
+ </TooltipTrigger>
193
+ <TooltipContent side="top">Selected</TooltipContent>
194
+ </Tooltip>
195
+ );
196
+ })}
197
+ </TabsList>
198
+ </TooltipProvider>
199
+ </PrototypeComponent>
200
+ </Tabs>
201
+
202
+ <span className="text-xs text-muted-foreground sr-only" aria-live="polite">
203
+ {`${activeIndex + 1} of ${options.length}: ${options[activeIndex]?.label ?? ""}`}
204
+ </span>
205
+ </PrototypeComponent>
206
+ );
207
+ }