@glossarist/concept-browser 0.7.22 → 0.7.24

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 +121 -70
  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
@@ -9,25 +9,42 @@ export class AdapterFactory {
9
9
  private urnMap = new Map<string, string>();
10
10
  readonly router = new UriRouter();
11
11
  readonly resolver: ReferenceResolver;
12
+ private crossRefIndex: Record<string, string[]> | null = null;
12
13
 
13
14
  constructor() {
14
15
  this.resolver = new ReferenceResolver();
15
16
  }
16
17
 
17
18
  async discoverDatasets(datasetsUrl: string): Promise<DatasetAdapter[]> {
18
- const resp = await fetch(datasetsUrl);
19
- if (!resp.ok) throw new Error(`Failed to load dataset registry: ${resp.status}`);
20
- const registry = (await resp.json()) as DatasetRegistry[];
19
+ let registry: DatasetRegistry[];
20
+ const inline = document.getElementById('datasets-json');
21
+ if (inline?.textContent) {
22
+ registry = JSON.parse(inline.textContent) as DatasetRegistry[];
23
+ } else {
24
+ const resp = await fetch(datasetsUrl);
25
+ if (!resp.ok) throw new Error(`Failed to load dataset registry: ${resp.status}`);
26
+ registry = await resp.json() as DatasetRegistry[];
27
+ }
21
28
 
22
29
  const base = import.meta.env.BASE_URL;
23
30
  const adapters: DatasetAdapter[] = [];
31
+ const needManifest: DatasetAdapter[] = [];
32
+
24
33
  for (const reg of registry) {
25
34
  const adapter = new DatasetAdapter(reg.id, `${base}data/${reg.id}`);
26
35
  this.adapters.set(reg.id, adapter);
27
36
  adapters.push(adapter);
37
+
38
+ if (reg.summary) {
39
+ adapter.setSummaryManifest(reg.summary);
40
+ } else {
41
+ needManifest.push(adapter);
42
+ }
28
43
  }
29
44
 
30
- await Promise.all(adapters.map(a => a.loadManifest().catch(() => {})));
45
+ if (needManifest.length > 0) {
46
+ await Promise.all(needManifest.map(a => a.loadManifest().catch(() => {})));
47
+ }
31
48
 
32
49
  return adapters;
33
50
  }
@@ -89,6 +106,20 @@ export class AdapterFactory {
89
106
  resolveCitation(source: string, referenceFrom: string, sourceDatasetId?: string): Resolution | null {
90
107
  return this.resolver.resolveCitation(source, referenceFrom, sourceDatasetId);
91
108
  }
109
+
110
+ async loadCrossRefIndex(): Promise<Record<string, string[]>> {
111
+ if (this.crossRefIndex) return this.crossRefIndex;
112
+ try {
113
+ const resp = await fetch(`${import.meta.env.BASE_URL}data/cross-ref-index.json`);
114
+ if (resp.ok) {
115
+ this.crossRefIndex = await resp.json();
116
+ }
117
+ } catch {
118
+ // Fall through to empty index
119
+ }
120
+ this.crossRefIndex = this.crossRefIndex || {};
121
+ return this.crossRefIndex;
122
+ }
92
123
  }
93
124
 
94
125
  let _instance: AdapterFactory | null = null;
@@ -25,7 +25,7 @@ export interface Taxonomy {
25
25
 
26
26
  type TaxonomyKey = keyof typeof taxonomyData;
27
27
 
28
- class OntologyRegistry {
28
+ export class OntologyRegistry {
29
29
  private data: Record<string, Taxonomy>;
30
30
 
31
31
  constructor() {
@@ -88,6 +88,7 @@ export interface ConceptSummary {
88
88
  designations: Record<string, string>;
89
89
  eng: string;
90
90
  status: string;
91
+ groups?: string[];
91
92
  }
92
93
 
93
94
  export interface ConceptEntry {
@@ -98,9 +99,20 @@ export interface ConceptEntry {
98
99
  status: string;
99
100
  }
100
101
 
102
+ export interface DatasetSummary {
103
+ title: string;
104
+ description: string;
105
+ conceptCount: number;
106
+ languages: string[];
107
+ owner: string;
108
+ tags: string[];
109
+ color?: string;
110
+ }
111
+
101
112
  export interface DatasetRegistry {
102
113
  id: string;
103
114
  manifestUrl: string;
115
+ summary?: DatasetSummary;
104
116
  }
105
117
 
106
118
  // ── Graph types ────────────────────────────────────────────────────────────
@@ -1,16 +1,16 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref } from 'vue';
2
+ import { computed, ref, defineAsyncComponent } from 'vue';
3
3
  import { useVocabularyStore } from '../stores/vocabulary';
4
4
  import { useUiStore } from '../stores/ui';
5
5
  import { useRoute, useRouter } from 'vue-router';
6
6
  import { useDsStyle } from '../utils/dataset-style';
7
7
  import { useSiteConfig } from '../config/use-site-config';
8
- import type { DatasetGroup } from '../config/types';
9
- import { useOntologyNav, compactToSlug } from '../composables/use-ontology-nav';
10
8
  import NavIcon from './NavIcon.vue';
11
9
  import { useI18n, locale } from '../i18n';
12
10
  import type { SectionNode } from '../adapters/types';
13
11
 
12
+ const OntologySidebarSection = defineAsyncComponent(() => import('./OntologySidebarSection.vue'));
13
+
14
14
  const store = useVocabularyStore();
15
15
  const ui = useUiStore();
16
16
  const router = useRouter();
@@ -21,56 +21,10 @@ const { t } = useI18n();
21
21
 
22
22
  const currentDataset = computed(() => route.params.registerId as string ?? '');
23
23
 
24
- const {
25
- expandedClasses,
26
- collapsedSections,
27
- searchQuery,
28
- taxonomyKeys,
29
- taxonomyLabels,
30
- treeRoots,
31
- allShapes,
32
- objectProperties,
33
- datatypeProperties,
34
- annotationProperties,
35
- groupedIndividuals,
36
- totalIndividuals,
37
- searchResults,
38
- toggleExpand,
39
- toggleSection,
40
- childClasses,
41
- hasChildren,
42
- ENTITY_TYPE_META,
43
- } = useOntologyNav();
44
-
45
24
  const isOntologyRoute = computed(() =>
46
25
  ['ontology', 'ontology-class', 'ontology-taxonomy', 'ontology-shape', 'ontology-property'].includes(route.name as string)
47
26
  );
48
27
 
49
- const activeClassId = computed(() => {
50
- if (route.name !== 'ontology-class') return null;
51
- const slug = route.params.classId as string;
52
- return slug.replace(/-/g, ':');
53
- });
54
-
55
- const activeTaxonomy = computed(() => {
56
- if (route.name !== 'ontology-taxonomy') return null;
57
- return route.params.taxonomyKey as string;
58
- });
59
-
60
- const activeShapeId = computed(() => {
61
- if (route.name !== 'ontology-shape') return null;
62
- const slug = route.params.shapeId as string;
63
- return slug.replace(/-/g, ':');
64
- });
65
-
66
- const activePropertyId = computed(() => {
67
- if (route.name !== 'ontology-property') return null;
68
- const slug = route.params.propertyId as string;
69
- return slug.replace(/-/g, ':');
70
- });
71
-
72
- const isOverview = computed(() => route.name === 'ontology');
73
-
74
28
  const datasetEntries = computed(() => {
75
29
  const entries: { id: string; title: string; loaded: boolean; conceptCount: number }[] = [];
76
30
  for (const [id, adapter] of store.datasets) {
@@ -171,8 +125,6 @@ const provenance = computed(() => {
171
125
  };
172
126
  });
173
127
 
174
- const ontologyExpanded = ref(true);
175
-
176
128
  function closeMobile() { ui.sidebarOpen = false; }
177
129
 
178
130
  function goToDataset(id: string) {
@@ -200,24 +152,6 @@ function isActive(page: { route: string; datasetScoped?: boolean }): boolean {
200
152
  return route.name === page.route || route.name === `${page.route}-global`;
201
153
  }
202
154
 
203
- function selectClass(id: string) {
204
- router.push(`/ontology/class/${compactToSlug(id)}`);
205
- }
206
-
207
- function selectTaxonomy(key: string) {
208
- router.push(`/ontology/taxonomy/${key}`);
209
- }
210
-
211
- function selectShape(id: string) {
212
- router.push(`/ontology/shape/${compactToSlug(id)}`);
213
- }
214
-
215
- function selectProperty(id: string) {
216
- router.push(`/ontology/property/${compactToSlug(id)}`);
217
- }
218
-
219
- const isSearching = computed(() => !!searchQuery.value.trim());
220
-
221
155
  function navTitle(page: { route: string }): string {
222
156
  const route = page.route || 'home';
223
157
  const key = `nav.${route}`;
@@ -249,7 +183,14 @@ function enrichSectionNode(s: { id: string; names: Record<string, string>; child
249
183
  }
250
184
 
251
185
  function sectionLabel(section: SectionNode): string {
252
- return section.names[locale.value] || section.names.eng || section.id;
186
+ const name = section.names[locale.value] || section.names.eng || '';
187
+ return name || section.id;
188
+ }
189
+
190
+ function sectionDisplay(section: SectionNode): string {
191
+ const name = sectionLabel(section);
192
+ if (name && name !== section.id) return `${section.id} — ${name}`;
193
+ return name || section.id;
253
194
  }
254
195
 
255
196
  function goToSection(dsId: string, sectionId: string) {
@@ -292,278 +233,11 @@ const activeSectionId = computed(() => {
292
233
  {{ navTitle(page) }}
293
234
  </router-link>
294
235
 
295
- <!-- Ontology entity sections nested under Ontology nav item -->
236
+ <!-- Ontology entity sections (lazy-loaded) -->
296
237
  <div v-if="page.route === 'ontology' && isOntologyRoute" class="ml-3 mt-1 mb-2">
297
- <button @click="ontologyExpanded = !ontologyExpanded"
298
- 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 mb-1"
299
- >
300
- <span class="w-3 text-[10px]">{{ ontologyExpanded ? '▾' : '▸' }}</span>
301
- <span class="flex-1 text-left">{{ t('nav.ontology') }}</span>
302
- </button>
303
- <div v-if="ontologyExpanded" class="space-y-0.5">
304
- <!-- Search input -->
305
- <div class="relative mb-1.5">
306
- <input
307
- v-model="searchQuery"
308
- type="text"
309
- placeholder="Search entities..."
310
- 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"
311
- />
312
- <span v-if="searchResults" class="absolute right-2 top-1/2 -translate-y-1/2 text-[9px] text-ink-400">
313
- {{ searchResults.total }}
314
- </span>
315
- </div>
316
-
317
- <!-- Overview link -->
318
- <router-link to="/ontology"
319
- class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
320
- :class="isOverview ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
321
- >
322
- <span class="w-3 text-ink-200">·</span>
323
- <span class="flex-1 text-left">Overview</span>
324
- </router-link>
325
-
326
- <!-- Search results mode -->
327
- <template v-if="isSearching && searchResults">
328
- <div v-if="searchResults.classes.length" class="mt-1">
329
- <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-blue-500 font-medium">Classes ({{ searchResults.classes.length }})</div>
330
- <button v-for="cls in searchResults.classes" :key="cls.compact"
331
- @click="selectClass(cls.compact); searchQuery = ''"
332
- 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"
333
- >
334
- <span class="w-3 text-ink-200">·</span>
335
- <span class="flex-1 text-left truncate">{{ cls.label }}</span>
336
- <code class="text-[9px] text-ink-300">{{ cls.compact }}</code>
337
- </button>
338
- </div>
339
- <div v-if="searchResults.objectProperties.length" class="mt-1">
340
- <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-emerald-500 font-medium">Object Properties ({{ searchResults.objectProperties.length }})</div>
341
- <button v-for="p in searchResults.objectProperties" :key="p.compact"
342
- @click="selectProperty(p.compact); searchQuery = ''"
343
- 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"
344
- >
345
- <span class="w-3 text-ink-200">·</span>
346
- <span class="flex-1 text-left truncate">{{ p.label }}</span>
347
- </button>
348
- </div>
349
- <div v-if="searchResults.datatypeProperties.length" class="mt-1">
350
- <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-amber-500 font-medium">Datatype Properties ({{ searchResults.datatypeProperties.length }})</div>
351
- <button v-for="p in searchResults.datatypeProperties" :key="p.compact"
352
- @click="selectProperty(p.compact); searchQuery = ''"
353
- 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"
354
- >
355
- <span class="w-3 text-ink-200">·</span>
356
- <span class="flex-1 text-left truncate">{{ p.label }}</span>
357
- </button>
358
- </div>
359
- <div v-if="searchResults.shapes.length" class="mt-1">
360
- <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-purple-500 font-medium">SHACL Shapes ({{ searchResults.shapes.length }})</div>
361
- <button v-for="s in searchResults.shapes" :key="s.compact"
362
- @click="selectShape(s.compact); searchQuery = ''"
363
- 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"
364
- >
365
- <span class="w-3 text-ink-200">·</span>
366
- <span class="flex-1 text-left truncate">{{ s.label }}</span>
367
- </button>
368
- </div>
369
- <div v-if="searchResults.individuals.length" class="mt-1">
370
- <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-rose-500 font-medium">Named Individuals ({{ searchResults.individuals.length }})</div>
371
- <button v-for="ind in searchResults.individuals" :key="ind.group + '/' + ind.id"
372
- @click="selectTaxonomy(ind.group); searchQuery = ''"
373
- 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"
374
- >
375
- <span class="w-3 text-ink-200">·</span>
376
- <span class="flex-1 text-left truncate">{{ ind.prefLabel }}</span>
377
- <span class="text-[9px] text-ink-300">{{ taxonomyLabels[ind.group] }}</span>
378
- </button>
379
- </div>
380
- <div v-if="searchResults.annotationProperties.length" class="mt-1">
381
- <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-pink-500 font-medium">Annotation Properties ({{ searchResults.annotationProperties.length }})</div>
382
- <div v-for="ap in searchResults.annotationProperties" :key="ap.compact"
383
- class="px-2 py-0.5 text-[11px] text-ink-500"
384
- >
385
- <span class="w-3 inline-block text-ink-200">·</span>
386
- {{ ap.compact }}
387
- </div>
388
- </div>
389
- <div v-if="searchResults.total === 0" class="px-2 py-3 text-[11px] text-ink-300 italic">
390
- No entities match "{{ searchQuery }}"
391
- </div>
392
- </template>
393
-
394
- <!-- Normal browse mode -->
395
- <template v-if="!isSearching">
396
- <!-- Classes section -->
397
- <div class="mt-1">
398
- <button @click="toggleSection('class')"
399
- 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"
400
- >
401
- <span class="w-3 text-[10px]">{{ collapsedSections.has('class') ? '▸' : '▾' }}</span>
402
- <span class="flex-1 text-left">Classes</span>
403
- <span class="badge text-[9px] bg-blue-50 text-blue-600 px-1 py-0.5">{{ treeRoots.length }}+</span>
404
- </button>
405
- <div v-if="!collapsedSections.has('class')" class="mt-0.5 space-y-0">
406
- <template v-for="root in treeRoots" :key="root.compact">
407
- <button @click="selectClass(root.compact); toggleExpand(root)"
408
- class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
409
- :class="activeClassId === root.compact && !activeTaxonomy && !activeShapeId && !activePropertyId ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
410
- >
411
- <span v-if="hasChildren(root)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(root.compact) ? '▾' : '▸' }}</span>
412
- <span v-else class="w-3 text-ink-200">·</span>
413
- <span class="flex-1 text-left">{{ root.label }}</span>
414
- </button>
415
- <div v-if="expandedClasses.has(root.compact) && hasChildren(root)" class="ml-3">
416
- <template v-for="child in childClasses(root.compact)" :key="child.compact">
417
- <button @click="selectClass(child.compact); toggleExpand(child)"
418
- class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
419
- :class="activeClassId === child.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
420
- >
421
- <span v-if="hasChildren(child)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(child.compact) ? '▾' : '▸' }}</span>
422
- <span v-else class="w-3 text-ink-200">·</span>
423
- <span class="flex-1 text-left">{{ child.label }}</span>
424
- </button>
425
- <div v-if="expandedClasses.has(child.compact) && hasChildren(child)" class="ml-3">
426
- <button v-for="gc in childClasses(child.compact)" :key="gc.compact"
427
- @click="selectClass(gc.compact)"
428
- class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
429
- :class="activeClassId === gc.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
430
- >
431
- <span class="w-3 text-ink-200">·</span>
432
- <span class="flex-1 text-left">{{ gc.label }}</span>
433
- </button>
434
- </div>
435
- </template>
436
- </div>
437
- </template>
438
- </div>
439
- </div>
440
-
441
- <!-- Object Properties section -->
442
- <div class="mt-1">
443
- <button @click="toggleSection('objectProperty')"
444
- 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"
445
- >
446
- <span class="w-3 text-[10px]">{{ collapsedSections.has('objectProperty') ? '▸' : '▾' }}</span>
447
- <span class="flex-1 text-left">Object Properties</span>
448
- <span class="badge text-[9px] bg-emerald-50 text-emerald-600 px-1 py-0.5">{{ objectProperties.length }}</span>
449
- </button>
450
- <div v-if="!collapsedSections.has('objectProperty')" class="mt-0.5 max-h-40 overflow-y-auto">
451
- <button v-for="p in objectProperties" :key="p.compact"
452
- @click="selectProperty(p.compact)"
453
- class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
454
- :class="activePropertyId === p.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
455
- >
456
- <span class="w-3 text-ink-200">·</span>
457
- <span class="flex-1 text-left truncate">{{ p.label }}</span>
458
- </button>
459
- </div>
460
- </div>
461
-
462
- <!-- Datatype Properties section -->
463
- <div class="mt-1">
464
- <button @click="toggleSection('datatypeProperty')"
465
- 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"
466
- >
467
- <span class="w-3 text-[10px]">{{ collapsedSections.has('datatypeProperty') ? '▸' : '▾' }}</span>
468
- <span class="flex-1 text-left">Datatype Properties</span>
469
- <span class="badge text-[9px] bg-amber-50 text-amber-600 px-1 py-0.5">{{ datatypeProperties.length }}</span>
470
- </button>
471
- <div v-if="!collapsedSections.has('datatypeProperty')" class="mt-0.5 max-h-40 overflow-y-auto">
472
- <button v-for="p in datatypeProperties" :key="p.compact"
473
- @click="selectProperty(p.compact)"
474
- class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
475
- :class="activePropertyId === p.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
476
- >
477
- <span class="w-3 text-ink-200">·</span>
478
- <span class="flex-1 text-left truncate">{{ p.label }}</span>
479
- </button>
480
- </div>
481
- </div>
482
-
483
- <!-- SHACL Shapes section -->
484
- <div class="mt-1">
485
- <button @click="toggleSection('shape')"
486
- 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"
487
- >
488
- <span class="w-3 text-[10px]">{{ collapsedSections.has('shape') ? '▸' : '▾' }}</span>
489
- <span class="flex-1 text-left">SHACL Shapes</span>
490
- <span class="badge text-[9px] bg-purple-50 text-purple-600 px-1 py-0.5">{{ allShapes.length }}</span>
491
- </button>
492
- <div v-if="!collapsedSections.has('shape')" class="mt-0.5 max-h-40 overflow-y-auto">
493
- <button v-for="s in allShapes" :key="s.compact"
494
- @click="selectShape(s.compact)"
495
- class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
496
- :class="activeShapeId === s.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
497
- >
498
- <span class="w-3 text-ink-200">·</span>
499
- <span class="flex-1 text-left truncate">{{ s.label }}</span>
500
- </button>
501
- </div>
502
- </div>
503
-
504
- <!-- Named Individuals section -->
505
- <div class="mt-1">
506
- <button @click="toggleSection('namedIndividual')"
507
- 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"
508
- >
509
- <span class="w-3 text-[10px]">{{ collapsedSections.has('namedIndividual') ? '▸' : '▾' }}</span>
510
- <span class="flex-1 text-left">Named Individuals</span>
511
- <span class="badge text-[9px] bg-rose-50 text-rose-600 px-1 py-0.5">{{ totalIndividuals }}</span>
512
- </button>
513
- <div v-if="!collapsedSections.has('namedIndividual')" class="mt-0.5 max-h-64 overflow-y-auto">
514
- <template v-for="group in groupedIndividuals" :key="group.key">
515
- <button @click="selectTaxonomy(group.key)"
516
- 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"
517
- >
518
- <span class="w-3 text-ink-200">·</span>
519
- <span class="flex-1 text-left">{{ group.label }}</span>
520
- <span class="text-[9px] text-ink-300">{{ group.concepts.length }}</span>
521
- </button>
522
- </template>
523
- </div>
524
- </div>
525
-
526
- <!-- SKOS Taxonomies section -->
527
- <div class="mt-1">
528
- <button @click="toggleSection('taxonomy')"
529
- 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"
530
- >
531
- <span class="w-3 text-[10px]">{{ collapsedSections.has('taxonomy') ? '▸' : '▾' }}</span>
532
- <span class="flex-1 text-left">SKOS Taxonomies</span>
533
- <span class="badge text-[9px] bg-rose-50 text-rose-600 px-1 py-0.5">{{ taxonomyKeys.length }}</span>
534
- </button>
535
- <div v-if="!collapsedSections.has('taxonomy')" class="mt-0.5">
536
- <button v-for="tk in taxonomyKeys" :key="tk"
537
- @click="selectTaxonomy(tk)"
538
- class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
539
- :class="activeTaxonomy === tk ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
540
- >
541
- <span class="w-3 text-ink-200">·</span>
542
- <span class="flex-1 text-left">{{ taxonomyLabels[tk] }}</span>
543
- </button>
544
- </div>
545
- </div>
546
-
547
- <!-- Annotation Properties section -->
548
- <div class="mt-1">
549
- <button @click="toggleSection('annotationProperty')"
550
- 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"
551
- >
552
- <span class="w-3 text-[10px]">{{ collapsedSections.has('annotationProperty') ? '▸' : '▾' }}</span>
553
- <span class="flex-1 text-left">Annotation Properties</span>
554
- <span class="badge text-[9px] bg-pink-50 text-pink-600 px-1 py-0.5">{{ annotationProperties.length }}</span>
555
- </button>
556
- <div v-if="!collapsedSections.has('annotationProperty')" class="mt-0.5">
557
- <div v-for="ap in annotationProperties" :key="ap.compact"
558
- class="w-full flex items-center gap-1.5 px-2 py-1 text-[11px] text-ink-500"
559
- >
560
- <span class="w-3 text-ink-200">·</span>
561
- <code class="text-ink-400">{{ ap.compact }}</code>
562
- </div>
563
- </div>
564
- </div>
565
- </template>
566
- </div>
238
+ <Suspense>
239
+ <OntologySidebarSection />
240
+ </Suspense>
567
241
  </div>
568
242
  </template>
569
243
  </nav>
@@ -650,7 +324,7 @@ const activeSectionId = computed(() => {
650
324
  >
651
325
  <span v-if="section.children?.length" class="text-[10px] text-ink-300 w-3 cursor-pointer" @click.stop="toggleSectionNode(ds.id + '-s-' + section.id)">{{ expandedSectionNodes.has(ds.id + '-s-' + section.id) ? '▾' : '▸' }}</span>
652
326
  <span v-else class="w-3 text-ink-200">&#183;</span>
653
- <span class="flex-1 text-left">{{ sectionLabel(section) }}</span>
327
+ <span class="flex-1 text-left truncate">{{ sectionDisplay(section) }}</span>
654
328
  </button>
655
329
  <div v-if="section.children?.length && expandedSectionNodes.has(ds.id + '-s-' + section.id)" class="ml-3">
656
330
  <button v-for="child in section.children" :key="child.id"
@@ -659,7 +333,7 @@ const activeSectionId = computed(() => {
659
333
  :class="activeSectionId === 'section-' + child.id ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
660
334
  >
661
335
  <span class="w-3 text-ink-200">&#183;</span>
662
- <span class="flex-1 text-left">{{ sectionLabel(child) }}</span>
336
+ <span class="flex-1 text-left truncate">{{ sectionDisplay(child) }}</span>
663
337
  </button>
664
338
  </div>
665
339
  </template>