@stapel/tokens-antd 0.2.0 → 0.4.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/README.md CHANGED
@@ -1,15 +1,18 @@
1
1
  # @stapel/tokens-antd
2
2
 
3
- The Ant Design leg of the **token bridge** (frontend-guidelines §2.4; owner
4
- decision §38 T3). It projects `@stapel/tokens` L2 core tokens onto an Ant Design
5
- `ConfigProvider` theme — so integrating a `@stapel/*-react` pair's default skin
6
- already carries the user's theme, light and dark.
3
+ The Ant Design leg of the **token bridge** (§68; frontend-guidelines §2.4;
4
+ owner decision §38 T3). It projects `@stapel/tokens`' neutral colour roles
5
+ onto an Ant Design `ConfigProvider` theme — so integrating a
6
+ `@stapel/*-react` pair's default skin already carries the user's theme,
7
+ light and dark.
7
8
 
8
- There is exactly **one** L2 design-system role mapping table, and it lives in
9
- `@stapel/tokens` (`bridgeColorRoles` + the radius/font-size roles). This package
10
- only renames those neutral roles to antd's flat token field names and picks the
11
- `light`/`dark` half it holds no colour decisions of its own, so it and
12
- `@stapel/tokens-mui` cannot drift.
9
+ The §68 neutral role dictionary IS the bridge vocabulary a role name
10
+ (`brand`, `surface-raised`, `text-muted`, …) is both the CSS var suffix
11
+ (`--stapel-brand`) and the exact key this package reads off `@stapel/tokens`'
12
+ `colors` object. There is no separate role→role indirection table (the old
13
+ `bridgeColorRoles` map is gone) — this package only renames roles to antd's
14
+ flat token field names and picks the `light`/`dark` half; it holds no colour
15
+ decisions of its own, so it and `@stapel/tokens-mui` cannot drift.
13
16
 
14
17
  ## Usage
15
18
 
@@ -34,20 +37,28 @@ import { toAntdThemeConfig } from "@stapel/tokens-antd";
34
37
  </ConfigProvider>;
35
38
  ```
36
39
 
37
- ## The mapping (frontend-guidelines §2.4)
38
-
39
- | `@stapel/tokens` role | L2 core token | antd `theme.token` |
40
- | --- | --- | --- |
41
- | `brand` | `accent` | `colorPrimary` |
42
- | `success` | `text-positive` | `colorSuccess` |
43
- | `warning` | `text-warning` | `colorWarning` |
44
- | `danger` | `text-negative` | `colorError` |
45
- | `info` | `text-info` | `colorInfo` |
46
- | `bgContainer` | `upperground-primary` | `colorBgContainer` / `colorBgElevated` |
47
- | `bgLayout` | `background-primary` | `colorBgLayout` |
48
- | `textPrimary` | `text-primary` | `colorText` |
49
- | `link` | `text-brand` | `colorLink` |
50
- | radius `md` | — | `borderRadius` |
51
- | font-size `md` | | `fontSize` |
40
+ ## The mapping (§68; frontend-guidelines §2.4)
41
+
42
+ | `@stapel/tokens` role | antd `theme.token` |
43
+ | --- | --- |
44
+ | `brand` | `colorPrimary` |
45
+ | `success` / `warning` / `error` / `info` | `colorSuccess` / `colorWarning` / `colorError` / `colorInfo` |
46
+ | `text` | `colorText` |
47
+ | `text-muted` | `colorTextSecondary` |
48
+ | `text-subtle` | `colorTextTertiary` |
49
+ | `surface` | `colorBgLayout` |
50
+ | `surface-raised` | `colorBgContainer` |
51
+ | `surface-overlay` | `colorBgElevated` |
52
+ | `border` | `colorBorder` |
53
+ | `border-subtle` | `colorBorderSecondary` |
54
+ | `link` / `link-hover` | `colorLink` / `colorLinkHover` |
55
+ | radius `md` | `borderRadius` |
56
+ | font-size `md` | `fontSize` |
57
+
58
+ Every field above is resolved by reading the host's **live**
59
+ `--stapel-<role>` CSS custom property off `document.documentElement` at call
60
+ time (falling back to the compiled-in default only with no DOM — SSR/tests) —
61
+ so a host's customized brand colour flows through even to antd's seed-token
62
+ colour derivation (hover/active shades), not just its own light/dark mode.
52
63
 
53
64
  `antd` is a peer dependency — the consuming app brings its own copy.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import type { ThemeConfig } from "antd";
2
- /** Light or dark — the half of every L2 core token's `{light,dark}` pair. */
2
+ /** Light or dark — the half of every role's `{light,dark}` pair. */
3
3
  export type ThemeMode = "light" | "dark";
4
4
  /** The flat antd token map (`ThemeConfig["token"]`), never undefined. */
5
5
  export type AntdThemeToken = NonNullable<ThemeConfig["token"]>;
6
6
  /**
7
- * `@stapel/tokens` L2 → antd `theme.token` (frontend-guidelines §2.4 table).
8
- * Pure: same `mode` in, same object out; reads no globals.
7
+ * `@stapel/tokens` §68 roles → antd `theme.token` (frontend-guidelines §2.4
8
+ * table). Pure: same `mode` in, same object out; reads no globals besides the
9
+ * live CSS custom properties documented on {@link readLiveCssVar}.
9
10
  */
10
11
  export declare function toAntdTheme(mode: ThemeMode): AntdThemeToken;
11
12
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AAYxC,6EAA6E;AAC7E,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAOzC,yEAAyE;AACzE,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;AAE/D;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,cAAc,CAoB3D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,WAAW,CAM9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AAYxC,oEAAoE;AACpE,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AA6CzC,yEAAyE;AACzE,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;AAE/D;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,cAAc,CAqB3D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,WAAW,CAM9D"}
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
- * `@stapel/tokens-antd` — the Ant Design leg of the token bridge
3
- * (frontend-guidelines §2.4, owner decision §38 T3). It translates the ONE
4
- * neutral role table in `@stapel/tokens` (`bridgeColorRoles` + the radius/
5
- * font-size roles) into Ant Design's flat `ConfigProvider` theme token. The
6
- * L2 role decision is NOT duplicated here this file only renames roles to
7
- * antd field names and picks the light/dark half, so it and `@stapel/tokens-mui`
8
- * cannot drift.
2
+ * `@stapel/tokens-antd` — the Ant Design leg of the token bridge (§68;
3
+ * frontend-guidelines §2.4, owner decision §38 T3). It translates the ONE
4
+ * neutral colour-role dictionary in `@stapel/tokens` (§68 `surface`,
5
+ * `brand`, `text-muted`, `success-bg`, …) straight into Ant Design's flat
6
+ * `ConfigProvider` theme token: a role name IS the antd mapping's input, no
7
+ * separate role→role indirection table (that table `bridgeColorRoles`
8
+ * is gone; §68 Ф1 "алиас-слой совместимости не делать"). The mapping table
9
+ * below (role → antd field) is the ONE place this decision lives; both this
10
+ * file and `@stapel/tokens-mui` read the same `colors` object so they cannot
11
+ * silently diverge on what a role visually means.
9
12
  *
10
13
  * ```tsx
11
14
  * import { toAntdTheme } from "@stapel/tokens-antd";
@@ -21,31 +24,71 @@
21
24
  * and spread it directly: `<ConfigProvider theme={toAntdThemeConfig(mode)}>`.
22
25
  */
23
26
  import { theme as antdTheme } from "antd";
24
- import { bridgeColorRoles, bridgeFontSizeRole, bridgeRadiusRole, colors, fontFamily, fontSize, radii, } from "@stapel/tokens";
25
- /** Resolve a neutral colour role to its hex for `mode` via the shared table. */
27
+ import { bridgeFontSizeRole, bridgeRadiusRole, colors, cssVar, fontFamily, fontSize, radii, } from "@stapel/tokens";
28
+ /**
29
+ * Resolve a neutral colour role to a real hex for `mode` (owner audit
30
+ * 2026-07-17, §54 theme-bridge root cause): a host that customizes its OWN
31
+ * brand colour does so exactly as `@stapel/tokens`' README prescribes — copy
32
+ * `theme.default.json` to `stapel.theme.json`, edit the `ramps`, run
33
+ * `stapel-tokens` (`pnpm gen:tokens`) — which regenerates THAT HOST's
34
+ * `tokens.css` custom properties (`--stapel-<role>`). It does NOT and CANNOT
35
+ * change this published package's own compiled-in `colors` object — that
36
+ * snapshot is frozen at `@stapel/tokens`' OWN publish time to `@stapel/tokens`'
37
+ * OWN default theme, forever, regardless of what any host does. Feeding
38
+ * `colors` straight into antd's `ConfigProvider` (the previous implementation)
39
+ * is why every default skin rendered Stapel's stock brand colour instead of
40
+ * the host's, no matter how correctly the host had customized its tokens: the
41
+ * bridge was never wired to read them.
42
+ *
43
+ * The fix reads the LIVE `--stapel-<role>` custom property off
44
+ * `document.documentElement` at call time — the exact value the host's own
45
+ * (re)generated `tokens.css` sets, already resolved through whichever
46
+ * `data-theme` is active — and falls back to the static default only where
47
+ * there is no DOM to read (SSR, tests, a host that never loaded
48
+ * `tokens.css`). This is the one change that makes "the skin takes its theme
49
+ * from the bridge automatically" (the doc comment on every default skin)
50
+ * literally true for a host's brand colour, not just its light/dark mode.
51
+ */
52
+ function readLiveCssVar(name, fallback) {
53
+ if (typeof document === "undefined")
54
+ return fallback;
55
+ // `getPropertyValue` wants the BARE custom-property name (`--stapel-x`);
56
+ // `cssVar()` deliberately returns the `var(--stapel-x)` wrapper for
57
+ // embedding in a CSS value, which `getPropertyValue` would never match —
58
+ // stripped back off here rather than duplicating the `--stapel-` prefix.
59
+ const propertyName = cssVar(name).slice("var(".length, -1);
60
+ const value = getComputedStyle(document.documentElement)
61
+ .getPropertyValue(propertyName)
62
+ .trim();
63
+ return value.length > 0 ? value : fallback;
64
+ }
65
+ /** Resolve a §68 colour role to its hex for `mode`, preferring the host's
66
+ * live CSS custom property over the compiled-in default. */
26
67
  function role(name, mode) {
27
- return colors[bridgeColorRoles[name]][mode];
68
+ return readLiveCssVar(name, colors[name][mode]);
28
69
  }
29
70
  /**
30
- * `@stapel/tokens` L2 → antd `theme.token` (frontend-guidelines §2.4 table).
31
- * Pure: same `mode` in, same object out; reads no globals.
71
+ * `@stapel/tokens` §68 roles → antd `theme.token` (frontend-guidelines §2.4
72
+ * table). Pure: same `mode` in, same object out; reads no globals besides the
73
+ * live CSS custom properties documented on {@link readLiveCssVar}.
32
74
  */
33
75
  export function toAntdTheme(mode) {
34
76
  return {
35
77
  colorPrimary: role("brand", mode),
36
78
  colorLink: role("link", mode),
37
- colorLinkHover: role("brandHover", mode),
79
+ colorLinkHover: role("link-hover", mode),
38
80
  colorSuccess: role("success", mode),
39
81
  colorWarning: role("warning", mode),
40
- colorError: role("danger", mode),
82
+ colorError: role("error", mode),
41
83
  colorInfo: role("info", mode),
42
- colorText: role("textPrimary", mode),
43
- colorTextSecondary: role("textSecondary", mode),
44
- colorBgContainer: role("bgContainer", mode),
45
- colorBgLayout: role("bgLayout", mode),
46
- colorBgElevated: role("bgContainer", mode),
84
+ colorText: role("text", mode),
85
+ colorTextSecondary: role("text-muted", mode),
86
+ colorTextTertiary: role("text-subtle", mode),
87
+ colorBgLayout: role("surface", mode),
88
+ colorBgContainer: role("surface-raised", mode),
89
+ colorBgElevated: role("surface-overlay", mode),
47
90
  colorBorder: role("border", mode),
48
- colorBorderSecondary: role("borderSecondary", mode),
91
+ colorBorderSecondary: role("border-subtle", mode),
49
92
  borderRadius: radii[bridgeRadiusRole],
50
93
  fontSize: fontSize[bridgeFontSizeRole].fontSize,
51
94
  fontFamily: fontFamily.sans,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,MAAM,EACN,UAAU,EACV,QAAQ,EACR,KAAK,GACN,MAAM,gBAAgB,CAAC;AAMxB,gFAAgF;AAChF,SAAS,IAAI,CAAC,IAAqB,EAAE,IAAe;IAClD,OAAO,MAAM,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAC9C,CAAC;AAKD;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,OAAO;QACL,YAAY,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC;QACjC,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;QAC7B,cAAc,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC;QACxC,YAAY,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC;QACnC,YAAY,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC;QACnC,UAAU,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC;QAChC,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;QAC7B,SAAS,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC;QACpC,kBAAkB,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC;QAC/C,gBAAgB,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC;QAC3C,aAAa,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC;QACrC,eAAe,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC;QAC1C,WAAW,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC;QACjC,oBAAoB,EAAE,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC;QACnD,YAAY,EAAE,KAAK,CAAC,gBAAgB,CAAC;QACrC,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC,QAAQ;QAC/C,UAAU,EAAE,UAAU,CAAC,IAAI;KAC5B,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAe;IAC/C,OAAO;QACL,SAAS,EACP,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,gBAAgB;QACxE,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC;KACzB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,MAAM,EACN,MAAM,EACN,UAAU,EACV,QAAQ,EACR,KAAK,GACN,MAAM,gBAAgB,CAAC;AAMxB;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAS,cAAc,CAAC,IAAmB,EAAE,QAAgB;IAC3D,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO,QAAQ,CAAC;IACrD,yEAAyE;IACzE,oEAAoE;IACpE,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3D,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,CAAC,eAAe,CAAC;SACrD,gBAAgB,CAAC,YAAY,CAAC;SAC9B,IAAI,EAAE,CAAC;IACV,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC7C,CAAC;AAED;4DAC4D;AAC5D,SAAS,IAAI,CAAC,IAAmB,EAAE,IAAe;IAChD,OAAO,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAClD,CAAC;AAKD;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,OAAO;QACL,YAAY,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC;QACjC,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;QAC7B,cAAc,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC;QACxC,YAAY,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC;QACnC,YAAY,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC;QACnC,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC;QAC/B,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;QAC7B,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;QAC7B,kBAAkB,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC;QAC5C,iBAAiB,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC;QAC5C,aAAa,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC;QACpC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC;QAC9C,eAAe,EAAE,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC;QAC9C,WAAW,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC;QACjC,oBAAoB,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC;QACjD,YAAY,EAAE,KAAK,CAAC,gBAAgB,CAAC;QACrC,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC,QAAQ;QAC/C,UAAU,EAAE,UAAU,CAAC,IAAI;KAC5B,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAe;IAC/C,OAAO;QACL,SAAS,EACP,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,gBAAgB;QACxE,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC;KACzB,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stapel/tokens-antd",
3
- "version": "0.2.0",
4
- "description": "Token bridge (frontend-guidelines §2.4, §38 T3): projects @stapel/tokens L2 core tokens onto an Ant Design ConfigProvider theme. `toAntdTheme(mode)` → theme.token; `toAntdThemeConfig(mode)` → the full ThemeConfig (algorithm + token). Pure functions, one mapping table shared with @stapel/tokens-mui. Not components.",
3
+ "version": "0.4.0",
4
+ "description": "Token bridge (§68 neutral colour-role dictionary; frontend-guidelines §2.4, §38 T3): projects @stapel/tokens' roles (surface, brand, text-muted, success-bg, ...) onto an Ant Design ConfigProvider theme. `toAntdTheme(mode)` → theme.token; `toAntdThemeConfig(mode)` → the full ThemeConfig (algorithm + token). Pure functions reading the same `colors` object as @stapel/tokens-mui, so they cannot diverge. Not components.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -35,7 +35,7 @@
35
35
  "antd": ">=5.20.0 <7"
36
36
  },
37
37
  "dependencies": {
38
- "@stapel/tokens": "^0.4.0"
38
+ "@stapel/tokens": "^0.5.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@size-limit/preset-small-lib": "^11.2.0",
package/src/index.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
- * `@stapel/tokens-antd` — the Ant Design leg of the token bridge
3
- * (frontend-guidelines §2.4, owner decision §38 T3). It translates the ONE
4
- * neutral role table in `@stapel/tokens` (`bridgeColorRoles` + the radius/
5
- * font-size roles) into Ant Design's flat `ConfigProvider` theme token. The
6
- * L2 role decision is NOT duplicated here this file only renames roles to
7
- * antd field names and picks the light/dark half, so it and `@stapel/tokens-mui`
8
- * cannot drift.
2
+ * `@stapel/tokens-antd` — the Ant Design leg of the token bridge (§68;
3
+ * frontend-guidelines §2.4, owner decision §38 T3). It translates the ONE
4
+ * neutral colour-role dictionary in `@stapel/tokens` (§68 `surface`,
5
+ * `brand`, `text-muted`, `success-bg`, …) straight into Ant Design's flat
6
+ * `ConfigProvider` theme token: a role name IS the antd mapping's input, no
7
+ * separate role→role indirection table (that table `bridgeColorRoles`
8
+ * is gone; §68 Ф1 "алиас-слой совместимости не делать"). The mapping table
9
+ * below (role → antd field) is the ONE place this decision lives; both this
10
+ * file and `@stapel/tokens-mui` read the same `colors` object so they cannot
11
+ * silently diverge on what a role visually means.
9
12
  *
10
13
  * ```tsx
11
14
  * import { toAntdTheme } from "@stapel/tokens-antd";
@@ -23,47 +26,87 @@
23
26
  import { theme as antdTheme } from "antd";
24
27
  import type { ThemeConfig } from "antd";
25
28
  import {
26
- bridgeColorRoles,
27
29
  bridgeFontSizeRole,
28
30
  bridgeRadiusRole,
29
31
  colors,
32
+ cssVar,
30
33
  fontFamily,
31
34
  fontSize,
32
35
  radii,
33
36
  } from "@stapel/tokens";
34
- import type { BridgeColorRole } from "@stapel/tokens";
37
+ import type { CoreTokenName } from "@stapel/tokens";
35
38
 
36
- /** Light or dark — the half of every L2 core token's `{light,dark}` pair. */
39
+ /** Light or dark — the half of every role's `{light,dark}` pair. */
37
40
  export type ThemeMode = "light" | "dark";
38
41
 
39
- /** Resolve a neutral colour role to its hex for `mode` via the shared table. */
40
- function role(name: BridgeColorRole, mode: ThemeMode): string {
41
- return colors[bridgeColorRoles[name]][mode];
42
+ /**
43
+ * Resolve a neutral colour role to a real hex for `mode` (owner audit
44
+ * 2026-07-17, §54 theme-bridge root cause): a host that customizes its OWN
45
+ * brand colour does so exactly as `@stapel/tokens`' README prescribes — copy
46
+ * `theme.default.json` to `stapel.theme.json`, edit the `ramps`, run
47
+ * `stapel-tokens` (`pnpm gen:tokens`) — which regenerates THAT HOST's
48
+ * `tokens.css` custom properties (`--stapel-<role>`). It does NOT and CANNOT
49
+ * change this published package's own compiled-in `colors` object — that
50
+ * snapshot is frozen at `@stapel/tokens`' OWN publish time to `@stapel/tokens`'
51
+ * OWN default theme, forever, regardless of what any host does. Feeding
52
+ * `colors` straight into antd's `ConfigProvider` (the previous implementation)
53
+ * is why every default skin rendered Stapel's stock brand colour instead of
54
+ * the host's, no matter how correctly the host had customized its tokens: the
55
+ * bridge was never wired to read them.
56
+ *
57
+ * The fix reads the LIVE `--stapel-<role>` custom property off
58
+ * `document.documentElement` at call time — the exact value the host's own
59
+ * (re)generated `tokens.css` sets, already resolved through whichever
60
+ * `data-theme` is active — and falls back to the static default only where
61
+ * there is no DOM to read (SSR, tests, a host that never loaded
62
+ * `tokens.css`). This is the one change that makes "the skin takes its theme
63
+ * from the bridge automatically" (the doc comment on every default skin)
64
+ * literally true for a host's brand colour, not just its light/dark mode.
65
+ */
66
+ function readLiveCssVar(name: CoreTokenName, fallback: string): string {
67
+ if (typeof document === "undefined") return fallback;
68
+ // `getPropertyValue` wants the BARE custom-property name (`--stapel-x`);
69
+ // `cssVar()` deliberately returns the `var(--stapel-x)` wrapper for
70
+ // embedding in a CSS value, which `getPropertyValue` would never match —
71
+ // stripped back off here rather than duplicating the `--stapel-` prefix.
72
+ const propertyName = cssVar(name).slice("var(".length, -1);
73
+ const value = getComputedStyle(document.documentElement)
74
+ .getPropertyValue(propertyName)
75
+ .trim();
76
+ return value.length > 0 ? value : fallback;
77
+ }
78
+
79
+ /** Resolve a §68 colour role to its hex for `mode`, preferring the host's
80
+ * live CSS custom property over the compiled-in default. */
81
+ function role(name: CoreTokenName, mode: ThemeMode): string {
82
+ return readLiveCssVar(name, colors[name][mode]);
42
83
  }
43
84
 
44
85
  /** The flat antd token map (`ThemeConfig["token"]`), never undefined. */
45
86
  export type AntdThemeToken = NonNullable<ThemeConfig["token"]>;
46
87
 
47
88
  /**
48
- * `@stapel/tokens` L2 → antd `theme.token` (frontend-guidelines §2.4 table).
49
- * Pure: same `mode` in, same object out; reads no globals.
89
+ * `@stapel/tokens` §68 roles → antd `theme.token` (frontend-guidelines §2.4
90
+ * table). Pure: same `mode` in, same object out; reads no globals besides the
91
+ * live CSS custom properties documented on {@link readLiveCssVar}.
50
92
  */
51
93
  export function toAntdTheme(mode: ThemeMode): AntdThemeToken {
52
94
  return {
53
95
  colorPrimary: role("brand", mode),
54
96
  colorLink: role("link", mode),
55
- colorLinkHover: role("brandHover", mode),
97
+ colorLinkHover: role("link-hover", mode),
56
98
  colorSuccess: role("success", mode),
57
99
  colorWarning: role("warning", mode),
58
- colorError: role("danger", mode),
100
+ colorError: role("error", mode),
59
101
  colorInfo: role("info", mode),
60
- colorText: role("textPrimary", mode),
61
- colorTextSecondary: role("textSecondary", mode),
62
- colorBgContainer: role("bgContainer", mode),
63
- colorBgLayout: role("bgLayout", mode),
64
- colorBgElevated: role("bgContainer", mode),
102
+ colorText: role("text", mode),
103
+ colorTextSecondary: role("text-muted", mode),
104
+ colorTextTertiary: role("text-subtle", mode),
105
+ colorBgLayout: role("surface", mode),
106
+ colorBgContainer: role("surface-raised", mode),
107
+ colorBgElevated: role("surface-overlay", mode),
65
108
  colorBorder: role("border", mode),
66
- colorBorderSecondary: role("borderSecondary", mode),
109
+ colorBorderSecondary: role("border-subtle", mode),
67
110
  borderRadius: radii[bridgeRadiusRole],
68
111
  fontSize: fontSize[bridgeFontSizeRole].fontSize,
69
112
  fontFamily: fontFamily.sans,