@glossarist/concept-browser 0.7.22 → 0.7.24
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/index.html +2 -1
- package/package.json +1 -1
- package/scripts/build-edges.js +50 -5
- package/scripts/generate-data.mjs +33 -6
- package/src/App.vue +10 -12
- package/src/__tests__/concept-view.test.ts +7 -1
- package/src/__tests__/dataset-adapter.test.ts +87 -0
- package/src/__tests__/dataset-view.test.ts +1 -0
- package/src/__tests__/factory-lazy.test.ts +183 -0
- package/src/__tests__/graph-engine-fixes.test.ts +104 -0
- package/src/__tests__/ontology-registry.test.ts +4 -4
- package/src/__tests__/performance-v2.test.ts +77 -0
- package/src/__tests__/performance.test.ts +95 -0
- package/src/__tests__/search-utils.test.ts +59 -0
- package/src/__tests__/test-helpers.ts +4 -0
- package/src/__tests__/utils-barrel.test.ts +15 -0
- package/src/__tests__/vocabulary-layered.test.ts +291 -0
- package/src/adapters/DatasetAdapter.ts +41 -1
- package/src/adapters/factory.ts +35 -4
- package/src/adapters/ontology-registry.ts +1 -1
- package/src/adapters/types.ts +12 -0
- package/src/components/AppSidebar.vue +17 -343
- package/src/components/ConceptDetail.vue +121 -70
- package/src/components/GraphPanel.vue +14 -6
- package/src/components/OntologySidebarSection.vue +338 -0
- package/src/config/use-site-config.ts +20 -9
- package/src/data/taxonomies.json +12 -6
- package/src/directives/v-math.ts +2 -3
- package/src/graph/GraphEngine.ts +22 -5
- package/src/i18n/index.ts +1 -1
- package/src/stores/vocabulary.ts +65 -105
- package/src/utils/index.ts +1 -0
- package/src/utils/relationship-categories.ts +3 -2
- package/src/utils/search.ts +15 -0
- package/src/views/ConceptView.vue +0 -2
- package/src/views/DatasetView.vue +64 -39
- package/src/views/HomeView.vue +0 -1
- package/vite.config.ts +94 -6
|
@@ -81,6 +81,7 @@ const engConcept = computed((): LocalizedConcept | null => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
const primaryTerm = computed(() => getPreferredTerm(engConcept.value, conceptId.value));
|
|
84
|
+
const renderedPrimaryTerm = computed(() => renderMath(primaryTerm.value));
|
|
84
85
|
|
|
85
86
|
// Managed concept status from Concept.status (7 values from concept-status.ttl)
|
|
86
87
|
const managedStatus = computed(() => props.concept.status);
|
|
@@ -139,7 +140,6 @@ function resolveRelatedRef(ref: { source: string | null; id: string | null } | n
|
|
|
139
140
|
async function navigateRelated(ref: { source: string | null; id: string | null }) {
|
|
140
141
|
const target = resolveRelatedRef(ref);
|
|
141
142
|
if (!target) return;
|
|
142
|
-
await store.viewConcept(target.registerId, target.conceptId);
|
|
143
143
|
router.push({ name: 'concept', params: { registerId: target.registerId, conceptId: target.conceptId } });
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -178,7 +178,6 @@ function handleContentClick(e: MouseEvent) {
|
|
|
178
178
|
const registerId = target.dataset.register;
|
|
179
179
|
const conceptId = target.dataset.concept;
|
|
180
180
|
if (registerId && conceptId) {
|
|
181
|
-
store.viewConcept(registerId, conceptId);
|
|
182
181
|
router.push({ name: 'concept', params: { registerId, conceptId } });
|
|
183
182
|
}
|
|
184
183
|
}
|
|
@@ -187,11 +186,16 @@ function handleContentClick(e: MouseEvent) {
|
|
|
187
186
|
interface LangContent {
|
|
188
187
|
lang: string;
|
|
189
188
|
lc: LocalizedConcept;
|
|
189
|
+
renderedTerm: string;
|
|
190
190
|
definition: string;
|
|
191
|
+
renderedDefinition: string;
|
|
191
192
|
notes: string[];
|
|
193
|
+
renderedNotes: string[];
|
|
192
194
|
examples: string[];
|
|
195
|
+
renderedExamples: string[];
|
|
193
196
|
sources: ConceptSource[];
|
|
194
197
|
designations: Designation[];
|
|
198
|
+
renderedDesignations: Map<string, string>;
|
|
195
199
|
entryStatus: string;
|
|
196
200
|
classification: string | null;
|
|
197
201
|
reviewType: string | null;
|
|
@@ -209,15 +213,22 @@ const allLangContent = computed(() => {
|
|
|
209
213
|
|
|
210
214
|
const definition = lc.definitions
|
|
211
215
|
.map(d => d.content).filter(Boolean).join('\n\n');
|
|
216
|
+
const notes = lc.notes.map(n => n.content).filter(Boolean);
|
|
217
|
+
const examples = lc.examples.map(e => e.content).filter(Boolean);
|
|
212
218
|
|
|
213
219
|
result.push({
|
|
214
220
|
lang,
|
|
215
221
|
lc,
|
|
222
|
+
renderedTerm: renderMath(getPreferredTerm(lc, '')),
|
|
216
223
|
definition,
|
|
217
|
-
|
|
218
|
-
|
|
224
|
+
renderedDefinition: renderMath(definition, renderOpts),
|
|
225
|
+
notes,
|
|
226
|
+
renderedNotes: notes.map(n => renderMath(n, renderOpts)),
|
|
227
|
+
examples,
|
|
228
|
+
renderedExamples: examples.map(e => renderMath(e, renderOpts)),
|
|
219
229
|
sources: lc.sources,
|
|
220
230
|
designations: lc.terms,
|
|
231
|
+
renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderMath(d.designation)])),
|
|
221
232
|
entryStatus: lc.entryStatus ?? '',
|
|
222
233
|
classification: lc.classification,
|
|
223
234
|
reviewType: lc.reviewType,
|
|
@@ -230,6 +241,12 @@ const allLangContent = computed(() => {
|
|
|
230
241
|
return result;
|
|
231
242
|
});
|
|
232
243
|
|
|
244
|
+
const langContentMap = computed(() => {
|
|
245
|
+
const map = new Map<string, LangContent>();
|
|
246
|
+
for (const lc of allLangContent.value) map.set(lc.lang, lc);
|
|
247
|
+
return map;
|
|
248
|
+
});
|
|
249
|
+
|
|
233
250
|
function hasContent(lc: LangContent): boolean {
|
|
234
251
|
return !!(lc.definition || lc.notes.length || lc.examples.length || lc.sources.length);
|
|
235
252
|
}
|
|
@@ -280,43 +297,83 @@ const conceptUriValue = computed(() =>
|
|
|
280
297
|
conceptUri(props.concept, props.registerId, props.manifest.uriBase)
|
|
281
298
|
);
|
|
282
299
|
|
|
283
|
-
const outgoingEdges = computed(() => props.edges.filter(e => e.source === conceptUriValue.value));
|
|
284
|
-
const incomingEdges = computed(() => props.edges.filter(e => e.target === conceptUriValue.value));
|
|
300
|
+
const outgoingEdges = computed(() => props.edges.filter(e => e.source === conceptUriValue.value && e.type !== 'domain' && e.type !== 'section'));
|
|
301
|
+
const incomingEdges = computed(() => props.edges.filter(e => e.target === conceptUriValue.value && e.type !== 'domain' && e.type !== 'section'));
|
|
285
302
|
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
return resolution.type === 'internal' && resolution.registerId === props.registerId;
|
|
303
|
+
function inverseEdgeType(type: string): string {
|
|
304
|
+
return INVERSE_RELATIONSHIPS[type] || type;
|
|
289
305
|
}
|
|
290
306
|
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
|
|
307
|
+
function edgeBadgeColor(type: string, direction: 'out' | 'in'): string {
|
|
308
|
+
if (type === 'supersedes' || type === 'superseded_by') {
|
|
309
|
+
return direction === 'out' ? 'text-orange-700 bg-orange-50' : 'text-red-700 bg-red-50';
|
|
310
|
+
}
|
|
311
|
+
return categorizeRelationship(type).color;
|
|
294
312
|
}
|
|
295
313
|
|
|
296
|
-
|
|
297
|
-
|
|
314
|
+
interface EdgeDisplay {
|
|
315
|
+
uri: string;
|
|
316
|
+
conceptId: string;
|
|
317
|
+
designation: string;
|
|
318
|
+
tooltip: string;
|
|
319
|
+
isLocal: boolean;
|
|
320
|
+
badge: { id: string; title: string } | null;
|
|
298
321
|
}
|
|
299
322
|
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
323
|
+
const edgeDisplayCache = computed(() => {
|
|
324
|
+
const cache = new Map<string, EdgeDisplay>();
|
|
325
|
+
for (const e of props.edges) {
|
|
326
|
+
const uri = e.source === conceptUriValue.value ? e.target : e.source;
|
|
327
|
+
if (cache.has(uri)) continue;
|
|
328
|
+
const resolution = factory.resolve(uri, props.registerId);
|
|
329
|
+
const isLocal = resolution.type === 'internal' && resolution.registerId === props.registerId;
|
|
330
|
+
const conceptId = uri.match(/\/concept\/([^/]+)$/)?.[1] ?? uri.split('/').pop() ?? uri;
|
|
331
|
+
const node = store.graph.getNode(uri);
|
|
332
|
+
const designation = node
|
|
333
|
+
? (node.designations[locale.value] || node.designations.eng || Object.values(node.designations)[0] || '')
|
|
334
|
+
: '';
|
|
335
|
+
const tooltipLines: string[] = [uri];
|
|
336
|
+
if (node) {
|
|
337
|
+
for (const [lang, des] of Object.entries(node.designations)) {
|
|
338
|
+
tooltipLines.push(`${langLabel(lang)}: ${des}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
let badge: { id: string; title: string } | null = null;
|
|
342
|
+
if (resolution.type === 'internal' && resolution.registerId !== props.registerId) {
|
|
343
|
+
const m = store.manifests.get(resolution.registerId);
|
|
344
|
+
badge = { id: resolution.registerId, title: m?.shortname || m?.title || resolution.registerId };
|
|
345
|
+
} else if (resolution.type === 'site') {
|
|
346
|
+
badge = { id: '', title: resolution.label };
|
|
347
|
+
} else if (resolution.type === 'url') {
|
|
348
|
+
badge = { id: '', title: resolution.label };
|
|
306
349
|
}
|
|
350
|
+
cache.set(uri, { uri, conceptId, designation, tooltip: tooltipLines.join('\n'), isLocal, badge });
|
|
307
351
|
}
|
|
308
|
-
return
|
|
352
|
+
return cache;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
function getEdgeDisplay(uri: string): EdgeDisplay {
|
|
356
|
+
return edgeDisplayCache.value.get(uri) ?? { uri, conceptId: uri, tooltip: uri, isLocal: false, badge: null };
|
|
309
357
|
}
|
|
310
358
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
359
|
+
interface ResolvedRef {
|
|
360
|
+
target: { registerId: string; conceptId: string } | null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const resolvedRefs = computed(() => {
|
|
364
|
+
const map = new Map<string, ResolvedRef>();
|
|
365
|
+
for (const cr of conceptRelated.value) {
|
|
366
|
+
const key = `${cr.ref?.source ?? ''}:${cr.ref?.id ?? ''}`;
|
|
367
|
+
if (map.has(key)) continue;
|
|
368
|
+
map.set(key, { target: resolveRelatedRef(cr.ref) });
|
|
316
369
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
370
|
+
return map;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
function getResolvedRef(ref: { source: string | null; id: string | null } | null): ResolvedRef {
|
|
374
|
+
if (!ref) return { target: null };
|
|
375
|
+
const key = `${ref.source ?? ''}:${ref.id ?? ''}`;
|
|
376
|
+
return resolvedRefs.value.get(key) ?? { target: resolveRelatedRef(ref) };
|
|
320
377
|
}
|
|
321
378
|
|
|
322
379
|
async function navigateEdge(edge: GraphEdge) {
|
|
@@ -324,7 +381,6 @@ async function navigateEdge(edge: GraphEdge) {
|
|
|
324
381
|
const resolution = factory.resolve(uri);
|
|
325
382
|
|
|
326
383
|
if (resolution.type === 'internal') {
|
|
327
|
-
await store.viewConcept(resolution.registerId, resolution.conceptId);
|
|
328
384
|
router.push({ name: 'concept', params: { registerId: resolution.registerId, conceptId: resolution.conceptId } });
|
|
329
385
|
} else if (resolution.type === 'site') {
|
|
330
386
|
window.open(`${resolution.baseUrl}/resolve/${encodeURIComponent(uri)}`, '_blank', 'noopener');
|
|
@@ -333,6 +389,11 @@ async function navigateEdge(edge: GraphEdge) {
|
|
|
333
389
|
}
|
|
334
390
|
}
|
|
335
391
|
|
|
392
|
+
function navigateDomain(domain: { slug: string; conceptId?: string }) {
|
|
393
|
+
const sectionId = domain.conceptId || domain.slug;
|
|
394
|
+
router.push({ name: 'dataset', params: { registerId: manifest.id }, query: { section: sectionId } });
|
|
395
|
+
}
|
|
396
|
+
|
|
336
397
|
function getTermForLang(lang: string): string {
|
|
337
398
|
const lc = props.concept.localization(lang);
|
|
338
399
|
return getPreferredTerm(lc);
|
|
@@ -452,7 +513,7 @@ const nonVerbalReps = computed(() => {
|
|
|
452
513
|
</button>
|
|
453
514
|
</div>
|
|
454
515
|
</div>
|
|
455
|
-
<h1 class="font-serif text-2xl sm:text-3xl text-ink-800 leading-snug mb-3" v-html="
|
|
516
|
+
<h1 class="font-serif text-2xl sm:text-3xl text-ink-800 leading-snug mb-3" v-html="renderedPrimaryTerm"></h1>
|
|
456
517
|
<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">
|
|
457
518
|
<span class="badge badge-blue font-mono">{{ conceptId }}</span>
|
|
458
519
|
<span
|
|
@@ -537,13 +598,13 @@ const nonVerbalReps = computed(() => {
|
|
|
537
598
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
538
599
|
</svg>
|
|
539
600
|
<span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(lc.lang) }}</span>
|
|
540
|
-
<span class="font-medium text-ink-800 text-sm" v-html="
|
|
601
|
+
<span class="font-medium text-ink-800 text-sm" v-html="lc.renderedTerm"></span>
|
|
541
602
|
<span v-if="lc.entryStatus" class="badge text-[10px] ml-auto" :class="entryStatusColor(lc.entryStatus)" :title="entryStatusDefinition(lc.entryStatus) ?? ''">{{ entryStatusLabel(lc.entryStatus) }}</span>
|
|
542
603
|
</button>
|
|
543
604
|
<!-- Non-collapsible header (designation only) -->
|
|
544
605
|
<div v-else class="w-full flex items-center gap-2.5 px-3 sm:px-4 py-3">
|
|
545
606
|
<span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(lc.lang) }}</span>
|
|
546
|
-
<span class="font-medium text-ink-800 text-sm" v-html="
|
|
607
|
+
<span class="font-medium text-ink-800 text-sm" v-html="lc.renderedTerm"></span>
|
|
547
608
|
<span class="text-xs text-ink-200 ml-2 italic">{{ t('concept.designationOnly') }}</span>
|
|
548
609
|
<span v-if="lc.entryStatus" class="badge text-[10px] ml-auto" :class="entryStatusColor(lc.entryStatus)" :title="entryStatusDefinition(lc.entryStatus) ?? ''">{{ entryStatusLabel(lc.entryStatus) }}</span>
|
|
549
610
|
</div>
|
|
@@ -563,7 +624,7 @@ const nonVerbalReps = computed(() => {
|
|
|
563
624
|
<div v-if="lc.designations.length > 0" class="space-y-1.5 pl-[22px]">
|
|
564
625
|
<div v-for="(d, i) in orderedDesignations(lc.lang)" :key="i">
|
|
565
626
|
<div class="flex items-center gap-1.5 text-sm flex-wrap">
|
|
566
|
-
<span :class="d.normativeStatus === 'preferred' ? 'font-bold text-ink-800' : 'font-normal text-ink-700'" v-html="
|
|
627
|
+
<span :class="d.normativeStatus === 'preferred' ? 'font-bold text-ink-800' : 'font-normal text-ink-700'" v-html="lc.renderedDesignations.get(d.designation) ?? d.designation"></span>
|
|
567
628
|
<span class="badge text-[10px] flex-shrink-0" :class="designationTypeInfo(d).color" :title="designationTypeInfo(d).definition ?? ''">{{ designationTypeInfo(d).label }}</span>
|
|
568
629
|
<span class="badge text-[10px] flex-shrink-0" :class="normativeStatusInfo(d.normativeStatus).color" :title="normativeStatusInfo(d.normativeStatus).definition ?? ''">{{ normativeStatusInfo(d.normativeStatus).label }}</span>
|
|
569
630
|
<!-- Abbreviation details -->
|
|
@@ -609,7 +670,7 @@ const nonVerbalReps = computed(() => {
|
|
|
609
670
|
<div v-if="d.related?.length" class="mt-0.5 space-y-0.5">
|
|
610
671
|
<div v-for="(dr, dri) in d.related" :key="'dr'+dri" class="text-xs text-ink-400 flex items-center gap-1.5">
|
|
611
672
|
<span class="badge text-[9px] bg-gray-50 text-gray-600">{{ relationshipLabel(dr.type) }}</span>
|
|
612
|
-
<button v-if="
|
|
673
|
+
<button v-if="getResolvedRef(dr.ref).target" @click="navigateRelated(dr.ref!)" class="concept-link">{{ dr.content || (dr.ref ? `${dr.ref.source || ''} ${dr.ref.id || ''}`.trim() : '') }}</button>
|
|
613
674
|
<span v-else>{{ dr.content || (dr.ref ? `${dr.ref.source || ''} ${dr.ref.id || ''}`.trim() : '') }}</span>
|
|
614
675
|
</div>
|
|
615
676
|
</div>
|
|
@@ -618,22 +679,22 @@ const nonVerbalReps = computed(() => {
|
|
|
618
679
|
|
|
619
680
|
<!-- Definition -->
|
|
620
681
|
<div v-if="lc.definition" class="p-4 rounded-lg bg-surface border-l-2" :style="{ borderLeftColor: getColor(manifest.id) }">
|
|
621
|
-
<div class="text-ink-800 leading-relaxed" v-html="
|
|
682
|
+
<div class="text-ink-800 leading-relaxed" v-html="lc.renderedDefinition"></div>
|
|
622
683
|
</div>
|
|
623
684
|
|
|
624
685
|
<!-- Notes -->
|
|
625
686
|
<div v-if="lc.notes.length" class="space-y-2">
|
|
626
|
-
<div v-for="(
|
|
687
|
+
<div v-for="(_, i) in lc.notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
|
|
627
688
|
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.note') }} {{ i + 1 }}</span>
|
|
628
|
-
<div class="mt-1" v-html="
|
|
689
|
+
<div class="mt-1" v-html="lc.renderedNotes[i]"></div>
|
|
629
690
|
</div>
|
|
630
691
|
</div>
|
|
631
692
|
|
|
632
693
|
<!-- Examples -->
|
|
633
694
|
<div v-if="lc.examples.length" class="space-y-2">
|
|
634
|
-
<div v-for="(
|
|
695
|
+
<div v-for="(_, i) in lc.examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
|
|
635
696
|
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ i + 1 }}</span>
|
|
636
|
-
<div class="mt-1" v-html="
|
|
697
|
+
<div class="mt-1" v-html="lc.renderedExamples[i]"></div>
|
|
637
698
|
</div>
|
|
638
699
|
</div>
|
|
639
700
|
|
|
@@ -704,13 +765,14 @@ const nonVerbalReps = computed(() => {
|
|
|
704
765
|
v-for="edge in outgoingEdges"
|
|
705
766
|
:key="edge.target + edge.type"
|
|
706
767
|
@click="navigateEdge(edge)"
|
|
707
|
-
:title="
|
|
768
|
+
:title="getEdgeDisplay(edge.target).tooltip"
|
|
708
769
|
class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
|
|
709
|
-
:class="
|
|
770
|
+
:class="getEdgeDisplay(edge.target).isLocal ? '' : 'xref-external'"
|
|
710
771
|
>
|
|
711
|
-
<span class="badge text-[9px] flex-shrink-0" :class="
|
|
712
|
-
{{ edge.label ||
|
|
713
|
-
<span
|
|
772
|
+
<span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'out')">{{ relationshipLabel(edge.type) }} →</span>
|
|
773
|
+
<span class="truncate">{{ getEdgeDisplay(edge.target).designation || edge.label || getEdgeDisplay(edge.target).conceptId }}</span>
|
|
774
|
+
<span class="text-[9px] text-ink-300 flex-shrink-0 font-mono">{{ getEdgeDisplay(edge.target).conceptId }}</span>
|
|
775
|
+
<span v-if="getEdgeDisplay(edge.target).badge" class="badge badge-gray text-[9px] flex-shrink-0 truncate max-w-[100px]">{{ getEdgeDisplay(edge.target).badge!.title }}</span>
|
|
714
776
|
</button>
|
|
715
777
|
</div>
|
|
716
778
|
</div>
|
|
@@ -721,47 +783,36 @@ const nonVerbalReps = computed(() => {
|
|
|
721
783
|
v-for="edge in incomingEdges"
|
|
722
784
|
:key="edge.source + edge.type"
|
|
723
785
|
@click="navigateEdge(edge)"
|
|
724
|
-
:title="
|
|
786
|
+
:title="getEdgeDisplay(edge.source).tooltip"
|
|
725
787
|
class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
|
|
726
|
-
:class="
|
|
788
|
+
:class="getEdgeDisplay(edge.source).isLocal ? '' : 'xref-external'"
|
|
727
789
|
>
|
|
728
|
-
{{
|
|
729
|
-
<span
|
|
790
|
+
<span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'in')">← {{ relationshipLabel(inverseEdgeType(edge.type)) }}</span>
|
|
791
|
+
<span class="truncate">{{ getEdgeDisplay(edge.source).designation || edge.label || getEdgeDisplay(edge.source).conceptId }}</span>
|
|
792
|
+
<span class="text-[9px] text-ink-300 flex-shrink-0 font-mono">{{ getEdgeDisplay(edge.source).conceptId }}</span>
|
|
793
|
+
<span v-if="getEdgeDisplay(edge.source).badge" class="badge badge-gray text-[9px] flex-shrink-0 truncate max-w-[100px]">{{ getEdgeDisplay(edge.source).badge!.title }}</span>
|
|
730
794
|
</button>
|
|
731
795
|
</div>
|
|
732
796
|
</div>
|
|
733
797
|
</div>
|
|
734
798
|
|
|
735
|
-
<!-- Cross-references (concept-level related) -->
|
|
736
|
-
<div v-if="conceptRelated.length" class="card p-5">
|
|
737
|
-
<div class="section-label">{{ t('concept.relations') }}</div>
|
|
738
|
-
<div class="mt-3 space-y-1">
|
|
739
|
-
<button
|
|
740
|
-
v-for="(cr, cri) in conceptRelated"
|
|
741
|
-
:key="'cr'+cri"
|
|
742
|
-
@click="navigateRelated(cr.ref!)"
|
|
743
|
-
class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
|
|
744
|
-
>
|
|
745
|
-
<span class="badge text-[9px] bg-gray-50 text-gray-600">{{ relationshipLabel(cr.type) }}</span>
|
|
746
|
-
<span v-if="resolveRelatedRef(cr.ref)" class="text-ink-600">{{ resolveRelatedRef(cr.ref)!.conceptId }}</span>
|
|
747
|
-
<span v-else class="text-ink-400">{{ cr.content || (cr.ref ? `${cr.ref.source || ''} ${cr.ref.id || ''}`.trim() : '') }}</span>
|
|
748
|
-
<span v-if="resolveRelatedRef(cr.ref) && resolveRelatedRef(cr.ref)!.registerId !== manifest.id" class="badge badge-gray text-[9px] flex-shrink-0">{{ resolveRelatedRef(cr.ref)!.registerId }}</span>
|
|
749
|
-
</button>
|
|
750
|
-
</div>
|
|
751
|
-
</div>
|
|
752
|
-
|
|
753
799
|
<!-- Domains -->
|
|
754
800
|
<div v-if="conceptDomains.length" class="card p-5">
|
|
755
801
|
<div class="section-label">{{ t('concept.domains') }}</div>
|
|
756
802
|
<div class="space-y-1 mt-3">
|
|
757
|
-
<
|
|
803
|
+
<button
|
|
804
|
+
v-for="domain in conceptDomains"
|
|
805
|
+
:key="domain.slug"
|
|
806
|
+
@click="navigateDomain(domain)"
|
|
807
|
+
class="flex items-center gap-1.5 text-sm concept-link w-full text-left"
|
|
808
|
+
>
|
|
758
809
|
<span class="w-2 h-1.5 rounded inline-block flex-shrink-0" style="background: #8b5cf6;"></span>
|
|
759
810
|
<span class="font-medium text-ink-700">{{ domain.label }}</span>
|
|
760
811
|
<span v-if="domain.conceptId" class="text-[10px] text-ink-300 font-mono">{{ domain.conceptId }}</span>
|
|
761
812
|
<span v-if="domain.langs.length > 0" class="text-[10px] text-ink-300 ml-1">
|
|
762
813
|
({{ domain.langs.map(l => l.toUpperCase()).join(', ') }})
|
|
763
814
|
</span>
|
|
764
|
-
</
|
|
815
|
+
</button>
|
|
765
816
|
</div>
|
|
766
817
|
</div>
|
|
767
818
|
|
|
@@ -818,7 +869,7 @@ const nonVerbalReps = computed(() => {
|
|
|
818
869
|
:class="hasDefinition(lang) ? 'bg-emerald-400' : 'bg-ink-200'"
|
|
819
870
|
:title="hasDefinition(lang) ? t('concept.hasDefinition') : t('concept.designationOnlyTitle')"
|
|
820
871
|
></span>
|
|
821
|
-
<span class="text-sm font-medium text-ink-800 group-hover:text-ink-900 transition-colors" v-html="
|
|
872
|
+
<span class="text-sm font-medium text-ink-800 group-hover:text-ink-900 transition-colors" v-html="langContentMap.get(lang)?.renderedTerm ?? getTermForLang(lang)"></span>
|
|
822
873
|
</div>
|
|
823
874
|
<div v-if="getDesignationsForLang(lang).length > 1" class="ml-5 mt-0.5 flex flex-wrap gap-1">
|
|
824
875
|
<span
|
|
@@ -399,24 +399,32 @@ watch(selectedNode, (node) => {
|
|
|
399
399
|
}
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
-
// Rebuild when data or filters change
|
|
402
|
+
// Rebuild when data or filters change — coalesce into single rebuild
|
|
403
|
+
let rebuildScheduled = false;
|
|
404
|
+
function scheduleRebuild() {
|
|
405
|
+
if (rebuildScheduled) return;
|
|
406
|
+
rebuildScheduled = true;
|
|
407
|
+
nextTick(() => {
|
|
408
|
+
rebuildScheduled = false;
|
|
409
|
+
rebuildGraph();
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
403
413
|
let prevDataKey = '';
|
|
404
414
|
watch([() => props.nodes.length, () => props.edges.length], ([nn, ne]) => {
|
|
405
415
|
const key = `${nn}:${ne}`;
|
|
406
416
|
if (key !== prevDataKey && nn > 0) {
|
|
407
417
|
prevDataKey = key;
|
|
408
|
-
|
|
418
|
+
scheduleRebuild();
|
|
409
419
|
}
|
|
410
420
|
});
|
|
411
421
|
|
|
412
|
-
// Rebuild when register filters change
|
|
413
422
|
watch(registerEnabled, () => {
|
|
414
|
-
|
|
423
|
+
scheduleRebuild();
|
|
415
424
|
});
|
|
416
425
|
|
|
417
|
-
// Rebuild when language changes (designation labels depend on language)
|
|
418
426
|
watch(() => uiStore.selectedLang, () => {
|
|
419
|
-
|
|
427
|
+
scheduleRebuild();
|
|
420
428
|
});
|
|
421
429
|
|
|
422
430
|
onMounted(() => {
|