dsh-width-tiers 1.0.7 → 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 CHANGED
@@ -28,9 +28,13 @@ budget" — so the built-in drag handles keep working:
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 always show the width a tier resolves to **at
32
- the current column**, so on narrow windows a clamped tier reads its real
33
- value (e.g. `宽 668px`) instead of its nominal one.
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".
34
38
 
35
39
  On narrow windows wider tiers are clamped to the available column, so nothing
36
40
  overflows under the sidebar.
@@ -41,6 +45,12 @@ a glance (all lit for custom). Dragging one of the app's native edge handles
41
45
  switches the picker to the custom tier — the plugin stands aside for native
42
46
  drags.
43
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
+
44
54
  ## Install
45
55
 
46
56
  ```bash
@@ -95,10 +105,12 @@ language, and Chinese is the last resort. No plugin-side setting needed.
95
105
  ## Limitations
96
106
 
97
107
  - Content width cannot exceed the window: on a narrow window, wider tiers are
98
- capped by the available column (the menu shows the capped value). Use
99
- `full` (which also collapses the sidebar) for the maximum.
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.
100
111
  - Requires the DSH Web client plugin system (dsh ≥ 0.1.0-rc.x with a `web`
101
- profile). Verified on dsh 0.1.7-alpha.1 and 0.2.0-rc.2.
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.
102
114
 
103
115
  ## Uninstall
104
116
 
package/README.zh.md CHANGED
@@ -25,8 +25,11 @@ DSH Web 把消息列——包括里面的所有表格——的宽度限制在
25
25
  | 全宽 | 列宽减手柄预留(占满可用宽度) | 收起为图标轨道 |
26
26
  | 自定义 | 拖拽原生手柄得出,可在菜单回切 | 不动 |
27
27
 
28
- 菜单与按钮标题显示的是**当前列宽下实际生效的 px**:窄窗口下被钳制的档位会
29
- 显示真实值(如「宽 668px」),而不是标称值。
28
+ 菜单与按钮标题中:中等 / 宽 / 超宽显示**固定标称值**(1120 / 1280 / 1400px),
29
+ 标准显示应用当前默认值(`clamp(680px, 列宽 × 0.64, 920px)`,如 710px),全宽
30
+ 显示当前列宽下的最大实际值(列宽 − 176px,如 934px),自定义显示已记录的
31
+ 宽度或「未设置」。实际生效宽度仍由应用按「列宽 − 176px」钳制;窄窗口下
32
+ 选定较高档位时,效果等同于「全宽」。
30
33
 
31
34
  窄窗口下超宽档位会自动被钳到可用宽度,不会溢出被侧栏覆盖。
32
35
 
@@ -34,6 +37,10 @@ DSH Web 把消息列——包括里面的所有表格——的宽度限制在
34
37
  的五条横条图标也直观显示当前档位(自定义档全亮)。拖动应用自带的
35
38
  对话区边缘手柄会自动切到「自定义」档,插件让位于原生拖拽。
36
39
 
40
+ 若从未拖拽过,点击「自定义」不会静默套用某个宽度:右侧对话边缘会出现
41
+ 拖拽引导,手柄高亮;拖拽时气泡实时显示对话列宽度,松开后记为该档的值。
42
+ 之后每次拖拽手柄(无论从哪个档位开始)都会重新高亮手柄并显示实时宽度。
43
+
37
44
  ## 安装
38
45
 
39
46
  ```bash
@@ -80,10 +87,12 @@ bundle patch 会自动插入 `dsh-width-tiers` 加载项——无需手动改
80
87
 
81
88
  ## 已知限制
82
89
 
83
- - 内容宽度不可能超过窗口:窗口窄时,高档位会被可用列宽截断(菜单会显示
84
- 截断后的实际值);想要最大空间请用「全宽」(同时收起侧边栏)。
90
+ - 内容宽度不可能超过窗口:窗口窄时,高档位会被可用列宽截断(选择器仍显示
91
+ 标称值,实际宽度以拖拽气泡/窗口效果为准);想要最大空间请用「全宽」
92
+ (同时收起侧边栏)。
85
93
  - 依赖 DSH Web 客户端插件体系(带 web profile 的 dsh ≥ 0.1.0-rc.x)。
86
- 已在 dsh 0.1.7-alpha.1 与 0.2.0-rc.2 上验证。
94
+ 已在 dsh 0.1.7-alpha.1、0.2.0-rc.2 上验证;0.2.1-rc.1 的宽度轴经 bundle
95
+ 对比确认一致。
87
96
 
88
97
  ## 卸载
89
98
 
package/lib/client.js CHANGED
@@ -29,11 +29,17 @@ window.__ModuleLoader__.load({
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.
33
35
  *
34
- * The menu and title always show the px a tier resolves to at the
35
- * CURRENT column (see `tierValueText`), so a clamped tier never
36
- * claims its nominal width on a narrow window.
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.
37
43
  *
38
44
  * - Panels are driven through the layout service (ctx.layout), never
39
45
  * by simulating clicks.
@@ -59,9 +65,10 @@ window.__ModuleLoader__.load({
59
65
  * the horizontal budget both width handles + safe edges reserve. */
60
66
  const CONTENT_MIN_PX = 640;
61
67
  const CONTENT_EDGE_BUDGET_PX = 176;
62
- /* The app's default transcript width (resolveContentWidth's
63
- * no-preference branch): clamp(680px, column × 0.64, 920px). Used to
64
- * show what `standard` actually resolves to at the current column. */
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. */
65
72
  const DEFAULT_MIN_PX = 680;
66
73
  const DEFAULT_MAX_PX = 920;
67
74
  const DEFAULT_COLUMN_RATIO = 0.64;
@@ -87,6 +94,8 @@ window.__ModuleLoader__.load({
87
94
  "tier.ultra": "超宽",
88
95
  "tier.full": "全宽",
89
96
  "tier.custom": "自定义",
97
+ "value.custom.unset": "未设置",
98
+ "guide.hint": "拖拽右侧边缘调整对话宽度",
90
99
  "button.aria": "对话区宽度档位:{label}({value}),点击选择档位",
91
100
  "button.title.summary": "对话区宽度档位:{label}({value})",
92
101
  "button.title.hint": "点击选择:{list}"
@@ -99,22 +108,25 @@ window.__ModuleLoader__.load({
99
108
  "tier.ultra": "Ultra",
100
109
  "tier.full": "Full",
101
110
  "tier.custom": "Custom",
111
+ "value.custom.unset": "Not set",
112
+ "guide.hint": "Drag the right edge to resize",
102
113
  "button.aria": "Chat width tier: {label} ({value}). Click to choose a tier.",
103
114
  "button.title.summary": "Chat width tier: {label} ({value})",
104
115
  "button.title.hint": "Click to choose: {list}"
105
116
  };
106
117
  //#endregion
107
118
 
108
- /* Tier ids and label keys only — every displayed width is resolved at
109
- * render time from the live column (see tierValueText), so the menu
110
- * never claims a width the app would not actually apply. */
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). */
111
122
  const TIERS = [
112
123
  { id: "standard", key: "tier.standard" },
113
124
  { id: "medium", key: "tier.medium" },
114
125
  { id: "wide", key: "tier.wide" },
115
126
  { id: "ultra", key: "tier.ultra" },
116
127
  { id: "full", key: "tier.full" },
117
- /* Reached only by dragging a native width handle. */
128
+ /* Reached by dragging a native width handle, or by picking it in
129
+ * the menu and following the right-edge drag guide. */
118
130
  { id: "custom", key: "tier.custom" }
119
131
  ];
120
132
 
@@ -141,10 +153,15 @@ window.__ModuleLoader__.load({
141
153
  "#dsh-width-menu[hidden]{display:none}",
142
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}",
143
155
  "#dsh-width-menu button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
144
- "#dsh-width-menu button.disabled{opacity:.45;cursor:default;pointer-events:none}",
145
156
  "#dsh-width-menu button.selected{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-state-business-primary)}",
146
157
  "#dsh-width-menu .dshwm-value{color:var(--dsw-alias-label-tertiary);font-size:12px}",
147
- "#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}"
148
165
  ].join("");
149
166
  //#endregion
150
167
 
@@ -299,9 +316,10 @@ window.__ModuleLoader__.load({
299
316
  function tierPx(tier, root) {
300
317
  const cap = Math.max(CONTENT_MIN_PX, root.offsetWidth - CONTENT_EDGE_BUDGET_PX);
301
318
  switch (tier) {
302
- case "medium": return Math.min(1120, cap);
303
- case "wide": return Math.min(1280, cap);
304
- case "ultra": return Math.min(1400, cap);
319
+ case "medium":
320
+ case "wide":
321
+ case "ultra":
322
+ return Math.min(NOMINAL_WIDTH_PX[tier], cap);
305
323
  case "full": return cap;
306
324
  case "custom": {
307
325
  const dragged = readStoredNumber(CUSTOM_KEY);
@@ -311,15 +329,23 @@ window.__ModuleLoader__.load({
311
329
  }
312
330
  }
313
331
 
314
- /* The px a tier resolves to right now, including the app's own
315
- * default formula for `standard`; null when the tier has no
316
- * width of its own yet (custom before the first drag). */
317
- function resolvedWidthPx(tier, root) {
318
- if (tier === "standard") {
319
- const column = root.offsetWidth;
320
- return Math.round(Math.max(DEFAULT_MIN_PX, Math.min(column * DEFAULT_COLUMN_RATIO, DEFAULT_MAX_PX)));
321
- }
322
- return tierPx(tier, root);
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
+ ));
323
349
  }
324
350
 
325
351
  function applyContentWidth(tier) {
@@ -407,18 +433,38 @@ window.__ModuleLoader__.load({
407
433
  return `<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">${bars}</svg>`;
408
434
  }
409
435
 
410
- /* Display value for a tier: the px it resolves to at the live
411
- * column (what the app would actually apply), so narrow windows
412
- * show the clamped value instead of the nominal one. Falls back
413
- * to the recorded drag when the width root is not mounted. */
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). */
414
440
  function tierValueText(tierId) {
415
- const root = findWidthRoot();
416
- if (root !== null) {
417
- const px = resolvedWidthPx(tierId, root);
418
- if (px !== null) return `${px}px`;
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 "—";
419
467
  }
420
- const dragged = readStoredNumber(CUSTOM_KEY);
421
- return dragged === null ? "—" : `${Math.round(dragged)}px`;
422
468
  }
423
469
 
424
470
  function syncButton() {
@@ -450,7 +496,14 @@ window.__ModuleLoader__.load({
450
496
  item.innerHTML = `<span class="dshwm-label">${t(def.key)}</span><span class="dshwm-value">${tierValueText(def.id)}</span>`;
451
497
  item.addEventListener("click", (e) => {
452
498
  if (e && e.stopPropagation) e.stopPropagation();
453
- if (item.classList.contains("disabled")) return;
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);
454
507
  applyTier(def.id);
455
508
  hideMenu();
456
509
  });
@@ -476,10 +529,6 @@ window.__ModuleLoader__.load({
476
529
  if (labelEl) setText(labelEl, t(tierOf(id).key));
477
530
  const valueEl = items[i].querySelector(".dshwm-value");
478
531
  if (valueEl) setText(valueEl, tierValueText(id));
479
- /* custom is inert until the user has dragged once */
480
- const disabled = id === "custom" && readStoredNumber(CUSTOM_KEY) === null;
481
- items[i].classList.toggle("disabled", disabled);
482
- items[i].setAttribute("aria-disabled", String(disabled));
483
532
  }
484
533
  }
485
534
 
@@ -501,15 +550,151 @@ window.__ModuleLoader__.load({
501
550
  });
502
551
 
503
552
  const onDocClick = (e) => {
504
- if (menu.hidden) return;
505
- if (e.target !== btn && !menu.contains(e.target)) hideMenu();
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
+ }
506
559
  };
507
560
  const onDocKey = (e) => {
508
- if (e.key === "Escape" && !menu.hidden) hideMenu();
561
+ if (e.key !== "Escape") return;
562
+ if (!menu.hidden) hideMenu();
563
+ if (guideActive) endGuide(true);
509
564
  };
510
565
  document.addEventListener("click", onDocClick);
511
566
  document.addEventListener("keydown", onDocKey);
512
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
+
513
698
  /* ---- hand the width over to the app's native drag handles ----
514
699
  * Pressing a `[data-width-handle]` means the user is taking the
515
700
  * width over: switch to the `custom` tier so healing stops
@@ -521,7 +706,9 @@ window.__ModuleLoader__.load({
521
706
  e.target instanceof Element && e.target.closest("[data-width-handle]") !== null;
522
707
 
523
708
  const onHandlePointerDown = (e) => {
524
- if (!isHandleEvent(e) || currentTier() === "custom") return;
709
+ if (!isHandleEvent(e)) return;
710
+ startLiveGuide();
711
+ if (currentTier() === "custom") return;
525
712
  body().setAttribute("data-dsh-width", "custom");
526
713
  try {
527
714
  window.localStorage.setItem(KEY, "custom");
@@ -536,16 +723,19 @@ window.__ModuleLoader__.load({
536
723
  const onHandlePointerUp = (e) => {
537
724
  if (!isHandleEvent(e)) return;
538
725
  const root = findWidthRoot();
539
- if (root === null) return;
540
- const px = Number.parseFloat(
541
- widthTarget(root).style.getPropertyValue("--dsh-chat-user-width")
542
- );
543
- if (!Number.isFinite(px) || px <= 0) return;
544
- try {
545
- window.localStorage.setItem(CUSTOM_KEY, String(Math.round(px)));
546
- } catch (err) {
547
- /* ignore */
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
+ }
548
737
  }
738
+ if (guideActive) finishGuide();
549
739
  syncButton();
550
740
  syncMenu();
551
741
  };
@@ -564,6 +754,9 @@ window.__ModuleLoader__.load({
564
754
  const renderTexts = () => {
565
755
  syncButton();
566
756
  syncMenu();
757
+ if (guideActive && !guideDragging && guide.dataset.mode === "hint") {
758
+ setText(guideTextEl, t("guide.hint"));
759
+ }
567
760
  };
568
761
  const unsubscribeLocale = ctx.locale.subscribe(renderTexts);
569
762
 
@@ -588,6 +781,7 @@ window.__ModuleLoader__.load({
588
781
  }
589
782
  body().appendChild(btn);
590
783
  body().appendChild(menu);
784
+ body().appendChild(guide);
591
785
  syncButton();
592
786
  const saved = getSavedTier();
593
787
  if (saved !== "standard") applyTier(saved);
@@ -648,6 +842,9 @@ window.__ModuleLoader__.load({
648
842
  }
649
843
 
650
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();
651
848
  const tier = currentTier();
652
849
  if (tier === "standard") return;
653
850
  const prev = widthRoot;
@@ -702,10 +899,9 @@ window.__ModuleLoader__.load({
702
899
  fallbackTimer = setInterval(() => heal(false), FALLBACK_HEAL_MS);
703
900
  }
704
901
 
705
- /* ---- keep the displayed px honest when the column changes ----
706
- * Window resizes (and layout handle drags) change what every
707
- * tier resolves to, so re-render the copy on the next frame.
708
- * The menu is also re-synced whenever it opens. */
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. ---- */
709
905
  let resizeFrame = null;
710
906
 
711
907
  const onWindowResize = () => {
@@ -714,6 +910,7 @@ window.__ModuleLoader__.load({
714
910
  resizeFrame = null;
715
911
  syncButton();
716
912
  syncMenu();
913
+ positionGuide();
717
914
  });
718
915
  };
719
916
  window.addEventListener("resize", onWindowResize);
@@ -725,6 +922,7 @@ window.__ModuleLoader__.load({
725
922
  clearInterval(fallbackTimer);
726
923
  clearTimeout(timeout);
727
924
  clearTimeout(rootRetryTimer);
925
+ clearTimeout(guideHideTimer);
728
926
  if (healFrame !== null) cancelFrame(healFrame);
729
927
  if (resizeFrame !== null) cancelFrame(resizeFrame);
730
928
  window.removeEventListener("resize", onWindowResize);
@@ -733,8 +931,10 @@ window.__ModuleLoader__.load({
733
931
  document.removeEventListener("keydown", onDocKey);
734
932
  document.removeEventListener("pointerdown", onHandlePointerDown, true);
735
933
  document.removeEventListener("pointerup", onHandlePointerUp, true);
934
+ document.removeEventListener("pointermove", onGuidePointerMove, true);
736
935
  if (btn.parentNode) btn.parentNode.removeChild(btn);
737
936
  if (menu.parentNode) menu.parentNode.removeChild(menu);
937
+ if (guide.parentNode) guide.parentNode.removeChild(guide);
738
938
  if (styleTag.parentNode) styleTag.parentNode.removeChild(styleTag);
739
939
  /* Hand the width back to the app default; the app's own
740
940
  * preference (which this plugin mirrors) stays so the last
@@ -744,6 +944,7 @@ window.__ModuleLoader__.load({
744
944
  widthTarget(widthRoot).style.removeProperty("--dsh-chat-user-width");
745
945
  }
746
946
  body().removeAttribute("data-dsh-width");
947
+ body().removeAttribute("data-dsh-width-guide");
747
948
  };
748
949
  }, "dsh-width-tiers: ui");
749
950
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-width-tiers",
3
- "version": "1.0.7",
4
- "description": "DSH Web client plugin: chat content width tiers (standard / medium / wide / ultra / full / custom) with a floating picker button. Drives the app's native width axis on DSH 0.1.7+ (verified on 0.2.0-rc.2), so the built-in drag handles keep working. UI copy follows the app language (zh / en).",
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": {