feat(*): 用户工作台 - 统一黑白灰空间界面并整理画布入口
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Infinity as InfinityIcon, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Fragment, useEffect, useState, type ReactNode } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router";
|
||||
|
||||
import { ModelSetupGuide } from "@/components/layout/model-setup-guide";
|
||||
@@ -7,6 +7,7 @@ import { WORKSPACE_SIDEBAR_STORAGE_KEY } from "@/components/layout/workspace-sid
|
||||
import { WorkspaceSidebarFooter } from "@/components/layout/workspace-sidebar-footer";
|
||||
import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isSpatialWorkbenchPath } from "@/lib/workspace-routes";
|
||||
|
||||
export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
const { pathname } = useLocation();
|
||||
@@ -14,9 +15,11 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
const [mobileSidebarExpanded, setMobileSidebarExpanded] = useState(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem(WORKSPACE_SIDEBAR_STORAGE_KEY) === "1");
|
||||
const hideChrome = pathname.startsWith("/admin") || /^\/canvas\/[^/]+/.test(pathname);
|
||||
const spatialWorkbench = isSpatialWorkbenchPath(pathname);
|
||||
const compactSidebar = sidebarCollapsed;
|
||||
const slug = pathname.split("/").filter(Boolean)[0];
|
||||
const activeToolSlug = navigationTools.some((tool) => tool.slug === slug) ? (slug as NavigationToolSlug) : undefined;
|
||||
const visibleNavigationTools = spatialWorkbench ? navigationTools : navigationTools.filter((tool) => tool.section === "创作空间");
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(WORKSPACE_SIDEBAR_STORAGE_KEY, sidebarCollapsed ? "1" : "0");
|
||||
@@ -35,9 +38,10 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-dvh min-h-0 w-full overflow-hidden">
|
||||
<div className={cn("app-workspace-shell flex h-dvh min-h-0 w-full overflow-hidden", spatialWorkbench && "is-spatial")}>
|
||||
{spatialWorkbench && mobileSidebarExpanded ? <button type="button" className="app-workspace-sidebar-scrim lg:hidden" aria-label="收起侧栏" onClick={() => setMobileSidebarExpanded(false)} /> : null}
|
||||
{!hideChrome ? (
|
||||
<aside className={cn("app-workspace-sidebar flex shrink-0 flex-col overflow-hidden", mobileSidebarExpanded ? "w-[196px]" : "w-[52px]", compactSidebar ? "lg:w-[60px]" : "lg:w-[220px]")}>
|
||||
<aside className={cn("app-workspace-sidebar flex shrink-0 flex-col overflow-hidden", mobileSidebarExpanded ? "is-mobile-expanded w-[196px]" : "w-[52px]", compactSidebar ? "lg:w-[60px]" : "lg:w-[220px]")}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-14 shrink-0 items-center border-b border-border/55 text-foreground",
|
||||
@@ -46,8 +50,8 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
)}
|
||||
>
|
||||
<Link to="/" className={cn("min-w-0 items-center gap-2", mobileSidebarExpanded ? "flex" : "hidden", compactSidebar ? "lg:hidden" : "lg:flex")} title="影策">
|
||||
<span className="grid size-7 shrink-0 place-items-center rounded-md bg-foreground text-background"><InfinityIcon className="size-4" /></span>
|
||||
<span className="truncate text-[13px] font-semibold">影策</span>
|
||||
<span className="app-workspace-brand-mark grid size-7 shrink-0 place-items-center rounded-md bg-foreground text-background"><InfinityIcon className="size-4" /></span>
|
||||
{spatialWorkbench ? <span className="min-w-0"><span className="block truncate text-[13px] font-semibold">影策</span><span className="block truncate text-[9px] text-foreground/36">AI 叙事工作台</span></span> : <span className="truncate text-[13px] font-semibold">影策</span>}
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
@@ -65,28 +69,31 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
</div>
|
||||
|
||||
<nav className="flex min-h-0 flex-1 flex-col overflow-y-auto px-2 py-3">
|
||||
<div className={cn("mb-2 px-2 text-[10px] font-medium text-foreground/34", mobileSidebarExpanded ? "block" : "hidden", compactSidebar ? "lg:hidden" : "lg:block")}>创作空间</div>
|
||||
{navigationTools.map((tool) => {
|
||||
{visibleNavigationTools.map((tool, index) => {
|
||||
const Icon = tool.icon;
|
||||
const active = tool.slug === activeToolSlug;
|
||||
const showSection = index === 0 || tool.section !== visibleNavigationTools[index - 1]?.section;
|
||||
return (
|
||||
<Link
|
||||
key={tool.slug}
|
||||
to={`/${tool.slug}`}
|
||||
title={tool.label}
|
||||
onClick={() => {
|
||||
if (window.innerWidth < 1024) setMobileSidebarExpanded(false);
|
||||
}}
|
||||
className={cn(
|
||||
"app-workspace-nav-link relative mb-1 flex h-9 shrink-0 items-center rounded-md text-[13px] transition-colors",
|
||||
mobileSidebarExpanded ? "gap-3 px-2.5" : "justify-center px-0",
|
||||
compactSidebar ? "lg:justify-center lg:px-0" : "lg:justify-start lg:gap-3 lg:px-2.5",
|
||||
active ? "is-active font-medium" : "text-foreground/55 hover:bg-foreground/[0.045] hover:text-foreground/85",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4 shrink-0" />
|
||||
<span className={cn("truncate", mobileSidebarExpanded ? "inline" : "hidden", compactSidebar ? "lg:hidden" : "lg:inline")}>{tool.label}</span>
|
||||
</Link>
|
||||
<Fragment key={tool.slug}>
|
||||
{showSection ? <div className={cn("mb-2 px-2 text-[10px] font-medium text-foreground/34", index > 0 && "mt-4", mobileSidebarExpanded ? "block" : "hidden", compactSidebar ? "lg:hidden" : "lg:block")}>{tool.section}</div> : null}
|
||||
<Link
|
||||
to={`/${tool.slug}`}
|
||||
title={tool.label}
|
||||
onClick={() => {
|
||||
if (window.innerWidth < 1024) setMobileSidebarExpanded(false);
|
||||
}}
|
||||
className={cn(
|
||||
"app-workspace-nav-link relative mb-1 flex shrink-0 items-center rounded-md text-[13px] transition-colors",
|
||||
spatialWorkbench ? "h-11" : "h-9",
|
||||
mobileSidebarExpanded ? "gap-3 px-2.5" : "justify-center px-0",
|
||||
compactSidebar ? "lg:justify-center lg:px-0" : "lg:justify-start lg:gap-3 lg:px-2.5",
|
||||
active ? "is-active font-medium" : "text-foreground/55 hover:bg-foreground/[0.045] hover:text-foreground/85",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4 shrink-0" />
|
||||
<span className={cn("truncate", mobileSidebarExpanded ? "inline" : "hidden", compactSidebar ? "lg:hidden" : "lg:inline")}>{tool.label}</span>
|
||||
</Link>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
@@ -106,7 +113,7 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
</aside>
|
||||
) : null}
|
||||
|
||||
<div className="relative min-h-0 min-w-0 flex-1 overflow-hidden">{children}</div>
|
||||
<div className="app-workspace-stage relative min-h-0 min-w-0 flex-1 overflow-hidden">{children}</div>
|
||||
</div>
|
||||
<ModelSetupGuide hidden={pathname === "/login" || pathname === "/register" || pathname.startsWith("/admin")} />
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { App, Popover, Switch } from "antd";
|
||||
import { BookOpenCheck, ChevronRight, Coins, LogIn, LogOut, Moon, Settings2, ShieldCheck, Sun, UserRound } from "lucide-react";
|
||||
import { ChevronRight, LogIn, LogOut, Moon, ShieldCheck, Sun, UserRound } from "lucide-react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
|
||||
@@ -73,12 +73,11 @@ export function WorkspaceSidebarFooter({ expandedClassName, collapsedClassName,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="py-2" aria-label="账号与工具">
|
||||
<MenuLink to="/wallet" icon={<Coins />} label="积分与账单" onNavigate={() => setMenuOpen(false)} />
|
||||
<MenuLink to="/skills" icon={<BookOpenCheck />} label="技能库" onNavigate={() => setMenuOpen(false)} />
|
||||
<MenuLink to="/settings" icon={<Settings2 />} label="设置" onNavigate={() => setMenuOpen(false)} />
|
||||
{user.role === "admin" ? <MenuLink to="/admin" icon={<ShieldCheck />} label="管理员后台" onNavigate={() => setMenuOpen(false)} /> : null}
|
||||
</nav>
|
||||
{user.role === "admin" ? (
|
||||
<nav className="py-2" aria-label="管理工具">
|
||||
<MenuLink to="/admin" icon={<ShieldCheck />} label="管理员后台" onNavigate={() => setMenuOpen(false)} />
|
||||
</nav>
|
||||
) : null}
|
||||
|
||||
<div className="border-y border-border/65 py-2">
|
||||
<AppChangelogButton className="flex h-8 w-full items-center gap-2 rounded px-2 text-[11px] text-foreground/58 hover:bg-foreground/[.055] hover:text-foreground [&_svg]:size-3.5" showLabel showVersion versionClassName="ml-auto text-[9px] tabular-nums text-foreground/32" />
|
||||
@@ -89,7 +88,7 @@ export function WorkspaceSidebarFooter({ expandedClassName, collapsedClassName,
|
||||
<span className="ml-2 flex-1 text-xs text-foreground/65">深色模式</span>
|
||||
<Switch size="small" checked={theme === "dark"} onChange={(checked) => setTheme(checked ? "dark" : "light")} aria-label="深色模式" />
|
||||
</div>
|
||||
<button type="button" className="flex h-9 w-full items-center gap-2 rounded px-2 text-xs text-foreground/55 hover:bg-red-500/[.08] hover:text-red-600" onClick={() => void handleLogout()}><LogOut className="size-3.5" />退出登录</button>
|
||||
<button type="button" className="flex h-9 w-full items-center gap-2 rounded px-2 text-xs text-foreground/55 hover:bg-foreground/[.055] hover:text-foreground" onClick={() => void handleLogout()}><LogOut className="size-3.5" />退出登录</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
import { FolderKanban, Images, ListChecks, Maximize2 } from "lucide-react";
|
||||
import { BookOpenCheck, Coins, FolderKanban, Images, ListChecks, Maximize2, Settings2 } from "lucide-react";
|
||||
|
||||
export const navigationTools = [
|
||||
{
|
||||
slug: "projects",
|
||||
label: "短剧创作",
|
||||
icon: FolderKanban,
|
||||
section: "创作空间",
|
||||
},
|
||||
{
|
||||
slug: "canvas",
|
||||
label: "画布",
|
||||
icon: Maximize2,
|
||||
section: "创作空间",
|
||||
},
|
||||
{
|
||||
slug: "tasks",
|
||||
label: "任务",
|
||||
icon: ListChecks,
|
||||
section: "创作空间",
|
||||
},
|
||||
{
|
||||
slug: "assets",
|
||||
label: "素材",
|
||||
icon: Images,
|
||||
section: "创作空间",
|
||||
},
|
||||
{
|
||||
slug: "skills",
|
||||
label: "技能库",
|
||||
icon: BookOpenCheck,
|
||||
section: "工作台管理",
|
||||
},
|
||||
{
|
||||
slug: "wallet",
|
||||
label: "积分中心",
|
||||
icon: Coins,
|
||||
section: "工作台管理",
|
||||
},
|
||||
{
|
||||
slug: "settings",
|
||||
label: "设置",
|
||||
icon: Settings2,
|
||||
section: "工作台管理",
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
import { CanvasDeleteProjectsDialog } from "@/components/canvas/canvas-delete-projects-dialog";
|
||||
import { AppWorkspaceShell } from "@/components/layout/app-top-nav";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isSpatialWorkbenchPath } from "@/lib/workspace-routes";
|
||||
|
||||
export default function UserLayout({ children }: { children: ReactNode }) {
|
||||
const { pathname } = useLocation();
|
||||
const spatialWorkbench = isSpatialWorkbenchPath(pathname);
|
||||
return (
|
||||
<div className="app-user-workspace h-dvh overflow-hidden text-foreground">
|
||||
<div className={cn("app-user-workspace h-dvh overflow-hidden text-foreground", spatialWorkbench && "app-spatial-workspace")}>
|
||||
<AppWorkspaceShell>{children}</AppWorkspaceShell>
|
||||
<CanvasDeleteProjectsDialog />
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ export function canvasDockStyle(theme: CanvasTheme, color: string = theme.toolba
|
||||
boxShadow: `0 18px 52px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.14)`,
|
||||
"--dock-command-bg": theme.spatial.surface,
|
||||
"--dock-command-hover": theme.toolbar.itemHover,
|
||||
"--dock-command-active": theme.node.text,
|
||||
"--dock-command-active-text": theme.canvas.background,
|
||||
"--dock-command-active": theme.toolbar.activeBg,
|
||||
"--dock-command-active-text": theme.toolbar.activeText,
|
||||
"--dock-command-danger": theme.accent.danger,
|
||||
"--dock-tooltip-bg": theme.spatial.elevated,
|
||||
"--dock-tooltip-border": theme.toolbar.border,
|
||||
|
||||
5
web/src/lib/workspace-routes.ts
Normal file
5
web/src/lib/workspace-routes.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function isSpatialWorkbenchPath(pathname: string) {
|
||||
// 画布列表属于用户工作台,只有具体画布编辑器与运营后台保留各自现有视觉契约。
|
||||
const canvasEditor = /^\/canvas\/[^/]+(?:\/|$)/.test(pathname);
|
||||
return !canvasEditor && !pathname.startsWith("/admin");
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { useMemo, type CSSProperties, type ReactNode } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { App, Button } from "antd";
|
||||
import { ArrowRight, Bot, CheckCircle2, CircleAlert, Clapperboard, Clock3, FolderKanban, Images, LayoutGrid, ListChecks, Plus, Sparkles } from "lucide-react";
|
||||
import { ArrowRight, BookOpenText, Bot, CheckCircle2, CircleAlert, Clapperboard, Clock3, FolderKanban, Images, LayoutGrid, ListChecks, Plus, Sparkles } from "lucide-react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
|
||||
import { CanvasProjectCard } from "@/components/canvas/canvas-project-card";
|
||||
import { WorkspaceErrorState, WorkspaceLoadingState } from "@/components/layout/workspace-state";
|
||||
import { WorkspaceSignalIcon } from "@/components/ui/aceternity/workspace-signal-icon";
|
||||
import { projectContinueTarget, projectDetailStage, projectNextActions, projectSummaryCompletion } from "@/lib/project-workbench";
|
||||
import { projectAttentionCount, projectContinueTarget, projectDetailStage, projectNextActions, projectSummaryCompletion } from "@/lib/project-workbench";
|
||||
import { getProject, listProjects, type ProjectSummary } from "@/services/api/projects";
|
||||
import { createCanvasProjectWithRemoteSync } from "@/services/user-data-sync";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
@@ -58,7 +58,7 @@ export default function IndexPage() {
|
||||
const loadingUserWorkspace = !userHydrated || (Boolean(user) && domainProjectsQuery.isLoading);
|
||||
return (
|
||||
<main className="app-user-content app-workspace-canvas h-full overflow-y-auto text-foreground">
|
||||
<div className="mx-auto w-full max-w-[1440px] px-4 pb-12 pt-5 sm:px-6 lg:px-8">
|
||||
<div className="app-home-workbench mx-auto w-full max-w-[1440px] px-4 pb-12 pt-5 sm:px-6 lg:px-8">
|
||||
{loadingUserWorkspace ? (
|
||||
<WorkspaceLoadingState className="mt-3 max-w-[980px]" label="正在恢复工作台" detail="读取项目、章节和最近画布" rows={5} />
|
||||
) : user && domainProjectsQuery.isError ? (
|
||||
@@ -99,9 +99,10 @@ function ReturningWorkspace({ summary, detail, detailLoading, detailError, recen
|
||||
const continueTarget = detail ? projectContinueTarget(detail) : { href: `/projects/${summary.project.id}/overview`, title: summary.project.name, context: "打开项目概览", updatedAt: summary.project.updatedAt };
|
||||
const nextActions = detail ? projectNextActions(detail, 3) : [];
|
||||
const completion = projectSummaryCompletion(summary);
|
||||
const attentionCount = detail ? projectAttentionCount(detail) : 0;
|
||||
return (
|
||||
<>
|
||||
<header className="flex flex-col gap-4 border-b border-border/80 pb-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<header className="app-home-header flex flex-col gap-4 border-b border-border/80 pb-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<WorkspaceSignalIcon variant="home" />
|
||||
<div className="min-w-0">
|
||||
@@ -115,8 +116,15 @@ function ReturningWorkspace({ summary, detail, detailLoading, detailError, recen
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="grid gap-0 border-b border-border/80 py-5 lg:grid-cols-[minmax(0,1.35fr)_minmax(320px,.65fr)]">
|
||||
<div className="min-w-0 py-2 lg:pr-8">
|
||||
<section className="app-home-metrics grid grid-cols-2 gap-3 py-5 xl:grid-cols-4" aria-label="项目状态概览">
|
||||
<WorkspaceMetric icon={<BookOpenText />} label="剧情章节" value={summary.unitCount} detail={`${summary.completedUnitCount} 章已完成`} />
|
||||
<WorkspaceMetric icon={<LayoutGrid />} label="项目画布" value={summary.canvasCount} detail="已关联制作空间" />
|
||||
<WorkspaceMetric icon={<Images />} label="项目资产" value={summary.assetCount} detail="角色、场景与媒体" />
|
||||
<WorkspaceMetric icon={<CircleAlert />} label="需要处理" value={attentionCount} detail={attentionCount ? "建议优先处理" : "当前流程顺畅"} attention={attentionCount > 0} />
|
||||
</section>
|
||||
|
||||
<section className="app-home-focus-panel grid overflow-hidden rounded-lg border border-border/80 lg:grid-cols-[minmax(260px,.8fr)_minmax(320px,1fr)_minmax(280px,.72fr)]">
|
||||
<div className="min-w-0 p-5 sm:p-6 lg:p-7">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-foreground/50">
|
||||
<span className="font-medium text-[var(--workspace-accent)]">{stage.label}</span>
|
||||
<span aria-hidden>·</span>
|
||||
@@ -138,7 +146,9 @@ function ReturningWorkspace({ summary, detail, detailLoading, detailError, recen
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 border-t border-border/75 pt-5 lg:mt-0 lg:border-l lg:border-t-0 lg:pl-8 lg:pt-1">
|
||||
<SpatialChapterStack detail={detail} loading={detailLoading} projectId={summary.project.id} />
|
||||
|
||||
<div className="border-t border-border/75 p-5 sm:p-6 lg:border-l lg:border-t-0 lg:p-7">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold">下一步</h2>
|
||||
<Link to={`/projects/${summary.project.id}/overview`} className="text-xs text-foreground/48 hover:text-foreground">查看项目</Link>
|
||||
@@ -153,7 +163,7 @@ function ReturningWorkspace({ summary, detail, detailLoading, detailError, recen
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-8 py-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,.8fr)]">
|
||||
<section className="grid gap-8 py-7 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,.8fr)]">
|
||||
<div className="min-w-0">
|
||||
<div className="mb-3 flex items-center justify-between gap-4">
|
||||
<div><h2 className="text-base font-semibold">最近项目</h2><p className="mt-1 text-xs text-foreground/45">按最近更新时间排列</p></div>
|
||||
@@ -193,7 +203,7 @@ function FirstProjectWorkspace({ authenticated, canvasHydrated, recentIndependen
|
||||
const projectHref = authenticated ? "/projects?create=1" : `/login?next=${encodeURIComponent("/projects?create=1")}`;
|
||||
return (
|
||||
<>
|
||||
<section className="border-b border-border/80 pb-8 pt-3 sm:pb-10 sm:pt-6">
|
||||
<section className="app-first-project-intro border-b border-border/80 pb-8 pt-3 sm:pb-10 sm:pt-6">
|
||||
<div className="inline-flex items-center gap-2 text-xs font-semibold text-foreground/48"><WorkspaceSignalIcon variant="home" size="sm" />影策</div>
|
||||
<h1 className="mt-5 max-w-[780px] text-3xl font-semibold leading-[1.08] sm:text-4xl lg:text-5xl">把一个故事推进到可交付的镜头</h1>
|
||||
<p className="mt-5 max-w-[680px] text-sm leading-7 text-foreground/58 sm:text-base">从章节、角色和参考图开始,逐步生成分镜、视频和可复用资产。需要自由探索时,也可以先打开一张自由画布。</p>
|
||||
@@ -205,9 +215,9 @@ function FirstProjectWorkspace({ authenticated, canvasHydrated, recentIndependen
|
||||
|
||||
<section className="border-b border-border/80 py-7">
|
||||
<div className="mb-5"><h2 className="text-lg font-semibold">从故事到结果</h2><p className="mt-1 text-xs leading-5 text-foreground/48">每一步都保留输入、版本和生成记录,可以随时返回调整。</p></div>
|
||||
<div className="grid border-t border-border/75 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="app-workflow-rail grid border-t border-border/75 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{workflow.map((item, index) => (
|
||||
<div key={item.title} className={`min-w-0 border-b border-border/75 py-4 sm:px-4 xl:border-b-0 xl:border-r ${index % 2 === 0 ? "sm:pl-0" : "sm:border-l"} ${index === workflow.length - 1 ? "xl:border-r-0" : ""}`}>
|
||||
<div key={item.title} className={`app-workflow-step min-w-0 border-b border-border/75 py-4 sm:px-4 xl:border-b-0 xl:border-r ${index % 2 === 0 ? "sm:pl-0" : "sm:border-l"} ${index === workflow.length - 1 ? "xl:border-r-0" : ""}`}>
|
||||
<span className="text-[11px] font-semibold tabular-nums text-[var(--workspace-accent)]">0{index + 1}</span>
|
||||
<h3 className="mt-2 text-sm font-semibold">{item.title}</h3>
|
||||
<p className="mt-1 text-xs leading-5 text-foreground/48">{item.description}</p>
|
||||
@@ -244,11 +254,54 @@ function FirstProjectWorkspace({ authenticated, canvasHydrated, recentIndependen
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceMetric({ icon, label, value, detail, attention = false }: { icon: ReactNode; label: string; value: number; detail: string; attention?: boolean }) {
|
||||
return (
|
||||
<article className="app-home-metric min-w-0 rounded-lg border p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span className={attention ? "text-foreground/75" : "text-[var(--workspace-accent)]"}>{icon}</span>
|
||||
<span className={`size-1.5 rounded-full ${attention ? "bg-foreground/75" : "bg-[var(--workspace-accent)]"}`} />
|
||||
</div>
|
||||
<div className="mt-5 text-[11px] text-foreground/45">{label}</div>
|
||||
<div className="mt-1 text-2xl font-semibold tabular-nums">{value}</div>
|
||||
<div className="mt-1 truncate text-[10px] text-foreground/38">{detail}</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function SpatialChapterStack({ detail, loading, projectId }: { detail?: Awaited<ReturnType<typeof getProject>>; loading: boolean; projectId: string }) {
|
||||
const units = detail?.units.slice().sort((left, right) => left.position - right.position).slice(0, 6) || [];
|
||||
const activeUnitId = units.find((unit) => unit.status !== "completed")?.id || units.at(-1)?.id;
|
||||
return (
|
||||
<div className="spatial-chapter-panel min-w-0 border-t border-border/75 p-5 sm:p-6 lg:border-l lg:border-t-0 lg:p-7">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div><div className="text-[10px] font-medium text-foreground/38">制作层级</div><h2 className="mt-1 text-sm font-semibold">章节轨道</h2></div>
|
||||
<span className="text-[10px] tabular-nums text-foreground/35">{units.length ? `${units.length} 章` : "待建立"}</span>
|
||||
</div>
|
||||
<div className="spatial-chapter-deck mt-4" aria-label="项目章节制作层级">
|
||||
{loading ? Array.from({ length: 4 }, (_, index) => <span key={index} className="spatial-chapter-card is-loading" style={{ "--deck-x": `${index * 12}px`, "--deck-y": `${index * 28}px`, "--deck-z": `${-index * 18}px`, zIndex: 8 - index } as CSSProperties} />) : null}
|
||||
{!loading && units.length ? units.map((unit, index) => (
|
||||
<Link
|
||||
key={unit.id}
|
||||
to={`/projects/${projectId}/chapters/${unit.id}`}
|
||||
className={`spatial-chapter-card ${unit.id === activeUnitId ? "is-active" : ""}`}
|
||||
style={{ "--deck-x": `${index * 12}px`, "--deck-y": `${index * 28}px`, "--deck-z": `${-index * 18}px`, zIndex: 8 - index } as CSSProperties}
|
||||
>
|
||||
<span className="text-[9px] font-semibold tabular-nums opacity-55">{String(unit.position + 1).padStart(2, "0")}</span>
|
||||
<span className="mt-2 block truncate text-xs font-semibold">{unit.title}</span>
|
||||
<span className="mt-1 block text-[9px] opacity-50">{unit.status === "completed" ? "已完成" : unit.id === activeUnitId ? "当前制作" : "等待推进"}</span>
|
||||
</Link>
|
||||
)) : null}
|
||||
{!loading && !units.length ? <div className="grid min-h-52 place-items-center text-center text-xs leading-5 text-foreground/42">创建剧情章节后<br />这里会形成制作层级</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkbenchActionLink({ action }: { action: ReturnType<typeof projectNextActions>[number] }) {
|
||||
const Icon = action.tone === "danger" ? CircleAlert : action.tone === "attention" ? Clock3 : CheckCircle2;
|
||||
return (
|
||||
<Link to={action.href} className="group grid grid-cols-[20px_minmax(0,1fr)_auto] gap-2 py-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/20">
|
||||
<Icon className={`mt-0.5 size-4 ${action.tone === "danger" ? "text-red-500" : action.tone === "attention" ? "text-amber-500" : "text-foreground/35"}`} />
|
||||
<Icon className={`mt-0.5 size-4 ${action.tone === "danger" ? "text-foreground/80" : action.tone === "attention" ? "text-foreground/60" : "text-foreground/35"}`} />
|
||||
<span className="min-w-0"><span className="block text-xs font-medium">{action.title}</span><span className="mt-1 line-clamp-2 block text-[11px] leading-4 text-foreground/45">{action.description}</span></span>
|
||||
<span className="self-center text-[11px] font-medium text-foreground/45 transition-colors group-hover:text-foreground">{action.actionLabel}</span>
|
||||
</Link>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function ProjectDetailPage() {
|
||||
return (
|
||||
<WorkspacePage className="project-workbench-page !overflow-hidden" fluid>
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<header className="shrink-0 border-b border-border/65 bg-background/80 px-3 py-2 backdrop-blur-md sm:px-4 lg:px-5 lg:py-0">
|
||||
<header className="project-workbench-header shrink-0 border-b border-border/65 bg-background/80 px-3 py-2 backdrop-blur-md sm:px-4 lg:px-5 lg:py-0">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-3 lg:min-h-16 lg:flex-nowrap">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2.5 lg:w-[250px] lg:flex-none xl:w-[290px]">
|
||||
<button type="button" onClick={() => navigate("/projects")} className="grid size-9 shrink-0 place-items-center rounded-md text-foreground/42 transition-colors hover:bg-foreground/[.055] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label="返回项目" title="返回项目"><ArrowLeft className="size-4" /></button>
|
||||
|
||||
@@ -20,14 +20,14 @@ export default function ProjectOverviewView({ detail }: ProjectDetailViewProps)
|
||||
|
||||
return (
|
||||
<div className="space-y-7">
|
||||
<section className="overflow-hidden rounded-lg border border-border/75 bg-background/80 shadow-sm">
|
||||
<section className="project-overview-focus overflow-hidden rounded-lg border border-border/75 bg-background/80 shadow-sm">
|
||||
<div className="grid lg:grid-cols-[minmax(0,1fr)_300px]">
|
||||
<div className="min-w-0 p-5 sm:p-6 lg:p-7">
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px]">
|
||||
<span className="font-medium text-[var(--workspace-accent)]">当前任务</span>
|
||||
<span className="text-foreground/20" aria-hidden>/</span>
|
||||
<span className="text-foreground/45">{stage.label}</span>
|
||||
{attentionCount ? <span className="rounded bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400">{attentionCount} 项待处理</span> : null}
|
||||
{attentionCount ? <span className="rounded bg-foreground/[.07] px-1.5 py-0.5 text-[10px] font-medium text-foreground/70">{attentionCount} 项待处理</span> : null}
|
||||
</div>
|
||||
<h2 className="mt-3 max-w-[680px] text-2xl font-semibold leading-8 text-balance">{primaryAction.title}</h2>
|
||||
<p className="mt-2 max-w-[680px] text-sm leading-6 text-foreground/52 text-pretty">{primaryAction.description}</p>
|
||||
@@ -39,7 +39,7 @@ export default function ProjectOverviewView({ detail }: ProjectDetailViewProps)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="border-t border-border/70 bg-foreground/[.018] p-5 lg:border-l lg:border-t-0 lg:p-6" aria-label="项目进度">
|
||||
<aside className="project-overview-status border-t border-border/70 bg-foreground/[.018] p-5 lg:border-l lg:border-t-0 lg:p-6" aria-label="项目进度">
|
||||
<div className="flex items-end justify-between gap-3">
|
||||
<div><div className="text-[10px] font-medium text-foreground/38">章节进度</div><div className="mt-1 text-lg font-semibold tabular-nums">{completedUnits}<span className="mx-1 text-sm font-normal text-foreground/28">/</span>{units.length}</div></div>
|
||||
<span className="text-xs font-medium tabular-nums text-foreground/42">{completion}%</span>
|
||||
@@ -72,7 +72,7 @@ export default function ProjectOverviewView({ detail }: ProjectDetailViewProps)
|
||||
</div>
|
||||
|
||||
{unitStages.length ? (
|
||||
<div className="mt-4 overflow-hidden rounded-lg border border-border/75 bg-background/70">
|
||||
<div className="project-pipeline-surface mt-4 overflow-hidden rounded-lg border border-border/75 bg-background/70">
|
||||
<div className="divide-y divide-border/65">
|
||||
{unitStages.map((item) => (
|
||||
<Link key={item.unit.id} to={`/projects/${project.id}/chapters/${item.unit.id}`} className="group grid gap-4 p-4 transition-colors hover:bg-foreground/[.025] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-foreground/20 sm:p-5 lg:grid-cols-[minmax(220px,.75fr)_minmax(480px,1.35fr)_24px] lg:items-center">
|
||||
@@ -93,12 +93,12 @@ export default function ProjectOverviewView({ detail }: ProjectDetailViewProps)
|
||||
}
|
||||
|
||||
function ProjectFact({ label, value, attention = false }: { label: string; value: string; attention?: boolean }) {
|
||||
return <div className="min-w-0"><dt className="text-[10px] text-foreground/36">{label}</dt><dd className={`mt-1 truncate font-medium ${attention ? "text-amber-600 dark:text-amber-400" : "text-foreground/72"}`}>{value}</dd></div>;
|
||||
return <div className="min-w-0"><dt className="text-[10px] text-foreground/36">{label}</dt><dd className={`mt-1 truncate font-medium ${attention ? "text-foreground/85" : "text-foreground/72"}`}>{value}</dd></div>;
|
||||
}
|
||||
|
||||
function SecondaryAction({ action }: { action: ProjectWorkbenchAction }) {
|
||||
const Icon = action.tone === "danger" ? CircleAlert : action.tone === "attention" ? Clock3 : CheckCircle2;
|
||||
return <Link to={action.href} className="group flex min-w-0 items-center gap-2 rounded px-1 py-1.5 text-[11px] text-foreground/52 hover:bg-foreground/[.04] hover:text-foreground"><Icon className={`size-3.5 shrink-0 ${action.tone === "danger" ? "text-red-500" : action.tone === "attention" ? "text-amber-500" : "text-foreground/30"}`} /><span className="min-w-0 flex-1 truncate">{action.title}</span><ArrowRight className="size-3 shrink-0 text-foreground/25 transition group-hover:text-foreground/55" /></Link>;
|
||||
return <Link to={action.href} className="group flex min-w-0 items-center gap-2 rounded px-1 py-1.5 text-[11px] text-foreground/52 hover:bg-foreground/[.04] hover:text-foreground"><Icon className={`size-3.5 shrink-0 ${action.tone === "danger" ? "text-foreground/80" : action.tone === "attention" ? "text-foreground/60" : "text-foreground/30"}`} /><span className="min-w-0 flex-1 truncate">{action.title}</span><ArrowRight className="size-3 shrink-0 text-foreground/25 transition group-hover:text-foreground/55" /></Link>;
|
||||
}
|
||||
|
||||
function StagePipeline({ content, assets, storyboard, canvas }: { content: ProjectStageCell; assets: ProjectStageCell; storyboard: ProjectStageCell; canvas: ProjectStageCell }) {
|
||||
@@ -111,7 +111,7 @@ function StagePipeline({ content, assets, storyboard, canvas }: { content: Proje
|
||||
}
|
||||
|
||||
function StageStep({ label, cell }: { label: string; cell: ProjectStageCell }) {
|
||||
const bar = cell.state === "completed" ? "bg-emerald-500" : cell.state === "attention" ? "bg-amber-500" : cell.state === "active" ? "bg-[var(--workspace-accent)]" : "bg-foreground/10";
|
||||
const bar = cell.state === "completed" ? "bg-foreground/65" : cell.state === "attention" ? "bg-foreground/40" : cell.state === "active" ? "bg-[var(--workspace-accent)]" : "bg-foreground/10";
|
||||
return (
|
||||
<span className="min-w-0">
|
||||
<span className="block text-[10px] font-medium text-foreground/34">{label}</span>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function ProjectsPage() {
|
||||
title="短剧创作"
|
||||
description="按制作阶段查看故事项目,继续最近工作或处理未完成章节。"
|
||||
meta={<span className="text-xs text-foreground/45">{rows.length} 个</span>}
|
||||
actions={<><Button icon={<LayoutGrid className="size-3.5" />} onClick={() => navigate("/canvas")}>画布</Button><Button type="primary" icon={<Plus className="size-3.5" />} onClick={() => setCreateOpen(true)}>创建项目</Button></>}
|
||||
actions={<Button type="primary" icon={<Plus className="size-3.5" />} onClick={() => setCreateOpen(true)}>创建项目</Button>}
|
||||
/>
|
||||
<ListToolbar active={Boolean(keyword || status !== "all" || sort !== "updated")} onReset={() => { setKeyword(""); setStatus("all"); setSort("updated"); }}>
|
||||
<Input allowClear className="app-list-search" prefix={<Search className="size-4 text-foreground/40" />} value={keyword} placeholder="搜索项目、简介或画风" onChange={(event) => setKeyword(event.target.value)} />
|
||||
|
||||
@@ -178,6 +178,340 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.app-spatial-workspace {
|
||||
--workspace-canvas: #f5f5f5;
|
||||
--workspace-surface: rgba(255, 255, 255, 0.74);
|
||||
--workspace-surface-strong: rgba(255, 255, 255, 0.94);
|
||||
--workspace-border: rgba(17, 17, 17, 0.1);
|
||||
--workspace-border-strong: rgba(17, 17, 17, 0.2);
|
||||
--workspace-grid-line: rgba(17, 17, 17, 0.035);
|
||||
--workspace-nav: rgba(250, 250, 250, 0.86);
|
||||
--workspace-sidebar: rgba(250, 250, 250, 0.88);
|
||||
--workspace-accent: #171717;
|
||||
--workspace-accent-soft: rgba(17, 17, 17, 0.065);
|
||||
background-color: var(--workspace-canvas);
|
||||
background-image:
|
||||
linear-gradient(var(--workspace-grid-line) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--workspace-grid-line) 1px, transparent 1px);
|
||||
background-size: 34px 34px;
|
||||
}
|
||||
|
||||
.dark .app-spatial-workspace {
|
||||
--workspace-canvas: #080808;
|
||||
--workspace-surface: rgba(18, 18, 18, 0.76);
|
||||
--workspace-surface-strong: rgba(22, 22, 22, 0.94);
|
||||
--workspace-border: rgba(255, 255, 255, 0.09);
|
||||
--workspace-border-strong: rgba(255, 255, 255, 0.18);
|
||||
--workspace-grid-line: rgba(255, 255, 255, 0.026);
|
||||
--workspace-nav: rgba(10, 10, 10, 0.88);
|
||||
--workspace-sidebar: rgba(14, 14, 14, 0.9);
|
||||
--workspace-accent: #f5f5f5;
|
||||
--workspace-accent-soft: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.app-workspace-shell.is-spatial {
|
||||
position: relative;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.app-workspace-sidebar-scrim {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
inset: 0;
|
||||
border: 0;
|
||||
background: rgba(0, 0, 0, 0.42);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.app-workspace-shell.is-spatial .app-workspace-sidebar {
|
||||
border: 1px solid var(--workspace-border);
|
||||
border-radius: 8px;
|
||||
background: var(--workspace-sidebar);
|
||||
box-shadow:
|
||||
0 26px 70px rgba(0, 0, 0, 0.12),
|
||||
inset 0 1px 0 color-mix(in srgb, white 12%, transparent);
|
||||
backdrop-filter: blur(24px) saturate(125%);
|
||||
}
|
||||
|
||||
.dark .app-workspace-shell.is-spatial .app-workspace-sidebar {
|
||||
box-shadow:
|
||||
0 28px 80px rgba(0, 0, 0, 0.46),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.app-workspace-shell.is-spatial .app-workspace-brand-mark {
|
||||
border: 1px solid color-mix(in srgb, var(--workspace-accent) 36%, transparent);
|
||||
background: var(--workspace-accent);
|
||||
color: color-mix(in srgb, var(--workspace-canvas) 92%, black);
|
||||
box-shadow: 0 8px 24px color-mix(in srgb, var(--workspace-accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.app-workspace-shell.is-spatial .app-workspace-nav-link.is-active {
|
||||
background: var(--workspace-accent-soft);
|
||||
color: var(--foreground);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--workspace-accent) 13%, transparent);
|
||||
}
|
||||
|
||||
.app-workspace-shell.is-spatial .app-workspace-nav-link.is-active::before {
|
||||
left: -9px;
|
||||
width: 3px;
|
||||
height: 20px;
|
||||
box-shadow: 0 0 18px color-mix(in srgb, var(--workspace-accent) 42%, transparent);
|
||||
}
|
||||
|
||||
.app-workspace-shell.is-spatial .app-workspace-stage {
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--workspace-canvas) 92%, transparent);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-user-content {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-page-header {
|
||||
border-color: var(--workspace-border);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-page-header h1 {
|
||||
letter-spacing: 0;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-workspace-surface,
|
||||
.app-spatial-workspace .app-collection-card {
|
||||
border-color: var(--workspace-border);
|
||||
background: var(--workspace-surface);
|
||||
box-shadow:
|
||||
0 18px 50px color-mix(in srgb, var(--workspace-canvas) 54%, transparent),
|
||||
inset 0 1px 0 color-mix(in srgb, white 8%, transparent);
|
||||
backdrop-filter: blur(18px) saturate(118%);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-collection-card:hover,
|
||||
.app-spatial-workspace .app-collection-card:focus-within {
|
||||
border-color: var(--workspace-border-strong);
|
||||
background: color-mix(in srgb, var(--workspace-surface-strong) 96%, var(--workspace-accent) 4%);
|
||||
box-shadow: 0 18px 42px color-mix(in srgb, var(--workspace-accent) 7%, transparent);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-table-surface {
|
||||
border-color: var(--workspace-border);
|
||||
background: var(--workspace-surface-strong);
|
||||
box-shadow:
|
||||
0 20px 54px color-mix(in srgb, var(--workspace-canvas) 58%, transparent),
|
||||
inset 0 1px 0 color-mix(in srgb, white 7%, transparent);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-data-table .ant-table,
|
||||
.app-spatial-workspace .app-data-table .ant-table-container {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-list-search:focus-within {
|
||||
filter: drop-shadow(0 0 16px color-mix(in srgb, var(--workspace-accent) 10%, transparent));
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-pagination-bar {
|
||||
border-color: var(--workspace-border);
|
||||
background: color-mix(in srgb, var(--workspace-surface-strong) 92%, transparent);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-home-header {
|
||||
border-color: var(--workspace-border);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-home-metric {
|
||||
border-color: var(--workspace-border);
|
||||
background: var(--workspace-surface);
|
||||
box-shadow:
|
||||
0 16px 42px color-mix(in srgb, var(--workspace-canvas) 56%, transparent),
|
||||
inset 0 1px 0 color-mix(in srgb, white 8%, transparent);
|
||||
backdrop-filter: blur(18px) saturate(115%);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-home-metric svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.app-spatial-workspace .workspace-signal-icon path {
|
||||
stroke: var(--workspace-accent) !important;
|
||||
}
|
||||
|
||||
.app-spatial-workspace .workspace-signal-icon circle {
|
||||
fill: color-mix(in srgb, var(--workspace-accent) 62%, transparent) !important;
|
||||
}
|
||||
|
||||
.app-spatial-workspace .ant-switch.ant-switch-checked {
|
||||
background: var(--workspace-accent);
|
||||
}
|
||||
|
||||
.dark .app-spatial-workspace .ant-switch.ant-switch-checked .ant-switch-handle::before {
|
||||
background: #171717;
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-home-focus-panel,
|
||||
.app-spatial-workspace .project-overview-focus,
|
||||
.app-spatial-workspace .project-pipeline-surface {
|
||||
border-color: var(--workspace-border);
|
||||
background: var(--workspace-surface-strong);
|
||||
box-shadow:
|
||||
0 26px 72px color-mix(in srgb, var(--workspace-canvas) 64%, transparent),
|
||||
inset 0 1px 0 color-mix(in srgb, white 8%, transparent);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-home-focus-panel {
|
||||
min-height: 330px;
|
||||
}
|
||||
|
||||
.app-spatial-workspace .spatial-chapter-panel,
|
||||
.app-spatial-workspace .project-overview-status {
|
||||
background: color-mix(in srgb, var(--workspace-surface) 82%, var(--workspace-accent) 3%);
|
||||
}
|
||||
|
||||
.spatial-chapter-deck {
|
||||
position: relative;
|
||||
min-height: 242px;
|
||||
perspective: 900px;
|
||||
perspective-origin: 50% 34%;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.spatial-chapter-card {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 3%;
|
||||
display: block;
|
||||
width: min(82%, 290px);
|
||||
height: 92px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--workspace-border-strong);
|
||||
border-radius: 7px;
|
||||
background: color-mix(in srgb, var(--workspace-surface-strong) 95%, white 5%);
|
||||
padding: 13px 15px;
|
||||
color: color-mix(in srgb, var(--foreground) 76%, transparent);
|
||||
box-shadow:
|
||||
0 18px 28px rgba(0, 0, 0, 0.24),
|
||||
inset 0 1px 0 color-mix(in srgb, white 13%, transparent);
|
||||
transform: translate3d(var(--deck-x), var(--deck-y), var(--deck-z)) rotateX(46deg) rotateZ(-7deg);
|
||||
transform-origin: center top;
|
||||
transition:
|
||||
border-color var(--motion-state) var(--ease-aceternity),
|
||||
background-color var(--motion-state) var(--ease-aceternity),
|
||||
color var(--motion-state) var(--ease-aceternity),
|
||||
transform var(--motion-state) var(--ease-aceternity);
|
||||
}
|
||||
|
||||
.spatial-chapter-card:hover,
|
||||
.spatial-chapter-card:focus-visible {
|
||||
z-index: 20 !important;
|
||||
border-color: color-mix(in srgb, var(--workspace-accent) 58%, var(--workspace-border));
|
||||
color: var(--foreground);
|
||||
outline: none;
|
||||
transform: translate3d(calc(var(--deck-x) + 8px), calc(var(--deck-y) - 5px), 18px) rotateX(34deg) rotateZ(-5deg);
|
||||
}
|
||||
|
||||
.spatial-chapter-card.is-active {
|
||||
border-color: color-mix(in srgb, var(--workspace-accent) 74%, white 8%);
|
||||
background: var(--workspace-accent);
|
||||
color: color-mix(in srgb, var(--workspace-canvas) 90%, black);
|
||||
box-shadow:
|
||||
0 22px 50px color-mix(in srgb, var(--workspace-accent) 23%, transparent),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
.spatial-chapter-card.is-loading {
|
||||
border-color: var(--workspace-border);
|
||||
background: color-mix(in srgb, var(--foreground) 4%, var(--workspace-surface-strong));
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-workflow-rail {
|
||||
border-color: var(--workspace-border);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-workflow-step {
|
||||
border-color: var(--workspace-border);
|
||||
transition: background-color var(--motion-instant) ease-out;
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-workflow-step:hover {
|
||||
background: var(--workspace-accent-soft);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .project-workbench-header {
|
||||
border-color: var(--workspace-border);
|
||||
background: color-mix(in srgb, var(--workspace-nav) 92%, transparent);
|
||||
box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--workspace-accent) 4%, transparent);
|
||||
backdrop-filter: blur(22px) saturate(120%);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .project-pipeline-surface > div > a:hover {
|
||||
background: color-mix(in srgb, var(--workspace-accent) 5%, transparent);
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.app-workspace-shell.is-spatial {
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.app-workspace-shell.is-spatial .app-workspace-sidebar,
|
||||
.app-workspace-shell.is-spatial .app-workspace-stage {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.app-workspace-sidebar-scrim {
|
||||
left: 202px;
|
||||
}
|
||||
|
||||
.app-workspace-shell.is-spatial .app-workspace-sidebar.is-mobile-expanded {
|
||||
position: absolute;
|
||||
z-index: 40;
|
||||
inset: 6px auto 6px 6px;
|
||||
width: min(196px, calc(100vw - 58px));
|
||||
box-shadow: 22px 0 70px rgba(0, 0, 0, 0.58), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.app-spatial-workspace .app-home-focus-panel {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.spatial-chapter-deck {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
gap: 8px;
|
||||
perspective: none;
|
||||
}
|
||||
|
||||
.spatial-chapter-card {
|
||||
position: relative;
|
||||
left: auto;
|
||||
top: auto;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 68px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.spatial-chapter-card:hover,
|
||||
.spatial-chapter-card:focus-visible {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.spatial-chapter-card,
|
||||
.spatial-chapter-card:hover,
|
||||
.spatial-chapter-card:focus-visible {
|
||||
transition: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.app-user-workspace {
|
||||
background-color: var(--workspace-canvas);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user