dsh-font-enhancer 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-font-enhancer contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # dsh-font-enhancer
2
+
3
+ > 按区域分别设置 DSH 界面字体、字号、颜色,支持统一控制、主题保存/切换、随机配色/字体。
4
+
5
+ A DSH (DeepSeek Harness) plugin that lets you style the UI **per-region** — each
6
+ region (sidebar, message area, input box, code block, etc.) gets its own
7
+ 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.
10
+
11
+ ---
12
+
13
+ ## 功能 Features
14
+
15
+ ### 区域样式 Per-region Styling
16
+ - **15 pre-calibrated regions** covering the entire DSH Desktop UI:
17
+ Left sidebar, Logo, New Session, Taskboard entry, Workspace name, Settings,
18
+ Conversation messages, Session title, Composer/input, Tabs, Turn indicator,
19
+ Agent/model name, Right extension panel, Code blocks, Bottom expand bar.
20
+ - **Per-region controls**: Chinese font, English font, font-size, line-height,
21
+ font-weight (100–900 slider), italic toggle, text color with color picker.
22
+ - Each region has a **checkbox** — tick it to expand the settings panel for that region.
23
+
24
+ ### 全局统一 Unified Controls
25
+ - **Unified Color** checkbox: one color picker applies to **all** enabled regions.
26
+ - **Unified Font** checkbox: one font family + size applies to **all** enabled regions.
27
+ - When unified is ON, the randomizer randomizes the unified value (not per-region).
28
+
29
+ ### 主题系统 Theme System
30
+ - **Save** the current full configuration (all regions' fonts + colors) as a named theme.
31
+ - **Apply** any saved theme to restore its settings.
32
+ - **Rename** themes via the inline input + ✏ button.
33
+ - **Delete** unwanted themes.
34
+ - Themes survive DSH restarts and plugin version bumps.
35
+
36
+ ### 随机生成 Random Generation
37
+ - **🎲 Random Font** — randomize Chinese + English font for all (or one) enabled regions.
38
+ - **🎨 Random Color** — randomize vivid, high-distinction colors (golden-angle stepping).
39
+ - Per-region 🎲/🎨 buttons on each card head for single-region random.
40
+
41
+ ### 面板 UI Panel UI
42
+ - Floating **Aa** button (bottom-right, draggable like a pet).
43
+ - **Left-right layout** in card head: checkbox + color dot + name on the left,
44
+ 🎲font/🎨color/⟳reset/✕delete buttons on the right.
45
+ - **Card body** splits into two columns: fonts on the left, size/weight/italic/color
46
+ on the right.
47
+ - **Toast** notifications (no window.alert) for all save/apply/delete operations.
48
+ - **Reload** the plugin by collapsing the floating button (hide → rebuild).
49
+
50
+ ### 其他
51
+ - Font lists mirror Microsoft Word (Times New Roman, Arial, Calibri, 楷体, 宋体, 微软雅黑…).
52
+ - Default: English = Times New Roman, Chinese = 楷体 (KaiTi).
53
+ - All settings persist in `localStorage`; survive page refresh and DSH restart.
54
+ - Panel font is fixed to KaiTi (楷体) so it never changes with the plugin's own rules.
55
+
56
+ ---
57
+
58
+ ## 安装 Install
59
+
60
+ ```bash
61
+ # From the DSH profile directory:
62
+ dsh plugin --profile desktop add ./dsh-font-enhancer
63
+ ```
64
+
65
+ Or copy the folder into `node_modules/` and add the package name to the profile
66
+ `bundles` list, then restart DSH.
67
+
68
+ ### Requirements
69
+ - DSH Desktop 2.0+ (tested on `0.1.0-rc.6`).
70
+
71
+ ---
72
+
73
+ ## 用法 Usage
74
+
75
+ 1. Click the floating **Aa** button (bottom-right).
76
+ 2. **Tick** a region's checkbox → the settings panel expands below.
77
+ 3. Adjust Chinese/English font, size, line-height, weight, italic, and color.
78
+ 4. Use **统一颜色** / **统一字体** to apply a single setting to all regions.
79
+ 5. Save your configuration as a **theme** (type a name, click 💾存).
80
+ 6. Switch themes anytime via the dropdown + **应用** button.
81
+ 7. Use **🎲 随机字体** / **🎨 随机颜色** for quick experimentation.
82
+
83
+ ### 插件卡顿 Plugin stuck?
84
+ - Click the DSH "重载" (Reload) button, or
85
+ - Click the **Aa** floating button to collapse it — this triggers a full plugin rebuild.
86
+
87
+ ---
88
+
89
+ ## 原理 How it works
90
+
91
+ Pure **client-side** plugin. `lib/client.js` is a `__ModuleLoader__` bundle that:
92
+ 1. Injects a `<style>` tag with CSS rules per enabled region.
93
+ 2. Renders a floating control panel (vanilla JS, no framework).
94
+ 3. Uses `localStorage` for persistence with a `SCHEMA_VERSION` sentinel.
95
+ 4. `lib/index.js` is a minimal Cordis module so the bundle loads at all.
96
+
97
+ No host services, no external dependencies, no build step.
98
+
99
+ ---
100
+
101
+ ## 文件结构 File structure
102
+
103
+ ```
104
+ dsh-font-enhancer/
105
+ ├── package.json # Plugin manifest with dsh.bundle + dsh.client
106
+ ├── cordis.patch.yml # Cordis patch: registers the bundle
107
+ ├── lib/
108
+ │ ├── index.js # Host half (minimal Cordis module)
109
+ │ └── client.js # Client bundle (all UI + logic, ~1000 lines)
110
+ ├── README.md # This file
111
+ ├── LICENSE # MIT
112
+ └── .gitignore
113
+ ```
114
+
115
+ ---
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,6 @@
1
+ # dsh-font-enhancer — global Chinese/English font, size and line-height picker.
2
+ # Pure client-side plugin: it injects a floating font panel and a global
3
+ # stylesheet. No host services are required.
4
+ - insert:
5
+ - id: dsh-font-enhancer
6
+ name: dsh-font-enhancer
package/lib/client.js ADDED
@@ -0,0 +1,1008 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-font-enhancer",
3
+ factory: function (require) {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ // ---- Word-style font lists (label, css-family) ----
8
+ var EN_FONTS = [
9
+ ["Times New Roman", "Times New Roman"],
10
+ ["Arial", "Arial"],
11
+ ["Calibri", "Calibri"],
12
+ ["Cambria", "Cambria"],
13
+ ["Georgia", "Georgia"],
14
+ ["Verdana", "Verdana"],
15
+ ["Tahoma", "Tahoma"],
16
+ ["Segoe UI", "Segoe UI"],
17
+ ["Garamond", "Garamond"],
18
+ ["Book Antiqua", "Book Antiqua"],
19
+ ["Palatino Linotype", "Palatino Linotype"],
20
+ ["Trebuchet MS", "Trebuchet MS"],
21
+ ["Lucida Sans Unicode", "Lucida Sans Unicode"],
22
+ ["Comic Sans MS", "Comic Sans MS"],
23
+ ["Consolas (mono)", "Consolas"],
24
+ ["Courier New (mono)", "Courier New"],
25
+ ["Impact", "Impact"],
26
+ ["Bahnschrift", "Bahnschrift"]
27
+ ];
28
+ // Simplified-Chinese fonts shipped with Word / Windows.
29
+ // 楷体 (KaiTi) is first so it is both the default and the first option.
30
+ var ZH_FONTS = [
31
+ ["楷体 (KaiTi)", "KaiTi"],
32
+ ["微软雅黑 (Microsoft YaHei)", "Microsoft YaHei"],
33
+ ["宋体 (SimSun)", "SimSun"],
34
+ ["黑体 (SimHei)", "SimHei"],
35
+ ["新宋体 (NSimSun)", "NSimSun"],
36
+ ["仿宋 (FangSong)", "FangSong"],
37
+ ["微软雅黑 Light", "Microsoft YaHei Light"],
38
+ ["等线 (DengXian)", "DengXian"],
39
+ ["等线 Light", "DengXian Light"],
40
+ ["华文细黑 (STXihei)", "STXihei"],
41
+ ["华文宋体 (STSong)", "STSong"],
42
+ ["华文中宋 (STZhongsong)", "STZhongsong"],
43
+ ["华文楷体 (STKaiti)", "STKaiti"],
44
+ ["华文行楷 (STXingkai)", "STXingkai"],
45
+ ["隶书 (LiSu)", "LiSu"],
46
+ ["幼圆 (YouYuan)", "YouYuan"],
47
+ ["方正舒体 (FZShuTi)", "FZShuTi"],
48
+ ["方正姚体 (FZYaoti)", "FZYaoti"]
49
+ ];
50
+
51
+ var STORE_KEY = "dsh-font-enhancer";
52
+ // Bump this every time the built-in default region palette changes. A stored
53
+ // settings payload whose `ver` is older than this is treated as stale: the
54
+ // old region colors (e.g. the first cyan/blue scheme) are dropped and the
55
+ // latest DEFAULT_REGIONS() palette is loaded instead. This makes palette
56
+ // refreshes survive a restart without needing the user to clear localStorage.
57
+ var SCHEMA_VERSION = 19;
58
+
59
+ // ---------- helpers ----------
60
+ function esc(s) {
61
+ return String(s)
62
+ .replace(/&/g, "&amp;")
63
+ .replace(/</g, "&lt;")
64
+ .replace(/>/g, "&gt;")
65
+ .replace(/"/g, "&quot;");
66
+ }
67
+ function clamp(v, lo, hi) {
68
+ v = Number(v);
69
+ if (!isFinite(v)) v = lo;
70
+ return Math.max(lo, Math.min(hi, v));
71
+ }
72
+ function uid() {
73
+ return "r" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
74
+ }
75
+ function pickWeight() {
76
+ return [400, 500, 600, 700, 800][Math.floor(Math.random() * 5)];
77
+ }
78
+ function randFont(list) {
79
+ return list[Math.floor(Math.random() * list.length)][1];
80
+ }
81
+
82
+ // ---------- region model ----------
83
+ function defaultRegion() {
84
+ return {
85
+ id: uid(),
86
+ name: "新区域",
87
+ selector: "body",
88
+ enabled: true,
89
+ en: "Times New Roman",
90
+ zh: "KaiTi",
91
+ size: 15,
92
+ lh: 1.6,
93
+ weight: 400,
94
+ italic: false,
95
+ color: "#FFFFFF",
96
+ useColor: false,
97
+ group: "自定义",
98
+ _open: false
99
+ };
100
+ }
101
+ function normRegion(o) {
102
+ var d = defaultRegion();
103
+ if (!o || typeof o !== "object") return d;
104
+ if (typeof o.id === "string") d.id = o.id;
105
+ d.name = typeof o.name === "string" ? o.name : d.name;
106
+ d.selector = typeof o.selector === "string" ? o.selector : d.selector;
107
+ d.enabled = o.enabled !== false;
108
+ if (typeof o.en === "string") d.en = o.en;
109
+ if (typeof o.zh === "string") d.zh = o.zh;
110
+ d.size = clamp(o.size, 12, 22);
111
+ d.lh = clamp(o.lh, 1.3, 2.2);
112
+ d.weight = clamp(o.weight, 100, 900);
113
+ d.italic = o.italic === true;
114
+ if (typeof o.color === "string") d.color = o.color;
115
+ d.useColor = o.useColor === true;
116
+ if (typeof o.group === "string") d.group = o.group;
117
+ return d;
118
+ }
119
+ 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.
129
+ 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: "对话区" })
190
+ ];
191
+ }
192
+
193
+ function normThemes(o) {
194
+ if (!o || !Array.isArray(o.themes)) return [];
195
+ return o.themes.filter(function (t) {
196
+ return t && typeof t.name === "string" && t.recipe && typeof t.recipe === "object";
197
+ });
198
+ }
199
+ function normUnified(o) {
200
+ var u = { uc: { enabled: false, color: "#FFFFFF" }, uf: { enabled: false, en: "Times New Roman", zh: "KaiTi", size: 15 } };
201
+ if (!o || !o.unified || typeof o.unified !== "object") return u;
202
+ var s = o.unified;
203
+ if (s.uc && typeof s.uc === "object") {
204
+ u.uc.enabled = s.uc.enabled === true;
205
+ if (typeof s.uc.color === "string") u.uc.color = s.uc.color;
206
+ }
207
+ if (s.uf && typeof s.uf === "object") {
208
+ u.uf.enabled = s.uf.enabled === true;
209
+ if (typeof s.uf.en === "string") u.uf.en = s.uf.en;
210
+ if (typeof s.uf.zh === "string") u.uf.zh = s.uf.zh;
211
+ u.uf.size = clamp(s.uf.size, 12, 22);
212
+ }
213
+ return u;
214
+ }
215
+ function loadSettings() {
216
+ try {
217
+ var raw = localStorage.getItem(STORE_KEY);
218
+ if (!raw) return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), bx: null, by: null };
219
+ var o = JSON.parse(raw);
220
+ var themes = normThemes(o);
221
+ var unified = normUnified(o);
222
+ if (o && (typeof o.ver !== "number" || o.ver < SCHEMA_VERSION)) {
223
+ // Version bump: write back fresh region palette but KEEP themes &
224
+ // unified settings so users don't lose their saved themes.
225
+ var fresh = { regions: DEFAULT_REGIONS(), themes: themes, unified: unified, ver: SCHEMA_VERSION };
226
+ if (typeof o.bx === "number") fresh.bx = o.bx;
227
+ if (typeof o.by === "number") fresh.by = o.by;
228
+ localStorage.setItem(STORE_KEY, JSON.stringify(fresh));
229
+ 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
+ }
231
+ if (o && Array.isArray(o.regions)) {
232
+ return {
233
+ regions: o.regions.map(normRegion),
234
+ themes: themes,
235
+ unified: unified,
236
+ bx: typeof o.bx === "number" ? o.bx : null,
237
+ by: typeof o.by === "number" ? o.by : null
238
+ };
239
+ }
240
+ var g = defaultRegion();
241
+ g.selector = "body";
242
+ g.name = "全局界面(整站兜底)";
243
+ g.enabled = true;
244
+ g._open = true;
245
+ if (typeof o.en === "string") g.en = o.en;
246
+ if (typeof o.zh === "string") g.zh = o.zh;
247
+ g.size = clamp(o.size, 12, 22);
248
+ g.lh = clamp(o.lh, 1.3, 2.2);
249
+ g.weight = clamp(o.weight, 100, 900);
250
+ g.italic = o.italic === true;
251
+ return { regions: [g], themes: themes, unified: unified, bx: typeof o.bx === "number" ? o.bx : null, by: typeof o.by === "number" ? o.by : null };
252
+ } catch (e) {
253
+ return { regions: DEFAULT_REGIONS(), themes: [], unified: normUnified(null), bx: null, by: null };
254
+ }
255
+ }
256
+ function saveSettings(data) {
257
+ try {
258
+ data.ver = SCHEMA_VERSION;
259
+ localStorage.setItem(STORE_KEY, JSON.stringify(data));
260
+ } catch (e) {}
261
+ }
262
+
263
+ // ---------- CSS generation ----------
264
+ // No blanket descendant diffusion. Every region is scoped to its own
265
+ // text-bearing nodes only (the element itself + its direct inline/block
266
+ // text wrappers). This is what stops one region's rule from painting the
267
+ // children that belong to a SIBLING region (the previous withDesc() made
268
+ // every region apply "sel *" over all descendants, so body/sidebarCol
269
+ // recoloured every nested label and the regions bled into each other).
270
+ function withText(sel) {
271
+ return sel
272
+ .split(",")
273
+ .map(function (s) {
274
+ s = s.trim();
275
+ // body * 会匹配所有元素(包括输入框),用 !important 覆盖字号可能
276
+ // 破坏交互区域布局。body 只染 body 本身和 SVG text 元素就够了。
277
+ if (s === "body") return "body, body text";
278
+ return s + ", " + s + " *, " + s + " text";
279
+ })
280
+ .join(", ");
281
+ }
282
+ function buildRegionCss(r) {
283
+ var sel = (r.selector || "").trim();
284
+ if (!sel) return "";
285
+ var fam = '"' + r.en + '","' + r.zh + '",sans-serif';
286
+ var style = r.italic ? "italic" : "normal";
287
+ var scope = withText(sel);
288
+ var lines = [
289
+ scope + " {",
290
+ " font-family: " + fam + " !important;",
291
+ " font-weight: " + r.weight + " !important;",
292
+ " font-style: " + style + " !important;"
293
+ ];
294
+ if (r.useColor && r.color) lines.push(" color: " + r.color + " !important;");
295
+ lines.push(" font-size: " + r.size + "px !important;");
296
+ lines.push(" line-height: " + r.lh + " !important;");
297
+ lines.push("}");
298
+ return lines.join("\n");
299
+ }
300
+ function applyAll(regions) {
301
+ var u = state.unified || { uc: { enabled: false }, uf: { enabled: false } };
302
+ var css = regions
303
+ .filter(function (r) {
304
+ return r.enabled;
305
+ })
306
+ .map(function (r) {
307
+ // apply unified overrides without mutating the region objects
308
+ var tmp = {
309
+ selector: r.selector,
310
+ en: u.uf.enabled ? u.uf.en : r.en,
311
+ zh: u.uf.enabled ? u.uf.zh : r.zh,
312
+ size: u.uf.enabled ? u.uf.size : r.size,
313
+ weight: r.weight,
314
+ italic: r.italic,
315
+ lh: r.lh,
316
+ useColor: u.uc.enabled || r.useColor,
317
+ color: u.uc.enabled ? u.uc.color : r.color
318
+ };
319
+ return buildRegionCss(tmp);
320
+ })
321
+ .join("\n\n");
322
+ var tag = document.getElementById("dsh-font-enhancer-style");
323
+ if (!tag) {
324
+ tag = document.createElement("style");
325
+ tag.id = "dsh-font-enhancer-style";
326
+ tag.dataset.plugin = "dsh-font-enhancer";
327
+ document.head.appendChild(tag);
328
+ }
329
+ tag.textContent = css;
330
+ }
331
+
332
+ function optionsHtml(list, selected) {
333
+ return list
334
+ .map(function (f) {
335
+ var sel = f[1] === selected ? " selected" : "";
336
+ return '<option value="' + esc(f[1]) + '"' + sel + ">" + esc(f[0]) + "</option>";
337
+ })
338
+ .join("");
339
+ }
340
+
341
+ // ---------- panel styles — premium dark design-tool aesthetic ----------
342
+ function panelCss() {
343
+ return [
344
+ "#dsh-fe-root *{box-sizing:border-box;scrollbar-width:thin}",
345
+ "",
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)}",
353
+ "#dsh-fe-panel::-webkit-scrollbar{width:3px}",
354
+ "#dsh-fe-panel::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:3px}",
355
+ "#dsh-fe-panel::-webkit-scrollbar-track{background:transparent}",
356
+ "#dsh-fe-root,#dsh-fe-root *{font-family:KaiTi,'Microsoft YaHei',sans-serif!important;font-size:13px!important;line-height:1.5!important}",
357
+ "",
358
+ "/* ── panel header ── */",
359
+ "#dsh-fe-panel .dsh-fe-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid rgba(255,255,255,.06)}",
360
+ "#dsh-fe-panel .dsh-fe-header .dsh-fe-hl{display:flex;flex-direction:column;gap:2px}",
361
+ "#dsh-fe-panel .dsh-fe-header h4{margin:0;font-size:16px;font-weight:700;letter-spacing:0;color:#f0f2f5;line-height:1.2}",
362
+ "#dsh-fe-panel .dsh-fe-header .dsh-fe-sub{font-size:11px;color:#6b7280;font-weight:400;letter-spacing:.01em}",
363
+ "#dsh-fe-close{border:0;background:rgba(255,255,255,.06);color:#8b93a1;border-radius:8px;padding:5px 11px;font-size:14px;cursor:pointer;line-height:1.3;transition:all .15s;flex-shrink:0;margin-top:2px}",
364
+ "#dsh-fe-close:hover{background:rgba(255,255,255,.13);color:#e6e6e6}",
365
+ "",
366
+ "/* ── theme bar ── */",
367
+ "#dsh-fe-themeblock{display:flex;gap:5px;margin-bottom:14px;padding:10px 12px;background:linear-gradient(135deg,rgba(255,255,255,.04),rgba(255,255,255,.01));border-radius:12px;align-items:center;border:1px solid rgba(255,255,255,.05)}",
368
+ "#dsh-fe-themeblock select{flex:1;min-width:0;padding:5px 9px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:12px;appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg width='8' height='6' viewBox='0 0 8 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L4 4.5L7 1.5' stroke='%238b93a1' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 8px center;padding-right:28px;cursor:pointer;transition:border-color .15s}",
369
+ "#dsh-fe-themeblock select:focus{border-color:rgba(106,166,255,.5);outline:0}",
370
+ "#dsh-fe-themeblock button{padding:5px 9px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#cfd4dc;cursor:pointer;font:inherit;font-size:12px;white-space:nowrap;transition:all .15s;flex-shrink:0}",
371
+ "#dsh-fe-themeblock button:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.2)}",
372
+ "",
373
+ "/* ── toolbar ── */",
374
+ "#dsh-fe-toolbar{display:flex;gap:5px;margin-bottom:14px}",
375
+ "#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
+ "#dsh-fe-toolbar button:hover{background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.2);transform:translateY(-1px)}",
377
+ "#dsh-fe-toolbar button:active{transform:translateY(0)}",
378
+ "#dsh-fe-toolbar .dsh-fe-btn-accent{border-color:rgba(106,166,255,.3);background:linear-gradient(135deg,rgba(106,166,255,.12),rgba(106,166,255,.06));color:#8ab8ff;text-shadow:0 0 12px rgba(106,166,255,.2)}",
379
+ "#dsh-fe-toolbar .dsh-fe-btn-accent:hover{background:linear-gradient(135deg,rgba(106,166,255,.2),rgba(106,166,255,.1));border-color:rgba(106,166,255,.5);box-shadow:0 0 20px rgba(106,166,255,.08)}",
380
+ "",
381
+ "/* ── unified controls ── */",
382
+ "#dsh-fe-unified{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:10px 12px;margin-bottom:12px}",
383
+ "#dsh-fe-unified .dsh-fe-u-row{display:flex;align-items:center;gap:6px;margin-bottom:6px;flex-wrap:wrap}",
384
+ "#dsh-fe-unified .dsh-fe-u-row:last-child{margin-bottom:0}",
385
+ "#dsh-fe-unified label{font-size:11px;color:#8b93a1;display:flex;align-items:center;gap:4px;cursor:pointer;font-weight:500}",
386
+ "#dsh-fe-unified label input[type=checkbox]{accent-color:#6aa6ff;width:14px;height:14px;margin:0}",
387
+ "#dsh-fe-unified select,#dsh-fe-unified input[type=color],#dsh-fe-unified input[type=number]{padding:3px 6px;border-radius:6px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:11px;font-family:KaiTi,'Microsoft YaHei',sans-serif!important;transition:border-color .15s}",
388
+ "#dsh-fe-unified select:focus,#dsh-fe-unified input:focus{border-color:rgba(106,166,255,.45);outline:0}",
389
+ "#dsh-fe-unified input[type=color]{width:28px;height:22px;padding:0;cursor:pointer}",
390
+ "#dsh-fe-unified input[type=number]{width:48px}",
391
+ "#dsh-fe-unified .dsh-fe-u-label{font-size:10px;color:#6b7280;font-weight:600;letter-spacing:.03em;text-transform:uppercase;margin-right:2px;flex-basis:100%}",
392
+ "",
393
+ "/* ── card ── */",
394
+ "#dsh-fe-card{border:1px solid rgba(255,255,255,.05);border-radius:12px;padding:12px 14px;margin-bottom:8px;background:linear-gradient(135deg,rgba(255,255,255,.035),rgba(255,255,255,.015));transition:all .2s}",
395
+ "#dsh-fe-card:hover{border-color:rgba(255,255,255,.12);background:linear-gradient(135deg,rgba(255,255,255,.055),rgba(255,255,255,.025));box-shadow:0 2px 12px rgba(0,0,0,.15)}",
396
+ "#dsh-fe-card-head{display:flex;align-items:center;gap:8px;justify-content:space-between}",
397
+ "#dsh-fe-head-left{display:flex;align-items:center;gap:6px;flex:1;min-width:0}",
398
+ "#dsh-fe-head-right{display:flex;align-items:center;gap:4px;flex-shrink:0}",
399
+ "#dsh-fe-card-head .dsh-fe-cb{display:flex;align-items:center;cursor:pointer;flex-shrink:0;width:18px;height:18px;justify-content:center}",
400
+ "#dsh-fe-card-head .dsh-fe-cb input{accent-color:#6aa6ff;width:15px;height:15px;cursor:pointer;margin:0;transition:transform .15s}",
401
+ "#dsh-fe-card-head .dsh-fe-cb input:checked{transform:scale(1.18)}",
402
+ "#dsh-fe-card-head input[type=text]{flex:1;min-width:0;padding:4px 8px;border-radius:7px;border:1px solid rgba(255,255,255,.08);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:12px;font-weight:500;transition:all .15s}",
403
+ "#dsh-fe-card-head input[type=text]:focus{border-color:rgba(106,166,255,.45);background:rgba(0,0,0,.45);outline:0;box-shadow:0 0 0 3px rgba(106,166,255,.08)}",
404
+ "#dsh-fe-card-head button{border:0;background:rgba(255,255,255,.05);color:#8b93a1;border-radius:6px;padding:3px 7px;cursor:pointer;font-size:11px;transition:all .15s;flex-shrink:0;font-weight:500}",
405
+ "#dsh-fe-card-head button:hover{background:rgba(255,255,255,.13);color:#e6e6e6}",
406
+ "#dsh-fe-card-head .dsh-fe-del{color:#8b6c6c;font-size:13px;padding:4px 7px}",
407
+ "#dsh-fe-card-head .dsh-fe-del:hover{color:#d47a7a;background:rgba(200,80,80,.15)}",
408
+ "#dsh-fe-enb{display:flex;align-items:center;gap:4px;color:#8b93a1;font-size:11px;white-space:nowrap;cursor:pointer;padding:3px 7px;border-radius:6px;transition:all .15s;font-weight:500}",
409
+ "#dsh-fe-enb:hover{background:rgba(255,255,255,.06);color:#b9bfc9}",
410
+ "#dsh-fe-enb input{accent-color:#6aa6ff;width:13px;height:13px;margin:0}",
411
+ "#dsh-fe-dot{width:18px;height:18px;border-radius:50%;flex:none;border:2px solid rgba(255,255,255,.2);background:#888;transition:all .25s;box-shadow:0 0 8px rgba(0,0,0,.2)}",
412
+ "#dsh-fe-card:hover #dsh-fe-dot{border-color:rgba(255,255,255,.35);transform:scale(1.05)}",
413
+ "",
414
+ "/* ── card body ── */",
415
+ "#dsh-fe-card-body{display:flex;gap:12px;border-top:1px solid rgba(255,255,255,.05);margin-top:9px;padding-top:9px;animation:dsh-fe-fadeIn .18s ease}",
416
+ "@keyframes dsh-fe-fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}",
417
+ "#dsh-fe-body-col{flex:1;min-width:0}",
418
+ "#dsh-fe-body-col label{display:block;margin:6px 0 3px;color:#8b93a1;font-size:10px;font-weight:600;letter-spacing:.03em;text-transform:uppercase}",
419
+ "#dsh-fe-body-col select,#dsh-fe-body-col input[type=text],#dsh-fe-body-col input[type=number]{width:100%;padding:4px 8px;border-radius:7px;border:1px solid rgba(255,255,255,.08);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:12px;transition:all .15s}",
420
+ "#dsh-fe-body-col select:focus,#dsh-fe-body-col input:focus{border-color:rgba(106,166,255,.45);outline:0;box-shadow:0 0 0 3px rgba(106,166,255,.08)}",
421
+ "#dsh-fe-row{display:flex;gap:6px}",
422
+ "#dsh-fe-row>div{flex:1;min-width:0}",
423
+ "#dsh-fe-weightrow{margin-top:5px}",
424
+ "#dsh-fe-weightrow label{display:flex;justify-content:space-between;align-items:center;margin:0}",
425
+ "#dsh-fe-weight-val{color:#8b93a1;font-weight:500}",
426
+ "#dsh-fe-panel input[type=range]{width:100%;margin-top:5px;accent-color:#6aa6ff;height:3px;border-radius:2px;cursor:pointer}",
427
+ "#dsh-fe-italicrow,#dsh-fe-colorrow{display:flex;align-items:center;gap:6px;margin-top:6px;color:#8b93a1;font-size:12px;cursor:pointer;padding:3px 0;border-radius:6px;transition:all .15s}",
428
+ "#dsh-fe-italicrow:hover,#dsh-fe-colorrow:hover{color:#b9bfc9}",
429
+ "#dsh-fe-italicrow input,#dsh-fe-colorrow input{accent-color:#6aa6ff;width:14px;height:14px;margin:0}",
430
+ "#dsh-fe-colorrow input[type=color]{width:38px;height:28px;padding:0;border:1px solid rgba(255,255,255,.12);border-radius:8px;background:#1b1d22;cursor:pointer;transition:border-color .15s}",
431
+ "#dsh-fe-colorrow input[type=color]:hover{border-color:rgba(255,255,255,.25)}",
432
+ "",
433
+ "/* ── hint ── */",
434
+ "#dsh-fe-hint{margin-top:12px;font-size:10px;color:#6b7280;line-height:1.6;padding:10px 12px;background:rgba(255,255,255,.02);border-radius:10px;border:1px solid rgba(255,255,255,.04)}",
435
+ "",
436
+ "/* ── toast ── */",
437
+ "#dsh-fe-toast{position:fixed;left:50%;bottom:72px;z-index:2147483601;transform:translateX(-50%);padding:8px 18px;border-radius:10px;font:12px/1.4 KaiTi,'Microsoft YaHei',sans-serif;background:rgba(0,0,0,.85);border:1px solid rgba(255,255,255,.12);box-shadow:0 4px 20px rgba(0,0,0,.5);pointer-events:none;opacity:0;transition:opacity .25s;white-space:nowrap;color:#e6e6e6}",
438
+ "#dsh-fe-toast.dsh-fe-toast-ok{color:#7bc97b;border-color:rgba(123,201,123,.3)}",
439
+ "#dsh-fe-toast.dsh-fe-toast-err{color:#e08080;border-color:rgba(224,128,128,.3)}",
440
+ "#dsh-fe-toast.dsh-fe-toast-show{opacity:1}"
441
+ ].join("\n");
442
+ }
443
+
444
+ // ---------- state ----------
445
+ var state = loadSettings();
446
+ var regions = state.regions;
447
+ var root = null;
448
+ var panel = null;
449
+ var cardsEl = null;
450
+
451
+ function findRegion(id) {
452
+ for (var i = 0; i < regions.length; i++) if (regions[i].id === id) return regions[i];
453
+ return null;
454
+ }
455
+ function getThemes() {
456
+ return Array.isArray(state.themes) ? state.themes : [];
457
+ }
458
+ function persist() {
459
+ saveSettings({ regions: regions, themes: getThemes(), unified: state.unified, bx: state.bx, by: state.by });
460
+ }
461
+ function showToast(msg, type) {
462
+ var t = document.getElementById("dsh-fe-toast");
463
+ if (!t) return;
464
+ t.textContent = msg;
465
+ t.className = "dsh-fe-toast" + (type === "ok" ? " dsh-fe-toast-ok" : type === "err" ? " dsh-fe-toast-err" : "") + " dsh-fe-toast-show";
466
+ clearTimeout(t._hide);
467
+ t._hide = setTimeout(function () { t.className = "dsh-fe-toast"; }, 2500);
468
+ }
469
+ function reloadPlugin() {
470
+ try {
471
+ var r = document.getElementById("dsh-fe-root");
472
+ if (r) r.remove();
473
+ var pc = document.getElementById("dsh-fe-panel-css");
474
+ if (pc) pc.remove();
475
+ var st = document.getElementById("dsh-font-enhancer-style");
476
+ if (st) st.remove();
477
+ buildUI();
478
+ showToast("插件已刷新", "ok");
479
+ } catch (e) {}
480
+ }
481
+ // Random vivid colors with golden-angle hue stepping so simultaneous picks
482
+ // land far apart on the color wheel.
483
+ function randVivid(i) {
484
+ var h = Math.floor((i * 137.508 + Math.random() * 40) % 360);
485
+ return "hsl(" + h + ", 95%, 62%)";
486
+ }
487
+ function randomizeFonts(rid) {
488
+ var r = findRegion(rid);
489
+ if (!r) return;
490
+ r.en = randFont(EN_FONTS);
491
+ r.zh = randFont(ZH_FONTS);
492
+ renderCards();
493
+ applyAll(regions);
494
+ persist();
495
+ }
496
+ function randomizeColor(rid) {
497
+ var r = findRegion(rid);
498
+ if (!r) return;
499
+ r.useColor = true;
500
+ r.color = randVivid(0);
501
+ renderCards();
502
+ applyAll(regions);
503
+ persist();
504
+ }
505
+ function randomizeAllFonts() {
506
+ // 统一字体开着时,随机统一字体值(视觉立即可见),否则随机各区域
507
+ if (state.unified.uf.enabled) {
508
+ state.unified.uf.en = randFont(EN_FONTS);
509
+ state.unified.uf.zh = randFont(ZH_FONTS);
510
+ var se = root.querySelector("#dsh-fe-u-en");
511
+ var sz = root.querySelector("#dsh-fe-u-zh");
512
+ if (se) se.value = state.unified.uf.en;
513
+ if (sz) sz.value = state.unified.uf.zh;
514
+ applyAll(regions);
515
+ persist();
516
+ return;
517
+ }
518
+ regions.forEach(function (r) {
519
+ if (!r.enabled) return;
520
+ r.en = randFont(EN_FONTS);
521
+ r.zh = randFont(ZH_FONTS);
522
+ });
523
+ renderCards();
524
+ applyAll(regions);
525
+ persist();
526
+ }
527
+ function randomizeAllColors() {
528
+ // 统一颜色开着时,随机统一颜色值(视觉立即可见),否则随机各区域
529
+ if (state.unified.uc.enabled) {
530
+ state.unified.uc.color = randVivid(0);
531
+ var pc = root.querySelector("#dsh-fe-u-color-val");
532
+ if (pc) pc.value = state.unified.uc.color;
533
+ applyAll(regions);
534
+ persist();
535
+ return;
536
+ }
537
+ var i = 0;
538
+ regions.forEach(function (r) {
539
+ if (!r.enabled) return;
540
+ r.useColor = true;
541
+ r.color = randVivid(i++);
542
+ });
543
+ renderCards();
544
+ applyAll(regions);
545
+ persist();
546
+ }
547
+ function renderThemeSel() {
548
+ var sel = root ? root.querySelector("#dsh-fe-theme-sel") : null;
549
+ var inp = root ? root.querySelector("#dsh-fe-theme-name") : null;
550
+ if (!sel) return;
551
+ var opts = '<option value="">— 选择主题 —</option>';
552
+ getThemes().forEach(function (t) {
553
+ opts += '<option value="' + t.name.replace(/"/g, "&quot;") + '">' + esc(t.name) + "</option>";
554
+ });
555
+ sel.innerHTML = opts;
556
+ // auto-fill the name input with the default name
557
+ if (inp) inp.value = "主题 " + (getThemes().length + 1);
558
+ }
559
+ function saveTheme() {
560
+ var inp = root.querySelector("#dsh-fe-theme-name");
561
+ var name = inp ? String(inp.value).trim() : "";
562
+ if (!name) { showToast("请先在上方输入框填写主题名称", "err"); return; }
563
+ var recipe = {};
564
+ regions.forEach(function (r) {
565
+ recipe[r.name] = {
566
+ en: r.en, zh: r.zh, size: r.size, lh: r.lh,
567
+ weight: r.weight, italic: r.italic === true,
568
+ color: r.color, useColor: r.useColor === true
569
+ };
570
+ });
571
+ var th = getThemes();
572
+ // 只新增或覆盖同名主题,绝不删除其他主题
573
+ var found = false;
574
+ for (var i = 0; i < th.length; i++) {
575
+ if (th[i].name === name) { th[i].recipe = recipe; th[i].ts = Date.now(); found = true; break; }
576
+ }
577
+ if (!found) th.push({ name: name, recipe: recipe, ts: Date.now() });
578
+ state.themes = th;
579
+ renderThemeSel();
580
+ persist();
581
+ var sel = root.querySelector("#dsh-fe-theme-sel");
582
+ if (sel) sel.value = name;
583
+ showToast("已保存:" + name, "ok");
584
+ }
585
+ function themeRename() {
586
+ var sel = root.querySelector("#dsh-fe-theme-sel");
587
+ var oldName = sel && sel.value;
588
+ if (!oldName) { showToast("请先在下拉框选择要重命名的主题", "err"); return; }
589
+ var inp = root.querySelector("#dsh-fe-theme-name");
590
+ var newName = inp ? String(inp.value).trim() : "";
591
+ if (!newName) { showToast("请先在上方输入框填写新名称", "err"); return; }
592
+ if (oldName === newName) { showToast("新名称与旧名称相同", "err"); return; }
593
+ var th = getThemes();
594
+ var oldRecipe = null;
595
+ var found = -1;
596
+ for (var i = 0; i < th.length; i++) {
597
+ if (th[i].name === oldName) { oldRecipe = th[i].recipe; found = i; break; }
598
+ }
599
+ if (!oldRecipe) { showToast("未找到原主题数据", "err"); return; }
600
+ th.splice(found, 1);
601
+ var found2 = false;
602
+ for (var j = 0; j < th.length; j++) {
603
+ if (th[j].name === newName) { th[j].recipe = oldRecipe; th[j].ts = Date.now(); found2 = true; break; }
604
+ }
605
+ if (!found2) th.push({ name: newName, recipe: oldRecipe, ts: Date.now() });
606
+ state.themes = th;
607
+ renderThemeSel();
608
+ persist();
609
+ if (sel) sel.value = newName;
610
+ showToast("已重命名:" + oldName + " → " + newName, "ok");
611
+ }
612
+ function applyTheme() {
613
+ var sel = root.querySelector("#dsh-fe-theme-sel");
614
+ var name = sel && sel.value;
615
+ if (!name) { showToast("请先在下拉框选择要应用的主题", "err"); return; }
616
+ var t = null;
617
+ getThemes().forEach(function (x) { if (x.name === name) t = x; });
618
+ if (!t) { showToast("未找到主题数据,请重新保存", "err"); return; }
619
+ var matched = 0;
620
+ regions.forEach(function (r) {
621
+ var rc = t.recipe[r.name];
622
+ if (rc) {
623
+ matched++;
624
+ r.en = rc.en; r.zh = rc.zh; r.size = rc.size; r.lh = rc.lh;
625
+ r.weight = rc.weight; r.italic = rc.italic === true;
626
+ r.color = rc.color; r.useColor = rc.useColor === true;
627
+ }
628
+ });
629
+ // 应用主题时自动关闭统一颜色和统一字体,以免覆盖
630
+ if (state.unified.uc.enabled) {
631
+ state.unified.uc.enabled = false;
632
+ var ucEl = root.querySelector("#dsh-fe-u-color");
633
+ if (ucEl) ucEl.checked = false;
634
+ }
635
+ if (state.unified.uf.enabled) {
636
+ state.unified.uf.enabled = false;
637
+ var ufEl = root.querySelector("#dsh-fe-u-font");
638
+ if (ufEl) ufEl.checked = false;
639
+ }
640
+ renderCards();
641
+ applyAll(regions);
642
+ persist();
643
+ if (matched === 0) { showToast("主题 " + name + " 已应用(无匹配区域)", "ok"); return; }
644
+ showToast("已应用主题:" + name + "(" + matched + "个区域)", "ok");
645
+ }
646
+ function deleteTheme() {
647
+ var sel = root.querySelector("#dsh-fe-theme-sel");
648
+ var name = sel && sel.value;
649
+ if (!name) return;
650
+ state.themes = getThemes().filter(function (x) { return x.name !== name; });
651
+ renderThemeSel();
652
+ persist();
653
+ showToast("已删除:" + name, "ok");
654
+ }
655
+
656
+ // ---------- render region cards ----------
657
+ function cardHtml(r) {
658
+ var body = "";
659
+ if (r.enabled) {
660
+ body =
661
+ '<div class="dsh-fe-card-body">' +
662
+ '<div class="dsh-fe-body-col">' +
663
+ "<label>英文字体</label><select data-rid='" + r.id + "' data-fld='en'>" + optionsHtml(EN_FONTS, r.en) + "</select>" +
664
+ "<label>中文字体</label><select data-rid='" + r.id + "' data-fld='zh'>" + optionsHtml(ZH_FONTS, r.zh) + "</select>" +
665
+ "</div>" +
666
+ '<div class="dsh-fe-body-col">' +
667
+ '<div class="dsh-fe-row"><div><label>字号</label><input type="number" min="12" max="22" step="1" data-rid="' + r.id + '" data-fld="size" value="' + r.size + '"></div>' +
668
+ '<div><label>行距</label><input type="number" min="1.3" max="2.2" step="0.1" data-rid="' + r.id + '" data-fld="lh" value="' + r.lh + '"></div></div>' +
669
+ '<div class="dsh-fe-weightrow"><label>字重 <span data-rid="' + r.id + '" data-fld="weightVal">' + r.weight + "</span></label>" +
670
+ '<input type="range" min="100" max="900" step="100" data-rid="' + r.id + '" data-fld="weight" value="' + r.weight + '"></div>' +
671
+ '<label class="dsh-fe-italicrow"><input type="checkbox" data-rid="' + r.id + '" data-fld="italic"' + (r.italic ? " checked" : "") + "> 斜体</label>" +
672
+ '<label class="dsh-fe-colorrow"><input type="checkbox" data-rid="' + r.id + '" data-fld="useColor"' + (r.useColor ? " checked" : "") + "> 文字颜色</label>" +
673
+ '<input type="color" data-rid="' + r.id + '" data-fld="color" value="' + esc(r.color || "#e6e6e6") + '">' +
674
+ "</div>" +
675
+ "</div>";
676
+ }
677
+ return (
678
+ '<div class="dsh-fe-card" data-card="' + r.id + '">' +
679
+ '<div class="dsh-fe-card-head">' +
680
+ '<div class="dsh-fe-head-left">' +
681
+ '<label class="dsh-fe-cb"><input type="checkbox" data-rid="' + r.id + '" data-fld="enabled"' + (r.enabled ? " checked" : "") + "></label>" +
682
+ '<span class="dsh-fe-dot" data-dot="' + r.id + '" style="background:' + esc(r.color || "#888") + '"></span>' +
683
+ '<input type="text" data-rid="' + r.id + '" data-fld="name" value="' + esc(r.name) + '" title="区域名称">' +
684
+ "</div>" +
685
+ '<div class="dsh-fe-head-right">' +
686
+ '<button type="button" data-rid="' + r.id + '" data-act="randomFont" title="随机字体">🎲字体</button>' +
687
+ '<button type="button" data-rid="' + r.id + '" data-act="randomColor" title="随机颜色">🎨颜色</button>' +
688
+ '<button type="button" data-rid="' + r.id + '" data-act="reset" title="重置本区域">⟳</button>' +
689
+ '<button type="button" data-rid="' + r.id + '" data-act="delete" title="删除区域">✕</button>' +
690
+ "</div>" +
691
+ "</div>" +
692
+ body +
693
+ "</div>"
694
+ );
695
+ }
696
+ function renderCards() {
697
+ if (!cardsEl) return;
698
+ cardsEl.innerHTML = regions
699
+ .map(cardHtml)
700
+ .join("");
701
+ }
702
+
703
+ // ---------- build UI ----------
704
+ function buildUI() {
705
+ if (document.getElementById("dsh-fe-root")) return;
706
+ if (!document.body) return;
707
+
708
+ var pc = document.createElement("style");
709
+ pc.id = "dsh-fe-panel-css";
710
+ pc.dataset.plugin = "dsh-font-enhancer";
711
+ pc.textContent = panelCss();
712
+ document.head.appendChild(pc);
713
+
714
+ root = document.createElement("div");
715
+ root.id = "dsh-fe-root";
716
+ root.innerHTML =
717
+ '<button id="dsh-fe-toggle" type="button" title="字体区域设置 Font settings">Aa</button>' +
718
+ '<div id="dsh-fe-panel" style="display:none">' +
719
+ '<div class="dsh-fe-header"><div class="dsh-fe-hl"><h4>字体区域设置</h4><div class="dsh-fe-sub">每个区域独立调字体/颜色</div></div><button id="dsh-fe-close" type="button" title="关闭">✕</button></div>' +
720
+ '<div class="dsh-fe-themeblock">' +
721
+ '<select id="dsh-fe-theme-sel"></select>' +
722
+ '<input type="text" id="dsh-fe-theme-name" placeholder="主题名" style="width:60px;padding:4px 6px;border-radius:6px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.35);color:#e6e6e6;font:inherit;font-size:11px">' +
723
+ '<button type="button" data-act="themeSave" title="保存当前设置为选中主题">💾存</button>' +
724
+ '<button type="button" data-act="themeRename" title="重命名所选主题为输入框的名字">✏改</button>' +
725
+ '<button type="button" data-act="themeApply" title="应用选中主题">应用</button>' +
726
+ '<button type="button" data-act="themeDel" title="删除选中主题">🗑</button>' +
727
+ "</div>" +
728
+ '<div class="dsh-fe-unified" id="dsh-fe-unified">' +
729
+ '<div class="dsh-fe-u-row">' +
730
+ '<span class="dsh-fe-u-label">全局统一</span>' +
731
+ "</div>" +
732
+ '<div class="dsh-fe-u-row">' +
733
+ '<label><input type="checkbox" id="dsh-fe-u-color" data-u="uc" data-u-fld="enabled">统一颜色</label>' +
734
+ '<input type="color" id="dsh-fe-u-color-val" data-u="uc" data-u-fld="color" value="#FFFFFF">' +
735
+ "</div>" +
736
+ '<div class="dsh-fe-u-row">' +
737
+ '<label><input type="checkbox" id="dsh-fe-u-font" data-u="uf" data-u-fld="enabled">统一字体</label>' +
738
+ '<select id="dsh-fe-u-en" data-u="uf" data-u-fld="en" style="min-width:80px">' + optionsHtml(EN_FONTS, "Times New Roman") + "</select>" +
739
+ '<select id="dsh-fe-u-zh" data-u="uf" data-u-fld="zh" style="min-width:70px">' + optionsHtml(ZH_FONTS, "KaiTi") + "</select>" +
740
+ '<input type="number" id="dsh-fe-u-size" data-u="uf" data-u-fld="size" value="15" min="12" max="22" step="1" style="width:44px" title="字号">' +
741
+ "</div>" +
742
+ "</div>" +
743
+ '<div class="dsh-fe-toolbar">' +
744
+ '<button type="button" data-act="randomAllFonts" title="给所有启用区域随机分配英/中文字体">🎲 随机字体</button>' +
745
+ '<button type="button" class="dsh-fe-btn-accent" data-act="randomAllColors" title="给所有启用区域随机分配颜色">🎨 随机颜色</button>' +
746
+ '<button type="button" data-act="resetAll">恢复默认</button>' +
747
+ "</div>" +
748
+ '<div id="dsh-fe-cards"></div>' +
749
+ '<div id="dsh-fe-hint">勾选区域前的复选框展开该区域设置。🎲字体只随机英/中文字体,🎨颜色随机颜色。调好的配置可保存为主题随时切换。<br><br>💡 插件卡顿/卡住时:① 点击 DSH 左下角"重载"按钮;② 或点一下右下角 Aa 悬浮球把它折叠,即可刷新插件样式。</div>' +
750
+ '<div id="dsh-fe-toast"></div>' +
751
+ "</div>";
752
+ document.body.appendChild(root);
753
+
754
+ panel = root.querySelector("#dsh-fe-panel");
755
+ cardsEl = root.querySelector("#dsh-fe-cards");
756
+ renderCards();
757
+ renderThemeSel();
758
+ applyAll(regions);
759
+
760
+ // sync unified controls with saved state
761
+ var u = state.unified;
762
+ var ucEl = root.querySelector("#dsh-fe-u-color");
763
+ var ucVal = root.querySelector("#dsh-fe-u-color-val");
764
+ if (ucEl) ucEl.checked = u.uc.enabled;
765
+ if (ucVal) ucVal.value = u.uc.color;
766
+ var ufEl = root.querySelector("#dsh-fe-u-font");
767
+ var ufEn = root.querySelector("#dsh-fe-u-en");
768
+ var ufZh = root.querySelector("#dsh-fe-u-zh");
769
+ var ufSz = root.querySelector("#dsh-fe-u-size");
770
+ if (ufEl) ufEl.checked = u.uf.enabled;
771
+ if (ufEn) ufEn.value = u.uf.en;
772
+ if (ufZh) ufZh.value = u.uf.zh;
773
+ if (ufSz) ufSz.value = u.uf.size;
774
+
775
+ // Direct event listener for unified color picker (bypass delegation)
776
+ var ucPicker = root.querySelector("#dsh-fe-u-color-val");
777
+ if (ucPicker) {
778
+ ucPicker.addEventListener("input", function () {
779
+ state.unified.uc.color = ucPicker.value;
780
+ applyAll(regions);
781
+ persist();
782
+ });
783
+ }
784
+
785
+ // initial button position (saved, else default bottom-right)
786
+ var btn = root.querySelector("#dsh-fe-toggle");
787
+ if (typeof state.bx === "number" && typeof state.by === "number") {
788
+ btn.style.left = state.bx + "px";
789
+ btn.style.top = state.by + "px";
790
+ btn.style.right = "auto";
791
+ btn.style.bottom = "auto";
792
+ }
793
+
794
+ wireEvents();
795
+ }
796
+
797
+ // ---------- event wiring ----------
798
+ function wireEvents() {
799
+ var toggle = root.querySelector("#dsh-fe-toggle");
800
+
801
+ function show() {
802
+ panel.style.display = "block";
803
+ positionPanel();
804
+ }
805
+ function hide() {
806
+ panel.style.display = "none";
807
+ reloadPlugin();
808
+ }
809
+ function isOpen() {
810
+ return panel.style.display === "block";
811
+ }
812
+ function positionPanel() {
813
+ var r = toggle.getBoundingClientRect();
814
+ var pw = panel.offsetWidth || 320;
815
+ var ph = panel.offsetHeight || 360;
816
+ var left = r.left + r.width / 2 - pw / 2;
817
+ left = Math.max(8, Math.min(window.innerWidth - pw - 8, left));
818
+ var top = r.top - ph - 10;
819
+ if (top < 8) top = r.bottom + 10;
820
+ panel.style.left = left + "px";
821
+ panel.style.top = top + "px";
822
+ panel.style.right = "auto";
823
+ panel.style.bottom = "auto";
824
+ }
825
+
826
+ // ---- drag the button (pet-like) ----
827
+ var drag = { active: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
828
+ toggle.addEventListener("pointerdown", function (e) {
829
+ drag.active = true;
830
+ drag.moved = false;
831
+ drag.sx = e.clientX;
832
+ drag.sy = e.clientY;
833
+ var r = toggle.getBoundingClientRect();
834
+ drag.ox = r.left;
835
+ drag.oy = r.top;
836
+ try {
837
+ toggle.setPointerCapture(e.pointerId);
838
+ } catch (err) {}
839
+ toggle.classList.add("dsh-fe-dragging");
840
+ e.preventDefault();
841
+ });
842
+ toggle.addEventListener("pointermove", function (e) {
843
+ if (!drag.active) return;
844
+ var dx = e.clientX - drag.sx;
845
+ var dy = e.clientY - drag.sy;
846
+ if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 5) return;
847
+ drag.moved = true;
848
+ var nx = Math.max(0, Math.min(window.innerWidth - toggle.offsetWidth, drag.ox + dx));
849
+ var ny = Math.max(0, Math.min(window.innerHeight - toggle.offsetHeight, drag.oy + dy));
850
+ toggle.style.left = nx + "px";
851
+ toggle.style.top = ny + "px";
852
+ toggle.style.right = "auto";
853
+ toggle.style.bottom = "auto";
854
+ state.bx = Math.round(nx);
855
+ state.by = Math.round(ny);
856
+ persist();
857
+ // 面板展开时跟随悬浮球移动
858
+ if (isOpen()) positionPanel();
859
+ });
860
+ function endDrag(e) {
861
+ if (!drag.active) return;
862
+ drag.active = false;
863
+ toggle.classList.remove("dsh-fe-dragging");
864
+ try {
865
+ toggle.releasePointerCapture(e.pointerId);
866
+ } catch (err) {}
867
+ if (!drag.moved) {
868
+ if (isOpen()) hide();
869
+ else show();
870
+ }
871
+ }
872
+ toggle.addEventListener("pointerup", endDrag);
873
+ toggle.addEventListener("pointercancel", endDrag);
874
+
875
+ root.querySelector("#dsh-fe-close").addEventListener("click", hide);
876
+
877
+ // ---- delegated field edits (no re-render, keep focus) ----
878
+ function onField(e) {
879
+ var el = e.target;
880
+ var rid = el.getAttribute("data-rid");
881
+ var fld = el.getAttribute("data-fld");
882
+ if (!rid || !fld) return;
883
+ var r = findRegion(rid);
884
+ if (!r) return;
885
+ if (fld === "enabled") { r.enabled = el.checked; renderCards(); }
886
+ else if (fld === "italic") r.italic = el.checked;
887
+ else if (fld === "useColor") r.useColor = el.checked;
888
+ else if (fld === "name") r.name = el.value;
889
+ else if (fld === "selector") r.selector = el.value;
890
+ else if (fld === "en" || fld === "zh") r[fld] = el.value;
891
+ else if (fld === "size") r.size = clamp(el.value, 12, 22);
892
+ else if (fld === "lh") r.lh = clamp(el.value, 1.3, 2.2);
893
+ else if (fld === "weight") {
894
+ r.weight = clamp(el.value, 100, 900);
895
+ var v = root.querySelector('[data-rid="' + rid + '"][data-fld="weightVal"]');
896
+ if (v) v.textContent = r.weight;
897
+ } else if (fld === "color") {
898
+ r.color = el.value;
899
+ var dot = root.querySelector('[data-dot="' + rid + '"]');
900
+ if (dot) dot.style.background = r.color;
901
+ }
902
+ else return;
903
+ applyAll(regions);
904
+ persist();
905
+ }
906
+ root.addEventListener("input", onField);
907
+ root.addEventListener("change", onField);
908
+
909
+ // ---- unified controls ----
910
+ function onUnified(e) {
911
+ var el = e.target;
912
+ var u = el.getAttribute("data-u");
913
+ var fld = el.getAttribute("data-u-fld");
914
+ if (!u || !fld) return;
915
+ var sect = u === "uc" ? "uc" : "uf";
916
+ if (fld === "enabled") state.unified[sect].enabled = el.checked;
917
+ else if (fld === "color") state.unified[sect].color = el.value;
918
+ else if (fld === "en" || fld === "zh") state.unified[sect][fld] = el.value;
919
+ else if (fld === "size") state.unified[sect].size = clamp(el.value, 12, 22);
920
+ else return;
921
+ applyAll(regions);
922
+ persist();
923
+ }
924
+ root.addEventListener("input", onUnified);
925
+ root.addEventListener("change", onUnified);
926
+
927
+ // ---- theme select: auto-fill name input ----
928
+ var themeSel = root.querySelector("#dsh-fe-theme-sel");
929
+ if (themeSel) {
930
+ themeSel.addEventListener("change", function () {
931
+ var inp = root.querySelector("#dsh-fe-theme-name");
932
+ if (inp && themeSel.value) inp.value = themeSel.value;
933
+ });
934
+ }
935
+
936
+ // ---- delegated actions ----
937
+ root.addEventListener("click", function (e) {
938
+ var btn = e.target.closest ? e.target.closest("[data-act]") : null;
939
+ if (!btn) return;
940
+ var act = btn.getAttribute("data-act");
941
+ var rid = btn.getAttribute("data-rid");
942
+
943
+ if (act === "resetAll") {
944
+ regions = DEFAULT_REGIONS();
945
+ renderCards();
946
+ applyAll(regions);
947
+ persist();
948
+ return;
949
+ }
950
+ if (act === "randomAllFonts") { randomizeAllFonts(); return; }
951
+ if (act === "randomAllColors") { randomizeAllColors(); return; }
952
+ if (act === "themeSave") { saveTheme(); return; }
953
+ if (act === "themeRename") { themeRename(); return; }
954
+ if (act === "themeApply") { applyTheme(); return; }
955
+ if (act === "themeDel") { deleteTheme(); return; }
956
+
957
+ var r = rid ? findRegion(rid) : null;
958
+ if (!r) return;
959
+ if (act === "delete") {
960
+ regions = regions.filter(function (x) {
961
+ return x.id !== rid;
962
+ });
963
+ renderCards();
964
+ applyAll(regions);
965
+ persist();
966
+ } else if (act === "randomFont") {
967
+ randomizeFonts(rid);
968
+ } else if (act === "randomColor") {
969
+ randomizeColor(rid);
970
+ } else if (act === "reset") {
971
+ // reset styling + color — keep the calibrated name/selector/group set
972
+ r.en = "Times New Roman";
973
+ r.zh = "KaiTi";
974
+ r.size = 15;
975
+ r.lh = 1.6;
976
+ r.weight = 400;
977
+ r.italic = false;
978
+ r.useColor = false;
979
+ r.color = "#e6e6e6";
980
+ renderCards();
981
+ applyAll(regions);
982
+ persist();
983
+ }
984
+ });
985
+
986
+ document.addEventListener("keydown", function (e) {
987
+ if (e.key === "Escape" && isOpen()) hide();
988
+ });
989
+ window.addEventListener("resize", function () {
990
+ if (isOpen()) positionPanel();
991
+ });
992
+ }
993
+
994
+ // ---------- entry ----------
995
+ var inject = [];
996
+ function apply() {
997
+ if (document.readyState === "loading") {
998
+ document.addEventListener("DOMContentLoaded", buildUI);
999
+ } else {
1000
+ buildUI();
1001
+ }
1002
+ }
1003
+
1004
+ exports.apply = apply;
1005
+ exports.inject = inject;
1006
+ return module.exports;
1007
+ }
1008
+ });
package/lib/index.js ADDED
@@ -0,0 +1,9 @@
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 = [];
6
+
7
+ export function apply(ctx) {
8
+ ctx.logger.info("dsh-font-enhancer: host half loaded (client-only plugin)");
9
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
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.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib/**/*.js",
14
+ "cordis.patch.yml",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "license": "MIT",
19
+ "keywords": [
20
+ "dsh-plugin",
21
+ "deepseek-harness",
22
+ "font",
23
+ "typography",
24
+ "theme",
25
+ "ui",
26
+ "chinese-font",
27
+ "english-font",
28
+ "color-theme",
29
+ "font-picker",
30
+ "text-styling"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/loyalchiiina/dsh-font-enhancer.git"
35
+ },
36
+ "homepage": "https://github.com/loyalchiiina/dsh-font-enhancer#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/loyalchiiina/dsh-font-enhancer/issues"
39
+ },
40
+ "dsh": {
41
+ "bundle": {
42
+ "patch": "./cordis.patch.yml"
43
+ },
44
+ "client": {
45
+ "inject": ["@deepseek-ai/dsh-client-runtime"],
46
+ "platform": "web"
47
+ }
48
+ }
49
+ }