fix(workspace): 创作工作台 - 修复线上样式层级并优化 Agent、菜单与素材密度

This commit is contained in:
ddCat
2026-09-20 05:29:52 +08:00
parent b9d60f9807
commit cfcc53a904
21 changed files with 671 additions and 110 deletions

View File

@@ -0,0 +1,49 @@
import { expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { AgentWelcome } from "@/components/canvas/canvas-agent-welcome";
import { AgentChatComposer } from "@/components/canvas/canvas-cloud-agent-chat-ui";
import { agentPermissionVisual } from "@/components/canvas/canvas-cloud-agent-settings";
import { canvasThemes } from "@/lib/canvas-theme";
const noop = () => {};
test("welcome uses the configured brand and escapes it as text", () => {
const html = renderToStaticMarkup(<AgentWelcome brandName="我的<工作室>" nodeCount={2} onChooseSkill={noop} onDraftPrompt={noop} />);
expect(html).toContain("在这里,和我的&lt;工作室&gt;让灵感,慢慢成形");
expect(html).toContain("2 个节点");
expect(html.match(/type="button"/g)).toHaveLength(3);
expect(html).not.toContain('disabled=""');
});
test("empty canvas disables only the canvas-analysis shortcut", () => {
const html = renderToStaticMarkup(<AgentWelcome brandName="影策" nodeCount={0} onChooseSkill={noop} onDraftPrompt={noop} />);
expect(html.match(/disabled=""/g)).toHaveLength(1);
expect(html).toContain("添加节点后,一起梳理创作思路");
expect(html).not.toContain("生成前由你确认");
});
test("permission modes have distinct icons, not just distinct colors", () => {
const icons = (["read_only", "request_approval", "auto"] as const).map((mode) => {
const Icon = agentPermissionVisual(mode).icon;
return renderToStaticMarkup(<Icon />);
});
expect(new Set(icons).size).toBe(3);
expect(icons[0]).toContain("lucide-lock-keyhole");
expect(icons[1]).toContain("lucide-shield-check");
expect(icons[2]).toContain("lucide-sparkles");
});
test("compact send button preserves empty-draft and sending guards", () => {
for (const [prompt, sending, disabled] of [
["", false, true],
["故事灵感", false, false],
["故事灵感", true, true],
] as const) {
const html = renderToStaticMarkup(<AgentChatComposer prompt={prompt} sending={sending} placeholder="聊聊想法" theme={canvasThemes.dark} onPromptChange={noop} onSubmit={noop} />);
const button = html.match(/<button\b[^>]*class="agent-composer-send [^"]*"[^>]*>/)?.[0];
expect(button).toBeDefined();
expect(button).toContain("size-7");
expect(button?.includes('disabled=""')).toBe(disabled);
expect(button).not.toContain("box-shadow:");
}
});

View File

@@ -0,0 +1,29 @@
import { expect, test } from "bun:test";
const html = await Bun.file(new URL("../index.html", import.meta.url)).text();
const layerOrder = "@layer theme, base, components, utilities;";
test("HTML 在任何外部样式或脚本之前固定 CSS 层顺序", () => {
const firstStyle = html.match(/<style\b[^>]*>([\s\S]*?)<\/style>/i);
expect(firstStyle).not.toBeNull();
const css = firstStyle![1].replace(/\/\*[\s\S]*?\*\//g, "").trim();
expect(css.startsWith(layerOrder)).toBe(true);
const firstLoad = html.search(/<script\b|<link\b[^>]*\brel=["'](?:stylesheet|modulepreload)["']/i);
expect(firstLoad).toBeGreaterThan(html.indexOf(layerOrder));
});
test("入口层顺序与 Tailwind 一致,不依赖插件和全局 CSS 的加载先后", async () => {
const tailwind = await Bun.file(new URL("../node_modules/tailwindcss/index.css", import.meta.url)).text();
const editor = await Bun.file(new URL("../src/lib/plugins/builtin/editor/editor-shell.css", import.meta.url)).text();
const firstStyle = html.match(/<style\b[^>]*>([\s\S]*?)<\/style>/i)![1];
expect(tailwind.trimStart().startsWith(layerOrder)).toBe(true);
expect(editor).toContain("@layer utilities {");
// CSS layers retain their first declared order, even when a later chunk repeats them.
const layersIn = (css: string) => [...new Set([...css.matchAll(/@layer\s+([^;{]+)[;{]/g)].flatMap((match) => match[1].split(",").map((name) => name.trim())))];
expect(layersIn(editor + tailwind).indexOf("utilities")).toBeLessThan(layersIn(editor + tailwind).indexOf("base"));
for (const chunks of [editor + tailwind, tailwind + editor]) {
expect(layersIn(firstStyle + chunks)).toEqual(["theme", "base", "components", "utilities"]);
}
});

View File

@@ -0,0 +1,92 @@
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { renderToStaticMarkup } from "react-dom/server";
import { ConfigProvider, Select } from "antd";
import { useContext } from "react";
import { CollectionToolbar } from "@/components/layout/collection-toolbar";
import { getWorkspaceAntThemeConfig } from "@/lib/app-theme";
import { assetGridCardMinWidth, assetGridDensityOptions, parseAssetGridDensity } from "@/pages/assets/asset-grid-density";
test("density restores all saved choices and defaults invalid preferences to standard", () => {
for (const value of [6, 8, 10] as const) {
expect(parseAssetGridDensity(String(value))).toBe(value);
expect(parseAssetGridDensity(value)).toBe(value);
}
for (const value of [null, undefined, "", "broken", "12"]) {
expect(parseAssetGridDensity(value)).toBe(8);
}
});
test("each density has a distinct usable card size at normal library widths", () => {
expect(assetGridDensityOptions.map(({ value }) => value)).toEqual([6, 8, 10]);
const widths = [6, 8, 10].map((density) => assetGridCardMinWidth[parseAssetGridDensity(density)]);
expect(widths).toEqual([272, 208, 160]);
// CSS auto-fill: n columns fit when n * minimum + (n - 1) * gap <= width.
for (const available of [720, 900, 1100]) {
const columns = widths.map((minimum) => Math.floor((available + 16) / (minimum + 16)));
expect(columns[0]).toBeLessThan(columns[1]);
expect(columns[1]).toBeLessThan(columns[2]);
}
});
test("real collection toolbar supplies filled selects without changing their labels or value", () => {
function PopupConfigProbe() {
const { select } = useContext(ConfigProvider.ConfigContext);
return <span>{JSON.stringify(select?.classNames)}</span>;
}
const html = renderToStaticMarkup(
<CollectionToolbar>
<Select aria-label="素材显示密度" value={10} options={assetGridDensityOptions} />
<PopupConfigProbe />
</CollectionToolbar>,
);
expect(html).toContain("ant-select-filled");
expect(html).toContain("素材显示密度");
expect(html).toContain("紧凑");
expect(html).toContain('role="combobox"');
expect(html).toContain("workspace-quiet-popup");
expect(html).toContain('data-input-modality="keyboard"');
});
test("collection select focus uses a thin keyboard ring, not a persistent pointer outline", () => {
const css = readFileSync(new URL("../src/styles/workspace-product.css", import.meta.url), "utf8");
const toolbar = readFileSync(new URL("../src/components/layout/collection-toolbar.tsx", import.meta.url), "utf8");
expect(css).toContain(".collection-toolbar .ant-select {\n outline: none !important;");
expect(css).toContain('[data-input-modality="keyboard"] .ant-select:not(.ant-select-disabled):focus-within');
expect(css).toContain("outline: 1px solid var(--control-focus-ring) !important;");
expect(toolbar).toContain('onPointerDownCapture={() => setInputModality("pointer")}');
expect(toolbar).toContain("onKeyDownCapture=");
expect(toolbar).toContain("event.currentTarget.contains(event.relatedTarget)");
expect(toolbar).not.toContain(".blur()");
});
test("workspace menu tokens are shadow-free and use theme-aware surfaces", () => {
const { components } = getWorkspaceAntThemeConfig();
for (const name of ["Select", "Dropdown", "Popover"] as const) {
expect(components?.[name]?.boxShadowSecondary).toBe("none");
expect(components?.[name]?.colorBgElevated).toBe("var(--user-surface-raised)");
}
expect(components?.Select?.optionSelectedBg).toBe("var(--user-surface-hover)");
});
test("real grid consumes density rather than the old fixed column calculation", () => {
const page = readFileSync(new URL("../src/pages/assets/index.tsx", import.meta.url), "utf8");
const css = readFileSync(new URL("../src/styles/workspace-product.css", import.meta.url), "utf8");
const globals = readFileSync(new URL("../src/styles/globals.css", import.meta.url), "utf8");
expect(page).toContain('"--collection-grid-min-width": `${assetGridCardMinWidth[gridDensity]}px`');
expect(css).toContain("minmax(min(100%, var(--collection-grid-min-width, 224px)), 1fr)");
expect(globals).not.toContain("--assets-grid-columns");
expect(css).not.toContain(".assets-library-page .assets-library-grid { grid-template-columns: repeat(2");
});
test("menu surfaces are explicitly scoped and old account inner overrides are removed", () => {
const css = readFileSync(new URL("../src/styles/workspace-menus.css", import.meta.url), "utf8");
const globals = readFileSync(new URL("../src/styles/globals.css", import.meta.url), "utf8");
expect(css).toContain("@layer utilities");
expect(css).toContain(".workspace-account-popover .ant-popover-container");
expect(css).toContain("border: 0 !important");
expect(css).toContain("box-shadow: none !important");
expect(css).not.toContain(".ant-modal");
expect(globals).not.toContain(".workspace-account-popover .ant-popover-inner");
expect(globals).toContain(".ant-select:has(input:focus-visible)");
});