style(web): 工作台入口 - 侧栏容量加硬盘图标,积分入口改为礼盒标

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCatandCursor committed 2026-09-18 10:30:08 +08:00
1 parent 521629c2a8
commit 1077a9106d
10 files changed
+254 -50

No files matched your search

+1 -1
View File
@@ -26,7 +26,7 @@ description: 当前项目已实现的核心工作台能力与使用入口
## 工作区账号容量
登录后,首页侧栏左下角用户头像上方显示账号文件容量进度:已用容量、剩余容量和总容量,细进度条表示已用比例。用量达到 70% 变为警告色,达到 90% 或已满变为危险色。点击进入素材库;资产页页头仍展示同一用量。收起侧栏时保留细进度条,悬停可看到已用、剩余与总容量。统计包含素材文件和 Agent 会话附件。
登录后,首页侧栏左下角用户头像上方显示账号文件容量进度:左侧硬盘图标,右侧已用容量与总容量,进度条旁显示剩余容量。用量达到 70% 变为警告色,达到 90% 或已满变为危险色。点击进入素材库;资产页页头仍展示同一用量。收起侧栏时保留图标和细进度条,悬停可看到已用、剩余与总容量。统计包含素材文件和 Agent 会话附件。
### 相关实现
+2 -2
View File
@@ -68,8 +68,8 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
登录后首页侧栏左下角用户头像上方展示账号文件容量。待登录工作台后:
- [ ] 展开侧栏时能看到已用容量、剩余容量、总容量和进度条;数字为真实配额,不是占位。
- [ ] 收起侧栏后头像上方仍有细进度条,悬停能看到剩余/总容量。
- [ ] 展开侧栏时能看到硬盘图标、已用容量、剩余容量、总容量和进度条;数字为真实配额,不是占位。
- [ ] 收起侧栏后头像上方仍有硬盘图标和细进度条,悬停能看到剩余/总容量。
- [ ] 用量到 70% 变警告色,到 90% 或已满变危险色;点进度条进入资产页,两边数字一致。
- [ ] 未登录不显示该进度;接口失败时可重试。
@@ -0,0 +1,44 @@
import { useId } from "react";
import { cn } from "@/lib/utils";
export function WorkspaceCreditGiftMark({ className }: { className?: string }) {
const uid = useId().replace(/:/g, "");
const box = `${uid}-box`;
const lid = `${uid}-lid`;
const ribbon = `${uid}-ribbon`;
return (
<span className={cn("app-workspace-credit-gift", className)} aria-hidden="true">
<svg viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id={box} x1="8" y1="14" x2="24" y2="28" gradientUnits="userSpaceOnUse">
<stop stopColor="#FFB34A" />
<stop offset="1" stopColor="#F26B12" />
</linearGradient>
<linearGradient id={lid} x1="6" y1="10" x2="26" y2="16" gradientUnits="userSpaceOnUse">
<stop stopColor="#FFC66A" />
<stop offset="1" stopColor="#FF8A28" />
</linearGradient>
<linearGradient id={ribbon} x1="16" y1="6" x2="16" y2="28" gradientUnits="userSpaceOnUse">
<stop stopColor="#5FE8D8" />
<stop offset="1" stopColor="#12B8A8" />
</linearGradient>
</defs>
<ellipse cx="16" cy="28.5" rx="9.2" ry="1.6" fill="#7A3A00" opacity=".22" />
<rect x="6.5" y="14" width="19" height="13.2" rx="2.4" fill={`url(#${box})`} />
<path d="M23.4 16.4v8.4c0 1.3-1 2.4-2.3 2.4h.1V14c1.3.2 2.2 1.2 2.2 2.4Z" fill="#7A3A00" opacity=".16" />
<rect x="14.4" y="14" width="3.2" height="13.2" fill={`url(#${ribbon})`} />
<rect x="6.5" y="19.3" width="19" height="2.8" fill={`url(#${ribbon})`} />
<rect x="5.4" y="10.3" width="21.2" height="5.5" rx="2.1" fill={`url(#${lid})`} />
<rect x="5.4" y="10.3" width="21.2" height="2.1" rx="2.1" fill="#fff" opacity=".24" />
<rect x="14.4" y="10.3" width="3.2" height="5.5" fill={`url(#${ribbon})`} />
<path d="M15.8 11.1C10.4 5.6 7.2 11.8 12.8 13.7c1.6.5 2.4 0 3-1.2Z" fill="#4FE3D4" />
<path d="M16.2 11.1C21.6 5.6 24.8 11.8 19.2 13.7c-1.6.5-2.4 0-3-1.2Z" fill="#14C4B4" />
<rect x="14.7" y="9.3" width="2.6" height="2.6" rx=".8" fill="#0E9E92" />
<path d="M14.9 12.1 11.6 16.4l3.8-3.1Z" fill="#3FDDD0" />
<path d="M17.1 12.1 20.4 16.4l-3.8-3.1Z" fill="#12B6A6" />
</svg>
</span>
);
}
@@ -1,3 +1,4 @@
import { HardDrive } from "lucide-react";
import { Link } from "react-router";
import { useAccountFileStorageUsage } from "@/hooks/use-account-file-storage-usage";
@@ -5,6 +6,14 @@ import { accountStorageMeter } from "@/lib/account-storage-usage";
import { cn } from "@/lib/utils";
import { preloadWorkspaceRoute } from "@/lib/workspace-route-modules";
function StorageGlyph() {
return (
<span className="app-workspace-sidebar-storage-icon" aria-hidden="true">
<HardDrive strokeWidth={1.75} />
</span>
);
}
export function WorkspaceSidebarStorageMeter({ collapsed }: { collapsed: boolean }) {
const query = useAccountFileStorageUsage();
const meter = accountStorageMeter(query.data);
@@ -13,13 +22,30 @@ export function WorkspaceSidebarStorageMeter({ collapsed }: { collapsed: boolean
const totalText = query.data ? `共 ${meter.totalLabel}` : "";
const summary = query.data ? `${usedText},${remainingText},${totalText}` : usedText;
const track = (
<span
className="app-workspace-sidebar-storage-track"
role="progressbar"
aria-label="账号文件容量使用进度"
aria-valuemin={0}
aria-valuemax={query.data?.totalBytes ?? 0}
aria-valuenow={query.data ? Math.min(query.data.usedBytes, query.data.totalBytes) : 0}
aria-valuetext={summary}
>
<span style={{ width: `${meter.percent}%` }} />
</span>
);
if (query.isError && !query.data) {
return (
<div className={cn("app-workspace-sidebar-storage is-error", collapsed && "is-collapsed")}>
{collapsed ? (
<button type="button" title="容量统计暂时不可用,点击重试" aria-label="重试加载账号容量" onClick={() => void query.refetch()} />
<button type="button" title="容量统计暂时不可用,点击重试" aria-label="重试加载账号容量" onClick={() => void query.refetch()}>
<StorageGlyph />
</button>
) : (
<>
<StorageGlyph />
<span>容量暂不可用</span>
<button type="button" onClick={() => void query.refetch()}>
重试
@@ -47,26 +73,23 @@ export function WorkspaceSidebarStorageMeter({ collapsed }: { collapsed: boolean
onFocus={() => preloadWorkspaceRoute("/assets")}
onPointerEnter={() => preloadWorkspaceRoute("/assets")}
>
{collapsed ? null : (
<span className="app-workspace-sidebar-storage-copy">
<span className="app-workspace-sidebar-storage-meta">
<span className="app-workspace-sidebar-storage-used">{usedText}</span>
{totalText ? <span className="app-workspace-sidebar-storage-total">{totalText}</span> : null}
<StorageGlyph />
{collapsed ? (
track
) : (
<span className="app-workspace-sidebar-storage-body">
<span className="app-workspace-sidebar-storage-copy">
<span className="app-workspace-sidebar-storage-meta">
<span className="app-workspace-sidebar-storage-used">{usedText}</span>
{totalText ? <span className="app-workspace-sidebar-storage-total">{totalText}</span> : null}
</span>
</span>
<span className="app-workspace-sidebar-storage-foot">
{track}
{remainingText ? <span className="app-workspace-sidebar-storage-remain">{remainingText}</span> : null}
</span>
{remainingText ? <span className="app-workspace-sidebar-storage-remain">{remainingText}</span> : null}
</span>
)}
<span
className="app-workspace-sidebar-storage-track"
role="progressbar"
aria-label="账号文件容量使用进度"
aria-valuemin={0}
aria-valuemax={query.data?.totalBytes ?? 0}
aria-valuenow={query.data ? Math.min(query.data.usedBytes, query.data.totalBytes) : 0}
aria-valuetext={summary}
>
<span style={{ width: `${meter.percent}%` }} />
</span>
</Link>
);
}
@@ -1,8 +1,9 @@
import { Coins, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { Link, useLocation } from "react-router";
import { SystemAnnouncementCenter } from "@/components/layout/system-announcement-center";
import { WorkspaceAccountMenu } from "@/components/layout/workspace-account-menu";
import { WorkspaceCreditGiftMark } from "@/components/layout/workspace-credit-gift-mark";
import { WorkspaceTopBarExtensionSlot } from "@/components/layout/workspace-top-bar-extension";
import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler";
import { useThemeStore } from "@/stores/use-theme-store";
@@ -48,7 +49,7 @@ export function WorkspaceTopBar({ sidebarOpen, onToggleSidebar }: { sidebarOpen:
<WorkspaceTopBarExtensionSlot />
<div className="app-workspace-topbar-actions">
{creditsEnabled ? <button type="button" className="app-workspace-topbar-credit-pill" aria-label={`打开积分中心,可用 ${balance} 积分`} onClick={() => openWorkspaceWallet()}>
<Coins aria-hidden="true" />
<WorkspaceCreditGiftMark />
<span>积分</span>
<strong>{balance}</strong>
</button> : null}
@@ -1,8 +1,9 @@
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { Link } from "react-router";
import { Clapperboard, CloudDownload, CloudUpload, Coins, CopyPlus, Focus, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Save, Search, Share2, Trash2, Undo2, Upload } from "lucide-react";
import { Clapperboard, CloudDownload, CloudUpload, CopyPlus, Focus, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Save, Search, Share2, Trash2, Undo2, Upload } from "lucide-react";
import { Button, Dropdown, Tooltip } from "antd";
import { WorkspaceCreditGiftMark } from "@/components/layout/workspace-credit-gift-mark";
import { useWalletBalance } from "@/hooks/use-wallet-balance";
import { openWorkspaceWallet } from "@/lib/workspace-wallet";
import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style";
@@ -241,7 +242,7 @@ export function CanvasTopBar({
aria-label="打开积分中心"
onClick={() => openWorkspaceWallet()}
>
{refreshing && availableMicrocredits === null ? <LoaderCircle className="size-3.5 animate-spin opacity-60" style={{ color: theme.accent.primary }} /> : <Coins className="size-3.5" style={{ color: theme.accent.primary }} />}
{refreshing && availableMicrocredits === null ? <LoaderCircle className="size-3.5 animate-spin opacity-60" style={{ color: theme.accent.primary }} /> : <WorkspaceCreditGiftMark className="is-compact" />}
<span>{availableMicrocredits === null ? "--" : (availableMicrocredits / 1_000_000).toLocaleString("zh-CN", { maximumFractionDigits: 3 })}</span>
</button>
</CanvasTopBarTooltip>
+135 -24
View File
@@ -1863,6 +1863,10 @@
background: var(--dock-command-hover) !important;
}
.canvas-topbar-action:hover:not(:disabled) .app-workspace-credit-gift {
transform: translateY(-1px) rotate(-8deg);
}
.canvas-topbar-project-cluster {
width: max-content;
max-width: min(360px, calc(50vw - 88px));
@@ -16099,11 +16103,12 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
.app-user-workspace .app-workspace-topbar-credit-pill {
display: inline-flex;
flex-shrink: 0;
overflow: visible;
white-space: nowrap;
height: 36px;
align-items: center;
gap: 7px;
padding: 0 12px;
gap: 8px;
padding: 0 12px 0 8px;
border: 1px solid rgba(0, 0, 0, .075);
border-radius: 999px;
background: #fff;
@@ -16113,15 +16118,46 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
}
.app-user-workspace .app-workspace-topbar-credit-pill:hover {
border-color: rgba(91, 78, 224, .26);
background: #fbfaff;
border-color: color-mix(in srgb, #ff8a28 34%, transparent);
background: #fffaf4;
transform: translateY(-1px);
}
.app-user-workspace .app-workspace-topbar-credit-pill svg {
width: 15px;
height: 15px;
color: #6a58e8;
.app-user-workspace .app-workspace-credit-gift {
display: grid;
width: 24px;
height: 24px;
flex: none;
place-items: center;
filter: drop-shadow(0 1px 1px rgba(92, 42, 0, .22));
transform-origin: 50% 70%;
transition: transform .18s ease;
}
.app-user-workspace .app-workspace-credit-gift svg {
width: 24px;
height: 24px;
overflow: visible;
color: unset;
}
.app-user-workspace .app-workspace-credit-gift.is-compact,
.app-user-workspace .app-workspace-credit-gift.is-compact svg {
width: 18px;
height: 18px;
}
.app-user-workspace .app-workspace-topbar-credit-pill:hover .app-workspace-credit-gift {
transform: translateY(-1px) rotate(-8deg);
}
@media (prefers-reduced-motion: reduce) {
.app-user-workspace .app-workspace-topbar-credit-pill,
.app-user-workspace .app-workspace-credit-gift,
.app-user-workspace .app-workspace-topbar-credit-pill:hover .app-workspace-credit-gift {
transform: none;
transition: none;
}
}
.app-user-workspace .app-workspace-topbar-credit-pill strong {
@@ -16268,9 +16304,9 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
.app-user-workspace .app-workspace-sidebar-storage {
display: flex;
min-width: 0;
flex-direction: column;
gap: 6px;
padding: 8px 8px 6px;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 12px;
color: inherit;
text-decoration: none;
@@ -16282,11 +16318,40 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
background: var(--user-surface-hover, rgba(72, 72, 73, .055));
}
.app-user-workspace .app-workspace-sidebar-storage:hover .app-workspace-sidebar-storage-icon,
.app-user-workspace .app-workspace-sidebar-storage:focus-visible .app-workspace-sidebar-storage-icon {
color: var(--user-ink, var(--foreground));
}
.app-user-workspace .app-workspace-sidebar-storage:focus-visible {
outline: 2px solid var(--control-focus-ring);
outline-offset: 2px;
}
.app-user-workspace .app-workspace-sidebar-storage-icon {
display: grid;
width: 32px;
height: 32px;
flex: none;
place-items: center;
border-radius: 9px;
background: color-mix(in srgb, var(--user-ink, var(--foreground)) 8%, transparent);
color: color-mix(in srgb, var(--user-ink, var(--foreground)) 78%, transparent);
}
.app-user-workspace .app-workspace-sidebar-storage-icon svg {
width: 16px;
height: 16px;
}
.app-user-workspace .app-workspace-sidebar-storage-body {
display: flex;
min-width: 0;
flex: 1 1 auto;
flex-direction: column;
gap: 6px;
}
.app-user-workspace .app-workspace-sidebar-storage-copy {
display: flex;
min-width: 0;
@@ -16294,6 +16359,13 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
gap: 2px;
}
.app-user-workspace .app-workspace-sidebar-storage-foot {
display: flex;
min-width: 0;
align-items: center;
gap: 8px;
}
.app-user-workspace .app-workspace-sidebar-storage-meta {
display: flex;
min-width: 0;
@@ -16326,12 +16398,14 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
}
.app-user-workspace .app-workspace-sidebar-storage-remain {
min-width: 0;
line-height: 1;
}
.app-user-workspace .app-workspace-sidebar-storage-track {
display: block;
height: 5px;
flex: 1 1 auto;
min-width: 0;
height: 4px;
overflow: hidden;
border-radius: 999px;
background: color-mix(in srgb, var(--user-ink, var(--foreground)) 10%, transparent);
@@ -16353,18 +16427,28 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
background: color-mix(in srgb, var(--user-ink, var(--foreground)) 8%, transparent);
}
.app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-used {
.app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-used,
.app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-icon {
color: var(--status-warning);
}
.app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-icon {
background: color-mix(in srgb, var(--status-warning) 14%, transparent);
}
.app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-track > span {
background: var(--status-warning);
}
.app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-used {
.app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-used,
.app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-icon {
color: var(--status-error);
}
.app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-icon {
background: color-mix(in srgb, var(--status-error) 14%, transparent);
}
.app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-track > span {
background: var(--progress-bar-fill-error);
}
@@ -16379,6 +16463,11 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
font-size: var(--fs-tiny);
}
.app-user-workspace .app-workspace-sidebar-storage.is-error .app-workspace-sidebar-storage-icon {
color: color-mix(in srgb, var(--status-error) 72%, transparent);
background: color-mix(in srgb, var(--status-error) 12%, transparent);
}
.app-user-workspace .app-workspace-sidebar-storage.is-error button {
color: var(--user-ink, var(--foreground));
text-decoration: underline;
@@ -16391,26 +16480,43 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
}
.app-user-workspace .app-workspace-sidebar-storage.is-collapsed {
width: 36px;
padding: 8px 0 6px;
width: 40px;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 6px 0;
margin-inline: auto;
}
.app-user-workspace .app-workspace-sidebar-storage.is-collapsed .app-workspace-sidebar-storage-track {
flex: none;
width: 32px;
}
.app-user-workspace .app-workspace-sidebar-storage.is-collapsed.is-error {
width: 36px;
width: 40px;
min-height: 0;
padding: 2px 0 4px;
padding: 2px 0;
}
.app-user-workspace .app-workspace-sidebar-storage.is-collapsed.is-error button {
display: block;
width: 36px;
height: 5px;
border-radius: 999px;
background: color-mix(in srgb, var(--status-error) 48%, transparent);
display: grid;
width: 32px;
height: 32px;
place-items: center;
border-radius: 9px;
background: color-mix(in srgb, var(--status-error) 14%, transparent);
color: var(--status-error);
text-decoration: none;
}
.app-user-workspace .app-workspace-sidebar-storage.is-collapsed.is-error .app-workspace-sidebar-storage-icon {
width: 32px;
height: 32px;
background: transparent;
color: inherit;
}
.app-user-workspace .app-workspace-sidebar-profile-row {
display: flex;
min-width: 0;
@@ -18604,6 +18710,11 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
}
.app-user-workspace .app-workspace-topbar-breadcrumb { color: var(--user-ink-muted) !important; }
.app-user-workspace .app-workspace-topbar-credit-pill { border-color: var(--user-border) !important; background: var(--user-surface) !important; color: var(--user-ink-muted) !important; }
.app-user-workspace .app-workspace-topbar-credit-pill:hover,
.app-user-workspace .app-workspace-topbar-credit-pill:focus-visible {
border-color: color-mix(in srgb, #ff8a28 36%, var(--user-border)) !important;
background: color-mix(in srgb, #ff8a28 10%, var(--user-surface)) !important;
}
.app-user-workspace .app-workspace-topbar-credit-pill strong { color: var(--user-ink) !important; }
/* One scroll owner on the creation route. */
+1 -1
View File
@@ -469,7 +469,7 @@
.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-nav-link .app-workspace-nav-main > svg { width: 18px !important; height: 18px !important; flex: 0 0 18px; }
.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-account { width: 100%; align-items: center; }
.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-checkin { width: 48px; }
.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-storage { width: 36px; }
.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-storage { width: 40px; }
.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-profile-row { width: 100%; justify-content: center; }
.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-profile { width: 48px; min-width: 48px; flex: 0 0 48px; margin-inline: auto; }
.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-notification-spacer { display: none; }
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("workspace credit gift mark", () => {
test("top-bar credit entries use a product gift instead of a lucide coin", () => {
const topBar = readFileSync(resolve(import.meta.dir, "../src/components/layout/workspace-top-bar.tsx"), "utf8");
const canvas = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/canvas-project-top-bar.tsx"), "utf8");
const mark = readFileSync(resolve(import.meta.dir, "../src/components/layout/workspace-credit-gift-mark.tsx"), "utf8");
const css = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8");
expect(topBar).toContain("<WorkspaceCreditGiftMark />");
expect(topBar).not.toContain("Coins");
expect(canvas).toContain("<WorkspaceCreditGiftMark className=\"is-compact\" />");
expect(canvas).not.toContain("Coins");
expect(mark).toContain("#FFB34A");
expect(mark).toContain("#12B8A8");
expect(css).toContain(".app-workspace-credit-gift");
expect(css).toContain("rotate(-8deg)");
});
});
@@ -55,10 +55,13 @@ describe("workspace sidebar storage meter", () => {
expect(sidebar).toContain("<WorkspaceSidebarStorageMeter collapsed={collapsed} />");
expect(sidebar.indexOf("WorkspaceSidebarStorageMeter")).toBeLessThan(sidebar.indexOf("app-workspace-sidebar-profile-row"));
expect(meter).toContain("HardDrive");
expect(meter).toContain("app-workspace-sidebar-storage-icon");
expect(meter).toContain("已用 ${meter.usedLabel}");
expect(meter).toContain("剩余 ${meter.remainingLabel}");
expect(meter).toContain("共 ${meter.totalLabel}");
expect(meter).toContain('to="/assets"');
expect(css).toContain(".app-workspace-sidebar-storage-icon");
expect(css).toMatch(/\.app-workspace-sidebar-storage-used\s*\{[^}]*font-variant-numeric:\s*tabular-nums/s);
expect(css).toContain(".app-workspace-sidebar-storage.is-warn");
expect(css).toContain(".app-workspace-sidebar-storage.is-critical");