dsh-m 0.9.51 → 0.9.53

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.53 变更:ZoneChips 方案A「隐身全量测量」——冷挂载分类行错误折叠根除 + 行容量即时重测
12
+
13
+ - **动机(0.9.52 端到端验证发现)**:新浏览器 profile(无 localStorage 快照)首开面板时,市场数据未到 → `fit` 在空列表上测量 → 数据到达后重测面对的是按陈旧预算折叠的 DOM(只渲染 1 颗 + 「+N」)→ 棘轮式冻结在「3 颗 + +20」单行折叠。暖会话(快照命中)首挂载即全量数据、一次测准,故 0.7.0 以来从未暴露。姊妹问题:测量 effect 不监听宽度变化,窗口缩放后 `+N` 与实际容量脱节。完整根因链与无头实证见 `docs/plans/2026-10-08-zonechips-visual-clip-requirements.md`。
14
+ - **方案(方案A「隐身全量测量」,经独立评审三轮 11 条意见收敛,评审记录见计划文档修订记录)**:废除 slice 预算,分类 chips **全量渲染**,折叠改为 `.dshm-chips` 上的 `max-height`(实测行高 × 行数)+ `overflow:hidden` 视觉裁剪;越界 chip 逐颗 `visibility:hidden`(与现状「未渲染不进 tab 序」对齐);`+N`/`⌃` 改右侧 overlay 组(水平并排、钉末可见行右端,gutter `--dshm-clip-gutter:132px` 几何保证与 chip 零相交);筛选触发器折叠态钉 overlay、展开态维持 in-flow;重测钩子 = deps(`chips.length/expanded/zone/stuck/rowHeight`)+ `ResizeObserver`(rAF 节流)+ `document.fonts.ready`,均 feature-detect 降级(先例 `:947`)。测量纯函数(`chipRows/countBeyondRows/clipTopOf/autoExpandDecision`)下沉 `market-state.js`,20 例 Node 单测直测。
15
+ - **语义保持**:吸顶收一行/回滚两行(0.9.52 哨兵链路不动,裁剪宿主为 `.dshm-chips`、严禁 wrap 裁剪——wrap 裁剪会剪掉哨兵致 stuck 永久误判,评审 Q1 阻断项);`+N` 展开/`⌃` 收起;激活分类越界自动展开 + autoRef「手动收起后不弹回」(实现拆分为重置/决策两 effect,评审 Q3);0 计数降透明。行为改进一处:折叠态筛选由「行尾第 3 行 in-flow」改为「末可见行右端 overlay 常驻」。
16
+ - **测试**:新增 `client-zonechips-clip.test.mjs` 20 例 + smoke 全量渲染断言 1 例,全量 1221 例零失败、typecheck 零错误;无头探针 V1–V6 矩阵实证(冷数据到达两行折叠正确 / 三档宽度即时重算 / 精选区不预留 gutter / 吸顶一行重算且回滚恢复 / 筛选与 +N 零遮挡均可点 / 自动展开与 autoRef 不弹回),探针不入库(0.9.52 惯例)。
17
+
18
+ ### 0.9.52 变更:头部行距统一——吸顶哨兵移出 flex 流,消除搜索框与分类行间 25px 空带
19
+
20
+ - **动机(主人截图发现)**:市场头部「搜索框行 → 分类 chips 行」间距实测 25px(12 gap + 1px 哨兵 + 12 gap),是「分区 chips → 搜索框行」12px 节奏的两倍余,社区/精选两区一致,视觉上是一条全宽无内容死带;搜索态下 ZoneChips 连带哨兵整体卸载、空带自动消失——证明是布局副作用而非设计节奏。
21
+ - **根因**:0.7.0 Task 10 引入的吸顶检测哨兵(1px div,IntersectionObserver 观察其滚出视口 → chips 吸顶自动收一行)是 `.dshm-body`(flex column, gap:12px)的直接子项,多吃一份 gap。哨兵必须在流内才能服务吸顶:直接删除 / `display:contents` / 与 wrap 外包 wrapper 三种朴素方案分别杀死 IO、令收缩永久生效、锁死吸顶行程,均否决。
22
+ - **改动(单点,ZoneChips 渲染尾)**:哨兵移入 `.dsvm-chipswrap` 内部绝对定位(`position:sticky` 本身即定位上下文),`top:-5px` 复刻旧几何——旧兄弟哨兵顶边恰在 wrap 上沿上方 5px,IO 出视口触发点逐像素等价。无头 Chromium 探针实证吸顶链路完整:pin 后 wrapTop=0、哨兵 −5px 出视口、IO `isIntersecting=false`(stuck 触发正常)。行距恢复 `zoneBar |12| searchRow |12| chips`:搜索框→分类行 24.36px→12px,头部净省 ~13px,浏览态与搜索态节奏一致。
23
+ - **测试**:全量 1200 例零失败、typecheck 零错误;几何与吸顶行为以临时无头探针实证后移除(不入正式套件)。
24
+
11
25
  ### 0.9.51 变更:社区条目 github 字段补全(图标覆盖 45.6% → 100%)
12
26
 
13
27
  - **动机**:社区区 npm 源条目卡片恒为首字母色块——适配层 npm 分支只产出 `npm` 字段即定源,上游目录 `url` 里的 GitHub repo 映射只被降级进 homepage,`github` 字段恒缺,客户端 Icon 的 owner 头像兜底(`github.com/<owner>.png?size=64`)无从触发。实测 `dsh-plugin-catalog@2026.1007.4837`:4,226 条可收录条目中 npm 源 2,298 条 **100% 带 github.com url**,纯数据丢弃,非上游缺失。
@@ -386,6 +400,20 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
386
400
 
387
401
  ## English
388
402
 
403
+ ### Changed in 0.9.53 — ZoneChips Plan A "invisible full-list measurement": cold-mount mis-collapse eradicated + live row-capacity re-measurement
404
+
405
+ - **Motivation (found during 0.9.52 end-to-end verification)**: with a fresh browser profile (no localStorage snapshot), the market data had not arrived when the panel first mounted, so the old `fit` measurement ran on an EMPTY list; when data arrived the re-measure faced a DOM already collapsed by the stale budget (1 chip + "+N") and ratcheted to a frozen "3 chips + +20" single row. Warm sessions hit the snapshot on first mount and measured correctly — which is why this 0.7.0-era defect never surfaced. Sister issue: the measure effect ignores width changes, so `+N` drifts from real capacity after window resizes. Full causal chain with headless evidence: `docs/plans/2026-10-08-zonechips-visual-clip-requirements.md`.
406
+ - **Change (Plan A "invisible full-list measurement", converged through an independent 3-round review with 11 findings)**: the slice-budget is gone — category chips **always render in full**; collapsing is a `max-height` (measured row-height × rows) + `overflow:hidden` visual clip on `.dshm-chips`; chips beyond the clip get per-chip `visibility:hidden` (parity with today's "unrendered = not in tab order"); `+N`/`⌃` become a right-side overlay group (horizontal pair pinned to the last visible row, gutter `--dshm-clip-gutter:132px` geometrically guarantees zero chip intersection); the filter trigger is pinned as an overlay while collapsed and stays in-flow when expanded; re-measure hooks = deps (`chips.length/expanded/zone/stuck/rowHeight`) + `ResizeObserver` (rAF-throttled) + `document.fonts.ready`, all feature-detected with graceful degradation (precedent `:947`). Measurement pure functions (`chipRows/countBeyondRows/clipTopOf/autoExpandDecision`) moved to `market-state.js` with 20 direct Node unit tests.
407
+ - **Semantics preserved**: sticky collapse to one row / restore on scroll-up (the 0.9.52 sentinel chain untouched; the clip host is `.dshm-chips` and clipping the wrap is forbidden — it would clip the sentinel and permanently mis-set stuck, review blocker Q1); `+N` expand / `⌃` collapse; auto-expand when the active category falls beyond the clip + autoRef "manual collapse is respected" (implemented as split reset/decision effects, review Q3); zero-count dimming. One intentional behavior improvement: while collapsed, the filter trigger moved from "third in-flow row end" to a persistent overlay at the last visible row's right end.
408
+ - **Tests**: new `client-zonechips-clip.test.mjs` (20 cases) + 1 full-render smoke assertion; full suite 1221 cases zero failures, typecheck clean; headless probe matrix V1–V6 verified (cold data arrival collapses correctly at two rows / three-width live recompute / curated zone reserves no gutter / sticky one-row recompute and restore / filter & +N zero-overlap and clickable / auto-expand & autoRef no-bounce); probes not committed (0.9.52 convention).
409
+
410
+ ### Changed in 0.9.52 — uniform header rhythm: sticky sentinel leaves the flex flow, killing the 25px dead band between search row and category chips
411
+
412
+ - **Motivation (spotted in the owner's screenshots)**: the market header gap between the search row and the category chips row measures 25px (12 gap + 1px sentinel + 12 gap) — more than double the 12px rhythm between zone chips and the search row, identical in both community and curated zones, visually a full-width empty dead band; in search mode ZoneChips (and its sentinel) unmount entirely and the band disappears — proof it is a layout side effect, not designed rhythm.
413
+ - **Root cause**: the sticky-detection sentinel added in 0.7.0 Task 10 (a 1px div watched by an IntersectionObserver — leaving the viewport means the chips row is pinned → auto-collapse to one row) is a direct flex child of `.dshm-body` (flex column, gap:12px) and consumes an extra gap. The sentinel must stay in-flow to serve stickiness: naive alternatives were all rejected — removing it / `display:contents` kills the observer (permanent one-row state), wrapping sentinel+wrap in a div locks the sticky travel.
414
+ - **Change (single point, ZoneChips render tail)**: the sentinel moves inside `.dsvm-chipswrap` as an absolutely-positioned child (`position:sticky` is itself a positioning context), with `top:-5px` replicating the old geometry — the old sibling sentinel's top edge sat exactly 5px above the wrap's top edge, so the IO exit trigger is pixel-equivalent. A headless-Chromium probe verified the full stuck chain when pinned: wrapTop=0, sentinel at −5px out of viewport, IO `isIntersecting=false`. Spacing returns to `zoneBar |12| searchRow |12| chips`: search→chips 24.36px → 12px, header ~13px shorter, browse-mode rhythm now matches search mode.
415
+ - **Tests**: full suite 1200 cases with zero failures and a clean typecheck; geometry and sticky behavior verified via temporary headless probes, then removed (not added to the formal suite).
416
+
389
417
  ### Added in 0.9.51 — community entries gain a derived github field (icon coverage 45.6% → 100%)
390
418
 
391
419
  - **Motivation**: community npm-source cards always fell back to the initial letter — the adapter's npm branch only produced the `npm` field and discarded the GitHub repo mapping inside the upstream `url` (demoted to `homepage` only), so `github` stayed unset and the client Icon owner-avatar fallback (`github.com/<owner>.png?size=64`) never fired. Measured against `dsh-plugin-catalog@2026.1007.4837`: 2,298 of 2,298 npm-source entries among 4,226 adaptable ones (**100%**) carry a github.com url — pure data discarding, not an upstream gap.
package/lib/client.js CHANGED
@@ -28,6 +28,10 @@ var market_state_exports = {};
28
28
  __export(market_state_exports, {
29
29
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
30
30
  MARKET_PAGE_SIZES: () => MARKET_PAGE_SIZES,
31
+ autoExpandDecision: () => autoExpandDecision,
32
+ chipRows: () => chipRows,
33
+ clipTopOf: () => clipTopOf,
34
+ countBeyondRows: () => countBeyondRows,
31
35
  createZoneState: () => createZoneState,
32
36
  marketNotice: () => marketNotice,
33
37
  mergeLatestFields: () => mergeLatestFields,
@@ -230,6 +234,55 @@ function mergeLatestFields(nextItems, prevItems) {
230
234
  });
231
235
  return touched ? out : nextItems;
232
236
  }
237
+ function chipRows(offsets) {
238
+ if (!Array.isArray(offsets)) return 0;
239
+ let rows = 0;
240
+ let last = null;
241
+ for (const t of offsets) {
242
+ if (typeof t !== "number" || !Number.isFinite(t)) continue;
243
+ if (last === null || t !== last) {
244
+ rows += 1;
245
+ last = t;
246
+ }
247
+ }
248
+ return rows;
249
+ }
250
+ function countBeyondRows(offsets, maxRows) {
251
+ if (!Array.isArray(offsets)) return 0;
252
+ if (!Number.isInteger(maxRows) || maxRows < 1) return 0;
253
+ let beyond = 0;
254
+ let rowCount = 0;
255
+ let last = null;
256
+ for (const t of offsets) {
257
+ if (typeof t !== "number" || !Number.isFinite(t)) continue;
258
+ if (last === null || t !== last) {
259
+ rowCount += 1;
260
+ last = t;
261
+ }
262
+ if (rowCount > maxRows) beyond += 1;
263
+ }
264
+ return beyond;
265
+ }
266
+ function clipTopOf(offsets, maxRows) {
267
+ if (!Array.isArray(offsets)) return Infinity;
268
+ if (!Number.isInteger(maxRows) || maxRows < 1) return Infinity;
269
+ let rowCount = 0;
270
+ let last = null;
271
+ for (const t of offsets) {
272
+ if (typeof t !== "number" || !Number.isFinite(t)) continue;
273
+ if (last === null || t !== last) {
274
+ rowCount += 1;
275
+ last = t;
276
+ if (rowCount === maxRows + 1) return t;
277
+ }
278
+ }
279
+ return Infinity;
280
+ }
281
+ function autoExpandDecision(activeTop, clipTop, autoRefMatched, expanded) {
282
+ const beyond = typeof activeTop === "number" && Number.isFinite(activeTop) && typeof clipTop === "number" && Number.isFinite(clipTop) && activeTop >= clipTop;
283
+ if (!beyond || autoRefMatched) return { record: false, expand: false };
284
+ return { record: true, expand: !expanded };
285
+ }
233
286
  var MARKET_PAGE_SIZES, DEFAULT_PAGE_SIZE, CURATED_ORDER, CURATED_LABELS, CURATED_IDS, SLUG_RE, SORT_FIELDS, sameSort, FALLBACK_REGISTRY_STATE, FALLBACK_COMMUNITY, LATEST_MERGE_FIELDS;
234
287
  var init_market_state = __esm({
235
288
  "src/client/market-state.js"() {
@@ -1505,7 +1558,7 @@ var h = React.createElement;
1505
1558
  var { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } = React;
1506
1559
  var PLUGIN_ID = "dsh-m";
1507
1560
  var API = "/dshm";
1508
- 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 } = (init_market_state(), __toCommonJS(market_state_exports));
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));
1509
1562
  var { readMarketSnapshot: readMarketSnapshot2, writeMarketSnapshot: writeMarketSnapshot2, isDefaultFirstPageQuery: isDefaultFirstPageQuery2 } = (init_market_snapshot(), __toCommonJS(market_snapshot_exports));
1510
1563
  var { backdropCloseHandlers: backdropCloseHandlers2 } = (init_backdrop(), __toCommonJS(backdrop_exports));
1511
1564
  var { createMarkdown: createMarkdown2 } = (init_markdown(), __toCommonJS(markdown_exports));
@@ -2133,7 +2186,10 @@ var CSS = `
2133
2186
  .dshm-btn.sm{padding:3px 9px;font-size:11px}
2134
2187
  .dshm-input{flex:1;min-width:120px;border:1px solid var(--dsw-alias-border-l2,#c7d2fe);background:var(--dsw-alias-bg-layer-2,transparent);color:var(--dsw-alias-label-primary,inherit);border-radius:999px;padding:7px 14px;font:inherit;font-size:12px;outline:none}
2135
2188
  .dshm-input:focus{border-color:var(--dsw-alias-interactive-bg-selected,#4f46e5)}
2136
- .dshm-chips{display:flex;flex-wrap:wrap;gap:6px}
2189
+ .dshm-chips{display:flex;flex-wrap:wrap;gap:6px;position:relative}
2190
+ .dshm-chips-clip{overflow:hidden}
2191
+ .dshm-chips-gutter{padding-right:var(--dshm-clip-gutter,132px)}
2192
+ .dsvm-chipmore{position:absolute;right:8px;bottom:8px;display:flex;align-items:center;gap:8px;min-height:28px;background: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}
2137
2193
  .dsvm-searchrow{display:flex;align-items:center;gap:8px}
2138
2194
  .dsvm-searchrow .dshm-search{flex:1;display:flex}
2139
2195
  .dshm-search{position:relative}
@@ -2656,42 +2712,69 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing, wrapTitle }
2656
2712
  const chips = useMemo(() => zoneChips2(counts, labels, zone), [counts, labels, zone]);
2657
2713
  const [expanded, setExpanded] = useState(false);
2658
2714
  const [stuck, setStuck] = useState(false);
2659
- const [fit, setFit] = useState({ rows2: 99, rows1: 99 });
2715
+ const [geom, setGeom] = useState(null);
2716
+ const [rowHeight, setRowHeight] = useState(0);
2660
2717
  const wrapRef = useRef(null);
2661
2718
  const sentinelRef = useRef(null);
2662
2719
  const autoRef = useRef(null);
2663
- const collapsedBudget = Math.min(stuck ? fit.rows1 : fit.rows2, chips.length);
2720
+ const maxRows = stuck ? 1 : 2;
2664
2721
  const activeIdx = active ? chips.findIndex((c) => c.id === active) : -1;
2722
+ const maxH = maxRows * (rowHeight || 22) + (maxRows - 1) * 6;
2723
+ const measure = useCallback(() => {
2724
+ const el = wrapRef.current;
2725
+ if (!el || expanded) return;
2726
+ const tops = [];
2727
+ let rh = 0;
2728
+ for (const k of el.children) {
2729
+ if (!(k instanceof HTMLElement) || k.getAttribute("data-chip") !== "1") continue;
2730
+ tops.push(k.offsetTop);
2731
+ if (!rh && k.offsetHeight) rh = k.offsetHeight;
2732
+ }
2733
+ if (!tops.length) return;
2734
+ const next = { tops, clipTop: clipTopOf2(tops, maxRows), hiddenCount: countBeyondRows2(tops.slice(1), maxRows) };
2735
+ setGeom((prev) => prev && prev.tops.length === next.tops.length && prev.clipTop === next.clipTop && prev.hiddenCount === next.hiddenCount ? prev : next);
2736
+ setRowHeight((prev) => Math.abs(prev - rh) <= 0.5 ? prev : rh);
2737
+ }, [expanded, maxRows]);
2665
2738
  useEffect(() => {
2666
2739
  if (!active) {
2667
2740
  autoRef.current = null;
2668
2741
  setExpanded(false);
2669
- return;
2670
- }
2671
- if (activeIdx >= collapsedBudget && autoRef.current !== active) {
2672
- autoRef.current = active;
2673
- setExpanded(true);
2674
2742
  }
2675
- }, [active, activeIdx, collapsedBudget]);
2743
+ }, [active]);
2744
+ useEffect(() => {
2745
+ if (!active || !geom) return;
2746
+ const activeTop = geom.tops[activeIdx + 1];
2747
+ const d = autoExpandDecision2(activeTop, geom.clipTop, autoRef.current === active, expanded);
2748
+ if (d.record) autoRef.current = active;
2749
+ if (d.expand) setExpanded(true);
2750
+ }, [active, activeIdx, expanded, geom]);
2676
2751
  useLayoutEffect(() => {
2752
+ if (!expanded) measure();
2753
+ }, [chips.length, expanded, zone, stuck, rowHeight, measure]);
2754
+ useEffect(() => {
2677
2755
  const el = wrapRef.current;
2678
- if (!el || expanded) return;
2679
- let rows2 = 0;
2680
- let rows1 = 0;
2681
- let rowCount = 0;
2682
- let lastTop = null;
2683
- for (const k of el.children) {
2684
- if (!(k instanceof HTMLElement) || k.getAttribute("data-chip") !== "1") continue;
2685
- const t = k.offsetTop;
2686
- if (lastTop === null || t !== lastTop) {
2687
- rowCount += 1;
2688
- lastTop = t;
2689
- }
2690
- if (rowCount <= 2) rows2 += 1;
2691
- if (rowCount <= 1) rows1 += 1;
2692
- }
2693
- setFit({ rows2, rows1 });
2694
- }, [chips.length, expanded, zone]);
2756
+ if (!el || expanded || typeof ResizeObserver === "undefined") return;
2757
+ let raf = 0;
2758
+ const ro = new ResizeObserver(() => {
2759
+ cancelAnimationFrame(raf);
2760
+ raf = requestAnimationFrame(measure);
2761
+ });
2762
+ ro.observe(el);
2763
+ return () => {
2764
+ ro.disconnect();
2765
+ cancelAnimationFrame(raf);
2766
+ };
2767
+ }, [expanded, measure]);
2768
+ useEffect(() => {
2769
+ if (expanded || typeof document === "undefined" || !document.fonts?.ready) return;
2770
+ let cancelled = false;
2771
+ document.fonts.ready.then(() => {
2772
+ if (!cancelled) measure();
2773
+ });
2774
+ return () => {
2775
+ cancelled = true;
2776
+ };
2777
+ }, [expanded, measure]);
2695
2778
  useEffect(() => {
2696
2779
  const s = sentinelRef.current;
2697
2780
  if (!s || typeof IntersectionObserver === "undefined") return;
@@ -2699,34 +2782,62 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing, wrapTitle }
2699
2782
  io.observe(s);
2700
2783
  return () => io.disconnect();
2701
2784
  }, []);
2702
- const budget = expanded ? chips.length : collapsedBudget;
2703
- const shown = chips.slice(0, budget);
2704
- const hidden = chips.length - shown.length;
2705
- const btn = (c) => h(
2706
- "button",
2707
- { key: c.id, "data-chip": "1", className: `dshm-chip${active === c.id ? " on" : ""}${c.count === 0 ? " zero" : ""}`, onClick: () => onPick(active === c.id ? null : c.id) },
2708
- // 0.9.45 U3/U7:0 计数桶显式渲染「0」+ 降透明(社区区 chip 计数恒 >0,行为不变)
2709
- `${c.labelKey ? lookup(c.labelKey) : c.label} ${c.count || 0}`
2710
- );
2785
+ const clip = !expanded;
2786
+ const overlay = clip && (trailing != null || geom && geom.hiddenCount > 0);
2787
+ const clipTop = geom ? geom.clipTop : Infinity;
2788
+ const hiddenCount = geom ? geom.hiddenCount : 0;
2789
+ const btn = (c, i) => {
2790
+ const top = geom ? geom.tops[i + 1] : null;
2791
+ const clipped = clip && geom && typeof top === "number" && top >= clipTop;
2792
+ return h(
2793
+ "button",
2794
+ {
2795
+ key: c.id,
2796
+ "data-chip": "1",
2797
+ style: clipped ? { visibility: "hidden" } : void 0,
2798
+ className: `dshm-chip${active === c.id ? " on" : ""}${c.count === 0 ? " zero" : ""}`,
2799
+ onClick: () => onPick(active === c.id ? null : c.id)
2800
+ },
2801
+ // 0.9.45 U3/U7:0 计数桶显式渲染「0」+ 降透明(社区区 chip 计数恒 >0,行为不变)
2802
+ `${c.labelKey ? lookup(c.labelKey) : c.label} ${c.count || 0}`
2803
+ );
2804
+ };
2711
2805
  return h(
2712
2806
  React.Fragment,
2713
2807
  null,
2714
- h("div", { ref: sentinelRef, style: { height: "1px" } }),
2715
2808
  h(
2716
2809
  "div",
2717
2810
  { className: "dsvm-chipswrap" },
2811
+ // 吸顶检测哨兵(0.9.52):wrap 内 top:-5px 绝对定位,出视口 ⇒ stuck。⚠️ 裁剪宿主是
2812
+ // .dshm-chips(D3):wrap 决不可 overflow:hidden,否则哨兵被剪 → IO 恒 false → 永久吸顶态。
2813
+ h("div", {
2814
+ ref: sentinelRef,
2815
+ style: { position: "absolute", top: "-5px", left: 0, width: "1px", height: "1px", pointerEvents: "none" }
2816
+ }),
2718
2817
  h(
2719
2818
  "div",
2720
- { ref: wrapRef, className: "dshm-chips", title: wrapTitle || void 0 },
2819
+ {
2820
+ ref: wrapRef,
2821
+ className: `dshm-chips${clip ? " dshm-chips-clip" : ""}${clip && overlay ? " dshm-chips-gutter" : ""}`,
2822
+ title: wrapTitle || void 0,
2823
+ style: { maxHeight: clip ? `${maxH}px` : "none" }
2824
+ },
2721
2825
  h("button", { "data-chip": "1", className: `dshm-chip${active == null ? " on" : ""}`, onClick: () => onPick(null) }, lookup("cat.all")),
2722
- ...shown.map(btn),
2723
- hidden > 0 ? h(
2724
- "button",
2725
- { "data-chip": "1", className: "dshm-chip", onClick: () => setExpanded(!expanded) },
2726
- expanded ? "\u2303" : `+${hidden}`
2727
- ) : null,
2728
- trailing || null
2729
- )
2826
+ ...chips.map((c, i) => btn(c, i)),
2827
+ expanded ? [
2828
+ h("button", { key: "collapse", "data-chip": "1", className: "dshm-chip", onClick: () => setExpanded(false) }, "\u2303"),
2829
+ trailing || null
2830
+ ] : null
2831
+ ),
2832
+ // 折叠态右侧 overlay 组(D5/D6):[筛选][+N] 水平并排、钉末可见行右端;gutter 保证与 chip 零相交。
2833
+ // 吸顶单行态末可见行=第 1 行,两按钮仍同行并排(第④类碰撞结构性消除,R2-N1)。
2834
+ // ⚠️ 展开态不用 overlay(⌃/筛选 in-flow,与现状一致);弹层锚点 .dsvm-filterwrap 随组定位。
2835
+ clip && overlay ? h(
2836
+ "div",
2837
+ { className: "dsvm-chipmore" },
2838
+ trailing || null,
2839
+ hiddenCount > 0 ? h("button", { className: "dshm-chip", onClick: () => setExpanded(true) }, `+${hiddenCount}`) : null
2840
+ ) : null
2730
2841
  )
2731
2842
  );
2732
2843
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.51",
3
+ "version": "0.9.53",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/registry.json CHANGED
@@ -87,7 +87,7 @@
87
87
  {
88
88
  "id": "dsh-skip-browser-auth",
89
89
  "name": "DSH Skip Browser Auth",
90
- "description": "替换官方 Connection 跳过 BrowserAuth,免 Token;已适配 0.1.2-rc.1、0.1.5-rc.1/rc.2、0.1.7-rc.1/rc.2 与 0.2.0-rc.1/rc.2。",
90
+ "description": "替换官方 Connection 跳过 BrowserAuth,免 Token;已适配七个已审计精确版本对,详见仓库。仅支持 web profile;desktop profile(存在 dsh-desktop-host 的环境)自动休眠,属设计行为。",
91
91
  "category": "self-dev",
92
92
  "tags": ["BrowserAuth", "连接", "安全", "工具"],
93
93
  "verified": ["0.1.2-rc.1", "0.1.5-rc.1", "0.1.5-rc.2", "0.1.7-rc.1", "0.1.7-rc.2", "0.2.0-rc.1", "0.2.0-rc.2"],