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