@uniweb/theming 0.1.6 → 0.1.8
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 +40 -67
- package/src/index.js +1 -0
- package/src/processor.js +135 -13
package/package.json
CHANGED
package/src/css-generator.js
CHANGED
|
@@ -210,54 +210,36 @@ const GENERIC_FAMILIES = new Set([
|
|
|
210
210
|
'inherit', 'initial', 'revert', 'unset',
|
|
211
211
|
])
|
|
212
212
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
'font-weight', 'font-style', 'font-size', 'font-stretch',
|
|
217
|
-
'font-feature-settings', 'font-variation-settings', 'font-variant',
|
|
218
|
-
'font-kerning', 'font-optical-sizing', 'font-synthesis',
|
|
219
|
-
'font-size-adjust', 'font-smoothing', 'font-palette', 'font-language-override',
|
|
220
|
-
])
|
|
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.
|
|
221
216
|
|
|
222
217
|
/**
|
|
223
|
-
* Extract the font family names
|
|
218
|
+
* Extract the font family names to load, from a resolved font-role map.
|
|
224
219
|
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* 2. Foundation `font-*` vars (e.g. an editorial `font-serif`). A foundation
|
|
230
|
-
* can declare a themeable typeface beyond the three built-in roles; the
|
|
231
|
-
* site sets its value and loads it with `fonts.import` / `fonts.faces`.
|
|
232
|
-
* Without counting these, such a family is filtered out as "unused" and
|
|
233
|
-
* never loads — capping a foundation at the three role slots.
|
|
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.
|
|
234
224
|
*
|
|
235
|
-
* @param {Object}
|
|
236
|
-
* @
|
|
237
|
-
* @returns {Set<string>} Lowercase family names referenced by the site
|
|
225
|
+
* @param {Object} fontRoles - Resolved roles: name → { value, applyTo, apply, load }
|
|
226
|
+
* @returns {Set<string>} Lowercase family names to load (@font-face / import filter)
|
|
238
227
|
*/
|
|
239
|
-
function extractUsedFamilies(
|
|
228
|
+
function extractUsedFamilies(fontRoles = {}) {
|
|
240
229
|
const used = new Set()
|
|
241
230
|
|
|
242
231
|
const addFamiliesFrom = (value) => {
|
|
243
232
|
if (typeof value !== 'string') return
|
|
244
233
|
for (const segment of value.split(',')) {
|
|
245
234
|
const name = segment.trim().replace(/^["']|["']$/g, '').toLowerCase()
|
|
246
|
-
// Skip generics, empties, and numeric/length tokens
|
|
247
|
-
// size "1rem" a non-family font-* var might carry).
|
|
235
|
+
// Skip generics, empties, and numeric/length tokens.
|
|
248
236
|
if (!name || GENERIC_FAMILIES.has(name) || /^\d/.test(name)) continue
|
|
249
237
|
used.add(name)
|
|
250
238
|
}
|
|
251
239
|
}
|
|
252
240
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
addFamiliesFrom(fonts[slot])
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
for (const [name, config] of Object.entries(foundationVars)) {
|
|
259
|
-
if (!name.startsWith('font-') || NON_FAMILY_FONT_VARS.has(name)) continue
|
|
260
|
-
addFamiliesFrom(typeof config === 'object' ? config?.default : config)
|
|
241
|
+
for (const role of Object.values(fontRoles)) {
|
|
242
|
+
if (role && role.load) addFamiliesFrom(role.value)
|
|
261
243
|
}
|
|
262
244
|
|
|
263
245
|
return used
|
|
@@ -309,14 +291,15 @@ function isGoogleFontsUrl(url) {
|
|
|
309
291
|
/**
|
|
310
292
|
* Generate font CSS and external link tags.
|
|
311
293
|
*
|
|
312
|
-
* @param {Object}
|
|
294
|
+
* @param {Object} fontRoles - Resolved font roles (name → { value, applyTo, apply, load })
|
|
295
|
+
* @param {Object} fonts - Font loading config (import[] / faces[])
|
|
313
296
|
* @returns {{ css: string, links: string }} css for <style>, links for <head>
|
|
314
297
|
*/
|
|
315
|
-
function generateFontCSS(
|
|
298
|
+
function generateFontCSS(fontRoles = {}, fonts = {}) {
|
|
316
299
|
const cssLines = []
|
|
317
300
|
const linkTags = []
|
|
318
301
|
|
|
319
|
-
const usedFamilies = extractUsedFamilies(
|
|
302
|
+
const usedFamilies = extractUsedFamilies(fontRoles)
|
|
320
303
|
|
|
321
304
|
// --- @font-face rules from faces[] (filtered to used families) ---
|
|
322
305
|
// Also generate <link rel="preload"> hints so browsers fetch fonts early
|
|
@@ -402,16 +385,12 @@ function generateFontCSS(fonts = {}, foundationVars = {}) {
|
|
|
402
385
|
}
|
|
403
386
|
}
|
|
404
387
|
|
|
405
|
-
// --- :root font variables ---
|
|
388
|
+
// --- :root font variables (one --font-<name> per role, emitted once) ---
|
|
406
389
|
const fontVars = []
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
fontVars.push(` --font-heading: ${fonts.heading};`)
|
|
412
|
-
}
|
|
413
|
-
if (fonts.mono) {
|
|
414
|
-
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
|
+
}
|
|
415
394
|
}
|
|
416
395
|
|
|
417
396
|
if (fontVars.length > 0) {
|
|
@@ -421,26 +400,18 @@ function generateFontCSS(fonts = {}, foundationVars = {}) {
|
|
|
421
400
|
}
|
|
422
401
|
|
|
423
402
|
// --- Apply font families to elements ---
|
|
424
|
-
// Defining --font-* on :root is not enough — something must
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
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.
|
|
432
410
|
const applyRules = []
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// Only the prominent headings — display/heading faces are drawn for large
|
|
438
|
-
// sizes; h4–h6 usually render at or near body size where a display face
|
|
439
|
-
// reads awkwardly, so they stay in the body font.
|
|
440
|
-
applyRules.push('h1, h2, h3 { font-family: var(--font-heading); }')
|
|
441
|
-
}
|
|
442
|
-
if (userSlots.has('mono')) {
|
|
443
|
-
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}); }`)
|
|
444
415
|
}
|
|
445
416
|
if (applyRules.length > 0) {
|
|
446
417
|
cssLines.push('')
|
|
@@ -518,6 +489,7 @@ export function generateThemeCSS(config = {}) {
|
|
|
518
489
|
colors = DEFAULT_COLORS,
|
|
519
490
|
contexts = {},
|
|
520
491
|
fonts = {},
|
|
492
|
+
fontRoles = {},
|
|
521
493
|
appearance = {},
|
|
522
494
|
foundationVars = {},
|
|
523
495
|
colorVars = {},
|
|
@@ -525,10 +497,11 @@ export function generateThemeCSS(config = {}) {
|
|
|
525
497
|
|
|
526
498
|
const sections = []
|
|
527
499
|
|
|
528
|
-
// 1. Font face rules and variables (links returned separately)
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
|
|
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)
|
|
532
505
|
if (fontResult.css) {
|
|
533
506
|
sections.push('/* Typography */\n' + fontResult.css)
|
|
534
507
|
}
|
package/src/index.js
CHANGED
package/src/processor.js
CHANGED
|
@@ -56,7 +56,54 @@ 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'`, or — the inferred case — its name is `font-*` and not a known
|
|
87
|
+
* CSS longhand. Exported so the build can persist the inferred `type: 'font'`
|
|
88
|
+
* into schema.json (the runtime relies on the same predicate).
|
|
89
|
+
*/
|
|
90
|
+
export function isFontVar(name, config) {
|
|
91
|
+
const type = typeof config === 'object' ? config?.type : undefined
|
|
92
|
+
if (type === 'font') return true
|
|
93
|
+
return name.startsWith('font-') && !NON_FAMILY_FONT_VARS.has(name)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A font role's canonical name is bare; `font-X` and `X` are the same role.
|
|
97
|
+
function normalizeFontRoleName(name) {
|
|
98
|
+
return name.startsWith('font-') ? name.slice(5) : name
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Normalize applyTo to an array of trimmed selectors (or null).
|
|
102
|
+
function normalizeApplyTo(applyTo) {
|
|
103
|
+
if (applyTo == null) return null
|
|
104
|
+
const list = Array.isArray(applyTo) ? applyTo : String(applyTo).split(',')
|
|
105
|
+
const cleaned = list.map((s) => String(s).trim()).filter(Boolean)
|
|
106
|
+
return cleaned.length ? cleaned : null
|
|
60
107
|
}
|
|
61
108
|
|
|
62
109
|
/**
|
|
@@ -261,7 +308,10 @@ const CONTEXT_AWARE_TYPES = new Set(['color', 'gradient'])
|
|
|
261
308
|
/**
|
|
262
309
|
* Validate foundation variables configuration
|
|
263
310
|
*
|
|
264
|
-
* Accepts enriched var schema: { default, description, label, type, options, group, globalOnly }
|
|
311
|
+
* Accepts enriched var schema: { default, description, label, type, options, group, globalOnly, applyTo }
|
|
312
|
+
* A `type: 'font'` var is a themeable typeface (family loads, schema tags it as
|
|
313
|
+
* a font); its optional `applyTo` is a list of selectors the framework paints
|
|
314
|
+
* `font-family: var(--<name>)` onto, like a built-in role.
|
|
265
315
|
*
|
|
266
316
|
* @param {Object} vars - Foundation variables
|
|
267
317
|
* @returns {{ valid: boolean, errors: string[] }}
|
|
@@ -478,15 +528,12 @@ export function processTheme(rawConfig = {}, options = {}) {
|
|
|
478
528
|
contexts[name] = { ...defaultContexts[name], ...normalized }
|
|
479
529
|
}
|
|
480
530
|
|
|
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)
|
|
531
|
+
// Fonts resolve into the font-role namespace after foundation vars are merged
|
|
532
|
+
// (below), so a foundation's font vars can redefine/extend the roles. `mono`
|
|
533
|
+
// is not a built-in role — the code/monospace role is `code`. It flows through
|
|
534
|
+
// as an ordinary name (a foundation may define a `mono` typeface); a
|
|
535
|
+
// post-resolution check nudges a site that set `fonts.mono` expecting code.
|
|
536
|
+
const rawFonts = { ...(rawConfig.fonts || {}) }
|
|
490
537
|
|
|
491
538
|
// Normalize and process appearance
|
|
492
539
|
const appearance = normalizeAppearance(rawConfig.appearance)
|
|
@@ -504,6 +551,80 @@ export function processTheme(rawConfig = {}, options = {}) {
|
|
|
504
551
|
warnings.push(...foundationValidation.errors)
|
|
505
552
|
}
|
|
506
553
|
|
|
554
|
+
// Resolve the font-role namespace. The three roles are framework *defaults*;
|
|
555
|
+
// a foundation `type: 'font'` var can redefine a role (retarget applyTo /
|
|
556
|
+
// change the default) or add a new role; the site sets any role's value in
|
|
557
|
+
// `fonts:` (or `vars:`). Everything lands in one map so each --font-<name> is
|
|
558
|
+
// emitted once, site-value-wins. Font vars are split OUT of foundationVars so
|
|
559
|
+
// they aren't also emitted by the generic foundation-var path (which would
|
|
560
|
+
// double-write --font-<name>, the later foundation default clobbering the site).
|
|
561
|
+
const fontRoles = {}
|
|
562
|
+
for (const [name, applyTo] of Object.entries(DEFAULT_FONT_ROLES)) {
|
|
563
|
+
fontRoles[name] = {
|
|
564
|
+
value: DEFAULT_FONTS[name],
|
|
565
|
+
applyTo: [...applyTo],
|
|
566
|
+
foundationOwned: false,
|
|
567
|
+
userSet: false,
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const nonFontVars = {}
|
|
572
|
+
for (const [rawName, cfg] of Object.entries(mergedFoundationVars)) {
|
|
573
|
+
if (!isFontVar(rawName, cfg)) {
|
|
574
|
+
nonFontVars[rawName] = cfg
|
|
575
|
+
continue
|
|
576
|
+
}
|
|
577
|
+
const name = normalizeFontRoleName(rawName)
|
|
578
|
+
const cfgObj = typeof cfg === 'object' ? cfg : { default: cfg }
|
|
579
|
+
const existing = fontRoles[name]
|
|
580
|
+
fontRoles[name] = {
|
|
581
|
+
value: cfgObj.default ?? existing?.value,
|
|
582
|
+
applyTo:
|
|
583
|
+
cfgObj.applyTo !== undefined
|
|
584
|
+
? normalizeApplyTo(cfgObj.applyTo)
|
|
585
|
+
: existing?.applyTo ?? null,
|
|
586
|
+
foundationOwned: true,
|
|
587
|
+
userSet: false,
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const FONT_LOADING_KEYS = new Set(['import', 'faces'])
|
|
592
|
+
for (const [rawName, value] of Object.entries(rawFonts)) {
|
|
593
|
+
if (FONT_LOADING_KEYS.has(rawName)) continue
|
|
594
|
+
const name = normalizeFontRoleName(rawName)
|
|
595
|
+
const existing = fontRoles[name]
|
|
596
|
+
fontRoles[name] = {
|
|
597
|
+
value,
|
|
598
|
+
applyTo: existing?.applyTo ?? null,
|
|
599
|
+
foundationOwned: existing?.foundationOwned ?? false,
|
|
600
|
+
userSet: true,
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Per-role application + loading flags. apply: emit the font-family rule when
|
|
605
|
+
// the role has selectors AND is "intended" (site-set or foundation-owned) —
|
|
606
|
+
// a bare framework default is never forced onto a foundation's own typography.
|
|
607
|
+
// load: pull the family through the @font-face / import filter.
|
|
608
|
+
for (const role of Object.values(fontRoles)) {
|
|
609
|
+
const intended = role.foundationOwned || role.userSet
|
|
610
|
+
role.apply = intended && Array.isArray(role.applyTo) && role.applyTo.length > 0
|
|
611
|
+
role.load = intended
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// `mono` is no longer aliased to `code`. Nudge a site that set `fonts.mono`
|
|
615
|
+
// expecting the code font, unless the foundation actually defines a `mono` role.
|
|
616
|
+
if (rawFonts.mono !== undefined && !fontRoles.mono?.foundationOwned) {
|
|
617
|
+
warnings.push('theme.yml `fonts.mono` is not a built-in font role — the code/monospace role is `code`. Rename `fonts.mono` → `fonts.code`.')
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// config.fonts: name→value (for the Theme API) plus import/faces (loading).
|
|
621
|
+
const fonts = {}
|
|
622
|
+
if (rawFonts.import !== undefined) fonts.import = rawFonts.import
|
|
623
|
+
if (rawFonts.faces !== undefined) fonts.faces = rawFonts.faces
|
|
624
|
+
for (const [name, role] of Object.entries(fontRoles)) {
|
|
625
|
+
fonts[name] = role.value
|
|
626
|
+
}
|
|
627
|
+
|
|
507
628
|
// Process code block theme
|
|
508
629
|
// These values are stored for runtime injection by kit's Code component
|
|
509
630
|
// (not converted to CSS here - enables tree-shaking when code blocks aren't used)
|
|
@@ -523,9 +644,10 @@ export function processTheme(rawConfig = {}, options = {}) {
|
|
|
523
644
|
colors, // Raw colors for CSS generator
|
|
524
645
|
palettes, // Generated palettes for Theme class
|
|
525
646
|
contexts,
|
|
526
|
-
fonts,
|
|
647
|
+
fonts, // name→value (+ import/faces) for the Theme API and loading
|
|
648
|
+
fontRoles, // resolved font-role map (value/applyTo/apply/load) for CSS gen
|
|
527
649
|
appearance,
|
|
528
|
-
foundationVars:
|
|
650
|
+
foundationVars: nonFontVars, // NON-font vars only (font vars live in fontRoles)
|
|
529
651
|
colorVars, // Context-aware vars: { light: {...}, dark: {...} }
|
|
530
652
|
code, // Code block theme for runtime injection
|
|
531
653
|
background, // Site-level background CSS value
|