perf(webui): keep settings-only appearance defaults out of startup
This commit is contained in:
@@ -12,6 +12,7 @@ const settings = readFileSync(resolve(webuiRoot, "src/features/settings/Settings
|
||||
const paletteControl = readFileSync(resolve(webuiRoot, "src/components/AppearancePaletteControl.tsx"), "utf8");
|
||||
const overridesEditor = readFileSync(resolve(webuiRoot, "src/components/AppearanceOverridesEditor.tsx"), "utf8");
|
||||
const overridesRuntime = readFileSync(resolve(webuiRoot, "src/components/appearanceOverrides.ts"), "utf8");
|
||||
const overridesDefaults = readFileSync(resolve(webuiRoot, "src/components/appearanceOverrideDefaults.ts"), "utf8");
|
||||
|
||||
assert.match(tokens, /:root\[data-theme="dark"\]/, "dark token overrides are required");
|
||||
assert.match(tokens, /color-scheme:\s*dark/, "native controls must receive the dark color scheme");
|
||||
@@ -30,14 +31,24 @@ assert.match(app, /applyAppearanceOverrides/, "the shell must apply validated ov
|
||||
assert.match(app, /import \{ applyAppearanceOverrides \} from "\.\/components\/appearanceOverrides"/, "startup applies themes synchronously without importing settings editor controls");
|
||||
assert.doesNotMatch(overridesRuntime, /import .*from ["']react["']|\.tsx|ColorPickerField|ContentGrid/, "the startup theme runtime must stay independent of editor UI");
|
||||
assert.match(overridesEditor, /from "\.\/appearanceOverrides"/, "editor and runtime share the same validation implementation");
|
||||
assert.match(overridesRuntime, /schema_version:\s*"1"/, "override exchange must use an explicit versioned schema");
|
||||
assert.match(overridesEditor, /from "\.\/appearanceOverrideDefaults"/, "settings owns the unchanged default document");
|
||||
assert.doesNotMatch(overridesRuntime, /appearanceOverrideDefaults|DEFAULT_APPEARANCE_OVERRIDES|cloneDefaultAppearanceOverrides/, "startup must not import settings-only default documents");
|
||||
assert.match(overridesDefaults, /schema_version:\s*"1"/, "override exchange must use an explicit versioned schema");
|
||||
assert.match(overridesRuntime, /contrastRatio[\s\S]*?<\s*4\.5/, "custom pairs must enforce WCAG AA contrast");
|
||||
assert.match(overridesRuntime, /rgbDistance[\s\S]*?<\s*12/, "custom status colors must enforce differentiation");
|
||||
const runtimeExports = {};
|
||||
const defaultsExports = {};
|
||||
vm.runInNewContext(ts.transpileModule(overridesRuntime, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
|
||||
}).outputText, { exports: runtimeExports });
|
||||
const validOverrides = runtimeExports.cloneDefaultAppearanceOverrides();
|
||||
vm.runInNewContext(ts.transpileModule(overridesDefaults, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
|
||||
}).outputText, { exports: defaultsExports });
|
||||
const validOverrides = defaultsExports.cloneDefaultAppearanceOverrides();
|
||||
assert.deepEqual(validOverrides, defaultsExports.DEFAULT_APPEARANCE_OVERRIDES);
|
||||
assert.notEqual(validOverrides, defaultsExports.DEFAULT_APPEARANCE_OVERRIDES);
|
||||
assert.notEqual(validOverrides.light, defaultsExports.DEFAULT_APPEARANCE_OVERRIDES.light);
|
||||
assert.notEqual(validOverrides.dark, defaultsExports.DEFAULT_APPEARANCE_OVERRIDES.dark);
|
||||
assert.equal(runtimeExports.validateAppearanceOverrides(validOverrides), validOverrides);
|
||||
const colorProperties = new Map();
|
||||
const root = { style: {
|
||||
@@ -49,8 +60,10 @@ assert.equal(colorProperties.get("--accent"), validOverrides.dark.accent);
|
||||
assert.equal(colorProperties.get("--danger-text"), validOverrides.dark.danger_foreground);
|
||||
runtimeExports.applyAppearanceOverrides(root, validOverrides, "light");
|
||||
assert.equal(colorProperties.get("--accent"), validOverrides.light.accent);
|
||||
const invalidOverrides = runtimeExports.cloneDefaultAppearanceOverrides();
|
||||
const invalidOverrides = defaultsExports.cloneDefaultAppearanceOverrides();
|
||||
invalidOverrides.light.accent_foreground = invalidOverrides.light.accent;
|
||||
assert.equal(defaultsExports.DEFAULT_APPEARANCE_OVERRIDES.light.accent_foreground, "#ffffff", "draft edits must not mutate shared defaults");
|
||||
assert.equal(validOverrides.light.accent_foreground, "#ffffff", "independent override drafts must not share nested modes");
|
||||
assert.throws(() => runtimeExports.validateAppearanceOverrides(invalidOverrides));
|
||||
runtimeExports.applyAppearanceOverrides(root, invalidOverrides, "dark");
|
||||
assert.equal(colorProperties.size, 0, "invalid documents clear previous custom tokens and never partially apply");
|
||||
|
||||
@@ -12,8 +12,10 @@ import DismissibleAlert from "./DismissibleAlert";
|
||||
import FormField from "./FormField";
|
||||
import SegmentedControl from "./SegmentedControl";
|
||||
|
||||
import { APPEARANCE_OVERRIDE_TOKENS, STATUS_TOKENS, cloneDefaultAppearanceOverrides, validateAppearanceOverrides } from "./appearanceOverrides";
|
||||
export { APPEARANCE_OVERRIDE_TOKENS, DEFAULT_APPEARANCE_OVERRIDES, applyAppearanceOverrides, cloneDefaultAppearanceOverrides, validateAppearanceOverrides } from "./appearanceOverrides";
|
||||
import { APPEARANCE_OVERRIDE_TOKENS, STATUS_TOKENS, validateAppearanceOverrides } from "./appearanceOverrides";
|
||||
import { cloneDefaultAppearanceOverrides } from "./appearanceOverrideDefaults";
|
||||
export { APPEARANCE_OVERRIDE_TOKENS, applyAppearanceOverrides, validateAppearanceOverrides } from "./appearanceOverrides";
|
||||
export { DEFAULT_APPEARANCE_OVERRIDES, cloneDefaultAppearanceOverrides } from "./appearanceOverrideDefaults";
|
||||
|
||||
const TOKEN_LABELS: Record<AppearanceOverrideToken, string> = {
|
||||
accent: "i18n:govoplan-core.override_accent",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/** Settings-editor defaults; the startup runtime only applies saved documents. */
|
||||
import type { AppearanceOverridesDocument } from "../types";
|
||||
|
||||
export const DEFAULT_APPEARANCE_OVERRIDES: AppearanceOverridesDocument = {
|
||||
schema_version: "1",
|
||||
light: {
|
||||
accent: "#245f91", accent_foreground: "#ffffff",
|
||||
surface: "#ffffff", surface_foreground: "#303135",
|
||||
success: "#d8eee8", success_foreground: "#315f55",
|
||||
info: "#dce9f3", info_foreground: "#294a61",
|
||||
warning: "#ffe1a3", warning_foreground: "#593700",
|
||||
danger: "#f8d1cc", danger_foreground: "#873c35"
|
||||
},
|
||||
dark: {
|
||||
accent: "#7ea6c5", accent_foreground: "#242424",
|
||||
surface: "#262724", surface_foreground: "#f1f1f1",
|
||||
success: "#24473f", success_foreground: "#d8eee8",
|
||||
info: "#243d4e", info_foreground: "#dce9f3",
|
||||
warning: "#5a431f", warning_foreground: "#ffe1a3",
|
||||
danger: "#4f2d2a", danger_foreground: "#f8d1cc"
|
||||
}
|
||||
};
|
||||
|
||||
export function cloneDefaultAppearanceOverrides(): AppearanceOverridesDocument {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_APPEARANCE_OVERRIDES)) as AppearanceOverridesDocument;
|
||||
}
|
||||
@@ -30,30 +30,6 @@ for (const properties of Object.values(RUNTIME_TOKEN_PROPERTIES)) {
|
||||
for (const property of properties) RUNTIME_PROPERTIES.add(property);
|
||||
}
|
||||
|
||||
export const DEFAULT_APPEARANCE_OVERRIDES: AppearanceOverridesDocument = {
|
||||
schema_version: "1",
|
||||
light: {
|
||||
accent: "#245f91", accent_foreground: "#ffffff",
|
||||
surface: "#ffffff", surface_foreground: "#303135",
|
||||
success: "#d8eee8", success_foreground: "#315f55",
|
||||
info: "#dce9f3", info_foreground: "#294a61",
|
||||
warning: "#ffe1a3", warning_foreground: "#593700",
|
||||
danger: "#f8d1cc", danger_foreground: "#873c35"
|
||||
},
|
||||
dark: {
|
||||
accent: "#7ea6c5", accent_foreground: "#242424",
|
||||
surface: "#262724", surface_foreground: "#f1f1f1",
|
||||
success: "#24473f", success_foreground: "#d8eee8",
|
||||
info: "#243d4e", info_foreground: "#dce9f3",
|
||||
warning: "#5a431f", warning_foreground: "#ffe1a3",
|
||||
danger: "#4f2d2a", danger_foreground: "#f8d1cc"
|
||||
}
|
||||
};
|
||||
|
||||
export function cloneDefaultAppearanceOverrides(): AppearanceOverridesDocument {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_APPEARANCE_OVERRIDES)) as AppearanceOverridesDocument;
|
||||
}
|
||||
|
||||
export function validateAppearanceOverrides(value: unknown): AppearanceOverridesDocument {
|
||||
if (!isRecord(value) || value.schema_version !== "1" || !isRecord(value.light) || !isRecord(value.dark)) {
|
||||
throw new Error("i18n:govoplan-core.appearance_override_invalid_schema");
|
||||
|
||||
+1
-2
@@ -69,11 +69,10 @@ export { AppearancePalettePreview, AppearancePaletteSelect, APPEARANCE_PALETTE_O
|
||||
export { default as AppearanceOverridesEditor } from "./components/AppearanceOverridesEditor";
|
||||
export {
|
||||
APPEARANCE_OVERRIDE_TOKENS,
|
||||
DEFAULT_APPEARANCE_OVERRIDES,
|
||||
applyAppearanceOverrides,
|
||||
cloneDefaultAppearanceOverrides,
|
||||
validateAppearanceOverrides
|
||||
} from "./components/appearanceOverrides";
|
||||
export { DEFAULT_APPEARANCE_OVERRIDES, cloneDefaultAppearanceOverrides } from "./components/appearanceOverrideDefaults";
|
||||
export type { ButtonProps } from "./components/Button";
|
||||
export { default as Card } from "./components/Card";
|
||||
export type { CardProps } from "./components/Card";
|
||||
|
||||
Reference in New Issue
Block a user