@glossarist/concept-browser 0.3.7 → 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.
- package/README.md +3 -2
- package/cli/index.mjs +2 -1
- package/env.d.ts +5 -0
- package/package.json +4 -3
- package/scripts/build-edges.js +78 -10
- package/scripts/generate-data.mjs +152 -20
- package/scripts/generate-ontology-data.mjs +184 -0
- package/scripts/generate-ontology-schema.mjs +315 -0
- package/src/__tests__/concept-card.test.ts +1 -1
- package/src/__tests__/concept-detail-interaction.test.ts +40 -18
- package/src/__tests__/concept-formats.test.ts +32 -30
- package/src/__tests__/concept-timeline.test.ts +108 -83
- package/src/__tests__/concept-view.test.ts +15 -2
- package/src/__tests__/dataset-adapter.test.ts +172 -23
- package/src/__tests__/dataset-view.test.ts +6 -5
- package/src/__tests__/designation-registry.test.ts +161 -0
- package/src/__tests__/graph.test.ts +62 -0
- package/src/__tests__/language-detail.test.ts +117 -60
- package/src/__tests__/ontology-registry.test.ts +109 -0
- package/src/__tests__/relationship-categories.test.ts +62 -0
- package/src/__tests__/test-helpers.ts +11 -8
- package/src/adapters/DatasetAdapter.ts +171 -48
- package/src/adapters/model-bridge.ts +277 -0
- package/src/adapters/ontology-registry.ts +75 -0
- package/src/adapters/ontology-schema.ts +100 -0
- package/src/adapters/types.ts +52 -77
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/CitationDisplay.vue +35 -0
- package/src/components/ConceptDetail.vue +334 -93
- package/src/components/ConceptRdfView.vue +397 -0
- package/src/components/ConceptTimeline.vue +56 -52
- package/src/components/GraphPanel.vue +96 -31
- package/src/components/LanguageDetail.vue +45 -37
- package/src/components/NavIcon.vue +1 -0
- package/src/components/NonVerbalRepDisplay.vue +38 -0
- package/src/components/RelationshipList.vue +99 -0
- package/src/config/use-site-config.ts +3 -0
- package/src/data/ontology-schema.json +1551 -0
- package/src/data/taxonomies.json +543 -0
- package/src/graph/GraphEngine.ts +7 -4
- package/src/router/index.ts +5 -0
- package/src/shims/empty.ts +1 -0
- package/src/shims/node-crypto.ts +6 -0
- package/src/shims/node-path.ts +10 -0
- package/src/stores/vocabulary.ts +75 -25
- package/src/style.css +74 -20
- package/src/utils/concept-formats.ts +22 -20
- package/src/utils/concept-helpers.ts +43 -23
- package/src/utils/designation-registry.ts +124 -0
- package/src/utils/relationship-categories.ts +84 -0
- package/src/views/OntologySchemaView.vue +302 -0
- package/src/views/PageView.vue +28 -17
- package/src/views/StatsView.vue +34 -12
- package/vite.config.ts +8 -0
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
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
8
|
import { escapeAttr } from '../utils/escape';
|
|
9
|
-
import { entryStatusColor,
|
|
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';
|
|
10
12
|
import { useRouter } from 'vue-router';
|
|
11
13
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
12
14
|
import { useDsStyle } from '../utils/dataset-style';
|
|
13
15
|
import { getFactory } from '../adapters/factory';
|
|
14
16
|
import { useRenderOptions } from '../composables/use-render-options';
|
|
17
|
+
import { categorizeRelationship, relationshipLabel, relationshipDefinition } from '../utils/relationship-categories';
|
|
15
18
|
import ConceptTimeline from './ConceptTimeline.vue';
|
|
19
|
+
import ConceptRdfView from './ConceptRdfView.vue';
|
|
16
20
|
import FormatDownloads from './FormatDownloads.vue';
|
|
21
|
+
import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
|
|
22
|
+
import CitationDisplay from './CitationDisplay.vue';
|
|
17
23
|
|
|
18
24
|
const props = defineProps<{
|
|
19
|
-
concept:
|
|
25
|
+
concept: Concept;
|
|
20
26
|
manifest: Manifest;
|
|
21
27
|
edges: GraphEdge[];
|
|
22
28
|
registerId: string;
|
|
@@ -28,10 +34,10 @@ const store = useVocabularyStore();
|
|
|
28
34
|
const { getColor } = useDsStyle();
|
|
29
35
|
const factory = getFactory();
|
|
30
36
|
|
|
31
|
-
const activeTab = ref<'definition' | 'history'>('definition');
|
|
37
|
+
const activeTab = ref<'rdf' | 'definition' | 'history'>('definition');
|
|
32
38
|
const activeHistoryLang = ref('eng');
|
|
33
39
|
|
|
34
|
-
const conceptId = computed(() => props.concept
|
|
40
|
+
const conceptId = computed(() => props.concept.id);
|
|
35
41
|
|
|
36
42
|
const conceptPosition = computed(() => {
|
|
37
43
|
const adapter = store.datasets.get(props.registerId);
|
|
@@ -43,7 +49,8 @@ const conceptPosition = computed(() => {
|
|
|
43
49
|
|
|
44
50
|
const uriCopied = ref(false);
|
|
45
51
|
function copyUri() {
|
|
46
|
-
|
|
52
|
+
const uri = conceptUri(props.concept, props.registerId, props.manifest.uriBase);
|
|
53
|
+
navigator.clipboard.writeText(uri).then(() => {
|
|
47
54
|
uriCopied.value = true;
|
|
48
55
|
setTimeout(() => { uriCopied.value = false; }, 2000);
|
|
49
56
|
});
|
|
@@ -51,16 +58,16 @@ function copyUri() {
|
|
|
51
58
|
|
|
52
59
|
const languages = computed(() => {
|
|
53
60
|
const order = props.manifest.languageOrder;
|
|
54
|
-
const keys =
|
|
61
|
+
const keys = props.concept.languages;
|
|
55
62
|
if (!order) {
|
|
56
|
-
return keys.sort((a, b) => {
|
|
63
|
+
return [...keys].sort((a, b) => {
|
|
57
64
|
if (a === 'eng') return -1;
|
|
58
|
-
if (
|
|
65
|
+
if (a === 'eng') return 1;
|
|
59
66
|
return a.localeCompare(b);
|
|
60
67
|
});
|
|
61
68
|
}
|
|
62
69
|
const orderIndex = new Map(order.map((lang, i) => [lang, i]));
|
|
63
|
-
return keys.sort((a, b) => {
|
|
70
|
+
return [...keys].sort((a, b) => {
|
|
64
71
|
const ai = orderIndex.get(a) ?? order.length;
|
|
65
72
|
const bi = orderIndex.get(b) ?? order.length;
|
|
66
73
|
if (ai !== bi) return ai - bi;
|
|
@@ -80,11 +87,23 @@ function initCollapsed(langs: string[]) {
|
|
|
80
87
|
watch(languages, (langs) => { initCollapsed(langs); }, { immediate: true });
|
|
81
88
|
|
|
82
89
|
const engConcept = computed((): LocalizedConcept | null => {
|
|
83
|
-
return props.concept
|
|
90
|
+
return props.concept.localization('eng') ?? null;
|
|
84
91
|
});
|
|
85
92
|
|
|
86
93
|
const primaryTerm = computed(() => getPreferredTerm(engConcept.value, conceptId.value));
|
|
87
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);
|
|
106
|
+
|
|
88
107
|
// Cross-reference resolver: generates clickable links for inline refs
|
|
89
108
|
|
|
90
109
|
const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
|
|
@@ -125,33 +144,45 @@ function handleContentClick(e: MouseEvent) {
|
|
|
125
144
|
// Pre-computed content for all languages (sorted eng first)
|
|
126
145
|
interface LangContent {
|
|
127
146
|
lang: string;
|
|
147
|
+
lc: LocalizedConcept;
|
|
128
148
|
definition: string;
|
|
129
149
|
notes: string[];
|
|
130
150
|
examples: string[];
|
|
131
|
-
sources:
|
|
132
|
-
designations:
|
|
151
|
+
sources: ConceptSource[];
|
|
152
|
+
designations: Designation[];
|
|
133
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;
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
const allLangContent = computed(() => {
|
|
137
163
|
const result: LangContent[] = [];
|
|
138
164
|
for (const lang of languages.value) {
|
|
139
|
-
const lc = props.concept
|
|
165
|
+
const lc = props.concept.localization(lang);
|
|
140
166
|
if (!lc) continue;
|
|
141
167
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
? defs.map(d => d['gl:content']).filter(Boolean).join('\n\n')
|
|
145
|
-
: '';
|
|
168
|
+
const definition = lc.definitions
|
|
169
|
+
.map(d => d.content).filter(Boolean).join('\n\n');
|
|
146
170
|
|
|
147
171
|
result.push({
|
|
148
172
|
lang,
|
|
173
|
+
lc,
|
|
149
174
|
definition,
|
|
150
|
-
notes: lc
|
|
151
|
-
examples: lc
|
|
152
|
-
sources: lc
|
|
153
|
-
designations: lc
|
|
154
|
-
entryStatus: lc
|
|
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,
|
|
155
186
|
});
|
|
156
187
|
}
|
|
157
188
|
return result;
|
|
@@ -178,21 +209,23 @@ function toggleAll() {
|
|
|
178
209
|
}
|
|
179
210
|
|
|
180
211
|
function scrollToLang(lang: string) {
|
|
181
|
-
// Expand if collapsed
|
|
182
212
|
if (collapsedLangs.value.has(lang)) {
|
|
183
213
|
const s = new Set(collapsedLangs.value);
|
|
184
214
|
s.delete(lang);
|
|
185
215
|
collapsedLangs.value = s;
|
|
186
216
|
}
|
|
187
|
-
// Switch to definition tab if needed
|
|
188
217
|
activeTab.value = 'definition';
|
|
189
218
|
nextTick(() => {
|
|
190
219
|
document.getElementById(`lang-${lang}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
191
220
|
});
|
|
192
221
|
}
|
|
193
222
|
|
|
194
|
-
const
|
|
195
|
-
|
|
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));
|
|
196
229
|
|
|
197
230
|
function isLocalRef(uri: string): boolean {
|
|
198
231
|
const resolution = factory.resolve(uri, props.registerId);
|
|
@@ -231,7 +264,7 @@ function edgeDatasetBadge(uri: string): { id: string; title: string } | null {
|
|
|
231
264
|
}
|
|
232
265
|
|
|
233
266
|
async function navigateEdge(edge: GraphEdge) {
|
|
234
|
-
const uri = edge.source ===
|
|
267
|
+
const uri = edge.source === conceptUriValue.value ? edge.target : edge.source;
|
|
235
268
|
const resolution = factory.resolve(uri);
|
|
236
269
|
|
|
237
270
|
if (resolution.type === 'internal') {
|
|
@@ -245,27 +278,27 @@ async function navigateEdge(edge: GraphEdge) {
|
|
|
245
278
|
}
|
|
246
279
|
|
|
247
280
|
function getTermForLang(lang: string): string {
|
|
248
|
-
const lc = props.concept
|
|
281
|
+
const lc = props.concept.localization(lang);
|
|
249
282
|
return getPreferredTerm(lc);
|
|
250
283
|
}
|
|
251
284
|
|
|
252
|
-
function getDesignationsForLang(lang: string) {
|
|
253
|
-
const lc = props.concept
|
|
254
|
-
return lc?.
|
|
285
|
+
function getDesignationsForLang(lang: string): Designation[] {
|
|
286
|
+
const lc = props.concept.localization(lang);
|
|
287
|
+
return lc?.terms ?? [];
|
|
255
288
|
}
|
|
256
289
|
|
|
257
|
-
function orderedDesignations(lang: string) {
|
|
290
|
+
function orderedDesignations(lang: string): Designation[] {
|
|
258
291
|
const desigs = getDesignationsForLang(lang);
|
|
259
|
-
const preferred = desigs.filter(d => d
|
|
260
|
-
const admitted = desigs.filter(d => d
|
|
261
|
-
const rest = desigs.filter(d => d
|
|
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');
|
|
262
295
|
return [...preferred, ...admitted, ...rest];
|
|
263
296
|
}
|
|
264
297
|
|
|
265
298
|
function hasDefinition(lang: string): boolean {
|
|
266
|
-
const lc = props.concept
|
|
299
|
+
const lc = props.concept.localization(lang);
|
|
267
300
|
if (!lc) return false;
|
|
268
|
-
return lc
|
|
301
|
+
return lc.definitions.some(d => d.content);
|
|
269
302
|
}
|
|
270
303
|
|
|
271
304
|
function goAdjacent(id: string) {
|
|
@@ -275,8 +308,56 @@ function goAdjacent(id: string) {
|
|
|
275
308
|
|
|
276
309
|
function plainTruncate(html: string, max: number = 120): string {
|
|
277
310
|
const text = cleanContent(html).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
278
|
-
return text.length <= max ? text : text.slice(0, max).trimEnd() + '
|
|
311
|
+
return text.length <= max ? text : text.slice(0, max).trimEnd() + '…';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function slugify(text: string): string {
|
|
315
|
+
return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
|
|
279
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
|
+
|
|
280
361
|
</script>
|
|
281
362
|
|
|
282
363
|
<template>
|
|
@@ -300,7 +381,7 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
300
381
|
<button
|
|
301
382
|
v-if="adjacent.prev"
|
|
302
383
|
@click="goAdjacent(adjacent.prev)"
|
|
303
|
-
class="p-
|
|
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"
|
|
304
385
|
title="Previous concept (←)"
|
|
305
386
|
>
|
|
306
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>
|
|
@@ -308,7 +389,7 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
308
389
|
<button
|
|
309
390
|
v-if="adjacent.next"
|
|
310
391
|
@click="goAdjacent(adjacent.next)"
|
|
311
|
-
class="p-
|
|
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"
|
|
312
393
|
title="Next concept (→)"
|
|
313
394
|
>
|
|
314
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>
|
|
@@ -316,49 +397,71 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
316
397
|
</div>
|
|
317
398
|
</div>
|
|
318
399
|
<h1 class="font-serif text-2xl sm:text-3xl text-ink-800 leading-snug mb-3" v-html="renderMath(primaryTerm)"></h1>
|
|
319
|
-
<div class="flex flex-wrap
|
|
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">
|
|
320
401
|
<span class="badge badge-blue font-mono">{{ conceptId }}</span>
|
|
321
|
-
<span
|
|
322
|
-
|
|
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) }}
|
|
323
412
|
</span>
|
|
324
413
|
<span class="badge badge-gray" v-if="manifest.owner">{{ manifest.owner }}</span>
|
|
325
414
|
<span class="badge badge-purple">{{ languages.length }} languages</span>
|
|
326
415
|
</div>
|
|
327
416
|
</div>
|
|
328
417
|
|
|
329
|
-
<!-- Tab navigation -->
|
|
330
|
-
<div role="tablist"
|
|
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">
|
|
331
421
|
<button
|
|
332
422
|
role="tab"
|
|
333
423
|
:aria-selected="activeTab === 'definition'"
|
|
334
424
|
@click="activeTab = 'definition'"
|
|
335
|
-
|
|
336
|
-
class="
|
|
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'"
|
|
337
429
|
>
|
|
338
430
|
Definition
|
|
339
431
|
</button>
|
|
340
432
|
<button
|
|
341
433
|
role="tab"
|
|
342
|
-
:aria-selected="activeTab === '
|
|
343
|
-
@click="activeTab = '
|
|
344
|
-
|
|
345
|
-
class="
|
|
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'"
|
|
346
440
|
>
|
|
347
|
-
|
|
441
|
+
RDF
|
|
348
442
|
</button>
|
|
349
|
-
<!-- Expand/Collapse all toggle (definition tab only) -->
|
|
350
443
|
<button
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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'"
|
|
354
451
|
>
|
|
355
|
-
|
|
356
|
-
<span class="text-ink-300 ml-0.5">({{ languages.length }})</span>
|
|
452
|
+
History
|
|
357
453
|
</button>
|
|
358
454
|
</div>
|
|
359
455
|
|
|
360
456
|
<!-- Tab: Definition -->
|
|
361
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>
|
|
362
465
|
<div class="lg:flex lg:gap-8">
|
|
363
466
|
<!-- Left: all language content -->
|
|
364
467
|
<div class="flex-1 min-w-0 space-y-2" @click="handleContentClick">
|
|
@@ -379,14 +482,14 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
379
482
|
</svg>
|
|
380
483
|
<span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(lc.lang) }}</span>
|
|
381
484
|
<span class="font-medium text-ink-800 text-sm" v-html="renderMath(getTermForLang(lc.lang))"></span>
|
|
382
|
-
<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>
|
|
383
486
|
</button>
|
|
384
487
|
<!-- Non-collapsible header (designation only) -->
|
|
385
488
|
<div v-else class="w-full flex items-center gap-2.5 px-3 sm:px-4 py-3">
|
|
386
489
|
<span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(lc.lang) }}</span>
|
|
387
490
|
<span class="font-medium text-ink-800 text-sm" v-html="renderMath(getTermForLang(lc.lang))"></span>
|
|
388
491
|
<span class="text-xs text-ink-200 ml-2 italic">designation only</span>
|
|
389
|
-
<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>
|
|
390
493
|
</div>
|
|
391
494
|
<!-- Collapsed preview -->
|
|
392
495
|
<div v-if="hasContent(lc) && collapsedLangs.has(lc.lang)" class="px-3 sm:px-4 pb-3 -mt-0.5">
|
|
@@ -400,12 +503,59 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
400
503
|
|
|
401
504
|
<!-- Expandable content -->
|
|
402
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">
|
|
403
|
-
<!-- Designations -->
|
|
404
|
-
<div v-if="lc.designations.length >
|
|
405
|
-
<div v-for="(d, i) in orderedDesignations(lc.lang)" :key="i"
|
|
406
|
-
<
|
|
407
|
-
|
|
408
|
-
|
|
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>
|
|
409
559
|
</div>
|
|
410
560
|
</div>
|
|
411
561
|
|
|
@@ -430,23 +580,59 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
430
580
|
</div>
|
|
431
581
|
</div>
|
|
432
582
|
|
|
583
|
+
<!-- Non-verbal representations -->
|
|
584
|
+
<NonVerbalRepDisplay v-if="lc.lc.nonVerbalRep?.length" :reps="lc.lc.nonVerbalRep" />
|
|
585
|
+
|
|
433
586
|
<!-- Sources -->
|
|
434
587
|
<div v-if="lc.sources.length" class="space-y-2">
|
|
435
588
|
<div v-for="(src, i) in lc.sources" :key="i" class="text-sm">
|
|
436
589
|
<div class="flex items-center gap-1.5 flex-wrap mb-1">
|
|
437
|
-
<span v-if="src
|
|
438
|
-
<span v-if="src
|
|
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>
|
|
439
592
|
</div>
|
|
440
593
|
<div class="text-ink-700">
|
|
441
|
-
<
|
|
442
|
-
<span v-if="src
|
|
443
|
-
<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>
|
|
444
596
|
</div>
|
|
445
|
-
<div v-if="src
|
|
597
|
+
<div v-if="src.modification" class="text-xs text-ink-300 mt-1">{{ src.modification }}</div>
|
|
446
598
|
</div>
|
|
447
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>
|
|
448
631
|
</div>
|
|
449
632
|
</div>
|
|
633
|
+
|
|
634
|
+
<!-- Non-verbal reps (concept-level) -->
|
|
635
|
+
<NonVerbalRepDisplay v-if="nonVerbalReps.length" :reps="nonVerbalReps" />
|
|
450
636
|
</div>
|
|
451
637
|
|
|
452
638
|
<!-- Right sidebar -->
|
|
@@ -455,29 +641,28 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
455
641
|
<div v-if="outgoingEdges.length || incomingEdges.length" class="card p-5">
|
|
456
642
|
<div class="section-label">Relations</div>
|
|
457
643
|
<div v-if="outgoingEdges.length" class="mt-3">
|
|
458
|
-
<div class="text-xs text-ink-300 mb-2">
|
|
459
|
-
<div class="space-y-1 max-h-
|
|
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">
|
|
460
646
|
<button
|
|
461
647
|
v-for="edge in outgoingEdges"
|
|
462
|
-
:key="edge.target"
|
|
648
|
+
:key="edge.target + edge.type"
|
|
463
649
|
@click="navigateEdge(edge)"
|
|
464
650
|
:title="edgeTooltip(edge.target)"
|
|
465
651
|
class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
|
|
466
652
|
:class="isLocalRef(edge.target) ? '' : 'xref-external'"
|
|
467
653
|
>
|
|
468
|
-
{{
|
|
654
|
+
<span class="badge text-[9px] flex-shrink-0" :class="categorizeRelationship(edge.type).color">{{ relationshipLabel(edge.type) }}</span>
|
|
655
|
+
{{ edge.label || edgeConceptId(edge.target) }}
|
|
469
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>
|
|
470
|
-
<span v-if="isLocalRef(edge.target)" class="text-[9px] text-ink-200 flex-shrink-0">local</span>
|
|
471
|
-
<span v-else class="text-[9px] text-amber-500 flex-shrink-0">external</span>
|
|
472
657
|
</button>
|
|
473
658
|
</div>
|
|
474
659
|
</div>
|
|
475
660
|
<div v-if="incomingEdges.length" class="mt-3 pt-3 border-t border-ink-100/60">
|
|
476
|
-
<div class="text-xs text-ink-300 mb-2">
|
|
661
|
+
<div class="text-xs text-ink-300 mb-2">Incoming ({{ incomingEdges.length }})</div>
|
|
477
662
|
<div class="space-y-1 max-h-48 overflow-y-auto">
|
|
478
663
|
<button
|
|
479
664
|
v-for="edge in incomingEdges"
|
|
480
|
-
:key="edge.source"
|
|
665
|
+
:key="edge.source + edge.type"
|
|
481
666
|
@click="navigateEdge(edge)"
|
|
482
667
|
:title="edgeTooltip(edge.source)"
|
|
483
668
|
class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
|
|
@@ -485,13 +670,54 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
485
670
|
>
|
|
486
671
|
{{ edgeConceptId(edge.source) }}
|
|
487
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>
|
|
488
|
-
<span v-if="isLocalRef(edge.source)" class="text-[9px] text-ink-200 flex-shrink-0">local</span>
|
|
489
|
-
<span v-else class="text-[9px] text-amber-500 flex-shrink-0">external</span>
|
|
490
673
|
</button>
|
|
491
674
|
</div>
|
|
492
675
|
</div>
|
|
493
676
|
</div>
|
|
494
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
|
+
|
|
495
721
|
<!-- Language quick-jump -->
|
|
496
722
|
<div class="card p-5">
|
|
497
723
|
<div class="section-label">Languages ({{ languages.length }})</div>
|
|
@@ -514,11 +740,11 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
514
740
|
<div v-if="getDesignationsForLang(lang).length > 1" class="ml-5 mt-0.5 flex flex-wrap gap-1">
|
|
515
741
|
<span
|
|
516
742
|
v-for="d in getDesignationsForLang(lang)"
|
|
517
|
-
:key="d
|
|
518
|
-
:class="d
|
|
743
|
+
:key="d.designation"
|
|
744
|
+
:class="d.type === 'symbol' ? 'badge-purple' : 'badge-gray'"
|
|
519
745
|
class="badge text-[10px]"
|
|
520
746
|
>
|
|
521
|
-
{{ d
|
|
747
|
+
{{ d.designation }}
|
|
522
748
|
</span>
|
|
523
749
|
</div>
|
|
524
750
|
</button>
|
|
@@ -529,18 +755,24 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
529
755
|
<div class="card p-5">
|
|
530
756
|
<div class="section-label">Metadata</div>
|
|
531
757
|
<dl class="space-y-2 text-xs mt-3">
|
|
532
|
-
<div v-if="
|
|
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">
|
|
533
765
|
<dt class="text-ink-300">Review Date</dt>
|
|
534
|
-
<dd class="text-ink-700 mt-0.5">{{ engConcept
|
|
766
|
+
<dd class="text-ink-700 mt-0.5">{{ engConcept.reviewDate.slice(0, 10) }}</dd>
|
|
535
767
|
</div>
|
|
536
|
-
<div v-if="engConcept?.
|
|
768
|
+
<div v-if="engConcept?.reviewDecisionEvent">
|
|
537
769
|
<dt class="text-ink-300">Decision</dt>
|
|
538
|
-
<dd class="text-ink-700 mt-0.5">{{ engConcept
|
|
770
|
+
<dd class="text-ink-700 mt-0.5">{{ engConcept.reviewDecisionEvent }}</dd>
|
|
539
771
|
</div>
|
|
540
772
|
<div>
|
|
541
773
|
<dt class="text-ink-300">URI</dt>
|
|
542
774
|
<dd class="font-mono text-ink-600 break-all mt-0.5 text-[11px] flex items-start gap-1.5">
|
|
543
|
-
<span class="break-all">{{
|
|
775
|
+
<span class="break-all">{{ conceptUriValue }}</span>
|
|
544
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'">
|
|
545
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>
|
|
546
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>
|
|
@@ -560,9 +792,18 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
560
792
|
</div>
|
|
561
793
|
|
|
562
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
|
+
|
|
563
804
|
<div v-if="activeTab === 'history'" role="tabpanel">
|
|
564
805
|
<ConceptTimeline
|
|
565
|
-
:
|
|
806
|
+
:concept="concept"
|
|
566
807
|
:language-order="manifest.languageOrder"
|
|
567
808
|
v-model:active-lang="activeHistoryLang"
|
|
568
809
|
/>
|