@uniweb/theming 0.1.6 → 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 +40 -67
- package/src/processor.js +134 -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/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
|