proto-plugin 0.1.2 → 0.1.3
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/package.json +2 -1
- package/src/components/prototypes/prototype-change-log-panel.tsx +55 -55
- package/src/components/prototypes/prototype-comment-provider.tsx +0 -1
- package/src/components/prototypes/prototype-review-chrome.tsx +21 -2
- package/src/components/prototypes/prototype-review-sidebar.tsx +0 -13
- package/src/components/shell/prototype-gallery-nav.tsx +3 -3
- package/src/components/shell/prototype-gallery-shell.tsx +4 -4
- package/src/components/shell/prototype-plugin-update-popup.tsx +74 -0
- package/src/components/ui/button.tsx +1 -1
- package/src/components/ui/dialog.tsx +3 -3
- package/src/components/ui/dropdown-menu.tsx +2 -2
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/components/ui/option-select.tsx +3 -3
- package/src/components/ui/scroll-area.tsx +1 -1
- package/src/components/ui/separator.tsx +1 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/ui/switch.tsx +1 -1
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/ui/toggle-group.tsx +1 -1
- package/src/components/ui/tooltip.tsx +2 -2
- package/src/config/prototype-site-layout-client.tsx +3 -0
- package/src/config/with-prototype.ts +33 -0
- package/src/index.ts +18 -2
- package/src/lib/prototypes/changelog-description-prompt.ts +58 -0
- package/src/lib/prototypes/proto-plugin-version.ts +19 -0
- package/src/lib/prototypes/upgrade-proto-plugin-prompt.ts +40 -0
- package/src/lib/prototypes/use-proto-plugin-version-check.ts +67 -0
- package/src/server/create-host-api-route.ts +7 -0
- package/src/server/proto-plugin-version-route.ts +145 -0
- package/src/server.ts +2 -0
- package/src/styles/globals.css +17 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proto-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"./app": "./src/app.ts",
|
|
28
28
|
"./with-prototype": "./src/config/with-prototype.ts",
|
|
29
29
|
"./server": "./src/server.ts",
|
|
30
|
+
"./ui/*": "./src/components/ui/*.tsx",
|
|
30
31
|
"./styles/globals.css": "./src/styles/globals.css"
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
@@ -4,20 +4,20 @@ import { filterAnnotationsByChannel } from "@prototype/lib/prototype-comments/co
|
|
|
4
4
|
import { useCommentStore } from "@prototype/lib/prototype-comments/react/CommentProvider";
|
|
5
5
|
import { useCommentCaptureBridgeOptional } from "@prototype/lib/prototype-comments/react/CommentCaptureToolbar";
|
|
6
6
|
import { CommentsGrid } from "@prototype/lib/prototype-comments/ui/CommentsGrid";
|
|
7
|
-
import {
|
|
7
|
+
import { buildChangelogDescriptionCopyText } from "@prototype/lib/prototypes/changelog-description-prompt";
|
|
8
8
|
import { usePrototypeReview } from "@prototype/lib/prototypes/prototype-review-context";
|
|
9
|
+
import {
|
|
10
|
+
prototypeReviewPreferenceKey,
|
|
11
|
+
usePersistedLocalString,
|
|
12
|
+
} from "@prototype/lib/prototypes/use-persisted-local-state";
|
|
9
13
|
import { ToolbarIconButton } from "@prototype/components/platform-ui/toolbar-icon-button";
|
|
14
|
+
import { Button } from "@prototype/components/ui/button";
|
|
10
15
|
import { Label } from "@prototype/components/ui/label";
|
|
11
16
|
import { Switch } from "@prototype/components/ui/switch";
|
|
12
|
-
import
|
|
17
|
+
import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
|
|
18
|
+
import { Plus } from "lucide-react";
|
|
13
19
|
import { cn } from "@prototype/lib/utils";
|
|
14
|
-
import {
|
|
15
|
-
useCallback,
|
|
16
|
-
useEffect,
|
|
17
|
-
useMemo,
|
|
18
|
-
useState,
|
|
19
|
-
type ChangeEvent,
|
|
20
|
-
} from "react";
|
|
20
|
+
import { useMemo } from "react";
|
|
21
21
|
|
|
22
22
|
import { PrototypeReferenceDocs } from "./prototype-reference-docs";
|
|
23
23
|
import { PrototypeCommentStorageEmptyState } from "./prototype-comment-storage-empty-state";
|
|
@@ -108,63 +108,63 @@ export function PrototypeChangeLogPanel({
|
|
|
108
108
|
storageReady,
|
|
109
109
|
storageConfigured,
|
|
110
110
|
} = useCommentStore();
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
const review = usePrototypeReview();
|
|
112
|
+
const { value: description, updateValue: updateDescription } =
|
|
113
|
+
usePersistedLocalString(
|
|
114
|
+
prototypeReviewPreferenceKey(review.slug, "changelog-description-brief"),
|
|
115
|
+
"",
|
|
116
|
+
);
|
|
117
|
+
const {
|
|
118
|
+
copy: copyDescriptionPrompt,
|
|
119
|
+
icon: copyDescriptionIcon,
|
|
120
|
+
isCopied: isDescriptionCopied,
|
|
121
|
+
} = useCopyToClipboard();
|
|
114
122
|
const changelogAnnotations = useMemo(
|
|
115
123
|
() => filterAnnotationsByChannel(annotations, "changelog"),
|
|
116
124
|
[annotations],
|
|
117
125
|
);
|
|
126
|
+
const canCopyDescription = description.trim().length > 0;
|
|
118
127
|
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
setDraftOverview(overview);
|
|
122
|
-
}
|
|
123
|
-
}, [metaReady, overview]);
|
|
124
|
-
|
|
125
|
-
const isOverviewDirty = draftOverview !== overview;
|
|
128
|
+
const handleCopyDescriptionPrompt = () => {
|
|
129
|
+
if (!canCopyDescription) return;
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
setDraftOverview(event.target.value);
|
|
130
|
-
},
|
|
131
|
-
[],
|
|
132
|
-
);
|
|
131
|
+
const origin =
|
|
132
|
+
typeof window !== "undefined" ? window.location.origin : undefined;
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
copyDescriptionPrompt(
|
|
135
|
+
buildChangelogDescriptionCopyText({
|
|
136
|
+
slug: review.slug,
|
|
137
|
+
description,
|
|
138
|
+
origin,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
};
|
|
137
142
|
|
|
138
143
|
return (
|
|
139
144
|
<div className="flex flex-col">
|
|
140
145
|
<section className={CHANGELOG_SECTION_CLASS}>
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
placeholder="Summarize what changed and why…"
|
|
164
|
-
aria-label="Description"
|
|
165
|
-
className={CHANGELOG_OVERVIEW_FIELD_CLASS}
|
|
166
|
-
/>
|
|
167
|
-
)}
|
|
146
|
+
<span className={CHANGELOG_SECTION_TITLE_CLASS}>Description</span>
|
|
147
|
+
<textarea
|
|
148
|
+
id="prototype-changelog-overview"
|
|
149
|
+
value={description}
|
|
150
|
+
onChange={(event) => updateDescription(event.target.value)}
|
|
151
|
+
rows={3}
|
|
152
|
+
placeholder="Summarize what changed and why…"
|
|
153
|
+
aria-label="Description"
|
|
154
|
+
className={CHANGELOG_OVERVIEW_FIELD_CLASS}
|
|
155
|
+
/>
|
|
156
|
+
<Button
|
|
157
|
+
type="button"
|
|
158
|
+
variant="chrome"
|
|
159
|
+
size="sm"
|
|
160
|
+
className="h-8 gap-1.5 self-start"
|
|
161
|
+
disabled={!canCopyDescription}
|
|
162
|
+
onClick={handleCopyDescriptionPrompt}
|
|
163
|
+
aria-label="Copy description prompt"
|
|
164
|
+
>
|
|
165
|
+
{isDescriptionCopied ? "Copied prompt" : "Copy prompt"}
|
|
166
|
+
{copyDescriptionIcon}
|
|
167
|
+
</Button>
|
|
168
168
|
</section>
|
|
169
169
|
|
|
170
170
|
<PrototypeReferenceDocs
|
|
@@ -4,7 +4,6 @@ import type { ReactNode } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { PrototypeTargetIdProvider } from "@prototype/components/prototypes/prototype-target";
|
|
6
6
|
import { CommentProvider } from "@prototype/lib/prototype-comments/react/CommentProvider";
|
|
7
|
-
import { ChangelogMetaProvider } from "@prototype/lib/prototypes/changelog-meta-context";
|
|
8
7
|
import type { PrototypeComponentRegistry } from "@prototype/lib/prototypes/prototype-component-registry";
|
|
9
8
|
import { getPrototypeComponentRegistryForSlug } from "@prototype/lib/prototypes/create-prototype-registry";
|
|
10
9
|
import {
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
LayoutGrid,
|
|
49
49
|
MapPin,
|
|
50
50
|
MessageSquare,
|
|
51
|
+
MessageSquarePlus,
|
|
51
52
|
Monitor,
|
|
52
53
|
Moon,
|
|
53
54
|
Network,
|
|
@@ -250,6 +251,7 @@ function ReviewToolbarButton({
|
|
|
250
251
|
onClick,
|
|
251
252
|
hoverOnly = false,
|
|
252
253
|
hoverTriggerProps,
|
|
254
|
+
commentModeToggle = false,
|
|
253
255
|
children,
|
|
254
256
|
}: {
|
|
255
257
|
label: string;
|
|
@@ -260,6 +262,7 @@ function ReviewToolbarButton({
|
|
|
260
262
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
261
263
|
hoverOnly?: boolean;
|
|
262
264
|
hoverTriggerProps?: ToolbarTooltipAnchorProps;
|
|
265
|
+
commentModeToggle?: boolean;
|
|
263
266
|
children: ReactNode;
|
|
264
267
|
}) {
|
|
265
268
|
return (
|
|
@@ -272,6 +275,9 @@ function ReviewToolbarButton({
|
|
|
272
275
|
pressed && !highlightActive && styles.toolbarButtonHovered,
|
|
273
276
|
)}
|
|
274
277
|
data-active={highlightActive && pressed ? "true" : undefined}
|
|
278
|
+
data-prototype-comment-mode-toggle={
|
|
279
|
+
commentModeToggle ? true : undefined
|
|
280
|
+
}
|
|
275
281
|
aria-label={label}
|
|
276
282
|
aria-pressed={pressed}
|
|
277
283
|
onClick={(event) => {
|
|
@@ -1121,6 +1127,21 @@ export function PrototypeReviewChrome({
|
|
|
1121
1127
|
</DropdownMenuContent>
|
|
1122
1128
|
</DropdownMenu>
|
|
1123
1129
|
|
|
1130
|
+
{onToggleCommentMode ? (
|
|
1131
|
+
<ReviewToolbarButton
|
|
1132
|
+
label={
|
|
1133
|
+
isCommentModeActive ? "Exit comment mode" : "Add comment"
|
|
1134
|
+
}
|
|
1135
|
+
tooltip="Add comment"
|
|
1136
|
+
pressed={isCommentModeActive ?? false}
|
|
1137
|
+
highlightActive={isCommentModeActive ?? false}
|
|
1138
|
+
commentModeToggle
|
|
1139
|
+
onClick={() => onToggleCommentMode()}
|
|
1140
|
+
>
|
|
1141
|
+
<MessageSquarePlus size={16} strokeWidth={2} />
|
|
1142
|
+
</ReviewToolbarButton>
|
|
1143
|
+
) : null}
|
|
1144
|
+
|
|
1124
1145
|
<ReviewToolbarButton
|
|
1125
1146
|
label={
|
|
1126
1147
|
review.open && review.sidebarPanel === "comments"
|
|
@@ -1204,8 +1225,6 @@ export function PrototypeReviewChrome({
|
|
|
1204
1225
|
onSelect={onSelect}
|
|
1205
1226
|
selectedId={selectedId}
|
|
1206
1227
|
onClose={onClose}
|
|
1207
|
-
isCommentModeActive={isCommentModeActive}
|
|
1208
|
-
onToggleCommentMode={onToggleCommentMode}
|
|
1209
1228
|
/>,
|
|
1210
1229
|
sidebarPortalTarget,
|
|
1211
1230
|
)
|
|
@@ -7,7 +7,6 @@ import { useCommentStore } from "@prototype/lib/prototype-comments/react/Comment
|
|
|
7
7
|
import { CommentHeaderCount } from "@prototype/lib/prototype-comments/ui/CommentHeaderCount";
|
|
8
8
|
import { CommentsGrid } from "@prototype/lib/prototype-comments/ui/CommentsGrid";
|
|
9
9
|
import { usePrototypeReview } from "@prototype/lib/prototypes/prototype-review-context";
|
|
10
|
-
import { CommentModeToggleButton } from "@prototype/lib/prototype-comments/ui/CommentModeToggleButton";
|
|
11
10
|
import { Sidebar } from "@prototype/components/platform-ui/sidebar";
|
|
12
11
|
import { EmptyState } from "@prototype/components/platform-ui/empty-state";
|
|
13
12
|
import { PrototypeChangeLogPanel } from "@prototype/components/prototypes/prototype-change-log-panel";
|
|
@@ -20,16 +19,12 @@ type PrototypeReviewSidebarProps = {
|
|
|
20
19
|
onSelect: (id: string) => void;
|
|
21
20
|
selectedId?: string | null;
|
|
22
21
|
onClose: () => void;
|
|
23
|
-
isCommentModeActive?: boolean;
|
|
24
|
-
onToggleCommentMode?: () => void;
|
|
25
22
|
};
|
|
26
23
|
|
|
27
24
|
export function PrototypeReviewSidebar({
|
|
28
25
|
onSelect,
|
|
29
26
|
selectedId,
|
|
30
27
|
onClose,
|
|
31
|
-
isCommentModeActive = false,
|
|
32
|
-
onToggleCommentMode,
|
|
33
28
|
}: PrototypeReviewSidebarProps) {
|
|
34
29
|
const review = usePrototypeReview();
|
|
35
30
|
const { annotations, deleteAnnotation, updateAnnotation, resolveAnnotation, storageError, storageReady, storageConfigured } = useCommentStore();
|
|
@@ -91,14 +86,6 @@ export function PrototypeReviewSidebar({
|
|
|
91
86
|
/>
|
|
92
87
|
) : undefined
|
|
93
88
|
}
|
|
94
|
-
headerActions={
|
|
95
|
-
isCommentsPanel && onToggleCommentMode ? (
|
|
96
|
-
<CommentModeToggleButton
|
|
97
|
-
isActive={isCommentModeActive}
|
|
98
|
-
onClick={onToggleCommentMode}
|
|
99
|
-
/>
|
|
100
|
-
) : undefined
|
|
101
|
-
}
|
|
102
89
|
onClose={
|
|
103
90
|
isVariantsPanel
|
|
104
91
|
? review.closeVariants
|
|
@@ -5,7 +5,7 @@ import { Button } from "@prototype/components/ui/button";
|
|
|
5
5
|
import { PrototypeLinkSourceModal } from "@prototype/components/shell/prototype-link-source-modal";
|
|
6
6
|
import { cn } from "@prototype/lib/utils";
|
|
7
7
|
import { usePrototypeToolTheme } from "@prototype/lib/prototypes/use-prototype-tool-theme";
|
|
8
|
-
import { Moon, Pencil, Shapes, Sun
|
|
8
|
+
import { LibraryBig, Moon, Pencil, Shapes, Sun } from "lucide-react";
|
|
9
9
|
import Link from "next/link";
|
|
10
10
|
import { usePathname } from "next/navigation";
|
|
11
11
|
import { useState } from "react";
|
|
@@ -19,7 +19,7 @@ const GALLERY_NAV_ITEMS = [
|
|
|
19
19
|
{
|
|
20
20
|
href: "/component-library",
|
|
21
21
|
label: "Component Library",
|
|
22
|
-
icon:
|
|
22
|
+
icon: LibraryBig,
|
|
23
23
|
},
|
|
24
24
|
] as const;
|
|
25
25
|
|
|
@@ -37,7 +37,7 @@ export function PrototypeGalleryNav({
|
|
|
37
37
|
const [linkSourceOpen, setLinkSourceOpen] = useState(false);
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
|
-
<aside className="border-[var(--tool-chrome-border)] bg-[var(--bg-
|
|
40
|
+
<aside className="border-[var(--tool-chrome-border)] bg-[var(--bg-main)] flex h-full min-h-0 w-56 shrink-0 flex-col self-stretch overflow-hidden border-r">
|
|
41
41
|
<nav aria-label="Gallery" className="flex flex-col gap-1 p-3">
|
|
42
42
|
{GALLERY_NAV_ITEMS.map(({ href, label, icon: Icon }) => {
|
|
43
43
|
const isActive =
|
|
@@ -21,7 +21,7 @@ export function PrototypeGalleryShell({
|
|
|
21
21
|
return (
|
|
22
22
|
<PrototypeProvider className="bg-[var(--bg-main)] flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden">
|
|
23
23
|
{footer ? (
|
|
24
|
-
<div className="border-[var(--tool-chrome-border)] bg-[var(--bg-
|
|
24
|
+
<div className="border-[var(--tool-chrome-border)] bg-[var(--bg-main)] flex shrink-0 items-center justify-end border-b px-10 py-3 lg:px-16">
|
|
25
25
|
{footer}
|
|
26
26
|
</div>
|
|
27
27
|
) : null}
|
|
@@ -31,7 +31,7 @@ export function PrototypeGalleryShell({
|
|
|
31
31
|
sourceDirectoryName={sourceDirectoryName}
|
|
32
32
|
sourcePath={sourcePath}
|
|
33
33
|
/>
|
|
34
|
-
<main className="bg-[var(--bg-
|
|
34
|
+
<main className="bg-[var(--bg-ground)] flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
35
35
|
{children}
|
|
36
36
|
</main>
|
|
37
37
|
</div>
|
|
@@ -52,7 +52,7 @@ export function PrototypeGalleryPageLayout({
|
|
|
52
52
|
return (
|
|
53
53
|
<div className="flex h-full min-h-0 flex-1 flex-col overflow-hidden">
|
|
54
54
|
{header}
|
|
55
|
-
<div className="bg-[var(--bg-
|
|
55
|
+
<div className="bg-[var(--bg-ground)] min-h-0 flex-1 overflow-y-auto overscroll-y-contain">
|
|
56
56
|
<div className="h-fit w-full">{children}</div>
|
|
57
57
|
</div>
|
|
58
58
|
</div>
|
|
@@ -75,7 +75,7 @@ export function PrototypeGalleryHeader({
|
|
|
75
75
|
return (
|
|
76
76
|
<div
|
|
77
77
|
className={cn(
|
|
78
|
-
"border-[var(--tool-chrome-border)] bg-[var(--bg-
|
|
78
|
+
"border-[var(--tool-chrome-border)] bg-[var(--bg-main)] shrink-0 border-b px-10 py-6 lg:px-16",
|
|
79
79
|
className,
|
|
80
80
|
)}
|
|
81
81
|
>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@prototype/components/ui/button";
|
|
4
|
+
import { buildUpgradeProtoPluginCopyText } from "@prototype/lib/prototypes/upgrade-proto-plugin-prompt";
|
|
5
|
+
import { useProtoPluginVersionCheck } from "@prototype/lib/prototypes/use-proto-plugin-version-check";
|
|
6
|
+
import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
|
|
7
|
+
import { cn } from "@prototype/lib/utils";
|
|
8
|
+
import { X } from "lucide-react";
|
|
9
|
+
import { toast } from "sonner";
|
|
10
|
+
|
|
11
|
+
export function PrototypePluginUpdatePopup() {
|
|
12
|
+
const { status, shouldShowPopup, dismiss } = useProtoPluginVersionCheck();
|
|
13
|
+
const { copy, icon, isCopied } = useCopyToClipboard();
|
|
14
|
+
|
|
15
|
+
if (!shouldShowPopup || !status?.installed || !status.latest) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const handleCopy = async () => {
|
|
20
|
+
try {
|
|
21
|
+
await copy(
|
|
22
|
+
buildUpgradeProtoPluginCopyText({
|
|
23
|
+
installed: status.installed!,
|
|
24
|
+
latest: status.latest!,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
} catch {
|
|
28
|
+
toast.error("Copy error", {
|
|
29
|
+
description: "Failed to copy upgrade prompt",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
role="status"
|
|
37
|
+
aria-live="polite"
|
|
38
|
+
className={cn(
|
|
39
|
+
"border-border bg-card ring-border pointer-events-auto fixed bottom-4 left-4 z-[1050] flex w-[min(20rem,calc(100vw-2rem))] flex-col gap-3 rounded-xl border p-4 shadow-lg ring-[0.5px]",
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
<div className="flex items-start justify-between gap-2">
|
|
43
|
+
<div className="min-w-0 flex-1">
|
|
44
|
+
<p className="text-sm font-medium text-foreground">Update proto-plugin</p>
|
|
45
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
46
|
+
v{status.latest} is available. You're on v{status.installed}.
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
aria-label="Dismiss update notification"
|
|
52
|
+
className="text-muted-foreground hover:text-foreground shrink-0 rounded p-1 transition-colors duration-200 ease"
|
|
53
|
+
onClick={dismiss}
|
|
54
|
+
>
|
|
55
|
+
<X size={14} />
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<Button
|
|
60
|
+
type="button"
|
|
61
|
+
variant="outline"
|
|
62
|
+
size="sm"
|
|
63
|
+
className="h-8 w-fit cursor-pointer gap-1.5"
|
|
64
|
+
onClick={() => {
|
|
65
|
+
void handleCopy();
|
|
66
|
+
}}
|
|
67
|
+
aria-label="Copy upgrade prompt"
|
|
68
|
+
>
|
|
69
|
+
{isCopied ? "Copied prompt" : "Copy prompt"}
|
|
70
|
+
{icon}
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -4,7 +4,7 @@ import { Slot } from "@radix-ui/react-slot";
|
|
|
4
4
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
|
|
7
|
-
import { cn } from "
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
8
|
|
|
9
9
|
const buttonVariants = cva(
|
|
10
10
|
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
@@ -4,12 +4,12 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
|
|
7
|
-
import { isCommentCaptureBlockedTarget } from "
|
|
7
|
+
import { isCommentCaptureBlockedTarget } from "../../lib/prototype-comments/core/comment-capture-blocked";
|
|
8
8
|
import {
|
|
9
9
|
getPrototypeDialogPortalContainer,
|
|
10
10
|
getPrototypeToolDialogPortalContainer,
|
|
11
|
-
} from "
|
|
12
|
-
import { cn } from "
|
|
11
|
+
} from "../../lib/tool-portal";
|
|
12
|
+
import { cn } from "../../lib/utils";
|
|
13
13
|
|
|
14
14
|
import styles from "./dialog.module.scss";
|
|
15
15
|
|
|
@@ -4,8 +4,8 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
|
4
4
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
|
|
7
|
-
import { getPrototypePortalContainer } from "
|
|
8
|
-
import { cn } from "
|
|
7
|
+
import { getPrototypePortalContainer } from "../../lib/tool-portal";
|
|
8
|
+
import { cn } from "../../lib/utils";
|
|
9
9
|
|
|
10
10
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
11
11
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
@@ -4,7 +4,7 @@ import * as LabelPrimitive from "@radix-ui/react-label";
|
|
|
4
4
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
|
|
7
|
-
import { cn } from "
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
8
|
|
|
9
9
|
const labelVariants = cva(
|
|
10
10
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
import { ChevronDown } from "lucide-react";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
|
|
6
|
-
import { Button } from "
|
|
6
|
+
import { Button } from "./button";
|
|
7
7
|
import {
|
|
8
8
|
DropdownMenu,
|
|
9
9
|
DropdownMenuContent,
|
|
10
10
|
DropdownMenuItem,
|
|
11
11
|
DropdownMenuTrigger,
|
|
12
|
-
} from "
|
|
13
|
-
import { cn } from "
|
|
12
|
+
} from "./dropdown-menu";
|
|
13
|
+
import { cn } from "../../lib/utils";
|
|
14
14
|
|
|
15
15
|
export type OptionSelectItem<T extends string = string> = {
|
|
16
16
|
value: T;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
|
|
6
|
-
import { cn } from "
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
7
|
|
|
8
8
|
const ScrollArea = React.forwardRef<
|
|
9
9
|
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
|
|
6
|
-
import { cn } from "
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
7
|
|
|
8
8
|
const Separator = React.forwardRef<
|
|
9
9
|
React.ComponentRef<typeof SeparatorPrimitive.Root>,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
|
|
6
|
-
import { cn } from "
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
7
|
|
|
8
8
|
const Slider = React.forwardRef<
|
|
9
9
|
React.ComponentRef<typeof SliderPrimitive.Root>,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
|
|
6
|
-
import { cn } from "
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
7
|
|
|
8
8
|
const Switch = React.forwardRef<
|
|
9
9
|
React.ComponentRef<typeof SwitchPrimitives.Root>,
|
|
@@ -4,7 +4,7 @@ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
|
|
4
4
|
import { type VariantProps, cva } from "class-variance-authority";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
|
|
7
|
-
import { cn } from "
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
8
|
|
|
9
9
|
const toggleVariants = cva(
|
|
10
10
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
|
|
6
|
-
import { getPrototypePortalContainer } from "
|
|
7
|
-
import { cn } from "
|
|
6
|
+
import { getPrototypePortalContainer } from "../../lib/tool-portal";
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
8
|
|
|
9
9
|
const TooltipProvider = TooltipPrimitive.Provider;
|
|
10
10
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { PrototypePluginUpdatePopup } from "@prototype/components/shell/prototype-plugin-update-popup";
|
|
3
4
|
import { usePathname } from "next/navigation";
|
|
4
5
|
import type { ComponentType, ReactNode } from "react";
|
|
5
6
|
|
|
@@ -35,6 +36,7 @@ export function PrototypeSiteLayoutClient({
|
|
|
35
36
|
>
|
|
36
37
|
{children}
|
|
37
38
|
</GalleryShell>
|
|
39
|
+
<PrototypePluginUpdatePopup />
|
|
38
40
|
{toaster}
|
|
39
41
|
</div>
|
|
40
42
|
);
|
|
@@ -43,6 +45,7 @@ export function PrototypeSiteLayoutClient({
|
|
|
43
45
|
return (
|
|
44
46
|
<div className="bg-[var(--bg-ground)] flex h-svh overflow-hidden">
|
|
45
47
|
{children}
|
|
48
|
+
<PrototypePluginUpdatePopup />
|
|
46
49
|
{toaster}
|
|
47
50
|
</div>
|
|
48
51
|
);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
1
4
|
import type { NextConfig } from "next";
|
|
2
5
|
|
|
3
6
|
export type WithPrototypeOptions = {
|
|
@@ -5,10 +8,25 @@ export type WithPrototypeOptions = {
|
|
|
5
8
|
nextConfig?: NextConfig;
|
|
6
9
|
};
|
|
7
10
|
|
|
11
|
+
/** Package root — resolved lazily so this module is safe if ever pulled into a bundle. */
|
|
12
|
+
function getPackageRoot(): string {
|
|
13
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getPrototypeInternalAlias() {
|
|
17
|
+
return {
|
|
18
|
+
"@prototype": path.join(getPackageRoot(), "src"),
|
|
19
|
+
} as const;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export function withPrototype(
|
|
9
23
|
options: WithPrototypeOptions = {},
|
|
10
24
|
): NextConfig {
|
|
11
25
|
const { nextConfig = {} } = options;
|
|
26
|
+
const prototypeInternalAlias = getPrototypeInternalAlias();
|
|
27
|
+
|
|
28
|
+
const existingTurbopack = nextConfig.turbopack ?? {};
|
|
29
|
+
const existingResolveAlias = existingTurbopack.resolveAlias ?? {};
|
|
12
30
|
|
|
13
31
|
return {
|
|
14
32
|
...nextConfig,
|
|
@@ -28,6 +46,21 @@ export function withPrototype(
|
|
|
28
46
|
...(nextConfig.transpilePackages ?? []),
|
|
29
47
|
"proto-plugin",
|
|
30
48
|
],
|
|
49
|
+
turbopack: {
|
|
50
|
+
...existingTurbopack,
|
|
51
|
+
resolveAlias: {
|
|
52
|
+
...existingResolveAlias,
|
|
53
|
+
...prototypeInternalAlias,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
webpack: (config, ctx) => {
|
|
57
|
+
config.resolve ??= {};
|
|
58
|
+
config.resolve.alias = {
|
|
59
|
+
...config.resolve.alias,
|
|
60
|
+
...prototypeInternalAlias,
|
|
61
|
+
};
|
|
62
|
+
return nextConfig.webpack?.(config, ctx) ?? config;
|
|
63
|
+
},
|
|
31
64
|
images: {
|
|
32
65
|
...nextConfig.images,
|
|
33
66
|
localPatterns: [
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export { definePrototypeConfig } from "./config/define-prototype-config";
|
|
2
|
-
|
|
2
|
+
// NOTE: `createPrototypeGalleryPage` is server-only (reads the screenshot manifest
|
|
3
|
+
// from disk) and lives in `proto-plugin/server`. Keeping it out of this barrel is
|
|
4
|
+
// what lets client prototype components import from `proto-plugin` safely.
|
|
3
5
|
export { createPrototypeComponentLibraryPage } from "./config/create-prototype-component-library-page";
|
|
4
6
|
export { createPrototypePage } from "./config/create-prototype-page";
|
|
5
7
|
export { createPrototypeStateMapPage } from "./config/create-prototype-state-map-page";
|
|
6
8
|
export { createPrototypeSiteLayout } from "./config/create-prototype-site-layout";
|
|
7
|
-
|
|
9
|
+
// NOTE: `withPrototype` uses Node path APIs and belongs in `proto-plugin/with-prototype`
|
|
10
|
+
// (next.config.ts only). Do not re-export it here — the barrel is imported by client
|
|
11
|
+
// prototype components and must stay browser-safe.
|
|
8
12
|
|
|
9
13
|
export {
|
|
10
14
|
createPrototypeRegistry,
|
|
@@ -55,6 +59,16 @@ export {
|
|
|
55
59
|
|
|
56
60
|
export { usePrototypeComments } from "./lib/prototypes/use-prototype-comments";
|
|
57
61
|
|
|
62
|
+
export {
|
|
63
|
+
SHARE_TARGET_PARAM,
|
|
64
|
+
SHARE_STATE_PARAM,
|
|
65
|
+
SHARE_COMMENT_PARAM,
|
|
66
|
+
encodeShareState,
|
|
67
|
+
decodeShareState,
|
|
68
|
+
readShareLinkParams,
|
|
69
|
+
} from "./lib/prototypes/prototype-share-link";
|
|
70
|
+
export type { ShareLinkParams } from "./lib/prototypes/prototype-share-link";
|
|
71
|
+
|
|
58
72
|
export {
|
|
59
73
|
usePersistPrototypeLiveState,
|
|
60
74
|
readPersistedPrototypeLiveState,
|
|
@@ -63,6 +77,8 @@ export {
|
|
|
63
77
|
export type {
|
|
64
78
|
DesignExplorationBaselineOption,
|
|
65
79
|
DesignExplorationConfig,
|
|
80
|
+
DesignExplorationRationale,
|
|
81
|
+
MobbinReference,
|
|
66
82
|
} from "./lib/prototypes/design-exploration-types";
|
|
67
83
|
|
|
68
84
|
export {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function buildChangelogDescriptionPrompt({
|
|
2
|
+
slug,
|
|
3
|
+
description,
|
|
4
|
+
origin = "http://localhost:3003",
|
|
5
|
+
}: {
|
|
6
|
+
slug: string;
|
|
7
|
+
description: string;
|
|
8
|
+
origin?: string;
|
|
9
|
+
}): string {
|
|
10
|
+
const prototypeUrl = `${origin}/prototypes/${slug}`;
|
|
11
|
+
const trimmed = description.trim();
|
|
12
|
+
|
|
13
|
+
const lines = [
|
|
14
|
+
`Update the "${slug}" prototype to match the following change log overview.`,
|
|
15
|
+
"",
|
|
16
|
+
"## Description",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
if (trimmed) {
|
|
20
|
+
lines.push(trimmed);
|
|
21
|
+
} else {
|
|
22
|
+
lines.push(
|
|
23
|
+
"Summarize what changed in this prototype and why:",
|
|
24
|
+
"- What changed:",
|
|
25
|
+
"- Why:",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
lines.push(
|
|
30
|
+
"",
|
|
31
|
+
"## Prototype",
|
|
32
|
+
`- Slug: \`${slug}\``,
|
|
33
|
+
`- Preview: ${prototypeUrl}`,
|
|
34
|
+
`- Work in: \`src/prototypes/${slug}/\``,
|
|
35
|
+
"",
|
|
36
|
+
"## Instructions",
|
|
37
|
+
"1. Review the overview and anchored changelog entries in the prototype review sidebar.",
|
|
38
|
+
"2. Apply any missing updates so the prototype matches the described changes.",
|
|
39
|
+
"3. Do not persist this overview in KV — it is prompt-only.",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildChangelogDescriptionCopyText({
|
|
46
|
+
slug,
|
|
47
|
+
description,
|
|
48
|
+
origin,
|
|
49
|
+
}: {
|
|
50
|
+
slug: string;
|
|
51
|
+
description: string;
|
|
52
|
+
origin?: string;
|
|
53
|
+
}): string {
|
|
54
|
+
const prompt = buildChangelogDescriptionPrompt({ slug, description, origin });
|
|
55
|
+
const trimmed = description.trim();
|
|
56
|
+
if (!trimmed) return prompt;
|
|
57
|
+
return `${trimmed}\n\n---\n\n${prompt}`;
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ProtoPluginVersionStatus = {
|
|
2
|
+
installed: string | null;
|
|
3
|
+
latest: string | null;
|
|
4
|
+
updateAvailable: boolean;
|
|
5
|
+
isWorkspaceLink: boolean;
|
|
6
|
+
repositoryUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const PROTOTYPE_PLUGIN_VERSION_PATH = "/api/proto-plugin/version";
|
|
10
|
+
|
|
11
|
+
export async function fetchProtoPluginVersionStatus(): Promise<ProtoPluginVersionStatus> {
|
|
12
|
+
const response = await fetch(PROTOTYPE_PLUGIN_VERSION_PATH);
|
|
13
|
+
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
throw new Error("Failed to check proto-plugin version.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (await response.json()) as ProtoPluginVersionStatus;
|
|
19
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type UpgradeProtoPluginPromptOptions = {
|
|
2
|
+
installed: string;
|
|
3
|
+
latest: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function buildUpgradeProtoPluginPrompt({
|
|
7
|
+
installed,
|
|
8
|
+
latest,
|
|
9
|
+
}: UpgradeProtoPluginPromptOptions): string {
|
|
10
|
+
return [
|
|
11
|
+
`Upgrade \`proto-plugin\` in this host app from \`${installed}\` to \`${latest}\`. **Do every step yourself** — the user should not run commands manually.`,
|
|
12
|
+
"",
|
|
13
|
+
"## Your job",
|
|
14
|
+
`Update the dependency, restart the dev server if needed, and confirm the gallery and at least one prototype still work on \`${latest}\`.`,
|
|
15
|
+
"",
|
|
16
|
+
"## Versions",
|
|
17
|
+
`- Installed: \`${installed}\``,
|
|
18
|
+
`- Latest: \`${latest}\``,
|
|
19
|
+
"",
|
|
20
|
+
"## Steps",
|
|
21
|
+
"1. Detect the package manager from the lockfile (`pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`).",
|
|
22
|
+
"2. Update the dependency:",
|
|
23
|
+
` - pnpm: \`pnpm update proto-plugin\` or \`pnpm add proto-plugin@${latest}\``,
|
|
24
|
+
` - npm: \`npm install proto-plugin@${latest}\``,
|
|
25
|
+
` - yarn: \`yarn add proto-plugin@${latest}\``,
|
|
26
|
+
"3. Restart the dev server if it is already running.",
|
|
27
|
+
`4. Skim \`node_modules/proto-plugin/AGENTS.md\` (or the package changelog on npm) for breaking changes since \`${installed}\`.`,
|
|
28
|
+
"5. Open `/` and one prototype preview — confirm the gallery and review chrome load without errors.",
|
|
29
|
+
"",
|
|
30
|
+
"## Notes",
|
|
31
|
+
"- Do not modify prototype source under `src/prototypes/` unless AGENTS.md or the changelog requires migration steps for this upgrade.",
|
|
32
|
+
"- If the update fails (peer dependency conflicts, build errors), diagnose and fix before stopping.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildUpgradeProtoPluginCopyText(
|
|
37
|
+
options: UpgradeProtoPluginPromptOptions,
|
|
38
|
+
): string {
|
|
39
|
+
return buildUpgradeProtoPluginPrompt(options);
|
|
40
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
fetchProtoPluginVersionStatus,
|
|
5
|
+
type ProtoPluginVersionStatus,
|
|
6
|
+
} from "@prototype/lib/prototypes/proto-plugin-version";
|
|
7
|
+
import { usePersistedLocalString } from "@prototype/lib/prototypes/use-persisted-local-state";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
9
|
+
|
|
10
|
+
const DISMISSED_VERSION_KEY = "prototype-review:global:dismissed-plugin-version";
|
|
11
|
+
|
|
12
|
+
export function useProtoPluginVersionCheck() {
|
|
13
|
+
const [status, setStatus] = useState<ProtoPluginVersionStatus | null>(null);
|
|
14
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
15
|
+
const { value: dismissedVersion, updateValue: setDismissedVersion } =
|
|
16
|
+
usePersistedLocalString(DISMISSED_VERSION_KEY, "");
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let cancelled = false;
|
|
20
|
+
|
|
21
|
+
void (async () => {
|
|
22
|
+
try {
|
|
23
|
+
const nextStatus = await fetchProtoPluginVersionStatus();
|
|
24
|
+
if (!cancelled) {
|
|
25
|
+
setStatus(nextStatus);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
if (!cancelled) {
|
|
29
|
+
setStatus(null);
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
if (!cancelled) {
|
|
33
|
+
setIsLoading(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const dismiss = useCallback(() => {
|
|
44
|
+
if (status?.latest) {
|
|
45
|
+
setDismissedVersion(status.latest);
|
|
46
|
+
}
|
|
47
|
+
}, [setDismissedVersion, status?.latest]);
|
|
48
|
+
|
|
49
|
+
const shouldShowPopup = useMemo(() => {
|
|
50
|
+
if (isLoading || !status?.updateAvailable || !status.latest || !status.installed) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!dismissedVersion) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return dismissedVersion !== status.latest;
|
|
59
|
+
}, [dismissedVersion, isLoading, status]);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
status,
|
|
63
|
+
shouldShowPopup,
|
|
64
|
+
dismiss,
|
|
65
|
+
isLoading,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./create-prototype-api-routes";
|
|
11
11
|
import { createGalleryApi } from "./gallery/create-gallery-api";
|
|
12
12
|
import { GET as getPrototypeStorageStatus } from "./prototype-storage-status-route";
|
|
13
|
+
import { GET as getProtoPluginVersion } from "./proto-plugin-version-route";
|
|
13
14
|
import { GET as getPrVercelPreview } from "./pr-vercel-preview-route";
|
|
14
15
|
|
|
15
16
|
type ApiRouteContext = {
|
|
@@ -42,6 +43,12 @@ export function createPrototypeApiRoute(_config: PrototypeConfig) {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
if (path[0] === "proto-plugin" && path.length === 2 && path[1] === "version") {
|
|
47
|
+
if (method === "GET") {
|
|
48
|
+
return getProtoPluginVersion();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
if (path[0] === "prototypes" && path.length === 2 && path[1] === "storage-status") {
|
|
46
53
|
if (method === "GET") {
|
|
47
54
|
return getPrototypeStorageStatus();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readFileSync, lstatSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
import { NextResponse } from "next/server";
|
|
5
|
+
|
|
6
|
+
const PACKAGE_NAME = "proto-plugin";
|
|
7
|
+
const NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
8
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export type ProtoPluginVersionStatus = {
|
|
11
|
+
installed: string | null;
|
|
12
|
+
latest: string | null;
|
|
13
|
+
updateAvailable: boolean;
|
|
14
|
+
isWorkspaceLink: boolean;
|
|
15
|
+
repositoryUrl?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type NpmLatestResponse = {
|
|
19
|
+
version?: string;
|
|
20
|
+
repository?: { url?: string };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let cachedLatest: { version: string; repositoryUrl?: string; fetchedAt: number } | null =
|
|
24
|
+
null;
|
|
25
|
+
|
|
26
|
+
function parseSemver(version: string): [number, number, number] | null {
|
|
27
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isNewerVersion(latest: string, installed: string): boolean {
|
|
33
|
+
const latestParts = parseSemver(latest);
|
|
34
|
+
const installedParts = parseSemver(installed);
|
|
35
|
+
if (!latestParts || !installedParts) return false;
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < 3; index += 1) {
|
|
38
|
+
if (latestParts[index]! > installedParts[index]!) return true;
|
|
39
|
+
if (latestParts[index]! < installedParts[index]!) return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readInstalledVersion(cwd: string): {
|
|
46
|
+
installed: string | null;
|
|
47
|
+
isWorkspaceLink: boolean;
|
|
48
|
+
} {
|
|
49
|
+
const packagePath = join(cwd, "node_modules", PACKAGE_NAME, "package.json");
|
|
50
|
+
const linkPath = join(cwd, "node_modules", PACKAGE_NAME);
|
|
51
|
+
|
|
52
|
+
let isWorkspaceLink = false;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
isWorkspaceLink = lstatSync(linkPath).isSymbolicLink();
|
|
56
|
+
} catch {
|
|
57
|
+
try {
|
|
58
|
+
const hostPkg = JSON.parse(
|
|
59
|
+
readFileSync(join(cwd, "package.json"), "utf8"),
|
|
60
|
+
) as {
|
|
61
|
+
dependencies?: Record<string, string>;
|
|
62
|
+
devDependencies?: Record<string, string>;
|
|
63
|
+
};
|
|
64
|
+
const depSpec =
|
|
65
|
+
hostPkg.dependencies?.[PACKAGE_NAME] ??
|
|
66
|
+
hostPkg.devDependencies?.[PACKAGE_NAME];
|
|
67
|
+
isWorkspaceLink = depSpec?.startsWith("workspace:") ?? false;
|
|
68
|
+
} catch {
|
|
69
|
+
// Ignore — treat as non-workspace.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf8")) as { version?: string };
|
|
75
|
+
return { installed: pkg.version ?? null, isWorkspaceLink };
|
|
76
|
+
} catch {
|
|
77
|
+
return { installed: null, isWorkspaceLink };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function fetchLatestFromNpm(): Promise<{
|
|
82
|
+
version: string | null;
|
|
83
|
+
repositoryUrl?: string;
|
|
84
|
+
}> {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
|
|
87
|
+
if (cachedLatest && now - cachedLatest.fetchedAt < CACHE_TTL_MS) {
|
|
88
|
+
return {
|
|
89
|
+
version: cachedLatest.version,
|
|
90
|
+
repositoryUrl: cachedLatest.repositoryUrl,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(NPM_LATEST_URL, {
|
|
96
|
+
next: { revalidate: 3600 },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
return { version: null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const data = (await response.json()) as NpmLatestResponse;
|
|
104
|
+
const version = data.version ?? null;
|
|
105
|
+
const repositoryUrl = data.repository?.url?.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
106
|
+
|
|
107
|
+
if (version) {
|
|
108
|
+
cachedLatest = { version, repositoryUrl, fetchedAt: now };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { version, repositoryUrl };
|
|
112
|
+
} catch {
|
|
113
|
+
return { version: null };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function GET() {
|
|
118
|
+
const cwd = process.cwd();
|
|
119
|
+
const { installed, isWorkspaceLink } = readInstalledVersion(cwd);
|
|
120
|
+
|
|
121
|
+
if (isWorkspaceLink) {
|
|
122
|
+
const status: ProtoPluginVersionStatus = {
|
|
123
|
+
installed,
|
|
124
|
+
latest: null,
|
|
125
|
+
updateAvailable: false,
|
|
126
|
+
isWorkspaceLink: true,
|
|
127
|
+
};
|
|
128
|
+
return NextResponse.json(status);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { version: latest, repositoryUrl } = await fetchLatestFromNpm();
|
|
132
|
+
|
|
133
|
+
const updateAvailable =
|
|
134
|
+
installed != null && latest != null && isNewerVersion(latest, installed);
|
|
135
|
+
|
|
136
|
+
const status: ProtoPluginVersionStatus = {
|
|
137
|
+
installed,
|
|
138
|
+
latest,
|
|
139
|
+
updateAvailable,
|
|
140
|
+
isWorkspaceLink: false,
|
|
141
|
+
repositoryUrl,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return NextResponse.json(status);
|
|
145
|
+
}
|
package/src/server.ts
CHANGED
package/src/styles/globals.css
CHANGED
|
@@ -40,17 +40,17 @@
|
|
|
40
40
|
Chrome (ground) is darkest for sidebar + header; content (bg) is a lighter
|
|
41
41
|
elevated panel for the gallery grid and review preview frame.
|
|
42
42
|
|
|
43
|
-
Neutral scale (measured against the #
|
|
43
|
+
Neutral scale (measured against the #151515 content surface):
|
|
44
44
|
heading #f0f0f0 ~13:1 crisp titles
|
|
45
45
|
text #d0d0d0 ~9:1 body copy
|
|
46
46
|
muted #a0a0a0 ~5:1 labels, descriptions (AA)
|
|
47
47
|
icon #9a9a9a glyphs, dimmer than body
|
|
48
48
|
border #333333 hairline dividers
|
|
49
49
|
strong #454545 inputs, structural edges */
|
|
50
|
-
--tool-chrome-ground: #
|
|
51
|
-
--tool-chrome-bg: #
|
|
52
|
-
--tool-chrome-surface: #
|
|
53
|
-
--tool-chrome-surface-muted: #
|
|
50
|
+
--tool-chrome-ground: #121212;
|
|
51
|
+
--tool-chrome-bg: #151515;
|
|
52
|
+
--tool-chrome-surface: #1c1c1c;
|
|
53
|
+
--tool-chrome-surface-muted: #181818;
|
|
54
54
|
--tool-chrome-text: #d0d0d0;
|
|
55
55
|
--tool-chrome-text-heading: #f0f0f0;
|
|
56
56
|
--tool-chrome-text-muted: #9c9c9c;
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
--bg-main: var(--tool-chrome-bg);
|
|
84
84
|
--bg-top: var(--tool-chrome-surface);
|
|
85
85
|
--bg-subtle: var(--tool-chrome-surface-muted);
|
|
86
|
-
--bg-layered: #
|
|
86
|
+
--bg-layered: #1c1c1c;
|
|
87
87
|
--bg-invert: var(--tool-chrome-text-heading);
|
|
88
88
|
--bg-white: var(--tool-chrome-bg);
|
|
89
89
|
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
--color-bg-main: var(--tool-chrome-bg);
|
|
166
166
|
--color-bg-top: var(--tool-chrome-surface);
|
|
167
167
|
--color-bg-subtle: var(--tool-chrome-surface-muted);
|
|
168
|
-
--color-bg-layered: #
|
|
168
|
+
--color-bg-layered: #1c1c1c;
|
|
169
169
|
--color-bg-invert: var(--tool-chrome-text-heading);
|
|
170
170
|
--color-bg-white: var(--tool-chrome-bg);
|
|
171
171
|
--color-text-primary: var(--tool-chrome-text);
|
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
--bg-main: var(--tool-chrome-bg);
|
|
210
210
|
--bg-top: var(--tool-chrome-bg);
|
|
211
211
|
--bg-subtle: var(--tool-chrome-surface-muted);
|
|
212
|
-
--bg-layered: #
|
|
212
|
+
--bg-layered: #1a1a1a;
|
|
213
213
|
--border-faint: var(--tool-chrome-border);
|
|
214
214
|
--border-solid: var(--tool-chrome-border);
|
|
215
215
|
--border-medium: var(--tool-chrome-border-strong);
|
|
@@ -241,7 +241,7 @@
|
|
|
241
241
|
--color-bg-main: var(--tool-chrome-bg);
|
|
242
242
|
--color-bg-top: var(--tool-chrome-bg);
|
|
243
243
|
--color-bg-subtle: var(--tool-chrome-surface-muted);
|
|
244
|
-
--color-bg-layered: #
|
|
244
|
+
--color-bg-layered: #1a1a1a;
|
|
245
245
|
--color-text-primary: var(--tool-chrome-text-heading);
|
|
246
246
|
--color-text-secondary: var(--tool-chrome-text);
|
|
247
247
|
--color-text-tertiary: var(--tool-chrome-text-muted);
|
|
@@ -490,7 +490,7 @@ html.prototype-theme-transition-disable :is(*, *::before, *::after) {
|
|
|
490
490
|
|
|
491
491
|
@media (hover: hover) and (pointer: fine) {
|
|
492
492
|
[data-prototype-root] .tool-toolbar-button:hover {
|
|
493
|
-
background:
|
|
493
|
+
background: var(--tool-chrome-surface);
|
|
494
494
|
color: var(--tool-chrome-icon-hover);
|
|
495
495
|
}
|
|
496
496
|
}
|
|
@@ -728,7 +728,7 @@ html.prototype-theme-transition-disable :is(*, *::before, *::after) {
|
|
|
728
728
|
|
|
729
729
|
/* State map chrome — dedicated canvas bed + node footer tokens per theme. */
|
|
730
730
|
[data-prototype-root] [data-prototype-state-canvas] {
|
|
731
|
-
--prototype-state-map-canvas-bg: var(--tool-chrome-
|
|
731
|
+
--prototype-state-map-canvas-bg: var(--tool-chrome-ground);
|
|
732
732
|
--prototype-state-map-node-surface: var(--tool-chrome-surface);
|
|
733
733
|
--prototype-state-map-node-preview-bg: var(--tool-chrome-surface-muted);
|
|
734
734
|
--prototype-state-map-node-footer-bg: var(--tool-chrome-surface-muted);
|
|
@@ -740,12 +740,12 @@ html.prototype-theme-transition-disable :is(*, *::before, *::after) {
|
|
|
740
740
|
}
|
|
741
741
|
|
|
742
742
|
[data-prototype-root][data-prototype-comment-theme="dark"] [data-prototype-state-canvas] {
|
|
743
|
-
--prototype-state-map-canvas-bg:
|
|
744
|
-
--prototype-state-map-node-surface:
|
|
745
|
-
--prototype-state-map-node-preview-bg:
|
|
746
|
-
--prototype-state-map-node-footer-bg:
|
|
747
|
-
--prototype-state-map-node-footer-border:
|
|
748
|
-
--prototype-state-map-node-footer-text:
|
|
743
|
+
--prototype-state-map-canvas-bg: var(--tool-chrome-ground);
|
|
744
|
+
--prototype-state-map-node-surface: var(--tool-chrome-surface);
|
|
745
|
+
--prototype-state-map-node-preview-bg: var(--tool-chrome-surface-muted);
|
|
746
|
+
--prototype-state-map-node-footer-bg: var(--tool-chrome-surface-muted);
|
|
747
|
+
--prototype-state-map-node-footer-border: var(--tool-chrome-border-strong);
|
|
748
|
+
--prototype-state-map-node-footer-text: var(--tool-chrome-text-heading);
|
|
749
749
|
}
|
|
750
750
|
|
|
751
751
|
/* Restore host prototype preview theme inside screenshot capture and source
|