@glossarist/concept-browser 0.7.107 → 0.7.109
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/README.md +107 -9
- package/cli/index.mjs +33 -4
- package/cli/index.ts +38 -10
- package/package.json +1 -1
- package/src/__tests__/utils/favicon-base.test.ts +89 -0
- package/src/__tests__/utils/favicon-links.test.ts +185 -0
- package/src/__tests__/utils/font-stack.test.ts +105 -0
- package/src/components/ConceptDetail.vue +1 -1
- package/src/config/types.ts +44 -7
- package/src/config/use-site-config.ts +22 -6
- package/src/layouts/Default.astro +94 -50
- package/src/styles/tailwind.css +5 -0
- package/src/utils/favicon-base.ts +45 -0
- package/src/utils/favicon-links.ts +128 -0
- package/src/utils/font-stack.ts +77 -0
- package/src/views/DatasetView.vue +1 -1
- package/src/views/GroupView.vue +1 -1
- package/src/views/HomeView.vue +1 -1
- package/tailwind.config.js +3 -1
package/src/config/types.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
// === Branding ===
|
|
2
2
|
|
|
3
|
+
export type FontCategory = 'serif' | 'sans-serif' | 'monospace';
|
|
4
|
+
|
|
3
5
|
export interface FontConfig {
|
|
4
6
|
family: string;
|
|
5
7
|
source: 'google' | 'url' | 'local';
|
|
6
8
|
weights?: number[];
|
|
7
9
|
url?: string;
|
|
10
|
+
/** Font category — controls the fallback chain when the primary family
|
|
11
|
+
* fails to load. Defaults preserve prior behavior: `'serif'` for header,
|
|
12
|
+
* `'sans-serif'` for body, `'monospace'` for mono.
|
|
13
|
+
*
|
|
14
|
+
* Set explicitly when the consumer's chosen family doesn't match the
|
|
15
|
+
* slot's default category — e.g. set `header: { family: 'Inter',
|
|
16
|
+
* category: 'sans-serif' }` so the fallback chain is
|
|
17
|
+
* `'Inter', system-ui, sans-serif` instead of `'Inter', Georgia, serif`.
|
|
18
|
+
*/
|
|
19
|
+
category?: FontCategory;
|
|
8
20
|
}
|
|
9
21
|
|
|
10
22
|
export interface LogoConfig {
|
|
@@ -18,16 +30,31 @@ export interface LogoConfig {
|
|
|
18
30
|
localDark?: string;
|
|
19
31
|
}
|
|
20
32
|
|
|
33
|
+
export interface FaviconIcon {
|
|
34
|
+
/** The `rel` attribute: "icon", "shortcut icon", "apple-touch-icon", "manifest". */
|
|
35
|
+
rel: string;
|
|
36
|
+
/** Icon file path. Bare filenames ("favicon.ico") get BASE_PATH-prefixed
|
|
37
|
+
* automatically; absolute URLs and root-relative paths are emitted as-is. */
|
|
38
|
+
href: string;
|
|
39
|
+
/** Optional MIME type hint (e.g., "image/png", "image/svg+xml"). */
|
|
40
|
+
type?: string;
|
|
41
|
+
/** Optional sizes attribute (e.g., "180x180", "any"). */
|
|
42
|
+
sizes?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
21
45
|
export interface FaviconConfig {
|
|
22
|
-
/** Path prefix for all favicon URLs. Defaults to '/'
|
|
23
|
-
* when deploying under BASE_PATH != '/'. */
|
|
46
|
+
/** Path prefix for all favicon URLs. Defaults to BASE_PATH-aware '/'. */
|
|
24
47
|
base_path?: string;
|
|
25
|
-
/** Skip emitting the default <link
|
|
26
|
-
*
|
|
27
|
-
* `links_html`. Default: false (emit default tags). */
|
|
48
|
+
/** Skip emitting the default <link> block. Use when the consumer provides
|
|
49
|
+
* their own icons via `icons` or wants zero favicon <link> tags. */
|
|
28
50
|
skip_default_links?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
*
|
|
51
|
+
/** Consumer-declared icons (DATA, not HTML). Replaces the default 16-link
|
|
52
|
+
* set when non-empty. Each entry renders to a single <link> tag with
|
|
53
|
+
* BASE_PATH-aware href. */
|
|
54
|
+
icons?: FaviconIcon[];
|
|
55
|
+
/** @deprecated Use `icons` instead. Raw HTML emitted verbatim — impossible
|
|
56
|
+
* to validate, can't be safely BASE_PATH-rewritten. Kept for backward
|
|
57
|
+
* compat; emits a console warning when used. */
|
|
31
58
|
links_html?: string;
|
|
32
59
|
/** Consumer-side directory (relative to cwd) holding canonical favicon files.
|
|
33
60
|
* The CLI's favicon step copies these into public/, overriding defaults. */
|
|
@@ -38,8 +65,18 @@ export interface SiteBranding {
|
|
|
38
65
|
primaryColor?: string;
|
|
39
66
|
darkColor?: string;
|
|
40
67
|
fonts?: {
|
|
68
|
+
/** Most prominent display text on each page — home hero, concept name
|
|
69
|
+
* on the detail page, group/dataset title. No category is dictated;
|
|
70
|
+
* pick `serif`, `sans-serif`, or `monospace` for any slot. */
|
|
71
|
+
title?: FontConfig;
|
|
72
|
+
/** Section headings (h2–h6). */
|
|
73
|
+
heading?: FontConfig;
|
|
74
|
+
/** Backward-compat alias for `heading` (deprecated). */
|
|
41
75
|
header?: FontConfig;
|
|
76
|
+
/** Paragraph text, lists, table cells. */
|
|
42
77
|
body?: FontConfig;
|
|
78
|
+
/** Code blocks, inline code, kbd. */
|
|
79
|
+
mono?: FontConfig;
|
|
43
80
|
};
|
|
44
81
|
logo?: LogoConfig;
|
|
45
82
|
footerLogo?: LogoConfig;
|
|
@@ -3,6 +3,7 @@ import type { PageConfig, SiteColors } from './types';
|
|
|
3
3
|
import type { DatasetGroup } from './types';
|
|
4
4
|
import { synthesizePages } from './page-types';
|
|
5
5
|
import { locale } from '../i18n';
|
|
6
|
+
import { fontStack, DEFAULT_FONTS, DEFAULT_CATEGORY } from '../utils/font-stack';
|
|
6
7
|
|
|
7
8
|
export interface RuntimeSiteConfig {
|
|
8
9
|
id: string;
|
|
@@ -21,8 +22,11 @@ export interface RuntimeSiteConfig {
|
|
|
21
22
|
primaryColor?: string;
|
|
22
23
|
darkColor?: string;
|
|
23
24
|
fonts?: {
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
title?: { family: string; source: string; weights?: number[]; url?: string; category?: 'serif' | 'sans-serif' | 'monospace' };
|
|
26
|
+
heading?: { family: string; source: string; weights?: number[]; url?: string; category?: 'serif' | 'sans-serif' | 'monospace' };
|
|
27
|
+
header?: { family: string; source: string; weights?: number[]; url?: string; category?: 'serif' | 'sans-serif' | 'monospace' }; // deprecated alias for heading
|
|
28
|
+
body?: { family: string; source: string; weights?: number[]; url?: string; category?: 'serif' | 'sans-serif' | 'monospace' };
|
|
29
|
+
mono?: { family: string; source: string; weights?: number[]; url?: string; category?: 'serif' | 'sans-serif' | 'monospace' };
|
|
26
30
|
};
|
|
27
31
|
logo?: { path: string; alt: string; url?: string; light?: string; dark?: string };
|
|
28
32
|
footerLogo?: { path: string; alt: string; url?: string; light?: string; dark?: string };
|
|
@@ -95,13 +99,25 @@ function applyBranding(config: RuntimeSiteConfig) {
|
|
|
95
99
|
root.style.setProperty('--brand-dark', b.darkColor);
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
if (b.fonts?.
|
|
99
|
-
loadFont(b.fonts.
|
|
100
|
-
root.style.setProperty('--font-
|
|
102
|
+
if (b.fonts?.title) {
|
|
103
|
+
loadFont(b.fonts.title);
|
|
104
|
+
root.style.setProperty('--font-title', fontStack(b.fonts.title, DEFAULT_CATEGORY.title) ?? DEFAULT_FONTS.title);
|
|
105
|
+
}
|
|
106
|
+
// heading is the new slot name; header is the backward-compat alias.
|
|
107
|
+
const headingFont = b.fonts?.heading ?? b.fonts?.header;
|
|
108
|
+
if (headingFont) {
|
|
109
|
+
loadFont(headingFont);
|
|
110
|
+
const stack = fontStack(headingFont, DEFAULT_CATEGORY.heading) ?? DEFAULT_FONTS.heading;
|
|
111
|
+
root.style.setProperty('--font-heading', stack);
|
|
112
|
+
root.style.setProperty('--font-header', stack);
|
|
101
113
|
}
|
|
102
114
|
if (b.fonts?.body) {
|
|
103
115
|
loadFont(b.fonts.body);
|
|
104
|
-
root.style.setProperty('--font-body',
|
|
116
|
+
root.style.setProperty('--font-body', fontStack(b.fonts.body, DEFAULT_CATEGORY.body) ?? DEFAULT_FONTS.body);
|
|
117
|
+
}
|
|
118
|
+
if (b.fonts?.mono) {
|
|
119
|
+
loadFont(b.fonts.mono);
|
|
120
|
+
root.style.setProperty('--font-mono', fontStack(b.fonts.mono, DEFAULT_CATEGORY.mono) ?? DEFAULT_FONTS.mono);
|
|
105
121
|
}
|
|
106
122
|
}
|
|
107
123
|
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
import '@/styles/tailwind.css';
|
|
3
3
|
import AppIsland from '../islands/AppIsland.vue';
|
|
4
4
|
import { getSiteConfig } from '../lib/site-config';
|
|
5
|
+
import {
|
|
6
|
+
fontStack,
|
|
7
|
+
DEFAULT_FONTS,
|
|
8
|
+
DEFAULT_CATEGORY,
|
|
9
|
+
type FontConfigLike,
|
|
10
|
+
} from '../utils/font-stack';
|
|
11
|
+
import { resolveFaviconBase } from '../utils/favicon-base';
|
|
12
|
+
import { buildDefaultFaviconLinks, renderFaviconLinkTag } from '../utils/favicon-links';
|
|
5
13
|
|
|
6
14
|
interface Props {
|
|
7
15
|
title: string;
|
|
@@ -14,75 +22,111 @@ const { title, description = '', conceptTerm, conceptDefinition } = Astro.props;
|
|
|
14
22
|
const siteConfig = await getSiteConfig();
|
|
15
23
|
|
|
16
24
|
// Derive Google Fonts URL from siteConfig branding (build-time SSG).
|
|
17
|
-
//
|
|
18
|
-
//
|
|
25
|
+
// Includes every slot the consumer declared (title / heading / body / mono,
|
|
26
|
+
// plus the deprecated `header` alias). Falls back to the default
|
|
27
|
+
// DM Sans / DM Serif Display / JetBrains Mono when nothing is declared.
|
|
19
28
|
const brandingFonts = siteConfig?.branding?.fonts;
|
|
29
|
+
const headingFont = brandingFonts?.heading ?? brandingFonts?.header; // header = deprecated alias
|
|
30
|
+
const fontSlots = [
|
|
31
|
+
brandingFonts?.title,
|
|
32
|
+
headingFont,
|
|
33
|
+
brandingFonts?.body,
|
|
34
|
+
brandingFonts?.mono,
|
|
35
|
+
];
|
|
20
36
|
const fontParts: string[] = [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const w = (brandingFonts.body.weights || [400, 700]).join(';');
|
|
27
|
-
fontParts.push(`family=${brandingFonts.body.family.replace(/ /g, '+')}:wght@${w}`);
|
|
37
|
+
for (const f of fontSlots) {
|
|
38
|
+
if (f && (f as FontConfigLike).source === 'google' && (f as FontConfigLike).family) {
|
|
39
|
+
const w = (f.weights || [400, 700]).join(';');
|
|
40
|
+
fontParts.push(`family=${(f as FontConfigLike).family.replace(/ /g, '+')}:wght@${w}`);
|
|
41
|
+
}
|
|
28
42
|
}
|
|
29
43
|
const fontsUrl = fontParts.length > 0
|
|
30
44
|
? `https://fonts.googleapis.com/css2?${fontParts.join('&')}&display=swap`
|
|
31
45
|
: 'https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Serif+Display:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap';
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
// Build CSS font-family stacks via the shared SSOT. Consumers can pick
|
|
48
|
+
// any category for any slot — e.g. sans-serif title + serif heading —
|
|
49
|
+
// and the fallback chain follows the declared category.
|
|
50
|
+
const fontTitle = fontStack(brandingFonts?.title, DEFAULT_CATEGORY.title) ?? DEFAULT_FONTS.title;
|
|
51
|
+
const fontHeading = fontStack(headingFont, DEFAULT_CATEGORY.heading) ?? DEFAULT_FONTS.heading;
|
|
52
|
+
const fontBody = fontStack(brandingFonts?.body, DEFAULT_CATEGORY.body) ?? DEFAULT_FONTS.body;
|
|
53
|
+
const fontMono = fontStack(brandingFonts?.mono, DEFAULT_CATEGORY.mono) ?? DEFAULT_FONTS.mono;
|
|
39
54
|
|
|
40
|
-
// Favicon configuration (
|
|
55
|
+
// Favicon configuration (issues #173, #177, and the data-driven refactor).
|
|
56
|
+
//
|
|
57
|
+
// Consumers provide their own favicon set in one of these ways:
|
|
41
58
|
//
|
|
42
|
-
//
|
|
59
|
+
// 1. Declare icons as DATA (recommended):
|
|
60
|
+
// branding:
|
|
61
|
+
// favicon:
|
|
62
|
+
// icons:
|
|
63
|
+
// - rel: icon
|
|
64
|
+
// type: image/svg+xml
|
|
65
|
+
// href: favicon.svg
|
|
66
|
+
// - rel: icon
|
|
67
|
+
// type: image/png
|
|
68
|
+
// sizes: 96x96
|
|
69
|
+
// href: favicon-96x96.png
|
|
70
|
+
// - rel: shortcut icon
|
|
71
|
+
// href: favicon.ico
|
|
72
|
+
// - rel: apple-touch-icon
|
|
73
|
+
// sizes: 180x180
|
|
74
|
+
// href: apple-touch-icon.png
|
|
75
|
+
// - rel: manifest
|
|
76
|
+
// href: site.webmanifest
|
|
77
|
+
// The hrefs are filenames (or absolute URLs) — the layout applies
|
|
78
|
+
// the correct BASE_PATH prefix automatically.
|
|
43
79
|
//
|
|
44
|
-
//
|
|
45
|
-
// to the consumer's cwd). The CLI
|
|
46
|
-
//
|
|
47
|
-
// just needs to emit the right <link> tags.
|
|
80
|
+
// 2. Drop canonical files at `branding.favicon.source_dir` (relative
|
|
81
|
+
// to the consumer's cwd). The CLI copies these into public/, and
|
|
82
|
+
// the layout emits the default 16-link set with BASE_PATH-aware URLs.
|
|
48
83
|
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// string (e.g. RealFaviconGenerator output). The layout emits
|
|
52
|
-
// that verbatim and skips the default tag block.
|
|
84
|
+
// 3. Set `branding.favicon.skip_default_links: true` to emit nothing
|
|
85
|
+
// (consumer takes full control via their own layout patching).
|
|
53
86
|
//
|
|
54
|
-
//
|
|
87
|
+
// `links_html` is DEPRECATED — raw HTML can't be safely BASE_PATH-rewritten
|
|
88
|
+
// or validated. It still works for backward compat, with a console warning.
|
|
55
89
|
const faviconCfg = siteConfig?.branding?.favicon ?? {};
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const
|
|
90
|
+
const faviconCfgIsObject =
|
|
91
|
+
faviconCfg !== null && typeof faviconCfg === 'object' && !Array.isArray(faviconCfg);
|
|
92
|
+
const skipDefaultFaviconLinks = faviconCfgIsObject && faviconCfg.skip_default_links === true;
|
|
93
|
+
const consumerIcons = faviconCfgIsObject ? (faviconCfg.icons ?? null) : null;
|
|
94
|
+
const deprecatedLinksHtml: string = faviconCfgIsObject ? (faviconCfg.links_html ?? '') : '';
|
|
95
|
+
if (deprecatedLinksHtml) {
|
|
96
|
+
console.warn(
|
|
97
|
+
'branding.favicon.links_html is deprecated. Use branding.favicon.icons (data) instead — ' +
|
|
98
|
+
'see src/utils/favicon-links.ts for the schema. links_html will be removed in a future release.',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// BASE_PATH-aware favicon URL prefix. Walks the configuration cascade:
|
|
103
|
+
// branding.favicon.base_path → siteConfig.basePath → import.meta.env.BASE_URL → '/'.
|
|
104
|
+
// Always ends with '/' so `${faviconBase}favicon.ico` resolves correctly
|
|
105
|
+
// whether the deployment is at '/' or '/cie-eilv/'.
|
|
106
|
+
//
|
|
107
|
+
// See src/utils/favicon-base.ts for the shared resolver + regression tests.
|
|
108
|
+
const faviconBaseInput = {
|
|
109
|
+
brandingFaviconBasePath: typeof faviconCfg === 'object' ? faviconCfg.base_path : undefined,
|
|
110
|
+
siteConfigBasePath: siteConfig?.basePath,
|
|
111
|
+
astroBaseUrl: import.meta.env.BASE_URL,
|
|
112
|
+
};
|
|
113
|
+
const faviconBase = resolveFaviconBase(faviconBaseInput);
|
|
114
|
+
// Build the favicon <link> block from data (consumer-declared icons or
|
|
115
|
+
// the default 16-link set). BASE_PATH is applied to every href.
|
|
116
|
+
const faviconLinks = buildFaviconLinks({
|
|
117
|
+
...faviconBaseInput,
|
|
118
|
+
icons: consumerIcons ?? undefined,
|
|
119
|
+
}).map(renderFaviconLinkTag);
|
|
59
120
|
---
|
|
60
121
|
|
|
61
122
|
<!DOCTYPE html>
|
|
62
123
|
<html lang="en">
|
|
63
124
|
<head>
|
|
64
125
|
<meta charset="utf-8" />
|
|
65
|
-
{!skipDefaultFaviconLinks && (
|
|
66
|
-
|
|
67
|
-
<link rel="icon" type="image/x-icon" href={`${faviconBase}favicon.ico`} />
|
|
68
|
-
<link rel="icon" type="image/svg+xml" href={`${faviconBase}favicon.svg`} />
|
|
69
|
-
<link rel="icon" type="image/png" sizes="16x16" href={`${faviconBase}favicon-16x16.png`} />
|
|
70
|
-
<link rel="icon" type="image/png" sizes="32x32" href={`${faviconBase}favicon-32x32.png`} />
|
|
71
|
-
<link rel="icon" type="image/png" sizes="48x48" href={`${faviconBase}favicon-48x48.png`} />
|
|
72
|
-
<link rel="apple-touch-icon" sizes="57x57" href={`${faviconBase}apple-touch-icon-57x57.png`} />
|
|
73
|
-
<link rel="apple-touch-icon" sizes="60x60" href={`${faviconBase}apple-touch-icon-60x60.png`} />
|
|
74
|
-
<link rel="apple-touch-icon" sizes="72x72" href={`${faviconBase}apple-touch-icon-72x72.png`} />
|
|
75
|
-
<link rel="apple-touch-icon" sizes="76x76" href={`${faviconBase}apple-touch-icon-76x76.png`} />
|
|
76
|
-
<link rel="apple-touch-icon" sizes="114x114" href={`${faviconBase}apple-touch-icon-114x114.png`} />
|
|
77
|
-
<link rel="apple-touch-icon" sizes="120x120" href={`${faviconBase}apple-touch-icon-120x120.png`} />
|
|
78
|
-
<link rel="apple-touch-icon" sizes="144x144" href={`${faviconBase}apple-touch-icon-144x144.png`} />
|
|
79
|
-
<link rel="apple-touch-icon" sizes="152x152" href={`${faviconBase}apple-touch-icon-152x152.png`} />
|
|
80
|
-
<link rel="apple-touch-icon" sizes="167x167" href={`${faviconBase}apple-touch-icon-167x167.png`} />
|
|
81
|
-
<link rel="apple-touch-icon" sizes="180x180" href={`${faviconBase}apple-touch-icon-180x180.png`} />
|
|
82
|
-
<link rel="apple-touch-icon" sizes="1024x1024" href={`${faviconBase}apple-touch-icon-1024x1024.png`} />
|
|
83
|
-
</>
|
|
126
|
+
{!skipDefaultFaviconLinks && faviconLinks.length > 0 && (
|
|
127
|
+
<Fragment set:html={faviconLinks.join('\n ')} />
|
|
84
128
|
)}
|
|
85
|
-
{
|
|
129
|
+
{deprecatedLinksHtml && <Fragment set:html={deprecatedLinksHtml} />}
|
|
86
130
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
87
131
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
88
132
|
<meta name="apple-mobile-web-app-title" content={siteConfig.title} />
|
|
@@ -93,7 +137,7 @@ const faviconBase = faviconCfg.base_path ?? '/';
|
|
|
93
137
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
94
138
|
<link rel="preload" as="style" href={fontsUrl} onload="this.rel='stylesheet'">
|
|
95
139
|
<noscript><link href={fontsUrl} rel="stylesheet"></noscript>
|
|
96
|
-
<Fragment set:html={`<style>:root{--font-header:${
|
|
140
|
+
<Fragment set:html={`<style>:root{--font-title:${fontTitle};--font-heading:${fontHeading};--font-header:${fontHeading};--font-body:${fontBody};--font-mono:${fontMono}}</style>`} />
|
|
97
141
|
<script is:inline>
|
|
98
142
|
(function() {
|
|
99
143
|
const c = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith('theme='));
|
package/src/styles/tailwind.css
CHANGED
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
--brand-primary: #2563eb;
|
|
61
61
|
--brand-primary-rgb: 37, 99, 235;
|
|
62
62
|
--brand-dark: #1e293b;
|
|
63
|
+
--font-title: "DM Serif Display", Georgia, serif;
|
|
64
|
+
--font-heading: "DM Serif Display", Georgia, serif;
|
|
63
65
|
--font-header: "DM Serif Display", Georgia, serif;
|
|
64
66
|
--font-body: "DM Sans", system-ui, sans-serif;
|
|
65
67
|
--gold-accent: #B8935A;
|
|
@@ -73,6 +75,9 @@
|
|
|
73
75
|
-webkit-font-smoothing: antialiased;
|
|
74
76
|
-moz-osx-font-smoothing: grayscale;
|
|
75
77
|
}
|
|
78
|
+
.font-title {
|
|
79
|
+
font-family: var(--font-title);
|
|
80
|
+
}
|
|
76
81
|
h1, h2, h3, .font-serif {
|
|
77
82
|
font-family: var(--font-header);
|
|
78
83
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Favicon base path resolver — shared SSOT for the URL prefix that
|
|
3
|
+
* goes in front of every favicon `<link href="...">`.
|
|
4
|
+
*
|
|
5
|
+
* Used by:
|
|
6
|
+
* - `src/layouts/Default.astro` (build-time SSG <link> emission)
|
|
7
|
+
* - `cli/index.mjs` / `cli/index.ts` (runtime <link> rewriting)
|
|
8
|
+
*
|
|
9
|
+
* Why this exists: a deployment at `/cie-eilv/` has its favicon files
|
|
10
|
+
* at `/cie-eilv/favicon.ico`, NOT `/favicon.ico`. Hardcoding `/` as
|
|
11
|
+
* the URL prefix (which 0.7.106 did) caused 404s on every sub-path
|
|
12
|
+
* deployment. This resolver walks the configuration cascade to find
|
|
13
|
+
* the right prefix.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface FaviconBaseInput {
|
|
17
|
+
/** Explicit consumer override via `branding.favicon.base_path`. */
|
|
18
|
+
brandingFaviconBasePath?: string;
|
|
19
|
+
/** Consumer's declared sub-path via `site-config.yml:basePath`. */
|
|
20
|
+
siteConfigBasePath?: string;
|
|
21
|
+
/** Build-time BASE_PATH from Astro/Vite (`import.meta.env.BASE_URL`). */
|
|
22
|
+
astroBaseUrl?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the URL prefix for favicon links. Always returns a string
|
|
27
|
+
* ending with `/` so callers can do `${base}favicon.ico` without
|
|
28
|
+
* worrying about the separator.
|
|
29
|
+
*
|
|
30
|
+
* Cascade (first non-empty wins):
|
|
31
|
+
* 1. branding.favicon.base_path (explicit per-deployment override)
|
|
32
|
+
* 2. siteConfig.basePath (consumer's declared sub-path)
|
|
33
|
+
* 3. import.meta.env.BASE_URL (build-time BASE_PATH)
|
|
34
|
+
* 4. '/' (last resort — root deployment)
|
|
35
|
+
*/
|
|
36
|
+
export function resolveFaviconBase(input: FaviconBaseInput): string {
|
|
37
|
+
const candidates = [
|
|
38
|
+
input.brandingFaviconBasePath,
|
|
39
|
+
input.siteConfigBasePath,
|
|
40
|
+
input.astroBaseUrl,
|
|
41
|
+
'/',
|
|
42
|
+
];
|
|
43
|
+
const raw = candidates.find(c => c != null && c.length > 0) ?? '/';
|
|
44
|
+
return raw.endsWith('/') ? raw : `${raw}/`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Favicon link-tag builder — shared SSOT for the `<link rel="icon">`
|
|
3
|
+
* block emitted in `<head>`.
|
|
4
|
+
*
|
|
5
|
+
* Used by:
|
|
6
|
+
* - `src/layouts/Default.astro` (build-time SSG)
|
|
7
|
+
* - `cli/index.mjs` (consumer-side favicon pipeline)
|
|
8
|
+
*
|
|
9
|
+
* Design principle: declare icons as DATA, not HTML. Consumers describe
|
|
10
|
+
* their favicon set via `branding.favicon.icons: [{ rel, href, type?,
|
|
11
|
+
* sizes? }]`. The href is a filename (or path relative to the deployment
|
|
12
|
+
* root); the system applies the correct BASE_PATH prefix and renders
|
|
13
|
+
* well-formed `<link>` tags. No raw HTML in YAML.
|
|
14
|
+
*
|
|
15
|
+
* Why data over HTML: HTML-in-YAML is impossible to validate, can't be
|
|
16
|
+
* BASE_PATH-rewritten safely (regex on user-supplied markup is brittle),
|
|
17
|
+
* and forces the consumer to know the exact attribute order/shape the
|
|
18
|
+
* browser expects. Data lets us validate, normalize, and render.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { resolveFaviconBase } from './favicon-base';
|
|
22
|
+
|
|
23
|
+
export interface FaviconIcon {
|
|
24
|
+
/** The `rel` attribute. Most common: "icon", "shortcut icon",
|
|
25
|
+
* "apple-touch-icon", "manifest". */
|
|
26
|
+
rel: string;
|
|
27
|
+
/** The icon file path. May be either:
|
|
28
|
+
* - A bare filename ("favicon.ico") — gets prefixed with the resolved
|
|
29
|
+
* base path automatically.
|
|
30
|
+
* - An absolute URL ("https://...") or root-relative path ("/x.png")
|
|
31
|
+
* — emitted as-is, no prefixing.
|
|
32
|
+
*/
|
|
33
|
+
href: string;
|
|
34
|
+
/** Optional MIME type hint. */
|
|
35
|
+
type?: string;
|
|
36
|
+
/** Optional `sizes` attribute (e.g., "180x180", "any"). */
|
|
37
|
+
sizes?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FaviconLinkTag {
|
|
41
|
+
rel: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
sizes?: string;
|
|
44
|
+
href: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Default favicon file set emitted by the CLI's `favicons` pipeline.
|
|
48
|
+
* Used when the consumer doesn't declare `branding.favicon.icons`. */
|
|
49
|
+
const DEFAULT_FAVICON_FILES: readonly FaviconIcon[] = [
|
|
50
|
+
{ rel: 'icon', type: 'image/x-icon', href: 'favicon.ico' },
|
|
51
|
+
{ rel: 'icon', type: 'image/svg+xml', href: 'favicon.svg' },
|
|
52
|
+
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: 'favicon-16x16.png' },
|
|
53
|
+
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: 'favicon-32x32.png' },
|
|
54
|
+
{ rel: 'icon', type: 'image/png', sizes: '48x48', href: 'favicon-48x48.png' },
|
|
55
|
+
{ rel: 'apple-touch-icon', sizes: '57x57', href: 'apple-touch-icon-57x57.png' },
|
|
56
|
+
{ rel: 'apple-touch-icon', sizes: '60x60', href: 'apple-touch-icon-60x60.png' },
|
|
57
|
+
{ rel: 'apple-touch-icon', sizes: '72x72', href: 'apple-touch-icon-72x72.png' },
|
|
58
|
+
{ rel: 'apple-touch-icon', sizes: '76x76', href: 'apple-touch-icon-76x76.png' },
|
|
59
|
+
{ rel: 'apple-touch-icon', sizes: '114x114',href: 'apple-touch-icon-114x114.png' },
|
|
60
|
+
{ rel: 'apple-touch-icon', sizes: '120x120',href: 'apple-touch-icon-120x120.png' },
|
|
61
|
+
{ rel: 'apple-touch-icon', sizes: '144x144',href: 'apple-touch-icon-144x144.png' },
|
|
62
|
+
{ rel: 'apple-touch-icon', sizes: '152x152',href: 'apple-touch-icon-152x152.png' },
|
|
63
|
+
{ rel: 'apple-touch-icon', sizes: '167x167',href: 'apple-touch-icon-167x167.png' },
|
|
64
|
+
{ rel: 'apple-touch-icon', sizes: '180x180',href: 'apple-touch-icon-180x180.png' },
|
|
65
|
+
{ rel: 'apple-touch-icon', sizes: '1024x1024',href: 'apple-touch-icon-1024x1024.png' },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
export interface BuildFaviconLinksInput {
|
|
69
|
+
/** Explicit consumer override via `branding.favicon.base_path`. */
|
|
70
|
+
brandingFaviconBasePath?: string;
|
|
71
|
+
/** Consumer's declared sub-path via `site-config.yml:basePath`. */
|
|
72
|
+
siteConfigBasePath?: string;
|
|
73
|
+
/** Build-time BASE_PATH from Astro/Vite (`import.meta.env.BASE_URL`). */
|
|
74
|
+
astroBaseUrl?: string;
|
|
75
|
+
/** Consumer-declared icons. When set, replaces the default set. */
|
|
76
|
+
icons?: readonly FaviconIcon[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the list of favicon link tags with BASE_PATH-aware URLs.
|
|
81
|
+
*
|
|
82
|
+
* - If `input.icons` is provided (non-empty), uses that set; otherwise
|
|
83
|
+
* falls back to the default 16-link set.
|
|
84
|
+
* - For each icon, the href is normalized:
|
|
85
|
+
* - Filenames ("favicon.ico") → prefixed with the resolved base path.
|
|
86
|
+
* - Absolute URLs ("https://...") → emitted as-is.
|
|
87
|
+
* - Root-relative paths ("/x.png") → emitted as-is.
|
|
88
|
+
*/
|
|
89
|
+
export function buildFaviconLinks(input: BuildFaviconLinksInput): FaviconLinkTag[] {
|
|
90
|
+
const base = resolveFaviconBase(input);
|
|
91
|
+
const icons = input.icons && input.icons.length > 0 ? input.icons : DEFAULT_FAVICON_FILES;
|
|
92
|
+
return icons.map(icon => ({
|
|
93
|
+
rel: icon.rel,
|
|
94
|
+
type: icon.type,
|
|
95
|
+
sizes: icon.sizes,
|
|
96
|
+
href: prefixHref(icon.href, base),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Back-compat alias — older name for the same operation. */
|
|
101
|
+
export const buildDefaultFaviconLinks = buildFaviconLinks;
|
|
102
|
+
|
|
103
|
+
/** Decide whether to prefix the href with the base path.
|
|
104
|
+
* Absolute URLs and root-relative paths are emitted as-is; bare filenames
|
|
105
|
+
* and relative paths are prefixed. */
|
|
106
|
+
function prefixHref(href: string, base: string): string {
|
|
107
|
+
if (!href) return base;
|
|
108
|
+
// Absolute URL: https://, http://, //, data:, etc.
|
|
109
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//')) return href;
|
|
110
|
+
// Root-relative: /anything
|
|
111
|
+
if (href.startsWith('/')) return href;
|
|
112
|
+
// Bare filename or relative path: prefix with base.
|
|
113
|
+
return `${base}${href}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Render a single FaviconLinkTag to an HTML <link> string.
|
|
118
|
+
*
|
|
119
|
+
* Attribute order: rel, type (if present), sizes (if present), href.
|
|
120
|
+
* Deterministic for snapshot testing.
|
|
121
|
+
*/
|
|
122
|
+
export function renderFaviconLinkTag(tag: FaviconLinkTag): string {
|
|
123
|
+
const attrs: string[] = [`rel="${tag.rel}"`];
|
|
124
|
+
if (tag.type) attrs.push(`type="${tag.type}"`);
|
|
125
|
+
if (tag.sizes) attrs.push(`sizes="${tag.sizes}"`);
|
|
126
|
+
attrs.push(`href="${tag.href}"`);
|
|
127
|
+
return `<link ${attrs.join(' ')} />`;
|
|
128
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font stack builder — shared SSOT for `branding.fonts.*` fallback chains.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - `src/layouts/Default.astro` (build-time SSG CSS variables)
|
|
6
|
+
* - `src/config/use-site-config.ts` (runtime CSS variable overrides)
|
|
7
|
+
*
|
|
8
|
+
* Design principle: NO slot dictates a font category. The consumer picks
|
|
9
|
+
* `serif`, `sans-serif`, or `monospace` for each slot independently. The
|
|
10
|
+
* defaults below preserve the Glossarist visual identity (serif title +
|
|
11
|
+
* serif heading + sans-serif body + monospace code) but every slot can
|
|
12
|
+
* be overridden to any category.
|
|
13
|
+
*
|
|
14
|
+
* Slot semantics:
|
|
15
|
+
* - title : single most-prominent display text on the page
|
|
16
|
+
* (home hero h1, concept name on detail page, group title)
|
|
17
|
+
* - heading : h2–h6 section headings
|
|
18
|
+
* - body : paragraph text, lists, table cells
|
|
19
|
+
* - mono : code blocks, inline code, kbd
|
|
20
|
+
*
|
|
21
|
+
* Backward compat:
|
|
22
|
+
* - `branding.fonts.header` is accepted as a deprecated alias for
|
|
23
|
+
* `branding.fonts.heading` (both write to `--font-heading`, which
|
|
24
|
+
* also surfaces as `--font-header` for legacy CSS).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export type FontCategory = 'serif' | 'sans-serif' | 'monospace';
|
|
28
|
+
|
|
29
|
+
export interface FontConfigLike {
|
|
30
|
+
family: string;
|
|
31
|
+
category?: FontCategory;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FALLBACKS: Readonly<Record<FontCategory, string>> = {
|
|
35
|
+
serif: 'Georgia, serif',
|
|
36
|
+
'sans-serif': 'system-ui, sans-serif',
|
|
37
|
+
monospace: 'ui-monospace, "JetBrains Mono", Menlo, Monaco, monospace',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a CSS font-family stack from a FontConfig-like object.
|
|
42
|
+
*
|
|
43
|
+
* - If `font` is null/undefined or has no family, returns `undefined`
|
|
44
|
+
* and the caller applies its slot-specific default.
|
|
45
|
+
* - If `font.category` is omitted, falls back to `defaultCategory`
|
|
46
|
+
* so existing configs preserve prior behavior.
|
|
47
|
+
*/
|
|
48
|
+
export function fontStack(
|
|
49
|
+
font: FontConfigLike | null | undefined,
|
|
50
|
+
defaultCategory: FontCategory,
|
|
51
|
+
): string | undefined {
|
|
52
|
+
if (!font?.family) return undefined;
|
|
53
|
+
const category: FontCategory = font.category ?? defaultCategory;
|
|
54
|
+
const fallback = FALLBACKS[category];
|
|
55
|
+
return `'${font.family}', ${fallback}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Default font stacks (used when the consumer declares nothing).
|
|
59
|
+
* Preserves the Glossarist visual identity. NOT a dictate — every
|
|
60
|
+
* slot can be overridden to any category via branding.fonts.*. */
|
|
61
|
+
export const DEFAULT_FONTS = {
|
|
62
|
+
title: `'DM Serif Display', ${FALLBACKS.serif}`,
|
|
63
|
+
heading: `'DM Serif Display', ${FALLBACKS.serif}`,
|
|
64
|
+
body: `'DM Sans', ${FALLBACKS['sans-serif']}`,
|
|
65
|
+
mono: `'JetBrains Mono', ${FALLBACKS.monospace}`,
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
/** Slot → default category, used when the consumer omits `category`.
|
|
69
|
+
* These defaults preserve prior behavior and are NOT a constraint —
|
|
70
|
+
* consumers can pick any category for any slot. */
|
|
71
|
+
export const DEFAULT_CATEGORY = {
|
|
72
|
+
title: 'serif' as FontCategory,
|
|
73
|
+
heading: 'serif' as FontCategory,
|
|
74
|
+
body: 'sans-serif' as FontCategory,
|
|
75
|
+
mono: 'monospace' as FontCategory,
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
@@ -321,7 +321,7 @@ function clearSection() {
|
|
|
321
321
|
|
|
322
322
|
<!-- Header -->
|
|
323
323
|
<div v-if="manifest" class="mb-8">
|
|
324
|
-
<h1 class="font-
|
|
324
|
+
<h1 class="font-title text-3xl text-ink-800 mb-2">{{ localizedTitle }}</h1>
|
|
325
325
|
<p class="text-ink-400 leading-relaxed max-w-2xl">{{ localizedDescription }}</p>
|
|
326
326
|
<div class="flex flex-wrap gap-2 mt-4">
|
|
327
327
|
<span class="badge" :style="conceptCountBadgeStyle">{{ manifest.conceptCount.toLocaleString() }} {{ t('dataset.concepts') }}</span>
|
package/src/views/GroupView.vue
CHANGED
|
@@ -96,7 +96,7 @@ function openDataset(id: string) {
|
|
|
96
96
|
<template v-else-if="group">
|
|
97
97
|
<div class="flex items-baseline gap-3 mb-2">
|
|
98
98
|
<span class="text-2xl">{{ groupMeta.glyph }}</span>
|
|
99
|
-
<h1 class="font-
|
|
99
|
+
<h1 class="font-title text-3xl text-ink-800 dark:text-ink-50">
|
|
100
100
|
{{ group.label }}
|
|
101
101
|
</h1>
|
|
102
102
|
</div>
|
package/src/views/HomeView.vue
CHANGED
|
@@ -77,7 +77,7 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
77
77
|
<span class="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-300 hidden sm:inline">{{ localizedSubtitle }}</span>
|
|
78
78
|
</template>
|
|
79
79
|
</div>
|
|
80
|
-
<h1 class="font-
|
|
80
|
+
<h1 class="font-title text-[2rem] sm:text-[2.75rem] text-ink-800 leading-[1.1] mb-4 tracking-tight">
|
|
81
81
|
{{ localizedTitle }}
|
|
82
82
|
<template v-if="localizedSubtitle"><br class="hidden sm:block" /> {{ localizedSubtitle }}</template>
|
|
83
83
|
</h1>
|
package/tailwind.config.js
CHANGED
|
@@ -13,9 +13,11 @@ export default {
|
|
|
13
13
|
theme: {
|
|
14
14
|
extend: {
|
|
15
15
|
fontFamily: {
|
|
16
|
+
title: ['var(--font-title, "DM Serif Display")', 'Georgia', 'serif'],
|
|
17
|
+
heading: ['var(--font-heading, "DM Serif Display")', 'Georgia', 'serif'],
|
|
16
18
|
serif: ['var(--font-header, "DM Serif Display")', 'Georgia', 'serif'],
|
|
17
19
|
sans: ['var(--font-body, "DM Sans")', 'system-ui', 'sans-serif'],
|
|
18
|
-
mono: ['"JetBrains Mono"', 'monospace'],
|
|
20
|
+
mono: ['var(--font-mono, "JetBrains Mono")', 'ui-monospace', 'monospace'],
|
|
19
21
|
},
|
|
20
22
|
colors: {
|
|
21
23
|
ink: {
|