@cowwo/dsh-icon-custom 0.9.0 → 0.9.2
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/README.zh.md +2 -2
- package/client/client.js +238 -53
- package/package.json +1 -1
package/README.zh.md
CHANGED
|
@@ -58,12 +58,12 @@ SVG 会在浏览器里先栅格化成 512×512 PNG 再上传,ICO 则由后端提
|
|
|
58
58
|
规则:
|
|
59
59
|
|
|
60
60
|
- 数字 = **几个会话**在等你(不是几件事);同一个会话只占一格。
|
|
61
|
-
-
|
|
61
|
+
- 只有子代理会话不计。**你正开着的那个会话也算**——窗口可能被最小化、页面可能切到后台,这里无从判断你在不在看,所以不做这个区分;要清掉它,切走再切回来即可。
|
|
62
62
|
- **已读记在每个浏览器自己身上**(localStorage):打开过 = 处理过,刷新/重启不会"复活";装插件之前的历史结束也不算。
|
|
63
63
|
- 数据全部来自官方:`turn/end` 事件经官方 session projection 折叠,加上官方的"等待交互"注册表——**不依赖任何其他插件**。
|
|
64
64
|
- 设置页的**数据来源**可以切到"手动测试",手填数字试看效果。
|
|
65
65
|
- **工作区和会话行上的红点(实验)**:侧栏每个**工作区**那一行的文件夹图标右上角一个小红点,每个**有未读的会话**在标题右边跟一个小红点——一眼看出"事情在哪个工作区的哪个会话"。这一项是**贴着页面结构**做的——DSH 大版本升级后**可能失效**,失效时只会变成"不显示",**不影响**红点数字、清单和跳转;设置页里有一个开关可以随时关掉它。
|
|
66
|
-
- **想知道具体是哪个会话?**
|
|
66
|
+
- **想知道具体是哪个会话?** 侧栏「工作区」标题的**右上角**有一个**红底数字**(没有待处理时是灰色 0,位置不变);点开是待处理会话清单,**按工作区分组**(工作区名 · 会话名 · 在等你/刚结束),点其中一条**直接跳到那个会话**。
|
|
67
67
|
|
|
68
68
|
## 支持的格式
|
|
69
69
|
|
package/client/client.js
CHANGED
|
@@ -62,7 +62,7 @@ window.__ModuleLoader__.load({
|
|
|
62
62
|
unreadPanelOther: "其他",
|
|
63
63
|
workspaceDotLabel: "在工作区和会话行上显示红点(实验)",
|
|
64
64
|
workspaceDotHint: "在侧栏工作区那一行的文件夹图标上点一个小红点。这一项是贴着页面结构做的,DSH 升级后可能失效;失效时只会变成\"不显示\",不会影响红点数字、清单、跳转这些功能,随时可以关掉它。",
|
|
65
|
-
badgeTestHint: "真实未读 = 有会话发生了上面勾选的情况、而且你还没看过它(打开该会话即视为已读)
|
|
65
|
+
badgeTestHint: "真实未读 = 有会话发生了上面勾选的情况、而且你还没看过它(打开该会话即视为已读)。你正开着的会话也算——窗口可能被最小化、页面可能切到后台,这里无从判断你在不在看,所以不做这个区分。只有子代理不计。手动模式:自己填数字试看效果。刷新页面后回到真实未读。"
|
|
66
66
|
};
|
|
67
67
|
const en = {
|
|
68
68
|
nav: "Favicon",
|
|
@@ -117,7 +117,7 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
unreadPanelOther: "Other",
|
|
118
118
|
workspaceDotLabel: "Dots on workspace and session rows (experimental)",
|
|
119
119
|
workspaceDotHint: "Adds a small red dot to the folder icon of each workspace row. This one reads the page structure, so a DSH upgrade may break it; when it does it simply stops showing, never affecting the counts, the list, or navigation. Turn it off any time.",
|
|
120
|
-
badgeTestHint: "Real unread = a session ended for one of the checked reasons and you have not looked at it yet (opening a session marks it read). The session you are viewing and sub-agents never count. Manual = type a number to preview. Resets to Real on reload."
|
|
120
|
+
badgeTestHint: "Real unread = a session ended for one of the checked reasons and you have not looked at it yet (opening a session marks it read). The session you are viewing counts too — the window may be minimised or the page in the background, so this half cannot tell whether you are looking, and does not pretend to. Only sub-agents never count. Manual = type a number to preview. Resets to Real on reload."
|
|
121
121
|
};
|
|
122
122
|
//#endregion
|
|
123
123
|
|
|
@@ -329,8 +329,15 @@ window.__ModuleLoader__.load({
|
|
|
329
329
|
/**
|
|
330
330
|
* How many sessions deserve the badge right now: endings whose reason is
|
|
331
331
|
* enabled and that this browser has not seen since, plus sessions waiting
|
|
332
|
-
* for you (when that source is enabled).
|
|
333
|
-
*
|
|
332
|
+
* for you (when that source is enabled). Only sub-agent sessions never count.
|
|
333
|
+
*
|
|
334
|
+
* The session you are LOOKING AT counts too. It used to be excluded, on the
|
|
335
|
+
* theory that "you have it open, so you have seen it" — but that premise is
|
|
336
|
+
* not knowable from here: the window may be minimised, the tab may be in the
|
|
337
|
+
* background, the page may be on another desktop. The browser half cannot
|
|
338
|
+
* tell "watching" from "left open", and a rule that cannot tell two cases
|
|
339
|
+
* apart should not pretend to: a session counts from the moment it ends until
|
|
340
|
+
* it is opened again. Leaving and re-entering it is what clears the entry.
|
|
334
341
|
* @param list - the session list snapshot (may be absent).
|
|
335
342
|
* @param pending - the pending-interaction map (may be absent).
|
|
336
343
|
* @param config - the unread rule to apply.
|
|
@@ -344,7 +351,6 @@ window.__ModuleLoader__.load({
|
|
|
344
351
|
/** { id, title, waiting, kind, at } — newest first, "in wait" ahead of "ended". */
|
|
345
352
|
const hits = [];
|
|
346
353
|
const ids = new Set();
|
|
347
|
-
const current = currentSessionId(list);
|
|
348
354
|
const byId = list !== null && typeof list === "object" && list.byId !== null && typeof list.byId === "object" ? list.byId : {};
|
|
349
355
|
const titleOf = (id, entry) => (entry !== null && typeof entry === "object" && typeof entry.displayTitle === "string" && entry.displayTitle !== "" ? entry.displayTitle : id);
|
|
350
356
|
// Which workspace the session lives in. The sidebar's own workspace rows are
|
|
@@ -363,7 +369,6 @@ window.__ModuleLoader__.load({
|
|
|
363
369
|
const entry = byId[id];
|
|
364
370
|
if (entry === null || typeof entry !== "object") continue;
|
|
365
371
|
if (entry.origin === "subagent") continue;
|
|
366
|
-
if (id === current) continue;
|
|
367
372
|
const values = entry.projectionValues;
|
|
368
373
|
const value = values !== null && typeof values === "object" ? values[LAST_TURN_END_KEY] : undefined;
|
|
369
374
|
if (value === null || typeof value !== "object") continue;
|
|
@@ -380,7 +385,6 @@ window.__ModuleLoader__.load({
|
|
|
380
385
|
if (config === null || typeof config !== "object" || config.pending !== false) {
|
|
381
386
|
if (pending !== null && pending !== undefined && typeof pending.forEach === "function") {
|
|
382
387
|
pending.forEach((value, id) => {
|
|
383
|
-
if (id === current) return;
|
|
384
388
|
const kind = value !== null && typeof value === "object" && typeof value.kind === "string" ? value.kind : "";
|
|
385
389
|
if (ids.has(id)) {
|
|
386
390
|
// Already unread by its last turn: it is waiting too, which wins.
|
|
@@ -426,6 +430,9 @@ window.__ModuleLoader__.load({
|
|
|
426
430
|
function notifyBadge() {
|
|
427
431
|
const snapshot = badgeSnapshot();
|
|
428
432
|
badgeListeners.forEach((fn) => { try { fn(snapshot); } catch {} });
|
|
433
|
+
// The header badge is a DOM node, not a listener, so it is reconciled
|
|
434
|
+
// here as well as from the mutation watchdog. Idempotent either way.
|
|
435
|
+
paintHeaderBadge();
|
|
429
436
|
}
|
|
430
437
|
/** Manual test input. */
|
|
431
438
|
function emitBadge(value) {
|
|
@@ -1012,48 +1019,218 @@ window.__ModuleLoader__.load({
|
|
|
1012
1019
|
}
|
|
1013
1020
|
|
|
1014
1021
|
/**
|
|
1015
|
-
* The
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1022
|
+
* The unread badge, injected into the sidebar's section header.
|
|
1023
|
+
*
|
|
1024
|
+
* There is no slot to register into: `sidebar.workspaces` is a SINGLE slot
|
|
1025
|
+
* owned by ui-workspace that covers the whole browsing region *including* its
|
|
1026
|
+
* header, and shadowing it would mean reimplementing the shipped browser. So
|
|
1027
|
+
* the badge is injected into the DOM — the same technique, the same contract,
|
|
1028
|
+
* and the same failure posture as the workspace-row dots: decorative, never
|
|
1029
|
+
* throws, and after repeated failures it gives up on its own with one warning.
|
|
1030
|
+
*
|
|
1031
|
+
* Placement is measured, not inherited, because the shipped CSS forbids the
|
|
1032
|
+
* obvious approach: `.sectionHeader` is 36px tall with `overflow:hidden`, the
|
|
1033
|
+
* label inside it is a 20px line box that is ALSO `overflow:hidden`, so a badge
|
|
1034
|
+
* parented to the label and raised clear of the glyph is clipped to a sliver by
|
|
1035
|
+
* the label's own overflow. The badge is therefore a SIBLING of the label,
|
|
1036
|
+
* absolutely positioned over the label's top-right corner from its measured
|
|
1037
|
+
* rect, and clamped so it never leaves the header's 36px box.
|
|
1038
|
+
*
|
|
1039
|
+
* The count is shown for EVERY value including zero: a grey 0 keeps the
|
|
1040
|
+
* position from jumping, and doubles as the only way back to the panel once
|
|
1041
|
+
* the old sidebar-foot button is gone.
|
|
1019
1042
|
*/
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
+
const HEAD_BADGE_ATTR = "data-icon-custom-unreadbadge";
|
|
1044
|
+
const HEAD_BADGE_STYLE_ID = "dsh-icon-custom-unreadbadge-css";
|
|
1045
|
+
const HEAD_BADGE_WIRED = "data-icon-custom-unreadbadge-wired";
|
|
1046
|
+
const HEAD_BADGE_MAX_FAILURES = 40;
|
|
1047
|
+
/** How many ancestors to walk up looking for the header row. */
|
|
1048
|
+
const HEAD_BADGE_MAX_STEPS = 6;
|
|
1049
|
+
/** Raise above the label's top edge; the header only leaves 8px of headroom. */
|
|
1050
|
+
const HEAD_BADGE_RAISE = 7;
|
|
1051
|
+
/** Overhang past the label's right edge. */
|
|
1052
|
+
const HEAD_BADGE_OVERHANG = 9;
|
|
1053
|
+
let headerBadgeHost = null;
|
|
1054
|
+
let headerBadgeHostPosition = "";
|
|
1055
|
+
let headerBadgeObserved = null;
|
|
1056
|
+
let headerBadgeObserver = null;
|
|
1057
|
+
let headerBadgeFailures = 0;
|
|
1058
|
+
let headerBadgeGivenUp = false;
|
|
1059
|
+
/** Locale binder, installed by `apply()` so the injected node can label itself. */
|
|
1060
|
+
let badgeT = null;
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Rules for the injected badge. A stylesheet rather than inline styles because
|
|
1064
|
+
* hover and the theme-following ring cannot be expressed inline. Colours come
|
|
1065
|
+
* from the official tokens wherever one exists:
|
|
1066
|
+
* * the red is the one this plugin has always drawn;
|
|
1067
|
+
* * the "nothing pending" grey is the official idle-state colour, so it
|
|
1068
|
+
* follows light/dark without naming either;
|
|
1069
|
+
* * the ring is the sidebar's own fill — that is what makes a badge sitting
|
|
1070
|
+
* on top of a glyph read as a badge instead of a smudge.
|
|
1071
|
+
* The hit area grows sideways and downward only: there are just 8px above the
|
|
1072
|
+
* label, so anything reaching further up is clipped by the header's overflow.
|
|
1073
|
+
*/
|
|
1074
|
+
const HEAD_BADGE_CSS = [
|
|
1075
|
+
`[${HEAD_BADGE_ATTR}]{position:absolute;display:none;align-items:center;justify-content:center;min-width:15px;height:15px;padding:0 3.5px;box-sizing:border-box;border:0;border-radius:999px;background:#e5484d;color:#fff;font:inherit;font-size:9.5px;font-weight:700;line-height:1;font-variant-numeric:tabular-nums;letter-spacing:-.02em;cursor:pointer;z-index:3;box-shadow:0 0 0 1.5px var(--dsw-specific-sidebar-fill,#fff)}`,
|
|
1076
|
+
`[${HEAD_BADGE_ATTR}]::after{content:"";position:absolute;inset:0 -5px -5px -5px;border-radius:999px}`,
|
|
1077
|
+
`[${HEAD_BADGE_ATTR}]:hover{filter:brightness(1.12)}`,
|
|
1078
|
+
`[${HEAD_BADGE_ATTR}][data-zero="1"]{background:var(--dsw-alias-state-idle-primary,#b6bcc4)}`
|
|
1079
|
+
].join("");
|
|
1080
|
+
|
|
1081
|
+
/** Install the badge rules once per page. */
|
|
1082
|
+
function ensureHeaderBadgeStyle() {
|
|
1083
|
+
try {
|
|
1084
|
+
if (document.getElementById(HEAD_BADGE_STYLE_ID) !== null) return;
|
|
1085
|
+
const style = document.createElement("style");
|
|
1086
|
+
style.id = HEAD_BADGE_STYLE_ID;
|
|
1087
|
+
style.textContent = HEAD_BADGE_CSS;
|
|
1088
|
+
document.head.appendChild(style);
|
|
1089
|
+
} catch { /* styling is cosmetic; never let it reach a render tree */ }
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* The section-header row and the label inside it.
|
|
1094
|
+
*
|
|
1095
|
+
* The title TEXT is deliberately not matched: it flips between 工作区 and 会话
|
|
1096
|
+
* with the group-by mode and is localized, so any string table would be both
|
|
1097
|
+
* incomplete and wrong half the time. Structure is stable instead —
|
|
1098
|
+
* `sidebar.workspaces` is a slot root (the official renderer stamps
|
|
1099
|
+
* `data-slot` on those), the header row's first button is the search control,
|
|
1100
|
+
* and the label is the row's first text-bearing child that owns no button.
|
|
1101
|
+
* @param container - the browsing region root.
|
|
1102
|
+
* @returns `{ row, label }`, or null when this build's DOM is not recognised.
|
|
1103
|
+
*/
|
|
1104
|
+
function sectionHeaderParts(container) {
|
|
1105
|
+
// Our own badge is a button too; never let it stand in for the search
|
|
1106
|
+
// control, or the row anchor would resolve to the wrong element.
|
|
1107
|
+
let firstButton = null;
|
|
1108
|
+
for (const candidate of container.querySelectorAll("button")) {
|
|
1109
|
+
if (candidate.getAttribute(HEAD_BADGE_ATTR) !== null) continue;
|
|
1110
|
+
firstButton = candidate;
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
if (firstButton === null) return null;
|
|
1114
|
+
let row = firstButton.parentElement;
|
|
1115
|
+
for (let step = 0; step < HEAD_BADGE_MAX_STEPS && row !== null && row !== container; step++) {
|
|
1116
|
+
for (const child of row.children) {
|
|
1117
|
+
if (child === firstButton || child.contains(firstButton)) continue;
|
|
1118
|
+
if (child.tagName === "BUTTON") continue;
|
|
1119
|
+
if (child.querySelector("button") !== null) continue;
|
|
1120
|
+
if ((child.textContent ?? "").trim() !== "") return { row, label: child };
|
|
1043
1121
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1122
|
+
row = row.parentElement;
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** Warn once, then give up for this page — the row dots' exact posture. */
|
|
1128
|
+
function noteHeaderBadgeFailure(why) {
|
|
1129
|
+
headerBadgeFailures++;
|
|
1130
|
+
if (headerBadgeFailures === 1) {
|
|
1131
|
+
try { console.warn("dsh-icon-custom: 工作区标题红点定位失败,已降级(不影响其他功能):", why); } catch {}
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (headerBadgeFailures < HEAD_BADGE_MAX_FAILURES || headerBadgeGivenUp) return;
|
|
1135
|
+
headerBadgeGivenUp = true;
|
|
1136
|
+
try { console.warn("dsh-icon-custom: 工作区标题红点连续失败,本次会话内已自动关闭。"); } catch {}
|
|
1055
1137
|
}
|
|
1056
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* Reconcile the header badge with the current count and the label's position.
|
|
1141
|
+
*
|
|
1142
|
+
* Driven by the same mutation watchdog as the row dots, so it must be
|
|
1143
|
+
* idempotent: it writes only what actually differs, or the observer that
|
|
1144
|
+
* called it would re-arm on its own output. Geometry is read from the live
|
|
1145
|
+
* rects, so the badge follows a sidebar resize, a locale change and the
|
|
1146
|
+
* label's own collapse animation without any hardcoded offsets.
|
|
1147
|
+
*/
|
|
1148
|
+
function paintHeaderBadge() {
|
|
1149
|
+
if (headerBadgeGivenUp) return;
|
|
1150
|
+
const container = workspaceDotContainer();
|
|
1151
|
+
if (container === null) return; // start-up race, not a failure
|
|
1152
|
+
const parts = sectionHeaderParts(container);
|
|
1153
|
+
if (parts === null) { noteHeaderBadgeFailure("找不到分区标题"); return; }
|
|
1154
|
+
const row = parts.row;
|
|
1155
|
+
const label = parts.label;
|
|
1156
|
+
ensureHeaderBadgeStyle();
|
|
1157
|
+
if (headerBadgeHost !== row) {
|
|
1158
|
+
if (headerBadgeHost !== null) { try { headerBadgeHost.style.position = headerBadgeHostPosition; } catch {} }
|
|
1159
|
+
headerBadgeHost = row;
|
|
1160
|
+
headerBadgeHostPosition = row.style.position;
|
|
1161
|
+
try { if (window.getComputedStyle(row).position === "static") row.style.position = "relative"; } catch {}
|
|
1162
|
+
}
|
|
1163
|
+
let node = row.querySelector(`[${HEAD_BADGE_ATTR}]`);
|
|
1164
|
+
if (node === null) {
|
|
1165
|
+
node = document.createElement("button");
|
|
1166
|
+
node.type = "button";
|
|
1167
|
+
node.setAttribute(HEAD_BADGE_ATTR, "1");
|
|
1168
|
+
// Appended AFTER the shipped controls, so it can never be mistaken for
|
|
1169
|
+
// the header's first button.
|
|
1170
|
+
row.appendChild(node);
|
|
1171
|
+
}
|
|
1172
|
+
if (node.getAttribute(HEAD_BADGE_WIRED) !== "1") {
|
|
1173
|
+
node.setAttribute(HEAD_BADGE_WIRED, "1");
|
|
1174
|
+
node.addEventListener("click", (event) => {
|
|
1175
|
+
// The row belongs to the sidebar; keep the click to ourselves.
|
|
1176
|
+
event.preventDefault();
|
|
1177
|
+
event.stopPropagation();
|
|
1178
|
+
const rect = node.getBoundingClientRect();
|
|
1179
|
+
toggleUnreadPanel({ left: rect.left, top: rect.top });
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
const count = effectiveCount();
|
|
1183
|
+
const text = badgeLabel(count);
|
|
1184
|
+
const zero = count > 0 ? "0" : "1";
|
|
1185
|
+
if (node.getAttribute("data-zero") !== zero) node.setAttribute("data-zero", zero);
|
|
1186
|
+
if (typeof badgeT === "function") {
|
|
1187
|
+
const spoken = count > 0 ? badgeT("unreadPanelCount").replace("{n}", String(count)) : badgeT("unreadPanelNone");
|
|
1188
|
+
if (node.getAttribute("aria-label") !== spoken) {
|
|
1189
|
+
node.setAttribute("aria-label", spoken);
|
|
1190
|
+
node.setAttribute("title", spoken);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (node.textContent !== text) node.textContent = text;
|
|
1194
|
+
// The label collapses to nothing while the search box is open; the badge
|
|
1195
|
+
// has no meaning then, and the label's own visibility cannot hide it
|
|
1196
|
+
// because it is a sibling.
|
|
1197
|
+
const rowRect = row.getBoundingClientRect();
|
|
1198
|
+
const labelRect = label.getBoundingClientRect();
|
|
1199
|
+
if (labelRect.width < 8) {
|
|
1200
|
+
if (node.style.display !== "none") node.style.display = "none";
|
|
1201
|
+
} else {
|
|
1202
|
+
const width = typeof node.offsetWidth === "number" && node.offsetWidth > 0 ? node.offsetWidth : 15;
|
|
1203
|
+
const left = Math.round(labelRect.right - rowRect.left - width + HEAD_BADGE_OVERHANG);
|
|
1204
|
+
// Clamped: the header clips its overflow, and only ~8px sit above the label.
|
|
1205
|
+
const top = Math.max(0, Math.round(labelRect.top - rowRect.top - HEAD_BADGE_RAISE));
|
|
1206
|
+
if (node.style.left !== `${left}px`) node.style.left = `${left}px`;
|
|
1207
|
+
if (node.style.top !== `${top}px`) node.style.top = `${top}px`;
|
|
1208
|
+
if (node.style.display !== "flex") node.style.display = "flex";
|
|
1209
|
+
}
|
|
1210
|
+
// The label animates its own width; follow it instead of waiting for the
|
|
1211
|
+
// next mutation, which a CSS transition never produces.
|
|
1212
|
+
if (typeof window.ResizeObserver === "function") {
|
|
1213
|
+
if (headerBadgeObserver === null) headerBadgeObserver = new window.ResizeObserver(() => { paintHeaderBadge(); });
|
|
1214
|
+
if (headerBadgeObserved !== label) {
|
|
1215
|
+
try { headerBadgeObserver.disconnect(); headerBadgeObserver.observe(label); headerBadgeObserved = label; } catch {}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/** Take the badge back out and restore the row's own positioning. */
|
|
1221
|
+
function clearHeaderBadge() {
|
|
1222
|
+
try { document.querySelectorAll(`[${HEAD_BADGE_ATTR}]`).forEach((node) => node.remove()); } catch {}
|
|
1223
|
+
if (headerBadgeObserver !== null) {
|
|
1224
|
+
try { headerBadgeObserver.disconnect(); } catch {}
|
|
1225
|
+
headerBadgeObserver = null;
|
|
1226
|
+
headerBadgeObserved = null;
|
|
1227
|
+
}
|
|
1228
|
+
if (headerBadgeHost !== null) {
|
|
1229
|
+
try { headerBadgeHost.style.position = headerBadgeHostPosition; } catch {}
|
|
1230
|
+
}
|
|
1231
|
+
headerBadgeHost = null;
|
|
1232
|
+
headerBadgeHostPosition = "";
|
|
1233
|
+
}
|
|
1057
1234
|
/**
|
|
1058
1235
|
* The list behind that number, in the frame-wide overlay layer so it escapes
|
|
1059
1236
|
* the sidebar's clipping and scroll container. Clicking a row opens that
|
|
@@ -1577,9 +1754,11 @@ window.__ModuleLoader__.load({
|
|
|
1577
1754
|
inject: () => ({ t })
|
|
1578
1755
|
}, (props) => React.createElement(FaviconSection, { ...props, t, rpc })));
|
|
1579
1756
|
|
|
1580
|
-
//
|
|
1581
|
-
//
|
|
1582
|
-
//
|
|
1757
|
+
// The badge itself is NOT a slot occupant: it is injected into the
|
|
1758
|
+
// sidebar's section header by `paintHeaderBadge()` (see above), because
|
|
1759
|
+
// `sidebar.workspaces` is a single slot covering the whole region. Only
|
|
1760
|
+
// the list it opens still rides a slot — the frame-wide overlay, which
|
|
1761
|
+
// escapes the sidebar's clipping.
|
|
1583
1762
|
//
|
|
1584
1763
|
// 0.2.x has no `ctx.sessions.open(id)` (the ClientSessions service exposes
|
|
1585
1764
|
// retain/using/retainInfo/refreshProjections/search/fork/scope/binding — no
|
|
@@ -1590,10 +1769,9 @@ window.__ModuleLoader__.load({
|
|
|
1590
1769
|
const openSession = (id) => {
|
|
1591
1770
|
try { ctx.get("uiWorkspace")?.openSession(id); } catch { /* navigation is best effort */ }
|
|
1592
1771
|
};
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
));
|
|
1772
|
+
// The injected node labels itself for screen readers, so it needs the
|
|
1773
|
+
// locale binder that only exists inside `apply()`.
|
|
1774
|
+
badgeT = t;
|
|
1597
1775
|
ctx.slots.inject("shell.overlay", () => ctx.slots.register(
|
|
1598
1776
|
{ name: "shell.overlay", id: "icon-custom-unread-popup", order: 30 },
|
|
1599
1777
|
() => React.createElement(UnreadPopup, { t, onOpen: openSession })
|
|
@@ -1656,7 +1834,13 @@ window.__ModuleLoader__.load({
|
|
|
1656
1834
|
const schedule = () => {
|
|
1657
1835
|
if (queued) return;
|
|
1658
1836
|
queued = true;
|
|
1659
|
-
window.setTimeout(() => {
|
|
1837
|
+
window.setTimeout(() => {
|
|
1838
|
+
queued = false;
|
|
1839
|
+
paintWorkspaceDots();
|
|
1840
|
+
// Same region, same watchdog: the section-header badge is
|
|
1841
|
+
// re-injected whenever the shipped UI re-renders over it.
|
|
1842
|
+
paintHeaderBadge();
|
|
1843
|
+
}, 200);
|
|
1660
1844
|
};
|
|
1661
1845
|
const attach = () => {
|
|
1662
1846
|
const container = workspaceDotContainer();
|
|
@@ -1683,6 +1867,7 @@ window.__ModuleLoader__.load({
|
|
|
1683
1867
|
offDots();
|
|
1684
1868
|
offConfig();
|
|
1685
1869
|
clearWorkspaceDots();
|
|
1870
|
+
clearHeaderBadge();
|
|
1686
1871
|
};
|
|
1687
1872
|
}, "dsh-icon-custom: workspace dots");
|
|
1688
1873
|
// The platform favicon.svg repaints itself for dark mode through
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cowwo/dsh-icon-custom",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存,自动适配安卓/iOS/平板 PWA 图标。· Custom browser tab favicon: upload SVG/PNG/ICO from the settings page, applies instantly and persists, auto-sizes PWA icons for Android/iOS/tablets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|