@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 +1 -1
- package/src/components/AppSidebar.vue +119 -12
- package/src/composables/use-ontology-nav.ts +92 -0
- package/src/views/OntologySchemaView.vue +137 -231
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.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
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 {
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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">
|
|
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">{{ '
|
|
75
|
+
<span v-if="item.depth > 0" class="text-ink-300">{{ ' '.repeat(item.depth * 2) }}</span>
|
|
116
76
|
{{ item.label }}
|
|
117
77
|
</button>
|
|
118
|
-
<button @click="activeTaxonomy = activeTaxonomy ? null :
|
|
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
|
-
<!--
|
|
128
|
-
<div
|
|
129
|
-
<!--
|
|
130
|
-
<
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
class="
|
|
138
|
-
|
|
139
|
-
>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
<
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
</
|
|
299
|
-
</
|
|
204
|
+
</div>
|
|
205
|
+
</template>
|
|
300
206
|
</div>
|
|
301
207
|
</div>
|
|
302
208
|
</template>
|