dsh-folia 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.beginner.md +13 -13
  2. package/README.en.md +25 -25
  3. package/README.md +378 -62
  4. package/assets/about/README.md +89 -0
  5. package/assets/readme/mascot-eating-rice-1280x1784.jpg +0 -0
  6. package/cordis.patch.yml +16 -86
  7. package/lib/about/README.md +2 -2
  8. package/lib/client.js +3736 -89
  9. package/lib/components/about-notice/index.js +125 -0
  10. package/lib/components/about-notice/routes/about-qr.js +92 -0
  11. package/lib/components/about-notice/routes/github-stars.js +143 -0
  12. package/lib/components/fonts/client.build.json +22 -0
  13. package/lib/components/fonts/index.js +129 -0
  14. package/lib/components/fonts/lib/client.fonts.js +656 -0
  15. package/lib/components/fonts/routes/fontsets.js +415 -0
  16. package/lib/components/fonts/routes/system-fonts.js +530 -0
  17. package/lib/components/fonts/src/appearance-font-section.js +389 -0
  18. package/lib/components/fonts/src/client.js +82 -0
  19. package/lib/components/fonts/src/fontset-editor.js +209 -0
  20. package/lib/components/fx/client.build.json +16 -0
  21. package/lib/components/fx/index.js +44 -0
  22. package/lib/components/fx/lib/client.fx.js +847 -0
  23. package/lib/components/fx/src/client.js +75 -0
  24. package/lib/components/fx/src/ext-fx.js +155 -0
  25. package/lib/components/fx/src/fx-layer.js +635 -0
  26. package/lib/components/mascot-avatar/index.js +147 -0
  27. package/lib/components/mascot-avatar/routes/avatar.js +137 -0
  28. package/lib/components/mascot-avatar/routes/mascot.js +124 -0
  29. package/lib/components/media-bridge/audio-tap.swift +146 -0
  30. package/lib/components/media-bridge/backend.js +144 -0
  31. package/lib/components/media-bridge/index.js +129 -0
  32. package/lib/components/media-bridge/legacy.js +467 -0
  33. package/lib/components/media-bridge/provision.js +301 -0
  34. package/lib/components/media-bridge/routes/now-playing.js +177 -0
  35. package/lib/components/media-bridge/supervisor.js +765 -0
  36. package/lib/components/parallax/client.build.json +16 -0
  37. package/lib/components/parallax/index.js +44 -0
  38. package/lib/components/parallax/lib/client.parallax.js +1492 -0
  39. package/lib/components/parallax/src/client.js +60 -0
  40. package/lib/components/parallax/src/ext-parallax.js +148 -0
  41. package/lib/components/parallax/src/parallax-layer.js +1299 -0
  42. package/lib/components/pet-link/client.build.json +21 -0
  43. package/lib/components/pet-link/index.js +34 -0
  44. package/lib/components/pet-link/lib/client.pet-link.js +641 -0
  45. package/lib/components/pet-link/official-provider.js +151 -0
  46. package/lib/components/pet-link/prepared.js +10 -0
  47. package/lib/components/pet-link/routes/official-pet.js +118 -0
  48. package/lib/components/pet-link/src/client.js +55 -0
  49. package/lib/components/pet-link/src/official-pet.js +130 -0
  50. package/lib/components/pet-link/src/pet-link-actions.json +18 -0
  51. package/lib/components/pet-link/src/pet-link-timings.json +61 -0
  52. package/lib/components/pet-link/src/pet-link-tuner.js +142 -0
  53. package/lib/components/pet-link/src/pet-link.js +295 -0
  54. package/lib/diag-trace.js +1 -1
  55. package/lib/faststart.js +3 -3
  56. package/lib/host-boot.js +3 -3
  57. package/lib/index.js +51 -13
  58. package/lib/inventory.js +2 -2
  59. package/lib/kernel/component-session.js +3 -3
  60. package/lib/kernel/config.js +4 -4
  61. package/lib/kernel/data-dir.js +55 -9
  62. package/lib/kernel/host-session.js +4 -4
  63. package/lib/log.js +3 -3
  64. package/lib/media/frame-store.js +2 -2
  65. package/lib/media/media-dirs.js +1 -1
  66. package/lib/media/transcode.js +10 -10
  67. package/lib/notice.js +5 -5
  68. package/lib/routes/cache-dir.js +2 -2
  69. package/lib/routes/inventory.js +93 -33
  70. package/lib/routes/live-frame.js +1 -1
  71. package/lib/routes/media-bytes.js +1 -1
  72. package/lib/routes/occlusion.js +2 -2
  73. package/lib/routes/props.js +1 -1
  74. package/lib/routes/settings.js +3 -3
  75. package/lib/routes/we-assets.js +1 -1
  76. package/lib/serve.js +1 -1
  77. package/lib/settings-schema.js +5 -5
  78. package/lib/startup-sweep.js +1 -1
  79. package/lib/steam-discovery.js +5 -5
  80. package/lib/subpackage-boot.js +11 -11
  81. package/lib/types/client.d.ts +2 -2
  82. package/lib/types/index.d.ts +15 -15
  83. package/lib/upload-dir.js +3 -3
  84. package/lib/we-assets.js +4 -4
  85. package/lib/webwallgl/renderer.js +1 -1
  86. package/package.json +149 -22
@@ -0,0 +1,389 @@
1
+ /**
2
+ * appearance-font-section.js — 外观页签第六节「全局字体」的渲染器(P-C 搬出)。
3
+ *
4
+ * 原来住在 src/panel-tabs-appearance.js 的 54-420 行,**函数体逐字未改**,只是换了住处。
5
+ *
6
+ * 跨包形状(契约 §2.1 / §2.2):
7
+ * · 本文件**只画界面**:稳定零件(React / weT / switchRow / ctlText / sectionHead /
8
+ * moduleResetButton / 字族与主题数据表)由入口 src/client.js 从 host 的**显式键集**解构,
9
+ * 落在同一个工厂作用域里 —— 所以这里照旧按裸名引用,不 import、不 require。
10
+ * · 每次渲染才存在的值与意图回调(sel / fontSet / onThemeFamily / onToggleFontSection …)
11
+ * 仍走 ctx(主包在渲染处构造)。
12
+ * · 屏上顺序由入口登记的描述符决定:不写 order ⇒ 排在五个内置节之后(见 panel-tabs.js)。
13
+ *
14
+ * ⚠️ 归属(§6.1 = A / ADR-0013 D20):本节只画界面。字体的**键与状态真源**在主包
15
+ * `lib/settings-schema.js` 的 `FONTSET_KEYS` 与 `fontsets/<id>.json`;本文件不持键、
16
+ * 不落盘、不注册 settings 段 —— 读值只走渲染 ctx 与入口解构的 host 显式键集。
17
+ */
18
+ // ── 字族下拉的**选项**(一处生成,四张表共用)────────────────────────────────
19
+ // 两组:内置族键(FONT_FAMILY_LABELS,栈写死在 client.js)+ **本机字体**
20
+ // (宿主枚举出来的族名,经 systemFontKeyOf 变成 `sys:` 族键,见 src/system-fonts.js)。
21
+ // ⚠️ 本机那一组先过 **`filterUsableSystemFonts`**:只留**本浏览器真的能匹配**的名字 ——
22
+ // 系统列出的族名里有相当一部分(系统保留字体、同一字体的另一种写法)浏览器压根取不到,
23
+ // 摆出来就是"选了没反应"(现场缺陷:"控制台切换了字体,特殊文字还是显示成口")。
24
+ // 量不到时(替身 DOM / 无布局)它原样返回 ⇒ 这里不会因为"测不出来"而少给。
25
+ // 本机那一组可能为空(清单还在路上 / 取不到)—— 空组**不渲染** optgroup
26
+ //(给一个拉不动的分组比没有这个分组更让人困惑),状态由 sysFontNote 那一行说。
27
+ // `skipInherit`:全局字体那一行不要「默认」项(它在那里等于"不覆盖",与「跟随 DSH」同义)。
28
+ // `current`:**当前值**。它即使不在清单里(换了机器 / 之前选过一个本浏览器取不到的名字)
29
+ // 也必须摆进去 —— 否则 `<select>` 会显示成第一项(「跟随」)而值其实还存着,界面在撒谎。
30
+ function familyOptions(sel, opts) {
31
+ const o = opts || {};
32
+ const builtin = o.skipInherit
33
+ ? FONT_FAMILY_LABELS.filter((f) => f.v !== "inherit") : FONT_FAMILY_LABELS;
34
+ const nodes = builtin.map((f) => React.createElement("option", { key: f.v, value: f.v }, f.label));
35
+ const sys = filterUsableSystemFonts(sel.systemFonts).fonts
36
+ .map((name) => ({ v: systemFontKeyOf(name), label: name }))
37
+ .filter((x) => x.v);
38
+ // ⚠️ 局部名不叫 `current`:那是本仓 `MUTABLE_ALIASES` 里的一员(picker 的 ctx 别名),
39
+ // `verify-client` 的"渲染器不得改写别名"判据会把这个**声明**误认成赋值。
40
+ const curKey = typeof o.current === "string" ? o.current : "";
41
+ if (curKey && !builtin.some((f) => f.v === curKey) && !sys.some((x) => x.v === curKey)) {
42
+ const name = systemFontNameOf(curKey);
43
+ if (name) sys.unshift({ v: curKey, label: name });
44
+ }
45
+ if (sys.length) {
46
+ nodes.push(React.createElement("optgroup", { key: "__sys", label: weT("本机字体") },
47
+ sys.map((x) => React.createElement("option", { key: x.v, value: x.v }, x.label))));
48
+ }
49
+ return nodes;
50
+ }
51
+
52
+ /**
53
+ * 本机字体那一组的**状态行**(空串 = 没什么要说的)。
54
+ * 四种要说话的情形都是"用户会以为坏了"的:还在扫、取不到、**按文件名推测**出来的清单、
55
+ * 以及**有名字被本浏览器过滤掉** —— 最后这种必须说:否则用户找 `Apple Color Emoji` 找不到,
56
+ * 只会以为清单又漏了,而真相是这个名字在本浏览器里取不到。
57
+ */
58
+ function sysFontNote(sel) {
59
+ if (sel.systemFontsLoading) return weT("正在读取本机字体…");
60
+ if (sel.systemFontsError) return weT("本机字体读不到:{why}", { why: sel.systemFontsError });
61
+ if (sel.systemFontsApproximate && (sel.systemFonts || []).length) {
62
+ return weT("本机字体是按文件名推测的(没拿到系统字体清单)");
63
+ }
64
+ const probe = filterUsableSystemFonts(sel.systemFonts);
65
+ if (probe.skipped > 0) {
66
+ return weT("已略过 {count} 个本浏览器取不到的字体名(选了也不会生效)", { count: probe.skipped });
67
+ }
68
+ return "";
69
+ }
70
+
71
+ /** 本机字体那一组的「重新扫描」(刚装完字体时的出路;在途时禁用)。 */
72
+ function sysFontRefresh(sel, onRefresh) {
73
+ return React.createElement("button", {
74
+ key: "sys-font-refresh",
75
+ type: "button",
76
+ className: "we-picker__chip",
77
+ disabled: sel.systemFontsLoading === true,
78
+ onClick: () => onRefresh(),
79
+ title: weT("重新读取本机已安装的字体(装了新字体之后用)"),
80
+ }, weT("重新扫描"));
81
+ }
82
+
83
+ // ── 全局 / 终端字体两行 + 本机字体状态行 ──
84
+ function renderFontGlobalFamily(sel, onGlobalFamily, onComponentFamily, onRefreshSystemFonts) {
85
+ return React.createElement(React.Fragment, null,
86
+ // ── 全局字体 / 终端字体:本机字体的两个入口 ──────────────────────────────
87
+ // 两行一个形状。**全局**是"默认"不是"强制":角色表与组件表里单独设过的仍以那里为准
88
+ // (解析在 src/font/typography.js 的 buildTypePayload —— 角色没设才落到全局)。
89
+ // **终端**写的就是「高级字体设置 → 终端」那一行的同一个键
90
+ // (`componentFonts.terminal.family`):两处是同一个值的两个入口,不会漂。
91
+ React.createElement("div", { className: "we-picker__ctl", key: "global-family" },
92
+ ctlText(weT("默认字体"), weT("整套界面的默认字族(全局);角色 / 组件里单独设过的仍以那里为准")),
93
+ React.createElement("select", {
94
+ value: sanitizeFamilyKey(sel.globalFamily),
95
+ style: { width: "150px" },
96
+ onChange: (e) => onGlobalFamily(e.target.value),
97
+ title: weT("整套界面(含角色表覆盖不到的文字)的默认字体;任意角色 / 组件单独设了字族,那里优先"),
98
+ },
99
+ React.createElement("option", { value: "" }, weT("跟随 DSH")),
100
+ familyOptions(sel, { skipInherit: true, current: sanitizeFamilyKey(sel.globalFamily) }),
101
+ ),
102
+ ),
103
+ React.createElement("div", { className: "we-picker__ctl", key: "terminal-family" },
104
+ ctlText(weT("终端字体"), weT("对话里的终端块 + 侧栏终端面板(dsh-ssh);与「高级字体设置 → 终端」同一项")),
105
+ React.createElement("select", {
106
+ value: fontFamilyKeyOf((sel.componentFonts.terminal || {}).family),
107
+ style: { width: "150px" },
108
+ onChange: (e) => onComponentFamily("terminal", e.target.value),
109
+ title: weT("终端字体:① 对话里的终端块(走官方 --dsl-terminal-font 钩子)② 侧栏 / SSH 终端面板(走 dsh-ssh 给皮肤留的 --dsh-ssh-terminal-font 钩子);「跟随」= 都不覆盖,各用它们自己的默认。⚠️ 若在 dsh-ssh 的设置里填过 terminalFontFamily,那个值优先级更高"),
110
+ },
111
+ React.createElement("option", { value: "" }, weT("跟随")),
112
+ familyOptions(sel, { current: fontFamilyKeyOf((sel.componentFonts.terminal || {}).family) }),
113
+ ),
114
+ ),
115
+ // 「本机字体」那一组的状态行 + 重新扫描:在途 / 取不到 / 按文件名推测都要说出来
116
+ //(后两种用户会以为坏了;推测那种不说的话,用户会以为系统里真有那个族名)。
117
+ React.createElement("div", { className: "we-picker__row", key: "sys-font-note" },
118
+ React.createElement("span", { className: "we-picker__hint" }, sysFontNote(sel)),
119
+ sysFontRefresh(sel, onRefreshSystemFonts),
120
+ ),
121
+ );
122
+ }
123
+
124
+ // ── 文字颜色角色:逐角色上色 + 深色单独设置 ──
125
+ function renderFontColorRoles(sel, officialColorOf, onThemeDarkSeparate, onThemeColor, onThemeColorClear) {
126
+ return React.createElement(React.Fragment, null,
127
+ // 分角色上色 —— 每个角色独立放开(不把四个角色压成同一个色而压平 DSH 的四级层次),
128
+ // 留空 = 跟随原生。经 theme 令牌层生效:body 内联、免 !important、{light,dark} 随配色自动换值。
129
+ React.createElement("div", { className: "we-picker__ctl we-picker__ctl--wrap" },
130
+ ctlText(weT("文字颜色角色"), weT("未设置 = 用 DSH 默认色(色块显示当前值)")),
131
+ ),
132
+ switchRow(weT("深色单独设置"), sel.themeDarkSeparate, (e) => onThemeDarkSeparate(e.target.checked), {
133
+ tooltip: weT("关闭时一个颜色同时用于浅色与深色两套(内部仍存两套值);开启后浅色/深色分别设置"),
134
+ }),
135
+ THEME_COLOR_ROLES.map((role) => {
136
+ const v = sel.themeColors[role.id] || { light: "", dark: "" };
137
+ return React.createElement("div", { className: "we-picker__ctl", key: role.id },
138
+ ctlText(role.label),
139
+ React.createElement("label", { className: "we-picker__swatch-custom" },
140
+ React.createElement("input", {
141
+ type: "color",
142
+ value: v.light || officialColorOf(role.tokens) || "#ffffff",
143
+ onInput: (e) => onThemeColor(role.id, "light", e.target.value, sel.themeDarkSeparate),
144
+ onChange: (e) => onThemeColor(role.id, "light", e.target.value, sel.themeDarkSeparate),
145
+ title: weT("{role} · 浅色配色", { role: weT(role.label) }),
146
+ }),
147
+ React.createElement("span", { className: "we-picker__hint we-picker__value" },
148
+ v.light ? weT("浅 {color}", { color: v.light }) : (officialColorOf(role.tokens) || weT("跟随"))),
149
+ ),
150
+ sel.themeDarkSeparate && React.createElement("label", { className: "we-picker__swatch-custom" },
151
+ React.createElement("input", {
152
+ type: "color",
153
+ value: v.dark || officialColorOf(role.tokens) || "#000000",
154
+ onInput: (e) => onThemeColor(role.id, "dark", e.target.value, true),
155
+ onChange: (e) => onThemeColor(role.id, "dark", e.target.value, true),
156
+ title: weT("{role} · 深色配色", { role: weT(role.label) }),
157
+ }),
158
+ React.createElement("span", { className: "we-picker__hint we-picker__value" },
159
+ v.dark ? weT("深 {color}", { color: v.dark }) : (officialColorOf(role.tokens) || weT("跟随"))),
160
+ ),
161
+ (v.light || v.dark) && React.createElement("button", {
162
+ type: "button",
163
+ className: "we-picker__chip",
164
+ onClick: () => onThemeColorClear(role.id),
165
+ title: weT("清除该角色,回到原生层次"),
166
+ }, weT("清除")),
167
+ );
168
+ }),
169
+ );
170
+ }
171
+
172
+ // ── 排版角色(F2/G4):字号 / 字重 / 字族表 ──
173
+ function renderFontTypeRoles(sel, typeRoles, onThemeTypeOnly, onThemeSize, onThemeWeight, onThemeFamily) {
174
+ return React.createElement(React.Fragment, null,
175
+ // F2/G4:排版角色(**绝对字号**)。用户口径:字号用绝对值、默认值可见 ——
176
+ // 未填时输入框显示 DSH 官方字号(角色表的 defaultPx),清空即回它。
177
+ // 已确认接受的副作用:设过绝对值的角色不再随 DSH「通用 → 字号」缩放(未设的照旧跟随);
178
+ // 行高一律沿用 DSH 的令牌,不随绝对值缩放。我们始终**不写** --dsh-content-font-size(红线 3)。
179
+ React.createElement("div", { className: "we-picker__ctl we-picker__ctl--wrap" },
180
+ ctlText(weT("排版角色"), weT("字号 px;留空 = 不改(字号/字重显示 DSH 当前值,字族选「跟随」)")),
181
+ ),
182
+ switchRow(weT("只看改过的"), sel.themeTypeOnly, (e) => onThemeTypeOnly(e.target.checked), {
183
+ tooltip: weT("只列出改过字号/字重/字族的角色,便于收尾核对"),
184
+ }),
185
+ // 表格化:表头放「字号 / 字重 / 字体」,一行一个角色 —— 三项在固定列上对齐,
186
+ // 比每行重复三个无标签控件好扫读(颜色角色那张形状不同,仍用行式)。
187
+ // 「只看改过的」**默认开** ⇒ 一行都没改过时表是空的:那种"空表"必须有话说,
188
+ // 否则看着像坏了(这也是把默认值翻成开之后必须同时补的一件事)。
189
+ typeRoles.length === 0
190
+ ? React.createElement("div", { className: "we-picker__hint" },
191
+ weT("没有改过的角色 —— 「只看改过的」正开着(共 {count} 个角色)。关掉它就能看到全部。", { count: THEME_TYPE_ROLES.length }))
192
+ : null,
193
+ React.createElement("table", { className: "we-picker__font-table" },
194
+ React.createElement("thead", null,
195
+ React.createElement("tr", null,
196
+ React.createElement("th", null, weT("角色")),
197
+ React.createElement("th", null, weT("字号")),
198
+ React.createElement("th", null, weT("字重")),
199
+ React.createElement("th", null, weT("字体")),
200
+ ),
201
+ ),
202
+ React.createElement("tbody", null,
203
+ typeRoles
204
+ .map((role) => {
205
+ const size = sel.themeSize[role.id];
206
+ return React.createElement("tr", { key: role.id },
207
+ React.createElement("td", null, ctlText(role.label)),
208
+ React.createElement("td", null, React.createElement("input", {
209
+ type: "number",
210
+ value: size === undefined ? role.defaultPx : size,
211
+ min: THEME_SIZE_MIN,
212
+ max: THEME_SIZE_MAX,
213
+ step: 1,
214
+ style: { width: "46px" },
215
+ onChange: (e) => onThemeSize(role.id, e.target.value),
216
+ title: weT("{role}:字号 px(清空即回 DSH 默认 {px}px)", { role: weT(role.label), px: role.defaultPx }),
217
+ })),
218
+ React.createElement("td", null, React.createElement("input", {
219
+ type: "number",
220
+ value: sel.themeWeight[role.id] === undefined
221
+ ? (role.prefix ? Number(role.prefix) : 400)
222
+ : sel.themeWeight[role.id],
223
+ min: 100,
224
+ max: 900,
225
+ step: 100,
226
+ style: { width: "54px" },
227
+ onChange: (e) => onThemeWeight(role.id, e.target.value),
228
+ title: weT("{role}:字重 100–900(清空即回默认)", { role: weT(role.label) }),
229
+ })),
230
+ React.createElement("td", null, React.createElement("select", {
231
+ value: sel.themeFamily[role.id] === undefined ? "" : sel.themeFamily[role.id],
232
+ style: { width: "92px" },
233
+ onChange: (e) => onThemeFamily(role.id, e.target.value),
234
+ title: weT("{role}:字族(跟随 = 不覆盖,用 DSH 该角色的字族)", { role: weT(role.label) }),
235
+ },
236
+ React.createElement("option", { value: "" }, weT("跟随")),
237
+ familyOptions(sel, { current: sel.themeFamily[role.id] || "" }),
238
+ )),
239
+ );
240
+ }),
241
+ ),
242
+ ),
243
+ );
244
+ }
245
+
246
+ // ── 组件字体(G3/G4,高级):按组件细化表 ──
247
+ function renderFontComponents(sel, onFontAdvanced, onComponentFont, onComponentFamily) {
248
+ return React.createElement(React.Fragment, null,
249
+ // G3/G4:组件字体 —— 属"高级",收进本区内的「高级字体设置」子分支
250
+ //(是"字体"的子分支,**不是**「高级」页签)。三条来自静态分析的纪律:
251
+ // ① 命中靠启动自探测(未命中的组件整条不生效,改名即降级、不误伤);
252
+ // ② 代码块/终端的字体来自后代 `font:` 简写 ⇒ 只有官方 `--dsl-*` 钩子这条腿有效;
253
+ // ③ 不填 = 不生成规则(DSH 官方值)。
254
+ switchRow(weT("高级字体设置"), sel.fontAdvanced, (e) => onFontAdvanced(e.target.checked), {
255
+ tooltip: weT("按组件细化(模块前缀通道 + 官方 --dsl-* 钩子):只对探测到的组件生效;不填即用 DSH 默认值"),
256
+ }),
257
+ sel.fontAdvanced && React.createElement(React.Fragment, null,
258
+ React.createElement("div", { className: "we-picker__ctl we-picker__ctl--wrap" },
259
+ ctlText(weT("组件字体"), weT("未填时显示该组件当前的 DSH 默认值(— = 此刻不在页面上);清空即回默认")),
260
+ ),
261
+ // 表格化:与「排版角色」表同构 —— 表头放「字号 / 字重 / 字体」,一行一个组件。
262
+ React.createElement("table", { className: "we-picker__font-table" },
263
+ React.createElement("thead", null,
264
+ React.createElement("tr", null,
265
+ React.createElement("th", null, weT("组件")),
266
+ React.createElement("th", null, weT("字号")),
267
+ React.createElement("th", null, weT("字重")),
268
+ React.createElement("th", null, weT("字体")),
269
+ ),
270
+ ),
271
+ React.createElement("tbody", null,
272
+ COMPONENT_FONT_TARGETS.map((target) => {
273
+ const c = sel.componentFonts[target.id] || {};
274
+ // 未填时**直接显示 DSH 当前默认值**(启动自探测时顺带读回的 computed 值)。
275
+ const d = (typeof componentFontDefaults === "function"
276
+ ? componentFontDefaults()[target.id] : null) || {};
277
+ const famKey = fontFamilyKeyOf(c.family);
278
+ return React.createElement("tr", { key: target.id },
279
+ React.createElement("td", null, ctlText(weT(target.label), weT("{group} · 走 {route}", { group: weT(target.group), route: target.route }))),
280
+ React.createElement("td", null, React.createElement("input", {
281
+ type: "number",
282
+ value: c.size === undefined ? (d.size || "") : c.size,
283
+ placeholder: d.size ? "" : "—",
284
+ min: 6,
285
+ max: 40,
286
+ style: { width: "44px" },
287
+ onChange: (e) => onComponentFont(target.id, "size", e.target.value),
288
+ title: weT("{target}:字号 px(清空即回 DSH 默认{suffix})", { target: weT(target.label), suffix: d.size ? " " + d.size + "px" : "" }),
289
+ })),
290
+ React.createElement("td", null, React.createElement("input", {
291
+ type: "number",
292
+ value: c.weight === undefined ? (d.weight || "") : c.weight,
293
+ placeholder: d.weight ? "" : "—",
294
+ min: 100,
295
+ max: 900,
296
+ step: 100,
297
+ style: { width: "54px" },
298
+ onChange: (e) => onComponentFont(target.id, "weight", e.target.value),
299
+ title: weT("{target}:字重 100–900(清空即回 DSH 默认{suffix})", { target: weT(target.label), suffix: d.weight ? " " + d.weight : "" }),
300
+ })),
301
+ React.createElement("td", null, React.createElement("select", {
302
+ value: famKey,
303
+ style: { width: "96px" },
304
+ onChange: (e) => onComponentFamily(target.id, e.target.value),
305
+ title: weT("{target}:字体族(跟随 = 不覆盖,用 DSH 默认)", { target: weT(target.label) }),
306
+ },
307
+ React.createElement("option", { value: "" }, weT("跟随")),
308
+ familyOptions(sel, { current: famKey }),
309
+ )),
310
+ );
311
+ }),
312
+ ),
313
+ ),
314
+ ),
315
+ );
316
+ }
317
+
318
+ // ── 字体集预设:总开关 + 编辑器 ──
319
+ function renderFontSet(fontSet) {
320
+ return React.createElement(React.Fragment, null,
321
+ // 字重**没有全局值**(一个全局值会把 DSH 的粗细层次压成一档):按角色(下面「排版角色」
322
+ // 每行一个输入框)与按组件(「高级字体设置」里每组件一项)细化,都能填任意值;
323
+ // 留空 /「恢复默认」即回 DSH 官方字重。
324
+ // 字族同样无全局值:按角色(「排版角色」每行的字族下拉)与按组件(「高级字体设置」里每项
325
+ // 的下拉)设置 —— 全局字族只经 body 继承,既压平 DSH 的字体栈层次,又够不到用 `font:` 简写的标题/表格/代码。
326
+ // ── 字体集(**「字体自定义」的附属**):总开关管"要不要自定义",这里管"用哪一整套"。
327
+ // 放在 `sel.fontCustom` 这一支**里面** —— 关掉自定义就整块收起(那时这一整套并不
328
+ // 生效,摆出来只会让人以为它在起作用)。
329
+ switchRow(weT("字体集预设"), fontSet.open, (e) => fontSet.onOpen(e.target.checked), {
330
+ tooltip: weT("预设 = 一整套字体外观;改动只落到当前这一套,随时可以恢复原样"),
331
+ }),
332
+ fontSet.open && renderFontSetEditor(fontSet),
333
+ );
334
+ }
335
+
336
+ function renderAppearanceFontSection(ctx) {
337
+ const { officialColorOf, onComponentFamily, onComponentFont, onFontAdvanced, onFontResetAll, onGlobalFamily, onRefreshSystemFonts, onResetModule, onThemeColor, onThemeColorClear, onThemeDarkSeparate, onThemeFamily, onThemeSize, onThemeTypeOnly, onThemeWeight, onToggleFontCustom, fontOpen, onToggleFontSection, fontSet, sel } = ctx;
338
+ // 「排版角色」表要按「只看改过的」筛,而**筛完是空**时要单独给一行提示 ⇒ 先算出来再渲染表。
339
+ // ⚠️ 必须在 `React.createElement(...)` **之前**算(写成参数位置上的赋值表达式 ——
340
+ // 赋值表达式的值是那个**数组本身**,于是它被当成一个子节点 ⇒ React #31「对象不能作为子节点」;
341
+ // 空数组时看不出来,一旦有筛出来的角色就整块面板崩掉)。
342
+ const typeRoles = THEME_TYPE_ROLES.filter((role) => !sel.themeTypeOnly
343
+ || sel.themeSize[role.id] !== undefined
344
+ || sel.themeWeight[role.id] !== undefined
345
+ || sel.themeFamily[role.id] !== undefined);
346
+ // ── 折叠态:整节一个开关(`fontOpen`,**默认收起** —— 调节项太多,默认全开太杂乱)。
347
+ // 开合状态住 localStorage,渲染器只经 ctx 读它 + 调 onToggleFontSection,保持纯读契约。
348
+ // ⚠️ 两档各有自己的键(侧栏 `qp-font-open` / 设置页 `picker-font-open`)—— 与
349
+ // `qp-tab` / `picker-tab` 同一条"每档各自的 UI 状态"口径;两档默认值都是收起。
350
+ const open = fontOpen === true;
351
+ return React.createElement(React.Fragment, null,
352
+ // ── 字体 (custom typography):总开关(关 = 恢复 dsh 原生字体)+ 颜色角色 / 排版角色 /
353
+ // 字体族 / 组件字体(高级),开启时才渲染细节控件。字重不设全局值:按角色与按组件细化。
354
+ // ⚠️ 分档(ADR-0008 D4 **三次修订**,用户口径):这一节**两档都画**
355
+ // (侧栏 + 设置页),无 surface 门。二次修订曾把它单向迁进侧栏,随后用户在设置页
356
+ // 找不到该节、报"字体自定义被干掉了" ⇒ 用户口径"两边都有"。两档都**默认收起**
357
+ // (节头点击展开),开合状态各记各的键。──
358
+ React.createElement("div", { className: "we-picker__section" },
359
+ // 节头一律走共用构件(`sectionHead`):折叠三角与模块级 ↺ 的**次序只有一份实现**
360
+ // ——手写副本正是次序会漂的地方(一处改了另一处不改 ⇒ 只有部分模块对齐)。
361
+ sectionHead(weT("全局字体"), open, onToggleFontSection, moduleResetButton("font", onResetModule)),
362
+ open && switchRow(weT("字体自定义"), sel.fontCustom, (e) => onToggleFontCustom(e.target.checked), {
363
+ tooltip: weT("关闭后恢复 dsh 默认字体外观;开启后可调颜色角色、排版角色(字号/字重/字族)与组件字体"),
364
+ }),
365
+ // 「恢复默认」只在总开关开启时出现:关闭时字体本就是 DSH 默认值,摆一个"恢复默认"
366
+ // 没有意义(也会让人以为关掉开关还残留了什么自定义)。
367
+ open && sel.fontCustom && React.createElement("div", { className: "we-picker__ctl" },
368
+ React.createElement("button", {
369
+ type: "button",
370
+ className: "we-picker__chip",
371
+ onClick: onFontResetAll,
372
+ title: weT("清空所有字体自定义项(颜色角色 / 排版 / 字重 / 字族 / 组件字体),回到 DSH 默认"),
373
+ }, weT("恢复默认")),
374
+ ),
375
+ open && sel.fontCustom && React.createElement(React.Fragment, null,
376
+ renderFontGlobalFamily(sel, onGlobalFamily, onComponentFamily, onRefreshSystemFonts),
377
+ renderFontColorRoles(sel, officialColorOf, onThemeDarkSeparate, onThemeColor, onThemeColorClear),
378
+ renderFontTypeRoles(sel, typeRoles, onThemeTypeOnly, onThemeSize, onThemeWeight, onThemeFamily),
379
+ renderFontComponents(sel, onFontAdvanced, onComponentFont, onComponentFamily),
380
+ renderFontSet(fontSet),
381
+ ),
382
+ ),
383
+ );
384
+ }
385
+
386
+ // 独立文件形态下必须有导出:本文件被内联进产物时这一行会被构建器剥掉
387
+ // (scripts/build-client.mjs 的 EXPORT_BLOCK,同 packages/fonts/src/fontset-editor.js:205 的先例),
388
+ // 但渲染挂载台(test/verify-scene-live.mjs 等)是**按文件 import** 的,没有它就取不到渲染器。
389
+ export { renderAppearanceFontSection };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * client.js — 字体子包的**浏览器半边**入口:外观页签第六节「全局字体」的渲染器 + 字体集编辑器。
3
+ *
4
+ * ⚠️ 归属(务必先读):用户口径 **§6.1 = A「代码归属,不是状态归属」**(正本 = ADR-0013 **D20**)。
5
+ * 随包走的是**渲染器与描述符**;字体的**键、默认值、kind、消毒、持久化**仍在主包
6
+ * `lib/settings-schema.js`(`FONTSET_KEYS` 单点定义)与 `fontsets/<id>.json`
7
+ * ⇒ 本包**不持键、不持久化、不注册 `settings.section`**;值一律经主包构造的渲染 ctx
8
+ * 或下面的 host 显式键集拿到。
9
+ *
10
+ * ## 构建期形状(与 packages/fx/src/client.js 逐条同形)
11
+ * · 产物 = packages/fonts/lib/client.fonts.js,id = 子包真实包名 dsh-plugin-folia-fonts
12
+ * (与宿主 graph row 逐字相等);入口 = 本文件,正文只取**首个块注释之后**的内容。
13
+ * · 本包的 INLINE_MODULES(client.build.json 的 inline[])内联在正文之前 ⇒ 兄弟文件与入口落在
14
+ * **同一个工厂作用域**:互相按裸名引用,不 import、不 require、不许 `...host` 展开、不许动态键。
15
+ * · 唯一例外 = 产物里由构建循环 outline 在正文与内联件之前发出的那一行
16
+ * `const host = require('<主包名>');`(BUNDLE-SPLIT §5.1 ②)。源码面写 require 会被
17
+ * test/verify-subpackage-purity.mjs 判红(只放行产物、只放行主包名出现在本包 lib/client*.js 里)。
18
+ *
19
+ * ## 消费侧契约(唯一跨包通道 = 主 bundle 的显式键集,ADR-0014 S5.1 / S5.4)
20
+ * 下面的 FONTS_HOST_REQUIRED 与主包 `src/client.js` 的 HOST_BRIDGE_BINDINGS 字体段**逐键一致**
21
+ * (缺键大声抛,不许静默 undefined)。键集的依据 = 两个已搬文件的**自由标识符**逐个点名,
22
+ * 取证脚本 `.integration-notes/scratch/fonts-host-keys.mjs`;逐键清单另见
23
+ * `.integration-notes/p-c-contract.md` §5(那面是唯一真源,本注释是它的副本):
24
+ * packages/fonts/src/appearance-font-section.js(原 src/panel-tabs-appearance.js:54-420)
25
+ * React×77 · weT×70 · ctlText×8 · switchRow×5 · sectionHead×1 · moduleResetButton×1
26
+ * · FONT_FAMILY_LABELS×2 · fontFamilyKeyOf×3 · filterUsableSystemFonts×2 · systemFontKeyOf×1
27
+ * · systemFontNameOf×1 · sanitizeFamilyKey×2 · THEME_COLOR_ROLES×1 · THEME_TYPE_ROLES×2
28
+ * · THEME_SIZE_MIN×1 · THEME_SIZE_MAX×1 · COMPONENT_FONT_TARGETS×1 · componentFontDefaults×2
29
+ * packages/fonts/src/fontset-editor.js(原 src/fontset-editor.js)
30
+ * weT×35 · React×29 · ctlText×1
31
+ * ⇒ 并集 18 键,再加贡献面 `registerPanelSection`(主包 src/extension-registry.js 的收件箱)= 19 键。
32
+ * ⚠️ 每多一个键就是多一条没人维护的隐式契约(ADR-0014 S5.2)⇒ 清单之外一个都**不**解构。
33
+ *
34
+ * ## 每渲染才存在的值走 ctx,不走键集(契约 §2.1)
35
+ * 渲染器需要的一切"这次渲染才存在"的值与意图回调(sel / fontSet / fontOpen / onToggleFontSection /
36
+ * onThemeFamily …)仍由主包在**渲染处**构造 ctx 传进来:本包不缓存它们、不自建订阅、不自起定时器
37
+ * (ADR-0014 S6.3),配置键与状态全留主包(ADR-0013 D20)。
38
+ */
39
+
40
+ /**
41
+ * 缺键**大声失败**(ADR-0014 S5.1 / S5.4 显式键集 + ADR-0013 D3③「宁可显示、不可静默消失」)。
42
+ * 主包被关 ⇒ 本 bundle 根本不会被 require(ADR-0014 S7.2 连坐);能走到这里却缺键 = 键集与申报表脱钩。
43
+ */
44
+ const FONTS_HOST_REQUIRED = [
45
+ // 内核与渲染助手(主 bundle 的共享零件表)
46
+ 'React', 'weT', 'ctlText', 'switchRow', 'sectionHead', 'moduleResetButton',
47
+ // 字族与主题数据表 / 取值器(产地在留在主包的文件里)
48
+ 'FONT_FAMILY_LABELS', 'fontFamilyKeyOf', 'filterUsableSystemFonts', 'systemFontKeyOf',
49
+ 'systemFontNameOf', 'sanitizeFamilyKey',
50
+ 'THEME_COLOR_ROLES', 'THEME_TYPE_ROLES', 'THEME_SIZE_MIN', 'THEME_SIZE_MAX',
51
+ 'COMPONENT_FONT_TARGETS', 'componentFontDefaults',
52
+ // 贡献面(主 bundle 的收件箱:值 + 回调,写权留主包)
53
+ 'registerPanelSection',
54
+ ];
55
+ for (const key of FONTS_HOST_REQUIRED) {
56
+ if (!(key in host)) {
57
+ throw new Error('[folia-fonts] 主 bundle 键集缺少 "' + key
58
+ + '"(ADR-0014 S5.1 / S5.4 显式键集:要么补键,要么改申报表,不许静默 undefined)');
59
+ }
60
+ }
61
+ const {
62
+ React, weT, ctlText, switchRow, sectionHead, moduleResetButton,
63
+ FONT_FAMILY_LABELS, fontFamilyKeyOf, filterUsableSystemFonts, systemFontKeyOf,
64
+ systemFontNameOf, sanitizeFamilyKey,
65
+ THEME_COLOR_ROLES, THEME_TYPE_ROLES, THEME_SIZE_MIN, THEME_SIZE_MAX,
66
+ COMPONENT_FONT_TARGETS, componentFontDefaults,
67
+ registerPanelSection,
68
+ } = host;
69
+
70
+ /* ===== 唯一的跨包登记:外观页签第六节「全局字体」===== */
71
+
72
+ // 主包 src/panel-tabs.js 的 `renderAppearanceTab(ctx)` 画完五个内置节之后,按注册表续接本包登记的节
73
+ // (`wePanelSectionsOf('appearance')`,见 src/extension-registry.js)。两个硬口径:
74
+ // ① `title` 必须是 **getter**:注册发生在 bundle 求值期,那时 i18n 表/`weT` 都还没到位,
75
+ // 注册期读词会把 `weT` 拉到求值期(fx/parallax 的 `title` getter 同款理由)。
76
+ // ② 不写 `order` ⇒ 落到默认位 100,排在**所有显式 order 的节之后**:今天屏上就是这个位置
77
+ // (五个内置节都在本节的上面)。搬包**不改变屏上顺序**,顺序判据见 src/panel-tabs.js。
78
+ registerPanelSection('appearance', 'fonts', {
79
+ id: 'fonts',
80
+ get title() { return weT("全局字体"); },
81
+ render: renderAppearanceFontSection,
82
+ });