@glossarist/concept-browser 0.7.68 → 0.7.71

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.
Files changed (60) hide show
  1. package/cli/index.mjs +28 -9
  2. package/package.json +14 -8
  3. package/public/favicon.svg +1 -4
  4. package/scripts/bridge-to-astro.mjs +155 -0
  5. package/scripts/generate-data.mjs +13 -0
  6. package/scripts/load-site-config.mjs +73 -1
  7. package/src/__tests__/tc204-deployment.test.ts +21 -0
  8. package/src/adapters/DatasetAdapter.ts +1 -0
  9. package/src/adapters/model-bridge.ts +50 -12
  10. package/src/adapters/non-verbal/glossarist-augment.d.ts +5 -1
  11. package/src/adapters/types.ts +1 -0
  12. package/src/assets/glossarist-logo-full.svg +1 -0
  13. package/src/astro-islands/app.ts +3 -0
  14. package/src/components/AppFooter.vue +3 -2
  15. package/src/components/AppHeader.vue +4 -7
  16. package/src/components/AppSidebar.vue +8 -5
  17. package/src/components/ConceptDetail.vue +12 -0
  18. package/src/components/DesignationList.vue +15 -4
  19. package/src/components/LanguageDetail.vue +6 -0
  20. package/src/components/RelationSphere.vue +36 -32
  21. package/src/components/site/SeoMeta.astro +23 -0
  22. package/src/components/site/SiteFooter.astro +14 -0
  23. package/src/components/site/SiteHeader.astro +25 -0
  24. package/src/components/site/SiteSidebar.astro +56 -0
  25. package/src/content.config.ts +85 -0
  26. package/src/i18n/locales/eng.yml +1 -0
  27. package/src/i18n/locales/fra.yml +1 -0
  28. package/src/islands/AppIsland.vue +7 -0
  29. package/src/islands/BibliographyIsland.vue +26 -0
  30. package/src/islands/ConceptDetailIsland.vue +27 -0
  31. package/src/islands/ConceptDiffIsland.vue +214 -0
  32. package/src/islands/ConceptIsland.vue +102 -0
  33. package/src/islands/GraphIsland.vue +15 -0
  34. package/src/islands/GroupRendererIsland.vue +15 -0
  35. package/src/islands/HeaderIsland.vue +10 -0
  36. package/src/islands/NonVerbalIsland.vue +75 -0
  37. package/src/islands/RelationSphereIsland.vue +20 -0
  38. package/src/islands/SearchIsland.vue +23 -0
  39. package/src/islands/SeriesCardIsland.vue +12 -0
  40. package/src/islands/ShortcutsIsland.vue +13 -0
  41. package/src/islands/SidebarIsland.vue +15 -0
  42. package/src/islands/SupersessionDiffIsland.vue +267 -0
  43. package/src/islands/app-entry.ts +8 -0
  44. package/src/islands/app.ts +4 -0
  45. package/src/layouts/Default.astro +70 -0
  46. package/src/lib/site-config.ts +26 -0
  47. package/src/pages/404.astro +9 -0
  48. package/src/pages/[...path].astro +29 -0
  49. package/src/pages/dataset/[id]/concept/[cid].astro +23 -0
  50. package/src/pages/dataset/[id]/index.astro +16 -0
  51. package/src/pages/graph.astro +5 -0
  52. package/src/pages/group/[id]/index.astro +16 -0
  53. package/src/pages/index.astro +10 -0
  54. package/src/pages/search.astro +5 -0
  55. package/src/styles/tailwind.css +503 -0
  56. package/src/views/ConceptView.vue +18 -23
  57. package/src/views/StatsView.vue +1 -1
  58. package/tsconfig.json +2 -1
  59. package/vite.config.ts +3 -1
  60. package/postcss.config.js +0 -13
package/cli/index.mjs CHANGED
@@ -173,15 +173,34 @@ Environment:
173
173
  }
174
174
  }
175
175
 
176
- // Run vite build using the package's vite.config.ts via programmatic API
177
- console.log(`\n=== BUILD SPA ===\n`);
178
- const viteConfig = resolve(pkgRoot, 'vite.config.ts');
179
- const { build: viteBuild } = await import('vite');
180
- await viteBuild({
181
- configFile: viteConfig,
182
- root: pkgRoot,
183
- mode: 'production',
184
- });
176
+ // Bridge data to Astro content collections
177
+ console.log(`\n=== BRIDGE DATA ===\n`);
178
+ const bridge = resolve(pkgRoot, 'scripts', 'bridge-to-astro.mjs');
179
+ if (existsSync(bridge)) {
180
+ await import(`file://${bridge}`);
181
+ }
182
+
183
+ // Run Astro build (TODO.astro/22)
184
+ console.log(`\n=== BUILD ASTRO ===\n`);
185
+ const astroConfig = resolve(pkgRoot, 'astro.config.mjs');
186
+ if (existsSync(astroConfig)) {
187
+ try {
188
+ const { build: astroBuild } = await import('astro');
189
+ await astroBuild({ root: pkgRoot, logLevel: 'info' });
190
+ } catch (e) {
191
+ console.warn(` Astro build failed (${e.message}), falling back to Vite SPA`);
192
+ console.log(`\n=== BUILD SPA ===\n`);
193
+ const viteConfig = resolve(pkgRoot, 'vite.config.ts');
194
+ const { build: viteBuild } = await import('vite');
195
+ await viteBuild({ configFile: viteConfig, root: pkgRoot, mode: 'production' });
196
+ }
197
+ } else {
198
+ // Fall back to Vite SPA if Astro config doesn't exist
199
+ console.log(`\n=== BUILD SPA ===\n`);
200
+ const viteConfig = resolve(pkgRoot, 'vite.config.ts');
201
+ const { build: viteBuild } = await import('vite');
202
+ await viteBuild({ configFile: viteConfig, root: pkgRoot, mode: 'production' });
203
+ }
185
204
 
186
205
  // Run postbuild (404 page) via dynamic import
187
206
  const postbuild = resolve(pkgRoot, 'scripts', 'generate-404.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.68",
3
+ "version": "0.7.71",
4
4
  "description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,29 +23,34 @@
23
23
  "test": "vitest run",
24
24
  "test:watch": "vitest",
25
25
  "mutation:test": "stryker run stryker.config.mjs",
26
- "validate:shacl": "node scripts/validate-shacl.mjs"
26
+ "validate:shacl": "node scripts/validate-shacl.mjs",
27
+ "dev:astro": "astro dev",
28
+ "build:astro": "astro build",
29
+ "preview:astro": "astro preview"
27
30
  },
28
31
  "dependencies": {
32
+ "@astrojs/sitemap": "^3.7.3",
33
+ "@astrojs/vue": "^7.0.1",
29
34
  "@plurimath/plurimath": "^0.2.2",
30
- "@vitejs/plugin-vue": "^5.2.3",
31
- "autoprefixer": "^10.4.21",
35
+ "@vitejs/plugin-vue": "^6.0.7",
36
+ "astro": "^7.0.7",
32
37
  "d3": "^7.9.0",
33
38
  "favicons": "^7.2.0",
34
- "glossarist": "^0.4.15",
39
+ "glossarist": "^0.4.17",
35
40
  "js-yaml": "^4.1.0",
36
41
  "jszip": "^3.10.1",
37
42
  "pinia": "^2.3.1",
38
- "postcss": "^8.5.3",
39
43
  "sharp": "^0.34.5",
40
- "tailwindcss": "^3.4.17",
41
- "vite": "^6.3.5",
44
+ "vite": "^8.1.4",
42
45
  "vue": "^3.5.13",
43
46
  "vue-router": "^4.5.1"
44
47
  },
45
48
  "devDependencies": {
49
+ "@astrojs/check": "^0.9.9",
46
50
  "@rdfjs/dataset": "^2.0.2",
47
51
  "@stryker-mutator/core": "^9.6.1",
48
52
  "@stryker-mutator/vitest-runner": "^9.6.1",
53
+ "@tailwindcss/vite": "^4.3.2",
49
54
  "@types/d3": "^7.4.3",
50
55
  "@types/n3": "^1.26.1",
51
56
  "@vue/test-utils": "^2.4.8",
@@ -53,6 +58,7 @@
53
58
  "happy-dom": "^20.9.0",
54
59
  "n3": "^1.26.0",
55
60
  "rdf-validate-shacl": "^0.4.5",
61
+ "tailwindcss": "^4.3.2",
56
62
  "typescript": "~5.7.3",
57
63
  "vitest": "^4.1.5",
58
64
  "vue-tsc": "^2.2.8"
@@ -1,4 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2
- <rect width="32" height="32" rx="6" fill="#2563eb"/>
3
- <text x="16" y="22" text-anchor="middle" font-family="system-ui" font-weight="bold" font-size="18" fill="white">G</text>
4
- </svg>
1
+ <?xml version="1.0" encoding="UTF-8"?><svg id="uuid-e2e4f56e-6766-4eaf-8ba1-80c056213369" xmlns="http://www.w3.org/2000/svg" width="183.1" height="172.48" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 183.1 172.48"><defs><linearGradient id="uuid-526bfbac-cffb-4a71-b92d-c02fefae5080" x1="51.31" y1="161.8" x2="102.32" y2="73.02" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3fb6b0"/><stop offset=".07" stop-color="#5cc5be"/><stop offset=".15" stop-color="#78d3cc"/><stop offset=".24" stop-color="#8fe0d8"/><stop offset=".34" stop-color="#a1e9e1"/><stop offset=".47" stop-color="#aef0e7"/><stop offset=".63" stop-color="#b5f3ea"/><stop offset="1" stop-color="#b8f5ec"/></linearGradient><linearGradient id="uuid-1b1e62d8-3f5c-498e-8428-628e5706abf6" x1="0" y1="59.58" x2="183.1" y2="59.58" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2d4164"/><stop offset=".47" stop-color="#2d4164" stop-opacity=".94"/><stop offset="1" stop-color="#2d4164" stop-opacity=".8"/></linearGradient><linearGradient id="uuid-b44d9113-c74c-48d0-8db3-7071ba63ee91" x1="71.68" y1="31.06" x2="111.41" y2="31.06" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2d4164"/><stop offset=".28" stop-color="#2c4365"/><stop offset="1" stop-color="#588ac1"/></linearGradient><linearGradient id="uuid-ef83f354-a6ba-455c-a262-42ad1af8aab0" x1="76.66" y1="69.37" x2="183.1" y2="69.37" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#34798a" stop-opacity=".7"/><stop offset="1" stop-color="#62c4dd"/></linearGradient><linearGradient id="uuid-9c4f3d11-41f0-40bc-8383-a054e148eefb" x1="81.4" y1="129.46" x2="127.22" y2="50.28" gradientUnits="userSpaceOnUse"><stop offset=".2" stop-color="#456399"/><stop offset="1" stop-color="#3a5c80" stop-opacity=".7"/></linearGradient><linearGradient id="uuid-75c138e5-3eb7-4084-88c8-ea7b28bec669" x1="88.72" y1="146.48" x2="54.34" y2="146.48" xlink:href="#uuid-b44d9113-c74c-48d0-8db3-7071ba63ee91"/><linearGradient id="uuid-648153de-b6a2-4cdb-bf68-e5b0950e981e" x1="86.16" y1="99.24" x2="47.06" y2="25.85" gradientUnits="userSpaceOnUse"><stop offset=".2" stop-color="#456399"/><stop offset="1" stop-color="#3a5c80" stop-opacity=".7"/></linearGradient><linearGradient id="uuid-e1874ccf-ad77-48dc-8e9c-b19bc34aaaf5" x1="89.58" y1="107.94" x2="63.88" y2="57.26" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#456399" stop-opacity=".5"/><stop offset="1" stop-color="#97a4b7" stop-opacity=".8"/></linearGradient></defs><path d="m87.41,59.48l-61.18,106.11c-1.63,2.85.6,6.85,3.98,6.88h37.88l63.56-112.75-44.23-.25Z" style="fill:url(#uuid-526bfbac-cffb-4a71-b92d-c02fefae5080); stroke-width:0px;"/><path d="m40.39,141.03c.29.28,1.01,2.7,1.61,4.91.67,2.49,2.77,4.38,5.33,4.68,21.2,2.5,21.4,20.71,21.4,20.71l22.5-39.92c-23.71,14.02-41.88,6.43-40.64-8.09-2.71,4.71-7.5,12.99-10.21,17.7Z" style="fill:#3a5c80; stroke-width:0px;"/><path d="m75.5,80.4s-14.96,25.41-18.41,31.59c8.71-8.42,17.69,2.79,24.11-8.66,8.13-14.5-5.7-22.93-5.7-22.93Z" style="fill:#60abbc; stroke-width:0px;"/><path d="m113.82,172.48h38.28c4.52-.01,6.66-3.57,4.43-7.48-6.55-11.51-59.96-105.41-59.96-105.41l-43.04-.37,60.29,113.27Z" style="fill:#b8f5ec; stroke-width:0px;"/><path d="m106.47,91.98c-9.24,2.3-34.69,12.42-21.32,20.21,12.57,7.31,28.11.2,32.05-11.73,1.79-4.66-2.29-10.67-10.74-8.47Z" style="fill:#3fb6b0; stroke-width:0px;"/><path d="m137.32,131.2l-13.09-23.07c.37,24.63-35.5,17.21-35.5,17.21l24.95,46.87c-1-25.77,10.54-35.8,23.65-41Z" style="fill:#7ee0d4; stroke-width:0px;"/><path d="m57.35,132.11s0-.01.01-.02c0,0,0,0,0,0" style="fill:#3a5c80; stroke-width:0px;"/><path d="m183.1,52.52c-.1-3.25-2.07-5.56-6-5.59-13.09-.07-163.15,0-163.15,0L0,72.26h183.1v-19.75Z" style="fill:url(#uuid-1b1e62d8-3f5c-498e-8428-628e5706abf6); stroke-width:0px;"/><path d="m71.68,47s19.67-31.93,27.9-31.9c10.36,0,15.93,31.9,8.24,31.83l-36.15.07Z" style="fill:url(#uuid-b44d9113-c74c-48d0-8db3-7071ba63ee91); stroke-width:0px;"/><path d="m183.1,59.91l-.13,10.94c.07,6.28-2.04,8.39-8.33,8.31h-97.99l10.97-19.59,95.47.33Z" style="fill:url(#uuid-ef83f354-a6ba-455c-a262-42ad1af8aab0); opacity:.78; stroke-width:0px;"/><polygon points="7.1 59.58 0 72.26 80.52 72.26 87.63 59.58 7.1 59.58" style="fill:#3a5c80; opacity:.8; stroke-width:0px;"/><path d="m117.54,47s-65.24,112.78-69.85,120.82c-1.1,1.92.38,4.63,2.65,4.65.71,0,17.74,0,17.74,0l70.73-125.47h-21.27Z" style="fill:url(#uuid-9c4f3d11-41f0-40bc-8383-a054e148eefb); opacity:.8; stroke-width:0px;"/><path d="m84.52,142.79c.55-.64.79-1.48,1.27-2.18,10.98-16.02-12.03-16.74-17.61-8.28l-12.79,23.17c-.14.24-.26.48-.38.72-3.39,7.29,6.71,13.38,11.92,7.26l17.59-20.69Z" style="fill:url(#uuid-75c138e5-3eb7-4084-88c8-ea7b28bec669); opacity:.8; stroke-width:0px;"/><path d="m124.71,126.57s0-.01-.01-.02c0,0,0,0,0,0,0,0,0,.02,0,.02Z" style="fill:#2e4365; stroke-width:0px;"/><polygon points="136.69 172.48 113.82 172.48 53.53 59.58 73.24 59.58 136.69 172.48" style="fill:url(#uuid-648153de-b6a2-4cdb-bf68-e5b0950e981e); opacity:.75; stroke-width:0px;"/><path d="m73.39,59.58l33.44,59.63c2.82,7.65-7.17,26.67-12.28,17.06L53.53,59.58h19.86Z" style="fill:url(#uuid-e1874ccf-ad77-48dc-8e9c-b19bc34aaaf5); stroke-width:0px;"/><path d="m115.87,135.34l18.22,32.5s-36.61-14.66-18.22-32.5Z" style="fill:#4977a4; stroke-width:0px;"/></svg>
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Data bridge: converts existing public/data/*.json into Astro
3
+ * content collections at src/content/. Run before `astro build`.
4
+ *
5
+ * Reads: public/data/datasets.json, public/data/{id}/manifest.json,
6
+ * public/data/{id}/index.json
7
+ * Writes: src/content/datasets/*.json, src/content/groups/*.json,
8
+ * src/content/concepts/{registerId}/{conceptId}.json
9
+ */
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs';
11
+ import { join, dirname } from 'path';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const CWD = process.cwd();
16
+ const PKG_ROOT = join(__dirname, '..');
17
+ // Read generated data from the CONSUMER's working directory (where
18
+ // generate-data.mjs writes public/data/). Write content collections
19
+ // to the PACKAGE's src/content/ (where Astro expects them).
20
+ const PUBLIC_DATA = join(CWD, 'public', 'data');
21
+ const PUBLIC_ROOT = join(CWD, 'public');
22
+ const CONTENT_DIR = join(PKG_ROOT, 'src', 'content');
23
+
24
+ function readJson(p) {
25
+ if (!existsSync(p)) return null;
26
+ return JSON.parse(readFileSync(p, 'utf-8'));
27
+ }
28
+
29
+ function writeJson(dir, name, data) {
30
+ mkdirSync(dir, { recursive: true });
31
+ writeFileSync(join(dir, name), JSON.stringify(data, null, 2));
32
+ }
33
+
34
+ // --- Datasets ---
35
+ const registryPath = join(PUBLIC_ROOT, 'datasets.json');
36
+ const siteConfigPath = join(PUBLIC_ROOT, 'site-config.json');
37
+ const registry = readJson(registryPath) || [];
38
+ const siteConfig = readJson(siteConfigPath) || {};
39
+
40
+ console.log(`Bridging ${registry.length} datasets to content collections...`);
41
+
42
+ for (const reg of registry) {
43
+ const id = reg.id;
44
+ const manifestPath = join(PUBLIC_DATA, id, 'manifest.json');
45
+ const manifest = readJson(manifestPath);
46
+ if (!manifest) {
47
+ console.warn(` Skipping ${id}: no manifest.json`);
48
+ continue;
49
+ }
50
+
51
+ const summary = reg.summary || {};
52
+ const entry = {
53
+ id: manifest.id || id,
54
+ title: summary.title || manifest.title || id,
55
+ description: summary.description || manifest.description || '',
56
+ uri: manifest.datasetUri || reg.datasetUri || '',
57
+ uriBase: manifest.uriBase || reg.uriBase || '',
58
+ year: manifest.year ?? summary.year ?? undefined,
59
+ status: manifest.status || summary.status || 'valid',
60
+ ref: manifest.ref || reg.ref || undefined,
61
+ refAliases: manifest.refAliases || reg.refAliases || undefined,
62
+ owner: summary.owner || manifest.owner || '',
63
+ languages: manifest.languages || summary.languages || ['eng'],
64
+ conceptCount: manifest.conceptCount || summary.conceptCount || 0,
65
+ color: summary.color || manifest.color || undefined,
66
+ tags: summary.tags || manifest.tags || [],
67
+ sections: manifest.sections || [],
68
+ sourceRepo: manifest.sourceRepo || '',
69
+ lastUpdated: manifest.lastUpdated || '',
70
+ };
71
+
72
+ // Remove undefined values
73
+ Object.keys(entry).forEach(k => entry[k] === undefined && delete entry[k]);
74
+
75
+ writeJson(join(CONTENT_DIR, 'datasets'), `${id}.json`, entry);
76
+ console.log(` Dataset: ${id} (${entry.conceptCount} concepts)`);
77
+
78
+ // --- Concepts for this dataset ---
79
+ const indexPath = join(PUBLIC_DATA, id, 'index.json');
80
+ const index = readJson(indexPath);
81
+ if (index && index.concepts) {
82
+ for (const concept of index.concepts) {
83
+ if (!concept) continue;
84
+ const conceptEntry = {
85
+ registerId: id,
86
+ conceptId: concept.id,
87
+ uri: `${entry.uriBase || ''}/${id}/concept/${concept.id}`,
88
+ status: concept.status || 'valid',
89
+ designations: concept.designations || {},
90
+ eng: concept.eng || concept.designations?.eng || '',
91
+ groups: concept.groups || [],
92
+ tags: [],
93
+ };
94
+
95
+ const fullConceptPath = join(PUBLIC_DATA, id, 'concepts', `${concept.id}.json`);
96
+ const fullConcept = readJson(fullConceptPath);
97
+ if (fullConcept) {
98
+ const lc = fullConcept['gl:localizedConcept'] || fullConcept.localizedConcept || {};
99
+ const localizations = {};
100
+ for (const [lang, loc] of Object.entries(lc)) {
101
+ localizations[lang] = {
102
+ languageCode: loc['gl:languageCode'] || lang,
103
+ terms: (loc['gl:designation'] || []).map(d => ({ designation: d['gl:term'] || d.term || '', normativeStatus: d['gl:normativeStatus'] || d.normativeStatus })).filter(d => d.designation),
104
+ definitions: (loc['gl:definition'] || []).map(d => ({ content: d['gl:content'] || d.content || '' })).filter(d => d.content),
105
+ notes: (loc['gl:notes'] || []).map(n => ({ content: n['gl:content'] || n.content || '' })).filter(n => n.content),
106
+ examples: (loc['gl:examples'] || []).map(e => ({ content: e['gl:content'] || e.content || '' })).filter(e => e.content),
107
+ };
108
+ }
109
+ conceptEntry.localizations = localizations;
110
+ conceptEntry.languages = Object.keys(localizations);
111
+ }
112
+
113
+ writeJson(
114
+ join(CONTENT_DIR, 'concepts', id),
115
+ `${concept.id}.json`,
116
+ conceptEntry
117
+ );
118
+ }
119
+ console.log(` ${index.concepts.filter(Boolean).length} concepts bridged`);
120
+ }
121
+ }
122
+
123
+ // --- Groups ---
124
+ const groups = siteConfig.datasetGroups || [];
125
+ for (const g of groups) {
126
+ const entry = {
127
+ id: g.id,
128
+ label: g.label || g.id,
129
+ kind: g.kind || (g.series ? 'lineage' : 'default'),
130
+ description: g.description || '',
131
+ current: g.current || undefined,
132
+ datasets: g.datasets || [],
133
+ color: g.color || undefined,
134
+ };
135
+ Object.keys(entry).forEach(k => entry[k] === undefined && delete entry[k]);
136
+ writeJson(join(CONTENT_DIR, 'groups'), `${g.id}.json`, entry);
137
+ console.log(` Group: ${g.id} (${entry.datasets.length} datasets)`);
138
+ }
139
+
140
+ // --- Pages (from public/pages/*.json) ---
141
+ const pagesDir = join(CWD, 'public', 'pages');
142
+ if (existsSync(pagesDir)) {
143
+ for (const file of readdirSync(pagesDir)) {
144
+ if (!file.endsWith('.json')) continue;
145
+ const page = readJson(join(pagesDir, file));
146
+ if (!page) continue;
147
+ // Write as a minimal markdown file for the pages content collection
148
+ const slug = file.replace('.json', '');
149
+ const md = `---\ntitle: "${page.title || slug}"\ntype: "page"\n---\n\n${page.html || ''}\n`;
150
+ writeFileSync(join(CONTENT_DIR, 'pages', `${slug}.md`), md);
151
+ }
152
+ console.log(' Pages bridged from public/pages/');
153
+ }
154
+
155
+ console.log('Data bridge complete.');
@@ -273,6 +273,7 @@ function sourcesToJsonLd(sources) {
273
273
  if (s.id) doc['gl:id'] = s.id;
274
274
  if (s.type) doc['gl:sourceType'] = s.type;
275
275
  if (s.status) doc['gl:sourceStatus'] = s.status;
276
+ if (s.modification) doc['gl:modification'] = s.modification;
276
277
  if (s.origin) {
277
278
  const origin = { '@type': 'gl:Citation' };
278
279
  const ref = refToJsonLd(s.origin.ref);
@@ -282,6 +283,17 @@ function sourcesToJsonLd(sources) {
282
283
  if (s.origin.link) origin['gl:link'] = s.origin.link;
283
284
  doc['gl:origin'] = origin;
284
285
  }
286
+ if (s.sourced_from && s.sourced_from.length) {
287
+ doc['gl:sourcedFrom'] = s.sourced_from.map(sf => {
288
+ const cite = { '@type': 'gl:Citation' };
289
+ const ref = refToJsonLd(sf.ref);
290
+ if (ref) cite['gl:ref'] = ref;
291
+ const loc = localityToJsonLd(sf.locality);
292
+ if (loc) cite['gl:locality'] = loc;
293
+ if (sf.link) cite['gl:link'] = sf.link;
294
+ return cite;
295
+ });
296
+ }
285
297
  return doc;
286
298
  });
287
299
  }
@@ -1228,6 +1240,7 @@ for (let i = 0; i < config.datasets.length; i++) {
1228
1240
  owner: resolvedOwner,
1229
1241
  tags: resolvedTags,
1230
1242
  color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
1243
+ year: reg?.year ?? undefined,
1231
1244
  },
1232
1245
  datasetUri: resolvedDatasetUri,
1233
1246
  uriBase: config.uriBase || undefined,
@@ -8,6 +8,14 @@
8
8
  * 2. SITE_ID env var → configs/{SITE_ID}.yml
9
9
  * 3. --site CLI flag → same as SITE_ID
10
10
  * 4. Fallback → site-config.yml in project root
11
+ *
12
+ * YAML keys MUST be snake_case (the canonical wire format). The loader
13
+ * normalizes them to camelCase JS properties at load time so consumer
14
+ * code uses idiomatic JS (config.uriBase, ds.gcrPackage, etc.) while
15
+ * the on-disk format stays snake_case.
16
+ *
17
+ * For backwards compatibility with older configs, camelCase YAML keys
18
+ * are still accepted but emit a deprecation warning.
11
19
  */
12
20
 
13
21
  import { readFileSync, existsSync } from 'fs';
@@ -18,6 +26,69 @@ import yaml from 'js-yaml';
18
26
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
27
  const projectRoot = resolve(__dirname, '..');
20
28
 
29
+ /**
30
+ * Known snake_case → camelCase mappings for site-config + register.
31
+ * Every YAML key must appear here in snake_case form. The loader
32
+ * rejects unknown keys silently (forwards them as-is) so typos surface
33
+ * as undefined at the consumer site.
34
+ */
35
+ const SNAKE_TO_CAMEL = {
36
+ // Site-level
37
+ uri_base: 'uriBase',
38
+ base_path: 'basePath',
39
+ ui_languages: 'uiLanguages',
40
+ dataset_groups: 'datasetGroups',
41
+ dataset_translations: 'datasetTranslations',
42
+ primary_color: 'primaryColor',
43
+ dark_color: 'darkColor',
44
+ owner_name: 'ownerName',
45
+ owner_url: 'ownerUrl',
46
+ powered_by: 'poweredBy',
47
+ footer_nav: 'footerNav',
48
+ footer_logo: 'footerLogo',
49
+ news_dir: 'newsDir',
50
+ // Dataset-level
51
+ gcr_package: 'gcrPackage',
52
+ source_repo: 'sourceRepo',
53
+ local_path: 'localPath',
54
+ local_light: 'localLight',
55
+ local_dark: 'localDark',
56
+ uri_aliases: 'uriAliases',
57
+ language_order: 'languageOrder',
58
+ ref_aliases: 'refAliases',
59
+ urn_aliases: 'urnAliases',
60
+ existing_site_url: 'existingSiteUrl',
61
+ edition_status: 'editionStatus',
62
+ dataset_uri: 'datasetUri',
63
+ schema_version: 'schemaVersion',
64
+ };
65
+
66
+ function normalizeKey(key) {
67
+ if (typeof key !== 'string') return key;
68
+ // Known snake_case key → camelCase
69
+ if (Object.prototype.hasOwnProperty.call(SNAKE_TO_CAMEL, key)) {
70
+ return SNAKE_TO_CAMEL[key];
71
+ }
72
+ // Generic snake_case → camelCase fallback for unknown keys
73
+ if (key.includes('_') && /^[a-z][a-z0-9_]*$/.test(key)) {
74
+ return key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
75
+ }
76
+ // camelCase or other → pass through (backwards compat)
77
+ return key;
78
+ }
79
+
80
+ function normalizeObject(obj) {
81
+ if (Array.isArray(obj)) return obj.map(normalizeObject);
82
+ if (obj && typeof obj === 'object') {
83
+ const out = {};
84
+ for (const [k, v] of Object.entries(obj)) {
85
+ out[normalizeKey(k)] = normalizeObject(v);
86
+ }
87
+ return out;
88
+ }
89
+ return obj;
90
+ }
91
+
21
92
  function findConfigFile(args = []) {
22
93
  if (process.env.SITE_CONFIG) {
23
94
  return resolve(process.env.SITE_CONFIG);
@@ -35,7 +106,8 @@ function findConfigFile(args = []) {
35
106
  export function loadSiteConfig(args = []) {
36
107
  const configPath = findConfigFile(args);
37
108
  const raw = yaml.load(readFileSync(configPath, 'utf-8'));
38
- return { config: raw, configPath };
109
+ const config = normalizeObject(raw);
110
+ return { config, configPath };
39
111
  }
40
112
 
41
113
  export function getProjectRoot() {
@@ -120,6 +120,27 @@ describe('tc204 deployment: multi-edition lineage series', () => {
120
120
  expect(series[0].current?.id).toBe('isotc204-ed3');
121
121
  expect(series[0].current?.status).toBe('unknown');
122
122
  });
123
+
124
+ it('year propagates through DatasetSummary → setSummaryManifest → member', () => {
125
+ // When the factory loads the registry, datasets with a summary skip
126
+ // the full manifest fetch and use a minimal manifest built from the
127
+ // summary. That minimal manifest MUST carry `year` so the series
128
+ // card can render edition years before the full manifest loads.
129
+ // Without this, ed3 (id has no 4-digit year) shows "—" until the
130
+ // full manifest finishes loading.
131
+ const manifests = [
132
+ makeManifest({ id: 'isotc204-ed3', status: 'draft', year: 2026 }),
133
+ makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025 }),
134
+ makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022 }),
135
+ ];
136
+ const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
137
+ // Every member has a year — no "—" in the series card
138
+ for (const m of series[0].members) {
139
+ expect(m.year).toBeDefined();
140
+ expect(typeof m.year).toBe('number');
141
+ }
142
+ expect(series[0].members.find(m => m.id === 'isotc204-ed3')?.year).toBe(2026);
143
+ });
123
144
  });
124
145
 
125
146
  describe('tc204 deployment: group about discovery', () => {
@@ -81,6 +81,7 @@ export class DatasetAdapter {
81
81
  sourceRepo: '',
82
82
  chunkSize: 1000,
83
83
  color: summary.color,
84
+ year: summary.year,
84
85
  };
85
86
  this.manifestComplete = false;
86
87
  }
@@ -103,6 +103,8 @@ interface JsonLdSource {
103
103
  'gl:sourceStatus'?: string;
104
104
  'gl:modification'?: string;
105
105
  'gl:origin'?: JsonLdOrigin;
106
+ 'gl:sourcedFrom'?: JsonLdOrigin[];
107
+ 'gl:sourced_from'?: JsonLdOrigin[];
106
108
  }
107
109
 
108
110
  interface JsonLdRelated {
@@ -216,6 +218,21 @@ export function getRelatedCitation(rc: object): Record<string, unknown> | null {
216
218
  // Relationship types whose target is a designation string, not a concept ref.
217
219
  const DESIGNATION_REL_TYPES = new Set(['abbreviated_form_for', 'short_form_for']);
218
220
 
221
+ function attachSourcedFromToSources(
222
+ sources: readonly { sourced_from?: unknown }[],
223
+ rawSources: unknown,
224
+ ): void {
225
+ if (!Array.isArray(rawSources)) return;
226
+ for (let i = 0; i < sources.length && i < rawSources.length; i++) {
227
+ const raw = rawSources[i] as Record<string, unknown> | undefined;
228
+ if (raw?.sourced_from && Array.isArray(raw.sourced_from)) {
229
+ (sources[i] as { sourced_from: unknown }).sourced_from = raw.sourced_from.map(
230
+ (sf: unknown) => sf instanceof Citation ? sf : Citation.fromJSON(sf as Record<string, unknown>)
231
+ );
232
+ }
233
+ }
234
+ }
235
+
219
236
  function attachBridges(concept: Concept, localizations: Record<string, unknown>): void {
220
237
  for (const lang of concept.languages) {
221
238
  const lc = concept.localization(lang);
@@ -223,6 +240,9 @@ function attachBridges(concept: Concept, localizations: Record<string, unknown>)
223
240
  if (!lc || !raw || typeof raw !== 'object') continue;
224
241
  const rawObj = raw as Record<string, unknown>;
225
242
 
243
+ // Attach sourced_from to localization-level sources
244
+ attachSourcedFromToSources(lc.sources as unknown as { sourced_from?: unknown }[], rawObj.sources);
245
+
226
246
  // Annotations
227
247
  const annList = rawObj.annotations;
228
248
  if (Array.isArray(annList) && annList.length > 0) {
@@ -234,7 +254,8 @@ function attachBridges(concept: Concept, localizations: Record<string, unknown>)
234
254
  // Designation-level relationship targets, ref text, sourceId, citation
235
255
  const rawTerms = rawObj.terms;
236
256
  if (Array.isArray(rawTerms)) {
237
- for (const rawTerm of rawTerms) {
257
+ for (let ti = 0; ti < rawTerms.length; ti++) {
258
+ const rawTerm = rawTerms[ti];
238
259
  if (!rawTerm || typeof rawTerm !== 'object') continue;
239
260
  const rawT = rawTerm as Record<string, unknown>;
240
261
  const rawDesignation = rawT.designation as string | undefined;
@@ -244,6 +265,11 @@ function attachBridges(concept: Concept, localizations: Record<string, unknown>)
244
265
  const rawRelated = rawT.related;
245
266
  if (!Array.isArray(rawRelated)) continue;
246
267
  attachRelatedBridges(designation.related, rawRelated);
268
+ // Attach sourced_from to designation-level sources
269
+ attachSourcedFromToSources(
270
+ designation.sources as unknown as { sourced_from?: unknown }[],
271
+ rawT.sources,
272
+ );
247
273
  }
248
274
  }
249
275
 
@@ -408,6 +434,19 @@ function mapDetailedDefinitionFromJsonLd(d: any): Record<string, unknown> {
408
434
  return result;
409
435
  }
410
436
 
437
+ function mapOriginFromJsonLd(o: JsonLdOrigin): Record<string, unknown> {
438
+ const origin: Record<string, unknown> = {};
439
+ const ref = mapRefFromJsonLd(o['gl:ref']);
440
+ if (ref) origin.ref = ref;
441
+ const loc = mapLocalityFromJsonLd(o['gl:locality']);
442
+ if (loc) origin.locality = loc;
443
+ if (o['gl:link']) origin.link = o['gl:link'];
444
+ if (o['gl:id']) origin.id = o['gl:id'];
445
+ if (o['gl:version']) origin.version = o['gl:version'];
446
+ if (o['gl:source']) origin.source = o['gl:source'];
447
+ return origin;
448
+ }
449
+
411
450
  function mapSourceFromJsonLd(s: JsonLdSource): Record<string, unknown> {
412
451
  const result: Record<string, unknown> = {};
413
452
  if (s['gl:id']) result.id = s['gl:id'];
@@ -416,17 +455,12 @@ function mapSourceFromJsonLd(s: JsonLdSource): Record<string, unknown> {
416
455
  if (s['gl:modification']) result.modification = s['gl:modification'];
417
456
 
418
457
  if (s['gl:origin']) {
419
- const origin: Record<string, unknown> = {};
420
- const o = s['gl:origin'];
421
- const ref = mapRefFromJsonLd(o['gl:ref']);
422
- if (ref) origin.ref = ref;
423
- const loc = mapLocalityFromJsonLd(o['gl:locality']);
424
- if (loc) origin.locality = loc;
425
- if (o['gl:link']) origin.link = o['gl:link'];
426
- if (o['gl:id']) origin.id = o['gl:id'];
427
- if (o['gl:version']) origin.version = o['gl:version'];
428
- if (o['gl:source']) origin.source = o['gl:source'];
429
- result.origin = origin;
458
+ result.origin = mapOriginFromJsonLd(s['gl:origin']);
459
+ }
460
+
461
+ const sf = s['gl:sourcedFrom'] ?? s['gl:sourced_from'];
462
+ if (sf?.length) {
463
+ result.sourced_from = sf.map(item => mapOriginFromJsonLd(item));
430
464
  }
431
465
 
432
466
  return result;
@@ -614,6 +648,10 @@ export function conceptFromJson(doc: Record<string, unknown>): Concept {
614
648
  const concept = Concept.fromJSON(doc);
615
649
  const locs = (doc as Record<string, unknown>).localizations as Record<string, unknown> | undefined;
616
650
  if (locs) attachBridges(concept, locs);
651
+ attachSourcedFromToSources(
652
+ concept.sources as unknown as { sourced_from?: unknown }[],
653
+ (doc as Record<string, unknown>).sources,
654
+ );
617
655
  return concept;
618
656
  }
619
657
 
@@ -11,9 +11,13 @@
11
11
  // DELETE this file when upstream ships proper declarations — tracked by
12
12
  // PR glossarist/glossarist-js#31 (targets v0.4.3+).
13
13
 
14
- import type { ConceptSource, GlossaristModel } from 'glossarist';
14
+ import type { ConceptSource, Citation, GlossaristModel } from 'glossarist';
15
15
 
16
16
  declare module 'glossarist' {
17
+ interface ConceptSource {
18
+ sourced_from?: Citation[];
19
+ }
20
+
17
21
  class RegistrableModel extends GlossaristModel {
18
22
  static register(type: string, cls: typeof RegistrableModel): void;
19
23
  static fromData(data: Record<string, unknown>): RegistrableModel;
@@ -117,6 +117,7 @@ export interface DatasetSummary {
117
117
  owner: string;
118
118
  tags: string[];
119
119
  color?: string | { light: string; dark: string };
120
+ year?: number;
120
121
  }
121
122
 
122
123
  export interface DatasetRegistry {
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><svg id="uuid-fffeb195-1711-411a-b374-52204ad82fd6" xmlns="http://www.w3.org/2000/svg" width="183.1" height="172.48" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 183.1 172.48"><defs><linearGradient id="uuid-6317b9cc-2e5f-41e2-85f1-7443049bc3fc" x1="51.31" y1="161.8" x2="102.32" y2="73.02" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3fb6b0"/><stop offset=".07" stop-color="#5cc5be"/><stop offset=".15" stop-color="#78d3cc"/><stop offset=".24" stop-color="#8fe0d8"/><stop offset=".34" stop-color="#a1e9e1"/><stop offset=".47" stop-color="#aef0e7"/><stop offset=".63" stop-color="#b5f3ea"/><stop offset="1" stop-color="#b8f5ec"/></linearGradient><linearGradient id="uuid-ea13e7e7-b134-4ab8-aa9c-2ec35d57d2e0" x1="0" y1="59.58" x2="183.1" y2="59.58" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2d4164"/><stop offset=".47" stop-color="#2d4164" stop-opacity=".94"/><stop offset="1" stop-color="#2d4164" stop-opacity=".8"/></linearGradient><linearGradient id="uuid-b04c0104-3d2b-4e91-a646-00a62f882d94" x1="71.68" y1="31.06" x2="111.41" y2="31.06" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2d4164"/><stop offset=".28" stop-color="#2c4365"/><stop offset="1" stop-color="#588ac1"/></linearGradient><linearGradient id="uuid-3270c642-e286-4980-b9d4-c5a586959dc8" x1="76.66" y1="69.37" x2="183.1" y2="69.37" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#34798a" stop-opacity=".7"/><stop offset="1" stop-color="#62c4dd"/></linearGradient><linearGradient id="uuid-b517cc88-1e38-4408-9af5-0022fab7d735" x1="81.4" y1="129.46" x2="127.22" y2="50.28" gradientUnits="userSpaceOnUse"><stop offset=".2" stop-color="#456399"/><stop offset="1" stop-color="#3a5c80" stop-opacity=".7"/></linearGradient><linearGradient id="uuid-30648514-5669-4d95-96c9-4dab36559efb" x1="88.72" y1="146.48" x2="54.34" y2="146.48" xlink:href="#uuid-b04c0104-3d2b-4e91-a646-00a62f882d94"/><linearGradient id="uuid-ab3522e9-c0cd-4474-8859-9f36cdf98f81" x1="86.16" y1="99.24" x2="47.06" y2="25.85" gradientUnits="userSpaceOnUse"><stop offset=".2" stop-color="#456399"/><stop offset="1" stop-color="#3a5c80" stop-opacity=".7"/></linearGradient><linearGradient id="uuid-4f227c7d-7a65-4e56-9321-c6145c47210b" x1="89.58" y1="107.94" x2="63.88" y2="57.26" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#456399" stop-opacity=".5"/><stop offset="1" stop-color="#97a4b7" stop-opacity=".8"/></linearGradient></defs><path d="m87.41,59.48l-61.18,106.11c-1.63,2.85.6,6.85,3.98,6.88h37.88l63.56-112.75-44.23-.25Z" style="fill:url(#uuid-6317b9cc-2e5f-41e2-85f1-7443049bc3fc); stroke-width:0px;"/><path d="m40.39,141.03c.29.28,1.01,2.7,1.61,4.91.67,2.49,2.77,4.38,5.33,4.68,21.2,2.5,21.4,20.71,21.4,20.71l22.5-39.92c-23.71,14.02-41.88,6.43-40.64-8.09-2.71,4.71-7.5,12.99-10.21,17.7Z" style="fill:#3a5c80; stroke-width:0px;"/><path d="m75.5,80.4s-14.96,25.41-18.41,31.59c8.71-8.42,17.69,2.79,24.11-8.66,8.13-14.5-5.7-22.93-5.7-22.93Z" style="fill:#60abbc; stroke-width:0px;"/><path d="m113.82,172.48h38.28c4.52-.01,6.66-3.57,4.43-7.48-6.55-11.51-59.96-105.41-59.96-105.41l-43.04-.37,60.29,113.27Z" style="fill:#b8f5ec; stroke-width:0px;"/><path d="m106.47,91.98c-9.24,2.3-34.69,12.42-21.32,20.21,12.57,7.31,28.11.2,32.05-11.73,1.79-4.66-2.29-10.67-10.74-8.47Z" style="fill:#3fb6b0; stroke-width:0px;"/><path d="m137.32,131.2l-13.09-23.07c.37,24.63-35.5,17.21-35.5,17.21l24.95,46.87c-1-25.77,10.54-35.8,23.65-41Z" style="fill:#7ee0d4; stroke-width:0px;"/><path d="m57.35,132.11s0-.01.01-.02c0,0,0,0,0,0" style="fill:#3a5c80; stroke-width:0px;"/><path d="m183.1,52.52c-.1-3.25-2.07-5.56-6-5.59-13.09-.07-163.15,0-163.15,0L0,72.26h183.1v-19.75Z" style="fill:url(#uuid-ea13e7e7-b134-4ab8-aa9c-2ec35d57d2e0); stroke-width:0px;"/><path d="m71.68,47s19.67-31.93,27.9-31.9c10.36,0,15.93,31.9,8.24,31.83l-36.15.07Z" style="fill:url(#uuid-b04c0104-3d2b-4e91-a646-00a62f882d94); stroke-width:0px;"/><path d="m183.1,59.91l-.13,10.94c.07,6.28-2.04,8.39-8.33,8.31h-97.99l10.97-19.59,95.47.33Z" style="fill:url(#uuid-3270c642-e286-4980-b9d4-c5a586959dc8); opacity:.78; stroke-width:0px;"/><polygon points="7.1 59.58 0 72.26 80.52 72.26 87.63 59.58 7.1 59.58" style="fill:#3a5c80; opacity:.8; stroke-width:0px;"/><path d="m117.54,47s-65.24,112.78-69.85,120.82c-1.1,1.92.38,4.63,2.65,4.65.71,0,17.74,0,17.74,0l70.73-125.47h-21.27Z" style="fill:url(#uuid-b517cc88-1e38-4408-9af5-0022fab7d735); opacity:.8; stroke-width:0px;"/><path d="m84.52,142.79c.55-.64.79-1.48,1.27-2.18,10.98-16.02-12.03-16.74-17.61-8.28l-12.79,23.17c-.14.24-.26.48-.38.72-3.39,7.29,6.71,13.38,11.92,7.26l17.59-20.69Z" style="fill:url(#uuid-30648514-5669-4d95-96c9-4dab36559efb); opacity:.8; stroke-width:0px;"/><path d="m124.71,126.57s0-.01-.01-.02c0,0,0,0,0,0,0,0,0,.02,0,.02Z" style="fill:#2e4365; stroke-width:0px;"/><polygon points="136.69 172.48 113.82 172.48 53.53 59.58 73.24 59.58 136.69 172.48" style="fill:url(#uuid-ab3522e9-c0cd-4474-8859-9f36cdf98f81); opacity:.75; stroke-width:0px;"/><path d="m73.39,59.58l33.44,59.63c2.82,7.65-7.17,26.67-12.28,17.06L53.53,59.58h19.86Z" style="fill:url(#uuid-4f227c7d-7a65-4e56-9321-c6145c47210b); stroke-width:0px;"/><path d="m115.87,135.34l18.22,32.5s-36.61-14.66-18.22-32.5Z" style="fill:#4977a4; stroke-width:0px;"/></svg>
@@ -0,0 +1,3 @@
1
+ import { createPinia } from 'pinia';
2
+ export const pinia = createPinia();
3
+ export default pinia;
@@ -2,7 +2,8 @@
2
2
  import { computed } from 'vue';
3
3
  import { useSiteConfig } from '../config/use-site-config';
4
4
  import { useI18n } from '../i18n';
5
- import glossaristLogo from '../assets/glossarist-logo.svg';
5
+
6
+ const glossaristLogo = new URL('../assets/glossarist-logo.svg', import.meta.url).href;
6
7
 
7
8
  const { config } = useSiteConfig();
8
9
  const { t } = useI18n();
@@ -58,7 +59,7 @@ const ownerUrl = computed(() => config.value?.branding?.ownerUrl || '/');
58
59
  >{{ link.label }}</a>
59
60
  <span class="text-ink-200">|</span>
60
61
  <span class="text-xs inline-flex items-center gap-1.5">
61
- <img :src="glossaristLogo" alt="" class="w-4 h-4 opacity-80" />
62
+ <img :src="glossaristLogo" alt="Glossarist" class="w-4 h-4 opacity-80" />
62
63
  <a :href="poweredBy.url" target="_blank" rel="noopener" class="concept-link">{{ poweredBy.message }}</a>
63
64
  </span>
64
65
  </div>
@@ -6,6 +6,8 @@ import { useSiteConfig } from '../config/use-site-config';
6
6
  import { useI18n } from '../i18n';
7
7
  import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
8
8
 
9
+ const glossaristLogo = new URL('../assets/glossarist-logo.svg', import.meta.url).href;
10
+
9
11
  const router = useRouter();
10
12
  const ui = useUiStore();
11
13
  const store = useVocabularyStore();
@@ -76,13 +78,8 @@ onBeforeUnmount(() => document.removeEventListener('click', closeLangOnOutside))
76
78
  class="h-8 max-w-[48px] object-contain rounded"
77
79
  />
78
80
  </div>
79
- <div v-else class="w-8 h-8 rounded-lg flex items-center justify-center transition-colors" style="background-color: var(--brand-dark)">
80
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
81
- <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
82
- <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
83
- <line x1="9" y1="7" x2="17" y2="7"/>
84
- <line x1="9" y1="11" x2="15" y2="11"/>
85
- </svg>
81
+ <div v-else class="h-8 flex items-center">
82
+ <img :src="glossaristLogo" alt="Glossarist" class="h-8 max-w-[48px] object-contain rounded" />
86
83
  </div>
87
84
  <span class="font-serif text-lg text-ink-800 leading-none hidden sm:inline">{{ localizedTitle }}</span>
88
85
  </button>