feat(web): 双档 UI 模式(性能档 = 现状 / Smooth Corner 华丽档)+ 开关挂载(裁决 16.4.4)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- design/uiMode.ts:档位纯逻辑(localStorage `cyrene.ui-mode` 持久化、跨标签页同步、未知值回落默认档),
  只改 <html> 类名与 data 属性 ⇒ 不碰 React 树、不重挂载、几何尺寸不变
- 性能档=**现状**:tokens.css / styles/index.css 的改动**全为新增(0 删除)**,且全部限定在
  `:root.ui-mode-fancy` 下;75 元素签名比对证明性能档渲染与改动前逐元素一致
- 华丽档对齐六大核心:连续曲率 corner-shape / 多层 Surface 环境光 / 弹簧动效 / 深色优先(浅色单列补偿)/
  毛玻璃升级 / Liquid Glass(4 折射 + RGB 色差 + 鼠标跟踪 + 双层动态边框)
- 开关挂载:main.tsx 在 createRoot 之前 initUiMode(零闪烁);TopBar/Nav/AdminTopBar 三处挂载,
  窄屏(<640px)按既有约定收起(实测否则 375px 角色块被压到 27px、面板溢出 73px),移动端由侧栏抽屉覆盖
- 复核:tsc 0 错 + vite build 成功;uiMode 38 项 node 测试 + headless Chrome 真渲染 30 项(真点击/真持久化/
  真指针事件);变异反证(去掉读回 → 持久化往返必红)
This commit is contained in:
AskaEth committed 2026-10-04 13:41:09 +08:00
1 parent c1a98d7c01
commit 8d70f6d331
9 files changed
+961

No files matched your search

+150
View File
@@ -0,0 +1,150 @@
// Logic test for the Miuix dual-tier UI mode (裁决 16.4.4 / design/07 §0.1):
// 默认档 = UI 性能模式、未知值回落、持久化往返、存储不可用降级、类名/属性落地。
//
// The project has no frontend test runner, so this is a dependency-free plain-node
// check that drives the real module and throws on the first failure. Run it with
// the esbuild that already ships with vite (no new dependency, no package.json
// change):
//
// cd frontend/web \
// && ./node_modules/.bin/esbuild src/design/uiMode.test.ts \
// --bundle --platform=node --format=esm \
// --outfile=/tmp/cyrene-uimode.test.mjs \
// && node /tmp/cyrene-uimode.test.mjs
//
// `tsc --noEmit` (part of `npm run build`) type-checks this file, which is why it
// imports nothing from node:*.
import {
DEFAULT_UI_MODE,
UI_MODE_ATTR,
UI_MODE_CLASS,
UI_MODE_HINT,
UI_MODE_LABEL,
UI_MODE_ORDER,
UI_MODE_STORAGE_KEY,
applyUiMode,
isUiMode,
normalizeUiMode,
readUiMode,
setUiMode,
toggleUiMode,
writeUiMode,
type UiMode,
type UiModeStorage,
} from "./uiMode";
let checks = 0;
function ok(cond: boolean, what: string): void {
checks += 1;
if (!cond) throw new Error(`FAILED: ${what}`);
}
function eq(got: string, want: string, what: string): void {
ok(got === want, `${what} — got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`);
}
/** 内存版 localStorage。 */
function memoryStorage(seed: Record<string, string> = {}): UiModeStorage & { raw: Map<string, string> } {
const raw = new Map<string, string>(Object.entries(seed));
return {
raw,
getItem: (key: string) => raw.get(key) ?? null,
setItem: (key: string, value: string) => {
raw.set(key, value);
},
};
}
/** 隐私模式 / 禁用存储:读写都抛。 */
const throwingStorage: UiModeStorage = {
getItem: () => {
throw new Error("storage disabled");
},
setItem: () => {
throw new Error("storage disabled");
},
};
/** 最小 DOM 替身:applyUiMode 只用 classList.add/remove + setAttribute。 */
function fakeRoot(): { classes: Set<string>; attrs: Map<string, string>; el: HTMLElement } {
const classes = new Set<string>();
const attrs = new Map<string, string>();
const el = {
classList: {
add: (name: string) => classes.add(name),
remove: (name: string) => classes.delete(name),
contains: (name: string) => classes.has(name),
},
setAttribute: (key: string, value: string) => attrs.set(key, value),
getAttribute: (key: string) => attrs.get(key) ?? null,
} as unknown as HTMLElement;
return { classes, attrs, el };
}
// 默认档 = UI 性能模式(裁决 16.4.4 / design/07 §0.1「默认 = UI 性能档」)。
eq(DEFAULT_UI_MODE, "performance", "默认档是 UI 性能模式");
eq(UI_MODE_ORDER[0], DEFAULT_UI_MODE, "档位顺序首项 = 默认档");
eq(String(UI_MODE_ORDER.length), "2", "只有两档");
// 空存储 → 默认档。
eq(readUiMode(memoryStorage()), "performance", "空存储读默认档");
// 未知 / 损坏 / 大小写不符的值一律回落默认档(前向兼容,不抛)。
const junk: Array<string | null | undefined> = ["", " ", "FANCY", "Fancy", "fancyx", "performance ", "2", null, undefined];
for (const value of junk) {
eq(normalizeUiMode(value), DEFAULT_UI_MODE, `未知值 ${JSON.stringify(value)} 回落默认档`);
}
eq(normalizeUiMode("fancy"), "fancy", "合法值 fancy 保留");
eq(normalizeUiMode("performance"), "performance", "合法值 performance 保留");
ok(isUiMode("fancy") && isUiMode("performance"), "isUiMode 认两档");
ok(!isUiMode("FANCY") && !isUiMode("") && !isUiMode(null), "isUiMode 拒绝非法值");
// 持久化往返:写进去必须读得回来(这是「开关状态存 localStorage」的核心断言)。
const store = memoryStorage();
writeUiMode("fancy", store);
eq(readUiMode(store), "fancy", "fancy 持久化往返");
eq(store.raw.get(UI_MODE_STORAGE_KEY) ?? "", "fancy", "落盘键名与值");
writeUiMode("performance", store);
eq(readUiMode(store), "performance", "回切 performance 也往返");
// setUiMode(切档入口)同样落盘;无 DOM 环境下仍不抛。
setUiMode("fancy", store);
eq(readUiMode(store), "fancy", "setUiMode 落盘");
setUiMode("bogus" as UiMode, store);
eq(readUiMode(store), "performance", "setUiMode 非法值回落默认档并落盘");
// 存储不可用(隐私模式)→ 读默认档、写不抛(退化为仅本次会话生效)。
eq(readUiMode(throwingStorage), "performance", "存储抛错时读默认档");
writeUiMode("fancy", throwingStorage);
eq(readUiMode(throwingStorage), "performance", "存储抛错时不崩且仍读默认档");
eq(readUiMode(null), "performance", "无存储对象时读默认档");
// 两档互切。
eq(toggleUiMode("performance"), "fancy", "performance → fancy");
eq(toggleUiMode("fancy"), "performance", "fancy → performance");
// 类名/属性落地:切档只改 <html>(不碰组件树 ⇒ 不丢会话状态)。
const root = fakeRoot();
applyUiMode("fancy", root.el);
ok(root.classes.has(UI_MODE_CLASS.fancy), "华丽档加 ui-mode-fancy 类");
ok(!root.classes.has(UI_MODE_CLASS.performance), "华丽档移除 ui-mode-performance 类");
eq(root.attrs.get(UI_MODE_ATTR) ?? "", "fancy", "data-ui-mode = fancy");
applyUiMode("performance", root.el);
ok(root.classes.has(UI_MODE_CLASS.performance), "性能档加 ui-mode-performance 类");
ok(!root.classes.has(UI_MODE_CLASS.fancy), "性能档移除 ui-mode-fancy 类");
eq(root.attrs.get(UI_MODE_ATTR) ?? "", "performance", "data-ui-mode = performance");
applyUiMode("nonsense" as UiMode, root.el);
ok(root.classes.has(UI_MODE_CLASS.performance), "非法档位回落到性能档类名");
// 无 root(SSR / 无 DOM)时不抛。
applyUiMode("fancy", null);
// 开关文案齐全(两档都有标签与说明,UI 不会出现空档位)。
for (const id of UI_MODE_ORDER) {
ok(UI_MODE_LABEL[id].length > 0, `${id} 有标签`);
ok(UI_MODE_HINT[id].length > 0, `${id} 有说明`);
}
console.log(`uiMode.test.ts OK — ${checks} checks`);