dsh-width-tiers 1.0.5 → 1.0.7
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 +30 -16
- package/README.zh.md +24 -13
- package/lib/client.js +260 -59
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,29 +4,42 @@
|
|
|
4
4
|
|
|
5
5
|
A client plugin for the **DeepSeek Harness Web GUI** that adds **chat content
|
|
6
6
|
width tiers** with a floating picker button (bottom-right, above the
|
|
7
|
-
composer): 标准 / 中等 / 宽 / 超宽 / 全宽 (standard / medium / wide /
|
|
8
|
-
full).
|
|
7
|
+
composer): 标准 / 中等 / 宽 / 超宽 / 全宽 / 自定义 (standard / medium / wide /
|
|
8
|
+
ultra / full / custom).
|
|
9
9
|
|
|
10
10
|
## Why
|
|
11
11
|
|
|
12
|
-
The DSH Web app
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
16
|
|
|
17
|
-
This plugin
|
|
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:
|
|
18
21
|
|
|
19
22
|
| Tier | Content width | Sidebar |
|
|
20
23
|
|---|---|---|
|
|
21
|
-
| standard 标准 |
|
|
22
|
-
| medium 中等 |
|
|
24
|
+
| standard 标准 | app default | untouched |
|
|
25
|
+
| medium 中等 | 1120px | untouched |
|
|
23
26
|
| wide 宽 | 1280px | untouched |
|
|
24
27
|
| ultra 超宽 | 1400px | untouched |
|
|
25
|
-
| full 全宽 |
|
|
28
|
+
| full 全宽 | column minus the handle budget (fills the available width) | collapsed to the icon rail |
|
|
29
|
+
| custom 自定义 | your dragged width, re-selectable from the menu | untouched |
|
|
30
|
+
|
|
31
|
+
The menu and the button title always show the width a tier resolves to **at
|
|
32
|
+
the current column**, so on narrow windows a clamped tier reads its real
|
|
33
|
+
value (e.g. `宽 668px`) instead of its nominal one.
|
|
34
|
+
|
|
35
|
+
On narrow windows wider tiers are clamped to the available column, so nothing
|
|
36
|
+
overflows under the sidebar.
|
|
26
37
|
|
|
27
38
|
The choice persists in `localStorage` and is restored on the next visit; the
|
|
28
39
|
menu highlights the current tier, and the button's five level-bars show it at
|
|
29
|
-
a glance.
|
|
40
|
+
a glance (all lit for custom). Dragging one of the app's native edge handles
|
|
41
|
+
switches the picker to the custom tier — the plugin stands aside for native
|
|
42
|
+
drags.
|
|
30
43
|
|
|
31
44
|
## Install
|
|
32
45
|
|
|
@@ -53,8 +66,9 @@ Restart `dsh web` (the plugin loads at boot) and hard-refresh the page
|
|
|
53
66
|
- `full` collapses the sidebar to the icon rail through the app's own toggle
|
|
54
67
|
— it is never force-hidden or locked: click a rail icon any time to expand
|
|
55
68
|
it, and it stays expanded.
|
|
56
|
-
-
|
|
57
|
-
|
|
69
|
+
- The right panel (rightbar) is never touched: its track and panel are fully
|
|
70
|
+
owned by the app, and the conversation yields space to it on its own.
|
|
71
|
+
`standard` leaves the layout entirely to the app.
|
|
58
72
|
|
|
59
73
|
## Language
|
|
60
74
|
|
|
@@ -81,10 +95,10 @@ language, and Chinese is the last resort. No plugin-side setting needed.
|
|
|
81
95
|
## Limitations
|
|
82
96
|
|
|
83
97
|
- Content width cannot exceed the window: on a narrow window, wider tiers are
|
|
84
|
-
capped by the available column
|
|
85
|
-
sidebar) for the maximum.
|
|
98
|
+
capped by the available column (the menu shows the capped value). Use
|
|
99
|
+
`full` (which also collapses the sidebar) for the maximum.
|
|
86
100
|
- Requires the DSH Web client plugin system (dsh ≥ 0.1.0-rc.x with a `web`
|
|
87
|
-
profile).
|
|
101
|
+
profile). Verified on dsh 0.1.7-alpha.1 and 0.2.0-rc.2.
|
|
88
102
|
|
|
89
103
|
## Uninstall
|
|
90
104
|
|
package/README.zh.md
CHANGED
|
@@ -3,27 +3,36 @@
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
|
|
5
5
|
DeepSeek Harness Web GUI 的客户端插件:给对话区加**内容宽度档位选择器**
|
|
6
|
-
(右下角圆形按钮,输入框上方),共
|
|
7
|
-
标准 / 中等 / 宽 / 超宽 /
|
|
6
|
+
(右下角圆形按钮,输入框上方),共 6 档:
|
|
7
|
+
标准 / 中等 / 宽 / 超宽 / 全宽 / 自定义。
|
|
8
8
|
|
|
9
9
|
## 为什么需要它
|
|
10
10
|
|
|
11
|
-
DSH Web
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
DSH Web 把消息列——包括里面的所有表格——的宽度限制在
|
|
12
|
+
`--dsh-chat-content-width`(0.1.7–0.2.0 默认
|
|
13
|
+
`clamp(680px, 列宽 × 0.64, 920px)`),不管窗口多宽都不变,宽表格只能横向滚动。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
本插件通过应用自身的**原生宽度通道**(`--dsh-chat-user-width` +
|
|
16
|
+
`dsh.conversation.contentWidth` 偏好)按档位设定宽度,由应用自身钳制到
|
|
17
|
+
「列宽 − 176px 手柄预留」,因此自带的原生拖拽手柄始终可用:
|
|
16
18
|
|
|
17
19
|
| 档位 | 内容宽度 | 侧边栏 |
|
|
18
20
|
|---|---|---|
|
|
19
|
-
| 标准 |
|
|
20
|
-
| 中等 |
|
|
21
|
+
| 标准 | 应用默认 | 不动 |
|
|
22
|
+
| 中等 | 1120px | 不动 |
|
|
21
23
|
| 宽 | 1280px | 不动 |
|
|
22
24
|
| 超宽 | 1400px | 不动 |
|
|
23
|
-
| 全宽 |
|
|
25
|
+
| 全宽 | 列宽减手柄预留(占满可用宽度) | 收起为图标轨道 |
|
|
26
|
+
| 自定义 | 拖拽原生手柄得出,可在菜单回切 | 不动 |
|
|
27
|
+
|
|
28
|
+
菜单与按钮标题显示的是**当前列宽下实际生效的 px**:窄窗口下被钳制的档位会
|
|
29
|
+
显示真实值(如「宽 668px」),而不是标称值。
|
|
30
|
+
|
|
31
|
+
窄窗口下超宽档位会自动被钳到可用宽度,不会溢出被侧栏覆盖。
|
|
24
32
|
|
|
25
33
|
选择会记住(localStorage),下次打开自动恢复;菜单里当前档位高亮,按钮上
|
|
26
|
-
|
|
34
|
+
的五条横条图标也直观显示当前档位(自定义档全亮)。拖动应用自带的
|
|
35
|
+
对话区边缘手柄会自动切到「自定义」档,插件让位于原生拖拽。
|
|
27
36
|
|
|
28
37
|
## 安装
|
|
29
38
|
|
|
@@ -49,7 +58,8 @@ bundle patch 会自动插入 `dsh-width-tiers` 加载项——无需手动改
|
|
|
49
58
|
- 点圆形按钮 → 选择档位,当前档位高亮。
|
|
50
59
|
- 「全宽」通过应用自己的收起按钮把侧边栏收成图标轨道——**不会被锁死**:
|
|
51
60
|
随时点轨道图标展开,展开后保持展开。
|
|
52
|
-
-
|
|
61
|
+
- 插件**不碰右侧面板**:右栏的轨道与面板完全由应用管理,展开时对话内容
|
|
62
|
+
自适应让位;「标准」档完全交给应用。
|
|
53
63
|
|
|
54
64
|
## 多语言
|
|
55
65
|
|
|
@@ -70,9 +80,10 @@ bundle patch 会自动插入 `dsh-width-tiers` 加载项——无需手动改
|
|
|
70
80
|
|
|
71
81
|
## 已知限制
|
|
72
82
|
|
|
73
|
-
-
|
|
74
|
-
|
|
83
|
+
- 内容宽度不可能超过窗口:窗口窄时,高档位会被可用列宽截断(菜单会显示
|
|
84
|
+
截断后的实际值);想要最大空间请用「全宽」(同时收起侧边栏)。
|
|
75
85
|
- 依赖 DSH Web 客户端插件体系(带 web profile 的 dsh ≥ 0.1.0-rc.x)。
|
|
86
|
+
已在 dsh 0.1.7-alpha.1 与 0.2.0-rc.2 上验证。
|
|
76
87
|
|
|
77
88
|
## 卸载
|
|
78
89
|
|
package/lib/client.js
CHANGED
|
@@ -9,30 +9,40 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
/**
|
|
10
10
|
* dsh-width-tiers — chat content width tiers for the DSH Web GUI.
|
|
11
11
|
*
|
|
12
|
-
* The app
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* The app derives the transcript width from
|
|
13
|
+
* `--dsh-chat-content-width`, which 0.1.7 and 0.2.0 resolve as
|
|
14
|
+
* `var(--dsh-chat-user-width, clamp(680px, column * .64, 920px))` on
|
|
15
|
+
* the conversation body. 0.1.7 also added native drag handles
|
|
16
|
+
* (ConversationWidthControls) that publish
|
|
17
|
+
* `--dsh-conversation-column-width` / `--dsh-chat-user-width` on the
|
|
18
|
+
* body's PARENT and clamp every value to the column minus a 176px
|
|
19
|
+
* handle budget. Forcing `--dsh-chat-content-width` with `!important`
|
|
20
|
+
* (the 1.0.5 approach) muted those handles and let over-wide
|
|
21
|
+
* transcripts overflow under the sidebar/rightbar columns, so this
|
|
22
|
+
* version drives the app's own width axis instead — see
|
|
23
|
+
* `applyContentWidth`. Other invariants:
|
|
17
24
|
*
|
|
18
|
-
* 标准 standard
|
|
19
|
-
* 中等 medium
|
|
25
|
+
* 标准 standard app default (no override)
|
|
26
|
+
* 中等 medium 1120px (sidebar untouched)
|
|
20
27
|
* 宽 wide 1280px (sidebar untouched)
|
|
21
28
|
* 超宽 ultra 1400px (sidebar untouched)
|
|
22
|
-
* 全宽 full
|
|
29
|
+
* 全宽 full column max (sidebar collapsed to the icon rail)
|
|
30
|
+
* 自定义 custom user-dragged width (plugin hands width over to
|
|
31
|
+
* the app's native handles; the dragged value stays
|
|
32
|
+
* re-selectable from the menu)
|
|
33
|
+
*
|
|
34
|
+
* The menu and title always show the px a tier resolves to at the
|
|
35
|
+
* CURRENT column (see `tierValueText`), so a clamped tier never
|
|
36
|
+
* claims its nominal width on a narrow window.
|
|
23
37
|
*
|
|
24
|
-
* Design notes:
|
|
25
|
-
* - The element that defines `--dsh-chat-content-width` is located at
|
|
26
|
-
* runtime (first element whose computed value is non-empty) and the
|
|
27
|
-
* tier is applied as an INLINE override on it — no hashed class
|
|
28
|
-
* names, survives bundle upgrades. Re-detected if it remounts.
|
|
29
38
|
* - Panels are driven through the layout service (ctx.layout), never
|
|
30
39
|
* by simulating clicks.
|
|
31
40
|
* - The sidebar is only ever collapsed by the `full` tier, through the
|
|
32
41
|
* app's own toggle — never force-hidden, never locked. Going back to
|
|
33
42
|
* `standard` restores it only if this plugin collapsed it.
|
|
34
|
-
* -
|
|
35
|
-
*
|
|
43
|
+
* - The rightbar is never touched: on 0.1.7+ its panel lives in its
|
|
44
|
+
* own seat, so closing the track would leave it overlaying the
|
|
45
|
+
* conversation; the app's width axis yields space on its own.
|
|
36
46
|
* - The chosen tier persists in localStorage and is restored on the
|
|
37
47
|
* next visit.
|
|
38
48
|
* - All copy is localized through the app's locale service: tier
|
|
@@ -41,6 +51,20 @@ window.__ModuleLoader__.load({
|
|
|
41
51
|
*/
|
|
42
52
|
const KEY = "dsh.widthTier";
|
|
43
53
|
const OLD_KEY = "dsh.wide";
|
|
54
|
+
/** Last user-dragged width (px), backing the `custom` tier. */
|
|
55
|
+
const CUSTOM_KEY = "dsh.widthTierCustom";
|
|
56
|
+
/** The app's own transcript width preference (ConversationWidthControls). */
|
|
57
|
+
const APP_PREF_KEY = "dsh.conversation.contentWidth";
|
|
58
|
+
/* Mirrors ConversationWidthControls in 0.1.7-alpha.1: clamp floor and
|
|
59
|
+
* the horizontal budget both width handles + safe edges reserve. */
|
|
60
|
+
const CONTENT_MIN_PX = 640;
|
|
61
|
+
const CONTENT_EDGE_BUDGET_PX = 176;
|
|
62
|
+
/* The app's default transcript width (resolveContentWidth's
|
|
63
|
+
* no-preference branch): clamp(680px, column × 0.64, 920px). Used to
|
|
64
|
+
* show what `standard` actually resolves to at the current column. */
|
|
65
|
+
const DEFAULT_MIN_PX = 680;
|
|
66
|
+
const DEFAULT_MAX_PX = 920;
|
|
67
|
+
const DEFAULT_COLUMN_RATIO = 0.64;
|
|
44
68
|
const READY_TIMEOUT_MS = 30000;
|
|
45
69
|
/* Self-heal cadence: DOM-driven (MutationObserver scheduling on the
|
|
46
70
|
* next animation frame — before paint, so the width never flashes
|
|
@@ -62,6 +86,7 @@ window.__ModuleLoader__.load({
|
|
|
62
86
|
"tier.wide": "宽",
|
|
63
87
|
"tier.ultra": "超宽",
|
|
64
88
|
"tier.full": "全宽",
|
|
89
|
+
"tier.custom": "自定义",
|
|
65
90
|
"button.aria": "对话区宽度档位:{label}({value}),点击选择档位",
|
|
66
91
|
"button.title.summary": "对话区宽度档位:{label}({value})",
|
|
67
92
|
"button.title.hint": "点击选择:{list}"
|
|
@@ -73,18 +98,24 @@ window.__ModuleLoader__.load({
|
|
|
73
98
|
"tier.wide": "Wide",
|
|
74
99
|
"tier.ultra": "Ultra",
|
|
75
100
|
"tier.full": "Full",
|
|
101
|
+
"tier.custom": "Custom",
|
|
76
102
|
"button.aria": "Chat width tier: {label} ({value}). Click to choose a tier.",
|
|
77
103
|
"button.title.summary": "Chat width tier: {label} ({value})",
|
|
78
104
|
"button.title.hint": "Click to choose: {list}"
|
|
79
105
|
};
|
|
80
106
|
//#endregion
|
|
81
107
|
|
|
108
|
+
/* Tier ids and label keys only — every displayed width is resolved at
|
|
109
|
+
* render time from the live column (see tierValueText), so the menu
|
|
110
|
+
* never claims a width the app would not actually apply. */
|
|
82
111
|
const TIERS = [
|
|
83
|
-
{ id: "standard", key: "tier.standard"
|
|
84
|
-
{ id: "medium", key: "tier.medium"
|
|
85
|
-
{ id: "wide", key: "tier.wide"
|
|
86
|
-
{ id: "ultra", key: "tier.ultra"
|
|
87
|
-
{ id: "full", key: "tier.full"
|
|
112
|
+
{ id: "standard", key: "tier.standard" },
|
|
113
|
+
{ id: "medium", key: "tier.medium" },
|
|
114
|
+
{ id: "wide", key: "tier.wide" },
|
|
115
|
+
{ id: "ultra", key: "tier.ultra" },
|
|
116
|
+
{ id: "full", key: "tier.full" },
|
|
117
|
+
/* Reached only by dragging a native width handle. */
|
|
118
|
+
{ id: "custom", key: "tier.custom" }
|
|
88
119
|
];
|
|
89
120
|
|
|
90
121
|
function tierOf(id) {
|
|
@@ -110,6 +141,7 @@ window.__ModuleLoader__.load({
|
|
|
110
141
|
"#dsh-width-menu[hidden]{display:none}",
|
|
111
142
|
"#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}",
|
|
112
143
|
"#dsh-width-menu button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
144
|
+
"#dsh-width-menu button.disabled{opacity:.45;cursor:default;pointer-events:none}",
|
|
113
145
|
"#dsh-width-menu button.selected{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-state-business-primary)}",
|
|
114
146
|
"#dsh-width-menu .dshwm-value{color:var(--dsw-alias-label-tertiary);font-size:12px}",
|
|
115
147
|
"#dsh-width-menu button.selected .dshwm-value{color:var(--dsw-alias-state-business-primary)}"
|
|
@@ -165,12 +197,15 @@ window.__ModuleLoader__.load({
|
|
|
165
197
|
return document.querySelector("[data-sidebar-collapsed]") !== null;
|
|
166
198
|
}
|
|
167
199
|
|
|
168
|
-
/*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
200
|
+
/* NOTE: the 1.0.x "close the right panel on non-standard tiers"
|
|
201
|
+
* self-heal is gone on purpose. On 0.1.7 the rightbar panel
|
|
202
|
+
* lives in its own seat: closeRightbar() only retracts the
|
|
203
|
+
* grid TRACK (rightbarTrack=false) while the seat stays
|
|
204
|
+
* expanded, so the panel keeps rendering as an OVERLAY pinned
|
|
205
|
+
* to the frame's right edge — hanging over and covering the
|
|
206
|
+
* conversation (对话窗口被边侧栏覆盖). The plugin must never
|
|
207
|
+
* touch the rightbar; the app's own width axis already yields
|
|
208
|
+
* space to the panel. */
|
|
174
209
|
|
|
175
210
|
function collapseSidebar() {
|
|
176
211
|
if (!isSidebarCollapsed()) layout.toggleSidebar();
|
|
@@ -180,10 +215,6 @@ window.__ModuleLoader__.load({
|
|
|
180
215
|
if (isSidebarCollapsed()) layout.toggleSidebar();
|
|
181
216
|
}
|
|
182
217
|
|
|
183
|
-
function closeDetails() {
|
|
184
|
-
closeRightbar();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
218
|
/* Locate the element that defines --dsh-chat-content-width at
|
|
188
219
|
* runtime (first match wins; only one element declares it).
|
|
189
220
|
* Cached; the app remounts the chat tree on conversation switch,
|
|
@@ -242,22 +273,88 @@ window.__ModuleLoader__.load({
|
|
|
242
273
|
return scanWidthRoot();
|
|
243
274
|
}
|
|
244
275
|
|
|
245
|
-
|
|
276
|
+
/* ---- 0.1.7 native width axis ----
|
|
277
|
+
* The conversation body's parent is the element the app's
|
|
278
|
+
* ConversationWidthControls publishes
|
|
279
|
+
* `--dsh-conversation-column-width` / `--dsh-chat-user-width`
|
|
280
|
+
* on. Writing there (never with `!important`) keeps native
|
|
281
|
+
* drag handles in charge: during a drag the app rewrites the
|
|
282
|
+
* same inline property every frame and wins the cascade. */
|
|
283
|
+
function widthTarget(root) {
|
|
284
|
+
return (root && root.parentElement) || root;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function readStoredNumber(key) {
|
|
288
|
+
try {
|
|
289
|
+
const v = Number(window.localStorage.getItem(key));
|
|
290
|
+
return Number.isFinite(v) && v > 0 ? v : null;
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* Resolve a tier to the px the app would clamp it to
|
|
297
|
+
* (resolveContentWidth: max = column - handle budget), or
|
|
298
|
+
* null when the tier leaves the width to the app. */
|
|
299
|
+
function tierPx(tier, root) {
|
|
300
|
+
const cap = Math.max(CONTENT_MIN_PX, root.offsetWidth - CONTENT_EDGE_BUDGET_PX);
|
|
246
301
|
switch (tier) {
|
|
247
|
-
case "medium": return
|
|
248
|
-
case "wide": return
|
|
249
|
-
case "ultra": return
|
|
250
|
-
case "full": return
|
|
251
|
-
|
|
302
|
+
case "medium": return Math.min(1120, cap);
|
|
303
|
+
case "wide": return Math.min(1280, cap);
|
|
304
|
+
case "ultra": return Math.min(1400, cap);
|
|
305
|
+
case "full": return cap;
|
|
306
|
+
case "custom": {
|
|
307
|
+
const dragged = readStoredNumber(CUSTOM_KEY);
|
|
308
|
+
return dragged === null ? null : Math.min(dragged, cap);
|
|
309
|
+
}
|
|
310
|
+
default: return null; /* standard: app default */
|
|
252
311
|
}
|
|
253
312
|
}
|
|
254
313
|
|
|
314
|
+
/* The px a tier resolves to right now, including the app's own
|
|
315
|
+
* default formula for `standard`; null when the tier has no
|
|
316
|
+
* width of its own yet (custom before the first drag). */
|
|
317
|
+
function resolvedWidthPx(tier, root) {
|
|
318
|
+
if (tier === "standard") {
|
|
319
|
+
const column = root.offsetWidth;
|
|
320
|
+
return Math.round(Math.max(DEFAULT_MIN_PX, Math.min(column * DEFAULT_COLUMN_RATIO, DEFAULT_MAX_PX)));
|
|
321
|
+
}
|
|
322
|
+
return tierPx(tier, root);
|
|
323
|
+
}
|
|
324
|
+
|
|
255
325
|
function applyContentWidth(tier) {
|
|
256
326
|
const root = findWidthRoot();
|
|
257
327
|
if (root === null) return;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
328
|
+
const target = widthTarget(root);
|
|
329
|
+
/* custom before the first drag has no width of its own —
|
|
330
|
+
* leave whatever the app last published in place. */
|
|
331
|
+
if (tier === "custom" && readStoredNumber(CUSTOM_KEY) === null) return;
|
|
332
|
+
const px = tierPx(tier, root);
|
|
333
|
+
if (px === null) {
|
|
334
|
+
if (target.style.getPropertyValue("--dsh-chat-user-width") !== "") {
|
|
335
|
+
target.style.removeProperty("--dsh-chat-user-width");
|
|
336
|
+
try {
|
|
337
|
+
window.localStorage.removeItem(APP_PREF_KEY);
|
|
338
|
+
} catch (e) {
|
|
339
|
+
/* ignore */
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
/* Mirror into the app's preference so window resizes and
|
|
345
|
+
* conversation remounts re-clamp through the app's own
|
|
346
|
+
* publish cycle instead of relying on plugin healing. */
|
|
347
|
+
const next = String(Math.round(px));
|
|
348
|
+
if (target.style.getPropertyValue("--dsh-chat-user-width") !== next + "px") {
|
|
349
|
+
target.style.setProperty("--dsh-chat-user-width", next + "px");
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
if (window.localStorage.getItem(APP_PREF_KEY) !== next) {
|
|
353
|
+
window.localStorage.setItem(APP_PREF_KEY, next);
|
|
354
|
+
}
|
|
355
|
+
} catch (e) {
|
|
356
|
+
/* ignore */
|
|
357
|
+
}
|
|
261
358
|
}
|
|
262
359
|
|
|
263
360
|
/* true while the CURRENT tier is the one that collapsed the sidebar */
|
|
@@ -284,7 +381,6 @@ window.__ModuleLoader__.load({
|
|
|
284
381
|
} else {
|
|
285
382
|
collapsedByTier = false;
|
|
286
383
|
}
|
|
287
|
-
if (tier !== "standard") closeDetails();
|
|
288
384
|
syncButton();
|
|
289
385
|
syncMenu();
|
|
290
386
|
pulse();
|
|
@@ -311,17 +407,32 @@ window.__ModuleLoader__.load({
|
|
|
311
407
|
return `<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">${bars}</svg>`;
|
|
312
408
|
}
|
|
313
409
|
|
|
410
|
+
/* Display value for a tier: the px it resolves to at the live
|
|
411
|
+
* column (what the app would actually apply), so narrow windows
|
|
412
|
+
* show the clamped value instead of the nominal one. Falls back
|
|
413
|
+
* to the recorded drag when the width root is not mounted. */
|
|
414
|
+
function tierValueText(tierId) {
|
|
415
|
+
const root = findWidthRoot();
|
|
416
|
+
if (root !== null) {
|
|
417
|
+
const px = resolvedWidthPx(tierId, root);
|
|
418
|
+
if (px !== null) return `${px}px`;
|
|
419
|
+
}
|
|
420
|
+
const dragged = readStoredNumber(CUSTOM_KEY);
|
|
421
|
+
return dragged === null ? "—" : `${Math.round(dragged)}px`;
|
|
422
|
+
}
|
|
423
|
+
|
|
314
424
|
function syncButton() {
|
|
315
425
|
const tier = currentTier();
|
|
316
426
|
const def = tierOf(tier);
|
|
317
427
|
const label = t(def.key);
|
|
318
|
-
const
|
|
428
|
+
const value = tierValueText(tier);
|
|
429
|
+
const summary = t("button.title.summary", { label, value });
|
|
319
430
|
const hint = t("button.title.hint", {
|
|
320
|
-
list: TIERS.map((tierDef) => `${t(tierDef.key)} ${tierDef.
|
|
431
|
+
list: TIERS.map((tierDef) => `${t(tierDef.key)} ${tierValueText(tierDef.id)}`).join(" / ")
|
|
321
432
|
});
|
|
322
433
|
btn.innerHTML = barIcon(tier);
|
|
323
434
|
btn.classList.toggle("active", tier !== "standard");
|
|
324
|
-
btn.setAttribute("aria-label", t("button.aria", { label, value
|
|
435
|
+
btn.setAttribute("aria-label", t("button.aria", { label, value }));
|
|
325
436
|
btn.title = `${summary}\n${hint}`;
|
|
326
437
|
}
|
|
327
438
|
|
|
@@ -336,24 +447,39 @@ window.__ModuleLoader__.load({
|
|
|
336
447
|
item.type = "button";
|
|
337
448
|
item.setAttribute("role", "menuitemradio");
|
|
338
449
|
item.setAttribute("data-tier", def.id);
|
|
339
|
-
item.innerHTML = `<span class="dshwm-label">${t(def.key)}</span><span class="dshwm-value">${def.
|
|
450
|
+
item.innerHTML = `<span class="dshwm-label">${t(def.key)}</span><span class="dshwm-value">${tierValueText(def.id)}</span>`;
|
|
340
451
|
item.addEventListener("click", (e) => {
|
|
341
452
|
if (e && e.stopPropagation) e.stopPropagation();
|
|
453
|
+
if (item.classList.contains("disabled")) return;
|
|
342
454
|
applyTier(def.id);
|
|
343
455
|
hideMenu();
|
|
344
456
|
});
|
|
345
457
|
menu.appendChild(item);
|
|
346
458
|
}
|
|
347
459
|
|
|
460
|
+
/* Write only on change: syncMenu also runs on window resizes,
|
|
461
|
+
* and untouched text should not churn the DOM (the plugin's own
|
|
462
|
+
* MutationObserver watches childList). */
|
|
463
|
+
function setText(el, text) {
|
|
464
|
+
if (el.textContent !== text) el.textContent = text;
|
|
465
|
+
}
|
|
466
|
+
|
|
348
467
|
function syncMenu() {
|
|
349
468
|
const tier = currentTier();
|
|
350
469
|
const items = menu.querySelectorAll("button[data-tier]");
|
|
351
470
|
for (let i = 0; i < items.length; i++) {
|
|
352
|
-
const
|
|
471
|
+
const id = items[i].getAttribute("data-tier");
|
|
472
|
+
const on = id === tier;
|
|
353
473
|
items[i].classList.toggle("selected", on);
|
|
354
474
|
items[i].setAttribute("aria-checked", String(on));
|
|
355
475
|
const labelEl = items[i].querySelector(".dshwm-label");
|
|
356
|
-
if (labelEl) labelEl
|
|
476
|
+
if (labelEl) setText(labelEl, t(tierOf(id).key));
|
|
477
|
+
const valueEl = items[i].querySelector(".dshwm-value");
|
|
478
|
+
if (valueEl) setText(valueEl, tierValueText(id));
|
|
479
|
+
/* custom is inert until the user has dragged once */
|
|
480
|
+
const disabled = id === "custom" && readStoredNumber(CUSTOM_KEY) === null;
|
|
481
|
+
items[i].classList.toggle("disabled", disabled);
|
|
482
|
+
items[i].setAttribute("aria-disabled", String(disabled));
|
|
357
483
|
}
|
|
358
484
|
}
|
|
359
485
|
|
|
@@ -384,6 +510,48 @@ window.__ModuleLoader__.load({
|
|
|
384
510
|
document.addEventListener("click", onDocClick);
|
|
385
511
|
document.addEventListener("keydown", onDocKey);
|
|
386
512
|
|
|
513
|
+
/* ---- hand the width over to the app's native drag handles ----
|
|
514
|
+
* Pressing a `[data-width-handle]` means the user is taking the
|
|
515
|
+
* width over: switch to the `custom` tier so healing stops
|
|
516
|
+
* enforcing a preset, and let the app's drag win the cascade
|
|
517
|
+
* (it rewrites the same inline property every frame). The
|
|
518
|
+
* settled width is recorded on release so it stays
|
|
519
|
+
* re-selectable from the menu. */
|
|
520
|
+
const isHandleEvent = (e) =>
|
|
521
|
+
e.target instanceof Element && e.target.closest("[data-width-handle]") !== null;
|
|
522
|
+
|
|
523
|
+
const onHandlePointerDown = (e) => {
|
|
524
|
+
if (!isHandleEvent(e) || currentTier() === "custom") return;
|
|
525
|
+
body().setAttribute("data-dsh-width", "custom");
|
|
526
|
+
try {
|
|
527
|
+
window.localStorage.setItem(KEY, "custom");
|
|
528
|
+
} catch (err) {
|
|
529
|
+
/* ignore */
|
|
530
|
+
}
|
|
531
|
+
collapsedByTier = false;
|
|
532
|
+
syncButton();
|
|
533
|
+
syncMenu();
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const onHandlePointerUp = (e) => {
|
|
537
|
+
if (!isHandleEvent(e)) return;
|
|
538
|
+
const root = findWidthRoot();
|
|
539
|
+
if (root === null) return;
|
|
540
|
+
const px = Number.parseFloat(
|
|
541
|
+
widthTarget(root).style.getPropertyValue("--dsh-chat-user-width")
|
|
542
|
+
);
|
|
543
|
+
if (!Number.isFinite(px) || px <= 0) return;
|
|
544
|
+
try {
|
|
545
|
+
window.localStorage.setItem(CUSTOM_KEY, String(Math.round(px)));
|
|
546
|
+
} catch (err) {
|
|
547
|
+
/* ignore */
|
|
548
|
+
}
|
|
549
|
+
syncButton();
|
|
550
|
+
syncMenu();
|
|
551
|
+
};
|
|
552
|
+
document.addEventListener("pointerdown", onHandlePointerDown, true);
|
|
553
|
+
document.addEventListener("pointerup", onHandlePointerUp, true);
|
|
554
|
+
|
|
387
555
|
/* ---- feedback ring so every selection is visibly answered ---- */
|
|
388
556
|
function pulse() {
|
|
389
557
|
btn.classList.remove("pulse");
|
|
@@ -433,16 +601,19 @@ window.__ModuleLoader__.load({
|
|
|
433
601
|
attach();
|
|
434
602
|
}, READY_TIMEOUT_MS);
|
|
435
603
|
|
|
436
|
-
/* ---- self-heal:
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
604
|
+
/* ---- self-heal: re-apply the width when the chat tree
|
|
605
|
+
* remounts (conversation switch). Panels are never managed
|
|
606
|
+
* here — the user may expand/collapse either side any time;
|
|
607
|
+
* forcing the rightbar closed only retracts the grid track
|
|
608
|
+
* while the seat keeps rendering as an overlay that covers
|
|
609
|
+
* the conversation. Driven by DOM mutations, scheduled on the
|
|
610
|
+
* next animation frame (before the browser paints) instead of
|
|
611
|
+
* a fixed hot loop: the width must land on the fresh root
|
|
612
|
+
* before its first paint — otherwise the default width
|
|
613
|
+
* flashes for a frame. While the new root has not appeared
|
|
614
|
+
* yet (mid-remount), full scans are owned by a backoff retry;
|
|
615
|
+
* mutation-driven heals only try the cheap path walk so DOM
|
|
616
|
+
* churn cannot storm. ---- */
|
|
446
617
|
let rootMissing = false;
|
|
447
618
|
let rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
448
619
|
let rootRetryTimer = null;
|
|
@@ -479,7 +650,6 @@ window.__ModuleLoader__.load({
|
|
|
479
650
|
function heal(fromObserver) {
|
|
480
651
|
const tier = currentTier();
|
|
481
652
|
if (tier === "standard") return;
|
|
482
|
-
closeDetails();
|
|
483
653
|
const prev = widthRoot;
|
|
484
654
|
let root;
|
|
485
655
|
if (rootMissing) {
|
|
@@ -503,6 +673,11 @@ window.__ModuleLoader__.load({
|
|
|
503
673
|
rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
504
674
|
}
|
|
505
675
|
if (root !== prev) applyContentWidth(tier);
|
|
676
|
+
/* full tracks the live column: after the sidebar collapse
|
|
677
|
+
* animation settles the column widens, and the width must
|
|
678
|
+
* follow (applyContentWidth is diff-checked, so steady
|
|
679
|
+
* state writes nothing). */
|
|
680
|
+
if (tier === "full") applyContentWidth("full");
|
|
506
681
|
}
|
|
507
682
|
|
|
508
683
|
function startHealing() {
|
|
@@ -518,7 +693,7 @@ window.__ModuleLoader__.load({
|
|
|
518
693
|
subtree: true,
|
|
519
694
|
childList: true,
|
|
520
695
|
attributes: true,
|
|
521
|
-
attributeFilter: ["data-sidebar-collapsed"
|
|
696
|
+
attributeFilter: ["data-sidebar-collapsed"]
|
|
522
697
|
});
|
|
523
698
|
}
|
|
524
699
|
/* Low-frequency fallback so self-heal still runs if the
|
|
@@ -527,6 +702,22 @@ window.__ModuleLoader__.load({
|
|
|
527
702
|
fallbackTimer = setInterval(() => heal(false), FALLBACK_HEAL_MS);
|
|
528
703
|
}
|
|
529
704
|
|
|
705
|
+
/* ---- keep the displayed px honest when the column changes ----
|
|
706
|
+
* Window resizes (and layout handle drags) change what every
|
|
707
|
+
* tier resolves to, so re-render the copy on the next frame.
|
|
708
|
+
* The menu is also re-synced whenever it opens. */
|
|
709
|
+
let resizeFrame = null;
|
|
710
|
+
|
|
711
|
+
const onWindowResize = () => {
|
|
712
|
+
if (resizeFrame !== null) return;
|
|
713
|
+
scheduleFrame(() => {
|
|
714
|
+
resizeFrame = null;
|
|
715
|
+
syncButton();
|
|
716
|
+
syncMenu();
|
|
717
|
+
});
|
|
718
|
+
};
|
|
719
|
+
window.addEventListener("resize", onWindowResize);
|
|
720
|
+
|
|
530
721
|
/* ---- disposer ---- */
|
|
531
722
|
return () => {
|
|
532
723
|
unsubscribeLocale();
|
|
@@ -535,13 +726,23 @@ window.__ModuleLoader__.load({
|
|
|
535
726
|
clearTimeout(timeout);
|
|
536
727
|
clearTimeout(rootRetryTimer);
|
|
537
728
|
if (healFrame !== null) cancelFrame(healFrame);
|
|
729
|
+
if (resizeFrame !== null) cancelFrame(resizeFrame);
|
|
730
|
+
window.removeEventListener("resize", onWindowResize);
|
|
538
731
|
if (observer !== null) observer.disconnect();
|
|
539
732
|
document.removeEventListener("click", onDocClick);
|
|
540
733
|
document.removeEventListener("keydown", onDocKey);
|
|
734
|
+
document.removeEventListener("pointerdown", onHandlePointerDown, true);
|
|
735
|
+
document.removeEventListener("pointerup", onHandlePointerUp, true);
|
|
541
736
|
if (btn.parentNode) btn.parentNode.removeChild(btn);
|
|
542
737
|
if (menu.parentNode) menu.parentNode.removeChild(menu);
|
|
543
738
|
if (styleTag.parentNode) styleTag.parentNode.removeChild(styleTag);
|
|
544
|
-
|
|
739
|
+
/* Hand the width back to the app default; the app's own
|
|
740
|
+
* preference (which this plugin mirrors) stays so the last
|
|
741
|
+
* width survives a plugin reload — matching native-drag
|
|
742
|
+
* semantics. */
|
|
743
|
+
if (widthRoot !== null) {
|
|
744
|
+
widthTarget(widthRoot).style.removeProperty("--dsh-chat-user-width");
|
|
745
|
+
}
|
|
545
746
|
body().removeAttribute("data-dsh-width");
|
|
546
747
|
};
|
|
547
748
|
}, "dsh-width-tiers: ui");
|
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) with a floating picker button.
|
|
3
|
+
"version": "1.0.7",
|
|
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+ (verified on 0.2.0-rc.2), 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": {
|