feat(admin): 站点外观 - 支持 Tab 分类与主题渐变按钮配置
This commit is contained in:
41
web/test/app-theme.test.ts
Normal file
41
web/test/app-theme.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { getAntThemeConfig, getWorkspaceAntThemeConfig } from "../src/lib/app-theme";
|
||||
import { DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition } from "../src/lib/skin-themes";
|
||||
import { getIsolatedAdminAntTheme } from "../src/pages/admin/theme/admin-ant-theme";
|
||||
|
||||
describe("shared action colors and focus feedback", () => {
|
||||
for (const dark of [false, true]) {
|
||||
test(`classic solid tokens remain monochrome beneath the button fill adapter in ${dark ? "dark" : "light"} mode`, () => {
|
||||
const product = getAntThemeConfig(dark, DEFAULT_CLASSIC_SKIN);
|
||||
const admin = getIsolatedAdminAntTheme(dark, DEFAULT_CLASSIC_SKIN);
|
||||
expect(product.components?.Button?.colorPrimary).toBe(dark ? "#f5f5f5" : "#171717");
|
||||
expect(admin.components?.Button?.colorPrimary).toBe(product.components?.Button?.colorPrimary);
|
||||
expect(admin.components?.Button?.primaryColor).toBe(product.components?.Button?.primaryColor);
|
||||
});
|
||||
|
||||
test(`custom primary action states reach product and admin in ${dark ? "dark" : "light"} mode`, () => {
|
||||
const skin = duplicateSkinDefinition(DEFAULT_CLASSIC_SKIN, ["classic"]);
|
||||
Object.assign(skin.tokens[dark ? "dark" : "light"], {
|
||||
primary: "#123456", primaryHover: "#234567", primaryActive: "#345678", primaryForeground: "#abcdef",
|
||||
});
|
||||
for (const theme of [getAntThemeConfig(dark, skin), getIsolatedAdminAntTheme(dark, skin)]) {
|
||||
expect(theme.components?.Button).toMatchObject({
|
||||
colorPrimary: "#123456", colorPrimaryHover: "#234567", colorPrimaryActive: "#345678", primaryColor: "#abcdef",
|
||||
});
|
||||
expect(theme.token?.controlOutlineWidth).toBe(0);
|
||||
expect(theme.components?.Input?.activeShadow).toBe("none");
|
||||
expect(theme.components?.InputNumber?.activeShadow).toBe("none");
|
||||
expect(theme.components?.DatePicker?.activeShadow).toBe("none");
|
||||
expect(theme.components?.Select?.activeOutlineColor).toBe("transparent");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("workspace density overrides inherit action colors and shadow-free focus", () => {
|
||||
const theme = getWorkspaceAntThemeConfig();
|
||||
expect(theme.components?.Button?.colorPrimary).toBeUndefined();
|
||||
expect(theme.components?.Button?.primaryColor).toBeUndefined();
|
||||
expect(theme.components?.Input?.activeShadow).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { applySkinTheme, DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition, getSkinA
|
||||
import { normalizePublicAppearance } from "../src/stores/use-appearance-store";
|
||||
|
||||
describe("site appearance and editable skin library", () => {
|
||||
test("classic is immutable and keeps the existing runtime token system unchanged", () => {
|
||||
test("classic preserves foundation tokens and applies only primary button fills", () => {
|
||||
expect(DEFAULT_CLASSIC_SKIN.locked).toBe(true);
|
||||
expect(getSkinAntOverrides(DEFAULT_CLASSIC_SKIN, "light")).toEqual({});
|
||||
expect(getSkinAntOverrides(DEFAULT_CLASSIC_SKIN, "dark")).toEqual({});
|
||||
@@ -22,7 +22,9 @@ describe("site appearance and editable skin library", () => {
|
||||
} as unknown as Document;
|
||||
applySkinTheme(DEFAULT_CLASSIC_SKIN, "light", target);
|
||||
expect(removed.length).toBeGreaterThan(60);
|
||||
expect(assigned.size).toBe(0);
|
||||
expect(assigned.size).toBe(4);
|
||||
expect(assigned.get("--button-primary-bg")).toBe("linear-gradient(115deg, #6554df, #386fbc)");
|
||||
expect(assigned.has("--background")).toBe(false);
|
||||
expect(target.documentElement.dataset.skin).toBe("classic");
|
||||
});
|
||||
|
||||
@@ -142,7 +144,7 @@ describe("site appearance and editable skin library", () => {
|
||||
expect(storeSource).toContain('setMeta(targetDocument, "property", "og:title"');
|
||||
expect(footerSource).toContain("https://beian.miit.gov.cn/");
|
||||
expect(footerSource).toContain('rel="noopener noreferrer"');
|
||||
expect(pageSource).toContain('title="5. 皮肤主题"');
|
||||
expect(pageSource).toContain('title="皮肤主题"');
|
||||
expect(editorSource).toContain("从默认新建");
|
||||
expect(editorSource).toContain("复制当前");
|
||||
expect(editorSource).toContain("删除这套主题");
|
||||
|
||||
55
web/test/skin-button-fill.test.ts
Normal file
55
web/test/skin-button-fill.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { applySkinTheme, DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition, getSkinButtonAppearance, isSkinButtonFill, normalizeSkinDefinition } from "../src/lib/skin-themes";
|
||||
|
||||
describe("theme primary button fills", () => {
|
||||
test("classic uses the reference gradient in both modes without changing selection colors", () => {
|
||||
for (const mode of ["light", "dark"] as const) {
|
||||
expect(getSkinButtonAppearance(DEFAULT_CLASSIC_SKIN, mode)).toEqual({
|
||||
background: "linear-gradient(115deg, #6554df, #386fbc)",
|
||||
hover: "linear-gradient(115deg, #5744cf, #356bbb)",
|
||||
active: "linear-gradient(115deg, #4938b8, #2c5da5)", foreground: "#ffffff",
|
||||
});
|
||||
}
|
||||
expect(DEFAULT_CLASSIC_SKIN.tokens.light.selected).toBe("#e8e8e8");
|
||||
});
|
||||
|
||||
test("copies have independent fills and switching to solid clears every gradient state", () => {
|
||||
const skin = duplicateSkinDefinition(DEFAULT_CLASSIC_SKIN, ["classic"]);
|
||||
skin.tokens.buttons.light.angle = 45;
|
||||
skin.tokens.buttons.light.start = "#123456";
|
||||
expect(DEFAULT_CLASSIC_SKIN.tokens.buttons.light.angle).toBe(115);
|
||||
expect(skin.tokens.buttons.dark.angle).toBe(115);
|
||||
expect(getSkinButtonAppearance(skin, "light").background).toBe("linear-gradient(45deg, #123456, #386fbc)");
|
||||
const values = new Map<string, string>();
|
||||
const doc = { documentElement: { dataset: {}, style: {
|
||||
removeProperty: (key: string) => values.delete(key), setProperty: (key: string, value: string) => values.set(key, value),
|
||||
} } } as unknown as Document;
|
||||
applySkinTheme(skin, "light", doc);
|
||||
skin.tokens.buttons.light.mode = "solid";
|
||||
Object.assign(skin.tokens.light, { primary: "#123456", primaryHover: "#234567", primaryActive: "#345678", primaryForeground: "#ffffff" });
|
||||
applySkinTheme(skin, "light", doc);
|
||||
expect(values.get("--button-primary-bg")).toBe("#123456");
|
||||
expect(values.get("--button-primary-hover-bg")).toBe("#234567");
|
||||
expect(values.get("--button-primary-active-bg")).toBe("#345678");
|
||||
applySkinTheme(DEFAULT_CLASSIC_SKIN, "dark", doc);
|
||||
expect(values.has("--background")).toBe(false);
|
||||
expect(values.get("--button-primary-fg")).toBe("#ffffff");
|
||||
});
|
||||
|
||||
test("legacy custom themes retain solid actions; legacy classic gains its default gradient", () => {
|
||||
for (const original of [DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition(DEFAULT_CLASSIC_SKIN, ["classic"])]) {
|
||||
const legacy = JSON.parse(JSON.stringify(original));
|
||||
delete legacy.tokens.buttons;
|
||||
expect(normalizeSkinDefinition(legacy).tokens.buttons.light.mode).toBe(original.id === "classic" ? "gradient" : "solid");
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid modes, angles and CSS injection cannot become runtime gradients", () => {
|
||||
for (const patch of [{ mode: "url(x)" }, { angle: 361 }, { angle: -1 }, { angle: 1.5 }, { angle: NaN }, { start: "red; background:url(x)" }, { foreground: "white" }]) {
|
||||
const skin = duplicateSkinDefinition(DEFAULT_CLASSIC_SKIN, ["classic"]);
|
||||
Object.assign(skin.tokens.buttons.light, patch);
|
||||
expect(isSkinButtonFill(skin.tokens.buttons.light)).toBe(false);
|
||||
expect(getSkinButtonAppearance(normalizeSkinDefinition(skin), "light").background).toBe(skin.tokens.light.primary);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user