dsh-m 0.9.64 → 0.9.65
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 +14 -0
- package/lib/client.js +67 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,13 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.65 优化:灯箱邻图预取 + 触屏滑动切换 + 焦点圈闭
|
|
12
|
+
|
|
13
|
+
- **邻图预取**:当前图加载完成(`useImgChain` settled)后静默预热左右邻——URL 用与灯箱真实请求**逐字节同形**的 `weservUrl(w=1600)`,翻页直接命中浏览器缓存近零等待。纯 best-effort:不碰赢家记忆/链状态,预取失败对真实链零影响(导航时照常走 weserv→原图双兜底);`prefetchedRef` Set 去重防来回翻页重复发起,Set 随灯箱挂载期生命周期(重开重新预热);单图由 `lbNeighbors` 空表自然短路。
|
|
14
|
+
- **触屏滑动切换**:touch 指针横向滑过阈值即翻页(`swipeDir` 纯函数判向:|dx| ≥ 48px 且 |dx| ≥ 2|dy|;不设时长门槛,慢拖也算;竖滑/斜滑归零——纵向手势与 pinch 缩放可达性留给原生,`touch-action:pan-y pinch-zoom`)。只认 `pointerType==="touch"`——鼠标拖拽维持原生语义(图=原生拖拽、遮罩=按下+点击才关,0.9.27 防拖拽误关守卫不破);滑动终结后 500ms 内根上 capture 吞掉合成 click(双保险:即便滑动止于 ✕/遮罩也不触发其点击语义)。判向/邻图两枚纯函数入 `lightbox.js`(`swipeDir`/`lbNeighbors`,Node 单测覆盖)。
|
|
15
|
+
- **焦点圈闭**:灯箱开着时 Tab/Shift+Tab 只在灯箱内可聚焦控件(按钮/链接:✕/‹›/失败态重试+打开原图)间回绕;焦点已逃出灯箱(如点击遮罩落在 body)时拉回首/尾。portal 挂 body 后 Tab 本会走到背景面板,此处收口;DetailModal 自身 U8 债(无圈闭)维持不变,Esc 仍只关最上层弹层。
|
|
16
|
+
- **验证**:1289 项测试全绿、typecheck 零错误;`client-lightbox.test.mjs` 新增 6 项(`lbNeighbors`/`swipeDir` 纯逻辑单测 + 预取/滑动/圈闭/touch-action 结构锚),0.9.60/0.9.62 批次既有锚全数保持。
|
|
17
|
+
|
|
11
18
|
### 0.9.64 优化:设置页双清单并排重排 + 操作记录整体搬迁设置 tab
|
|
12
19
|
|
|
13
20
|
- **设置页网格(mock 驱动终审)**:社区清单/精选清单两卡并排 **4:7**(社区卡只读五项 ~250px 足矣,宽度让给交互更多的精选卡);全屏态「关于」升入三卡行(`4fr 7fr 3fr`,不封顶——对齐市场「加列不加宽」哲学,设置页恰有三个卡片单元可用);视口 ≤760px 单列回落;关于卡常态通栏、操作记录恒通栏(`nth-child` 定位,与 `Section()` 签名解耦)。
|
|
@@ -475,6 +482,13 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
475
482
|
|
|
476
483
|
## English
|
|
477
484
|
|
|
485
|
+
### Added in 0.9.65 — lightbox: neighbor prefetch + touch swipe navigation + focus trap
|
|
486
|
+
|
|
487
|
+
- **Neighbor prefetch**: once the current image settles (`useImgChain` settled), the left/right neighbors are warmed silently - using a `weservUrl(w=1600)` URL **byte-identical** to the lightbox's real request, so a swipe/arrow navigation hits the browser cache with near-zero wait. Strictly best-effort: no winner-memory or chain-state mutation, a failed prefetch leaves the real chain untouched (navigation still runs the weserv→original dual fallback); a `prefetchedRef` Set dedupes repeated crossings (no re-issuing when bouncing back), the Set lives for the lightbox mount lifetime (a fresh open warms again); single-image boxes short-circuit naturally via `lbNeighbors` returning an empty list.
|
|
488
|
+
- **Touch swipe navigation**: a horizontal touch drag past the threshold flips images (`swipeDir` pure predicate: |dx| ≥ 48px and |dx| ≥ 2|dy|; no time gate - slow deliberate drags count; vertical/diagonal drags return 0 - vertical gestures and pinch-zoom accessibility stay native, via `touch-action:pan-y pinch-zoom`). Only `pointerType==="touch"` is recognized - mouse drags keep their native semantics (image = native drag, backdrop = press+click to close, the 0.9.27 drag-misfire guard intact); for 500ms after a recognized swipe the root's capture phase swallows the synthetic click (double belt: a swipe ending on ✕ or the backdrop triggers neither).
|
|
489
|
+
- **Focus trap**: while the lightbox is open, Tab/Shift+Tab cycles only among focusable controls inside it (buttons/links: ✕/‹›/failed-state retry + open-original); if focus has escaped (e.g. a backdrop click left it on body), the next Tab pulls it back to the first/last control. After the portal-to-body change, Tab used to walk into the background panel - this closes that gap; DetailModal's own U8 debt (no trap) is unchanged and Esc still closes only the topmost layer.
|
|
490
|
+
- **Verification**: 1289 tests green, typecheck clean; `client-lightbox.test.mjs` gains 6 items (`lbNeighbors`/`swipeDir` pure-logic units + prefetch/swipe/trap/touch-action structural anchors); all 0.9.60/0.9.62-era anchors retained.
|
|
491
|
+
|
|
478
492
|
### Changed in 0.9.64 — settings page: dual-catalog side-by-side layout + operations history moved into the settings tab
|
|
479
493
|
|
|
480
494
|
- **Settings grid (mock-driven final review)**: the Community and Curated cards now sit side by side at **4:7** (the read-only community card needs ~250px; width goes to the more interactive registry card); in fullscreen the About card joins a three-card row (`4fr 7fr 3fr`, uncapped — the same "add columns, don't stretch units" philosophy as the market tab); below a 760px viewport it falls back to a single column; About spans full width in normal mode, the operations card is always full width.
|
package/lib/client.js
CHANGED
|
@@ -466,12 +466,28 @@ var init_backdrop = __esm({
|
|
|
466
466
|
// src/client/lightbox.js
|
|
467
467
|
var lightbox_exports = {};
|
|
468
468
|
__export(lightbox_exports, {
|
|
469
|
-
|
|
469
|
+
lbNeighbors: () => lbNeighbors,
|
|
470
|
+
lbStep: () => lbStep,
|
|
471
|
+
swipeDir: () => swipeDir
|
|
470
472
|
});
|
|
471
473
|
function lbStep(index, step, length) {
|
|
472
474
|
if (!Number.isFinite(index) || !Number.isFinite(step) || !Number.isInteger(length) || length <= 1) return index;
|
|
473
475
|
return ((index + step) % length + length) % length;
|
|
474
476
|
}
|
|
477
|
+
function lbNeighbors(index, length) {
|
|
478
|
+
if (!Number.isFinite(index) || !Number.isInteger(length) || length <= 1) return [];
|
|
479
|
+
const out = [];
|
|
480
|
+
for (const n of [lbStep(index, 1, length), lbStep(index, -1, length)]) {
|
|
481
|
+
if (Number.isFinite(n) && !out.includes(n)) out.push(n);
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
function swipeDir(dx, dy, { threshold = 48, ratio = 2 } = {}) {
|
|
486
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) return 0;
|
|
487
|
+
const ax = Math.abs(dx);
|
|
488
|
+
if (ax < threshold || ax < ratio * Math.abs(dy)) return 0;
|
|
489
|
+
return dx > 0 ? 1 : -1;
|
|
490
|
+
}
|
|
475
491
|
var init_lightbox = __esm({
|
|
476
492
|
"src/client/lightbox.js"() {
|
|
477
493
|
"use strict";
|
|
@@ -1651,7 +1667,7 @@ var API = "/dshm";
|
|
|
1651
1667
|
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, visibleRowTop: visibleRowTop2, autoExpandDecision: autoExpandDecision2 } = (init_market_state(), __toCommonJS(market_state_exports));
|
|
1652
1668
|
var { readMarketSnapshot: readMarketSnapshot2, writeMarketSnapshot: writeMarketSnapshot2, isDefaultFirstPageQuery: isDefaultFirstPageQuery2 } = (init_market_snapshot(), __toCommonJS(market_snapshot_exports));
|
|
1653
1669
|
var { backdropCloseHandlers: backdropCloseHandlers2 } = (init_backdrop(), __toCommonJS(backdrop_exports));
|
|
1654
|
-
var { lbStep: lbStep2 } = (init_lightbox(), __toCommonJS(lightbox_exports));
|
|
1670
|
+
var { lbStep: lbStep2, lbNeighbors: lbNeighbors2, swipeDir: swipeDir2 } = (init_lightbox(), __toCommonJS(lightbox_exports));
|
|
1655
1671
|
var { WESERV_TIMEOUT_MS: WESERV_TIMEOUT_MS2, weservUrl: weservUrl2, serviceBucketOf: serviceBucketOf2, tierOrder: tierOrder2, nextTier: nextTier2, needsTimeout: needsTimeout2, preferredTier: preferredTier2, rememberSuccess: rememberSuccess2 } = (init_img_chain(), __toCommonJS(img_chain_exports));
|
|
1656
1672
|
var { createMarkdown: createMarkdown2 } = (init_markdown(), __toCommonJS(markdown_exports));
|
|
1657
1673
|
var { ExtLink, MdImg, renderMarkdown } = createMarkdown2(h);
|
|
@@ -2374,8 +2390,10 @@ var CSS = `
|
|
|
2374
2390
|
\u5728\u6D45\u5E95\u4E0A\u4E0D\u53EF\u89C1\uFF08\u7528\u6237\u53CC\u6A21\u5F0F\u5B9E\u62CD\uFF09\u3002\u53CC\u4FA7\u4FEE\u590D\u2014\u2014\u76AE\u80A4\u4FA7 :not(.dsvm-lightbox) \u8C41\u514D
|
|
2375
2391
|
\uFF08dsh-skins 1.5.1 \u8D77\uFF09\uFF1B\u672C\u4FA7 background !important \u81EA\u6301\uFF1Amarketplace \u793E\u533A\u76AE\u80A4\u4E0D\u53EF
|
|
2376
2392
|
\u679A\u4E3E\uFF0C\u5F71\u9662\u4E0D\u53D8\u91CF\u5FC5\u987B\u7531\u7EC4\u4EF6\u81EA\u5DF1\u515C\u5E95\u3002!important \u6E05\u5355\u5B9A\u957F\uFF08\u6CE8\u91CA\u63D0\u53CA\xD72 + \u672C\u89C4\u5219
|
|
2377
|
-
+ sidebar footer \u517C\u5BB9\u89C4\u5219\uFF09\uFF0C\u65B0\u589E\u987B\u6539 client-lightbox \u65AD\u8A00\u663E\u5F0F\u767B\u8BB0\uFF0C\u6CE8\u91CA\u5373\u5B88\u536B\u3002
|
|
2378
|
-
.
|
|
2393
|
+
+ sidebar footer \u517C\u5BB9\u89C4\u5219\uFF09\uFF0C\u65B0\u589E\u987B\u6539 client-lightbox \u65AD\u8A00\u663E\u5F0F\u767B\u8BB0\uFF0C\u6CE8\u91CA\u5373\u5B88\u536B\u3002
|
|
2394
|
+
0.9.65\uFF1Atouch-action:pan-y pinch-zoom\u2014\u2014\u6A2A\u6ED1\u5224\u5411\u4EA4\u7ED9\u706F\u7BB1\u6ED1\u52A8\u5207\u6362\uFF0C\u7EB5\u5411\u539F\u751F
|
|
2395
|
+
\u624B\u52BF\u4E0E pinch \u7F29\u653E\uFF08\u4F4E\u89C6\u529B\u53EF\u8FBE\u6027\uFF09\u4E0D\u88AB\u52AB\u6301\u3002 */
|
|
2396
|
+
.dsvm-lightbox{position:fixed;top:var(--dsh-windows-titlebar-height,0px);left:0;right:0;bottom:0;z-index:2147483200;background:rgba(0,0,0,.88)!important;display:flex;align-items:center;justify-content:center;touch-action:pan-y pinch-zoom}
|
|
2379
2397
|
.dsvm-lightbox img{max-width:min(92vw,1500px);max-height:74vh;border-radius:8px;cursor:zoom-out;box-shadow:0 10px 44px rgba(0,0,0,.55);display:block}
|
|
2380
2398
|
.dsvm-btn:focus-visible{outline:2px solid rgba(255,255,255,.75);outline-offset:2px}
|
|
2381
2399
|
.dsvm-btn.dsvm-lbclose{position:absolute;top:12px;right:12px;width:38px;height:38px;padding:0;border-radius:10px;display:flex;align-items:center;justify-content:center}
|
|
@@ -3191,9 +3209,40 @@ function Lightbox({ shots, index, onNav, onClose }) {
|
|
|
3191
3209
|
if (closeRef.current) closeRef.current.focus();
|
|
3192
3210
|
}, []);
|
|
3193
3211
|
const chain = useImgChain(shots[index], { w: 1600 });
|
|
3212
|
+
const prefetchedRef = useRef(null);
|
|
3213
|
+
if (prefetchedRef.current === null) prefetchedRef.current = /* @__PURE__ */ new Set();
|
|
3214
|
+
useEffect(() => {
|
|
3215
|
+
if (typeof Image === "undefined") return;
|
|
3216
|
+
if (!chain.settled || chain.failed) return;
|
|
3217
|
+
for (const n of lbNeighbors2(index, shots.length)) {
|
|
3218
|
+
const url = shots[n];
|
|
3219
|
+
if (typeof url !== "string" || url === "" || prefetchedRef.current.has(url)) continue;
|
|
3220
|
+
prefetchedRef.current.add(url);
|
|
3221
|
+
const img = new Image();
|
|
3222
|
+
img.decoding = "async";
|
|
3223
|
+
img.src = weservUrl2(url, { w: 1600 });
|
|
3224
|
+
}
|
|
3225
|
+
}, [chain.settled, chain.failed, index, shots]);
|
|
3226
|
+
const swipeRef = useRef(null);
|
|
3227
|
+
const swipedAtRef = useRef(0);
|
|
3228
|
+
const onPointerDown = (e) => {
|
|
3229
|
+
if (e.pointerType !== "touch") return;
|
|
3230
|
+
swipeRef.current = { x: e.clientX, y: e.clientY };
|
|
3231
|
+
};
|
|
3232
|
+
const onPointerUp = (e) => {
|
|
3233
|
+
if (e.pointerType !== "touch" || !swipeRef.current) return;
|
|
3234
|
+
const dir = swipeDir2(e.clientX - swipeRef.current.x, e.clientY - swipeRef.current.y);
|
|
3235
|
+
swipeRef.current = null;
|
|
3236
|
+
if (!dir || single) return;
|
|
3237
|
+
swipedAtRef.current = Date.now();
|
|
3238
|
+
onNav(lbStep2(index, dir, shots.length));
|
|
3239
|
+
};
|
|
3240
|
+
const onClickCapture = (e) => {
|
|
3241
|
+
if (Date.now() - swipedAtRef.current < 500) e.stopPropagation();
|
|
3242
|
+
};
|
|
3194
3243
|
const node = h(
|
|
3195
3244
|
"div",
|
|
3196
|
-
{ className: "dsvm-lightbox", role: "dialog", "aria-modal": "true", "aria-label": lookup("lb.title"), ...backdropCloseHandlers2(onClose) },
|
|
3245
|
+
{ className: "dsvm-lightbox", role: "dialog", "aria-modal": "true", "aria-label": lookup("lb.title"), onPointerDown, onPointerUp, onClickCapture, ...backdropCloseHandlers2(onClose) },
|
|
3197
3246
|
chain.failed ? h(
|
|
3198
3247
|
"div",
|
|
3199
3248
|
{ className: "dsvm-lbfail", onClick: (e) => e.stopPropagation() },
|
|
@@ -3283,6 +3332,19 @@ function DetailModal({ it, labels, busy, onClose, onInstall, onUpgrade, upgradeB
|
|
|
3283
3332
|
if (e.key === "ArrowLeft") setLb((i) => lbStep2(i, -1, visible.length));
|
|
3284
3333
|
if (e.key === "ArrowRight") setLb((i) => lbStep2(i, 1, visible.length));
|
|
3285
3334
|
}
|
|
3335
|
+
if (e.key === "Tab" && lb !== null) {
|
|
3336
|
+
const box = typeof document !== "undefined" ? document.querySelector(".dsvm-lightbox") : null;
|
|
3337
|
+
if (!box) return;
|
|
3338
|
+
const focusables = Array.from(box.querySelectorAll("button, a[href]")).filter((el) => el.offsetWidth > 0 && el.offsetHeight > 0);
|
|
3339
|
+
if (!focusables.length) return;
|
|
3340
|
+
const first = focusables[0];
|
|
3341
|
+
const last = focusables[focusables.length - 1];
|
|
3342
|
+
const active = document.activeElement;
|
|
3343
|
+
if (!box.contains(active) || (e.shiftKey ? active === first : active === last)) {
|
|
3344
|
+
e.preventDefault();
|
|
3345
|
+
(e.shiftKey ? last : first).focus();
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3286
3348
|
};
|
|
3287
3349
|
document.addEventListener("keydown", onKey);
|
|
3288
3350
|
return () => document.removeEventListener("keydown", onKey);
|