@uniweb/theming 0.1.4 → 0.1.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/theming",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Theming utilities for the Uniweb platform — shade generation, CSS generation, theme processing",
5
5
  "type": "module",
6
6
  "exports": {
@@ -210,32 +210,56 @@ const GENERIC_FAMILIES = new Set([
210
210
  'inherit', 'initial', 'revert', 'unset',
211
211
  ])
212
212
 
213
+ // Foundation `font-*` vars that are CSS longhands, not typefaces — never
214
+ // treat their value as a family to load.
215
+ const NON_FAMILY_FONT_VARS = new Set([
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
+ ])
221
+
213
222
  /**
214
- * Extract font family names actually used by body/heading/mono slots.
223
+ * Extract the font family names the site actually references.
215
224
  *
216
- * Only considers slots the user explicitly configured (tracked via _userSlots
217
- * from the processor). Default system font stacks are ignored so they don't
218
- * trigger unnecessary font loading.
225
+ * Two sources count as "used":
226
+ * 1. The `body`/`heading`/`mono` slots the user explicitly configured
227
+ * (tracked via _userSlots from the processor). Default system stacks are
228
+ * ignored so they don't trigger unnecessary font loading.
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.
219
234
  *
220
- * @param {Object} fonts - Font configuration
221
- * @returns {Set<string>} Lowercase family names referenced by user-set slots
235
+ * @param {Object} fonts - Font configuration (may carry _userSlots)
236
+ * @param {Object} [foundationVars] - Resolved foundation vars (name → { default } | value)
237
+ * @returns {Set<string>} Lowercase family names referenced by the site
222
238
  */
223
- function extractUsedFamilies(fonts) {
239
+ function extractUsedFamilies(fonts, foundationVars = {}) {
224
240
  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
241
 
242
+ const addFamiliesFrom = (value) => {
243
+ if (typeof value !== 'string') return
231
244
  for (const segment of value.split(',')) {
232
245
  const name = segment.trim().replace(/^["']|["']$/g, '').toLowerCase()
233
- if (name && !GENERIC_FAMILIES.has(name)) {
234
- used.add(name)
235
- }
246
+ // Skip generics, empties, and numeric/length tokens (a weight "600" or
247
+ // size "1rem" a non-family font-* var might carry).
248
+ if (!name || GENERIC_FAMILIES.has(name) || /^\d/.test(name)) continue
249
+ used.add(name)
236
250
  }
237
251
  }
238
252
 
253
+ const slots = fonts._userSlots || ['body', 'heading', 'mono']
254
+ for (const slot of slots) {
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)
261
+ }
262
+
239
263
  return used
240
264
  }
241
265
 
@@ -288,11 +312,11 @@ function isGoogleFontsUrl(url) {
288
312
  * @param {Object} fonts - Font configuration
289
313
  * @returns {{ css: string, links: string }} css for <style>, links for <head>
290
314
  */
291
- function generateFontCSS(fonts = {}) {
315
+ function generateFontCSS(fonts = {}, foundationVars = {}) {
292
316
  const cssLines = []
293
317
  const linkTags = []
294
318
 
295
- const usedFamilies = extractUsedFamilies(fonts)
319
+ const usedFamilies = extractUsedFamilies(fonts, foundationVars)
296
320
 
297
321
  // --- @font-face rules from faces[] (filtered to used families) ---
298
322
  // Also generate <link rel="preload"> hints so browsers fetch fonts early
@@ -358,16 +382,22 @@ function generateFontCSS(fonts = {}) {
358
382
  }
359
383
 
360
384
  if (allFamilies.length > 0) {
361
- const merged = new URL('https://fonts.googleapis.com/css2')
362
- for (const f of allFamilies) {
363
- merged.searchParams.append('family', f)
364
- }
365
- merged.searchParams.set('display', display)
385
+ // Build the css2 URL by hand. Google's css2 axis syntax uses ':' ',' ';'
386
+ // '@' as LITERAL separators and '+' for spaces. URLSearchParams.toString()
387
+ // percent-encodes all of them (%3A/%2C/%3B/%40), producing a URL that
388
+ // parses but is non-canonical — it only loads because Google's server
389
+ // decodes it leniently. A strict Google Fonts mirror/proxy or a future API
390
+ // change could reject it. Emit the canonical literal form instead.
391
+ // (getAll() already decoded '+'/'%20' to spaces, so restore spaces to '+'.)
392
+ const familyQuery = allFamilies
393
+ .map((f) => `family=${f.trim().replace(/\s+/g, '+')}`)
394
+ .join('&')
395
+ const href = `https://fonts.googleapis.com/css2?${familyQuery}&display=${display}`
366
396
 
367
397
  linkTags.unshift(
368
398
  `<link rel="preconnect" href="https://fonts.googleapis.com">`,
369
399
  `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`,
370
- `<link rel="stylesheet" href="${merged.toString()}">`,
400
+ `<link rel="stylesheet" href="${href}">`,
371
401
  )
372
402
  }
373
403
  }
@@ -390,6 +420,33 @@ function generateFontCSS(fonts = {}) {
390
420
  cssLines.push('}')
391
421
  }
392
422
 
423
+ // --- Apply font families to elements ---
424
+ // Defining --font-* on :root is not enough — something must actually set
425
+ // `font-family` on rendered elements, the same way colors get an application
426
+ // rule ([id^="section-"] { background-color: var(--section) }). Without this
427
+ // the font vars are orphaned and text never picks them up, even though the
428
+ // vars are set and the @import/@font-face load. Only apply slots the SITE
429
+ // explicitly set (fonts._userSlots) so a foundation's own default typography
430
+ // is left untouched when theme.yml doesn't override that slot.
431
+ const userSlots = new Set(fonts._userSlots || [])
432
+ const applyRules = []
433
+ if (userSlots.has('body')) {
434
+ applyRules.push('body { font-family: var(--font-body); }')
435
+ }
436
+ if (userSlots.has('heading')) {
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); }')
444
+ }
445
+ if (applyRules.length > 0) {
446
+ cssLines.push('')
447
+ cssLines.push(...applyRules)
448
+ }
449
+
393
450
  return {
394
451
  css: cssLines.join('\n'),
395
452
  links: linkTags.join('\n'),
@@ -469,7 +526,9 @@ export function generateThemeCSS(config = {}) {
469
526
  const sections = []
470
527
 
471
528
  // 1. Font face rules and variables (links returned separately)
472
- const fontResult = generateFontCSS(fonts)
529
+ // foundationVars is passed so families referenced by a foundation `font-*`
530
+ // var (e.g. an editorial `font-serif`) survive the used-family filter.
531
+ const fontResult = generateFontCSS(fonts, foundationVars)
473
532
  if (fontResult.css) {
474
533
  sections.push('/* Typography */\n' + fontResult.css)
475
534
  }