dsh-fonttune 0.1.6 → 0.2.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/lib/index.js CHANGED
@@ -10,32 +10,43 @@
10
10
  * 2. Contribute a `<style>` row to the served index. The browser half applies
11
11
  * the same declarations once it activates, but that is after the shell has
12
12
  * already painted; injecting here is what keeps the first frame from
13
- * flashing DSH's default typography.
13
+ * flashing DSH's default typography. When `perTheme` is on, the row carries
14
+ * the dark theme's rules prefixed with the dark-theme attribute, so the
15
+ * first paint of each theme already uses that theme's own values.
14
16
  *
15
17
  * @module dsh-fonttune
16
18
  */
17
19
  import z from "@deepseek-ai/schemastery";
18
20
  // Default import, not named: `shared.cjs` is a CommonJS module, and Node's
19
21
  // CJS-to-ESM interop only guarantees `module.exports` as the default binding
20
- // (staticky-detected named exports would break this half at boot).
22
+ // (statically-detected named exports would break this half at boot).
21
23
  import shared from "./shared.cjs";
22
24
 
23
25
  const {
24
26
  buildFontCss,
27
+ CODE_LINE_HEIGHT_FIELD,
25
28
  CODE_SIZE_FIELD,
26
29
  CODE_WEIGHT_FIELD,
30
+ DARK_VALUES_FIELD,
27
31
  FALLBACK_TOKENS,
32
+ FEATURES_FIELD,
33
+ LIGATURES_FIELD,
34
+ LINE_HEIGHT_DIALOG_FIELD,
35
+ LINE_HEIGHT_FIELD,
36
+ MAX_FEATURES_LENGTH,
37
+ MAX_STACK_LENGTH,
28
38
  MONO_FIELD,
29
39
  NAMESPACE,
30
40
  normalizeConfig,
41
+ NO_SYNTHETIC_BOLD_FIELD,
42
+ NO_SYNTHETIC_ITALIC_FIELD,
43
+ PER_THEME_FIELD,
31
44
  SANS_FIELD,
45
+ SIZE_DIALOG_FIELD,
32
46
  SIZE_FIELD,
33
- SIZE_MAX,
34
- SIZE_MIN,
47
+ STACK_DIALOG_FIELD,
48
+ WEIGHT_DIALOG_FIELD,
35
49
  WEIGHT_FIELD,
36
- WEIGHT_MAX,
37
- WEIGHT_MIN,
38
- WEIGHT_UNSET,
39
50
  } = shared;
40
51
 
41
52
  /**
@@ -49,14 +60,22 @@ const SAFE_STACK = /^[^{};<>\\]*$/;
49
60
  */
50
61
  const MAX_STACK = 200;
51
62
 
63
+ /**
64
+ * A `font-feature-settings` value may only carry feature tags, on/off/number
65
+ * and the entry separators — anything else cannot appear in a valid value.
66
+ */
67
+ const SAFE_FEATURES = /^[a-zA-Z0-9"' ,]*$/;
68
+
52
69
  /**
53
70
  * The durable settings section.
54
71
  *
55
72
  * Every field defaults to "leave DSH alone": an empty stack injects no family
56
- * rule, a zero offset injects no size rule on either axis, and a zero weight
57
- * injects no weight rule (an unset CODE weight still writes the rule that keeps
58
- * code out of the body weight). Installing the plugin therefore changes nothing
59
- * until the user asks for something.
73
+ * rule, a zero offset injects no size rule, a ratio of 100 injects no
74
+ * line-height rule, and a zero weight injects no weight rule (an unset code
75
+ * weight still writes the rule that keeps code out of the interface/dialog
76
+ * weights). A dialog field left at its neutral value follows the interface
77
+ * value. Installing the plugin therefore changes nothing until the user asks
78
+ * for something.
60
79
  */
61
80
  export const Config = z.object({
62
81
  [SANS_FIELD]: z
@@ -64,7 +83,15 @@ export const Config = z.object({
64
83
  .max(MAX_STACK)
65
84
  .pattern(SAFE_STACK)
66
85
  .default("")
67
- .description("CSS font-family list for the UI and conversation text"),
86
+ .description("CSS font-family list for the interface text"),
87
+ [STACK_DIALOG_FIELD]: z
88
+ .string()
89
+ .max(MAX_STACK)
90
+ .pattern(SAFE_STACK)
91
+ .default("")
92
+ .description(
93
+ "CSS font-family list for the conversation markdown; empty follows the interface stack"
94
+ ),
68
95
  [MONO_FIELD]: z
69
96
  .string()
70
97
  .max(MAX_STACK)
@@ -73,36 +100,121 @@ export const Config = z.object({
73
100
  .description("CSS font-family list for code and monospaced text"),
74
101
  [SIZE_FIELD]: z
75
102
  .number()
76
- .min(SIZE_MIN)
77
- .max(SIZE_MAX)
103
+ .min(shared.SIZE_MIN)
104
+ .max(shared.SIZE_MAX)
78
105
  .default(0)
79
106
  .description(
80
- `Body/UI font-size offset in px (${SIZE_MIN}..${SIZE_MAX}, 0 keeps DSH's own sizes)`
107
+ `Interface font-size offset in px (${shared.SIZE_MIN}..${shared.SIZE_MAX}, 0 keeps DSH's own sizes)`
108
+ ),
109
+ [SIZE_DIALOG_FIELD]: z
110
+ .number()
111
+ .min(shared.SIZE_MIN)
112
+ .max(shared.SIZE_MAX)
113
+ .default(0)
114
+ .description(
115
+ "Conversation font-size offset in px; 0 follows the interface offset"
81
116
  ),
82
117
  [CODE_SIZE_FIELD]: z
83
118
  .number()
84
- .min(SIZE_MIN)
85
- .max(SIZE_MAX)
119
+ .min(shared.SIZE_MIN)
120
+ .max(shared.SIZE_MAX)
86
121
  .default(0)
87
122
  .description(
88
- `Code font-size offset in px (${SIZE_MIN}..${SIZE_MAX}, 0 keeps DSH's own sizes; independent of the body offset)`
123
+ `Code font-size offset in px (${shared.SIZE_MIN}..${shared.SIZE_MAX}, 0 keeps DSH's own sizes; independent of the interface offset)`
89
124
  ),
90
125
  [WEIGHT_FIELD]: z
91
126
  .number()
92
- .min(WEIGHT_UNSET)
93
- .max(WEIGHT_MAX)
94
- .default(WEIGHT_UNSET)
127
+ .min(shared.WEIGHT_UNSET)
128
+ .max(shared.WEIGHT_MAX)
129
+ .default(shared.WEIGHT_UNSET)
95
130
  .description(
96
- `Body/UI font weight (${WEIGHT_MIN}..${WEIGHT_MAX}, ${WEIGHT_UNSET} keeps DSH's own weights; code is not affected)`
131
+ `Interface font weight (${shared.WEIGHT_MIN}..${shared.WEIGHT_MAX}, ${shared.WEIGHT_UNSET} keeps DSH's own weights; code is not affected)`
132
+ ),
133
+ [WEIGHT_DIALOG_FIELD]: z
134
+ .number()
135
+ .min(shared.WEIGHT_UNSET)
136
+ .max(shared.WEIGHT_MAX)
137
+ .default(shared.WEIGHT_UNSET)
138
+ .description(
139
+ "Conversation font weight; 0 follows the interface weight"
97
140
  ),
98
141
  [CODE_WEIGHT_FIELD]: z
99
142
  .number()
100
- .min(WEIGHT_UNSET)
101
- .max(WEIGHT_MAX)
102
- .default(WEIGHT_UNSET)
143
+ .min(shared.WEIGHT_UNSET)
144
+ .max(shared.WEIGHT_MAX)
145
+ .default(shared.WEIGHT_UNSET)
146
+ .description(
147
+ `Code font weight (${shared.WEIGHT_MIN}..${shared.WEIGHT_MAX}, ${shared.WEIGHT_UNSET} keeps DSH's own weights; independent of the interface weight)`
148
+ ),
149
+ [LINE_HEIGHT_FIELD]: z
150
+ .number()
151
+ .min(shared.LINE_HEIGHT_MIN)
152
+ .max(shared.LINE_HEIGHT_MAX)
153
+ .default(shared.LINE_HEIGHT_MIN)
154
+ .description(
155
+ "Interface line-height ratio in percent (100 keeps DSH's own line heights)"
156
+ ),
157
+ [LINE_HEIGHT_DIALOG_FIELD]: z
158
+ .number()
159
+ .min(shared.LINE_HEIGHT_MIN)
160
+ .max(shared.LINE_HEIGHT_MAX)
161
+ .default(shared.LINE_HEIGHT_MIN)
162
+ .description(
163
+ "Conversation line-height ratio in percent; 100 follows the interface ratio"
164
+ ),
165
+ [CODE_LINE_HEIGHT_FIELD]: z
166
+ .number()
167
+ .min(shared.CODE_LINE_HEIGHT_MIN)
168
+ .max(shared.CODE_LINE_HEIGHT_MAX)
169
+ .default(0)
170
+ .description(
171
+ "Code line-height offset in px (0 keeps DSH's own line heights)"
172
+ ),
173
+ [LIGATURES_FIELD]: z
174
+ .number()
175
+ .min(0)
176
+ .max(2)
177
+ .default(0)
178
+ .description("Code ligatures: 0 DSH default, 1 forced on, 2 forced off"),
179
+ [FEATURES_FIELD]: z
180
+ .string()
181
+ .max(MAX_FEATURES_LENGTH)
182
+ .pattern(SAFE_FEATURES)
183
+ .default("")
184
+ .description("Advanced font-feature-settings value for code"),
185
+ [NO_SYNTHETIC_ITALIC_FIELD]: z
186
+ .boolean()
187
+ .default(false)
188
+ .description("Refuse synthetic italic (stops CJK text from being tilted)"),
189
+ [NO_SYNTHETIC_BOLD_FIELD]: z
190
+ .boolean()
191
+ .default(false)
192
+ .description("Refuse synthetic bold (for faces without a real bold)"),
193
+ [shared.UI_FOLLOWS_FIELD]: z
194
+ .boolean()
195
+ .default(true)
103
196
  .description(
104
- `Code font weight (${WEIGHT_MIN}..${WEIGHT_MAX}, ${WEIGHT_UNSET} keeps DSH's own weights; independent of the body weight)`
197
+ "The interface follows the conversation: its family and weight take the conversation's values (off = the interface keeps its own)"
105
198
  ),
199
+ [PER_THEME_FIELD]: z
200
+ .boolean()
201
+ .default(false)
202
+ .description("Keep a separate value set for the dark theme"),
203
+ [DARK_VALUES_FIELD]: z
204
+ .string()
205
+ .max(8000)
206
+ .default("{}")
207
+ .description("Dark theme sparse overrides (JSON map of axis field to value)"),
208
+ [shared.PRESETS_FIELD]: z
209
+ .string()
210
+ .max(20000)
211
+ .default("[]")
212
+ .description("Saved presets (JSON array of {name, values, savedAt})"),
213
+ [shared.ACTIVE_PRESET_FIELD]: z
214
+ .string()
215
+ .max(shared.MAX_PRESET_NAME)
216
+ .default("")
217
+ .description("Name of the preset the card currently edits (edits auto-save into it)"),
106
218
  });
107
219
 
108
220
  /**
@@ -116,7 +228,11 @@ export const Config = z.object({
116
228
  * @returns {StyleRow} the row the web server renders into `<head>`.
117
229
  */
118
230
  function styleRow(resolved) {
119
- const text = buildFontCss(normalizeConfig(resolved), FALLBACK_TOKENS);
231
+ const cfg = normalizeConfig(resolved);
232
+ // 0.2.0 ships the per-theme editor out; light and dark stay one value set
233
+ // (the dark fields remain in the schema, dormant, for a future release).
234
+ cfg[PER_THEME_FIELD] = false;
235
+ const text = buildFontCss(cfg, FALLBACK_TOKENS);
120
236
  return { kind: "style", text };
121
237
  }
122
238