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,98 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef } from "react";
4
+ import type {
5
+ MouseEvent as ReactMouseEvent,
6
+ PointerEvent as ReactPointerEvent,
7
+ } from "react";
8
+
9
+ export const TOOLBAR_PANEL_HOVER_OPEN_DELAY_MS = 100;
10
+ export const TOOLBAR_PANEL_HOVER_CLOSE_DELAY_MS = 150;
11
+
12
+ export function preventToolbarHoverButtonActivation(
13
+ event:
14
+ | ReactMouseEvent<HTMLButtonElement>
15
+ | ReactPointerEvent<HTMLButtonElement>,
16
+ ) {
17
+ event.preventDefault();
18
+ event.stopPropagation();
19
+ }
20
+
21
+ export const TOOLBAR_HOVER_ONLY_BUTTON_PROPS = {
22
+ onClick: preventToolbarHoverButtonActivation,
23
+ onPointerDown: preventToolbarHoverButtonActivation,
24
+ } as const;
25
+
26
+ type UseToolbarHoverPanelOptions = {
27
+ onOpen: () => void;
28
+ onClose: () => void;
29
+ openDelayMs?: number;
30
+ closeDelayMs?: number;
31
+ };
32
+
33
+ export function useToolbarHoverPanel({
34
+ onOpen,
35
+ onClose,
36
+ openDelayMs = TOOLBAR_PANEL_HOVER_OPEN_DELAY_MS,
37
+ closeDelayMs = TOOLBAR_PANEL_HOVER_CLOSE_DELAY_MS,
38
+ }: UseToolbarHoverPanelOptions) {
39
+ const openTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
40
+ const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
41
+
42
+ const clearOpenTimer = useCallback(() => {
43
+ if (openTimerRef.current) {
44
+ clearTimeout(openTimerRef.current);
45
+ openTimerRef.current = null;
46
+ }
47
+ }, []);
48
+
49
+ const clearCloseTimer = useCallback(() => {
50
+ if (closeTimerRef.current) {
51
+ clearTimeout(closeTimerRef.current);
52
+ closeTimerRef.current = null;
53
+ }
54
+ }, []);
55
+
56
+ const cancelClose = useCallback(() => {
57
+ clearCloseTimer();
58
+ }, [clearCloseTimer]);
59
+
60
+ const scheduleOpen = useCallback(
61
+ (event: ReactPointerEvent<HTMLElement>) => {
62
+ if (event.pointerType === "touch") return;
63
+
64
+ clearCloseTimer();
65
+ clearOpenTimer();
66
+ openTimerRef.current = setTimeout(() => {
67
+ onOpen();
68
+ }, openDelayMs);
69
+ },
70
+ [clearCloseTimer, clearOpenTimer, onOpen, openDelayMs],
71
+ );
72
+
73
+ const scheduleClose = useCallback(() => {
74
+ clearOpenTimer();
75
+ clearCloseTimer();
76
+ closeTimerRef.current = setTimeout(() => {
77
+ onClose();
78
+ }, closeDelayMs);
79
+ }, [clearCloseTimer, clearOpenTimer, closeDelayMs, onClose]);
80
+
81
+ useEffect(() => {
82
+ return () => {
83
+ clearOpenTimer();
84
+ clearCloseTimer();
85
+ };
86
+ }, [clearCloseTimer, clearOpenTimer]);
87
+
88
+ return {
89
+ triggerProps: {
90
+ onPointerEnter: scheduleOpen,
91
+ onPointerLeave: scheduleClose,
92
+ },
93
+ panelProps: {
94
+ onPointerEnter: cancelClose,
95
+ onPointerLeave: scheduleClose,
96
+ },
97
+ };
98
+ }
@@ -0,0 +1,55 @@
1
+ import type { LucideIcon, LucideProps } from "lucide-react";
2
+ import {
3
+ ArrowDownUp,
4
+ CalendarDays,
5
+ Clock,
6
+ FilterX,
7
+ Inbox,
8
+ LayoutGrid,
9
+ PanelBottom,
10
+ Shapes,
11
+ Smartphone,
12
+ Table,
13
+ } from "lucide-react";
14
+
15
+ type VariantSetIconRule = {
16
+ test: RegExp;
17
+ icon: LucideIcon;
18
+ };
19
+
20
+ /** First match wins — order rules from most specific to broadest. */
21
+ const VARIANT_SET_ICON_RULES: VariantSetIconRule[] = [
22
+ { test: /retention|expir|before/i, icon: Clock },
23
+ { test: /calendar|date.?picker|date-range/i, icon: CalendarDays },
24
+ { test: /mobile/i, icon: Smartphone },
25
+ { test: /order|sort/i, icon: ArrowDownUp },
26
+ { test: /filter/i, icon: FilterX },
27
+ { test: /no.?data|empty/i, icon: Inbox },
28
+ { test: /table|row/i, icon: Table },
29
+ { test: /footer|dock/i, icon: PanelBottom },
30
+ { test: /icon/i, icon: Shapes },
31
+ ];
32
+
33
+ export function resolveVariantSetLucideIcon(id: string, label: string): LucideIcon {
34
+ const haystack = `${id} ${label}`.toLowerCase();
35
+
36
+ for (const rule of VARIANT_SET_ICON_RULES) {
37
+ if (rule.test.test(haystack)) return rule.icon;
38
+ }
39
+
40
+ return LayoutGrid;
41
+ }
42
+
43
+ type VariantSetLucideIconProps = LucideProps & {
44
+ variantSetId: string;
45
+ variantSetLabel: string;
46
+ };
47
+
48
+ export function VariantSetLucideIcon({
49
+ variantSetId,
50
+ variantSetLabel,
51
+ ...props
52
+ }: VariantSetLucideIconProps) {
53
+ const Icon = resolveVariantSetLucideIcon(variantSetId, variantSetLabel);
54
+ return <Icon {...props} />;
55
+ }
@@ -0,0 +1,33 @@
1
+ export const PROTOTYPE_ROOT_ID = "prototype-root";
2
+
3
+ export const PROTOTYPE_PREVIEW_STAGE_ID = "prototype-preview-stage";
4
+
5
+ export const PROTOTYPE_CHROME_ROOT_ID = "prototype-chrome-root";
6
+
7
+ export const PROTOTYPE_VIEWPORT_ID = "prototype-viewport";
8
+
9
+ export const PROTOTYPE_SCREENSHOT_ATTR = "data-prototype-screenshot";
10
+
11
+ export const PROTOTYPE_COMMENTS_SIDEBAR_ROOT_ID =
12
+ "prototype-comments-sidebar-root";
13
+
14
+ export function getPrototypePortalContainer(): HTMLElement | undefined {
15
+ if (typeof document === "undefined") return undefined;
16
+ return document.getElementById(PROTOTYPE_ROOT_ID) ?? undefined;
17
+ }
18
+
19
+ export function getPrototypePreviewStage(): HTMLElement | undefined {
20
+ if (typeof document === "undefined") return undefined;
21
+ return document.getElementById(PROTOTYPE_PREVIEW_STAGE_ID) ?? undefined;
22
+ }
23
+
24
+ export function getPrototypeChromeRoot(): HTMLElement | undefined {
25
+ if (typeof document === "undefined") return undefined;
26
+ return document.getElementById(PROTOTYPE_CHROME_ROOT_ID) ?? undefined;
27
+ }
28
+
29
+ export function getPrototypeScreenshotRoot(): HTMLElement | undefined {
30
+ if (typeof document === "undefined") return undefined;
31
+ const root = document.querySelector(`[${PROTOTYPE_SCREENSHOT_ATTR}]`);
32
+ return root instanceof HTMLElement ? root : undefined;
33
+ }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { cn } from "@prototype/lib/utils";
4
+ import { CheckIcon, CopyIcon, SaveIcon } from "lucide-react";
5
+ import { createElement, useCallback, useRef, useState } from "react";
6
+ import { toast } from "sonner";
7
+
8
+ type CopyState = "idle" | "scaling-out" | "copied" | "reverting-out" | null;
9
+
10
+ const SCALE_OUT_MS = 200;
11
+ const COPIED_MS = 1500;
12
+
13
+ // Hook to copy text to clipboard with a cooldown. the cooldownTime is the time
14
+ // in milliseconds before the isCopied state is reset to false. This is useful
15
+ // to display UI that the value was copied.
16
+ export default function useCopyToClipboard({
17
+ cooldownTime = COPIED_MS,
18
+ idleIcon = "copy",
19
+ }: {
20
+ cooldownTime?: number;
21
+ idleIcon?: "copy" | "save";
22
+ } = {}) {
23
+ const [state, setState] = useState<CopyState>("idle");
24
+ const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
25
+ const isCopied = state === "copied" || state === "reverting-out";
26
+
27
+ const copy = useCallback(
28
+ (text: string) => {
29
+ clearTimeout(timerRef.current);
30
+ setState("idle");
31
+
32
+ const idle = () => {
33
+ setState("idle");
34
+ };
35
+
36
+ const revert = () => {
37
+ setState("reverting-out");
38
+ timerRef.current = setTimeout(idle, SCALE_OUT_MS);
39
+ };
40
+
41
+ const copied = () => {
42
+ setState("copied");
43
+ timerRef.current = setTimeout(revert, cooldownTime);
44
+ };
45
+
46
+ navigator.clipboard
47
+ .writeText(text)
48
+ .catch(() => {
49
+ toast.error("Copy error", {
50
+ description: "Failed to copy value to clipboard",
51
+ });
52
+ })
53
+ .finally(() => {
54
+ setState("scaling-out");
55
+ timerRef.current = setTimeout(copied, SCALE_OUT_MS);
56
+ });
57
+ },
58
+ [cooldownTime],
59
+ );
60
+
61
+ const icon = isCopied
62
+ ? createElement(CheckIcon, {
63
+ size: 13,
64
+ strokeWidth: 2,
65
+ className: cn(
66
+ "motion-reduce:animate-none",
67
+ state === "copied" && "animate-blur-scale-in",
68
+ state === "reverting-out" && "animate-blur-scale-out",
69
+ ),
70
+ })
71
+ : createElement(idleIcon === "save" ? SaveIcon : CopyIcon, {
72
+ size: 13,
73
+ className: cn(
74
+ "motion-reduce:animate-none",
75
+ state === "idle" && "animate-blur-scale-in",
76
+ state === "scaling-out" && "animate-blur-scale-out",
77
+ ),
78
+ });
79
+
80
+ return { isCopied, copy, icon };
81
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,81 @@
1
+ export type VercelPreviewProject = {
2
+ name: string;
3
+ previewUrl: string;
4
+ inspectorUrl?: string;
5
+ status?: string;
6
+ };
7
+
8
+ export type VercelPreviewFromPr = {
9
+ projects: VercelPreviewProject[];
10
+ requestReviewUrl?: string;
11
+ };
12
+
13
+ type VercelGithubCommentPayload = {
14
+ projects?: Array<{
15
+ name?: string;
16
+ previewUrl?: string;
17
+ inspectorUrl?: string;
18
+ nextCommitStatus?: string;
19
+ }>;
20
+ requestReviewUrl?: string;
21
+ };
22
+
23
+ const VERCEL_COMMENT_PATTERN = /\[vc\]:\s*#[^:]+:([A-Za-z0-9+/=]+)/;
24
+
25
+ export function parseGithubPrUrl(
26
+ prUrl: string,
27
+ ): { owner: string; repo: string; number: number } | null {
28
+ try {
29
+ const url = new URL(prUrl);
30
+ const match = url.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
31
+ if (!match) return null;
32
+ const number = Number(match[3]);
33
+ if (!Number.isInteger(number) || number < 1) return null;
34
+ return { owner: match[1], repo: match[2], number };
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export function parseVercelGithubCommentBody(
41
+ body: string,
42
+ ): VercelPreviewFromPr | null {
43
+ const match = body.match(VERCEL_COMMENT_PATTERN);
44
+ if (!match) return null;
45
+
46
+ try {
47
+ const payload = JSON.parse(
48
+ Buffer.from(match[1], "base64").toString("utf8"),
49
+ ) as VercelGithubCommentPayload;
50
+
51
+ const projects = (payload.projects ?? [])
52
+ .filter((project) => typeof project.previewUrl === "string")
53
+ .map((project) => ({
54
+ name: project.name ?? "preview",
55
+ previewUrl: project.previewUrl as string,
56
+ inspectorUrl: project.inspectorUrl,
57
+ status: project.nextCommitStatus,
58
+ }));
59
+
60
+ if (projects.length === 0) return null;
61
+
62
+ return {
63
+ projects,
64
+ requestReviewUrl: payload.requestReviewUrl,
65
+ };
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ export function buildVercelPreviewPageUrl(
72
+ previewHostOrUrl: string,
73
+ previewPath = "/",
74
+ ): string {
75
+ const normalized = previewHostOrUrl.startsWith("http")
76
+ ? previewHostOrUrl
77
+ : `https://${previewHostOrUrl}`;
78
+ const url = new URL(normalized);
79
+ url.pathname = previewPath;
80
+ return url.toString();
81
+ }
@@ -0,0 +1,100 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ import type { PrototypeConfig } from "@prototype/lib/prototypes/prototype-config-types";
4
+
5
+ import {
6
+ createPrototypeChangelogMetaRoute,
7
+ createPrototypeCommentIdRoute,
8
+ createPrototypeCommentsRoute,
9
+ createPrototypeDesignExplorationDefaultRoute,
10
+ createPrototypeScreenshotRoute,
11
+ } from "./create-prototype-api-routes";
12
+ import { createGalleryApi } from "./gallery/create-gallery-api";
13
+ import { GET as getPrVercelPreview } from "./pr-vercel-preview-route";
14
+
15
+ type ApiRouteContext = {
16
+ params: Promise<{ path: string[] }>;
17
+ };
18
+
19
+ export function createPrototypeApiRoute(_config: PrototypeConfig) {
20
+ const commentsRoute = createPrototypeCommentsRoute(_config);
21
+ const commentIdRoute = createPrototypeCommentIdRoute(_config);
22
+ const changelogMetaRoute = createPrototypeChangelogMetaRoute(_config);
23
+ const designExplorationDefaultRoute =
24
+ createPrototypeDesignExplorationDefaultRoute(_config);
25
+ const screenshotRoute = createPrototypeScreenshotRoute(_config);
26
+ const galleryApi =
27
+ _config.gallery === false ? null : createGalleryApi(_config.gallery);
28
+
29
+ async function dispatch(
30
+ request: NextRequest,
31
+ context: ApiRouteContext,
32
+ ): Promise<Response> {
33
+ const { path } = await context.params;
34
+ const method = request.method;
35
+
36
+ if (path[0] === "pr-vercel-preview" && path.length === 1 && method === "GET") {
37
+ return getPrVercelPreview(request);
38
+ }
39
+
40
+ if (galleryApi) {
41
+ const galleryResponse = await galleryApi.dispatch(request, path);
42
+ if (galleryResponse) {
43
+ return galleryResponse;
44
+ }
45
+ }
46
+
47
+ if (path[0] === "prototypes" && path.length >= 2) {
48
+ const slug = path[1]!;
49
+
50
+ if (path.length === 3 && path[2] === "comments") {
51
+ if (method === "GET") {
52
+ return commentsRoute.GET(request, { params: Promise.resolve({ slug }) });
53
+ }
54
+ if (method === "PUT") {
55
+ return commentsRoute.PUT(request, { params: Promise.resolve({ slug }) });
56
+ }
57
+ }
58
+
59
+ if (path.length === 4 && path[2] === "comments" && method === "PUT") {
60
+ const id = path[3]!;
61
+ return commentIdRoute.PUT(request, {
62
+ params: Promise.resolve({ slug, id }),
63
+ });
64
+ }
65
+
66
+ if (path.length === 3 && path[2] === "changelog-meta") {
67
+ if (method === "GET") {
68
+ return changelogMetaRoute.GET(request, { params: Promise.resolve({ slug }) });
69
+ }
70
+ if (method === "PUT") {
71
+ return changelogMetaRoute.PUT(request, { params: Promise.resolve({ slug }) });
72
+ }
73
+ }
74
+
75
+ if (path.length === 3 && path[2] === "design-exploration-default") {
76
+ if (method === "GET") {
77
+ return designExplorationDefaultRoute.GET(request, {
78
+ params: Promise.resolve({ slug }),
79
+ });
80
+ }
81
+ if (method === "PUT") {
82
+ return designExplorationDefaultRoute.PUT(request, {
83
+ params: Promise.resolve({ slug }),
84
+ });
85
+ }
86
+ }
87
+
88
+ if (path.length === 3 && path[2] === "screenshot" && method === "POST") {
89
+ return screenshotRoute.POST(request, { params: Promise.resolve({ slug }) });
90
+ }
91
+ }
92
+
93
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
94
+ }
95
+
96
+ return { dispatch };
97
+ }
98
+
99
+ /** @deprecated Use createPrototypeApiRoute */
100
+ export const createHostApiRoute = createPrototypeApiRoute;