@uniweb/theming 0.1.8 → 0.1.10
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 +53 -5
- package/src/index.js +3 -1
package/package.json
CHANGED
package/src/css-generator.js
CHANGED
|
@@ -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('${
|
|
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="${
|
|
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
|
}
|
|
@@ -533,6 +573,14 @@ export function generateThemeCSS(config = {}) {
|
|
|
533
573
|
}
|
|
534
574
|
|
|
535
575
|
// 6. Dark scheme support (if enabled, default is dark, or follows system preference)
|
|
576
|
+
//
|
|
577
|
+
// This guard decides whether the `.scheme-dark` rules physically exist. It is
|
|
578
|
+
// the CANONICAL "site reaches dark" predicate; the model-side copy that the
|
|
579
|
+
// runtime boot resolver and kit's useAppearance consume is hasDarkScheme() in
|
|
580
|
+
// @uniweb/core (theme.js). These MUST agree — a scheme applied with no
|
|
581
|
+
// matching CSS renders as a light page claiming to be dark. @uniweb/theming
|
|
582
|
+
// has no @uniweb/core dependency (it also runs server-side in the platform),
|
|
583
|
+
// so the predicate is duplicated here by necessity; keep the two in lockstep.
|
|
536
584
|
if (appearance.allowToggle || appearance.default === 'dark' || appearance.default === 'system' || appearance.schemes?.includes('dark')) {
|
|
537
585
|
sections.push(generateDarkSchemeCSS(appearance, contexts.dark, colorVars))
|
|
538
586
|
}
|
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,
|