@uniweb/theming 0.1.8 → 0.1.9

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.8",
3
+ "version": "0.1.9",
4
4
  "description": "Theming utilities for the Uniweb platform — shade generation, CSS generation, theme processing",
5
5
  "type": "module",
6
6
  "exports": {
@@ -277,6 +277,27 @@ function filterGoogleFontsUrl(url, usedFamilies) {
277
277
  }
278
278
  }
279
279
 
280
+ /**
281
+ * Prefix a root-relative asset URL with the site's base path.
282
+ *
283
+ * Self-hosted font `src` values are authored root-relative (`/fonts/x.woff2`)
284
+ * because that is what they are relative to the site. Under a subdirectory
285
+ * deployment (GitHub Pages project sites, `/docs/`, ...) the served root moves,
286
+ * so the emitted URL has to carry the base. A relative URL is not an option:
287
+ * the rules live in an inline `<style>`, so they would resolve against the
288
+ * *document* URL, which differs per route.
289
+ *
290
+ * Left untouched: absolute URLs (http:, //, data:), already-based URLs, and
291
+ * document-relative ones (`./`, `../`) whose author clearly meant them.
292
+ */
293
+ function withBase(url, base) {
294
+ if (!url || !base || base === '/') return url
295
+ if (!url.startsWith('/') || url.startsWith('//')) return url
296
+ const prefix = base.endsWith('/') ? base.slice(0, -1) : base
297
+ if (url === prefix || url.startsWith(prefix + '/')) return url // already based
298
+ return prefix + url
299
+ }
300
+
280
301
  /**
281
302
  * Check whether a URL points to Google Fonts.
282
303
  */
@@ -293,9 +314,10 @@ function isGoogleFontsUrl(url) {
293
314
  *
294
315
  * @param {Object} fontRoles - Resolved font roles (name → { value, applyTo, apply, load })
295
316
  * @param {Object} fonts - Font loading config (import[] / faces[])
317
+ * @param {string} [base='/'] - Site base path, prefixed onto root-relative face srcs
296
318
  * @returns {{ css: string, links: string }} css for <style>, links for <head>
297
319
  */
298
- function generateFontCSS(fontRoles = {}, fonts = {}) {
320
+ function generateFontCSS(fontRoles = {}, fonts = {}, base = '/') {
299
321
  const cssLines = []
300
322
  const linkTags = []
301
323
 
@@ -311,10 +333,11 @@ function generateFontCSS(fontRoles = {}, fonts = {}) {
311
333
  if (!usedFamilies.has(face.family.toLowerCase())) continue
312
334
 
313
335
  const format = face.format || (face.src.endsWith('.woff2') ? 'woff2' : 'woff')
336
+ const src = withBase(face.src, base)
314
337
  cssLines.push(
315
338
  `@font-face {`,
316
339
  ` font-family: ${face.family};`,
317
- ` src: url('${face.src}') format('${format}');`,
340
+ ` src: url('${src}') format('${format}');`,
318
341
  ` font-weight: ${face.weight || 400};`,
319
342
  ` font-style: ${face.style || 'normal'};`,
320
343
  ` font-display: swap;`,
@@ -323,7 +346,7 @@ function generateFontCSS(fontRoles = {}, fonts = {}) {
323
346
 
324
347
  const mimeType = PRELOAD_TYPE[format]
325
348
  if (mimeType) {
326
- linkTags.push(`<link rel="preload" href="${face.src}" as="font" type="${mimeType}" crossorigin>`)
349
+ linkTags.push(`<link rel="preload" href="${src}" as="font" type="${mimeType}" crossorigin>`)
327
350
  }
328
351
  }
329
352
  if (cssLines.length > 0) {
@@ -333,6 +356,7 @@ function generateFontCSS(fontRoles = {}, fonts = {}) {
333
356
 
334
357
  // --- External imports → <link> tags (filtered to used families) ---
335
358
  const googleUrls = []
359
+ const otherOrigins = new Set()
336
360
 
337
361
  if (fonts.import && Array.isArray(fonts.import)) {
338
362
  for (const entry of fonts.import) {
@@ -343,10 +367,23 @@ function generateFontCSS(fontRoles = {}, fonts = {}) {
343
367
  if (filtered) googleUrls.push(filtered)
344
368
  } else {
345
369
  linkTags.push(`<link rel="stylesheet" href="${entry.url}">`)
370
+ try {
371
+ otherOrigins.add(new URL(entry.url).origin)
372
+ } catch {
373
+ // relative or malformed — no origin to preconnect to
374
+ }
346
375
  }
347
376
  }
348
377
  }
349
378
 
379
+ // Preconnect to non-Google font hosts so the DNS/TLS handshake overlaps the
380
+ // stylesheet request instead of following it. Google's own preconnects are
381
+ // emitted below, alongside the merged css2 link. Emitting every font <link>
382
+ // from one place keeps consumers from having to re-derive any of them.
383
+ for (const origin of otherOrigins) {
384
+ linkTags.unshift(`<link rel="preconnect" href="${origin}">`)
385
+ }
386
+
350
387
  // Merge Google Fonts into a single <link> with preconnect
351
388
  if (googleUrls.length > 0) {
352
389
  // Collect all family params from all URLs into one
@@ -482,9 +519,12 @@ export function generateFoundationVars(vars = {}) {
482
519
  * @param {Object} config.fonts - Font configuration
483
520
  * @param {Object} config.appearance - Appearance settings (dark mode, etc.)
484
521
  * @param {Object} config.foundationVars - Foundation-specific variables
522
+ * @param {Object} [options] - Generation options
523
+ * @param {string} [options.base='/'] - Site base path for subdirectory deployments
485
524
  * @returns {{ css: string, links: string }} CSS string and font link tags
486
525
  */
487
- export function generateThemeCSS(config = {}) {
526
+ export function generateThemeCSS(config = {}, options = {}) {
527
+ const { base = '/' } = options
488
528
  const {
489
529
  colors = DEFAULT_COLORS,
490
530
  contexts = {},
@@ -501,7 +541,7 @@ export function generateThemeCSS(config = {}) {
501
541
  // resolved by the processor; `fonts` carries only import[] / faces[] here.
502
542
  // Font-typed vars are already split out of `foundationVars`, so the generic
503
543
  // foundation-var emission below never double-writes a --font-<name>.
504
- const fontResult = generateFontCSS(fontRoles, fonts)
544
+ const fontResult = generateFontCSS(fontRoles, fonts, base)
505
545
  if (fontResult.css) {
506
546
  sections.push('/* Typography */\n' + fontResult.css)
507
547
  }
package/src/index.js CHANGED
@@ -53,11 +53,13 @@ import { generateThemeCSS } from './css-generator.js'
53
53
  * @param {Object} themeYml - Raw theme.yml content
54
54
  * @param {Object} options - Processing options
55
55
  * @param {Object} options.foundationVars - Foundation variables
56
+ * @param {string} [options.base='/'] - Site base path, prefixed onto root-relative
57
+ * font `src` URLs so self-hosted fonts resolve under subdirectory deployments
56
58
  * @returns {{ css: string, links: string, config: Object, errors: string[], warnings: string[] }}
57
59
  */
58
60
  export function buildTheme(themeYml = {}, options = {}) {
59
61
  const { config, errors, warnings } = processTheme(themeYml, options)
60
- const { css, links } = generateThemeCSS(config)
62
+ const { css, links } = generateThemeCSS(config, { base: options.base })
61
63
 
62
64
  return {
63
65
  css,