From 71a1b3ace4d3ec761f08fda1de681d6a2dea793c Mon Sep 17 00:00:00 2001 From: liangxu Date: Fri, 10 Jul 2026 22:19:22 +0800 Subject: [PATCH] feat(frontend): add canvas agent empty state with server-driven starters Render a CanvasAgentEmptyState in the Agent panel that fetches localized starter content from the new agent-starters endpoint, with local defaults as fallback. Clicking a starter focuses the prompt composer (new focus handle). Includes accompanying UI polish: side-nav cleanup, animated prompt examples, and canvas panel spacing tweaks. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/domain/design.ts | 13 ++ frontend/src/i18n/locales/en-US.json | 8 + frontend/src/i18n/locales/zh-CN.json | 10 +- frontend/src/infrastructure/designGateway.ts | 2 + .../infrastructure/designGateway/uiContent.ts | 12 ++ .../CanvasAgentEmptyState/defaults.ts | 55 +++++++ .../CanvasAgentEmptyState/index.css | 154 ++++++++++++++++++ .../CanvasAgentEmptyState/index.tsx | 49 ++++++ .../src/ui/components/CanvasPanels/index.tsx | 2 +- .../ui/components/PromptComposer/index.tsx | 7 + frontend/src/ui/pages/BrandKitPage/index.css | 16 +- .../src/ui/pages/CanvasWorkspace/index.tsx | 60 +++++-- frontend/src/ui/pages/HomePage/index.tsx | 8 +- frontend/src/ui/pages/HomeShell/index.css | 11 +- frontend/src/ui/pages/HomeShell/index.tsx | 8 +- frontend/src/ui/styles.css | 134 +++++++++++---- 16 files changed, 472 insertions(+), 77 deletions(-) create mode 100644 frontend/src/infrastructure/designGateway/uiContent.ts create mode 100644 frontend/src/ui/components/CanvasAgentEmptyState/defaults.ts create mode 100644 frontend/src/ui/components/CanvasAgentEmptyState/index.css create mode 100644 frontend/src/ui/components/CanvasAgentEmptyState/index.tsx diff --git a/frontend/src/domain/design.ts b/frontend/src/domain/design.ts index a94b9d0..1a28ea0 100644 --- a/frontend/src/domain/design.ts +++ b/frontend/src/domain/design.ts @@ -147,6 +147,19 @@ export type QuickAction = { icon: string; }; +export type AgentStarter = { + id: string; + label: string; + prompt: string; +}; + +export type AgentStarterConfig = { + title: string; + description: string; + previewImages: string[]; + starters: AgentStarter[]; +}; + export type InspirationItem = { id: string; title: string; diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index ce14cd1..c4f2959 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -452,6 +452,14 @@ "agentDesign": "Agent Design", "agentShort": "Agent", "agentDesignHint": "Use web search and image models to advance this canvas.", + "agentEmptyDescription": "Enter a prompt to begin, or use one of the starting points below.", + "agentEmptyStarterLabel": "Creative starting points", + "agentEmptyTitle": "What would you like to create today?", + "agentStarterBrandVisual": "Brand visual", + "agentStarterEcommerce": "E-commerce product", + "agentStarterLogo": "Logo design", + "agentStarterPoster": "Poster", + "agentStarterSocialMedia": "Social media", "agentWebSearch": "Web research", "agentWebSearchHint": "Search the web and summarize usable design evidence.", "referenceLibrary": "Reference library", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 113743f..23e2d17 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -346,7 +346,7 @@ "deleteConversation": "删除对话", "noConversationHistory": "暂无历史对话", "copyProject": "复制项目", - "canvasUntitled": "Untitled", + "canvasUntitled": "未命名设计项目", "renameCanvasTitle": "重命名画布", "renameImageNode": "重命名图片", "imageTitlePlaceholder": "输入图片名称", @@ -452,6 +452,14 @@ "agentDesign": "Agent 设计", "agentShort": "Agent", "agentDesignHint": "使用联网搜索和图片模型推进当前画布", + "agentEmptyDescription": "输入提示词开始——或使用下方的起点", + "agentEmptyStarterLabel": "创作起点", + "agentEmptyTitle": "今天想创作什么?", + "agentStarterBrandVisual": "品牌视觉", + "agentStarterEcommerce": "电商商品图", + "agentStarterLogo": "Logo 设计", + "agentStarterPoster": "海报", + "agentStarterSocialMedia": "社交媒体", "agentWebSearch": "联网研究", "agentWebSearchHint": "搜索网页信息并整理可用设计依据", "referenceLibrary": "参考库", diff --git a/frontend/src/infrastructure/designGateway.ts b/frontend/src/infrastructure/designGateway.ts index b9f3460..b923ab9 100644 --- a/frontend/src/infrastructure/designGateway.ts +++ b/frontend/src/infrastructure/designGateway.ts @@ -5,9 +5,11 @@ import { eventOperations } from "./designGateway/events"; import { generationOperations } from "./designGateway/generation"; import { projectOperations } from "./designGateway/projects"; import { sharingOperations } from "./designGateway/sharing"; +import { uiContentOperations } from "./designGateway/uiContent"; export const designGateway = { dashboard: projectOperations.dashboard, + getAgentStarterConfig: uiContentOperations.getAgentStarterConfig, listProjects: projectOperations.listProjects, createProject: projectOperations.createProject, createProjectAsync: projectOperations.createProjectAsync, diff --git a/frontend/src/infrastructure/designGateway/uiContent.ts b/frontend/src/infrastructure/designGateway/uiContent.ts new file mode 100644 index 0000000..a5989ee --- /dev/null +++ b/frontend/src/infrastructure/designGateway/uiContent.ts @@ -0,0 +1,12 @@ +import type { AgentStarterConfig } from "@/domain/design"; + +import { request } from "./request"; + +function getAgentStarterConfig(locale: string) { + const params = new URLSearchParams({ locale }); + return request(`/api/ui/agent-starters?${params.toString()}`); +} + +export const uiContentOperations = { + getAgentStarterConfig +}; diff --git a/frontend/src/ui/components/CanvasAgentEmptyState/defaults.ts b/frontend/src/ui/components/CanvasAgentEmptyState/defaults.ts new file mode 100644 index 0000000..a3ffe8f --- /dev/null +++ b/frontend/src/ui/components/CanvasAgentEmptyState/defaults.ts @@ -0,0 +1,55 @@ +import type { AgentStarterConfig } from "@/domain/design"; + +type Translate = (key: string) => string; + +const starterPrompts = { + ecommerce: "Create a premium ecommerce product image for a portable espresso machine, with realistic lighting, clean composition, benefit callouts, and marketplace-ready visual hierarchy", + poster: "Design a bold launch poster for a new skincare product line, with strong typography, product focus, campaign headline, date, and a polished premium layout", + brandVisual: "Create a brand identity direction for a modern wellness drink brand, including color palette, typography mood, logo usage, packaging accents, and social-ready visual elements", + socialMedia: "Create a square Instagram promotion for a weekend fashion sale, with product cutouts, discount headline, brand colors, and a layout that works as a paid social ad", + logo: "Design a clean vector logo for a direct-to-consumer home goods brand named Loom & Line, with a simple mark, balanced wordmark, and clear black-and-white usage" +} as const; + +export function defaultAgentStarterConfig(t: Translate): AgentStarterConfig { + return { + title: t("agentEmptyTitle"), + description: t("agentEmptyDescription"), + previewImages: [ + "https://static.codia.ai/public/images/f788bdcc/lg.webp", + "https://static.codia.ai/public/images/ec25067a/lg.webp", + "https://static.codia.ai/public/images/d4ccbe39/lg.webp" + ], + starters: [ + { id: "ecommerce", label: t("agentStarterEcommerce"), prompt: starterPrompts.ecommerce }, + { id: "poster", label: t("agentStarterPoster"), prompt: starterPrompts.poster }, + { id: "brand-visual", label: t("agentStarterBrandVisual"), prompt: starterPrompts.brandVisual }, + { id: "social-media", label: t("agentStarterSocialMedia"), prompt: starterPrompts.socialMedia }, + { id: "logo", label: t("agentStarterLogo"), prompt: starterPrompts.logo } + ] + }; +} + +export function normalizeAgentStarterConfig(config: AgentStarterConfig, fallback: AgentStarterConfig): AgentStarterConfig { + const previewImages = Array.isArray(config.previewImages) ? config.previewImages.map((image) => image.trim()).filter(Boolean).slice(0, 3) : []; + const seen = new Set(); + const starters = Array.isArray(config.starters) + ? config.starters + .map((starter) => ({ + id: starter.id.trim(), + label: starter.label.trim(), + prompt: starter.prompt.trim() + })) + .filter((starter) => { + if (!starter.id || !starter.label || !starter.prompt || seen.has(starter.id)) return false; + seen.add(starter.id); + return true; + }) + : []; + + return { + title: config.title?.trim() || fallback.title, + description: config.description?.trim() || fallback.description, + previewImages: previewImages.length > 0 ? previewImages : fallback.previewImages, + starters: starters.length > 0 ? starters : fallback.starters + }; +} diff --git a/frontend/src/ui/components/CanvasAgentEmptyState/index.css b/frontend/src/ui/components/CanvasAgentEmptyState/index.css new file mode 100644 index 0000000..47abdf9 --- /dev/null +++ b/frontend/src/ui/components/CanvasAgentEmptyState/index.css @@ -0,0 +1,154 @@ +.canvas-agent-empty-state { + width: 100%; + min-height: 100%; + flex: 1 0 auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 28px 20px 36px; + text-align: center; +} + +.canvas-agent-empty-visual { + height: 130px; + display: flex; + align-items: flex-end; + justify-content: center; + margin-bottom: 30px; +} + +.canvas-agent-empty-image { + position: relative; + width: 80px; + height: 105px; + display: block; + flex: 0 0 auto; + overflow: hidden; + border: 1px solid rgba(20, 20, 20, 0.08); + border-radius: 12px; + background: #f2f2f0; + box-shadow: 0 12px 26px rgba(20, 20, 20, 0.14); +} + +.canvas-agent-empty-image.left { + z-index: 1; + transform: rotate(-8deg); + transform-origin: right bottom; +} + +.canvas-agent-empty-image.center { + z-index: 2; + width: 100px; + height: 130px; + margin-inline: -8px; +} + +.canvas-agent-empty-image:only-child.center { + margin-inline: 0; +} + +.canvas-agent-empty-image.right { + z-index: 1; + transform: rotate(6deg); + transform-origin: left bottom; +} + +.canvas-agent-empty-image img { + width: 100%; + height: 100%; + display: block; + object-fit: cover; +} + +.canvas-agent-empty-state h2 { + margin: 0 0 8px; + color: #202124; + font-size: 20px; + font-weight: 650; + line-height: 1.4; +} + +.canvas-agent-empty-state p { + margin: 0 0 28px; + color: #96999f; + font-size: 14px; + font-weight: 400; + line-height: 1.6; +} + +.canvas-agent-starters { + width: 100%; + max-width: 390px; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 12px; +} + +.canvas-agent-starters button { + min-height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 9px 20px; + border: 1px solid #e1e3e6; + border-radius: 999px; + color: #27282b; + background: #fff; + font: inherit; + font-size: 14px; + font-weight: 500; + line-height: 1.5; + white-space: nowrap; + box-shadow: 0 1px 2px rgba(20, 20, 20, 0.02); + transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; +} + +.canvas-agent-starters button:hover { + border-color: #d5d8dc; + background: #f7f7f6; + transform: translateY(-1px); +} + +.canvas-agent-starters button:focus-visible { + outline: 2px solid #202124; + outline-offset: 2px; +} + +.canvas-agent-starters button:active { + transform: translateY(0); +} + +.canvas-agent-starters button:disabled { + color: #a6a9ae; + background: #f8f8f7; + transform: none; + cursor: default; +} + +@media (max-width: 480px) { + .canvas-agent-empty-state { + padding-inline: 12px; + } + + .canvas-agent-empty-visual { + margin-bottom: 24px; + } + + .canvas-agent-empty-state p { + margin-bottom: 22px; + } + + .canvas-agent-starters { + max-width: 340px; + gap: 8px; + } + + .canvas-agent-starters button { + min-height: 38px; + padding: 7px 16px; + font-size: 13px; + } +} diff --git a/frontend/src/ui/components/CanvasAgentEmptyState/index.tsx b/frontend/src/ui/components/CanvasAgentEmptyState/index.tsx new file mode 100644 index 0000000..fa386ec --- /dev/null +++ b/frontend/src/ui/components/CanvasAgentEmptyState/index.tsx @@ -0,0 +1,49 @@ +"use client"; + +import type { AgentStarterConfig } from "@/domain/design"; +import { useI18n } from "@/i18n/i18n"; +import "./index.css"; + +type CanvasAgentEmptyStateProps = { + config: AgentStarterConfig; + disabled?: boolean; + onSelectPrompt: (prompt: string) => void; +}; + +export function CanvasAgentEmptyState({ config, disabled = false, onSelectPrompt }: CanvasAgentEmptyStateProps) { + const { t } = useI18n(); + const previewImages = config.previewImages.slice(0, 3); + + return ( +
+ +

{config.title}

+

{config.description}

+
+ {config.starters.map((starter) => ( + + ))} +
+
+ ); +} + +function previewImagePosition(index: number, count: number) { + if (count === 1) return "center"; + if (count === 2) return index === 0 ? "left" : "right"; + return ["left", "center", "right"][index] ?? "center"; +} diff --git a/frontend/src/ui/components/CanvasPanels/index.tsx b/frontend/src/ui/components/CanvasPanels/index.tsx index d5b5a9b..6bfc05c 100644 --- a/frontend/src/ui/components/CanvasPanels/index.tsx +++ b/frontend/src/ui/components/CanvasPanels/index.tsx @@ -107,7 +107,7 @@ export function CanvasGeneratedFilesPanel({ onDownload(node); }} > - + ))} diff --git a/frontend/src/ui/components/PromptComposer/index.tsx b/frontend/src/ui/components/PromptComposer/index.tsx index 9355c7e..a1d2d5f 100644 --- a/frontend/src/ui/components/PromptComposer/index.tsx +++ b/frontend/src/ui/components/PromptComposer/index.tsx @@ -59,6 +59,7 @@ type PromptPart = export type PromptComposerHandle = { insertReference: (reference: UploadedReferenceImage) => void; upsertReference: (reference: UploadedReferenceImage, options?: InsertReferenceOptions) => void; + focus: () => void; isFocused: () => boolean; insertAnnotation: (annotation: CanvasAnnotation) => void; serialize: (t: Translate) => string; @@ -833,6 +834,12 @@ export const PromptComposer = forwardRef< upsertReference(reference, options) { upsertReferenceIntoEditor(reference, options); }, + focus() { + const editor = editorRef.current; + if (!editor) return; + editor.focus(); + placeCaretAtEnd(editor); + }, isFocused() { const editor = editorRef.current; return Boolean(editor && document.activeElement && editor.contains(document.activeElement)); diff --git a/frontend/src/ui/pages/BrandKitPage/index.css b/frontend/src/ui/pages/BrandKitPage/index.css index 760757e..d8f3244 100644 --- a/frontend/src/ui/pages/BrandKitPage/index.css +++ b/frontend/src/ui/pages/BrandKitPage/index.css @@ -649,16 +649,16 @@ } .brand-kit-section-nav button { - min-height: 32px; + min-height: 40px; display: flex; align-items: center; - gap: 9px; + gap: 10px; padding: 0 8px; border-radius: 6px; - color: #676d77; + color: #5f6570; background: transparent; - font-size: 11px; - font-weight: 650; + font-size: 16px; + font-weight: 680; text-align: left; } @@ -668,9 +668,9 @@ } .brand-kit-section-nav button span { - width: 19px; - color: #a1a6ae; - font-size: 9px; + width: 22px; + color: #9aa0aa; + font-size: 14px; font-variant-numeric: tabular-nums; } diff --git a/frontend/src/ui/pages/CanvasWorkspace/index.tsx b/frontend/src/ui/pages/CanvasWorkspace/index.tsx index 4a7e41b..54ad83a 100644 --- a/frontend/src/ui/pages/CanvasWorkspace/index.tsx +++ b/frontend/src/ui/pages/CanvasWorkspace/index.tsx @@ -6,7 +6,7 @@ import { openBrandKit, openHome } from "@/application/route"; import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import type { BrandKit } from "@/domain/brandKit"; -import type { AgentContent, AgentMessage, AgentThreadSummary, CanvasNode, CanvasViewport, ExtractedTextLayer, ExtractNodeTextResponse, MockupModel, MockupModelMap, MockupPlacement, MockupSurface, NodeActionRequest, Project, ShareAccess } from "@/domain/design"; +import type { AgentContent, AgentMessage, AgentStarterConfig, AgentThreadSummary, CanvasNode, CanvasViewport, ExtractedTextLayer, ExtractNodeTextResponse, MockupModel, MockupModelMap, MockupPlacement, MockupSurface, NodeActionRequest, Project, ShareAccess } from "@/domain/design"; import { useI18n, type Locale } from "@/i18n/i18n"; import { brandKitRepository, brandKitStorageScope } from "@/infrastructure/brandKitRepository"; import { designGateway } from "@/infrastructure/designGateway"; @@ -18,6 +18,8 @@ import { BrandKitSelector } from "@/ui/components/BrandKitSelector"; import { CanvasAnnotationPreview } from "@/ui/components/CanvasAnnotationPreview"; import { CanvasAnnotations } from "@/ui/components/CanvasAnnotations"; import { CanvasAgentComposer, composerImageSize, defaultComposerImageSettings, type ComposerImageSettings, type ComposerMode } from "@/ui/components/CanvasAgentComposer"; +import { CanvasAgentEmptyState } from "@/ui/components/CanvasAgentEmptyState"; +import { defaultAgentStarterConfig, normalizeAgentStarterConfig } from "@/ui/components/CanvasAgentEmptyState/defaults"; import { CanvasAgentTimeline } from "@/ui/components/CanvasAgentTimeline"; import { CanvasGeneratedFilesPanel, CanvasGrid, CanvasLayerPanel } from "@/ui/components/CanvasPanels"; import { CanvasMinimap } from "@/ui/components/CanvasMinimap"; @@ -85,8 +87,7 @@ const minCanvasZoom = 0.1; const maxCanvasZoom = 2.8; const defaultCanvasBackgroundColor = "#F7F7F4"; const canvasBackgroundSwatches = ["#F5F5F5", "#000000", "#FFFFFF", "#00F016", "#A855F7", "#E9D5FF"]; -const generatedFilesPanelWidth = 408; -const layersPanelWidth = 274; +const canvasSidePanelWidth = 268; const newAgentConversationId = "__new_agent_conversation__"; const canvasWheelGuardSelector = [ ".ui-popover-content", @@ -575,6 +576,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess const { selectedId, selectedIds, setSelectedIds, selectedNodes, selectedNode, selectOnlyNode, selectNodes, toggleNodeSelection } = useCanvasSelection(nodes); const [messages, setMessages] = useState([]); const [prompt, setPrompt] = useState(""); + const [agentStarterConfig, setAgentStarterConfig] = useState(() => defaultAgentStarterConfig(t)); const [agentThreads, setAgentThreads] = useState([]); const [credits, setCredits] = useState(30); const [assistantPanelOpen, setAssistantPanelOpen] = useState(canViewChat); @@ -703,6 +705,21 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess .catch(() => undefined); }, [isAuthenticated]); + useEffect(() => { + let active = true; + const fallback = defaultAgentStarterConfig(t); + setAgentStarterConfig(fallback); + designGateway + .getAgentStarterConfig(locale) + .then((config) => { + if (active) setAgentStarterConfig(normalizeAgentStarterConfig(config, fallback)); + }) + .catch(() => undefined); + return () => { + active = false; + }; + }, [locale, t]); + useEffect(() => { let active = true; if (!isAuthenticated || !canManageBrandKit) { @@ -1083,7 +1100,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess height: selectedTextNode.height * viewport.k }; }, [selectedTextNode, viewport]); - const floatingToolbarLeftInset = generatedFilesPanelOpen ? generatedFilesPanelWidth : layersPanelOpen ? layersPanelWidth : 0; + const floatingToolbarLeftInset = generatedFilesPanelOpen || layersPanelOpen ? canvasSidePanelWidth : 0; const insertCanvasImageReference = useCallback((node: CanvasNode, options: { focus?: boolean; pending?: boolean; replacePending?: boolean } = {}) => { const reference = canvasNodeToReferenceImage(node); if (!reference) return; @@ -2640,6 +2657,13 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess activeAgentThreadIdRef.current = null; }; + const selectAgentStarterPrompt = (nextPrompt: string) => { + if (!canEdit) return; + setComposerMode("agent"); + setPrompt(nextPrompt); + window.requestAnimationFrame(() => agentComposerRef.current?.focus()); + }; + const selectAgentThread = (threadId: string) => { autoScrollAgentFeedRef.current = false; selectedAgentThreadIdRef.current = threadId; @@ -3477,6 +3501,8 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess ); } + const displayProjectTitle = localizedCanvasTitle(project.title, t("canvasUntitled")); + return (
@@ -3512,9 +3538,9 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess onDrop={canEdit ? handleCanvasDrop : undefined} > -
+
- {agentHeaderTitle(messages, project, t("canvasUntitled"))} + {agentHeaderTitle(messages, project, displayProjectTitle)}
- + {!hasCurrentConversation && !agentTimelineActive ? ( + + ) : ( + + )}
{showAssistantScrollButton && ( @@ -5616,11 +5646,21 @@ function agentThreadTitleText(content: string) { return visiblePromptText(content); } -function agentHeaderTitle(messages: AgentMessage[], project: Project, fallback: string) { +function agentHeaderTitle(messages: AgentMessage[], project: Project, displayProjectTitle: string) { const firstUserMessage = messages.find((message) => message.role === "user"); const firstUserText = firstUserMessage ? visiblePromptText(firstUserMessage.content) : ""; if (firstUserText) return firstUserText; - return visiblePromptText(project.brief) || project.title.trim() || fallback; + const brief = visiblePromptText(project.brief); + return brief && !isDefaultCanvasTitle(brief) ? brief : displayProjectTitle; +} + +function localizedCanvasTitle(title: string, fallback: string) { + const trimmedTitle = title.trim(); + return !trimmedTitle || isDefaultCanvasTitle(trimmedTitle) ? fallback : trimmedTitle; +} + +function isDefaultCanvasTitle(title: string) { + return ["Untitled", "Untitled design project", "未命名设计项目"].includes(title.trim()); } function isProjectGenerating(project: Project) { diff --git a/frontend/src/ui/pages/HomePage/index.tsx b/frontend/src/ui/pages/HomePage/index.tsx index a2a203b..e16d5f3 100644 --- a/frontend/src/ui/pages/HomePage/index.tsx +++ b/frontend/src/ui/pages/HomePage/index.tsx @@ -942,10 +942,10 @@ export function HomePage() { })}
-
- {activeMode.label} - {activeMode.examples.map((example) => ( - ))} diff --git a/frontend/src/ui/pages/HomeShell/index.css b/frontend/src/ui/pages/HomeShell/index.css index b21a731..0fe9a57 100644 --- a/frontend/src/ui/pages/HomeShell/index.css +++ b/frontend/src/ui/pages/HomeShell/index.css @@ -249,8 +249,7 @@ transform: translateY(-50%); } -.side-nav nav, -.settings-fab { +.side-nav nav { display: grid; gap: 18px; justify-items: center; @@ -287,11 +286,6 @@ box-shadow: 0 18px 32px rgba(0, 0, 0, 0.18); } -.settings-fab { - padding: 0; - height: 58px; -} - .section-header { display: flex; align-items: end; @@ -354,9 +348,6 @@ height: 52px !important; } - .side-nav .settings-fab { - display: none; - } } @media (max-width: 640px) { diff --git a/frontend/src/ui/pages/HomeShell/index.tsx b/frontend/src/ui/pages/HomeShell/index.tsx index cbfc130..e4058bd 100644 --- a/frontend/src/ui/pages/HomeShell/index.tsx +++ b/frontend/src/ui/pages/HomeShell/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { ArrowRight, BookOpen, Folder, Globe2, Home, Info, LogOut, MessageCircle, Plus, Settings, User, Zap } from "lucide-react"; +import { ArrowRight, BookOpen, Folder, Globe2, Home, LogOut, MessageCircle, Plus, User, Zap } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useI18n } from "@/i18n/i18n"; import { useAuth } from "@/ui/auth/AuthProvider"; @@ -195,13 +195,7 @@ export function SideNav({ - - ); } diff --git a/frontend/src/ui/styles.css b/frontend/src/ui/styles.css index 78fbfc4..1bc9854 100644 --- a/frontend/src/ui/styles.css +++ b/frontend/src/ui/styles.css @@ -29,6 +29,7 @@ --blue-soft: #dbeafe; --black: #050505; --assistant-panel-width: 600px; + --canvas-side-panel-width: 268px; --shadow-sm: 0 2px 8px rgba(17, 24, 39, 0.06); --shadow-md: 0 12px 30px rgba(17, 24, 39, 0.08); } @@ -495,6 +496,20 @@ button:disabled { border-radius: 999px; font-size: 12px; font-weight: 800; + animation: prompt-example-enter 360ms cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: calc(var(--prompt-example-index, 0) * 45ms); +} + +@keyframes prompt-example-enter { + from { + opacity: 0; + transform: translate3d(0, 9px, 0) scale(0.97); + } + + to { + opacity: 1; + transform: translate3d(0, 0, 0) scale(1); + } } .prompt-examples strong { @@ -1287,7 +1302,7 @@ button:disabled { position: absolute; top: 0; left: 8px; - z-index: 45; + z-index: 43; height: 48px; display: flex; align-items: center; @@ -1295,6 +1310,14 @@ button:disabled { max-width: calc(100% - 220px); min-width: 0; pointer-events: auto; + transform: translate3d(0, 0, 0); + transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1), max-width 260ms cubic-bezier(0.22, 1, 0.36, 1); + will-change: transform; +} + +.workspace-stage-title.is-offset-by-canvas-panel { + max-width: calc(100% - var(--canvas-side-panel-width) - 220px); + transform: translate3d(var(--canvas-side-panel-width), 0, 0); } .canvas-account-controls { @@ -2524,7 +2547,7 @@ button:disabled { } .canvas-layer-panel { - --layer-panel-width: 274px; + --layer-panel-width: var(--canvas-side-panel-width); position: absolute; z-index: 44; left: 0; @@ -2825,7 +2848,7 @@ button:disabled { } .canvas-generated-files-panel { - --generated-files-panel-width: 408px; + --generated-files-panel-width: var(--canvas-side-panel-width); position: absolute; z-index: 44; left: 0; @@ -2841,6 +2864,22 @@ button:disabled { font-size: 14px; } +.canvas-layer-panel, +.canvas-generated-files-panel { + animation: canvas-side-panel-enter 260ms cubic-bezier(0.22, 1, 0.36, 1) both; + will-change: transform; +} + +@keyframes canvas-side-panel-enter { + from { + transform: translate3d(-100%, 0, 0); + } + + to { + transform: translate3d(0, 0, 0); + } +} + .canvas-generated-files-panel header { display: flex; align-items: center; @@ -2872,38 +2911,41 @@ button:disabled { .generated-files-list { min-height: 0; - display: grid; - align-content: start; - gap: 10px; - padding: 2px 14px 22px; + display: flex; + flex-direction: column; + gap: 4px; + padding: 2px 8px 22px; overflow-y: auto; } .generated-file-row { - min-height: 74px; + min-height: 56px; display: grid; - grid-template-columns: 72px minmax(0, 1fr) 32px; + grid-template-columns: 48px minmax(0, 1fr) 16px; align-items: center; - gap: 18px; - padding: 4px 0; - border-radius: 10px; - color: #262b33; + gap: 12px; + padding: 4px 8px; + border-radius: 8px; + color: #363636; cursor: pointer; } -.generated-file-row:hover, +.generated-file-row:hover { + background: rgba(12, 12, 13, 0.04); +} + .generated-file-row.selected { - background: #f4f6f8; + background: rgba(12, 12, 13, 0.06); } .generated-file-thumb { - width: 72px; - height: 72px; + width: 48px; + height: 48px; display: block; overflow: hidden; - border-radius: 10px; + border-radius: 8px; background: #f2f3f5; - box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.06); + box-shadow: 0 2px 10px rgba(1, 1, 1, 0.1); } .generated-file-thumb img { @@ -2916,27 +2958,28 @@ button:disabled { .generated-file-name { min-width: 0; overflow: hidden; - color: #2c3138; + color: #363636; font-size: 14px; - font-weight: 520; + font-weight: 400; + line-height: 22px; letter-spacing: 0; text-overflow: ellipsis; white-space: nowrap; } .generated-file-download { - width: 32px; - height: 32px; + width: 16px; + height: 16px; display: grid; place-items: center; - border-radius: 9px; - color: #535a64; + border-radius: 4px; + color: #363636; background: transparent; } .generated-file-download:hover { - color: #111827; - background: #eceff3; + color: #111; + background: transparent; } .generated-files-empty { @@ -2955,10 +2998,11 @@ button:disabled { } .generated-files-end { - padding: 16px 0 0; - color: #a4a9b1; + padding: 8px 0; + color: #86909c; font-size: 14px; - font-weight: 560; + font-weight: 400; + line-height: 20px; text-align: center; } @@ -2971,11 +3015,11 @@ button:disabled { } .canvas-bottom-left.is-offset-by-files-panel { - left: 424px; + left: calc(var(--canvas-side-panel-width) + 16px); } .canvas-bottom-left.is-offset-by-layers-panel { - left: 290px; + left: calc(var(--canvas-side-panel-width) + 16px); } .canvas-statusbar, @@ -3084,11 +3128,11 @@ button:disabled { } .workspace-toolbar.is-offset-by-files-panel { - left: calc(408px + (100% - 408px) / 2); + left: calc(var(--canvas-side-panel-width) + (100% - var(--canvas-side-panel-width)) / 2); } .workspace-toolbar.is-offset-by-layers-panel { - left: calc(274px + (100% - 274px) / 2); + left: calc(var(--canvas-side-panel-width) + (100% - var(--canvas-side-panel-width)) / 2); } .workspace-toolbar button { @@ -3152,11 +3196,11 @@ button:disabled { } .pen-tool-controls.is-offset-by-files-panel { - left: calc(408px + (100% - 408px) / 2); + left: calc(var(--canvas-side-panel-width) + (100% - var(--canvas-side-panel-width)) / 2); } .pen-tool-controls.is-offset-by-layers-panel { - left: calc(274px + (100% - 274px) / 2); + left: calc(var(--canvas-side-panel-width) + (100% - var(--canvas-side-panel-width)) / 2); } .pen-color-trigger { @@ -3950,6 +3994,11 @@ button:disabled { min-width: 0; } + .workspace-stage-title.is-offset-by-canvas-panel { + max-width: none; + transform: none; + } + .workspace-stage-title .brand-kit-selector-trigger.is-compact { max-width: 104px; } @@ -4088,6 +4137,19 @@ button:disabled { } } +@media (prefers-reduced-motion: reduce) { + .workspace-stage-title { + transition: none; + } + + .canvas-layer-panel, + .canvas-generated-files-panel, + .prompt-examples strong, + .prompt-examples button { + animation: none; + } +} + @media (max-width: 480px) { .workspace-countdown div { display: none;