@citisen/dsh-font 0.1.0 → 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
@@ -27,6 +27,18 @@ export const FONT_SETTINGS_NAMESPACE = 'ui-font'
27
27
  export const UI_FONT_FAMILY_FIELD = 'uiFontFamily'
28
28
  /** Field: CSS font-family list for code, terminal, and monospace text. */
29
29
  export const CODE_FONT_FAMILY_FIELD = 'codeFontFamily'
30
+ /** Field: numeric `font-weight` for code, terminal, and monospace text. */
31
+ export const CODE_FONT_WEIGHT_FIELD = 'codeFontWeight'
32
+ /**
33
+ * Field: numeric `font-weight` for interface text.
34
+ *
35
+ * Unlike the code weight this one is opted into: `400` — the value every
36
+ * untouched install resolves to — is the design system's own base, so the
37
+ * plugin emits no weight rule at all for it. Anything else is written as an
38
+ * explicit override, which is why "not set" and "set to 400" are the same
39
+ * state rather than two.
40
+ */
41
+ export const UI_FONT_WEIGHT_FIELD = 'uiFontWeight'
30
42
  /** Field: scale applied to every interface font size, as a unitless number. */
31
43
  export const UI_FONT_SCALE_FIELD = 'uiFontScale'
32
44
  /** Field: conversation content font size in px (owns `--dsh-content-font-size`). */
@@ -41,6 +53,36 @@ export const DEFAULT_UI_FONT_FAMILY =
41
53
  export const DEFAULT_CODE_FONT_FAMILY =
42
54
  '"SF Mono", "JetBrains Mono", "Fira Code", Consolas, "Liberation Mono", Menlo, Courier, "PingFang SC", "Microsoft YaHei"'
43
55
 
56
+ /**
57
+ * Weights the code font may be set to.
58
+ *
59
+ * The design system ships **no** weight token — every `font: …` declaration in
60
+ * the interface is a literal, and the code ladder is literally `400`. So the
61
+ * weight is delivered as a `font-weight` override the plugin owns, and it is a
62
+ * closed list rather than a free number: a `font-weight` a family does not have
63
+ * is synthesized by the browser (faux-bold), and offering that silently would
64
+ * be a poor default. The list is the CSS Fonts keyword scale, which is also the
65
+ * scale the Local Font Access API reports individual faces on.
66
+ *
67
+ * Kept in sync by hand with the browser half's table in `src/client.js`; the
68
+ * two halves are separate bundles and share no module.
69
+ */
70
+ export const FONT_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900]
71
+
72
+ /**
73
+ * The shipped code weight. Matches the design system's own literal (`400` in
74
+ * every `--dsw-font-markdown-code*` token), so "no customization" is the
75
+ * default and an untouched install paints exactly what it painted before.
76
+ */
77
+ export const DEFAULT_CODE_FONT_WEIGHT = 400
78
+
79
+ /**
80
+ * The shipped interface weight. Identical to {@link DEFAULT_CODE_FONT_WEIGHT}
81
+ * for the same reason: it is what the shipped interface already computes, so an
82
+ * untouched install paints exactly what it painted before this field existed.
83
+ */
84
+ export const DEFAULT_UI_FONT_WEIGHT = 400
85
+
44
86
  /** Accepted interface scale range (1 = the shipped sizes). */
45
87
  export const UI_FONT_SCALE_MIN = 0.75
46
88
  /** Accepted interface scale range upper bound. */
@@ -67,6 +109,8 @@ export const FONT_STYLE_ID = 'dsh-font/variables'
67
109
  export const FontSettingsSchema = z.object({
68
110
  [UI_FONT_FAMILY_FIELD]: z.string().default(DEFAULT_UI_FONT_FAMILY),
69
111
  [CODE_FONT_FAMILY_FIELD]: z.string().default(DEFAULT_CODE_FONT_FAMILY),
112
+ [CODE_FONT_WEIGHT_FIELD]: z.union(FONT_WEIGHTS).default(DEFAULT_CODE_FONT_WEIGHT),
113
+ [UI_FONT_WEIGHT_FIELD]: z.union(FONT_WEIGHTS).default(DEFAULT_UI_FONT_WEIGHT),
70
114
  [UI_FONT_SCALE_FIELD]: z
71
115
  .number()
72
116
  .min(UI_FONT_SCALE_MIN)
@@ -108,10 +152,9 @@ function uiScaleRules(scale) {
108
152
  * Build the pre-paint stylesheet for one resolved settings section.
109
153
  *
110
154
  * This is the subset of the browser half's sheet that must exist before any
111
- * plugin runs: the interface scale and the conversation text size. Families are
112
- * handled separately (see {@link fontBootstrapScript}) because the design
113
- * system resolves them through `var()` chains the shell's own `body` rule
114
- * already reads.
155
+ * plugin runs: the interface scale and the conversation text size. Families and
156
+ * the code weight are handled separately (see {@link fontBootstrapScript})
157
+ * because the design system resolves them through the token chain.
115
158
  *
116
159
  * Written as a `<style>` element rather than inline custom properties because
117
160
  * `ui-layout`'s theme presenter owns `document.body.style` and rewrites the
@@ -126,6 +169,19 @@ export function fontStyleSheet(section) {
126
169
  const uiScale = section[UI_FONT_SCALE_FIELD]
127
170
  const contentSize = section[CONTENT_FONT_SIZE_FIELD]
128
171
  const codeSize = section[CODE_FONT_SIZE_FIELD]
172
+ const codeWeight = section[CODE_FONT_WEIGHT_FIELD]
173
+ const uiWeight = section[UI_FONT_WEIGHT_FIELD]
174
+
175
+ // The interface weight is the one field that is *not* always emitted: at the
176
+ // shipped 400 the rule would be a no-op (`normal` inherits as 400 anyway), and
177
+ // leaving it out keeps a default install's first frame byte-identical to the
178
+ // shipped one. The browser half repeats this rule in its own — larger — sheet,
179
+ // which replaces this one the moment the plugin activates; both exist so the
180
+ // weight cannot flash between the two.
181
+ const uiWeightRules =
182
+ uiWeight === undefined || uiWeight === DEFAULT_UI_FONT_WEIGHT
183
+ ? []
184
+ : [`html body{font-weight:${String(uiWeight)}}`]
129
185
 
130
186
  return [
131
187
  '/* dsh-font: pre-paint variables and interface scale */',
@@ -133,7 +189,9 @@ export function fontStyleSheet(section) {
133
189
  `--dsh-font-ui-scale:${String(uiScale)};`,
134
190
  `--dsh-font-content-size:${String(contentSize)}px;`,
135
191
  `--dsh-font-code-size:${String(codeSize)}px;`,
192
+ `--dsh-font-code-weight:${String(codeWeight)};`,
136
193
  '}',
194
+ ...uiWeightRules,
137
195
  'html body,html body *{',
138
196
  uiScaleRules(uiScale),
139
197
  '}',
@@ -161,6 +219,7 @@ function escapeForScript(text) {
161
219
  export function fontBootstrapScript(section) {
162
220
  const uiFamily = JSON.stringify(section[UI_FONT_FAMILY_FIELD])
163
221
  const codeFamily = JSON.stringify(section[CODE_FONT_FAMILY_FIELD])
222
+ const codeWeight = JSON.stringify(String(section[CODE_FONT_WEIGHT_FIELD]))
164
223
  // Keep the stylesheet after the user-controlled family strings so the
165
224
  // `</`-escaping cannot be defeated by a family value that itself contains a
166
225
  // script-closing sequence.
@@ -181,6 +240,7 @@ export function fontBootstrapScript(section) {
181
240
  const root = document.documentElement.style
182
241
  root.setProperty('--dsw-font-family', ${uiFamily})
183
242
  root.setProperty('--ds-font-family-code', ${codeFamily})
243
+ root.setProperty('--dsh-font-code-weight', ${codeWeight})
184
244
  } catch (error) {
185
245
  console.warn('dsh-font: pre-paint bootstrap failed', error)
186
246
  }
@@ -210,6 +270,8 @@ function readSection(ctx) {
210
270
  const fallback = {
211
271
  [UI_FONT_FAMILY_FIELD]: DEFAULT_UI_FONT_FAMILY,
212
272
  [CODE_FONT_FAMILY_FIELD]: DEFAULT_CODE_FONT_FAMILY,
273
+ [CODE_FONT_WEIGHT_FIELD]: DEFAULT_CODE_FONT_WEIGHT,
274
+ [UI_FONT_WEIGHT_FIELD]: DEFAULT_UI_FONT_WEIGHT,
213
275
  [UI_FONT_SCALE_FIELD]: 1,
214
276
  [CONTENT_FONT_SIZE_FIELD]: 14,
215
277
  [CODE_FONT_SIZE_FIELD]: 12,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citisen/dsh-font",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "DeepSeek Harness plugin: customize the Web GUI fonts (interface family, code family, and three font-size axes) from Settings",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,7 +49,10 @@
49
49
  "build": "node scripts/build-client.mjs",
50
50
  "watch": "node scripts/build-client.mjs --watch",
51
51
  "verify": "node scripts/verify-host.mjs && node scripts/verify-client.mjs",
52
- "check": "node scripts/build-client.mjs --check && npm run verify"
52
+ "check": "node scripts/build-client.mjs --check && npm run verify",
53
+ "check:all": "npm run check && node scripts/verify-profile.mjs",
54
+ "release": "node scripts/release.mjs",
55
+ "prepublishOnly": "npm run check"
53
56
  },
54
57
  "peerDependencies": {
55
58
  "@deepseek-ai/cordis": "^4.0.2"
@@ -72,5 +75,8 @@
72
75
  "theme",
73
76
  "web-ui",
74
77
  "web-gui"
75
- ]
78
+ ],
79
+ "dependencies": {
80
+ "@deepseek-ai/schemastery": "^3.18.2"
81
+ }
76
82
  }
@@ -38,7 +38,30 @@ const PACKAGE_NAME = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')
38
38
  * what the loader consumes; the rest exist so the verifier can unit-test the
39
39
  * pure builders without re-parsing the source.
40
40
  */
41
- const ENVELOPE_EXPORTS = ['apply', 'inject', 'fontStyleSheet', 'applyFonts']
41
+ const ENVELOPE_EXPORTS = [
42
+ 'apply',
43
+ 'inject',
44
+ 'fontStyleSheet',
45
+ 'applyFonts',
46
+ 'parseFamilyList',
47
+ 'serializeFamilyList',
48
+ 'normalizeWeight',
49
+ 'weightWord',
50
+ 'faceWeights',
51
+ 'emphasisWeight',
52
+ 'parseFontQuery',
53
+ 'fontQueryTokens',
54
+ 'serializeFontQuery',
55
+ 'queryContextAt',
56
+ 'querySuggestions',
57
+ 'applySuggestion',
58
+ 'moveFontQueryEntry',
59
+ 'queryTokenClass',
60
+ 'clampHighlight',
61
+ 'describeDiagnostic',
62
+ 'rankFamilyMatches',
63
+ 'FontQueryEditor',
64
+ ]
42
65
 
43
66
  /**
44
67
  * `import [default][, { named }] from 'spec'`, with no nested braces.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Maintainer-side release helper.
3
+ *
4
+ * Publishing is split in two on purpose: CI stages a tarball (see
5
+ * `.github/workflows/stage.yml`), and a human approves it here, where npm
6
+ * demands that human's 2FA. A compromised runner or a malicious commit can
7
+ * therefore stage but never publish.
8
+ *
9
+ * `npm stage` needs npm >= 11.6 (12 for the current subcommands) and Node 22.22
10
+ * / 24.15 / 26+, which the local Node may not satisfy. So rather than requiring
11
+ * a global npm upgrade, this delegates to `npx npm@12`, and borrows the system
12
+ * `git` for the OTP flow npm uses for a 2FA-gated web approval.
13
+ *
14
+ * Usage:
15
+ * node scripts/release.mjs list [package]
16
+ * node scripts/release.mjs view <stage-id>
17
+ * node scripts/release.mjs approve <stage-id>
18
+ * node scripts/release.mjs reject <stage-id>
19
+ * node scripts/release.mjs download <stage-id> [--dry-run]
20
+ */
21
+
22
+ import { spawnSync } from 'node:child_process'
23
+ import { readFileSync } from 'node:fs'
24
+ import { dirname, join } from 'node:path'
25
+ import { fileURLToPath } from 'node:url'
26
+
27
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
28
+ const [, , subcommand, ...rest] = process.argv
29
+
30
+ const PACKAGE_NAME = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name
31
+ const VERSION = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version
32
+
33
+ /** npm CLI major to run; `npm stage` needs 12 for `list`/`approve`. */
34
+ const NPM_SPEC = 'npm@12'
35
+
36
+ const USAGE = `usage: node scripts/release.mjs <command> [args]
37
+
38
+ list [package] staged versions (default: ${PACKAGE_NAME})
39
+ view <stage-id> details of one staged version
40
+ approve <stage-id> publish it — REQUIRES your 2FA
41
+ reject <stage-id> discard it
42
+ download <stage-id> fetch the staged tarball for inspection
43
+
44
+ Nothing here publishes without an approval, which is the point: CI can only
45
+ stage.`
46
+
47
+ const KNOWN = new Set(['list', 'view', 'approve', 'reject', 'download'])
48
+
49
+ if (subcommand === undefined || subcommand === '--help' || subcommand === '-h') {
50
+ console.log(USAGE)
51
+ process.exit(0)
52
+ }
53
+ if (!KNOWN.has(subcommand)) {
54
+ console.error(`release: unknown command "${subcommand}"\n\n${USAGE}`)
55
+ process.exit(2)
56
+ }
57
+ if (subcommand !== 'list' && rest.length === 0) {
58
+ console.error(`release: "${subcommand}" needs a stage-id\n\n${USAGE}`)
59
+ process.exit(2)
60
+ }
61
+
62
+ const args = [NPM_SPEC, 'stage', subcommand]
63
+ if (subcommand === 'list') args.push(rest[0] ?? PACKAGE_NAME)
64
+ else args.push(...rest)
65
+
66
+ console.log(`release: npx ${args.join(' ')}`)
67
+ if (subcommand === 'approve') {
68
+ console.log('release: approving publishes to the registry and requires your 2FA')
69
+ }
70
+
71
+ const result = spawnSync('npx', ['--yes', ...args], {
72
+ cwd: root,
73
+ stdio: 'inherit',
74
+ shell: process.platform === 'win32',
75
+ })
76
+
77
+ if (result.error !== undefined) {
78
+ if (result.error.code === 'ENOENT') {
79
+ console.error('release: npx not found on PATH — install Node.js')
80
+ process.exit(127)
81
+ }
82
+ throw result.error
83
+ }
84
+ const exitCode = result.status ?? 1
85
+
86
+ if (exitCode === 0 && subcommand === 'list') {
87
+ console.log(`\nrelease: nothing is installable until a stage is approved.`)
88
+ console.log(`release: a local release runs \`npm run release -- approve <stage-id>\` from v${VERSION}.`)
89
+ }
90
+
91
+ process.exit(exitCode)