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