@citisen/dsh-font 0.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/LICENSE +21 -0
- package/README.md +241 -0
- package/README.zh.md +165 -0
- package/cordis.patch.yml +20 -0
- package/lib/client.js +847 -0
- package/lib/index.js +239 -0
- package/package.json +76 -0
- package/scripts/build-client.mjs +297 -0
- package/scripts/verify-client.mjs +410 -0
- package/scripts/verify-host.mjs +187 -0
- package/scripts/verify-profile.mjs +146 -0
- package/src/client.js +838 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of `dsh-font` — the source of the prebuilt `lib/client.js`.
|
|
3
|
+
*
|
|
4
|
+
* This file is NOT loaded as an ES module. `scripts/build-client.mjs` wraps it
|
|
5
|
+
* in the DSH client-bundle envelope and writes `lib/client.js`, which is what
|
|
6
|
+
* the Web shell fetches. Keep it dependency-light: the only modules it may
|
|
7
|
+
* `import` are the platform-singleton specifiers the shell seeds into its
|
|
8
|
+
* module table, and the only Node-side API it may use is that table.
|
|
9
|
+
*
|
|
10
|
+
* Presentation strategy
|
|
11
|
+
* ---------------------
|
|
12
|
+
* Families are *not* written as CSS custom properties directly, because
|
|
13
|
+
* `ui-layout`'s theme presenter owns `document.body.style` and deletes any
|
|
14
|
+
* custom property it did not write on every theme change. Instead the families
|
|
15
|
+
* are stacked onto the design system through the theme service
|
|
16
|
+
* (`ctx.theme.overrideTokens`), which folds them into the active token set so
|
|
17
|
+
* the presenter republishes them — and keeps republishing them — itself.
|
|
18
|
+
*
|
|
19
|
+
* Sizes are a stylesheet, because the shipped components hard-code every
|
|
20
|
+
* interface text size in px and the shipped token scale is a fixed ladder
|
|
21
|
+
* (`--dsw-font-xs-13` is 13px, period). The sheet re-derives each hard-coded
|
|
22
|
+
* step from `--dsh-font-ui-scale` and redefines the conversation text tokens
|
|
23
|
+
* as absolute px, so one integer drives the whole surface.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-font/client
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
29
|
+
import { defineStore } from '@deepseek-ai/dsh-client-store'
|
|
30
|
+
import {
|
|
31
|
+
IconChevronDownOutline14,
|
|
32
|
+
IconChevronUpOutline14,
|
|
33
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
34
|
+
|
|
35
|
+
/** Settings namespace owned by this plugin (mirrors the host half). */
|
|
36
|
+
const FONT_SETTINGS_NAMESPACE = 'ui-font'
|
|
37
|
+
/** Locale namespace owning this feature's settings-row copy. */
|
|
38
|
+
const SETTINGS_LOCALE_NAMESPACE = 'settings.font'
|
|
39
|
+
/** Cosmetic namespace for this plugin's CSS classes and tags. */
|
|
40
|
+
const STYLE_PREFIX = 'dsh-font'
|
|
41
|
+
/**
|
|
42
|
+
* Id of the single stylesheet this plugin owns. The host half's pre-paint
|
|
43
|
+
* bootstrap looks the same element up by this id, so the two must agree;
|
|
44
|
+
* `scripts/verify-client.mjs` asserts that they do.
|
|
45
|
+
*/
|
|
46
|
+
const FONT_STYLE_ID = `${STYLE_PREFIX}/variables`
|
|
47
|
+
/**
|
|
48
|
+
* The plugin's identity in the theme registry, substituted with the real
|
|
49
|
+
* package name by `scripts/build-client.mjs`. The theme pins one override layer
|
|
50
|
+
* per source, so this must equal the package the roster mounted.
|
|
51
|
+
*/
|
|
52
|
+
const PLUGIN_ID = /* dsh:plugin-id */ 'dsh-font'
|
|
53
|
+
|
|
54
|
+
/** Field names — must match the host schema in `lib/index.js`. */
|
|
55
|
+
const UI_FONT_FAMILY_FIELD = 'uiFontFamily'
|
|
56
|
+
const CODE_FONT_FAMILY_FIELD = 'codeFontFamily'
|
|
57
|
+
const UI_FONT_SCALE_FIELD = 'uiFontScale'
|
|
58
|
+
const CONTENT_FONT_SIZE_FIELD = 'contentFontSize'
|
|
59
|
+
const CODE_FONT_SIZE_FIELD = 'codeFontSize'
|
|
60
|
+
|
|
61
|
+
/** Defaults — must match the host schema in `lib/index.js`. */
|
|
62
|
+
const DEFAULT_UI_FONT_FAMILY =
|
|
63
|
+
'-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif'
|
|
64
|
+
const DEFAULT_CODE_FONT_FAMILY =
|
|
65
|
+
'"SF Mono", "JetBrains Mono", "Fira Code", Consolas, "Liberation Mono", Menlo, Courier, "PingFang SC", "Microsoft YaHei"'
|
|
66
|
+
|
|
67
|
+
/** Accepted ranges — must match the host schema in `lib/index.js`. */
|
|
68
|
+
const UI_FONT_SCALE_MIN = 0.75
|
|
69
|
+
const UI_FONT_SCALE_MAX = 1.5
|
|
70
|
+
const UI_FONT_SCALE_STEP = 0.05
|
|
71
|
+
const CONTENT_FONT_SIZE_MIN = 12
|
|
72
|
+
const CONTENT_FONT_SIZE_MAX = 20
|
|
73
|
+
const CODE_FONT_SIZE_MIN = 10
|
|
74
|
+
const CODE_FONT_SIZE_MAX = 20
|
|
75
|
+
|
|
76
|
+
/** The interface text sizes the shipped components hard-code, in px. */
|
|
77
|
+
const UI_TEXT_STEPS = [11, 12, 13, 14, 16, 20, 24]
|
|
78
|
+
|
|
79
|
+
/** Curated family presets, offered as one-click fills for both inputs. */
|
|
80
|
+
const UI_FAMILY_PRESETS = [
|
|
81
|
+
{ id: 'system', label: 'System', value: DEFAULT_UI_FONT_FAMILY },
|
|
82
|
+
{
|
|
83
|
+
id: 'inter',
|
|
84
|
+
label: 'Inter',
|
|
85
|
+
value: 'Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'plex',
|
|
89
|
+
label: 'IBM Plex Sans',
|
|
90
|
+
value: '"IBM Plex Sans", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'noto',
|
|
94
|
+
label: 'Noto Sans',
|
|
95
|
+
value: '"Noto Sans", "Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei", sans-serif',
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
/** Curated code-family presets. */
|
|
100
|
+
const CODE_FAMILY_PRESETS = [
|
|
101
|
+
{ id: 'system', label: 'System mono', value: DEFAULT_CODE_FONT_FAMILY },
|
|
102
|
+
{
|
|
103
|
+
id: 'jetbrains',
|
|
104
|
+
label: 'JetBrains Mono',
|
|
105
|
+
value: '"JetBrains Mono", "Fira Code", Consolas, monospace',
|
|
106
|
+
},
|
|
107
|
+
{ id: 'fira', label: 'Fira Code', value: '"Fira Code", Consolas, monospace' },
|
|
108
|
+
{
|
|
109
|
+
id: 'cascadia',
|
|
110
|
+
label: 'Cascadia Code',
|
|
111
|
+
value: '"Cascadia Code", "Cascadia Mono", Consolas, monospace',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'maple',
|
|
115
|
+
label: 'Maple Mono',
|
|
116
|
+
value: '"Maple Mono", "JetBrains Mono", Consolas, monospace',
|
|
117
|
+
},
|
|
118
|
+
{ id: 'mono', label: 'Generic mono', value: 'ui-monospace, SFMono-Regular, Menlo, monospace' },
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
/** Row copy, keyed by locale. `zh` is the key-set source of truth. */
|
|
122
|
+
const zh = {
|
|
123
|
+
'font.title': '字体',
|
|
124
|
+
'font.description': '自定义界面与代码字体、字号,设置会保存到本机',
|
|
125
|
+
'font.uiFamily': '界面字体',
|
|
126
|
+
'font.uiFamilyHint': 'CSS font-family 列表,例如 Inter, "PingFang SC", sans-serif',
|
|
127
|
+
'font.codeFamily': '代码字体',
|
|
128
|
+
'font.codeFamilyHint': '用于代码块、行内代码与终端等宽文本',
|
|
129
|
+
'font.uiScale': '界面字号',
|
|
130
|
+
'font.uiScaleHint': '按比例缩放整个界面的文字大小',
|
|
131
|
+
'font.contentSize': '会话正文字号',
|
|
132
|
+
'font.contentSizeHint': '对话正文、标题与表格的字号',
|
|
133
|
+
'font.codeSize': '代码字号',
|
|
134
|
+
'font.codeSizeHint': '代码块与行内代码的字号',
|
|
135
|
+
'font.unit': 'px',
|
|
136
|
+
'font.reset': '恢复默认',
|
|
137
|
+
'font.presets': '预设',
|
|
138
|
+
'font.increase': '增大',
|
|
139
|
+
'font.decrease': '减小',
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** English dictionary, checked complete against the `zh` key set. */
|
|
143
|
+
const en = {
|
|
144
|
+
'font.title': 'Fonts',
|
|
145
|
+
'font.description':
|
|
146
|
+
'Customize the interface and code fonts and sizes; values are saved on this machine',
|
|
147
|
+
'font.uiFamily': 'Interface font',
|
|
148
|
+
'font.uiFamilyHint': 'A CSS font-family list, e.g. Inter, "PingFang SC", sans-serif',
|
|
149
|
+
'font.codeFamily': 'Code font',
|
|
150
|
+
'font.codeFamilyHint': 'Used for code blocks, inline code, and monospace text',
|
|
151
|
+
'font.uiScale': 'Interface text size',
|
|
152
|
+
'font.uiScaleHint': 'Scales every interface text size proportionally',
|
|
153
|
+
'font.contentSize': 'Conversation text size',
|
|
154
|
+
'font.contentSizeHint': 'Size of message bodies, headings, and tables',
|
|
155
|
+
'font.codeSize': 'Code text size',
|
|
156
|
+
'font.codeSizeHint': 'Size of code blocks and inline code',
|
|
157
|
+
'font.unit': 'px',
|
|
158
|
+
'font.reset': 'Reset to defaults',
|
|
159
|
+
'font.presets': 'Presets',
|
|
160
|
+
'font.increase': 'Increase',
|
|
161
|
+
'font.decrease': 'Decrease',
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The stylesheet this plugin owns, keyed by the resolved settings section. */
|
|
165
|
+
function fontStyleSheet(section) {
|
|
166
|
+
const scale = section[UI_FONT_SCALE_FIELD]
|
|
167
|
+
const contentSize = section[CONTENT_FONT_SIZE_FIELD]
|
|
168
|
+
const codeSize = section[CODE_FONT_SIZE_FIELD]
|
|
169
|
+
|
|
170
|
+
const scaleRules = UI_TEXT_STEPS.map(
|
|
171
|
+
(step) => `.dsh-font-size-${String(step)}{font-size:calc(${String(step)}px * var(--dsh-font-ui-scale,1)) !important}`,
|
|
172
|
+
).join('')
|
|
173
|
+
|
|
174
|
+
return [
|
|
175
|
+
'/* dsh-font: interface scale + conversation text sizes.',
|
|
176
|
+
' The scale is emitted as one utility class per hard-coded UI text size and',
|
|
177
|
+
' stamped onto every element by applyFonts(), so it is a deterministic',
|
|
178
|
+
' override that never inherits into a subtree. */',
|
|
179
|
+
'html body{',
|
|
180
|
+
`--dsh-font-ui-scale:${String(scale)};`,
|
|
181
|
+
`--dsh-font-code-size:${String(codeSize)}px;`,
|
|
182
|
+
// A CSS-side mirror of the content size. The authoritative declaration is
|
|
183
|
+
// inline on `body` (written by applyFonts), because ui-layout's theme
|
|
184
|
+
// presenter owns that one and an inline value outranks any stylesheet.
|
|
185
|
+
`--dsh-font-conversation-size:${String(contentSize)}px;`,
|
|
186
|
+
'}',
|
|
187
|
+
scaleRules,
|
|
188
|
+
'/* Conversation text sizes: absolute px, replacing the shipped 12..17px',
|
|
189
|
+
' ladder that ui-layout drives from the `ui-theme` namespace. This is the',
|
|
190
|
+
' size axis for conversation content; the scale above is the axis for the',
|
|
191
|
+
' surrounding interface, and the two never compose. */',
|
|
192
|
+
'html body{',
|
|
193
|
+
`--dsh-font-markdown-base:var(--dsh-font-conversation-size,14px) / calc(${String(contentSize)}px + 10px) var(--dsw-font-family) !important;`,
|
|
194
|
+
`--dsh-font-markdown-h1:700 calc(${String(contentSize)}px + 7px) / calc(${String(contentSize)}px + 16px) var(--dsw-font-family) !important;`,
|
|
195
|
+
`--dsh-font-markdown-h2:700 calc(${String(contentSize)}px + 5px) / calc(${String(contentSize)}px + 14px) var(--dsw-font-family) !important;`,
|
|
196
|
+
`--dsh-font-markdown-h3:700 calc(${String(contentSize)}px + 4px) / calc(${String(contentSize)}px + 12px) var(--dsw-font-family) !important;`,
|
|
197
|
+
`--dsh-font-markdown-h4:600 var(--dsh-font-conversation-size,14px) / calc(${String(contentSize)}px + 10px) var(--dsw-font-family) !important;`,
|
|
198
|
+
`--dsh-font-markdown-table:calc(${String(contentSize)}px - 1px) / calc(${String(contentSize)}px + 9px) var(--dsw-font-family) !important;`,
|
|
199
|
+
`--dsh-font-markdown-table-head:500 calc(${String(contentSize)}px - 1px) / calc(${String(contentSize)}px + 9px) var(--dsw-font-family) !important;`,
|
|
200
|
+
'}',
|
|
201
|
+
'/* Code text sizes, on their own axis. */',
|
|
202
|
+
'html body{',
|
|
203
|
+
`--dsw-font-markdown-code:var(--dsh-font-code-size,12px) / calc(var(--dsh-font-code-size,12px) + 7px) var(--ds-font-family-code) !important;`,
|
|
204
|
+
`--dsw-font-markdown-code-font-size:var(--dsh-font-code-size,12px) !important;`,
|
|
205
|
+
`--dsw-font-markdown-code-block:var(--dsh-font-code-size,12px) / calc(var(--dsh-font-code-size,12px) + 8px) var(--ds-font-family-code) !important;`,
|
|
206
|
+
`--dsw-font-markdown-code-block-font-size:var(--dsh-font-code-size,12px) !important;`,
|
|
207
|
+
`--dsw-font-markdown-code-block-small:calc(var(--dsh-font-code-size,12px) - 1px) / calc(var(--dsh-font-code-size,12px) + 5px) var(--ds-font-family-code) !important;`,
|
|
208
|
+
`--dsw-font-markdown-code-block-small-font-size:calc(var(--dsh-font-code-size,12px) - 1px) !important;`,
|
|
209
|
+
'}',
|
|
210
|
+
].join('\n')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The most recent resolved section; the observer re-stamps against it. */
|
|
214
|
+
let currentSection
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Keep the interface-scale stamps current as the interface mounts new
|
|
218
|
+
* elements. A single coalesced pass per frame is enough: the conversation,
|
|
219
|
+
* tool panels, and dialogs all grow by appending nodes.
|
|
220
|
+
* @param ctx - client context owning the observer's lifetime.
|
|
221
|
+
*/
|
|
222
|
+
function observeScale(ctx) {
|
|
223
|
+
if (typeof document === 'undefined' || typeof MutationObserver !== 'function') return
|
|
224
|
+
const target = document.body
|
|
225
|
+
if (target === null || target === undefined) return
|
|
226
|
+
let scheduled = false
|
|
227
|
+
const observer = new MutationObserver(() => {
|
|
228
|
+
if (scheduled || currentSection === undefined) return
|
|
229
|
+
scheduled = true
|
|
230
|
+
const run = () => {
|
|
231
|
+
scheduled = false
|
|
232
|
+
stampScaleClasses(currentSection[UI_FONT_SCALE_FIELD])
|
|
233
|
+
}
|
|
234
|
+
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(run)
|
|
235
|
+
else setTimeout(run, 16)
|
|
236
|
+
})
|
|
237
|
+
ctx.effect(() => {
|
|
238
|
+
observer.observe(target, { childList: true, subtree: true })
|
|
239
|
+
return () => {
|
|
240
|
+
observer.disconnect()
|
|
241
|
+
}
|
|
242
|
+
}, 'dsh-font: interface scale observer')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Write the owned stylesheet and the two family tokens onto the document.
|
|
247
|
+
* @param section - resolved settings section.
|
|
248
|
+
*/
|
|
249
|
+
function applyFonts(section) {
|
|
250
|
+
if (typeof document === 'undefined') return
|
|
251
|
+
currentSection = section
|
|
252
|
+
const uiFamily = section[UI_FONT_FAMILY_FIELD]
|
|
253
|
+
const codeFamily = section[CODE_FONT_FAMILY_FIELD]
|
|
254
|
+
|
|
255
|
+
let tag = document.getElementById(FONT_STYLE_ID)
|
|
256
|
+
if (tag === null) {
|
|
257
|
+
tag = document.createElement('style')
|
|
258
|
+
tag.id = FONT_STYLE_ID
|
|
259
|
+
tag.dataset.plugin = STYLE_PREFIX
|
|
260
|
+
const parent = document.head || document.documentElement
|
|
261
|
+
parent.appendChild(tag)
|
|
262
|
+
}
|
|
263
|
+
tag.textContent = fontStyleSheet(section)
|
|
264
|
+
|
|
265
|
+
// Pre-paint parity. The theme presenter re-asserts both of these on `body`
|
|
266
|
+
// from the override layer installed by `publishFamilyTokens`.
|
|
267
|
+
const root = document.documentElement.style
|
|
268
|
+
root.setProperty('--dsw-font-family', uiFamily)
|
|
269
|
+
root.setProperty('--ds-font-family-code', codeFamily)
|
|
270
|
+
|
|
271
|
+
// `--dsh-content-font-size` is an INLINE custom property on `body`, written
|
|
272
|
+
// by ui-layout's presenter from the `ui-theme` namespace. Inline declarations
|
|
273
|
+
// outrank a stylesheet regardless of `!important`, so the one authoritative
|
|
274
|
+
// value has to be written here too, in the same place the presenter writes.
|
|
275
|
+
if (document.body !== null && document.body !== undefined) {
|
|
276
|
+
document.body.style.setProperty(
|
|
277
|
+
'--dsh-content-font-size',
|
|
278
|
+
`${String(section[CONTENT_FONT_SIZE_FIELD])}px`,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
stampScaleClasses(section[UI_FONT_SCALE_FIELD])
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The stamped utility class for one shipped text size. */
|
|
286
|
+
function sizeClass(step) {
|
|
287
|
+
return `dsh-font-size-${String(step)}`
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Selector matching any element already stamped by {@link stampScaleClasses}. */
|
|
291
|
+
const SCALE_STAMPED_SELECTOR = UI_TEXT_STEPS.map((step) => `.${sizeClass(step)}`).join(',')
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The shipped text size measured for each element, so a later pass reuses the
|
|
295
|
+
* measurement instead of calling `getComputedStyle` again. Cleared whenever the
|
|
296
|
+
* stamps are removed, which is what makes a re-measure exact.
|
|
297
|
+
* @type {WeakMap<Element, number>}
|
|
298
|
+
*/
|
|
299
|
+
const measuredScaleStep = new WeakMap()
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Stamp the interface-scale utility class onto every element whose shipped
|
|
303
|
+
* stylesheet fixes a font size in px.
|
|
304
|
+
*
|
|
305
|
+
* The shipped design system has no interface size variable — each component
|
|
306
|
+
* hard-codes one of {@link UI_TEXT_STEPS} — so the scale is applied per element
|
|
307
|
+
* rather than through inheritance. That keeps it a deterministic override with
|
|
308
|
+
* no compounding, and it needs no knowledge of any component's class names.
|
|
309
|
+
*
|
|
310
|
+
* An element is measured while it carries no stamp; changing the scale clears
|
|
311
|
+
* every stamp first, which restores the shipped value and re-measures.
|
|
312
|
+
* @param scale - current interface scale; 1 means "shipped sizes" and stamps nothing.
|
|
313
|
+
*/
|
|
314
|
+
function stampScaleClasses(scale) {
|
|
315
|
+
if (typeof document === 'undefined' || typeof getComputedStyle !== 'function') return
|
|
316
|
+
const root = document.body
|
|
317
|
+
if (root === null || root === undefined) return
|
|
318
|
+
if (typeof root.querySelectorAll !== 'function') return
|
|
319
|
+
|
|
320
|
+
const stamped = [...root.querySelectorAll(SCALE_STAMPED_SELECTOR)]
|
|
321
|
+
if (scale === 1) {
|
|
322
|
+
for (const element of stamped) {
|
|
323
|
+
for (const step of UI_TEXT_STEPS) element.classList.remove(sizeClass(step))
|
|
324
|
+
measuredScaleStep.delete(element)
|
|
325
|
+
}
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const candidates = [...stamped, ...root.querySelectorAll('*')].filter(
|
|
330
|
+
(element) =>
|
|
331
|
+
element.classList !== undefined &&
|
|
332
|
+
(element.className !== '' ||
|
|
333
|
+
(typeof element.getAttribute === 'function' && element.getAttribute('style') !== null)),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
for (const element of candidates) {
|
|
337
|
+
let step = measuredScaleStep.get(element)
|
|
338
|
+
if (step === undefined) {
|
|
339
|
+
const measured = getComputedStyle(element).fontSize
|
|
340
|
+
if (typeof measured !== 'string' || !measured.endsWith('px')) continue
|
|
341
|
+
const exact = Number.parseFloat(measured)
|
|
342
|
+
const rounded = Math.round(exact)
|
|
343
|
+
if (!UI_TEXT_STEPS.includes(rounded) || Math.abs(exact - rounded) > 0.01) continue
|
|
344
|
+
step = rounded
|
|
345
|
+
measuredScaleStep.set(element, step)
|
|
346
|
+
}
|
|
347
|
+
// Move the element to the stamp for its measured size, dropping any other.
|
|
348
|
+
for (const candidate of UI_TEXT_STEPS) {
|
|
349
|
+
if (candidate !== step) element.classList.remove(sizeClass(candidate))
|
|
350
|
+
}
|
|
351
|
+
element.classList.add(sizeClass(step))
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Stack the two family tokens onto the design system through the theme
|
|
357
|
+
* service, so the theme presenter keeps them applied across palette changes.
|
|
358
|
+
* @param ctx - client context carrying the theme service.
|
|
359
|
+
* @param section - resolved settings section.
|
|
360
|
+
*/
|
|
361
|
+
function publishFamilyTokens(ctx, section) {
|
|
362
|
+
const theme = ctx.get('theme')
|
|
363
|
+
if (theme === undefined) return
|
|
364
|
+
theme.overrideTokens(PLUGIN_ID, {
|
|
365
|
+
'--dsw-font-family': {
|
|
366
|
+
light: section[UI_FONT_FAMILY_FIELD],
|
|
367
|
+
dark: section[UI_FONT_FAMILY_FIELD],
|
|
368
|
+
},
|
|
369
|
+
'--ds-font-family-code': {
|
|
370
|
+
light: section[CODE_FONT_FAMILY_FIELD],
|
|
371
|
+
dark: section[CODE_FONT_FAMILY_FIELD],
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Live-state store behind the General settings row. */
|
|
377
|
+
function createFontRowStore() {
|
|
378
|
+
return defineStore({
|
|
379
|
+
init: () => ({
|
|
380
|
+
uiFontFamily: DEFAULT_UI_FONT_FAMILY,
|
|
381
|
+
codeFontFamily: DEFAULT_CODE_FONT_FAMILY,
|
|
382
|
+
uiFontScale: 1,
|
|
383
|
+
contentFontSize: 14,
|
|
384
|
+
codeFontSize: 12,
|
|
385
|
+
revision: -1,
|
|
386
|
+
}),
|
|
387
|
+
actions: {
|
|
388
|
+
sync: (draft, section, revision) => {
|
|
389
|
+
if (revision !== undefined && revision <= draft.revision) return
|
|
390
|
+
draft.uiFontFamily = section[UI_FONT_FAMILY_FIELD]
|
|
391
|
+
draft.codeFontFamily = section[CODE_FONT_FAMILY_FIELD]
|
|
392
|
+
draft.uiFontScale = section[UI_FONT_SCALE_FIELD]
|
|
393
|
+
draft.contentFontSize = section[CONTENT_FONT_SIZE_FIELD]
|
|
394
|
+
draft.codeFontSize = section[CODE_FONT_SIZE_FIELD]
|
|
395
|
+
if (revision !== undefined) draft.revision = revision
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The stylesheet for the row's own chrome. */
|
|
402
|
+
const ROW_CSS = [
|
|
403
|
+
'.dsh-font-row{border-bottom:.5px solid var(--dsw-alias-border-l2);flex-direction:column;gap:16px;padding:16px 0;display:flex}',
|
|
404
|
+
'.dsh-font-head{flex-direction:column;gap:4px;display:flex}',
|
|
405
|
+
'.dsh-font-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}',
|
|
406
|
+
'.dsh-font-desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}',
|
|
407
|
+
'.dsh-font-field{flex-direction:column;gap:6px;display:flex}',
|
|
408
|
+
'.dsh-font-labelRow{align-items:center;justify-content:space-between;gap:8px;display:flex}',
|
|
409
|
+
'.dsh-font-label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}',
|
|
410
|
+
'.dsh-font-value{color:var(--dsw-alias-label-secondary);font-size:12px;font-variant-numeric:tabular-nums;line-height:18px}',
|
|
411
|
+
'.dsh-font-hint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
|
|
412
|
+
'.dsh-font-input{box-sizing:border-box;width:100%;height:32px;padding:0 10px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-module-platform);border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;font-family:inherit;font-size:13px;line-height:20px;outline:none}',
|
|
413
|
+
'.dsh-font-input:focus{border-color:var(--dsw-alias-brand-primary)}',
|
|
414
|
+
'.dsh-font-code{font-family:var(--ds-font-family-code)}',
|
|
415
|
+
'.dsh-font-sliderRow{align-items:center;gap:10px;display:flex}',
|
|
416
|
+
'.dsh-font-slider{flex:1;min-width:0;accent-color:var(--dsw-alias-brand-primary)}',
|
|
417
|
+
'.dsh-font-stepper{background:var(--dsw-alias-bg-module-platform);border-radius:16px;justify-content:center;align-items:center;gap:2px;min-width:96px;height:32px;display:inline-flex;flex:none;padding:0 4px}',
|
|
418
|
+
'.dsh-font-step{border:none;background:0 0;color:var(--dsw-alias-label-primary);cursor:pointer;width:20px;height:20px;border-radius:4px;justify-content:center;align-items:center;padding:0;display:inline-flex}',
|
|
419
|
+
'.dsh-font-step:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
420
|
+
'.dsh-font-step:disabled{color:var(--dsw-alias-label-caption);cursor:default}',
|
|
421
|
+
'.dsh-font-stepValue{text-align:center;min-width:32px;color:var(--dsw-alias-label-primary);font-size:13px;font-variant-numeric:tabular-nums;line-height:20px}',
|
|
422
|
+
'.dsh-font-presets{flex-wrap:wrap;gap:6px;display:flex}',
|
|
423
|
+
'.dsh-font-chip{border:.5px solid var(--dsw-alias-border-l4);background:0 0;color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:12px;padding:3px 10px;font-family:inherit;font-size:11px;line-height:16px}',
|
|
424
|
+
'.dsh-font-chip:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
425
|
+
'.dsh-font-chip[data-active="true"]{border-color:var(--dsw-static-neutral-bluish-400);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-module-platform)}',
|
|
426
|
+
'.dsh-font-reset{align-self:flex-start;border:.5px solid var(--dsw-alias-border-l4);background:0 0;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:10px;padding:5px 12px;font-family:inherit;font-size:12px;line-height:18px}',
|
|
427
|
+
'.dsh-font-reset:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
428
|
+
].join('')
|
|
429
|
+
|
|
430
|
+
/** Install the row chrome stylesheet for the plugin's lifetime. */
|
|
431
|
+
function installRowStyles(ctx) {
|
|
432
|
+
if (typeof document === 'undefined') return
|
|
433
|
+
ctx.effect(() => {
|
|
434
|
+
const tag = document.createElement('style')
|
|
435
|
+
tag.dataset.plugin = STYLE_PREFIX
|
|
436
|
+
tag.dataset.pluginCss = `${STYLE_PREFIX}/row.css`
|
|
437
|
+
tag.textContent = ROW_CSS
|
|
438
|
+
document.head.appendChild(tag)
|
|
439
|
+
return () => {
|
|
440
|
+
tag.remove()
|
|
441
|
+
}
|
|
442
|
+
}, 'dsh-font: row stylesheet')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* One labelled field wrapper.
|
|
447
|
+
* @param props - React props.
|
|
448
|
+
* @returns the field element.
|
|
449
|
+
*/
|
|
450
|
+
function Field({ label, value, hint, children }) {
|
|
451
|
+
return React.createElement(
|
|
452
|
+
'div',
|
|
453
|
+
{ className: 'dsh-font-field' },
|
|
454
|
+
React.createElement(
|
|
455
|
+
'div',
|
|
456
|
+
{ className: 'dsh-font-labelRow' },
|
|
457
|
+
React.createElement('span', { className: 'dsh-font-label' }, label),
|
|
458
|
+
value === undefined
|
|
459
|
+
? null
|
|
460
|
+
: React.createElement('span', { className: 'dsh-font-value' }, value),
|
|
461
|
+
),
|
|
462
|
+
children,
|
|
463
|
+
hint === undefined
|
|
464
|
+
? null
|
|
465
|
+
: React.createElement('div', { className: 'dsh-font-hint' }, hint),
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* A commit-on-blur/Enter text input that always reflects the durable value
|
|
471
|
+
* once the user stops editing.
|
|
472
|
+
* @param props - React props.
|
|
473
|
+
* @returns the input element.
|
|
474
|
+
*/
|
|
475
|
+
function FamilyInput({ value, placeholder, onCommit, monospace }) {
|
|
476
|
+
const [draft, setDraft] = useState(value)
|
|
477
|
+
const editing = useRef(false)
|
|
478
|
+
|
|
479
|
+
useEffect(() => {
|
|
480
|
+
if (!editing.current) setDraft(value)
|
|
481
|
+
}, [value])
|
|
482
|
+
|
|
483
|
+
const commit = useCallback(() => {
|
|
484
|
+
editing.current = false
|
|
485
|
+
const next = draft.trim()
|
|
486
|
+
if (next === '') {
|
|
487
|
+
setDraft(value)
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
if (next !== value) onCommit(next)
|
|
491
|
+
}, [draft, onCommit, value])
|
|
492
|
+
|
|
493
|
+
return React.createElement('input', {
|
|
494
|
+
type: 'text',
|
|
495
|
+
spellCheck: false,
|
|
496
|
+
className: monospace === true ? 'dsh-font-input dsh-font-code' : 'dsh-font-input',
|
|
497
|
+
value: draft,
|
|
498
|
+
placeholder,
|
|
499
|
+
onChange: (event) => {
|
|
500
|
+
editing.current = true
|
|
501
|
+
setDraft(event.target.value)
|
|
502
|
+
},
|
|
503
|
+
onBlur: commit,
|
|
504
|
+
onKeyDown: (event) => {
|
|
505
|
+
if (event.key === 'Enter') event.currentTarget.blur()
|
|
506
|
+
if (event.key === 'Escape') {
|
|
507
|
+
editing.current = false
|
|
508
|
+
setDraft(value)
|
|
509
|
+
event.currentTarget.blur()
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* A horizontal slider plus an exact stepper.
|
|
517
|
+
* @param props - React props.
|
|
518
|
+
* @returns the control element.
|
|
519
|
+
*/
|
|
520
|
+
function SliderControl({ min, max, step, value, format, onChange, ariaLabel, increase, decrease }) {
|
|
521
|
+
return React.createElement(
|
|
522
|
+
'div',
|
|
523
|
+
{ className: 'dsh-font-sliderRow' },
|
|
524
|
+
React.createElement('input', {
|
|
525
|
+
type: 'range',
|
|
526
|
+
className: 'dsh-font-slider',
|
|
527
|
+
min,
|
|
528
|
+
max,
|
|
529
|
+
step,
|
|
530
|
+
value,
|
|
531
|
+
'aria-label': ariaLabel,
|
|
532
|
+
onChange: (event) => {
|
|
533
|
+
onChange(Number(event.target.value))
|
|
534
|
+
},
|
|
535
|
+
}),
|
|
536
|
+
React.createElement(
|
|
537
|
+
'div',
|
|
538
|
+
{ className: 'dsh-font-stepper' },
|
|
539
|
+
React.createElement(
|
|
540
|
+
'button',
|
|
541
|
+
{
|
|
542
|
+
type: 'button',
|
|
543
|
+
className: 'dsh-font-step',
|
|
544
|
+
'aria-label': decrease,
|
|
545
|
+
disabled: value <= min,
|
|
546
|
+
onClick: () => {
|
|
547
|
+
onChange(Number((value - step).toFixed(2)))
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
React.createElement(IconChevronDownOutline14, { size: 9 }),
|
|
551
|
+
),
|
|
552
|
+
React.createElement('span', { className: 'dsh-font-stepValue' }, format(value)),
|
|
553
|
+
React.createElement(
|
|
554
|
+
'button',
|
|
555
|
+
{
|
|
556
|
+
type: 'button',
|
|
557
|
+
className: 'dsh-font-step',
|
|
558
|
+
'aria-label': increase,
|
|
559
|
+
disabled: value >= max,
|
|
560
|
+
onClick: () => {
|
|
561
|
+
onChange(Number((value + step).toFixed(2)))
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
React.createElement(IconChevronUpOutline14, { size: 9 }),
|
|
565
|
+
),
|
|
566
|
+
),
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* A row of one-click family presets.
|
|
572
|
+
* @param props - React props.
|
|
573
|
+
* @returns the preset strip.
|
|
574
|
+
*/
|
|
575
|
+
function PresetStrip({ presets, current, onPick }) {
|
|
576
|
+
return React.createElement(
|
|
577
|
+
'div',
|
|
578
|
+
{ className: 'dsh-font-presets' },
|
|
579
|
+
presets.map((preset) =>
|
|
580
|
+
React.createElement(
|
|
581
|
+
'button',
|
|
582
|
+
{
|
|
583
|
+
key: preset.id,
|
|
584
|
+
type: 'button',
|
|
585
|
+
className: 'dsh-font-chip',
|
|
586
|
+
'data-active': preset.value === current ? 'true' : 'false',
|
|
587
|
+
onClick: () => {
|
|
588
|
+
onPick(preset.value)
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
preset.label,
|
|
592
|
+
),
|
|
593
|
+
),
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The General-settings row: five controls over the `ui-font` namespace.
|
|
599
|
+
* @param props - composed slot props (`t`, `useStore`, and the inject actions).
|
|
600
|
+
* @returns the row element tree.
|
|
601
|
+
*/
|
|
602
|
+
function FontRow({ t, useStore, setField, reset }) {
|
|
603
|
+
const uiFontFamily = useStore((s) => s.uiFontFamily)
|
|
604
|
+
const codeFontFamily = useStore((s) => s.codeFontFamily)
|
|
605
|
+
const uiFontScale = useStore((s) => s.uiFontScale)
|
|
606
|
+
const contentFontSize = useStore((s) => s.contentFontSize)
|
|
607
|
+
const codeFontSize = useStore((s) => s.codeFontSize)
|
|
608
|
+
|
|
609
|
+
return React.createElement(
|
|
610
|
+
'div',
|
|
611
|
+
{ className: 'dsh-font-row' },
|
|
612
|
+
React.createElement(
|
|
613
|
+
'div',
|
|
614
|
+
{ className: 'dsh-font-head' },
|
|
615
|
+
React.createElement('div', { className: 'dsh-font-title' }, t('font.title')),
|
|
616
|
+
React.createElement('div', { className: 'dsh-font-desc' }, t('font.description')),
|
|
617
|
+
),
|
|
618
|
+
React.createElement(
|
|
619
|
+
Field,
|
|
620
|
+
{ label: t('font.uiFamily'), hint: t('font.uiFamilyHint') },
|
|
621
|
+
React.createElement(FamilyInput, {
|
|
622
|
+
value: uiFontFamily,
|
|
623
|
+
placeholder: DEFAULT_UI_FONT_FAMILY,
|
|
624
|
+
onCommit: (value) => {
|
|
625
|
+
setField(UI_FONT_FAMILY_FIELD, value)
|
|
626
|
+
},
|
|
627
|
+
}),
|
|
628
|
+
React.createElement(PresetStrip, {
|
|
629
|
+
presets: UI_FAMILY_PRESETS,
|
|
630
|
+
current: uiFontFamily,
|
|
631
|
+
onPick: (value) => {
|
|
632
|
+
setField(UI_FONT_FAMILY_FIELD, value)
|
|
633
|
+
},
|
|
634
|
+
}),
|
|
635
|
+
),
|
|
636
|
+
React.createElement(
|
|
637
|
+
Field,
|
|
638
|
+
{ label: t('font.codeFamily'), hint: t('font.codeFamilyHint') },
|
|
639
|
+
React.createElement(FamilyInput, {
|
|
640
|
+
value: codeFontFamily,
|
|
641
|
+
placeholder: DEFAULT_CODE_FONT_FAMILY,
|
|
642
|
+
monospace: true,
|
|
643
|
+
onCommit: (value) => {
|
|
644
|
+
setField(CODE_FONT_FAMILY_FIELD, value)
|
|
645
|
+
},
|
|
646
|
+
}),
|
|
647
|
+
React.createElement(PresetStrip, {
|
|
648
|
+
presets: CODE_FAMILY_PRESETS,
|
|
649
|
+
current: codeFontFamily,
|
|
650
|
+
onPick: (value) => {
|
|
651
|
+
setField(CODE_FONT_FAMILY_FIELD, value)
|
|
652
|
+
},
|
|
653
|
+
}),
|
|
654
|
+
),
|
|
655
|
+
React.createElement(
|
|
656
|
+
Field,
|
|
657
|
+
{
|
|
658
|
+
label: t('font.uiScale'),
|
|
659
|
+
value: `${String(Math.round(uiFontScale * 100))}%`,
|
|
660
|
+
hint: t('font.uiScaleHint'),
|
|
661
|
+
},
|
|
662
|
+
React.createElement(SliderControl, {
|
|
663
|
+
min: UI_FONT_SCALE_MIN,
|
|
664
|
+
max: UI_FONT_SCALE_MAX,
|
|
665
|
+
step: UI_FONT_SCALE_STEP,
|
|
666
|
+
value: uiFontScale,
|
|
667
|
+
format: (value) => `${String(Math.round(value * 100))}%`,
|
|
668
|
+
ariaLabel: t('font.uiScale'),
|
|
669
|
+
increase: t('font.increase'),
|
|
670
|
+
decrease: t('font.decrease'),
|
|
671
|
+
onChange: (value) => {
|
|
672
|
+
setField(UI_FONT_SCALE_FIELD, value)
|
|
673
|
+
},
|
|
674
|
+
}),
|
|
675
|
+
),
|
|
676
|
+
React.createElement(
|
|
677
|
+
Field,
|
|
678
|
+
{
|
|
679
|
+
label: t('font.contentSize'),
|
|
680
|
+
value: `${String(contentFontSize)} ${t('font.unit')}`,
|
|
681
|
+
hint: t('font.contentSizeHint'),
|
|
682
|
+
},
|
|
683
|
+
React.createElement(SliderControl, {
|
|
684
|
+
min: CONTENT_FONT_SIZE_MIN,
|
|
685
|
+
max: CONTENT_FONT_SIZE_MAX,
|
|
686
|
+
step: 1,
|
|
687
|
+
value: contentFontSize,
|
|
688
|
+
format: (value) => `${String(value)} ${t('font.unit')}`,
|
|
689
|
+
ariaLabel: t('font.contentSize'),
|
|
690
|
+
increase: t('font.increase'),
|
|
691
|
+
decrease: t('font.decrease'),
|
|
692
|
+
onChange: (value) => {
|
|
693
|
+
setField(CONTENT_FONT_SIZE_FIELD, value)
|
|
694
|
+
},
|
|
695
|
+
}),
|
|
696
|
+
),
|
|
697
|
+
React.createElement(
|
|
698
|
+
Field,
|
|
699
|
+
{
|
|
700
|
+
label: t('font.codeSize'),
|
|
701
|
+
value: `${String(codeFontSize)} ${t('font.unit')}`,
|
|
702
|
+
hint: t('font.codeSizeHint'),
|
|
703
|
+
},
|
|
704
|
+
React.createElement(SliderControl, {
|
|
705
|
+
min: CODE_FONT_SIZE_MIN,
|
|
706
|
+
max: CODE_FONT_SIZE_MAX,
|
|
707
|
+
step: 1,
|
|
708
|
+
value: codeFontSize,
|
|
709
|
+
format: (value) => `${String(value)} ${t('font.unit')}`,
|
|
710
|
+
ariaLabel: t('font.codeSize'),
|
|
711
|
+
increase: t('font.increase'),
|
|
712
|
+
decrease: t('font.decrease'),
|
|
713
|
+
onChange: (value) => {
|
|
714
|
+
setField(CODE_FONT_SIZE_FIELD, value)
|
|
715
|
+
},
|
|
716
|
+
}),
|
|
717
|
+
),
|
|
718
|
+
React.createElement(
|
|
719
|
+
'button',
|
|
720
|
+
{
|
|
721
|
+
type: 'button',
|
|
722
|
+
className: 'dsh-font-reset',
|
|
723
|
+
onClick: () => {
|
|
724
|
+
reset()
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
t('font.reset'),
|
|
728
|
+
),
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* The services this plugin waits for: the settings transport plus slots/locale
|
|
734
|
+
* for the row. The theme service is read through `ctx.get` (optional) so a
|
|
735
|
+
* composition without `ui-theme` still gets families from the stylesheet path.
|
|
736
|
+
*/
|
|
737
|
+
const inject = ['slots', 'locale', 'settingsScope']
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Client plugin body: resolve the durable section, paint it, and register the
|
|
741
|
+
* feature-owned Fonts row into the General section's item slot.
|
|
742
|
+
* @param ctx - client cordis context.
|
|
743
|
+
*/
|
|
744
|
+
export function apply(ctx) {
|
|
745
|
+
installRowStyles(ctx)
|
|
746
|
+
observeScale(ctx)
|
|
747
|
+
|
|
748
|
+
const scope = ctx.settingsScope.bind({
|
|
749
|
+
namespace: FONT_SETTINGS_NAMESPACE,
|
|
750
|
+
decode: (section) => {
|
|
751
|
+
if (section === null || typeof section !== 'object') return undefined
|
|
752
|
+
const raw = section
|
|
753
|
+
const text = (key, fallback) =>
|
|
754
|
+
typeof raw[key] === 'string' && raw[key] !== '' ? raw[key] : fallback
|
|
755
|
+
const number = (key, fallback, min, max) => {
|
|
756
|
+
const value = Number(raw[key])
|
|
757
|
+
return Number.isFinite(value) && value >= min && value <= max ? value : fallback
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
[UI_FONT_FAMILY_FIELD]: text(UI_FONT_FAMILY_FIELD, DEFAULT_UI_FONT_FAMILY),
|
|
761
|
+
[CODE_FONT_FAMILY_FIELD]: text(CODE_FONT_FAMILY_FIELD, DEFAULT_CODE_FONT_FAMILY),
|
|
762
|
+
[UI_FONT_SCALE_FIELD]: number(UI_FONT_SCALE_FIELD, 1, UI_FONT_SCALE_MIN, UI_FONT_SCALE_MAX),
|
|
763
|
+
[CONTENT_FONT_SIZE_FIELD]: Math.round(
|
|
764
|
+
number(CONTENT_FONT_SIZE_FIELD, 14, CONTENT_FONT_SIZE_MIN, CONTENT_FONT_SIZE_MAX),
|
|
765
|
+
),
|
|
766
|
+
[CODE_FONT_SIZE_FIELD]: Math.round(
|
|
767
|
+
number(CODE_FONT_SIZE_FIELD, 12, CODE_FONT_SIZE_MIN, CODE_FONT_SIZE_MAX),
|
|
768
|
+
),
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
const store = createFontRowStore()
|
|
774
|
+
|
|
775
|
+
ctx.effect(
|
|
776
|
+
() =>
|
|
777
|
+
ctx.locale.register(SETTINGS_LOCALE_NAMESPACE, {
|
|
778
|
+
zh,
|
|
779
|
+
en,
|
|
780
|
+
}),
|
|
781
|
+
'dsh-font: settings row dictionaries',
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
const defaults = {
|
|
785
|
+
[UI_FONT_FAMILY_FIELD]: DEFAULT_UI_FONT_FAMILY,
|
|
786
|
+
[CODE_FONT_FAMILY_FIELD]: DEFAULT_CODE_FONT_FAMILY,
|
|
787
|
+
[UI_FONT_SCALE_FIELD]: 1,
|
|
788
|
+
[CONTENT_FONT_SIZE_FIELD]: 14,
|
|
789
|
+
[CODE_FONT_SIZE_FIELD]: 12,
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
let actions
|
|
793
|
+
|
|
794
|
+
const paint = (section, revision) => {
|
|
795
|
+
actions?.sync(section, revision)
|
|
796
|
+
applyFonts(section)
|
|
797
|
+
publishFamilyTokens(ctx, section)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
ctx.effect(
|
|
801
|
+
() =>
|
|
802
|
+
scope.subscribe(() => {
|
|
803
|
+
const snapshot = scope.getSnapshot()
|
|
804
|
+
if (snapshot.value === undefined) return
|
|
805
|
+
paint(snapshot.value, snapshot.revision)
|
|
806
|
+
}),
|
|
807
|
+
'dsh-font: settings adoption',
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
const initial = scope.getSnapshot()
|
|
811
|
+
paint(initial.value ?? defaults, initial.revision)
|
|
812
|
+
|
|
813
|
+
ctx.slots.inject('settings.general.item', () =>
|
|
814
|
+
ctx.slots.register(
|
|
815
|
+
{
|
|
816
|
+
name: 'settings.general.item',
|
|
817
|
+
id: 'font',
|
|
818
|
+
order: 12,
|
|
819
|
+
store,
|
|
820
|
+
locale: SETTINGS_LOCALE_NAMESPACE,
|
|
821
|
+
inject: (bound) => {
|
|
822
|
+
actions = bound
|
|
823
|
+
const snapshot = scope.getSnapshot()
|
|
824
|
+
paint(snapshot.value ?? defaults, snapshot.revision)
|
|
825
|
+
return {
|
|
826
|
+
setField: (field, value) => {
|
|
827
|
+
scope.set(field, value)
|
|
828
|
+
},
|
|
829
|
+
reset: () => {
|
|
830
|
+
for (const field of Object.keys(defaults)) scope.unset(field)
|
|
831
|
+
},
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
FontRow,
|
|
836
|
+
),
|
|
837
|
+
)
|
|
838
|
+
}
|