From d3dda8998c0879abddd3a58bb6b9afac6626949a Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Sat, 15 Aug 2026 13:51:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(platform):=20=E5=B7=A5=E4=BD=9C=E5=8C=BA?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=20-=20=E9=81=BF=E5=85=8D=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=85=A8=E5=B1=8F=E5=8A=A0=E8=BD=BD=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=90=AF=E5=8A=A8=E7=8A=B6=E6=80=81=E4=B8=8E?= =?UTF-8?q?=E7=A7=AF=E5=88=86=E6=91=98=E8=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/auth/auth-session-hydrator.tsx | 3 + web/src/components/layout/app-top-nav.tsx | 7 + .../ui/aceternity/full-screen-loader.tsx | 135 +++++----- web/src/lib/workspace-route-modules.ts | 19 ++ web/src/pages/wallet/index.tsx | 40 ++- web/src/router.tsx | 27 +- web/src/styles/globals.css | 242 +++++++++++++++--- web/test/workspace-route-loading.test.ts | 57 +++++ 8 files changed, 397 insertions(+), 133 deletions(-) create mode 100644 web/src/lib/workspace-route-modules.ts create mode 100644 web/test/workspace-route-loading.test.ts diff --git a/web/src/components/auth/auth-session-hydrator.tsx b/web/src/components/auth/auth-session-hydrator.tsx index 1d570042d..148d6858d 100644 --- a/web/src/components/auth/auth-session-hydrator.tsx +++ b/web/src/components/auth/auth-session-hydrator.tsx @@ -4,6 +4,7 @@ import { useEffect } from "react"; import { applyUserSession } from "@/lib/user-session"; import { getAuthSession } from "@/services/api/auth"; import { FullScreenLoader } from "@/components/ui/aceternity/full-screen-loader"; +import { preloadWorkspaceRoute } from "@/lib/workspace-route-modules"; import { useUserStore } from "@/stores/use-user-store"; export function AuthSessionHydrator({ children }: { children: ReactNode }) { @@ -11,6 +12,8 @@ export function AuthSessionHydrator({ children }: { children: ReactNode }) { useEffect(() => { let cancelled = false; + // 登录态与当前工作区 chunk 并行恢复,避免进入应用后再出现一次页面级等待。 + preloadWorkspaceRoute(window.location.pathname); getAuthSession() .then(async (payload) => { if (!cancelled) await applyUserSession(payload); diff --git a/web/src/components/layout/app-top-nav.tsx b/web/src/components/layout/app-top-nav.tsx index f20799198..c2660f47e 100644 --- a/web/src/components/layout/app-top-nav.tsx +++ b/web/src/components/layout/app-top-nav.tsx @@ -9,6 +9,7 @@ import { navigationTools, type NavigationToolSlug } from "@/constant/navigation- import { cn } from "@/lib/utils"; import { refreshFeatureAvailability } from "@/lib/user-session"; import { isSpatialWorkbenchPath } from "@/lib/workspace-routes"; +import { preloadWorkspaceRoute } from "@/lib/workspace-route-modules"; import { useUserStore } from "@/stores/use-user-store"; export function AppWorkspaceShell({ children }: { children: ReactNode }) { @@ -60,6 +61,9 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) { to={`/${tool.slug}`} title={tool.label} onClick={handleNavClick} + onFocus={() => preloadWorkspaceRoute(tool.slug)} + onPointerDown={() => preloadWorkspaceRoute(tool.slug)} + onPointerEnter={() => preloadWorkspaceRoute(tool.slug)} className={cn( "app-workspace-nav-link app-workspace-rail-tile grid size-10 place-items-center rounded-md text-[var(--fs-tiny)] transition-colors", active ? "is-active font-medium" : "", @@ -167,6 +171,9 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) { to={`/${tool.slug}`} title={tool.label} onClick={handleNavClick} + onFocus={() => preloadWorkspaceRoute(tool.slug)} + onPointerDown={() => preloadWorkspaceRoute(tool.slug)} + onPointerEnter={() => preloadWorkspaceRoute(tool.slug)} className={cn("app-workspace-nav-link relative mb-1 flex h-11 shrink-0 items-center gap-3 rounded-md px-2.5 text-[var(--fs-body)] transition-colors", active ? "is-active font-medium" : "text-foreground/55 hover:bg-surface-hover hover:text-foreground/85")} > diff --git a/web/src/components/ui/aceternity/full-screen-loader.tsx b/web/src/components/ui/aceternity/full-screen-loader.tsx index e72c1735e..88c169b62 100644 --- a/web/src/components/ui/aceternity/full-screen-loader.tsx +++ b/web/src/components/ui/aceternity/full-screen-loader.tsx @@ -1,6 +1,5 @@ -import { motion, useReducedMotion } from "motion/react"; +import { useEffect, useState } from "react"; -import { aceternityMotion } from "@/lib/aceternity-motion"; import { cn } from "@/lib/utils"; type FullScreenLoaderProps = { @@ -9,69 +8,91 @@ type FullScreenLoaderProps = { className?: string; }; -export function FullScreenLoader({ label = "正在恢复创作空间", detail = "同步账号、模型和项目数据", className }: FullScreenLoaderProps) { - const reducedMotion = useReducedMotion(); - +export function FullScreenLoader({ label = "正在恢复工作区", detail = "同步账号、模型和项目数据", className }: FullScreenLoaderProps) { return ( - -
-
- {[0, 1, 2].map((index) => ( - - ))} -
+
+ -
-
- - - -
+
+ -

{label}

-

{detail}

-
- +
+
+
+ + {label} +
+
+ +
- +
+ ); +} + +export function WorkspaceRouteLoader({ label = "正在打开页面" }: { label?: string }) { + const [visible, setVisible] = useState(false); + + useEffect(() => { + const timer = window.setTimeout(() => setVisible(true), 140); + return () => window.clearTimeout(timer); + }, []); + + return ( +
+
+ + {label} +
+
+ ); +} + +function LoadingSignal() { + return
-
-
-
-
-
- - 可用创作积分 -
-
- {formatCredits(account?.availableMicrocredits || 0, 6)} - 积分 -
-
最近更新 {formatTime(account?.updatedAt)}
+
+
+
+
+ +
可用创作积分最近更新 {formatTime(account?.updatedAt)}
+
+
+ {formatCredits(account?.availableMicrocredits || 0, 6)} + 积分
- - - 账户正常 -
-
+
+ 账户正常 } /> } />
@@ -234,13 +228,9 @@ export default function WalletPage() { function BalanceMetric({ label, description, value, icon }: { label: string; description: string; value: number; icon: ReactNode }) { return ( -
-
-
{label}
-
{formatCredits(value, 6)}
-
{description}
-
- {icon} +
+ {icon} +
{label}{formatCredits(value, 6)}{description}
); } diff --git a/web/src/router.tsx b/web/src/router.tsx index e9555ba6f..b672be6c0 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -3,7 +3,8 @@ import { createBrowserRouter, Navigate, Outlet } from "react-router"; import { RequireAuth } from "@/components/auth/require-auth"; import { RequireFeature } from "@/components/auth/require-feature"; -import { FullScreenLoader } from "@/components/ui/aceternity/full-screen-loader"; +import { FullScreenLoader, WorkspaceRouteLoader } from "@/components/ui/aceternity/full-screen-loader"; +import { loadAssetsPage, loadCanvasPage, loadCreatePage, loadProjectsPage, loadWalletPage } from "@/lib/workspace-route-modules"; import UserLayout from "@/layouts/user-layout"; import { AuthScene } from "@/pages/auth/auth-scene"; import RouteErrorPage from "@/pages/route-error"; @@ -23,25 +24,29 @@ const DrawingEngineSettingsPage = lazy(() => import("@/pages/admin/settings/draw const StorageSettingsPage = lazy(() => import("@/pages/admin/settings/storage-settings-page")); const StoryboardPromptsPage = lazy(() => import("@/pages/admin/storyboard-prompts/storyboard-prompts-page")); const UsersPage = lazy(() => import("@/pages/admin/users/users-page")); -const AssetsPage = lazy(() => import("@/pages/assets")); +const AssetsPage = lazy(loadAssetsPage); const LoginPage = lazy(() => import("@/pages/auth/login")); const RegisterPage = lazy(() => import("@/pages/auth/register")); -const CanvasPage = lazy(() => import("@/pages/canvas")); +const CanvasPage = lazy(loadCanvasPage); const CanvasProjectPage = lazy(() => import("@/pages/canvas/project")); const SharedCanvasPage = lazy(() => import("@/pages/canvas/shared")); -const CreatePage = lazy(() => import("@/pages/create")); +const CreatePage = lazy(loadCreatePage); const HomePage = lazy(() => import("@/pages/home")); const NotFound = lazy(() => import("@/pages/not-found")); const SkillsPage = lazy(() => import("@/pages/skills")); const TasksPage = lazy(() => import("@/pages/tasks")); -const WalletPage = lazy(() => import("@/pages/wallet")); -const ProjectsPage = lazy(() => import("@/pages/projects")); +const WalletPage = lazy(loadWalletPage); +const ProjectsPage = lazy(loadProjectsPage); const ProjectDetailPage = lazy(() => import("@/pages/projects/detail")); const SettingsPage = lazy(() => import("@/pages/settings")); const TestVoiceRecording = lazy(() => import("@/pages/test-voice-recording")); function deferred(element: ReactNode) { - return }>{element}; + return }>{element}; +} + +function fullScreenDeferred(element: ReactNode) { + return }>{element}; } export const router = createBrowserRouter([ @@ -49,11 +54,11 @@ export const router = createBrowserRouter([ element: , errorElement: , children: [ - { path: "/login", element: deferred() }, - { path: "/register", element: deferred() }, + { path: "/login", element: fullScreenDeferred() }, + { path: "/register", element: fullScreenDeferred() }, ], }, - { path: "/share/canvas/:token", element: deferred(), errorElement: }, + { path: "/share/canvas/:token", element: fullScreenDeferred(), errorElement: }, { element: ( @@ -102,5 +107,5 @@ export const router = createBrowserRouter([ }, ], }, - { path: "*", element: deferred() }, + { path: "*", element: fullScreenDeferred() }, ]); diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index 09dad2995..5aa346842 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -1559,6 +1559,168 @@ border-radius: 4px; } + /* 全屏仅用于应用启动;骨架延续真实工作区结构,路由加载只占内容舞台。 */ + .full-screen-loader { + position: fixed; + inset: 0; + z-index: var(--loader-z); + min-height: 100dvh; + overflow: hidden; + background: var(--workspace-page); + color: var(--foreground); + animation: loader-surface-enter var(--motion-panel) var(--motion-ease-out) both; + } + + .full-screen-loader-shell { + display: flex; + width: 100%; + height: 100dvh; + flex-direction: column; + } + + .full-screen-loader-topbar { + display: flex; + height: var(--page-topbar-h); + flex: 0 0 var(--page-topbar-h); + align-items: center; + justify-content: space-between; + padding-inline: var(--space-5); + background: var(--workspace-navigation); + } + + .full-screen-loader-topbar-group { + display: flex; + align-items: center; + gap: var(--space-2); + } + + .loader-placeholder { + display: block; + background: color-mix(in srgb, var(--foreground) 6%, transparent); + } + + .loader-placeholder-control, + .loader-placeholder-mark, + .loader-placeholder-rail-item { + border-radius: var(--r-md); + } + + .loader-placeholder-control { width: 36px; height: 36px; } + .loader-placeholder-mark { width: 28px; height: 28px; } + .loader-placeholder-wordmark { width: 40px; height: 10px; border-radius: var(--r-full); } + .loader-placeholder-action { width: 88px; height: 32px; border-radius: var(--r-md); } + + .full-screen-loader-main { + display: flex; + min-height: 0; + flex: 1 1 auto; + } + + .full-screen-loader-rail { + display: flex; + width: 64px; + flex: 0 0 64px; + flex-direction: column; + align-items: center; + gap: var(--space-2); + padding-top: calc(var(--space-12) + var(--space-3)); + background: var(--workspace-navigation); + } + + .loader-placeholder-rail-item { width: 40px; height: 40px; } + + .full-screen-loader-stage { + min-width: 0; + flex: 1 1 auto; + overflow: hidden; + padding: var(--space-8); + background: var(--workspace-content); + } + + .full-screen-loader-stage-header { + display: flex; + min-height: var(--space-10); + align-items: center; + justify-content: space-between; + gap: var(--space-4); + } + + .full-screen-loader-status, + .workspace-route-loader-content { + display: flex; + align-items: center; + gap: var(--space-3); + color: color-mix(in srgb, var(--foreground) 54%, transparent); + font-size: var(--fs-caption); + } + + .loading-signal { + width: 20px; + height: 20px; + flex: 0 0 20px; + border: 2px solid color-mix(in srgb, var(--foreground) 12%, transparent); + border-top-color: color-mix(in srgb, var(--foreground) 72%, transparent); + border-radius: var(--r-full); + animation: loading-signal-spin 900ms linear infinite; + } + + .full-screen-loader-stage-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--space-4); + margin-top: var(--space-6); + } + + .full-screen-loader-surface { min-width: 0; } + .loader-placeholder-media { aspect-ratio: 16 / 10; border-radius: var(--r-lg); } + .loader-placeholder-title { width: 58%; height: 10px; margin-top: var(--space-3); border-radius: var(--r-full); } + .loader-placeholder-meta { width: 34%; height: 8px; margin-top: var(--space-2); border-radius: var(--r-full); opacity: 0.72; } + + .workspace-route-loader { + position: absolute; + inset: 0; + display: grid; + min-height: 100%; + place-items: center; + background: var(--workspace-page); + color: var(--foreground); + opacity: 0; + transition: opacity var(--motion-instant) ease-out; + } + + .workspace-route-loader.is-visible { opacity: 1; } + .workspace-route-loader-content { justify-content: center; } + + @keyframes loader-surface-enter { + from { opacity: 0; } + to { opacity: 1; } + } + + @keyframes loading-signal-spin { + to { transform: rotate(360deg); } + } + + @media (max-width: 1023px) { + .full-screen-loader-rail { display: none; } + .full-screen-loader-stage { padding: var(--space-6); } + .full-screen-loader-stage-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + } + + @media (max-width: 639px) { + .full-screen-loader-topbar { padding-inline: var(--space-4); } + .full-screen-loader-topbar-group:last-child .loader-placeholder-control:not(:last-child) { display: none; } + .full-screen-loader-stage { padding: var(--space-5) var(--space-4); } + .full-screen-loader-stage-grid { grid-template-columns: 1fr; } + .full-screen-loader-stage-grid .full-screen-loader-surface:nth-child(n + 3) { display: none; } + .loader-placeholder-action { width: var(--space-10); } + } + + @media (prefers-reduced-motion: reduce) { + .full-screen-loader { animation: none; } + .loading-signal { animation: none; } + .workspace-route-loader { transition: none; } + } + :where(.ant-btn) { letter-spacing: 0; transition: @@ -3405,13 +3567,6 @@ box-shadow: 0 18px 54px color-mix(in srgb, var(--workspace-canvas) 62%, transparent), inset 0 1px 0 color-mix(in srgb, white 7%, transparent); } - .credit-balance-card { - border: 0; - background: var(--btn-solid-bg); - color: var(--btn-solid-fg); - box-shadow: none; - } - .app-collection-card { position: relative; overflow: hidden; @@ -4200,10 +4355,50 @@ .skill-card-added-count { flex: 0 0 auto; color: color-mix(in srgb, var(--foreground) 36%, transparent); font-size: var(--fs-label); font-variant-numeric: tabular-nums; } .skill-card-owner-flag { flex: 1 1 auto; color: color-mix(in srgb, var(--foreground) 46%, transparent); font-size: var(--fs-caption); } + /* 积分页使用同一工作台表面,通过排版而非反色卡片表达资产层级。 */ .wallet-library-page .app-page-header { padding-bottom: 0; } .wallet-library-page .app-workspace-surface { border: 0; background: color-mix(in srgb, var(--workspace-surface-strong) 76%, transparent); box-shadow: none; } + .wallet-library-page .wallet-credit-meta { gap: 6px; } + .wallet-library-page .wallet-credit-meta svg { color: currentColor; } + .credit-balance-card { overflow: hidden; border: 0; border-radius: var(--r-lg); background: var(--library-surface); color: var(--foreground); box-shadow: none; } + .wallet-balance-inner { display: grid; min-height: 210px; grid-template-columns: minmax(0, 1.15fr) minmax(280px, .85fr); gap: 32px; padding: 24px; } + .wallet-balance-primary { display: flex; min-width: 0; flex-direction: column; justify-content: space-between; } + .wallet-balance-heading { display: flex; min-width: 0; align-items: center; gap: 12px; } + .wallet-balance-heading > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; } + .wallet-balance-heading strong { font-size: var(--fs-caption); font-weight: 600; } + .wallet-balance-heading div > span { overflow: hidden; color: color-mix(in srgb, var(--foreground) 44%, transparent); font-size: var(--fs-label); text-overflow: ellipsis; white-space: nowrap; } + .wallet-balance-icon { display: grid; width: 36px; height: 36px; flex: 0 0 auto; place-items: center; border-radius: var(--r-md); background: var(--library-icon-surface); color: color-mix(in srgb, var(--foreground) 68%, transparent); } + .wallet-balance-icon svg { width: 17px; height: 17px; } + .wallet-balance-number { display: flex; min-width: 0; align-items: baseline; gap: 8px; margin-top: 28px; } + .wallet-balance-number strong { min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-display); font-variant-numeric: tabular-nums; font-weight: 620; line-height: 1; } + .wallet-balance-number > span { flex: 0 0 auto; color: color-mix(in srgb, var(--foreground) 48%, transparent); font-size: var(--fs-caption); } + .wallet-balance-details { display: grid; min-width: 0; grid-template-columns: repeat(2, minmax(0, 1fr)); align-content: end; gap: 20px; } + .wallet-account-status { display: inline-flex; grid-column: 1 / -1; align-items: center; justify-self: end; gap: 6px; color: color-mix(in srgb, var(--foreground) 58%, transparent); font-size: var(--fs-label); font-weight: 560; } + .wallet-account-status svg { width: 15px; height: 15px; } + .wallet-balance-metric { display: grid; min-width: 0; grid-template-columns: 28px minmax(0, 1fr); align-items: start; gap: 9px; } + .wallet-balance-metric-icon { display: grid; width: 28px; height: 28px; place-items: center; border-radius: var(--r-sm); background: color-mix(in srgb, var(--foreground) 5%, transparent); color: color-mix(in srgb, var(--foreground) 42%, transparent); } + .wallet-balance-metric-icon svg { width: 14px; height: 14px; } + .wallet-balance-metric > div { display: flex; min-width: 0; flex-direction: column; } + .wallet-balance-metric span { color: color-mix(in srgb, var(--foreground) 50%, transparent); font-size: var(--fs-tiny); } + .wallet-balance-metric strong { margin-top: 3px; overflow: hidden; font-size: var(--fs-heading); font-variant-numeric: tabular-nums; font-weight: 620; text-overflow: ellipsis; white-space: nowrap; } + .wallet-balance-metric small { margin-top: 2px; overflow: hidden; color: color-mix(in srgb, var(--foreground) 35%, transparent); font-size: var(--fs-tiny); text-overflow: ellipsis; white-space: nowrap; } + .wallet-library-page .wallet-redeem-icon { background: color-mix(in srgb, var(--foreground) 6%, transparent); color: color-mix(in srgb, var(--foreground) 72%, transparent); } .wallet-library-page .wallet-ledger-panel { margin-top: 28px; } .wallet-library-page .wallet-ledger-panel .app-table-surface { border: 0; box-shadow: none; } + .wallet-library-page .wallet-ledger-table .ant-table-thead > tr > th { height: 38px; padding: 8px 12px !important; background: color-mix(in srgb, var(--foreground) 4%, var(--background)) !important; color: color-mix(in srgb, var(--foreground) 52%, transparent) !important; font-size: var(--fs-label); font-weight: 500; } + .wallet-library-page .wallet-ledger-table .ant-table-tbody > tr > td { padding: 10px 12px !important; transition: background-color var(--motion-instant) ease-out; } + .wallet-library-page .wallet-ledger-table .ant-table-tbody > tr:hover > td { background: color-mix(in srgb, var(--foreground) 2.5%, var(--background)) !important; } + @media (max-width: 1120px) { + .wallet-balance-inner { grid-template-columns: minmax(0, 1fr); gap: 24px; } + .wallet-balance-details { align-content: start; } + .wallet-account-status { grid-row: 2; grid-column: 1 / -1; justify-self: start; } + } + @media (max-width: 520px) { + .wallet-balance-inner { min-height: 0; gap: 22px; padding: 20px; } + .wallet-balance-number { margin-top: 32px; } + .wallet-balance-details { grid-template-columns: minmax(0, 1fr); gap: 16px; } + .wallet-account-status { grid-row: auto; grid-column: auto; } + } .library-loading-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 26px 18px; margin-top: 20px; } /* 画布骨架屏 - 使用新token */ @@ -10068,39 +10263,6 @@ html:not(.dark) .creation-home .storyboard-workbench-bar-action:hover { backgrou .assets-cover.is-light .assets-cover-more:hover { background: color-mix(in srgb, var(--foreground) 9%, transparent); color: var(--foreground); } .assets-cover.is-light .assets-cover-play::before { border-color: color-mix(in srgb, var(--foreground) 30%, transparent); background: color-mix(in srgb, var(--surface-card) 78%, transparent); } -/* ===== 积分中心页重构 ===== */ - -/* 1. 页头 · 统一 studio band(全局 .studio-band 提供) */ -.wallet-library-page .wallet-credit-meta { gap: 6px; } -.wallet-library-page .wallet-credit-meta svg { color: currentColor; } - -/* 2. 余额卡与其他工作台卡片一样不使用装饰边线和侧条。 */ -.wallet-library-page .wallet-balance-inner { padding-left: 0; } -.wallet-library-page .wallet-balance-number { letter-spacing: 0; } - -/* 3. 兑换面板 · 去琥珀图标块 */ -.wallet-library-page .wallet-redeem-icon { - background: color-mix(in srgb, var(--foreground) 6%, transparent); - color: color-mix(in srgb, var(--foreground) 72%, transparent); -} - -/* 4. 流水表 · 表头收敛 + 行 hover 节奏 */ -.wallet-library-page .wallet-ledger-table .ant-table-thead > tr > th { - height: 38px; - padding: 8px 12px !important; - background: color-mix(in srgb, var(--foreground) 4%, var(--background)) !important; - color: color-mix(in srgb, var(--foreground) 52%, transparent) !important; - font-size: 11px; - font-weight: 500; -} -.wallet-library-page .wallet-ledger-table .ant-table-tbody > tr > td { - padding: 10px 12px !important; - transition: background-color var(--motion-instant) ease-out; -} -.wallet-library-page .wallet-ledger-table .ant-table-tbody > tr:hover > td { - background: color-mix(in srgb, var(--foreground) 2.5%, var(--background)) !important; -} - /* ===== AI Markdown ===== */ .ai-message-markdown { diff --git a/web/test/workspace-route-loading.test.ts b/web/test/workspace-route-loading.test.ts new file mode 100644 index 000000000..126bf345f --- /dev/null +++ b/web/test/workspace-route-loading.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +function source(path: string) { + return readFileSync(resolve(import.meta.dir, path), "utf8"); +} + +describe("workspace route loading", () => { + test("keeps lazy workspace routes inside the workspace stage", () => { + const router = source("../src/router.tsx"); + const deferred = router.slice(router.indexOf("function deferred"), router.indexOf("function fullScreenDeferred")); + + expect(deferred).toContain(""); + expect(deferred).not.toContain("FullScreenLoader"); + expect(router).toContain("fullScreenDeferred()"); + expect(router).toContain("fullScreenDeferred()"); + }); + + test("preloads the reported workspace routes before navigation", () => { + const modules = source("../src/lib/workspace-route-modules.ts"); + const navigation = source("../src/components/layout/app-top-nav.tsx"); + + for (const route of ["projects", "canvas", "assets", "wallet", "create"]) { + expect(modules).toContain(`${route}: () => import`); + } + expect(navigation).toContain("onPointerEnter={() => preloadWorkspaceRoute(tool.slug)}"); + expect(navigation).toContain("onFocus={() => preloadWorkspaceRoute(tool.slug)}"); + }); + + test("uses a quiet workspace skeleton for initial hydration", () => { + const loader = source("../src/components/ui/aceternity/full-screen-loader.tsx"); + const css = source("../src/styles/globals.css"); + + expect(loader).toContain("full-screen-loader-topbar"); + expect(loader).toContain("full-screen-loader-rail"); + expect(loader).toContain("LoadingSignal"); + expect(loader).not.toContain("YINGCE STUDIO"); + expect(loader).not.toContain("loading-cue"); + expect(css).toContain("@keyframes loading-signal-spin"); + expect(css).toContain("@media (prefers-reduced-motion: reduce)"); + expect(css).not.toContain("@keyframes loading-cue-pulse"); + }); +}); + +describe("wallet balance summary", () => { + test("uses the workspace surface instead of an inverted primary button surface", () => { + const css = source("../src/styles/globals.css"); + const rule = css.match(/\.credit-balance-card \{[^}]+}/)?.[0] || ""; + + expect(rule).toContain("background: var(--library-surface)"); + expect(rule).toContain("color: var(--foreground)"); + expect(rule).not.toContain("--btn-solid-bg"); + expect(css.match(/\.wallet-balance-inner \{/g)).toHaveLength(3); + expect(css).not.toContain(".wallet-library-page .wallet-balance-inner { padding-left: 0; }"); + }); +});