dsh-m 0.9.55 → 0.9.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,20 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
8
8
 
9
9
  ## 中文
10
10
 
11
+ ### 0.9.57 修复:精选切换后面板崩溃/无法重开 + 折叠筛选双框与 +N 重叠
12
+
13
+ - **根因(0.9.56 真机复现)**:精选区的标签对象可能每次父级重渲染都重建;layout effect 依赖 `chips` 数组引用及自身更新的 `geom`,同步测量反复进入、最终触发 React `Maximum update depth exceeded`(#185)。React 清空面板树但留下 `#dshm-panel-root`,旧入口守卫只检查容器存在,导致点击入口永久无效。叠加折叠筛选定位壳和按钮的双边框、以及 +N 位置夹取/文档流试验在窄宽下压住相邻 chip。
14
+ - **修复**:按实际渲染的分类 id/文案/计数生成稳定内容签名,layout effect 不再同步依赖自身 `geom`;预留槽变化改在上一轮状态提交后的 rAF 复测。回退 0.9.56 的 `display:none`/in-flow 反馈链,恢复分类全量占位 + 越界 `visibility:hidden`,以完整 `offsetTop` 按行纯派生隐藏数;+N 脱离文档流,独占右侧 52px 槽(筛选另占 96px),双类 CSS 选择器确保总预留不被 gutter 覆盖。末可见行顶按**去重行**而非第 N 颗 chip 计算。筛选外壳去边框仅保留定位;若崩溃留下无可见面板的空容器,再次点击时先移除再挂载。
15
+ - **取舍**:+N 优先保证零重叠、正确计数和可重开;不再强求其贴着最后一颗分类。窄宽下边界分类自然少显示,`+N` 如实计入;展开态 `⌃` 仍在分类列表末尾。
16
+ - **验证**:新增 3 个回归断言,累计 1224 测试全绿、typecheck 零错误;真实 DSH Web 加载候选客户端验证精选↔社区连续三次、关闭再开及崩溃残留空容器恢复;260–880px 六档 +N/chip/筛选相交面积恒 0,精选五类不出现错误 +N。
17
+
18
+ ### 0.9.56 变更:ZoneChips +N 重叠根治——in-flow 文档流布局 + 折行减类(主人方案)
19
+
20
+ - **缺陷(0.9.55 真机复现)**:折叠态 `+N` 采用「绝对定位 + contentW−48 夹取」,当末可见行剩余空间小于徽章宽时,徽章压住末可见 chip(真机:+9 压住「文档与渲染 61」右端计数)。
21
+ - **根治(in-flow 文档流布局)**:`+N` 弃绝对定位,以 in-flow 插在「末可见分类」与「被裁分类」之间——flow 布局与 chip 的重叠在构造上不可能。
22
+ - **折行减类(主人方案)**:被裁分类 `visibility:hidden` → **`display:none`**(不占位,折行减类生效前提);测量发现 `+N` 顶 > 末可见 chip 行顶(被折出)⇒ `hiddenCount +1`,一枚可见分类退位、`+N` 退回末可见行,同时 `max-height` 扩一行给 `+N` 独占;棘轮单向下行,宽度增大时保守不多显(+N 计数仍真实)。
23
+ - **测试**:全量 1221 例零失败、typecheck 零错误;无头探针实证常宽 880/窄宽 420/展开/收起/Q1 回归全部 `chipOverlapPx = 0`、`+N` 全程可见、展开态 23+⌃+筛选全量、收起后 +N 回归蓝筐位。
24
+
11
25
  ### 0.9.55 变更:ZoneChips 折叠/展开切换钮内联化(对齐 dsh-market 参考设计)+ overlay 叠涂 scrim 强化对比度
12
26
 
13
27
  - **切换钮内联化(主人提出,dsh-market v1.66.11 参考设计)**:`+N` 从右侧 overlay 组改为**绝对定位内联**——钉在「末可见分类 chip 右侧 +6px」(即末可见行剩余空位),与展开态 `⌃`(列表末尾 in-flow)构成统一位置语义「紧跟当前列表末尾」,只翻方向不挪位;窄宽度放不下时贴内容右缘收口(不侵入筛选槽)。gutter 随 `+N` 移出由 132px 瘦身至 96px(仅承载钉住筛选),同宽度可多显示约 1 颗分类。overlay 组内 `+N` 移除后仅剩钉住筛选,第④类碰撞(R2-N1)自然消解。
@@ -412,6 +426,20 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
412
426
 
413
427
  ## English
414
428
 
429
+ ### Fixed in 0.9.57 — curated-zone update loop, dead panel entry, and collapsed filter/overflow overlaps
430
+
431
+ - **Root cause (reproduced on 0.9.56)**: curated labels may get a fresh object on every parent render; the layout effect depended on the `chips` array identity and on its own `geom` update. Synchronous remeasurement repeatedly entered the same commit, eventually throwing React `Maximum update depth exceeded` (#185). React emptied the panel tree while leaving `#dshm-panel-root`; the old entry guard checked only for a container and silently blocked every later open. The pinned Filter wrapper also added a second border around its bordered button, while the clamped/inline `+N` iterations overlapped neighboring chips at narrow widths.
432
+ - **Fix**: depend on a stable signature of rendered category IDs, labels and counts; defer geometry-dependent remeasurement to an animation frame after state commits. Retire the 0.9.56 `display:none`/in-flow feedback path in favor of a full in-flow chip layout with `visibility:hidden` beyond the clip; derive the hidden count from all chip row offsets. Give out-of-flow `+N` its own 52px right slot, separate from Filter's 96px, with a more specific selector so the total reservation wins the cascade. Measure the last visible **distinct row** rather than the Nth chip. Strip the redundant Filter-wrapper border; discard an orphaned panel root on the next open.
433
+ - **Tradeoff**: correctness, zero overlap and reopenability take precedence over keeping `+N` flush against the last chip. Its count remains truthful; expanded `⌃` remains at list end.
434
+ - **Verification**: three additional regression assertions, 1224 tests passing and clean typecheck; real DSH Web with the candidate client survived three curated↔community round trips, close/reopen and stale-root recovery; six widths from 260–880px showed zero badge/chip/Filter intersection, and curated's five categories showed no spurious `+N`.
435
+
436
+ ### Fixed in 0.9.56 — ZoneChips +N overlap eradicated: in-flow document layout + wrap-yield-a-category (owner's design)
437
+
438
+ - **Defect (reproduced on 0.9.55)**: the collapsed `+N` used "absolute positioning + contentW−48 clamp"; when the last visible row's leftover was narrower than the badge, the badge covered the last visible chip (real panel: +9 covering the tail of 文档与渲染 61).
439
+ - **Fix (in-flow document layout)**: `+N` abandons absolute positioning and flows inline between the last visible category and the clipped categories — chip/badge overlap is impossible by construction.
440
+ - **Wrap-yield-a-category (owner's design)**: clipped categories switch `visibility:hidden` → **`display:none`** (no space — the prerequisite for yielding to work); when the measure finds the `+N` top beyond the last visible chip's row top (wrapped), `hiddenCount +1` — one visible category yields, `+N` pulls back into the last visible row, and `max-height` grows one row to give `+N` its own line; the ratchet is single-direction (down), staying conservative on width growth while the count stays truthful.
441
+ - **Tests**: full suite 1221 cases zero failures, typecheck clean; headless probe verified 880px/420px/expanded/collapsed/Q1-regression all at `chipOverlapPx = 0` with `+N` visible throughout, expanded view showing 23+⌃+filter in full, and `+N` returning to its inline slot after collapse.
442
+
415
443
  ### Changed in 0.9.55 — ZoneChips expand/collapse toggle inline (aligned with the dsh-market reference design) + overlay stacked scrim for contrast
416
444
 
417
445
  - **Inline toggle (owner-requested, reference: dsh-market v1.66.11)**: `+N` moves out of the right-side overlay group into an **absolutely-positioned inline slot** pinned at "last visible category chip's right edge + 6px" (the leftover of the last visible row), sharing one position semantic with the expanded `⌃` (in-flow at list end): "hug the current end of the list", flipping direction without relocating; when the leftover is too narrow it clamps to the content right edge without entering the filter gutter. The gutter slims from 132px to 96px (hosting only the pinned filter), showing ~1 more category per width. With `+N` removed, the overlay group hosts only the pinned filter and the R2-N1 4th collision class dissolves naturally.
package/lib/client.js CHANGED
@@ -41,6 +41,7 @@ __export(market_state_exports, {
41
41
  registryNotice: () => registryNotice,
42
42
  resetPageOnFilterChange: () => resetPageOnFilterChange,
43
43
  searchSourceOf: () => searchSourceOf,
44
+ visibleRowTop: () => visibleRowTop,
44
45
  zoneChips: () => zoneChips
45
46
  });
46
47
  function toSafeInt(value, fallback, min, max) {
@@ -278,6 +279,20 @@ function clipTopOf(offsets, maxRows) {
278
279
  }
279
280
  return Infinity;
280
281
  }
282
+ function visibleRowTop(offsets, maxRows) {
283
+ if (!Array.isArray(offsets) || !Number.isInteger(maxRows) || maxRows < 1) return 0;
284
+ let rows = 0;
285
+ let last = null;
286
+ for (const top of offsets) {
287
+ if (typeof top !== "number" || !Number.isFinite(top)) continue;
288
+ if (last === null || top !== last) {
289
+ rows += 1;
290
+ last = top;
291
+ if (rows === maxRows) return top;
292
+ }
293
+ }
294
+ return last ?? 0;
295
+ }
281
296
  function autoExpandDecision(activeTop, clipTop, autoRefMatched, expanded) {
282
297
  const beyond = typeof activeTop === "number" && Number.isFinite(activeTop) && typeof clipTop === "number" && Number.isFinite(clipTop) && activeTop >= clipTop;
283
298
  if (!beyond || autoRefMatched) return { record: false, expand: false };
@@ -1558,7 +1573,7 @@ var h = React.createElement;
1558
1573
  var { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } = React;
1559
1574
  var PLUGIN_ID = "dsh-m";
1560
1575
  var API = "/dshm";
1561
- var { DEFAULT_PAGE_SIZE: DEFAULT_PAGE_SIZE2, MARKET_PAGE_SIZES: MARKET_PAGE_SIZES2, pageItems: pageItems2, createZoneState: createZoneState2, normalizeMarketQuery: normalizeMarketQuery2, resetPageOnFilterChange: resetPageOnFilterChange2, normalizeMarketResponse: normalizeMarketResponse2, registryNotice: registryNotice2, zoneChips: zoneChips2, marketNotice: marketNotice2, searchSourceOf: searchSourceOf2, mergeLatestFields: mergeLatestFields2, chipRows: chipRows2, countBeyondRows: countBeyondRows2, clipTopOf: clipTopOf2, autoExpandDecision: autoExpandDecision2 } = (init_market_state(), __toCommonJS(market_state_exports));
1576
+ var { DEFAULT_PAGE_SIZE: DEFAULT_PAGE_SIZE2, MARKET_PAGE_SIZES: MARKET_PAGE_SIZES2, pageItems: pageItems2, createZoneState: createZoneState2, normalizeMarketQuery: normalizeMarketQuery2, resetPageOnFilterChange: resetPageOnFilterChange2, normalizeMarketResponse: normalizeMarketResponse2, registryNotice: registryNotice2, zoneChips: zoneChips2, marketNotice: marketNotice2, searchSourceOf: searchSourceOf2, mergeLatestFields: mergeLatestFields2, countBeyondRows: countBeyondRows2, clipTopOf: clipTopOf2, visibleRowTop: visibleRowTop2, autoExpandDecision: autoExpandDecision2 } = (init_market_state(), __toCommonJS(market_state_exports));
1562
1577
  var { readMarketSnapshot: readMarketSnapshot2, writeMarketSnapshot: writeMarketSnapshot2, isDefaultFirstPageQuery: isDefaultFirstPageQuery2 } = (init_market_snapshot(), __toCommonJS(market_snapshot_exports));
1563
1578
  var { backdropCloseHandlers: backdropCloseHandlers2 } = (init_backdrop(), __toCommonJS(backdrop_exports));
1564
1579
  var { createMarkdown: createMarkdown2 } = (init_markdown(), __toCommonJS(markdown_exports));
@@ -2189,8 +2204,11 @@ var CSS = `
2189
2204
  .dshm-chips{display:flex;flex-wrap:wrap;gap:6px;position:relative}
2190
2205
  .dshm-chips-clip{overflow:hidden}
2191
2206
  .dshm-chips-gutter{padding-right:var(--dshm-clip-gutter,96px)}
2192
- .dsvm-chipmore{position:absolute;right:8px;display:flex;align-items:center;gap:8px;min-height:28px;background-color:var(--dsw-alias-bg-base,#fff);background-image:linear-gradient(var(--dsw-alias-bg-base,#fff),var(--dsw-alias-bg-base,#fff)),linear-gradient(var(--dsw-alias-bg-base,#fff),var(--dsw-alias-bg-base,#fff));border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18));border-radius:999px;padding:2px 8px;box-sizing:border-box}
2193
- .dsvm-chipmore-plus{position:absolute;z-index:2;background-color:var(--dsw-alias-bg-base,#fff);background-image:linear-gradient(var(--dsw-alias-bg-base,#fff),var(--dsw-alias-bg-base,#fff)),linear-gradient(var(--dsw-alias-bg-base,#fff),var(--dsw-alias-bg-base,#fff))}
2207
+ /* +N/\u7B5B\u9009\u5206\u522B\u5360\u53F3\u4FA7 52/96px\uFF0C\u4E0D\u80FD\u8BA9 gutter \u540C\u7279\u5F02\u6027\u89C4\u5219\u8986\u76D6\u603B\u9884\u7559\u5BBD\u5EA6\u3002 */
2208
+ .dshm-chips.dshm-chips-clip.dshm-chips-reserve{padding-right:52px}
2209
+ .dshm-chips.dshm-chips-clip.dshm-chips-gutter.dshm-chips-reserve{padding-right:calc(var(--dshm-clip-gutter,96px) + 52px)}
2210
+ .dsvm-chipmore{position:absolute;right:8px}
2211
+ .dsvm-chipmore-plus{position:absolute;right:calc(var(--dshm-clip-gutter,96px) + 6px);z-index:1;background-color:var(--dsw-alias-bg-base,#fff);background-image:linear-gradient(var(--dsw-alias-bg-base,#fff),var(--dsw-alias-bg-base,#fff)),linear-gradient(var(--dsw-alias-bg-base,#fff),var(--dsw-alias-bg-base,#fff))}
2194
2212
  .dsvm-searchrow{display:flex;align-items:center;gap:8px}
2195
2213
  .dsvm-searchrow .dshm-search{flex:1;display:flex}
2196
2214
  .dshm-search{position:relative}
@@ -2711,6 +2729,7 @@ function SearchBox({ placeholder, initial, onCommit }) {
2711
2729
  }
2712
2730
  function ZoneChips({ zone, counts, labels, active, onPick, trailing, wrapTitle }) {
2713
2731
  const chips = useMemo(() => zoneChips2(counts, labels, zone), [counts, labels, zone]);
2732
+ const chipsSignature = JSON.stringify(chips.map((c) => [c.id, c.labelKey ? lookup(c.labelKey) : c.label, c.count]));
2714
2733
  const [expanded, setExpanded] = useState(false);
2715
2734
  const [stuck, setStuck] = useState(false);
2716
2735
  const [geom, setGeom] = useState(null);
@@ -2732,32 +2751,16 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing, wrapTitle }
2732
2751
  if (!rh && k.offsetHeight) rh = k.offsetHeight;
2733
2752
  }
2734
2753
  if (!tops.length) return;
2735
- const clipTop2 = clipTopOf2(tops, maxRows);
2736
- let lastVisRight = 0;
2737
- let lastVisTop = 0;
2738
- let idx = -1;
2739
- for (const k of el.children) {
2740
- if (!(k instanceof HTMLElement) || k.getAttribute("data-chip") !== "1") continue;
2741
- idx += 1;
2742
- if (idx === 0) continue;
2743
- if (k.offsetTop < clipTop2) {
2744
- lastVisRight = k.offsetLeft + k.offsetWidth;
2745
- lastVisTop = k.offsetTop;
2746
- }
2747
- }
2748
- const cs = getComputedStyle(el);
2749
- const contentW = el.clientWidth - (parseFloat(cs.paddingRight) || 0);
2750
- const plusLeft = Math.max(0, Math.min(lastVisRight + 6, contentW - 48));
2751
- const next = {
2752
- tops,
2753
- clipTop: clipTop2,
2754
- hiddenCount: countBeyondRows2(tops.slice(1), maxRows),
2755
- plusLeft,
2756
- plusTop: lastVisTop
2757
- };
2758
- setGeom((prev) => prev && prev.tops.length === next.tops.length && prev.clipTop === next.clipTop && prev.hiddenCount === next.hiddenCount && prev.plusLeft === next.plusLeft && prev.plusTop === next.plusTop ? prev : next);
2754
+ setGeom((prev) => {
2755
+ const clipTopV = clipTopOf2(tops, maxRows);
2756
+ const hiddenCount2 = countBeyondRows2(tops, maxRows);
2757
+ const lastVisTop2 = visibleRowTop2(tops, maxRows);
2758
+ const next = { tops, clipTop: clipTopV, hiddenCount: hiddenCount2, lastVisTop: lastVisTop2 };
2759
+ const same = prev && prev.hiddenCount === next.hiddenCount && prev.clipTop === next.clipTop && prev.lastVisTop === next.lastVisTop && prev.tops.length === next.tops.length && prev.tops.every((t, i) => t === next.tops[i]);
2760
+ return same ? prev : next;
2761
+ });
2759
2762
  setRowHeight((prev) => Math.abs(prev - rh) <= 0.5 ? prev : rh);
2760
- }, [expanded, maxRows]);
2763
+ }, [expanded, maxRows, chips.length]);
2761
2764
  useEffect(() => {
2762
2765
  if (!active) {
2763
2766
  autoRef.current = null;
@@ -2773,7 +2776,12 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing, wrapTitle }
2773
2776
  }, [active, activeIdx, expanded, geom]);
2774
2777
  useLayoutEffect(() => {
2775
2778
  if (!expanded) measure();
2776
- }, [chips, active, expanded, zone, stuck, rowHeight, measure]);
2779
+ }, [chipsSignature, active, expanded, zone, stuck, rowHeight, measure]);
2780
+ useEffect(() => {
2781
+ if (expanded || !geom || typeof requestAnimationFrame !== "function") return;
2782
+ const raf = requestAnimationFrame(measure);
2783
+ return () => cancelAnimationFrame(raf);
2784
+ }, [geom, expanded, measure]);
2777
2785
  useEffect(() => {
2778
2786
  const el = wrapRef.current;
2779
2787
  if (!el || expanded || typeof ResizeObserver === "undefined") return;
@@ -2809,6 +2817,7 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing, wrapTitle }
2809
2817
  const filterPinned = clip && trailing != null;
2810
2818
  const clipTop = geom ? geom.clipTop : Infinity;
2811
2819
  const hiddenCount = geom ? geom.hiddenCount : 0;
2820
+ const lastVisTop = geom ? geom.lastVisTop : 0;
2812
2821
  const btn = (c, i) => {
2813
2822
  const top = geom ? geom.tops[i + 1] : null;
2814
2823
  const clipped = clip && geom && typeof top === "number" && top >= clipTop;
@@ -2841,19 +2850,20 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing, wrapTitle }
2841
2850
  "div",
2842
2851
  {
2843
2852
  ref: wrapRef,
2844
- className: `dshm-chips${clip ? " dshm-chips-clip" : ""}${filterPinned ? " dshm-chips-gutter" : ""}`,
2853
+ className: `dshm-chips${clip ? " dshm-chips-clip" : ""}${filterPinned ? " dshm-chips-gutter" : ""}${clip && hiddenCount > 0 ? " dshm-chips-reserve" : ""}`,
2845
2854
  title: wrapTitle || void 0,
2846
2855
  style: { maxHeight: clip ? `${maxH}px` : "none" }
2847
2856
  },
2848
2857
  h("button", { "data-chip": "1", className: `dshm-chip${active == null ? " on" : ""}`, onClick: () => onPick(null) }, lookup("cat.all")),
2849
2858
  ...chips.map((c, i) => btn(c, i)),
2850
- // 折叠态 +N(0.9.55 对齐 dsh-market 参考设计):绝对定位内联钉在「末可见分类 chip 右侧 +6px」,
2851
- // 与展开态 ⌃(列表末尾 in-flow)构成同一位置语义——「紧跟当前列表末尾」,只翻方向不挪位。
2852
- // 不带 data-chip(不参与行分组测量);位于可见行区内,不受 overflow 裁剪。
2859
+ // 折叠态 +N(0.9.57 终版):绝对定位钉在「末可见行右端预留槽」——出流不挤压 chip、不参与
2860
+ // 行分组(data-more);预留槽由 .dshm-chips-clip 的 padding-right 划出,几何保证零重叠;
2861
+ // 位置右对齐紧贴筛选 gutter 左侧,垂直对齐末可见行(top = lastVisTop,测量派生)。
2853
2862
  clip && geom && hiddenCount > 0 ? h("button", {
2854
2863
  key: "more",
2864
+ "data-more": "1",
2855
2865
  className: "dshm-chip dsvm-chipmore-plus",
2856
- style: { left: `${geom.plusLeft}px`, top: `${geom.plusTop}px` },
2866
+ style: { top: `${lastVisTop}px` },
2857
2867
  onClick: () => setExpanded(true)
2858
2868
  }, `+${hiddenCount}`) : null,
2859
2869
  expanded ? [
@@ -4510,7 +4520,11 @@ function MarketPanel({ onClose }) {
4510
4520
  function mountPanel() {
4511
4521
  if (typeof document === "undefined") return;
4512
4522
  ensureCss();
4513
- if (document.getElementById("dshm-panel-root")) return;
4523
+ const existing = document.getElementById("dshm-panel-root");
4524
+ if (existing) {
4525
+ if (existing.querySelector(".dshm-panel")) return;
4526
+ existing.remove();
4527
+ }
4514
4528
  const container = document.createElement("div");
4515
4529
  container.id = "dshm-panel-root";
4516
4530
  document.body.appendChild(container);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.55",
3
+ "version": "0.9.57",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",