dsh-width-tiers 1.0.4 → 1.0.6
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 +19 -10
- package/README.zh.md +15 -9
- package/lib/client.js +210 -54
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,29 +4,38 @@
|
|
|
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 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 0.1.7'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 标准 |
|
|
24
|
+
| standard 标准 | app default | untouched |
|
|
22
25
|
| medium 中等 | 960px | 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
|
+
On narrow windows wider tiers are clamped to the available column, so nothing
|
|
32
|
+
overflows under the sidebar.
|
|
26
33
|
|
|
27
34
|
The choice persists in `localStorage` and is restored on the next visit; the
|
|
28
35
|
menu highlights the current tier, and the button's five level-bars show it at
|
|
29
|
-
a glance.
|
|
36
|
+
a glance (all lit for custom). Dragging one of 0.1.7's native edge handles
|
|
37
|
+
switches the picker to the custom tier — the plugin stands aside for native
|
|
38
|
+
drags.
|
|
30
39
|
|
|
31
40
|
## Install
|
|
32
41
|
|
package/README.zh.md
CHANGED
|
@@ -3,27 +3,33 @@
|
|
|
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 默认 `clamp(680px, 列宽 × 0.64, 920px)`),
|
|
13
|
+
不管窗口多宽都不变,宽表格只能横向滚动。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
本插件通过 0.1.7 的**原生宽度通道**(`--dsh-chat-user-width` +
|
|
16
|
+
`dsh.conversation.contentWidth` 偏好)按档位设定宽度,由应用自身钳制到
|
|
17
|
+
「列宽 − 176px 手柄预留」,因此自带的原生拖拽手柄始终可用:
|
|
16
18
|
|
|
17
19
|
| 档位 | 内容宽度 | 侧边栏 |
|
|
18
20
|
|---|---|---|
|
|
19
|
-
| 标准 |
|
|
21
|
+
| 标准 | 应用默认 | 不动 |
|
|
20
22
|
| 中等 | 960px | 不动 |
|
|
21
23
|
| 宽 | 1280px | 不动 |
|
|
22
24
|
| 超宽 | 1400px | 不动 |
|
|
23
|
-
| 全宽 |
|
|
25
|
+
| 全宽 | 列宽减手柄预留(占满可用宽度) | 收起为图标轨道 |
|
|
26
|
+
| 自定义 | 拖拽原生手柄得出,可在菜单回切 | 不动 |
|
|
27
|
+
|
|
28
|
+
窄窗口下超宽档位会自动被钳到可用宽度,不会溢出被侧栏覆盖。
|
|
24
29
|
|
|
25
30
|
选择会记住(localStorage),下次打开自动恢复;菜单里当前档位高亮,按钮上
|
|
26
|
-
|
|
31
|
+
的五条横条图标也直观显示当前档位(自定义档全亮)。拖动 0.1.7 自带的
|
|
32
|
+
对话区边缘手柄会自动切到「自定义」档,插件让位于原生拖拽。
|
|
27
33
|
|
|
28
34
|
## 安装
|
|
29
35
|
|
package/lib/client.js
CHANGED
|
@@ -9,30 +9,35 @@ 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-alpha.1 resolves 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
|
|
25
|
+
* 标准 standard app default (no override)
|
|
19
26
|
* 中等 medium 960px (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)
|
|
23
33
|
*
|
|
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
34
|
* - Panels are driven through the layout service (ctx.layout), never
|
|
30
35
|
* by simulating clicks.
|
|
31
36
|
* - The sidebar is only ever collapsed by the `full` tier, through the
|
|
32
37
|
* app's own toggle — never force-hidden, never locked. Going back to
|
|
33
38
|
* `standard` restores it only if this plugin collapsed it.
|
|
34
|
-
* - Non-standard tiers close the
|
|
35
|
-
* active); `standard` leaves everything to the app.
|
|
39
|
+
* - Non-standard tiers close the right panel (rightbar, self-heal
|
|
40
|
+
* while active); `standard` leaves everything to the app.
|
|
36
41
|
* - The chosen tier persists in localStorage and is restored on the
|
|
37
42
|
* next visit.
|
|
38
43
|
* - All copy is localized through the app's locale service: tier
|
|
@@ -41,6 +46,14 @@ window.__ModuleLoader__.load({
|
|
|
41
46
|
*/
|
|
42
47
|
const KEY = "dsh.widthTier";
|
|
43
48
|
const OLD_KEY = "dsh.wide";
|
|
49
|
+
/** Last user-dragged width (px), backing the `custom` tier. */
|
|
50
|
+
const CUSTOM_KEY = "dsh.widthTierCustom";
|
|
51
|
+
/** The app's own transcript width preference (ConversationWidthControls). */
|
|
52
|
+
const APP_PREF_KEY = "dsh.conversation.contentWidth";
|
|
53
|
+
/* Mirrors ConversationWidthControls in 0.1.7-alpha.1: clamp floor and
|
|
54
|
+
* the horizontal budget both width handles + safe edges reserve. */
|
|
55
|
+
const CONTENT_MIN_PX = 640;
|
|
56
|
+
const CONTENT_EDGE_BUDGET_PX = 176;
|
|
44
57
|
const READY_TIMEOUT_MS = 30000;
|
|
45
58
|
/* Self-heal cadence: DOM-driven (MutationObserver scheduling on the
|
|
46
59
|
* next animation frame — before paint, so the width never flashes
|
|
@@ -62,6 +75,7 @@ window.__ModuleLoader__.load({
|
|
|
62
75
|
"tier.wide": "宽",
|
|
63
76
|
"tier.ultra": "超宽",
|
|
64
77
|
"tier.full": "全宽",
|
|
78
|
+
"tier.custom": "自定义",
|
|
65
79
|
"button.aria": "对话区宽度档位:{label}({value}),点击选择档位",
|
|
66
80
|
"button.title.summary": "对话区宽度档位:{label}({value})",
|
|
67
81
|
"button.title.hint": "点击选择:{list}"
|
|
@@ -73,6 +87,7 @@ window.__ModuleLoader__.load({
|
|
|
73
87
|
"tier.wide": "Wide",
|
|
74
88
|
"tier.ultra": "Ultra",
|
|
75
89
|
"tier.full": "Full",
|
|
90
|
+
"tier.custom": "Custom",
|
|
76
91
|
"button.aria": "Chat width tier: {label} ({value}). Click to choose a tier.",
|
|
77
92
|
"button.title.summary": "Chat width tier: {label} ({value})",
|
|
78
93
|
"button.title.hint": "Click to choose: {list}"
|
|
@@ -84,7 +99,10 @@ window.__ModuleLoader__.load({
|
|
|
84
99
|
{ id: "medium", key: "tier.medium", value: "960px" },
|
|
85
100
|
{ id: "wide", key: "tier.wide", value: "1280px" },
|
|
86
101
|
{ id: "ultra", key: "tier.ultra", value: "1400px" },
|
|
87
|
-
{ id: "full", key: "tier.full", value: "100%" }
|
|
102
|
+
{ id: "full", key: "tier.full", value: "100%" },
|
|
103
|
+
/* Reached only by dragging a native width handle; the displayed
|
|
104
|
+
* value is the dragged width, resolved at render time. */
|
|
105
|
+
{ id: "custom", key: "tier.custom", value: null }
|
|
88
106
|
];
|
|
89
107
|
|
|
90
108
|
function tierOf(id) {
|
|
@@ -110,6 +128,7 @@ window.__ModuleLoader__.load({
|
|
|
110
128
|
"#dsh-width-menu[hidden]{display:none}",
|
|
111
129
|
"#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
130
|
"#dsh-width-menu button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
131
|
+
"#dsh-width-menu button.disabled{opacity:.45;cursor:default;pointer-events:none}",
|
|
113
132
|
"#dsh-width-menu button.selected{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-state-business-primary)}",
|
|
114
133
|
"#dsh-width-menu .dshwm-value{color:var(--dsw-alias-label-tertiary);font-size:12px}",
|
|
115
134
|
"#dsh-width-menu button.selected .dshwm-value{color:var(--dsw-alias-state-business-primary)}"
|
|
@@ -159,15 +178,21 @@ window.__ModuleLoader__.load({
|
|
|
159
178
|
return "standard";
|
|
160
179
|
}
|
|
161
180
|
|
|
162
|
-
/* The AppFrame sets data-sidebar-collapsed
|
|
163
|
-
*
|
|
181
|
+
/* The AppFrame sets data-sidebar-collapsed on the frame element
|
|
182
|
+
* (conditionally, only while collapsed) after each render. */
|
|
164
183
|
function isSidebarCollapsed() {
|
|
165
184
|
return document.querySelector("[data-sidebar-collapsed]") !== null;
|
|
166
185
|
}
|
|
167
186
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
187
|
+
/* NOTE: the 1.0.x "close the right panel on non-standard tiers"
|
|
188
|
+
* self-heal is gone on purpose. On 0.1.7 the rightbar panel
|
|
189
|
+
* lives in its own seat: closeRightbar() only retracts the
|
|
190
|
+
* grid TRACK (rightbarTrack=false) while the seat stays
|
|
191
|
+
* expanded, so the panel keeps rendering as an OVERLAY pinned
|
|
192
|
+
* to the frame's right edge — hanging over and covering the
|
|
193
|
+
* conversation (对话窗口被边侧栏覆盖). The plugin must never
|
|
194
|
+
* touch the rightbar; the app's own width axis already yields
|
|
195
|
+
* space to the panel. */
|
|
171
196
|
|
|
172
197
|
function collapseSidebar() {
|
|
173
198
|
if (!isSidebarCollapsed()) layout.toggleSidebar();
|
|
@@ -177,10 +202,6 @@ window.__ModuleLoader__.load({
|
|
|
177
202
|
if (isSidebarCollapsed()) layout.toggleSidebar();
|
|
178
203
|
}
|
|
179
204
|
|
|
180
|
-
function closeDetails() {
|
|
181
|
-
if (isDetailsOpen()) layout.closeDetails();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
205
|
/* Locate the element that defines --dsh-chat-content-width at
|
|
185
206
|
* runtime (first match wins; only one element declares it).
|
|
186
207
|
* Cached; the app remounts the chat tree on conversation switch,
|
|
@@ -239,22 +260,77 @@ window.__ModuleLoader__.load({
|
|
|
239
260
|
return scanWidthRoot();
|
|
240
261
|
}
|
|
241
262
|
|
|
242
|
-
|
|
263
|
+
/* ---- 0.1.7 native width axis ----
|
|
264
|
+
* The conversation body's parent is the element the app's
|
|
265
|
+
* ConversationWidthControls publishes
|
|
266
|
+
* `--dsh-conversation-column-width` / `--dsh-chat-user-width`
|
|
267
|
+
* on. Writing there (never with `!important`) keeps native
|
|
268
|
+
* drag handles in charge: during a drag the app rewrites the
|
|
269
|
+
* same inline property every frame and wins the cascade. */
|
|
270
|
+
function widthTarget(root) {
|
|
271
|
+
return (root && root.parentElement) || root;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function readStoredNumber(key) {
|
|
275
|
+
try {
|
|
276
|
+
const v = Number(window.localStorage.getItem(key));
|
|
277
|
+
return Number.isFinite(v) && v > 0 ? v : null;
|
|
278
|
+
} catch (e) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* Resolve a tier to the px the app would clamp it to
|
|
284
|
+
* (resolveContentWidth: max = column - handle budget), or
|
|
285
|
+
* null when the tier leaves the width to the app. */
|
|
286
|
+
function tierPx(tier, root) {
|
|
287
|
+
const cap = Math.max(CONTENT_MIN_PX, root.offsetWidth - CONTENT_EDGE_BUDGET_PX);
|
|
243
288
|
switch (tier) {
|
|
244
|
-
case "medium": return
|
|
245
|
-
case "wide": return
|
|
246
|
-
case "ultra": return
|
|
247
|
-
case "full": return
|
|
248
|
-
|
|
289
|
+
case "medium": return Math.min(960, cap);
|
|
290
|
+
case "wide": return Math.min(1280, cap);
|
|
291
|
+
case "ultra": return Math.min(1400, cap);
|
|
292
|
+
case "full": return cap;
|
|
293
|
+
case "custom": {
|
|
294
|
+
const dragged = readStoredNumber(CUSTOM_KEY);
|
|
295
|
+
return dragged === null ? null : Math.min(dragged, cap);
|
|
296
|
+
}
|
|
297
|
+
default: return null; /* standard: app default */
|
|
249
298
|
}
|
|
250
299
|
}
|
|
251
300
|
|
|
252
301
|
function applyContentWidth(tier) {
|
|
253
302
|
const root = findWidthRoot();
|
|
254
303
|
if (root === null) return;
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
304
|
+
const target = widthTarget(root);
|
|
305
|
+
/* custom before the first drag has no width of its own —
|
|
306
|
+
* leave whatever the app last published in place. */
|
|
307
|
+
if (tier === "custom" && readStoredNumber(CUSTOM_KEY) === null) return;
|
|
308
|
+
const px = tierPx(tier, root);
|
|
309
|
+
if (px === null) {
|
|
310
|
+
if (target.style.getPropertyValue("--dsh-chat-user-width") !== "") {
|
|
311
|
+
target.style.removeProperty("--dsh-chat-user-width");
|
|
312
|
+
try {
|
|
313
|
+
window.localStorage.removeItem(APP_PREF_KEY);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
/* ignore */
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
/* Mirror into the app's preference so window resizes and
|
|
321
|
+
* conversation remounts re-clamp through the app's own
|
|
322
|
+
* publish cycle instead of relying on plugin healing. */
|
|
323
|
+
const next = String(Math.round(px));
|
|
324
|
+
if (target.style.getPropertyValue("--dsh-chat-user-width") !== next + "px") {
|
|
325
|
+
target.style.setProperty("--dsh-chat-user-width", next + "px");
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
if (window.localStorage.getItem(APP_PREF_KEY) !== next) {
|
|
329
|
+
window.localStorage.setItem(APP_PREF_KEY, next);
|
|
330
|
+
}
|
|
331
|
+
} catch (e) {
|
|
332
|
+
/* ignore */
|
|
333
|
+
}
|
|
258
334
|
}
|
|
259
335
|
|
|
260
336
|
/* true while the CURRENT tier is the one that collapsed the sidebar */
|
|
@@ -281,7 +357,6 @@ window.__ModuleLoader__.load({
|
|
|
281
357
|
} else {
|
|
282
358
|
collapsedByTier = false;
|
|
283
359
|
}
|
|
284
|
-
if (tier !== "standard") closeDetails();
|
|
285
360
|
syncButton();
|
|
286
361
|
syncMenu();
|
|
287
362
|
pulse();
|
|
@@ -308,17 +383,26 @@ window.__ModuleLoader__.load({
|
|
|
308
383
|
return `<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">${bars}</svg>`;
|
|
309
384
|
}
|
|
310
385
|
|
|
386
|
+
/* Display value for a tier; `custom` shows the dragged width. */
|
|
387
|
+
function tierValueText(tierId) {
|
|
388
|
+
const def = tierOf(tierId);
|
|
389
|
+
if (def.value !== null) return def.value;
|
|
390
|
+
const px = readStoredNumber(CUSTOM_KEY);
|
|
391
|
+
return px === null ? "—" : `${Math.round(px)}px`;
|
|
392
|
+
}
|
|
393
|
+
|
|
311
394
|
function syncButton() {
|
|
312
395
|
const tier = currentTier();
|
|
313
396
|
const def = tierOf(tier);
|
|
314
397
|
const label = t(def.key);
|
|
315
|
-
const
|
|
398
|
+
const value = tierValueText(tier);
|
|
399
|
+
const summary = t("button.title.summary", { label, value });
|
|
316
400
|
const hint = t("button.title.hint", {
|
|
317
|
-
list: TIERS.map((tierDef) => `${t(tierDef.key)} ${tierDef.
|
|
401
|
+
list: TIERS.map((tierDef) => `${t(tierDef.key)} ${tierValueText(tierDef.id)}`).join(" / ")
|
|
318
402
|
});
|
|
319
403
|
btn.innerHTML = barIcon(tier);
|
|
320
404
|
btn.classList.toggle("active", tier !== "standard");
|
|
321
|
-
btn.setAttribute("aria-label", t("button.aria", { label, value
|
|
405
|
+
btn.setAttribute("aria-label", t("button.aria", { label, value }));
|
|
322
406
|
btn.title = `${summary}\n${hint}`;
|
|
323
407
|
}
|
|
324
408
|
|
|
@@ -333,9 +417,10 @@ window.__ModuleLoader__.load({
|
|
|
333
417
|
item.type = "button";
|
|
334
418
|
item.setAttribute("role", "menuitemradio");
|
|
335
419
|
item.setAttribute("data-tier", def.id);
|
|
336
|
-
item.innerHTML = `<span class="dshwm-label">${t(def.key)}</span><span class="dshwm-value">${def.
|
|
420
|
+
item.innerHTML = `<span class="dshwm-label">${t(def.key)}</span><span class="dshwm-value">${tierValueText(def.id)}</span>`;
|
|
337
421
|
item.addEventListener("click", (e) => {
|
|
338
422
|
if (e && e.stopPropagation) e.stopPropagation();
|
|
423
|
+
if (item.classList.contains("disabled")) return;
|
|
339
424
|
applyTier(def.id);
|
|
340
425
|
hideMenu();
|
|
341
426
|
});
|
|
@@ -346,11 +431,18 @@ window.__ModuleLoader__.load({
|
|
|
346
431
|
const tier = currentTier();
|
|
347
432
|
const items = menu.querySelectorAll("button[data-tier]");
|
|
348
433
|
for (let i = 0; i < items.length; i++) {
|
|
349
|
-
const
|
|
434
|
+
const id = items[i].getAttribute("data-tier");
|
|
435
|
+
const on = id === tier;
|
|
350
436
|
items[i].classList.toggle("selected", on);
|
|
351
437
|
items[i].setAttribute("aria-checked", String(on));
|
|
352
438
|
const labelEl = items[i].querySelector(".dshwm-label");
|
|
353
|
-
if (labelEl) labelEl.textContent = t(tierOf(
|
|
439
|
+
if (labelEl) labelEl.textContent = t(tierOf(id).key);
|
|
440
|
+
const valueEl = items[i].querySelector(".dshwm-value");
|
|
441
|
+
if (valueEl) valueEl.textContent = tierValueText(id);
|
|
442
|
+
/* custom is inert until the user has dragged once */
|
|
443
|
+
const disabled = id === "custom" && readStoredNumber(CUSTOM_KEY) === null;
|
|
444
|
+
items[i].classList.toggle("disabled", disabled);
|
|
445
|
+
items[i].setAttribute("aria-disabled", String(disabled));
|
|
354
446
|
}
|
|
355
447
|
}
|
|
356
448
|
|
|
@@ -381,6 +473,48 @@ window.__ModuleLoader__.load({
|
|
|
381
473
|
document.addEventListener("click", onDocClick);
|
|
382
474
|
document.addEventListener("keydown", onDocKey);
|
|
383
475
|
|
|
476
|
+
/* ---- hand the width over to the app's native drag handles ----
|
|
477
|
+
* Pressing a `[data-width-handle]` means the user is taking the
|
|
478
|
+
* width over: switch to the `custom` tier so healing stops
|
|
479
|
+
* enforcing a preset, and let the app's drag win the cascade
|
|
480
|
+
* (it rewrites the same inline property every frame). The
|
|
481
|
+
* settled width is recorded on release so it stays
|
|
482
|
+
* re-selectable from the menu. */
|
|
483
|
+
const isHandleEvent = (e) =>
|
|
484
|
+
e.target instanceof Element && e.target.closest("[data-width-handle]") !== null;
|
|
485
|
+
|
|
486
|
+
const onHandlePointerDown = (e) => {
|
|
487
|
+
if (!isHandleEvent(e) || currentTier() === "custom") return;
|
|
488
|
+
body().setAttribute("data-dsh-width", "custom");
|
|
489
|
+
try {
|
|
490
|
+
window.localStorage.setItem(KEY, "custom");
|
|
491
|
+
} catch (err) {
|
|
492
|
+
/* ignore */
|
|
493
|
+
}
|
|
494
|
+
collapsedByTier = false;
|
|
495
|
+
syncButton();
|
|
496
|
+
syncMenu();
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const onHandlePointerUp = (e) => {
|
|
500
|
+
if (!isHandleEvent(e)) return;
|
|
501
|
+
const root = findWidthRoot();
|
|
502
|
+
if (root === null) return;
|
|
503
|
+
const px = Number.parseFloat(
|
|
504
|
+
widthTarget(root).style.getPropertyValue("--dsh-chat-user-width")
|
|
505
|
+
);
|
|
506
|
+
if (!Number.isFinite(px) || px <= 0) return;
|
|
507
|
+
try {
|
|
508
|
+
window.localStorage.setItem(CUSTOM_KEY, String(Math.round(px)));
|
|
509
|
+
} catch (err) {
|
|
510
|
+
/* ignore */
|
|
511
|
+
}
|
|
512
|
+
syncButton();
|
|
513
|
+
syncMenu();
|
|
514
|
+
};
|
|
515
|
+
document.addEventListener("pointerdown", onHandlePointerDown, true);
|
|
516
|
+
document.addEventListener("pointerup", onHandlePointerUp, true);
|
|
517
|
+
|
|
384
518
|
/* ---- feedback ring so every selection is visibly answered ---- */
|
|
385
519
|
function pulse() {
|
|
386
520
|
btn.classList.remove("pulse");
|
|
@@ -399,9 +533,16 @@ window.__ModuleLoader__.load({
|
|
|
399
533
|
/* ---- attach once the app shell has mounted ---- */
|
|
400
534
|
let attached = false;
|
|
401
535
|
|
|
536
|
+
/* The old gate matched [data-sidebar-collapsed] /
|
|
537
|
+
* [data-details-collapsed]. 0.1.5-rc.2 renders collapsed
|
|
538
|
+
* markers conditionally (`sidebarCollapsed || void 0`) and
|
|
539
|
+
* removed data-details-collapsed, so with the sidebar expanded
|
|
540
|
+
* nothing ever matched and the button never mounted. Wait for
|
|
541
|
+
* the AppFrame overlay layer instead — `data-shell-overlay` is
|
|
542
|
+
* rendered unconditionally once the shell exists. */
|
|
402
543
|
function attach() {
|
|
403
544
|
if (attached || !body()) return;
|
|
404
|
-
if (document.querySelector("[data-
|
|
545
|
+
if (document.querySelector("[data-shell-overlay]") === null) return;
|
|
405
546
|
attached = true;
|
|
406
547
|
try {
|
|
407
548
|
window.localStorage.removeItem(OLD_KEY);
|
|
@@ -423,16 +564,19 @@ window.__ModuleLoader__.load({
|
|
|
423
564
|
attach();
|
|
424
565
|
}, READY_TIMEOUT_MS);
|
|
425
566
|
|
|
426
|
-
/* ---- self-heal:
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
567
|
+
/* ---- self-heal: re-apply the width when the chat tree
|
|
568
|
+
* remounts (conversation switch). Panels are never managed
|
|
569
|
+
* here — the user may expand/collapse either side any time;
|
|
570
|
+
* forcing the rightbar closed only retracts the grid track
|
|
571
|
+
* while the seat keeps rendering as an overlay that covers
|
|
572
|
+
* the conversation. Driven by DOM mutations, scheduled on the
|
|
573
|
+
* next animation frame (before the browser paints) instead of
|
|
574
|
+
* a fixed hot loop: the width must land on the fresh root
|
|
575
|
+
* before its first paint — otherwise the default width
|
|
576
|
+
* flashes for a frame. While the new root has not appeared
|
|
577
|
+
* yet (mid-remount), full scans are owned by a backoff retry;
|
|
578
|
+
* mutation-driven heals only try the cheap path walk so DOM
|
|
579
|
+
* churn cannot storm. ---- */
|
|
436
580
|
let rootMissing = false;
|
|
437
581
|
let rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
438
582
|
let rootRetryTimer = null;
|
|
@@ -469,7 +613,6 @@ window.__ModuleLoader__.load({
|
|
|
469
613
|
function heal(fromObserver) {
|
|
470
614
|
const tier = currentTier();
|
|
471
615
|
if (tier === "standard") return;
|
|
472
|
-
closeDetails();
|
|
473
616
|
const prev = widthRoot;
|
|
474
617
|
let root;
|
|
475
618
|
if (rootMissing) {
|
|
@@ -493,6 +636,11 @@ window.__ModuleLoader__.load({
|
|
|
493
636
|
rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
494
637
|
}
|
|
495
638
|
if (root !== prev) applyContentWidth(tier);
|
|
639
|
+
/* full tracks the live column: after the sidebar collapse
|
|
640
|
+
* animation settles the column widens, and the width must
|
|
641
|
+
* follow (applyContentWidth is diff-checked, so steady
|
|
642
|
+
* state writes nothing). */
|
|
643
|
+
if (tier === "full") applyContentWidth("full");
|
|
496
644
|
}
|
|
497
645
|
|
|
498
646
|
function startHealing() {
|
|
@@ -508,7 +656,7 @@ window.__ModuleLoader__.load({
|
|
|
508
656
|
subtree: true,
|
|
509
657
|
childList: true,
|
|
510
658
|
attributes: true,
|
|
511
|
-
attributeFilter: ["data-
|
|
659
|
+
attributeFilter: ["data-sidebar-collapsed"]
|
|
512
660
|
});
|
|
513
661
|
}
|
|
514
662
|
/* Low-frequency fallback so self-heal still runs if the
|
|
@@ -528,10 +676,18 @@ window.__ModuleLoader__.load({
|
|
|
528
676
|
if (observer !== null) observer.disconnect();
|
|
529
677
|
document.removeEventListener("click", onDocClick);
|
|
530
678
|
document.removeEventListener("keydown", onDocKey);
|
|
679
|
+
document.removeEventListener("pointerdown", onHandlePointerDown, true);
|
|
680
|
+
document.removeEventListener("pointerup", onHandlePointerUp, true);
|
|
531
681
|
if (btn.parentNode) btn.parentNode.removeChild(btn);
|
|
532
682
|
if (menu.parentNode) menu.parentNode.removeChild(menu);
|
|
533
683
|
if (styleTag.parentNode) styleTag.parentNode.removeChild(styleTag);
|
|
534
|
-
|
|
684
|
+
/* Hand the width back to the app default; the app's own
|
|
685
|
+
* preference (which this plugin mirrors) stays so the last
|
|
686
|
+
* width survives a plugin reload — matching native-drag
|
|
687
|
+
* semantics. */
|
|
688
|
+
if (widthRoot !== null) {
|
|
689
|
+
widthTarget(widthRoot).style.removeProperty("--dsh-chat-user-width");
|
|
690
|
+
}
|
|
535
691
|
body().removeAttribute("data-dsh-width");
|
|
536
692
|
};
|
|
537
693
|
}, "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.6",
|
|
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+, 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": {
|