@sorb/seed 0.3.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/package.json +2 -2
- package/src/emit/sorbMantine.js +116 -0
- package/src/emit/sorbMantine.test.js +68 -0
- package/src/emit/sorbMatSys.js +159 -0
- package/src/emit/sorbMatSys.test.js +75 -0
- package/src/emit/sorbMui.js +165 -0
- package/src/emit/sorbMui.test.js +102 -0
- package/src/emit/sorbPrimevue.js +240 -0
- package/src/emit/sorbPrimevue.test.js +130 -0
- package/src/emit/sorbShadcn.js +186 -0
- package/src/emit/sorbShadcn.test.js +102 -0
- package/src/index.js +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sorb/seed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Storybook→Figma capture for Sorb, the design-token bridge for your running app. (Seed.)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@babel/generator": "^7.29.7",
|
|
36
36
|
"@babel/parser": "^7.29.7",
|
|
37
37
|
"@babel/traverse": "^7.29.7",
|
|
38
|
-
"@sorb/core": "^0.
|
|
38
|
+
"@sorb/core": "^0.3.0",
|
|
39
39
|
"esbuild": "^0.21.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Sorb Mantine v7 CSS-var override format (`sorb/mantine-vars`).
|
|
2
|
+
//
|
|
3
|
+
// PROMOTED from `sorb-demo-mantine/sd/mantine-format.js` (P2 spike →
|
|
4
|
+
// framework-targets-productization T2, first of the six JJ-demo formats to
|
|
5
|
+
// graduate into `@sorb/seed`). The demo file stays in place for now (T8
|
|
6
|
+
// retrofit is founder-gated); this is the canonical, generalized home.
|
|
7
|
+
//
|
|
8
|
+
// GENERALIZATION (T0 semantic-role contract): the spike hardcoded Janes
|
|
9
|
+
// Jeans' own token ids as the map's left-hand column. Promotion makes that
|
|
10
|
+
// column ROLE-RESOLVED — a non-JJ kit passes `options.roleMap` (role id →
|
|
11
|
+
// its own token id) instead of forking this file. Resolution is the same
|
|
12
|
+
// shape as `@sorb/core`'s `resolveRole` (see `sorb-core/src/index.js`
|
|
13
|
+
// `DEFAULT_ROLE_IDS`/`resolveRole`), inlined here rather than imported so
|
|
14
|
+
// this format degrades gracefully against an older published `@sorb/core`
|
|
15
|
+
// that predates the role contract (same feature-detect posture as the
|
|
16
|
+
// TargetAdapter registration side — see `sorb-leaf/src/targets/mantine.js`).
|
|
17
|
+
// Not every key below is one of `@sorb/core`'s canonical `DEFAULT_ROLE_IDS`
|
|
18
|
+
// — several (`button.primary.bg.*`, `button.radius`, `card.radius`) are
|
|
19
|
+
// Janes-Jeans component-tier ids with no canonical role counterpart yet
|
|
20
|
+
// (kit-private, per the productization spec's "contract scope = the union
|
|
21
|
+
// of the six maps' role columns, not a kit's full token tree"). They still
|
|
22
|
+
// go through the same `roleMap` resolution for consistency and so a kit
|
|
23
|
+
// that DOES want to override them can.
|
|
24
|
+
//
|
|
25
|
+
// The `--mantine-*` right-hand column is Mantine's own documented CSS
|
|
26
|
+
// surface — universal, and stays baked (not role-resolved).
|
|
27
|
+
//
|
|
28
|
+
// PRECEDENCE FINDINGS (carried verbatim from the P2 spike header — this is
|
|
29
|
+
// the load-bearing reason every declaration below is `!important`, so don't
|
|
30
|
+
// drop it in a future edit): Mantine's OWN core stylesheet declares these
|
|
31
|
+
// vars on `:root[data-mantine-color-scheme="light"]` — an attribute-selector
|
|
32
|
+
// rule, specificity (0,2,0) — and `MantineProvider` injects that stylesheet
|
|
33
|
+
// as a runtime `<style>` tag whose position in `<head>` is NOT guaranteed to
|
|
34
|
+
// precede ours. A first attempt used `:where(html) { ... }`, which carries
|
|
35
|
+
// ZERO specificity by definition and lost outright (confirmed via Playwright:
|
|
36
|
+
// overriding the underlying Sorb var had no effect on the Button's computed
|
|
37
|
+
// background). The fix is a plain `:root` block with `!important` on every
|
|
38
|
+
// declaration — `!important` author declarations win over normal-importance
|
|
39
|
+
// declarations regardless of specificity or source order, which is exactly
|
|
40
|
+
// the guarantee needed against a dynamically-injected, order-unstable
|
|
41
|
+
// stylesheet. Zero-dep, zero-JS (Mechanism A): a static CSS file of `var()`
|
|
42
|
+
// refs, reacting to a live bridge push the same way `variables.css` already
|
|
43
|
+
// does.
|
|
44
|
+
|
|
45
|
+
export const SORB_MANTINE_VARS = 'sorb/mantine-vars'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* role id (JJ id when unmapped) → Mantine CSS var name.
|
|
49
|
+
* Left side is resolved through `options.roleMap` at format time (identity
|
|
50
|
+
* when the kit uses these ids directly — the JJ reference); right side is
|
|
51
|
+
* the Mantine v7 documented variable it overrides.
|
|
52
|
+
*/
|
|
53
|
+
export const MANTINE_VAR_MAP = {
|
|
54
|
+
'color.surface': '--mantine-color-body',
|
|
55
|
+
'color.ink': '--mantine-color-text',
|
|
56
|
+
'color.ink-muted': '--mantine-color-placeholder',
|
|
57
|
+
'color.brand': '--mantine-color-anchor',
|
|
58
|
+
'color.danger': '--mantine-color-error',
|
|
59
|
+
'color.border': '--mantine-default-border-color',
|
|
60
|
+
|
|
61
|
+
'button.primary.bg.default': '--mantine-primary-color-filled',
|
|
62
|
+
'button.primary.bg.hover': '--mantine-primary-color-filled-hover',
|
|
63
|
+
|
|
64
|
+
'color.surface-raised': '--mantine-primary-color-light',
|
|
65
|
+
'color.surface-sunken': '--mantine-primary-color-light-hover',
|
|
66
|
+
|
|
67
|
+
'radius.control': '--mantine-radius-default',
|
|
68
|
+
'button.radius': '--mantine-radius-md',
|
|
69
|
+
'card.radius': '--mantine-radius-lg',
|
|
70
|
+
'radius.pill': '--mantine-radius-xl',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const cssVarOf = (id) => '--' + String(id).split('.').join('-')
|
|
74
|
+
|
|
75
|
+
// Same shape as `@sorb/core`'s `resolveRole` — inlined per the promotion
|
|
76
|
+
// spec so this format has no hard runtime dependency on a core version that
|
|
77
|
+
// ships the role contract (see file header).
|
|
78
|
+
const resolveRoleId = (roleId, roleMap) => (roleMap && roleMap[roleId]) || roleId
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* format: sorb/mantine-vars
|
|
82
|
+
* Emits a CSS file that redeclares the mapped `--mantine-*` vars as
|
|
83
|
+
* `var(--<kit-token>) !important` refs. `options.roleMap` (role id → kit
|
|
84
|
+
* token id) lets a non-JJ kit reuse this format unmodified; defaults to the
|
|
85
|
+
* canonical/JJ ids (identity resolution) when omitted.
|
|
86
|
+
* @param {{dictionary: {allTokens: Array}, options?: {roleMap?: Record<string,string>}}} args
|
|
87
|
+
* @returns {string} the generated CSS.
|
|
88
|
+
*/
|
|
89
|
+
export const sorbMantineVars = ({ dictionary, options }) => {
|
|
90
|
+
const roleMap = options && options.roleMap
|
|
91
|
+
const byId = new Map(dictionary.allTokens.map((t) => [t.path.join('.'), t]))
|
|
92
|
+
const lines = []
|
|
93
|
+
const missing = []
|
|
94
|
+
for (const [roleId, mantineVar] of Object.entries(MANTINE_VAR_MAP)) {
|
|
95
|
+
const tokenId = resolveRoleId(roleId, roleMap)
|
|
96
|
+
const t = byId.get(tokenId)
|
|
97
|
+
if (!t) { missing.push(tokenId); continue }
|
|
98
|
+
// !important is load-bearing here — see file header "Precedence findings".
|
|
99
|
+
lines.push(` ${mantineVar}: var(${cssVarOf(tokenId)}) !important;`)
|
|
100
|
+
}
|
|
101
|
+
if (missing.length) {
|
|
102
|
+
console.warn(` ⚠ sorb/mantine-vars: ${missing.length} unmapped token id(s) skipped: ` + missing.join(', '))
|
|
103
|
+
}
|
|
104
|
+
return (
|
|
105
|
+
'/* AUTO-GENERATED by Style Dictionary (sorb/mantine-vars) — do not edit.\n' +
|
|
106
|
+
' Overrides Mantine v7 core CSS vars with var(--token) refs so a Sorb bridge\n' +
|
|
107
|
+
' push re-themes Mantine components with zero component-level code changes.\n' +
|
|
108
|
+
' !important is required — see this format\'s header "Precedence findings":\n' +
|
|
109
|
+
' Mantine\'s :root[data-mantine-color-scheme] rule otherwise wins regardless\n' +
|
|
110
|
+
' of this file\'s load order (its <style> is injected by MantineProvider at\n' +
|
|
111
|
+
' runtime, not statically ordered in <head>). */\n' +
|
|
112
|
+
':root {\n' +
|
|
113
|
+
lines.join('\n') +
|
|
114
|
+
'\n}\n'
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Tests for the Sorb Mantine v7 CSS-var override format (`sorb/mantine-vars`).
|
|
2
|
+
// Run: node --test (zero-dep, Node's built-in runner — matches the workspace convention).
|
|
3
|
+
import { test } from 'node:test'
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import { SORB_MANTINE_VARS, MANTINE_VAR_MAP, sorbMantineVars } from './sorbMantine.js'
|
|
6
|
+
|
|
7
|
+
// Helper: a fabricated dictionary of resolved tokens, keyed by dot-path id.
|
|
8
|
+
const tok = (id) => ({ path: id.split('.') })
|
|
9
|
+
const dictOf = (ids) => ({ allTokens: ids.map(tok) })
|
|
10
|
+
|
|
11
|
+
test('format name constant is the documented id', () => {
|
|
12
|
+
assert.equal(SORB_MANTINE_VARS, 'sorb/mantine-vars')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('emits a :root block with every mapped id present in the dictionary', () => {
|
|
16
|
+
const dictionary = dictOf(Object.keys(MANTINE_VAR_MAP))
|
|
17
|
+
const css = sorbMantineVars({ dictionary })
|
|
18
|
+
assert.match(css, /^:root \{/m)
|
|
19
|
+
assert.match(css, /\}\s*$/)
|
|
20
|
+
// one declaration per map entry
|
|
21
|
+
const declCount = (css.match(/!important;/g) || []).length
|
|
22
|
+
assert.equal(declCount, Object.keys(MANTINE_VAR_MAP).length)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('each declaration maps the correct --mantine-* var to var(--kebab-id) and keeps !important', () => {
|
|
26
|
+
const dictionary = dictOf(Object.keys(MANTINE_VAR_MAP))
|
|
27
|
+
const css = sorbMantineVars({ dictionary })
|
|
28
|
+
assert.match(css, /--mantine-color-body: var\(--color-surface\) !important;/)
|
|
29
|
+
assert.match(css, /--mantine-color-text: var\(--color-ink\) !important;/)
|
|
30
|
+
assert.match(css, /--mantine-color-anchor: var\(--color-brand\) !important;/)
|
|
31
|
+
assert.match(css, /--mantine-color-error: var\(--color-danger\) !important;/)
|
|
32
|
+
assert.match(css, /--mantine-default-border-color: var\(--color-border\) !important;/)
|
|
33
|
+
assert.match(css, /--mantine-primary-color-filled: var\(--button-primary-bg-default\) !important;/)
|
|
34
|
+
assert.match(css, /--mantine-primary-color-filled-hover: var\(--button-primary-bg-hover\) !important;/)
|
|
35
|
+
assert.match(css, /--mantine-radius-default: var\(--radius-control\) !important;/)
|
|
36
|
+
assert.match(css, /--mantine-radius-md: var\(--button-radius\) !important;/)
|
|
37
|
+
assert.match(css, /--mantine-radius-lg: var\(--card-radius\) !important;/)
|
|
38
|
+
assert.match(css, /--mantine-radius-xl: var\(--radius-pill\) !important;/)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('every value is a var() reference, never a baked literal (live-preview invariant)', () => {
|
|
42
|
+
const dictionary = dictOf(Object.keys(MANTINE_VAR_MAP))
|
|
43
|
+
const css = sorbMantineVars({ dictionary })
|
|
44
|
+
for (const line of css.split('\n').filter((l) => l.trim().startsWith('--mantine'))) {
|
|
45
|
+
assert.match(line, /: var\(--[a-z0-9-]+\) !important;$/)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('options.roleMap remaps the JJ-token-id column without forking the format', () => {
|
|
50
|
+
// A hypothetical non-JJ kit names its surface/ink/brand roles differently.
|
|
51
|
+
const roleMap = {
|
|
52
|
+
'color.surface': 'kit.bg',
|
|
53
|
+
'color.brand': 'kit.primary',
|
|
54
|
+
}
|
|
55
|
+
const dictionary = dictOf(['kit.bg', 'kit.primary', 'color.ink'])
|
|
56
|
+
const css = sorbMantineVars({ dictionary, options: { roleMap } })
|
|
57
|
+
assert.match(css, /--mantine-color-body: var\(--kit-bg\) !important;/)
|
|
58
|
+
assert.match(css, /--mantine-color-anchor: var\(--kit-primary\) !important;/)
|
|
59
|
+
// unmapped roles fall back to their canonical/default id
|
|
60
|
+
assert.match(css, /--mantine-color-text: var\(--color-ink\) !important;/)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('unmapped/missing token ids are skipped, not emitted as broken var() refs', () => {
|
|
64
|
+
const dictionary = dictOf(['color.surface']) // only one of the map's ids present
|
|
65
|
+
const css = sorbMantineVars({ dictionary })
|
|
66
|
+
assert.match(css, /--mantine-color-body: var\(--color-surface\) !important;/)
|
|
67
|
+
assert.equal((css.match(/!important;/g) || []).length, 1)
|
|
68
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Sorb Angular Material 20 M3 system-variable override format
|
|
2
|
+
// (`sorb/mat-sys-vars`).
|
|
3
|
+
//
|
|
4
|
+
// PROMOTED from `sorb-demo-angular/sd.config.js:96-113` (the inline
|
|
5
|
+
// `SORB_MAT_SYS_VARS` format + `MAT_SYS_MAP`) — framework-targets-
|
|
6
|
+
// productization T5, the fifth of the six JJ-demo formats to graduate into
|
|
7
|
+
// `@sorb/seed`. The demo file stays in place for now (T8 retrofit is
|
|
8
|
+
// founder-gated); this is the canonical, generalized home.
|
|
9
|
+
//
|
|
10
|
+
// GENERALIZATION (T0 semantic-role contract): the demo hardcoded Janes
|
|
11
|
+
// Jeans' own token ids (`color.brand`, `radius.control`, …) as the map's
|
|
12
|
+
// right-hand column. Promotion makes that column ROLE-RESOLVED —
|
|
13
|
+
// `options.roleMap` (role id → kit-token-id overrides) lets a non-JJ kit
|
|
14
|
+
// reuse this format unmodified instead of forking it. Resolution is the
|
|
15
|
+
// same shape as `@sorb/core`'s `resolveRole` (`sorb-core/src/index.js`
|
|
16
|
+
// `DEFAULT_ROLE_IDS`/`resolveRole`), inlined here rather than imported so
|
|
17
|
+
// this format degrades gracefully against an older published `@sorb/core`
|
|
18
|
+
// that predates the role contract (same feature-detect posture as the
|
|
19
|
+
// TargetAdapter registration side — see
|
|
20
|
+
// `sorb-leaf/src/targets/angularMaterial.js`).
|
|
21
|
+
//
|
|
22
|
+
// Not every role id below is one of `@sorb/core`'s canonical
|
|
23
|
+
// `DEFAULT_ROLE_IDS` — `color.white` (the on-error/on-error-container rows)
|
|
24
|
+
// is a kit-private primitive, carried over from the demo's original literal
|
|
25
|
+
// `var(--color-white)` mapping. All ids go through the same `roleMap`
|
|
26
|
+
// resolution for consistency and so a kit without that primitive can
|
|
27
|
+
// override it.
|
|
28
|
+
//
|
|
29
|
+
// The `--mat-sys-*` left-hand column is Angular Material's own documented
|
|
30
|
+
// M3 system-variable surface (emitted by the `mat.theme()` mixin) —
|
|
31
|
+
// universal, and stays baked (not role-resolved). The map is left keyed by
|
|
32
|
+
// `--mat-sys-*` var (not role id, unlike `sorb/mantine-vars`) because several
|
|
33
|
+
// distinct Material system slots legitimately resolve to the same kit role
|
|
34
|
+
// (e.g. `--mat-sys-surface` and `--mat-sys-surface-bright` both track
|
|
35
|
+
// `color.surface`) — duplicate values are fine, duplicate object keys are
|
|
36
|
+
// not, so keying by the unique `--mat-sys-*` name preserves the full
|
|
37
|
+
// ~40-row demo mapping verbatim.
|
|
38
|
+
//
|
|
39
|
+
// PRECEDENCE FINDINGS (carried verbatim from the demo's header — this is the
|
|
40
|
+
// load-bearing reason every declaration below is `!important`, so don't drop
|
|
41
|
+
// it in a future edit): Angular Material's `mat.theme()` mixin emits its own
|
|
42
|
+
// `html { ... }` rule at a point in the stylesheet whose order relative to
|
|
43
|
+
// this override is not guaranteed across HMR/rebuild ordering. CSS's "later
|
|
44
|
+
// wins on equal specificity" rule alone won't reliably beat a component
|
|
45
|
+
// library's own injected styles — the same P2/Mantine specificity lesson
|
|
46
|
+
// travels here. `!important` on every declaration is the fix, not
|
|
47
|
+
// decoration.
|
|
48
|
+
//
|
|
49
|
+
// Zero-dep, zero-JS (Mechanism A): a static CSS file of `var()` refs,
|
|
50
|
+
// reacting to a live bridge push the same way `variables.css` already does.
|
|
51
|
+
|
|
52
|
+
export const SORB_MAT_SYS_VARS = 'sorb/mat-sys-vars'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Angular Material `--mat-sys-*` CSS var name → role id (JJ id when
|
|
56
|
+
* unmapped). The role id is resolved through `options.roleMap` at format
|
|
57
|
+
* time (identity when the kit uses these ids directly — the JJ reference).
|
|
58
|
+
* Semantic-tier-first per the field corrections carried from P2/P4: only
|
|
59
|
+
* the handful of system roots Material's own components actually cascade
|
|
60
|
+
* from — not every `--mat-sys-*` var Material defines (many, e.g.
|
|
61
|
+
* per-component elevation/state-layer opacities, aren't part of the kit's
|
|
62
|
+
* vocabulary and are intentionally left at Material's own defaults).
|
|
63
|
+
*/
|
|
64
|
+
export const MAT_SYS_MAP = {
|
|
65
|
+
// Brand / primary action
|
|
66
|
+
'--mat-sys-primary': 'color.brand',
|
|
67
|
+
'--mat-sys-on-primary': 'color.brand-contrast',
|
|
68
|
+
'--mat-sys-primary-container': 'color.brand-hover',
|
|
69
|
+
'--mat-sys-on-primary-container': 'color.brand-contrast',
|
|
70
|
+
'--mat-sys-inverse-primary': 'color.brand-hover',
|
|
71
|
+
// Accent / secondary action
|
|
72
|
+
'--mat-sys-secondary': 'color.accent',
|
|
73
|
+
'--mat-sys-on-secondary': 'color.accent-contrast',
|
|
74
|
+
'--mat-sys-secondary-container': 'color.surface-raised',
|
|
75
|
+
'--mat-sys-on-secondary-container': 'color.ink',
|
|
76
|
+
'--mat-sys-tertiary': 'color.accent',
|
|
77
|
+
'--mat-sys-on-tertiary': 'color.accent-contrast',
|
|
78
|
+
// Surfaces
|
|
79
|
+
'--mat-sys-surface': 'color.surface',
|
|
80
|
+
'--mat-sys-on-surface': 'color.ink',
|
|
81
|
+
'--mat-sys-on-surface-variant': 'color.ink-muted',
|
|
82
|
+
'--mat-sys-surface-container': 'color.surface-raised',
|
|
83
|
+
'--mat-sys-surface-container-low': 'color.surface-raised',
|
|
84
|
+
'--mat-sys-surface-container-high': 'color.surface-sunken',
|
|
85
|
+
'--mat-sys-surface-container-highest': 'color.surface-sunken',
|
|
86
|
+
'--mat-sys-surface-dim': 'color.surface-sunken',
|
|
87
|
+
'--mat-sys-surface-bright': 'color.surface',
|
|
88
|
+
'--mat-sys-inverse-surface': 'color.ink',
|
|
89
|
+
'--mat-sys-inverse-on-surface': 'color.surface',
|
|
90
|
+
'--mat-sys-background': 'color.surface',
|
|
91
|
+
'--mat-sys-on-background': 'color.ink',
|
|
92
|
+
// Structure
|
|
93
|
+
'--mat-sys-outline': 'color.border',
|
|
94
|
+
'--mat-sys-outline-variant': 'color.border-subtle',
|
|
95
|
+
// Status
|
|
96
|
+
'--mat-sys-error': 'color.danger',
|
|
97
|
+
// `color.white` is a kit-private primitive (not a canonical role), carried
|
|
98
|
+
// verbatim from the demo's original literal `var(--color-white)` mapping
|
|
99
|
+
// for byte-parity (T8) — still routed through the same roleMap resolution
|
|
100
|
+
// so a kit without a `color.white` id can override it.
|
|
101
|
+
'--mat-sys-on-error': 'color.white',
|
|
102
|
+
'--mat-sys-error-container': 'color.danger',
|
|
103
|
+
'--mat-sys-on-error-container': 'color.white',
|
|
104
|
+
// Shape (corner radii)
|
|
105
|
+
'--mat-sys-corner-small': 'radius.control',
|
|
106
|
+
'--mat-sys-corner-medium': 'radius.control',
|
|
107
|
+
'--mat-sys-corner-large': 'radius.card',
|
|
108
|
+
'--mat-sys-corner-extra-large': 'radius.card',
|
|
109
|
+
'--mat-sys-corner-full': 'radius.pill',
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const cssVarOf = (id) => '--' + String(id).split('.').join('-')
|
|
113
|
+
|
|
114
|
+
// Same shape as `@sorb/core`'s `resolveRole` — inlined per the promotion
|
|
115
|
+
// spec so this format has no hard runtime dependency on a core version that
|
|
116
|
+
// ships the role contract (see file header).
|
|
117
|
+
const resolveRoleId = (roleId, roleMap) => (roleMap && roleMap[roleId]) || roleId
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* format: sorb/mat-sys-vars
|
|
121
|
+
* Emits a CSS file that redeclares the mapped `--mat-sys-*` vars as
|
|
122
|
+
* `var(--<kit-token>) !important` refs. `options.roleMap` (role id → kit
|
|
123
|
+
* token id) lets a non-JJ kit reuse this format unmodified; defaults to the
|
|
124
|
+
* canonical/JJ ids (identity resolution) when omitted.
|
|
125
|
+
* @param {{dictionary: {allTokens: Array}, options?: {roleMap?: Record<string,string>}}} args
|
|
126
|
+
* @returns {string} the generated CSS.
|
|
127
|
+
*/
|
|
128
|
+
export const sorbMatSysVars = ({ dictionary, options }) => {
|
|
129
|
+
const roleMap = options && options.roleMap
|
|
130
|
+
const byId = new Map(dictionary.allTokens.map((t) => [t.path.join('.'), t]))
|
|
131
|
+
const lines = []
|
|
132
|
+
const missing = []
|
|
133
|
+
for (const [matVar, roleId] of Object.entries(MAT_SYS_MAP)) {
|
|
134
|
+
const tokenId = resolveRoleId(roleId, roleMap)
|
|
135
|
+
const t = byId.get(tokenId)
|
|
136
|
+
if (!t) { missing.push(tokenId); continue }
|
|
137
|
+
// !important is load-bearing here — see file header "Precedence findings".
|
|
138
|
+
lines.push(` ${matVar}: var(${cssVarOf(tokenId)}) !important;`)
|
|
139
|
+
}
|
|
140
|
+
if (missing.length) {
|
|
141
|
+
console.warn(` ⚠ sorb/mat-sys-vars: ${missing.length} unmapped token id(s) skipped: ` + missing.join(', '))
|
|
142
|
+
}
|
|
143
|
+
return (
|
|
144
|
+
'/**\n' +
|
|
145
|
+
' * AUTO-GENERATED by Style Dictionary (sorb/mat-sys-vars) — do not edit.\n' +
|
|
146
|
+
' *\n' +
|
|
147
|
+
' * Remaps Angular Material\'s M3 system-variable layer onto the kit\n' +
|
|
148
|
+
' * vocabulary. Import AFTER the kit\'s own variables.css and AFTER Angular\n' +
|
|
149
|
+
' * Material\'s own theme styles so this :root block wins the cascade —\n' +
|
|
150
|
+
' * !important is required, not decorative (Angular Material\'s `mat.theme()`\n' +
|
|
151
|
+
' * mixin emits its own `html{}` rule whose order relative to this file is\n' +
|
|
152
|
+
' * not guaranteed across HMR/rebuild ordering; see this format\'s header\n' +
|
|
153
|
+
' * "Precedence findings").\n' +
|
|
154
|
+
' */\n' +
|
|
155
|
+
':root {\n' +
|
|
156
|
+
lines.join('\n') +
|
|
157
|
+
'\n}\n'
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Tests for the Sorb Angular Material M3 system-variable override format
|
|
2
|
+
// (`sorb/mat-sys-vars`).
|
|
3
|
+
// Run: node --test (zero-dep, Node's built-in runner — matches the workspace convention).
|
|
4
|
+
import { test } from 'node:test'
|
|
5
|
+
import assert from 'node:assert/strict'
|
|
6
|
+
import { SORB_MAT_SYS_VARS, MAT_SYS_MAP, sorbMatSysVars } from './sorbMatSys.js'
|
|
7
|
+
|
|
8
|
+
// Helper: a fabricated dictionary of resolved tokens, keyed by dot-path id.
|
|
9
|
+
const tok = (id) => ({ path: id.split('.') })
|
|
10
|
+
const dictOf = (ids) => ({ allTokens: ids.map(tok) })
|
|
11
|
+
|
|
12
|
+
test('format name constant is the documented id', () => {
|
|
13
|
+
assert.equal(SORB_MAT_SYS_VARS, 'sorb/mat-sys-vars')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('emits a :root block with every mapped id present in the dictionary', () => {
|
|
17
|
+
const dictionary = dictOf([...new Set(Object.values(MAT_SYS_MAP))])
|
|
18
|
+
const css = sorbMatSysVars({ dictionary })
|
|
19
|
+
assert.match(css, /^\/\*\*/) // header comment
|
|
20
|
+
assert.match(css, /:root \{/)
|
|
21
|
+
assert.match(css, /\}\s*$/)
|
|
22
|
+
// one declaration per map entry (right-hand roles can repeat, so count
|
|
23
|
+
// declarations, not unique role ids)
|
|
24
|
+
const declCount = (css.match(/!important;/g) || []).length
|
|
25
|
+
assert.equal(declCount, Object.keys(MAT_SYS_MAP).length)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('each declaration maps the correct --mat-sys-* var to var(--kebab-id) and keeps !important', () => {
|
|
29
|
+
const dictionary = dictOf([...new Set(Object.values(MAT_SYS_MAP))])
|
|
30
|
+
const css = sorbMatSysVars({ dictionary })
|
|
31
|
+
assert.match(css, /--mat-sys-primary: var\(--color-brand\) !important;/)
|
|
32
|
+
assert.match(css, /--mat-sys-on-primary: var\(--color-brand-contrast\) !important;/)
|
|
33
|
+
assert.match(css, /--mat-sys-secondary: var\(--color-accent\) !important;/)
|
|
34
|
+
assert.match(css, /--mat-sys-surface: var\(--color-surface\) !important;/)
|
|
35
|
+
assert.match(css, /--mat-sys-on-surface: var\(--color-ink\) !important;/)
|
|
36
|
+
assert.match(css, /--mat-sys-outline: var\(--color-border\) !important;/)
|
|
37
|
+
assert.match(css, /--mat-sys-error: var\(--color-danger\) !important;/)
|
|
38
|
+
assert.match(css, /--mat-sys-on-error: var\(--color-white\) !important;/)
|
|
39
|
+
assert.match(css, /--mat-sys-corner-small: var\(--radius-control\) !important;/)
|
|
40
|
+
assert.match(css, /--mat-sys-corner-large: var\(--radius-card\) !important;/)
|
|
41
|
+
assert.match(css, /--mat-sys-corner-full: var\(--radius-pill\) !important;/)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('every value is a var() reference, never a baked literal (live-preview invariant)', () => {
|
|
45
|
+
const dictionary = dictOf([...new Set(Object.values(MAT_SYS_MAP))])
|
|
46
|
+
const css = sorbMatSysVars({ dictionary })
|
|
47
|
+
for (const line of css.split('\n').filter((l) => l.trim().startsWith('--mat-sys'))) {
|
|
48
|
+
assert.match(line, /: var\(--[a-z0-9-]+\) !important;$/)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('options.roleMap remaps the JJ-token-id column without forking the format', () => {
|
|
53
|
+
// A hypothetical non-JJ kit names its brand/surface roles differently.
|
|
54
|
+
const roleMap = {
|
|
55
|
+
'color.brand': 'kit.primary',
|
|
56
|
+
'color.surface': 'kit.bg',
|
|
57
|
+
}
|
|
58
|
+
const dictionary = dictOf(['kit.primary', 'kit.bg', 'color.brand-contrast'])
|
|
59
|
+
const css = sorbMatSysVars({ dictionary, options: { roleMap } })
|
|
60
|
+
assert.match(css, /--mat-sys-primary: var\(--kit-primary\) !important;/)
|
|
61
|
+
assert.match(css, /--mat-sys-surface: var\(--kit-bg\) !important;/)
|
|
62
|
+
// unmapped roles fall back to their canonical/default id
|
|
63
|
+
assert.match(css, /--mat-sys-on-primary: var\(--color-brand-contrast\) !important;/)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('unmapped/missing token ids are skipped, not emitted as broken var() refs', () => {
|
|
67
|
+
const dictionary = dictOf(['color.brand']) // only one of the map's ids present
|
|
68
|
+
const css = sorbMatSysVars({ dictionary })
|
|
69
|
+
assert.match(css, /--mat-sys-primary: var\(--color-brand\) !important;/)
|
|
70
|
+
// color.brand also backs --mat-sys-inverse-primary (both map to color.brand-hover
|
|
71
|
+
// actually) — assert exact count matches how many map entries resolve to
|
|
72
|
+
// 'color.brand' specifically.
|
|
73
|
+
const brandEntries = Object.values(MAT_SYS_MAP).filter((r) => r === 'color.brand').length
|
|
74
|
+
assert.equal((css.match(/!important;/g) || []).length, brandEntries)
|
|
75
|
+
})
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Sorb MUI v6 CSS-var override format (`sorb/mui-vars`).
|
|
2
|
+
//
|
|
3
|
+
// PROMOTED from `sorb-demo-mui/sd.config.js:18-86` (P3 spike →
|
|
4
|
+
// framework-targets-productization T3, third of the six JJ-demo formats to
|
|
5
|
+
// graduate into `@sorb/seed`). The demo file stays in place for now (T8
|
|
6
|
+
// retrofit is founder-gated); this is the canonical, generalized home.
|
|
7
|
+
//
|
|
8
|
+
// WHY THE INDIRECTION LAYER EXISTS: MUI v6 `createTheme({ cssVariables: true })`
|
|
9
|
+
// rejects `var(...)` strings as palette input (it needs real colors to compute
|
|
10
|
+
// contrast/tonal variants — verified via spike, see the demo's README §MUI
|
|
11
|
+
// integration). So MUI still computes its own `--mui-*` custom properties from
|
|
12
|
+
// hardcoded seed values, and this format emits a *second*, later-cascading
|
|
13
|
+
// `:root` block that re-points each covered `--mui-*` var at the matching kit
|
|
14
|
+
// token via `var(--kit-token, <seed-fallback>)`. Any component reading
|
|
15
|
+
// `var(--mui-palette-primary-main)` transitively resolves through to the kit
|
|
16
|
+
// token, and a Sorb bridge push (which sets `--color-*` on `:root`) re-themes
|
|
17
|
+
// it live with zero MUI reinitialization.
|
|
18
|
+
//
|
|
19
|
+
// GENERALIZATION (T0 semantic-role contract): the spike hardcoded two things
|
|
20
|
+
// that promotion must generalize —
|
|
21
|
+
// (a) the JJ-token-id column (left side of the map): now ROLE-RESOLVED via
|
|
22
|
+
// `options.roleMap` (role id → kit token id), same shape as
|
|
23
|
+
// `@sorb/core`'s `resolveRole`/`DEFAULT_ROLE_IDS`, inlined here rather
|
|
24
|
+
// than imported so this format degrades gracefully against an older
|
|
25
|
+
// published `@sorb/core` that predates the role contract (same
|
|
26
|
+
// feature-detect posture as the TargetAdapter registration side — see
|
|
27
|
+
// `sorb-leaf/src/targets/mui.js`).
|
|
28
|
+
// (b) the MUI fallback LITERAL (var()'s 2nd arg, e.g. `#1976d2`): this was
|
|
29
|
+
// the demo's own Janes-Jeans hex values baked directly into the format
|
|
30
|
+
// function. Promotion REQUIRES callers to supply these via
|
|
31
|
+
// `options.seedValues` (role id → literal) — a public `@sorb/seed`
|
|
32
|
+
// format must never ship one kit's brand hexes as its defaults (RISK,
|
|
33
|
+
// framework-targets-productization.md risk table: "MUI fallback
|
|
34
|
+
// literals leak kit values into public seed"). A role with no
|
|
35
|
+
// `seedValues` entry emits `var(--kit-token)` with NO fallback — MUI may
|
|
36
|
+
// then fail to compute tonal variants for that slot until a real value
|
|
37
|
+
// resolves at runtime (documented, not silently patched over).
|
|
38
|
+
//
|
|
39
|
+
// Not every key below is one of `@sorb/core`'s canonical `DEFAULT_ROLE_IDS`
|
|
40
|
+
// — several (contrast-text roles) are Janes-Jeans-shaped ids with no
|
|
41
|
+
// canonical role counterpart yet (kit-private, per the productization spec's
|
|
42
|
+
// "contract scope = the union of the six maps' role columns, not a kit's
|
|
43
|
+
// full token tree"). They still go through the same `roleMap` resolution for
|
|
44
|
+
// consistency and so a kit that DOES want to override them can.
|
|
45
|
+
//
|
|
46
|
+
// The `--mui-*` right-hand column is MUI's own documented CSS-variables
|
|
47
|
+
// surface (verified against @mui/material 6.5.0's actual `theme.vars`
|
|
48
|
+
// shape) — universal, and stays baked (not role-resolved).
|
|
49
|
+
//
|
|
50
|
+
// COVERAGE BOUNDARY (EVIDENCE framing — verified against @mui/material
|
|
51
|
+
// 6.5.0's actual `theme.vars` shape, carried from the P3 spike header):
|
|
52
|
+
// COVERED — palette.{primary,secondary,error,success}.{main,dark,contrastText}
|
|
53
|
+
// (secondary/error/success contrastText not yet mapped — see
|
|
54
|
+
// role gaps in the T3 report), palette.background.{default,paper},
|
|
55
|
+
// palette.text.{primary,secondary}, palette.divider,
|
|
56
|
+
// shape.borderRadius.
|
|
57
|
+
// NOT COVERED — typography (fontFamily/fontSize/fontWeight per variant — MUI
|
|
58
|
+
// v6 does not expose these as CSS vars by default), spacing
|
|
59
|
+
// function, transitions, zIndex, breakpoints. These stay
|
|
60
|
+
// JS-only theme values; a live preview cannot re-theme them.
|
|
61
|
+
//
|
|
62
|
+
// PRECEDENCE: !important, not just document-order — MUI/emotion injects its
|
|
63
|
+
// cssVariables stylesheet at RUNTIME (on ThemeProvider mount), so there is no
|
|
64
|
+
// guaranteed head-order relative to this override layer (a sibling pod, P2
|
|
65
|
+
// Mantine, hit exactly this as a silent no-op with a bare `:root` rule).
|
|
66
|
+
// `!important` makes the win unconditional regardless of insertion order.
|
|
67
|
+
|
|
68
|
+
export const SORB_MUI_VARS = 'sorb/mui-vars'
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* role id (JJ id when unmapped) → MUI CSS var name.
|
|
72
|
+
* Left side is resolved through `options.roleMap` at format time (identity
|
|
73
|
+
* when the kit uses these ids directly — the JJ reference); right side is
|
|
74
|
+
* the MUI v6 documented `cssVariables:true` var it overrides.
|
|
75
|
+
*/
|
|
76
|
+
export const MUI_VAR_MAP = {
|
|
77
|
+
'color.brand': '--mui-palette-primary-main',
|
|
78
|
+
'color.brand-hover': '--mui-palette-primary-dark',
|
|
79
|
+
'color.brand-contrast': '--mui-palette-primary-contrastText',
|
|
80
|
+
'color.accent': '--mui-palette-secondary-main',
|
|
81
|
+
'color.accent-hover': '--mui-palette-secondary-dark',
|
|
82
|
+
'color.accent-contrast': '--mui-palette-secondary-contrastText',
|
|
83
|
+
'color.danger': '--mui-palette-error-main',
|
|
84
|
+
'color.danger-hover': '--mui-palette-error-dark',
|
|
85
|
+
'color.success': '--mui-palette-success-main',
|
|
86
|
+
'color.success-hover': '--mui-palette-success-dark',
|
|
87
|
+
'color.surface': '--mui-palette-background-default',
|
|
88
|
+
'color.surface-raised': '--mui-palette-background-paper',
|
|
89
|
+
'color.ink': '--mui-palette-text-primary',
|
|
90
|
+
'color.ink-muted': '--mui-palette-text-secondary',
|
|
91
|
+
'color.border': '--mui-palette-divider',
|
|
92
|
+
'radius.control': '--mui-shape-borderRadius',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cssVarOf = (id) => '--' + String(id).split('.').join('-')
|
|
96
|
+
|
|
97
|
+
// Same shape as `@sorb/core`'s `resolveRole` — inlined per the promotion
|
|
98
|
+
// spec so this format has no hard runtime dependency on a core version that
|
|
99
|
+
// ships the role contract (see file header).
|
|
100
|
+
const resolveRoleId = (roleId, roleMap) => (roleMap && roleMap[roleId]) || roleId
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* format: sorb/mui-vars
|
|
104
|
+
* Emits a CSS file that redeclares the mapped `--mui-*` vars as
|
|
105
|
+
* `var(--<kit-token>, <seed-fallback>) !important` refs. `options.roleMap`
|
|
106
|
+
* (role id → kit token id) lets a non-JJ kit reuse this format unmodified;
|
|
107
|
+
* defaults to the canonical/JJ ids (identity resolution) when omitted.
|
|
108
|
+
* `options.seedValues` (role id → literal fallback) is REQUIRED to avoid
|
|
109
|
+
* baking any one kit's hex values into the public format — a role with no
|
|
110
|
+
* `seedValues` entry emits `var(--kit-token)` with no fallback.
|
|
111
|
+
* @param {{dictionary: {allTokens: Array}, options?: {roleMap?: Record<string,string>, seedValues?: Record<string,string>}}} args
|
|
112
|
+
* @returns {string} the generated CSS.
|
|
113
|
+
*/
|
|
114
|
+
export const sorbMuiVars = ({ dictionary, options }) => {
|
|
115
|
+
const roleMap = options && options.roleMap
|
|
116
|
+
const seedValues = (options && options.seedValues) || {}
|
|
117
|
+
if (!options || !options.seedValues) {
|
|
118
|
+
// eslint-disable-next-line no-console
|
|
119
|
+
console.warn(
|
|
120
|
+
' ⚠ sorb/mui-vars: options.seedValues not supplied — every declaration will emit ' +
|
|
121
|
+
'var(--kit-token) with no fallback; MUI may not compute tonal variants until a real ' +
|
|
122
|
+
'value resolves at runtime.'
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
const byId = new Map(dictionary.allTokens.map((t) => [t.path.join('.'), t]))
|
|
126
|
+
const lines = []
|
|
127
|
+
const missing = []
|
|
128
|
+
const noFallback = []
|
|
129
|
+
for (const [roleId, muiVar] of Object.entries(MUI_VAR_MAP)) {
|
|
130
|
+
const tokenId = resolveRoleId(roleId, roleMap)
|
|
131
|
+
const t = byId.get(tokenId)
|
|
132
|
+
if (!t) { missing.push(tokenId); continue }
|
|
133
|
+
const seed = seedValues[roleId]
|
|
134
|
+
const ref = seed != null ? `var(${cssVarOf(tokenId)}, ${seed})` : `var(${cssVarOf(tokenId)})`
|
|
135
|
+
if (seed == null) noFallback.push(roleId)
|
|
136
|
+
// !important is load-bearing here — see file header "Precedence".
|
|
137
|
+
lines.push(` ${muiVar}: ${ref} !important;`)
|
|
138
|
+
}
|
|
139
|
+
if (missing.length) {
|
|
140
|
+
console.warn(` ⚠ sorb/mui-vars: ${missing.length} unmapped token id(s) skipped: ` + missing.join(', '))
|
|
141
|
+
}
|
|
142
|
+
if (noFallback.length) {
|
|
143
|
+
console.warn(
|
|
144
|
+
` ⚠ sorb/mui-vars: ${noFallback.length} role(s) with no seedValues fallback (var() with ` +
|
|
145
|
+
'no 2nd arg): ' + noFallback.join(', ')
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
return (
|
|
149
|
+
'/* AUTOGENERATED by Style Dictionary (sorb/mui-vars) — do not edit.\n' +
|
|
150
|
+
' Maps MUI cssVariables:true output vars -> kit token vars, so a Sorb bridge push\n' +
|
|
151
|
+
' against --color-*, --radius-*, etc. cascades through to MUI. Load AFTER MUI\'s\n' +
|
|
152
|
+
' own theme stylesheet (see the consuming app\'s entry import order).\n' +
|
|
153
|
+
' !important is load-bearing, not decorative — see this format\'s header "Precedence".\n' +
|
|
154
|
+
'\n' +
|
|
155
|
+
' COVERAGE (verified against @mui/material 6.5.0\'s actual theme.vars shape):\n' +
|
|
156
|
+
' COVERED — palette.{primary,secondary,error,success}.{main,dark,contrastText},\n' +
|
|
157
|
+
' palette.background.{default,paper}, palette.text.{primary,secondary},\n' +
|
|
158
|
+
' palette.divider, shape.borderRadius.\n' +
|
|
159
|
+
' NOT COVERED — typography, spacing function, transitions, zIndex, breakpoints\n' +
|
|
160
|
+
' (JS-only theme values; a live preview cannot re-theme them). */\n' +
|
|
161
|
+
':root, [data-mui-color-scheme] {\n' +
|
|
162
|
+
lines.join('\n') +
|
|
163
|
+
'\n}\n'
|
|
164
|
+
)
|
|
165
|
+
}
|