@glossarist/concept-browser 0.5.0 → 0.5.1

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.
@@ -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
- route.name === 'ontology' || route.name === 'ontology-class' || route.name === 'ontology-taxonomy'
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 class tree nested under Ontology nav item -->
123
- <div v-if="page.route === 'ontology' && isOntologyRoute" class="ml-4 mt-1 mb-2 space-y-0.5">
124
- <!-- Overview -->
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
- <div class="text-[10px] uppercase tracking-wide text-ink-300 mt-2 mb-1 px-2">Classes</div>
134
- <template v-for="root in treeRoots" :key="root.compact">
135
- <button @click="selectClass(root.compact); toggleExpand(root)"
136
- class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
137
- :class="activeClassId === root.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
138
- >
139
- <span v-if="hasChildren(root)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(root.compact) ? '▾' : '▸' }}</span>
140
- <span v-else class="w-3 text-ink-200">·</span>
141
- <span class="flex-1 text-left">{{ root.label }}</span>
142
- </button>
143
- <!-- Children -->
144
- <div v-if="expandedClasses.has(root.compact) && hasChildren(root)" class="ml-3">
145
- <template v-for="child in childClasses(root.compact)" :key="child.compact">
146
- <button @click="selectClass(child.compact); toggleExpand(child)"
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="activeClassId === child.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
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 v-if="hasChildren(child)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(child.compact) ? '▾' : '▸' }}</span>
151
- <span v-else class="w-3 text-ink-200">·</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
- <!-- Grandchildren -->
155
- <div v-if="expandedClasses.has(child.compact) && hasChildren(child)" class="ml-3">
156
- <button v-for="gc in childClasses(child.compact)" :key="gc.compact"
157
- @click="selectClass(gc.compact)"
158
- class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
159
- :class="activeClassId === gc.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
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">{{ gc.label }}</span>
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
- </template>
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>
@@ -8,6 +8,7 @@ import { useVocabularyStore } from '../stores/vocabulary';
8
8
  const props = defineProps<{
9
9
  entry: ConceptSummary;
10
10
  registerId: string;
11
+ displayLang?: string | null;
11
12
  }>();
12
13
 
13
14
  const router = useRouter();
@@ -29,6 +30,17 @@ function statusColor(status: string): string {
29
30
  }
30
31
 
31
32
  const manifestLanguages = computed(() => store.manifests.get(props.registerId)?.languages ?? []);
33
+
34
+ const displayTitle = computed(() => {
35
+ if (props.displayLang && props.entry.designations?.[props.displayLang]) {
36
+ return props.entry.designations[props.displayLang];
37
+ }
38
+ return props.entry.eng || props.entry.id;
39
+ });
40
+
41
+ const langCount = computed(() => {
42
+ return Object.keys(props.entry.designations ?? {}).length;
43
+ });
32
44
  </script>
33
45
 
34
46
  <template>
@@ -41,7 +53,7 @@ const manifestLanguages = computed(() => store.manifests.get(props.registerId)?.
41
53
  <div class="flex items-start justify-between gap-2">
42
54
  <div class="min-w-0">
43
55
  <h3 class="font-medium text-ink-800 truncate group-hover:text-ink-900 transition-colors leading-snug text-[15px]">
44
- {{ entry.eng || entry.id }}
56
+ {{ displayTitle }}
45
57
  </h3>
46
58
  <p class="text-[11px] text-ink-300 mt-1 font-mono tabular-nums">{{ entry.id }}</p>
47
59
  </div>
@@ -53,14 +65,14 @@ const manifestLanguages = computed(() => store.manifests.get(props.registerId)?.
53
65
  </span>
54
66
  </div>
55
67
  <!-- Language coverage -->
56
- <div class="flex items-center gap-1.5 mt-2.5" :aria-label="`${manifestLanguages.length} languages`">
57
- <span class="text-[11px] text-ink-300">{{ manifestLanguages.length }} lang</span>
68
+ <div class="flex items-center gap-1.5 mt-2.5">
69
+ <span class="text-[11px] text-ink-300">{{ langCount }} lang</span>
58
70
  <div class="flex gap-0.5">
59
71
  <span
60
72
  v-for="lang in manifestLanguages"
61
73
  :key="lang"
62
74
  class="w-1.5 h-1.5 rounded-full"
63
- :style="{ backgroundColor: getColor(registerId) + '40' }"
75
+ :style="{ backgroundColor: (lang in (entry.designations ?? {})) ? getColor(registerId) : getColor(registerId) + '20' }"
64
76
  :aria-label="lang"
65
77
  ></span>
66
78
  </div>
@@ -2,7 +2,7 @@
2
2
  import type { Concept, LocalizedConcept, Designation, Expression, ConceptSource } from 'glossarist';
3
3
  import type { Manifest, GraphEdge } from '../adapters/types';
4
4
  import { computed, ref, nextTick, watch } from 'vue';
5
- import { langName, langLabel } from '../utils/lang';
5
+ import { langName, langLabel, sortLanguages } from '../utils/lang';
6
6
  import { renderMath, cleanContent } from '../utils/math';
7
7
  import type { RenderOptions } from '../utils/math';
8
8
  import { escapeAttr } from '../utils/escape';
@@ -15,6 +15,7 @@ import { useDsStyle } from '../utils/dataset-style';
15
15
  import { getFactory } from '../adapters/factory';
16
16
  import { useRenderOptions } from '../composables/use-render-options';
17
17
  import { categorizeRelationship, relationshipLabel, relationshipDefinition } from '../utils/relationship-categories';
18
+ import { useSiteConfig } from '../config/use-site-config';
18
19
  import ConceptTimeline from './ConceptTimeline.vue';
19
20
  import ConceptRdfView from './ConceptRdfView.vue';
20
21
  import FormatDownloads from './FormatDownloads.vue';
@@ -32,6 +33,7 @@ const props = defineProps<{
32
33
  const router = useRouter();
33
34
  const store = useVocabularyStore();
34
35
  const { getColor } = useDsStyle();
36
+ const { config: siteConfig } = useSiteConfig();
35
37
  const factory = getFactory();
36
38
 
37
39
  const activeTab = ref<'rdf' | 'definition' | 'history'>('definition');
@@ -57,35 +59,12 @@ function copyUri() {
57
59
  }
58
60
 
59
61
  const languages = computed(() => {
60
- const order = props.manifest.languageOrder;
61
- const keys = props.concept.languages;
62
- if (!order) {
63
- return [...keys].sort((a, b) => {
64
- if (a === 'eng') return -1;
65
- if (a === 'eng') return 1;
66
- return a.localeCompare(b);
67
- });
68
- }
69
- const orderIndex = new Map(order.map((lang, i) => [lang, i]));
70
- return [...keys].sort((a, b) => {
71
- const ai = orderIndex.get(a) ?? order.length;
72
- const bi = orderIndex.get(b) ?? order.length;
73
- if (ai !== bi) return ai - bi;
74
- return a.localeCompare(b);
75
- });
62
+ return sortLanguages(props.concept.languages, props.manifest.languageOrder);
76
63
  });
77
64
 
78
- // Collapsible language sections — auto-collapse non-eng when 6+ languages
65
+ // Collapsible language sections — expand all with content, collapse those without
79
66
  const collapsedLangs = ref(new Set<string>());
80
67
 
81
- function initCollapsed(langs: string[]) {
82
- if (langs.length >= 6) {
83
- collapsedLangs.value = new Set(langs.filter(l => l !== 'eng'));
84
- }
85
- }
86
-
87
- watch(languages, (langs) => { initCollapsed(langs); }, { immediate: true });
88
-
89
68
  const engConcept = computed((): LocalizedConcept | null => {
90
69
  return props.concept.localization('eng') ?? null;
91
70
  });
@@ -104,6 +83,9 @@ const conceptDates = computed(() => props.concept.dates);
104
83
  // Managed concept sources (distinct from localized sources)
105
84
  const conceptSources = computed(() => props.concept.sources);
106
85
 
86
+ // Managed concept tags
87
+ const conceptTags = computed(() => props.concept.tags ?? []);
88
+
107
89
  // Cross-reference resolver: generates clickable links for inline refs
108
90
 
109
91
  const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
@@ -192,6 +174,20 @@ function hasContent(lc: LangContent): boolean {
192
174
  return !!(lc.definition || lc.notes.length || lc.examples.length || lc.sources.length);
193
175
  }
194
176
 
177
+ function initCollapsed() {
178
+ const mainLangs = siteConfig.value?.defaults?.mainLanguages || [];
179
+ const mainSet = new Set(mainLangs.length ? mainLangs : ['eng']);
180
+ const collapsed = new Set<string>();
181
+ for (const lc of allLangContent.value) {
182
+ if (!hasContent(lc) && !mainSet.has(lc.lang)) {
183
+ collapsed.add(lc.lang);
184
+ }
185
+ }
186
+ collapsedLangs.value = collapsed;
187
+ }
188
+
189
+ watch(languages, () => { initCollapsed(); }, { immediate: true });
190
+
195
191
  const allCollapsed = computed(() => collapsedLangs.value.size === allLangContent.value.length);
196
192
 
197
193
  function toggleLang(lang: string) {
@@ -690,6 +686,14 @@ const nonVerbalReps = computed(() => {
690
686
  </div>
691
687
  </div>
692
688
 
689
+ <!-- Tags -->
690
+ <div v-if="conceptTags.length" class="card p-5">
691
+ <div class="section-label">Tags</div>
692
+ <div class="flex flex-wrap gap-1.5 mt-3">
693
+ <span v-for="tag in conceptTags" :key="tag" class="badge badge-gray text-[10px]">{{ tag }}</span>
694
+ </div>
695
+ </div>
696
+
693
697
  <!-- Managed concept dates -->
694
698
  <div v-if="conceptDates.length" class="card p-5">
695
699
  <div class="section-label">Lifecycle dates</div>
@@ -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
- <span 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">gloss:Concept</span>
337
- <span 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">skos:Concept</span>
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
- <code class="text-xs font-semibold text-ink-700">{{ section.classId }}</code>
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>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { Concept, LocalizedConcept } from 'glossarist';
3
3
  import { computed } from 'vue';
4
- import { langName, langLabel } from '../utils/lang';
4
+ import { langName, langLabel, sortLanguages } from '../utils/lang';
5
5
  import { entryStatusColor } from '../utils/concept-helpers';
6
6
 
7
7
  const props = defineProps<{
@@ -141,19 +141,7 @@ const languagesWithHistory = computed(() => {
141
141
  langs.push(lang);
142
142
  }
143
143
  }
144
- const order = props.languageOrder;
145
- if (order) {
146
- const orderIndex = new Map(order.map((l, i) => [l, i]));
147
- langs.sort((a, b) => {
148
- const ai = orderIndex.get(a) ?? order.length;
149
- const bi = orderIndex.get(b) ?? order.length;
150
- if (ai !== bi) return ai - bi;
151
- return a.localeCompare(b);
152
- });
153
- } else {
154
- langs.sort();
155
- }
156
- return langs;
144
+ return sortLanguages(langs, props.languageOrder);
157
145
  });
158
146
 
159
147
  function formatDate(isoDate: string): string {
@@ -115,6 +115,7 @@ interface SimNode extends SimulationNodeDatum {
115
115
  register: string;
116
116
  conceptId: string;
117
117
  designation: string;
118
+ altDesignations: string[];
118
119
  hasDesignation: boolean;
119
120
  loaded: boolean;
120
121
  nodeType?: 'concept' | 'domain';
@@ -178,11 +179,15 @@ function buildSimulation(width: number, height: number) {
178
179
  const simNodes: SimNode[] = renderNodes.map(n => {
179
180
  const lang = uiStore.selectedLang;
180
181
  const desig = n.designations[lang] || Object.values(n.designations)[0] || '';
182
+ const alts = Object.entries(n.designations)
183
+ .filter(([l, t]) => l !== lang && t && t !== desig)
184
+ .map(([, t]) => t);
181
185
  return {
182
186
  uri: n.uri,
183
187
  register: n.register,
184
188
  conceptId: n.conceptId,
185
189
  designation: desig,
190
+ altDesignations: alts,
186
191
  hasDesignation: !!n.designations[lang],
187
192
  loaded: n.loaded,
188
193
  nodeType: n.nodeType,
@@ -272,6 +277,20 @@ function buildSimulation(width: number, height: number) {
272
277
  return '#636588';
273
278
  });
274
279
 
280
+ conceptNodes.append('text')
281
+ .attr('dy', -9)
282
+ .attr('y', 7)
283
+ .attr('text-anchor', 'middle')
284
+ .attr('font-size', '6px')
285
+ .attr('font-family', '"DM Sans", system-ui, sans-serif')
286
+ .attr('fill', '#a0a1b5')
287
+ .attr('pointer-events', 'none')
288
+ .text(d => {
289
+ if (labelMode.value !== 'designation') return '';
290
+ const alts = d.altDesignations.slice(0, 2);
291
+ return alts.join(' · ');
292
+ });
293
+
275
294
  const dragBehavior = drag<SVGGElement, SimNode>()
276
295
  .on('start', (event: D3DragEvent<SVGGElement, SimNode, SimNode>, d) => {
277
296
  if (!event.active) simulation?.alphaTarget(0.3).restart();