@glossarist/concept-browser 0.5.0 → 0.6.0

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 (46) hide show
  1. package/README.md +32 -0
  2. package/cli/index.mjs +21 -0
  3. package/package.json +1 -1
  4. package/scripts/generate-data.mjs +43 -1
  5. package/scripts/generate-ontology-schema.mjs +312 -10
  6. package/src/App.vue +3 -0
  7. package/src/__tests__/concept-card.test.ts +16 -2
  8. package/src/__tests__/markdown-lite.test.ts +26 -0
  9. package/src/adapters/factory.ts +3 -2
  10. package/src/adapters/model-bridge.ts +2 -0
  11. package/src/adapters/ontology-schema.ts +89 -4
  12. package/src/adapters/types.ts +1 -0
  13. package/src/components/AppFooter.vue +3 -1
  14. package/src/components/AppHeader.vue +46 -4
  15. package/src/components/AppSidebar.vue +286 -46
  16. package/src/components/ConceptCard.vue +16 -4
  17. package/src/components/ConceptDetail.vue +42 -35
  18. package/src/components/ConceptRdfView.vue +3 -3
  19. package/src/components/ConceptTimeline.vue +2 -14
  20. package/src/components/GraphPanel.vue +19 -0
  21. package/src/components/LanguageDetail.vue +11 -8
  22. package/src/composables/use-ontology-nav.ts +183 -13
  23. package/src/composables/use-render-options.ts +2 -2
  24. package/src/config/types.ts +1 -0
  25. package/src/config/use-site-config.ts +3 -2
  26. package/src/data/ontology-schema.json +1721 -153
  27. package/src/i18n/index.ts +49 -0
  28. package/src/i18n/locales/eng.yml +66 -0
  29. package/src/i18n/locales/fra.yml +66 -0
  30. package/src/router/index.ts +10 -0
  31. package/src/shims/glossarist-tags.ts +10 -0
  32. package/src/stores/vocabulary.ts +1 -1
  33. package/src/style.css +12 -0
  34. package/src/utils/lang.ts +13 -0
  35. package/src/utils/markdown-lite.ts +15 -0
  36. package/src/views/AboutView.vue +1 -1
  37. package/src/views/ContributorsView.vue +1 -1
  38. package/src/views/DatasetView.vue +77 -6
  39. package/src/views/HomeView.vue +21 -17
  40. package/src/views/NewsView.vue +2 -2
  41. package/src/views/OntologySchemaView.vue +331 -14
  42. package/src/views/PageView.vue +27 -11
  43. package/src/views/ResolveView.vue +1 -1
  44. package/src/views/SearchView.vue +4 -2
  45. package/src/views/StatsView.vue +1 -1
  46. package/vite.config.ts +34 -1
@@ -2,7 +2,7 @@
2
2
  import type { Concept, LocalizedConcept, Designation, Expression, ConceptSource } from 'glossarist';
3
3
  import type { Manifest, GraphEdge } from '../adapters/types';
4
4
  import { computed, ref, nextTick, watch } from 'vue';
5
- import { langName, langLabel } from '../utils/lang';
5
+ import { langName, langLabel, sortLanguages } from '../utils/lang';
6
6
  import { renderMath, cleanContent } from '../utils/math';
7
7
  import type { RenderOptions } from '../utils/math';
8
8
  import { escapeAttr } from '../utils/escape';
@@ -15,11 +15,15 @@ import { useDsStyle } from '../utils/dataset-style';
15
15
  import { getFactory } from '../adapters/factory';
16
16
  import { useRenderOptions } from '../composables/use-render-options';
17
17
  import { categorizeRelationship, relationshipLabel, relationshipDefinition } from '../utils/relationship-categories';
18
+ import { useSiteConfig } from '../config/use-site-config';
18
19
  import ConceptTimeline from './ConceptTimeline.vue';
19
20
  import ConceptRdfView from './ConceptRdfView.vue';
20
21
  import FormatDownloads from './FormatDownloads.vue';
21
22
  import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
22
23
  import CitationDisplay from './CitationDisplay.vue';
24
+ import { useI18n } from '../i18n';
25
+
26
+ const { t } = useI18n();
23
27
 
24
28
  const props = defineProps<{
25
29
  concept: Concept;
@@ -32,6 +36,7 @@ const props = defineProps<{
32
36
  const router = useRouter();
33
37
  const store = useVocabularyStore();
34
38
  const { getColor } = useDsStyle();
39
+ const { config: siteConfig } = useSiteConfig();
35
40
  const factory = getFactory();
36
41
 
37
42
  const activeTab = ref<'rdf' | 'definition' | 'history'>('definition');
@@ -57,35 +62,12 @@ function copyUri() {
57
62
  }
58
63
 
59
64
  const languages = computed(() => {
60
- const order = props.manifest.languageOrder;
61
- const keys = props.concept.languages;
62
- if (!order) {
63
- return [...keys].sort((a, b) => {
64
- if (a === 'eng') return -1;
65
- if (a === 'eng') return 1;
66
- return a.localeCompare(b);
67
- });
68
- }
69
- const orderIndex = new Map(order.map((lang, i) => [lang, i]));
70
- return [...keys].sort((a, b) => {
71
- const ai = orderIndex.get(a) ?? order.length;
72
- const bi = orderIndex.get(b) ?? order.length;
73
- if (ai !== bi) return ai - bi;
74
- return a.localeCompare(b);
75
- });
65
+ return sortLanguages(props.concept.languages, props.manifest.languageOrder);
76
66
  });
77
67
 
78
- // Collapsible language sections — auto-collapse non-eng when 6+ languages
68
+ // Collapsible language sections — expand all with content, collapse those without
79
69
  const collapsedLangs = ref(new Set<string>());
80
70
 
81
- function initCollapsed(langs: string[]) {
82
- if (langs.length >= 6) {
83
- collapsedLangs.value = new Set(langs.filter(l => l !== 'eng'));
84
- }
85
- }
86
-
87
- watch(languages, (langs) => { initCollapsed(langs); }, { immediate: true });
88
-
89
71
  const engConcept = computed((): LocalizedConcept | null => {
90
72
  return props.concept.localization('eng') ?? null;
91
73
  });
@@ -104,6 +86,9 @@ const conceptDates = computed(() => props.concept.dates);
104
86
  // Managed concept sources (distinct from localized sources)
105
87
  const conceptSources = computed(() => props.concept.sources);
106
88
 
89
+ // Managed concept tags
90
+ const conceptTags = computed(() => props.concept.tags ?? []);
91
+
107
92
  // Cross-reference resolver: generates clickable links for inline refs
108
93
 
109
94
  const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
@@ -192,6 +177,20 @@ function hasContent(lc: LangContent): boolean {
192
177
  return !!(lc.definition || lc.notes.length || lc.examples.length || lc.sources.length);
193
178
  }
194
179
 
180
+ function initCollapsed() {
181
+ const mainLangs = siteConfig.value?.defaults?.mainLanguages || [];
182
+ const mainSet = new Set(mainLangs.length ? mainLangs : ['eng']);
183
+ const collapsed = new Set<string>();
184
+ for (const lc of allLangContent.value) {
185
+ if (!hasContent(lc) && !mainSet.has(lc.lang)) {
186
+ collapsed.add(lc.lang);
187
+ }
188
+ }
189
+ collapsedLangs.value = collapsed;
190
+ }
191
+
192
+ watch(languages, () => { initCollapsed(); }, { immediate: true });
193
+
195
194
  const allCollapsed = computed(() => collapsedLangs.value.size === allLangContent.value.length);
196
195
 
197
196
  function toggleLang(lang: string) {
@@ -567,7 +566,7 @@ const nonVerbalReps = computed(() => {
567
566
  <!-- Notes -->
568
567
  <div v-if="lc.notes.length" class="space-y-2">
569
568
  <div v-for="(note, i) in lc.notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
570
- <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">Note {{ i + 1 }}</span>
569
+ <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.note') }} {{ i + 1 }}</span>
571
570
  <div class="mt-1" v-html="renderMath(note, renderOpts)"></div>
572
571
  </div>
573
572
  </div>
@@ -575,7 +574,7 @@ const nonVerbalReps = computed(() => {
575
574
  <!-- Examples -->
576
575
  <div v-if="lc.examples.length" class="space-y-2">
577
576
  <div v-for="(ex, i) in lc.examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
578
- <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">Example {{ i + 1 }}</span>
577
+ <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ i + 1 }}</span>
579
578
  <div class="mt-1" v-html="renderMath(ex, renderOpts)"></div>
580
579
  </div>
581
580
  </div>
@@ -639,9 +638,9 @@ const nonVerbalReps = computed(() => {
639
638
  <div class="w-full lg:w-64 flex-shrink-0 space-y-4 mt-6 lg:mt-0">
640
639
  <!-- Relations -->
641
640
  <div v-if="outgoingEdges.length || incomingEdges.length" class="card p-5">
642
- <div class="section-label">Relations</div>
641
+ <div class="section-label">{{ t('concept.relations') }}</div>
643
642
  <div v-if="outgoingEdges.length" class="mt-3">
644
- <div class="text-xs text-ink-300 mb-2">Outgoing ({{ outgoingEdges.length }})</div>
643
+ <div class="text-xs text-ink-300 mb-2">{{ t('concept.outgoing') }} ({{ outgoingEdges.length }})</div>
645
644
  <div class="space-y-1 max-h-64 overflow-y-auto">
646
645
  <button
647
646
  v-for="edge in outgoingEdges"
@@ -658,7 +657,7 @@ const nonVerbalReps = computed(() => {
658
657
  </div>
659
658
  </div>
660
659
  <div v-if="incomingEdges.length" class="mt-3 pt-3 border-t border-ink-100/60">
661
- <div class="text-xs text-ink-300 mb-2">Incoming ({{ incomingEdges.length }})</div>
660
+ <div class="text-xs text-ink-300 mb-2">{{ t('concept.incoming') }} ({{ incomingEdges.length }})</div>
662
661
  <div class="space-y-1 max-h-48 overflow-y-auto">
663
662
  <button
664
663
  v-for="edge in incomingEdges"
@@ -677,7 +676,7 @@ const nonVerbalReps = computed(() => {
677
676
 
678
677
  <!-- Domains -->
679
678
  <div v-if="conceptDomains.length" class="card p-5">
680
- <div class="section-label">Domains</div>
679
+ <div class="section-label">{{ t('concept.domains') }}</div>
681
680
  <div class="space-y-1 mt-3">
682
681
  <div v-for="domain in conceptDomains" :key="domain.slug" class="flex items-center gap-1.5 text-sm">
683
682
  <span class="w-2 h-1.5 rounded inline-block flex-shrink-0" style="background: #8b5cf6;"></span>
@@ -690,9 +689,17 @@ const nonVerbalReps = computed(() => {
690
689
  </div>
691
690
  </div>
692
691
 
692
+ <!-- Tags -->
693
+ <div v-if="conceptTags.length" class="card p-5">
694
+ <div class="section-label">{{ t('concept.tags') }}</div>
695
+ <div class="flex flex-wrap gap-1.5 mt-3">
696
+ <span v-for="tag in conceptTags" :key="tag" class="badge badge-gray text-[10px]">{{ tag }}</span>
697
+ </div>
698
+ </div>
699
+
693
700
  <!-- Managed concept dates -->
694
701
  <div v-if="conceptDates.length" class="card p-5">
695
- <div class="section-label">Lifecycle dates</div>
702
+ <div class="section-label">{{ t('concept.lifecycleDates') }}</div>
696
703
  <dl class="mt-3 space-y-1.5 text-xs">
697
704
  <div v-for="(d, i) in conceptDates" :key="i" class="flex gap-2">
698
705
  <dt class="text-ink-300 min-w-[70px]">{{ d.type }}</dt>
@@ -703,7 +710,7 @@ const nonVerbalReps = computed(() => {
703
710
 
704
711
  <!-- Managed concept sources -->
705
712
  <div v-if="conceptSources.length" class="card p-5">
706
- <div class="section-label">Concept sources</div>
713
+ <div class="section-label">{{ t('concept.conceptSources') }}</div>
707
714
  <div class="space-y-2 mt-3">
708
715
  <div v-for="(src, i) in conceptSources" :key="i" class="text-xs">
709
716
  <div class="flex items-center gap-1.5 flex-wrap mb-0.5">
@@ -753,7 +760,7 @@ const nonVerbalReps = computed(() => {
753
760
 
754
761
  <!-- Metadata -->
755
762
  <div class="card p-5">
756
- <div class="section-label">Metadata</div>
763
+ <div class="section-label">{{ t('concept.metadata') }}</div>
757
764
  <dl class="space-y-2 text-xs mt-3">
758
765
  <div v-if="managedStatus">
759
766
  <dt class="text-ink-300">Status</dt>
@@ -333,8 +333,8 @@ const rdfSource = computed(() => rdfFormat.value === 'turtle' ? turtleSource.val
333
333
  </button>
334
334
  </div>
335
335
  <div class="flex gap-1.5 mt-2.5">
336
- <span class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-blue-50 text-blue-700 border border-blue-100">gloss:Concept</span>
337
- <span class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-100">skos:Concept</span>
336
+ <router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-blue-50 text-blue-700 border border-blue-100 hover:bg-blue-100 transition-colors">gloss:Concept</router-link>
337
+ <router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-100 hover:bg-emerald-100 transition-colors">skos:Concept</router-link>
338
338
  </div>
339
339
  </div>
340
340
  </div>
@@ -356,7 +356,7 @@ const rdfSource = computed(() => rdfFormat.value === 'turtle' ? turtleSource.val
356
356
  <div v-for="(section, si) in sections" :key="si" class="card p-5">
357
357
  <div class="flex items-center gap-2 mb-3">
358
358
  <div class="w-1 h-4 rounded-full" :class="section.classId === 'gloss:Concept' ? 'bg-blue-500' : section.classId === 'gloss:LocalizedConcept' ? 'bg-emerald-500' : 'bg-amber-500'"></div>
359
- <code class="text-xs font-semibold text-ink-700">{{ section.classId }}</code>
359
+ <router-link :to="`/ontology/class/${section.classId.replace(/:/g, '-')}`" class="text-xs font-semibold text-ink-700 hover:text-blue-600 transition-colors">{{ section.classId }}</router-link>
360
360
  <span class="text-xs text-ink-400">·</span>
361
361
  <span class="text-xs text-ink-500">{{ section.label }}</span>
362
362
  </div>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { Concept, LocalizedConcept } from 'glossarist';
3
3
  import { computed } from 'vue';
4
- import { langName, langLabel } from '../utils/lang';
4
+ import { langName, langLabel, sortLanguages } from '../utils/lang';
5
5
  import { entryStatusColor } from '../utils/concept-helpers';
6
6
 
7
7
  const props = defineProps<{
@@ -141,19 +141,7 @@ const languagesWithHistory = computed(() => {
141
141
  langs.push(lang);
142
142
  }
143
143
  }
144
- const order = props.languageOrder;
145
- if (order) {
146
- const orderIndex = new Map(order.map((l, i) => [l, i]));
147
- langs.sort((a, b) => {
148
- const ai = orderIndex.get(a) ?? order.length;
149
- const bi = orderIndex.get(b) ?? order.length;
150
- if (ai !== bi) return ai - bi;
151
- return a.localeCompare(b);
152
- });
153
- } else {
154
- langs.sort();
155
- }
156
- return langs;
144
+ return sortLanguages(langs, props.languageOrder);
157
145
  });
158
146
 
159
147
  function formatDate(isoDate: string): string {
@@ -115,6 +115,7 @@ interface SimNode extends SimulationNodeDatum {
115
115
  register: string;
116
116
  conceptId: string;
117
117
  designation: string;
118
+ altDesignations: string[];
118
119
  hasDesignation: boolean;
119
120
  loaded: boolean;
120
121
  nodeType?: 'concept' | 'domain';
@@ -178,11 +179,15 @@ function buildSimulation(width: number, height: number) {
178
179
  const simNodes: SimNode[] = renderNodes.map(n => {
179
180
  const lang = uiStore.selectedLang;
180
181
  const desig = n.designations[lang] || Object.values(n.designations)[0] || '';
182
+ const alts = Object.entries(n.designations)
183
+ .filter(([l, t]) => l !== lang && t && t !== desig)
184
+ .map(([, t]) => t);
181
185
  return {
182
186
  uri: n.uri,
183
187
  register: n.register,
184
188
  conceptId: n.conceptId,
185
189
  designation: desig,
190
+ altDesignations: alts,
186
191
  hasDesignation: !!n.designations[lang],
187
192
  loaded: n.loaded,
188
193
  nodeType: n.nodeType,
@@ -272,6 +277,20 @@ function buildSimulation(width: number, height: number) {
272
277
  return '#636588';
273
278
  });
274
279
 
280
+ conceptNodes.append('text')
281
+ .attr('dy', -9)
282
+ .attr('y', 7)
283
+ .attr('text-anchor', 'middle')
284
+ .attr('font-size', '6px')
285
+ .attr('font-family', '"DM Sans", system-ui, sans-serif')
286
+ .attr('fill', '#a0a1b5')
287
+ .attr('pointer-events', 'none')
288
+ .text(d => {
289
+ if (labelMode.value !== 'designation') return '';
290
+ const alts = d.altDesignations.slice(0, 2);
291
+ return alts.join(' · ');
292
+ });
293
+
275
294
  const dragBehavior = drag<SVGGElement, SimNode>()
276
295
  .on('start', (event: D3DragEvent<SVGGElement, SimNode, SimNode>, d) => {
277
296
  if (!event.active) simulation?.alphaTarget(0.3).restart();
@@ -11,6 +11,9 @@ import { useRouter } from 'vue-router';
11
11
  import { useVocabularyStore } from '../stores/vocabulary';
12
12
  import { getFactory } from '../adapters/factory';
13
13
  import CitationDisplay from './CitationDisplay.vue';
14
+ import { useI18n } from '../i18n';
15
+
16
+ const { t } = useI18n();
14
17
 
15
18
  const props = defineProps<{
16
19
  concept: Concept;
@@ -109,7 +112,7 @@ function handleContentClick(e: MouseEvent) {
109
112
 
110
113
  <!-- Designations -->
111
114
  <div v-if="designations.length > 0" class="card p-5">
112
- <div class="section-label">Designations</div>
115
+ <div class="section-label">{{ t('concept.designations') }}</div>
113
116
  <div class="space-y-2 mt-3">
114
117
  <div v-for="(d, i) in designations" :key="i" class="flex items-center gap-2 flex-wrap">
115
118
  <span class="font-medium text-ink-800 text-lg" v-html="renderMath(d.designation)"></span>
@@ -138,16 +141,16 @@ function handleContentClick(e: MouseEvent) {
138
141
 
139
142
  <!-- Definition -->
140
143
  <div v-if="definition" class="card p-5">
141
- <div class="section-label">Definition</div>
144
+ <div class="section-label">{{ t('concept.definition') }}</div>
142
145
  <div class="text-ink-800 leading-relaxed mt-3" v-html="renderMath(definition, renderOpts)"></div>
143
146
  </div>
144
147
 
145
148
  <!-- Notes -->
146
149
  <div v-if="notes.length" class="card p-5">
147
- <div class="section-label">Notes</div>
150
+ <div class="section-label">{{ t('concept.notes') }}</div>
148
151
  <div class="space-y-3 mt-3">
149
152
  <div v-for="(note, i) in notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
150
- <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">Note {{ i + 1 }}</span>
153
+ <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.note') }} {{ i + 1 }}</span>
151
154
  <div class="mt-1" v-html="renderMath(note, renderOpts)"></div>
152
155
  </div>
153
156
  </div>
@@ -155,10 +158,10 @@ function handleContentClick(e: MouseEvent) {
155
158
 
156
159
  <!-- Examples -->
157
160
  <div v-if="examples.length" class="card p-5">
158
- <div class="section-label">Examples</div>
161
+ <div class="section-label">{{ t('concept.examples') }}</div>
159
162
  <div class="space-y-3 mt-3">
160
163
  <div v-for="(ex, i) in examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
161
- <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">Example {{ i + 1 }}</span>
164
+ <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ i + 1 }}</span>
162
165
  <div class="mt-1" v-html="renderMath(ex, renderOpts)"></div>
163
166
  </div>
164
167
  </div>
@@ -187,7 +190,7 @@ function handleContentClick(e: MouseEvent) {
187
190
  <span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langLabel(activeLang) }}</span>
188
191
  </div>
189
192
  <div>
190
- <p class="text-sm text-ink-600 font-medium">Term only in {{ langName(activeLang) }}</p>
193
+ <p class="text-sm text-ink-600 font-medium">{{ t('lang.termOnlyIn') }} {{ langName(activeLang) }}</p>
191
194
  <p class="text-xs text-ink-400 mt-1 leading-relaxed">
192
195
  This concept has a registered designation in {{ langName(activeLang) }} but no definition or notes.
193
196
  </p>
@@ -198,7 +201,7 @@ function handleContentClick(e: MouseEvent) {
198
201
 
199
202
  <!-- No data for this language -->
200
203
  <div v-else class="card p-5 text-center">
201
- <p class="text-sm text-ink-400">No data available for {{ langName(activeLang) }}.</p>
204
+ <p class="text-sm text-ink-400">{{ t('concept.noData') }}</p>
202
205
  </div>
203
206
  </div>
204
207
  </template>
@@ -2,8 +2,21 @@ import { ref, computed } from 'vue';
2
2
  import {
3
3
  getClass,
4
4
  getClassTree,
5
+ getAllShapes,
6
+ getObjectProperties,
7
+ getDatatypeProperties,
8
+ getAnnotationProperties,
9
+ getOntology,
10
+ getStats,
5
11
  type OwlClass,
12
+ type OwlShape,
13
+ type OwlProperty,
14
+ type AnnotationProperty,
15
+ type OwlOntology,
16
+ ENTITY_TYPE_META,
17
+ type EntityType,
6
18
  } from '../adapters/ontology-schema';
19
+ import taxonomyData from '../data/taxonomies.json';
7
20
 
8
21
  export function slugToCompact(slug: string): string {
9
22
  return slug.replace(/-/g, ':');
@@ -15,6 +28,57 @@ export function compactToSlug(compact: string): string {
15
28
 
16
29
  const expandedClasses = ref(new Set<string>(['gloss:Designation']));
17
30
 
31
+ const collapsedSections = ref(new Set<string>([
32
+ 'objectProperty',
33
+ 'datatypeProperty',
34
+ 'shape',
35
+ 'taxonomy',
36
+ 'namedIndividual',
37
+ 'annotationProperty',
38
+ ]));
39
+
40
+ const searchQuery = ref('');
41
+
42
+ function toggleExpand(cls: OwlClass) {
43
+ const s = new Set(expandedClasses.value);
44
+ if (s.has(cls.compact)) s.delete(cls.compact);
45
+ else s.add(cls.compact);
46
+ expandedClasses.value = s;
47
+ }
48
+
49
+ function toggleSection(key: string) {
50
+ const s = new Set(collapsedSections.value);
51
+ if (s.has(key)) s.delete(key);
52
+ else s.add(key);
53
+ collapsedSections.value = s;
54
+ }
55
+
56
+ function expandAllSections() {
57
+ collapsedSections.value = new Set();
58
+ }
59
+
60
+ function collapseAllSections() {
61
+ collapsedSections.value = new Set(['objectProperty', 'datatypeProperty', 'shape', 'taxonomy', 'class', 'namedIndividual', 'annotationProperty']);
62
+ }
63
+
64
+ function childClasses(parentId: string): OwlClass[] {
65
+ const cls = getClass(parentId);
66
+ if (!cls) return [];
67
+ return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
68
+ }
69
+
70
+ function hasChildren(cls: OwlClass): boolean {
71
+ return cls.children.length > 0;
72
+ }
73
+
74
+ const treeRoots = getClassTree();
75
+ const allShapes = getAllShapes();
76
+ const objectProperties = getObjectProperties();
77
+ const datatypeProperties = getDatatypeProperties();
78
+ const annotationProperties = getAnnotationProperties();
79
+ const ontology = getOntology();
80
+ const stats = getStats();
81
+
18
82
  const taxonomyKeys = [
19
83
  'conceptStatus', 'entryStatus', 'normativeStatus', 'sourceType', 'sourceStatus',
20
84
  'relationshipType', 'designationType', 'termType', 'grammarGender', 'grammarNumber',
@@ -33,24 +97,53 @@ const taxonomyLabels: Record<string, string> = {
33
97
  grammarNumber: 'Grammar Number',
34
98
  };
35
99
 
36
- function toggleExpand(cls: OwlClass) {
37
- const s = new Set(expandedClasses.value);
38
- if (s.has(cls.compact)) s.delete(cls.compact);
39
- else s.add(cls.compact);
40
- expandedClasses.value = s;
100
+ interface IndividualGroup {
101
+ key: string;
102
+ label: string;
103
+ concepts: { id: string; prefLabel: string }[];
41
104
  }
42
105
 
43
- function childClasses(parentId: string): OwlClass[] {
44
- const cls = getClass(parentId);
45
- if (!cls) return [];
46
- return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
47
- }
106
+ const groupedIndividuals = computed<IndividualGroup[]>(() => {
107
+ return taxonomyKeys.map(key => {
108
+ const tax = (taxonomyData as Record<string, any>)[key];
109
+ if (!tax) return { key, label: taxonomyLabels[key] || key, concepts: [] };
110
+ const concepts = Object.values(tax.concepts as Record<string, any>).map((c: any) => ({
111
+ id: c.id,
112
+ prefLabel: c.prefLabel,
113
+ }));
114
+ return { key, label: tax.schemeLabel || taxonomyLabels[key] || key, concepts };
115
+ });
116
+ });
48
117
 
49
- function hasChildren(cls: OwlClass): boolean {
50
- return cls.children.length > 0;
118
+ const totalIndividuals = computed(() =>
119
+ groupedIndividuals.value.reduce((sum, g) => sum + g.concepts.length, 0),
120
+ );
121
+
122
+ const valuesToTaxonomy: Record<string, string> = {
123
+ 'gloss:status': 'conceptStatus',
124
+ 'gloss:entstatus': 'entryStatus',
125
+ 'gloss:norm': 'normativeStatus',
126
+ 'gloss:sourceType': 'sourceType',
127
+ 'gloss:sourceStatus': 'sourceStatus',
128
+ 'gloss:rel': 'relationshipType',
129
+ 'gloss:desigType': 'designationType',
130
+ 'gloss:termType': 'termType',
131
+ 'gloss:gender': 'grammarGender',
132
+ 'gloss:number': 'grammarNumber',
133
+ };
134
+
135
+ function taxonomyKeyForValuesFrom(valuesFrom: string | null): string | null {
136
+ if (!valuesFrom) return null;
137
+ return valuesToTaxonomy[valuesFrom] ?? null;
51
138
  }
52
139
 
53
- const treeRoots = getClassTree();
140
+ function getShapesForTaxonomy(taxonomyKey: string): OwlShape[] {
141
+ const targetScheme = Object.entries(valuesToTaxonomy).find(([, v]) => v === taxonomyKey)?.[0];
142
+ if (!targetScheme) return [];
143
+ return allShapes.filter(s =>
144
+ s.constraints.some(c => c.valuesFrom === targetScheme),
145
+ );
146
+ }
54
147
 
55
148
  const allNavItems = computed(() => {
56
149
  const items: { id: string; label: string; depth: number }[] = [];
@@ -66,15 +159,92 @@ const allNavItems = computed(() => {
66
159
  return items;
67
160
  });
68
161
 
162
+ function matchesSearch(text: string, query: string): boolean {
163
+ return text.toLowerCase().includes(query.toLowerCase());
164
+ }
165
+
166
+ const searchResults = computed(() => {
167
+ const q = searchQuery.value.trim();
168
+ if (!q) return null;
169
+
170
+ const matchedClasses: OwlClass[] = [];
171
+ const matchedObjectProps: OwlProperty[] = [];
172
+ const matchedDatatypeProps: OwlProperty[] = [];
173
+ const matchedShapes: OwlShape[] = [];
174
+ const matchedIndividuals: { group: string; id: string; prefLabel: string }[] = [];
175
+ const matchedAnnotationProps: AnnotationProperty[] = [];
176
+
177
+ // Walk all classes (tree + leaves)
178
+ function walkAll(classes: OwlClass[]) {
179
+ for (const cls of classes) {
180
+ if (matchesSearch(cls.label, q) || matchesSearch(cls.compact, q)) {
181
+ matchedClasses.push(cls);
182
+ }
183
+ walkAll(childClasses(cls.compact));
184
+ }
185
+ }
186
+ walkAll(treeRoots);
187
+
188
+ for (const p of objectProperties) {
189
+ if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedObjectProps.push(p);
190
+ }
191
+ for (const p of datatypeProperties) {
192
+ if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedDatatypeProps.push(p);
193
+ }
194
+ for (const s of allShapes) {
195
+ if (matchesSearch(s.label, q) || matchesSearch(s.compact, q)) matchedShapes.push(s);
196
+ }
197
+ for (const g of groupedIndividuals.value) {
198
+ for (const c of g.concepts) {
199
+ if (matchesSearch(c.prefLabel, q) || matchesSearch(c.id, q)) {
200
+ matchedIndividuals.push({ group: g.key, id: c.id, prefLabel: c.prefLabel });
201
+ }
202
+ }
203
+ }
204
+ for (const ap of annotationProperties) {
205
+ if (matchesSearch(ap.label, q) || matchesSearch(ap.compact, q)) matchedAnnotationProps.push(ap);
206
+ }
207
+
208
+ const total = matchedClasses.length + matchedObjectProps.length + matchedDatatypeProps.length + matchedShapes.length + matchedIndividuals.length + matchedAnnotationProps.length;
209
+
210
+ return {
211
+ total,
212
+ classes: matchedClasses,
213
+ objectProperties: matchedObjectProps,
214
+ datatypeProperties: matchedDatatypeProps,
215
+ shapes: matchedShapes,
216
+ individuals: matchedIndividuals,
217
+ annotationProperties: matchedAnnotationProps,
218
+ };
219
+ });
220
+
69
221
  export function useOntologyNav() {
70
222
  return {
71
223
  expandedClasses,
224
+ collapsedSections,
225
+ searchQuery,
72
226
  taxonomyKeys,
73
227
  taxonomyLabels,
74
228
  treeRoots,
229
+ allShapes,
230
+ objectProperties,
231
+ datatypeProperties,
232
+ annotationProperties,
233
+ ontology,
234
+ stats,
235
+ groupedIndividuals,
236
+ totalIndividuals,
75
237
  allNavItems,
238
+ searchResults,
239
+ valuesToTaxonomy,
240
+ taxonomyKeyForValuesFrom,
241
+ getShapesForTaxonomy,
76
242
  toggleExpand,
243
+ toggleSection,
244
+ expandAllSections,
245
+ collapseAllSections,
77
246
  childClasses,
78
247
  hasChildren,
248
+ ENTITY_TYPE_META,
79
249
  };
80
250
  }
@@ -14,7 +14,7 @@ const bibCache = new Map<string, Record<string, BibEntry>>();
14
14
  async function loadBibliography(registerId: string): Promise<Record<string, BibEntry> | null> {
15
15
  if (bibCache.has(registerId)) return bibCache.get(registerId)!;
16
16
  try {
17
- const resp = await fetch(`/data/${registerId}/bibliography.json`);
17
+ const resp = await fetch(`${import.meta.env.BASE_URL}data/${registerId}/bibliography.json`);
18
18
  if (!resp.ok) return null;
19
19
  const data = await resp.json();
20
20
  bibCache.set(registerId, data);
@@ -47,7 +47,7 @@ export function useRenderOptions(registerId: () => string) {
47
47
 
48
48
  const figResolver: FigResolver = (figId) => {
49
49
  const id = registerId();
50
- const imgSrc = `/data/${id}/images/${figId}.png`;
50
+ const imgSrc = `${import.meta.env.BASE_URL}data/${id}/images/${figId}.png`;
51
51
  return `<span class="fig-ref"><a href="${escapeAttr(imgSrc)}" target="_blank" rel="noopener">${escapeAttr(figId)}</a></span>`;
52
52
  };
53
53
 
@@ -152,6 +152,7 @@ export interface SiteConfig {
152
152
  defaults: {
153
153
  language?: string;
154
154
  languageOrder?: string[];
155
+ mainLanguages?: string[];
155
156
  };
156
157
  email?: string;
157
158
  pages?: PageConfig[];
@@ -10,6 +10,7 @@ export interface RuntimeSiteConfig {
10
10
  description?: string;
11
11
  datasets: string[];
12
12
  defaultDataset?: string;
13
+ uiLanguages?: { code: string; label: string }[];
13
14
  branding?: {
14
15
  primaryColor?: string;
15
16
  darkColor?: string;
@@ -27,7 +28,7 @@ export interface RuntimeSiteConfig {
27
28
  social?: Record<string, string>;
28
29
  nav?: { label: string; route: string }[];
29
30
  footerNav?: { label: string; route: string }[];
30
- defaults?: { language?: string; languageOrder?: string[] };
31
+ defaults?: { language?: string; languageOrder?: string[]; mainLanguages?: string[] };
31
32
  email?: string;
32
33
  pages?: PageConfig[];
33
34
  contributors?: { name: string; role?: string; organization?: string; url?: string; email?: string }[];
@@ -94,7 +95,7 @@ function applyBranding(config: RuntimeSiteConfig) {
94
95
  async function loadConfig(): Promise<RuntimeSiteConfig | null> {
95
96
  if (loaded.value) return siteConfig.value;
96
97
  try {
97
- const resp = await fetch('/site-config.json');
98
+ const resp = await fetch(`${import.meta.env.BASE_URL}site-config.json`);
98
99
  if (resp.ok) {
99
100
  siteConfig.value = await resp.json();
100
101
  if (siteConfig.value) applyBranding(siteConfig.value);