@uniweb/theming 0.1.5 → 0.1.7
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 +1 -1
- package/src/css-generator.js +47 -48
- package/src/processor.js +134 -13
package/package.json
CHANGED
package/src/css-generator.js
CHANGED
|
@@ -210,32 +210,38 @@ const GENERIC_FAMILIES = new Set([
|
|
|
210
210
|
'inherit', 'initial', 'revert', 'unset',
|
|
211
211
|
])
|
|
212
212
|
|
|
213
|
+
// Font-role classification (isFontVar, NON_FAMILY_FONT_VARS) and the default
|
|
214
|
+
// role selectors now live in processor.js, which resolves the whole font-role
|
|
215
|
+
// namespace (built-in roles + foundation redefinitions/additions) into config.
|
|
216
|
+
|
|
213
217
|
/**
|
|
214
|
-
* Extract font family names
|
|
218
|
+
* Extract the font family names to load, from a resolved font-role map.
|
|
215
219
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
220
|
+
* A role contributes its family only when the processor flagged it `load`
|
|
221
|
+
* (site-set or foundation-declared — not a bare framework default like
|
|
222
|
+
* `system-ui`). Generic keywords and numeric tokens are skipped, so a role's
|
|
223
|
+
* fallback stack never triggers an unnecessary fetch.
|
|
219
224
|
*
|
|
220
|
-
* @param {Object}
|
|
221
|
-
* @returns {Set<string>} Lowercase family names
|
|
225
|
+
* @param {Object} fontRoles - Resolved roles: name → { value, applyTo, apply, load }
|
|
226
|
+
* @returns {Set<string>} Lowercase family names to load (@font-face / import filter)
|
|
222
227
|
*/
|
|
223
|
-
function extractUsedFamilies(
|
|
228
|
+
function extractUsedFamilies(fontRoles = {}) {
|
|
224
229
|
const used = new Set()
|
|
225
|
-
const slots = fonts._userSlots || ['body', 'heading', 'mono']
|
|
226
|
-
|
|
227
|
-
for (const slot of slots) {
|
|
228
|
-
const value = fonts[slot]
|
|
229
|
-
if (!value) continue
|
|
230
230
|
|
|
231
|
+
const addFamiliesFrom = (value) => {
|
|
232
|
+
if (typeof value !== 'string') return
|
|
231
233
|
for (const segment of value.split(',')) {
|
|
232
234
|
const name = segment.trim().replace(/^["']|["']$/g, '').toLowerCase()
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
// Skip generics, empties, and numeric/length tokens.
|
|
236
|
+
if (!name || GENERIC_FAMILIES.has(name) || /^\d/.test(name)) continue
|
|
237
|
+
used.add(name)
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
|
|
241
|
+
for (const role of Object.values(fontRoles)) {
|
|
242
|
+
if (role && role.load) addFamiliesFrom(role.value)
|
|
243
|
+
}
|
|
244
|
+
|
|
239
245
|
return used
|
|
240
246
|
}
|
|
241
247
|
|
|
@@ -285,14 +291,15 @@ function isGoogleFontsUrl(url) {
|
|
|
285
291
|
/**
|
|
286
292
|
* Generate font CSS and external link tags.
|
|
287
293
|
*
|
|
288
|
-
* @param {Object}
|
|
294
|
+
* @param {Object} fontRoles - Resolved font roles (name → { value, applyTo, apply, load })
|
|
295
|
+
* @param {Object} fonts - Font loading config (import[] / faces[])
|
|
289
296
|
* @returns {{ css: string, links: string }} css for <style>, links for <head>
|
|
290
297
|
*/
|
|
291
|
-
function generateFontCSS(fonts = {}) {
|
|
298
|
+
function generateFontCSS(fontRoles = {}, fonts = {}) {
|
|
292
299
|
const cssLines = []
|
|
293
300
|
const linkTags = []
|
|
294
301
|
|
|
295
|
-
const usedFamilies = extractUsedFamilies(
|
|
302
|
+
const usedFamilies = extractUsedFamilies(fontRoles)
|
|
296
303
|
|
|
297
304
|
// --- @font-face rules from faces[] (filtered to used families) ---
|
|
298
305
|
// Also generate <link rel="preload"> hints so browsers fetch fonts early
|
|
@@ -378,16 +385,12 @@ function generateFontCSS(fonts = {}) {
|
|
|
378
385
|
}
|
|
379
386
|
}
|
|
380
387
|
|
|
381
|
-
// --- :root font variables ---
|
|
388
|
+
// --- :root font variables (one --font-<name> per role, emitted once) ---
|
|
382
389
|
const fontVars = []
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
fontVars.push(` --font-heading: ${fonts.heading};`)
|
|
388
|
-
}
|
|
389
|
-
if (fonts.mono) {
|
|
390
|
-
fontVars.push(` --font-mono: ${fonts.mono};`)
|
|
390
|
+
for (const [name, role] of Object.entries(fontRoles)) {
|
|
391
|
+
if (role.value) {
|
|
392
|
+
fontVars.push(` --font-${name}: ${role.value};`)
|
|
393
|
+
}
|
|
391
394
|
}
|
|
392
395
|
|
|
393
396
|
if (fontVars.length > 0) {
|
|
@@ -397,26 +400,18 @@ function generateFontCSS(fonts = {}) {
|
|
|
397
400
|
}
|
|
398
401
|
|
|
399
402
|
// --- Apply font families to elements ---
|
|
400
|
-
// Defining --font-* on :root is not enough — something must
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
const userSlots = new Set(fonts._userSlots || [])
|
|
403
|
+
// Defining --font-* on :root is not enough — something must set `font-family`
|
|
404
|
+
// on rendered elements, the same way colors get an application rule
|
|
405
|
+
// ([id^="section-"] { background-color: var(--section) }). The processor has
|
|
406
|
+
// already resolved which roles apply (`role.apply`): a built-in role applies
|
|
407
|
+
// only when the site set it (don't stomp a foundation's own typography with
|
|
408
|
+
// system-ui); a foundation-owned/redefined role applies always via its
|
|
409
|
+
// `applyTo` selectors.
|
|
408
410
|
const applyRules = []
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
// Only the prominent headings — display/heading faces are drawn for large
|
|
414
|
-
// sizes; h4–h6 usually render at or near body size where a display face
|
|
415
|
-
// reads awkwardly, so they stay in the body font.
|
|
416
|
-
applyRules.push('h1, h2, h3 { font-family: var(--font-heading); }')
|
|
417
|
-
}
|
|
418
|
-
if (userSlots.has('mono')) {
|
|
419
|
-
applyRules.push('code, pre, kbd, samp { font-family: var(--font-mono); }')
|
|
411
|
+
for (const [name, role] of Object.entries(fontRoles)) {
|
|
412
|
+
if (!role.apply || !role.applyTo?.length) continue
|
|
413
|
+
const selector = role.applyTo.join(', ')
|
|
414
|
+
applyRules.push(`${selector} { font-family: var(--font-${name}); }`)
|
|
420
415
|
}
|
|
421
416
|
if (applyRules.length > 0) {
|
|
422
417
|
cssLines.push('')
|
|
@@ -494,6 +489,7 @@ export function generateThemeCSS(config = {}) {
|
|
|
494
489
|
colors = DEFAULT_COLORS,
|
|
495
490
|
contexts = {},
|
|
496
491
|
fonts = {},
|
|
492
|
+
fontRoles = {},
|
|
497
493
|
appearance = {},
|
|
498
494
|
foundationVars = {},
|
|
499
495
|
colorVars = {},
|
|
@@ -501,8 +497,11 @@ export function generateThemeCSS(config = {}) {
|
|
|
501
497
|
|
|
502
498
|
const sections = []
|
|
503
499
|
|
|
504
|
-
// 1. Font face rules and variables (links returned separately)
|
|
505
|
-
|
|
500
|
+
// 1. Font face rules and variables (links returned separately). Font roles are
|
|
501
|
+
// resolved by the processor; `fonts` carries only import[] / faces[] here.
|
|
502
|
+
// Font-typed vars are already split out of `foundationVars`, so the generic
|
|
503
|
+
// foundation-var emission below never double-writes a --font-<name>.
|
|
504
|
+
const fontResult = generateFontCSS(fontRoles, fonts)
|
|
506
505
|
if (fontResult.css) {
|
|
507
506
|
sections.push('/* Typography */\n' + fontResult.css)
|
|
508
507
|
}
|
package/src/processor.js
CHANGED
|
@@ -56,7 +56,53 @@ const DEFAULT_APPEARANCE = {
|
|
|
56
56
|
const DEFAULT_FONTS = {
|
|
57
57
|
body: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
58
58
|
heading: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
59
|
-
|
|
59
|
+
code: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Default application selectors for the three built-in font roles. These are
|
|
64
|
+
* *defaults*: a foundation can redefine a role's `applyTo` (or add new roles)
|
|
65
|
+
* via a `type: 'font'` var, so the roles and their selectors are one editable
|
|
66
|
+
* font-var namespace, not a fixed contract. `heading` is scoped to h1–h3 (h4–h6
|
|
67
|
+
* render near body size, where a display face reads awkwardly); `code` (formerly
|
|
68
|
+
* `mono`) owns `--font-code`.
|
|
69
|
+
*/
|
|
70
|
+
const DEFAULT_FONT_ROLES = {
|
|
71
|
+
body: ['body'],
|
|
72
|
+
heading: ['h1', 'h2', 'h3'],
|
|
73
|
+
code: ['code', 'pre', 'kbd', 'samp'],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Foundation `font-*` vars that are CSS longhands, not typefaces to load.
|
|
77
|
+
const NON_FAMILY_FONT_VARS = new Set([
|
|
78
|
+
'font-weight', 'font-style', 'font-size', 'font-stretch',
|
|
79
|
+
'font-feature-settings', 'font-variation-settings', 'font-variant',
|
|
80
|
+
'font-kerning', 'font-optical-sizing', 'font-synthesis',
|
|
81
|
+
'font-size-adjust', 'font-smoothing', 'font-palette', 'font-language-override',
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A foundation var is a themeable typeface (font role) when it declares
|
|
86
|
+
* `type: 'font'`. Deprecated fallback: a `font-*`-prefixed name that isn't a
|
|
87
|
+
* known CSS longhand — kept so foundations predating the convention still work.
|
|
88
|
+
*/
|
|
89
|
+
function isFontVar(name, config) {
|
|
90
|
+
const type = typeof config === 'object' ? config?.type : undefined
|
|
91
|
+
if (type === 'font') return true
|
|
92
|
+
return name.startsWith('font-') && !NON_FAMILY_FONT_VARS.has(name)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A font role's canonical name is bare; `font-X` and `X` are the same role.
|
|
96
|
+
function normalizeFontRoleName(name) {
|
|
97
|
+
return name.startsWith('font-') ? name.slice(5) : name
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Normalize applyTo to an array of trimmed selectors (or null).
|
|
101
|
+
function normalizeApplyTo(applyTo) {
|
|
102
|
+
if (applyTo == null) return null
|
|
103
|
+
const list = Array.isArray(applyTo) ? applyTo : String(applyTo).split(',')
|
|
104
|
+
const cleaned = list.map((s) => String(s).trim()).filter(Boolean)
|
|
105
|
+
return cleaned.length ? cleaned : null
|
|
60
106
|
}
|
|
61
107
|
|
|
62
108
|
/**
|
|
@@ -261,7 +307,10 @@ const CONTEXT_AWARE_TYPES = new Set(['color', 'gradient'])
|
|
|
261
307
|
/**
|
|
262
308
|
* Validate foundation variables configuration
|
|
263
309
|
*
|
|
264
|
-
* Accepts enriched var schema: { default, description, label, type, options, group, globalOnly }
|
|
310
|
+
* Accepts enriched var schema: { default, description, label, type, options, group, globalOnly, applyTo }
|
|
311
|
+
* A `type: 'font'` var is a themeable typeface (family loads, schema tags it as
|
|
312
|
+
* a font); its optional `applyTo` is a list of selectors the framework paints
|
|
313
|
+
* `font-family: var(--<name>)` onto, like a built-in role.
|
|
265
314
|
*
|
|
266
315
|
* @param {Object} vars - Foundation variables
|
|
267
316
|
* @returns {{ valid: boolean, errors: string[] }}
|
|
@@ -478,15 +527,12 @@ export function processTheme(rawConfig = {}, options = {}) {
|
|
|
478
527
|
contexts[name] = { ...defaultContexts[name], ...normalized }
|
|
479
528
|
}
|
|
480
529
|
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
// Used by extractUsedFamilies to avoid loading fonts referenced only in defaults.
|
|
488
|
-
const userFonts = rawConfig.fonts || {}
|
|
489
|
-
fonts._userSlots = ['body', 'heading', 'mono'].filter((s) => userFonts[s] !== undefined)
|
|
530
|
+
// Fonts resolve into the font-role namespace after foundation vars are merged
|
|
531
|
+
// (below), so a foundation's font vars can redefine/extend the roles. `mono`
|
|
532
|
+
// is not a built-in role — the code/monospace role is `code`. It flows through
|
|
533
|
+
// as an ordinary name (a foundation may define a `mono` typeface); a
|
|
534
|
+
// post-resolution check nudges a site that set `fonts.mono` expecting code.
|
|
535
|
+
const rawFonts = { ...(rawConfig.fonts || {}) }
|
|
490
536
|
|
|
491
537
|
// Normalize and process appearance
|
|
492
538
|
const appearance = normalizeAppearance(rawConfig.appearance)
|
|
@@ -504,6 +550,80 @@ export function processTheme(rawConfig = {}, options = {}) {
|
|
|
504
550
|
warnings.push(...foundationValidation.errors)
|
|
505
551
|
}
|
|
506
552
|
|
|
553
|
+
// Resolve the font-role namespace. The three roles are framework *defaults*;
|
|
554
|
+
// a foundation `type: 'font'` var can redefine a role (retarget applyTo /
|
|
555
|
+
// change the default) or add a new role; the site sets any role's value in
|
|
556
|
+
// `fonts:` (or `vars:`). Everything lands in one map so each --font-<name> is
|
|
557
|
+
// emitted once, site-value-wins. Font vars are split OUT of foundationVars so
|
|
558
|
+
// they aren't also emitted by the generic foundation-var path (which would
|
|
559
|
+
// double-write --font-<name>, the later foundation default clobbering the site).
|
|
560
|
+
const fontRoles = {}
|
|
561
|
+
for (const [name, applyTo] of Object.entries(DEFAULT_FONT_ROLES)) {
|
|
562
|
+
fontRoles[name] = {
|
|
563
|
+
value: DEFAULT_FONTS[name],
|
|
564
|
+
applyTo: [...applyTo],
|
|
565
|
+
foundationOwned: false,
|
|
566
|
+
userSet: false,
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const nonFontVars = {}
|
|
571
|
+
for (const [rawName, cfg] of Object.entries(mergedFoundationVars)) {
|
|
572
|
+
if (!isFontVar(rawName, cfg)) {
|
|
573
|
+
nonFontVars[rawName] = cfg
|
|
574
|
+
continue
|
|
575
|
+
}
|
|
576
|
+
const name = normalizeFontRoleName(rawName)
|
|
577
|
+
const cfgObj = typeof cfg === 'object' ? cfg : { default: cfg }
|
|
578
|
+
const existing = fontRoles[name]
|
|
579
|
+
fontRoles[name] = {
|
|
580
|
+
value: cfgObj.default ?? existing?.value,
|
|
581
|
+
applyTo:
|
|
582
|
+
cfgObj.applyTo !== undefined
|
|
583
|
+
? normalizeApplyTo(cfgObj.applyTo)
|
|
584
|
+
: existing?.applyTo ?? null,
|
|
585
|
+
foundationOwned: true,
|
|
586
|
+
userSet: false,
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const FONT_LOADING_KEYS = new Set(['import', 'faces'])
|
|
591
|
+
for (const [rawName, value] of Object.entries(rawFonts)) {
|
|
592
|
+
if (FONT_LOADING_KEYS.has(rawName)) continue
|
|
593
|
+
const name = normalizeFontRoleName(rawName)
|
|
594
|
+
const existing = fontRoles[name]
|
|
595
|
+
fontRoles[name] = {
|
|
596
|
+
value,
|
|
597
|
+
applyTo: existing?.applyTo ?? null,
|
|
598
|
+
foundationOwned: existing?.foundationOwned ?? false,
|
|
599
|
+
userSet: true,
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Per-role application + loading flags. apply: emit the font-family rule when
|
|
604
|
+
// the role has selectors AND is "intended" (site-set or foundation-owned) —
|
|
605
|
+
// a bare framework default is never forced onto a foundation's own typography.
|
|
606
|
+
// load: pull the family through the @font-face / import filter.
|
|
607
|
+
for (const role of Object.values(fontRoles)) {
|
|
608
|
+
const intended = role.foundationOwned || role.userSet
|
|
609
|
+
role.apply = intended && Array.isArray(role.applyTo) && role.applyTo.length > 0
|
|
610
|
+
role.load = intended
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// `mono` is no longer aliased to `code`. Nudge a site that set `fonts.mono`
|
|
614
|
+
// expecting the code font, unless the foundation actually defines a `mono` role.
|
|
615
|
+
if (rawFonts.mono !== undefined && !fontRoles.mono?.foundationOwned) {
|
|
616
|
+
warnings.push('theme.yml `fonts.mono` is not a built-in font role — the code/monospace role is `code`. Rename `fonts.mono` → `fonts.code`.')
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// config.fonts: name→value (for the Theme API) plus import/faces (loading).
|
|
620
|
+
const fonts = {}
|
|
621
|
+
if (rawFonts.import !== undefined) fonts.import = rawFonts.import
|
|
622
|
+
if (rawFonts.faces !== undefined) fonts.faces = rawFonts.faces
|
|
623
|
+
for (const [name, role] of Object.entries(fontRoles)) {
|
|
624
|
+
fonts[name] = role.value
|
|
625
|
+
}
|
|
626
|
+
|
|
507
627
|
// Process code block theme
|
|
508
628
|
// These values are stored for runtime injection by kit's Code component
|
|
509
629
|
// (not converted to CSS here - enables tree-shaking when code blocks aren't used)
|
|
@@ -523,9 +643,10 @@ export function processTheme(rawConfig = {}, options = {}) {
|
|
|
523
643
|
colors, // Raw colors for CSS generator
|
|
524
644
|
palettes, // Generated palettes for Theme class
|
|
525
645
|
contexts,
|
|
526
|
-
fonts,
|
|
646
|
+
fonts, // name→value (+ import/faces) for the Theme API and loading
|
|
647
|
+
fontRoles, // resolved font-role map (value/applyTo/apply/load) for CSS gen
|
|
527
648
|
appearance,
|
|
528
|
-
foundationVars:
|
|
649
|
+
foundationVars: nonFontVars, // NON-font vars only (font vars live in fontRoles)
|
|
529
650
|
colorVars, // Context-aware vars: { light: {...}, dark: {...} }
|
|
530
651
|
code, // Code block theme for runtime injection
|
|
531
652
|
background, // Site-level background CSS value
|