dsh-width-tiers 1.0.6 → 1.0.8
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 +26 -9
- package/README.zh.md +22 -8
- package/lib/client.js +299 -43
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,11 +10,11 @@ ultra / full / custom).
|
|
|
10
10
|
## Why
|
|
11
11
|
|
|
12
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,
|
|
13
|
+
`--dsh-chat-content-width` (0.1.7–0.2.0 default `clamp(680px, column × 0.64,
|
|
14
14
|
920px)`), no matter how wide your window is, so wide tables always need
|
|
15
15
|
horizontal scrolling.
|
|
16
16
|
|
|
17
|
-
This plugin sets the width per tier through
|
|
17
|
+
This plugin sets the width per tier through the app's **native width axis**
|
|
18
18
|
(`--dsh-chat-user-width` + the app's `dsh.conversation.contentWidth`
|
|
19
19
|
preference), letting the app clamp every value to "column − 176px handle
|
|
20
20
|
budget" — so the built-in drag handles keep working:
|
|
@@ -22,21 +22,35 @@ budget" — so the built-in drag handles keep working:
|
|
|
22
22
|
| Tier | Content width | Sidebar |
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| standard 标准 | app default | untouched |
|
|
25
|
-
| medium 中等 |
|
|
25
|
+
| medium 中等 | 1120px | untouched |
|
|
26
26
|
| wide 宽 | 1280px | untouched |
|
|
27
27
|
| ultra 超宽 | 1400px | untouched |
|
|
28
28
|
| full 全宽 | column minus the handle budget (fills the available width) | collapsed to the icon rail |
|
|
29
29
|
| custom 自定义 | your dragged width, re-selectable from the menu | untouched |
|
|
30
30
|
|
|
31
|
+
The menu and the button title show nominal px for the fixed tiers (medium
|
|
32
|
+
1120px, wide 1280px, ultra 1400px). Standard shows the app default px it
|
|
33
|
+
resolves right now (`clamp(680px, column × 0.64, 920px)`, e.g. 710px); full
|
|
34
|
+
shows the live maximum (`column − 176px`, e.g. 934px); custom shows its
|
|
35
|
+
recorded width or "Not set". The app still clamps the applied width to
|
|
36
|
+
"column − 176px"; on a narrow window a higher tier therefore behaves like
|
|
37
|
+
"full".
|
|
38
|
+
|
|
31
39
|
On narrow windows wider tiers are clamped to the available column, so nothing
|
|
32
40
|
overflows under the sidebar.
|
|
33
41
|
|
|
34
42
|
The choice persists in `localStorage` and is restored on the next visit; the
|
|
35
43
|
menu highlights the current tier, and the button's five level-bars show it at
|
|
36
|
-
a glance (all lit for custom). Dragging one of
|
|
44
|
+
a glance (all lit for custom). Dragging one of the app's native edge handles
|
|
37
45
|
switches the picker to the custom tier — the plugin stands aside for native
|
|
38
46
|
drags.
|
|
39
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.
|
|
53
|
+
|
|
40
54
|
## Install
|
|
41
55
|
|
|
42
56
|
```bash
|
|
@@ -62,8 +76,9 @@ Restart `dsh web` (the plugin loads at boot) and hard-refresh the page
|
|
|
62
76
|
- `full` collapses the sidebar to the icon rail through the app's own toggle
|
|
63
77
|
— it is never force-hidden or locked: click a rail icon any time to expand
|
|
64
78
|
it, and it stays expanded.
|
|
65
|
-
-
|
|
66
|
-
|
|
79
|
+
- The right panel (rightbar) is never touched: its track and panel are fully
|
|
80
|
+
owned by the app, and the conversation yields space to it on its own.
|
|
81
|
+
`standard` leaves the layout entirely to the app.
|
|
67
82
|
|
|
68
83
|
## Language
|
|
69
84
|
|
|
@@ -90,10 +105,12 @@ language, and Chinese is the last resort. No plugin-side setting needed.
|
|
|
90
105
|
## Limitations
|
|
91
106
|
|
|
92
107
|
- Content width cannot exceed the window: on a narrow window, wider tiers are
|
|
93
|
-
capped by the available column
|
|
94
|
-
|
|
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.
|
|
95
111
|
- Requires the DSH Web client plugin system (dsh ≥ 0.1.0-rc.x with a `web`
|
|
96
|
-
profile).
|
|
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.
|
|
97
114
|
|
|
98
115
|
## Uninstall
|
|
99
116
|
|
package/README.zh.md
CHANGED
|
@@ -9,28 +9,38 @@ DeepSeek Harness Web GUI 的客户端插件:给对话区加**内容宽度档
|
|
|
9
9
|
## 为什么需要它
|
|
10
10
|
|
|
11
11
|
DSH Web 把消息列——包括里面的所有表格——的宽度限制在
|
|
12
|
-
`--dsh-chat-content-width`(0.1.7
|
|
13
|
-
|
|
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
16
|
`dsh.conversation.contentWidth` 偏好)按档位设定宽度,由应用自身钳制到
|
|
17
17
|
「列宽 − 176px 手柄预留」,因此自带的原生拖拽手柄始终可用:
|
|
18
18
|
|
|
19
19
|
| 档位 | 内容宽度 | 侧边栏 |
|
|
20
20
|
|---|---|---|
|
|
21
21
|
| 标准 | 应用默认 | 不动 |
|
|
22
|
-
| 中等 |
|
|
22
|
+
| 中等 | 1120px | 不动 |
|
|
23
23
|
| 宽 | 1280px | 不动 |
|
|
24
24
|
| 超宽 | 1400px | 不动 |
|
|
25
25
|
| 全宽 | 列宽减手柄预留(占满可用宽度) | 收起为图标轨道 |
|
|
26
26
|
| 自定义 | 拖拽原生手柄得出,可在菜单回切 | 不动 |
|
|
27
27
|
|
|
28
|
+
菜单与按钮标题中:中等 / 宽 / 超宽显示**固定标称值**(1120 / 1280 / 1400px),
|
|
29
|
+
标准显示应用当前默认值(`clamp(680px, 列宽 × 0.64, 920px)`,如 710px),全宽
|
|
30
|
+
显示当前列宽下的最大实际值(列宽 − 176px,如 934px),自定义显示已记录的
|
|
31
|
+
宽度或「未设置」。实际生效宽度仍由应用按「列宽 − 176px」钳制;窄窗口下
|
|
32
|
+
选定较高档位时,效果等同于「全宽」。
|
|
33
|
+
|
|
28
34
|
窄窗口下超宽档位会自动被钳到可用宽度,不会溢出被侧栏覆盖。
|
|
29
35
|
|
|
30
36
|
选择会记住(localStorage),下次打开自动恢复;菜单里当前档位高亮,按钮上
|
|
31
|
-
|
|
37
|
+
的五条横条图标也直观显示当前档位(自定义档全亮)。拖动应用自带的
|
|
32
38
|
对话区边缘手柄会自动切到「自定义」档,插件让位于原生拖拽。
|
|
33
39
|
|
|
40
|
+
若从未拖拽过,点击「自定义」不会静默套用某个宽度:右侧对话边缘会出现
|
|
41
|
+
拖拽引导,手柄高亮;拖拽时气泡实时显示对话列宽度,松开后记为该档的值。
|
|
42
|
+
之后每次拖拽手柄(无论从哪个档位开始)都会重新高亮手柄并显示实时宽度。
|
|
43
|
+
|
|
34
44
|
## 安装
|
|
35
45
|
|
|
36
46
|
```bash
|
|
@@ -55,7 +65,8 @@ bundle patch 会自动插入 `dsh-width-tiers` 加载项——无需手动改
|
|
|
55
65
|
- 点圆形按钮 → 选择档位,当前档位高亮。
|
|
56
66
|
- 「全宽」通过应用自己的收起按钮把侧边栏收成图标轨道——**不会被锁死**:
|
|
57
67
|
随时点轨道图标展开,展开后保持展开。
|
|
58
|
-
-
|
|
68
|
+
- 插件**不碰右侧面板**:右栏的轨道与面板完全由应用管理,展开时对话内容
|
|
69
|
+
自适应让位;「标准」档完全交给应用。
|
|
59
70
|
|
|
60
71
|
## 多语言
|
|
61
72
|
|
|
@@ -76,9 +87,12 @@ bundle patch 会自动插入 `dsh-width-tiers` 加载项——无需手动改
|
|
|
76
87
|
|
|
77
88
|
## 已知限制
|
|
78
89
|
|
|
79
|
-
-
|
|
80
|
-
|
|
90
|
+
- 内容宽度不可能超过窗口:窗口窄时,高档位会被可用列宽截断(选择器仍显示
|
|
91
|
+
标称值,实际宽度以拖拽气泡/窗口效果为准);想要最大空间请用「全宽」
|
|
92
|
+
(同时收起侧边栏)。
|
|
81
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
|
+
对比确认一致。
|
|
82
96
|
|
|
83
97
|
## 卸载
|
|
84
98
|
|
package/lib/client.js
CHANGED
|
@@ -10,7 +10,7 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
* dsh-width-tiers — chat content width tiers for the DSH Web GUI.
|
|
11
11
|
*
|
|
12
12
|
* The app derives the transcript width from
|
|
13
|
-
* `--dsh-chat-content-width`, which 0.1.7
|
|
13
|
+
* `--dsh-chat-content-width`, which 0.1.7 and 0.2.0 resolve as
|
|
14
14
|
* `var(--dsh-chat-user-width, clamp(680px, column * .64, 920px))` on
|
|
15
15
|
* the conversation body. 0.1.7 also added native drag handles
|
|
16
16
|
* (ConversationWidthControls) that publish
|
|
@@ -23,21 +23,32 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
* `applyContentWidth`. Other invariants:
|
|
24
24
|
*
|
|
25
25
|
* 标准 standard app default (no override)
|
|
26
|
-
* 中等 medium
|
|
26
|
+
* 中等 medium 1120px (sidebar untouched)
|
|
27
27
|
* 宽 wide 1280px (sidebar untouched)
|
|
28
28
|
* 超宽 ultra 1400px (sidebar untouched)
|
|
29
29
|
* 全宽 full column max (sidebar collapsed to the icon rail)
|
|
30
30
|
* 自定义 custom user-dragged width (plugin hands width over to
|
|
31
31
|
* the app's native handles; the dragged value stays
|
|
32
|
-
* re-selectable from the menu)
|
|
32
|
+
* re-selectable from the menu). Before the first
|
|
33
|
+
* drag, picking it opens a guide at the right edge
|
|
34
|
+
* and the live column width is shown while dragging.
|
|
35
|
+
*
|
|
36
|
+
* The menu and title show fixed px for medium/wide/ultra
|
|
37
|
+
* (1120/1280/1400), the live app default px for standard, the live
|
|
38
|
+
* maximum (column − 176) for full, and the recorded drag width or
|
|
39
|
+
* "未设置" for custom. The app still clamps every applied px to
|
|
40
|
+
* column − 176. Every handle press (not just the first custom one)
|
|
41
|
+
* opens the live width readout, so repeat drags keep the highlight
|
|
42
|
+
* and the px bubble.
|
|
33
43
|
*
|
|
34
44
|
* - Panels are driven through the layout service (ctx.layout), never
|
|
35
45
|
* by simulating clicks.
|
|
36
46
|
* - The sidebar is only ever collapsed by the `full` tier, through the
|
|
37
47
|
* app's own toggle — never force-hidden, never locked. Going back to
|
|
38
48
|
* `standard` restores it only if this plugin collapsed it.
|
|
39
|
-
* -
|
|
40
|
-
*
|
|
49
|
+
* - The rightbar is never touched: on 0.1.7+ its panel lives in its
|
|
50
|
+
* own seat, so closing the track would leave it overlaying the
|
|
51
|
+
* conversation; the app's width axis yields space on its own.
|
|
41
52
|
* - The chosen tier persists in localStorage and is restored on the
|
|
42
53
|
* next visit.
|
|
43
54
|
* - All copy is localized through the app's locale service: tier
|
|
@@ -54,6 +65,13 @@ window.__ModuleLoader__.load({
|
|
|
54
65
|
* the horizontal budget both width handles + safe edges reserve. */
|
|
55
66
|
const CONTENT_MIN_PX = 640;
|
|
56
67
|
const CONTENT_EDGE_BUDGET_PX = 176;
|
|
68
|
+
/* Nominal content widths of the fixed tiers (px). */
|
|
69
|
+
const NOMINAL_WIDTH_PX = { medium: 1120, wide: 1280, ultra: 1400 };
|
|
70
|
+
/* The app's no-preference formula: clamp(680px, column × 0.64, 920px).
|
|
71
|
+
* Used as the last-resort live readout before the first drag. */
|
|
72
|
+
const DEFAULT_MIN_PX = 680;
|
|
73
|
+
const DEFAULT_MAX_PX = 920;
|
|
74
|
+
const DEFAULT_COLUMN_RATIO = 0.64;
|
|
57
75
|
const READY_TIMEOUT_MS = 30000;
|
|
58
76
|
/* Self-heal cadence: DOM-driven (MutationObserver scheduling on the
|
|
59
77
|
* next animation frame — before paint, so the width never flashes
|
|
@@ -76,6 +94,8 @@ window.__ModuleLoader__.load({
|
|
|
76
94
|
"tier.ultra": "超宽",
|
|
77
95
|
"tier.full": "全宽",
|
|
78
96
|
"tier.custom": "自定义",
|
|
97
|
+
"value.custom.unset": "未设置",
|
|
98
|
+
"guide.hint": "拖拽右侧边缘调整对话宽度",
|
|
79
99
|
"button.aria": "对话区宽度档位:{label}({value}),点击选择档位",
|
|
80
100
|
"button.title.summary": "对话区宽度档位:{label}({value})",
|
|
81
101
|
"button.title.hint": "点击选择:{list}"
|
|
@@ -88,21 +108,26 @@ window.__ModuleLoader__.load({
|
|
|
88
108
|
"tier.ultra": "Ultra",
|
|
89
109
|
"tier.full": "Full",
|
|
90
110
|
"tier.custom": "Custom",
|
|
111
|
+
"value.custom.unset": "Not set",
|
|
112
|
+
"guide.hint": "Drag the right edge to resize",
|
|
91
113
|
"button.aria": "Chat width tier: {label} ({value}). Click to choose a tier.",
|
|
92
114
|
"button.title.summary": "Chat width tier: {label} ({value})",
|
|
93
115
|
"button.title.hint": "Click to choose: {list}"
|
|
94
116
|
};
|
|
95
117
|
//#endregion
|
|
96
118
|
|
|
119
|
+
/* Tier ids and label keys only — the picker resolves displayed values
|
|
120
|
+
* in tierValueText (fixed px for medium/wide/ultra, live px for
|
|
121
|
+
* standard/full, recorded px for custom). */
|
|
97
122
|
const TIERS = [
|
|
98
|
-
{ id: "standard", key: "tier.standard"
|
|
99
|
-
{ id: "medium", key: "tier.medium"
|
|
100
|
-
{ id: "wide", key: "tier.wide"
|
|
101
|
-
{ id: "ultra", key: "tier.ultra"
|
|
102
|
-
{ id: "full", key: "tier.full"
|
|
103
|
-
/* Reached
|
|
104
|
-
*
|
|
105
|
-
{ id: "custom", key: "tier.custom"
|
|
123
|
+
{ id: "standard", key: "tier.standard" },
|
|
124
|
+
{ id: "medium", key: "tier.medium" },
|
|
125
|
+
{ id: "wide", key: "tier.wide" },
|
|
126
|
+
{ id: "ultra", key: "tier.ultra" },
|
|
127
|
+
{ id: "full", key: "tier.full" },
|
|
128
|
+
/* Reached by dragging a native width handle, or by picking it in
|
|
129
|
+
* the menu and following the right-edge drag guide. */
|
|
130
|
+
{ id: "custom", key: "tier.custom" }
|
|
106
131
|
];
|
|
107
132
|
|
|
108
133
|
function tierOf(id) {
|
|
@@ -128,10 +153,15 @@ window.__ModuleLoader__.load({
|
|
|
128
153
|
"#dsh-width-menu[hidden]{display:none}",
|
|
129
154
|
"#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}",
|
|
130
155
|
"#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}",
|
|
132
156
|
"#dsh-width-menu button.selected{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-state-business-primary)}",
|
|
133
157
|
"#dsh-width-menu .dshwm-value{color:var(--dsw-alias-label-tertiary);font-size:12px}",
|
|
134
|
-
"#dsh-width-menu button.selected .dshwm-value{color:var(--dsw-alias-state-business-primary)}"
|
|
158
|
+
"#dsh-width-menu button.selected .dshwm-value{color:var(--dsw-alias-state-business-primary)}",
|
|
159
|
+
"#dsh-width-guide{position:fixed;z-index:1001;display:flex;align-items:center;gap:8px;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-primary);font-size:12px;line-height:16px;box-shadow:0 8px 28px rgba(0,0,0,.24);pointer-events:none;white-space:nowrap;transform:translateY(-50%)}",
|
|
160
|
+
"#dsh-width-guide[hidden]{display:none}",
|
|
161
|
+
"#dsh-width-guide svg{flex:none;color:var(--dsw-alias-state-business-primary)}",
|
|
162
|
+
"#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=right]{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}",
|
|
164
|
+
"body[data-dsh-width-guide] [data-width-handle=right]::after{opacity:1}"
|
|
135
165
|
].join("");
|
|
136
166
|
//#endregion
|
|
137
167
|
|
|
@@ -286,9 +316,10 @@ window.__ModuleLoader__.load({
|
|
|
286
316
|
function tierPx(tier, root) {
|
|
287
317
|
const cap = Math.max(CONTENT_MIN_PX, root.offsetWidth - CONTENT_EDGE_BUDGET_PX);
|
|
288
318
|
switch (tier) {
|
|
289
|
-
case "medium":
|
|
290
|
-
case "wide":
|
|
291
|
-
case "ultra":
|
|
319
|
+
case "medium":
|
|
320
|
+
case "wide":
|
|
321
|
+
case "ultra":
|
|
322
|
+
return Math.min(NOMINAL_WIDTH_PX[tier], cap);
|
|
292
323
|
case "full": return cap;
|
|
293
324
|
case "custom": {
|
|
294
325
|
const dragged = readStoredNumber(CUSTOM_KEY);
|
|
@@ -298,6 +329,25 @@ window.__ModuleLoader__.load({
|
|
|
298
329
|
}
|
|
299
330
|
}
|
|
300
331
|
|
|
332
|
+
/* The transcript width the app currently resolves for the live
|
|
333
|
+
* column (after its own clamp), used by the custom-drag guide.
|
|
334
|
+
* During a drag the app writes the clamped px to the target's
|
|
335
|
+
* inline `--dsh-chat-user-width`, so reading that is exact and
|
|
336
|
+
* avoids relying on how a browser serializes a computed custom
|
|
337
|
+
* property (which may stay as a var()/clamp() token stream). */
|
|
338
|
+
function liveWidthPx(root) {
|
|
339
|
+
const target = widthTarget(root);
|
|
340
|
+
const inline = Number.parseFloat(target.style.getPropertyValue("--dsh-chat-user-width"));
|
|
341
|
+
if (Number.isFinite(inline) && inline > 0) return Math.round(inline);
|
|
342
|
+
const raw = getComputedStyle(root).getPropertyValue("--dsh-chat-content-width");
|
|
343
|
+
const computed = Number.parseFloat(raw);
|
|
344
|
+
if (Number.isFinite(computed) && computed > 0) return Math.round(computed);
|
|
345
|
+
return Math.round(Math.max(
|
|
346
|
+
DEFAULT_MIN_PX,
|
|
347
|
+
Math.min(root.offsetWidth * DEFAULT_COLUMN_RATIO, DEFAULT_MAX_PX)
|
|
348
|
+
));
|
|
349
|
+
}
|
|
350
|
+
|
|
301
351
|
function applyContentWidth(tier) {
|
|
302
352
|
const root = findWidthRoot();
|
|
303
353
|
if (root === null) return;
|
|
@@ -383,12 +433,38 @@ window.__ModuleLoader__.load({
|
|
|
383
433
|
return `<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">${bars}</svg>`;
|
|
384
434
|
}
|
|
385
435
|
|
|
386
|
-
/*
|
|
436
|
+
/* Value shown in the picker. Fixed tiers advertise their
|
|
437
|
+
* nominal px; standard and full are inherently relative to the
|
|
438
|
+
* current column, so they show the px the app resolves right
|
|
439
|
+
* now (full is the column's maximum, not a word). */
|
|
387
440
|
function tierValueText(tierId) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
441
|
+
switch (tierId) {
|
|
442
|
+
case "standard": {
|
|
443
|
+
const root = findWidthRoot();
|
|
444
|
+
if (root === null) return "—";
|
|
445
|
+
return `${Math.round(Math.max(
|
|
446
|
+
DEFAULT_MIN_PX,
|
|
447
|
+
Math.min(root.offsetWidth * DEFAULT_COLUMN_RATIO, DEFAULT_MAX_PX)
|
|
448
|
+
))}px`;
|
|
449
|
+
}
|
|
450
|
+
case "medium":
|
|
451
|
+
case "wide":
|
|
452
|
+
case "ultra":
|
|
453
|
+
return `${NOMINAL_WIDTH_PX[tierId]}px`;
|
|
454
|
+
case "full": {
|
|
455
|
+
const root = findWidthRoot();
|
|
456
|
+
if (root === null) return "—";
|
|
457
|
+
return `${Math.round(Math.max(
|
|
458
|
+
CONTENT_MIN_PX,
|
|
459
|
+
root.offsetWidth - CONTENT_EDGE_BUDGET_PX
|
|
460
|
+
))}px`;
|
|
461
|
+
}
|
|
462
|
+
case "custom": {
|
|
463
|
+
const dragged = readStoredNumber(CUSTOM_KEY);
|
|
464
|
+
return dragged === null ? t("value.custom.unset") : `${Math.round(dragged)}px`;
|
|
465
|
+
}
|
|
466
|
+
default: return "—";
|
|
467
|
+
}
|
|
392
468
|
}
|
|
393
469
|
|
|
394
470
|
function syncButton() {
|
|
@@ -420,13 +496,27 @@ window.__ModuleLoader__.load({
|
|
|
420
496
|
item.innerHTML = `<span class="dshwm-label">${t(def.key)}</span><span class="dshwm-value">${tierValueText(def.id)}</span>`;
|
|
421
497
|
item.addEventListener("click", (e) => {
|
|
422
498
|
if (e && e.stopPropagation) e.stopPropagation();
|
|
423
|
-
|
|
499
|
+
/* Custom before the first drag has no width of its own:
|
|
500
|
+
* open the right-edge guide instead of applying a value. */
|
|
501
|
+
if (def.id === "custom" && readStoredNumber(CUSTOM_KEY) === null) {
|
|
502
|
+
beginCustomGuide();
|
|
503
|
+
hideMenu();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
endGuide(false);
|
|
424
507
|
applyTier(def.id);
|
|
425
508
|
hideMenu();
|
|
426
509
|
});
|
|
427
510
|
menu.appendChild(item);
|
|
428
511
|
}
|
|
429
512
|
|
|
513
|
+
/* Write only on change: syncMenu also runs on window resizes,
|
|
514
|
+
* and untouched text should not churn the DOM (the plugin's own
|
|
515
|
+
* MutationObserver watches childList). */
|
|
516
|
+
function setText(el, text) {
|
|
517
|
+
if (el.textContent !== text) el.textContent = text;
|
|
518
|
+
}
|
|
519
|
+
|
|
430
520
|
function syncMenu() {
|
|
431
521
|
const tier = currentTier();
|
|
432
522
|
const items = menu.querySelectorAll("button[data-tier]");
|
|
@@ -436,13 +526,9 @@ window.__ModuleLoader__.load({
|
|
|
436
526
|
items[i].classList.toggle("selected", on);
|
|
437
527
|
items[i].setAttribute("aria-checked", String(on));
|
|
438
528
|
const labelEl = items[i].querySelector(".dshwm-label");
|
|
439
|
-
if (labelEl) labelEl
|
|
529
|
+
if (labelEl) setText(labelEl, t(tierOf(id).key));
|
|
440
530
|
const valueEl = items[i].querySelector(".dshwm-value");
|
|
441
|
-
if (valueEl) valueEl
|
|
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));
|
|
531
|
+
if (valueEl) setText(valueEl, tierValueText(id));
|
|
446
532
|
}
|
|
447
533
|
}
|
|
448
534
|
|
|
@@ -464,15 +550,151 @@ window.__ModuleLoader__.load({
|
|
|
464
550
|
});
|
|
465
551
|
|
|
466
552
|
const onDocClick = (e) => {
|
|
467
|
-
if (menu.hidden)
|
|
468
|
-
|
|
553
|
+
if (!menu.hidden && e.target !== btn && !menu.contains(e.target)) hideMenu();
|
|
554
|
+
/* Clicking anywhere but the guide's handle cancels a
|
|
555
|
+
* pending custom guide and restores the previous tier. */
|
|
556
|
+
if (guideActive && !guideDragging && e.target !== btn && !menu.contains(e.target) && !isHandleEvent(e)) {
|
|
557
|
+
endGuide(true);
|
|
558
|
+
}
|
|
469
559
|
};
|
|
470
560
|
const onDocKey = (e) => {
|
|
471
|
-
if (e.key
|
|
561
|
+
if (e.key !== "Escape") return;
|
|
562
|
+
if (!menu.hidden) hideMenu();
|
|
563
|
+
if (guideActive) endGuide(true);
|
|
472
564
|
};
|
|
473
565
|
document.addEventListener("click", onDocClick);
|
|
474
566
|
document.addEventListener("keydown", onDocKey);
|
|
475
567
|
|
|
568
|
+
/* ---- custom-drag guide ----
|
|
569
|
+
* A first-time `custom` pick has no recorded width. Instead of
|
|
570
|
+
* silently keeping the current width, point at the app's
|
|
571
|
+
* right-edge width handle and show the live transcript width
|
|
572
|
+
* while the user drags. The guide never writes the width
|
|
573
|
+
* itself: the app's native drag does (and wins the cascade),
|
|
574
|
+
* and the plugin only records the settled value on release. */
|
|
575
|
+
const guide = document.createElement("div");
|
|
576
|
+
guide.id = "dsh-width-guide";
|
|
577
|
+
guide.setAttribute("aria-hidden", "true");
|
|
578
|
+
guide.hidden = true;
|
|
579
|
+
guide.innerHTML =
|
|
580
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" focusable="false">' +
|
|
581
|
+
'<path d="M6 3 2 8l4 5M10 3l4 5-4 5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
|
|
582
|
+
'<span class="dshwg-text"></span>';
|
|
583
|
+
const guideTextEl = guide.querySelector(".dshwg-text");
|
|
584
|
+
|
|
585
|
+
let guideActive = false;
|
|
586
|
+
let guideDragging = false;
|
|
587
|
+
/* true while the bubble came from picking custom in the menu (its
|
|
588
|
+
* cancel path restores the previous tier). Direct handle drags
|
|
589
|
+
* open the same bubble but never "cancel" a tier. */
|
|
590
|
+
let guideFromMenu = false;
|
|
591
|
+
let guideHideTimer = null;
|
|
592
|
+
let tierBeforeGuide = "standard";
|
|
593
|
+
|
|
594
|
+
/* The px the app currently resolves for the transcript, after
|
|
595
|
+
* its own clamp — what the guide's live readout shows. */
|
|
596
|
+
function guideValueText() {
|
|
597
|
+
const root = findWidthRoot();
|
|
598
|
+
const px = root === null ? null : liveWidthPx(root);
|
|
599
|
+
return px === null ? "—" : `${px}px`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* Keep the bubble beside the right handle (it moves with the
|
|
603
|
+
* dragged edge); fall back to the frame's right edge when the
|
|
604
|
+
* handle is not mounted yet (hero / settling phase). */
|
|
605
|
+
function positionGuide() {
|
|
606
|
+
if (!guideActive) return;
|
|
607
|
+
const handle = document.querySelector('[data-width-handle="right"]');
|
|
608
|
+
const viewport = window.innerWidth || 0;
|
|
609
|
+
if (handle === null || viewport === 0) {
|
|
610
|
+
guide.style.left = "auto";
|
|
611
|
+
guide.style.right = "24px";
|
|
612
|
+
guide.style.top = "50%";
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const rect = handle.getBoundingClientRect();
|
|
616
|
+
const gap = 10;
|
|
617
|
+
const width = guide.offsetWidth || 180;
|
|
618
|
+
guide.style.top = `${rect.top + rect.height / 2}px`;
|
|
619
|
+
guide.style.right = "auto";
|
|
620
|
+
if (viewport - rect.right >= width + gap) {
|
|
621
|
+
guide.style.left = `${rect.right + gap}px`;
|
|
622
|
+
} else {
|
|
623
|
+
guide.style.left = `${Math.max(8, rect.left - gap - width)}px`;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function setGuideText(mode, text) {
|
|
628
|
+
guide.dataset.mode = mode;
|
|
629
|
+
setText(guideTextEl, text);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function startGuide() {
|
|
633
|
+
guideFromMenu = true;
|
|
634
|
+
guideActive = true;
|
|
635
|
+
guideDragging = false;
|
|
636
|
+
guide.hidden = false;
|
|
637
|
+
body().setAttribute("data-dsh-width-guide", "custom");
|
|
638
|
+
setGuideText("hint", t("guide.hint"));
|
|
639
|
+
positionGuide();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* Every handle press shows the live readout, not just the first
|
|
643
|
+
* guided drag — a repeat custom drag must keep the handle
|
|
644
|
+
* highlight and the width bubble. */
|
|
645
|
+
function startLiveGuide() {
|
|
646
|
+
window.clearTimeout(guideHideTimer);
|
|
647
|
+
guideActive = true;
|
|
648
|
+
guideDragging = true;
|
|
649
|
+
guide.hidden = false;
|
|
650
|
+
body().setAttribute("data-dsh-width-guide", "custom");
|
|
651
|
+
setGuideText("live", guideValueText());
|
|
652
|
+
positionGuide();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function beginCustomGuide() {
|
|
656
|
+
/* Remember the tier to restore on cancel only on the first
|
|
657
|
+
* entry; re-picking custom while the guide is open must not
|
|
658
|
+
* make "custom" the restore target. */
|
|
659
|
+
if (!guideActive) {
|
|
660
|
+
tierBeforeGuide = currentTier();
|
|
661
|
+
applyTier("custom");
|
|
662
|
+
}
|
|
663
|
+
startGuide();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* Show the settled width briefly, then remove the guide. */
|
|
667
|
+
function finishGuide() {
|
|
668
|
+
if (!guideActive) return;
|
|
669
|
+
guideDragging = false;
|
|
670
|
+
setGuideText("live", guideValueText());
|
|
671
|
+
window.clearTimeout(guideHideTimer);
|
|
672
|
+
guideHideTimer = window.setTimeout(() => endGuide(false), 800);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/* `restoreTier` is for cancelling a menu-opened guide (click-away /
|
|
676
|
+
* Escape): if it never recorded a width, go back to the tier the
|
|
677
|
+
* user had before opening it. Drag-opened bubbles keep custom. */
|
|
678
|
+
function endGuide(restoreTier) {
|
|
679
|
+
if (!guideActive) return;
|
|
680
|
+
guideActive = false;
|
|
681
|
+
guideDragging = false;
|
|
682
|
+
window.clearTimeout(guideHideTimer);
|
|
683
|
+
guide.hidden = true;
|
|
684
|
+
guide.dataset.mode = "hint";
|
|
685
|
+
body().removeAttribute("data-dsh-width-guide");
|
|
686
|
+
const restore = restoreTier && guideFromMenu && readStoredNumber(CUSTOM_KEY) === null;
|
|
687
|
+
guideFromMenu = false;
|
|
688
|
+
if (restore) applyTier(tierBeforeGuide);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const onGuidePointerMove = () => {
|
|
692
|
+
if (!guideActive || !guideDragging) return;
|
|
693
|
+
setGuideText("live", guideValueText());
|
|
694
|
+
positionGuide();
|
|
695
|
+
};
|
|
696
|
+
document.addEventListener("pointermove", onGuidePointerMove, true);
|
|
697
|
+
|
|
476
698
|
/* ---- hand the width over to the app's native drag handles ----
|
|
477
699
|
* Pressing a `[data-width-handle]` means the user is taking the
|
|
478
700
|
* width over: switch to the `custom` tier so healing stops
|
|
@@ -484,7 +706,9 @@ window.__ModuleLoader__.load({
|
|
|
484
706
|
e.target instanceof Element && e.target.closest("[data-width-handle]") !== null;
|
|
485
707
|
|
|
486
708
|
const onHandlePointerDown = (e) => {
|
|
487
|
-
if (!isHandleEvent(e)
|
|
709
|
+
if (!isHandleEvent(e)) return;
|
|
710
|
+
startLiveGuide();
|
|
711
|
+
if (currentTier() === "custom") return;
|
|
488
712
|
body().setAttribute("data-dsh-width", "custom");
|
|
489
713
|
try {
|
|
490
714
|
window.localStorage.setItem(KEY, "custom");
|
|
@@ -499,16 +723,19 @@ window.__ModuleLoader__.load({
|
|
|
499
723
|
const onHandlePointerUp = (e) => {
|
|
500
724
|
if (!isHandleEvent(e)) return;
|
|
501
725
|
const root = findWidthRoot();
|
|
502
|
-
if (root
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
726
|
+
if (root !== null) {
|
|
727
|
+
const px = Number.parseFloat(
|
|
728
|
+
widthTarget(root).style.getPropertyValue("--dsh-chat-user-width")
|
|
729
|
+
);
|
|
730
|
+
if (Number.isFinite(px) && px > 0) {
|
|
731
|
+
try {
|
|
732
|
+
window.localStorage.setItem(CUSTOM_KEY, String(Math.round(px)));
|
|
733
|
+
} catch (err) {
|
|
734
|
+
/* ignore */
|
|
735
|
+
}
|
|
736
|
+
}
|
|
511
737
|
}
|
|
738
|
+
if (guideActive) finishGuide();
|
|
512
739
|
syncButton();
|
|
513
740
|
syncMenu();
|
|
514
741
|
};
|
|
@@ -527,6 +754,9 @@ window.__ModuleLoader__.load({
|
|
|
527
754
|
const renderTexts = () => {
|
|
528
755
|
syncButton();
|
|
529
756
|
syncMenu();
|
|
757
|
+
if (guideActive && !guideDragging && guide.dataset.mode === "hint") {
|
|
758
|
+
setText(guideTextEl, t("guide.hint"));
|
|
759
|
+
}
|
|
530
760
|
};
|
|
531
761
|
const unsubscribeLocale = ctx.locale.subscribe(renderTexts);
|
|
532
762
|
|
|
@@ -551,6 +781,7 @@ window.__ModuleLoader__.load({
|
|
|
551
781
|
}
|
|
552
782
|
body().appendChild(btn);
|
|
553
783
|
body().appendChild(menu);
|
|
784
|
+
body().appendChild(guide);
|
|
554
785
|
syncButton();
|
|
555
786
|
const saved = getSavedTier();
|
|
556
787
|
if (saved !== "standard") applyTier(saved);
|
|
@@ -611,6 +842,9 @@ window.__ModuleLoader__.load({
|
|
|
611
842
|
}
|
|
612
843
|
|
|
613
844
|
function heal(fromObserver) {
|
|
845
|
+
/* The right handle only mounts in the active phase; keep the
|
|
846
|
+
* guide pinned to it once the chat tree settles. */
|
|
847
|
+
if (guideActive) positionGuide();
|
|
614
848
|
const tier = currentTier();
|
|
615
849
|
if (tier === "standard") return;
|
|
616
850
|
const prev = widthRoot;
|
|
@@ -665,6 +899,22 @@ window.__ModuleLoader__.load({
|
|
|
665
899
|
fallbackTimer = setInterval(() => heal(false), FALLBACK_HEAL_MS);
|
|
666
900
|
}
|
|
667
901
|
|
|
902
|
+
/* ---- keep the picker and the guide aligned with the live
|
|
903
|
+
* column. Standard/full re-resolve on resize, and the guide
|
|
904
|
+
* must stay beside the handle when the frame resizes. ---- */
|
|
905
|
+
let resizeFrame = null;
|
|
906
|
+
|
|
907
|
+
const onWindowResize = () => {
|
|
908
|
+
if (resizeFrame !== null) return;
|
|
909
|
+
scheduleFrame(() => {
|
|
910
|
+
resizeFrame = null;
|
|
911
|
+
syncButton();
|
|
912
|
+
syncMenu();
|
|
913
|
+
positionGuide();
|
|
914
|
+
});
|
|
915
|
+
};
|
|
916
|
+
window.addEventListener("resize", onWindowResize);
|
|
917
|
+
|
|
668
918
|
/* ---- disposer ---- */
|
|
669
919
|
return () => {
|
|
670
920
|
unsubscribeLocale();
|
|
@@ -672,14 +922,19 @@ window.__ModuleLoader__.load({
|
|
|
672
922
|
clearInterval(fallbackTimer);
|
|
673
923
|
clearTimeout(timeout);
|
|
674
924
|
clearTimeout(rootRetryTimer);
|
|
925
|
+
clearTimeout(guideHideTimer);
|
|
675
926
|
if (healFrame !== null) cancelFrame(healFrame);
|
|
927
|
+
if (resizeFrame !== null) cancelFrame(resizeFrame);
|
|
928
|
+
window.removeEventListener("resize", onWindowResize);
|
|
676
929
|
if (observer !== null) observer.disconnect();
|
|
677
930
|
document.removeEventListener("click", onDocClick);
|
|
678
931
|
document.removeEventListener("keydown", onDocKey);
|
|
679
932
|
document.removeEventListener("pointerdown", onHandlePointerDown, true);
|
|
680
933
|
document.removeEventListener("pointerup", onHandlePointerUp, true);
|
|
934
|
+
document.removeEventListener("pointermove", onGuidePointerMove, true);
|
|
681
935
|
if (btn.parentNode) btn.parentNode.removeChild(btn);
|
|
682
936
|
if (menu.parentNode) menu.parentNode.removeChild(menu);
|
|
937
|
+
if (guide.parentNode) guide.parentNode.removeChild(guide);
|
|
683
938
|
if (styleTag.parentNode) styleTag.parentNode.removeChild(styleTag);
|
|
684
939
|
/* Hand the width back to the app default; the app's own
|
|
685
940
|
* preference (which this plugin mirrors) stays so the last
|
|
@@ -689,6 +944,7 @@ window.__ModuleLoader__.load({
|
|
|
689
944
|
widthTarget(widthRoot).style.removeProperty("--dsh-chat-user-width");
|
|
690
945
|
}
|
|
691
946
|
body().removeAttribute("data-dsh-width");
|
|
947
|
+
body().removeAttribute("data-dsh-width-guide");
|
|
692
948
|
};
|
|
693
949
|
}, "dsh-width-tiers: ui");
|
|
694
950
|
}
|
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
|
|
3
|
+
"version": "1.0.8",
|
|
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.1 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": {
|