dsh-width-tiers 1.0.8 → 1.0.9
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.md +71 -96
- package/README.zh.md +54 -71
- package/lib/client.js +356 -96
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,55 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">dsh-width-tiers</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">Chat content-width tiers for the DeepSeek Harness Web GUI.</p>
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
width
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/dsh-width-tiers"><img alt="npm" src="https://img.shields.io/npm/v/dsh-width-tiers?style=flat-square"></a>
|
|
7
|
+
<a href="https://github.com/aaronlei/dsh-width-tiers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/aaronlei/dsh-width-tiers?style=flat-square"></a>
|
|
8
|
+
<a href="LICENSE"><img alt="MIT" src="https://img.shields.io/badge/license-MIT-yellow?style=flat-square"></a>
|
|
9
|
+
<a href="#compatibility"><img alt="DSH 0.1.7+" src="https://img.shields.io/badge/DSH-0.1.7%2B-blue?style=flat-square"></a>
|
|
10
|
+
</p>
|
|
9
11
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
The DSH Web app caps the message column — and every table inside it — at
|
|
13
|
-
`--dsh-chat-content-width` (0.1.7–0.2.0 default `clamp(680px, column × 0.64,
|
|
14
|
-
920px)`), no matter how wide your window is, so wide tables always need
|
|
15
|
-
horizontal scrolling.
|
|
16
|
-
|
|
17
|
-
This plugin sets the width per tier through the app's **native width axis**
|
|
18
|
-
(`--dsh-chat-user-width` + the app's `dsh.conversation.contentWidth`
|
|
19
|
-
preference), letting the app clamp every value to "column − 176px handle
|
|
20
|
-
budget" — so the built-in drag handles keep working:
|
|
12
|
+
## Tiers
|
|
21
13
|
|
|
22
14
|
| Tier | Content width | Sidebar |
|
|
23
|
-
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Before the first drag, picking custom does not silently keep a width: a guide
|
|
49
|
-
appears at the conversation's right edge and highlights the width handle.
|
|
50
|
-
While dragging, a bubble shows the live transcript width; releasing records it
|
|
51
|
-
as the custom value. Every later handle drag opens the same highlight + live
|
|
52
|
-
readout, no matter which tier was active.
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Standard 标准 | App default: `max(680px, min(column × 0.64, 920px))` | unchanged |
|
|
17
|
+
| Medium 中等 | 1120px | unchanged |
|
|
18
|
+
| Wide 宽 | 1280px | unchanged |
|
|
19
|
+
| Ultra 超宽 | 1400px | unchanged |
|
|
20
|
+
| Full 全宽 | `max(640px, column − 176px)` | collapsed to icon rail |
|
|
21
|
+
| Custom 自定义 | width recorded from a native drag | unchanged |
|
|
22
|
+
|
|
23
|
+
Fixed and custom widths are clamped by the app to
|
|
24
|
+
`max(640px, column − 176px)`; the standard tier follows the app default
|
|
25
|
+
(680–920px). On a narrow window a wider fixed tier therefore behaves like
|
|
26
|
+
`full`.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- Uses the app's native width axis (`--dsh-chat-user-width` and
|
|
31
|
+
`dsh.conversation.contentWidth`), so the built-in drag handles keep working.
|
|
32
|
+
- Persists the selected tier and restores it on the next visit.
|
|
33
|
+
- Follows **Settings → Language** (中文 / English).
|
|
34
|
+
- Menu supports ArrowUp/Down, Home/End, Escape and Tab, with focus restoration.
|
|
35
|
+
- Tier changes sync across tabs.
|
|
36
|
+
- Respects `prefers-reduced-motion`.
|
|
37
|
+
- Never touches the right panel. Only `full` collapses the left sidebar, and
|
|
38
|
+
you can expand it again at any time.
|
|
53
39
|
|
|
54
40
|
## Install
|
|
55
41
|
|
|
@@ -57,68 +43,57 @@ readout, no matter which tier was active.
|
|
|
57
43
|
dsh plugin --profile web add dsh-width-tiers
|
|
58
44
|
```
|
|
59
45
|
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
Restart `dsh web`, then hard-refresh the page. A six-bar picker appears at the
|
|
47
|
+
bottom-right.
|
|
62
48
|
|
|
63
|
-
|
|
64
|
-
(Ctrl+Shift+R). A round button with five bars appears at the bottom-right.
|
|
49
|
+
For a local checkout:
|
|
65
50
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
> plugin source, re-sync the copy (e.g. `rsync -a --delete --exclude .git
|
|
70
|
-
> /path/to/dsh-width-tiers/ ~/.dsh/profiles/web/node_modules/dsh-width-tiers/`)
|
|
71
|
-
> or re-install after removing it, then restart `dsh web` and hard-refresh.
|
|
51
|
+
```bash
|
|
52
|
+
dsh plugin --profile web add link:/path/to/dsh-width-tiers
|
|
53
|
+
```
|
|
72
54
|
|
|
73
55
|
## Usage
|
|
74
56
|
|
|
75
|
-
- Click the
|
|
76
|
-
- `
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
57
|
+
- Click the picker and choose a tier.
|
|
58
|
+
- Picking `Custom` before the first drag opens a guide at the right edge; drag
|
|
59
|
+
a native handle to record a width.
|
|
60
|
+
- Dragging any native handle switches to `Custom`; the plugin steps aside while
|
|
61
|
+
you drag.
|
|
62
|
+
- `Standard` clears the width preference and returns to the app default.
|
|
63
|
+
- `Full` collapses the left sidebar; the icon rail stays available.
|
|
82
64
|
|
|
83
|
-
##
|
|
65
|
+
## How it works
|
|
84
66
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
67
|
+
- Locates the conversation body that declares `--dsh-chat-content-width`
|
|
68
|
+
(prefers `[data-conversation-content]`, skips the right sidebar's embedded
|
|
69
|
+
conversation), then writes `--dsh-chat-user-width` on its parent.
|
|
70
|
+
- Stores the configured target in the app preference; only the inline width is
|
|
71
|
+
clamped to `max(640px, column − 176px)`.
|
|
72
|
+
- Watches the column with `ResizeObserver` so `full` follows sidebar, rightbar
|
|
73
|
+
and window changes.
|
|
74
|
+
- Drives panels through `ctx.layout` and registers copy through `ctx.locale`.
|
|
90
75
|
|
|
91
|
-
##
|
|
76
|
+
## Compatibility
|
|
77
|
+
|
|
78
|
+
- DSH Web 0.1.7+ with a `web` profile.
|
|
79
|
+
- Width axis checked against DSH 0.2.1-rc.2: 640px floor, 176px handle budget,
|
|
80
|
+
`--dsh-chat-*` variables, `data-width-handle`, layout/locale services.
|
|
92
81
|
|
|
93
|
-
|
|
94
|
-
runtime** (the first element whose computed value is non-empty) and the
|
|
95
|
-
tier is applied as an inline override — no hashed class names, so it
|
|
96
|
-
survives bundle upgrades.
|
|
97
|
-
- Panels are driven through the **layout service** (`ctx.layout`), never by
|
|
98
|
-
simulating clicks.
|
|
99
|
-
- The sidebar is only ever collapsed by the `full` tier; returning to
|
|
100
|
-
`standard` restores it only if this plugin collapsed it.
|
|
101
|
-
- Dictionaries are registered with the app's **locale service**
|
|
102
|
-
(`ctx.locale.register` / `bind`) and re-rendered through its change
|
|
103
|
-
subscription, so the copy always matches the app's active language.
|
|
104
|
-
|
|
105
|
-
## Limitations
|
|
106
|
-
|
|
107
|
-
- Content width cannot exceed the window: on a narrow window, wider tiers are
|
|
108
|
-
capped by the available column (the picker keeps showing the nominal value;
|
|
109
|
-
the live bubble during a drag shows the applied width). Use `full` (which
|
|
110
|
-
also collapses the sidebar) for the maximum.
|
|
111
|
-
- Requires the DSH Web client plugin system (dsh ≥ 0.1.0-rc.x with a `web`
|
|
112
|
-
profile). Verified on dsh 0.1.7-alpha.1 and 0.2.0-rc.2; the 0.2.1-rc.1
|
|
113
|
-
width axis matches by bundle diff.
|
|
114
|
-
|
|
115
|
-
## Uninstall
|
|
82
|
+
## Development
|
|
116
83
|
|
|
117
84
|
```bash
|
|
118
|
-
|
|
85
|
+
npm test
|
|
119
86
|
```
|
|
120
87
|
|
|
121
|
-
|
|
88
|
+
The test loads the shipped `lib/client.js` bundle through a DOM stub; no live
|
|
89
|
+
DSH instance is required. The package ships prebuilt `lib/` and does not
|
|
90
|
+
include a `src/` tree.
|
|
91
|
+
|
|
92
|
+
## Notes
|
|
93
|
+
|
|
94
|
+
- Wider fixed tiers are clamped on narrow windows.
|
|
95
|
+
- A custom width dragged on a narrow window is clamped by the app before it is
|
|
96
|
+
recorded.
|
|
122
97
|
|
|
123
98
|
## License
|
|
124
99
|
|
package/README.zh.md
CHANGED
|
@@ -1,45 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">dsh-width-tiers</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">DeepSeek Harness Web GUI 的对话区内容宽度档位插件。</p>
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/dsh-width-tiers"><img alt="npm" src="https://img.shields.io/npm/v/dsh-width-tiers?style=flat-square"></a>
|
|
7
|
+
<a href="https://github.com/aaronlei/dsh-width-tiers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/aaronlei/dsh-width-tiers?style=flat-square"></a>
|
|
8
|
+
<a href="LICENSE"><img alt="MIT" src="https://img.shields.io/badge/license-MIT-yellow?style=flat-square"></a>
|
|
9
|
+
<a href="#兼容性"><img alt="DSH 0.1.7+" src="https://img.shields.io/badge/DSH-0.1.7%2B-blue?style=flat-square"></a>
|
|
10
|
+
</p>
|
|
8
11
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
DSH Web 把消息列——包括里面的所有表格——的宽度限制在
|
|
12
|
-
`--dsh-chat-content-width`(0.1.7–0.2.0 默认
|
|
13
|
-
`clamp(680px, 列宽 × 0.64, 920px)`),不管窗口多宽都不变,宽表格只能横向滚动。
|
|
14
|
-
|
|
15
|
-
本插件通过应用自身的**原生宽度通道**(`--dsh-chat-user-width` +
|
|
16
|
-
`dsh.conversation.contentWidth` 偏好)按档位设定宽度,由应用自身钳制到
|
|
17
|
-
「列宽 − 176px 手柄预留」,因此自带的原生拖拽手柄始终可用:
|
|
12
|
+
## 档位
|
|
18
13
|
|
|
19
14
|
| 档位 | 内容宽度 | 侧边栏 |
|
|
20
|
-
|
|
21
|
-
| 标准 |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| 标准 | 应用默认:`max(680px, min(列宽 × 0.64, 920px))` | 不动 |
|
|
22
17
|
| 中等 | 1120px | 不动 |
|
|
23
18
|
| 宽 | 1280px | 不动 |
|
|
24
19
|
| 超宽 | 1400px | 不动 |
|
|
25
|
-
| 全宽 |
|
|
26
|
-
| 自定义 |
|
|
27
|
-
|
|
28
|
-
菜单与按钮标题中:中等 / 宽 / 超宽显示**固定标称值**(1120 / 1280 / 1400px),
|
|
29
|
-
标准显示应用当前默认值(`clamp(680px, 列宽 × 0.64, 920px)`,如 710px),全宽
|
|
30
|
-
显示当前列宽下的最大实际值(列宽 − 176px,如 934px),自定义显示已记录的
|
|
31
|
-
宽度或「未设置」。实际生效宽度仍由应用按「列宽 − 176px」钳制;窄窗口下
|
|
32
|
-
选定较高档位时,效果等同于「全宽」。
|
|
20
|
+
| 全宽 | `max(640px, 列宽 − 176px)` | 收起为图标轨道 |
|
|
21
|
+
| 自定义 | 原生拖拽记录的宽度 | 不动 |
|
|
33
22
|
|
|
34
|
-
|
|
23
|
+
固定档和自定义宽度会被应用钳制到 `max(640px, 列宽 − 176px)`;标准档
|
|
24
|
+
使用应用默认的 680–920px。窄窗口下较宽的固定档等同「全宽」。
|
|
35
25
|
|
|
36
|
-
|
|
37
|
-
的五条横条图标也直观显示当前档位(自定义档全亮)。拖动应用自带的
|
|
38
|
-
对话区边缘手柄会自动切到「自定义」档,插件让位于原生拖拽。
|
|
26
|
+
## 特性
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
- 走应用原生宽度通道(`--dsh-chat-user-width` 与
|
|
29
|
+
`dsh.conversation.contentWidth`),自带拖拽手柄始终可用。
|
|
30
|
+
- 记住当前档位,下次打开自动恢复。
|
|
31
|
+
- 跟随 **设置 → 语言**(中文 / English)。
|
|
32
|
+
- 菜单支持方向键、Home/End、Esc、Tab,并恢复焦点。
|
|
33
|
+
- 档位选择跨标签页同步。
|
|
34
|
+
- 支持 `prefers-reduced-motion`。
|
|
35
|
+
- 不碰右侧面板;只有「全宽」会收起左侧栏,随时可以再展开。
|
|
43
36
|
|
|
44
37
|
## 安装
|
|
45
38
|
|
|
@@ -47,61 +40,51 @@ DSH Web 把消息列——包括里面的所有表格——的宽度限制在
|
|
|
47
40
|
dsh plugin --profile web add dsh-width-tiers
|
|
48
41
|
```
|
|
49
42
|
|
|
50
|
-
|
|
51
|
-
`cordis.patch.yml`。
|
|
43
|
+
重启 `dsh web` 并硬刷新页面。右下角会出现六条横条的圆形按钮。
|
|
52
44
|
|
|
53
|
-
|
|
54
|
-
右下角会出现五条横条的圆形按钮。
|
|
45
|
+
本地目录:
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
> --exclude .git /path/to/dsh-width-tiers/
|
|
60
|
-
> ~/.dsh/profiles/web/node_modules/dsh-width-tiers/`)或先 remove 再 add,
|
|
61
|
-
> 然后重启 `dsh web` 并硬刷新。
|
|
47
|
+
```bash
|
|
48
|
+
dsh plugin --profile web add link:/path/to/dsh-width-tiers
|
|
49
|
+
```
|
|
62
50
|
|
|
63
51
|
## 使用
|
|
64
52
|
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
## 多语言
|
|
72
|
-
|
|
73
|
-
选择器里的所有文案(档位名、按钮的 `aria-label` 和标题)跟随应用的
|
|
74
|
-
**设置 → 语言**(中文 / English),通过应用自身的 locale 服务实现。切换
|
|
75
|
-
语言后按钮和菜单会立即重渲染;未显式选择语言时,应用会回退到浏览器语言,
|
|
76
|
-
最后兜底中文。无需插件侧单独设置。
|
|
53
|
+
- 点击圆形按钮选择档位。
|
|
54
|
+
- 首次选择「自定义」会在右侧边缘显示引导;拖动原生手柄记录宽度。
|
|
55
|
+
- 拖动任意原生手柄都会切换到「自定义」;拖拽期间插件让位于应用。
|
|
56
|
+
- 「标准」清除宽度偏好,回到应用默认宽度。
|
|
57
|
+
- 「全宽」收起左侧栏,图标轨道仍可随时展开。
|
|
77
58
|
|
|
78
59
|
## 原理
|
|
79
60
|
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
订阅其变更事件重渲染,文案始终与应用的当前语言一致。
|
|
61
|
+
- 定位声明 `--dsh-chat-content-width` 的对话 body(优先
|
|
62
|
+
`[data-conversation-content]`,跳过右侧栏的 embedded 对话),在它的
|
|
63
|
+
父级写入 `--dsh-chat-user-width`。
|
|
64
|
+
- 应用偏好保存设定值,只有行内宽度按 `max(640px, 列宽 − 176px)` 钳制。
|
|
65
|
+
- 用 `ResizeObserver` 观察列宽,让「全宽」跟随左右栏与窗口变化。
|
|
66
|
+
- 面板操作走 `ctx.layout`,文案注册走 `ctx.locale`。
|
|
87
67
|
|
|
88
|
-
##
|
|
68
|
+
## 兼容性
|
|
89
69
|
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- 依赖 DSH Web 客户端插件体系(带 web profile 的 dsh ≥ 0.1.0-rc.x)。
|
|
94
|
-
已在 dsh 0.1.7-alpha.1、0.2.0-rc.2 上验证;0.2.1-rc.1 的宽度轴经 bundle
|
|
95
|
-
对比确认一致。
|
|
70
|
+
- DSH Web 0.1.7+,带 `web` profile。
|
|
71
|
+
- 宽度轴已按 DSH 0.2.1-rc.2 对照:640px 下限、176px 手柄预留、
|
|
72
|
+
`--dsh-chat-*` 变量、`data-width-handle`、layout/locale 服务。
|
|
96
73
|
|
|
97
|
-
##
|
|
74
|
+
## 开发
|
|
98
75
|
|
|
99
76
|
```bash
|
|
100
|
-
|
|
77
|
+
npm test
|
|
101
78
|
```
|
|
102
79
|
|
|
103
|
-
|
|
80
|
+
测试通过 DOM stub 加载已发布的 `lib/client.js`,不需要启动真实 DSH。包内
|
|
81
|
+
直接发布预构建的 `lib/`,不包含 `src/`。
|
|
82
|
+
|
|
83
|
+
## 注意
|
|
84
|
+
|
|
85
|
+
- 窄窗口下较宽的固定档位会被应用钳制。
|
|
86
|
+
- 窄窗口下拖出的自定义宽度会先被应用钳制,再被记录。
|
|
104
87
|
|
|
105
88
|
## 许可证
|
|
106
89
|
|
|
107
|
-
MIT
|
|
90
|
+
MIT — 见 [LICENSE](LICENSE)。Copyright (c) 2026 aaronlei。
|
package/lib/client.js
CHANGED
|
@@ -41,6 +41,15 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
* opens the live width readout, so repeat drags keep the highlight
|
|
42
42
|
* and the px bubble.
|
|
43
43
|
*
|
|
44
|
+
* The app preference stores the tier's configured target (nominal px,
|
|
45
|
+
* the recorded custom width, or the live cap for full); only the
|
|
46
|
+
* inline value is clamped. This lets the app restore the target when
|
|
47
|
+
* the window widens instead of staying pinned to a narrow clamp.
|
|
48
|
+
* The picker icon has one bar per tier (full lights five, custom all
|
|
49
|
+
* six), tier changes sync across tabs, the menu supports
|
|
50
|
+
* ArrowUp/Down/Home/End/Escape, and the sidebar's embedded
|
|
51
|
+
* Conversation is excluded from the width-root lookup.
|
|
52
|
+
*
|
|
44
53
|
* - Panels are driven through the layout service (ctx.layout), never
|
|
45
54
|
* by simulating clicks.
|
|
46
55
|
* - The sidebar is only ever collapsed by the `full` tier, through the
|
|
@@ -148,11 +157,13 @@ window.__ModuleLoader__.load({
|
|
|
148
157
|
"#dsh-wide-toggle:not(:hover):not(:focus-visible) svg rect{width:6px;x:5px}",
|
|
149
158
|
"#dsh-wide-toggle.active{background:var(--dsw-alias-state-business-primary);border-color:transparent;color:var(--dsw-static-neutral-00);opacity:1}",
|
|
150
159
|
"#dsh-wide-toggle.pulse{animation:dsh-wide-pulse .65s ease-out}",
|
|
160
|
+
"@media (prefers-reduced-motion:reduce){#dsh-wide-toggle{transition:none}#dsh-wide-toggle.pulse{animation:none}}",
|
|
151
161
|
"@keyframes dsh-wide-pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--dsw-alias-state-business-primary) 60%,transparent)}100%{box-shadow:0 0 0 14px transparent}}",
|
|
152
162
|
"#dsh-width-menu{position:fixed;right:20px;bottom:264px;z-index:1000;min-width:196px;padding:6px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-specific-menu);box-shadow:0 8px 28px rgba(0,0,0,.24);display:flex;flex-direction:column;gap:2px}",
|
|
153
163
|
"#dsh-width-menu[hidden]{display:none}",
|
|
154
164
|
"#dsh-width-menu button{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 12px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:18px;text-align:left;cursor:pointer}",
|
|
155
165
|
"#dsh-width-menu button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
166
|
+
"#dsh-width-menu button:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}",
|
|
156
167
|
"#dsh-width-menu button.selected{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-state-business-primary)}",
|
|
157
168
|
"#dsh-width-menu .dshwm-value{color:var(--dsw-alias-label-tertiary);font-size:12px}",
|
|
158
169
|
"#dsh-width-menu button.selected .dshwm-value{color:var(--dsw-alias-state-business-primary)}",
|
|
@@ -160,8 +171,8 @@ window.__ModuleLoader__.load({
|
|
|
160
171
|
"#dsh-width-guide[hidden]{display:none}",
|
|
161
172
|
"#dsh-width-guide svg{flex:none;color:var(--dsw-alias-state-business-primary)}",
|
|
162
173
|
"#dsh-width-guide[data-mode=live]{font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}",
|
|
163
|
-
"body[data-dsh-width-guide] [data-width-handle
|
|
164
|
-
"body[data-dsh-width-guide] [data-width-handle
|
|
174
|
+
"body[data-dsh-width-guide] [data-width-handle]{background:linear-gradient(to bottom,transparent calc(50% - 64px),color-mix(in srgb,var(--dsw-alias-state-business-primary) 18%,transparent) calc(50% - 64px),color-mix(in srgb,var(--dsw-alias-state-business-primary) 18%,transparent) calc(50% + 64px),transparent calc(50% + 64px));border-radius:3px}",
|
|
175
|
+
"body[data-dsh-width-guide] [data-width-handle]::after{opacity:1}"
|
|
165
176
|
].join("");
|
|
166
177
|
//#endregion
|
|
167
178
|
|
|
@@ -241,6 +252,13 @@ window.__ModuleLoader__.load({
|
|
|
241
252
|
let widthRoot = null;
|
|
242
253
|
let widthPath = null;
|
|
243
254
|
|
|
255
|
+
/* Whether this plugin wrote the app's width preference. Native
|
|
256
|
+
* drags also write it, so unload only removes a value this
|
|
257
|
+
* plugin owns. */
|
|
258
|
+
let preferenceOwned = false;
|
|
259
|
+
let preferenceWritten = null;
|
|
260
|
+
let pulseTimer = null;
|
|
261
|
+
|
|
244
262
|
function hasWidthVar(el) {
|
|
245
263
|
return getComputedStyle(el)
|
|
246
264
|
.getPropertyValue("--dsh-chat-content-width")
|
|
@@ -259,14 +277,35 @@ window.__ModuleLoader__.load({
|
|
|
259
277
|
widthPath = path.length > 0 ? path : null;
|
|
260
278
|
}
|
|
261
279
|
|
|
280
|
+
/* Sidebar chat tabs render an embedded Conversation whose
|
|
281
|
+
* `--dsh-chat-content-width` is a local min() and never reads
|
|
282
|
+
* `--dsh-chat-user-width`. Writing there would be a no-op on
|
|
283
|
+
* the visible column and could starve the real transcript. */
|
|
284
|
+
function isSidebarConversation(el) {
|
|
285
|
+
return typeof el.closest === "function"
|
|
286
|
+
&& el.closest("[data-sidebar-chat]") !== null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function firstWidthRoot(list) {
|
|
290
|
+
for (let i = 0; i < list.length; i++) {
|
|
291
|
+
const el = list[i];
|
|
292
|
+
if (isSidebarConversation(el)) continue;
|
|
293
|
+
if (hasWidthVar(el)) return el;
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
262
298
|
function scanWidthRoot() {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
299
|
+
/* Prefer the stable Conversation data attribute (one or two
|
|
300
|
+
* elements) over every classed node in the document. The
|
|
301
|
+
* [class] walk remains the compatibility fallback for older
|
|
302
|
+
* clients that do not publish data-conversation-content. */
|
|
303
|
+
let found = firstWidthRoot(document.querySelectorAll("[data-conversation-content]"));
|
|
304
|
+
if (found === null) found = firstWidthRoot(document.querySelectorAll("[class]"));
|
|
305
|
+
if (found !== null) {
|
|
306
|
+
widthRoot = found;
|
|
307
|
+
recordWidthPath(widthRoot);
|
|
308
|
+
return widthRoot;
|
|
270
309
|
}
|
|
271
310
|
widthRoot = null;
|
|
272
311
|
widthPath = null;
|
|
@@ -274,14 +313,16 @@ window.__ModuleLoader__.load({
|
|
|
274
313
|
}
|
|
275
314
|
|
|
276
315
|
function findWidthRoot(allowScan) {
|
|
277
|
-
if (widthRoot !== null && document.contains(widthRoot)
|
|
316
|
+
if (widthRoot !== null && document.contains(widthRoot)
|
|
317
|
+
&& !isSidebarConversation(widthRoot)) return widthRoot;
|
|
278
318
|
if (widthPath !== null) {
|
|
279
319
|
let el = document.body;
|
|
280
320
|
for (let i = 0; i < widthPath.length; i++) {
|
|
281
321
|
el = el.children[widthPath[i]];
|
|
282
322
|
if (!el) break;
|
|
283
323
|
}
|
|
284
|
-
if (el && el !== widthRoot && document.contains(el)
|
|
324
|
+
if (el && el !== widthRoot && document.contains(el)
|
|
325
|
+
&& !isSidebarConversation(el) && hasWidthVar(el)) {
|
|
285
326
|
widthRoot = el;
|
|
286
327
|
return widthRoot;
|
|
287
328
|
}
|
|
@@ -310,25 +351,34 @@ window.__ModuleLoader__.load({
|
|
|
310
351
|
}
|
|
311
352
|
}
|
|
312
353
|
|
|
313
|
-
/*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
|
|
317
|
-
|
|
354
|
+
/* The tier's configured target, before the app's column clamp.
|
|
355
|
+
* This is the value persisted to the app preference: on a
|
|
356
|
+
* narrow column the app clamps it for display, and when the
|
|
357
|
+
* window widens it restores the configured target instead of
|
|
358
|
+
* staying pinned to the old clamp. `full` is inherently the
|
|
359
|
+
* live maximum. */
|
|
360
|
+
function tierTargetPx(tier, root) {
|
|
318
361
|
switch (tier) {
|
|
319
362
|
case "medium":
|
|
320
363
|
case "wide":
|
|
321
364
|
case "ultra":
|
|
322
|
-
return
|
|
323
|
-
case "full":
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return dragged === null ? null : Math.min(dragged, cap);
|
|
327
|
-
}
|
|
365
|
+
return NOMINAL_WIDTH_PX[tier];
|
|
366
|
+
case "full":
|
|
367
|
+
return Math.max(CONTENT_MIN_PX, root.offsetWidth - CONTENT_EDGE_BUDGET_PX);
|
|
368
|
+
case "custom": return readStoredNumber(CUSTOM_KEY);
|
|
328
369
|
default: return null; /* standard: app default */
|
|
329
370
|
}
|
|
330
371
|
}
|
|
331
372
|
|
|
373
|
+
/* The px actually applied to the column (same floor/max as the
|
|
374
|
+
* app's resolveContentWidth). */
|
|
375
|
+
function appliedTierPx(tier, root) {
|
|
376
|
+
const target = tierTargetPx(tier, root);
|
|
377
|
+
if (target === null) return null;
|
|
378
|
+
const cap = Math.max(CONTENT_MIN_PX, root.offsetWidth - CONTENT_EDGE_BUDGET_PX);
|
|
379
|
+
return Math.min(Math.max(target, CONTENT_MIN_PX), cap);
|
|
380
|
+
}
|
|
381
|
+
|
|
332
382
|
/* The transcript width the app currently resolves for the live
|
|
333
383
|
* column (after its own clamp), used by the custom-drag guide.
|
|
334
384
|
* During a drag the app writes the clamped px to the target's
|
|
@@ -355,28 +405,47 @@ window.__ModuleLoader__.load({
|
|
|
355
405
|
/* custom before the first drag has no width of its own —
|
|
356
406
|
* leave whatever the app last published in place. */
|
|
357
407
|
if (tier === "custom" && readStoredNumber(CUSTOM_KEY) === null) return;
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
360
|
-
|
|
408
|
+
const targetPx = tierTargetPx(tier, root);
|
|
409
|
+
if (targetPx === null) {
|
|
410
|
+
/* standard: hand the width back to the app default. The
|
|
411
|
+
* app's width preference is cleared as well; standard is
|
|
412
|
+
* deliberately the reset to the app's default width. */
|
|
413
|
+
if (target.style.getPropertyValue("--dsh-chat-user-width") !== "" || preferenceOwned) {
|
|
361
414
|
target.style.removeProperty("--dsh-chat-user-width");
|
|
362
415
|
try {
|
|
363
416
|
window.localStorage.removeItem(APP_PREF_KEY);
|
|
364
417
|
} catch (e) {
|
|
365
418
|
/* ignore */
|
|
366
419
|
}
|
|
420
|
+
preferenceOwned = false;
|
|
421
|
+
preferenceWritten = null;
|
|
367
422
|
}
|
|
368
423
|
return;
|
|
369
424
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
*
|
|
373
|
-
|
|
374
|
-
if (
|
|
375
|
-
|
|
425
|
+
const laidOut = root.offsetWidth > 0;
|
|
426
|
+
/* `full` derives its target from the live column; without a
|
|
427
|
+
* layout box it would collapse to the 640px floor and pin a
|
|
428
|
+
* narrow width. Wait for the fallback/resize pass. */
|
|
429
|
+
if (!laidOut && tier === "full") return;
|
|
430
|
+
if (laidOut) {
|
|
431
|
+
const px = appliedTierPx(tier, root);
|
|
432
|
+
if (px !== null) {
|
|
433
|
+
const next = String(Math.round(px));
|
|
434
|
+
if (target.style.getPropertyValue("--dsh-chat-user-width") !== next + "px") {
|
|
435
|
+
target.style.setProperty("--dsh-chat-user-width", next + "px");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
376
438
|
}
|
|
439
|
+
/* Persist the unclamped target even while the root is not
|
|
440
|
+
* laid out (fixed tiers / recorded custom width do not
|
|
441
|
+
* depend on the column). The app clamps it for display and
|
|
442
|
+
* restores it after a resize / remount. */
|
|
443
|
+
const preference = String(Math.round(targetPx));
|
|
377
444
|
try {
|
|
378
|
-
if (window.localStorage.getItem(APP_PREF_KEY) !==
|
|
379
|
-
window.localStorage.setItem(APP_PREF_KEY,
|
|
445
|
+
if (window.localStorage.getItem(APP_PREF_KEY) !== preference) {
|
|
446
|
+
window.localStorage.setItem(APP_PREF_KEY, preference);
|
|
447
|
+
preferenceOwned = true;
|
|
448
|
+
preferenceWritten = preference;
|
|
380
449
|
}
|
|
381
450
|
} catch (e) {
|
|
382
451
|
/* ignore */
|
|
@@ -386,25 +455,30 @@ window.__ModuleLoader__.load({
|
|
|
386
455
|
/* true while the CURRENT tier is the one that collapsed the sidebar */
|
|
387
456
|
let collapsedByTier = false;
|
|
388
457
|
|
|
389
|
-
|
|
458
|
+
/* `persist` is false when another tab already wrote the tier:
|
|
459
|
+
* adopting it must not write localStorage again. */
|
|
460
|
+
function applyTier(tier, persist = true) {
|
|
390
461
|
body().setAttribute("data-dsh-width", tier);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
462
|
+
if (persist) {
|
|
463
|
+
try {
|
|
464
|
+
window.localStorage.setItem(KEY, tier);
|
|
465
|
+
} catch (e) {
|
|
466
|
+
/* storage unavailable — state still applies this session */
|
|
467
|
+
}
|
|
395
468
|
}
|
|
396
469
|
applyContentWidth(tier);
|
|
397
470
|
if (tier === "full") {
|
|
398
471
|
if (!isSidebarCollapsed()) {
|
|
399
472
|
collapseSidebar();
|
|
400
473
|
collapsedByTier = true;
|
|
401
|
-
} else {
|
|
402
|
-
collapsedByTier = false;
|
|
403
474
|
}
|
|
475
|
+
/* already collapsed: don't claim ownership we may not have */
|
|
404
476
|
} else if (tier === "standard") {
|
|
405
477
|
if (collapsedByTier && isSidebarCollapsed()) expandSidebar();
|
|
406
478
|
collapsedByTier = false;
|
|
407
|
-
} else {
|
|
479
|
+
} else if (!isSidebarCollapsed()) {
|
|
480
|
+
/* the user (or another panel) expanded it — the plugin no
|
|
481
|
+
* longer owns the collapsed state */
|
|
408
482
|
collapsedByTier = false;
|
|
409
483
|
}
|
|
410
484
|
syncButton();
|
|
@@ -424,11 +498,13 @@ window.__ModuleLoader__.load({
|
|
|
424
498
|
for (let i = 0; i < TIERS.length; i++) {
|
|
425
499
|
if (TIERS[i].id === tier) idx = i;
|
|
426
500
|
}
|
|
427
|
-
|
|
501
|
+
/* One bar per tier so full (5 bars) and custom (6 bars,
|
|
502
|
+
* all lit) remain distinguishable. */
|
|
503
|
+
const widths = [3.5, 5.3, 7.1, 8.9, 10.7, 12.5];
|
|
428
504
|
let bars = "";
|
|
429
|
-
for (let b = 0; b <
|
|
430
|
-
const y = 0.
|
|
431
|
-
bars += `<rect x="1.5" y="${y}" width="${widths[b]}" height="
|
|
505
|
+
for (let b = 0; b < TIERS.length; b++) {
|
|
506
|
+
const y = 0.75 + b * 2.55;
|
|
507
|
+
bars += `<rect x="1.5" y="${y}" width="${widths[b]}" height="1.6" rx="0.8" fill="currentColor" opacity="${b <= idx ? 1 : 0.3}"/>`;
|
|
432
508
|
}
|
|
433
509
|
return `<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">${bars}</svg>`;
|
|
434
510
|
}
|
|
@@ -493,6 +569,9 @@ window.__ModuleLoader__.load({
|
|
|
493
569
|
item.type = "button";
|
|
494
570
|
item.setAttribute("role", "menuitemradio");
|
|
495
571
|
item.setAttribute("data-tier", def.id);
|
|
572
|
+
/* Roving focus: the menu is opened by focusing one item, and
|
|
573
|
+
* ArrowUp/Down/Home/End move between items. */
|
|
574
|
+
item.tabIndex = -1;
|
|
496
575
|
item.innerHTML = `<span class="dshwm-label">${t(def.key)}</span><span class="dshwm-value">${tierValueText(def.id)}</span>`;
|
|
497
576
|
item.addEventListener("click", (e) => {
|
|
498
577
|
if (e && e.stopPropagation) e.stopPropagation();
|
|
@@ -500,12 +579,12 @@ window.__ModuleLoader__.load({
|
|
|
500
579
|
* open the right-edge guide instead of applying a value. */
|
|
501
580
|
if (def.id === "custom" && readStoredNumber(CUSTOM_KEY) === null) {
|
|
502
581
|
beginCustomGuide();
|
|
503
|
-
hideMenu();
|
|
582
|
+
hideMenu(true);
|
|
504
583
|
return;
|
|
505
584
|
}
|
|
506
585
|
endGuide(false);
|
|
507
586
|
applyTier(def.id);
|
|
508
|
-
hideMenu();
|
|
587
|
+
hideMenu(true);
|
|
509
588
|
});
|
|
510
589
|
menu.appendChild(item);
|
|
511
590
|
}
|
|
@@ -532,25 +611,36 @@ window.__ModuleLoader__.load({
|
|
|
532
611
|
}
|
|
533
612
|
}
|
|
534
613
|
|
|
614
|
+
function menuItems() {
|
|
615
|
+
return Array.from(menu.querySelectorAll("button[data-tier]"));
|
|
616
|
+
}
|
|
617
|
+
|
|
535
618
|
function showMenu() {
|
|
536
619
|
menu.hidden = false;
|
|
537
620
|
btn.setAttribute("aria-expanded", "true");
|
|
538
621
|
syncMenu();
|
|
622
|
+
const items = menuItems();
|
|
623
|
+
const selected = items.find((item) => item.getAttribute("data-tier") === currentTier()) || items[0];
|
|
624
|
+
if (selected && typeof selected.focus === "function") selected.focus();
|
|
539
625
|
}
|
|
540
626
|
|
|
541
|
-
|
|
627
|
+
/* restoreFocus is true for Escape / item selection, false for a
|
|
628
|
+
* click-away so the click target keeps focus. */
|
|
629
|
+
function hideMenu(restoreFocus) {
|
|
630
|
+
const hadFocus = menu.contains(document.activeElement);
|
|
542
631
|
menu.hidden = true;
|
|
543
632
|
btn.setAttribute("aria-expanded", "false");
|
|
633
|
+
if (restoreFocus && hadFocus && typeof btn.focus === "function") btn.focus();
|
|
544
634
|
}
|
|
545
635
|
|
|
546
636
|
btn.addEventListener("click", (e) => {
|
|
547
637
|
if (e && e.stopPropagation) e.stopPropagation();
|
|
548
638
|
if (menu.hidden) showMenu();
|
|
549
|
-
else hideMenu();
|
|
639
|
+
else hideMenu(true);
|
|
550
640
|
});
|
|
551
641
|
|
|
552
642
|
const onDocClick = (e) => {
|
|
553
|
-
if (!menu.hidden && e.target !== btn && !menu.contains(e.target)) hideMenu();
|
|
643
|
+
if (!menu.hidden && e.target !== btn && !menu.contains(e.target)) hideMenu(false);
|
|
554
644
|
/* Clicking anywhere but the guide's handle cancels a
|
|
555
645
|
* pending custom guide and restores the previous tier. */
|
|
556
646
|
if (guideActive && !guideDragging && e.target !== btn && !menu.contains(e.target) && !isHandleEvent(e)) {
|
|
@@ -558,9 +648,29 @@ window.__ModuleLoader__.load({
|
|
|
558
648
|
}
|
|
559
649
|
};
|
|
560
650
|
const onDocKey = (e) => {
|
|
561
|
-
if (e.key
|
|
562
|
-
|
|
563
|
-
|
|
651
|
+
if (e.key === "Escape") {
|
|
652
|
+
if (!menu.hidden) hideMenu(true);
|
|
653
|
+
if (guideActive) endGuide(true);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (menu.hidden) return;
|
|
657
|
+
if (e.key === "Tab") {
|
|
658
|
+
/* Let Tab move on, but do not leave an orphaned menu. */
|
|
659
|
+
hideMenu(false);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const items = menuItems();
|
|
663
|
+
if (items.length === 0) return;
|
|
664
|
+
const current = items.indexOf(document.activeElement);
|
|
665
|
+
let next = -1;
|
|
666
|
+
if (e.key === "ArrowDown") next = current < 0 ? 0 : (current + 1) % items.length;
|
|
667
|
+
else if (e.key === "ArrowUp") next = current < 0 ? items.length - 1 : (current - 1 + items.length) % items.length;
|
|
668
|
+
else if (e.key === "Home") next = 0;
|
|
669
|
+
else if (e.key === "End") next = items.length - 1;
|
|
670
|
+
if (next < 0) return;
|
|
671
|
+
e.preventDefault();
|
|
672
|
+
const item = items[next];
|
|
673
|
+
if (typeof item.focus === "function") item.focus();
|
|
564
674
|
};
|
|
565
675
|
document.addEventListener("click", onDocClick);
|
|
566
676
|
document.addEventListener("keydown", onDocKey);
|
|
@@ -589,6 +699,7 @@ window.__ModuleLoader__.load({
|
|
|
589
699
|
* open the same bubble but never "cancel" a tier. */
|
|
590
700
|
let guideFromMenu = false;
|
|
591
701
|
let guideHideTimer = null;
|
|
702
|
+
let guideHandle = null;
|
|
592
703
|
let tierBeforeGuide = "standard";
|
|
593
704
|
|
|
594
705
|
/* The px the app currently resolves for the transcript, after
|
|
@@ -599,14 +710,17 @@ window.__ModuleLoader__.load({
|
|
|
599
710
|
return px === null ? "—" : `${px}px`;
|
|
600
711
|
}
|
|
601
712
|
|
|
602
|
-
/* Keep the bubble beside the
|
|
603
|
-
* dragged edge); fall back to the
|
|
604
|
-
* handle
|
|
713
|
+
/* Keep the bubble beside the handle the user actually grabbed
|
|
714
|
+
* (it moves with the dragged edge); fall back to the right
|
|
715
|
+
* handle / frame edge when the handle is not mounted yet
|
|
716
|
+
* (menu-opened guide, hero or settling phase). */
|
|
605
717
|
function positionGuide() {
|
|
606
718
|
if (!guideActive) return;
|
|
607
|
-
const handle = document.
|
|
719
|
+
const handle = guideHandle !== null && document.contains(guideHandle)
|
|
720
|
+
? guideHandle
|
|
721
|
+
: document.querySelector('[data-width-handle="right"]');
|
|
608
722
|
const viewport = window.innerWidth || 0;
|
|
609
|
-
if (handle === null || viewport === 0) {
|
|
723
|
+
if (handle === null || typeof handle.getBoundingClientRect !== "function" || viewport === 0) {
|
|
610
724
|
guide.style.left = "auto";
|
|
611
725
|
guide.style.right = "24px";
|
|
612
726
|
guide.style.top = "50%";
|
|
@@ -633,6 +747,7 @@ window.__ModuleLoader__.load({
|
|
|
633
747
|
guideFromMenu = true;
|
|
634
748
|
guideActive = true;
|
|
635
749
|
guideDragging = false;
|
|
750
|
+
guideHandle = null;
|
|
636
751
|
guide.hidden = false;
|
|
637
752
|
body().setAttribute("data-dsh-width-guide", "custom");
|
|
638
753
|
setGuideText("hint", t("guide.hint"));
|
|
@@ -642,10 +757,11 @@ window.__ModuleLoader__.load({
|
|
|
642
757
|
/* Every handle press shows the live readout, not just the first
|
|
643
758
|
* guided drag — a repeat custom drag must keep the handle
|
|
644
759
|
* highlight and the width bubble. */
|
|
645
|
-
function startLiveGuide() {
|
|
760
|
+
function startLiveGuide(handle) {
|
|
646
761
|
window.clearTimeout(guideHideTimer);
|
|
647
762
|
guideActive = true;
|
|
648
763
|
guideDragging = true;
|
|
764
|
+
guideHandle = handle || null;
|
|
649
765
|
guide.hidden = false;
|
|
650
766
|
body().setAttribute("data-dsh-width-guide", "custom");
|
|
651
767
|
setGuideText("live", guideValueText());
|
|
@@ -682,6 +798,7 @@ window.__ModuleLoader__.load({
|
|
|
682
798
|
window.clearTimeout(guideHideTimer);
|
|
683
799
|
guide.hidden = true;
|
|
684
800
|
guide.dataset.mode = "hint";
|
|
801
|
+
guideHandle = null;
|
|
685
802
|
body().removeAttribute("data-dsh-width-guide");
|
|
686
803
|
const restore = restoreTier && guideFromMenu && readStoredNumber(CUSTOM_KEY) === null;
|
|
687
804
|
guideFromMenu = false;
|
|
@@ -707,7 +824,7 @@ window.__ModuleLoader__.load({
|
|
|
707
824
|
|
|
708
825
|
const onHandlePointerDown = (e) => {
|
|
709
826
|
if (!isHandleEvent(e)) return;
|
|
710
|
-
startLiveGuide();
|
|
827
|
+
startLiveGuide(e.target.closest("[data-width-handle]"));
|
|
711
828
|
if (currentTier() === "custom") return;
|
|
712
829
|
body().setAttribute("data-dsh-width", "custom");
|
|
713
830
|
try {
|
|
@@ -715,7 +832,6 @@ window.__ModuleLoader__.load({
|
|
|
715
832
|
} catch (err) {
|
|
716
833
|
/* ignore */
|
|
717
834
|
}
|
|
718
|
-
collapsedByTier = false;
|
|
719
835
|
syncButton();
|
|
720
836
|
syncMenu();
|
|
721
837
|
};
|
|
@@ -739,15 +855,32 @@ window.__ModuleLoader__.load({
|
|
|
739
855
|
syncButton();
|
|
740
856
|
syncMenu();
|
|
741
857
|
};
|
|
858
|
+
|
|
859
|
+
/* A cancelled drag (touch gesture, window switch) ends without
|
|
860
|
+
* pointerup; without this handler the bubble would stay stuck
|
|
861
|
+
* in dragging mode and click-away would no longer dismiss it. */
|
|
862
|
+
const onHandlePointerCancel = () => {
|
|
863
|
+
/* The cancel target can be the capture element or the
|
|
864
|
+
* document; while a drag is active, end it regardless. */
|
|
865
|
+
if (!guideActive || !guideDragging) return;
|
|
866
|
+
endGuide(false);
|
|
867
|
+
syncButton();
|
|
868
|
+
syncMenu();
|
|
869
|
+
};
|
|
742
870
|
document.addEventListener("pointerdown", onHandlePointerDown, true);
|
|
743
871
|
document.addEventListener("pointerup", onHandlePointerUp, true);
|
|
872
|
+
document.addEventListener("pointercancel", onHandlePointerCancel, true);
|
|
744
873
|
|
|
745
874
|
/* ---- feedback ring so every selection is visibly answered ---- */
|
|
746
875
|
function pulse() {
|
|
747
876
|
btn.classList.remove("pulse");
|
|
748
877
|
void btn.offsetWidth;
|
|
749
878
|
btn.classList.add("pulse");
|
|
750
|
-
window.
|
|
879
|
+
window.clearTimeout(pulseTimer);
|
|
880
|
+
pulseTimer = window.setTimeout(() => {
|
|
881
|
+
btn.classList.remove("pulse");
|
|
882
|
+
pulseTimer = null;
|
|
883
|
+
}, 700);
|
|
751
884
|
}
|
|
752
885
|
|
|
753
886
|
/* ---- re-render localized copy when the app language changes ---- */
|
|
@@ -762,6 +895,24 @@ window.__ModuleLoader__.load({
|
|
|
762
895
|
|
|
763
896
|
/* ---- attach once the app shell has mounted ---- */
|
|
764
897
|
let attached = false;
|
|
898
|
+
let readyTimer = null;
|
|
899
|
+
let slowTimer = null;
|
|
900
|
+
let readyTimeout = null;
|
|
901
|
+
|
|
902
|
+
function stopWaiting() {
|
|
903
|
+
if (readyTimer !== null) {
|
|
904
|
+
clearInterval(readyTimer);
|
|
905
|
+
readyTimer = null;
|
|
906
|
+
}
|
|
907
|
+
if (slowTimer !== null) {
|
|
908
|
+
clearInterval(slowTimer);
|
|
909
|
+
slowTimer = null;
|
|
910
|
+
}
|
|
911
|
+
if (readyTimeout !== null) {
|
|
912
|
+
window.clearTimeout(readyTimeout);
|
|
913
|
+
readyTimeout = null;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
765
916
|
|
|
766
917
|
/* The old gate matched [data-sidebar-collapsed] /
|
|
767
918
|
* [data-details-collapsed]. 0.1.5-rc.2 renders collapsed
|
|
@@ -774,6 +925,7 @@ window.__ModuleLoader__.load({
|
|
|
774
925
|
if (attached || !body()) return;
|
|
775
926
|
if (document.querySelector("[data-shell-overlay]") === null) return;
|
|
776
927
|
attached = true;
|
|
928
|
+
stopWaiting();
|
|
777
929
|
try {
|
|
778
930
|
window.localStorage.removeItem(OLD_KEY);
|
|
779
931
|
} catch (e) {
|
|
@@ -786,14 +938,34 @@ window.__ModuleLoader__.load({
|
|
|
786
938
|
const saved = getSavedTier();
|
|
787
939
|
if (saved !== "standard") applyTier(saved);
|
|
788
940
|
startHealing();
|
|
941
|
+
if (widthRoot !== null) observeWidthTarget(widthRoot);
|
|
789
942
|
}
|
|
790
943
|
|
|
791
|
-
|
|
792
|
-
window.
|
|
793
|
-
|
|
944
|
+
readyTimer = setInterval(attach, 300);
|
|
945
|
+
readyTimeout = window.setTimeout(() => {
|
|
946
|
+
if (attached) return;
|
|
794
947
|
clearInterval(readyTimer);
|
|
795
|
-
|
|
948
|
+
readyTimer = null;
|
|
949
|
+
/* A very slow boot should not leave the picker permanently
|
|
950
|
+
* unmounted: keep a low-frequency retry instead of giving up. */
|
|
951
|
+
slowTimer = setInterval(attach, 2000);
|
|
796
952
|
}, READY_TIMEOUT_MS);
|
|
953
|
+
window.addEventListener("load", attach);
|
|
954
|
+
|
|
955
|
+
/* Tier changes from another tab: adopt the stored tier without
|
|
956
|
+
* writing localStorage again (that would loop). */
|
|
957
|
+
const onStorage = (e) => {
|
|
958
|
+
if (!e || e.key !== KEY) return;
|
|
959
|
+
/* No storageArea identity check: spec does not require the
|
|
960
|
+
* event object to be the same Storage instance as
|
|
961
|
+
* window.localStorage, and dropping the event would break
|
|
962
|
+
* cross-tab sync. */
|
|
963
|
+
const next = e.newValue;
|
|
964
|
+
if (typeof next !== "string" || !TIERS.some((tier) => tier.id === next)) return;
|
|
965
|
+
if (currentTier() === next) return;
|
|
966
|
+
applyTier(next, false);
|
|
967
|
+
};
|
|
968
|
+
window.addEventListener("storage", onStorage);
|
|
797
969
|
|
|
798
970
|
/* ---- self-heal: re-apply the width when the chat tree
|
|
799
971
|
* remounts (conversation switch). Panels are never managed
|
|
@@ -812,15 +984,51 @@ window.__ModuleLoader__.load({
|
|
|
812
984
|
let rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
813
985
|
let rootRetryTimer = null;
|
|
814
986
|
let healFrame = null;
|
|
987
|
+
let resizeFrame = null;
|
|
815
988
|
let observer = null;
|
|
816
989
|
let fallbackTimer = null;
|
|
990
|
+
let widthObserver = null;
|
|
991
|
+
let observedTarget = null;
|
|
992
|
+
|
|
993
|
+
/* Return the frame id so heal and resize can be debounced and
|
|
994
|
+
* cancelled independently. The old helper always wrote the
|
|
995
|
+
* heal slot, so resizeFrame never became non-null and every
|
|
996
|
+
* resize event scheduled another frame. */
|
|
997
|
+
function scheduleFrame(cb) {
|
|
998
|
+
return typeof requestAnimationFrame === "function"
|
|
999
|
+
? requestAnimationFrame(cb)
|
|
1000
|
+
: window.setTimeout(cb, 0);
|
|
1001
|
+
}
|
|
1002
|
+
function cancelFrame(id) {
|
|
1003
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(id);
|
|
1004
|
+
else window.clearTimeout(id);
|
|
1005
|
+
}
|
|
817
1006
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1007
|
+
/* Watch the column that sizes the transcript. Sidebar/rightbar
|
|
1008
|
+
* changes and frame resizes alter it; `full` (and a clamped
|
|
1009
|
+
* fixed/custom width) must follow without polling every
|
|
1010
|
+
* mutation. Guarded for browsers/tests without ResizeObserver. */
|
|
1011
|
+
function observeWidthTarget(root) {
|
|
1012
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
1013
|
+
const target = widthTarget(root);
|
|
1014
|
+
if (target === observedTarget && widthObserver !== null) return;
|
|
1015
|
+
if (widthObserver !== null) {
|
|
1016
|
+
widthObserver.disconnect();
|
|
1017
|
+
widthObserver = null;
|
|
1018
|
+
}
|
|
1019
|
+
observedTarget = target;
|
|
1020
|
+
widthObserver = new ResizeObserver(() => {
|
|
1021
|
+
/* Never fight the app's native drag: it owns the width
|
|
1022
|
+
* while the user is moving a handle. */
|
|
1023
|
+
if (guideActive && guideDragging) return;
|
|
1024
|
+
const tier = currentTier();
|
|
1025
|
+
if (tier === "standard") return;
|
|
1026
|
+
applyContentWidth(tier);
|
|
1027
|
+
syncButton();
|
|
1028
|
+
syncMenu();
|
|
1029
|
+
});
|
|
1030
|
+
widthObserver.observe(target);
|
|
1031
|
+
}
|
|
824
1032
|
|
|
825
1033
|
function scheduleRootRetry() {
|
|
826
1034
|
if (rootRetryTimer !== null) return;
|
|
@@ -836,7 +1044,12 @@ window.__ModuleLoader__.load({
|
|
|
836
1044
|
} else {
|
|
837
1045
|
rootMissing = false;
|
|
838
1046
|
rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
839
|
-
if (root !== prev)
|
|
1047
|
+
if (root !== prev) {
|
|
1048
|
+
applyContentWidth(tier);
|
|
1049
|
+
syncButton();
|
|
1050
|
+
syncMenu();
|
|
1051
|
+
}
|
|
1052
|
+
observeWidthTarget(root);
|
|
840
1053
|
}
|
|
841
1054
|
}, rootRetryDelay);
|
|
842
1055
|
}
|
|
@@ -845,15 +1058,18 @@ window.__ModuleLoader__.load({
|
|
|
845
1058
|
/* The right handle only mounts in the active phase; keep the
|
|
846
1059
|
* guide pinned to it once the chat tree settles. */
|
|
847
1060
|
if (guideActive) positionGuide();
|
|
1061
|
+
/* The app's native drag owns the inline width until release;
|
|
1062
|
+
* a heal must not restore the previous custom value on top. */
|
|
1063
|
+
if (guideActive && guideDragging) return;
|
|
848
1064
|
const tier = currentTier();
|
|
849
1065
|
if (tier === "standard") return;
|
|
850
1066
|
const prev = widthRoot;
|
|
851
1067
|
let root;
|
|
852
|
-
if (rootMissing) {
|
|
853
|
-
/* Mid-remount
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
root =
|
|
1068
|
+
if (rootMissing || fromObserver) {
|
|
1069
|
+
/* Mid-remount / mutation churn: walk the recorded path
|
|
1070
|
+
* only (O(depth)); the backoff retry owns full scans
|
|
1071
|
+
* until the new root exists. */
|
|
1072
|
+
root = findWidthRoot(false);
|
|
857
1073
|
} else {
|
|
858
1074
|
root = findWidthRoot();
|
|
859
1075
|
}
|
|
@@ -861,27 +1077,33 @@ window.__ModuleLoader__.load({
|
|
|
861
1077
|
if (!rootMissing) {
|
|
862
1078
|
rootMissing = true;
|
|
863
1079
|
rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
864
|
-
scheduleRootRetry();
|
|
865
1080
|
}
|
|
1081
|
+
/* Idempotent: keep a backoff retry alive even if the
|
|
1082
|
+
* previous one ended while this tier was standard. */
|
|
1083
|
+
scheduleRootRetry();
|
|
866
1084
|
return;
|
|
867
1085
|
}
|
|
868
1086
|
if (rootMissing) {
|
|
869
1087
|
rootMissing = false;
|
|
870
1088
|
rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
871
1089
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if (
|
|
1090
|
+
observeWidthTarget(root);
|
|
1091
|
+
if (root !== prev) {
|
|
1092
|
+
applyContentWidth(tier);
|
|
1093
|
+
syncButton();
|
|
1094
|
+
syncMenu();
|
|
1095
|
+
} else if (!fromObserver) {
|
|
1096
|
+
/* Fallback tick: retry an apply that was skipped while
|
|
1097
|
+
* the root had no layout box. */
|
|
1098
|
+
applyContentWidth(tier);
|
|
1099
|
+
}
|
|
878
1100
|
}
|
|
879
1101
|
|
|
880
1102
|
function startHealing() {
|
|
881
1103
|
if (typeof MutationObserver !== "undefined") {
|
|
882
1104
|
observer = new MutationObserver(() => {
|
|
883
1105
|
if (healFrame !== null) return;
|
|
884
|
-
scheduleFrame(() => {
|
|
1106
|
+
healFrame = scheduleFrame(() => {
|
|
885
1107
|
healFrame = null;
|
|
886
1108
|
heal(true);
|
|
887
1109
|
});
|
|
@@ -894,7 +1116,7 @@ window.__ModuleLoader__.load({
|
|
|
894
1116
|
});
|
|
895
1117
|
}
|
|
896
1118
|
/* Low-frequency fallback so self-heal still runs if the
|
|
897
|
-
* observer misses something — cheap: no scan while the
|
|
1119
|
+
* observer misses something — cheap: no full scan while the
|
|
898
1120
|
* cached root is alive. */
|
|
899
1121
|
fallbackTimer = setInterval(() => heal(false), FALLBACK_HEAL_MS);
|
|
900
1122
|
}
|
|
@@ -902,12 +1124,14 @@ window.__ModuleLoader__.load({
|
|
|
902
1124
|
/* ---- keep the picker and the guide aligned with the live
|
|
903
1125
|
* column. Standard/full re-resolve on resize, and the guide
|
|
904
1126
|
* must stay beside the handle when the frame resizes. ---- */
|
|
905
|
-
let resizeFrame = null;
|
|
906
|
-
|
|
907
1127
|
const onWindowResize = () => {
|
|
908
1128
|
if (resizeFrame !== null) return;
|
|
909
|
-
scheduleFrame(() => {
|
|
1129
|
+
resizeFrame = scheduleFrame(() => {
|
|
910
1130
|
resizeFrame = null;
|
|
1131
|
+
if (!(guideActive && guideDragging)) {
|
|
1132
|
+
const tier = currentTier();
|
|
1133
|
+
if (tier !== "standard") applyContentWidth(tier);
|
|
1134
|
+
}
|
|
911
1135
|
syncButton();
|
|
912
1136
|
syncMenu();
|
|
913
1137
|
positionGuide();
|
|
@@ -915,34 +1139,70 @@ window.__ModuleLoader__.load({
|
|
|
915
1139
|
};
|
|
916
1140
|
window.addEventListener("resize", onWindowResize);
|
|
917
1141
|
|
|
1142
|
+
/* All healing/observer state is initialized by now; try an
|
|
1143
|
+
* immediate mount instead of waiting for the first interval. */
|
|
1144
|
+
attach();
|
|
1145
|
+
|
|
918
1146
|
/* ---- disposer ---- */
|
|
919
1147
|
return () => {
|
|
920
1148
|
unsubscribeLocale();
|
|
921
|
-
|
|
1149
|
+
stopWaiting();
|
|
922
1150
|
clearInterval(fallbackTimer);
|
|
923
|
-
clearTimeout(timeout);
|
|
924
1151
|
clearTimeout(rootRetryTimer);
|
|
925
1152
|
clearTimeout(guideHideTimer);
|
|
1153
|
+
clearTimeout(pulseTimer);
|
|
926
1154
|
if (healFrame !== null) cancelFrame(healFrame);
|
|
927
1155
|
if (resizeFrame !== null) cancelFrame(resizeFrame);
|
|
1156
|
+
if (widthObserver !== null) {
|
|
1157
|
+
widthObserver.disconnect();
|
|
1158
|
+
widthObserver = null;
|
|
1159
|
+
observedTarget = null;
|
|
1160
|
+
}
|
|
928
1161
|
window.removeEventListener("resize", onWindowResize);
|
|
1162
|
+
window.removeEventListener("load", attach);
|
|
1163
|
+
window.removeEventListener("storage", onStorage);
|
|
929
1164
|
if (observer !== null) observer.disconnect();
|
|
930
1165
|
document.removeEventListener("click", onDocClick);
|
|
931
1166
|
document.removeEventListener("keydown", onDocKey);
|
|
932
1167
|
document.removeEventListener("pointerdown", onHandlePointerDown, true);
|
|
933
1168
|
document.removeEventListener("pointerup", onHandlePointerUp, true);
|
|
1169
|
+
document.removeEventListener("pointercancel", onHandlePointerCancel, true);
|
|
934
1170
|
document.removeEventListener("pointermove", onGuidePointerMove, true);
|
|
935
1171
|
if (btn.parentNode) btn.parentNode.removeChild(btn);
|
|
936
1172
|
if (menu.parentNode) menu.parentNode.removeChild(menu);
|
|
937
1173
|
if (guide.parentNode) guide.parentNode.removeChild(guide);
|
|
938
1174
|
if (styleTag.parentNode) styleTag.parentNode.removeChild(styleTag);
|
|
939
|
-
/* Hand the width back to the app default
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
* semantics. */
|
|
1175
|
+
/* Hand the width back to the app default. Only a value this
|
|
1176
|
+
* plugin wrote is removed, so a native drag preference that
|
|
1177
|
+
* predates the plugin survives unload. */
|
|
943
1178
|
if (widthRoot !== null) {
|
|
944
1179
|
widthTarget(widthRoot).style.removeProperty("--dsh-chat-user-width");
|
|
945
1180
|
}
|
|
1181
|
+
if (preferenceOwned) {
|
|
1182
|
+
try {
|
|
1183
|
+
/* Only remove the exact value this plugin wrote; a
|
|
1184
|
+
* native drag since then owns the preference. */
|
|
1185
|
+
if (preferenceWritten !== null
|
|
1186
|
+
&& window.localStorage.getItem(APP_PREF_KEY) === preferenceWritten) {
|
|
1187
|
+
window.localStorage.removeItem(APP_PREF_KEY);
|
|
1188
|
+
}
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
/* ignore */
|
|
1191
|
+
}
|
|
1192
|
+
preferenceOwned = false;
|
|
1193
|
+
preferenceWritten = null;
|
|
1194
|
+
}
|
|
1195
|
+
/* Undo a collapse this plugin owned; a manual collapse is
|
|
1196
|
+
* left alone. */
|
|
1197
|
+
if (collapsedByTier) {
|
|
1198
|
+
try {
|
|
1199
|
+
if (isSidebarCollapsed()) layout.toggleSidebar();
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
/* ignore */
|
|
1202
|
+
}
|
|
1203
|
+
collapsedByTier = false;
|
|
1204
|
+
}
|
|
1205
|
+
guideHandle = null;
|
|
946
1206
|
body().removeAttribute("data-dsh-width");
|
|
947
1207
|
body().removeAttribute("data-dsh-width-guide");
|
|
948
1208
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-width-tiers",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "DSH Web client plugin: chat content width tiers (standard / medium / wide / ultra / full / custom) with a floating picker button. Drives the app's native width axis on DSH 0.1.7+ (0.2.1-rc.
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "DSH Web client plugin: chat content width tiers (standard / medium / wide / ultra / full / custom) with a floating picker button. Drives the app's native width axis on DSH 0.1.7+ (0.2.1-rc.2 width axis checked), so the built-in drag handles keep working. UI copy follows the app language (zh / en).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "aaronlei",
|
|
7
7
|
"publishConfig": {
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
"type": "git",
|
|
49
49
|
"url": "git+https://github.com/aaronlei/dsh-width-tiers.git"
|
|
50
50
|
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"test": "node test.cjs"
|
|
53
|
+
},
|
|
51
54
|
"engines": {
|
|
52
55
|
"node": ">=18"
|
|
53
56
|
}
|