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,1241 @@
1
+ "use client";
2
+
3
+ import { filterAnnotationsByChannel } from "@prototype/lib/prototype-comments/core/annotation-channel";
4
+ import { countUnresolvedAnnotations } from "@prototype/lib/prototype-comments/core/annotation-status";
5
+ import { injectCaptureColorTokens } from "@prototype/lib/prototype-comments/core/capture-theme";
6
+ import { useCommentStore } from "@prototype/lib/prototype-comments/react/CommentProvider";
7
+ import toolbarStyles from "@prototype/lib/prototype-comments/ui/capture-styles.module.scss";
8
+ import {
9
+ getPrototypeTweaksRegistration,
10
+ subscribePrototypeTweaks,
11
+ } from "@prototype/lib/prototypes/prototype-tweak-registry";
12
+ import { groupPrototypeTweaksByCategory } from "@prototype/lib/prototypes/prototype-tweak-types";
13
+ import { usePrototypeReview } from "@prototype/lib/prototypes/prototype-review-context";
14
+ import type { PrototypeViewportLayout } from "@prototype/lib/prototypes/prototype-review-context";
15
+ import {
16
+ usePrototypeToolTheme,
17
+ type PrototypeToolTheme,
18
+ } from "@prototype/lib/prototypes/use-prototype-tool-theme";
19
+ import { getPrototypeChromeRoot } from "@prototype/lib/tool-portal";
20
+ import {
21
+ buildStateMapHref,
22
+ getDefaultPrototypeStateMapPath,
23
+ } from "@prototype/lib/prototypes/prototype-state-canvas-types";
24
+ import {
25
+ getPrototypeShareCommand,
26
+ getPrototypeSlugFromPathname,
27
+ } from "@prototype/lib/prototypes/share-command";
28
+ import {
29
+ preventToolbarHoverButtonActivation,
30
+ TOOLBAR_HOVER_ONLY_BUTTON_PROPS,
31
+ useToolbarHoverPanel,
32
+ } from "@prototype/lib/prototypes/use-toolbar-hover-panel";
33
+ import { cn } from "@prototype/lib/utils";
34
+ import {
35
+ DropdownMenu,
36
+ DropdownMenuContent,
37
+ DropdownMenuItem,
38
+ DropdownMenuTrigger,
39
+ } from "@prototype/components/ui/dropdown-menu";
40
+ import {
41
+ Tooltip,
42
+ TooltipContent,
43
+ TooltipTrigger,
44
+ } from "@prototype/components/ui/tooltip";
45
+ import {
46
+ ArrowLeft,
47
+ Layers,
48
+ LayoutGrid,
49
+ MapPin,
50
+ MessageSquare,
51
+ Monitor,
52
+ Moon,
53
+ Network,
54
+ Plus,
55
+ Rocket,
56
+ ScrollText,
57
+ Share2,
58
+ SlidersHorizontal,
59
+ Smartphone,
60
+ Sun,
61
+ } from "lucide-react";
62
+ import Link from "next/link";
63
+ import { usePathname, useSearchParams } from "next/navigation";
64
+ import { createPortal } from "react-dom";
65
+ import {
66
+ cloneElement,
67
+ useCallback,
68
+ useEffect,
69
+ useRef,
70
+ useState,
71
+ type MouseEvent,
72
+ type PointerEvent as ReactPointerEvent,
73
+ type ReactElement,
74
+ type ReactNode,
75
+ } from "react";
76
+
77
+ import { VariantSetLucideIcon } from "@prototype/lib/prototypes/variant-set-lucide-icon";
78
+ import { miniPillTextFromLabel } from "./mini-pill-label";
79
+ import { PrototypeCreateExplorationModal } from "./prototype-create-exploration-modal";
80
+ import { PrototypeCreateStateModal } from "./prototype-create-state-modal";
81
+ import { PrototypeShareCommandButton } from "./prototype-controls";
82
+ import { PrototypeTweakHighlight } from "./prototype-tweak-highlight";
83
+ import { PrototypeReviewSidebar } from "./prototype-review-sidebar";
84
+ import pillStyles from "./prototype-floating-pill.module.scss";
85
+ import styles from "./prototype-review-chrome.module.scss";
86
+
87
+ type PrototypeReviewChromeProps = {
88
+ onSelect: (id: string) => void;
89
+ selectedId?: string | null;
90
+ onOpenCommentsPanel?: () => void;
91
+ isCommentModeActive?: boolean;
92
+ onToggleCommentMode?: () => void;
93
+ onClose: () => void;
94
+ };
95
+
96
+ function clearPillTextSelection() {
97
+ window.getSelection()?.removeAllRanges();
98
+ }
99
+
100
+ function isTypingTarget(target: EventTarget | null): boolean {
101
+ if (!(target instanceof HTMLElement)) return false;
102
+ return (
103
+ target instanceof HTMLInputElement ||
104
+ target instanceof HTMLTextAreaElement ||
105
+ target.isContentEditable
106
+ );
107
+ }
108
+
109
+ const TOOLBAR_MENU_CONTENT_CLASS = cn(
110
+ pillStyles.dropdownContent,
111
+ "z-[1051] border-0 bg-transparent p-0 text-foreground shadow-none ring-0",
112
+ "data-[state=open]:animate-none data-[state=closed]:animate-none",
113
+ );
114
+
115
+ const TOOLBAR_MENU_CONTENT_PROPS = {
116
+ side: "top" as const,
117
+ align: "center" as const,
118
+ sideOffset: 8,
119
+ className: TOOLBAR_MENU_CONTENT_CLASS,
120
+ onCloseAutoFocus: (event: Event) => event.preventDefault(),
121
+ };
122
+
123
+ const TOOLBAR_TOOLTIP_PROPS = {
124
+ side: "top" as const,
125
+ sideOffset: 8,
126
+ className: styles.toolbarTooltipContent,
127
+ } as const;
128
+
129
+ /** Match TooltipProvider delayDuration in prototype-provider.tsx */
130
+ const TOOLBAR_TOOLTIP_HOVER_DELAY_MS = 300;
131
+
132
+ const TOOLBAR_TOOLTIP_DISMISS_EVENT = "prototype-review-toolbar-dismiss-tooltips";
133
+
134
+ function dismissReviewToolbarTooltips() {
135
+ document.dispatchEvent(new Event(TOOLBAR_TOOLTIP_DISMISS_EVENT));
136
+ }
137
+
138
+ function chainPointerHandlers(
139
+ existing: ((event: ReactPointerEvent<HTMLElement>) => void) | undefined,
140
+ next: (event: ReactPointerEvent<HTMLElement>) => void,
141
+ ) {
142
+ return (event: ReactPointerEvent<HTMLElement>) => {
143
+ existing?.(event);
144
+ if (!event.defaultPrevented) {
145
+ next(event);
146
+ }
147
+ };
148
+ }
149
+
150
+ type ToolbarTooltipAnchorProps = {
151
+ onPointerEnter?: (event: ReactPointerEvent<HTMLElement>) => void;
152
+ onPointerLeave?: (event: ReactPointerEvent<HTMLElement>) => void;
153
+ };
154
+
155
+ function ReviewToolbarTooltip({
156
+ label,
157
+ disabled = false,
158
+ children,
159
+ }: {
160
+ label: string;
161
+ disabled?: boolean;
162
+ children: ReactElement<ToolbarTooltipAnchorProps>;
163
+ }) {
164
+ const [open, setOpen] = useState(false);
165
+ const showTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
166
+ const isHoveringRef = useRef(false);
167
+
168
+ const clearShowTimer = useCallback(() => {
169
+ if (showTimerRef.current) {
170
+ clearTimeout(showTimerRef.current);
171
+ showTimerRef.current = null;
172
+ }
173
+ }, []);
174
+
175
+ const close = useCallback(() => {
176
+ isHoveringRef.current = false;
177
+ clearShowTimer();
178
+ setOpen(false);
179
+ }, [clearShowTimer]);
180
+
181
+ const openFromHover = useCallback(() => {
182
+ if (disabled || !isHoveringRef.current) return;
183
+ setOpen(true);
184
+ }, [disabled]);
185
+
186
+ const handlePointerEnter = useCallback(
187
+ (event: ReactPointerEvent<HTMLElement>) => {
188
+ if (disabled || event.pointerType === "touch") return;
189
+
190
+ isHoveringRef.current = true;
191
+ clearShowTimer();
192
+ showTimerRef.current = setTimeout(
193
+ openFromHover,
194
+ TOOLBAR_TOOLTIP_HOVER_DELAY_MS,
195
+ );
196
+ },
197
+ [clearShowTimer, disabled, openFromHover],
198
+ );
199
+
200
+ const handlePointerLeave = useCallback(() => {
201
+ close();
202
+ }, [close]);
203
+
204
+ useEffect(() => {
205
+ if (disabled) close();
206
+ }, [close, disabled]);
207
+
208
+ useEffect(() => {
209
+ return () => clearShowTimer();
210
+ }, [clearShowTimer]);
211
+
212
+ useEffect(() => {
213
+ document.addEventListener(TOOLBAR_TOOLTIP_DISMISS_EVENT, close);
214
+ return () =>
215
+ document.removeEventListener(TOOLBAR_TOOLTIP_DISMISS_EVENT, close);
216
+ }, [close]);
217
+
218
+ const trigger = cloneElement<ToolbarTooltipAnchorProps>(children, {
219
+ onPointerEnter: chainPointerHandlers(
220
+ children.props.onPointerEnter,
221
+ handlePointerEnter,
222
+ ),
223
+ onPointerLeave: chainPointerHandlers(
224
+ children.props.onPointerLeave,
225
+ handlePointerLeave,
226
+ ),
227
+ });
228
+
229
+ return (
230
+ <Tooltip
231
+ open={open}
232
+ delayDuration={0}
233
+ disableHoverableContent
234
+ onOpenChange={(next) => {
235
+ if (!next) setOpen(false);
236
+ }}
237
+ >
238
+ <TooltipTrigger asChild>{trigger}</TooltipTrigger>
239
+ <TooltipContent {...TOOLBAR_TOOLTIP_PROPS}>{label}</TooltipContent>
240
+ </Tooltip>
241
+ );
242
+ }
243
+
244
+ function ReviewToolbarButton({
245
+ label,
246
+ tooltip,
247
+ pressed = false,
248
+ highlightActive = false,
249
+ badge,
250
+ onClick,
251
+ hoverOnly = false,
252
+ hoverTriggerProps,
253
+ children,
254
+ }: {
255
+ label: string;
256
+ tooltip?: string;
257
+ pressed?: boolean;
258
+ highlightActive?: boolean;
259
+ badge?: ReactNode;
260
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
261
+ hoverOnly?: boolean;
262
+ hoverTriggerProps?: ToolbarTooltipAnchorProps;
263
+ children: ReactNode;
264
+ }) {
265
+ return (
266
+ <ReviewToolbarTooltip label={tooltip ?? label} disabled={pressed}>
267
+ <div className={toolbarStyles.buttonWrapper} {...hoverTriggerProps}>
268
+ <button
269
+ type="button"
270
+ className={cn(
271
+ toolbarStyles.controlButton,
272
+ pressed && !highlightActive && styles.toolbarButtonHovered,
273
+ )}
274
+ data-active={highlightActive && pressed ? "true" : undefined}
275
+ aria-label={label}
276
+ aria-pressed={pressed}
277
+ onClick={(event) => {
278
+ if (hoverOnly) {
279
+ preventToolbarHoverButtonActivation(event);
280
+ return;
281
+ }
282
+ event.stopPropagation();
283
+ onClick?.(event);
284
+ }}
285
+ onPointerDown={
286
+ hoverOnly ? preventToolbarHoverButtonActivation : undefined
287
+ }
288
+ >
289
+ {children}
290
+ {badge}
291
+ </button>
292
+ </div>
293
+ </ReviewToolbarTooltip>
294
+ );
295
+ }
296
+
297
+ function ReviewToolbarLink({
298
+ label,
299
+ href,
300
+ children,
301
+ }: {
302
+ label: string;
303
+ href: string;
304
+ children: ReactNode;
305
+ }) {
306
+ return (
307
+ <ReviewToolbarTooltip label={label}>
308
+ <div className={toolbarStyles.buttonWrapper}>
309
+ <Link
310
+ href={href}
311
+ className={toolbarStyles.controlButton}
312
+ style={{ textDecoration: "none" }}
313
+ aria-label={label}
314
+ onClick={(event) => event.stopPropagation()}
315
+ >
316
+ {children}
317
+ </Link>
318
+ </div>
319
+ </ReviewToolbarTooltip>
320
+ );
321
+ }
322
+
323
+ function CreateExplorationPanelTrigger({
324
+ onOpen,
325
+ }: {
326
+ onOpen: () => void;
327
+ }) {
328
+ return (
329
+ <CreatePanelTrigger onOpen={onOpen} label="New exploration…" />
330
+ );
331
+ }
332
+
333
+ function CreateStatePanelTrigger({
334
+ onOpen,
335
+ }: {
336
+ onOpen: () => void;
337
+ }) {
338
+ return (
339
+ <CreatePanelTrigger onOpen={onOpen} label="New state…" />
340
+ );
341
+ }
342
+
343
+ function CreatePanelTrigger({
344
+ onOpen,
345
+ label,
346
+ }: {
347
+ onOpen: () => void;
348
+ label: string;
349
+ }) {
350
+ return (
351
+ <div
352
+ className={pillStyles.explorationBriefPanel}
353
+ onPointerDown={(event) => event.stopPropagation()}
354
+ >
355
+ <button
356
+ type="button"
357
+ className={pillStyles.explorationBriefTrigger}
358
+ onClick={onOpen}
359
+ aria-label={label}
360
+ >
361
+ <span className={pillStyles.explorationBriefTriggerLabel}>
362
+ {label}
363
+ </span>
364
+ <Plus
365
+ size={12}
366
+ strokeWidth={2}
367
+ className={pillStyles.dropdownItemIcon}
368
+ aria-hidden
369
+ />
370
+ </button>
371
+ </div>
372
+ );
373
+ }
374
+
375
+ type ReviewToolbarIconPickerOption<T extends string> = {
376
+ value: T;
377
+ label: string;
378
+ icon: ReactNode;
379
+ };
380
+
381
+ function ReviewToolbarIconPickerMenu<T extends string>({
382
+ label,
383
+ tooltip,
384
+ value,
385
+ onValueChange,
386
+ options,
387
+ open,
388
+ onOpenChange,
389
+ hoverPanel,
390
+ triggerIcon,
391
+ panelId,
392
+ }: {
393
+ label: string;
394
+ tooltip?: string;
395
+ value: T;
396
+ onValueChange: (value: T) => void;
397
+ options: ReviewToolbarIconPickerOption<T>[];
398
+ open: boolean;
399
+ onOpenChange: (open: boolean) => void;
400
+ hoverPanel: ReturnType<typeof useToolbarHoverPanel>;
401
+ triggerIcon: ReactNode;
402
+ panelId: string;
403
+ }) {
404
+ const { commentTheme } = usePrototypeToolTheme();
405
+
406
+ return (
407
+ <DropdownMenu modal={false} open={open} onOpenChange={onOpenChange}>
408
+ <ReviewToolbarTooltip label={tooltip ?? label} disabled={open}>
409
+ <div
410
+ className={cn(
411
+ styles.footerBarControls,
412
+ toolbarStyles.buttonWrapper,
413
+ )}
414
+ {...hoverPanel.triggerProps}
415
+ >
416
+ <DropdownMenuTrigger asChild>
417
+ <button
418
+ type="button"
419
+ className={cn(
420
+ toolbarStyles.controlButton,
421
+ styles.toolbarMenuTrigger,
422
+ open && styles.toolbarButtonHovered,
423
+ )}
424
+ data-menu-open={open ? "true" : undefined}
425
+ aria-label={label}
426
+ aria-expanded={open}
427
+ aria-pressed={open}
428
+ {...TOOLBAR_HOVER_ONLY_BUTTON_PROPS}
429
+ >
430
+ {triggerIcon}
431
+ </button>
432
+ </DropdownMenuTrigger>
433
+ </div>
434
+ </ReviewToolbarTooltip>
435
+
436
+ <DropdownMenuContent
437
+ {...TOOLBAR_MENU_CONTENT_PROPS}
438
+ {...hoverPanel.panelProps}
439
+ className={cn(
440
+ TOOLBAR_MENU_CONTENT_CLASS,
441
+ pillStyles.dropdownContentIconPicker,
442
+ )}
443
+ data-prototype-comment-theme={commentTheme}
444
+ data-prototype-review-panel={panelId}
445
+ >
446
+ <div
447
+ className={pillStyles.toolbarIconPicker}
448
+ role="group"
449
+ aria-label={label}
450
+ >
451
+ {options.map((option) => (
452
+ <button
453
+ key={option.value}
454
+ type="button"
455
+ className={pillStyles.toolbarIconPickerOption}
456
+ data-selected={value === option.value ? "true" : undefined}
457
+ aria-label={option.label}
458
+ aria-pressed={value === option.value}
459
+ onClick={() => {
460
+ onValueChange(option.value);
461
+ onOpenChange(false);
462
+ }}
463
+ >
464
+ {option.icon}
465
+ </button>
466
+ ))}
467
+ </div>
468
+ </DropdownMenuContent>
469
+ </DropdownMenu>
470
+ );
471
+ }
472
+
473
+ const VIEWPORT_LAYOUT_OPTIONS: ReviewToolbarIconPickerOption<PrototypeViewportLayout>[] =
474
+ [
475
+ {
476
+ value: "desktop",
477
+ label: "Desktop layout",
478
+ icon: <Monitor size={16} strokeWidth={2} />,
479
+ },
480
+ {
481
+ value: "mobile",
482
+ label: "Mobile layout",
483
+ icon: <Smartphone size={16} strokeWidth={2} />,
484
+ },
485
+ ];
486
+
487
+ const THEME_OPTIONS: ReviewToolbarIconPickerOption<PrototypeToolTheme>[] = [
488
+ {
489
+ value: "light",
490
+ label: "Light mode",
491
+ icon: <Sun size={16} strokeWidth={2} />,
492
+ },
493
+ {
494
+ value: "dark",
495
+ label: "Dark mode",
496
+ icon: <Moon size={16} strokeWidth={2} />,
497
+ },
498
+ ];
499
+
500
+ export function PrototypeReviewChrome({
501
+ onSelect,
502
+ selectedId,
503
+ onOpenCommentsPanel,
504
+ isCommentModeActive,
505
+ onToggleCommentMode,
506
+ onClose,
507
+ }: PrototypeReviewChromeProps) {
508
+ const review = usePrototypeReview();
509
+ const { commentTheme, theme, setTheme } = usePrototypeToolTheme();
510
+ const chromeRef = useRef<HTMLDivElement>(null);
511
+ const [statesOpen, setStatesOpen] = useState(false);
512
+ const [explorationsMenuOpen, setExplorationsMenuOpen] = useState(false);
513
+ const [createExplorationModalOpen, setCreateExplorationModalOpen] =
514
+ useState(false);
515
+ const [createStateModalOpen, setCreateStateModalOpen] = useState(false);
516
+ const [shareMenuOpen, setShareMenuOpen] = useState(false);
517
+ const [viewportMenuOpen, setViewportMenuOpen] = useState(false);
518
+ const [themeMenuOpen, setThemeMenuOpen] = useState(false);
519
+ const [tweakVersion, setTweakVersion] = useState(0);
520
+ const pathname = usePathname();
521
+ const searchParams = useSearchParams();
522
+ const { annotations } = useCommentStore();
523
+ const [mounted, setMounted] = useState(false);
524
+
525
+ const slug = getPrototypeSlugFromPathname(pathname);
526
+ const shareCommand = slug ? getPrototypeShareCommand(slug) : null;
527
+
528
+ useEffect(() => {
529
+ injectCaptureColorTokens();
530
+ setMounted(true);
531
+ setTweakVersion((v) => v + 1);
532
+ }, []);
533
+
534
+ useEffect(() => {
535
+ const unsubscribe = subscribePrototypeTweaks(() => {
536
+ setTweakVersion((v) => v + 1);
537
+ });
538
+ setTweakVersion((v) => v + 1);
539
+ return unsubscribe;
540
+ }, []);
541
+
542
+ const closeToolbarMenus = useCallback(() => {
543
+ setStatesOpen(false);
544
+ setExplorationsMenuOpen(false);
545
+ setShareMenuOpen(false);
546
+ setViewportMenuOpen(false);
547
+ setThemeMenuOpen(false);
548
+ review.closeTweaks();
549
+ dismissReviewToolbarTooltips();
550
+ }, [review.closeTweaks]);
551
+
552
+ const blurToolbarFocus = useCallback(() => {
553
+ const active = document.activeElement;
554
+ if (active instanceof HTMLElement && chromeRef.current?.contains(active)) {
555
+ active.blur();
556
+ }
557
+ }, []);
558
+
559
+ const openStatesMenu = useCallback(() => {
560
+ dismissReviewToolbarTooltips();
561
+ review.closeSettings();
562
+ setExplorationsMenuOpen(false);
563
+ setShareMenuOpen(false);
564
+ setViewportMenuOpen(false);
565
+ setThemeMenuOpen(false);
566
+ review.closeTweaks();
567
+ setStatesOpen(true);
568
+ }, [review.closeSettings, review.closeTweaks]);
569
+
570
+ const closeStatesMenu = useCallback(() => {
571
+ setStatesOpen(false);
572
+ blurToolbarFocus();
573
+ }, [blurToolbarFocus]);
574
+
575
+ const openExplorationsMenu = useCallback(() => {
576
+ dismissReviewToolbarTooltips();
577
+ review.closeSettings();
578
+ setStatesOpen(false);
579
+ setShareMenuOpen(false);
580
+ setViewportMenuOpen(false);
581
+ setThemeMenuOpen(false);
582
+ review.closeTweaks();
583
+ setExplorationsMenuOpen(true);
584
+ }, [review.closeSettings, review.closeTweaks]);
585
+
586
+ const closeExplorationsMenu = useCallback(() => {
587
+ setExplorationsMenuOpen(false);
588
+ blurToolbarFocus();
589
+ }, [blurToolbarFocus]);
590
+
591
+ const openCreateExplorationModal = useCallback(() => {
592
+ setExplorationsMenuOpen(false);
593
+ blurToolbarFocus();
594
+ setCreateExplorationModalOpen(true);
595
+ }, [blurToolbarFocus]);
596
+
597
+ const openCreateStateModal = useCallback(() => {
598
+ setStatesOpen(false);
599
+ blurToolbarFocus();
600
+ setCreateStateModalOpen(true);
601
+ }, [blurToolbarFocus]);
602
+
603
+ const openShareMenu = useCallback(() => {
604
+ dismissReviewToolbarTooltips();
605
+ closeToolbarMenus();
606
+ review.closeSettings();
607
+ setShareMenuOpen(true);
608
+ }, [closeToolbarMenus, review.closeSettings]);
609
+
610
+ const closeShareMenu = useCallback(() => {
611
+ setShareMenuOpen(false);
612
+ blurToolbarFocus();
613
+ }, [blurToolbarFocus]);
614
+
615
+ const openViewportMenu = useCallback(() => {
616
+ dismissReviewToolbarTooltips();
617
+ review.closeSettings();
618
+ setStatesOpen(false);
619
+ setExplorationsMenuOpen(false);
620
+ setShareMenuOpen(false);
621
+ setThemeMenuOpen(false);
622
+ review.closeTweaks();
623
+ setViewportMenuOpen(true);
624
+ }, [review.closeSettings, review.closeTweaks]);
625
+
626
+ const closeViewportMenu = useCallback(() => {
627
+ setViewportMenuOpen(false);
628
+ blurToolbarFocus();
629
+ }, [blurToolbarFocus]);
630
+
631
+ const openThemeMenu = useCallback(() => {
632
+ dismissReviewToolbarTooltips();
633
+ review.closeSettings();
634
+ setStatesOpen(false);
635
+ setExplorationsMenuOpen(false);
636
+ setShareMenuOpen(false);
637
+ setViewportMenuOpen(false);
638
+ review.closeTweaks();
639
+ setThemeMenuOpen(true);
640
+ }, [review.closeSettings, review.closeTweaks]);
641
+
642
+ const closeThemeMenu = useCallback(() => {
643
+ setThemeMenuOpen(false);
644
+ blurToolbarFocus();
645
+ }, [blurToolbarFocus]);
646
+
647
+ const statesHover = useToolbarHoverPanel({
648
+ onOpen: openStatesMenu,
649
+ onClose: closeStatesMenu,
650
+ });
651
+
652
+ const explorationsHover = useToolbarHoverPanel({
653
+ onOpen: openExplorationsMenu,
654
+ onClose: closeExplorationsMenu,
655
+ });
656
+
657
+ const shareHover = useToolbarHoverPanel({
658
+ onOpen: openShareMenu,
659
+ onClose: closeShareMenu,
660
+ });
661
+
662
+ const viewportHover = useToolbarHoverPanel({
663
+ onOpen: openViewportMenu,
664
+ onClose: closeViewportMenu,
665
+ });
666
+
667
+ const themeHover = useToolbarHoverPanel({
668
+ onOpen: openThemeMenu,
669
+ onClose: closeThemeMenu,
670
+ });
671
+
672
+ useEffect(() => {
673
+ const handleKeyDown = (event: KeyboardEvent) => {
674
+ if (event.key !== "Escape") return;
675
+ if (isTypingTarget(event.target)) return;
676
+ if (document.querySelector("[data-mobbin-lightbox-open]")) return;
677
+ if (document.querySelector('[role="dialog"][data-state="open"]')) return;
678
+
679
+ dismissReviewToolbarTooltips();
680
+
681
+ let dismissed = false;
682
+
683
+ if (statesOpen || explorationsMenuOpen || review.tweaksOpen || shareMenuOpen || viewportMenuOpen || themeMenuOpen) {
684
+ closeToolbarMenus();
685
+ dismissed = true;
686
+ } else if (review.variantsOpen) {
687
+ review.closeVariants();
688
+ setExplorationsMenuOpen(false);
689
+ dismissed = true;
690
+ }
691
+
692
+ if (dismissed) {
693
+ event.preventDefault();
694
+ event.stopPropagation();
695
+ blurToolbarFocus();
696
+ }
697
+ };
698
+
699
+ document.addEventListener("keydown", handleKeyDown, true);
700
+ return () => document.removeEventListener("keydown", handleKeyDown, true);
701
+ }, [
702
+ blurToolbarFocus,
703
+ closeToolbarMenus,
704
+ explorationsMenuOpen,
705
+ review.closeVariants,
706
+ review.tweaksOpen,
707
+ review.variantsOpen,
708
+ shareMenuOpen,
709
+ statesOpen,
710
+ themeMenuOpen,
711
+ viewportMenuOpen,
712
+ ]);
713
+
714
+ if (!mounted) return null;
715
+
716
+ const chromePortalTarget = getPrototypeChromeRoot() ?? null;
717
+
718
+ const sidebarPortalTarget = document.getElementById(
719
+ "prototype-comments-sidebar-root",
720
+ );
721
+
722
+ const commentAnnotations = filterAnnotationsByChannel(annotations, "comment");
723
+ const commentCount = countUnresolvedAnnotations(commentAnnotations);
724
+ void tweakVersion;
725
+ const tweaksRegistration = getPrototypeTweaksRegistration();
726
+ const tweakItems = tweaksRegistration?.items ?? [];
727
+ const tweakGroups = groupPrototypeTweaksByCategory(tweakItems);
728
+ const hasTweakItems = tweakItems.length > 0;
729
+ const showTweaksMenu = hasTweakItems;
730
+
731
+ const tweaksContent = review.tweaksContentRef.current;
732
+ const showShareMenu = Boolean(shareCommand);
733
+ const stateMapPagePath =
734
+ review.stateCanvasPagePath ?? getDefaultPrototypeStateMapPath(review.slug);
735
+ const stateMapQuery = searchParams.toString();
736
+ const prototypeReturnTo = `${pathname}${stateMapQuery ? `?${stateMapQuery}` : ""}`;
737
+ const stateMapHref = buildStateMapHref(
738
+ stateMapPagePath,
739
+ prototypeReturnTo,
740
+ searchParams,
741
+ );
742
+ const onStateMapPage = pathname.startsWith(stateMapPagePath);
743
+ const chrome = (
744
+ <div
745
+ ref={chromeRef}
746
+ data-prototype-root
747
+ data-prototype-comment-theme={commentTheme}
748
+ data-prototype-comment-accent="blue"
749
+ data-prototype-comment-root=""
750
+ className={styles.footerRoot}
751
+ data-prototype-review-trigger
752
+ >
753
+ <div className={styles.footerBar}>
754
+ <div className={styles.footerGroup}>
755
+ <ReviewToolbarLink label="Back to gallery" href="/">
756
+ <ArrowLeft size={16} strokeWidth={2} />
757
+ </ReviewToolbarLink>
758
+
759
+ <ReviewToolbarIconPickerMenu
760
+ label="Layout"
761
+ tooltip="Layout"
762
+ value={review.viewportLayout}
763
+ onValueChange={review.setViewportLayout}
764
+ options={VIEWPORT_LAYOUT_OPTIONS}
765
+ open={viewportMenuOpen}
766
+ onOpenChange={(open) => {
767
+ if (open) {
768
+ openViewportMenu();
769
+ return;
770
+ }
771
+ closeViewportMenu();
772
+ }}
773
+ hoverPanel={viewportHover}
774
+ panelId="viewport"
775
+ triggerIcon={
776
+ review.viewportLayout === "mobile" ? (
777
+ <Smartphone size={16} strokeWidth={2} />
778
+ ) : (
779
+ <Monitor size={16} strokeWidth={2} />
780
+ )
781
+ }
782
+ />
783
+
784
+ <ReviewToolbarIconPickerMenu
785
+ label="Brightness"
786
+ tooltip="Brightness"
787
+ value={theme}
788
+ onValueChange={setTheme}
789
+ options={THEME_OPTIONS}
790
+ open={themeMenuOpen}
791
+ onOpenChange={(open) => {
792
+ if (open) {
793
+ openThemeMenu();
794
+ return;
795
+ }
796
+ closeThemeMenu();
797
+ }}
798
+ hoverPanel={themeHover}
799
+ panelId="theme"
800
+ triggerIcon={
801
+ theme === "light" ? (
802
+ <Sun size={16} strokeWidth={2} />
803
+ ) : (
804
+ <Moon size={16} strokeWidth={2} />
805
+ )
806
+ }
807
+ />
808
+
809
+ <DropdownMenu
810
+ modal={false}
811
+ open={statesOpen}
812
+ onOpenChange={(open) => {
813
+ if (open) {
814
+ openStatesMenu();
815
+ return;
816
+ }
817
+ closeStatesMenu();
818
+ }}
819
+ >
820
+ <ReviewToolbarTooltip label="States" disabled={statesOpen}>
821
+ <div
822
+ className={cn(
823
+ styles.footerBarControls,
824
+ toolbarStyles.buttonWrapper,
825
+ )}
826
+ {...statesHover.triggerProps}
827
+ >
828
+ <DropdownMenuTrigger asChild>
829
+ <button
830
+ type="button"
831
+ className={cn(
832
+ toolbarStyles.controlButton,
833
+ styles.toolbarMenuTrigger,
834
+ statesOpen && styles.toolbarButtonHovered,
835
+ )}
836
+ data-menu-open={statesOpen ? "true" : undefined}
837
+ aria-label="States"
838
+ aria-expanded={statesOpen}
839
+ aria-pressed={statesOpen}
840
+ {...TOOLBAR_HOVER_ONLY_BUTTON_PROPS}
841
+ >
842
+ <Layers size={16} strokeWidth={2} />
843
+ </button>
844
+ </DropdownMenuTrigger>
845
+ </div>
846
+ </ReviewToolbarTooltip>
847
+
848
+ <DropdownMenuContent
849
+ {...TOOLBAR_MENU_CONTENT_PROPS}
850
+ {...statesHover.panelProps}
851
+ data-prototype-comment-theme={commentTheme}
852
+ data-prototype-review-panel="states"
853
+ >
854
+ <div
855
+ className={cn(
856
+ pillStyles.controlsPanel,
857
+ pillStyles.controlsPanelStates,
858
+ )}
859
+ >
860
+ <CreateStatePanelTrigger onOpen={openCreateStateModal} />
861
+ {tweaksContent ? (
862
+ <div
863
+ className={pillStyles.explorationBriefSeparator}
864
+ role="separator"
865
+ aria-hidden
866
+ />
867
+ ) : null}
868
+ {tweaksContent}
869
+ </div>
870
+ </DropdownMenuContent>
871
+ </DropdownMenu>
872
+
873
+ <ReviewToolbarLink label="State map" href={stateMapHref}>
874
+ <span
875
+ className={cn(onStateMapPage && styles.toolbarButtonHovered)}
876
+ >
877
+ <Network size={16} strokeWidth={2} />
878
+ </span>
879
+ </ReviewToolbarLink>
880
+
881
+ {showTweaksMenu ? (
882
+ <DropdownMenu
883
+ modal={false}
884
+ open={review.tweaksOpen}
885
+ onOpenChange={(open) => {
886
+ if (!open) {
887
+ review.closeTweaks();
888
+ blurToolbarFocus();
889
+ return;
890
+ }
891
+ review.closeSettings();
892
+ setStatesOpen(false);
893
+ setExplorationsMenuOpen(false);
894
+ setShareMenuOpen(false);
895
+ setViewportMenuOpen(false);
896
+ setThemeMenuOpen(false);
897
+ review.openTweaks();
898
+ }}
899
+ >
900
+ <ReviewToolbarTooltip label="Tweaks" disabled={review.tweaksOpen}>
901
+ <div
902
+ className={cn(
903
+ styles.footerBarControls,
904
+ toolbarStyles.buttonWrapper,
905
+ )}
906
+ >
907
+ <DropdownMenuTrigger asChild>
908
+ <button
909
+ type="button"
910
+ className={cn(
911
+ toolbarStyles.controlButton,
912
+ styles.toolbarMenuTrigger,
913
+ review.tweaksOpen && styles.toolbarButtonHovered,
914
+ )}
915
+ data-menu-open={review.tweaksOpen ? "true" : undefined}
916
+ aria-label="Prototype tweaks"
917
+ aria-expanded={review.tweaksOpen}
918
+ aria-pressed={review.tweaksOpen}
919
+ onClick={(event) => {
920
+ event.stopPropagation();
921
+ clearPillTextSelection();
922
+ }}
923
+ onMouseDown={(event) => event.preventDefault()}
924
+ onPointerDown={(event) => event.stopPropagation()}
925
+ >
926
+ <SlidersHorizontal size={16} strokeWidth={2} />
927
+ </button>
928
+ </DropdownMenuTrigger>
929
+ </div>
930
+ </ReviewToolbarTooltip>
931
+
932
+ <DropdownMenuContent
933
+ {...TOOLBAR_MENU_CONTENT_PROPS}
934
+ data-prototype-comment-theme={commentTheme}
935
+ data-prototype-review-panel="tweaks"
936
+ >
937
+ <div className={pillStyles.controlsPanel}>
938
+ {tweakGroups.map((group) => (
939
+ <div
940
+ key={group.category}
941
+ className={pillStyles.controlsPanelSection}
942
+ >
943
+ <div className={pillStyles.dropdownLabel}>
944
+ {group.label}
945
+ </div>
946
+ {group.items.map((tweak) => (
947
+ <DropdownMenuItem
948
+ key={tweak.id}
949
+ className={pillStyles.dropdownItem}
950
+ onSelect={() => {
951
+ review.closeTweaks();
952
+ blurToolbarFocus();
953
+ review.focusTweak(tweak.id);
954
+ }}
955
+ >
956
+ {tweak.label}
957
+ <MapPin
958
+ size={10}
959
+ className={pillStyles.dropdownItemIcon}
960
+ />
961
+ </DropdownMenuItem>
962
+ ))}
963
+ </div>
964
+ ))}
965
+ </div>
966
+ </DropdownMenuContent>
967
+ </DropdownMenu>
968
+ ) : null}
969
+
970
+ {showShareMenu ? (
971
+ <DropdownMenu
972
+ modal={false}
973
+ open={shareMenuOpen}
974
+ onOpenChange={(open) => {
975
+ if (open) {
976
+ openShareMenu();
977
+ return;
978
+ }
979
+ closeShareMenu();
980
+ }}
981
+ >
982
+ <ReviewToolbarTooltip label="Share" disabled={shareMenuOpen}>
983
+ <div
984
+ className={cn(
985
+ styles.footerBarControls,
986
+ toolbarStyles.buttonWrapper,
987
+ )}
988
+ {...shareHover.triggerProps}
989
+ >
990
+ <DropdownMenuTrigger asChild>
991
+ <button
992
+ type="button"
993
+ className={cn(
994
+ toolbarStyles.controlButton,
995
+ styles.toolbarMenuTrigger,
996
+ shareMenuOpen && styles.toolbarButtonHovered,
997
+ )}
998
+ data-menu-open={shareMenuOpen ? "true" : undefined}
999
+ aria-label="Share"
1000
+ aria-expanded={shareMenuOpen}
1001
+ aria-pressed={shareMenuOpen}
1002
+ {...TOOLBAR_HOVER_ONLY_BUTTON_PROPS}
1003
+ >
1004
+ <Share2 size={16} strokeWidth={2} />
1005
+ </button>
1006
+ </DropdownMenuTrigger>
1007
+ </div>
1008
+ </ReviewToolbarTooltip>
1009
+
1010
+ <DropdownMenuContent
1011
+ {...TOOLBAR_MENU_CONTENT_PROPS}
1012
+ {...shareHover.panelProps}
1013
+ data-prototype-comment-theme={commentTheme}
1014
+ data-prototype-review-panel="share"
1015
+ >
1016
+ <div className={pillStyles.controlsPanel}>
1017
+ <PrototypeShareCommandButton command={shareCommand!} />
1018
+ </div>
1019
+ </DropdownMenuContent>
1020
+ </DropdownMenu>
1021
+ ) : null}
1022
+ </div>
1023
+
1024
+ <div className={styles.footerGroup}>
1025
+ <DropdownMenu
1026
+ modal={false}
1027
+ open={explorationsMenuOpen}
1028
+ onOpenChange={(open) => {
1029
+ if (open) {
1030
+ openExplorationsMenu();
1031
+ return;
1032
+ }
1033
+ closeExplorationsMenu();
1034
+ }}
1035
+ >
1036
+ <ReviewToolbarTooltip
1037
+ label="Explorations"
1038
+ disabled={explorationsMenuOpen}
1039
+ >
1040
+ <div
1041
+ className={cn(
1042
+ styles.footerBarControls,
1043
+ toolbarStyles.buttonWrapper,
1044
+ )}
1045
+ {...explorationsHover.triggerProps}
1046
+ >
1047
+ <DropdownMenuTrigger asChild>
1048
+ <button
1049
+ type="button"
1050
+ className={cn(
1051
+ toolbarStyles.controlButton,
1052
+ styles.toolbarMenuTrigger,
1053
+ explorationsMenuOpen && styles.toolbarButtonHovered,
1054
+ )}
1055
+ data-menu-open={
1056
+ explorationsMenuOpen ? "true" : undefined
1057
+ }
1058
+ aria-label="Explorations"
1059
+ aria-expanded={explorationsMenuOpen}
1060
+ aria-pressed={explorationsMenuOpen}
1061
+ {...TOOLBAR_HOVER_ONLY_BUTTON_PROPS}
1062
+ >
1063
+ <LayoutGrid size={16} strokeWidth={2} />
1064
+ </button>
1065
+ </DropdownMenuTrigger>
1066
+ </div>
1067
+ </ReviewToolbarTooltip>
1068
+
1069
+ <DropdownMenuContent
1070
+ {...TOOLBAR_MENU_CONTENT_PROPS}
1071
+ {...explorationsHover.panelProps}
1072
+ data-prototype-comment-theme={commentTheme}
1073
+ data-prototype-review-panel="explorations"
1074
+ >
1075
+ <div
1076
+ className={cn(
1077
+ pillStyles.controlsPanel,
1078
+ pillStyles.controlsPanelExplorations,
1079
+ )}
1080
+ >
1081
+ <CreateExplorationPanelTrigger onOpen={openCreateExplorationModal} />
1082
+ {review.variantSets.length > 0 ? (
1083
+ <div
1084
+ className={pillStyles.explorationBriefSeparator}
1085
+ role="separator"
1086
+ aria-hidden
1087
+ />
1088
+ ) : null}
1089
+ {review.variantSets.map((variantSet) => {
1090
+ const selected =
1091
+ review.variantsOpen &&
1092
+ review.activeVariantSetId === variantSet.id;
1093
+
1094
+ return (
1095
+ <DropdownMenuItem
1096
+ key={variantSet.id}
1097
+ className={cn(
1098
+ pillStyles.dropdownMenuItem,
1099
+ pillStyles.dropdownItemExploration,
1100
+ selected && pillStyles.dropdownMenuItemSelected,
1101
+ )}
1102
+ onSelect={() => {
1103
+ setExplorationsMenuOpen(false);
1104
+ blurToolbarFocus();
1105
+ review.openVariantSet(variantSet.id);
1106
+ }}
1107
+ >
1108
+ <span>{miniPillTextFromLabel(variantSet.label)}</span>
1109
+ <VariantSetLucideIcon
1110
+ variantSetId={variantSet.id}
1111
+ variantSetLabel={variantSet.label}
1112
+ size={12}
1113
+ strokeWidth={2}
1114
+ className={pillStyles.dropdownItemIcon}
1115
+ aria-hidden
1116
+ />
1117
+ </DropdownMenuItem>
1118
+ );
1119
+ })}
1120
+ </div>
1121
+ </DropdownMenuContent>
1122
+ </DropdownMenu>
1123
+
1124
+ <ReviewToolbarButton
1125
+ label={
1126
+ review.open && review.sidebarPanel === "comments"
1127
+ ? "Close comments panel"
1128
+ : "Open comments panel"
1129
+ }
1130
+ tooltip="Comments"
1131
+ pressed={review.open && review.sidebarPanel === "comments"}
1132
+ onClick={() => {
1133
+ if (review.open && review.sidebarPanel === "comments") {
1134
+ onClose();
1135
+ } else {
1136
+ closeShareMenu();
1137
+ onOpenCommentsPanel?.();
1138
+ }
1139
+ }}
1140
+ badge={
1141
+ commentCount > 0 ? (
1142
+ <span className={toolbarStyles.buttonBadge}>
1143
+ {commentCount > 99 ? "99+" : commentCount}
1144
+ </span>
1145
+ ) : undefined
1146
+ }
1147
+ >
1148
+ <MessageSquare size={16} strokeWidth={2} />
1149
+ </ReviewToolbarButton>
1150
+
1151
+ <ReviewToolbarButton
1152
+ label={
1153
+ review.open && review.sidebarPanel === "change-log"
1154
+ ? "Close overview"
1155
+ : "Open overview"
1156
+ }
1157
+ tooltip="Overview"
1158
+ pressed={review.open && review.sidebarPanel === "change-log"}
1159
+ onClick={() => {
1160
+ if (review.open && review.sidebarPanel === "change-log") {
1161
+ review.closeSidebar();
1162
+ } else {
1163
+ review.closeSettings();
1164
+ closeToolbarMenus();
1165
+ closeShareMenu();
1166
+ review.openSidebar("change-log");
1167
+ }
1168
+ }}
1169
+ >
1170
+ <ScrollText size={16} strokeWidth={2} />
1171
+ </ReviewToolbarButton>
1172
+
1173
+ <ReviewToolbarButton
1174
+ label={
1175
+ review.open && review.sidebarPanel === "spec"
1176
+ ? "Close PRs"
1177
+ : "Open PRs"
1178
+ }
1179
+ tooltip="PRs"
1180
+ pressed={review.open && review.sidebarPanel === "spec"}
1181
+ onClick={() => {
1182
+ if (review.open && review.sidebarPanel === "spec") {
1183
+ review.closeSidebar();
1184
+ } else {
1185
+ review.closeSettings();
1186
+ closeToolbarMenus();
1187
+ closeShareMenu();
1188
+ review.openSidebar("spec");
1189
+ }
1190
+ }}
1191
+ >
1192
+ <Rocket size={16} strokeWidth={2} />
1193
+ </ReviewToolbarButton>
1194
+ </div>
1195
+ </div>
1196
+ </div>
1197
+ );
1198
+
1199
+ return (
1200
+ <>
1201
+ {sidebarPortalTarget
1202
+ ? createPortal(
1203
+ <PrototypeReviewSidebar
1204
+ onSelect={onSelect}
1205
+ selectedId={selectedId}
1206
+ onClose={onClose}
1207
+ isCommentModeActive={isCommentModeActive}
1208
+ onToggleCommentMode={onToggleCommentMode}
1209
+ />,
1210
+ sidebarPortalTarget,
1211
+ )
1212
+ : null}
1213
+
1214
+ {chromePortalTarget ? createPortal(chrome, chromePortalTarget) : null}
1215
+
1216
+ {review.highlightTargetId ? (
1217
+ <PrototypeTweakHighlight
1218
+ targetId={review.highlightTargetId}
1219
+ onComplete={review.clearHighlight}
1220
+ />
1221
+ ) : null}
1222
+
1223
+ <PrototypeCreateExplorationModal
1224
+ open={createExplorationModalOpen}
1225
+ onOpenChange={setCreateExplorationModalOpen}
1226
+ slug={review.slug}
1227
+ existingExplorations={review.variantSets.map((variantSet) => ({
1228
+ id: variantSet.id,
1229
+ label: variantSet.label,
1230
+ }))}
1231
+ />
1232
+
1233
+ <PrototypeCreateStateModal
1234
+ open={createStateModalOpen}
1235
+ onOpenChange={setCreateStateModalOpen}
1236
+ slug={review.slug}
1237
+ existingStates={review.previewStatesRef.current}
1238
+ />
1239
+ </>
1240
+ );
1241
+ }