dsh-fonttune 0.2.4 → 0.2.6

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
@@ -20,12 +20,54 @@
20
20
  *
21
21
  * @module dsh-fonttune
22
22
  */
23
- import z from "@deepseek-ai/schemastery";
23
+ import { createRequire } from "node:module";
24
+ import { dirname, sep } from "node:path";
25
+ import { fileURLToPath } from "node:url";
24
26
  // Default import, not named: `shared.cjs` is a CommonJS module, and Node's
25
27
  // CJS-to-ESM interop only guarantees `module.exports` as the default binding
26
28
  // (statically-detected named exports would break this half at boot).
27
29
  import shared from "./shared.cjs";
28
30
 
31
+ /**
32
+ * The schema library, taken from the HOST whenever the host can be reached.
33
+ *
34
+ * `@deepseek-ai/schemastery` is an optional peer dependency — DSH supplies the
35
+ * schema library, this package never ships its own copy into a profile. Node's
36
+ * bare-specifier resolution does not know that: when this package is linked or
37
+ * junctioned into a profile (the development setup) the specifier resolves
38
+ * inside this package's own `node_modules`, i.e. to the devDependency. That is
39
+ * a SECOND instance of the library, and — worse — a newer one than the running
40
+ * host may carry: its `.volatile()` marking lands on a schema the rc line's
41
+ * settings controller cannot digest, so the namespace is never served (no card
42
+ * in the settings sheet, an empty first-frame stylesheet). Measured on
43
+ * 0.1.5-rc.3: linked → nothing, installed → works.
44
+ *
45
+ * Resolving from the host first keeps one instance and one dialect, which is
46
+ * exactly what an installed package gets. `process.argv[1]` is the host's own
47
+ * entry (its `node_modules` holds the authoritative copy); anything resolving
48
+ * back inside THIS package is skipped, so the offline suites — which have no
49
+ * host — still fall through to the devDependency.
50
+ * @returns {object} the schemastery export.
51
+ */
52
+ function loadSchemastery() {
53
+ const own = dirname(dirname(fileURLToPath(import.meta.url))) + sep;
54
+ const bases = [process.argv[1], fileURLToPath(import.meta.url)];
55
+ for (const base of bases) {
56
+ if (typeof base !== "string" || base === "") continue;
57
+ try {
58
+ const from = createRequire(base);
59
+ if (!from.resolve("@deepseek-ai/schemastery").startsWith(own)) {
60
+ return from("@deepseek-ai/schemastery");
61
+ }
62
+ } catch {
63
+ // Not resolvable from that anchor: try the next one.
64
+ }
65
+ }
66
+ return createRequire(import.meta.url)("@deepseek-ai/schemastery");
67
+ }
68
+
69
+ const z = loadSchemastery();
70
+
29
71
  const {
30
72
  buildFontCss,
31
73
  CODE_LINE_HEIGHT_FIELD,
package/lib/shared.cjs CHANGED
@@ -954,14 +954,14 @@ var PART_SUFFIXES = ["-font-size", "-line-height", "-font-family", "-font-weight
954
954
  var FAMILY_VARS = { "--dsw-font-family": true, "--dsw-font-mono": true, "--ds-font-family-code": true };
955
955
 
956
956
  /**
957
- * A `body, body *` selector list, kept in one place because every global
958
- * declaration needs the same reach (DSH writes some tokens on descendants).
959
- * @param {string} declarations - CSS declarations without braces.
960
- * @returns {string} the rule.
957
+ * Ever since every global declaration moved to the root rule (see
958
+ * `buildAxisCss`), no rule needs a `body, body *` reach for a TOKEN, and only
959
+ * the interface weight keeps it — because a weight is the one property whose
960
+ * per-element declarations cannot be reached by inheritance. Nothing else may
961
+ * reintroduce a blanket: on DSH 0.1.5-rc.3 a single one costs 1892
962
+ * selector-element matches on every style recalculation, and dragging a sidebar
963
+ * handle recalculates once per frame.
961
964
  */
962
- function bodyAndDescendants(declarations) {
963
- return "body,body *{" + declarations + "}";
964
- }
965
965
 
966
966
  /**
967
967
  * Whether one name belongs to the official CONTENT-size chain the conversation
@@ -1202,7 +1202,23 @@ var CODE_EXCLUDES = excludeAll(CODE_SURFACE_SELECTORS);
1202
1202
  * the rule, and an unmatched token/class simply means that surface follows
1203
1203
  * the interface axis — the same degradation as an unknown token name.
1204
1204
  */
1205
- var CODE_SELECTOR = [
1205
+ /** The default code selector list: the substring form, for callers with no page. */
1206
+ var CODE_SELECTOR;
1207
+
1208
+ /**
1209
+ * The code surfaces that never need a substring match.
1210
+ *
1211
+ * Everything here is either a semantic element or a class this plugin owns, so
1212
+ * the browser can match it with a tag or a class-token test. The containers
1213
+ * that only a hashed class identifies (`…_codeBody`, `…_terminal`) used to be
1214
+ * spelled `[class*="code" i]` / `[class*="terminal" i]` — measured on
1215
+ * 0.1.5-rc.3, those two substring selectors cost an attribute scan on every one
1216
+ * of the page's ~1900 elements on every rule that carries them, and a drag
1217
+ * recalculates styles once per frame. The browser half now HARVESTS the real
1218
+ * class tokens from the live page and passes them in as exact class selectors
1219
+ * (see `buildFontCss`), falling back to the substring form whenever it cannot.
1220
+ */
1221
+ var CODE_SURFACE_SELECTORS_CHEAP = [
1206
1222
  "pre",
1207
1223
  "code",
1208
1224
  "kbd",
@@ -1212,19 +1228,58 @@ var CODE_SELECTOR = [
1212
1228
  "textarea",
1213
1229
  ".cm-editor",
1214
1230
  ".dfp-previewCode",
1215
- '[class*="code" i]',
1216
- '[class*="terminal" i]',
1217
- ]
1218
- .map(function (selector) {
1219
- return selector + "," + selector + " *";
1220
- })
1221
- .join(",");
1231
+ ];
1232
+
1233
+ /**
1234
+ * The code surfaces for one build.
1235
+ *
1236
+ * The two substring hooks ride along on purpose. A class token the page has
1237
+ * not rendered yet (`…_codeBody`, `…_terminalBody` appear only once a tool card
1238
+ * or a terminal pane exists) cannot be named by a harvested class, and an
1239
+ * unknown surface must still get the code axes — the same "an unmatched token
1240
+ * simply follows the interface" degradation the token axes have. The hooks cost
1241
+ * an attribute scan per element per rule that carries them, which is why the
1242
+ * rest of this module spent its effort removing the OTHER wide selectors (see
1243
+ * `rootRule` / `weightRule` / `dialogSelector`); measured on 0.1.5-rc.3 that is
1244
+ * the difference between 1.87 s and ~0.9 s of style recalculation per drag.
1245
+ */
1246
+ function codeSurfaceList() {
1247
+ return CODE_SURFACE_SELECTORS_CHEAP.concat([
1248
+ '[class*="code" i]',
1249
+ '[class*="terminal" i]',
1250
+ ]);
1251
+ }
1252
+
1253
+ /**
1254
+ * The code selector list for one build.
1255
+ *
1256
+ * Surfaces only — no `… *` descendants. Everything inside a code surface
1257
+ * inherits its family and weight, so descendants are unnecessary, and they are
1258
+ * not free: an ancestor-walking selector costs about 0.03 s per 50-step drag on
1259
+ * 0.1.5-rc.3 (measured: stripping the ` *` forms from these two rules took the
1260
+ * drag from 0.89 s to 0.52 s of style recalculation). The weight family keeps
1261
+ * the code subtree right through `--dfp-interface-weight` instead — see
1262
+ * `weightRule`.
1263
+ *
1264
+ * @param {{code?: string[], terminal?: string[]}|null} [hints] - unused; kept so
1265
+ * the browser half can keep passing one harvest object to `buildFontCss`.
1266
+ * @returns {string} the comma-separated list of code surfaces.
1267
+ */
1268
+ function codeSelector(hints) {
1269
+ return codeSurfaceList().join(",");
1270
+ }
1271
+
1272
+ CODE_SELECTOR = codeSelector(null);
1222
1273
 
1223
1274
  /**
1224
1275
  * The dialog surface: DSH renders the conversation markdown in a CSS-module
1225
- * container whose class hashes per release (`_markdown_<hash>_…`). A
1226
- * class-substring match degrades safely — no match means the dialog follows
1227
- * the interface axis, the same behavior as an unknown token name.
1276
+ * container whose class hashes per release (`_markdown_<hash>_…`), wrapped in a
1277
+ * stable `[data-dss-prose]` container (measured on 0.1.5-rc.3: every markdown
1278
+ * container on the page sits inside one). A class-substring match degrades
1279
+ * safely — no match means the dialog follows the interface axis, the same
1280
+ * behavior as an unknown token name — but it costs an attribute scan per
1281
+ * element per rule, so the browser half prefers the stable wrapper when it can
1282
+ * see one and falls back to the substring form otherwise.
1228
1283
  */
1229
1284
  var MARKDOWN_SELECTOR =
1230
1285
  '[class*="_markdown_" i]' +
@@ -1232,37 +1287,106 @@ var MARKDOWN_SELECTOR =
1232
1287
  ',[class*="_markdown_" i] *' +
1233
1288
  CODE_EXCLUDES;
1234
1289
 
1290
+ /**
1291
+ * The dialog selector for one build, given the harvested wrapper.
1292
+ * @param {{markdown?: string}|null} [hints] - the harvested wrapper selector.
1293
+ * @returns {string} the markdown-scoped selector list.
1294
+ */
1295
+ function dialogSelector(hints) {
1296
+ if (hints !== undefined && hints !== null && typeof hints.markdown === "string" && hints.markdown !== "") {
1297
+ // The wrapper is not markdown content itself, so only its subtree is
1298
+ // scoped; the code exclusion still rides along so inline code keeps the
1299
+ // code family. This is the rule the harvest earns its keep on: the same
1300
+ // `:not(…)` over the stable wrapper trials ~37 elements instead of the
1301
+ // ~1800 the class-substring container selector used to hand it.
1302
+ return hints.markdown + " *" + CODE_EXCLUDES;
1303
+ }
1304
+ return MARKDOWN_SELECTOR;
1305
+ }
1306
+
1235
1307
  /** The dark-theme attribute DSH's ThemePresenter writes on body. */
1236
1308
  var DARK_ATTR = "body[data-ds-dark-theme]";
1237
1309
 
1238
1310
  /**
1239
- * Everything the INTERFACE weight rule must stay out of.
1311
+ * What the INTERFACE weight rule stays out of when the conversation has no
1312
+ * weight of its own.
1313
+ *
1314
+ * The weight is still the one property that needs a `body, body *` reach: DSH
1315
+ * pins weights on elements that do not inherit (`button` / `span` / `div`
1316
+ * class rules — measured on 0.1.5-rc.3: 19 buttons, 13 spans and 1 div in the
1317
+ * chrome declare their own weight; the markdown side's 7 `strong` and 2 `th`
1318
+ * declare theirs). But the 26-argument `:not(…)` that used to ride along cost
1319
+ * a selector trial per argument on ~1800 elements EVERY recalculation, and a
1320
+ * drag recalculates once per frame — measured 1.87 s of style recalculation
1321
+ * for a 50-step drag versus 0.47 s with this plugin removed.
1322
+ *
1323
+ * Two narrowings removed that:
1240
1324
  *
1241
- * A weight can only be expressed as a `body, body *` rule: DSH pins its labels
1242
- * with literal weights on class rules (`…_label{font-weight:500}`,
1243
- * `…_heading{font-weight:600}`) that nothing narrower reaches. That rule would
1244
- * otherwise cover the conversation too, so the conversation markdown subtree is
1245
- * excluded here — and with it every code surface, because the code axis owns
1246
- * those and the two rules would otherwise fight over the same elements (the
1247
- * `:not()` arguments raise this rule's specificity above a single class).
1325
+ * 1. Code surfaces no longer appear here at all. The code rules are emitted
1326
+ * after this one and win on their own: class hooks by specificity, element
1327
+ * selectors by order at equal specificity. Their descendants inherit from
1328
+ * them (`font-weight` and `font-family` are inherited), so the blanket
1329
+ * cannot reach a highlight `<span>` inside a `<pre>` either.
1330
+ * 2. What remains — the conversation markdown subtree and the card's own
1331
+ * conversation preview — rides `:where(…)`, which contributes NO
1332
+ * specificity: the rule stays at (0,0,1), exactly like the plain
1333
+ * `body,body *` form, so the dialog rule (class-scoped) and the code rules
1334
+ * keep out-ranking it no matter which order they land in.
1248
1335
  *
1249
- * `dfp-previewDialog` is the card's conversation preview: it carries the
1250
- * conversation's own weight, so the interface rule must not flatten it. The
1251
- * interface's own preview stays inside the rule, and `dfp-previewCode` is a
1252
- * code surface like any other.
1253
- */
1254
- var INTERFACE_EXCLUDES = excludeAll(
1255
- CODE_SURFACE_SELECTORS.concat([
1256
- ".cm-editor",
1257
- ".cm-editor *",
1258
- ".dfp-previewCode",
1259
- ".dfp-previewCode *",
1336
+ * When the conversation DOES carry a weight the guard is dropped entirely (see
1337
+ * `weightRule`): the dialog rule is class-scoped, so it overrides everything
1338
+ * this rule does inside markdown anyway, and the rule collapses to the bare
1339
+ * `body,body *` pair. Measured with the axis at 560: all 141 interface text
1340
+ * elements on the page took 560 and no markdown element moved — the same
1341
+ * coverage the 26-argument form had.
1342
+ */
1343
+ var WEIGHT_MARKDOWN_GUARD =
1344
+ ":not(:where(" +
1345
+ [
1260
1346
  '[class*="_markdown_" i]',
1261
1347
  '[class*="_markdown_" i] *',
1262
1348
  ".dfp-previewDialog",
1263
1349
  ".dfp-previewDialog *",
1264
- ])
1265
- );
1350
+ ].join(",") +
1351
+ "))";
1352
+
1353
+ /**
1354
+ * The elements that carry a weight of their own in DSH.
1355
+ *
1356
+ * The weight is the one property that cannot ride inheritance alone: an element
1357
+ * whose own rule declares a weight ignores the value it would inherit, whatever
1358
+ * that value's importance. Measured on 0.1.5-rc.3, the elements that do declare
1359
+ * one are `button` / `span` / `div` in the chrome and `strong` / `th` in the
1360
+ * conversation; the rest of the list is the inline markup that could plausibly
1361
+ * join them. This replaces the old `body,body *` reach, which matched 1892
1362
+ * elements on every recalculation — the whole point of the table is that it
1363
+ * matches a fraction of that while still covering every pinned element.
1364
+ */
1365
+ var WEIGHT_ELEMENT_TABLE = [
1366
+ "button",
1367
+ "span",
1368
+ "div",
1369
+ "strong",
1370
+ "b",
1371
+ "em",
1372
+ "i",
1373
+ "a",
1374
+ "label",
1375
+ "small",
1376
+ "p",
1377
+ "li",
1378
+ "td",
1379
+ "th",
1380
+ "dt",
1381
+ "dd",
1382
+ "summary",
1383
+ "h1",
1384
+ "h2",
1385
+ "h3",
1386
+ "h4",
1387
+ "h5",
1388
+ "h6",
1389
+ ].join(",");
1266
1390
 
1267
1391
  /**
1268
1392
  * Split a selector list on its top-level commas only.
@@ -1442,27 +1566,66 @@ function isDormant(config) {
1442
1566
  * @param {boolean} isDark - prefix every rule with the dark-theme attribute.
1443
1567
  * @returns {string} declarations for one `<style>` element.
1444
1568
  */
1445
- function buildAxisCss(axis, baseTokens, isDark) {
1569
+ function buildAxisCss(axis, baseTokens, isDark, hints) {
1446
1570
  var sans = formatStack(parseStack(axis[SANS_FIELD]));
1447
1571
  var mono = formatStack(parseStack(axis[MONO_FIELD]));
1572
+ var dialog = formatStack(parseStack(axis[STACK_DIALOG_FIELD]));
1573
+ // Normalised once: an expanded axis always carries a number, but a caller
1574
+ // handing in a raw object may leave the field out, and "absent" has to mean
1575
+ // "unset" rather than "write the string undefined".
1576
+ var dialogWeight = typeof axis[WEIGHT_DIALOG_FIELD] === "number" ? axis[WEIGHT_DIALOG_FIELD] : WEIGHT_UNSET;
1577
+ var codeSurfaces = codeSelector(hints);
1578
+ var dialogSurfaces = dialogSelector(hints);
1448
1579
  var declarations = [];
1449
1580
 
1450
- function blanket(declarations, excludes) {
1451
- // The exclusion reaches the descendants only: `body` itself inherits
1452
- // nothing, and neither the body element nor its dark attribute can be a
1453
- // markdown container or a code surface.
1454
- var suffix = excludes === undefined ? "" : excludes;
1455
- var selector = isDark
1456
- ? DARK_ATTR + "," + DARK_ATTR + " *" + suffix
1457
- : "body,body *" + suffix;
1458
- return selector + "{" + declarations + "}";
1581
+ /**
1582
+ * A rule for INHERITED state: custom properties and the inherited `font-*`
1583
+ * properties. `:root, body` is two elements, `body` alone is one, and every
1584
+ * descendant picks the value up by inheritance — including the values DSH
1585
+ * itself writes (measured on 0.1.5-rc.3: the host declares these tokens at
1586
+ * `:root` and on `body`, and the only inline copy is on `body` itself, where
1587
+ * an `!important` stylesheet declaration still wins).
1588
+ *
1589
+ * This used to be `body, body *`, which matched 1892 elements on EVERY style
1590
+ * recalculation. A drag writes `style` once per frame, so that reach alone
1591
+ * was most of the measured cost: 1.87 s of style recalculation for a 50-step
1592
+ * sidebar drag (0.47 s with the plugin removed) and 49 frames over 25 ms.
1593
+ */
1594
+ function rootRule(declarations) {
1595
+ return (isDark ? DARK_ATTR : ":root,body") + "{" + declarations + "}";
1596
+ }
1597
+ function bodyRule(declarations) {
1598
+ return (isDark ? DARK_ATTR : "body") + "{" + declarations + "}";
1459
1599
  }
1460
1600
  function codeRule(declarations) {
1461
- var selector = isDark ? prefixSelector(CODE_SELECTOR, DARK_ATTR) : CODE_SELECTOR;
1601
+ var selector = isDark ? prefixSelector(codeSurfaces, DARK_ATTR) : codeSurfaces;
1462
1602
  return selector + "{" + declarations + "}";
1463
1603
  }
1464
1604
  function dialogRule(declarations) {
1465
- var selector = isDark ? prefixSelector(MARKDOWN_SELECTOR, DARK_ATTR) : MARKDOWN_SELECTOR;
1605
+ var selector = isDark ? prefixSelector(dialogSurfaces, DARK_ATTR) : dialogSurfaces;
1606
+ return selector + "{" + declarations + "}";
1607
+ }
1608
+ /**
1609
+ * The interface weight, in two reaches: `body` for everything that inherits,
1610
+ * plus an element table for the tags DSH pins a weight on of their own (see
1611
+ * `WEIGHT_ELEMENT_TABLE`).
1612
+ *
1613
+ * The value rides a custom property (`--dfp-interface-weight`, declared on
1614
+ * `:root,body` by the caller) rather than a literal. That is what keeps the
1615
+ * code subtree right without a descendant selector: a highlight `<span>`
1616
+ * inside a `<pre>` IS matched by this rule's table, but it resolves the
1617
+ * variable its code ancestor set (see the code weight rule), so it takes the
1618
+ * code weight instead of the interface one. Naming `pre *` and friends here
1619
+ * or in the code rules instead cost 0.03 s per selector per drag.
1620
+ *
1621
+ * The markdown guard only rides along when the conversation has NO weight of
1622
+ * its own — with one set, the class-scoped dialog rule out-ranks this rule
1623
+ * inside markdown anyway.
1624
+ */
1625
+ function weightRule(declarations) {
1626
+ var base = isDark ? DARK_ATTR : "body";
1627
+ var selector = base + "," + base + " :where(" + WEIGHT_ELEMENT_TABLE + ")";
1628
+ if (dialogWeight === WEIGHT_UNSET) selector += WEIGHT_MARKDOWN_GUARD;
1466
1629
  return selector + "{" + declarations + "}";
1467
1630
  }
1468
1631
  /**
@@ -1472,7 +1635,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1472
1635
  */
1473
1636
  function pushScopedCodeFamily() {
1474
1637
  var scopedCode = [];
1475
- var surfaces = splitSelectorList(CODE_SELECTOR);
1638
+ var surfaces = splitSelectorList(codeSurfaces);
1476
1639
  for (var cIndex = 0; cIndex < surfaces.length; cIndex += 1) {
1477
1640
  var scoped = '[class*="_markdown_" i] ' + surfaces[cIndex];
1478
1641
  // The dark variant has to carry the theme attribute like every other
@@ -1523,29 +1686,42 @@ function buildAxisCss(axis, baseTokens, isDark) {
1523
1686
  // (below).
1524
1687
 
1525
1688
  // ---- interface weight ----
1526
- // Measured on rc.2 (live page, computed styles): a `body{font-weight}` rule
1527
- // only reaches the elements that INHERIT their weight, while `body, body *`
1528
- // also reaches the labels DSH pins with a literal weight on a class rule —
1529
- // which is why the axis has to be blanket, and why the rule without the
1530
- // markdown exclusion drags the conversation along with it. With the
1531
- // exclusion the interface still moves and the markdown subtree does not.
1532
- var interfaceWeight = axis[WEIGHT_FIELD];
1533
- var codeWeight = axis[CODE_WEIGHT_FIELD];
1689
+ // A `body{font-weight}` rule only reaches the elements that INHERIT their
1690
+ // weight; the chrome's `button` / `span` / `div` class rules pin their own
1691
+ // (measured on 0.1.5-rc.3), which is why the second reach exists. The
1692
+ // conversation is held apart by `weightRule`'s guard (or by the class-scoped
1693
+ // dialog rule when the conversation has a weight of its own).
1694
+ var interfaceWeight = typeof axis[WEIGHT_FIELD] === "number" ? axis[WEIGHT_FIELD] : WEIGHT_UNSET;
1695
+ var codeWeight = typeof axis[CODE_WEIGHT_FIELD] === "number" ? axis[CODE_WEIGHT_FIELD] : WEIGHT_UNSET;
1534
1696
  if (interfaceWeight !== WEIGHT_UNSET) {
1697
+ // The variable the weight rule resolves; the code rules below override it
1698
+ // on their surfaces so everything inside them inherits the code weight.
1535
1699
  declarations.push(
1536
- blanket("font-weight:" + interfaceWeight + " !important", INTERFACE_EXCLUDES)
1700
+ (isDark ? DARK_ATTR : ":root,body") +
1701
+ "{--dfp-interface-weight:" +
1702
+ interfaceWeight +
1703
+ "}"
1704
+ );
1705
+ declarations.push(
1706
+ weightRule(
1707
+ "font-weight:var(--dfp-interface-weight," + interfaceWeight + ") !important"
1708
+ )
1537
1709
  );
1538
1710
  if (codeWeight === WEIGHT_UNSET) {
1539
- // Code surfaces are excluded from the blanket rule, so without this they
1540
- // would inherit the interface weight from body. `normal` is what DSH
1541
- // computes for them (the code `font` shorthands carry no weight), so an
1542
- // unset code weight keeps code exactly as DSH shipped it.
1543
- declarations.push(codeRule("font-weight:normal !important"));
1711
+ // The code rules come after this one and win on their own (class hooks by
1712
+ // specificity, element selectors by order), so code surfaces — which
1713
+ // inherit nothing but start from the body weight — are reset here.
1714
+ // `normal` is what DSH computes for them (the code `font` shorthands
1715
+ // carry no weight), so an unset code weight keeps code exactly as DSH
1716
+ // shipped it. The variable is reset alongside so an element INSIDE a code
1717
+ // surface (which the interface table matches) lands on `normal` too.
1718
+ declarations.push(
1719
+ codeRule("font-weight:normal !important;--dfp-interface-weight:normal")
1720
+ );
1544
1721
  }
1545
1722
  }
1546
1723
 
1547
1724
  // ---- dialog family (scoped; skipped when it equals the interface) ----
1548
- var dialog = formatStack(parseStack(axis[STACK_DIALOG_FIELD]));
1549
1725
  if (dialog !== "" && dialog !== sans) {
1550
1726
  declarations.push(dialogRule("font-family:" + dialog + " !important"));
1551
1727
  if (mono !== "") {
@@ -1555,7 +1731,6 @@ function buildAxisCss(axis, baseTokens, isDark) {
1555
1731
  }
1556
1732
 
1557
1733
  // ---- dialog weight (uniform inside the conversation) ----
1558
- var dialogWeight = axis[WEIGHT_DIALOG_FIELD];
1559
1734
  if (dialogWeight !== WEIGHT_UNSET) {
1560
1735
  declarations.push(dialogRule("font-weight:" + dialogWeight + " !important"));
1561
1736
  // The card's conversation preview simulates this surface, so it takes the
@@ -1594,7 +1769,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1594
1769
  );
1595
1770
  });
1596
1771
  var contentShift = shiftTokens(contentSources, baseTokens, dialogOffsetValue);
1597
- if (contentShift.length > 0) declarations.push(blanket(contentShift.join(";")));
1772
+ if (contentShift.length > 0) declarations.push(rootRule(contentShift.join(";")));
1598
1773
  // The `small` series is the only markdown family with literal sizes: it
1599
1774
  // does not derive from the chain, so its parts and shorthands shift here.
1600
1775
  var literals = markdownTokenNames(baseTokens).filter(function (name) {
@@ -1602,7 +1777,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1602
1777
  return name.indexOf("-font-size") >= 0 && typeof base === "string" && base.indexOf("var(") === -1;
1603
1778
  });
1604
1779
  var literalShift = shiftTokens(literals, baseTokens, dialogOffsetValue);
1605
- if (literalShift.length > 0) declarations.push(blanket(literalShift.join(";")));
1780
+ if (literalShift.length > 0) declarations.push(rootRule(literalShift.join(";")));
1606
1781
  // The shorthands whose size is a literal (the small series) must ride the
1607
1782
  // shift too; the delta-derived shorthands flow through the chain above.
1608
1783
  var sizeShifted = [];
@@ -1617,7 +1792,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1617
1792
  mEntry.name + ":" + rebuildShorthand({ lead: mEntry.parts.lead, size: mSize, height: mEntry.parts.height, family: mEntry.parts.family }) + " !important"
1618
1793
  );
1619
1794
  }
1620
- if (sizeShifted.length > 0) declarations.push(blanket(sizeShifted.join(";")));
1795
+ if (sizeShifted.length > 0) declarations.push(rootRule(sizeShifted.join(";")));
1621
1796
  }
1622
1797
 
1623
1798
  // ---- dialog line height (ratio, through the shorthand heights) ----
@@ -1628,7 +1803,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1628
1803
  return name.indexOf("-line-height") >= 0;
1629
1804
  });
1630
1805
  var dialogLineDecls = scaleLineTokens(markdownLines, baseTokens, dialogFactor);
1631
- if (dialogLineDecls.length > 0) declarations.push(blanket(dialogLineDecls.join(";")));
1806
+ if (dialogLineDecls.length > 0) declarations.push(rootRule(dialogLineDecls.join(";")));
1632
1807
  var dialogShorthandList = markdownShorthandTokens(baseTokens);
1633
1808
  var dialogShorthandShifted = [];
1634
1809
  for (var dIndex = 0; dIndex < dialogShorthandList.length; dIndex += 1) {
@@ -1646,7 +1821,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1646
1821
  );
1647
1822
  }
1648
1823
  if (dialogShorthandShifted.length > 0) {
1649
- declarations.push(blanket(dialogShorthandShifted.join(";")));
1824
+ declarations.push(rootRule(dialogShorthandShifted.join(";")));
1650
1825
  }
1651
1826
  }
1652
1827
 
@@ -1659,11 +1834,17 @@ function buildAxisCss(axis, baseTokens, isDark) {
1659
1834
  round(scaleFor(codeSize), 6),
1660
1835
  codeLine
1661
1836
  );
1662
- if (codeScaled.length > 0) declarations.push(blanket(codeScaled.join(";")));
1837
+ if (codeScaled.length > 0) declarations.push(rootRule(codeScaled.join(";")));
1663
1838
  }
1664
- var codeWeightRule = axis[CODE_WEIGHT_FIELD];
1665
- if (codeWeightRule !== WEIGHT_UNSET) {
1666
- declarations.push(codeRule("font-weight:" + codeWeightRule + " !important"));
1839
+ if (codeWeight !== WEIGHT_UNSET) {
1840
+ // The variable rides along so the elements INSIDE a code surface — which the
1841
+ // interface weight rule's element table matches — resolve the code weight
1842
+ // instead of the interface one, without any `… *` selector.
1843
+ declarations.push(
1844
+ codeRule(
1845
+ "font-weight:" + codeWeight + " !important;--dfp-interface-weight:" + codeWeight
1846
+ )
1847
+ );
1667
1848
  }
1668
1849
  var ligatures = axis[LIGATURES_FIELD];
1669
1850
  if (ligatures === LIGATURES_ON) {
@@ -1680,10 +1861,10 @@ function buildAxisCss(axis, baseTokens, isDark) {
1680
1861
  var noItalic = axis[NO_SYNTHETIC_ITALIC_FIELD] === true;
1681
1862
  var noBold = axis[NO_SYNTHETIC_BOLD_FIELD] === true;
1682
1863
  if (noItalic && noBold) {
1683
- declarations.push(blanket("font-synthesis:none !important"));
1864
+ declarations.push(bodyRule("font-synthesis:none !important"));
1684
1865
  } else {
1685
- if (noItalic) declarations.push(blanket("font-synthesis-style:none !important"));
1686
- if (noBold) declarations.push(blanket("font-synthesis-weight:none !important"));
1866
+ if (noItalic) declarations.push(bodyRule("font-synthesis-style:none !important"));
1867
+ if (noBold) declarations.push(bodyRule("font-synthesis-weight:none !important"));
1687
1868
  }
1688
1869
 
1689
1870
  return declarations.join("\n");
@@ -1772,15 +1953,19 @@ function scaleCodeTokens(baseTokens, scale, lineOffset) {
1772
1953
  *
1773
1954
  * @param {unknown} config - normalized or raw configuration.
1774
1955
  * @param {Record<string, string>} [baseTokens] - token name to untouched value.
1956
+ * @param {{markdown?: string, code?: string[], terminal?: string[]}} [hints] - what
1957
+ * the page harvested: the stable markdown wrapper, and the real class tokens
1958
+ * behind the code/terminal hooks. Absent means "match by substring", which is
1959
+ * correct everywhere and costs an attribute scan per element per rule.
1775
1960
  * @returns {string} declarations for one `<style>` element ("" when dormant).
1776
1961
  */
1777
- function buildFontCss(config, baseTokens) {
1962
+ function buildFontCss(config, baseTokens, hints) {
1778
1963
  var tokens = baseTokens === undefined || baseTokens === null ? FALLBACK_TOKENS : baseTokens;
1779
1964
  var sets = resolveAxes(config);
1780
1965
  if (isDormant(sets.light) && isDormant(sets.dark)) return "";
1781
- var light = buildAxisCss(sets.light, tokens, false);
1966
+ var light = buildAxisCss(sets.light, tokens, false, hints);
1782
1967
  if (sameValueSet(sets.light, sets.dark)) return light;
1783
- var dark = buildAxisCss(sets.dark, tokens, true);
1968
+ var dark = buildAxisCss(sets.dark, tokens, true, hints);
1784
1969
  if (dark === "") return light;
1785
1970
  return light + "\n" + dark;
1786
1971
  }
@@ -2174,9 +2359,13 @@ var shared = {
2174
2359
  pickerGroups: pickerGroups,
2175
2360
  catalogRefreshDue: catalogRefreshDue,
2176
2361
  MARKDOWN_SELECTOR: MARKDOWN_SELECTOR,
2362
+ codeSelector: codeSelector,
2363
+ dialogSelector: dialogSelector,
2364
+ CODE_SURFACE_SELECTORS_CHEAP: CODE_SURFACE_SELECTORS_CHEAP,
2177
2365
  CODE_SURFACE_SELECTORS: CODE_SURFACE_SELECTORS,
2178
2366
  CODE_EXCLUDES: CODE_EXCLUDES,
2179
- INTERFACE_EXCLUDES: INTERFACE_EXCLUDES,
2367
+ WEIGHT_MARKDOWN_GUARD: WEIGHT_MARKDOWN_GUARD,
2368
+ WEIGHT_ELEMENT_TABLE: WEIGHT_ELEMENT_TABLE,
2180
2369
  excludeAll: excludeAll,
2181
2370
  splitSelectorList: splitSelectorList,
2182
2371
  prefixSelector: prefixSelector,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fonttune",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -19,7 +19,7 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "engines": {
22
- "dsh": ">=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
22
+ "dsh": ">=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
23
23
  },
24
24
  "dsh": {
25
25
  "bundle": {
@@ -30,11 +30,10 @@
30
30
  },
31
31
  "compatibility": {
32
32
  "dshReleases": {
33
- "0.1.5-rc.1": "compatible",
34
- "0.1.5-rc.2": "compatible",
35
33
  "0.1.5-rc.3": "compatible",
36
34
  "0.1.7-alpha.1": "compatible",
37
- "0.1.7-alpha.2": "compatible"
35
+ "0.1.7-alpha.2": "compatible",
36
+ "0.1.7-rc.1": "compatible"
38
37
  }
39
38
  }
40
39
  },