dsh-url-router 0.4.0 → 0.4.1
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/AGENTS.md +13 -1
- package/README.md +16 -2
- package/lib/client.js +403 -58
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
## 桌面地址栏契约(2026-10-24 起,改之前先读)
|
|
84
84
|
|
|
85
85
|
- **形态**:Windows 桌面版的标题栏是**页面里画的** ✓ —— preload(`app.asar` 的 `lib/preload-app.cjs`)跑 `syncWindowsAppearance()`:给 `<html>` 打 `data-windows-titlebar`、设 `--dsh-windows-titlebar-height: 40px`,并用 shadow DOM 画菜单条(宿主 `[data-windows-menu]`,`position:fixed; top:0; left:var(--dsh-windows-menu-start,48px); z-index:1100; -webkit-app-region:no-drag`)。⇒ 那条 strip 是**页面空间** ✓,插件可以往里画 ✓。
|
|
86
|
-
-
|
|
86
|
+
- **门控**:Windows 那条路径只在 `documentElement.getAttribute('data-windows-titlebar') !== null` 时执行 ✓;Web 版**一行都不执行** ✓。macOS 走另一条分支 ✓(见下节 ✓),两者靠 `macInHeader = !windowsStrip && macDesktopWindow(root)` 互斥 ✓,`place()` 里就是一个 if/else ✓ —— **不是两个各自独立挂载的组件** ✓。
|
|
87
87
|
- **定位**:`left = 48 + 菜单宿主宽度 + 8` ✓(宽度用宿主 `getBoundingClientRect()` 量 ✓,**不要**读 shadow 内部 ✗);`right = TITLEBAR_CAPTION_WIDTH = 138px` 是 Windows 窗口按钮宽度常量 ✓(改缩放/换系统可能要调 ✓);高度一律走 CSS 变量 ✓,不要写死 40 ✓。
|
|
88
88
|
- **交互**:`-webkit-app-region: no-drag` 必须留 ✓ —— strip 本身是拖拽区 ✗,去掉它输入框点不动 ✗。
|
|
89
89
|
- **落地只写 hash** ✓:`window.location.hash = fragment` ✓,绝不自己调路由内部 ✗ —— 浏览器触发 `hashchange` ✓,落地就是"粘贴链接"那条已验证的路 ✓。
|
|
@@ -91,6 +91,18 @@
|
|
|
91
91
|
- **假 DOM 不支持 `#id` 选择器** ✗ → 地址栏带 `data-dsh-part="url-router-address-bar"` ✓,测试用属性选择器 ✓(行锚点当年同理 ✓)。
|
|
92
92
|
- **验收**:布局可在 Web 版上用 `document.documentElement.setAttribute('data-windows-titlebar','')` + 那个 CSS 变量预演 ✓(同一套 CSS ✓),但 `dsh-app://` 下的 `replaceState` / `hashchange` 行为**只能真机确认** ✓。
|
|
93
93
|
|
|
94
|
+
## macOS 地址栏契约(2026-10-25 起,改之前先读)
|
|
95
|
+
|
|
96
|
+
- **形态**:macOS 没有页面里画的 strip ✓(标题栏是原生的,红黄绿三个灯在左上 ✓)。借的是会话页顶部那行**常驻标题行** ✓:面包屑 + 模式标签 + 右侧一排按钮 + 电源按钮 ✓。bar 仍是 `position:fixed` 挂在 body 上 ✓ —— 和 Windows 同一套挂载方式 ✓。
|
|
97
|
+
- **判定**:`data-platform=darwin`(或 UA 是 Mac)**且**(UA 有 `Electron/<x>` **或**协议是 `dsh-app:`)✓。**实测桌面版把 UA 里的 `Electron/` 抹掉了** ✓(UA 形如 `… @deepseek-ai/dsh-desktop/0.2.1-rc` ✓),所以真正生效的是 `dsh-app:` 那条 ✓ —— 别以为 UA 分支在兜底 ✓。
|
|
98
|
+
- **定位 = 量空白段,不是探矩形** ✓:`macLeaves()` 走一遍行内**所有真正占位的元素** ✓(有自己文字、或 `button/a/input/[role=button]/[role=tab]/img/svg`、或没有子元素 ✓)→ 只留和电源按钮同一行的 ✓ → 按左边界排序并**合并重叠** ✓ → 求所有空白段 ✓ → 取**最宽**那段 ✓ → `fitBar()` 右对齐(贴着头部按钮)+ 两侧 `MAC_BAR_INSET = 25` 内缩 ✓。
|
|
99
|
+
- **宽度必须封顶** ✓(`MAC_BAR_MAX_WIDTH = 1150`):空白段的左边界就是**会话标题文字的右边界** ✓,不封顶的话每换一个会话框就变一次宽 ✓。
|
|
100
|
+
- **「探测矩形 + 一撞就隐藏」那套别再写** ✗:它把候选框放在电源按钮左侧 ✓,必然横跨头部那排按钮 ✓,于是每次都被判"不空"然后整框 `display:none` ✓ —— 这就是"框怎么都不出来"的历史 bug ✓。
|
|
101
|
+
- **没有电源按钮的界面**(插件页 / 设置页 / 面板页 ✓):沿用上一次量到的空白段 ✓,位置不动 ✓。**但要把窗口宽度一起存** ✓(`{top, viewport, gap}` ✓):头部按钮是**右对齐**的 ✓,窗口一变它们就移动 ✓,旧空白段会把框画到标题甚至侧边栏上 ✓(真机抓到过 `bar.left=122` 而 `row.left=301` ✓)。宽度差超过 `MAC_BAR_RESIZE_SLACK = 8` 就**不猜** ✓,先隐藏,等回会话页重新量 ✓。
|
|
102
|
+
- **只有一个框** ✓:挂载前先 `getElementById` 摘掉同 id 的旧 bar 和旧样式表 ✓ —— effect 重跑(热重载 / 重复 apply)不会多一个 ✓。
|
|
103
|
+
- **调试不在产品里** ✓:`dev/inspect/` 是一个**独立观察器插件** ✓(`private: true`,靠根 `files` 白名单不进 npm ✓),只读公开契约(`#dsh-url-router-titlebar`、`data-dsh-url-router-platform`、`[class*="titleRow"]`、`.dshpm-trigger`)✓。判据三条:`bars` 恒为 1 ✓、`onTop` ✓、`leaves` 有没有被压住 ✓。**别再把调试代码写回 `lib/client.js`** ✗ —— 发布物必须是跑过测试的那一份 ✓。
|
|
104
|
+
- **假 DOM 不支持 `#id` 选择器** ✗:mac 侧的新代码对 `getElementById` / `querySelectorAll` 全部用了可选调用 ✓,测试里缺失即跳过 ✓。
|
|
105
|
+
|
|
94
106
|
## 跨机开发日志
|
|
95
107
|
|
|
96
108
|
改动记录在私有库 [DDDMUC/repo-devlogs](https://github.com/DDDMUC/repo-devlogs) 的 **`dsh-url-router/`** 文件夹(`HANDOFF.md` 最新一轮在最上面;macOS 端写 `WORKLOG-macos.md`,条目以 `[macOS]` 开头)。按该库规矩,每条先写 `**运行环境**`(设备 / 应用 / 服务商与模型),再写做了什么、动了哪些文件、怎么验证、遗留问题。
|
package/README.md
CHANGED
|
@@ -55,8 +55,21 @@ dsh plugin --profile web add "link:/path/to/dsh-url-router"
|
|
|
55
55
|
|
|
56
56
|
DSH 桌面版(Windows)的标题栏是**画在网页里的**:preload 给 `<html>` 打上 `data-windows-titlebar`、给出 `--dsh-windows-titlebar-height`,菜单条本身是页面里的一个 shadow DOM 固定元素(`[data-windows-menu]`)。那条 strip 里的空白是**页面空间**,而桌面版**自己没有地址栏** —— 于是本插件把地址栏放在那儿:
|
|
57
57
|
|
|
58
|
-
- 只在 `html[data-windows-titlebar]` 存在时出现
|
|
58
|
+
- 只在 `html[data-windows-titlebar]` 存在时出现 ✓(纯 Web 版什么都不画 ✓)。
|
|
59
59
|
- 位置:紧跟 shell 自己的菜单条右侧 ✓、右端避开窗口按钮 ✓;高度取 `--dsh-windows-titlebar-height` ✓;`-webkit-app-region: no-drag` 保证能点 ✓。
|
|
60
|
+
|
|
61
|
+
### macOS 桌面的地址栏
|
|
62
|
+
|
|
63
|
+
macOS 的标题栏是**原生**的(左边红黄绿三个灯),页面里没有 Windows 那条 strip 可以借。但会话页顶部有一行**常驻的标题行**(面包屑 + 模式标签 + 右侧按钮 + 电源按钮),地址栏就落在那行里:
|
|
64
|
+
|
|
65
|
+
- 判定:shell 给 `<html>` 打上 `data-platform=darwin`(或 UA 是 Mac)**并且**(UA 里有 `Electron/<x>` **或**页面跑在 `dsh-app:` 协议上)→ 只有桌面 App 满足,浏览器里开 Web 版不画 ✓。(实测桌面版会把 UA 里的 `Electron/<x>` 抹掉,所以真正生效的是 `dsh-app:` 那条 ✓。)
|
|
66
|
+
- 位置:先量这一行里**所有真正占位的东西**(有自己文字的元素、按钮、图标),按左边界排序并合并重叠 → 得到若干空白段 → 取**最宽的那一段**,右端贴着头部按钮、左端跟着走,两边各留 25px 内缩 ✓。
|
|
67
|
+
- 宽度封顶 **1150px**:空白段的左边界是跟着**会话标题长度**跑的 ✓,不封顶的话换个会话框就变一次宽 ✓;放不下时按实际空白缩 ✓。
|
|
68
|
+
- 和电源按钮**同一行**(垂直居中于它 ✓),`position: fixed` —— 所以既不遮挡也不挤压任何东西 ✓。
|
|
69
|
+
- 没有电源按钮的界面(插件页、设置页、面板页):沿用上一次量到的空白段,**位置不动** ✓。但如果窗口尺寸变过(> 8px),旧空白段已经不可信 ✓(头部按钮是靠右对齐的,窗口一变它们就移动了),此时**不猜** —— 先隐藏,等回到会话页重新量 ✓。
|
|
70
|
+
- **只有一个框** ✓:挂载前先按 id 摘掉旧的 bar 和样式表 ✓,所以 effect 重跑多少次(热重载、重复 apply)都只有一个 ✓。
|
|
71
|
+
- Windows 那条路径完全不变,`data-windows-titlebar` 优先,两种形态互斥 ✓。
|
|
72
|
+
- 输入、反射、拒识、Escape 恢复的逻辑与 Windows 版共用同一份代码 ✓。
|
|
60
73
|
- 输入什么 ✓:任何 `#/…` 路由(会话 / 工作区 / 视图 / 轮次 / 行 / 面板 / 设置三层 / 右栏 pane)、**裸会话 id**、或**整条链接**(只取它的 fragment)✓;回车写 hash → 浏览器触发 `hashchange` → 落地走的就是"粘贴链接"那条已验证的路 ✓。
|
|
61
74
|
- 它同时是**反映** ✓:本插件每次写地址栏都会同步进这个框 ✓,所以它显示的就是"现在在哪" ✓。Escape 还原 ✓;认不出的文本不动地址栏 ✓。
|
|
62
75
|
|
|
@@ -130,7 +143,8 @@ Conversation rows and both plugin-list groups carry a transparent full-row ancho
|
|
|
130
143
|
|
|
131
144
|
The Windows desktop window draws its own title bar **inside the page**: the preload marks `<html data-windows-titlebar>` and publishes `--dsh-windows-titlebar-height`, and its menu is a fixed shadow-DOM element in the page (`[data-windows-menu]`). That strip is page space, and the desktop window has **no address bar of its own** — so this plugin puts one there:
|
|
132
145
|
|
|
133
|
-
- Drawn only when `html[data-windows-titlebar]` is present (nothing at all on the web app
|
|
146
|
+
- Drawn only when `html[data-windows-titlebar]` is present (nothing at all on the plain web app).
|
|
147
|
+
- On the **macOS desktop** build there is no in-page strip to borrow, so the bar becomes a floating pill instead: pinned just under the native title bar at the window's top-right, 380×30, rounded, blurred and shadowed. It is recognised by the shell's `<html data-platform="darwin">` marker (or a Mac user agent) together with the window being the application — Electron in the user agent or the `dsh-app:` scheme — so the web app in a browser never draws it. It sits inside the 48px the shell reserves above its frame (`--dsh-frame-top-clearance`), clear of the traffic lights. Both shapes share one code path; the Windows marker wins when present.
|
|
134
148
|
- Placed right after the shell's own menu and before the window buttons, at `--dsh-windows-titlebar-height`, with `-webkit-app-region: no-drag` so it stays clickable.
|
|
135
149
|
- Accepts any `#/…` route, a bare conversation id, or a whole link (only its fragment is used); Enter writes the hash, the browser fires `hashchange`, and the link lands through the very same path a pasted link takes.
|
|
136
150
|
- It also **reflects**: every write this plugin makes to the address bar is mirrored into the box, so it always names what is on screen. Escape restores it; text that names no route is refused.
|
package/lib/client.js
CHANGED
|
@@ -131,14 +131,14 @@ window.__ModuleLoader__.load({
|
|
|
131
131
|
* The router's own version, for the report a human files. A test keeps it equal to the
|
|
132
132
|
* package version, so a release cannot forget to bump it.
|
|
133
133
|
*/
|
|
134
|
-
const ROUTER_VERSION = '0.4.
|
|
134
|
+
const ROUTER_VERSION = '0.4.1'
|
|
135
135
|
/**
|
|
136
136
|
* Hand-maintained build marker, reported alongside the version. A working copy that is
|
|
137
137
|
* linked into a profile serves this file straight from disk, so the package version
|
|
138
138
|
* cannot say *which* copy a live window loaded — this can, and `report().build` is how a
|
|
139
139
|
* real-machine check answers that. Bump it whenever a live window must be told apart.
|
|
140
140
|
*/
|
|
141
|
-
const ROUTER_BUILD = 'desktop-bar+settings-marker+modal-dismiss'
|
|
141
|
+
const ROUTER_BUILD = 'desktop-bar+settings-marker+modal-dismiss+mac-header-bar.2'
|
|
142
142
|
|
|
143
143
|
const TAG = '[url-router]'
|
|
144
144
|
|
|
@@ -155,10 +155,173 @@ window.__ModuleLoader__.load({
|
|
|
155
155
|
const TITLEBAR_STYLE_ID = 'dsh-url-router-titlebar-style'
|
|
156
156
|
/** Width the window's caption buttons take at the right end of the strip. */
|
|
157
157
|
const TITLEBAR_CAPTION_WIDTH = 138
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
/** Gap between the shell's menu and the bar. */
|
|
159
|
+
const TITLEBAR_GAP = 8
|
|
160
|
+
/**
|
|
161
|
+
* macOS has no in-page title-bar strip, so the bar cannot borrow window chrome
|
|
162
|
+
* the way the Windows build does. It borrows the next best thing: the shell's
|
|
163
|
+
* resident header row, which exists on every screen. The bar stays mounted in
|
|
164
|
+
* the body with fixed positioning — exactly the Windows approach — and `place`
|
|
165
|
+
* measures the title cluster and the right-hand cluster to get its insets.
|
|
166
|
+
*/
|
|
167
|
+
const MAC_BAR_HEIGHT = 26
|
|
168
|
+
/** The least room the bar will draw itself into; below this it stays hidden. */
|
|
169
|
+
const MAC_BAR_MIN_WIDTH = 120
|
|
170
|
+
/** The breathing space the bar keeps from whatever stands next to it. */
|
|
171
|
+
const MAC_BAR_GAP = 8
|
|
172
|
+
/** The margin the bar leaves inside the free stretch, on each side. */
|
|
173
|
+
const MAC_BAR_INSET = 25
|
|
174
|
+
/**
|
|
175
|
+
* The bar's own length.
|
|
176
|
+
*
|
|
177
|
+
* The free stretch begins wherever the conversation's title happens to end, so
|
|
178
|
+
* without a length of its own the bar would grow and shrink as titles change.
|
|
179
|
+
* It stops here instead, and only gets shorter when the row cannot spare this
|
|
180
|
+
* much.
|
|
181
|
+
*/
|
|
182
|
+
const MAC_BAR_MAX_WIDTH = 1150
|
|
183
|
+
/**
|
|
184
|
+
* How much the window may change before a remembered stretch stops counting.
|
|
185
|
+
*
|
|
186
|
+
* The stretch was measured against one window; a different one moves the
|
|
187
|
+
* header's buttons, and there is nothing to re-measure from on a screen that
|
|
188
|
+
* has no power button. A couple of pixels is rounding, more is a resize.
|
|
189
|
+
*/
|
|
190
|
+
const MAC_BAR_RESIZE_SLACK = 8
|
|
191
|
+
const MAC_STYLE_TEXT = [
|
|
192
|
+
`#${TITLEBAR_BAR_ID}[data-dsh-url-router-platform="mac"]{position:fixed;display:flex;align-items:center;height:${MAC_BAR_HEIGHT}px;z-index:1200;-webkit-app-region:no-drag}`,
|
|
193
|
+
`#${TITLEBAR_BAR_ID}[data-dsh-url-router-platform="mac"] input{height:${MAC_BAR_HEIGHT}px;width:100%;box-sizing:border-box;border:1px solid rgba(127,127,127,.34);border-radius:8px;padding:0 10px;font:inherit;font-size:12px;color:inherit;background:rgba(127,127,127,.16);outline:none;-webkit-app-region:no-drag}`,
|
|
194
|
+
`#${TITLEBAR_BAR_ID}[data-dsh-url-router-platform="mac"] input:focus{border-color:var(--dsw-alias-brand-primary,rgba(79,124,255,.9))}`,
|
|
195
|
+
`#${TITLEBAR_BAR_ID}[data-dsh-url-router-platform="mac"][data-dsh-url-router-state="refused"] input{border-color:rgba(214,69,69,.85);color:rgba(214,69,69,1)}`,
|
|
196
|
+
].join('')
|
|
197
|
+
/** Where the bar's last spot is kept, so a screen without a power button still gets it. */
|
|
198
|
+
const MAC_PLACEMENT_KEY = 'dsh-url-router-placement'
|
|
199
|
+
/**
|
|
200
|
+
* Describe one element: what it is, and where it sits.
|
|
201
|
+
* @param {object} el - the element to describe.
|
|
202
|
+
* @returns {object|null} the description, or null when there is no element.
|
|
203
|
+
*/
|
|
204
|
+
function macDescribe(el) {
|
|
205
|
+
if (el === null || el === undefined) return null
|
|
206
|
+
const rect = el.getBoundingClientRect?.()
|
|
207
|
+
const cls = el.className?.baseVal ?? el.className ?? ''
|
|
208
|
+
return {
|
|
209
|
+
tag: String(el.tagName ?? '?').toLowerCase(),
|
|
210
|
+
cls: typeof cls === 'string' ? cls.slice(0, 70) : '',
|
|
211
|
+
rect: rect === undefined || rect === null
|
|
212
|
+
? null
|
|
213
|
+
: { top: Math.round(rect.top), left: Math.round(rect.left), width: Math.round(rect.width), height: Math.round(rect.height) },
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Every descendant of an element that carries text of its own or answers a
|
|
218
|
+
* click — the header's real contents, so the bar can find the gap between
|
|
219
|
+
* them instead of guessing where it is.
|
|
220
|
+
* @param {object} el - the element to walk.
|
|
221
|
+
* @param {number} depth - how far down to walk.
|
|
222
|
+
* @returns {Array<object>} the leaves, with their boxes.
|
|
223
|
+
*/
|
|
224
|
+
function macLeaves(el, depth) {
|
|
225
|
+
const leaves = []
|
|
226
|
+
if (el === null || el === undefined || depth > 6) return leaves
|
|
227
|
+
for (const child of el.children ?? []) {
|
|
228
|
+
const own = ownText(child)
|
|
229
|
+
const interactive = typeof child.closest === 'function'
|
|
230
|
+
&& child.matches?.('button, a, input, [role="button"], [role="tab"], img, svg') === true
|
|
231
|
+
if (own !== '' || interactive || child.children.length === 0) {
|
|
232
|
+
leaves.push({ ...macDescribe(child), own: own.slice(0, 40) })
|
|
233
|
+
} else {
|
|
234
|
+
leaves.push(...macLeaves(child, depth + 1))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return leaves
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The header row the bar has to live in: the row that carries the
|
|
241
|
+
* conversation's title on the left and the header's own buttons on the right.
|
|
242
|
+
*
|
|
243
|
+
* The shell names the row in its stylesheet, so that name is tried first; when
|
|
244
|
+
* it has changed, the row is taken from the power button's own corner, whose
|
|
245
|
+
* parent is the row the button was mounted on.
|
|
246
|
+
*
|
|
247
|
+
* @param {object} trigger - the power-menu trigger.
|
|
248
|
+
* @returns {object|undefined} the row element, or undefined when there is none.
|
|
249
|
+
*/
|
|
250
|
+
function macHeaderRow(trigger) {
|
|
251
|
+
const corner = document.querySelector?.('[data-conversation-header-corner]')
|
|
252
|
+
const named = document.querySelector?.('[class*="titleRow"]')
|
|
253
|
+
if (named !== undefined && named !== null) {
|
|
254
|
+
// Only the row that actually holds the button is the row to measure.
|
|
255
|
+
if (trigger === undefined || trigger === null) return named
|
|
256
|
+
if (typeof named.contains !== 'function' || named.contains(trigger)) return named
|
|
257
|
+
}
|
|
258
|
+
return corner?.parentElement ?? undefined
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The free stretch of the header row the bar can live in.
|
|
262
|
+
*
|
|
263
|
+
* The row is measured rather than guessed: every leaf that shares the power
|
|
264
|
+
* button's row — the title text, the mode label, the header's own buttons — is
|
|
265
|
+
* turned into an interval, overlapping intervals are merged, and the widest
|
|
266
|
+
* stretch between them is the one the bar goes into. Nothing is covered,
|
|
267
|
+
* because that stretch holds nothing; nothing is pushed, because the bar is a
|
|
268
|
+
* fixed-position layer of its own.
|
|
269
|
+
*
|
|
270
|
+
* @param {Array<object>} leaves - the row's measured contents.
|
|
271
|
+
* @param {object} rect - the power button's rectangle, which ends the search.
|
|
272
|
+
* @param {object} [bounds] - the row's own rectangle, which begins it.
|
|
273
|
+
* @returns {{left: number, right: number}|null} the stretch, or null when the row has no room.
|
|
274
|
+
*/
|
|
275
|
+
function macFreeGap(leaves, rect, bounds) {
|
|
276
|
+
if (rect === undefined || rect === null) return null
|
|
277
|
+
const rowTop = rect.top
|
|
278
|
+
const rowBottom = rect.top + rect.height
|
|
279
|
+
// The power button is the anchor: the bar never reaches past its left edge.
|
|
280
|
+
const limit = Math.round(rect.left)
|
|
281
|
+
const occupied = []
|
|
282
|
+
for (const leaf of leaves ?? []) {
|
|
283
|
+
const box = leaf?.rect
|
|
284
|
+
if (box === undefined || box === null) continue
|
|
285
|
+
if (!(box.width > 0) || !(box.height > 0)) continue
|
|
286
|
+
// Only what stands on the power button's row can be in the way.
|
|
287
|
+
if (box.top >= rowBottom || box.top + box.height <= rowTop) continue
|
|
288
|
+
const left = Math.round(box.left)
|
|
289
|
+
const right = Math.round(box.left + box.width)
|
|
290
|
+
if (right <= 0 || left >= limit) continue
|
|
291
|
+
occupied.push({ left, right })
|
|
292
|
+
}
|
|
293
|
+
occupied.sort((a, b) => a.left - b.left)
|
|
294
|
+
// Merge what overlaps, so a box inside another cannot split a stretch.
|
|
295
|
+
const merged = []
|
|
296
|
+
for (const span of occupied) {
|
|
297
|
+
const last = merged[merged.length - 1]
|
|
298
|
+
if (last !== undefined && span.left <= last.right) {
|
|
299
|
+
if (span.right > last.right) last.right = span.right
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
merged.push({ left: span.left, right: span.right })
|
|
303
|
+
}
|
|
304
|
+
// The row begins at its own left edge when that is known, and at the first
|
|
305
|
+
// thing standing on it otherwise.
|
|
306
|
+
const start = bounds !== undefined && bounds !== null && bounds.left > 0
|
|
307
|
+
? Math.round(bounds.left)
|
|
308
|
+
: (merged[0]?.left ?? limit)
|
|
309
|
+
const runs = []
|
|
310
|
+
let cursor = start
|
|
311
|
+
for (const span of merged) {
|
|
312
|
+
if (span.left > cursor) runs.push({ left: cursor, right: span.left })
|
|
313
|
+
if (span.right > cursor) cursor = span.right
|
|
314
|
+
}
|
|
315
|
+
if (limit > cursor) runs.push({ left: cursor, right: limit })
|
|
316
|
+
let best = null
|
|
317
|
+
for (const run of runs) {
|
|
318
|
+
if (run.right - run.left < MAC_BAR_MIN_WIDTH) continue
|
|
319
|
+
if (best === null || run.right - run.left > best.right - best.left) best = run
|
|
320
|
+
}
|
|
321
|
+
return best
|
|
322
|
+
}
|
|
323
|
+
/** How long the bar waits for a handed-over link to land before it says it did not. */
|
|
324
|
+
const TITLEBAR_SETTLE_MS = 900
|
|
162
325
|
/** What the address bar accepts, said in its placeholder. */
|
|
163
326
|
const TITLEBAR_PLACEHOLDER = '#/session/… · #/panel/… · #/settings · 会话 id · 链接'
|
|
164
327
|
/** The strip belongs to the shell, so only the bar itself is styled. */
|
|
@@ -284,12 +447,6 @@ window.__ModuleLoader__.load({
|
|
|
284
447
|
return undefined
|
|
285
448
|
}
|
|
286
449
|
|
|
287
|
-
/**
|
|
288
|
-
* The kind inside a tab id: the bar builds one as `sidebar://<kind>` and appends a uuid for
|
|
289
|
-
* kinds that may be opened more than once, so the kind is right there in the address.
|
|
290
|
-
* @param {string} value - a tab id.
|
|
291
|
-
* @returns {string|undefined} the kind, when the value is one of these addresses.
|
|
292
|
-
*/
|
|
293
450
|
/**
|
|
294
451
|
* The right bar's registry, normalised: one row per page type with its kind, the label
|
|
295
452
|
* the tab shows, whether more than one may be open, and its empty-state card (measured
|
|
@@ -297,36 +454,6 @@ window.__ModuleLoader__.load({
|
|
|
297
454
|
* @param {object} faces - the probed host faces.
|
|
298
455
|
* @returns {{kind: string, label: string|undefined, multiple: boolean}[]|undefined} the rows.
|
|
299
456
|
*/
|
|
300
|
-
/**
|
|
301
|
-
* The seq of a turn's start event, from the `turnOutline` projection.
|
|
302
|
-
*
|
|
303
|
-
* The projection is a public read on the session controller (the core reads it the same
|
|
304
|
-
* way: `session.projections.faceOf("turnOutline").getSnapshot()`), and each entry carries
|
|
305
|
-
* the `seq` of that turn's `turn/start` — which is exactly what `loadThrough` pages back
|
|
306
|
-
* to. Reading it is what makes a turn link work for a turn that is not loaded yet.
|
|
307
|
-
* @param {object} faces - the probed host faces.
|
|
308
|
-
* @param {string|undefined} sessionId - the session the turn belongs to.
|
|
309
|
-
* @param {string} turn - the turn number.
|
|
310
|
-
* @returns {number|undefined} the seq, when the projection names that turn.
|
|
311
|
-
*/
|
|
312
|
-
function turnSeqOf(faces, sessionId, turn) {
|
|
313
|
-
if (typeof sessionId !== 'string' || sessionId === '') return undefined
|
|
314
|
-
// Reached by retaining the session's scope: `sessions.binding(id)` answers undefined
|
|
315
|
-
// for a session nobody materialised yet (measured), while `retainScope(id, source)`
|
|
316
|
-
// materialises it and returns a reference whose `binding` is the live one. Released here
|
|
317
|
-
// because only the projection is read.
|
|
318
|
-
const reference = attemptValue(() => faces.sessions?.retainScope?.(sessionId, 'url-router'))
|
|
319
|
-
const outline = attemptValue(() => session?.projections?.faceOf?.('turnOutline')?.getSnapshot?.())
|
|
320
|
-
attempt(() => reference?.release?.())
|
|
321
|
-
if (!Array.isArray(outline)) return undefined
|
|
322
|
-
const wanted = String(turn)
|
|
323
|
-
for (const entry of outline) {
|
|
324
|
-
if (entry === null || typeof entry !== 'object') continue
|
|
325
|
-
if (String(entry.turn) !== wanted) continue
|
|
326
|
-
return typeof entry.seq === 'number' ? entry.seq : undefined
|
|
327
|
-
}
|
|
328
|
-
return undefined
|
|
329
|
-
}
|
|
330
457
|
|
|
331
458
|
function paneRegistryOf(faces) {
|
|
332
459
|
const rows = attemptValue(() => faces.sidebarRightTabs?.entries?.())
|
|
@@ -2383,6 +2510,55 @@ window.__ModuleLoader__.load({
|
|
|
2383
2510
|
return `打不开 ${name}(当前 ${shown},侧栏 ${rows} 行)`
|
|
2384
2511
|
}
|
|
2385
2512
|
|
|
2513
|
+
/**
|
|
2514
|
+
* Whether this window is the macOS desktop build.
|
|
2515
|
+
*
|
|
2516
|
+
* The macOS desktop shell draws a native title bar and publishes no in-page
|
|
2517
|
+
* strip, so the Windows marker is absent there — which used to mean "draw
|
|
2518
|
+
* nothing". Two facts together identify a macOS desktop window: the shell marks
|
|
2519
|
+
* `<html data-platform="darwin">` (or the user agent says Mac), and the window
|
|
2520
|
+
* is the application rather than a browser tab (Electron in the user agent, or
|
|
2521
|
+
* the page served over the app's own `dsh-app:` scheme). The plain web app in a
|
|
2522
|
+
* browser satisfies neither and stays empty, as before.
|
|
2523
|
+
*
|
|
2524
|
+
* @param {object} root - `document.documentElement`.
|
|
2525
|
+
* @returns {boolean} true when the address bar belongs on screen.
|
|
2526
|
+
*/
|
|
2527
|
+
function macDesktopWindow(root) {
|
|
2528
|
+
const ua = typeof navigator === 'object' && navigator !== null ? String(navigator.userAgent ?? '') : ''
|
|
2529
|
+
const platform = typeof root?.getAttribute === 'function' ? root.getAttribute('data-platform') : null
|
|
2530
|
+
const isMac = platform === 'darwin' || /Mac OS X|Macintosh/u.test(ua)
|
|
2531
|
+
if (!isMac) return false
|
|
2532
|
+
const protocol = String(globalThis.location?.protocol ?? '')
|
|
2533
|
+
return /Electron\/\d/u.test(ua) || protocol.startsWith('dsh-app')
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
|
|
2537
|
+
/**
|
|
2538
|
+
* The power-menu trigger, the one element that is guaranteed to sit on the
|
|
2539
|
+
* same row on every screen. The address bar anchors to it.
|
|
2540
|
+
*
|
|
2541
|
+
* @returns {object|undefined} the trigger element, or undefined when absent.
|
|
2542
|
+
*/
|
|
2543
|
+
function powerTrigger() {
|
|
2544
|
+
if (typeof document?.querySelector !== 'function') return undefined
|
|
2545
|
+
return document.querySelector('[data-dshpm-mount] .dshpm-trigger')
|
|
2546
|
+
?? document.querySelector('.dshpm-trigger')
|
|
2547
|
+
?? document.querySelector('[aria-label="电源菜单"]')
|
|
2548
|
+
?? undefined
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
/** An element's own text, ignoring the text of its children. */
|
|
2552
|
+
function ownText(el) {
|
|
2553
|
+
if (el === null || el === undefined || typeof el.childNodes !== 'object') return ''
|
|
2554
|
+
let text = ''
|
|
2555
|
+
for (const node of el.childNodes) {
|
|
2556
|
+
if (node !== null && node.nodeType === 3) text += node.textContent ?? ''
|
|
2557
|
+
}
|
|
2558
|
+
return text.trim()
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
|
|
2386
2562
|
/**
|
|
2387
2563
|
* Draw the address bar into the desktop window's title bar strip.
|
|
2388
2564
|
* @param {object} document - the document to draw into.
|
|
@@ -2393,14 +2569,32 @@ window.__ModuleLoader__.load({
|
|
|
2393
2569
|
if (document === undefined || document === null) return () => {}
|
|
2394
2570
|
const root = document.documentElement
|
|
2395
2571
|
// The marker is the shell's own statement that this page has a title bar drawn
|
|
2396
|
-
// inside it. Without it there is no strip
|
|
2397
|
-
|
|
2572
|
+
// inside it. Without it there is no strip — but the macOS desktop build still
|
|
2573
|
+
// gets a bar, drawn into the session header row instead.
|
|
2574
|
+
const windowsStrip = typeof root?.getAttribute === 'function' && root.getAttribute(WINDOWS_TITLEBAR_ATTRIBUTE) !== null
|
|
2575
|
+
const macInHeader = !windowsStrip && macDesktopWindow(root)
|
|
2576
|
+
if (!windowsStrip && !macInHeader) return () => {}
|
|
2398
2577
|
if (typeof document.createElement !== 'function' || typeof document.body?.appendChild !== 'function') return () => {}
|
|
2578
|
+
// One bar, one stylesheet — however many times this runs.
|
|
2579
|
+
//
|
|
2580
|
+
// The effect that draws the bar can run again in a live window (a hot
|
|
2581
|
+
// reload of this plugin re-applies it), and a second mount would leave a
|
|
2582
|
+
// second box on screen. Both are identified by id, so anything already
|
|
2583
|
+
// there is taken down before the new one goes up: the platform split
|
|
2584
|
+
// decides *where* the one bar lives, never how many there are.
|
|
2585
|
+
try {
|
|
2586
|
+
const staleBar = document.getElementById?.(TITLEBAR_BAR_ID)
|
|
2587
|
+
if (staleBar !== null && staleBar !== undefined) staleBar.remove?.()
|
|
2588
|
+
const staleStyle = document.getElementById?.(TITLEBAR_STYLE_ID)
|
|
2589
|
+
if (staleStyle !== null && staleStyle !== undefined) staleStyle.remove?.()
|
|
2590
|
+
} catch {
|
|
2591
|
+
// A document without getElementById keeps the disposer as its only guard.
|
|
2592
|
+
}
|
|
2399
2593
|
let style
|
|
2400
2594
|
try {
|
|
2401
2595
|
style = document.createElement('style')
|
|
2402
2596
|
style.id = TITLEBAR_STYLE_ID
|
|
2403
|
-
style.textContent = TITLEBAR_STYLE_TEXT
|
|
2597
|
+
style.textContent = macInHeader ? `${TITLEBAR_STYLE_TEXT}\n${MAC_STYLE_TEXT}` : TITLEBAR_STYLE_TEXT
|
|
2404
2598
|
const parent = document.head ?? document.body
|
|
2405
2599
|
if (parent !== undefined && parent !== null && typeof parent.appendChild === 'function') parent.appendChild(style)
|
|
2406
2600
|
} catch {
|
|
@@ -2408,26 +2602,156 @@ window.__ModuleLoader__.load({
|
|
|
2408
2602
|
}
|
|
2409
2603
|
const bar = document.createElement('div')
|
|
2410
2604
|
bar.id = TITLEBAR_BAR_ID
|
|
2411
|
-
if (typeof bar.setAttribute === 'function')
|
|
2605
|
+
if (typeof bar.setAttribute === 'function') {
|
|
2606
|
+
bar.setAttribute('data-dsh-part', TITLEBAR_PART)
|
|
2607
|
+
bar.setAttribute('data-dsh-url-router-platform', macInHeader ? 'mac' : 'windows')
|
|
2608
|
+
}
|
|
2412
2609
|
const input = document.createElement('input')
|
|
2413
2610
|
input.type = 'text'
|
|
2414
2611
|
input.spellcheck = false
|
|
2415
2612
|
input.placeholder = TITLEBAR_PLACEHOLDER
|
|
2416
2613
|
if (typeof input.setAttribute === 'function') input.setAttribute('aria-label', '地址栏 / address bar')
|
|
2417
2614
|
bar.appendChild(input)
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2615
|
+
/**
|
|
2616
|
+
* The free stretch the bar was last fitted into.
|
|
2617
|
+
*
|
|
2618
|
+
* A screen with no power button — a panel, the plugins page, settings — has
|
|
2619
|
+
* nothing to align to. The bar keeps this stretch there, so changing views
|
|
2620
|
+
* neither moves it nor makes it vanish. The stretch is also kept on disk, so
|
|
2621
|
+
* a window that starts on such a screen still gets the bar where it belongs;
|
|
2622
|
+
* and because the box is fitted from the stretch each time, a change to the
|
|
2623
|
+
* bar's margins applies there too, without waiting for a conversation.
|
|
2624
|
+
*/
|
|
2625
|
+
let lastPlacement = attemptValue(() => {
|
|
2626
|
+
const raw = globalThis.localStorage?.getItem?.(MAC_PLACEMENT_KEY)
|
|
2627
|
+
if (raw === null || raw === undefined || raw === '') return null
|
|
2628
|
+
const parsed = JSON.parse(raw)
|
|
2629
|
+
const gap = parsed?.gap
|
|
2630
|
+
if (typeof parsed?.top !== 'number' || typeof parsed?.viewport !== 'number') return null
|
|
2631
|
+
if (typeof gap?.left !== 'number' || typeof gap?.right !== 'number') return null
|
|
2632
|
+
return { top: parsed.top, viewport: parsed.viewport, gap: { left: gap.left, right: gap.right } }
|
|
2633
|
+
}) ?? null
|
|
2634
|
+
/**
|
|
2635
|
+
* Fit the bar into a free stretch of the header row.
|
|
2636
|
+
*
|
|
2637
|
+
* The bar takes the whole stretch bar a margin on each side, so it is as
|
|
2638
|
+
* long as the row can spare without touching what stands beside it. A
|
|
2639
|
+
* narrow row keeps the whole stretch rather than a sliver of it.
|
|
2640
|
+
*
|
|
2641
|
+
* @param {{left: number, right: number}} gap - the free stretch.
|
|
2642
|
+
* @param {number} top - the row's line, already centred on the power button.
|
|
2643
|
+
* @returns {{left: number, top: number, width: number}} the box to draw.
|
|
2644
|
+
*/
|
|
2645
|
+
const fitBar = (gap, top) => {
|
|
2646
|
+
const right = gap.right - MAC_BAR_GAP - MAC_BAR_INSET
|
|
2647
|
+
const room = gap.left + MAC_BAR_INSET
|
|
2648
|
+
const available = right - room
|
|
2649
|
+
const width = available >= MAC_BAR_MIN_WIDTH
|
|
2650
|
+
? Math.round(Math.min(MAC_BAR_MAX_WIDTH, available))
|
|
2651
|
+
: Math.round(gap.right - MAC_BAR_GAP - gap.left)
|
|
2652
|
+
return { left: Math.round(right - width), top, width }
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Remember the stretch the bar went into, and put it on disk when it moved.
|
|
2656
|
+
* @param {object} placement - the line and the stretch just used.
|
|
2657
|
+
*/
|
|
2658
|
+
const remember = placement => {
|
|
2659
|
+
const previous = lastPlacement
|
|
2660
|
+
lastPlacement = placement
|
|
2661
|
+
if (previous !== null
|
|
2662
|
+
&& previous.top === placement.top
|
|
2663
|
+
&& previous.viewport === placement.viewport
|
|
2664
|
+
&& previous.gap.left === placement.gap.left
|
|
2665
|
+
&& previous.gap.right === placement.gap.right) return
|
|
2666
|
+
attemptValue(() => globalThis.localStorage?.setItem?.(MAC_PLACEMENT_KEY, JSON.stringify(placement)))
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Put the bar where it belongs.
|
|
2670
|
+
*
|
|
2671
|
+
* Windows: start after the shell's menu, stop before its window buttons.
|
|
2672
|
+
* macOS: sit in the free stretch of the header row, on the power button's
|
|
2673
|
+
* own line.
|
|
2674
|
+
*/
|
|
2675
|
+
const place = () => {
|
|
2676
|
+
if (macInHeader) {
|
|
2677
|
+
// The bar lives on the header row, on the same line as the power
|
|
2678
|
+
// button, in the free stretch between the title text and the
|
|
2679
|
+
// header's own buttons. The row's real contents are measured and the
|
|
2680
|
+
// widest free stretch is chosen, so the bar covers nothing — and,
|
|
2681
|
+
// being a fixed-position layer, it pushes nothing either.
|
|
2682
|
+
const trigger = powerTrigger()
|
|
2683
|
+
const rect = trigger?.getBoundingClientRect?.()
|
|
2684
|
+
const row = macHeaderRow(trigger)
|
|
2685
|
+
const rowRect = row?.getBoundingClientRect?.()
|
|
2686
|
+
const anchored = rect !== undefined && rect !== null && rect.width > 0
|
|
2687
|
+
// The row is only walked when there is a button to measure from.
|
|
2688
|
+
const gap = anchored ? macFreeGap(macLeaves(row, 0), rect, rowRect) : null
|
|
2689
|
+
if (gap === null) {
|
|
2690
|
+
// Nothing on this screen to align to. Keep the stretch the
|
|
2691
|
+
// conversation gave the bar, so switching views does not move it
|
|
2692
|
+
// and does not make it disappear.
|
|
2693
|
+
//
|
|
2694
|
+
// Only while the window is still the one that stretch was measured
|
|
2695
|
+
// in: the header's buttons sit at the right end of the window, so a
|
|
2696
|
+
// resize moves them, and reusing an old stretch would draw the bar
|
|
2697
|
+
// straight across the title and the buttons. With no button to
|
|
2698
|
+
// measure from there is nothing to re-derive it from either, so a
|
|
2699
|
+
// resized window hides the bar until a conversation places it again.
|
|
2700
|
+
const viewport = Math.round(globalThis.innerWidth ?? 0)
|
|
2701
|
+
if (lastPlacement === null || viewport <= 0
|
|
2702
|
+
|| Math.abs(viewport - lastPlacement.viewport) > MAC_BAR_RESIZE_SLACK) {
|
|
2703
|
+
bar.style.display = 'none'
|
|
2704
|
+
return
|
|
2705
|
+
}
|
|
2706
|
+
const box = fitBar(lastPlacement.gap, lastPlacement.top)
|
|
2707
|
+
const left = Math.max(MAC_BAR_GAP, Math.min(box.left, viewport - box.width - MAC_BAR_GAP))
|
|
2708
|
+
bar.style.display = ''
|
|
2709
|
+
bar.style.top = `${box.top}px`
|
|
2710
|
+
bar.style.left = `${left}px`
|
|
2711
|
+
bar.style.right = 'auto'
|
|
2712
|
+
bar.style.width = `${box.width}px`
|
|
2713
|
+
return
|
|
2426
2714
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2715
|
+
const top = Math.round(rect.top + (rect.height - MAC_BAR_HEIGHT) / 2)
|
|
2716
|
+
const box = fitBar(gap, top)
|
|
2717
|
+
bar.style.display = ''
|
|
2718
|
+
bar.style.top = `${box.top}px`
|
|
2719
|
+
bar.style.left = `${box.left}px`
|
|
2720
|
+
bar.style.right = 'auto'
|
|
2721
|
+
bar.style.width = `${box.width}px`
|
|
2722
|
+
remember({ top, viewport: Math.round(globalThis.innerWidth ?? 0), gap })
|
|
2723
|
+
return
|
|
2429
2724
|
}
|
|
2430
|
-
|
|
2725
|
+
let menuWidth = 0
|
|
2726
|
+
try {
|
|
2727
|
+
const menu = typeof document.querySelector === 'function' ? document.querySelector(`[${WINDOWS_MENU_ATTRIBUTE}]`) : null
|
|
2728
|
+
if (menu !== null && menu !== undefined && typeof menu.getBoundingClientRect === 'function') menuWidth = menu.getBoundingClientRect().width || 0
|
|
2729
|
+
} catch {
|
|
2730
|
+
menuWidth = 0
|
|
2731
|
+
}
|
|
2732
|
+
bar.style.left = `${WINDOWS_MENU_START + menuWidth + TITLEBAR_GAP}px`
|
|
2733
|
+
bar.style.right = `${TITLEBAR_CAPTION_WIDTH}px`
|
|
2734
|
+
bar.style.width = 'auto'
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Place the bar at most once per frame.
|
|
2738
|
+
*
|
|
2739
|
+
* A streaming answer mutates the document constantly, and every mutation
|
|
2740
|
+
* would otherwise re-measure the header row. Coalescing the runs into one
|
|
2741
|
+
* frame keeps the bar where it belongs without measuring the header dozens
|
|
2742
|
+
* of times a second.
|
|
2743
|
+
*/
|
|
2744
|
+
let placeFrame
|
|
2745
|
+
const schedulePlace = () => {
|
|
2746
|
+
if (placeFrame !== undefined) return
|
|
2747
|
+
placeFrame = attemptValue(() => globalThis.requestAnimationFrame?.(() => {
|
|
2748
|
+
placeFrame = undefined
|
|
2749
|
+
place()
|
|
2750
|
+
}))
|
|
2751
|
+
// No animation frames in this host: place it straight away.
|
|
2752
|
+
if (placeFrame === undefined) place()
|
|
2753
|
+
}
|
|
2754
|
+
place()
|
|
2431
2755
|
/** How the bar looks right now: idle, waiting for a link to land, or refused. */
|
|
2432
2756
|
const mark = (state, hint) => {
|
|
2433
2757
|
if (typeof bar.setAttribute === 'function') bar.setAttribute('data-dsh-url-router-state', state)
|
|
@@ -2510,11 +2834,31 @@ window.__ModuleLoader__.load({
|
|
|
2510
2834
|
settle(fragment, false)
|
|
2511
2835
|
}
|
|
2512
2836
|
input.addEventListener('keydown', onKeyDown)
|
|
2837
|
+
// Both platforms mount the bar in the body: Windows pins it into the
|
|
2838
|
+
// title-bar strip, macOS pins it over the resident header row.
|
|
2513
2839
|
document.body.appendChild(bar)
|
|
2840
|
+
// The first placement measured the header before the bar was in the
|
|
2841
|
+
// document. Place it again now that it is, so the bar's own box is laid
|
|
2842
|
+
// out and can be checked, and so the first paint is already correct.
|
|
2843
|
+
place()
|
|
2844
|
+
/**
|
|
2845
|
+
* The shell re-renders the session header whenever the view changes, which
|
|
2846
|
+
* takes the bar with it. A watcher puts it back where it belongs — the same
|
|
2847
|
+
* node, so what was typed into it survives.
|
|
2848
|
+
*/
|
|
2849
|
+
let headerWatcher
|
|
2850
|
+
if (macInHeader && typeof MutationObserver === 'function') {
|
|
2851
|
+
try {
|
|
2852
|
+
headerWatcher = new MutationObserver(() => schedulePlace())
|
|
2853
|
+
headerWatcher.observe(document.body, { childList: true, subtree: true })
|
|
2854
|
+
} catch {
|
|
2855
|
+
headerWatcher = undefined
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2514
2858
|
const offRoute = subscribe(show)
|
|
2515
2859
|
let onResize
|
|
2516
2860
|
try {
|
|
2517
|
-
onResize = () =>
|
|
2861
|
+
onResize = () => schedulePlace()
|
|
2518
2862
|
window.addEventListener('resize', onResize)
|
|
2519
2863
|
} catch {
|
|
2520
2864
|
onResize = undefined
|
|
@@ -2524,6 +2868,7 @@ window.__ModuleLoader__.load({
|
|
|
2524
2868
|
stopSettle()
|
|
2525
2869
|
input.removeEventListener?.('keydown', onKeyDown)
|
|
2526
2870
|
if (onResize !== undefined) window.removeEventListener?.('resize', onResize)
|
|
2871
|
+
headerWatcher?.disconnect?.()
|
|
2527
2872
|
bar.remove?.()
|
|
2528
2873
|
style?.remove?.()
|
|
2529
2874
|
}
|