@glossarist/concept-browser 0.7.22 → 0.7.23

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 (38) hide show
  1. package/index.html +2 -1
  2. package/package.json +1 -1
  3. package/scripts/build-edges.js +50 -5
  4. package/scripts/generate-data.mjs +33 -6
  5. package/src/App.vue +10 -12
  6. package/src/__tests__/concept-view.test.ts +7 -1
  7. package/src/__tests__/dataset-adapter.test.ts +87 -0
  8. package/src/__tests__/dataset-view.test.ts +1 -0
  9. package/src/__tests__/factory-lazy.test.ts +183 -0
  10. package/src/__tests__/graph-engine-fixes.test.ts +104 -0
  11. package/src/__tests__/ontology-registry.test.ts +4 -4
  12. package/src/__tests__/performance-v2.test.ts +77 -0
  13. package/src/__tests__/performance.test.ts +95 -0
  14. package/src/__tests__/search-utils.test.ts +59 -0
  15. package/src/__tests__/test-helpers.ts +4 -0
  16. package/src/__tests__/utils-barrel.test.ts +15 -0
  17. package/src/__tests__/vocabulary-layered.test.ts +291 -0
  18. package/src/adapters/DatasetAdapter.ts +41 -1
  19. package/src/adapters/factory.ts +35 -4
  20. package/src/adapters/ontology-registry.ts +1 -1
  21. package/src/adapters/types.ts +12 -0
  22. package/src/components/AppSidebar.vue +17 -343
  23. package/src/components/ConceptDetail.vue +124 -55
  24. package/src/components/GraphPanel.vue +14 -6
  25. package/src/components/OntologySidebarSection.vue +338 -0
  26. package/src/config/use-site-config.ts +20 -9
  27. package/src/data/taxonomies.json +12 -6
  28. package/src/directives/v-math.ts +2 -3
  29. package/src/graph/GraphEngine.ts +22 -5
  30. package/src/i18n/index.ts +1 -1
  31. package/src/stores/vocabulary.ts +65 -105
  32. package/src/utils/index.ts +1 -0
  33. package/src/utils/relationship-categories.ts +3 -2
  34. package/src/utils/search.ts +15 -0
  35. package/src/views/ConceptView.vue +0 -2
  36. package/src/views/DatasetView.vue +64 -39
  37. package/src/views/HomeView.vue +0 -1
  38. package/vite.config.ts +94 -6
@@ -0,0 +1,338 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue';
3
+ import { useRoute, useRouter } from 'vue-router';
4
+ import { useOntologyNav, compactToSlug } from '../composables/use-ontology-nav';
5
+
6
+ const router = useRouter();
7
+ const route = useRoute();
8
+
9
+ const {
10
+ expandedClasses,
11
+ collapsedSections,
12
+ searchQuery,
13
+ taxonomyKeys,
14
+ taxonomyLabels,
15
+ treeRoots,
16
+ allShapes,
17
+ objectProperties,
18
+ datatypeProperties,
19
+ annotationProperties,
20
+ groupedIndividuals,
21
+ totalIndividuals,
22
+ searchResults,
23
+ toggleExpand,
24
+ toggleSection,
25
+ childClasses,
26
+ hasChildren,
27
+ ENTITY_TYPE_META,
28
+ } = useOntologyNav();
29
+
30
+ const activeClassId = computed(() => {
31
+ if (route.name !== 'ontology-class') return null;
32
+ const slug = route.params.classId as string;
33
+ return slug.replace(/-/g, ':');
34
+ });
35
+
36
+ const activeTaxonomy = computed(() => {
37
+ if (route.name !== 'ontology-taxonomy') return null;
38
+ return route.params.taxonomyKey as string;
39
+ });
40
+
41
+ const activeShapeId = computed(() => {
42
+ if (route.name !== 'ontology-shape') return null;
43
+ const slug = route.params.shapeId as string;
44
+ return slug.replace(/-/g, ':');
45
+ });
46
+
47
+ const activePropertyId = computed(() => {
48
+ if (route.name !== 'ontology-property') return null;
49
+ const slug = route.params.propertyId as string;
50
+ return slug.replace(/-/g, ':');
51
+ });
52
+
53
+ const isOverview = computed(() => route.name === 'ontology');
54
+ const isSearching = computed(() => !!searchQuery.value.trim());
55
+
56
+ const ontologyExpanded = ref(true);
57
+
58
+ function selectClass(id: string) {
59
+ router.push(`/ontology/class/${compactToSlug(id)}`);
60
+ }
61
+
62
+ function selectTaxonomy(key: string) {
63
+ router.push(`/ontology/taxonomy/${key}`);
64
+ }
65
+
66
+ function selectShape(id: string) {
67
+ router.push(`/ontology/shape/${compactToSlug(id)}`);
68
+ }
69
+
70
+ function selectProperty(id: string) {
71
+ router.push(`/ontology/property/${compactToSlug(id)}`);
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <!-- Search input -->
77
+ <div class="relative mb-1.5">
78
+ <input
79
+ v-model="searchQuery"
80
+ type="text"
81
+ placeholder="Search entities..."
82
+ class="w-full text-[11px] px-2 py-1.5 rounded-md border border-ink-200/60 bg-surface text-ink-700 placeholder:text-ink-300 focus:outline-none focus:border-blue-300 focus:ring-1 focus:ring-blue-200"
83
+ />
84
+ <span v-if="searchResults" class="absolute right-2 top-1/2 -translate-y-1/2 text-[9px] text-ink-400">
85
+ {{ searchResults.total }}
86
+ </span>
87
+ </div>
88
+
89
+ <!-- Overview link -->
90
+ <router-link to="/ontology"
91
+ class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
92
+ :class="isOverview ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
93
+ >
94
+ <span class="w-3 text-ink-200">·</span>
95
+ <span class="flex-1 text-left">Overview</span>
96
+ </router-link>
97
+
98
+ <!-- Search results mode -->
99
+ <template v-if="isSearching && searchResults">
100
+ <div v-if="searchResults.classes.length" class="mt-1">
101
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-blue-500 font-medium">Classes ({{ searchResults.classes.length }})</div>
102
+ <button v-for="cls in searchResults.classes" :key="cls.compact"
103
+ @click="selectClass(cls.compact); searchQuery = ''"
104
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors text-ink-600 hover:bg-ink-50"
105
+ >
106
+ <span class="w-3 text-ink-200">·</span>
107
+ <span class="flex-1 text-left truncate">{{ cls.label }}</span>
108
+ <code class="text-[9px] text-ink-300">{{ cls.compact }}</code>
109
+ </button>
110
+ </div>
111
+ <div v-if="searchResults.objectProperties.length" class="mt-1">
112
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-emerald-500 font-medium">Object Properties ({{ searchResults.objectProperties.length }})</div>
113
+ <button v-for="p in searchResults.objectProperties" :key="p.compact"
114
+ @click="selectProperty(p.compact); searchQuery = ''"
115
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors text-ink-600 hover:bg-ink-50"
116
+ >
117
+ <span class="w-3 text-ink-200">·</span>
118
+ <span class="flex-1 text-left truncate">{{ p.label }}</span>
119
+ </button>
120
+ </div>
121
+ <div v-if="searchResults.datatypeProperties.length" class="mt-1">
122
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-amber-500 font-medium">Datatype Properties ({{ searchResults.datatypeProperties.length }})</div>
123
+ <button v-for="p in searchResults.datatypeProperties" :key="p.compact"
124
+ @click="selectProperty(p.compact); searchQuery = ''"
125
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors text-ink-600 hover:bg-ink-50"
126
+ >
127
+ <span class="w-3 text-ink-200">·</span>
128
+ <span class="flex-1 text-left truncate">{{ p.label }}</span>
129
+ </button>
130
+ </div>
131
+ <div v-if="searchResults.shapes.length" class="mt-1">
132
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-purple-500 font-medium">SHACL Shapes ({{ searchResults.shapes.length }})</div>
133
+ <button v-for="s in searchResults.shapes" :key="s.compact"
134
+ @click="selectShape(s.compact); searchQuery = ''"
135
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors text-ink-600 hover:bg-ink-50"
136
+ >
137
+ <span class="w-3 text-ink-200">·</span>
138
+ <span class="flex-1 text-left truncate">{{ s.label }}</span>
139
+ </button>
140
+ </div>
141
+ <div v-if="searchResults.individuals.length" class="mt-1">
142
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-rose-500 font-medium">Named Individuals ({{ searchResults.individuals.length }})</div>
143
+ <button v-for="ind in searchResults.individuals" :key="ind.group + '/' + ind.id"
144
+ @click="selectTaxonomy(ind.group); searchQuery = ''"
145
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors text-ink-600 hover:bg-ink-50"
146
+ >
147
+ <span class="w-3 text-ink-200">·</span>
148
+ <span class="flex-1 text-left truncate">{{ ind.prefLabel }}</span>
149
+ <span class="text-[9px] text-ink-300">{{ taxonomyLabels[ind.group] }}</span>
150
+ </button>
151
+ </div>
152
+ <div v-if="searchResults.annotationProperties.length" class="mt-1">
153
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-pink-500 font-medium">Annotation Properties ({{ searchResults.annotationProperties.length }})</div>
154
+ <div v-for="ap in searchResults.annotationProperties" :key="ap.compact"
155
+ class="px-2 py-0.5 text-[11px] text-ink-500"
156
+ >
157
+ <span class="w-3 inline-block text-ink-200">·</span>
158
+ {{ ap.compact }}
159
+ </div>
160
+ </div>
161
+ <div v-if="searchResults.total === 0" class="px-2 py-3 text-[11px] text-ink-300 italic">
162
+ No entities match "{{ searchQuery }}"
163
+ </div>
164
+ </template>
165
+
166
+ <!-- Normal browse mode -->
167
+ <template v-if="!isSearching">
168
+ <!-- Classes section -->
169
+ <div class="mt-1">
170
+ <button @click="toggleSection('class')"
171
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors"
172
+ >
173
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('class') ? '▸' : '▾' }}</span>
174
+ <span class="flex-1 text-left">Classes</span>
175
+ <span class="badge text-[9px] bg-blue-50 text-blue-600 px-1 py-0.5">{{ treeRoots.length }}+</span>
176
+ </button>
177
+ <div v-if="!collapsedSections.has('class')" class="mt-0.5 space-y-0">
178
+ <template v-for="root in treeRoots" :key="root.compact">
179
+ <button @click="selectClass(root.compact); toggleExpand(root)"
180
+ class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
181
+ :class="activeClassId === root.compact && !activeTaxonomy && !activeShapeId && !activePropertyId ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
182
+ >
183
+ <span v-if="hasChildren(root)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(root.compact) ? '▾' : '▸' }}</span>
184
+ <span v-else class="w-3 text-ink-200">·</span>
185
+ <span class="flex-1 text-left">{{ root.label }}</span>
186
+ </button>
187
+ <div v-if="expandedClasses.has(root.compact) && hasChildren(root)" class="ml-3">
188
+ <template v-for="child in childClasses(root.compact)" :key="child.compact">
189
+ <button @click="selectClass(child.compact); toggleExpand(child)"
190
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
191
+ :class="activeClassId === child.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
192
+ >
193
+ <span v-if="hasChildren(child)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(child.compact) ? '▾' : '▸' }}</span>
194
+ <span v-else class="w-3 text-ink-200">·</span>
195
+ <span class="flex-1 text-left">{{ child.label }}</span>
196
+ </button>
197
+ <div v-if="expandedClasses.has(child.compact) && hasChildren(child)" class="ml-3">
198
+ <button v-for="gc in childClasses(child.compact)" :key="gc.compact"
199
+ @click="selectClass(gc.compact)"
200
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
201
+ :class="activeClassId === gc.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
202
+ >
203
+ <span class="w-3 text-ink-200">·</span>
204
+ <span class="flex-1 text-left">{{ gc.label }}</span>
205
+ </button>
206
+ </div>
207
+ </template>
208
+ </div>
209
+ </template>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- Object Properties section -->
214
+ <div class="mt-1">
215
+ <button @click="toggleSection('objectProperty')"
216
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors"
217
+ >
218
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('objectProperty') ? '▸' : '▾' }}</span>
219
+ <span class="flex-1 text-left">Object Properties</span>
220
+ <span class="badge text-[9px] bg-emerald-50 text-emerald-600 px-1 py-0.5">{{ objectProperties.length }}</span>
221
+ </button>
222
+ <div v-if="!collapsedSections.has('objectProperty')" class="mt-0.5 max-h-40 overflow-y-auto">
223
+ <button v-for="p in objectProperties" :key="p.compact"
224
+ @click="selectProperty(p.compact)"
225
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
226
+ :class="activePropertyId === p.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
227
+ >
228
+ <span class="w-3 text-ink-200">·</span>
229
+ <span class="flex-1 text-left truncate">{{ p.label }}</span>
230
+ </button>
231
+ </div>
232
+ </div>
233
+
234
+ <!-- Datatype Properties section -->
235
+ <div class="mt-1">
236
+ <button @click="toggleSection('datatypeProperty')"
237
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors"
238
+ >
239
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('datatypeProperty') ? '▸' : '▾' }}</span>
240
+ <span class="flex-1 text-left">Datatype Properties</span>
241
+ <span class="badge text-[9px] bg-amber-50 text-amber-600 px-1 py-0.5">{{ datatypeProperties.length }}</span>
242
+ </button>
243
+ <div v-if="!collapsedSections.has('datatypeProperty')" class="mt-0.5 max-h-40 overflow-y-auto">
244
+ <button v-for="p in datatypeProperties" :key="p.compact"
245
+ @click="selectProperty(p.compact)"
246
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
247
+ :class="activePropertyId === p.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
248
+ >
249
+ <span class="w-3 text-ink-200">·</span>
250
+ <span class="flex-1 text-left truncate">{{ p.label }}</span>
251
+ </button>
252
+ </div>
253
+ </div>
254
+
255
+ <!-- SHACL Shapes section -->
256
+ <div class="mt-1">
257
+ <button @click="toggleSection('shape')"
258
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors"
259
+ >
260
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('shape') ? '▸' : '▾' }}</span>
261
+ <span class="flex-1 text-left">SHACL Shapes</span>
262
+ <span class="badge text-[9px] bg-purple-50 text-purple-600 px-1 py-0.5">{{ allShapes.length }}</span>
263
+ </button>
264
+ <div v-if="!collapsedSections.has('shape')" class="mt-0.5 max-h-40 overflow-y-auto">
265
+ <button v-for="s in allShapes" :key="s.compact"
266
+ @click="selectShape(s.compact)"
267
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
268
+ :class="activeShapeId === s.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
269
+ >
270
+ <span class="w-3 text-ink-200">·</span>
271
+ <span class="flex-1 text-left truncate">{{ s.label }}</span>
272
+ </button>
273
+ </div>
274
+ </div>
275
+
276
+ <!-- Named Individuals section -->
277
+ <div class="mt-1">
278
+ <button @click="toggleSection('namedIndividual')"
279
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors"
280
+ >
281
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('namedIndividual') ? '▸' : '▾' }}</span>
282
+ <span class="flex-1 text-left">Named Individuals</span>
283
+ <span class="badge text-[9px] bg-rose-50 text-rose-600 px-1 py-0.5">{{ totalIndividuals }}</span>
284
+ </button>
285
+ <div v-if="!collapsedSections.has('namedIndividual')" class="mt-0.5 max-h-64 overflow-y-auto">
286
+ <template v-for="group in groupedIndividuals" :key="group.key">
287
+ <button @click="selectTaxonomy(group.key)"
288
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-300 hover:text-ink-500 transition-colors"
289
+ >
290
+ <span class="w-3 text-ink-200">·</span>
291
+ <span class="flex-1 text-left">{{ group.label }}</span>
292
+ <span class="text-[9px] text-ink-300">{{ group.concepts.length }}</span>
293
+ </button>
294
+ </template>
295
+ </div>
296
+ </div>
297
+
298
+ <!-- SKOS Taxonomies section -->
299
+ <div class="mt-1">
300
+ <button @click="toggleSection('taxonomy')"
301
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors"
302
+ >
303
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('taxonomy') ? '▸' : '▾' }}</span>
304
+ <span class="flex-1 text-left">SKOS Taxonomies</span>
305
+ <span class="badge text-[9px] bg-rose-50 text-rose-600 px-1 py-0.5">{{ taxonomyKeys.length }}</span>
306
+ </button>
307
+ <div v-if="!collapsedSections.has('taxonomy')" class="mt-0.5">
308
+ <button v-for="tk in taxonomyKeys" :key="tk"
309
+ @click="selectTaxonomy(tk)"
310
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
311
+ :class="activeTaxonomy === tk ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
312
+ >
313
+ <span class="w-3 text-ink-200">·</span>
314
+ <span class="flex-1 text-left">{{ taxonomyLabels[tk] }}</span>
315
+ </button>
316
+ </div>
317
+ </div>
318
+
319
+ <!-- Annotation Properties section -->
320
+ <div class="mt-1">
321
+ <button @click="toggleSection('annotationProperty')"
322
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors"
323
+ >
324
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('annotationProperty') ? '▸' : '▾' }}</span>
325
+ <span class="flex-1 text-left">Annotation Properties</span>
326
+ <span class="badge text-[9px] bg-pink-50 text-pink-600 px-1 py-0.5">{{ annotationProperties.length }}</span>
327
+ </button>
328
+ <div v-if="!collapsedSections.has('annotationProperty')" class="mt-0.5">
329
+ <div v-for="ap in annotationProperties" :key="ap.compact"
330
+ class="w-full flex items-center gap-1.5 px-2 py-1 text-[11px] text-ink-500"
331
+ >
332
+ <span class="w-3 text-ink-200">·</span>
333
+ <code class="text-ink-400">{{ ap.compact }}</code>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </template>
338
+ </template>
@@ -45,21 +45,27 @@ const loaded = ref(false);
45
45
 
46
46
  function loadFont(font: { family: string; source: string; weights?: number[]; url?: string }) {
47
47
  if (font.source === 'google') {
48
+ const familySlug = font.family.replace(/ /g, '+');
49
+ // Match by family name substring — build-time HTML uses combined URLs
50
+ if (document.querySelector(`link[href*="family=${familySlug}"]`)) return;
51
+
48
52
  const w = (font.weights || [400, 700]).join(';');
49
- const href = `https://fonts.googleapis.com/css2?family=${font.family.replace(/ /g, '+')}:wght@${w}&display=swap`;
50
- const existing = document.querySelector(`link[href="${href}"]`);
51
- if (existing) return;
53
+ const href = `https://fonts.googleapis.com/css2?family=${familySlug}:wght@${w}&display=swap`;
52
54
  const link = document.createElement('link');
53
- link.rel = 'stylesheet';
55
+ link.rel = 'preload';
56
+ link.as = 'style';
54
57
  link.href = href;
58
+ link.onload = () => { link.rel = 'stylesheet'; };
55
59
  document.head.appendChild(link);
56
60
  }
57
61
  if (font.source === 'url' && font.url) {
58
62
  const existing = document.querySelector(`link[href="${font.url}"]`);
59
63
  if (existing) return;
60
64
  const link = document.createElement('link');
61
- link.rel = 'stylesheet';
65
+ link.rel = 'preload';
66
+ link.as = 'style';
62
67
  link.href = font.url;
68
+ link.onload = () => { link.rel = 'stylesheet'; };
63
69
  document.head.appendChild(link);
64
70
  }
65
71
  }
@@ -100,11 +106,16 @@ function applyBranding(config: RuntimeSiteConfig) {
100
106
  async function loadConfig(): Promise<RuntimeSiteConfig | null> {
101
107
  if (loaded.value) return siteConfig.value;
102
108
  try {
103
- const resp = await fetch(`${import.meta.env.BASE_URL}site-config.json`);
104
- if (resp.ok) {
105
- siteConfig.value = await resp.json();
106
- if (siteConfig.value) applyBranding(siteConfig.value);
109
+ const inline = document.getElementById('site-config-json');
110
+ if (inline?.textContent) {
111
+ siteConfig.value = JSON.parse(inline.textContent);
112
+ } else {
113
+ const resp = await fetch(`${import.meta.env.BASE_URL}site-config.json`);
114
+ if (resp.ok) {
115
+ siteConfig.value = await resp.json();
116
+ }
107
117
  }
118
+ if (siteConfig.value) applyBranding(siteConfig.value);
108
119
  } catch {
109
120
  // Non-critical
110
121
  }
@@ -489,23 +489,29 @@
489
489
  "prefLabel": "references",
490
490
  "definition": "This concept references another concept."
491
491
  },
492
- "sequentially_related_concept": {
493
- "id": "sequentially_related_concept",
492
+ "sequentially_related": {
493
+ "id": "sequentially_related",
494
494
  "iri": "gloss:rel/sequentially_related",
495
495
  "prefLabel": "sequentially related",
496
496
  "definition": "Sequential spatiotemporal relationship (ISO 25964 / TBX)."
497
497
  },
498
- "spatially_related_concept": {
499
- "id": "spatially_related_concept",
498
+ "spatially_related": {
499
+ "id": "spatially_related",
500
500
  "iri": "gloss:rel/spatially_related",
501
501
  "prefLabel": "spatially related",
502
502
  "definition": "Spatial relationship (ISO 25964 / TBX)."
503
503
  },
504
- "temporally_related_concept": {
505
- "id": "temporally_related_concept",
504
+ "temporally_related": {
505
+ "id": "temporally_related",
506
506
  "iri": "gloss:rel/temporally_related",
507
507
  "prefLabel": "temporally related",
508
508
  "definition": "Temporal relationship (ISO 25964 / TBX)."
509
+ },
510
+ "exact_match": {
511
+ "id": "exact_match",
512
+ "iri": "gloss:rel/exact_match",
513
+ "prefLabel": "exact match",
514
+ "definition": "Cross-vocabulary exact equivalence (SKOS skos:exactMatch)."
509
515
  }
510
516
  }
511
517
  },
@@ -4,8 +4,7 @@ import { loadPlurimath, mathToHtml } from '../utils/plurimath';
4
4
  let loaded = false;
5
5
 
6
6
  function upgrade(el: HTMLElement) {
7
- const pending = el.querySelectorAll('.math-pending');
8
- if (!pending.length) return;
7
+ if (!el.querySelector('.math-pending')) return;
9
8
 
10
9
  if (!loaded) {
11
10
  loadPlurimath().then(() => {
@@ -15,7 +14,7 @@ function upgrade(el: HTMLElement) {
15
14
  return;
16
15
  }
17
16
 
18
- pending.forEach((span) => {
17
+ el.querySelectorAll('.math-pending').forEach((span) => {
19
18
  const expr = (span as HTMLElement).dataset.expr;
20
19
  const format = (span as HTMLElement).dataset.format || 'asciimath';
21
20
  const bold = span.classList.contains('math-bold');
@@ -72,15 +72,23 @@ export class GraphEngine {
72
72
  if (from) {
73
73
  const adj = this.adjacency.get(from);
74
74
  if (!adj) return [];
75
- return [...adj.values()].flat();
75
+ const result: GraphEdge[] = [];
76
+ for (const list of adj.values()) {
77
+ for (const e of list) result.push(e);
78
+ }
79
+ return result;
76
80
  }
77
- return this.edges;
81
+ return [...this.edges];
78
82
  }
79
83
 
80
84
  getIncomingEdges(uri: string): GraphEdge[] {
81
85
  const adj = this.reverseAdjacency.get(uri);
82
86
  if (!adj) return [];
83
- return [...adj.values()].flat();
87
+ const result: GraphEdge[] = [];
88
+ for (const list of adj.values()) {
89
+ for (const e of list) result.push(e);
90
+ }
91
+ return result;
84
92
  }
85
93
 
86
94
  getNeighbors(uri: string): { outgoing: string[]; incoming: string[] } {
@@ -100,9 +108,10 @@ export class GraphEngine {
100
108
  const collectedNodes: GraphNode[] = [];
101
109
  const collectedEdges: GraphEdge[] = [];
102
110
  const queue: { uri: string; d: number }[] = [{ uri: rootUri, d: 0 }];
111
+ let head = 0;
103
112
 
104
- while (queue.length > 0) {
105
- const { uri, d } = queue.shift()!;
113
+ while (head < queue.length) {
114
+ const { uri, d } = queue[head++];
106
115
  if (visited.has(uri) || d > depth) continue;
107
116
  visited.add(uri);
108
117
 
@@ -137,4 +146,12 @@ export class GraphEngine {
137
146
  get edgeCount(): number {
138
147
  return this.edges.length;
139
148
  }
149
+
150
+ clear(): void {
151
+ this.nodes.clear();
152
+ this.edges.length = 0;
153
+ this.edgeKeys.clear();
154
+ this.adjacency.clear();
155
+ this.reverseAdjacency.clear();
156
+ }
140
157
  }
package/src/i18n/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ref } from 'vue';
2
+ import { DEFAULT_LANG } from '../utils/lang';
2
3
 
3
4
  // Auto-discover all locale YAML files — adding a new .yml file is all that's needed
4
5
  const localeModules = import.meta.glob<{ default: Record<string, string> }>('./locales/*.yml', { eager: true });
@@ -14,7 +15,6 @@ for (const path of Object.keys(localeModules)) {
14
15
  }
15
16
  }
16
17
 
17
- const DEFAULT_LANG = 'eng';
18
18
  const stored = typeof localStorage !== 'undefined'
19
19
  ? (localStorage.getItem('ui-lang') || DEFAULT_LANG)
20
20
  : DEFAULT_LANG;