Initial commit: img-infinite-canvas AI design workbench MVP
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
.canvas-zoom-trigger {
|
||||
min-width: 46px;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
color: #687080;
|
||||
font-size: 13px;
|
||||
font-weight: 720;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.canvas-zoom-trigger:hover,
|
||||
.canvas-zoom-trigger[data-state="open"] {
|
||||
background: #f2f4f7;
|
||||
}
|
||||
|
||||
.canvas-zoom-popover {
|
||||
width: 264px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(226, 228, 234, 0.9);
|
||||
border-radius: 17px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 14px 34px rgba(17, 24, 39, 0.13);
|
||||
}
|
||||
|
||||
.canvas-zoom-menu {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.canvas-zoom-menu button {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 3px;
|
||||
border-radius: 8px;
|
||||
color: #20242c;
|
||||
background: transparent;
|
||||
font-size: 17px;
|
||||
font-weight: 520;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.canvas-zoom-menu button:hover {
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.canvas-zoom-menu kbd {
|
||||
color: #afb3ba;
|
||||
font: inherit;
|
||||
font-size: 17px;
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
.canvas-zoom-menu i {
|
||||
height: 1px;
|
||||
margin: 7px 0 8px;
|
||||
background: #eceef2;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { useI18n } from "@/i18n/i18n";
|
||||
import "./index.css";
|
||||
|
||||
type ZoomAction = {
|
||||
label: string;
|
||||
shortcut?: string;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
export function CanvasZoomMenu({
|
||||
zoom,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
onFitAll,
|
||||
onZoomTo
|
||||
}: {
|
||||
zoom: number;
|
||||
onZoomIn: () => void;
|
||||
onZoomOut: () => void;
|
||||
onFitAll: () => void;
|
||||
onZoomTo: (zoom: number) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const isMac = useMemo(isApplePlatform, []);
|
||||
const primary = isMac ? "⌘" : "Ctrl";
|
||||
const actions: ZoomAction[] = [
|
||||
{ label: t("zoomIn"), shortcut: `${primary} +`, onSelect: onZoomIn },
|
||||
{ label: t("zoomOut"), shortcut: `${primary} -`, onSelect: onZoomOut },
|
||||
{ label: t("zoomShowAll"), shortcut: isMac ? "⇧ 1" : "Shift 1", onSelect: onFitAll }
|
||||
];
|
||||
const presets = [0.5, 1, 2];
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button type="button" className="canvas-zoom-trigger" aria-label={t("zoomMenu")} title={t("zoomMenu")}>
|
||||
{Math.round(zoom * 100)}%
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="canvas-zoom-popover" side="top" align="start" sideOffset={12} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}>
|
||||
<div className="canvas-zoom-menu">
|
||||
{actions.map((action) => (
|
||||
<button key={action.label} type="button" onClick={action.onSelect}>
|
||||
<span>{action.label}</span>
|
||||
{action.shortcut && <kbd>{action.shortcut}</kbd>}
|
||||
</button>
|
||||
))}
|
||||
<i />
|
||||
{presets.map((preset) => (
|
||||
<button key={preset} type="button" onClick={() => onZoomTo(preset)}>
|
||||
<span>{t("zoomTo", { percent: Math.round(preset * 100) })}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function isApplePlatform() {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
const nav = navigator as Navigator & { userAgentData?: { platform?: string } };
|
||||
const platform = nav.userAgentData?.platform || navigator.platform || "";
|
||||
return /mac|iphone|ipad|ipod/i.test(platform);
|
||||
}
|
||||
Reference in New Issue
Block a user