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,330 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PrototypeComponent } from "@prototype/components/prototypes/prototype-target";
|
|
4
|
+
import { Button } from "@prototype/components/ui/button";
|
|
5
|
+
import { cn } from "@prototype/lib/utils";
|
|
6
|
+
import type { MobbinReference } from "@prototype/lib/prototypes/design-exploration-types";
|
|
7
|
+
import { ChevronLeft, ChevronRight, ExternalLink, X, ZoomIn } from "lucide-react";
|
|
8
|
+
import { useCallback, useEffect, useState } from "react";
|
|
9
|
+
|
|
10
|
+
const GALLERY_SECTION_TITLE_CLASS = "tool-chip tool-chip-pink";
|
|
11
|
+
|
|
12
|
+
const GALLERY_SECTION_DESC_CLASS = "tool-body max-w-2xl";
|
|
13
|
+
|
|
14
|
+
const GALLERY_CARD_TITLE_CLASS = "text-sm font-medium text-foreground";
|
|
15
|
+
|
|
16
|
+
const GALLERY_CARD_BODY_CLASS = "tool-body";
|
|
17
|
+
|
|
18
|
+
type PrototypeMobbinGalleryLayout = "default" | "sidebar";
|
|
19
|
+
|
|
20
|
+
type PrototypeMobbinGalleryProps = {
|
|
21
|
+
componentIdPrefix: string;
|
|
22
|
+
galleryId: string;
|
|
23
|
+
references: MobbinReference[];
|
|
24
|
+
title?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
imagePathForReference?: (id: string) => string;
|
|
27
|
+
layout?: PrototypeMobbinGalleryLayout;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function resolveReferenceImageUrl(
|
|
31
|
+
reference: MobbinReference,
|
|
32
|
+
imagePathForReference?: (id: string) => string,
|
|
33
|
+
) {
|
|
34
|
+
return imagePathForReference?.(reference.id) ?? reference.imageUrl;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function MobbinReferenceCard({
|
|
38
|
+
reference,
|
|
39
|
+
imageUrl,
|
|
40
|
+
componentId,
|
|
41
|
+
onOpen,
|
|
42
|
+
compact = false,
|
|
43
|
+
}: {
|
|
44
|
+
reference: MobbinReference;
|
|
45
|
+
imageUrl: string;
|
|
46
|
+
componentId: string;
|
|
47
|
+
onOpen: () => void;
|
|
48
|
+
compact?: boolean;
|
|
49
|
+
}) {
|
|
50
|
+
const cardTitleClass = compact
|
|
51
|
+
? "text-sm text-foreground font-medium"
|
|
52
|
+
: GALLERY_CARD_TITLE_CLASS;
|
|
53
|
+
const cardBodyClass = compact
|
|
54
|
+
? "text-sm text-muted-foreground leading-relaxed"
|
|
55
|
+
: GALLERY_CARD_BODY_CLASS;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<PrototypeComponent id={componentId} className="flex flex-col gap-2">
|
|
59
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-0.5">
|
|
60
|
+
<p className={cardTitleClass}>{reference.appName}</p>
|
|
61
|
+
{reference.variantHint ? (
|
|
62
|
+
<span className="text-sm text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[13px]">
|
|
63
|
+
{reference.variantHint}
|
|
64
|
+
</span>
|
|
65
|
+
) : null}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={onOpen}
|
|
71
|
+
className={cn(
|
|
72
|
+
"bg-card ring-border group relative block w-full cursor-zoom-in overflow-hidden rounded-xl text-left shadow-lg ring-[0.5px]",
|
|
73
|
+
"focus-visible:ring-border focus-visible:outline-none focus-visible:ring-2",
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
{/* eslint-disable-next-line @next/next/no-img-element -- Mobbin CDN URLs are ephemeral; local assets preferred when available. */}
|
|
77
|
+
<img
|
|
78
|
+
src={imageUrl}
|
|
79
|
+
alt={`${reference.appName} reference on Mobbin`}
|
|
80
|
+
className="aspect-[16/10] w-full object-cover object-top transition-opacity duration-200 ease group-hover:opacity-90"
|
|
81
|
+
loading="lazy"
|
|
82
|
+
/>
|
|
83
|
+
<span className="bg-background/90 text-muted-foreground text-sm absolute right-2 top-2 inline-flex items-center gap-1 rounded-md px-2 py-1 opacity-0 backdrop-blur-sm transition-opacity duration-200 ease group-hover:opacity-100">
|
|
84
|
+
<ZoomIn className="size-3" />
|
|
85
|
+
View larger
|
|
86
|
+
</span>
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
<div className="px-0.5">
|
|
90
|
+
<p className={cardBodyClass}>{reference.relevance}</p>
|
|
91
|
+
</div>
|
|
92
|
+
</PrototypeComponent>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function MobbinReferenceLightbox({
|
|
97
|
+
references,
|
|
98
|
+
imageUrls,
|
|
99
|
+
activeIndex,
|
|
100
|
+
onClose,
|
|
101
|
+
onNavigate,
|
|
102
|
+
galleryId,
|
|
103
|
+
}: {
|
|
104
|
+
references: MobbinReference[];
|
|
105
|
+
imageUrls: string[];
|
|
106
|
+
activeIndex: number;
|
|
107
|
+
onClose: () => void;
|
|
108
|
+
onNavigate: (index: number) => void;
|
|
109
|
+
galleryId: string;
|
|
110
|
+
}) {
|
|
111
|
+
const reference = references[activeIndex];
|
|
112
|
+
const imageUrl = imageUrls[activeIndex];
|
|
113
|
+
const total = references.length;
|
|
114
|
+
const hasPrev = activeIndex > 0;
|
|
115
|
+
const hasNext = activeIndex < total - 1;
|
|
116
|
+
|
|
117
|
+
const goPrev = useCallback(() => {
|
|
118
|
+
if (hasPrev) onNavigate(activeIndex - 1);
|
|
119
|
+
}, [activeIndex, hasPrev, onNavigate]);
|
|
120
|
+
|
|
121
|
+
const goNext = useCallback(() => {
|
|
122
|
+
if (hasNext) onNavigate(activeIndex + 1);
|
|
123
|
+
}, [activeIndex, hasNext, onNavigate]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
127
|
+
if (event.key === "Escape") {
|
|
128
|
+
onClose();
|
|
129
|
+
} else if (event.key === "ArrowLeft") {
|
|
130
|
+
goPrev();
|
|
131
|
+
} else if (event.key === "ArrowRight") {
|
|
132
|
+
goNext();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
137
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
138
|
+
}, [goNext, goPrev, onClose]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const previousOverflow = document.body.style.overflow;
|
|
142
|
+
document.body.style.overflow = "hidden";
|
|
143
|
+
return () => {
|
|
144
|
+
document.body.style.overflow = previousOverflow;
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<PrototypeComponent
|
|
150
|
+
id={`${galleryId}.lightbox`}
|
|
151
|
+
className="fixed inset-0 z-(--z-modal) flex items-center justify-center p-4 sm:p-8"
|
|
152
|
+
role="dialog"
|
|
153
|
+
aria-modal="true"
|
|
154
|
+
aria-label={`${reference.appName} Mobbin reference`}
|
|
155
|
+
data-mobbin-lightbox-open=""
|
|
156
|
+
>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
aria-label="Close lightbox"
|
|
160
|
+
className="bg-muted/70 absolute inset-0 backdrop-blur-[6px] transition-opacity duration-200 ease-(--ease-out-cubic) motion-reduce:transition-none"
|
|
161
|
+
onClick={onClose}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
<div className="pointer-events-none relative z-10 flex max-h-full w-full max-w-6xl flex-col gap-4">
|
|
165
|
+
<div className="pointer-events-auto flex items-center justify-between gap-4">
|
|
166
|
+
<p className="text-sm text-muted-foreground">
|
|
167
|
+
{activeIndex + 1} of {total}
|
|
168
|
+
</p>
|
|
169
|
+
<PrototypeComponent id={`${galleryId}.lightbox.close`}>
|
|
170
|
+
<Button
|
|
171
|
+
type="button"
|
|
172
|
+
variant="secondary"
|
|
173
|
+
size="icon"
|
|
174
|
+
aria-label="Close"
|
|
175
|
+
onClick={onClose}
|
|
176
|
+
>
|
|
177
|
+
<X />
|
|
178
|
+
</Button>
|
|
179
|
+
</PrototypeComponent>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className="relative flex min-h-0 flex-1 items-center justify-center">
|
|
183
|
+
{hasPrev ? (
|
|
184
|
+
<PrototypeComponent
|
|
185
|
+
id={`${galleryId}.lightbox.prev`}
|
|
186
|
+
className="pointer-events-auto absolute left-0 z-10 -translate-x-1/2 sm:-translate-x-full sm:pr-3"
|
|
187
|
+
>
|
|
188
|
+
<Button
|
|
189
|
+
type="button"
|
|
190
|
+
variant="secondary"
|
|
191
|
+
size="icon"
|
|
192
|
+
aria-label="Previous reference"
|
|
193
|
+
onClick={goPrev}
|
|
194
|
+
>
|
|
195
|
+
<ChevronLeft />
|
|
196
|
+
</Button>
|
|
197
|
+
</PrototypeComponent>
|
|
198
|
+
) : null}
|
|
199
|
+
|
|
200
|
+
<div className="bg-card ring-border pointer-events-auto max-h-[min(75vh,900px)] w-full overflow-hidden rounded-xl shadow-lg ring-[0.5px]">
|
|
201
|
+
{/* eslint-disable-next-line @next/next/no-img-element -- Mobbin CDN URLs are ephemeral; local assets preferred when available. */}
|
|
202
|
+
<img
|
|
203
|
+
src={imageUrl}
|
|
204
|
+
alt={`${reference.appName} reference on Mobbin`}
|
|
205
|
+
className="max-h-[min(75vh,900px)] w-full object-contain object-top"
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{hasNext ? (
|
|
210
|
+
<PrototypeComponent
|
|
211
|
+
id={`${galleryId}.lightbox.next`}
|
|
212
|
+
className="pointer-events-auto absolute right-0 z-10 translate-x-1/2 sm:translate-x-full sm:pl-3"
|
|
213
|
+
>
|
|
214
|
+
<Button
|
|
215
|
+
type="button"
|
|
216
|
+
variant="secondary"
|
|
217
|
+
size="icon"
|
|
218
|
+
aria-label="Next reference"
|
|
219
|
+
onClick={goNext}
|
|
220
|
+
>
|
|
221
|
+
<ChevronRight />
|
|
222
|
+
</Button>
|
|
223
|
+
</PrototypeComponent>
|
|
224
|
+
) : null}
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div className="bg-card/95 ring-border pointer-events-auto rounded-xl px-4 py-3 shadow-lg ring-[0.5px] backdrop-blur-sm sm:px-5 sm:py-4">
|
|
228
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
229
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
230
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
231
|
+
<p className={GALLERY_CARD_TITLE_CLASS}>{reference.appName}</p>
|
|
232
|
+
{reference.variantHint ? (
|
|
233
|
+
<span className="text-sm text-muted-foreground bg-muted rounded px-1.5 py-0.5">
|
|
234
|
+
{reference.variantHint}
|
|
235
|
+
</span>
|
|
236
|
+
) : null}
|
|
237
|
+
</div>
|
|
238
|
+
<p className={GALLERY_CARD_BODY_CLASS}>{reference.relevance}</p>
|
|
239
|
+
</div>
|
|
240
|
+
<a
|
|
241
|
+
href={reference.mobbinUrl}
|
|
242
|
+
target="_blank"
|
|
243
|
+
rel="noopener noreferrer"
|
|
244
|
+
className={cn(
|
|
245
|
+
GALLERY_CARD_BODY_CLASS,
|
|
246
|
+
"hover:text-foreground inline-flex shrink-0 items-center gap-1.5 transition-colors duration-200 ease",
|
|
247
|
+
)}
|
|
248
|
+
>
|
|
249
|
+
View on Mobbin
|
|
250
|
+
<ExternalLink className="size-3.5" />
|
|
251
|
+
</a>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</PrototypeComponent>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function PrototypeMobbinGallery({
|
|
260
|
+
componentIdPrefix,
|
|
261
|
+
galleryId,
|
|
262
|
+
references,
|
|
263
|
+
title = "Mobbin references",
|
|
264
|
+
description = "Reference screens from B2B and developer dashboards.",
|
|
265
|
+
imagePathForReference,
|
|
266
|
+
layout = "default",
|
|
267
|
+
}: PrototypeMobbinGalleryProps) {
|
|
268
|
+
const isSidebar = layout === "sidebar";
|
|
269
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
270
|
+
const imageUrls = references.map((reference) =>
|
|
271
|
+
resolveReferenceImageUrl(reference, imagePathForReference),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const sectionTitleClass = isSidebar
|
|
275
|
+
? "text-xs font-medium text-muted-foreground uppercase tracking-wide"
|
|
276
|
+
: GALLERY_SECTION_TITLE_CLASS;
|
|
277
|
+
const sectionDescClass = isSidebar
|
|
278
|
+
? "text-sm text-muted-foreground leading-relaxed"
|
|
279
|
+
: GALLERY_SECTION_DESC_CLASS;
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<PrototypeComponent id={galleryId}>
|
|
283
|
+
<PrototypeComponent
|
|
284
|
+
id={`${componentIdPrefix}.mobbin-references`}
|
|
285
|
+
className={cn(
|
|
286
|
+
isSidebar
|
|
287
|
+
? "flex flex-col gap-3"
|
|
288
|
+
: "border-border border-t px-6 py-8",
|
|
289
|
+
)}
|
|
290
|
+
>
|
|
291
|
+
<div className={cn("flex flex-col gap-2", !isSidebar && "mb-6")}>
|
|
292
|
+
<p className={sectionTitleClass}>{title}</p>
|
|
293
|
+
{!isSidebar && <p className={sectionDescClass}>{description}</p>}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div
|
|
297
|
+
className={cn(
|
|
298
|
+
"grid grid-cols-1",
|
|
299
|
+
isSidebar ? "gap-4" : "gap-10",
|
|
300
|
+
!isSidebar &&
|
|
301
|
+
references.length > 1 &&
|
|
302
|
+
"lg:grid-cols-2 lg:gap-x-8 lg:gap-y-12",
|
|
303
|
+
)}
|
|
304
|
+
>
|
|
305
|
+
{references.map((reference, index) => (
|
|
306
|
+
<MobbinReferenceCard
|
|
307
|
+
key={reference.id}
|
|
308
|
+
reference={reference}
|
|
309
|
+
imageUrl={imageUrls[index]}
|
|
310
|
+
componentId={`${componentIdPrefix}.mobbin-reference.${reference.id}`}
|
|
311
|
+
onOpen={() => setActiveIndex(index)}
|
|
312
|
+
compact={isSidebar}
|
|
313
|
+
/>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
</PrototypeComponent>
|
|
317
|
+
|
|
318
|
+
{activeIndex !== null ? (
|
|
319
|
+
<MobbinReferenceLightbox
|
|
320
|
+
references={references}
|
|
321
|
+
imageUrls={imageUrls}
|
|
322
|
+
activeIndex={activeIndex}
|
|
323
|
+
onClose={() => setActiveIndex(null)}
|
|
324
|
+
onNavigate={setActiveIndex}
|
|
325
|
+
galleryId={galleryId}
|
|
326
|
+
/>
|
|
327
|
+
) : null}
|
|
328
|
+
</PrototypeComponent>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ToolbarIconButton } from "@prototype/components/platform-ui/toolbar-icon-button";
|
|
4
|
+
import { Button } from "@prototype/components/ui/button";
|
|
5
|
+
import { Input } from "@prototype/components/ui/input";
|
|
6
|
+
import { Label } from "@prototype/components/ui/label";
|
|
7
|
+
import {
|
|
8
|
+
buildAddReferenceDocCopyText,
|
|
9
|
+
defaultReferenceDocsConfigPath,
|
|
10
|
+
isValidReferenceDocInput,
|
|
11
|
+
type PrototypeReferenceDoc,
|
|
12
|
+
} from "@prototype/lib/prototypes/reference-docs";
|
|
13
|
+
import { usePrototypeReviewOptional } from "@prototype/lib/prototypes/prototype-review-context";
|
|
14
|
+
import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
|
|
15
|
+
import { cn } from "@prototype/lib/utils";
|
|
16
|
+
import { ChevronDown, ExternalLink, Plus } from "lucide-react";
|
|
17
|
+
import { useId, useState } from "react";
|
|
18
|
+
|
|
19
|
+
const REFERENCE_DOC_CONTENT_FIELD_CLASS =
|
|
20
|
+
"field-sizing-content min-h-[5rem] w-full resize-y rounded-md border border-[var(--tool-chrome-border)] bg-[var(--tool-chrome-surface-muted)] px-2.5 py-2 text-sm text-[var(--tool-chrome-text)] leading-relaxed shadow-none outline-none placeholder:text-[var(--tool-chrome-text-muted)] focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-0";
|
|
21
|
+
|
|
22
|
+
function ReferenceDocItem({ doc }: { doc: PrototypeReferenceDoc }) {
|
|
23
|
+
const [expanded, setExpanded] = useState(false);
|
|
24
|
+
const hasContent = doc.content.trim().length > 0;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<li className="overflow-hidden rounded-md border border-[var(--tool-chrome-border)] bg-[var(--tool-chrome-surface-muted)]">
|
|
28
|
+
<div className="flex items-stretch">
|
|
29
|
+
<a
|
|
30
|
+
href={doc.link}
|
|
31
|
+
target="_blank"
|
|
32
|
+
rel="noopener noreferrer"
|
|
33
|
+
className="flex min-w-0 flex-1 items-center gap-1.5 px-2.5 py-2 text-sm text-[var(--tool-chrome-text)] transition-colors duration-200 ease hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-text-heading)]"
|
|
34
|
+
>
|
|
35
|
+
<ExternalLink className="size-3.5 shrink-0" aria-hidden />
|
|
36
|
+
<span className="truncate">{doc.name}</span>
|
|
37
|
+
</a>
|
|
38
|
+
{hasContent ? (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
className="inline-flex size-9 shrink-0 items-center justify-center border-l border-[var(--tool-chrome-border)] text-[var(--tool-chrome-text-muted)] transition-colors duration-200 ease hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-text)]"
|
|
42
|
+
aria-expanded={expanded}
|
|
43
|
+
aria-label={expanded ? "Hide content" : "Show content"}
|
|
44
|
+
onClick={() => setExpanded((current) => !current)}
|
|
45
|
+
>
|
|
46
|
+
<ChevronDown
|
|
47
|
+
className={cn(
|
|
48
|
+
"tool-panel-section-chevron size-3.5",
|
|
49
|
+
expanded && "is-expanded",
|
|
50
|
+
)}
|
|
51
|
+
aria-hidden
|
|
52
|
+
/>
|
|
53
|
+
</button>
|
|
54
|
+
) : null}
|
|
55
|
+
</div>
|
|
56
|
+
{hasContent ? (
|
|
57
|
+
<div
|
|
58
|
+
className={cn(
|
|
59
|
+
"tool-panel-section-content border-t border-[var(--tool-chrome-border)]",
|
|
60
|
+
expanded && "is-expanded",
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
<div className="overflow-hidden">
|
|
64
|
+
<p className="px-2.5 py-2 text-xs leading-relaxed whitespace-pre-wrap text-[var(--tool-chrome-text-muted)]">
|
|
65
|
+
{doc.content}
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
) : null}
|
|
70
|
+
</li>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type PrototypeReferenceDocsProps = {
|
|
75
|
+
slug?: string;
|
|
76
|
+
docs?: PrototypeReferenceDoc[];
|
|
77
|
+
configFilePath?: string;
|
|
78
|
+
className?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function PrototypeReferenceDocs({
|
|
82
|
+
slug: slugProp,
|
|
83
|
+
docs: docsProp,
|
|
84
|
+
configFilePath: configFilePathProp,
|
|
85
|
+
className,
|
|
86
|
+
}: PrototypeReferenceDocsProps) {
|
|
87
|
+
const review = usePrototypeReviewOptional();
|
|
88
|
+
const slug = slugProp ?? review?.slug;
|
|
89
|
+
const docs = docsProp ?? review?.referenceDocs ?? [];
|
|
90
|
+
const configFilePath =
|
|
91
|
+
configFilePathProp ??
|
|
92
|
+
review?.referenceDocsConfigFilePath ??
|
|
93
|
+
(slug ? defaultReferenceDocsConfigPath(slug) : undefined);
|
|
94
|
+
const nameFieldId = useId();
|
|
95
|
+
const linkFieldId = useId();
|
|
96
|
+
const contentFieldId = useId();
|
|
97
|
+
const [showForm, setShowForm] = useState(false);
|
|
98
|
+
const [name, setName] = useState("");
|
|
99
|
+
const [link, setLink] = useState("");
|
|
100
|
+
const [content, setContent] = useState("");
|
|
101
|
+
const { copy, icon, isCopied } = useCopyToClipboard();
|
|
102
|
+
|
|
103
|
+
const canCopy = Boolean(slug) && isValidReferenceDocInput(name, link, content);
|
|
104
|
+
|
|
105
|
+
const handleCopyPrompt = () => {
|
|
106
|
+
if (!slug || !canCopy) return;
|
|
107
|
+
|
|
108
|
+
const origin =
|
|
109
|
+
typeof window !== "undefined" ? window.location.origin : undefined;
|
|
110
|
+
|
|
111
|
+
copy(
|
|
112
|
+
buildAddReferenceDocCopyText({
|
|
113
|
+
slug,
|
|
114
|
+
name,
|
|
115
|
+
link,
|
|
116
|
+
content,
|
|
117
|
+
existingDocs: docs,
|
|
118
|
+
configFilePath,
|
|
119
|
+
origin,
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className={cn("flex flex-col gap-2", className)}>
|
|
126
|
+
<div className="flex items-center justify-between gap-2">
|
|
127
|
+
<span className="tool-section-label">Reference docs</span>
|
|
128
|
+
<ToolbarIconButton
|
|
129
|
+
onClick={() => setShowForm((current) => !current)}
|
|
130
|
+
aria-label={showForm ? "Cancel adding doc" : "Add doc"}
|
|
131
|
+
aria-expanded={showForm}
|
|
132
|
+
>
|
|
133
|
+
<Plus size={16} strokeWidth={2} aria-hidden />
|
|
134
|
+
</ToolbarIconButton>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{showForm ? (
|
|
138
|
+
<div className="flex flex-col gap-3 rounded-md border border-[var(--tool-chrome-border)] bg-[var(--tool-chrome-surface-muted)] p-3">
|
|
139
|
+
<div className="flex flex-col gap-1.5">
|
|
140
|
+
<Label htmlFor={nameFieldId} className="text-foreground">
|
|
141
|
+
Doc name
|
|
142
|
+
</Label>
|
|
143
|
+
<Input
|
|
144
|
+
id={nameFieldId}
|
|
145
|
+
value={name}
|
|
146
|
+
onChange={(event) => setName(event.target.value)}
|
|
147
|
+
placeholder="Design spec"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex flex-col gap-1.5">
|
|
151
|
+
<Label htmlFor={linkFieldId} className="text-foreground">
|
|
152
|
+
Doc link
|
|
153
|
+
</Label>
|
|
154
|
+
<Input
|
|
155
|
+
id={linkFieldId}
|
|
156
|
+
value={link}
|
|
157
|
+
onChange={(event) => setLink(event.target.value)}
|
|
158
|
+
placeholder="https://docs.google.com/..."
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="flex flex-col gap-1.5">
|
|
162
|
+
<Label htmlFor={contentFieldId} className="text-foreground">
|
|
163
|
+
Content
|
|
164
|
+
</Label>
|
|
165
|
+
<textarea
|
|
166
|
+
id={contentFieldId}
|
|
167
|
+
value={content}
|
|
168
|
+
onChange={(event) => setContent(event.target.value)}
|
|
169
|
+
placeholder="Summarize what this doc covers and why it matters…"
|
|
170
|
+
aria-label="Doc content"
|
|
171
|
+
rows={4}
|
|
172
|
+
className={REFERENCE_DOC_CONTENT_FIELD_CLASS}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
<div className="flex items-center gap-2">
|
|
176
|
+
<Button
|
|
177
|
+
type="button"
|
|
178
|
+
size="default"
|
|
179
|
+
disabled={!canCopy}
|
|
180
|
+
onClick={handleCopyPrompt}
|
|
181
|
+
aria-label="Copy reference doc prompt"
|
|
182
|
+
>
|
|
183
|
+
{isCopied ? "Copied" : "Copy prompt"}
|
|
184
|
+
{icon}
|
|
185
|
+
</Button>
|
|
186
|
+
<Button
|
|
187
|
+
type="button"
|
|
188
|
+
variant="ghost"
|
|
189
|
+
size="default"
|
|
190
|
+
onClick={() => {
|
|
191
|
+
setShowForm(false);
|
|
192
|
+
setName("");
|
|
193
|
+
setLink("");
|
|
194
|
+
setContent("");
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
Cancel
|
|
198
|
+
</Button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
) : null}
|
|
202
|
+
|
|
203
|
+
{docs.length === 0 ? (
|
|
204
|
+
<p className="text-xs text-muted-foreground">
|
|
205
|
+
No reference docs yet. Add one in{" "}
|
|
206
|
+
{configFilePath ? (
|
|
207
|
+
<code className="text-[11px]">{configFilePath}</code>
|
|
208
|
+
) : (
|
|
209
|
+
"the prototype codebase"
|
|
210
|
+
)}
|
|
211
|
+
.
|
|
212
|
+
</p>
|
|
213
|
+
) : (
|
|
214
|
+
<ul className="flex flex-col gap-1.5">
|
|
215
|
+
{docs.map((doc) => (
|
|
216
|
+
<ReferenceDocItem key={`${doc.name}:${doc.link}`} doc={doc} />
|
|
217
|
+
))}
|
|
218
|
+
</ul>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|