@glossarist/concept-browser 0.5.0 → 0.6.0

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.
Files changed (46) hide show
  1. package/README.md +32 -0
  2. package/cli/index.mjs +21 -0
  3. package/package.json +1 -1
  4. package/scripts/generate-data.mjs +43 -1
  5. package/scripts/generate-ontology-schema.mjs +312 -10
  6. package/src/App.vue +3 -0
  7. package/src/__tests__/concept-card.test.ts +16 -2
  8. package/src/__tests__/markdown-lite.test.ts +26 -0
  9. package/src/adapters/factory.ts +3 -2
  10. package/src/adapters/model-bridge.ts +2 -0
  11. package/src/adapters/ontology-schema.ts +89 -4
  12. package/src/adapters/types.ts +1 -0
  13. package/src/components/AppFooter.vue +3 -1
  14. package/src/components/AppHeader.vue +46 -4
  15. package/src/components/AppSidebar.vue +286 -46
  16. package/src/components/ConceptCard.vue +16 -4
  17. package/src/components/ConceptDetail.vue +42 -35
  18. package/src/components/ConceptRdfView.vue +3 -3
  19. package/src/components/ConceptTimeline.vue +2 -14
  20. package/src/components/GraphPanel.vue +19 -0
  21. package/src/components/LanguageDetail.vue +11 -8
  22. package/src/composables/use-ontology-nav.ts +183 -13
  23. package/src/composables/use-render-options.ts +2 -2
  24. package/src/config/types.ts +1 -0
  25. package/src/config/use-site-config.ts +3 -2
  26. package/src/data/ontology-schema.json +1721 -153
  27. package/src/i18n/index.ts +49 -0
  28. package/src/i18n/locales/eng.yml +66 -0
  29. package/src/i18n/locales/fra.yml +66 -0
  30. package/src/router/index.ts +10 -0
  31. package/src/shims/glossarist-tags.ts +10 -0
  32. package/src/stores/vocabulary.ts +1 -1
  33. package/src/style.css +12 -0
  34. package/src/utils/lang.ts +13 -0
  35. package/src/utils/markdown-lite.ts +15 -0
  36. package/src/views/AboutView.vue +1 -1
  37. package/src/views/ContributorsView.vue +1 -1
  38. package/src/views/DatasetView.vue +77 -6
  39. package/src/views/HomeView.vue +21 -17
  40. package/src/views/NewsView.vue +2 -2
  41. package/src/views/OntologySchemaView.vue +331 -14
  42. package/src/views/PageView.vue +27 -11
  43. package/src/views/ResolveView.vue +1 -1
  44. package/src/views/SearchView.vue +4 -2
  45. package/src/views/StatsView.vue +1 -1
  46. package/vite.config.ts +34 -1
@@ -7,6 +7,7 @@ import { useDsStyle } from '../utils/dataset-style';
7
7
  import { useSiteConfig } from '../config/use-site-config';
8
8
  import { useOntologyNav, compactToSlug } from '../composables/use-ontology-nav';
9
9
  import NavIcon from './NavIcon.vue';
10
+ import { useI18n } from '../i18n';
10
11
 
11
12
  const store = useVocabularyStore();
12
13
  const ui = useUiStore();
@@ -14,21 +15,33 @@ const router = useRouter();
14
15
  const route = useRoute();
15
16
  const { getColor } = useDsStyle();
16
17
  const { globalPages, datasetPages, config: siteConfig } = useSiteConfig();
18
+ const { t } = useI18n();
17
19
 
18
20
  const currentDataset = computed(() => route.params.registerId as string ?? '');
19
21
 
20
22
  const {
21
23
  expandedClasses,
24
+ collapsedSections,
25
+ searchQuery,
22
26
  taxonomyKeys,
23
27
  taxonomyLabels,
24
28
  treeRoots,
29
+ allShapes,
30
+ objectProperties,
31
+ datatypeProperties,
32
+ annotationProperties,
33
+ groupedIndividuals,
34
+ totalIndividuals,
35
+ searchResults,
25
36
  toggleExpand,
37
+ toggleSection,
26
38
  childClasses,
27
39
  hasChildren,
40
+ ENTITY_TYPE_META,
28
41
  } = useOntologyNav();
29
42
 
30
43
  const isOntologyRoute = computed(() =>
31
- route.name === 'ontology' || route.name === 'ontology-class' || route.name === 'ontology-taxonomy'
44
+ ['ontology', 'ontology-class', 'ontology-taxonomy', 'ontology-shape', 'ontology-property'].includes(route.name as string)
32
45
  );
33
46
 
34
47
  const activeClassId = computed(() => {
@@ -42,6 +55,18 @@ const activeTaxonomy = computed(() => {
42
55
  return route.params.taxonomyKey as string;
43
56
  });
44
57
 
58
+ const activeShapeId = computed(() => {
59
+ if (route.name !== 'ontology-shape') return null;
60
+ const slug = route.params.shapeId as string;
61
+ return slug.replace(/-/g, ':');
62
+ });
63
+
64
+ const activePropertyId = computed(() => {
65
+ if (route.name !== 'ontology-property') return null;
66
+ const slug = route.params.propertyId as string;
67
+ return slug.replace(/-/g, ':');
68
+ });
69
+
45
70
  const isOverview = computed(() => route.name === 'ontology');
46
71
 
47
72
  const datasetEntries = computed(() => {
@@ -92,6 +117,16 @@ function selectClass(id: string) {
92
117
  function selectTaxonomy(key: string) {
93
118
  router.push(`/ontology/taxonomy/${key}`);
94
119
  }
120
+
121
+ function selectShape(id: string) {
122
+ router.push(`/ontology/shape/${compactToSlug(id)}`);
123
+ }
124
+
125
+ function selectProperty(id: string) {
126
+ router.push(`/ontology/property/${compactToSlug(id)}`);
127
+ }
128
+
129
+ const isSearching = computed(() => !!searchQuery.value.trim());
95
130
  </script>
96
131
 
97
132
  <template>
@@ -106,7 +141,7 @@ function selectTaxonomy(key: string) {
106
141
  >
107
142
  <div class="p-4">
108
143
  <!-- Navigation -->
109
- <div class="section-label">Navigation</div>
144
+ <div class="section-label">{{ t('nav.navigation') }}</div>
110
145
  <nav class="space-y-0.5 mb-6">
111
146
  <template v-for="page in globalPages" :key="page.route || 'home'">
112
147
  <router-link
@@ -119,9 +154,22 @@ function selectTaxonomy(key: string) {
119
154
  {{ page.title }}
120
155
  </router-link>
121
156
 
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 -->
157
+ <!-- Ontology entity sections nested under Ontology nav item -->
158
+ <div v-if="page.route === 'ontology' && isOntologyRoute" class="ml-3 mt-1 mb-2 space-y-0.5">
159
+ <!-- Search input -->
160
+ <div class="relative mb-1.5">
161
+ <input
162
+ v-model="searchQuery"
163
+ type="text"
164
+ placeholder="Search entities..."
165
+ 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"
166
+ />
167
+ <span v-if="searchResults" class="absolute right-2 top-1/2 -translate-y-1/2 text-[9px] text-ink-400">
168
+ {{ searchResults.total }}
169
+ </span>
170
+ </div>
171
+
172
+ <!-- Overview link -->
125
173
  <router-link to="/ontology"
126
174
  class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
127
175
  :class="isOverview ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
@@ -130,54 +178,246 @@ function selectTaxonomy(key: string) {
130
178
  <span class="flex-1 text-left">Overview</span>
131
179
  </router-link>
132
180
 
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)"
181
+ <!-- Search results mode -->
182
+ <template v-if="isSearching && searchResults">
183
+ <div v-if="searchResults.classes.length" class="mt-1">
184
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-blue-500 font-medium">Classes ({{ searchResults.classes.length }})</div>
185
+ <button v-for="cls in searchResults.classes" :key="cls.compact"
186
+ @click="selectClass(cls.compact); searchQuery = ''"
187
+ 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"
188
+ >
189
+ <span class="w-3 text-ink-200">·</span>
190
+ <span class="flex-1 text-left truncate">{{ cls.label }}</span>
191
+ <code class="text-[9px] text-ink-300">{{ cls.compact }}</code>
192
+ </button>
193
+ </div>
194
+ <div v-if="searchResults.objectProperties.length" class="mt-1">
195
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-emerald-500 font-medium">Object Properties ({{ searchResults.objectProperties.length }})</div>
196
+ <button v-for="p in searchResults.objectProperties" :key="p.compact"
197
+ @click="selectProperty(p.compact); searchQuery = ''"
198
+ 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"
199
+ >
200
+ <span class="w-3 text-ink-200">·</span>
201
+ <span class="flex-1 text-left truncate">{{ p.label }}</span>
202
+ </button>
203
+ </div>
204
+ <div v-if="searchResults.datatypeProperties.length" class="mt-1">
205
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-amber-500 font-medium">Datatype Properties ({{ searchResults.datatypeProperties.length }})</div>
206
+ <button v-for="p in searchResults.datatypeProperties" :key="p.compact"
207
+ @click="selectProperty(p.compact); searchQuery = ''"
208
+ 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"
209
+ >
210
+ <span class="w-3 text-ink-200">·</span>
211
+ <span class="flex-1 text-left truncate">{{ p.label }}</span>
212
+ </button>
213
+ </div>
214
+ <div v-if="searchResults.shapes.length" class="mt-1">
215
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-purple-500 font-medium">SHACL Shapes ({{ searchResults.shapes.length }})</div>
216
+ <button v-for="s in searchResults.shapes" :key="s.compact"
217
+ @click="selectShape(s.compact); searchQuery = ''"
218
+ 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"
219
+ >
220
+ <span class="w-3 text-ink-200">·</span>
221
+ <span class="flex-1 text-left truncate">{{ s.label }}</span>
222
+ </button>
223
+ </div>
224
+ <div v-if="searchResults.individuals.length" class="mt-1">
225
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-rose-500 font-medium">Named Individuals ({{ searchResults.individuals.length }})</div>
226
+ <button v-for="ind in searchResults.individuals" :key="ind.group + '/' + ind.id"
227
+ @click="selectTaxonomy(ind.group); searchQuery = ''"
228
+ 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"
229
+ >
230
+ <span class="w-3 text-ink-200">·</span>
231
+ <span class="flex-1 text-left truncate">{{ ind.prefLabel }}</span>
232
+ <span class="text-[9px] text-ink-300">{{ taxonomyLabels[ind.group] }}</span>
233
+ </button>
234
+ </div>
235
+ <div v-if="searchResults.annotationProperties.length" class="mt-1">
236
+ <div class="px-2 py-1 text-[10px] uppercase tracking-wide text-pink-500 font-medium">Annotation Properties ({{ searchResults.annotationProperties.length }})</div>
237
+ <div v-for="ap in searchResults.annotationProperties" :key="ap.compact"
238
+ class="px-2 py-0.5 text-[11px] text-ink-500"
239
+ >
240
+ <span class="w-3 inline-block text-ink-200">·</span>
241
+ {{ ap.compact }}
242
+ </div>
243
+ </div>
244
+ <div v-if="searchResults.total === 0" class="px-2 py-3 text-[11px] text-ink-300 italic">
245
+ No entities match "{{ searchQuery }}"
246
+ </div>
247
+ </template>
248
+
249
+ <!-- Normal browse mode -->
250
+ <template v-if="!isSearching">
251
+ <!-- Classes section -->
252
+ <div class="mt-1">
253
+ <button @click="toggleSection('class')"
254
+ 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"
255
+ >
256
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('class') ? '▸' : '▾' }}</span>
257
+ <span class="flex-1 text-left">Classes</span>
258
+ <span class="badge text-[9px] bg-blue-50 text-blue-600 px-1 py-0.5">{{ treeRoots.length }}+</span>
259
+ </button>
260
+ <div v-if="!collapsedSections.has('class')" class="mt-0.5 space-y-0">
261
+ <template v-for="root in treeRoots" :key="root.compact">
262
+ <button @click="selectClass(root.compact); toggleExpand(root)"
263
+ class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
264
+ :class="activeClassId === root.compact && !activeTaxonomy && !activeShapeId && !activePropertyId ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
265
+ >
266
+ <span v-if="hasChildren(root)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(root.compact) ? '▾' : '▸' }}</span>
267
+ <span v-else class="w-3 text-ink-200">·</span>
268
+ <span class="flex-1 text-left">{{ root.label }}</span>
269
+ </button>
270
+ <div v-if="expandedClasses.has(root.compact) && hasChildren(root)" class="ml-3">
271
+ <template v-for="child in childClasses(root.compact)" :key="child.compact">
272
+ <button @click="selectClass(child.compact); toggleExpand(child)"
273
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
274
+ :class="activeClassId === child.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
275
+ >
276
+ <span v-if="hasChildren(child)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(child.compact) ? '▾' : '▸' }}</span>
277
+ <span v-else class="w-3 text-ink-200">·</span>
278
+ <span class="flex-1 text-left">{{ child.label }}</span>
279
+ </button>
280
+ <div v-if="expandedClasses.has(child.compact) && hasChildren(child)" class="ml-3">
281
+ <button v-for="gc in childClasses(child.compact)" :key="gc.compact"
282
+ @click="selectClass(gc.compact)"
283
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
284
+ :class="activeClassId === gc.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
285
+ >
286
+ <span class="w-3 text-ink-200">·</span>
287
+ <span class="flex-1 text-left">{{ gc.label }}</span>
288
+ </button>
289
+ </div>
290
+ </template>
291
+ </div>
292
+ </template>
293
+ </div>
294
+ </div>
295
+
296
+ <!-- Object Properties section -->
297
+ <div class="mt-1">
298
+ <button @click="toggleSection('objectProperty')"
299
+ 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"
300
+ >
301
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('objectProperty') ? '▸' : '▾' }}</span>
302
+ <span class="flex-1 text-left">Object Properties</span>
303
+ <span class="badge text-[9px] bg-emerald-50 text-emerald-600 px-1 py-0.5">{{ objectProperties.length }}</span>
304
+ </button>
305
+ <div v-if="!collapsedSections.has('objectProperty')" class="mt-0.5 max-h-40 overflow-y-auto">
306
+ <button v-for="p in objectProperties" :key="p.compact"
307
+ @click="selectProperty(p.compact)"
308
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
309
+ :class="activePropertyId === p.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
310
+ >
311
+ <span class="w-3 text-ink-200">·</span>
312
+ <span class="flex-1 text-left truncate">{{ p.label }}</span>
313
+ </button>
314
+ </div>
315
+ </div>
316
+
317
+ <!-- Datatype Properties section -->
318
+ <div class="mt-1">
319
+ <button @click="toggleSection('datatypeProperty')"
320
+ 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"
321
+ >
322
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('datatypeProperty') ? '▸' : '▾' }}</span>
323
+ <span class="flex-1 text-left">Datatype Properties</span>
324
+ <span class="badge text-[9px] bg-amber-50 text-amber-600 px-1 py-0.5">{{ datatypeProperties.length }}</span>
325
+ </button>
326
+ <div v-if="!collapsedSections.has('datatypeProperty')" class="mt-0.5 max-h-40 overflow-y-auto">
327
+ <button v-for="p in datatypeProperties" :key="p.compact"
328
+ @click="selectProperty(p.compact)"
329
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
330
+ :class="activePropertyId === p.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
331
+ >
332
+ <span class="w-3 text-ink-200">·</span>
333
+ <span class="flex-1 text-left truncate">{{ p.label }}</span>
334
+ </button>
335
+ </div>
336
+ </div>
337
+
338
+ <!-- SHACL Shapes section -->
339
+ <div class="mt-1">
340
+ <button @click="toggleSection('shape')"
341
+ 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"
342
+ >
343
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('shape') ? '▸' : '▾' }}</span>
344
+ <span class="flex-1 text-left">SHACL Shapes</span>
345
+ <span class="badge text-[9px] bg-purple-50 text-purple-600 px-1 py-0.5">{{ allShapes.length }}</span>
346
+ </button>
347
+ <div v-if="!collapsedSections.has('shape')" class="mt-0.5 max-h-40 overflow-y-auto">
348
+ <button v-for="s in allShapes" :key="s.compact"
349
+ @click="selectShape(s.compact)"
147
350
  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'"
351
+ :class="activeShapeId === s.compact ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
149
352
  >
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>
353
+ <span class="w-3 text-ink-200">·</span>
354
+ <span class="flex-1 text-left truncate">{{ s.label }}</span>
153
355
  </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'"
356
+ </div>
357
+ </div>
358
+
359
+ <!-- Named Individuals section -->
360
+ <div class="mt-1">
361
+ <button @click="toggleSection('namedIndividual')"
362
+ 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"
363
+ >
364
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('namedIndividual') ? '▸' : '▾' }}</span>
365
+ <span class="flex-1 text-left">Named Individuals</span>
366
+ <span class="badge text-[9px] bg-rose-50 text-rose-600 px-1 py-0.5">{{ totalIndividuals }}</span>
367
+ </button>
368
+ <div v-if="!collapsedSections.has('namedIndividual')" class="mt-0.5 max-h-64 overflow-y-auto">
369
+ <template v-for="group in groupedIndividuals" :key="group.key">
370
+ <button @click="selectTaxonomy(group.key)"
371
+ 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
372
  >
161
373
  <span class="w-3 text-ink-200">·</span>
162
- <span class="flex-1 text-left">{{ gc.label }}</span>
374
+ <span class="flex-1 text-left">{{ group.label }}</span>
375
+ <span class="text-[9px] text-ink-300">{{ group.concepts.length }}</span>
163
376
  </button>
377
+ </template>
378
+ </div>
379
+ </div>
380
+
381
+ <!-- SKOS Taxonomies section -->
382
+ <div class="mt-1">
383
+ <button @click="toggleSection('taxonomy')"
384
+ 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"
385
+ >
386
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('taxonomy') ? '▸' : '▾' }}</span>
387
+ <span class="flex-1 text-left">SKOS Taxonomies</span>
388
+ <span class="badge text-[9px] bg-rose-50 text-rose-600 px-1 py-0.5">{{ taxonomyKeys.length }}</span>
389
+ </button>
390
+ <div v-if="!collapsedSections.has('taxonomy')" class="mt-0.5">
391
+ <button v-for="tk in taxonomyKeys" :key="tk"
392
+ @click="selectTaxonomy(tk)"
393
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
394
+ :class="activeTaxonomy === tk ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
395
+ >
396
+ <span class="w-3 text-ink-200">·</span>
397
+ <span class="flex-1 text-left">{{ taxonomyLabels[tk] }}</span>
398
+ </button>
399
+ </div>
400
+ </div>
401
+
402
+ <!-- Annotation Properties section -->
403
+ <div class="mt-1">
404
+ <button @click="toggleSection('annotationProperty')"
405
+ 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"
406
+ >
407
+ <span class="w-3 text-[10px]">{{ collapsedSections.has('annotationProperty') ? '▸' : '▾' }}</span>
408
+ <span class="flex-1 text-left">Annotation Properties</span>
409
+ <span class="badge text-[9px] bg-pink-50 text-pink-600 px-1 py-0.5">{{ annotationProperties.length }}</span>
410
+ </button>
411
+ <div v-if="!collapsedSections.has('annotationProperty')" class="mt-0.5">
412
+ <div v-for="ap in annotationProperties" :key="ap.compact"
413
+ class="w-full flex items-center gap-1.5 px-2 py-1 text-[11px] text-ink-500"
414
+ >
415
+ <span class="w-3 text-ink-200">·</span>
416
+ <code class="text-ink-400">{{ ap.compact }}</code>
164
417
  </div>
165
- </template>
418
+ </div>
166
419
  </div>
167
420
  </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
421
  </div>
182
422
  </template>
183
423
  </nav>
@@ -201,7 +441,7 @@ function selectTaxonomy(key: string) {
201
441
  </div>
202
442
 
203
443
  <!-- Datasets -->
204
- <div class="section-label">Datasets</div>
444
+ <div class="section-label">{{ t('nav.datasets') }}</div>
205
445
  <nav class="space-y-1">
206
446
  <button
207
447
  v-for="ds in datasetEntries"
@@ -217,7 +457,7 @@ function selectTaxonomy(key: string) {
217
457
  >
218
458
  <div class="font-medium truncate leading-snug">{{ ds.title }}</div>
219
459
  <div v-if="ds.loaded" class="text-xs mt-0.5" :class="currentDataset === ds.id ? 'text-ink-400' : 'text-ink-300'">
220
- {{ ds.conceptCount.toLocaleString() }} concepts
460
+ {{ ds.conceptCount.toLocaleString() }} {{ t('home.concepts').toLowerCase() }}
221
461
  </div>
222
462
  </button>
223
463
  </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>