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,500 @@
|
||||
.artboard-image,
|
||||
.artboard-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.artboard-image {
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
object-position: center center;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.transparent-artboard-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.adjusted-artboard-image {
|
||||
--adjust-warm-opacity: 0;
|
||||
--adjust-cool-opacity: 0;
|
||||
--adjust-magenta-opacity: 0;
|
||||
--adjust-green-opacity: 0;
|
||||
--adjust-highlight-lift: 0;
|
||||
--adjust-highlight-recover: 0;
|
||||
--adjust-shadow-lift: 0;
|
||||
--adjust-shadow-crush: 0;
|
||||
--adjust-vignette-opacity: 0;
|
||||
--adjust-vignette-color: 0, 0, 0;
|
||||
--adjust-grain-opacity: 0;
|
||||
--adjust-bloom-opacity: 0;
|
||||
--adjust-bloom-blur: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.adjusted-artboard-image .artboard-image,
|
||||
.adjustment-bloom-image,
|
||||
.adjusted-artboard-image span {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.adjustment-bloom-image {
|
||||
object-fit: contain;
|
||||
object-position: center center;
|
||||
opacity: var(--adjust-bloom-opacity);
|
||||
filter: blur(var(--adjust-bloom-blur)) brightness(1.18) saturate(1.08);
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.adjusted-artboard-image span {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.adjustment-light-overlay {
|
||||
background:
|
||||
linear-gradient(rgba(255, 255, 255, var(--adjust-highlight-lift)), rgba(255, 255, 255, var(--adjust-highlight-lift))),
|
||||
linear-gradient(rgba(0, 0, 0, var(--adjust-highlight-recover)), rgba(0, 0, 0, var(--adjust-highlight-recover))),
|
||||
linear-gradient(rgba(255, 255, 255, var(--adjust-shadow-lift)), rgba(255, 255, 255, var(--adjust-shadow-lift))),
|
||||
linear-gradient(rgba(0, 0, 0, var(--adjust-shadow-crush)), rgba(0, 0, 0, var(--adjust-shadow-crush)));
|
||||
mix-blend-mode: soft-light;
|
||||
}
|
||||
|
||||
.adjustment-temperature-overlay {
|
||||
background:
|
||||
linear-gradient(rgba(255, 190, 74, var(--adjust-warm-opacity)), rgba(255, 190, 74, var(--adjust-warm-opacity))),
|
||||
linear-gradient(rgba(72, 150, 255, var(--adjust-cool-opacity)), rgba(72, 150, 255, var(--adjust-cool-opacity))),
|
||||
linear-gradient(rgba(255, 78, 205, var(--adjust-magenta-opacity)), rgba(255, 78, 205, var(--adjust-magenta-opacity))),
|
||||
linear-gradient(rgba(42, 190, 104, var(--adjust-green-opacity)), rgba(42, 190, 104, var(--adjust-green-opacity)));
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.adjustment-vignette-overlay {
|
||||
background: radial-gradient(circle at center, transparent 50%, rgba(var(--adjust-vignette-color), var(--adjust-vignette-opacity)) 100%);
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.adjustment-grain-overlay {
|
||||
opacity: var(--adjust-grain-opacity);
|
||||
background-image:
|
||||
radial-gradient(circle at 25% 30%, rgba(255, 255, 255, 0.75) 0 0.6px, transparent 0.8px),
|
||||
radial-gradient(circle at 72% 64%, rgba(0, 0, 0, 0.68) 0 0.55px, transparent 0.75px);
|
||||
background-size: 4px 4px, 5px 5px;
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.pen-stroke-node .artboard-image {
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
.manual-frame-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.image-generator-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.image-generator-label {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #2f80ff;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-generator-label svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
fill: #2f80ff;
|
||||
stroke: #2f80ff;
|
||||
}
|
||||
|
||||
.image-generator-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px solid #2f80ff;
|
||||
background: #eeeeed;
|
||||
}
|
||||
|
||||
.image-generator-frame svg {
|
||||
color: #cfd0d2;
|
||||
fill: #cfd0d2;
|
||||
stroke: #cfd0d2;
|
||||
}
|
||||
|
||||
.generation-skeleton {
|
||||
--canvas-work-ui-scale: 1;
|
||||
--canvas-work-label-font: 18px;
|
||||
--canvas-work-label-pad-x: 18px;
|
||||
--canvas-work-label-pad-y: 8px;
|
||||
--canvas-work-label-bottom: 18px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
align-content: end;
|
||||
gap: 7%;
|
||||
padding: 9%;
|
||||
background: #f5f6f8;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.generation-skeleton-image,
|
||||
.generation-skeleton-lines span {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #e6e9ee;
|
||||
}
|
||||
|
||||
.generation-skeleton-image::after,
|
||||
.generation-skeleton-lines span::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: translateX(-100%);
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.72), transparent);
|
||||
animation: skeleton-shimmer 1.35s infinite;
|
||||
}
|
||||
|
||||
.generation-skeleton-image {
|
||||
min-height: 0;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.generation-skeleton-lines {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.generation-skeleton-lines span {
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.generation-skeleton-lines span:nth-child(2) {
|
||||
width: 72%;
|
||||
}
|
||||
|
||||
.generation-skeleton-lines span:nth-child(3) {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.generation-skeleton-label {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(var(--canvas-work-label-bottom) * var(--canvas-work-ui-scale));
|
||||
z-index: 2;
|
||||
max-width: calc(100% - calc(56px * var(--canvas-work-ui-scale)));
|
||||
min-width: calc(64px * var(--canvas-work-ui-scale));
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: calc(var(--canvas-work-label-pad-y) * var(--canvas-work-ui-scale)) calc(var(--canvas-work-label-pad-x) * var(--canvas-work-ui-scale));
|
||||
border-radius: 999px;
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
background: rgba(87, 94, 94, 0.5);
|
||||
backdrop-filter: blur(12px);
|
||||
font-size: calc(var(--canvas-work-label-font) * var(--canvas-work-ui-scale));
|
||||
font-weight: 520;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
transform: translateX(-50%);
|
||||
will-change: transform, background-color;
|
||||
animation: generation-label-float 2.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.generation-skeleton-label::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(110deg, transparent 0%, rgba(255, 255, 255, 0.2) 38%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.16) 62%, transparent 100%);
|
||||
transform: translateX(-115%) skewX(-16deg);
|
||||
animation: generation-label-sheen 1.9s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes generation-label-float {
|
||||
0%,
|
||||
100% {
|
||||
background: rgba(87, 94, 94, 0.48);
|
||||
transform: translateX(-50%) translateY(0) scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
background: rgba(87, 94, 94, 0.56);
|
||||
transform: translateX(-50%) translateY(calc(-1px * var(--canvas-work-ui-scale))) scale(1.012);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes generation-label-sheen {
|
||||
0% {
|
||||
transform: translateX(-115%) skewX(-16deg);
|
||||
}
|
||||
|
||||
58%,
|
||||
100% {
|
||||
transform: translateX(115%) skewX(-16deg);
|
||||
}
|
||||
}
|
||||
|
||||
.artboard-preview {
|
||||
overflow: hidden;
|
||||
color: #171717;
|
||||
background: #fff;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.artboard-nav {
|
||||
height: 9%;
|
||||
min-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4%;
|
||||
padding: 0 4%;
|
||||
color: #18191b;
|
||||
background: #fff9f2;
|
||||
font-size: clamp(7px, 1.6%, 11px);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.artboard-nav b {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.wide-hero {
|
||||
position: relative;
|
||||
height: 91%;
|
||||
display: grid;
|
||||
grid-template-columns: 0.9fr 1fr 0.8fr;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 76% 22%, #ffce26 0 28%, transparent 29%),
|
||||
radial-gradient(circle at 63% -18%, #0067d8 0 30%, transparent 31%),
|
||||
linear-gradient(135deg, #ff4d3f 0 43%, #ffe9e2 43% 62%, #ffc928 62% 100%);
|
||||
}
|
||||
|
||||
.hero-copyline {
|
||||
z-index: 1;
|
||||
align-self: center;
|
||||
padding-left: 12%;
|
||||
}
|
||||
|
||||
.hero-copyline strong {
|
||||
display: block;
|
||||
color: #111;
|
||||
font-size: clamp(28px, 14.5cqw, 78px);
|
||||
line-height: 0.84;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.hero-copyline small {
|
||||
display: block;
|
||||
margin-top: 7%;
|
||||
font-size: clamp(7px, 2cqw, 12px);
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.hero-model {
|
||||
position: absolute;
|
||||
left: 48%;
|
||||
bottom: -8%;
|
||||
width: 26%;
|
||||
height: 93%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 46% 46% 12% 12%;
|
||||
background:
|
||||
radial-gradient(circle at 50% 12%, #9a6a50 0 9%, transparent 10%),
|
||||
linear-gradient(75deg, transparent 0 27%, #17285b 28% 70%, transparent 71%),
|
||||
linear-gradient(95deg, transparent 0 38%, #0f214d 39% 62%, transparent 63%);
|
||||
filter: drop-shadow(0 10px 12px rgba(15, 23, 42, 0.18));
|
||||
}
|
||||
|
||||
.hero-note {
|
||||
z-index: 1;
|
||||
align-self: center;
|
||||
max-width: 74%;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
color: #1b1b1b;
|
||||
font-size: clamp(8px, 2.2cqw, 13px);
|
||||
}
|
||||
|
||||
.hero-note span {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.artboard-tall {
|
||||
display: grid;
|
||||
grid-template-rows: 28% 32% 27% 13%;
|
||||
}
|
||||
|
||||
.artboard-tall .artboard-wide {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.mood-section {
|
||||
display: grid;
|
||||
grid-template-rows: 26% 1fr;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mood-section header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8%;
|
||||
padding: 0 6%;
|
||||
font-size: clamp(8px, 2.4cqw, 14px);
|
||||
}
|
||||
|
||||
.mood-section header b {
|
||||
margin-right: auto;
|
||||
font-size: clamp(18px, 6cqw, 34px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mood-section header span {
|
||||
color: #868686;
|
||||
}
|
||||
|
||||
.color-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.color-cards div {
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-content: end;
|
||||
justify-items: center;
|
||||
overflow: hidden;
|
||||
padding-bottom: 8%;
|
||||
}
|
||||
|
||||
.color-cards div:nth-child(1) {
|
||||
background: #ff5545;
|
||||
}
|
||||
|
||||
.color-cards div:nth-child(2) {
|
||||
background: #ffd72f;
|
||||
}
|
||||
|
||||
.color-cards div:nth-child(3) {
|
||||
background: #77c47c;
|
||||
}
|
||||
|
||||
.color-cards div:nth-child(4) {
|
||||
background: #2382dc;
|
||||
}
|
||||
|
||||
.color-cards i {
|
||||
width: 48%;
|
||||
height: 54%;
|
||||
margin-bottom: 6%;
|
||||
border-radius: 42% 42% 18% 18%;
|
||||
background:
|
||||
radial-gradient(circle at 50% 10%, rgba(255, 255, 255, 0.36) 0 11%, transparent 12%),
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.36), rgba(0, 0, 0, 0.12));
|
||||
box-shadow: 0 10px 12px rgba(17, 24, 39, 0.18);
|
||||
}
|
||||
|
||||
.color-cards strong {
|
||||
color: #111;
|
||||
font-size: clamp(10px, 3.6cqw, 24px);
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
.product-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 4%;
|
||||
padding: 6%;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.product-section span,
|
||||
.neutral-grid i {
|
||||
border-radius: 2px;
|
||||
background:
|
||||
radial-gradient(circle at 50% 22%, #f2eee6 0 25%, transparent 26%),
|
||||
linear-gradient(#d7cec2, #f8f4eb);
|
||||
}
|
||||
|
||||
.artboard-tall footer,
|
||||
.artboard-square footer {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
background: #111;
|
||||
font-size: clamp(10px, 3.1cqw, 18px);
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.artboard-square {
|
||||
display: grid;
|
||||
grid-template-rows: 32% 53% 15%;
|
||||
background: #f8f5ee;
|
||||
}
|
||||
|
||||
.neutral-hero {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 6px;
|
||||
padding: 0 9%;
|
||||
background: #eee8dc;
|
||||
}
|
||||
|
||||
.neutral-hero strong {
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(20px, 8cqw, 44px);
|
||||
line-height: 0.95;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.neutral-hero span {
|
||||
color: #6e665d;
|
||||
font-size: clamp(8px, 2.6cqw, 14px);
|
||||
}
|
||||
|
||||
.neutral-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 5%;
|
||||
padding: 6%;
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
"use client";
|
||||
|
||||
import { Circle, Image as ImageIcon } from "lucide-react";
|
||||
import type { CSSProperties } from "react";
|
||||
import type { CanvasNode } from "@/domain/design";
|
||||
import { useI18n } from "@/i18n/i18n";
|
||||
import { CanvasWorkOverlay } from "@/ui/components/CanvasWorkOverlay";
|
||||
import { LayerMergeSkeleton } from "@/ui/components/LayerMergeSkeleton";
|
||||
import { imageAdjustmentImageStyle, imageAdjustmentLayerStyle, isDefaultImageAdjustments, parseImageAdjustments } from "@/ui/lib/imageAdjustments";
|
||||
import { canvasImageUrl } from "@/ui/lib/imageDelivery";
|
||||
import { isImageUrl } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps";
|
||||
import { framePreviewStyle } from "@/ui/pages/CanvasWorkspace/canvas/frameStyle";
|
||||
import "./index.css";
|
||||
|
||||
export function ArtboardPreview({ node, viewportScale = 1 }: { node: CanvasNode; viewportScale?: number }) {
|
||||
const { t } = useI18n();
|
||||
const overlayUiStyle = workOverlayUiStyle(viewportScale, node);
|
||||
|
||||
if (node.status === "generating" && node.layerRole === "merge-layer") {
|
||||
return (
|
||||
<div className="layer-merge-preview">
|
||||
{isImageUrl(node.content) && <img className="layer-merge-preview-image" src={canvasImageUrl(node.content)} alt={node.title} draggable={false} />}
|
||||
<LayerMergeSkeleton title={node.title} style={overlayUiStyle} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.status === "generating" && node.type === "image" && isImageUrl(node.content)) {
|
||||
return <WorkingImagePreview node={node} label={node.title} style={overlayUiStyle} />;
|
||||
}
|
||||
|
||||
if (node.status === "generating") {
|
||||
return <GeneratingArtboardSkeleton title={node.title} style={overlayUiStyle} />;
|
||||
}
|
||||
|
||||
if (node.status === "text-extracting" && node.type === "image" && isImageUrl(node.content)) {
|
||||
return <WorkingImagePreview node={node} label={t("extractingText")} style={overlayUiStyle} />;
|
||||
}
|
||||
|
||||
if (node.status === "mockup-modeling" && node.type === "image" && isImageUrl(node.content)) {
|
||||
return <WorkingImagePreview node={node} label={t("mockupPreparing")} style={overlayUiStyle} />;
|
||||
}
|
||||
|
||||
if (node.status === "uploading" && node.type === "image" && isImageUrl(node.content)) {
|
||||
return <WorkingImagePreview node={node} label={t("uploadingImage")} style={overlayUiStyle} />;
|
||||
}
|
||||
|
||||
if (node.type === "image" && isImageUrl(node.content)) {
|
||||
const adjustments = parseImageAdjustments(node.imageAdjustments);
|
||||
if (!isDefaultImageAdjustments(adjustments)) {
|
||||
return <AdjustedImagePreview node={node} />;
|
||||
}
|
||||
if (node.layerRole === "background-removed" || node.tone === "transparent-image") {
|
||||
return <TransparentImagePreview node={node} />;
|
||||
}
|
||||
return <img className="artboard-image" src={canvasImageUrl(node.content)} alt={node.title} draggable={false} />;
|
||||
}
|
||||
|
||||
if (node.type === "image" && node.layerRole === "image-generator") {
|
||||
return <ImageGeneratorPreview node={node} />;
|
||||
}
|
||||
|
||||
if (node.type === "frame" && node.layerRole === "manual-frame") {
|
||||
return <div className="manual-frame-preview" style={framePreviewStyle(node)} />;
|
||||
}
|
||||
|
||||
const kind = artboardKind(node);
|
||||
if (kind === "wide") return <WideCommerceBoard node={node} />;
|
||||
if (kind === "tall") return <TallCommerceBoard node={node} />;
|
||||
return <SquareCommerceBoard node={node} />;
|
||||
}
|
||||
|
||||
function TransparentImagePreview({ node }: { node: CanvasNode }) {
|
||||
return (
|
||||
<div className="transparent-artboard-preview">
|
||||
<img className="artboard-image" src={canvasImageUrl(node.content)} alt={node.title} draggable={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdjustedImagePreview({ node }: { node: CanvasNode }) {
|
||||
const adjustments = parseImageAdjustments(node.imageAdjustments);
|
||||
const source = canvasImageUrl(node.content);
|
||||
return (
|
||||
<div className="adjusted-artboard-image" style={imageAdjustmentLayerStyle(adjustments)}>
|
||||
<img className="artboard-image" src={source} alt={node.title} draggable={false} style={imageAdjustmentImageStyle(adjustments)} />
|
||||
<img className="adjustment-bloom-image" src={source} alt="" draggable={false} aria-hidden="true" />
|
||||
<span className="adjustment-light-overlay" />
|
||||
<span className="adjustment-temperature-overlay" />
|
||||
<span className="adjustment-vignette-overlay" />
|
||||
<span className="adjustment-grain-overlay" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function workOverlayUiStyle(viewportScale: number, node: CanvasNode) {
|
||||
const scale = Math.max(viewportScale, 0.01);
|
||||
const screenMin = Math.max(1, Math.min(node.width * scale, node.height * scale));
|
||||
const labelFont = clampNumber(screenMin * 0.038, 10, 15);
|
||||
return {
|
||||
"--canvas-work-ui-scale": `${1 / scale}`,
|
||||
"--canvas-work-label-font": `${labelFont}px`,
|
||||
"--canvas-work-label-pad-y": `${clampNumber(labelFont * 0.34, 4, 6)}px`,
|
||||
"--canvas-work-label-pad-x": `${clampNumber(labelFont * 0.82, 9, 14)}px`,
|
||||
"--canvas-work-label-bottom": `${clampNumber(screenMin * 0.055, 10, 18)}px`
|
||||
} as CSSProperties;
|
||||
}
|
||||
|
||||
function clampNumber(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function artboardKind(node: CanvasNode) {
|
||||
if (node.height > node.width * 1.24) return "tall";
|
||||
if (node.width > node.height * 1.35) return "wide";
|
||||
return "square";
|
||||
}
|
||||
|
||||
function ImageGeneratorPreview({ node }: { node: CanvasNode }) {
|
||||
return (
|
||||
<div className="image-generator-preview">
|
||||
<div className="image-generator-label">
|
||||
<ImageIcon size={14} />
|
||||
<span>{node.title}</span>
|
||||
</div>
|
||||
<div className="image-generator-frame">
|
||||
<ImageIcon size={34} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkingImagePreview({ node, label, style }: { node: CanvasNode; label: string; style?: CSSProperties }) {
|
||||
return (
|
||||
<CanvasWorkOverlay label={label} ariaLabel={node.title} style={style}>
|
||||
<img className="artboard-image" src={canvasImageUrl(node.content)} alt={node.title} draggable={false} />
|
||||
</CanvasWorkOverlay>
|
||||
);
|
||||
}
|
||||
|
||||
function GeneratingArtboardSkeleton({ title, style }: { title: string; style?: CSSProperties }) {
|
||||
return (
|
||||
<div className="generation-skeleton" aria-label={title} style={style}>
|
||||
<div className="generation-skeleton-image" />
|
||||
<div className="generation-skeleton-lines">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
<span className="generation-skeleton-label">{title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WideCommerceBoard({ node }: { node: CanvasNode }) {
|
||||
return (
|
||||
<div className="artboard-preview artboard-wide">
|
||||
<div className="artboard-nav">
|
||||
<b>still day</b>
|
||||
<span>HOME</span>
|
||||
<span>SHOP</span>
|
||||
<span>COLLECTION</span>
|
||||
<Circle size={8} />
|
||||
</div>
|
||||
<div className="wide-hero">
|
||||
<div className="hero-copyline">
|
||||
<strong>
|
||||
Wear
|
||||
<br />
|
||||
Your
|
||||
<br />
|
||||
Mood.
|
||||
</strong>
|
||||
<small>{node.title}</small>
|
||||
</div>
|
||||
<div className="hero-model" />
|
||||
<div className="hero-note">
|
||||
<b>Every color tells a story.</b>
|
||||
<span>{node.content.slice(0, 52)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TallCommerceBoard({ node }: { node: CanvasNode }) {
|
||||
return (
|
||||
<div className="artboard-preview artboard-tall">
|
||||
<WideCommerceBoard node={node} />
|
||||
<section className="mood-section">
|
||||
<header>
|
||||
<b>MOOD × COLOR</b>
|
||||
<span>drop 04</span>
|
||||
</header>
|
||||
<div className="color-cards">
|
||||
{["ENERGETIC", "HAPPY", "CALM", "BOLD"].map((label) => (
|
||||
<div key={label}>
|
||||
<i />
|
||||
<strong>{label}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="product-section">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<span key={index} />
|
||||
))}
|
||||
</section>
|
||||
<footer>
|
||||
<b>Born from every mood.</b>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SquareCommerceBoard({ node }: { node: CanvasNode }) {
|
||||
return (
|
||||
<div className="artboard-preview artboard-square">
|
||||
<div className="neutral-hero">
|
||||
<strong>Wear Your Mood.</strong>
|
||||
<span>{node.title}</span>
|
||||
</div>
|
||||
<div className="neutral-grid">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<i key={index} />
|
||||
))}
|
||||
</div>
|
||||
<footer>Born from every mood.</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user