@glossarist/concept-browser 0.7.35 → 0.7.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-edges.js +16 -8
  3. package/scripts/generate-data.mjs +284 -86
  4. package/src/__tests__/citation-display.test.ts +165 -3
  5. package/src/__tests__/cite-ref.test.ts +112 -0
  6. package/src/__tests__/concept-detail-interaction.test.ts +1 -1
  7. package/src/__tests__/{math.test.ts → content-renderer.test.ts} +113 -29
  8. package/src/__tests__/escape.test.ts +76 -0
  9. package/src/__tests__/graph-data-source.test.ts +155 -0
  10. package/src/__tests__/model-bridge-bridges.test.ts +150 -0
  11. package/src/__tests__/model-bridge-citation.test.ts +163 -0
  12. package/src/__tests__/reference-resolver-cite.test.ts +122 -0
  13. package/src/__tests__/reference-resolver.test.ts +12 -7
  14. package/src/__tests__/resolve-view.test.ts +1 -1
  15. package/src/__tests__/sidebar-nav-highlighting.test.ts +178 -0
  16. package/src/__tests__/source-refs.test.ts +9 -6
  17. package/src/__tests__/test-helpers.ts +20 -0
  18. package/src/__tests__/uri-router.test.ts +39 -12
  19. package/src/adapters/DatasetAdapter.ts +12 -0
  20. package/src/adapters/GraphDataSource.ts +3 -3
  21. package/src/adapters/ReferenceResolver.ts +85 -55
  22. package/src/adapters/UriRouter.ts +82 -10
  23. package/src/adapters/factory.ts +34 -10
  24. package/src/adapters/model-bridge.ts +121 -71
  25. package/src/adapters/types.ts +3 -0
  26. package/src/components/AppSidebar.vue +7 -4
  27. package/src/components/CitationDisplay.vue +86 -30
  28. package/src/components/ConceptDetail.vue +56 -20
  29. package/src/components/LanguageDetail.vue +6 -6
  30. package/src/composables/use-concept-content.ts +8 -8
  31. package/src/composables/use-concept-edges.ts +2 -1
  32. package/src/composables/use-render-options.ts +1 -1
  33. package/src/graph/GraphEngine.ts +3 -3
  34. package/src/stores/vocabulary.ts +2 -2
  35. package/src/style.css +29 -0
  36. package/src/utils/content-renderer.ts +312 -0
  37. package/src/utils/markdown-lite.ts +2 -2
  38. package/src/utils/math.ts +0 -189
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { Citation } from 'glossarist';
3
+ import type { CitationClassification, CiteResolution } from '../adapters/types';
3
4
  import { computed, ref } from 'vue';
4
5
  import { getFactory } from '../adapters/factory';
5
6
  import { useRouter } from 'vue-router';
@@ -14,22 +15,26 @@ const router = useRouter();
14
15
  const store = useVocabularyStore();
15
16
  const factory = getFactory();
16
17
 
17
- function resolveCitation(): { registerId: string; conceptId: string } | null {
18
- const ref = props.citation.ref;
19
- const locality = props.citation.locality;
20
- if (!ref?.source || !locality?.referenceFrom) return null;
18
+ // ── Single source of truth for citation resolution ────────────────────────
19
+ // Both classification and navigation target come from the same resolveCite()
20
+ // call, so they can never disagree.
21
21
 
22
- const resolution = factory.resolveCitation(ref.source, locality.referenceFrom, props.registerId);
23
- if (!resolution || resolution.type !== 'internal') return null;
22
+ const citeResolution = computed<CiteResolution>(() =>
23
+ factory.resolver.resolveCite(props.citation, props.registerId),
24
+ );
24
25
 
25
- return { registerId: resolution.registerId, conceptId: resolution.conceptId };
26
- }
26
+ const classification = computed<CitationClassification>(() =>
27
+ citeResolution.value.classification,
28
+ );
29
+
30
+ const resolvedTarget = computed(() => citeResolution.value.resolved);
27
31
 
28
- const resolvedTarget = computed(() => resolveCitation());
29
32
  const isCrossDataset = computed(() =>
30
33
  resolvedTarget.value != null && resolvedTarget.value.registerId !== props.registerId,
31
34
  );
32
35
 
36
+ // ── Navigation ────────────────────────────────────────────────────────────
37
+
33
38
  async function navigateToCitation() {
34
39
  if (!resolvedTarget.value) return;
35
40
  const { registerId, conceptId } = resolvedTarget.value;
@@ -37,7 +42,37 @@ async function navigateToCitation() {
37
42
  router.push({ name: 'concept', params: { registerId, conceptId } });
38
43
  }
39
44
 
40
- // --- Hover preview ---
45
+ // ── Wrapper element determined by classification ──────────────────────────
46
+ // Internal → button (navigates to concept)
47
+ // Self-contained with link → anchor (external link)
48
+ // Everything else → span (plain text)
49
+
50
+ const sourceTag = computed(() => {
51
+ if (classification.value === 'internal-citation') return 'button';
52
+ if (classification.value === 'self-contained-citation' && props.citation.link) return 'a';
53
+ return 'span';
54
+ });
55
+
56
+ const sourceAttrs = computed(() => {
57
+ if (sourceTag.value === 'button') {
58
+ return { class: 'concept-link font-medium inline-flex items-center gap-0.5' };
59
+ }
60
+ if (sourceTag.value === 'a') {
61
+ return { href: props.citation.link!, target: '_blank', rel: 'noopener', class: 'concept-link font-medium' };
62
+ }
63
+ return { class: 'font-medium' };
64
+ });
65
+
66
+ const sourceEvents = computed(() => {
67
+ if (sourceTag.value !== 'button') return {};
68
+ return {
69
+ onClick: navigateToCitation,
70
+ onMouseenter: schedulePreview,
71
+ };
72
+ });
73
+
74
+ // ── Hover preview ─────────────────────────────────────────────────────────
75
+
41
76
  const triggerEl = ref<HTMLElement | null>(null);
42
77
  const preview = ref<{ designation: string; definition: string } | null>(null);
43
78
  const previewVisible = ref(false);
@@ -90,39 +125,60 @@ function hidePreview() {
90
125
  if (previewTimer) { clearTimeout(previewTimer); previewTimer = null; }
91
126
  previewVisible.value = false;
92
127
  }
128
+
129
+ // ── Locality formatting ───────────────────────────────────────────────────
130
+
131
+ function formatLocality(loc: NonNullable<Citation['locality']>): string {
132
+ const parts: string[] = [];
133
+ const lType = (loc as unknown as Record<string, unknown>).type as string | null;
134
+ const from = (loc as unknown as Record<string, unknown>).referenceFrom ?? (loc as unknown as Record<string, unknown>).reference_from;
135
+ const to = (loc as unknown as Record<string, unknown>).referenceTo ?? (loc as unknown as Record<string, unknown>).reference_to;
136
+ if (lType) parts.push(`, ${lType}`);
137
+ if (from) parts.push(to ? ` ${from}–${to}` : ` ${from}`);
138
+ return parts.join('');
139
+ }
93
140
  </script>
94
141
 
95
142
  <template>
96
143
  <span class="inline" @mouseleave="hidePreview">
97
- <template v-if="citation.ref">
98
- <button
99
- v-if="resolvedTarget"
100
- @click="navigateToCitation"
101
- @mouseenter="schedulePreview"
102
- class="concept-link font-medium inline-flex items-center gap-0.5"
144
+ <!-- Source reference: dynamic wrapper (button/a/span) -->
145
+ <template v-if="citation.ref?.source">
146
+ <component
147
+ :is="sourceTag"
148
+ v-bind="sourceAttrs"
149
+ v-on="sourceEvents"
103
150
  >
104
151
  {{ citation.ref.source }}
105
152
  <span v-if="isCrossDataset" class="text-[10px] opacity-60 leading-none">↗</span>
106
- </button>
107
- <span v-else-if="citation.ref.source" class="font-medium">{{ citation.ref.source }}</span>
153
+ </component>
108
154
  <span v-if="citation.ref.id"> {{ citation.ref.id }}</span>
109
155
  <span v-if="citation.ref.version" class="text-ink-400"> ({{ citation.ref.version }})</span>
110
156
  </template>
157
+
158
+ <!-- Locality: same formatting across all classifications -->
111
159
  <template v-if="citation.locality">
112
- <button v-if="resolvedTarget" @click="navigateToCitation" @mouseenter="schedulePreview" class="concept-link">
113
- <span v-if="citation.locality.type" class="text-ink-400">, {{ citation.locality.type }}</span>
114
- <span v-if="citation.locality.referenceFrom" class="text-ink-400">
115
- {{ citation.locality.referenceTo ? ` ${citation.locality.referenceFrom}–${citation.locality.referenceTo}` : ` ${citation.locality.referenceFrom}` }}
116
- </span>
160
+ <button
161
+ v-if="classification === 'internal-citation'"
162
+ @click="navigateToCitation"
163
+ @mouseenter="schedulePreview"
164
+ class="concept-link"
165
+ >
166
+ {{ formatLocality(citation.locality) }}
117
167
  </button>
118
- <template v-else>
119
- <span v-if="citation.locality.type" class="text-ink-400">, {{ citation.locality.type }}</span>
120
- <span v-if="citation.locality.referenceFrom" class="text-ink-400">
121
- {{ citation.locality.referenceTo ? ` ${citation.locality.referenceFrom}–${citation.locality.referenceTo}` : ` ${citation.locality.referenceFrom}` }}
122
- </span>
123
- </template>
168
+ <span v-else class="text-ink-400">
169
+ {{ formatLocality(citation.locality) }}
170
+ </span>
124
171
  </template>
125
- <a v-if="citation.link" :href="citation.link" target="_blank" rel="noopener" class="concept-link ml-1">[link]</a>
172
+
173
+ <!-- External link badge (only for non-self-contained citations that have a link) -->
174
+ <a
175
+ v-if="citation.link && classification !== 'self-contained-citation'"
176
+ :href="citation.link"
177
+ target="_blank"
178
+ rel="noopener"
179
+ class="concept-link ml-1"
180
+ >[link]</a>
181
+
126
182
  <span v-if="citation.original" class="text-xs text-ink-300 ml-1">(orig: {{ citation.original }})</span>
127
183
  <span v-if="resolvedTarget" class="text-[9px] text-ink-300 ml-1">→ {{ resolvedTarget.registerId }}/{{ resolvedTarget.conceptId }}</span>
128
184
 
@@ -3,8 +3,8 @@ import type { Concept, LocalizedConcept, Designation } from 'glossarist';
3
3
  import type { Manifest, GraphEdge } from '../adapters/types';
4
4
  import { computed, ref, nextTick, watch } from 'vue';
5
5
  import { langName } from '../utils/lang';
6
- import { renderMath } from '../utils/math';
7
- import type { RenderOptions } from '../utils/math';
6
+ import { renderContent } from '../utils/content-renderer';
7
+ import type { RenderOptions } from '../utils/content-renderer';
8
8
  import { escapeAttr } from '../utils/escape';
9
9
  import { entryStatusColor, conceptStatusColor, conceptStatusLabel, conceptStatusDefinition, entryStatusLabel, entryStatusDefinition, getPreferredTerm } from '../utils/concept-helpers';
10
10
  import { sourceTypeInfo, sourceStatusInfo } from '../utils/designation-registry';
@@ -74,6 +74,8 @@ const {
74
74
  navigateRelated,
75
75
  } = useConceptEdges(conceptComputed, registerIdComputed, manifestComputed, edgesComputed, router);
76
76
 
77
+ const hoveredEdgeDisplay = ref<{ designation: string; conceptId: string; tooltip: string } | null>(null);
78
+
77
79
  const uriCopied = ref(false);
78
80
  function copyUri() {
79
81
  const uri = conceptUri(props.concept, props.registerId, props.manifest.uriBase);
@@ -88,7 +90,7 @@ const engConcept = computed((): LocalizedConcept | null => {
88
90
  });
89
91
 
90
92
  const primaryTerm = computed(() => getPreferredTerm(engConcept.value, conceptId.value));
91
- const renderedPrimaryTerm = computed(() => renderMath(primaryTerm.value));
93
+ const renderedPrimaryTerm = computed(() => renderContent(primaryTerm.value));
92
94
 
93
95
  const managedStatus = computed(() => props.concept.status);
94
96
 
@@ -118,7 +120,9 @@ const renderOpts = computed<RenderOptions>(() => ({
118
120
  return escapeAttr(term);
119
121
  },
120
122
  conceptRefResolver: (conceptId, term) => {
121
- return `<a href="#" class="xref-link" data-register="${escapeAttr(props.registerId)}" data-concept="${escapeAttr(conceptId)}">${escapeAttr(term)}</a>`;
123
+ const adapter = factory.getAdapter(props.registerId);
124
+ const resolvedId = adapter?.lookupByDesignation(conceptId) ?? conceptId;
125
+ return `<a href="#" class="xref-link" data-register="${escapeAttr(props.registerId)}" data-concept="${escapeAttr(resolvedId)}">${escapeAttr(term)}</a>`;
122
126
  },
123
127
  bibResolver,
124
128
  figResolver,
@@ -471,42 +475,63 @@ const nonVerbalReps = computed(() => {
471
475
  <!-- Relations -->
472
476
  <div v-if="outgoingEdges.length || incomingEdges.length" class="card p-5">
473
477
  <div class="section-label">{{ t('concept.relations') }}</div>
478
+
479
+ <!-- Outgoing -->
474
480
  <div v-if="outgoingEdges.length" class="mt-3">
475
- <div class="text-xs text-ink-300 mb-2">{{ t('concept.outgoing') }} ({{ outgoingEdges.length }})</div>
476
- <div class="space-y-1 max-h-64 overflow-y-auto">
481
+ <div class="text-xs text-ink-300 mb-1.5">{{ t('concept.outgoing') }} ({{ outgoingEdges.length }})</div>
482
+ <div class="space-y-0.5 max-h-56 overflow-y-auto pr-1 -mr-1">
477
483
  <button
478
484
  v-for="edge in outgoingEdges"
479
485
  :key="edge.target + edge.type"
480
486
  @click="navigateEdge(edge)"
481
- :title="getEdgeDisplay(edge.target).tooltip"
482
- class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
487
+ @mouseenter="hoveredEdgeDisplay = getEdgeDisplay(edge.target)"
488
+ @mouseleave="hoveredEdgeDisplay = null"
489
+ @focus="hoveredEdgeDisplay = getEdgeDisplay(edge.target)"
490
+ class="concept-link block w-full text-left rounded-md px-1.5 py-1 hover:bg-ink-50 transition-colors"
483
491
  :class="getEdgeDisplay(edge.target).isLocal ? '' : 'xref-external'"
484
492
  >
485
- <span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'out')">{{ relationshipLabel(edge.type) }} →</span>
486
- <span class="truncate">{{ getEdgeDisplay(edge.target).designation || edge.label || getEdgeDisplay(edge.target).conceptId }}</span>
487
- <span class="text-[9px] text-ink-300 flex-shrink-0 font-mono">{{ getEdgeDisplay(edge.target).conceptId }}</span>
488
- <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>
493
+ <div class="flex items-center gap-1 mb-0.5">
494
+ <span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'out')">{{ relationshipLabel(edge.type) }} →</span>
495
+ <span v-if="getEdgeDisplay(edge.target).badge" class="badge badge-gray text-[9px] flex-shrink-0 truncate">{{ getEdgeDisplay(edge.target).badge!.title }}</span>
496
+ </div>
497
+ <div class="text-sm text-ink-700 leading-snug line-clamp-2">{{ getEdgeDisplay(edge.target).designation || edge.label || getEdgeDisplay(edge.target).conceptId }}</div>
489
498
  </button>
490
499
  </div>
491
500
  </div>
501
+
502
+ <!-- Incoming -->
492
503
  <div v-if="incomingEdges.length" class="mt-3 pt-3 border-t border-ink-100/60">
493
- <div class="text-xs text-ink-300 mb-2">{{ t('concept.incoming') }} ({{ incomingEdges.length }})</div>
494
- <div class="space-y-1 max-h-48 overflow-y-auto">
504
+ <div class="text-xs text-ink-300 mb-1.5">{{ t('concept.incoming') }} ({{ incomingEdges.length }})</div>
505
+ <div class="space-y-0.5 max-h-40 overflow-y-auto pr-1 -mr-1">
495
506
  <button
496
507
  v-for="edge in incomingEdges"
497
508
  :key="edge.source + edge.type"
498
509
  @click="navigateEdge(edge)"
499
- :title="getEdgeDisplay(edge.source).tooltip"
500
- class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
510
+ @mouseenter="hoveredEdgeDisplay = getEdgeDisplay(edge.source)"
511
+ @mouseleave="hoveredEdgeDisplay = null"
512
+ @focus="hoveredEdgeDisplay = getEdgeDisplay(edge.source)"
513
+ class="concept-link block w-full text-left rounded-md px-1.5 py-1 hover:bg-ink-50 transition-colors"
501
514
  :class="getEdgeDisplay(edge.source).isLocal ? '' : 'xref-external'"
502
515
  >
503
- <span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'in')">← {{ relationshipLabel(inverseEdgeType(edge.type)) }}</span>
504
- <span class="truncate">{{ getEdgeDisplay(edge.source).designation || edge.label || getEdgeDisplay(edge.source).conceptId }}</span>
505
- <span class="text-[9px] text-ink-300 flex-shrink-0 font-mono">{{ getEdgeDisplay(edge.source).conceptId }}</span>
506
- <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>
516
+ <div class="flex items-center gap-1 mb-0.5">
517
+ <span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'in')">← {{ relationshipLabel(inverseEdgeType(edge.type)) }}</span>
518
+ <span v-if="getEdgeDisplay(edge.source).badge" class="badge badge-gray text-[9px] flex-shrink-0 truncate">{{ getEdgeDisplay(edge.source).badge!.title }}</span>
519
+ </div>
520
+ <div class="text-sm text-ink-700 leading-snug line-clamp-2">{{ getEdgeDisplay(edge.source).designation || edge.label || getEdgeDisplay(edge.source).conceptId }}</div>
507
521
  </button>
508
522
  </div>
509
523
  </div>
524
+
525
+ <!-- Hover detail strip (fixed area at bottom of card) -->
526
+ <Transition name="fade">
527
+ <div
528
+ v-if="hoveredEdgeDisplay"
529
+ class="mt-3 pt-3 border-t border-ink-100/60"
530
+ >
531
+ <div class="text-xs text-ink-300 mb-0.5">{{ t('concept.identifier') || 'Identifier' }}</div>
532
+ <div class="font-mono text-[10px] text-ink-500 break-all leading-tight">{{ hoveredEdgeDisplay.conceptId }}</div>
533
+ </div>
534
+ </Transition>
510
535
  </div>
511
536
 
512
537
  <!-- Domains -->
@@ -657,3 +682,14 @@ const nonVerbalReps = computed(() => {
657
682
  </div>
658
683
  </div>
659
684
  </template>
685
+
686
+ <style scoped>
687
+ .fade-enter-active,
688
+ .fade-leave-active {
689
+ transition: opacity 0.15s ease;
690
+ }
691
+ .fade-enter-from,
692
+ .fade-leave-to {
693
+ opacity: 0;
694
+ }
695
+ </style>
@@ -2,8 +2,8 @@
2
2
  import type { Concept, LocalizedConcept, Designation, Expression, Abbreviation as AbbreviationType } from 'glossarist';
3
3
  import { computed } from 'vue';
4
4
  import { langName, langLabel } from '../utils/lang';
5
- import { renderMath } from '../utils/math';
6
- import type { RenderOptions } from '../utils/math';
5
+ import { renderContent } from '../utils/content-renderer';
6
+ import type { RenderOptions } from '../utils/content-renderer';
7
7
  import { escapeAttr } from '../utils/escape';
8
8
  import { entryStatusColor } from '../utils/concept-helpers';
9
9
  import { designationTypeInfo, normativeStatusInfo, grammarBadges, pronunciationLabel, pronunciationTooltip } from '../utils/designation-registry';
@@ -115,7 +115,7 @@ function handleContentClick(e: MouseEvent) {
115
115
  <div class="section-label">{{ t('concept.designations') }}</div>
116
116
  <div class="space-y-2 mt-3">
117
117
  <div v-for="(d, i) in designations" :key="i" class="flex items-center gap-2 flex-wrap">
118
- <span class="font-medium text-ink-800 text-lg" v-html="renderMath(d.designation)"></span>
118
+ <span class="font-medium text-ink-800 text-lg" v-html="renderContent(d.designation)"></span>
119
119
  <span class="badge text-[10px]" :class="designationTypeInfo(d).color">{{ designationTypeInfo(d).label }}</span>
120
120
  <span class="badge text-[10px]" :class="normativeStatusInfo(d.normativeStatus).color">{{ normativeStatusInfo(d.normativeStatus).label }}</span>
121
121
  <template v-if="d.type === 'expression' && (d as Expression).grammarInfo?.length">
@@ -142,7 +142,7 @@ function handleContentClick(e: MouseEvent) {
142
142
  <!-- Definition -->
143
143
  <div v-if="definition" class="card p-5">
144
144
  <div class="section-label">{{ t('concept.definition') }}</div>
145
- <div class="text-ink-800 leading-relaxed mt-3" v-html="renderMath(definition, renderOpts)"></div>
145
+ <div class="text-ink-800 leading-relaxed mt-3" v-html="renderContent(definition, renderOpts)"></div>
146
146
  </div>
147
147
 
148
148
  <!-- Notes -->
@@ -151,7 +151,7 @@ function handleContentClick(e: MouseEvent) {
151
151
  <div class="space-y-3 mt-3">
152
152
  <div v-for="(note, i) in notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
153
153
  <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.note') }} {{ i + 1 }}</span>
154
- <div class="mt-1" v-html="renderMath(note, renderOpts)"></div>
154
+ <div class="mt-1" v-html="renderContent(note, renderOpts)"></div>
155
155
  </div>
156
156
  </div>
157
157
  </div>
@@ -162,7 +162,7 @@ function handleContentClick(e: MouseEvent) {
162
162
  <div class="space-y-3 mt-3">
163
163
  <div v-for="(ex, i) in examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
164
164
  <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ i + 1 }}</span>
165
- <div class="mt-1" v-html="renderMath(ex, renderOpts)"></div>
165
+ <div class="mt-1" v-html="renderContent(ex, renderOpts)"></div>
166
166
  </div>
167
167
  </div>
168
168
  </div>
@@ -1,8 +1,8 @@
1
1
  import { computed, ref, watch, type ComputedRef } from 'vue';
2
2
  import type { Concept, LocalizedConcept, ConceptSource, Designation } from 'glossarist';
3
3
  import type { Manifest } from '../adapters/types';
4
- import type { RenderOptions } from '../utils/math';
5
- import { renderMath, cleanContent } from '../utils/math';
4
+ import type { RenderOptions } from '../utils/content-renderer';
5
+ import { renderContent, cleanContent } from '../utils/content-renderer';
6
6
  import { getAnnotations } from '../adapters/model-bridge';
7
7
  import { getPreferredTerm, entryStatusColor, entryStatusLabel, entryStatusDefinition } from '../utils/concept-helpers';
8
8
  import { sortLanguages } from '../utils/lang';
@@ -68,18 +68,18 @@ export function useConceptContent(
68
68
  result.push({
69
69
  lang,
70
70
  lc,
71
- renderedTerm: renderMath(getPreferredTerm(lc, '')),
71
+ renderedTerm: renderContent(getPreferredTerm(lc, '')),
72
72
  definition,
73
- renderedDefinition: renderMath(definition, opts),
73
+ renderedDefinition: renderContent(definition, opts),
74
74
  annotations,
75
- renderedAnnotations: annotations.map((a: string) => renderMath(a, opts)),
75
+ renderedAnnotations: annotations.map((a: string) => renderContent(a, opts)),
76
76
  notes,
77
- renderedNotes: notes.map(n => renderMath(n, opts)),
77
+ renderedNotes: notes.map(n => renderContent(n, opts)),
78
78
  examples,
79
- renderedExamples: examples.map(e => renderMath(e, opts)),
79
+ renderedExamples: examples.map(e => renderContent(e, opts)),
80
80
  sources: lc.sources,
81
81
  designations: lc.terms,
82
- renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderMath(d.designation)])),
82
+ renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderContent(d.designation)])),
83
83
  entryStatus: lc.entryStatus ?? '',
84
84
  classification: lc.classification,
85
85
  reviewType: lc.reviewType,
@@ -58,12 +58,13 @@ export function useConceptEdges(
58
58
  const designation = node
59
59
  ? (node.designations[locale.value] || node.designations.eng || Object.values(node.designations)[0] || '')
60
60
  : '';
61
- const tooltipLines: string[] = [uri];
61
+ const tooltipLines: string[] = [conceptId];
62
62
  if (node) {
63
63
  for (const [lang, des] of Object.entries(node.designations)) {
64
64
  tooltipLines.push(`${langLabel(lang)}: ${des}`);
65
65
  }
66
66
  }
67
+ tooltipLines.push(uri);
67
68
  let badge: { id: string; title: string } | null = null;
68
69
  if (resolution.type === 'internal' && resolution.registerId !== registerId.value) {
69
70
  const m = store.manifests.get(resolution.registerId);
@@ -1,5 +1,5 @@
1
1
  import { ref, watch } from 'vue';
2
- import type { RenderOptions, BibResolver, FigResolver } from '../utils/math';
2
+ import type { RenderOptions, BibResolver, FigResolver } from '../utils/content-renderer';
3
3
  import { getFactory } from '../adapters/factory';
4
4
  import { escapeAttr } from '../utils/escape';
5
5
 
@@ -1,5 +1,5 @@
1
1
  import type { GraphNode, GraphEdge } from '../adapters/types';
2
- import { ReferenceResolver } from '../adapters/ReferenceResolver';
2
+ import { UriRouter } from '../adapters/UriRouter';
3
3
 
4
4
  function hasDesignations(node: GraphNode): boolean {
5
5
  const d = node.designations;
@@ -33,9 +33,9 @@ export class GraphEngine {
33
33
  if (this.edgeKeys.has(key)) return;
34
34
  this.edgeKeys.add(key);
35
35
 
36
- const parsed = ReferenceResolver.parseUri(edge.target);
36
+ const parsed = UriRouter.parseUri(edge.target);
37
37
  if (!this.nodes.has(edge.source)) {
38
- const sourceParsed = ReferenceResolver.parseUri(edge.source);
38
+ const sourceParsed = UriRouter.parseUri(edge.source);
39
39
  this.nodes.set(edge.source, {
40
40
  uri: edge.source,
41
41
  register: sourceParsed?.registerId ?? edge.register,
@@ -6,7 +6,7 @@ import type { Manifest, SearchHit, GraphEdge } from '../adapters/types';
6
6
  import type { Concept } from 'glossarist';
7
7
  import { conceptUri } from '../adapters/model-bridge';
8
8
  import { GraphEngine } from '../graph';
9
- import { ReferenceResolver } from '../adapters/ReferenceResolver';
9
+ import { UriRouter } from '../adapters/UriRouter';
10
10
  import { deduplicateSearchHits } from '../utils/search';
11
11
 
12
12
  export const useVocabularyStore = defineStore('vocabulary', () => {
@@ -159,7 +159,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
159
159
  if (adapter && loadedEdges.length > 0) {
160
160
  const targetRegisters = new Set<string>();
161
161
  for (const edge of loadedEdges) {
162
- const parsed = ReferenceResolver.parseUri(edge.target);
162
+ const parsed = UriRouter.parseUri(edge.target);
163
163
  if (parsed?.registerId && parsed.registerId !== registerId) {
164
164
  targetRegisters.add(parsed.registerId);
165
165
  }
package/src/style.css CHANGED
@@ -334,6 +334,35 @@
334
334
  color: #dddde6;
335
335
  }
336
336
 
337
+ /* ── Brand colors: lighten for dark backgrounds ── */
338
+ /* ISO red (#e3000f) is too harsh on dark — use a softer coral */
339
+ .dark {
340
+ --brand-primary: #ff5d5d;
341
+ --brand-primary-rgb: 255, 93, 93;
342
+ --brand-dark: #1a1b2e;
343
+ }
344
+
345
+ /* Concept/xref links: lighten in dark mode (override pure red) */
346
+ .dark .concept-link,
347
+ .dark .xref-link {
348
+ color: #ff6b6b;
349
+ }
350
+ .dark .concept-link:hover,
351
+ .dark .xref-link:hover {
352
+ filter: brightness(1.15);
353
+ }
354
+
355
+ /* Brand badges: readable background in dark mode */
356
+ .dark .badge-brand {
357
+ background-color: rgba(255, 93, 93, 0.15);
358
+ color: #ff8a8a;
359
+ }
360
+
361
+ /* Primary buttons: avoid harsh red hover in dark mode */
362
+ .dark .btn-primary:hover {
363
+ background-color: #c41818;
364
+ }
365
+
337
366
  /* ── Surfaces ── */
338
367
  .dark .bg-surface { background-color: #0f1020 !important; }
339
368
  .dark .bg-surface-alt { background-color: #161728 !important; }