@glossarist/concept-browser 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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": {
@@ -24,6 +24,7 @@
24
24
  "@vitejs/plugin-vue": "^5.2.3",
25
25
  "autoprefixer": "^10.4.21",
26
26
  "d3": "^7.9.0",
27
+ "favicons": "^7.2.0",
27
28
  "glossarist": "^0.3.0",
28
29
  "js-yaml": "^4.1.0",
29
30
  "pinia": "^2.3.1",
@@ -36,7 +37,6 @@
36
37
  "devDependencies": {
37
38
  "@types/d3": "^7.4.3",
38
39
  "@vue/test-utils": "^2.4.8",
39
- "favicons": "^7.2.0",
40
40
  "happy-dom": "^20.9.0",
41
41
  "typescript": "~5.7.3",
42
42
  "vitest": "^4.1.5",
@@ -5,6 +5,7 @@ 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 { useOntologyNav } from '../composables/use-ontology-nav';
8
9
  import NavIcon from './NavIcon.vue';
9
10
 
10
11
  const store = useVocabularyStore();
@@ -16,6 +17,21 @@ const { globalPages, datasetPages, config: siteConfig } = useSiteConfig();
16
17
 
17
18
  const currentDataset = computed(() => route.params.registerId as string ?? '');
18
19
 
20
+ const {
21
+ activeClassId,
22
+ activeTaxonomy,
23
+ expandedClasses,
24
+ taxonomyKeys,
25
+ taxonomyLabels,
26
+ treeRoots,
27
+ supportingClasses,
28
+ toggleExpand,
29
+ childClasses,
30
+ hasChildren,
31
+ } = useOntologyNav();
32
+
33
+ const isOntologyRoute = computed(() => route.name === 'ontology');
34
+
19
35
  const datasetEntries = computed(() => {
20
36
  const entries: { id: string; title: string; loaded: boolean; conceptCount: number }[] = [];
21
37
  for (const [id, adapter] of store.datasets) {
@@ -85,6 +101,73 @@ function isActive(page: { route: string; datasetScoped?: boolean }): boolean {
85
101
  </router-link>
86
102
  </nav>
87
103
 
104
+ <!-- Ontology class tree (shown when on /ontology route) -->
105
+ <div v-if="isOntologyRoute" class="mb-6">
106
+ <div class="section-label">Classes</div>
107
+ <nav class="space-y-0.5">
108
+ <template v-for="root in treeRoots" :key="root.compact">
109
+ <button @click="activeClassId = root.compact; activeTaxonomy = null; toggleExpand(root)"
110
+ class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
111
+ :class="activeClassId === root.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
112
+ >
113
+ <span v-if="hasChildren(root)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(root.compact) ? '▾' : '▸' }}</span>
114
+ <span v-else class="w-3"></span>
115
+ <span class="flex-1 text-left">{{ root.label }}</span>
116
+ </button>
117
+ <!-- Children -->
118
+ <div v-if="expandedClasses.has(root.compact) && hasChildren(root)" class="ml-3">
119
+ <template v-for="child in childClasses(root.compact)" :key="child.compact">
120
+ <button @click="activeClassId = child.compact; activeTaxonomy = null; toggleExpand(child)"
121
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
122
+ :class="activeClassId === child.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
123
+ >
124
+ <span v-if="hasChildren(child)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(child.compact) ? '▾' : '▸' }}</span>
125
+ <span v-else class="w-3 text-ink-200">·</span>
126
+ <span class="flex-1 text-left">{{ child.label }}</span>
127
+ </button>
128
+ <!-- Grandchildren -->
129
+ <div v-if="expandedClasses.has(child.compact) && hasChildren(child)" class="ml-3">
130
+ <button v-for="gc in childClasses(child.compact)" :key="gc.compact"
131
+ @click="activeClassId = gc.compact; activeTaxonomy = null"
132
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
133
+ :class="activeClassId === gc.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
134
+ >
135
+ <span class="w-3 text-ink-200">·</span>
136
+ <span class="flex-1 text-left">{{ gc.label }}</span>
137
+ </button>
138
+ </div>
139
+ </template>
140
+ </div>
141
+ </template>
142
+
143
+ <!-- Supporting classes -->
144
+ <div v-if="supportingClasses.length" class="mt-3 pt-2 border-t border-ink-100/40">
145
+ <div class="text-[10px] uppercase tracking-wide text-ink-300 mb-1 px-2">Supporting</div>
146
+ <button v-for="cls in supportingClasses" :key="cls.compact"
147
+ @click="activeClassId = cls.compact; activeTaxonomy = null"
148
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
149
+ :class="activeClassId === cls.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
150
+ >
151
+ <span class="w-3 text-ink-200">·</span>
152
+ <span class="flex-1 text-left">{{ cls.label }}</span>
153
+ </button>
154
+ </div>
155
+
156
+ <!-- SKOS Taxonomies -->
157
+ <div class="mt-3 pt-2 border-t border-ink-100/40">
158
+ <div class="text-[10px] uppercase tracking-wide text-ink-300 mb-1 px-2">Taxonomies</div>
159
+ <button v-for="tk in taxonomyKeys" :key="tk"
160
+ @click="activeTaxonomy = tk"
161
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
162
+ :class="activeTaxonomy === tk ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
163
+ >
164
+ <span class="w-3 text-ink-200">·</span>
165
+ <span class="flex-1 text-left">{{ taxonomyLabels[tk] }}</span>
166
+ </button>
167
+ </div>
168
+ </nav>
169
+ </div>
170
+
88
171
  <!-- Dataset-level navigation (shown when viewing a dataset) -->
89
172
  <div v-if="showDatasetNav" class="mb-6">
90
173
  <div class="section-label">{{ currentManifest?.title || siteConfig?.title || 'Dataset' }}</div>
@@ -0,0 +1,87 @@
1
+ import { ref, computed } from 'vue';
2
+ import {
3
+ getClass,
4
+ getClassTree,
5
+ getAllClasses,
6
+ type OwlClass,
7
+ } from '../adapters/ontology-schema';
8
+
9
+ const activeClassId = ref('gloss:Concept');
10
+ const activeTaxonomy = ref<string | null>(null);
11
+ const expandedClasses = ref(new Set<string>(['gloss:Concept', 'gloss:Designation']));
12
+
13
+ const taxonomyKeys = [
14
+ 'conceptStatus', 'entryStatus', 'normativeStatus', 'sourceType', 'sourceStatus',
15
+ 'relationshipType', 'designationType', 'termType', 'grammarGender', 'grammarNumber',
16
+ ];
17
+
18
+ const taxonomyLabels: Record<string, string> = {
19
+ conceptStatus: 'Concept Status',
20
+ entryStatus: 'Entry Status',
21
+ normativeStatus: 'Normative Status',
22
+ sourceType: 'Source Type',
23
+ sourceStatus: 'Source Status',
24
+ relationshipType: 'Relationship Type',
25
+ designationType: 'Designation Type',
26
+ termType: 'Term Type',
27
+ grammarGender: 'Grammar Gender',
28
+ grammarNumber: 'Grammar Number',
29
+ };
30
+
31
+ function toggleExpand(cls: OwlClass) {
32
+ const s = new Set(expandedClasses.value);
33
+ if (s.has(cls.compact)) s.delete(cls.compact);
34
+ else s.add(cls.compact);
35
+ expandedClasses.value = s;
36
+ }
37
+
38
+ function childClasses(parentId: string): OwlClass[] {
39
+ const cls = getClass(parentId);
40
+ if (!cls) return [];
41
+ return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
42
+ }
43
+
44
+ function hasChildren(cls: OwlClass): boolean {
45
+ return cls.children.length > 0;
46
+ }
47
+
48
+ const treeRoots = getClassTree();
49
+
50
+ const supportingClasses = computed(() =>
51
+ getAllClasses().filter(
52
+ c => c.children.length === 0
53
+ && !c.subClassOf?.startsWith('gloss:')
54
+ && c.compact !== 'gloss:Concept'
55
+ && c.compact !== 'gloss:ConceptCollection'
56
+ )
57
+ );
58
+
59
+ const allNavItems = computed(() => {
60
+ const items: { id: string; label: string; depth: number }[] = [];
61
+ function walk(classes: OwlClass[], depth: number) {
62
+ for (const cls of classes) {
63
+ items.push({ id: cls.compact, label: cls.label, depth });
64
+ if (expandedClasses.value.has(cls.compact)) {
65
+ walk(childClasses(cls.compact), depth + 1);
66
+ }
67
+ }
68
+ }
69
+ walk(treeRoots, 0);
70
+ return items;
71
+ });
72
+
73
+ export function useOntologyNav() {
74
+ return {
75
+ activeClassId,
76
+ activeTaxonomy,
77
+ expandedClasses,
78
+ taxonomyKeys,
79
+ taxonomyLabels,
80
+ treeRoots,
81
+ supportingClasses,
82
+ allNavItems,
83
+ toggleExpand,
84
+ childClasses,
85
+ hasChildren,
86
+ };
87
+ }
@@ -1,40 +1,17 @@
1
1
  <script setup lang="ts">
2
- import { ref, computed } from 'vue';
3
- import {
4
- getClass,
5
- getAllClasses,
6
- getClassTree,
7
- getAllPropertiesForClass,
8
- getStats,
9
- type OwlClass,
10
- } from '../adapters/ontology-schema';
2
+ import { computed } from 'vue';
3
+ import { getClass, getAllPropertiesForClass, getStats } from '../adapters/ontology-schema';
11
4
  import { ontology, type TaxonomyConcept } from '../adapters/ontology-registry';
5
+ import { useOntologyNav } from '../composables/use-ontology-nav';
12
6
 
13
7
  const stats = getStats();
14
- const allClasses = getAllClasses();
15
- const treeRoots = getClassTree();
16
8
 
17
- const taxonomyKeys = [
18
- 'conceptStatus', 'entryStatus', 'normativeStatus', 'sourceType', 'sourceStatus',
19
- 'relationshipType', 'designationType', 'termType', 'grammarGender', 'grammarNumber',
20
- ];
21
-
22
- const taxonomyLabels: Record<string, string> = {
23
- conceptStatus: 'Concept Status',
24
- entryStatus: 'Entry Status',
25
- normativeStatus: 'Normative Status',
26
- sourceType: 'Source Type',
27
- sourceStatus: 'Source Status',
28
- relationshipType: 'Relationship Type',
29
- designationType: 'Designation Type',
30
- termType: 'Term Type',
31
- grammarGender: 'Grammar Gender',
32
- grammarNumber: 'Grammar Number',
33
- };
34
-
35
- const activeClassId = ref('gloss:Concept');
36
- const showTaxonomies = ref(false);
37
- const activeTaxonomy = ref<string | null>(null);
9
+ const {
10
+ activeClassId,
11
+ activeTaxonomy,
12
+ taxonomyLabels,
13
+ allNavItems,
14
+ } = useOntologyNav();
38
15
 
39
16
  const activeClass = computed(() => getClass(activeClassId.value));
40
17
  const activeProperties = computed(() => getAllPropertiesForClass(activeClassId.value));
@@ -46,39 +23,6 @@ function activeTaxonomyData() {
46
23
  const top = all.filter(c => !c.broader);
47
24
  return { scheme: ontology.getScheme(key), concepts: all, top };
48
25
  }
49
-
50
- function childClasses(parentId: string): OwlClass[] {
51
- const cls = getClass(parentId);
52
- if (!cls) return [];
53
- return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
54
- }
55
-
56
- function hasChildren(cls: OwlClass): boolean {
57
- return cls.children.length > 0;
58
- }
59
-
60
- const expandedClasses = ref(new Set<string>(['gloss:Concept', 'gloss:Designation']));
61
-
62
- function toggleExpand(cls: OwlClass) {
63
- const s = new Set(expandedClasses.value);
64
- if (s.has(cls.compact)) s.delete(cls.compact);
65
- else s.add(cls.compact);
66
- expandedClasses.value = s;
67
- }
68
-
69
- const allNavItems = computed(() => {
70
- const items: { id: string; label: string; depth: number }[] = [];
71
- function walk(classes: OwlClass[], depth: number) {
72
- for (const cls of classes) {
73
- items.push({ id: cls.compact, label: cls.label, depth });
74
- if (expandedClasses.value.has(cls.compact)) {
75
- walk(childClasses(cls.compact), depth + 1);
76
- }
77
- }
78
- }
79
- walk(treeRoots, 0);
80
- return items;
81
- });
82
26
  </script>
83
27
 
84
28
  <template>
@@ -97,12 +41,12 @@ const allNavItems = computed(() => {
97
41
  <span class="badge badge-blue text-[10px]">{{ stats.classCount }} classes</span>
98
42
  <span class="badge text-[10px] bg-emerald-50 text-emerald-700">{{ stats.objectPropertyCount }} object properties</span>
99
43
  <span class="badge text-[10px] bg-amber-50 text-amber-700">{{ stats.datatypePropertyCount }} datatype properties</span>
100
- <span class="badge text-[10px] bg-purple-50 text-purple-700">{{ taxonomyKeys.length }} SKOS taxonomies</span>
44
+ <span class="badge text-[10px] bg-purple-50 text-purple-700">10 SKOS taxonomies</span>
101
45
  </div>
102
46
  <code class="block text-xs text-ink-400 mt-2">https://www.glossarist.org/ontologies/glossarist</code>
103
47
  </div>
104
48
 
105
- <!-- Sticky mobile chips -->
49
+ <!-- Sticky mobile chips (for small screens where sidebar is hidden) -->
106
50
  <div class="lg:hidden sticky top-14 z-10 bg-surface -mx-4 px-4 py-2 border-b border-ink-100/60 mb-4">
107
51
  <div class="flex gap-2 overflow-x-auto scrollbar-none">
108
52
  <button v-for="item in allNavItems" :key="item.id"
@@ -112,10 +56,10 @@ const allNavItems = computed(() => {
112
56
  ? 'bg-ink-800 text-white'
113
57
  : 'bg-surface-raised border border-ink-100 text-ink-600 hover:bg-ink-50'"
114
58
  >
115
- <span v-if="item.depth > 0" class="text-ink-300">{{ ' '.repeat(item.depth * 2) }}</span>
59
+ <span v-if="item.depth > 0" class="text-ink-300">{{ '&nbsp;'.repeat(item.depth * 2) }}</span>
116
60
  {{ item.label }}
117
61
  </button>
118
- <button @click="activeTaxonomy = activeTaxonomy ? null : taxonomyKeys[0]; showTaxonomies = true"
62
+ <button @click="activeTaxonomy = activeTaxonomy ? null : 'conceptStatus'"
119
63
  class="flex-shrink-0 px-3 py-2.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors min-h-[44px] flex items-center gap-1.5"
120
64
  :class="activeTaxonomy
121
65
  ? 'bg-ink-800 text-white'
@@ -124,179 +68,106 @@ const allNavItems = computed(() => {
124
68
  </div>
125
69
  </div>
126
70
 
127
- <!-- Two-column layout -->
128
- <div class="lg:grid lg:grid-cols-[220px_1fr] lg:gap-6">
129
- <!-- Left: tree sidebar (desktop only) -->
130
- <nav class="hidden lg:block border-r border-ink-100/60 pr-4">
131
- <div class="section-label mb-2">Classes</div>
132
-
133
- <!-- Tree roots with recursive children -->
134
- <template v-for="root in treeRoots" :key="root.compact">
135
- <div>
136
- <button @click="activeClassId = root.compact; activeTaxonomy = null; toggleExpand(root)"
137
- class="w-full flex items-center gap-1.5 px-2 py-2 rounded-lg text-sm transition-colors"
138
- :class="activeClassId === root.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
139
- >
140
- <span v-if="hasChildren(root)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(root.compact) ? '▾' : '▸' }}</span>
141
- <span v-else class="w-3"></span>
142
- <span class="flex-1 text-left">{{ root.label }}</span>
143
- </button>
144
-
145
- <!-- Children -->
146
- <div v-if="expandedClasses.has(root.compact) && hasChildren(root)" class="ml-4">
147
- <template v-for="child in childClasses(root.compact)" :key="child.compact">
148
- <button @click="activeClassId = child.compact; activeTaxonomy = null; toggleExpand(child)"
149
- class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
150
- :class="activeClassId === child.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
151
- >
152
- <span v-if="hasChildren(child)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(child.compact) ? '▾' : '▸' }}</span>
153
- <span v-else class="w-3 text-ink-200">·</span>
154
- <span class="flex-1 text-left">{{ child.label }}</span>
155
- </button>
156
- <!-- Grandchildren -->
157
- <div v-if="expandedClasses.has(child.compact) && hasChildren(child)" class="ml-4">
158
- <button v-for="gc in childClasses(child.compact)" :key="gc.compact"
159
- @click="activeClassId = gc.compact; activeTaxonomy = null"
160
- class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
161
- :class="activeClassId === gc.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
162
- >
163
- <span class="w-3 text-ink-200">·</span>
164
- <span class="flex-1 text-left">{{ gc.label }}</span>
165
- </button>
166
- </div>
167
- </template>
168
- </div>
71
+ <!-- Detail panel -->
72
+ <div>
73
+ <!-- Class detail -->
74
+ <template v-if="!activeTaxonomy && activeClass">
75
+ <div class="pb-4 border-b border-ink-100/60 mb-4">
76
+ <h2 class="text-lg font-semibold text-ink-800">{{ activeClass.label }}</h2>
77
+ <code class="block text-xs text-ink-400 mt-1">{{ activeClass.iri }}</code>
78
+ <div v-if="activeClass.subClassOf" class="flex items-center gap-2 mt-2 text-sm">
79
+ <span class="text-ink-400 text-xs">subClassOf</span>
80
+ <code class="text-xs text-ink-600 bg-ink-50 px-2 py-0.5 rounded">{{ activeClass.subClassOf }}</code>
81
+ <template v-if="activeClass.ancestors.length > 1">
82
+ <span class="text-ink-300 text-xs">→</span>
83
+ <code class="text-xs text-ink-500">{{ activeClass.ancestors.slice(1).join(' → ') }}</code>
84
+ </template>
169
85
  </div>
170
- </template>
171
-
172
- <!-- Remaining classes not in tree (supporting) -->
173
- <div class="mt-4 pt-3 border-t border-ink-100/40">
174
- <div class="text-[10px] uppercase tracking-wide text-ink-300 mb-1.5">Supporting</div>
175
- <button v-for="cls in allClasses.filter(c => c.children.length === 0 && !c.subClassOf?.startsWith('gloss:') && c.compact !== 'gloss:Concept' && c.compact !== 'gloss:ConceptCollection')" :key="cls.compact"
176
- @click="activeClassId = cls.compact; activeTaxonomy = null"
177
- class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
178
- :class="activeClassId === cls.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
179
- >
180
- <span class="w-3 text-ink-200">·</span>
181
- <span class="flex-1 text-left">{{ cls.label }}</span>
182
- </button>
86
+ <p v-if="activeClass.comment" class="text-sm text-ink-500 mt-3 leading-relaxed">{{ activeClass.comment }}</p>
183
87
  </div>
184
88
 
185
- <!-- Taxonomies -->
186
- <div class="mt-4 pt-3 border-t border-ink-100/40">
187
- <div class="text-[10px] uppercase tracking-wide text-ink-300 mb-1.5">SKOS Taxonomies</div>
188
- <button v-for="tk in taxonomyKeys" :key="tk"
189
- @click="activeTaxonomy = tk; showTaxonomies = true"
190
- class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
191
- :class="activeTaxonomy === tk ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
192
- >
193
- <span class="w-3 text-ink-200">·</span>
194
- <span class="flex-1 text-left">{{ taxonomyLabels[tk] }}</span>
195
- </button>
89
+ <!-- Object Properties -->
90
+ <div v-if="activeProperties.object.length" class="mb-6">
91
+ <h3 class="text-xs uppercase tracking-wide text-ink-300 font-medium mb-2">
92
+ Object Properties ({{ activeProperties.object.length }})
93
+ </h3>
94
+ <table class="w-full text-sm border-collapse">
95
+ <thead>
96
+ <tr class="border-b border-ink-100/60">
97
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Property</th>
98
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Range</th>
99
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Description</th>
100
+ </tr>
101
+ </thead>
102
+ <tbody>
103
+ <tr v-for="p in activeProperties.object" :key="p.compact" class="border-b border-ink-100/30">
104
+ <td class="py-2 px-3 align-top">
105
+ <code class="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">{{ p.compact }}</code>
106
+ <div v-if="p.inverseOf" class="text-[10px] text-ink-300 mt-0.5">↔ {{ p.inverseOf }}</div>
107
+ </td>
108
+ <td class="py-2 px-3 align-top">
109
+ <code class="text-xs text-ink-500">{{ p.range || p.rangeUnion?.join(' | ') || '—' }}</code>
110
+ </td>
111
+ <td class="py-2 px-3 text-xs text-ink-400 align-top">{{ p.comment || '' }}</td>
112
+ </tr>
113
+ </tbody>
114
+ </table>
196
115
  </div>
197
- </nav>
198
116
 
199
- <!-- Right: detail panel -->
200
- <div class="mt-4 lg:mt-0">
201
- <!-- Class detail -->
202
- <template v-if="!activeTaxonomy && activeClass">
203
- <div class="pb-4 border-b border-ink-100/60 mb-4">
204
- <h2 class="text-lg font-semibold text-ink-800">{{ activeClass.label }}</h2>
205
- <code class="block text-xs text-ink-400 mt-1">{{ activeClass.iri }}</code>
206
- <div v-if="activeClass.subClassOf" class="flex items-center gap-2 mt-2 text-sm">
207
- <span class="text-ink-400 text-xs">subClassOf</span>
208
- <code class="text-xs text-ink-600 bg-ink-50 px-2 py-0.5 rounded">{{ activeClass.subClassOf }}</code>
209
- <template v-if="activeClass.ancestors.length > 1">
210
- <span class="text-ink-300 text-xs">→</span>
211
- <code class="text-xs text-ink-500">{{ activeClass.ancestors.slice(1).join(' → ') }}</code>
212
- </template>
213
- </div>
214
- <p v-if="activeClass.comment" class="text-sm text-ink-500 mt-3 leading-relaxed">{{ activeClass.comment }}</p>
215
- </div>
216
-
217
- <!-- Object Properties -->
218
- <div v-if="activeProperties.object.length" class="mb-6">
219
- <h3 class="text-xs uppercase tracking-wide text-ink-300 font-medium mb-2">
220
- Object Properties ({{ activeProperties.object.length }})
221
- </h3>
222
- <table class="w-full text-sm border-collapse">
223
- <thead>
224
- <tr class="border-b border-ink-100/60">
225
- <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Property</th>
226
- <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Range</th>
227
- <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Description</th>
228
- </tr>
229
- </thead>
230
- <tbody>
231
- <tr v-for="p in activeProperties.object" :key="p.compact" class="border-b border-ink-100/30">
232
- <td class="py-2 px-3 align-top">
233
- <code class="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">{{ p.compact }}</code>
234
- <div v-if="p.inverseOf" class="text-[10px] text-ink-300 mt-0.5">↔ {{ p.inverseOf }}</div>
235
- </td>
236
- <td class="py-2 px-3 align-top">
237
- <code class="text-xs text-ink-500">{{ p.range || p.rangeUnion?.join(' | ') || '—' }}</code>
238
- </td>
239
- <td class="py-2 px-3 text-xs text-ink-400 align-top">{{ p.comment || '' }}</td>
240
- </tr>
241
- </tbody>
242
- </table>
243
- </div>
244
-
245
- <!-- Datatype Properties -->
246
- <div v-if="activeProperties.datatype.length">
247
- <h3 class="text-xs uppercase tracking-wide text-ink-300 font-medium mb-2">
248
- Datatype Properties ({{ activeProperties.datatype.length }})
249
- </h3>
250
- <table class="w-full text-sm border-collapse">
251
- <thead>
252
- <tr class="border-b border-ink-100/60">
253
- <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Property</th>
254
- <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Datatype</th>
255
- <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Description</th>
256
- </tr>
257
- </thead>
258
- <tbody>
259
- <tr v-for="p in activeProperties.datatype" :key="p.compact" class="border-b border-ink-100/30">
260
- <td class="py-2 px-3 align-top">
261
- <code class="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">{{ p.compact }}</code>
262
- </td>
263
- <td class="py-2 px-3 align-top">
264
- <code class="text-xs text-ink-500">{{ p.range || '—' }}</code>
265
- </td>
266
- <td class="py-2 px-3 text-xs text-ink-400 align-top">{{ p.comment || '' }}</td>
267
- </tr>
268
- </tbody>
269
- </table>
270
- </div>
117
+ <!-- Datatype Properties -->
118
+ <div v-if="activeProperties.datatype.length">
119
+ <h3 class="text-xs uppercase tracking-wide text-ink-300 font-medium mb-2">
120
+ Datatype Properties ({{ activeProperties.datatype.length }})
121
+ </h3>
122
+ <table class="w-full text-sm border-collapse">
123
+ <thead>
124
+ <tr class="border-b border-ink-100/60">
125
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Property</th>
126
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Datatype</th>
127
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Description</th>
128
+ </tr>
129
+ </thead>
130
+ <tbody>
131
+ <tr v-for="p in activeProperties.datatype" :key="p.compact" class="border-b border-ink-100/30">
132
+ <td class="py-2 px-3 align-top">
133
+ <code class="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">{{ p.compact }}</code>
134
+ </td>
135
+ <td class="py-2 px-3 align-top">
136
+ <code class="text-xs text-ink-500">{{ p.range || '—' }}</code>
137
+ </td>
138
+ <td class="py-2 px-3 text-xs text-ink-400 align-top">{{ p.comment || '' }}</td>
139
+ </tr>
140
+ </tbody>
141
+ </table>
142
+ </div>
271
143
 
272
- <div v-if="!activeProperties.object.length && !activeProperties.datatype.length" class="text-sm text-ink-300 italic">
273
- No properties defined directly on this class.
274
- </div>
275
- </template>
144
+ <div v-if="!activeProperties.object.length && !activeProperties.datatype.length" class="text-sm text-ink-300 italic">
145
+ No properties defined directly on this class.
146
+ </div>
147
+ </template>
276
148
 
277
- <!-- Taxonomy detail -->
278
- <template v-if="activeTaxonomy && activeTaxonomyData()">
279
- <div class="pb-4 border-b border-ink-100/60 mb-4">
280
- <h2 class="text-lg font-semibold text-ink-800">{{ taxonomyLabels[activeTaxonomy] }}</h2>
281
- <code class="block text-xs text-ink-400 mt-1">{{ activeTaxonomyData()!.scheme }}</code>
282
- </div>
149
+ <!-- Taxonomy detail -->
150
+ <template v-if="activeTaxonomy && activeTaxonomyData()">
151
+ <div class="pb-4 border-b border-ink-100/60 mb-4">
152
+ <h2 class="text-lg font-semibold text-ink-800">{{ taxonomyLabels[activeTaxonomy] }}</h2>
153
+ <code class="block text-xs text-ink-400 mt-1">{{ activeTaxonomyData()!.scheme }}</code>
154
+ </div>
283
155
 
284
- <div class="space-y-2">
285
- <div v-for="concept in activeTaxonomyData()!.concepts" :key="concept.id"
286
- class="border border-ink-100/60 rounded-lg p-3">
287
- <div class="flex items-center gap-2">
288
- <code class="text-xs font-semibold text-ink-700">{{ concept.id }}</code>
289
- <span class="text-sm text-ink-600">{{ concept.prefLabel }}</span>
290
- <span v-if="concept.altLabel" class="text-xs text-ink-400">({{ concept.altLabel }})</span>
291
- </div>
292
- <p v-if="concept.definition" class="text-xs text-ink-400 mt-1 leading-relaxed">{{ concept.definition }}</p>
293
- <div v-if="concept.broader" class="text-[10px] text-ink-300 mt-1">
294
- broader: <code class="text-ink-400">{{ concept.broader }}</code>
295
- </div>
156
+ <div class="space-y-2">
157
+ <div v-for="concept in activeTaxonomyData()!.concepts" :key="concept.id"
158
+ class="border border-ink-100/60 rounded-lg p-3">
159
+ <div class="flex items-center gap-2">
160
+ <code class="text-xs font-semibold text-ink-700">{{ concept.id }}</code>
161
+ <span class="text-sm text-ink-600">{{ concept.prefLabel }}</span>
162
+ <span v-if="concept.altLabel" class="text-xs text-ink-400">({{ concept.altLabel }})</span>
163
+ </div>
164
+ <p v-if="concept.definition" class="text-xs text-ink-400 mt-1 leading-relaxed">{{ concept.definition }}</p>
165
+ <div v-if="concept.broader" class="text-[10px] text-ink-300 mt-1">
166
+ broader: <code class="text-ink-400">{{ concept.broader }}</code>
296
167
  </div>
297
168
  </div>
298
- </template>
299
- </div>
169
+ </div>
170
+ </template>
300
171
  </div>
301
172
  </div>
302
173
  </template>