dsh-font-enhancer 1.0.0 → 1.1.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 CHANGED
@@ -2,18 +2,35 @@
2
2
 
3
3
  > 按区域分别设置 DSH 界面字体、字号、颜色,支持统一控制、主题保存/切换、随机配色/字体。
4
4
 
5
+ ![npm](https://img.shields.io/npm/v/dsh-font-enhancer)
6
+ ![license](https://img.shields.io/github/license/loyalchiiina/dsh-font-enhancer)
7
+ ![dsh](https://img.shields.io/badge/DSH-Plugin-8A2BE2)
8
+
5
9
  A DSH (DeepSeek Harness) plugin that lets you style the UI **per-region** — each
6
10
  region (sidebar, message area, input box, code block, etc.) gets its own
7
11
  Chinese/English font, font-size, line-height, weight, italic, and text color.
8
- Features **unified** global font/color overrides, **theme** save/switch, and
9
- **random** font/color generators.
12
+ Features **unified** global font/color overrides, **theme** save/switch, **框选
13
+ (pick-region)** element selection, and **random** font/color generators.
14
+
15
+ > ⚠️ **当前版本(v1.1.0)仅保证在 DSH **网页端**(浏览器访问 DSH Web GUI,默认
16
+ > `http://127.0.0.1:43120`)正常生效。** 桌面端 Electron 内置窗口的样式适配仍在
17
+ > 开发中(框选标记 `data-fe-reg` 只打在网页渲染的 DOM 上,桌面窗口暂时匹配不到)。
18
+ > 桌面端用户请等待后续版本,或先用 `dsh plugin add dsh-font-enhancer@1.0.0` 回退到
19
+ > 旧版。
20
+ >
21
+ > ⚠️ Current version (v1.1.0) is verified on the DSH **Web GUI** (browser access,
22
+ > default `http://127.0.0.1:43120`) only. Desktop Electron window styling is still
23
+ > in progress — pick-region markers (`data-fe-reg`) are only attached to the DOM
24
+ > rendered in the browser, so the desktop window cannot match them yet. Desktop
25
+ > users should wait for a later release, or fall back with
26
+ > `dsh plugin add dsh-font-enhancer@1.0.0`.
10
27
 
11
28
  ---
12
29
 
13
30
  ## 功能 Features
14
31
 
15
32
  ### 区域样式 Per-region Styling
16
- - **15 pre-calibrated regions** covering the entire DSH Desktop UI:
33
+ - Pre-calibrated regions covering the DSH **Web GUI** UI:
17
34
  Left sidebar, Logo, New Session, Taskboard entry, Workspace name, Settings,
18
35
  Conversation messages, Session title, Composer/input, Tabs, Turn indicator,
19
36
  Agent/model name, Right extension panel, Code blocks, Bottom expand bar.
@@ -67,6 +84,13 @@ Or copy the folder into `node_modules/` and add the package name to the profile
67
84
 
68
85
  ### Requirements
69
86
  - DSH Desktop 2.0+ (tested on `0.1.0-rc.6`).
87
+ - **本版本仅验证 DSH 网页端(浏览器访问 Web GUI)**;桌面端窗口适配见顶部 ⚠️ 说明。
88
+
89
+ ### 版本 Versions
90
+ | 版本 | 说明 |
91
+ |------|------|
92
+ | `v1.1.0` | 增强版:新增框选区域(pick-region)+ 采集调试 + 随机配色。**仅网页端可用**。 |
93
+ | `v1.0.0` | 旧版:内置区域样式 + 统一控制 + 主题。相对简单,兼容性更好。 |
70
94
 
71
95
  ---
72
96
 
@@ -86,6 +110,24 @@ Or copy the folder into `node_modules/` and add the package name to the profile
86
110
 
87
111
  ---
88
112
 
113
+ ## 快速上手 Quick start
114
+
115
+ **想给所有消息区域换一种字体/颜色?**
116
+ 1. 点右下角 **Aa** 按钮打开面板。
117
+ 2. 在 **全局统一** 区勾选 **统一字体** → 选中文/英文字体 → 所有区域立即统一。
118
+ 3. 勾选 **统一颜色** → 点色块选色 → 所有区域统一变色。
119
+
120
+ **想单独设置 输入框 的字号?**
121
+ 1. 打开面板 → 找到「输入区」卡片 → 勾选它的复选框(展开设置)。
122
+ 2. 右侧列改 **字号** / **字重**,左侧列改中/英文字体。
123
+
124
+ **想存一套配色以后换着用?**
125
+ 1. 调好各区域字体、颜色。
126
+ 2. 在顶部输入框填名字 → 点 **💾存**。
127
+ 3. 之后随时在下拉框选中 → 点 **应用** 切换。
128
+
129
+ ---
130
+
89
131
  ## 原理 How it works
90
132
 
91
133
  Pure **client-side** plugin. `lib/client.js` is a `__ModuleLoader__` bundle that:
@@ -98,6 +140,29 @@ No host services, no external dependencies, no build step.
98
140
 
99
141
  ---
100
142
 
143
+ ## 常见问题 FAQ
144
+
145
+ ### 插件卡住了 / 样式不生效?
146
+ - 点击 DSH 左下角 **重载** 按钮。
147
+ - 或点击右下角 **Aa** 悬浮球使其折叠——折叠时会自动重建插件(删除旧注入样式并重新初始化)。
148
+
149
+ ### 区域颜色互相串色 / 某区域颜色不对?
150
+ - 检查该区域是否被更靠前的区域(如 `body` 兜底区域)覆盖。
151
+ - 15 个预校准区域的顺序已按层叠优先级排列,但如果你自定义了区域,注意后列出的规则会覆盖前面的(同特异性)。
152
+ - 各区域的具体 CSS 选择器可在卡片中查看和编辑。
153
+
154
+ ### 统一颜色/字体勾选了,但随机后没变化?
155
+ - 统一开关 **开启** 时,随机按钮直接改变统一值(统一颜色/字体本身),而不是改变各区域。这是正常行为——关掉统一开关后随机才会分别改各区域。
156
+
157
+ ### 主题保存后找不到?
158
+ - 保存成功会有 toast 提示"已保存:xxx"。主题在下拉框中,选中后点击 **应用** 即可切换。
159
+ - 主题保存在 `localStorage`,升级插件版本也不会丢失。
160
+
161
+ ### 面板字体怎么是楷体,和界面字体不一样?
162
+ - 面板字体固定为楷体(KaiTi),这是有意为之——防止插件自身的 CSS 规则覆盖面板 UI,确保面板始终可读。
163
+
164
+ ---
165
+
101
166
  ## 文件结构 File structure
102
167
 
103
168
  ```
@@ -114,6 +179,16 @@ dsh-font-enhancer/
114
179
 
115
180
  ---
116
181
 
182
+ ## 支持 Support
183
+
184
+ 发现 bug 或有功能建议?欢迎在
185
+ [GitHub Issues](https://github.com/loyalchiiina/dsh-font-enhancer/issues) 提 issue。
186
+
187
+ Found a bug or have an idea? Open an issue at
188
+ [GitHub Issues](https://github.com/loyalchiiina/dsh-font-enhancer/issues).
189
+
190
+ ---
191
+
117
192
  ## License
118
193
 
119
194
  MIT
package/lib/client.js CHANGED
@@ -54,7 +54,8 @@ window.__ModuleLoader__.load({
54
54
  // old region colors (e.g. the first cyan/blue scheme) are dropped and the
55
55
  // latest DEFAULT_REGIONS() palette is loaded instead. This makes palette
56
56
  // refreshes survive a restart without needing the user to clear localStorage.
57
- var SCHEMA_VERSION = 19;
57
+ var SCHEMA_VERSION = 21;
58
+ // 21: DEFAULT_REGIONS 清空 —— 用户从零开始逐项框选/添加、逐项测试颜色稳定生效
58
59
 
59
60
  // ---------- helpers ----------
60
61
  function esc(s) {
@@ -117,79 +118,30 @@ window.__ModuleLoader__.load({
117
118
  return d;
118
119
  }
119
120
  function DEFAULT_REGIONS() {
120
- function mk(o) {
121
- var d = defaultRegion();
122
- return Object.assign(d, o);
123
- }
124
- // Selectors calibrated against the installed DSH Desktop bundle
125
- // (packages @deepseek-ai/dsh-client-ui-* in app.asar.unpacked). DSH renders
126
- // with CSS-Modules hashed classes; the values below are the real container
127
- // classes for each area. They are editable per card, so if a DSH update
128
- // re-hashes the classes you can simply re-point them here.
121
+ // 默认区域 = 用户采集到的「稳定 DOM 路径」(operation.log REC_NEW),跨重启有效。
122
+ // 每项先用统一红色 #FF0000 验证是否稳定定位+变色,确认后再逐项调整颜色。
123
+ // 注意:部分类名是 CSS-Modules 哈希(.mMKm6W_*/.xUkysG_*),若 DSH 升级 re-hash 会失效,需重新采集。
124
+ function mk(o) { var d = defaultRegion(); return Object.assign(d, o); }
129
125
  return [
130
- // ---- Calibrated against the installed DSH Desktop bundle ----
131
- // Verified live class names (each dsh-client-ui-* package's lib/client.js
132
- // exports a semantic->hash map). Used here:
133
- // layout: sidebarCol/.pI_x6G_sidebarCol, detailsCol/.pI_x6G_detailsCol
134
- // conversation: root=.wSkVaW_root (whole chat view), composer=.wSkVaW_composer*,
135
- // title/header=.lXshSW_title|header, item=.lXshSW_item,
136
- // crumbs=.wSkVaW_crumb*
137
- // sidebar: brand/logoRow/brandMark/brandName=hHd-Xa_*,
138
- // newSession/newSessionLabel=hHd-Xa_*,
139
- // settingsArea/regionArea/footerActions=hHd-Xa_*
140
- // workspace: projectRow/projectText/sessionRow=YDXeBa_*
141
- // taskboard: sidebar entry row = [data-dsh-atb-entry] (stable attr)
142
- //
143
- // Ordering: body (widest) FIRST, then most-specific regions so later
144
- // rules win the cascade (same specificity, later wins with !important).
145
- // Never re-add ".pI_x6G_centerCol" — whole middle column, swallows title/
146
- // composer/token text into one color.
147
- //
148
- // whole-UI fallback: right-bottom expand bar + bottom bar
149
- mk({ name: "底部展开栏", selector: "body", enabled: true, _open: true, useColor: true, color: "#FF0000", group: "其他" }),
150
- // left workspace navigation column (root pane) — confirmed orange
151
- mk({ name: "左侧导航栏", selector: ".pI_x6G_sidebarCol, [data-pane=\"sidebar\"]", enabled: true, useColor: true, color: "#FF8000", group: "左侧栏" }),
152
- // top-left logo / brand (DeepSeek brand mark + name) — confirmed magenta
153
- mk({ name: "左上角 Logo", selector: ".hHd-Xa_brand, .hHd-Xa_logoRow, .hHd-Xa_brandMark, .hHd-Xa_brandName, .hHd-Xa_brandIdentity", enabled: true, useColor: true, color: "#FF00FF", group: "左侧栏" }),
154
- // Agent 任务看板 sidebar entry (dsh-taskboard) — confirmed yellow
155
- mk({ name: "任务看板入口", selector: "[data-dsh-atb-entry], .dsh-atb-entry, .dsh-atb-entry-label, .dsh-atb-entry-icon", enabled: true, useColor: true, color: "#FFFF00", group: "左侧栏" }),
156
- // "new session" button — used to read as green next to the workspace
157
- // name; black is the only base color no other region uses and reads
158
- // clearly on the light sidebar.
159
- mk({ name: "新会话", selector: ".hHd-Xa_newSession, .hHd-Xa_newSessionLabel", enabled: true, useColor: true, color: "#000000", group: "左侧栏" }),
160
- // workspace / project names (DSH desktop / DEEPSEEK-chat / deepseekharness) — green
161
- mk({ name: "工作区名称", selector: ".YDXeBa_projectText, .YDXeBa_projectRow", enabled: true, useColor: true, color: "#00FF00", group: "左侧栏" }),
162
- // settings area at the BOTTOM of the sidebar (footer only) — confirmed blue
163
- mk({ name: "设置区域", selector: ".hHd-Xa_settingsArea, .hHd-Xa_footerActions", enabled: true, useColor: true, color: "#0000FF", group: "左侧栏" }),
164
- // conversation messages (chat body) — confirmed white
165
- mk({ name: "对话区消息", selector: ".wSkVaW_root, .lXshSW_root", enabled: true, useColor: true, color: "#FFFFFF", group: "对话区" }),
166
- // conversation top title — session title text; unified cyan together with
167
- // the agent/model names per user decision
168
- mk({ name: "对话栏名称", selector: ".lXshSW_title, .lXshSW_header, .wSkVaW_crumb, .wSkVaW_crumbs", enabled: true, useColor: true, color: "#00FFFF", group: "对话区" }),
169
- // composer / input — confirmed pink; scoped narrowly (no textarea/
170
- // [contenteditable] wildcards or they bleed onto the turn indicator/code)
171
- mk({ name: "输入区", selector: ".wSkVaW_composerStack, .wSkVaW_composerSeat", enabled: true, useColor: true, color: "#FF69B4", group: "对话区" }),
172
- // 会话页签(对话/轨迹)above the input — verified wSkVaW_ hashes
173
- mk({ name: "会话页签", selector: ".wSkVaW_tabs, .wSkVaW_tab, .wSkVaW_tabActive", enabled: true, useColor: true, color: "#808080", group: "对话区" }),
174
- // 对话轮次指示 = the stats line BELOW the input (17 轮 · N 步 | LLM … |
175
- // 工具调用 … | 缓存命中 …) — StatsLine module FJxK0a_*. Must come after
176
- // 输入区 so it beats the pink composer descendants at equal specificity.
177
- mk({ name: "对话轮次指示", selector: ".FJxK0a_root, .FJxK0a_sep, .Md3f7G_turnStatus, .Md3f7G_turnStatusClock", enabled: true, useColor: true, color: "#FF8000", group: "对话区" }),
178
- // 智慧体/模型名称 — preset agent name right of the title.
179
- // Structural probe: paint everything in the header row EXCEPT the
180
- // crumbs subtree. :not(complex) is fine in Chromium; specificity
181
- // (0,2,0) beats the white root rule.
182
- mk({ name: "智慧体/模型名称", selector: ".wSkVaW_header :not(.wSkVaW_crumbs):not(.wSkVaW_crumbs *), .wSkVaW_crumbSubagent, .ZKlsPq_trigger, .ZKlsPq_switcherTrigger, .ZKlsPq_switcherTitle, ._7KE1Ra_trigger, ._7KE1Ra_triggerLabel, ._7KE1Ra_modelName", enabled: true, useColor: true, color: "#00FFFF", group: "对话区" }),
183
- // right extension / details column — panel is collapsed by default and
184
- // falls through to the body-red fallback; keep red so the list matches
185
- // what the user actually sees.
186
- mk({ name: "右侧扩展栏", selector: ".pI_x6G_detailsCol", enabled: true, useColor: true, color: "#FF0000", group: "其他" }),
187
- // code blocks — ancestor-prefixed forms out-specify ".wSkVaW_root *"
188
- // (the white message rule), so blue wins regardless of order
189
- mk({ name: "代码块", selector: "pre, code, .wSkVaW_root pre, .wSkVaW_root code, .lXshSW_root pre, .lXshSW_root code", enabled: true, useColor: true, color: "#0000FF", group: "对话区" })
126
+ // 探针方案:每个区域一个独特颜色,用户重启后对照界面变色位置→告诉我颜色对应哪个区域,我据此固化正确默认。
127
+ // logo(红)
128
+ mk({ name: "logo", selector: '[data-slot="sidebar"] .xUkysG_logoRow, [data-slot="sidebar"] [class*="logo"]', enabled: true, useColor: true, color: "#FF0000", group: "其他" }),
129
+ // 按钮(橙)
130
+ mk({ name: "按钮", selector: '[data-slot="sidebar"] .xUkysG_newSession, [data-slot="sidebar"] .dsh-atb-entry, [data-slot="sidebar"] .dsw-web-sessions-badge, [data-slot="sidebar"] .zOa2rq_trigger', enabled: true, useColor: true, color: "#FF8C00", group: "左侧栏" }),
131
+ // 对话栏上方(金)
132
+ mk({ name: "对话栏上方", selector: '[data-slot="conversation"] ._4RFuWq_titleRow, [data-slot="conversation"] ._4RFuWq_tabs', enabled: true, useColor: true, color: "#FFD700", group: "对话区" }),
133
+ // 工作区(亮绿)
134
+ mk({ name: "工作区", selector: '[data-slot="sidebar"] .xUkysG_regionArea', enabled: true, useColor: true, color: "#00FF00", group: "左侧栏" }),
135
+ // 对话区(深天蓝)
136
+ mk({ name: "对话区", selector: '[data-slot="conversation.chat.node"] .sDVxEG_body, [data-slot*="conversation.chat.node"] [data-slot="markdown"]', enabled: true, useColor: true, color: "#00BFFF", group: "对话区" }),
137
+ // 对话信息显示(蓝紫)
138
+ mk({ name: "对话信息显示", selector: '[data-slot="conversation.chat.node"] ._row_luwio_16, [data-slot="conversation.chat.node"] .QWj8lG_actions', enabled: true, useColor: true, color: "#8A2BE2", group: "对话区" }),
139
+ // 输入区(品红)
140
+ mk({ name: "输入区", selector: '[data-slot*="conversation.composer"] .krUYjW_card, [data-slot*="conversation.composer"] textarea, [data-slot*="conversation.composer"] [contenteditable="true"]', enabled: true, useColor: true, color: "#FF00FF", group: "对话区" }),
141
+ // 对话栏底部区域(青)
142
+ mk({ name: "对话栏底部区域", selector: '[data-slot="conversation.composer.dock"] .q2FAPq_root, [data-slot="conversation.composer.dock"] span button', enabled: true, useColor: true, color: "#00FFFF", group: "对话区" })
190
143
  ];
191
144
  }
192
-
193
145
  function normThemes(o) {
194
146
  if (!o || !Array.isArray(o.themes)) return [];
195
147
  return o.themes.filter(function (t) {
@@ -229,8 +181,15 @@ window.__ModuleLoader__.load({
229
181
  return { regions: DEFAULT_REGIONS(), themes: themes, unified: unified, bx: typeof o.bx === "number" ? o.bx : null, by: typeof o.by === "number" ? o.by : null };
230
182
  }
231
183
  if (o && Array.isArray(o.regions)) {
184
+ var loaded = o.regions.map(normRegion).filter(function (x) { return x.group !== "已识别"; });
185
+ var hasPicked = loaded.some(function (x) { return x.group === "框选"; });
186
+ var regions = hasPicked
187
+ // Once the user has box-picked regions, those SUPERSEDE the built-in
188
+ // preset regions — the panel shows only what the user picked/owns.
189
+ ? loaded.filter(function (x) { return x.group === "框选"; })
190
+ : loaded;
232
191
  return {
233
- regions: o.regions.map(normRegion),
192
+ regions: regions,
234
193
  themes: themes,
235
194
  unified: unified,
236
195
  bx: typeof o.bx === "number" ? o.bx : null,
@@ -343,13 +302,13 @@ window.__ModuleLoader__.load({
343
302
  return [
344
303
  "#dsh-fe-root *{box-sizing:border-box;scrollbar-width:thin}",
345
304
  "",
346
- "/* ── toggle button — gem-like floating orb ── */",
347
- "#dsh-fe-toggle{position:fixed;left:auto;top:auto;right:18px;bottom:18px;z-index:2147483600;width:48px;height:48px;border-radius:50%;border:0;background:radial-gradient(circle at 30% 25%,rgba(255,255,255,.15),rgba(40,44,54,.95) 70%);color:#eef0f4;font-size:22px;font-weight:700;cursor:grab;user-select:none;touch-action:none;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15);backdrop-filter:blur(12px);transition:box-shadow .25s,transform .2s;text-shadow:0 1px 4px rgba(0,0,0,.4);line-height:48px;text-align:center}",
348
- "#dsh-fe-toggle:hover{box-shadow:0 6px 32px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.2);transform:scale(1.06)}",
349
- "#dsh-fe-toggle.dsh-fe-dragging{cursor:grabbing;transform:scale(1.12);box-shadow:0 8px 40px rgba(0,0,0,.8)}",
350
- "",
351
- "/* ── panel root — deep glass studio ── */",
352
- "#dsh-fe-panel{position:fixed;left:18px;top:18px;z-index:2147483600;width:348px;max-width:calc(100vw - 36px);max-height:86vh;overflow-y:auto;overflow-x:hidden;color:#e6e6e6;border-radius:20px;padding:20px;font:13px/1.5 KaiTi,'Microsoft YaHei',sans-serif;background:rgba(16,18,22,.85);border:1px solid rgba(255,255,255,.08);box-shadow:0 20px 60px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.06);backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px)}",
305
+ "/* ── toggle button — gem-like floating orb (max-visible) ── */",
306
+ "#dsh-fe-root{position:relative;z-index:2147483647}",
307
+ "#dsh-fe-toggle{position:fixed;left:auto;top:auto;right:18px;bottom:18px;z-index:2147483647 !important;width:54px !important;height:54px !important;border-radius:50% !important;border:0 !important;background:radial-gradient(circle at 30% 25%,rgba(255,255,255,.15),rgba(40,44,54,.95) 70%) !important;color:#eef0f4 !important;font-size:22px !important;font-weight:700 !important;cursor:grab !important;user-select:none !important;touch-action:none !important;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15) !important;backdrop-filter:blur(12px) !important;line-height:54px !important;text-align:center !important;display:block !important;visibility:visible !important;opacity:1 !important;transform:none !important;pointer-events:auto !important}",
308
+ "#dsh-fe-toggle:hover{box-shadow:0 6px 32px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.2) !important;transform:scale(1.06) !important}",
309
+ "#dsh-fe-toggle.dsh-fe-dragging{cursor:grabbing !important;transform:scale(1.12) !important;box-shadow:0 8px 40px rgba(0,0,0,.8) !important}",
310
+ "/* ── panel root — deep glass studio (position set by JS positionPanel) ── */",
311
+ "#dsh-fe-panel{position:fixed !important;z-index:2147483647 !important;width:348px !important;max-width:calc(100vw - 36px) !important;max-height:86vh;overflow-y:auto !important;overflow-x:hidden !important;color:#e6e6e6 !important;border-radius:20px !important;padding:20px !important;font:13px/1.5 KaiTi,'Microsoft YaHei',sans-serif !important;background:rgba(16,18,22,.92) !important;border:1px solid rgba(255,255,255,.08) !important;box-shadow:0 20px 60px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.06) !important;backdrop-filter:blur(24px) !important;-webkit-backdrop-filter:blur(24px) !important;visibility:visible !important;pointer-events:auto !important}",
353
312
  "#dsh-fe-panel::-webkit-scrollbar{width:3px}",
354
313
  "#dsh-fe-panel::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:3px}",
355
314
  "#dsh-fe-panel::-webkit-scrollbar-track{background:transparent}",
@@ -371,7 +330,9 @@ window.__ModuleLoader__.load({
371
330
  "#dsh-fe-themeblock button:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.2)}",
372
331
  "",
373
332
  "/* ── toolbar ── */",
374
- "#dsh-fe-toolbar{display:flex;gap:5px;margin-bottom:14px}",
333
+ ".dsh-fe-toolbar{display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap}",
334
+ "#dsh-fe-toolbar .dsh-fe-toolbar2{margin-top:8px}",
335
+ ".dsh-fe-toolbar2{display:flex;gap:5px;margin:6px 0 14px;flex-wrap:wrap}",
375
336
  "#dsh-fe-toolbar button{flex:1;padding:8px 0;border-radius:10px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.04);color:#cfd4dc;cursor:pointer;font:inherit;font-size:12px;font-weight:500;transition:all .18s;letter-spacing:.01em}",
376
337
  "#dsh-fe-toolbar button:hover{background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.2);transform:translateY(-1px)}",
377
338
  "#dsh-fe-toolbar button:active{transform:translateY(0)}",
@@ -448,6 +409,63 @@ window.__ModuleLoader__.load({
448
409
  var panel = null;
449
410
  var cardsEl = null;
450
411
 
412
+ // ── 采集定位区域(录制模式)──────────────────────────────
413
+ // 用户点「🔴 采集区域」按钮开/关。开启期间,每次框选/加元素都会把被选中元素的
414
+ // 「稳定定位信息」收集并写入 ~/.dsh/data/dsh-font-enhancer/operation.log,
415
+ // 便于排查:恢复哪个稳定选择器能在重启后仍准确定位到该元素。
416
+ var RECORDING = false;
417
+ var recSamples = [];
418
+ function recToggle(on) {
419
+ RECORDING = !!on;
420
+ recSamples = [];
421
+ try { localStorage.setItem("dsh-fe-rec", RECORDING ? "1" : "0"); } catch (e) {}
422
+ // 同步按钮文案
423
+ if (root) {
424
+ var b = root.querySelector('[data-act="recordToggle"]');
425
+ if (b) { b.textContent = RECORDING ? "⏹ 停止采集" : "🔴 采集区域"; b.classList.toggle("dsh-fe-rec-on", RECORDING); }
426
+ }
427
+ showToast(RECORDING ? "采集模式已开启:现在框选/加元素会记录元素定位到文件" : "采集模式已关闭", RECORDING ? "ok" : "");
428
+ }
429
+ // 元素最简稳定选择器(优先语义 data-*、id、class,最后 tag)
430
+ function stableSelFor(node) {
431
+ if (!node || node.nodeType !== 1) return null;
432
+ if (node.id) return "#" + node.id;
433
+ var cn = node.getAttribute ? (node.getAttribute("class") || "") : "";
434
+ if (cn && cn.trim()) return "." + cn.trim().split(/\s+/)[0].replace(/[:>,\s]/g, "\\$&");
435
+ if (node.getAttribute) {
436
+ // 语义 data-* 钩子
437
+ var attrs = node.attributes;
438
+ for (var i = 0; i < attrs.length; i++) {
439
+ var n = attrs[i].nodeName;
440
+ if (n.indexOf("data-") === 0) return "[" + n + (attrs[i].nodeValue ? '="' + attrs[i].nodeValue + '"' : "") + "]";
441
+ }
442
+ }
443
+ return node.tagName ? node.tagName.toLowerCase() : null;
444
+ }
445
+ // 元素的「祖先链稳定路径」(向上最多 6 层,每层用最简稳定选择器)
446
+ function stablePathFor(el) {
447
+ var parts = [], p = el;
448
+ while (p && p !== document.body && parts.length < 6) {
449
+ var s = stableSelFor(p);
450
+ if (s) parts.unshift(s);
451
+ p = p.parentElement;
452
+ }
453
+ return parts.join(" > ") || (el.tagName ? el.tagName.toLowerCase() : "?");
454
+ }
455
+ // 把本次采集到的元素定位信息写入日志文件
456
+ function flushRecord(kind, name, selector) {
457
+ if (!RECORDING || !recSamples.length) return;
458
+ var payload = {
459
+ kind: kind || "REC_PICK",
460
+ t: new Date().toISOString(),
461
+ regionName: name || "",
462
+ regionSelector: selector || "",
463
+ samples: recSamples.slice()
464
+ };
465
+ try { fetch("/dsh-font-enhancer/log", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).catch(function(){}); } catch (e) {}
466
+ recSamples = [];
467
+ }
468
+
451
469
  function findRegion(id) {
452
470
  for (var i = 0; i < regions.length; i++) if (regions[i].id === id) return regions[i];
453
471
  return null;
@@ -458,6 +476,236 @@ window.__ModuleLoader__.load({
458
476
  function persist() {
459
477
  saveSettings({ regions: regions, themes: getThemes(), unified: state.unified, bx: state.bx, by: state.by });
460
478
  }
479
+
480
+ // ── Point-and-pick custom region: hover to preview, click to select, name ─
481
+ // User picks the exact UI text/elements they want as one region, then names
482
+ // it. Much finer-grained than auto-discovery and never floods the panel.
483
+ var __pick = null;
484
+ function pickRegion(targetRid) {
485
+ var target = (typeof targetRid === "string" && targetRid) ? findRegion(targetRid) : null;
486
+ if (__pick && __pick.active) { pickRegionExit(true); return; }
487
+ var toggle = document.getElementById("dsh-fe-toggle");
488
+ var pickbar = document.createElement("div");
489
+ pickbar.id = "dsh-fe-pickbar";
490
+ pickbar.style.cssText = "position:fixed;z-index:2147483647;left:50%;top:64px;display:flex;gap:8px;align-items:center;padding:10px 14px;background:rgba(18,20,26,.97);border:1px solid rgba(255,255,255,.16);border-radius:14px;box-shadow:0 12px 44px rgba(0,0,0,.7);color:#e6e6e6;font:13px/1.5 KaiTi,'Microsoft YaHei',sans-serif;pointer-events:auto;cursor:move";
491
+ var modeTip = target ? ('追加到区域「' + target.name + '」') : '拖动 ⠿ 移动工具条;Ctrl+点击多选';
492
+ var pickNamePh = target ? (target.name + '' ) : "给这个区域命名…";
493
+ pickbar.innerHTML =
494
+ '<span id="dsh-fe-pick-handle" title="拖动移动工具条" style="cursor:move;padding:0 8px 0 2px;color:#8b93a1;font-size:14px;user-select:none;-webkit-user-select:none">⠿</span>' +
495
+ '<span id="dsh-fe-pick-count">已选 <b>0</b> 个元素</span>' +
496
+ '<input id="dsh-fe-pick-name" placeholder="' + pickNamePh + '" style="width:180px;padding:6px 10px;border-radius:8px;border:1px solid rgba(255,255,255,.2);background:rgba(0,0,0,.45);color:#fff;font:inherit;font-size:13px">' +
497
+ '<button id="dsh-fe-pick-done" type="button" style="padding:6px 16px;border-radius:8px;border:1px solid rgba(106,166,255,.45);background:rgba(106,166,255,.25);color:#a8ccff;cursor:pointer;font:inherit;font-weight:600">✔ 完成</button>' +
498
+ '<button id="dsh-fe-pick-cancel" type="button" style="padding:6px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);color:#9aa;cursor:pointer;font:inherit">✕ 取消</button>' +
499
+ '<span style="font-size:11px;color:#8b93a1">' + modeTip + '</span>';
500
+ // center it initially (we dropped the translateX trick so dragging works)
501
+ pickbar.style.left = Math.max(8, Math.round((window.innerWidth - 420) / 2)) + "px";
502
+ var mount = document.documentElement || document.body;
503
+ mount.appendChild(pickbar);
504
+ // drag handle → move the whole pickbar without triggering element pick
505
+ var handle = document.getElementById("dsh-fe-pick-handle");
506
+ (function () {
507
+ var drag = false, sx = 0, sy = 0, ox = 0, oy = 0;
508
+ if (!handle) return;
509
+ handle.addEventListener("pointerdown", function (e) {
510
+ drag = true; sx = e.clientX; sy = e.clientY;
511
+ ox = pickbar.offsetLeft; oy = pickbar.offsetTop;
512
+ try { handle.setPointerCapture(e.pointerId); } catch (err) {}
513
+ e.preventDefault(); e.stopPropagation();
514
+ });
515
+ handle.addEventListener("pointermove", function (e) {
516
+ if (!drag) return;
517
+ var nx = Math.max(4, Math.min(window.innerWidth - pickbar.offsetWidth - 4, ox + (e.clientX - sx)));
518
+ var ny = Math.max(4, Math.min(window.innerHeight - pickbar.offsetHeight - 4, oy + (e.clientY - sy)));
519
+ pickbar.style.left = nx + "px"; pickbar.style.top = ny + "px";
520
+ });
521
+ function end() { drag = false; }
522
+ handle.addEventListener("pointerup", end);
523
+ handle.addEventListener("pointercancel", end);
524
+ })();
525
+
526
+ var picked = [];
527
+ var hoverEl = null;
528
+ function hilite(el) {
529
+ if (hoverEl && hoverEl !== el) {
530
+ hoverEl.style.outline = "";
531
+ hoverEl.style.outlineOffset = "";
532
+ }
533
+ if (el) { el.style.outline = "2px dashed #6aa6ff"; el.style.outlineOffset = "-1px"; }
534
+ hoverEl = el;
535
+ }
536
+ function onMove(e) {
537
+ var el = e.target;
538
+ if (!el || el === document.documentElement || el === document.body) return;
539
+ if (el.closest && el.closest("#dsh-fe-root, #dsh-fe-pickbar")) { hilite(null); return; }
540
+ if (el.nodeType === 1) hilite(el);
541
+ }
542
+ function onPick(e) {
543
+ var el = e.target;
544
+ if (!el || el.nodeType !== 1) return;
545
+ if (el.closest && el.closest("#dsh-fe-root, #dsh-fe-pickbar")) return;
546
+ e.preventDefault(); e.stopPropagation();
547
+ var multi = e.ctrlKey || e.metaKey || e.shiftKey;
548
+ var idx = picked.indexOf(el);
549
+ if (idx >= 0) {
550
+ // only un-pick on a plain click; Ctrl/Shift always keeps it selected
551
+ if (!multi) { picked.splice(idx, 1); el.style.outline = ""; }
552
+ } else {
553
+ picked.push(el);
554
+ // 采集模式:记录被框选元素的稳定定位路径
555
+ if (RECORDING) recSamples.push({ path: stablePathFor(el), tag: el.tagName, uid: "…下次分配" });
556
+ el.style.outline = multi ? "2px solid #ff8c5a" : "2px dotted #ffd27a";
557
+ el.style.outlineOffset = "-1px";
558
+ }
559
+ var c = document.getElementById("dsh-fe-pick-count");
560
+ if (c) c.innerHTML = "已选 <b>" + picked.length + "</b> 个元素";
561
+ }
562
+ function finish() {
563
+ var nameEl = document.getElementById("dsh-fe-pick-name");
564
+ var name = (nameEl && nameEl.value.trim()) || ("框选区域 " + (regions.length + 1));
565
+ if (!picked.length) { showToast("未选择任何元素", "err"); return; }
566
+ var marks = [];
567
+ picked.forEach(function (el) {
568
+ var m = uid();
569
+ el.setAttribute("data-fe-reg", m);
570
+ marks.push('[data-fe-reg="' + m + '"]');
571
+ el.style.outline = ""; el.style.outlineOffset = "";
572
+ });
573
+ var target = targetRid ? findRegion(targetRid) : null;
574
+ if (target) {
575
+ // Append the newly picked elements into an existing region instead of
576
+ // creating a new one ("关联框选" from that region's card).
577
+ var existing = (target.selector || "").split(",").map(function (s) { return s.trim(); }).filter(Boolean);
578
+ marks.forEach(function (m) { if (existing.indexOf(m) < 0) existing.push(m); });
579
+ target.selector = existing.join(", ");
580
+ target.enabled = true;
581
+ renderCards();
582
+ applyAll(regions);
583
+ persist();
584
+ pickRegionExit(true);
585
+ flushRecord("REC_ADD", target.name, target.selector);
586
+ showToast("已向区域「" + target.name + "」追加 " + picked.length + " 个元素", "ok");
587
+ return;
588
+ }
589
+ var nr = defaultRegion();
590
+ nr.id = uid();
591
+ nr.name = name;
592
+ nr.selector = marks.join(", ");
593
+ nr.enabled = true;
594
+ nr.useColor = false;
595
+ nr.color = "#e6e6e6";
596
+ nr.group = "框选";
597
+ // A user box-picked region REPLACES any preset/default region with the
598
+ // same name (so your custom "左侧导航栏" supersedes the built-in one).
599
+ regions = regions.filter(function (x) { return !(x.group && x.group !== "框选" && x.name === name); });
600
+ regions.push(nr);
601
+ renderCards();
602
+ applyAll(regions);
603
+ persist();
604
+ pickRegionExit(true);
605
+ flushRecord("REC_NEW", name, nr.selector);
606
+ // Bring the newly-added card into view and pulse it so the user can
607
+ // immediately find it and test font/color on it.
608
+ var newCard = cardsEl && cardsEl.querySelector('[data-card="' + nr.id + '"]');
609
+ if (newCard) {
610
+ try { newCard.scrollIntoView({ block: "nearest", behavior: "smooth" }); } catch (e) {}
611
+ newCard.style.transition = "box-shadow .4s";
612
+ newCard.style.boxShadow = "0 0 0 2px rgba(255,160,70,.7), 0 4px 18px rgba(0,0,0,.4)";
613
+ setTimeout(function () { if (newCard) newCard.style.boxShadow = ""; }, 1600);
614
+ }
615
+ showToast("已添加区域「" + name + "」(" + picked.length + " 个元素),可在卡片里改字体/颜色", "ok");
616
+ }
617
+ function pickRegionExit(silent) {
618
+ if (!__pick) return;
619
+ __pick.active = false;
620
+ document.removeEventListener("pointerover", __pick.onMove, true);
621
+ document.removeEventListener("click", __pick.onPick, true);
622
+ (picked || []).forEach(function (el) { try { el.style.outline = ""; el.style.outlineOffset = ""; } catch (e) {} });
623
+ var b = document.getElementById("dsh-fe-pickbar"); if (b) b.parentNode && b.parentNode.removeChild(b);
624
+ if (hoverEl) { try { hoverEl.style.outline = ""; } catch (e) {} }
625
+ var go = document.getElementById("dsh-fe-toggle");
626
+ if (go) { go.style.display = "block"; }
627
+ }
628
+ document.getElementById("dsh-fe-pick-done").addEventListener("click", finish);
629
+ document.getElementById("dsh-fe-pick-cancel").addEventListener("click", function () { pickRegionExit(true); });
630
+ document.addEventListener("pointerover", onMove, true);
631
+ document.addEventListener("click", onPick, true);
632
+ __pick = { active: true, onMove: onMove, onPick: onPick, exit: pickRegionExit };
633
+ if (toggle) toggle.style.display = "none";
634
+ }
635
+
636
+ // ── Show the elements a region currently applies to (like the pick outline) ─
637
+ // Lets the user see exactly which page elements a saved region covers, and
638
+ // doubles as a live check that the region's selector still matches anything.
639
+ function showScope(rid) {
640
+ if (!document.body || !rid) return;
641
+ var r = findRegion(rid);
642
+ if (!r) return;
643
+ var sel = (r.selector || "").trim();
644
+ if (!sel) { showToast("该区域没有选择器", "err"); return; }
645
+ var nodes;
646
+ try { nodes = document.querySelectorAll(sel); } catch (e) { showToast("选择器无效", "err"); return; }
647
+ var arr = Array.prototype.slice.call(nodes).filter(function (n) {
648
+ return n && n.nodeType === 1;
649
+ });
650
+ if (!arr.length) { showToast("该区域当前未匹配到任何元素(可能已失效)", "err"); return; }
651
+ arr.forEach(function (el) {
652
+ el.style.outline = "2px solid #3ddc84";
653
+ el.style.outlineOffset = "-1px";
654
+ el.style.transition = "outline-color .3s";
655
+ });
656
+ showToast("该区域覆盖 " + arr.length + " 个元素(绿色描边,2 秒后消失)", "ok");
657
+ // remember to clear after a beat; if another showScope fires we just reset
658
+ if (showScope._nodes) showScope._nodes.forEach(function (el) { try { el.style.outline = ""; el.style.outlineOffset = ""; } catch (e) {} });
659
+ showScope._nodes = arr;
660
+ clearTimeout(showScope._t);
661
+ showScope._t = setTimeout(function () {
662
+ (showScope._nodes || []).forEach(function (el) { try { el.style.outline = ""; el.style.outlineOffset = ""; } catch (e) {} });
663
+ showScope._nodes = null;
664
+ }, 2400);
665
+ }
666
+
667
+ // ── Region config management ──────────────────────────────────────────
668
+ // Replace: drop every non-box-picked region (defaults go away), keep picked.
669
+ function replaceWithPicked() {
670
+ var picked = regions.filter(function (x) { return x.group === "框选"; });
671
+ if (!picked.length) { showToast("当前没有框选区域,先新增框选", "err"); return; }
672
+ regions = picked;
673
+ renderCards(); applyAll(regions); persist();
674
+ showToast("已用「" + picked.length + " 个框选区域」取代默认区域", "ok");
675
+ }
676
+ // Restore: remove box-picked regions and reload the built-in defaults.
677
+ function restoreDefault() {
678
+ regions = DEFAULT_REGIONS();
679
+ renderCards(); applyAll(regions); persist();
680
+ showToast("已恢复默认区域(移除框选区域)", "ok");
681
+ }
682
+ // Save the whole current region set as a named preset ("配置方案").
683
+ // Save the whole current region set as a named preset ("保存配置方案").
684
+ // Reuses the theme-recipe shape so the saved plan can be applied back via
685
+ // the theme "应用" button too.
686
+ function saveConfig() {
687
+ var themes = getThemes();
688
+ var base = "配置方案";
689
+ var name = base + "1";
690
+ var n = 1;
691
+ while (themes.some(function (t) { return t.name === name; })) { n++; name = base + n; }
692
+ var recipe = {};
693
+ regions.forEach(function (r) {
694
+ recipe[r.name] = {
695
+ en: r.en, zh: r.zh, size: r.size, lh: r.lh,
696
+ weight: r.weight, italic: r.italic === true,
697
+ color: r.color, useColor: r.useColor === true
698
+ };
699
+ });
700
+ themes.push({ name: name, recipe: recipe, ts: Date.now() });
701
+ state.themes = themes;
702
+ renderThemeSel();
703
+ persist();
704
+ var sel = root.querySelector("#dsh-fe-theme-sel");
705
+ if (sel) sel.value = name;
706
+ showToast("已保存方案「" + name + "」(含全部区域字体/颜色)", "ok");
707
+ }
708
+
461
709
  function showToast(msg, type) {
462
710
  var t = document.getElementById("dsh-fe-toast");
463
711
  if (!t) return;
@@ -478,11 +726,14 @@ window.__ModuleLoader__.load({
478
726
  showToast("插件已刷新", "ok");
479
727
  } catch (e) {}
480
728
  }
481
- // Random vivid colors with golden-angle hue stepping so simultaneous picks
482
- // land far apart on the color wheel.
729
+ // A color drawn from the FULL hue wheel with random saturation/lightness,
730
+ // so every random pick can be green/blue/purple/yellow/red — not just a
731
+ // handful of near-red hues. `#ignore` kept for call-site compatibility.
483
732
  function randVivid(i) {
484
- var h = Math.floor((i * 137.508 + Math.random() * 40) % 360);
485
- return "hsl(" + h + ", 95%, 62%)";
733
+ var h = Math.floor(Math.random() * 360);
734
+ var s = 70 + Math.floor(Math.random() * 25); // 70–95%
735
+ var l = 42 + Math.floor(Math.random() * 28); // 42–70%
736
+ return "hsl(" + h + ", " + s + "%, " + l + "%)";
486
737
  }
487
738
  function randomizeFonts(rid) {
488
739
  var r = findRegion(rid);
@@ -683,6 +934,8 @@ window.__ModuleLoader__.load({
683
934
  '<input type="text" data-rid="' + r.id + '" data-fld="name" value="' + esc(r.name) + '" title="区域名称">' +
684
935
  "</div>" +
685
936
  '<div class="dsh-fe-head-right">' +
937
+ '<button type="button" data-rid="' + r.id + '" data-act="pickAdd" title="框选更多元素追加到这个区域">➕加元素</button>' +
938
+ '<button type="button" data-rid="' + r.id + '" data-act="showScope" title="高亮显示该区域覆盖的元素范围">👁范围</button>' +
686
939
  '<button type="button" data-rid="' + r.id + '" data-act="randomFont" title="随机字体">🎲字体</button>' +
687
940
  '<button type="button" data-rid="' + r.id + '" data-act="randomColor" title="随机颜色">🎨颜色</button>' +
688
941
  '<button type="button" data-rid="' + r.id + '" data-act="reset" title="重置本区域">⟳</button>' +
@@ -704,6 +957,9 @@ window.__ModuleLoader__.load({
704
957
  function buildUI() {
705
958
  if (document.getElementById("dsh-fe-root")) return;
706
959
  if (!document.body) return;
960
+ // DIAG: prove this client really runs in the browser by pinging host's
961
+ // client-alive route (host logs "CLIENT-ALIVE ping received from browser").
962
+ try { fetch("/dsh-font-enhancer/client-alive").catch(function(){}); } catch (e) {}
707
963
 
708
964
  var pc = document.createElement("style");
709
965
  pc.id = "dsh-fe-panel-css";
@@ -741,15 +997,28 @@ window.__ModuleLoader__.load({
741
997
  "</div>" +
742
998
  "</div>" +
743
999
  '<div class="dsh-fe-toolbar">' +
1000
+ '<button type="button" data-act="pickMode" title="在界面框选文字元素,合并成一个区域并命名">🖱 框选区域</button>' +
1001
+ '<button type="button" data-act="recordToggle" title="开启后框选/加元素会记录元素稳定定位到文件,用于排查定位区域是否跨重启失效">🔴 采集区域</button>' +
744
1002
  '<button type="button" data-act="randomAllFonts" title="给所有启用区域随机分配英/中文字体">🎲 随机字体</button>' +
745
1003
  '<button type="button" class="dsh-fe-btn-accent" data-act="randomAllColors" title="给所有启用区域随机分配颜色">🎨 随机颜色</button>' +
746
- '<button type="button" data-act="resetAll">恢复默认</button>' +
1004
+ "</div>" +
1005
+ '<div class="dsh-fe-toolbar dsh-fe-toolbar2">' +
1006
+ '<button type="button" data-act="replacePicked" title="删除全部默认区域,只保留你框选的区域">⟳ 框选取代默认</button>' +
1007
+ '<button type="button" data-act="restoreDefault" title="删除新增的框选区域,恢复内置默认区域">↺ 恢复默认区域</button>' +
1008
+ '<button type="button" class="dsh-fe-btn-accent" data-act="saveConfig" title="保存当前框选区域配置方案">💾 保存方案</button>' +
747
1009
  "</div>" +
748
1010
  '<div id="dsh-fe-cards"></div>' +
749
1011
  '<div id="dsh-fe-hint">勾选区域前的复选框展开该区域设置。🎲字体只随机英/中文字体,🎨颜色随机颜色。调好的配置可保存为主题随时切换。<br><br>💡 插件卡顿/卡住时:① 点击 DSH 左下角"重载"按钮;② 或点一下右下角 Aa 悬浮球把它折叠,即可刷新插件样式。</div>' +
750
1012
  '<div id="dsh-fe-toast"></div>' +
751
1013
  "</div>";
752
- document.body.appendChild(root);
1014
+ // Mount to the outermost <html> element, NOT document.body — DSH gives
1015
+ // the body/app containers `transform`/`filter`/`overflow` which make
1016
+ // position:fixed children (the orb + panel) position against a transformed
1017
+ // ancestor instead of the viewport, hiding them off screen. Appending to
1018
+ // <html> sidesteps that so the floating orb is always visible.
1019
+ var mount = document.documentElement || document.body;
1020
+ mount.appendChild(root);
1021
+ try { fetch("/dsh-font-enhancer/client-alive").catch(function(){}); } catch (e) {} // DIAG-2 root appended
753
1022
 
754
1023
  panel = root.querySelector("#dsh-fe-panel");
755
1024
  cardsEl = root.querySelector("#dsh-fe-cards");
@@ -782,13 +1051,23 @@ window.__ModuleLoader__.load({
782
1051
  });
783
1052
  }
784
1053
 
785
- // initial button position (saved, else default bottom-right)
1054
+ // initial button position (saved, else default bottom-right).
1055
+ // Clamp inside the viewport so a stale/off-screen saved position can
1056
+ // never hide the orb (it "disappears" when bx/by put it outside the window).
786
1057
  var btn = root.querySelector("#dsh-fe-toggle");
787
- if (typeof state.bx === "number" && typeof state.by === "number") {
1058
+ var bw = btn.offsetWidth || 54, bh = btn.offsetHeight || 54;
1059
+ if (typeof state.bx === "number" && typeof state.by === "number" &&
1060
+ state.bx >= 0 && state.by >= 0 &&
1061
+ state.bx <= Math.max(0, window.innerWidth - bw) &&
1062
+ state.by <= Math.max(0, window.innerHeight - bh)) {
788
1063
  btn.style.left = state.bx + "px";
789
1064
  btn.style.top = state.by + "px";
790
1065
  btn.style.right = "auto";
791
1066
  btn.style.bottom = "auto";
1067
+ } else {
1068
+ // default / invalid position → bottom-right (CSS default covers it)
1069
+ btn.style.left = "auto"; btn.style.top = "auto";
1070
+ btn.style.right = "18px"; btn.style.bottom = "18px";
792
1071
  }
793
1072
 
794
1073
  wireEvents();
@@ -896,8 +1175,11 @@ window.__ModuleLoader__.load({
896
1175
  if (v) v.textContent = r.weight;
897
1176
  } else if (fld === "color") {
898
1177
  r.color = el.value;
1178
+ r.useColor = true; // picking a color should always apply it
899
1179
  var dot = root.querySelector('[data-dot="' + rid + '"]');
900
1180
  if (dot) dot.style.background = r.color;
1181
+ var cb = root.querySelector('[data-rid="' + rid + '"][data-fld="useColor"]');
1182
+ if (cb) cb.checked = true;
901
1183
  }
902
1184
  else return;
903
1185
  applyAll(regions);
@@ -947,6 +1229,11 @@ window.__ModuleLoader__.load({
947
1229
  persist();
948
1230
  return;
949
1231
  }
1232
+ if (act === "pickMode") { pickRegion(); return; }
1233
+ if (act === "recordToggle") { recToggle(!RECORDING); return; }
1234
+ if (act === "replacePicked") { replaceWithPicked(); return; }
1235
+ if (act === "restoreDefault") { restoreDefault(); return; }
1236
+ if (act === "saveConfig") { saveConfig(); return; }
950
1237
  if (act === "randomAllFonts") { randomizeAllFonts(); return; }
951
1238
  if (act === "randomAllColors") { randomizeAllColors(); return; }
952
1239
  if (act === "themeSave") { saveTheme(); return; }
@@ -956,6 +1243,8 @@ window.__ModuleLoader__.load({
956
1243
 
957
1244
  var r = rid ? findRegion(rid) : null;
958
1245
  if (!r) return;
1246
+ if (act === "pickAdd") { pickRegion(rid); return; }
1247
+ if (act === "showScope") { showScope(rid); return; }
959
1248
  if (act === "delete") {
960
1249
  regions = regions.filter(function (x) {
961
1250
  return x.id !== rid;
@@ -989,6 +1278,7 @@ window.__ModuleLoader__.load({
989
1278
  window.addEventListener("resize", function () {
990
1279
  if (isOpen()) positionPanel();
991
1280
  });
1281
+ try { fetch("/dsh-font-enhancer/client-alive").catch(function(){}); } catch (e) {} // DIAG-3 buildUI complete
992
1282
  }
993
1283
 
994
1284
  // ---------- entry ----------
package/lib/index.js CHANGED
@@ -1,9 +1,97 @@
1
- // dsh-font-enhancer — host half.
2
- // This is a client-only plugin: all behavior lives in lib/client.js.
3
- // The host half only needs to be a valid Cordis module so the bundle loads.
4
- export const name = "dsh-font-enhancer";
5
- export const inject = [];
1
+ // dsh-font-enhancer - host half.
2
+ // Dual-face plugin (identical mount pattern to the verified dsh-chat-image-lightbox):
3
+ // host registers a loopback-only settings/health API so the bundle has a real cordis
4
+ // activate (inject=['webServer']); visual work lives in lib/client.js, mounted by the
5
+ // client-modules system once the host activates. Works on both web and desktop profiles.
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from "node:fs";
7
+ import { join, dirname } from "node:path";
8
+ import { homedir } from "node:os";
6
9
 
7
- export function apply(ctx) {
8
- ctx.logger.info("dsh-font-enhancer: host half loaded (client-only plugin)");
10
+ var SETTINGS_PREFIX = "/dsh-font-enhancer";
11
+ var SETTINGS_STORE = join(homedir(), ".dsh", "data", "dsh-font-enhancer", "settings.json");
12
+ var OPERATION_LOG = join(homedir(), ".dsh", "data", "dsh-font-enhancer", "operation.log");
13
+
14
+ export var name = "dsh-font-enhancer";
15
+ export var inject = ["webServer"];
16
+
17
+ function isLoopback(req) {
18
+ var a = (req.socket && req.socket.remoteAddress) || "";
19
+ var n = a.toLowerCase();
20
+ if (n === "::1") return true;
21
+ if (n.startsWith("::ffff:")) return n.slice(7).startsWith("127.");
22
+ return n.startsWith("127.");
23
+ }
24
+
25
+ function readSettings() {
26
+ try {
27
+ if (existsSync(SETTINGS_STORE)) return JSON.parse(readFileSync(SETTINGS_STORE, "utf8"));
28
+ } catch (e) {}
29
+ return { available: true };
9
30
  }
31
+
32
+ function writeSettings(data) {
33
+ try {
34
+ mkdirSync(dirname(SETTINGS_STORE), { recursive: true });
35
+ writeFileSync(SETTINGS_STORE, JSON.stringify(data, null, 2), "utf8");
36
+ return true;
37
+ } catch (e) { return false; }
38
+ }
39
+
40
+ export function apply(ctx) {
41
+ ctx.logger.info("dsh-font-enhancer: host half mounted (settings api + client)");
42
+ ctx.effect(function () {
43
+ var h1 = ctx.webServer.register({ kind: "exact", path: SETTINGS_PREFIX + "/health", handler: function (req, res) {
44
+ if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
45
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
46
+ res.end(JSON.stringify({ ok: true, plugin: "dsh-font-enhancer", version: "1.0.0" }));
47
+ }});
48
+ var h2 = ctx.webServer.register({ kind: "exact", path: SETTINGS_PREFIX + "/get", handler: function (req, res) {
49
+ if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
50
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
51
+ res.end(JSON.stringify(readSettings()));
52
+ }});
53
+ var h3 = ctx.webServer.register({ kind: "exact", path: SETTINGS_PREFIX + "/set", handler: function (req, res) {
54
+ if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
55
+ var chunks = [];
56
+ req.on("data", function (c) { chunks.push(c); });
57
+ req.on("end", function () {
58
+ try {
59
+ var body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
60
+ var ok = writeSettings(body);
61
+ res.writeHead(ok ? 200 : 500, { "content-type": "application/json; charset=utf-8" });
62
+ res.end(JSON.stringify({ ok: ok }));
63
+ } catch (e) {
64
+ res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
65
+ res.end(JSON.stringify({ ok: false, error: "bad json" }));
66
+ }
67
+ });
68
+ }});
69
+
70
+ var h4 = ctx.webServer.register({ kind: "exact", path: SETTINGS_PREFIX + "/client-alive", handler: function (req, res) {
71
+ ctx.logger.info("dsh-font-enhancer: CLIENT-ALIVE ping received from browser");
72
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
73
+ res.end(JSON.stringify({ ok: true }));
74
+ }});
75
+ var h5 = ctx.webServer.register({ kind: "exact", path: SETTINGS_PREFIX + "/log", handler: function (req, res) {
76
+ if (!isLoopback(req)) { res.writeHead(403); res.end(); return; }
77
+ var chunks = [];
78
+ req.on("data", function (c) { chunks.push(c); });
79
+ req.on("end", function () {
80
+ var ts = new Date().toISOString();
81
+ var line = "[" + ts + "] ";
82
+ try {
83
+ var body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
84
+ line += JSON.stringify(body);
85
+ } catch (e) { line += "[non-json] " + Buffer.concat(chunks).toString("utf8"); }
86
+ line += "\n";
87
+ try {
88
+ mkdirSync(dirname(OPERATION_LOG), { recursive: true });
89
+ appendFileSync(OPERATION_LOG, line, "utf8");
90
+ } catch (e) { ctx.logger.warn("dsh-font-enhancer: log write failed " + (e && e.message)); }
91
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
92
+ res.end(JSON.stringify({ ok: true }));
93
+ });
94
+ }});
95
+ return function () { h1(); h2(); h3(); h4(); h5(); };
96
+ }, "dsh-font-enhancer: settings routes");
97
+ }
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "dsh-font-enhancer",
3
- "version": "1.0.0",
4
- "description": "DIY 你的 DSH 界面:按区域自定义字体/字号/颜色,支持全局统一设置、主题保存切换、随机配色与字体。DIY your DSH UI fonts & colors: per-region font/size/color styling, unified overrides, theme save & switch, random font/color.",
3
+ "version": "1.1.0",
4
+ "description": "DIY 你的 DSH 界面:按区域自定义字体/字号/颜色,支持全局统一设置、主题保存切换、框选区域、随机配色与字体。DIY your DSH UI fonts & colors: per-region font/size/color styling, pick-region element selection, unified overrides, theme save & switch, random font/color.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
+ "sideEffects": false,
8
+ "engines": {
9
+ "node": ">=18"
10
+ },
7
11
  "exports": {
8
12
  ".": "./lib/index.js",
9
13
  "./client": "./lib/client.js",