@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.
- 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__/about-view.test.ts +98 -0
- package/src/__tests__/app-footer.test.ts +38 -0
- package/src/__tests__/app-header.test.ts +130 -0
- package/src/__tests__/app-sidebar.test.ts +159 -0
- package/src/__tests__/asciidoc-lite.test.ts +1 -1
- package/src/__tests__/concept-card.test.ts +115 -0
- package/src/__tests__/concept-detail-interaction.test.ts +273 -0
- package/src/__tests__/concept-formats.test.ts +32 -30
- package/src/__tests__/concept-timeline.test.ts +200 -0
- package/src/__tests__/concept-view.test.ts +88 -0
- package/src/__tests__/contributors-view.test.ts +103 -0
- package/src/__tests__/dataset-adapter.test.ts +172 -23
- package/src/__tests__/dataset-view.test.ts +232 -0
- package/src/__tests__/designation-registry.test.ts +161 -0
- package/src/__tests__/format-downloads.test.ts +98 -0
- package/src/__tests__/graph-view.test.ts +69 -0
- package/src/__tests__/graph.test.ts +62 -0
- package/src/__tests__/home-interaction.test.ts +157 -0
- package/src/__tests__/language-detail.test.ts +203 -0
- package/src/__tests__/nav-icon.test.ts +48 -0
- package/src/__tests__/news-view.test.ts +87 -0
- package/src/__tests__/ontology-registry.test.ts +109 -0
- package/src/__tests__/page-view.test.ts +83 -0
- package/src/__tests__/relationship-categories.test.ts +62 -0
- package/src/__tests__/resolve-view.test.ts +77 -0
- package/src/__tests__/router.test.ts +65 -0
- package/src/__tests__/search-bar.test.ts +219 -0
- package/src/__tests__/search-view.test.ts +41 -0
- package/src/__tests__/stats-view.test.ts +77 -0
- package/src/__tests__/test-helpers.ts +171 -0
- package/src/__tests__/ui-store.test.ts +100 -0
- package/src/__tests__/v-math.test.ts +8 -7
- package/src/adapters/DatasetAdapter.ts +188 -63
- 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 +53 -78
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/CitationDisplay.vue +35 -0
- package/src/components/ConceptDetail.vue +349 -146
- package/src/components/ConceptRdfView.vue +397 -0
- package/src/components/ConceptTimeline.vue +57 -60
- package/src/components/GraphPanel.vue +96 -31
- package/src/components/LanguageDetail.vue +46 -61
- package/src/components/NavIcon.vue +1 -0
- package/src/components/NonVerbalRepDisplay.vue +38 -0
- package/src/components/RelationshipList.vue +99 -0
- package/src/composables/use-render-options.ts +1 -4
- 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 +6 -1
- 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 +82 -32
- package/src/style.css +74 -20
- package/src/utils/asciidoc-lite.ts +17 -19
- package/src/utils/concept-formats.ts +22 -20
- package/src/utils/concept-helpers.ts +54 -0
- package/src/utils/designation-registry.ts +124 -0
- package/src/utils/escape.ts +7 -0
- package/src/utils/markdown-lite.ts +1 -3
- package/src/utils/math.ts +2 -11
- package/src/utils/plurimath.ts +2 -7
- package/src/utils/relationship-categories.ts +84 -0
- package/src/views/ConceptView.vue +22 -1
- package/src/views/DatasetView.vue +7 -2
- 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,20 +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
|
+
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:
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
90
|
+
return props.concept.localization('eng') ?? null;
|
|
74
91
|
});
|
|
75
92
|
|
|
76
|
-
const primaryTerm = computed(() =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
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:
|
|
133
|
-
designations:
|
|
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
|
|
165
|
+
const lc = props.concept.localization(lang);
|
|
141
166
|
if (!lc) continue;
|
|
142
167
|
|
|
143
|
-
const
|
|
144
|
-
|
|
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
|
|
152
|
-
examples: lc
|
|
153
|
-
sources: lc
|
|
154
|
-
designations: lc
|
|
155
|
-
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,
|
|
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
|
|
205
|
-
|
|
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 ===
|
|
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
|
|
259
|
-
|
|
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
|
|
269
|
-
return lc?.
|
|
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
|
|
275
|
-
const admitted = desigs.filter(d => d
|
|
276
|
-
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');
|
|
277
295
|
return [...preferred, ...admitted, ...rest];
|
|
278
296
|
}
|
|
279
297
|
|
|
280
298
|
function hasDefinition(lang: string): boolean {
|
|
281
|
-
const lc = props.concept
|
|
299
|
+
const lc = props.concept.localization(lang);
|
|
282
300
|
if (!lc) return false;
|
|
283
|
-
return lc
|
|
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() + '
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
360
|
-
|
|
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"
|
|
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
|
-
|
|
374
|
-
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'"
|
|
375
429
|
>
|
|
376
430
|
Definition
|
|
377
431
|
</button>
|
|
378
432
|
<button
|
|
379
433
|
role="tab"
|
|
380
|
-
:aria-selected="activeTab === '
|
|
381
|
-
@click="activeTab = '
|
|
382
|
-
|
|
383
|
-
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'"
|
|
384
440
|
>
|
|
385
|
-
|
|
441
|
+
RDF
|
|
386
442
|
</button>
|
|
387
|
-
<!-- Expand/Collapse all toggle (definition tab only) -->
|
|
388
443
|
<button
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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 >
|
|
443
|
-
<div v-for="(d, i) in orderedDesignations(lc.lang)" :key="i"
|
|
444
|
-
<
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
476
|
-
<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>
|
|
477
592
|
</div>
|
|
478
593
|
<div class="text-ink-700">
|
|
479
|
-
<
|
|
480
|
-
<span v-if="src
|
|
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
|
|
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">
|
|
497
|
-
<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">
|
|
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
|
-
{{
|
|
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">
|
|
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
|
|
556
|
-
:class="d
|
|
743
|
+
:key="d.designation"
|
|
744
|
+
:class="d.type === 'symbol' ? 'badge-purple' : 'badge-gray'"
|
|
557
745
|
class="badge text-[10px]"
|
|
558
746
|
>
|
|
559
|
-
{{ d
|
|
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="
|
|
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
|
|
766
|
+
<dd class="text-ink-700 mt-0.5">{{ engConcept.reviewDate.slice(0, 10) }}</dd>
|
|
573
767
|
</div>
|
|
574
|
-
<div v-if="engConcept?.
|
|
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
|
|
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">{{
|
|
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
|
-
:
|
|
806
|
+
:concept="concept"
|
|
604
807
|
:language-order="manifest.languageOrder"
|
|
605
808
|
v-model:active-lang="activeHistoryLang"
|
|
606
809
|
/>
|