dsh-width-tiers 1.0.8 → 2.0.0
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 +87 -95
- package/README.zh.md +68 -70
- package/lib/client.js +1044 -858
- package/lib/index.js +15 -16
- package/package.json +25 -2
package/lib/client.js
CHANGED
|
@@ -4,141 +4,152 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
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:
|
|
24
|
-
*
|
|
25
|
-
* 标准 standard app default (no override)
|
|
26
|
-
* 中等 medium 1120px (sidebar untouched)
|
|
27
|
-
* 宽 wide 1280px (sidebar untouched)
|
|
28
|
-
* 超宽 ultra 1400px (sidebar untouched)
|
|
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). 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.
|
|
43
|
-
*
|
|
44
|
-
* - Panels are driven through the layout service (ctx.layout), never
|
|
45
|
-
* by simulating clicks.
|
|
46
|
-
* - The sidebar is only ever collapsed by the `full` tier, through the
|
|
47
|
-
* app's own toggle — never force-hidden, never locked. Going back to
|
|
48
|
-
* `standard` restores it only if this plugin collapsed it.
|
|
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.
|
|
52
|
-
* - The chosen tier persists in localStorage and is restored on the
|
|
53
|
-
* next visit.
|
|
54
|
-
* - All copy is localized through the app's locale service: tier
|
|
55
|
-
* labels and the button's aria/title follow Settings → Language
|
|
56
|
-
* (zh / en, browser-derived fallback) and re-render on switch.
|
|
57
|
-
*/
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
// src/client/constants.ts
|
|
10
|
+
/** LocalStorage key holding the selected tier. */
|
|
58
11
|
const KEY = "dsh.widthTier";
|
|
12
|
+
/** Legacy key from the pre-tier releases; removed on attach. */
|
|
59
13
|
const OLD_KEY = "dsh.wide";
|
|
60
14
|
/** Last user-dragged width (px), backing the `custom` tier. */
|
|
61
15
|
const CUSTOM_KEY = "dsh.widthTierCustom";
|
|
62
16
|
/** The app's own transcript width preference (ConversationWidthControls). */
|
|
63
17
|
const APP_PREF_KEY = "dsh.conversation.contentWidth";
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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;
|
|
75
|
-
const READY_TIMEOUT_MS = 30000;
|
|
76
|
-
/* Self-heal cadence: DOM-driven (MutationObserver scheduling on the
|
|
77
|
-
* next animation frame — before paint, so the width never flashes
|
|
78
|
-
* default) with a low-frequency fallback and backoff retries while
|
|
79
|
-
* the width root is missing (conversation-switch remount). */
|
|
80
|
-
const ROOT_RETRY_MIN_MS = 200;
|
|
81
|
-
const ROOT_RETRY_MAX_MS = 2000;
|
|
82
|
-
const FALLBACK_HEAL_MS = 2000;
|
|
83
|
-
|
|
84
|
-
/** Dictionary namespace owned by this plugin. */
|
|
85
|
-
const NS = "width-tiers";
|
|
86
|
-
|
|
87
|
-
//#region lib/types/client/locales.js
|
|
88
|
-
/** `width-tiers` namespace dictionaries (tier labels and picker copy). */
|
|
89
|
-
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
90
|
-
const zh = {
|
|
91
|
-
"tier.standard": "标准",
|
|
92
|
-
"tier.medium": "中等",
|
|
93
|
-
"tier.wide": "宽",
|
|
94
|
-
"tier.ultra": "超宽",
|
|
95
|
-
"tier.full": "全宽",
|
|
96
|
-
"tier.custom": "自定义",
|
|
97
|
-
"value.custom.unset": "未设置",
|
|
98
|
-
"guide.hint": "拖拽右侧边缘调整对话宽度",
|
|
99
|
-
"button.aria": "对话区宽度档位:{label}({value}),点击选择档位",
|
|
100
|
-
"button.title.summary": "对话区宽度档位:{label}({value})",
|
|
101
|
-
"button.title.hint": "点击选择:{list}"
|
|
18
|
+
/** Nominal content widths of the fixed tiers (px). */
|
|
19
|
+
const NOMINAL_WIDTH_PX = {
|
|
20
|
+
medium: 1120,
|
|
21
|
+
wide: 1280,
|
|
22
|
+
ultra: 1400
|
|
102
23
|
};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"button.aria": "Chat width tier: {label} ({value}). Click to choose a tier.",
|
|
114
|
-
"button.title.summary": "Chat width tier: {label} ({value})",
|
|
115
|
-
"button.title.hint": "Click to choose: {list}"
|
|
116
|
-
};
|
|
117
|
-
//#endregion
|
|
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). */
|
|
24
|
+
const DEFAULT_COLUMN_RATIO = .64;
|
|
25
|
+
/** How long attach waits before falling back to a slow retry. */
|
|
26
|
+
const READY_TIMEOUT_MS = 3e4;
|
|
27
|
+
const ROOT_RETRY_MAX_MS = 2e3;
|
|
28
|
+
const FALLBACK_HEAL_MS = 2e3;
|
|
29
|
+
/** Attribute the plugin sets on <body> while a tier is active. */
|
|
30
|
+
const TIER_ATTR = "data-dsh-width";
|
|
31
|
+
/** Attribute the plugin sets on <body> while the custom-drag guide is open. */
|
|
32
|
+
const GUIDE_ATTR = "data-dsh-width-guide";
|
|
33
|
+
// src/client/model.ts
|
|
122
34
|
const TIERS = [
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
35
|
+
{
|
|
36
|
+
id: "standard",
|
|
37
|
+
key: "tier.standard"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "medium",
|
|
41
|
+
key: "tier.medium"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "wide",
|
|
45
|
+
key: "tier.wide"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "ultra",
|
|
49
|
+
key: "tier.ultra"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "full",
|
|
53
|
+
key: "tier.full"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "custom",
|
|
57
|
+
key: "tier.custom"
|
|
58
|
+
}
|
|
131
59
|
];
|
|
132
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a tier definition by id, falling back to `standard`.
|
|
62
|
+
* @param id - stored or attributed tier id.
|
|
63
|
+
* @returns the matching definition, or `standard`.
|
|
64
|
+
*/
|
|
133
65
|
function tierOf(id) {
|
|
134
|
-
for (
|
|
135
|
-
if (TIERS[i].id === id) return TIERS[i];
|
|
136
|
-
}
|
|
66
|
+
for (const tier of TIERS) if (tier.id === id) return tier;
|
|
137
67
|
return TIERS[0];
|
|
138
68
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Read a positive persisted number, or null when absent or corrupt.
|
|
71
|
+
* @param storage - backing storage.
|
|
72
|
+
* @param key - storage key.
|
|
73
|
+
* @returns the parsed positive number, or null.
|
|
74
|
+
*/
|
|
75
|
+
function readStoredNumber(storage, key) {
|
|
76
|
+
try {
|
|
77
|
+
const value = Number(storage.getItem(key));
|
|
78
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read the saved tier, defaulting to `standard` when absent or unknown.
|
|
85
|
+
* @param storage - backing storage.
|
|
86
|
+
* @returns a valid tier id.
|
|
87
|
+
*/
|
|
88
|
+
function getSavedTier(storage) {
|
|
89
|
+
try {
|
|
90
|
+
const value = storage.getItem(KEY);
|
|
91
|
+
const found = TIERS.find((tier) => tier.id === value);
|
|
92
|
+
return found === void 0 ? "standard" : found.id;
|
|
93
|
+
} catch {
|
|
94
|
+
return "standard";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The tier's configured target, before the app's column clamp. This is the
|
|
99
|
+
* value persisted to the app preference: on a narrow column the app clamps it
|
|
100
|
+
* for display, and when the window widens it restores the configured target
|
|
101
|
+
* instead of staying pinned to the old clamp. `full` is inherently the live
|
|
102
|
+
* maximum; `standard` has no target and delegates to the app default.
|
|
103
|
+
* @param tier - selected tier.
|
|
104
|
+
* @param columnWidth - live width of the app's width column (px).
|
|
105
|
+
* @param customPx - recorded custom width, if any.
|
|
106
|
+
* @returns the target width in px, or null for the app default.
|
|
107
|
+
*/
|
|
108
|
+
function tierTargetPx(tier, columnWidth, customPx) {
|
|
109
|
+
switch (tier) {
|
|
110
|
+
case "medium":
|
|
111
|
+
case "wide":
|
|
112
|
+
case "ultra": return NOMINAL_WIDTH_PX[tier];
|
|
113
|
+
case "full": return fullWidthPx(columnWidth);
|
|
114
|
+
case "custom": return customPx;
|
|
115
|
+
default: return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The px actually applied to the column (same floor/max as the app's
|
|
120
|
+
* `resolveContentWidth`).
|
|
121
|
+
* @param tier - selected tier.
|
|
122
|
+
* @param columnWidth - live width of the app's width column (px).
|
|
123
|
+
* @param customPx - recorded custom width, if any.
|
|
124
|
+
* @returns the clamped width in px, or null for the app default.
|
|
125
|
+
*/
|
|
126
|
+
function appliedTierPx(tier, columnWidth, customPx) {
|
|
127
|
+
const target = tierTargetPx(tier, columnWidth, customPx);
|
|
128
|
+
if (target === null) return null;
|
|
129
|
+
const cap = fullWidthPx(columnWidth);
|
|
130
|
+
return Math.min(Math.max(target, 640), cap);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the app's default transcript width for a column.
|
|
134
|
+
* @param columnWidth - live width of the app's width column (px).
|
|
135
|
+
* @returns `max(680, min(column × 0.64, 920))`.
|
|
136
|
+
*/
|
|
137
|
+
function defaultWidthPx(columnWidth) {
|
|
138
|
+
return Math.max(680, Math.min(columnWidth * DEFAULT_COLUMN_RATIO, 920));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the widest transcript the column can hold (app clamp maximum).
|
|
142
|
+
* @param columnWidth - live width of the app's width column (px).
|
|
143
|
+
* @returns `max(640, column − 176)`.
|
|
144
|
+
*/
|
|
145
|
+
function fullWidthPx(columnWidth) {
|
|
146
|
+
return Math.max(640, columnWidth - 176);
|
|
147
|
+
}
|
|
148
|
+
// src/client/styles.ts
|
|
149
|
+
/**
|
|
150
|
+
* Button + menu styles. Theme variables keep the picker in the app's light /
|
|
151
|
+
* dark palettes; every selector is scoped to this plugin's own ids and classes.
|
|
152
|
+
*/
|
|
142
153
|
const css = [
|
|
143
154
|
"#dsh-wide-toggle{position:fixed;right:10px;bottom:216px;z-index:999;display:flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid var(--dsw-alias-border-l2);border-radius:50%;overflow:hidden;background:var(--dsw-alias-button-floating-fill);color:var(--dsw-alias-label-secondary);box-shadow:0 2px 8px rgba(0,0,0,.18);cursor:pointer;opacity:.72;transition:opacity .2s ease,width .2s ease,border-radius .2s ease,background .2s ease,color .2s ease}",
|
|
144
155
|
"#dsh-wide-toggle::after{content:'';position:fixed;right:10px;bottom:216px;width:36px;height:36px;pointer-events:auto}",
|
|
@@ -148,11 +159,13 @@ window.__ModuleLoader__.load({
|
|
|
148
159
|
"#dsh-wide-toggle:not(:hover):not(:focus-visible) svg rect{width:6px;x:5px}",
|
|
149
160
|
"#dsh-wide-toggle.active{background:var(--dsw-alias-state-business-primary);border-color:transparent;color:var(--dsw-static-neutral-00);opacity:1}",
|
|
150
161
|
"#dsh-wide-toggle.pulse{animation:dsh-wide-pulse .65s ease-out}",
|
|
162
|
+
"@media (prefers-reduced-motion:reduce){#dsh-wide-toggle{transition:none}#dsh-wide-toggle.pulse{animation:none}}",
|
|
151
163
|
"@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
164
|
"#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
165
|
"#dsh-width-menu[hidden]{display:none}",
|
|
154
166
|
"#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
167
|
"#dsh-width-menu button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
168
|
+
"#dsh-width-menu button:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}",
|
|
156
169
|
"#dsh-width-menu button.selected{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-state-business-primary)}",
|
|
157
170
|
"#dsh-width-menu .dshwm-value{color:var(--dsw-alias-label-tertiary);font-size:12px}",
|
|
158
171
|
"#dsh-width-menu button.selected .dshwm-value{color:var(--dsw-alias-state-business-primary)}",
|
|
@@ -160,796 +173,969 @@ window.__ModuleLoader__.load({
|
|
|
160
173
|
"#dsh-width-guide[hidden]{display:none}",
|
|
161
174
|
"#dsh-width-guide svg{flex:none;color:var(--dsw-alias-state-business-primary)}",
|
|
162
175
|
"#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
|
|
176
|
+
"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}",
|
|
177
|
+
"body[data-dsh-width-guide] [data-width-handle]::after{opacity:1}"
|
|
165
178
|
].join("");
|
|
166
|
-
//#endregion
|
|
167
|
-
|
|
168
|
-
/** Services required by the browser bundle. */
|
|
169
|
-
const inject = ["layout", "locale"];
|
|
170
|
-
|
|
171
179
|
/**
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
* lives in its own seat: closeRightbar() only retracts the
|
|
220
|
-
* grid TRACK (rightbarTrack=false) while the seat stays
|
|
221
|
-
* expanded, so the panel keeps rendering as an OVERLAY pinned
|
|
222
|
-
* to the frame's right edge — hanging over and covering the
|
|
223
|
-
* conversation (对话窗口被边侧栏覆盖). The plugin must never
|
|
224
|
-
* touch the rightbar; the app's own width axis already yields
|
|
225
|
-
* space to the panel. */
|
|
226
|
-
|
|
227
|
-
function collapseSidebar() {
|
|
228
|
-
if (!isSidebarCollapsed()) layout.toggleSidebar();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function expandSidebar() {
|
|
232
|
-
if (isSidebarCollapsed()) layout.toggleSidebar();
|
|
180
|
+
* Install the plugin stylesheet and return its removal disposer.
|
|
181
|
+
* @returns removes the owned style element from the document head.
|
|
182
|
+
*/
|
|
183
|
+
function installStyles() {
|
|
184
|
+
const styleTag = document.createElement("style");
|
|
185
|
+
styleTag.dataset.plugin = "dsh-width-tiers";
|
|
186
|
+
styleTag.textContent = css;
|
|
187
|
+
document.head.appendChild(styleTag);
|
|
188
|
+
return () => {
|
|
189
|
+
if (styleTag.parentNode) styleTag.parentNode.removeChild(styleTag);
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// src/client/width-root.ts
|
|
193
|
+
/** Whether an element resolves a non-empty `--dsh-chat-content-width`. */
|
|
194
|
+
function hasWidthVar(el) {
|
|
195
|
+
return getComputedStyle(el).getPropertyValue("--dsh-chat-content-width").trim() !== "";
|
|
196
|
+
}
|
|
197
|
+
function isSidebarConversation(el) {
|
|
198
|
+
return typeof el.closest === "function" && el.closest("[data-sidebar-chat]") !== null;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The element the app's ConversationWidthControls publishes
|
|
202
|
+
* `--dsh-conversation-column-width` / `--dsh-chat-user-width` on.
|
|
203
|
+
* @param root - element that declares `--dsh-chat-content-width`.
|
|
204
|
+
* @returns the parent element, or the root itself when detached.
|
|
205
|
+
*/
|
|
206
|
+
function widthTarget(root) {
|
|
207
|
+
return root.parentElement ?? root;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Create the runtime locator. Cached; the app remounts the chat tree on
|
|
211
|
+
* conversation switch, so re-location first walks the recorded child-index
|
|
212
|
+
* path from <body> (React keeps the tree shape — O(depth), one style read)
|
|
213
|
+
* and only falls back to a full scan.
|
|
214
|
+
* @returns locator bound to this page's document.
|
|
215
|
+
*/
|
|
216
|
+
function createWidthRootLocator() {
|
|
217
|
+
let widthRoot = null;
|
|
218
|
+
let widthPath = null;
|
|
219
|
+
function recordWidthPath(el) {
|
|
220
|
+
const path = [];
|
|
221
|
+
let node = el;
|
|
222
|
+
while (node && node !== document.body) {
|
|
223
|
+
const parent = node.parentNode;
|
|
224
|
+
if (!parent || !parent.children) break;
|
|
225
|
+
path.unshift(Array.prototype.indexOf.call(parent.children, node));
|
|
226
|
+
node = parent;
|
|
233
227
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
let widthRoot = null;
|
|
242
|
-
let widthPath = null;
|
|
243
|
-
|
|
244
|
-
function hasWidthVar(el) {
|
|
245
|
-
return getComputedStyle(el)
|
|
246
|
-
.getPropertyValue("--dsh-chat-content-width")
|
|
247
|
-
.trim() !== "";
|
|
228
|
+
widthPath = path.length > 0 ? path : null;
|
|
229
|
+
}
|
|
230
|
+
function firstWidthRoot(list) {
|
|
231
|
+
for (let i = 0; i < list.length; i++) {
|
|
232
|
+
const el = list[i];
|
|
233
|
+
if (el === void 0 || isSidebarConversation(el)) continue;
|
|
234
|
+
if (hasWidthVar(el)) return el;
|
|
248
235
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
widthPath = path.length > 0 ? path : null;
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
function scanWidthRoot() {
|
|
239
|
+
let found = firstWidthRoot(document.querySelectorAll("[data-conversation-content]"));
|
|
240
|
+
if (found === null) found = firstWidthRoot(document.querySelectorAll("[class]"));
|
|
241
|
+
if (found !== null) {
|
|
242
|
+
widthRoot = found;
|
|
243
|
+
recordWidthPath(widthRoot);
|
|
244
|
+
return widthRoot;
|
|
260
245
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
246
|
+
widthRoot = null;
|
|
247
|
+
widthPath = null;
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
function find(allowScan = true) {
|
|
251
|
+
if (widthRoot !== null && document.contains(widthRoot) && !isSidebarConversation(widthRoot)) return widthRoot;
|
|
252
|
+
if (widthPath !== null) {
|
|
253
|
+
let el = document.body;
|
|
254
|
+
for (let i = 0; i < widthPath.length; i++) {
|
|
255
|
+
if (el === null) break;
|
|
256
|
+
const next = el.children[widthPath[i]];
|
|
257
|
+
if (!next) {
|
|
258
|
+
el = null;
|
|
259
|
+
break;
|
|
269
260
|
}
|
|
261
|
+
el = next;
|
|
270
262
|
}
|
|
271
|
-
widthRoot
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function findWidthRoot(allowScan) {
|
|
277
|
-
if (widthRoot !== null && document.contains(widthRoot)) return widthRoot;
|
|
278
|
-
if (widthPath !== null) {
|
|
279
|
-
let el = document.body;
|
|
280
|
-
for (let i = 0; i < widthPath.length; i++) {
|
|
281
|
-
el = el.children[widthPath[i]];
|
|
282
|
-
if (!el) break;
|
|
283
|
-
}
|
|
284
|
-
if (el && el !== widthRoot && document.contains(el) && hasWidthVar(el)) {
|
|
285
|
-
widthRoot = el;
|
|
286
|
-
return widthRoot;
|
|
287
|
-
}
|
|
263
|
+
if (el !== null && el !== widthRoot && document.contains(el) && !isSidebarConversation(el) && hasWidthVar(el)) {
|
|
264
|
+
widthRoot = el;
|
|
265
|
+
return widthRoot;
|
|
288
266
|
}
|
|
289
|
-
if (allowScan === false) return null;
|
|
290
|
-
return scanWidthRoot();
|
|
291
267
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return (root && root.parentElement) || root;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function readStoredNumber(key) {
|
|
305
|
-
try {
|
|
306
|
-
const v = Number(window.localStorage.getItem(key));
|
|
307
|
-
return Number.isFinite(v) && v > 0 ? v : null;
|
|
308
|
-
} catch (e) {
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
268
|
+
if (!allowScan) return null;
|
|
269
|
+
return scanWidthRoot();
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
find,
|
|
273
|
+
cached: () => widthRoot,
|
|
274
|
+
reset: () => {
|
|
275
|
+
widthRoot = null;
|
|
276
|
+
widthPath = null;
|
|
311
277
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function liveWidthPx(root) {
|
|
281
|
+
const target = widthTarget(root);
|
|
282
|
+
const inline = Number.parseFloat(target.style.getPropertyValue("--dsh-chat-user-width"));
|
|
283
|
+
if (Number.isFinite(inline) && inline > 0) return Math.round(inline);
|
|
284
|
+
const raw = getComputedStyle(root).getPropertyValue("--dsh-chat-content-width");
|
|
285
|
+
const computed = Number.parseFloat(raw);
|
|
286
|
+
if (Number.isFinite(computed) && computed > 0) return Math.round(computed);
|
|
287
|
+
return Math.round(defaultWidthPx(root.offsetWidth));
|
|
288
|
+
}
|
|
289
|
+
// src/client/controller.ts
|
|
290
|
+
/** Estimated bubble width used when placing the guide beside a drag handle. */
|
|
291
|
+
const GUIDE_WIDTH_PX = 180;
|
|
292
|
+
/** How long the settled width stays visible after a drag. */
|
|
293
|
+
const GUIDE_HIDE_MS = 800;
|
|
294
|
+
/** Default guide placement (frame edge, vertical center). */
|
|
295
|
+
const GUIDE_FALLBACK = {
|
|
296
|
+
left: "auto",
|
|
297
|
+
right: "24px",
|
|
298
|
+
top: "50%"
|
|
299
|
+
};
|
|
300
|
+
/**
|
|
301
|
+
* Own the width-tier model: the app's native width axis, the tier/guide state
|
|
302
|
+
* machine and persistence. Rendering lives in the React picker, which reads
|
|
303
|
+
* `source` and dispatches `actions`; this controller never touches the picker
|
|
304
|
+
* DOM, so the UI can remount without rebuilding observers.
|
|
305
|
+
* @param options - layout service, locale subscription and translate function.
|
|
306
|
+
* @returns observable state, actions and the disposer.
|
|
307
|
+
*/
|
|
308
|
+
function createWidthTiers(options) {
|
|
309
|
+
const { layout, t } = options;
|
|
310
|
+
const roots = createWidthRootLocator();
|
|
311
|
+
const removeStyles = installStyles();
|
|
312
|
+
const body = () => document.body;
|
|
313
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
314
|
+
let snapshot;
|
|
315
|
+
let menuOpen = false;
|
|
316
|
+
let pulseSeq = 0;
|
|
317
|
+
let guideActive = false;
|
|
318
|
+
let guideDragging = false;
|
|
319
|
+
let guideFromMenu = false;
|
|
320
|
+
let guideMode = "hint";
|
|
321
|
+
let guideText = "";
|
|
322
|
+
let guideStyle = GUIDE_FALLBACK;
|
|
323
|
+
let guideHandle = null;
|
|
324
|
+
let guideHideTimer = null;
|
|
325
|
+
let guideWidth = GUIDE_WIDTH_PX;
|
|
326
|
+
let tierBeforeGuide = "standard";
|
|
327
|
+
let preferenceOwned = false;
|
|
328
|
+
let preferenceWritten = null;
|
|
329
|
+
let collapsedByTier = false;
|
|
330
|
+
function currentTier() {
|
|
331
|
+
const value = body().getAttribute(TIER_ATTR);
|
|
332
|
+
const found = value === null ? void 0 : TIERS.find((tier) => tier.id === value);
|
|
333
|
+
return found === void 0 ? "standard" : found.id;
|
|
334
|
+
}
|
|
335
|
+
function getSavedTier$1() {
|
|
336
|
+
try {
|
|
337
|
+
return getSavedTier(window.localStorage);
|
|
338
|
+
} catch {
|
|
339
|
+
return "standard";
|
|
330
340
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
));
|
|
341
|
+
}
|
|
342
|
+
function readNumber(key) {
|
|
343
|
+
try {
|
|
344
|
+
return readStoredNumber(window.localStorage, key);
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
349
347
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
/* Mirror into the app's preference so window resizes and
|
|
371
|
-
* conversation remounts re-clamp through the app's own
|
|
372
|
-
* publish cycle instead of relying on plugin healing. */
|
|
373
|
-
const next = String(Math.round(px));
|
|
374
|
-
if (target.style.getPropertyValue("--dsh-chat-user-width") !== next + "px") {
|
|
375
|
-
target.style.setProperty("--dsh-chat-user-width", next + "px");
|
|
376
|
-
}
|
|
377
|
-
try {
|
|
378
|
-
if (window.localStorage.getItem(APP_PREF_KEY) !== next) {
|
|
379
|
-
window.localStorage.setItem(APP_PREF_KEY, next);
|
|
380
|
-
}
|
|
381
|
-
} catch (e) {
|
|
382
|
-
/* ignore */
|
|
348
|
+
}
|
|
349
|
+
function isSidebarCollapsed() {
|
|
350
|
+
return document.querySelector("[data-sidebar-collapsed]") !== null;
|
|
351
|
+
}
|
|
352
|
+
function collapseSidebar() {
|
|
353
|
+
if (!isSidebarCollapsed()) layout.toggleSidebar();
|
|
354
|
+
}
|
|
355
|
+
function expandSidebar() {
|
|
356
|
+
if (isSidebarCollapsed()) layout.toggleSidebar();
|
|
357
|
+
}
|
|
358
|
+
function tierValueText(tierId) {
|
|
359
|
+
switch (tierId) {
|
|
360
|
+
case "standard": {
|
|
361
|
+
const root = roots.find();
|
|
362
|
+
if (root === null) return "—";
|
|
363
|
+
return `${Math.round(defaultWidthPx(root.offsetWidth))}px`;
|
|
383
364
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
window.localStorage.setItem(KEY, tier);
|
|
393
|
-
} catch (e) {
|
|
394
|
-
/* storage unavailable — state still applies this session */
|
|
365
|
+
case "medium":
|
|
366
|
+
case "wide":
|
|
367
|
+
case "ultra": return `${NOMINAL_WIDTH_PX[tierId]}px`;
|
|
368
|
+
case "full": {
|
|
369
|
+
const root = roots.find();
|
|
370
|
+
if (root === null) return "—";
|
|
371
|
+
return `${Math.round(fullWidthPx(root.offsetWidth))}px`;
|
|
395
372
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
collapseSidebar();
|
|
400
|
-
collapsedByTier = true;
|
|
401
|
-
} else {
|
|
402
|
-
collapsedByTier = false;
|
|
403
|
-
}
|
|
404
|
-
} else if (tier === "standard") {
|
|
405
|
-
if (collapsedByTier && isSidebarCollapsed()) expandSidebar();
|
|
406
|
-
collapsedByTier = false;
|
|
407
|
-
} else {
|
|
408
|
-
collapsedByTier = false;
|
|
373
|
+
case "custom": {
|
|
374
|
+
const dragged = readNumber(CUSTOM_KEY);
|
|
375
|
+
return dragged === null ? t("value.custom.unset") : `${Math.round(dragged)}px`;
|
|
409
376
|
}
|
|
410
|
-
|
|
411
|
-
syncMenu();
|
|
412
|
-
pulse();
|
|
377
|
+
default: return "—";
|
|
413
378
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
379
|
+
}
|
|
380
|
+
function guideValueText() {
|
|
381
|
+
const root = roots.find();
|
|
382
|
+
const px = root === null ? null : liveWidthPx(root);
|
|
383
|
+
return px === null ? "—" : `${px}px`;
|
|
384
|
+
}
|
|
385
|
+
function buildSnapshot() {
|
|
386
|
+
const tier = currentTier();
|
|
387
|
+
const def = tierOf(tier);
|
|
388
|
+
const label = t(def.key);
|
|
389
|
+
const value = tierValueText(tier);
|
|
390
|
+
const summary = t("button.title.summary", {
|
|
391
|
+
label,
|
|
392
|
+
value
|
|
393
|
+
});
|
|
394
|
+
const hint = t("button.title.hint", { list: TIERS.map((tierDef) => `${t(tierDef.key)} ${tierValueText(tierDef.id)}`).join(" / ") });
|
|
395
|
+
return {
|
|
396
|
+
tier,
|
|
397
|
+
menuOpen,
|
|
398
|
+
pulse: pulseSeq,
|
|
399
|
+
button: {
|
|
400
|
+
active: tier !== "standard",
|
|
401
|
+
label,
|
|
402
|
+
value,
|
|
403
|
+
ariaLabel: t("button.aria", {
|
|
404
|
+
label,
|
|
405
|
+
value
|
|
406
|
+
}),
|
|
407
|
+
title: `${summary}\n${hint}`
|
|
408
|
+
},
|
|
409
|
+
items: TIERS.map((tierDef) => ({
|
|
410
|
+
id: tierDef.id,
|
|
411
|
+
label: t(tierDef.key),
|
|
412
|
+
value: tierValueText(tierDef.id),
|
|
413
|
+
selected: tierDef.id === tier
|
|
414
|
+
})),
|
|
415
|
+
guide: {
|
|
416
|
+
active: guideActive,
|
|
417
|
+
dragging: guideDragging,
|
|
418
|
+
mode: guideMode,
|
|
419
|
+
text: guideText,
|
|
420
|
+
style: guideStyle
|
|
426
421
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function emit() {
|
|
425
|
+
snapshot = buildSnapshot();
|
|
426
|
+
for (const listener of listeners) listener();
|
|
427
|
+
}
|
|
428
|
+
function applyContentWidth(tier) {
|
|
429
|
+
const root = roots.find();
|
|
430
|
+
if (root === null) return;
|
|
431
|
+
const target = widthTarget(root);
|
|
432
|
+
if (tier === "custom" && readNumber("dsh.widthTierCustom") === null) return;
|
|
433
|
+
const targetPx = tierTargetPx(tier, root.offsetWidth, readNumber(CUSTOM_KEY));
|
|
434
|
+
if (targetPx === null) {
|
|
435
|
+
if (target.style.getPropertyValue("--dsh-chat-user-width") !== "" || preferenceOwned) {
|
|
436
|
+
target.style.removeProperty("--dsh-chat-user-width");
|
|
437
|
+
try {
|
|
438
|
+
window.localStorage.removeItem(APP_PREF_KEY);
|
|
439
|
+
} catch {}
|
|
440
|
+
preferenceOwned = false;
|
|
441
|
+
preferenceWritten = null;
|
|
432
442
|
}
|
|
433
|
-
return
|
|
443
|
+
return;
|
|
434
444
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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 "—";
|
|
445
|
+
const laidOut = root.offsetWidth > 0;
|
|
446
|
+
if (!laidOut && tier === "full") return;
|
|
447
|
+
if (laidOut) {
|
|
448
|
+
const px = appliedTierPx(tier, root.offsetWidth, readNumber(CUSTOM_KEY));
|
|
449
|
+
if (px !== null) {
|
|
450
|
+
const next = String(Math.round(px));
|
|
451
|
+
if (target.style.getPropertyValue("--dsh-chat-user-width") !== next + "px") target.style.setProperty("--dsh-chat-user-width", next + "px");
|
|
467
452
|
}
|
|
468
453
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const summary = t("button.title.summary", { label, value });
|
|
476
|
-
const hint = t("button.title.hint", {
|
|
477
|
-
list: TIERS.map((tierDef) => `${t(tierDef.key)} ${tierValueText(tierDef.id)}`).join(" / ")
|
|
478
|
-
});
|
|
479
|
-
btn.innerHTML = barIcon(tier);
|
|
480
|
-
btn.classList.toggle("active", tier !== "standard");
|
|
481
|
-
btn.setAttribute("aria-label", t("button.aria", { label, value }));
|
|
482
|
-
btn.title = `${summary}\n${hint}`;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/* ---- tier menu ---- */
|
|
486
|
-
const menu = document.createElement("div");
|
|
487
|
-
menu.id = "dsh-width-menu";
|
|
488
|
-
menu.setAttribute("role", "menu");
|
|
489
|
-
menu.hidden = true;
|
|
490
|
-
|
|
491
|
-
for (const def of TIERS) {
|
|
492
|
-
const item = document.createElement("button");
|
|
493
|
-
item.type = "button";
|
|
494
|
-
item.setAttribute("role", "menuitemradio");
|
|
495
|
-
item.setAttribute("data-tier", def.id);
|
|
496
|
-
item.innerHTML = `<span class="dshwm-label">${t(def.key)}</span><span class="dshwm-value">${tierValueText(def.id)}</span>`;
|
|
497
|
-
item.addEventListener("click", (e) => {
|
|
498
|
-
if (e && e.stopPropagation) e.stopPropagation();
|
|
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);
|
|
507
|
-
applyTier(def.id);
|
|
508
|
-
hideMenu();
|
|
509
|
-
});
|
|
510
|
-
menu.appendChild(item);
|
|
511
|
-
}
|
|
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
|
-
|
|
520
|
-
function syncMenu() {
|
|
521
|
-
const tier = currentTier();
|
|
522
|
-
const items = menu.querySelectorAll("button[data-tier]");
|
|
523
|
-
for (let i = 0; i < items.length; i++) {
|
|
524
|
-
const id = items[i].getAttribute("data-tier");
|
|
525
|
-
const on = id === tier;
|
|
526
|
-
items[i].classList.toggle("selected", on);
|
|
527
|
-
items[i].setAttribute("aria-checked", String(on));
|
|
528
|
-
const labelEl = items[i].querySelector(".dshwm-label");
|
|
529
|
-
if (labelEl) setText(labelEl, t(tierOf(id).key));
|
|
530
|
-
const valueEl = items[i].querySelector(".dshwm-value");
|
|
531
|
-
if (valueEl) setText(valueEl, tierValueText(id));
|
|
454
|
+
const preference = String(Math.round(targetPx));
|
|
455
|
+
try {
|
|
456
|
+
if (window.localStorage.getItem("dsh.conversation.contentWidth") !== preference) {
|
|
457
|
+
window.localStorage.setItem(APP_PREF_KEY, preference);
|
|
458
|
+
preferenceOwned = true;
|
|
459
|
+
preferenceWritten = preference;
|
|
532
460
|
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
btn.addEventListener("click", (e) => {
|
|
547
|
-
if (e && e.stopPropagation) e.stopPropagation();
|
|
548
|
-
if (menu.hidden) showMenu();
|
|
549
|
-
else hideMenu();
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
const onDocClick = (e) => {
|
|
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);
|
|
461
|
+
} catch {}
|
|
462
|
+
}
|
|
463
|
+
function applyTier(tier, persist = true) {
|
|
464
|
+
body().setAttribute(TIER_ATTR, tier);
|
|
465
|
+
if (persist) try {
|
|
466
|
+
window.localStorage.setItem(KEY, tier);
|
|
467
|
+
} catch {}
|
|
468
|
+
applyContentWidth(tier);
|
|
469
|
+
if (tier === "full") {
|
|
470
|
+
if (!isSidebarCollapsed()) {
|
|
471
|
+
collapseSidebar();
|
|
472
|
+
collapsedByTier = true;
|
|
558
473
|
}
|
|
474
|
+
} else if (tier === "standard") {
|
|
475
|
+
if (collapsedByTier && isSidebarCollapsed()) expandSidebar();
|
|
476
|
+
collapsedByTier = false;
|
|
477
|
+
} else if (!isSidebarCollapsed()) collapsedByTier = false;
|
|
478
|
+
pulseSeq += 1;
|
|
479
|
+
emit();
|
|
480
|
+
}
|
|
481
|
+
function computeGuideStyle() {
|
|
482
|
+
const handle = guideHandle !== null && document.contains(guideHandle) ? guideHandle : document.querySelector("[data-width-handle=\"right\"]");
|
|
483
|
+
const viewport = window.innerWidth || 0;
|
|
484
|
+
if (handle === null || typeof handle.getBoundingClientRect !== "function" || viewport === 0) return GUIDE_FALLBACK;
|
|
485
|
+
const rect = handle.getBoundingClientRect();
|
|
486
|
+
const gap = 10;
|
|
487
|
+
const top = `${rect.top + rect.height / 2}px`;
|
|
488
|
+
if (viewport - rect.right >= guideWidth + gap) return {
|
|
489
|
+
left: `${rect.right + gap}px`,
|
|
490
|
+
right: "auto",
|
|
491
|
+
top
|
|
559
492
|
};
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
493
|
+
return {
|
|
494
|
+
left: `${Math.max(8, rect.left - gap - guideWidth)}px`,
|
|
495
|
+
right: "auto",
|
|
496
|
+
top
|
|
564
497
|
};
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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);
|
|
498
|
+
}
|
|
499
|
+
function updateGuideStyle() {
|
|
500
|
+
if (!guideActive) return;
|
|
501
|
+
const next = computeGuideStyle();
|
|
502
|
+
if (next.left === guideStyle.left && next.right === guideStyle.right && next.top === guideStyle.top) return;
|
|
503
|
+
guideStyle = next;
|
|
504
|
+
emit();
|
|
505
|
+
}
|
|
506
|
+
function startGuide() {
|
|
507
|
+
guideFromMenu = true;
|
|
508
|
+
guideActive = true;
|
|
509
|
+
guideDragging = false;
|
|
510
|
+
guideHandle = null;
|
|
511
|
+
body().setAttribute(GUIDE_ATTR, "custom");
|
|
512
|
+
guideMode = "hint";
|
|
513
|
+
guideText = t("guide.hint");
|
|
514
|
+
guideStyle = computeGuideStyle();
|
|
515
|
+
emit();
|
|
516
|
+
}
|
|
517
|
+
function startLiveGuide(handle) {
|
|
518
|
+
if (guideHideTimer !== null) {
|
|
519
|
+
window.clearTimeout(guideHideTimer);
|
|
520
|
+
guideHideTimer = null;
|
|
630
521
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
522
|
+
guideActive = true;
|
|
523
|
+
guideDragging = true;
|
|
524
|
+
guideHandle = handle ?? null;
|
|
525
|
+
body().setAttribute(GUIDE_ATTR, "custom");
|
|
526
|
+
guideMode = "live";
|
|
527
|
+
guideText = guideValueText();
|
|
528
|
+
guideStyle = computeGuideStyle();
|
|
529
|
+
emit();
|
|
530
|
+
}
|
|
531
|
+
function beginCustomGuide() {
|
|
532
|
+
if (!guideActive) {
|
|
533
|
+
tierBeforeGuide = currentTier();
|
|
534
|
+
applyTier("custom");
|
|
640
535
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
536
|
+
startGuide();
|
|
537
|
+
}
|
|
538
|
+
function finishGuide() {
|
|
539
|
+
if (!guideActive) return;
|
|
540
|
+
guideDragging = false;
|
|
541
|
+
guideMode = "live";
|
|
542
|
+
guideText = guideValueText();
|
|
543
|
+
if (guideHideTimer !== null) window.clearTimeout(guideHideTimer);
|
|
544
|
+
guideHideTimer = window.setTimeout(() => endGuide(false), GUIDE_HIDE_MS);
|
|
545
|
+
emit();
|
|
546
|
+
}
|
|
547
|
+
function endGuide(restoreTier) {
|
|
548
|
+
if (!guideActive) return;
|
|
549
|
+
guideActive = false;
|
|
550
|
+
guideDragging = false;
|
|
551
|
+
if (guideHideTimer !== null) {
|
|
646
552
|
window.clearTimeout(guideHideTimer);
|
|
647
|
-
|
|
648
|
-
guideDragging = true;
|
|
649
|
-
guide.hidden = false;
|
|
650
|
-
body().setAttribute("data-dsh-width-guide", "custom");
|
|
651
|
-
setGuideText("live", guideValueText());
|
|
652
|
-
positionGuide();
|
|
553
|
+
guideHideTimer = null;
|
|
653
554
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
555
|
+
guideMode = "hint";
|
|
556
|
+
guideHandle = null;
|
|
557
|
+
guideWidth = GUIDE_WIDTH_PX;
|
|
558
|
+
guideStyle = GUIDE_FALLBACK;
|
|
559
|
+
body().removeAttribute(GUIDE_ATTR);
|
|
560
|
+
const restore = restoreTier && guideFromMenu && readNumber("dsh.widthTierCustom") === null;
|
|
561
|
+
guideFromMenu = false;
|
|
562
|
+
if (restore) applyTier(tierBeforeGuide);
|
|
563
|
+
else emit();
|
|
564
|
+
}
|
|
565
|
+
const onGuidePointerMove = () => {
|
|
566
|
+
if (!guideActive || !guideDragging) return;
|
|
567
|
+
guideText = guideValueText();
|
|
568
|
+
guideStyle = computeGuideStyle();
|
|
569
|
+
emit();
|
|
570
|
+
};
|
|
571
|
+
document.addEventListener("pointermove", onGuidePointerMove, true);
|
|
572
|
+
const isHandleEvent = (e) => e.target instanceof Element && e.target.closest("[data-width-handle]") !== null;
|
|
573
|
+
const onHandlePointerDown = (e) => {
|
|
574
|
+
if (!isHandleEvent(e)) return;
|
|
575
|
+
const target = e.target;
|
|
576
|
+
startLiveGuide(target.closest("[data-width-handle]"));
|
|
577
|
+
if (currentTier() === "custom") return;
|
|
578
|
+
body().setAttribute(TIER_ATTR, "custom");
|
|
579
|
+
try {
|
|
580
|
+
window.localStorage.setItem(KEY, "custom");
|
|
581
|
+
} catch {}
|
|
582
|
+
emit();
|
|
583
|
+
};
|
|
584
|
+
const onHandlePointerUp = (e) => {
|
|
585
|
+
if (!isHandleEvent(e)) return;
|
|
586
|
+
const root = roots.find();
|
|
587
|
+
if (root !== null) {
|
|
588
|
+
const px = Number.parseFloat(widthTarget(root).style.getPropertyValue("--dsh-chat-user-width"));
|
|
589
|
+
if (Number.isFinite(px) && px > 0) try {
|
|
590
|
+
window.localStorage.setItem(CUSTOM_KEY, String(Math.round(px)));
|
|
591
|
+
} catch {}
|
|
664
592
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
593
|
+
if (guideActive) finishGuide();
|
|
594
|
+
else emit();
|
|
595
|
+
};
|
|
596
|
+
const onHandlePointerCancel = () => {
|
|
597
|
+
if (!guideActive || !guideDragging) return;
|
|
598
|
+
endGuide(false);
|
|
599
|
+
};
|
|
600
|
+
document.addEventListener("pointerdown", onHandlePointerDown, true);
|
|
601
|
+
document.addEventListener("pointerup", onHandlePointerUp, true);
|
|
602
|
+
document.addEventListener("pointercancel", onHandlePointerCancel, true);
|
|
603
|
+
const actions = {
|
|
604
|
+
toggleMenu() {
|
|
605
|
+
menuOpen = !menuOpen;
|
|
606
|
+
emit();
|
|
607
|
+
},
|
|
608
|
+
closeMenu() {
|
|
609
|
+
if (!menuOpen) return;
|
|
610
|
+
menuOpen = false;
|
|
611
|
+
emit();
|
|
612
|
+
},
|
|
613
|
+
selectTier(tier) {
|
|
614
|
+
menuOpen = false;
|
|
615
|
+
if (tier === "custom" && readNumber("dsh.widthTierCustom") === null) {
|
|
616
|
+
beginCustomGuide();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
endGuide(false);
|
|
620
|
+
applyTier(tier);
|
|
621
|
+
},
|
|
622
|
+
cancelGuide() {
|
|
623
|
+
endGuide(true);
|
|
624
|
+
},
|
|
625
|
+
measureGuide(width) {
|
|
626
|
+
const next = Math.round(width);
|
|
627
|
+
if (next <= 0 || next === guideWidth) return;
|
|
628
|
+
guideWidth = next;
|
|
668
629
|
if (!guideActive) return;
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
window.clearTimeout(guideHideTimer);
|
|
672
|
-
guideHideTimer = window.setTimeout(() => endGuide(false), 800);
|
|
630
|
+
guideStyle = computeGuideStyle();
|
|
631
|
+
emit();
|
|
673
632
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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);
|
|
633
|
+
};
|
|
634
|
+
let attached = false;
|
|
635
|
+
let readyTimer = null;
|
|
636
|
+
let slowTimer = null;
|
|
637
|
+
let readyTimeout = null;
|
|
638
|
+
function stopWaiting() {
|
|
639
|
+
if (readyTimer !== null) {
|
|
640
|
+
clearInterval(readyTimer);
|
|
641
|
+
readyTimer = null;
|
|
689
642
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
setGuideText("live", guideValueText());
|
|
694
|
-
positionGuide();
|
|
695
|
-
};
|
|
696
|
-
document.addEventListener("pointermove", onGuidePointerMove, true);
|
|
697
|
-
|
|
698
|
-
/* ---- hand the width over to the app's native drag handles ----
|
|
699
|
-
* Pressing a `[data-width-handle]` means the user is taking the
|
|
700
|
-
* width over: switch to the `custom` tier so healing stops
|
|
701
|
-
* enforcing a preset, and let the app's drag win the cascade
|
|
702
|
-
* (it rewrites the same inline property every frame). The
|
|
703
|
-
* settled width is recorded on release so it stays
|
|
704
|
-
* re-selectable from the menu. */
|
|
705
|
-
const isHandleEvent = (e) =>
|
|
706
|
-
e.target instanceof Element && e.target.closest("[data-width-handle]") !== null;
|
|
707
|
-
|
|
708
|
-
const onHandlePointerDown = (e) => {
|
|
709
|
-
if (!isHandleEvent(e)) return;
|
|
710
|
-
startLiveGuide();
|
|
711
|
-
if (currentTier() === "custom") return;
|
|
712
|
-
body().setAttribute("data-dsh-width", "custom");
|
|
713
|
-
try {
|
|
714
|
-
window.localStorage.setItem(KEY, "custom");
|
|
715
|
-
} catch (err) {
|
|
716
|
-
/* ignore */
|
|
717
|
-
}
|
|
718
|
-
collapsedByTier = false;
|
|
719
|
-
syncButton();
|
|
720
|
-
syncMenu();
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
const onHandlePointerUp = (e) => {
|
|
724
|
-
if (!isHandleEvent(e)) return;
|
|
725
|
-
const root = findWidthRoot();
|
|
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
|
-
}
|
|
737
|
-
}
|
|
738
|
-
if (guideActive) finishGuide();
|
|
739
|
-
syncButton();
|
|
740
|
-
syncMenu();
|
|
741
|
-
};
|
|
742
|
-
document.addEventListener("pointerdown", onHandlePointerDown, true);
|
|
743
|
-
document.addEventListener("pointerup", onHandlePointerUp, true);
|
|
744
|
-
|
|
745
|
-
/* ---- feedback ring so every selection is visibly answered ---- */
|
|
746
|
-
function pulse() {
|
|
747
|
-
btn.classList.remove("pulse");
|
|
748
|
-
void btn.offsetWidth;
|
|
749
|
-
btn.classList.add("pulse");
|
|
750
|
-
window.setTimeout(() => btn.classList.remove("pulse"), 700);
|
|
643
|
+
if (slowTimer !== null) {
|
|
644
|
+
clearInterval(slowTimer);
|
|
645
|
+
slowTimer = null;
|
|
751
646
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
syncButton();
|
|
756
|
-
syncMenu();
|
|
757
|
-
if (guideActive && !guideDragging && guide.dataset.mode === "hint") {
|
|
758
|
-
setText(guideTextEl, t("guide.hint"));
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
const unsubscribeLocale = ctx.locale.subscribe(renderTexts);
|
|
762
|
-
|
|
763
|
-
/* ---- attach once the app shell has mounted ---- */
|
|
764
|
-
let attached = false;
|
|
765
|
-
|
|
766
|
-
/* The old gate matched [data-sidebar-collapsed] /
|
|
767
|
-
* [data-details-collapsed]. 0.1.5-rc.2 renders collapsed
|
|
768
|
-
* markers conditionally (`sidebarCollapsed || void 0`) and
|
|
769
|
-
* removed data-details-collapsed, so with the sidebar expanded
|
|
770
|
-
* nothing ever matched and the button never mounted. Wait for
|
|
771
|
-
* the AppFrame overlay layer instead — `data-shell-overlay` is
|
|
772
|
-
* rendered unconditionally once the shell exists. */
|
|
773
|
-
function attach() {
|
|
774
|
-
if (attached || !body()) return;
|
|
775
|
-
if (document.querySelector("[data-shell-overlay]") === null) return;
|
|
776
|
-
attached = true;
|
|
777
|
-
try {
|
|
778
|
-
window.localStorage.removeItem(OLD_KEY);
|
|
779
|
-
} catch (e) {
|
|
780
|
-
/* ignore */
|
|
781
|
-
}
|
|
782
|
-
body().appendChild(btn);
|
|
783
|
-
body().appendChild(menu);
|
|
784
|
-
body().appendChild(guide);
|
|
785
|
-
syncButton();
|
|
786
|
-
const saved = getSavedTier();
|
|
787
|
-
if (saved !== "standard") applyTier(saved);
|
|
788
|
-
startHealing();
|
|
647
|
+
if (readyTimeout !== null) {
|
|
648
|
+
window.clearTimeout(readyTimeout);
|
|
649
|
+
readyTimeout = null;
|
|
789
650
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
651
|
+
}
|
|
652
|
+
function attach() {
|
|
653
|
+
if (attached || !body()) return;
|
|
654
|
+
if (document.querySelector("[data-shell-overlay]") === null) return;
|
|
655
|
+
attached = true;
|
|
656
|
+
stopWaiting();
|
|
657
|
+
try {
|
|
658
|
+
window.localStorage.removeItem(OLD_KEY);
|
|
659
|
+
} catch {}
|
|
660
|
+
const saved = getSavedTier$1();
|
|
661
|
+
if (saved !== "standard") applyTier(saved);
|
|
662
|
+
else emit();
|
|
663
|
+
startHealing();
|
|
664
|
+
const cached = roots.cached();
|
|
665
|
+
if (cached !== null) observeWidthTarget(cached);
|
|
666
|
+
}
|
|
667
|
+
readyTimer = setInterval(attach, 300);
|
|
668
|
+
readyTimeout = window.setTimeout(() => {
|
|
669
|
+
if (attached) return;
|
|
670
|
+
if (readyTimer !== null) {
|
|
794
671
|
clearInterval(readyTimer);
|
|
795
|
-
|
|
796
|
-
}, READY_TIMEOUT_MS);
|
|
797
|
-
|
|
798
|
-
/* ---- self-heal: re-apply the width when the chat tree
|
|
799
|
-
* remounts (conversation switch). Panels are never managed
|
|
800
|
-
* here — the user may expand/collapse either side any time;
|
|
801
|
-
* forcing the rightbar closed only retracts the grid track
|
|
802
|
-
* while the seat keeps rendering as an overlay that covers
|
|
803
|
-
* the conversation. Driven by DOM mutations, scheduled on the
|
|
804
|
-
* next animation frame (before the browser paints) instead of
|
|
805
|
-
* a fixed hot loop: the width must land on the fresh root
|
|
806
|
-
* before its first paint — otherwise the default width
|
|
807
|
-
* flashes for a frame. While the new root has not appeared
|
|
808
|
-
* yet (mid-remount), full scans are owned by a backoff retry;
|
|
809
|
-
* mutation-driven heals only try the cheap path walk so DOM
|
|
810
|
-
* churn cannot storm. ---- */
|
|
811
|
-
let rootMissing = false;
|
|
812
|
-
let rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
813
|
-
let rootRetryTimer = null;
|
|
814
|
-
let healFrame = null;
|
|
815
|
-
let observer = null;
|
|
816
|
-
let fallbackTimer = null;
|
|
817
|
-
|
|
818
|
-
const scheduleFrame = typeof requestAnimationFrame === "function"
|
|
819
|
-
? (cb) => { healFrame = requestAnimationFrame(cb); }
|
|
820
|
-
: (cb) => { healFrame = window.setTimeout(cb, 0); };
|
|
821
|
-
const cancelFrame = typeof cancelAnimationFrame === "function"
|
|
822
|
-
? (id) => cancelAnimationFrame(id)
|
|
823
|
-
: (id) => window.clearTimeout(id);
|
|
824
|
-
|
|
825
|
-
function scheduleRootRetry() {
|
|
826
|
-
if (rootRetryTimer !== null) return;
|
|
827
|
-
rootRetryTimer = window.setTimeout(() => {
|
|
828
|
-
rootRetryTimer = null;
|
|
829
|
-
const tier = currentTier();
|
|
830
|
-
if (tier === "standard") return;
|
|
831
|
-
const prev = widthRoot;
|
|
832
|
-
const root = findWidthRoot();
|
|
833
|
-
if (root === null) {
|
|
834
|
-
rootRetryDelay = Math.min(rootRetryDelay * 2, ROOT_RETRY_MAX_MS);
|
|
835
|
-
scheduleRootRetry();
|
|
836
|
-
} else {
|
|
837
|
-
rootMissing = false;
|
|
838
|
-
rootRetryDelay = ROOT_RETRY_MIN_MS;
|
|
839
|
-
if (root !== prev) applyContentWidth(tier);
|
|
840
|
-
}
|
|
841
|
-
}, rootRetryDelay);
|
|
672
|
+
readyTimer = null;
|
|
842
673
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
674
|
+
slowTimer = setInterval(attach, 2e3);
|
|
675
|
+
}, READY_TIMEOUT_MS);
|
|
676
|
+
window.addEventListener("load", attach);
|
|
677
|
+
const onStorage = (e) => {
|
|
678
|
+
if (!e || e.key !== "dsh.widthTier") return;
|
|
679
|
+
const next = e.newValue;
|
|
680
|
+
if (typeof next !== "string") return;
|
|
681
|
+
const found = TIERS.find((tier) => tier.id === next);
|
|
682
|
+
if (found === void 0) return;
|
|
683
|
+
if (currentTier() === found.id) return;
|
|
684
|
+
applyTier(found.id, false);
|
|
685
|
+
};
|
|
686
|
+
window.addEventListener("storage", onStorage);
|
|
687
|
+
let rootMissing = false;
|
|
688
|
+
let rootRetryDelay = 200;
|
|
689
|
+
let rootRetryTimer = null;
|
|
690
|
+
let healFrame = null;
|
|
691
|
+
let resizeFrame = null;
|
|
692
|
+
let observer = null;
|
|
693
|
+
let fallbackTimer = null;
|
|
694
|
+
let widthObserver = null;
|
|
695
|
+
let observedTarget = null;
|
|
696
|
+
function scheduleFrame(cb) {
|
|
697
|
+
return typeof requestAnimationFrame === "function" ? requestAnimationFrame(cb) : window.setTimeout(cb, 0);
|
|
698
|
+
}
|
|
699
|
+
function cancelFrame(id) {
|
|
700
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(id);
|
|
701
|
+
else window.clearTimeout(id);
|
|
702
|
+
}
|
|
703
|
+
function observeWidthTarget(root) {
|
|
704
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
705
|
+
const target = widthTarget(root);
|
|
706
|
+
if (target === observedTarget && widthObserver !== null) return;
|
|
707
|
+
if (widthObserver !== null) {
|
|
708
|
+
widthObserver.disconnect();
|
|
709
|
+
widthObserver = null;
|
|
710
|
+
}
|
|
711
|
+
observedTarget = target;
|
|
712
|
+
widthObserver = new ResizeObserver(() => {
|
|
713
|
+
if (guideActive && guideDragging) return;
|
|
848
714
|
const tier = currentTier();
|
|
849
715
|
if (tier === "standard") return;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
716
|
+
applyContentWidth(tier);
|
|
717
|
+
emit();
|
|
718
|
+
});
|
|
719
|
+
widthObserver.observe(target);
|
|
720
|
+
}
|
|
721
|
+
function scheduleRootRetry() {
|
|
722
|
+
if (rootRetryTimer !== null) return;
|
|
723
|
+
rootRetryTimer = window.setTimeout(() => {
|
|
724
|
+
rootRetryTimer = null;
|
|
725
|
+
const tier = currentTier();
|
|
726
|
+
if (tier === "standard") return;
|
|
727
|
+
const prev = roots.cached();
|
|
728
|
+
const root = roots.find();
|
|
860
729
|
if (root === null) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
730
|
+
rootRetryDelay = Math.min(rootRetryDelay * 2, ROOT_RETRY_MAX_MS);
|
|
731
|
+
scheduleRootRetry();
|
|
732
|
+
} else {
|
|
733
|
+
rootMissing = false;
|
|
734
|
+
rootRetryDelay = 200;
|
|
735
|
+
if (root !== prev) {
|
|
736
|
+
applyContentWidth(tier);
|
|
737
|
+
emit();
|
|
865
738
|
}
|
|
866
|
-
|
|
739
|
+
observeWidthTarget(root);
|
|
867
740
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
741
|
+
}, rootRetryDelay);
|
|
742
|
+
}
|
|
743
|
+
function heal(fromObserver) {
|
|
744
|
+
if (guideActive) updateGuideStyle();
|
|
745
|
+
if (guideActive && guideDragging) return;
|
|
746
|
+
const tier = currentTier();
|
|
747
|
+
if (tier === "standard") return;
|
|
748
|
+
const prev = roots.cached();
|
|
749
|
+
let root;
|
|
750
|
+
if (rootMissing || fromObserver) root = roots.find(false);
|
|
751
|
+
else root = roots.find();
|
|
752
|
+
if (root === null) {
|
|
753
|
+
if (!rootMissing) {
|
|
754
|
+
rootMissing = true;
|
|
755
|
+
rootRetryDelay = 200;
|
|
871
756
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
* animation settles the column widens, and the width must
|
|
875
|
-
* follow (applyContentWidth is diff-checked, so steady
|
|
876
|
-
* state writes nothing). */
|
|
877
|
-
if (tier === "full") applyContentWidth("full");
|
|
757
|
+
scheduleRootRetry();
|
|
758
|
+
return;
|
|
878
759
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
observer = new MutationObserver(() => {
|
|
883
|
-
if (healFrame !== null) return;
|
|
884
|
-
scheduleFrame(() => {
|
|
885
|
-
healFrame = null;
|
|
886
|
-
heal(true);
|
|
887
|
-
});
|
|
888
|
-
});
|
|
889
|
-
observer.observe(document.body, {
|
|
890
|
-
subtree: true,
|
|
891
|
-
childList: true,
|
|
892
|
-
attributes: true,
|
|
893
|
-
attributeFilter: ["data-sidebar-collapsed"]
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
/* Low-frequency fallback so self-heal still runs if the
|
|
897
|
-
* observer misses something — cheap: no scan while the
|
|
898
|
-
* cached root is alive. */
|
|
899
|
-
fallbackTimer = setInterval(() => heal(false), FALLBACK_HEAL_MS);
|
|
760
|
+
if (rootMissing) {
|
|
761
|
+
rootMissing = false;
|
|
762
|
+
rootRetryDelay = 200;
|
|
900
763
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
764
|
+
observeWidthTarget(root);
|
|
765
|
+
if (root !== prev) {
|
|
766
|
+
applyContentWidth(tier);
|
|
767
|
+
emit();
|
|
768
|
+
} else if (!fromObserver) {
|
|
769
|
+
applyContentWidth(tier);
|
|
770
|
+
emit();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function startHealing() {
|
|
774
|
+
if (typeof MutationObserver !== "undefined") {
|
|
775
|
+
observer = new MutationObserver(() => {
|
|
776
|
+
if (healFrame !== null) return;
|
|
777
|
+
healFrame = scheduleFrame(() => {
|
|
778
|
+
healFrame = null;
|
|
779
|
+
heal(true);
|
|
780
|
+
});
|
|
914
781
|
});
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
782
|
+
observer.observe(document.body, {
|
|
783
|
+
subtree: true,
|
|
784
|
+
childList: true,
|
|
785
|
+
attributes: true,
|
|
786
|
+
attributeFilter: ["data-sidebar-collapsed"]
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
fallbackTimer = setInterval(() => heal(false), FALLBACK_HEAL_MS);
|
|
790
|
+
}
|
|
791
|
+
const onWindowResize = () => {
|
|
792
|
+
if (resizeFrame !== null) return;
|
|
793
|
+
resizeFrame = scheduleFrame(() => {
|
|
794
|
+
resizeFrame = null;
|
|
795
|
+
if (!(guideActive && guideDragging)) {
|
|
796
|
+
const tier = currentTier();
|
|
797
|
+
if (tier !== "standard") applyContentWidth(tier);
|
|
798
|
+
}
|
|
799
|
+
if (guideActive) guideStyle = computeGuideStyle();
|
|
800
|
+
emit();
|
|
801
|
+
});
|
|
802
|
+
};
|
|
803
|
+
window.addEventListener("resize", onWindowResize);
|
|
804
|
+
snapshot = buildSnapshot();
|
|
805
|
+
const unsubscribeLocale = options.locale.subscribe(() => {
|
|
806
|
+
if (guideActive && !guideDragging && guideMode === "hint") guideText = t("guide.hint");
|
|
807
|
+
emit();
|
|
808
|
+
});
|
|
809
|
+
attach();
|
|
810
|
+
return {
|
|
811
|
+
source: {
|
|
812
|
+
getSnapshot: () => snapshot,
|
|
813
|
+
subscribe(listener) {
|
|
814
|
+
listeners.add(listener);
|
|
815
|
+
return () => {
|
|
816
|
+
listeners.delete(listener);
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
actions,
|
|
821
|
+
dispose() {
|
|
920
822
|
unsubscribeLocale();
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
if (
|
|
927
|
-
|
|
823
|
+
stopWaiting();
|
|
824
|
+
if (fallbackTimer !== null) {
|
|
825
|
+
clearInterval(fallbackTimer);
|
|
826
|
+
fallbackTimer = null;
|
|
827
|
+
}
|
|
828
|
+
if (rootRetryTimer !== null) {
|
|
829
|
+
window.clearTimeout(rootRetryTimer);
|
|
830
|
+
rootRetryTimer = null;
|
|
831
|
+
}
|
|
832
|
+
if (guideHideTimer !== null) {
|
|
833
|
+
window.clearTimeout(guideHideTimer);
|
|
834
|
+
guideHideTimer = null;
|
|
835
|
+
}
|
|
836
|
+
if (healFrame !== null) {
|
|
837
|
+
cancelFrame(healFrame);
|
|
838
|
+
healFrame = null;
|
|
839
|
+
}
|
|
840
|
+
if (resizeFrame !== null) {
|
|
841
|
+
cancelFrame(resizeFrame);
|
|
842
|
+
resizeFrame = null;
|
|
843
|
+
}
|
|
844
|
+
if (widthObserver !== null) {
|
|
845
|
+
widthObserver.disconnect();
|
|
846
|
+
widthObserver = null;
|
|
847
|
+
observedTarget = null;
|
|
848
|
+
}
|
|
928
849
|
window.removeEventListener("resize", onWindowResize);
|
|
850
|
+
window.removeEventListener("load", attach);
|
|
851
|
+
window.removeEventListener("storage", onStorage);
|
|
929
852
|
if (observer !== null) observer.disconnect();
|
|
930
|
-
document.removeEventListener("click", onDocClick);
|
|
931
|
-
document.removeEventListener("keydown", onDocKey);
|
|
932
853
|
document.removeEventListener("pointerdown", onHandlePointerDown, true);
|
|
933
854
|
document.removeEventListener("pointerup", onHandlePointerUp, true);
|
|
855
|
+
document.removeEventListener("pointercancel", onHandlePointerCancel, true);
|
|
934
856
|
document.removeEventListener("pointermove", onGuidePointerMove, true);
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (
|
|
938
|
-
if (
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
857
|
+
removeStyles();
|
|
858
|
+
const root = roots.cached();
|
|
859
|
+
if (root !== null) widthTarget(root).style.removeProperty("--dsh-chat-user-width");
|
|
860
|
+
if (preferenceOwned) {
|
|
861
|
+
try {
|
|
862
|
+
if (preferenceWritten !== null && window.localStorage.getItem("dsh.conversation.contentWidth") === preferenceWritten) window.localStorage.removeItem(APP_PREF_KEY);
|
|
863
|
+
} catch {}
|
|
864
|
+
preferenceOwned = false;
|
|
865
|
+
preferenceWritten = null;
|
|
866
|
+
}
|
|
867
|
+
if (collapsedByTier) {
|
|
868
|
+
try {
|
|
869
|
+
if (isSidebarCollapsed()) layout.toggleSidebar();
|
|
870
|
+
} catch {}
|
|
871
|
+
collapsedByTier = false;
|
|
872
|
+
}
|
|
873
|
+
guideHandle = null;
|
|
874
|
+
body().removeAttribute(TIER_ATTR);
|
|
875
|
+
body().removeAttribute(GUIDE_ATTR);
|
|
876
|
+
listeners.clear();
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
// src/client/locales.ts
|
|
881
|
+
/** Dictionary namespace owned by this plugin. */
|
|
882
|
+
const NS = "width-tiers";
|
|
883
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
884
|
+
const zh = {
|
|
885
|
+
"tier.standard": "标准",
|
|
886
|
+
"tier.medium": "中等",
|
|
887
|
+
"tier.wide": "宽",
|
|
888
|
+
"tier.ultra": "超宽",
|
|
889
|
+
"tier.full": "全宽",
|
|
890
|
+
"tier.custom": "自定义",
|
|
891
|
+
"value.custom.unset": "未设置",
|
|
892
|
+
"guide.hint": "拖拽右侧边缘调整对话宽度",
|
|
893
|
+
"button.aria": "对话区宽度档位:{label}({value}),点击选择档位",
|
|
894
|
+
"button.title.summary": "对话区宽度档位:{label}({value})",
|
|
895
|
+
"button.title.hint": "点击选择:{list}"
|
|
896
|
+
};
|
|
897
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
898
|
+
const en = {
|
|
899
|
+
"tier.standard": "Standard",
|
|
900
|
+
"tier.medium": "Medium",
|
|
901
|
+
"tier.wide": "Wide",
|
|
902
|
+
"tier.ultra": "Ultra",
|
|
903
|
+
"tier.full": "Full",
|
|
904
|
+
"tier.custom": "Custom",
|
|
905
|
+
"value.custom.unset": "Not set",
|
|
906
|
+
"guide.hint": "Drag the right edge to resize",
|
|
907
|
+
"button.aria": "Chat width tier: {label} ({value}). Click to choose a tier.",
|
|
908
|
+
"button.title.summary": "Chat width tier: {label} ({value})",
|
|
909
|
+
"button.title.hint": "Click to choose: {list}"
|
|
910
|
+
};
|
|
911
|
+
// src/client/WidthTierPicker.tsx
|
|
912
|
+
/** How long the selection ring stays on the button. */
|
|
913
|
+
const PULSE_MS = 700;
|
|
914
|
+
/** Six horizontal bars; one per tier, lit up to the selected index. */
|
|
915
|
+
function LevelIcon({ tier }) {
|
|
916
|
+
const index = TIERS.findIndex((candidate) => candidate.id === tier);
|
|
917
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
918
|
+
viewBox: "0 0 16 16",
|
|
919
|
+
width: "16",
|
|
920
|
+
height: "16",
|
|
921
|
+
"aria-hidden": "true",
|
|
922
|
+
focusable: "false",
|
|
923
|
+
children: [
|
|
924
|
+
3.5,
|
|
925
|
+
5.3,
|
|
926
|
+
7.1,
|
|
927
|
+
8.9,
|
|
928
|
+
10.7,
|
|
929
|
+
12.5
|
|
930
|
+
].map((width, bar) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
931
|
+
x: 1.5,
|
|
932
|
+
y: .75 + bar * 2.55,
|
|
933
|
+
width,
|
|
934
|
+
height: 1.6,
|
|
935
|
+
rx: .8,
|
|
936
|
+
fill: "currentColor",
|
|
937
|
+
opacity: bar <= index ? 1 : .3
|
|
938
|
+
}, bar))
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Floating tier picker and drag guide, mounted in `shell.overlay`.
|
|
943
|
+
*
|
|
944
|
+
* All width state, persistence and native-handle interception live in the
|
|
945
|
+
* controller; this component only renders the snapshot and dispatches
|
|
946
|
+
* actions. Menu focus, Escape/Tab handling, click-away dismissal and the
|
|
947
|
+
* selection ring are view concerns and stay local here.
|
|
948
|
+
* @param props - snapshot hook and controller actions.
|
|
949
|
+
* @returns the button, menu and guide inside one overlay host element.
|
|
950
|
+
*/
|
|
951
|
+
function WidthTierPicker({ usePicker, actions }) {
|
|
952
|
+
const state = usePicker((snapshot) => snapshot);
|
|
953
|
+
const buttonRef = (0, react.useRef)(null);
|
|
954
|
+
const menuRef = (0, react.useRef)(null);
|
|
955
|
+
const wrapperRef = (0, react.useRef)(null);
|
|
956
|
+
const guideRef = (0, react.useRef)(null);
|
|
957
|
+
const stateRef = (0, react.useRef)(state);
|
|
958
|
+
const [pulsing, setPulsing] = (0, react.useState)(false);
|
|
959
|
+
(0, react.useLayoutEffect)(() => {
|
|
960
|
+
stateRef.current = state;
|
|
961
|
+
}, [state]);
|
|
962
|
+
(0, react.useEffect)(() => {
|
|
963
|
+
if (state.pulse === 0) return;
|
|
964
|
+
setPulsing(false);
|
|
965
|
+
const frame = requestAnimationFrame(() => setPulsing(true));
|
|
966
|
+
const timer = window.setTimeout(() => setPulsing(false), PULSE_MS);
|
|
967
|
+
return () => {
|
|
968
|
+
cancelAnimationFrame(frame);
|
|
969
|
+
window.clearTimeout(timer);
|
|
970
|
+
};
|
|
971
|
+
}, [state.pulse]);
|
|
972
|
+
(0, react.useEffect)(() => {
|
|
973
|
+
if (!state.menuOpen || menuRef.current === null) return;
|
|
974
|
+
const rows = Array.from(menuRef.current.querySelectorAll("button[data-tier]"));
|
|
975
|
+
(rows.find((row) => row.dataset.tier === state.tier) ?? rows[0])?.focus();
|
|
976
|
+
}, [state.menuOpen, state.tier]);
|
|
977
|
+
(0, react.useLayoutEffect)(() => {
|
|
978
|
+
if (!state.guide.active) return;
|
|
979
|
+
const width = guideRef.current?.offsetWidth ?? 0;
|
|
980
|
+
if (width > 0) actions.measureGuide(width);
|
|
981
|
+
}, [
|
|
982
|
+
state.guide.active,
|
|
983
|
+
state.guide.text,
|
|
984
|
+
state.guide.mode,
|
|
985
|
+
actions
|
|
986
|
+
]);
|
|
987
|
+
(0, react.useEffect)(() => {
|
|
988
|
+
const onDocClick = (event) => {
|
|
989
|
+
const target = event.target;
|
|
990
|
+
if (!(target instanceof Node)) return;
|
|
991
|
+
const current = stateRef.current;
|
|
992
|
+
const inside = wrapperRef.current?.contains(target) ?? false;
|
|
993
|
+
if (current.menuOpen && !inside) actions.closeMenu();
|
|
994
|
+
if (current.guide.active && !current.guide.dragging && !inside && !(target instanceof Element && target.closest("[data-width-handle]") !== null)) actions.cancelGuide();
|
|
995
|
+
};
|
|
996
|
+
const onDocKey = (event) => {
|
|
997
|
+
const current = stateRef.current;
|
|
998
|
+
if (event.key === "Escape") {
|
|
999
|
+
if (current.menuOpen) {
|
|
1000
|
+
actions.closeMenu();
|
|
1001
|
+
buttonRef.current?.focus();
|
|
1002
|
+
}
|
|
1003
|
+
if (current.guide.active) actions.cancelGuide();
|
|
1004
|
+
return;
|
|
945
1005
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1006
|
+
if (!current.menuOpen) return;
|
|
1007
|
+
if (event.key === "Tab") {
|
|
1008
|
+
actions.closeMenu();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const rows = Array.from(menuRef.current?.querySelectorAll("button[data-tier]") ?? []);
|
|
1012
|
+
if (rows.length === 0) return;
|
|
1013
|
+
const active = document.activeElement;
|
|
1014
|
+
const index = active === null ? -1 : rows.indexOf(active);
|
|
1015
|
+
let next = -1;
|
|
1016
|
+
if (event.key === "ArrowDown") next = index < 0 ? 0 : (index + 1) % rows.length;
|
|
1017
|
+
else if (event.key === "ArrowUp") next = index < 0 ? rows.length - 1 : (index - 1 + rows.length) % rows.length;
|
|
1018
|
+
else if (event.key === "Home") next = 0;
|
|
1019
|
+
else if (event.key === "End") next = rows.length - 1;
|
|
1020
|
+
if (next < 0) return;
|
|
1021
|
+
event.preventDefault();
|
|
1022
|
+
rows[next]?.focus();
|
|
948
1023
|
};
|
|
949
|
-
|
|
1024
|
+
document.addEventListener("click", onDocClick);
|
|
1025
|
+
document.addEventListener("keydown", onDocKey);
|
|
1026
|
+
return () => {
|
|
1027
|
+
document.removeEventListener("click", onDocClick);
|
|
1028
|
+
document.removeEventListener("keydown", onDocKey);
|
|
1029
|
+
};
|
|
1030
|
+
}, [actions]);
|
|
1031
|
+
const className = [state.button.active ? "active" : "", pulsing ? "pulse" : ""].filter(Boolean).join(" ");
|
|
1032
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1033
|
+
ref: wrapperRef,
|
|
1034
|
+
"data-dsh-width-tiers-host": "",
|
|
1035
|
+
children: [
|
|
1036
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1037
|
+
ref: buttonRef,
|
|
1038
|
+
type: "button",
|
|
1039
|
+
id: "dsh-wide-toggle",
|
|
1040
|
+
"aria-haspopup": "menu",
|
|
1041
|
+
"aria-expanded": state.menuOpen,
|
|
1042
|
+
"aria-label": state.button.ariaLabel,
|
|
1043
|
+
title: state.button.title,
|
|
1044
|
+
className: className || void 0,
|
|
1045
|
+
onClick: () => actions.toggleMenu(),
|
|
1046
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LevelIcon, { tier: state.tier })
|
|
1047
|
+
}),
|
|
1048
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1049
|
+
ref: menuRef,
|
|
1050
|
+
id: "dsh-width-menu",
|
|
1051
|
+
role: state.menuOpen ? "menu" : void 0,
|
|
1052
|
+
hidden: !state.menuOpen,
|
|
1053
|
+
children: state.items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1054
|
+
type: "button",
|
|
1055
|
+
role: "menuitemradio",
|
|
1056
|
+
"data-tier": item.id,
|
|
1057
|
+
tabIndex: -1,
|
|
1058
|
+
"aria-checked": item.selected,
|
|
1059
|
+
className: item.selected ? "selected" : void 0,
|
|
1060
|
+
onClick: () => {
|
|
1061
|
+
actions.selectTier(item.id);
|
|
1062
|
+
buttonRef.current?.focus();
|
|
1063
|
+
},
|
|
1064
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1065
|
+
className: "dshwm-label",
|
|
1066
|
+
children: item.label
|
|
1067
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1068
|
+
className: "dshwm-value",
|
|
1069
|
+
children: item.value
|
|
1070
|
+
})]
|
|
1071
|
+
}, item.id))
|
|
1072
|
+
}),
|
|
1073
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1074
|
+
ref: guideRef,
|
|
1075
|
+
id: "dsh-width-guide",
|
|
1076
|
+
"aria-hidden": "true",
|
|
1077
|
+
hidden: !state.guide.active,
|
|
1078
|
+
"data-mode": state.guide.mode,
|
|
1079
|
+
style: state.guide.style,
|
|
1080
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1081
|
+
viewBox: "0 0 16 16",
|
|
1082
|
+
width: "14",
|
|
1083
|
+
height: "14",
|
|
1084
|
+
"aria-hidden": "true",
|
|
1085
|
+
focusable: "false",
|
|
1086
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1087
|
+
d: "M6 3 2 8l4 5M10 3l4 5-4 5",
|
|
1088
|
+
fill: "none",
|
|
1089
|
+
stroke: "currentColor",
|
|
1090
|
+
strokeWidth: 1.5,
|
|
1091
|
+
strokeLinecap: "round",
|
|
1092
|
+
strokeLinejoin: "round"
|
|
1093
|
+
})
|
|
1094
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1095
|
+
className: "dshwg-text",
|
|
1096
|
+
children: state.guide.text
|
|
1097
|
+
})]
|
|
1098
|
+
})
|
|
1099
|
+
]
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
// src/client/index.ts
|
|
1103
|
+
/** Services required by the browser bundle. */
|
|
1104
|
+
const inject = [
|
|
1105
|
+
"slots",
|
|
1106
|
+
"layout",
|
|
1107
|
+
"locale"
|
|
1108
|
+
];
|
|
1109
|
+
/**
|
|
1110
|
+
* Client plugin body: register the dictionaries, start the width-tier
|
|
1111
|
+
* controller, then contribute the React picker to the frame-wide overlay
|
|
1112
|
+
* layer. The controller owns the width axis and persistence; the slot entry
|
|
1113
|
+
* owns only its own render lifetime.
|
|
1114
|
+
* @param ctx - client root context (slots + layout + locale injected).
|
|
1115
|
+
*/
|
|
1116
|
+
function apply(ctx) {
|
|
1117
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1118
|
+
zh,
|
|
1119
|
+
en
|
|
1120
|
+
}), "dsh-width-tiers: dictionaries");
|
|
1121
|
+
const t = ctx.locale.bind(NS);
|
|
1122
|
+
const controller = createWidthTiers({
|
|
1123
|
+
layout: ctx.layout,
|
|
1124
|
+
locale: ctx.locale,
|
|
1125
|
+
t
|
|
1126
|
+
});
|
|
1127
|
+
ctx.effect(() => () => controller.dispose(), "dsh-width-tiers: controller");
|
|
1128
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
1129
|
+
name: "shell.overlay",
|
|
1130
|
+
id: "width-tiers",
|
|
1131
|
+
order: 100,
|
|
1132
|
+
locale: NS,
|
|
1133
|
+
inject: () => ({
|
|
1134
|
+
hooks: { picker: controller.source },
|
|
1135
|
+
actions: controller.actions
|
|
1136
|
+
})
|
|
1137
|
+
}, WidthTierPicker));
|
|
950
1138
|
}
|
|
951
|
-
//#endregion
|
|
952
|
-
|
|
953
1139
|
exports.apply = apply;
|
|
954
1140
|
exports.inject = inject;
|
|
955
1141
|
return module.exports;
|