@sorb/seed 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/package.json +7 -3
- package/src/adapt/__fixtures__/Button.legacy.jsx +38 -0
- package/src/adapt/__fixtures__/Button.tokenized.tsx +28 -0
- package/src/adapt/__fixtures__/corpus/button-inline.case.json +10 -0
- package/src/adapt/__fixtures__/corpus/card-styled.case.json +9 -0
- package/src/adapt/__fixtures__/corpus/misc-unmapped.case.json +7 -0
- package/src/adapt/adaptCli.js +110 -0
- package/src/adapt/adaptCli.test.js +109 -0
- package/src/adapt/benchmark.js +113 -0
- package/src/adapt/benchmark.test.js +79 -0
- package/src/adapt/codemod.js +160 -0
- package/src/adapt/codemod.test.js +120 -0
- package/src/adapt/detectHardcoded.js +165 -0
- package/src/adapt/detectHardcoded.test.js +62 -0
- package/src/adapt/glob.js +87 -0
- package/src/adapt/mapToToken.js +94 -0
- package/src/adapt/mapToToken.test.js +103 -0
- package/src/adapt/report.js +53 -0
- package/src/adapt/report.test.js +66 -0
- package/src/adapt/runBenchmark.js +16 -0
- package/src/adapt/types.js +48 -0
- package/src/captureCli.js +38 -139
- package/src/cli.js +16 -1
- package/src/emit/sorbFormat.js +313 -0
- package/src/emit/sorbFormat.test.js +133 -0
- 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 +48 -0
- package/src/sources/figmaPlugin.js +83 -0
- package/src/sources/figmaPlugin.test.js +65 -0
- package/src/sources/storybookDom.js +188 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Sorb PrimeVue v4 preset format (`sorb/primevue-preset`).
|
|
2
|
+
//
|
|
3
|
+
// PROMOTED from `sorb-demo-primevue/src/jjPreset.js` (P4/P4a spike →
|
|
4
|
+
// framework-targets-productization T4, first of the six JJ-demo formats that
|
|
5
|
+
// is a JS-EMITTING target rather than a CSS one). The demo file stays in
|
|
6
|
+
// place for now (T8 retrofit is founder-gated); this is the canonical,
|
|
7
|
+
// generalized home.
|
|
8
|
+
//
|
|
9
|
+
// MECHANISM (carried from the spike): a `definePreset` leaf value that is a
|
|
10
|
+
// plain string (not one of PrimeVue's own `{a.b.c}` refs) is written VERBATIM
|
|
11
|
+
// as the corresponding `--p-*` custom property's value. `var(--kebab-token)`
|
|
12
|
+
// strings therefore make every `--p-*` var a pure indirection onto the kit's
|
|
13
|
+
// own CSS vars — a Sorb bridge push re-themes PrimeVue with zero
|
|
14
|
+
// component-level code changes. Two override tiers, same as the spike:
|
|
15
|
+
// 1. SEMANTIC tier — a small set of roots (`primary`, `text`, `content`,
|
|
16
|
+
// `formField`, `navigation.item`, `overlay.*`, `colorScheme.light.surface`)
|
|
17
|
+
// that most PrimeVue components read from, so overriding them re-themes
|
|
18
|
+
// Card/InputText/Select/Dialog/Toast/Tabs/… all at once.
|
|
19
|
+
// 2. COMPONENT tier — overrides for components that read straight from the
|
|
20
|
+
// PrimeVue PRIMITIVE palette instead of a semantic root (severity-colored
|
|
21
|
+
// Tag/Toast) or need an explicit per-component override (Menubar's
|
|
22
|
+
// brand-colored bar — the kit's `nav.*` tokens are a deliberately
|
|
23
|
+
// different color story than `content`/`surface`).
|
|
24
|
+
//
|
|
25
|
+
// RISK (framework-targets-productization risk table, T4): the emitted
|
|
26
|
+
// module's `import { definePreset } from '@primeuix/themes'` (+ the base
|
|
27
|
+
// preset import, e.g. `@primeuix/themes/aura`) is a PEER EXPECTATION — this
|
|
28
|
+
// format does not install or vendor `@primeuix/themes`; the consuming app's
|
|
29
|
+
// own bundler/dependency tree must already have it (any PrimeVue v4 app
|
|
30
|
+
// does). `new Function`/eval can't prove this module resolves for real (no
|
|
31
|
+
// `@primeuix/themes` in this package's — or even the consumer's CI —
|
|
32
|
+
// test env); it only proves the emitted source is syntactically the right
|
|
33
|
+
// shape. The real bundler path is proven by the T8 demo retrofit, not this
|
|
34
|
+
// format's test. Document this in the adapter/README snippet too.
|
|
35
|
+
//
|
|
36
|
+
// GENERALIZATION (T0 semantic-role contract): the spike hardcoded Janes
|
|
37
|
+
// Jeans' own token ids as every leaf. Promotion makes every leaf
|
|
38
|
+
// ROLE-RESOLVED — a non-JJ kit passes `options.roleMap` (role id → its own
|
|
39
|
+
// token id) instead of forking this file. Resolution is the same shape as
|
|
40
|
+
// `@sorb/core`'s `resolveRole`/`DEFAULT_ROLE_IDS`, inlined here rather than
|
|
41
|
+
// imported so this format degrades gracefully against an older published
|
|
42
|
+
// `@sorb/core` that predates the role contract (same feature-detect posture
|
|
43
|
+
// as the TargetAdapter registration side — see
|
|
44
|
+
// `sorb-leaf/src/targets/primevue.js`). None of the role ids below are in
|
|
45
|
+
// `@sorb/core`'s canonical `DEFAULT_ROLE_IDS` (JJ's component-tier ids —
|
|
46
|
+
// `button.primary.bg.default`, `card.bg`, `nav.bg`, `toast.bg.success`, …
|
|
47
|
+
// have no canonical counterpart yet; they're kit-private, per the
|
|
48
|
+
// productization spec's "contract scope = the union of the six maps' role
|
|
49
|
+
// columns, not a kit's full token tree"). They still go through the same
|
|
50
|
+
// `roleMap` resolution for consistency, and so a kit that DOES want to
|
|
51
|
+
// override them can.
|
|
52
|
+
//
|
|
53
|
+
// `options.basePreset` (default `'Aura'`) controls BOTH the base-theme
|
|
54
|
+
// import (`@primeuix/themes/<lowercased-name>`) and the `definePreset(<Name>,
|
|
55
|
+
// …)` call's first argument — PrimeVue v4 ships `Aura`/`Material`/`Lara`/
|
|
56
|
+
// `Nora` as documented base presets, each importable the same way.
|
|
57
|
+
|
|
58
|
+
export const SORB_PRIMEVUE_PRESET = 'sorb/primevue-preset'
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Structural role tree describing the PrimeVue `definePreset` shape this
|
|
62
|
+
* format generates, mirroring `jjPreset.js`'s hand-authored nesting exactly.
|
|
63
|
+
* Every leaf is a role id (a dot-path token id, canonical-or-JJ) resolved
|
|
64
|
+
* through `options.roleMap` before becoming a `var(--kebab-id)` ref.
|
|
65
|
+
*/
|
|
66
|
+
export const PRIMEVUE_ROLE_TREE = {
|
|
67
|
+
semantic: {
|
|
68
|
+
primary: {
|
|
69
|
+
color: 'button.primary.bg.default',
|
|
70
|
+
hoverColor: 'button.primary.bg.hover',
|
|
71
|
+
activeColor: 'button.primary.bg.hover',
|
|
72
|
+
contrastColor: 'button.primary.fg.default',
|
|
73
|
+
},
|
|
74
|
+
text: {
|
|
75
|
+
color: 'color.ink',
|
|
76
|
+
hoverColor: 'color.ink',
|
|
77
|
+
mutedColor: 'color.ink-muted',
|
|
78
|
+
hoverMutedColor: 'color.ink-muted',
|
|
79
|
+
},
|
|
80
|
+
content: {
|
|
81
|
+
background: 'card.bg',
|
|
82
|
+
hoverBackground: 'color.surface-raised',
|
|
83
|
+
borderColor: 'card.border',
|
|
84
|
+
color: 'color.ink',
|
|
85
|
+
hoverColor: 'color.ink',
|
|
86
|
+
borderRadius: 'card.radius',
|
|
87
|
+
},
|
|
88
|
+
formField: {
|
|
89
|
+
background: 'input.bg.default',
|
|
90
|
+
disabledBackground: 'input.bg.disabled',
|
|
91
|
+
filledBackground: 'input.bg.default',
|
|
92
|
+
filledHoverBackground: 'input.bg.default',
|
|
93
|
+
filledFocusBackground: 'input.bg.default',
|
|
94
|
+
borderColor: 'input.border.default',
|
|
95
|
+
hoverBorderColor: 'input.border.default',
|
|
96
|
+
focusBorderColor: 'input.border.focus',
|
|
97
|
+
invalidBorderColor: 'input.border.invalid',
|
|
98
|
+
color: 'input.fg',
|
|
99
|
+
disabledColor: 'color.ink-muted',
|
|
100
|
+
placeholderColor: 'input.placeholder',
|
|
101
|
+
invalidPlaceholderColor: 'input.placeholder',
|
|
102
|
+
borderRadius: 'input.radius',
|
|
103
|
+
},
|
|
104
|
+
navigation: {
|
|
105
|
+
item: {
|
|
106
|
+
activeColor: 'nav.link-active',
|
|
107
|
+
focusColor: 'nav.link-active',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
overlay: {
|
|
111
|
+
select: { background: 'card.bg', borderColor: 'card.border', color: 'color.ink' },
|
|
112
|
+
popover: { background: 'card.bg', borderColor: 'card.border', color: 'color.ink' },
|
|
113
|
+
modal: { background: 'card.bg', borderColor: 'card.border', color: 'color.ink' },
|
|
114
|
+
},
|
|
115
|
+
colorScheme: {
|
|
116
|
+
light: {
|
|
117
|
+
surface: {
|
|
118
|
+
50: 'color.surface-raised',
|
|
119
|
+
100: 'color.surface-raised',
|
|
120
|
+
200: 'color.surface-sunken',
|
|
121
|
+
300: 'color.border',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
components: {
|
|
127
|
+
// The brand-colored top bar — the one place the kit's nav.* tokens
|
|
128
|
+
// (distinct from the content/surface story used everywhere else) apply.
|
|
129
|
+
menubar: {
|
|
130
|
+
colorScheme: {
|
|
131
|
+
light: {
|
|
132
|
+
root: { background: 'nav.bg', borderColor: 'nav.bg', color: 'nav.fg' },
|
|
133
|
+
item: {
|
|
134
|
+
color: 'nav.fg',
|
|
135
|
+
focusColor: 'nav.link-active',
|
|
136
|
+
activeColor: 'nav.link-active',
|
|
137
|
+
icon: { color: 'nav.fg-muted', focusColor: 'nav.link-active', activeColor: 'nav.link-active' },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
// Toast severities: success/danger/info only (no "warn" — PrimeVue's
|
|
143
|
+
// default stays themed by the semantic content/text overrides above).
|
|
144
|
+
toast: {
|
|
145
|
+
colorScheme: {
|
|
146
|
+
light: {
|
|
147
|
+
success: { background: 'toast.bg.success', color: 'toast.fg', borderColor: 'toast.bg.success' },
|
|
148
|
+
error: { background: 'toast.bg.danger', color: 'toast.fg', borderColor: 'toast.bg.danger' },
|
|
149
|
+
info: { background: 'toast.bg.info', color: 'toast.fg', borderColor: 'toast.bg.info' },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const cssVarOf = (id) => '--' + String(id).split('.').join('-')
|
|
157
|
+
|
|
158
|
+
// Same shape as `@sorb/core`'s `resolveRole` — inlined per the promotion
|
|
159
|
+
// spec so this format has no hard runtime dependency on a core version that
|
|
160
|
+
// ships the role contract (see file header).
|
|
161
|
+
const resolveRoleId = (roleId, roleMap) => (roleMap && roleMap[roleId]) || roleId
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Recursively walk `PRIMEVUE_ROLE_TREE` (or a subtree), resolving each leaf
|
|
165
|
+
* role id through `roleMap` and, when `known` is supplied, dropping any leaf
|
|
166
|
+
* whose resolved token id isn't in the dictionary (skip-missing, same
|
|
167
|
+
* posture as `sorbMantineVars` — a partial kit still emits a valid, if
|
|
168
|
+
* smaller, preset rather than a broken var() ref). Empty objects left behind
|
|
169
|
+
* by an all-missing branch are pruned too.
|
|
170
|
+
* @param {*} node
|
|
171
|
+
* @param {Set<string>|undefined} known
|
|
172
|
+
* @param {Record<string,string>|undefined} roleMap
|
|
173
|
+
* @param {string[]} missing accumulator for unresolved role ids (mutated)
|
|
174
|
+
* @returns {*}
|
|
175
|
+
*/
|
|
176
|
+
const buildPresetNode = (node, known, roleMap, missing) => {
|
|
177
|
+
if (typeof node === 'string') {
|
|
178
|
+
const tokenId = resolveRoleId(node, roleMap)
|
|
179
|
+
if (known && !known.has(tokenId)) {
|
|
180
|
+
missing.push(tokenId)
|
|
181
|
+
return undefined
|
|
182
|
+
}
|
|
183
|
+
return `var(${cssVarOf(tokenId)})`
|
|
184
|
+
}
|
|
185
|
+
const out = {}
|
|
186
|
+
for (const [key, value] of Object.entries(node)) {
|
|
187
|
+
const resolved = buildPresetNode(value, known, roleMap, missing)
|
|
188
|
+
if (resolved === undefined) continue
|
|
189
|
+
if (typeof resolved === 'object' && Object.keys(resolved).length === 0) continue
|
|
190
|
+
out[key] = resolved
|
|
191
|
+
}
|
|
192
|
+
return out
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const BASE_PRESET_IMPORTS = {
|
|
196
|
+
Aura: '@primeuix/themes/aura',
|
|
197
|
+
Material: '@primeuix/themes/material',
|
|
198
|
+
Lara: '@primeuix/themes/lara',
|
|
199
|
+
Nora: '@primeuix/themes/nora',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* format: sorb/primevue-preset
|
|
204
|
+
* Emits a JS/ESM module string:
|
|
205
|
+
* import { definePreset } from '@primeuix/themes';
|
|
206
|
+
* import Aura from '@primeuix/themes/aura';
|
|
207
|
+
* export const preset = definePreset(Aura, { semantic: {...}, components: {...} });
|
|
208
|
+
* Every leaf is a `var(--kebab-token-id)` string — the live-preview
|
|
209
|
+
* invariant, same as every other Sorb format (no baked literals). Pure
|
|
210
|
+
* function of `{dictionary, options}`; zero `style-dictionary` dep.
|
|
211
|
+
* @param {{dictionary: {allTokens: Array}, options?: {roleMap?: Record<string,string>, basePreset?: string}}} args
|
|
212
|
+
* @returns {string} the generated ESM module source.
|
|
213
|
+
*/
|
|
214
|
+
export const sorbPrimevuePreset = ({ dictionary, options }) => {
|
|
215
|
+
const roleMap = options && options.roleMap
|
|
216
|
+
const basePreset = (options && options.basePreset) || 'Aura'
|
|
217
|
+
const known = new Set((dictionary.allTokens || []).map((t) => t.path.join('.')))
|
|
218
|
+
const missing = []
|
|
219
|
+
|
|
220
|
+
const semantic = buildPresetNode(PRIMEVUE_ROLE_TREE.semantic, known, roleMap, missing)
|
|
221
|
+
const components = buildPresetNode(PRIMEVUE_ROLE_TREE.components, known, roleMap, missing)
|
|
222
|
+
|
|
223
|
+
if (missing.length) {
|
|
224
|
+
console.warn(` ⚠ sorb/primevue-preset: ${missing.length} unmapped token id(s) skipped: ` + missing.join(', '))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const basePresetImport = BASE_PRESET_IMPORTS[basePreset] || `@primeuix/themes/${String(basePreset).toLowerCase()}`
|
|
228
|
+
const presetBody = JSON.stringify({ semantic, components }, null, 2)
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
'// AUTO-GENERATED by Style Dictionary (sorb/primevue-preset) — do not edit.\n' +
|
|
232
|
+
'// PEER EXPECTATION: this module imports `@primeuix/themes` (+ its base preset\n' +
|
|
233
|
+
'// subpath) — install it in the consuming app; this format neither installs nor\n' +
|
|
234
|
+
'// vendors it. See sorb-seed/src/emit/sorbPrimevue.js header "RISK".\n' +
|
|
235
|
+
`import { definePreset } from '@primeuix/themes';\n` +
|
|
236
|
+
`import ${basePreset} from '${basePresetImport}';\n` +
|
|
237
|
+
'\n' +
|
|
238
|
+
`export const preset = definePreset(${basePreset}, ` + presetBody + ');\n'
|
|
239
|
+
)
|
|
240
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Tests for the Sorb PrimeVue v4 preset format (`sorb/primevue-preset`) — the
|
|
2
|
+
// first JS-EMITTING target format (the others emit CSS). `@primeuix/themes`
|
|
3
|
+
// isn't installed in this package's test env (it's a peer expectation of the
|
|
4
|
+
// EMITTED module, not a dep of `@sorb/seed` — see the format's file header),
|
|
5
|
+
// so these tests can't `import()` the real output. Instead they (a) assert
|
|
6
|
+
// the raw source string's shape by regex, and (b) `new Function`-evaluate a
|
|
7
|
+
// transformed copy (imports stripped, `definePreset`/base preset stubbed) to
|
|
8
|
+
// prove the object-literal body is syntactically valid and structurally
|
|
9
|
+
// correct — the same "prove it evaluates" spirit as sorbFormat.test.js's
|
|
10
|
+
// `new Function('module','exports', src)` check, adapted for ESM `import`/
|
|
11
|
+
// `export` syntax that `new Function` cannot parse directly.
|
|
12
|
+
import { test } from 'node:test'
|
|
13
|
+
import assert from 'node:assert/strict'
|
|
14
|
+
import { SORB_PRIMEVUE_PRESET, PRIMEVUE_ROLE_TREE, sorbPrimevuePreset } from './sorbPrimevue.js'
|
|
15
|
+
|
|
16
|
+
// Helper: a fabricated dictionary of resolved tokens, keyed by dot-path id.
|
|
17
|
+
const tok = (id) => ({ path: id.split('.') })
|
|
18
|
+
const dictOf = (ids) => ({ allTokens: ids.map(tok) })
|
|
19
|
+
|
|
20
|
+
// Every role id referenced anywhere in PRIMEVUE_ROLE_TREE, flattened.
|
|
21
|
+
const flattenRoleIds = (node, out = []) => {
|
|
22
|
+
if (typeof node === 'string') { out.push(node); return out }
|
|
23
|
+
for (const v of Object.values(node)) flattenRoleIds(v, out)
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
const ALL_ROLE_IDS = flattenRoleIds(PRIMEVUE_ROLE_TREE)
|
|
27
|
+
|
|
28
|
+
// Evaluate the object-literal body of an emitted module: strip the `import`
|
|
29
|
+
// lines and the `export const preset = definePreset(Base, ` … `);` wrapper,
|
|
30
|
+
// then run the remaining `{ ... }` object literal through `new Function` with
|
|
31
|
+
// no external bindings needed (it's pure data — strings only).
|
|
32
|
+
const evalPresetBody = (src) => {
|
|
33
|
+
const withoutImports = src.replace(/^import .*$/gm, '')
|
|
34
|
+
const match = withoutImports.match(/export const preset = definePreset\(\w+, ([\s\S]*)\);\s*$/)
|
|
35
|
+
assert.ok(match, 'module source must match the definePreset(Base, {...}); shape')
|
|
36
|
+
return new Function(`return (${match[1]})`)()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('format name constant is the documented id', () => {
|
|
40
|
+
assert.equal(SORB_PRIMEVUE_PRESET, 'sorb/primevue-preset')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('emits the documented import lines + definePreset(Aura, ...) call by default', () => {
|
|
44
|
+
const dictionary = dictOf(ALL_ROLE_IDS)
|
|
45
|
+
const src = sorbPrimevuePreset({ dictionary })
|
|
46
|
+
assert.match(src, /^import \{ definePreset \} from '@primeuix\/themes';$/m)
|
|
47
|
+
assert.match(src, /^import Aura from '@primeuix\/themes\/aura';$/m)
|
|
48
|
+
assert.match(src, /^export const preset = definePreset\(Aura, \{/m)
|
|
49
|
+
assert.match(src, /\}\);\s*$/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('every leaf is a var() reference, never a baked literal (live-preview invariant)', () => {
|
|
53
|
+
const dictionary = dictOf(ALL_ROLE_IDS)
|
|
54
|
+
const src = sorbPrimevuePreset({ dictionary })
|
|
55
|
+
const preset = evalPresetBody(src)
|
|
56
|
+
const leaves = (o) => Object.values(o).flatMap((v) => (typeof v === 'string' ? [v] : leaves(v)))
|
|
57
|
+
const values = [...leaves(preset.semantic), ...leaves(preset.components)]
|
|
58
|
+
assert.ok(values.length > 0)
|
|
59
|
+
for (const val of values) assert.match(val, /^var\(--[a-z0-9-]+\)$/)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('semantic tier matches jjPreset.js structure/values exactly (identity roleMap)', () => {
|
|
63
|
+
const dictionary = dictOf(ALL_ROLE_IDS)
|
|
64
|
+
const src = sorbPrimevuePreset({ dictionary })
|
|
65
|
+
const preset = evalPresetBody(src)
|
|
66
|
+
assert.equal(preset.semantic.primary.color, 'var(--button-primary-bg-default)')
|
|
67
|
+
assert.equal(preset.semantic.primary.contrastColor, 'var(--button-primary-fg-default)')
|
|
68
|
+
assert.equal(preset.semantic.text.mutedColor, 'var(--color-ink-muted)')
|
|
69
|
+
assert.equal(preset.semantic.content.borderRadius, 'var(--card-radius)')
|
|
70
|
+
assert.equal(preset.semantic.formField.focusBorderColor, 'var(--input-border-focus)')
|
|
71
|
+
assert.equal(preset.semantic.navigation.item.activeColor, 'var(--nav-link-active)')
|
|
72
|
+
assert.equal(preset.semantic.overlay.popover.background, 'var(--card-bg)')
|
|
73
|
+
assert.equal(preset.semantic.colorScheme.light.surface['200'], 'var(--color-surface-sunken)')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('component tier (Menubar/Toast reading the primitive palette) matches jjPreset.js', () => {
|
|
77
|
+
const dictionary = dictOf(ALL_ROLE_IDS)
|
|
78
|
+
const src = sorbPrimevuePreset({ dictionary })
|
|
79
|
+
const preset = evalPresetBody(src)
|
|
80
|
+
assert.equal(preset.components.menubar.colorScheme.light.root.background, 'var(--nav-bg)')
|
|
81
|
+
assert.equal(preset.components.menubar.colorScheme.light.item.icon.color, 'var(--nav-fg-muted)')
|
|
82
|
+
assert.equal(preset.components.toast.colorScheme.light.success.background, 'var(--toast-bg-success)')
|
|
83
|
+
assert.equal(preset.components.toast.colorScheme.light.error.borderColor, 'var(--toast-bg-danger)')
|
|
84
|
+
assert.equal(preset.components.toast.colorScheme.light.info.color, 'var(--toast-fg)')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('options.roleMap remaps the role-id column without forking the format', () => {
|
|
88
|
+
const roleMap = {
|
|
89
|
+
'button.primary.bg.default': 'kit.primary',
|
|
90
|
+
'color.ink': 'kit.text',
|
|
91
|
+
}
|
|
92
|
+
const dictionary = dictOf(['kit.primary', 'kit.text', ...ALL_ROLE_IDS.filter((id) => id !== 'button.primary.bg.default' && id !== 'color.ink')])
|
|
93
|
+
const src = sorbPrimevuePreset({ dictionary, options: { roleMap } })
|
|
94
|
+
const preset = evalPresetBody(src)
|
|
95
|
+
assert.equal(preset.semantic.primary.color, 'var(--kit-primary)')
|
|
96
|
+
assert.equal(preset.semantic.text.color, 'var(--kit-text)')
|
|
97
|
+
// unmapped roles fall back to their canonical/default id
|
|
98
|
+
assert.equal(preset.semantic.text.mutedColor, 'var(--color-ink-muted)')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('options.basePreset swaps both the base import and the definePreset() base arg', () => {
|
|
102
|
+
const dictionary = dictOf(ALL_ROLE_IDS)
|
|
103
|
+
const src = sorbPrimevuePreset({ dictionary, options: { basePreset: 'Lara' } })
|
|
104
|
+
assert.match(src, /^import Lara from '@primeuix\/themes\/lara';$/m)
|
|
105
|
+
assert.match(src, /^export const preset = definePreset\(Lara, \{/m)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('an unmapped/missing token id is skipped, not emitted as a broken var() ref', () => {
|
|
109
|
+
// Only supply the tokens for the `primary` subtree; everything else absent.
|
|
110
|
+
const dictionary = dictOf([
|
|
111
|
+
'button.primary.bg.default',
|
|
112
|
+
'button.primary.bg.hover',
|
|
113
|
+
'button.primary.fg.default',
|
|
114
|
+
])
|
|
115
|
+
const src = sorbPrimevuePreset({ dictionary })
|
|
116
|
+
const preset = evalPresetBody(src)
|
|
117
|
+
assert.equal(preset.semantic.primary.color, 'var(--button-primary-bg-default)')
|
|
118
|
+
// text/content/formField/etc never had their tokens present → pruned away
|
|
119
|
+
assert.equal(preset.semantic.text, undefined)
|
|
120
|
+
assert.equal(preset.components.menubar, undefined)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('emitted module source contains no baked literal — only var(--kebab) leaves', () => {
|
|
124
|
+
const dictionary = dictOf(ALL_ROLE_IDS)
|
|
125
|
+
const src = sorbPrimevuePreset({ dictionary })
|
|
126
|
+
// Every quoted string value in the object body must be a var() ref.
|
|
127
|
+
const stringValues = [...src.matchAll(/: "([^"]*)"/g)].map((m) => m[1])
|
|
128
|
+
assert.ok(stringValues.length > 0)
|
|
129
|
+
for (const v of stringValues) assert.match(v, /^var\(--[a-z0-9-]+\)$/)
|
|
130
|
+
})
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Sorb shadcn/ui theme format (`sorb/shadcn-theme`) — framework-targets-
|
|
2
|
+
// productization T1 (Tailwind v4 + shadcn/ui).
|
|
3
|
+
//
|
|
4
|
+
// Promoted from `sorb-demo-tailwind/src/tokens/shadcn-map.css` +
|
|
5
|
+
// `shadcn-theme.css` (hand-authored source material) into ONE generated CSS
|
|
6
|
+
// artifact: shadcn's conventional `:root{}` var map (--background,
|
|
7
|
+
// --primary, …) chained onto Sorb's own tokens via the T0 semantic-role
|
|
8
|
+
// contract, plus the `@theme inline{}` Tailwind-utility binding block
|
|
9
|
+
// (including the `calc()` radius scale). Zero `style-dictionary` dep — pure
|
|
10
|
+
// function of `{ dictionary, options }`, matching `sorbFormat.js`'s
|
|
11
|
+
// conventions.
|
|
12
|
+
//
|
|
13
|
+
// Every leaf is a `var(--<kebab>)` reference (live-preview invariant) — the
|
|
14
|
+
// bridge's runtime var-swap re-themes shadcn components with no
|
|
15
|
+
// shadcn-specific bridge code.
|
|
16
|
+
|
|
17
|
+
/** @type {'sorb/shadcn-theme'} */
|
|
18
|
+
export const SORB_SHADCN = 'sorb/shadcn-theme'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a canonical role id to the kit's actual token id via an optional
|
|
22
|
+
* override map — inlined one-liner matching `@sorb/core`'s `resolveRole`
|
|
23
|
+
* (T0). Kept local rather than imported so `@sorb/seed` doesn't take a
|
|
24
|
+
* runtime dep on a specific `@sorb/core` version (formats are plain
|
|
25
|
+
* functions; the published core can lag the connector contract).
|
|
26
|
+
* @param {string} roleId
|
|
27
|
+
* @param {Record<string,string>} [roleMap]
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
const resolveShadcnRole = (roleId, roleMap) => (roleMap && roleMap[roleId]) || roleId
|
|
31
|
+
|
|
32
|
+
const cssVarOf = (tokenId) => '--' + String(tokenId).split('.').join('-')
|
|
33
|
+
|
|
34
|
+
// shadcn CSS var → canonical role id (DEFAULT_ROLE_IDS.color / .radius from
|
|
35
|
+
// `@sorb/core`). Order matches the hand-authored `shadcn-map.css` for a
|
|
36
|
+
// clean round-trip diff.
|
|
37
|
+
const SHADCN_COLOR_ROLE_MAP = [
|
|
38
|
+
['--background', 'color.surface'],
|
|
39
|
+
['--foreground', 'color.ink'],
|
|
40
|
+
['--card', 'color.surface'],
|
|
41
|
+
['--card-foreground', 'color.ink'],
|
|
42
|
+
['--popover', 'color.surface'],
|
|
43
|
+
['--popover-foreground', 'color.ink'],
|
|
44
|
+
['--primary', 'color.brand'],
|
|
45
|
+
['--primary-foreground', 'color.brand-contrast'],
|
|
46
|
+
['--secondary', 'color.surface-raised'],
|
|
47
|
+
['--secondary-foreground', 'color.ink'],
|
|
48
|
+
['--muted', 'color.surface-sunken'],
|
|
49
|
+
['--muted-foreground', 'color.ink-muted'],
|
|
50
|
+
['--accent', 'color.accent'],
|
|
51
|
+
['--accent-foreground', 'color.accent-contrast'],
|
|
52
|
+
['--destructive', 'color.danger'],
|
|
53
|
+
['--border', 'color.border'],
|
|
54
|
+
['--input', 'color.border'],
|
|
55
|
+
['--ring', 'color.focus-ring'],
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
// ROLE-CONTRACT GAP (flagged, not invented — framework-targets-
|
|
59
|
+
// productization T1 note): shadcn's `--destructive-foreground` wants "the
|
|
60
|
+
// ink color that reads on top of --destructive". `@sorb/core`'s
|
|
61
|
+
// `DEFAULT_ROLE_IDS` only defines `-contrast` roles for brand/accent, not
|
|
62
|
+
// danger — there is no canonical `color.danger-contrast`. Rather than invent
|
|
63
|
+
// one unilaterally, this references the kit's raw `color.white` primitive
|
|
64
|
+
// (matches the JJ demo's hand-authored value) directly — NOT run through
|
|
65
|
+
// `options.roleMap`, since it isn't a role id. A kit without a `color.white`
|
|
66
|
+
// token overrides via `options.destructiveForeground`.
|
|
67
|
+
const DESTRUCTIVE_FOREGROUND_DEFAULT = 'color.white'
|
|
68
|
+
|
|
69
|
+
// `--radius` role — the one non-color entry in the `:root{}` map.
|
|
70
|
+
const RADIUS_ROLE_DEFAULT = 'radius.control'
|
|
71
|
+
|
|
72
|
+
// `@theme inline{}` block: purely mechanical — chains Tailwind's `--color-*`
|
|
73
|
+
// theme keys onto the shadcn vars defined above (never onto Sorb token ids
|
|
74
|
+
// directly), plus the fixed `calc()` radius scale. Independent of roleMap.
|
|
75
|
+
const THEME_INLINE_COLOR_ENTRIES = [
|
|
76
|
+
['--color-background', '--background'],
|
|
77
|
+
['--color-foreground', '--foreground'],
|
|
78
|
+
['--color-card', '--card'],
|
|
79
|
+
['--color-card-foreground', '--card-foreground'],
|
|
80
|
+
['--color-popover', '--popover'],
|
|
81
|
+
['--color-popover-foreground', '--popover-foreground'],
|
|
82
|
+
['--color-primary', '--primary'],
|
|
83
|
+
['--color-primary-foreground', '--primary-foreground'],
|
|
84
|
+
['--color-secondary', '--secondary'],
|
|
85
|
+
['--color-secondary-foreground', '--secondary-foreground'],
|
|
86
|
+
['--color-muted', '--muted'],
|
|
87
|
+
['--color-muted-foreground', '--muted-foreground'],
|
|
88
|
+
['--color-accent', '--accent'],
|
|
89
|
+
['--color-accent-foreground', '--accent-foreground'],
|
|
90
|
+
['--color-destructive', '--destructive'],
|
|
91
|
+
['--color-destructive-foreground', '--destructive-foreground'],
|
|
92
|
+
['--color-border', '--border'],
|
|
93
|
+
['--color-input', '--input'],
|
|
94
|
+
['--color-ring', '--ring'],
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the `:root{}` shadcn-var → Sorb-token map, resolving roles through
|
|
99
|
+
* `options.roleMap` (defaulting to identity — the JJ reference kit already
|
|
100
|
+
* uses canonical role ids).
|
|
101
|
+
* @param {{roleMap?: Record<string,string>, destructiveForeground?: string, radiusRole?: string}} [options]
|
|
102
|
+
* @param {Set<string>} [knownTokenIds] when provided, unknown resolved ids are warned (not skipped — CSS still emits, just points at an undefined var).
|
|
103
|
+
* @returns {string[]} lines
|
|
104
|
+
*/
|
|
105
|
+
export const shadcnRootLines = (options = {}, knownTokenIds) => {
|
|
106
|
+
const { roleMap, destructiveForeground, radiusRole } = options
|
|
107
|
+
const missing = []
|
|
108
|
+
const resolve = (roleId) => resolveShadcnRole(roleId, roleMap)
|
|
109
|
+
const checkAndVar = (tokenId, roleId) => {
|
|
110
|
+
if (knownTokenIds && !knownTokenIds.has(tokenId)) missing.push(`${roleId} → ${tokenId}`)
|
|
111
|
+
return `var(${cssVarOf(tokenId)})`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const lines = []
|
|
115
|
+
for (const [shadcnVar, roleId] of SHADCN_COLOR_ROLE_MAP) {
|
|
116
|
+
const tokenId = resolve(roleId)
|
|
117
|
+
lines.push(` ${shadcnVar}: ${checkAndVar(tokenId, roleId)};`)
|
|
118
|
+
if (shadcnVar === '--destructive') {
|
|
119
|
+
// --destructive-foreground has no canonical role (see the
|
|
120
|
+
// ROLE-CONTRACT GAP note above) — emitted right after --destructive to
|
|
121
|
+
// match the hand-authored `shadcn-map.css` ordering byte-for-byte.
|
|
122
|
+
const destructiveFgId = destructiveForeground || DESTRUCTIVE_FOREGROUND_DEFAULT
|
|
123
|
+
lines.push(` --destructive-foreground: ${checkAndVar(destructiveFgId, '(non-role) destructiveForeground')};`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const radiusId = resolve(radiusRole || RADIUS_ROLE_DEFAULT)
|
|
127
|
+
lines.push(` --radius: ${checkAndVar(radiusId, radiusRole || RADIUS_ROLE_DEFAULT)};`)
|
|
128
|
+
|
|
129
|
+
if (missing.length) {
|
|
130
|
+
// eslint-disable-next-line no-console
|
|
131
|
+
console.warn(` ⚠ sorb/shadcn-theme: ${missing.length} unresolved token id(s): ` + missing.join(', '))
|
|
132
|
+
}
|
|
133
|
+
return lines
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The fixed `@theme inline{}` block lines (mechanical; no roleMap input). */
|
|
137
|
+
export const shadcnThemeInlineLines = () => {
|
|
138
|
+
const lines = THEME_INLINE_COLOR_ENTRIES.map(([key, ref]) => ` ${key}: var(${ref});`)
|
|
139
|
+
lines.push(
|
|
140
|
+
' --radius-sm: calc(var(--radius) - 4px);',
|
|
141
|
+
' --radius-md: calc(var(--radius) - 2px);',
|
|
142
|
+
' --radius-lg: var(--radius);',
|
|
143
|
+
' --radius-xl: calc(var(--radius) + 4px);',
|
|
144
|
+
)
|
|
145
|
+
return lines
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* format: sorb/shadcn-theme
|
|
150
|
+
* Emits ONE CSS artifact: shadcn's `:root{}` semantic-var map (chained onto
|
|
151
|
+
* Sorb tokens via the role contract) followed by the `@theme inline{}`
|
|
152
|
+
* Tailwind-utility binding block — reproducing what
|
|
153
|
+
* `sorb-demo-tailwind/src/tokens/shadcn-map.css` + `shadcn-theme.css` hand-
|
|
154
|
+
* authored. Pair with `variables.css` (defines the Sorb vars this format
|
|
155
|
+
* references) in the consumer's CSS import order:
|
|
156
|
+
* @import "tailwindcss";
|
|
157
|
+
* @import "./variables.css"; sorb tokens — the bridge swaps these live
|
|
158
|
+
* @import "./shadcn-theme.css"; this file
|
|
159
|
+
* @param {{dictionary: {allTokens: Array<{path: string[]}>}, options?: {roleMap?: Record<string,string>, destructiveForeground?: string, radiusRole?: string}}} args
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
export const sorbShadcn = ({ dictionary, options } = {}) => {
|
|
163
|
+
const known = dictionary && Array.isArray(dictionary.allTokens)
|
|
164
|
+
? new Set(dictionary.allTokens.map((t) => t.path.join('.')))
|
|
165
|
+
: undefined
|
|
166
|
+
|
|
167
|
+
const rootLines = shadcnRootLines(options || {}, known)
|
|
168
|
+
const themeLines = shadcnThemeInlineLines()
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
'/* AUTO-GENERATED by Style Dictionary (sorb/shadcn-theme) — do not edit.\n' +
|
|
172
|
+
' shadcn/ui semantic-var map, chained onto Sorb\'s runtime CSS vars via the\n' +
|
|
173
|
+
' semantic-role contract (@sorb/core DEFAULT_ROLE_IDS), plus the `@theme\n' +
|
|
174
|
+
' inline` Tailwind-utility bindings shadcn components read (bg-primary,\n' +
|
|
175
|
+
' text-foreground, border-input, …). Import order in your entry CSS:\n' +
|
|
176
|
+
' @import "tailwindcss";\n' +
|
|
177
|
+
' @import "./variables.css"; sorb tokens — the bridge swaps these live\n' +
|
|
178
|
+
' @import "./shadcn-theme.css"; this file */\n' +
|
|
179
|
+
':root {\n' +
|
|
180
|
+
rootLines.join('\n') +
|
|
181
|
+
'\n}\n\n' +
|
|
182
|
+
'@theme inline {\n' +
|
|
183
|
+
themeLines.join('\n') +
|
|
184
|
+
'\n}\n'
|
|
185
|
+
)
|
|
186
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Tests for the Sorb shadcn/ui theme format (`sorb/shadcn-theme`).
|
|
2
|
+
// Run: node --test
|
|
3
|
+
import { test } from 'node:test'
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import { SORB_SHADCN, sorbShadcn, shadcnRootLines, shadcnThemeInlineLines } from './sorbShadcn.js'
|
|
6
|
+
|
|
7
|
+
// A fabricated dictionary carrying the JJ role tokens the format resolves
|
|
8
|
+
// against (identity roleMap case — the JJ reference kit already uses
|
|
9
|
+
// canonical role ids from `@sorb/core`'s DEFAULT_ROLE_IDS).
|
|
10
|
+
const JJ_ROLE_TOKEN_IDS = [
|
|
11
|
+
'color.surface', 'color.surface-raised', 'color.surface-sunken',
|
|
12
|
+
'color.ink', 'color.ink-muted',
|
|
13
|
+
'color.brand', 'color.brand-contrast',
|
|
14
|
+
'color.accent', 'color.accent-contrast',
|
|
15
|
+
'color.danger', 'color.border', 'color.focus-ring',
|
|
16
|
+
'color.white',
|
|
17
|
+
'radius.control',
|
|
18
|
+
]
|
|
19
|
+
const dictionary = { allTokens: JJ_ROLE_TOKEN_IDS.map((id) => ({ path: id.split('.') })) }
|
|
20
|
+
|
|
21
|
+
test('format id is sorb/shadcn-theme', () => {
|
|
22
|
+
assert.equal(SORB_SHADCN, 'sorb/shadcn-theme')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('root map: identity roleMap resolves every shadcn var to its canonical role\'s var()', () => {
|
|
26
|
+
const css = sorbShadcn({ dictionary })
|
|
27
|
+
assert.match(css, /--background: var\(--color-surface\);/)
|
|
28
|
+
assert.match(css, /--foreground: var\(--color-ink\);/)
|
|
29
|
+
assert.match(css, /--primary: var\(--color-brand\);/)
|
|
30
|
+
assert.match(css, /--primary-foreground: var\(--color-brand-contrast\);/)
|
|
31
|
+
assert.match(css, /--destructive: var\(--color-danger\);/)
|
|
32
|
+
assert.match(css, /--border: var\(--color-border\);/)
|
|
33
|
+
assert.match(css, /--input: var\(--color-border\);/)
|
|
34
|
+
assert.match(css, /--ring: var\(--color-focus-ring\);/)
|
|
35
|
+
assert.match(css, /--radius: var\(--radius-control\);/)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('root map: destructive-foreground falls back to the color.white primitive (role-contract gap)', () => {
|
|
39
|
+
const css = sorbShadcn({ dictionary })
|
|
40
|
+
assert.match(css, /--destructive-foreground: var\(--color-white\);/)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('root map: options.roleMap overrides a canonical role id to a non-JJ kit token', () => {
|
|
44
|
+
const css = sorbShadcn({
|
|
45
|
+
dictionary: { allTokens: [{ path: ['brand', 'primary'] }, ...dictionary.allTokens] },
|
|
46
|
+
options: { roleMap: { 'color.brand': 'brand.primary' } },
|
|
47
|
+
})
|
|
48
|
+
assert.match(css, /--primary: var\(--brand-primary\);/)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('options.destructiveForeground / options.radiusRole override the non-role defaults', () => {
|
|
52
|
+
const css = sorbShadcn({
|
|
53
|
+
dictionary: { allTokens: [{ path: ['ink', 'onDanger'] }, { path: ['radius', 'lg'] }, ...dictionary.allTokens] },
|
|
54
|
+
options: { destructiveForeground: 'ink.onDanger', radiusRole: 'radius.lg' },
|
|
55
|
+
})
|
|
56
|
+
assert.match(css, /--destructive-foreground: var\(--ink-onDanger\);/)
|
|
57
|
+
assert.match(css, /--radius: var\(--radius-lg\);/)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('emits an @theme inline block with the calc() radius scale, chained onto shadcn vars (not raw tokens)', () => {
|
|
61
|
+
const css = sorbShadcn({ dictionary })
|
|
62
|
+
assert.match(css, /@theme inline \{/)
|
|
63
|
+
assert.match(css, /--color-background: var\(--background\);/)
|
|
64
|
+
assert.match(css, /--color-primary-foreground: var\(--primary-foreground\);/)
|
|
65
|
+
assert.match(css, /--radius-sm: calc\(var\(--radius\) - 4px\);/)
|
|
66
|
+
assert.match(css, /--radius-md: calc\(var\(--radius\) - 2px\);/)
|
|
67
|
+
assert.match(css, /--radius-lg: var\(--radius\);/)
|
|
68
|
+
assert.match(css, /--radius-xl: calc\(var\(--radius\) \+ 4px\);/)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('every emitted value is a var()/calc(var()) reference — never a baked literal (live-preview invariant)', () => {
|
|
72
|
+
const css = sorbShadcn({ dictionary })
|
|
73
|
+
const declLines = css.split('\n').filter((l) => /^\s*--[a-z-]+:/.test(l))
|
|
74
|
+
assert.ok(declLines.length > 0)
|
|
75
|
+
for (const line of declLines) {
|
|
76
|
+
assert.match(line, /:\s*(var\(--[a-zA-Z0-9-]+\)|calc\(var\(--[a-zA-Z0-9-]+\)[^)]*\))\s*;\s*$/, `unexpected literal: ${line}`)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('warns (does not throw) when a resolved token id is not in the dictionary', () => {
|
|
81
|
+
const warnings = []
|
|
82
|
+
const origWarn = console.warn
|
|
83
|
+
console.warn = (msg) => warnings.push(msg)
|
|
84
|
+
try {
|
|
85
|
+
const css = sorbShadcn({ dictionary: { allTokens: [{ path: ['color', 'surface'] }] } })
|
|
86
|
+
assert.match(css, /--background: var\(--color-surface\);/) // still emits
|
|
87
|
+
} finally {
|
|
88
|
+
console.warn = origWarn
|
|
89
|
+
}
|
|
90
|
+
assert.ok(warnings.length > 0)
|
|
91
|
+
assert.match(warnings[0], /unresolved token id/)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('shadcnRootLines / shadcnThemeInlineLines are pure and independently testable', () => {
|
|
95
|
+
const rootLines = shadcnRootLines({})
|
|
96
|
+
assert.equal(rootLines.length, SHADCN_ROOT_LINE_COUNT)
|
|
97
|
+
const themeLines = shadcnThemeInlineLines()
|
|
98
|
+
assert.equal(themeLines.filter((l) => l.includes('--radius-')).length, 4)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// 18 color-role pairs + destructive-foreground + radius
|
|
102
|
+
const SHADCN_ROOT_LINE_COUNT = 20
|