@glossarist/concept-browser 0.4.12 → 0.4.14
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 +2 -2
- package/scripts/generate-ontology-schema.mjs +312 -10
- package/src/adapters/ontology-schema.ts +89 -4
- package/src/components/AppSidebar.vue +281 -43
- package/src/components/ConceptRdfView.vue +3 -3
- package/src/composables/use-ontology-nav.ts +183 -13
- package/src/data/ontology-schema.json +1721 -153
- package/src/router/index.ts +10 -0
- package/src/views/OntologySchemaView.vue +331 -14
|
@@ -19,16 +19,27 @@ const currentDataset = computed(() => route.params.registerId as string ?? '');
|
|
|
19
19
|
|
|
20
20
|
const {
|
|
21
21
|
expandedClasses,
|
|
22
|
+
collapsedSections,
|
|
23
|
+
searchQuery,
|
|
22
24
|
taxonomyKeys,
|
|
23
25
|
taxonomyLabels,
|
|
24
26
|
treeRoots,
|
|
27
|
+
allShapes,
|
|
28
|
+
objectProperties,
|
|
29
|
+
datatypeProperties,
|
|
30
|
+
annotationProperties,
|
|
31
|
+
groupedIndividuals,
|
|
32
|
+
totalIndividuals,
|
|
33
|
+
searchResults,
|
|
25
34
|
toggleExpand,
|
|
35
|
+
toggleSection,
|
|
26
36
|
childClasses,
|
|
27
37
|
hasChildren,
|
|
38
|
+
ENTITY_TYPE_META,
|
|
28
39
|
} = useOntologyNav();
|
|
29
40
|
|
|
30
41
|
const isOntologyRoute = computed(() =>
|
|
31
|
-
|
|
42
|
+
['ontology', 'ontology-class', 'ontology-taxonomy', 'ontology-shape', 'ontology-property'].includes(route.name as string)
|
|
32
43
|
);
|
|
33
44
|
|
|
34
45
|
const activeClassId = computed(() => {
|
|
@@ -42,6 +53,18 @@ const activeTaxonomy = computed(() => {
|
|
|
42
53
|
return route.params.taxonomyKey as string;
|
|
43
54
|
});
|
|
44
55
|
|
|
56
|
+
const activeShapeId = computed(() => {
|
|
57
|
+
if (route.name !== 'ontology-shape') return null;
|
|
58
|
+
const slug = route.params.shapeId as string;
|
|
59
|
+
return slug.replace(/-/g, ':');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const activePropertyId = computed(() => {
|
|
63
|
+
if (route.name !== 'ontology-property') return null;
|
|
64
|
+
const slug = route.params.propertyId as string;
|
|
65
|
+
return slug.replace(/-/g, ':');
|
|
66
|
+
});
|
|
67
|
+
|
|
45
68
|
const isOverview = computed(() => route.name === 'ontology');
|
|
46
69
|
|
|
47
70
|
const datasetEntries = computed(() => {
|
|
@@ -92,6 +115,16 @@ function selectClass(id: string) {
|
|
|
92
115
|
function selectTaxonomy(key: string) {
|
|
93
116
|
router.push(`/ontology/taxonomy/${key}`);
|
|
94
117
|
}
|
|
118
|
+
|
|
119
|
+
function selectShape(id: string) {
|
|
120
|
+
router.push(`/ontology/shape/${compactToSlug(id)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function selectProperty(id: string) {
|
|
124
|
+
router.push(`/ontology/property/${compactToSlug(id)}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const isSearching = computed(() => !!searchQuery.value.trim());
|
|
95
128
|
</script>
|
|
96
129
|
|
|
97
130
|
<template>
|
|
@@ -119,9 +152,22 @@ function selectTaxonomy(key: string) {
|
|
|
119
152
|
{{ page.title }}
|
|
120
153
|
</router-link>
|
|
121
154
|
|
|
122
|
-
<!-- Ontology
|
|
123
|
-
<div v-if="page.route === 'ontology' && isOntologyRoute" class="ml-
|
|
124
|
-
<!--
|
|
155
|
+
<!-- Ontology entity sections nested under Ontology nav item -->
|
|
156
|
+
<div v-if="page.route === 'ontology' && isOntologyRoute" class="ml-3 mt-1 mb-2 space-y-0.5">
|
|
157
|
+
<!-- Search input -->
|
|
158
|
+
<div class="relative mb-1.5">
|
|
159
|
+
<input
|
|
160
|
+
v-model="searchQuery"
|
|
161
|
+
type="text"
|
|
162
|
+
placeholder="Search entities..."
|
|
163
|
+
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"
|
|
164
|
+
/>
|
|
165
|
+
<span v-if="searchResults" class="absolute right-2 top-1/2 -translate-y-1/2 text-[9px] text-ink-400">
|
|
166
|
+
{{ searchResults.total }}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Overview link -->
|
|
125
171
|
<router-link to="/ontology"
|
|
126
172
|
class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
|
|
127
173
|
:class="isOverview ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
|
|
@@ -130,54 +176,246 @@ function selectTaxonomy(key: string) {
|
|
|
130
176
|
<span class="flex-1 text-left">Overview</span>
|
|
131
177
|
</router-link>
|
|
132
178
|
|
|
133
|
-
|
|
134
|
-
<template v-
|
|
135
|
-
<
|
|
136
|
-
class="
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
179
|
+
<!-- Search results mode -->
|
|
180
|
+
<template v-if="isSearching && searchResults">
|
|
181
|
+
<div v-if="searchResults.classes.length" class="mt-1">
|
|
182
|
+
<div class="px-2 py-1 text-[10px] uppercase tracking-wide text-blue-500 font-medium">Classes ({{ searchResults.classes.length }})</div>
|
|
183
|
+
<button v-for="cls in searchResults.classes" :key="cls.compact"
|
|
184
|
+
@click="selectClass(cls.compact); searchQuery = ''"
|
|
185
|
+
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"
|
|
186
|
+
>
|
|
187
|
+
<span class="w-3 text-ink-200">·</span>
|
|
188
|
+
<span class="flex-1 text-left truncate">{{ cls.label }}</span>
|
|
189
|
+
<code class="text-[9px] text-ink-300">{{ cls.compact }}</code>
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
<div v-if="searchResults.objectProperties.length" class="mt-1">
|
|
193
|
+
<div class="px-2 py-1 text-[10px] uppercase tracking-wide text-emerald-500 font-medium">Object Properties ({{ searchResults.objectProperties.length }})</div>
|
|
194
|
+
<button v-for="p in searchResults.objectProperties" :key="p.compact"
|
|
195
|
+
@click="selectProperty(p.compact); searchQuery = ''"
|
|
196
|
+
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"
|
|
197
|
+
>
|
|
198
|
+
<span class="w-3 text-ink-200">·</span>
|
|
199
|
+
<span class="flex-1 text-left truncate">{{ p.label }}</span>
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
<div v-if="searchResults.datatypeProperties.length" class="mt-1">
|
|
203
|
+
<div class="px-2 py-1 text-[10px] uppercase tracking-wide text-amber-500 font-medium">Datatype Properties ({{ searchResults.datatypeProperties.length }})</div>
|
|
204
|
+
<button v-for="p in searchResults.datatypeProperties" :key="p.compact"
|
|
205
|
+
@click="selectProperty(p.compact); searchQuery = ''"
|
|
206
|
+
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"
|
|
207
|
+
>
|
|
208
|
+
<span class="w-3 text-ink-200">·</span>
|
|
209
|
+
<span class="flex-1 text-left truncate">{{ p.label }}</span>
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
<div v-if="searchResults.shapes.length" class="mt-1">
|
|
213
|
+
<div class="px-2 py-1 text-[10px] uppercase tracking-wide text-purple-500 font-medium">SHACL Shapes ({{ searchResults.shapes.length }})</div>
|
|
214
|
+
<button v-for="s in searchResults.shapes" :key="s.compact"
|
|
215
|
+
@click="selectShape(s.compact); searchQuery = ''"
|
|
216
|
+
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"
|
|
217
|
+
>
|
|
218
|
+
<span class="w-3 text-ink-200">·</span>
|
|
219
|
+
<span class="flex-1 text-left truncate">{{ s.label }}</span>
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
<div v-if="searchResults.individuals.length" class="mt-1">
|
|
223
|
+
<div class="px-2 py-1 text-[10px] uppercase tracking-wide text-rose-500 font-medium">Named Individuals ({{ searchResults.individuals.length }})</div>
|
|
224
|
+
<button v-for="ind in searchResults.individuals" :key="ind.group + '/' + ind.id"
|
|
225
|
+
@click="selectTaxonomy(ind.group); searchQuery = ''"
|
|
226
|
+
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"
|
|
227
|
+
>
|
|
228
|
+
<span class="w-3 text-ink-200">·</span>
|
|
229
|
+
<span class="flex-1 text-left truncate">{{ ind.prefLabel }}</span>
|
|
230
|
+
<span class="text-[9px] text-ink-300">{{ taxonomyLabels[ind.group] }}</span>
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
<div v-if="searchResults.annotationProperties.length" class="mt-1">
|
|
234
|
+
<div class="px-2 py-1 text-[10px] uppercase tracking-wide text-pink-500 font-medium">Annotation Properties ({{ searchResults.annotationProperties.length }})</div>
|
|
235
|
+
<div v-for="ap in searchResults.annotationProperties" :key="ap.compact"
|
|
236
|
+
class="px-2 py-0.5 text-[11px] text-ink-500"
|
|
237
|
+
>
|
|
238
|
+
<span class="w-3 inline-block text-ink-200">·</span>
|
|
239
|
+
{{ ap.compact }}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
<div v-if="searchResults.total === 0" class="px-2 py-3 text-[11px] text-ink-300 italic">
|
|
243
|
+
No entities match "{{ searchQuery }}"
|
|
244
|
+
</div>
|
|
245
|
+
</template>
|
|
246
|
+
|
|
247
|
+
<!-- Normal browse mode -->
|
|
248
|
+
<template v-if="!isSearching">
|
|
249
|
+
<!-- Classes section -->
|
|
250
|
+
<div class="mt-1">
|
|
251
|
+
<button @click="toggleSection('class')"
|
|
252
|
+
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"
|
|
253
|
+
>
|
|
254
|
+
<span class="w-3 text-[10px]">{{ collapsedSections.has('class') ? '▸' : '▾' }}</span>
|
|
255
|
+
<span class="flex-1 text-left">Classes</span>
|
|
256
|
+
<span class="badge text-[9px] bg-blue-50 text-blue-600 px-1 py-0.5">{{ treeRoots.length }}+</span>
|
|
257
|
+
</button>
|
|
258
|
+
<div v-if="!collapsedSections.has('class')" class="mt-0.5 space-y-0">
|
|
259
|
+
<template v-for="root in treeRoots" :key="root.compact">
|
|
260
|
+
<button @click="selectClass(root.compact); toggleExpand(root)"
|
|
261
|
+
class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
|
|
262
|
+
:class="activeClassId === root.compact && !activeTaxonomy && !activeShapeId && !activePropertyId ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
|
|
263
|
+
>
|
|
264
|
+
<span v-if="hasChildren(root)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(root.compact) ? '▾' : '▸' }}</span>
|
|
265
|
+
<span v-else class="w-3 text-ink-200">·</span>
|
|
266
|
+
<span class="flex-1 text-left">{{ root.label }}</span>
|
|
267
|
+
</button>
|
|
268
|
+
<div v-if="expandedClasses.has(root.compact) && hasChildren(root)" class="ml-3">
|
|
269
|
+
<template v-for="child in childClasses(root.compact)" :key="child.compact">
|
|
270
|
+
<button @click="selectClass(child.compact); toggleExpand(child)"
|
|
271
|
+
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
272
|
+
:class="activeClassId === child.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
|
|
273
|
+
>
|
|
274
|
+
<span v-if="hasChildren(child)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(child.compact) ? '▾' : '▸' }}</span>
|
|
275
|
+
<span v-else class="w-3 text-ink-200">·</span>
|
|
276
|
+
<span class="flex-1 text-left">{{ child.label }}</span>
|
|
277
|
+
</button>
|
|
278
|
+
<div v-if="expandedClasses.has(child.compact) && hasChildren(child)" class="ml-3">
|
|
279
|
+
<button v-for="gc in childClasses(child.compact)" :key="gc.compact"
|
|
280
|
+
@click="selectClass(gc.compact)"
|
|
281
|
+
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
282
|
+
:class="activeClassId === gc.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
|
|
283
|
+
>
|
|
284
|
+
<span class="w-3 text-ink-200">·</span>
|
|
285
|
+
<span class="flex-1 text-left">{{ gc.label }}</span>
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
</template>
|
|
289
|
+
</div>
|
|
290
|
+
</template>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<!-- Object Properties section -->
|
|
295
|
+
<div class="mt-1">
|
|
296
|
+
<button @click="toggleSection('objectProperty')"
|
|
297
|
+
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"
|
|
298
|
+
>
|
|
299
|
+
<span class="w-3 text-[10px]">{{ collapsedSections.has('objectProperty') ? '▸' : '▾' }}</span>
|
|
300
|
+
<span class="flex-1 text-left">Object Properties</span>
|
|
301
|
+
<span class="badge text-[9px] bg-emerald-50 text-emerald-600 px-1 py-0.5">{{ objectProperties.length }}</span>
|
|
302
|
+
</button>
|
|
303
|
+
<div v-if="!collapsedSections.has('objectProperty')" class="mt-0.5 max-h-40 overflow-y-auto">
|
|
304
|
+
<button v-for="p in objectProperties" :key="p.compact"
|
|
305
|
+
@click="selectProperty(p.compact)"
|
|
306
|
+
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
307
|
+
:class="activePropertyId === p.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
|
|
308
|
+
>
|
|
309
|
+
<span class="w-3 text-ink-200">·</span>
|
|
310
|
+
<span class="flex-1 text-left truncate">{{ p.label }}</span>
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<!-- Datatype Properties section -->
|
|
316
|
+
<div class="mt-1">
|
|
317
|
+
<button @click="toggleSection('datatypeProperty')"
|
|
318
|
+
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"
|
|
319
|
+
>
|
|
320
|
+
<span class="w-3 text-[10px]">{{ collapsedSections.has('datatypeProperty') ? '▸' : '▾' }}</span>
|
|
321
|
+
<span class="flex-1 text-left">Datatype Properties</span>
|
|
322
|
+
<span class="badge text-[9px] bg-amber-50 text-amber-600 px-1 py-0.5">{{ datatypeProperties.length }}</span>
|
|
323
|
+
</button>
|
|
324
|
+
<div v-if="!collapsedSections.has('datatypeProperty')" class="mt-0.5 max-h-40 overflow-y-auto">
|
|
325
|
+
<button v-for="p in datatypeProperties" :key="p.compact"
|
|
326
|
+
@click="selectProperty(p.compact)"
|
|
327
|
+
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
328
|
+
:class="activePropertyId === p.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
|
|
329
|
+
>
|
|
330
|
+
<span class="w-3 text-ink-200">·</span>
|
|
331
|
+
<span class="flex-1 text-left truncate">{{ p.label }}</span>
|
|
332
|
+
</button>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<!-- SHACL Shapes section -->
|
|
337
|
+
<div class="mt-1">
|
|
338
|
+
<button @click="toggleSection('shape')"
|
|
339
|
+
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"
|
|
340
|
+
>
|
|
341
|
+
<span class="w-3 text-[10px]">{{ collapsedSections.has('shape') ? '▸' : '▾' }}</span>
|
|
342
|
+
<span class="flex-1 text-left">SHACL Shapes</span>
|
|
343
|
+
<span class="badge text-[9px] bg-purple-50 text-purple-600 px-1 py-0.5">{{ allShapes.length }}</span>
|
|
344
|
+
</button>
|
|
345
|
+
<div v-if="!collapsedSections.has('shape')" class="mt-0.5 max-h-40 overflow-y-auto">
|
|
346
|
+
<button v-for="s in allShapes" :key="s.compact"
|
|
347
|
+
@click="selectShape(s.compact)"
|
|
147
348
|
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
148
|
-
:class="
|
|
349
|
+
:class="activeShapeId === s.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
|
|
149
350
|
>
|
|
150
|
-
<span
|
|
151
|
-
<span
|
|
152
|
-
<span class="flex-1 text-left">{{ child.label }}</span>
|
|
351
|
+
<span class="w-3 text-ink-200">·</span>
|
|
352
|
+
<span class="flex-1 text-left truncate">{{ s.label }}</span>
|
|
153
353
|
</button>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<!-- Named Individuals section -->
|
|
358
|
+
<div class="mt-1">
|
|
359
|
+
<button @click="toggleSection('namedIndividual')"
|
|
360
|
+
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"
|
|
361
|
+
>
|
|
362
|
+
<span class="w-3 text-[10px]">{{ collapsedSections.has('namedIndividual') ? '▸' : '▾' }}</span>
|
|
363
|
+
<span class="flex-1 text-left">Named Individuals</span>
|
|
364
|
+
<span class="badge text-[9px] bg-rose-50 text-rose-600 px-1 py-0.5">{{ totalIndividuals }}</span>
|
|
365
|
+
</button>
|
|
366
|
+
<div v-if="!collapsedSections.has('namedIndividual')" class="mt-0.5 max-h-64 overflow-y-auto">
|
|
367
|
+
<template v-for="group in groupedIndividuals" :key="group.key">
|
|
368
|
+
<button @click="selectTaxonomy(group.key)"
|
|
369
|
+
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"
|
|
160
370
|
>
|
|
161
371
|
<span class="w-3 text-ink-200">·</span>
|
|
162
|
-
<span class="flex-1 text-left">{{
|
|
372
|
+
<span class="flex-1 text-left">{{ group.label }}</span>
|
|
373
|
+
<span class="text-[9px] text-ink-300">{{ group.concepts.length }}</span>
|
|
163
374
|
</button>
|
|
375
|
+
</template>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<!-- SKOS Taxonomies section -->
|
|
380
|
+
<div class="mt-1">
|
|
381
|
+
<button @click="toggleSection('taxonomy')"
|
|
382
|
+
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"
|
|
383
|
+
>
|
|
384
|
+
<span class="w-3 text-[10px]">{{ collapsedSections.has('taxonomy') ? '▸' : '▾' }}</span>
|
|
385
|
+
<span class="flex-1 text-left">SKOS Taxonomies</span>
|
|
386
|
+
<span class="badge text-[9px] bg-rose-50 text-rose-600 px-1 py-0.5">{{ taxonomyKeys.length }}</span>
|
|
387
|
+
</button>
|
|
388
|
+
<div v-if="!collapsedSections.has('taxonomy')" class="mt-0.5">
|
|
389
|
+
<button v-for="tk in taxonomyKeys" :key="tk"
|
|
390
|
+
@click="selectTaxonomy(tk)"
|
|
391
|
+
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
392
|
+
:class="activeTaxonomy === tk ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
|
|
393
|
+
>
|
|
394
|
+
<span class="w-3 text-ink-200">·</span>
|
|
395
|
+
<span class="flex-1 text-left">{{ taxonomyLabels[tk] }}</span>
|
|
396
|
+
</button>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<!-- Annotation Properties section -->
|
|
401
|
+
<div class="mt-1">
|
|
402
|
+
<button @click="toggleSection('annotationProperty')"
|
|
403
|
+
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"
|
|
404
|
+
>
|
|
405
|
+
<span class="w-3 text-[10px]">{{ collapsedSections.has('annotationProperty') ? '▸' : '▾' }}</span>
|
|
406
|
+
<span class="flex-1 text-left">Annotation Properties</span>
|
|
407
|
+
<span class="badge text-[9px] bg-pink-50 text-pink-600 px-1 py-0.5">{{ annotationProperties.length }}</span>
|
|
408
|
+
</button>
|
|
409
|
+
<div v-if="!collapsedSections.has('annotationProperty')" class="mt-0.5">
|
|
410
|
+
<div v-for="ap in annotationProperties" :key="ap.compact"
|
|
411
|
+
class="w-full flex items-center gap-1.5 px-2 py-1 text-[11px] text-ink-500"
|
|
412
|
+
>
|
|
413
|
+
<span class="w-3 text-ink-200">·</span>
|
|
414
|
+
<code class="text-ink-400">{{ ap.compact }}</code>
|
|
164
415
|
</div>
|
|
165
|
-
</
|
|
416
|
+
</div>
|
|
166
417
|
</div>
|
|
167
418
|
</template>
|
|
168
|
-
|
|
169
|
-
<!-- SKOS Taxonomies -->
|
|
170
|
-
<div class="mt-2 pt-2 border-t border-ink-100/40">
|
|
171
|
-
<div class="text-[10px] uppercase tracking-wide text-ink-300 mb-1 px-2">Taxonomies</div>
|
|
172
|
-
<button v-for="tk in taxonomyKeys" :key="tk"
|
|
173
|
-
@click="selectTaxonomy(tk)"
|
|
174
|
-
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
175
|
-
:class="activeTaxonomy === tk ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
|
|
176
|
-
>
|
|
177
|
-
<span class="w-3 text-ink-200">·</span>
|
|
178
|
-
<span class="flex-1 text-left">{{ taxonomyLabels[tk] }}</span>
|
|
179
|
-
</button>
|
|
180
|
-
</div>
|
|
181
419
|
</div>
|
|
182
420
|
</template>
|
|
183
421
|
</nav>
|
|
@@ -333,8 +333,8 @@ const rdfSource = computed(() => rdfFormat.value === 'turtle' ? turtleSource.val
|
|
|
333
333
|
</button>
|
|
334
334
|
</div>
|
|
335
335
|
<div class="flex gap-1.5 mt-2.5">
|
|
336
|
-
<
|
|
337
|
-
<
|
|
336
|
+
<router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-blue-50 text-blue-700 border border-blue-100 hover:bg-blue-100 transition-colors">gloss:Concept</router-link>
|
|
337
|
+
<router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-100 hover:bg-emerald-100 transition-colors">skos:Concept</router-link>
|
|
338
338
|
</div>
|
|
339
339
|
</div>
|
|
340
340
|
</div>
|
|
@@ -356,7 +356,7 @@ const rdfSource = computed(() => rdfFormat.value === 'turtle' ? turtleSource.val
|
|
|
356
356
|
<div v-for="(section, si) in sections" :key="si" class="card p-5">
|
|
357
357
|
<div class="flex items-center gap-2 mb-3">
|
|
358
358
|
<div class="w-1 h-4 rounded-full" :class="section.classId === 'gloss:Concept' ? 'bg-blue-500' : section.classId === 'gloss:LocalizedConcept' ? 'bg-emerald-500' : 'bg-amber-500'"></div>
|
|
359
|
-
<
|
|
359
|
+
<router-link :to="`/ontology/class/${section.classId.replace(/:/g, '-')}`" class="text-xs font-semibold text-ink-700 hover:text-blue-600 transition-colors">{{ section.classId }}</router-link>
|
|
360
360
|
<span class="text-xs text-ink-400">·</span>
|
|
361
361
|
<span class="text-xs text-ink-500">{{ section.label }}</span>
|
|
362
362
|
</div>
|
|
@@ -2,8 +2,21 @@ import { ref, computed } from 'vue';
|
|
|
2
2
|
import {
|
|
3
3
|
getClass,
|
|
4
4
|
getClassTree,
|
|
5
|
+
getAllShapes,
|
|
6
|
+
getObjectProperties,
|
|
7
|
+
getDatatypeProperties,
|
|
8
|
+
getAnnotationProperties,
|
|
9
|
+
getOntology,
|
|
10
|
+
getStats,
|
|
5
11
|
type OwlClass,
|
|
12
|
+
type OwlShape,
|
|
13
|
+
type OwlProperty,
|
|
14
|
+
type AnnotationProperty,
|
|
15
|
+
type OwlOntology,
|
|
16
|
+
ENTITY_TYPE_META,
|
|
17
|
+
type EntityType,
|
|
6
18
|
} from '../adapters/ontology-schema';
|
|
19
|
+
import taxonomyData from '../data/taxonomies.json';
|
|
7
20
|
|
|
8
21
|
export function slugToCompact(slug: string): string {
|
|
9
22
|
return slug.replace(/-/g, ':');
|
|
@@ -15,6 +28,57 @@ export function compactToSlug(compact: string): string {
|
|
|
15
28
|
|
|
16
29
|
const expandedClasses = ref(new Set<string>(['gloss:Designation']));
|
|
17
30
|
|
|
31
|
+
const collapsedSections = ref(new Set<string>([
|
|
32
|
+
'objectProperty',
|
|
33
|
+
'datatypeProperty',
|
|
34
|
+
'shape',
|
|
35
|
+
'taxonomy',
|
|
36
|
+
'namedIndividual',
|
|
37
|
+
'annotationProperty',
|
|
38
|
+
]));
|
|
39
|
+
|
|
40
|
+
const searchQuery = ref('');
|
|
41
|
+
|
|
42
|
+
function toggleExpand(cls: OwlClass) {
|
|
43
|
+
const s = new Set(expandedClasses.value);
|
|
44
|
+
if (s.has(cls.compact)) s.delete(cls.compact);
|
|
45
|
+
else s.add(cls.compact);
|
|
46
|
+
expandedClasses.value = s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toggleSection(key: string) {
|
|
50
|
+
const s = new Set(collapsedSections.value);
|
|
51
|
+
if (s.has(key)) s.delete(key);
|
|
52
|
+
else s.add(key);
|
|
53
|
+
collapsedSections.value = s;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function expandAllSections() {
|
|
57
|
+
collapsedSections.value = new Set();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function collapseAllSections() {
|
|
61
|
+
collapsedSections.value = new Set(['objectProperty', 'datatypeProperty', 'shape', 'taxonomy', 'class', 'namedIndividual', 'annotationProperty']);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function childClasses(parentId: string): OwlClass[] {
|
|
65
|
+
const cls = getClass(parentId);
|
|
66
|
+
if (!cls) return [];
|
|
67
|
+
return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hasChildren(cls: OwlClass): boolean {
|
|
71
|
+
return cls.children.length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const treeRoots = getClassTree();
|
|
75
|
+
const allShapes = getAllShapes();
|
|
76
|
+
const objectProperties = getObjectProperties();
|
|
77
|
+
const datatypeProperties = getDatatypeProperties();
|
|
78
|
+
const annotationProperties = getAnnotationProperties();
|
|
79
|
+
const ontology = getOntology();
|
|
80
|
+
const stats = getStats();
|
|
81
|
+
|
|
18
82
|
const taxonomyKeys = [
|
|
19
83
|
'conceptStatus', 'entryStatus', 'normativeStatus', 'sourceType', 'sourceStatus',
|
|
20
84
|
'relationshipType', 'designationType', 'termType', 'grammarGender', 'grammarNumber',
|
|
@@ -33,24 +97,53 @@ const taxonomyLabels: Record<string, string> = {
|
|
|
33
97
|
grammarNumber: 'Grammar Number',
|
|
34
98
|
};
|
|
35
99
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expandedClasses.value = s;
|
|
100
|
+
interface IndividualGroup {
|
|
101
|
+
key: string;
|
|
102
|
+
label: string;
|
|
103
|
+
concepts: { id: string; prefLabel: string }[];
|
|
41
104
|
}
|
|
42
105
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
const groupedIndividuals = computed<IndividualGroup[]>(() => {
|
|
107
|
+
return taxonomyKeys.map(key => {
|
|
108
|
+
const tax = (taxonomyData as Record<string, any>)[key];
|
|
109
|
+
if (!tax) return { key, label: taxonomyLabels[key] || key, concepts: [] };
|
|
110
|
+
const concepts = Object.values(tax.concepts as Record<string, any>).map((c: any) => ({
|
|
111
|
+
id: c.id,
|
|
112
|
+
prefLabel: c.prefLabel,
|
|
113
|
+
}));
|
|
114
|
+
return { key, label: tax.schemeLabel || taxonomyLabels[key] || key, concepts };
|
|
115
|
+
});
|
|
116
|
+
});
|
|
48
117
|
|
|
49
|
-
|
|
50
|
-
|
|
118
|
+
const totalIndividuals = computed(() =>
|
|
119
|
+
groupedIndividuals.value.reduce((sum, g) => sum + g.concepts.length, 0),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const valuesToTaxonomy: Record<string, string> = {
|
|
123
|
+
'gloss:status': 'conceptStatus',
|
|
124
|
+
'gloss:entstatus': 'entryStatus',
|
|
125
|
+
'gloss:norm': 'normativeStatus',
|
|
126
|
+
'gloss:sourceType': 'sourceType',
|
|
127
|
+
'gloss:sourceStatus': 'sourceStatus',
|
|
128
|
+
'gloss:rel': 'relationshipType',
|
|
129
|
+
'gloss:desigType': 'designationType',
|
|
130
|
+
'gloss:termType': 'termType',
|
|
131
|
+
'gloss:gender': 'grammarGender',
|
|
132
|
+
'gloss:number': 'grammarNumber',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function taxonomyKeyForValuesFrom(valuesFrom: string | null): string | null {
|
|
136
|
+
if (!valuesFrom) return null;
|
|
137
|
+
return valuesToTaxonomy[valuesFrom] ?? null;
|
|
51
138
|
}
|
|
52
139
|
|
|
53
|
-
|
|
140
|
+
function getShapesForTaxonomy(taxonomyKey: string): OwlShape[] {
|
|
141
|
+
const targetScheme = Object.entries(valuesToTaxonomy).find(([, v]) => v === taxonomyKey)?.[0];
|
|
142
|
+
if (!targetScheme) return [];
|
|
143
|
+
return allShapes.filter(s =>
|
|
144
|
+
s.constraints.some(c => c.valuesFrom === targetScheme),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
54
147
|
|
|
55
148
|
const allNavItems = computed(() => {
|
|
56
149
|
const items: { id: string; label: string; depth: number }[] = [];
|
|
@@ -66,15 +159,92 @@ const allNavItems = computed(() => {
|
|
|
66
159
|
return items;
|
|
67
160
|
});
|
|
68
161
|
|
|
162
|
+
function matchesSearch(text: string, query: string): boolean {
|
|
163
|
+
return text.toLowerCase().includes(query.toLowerCase());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const searchResults = computed(() => {
|
|
167
|
+
const q = searchQuery.value.trim();
|
|
168
|
+
if (!q) return null;
|
|
169
|
+
|
|
170
|
+
const matchedClasses: OwlClass[] = [];
|
|
171
|
+
const matchedObjectProps: OwlProperty[] = [];
|
|
172
|
+
const matchedDatatypeProps: OwlProperty[] = [];
|
|
173
|
+
const matchedShapes: OwlShape[] = [];
|
|
174
|
+
const matchedIndividuals: { group: string; id: string; prefLabel: string }[] = [];
|
|
175
|
+
const matchedAnnotationProps: AnnotationProperty[] = [];
|
|
176
|
+
|
|
177
|
+
// Walk all classes (tree + leaves)
|
|
178
|
+
function walkAll(classes: OwlClass[]) {
|
|
179
|
+
for (const cls of classes) {
|
|
180
|
+
if (matchesSearch(cls.label, q) || matchesSearch(cls.compact, q)) {
|
|
181
|
+
matchedClasses.push(cls);
|
|
182
|
+
}
|
|
183
|
+
walkAll(childClasses(cls.compact));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
walkAll(treeRoots);
|
|
187
|
+
|
|
188
|
+
for (const p of objectProperties) {
|
|
189
|
+
if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedObjectProps.push(p);
|
|
190
|
+
}
|
|
191
|
+
for (const p of datatypeProperties) {
|
|
192
|
+
if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedDatatypeProps.push(p);
|
|
193
|
+
}
|
|
194
|
+
for (const s of allShapes) {
|
|
195
|
+
if (matchesSearch(s.label, q) || matchesSearch(s.compact, q)) matchedShapes.push(s);
|
|
196
|
+
}
|
|
197
|
+
for (const g of groupedIndividuals.value) {
|
|
198
|
+
for (const c of g.concepts) {
|
|
199
|
+
if (matchesSearch(c.prefLabel, q) || matchesSearch(c.id, q)) {
|
|
200
|
+
matchedIndividuals.push({ group: g.key, id: c.id, prefLabel: c.prefLabel });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const ap of annotationProperties) {
|
|
205
|
+
if (matchesSearch(ap.label, q) || matchesSearch(ap.compact, q)) matchedAnnotationProps.push(ap);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const total = matchedClasses.length + matchedObjectProps.length + matchedDatatypeProps.length + matchedShapes.length + matchedIndividuals.length + matchedAnnotationProps.length;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
total,
|
|
212
|
+
classes: matchedClasses,
|
|
213
|
+
objectProperties: matchedObjectProps,
|
|
214
|
+
datatypeProperties: matchedDatatypeProps,
|
|
215
|
+
shapes: matchedShapes,
|
|
216
|
+
individuals: matchedIndividuals,
|
|
217
|
+
annotationProperties: matchedAnnotationProps,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
|
|
69
221
|
export function useOntologyNav() {
|
|
70
222
|
return {
|
|
71
223
|
expandedClasses,
|
|
224
|
+
collapsedSections,
|
|
225
|
+
searchQuery,
|
|
72
226
|
taxonomyKeys,
|
|
73
227
|
taxonomyLabels,
|
|
74
228
|
treeRoots,
|
|
229
|
+
allShapes,
|
|
230
|
+
objectProperties,
|
|
231
|
+
datatypeProperties,
|
|
232
|
+
annotationProperties,
|
|
233
|
+
ontology,
|
|
234
|
+
stats,
|
|
235
|
+
groupedIndividuals,
|
|
236
|
+
totalIndividuals,
|
|
75
237
|
allNavItems,
|
|
238
|
+
searchResults,
|
|
239
|
+
valuesToTaxonomy,
|
|
240
|
+
taxonomyKeyForValuesFrom,
|
|
241
|
+
getShapesForTaxonomy,
|
|
76
242
|
toggleExpand,
|
|
243
|
+
toggleSection,
|
|
244
|
+
expandAllSections,
|
|
245
|
+
collapseAllSections,
|
|
77
246
|
childClasses,
|
|
78
247
|
hasChildren,
|
|
248
|
+
ENTITY_TYPE_META,
|
|
79
249
|
};
|
|
80
250
|
}
|