@glossarist/concept-browser 0.4.4 → 0.4.6

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.4",
3
+ "version": "0.4.6",
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": {
@@ -1,10 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue';
2
+ import { computed, watch } 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 { useOntologyNav } from '../composables/use-ontology-nav';
8
9
  import NavIcon from './NavIcon.vue';
9
10
 
10
11
  const store = useVocabularyStore();
@@ -16,6 +17,22 @@ 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
+ isOverview,
29
+ toggleExpand,
30
+ childClasses,
31
+ hasChildren,
32
+ } = useOntologyNav();
33
+
34
+ const isOntologyRoute = computed(() => route.name === 'ontology');
35
+
19
36
  const datasetEntries = computed(() => {
20
37
  const entries: { id: string; title: string; loaded: boolean; conceptCount: number }[] = [];
21
38
  for (const [id, adapter] of store.datasets) {
@@ -56,6 +73,22 @@ function isActive(page: { route: string; datasetScoped?: boolean }): boolean {
56
73
  }
57
74
  return route.name === page.route;
58
75
  }
76
+
77
+ function selectClass(id: string) {
78
+ activeClassId.value = id;
79
+ activeTaxonomy.value = null;
80
+ scrollMainToTop();
81
+ }
82
+
83
+ function selectTaxonomy(key: string) {
84
+ activeTaxonomy.value = key;
85
+ scrollMainToTop();
86
+ }
87
+
88
+ function scrollMainToTop() {
89
+ const main = document.querySelector('main');
90
+ if (main) main.scrollTo({ top: 0, behavior: 'smooth' });
91
+ }
59
92
  </script>
60
93
 
61
94
  <template>
@@ -72,17 +105,91 @@ function isActive(page: { route: string; datasetScoped?: boolean }): boolean {
72
105
  <!-- Navigation -->
73
106
  <div class="section-label">Navigation</div>
74
107
  <nav class="space-y-0.5 mb-6">
75
- <router-link
76
- v-for="page in globalPages"
77
- :key="page.route || 'home'"
78
- :to="pageRoute(page)"
79
- class="btn-ghost w-full text-left flex items-center gap-2"
80
- :class="isActive(page) ? 'active' : ''"
81
- @click="closeMobile"
82
- >
83
- <NavIcon :name="page.icon" />
84
- {{ page.title }}
85
- </router-link>
108
+ <template v-for="page in globalPages" :key="page.route || 'home'">
109
+ <router-link
110
+ :to="pageRoute(page)"
111
+ class="btn-ghost w-full text-left flex items-center gap-2"
112
+ :class="isActive(page) ? 'active' : ''"
113
+ @click="closeMobile"
114
+ >
115
+ <NavIcon :name="page.icon" />
116
+ {{ page.title }}
117
+ </router-link>
118
+
119
+ <!-- Ontology class tree nested under Ontology nav item -->
120
+ <div v-if="page.route === 'ontology' && isOntologyRoute" class="ml-4 mt-1 mb-2 space-y-0.5">
121
+ <!-- Overview -->
122
+ <button @click="activeClassId = null; activeTaxonomy = null; scrollMainToTop()"
123
+ class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
124
+ :class="isOverview ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
125
+ >
126
+ <span class="w-3 text-ink-200">·</span>
127
+ <span class="flex-1 text-left">Overview</span>
128
+ </button>
129
+
130
+ <div class="text-[10px] uppercase tracking-wide text-ink-300 mt-2 mb-1 px-2">Classes</div>
131
+ <template v-for="root in treeRoots" :key="root.compact">
132
+ <button @click="selectClass(root.compact); toggleExpand(root)"
133
+ class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
134
+ :class="activeClassId === root.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
135
+ >
136
+ <span v-if="hasChildren(root)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(root.compact) ? '▾' : '▸' }}</span>
137
+ <span v-else class="w-3 text-ink-200">·</span>
138
+ <span class="flex-1 text-left">{{ root.label }}</span>
139
+ </button>
140
+ <!-- Children -->
141
+ <div v-if="expandedClasses.has(root.compact) && hasChildren(root)" class="ml-3">
142
+ <template v-for="child in childClasses(root.compact)" :key="child.compact">
143
+ <button @click="selectClass(child.compact); toggleExpand(child)"
144
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
145
+ :class="activeClassId === child.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
146
+ >
147
+ <span v-if="hasChildren(child)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(child.compact) ? '▾' : '▸' }}</span>
148
+ <span v-else class="w-3 text-ink-200">·</span>
149
+ <span class="flex-1 text-left">{{ child.label }}</span>
150
+ </button>
151
+ <!-- Grandchildren -->
152
+ <div v-if="expandedClasses.has(child.compact) && hasChildren(child)" class="ml-3">
153
+ <button v-for="gc in childClasses(child.compact)" :key="gc.compact"
154
+ @click="selectClass(gc.compact)"
155
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
156
+ :class="activeClassId === gc.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
157
+ >
158
+ <span class="w-3 text-ink-200">·</span>
159
+ <span class="flex-1 text-left">{{ gc.label }}</span>
160
+ </button>
161
+ </div>
162
+ </template>
163
+ </div>
164
+ </template>
165
+
166
+ <!-- Supporting classes -->
167
+ <div v-if="supportingClasses.length" class="mt-2 pt-2 border-t border-ink-100/40">
168
+ <div class="text-[10px] uppercase tracking-wide text-ink-300 mb-1 px-2">Supporting</div>
169
+ <button v-for="cls in supportingClasses" :key="cls.compact"
170
+ @click="selectClass(cls.compact)"
171
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
172
+ :class="activeClassId === cls.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
173
+ >
174
+ <span class="w-3 text-ink-200">·</span>
175
+ <span class="flex-1 text-left">{{ cls.label }}</span>
176
+ </button>
177
+ </div>
178
+
179
+ <!-- SKOS Taxonomies -->
180
+ <div class="mt-2 pt-2 border-t border-ink-100/40">
181
+ <div class="text-[10px] uppercase tracking-wide text-ink-300 mb-1 px-2">Taxonomies</div>
182
+ <button v-for="tk in taxonomyKeys" :key="tk"
183
+ @click="selectTaxonomy(tk)"
184
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
185
+ :class="activeTaxonomy === tk ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
186
+ >
187
+ <span class="w-3 text-ink-200">·</span>
188
+ <span class="flex-1 text-left">{{ taxonomyLabels[tk] }}</span>
189
+ </button>
190
+ </div>
191
+ </div>
192
+ </template>
86
193
  </nav>
87
194
 
88
195
  <!-- Dataset-level navigation (shown when viewing a dataset) -->
@@ -0,0 +1,92 @@
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 OVERVIEW_ID = '__overview__';
10
+
11
+ const activeClassId = ref<string | null>(null);
12
+ const activeTaxonomy = ref<string | null>(null);
13
+ const expandedClasses = ref(new Set<string>(['gloss:Designation']));
14
+
15
+ const taxonomyKeys = [
16
+ 'conceptStatus', 'entryStatus', 'normativeStatus', 'sourceType', 'sourceStatus',
17
+ 'relationshipType', 'designationType', 'termType', 'grammarGender', 'grammarNumber',
18
+ ];
19
+
20
+ const taxonomyLabels: Record<string, string> = {
21
+ conceptStatus: 'Concept Status',
22
+ entryStatus: 'Entry Status',
23
+ normativeStatus: 'Normative Status',
24
+ sourceType: 'Source Type',
25
+ sourceStatus: 'Source Status',
26
+ relationshipType: 'Relationship Type',
27
+ designationType: 'Designation Type',
28
+ termType: 'Term Type',
29
+ grammarGender: 'Grammar Gender',
30
+ grammarNumber: 'Grammar Number',
31
+ };
32
+
33
+ function toggleExpand(cls: OwlClass) {
34
+ const s = new Set(expandedClasses.value);
35
+ if (s.has(cls.compact)) s.delete(cls.compact);
36
+ else s.add(cls.compact);
37
+ expandedClasses.value = s;
38
+ }
39
+
40
+ function childClasses(parentId: string): OwlClass[] {
41
+ const cls = getClass(parentId);
42
+ if (!cls) return [];
43
+ return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
44
+ }
45
+
46
+ function hasChildren(cls: OwlClass): boolean {
47
+ return cls.children.length > 0;
48
+ }
49
+
50
+ const treeRoots = getClassTree();
51
+
52
+ const supportingClasses = computed(() =>
53
+ getAllClasses().filter(
54
+ c => c.children.length === 0
55
+ && !c.subClassOf?.startsWith('gloss:')
56
+ && c.compact !== 'gloss:Concept'
57
+ && c.compact !== 'gloss:ConceptCollection'
58
+ )
59
+ );
60
+
61
+ const isOverview = computed(() => activeClassId.value === null && activeTaxonomy.value === null);
62
+
63
+ const allNavItems = computed(() => {
64
+ const items: { id: string; label: string; depth: number }[] = [];
65
+ function walk(classes: OwlClass[], depth: number) {
66
+ for (const cls of classes) {
67
+ items.push({ id: cls.compact, label: cls.label, depth });
68
+ if (expandedClasses.value.has(cls.compact)) {
69
+ walk(childClasses(cls.compact), depth + 1);
70
+ }
71
+ }
72
+ }
73
+ walk(treeRoots, 0);
74
+ return items;
75
+ });
76
+
77
+ export function useOntologyNav() {
78
+ return {
79
+ activeClassId,
80
+ activeTaxonomy,
81
+ expandedClasses,
82
+ taxonomyKeys,
83
+ taxonomyLabels,
84
+ treeRoots,
85
+ supportingClasses,
86
+ allNavItems,
87
+ isOverview,
88
+ toggleExpand,
89
+ childClasses,
90
+ hasChildren,
91
+ };
92
+ }
@@ -1,43 +1,24 @@
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, watch } from 'vue';
3
+ import { getClass, getAllPropertiesForClass, getAllClasses, 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
-
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
8
 
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
+ isOverview,
15
+ treeRoots,
16
+ hasChildren,
17
+ childClasses,
18
+ } = useOntologyNav();
38
19
 
39
- const activeClass = computed(() => getClass(activeClassId.value));
40
- const activeProperties = computed(() => getAllPropertiesForClass(activeClassId.value));
20
+ const activeClass = computed(() => activeClassId.value ? getClass(activeClassId.value) : null);
21
+ const activeProperties = computed(() => activeClassId.value ? getAllPropertiesForClass(activeClassId.value) : { object: [], datatype: [] });
41
22
 
42
23
  function activeTaxonomyData() {
43
24
  if (!activeTaxonomy.value) return null;
@@ -47,38 +28,13 @@ function activeTaxonomyData() {
47
28
  return { scheme: ontology.getScheme(key), concepts: all, top };
48
29
  }
49
30
 
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;
31
+ // Scroll to top when selection changes
32
+ watch([activeClassId, activeTaxonomy], () => {
33
+ const main = document.querySelector('main');
34
+ if (main) main.scrollTo({ top: 0, behavior: 'smooth' });
81
35
  });
36
+
37
+ const allClasses = getAllClasses();
82
38
  </script>
83
39
 
84
40
  <template>
@@ -97,14 +53,18 @@ const allNavItems = computed(() => {
97
53
  <span class="badge badge-blue text-[10px]">{{ stats.classCount }} classes</span>
98
54
  <span class="badge text-[10px] bg-emerald-50 text-emerald-700">{{ stats.objectPropertyCount }} object properties</span>
99
55
  <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>
56
+ <span class="badge text-[10px] bg-purple-50 text-purple-700">10 SKOS taxonomies</span>
101
57
  </div>
102
58
  <code class="block text-xs text-ink-400 mt-2">https://www.glossarist.org/ontologies/glossarist</code>
103
59
  </div>
104
60
 
105
- <!-- Sticky mobile chips -->
61
+ <!-- Sticky mobile chips (for small screens where sidebar is hidden) -->
106
62
  <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
63
  <div class="flex gap-2 overflow-x-auto scrollbar-none">
64
+ <button @click="activeClassId = null; activeTaxonomy = null"
65
+ 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"
66
+ :class="isOverview ? 'bg-ink-800 text-white' : 'bg-surface-raised border border-ink-100 text-ink-600 hover:bg-ink-50'"
67
+ >Overview</button>
108
68
  <button v-for="item in allNavItems" :key="item.id"
109
69
  @click="activeClassId = item.id; activeTaxonomy = null"
110
70
  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"
@@ -112,10 +72,10 @@ const allNavItems = computed(() => {
112
72
  ? 'bg-ink-800 text-white'
113
73
  : 'bg-surface-raised border border-ink-100 text-ink-600 hover:bg-ink-50'"
114
74
  >
115
- <span v-if="item.depth > 0" class="text-ink-300">{{ ' '.repeat(item.depth * 2) }}</span>
75
+ <span v-if="item.depth > 0" class="text-ink-300">{{ '&nbsp;'.repeat(item.depth * 2) }}</span>
116
76
  {{ item.label }}
117
77
  </button>
118
- <button @click="activeTaxonomy = activeTaxonomy ? null : taxonomyKeys[0]; showTaxonomies = true"
78
+ <button @click="activeTaxonomy = activeTaxonomy ? null : 'conceptStatus'"
119
79
  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
80
  :class="activeTaxonomy
121
81
  ? 'bg-ink-800 text-white'
@@ -124,179 +84,125 @@ const allNavItems = computed(() => {
124
84
  </div>
125
85
  </div>
126
86
 
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>
87
+ <!-- Detail panel -->
88
+ <div>
89
+ <!-- Overview: all classes summary -->
90
+ <template v-if="isOverview">
91
+ <h2 class="text-lg font-semibold text-ink-800 mb-4">Class Overview</h2>
92
+ <div class="grid gap-3 sm:grid-cols-2">
93
+ <div v-for="cls in allClasses" :key="cls.compact"
94
+ @click="activeClassId = cls.compact"
95
+ class="border border-ink-100/60 rounded-lg p-3 cursor-pointer hover:border-ink-200 hover:bg-ink-50/50 transition-colors">
96
+ <div class="flex items-center gap-2">
97
+ <span class="text-sm font-medium text-ink-700">{{ cls.label }}</span>
98
+ <code class="text-[10px] text-ink-400 bg-ink-50 px-1.5 py-0.5 rounded">{{ cls.compact }}</code>
99
+ </div>
100
+ <p v-if="cls.comment" class="text-xs text-ink-400 mt-1 line-clamp-2">{{ cls.comment }}</p>
101
+ <div v-if="cls.subClassOf" class="text-[10px] text-ink-300 mt-1">
102
+ subClassOf <code class="text-ink-400">{{ cls.subClassOf }}</code>
168
103
  </div>
169
104
  </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>
183
105
  </div>
184
-
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>
196
- </div>
197
- </nav>
198
-
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>
106
+ </template>
107
+
108
+ <!-- Class detail -->
109
+ <template v-if="!activeTaxonomy && activeClass">
110
+ <div class="pb-4 border-b border-ink-100/60 mb-4">
111
+ <h2 class="text-lg font-semibold text-ink-800">{{ activeClass.label }}</h2>
112
+ <code class="block text-xs text-ink-400 mt-1">{{ activeClass.iri }}</code>
113
+ <div v-if="activeClass.subClassOf" class="flex items-center gap-2 mt-2 text-sm">
114
+ <span class="text-ink-400 text-xs">subClassOf</span>
115
+ <code class="text-xs text-ink-600 bg-ink-50 px-2 py-0.5 rounded">{{ activeClass.subClassOf }}</code>
116
+ <template v-if="activeClass.ancestors.length > 1">
117
+ <span class="text-ink-300 text-xs">→</span>
118
+ <code class="text-xs text-ink-500">{{ activeClass.ancestors.slice(1).join(' → ') }}</code>
119
+ </template>
215
120
  </div>
121
+ <p v-if="activeClass.comment" class="text-sm text-ink-500 mt-3 leading-relaxed">{{ activeClass.comment }}</p>
122
+ </div>
216
123
 
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>
124
+ <!-- Object Properties -->
125
+ <div v-if="activeProperties.object.length" class="mb-6">
126
+ <h3 class="text-xs uppercase tracking-wide text-ink-300 font-medium mb-2">
127
+ Object Properties ({{ activeProperties.object.length }})
128
+ </h3>
129
+ <table class="w-full text-sm border-collapse">
130
+ <thead>
131
+ <tr class="border-b border-ink-100/60">
132
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Property</th>
133
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Range</th>
134
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Description</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ <tr v-for="p in activeProperties.object" :key="p.compact" class="border-b border-ink-100/30">
139
+ <td class="py-2 px-3 align-top">
140
+ <code class="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">{{ p.compact }}</code>
141
+ <div v-if="p.inverseOf" class="text-[10px] text-ink-300 mt-0.5">↔ {{ p.inverseOf }}</div>
142
+ </td>
143
+ <td class="py-2 px-3 align-top">
144
+ <code class="text-xs text-ink-500">{{ p.range || p.rangeUnion?.join(' | ') || '—' }}</code>
145
+ </td>
146
+ <td class="py-2 px-3 text-xs text-ink-400 align-top">{{ p.comment || '' }}</td>
147
+ </tr>
148
+ </tbody>
149
+ </table>
150
+ </div>
244
151
 
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>
152
+ <!-- Datatype Properties -->
153
+ <div v-if="activeProperties.datatype.length">
154
+ <h3 class="text-xs uppercase tracking-wide text-ink-300 font-medium mb-2">
155
+ Datatype Properties ({{ activeProperties.datatype.length }})
156
+ </h3>
157
+ <table class="w-full text-sm border-collapse">
158
+ <thead>
159
+ <tr class="border-b border-ink-100/60">
160
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Property</th>
161
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Datatype</th>
162
+ <th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Description</th>
163
+ </tr>
164
+ </thead>
165
+ <tbody>
166
+ <tr v-for="p in activeProperties.datatype" :key="p.compact" class="border-b border-ink-100/30">
167
+ <td class="py-2 px-3 align-top">
168
+ <code class="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">{{ p.compact }}</code>
169
+ </td>
170
+ <td class="py-2 px-3 align-top">
171
+ <code class="text-xs text-ink-500">{{ p.range || '—' }}</code>
172
+ </td>
173
+ <td class="py-2 px-3 text-xs text-ink-400 align-top">{{ p.comment || '' }}</td>
174
+ </tr>
175
+ </tbody>
176
+ </table>
177
+ </div>
271
178
 
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>
179
+ <div v-if="!activeProperties.object.length && !activeProperties.datatype.length" class="text-sm text-ink-300 italic">
180
+ No properties defined directly on this class.
181
+ </div>
182
+ </template>
276
183
 
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>
184
+ <!-- Taxonomy detail -->
185
+ <template v-if="activeTaxonomy && activeTaxonomyData()">
186
+ <div class="pb-4 border-b border-ink-100/60 mb-4">
187
+ <h2 class="text-lg font-semibold text-ink-800">{{ taxonomyLabels[activeTaxonomy] }}</h2>
188
+ <code class="block text-xs text-ink-400 mt-1">{{ activeTaxonomyData()!.scheme }}</code>
189
+ </div>
283
190
 
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>
191
+ <div class="space-y-2">
192
+ <div v-for="concept in activeTaxonomyData()!.concepts" :key="concept.id"
193
+ class="border border-ink-100/60 rounded-lg p-3">
194
+ <div class="flex items-center gap-2">
195
+ <code class="text-xs font-semibold text-ink-700">{{ concept.id }}</code>
196
+ <span class="text-sm text-ink-600">{{ concept.prefLabel }}</span>
197
+ <span v-if="concept.altLabel" class="text-xs text-ink-400">({{ concept.altLabel }})</span>
198
+ </div>
199
+ <p v-if="concept.definition" class="text-xs text-ink-400 mt-1 leading-relaxed">{{ concept.definition }}</p>
200
+ <div v-if="concept.broader" class="text-[10px] text-ink-300 mt-1">
201
+ broader: <code class="text-ink-400">{{ concept.broader }}</code>
296
202
  </div>
297
203
  </div>
298
- </template>
299
- </div>
204
+ </div>
205
+ </template>
300
206
  </div>
301
207
  </div>
302
208
  </template>