@glossarist/concept-browser 0.3.4 → 0.4.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 (81) hide show
  1. package/README.md +3 -2
  2. package/cli/index.mjs +2 -1
  3. package/env.d.ts +5 -0
  4. package/package.json +4 -3
  5. package/scripts/build-edges.js +78 -10
  6. package/scripts/generate-data.mjs +152 -20
  7. package/scripts/generate-ontology-data.mjs +184 -0
  8. package/scripts/generate-ontology-schema.mjs +315 -0
  9. package/src/__tests__/about-view.test.ts +98 -0
  10. package/src/__tests__/app-footer.test.ts +38 -0
  11. package/src/__tests__/app-header.test.ts +130 -0
  12. package/src/__tests__/app-sidebar.test.ts +159 -0
  13. package/src/__tests__/asciidoc-lite.test.ts +1 -1
  14. package/src/__tests__/concept-card.test.ts +115 -0
  15. package/src/__tests__/concept-detail-interaction.test.ts +273 -0
  16. package/src/__tests__/concept-formats.test.ts +32 -30
  17. package/src/__tests__/concept-timeline.test.ts +200 -0
  18. package/src/__tests__/concept-view.test.ts +88 -0
  19. package/src/__tests__/contributors-view.test.ts +103 -0
  20. package/src/__tests__/dataset-adapter.test.ts +172 -23
  21. package/src/__tests__/dataset-view.test.ts +232 -0
  22. package/src/__tests__/designation-registry.test.ts +161 -0
  23. package/src/__tests__/format-downloads.test.ts +98 -0
  24. package/src/__tests__/graph-view.test.ts +69 -0
  25. package/src/__tests__/graph.test.ts +62 -0
  26. package/src/__tests__/home-interaction.test.ts +157 -0
  27. package/src/__tests__/language-detail.test.ts +203 -0
  28. package/src/__tests__/nav-icon.test.ts +48 -0
  29. package/src/__tests__/news-view.test.ts +87 -0
  30. package/src/__tests__/ontology-registry.test.ts +109 -0
  31. package/src/__tests__/page-view.test.ts +83 -0
  32. package/src/__tests__/relationship-categories.test.ts +62 -0
  33. package/src/__tests__/resolve-view.test.ts +77 -0
  34. package/src/__tests__/router.test.ts +65 -0
  35. package/src/__tests__/search-bar.test.ts +219 -0
  36. package/src/__tests__/search-view.test.ts +41 -0
  37. package/src/__tests__/stats-view.test.ts +77 -0
  38. package/src/__tests__/test-helpers.ts +171 -0
  39. package/src/__tests__/ui-store.test.ts +100 -0
  40. package/src/__tests__/v-math.test.ts +8 -7
  41. package/src/adapters/DatasetAdapter.ts +188 -63
  42. package/src/adapters/model-bridge.ts +277 -0
  43. package/src/adapters/ontology-registry.ts +75 -0
  44. package/src/adapters/ontology-schema.ts +100 -0
  45. package/src/adapters/types.ts +53 -78
  46. package/src/components/AppSidebar.vue +1 -1
  47. package/src/components/CitationDisplay.vue +35 -0
  48. package/src/components/ConceptDetail.vue +349 -146
  49. package/src/components/ConceptRdfView.vue +397 -0
  50. package/src/components/ConceptTimeline.vue +57 -60
  51. package/src/components/GraphPanel.vue +96 -31
  52. package/src/components/LanguageDetail.vue +46 -61
  53. package/src/components/NavIcon.vue +1 -0
  54. package/src/components/NonVerbalRepDisplay.vue +38 -0
  55. package/src/components/RelationshipList.vue +99 -0
  56. package/src/composables/use-render-options.ts +1 -4
  57. package/src/config/use-site-config.ts +3 -0
  58. package/src/data/ontology-schema.json +1551 -0
  59. package/src/data/taxonomies.json +543 -0
  60. package/src/graph/GraphEngine.ts +7 -4
  61. package/src/router/index.ts +6 -1
  62. package/src/shims/empty.ts +1 -0
  63. package/src/shims/node-crypto.ts +6 -0
  64. package/src/shims/node-path.ts +10 -0
  65. package/src/stores/vocabulary.ts +82 -32
  66. package/src/style.css +74 -20
  67. package/src/utils/asciidoc-lite.ts +17 -19
  68. package/src/utils/concept-formats.ts +22 -20
  69. package/src/utils/concept-helpers.ts +54 -0
  70. package/src/utils/designation-registry.ts +124 -0
  71. package/src/utils/escape.ts +7 -0
  72. package/src/utils/markdown-lite.ts +1 -3
  73. package/src/utils/math.ts +2 -11
  74. package/src/utils/plurimath.ts +2 -7
  75. package/src/utils/relationship-categories.ts +84 -0
  76. package/src/views/ConceptView.vue +22 -1
  77. package/src/views/DatasetView.vue +7 -2
  78. package/src/views/OntologySchemaView.vue +302 -0
  79. package/src/views/PageView.vue +28 -17
  80. package/src/views/StatsView.vue +34 -12
  81. package/vite.config.ts +8 -0
@@ -1,20 +1,28 @@
1
1
  <script setup lang="ts">
2
- import type { ConceptDocument, LocalizedConcept, GraphEdge } from '../adapters/types';
3
- import type { Manifest } from '../adapters/types';
2
+ import type { Concept, LocalizedConcept, Designation, Expression, ConceptSource } from 'glossarist';
3
+ import type { Manifest, GraphEdge } from '../adapters/types';
4
4
  import { computed, ref, nextTick, watch } from 'vue';
5
5
  import { langName, langLabel } from '../utils/lang';
6
6
  import { renderMath, cleanContent } from '../utils/math';
7
7
  import type { RenderOptions } from '../utils/math';
8
+ import { escapeAttr } from '../utils/escape';
9
+ import { entryStatusColor, conceptStatusColor, conceptStatusLabel, conceptStatusDefinition, entryStatusLabel, entryStatusDefinition, getPreferredTerm } from '../utils/concept-helpers';
10
+ import { designationTypeInfo, normativeStatusInfo, grammarBadges, pronunciationLabel, pronunciationTooltip, abbreviationDetails, sourceTypeInfo, sourceStatusInfo, termTypeInfo } from '../utils/designation-registry';
11
+ import { conceptUri } from '../adapters/model-bridge';
8
12
  import { useRouter } from 'vue-router';
9
13
  import { useVocabularyStore } from '../stores/vocabulary';
10
14
  import { useDsStyle } from '../utils/dataset-style';
11
15
  import { getFactory } from '../adapters/factory';
12
16
  import { useRenderOptions } from '../composables/use-render-options';
17
+ import { categorizeRelationship, relationshipLabel, relationshipDefinition } from '../utils/relationship-categories';
13
18
  import ConceptTimeline from './ConceptTimeline.vue';
19
+ import ConceptRdfView from './ConceptRdfView.vue';
14
20
  import FormatDownloads from './FormatDownloads.vue';
21
+ import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
22
+ import CitationDisplay from './CitationDisplay.vue';
15
23
 
16
24
  const props = defineProps<{
17
- concept: ConceptDocument;
25
+ concept: Concept;
18
26
  manifest: Manifest;
19
27
  edges: GraphEdge[];
20
28
  registerId: string;
@@ -26,10 +34,10 @@ const store = useVocabularyStore();
26
34
  const { getColor } = useDsStyle();
27
35
  const factory = getFactory();
28
36
 
29
- const activeTab = ref<'definition' | 'history'>('definition');
37
+ const activeTab = ref<'rdf' | 'definition' | 'history'>('definition');
30
38
  const activeHistoryLang = ref('eng');
31
39
 
32
- const conceptId = computed(() => props.concept['gl:identifier']);
40
+ const conceptId = computed(() => props.concept.id);
33
41
 
34
42
  const conceptPosition = computed(() => {
35
43
  const adapter = store.datasets.get(props.registerId);
@@ -41,7 +49,8 @@ const conceptPosition = computed(() => {
41
49
 
42
50
  const uriCopied = ref(false);
43
51
  function copyUri() {
44
- navigator.clipboard.writeText(props.concept['@id']).then(() => {
52
+ const uri = conceptUri(props.concept, props.registerId, props.manifest.uriBase);
53
+ navigator.clipboard.writeText(uri).then(() => {
45
54
  uriCopied.value = true;
46
55
  setTimeout(() => { uriCopied.value = false; }, 2000);
47
56
  });
@@ -49,16 +58,16 @@ function copyUri() {
49
58
 
50
59
  const languages = computed(() => {
51
60
  const order = props.manifest.languageOrder;
52
- const keys = Object.keys(props.concept['gl:localizedConcept'] || {});
61
+ const keys = props.concept.languages;
53
62
  if (!order) {
54
- return keys.sort((a, b) => {
63
+ return [...keys].sort((a, b) => {
55
64
  if (a === 'eng') return -1;
56
- if (b === 'eng') return 1;
65
+ if (a === 'eng') return 1;
57
66
  return a.localeCompare(b);
58
67
  });
59
68
  }
60
69
  const orderIndex = new Map(order.map((lang, i) => [lang, i]));
61
- return keys.sort((a, b) => {
70
+ return [...keys].sort((a, b) => {
62
71
  const ai = orderIndex.get(a) ?? order.length;
63
72
  const bi = orderIndex.get(b) ?? order.length;
64
73
  if (ai !== bi) return ai - bi;
@@ -66,27 +75,36 @@ const languages = computed(() => {
66
75
  });
67
76
  });
68
77
 
69
- // Initialize collapsed state when languages change
78
+ // Collapsible language sections — auto-collapse non-eng when 6+ languages
79
+ const collapsedLangs = ref(new Set<string>());
80
+
81
+ function initCollapsed(langs: string[]) {
82
+ if (langs.length >= 6) {
83
+ collapsedLangs.value = new Set(langs.filter(l => l !== 'eng'));
84
+ }
85
+ }
86
+
70
87
  watch(languages, (langs) => { initCollapsed(langs); }, { immediate: true });
71
88
 
72
89
  const engConcept = computed((): LocalizedConcept | null => {
73
- return props.concept['gl:localizedConcept']?.['eng'] ?? null;
90
+ return props.concept.localization('eng') ?? null;
74
91
  });
75
92
 
76
- const primaryTerm = computed(() => {
77
- const eng = engConcept.value;
78
- if (!eng?.['gl:designation']?.length) return conceptId.value;
79
- const desigs = eng['gl:designation'];
80
- const preferredExpr = desigs.find(d => d['gl:normativeStatus'] === 'preferred' && d['@type'] === 'gl:Expression');
81
- if (preferredExpr) return preferredExpr['gl:term'];
82
- const preferred = desigs.find(d => d['gl:normativeStatus'] === 'preferred');
83
- return preferred?.['gl:term'] ?? desigs[0]?.['gl:term'] ?? conceptId.value;
84
- });
93
+ const primaryTerm = computed(() => getPreferredTerm(engConcept.value, conceptId.value));
94
+
95
+ // Managed concept status from Concept.status (7 values from concept-status.ttl)
96
+ const managedStatus = computed(() => props.concept.status);
97
+
98
+ // ConceptReference domains from managed concept level
99
+ const conceptRefDomains = computed(() => props.concept.domains);
100
+
101
+ // Managed concept dates
102
+ const conceptDates = computed(() => props.concept.dates);
103
+
104
+ // Managed concept sources (distinct from localized sources)
105
+ const conceptSources = computed(() => props.concept.sources);
85
106
 
86
107
  // Cross-reference resolver: generates clickable links for inline refs
87
- function escapeAttr(s: string) {
88
- return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
89
- }
90
108
 
91
109
  const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
92
110
 
@@ -126,47 +144,50 @@ function handleContentClick(e: MouseEvent) {
126
144
  // Pre-computed content for all languages (sorted eng first)
127
145
  interface LangContent {
128
146
  lang: string;
147
+ lc: LocalizedConcept;
129
148
  definition: string;
130
149
  notes: string[];
131
150
  examples: string[];
132
- sources: any[];
133
- designations: any[];
151
+ sources: ConceptSource[];
152
+ designations: Designation[];
134
153
  entryStatus: string;
154
+ classification: string | null;
155
+ reviewType: string | null;
156
+ release: string | null;
157
+ lineageSourceSimilarity: number | null;
158
+ lcScript: string | null;
159
+ lcSystem: string | null;
135
160
  }
136
161
 
137
162
  const allLangContent = computed(() => {
138
163
  const result: LangContent[] = [];
139
164
  for (const lang of languages.value) {
140
- const lc = props.concept['gl:localizedConcept']?.[lang];
165
+ const lc = props.concept.localization(lang);
141
166
  if (!lc) continue;
142
167
 
143
- const defs = lc['gl:definition'];
144
- const definition = defs?.length
145
- ? defs.map(d => d['gl:content']).filter(Boolean).join('\n\n')
146
- : '';
168
+ const definition = lc.definitions
169
+ .map(d => d.content).filter(Boolean).join('\n\n');
147
170
 
148
171
  result.push({
149
172
  lang,
173
+ lc,
150
174
  definition,
151
- notes: lc['gl:notes']?.map((n: any) => n['gl:content']).filter(Boolean) ?? [],
152
- examples: lc['gl:examples']?.map((e: any) => e['gl:content']).filter(Boolean) ?? [],
153
- sources: lc['gl:source'] ?? [],
154
- designations: lc['gl:designation'] ?? [],
155
- entryStatus: lc['gl:entryStatus'] ?? '',
175
+ notes: lc.notes.map(n => n.content).filter(Boolean),
176
+ examples: lc.examples.map(e => e.content).filter(Boolean),
177
+ sources: lc.sources,
178
+ designations: lc.terms,
179
+ entryStatus: lc.entryStatus ?? '',
180
+ classification: lc.classification,
181
+ reviewType: lc.reviewType,
182
+ release: lc.release,
183
+ lineageSourceSimilarity: lc.lineageSourceSimilarity,
184
+ lcScript: lc.script,
185
+ lcSystem: lc.system,
156
186
  });
157
187
  }
158
188
  return result;
159
189
  });
160
190
 
161
- // Collapsible language sections — auto-collapse non-eng when 6+ languages
162
- const collapsedLangs = ref(new Set<string>());
163
-
164
- function initCollapsed(langs: string[]) {
165
- if (langs.length >= 6) {
166
- collapsedLangs.value = new Set(langs.filter(l => l !== 'eng'));
167
- }
168
- }
169
-
170
191
  function hasContent(lc: LangContent): boolean {
171
192
  return !!(lc.definition || lc.notes.length || lc.examples.length || lc.sources.length);
172
193
  }
@@ -188,21 +209,23 @@ function toggleAll() {
188
209
  }
189
210
 
190
211
  function scrollToLang(lang: string) {
191
- // Expand if collapsed
192
212
  if (collapsedLangs.value.has(lang)) {
193
213
  const s = new Set(collapsedLangs.value);
194
214
  s.delete(lang);
195
215
  collapsedLangs.value = s;
196
216
  }
197
- // Switch to definition tab if needed
198
217
  activeTab.value = 'definition';
199
218
  nextTick(() => {
200
219
  document.getElementById(`lang-${lang}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
201
220
  });
202
221
  }
203
222
 
204
- const outgoingEdges = computed(() => props.edges.filter(e => e.source === props.concept['@id']));
205
- const incomingEdges = computed(() => props.edges.filter(e => e.target === props.concept['@id']));
223
+ const conceptUriValue = computed(() =>
224
+ conceptUri(props.concept, props.registerId, props.manifest.uriBase)
225
+ );
226
+
227
+ const outgoingEdges = computed(() => props.edges.filter(e => e.source === conceptUriValue.value));
228
+ const incomingEdges = computed(() => props.edges.filter(e => e.target === conceptUriValue.value));
206
229
 
207
230
  function isLocalRef(uri: string): boolean {
208
231
  const resolution = factory.resolve(uri, props.registerId);
@@ -241,7 +264,7 @@ function edgeDatasetBadge(uri: string): { id: string; title: string } | null {
241
264
  }
242
265
 
243
266
  async function navigateEdge(edge: GraphEdge) {
244
- const uri = edge.source === props.concept['@id'] ? edge.target : edge.source;
267
+ const uri = edge.source === conceptUriValue.value ? edge.target : edge.source;
245
268
  const resolution = factory.resolve(uri);
246
269
 
247
270
  if (resolution.type === 'internal') {
@@ -255,66 +278,86 @@ async function navigateEdge(edge: GraphEdge) {
255
278
  }
256
279
 
257
280
  function getTermForLang(lang: string): string {
258
- const lc = props.concept['gl:localizedConcept']?.[lang];
259
- if (!lc?.['gl:designation']?.length) return '\u2014';
260
- const desigs = lc['gl:designation'];
261
- const preferredExpr = desigs.find(d => d['gl:normativeStatus'] === 'preferred' && d['@type'] === 'gl:Expression');
262
- if (preferredExpr) return preferredExpr['gl:term'];
263
- const preferred = desigs.find(d => d['gl:normativeStatus'] === 'preferred');
264
- return preferred?.['gl:term'] ?? desigs[0]?.['gl:term'] ?? '\u2014';
281
+ const lc = props.concept.localization(lang);
282
+ return getPreferredTerm(lc);
265
283
  }
266
284
 
267
- function getDesignationsForLang(lang: string) {
268
- const lc = props.concept['gl:localizedConcept']?.[lang];
269
- return lc?.['gl:designation'] ?? [];
285
+ function getDesignationsForLang(lang: string): Designation[] {
286
+ const lc = props.concept.localization(lang);
287
+ return lc?.terms ?? [];
270
288
  }
271
289
 
272
- function orderedDesignations(lang: string) {
290
+ function orderedDesignations(lang: string): Designation[] {
273
291
  const desigs = getDesignationsForLang(lang);
274
- const preferred = desigs.filter(d => d['gl:normativeStatus'] === 'preferred');
275
- const admitted = desigs.filter(d => d['gl:normativeStatus'] === 'admitted' || d['gl:normativeStatus'] === 'deprecated');
276
- const rest = desigs.filter(d => d['gl:normativeStatus'] !== 'preferred' && d['gl:normativeStatus'] !== 'admitted' && d['gl:normativeStatus'] !== 'deprecated');
292
+ const preferred = desigs.filter(d => d.normativeStatus === 'preferred');
293
+ const admitted = desigs.filter(d => d.normativeStatus === 'admitted' || d.normativeStatus === 'deprecated');
294
+ const rest = desigs.filter(d => d.normativeStatus !== 'preferred' && d.normativeStatus !== 'admitted' && d.normativeStatus !== 'deprecated');
277
295
  return [...preferred, ...admitted, ...rest];
278
296
  }
279
297
 
280
298
  function hasDefinition(lang: string): boolean {
281
- const lc = props.concept['gl:localizedConcept']?.[lang];
299
+ const lc = props.concept.localization(lang);
282
300
  if (!lc) return false;
283
- return lc['gl:definition']?.some((d: any) => d['gl:content']) ?? false;
284
- }
285
-
286
- function designationTypeLabel(type: string): string {
287
- const labels: Record<string, string> = {
288
- 'gl:Expression': 'Expression',
289
- 'gl:Symbol': 'Symbol',
290
- 'gl:Abbreviation': 'Abbreviation',
291
- 'gl:GraphicalSymbol': 'Graphical',
292
- };
293
- return labels[type] ?? type;
294
- }
295
-
296
- function designationTypeColor(type: string): string {
297
- if (type === 'gl:Symbol') return 'badge-purple';
298
- if (type === 'gl:Abbreviation') return 'badge-yellow';
299
- return 'badge-blue';
300
- }
301
-
302
- function entryStatusColor(status: string): string {
303
- if (status === 'valid' || status === 'Standard') return 'badge-green';
304
- if (status === 'superseded') return 'bg-red-50 text-red-700';
305
- if (status === 'withdrawn') return 'bg-red-100 text-red-800';
306
- if (status === 'draft') return 'badge-yellow';
307
- return 'badge-gray';
301
+ return lc.definitions.some(d => d.content);
308
302
  }
309
303
 
310
304
  function goAdjacent(id: string) {
311
305
  router.push({ name: 'concept', params: { registerId: props.registerId, conceptId: id } });
306
+ window.scrollTo({ top: 0, behavior: 'smooth' });
312
307
  }
313
308
 
314
309
  function plainTruncate(html: string, max: number = 120): string {
315
310
  const text = cleanContent(html).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
316
- return text.length <= max ? text : text.slice(0, max).trimEnd() + '\u2026';
311
+ return text.length <= max ? text : text.slice(0, max).trimEnd() + '';
317
312
  }
313
+
314
+ function slugify(text: string): string {
315
+ return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
316
+ }
317
+
318
+ // Domain rendering: merge ConceptReference domains and per-localization domain strings
319
+ const conceptDomains = computed(() => {
320
+ const domainMap = new Map<string, { slug: string; label: string; langs: string[]; conceptId?: string }>();
321
+
322
+ // Managed concept level ConceptReference domains (authoritative)
323
+ for (const ref of conceptRefDomains.value) {
324
+ const id = ref.conceptId ?? '';
325
+ const label = id || ref.urn || '';
326
+ if (label) {
327
+ const slug = slugify(label);
328
+ domainMap.set(slug, { slug, label, langs: [], conceptId: id });
329
+ }
330
+ }
331
+
332
+ // Per-localization domain strings
333
+ for (const lang of props.concept.languages) {
334
+ const lc = props.concept.localization(lang);
335
+ const domain = lc?.domain;
336
+ if (domain) {
337
+ const slug = slugify(domain);
338
+ const existing = domainMap.get(slug);
339
+ if (existing) {
340
+ if (!existing.langs.includes(lang)) existing.langs.push(lang);
341
+ } else {
342
+ domainMap.set(slug, { slug, label: domain, langs: [lang] });
343
+ }
344
+ }
345
+ }
346
+ return [...domainMap.values()].sort((a, b) => b.langs.length - a.langs.length);
347
+ });
348
+
349
+ // Non-verbal reps: aggregate across all localizations
350
+ const nonVerbalReps = computed(() => {
351
+ const reps: typeof import('glossarist').NonVerbRep.prototype[] = [];
352
+ for (const lang of props.concept.languages) {
353
+ const lc = props.concept.localization(lang);
354
+ if (lc?.nonVerbalRep?.length) {
355
+ reps.push(...lc.nonVerbalRep);
356
+ }
357
+ }
358
+ return reps;
359
+ });
360
+
318
361
  </script>
319
362
 
320
363
  <template>
@@ -338,65 +381,87 @@ function plainTruncate(html: string, max: number = 120): string {
338
381
  <button
339
382
  v-if="adjacent.prev"
340
383
  @click="goAdjacent(adjacent.prev)"
341
- class="p-1.5 rounded-md text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors"
342
- title="Previous concept"
384
+ class="p-2.5 rounded-lg text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors min-w-[44px] min-h-[44px] flex items-center justify-center"
385
+ title="Previous concept (←)"
343
386
  >
344
387
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
345
388
  </button>
346
389
  <button
347
390
  v-if="adjacent.next"
348
391
  @click="goAdjacent(adjacent.next)"
349
- class="p-1.5 rounded-md text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors"
350
- title="Next concept"
392
+ class="p-2.5 rounded-lg text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors min-w-[44px] min-h-[44px] flex items-center justify-center"
393
+ title="Next concept (→)"
351
394
  >
352
395
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
353
396
  </button>
354
397
  </div>
355
398
  </div>
356
399
  <h1 class="font-serif text-2xl sm:text-3xl text-ink-800 leading-snug mb-3" v-html="renderMath(primaryTerm)"></h1>
357
- <div class="flex flex-wrap gap-2">
400
+ <div class="flex gap-2 overflow-x-auto pb-1 -mx-4 px-4 sm:flex-wrap sm:overflow-visible sm:mx-0 sm:pb-0 scrollbar-none">
358
401
  <span class="badge badge-blue font-mono">{{ conceptId }}</span>
359
- <span class="badge" :class="entryStatusColor(engConcept?.['gl:entryStatus'] ?? '')" v-if="engConcept?.['gl:entryStatus']">
360
- {{ engConcept['gl:entryStatus'] }}
402
+ <span
403
+ v-if="managedStatus"
404
+ class="badge text-[10px]"
405
+ :class="conceptStatusColor(managedStatus)"
406
+ :title="conceptStatusDefinition(managedStatus) ?? ''"
407
+ >
408
+ {{ conceptStatusLabel(managedStatus) }}
409
+ </span>
410
+ <span class="badge" :class="entryStatusColor(engConcept?.entryStatus ?? '')" v-if="engConcept?.entryStatus" :title="entryStatusDefinition(engConcept.entryStatus) ?? ''">
411
+ {{ entryStatusLabel(engConcept.entryStatus) }}
361
412
  </span>
362
413
  <span class="badge badge-gray" v-if="manifest.owner">{{ manifest.owner }}</span>
363
414
  <span class="badge badge-purple">{{ languages.length }} languages</span>
364
415
  </div>
365
416
  </div>
366
417
 
367
- <!-- Tab navigation -->
368
- <div role="tablist" class="flex border-b border-ink-100/60 mb-6">
418
+ <!-- Tab navigation: segmented control on mobile, underline on desktop -->
419
+ <div role="tablist"
420
+ class="grid grid-cols-3 rounded-xl bg-surface-alt p-1 mb-6 md:bg-transparent md:p-0 md:flex md:border-b md:border-ink-100/60 md:rounded-none">
369
421
  <button
370
422
  role="tab"
371
423
  :aria-selected="activeTab === 'definition'"
372
424
  @click="activeTab = 'definition'"
373
- :class="activeTab === 'definition' ? 'border-ink-800 text-ink-800' : 'border-transparent text-ink-400 hover:text-ink-600'"
374
- class="px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px"
425
+ class="py-3 text-sm font-medium rounded-lg transition-colors md:rounded-none md:border-b-2 md:-mb-px md:px-5 md:py-3"
426
+ :class="activeTab === 'definition'
427
+ ? 'bg-blue-600 text-white shadow-sm md:bg-transparent md:text-blue-600 md:border-blue-500 md:shadow-none'
428
+ : 'text-ink-500 hover:text-ink-700 md:text-ink-400 md:border-transparent md:hover:text-ink-600'"
375
429
  >
376
430
  Definition
377
431
  </button>
378
432
  <button
379
433
  role="tab"
380
- :aria-selected="activeTab === 'history'"
381
- @click="activeTab = 'history'"
382
- :class="activeTab === 'history' ? 'border-ink-800 text-ink-800' : 'border-transparent text-ink-400 hover:text-ink-600'"
383
- class="px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px"
434
+ :aria-selected="activeTab === 'rdf'"
435
+ @click="activeTab = 'rdf'"
436
+ class="py-3 text-sm font-medium rounded-lg transition-colors md:rounded-none md:border-b-2 md:-mb-px md:px-5 md:py-3"
437
+ :class="activeTab === 'rdf'
438
+ ? 'bg-blue-600 text-white shadow-sm md:bg-transparent md:text-blue-600 md:border-blue-500 md:shadow-none'
439
+ : 'text-ink-500 hover:text-ink-700 md:text-ink-400 md:border-transparent md:hover:text-ink-600'"
384
440
  >
385
- History
441
+ RDF
386
442
  </button>
387
- <!-- Expand/Collapse all toggle (definition tab only) -->
388
443
  <button
389
- v-if="activeTab === 'definition'"
390
- @click="toggleAll"
391
- class="ml-auto px-3 py-2 text-xs text-ink-400 hover:text-ink-600 transition-colors"
444
+ role="tab"
445
+ :aria-selected="activeTab === 'history'"
446
+ @click="activeTab = 'history'"
447
+ class="py-3 text-sm font-medium rounded-lg transition-colors md:rounded-none md:border-b-2 md:-mb-px md:px-5 md:py-3"
448
+ :class="activeTab === 'history'
449
+ ? 'bg-blue-600 text-white shadow-sm md:bg-transparent md:text-blue-600 md:border-blue-500 md:shadow-none'
450
+ : 'text-ink-500 hover:text-ink-700 md:text-ink-400 md:border-transparent md:hover:text-ink-600'"
392
451
  >
393
- {{ allCollapsed ? 'Expand all' : 'Collapse all' }}
394
- <span class="text-ink-300 ml-0.5">({{ languages.length }})</span>
452
+ History
395
453
  </button>
396
454
  </div>
397
455
 
398
456
  <!-- Tab: Definition -->
399
457
  <div v-if="activeTab === 'definition'" role="tabpanel">
458
+ <!-- Expand/Collapse all toggle -->
459
+ <div v-if="allLangContent.length > 1" class="flex items-center justify-between mb-3">
460
+ <span class="text-xs text-ink-400">{{ languages.length }} languages</span>
461
+ <button @click="toggleAll" class="text-xs text-ink-400 hover:text-ink-600 transition-colors px-3 py-2">
462
+ {{ allCollapsed ? 'Expand all' : 'Collapse all' }}
463
+ </button>
464
+ </div>
400
465
  <div class="lg:flex lg:gap-8">
401
466
  <!-- Left: all language content -->
402
467
  <div class="flex-1 min-w-0 space-y-2" @click="handleContentClick">
@@ -417,14 +482,14 @@ function plainTruncate(html: string, max: number = 120): string {
417
482
  </svg>
418
483
  <span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(lc.lang) }}</span>
419
484
  <span class="font-medium text-ink-800 text-sm" v-html="renderMath(getTermForLang(lc.lang))"></span>
420
- <span v-if="lc.entryStatus" class="badge text-[10px] ml-auto" :class="entryStatusColor(lc.entryStatus)">{{ lc.entryStatus }}</span>
485
+ <span v-if="lc.entryStatus" class="badge text-[10px] ml-auto" :class="entryStatusColor(lc.entryStatus)" :title="entryStatusDefinition(lc.entryStatus) ?? ''">{{ entryStatusLabel(lc.entryStatus) }}</span>
421
486
  </button>
422
487
  <!-- Non-collapsible header (designation only) -->
423
488
  <div v-else class="w-full flex items-center gap-2.5 px-3 sm:px-4 py-3">
424
489
  <span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(lc.lang) }}</span>
425
490
  <span class="font-medium text-ink-800 text-sm" v-html="renderMath(getTermForLang(lc.lang))"></span>
426
491
  <span class="text-xs text-ink-200 ml-2 italic">designation only</span>
427
- <span v-if="lc.entryStatus" class="badge text-[10px] ml-auto" :class="entryStatusColor(lc.entryStatus)">{{ lc.entryStatus }}</span>
492
+ <span v-if="lc.entryStatus" class="badge text-[10px] ml-auto" :class="entryStatusColor(lc.entryStatus)" :title="entryStatusDefinition(lc.entryStatus) ?? ''">{{ entryStatusLabel(lc.entryStatus) }}</span>
428
493
  </div>
429
494
  <!-- Collapsed preview -->
430
495
  <div v-if="hasContent(lc) && collapsedLangs.has(lc.lang)" class="px-3 sm:px-4 pb-3 -mt-0.5">
@@ -438,12 +503,59 @@ function plainTruncate(html: string, max: number = 120): string {
438
503
 
439
504
  <!-- Expandable content -->
440
505
  <div v-if="hasContent(lc)" v-show="!collapsedLangs.has(lc.lang)" class="lang-content px-3 sm:px-4 pb-4 space-y-3">
441
- <!-- Designations -->
442
- <div v-if="lc.designations.length > 1" class="space-y-1 pl-[22px]">
443
- <div v-for="(d, i) in orderedDesignations(lc.lang)" :key="i" class="flex items-center gap-2 text-sm">
444
- <span :class="d['gl:normativeStatus'] === 'preferred' ? 'font-bold text-ink-800' : 'font-normal text-ink-700'" v-html="renderMath(d['gl:term'])"></span>
445
- <span class="badge text-[10px] flex-shrink-0" :class="designationTypeColor(d['@type'])">{{ designationTypeLabel(d['@type']) }}</span>
446
- <span v-if="d['gl:normativeStatus'] && d['gl:normativeStatus'] !== 'preferred'" class="badge badge-yellow text-[10px] flex-shrink-0">{{ d['gl:normativeStatus'] }}</span>
506
+ <!-- Designations (show all, with full metadata) -->
507
+ <div v-if="lc.designations.length > 0" class="space-y-1.5 pl-[22px]">
508
+ <div v-for="(d, i) in orderedDesignations(lc.lang)" :key="i">
509
+ <div class="flex items-center gap-1.5 text-sm flex-wrap">
510
+ <span :class="d.normativeStatus === 'preferred' ? 'font-bold text-ink-800' : 'font-normal text-ink-700'" v-html="renderMath(d.designation)"></span>
511
+ <span class="badge text-[10px] flex-shrink-0" :class="designationTypeInfo(d).color" :title="designationTypeInfo(d).definition ?? ''">{{ designationTypeInfo(d).label }}</span>
512
+ <span class="badge text-[10px] flex-shrink-0" :class="normativeStatusInfo(d.normativeStatus).color" :title="normativeStatusInfo(d.normativeStatus).definition ?? ''">{{ normativeStatusInfo(d.normativeStatus).label }}</span>
513
+ <!-- Abbreviation details -->
514
+ <template v-if="abbreviationDetails(d).length">
515
+ <span v-for="abbr in abbreviationDetails(d)" :key="abbr" class="badge text-[10px] bg-amber-50 text-amber-600">{{ abbr }}</span>
516
+ </template>
517
+ <!-- Term type (ISO 12620) -->
518
+ <span v-if="d.termType" class="badge text-[10px] bg-gray-50 text-gray-600" :title="termTypeInfo(d.termType).definition ?? ''">{{ termTypeInfo(d.termType).label }}</span>
519
+ <!-- Grammar info -->
520
+ <template v-if="d.type === 'expression' && (d as Expression).grammarInfo?.length">
521
+ <template v-for="(gi, giIdx) in (d as Expression).grammarInfo" :key="giIdx">
522
+ <span v-for="badge in grammarBadges(gi)" :key="giIdx + '-' + badge.label"
523
+ class="badge text-[10px] bg-gray-50 text-gray-600" :title="badge.definition ?? ''">{{ badge.label }}</span>
524
+ </template>
525
+ </template>
526
+ <!-- Pronunciation -->
527
+ <template v-if="d.pronunciations?.length">
528
+ <span v-for="(p, pi) in d.pronunciations" :key="'p'+pi"
529
+ class="text-xs text-ink-400 font-mono" :title="pronunciationTooltip(p)">{{ pronunciationLabel(p) }}</span>
530
+ </template>
531
+ <!-- Flags -->
532
+ <span v-if="d.international" class="badge text-[10px] bg-sky-50 text-sky-600">international</span>
533
+ <span v-if="d.absent" class="badge text-[10px] bg-red-50 text-red-600">absent</span>
534
+ <span v-if="d.geographicalArea" class="badge text-[10px] bg-gray-50 text-gray-600">{{ d.geographicalArea }}</span>
535
+ <span v-if="d.usageInfo" class="text-xs text-ink-300">{{ d.usageInfo }}</span>
536
+ <span v-if="d.fieldOfApplication" class="text-xs text-ink-300">field: {{ d.fieldOfApplication }}</span>
537
+ <!-- Per-designation language/script/system overrides -->
538
+ <template v-if="d.language && d.language !== lc.lang">
539
+ <span class="badge text-[10px] bg-teal-50 text-teal-600">lang: {{ langName(d.language) }}</span>
540
+ </template>
541
+ <span v-if="d.script" class="badge text-[10px] bg-gray-50 text-gray-600">script: {{ d.script }}</span>
542
+ <span v-if="d.system" class="badge text-[10px] bg-gray-50 text-gray-600">system: {{ d.system }}</span>
543
+ </div>
544
+ <!-- Designation sources -->
545
+ <div v-if="d.sources?.length" class="mt-1 space-y-0.5">
546
+ <div v-for="(ds, dsi) in d.sources" :key="'ds'+dsi" class="text-xs text-ink-400 flex items-center gap-1.5">
547
+ <span v-if="ds.type" class="badge text-[9px]" :class="sourceTypeInfo(ds.type).color">{{ sourceTypeInfo(ds.type).label }}</span>
548
+ <CitationDisplay v-if="ds.origin" :citation="ds.origin" />
549
+ <span v-else-if="ds.modification" class="text-ink-300">{{ ds.modification }}</span>
550
+ </div>
551
+ </div>
552
+ <!-- Designation relationships -->
553
+ <div v-if="d.related?.length" class="mt-0.5 space-y-0.5">
554
+ <div v-for="(dr, dri) in d.related" :key="'dr'+dri" class="text-xs text-ink-400 flex items-center gap-1.5">
555
+ <span class="badge text-[9px] bg-gray-50 text-gray-600">{{ relationshipLabel(dr.type) }}</span>
556
+ <span>{{ dr.content || (dr.ref ? `${dr.ref.source || ''} ${dr.ref.id || ''}`.trim() : '') }}</span>
557
+ </div>
558
+ </div>
447
559
  </div>
448
560
  </div>
449
561
 
@@ -468,23 +580,59 @@ function plainTruncate(html: string, max: number = 120): string {
468
580
  </div>
469
581
  </div>
470
582
 
583
+ <!-- Non-verbal representations -->
584
+ <NonVerbalRepDisplay v-if="lc.lc.nonVerbalRep?.length" :reps="lc.lc.nonVerbalRep" />
585
+
471
586
  <!-- Sources -->
472
587
  <div v-if="lc.sources.length" class="space-y-2">
473
588
  <div v-for="(src, i) in lc.sources" :key="i" class="text-sm">
474
589
  <div class="flex items-center gap-1.5 flex-wrap mb-1">
475
- <span v-if="src['gl:sourceType']" class="badge badge-blue text-[10px]">{{ src['gl:sourceType'] }}</span>
476
- <span v-if="src['gl:sourceStatus']" class="badge badge-gray text-[10px]">{{ src['gl:sourceStatus'] }}</span>
590
+ <span v-if="src.type" class="badge text-[10px]" :class="sourceTypeInfo(src.type).color" :title="sourceTypeInfo(src.type).definition ?? ''">{{ sourceTypeInfo(src.type).label }}</span>
591
+ <span v-if="src.status" class="badge text-[10px]" :title="sourceStatusInfo(src.status).definition ?? ''" :class="sourceStatusInfo(src.status).color">{{ sourceStatusInfo(src.status).label }}</span>
477
592
  </div>
478
593
  <div class="text-ink-700">
479
- <span v-if="src['gl:origin']?.['gl:ref']" class="font-medium">{{ src['gl:origin']['gl:ref'] }}</span>
480
- <span v-if="src['gl:origin']?.['gl:clause']">, {{ src['gl:origin']['gl:clause'] }}</span>
481
- <a v-if="src['gl:origin']?.['gl:link']" :href="src['gl:origin']['gl:link']" target="_blank" class="concept-link ml-1">[link]</a>
594
+ <CitationDisplay v-if="src.origin" :citation="src.origin" />
595
+ <span v-if="!src.origin && src.modification" class="text-ink-400">{{ src.modification }}</span>
482
596
  </div>
483
- <div v-if="src['gl:modification']" class="text-xs text-ink-300 mt-1">{{ src['gl:modification'] }}</div>
597
+ <div v-if="src.modification" class="text-xs text-ink-300 mt-1">{{ src.modification }}</div>
484
598
  </div>
485
599
  </div>
600
+
601
+ <!-- Ontological metadata -->
602
+ <div v-if="lc.classification || lc.reviewType || lc.release || lc.lineageSourceSimilarity != null || lc.lcScript || lc.lcSystem" class="border-t border-ink-100/60 pt-2 mt-2">
603
+ <div class="text-[10px] uppercase tracking-wide text-ink-300 font-medium mb-1.5">Ontological metadata</div>
604
+ <dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
605
+ <template v-if="lc.classification">
606
+ <dt class="text-ink-300">Classification</dt>
607
+ <dd class="text-ink-700">{{ lc.classification }}</dd>
608
+ </template>
609
+ <template v-if="lc.reviewType">
610
+ <dt class="text-ink-300">Review type</dt>
611
+ <dd class="text-ink-700">{{ lc.reviewType }}</dd>
612
+ </template>
613
+ <template v-if="lc.release">
614
+ <dt class="text-ink-300">Release</dt>
615
+ <dd class="text-ink-700">{{ lc.release }}</dd>
616
+ </template>
617
+ <template v-if="lc.lineageSourceSimilarity != null">
618
+ <dt class="text-ink-300">Lineage similarity</dt>
619
+ <dd class="text-ink-700">{{ lc.lineageSourceSimilarity }}%</dd>
620
+ </template>
621
+ <template v-if="lc.lcScript">
622
+ <dt class="text-ink-300">Script</dt>
623
+ <dd class="text-ink-700 font-mono">{{ lc.lcScript }}</dd>
624
+ </template>
625
+ <template v-if="lc.lcSystem">
626
+ <dt class="text-ink-300">Conversion system</dt>
627
+ <dd class="text-ink-700 font-mono">{{ lc.lcSystem }}</dd>
628
+ </template>
629
+ </dl>
630
+ </div>
486
631
  </div>
487
632
  </div>
633
+
634
+ <!-- Non-verbal reps (concept-level) -->
635
+ <NonVerbalRepDisplay v-if="nonVerbalReps.length" :reps="nonVerbalReps" />
488
636
  </div>
489
637
 
490
638
  <!-- Right sidebar -->
@@ -493,29 +641,28 @@ function plainTruncate(html: string, max: number = 120): string {
493
641
  <div v-if="outgoingEdges.length || incomingEdges.length" class="card p-5">
494
642
  <div class="section-label">Relations</div>
495
643
  <div v-if="outgoingEdges.length" class="mt-3">
496
- <div class="text-xs text-ink-300 mb-2">References ({{ outgoingEdges.length }})</div>
497
- <div class="space-y-1 max-h-48 overflow-y-auto">
644
+ <div class="text-xs text-ink-300 mb-2">Outgoing ({{ outgoingEdges.length }})</div>
645
+ <div class="space-y-1 max-h-64 overflow-y-auto">
498
646
  <button
499
647
  v-for="edge in outgoingEdges"
500
- :key="edge.target"
648
+ :key="edge.target + edge.type"
501
649
  @click="navigateEdge(edge)"
502
650
  :title="edgeTooltip(edge.target)"
503
651
  class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
504
652
  :class="isLocalRef(edge.target) ? '' : 'xref-external'"
505
653
  >
506
- {{ edgeConceptId(edge.target) }}
654
+ <span class="badge text-[9px] flex-shrink-0" :class="categorizeRelationship(edge.type).color">{{ relationshipLabel(edge.type) }}</span>
655
+ {{ edge.label || edgeConceptId(edge.target) }}
507
656
  <span v-if="edgeDatasetBadge(edge.target)" class="badge badge-gray text-[9px] flex-shrink-0 truncate max-w-[100px]">{{ edgeDatasetBadge(edge.target)!.title }}</span>
508
- <span v-if="isLocalRef(edge.target)" class="text-[9px] text-ink-200 flex-shrink-0">local</span>
509
- <span v-else class="text-[9px] text-amber-500 flex-shrink-0">external</span>
510
657
  </button>
511
658
  </div>
512
659
  </div>
513
660
  <div v-if="incomingEdges.length" class="mt-3 pt-3 border-t border-ink-100/60">
514
- <div class="text-xs text-ink-300 mb-2">Referenced by ({{ incomingEdges.length }})</div>
661
+ <div class="text-xs text-ink-300 mb-2">Incoming ({{ incomingEdges.length }})</div>
515
662
  <div class="space-y-1 max-h-48 overflow-y-auto">
516
663
  <button
517
664
  v-for="edge in incomingEdges"
518
- :key="edge.source"
665
+ :key="edge.source + edge.type"
519
666
  @click="navigateEdge(edge)"
520
667
  :title="edgeTooltip(edge.source)"
521
668
  class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
@@ -523,13 +670,54 @@ function plainTruncate(html: string, max: number = 120): string {
523
670
  >
524
671
  {{ edgeConceptId(edge.source) }}
525
672
  <span v-if="edgeDatasetBadge(edge.source)" class="badge badge-gray text-[9px] flex-shrink-0 truncate max-w-[100px]">{{ edgeDatasetBadge(edge.source)!.title }}</span>
526
- <span v-if="isLocalRef(edge.source)" class="text-[9px] text-ink-200 flex-shrink-0">local</span>
527
- <span v-else class="text-[9px] text-amber-500 flex-shrink-0">external</span>
528
673
  </button>
529
674
  </div>
530
675
  </div>
531
676
  </div>
532
677
 
678
+ <!-- Domains -->
679
+ <div v-if="conceptDomains.length" class="card p-5">
680
+ <div class="section-label">Domains</div>
681
+ <div class="space-y-1 mt-3">
682
+ <div v-for="domain in conceptDomains" :key="domain.slug" class="flex items-center gap-1.5 text-sm">
683
+ <span class="w-2 h-1.5 rounded inline-block flex-shrink-0" style="background: #8b5cf6;"></span>
684
+ <span class="font-medium text-ink-700">{{ domain.label }}</span>
685
+ <span v-if="domain.conceptId" class="text-[10px] text-ink-300 font-mono">{{ domain.conceptId }}</span>
686
+ <span v-if="domain.langs.length > 0" class="text-[10px] text-ink-300 ml-1">
687
+ ({{ domain.langs.map(l => l.toUpperCase()).join(', ') }})
688
+ </span>
689
+ </div>
690
+ </div>
691
+ </div>
692
+
693
+ <!-- Managed concept dates -->
694
+ <div v-if="conceptDates.length" class="card p-5">
695
+ <div class="section-label">Lifecycle dates</div>
696
+ <dl class="mt-3 space-y-1.5 text-xs">
697
+ <div v-for="(d, i) in conceptDates" :key="i" class="flex gap-2">
698
+ <dt class="text-ink-300 min-w-[70px]">{{ d.type }}</dt>
699
+ <dd class="text-ink-700">{{ d.date }}</dd>
700
+ </div>
701
+ </dl>
702
+ </div>
703
+
704
+ <!-- Managed concept sources -->
705
+ <div v-if="conceptSources.length" class="card p-5">
706
+ <div class="section-label">Concept sources</div>
707
+ <div class="space-y-2 mt-3">
708
+ <div v-for="(src, i) in conceptSources" :key="i" class="text-xs">
709
+ <div class="flex items-center gap-1.5 flex-wrap mb-0.5">
710
+ <span v-if="src.type" class="badge text-[10px]" :class="sourceTypeInfo(src.type).color" :title="sourceTypeInfo(src.type).definition ?? ''">{{ sourceTypeInfo(src.type).label }}</span>
711
+ <span v-if="src.status" class="badge text-[10px]" :title="sourceStatusInfo(src.status).definition ?? ''" :class="sourceStatusInfo(src.status).color">{{ sourceStatusInfo(src.status).label }}</span>
712
+ </div>
713
+ <div class="text-ink-700">
714
+ <CitationDisplay v-if="src.origin" :citation="src.origin" />
715
+ </div>
716
+ <div v-if="src.modification" class="text-ink-300 mt-0.5">{{ src.modification }}</div>
717
+ </div>
718
+ </div>
719
+ </div>
720
+
533
721
  <!-- Language quick-jump -->
534
722
  <div class="card p-5">
535
723
  <div class="section-label">Languages ({{ languages.length }})</div>
@@ -552,11 +740,11 @@ function plainTruncate(html: string, max: number = 120): string {
552
740
  <div v-if="getDesignationsForLang(lang).length > 1" class="ml-5 mt-0.5 flex flex-wrap gap-1">
553
741
  <span
554
742
  v-for="d in getDesignationsForLang(lang)"
555
- :key="d['gl:term']"
556
- :class="d['@type'] === 'gl:Symbol' ? 'badge-purple' : 'badge-gray'"
743
+ :key="d.designation"
744
+ :class="d.type === 'symbol' ? 'badge-purple' : 'badge-gray'"
557
745
  class="badge text-[10px]"
558
746
  >
559
- {{ d['gl:term'] }}
747
+ {{ d.designation }}
560
748
  </span>
561
749
  </div>
562
750
  </button>
@@ -567,18 +755,24 @@ function plainTruncate(html: string, max: number = 120): string {
567
755
  <div class="card p-5">
568
756
  <div class="section-label">Metadata</div>
569
757
  <dl class="space-y-2 text-xs mt-3">
570
- <div v-if="engConcept?.['gl:reviewDate']">
758
+ <div v-if="managedStatus">
759
+ <dt class="text-ink-300">Status</dt>
760
+ <dd class="mt-0.5">
761
+ <span class="badge text-[10px]" :class="conceptStatusColor(managedStatus)" :title="conceptStatusDefinition(managedStatus) ?? ''">{{ conceptStatusLabel(managedStatus) }}</span>
762
+ </dd>
763
+ </div>
764
+ <div v-if="engConcept?.reviewDate">
571
765
  <dt class="text-ink-300">Review Date</dt>
572
- <dd class="text-ink-700 mt-0.5">{{ engConcept['gl:reviewDate'].slice(0, 10) }}</dd>
766
+ <dd class="text-ink-700 mt-0.5">{{ engConcept.reviewDate.slice(0, 10) }}</dd>
573
767
  </div>
574
- <div v-if="engConcept?.['gl:reviewDecisionEvent']">
768
+ <div v-if="engConcept?.reviewDecisionEvent">
575
769
  <dt class="text-ink-300">Decision</dt>
576
- <dd class="text-ink-700 mt-0.5">{{ engConcept['gl:reviewDecisionEvent'] }}</dd>
770
+ <dd class="text-ink-700 mt-0.5">{{ engConcept.reviewDecisionEvent }}</dd>
577
771
  </div>
578
772
  <div>
579
773
  <dt class="text-ink-300">URI</dt>
580
774
  <dd class="font-mono text-ink-600 break-all mt-0.5 text-[11px] flex items-start gap-1.5">
581
- <span class="break-all">{{ concept['@id'] }}</span>
775
+ <span class="break-all">{{ conceptUriValue }}</span>
582
776
  <button @click="copyUri" class="flex-shrink-0 p-0.5 rounded text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors" :title="uriCopied ? 'Copied!' : 'Copy URI'" :aria-label="uriCopied ? 'URI copied' : 'Copy URI to clipboard'">
583
777
  <svg v-if="!uriCopied" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10a2 2 0 01-2-2v-1m6 4v-3a2 2 0 00-2-2H8"/></svg>
584
778
  <svg v-else class="w-3.5 h-3.5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
@@ -598,9 +792,18 @@ function plainTruncate(html: string, max: number = 120): string {
598
792
  </div>
599
793
 
600
794
  <!-- Tab: History -->
795
+ <!-- Tab: RDF -->
796
+ <div v-if="activeTab === 'rdf'" role="tabpanel">
797
+ <ConceptRdfView
798
+ :concept="concept"
799
+ :register-id="registerId"
800
+ :concept-uri-value="conceptUriValue"
801
+ />
802
+ </div>
803
+
601
804
  <div v-if="activeTab === 'history'" role="tabpanel">
602
805
  <ConceptTimeline
603
- :localized-concepts="concept['gl:localizedConcept'] || {}"
806
+ :concept="concept"
604
807
  :language-order="manifest.languageOrder"
605
808
  v-model:active-lang="activeHistoryLang"
606
809
  />