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,553 @@
1
+ "use client";
2
+
3
+ import { Button } from "@prototype/components/ui/button";
4
+ import { Label } from "@prototype/components/ui/label";
5
+ import { OptionSelect } from "@prototype/components/ui/option-select";
6
+ import { ScrollArea } from "@prototype/components/ui/scroll-area";
7
+ import { Slider } from "@prototype/components/ui/slider";
8
+ import { Switch } from "@prototype/components/ui/switch";
9
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@prototype/components/ui/tabs";
10
+ import { ToggleGroup, ToggleGroupItem } from "@prototype/components/ui/toggle-group";
11
+ import {
12
+ getPrototypeShareCommand,
13
+ getPrototypeSlugFromPathname,
14
+ } from "@prototype/lib/prototypes/share-command";
15
+ import { cn } from "@prototype/lib/utils";
16
+ import {
17
+ ArrowLeft,
18
+ Copy,
19
+ Info,
20
+ SlidersHorizontal,
21
+ Terminal,
22
+ } from "lucide-react";
23
+ import Link from "next/link";
24
+ import { usePathname } from "next/navigation";
25
+ import {
26
+ Children,
27
+ isValidElement,
28
+ useEffect,
29
+ useId,
30
+ useLayoutEffect,
31
+ useMemo,
32
+ useRef,
33
+ useState,
34
+ type ReactElement,
35
+ type ReactNode,
36
+ } from "react";
37
+ import { toast } from "sonner";
38
+ import { usePrototypeReviewOptional } from "@prototype/lib/prototypes/prototype-review-context";
39
+ import { PrototypeReferenceDocs } from "./prototype-reference-docs";
40
+
41
+ type PrototypeControlsTabProps = {
42
+ value: string;
43
+ label: string;
44
+ children: ReactNode;
45
+ };
46
+
47
+ type PrototypeControlsFieldProps = {
48
+ label: string;
49
+ htmlFor?: string;
50
+ description?: string;
51
+ children: ReactNode;
52
+ className?: string;
53
+ };
54
+
55
+ type PrototypeControlsToggleProps = {
56
+ label: string;
57
+ checked: boolean;
58
+ onCheckedChange: (checked: boolean) => void;
59
+ disabled?: boolean;
60
+ id?: string;
61
+ };
62
+
63
+ type PrototypeControlsSelectProps<T extends string = string> = {
64
+ label: string;
65
+ value: T;
66
+ onValueChange: (value: T) => void;
67
+ options: { value: T; label: string }[];
68
+ disabled?: boolean;
69
+ };
70
+
71
+ type PrototypeControlsSliderProps = {
72
+ label: string;
73
+ value: number[];
74
+ onValueChange: (value: number[]) => void;
75
+ min?: number;
76
+ max?: number;
77
+ step?: number;
78
+ formatValue?: (value: number) => string;
79
+ disabled?: boolean;
80
+ };
81
+
82
+ type PrototypeControlsDropdownProps<T extends string = string> = {
83
+ label: string;
84
+ value: T;
85
+ onValueChange: (value: T) => void;
86
+ options: { value: T; label: string }[];
87
+ disabled?: boolean;
88
+ };
89
+
90
+ type PrototypeControlsToggleGroupProps<T extends string = string> = {
91
+ label: string;
92
+ type?: "single" | "multiple";
93
+ value?: T | T[];
94
+ onValueChange?: (value: T | T[]) => void;
95
+ options: { value: T; label: string }[];
96
+ disabled?: boolean;
97
+ };
98
+
99
+ function PrototypeControlsTab(_props: PrototypeControlsTabProps) {
100
+ return null;
101
+ }
102
+
103
+ PrototypeControlsTab.displayName = "PrototypeControlsTab";
104
+
105
+ function isPrototypeControlsTab(
106
+ child: ReactNode,
107
+ ): child is ReactElement<PrototypeControlsTabProps> {
108
+ return (
109
+ isValidElement(child) &&
110
+ (child.type as { displayName?: string }).displayName ===
111
+ "PrototypeControlsTab"
112
+ );
113
+ }
114
+
115
+ function PrototypeControlsField({
116
+ label,
117
+ htmlFor,
118
+ description,
119
+ children,
120
+ className,
121
+ }: PrototypeControlsFieldProps) {
122
+ return (
123
+ <div className={cn("flex flex-col gap-1.5", className)}>
124
+ <div className="flex items-center justify-between gap-3">
125
+ <Label htmlFor={htmlFor} className="text-foreground">
126
+ {label}
127
+ </Label>
128
+ </div>
129
+ {description ? (
130
+ <p className="text-xs text-muted-foreground -mt-0.5">{description}</p>
131
+ ) : null}
132
+ {children}
133
+ </div>
134
+ );
135
+ }
136
+
137
+ function PrototypeControlsToggle({
138
+ label,
139
+ checked,
140
+ onCheckedChange,
141
+ disabled,
142
+ id,
143
+ }: PrototypeControlsToggleProps) {
144
+ const generatedId = useId();
145
+ const fieldId = id ?? generatedId;
146
+
147
+ return (
148
+ <div className="flex items-center justify-between gap-3">
149
+ <Label htmlFor={fieldId} className="text-foreground">
150
+ {label}
151
+ </Label>
152
+ <Switch
153
+ id={fieldId}
154
+ checked={checked}
155
+ onCheckedChange={onCheckedChange}
156
+ disabled={disabled}
157
+ />
158
+ </div>
159
+ );
160
+ }
161
+
162
+ function PrototypeControlsSelect<T extends string>({
163
+ label,
164
+ value,
165
+ onValueChange,
166
+ options,
167
+ disabled,
168
+ }: PrototypeControlsSelectProps<T>) {
169
+ return (
170
+ <PrototypeControlsField label={label}>
171
+ <OptionSelect
172
+ value={value}
173
+ onValueChange={onValueChange}
174
+ options={options}
175
+ disabled={disabled}
176
+ triggerClassName="w-full"
177
+ />
178
+ </PrototypeControlsField>
179
+ );
180
+ }
181
+
182
+ function PrototypeControlsSlider({
183
+ label,
184
+ value,
185
+ onValueChange,
186
+ min = 0,
187
+ max = 100,
188
+ step = 1,
189
+ formatValue = (nextValue) => String(nextValue),
190
+ disabled,
191
+ }: PrototypeControlsSliderProps) {
192
+ const currentValue = value[0] ?? min;
193
+
194
+ return (
195
+ <PrototypeControlsField label={label}>
196
+ <div className="flex items-center gap-3">
197
+ <Slider
198
+ value={value}
199
+ onValueChange={onValueChange}
200
+ min={min}
201
+ max={max}
202
+ step={step}
203
+ disabled={disabled}
204
+ className="flex-1"
205
+ />
206
+ <span className="text-xs text-muted-foreground tabular-nums">
207
+ {formatValue(currentValue)}
208
+ </span>
209
+ </div>
210
+ </PrototypeControlsField>
211
+ );
212
+ }
213
+
214
+ function PrototypeControlsDropdown<T extends string>({
215
+ label,
216
+ value,
217
+ onValueChange,
218
+ options,
219
+ disabled,
220
+ }: PrototypeControlsDropdownProps<T>) {
221
+ return (
222
+ <PrototypeControlsField label={label}>
223
+ <OptionSelect
224
+ value={value}
225
+ onValueChange={onValueChange}
226
+ options={options}
227
+ disabled={disabled}
228
+ triggerClassName="w-full"
229
+ />
230
+ </PrototypeControlsField>
231
+ );
232
+ }
233
+
234
+ function PrototypeControlsToggleGroup<T extends string>({
235
+ label,
236
+ type = "single",
237
+ value,
238
+ onValueChange,
239
+ options,
240
+ disabled,
241
+ }: PrototypeControlsToggleGroupProps<T>) {
242
+ const items = options.map((option) => (
243
+ <ToggleGroupItem
244
+ key={option.value}
245
+ value={option.value}
246
+ className="flex-1"
247
+ >
248
+ {option.label}
249
+ </ToggleGroupItem>
250
+ ));
251
+
252
+ return (
253
+ <PrototypeControlsField label={label}>
254
+ {type === "multiple" ? (
255
+ <ToggleGroup
256
+ type="multiple"
257
+ variant="outline"
258
+ value={value as T[] | undefined}
259
+ onValueChange={(nextValue) => {
260
+ onValueChange?.(nextValue as T[]);
261
+ }}
262
+ disabled={disabled}
263
+ className="w-full"
264
+ >
265
+ {items}
266
+ </ToggleGroup>
267
+ ) : (
268
+ <ToggleGroup
269
+ type="single"
270
+ variant="outline"
271
+ value={value as T | undefined}
272
+ onValueChange={(nextValue) => {
273
+ onValueChange?.(nextValue as T);
274
+ }}
275
+ disabled={disabled}
276
+ className="w-full"
277
+ >
278
+ {items}
279
+ </ToggleGroup>
280
+ )}
281
+ </PrototypeControlsField>
282
+ );
283
+ }
284
+
285
+ type PrototypeShareCommandButtonProps = {
286
+ command: string;
287
+ };
288
+
289
+ export function PrototypeShareCommandButton({
290
+ command,
291
+ }: PrototypeShareCommandButtonProps) {
292
+ return (
293
+ <button
294
+ type="button"
295
+ aria-label={`Copy share command: ${command}`}
296
+ className={cn(
297
+ "group bg-muted border-border w-full rounded-md border p-2.5 text-left",
298
+ "ring-focus-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
299
+ "transition-colors duration-200 ease hover:bg-muted",
300
+ )}
301
+ onClick={async () => {
302
+ try {
303
+ await navigator.clipboard.writeText(command);
304
+ toast.success("Command copied", {
305
+ description: "Paste in your terminal to push and get a preview link.",
306
+ duration: 3000,
307
+ });
308
+ } catch {
309
+ toast.error("Could not copy to clipboard");
310
+ }
311
+ }}
312
+ >
313
+ <div className="mb-1.5 flex items-center justify-between gap-2">
314
+ <span className="text-xs text-muted-foreground flex items-center gap-1.5 uppercase tracking-wide">
315
+ <Terminal className="size-3 shrink-0" aria-hidden />
316
+ Share
317
+ </span>
318
+ <span className="text-xs text-muted-foreground flex items-center gap-1 transition-colors duration-200 ease group-hover:text-muted-foreground">
319
+ <Copy className="size-3 shrink-0" aria-hidden />
320
+ Copy
321
+ </span>
322
+ </div>
323
+ <p className="text-xs text-foreground overflow-x-auto font-mono leading-relaxed whitespace-nowrap">
324
+ <span className="text-success select-none">$ </span>
325
+ {command}
326
+ </p>
327
+ </button>
328
+ );
329
+ }
330
+
331
+ type PrototypeControlsProps = {
332
+ children: ReactNode;
333
+ defaultOpen?: boolean;
334
+ className?: string;
335
+ slug?: string;
336
+ };
337
+
338
+ type PrototypeControlsPanelProps = {
339
+ children: ReactNode;
340
+ tabs: Array<{ value: string; label: string; content: ReactNode }>;
341
+ hasTabs: boolean;
342
+ };
343
+
344
+ function PrototypeControlsPanel({
345
+ children,
346
+ tabs,
347
+ hasTabs,
348
+ }: PrototypeControlsPanelProps) {
349
+ const defaultTab = tabs[0]?.value;
350
+
351
+ return hasTabs ? (
352
+ <Tabs defaultValue={defaultTab} className="gap-0">
353
+ {tabs.length > 1 ? (
354
+ <div className="border-border border-b px-0 pb-2">
355
+ <TabsList className="w-full">
356
+ {tabs.map((tab) => (
357
+ <TabsTrigger key={tab.value} value={tab.value}>
358
+ {tab.label}
359
+ </TabsTrigger>
360
+ ))}
361
+ </TabsList>
362
+ </div>
363
+ ) : null}
364
+
365
+ <ScrollArea className="max-h-[min(32rem,calc(100svh-12rem))]">
366
+ {tabs.map((tab) => (
367
+ <TabsContent
368
+ key={tab.value}
369
+ value={tab.value}
370
+ className="mt-0 px-0 py-3"
371
+ >
372
+ <div className="flex flex-col gap-4">{tab.content}</div>
373
+ </TabsContent>
374
+ ))}
375
+ </ScrollArea>
376
+ </Tabs>
377
+ ) : (
378
+ <div className="flex flex-col gap-4">{children}</div>
379
+ );
380
+ }
381
+
382
+ type PrototypeControlsActionsProps = {
383
+ shareCommand: string | null;
384
+ slug?: string | null;
385
+ className?: string;
386
+ };
387
+
388
+ export function PrototypeGalleryBackButton({ className }: { className?: string }) {
389
+ return (
390
+ <Button
391
+ variant="secondary"
392
+ size="icon"
393
+ aria-label="Back to gallery"
394
+ className={cn("rounded-md", className)}
395
+ asChild
396
+ >
397
+ <Link href="/">
398
+ <ArrowLeft size={16} strokeWidth={2} />
399
+ </Link>
400
+ </Button>
401
+ );
402
+ }
403
+
404
+ export function PrototypeControlsActions({
405
+ shareCommand,
406
+ slug,
407
+ className,
408
+ }: PrototypeControlsActionsProps) {
409
+ return (
410
+ <div className={cn("flex flex-col gap-4", className)}>
411
+ {shareCommand ? (
412
+ <PrototypeShareCommandButton command={shareCommand} />
413
+ ) : null}
414
+ {slug ? <PrototypeReferenceDocs slug={slug} /> : null}
415
+ </div>
416
+ );
417
+ }
418
+
419
+ function PrototypeControlsRoot({
420
+ children,
421
+ defaultOpen = false,
422
+ className,
423
+ slug: slugProp,
424
+ }: PrototypeControlsProps) {
425
+ const pathname = usePathname();
426
+ const review = usePrototypeReviewOptional();
427
+ const [tweaksOpen, setTweaksOpen] = useState(defaultOpen);
428
+ const [settingsOpen, setSettingsOpen] = useState(false);
429
+ const slug = slugProp ?? getPrototypeSlugFromPathname(pathname);
430
+ const shareCommand = slug ? getPrototypeShareCommand(slug) : null;
431
+
432
+ const tabs = useMemo(() => {
433
+ return Children.toArray(children)
434
+ .filter(isPrototypeControlsTab)
435
+ .map((child) => ({
436
+ value: child.props.value,
437
+ label: child.props.label,
438
+ content: child.props.children,
439
+ }));
440
+ }, [children]);
441
+
442
+ const hasTabs = tabs.length > 0;
443
+
444
+ const setTweaksContent = review?.setTweaksContent;
445
+ const setTweaksContentRef = useRef(setTweaksContent);
446
+ setTweaksContentRef.current = setTweaksContent;
447
+
448
+ useLayoutEffect(() => {
449
+ setTweaksContentRef.current?.(children);
450
+ }, [children]);
451
+
452
+ useEffect(() => {
453
+ return () => setTweaksContentRef.current?.(null);
454
+ }, []);
455
+
456
+ if (review) {
457
+ return null;
458
+ }
459
+
460
+ const toggleTweaks = () => {
461
+ setTweaksOpen((current) => {
462
+ const next = !current;
463
+ if (next) setSettingsOpen(false);
464
+ return next;
465
+ });
466
+ };
467
+
468
+ const toggleSettings = () => {
469
+ setSettingsOpen((current) => {
470
+ const next = !current;
471
+ if (next) setTweaksOpen(false);
472
+ return next;
473
+ });
474
+ };
475
+
476
+ return (
477
+ <div
478
+ data-prototype-controls
479
+ className={cn(
480
+ "pointer-events-none fixed right-4 bottom-4 z-50 flex flex-col items-end gap-2",
481
+ className,
482
+ )}
483
+ >
484
+ <div
485
+ className={cn(
486
+ "pointer-events-auto transition-opacity duration-150 ease-out motion-reduce:transition-none",
487
+ tweaksOpen
488
+ ? "opacity-100"
489
+ : "pointer-events-none opacity-0",
490
+ )}
491
+ aria-hidden={!tweaksOpen}
492
+ >
493
+ <div className="border-border bg-card ring-border flex w-[min(20rem,calc(100vw-2rem))] flex-col overflow-hidden rounded-xl shadow-lg ring-[0.5px]">
494
+ <div className="px-3 py-3">
495
+ <PrototypeControlsPanel tabs={tabs} hasTabs={hasTabs}>
496
+ {children}
497
+ </PrototypeControlsPanel>
498
+ </div>
499
+ </div>
500
+ </div>
501
+
502
+ <div
503
+ className={cn(
504
+ "pointer-events-auto transition-opacity duration-150 ease-out motion-reduce:transition-none",
505
+ settingsOpen
506
+ ? "opacity-100"
507
+ : "pointer-events-none opacity-0",
508
+ )}
509
+ aria-hidden={!settingsOpen}
510
+ >
511
+ <div className="border-border bg-card ring-border flex w-[min(20rem,calc(100vw-2rem))] flex-col overflow-hidden rounded-xl p-3 shadow-lg ring-[0.5px]">
512
+ <PrototypeControlsActions shareCommand={shareCommand} slug={slug} />
513
+ </div>
514
+ </div>
515
+
516
+ <div className="bg-card ring-border pointer-events-auto flex items-center gap-1.5 rounded-lg p-1.5 shadow-md ring-[0.5px]">
517
+ <PrototypeGalleryBackButton />
518
+ <Button
519
+ type="button"
520
+ variant="secondary"
521
+ size="icon"
522
+ aria-expanded={tweaksOpen}
523
+ aria-label={tweaksOpen ? "Close controls" : "Open controls"}
524
+ className={cn("rounded-md", tweaksOpen && "bg-muted")}
525
+ onClick={toggleTweaks}
526
+ >
527
+ <SlidersHorizontal />
528
+ </Button>
529
+ <Button
530
+ type="button"
531
+ variant="secondary"
532
+ size="icon"
533
+ aria-expanded={settingsOpen}
534
+ aria-label={settingsOpen ? "Close info" : "Open info"}
535
+ className={cn("rounded-md", settingsOpen && "bg-muted")}
536
+ onClick={toggleSettings}
537
+ >
538
+ <Info />
539
+ </Button>
540
+ </div>
541
+ </div>
542
+ );
543
+ }
544
+
545
+ export const PrototypeControls = Object.assign(PrototypeControlsRoot, {
546
+ Tab: PrototypeControlsTab,
547
+ Field: PrototypeControlsField,
548
+ Toggle: PrototypeControlsToggle,
549
+ Select: PrototypeControlsSelect,
550
+ Slider: PrototypeControlsSlider,
551
+ Dropdown: PrototypeControlsDropdown,
552
+ ToggleGroup: PrototypeControlsToggleGroup,
553
+ });
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import {
4
+ Dialog,
5
+ DialogClose,
6
+ DialogContent,
7
+ DialogTitle,
8
+ } from "@prototype/components/ui/dialog";
9
+ import { buildCreateDesignExplorationCopyText } from "@prototype/lib/prototypes/design-exploration-types";
10
+ import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
11
+ import { X } from "lucide-react";
12
+ import { useEffect, useState } from "react";
13
+
14
+ const MODAL_CHROME_BUTTON_CLASS =
15
+ "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 duration-200 ease 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";
16
+
17
+ const BRIEF_TEXTAREA_CLASS =
18
+ "field-sizing-content min-h-[min(50vh,20rem)] w-full resize-y border-0 bg-transparent px-0 py-0 text-sm leading-relaxed text-foreground shadow-none outline-none placeholder:text-muted-foreground focus-visible:ring-0";
19
+
20
+ export type PrototypeCreateExplorationModalProps = {
21
+ open: boolean;
22
+ onOpenChange: (open: boolean) => void;
23
+ slug: string;
24
+ existingExplorations: Array<{ id: string; label: string }>;
25
+ };
26
+
27
+ export function PrototypeCreateExplorationModal({
28
+ open,
29
+ onOpenChange,
30
+ slug,
31
+ existingExplorations,
32
+ }: PrototypeCreateExplorationModalProps) {
33
+ const [brief, setBrief] = useState("");
34
+ const { copy, icon, isCopied } = useCopyToClipboard();
35
+ const canCopy = brief.trim().length > 0;
36
+
37
+ useEffect(() => {
38
+ if (!open) {
39
+ setBrief("");
40
+ }
41
+ }, [open]);
42
+
43
+ const handleCopy = () => {
44
+ if (!canCopy) return;
45
+
46
+ const origin =
47
+ typeof window !== "undefined" ? window.location.origin : undefined;
48
+ copy(
49
+ buildCreateDesignExplorationCopyText({
50
+ slug,
51
+ existingExplorations,
52
+ origin,
53
+ briefText: brief,
54
+ }),
55
+ );
56
+ };
57
+
58
+ return (
59
+ <Dialog open={open} onOpenChange={onOpenChange}>
60
+ <DialogContent
61
+ showCloseButton={false}
62
+ className="tool-dialog-surface flex max-h-[min(92svh,calc(100%-2rem))] w-[min(42rem,calc(100vw-2rem))] max-w-[min(42rem,calc(100vw-2rem))] flex-col gap-0 overflow-hidden border-0 p-0 sm:max-w-[min(42rem,calc(100vw-2rem))]"
63
+ >
64
+ <div className="flex items-start justify-between gap-3 px-5 py-4">
65
+ <div className="flex min-w-0 flex-col gap-1">
66
+ <DialogTitle className="text-base font-medium text-foreground">
67
+ New exploration
68
+ </DialogTitle>
69
+ <p className="text-sm text-muted-foreground">
70
+ Describe what you&apos;re trying to build, and we&apos;ll give you
71
+ a few different versions.
72
+ </p>
73
+ </div>
74
+ <DialogClose className={MODAL_CHROME_BUTTON_CLASS}>
75
+ <X />
76
+ <span className="sr-only">Close</span>
77
+ </DialogClose>
78
+ </div>
79
+
80
+ <div
81
+ className="h-px w-full shrink-0 bg-border"
82
+ role="separator"
83
+ aria-hidden
84
+ />
85
+
86
+ <div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-4">
87
+ <textarea
88
+ value={brief}
89
+ onChange={(event) => setBrief(event.target.value)}
90
+ placeholder="e.g. Give me a settings page with a profile picture, name, plan, and bio"
91
+ aria-label="Exploration brief"
92
+ className={BRIEF_TEXTAREA_CLASS}
93
+ />
94
+ </div>
95
+
96
+ <div
97
+ className="h-px w-full shrink-0 bg-border"
98
+ role="separator"
99
+ aria-hidden
100
+ />
101
+
102
+ <div className="flex items-center justify-end gap-2 px-5 py-4">
103
+ <button
104
+ type="button"
105
+ disabled={!canCopy}
106
+ onClick={handleCopy}
107
+ className="inline-flex h-9 items-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors duration-200 ease hover:bg-primary-dark disabled:pointer-events-none disabled:opacity-50"
108
+ aria-label="Copy exploration brief and prompt"
109
+ >
110
+ {isCopied ? "Copied" : "Copy prompt"}
111
+ {icon}
112
+ </button>
113
+ </div>
114
+ </DialogContent>
115
+ </Dialog>
116
+ );
117
+ }