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,330 @@
1
+ "use client";
2
+
3
+ import { PrototypeComponent } from "@prototype/components/prototypes/prototype-target";
4
+ import { Button } from "@prototype/components/ui/button";
5
+ import { cn } from "@prototype/lib/utils";
6
+ import type { MobbinReference } from "@prototype/lib/prototypes/design-exploration-types";
7
+ import { ChevronLeft, ChevronRight, ExternalLink, X, ZoomIn } from "lucide-react";
8
+ import { useCallback, useEffect, useState } from "react";
9
+
10
+ const GALLERY_SECTION_TITLE_CLASS = "tool-chip tool-chip-pink";
11
+
12
+ const GALLERY_SECTION_DESC_CLASS = "tool-body max-w-2xl";
13
+
14
+ const GALLERY_CARD_TITLE_CLASS = "text-sm font-medium text-foreground";
15
+
16
+ const GALLERY_CARD_BODY_CLASS = "tool-body";
17
+
18
+ type PrototypeMobbinGalleryLayout = "default" | "sidebar";
19
+
20
+ type PrototypeMobbinGalleryProps = {
21
+ componentIdPrefix: string;
22
+ galleryId: string;
23
+ references: MobbinReference[];
24
+ title?: string;
25
+ description?: string;
26
+ imagePathForReference?: (id: string) => string;
27
+ layout?: PrototypeMobbinGalleryLayout;
28
+ };
29
+
30
+ function resolveReferenceImageUrl(
31
+ reference: MobbinReference,
32
+ imagePathForReference?: (id: string) => string,
33
+ ) {
34
+ return imagePathForReference?.(reference.id) ?? reference.imageUrl;
35
+ }
36
+
37
+ function MobbinReferenceCard({
38
+ reference,
39
+ imageUrl,
40
+ componentId,
41
+ onOpen,
42
+ compact = false,
43
+ }: {
44
+ reference: MobbinReference;
45
+ imageUrl: string;
46
+ componentId: string;
47
+ onOpen: () => void;
48
+ compact?: boolean;
49
+ }) {
50
+ const cardTitleClass = compact
51
+ ? "text-sm text-foreground font-medium"
52
+ : GALLERY_CARD_TITLE_CLASS;
53
+ const cardBodyClass = compact
54
+ ? "text-sm text-muted-foreground leading-relaxed"
55
+ : GALLERY_CARD_BODY_CLASS;
56
+
57
+ return (
58
+ <PrototypeComponent id={componentId} className="flex flex-col gap-2">
59
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-0.5">
60
+ <p className={cardTitleClass}>{reference.appName}</p>
61
+ {reference.variantHint ? (
62
+ <span className="text-sm text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[13px]">
63
+ {reference.variantHint}
64
+ </span>
65
+ ) : null}
66
+ </div>
67
+
68
+ <button
69
+ type="button"
70
+ onClick={onOpen}
71
+ className={cn(
72
+ "bg-card ring-border group relative block w-full cursor-zoom-in overflow-hidden rounded-xl text-left shadow-lg ring-[0.5px]",
73
+ "focus-visible:ring-border focus-visible:outline-none focus-visible:ring-2",
74
+ )}
75
+ >
76
+ {/* eslint-disable-next-line @next/next/no-img-element -- Mobbin CDN URLs are ephemeral; local assets preferred when available. */}
77
+ <img
78
+ src={imageUrl}
79
+ alt={`${reference.appName} reference on Mobbin`}
80
+ className="aspect-[16/10] w-full object-cover object-top transition-opacity duration-200 ease group-hover:opacity-90"
81
+ loading="lazy"
82
+ />
83
+ <span className="bg-background/90 text-muted-foreground text-sm absolute right-2 top-2 inline-flex items-center gap-1 rounded-md px-2 py-1 opacity-0 backdrop-blur-sm transition-opacity duration-200 ease group-hover:opacity-100">
84
+ <ZoomIn className="size-3" />
85
+ View larger
86
+ </span>
87
+ </button>
88
+
89
+ <div className="px-0.5">
90
+ <p className={cardBodyClass}>{reference.relevance}</p>
91
+ </div>
92
+ </PrototypeComponent>
93
+ );
94
+ }
95
+
96
+ function MobbinReferenceLightbox({
97
+ references,
98
+ imageUrls,
99
+ activeIndex,
100
+ onClose,
101
+ onNavigate,
102
+ galleryId,
103
+ }: {
104
+ references: MobbinReference[];
105
+ imageUrls: string[];
106
+ activeIndex: number;
107
+ onClose: () => void;
108
+ onNavigate: (index: number) => void;
109
+ galleryId: string;
110
+ }) {
111
+ const reference = references[activeIndex];
112
+ const imageUrl = imageUrls[activeIndex];
113
+ const total = references.length;
114
+ const hasPrev = activeIndex > 0;
115
+ const hasNext = activeIndex < total - 1;
116
+
117
+ const goPrev = useCallback(() => {
118
+ if (hasPrev) onNavigate(activeIndex - 1);
119
+ }, [activeIndex, hasPrev, onNavigate]);
120
+
121
+ const goNext = useCallback(() => {
122
+ if (hasNext) onNavigate(activeIndex + 1);
123
+ }, [activeIndex, hasNext, onNavigate]);
124
+
125
+ useEffect(() => {
126
+ function handleKeyDown(event: KeyboardEvent) {
127
+ if (event.key === "Escape") {
128
+ onClose();
129
+ } else if (event.key === "ArrowLeft") {
130
+ goPrev();
131
+ } else if (event.key === "ArrowRight") {
132
+ goNext();
133
+ }
134
+ }
135
+
136
+ window.addEventListener("keydown", handleKeyDown);
137
+ return () => window.removeEventListener("keydown", handleKeyDown);
138
+ }, [goNext, goPrev, onClose]);
139
+
140
+ useEffect(() => {
141
+ const previousOverflow = document.body.style.overflow;
142
+ document.body.style.overflow = "hidden";
143
+ return () => {
144
+ document.body.style.overflow = previousOverflow;
145
+ };
146
+ }, []);
147
+
148
+ return (
149
+ <PrototypeComponent
150
+ id={`${galleryId}.lightbox`}
151
+ className="fixed inset-0 z-(--z-modal) flex items-center justify-center p-4 sm:p-8"
152
+ role="dialog"
153
+ aria-modal="true"
154
+ aria-label={`${reference.appName} Mobbin reference`}
155
+ data-mobbin-lightbox-open=""
156
+ >
157
+ <button
158
+ type="button"
159
+ aria-label="Close lightbox"
160
+ className="bg-muted/70 absolute inset-0 backdrop-blur-[6px] transition-opacity duration-200 ease-(--ease-out-cubic) motion-reduce:transition-none"
161
+ onClick={onClose}
162
+ />
163
+
164
+ <div className="pointer-events-none relative z-10 flex max-h-full w-full max-w-6xl flex-col gap-4">
165
+ <div className="pointer-events-auto flex items-center justify-between gap-4">
166
+ <p className="text-sm text-muted-foreground">
167
+ {activeIndex + 1} of {total}
168
+ </p>
169
+ <PrototypeComponent id={`${galleryId}.lightbox.close`}>
170
+ <Button
171
+ type="button"
172
+ variant="secondary"
173
+ size="icon"
174
+ aria-label="Close"
175
+ onClick={onClose}
176
+ >
177
+ <X />
178
+ </Button>
179
+ </PrototypeComponent>
180
+ </div>
181
+
182
+ <div className="relative flex min-h-0 flex-1 items-center justify-center">
183
+ {hasPrev ? (
184
+ <PrototypeComponent
185
+ id={`${galleryId}.lightbox.prev`}
186
+ className="pointer-events-auto absolute left-0 z-10 -translate-x-1/2 sm:-translate-x-full sm:pr-3"
187
+ >
188
+ <Button
189
+ type="button"
190
+ variant="secondary"
191
+ size="icon"
192
+ aria-label="Previous reference"
193
+ onClick={goPrev}
194
+ >
195
+ <ChevronLeft />
196
+ </Button>
197
+ </PrototypeComponent>
198
+ ) : null}
199
+
200
+ <div className="bg-card ring-border pointer-events-auto max-h-[min(75vh,900px)] w-full overflow-hidden rounded-xl shadow-lg ring-[0.5px]">
201
+ {/* eslint-disable-next-line @next/next/no-img-element -- Mobbin CDN URLs are ephemeral; local assets preferred when available. */}
202
+ <img
203
+ src={imageUrl}
204
+ alt={`${reference.appName} reference on Mobbin`}
205
+ className="max-h-[min(75vh,900px)] w-full object-contain object-top"
206
+ />
207
+ </div>
208
+
209
+ {hasNext ? (
210
+ <PrototypeComponent
211
+ id={`${galleryId}.lightbox.next`}
212
+ className="pointer-events-auto absolute right-0 z-10 translate-x-1/2 sm:translate-x-full sm:pl-3"
213
+ >
214
+ <Button
215
+ type="button"
216
+ variant="secondary"
217
+ size="icon"
218
+ aria-label="Next reference"
219
+ onClick={goNext}
220
+ >
221
+ <ChevronRight />
222
+ </Button>
223
+ </PrototypeComponent>
224
+ ) : null}
225
+ </div>
226
+
227
+ <div className="bg-card/95 ring-border pointer-events-auto rounded-xl px-4 py-3 shadow-lg ring-[0.5px] backdrop-blur-sm sm:px-5 sm:py-4">
228
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
229
+ <div className="flex min-w-0 flex-col gap-1">
230
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
231
+ <p className={GALLERY_CARD_TITLE_CLASS}>{reference.appName}</p>
232
+ {reference.variantHint ? (
233
+ <span className="text-sm text-muted-foreground bg-muted rounded px-1.5 py-0.5">
234
+ {reference.variantHint}
235
+ </span>
236
+ ) : null}
237
+ </div>
238
+ <p className={GALLERY_CARD_BODY_CLASS}>{reference.relevance}</p>
239
+ </div>
240
+ <a
241
+ href={reference.mobbinUrl}
242
+ target="_blank"
243
+ rel="noopener noreferrer"
244
+ className={cn(
245
+ GALLERY_CARD_BODY_CLASS,
246
+ "hover:text-foreground inline-flex shrink-0 items-center gap-1.5 transition-colors duration-200 ease",
247
+ )}
248
+ >
249
+ View on Mobbin
250
+ <ExternalLink className="size-3.5" />
251
+ </a>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </PrototypeComponent>
256
+ );
257
+ }
258
+
259
+ export function PrototypeMobbinGallery({
260
+ componentIdPrefix,
261
+ galleryId,
262
+ references,
263
+ title = "Mobbin references",
264
+ description = "Reference screens from B2B and developer dashboards.",
265
+ imagePathForReference,
266
+ layout = "default",
267
+ }: PrototypeMobbinGalleryProps) {
268
+ const isSidebar = layout === "sidebar";
269
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
270
+ const imageUrls = references.map((reference) =>
271
+ resolveReferenceImageUrl(reference, imagePathForReference),
272
+ );
273
+
274
+ const sectionTitleClass = isSidebar
275
+ ? "text-xs font-medium text-muted-foreground uppercase tracking-wide"
276
+ : GALLERY_SECTION_TITLE_CLASS;
277
+ const sectionDescClass = isSidebar
278
+ ? "text-sm text-muted-foreground leading-relaxed"
279
+ : GALLERY_SECTION_DESC_CLASS;
280
+
281
+ return (
282
+ <PrototypeComponent id={galleryId}>
283
+ <PrototypeComponent
284
+ id={`${componentIdPrefix}.mobbin-references`}
285
+ className={cn(
286
+ isSidebar
287
+ ? "flex flex-col gap-3"
288
+ : "border-border border-t px-6 py-8",
289
+ )}
290
+ >
291
+ <div className={cn("flex flex-col gap-2", !isSidebar && "mb-6")}>
292
+ <p className={sectionTitleClass}>{title}</p>
293
+ {!isSidebar && <p className={sectionDescClass}>{description}</p>}
294
+ </div>
295
+
296
+ <div
297
+ className={cn(
298
+ "grid grid-cols-1",
299
+ isSidebar ? "gap-4" : "gap-10",
300
+ !isSidebar &&
301
+ references.length > 1 &&
302
+ "lg:grid-cols-2 lg:gap-x-8 lg:gap-y-12",
303
+ )}
304
+ >
305
+ {references.map((reference, index) => (
306
+ <MobbinReferenceCard
307
+ key={reference.id}
308
+ reference={reference}
309
+ imageUrl={imageUrls[index]}
310
+ componentId={`${componentIdPrefix}.mobbin-reference.${reference.id}`}
311
+ onOpen={() => setActiveIndex(index)}
312
+ compact={isSidebar}
313
+ />
314
+ ))}
315
+ </div>
316
+ </PrototypeComponent>
317
+
318
+ {activeIndex !== null ? (
319
+ <MobbinReferenceLightbox
320
+ references={references}
321
+ imageUrls={imageUrls}
322
+ activeIndex={activeIndex}
323
+ onClose={() => setActiveIndex(null)}
324
+ onNavigate={setActiveIndex}
325
+ galleryId={galleryId}
326
+ />
327
+ ) : null}
328
+ </PrototypeComponent>
329
+ );
330
+ }
@@ -0,0 +1,222 @@
1
+ "use client";
2
+
3
+ import { ToolbarIconButton } from "@prototype/components/platform-ui/toolbar-icon-button";
4
+ import { Button } from "@prototype/components/ui/button";
5
+ import { Input } from "@prototype/components/ui/input";
6
+ import { Label } from "@prototype/components/ui/label";
7
+ import {
8
+ buildAddReferenceDocCopyText,
9
+ defaultReferenceDocsConfigPath,
10
+ isValidReferenceDocInput,
11
+ type PrototypeReferenceDoc,
12
+ } from "@prototype/lib/prototypes/reference-docs";
13
+ import { usePrototypeReviewOptional } from "@prototype/lib/prototypes/prototype-review-context";
14
+ import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
15
+ import { cn } from "@prototype/lib/utils";
16
+ import { ChevronDown, ExternalLink, Plus } from "lucide-react";
17
+ import { useId, useState } from "react";
18
+
19
+ const REFERENCE_DOC_CONTENT_FIELD_CLASS =
20
+ "field-sizing-content min-h-[5rem] w-full resize-y rounded-md border border-[var(--tool-chrome-border)] bg-[var(--tool-chrome-surface-muted)] px-2.5 py-2 text-sm text-[var(--tool-chrome-text)] leading-relaxed shadow-none outline-none placeholder:text-[var(--tool-chrome-text-muted)] focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-0";
21
+
22
+ function ReferenceDocItem({ doc }: { doc: PrototypeReferenceDoc }) {
23
+ const [expanded, setExpanded] = useState(false);
24
+ const hasContent = doc.content.trim().length > 0;
25
+
26
+ return (
27
+ <li className="overflow-hidden rounded-md border border-[var(--tool-chrome-border)] bg-[var(--tool-chrome-surface-muted)]">
28
+ <div className="flex items-stretch">
29
+ <a
30
+ href={doc.link}
31
+ target="_blank"
32
+ rel="noopener noreferrer"
33
+ className="flex min-w-0 flex-1 items-center gap-1.5 px-2.5 py-2 text-sm text-[var(--tool-chrome-text)] transition-colors duration-200 ease hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-text-heading)]"
34
+ >
35
+ <ExternalLink className="size-3.5 shrink-0" aria-hidden />
36
+ <span className="truncate">{doc.name}</span>
37
+ </a>
38
+ {hasContent ? (
39
+ <button
40
+ type="button"
41
+ className="inline-flex size-9 shrink-0 items-center justify-center border-l border-[var(--tool-chrome-border)] text-[var(--tool-chrome-text-muted)] transition-colors duration-200 ease hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-text)]"
42
+ aria-expanded={expanded}
43
+ aria-label={expanded ? "Hide content" : "Show content"}
44
+ onClick={() => setExpanded((current) => !current)}
45
+ >
46
+ <ChevronDown
47
+ className={cn(
48
+ "tool-panel-section-chevron size-3.5",
49
+ expanded && "is-expanded",
50
+ )}
51
+ aria-hidden
52
+ />
53
+ </button>
54
+ ) : null}
55
+ </div>
56
+ {hasContent ? (
57
+ <div
58
+ className={cn(
59
+ "tool-panel-section-content border-t border-[var(--tool-chrome-border)]",
60
+ expanded && "is-expanded",
61
+ )}
62
+ >
63
+ <div className="overflow-hidden">
64
+ <p className="px-2.5 py-2 text-xs leading-relaxed whitespace-pre-wrap text-[var(--tool-chrome-text-muted)]">
65
+ {doc.content}
66
+ </p>
67
+ </div>
68
+ </div>
69
+ ) : null}
70
+ </li>
71
+ );
72
+ }
73
+
74
+ type PrototypeReferenceDocsProps = {
75
+ slug?: string;
76
+ docs?: PrototypeReferenceDoc[];
77
+ configFilePath?: string;
78
+ className?: string;
79
+ };
80
+
81
+ export function PrototypeReferenceDocs({
82
+ slug: slugProp,
83
+ docs: docsProp,
84
+ configFilePath: configFilePathProp,
85
+ className,
86
+ }: PrototypeReferenceDocsProps) {
87
+ const review = usePrototypeReviewOptional();
88
+ const slug = slugProp ?? review?.slug;
89
+ const docs = docsProp ?? review?.referenceDocs ?? [];
90
+ const configFilePath =
91
+ configFilePathProp ??
92
+ review?.referenceDocsConfigFilePath ??
93
+ (slug ? defaultReferenceDocsConfigPath(slug) : undefined);
94
+ const nameFieldId = useId();
95
+ const linkFieldId = useId();
96
+ const contentFieldId = useId();
97
+ const [showForm, setShowForm] = useState(false);
98
+ const [name, setName] = useState("");
99
+ const [link, setLink] = useState("");
100
+ const [content, setContent] = useState("");
101
+ const { copy, icon, isCopied } = useCopyToClipboard();
102
+
103
+ const canCopy = Boolean(slug) && isValidReferenceDocInput(name, link, content);
104
+
105
+ const handleCopyPrompt = () => {
106
+ if (!slug || !canCopy) return;
107
+
108
+ const origin =
109
+ typeof window !== "undefined" ? window.location.origin : undefined;
110
+
111
+ copy(
112
+ buildAddReferenceDocCopyText({
113
+ slug,
114
+ name,
115
+ link,
116
+ content,
117
+ existingDocs: docs,
118
+ configFilePath,
119
+ origin,
120
+ }),
121
+ );
122
+ };
123
+
124
+ return (
125
+ <div className={cn("flex flex-col gap-2", className)}>
126
+ <div className="flex items-center justify-between gap-2">
127
+ <span className="tool-section-label">Reference docs</span>
128
+ <ToolbarIconButton
129
+ onClick={() => setShowForm((current) => !current)}
130
+ aria-label={showForm ? "Cancel adding doc" : "Add doc"}
131
+ aria-expanded={showForm}
132
+ >
133
+ <Plus size={16} strokeWidth={2} aria-hidden />
134
+ </ToolbarIconButton>
135
+ </div>
136
+
137
+ {showForm ? (
138
+ <div className="flex flex-col gap-3 rounded-md border border-[var(--tool-chrome-border)] bg-[var(--tool-chrome-surface-muted)] p-3">
139
+ <div className="flex flex-col gap-1.5">
140
+ <Label htmlFor={nameFieldId} className="text-foreground">
141
+ Doc name
142
+ </Label>
143
+ <Input
144
+ id={nameFieldId}
145
+ value={name}
146
+ onChange={(event) => setName(event.target.value)}
147
+ placeholder="Design spec"
148
+ />
149
+ </div>
150
+ <div className="flex flex-col gap-1.5">
151
+ <Label htmlFor={linkFieldId} className="text-foreground">
152
+ Doc link
153
+ </Label>
154
+ <Input
155
+ id={linkFieldId}
156
+ value={link}
157
+ onChange={(event) => setLink(event.target.value)}
158
+ placeholder="https://docs.google.com/..."
159
+ />
160
+ </div>
161
+ <div className="flex flex-col gap-1.5">
162
+ <Label htmlFor={contentFieldId} className="text-foreground">
163
+ Content
164
+ </Label>
165
+ <textarea
166
+ id={contentFieldId}
167
+ value={content}
168
+ onChange={(event) => setContent(event.target.value)}
169
+ placeholder="Summarize what this doc covers and why it matters…"
170
+ aria-label="Doc content"
171
+ rows={4}
172
+ className={REFERENCE_DOC_CONTENT_FIELD_CLASS}
173
+ />
174
+ </div>
175
+ <div className="flex items-center gap-2">
176
+ <Button
177
+ type="button"
178
+ size="default"
179
+ disabled={!canCopy}
180
+ onClick={handleCopyPrompt}
181
+ aria-label="Copy reference doc prompt"
182
+ >
183
+ {isCopied ? "Copied" : "Copy prompt"}
184
+ {icon}
185
+ </Button>
186
+ <Button
187
+ type="button"
188
+ variant="ghost"
189
+ size="default"
190
+ onClick={() => {
191
+ setShowForm(false);
192
+ setName("");
193
+ setLink("");
194
+ setContent("");
195
+ }}
196
+ >
197
+ Cancel
198
+ </Button>
199
+ </div>
200
+ </div>
201
+ ) : null}
202
+
203
+ {docs.length === 0 ? (
204
+ <p className="text-xs text-muted-foreground">
205
+ No reference docs yet. Add one in{" "}
206
+ {configFilePath ? (
207
+ <code className="text-[11px]">{configFilePath}</code>
208
+ ) : (
209
+ "the prototype codebase"
210
+ )}
211
+ .
212
+ </p>
213
+ ) : (
214
+ <ul className="flex flex-col gap-1.5">
215
+ {docs.map((doc) => (
216
+ <ReferenceDocItem key={`${doc.name}:${doc.link}`} doc={doc} />
217
+ ))}
218
+ </ul>
219
+ )}
220
+ </div>
221
+ );
222
+ }