From d0de8fdfdcfc6f938a0a6868c79370a94b6ce445 Mon Sep 17 00:00:00 2001 From: chengxinshengglj-png <319807006+chengxinshengglj-png@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:08:30 +0800 Subject: [PATCH] fix(client): probe both generations of the archive glyph name (Issue #287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit primitives renamed the archive glyph from a pixel suffix (`IconArchiveOutline20`) to weight suffixes (`IconArchiveOutlineRegular` / `IconArchiveOutlineMedium`) in 0.1.7 and dropped the old name without keeping an alias — while peerDependencies still span both the 0.1.6 and 0.1.7 host generations. Picking either name alone hands `undefined` to `h()` on the other generation, which surfaces as React #130 and takes the whole slot entry down (observed in `sidebar.footer.action`). Probe the 0.1.6 pixel name first (so older hosts keep their glyph rather than falling back to nothing), then the 0.1.7 weight names, then degrade to no glyph when none of them is present. All four render sites (overlay title, sidebar trigger, portalled native-sidebar entry, better-sidebar tab icon) now go through a null-guarded helper. The naming-axis change is why this is not a mechanical rename: the pixel number that used to carry the size is consumed by the weight axis now, so the chain has to be resolved at runtime against whatever the host actually exports. Adds a regression guard in client.test.js asserting the chain order, that the raw constant never reaches h(), and that all four render sites use the helper. Verified: `npm test` green (1343 tests / 1342 pass / 0 fail / 1 pre-existing skip), `npm run sync` keeps lib/client.js untouched (lib-only, no src counterpart). --- dsh-mneme/lib/client.js | 22 +++++++++++++++++----- dsh-mneme/test/client.test.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/dsh-mneme/lib/client.js b/dsh-mneme/lib/client.js index 57a46578..81bf49a5 100644 --- a/dsh-mneme/lib/client.js +++ b/dsh-mneme/lib/client.js @@ -68,7 +68,19 @@ window.__ModuleLoader__.load({ if (ops.filter((o) => o.kind !== "same").length > 200) return null; return ops; } - const IconArchiveOutline20 = primitives.IconArchiveOutline20; + // #287 归档图标跨代兼容:primitives 在 0.1.7 把图标命名从「像素后缀」改成 + // 「字重后缀」(IconArchiveOutline20 → …OutlineRegular / …OutlineMedium), + // 旧名不留别名,而 peerDependencies 同时覆盖 0.1.6 与 0.1.7 两代宿主—— + // 只认某一代的名字,另一代就会取到 undefined,h(undefined) 即 React #130 + // (整个 slot entry 崩掉)。故按「旧名 → 新名」顺序取第一个存在的; + // 两代都缺时降级为不渲染图标,宁可无图标也不把 slot 打崩。 + const IconArchive = primitives.IconArchiveOutline20 + ?? primitives.IconArchiveOutlineRegular + ?? primitives.IconArchiveOutlineMedium + ?? null; + + /** 渲染归档图标;宿主未提供任一候选名时返回 null(无图标,不影响其余内容)。 */ + const renderArchiveIcon = (props) => (IconArchive ? h(IconArchive, props) : null); // Portal target for the hero fallback surface. The host whitelists // react-dom for its own bundles (dsh-client-ui-trajectory requires it); @@ -2872,7 +2884,7 @@ window.__ModuleLoader__.load({ h("div", { className: "mneme-overlay", role: "region", "aria-label": t("memory.view.label"), ref: panelRef }, h("div", { className: "mneme-overlaybar" }, h("span", { className: "mneme-overlaytitle" }, - h(IconArchiveOutline20, { size: 15 }), + renderArchiveIcon({ size: 15 }), t("memory.view.label") ), h("button", { @@ -4681,7 +4693,7 @@ window.__ModuleLoader__.load({ onClick: openLibrary, "data-mneme-overlay-opener": "true" }, - h(IconArchiveOutline20, { size: wide ? 16 : 18 }), + renderArchiveIcon({ size: wide ? 16 : 18 }), wide && h("span", { className: "mneme-trigger-label" }, t("memory.panel.open")) ), h(ConflictBadge, { pending }) @@ -4774,7 +4786,7 @@ window.__ModuleLoader__.load({ onClick: openLibrary, "data-mneme-overlay-opener": "true" }, - h(IconArchiveOutline20, { size: wide ? 15 : 18 }), + renderArchiveIcon({ size: wide ? 15 : 18 }), wide && h("span", { className: "mneme-topentry-label" }, t("memory.panel.open")), h(ConflictBadge, { pending }) ) @@ -4836,7 +4848,7 @@ window.__ModuleLoader__.load({ reg.registerTab({ id: TAB_ID, title: () => t("memory.view.label"), - icon: (size) => h(IconArchiveOutline20, { size }), + icon: (size) => renderArchiveIcon({ size }), order: 60, component: () => h(MemoryExplorer, { t }) }); diff --git a/dsh-mneme/test/client.test.js b/dsh-mneme/test/client.test.js index 48721da2..b273826f 100644 --- a/dsh-mneme/test/client.test.js +++ b/dsh-mneme/test/client.test.js @@ -313,6 +313,35 @@ test("graph toggle uses a node-graph glyph, not the share icon", () => { ); }); +// #287 跨代图标守卫:primitives 在 0.1.7 把归档图标从「像素后缀」改名成「字重后缀」 +// (IconArchiveOutline20 → …OutlineRegular / …OutlineMedium),旧名不留别名,而 +// peerDependencies 仍同时覆盖 0.1.6 与 0.1.7 两代宿主。只认某一代的名字,另一代就会 +// 把 undefined 交给 h(),落成 slot entry 里的 React #130(该 entry 整块崩),因此必须 +// 探测两代名字、并在全缺时降级为不渲染图标。 +test("#287: archive glyph probes both naming generations and degrades to no glyph", () => { + assert.ok( + /primitives\.IconArchiveOutline20\s*\?\?\s*primitives\.IconArchiveOutlineRegular\s*\?\?\s*primitives\.IconArchiveOutlineMedium/.test(clientSource), + "the glyph must probe the 0.1.6 pixel name before the 0.1.7 weight names" + ); + assert.equal( + /h\(IconArchiveOutline20/.test(clientSource), + false, + "the raw constant must never reach h() — on a host lacking that name it renders undefined (React #130)" + ); + assert.ok( + /const renderArchiveIcon = \(props\) => \(IconArchive \? h\(IconArchive, props\) : null\)/.test(clientSource), + "the glyph must render through a null-guarded helper so a missing icon degrades to nothing" + ); + for (const site of [ + "renderArchiveIcon({ size: 15 })", // 浮层标题栏 + "renderArchiveIcon({ size: wide ? 16 : 18 })", // 侧栏 trigger + "renderArchiveIcon({ size: wide ? 15 : 18 })", // portal 到宿主原生侧栏的入口 + "renderArchiveIcon({ size })" // better-sidebar tab 图标 + ]) { + assert.ok(clientSource.includes(site), `every render site must go through the helper: ${site}`); + } +}); + // 方案 A:查询收敛。状态页只做仪表盘(小页预览 + 服务端 total + 查看全部), // 沉淀/归档的完整浏览走记忆库的 deposited/archived 筛选视图(chip 预置 + // 状态页入口跳转),详情抽屉给归档记忆一个反向的「恢复」。