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,97 @@
1
+ "use client";
2
+
3
+ import { cn } from "@prototype/lib/utils";
4
+ import { ChevronDown } from "lucide-react";
5
+ import type { ReactNode } from "react";
6
+
7
+ type PanelProps = {
8
+ children: ReactNode;
9
+ className?: string;
10
+ };
11
+
12
+ export function Panel({ children, className }: PanelProps) {
13
+ return (
14
+ <div className={cn("flex flex-col gap-0", className)}>{children}</div>
15
+ );
16
+ }
17
+
18
+ type PanelSectionProps = {
19
+ title: ReactNode;
20
+ expanded: boolean;
21
+ onExpandedChange: (expanded: boolean) => void;
22
+ children: ReactNode;
23
+ className?: string;
24
+ headerClassName?: string;
25
+ };
26
+
27
+ export function PanelSection({
28
+ title,
29
+ expanded,
30
+ onExpandedChange,
31
+ children,
32
+ className,
33
+ headerClassName,
34
+ }: PanelSectionProps) {
35
+ return (
36
+ <section className={cn("border-t border-border", className)}>
37
+ <button
38
+ type="button"
39
+ className={cn(
40
+ "flex w-full items-center justify-between gap-2 px-3 py-2.5 text-left text-xs font-medium uppercase tracking-wide text-foreground transition-colors duration-200 ease hover:text-foreground",
41
+ headerClassName,
42
+ )}
43
+ aria-expanded={expanded}
44
+ onClick={() => onExpandedChange(!expanded)}
45
+ >
46
+ {title}
47
+ <ChevronDown
48
+ className={cn(
49
+ "tool-panel-section-chevron size-4 shrink-0 text-muted-foreground",
50
+ expanded && "is-expanded",
51
+ )}
52
+ aria-hidden
53
+ />
54
+ </button>
55
+ <div
56
+ className={cn("tool-panel-section-content", expanded && "is-expanded")}
57
+ >
58
+ <div className="overflow-hidden">
59
+ <PanelBody>{children}</PanelBody>
60
+ </div>
61
+ </div>
62
+ </section>
63
+ );
64
+ }
65
+
66
+ export function PanelBody({
67
+ children,
68
+ className,
69
+ }: {
70
+ children: ReactNode;
71
+ className?: string;
72
+ }) {
73
+ return (
74
+ <div className={cn("px-3 pb-3 text-sm text-muted-foreground", className)}>
75
+ {children}
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export function PanelHeader({
81
+ children,
82
+ className,
83
+ }: {
84
+ children: ReactNode;
85
+ className?: string;
86
+ }) {
87
+ return (
88
+ <div
89
+ className={cn(
90
+ "text-lg font-semibold tracking-tight text-foreground",
91
+ className,
92
+ )}
93
+ >
94
+ {children}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { DropdownMenuItem } from "@prototype/components/ui/dropdown-menu";
4
+ import { Separator } from "@prototype/components/ui/separator";
5
+ import { cn } from "@prototype/lib/utils";
6
+ import type { ComponentProps, ReactNode } from "react";
7
+
8
+ type ReviewDropdownMenuItemProps = ComponentProps<typeof DropdownMenuItem> & {
9
+ selected?: boolean;
10
+ children: ReactNode;
11
+ };
12
+
13
+ export function ReviewDropdownMenuItem({
14
+ selected = false,
15
+ className,
16
+ children,
17
+ ...props
18
+ }: ReviewDropdownMenuItemProps) {
19
+ return (
20
+ <DropdownMenuItem
21
+ className={cn(selected && "bg-accent", className)}
22
+ {...props}
23
+ >
24
+ {children}
25
+ </DropdownMenuItem>
26
+ );
27
+ }
28
+
29
+ export function ReviewDropdownMenuSeparator({
30
+ className,
31
+ }: {
32
+ className?: string;
33
+ }) {
34
+ return <Separator className={cn("my-1", className)} />;
35
+ }
@@ -0,0 +1,293 @@
1
+ "use client";
2
+
3
+ import { cn } from "@prototype/lib/utils";
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ type PointerEvent as ReactPointerEvent,
10
+ type ReactNode,
11
+ } from "react";
12
+
13
+ import { IconButton } from "./icon-button";
14
+
15
+ export const DEFAULT_SIDEBAR_WIDTH = 400;
16
+ export const MIN_SIDEBAR_WIDTH = 280;
17
+ export const MAX_SIDEBAR_WIDTH = 720;
18
+
19
+ export function clampSidebarWidth(width: number) {
20
+ return Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, width));
21
+ }
22
+
23
+ const SIDEBAR_TITLE_CLASS =
24
+ "font-display truncate text-sm font-semibold text-[var(--tool-chrome-icon-hover)]";
25
+
26
+ export const SIDEBAR_ICON_BUTTON_CLASS =
27
+ "text-[var(--tool-chrome-icon)] hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-icon-hover)] focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0";
28
+
29
+ type SidebarProps = {
30
+ open: boolean;
31
+ text: string;
32
+ onClose: () => void;
33
+ children: ReactNode;
34
+ titleAddon?: ReactNode;
35
+ headerActions?: ReactNode;
36
+ ariaLabel?: string;
37
+ dataAttribute?: string;
38
+ widthCssVar?: string;
39
+ className?: string;
40
+ /**
41
+ * When true, the panel floats over page content instead of reserving inline
42
+ * layout width. It is taken out of flow (clipped, transform slide-in) so
43
+ * opening/closing it never reflows or squishes the parent. Use this for
44
+ * selector-style panels that should overlay, not push, the page.
45
+ */
46
+ overlay?: boolean;
47
+ };
48
+
49
+ export function Sidebar({
50
+ open,
51
+ text,
52
+ onClose,
53
+ children,
54
+ titleAddon,
55
+ headerActions,
56
+ ariaLabel = "Sidebar",
57
+ dataAttribute = "data-platform-sidebar",
58
+ widthCssVar = "--platform-sidebar-width",
59
+ className,
60
+ overlay = false,
61
+ }: SidebarProps) {
62
+ const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH);
63
+ const [isResizing, setIsResizing] = useState(false);
64
+ const resizeStartRef = useRef<{ startX: number; startWidth: number } | null>(
65
+ null,
66
+ );
67
+
68
+ const applySidebarWidth = useCallback(
69
+ (width: number) => {
70
+ const clampedWidth = clampSidebarWidth(width);
71
+ setSidebarWidth((current) =>
72
+ current === clampedWidth ? current : clampedWidth,
73
+ );
74
+ document.documentElement.style.setProperty(
75
+ widthCssVar,
76
+ `${clampedWidth}px`,
77
+ );
78
+ },
79
+ [widthCssVar],
80
+ );
81
+
82
+ useEffect(() => {
83
+ document.documentElement.setAttribute("data-platform-sidebar-inline", "");
84
+ document.documentElement.style.setProperty(
85
+ widthCssVar,
86
+ `${DEFAULT_SIDEBAR_WIDTH}px`,
87
+ );
88
+
89
+ return () => {
90
+ document.documentElement.style.removeProperty(widthCssVar);
91
+ document.documentElement.removeAttribute("data-platform-sidebar-open");
92
+ document.documentElement.removeAttribute("data-platform-sidebar-resizing");
93
+ document.documentElement.removeAttribute("data-platform-sidebar-inline");
94
+ };
95
+ }, [widthCssVar]);
96
+
97
+ useEffect(() => {
98
+ document.documentElement.toggleAttribute("data-platform-sidebar-open", open);
99
+ }, [open]);
100
+
101
+ useEffect(() => {
102
+ document.documentElement.toggleAttribute(
103
+ "data-platform-sidebar-resizing",
104
+ isResizing,
105
+ );
106
+ }, [isResizing]);
107
+
108
+ const handleResizeStart = useCallback(
109
+ (event: ReactPointerEvent<HTMLDivElement>) => {
110
+ if (!open) return;
111
+ event.preventDefault();
112
+ resizeStartRef.current = {
113
+ startX: event.clientX,
114
+ startWidth: sidebarWidth,
115
+ };
116
+ setIsResizing(true);
117
+ event.currentTarget.setPointerCapture(event.pointerId);
118
+ },
119
+ [open, sidebarWidth],
120
+ );
121
+
122
+ const handleResizeMove = useCallback(
123
+ (event: ReactPointerEvent<HTMLDivElement>) => {
124
+ if (!resizeStartRef.current) return;
125
+ const { startX, startWidth } = resizeStartRef.current;
126
+ applySidebarWidth(startWidth + (startX - event.clientX));
127
+ },
128
+ [applySidebarWidth],
129
+ );
130
+
131
+ const handleResizeEnd = useCallback(
132
+ (event: ReactPointerEvent<HTMLDivElement>) => {
133
+ if (!resizeStartRef.current) return;
134
+ resizeStartRef.current = null;
135
+ setIsResizing(false);
136
+ event.currentTarget.releasePointerCapture(event.pointerId);
137
+ },
138
+ [],
139
+ );
140
+
141
+ const aside = (
142
+ <aside
143
+ {...{ [dataAttribute]: "" }}
144
+ aria-hidden={!open}
145
+ aria-label={ariaLabel}
146
+ style={
147
+ overlay
148
+ ? {
149
+ width: sidebarWidth,
150
+ transform: open ? "translateX(0)" : "translateX(100%)",
151
+ }
152
+ : { width: open ? sidebarWidth : 0 }
153
+ }
154
+ className={cn(
155
+ "flex h-full shrink-0 flex-col overflow-hidden bg-[var(--tool-chrome-ground)] text-[var(--tool-chrome-icon)]",
156
+ isResizing && "select-none transition-none",
157
+ overlay
158
+ ? "absolute inset-y-0 right-0 shadow-2xl transition-transform duration-200 ease-out"
159
+ : "relative transition-[width] duration-200 ease-out",
160
+ overlay && (open ? "pointer-events-auto" : "pointer-events-none"),
161
+ className,
162
+ )}
163
+ >
164
+ {open ? (
165
+ <SidebarResizeHandle
166
+ open={open}
167
+ sidebarWidth={sidebarWidth}
168
+ isResizing={isResizing}
169
+ onResizeStart={handleResizeStart}
170
+ onResizeMove={handleResizeMove}
171
+ onResizeEnd={handleResizeEnd}
172
+ />
173
+ ) : null}
174
+ {open ? (
175
+ <SidebarHeader
176
+ text={text}
177
+ titleAddon={titleAddon}
178
+ actions={
179
+ <>
180
+ {headerActions}
181
+ <IconButton
182
+ type="button"
183
+ onClick={onClose}
184
+ aria-label="Close sidebar"
185
+ className={SIDEBAR_ICON_BUTTON_CLASS}
186
+ >
187
+ <CloseIcon />
188
+ </IconButton>
189
+ </>
190
+ }
191
+ />
192
+ ) : null}
193
+ {open ? (
194
+ <SidebarContent>
195
+ <div className="px-4 pt-4">{children}</div>
196
+ </SidebarContent>
197
+ ) : null}
198
+ </aside>
199
+ );
200
+
201
+ if (!overlay) return aside;
202
+
203
+ // Clip layer: full-viewport, click-through, hides the panel off the right
204
+ // edge while it is closed so it never produces horizontal overflow. Sits
205
+ // below the floating review pill (z-index 1050) so its variant switcher
206
+ // stays usable while the panel is open.
207
+ return (
208
+ <div
209
+ aria-hidden={!open}
210
+ className="pointer-events-none fixed inset-0 z-40 overflow-hidden"
211
+ >
212
+ {aside}
213
+ </div>
214
+ );
215
+ }
216
+
217
+ type SidebarResizeHandleProps = {
218
+ open: boolean;
219
+ sidebarWidth: number;
220
+ isResizing: boolean;
221
+ onResizeStart: (event: ReactPointerEvent<HTMLDivElement>) => void;
222
+ onResizeMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
223
+ onResizeEnd: (event: ReactPointerEvent<HTMLDivElement>) => void;
224
+ };
225
+
226
+ export function SidebarResizeHandle({
227
+ open,
228
+ sidebarWidth,
229
+ onResizeStart,
230
+ onResizeMove,
231
+ onResizeEnd,
232
+ }: SidebarResizeHandleProps) {
233
+ return (
234
+ <div
235
+ className="absolute top-0 left-[-4px] z-[1] h-full w-2 cursor-col-resize touch-none before:absolute before:top-0 before:left-[3px] before:h-full before:w-0.5 before:bg-transparent before:transition-colors hover:before:bg-border [[data-platform-sidebar-resizing]_&]:before:bg-border"
236
+ onPointerDown={onResizeStart}
237
+ onPointerMove={onResizeMove}
238
+ onPointerUp={onResizeEnd}
239
+ onPointerCancel={onResizeEnd}
240
+ role="separator"
241
+ aria-orientation="vertical"
242
+ aria-label="Resize sidebar"
243
+ aria-valuemin={MIN_SIDEBAR_WIDTH}
244
+ aria-valuemax={MAX_SIDEBAR_WIDTH}
245
+ aria-valuenow={open ? sidebarWidth : MIN_SIDEBAR_WIDTH}
246
+ />
247
+ );
248
+ }
249
+
250
+ export function SidebarHeader({
251
+ text,
252
+ titleAddon,
253
+ actions,
254
+ }: {
255
+ text: string;
256
+ titleAddon?: ReactNode;
257
+ actions?: ReactNode;
258
+ }) {
259
+ return (
260
+ <div className="flex h-[3.25rem] max-h-[3.25rem] min-h-[3.25rem] shrink-0 items-center justify-between gap-3 border-b border-[var(--tool-chrome-border)] px-4">
261
+ <div className="flex min-w-0 flex-1 items-baseline gap-0">
262
+ <span className={cn(SIDEBAR_TITLE_CLASS, "truncate")}>{text}</span>
263
+ {titleAddon}
264
+ </div>
265
+ {actions ? (
266
+ <div className="flex shrink-0 items-center gap-1">{actions}</div>
267
+ ) : null}
268
+ </div>
269
+ );
270
+ }
271
+
272
+ export function SidebarContent({ children }: { children: ReactNode }) {
273
+ return (
274
+ <div className="min-h-0 flex-1 overflow-y-auto pb-4">{children}</div>
275
+ );
276
+ }
277
+
278
+ function CloseIcon() {
279
+ return (
280
+ <svg
281
+ width="18"
282
+ height="18"
283
+ viewBox="0 0 24 24"
284
+ fill="none"
285
+ stroke="currentColor"
286
+ strokeWidth="2"
287
+ strokeLinecap="round"
288
+ aria-hidden
289
+ >
290
+ <path d="M6 6l12 12M18 6L6 18" />
291
+ </svg>
292
+ );
293
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { IconButton } from "@prototype/components/platform-ui/icon-button";
4
+ import { cn } from "@prototype/lib/utils";
5
+ import type { ComponentProps, ReactNode } from "react";
6
+
7
+ type ToolbarIconButtonProps = ComponentProps<typeof IconButton> & {
8
+ children: ReactNode;
9
+ wrapperClassName?: string;
10
+ };
11
+
12
+ /** Sidebar / panel icon control — shadcn ghost button. */
13
+ export function ToolbarIconButton({
14
+ className,
15
+ wrapperClassName,
16
+ children,
17
+ ...props
18
+ }: ToolbarIconButtonProps) {
19
+ return (
20
+ <div className={cn("inline-flex shrink-0", wrapperClassName)}>
21
+ <IconButton
22
+ variant="ghost"
23
+ size="icon"
24
+ className={cn(
25
+ "size-8 text-[var(--tool-chrome-icon)] hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-icon-hover)]",
26
+ className,
27
+ )}
28
+ {...props}
29
+ >
30
+ {children}
31
+ </IconButton>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { cn } from "@prototype/lib/utils";
4
+ import type { ReactNode } from "react";
5
+
6
+ export function Toolbar({
7
+ children,
8
+ className,
9
+ }: {
10
+ children: ReactNode;
11
+ className?: string;
12
+ }) {
13
+ return (
14
+ <div
15
+ className={cn(
16
+ "inline-flex items-center gap-1 rounded-full border border-border bg-card p-1 shadow-sm",
17
+ className,
18
+ )}
19
+ >
20
+ {children}
21
+ </div>
22
+ );
23
+ }
24
+
25
+ export function ToolbarGroup({
26
+ children,
27
+ className,
28
+ }: {
29
+ children: ReactNode;
30
+ className?: string;
31
+ }) {
32
+ return (
33
+ <div className={cn("flex items-center gap-0.5", className)}>{children}</div>
34
+ );
35
+ }
36
+
37
+ export { IconButton as ToolbarButton } from "./icon-button";
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import Image from "next/image";
4
+ import Link from "next/link";
5
+
6
+ import type { PrototypeMetadata } from "@prototype/lib/prototypes/prototype-config-types";
7
+ import { screenshotSrc } from "@prototype/lib/prototypes/screenshot-src";
8
+ import { cn } from "@prototype/lib/utils";
9
+
10
+ type PrototypeGalleryClientProps = {
11
+ prototypes: PrototypeMetadata[];
12
+ screenshotVersions: Record<string, number>;
13
+ };
14
+
15
+ export function PrototypeGalleryClient({
16
+ prototypes,
17
+ screenshotVersions,
18
+ }: PrototypeGalleryClientProps) {
19
+ return (
20
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
21
+ {prototypes.map((prototype) => (
22
+ <Link
23
+ key={prototype.slug}
24
+ href={`/prototypes/${prototype.slug}`}
25
+ className="group"
26
+ >
27
+ <div
28
+ className={cn(
29
+ "rounded-xl",
30
+ "shadow-[0_1px_3px_0_rgb(0_0_0/0.06)]",
31
+ "translate-y-0 transition-[translate,box-shadow] duration-300 ease-out will-change-[translate]",
32
+ "group-hover:-translate-y-1 group-hover:shadow-[0_4px_12px_-4px_rgb(0_0_0/0.10)]",
33
+ )}
34
+ >
35
+ <div className="border-border bg-background relative aspect-video overflow-hidden rounded-xl border">
36
+ <Image
37
+ src={screenshotSrc(
38
+ prototype.screenshot,
39
+ screenshotVersions[prototype.slug],
40
+ )}
41
+ alt={prototype.title}
42
+ width={1600}
43
+ height={900}
44
+ quality={90}
45
+ sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
46
+ className="h-full w-full object-cover object-top"
47
+ />
48
+ </div>
49
+ </div>
50
+ <p className="text-sm text-muted-foreground group-hover:text-foreground mt-2 transition-colors duration-300 ease-out">
51
+ {prototype.title}
52
+ </p>
53
+ </Link>
54
+ ))}
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import { PrototypeToolThemeProvider } from "@prototype/lib/prototypes/use-prototype-tool-theme";
4
+ import type { ReactNode } from "react";
5
+
6
+ type PrototypeProviderProps = {
7
+ children: ReactNode;
8
+ className?: string;
9
+ };
10
+
11
+ export function PrototypeProvider({
12
+ children,
13
+ className,
14
+ }: PrototypeProviderProps) {
15
+ return (
16
+ <PrototypeToolThemeProvider className={className}>
17
+ {children}
18
+ </PrototypeToolThemeProvider>
19
+ );
20
+ }
@@ -0,0 +1,37 @@
1
+ import pillStyles from "@prototype/components/prototypes/prototype-floating-pill.module.scss";
2
+
3
+ type MiniPillLabelProps = {
4
+ label: string;
5
+ };
6
+
7
+ /** Text after a leading symbol/emoji token in legacy "↕️ Label" mini pill copy. */
8
+ export function miniPillTextFromLabel(label: string): string {
9
+ const spaceIndex = label.indexOf(" ");
10
+ if (spaceIndex <= 0) return label;
11
+
12
+ const prefix = label.slice(0, spaceIndex);
13
+ if (!/[\p{L}\p{N}]/u.test(prefix)) {
14
+ return label.slice(spaceIndex + 1);
15
+ }
16
+
17
+ return label;
18
+ }
19
+
20
+ /** Renders "emoji Label" mini pill copy with spacing between icon and text. */
21
+ export function MiniPillLabel({ label }: MiniPillLabelProps) {
22
+ const spaceIndex = label.indexOf(" ");
23
+ if (spaceIndex <= 0) {
24
+ return <>{label}</>;
25
+ }
26
+
27
+ return (
28
+ <>
29
+ <span className={pillStyles.miniPillEmoji} aria-hidden>
30
+ {label.slice(0, spaceIndex)}
31
+ </span>
32
+ <span className={pillStyles.miniPillText}>
33
+ {miniPillTextFromLabel(label)}
34
+ </span>
35
+ </>
36
+ );
37
+ }