dsh-focus-overlay 0.9.0 → 0.10.0
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/lib/client.js +108 -13
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -314,6 +314,41 @@ window.__ModuleLoader__.load({
|
|
|
314
314
|
if (distance >= 160) return false;
|
|
315
315
|
return prev;
|
|
316
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Index of the conversation-turn dot the nav rail should highlight, from live
|
|
319
|
+
* scroll geometry. The rail is a position indicator, so the decision is pure
|
|
320
|
+
* math over measured anchor tops (document order) plus the distance to the
|
|
321
|
+
* scroll end:
|
|
322
|
+
*
|
|
323
|
+
* 1. Bottom force: within {@link BOTTOM_ENTER_PX} of the scroll end the last
|
|
324
|
+
* turn owns the highlight, whatever its height. Without this rule a short
|
|
325
|
+
* final turn could never light its own dot — with less than a viewport of
|
|
326
|
+
* content below its anchor, that anchor physically cannot reach the top
|
|
327
|
+
* threshold even pinned at the very bottom, so the highlight would stick
|
|
328
|
+
* to an earlier turn forever.
|
|
329
|
+
* 2. Geometric scan: the last anchor whose top sits at/above the
|
|
330
|
+
* {@link NAV_TOP_PX} line — "which turn is at the top of the viewport".
|
|
331
|
+
* Non-finite entries (a stale anchor slot after the item list shifted) are
|
|
332
|
+
* skipped without stopping the scan, matching the old ref-null behavior.
|
|
333
|
+
* 3. Top clamp: every real anchor still below the line means the reader is
|
|
334
|
+
* above the first message's threshold (scrollTop ≈ 0 with leading
|
|
335
|
+
* padding), and the first turn owns the position.
|
|
336
|
+
*
|
|
337
|
+
* A bottom-forced index may land on a stale slot; the caller maps it back to
|
|
338
|
+
* the nearest existing anchor.
|
|
339
|
+
*/
|
|
340
|
+
function activeNavIndex(tops, bottomDistance, topPx = 96, bottomPx = 48) {
|
|
341
|
+
if (!tops.length) return -1;
|
|
342
|
+
if (bottomDistance <= bottomPx) return tops.length - 1;
|
|
343
|
+
let cur = -1;
|
|
344
|
+
for (let i = 0; i < tops.length; i++) {
|
|
345
|
+
const top = tops[i];
|
|
346
|
+
if (!Number.isFinite(top)) continue;
|
|
347
|
+
if (top <= topPx) cur = i;
|
|
348
|
+
else break;
|
|
349
|
+
}
|
|
350
|
+
return cur >= 0 ? cur : 0;
|
|
351
|
+
}
|
|
317
352
|
/** A question is answered when at least one option is selected or the custom
|
|
318
353
|
* text is non-blank — the same completeness rule the official question
|
|
319
354
|
* composer applies before enabling submit. */
|
|
@@ -1000,20 +1035,19 @@ window.__ModuleLoader__.load({
|
|
|
1000
1035
|
const focusBarOnce = (0, react.useRef)(false);
|
|
1001
1036
|
const computeActive = () => {
|
|
1002
1037
|
if (!bodyEl) return null;
|
|
1003
|
-
|
|
1004
|
-
|
|
1038
|
+
const bodyTop = bodyEl.getBoundingClientRect().top;
|
|
1039
|
+
const i = activeNavIndex(navKeys.map((k) => {
|
|
1005
1040
|
const el = anchors[k];
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
return cur;
|
|
1041
|
+
return el ? el.getBoundingClientRect().top - bodyTop : Number.POSITIVE_INFINITY;
|
|
1042
|
+
}), bottomDistance());
|
|
1043
|
+
for (let j = i; j >= 0; j--) if (anchors[navKeys[j]]) return navKeys[j];
|
|
1044
|
+
return null;
|
|
1011
1045
|
};
|
|
1012
1046
|
const bottomDistance = () => {
|
|
1013
1047
|
if (!bodyEl) return Number.POSITIVE_INFINITY;
|
|
1014
1048
|
return bodyEl.scrollHeight - bodyEl.scrollTop - bodyEl.clientHeight;
|
|
1015
1049
|
};
|
|
1016
|
-
const
|
|
1050
|
+
const scheduleRecompute = () => {
|
|
1017
1051
|
if (rafId.current !== null) return;
|
|
1018
1052
|
rafId.current = requestAnimationFrame(() => {
|
|
1019
1053
|
rafId.current = null;
|
|
@@ -1021,6 +1055,10 @@ window.__ModuleLoader__.load({
|
|
|
1021
1055
|
setZone((prev) => bottomZoneAfter(prev, bottomDistance()));
|
|
1022
1056
|
});
|
|
1023
1057
|
};
|
|
1058
|
+
const scheduleRef = (0, react.useRef)(scheduleRecompute);
|
|
1059
|
+
(0, react.useEffect)(() => {
|
|
1060
|
+
scheduleRef.current = scheduleRecompute;
|
|
1061
|
+
});
|
|
1024
1062
|
const scrollToKey = (key, smooth) => {
|
|
1025
1063
|
const el = anchors[key];
|
|
1026
1064
|
if (!el || !bodyEl) return;
|
|
@@ -1081,6 +1119,22 @@ window.__ModuleLoader__.load({
|
|
|
1081
1119
|
(0, react.useEffect)(() => {
|
|
1082
1120
|
setZone(bodyEl ? bottomZoneAfter(false, bottomDistance()) : true);
|
|
1083
1121
|
}, []);
|
|
1122
|
+
(0, react.useEffect)(() => {
|
|
1123
|
+
const el = bodyEl;
|
|
1124
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
1125
|
+
const ro = new ResizeObserver(() => scheduleRef.current());
|
|
1126
|
+
ro.observe(el);
|
|
1127
|
+
return () => ro.disconnect();
|
|
1128
|
+
}, []);
|
|
1129
|
+
(0, react.useEffect)(() => {
|
|
1130
|
+
scheduleRef.current();
|
|
1131
|
+
}, [snap, prefs.width]);
|
|
1132
|
+
(0, react.useEffect)(() => () => {
|
|
1133
|
+
if (rafId.current !== null) {
|
|
1134
|
+
cancelAnimationFrame(rafId.current);
|
|
1135
|
+
rafId.current = null;
|
|
1136
|
+
}
|
|
1137
|
+
}, []);
|
|
1084
1138
|
(0, react.useEffect)(() => {
|
|
1085
1139
|
overlayRef.current?.focus();
|
|
1086
1140
|
}, []);
|
|
@@ -1272,7 +1326,23 @@ window.__ModuleLoader__.load({
|
|
|
1272
1326
|
}, "fm-partial"));
|
|
1273
1327
|
if (running) kids.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1274
1328
|
className: "fm-running",
|
|
1275
|
-
|
|
1329
|
+
role: "status",
|
|
1330
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1331
|
+
className: "fm-running-text",
|
|
1332
|
+
children: [
|
|
1333
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1334
|
+
className: "fm-running-dash",
|
|
1335
|
+
"aria-hidden": "true",
|
|
1336
|
+
children: "–"
|
|
1337
|
+
}),
|
|
1338
|
+
t("running"),
|
|
1339
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1340
|
+
className: "fm-running-dash",
|
|
1341
|
+
"aria-hidden": "true",
|
|
1342
|
+
children: "–"
|
|
1343
|
+
})
|
|
1344
|
+
]
|
|
1345
|
+
})
|
|
1276
1346
|
}, "fm-running"));
|
|
1277
1347
|
body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1278
1348
|
className: "fm-inner",
|
|
@@ -1371,7 +1441,7 @@ window.__ModuleLoader__.load({
|
|
|
1371
1441
|
ref: (el) => {
|
|
1372
1442
|
bodyEl = el;
|
|
1373
1443
|
},
|
|
1374
|
-
onScroll:
|
|
1444
|
+
onScroll: scheduleRecompute,
|
|
1375
1445
|
children: body
|
|
1376
1446
|
}),
|
|
1377
1447
|
nav,
|
|
@@ -1651,7 +1721,32 @@ window.__ModuleLoader__.load({
|
|
|
1651
1721
|
.fm-image{max-width:100%;border-radius:8px;margin:8px 0}
|
|
1652
1722
|
.fm-hidden{text-align:center;color:var(--dsw-alias-label-secondary,#888);font-size:12px;line-height:1.6;margin:6px 0 18px;user-select:none}
|
|
1653
1723
|
.fm-error{color:var(--dsw-alias-state-error-primary,#d23);font-size:13px;white-space:pre-wrap}
|
|
1654
|
-
|
|
1724
|
+
/* "正在工作" live line — the shimmer is a 1:1 replica of the official
|
|
1725
|
+
generating indicator in dsh-client-ui-conversation (the "Deep Diving"
|
|
1726
|
+
turn-status): brand-blue text (--dsw-static-deepseek-500) with a light
|
|
1727
|
+
highlight (--dsw-static-deepseek-200) sweeping through — gradient stops
|
|
1728
|
+
0/40/50/60/100 on a 250% tile, background-position 100%→0, 1.8s linear,
|
|
1729
|
+
font --dsw-font-s-strong-14 (500 14px/22px). The official class name
|
|
1730
|
+
(Md3f7G_turnStatus) is a per-build CSS-modules hash, so reusing it would
|
|
1731
|
+
break silently on dsh updates — the declarations are copied verbatim onto
|
|
1732
|
+
our own class using the same theme tokens instead. The flanking short
|
|
1733
|
+
dashes are part of the same shimmer span, so the sweep covers dashes and
|
|
1734
|
+
label as one unit. Fallbacks: no background-clip:text → secondary gray
|
|
1735
|
+
label; prefers-reduced-motion → animation off, label stays solid
|
|
1736
|
+
deepseek-500 (the official base color). */
|
|
1737
|
+
.fm-running{text-align:center;margin:10px 0 20px;user-select:none;animation:fm-up .18s ease}
|
|
1738
|
+
.fm-running-text{color:var(--dsw-alias-label-secondary,#888);font:var(--dsw-font-s-strong-14,500 14px/22px var(--dsw-font-family,sans-serif));white-space:nowrap}
|
|
1739
|
+
/* Dashes: separate spans with fixed margins instead of space characters — in
|
|
1740
|
+
CJK/Latin mixed runs the browser itemizes the two U+0020 spaces into
|
|
1741
|
+
different fonts (one Latin-narrow, one Han-wide), and the Han font's en
|
|
1742
|
+
dash glyph is left-biased, which made the two gaps visually unequal. The
|
|
1743
|
+
forced Latin-first stack renders a symmetric en dash; margins are exact. */
|
|
1744
|
+
.fm-running-dash{font-family:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;margin:0 8px}
|
|
1745
|
+
@supports ((-webkit-background-clip:text) or (background-clip:text)){
|
|
1746
|
+
.fm-running-text{background:linear-gradient(90deg,var(--dsw-static-deepseek-500,#4176e6) 0%,var(--dsw-static-deepseek-500,#4176e6) 40%,var(--dsw-static-deepseek-200,#d3e2ff) 50%,var(--dsw-static-deepseek-500,#4176e6) 60%,var(--dsw-static-deepseek-500,#4176e6) 100%);color:#0000;-webkit-text-fill-color:transparent;background-position:100% 0;background-size:250% 100%;-webkit-background-clip:text;background-clip:text;animation:fm-sweep 1.8s linear infinite}
|
|
1747
|
+
}
|
|
1748
|
+
@keyframes fm-sweep{to{background-position:0 0}}
|
|
1749
|
+
@media (prefers-reduced-motion:reduce){.fm-running{animation:none}.fm-running-text{background:0 0;color:var(--dsw-static-deepseek-500,#4176e6);-webkit-text-fill-color:currentColor;animation:none}}
|
|
1655
1750
|
.fm-empty{color:var(--dsw-alias-label-secondary,#888);padding:48px 16px;text-align:center}
|
|
1656
1751
|
.fm-nav{position:absolute;right:20px;top:50%;transform:translateY(-50%);display:block;width:16px;z-index:2;pointer-events:auto;transition:transform .18s ease}
|
|
1657
1752
|
.fm-nav:hover{transform:translateY(-50%) translateX(-6px)}
|
|
@@ -1796,7 +1891,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1796
1891
|
"settings.width.hint": "专注模式正文的阅读列宽(480–1200px)",
|
|
1797
1892
|
"empty": "暂无打开的会话",
|
|
1798
1893
|
"loading": "正在读取会话…",
|
|
1799
|
-
"running": "
|
|
1894
|
+
"running": "正在工作",
|
|
1800
1895
|
"reply.ready": "新回复已生成",
|
|
1801
1896
|
"reply.view": "查看",
|
|
1802
1897
|
"reply.waiting": "AI 正在等待你的回复",
|
|
@@ -1865,7 +1960,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1865
1960
|
"settings.width.hint": "Reading column width (480–1200px)",
|
|
1866
1961
|
"empty": "No open session",
|
|
1867
1962
|
"loading": "Reading session…",
|
|
1868
|
-
"running": "
|
|
1963
|
+
"running": "Working",
|
|
1869
1964
|
"reply.ready": "New reply ready",
|
|
1870
1965
|
"reply.view": "View",
|
|
1871
1966
|
"reply.waiting": "AI is waiting for your reply",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-focus-overlay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Focus Mode for the dsh web GUI: a full-screen reading overlay that hides chrome and folds the AI tool-call flow into summaries, reusing the official Markdown / image primitives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.mjs",
|