@glossarist/concept-browser 0.7.26 → 0.7.28

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.
@@ -23,6 +23,7 @@ import {
23
23
  NonVerbRep,
24
24
  RELATIONSHIP_TYPES,
25
25
  DATE_TYPES,
26
+ ConceptRef,
26
27
  } from 'glossarist';
27
28
  import {
28
29
  LetterSymbol,
@@ -36,6 +37,218 @@ import {
36
37
  } from 'glossarist/models';
37
38
  import type { ConceptSummary } from './types';
38
39
 
40
+ // ── JSON-LD wire-format types ─────────────────────────────────────────────
41
+
42
+ interface JsonLdContent {
43
+ 'gl:content'?: string;
44
+ }
45
+
46
+ interface JsonLdDate {
47
+ 'gl:date'?: string;
48
+ 'gl:dateType'?: string;
49
+ }
50
+
51
+ interface JsonLdPronunciation {
52
+ 'gl:content'?: string;
53
+ 'gl:language'?: string;
54
+ 'gl:script'?: string;
55
+ 'gl:system'?: string;
56
+ 'gl:country'?: string;
57
+ }
58
+
59
+ interface JsonLdGrammarInfo {
60
+ 'gl:gender'?: string;
61
+ 'gl:number'?: string;
62
+ 'gl:partOfSpeech'?: string;
63
+ 'gl:noun'?: boolean;
64
+ 'gl:verb'?: boolean;
65
+ 'gl:adj'?: boolean;
66
+ 'gl:adverb'?: boolean;
67
+ 'gl:preposition'?: boolean;
68
+ 'gl:participle'?: boolean;
69
+ }
70
+
71
+ interface JsonLdRef {
72
+ 'gl:source'?: string;
73
+ 'gl:id'?: string;
74
+ 'gl:version'?: string;
75
+ 'gl:text'?: string;
76
+ 'source'?: string;
77
+ 'id'?: string;
78
+ 'version'?: string;
79
+ }
80
+
81
+ interface JsonLdLocality {
82
+ 'gl:localityType'?: string;
83
+ 'gl:referenceFrom'?: string;
84
+ 'gl:referenceTo'?: string;
85
+ 'type'?: string;
86
+ 'reference_from'?: string;
87
+ 'reference_to'?: string;
88
+ }
89
+
90
+ interface JsonLdOrigin {
91
+ 'gl:ref'?: string | JsonLdRef;
92
+ 'gl:locality'?: JsonLdLocality;
93
+ 'gl:link'?: string;
94
+ 'gl:id'?: string;
95
+ 'gl:version'?: string;
96
+ 'gl:source'?: string;
97
+ }
98
+
99
+ interface JsonLdSource {
100
+ 'gl:sourceType'?: string;
101
+ 'gl:sourceStatus'?: string;
102
+ 'gl:modification'?: string;
103
+ 'gl:origin'?: JsonLdOrigin;
104
+ }
105
+
106
+ interface JsonLdRelated {
107
+ 'gl:relationshipType'?: string;
108
+ 'gl:ref'?: JsonLdRef;
109
+ '@id'?: string;
110
+ 'gl:term'?: string;
111
+ 'gl:target'?: string;
112
+ }
113
+
114
+ interface JsonLdDesignation {
115
+ '@type'?: string;
116
+ 'gl:term'?: string;
117
+ 'gl:normativeStatus'?: string;
118
+ 'gl:absent'?: unknown;
119
+ 'gl:fieldOfApplication'?: string;
120
+ 'gl:usageInfo'?: string;
121
+ 'gl:geographicalArea'?: string;
122
+ 'gl:language'?: string;
123
+ 'gl:script'?: string;
124
+ 'gl:system'?: string;
125
+ 'gl:international'?: boolean;
126
+ 'gl:termType'?: string;
127
+ 'gl:pronunciation'?: JsonLdPronunciation[];
128
+ 'gl:source'?: JsonLdSource[];
129
+ 'gl:related'?: JsonLdRelated[];
130
+ 'gl:prefix'?: string;
131
+ 'gl:gender'?: string;
132
+ 'gl:grammarInfo'?: JsonLdGrammarInfo[];
133
+ }
134
+
135
+ interface JsonLdLocalizedConcept {
136
+ 'gl:languageCode'?: string;
137
+ 'gl:entryStatus'?: string;
138
+ 'gl:classification'?: string;
139
+ 'gl:reviewType'?: string;
140
+ 'gl:domain'?: string;
141
+ 'gl:release'?: string;
142
+ 'gl:lineageSourceSimilarity'?: number;
143
+ 'gl:script'?: string;
144
+ 'gl:system'?: string;
145
+ 'gl:designation'?: JsonLdDesignation[];
146
+ 'gl:definition'?: JsonLdContent[];
147
+ 'gl:notes'?: JsonLdContent[];
148
+ 'gl:annotations'?: JsonLdContent[];
149
+ 'gl:examples'?: JsonLdContent[];
150
+ 'gl:source'?: JsonLdSource[];
151
+ 'gl:dates'?: JsonLdDate[];
152
+ 'gl:references'?: JsonLdRelated[];
153
+ 'gl:reviewDate'?: string;
154
+ 'gl:reviewDecisionDate'?: string;
155
+ 'gl:reviewDecisionEvent'?: string;
156
+ 'gl:reviewStatus'?: string;
157
+ 'gl:reviewDecision'?: string;
158
+ 'gl:reviewDecisionNotes'?: string;
159
+ }
160
+
161
+ interface JsonLdConcept {
162
+ '@type'?: string;
163
+ '@id'?: string;
164
+ 'gl:identifier'?: string | number;
165
+ 'gl:term'?: string;
166
+ 'gl:localizedConcept'?: Record<string, JsonLdLocalizedConcept>;
167
+ 'gl:related'?: JsonLdRelated[];
168
+ 'gl:tags'?: string[];
169
+ }
170
+
171
+ // ── Bridges for fields not yet in glossarist-js ────────────────────────────
172
+ // Remove each bridge when glossarist-js publishes native support.
173
+
174
+ // Annotations: LocalizedConcept.annotations
175
+ const extraAnnotations = new WeakMap<LocalizedConcept, DetailedDefinition[]>();
176
+
177
+ export function getAnnotations(lc: LocalizedConcept): DetailedDefinition[] {
178
+ return extraAnnotations.get(lc) ?? [];
179
+ }
180
+
181
+ // Designation relationship targets: RelatedConcept.target (string)
182
+ const designationTargets = new WeakMap<RelatedConcept, string>();
183
+
184
+ export function getDesignationTarget(rc: RelatedConcept): string | null {
185
+ return designationTargets.get(rc) ?? null;
186
+ }
187
+
188
+ // ConceptRef text: human-readable label alongside source/id
189
+ const refTexts = new WeakMap<ConceptRef, string>();
190
+
191
+ export function getRefText(ref: ConceptRef): string | null {
192
+ return refTexts.get(ref) ?? null;
193
+ }
194
+
195
+ // Relationship types whose target is a designation string, not a concept ref.
196
+ const DESIGNATION_REL_TYPES = new Set(['abbreviated_form_for', 'short_form_for']);
197
+
198
+ function attachAnnotations(concept: Concept, localizations: Record<string, unknown>): void {
199
+ for (const lang of concept.languages) {
200
+ const lc = concept.localization(lang);
201
+ const raw = localizations[lang];
202
+ if (!lc || !raw || typeof raw !== 'object') continue;
203
+ const rawObj = raw as Record<string, unknown>;
204
+
205
+ // Annotations
206
+ const annList = rawObj.annotations;
207
+ if (Array.isArray(annList) && annList.length > 0) {
208
+ extraAnnotations.set(lc, annList.map((a: Record<string, unknown>) =>
209
+ DetailedDefinition.fromJSON({ content: (a.content as string) ?? '' }) as DetailedDefinition,
210
+ ));
211
+ }
212
+
213
+ // Designation-level relationship targets and ref text
214
+ const rawTerms = rawObj.terms;
215
+ if (Array.isArray(rawTerms)) {
216
+ for (let i = 0; i < lc.terms.length && i < rawTerms.length; i++) {
217
+ const rawTerm = rawTerms[i] as Record<string, unknown>;
218
+ const rawRelated = rawTerm.related;
219
+ if (!Array.isArray(rawRelated)) continue;
220
+ const designation = lc.terms[i];
221
+ for (let j = 0; j < designation.related.length && j < rawRelated.length; j++) {
222
+ const rawRel = rawRelated[j] as Record<string, unknown>;
223
+ const rc = designation.related[j];
224
+ if (rawRel.target && typeof rawRel.target === 'string') {
225
+ designationTargets.set(rc, rawRel.target);
226
+ }
227
+ if (rc.ref) {
228
+ const rawRef = rawRel.ref as Record<string, unknown> | undefined;
229
+ if (rawRef?.text && typeof rawRef.text === 'string') {
230
+ refTexts.set(rc.ref, rawRef.text);
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ // Localization-level ref text
238
+ const rawRelated = rawObj.related;
239
+ if (Array.isArray(rawRelated)) {
240
+ for (let i = 0; i < lc.related.length && i < rawRelated.length; i++) {
241
+ const rc = lc.related[i];
242
+ const rawRel = rawRelated[i] as Record<string, unknown>;
243
+ const rawRef = rawRel.ref as Record<string, unknown> | undefined;
244
+ if (rc.ref && rawRef?.text && typeof rawRef.text === 'string') {
245
+ refTexts.set(rc.ref, rawRef.text);
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+
39
252
  // ── Detection ─────────────────────────────────────────────────────────────
40
253
 
41
254
  function isJsonLd(doc: Record<string, unknown>): boolean {
@@ -44,11 +257,10 @@ function isJsonLd(doc: Record<string, unknown>): boolean {
44
257
 
45
258
  // ── JSON-LD → Glossarist native mapping ───────────────────────────────────
46
259
 
47
- function mapDesignationFromJsonLd(d: any): Record<string, unknown> {
260
+ function mapDesignationFromJsonLd(d: JsonLdDesignation): Record<string, unknown> {
48
261
  const result: Record<string, unknown> = {};
49
- const rawType = (d['@type'] as string) || '';
262
+ const rawType = d['@type'] || '';
50
263
 
51
- // Map type
52
264
  if (rawType.includes('Abbreviation')) result.type = 'abbreviation';
53
265
  else if (rawType.includes('LetterSymbol')) result.type = 'letter_symbol';
54
266
  else if (rawType.includes('GraphicalSymbol')) result.type = 'graphical_symbol';
@@ -69,7 +281,7 @@ function mapDesignationFromJsonLd(d: any): Record<string, unknown> {
69
281
  if (d['gl:termType']) result.term_type = d['gl:termType'];
70
282
 
71
283
  if (d['gl:pronunciation']?.length) {
72
- result.pronunciation = d['gl:pronunciation'].map((p: any) => ({
284
+ result.pronunciation = d['gl:pronunciation'].map(p => ({
73
285
  content: p['gl:content'] ?? null,
74
286
  language: p['gl:language'] ?? null,
75
287
  script: p['gl:script'] ?? null,
@@ -83,16 +295,21 @@ function mapDesignationFromJsonLd(d: any): Record<string, unknown> {
83
295
  }
84
296
 
85
297
  if (d['gl:related']?.length) {
86
- result.related = d['gl:related'].map(mapRelatedFromJsonLd);
298
+ result.related = d['gl:related'].map(r => {
299
+ const relType = r['gl:relationshipType'] ?? 'references';
300
+ if (DESIGNATION_REL_TYPES.has(relType) && r['gl:target']) {
301
+ return { type: relType, target: r['gl:target'] };
302
+ }
303
+ return mapRelatedFromJsonLd(r);
304
+ });
87
305
  }
88
306
 
89
- // Expression-specific
90
307
  if (d['gl:prefix'] != null) result.prefix = d['gl:prefix'];
91
308
  if (d['gl:gender']) {
92
309
  result.grammar_info = [{ gender: d['gl:gender'] }];
93
310
  }
94
311
  if (d['gl:grammarInfo']?.length) {
95
- result.grammar_info = d['gl:grammarInfo'].map((gi: any) => ({
312
+ result.grammar_info = d['gl:grammarInfo'].map(gi => ({
96
313
  gender: gi['gl:gender'] ?? null,
97
314
  number: gi['gl:number'] ?? null,
98
315
  part_of_speech: gi['gl:partOfSpeech'] ?? null,
@@ -108,7 +325,7 @@ function mapDesignationFromJsonLd(d: any): Record<string, unknown> {
108
325
  return result;
109
326
  }
110
327
 
111
- function mapSourceFromJsonLd(s: any): Record<string, unknown> {
328
+ function mapSourceFromJsonLd(s: JsonLdSource): Record<string, unknown> {
112
329
  const result: Record<string, unknown> = {};
113
330
  if (s['gl:sourceType']) result.type = s['gl:sourceType'];
114
331
  if (s['gl:sourceStatus']) result.status = s['gl:sourceStatus'];
@@ -120,7 +337,6 @@ function mapSourceFromJsonLd(s: any): Record<string, unknown> {
120
337
  if (o['gl:ref']) {
121
338
  const rawRef = o['gl:ref'];
122
339
  if (typeof rawRef === 'string') {
123
- // Legacy format: gl:ref is a plain string (e.g. "ISO/TS 14812:2022")
124
340
  origin.ref = { source: rawRef };
125
341
  } else {
126
342
  const refObj: Record<string, unknown> = {};
@@ -154,7 +370,7 @@ function mapSourceFromJsonLd(s: any): Record<string, unknown> {
154
370
  return result;
155
371
  }
156
372
 
157
- function mapRelatedFromJsonLd(r: any): Record<string, unknown> {
373
+ function mapRelatedFromJsonLd(r: JsonLdRelated): Record<string, unknown> {
158
374
  const result: Record<string, unknown> = { type: 'references' };
159
375
 
160
376
  if (r['gl:relationshipType']) {
@@ -168,11 +384,12 @@ function mapRelatedFromJsonLd(r: any): Record<string, unknown> {
168
384
  if (ref['gl:id']) refObj.id = ref['gl:id'];
169
385
  if (ref['source']) refObj.source = ref['source'];
170
386
  if (ref['id']) refObj.id = ref['id'];
387
+ if (ref['gl:text']) refObj.text = ref['gl:text'];
171
388
  if (Object.keys(refObj).length > 0) result.ref = refObj;
172
389
  }
173
390
 
174
391
  if (!result.ref && r['@id']) {
175
- const uri = r['@id'] as string;
392
+ const uri = r['@id'];
176
393
  const idMatch = uri.match(/\/concept\/([^/]+)$/);
177
394
  result.ref = idMatch
178
395
  ? { source: uri.split('/').slice(-3, -2)[0] || '', id: idMatch[1] }
@@ -182,7 +399,7 @@ function mapRelatedFromJsonLd(r: any): Record<string, unknown> {
182
399
  return result;
183
400
  }
184
401
 
185
- function mapLocalizedFromJsonLd(lc: any): Record<string, unknown> {
402
+ function mapLocalizedFromJsonLd(lc: JsonLdLocalizedConcept): Record<string, unknown> {
186
403
  const data: Record<string, unknown> = {};
187
404
 
188
405
  if (lc['gl:languageCode']) data.language_code = lc['gl:languageCode'];
@@ -200,18 +417,21 @@ function mapLocalizedFromJsonLd(lc: any): Record<string, unknown> {
200
417
  }
201
418
 
202
419
  if (lc['gl:definition']?.length) {
203
- data.definition = lc['gl:definition'].map((d: any) => {
204
- const def: Record<string, unknown> = { content: d['gl:content'] ?? '' };
205
- return def;
206
- });
420
+ data.definition = lc['gl:definition'].map(d => ({
421
+ content: d['gl:content'] ?? '',
422
+ }));
207
423
  }
208
424
 
209
425
  if (lc['gl:notes']?.length) {
210
- data.notes = lc['gl:notes'].map((n: any) => ({ content: n['gl:content'] ?? '' }));
426
+ data.notes = lc['gl:notes'].map(n => ({ content: n['gl:content'] ?? '' }));
427
+ }
428
+
429
+ if (lc['gl:annotations']?.length) {
430
+ data.annotations = lc['gl:annotations'].map(a => ({ content: a['gl:content'] ?? '' }));
211
431
  }
212
432
 
213
433
  if (lc['gl:examples']?.length) {
214
- data.examples = lc['gl:examples'].map((e: any) => ({ content: e['gl:content'] ?? '' }));
434
+ data.examples = lc['gl:examples'].map(e => ({ content: e['gl:content'] ?? '' }));
215
435
  }
216
436
 
217
437
  if (lc['gl:source']?.length) {
@@ -219,7 +439,7 @@ function mapLocalizedFromJsonLd(lc: any): Record<string, unknown> {
219
439
  }
220
440
 
221
441
  if (lc['gl:dates']?.length) {
222
- data.dates = lc['gl:dates'].map((d: any) => ({
442
+ data.dates = lc['gl:dates'].map(d => ({
223
443
  date: d['gl:date'] ?? null,
224
444
  type: d['gl:dateType'] ?? null,
225
445
  }));
@@ -229,7 +449,6 @@ function mapLocalizedFromJsonLd(lc: any): Record<string, unknown> {
229
449
  data.related = lc['gl:references'].map(mapRelatedFromJsonLd);
230
450
  }
231
451
 
232
- // Review metadata — passed through to LocalizedConcept constructor
233
452
  if (lc['gl:reviewDate']) data.review_date = lc['gl:reviewDate'];
234
453
  if (lc['gl:reviewDecisionDate']) data.review_decision_date = lc['gl:reviewDecisionDate'];
235
454
  if (lc['gl:reviewDecisionEvent']) data.review_decision_event = lc['gl:reviewDecisionEvent'];
@@ -240,9 +459,9 @@ function mapLocalizedFromJsonLd(lc: any): Record<string, unknown> {
240
459
  return data;
241
460
  }
242
461
 
243
- function conceptFromJsonLd(doc: Record<string, any>): Concept {
462
+ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
244
463
  const id = String(doc['gl:identifier'] ?? doc['@id']?.split('/').pop() ?? '');
245
- const localizations: Record<string, any> = {};
464
+ const localizations: Record<string, unknown> = {};
246
465
 
247
466
  const rawLc = doc['gl:localizedConcept'] ?? {};
248
467
  for (const [lang, lc] of Object.entries(rawLc)) {
@@ -254,7 +473,7 @@ function conceptFromJsonLd(doc: Record<string, any>): Concept {
254
473
  const related = (doc['gl:related'] ?? []).map(mapRelatedFromJsonLd);
255
474
  const tags = Array.isArray(doc['gl:tags']) ? [...doc['gl:tags']] : [];
256
475
 
257
- return Concept.fromJSON({
476
+ const concept = Concept.fromJSON({
258
477
  id,
259
478
  term: doc['gl:term'] ?? null,
260
479
  uri: doc['@id'] ?? null,
@@ -263,16 +482,21 @@ function conceptFromJsonLd(doc: Record<string, any>): Concept {
263
482
  tags,
264
483
  status: null,
265
484
  });
485
+
486
+ attachAnnotations(concept, localizations);
487
+ return concept;
266
488
  }
267
489
 
268
490
  // ── Public API ────────────────────────────────────────────────────────────
269
491
 
270
- export function conceptFromJson(doc: Record<string, any>): Concept {
492
+ export function conceptFromJson(doc: Record<string, unknown>): Concept {
271
493
  if (isJsonLd(doc)) {
272
- return conceptFromJsonLd(doc);
494
+ return conceptFromJsonLd(doc as JsonLdConcept);
273
495
  }
274
- // glossarist native format — use fromJSON directly
275
- return Concept.fromJSON(doc as Record<string, unknown>);
496
+ const concept = Concept.fromJSON(doc);
497
+ const locs = (doc as Record<string, unknown>).localizations as Record<string, unknown> | undefined;
498
+ if (locs) attachAnnotations(concept, locs);
499
+ return concept;
276
500
  }
277
501
 
278
502
  export function conceptToSummary(concept: Concept): ConceptSummary {
@@ -14,12 +14,20 @@ export interface TaxonomyConcept {
14
14
  altLabel?: string;
15
15
  definition?: string;
16
16
  broader?: string;
17
+ category?: string;
18
+ inverseOf?: string;
19
+ }
20
+
21
+ export interface TaxonomyCategory {
22
+ label: string;
23
+ color: string;
17
24
  }
18
25
 
19
26
  export interface Taxonomy {
20
27
  scheme: string;
21
28
  schemeLabel: string | null;
22
29
  schemeDefinition: string | null;
30
+ categories?: Record<string, TaxonomyCategory>;
23
31
  concepts: Record<string, TaxonomyConcept>;
24
32
  }
25
33
 
@@ -61,15 +69,35 @@ export class OntologyRegistry {
61
69
  return id in (this.data[taxonomy]?.concepts ?? {});
62
70
  }
63
71
 
64
- /** Get broader concept ID, if any (for hierarchical taxonomies like designation-type). */
65
72
  getBroader(taxonomy: TaxonomyKey, id: string): string | null {
66
73
  return this.getConcept(taxonomy, id)?.broader ?? null;
67
74
  }
68
75
 
69
- /** Get all child concept IDs of a given concept. */
70
76
  getNarrower(taxonomy: TaxonomyKey, id: string): TaxonomyConcept[] {
71
77
  return this.getAll(taxonomy).filter(c => c.broader === id);
72
78
  }
79
+
80
+ getCategory(taxonomy: TaxonomyKey, id: string): string | null {
81
+ return this.getConcept(taxonomy, id)?.category ?? null;
82
+ }
83
+
84
+ getInverse(taxonomy: TaxonomyKey, id: string): string | null {
85
+ return this.getConcept(taxonomy, id)?.inverseOf ?? null;
86
+ }
87
+
88
+ getCategoryConfig(taxonomy: TaxonomyKey, categoryId: string): TaxonomyCategory | null {
89
+ return this.data[taxonomy]?.categories?.[categoryId] ?? null;
90
+ }
91
+
92
+ getCategories(taxonomy: TaxonomyKey): Record<string, TaxonomyCategory> {
93
+ return this.data[taxonomy]?.categories ?? {};
94
+ }
95
+
96
+ getColor(taxonomy: TaxonomyKey, id: string): string | null {
97
+ const entry = this.data[taxonomy] as unknown as Record<string, unknown>;
98
+ const colors = entry?.colors as Record<string, string> | undefined;
99
+ return colors?.[id] ?? null;
100
+ }
73
101
  }
74
102
 
75
103
  export const ontology = new OntologyRegistry();
@@ -252,7 +252,7 @@ const activeSectionId = computed(() => {
252
252
  <button
253
253
  v-if="group.label"
254
254
  @click="toggleGroup(group.id)"
255
- class="w-full flex items-start gap-1.5 px-2 py-1.5 rounded-lg text-xs font-semibold transition-colors hover:bg-ink-50"
255
+ class="sidebar-group-label w-full flex items-start gap-1.5 px-2 py-1.5 rounded-lg text-xs font-semibold transition-colors hover:bg-ink-50"
256
256
  :style="group.color ? { color: group.color } : {}"
257
257
  >
258
258
  <span class="w-3 text-[10px] mt-0.5 flex-shrink-0">{{ isGroupExpanded(group.id) ? '▾' : '▸' }}</span>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { Concept, LocalizedConcept, Designation, Expression, ConceptSource } from 'glossarist';
2
+ import type { Concept, LocalizedConcept, Designation, ConceptSource } from 'glossarist';
3
3
  import type { Manifest, GraphEdge } from '../adapters/types';
4
4
  import { computed, ref, nextTick, watch } from 'vue';
5
5
  import { langName, langLabel, sortLanguages } from '../utils/lang';
@@ -7,8 +7,8 @@ import { renderMath, cleanContent } from '../utils/math';
7
7
  import type { RenderOptions } from '../utils/math';
8
8
  import { escapeAttr } from '../utils/escape';
9
9
  import { entryStatusColor, conceptStatusColor, conceptStatusLabel, conceptStatusDefinition, entryStatusLabel, entryStatusDefinition, getPreferredTerm } from '../utils/concept-helpers';
10
- import { designationTypeInfo, normativeStatusInfo, grammarBadges, pronunciationLabel, pronunciationTooltip, abbreviationDetails, sourceTypeInfo, sourceStatusInfo, termTypeInfo } from '../utils/designation-registry';
11
- import { conceptUri } from '../adapters/model-bridge';
10
+ import { sourceTypeInfo, sourceStatusInfo } from '../utils/designation-registry';
11
+ import { conceptUri, getAnnotations } from '../adapters/model-bridge';
12
12
  import { useRouter } from 'vue-router';
13
13
  import { useVocabularyStore } from '../stores/vocabulary';
14
14
  import { useDsStyle } from '../utils/dataset-style';
@@ -21,6 +21,7 @@ import ConceptRdfView from './ConceptRdfView.vue';
21
21
  import FormatDownloads from './FormatDownloads.vue';
22
22
  import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
23
23
  import CitationDisplay from './CitationDisplay.vue';
24
+ import DesignationList from './DesignationList.vue';
24
25
  import { useI18n } from '../i18n';
25
26
 
26
27
  const { t, locale } = useI18n();
@@ -143,7 +144,7 @@ async function navigateRelated(ref: { source: string | null; id: string | null }
143
144
  router.push({ name: 'concept', params: { registerId: target.registerId, conceptId: target.conceptId } });
144
145
  }
145
146
 
146
- function relatedLabel(dr: { content?: string; ref?: { source: string | null; id: string | null } | null }): string {
147
+ function relatedLabel(dr: { content?: string | null; ref?: { source: string | null; id: string | null } | null }): string {
147
148
  if (dr.content) return dr.content;
148
149
  const resolved = dr.ref ? getResolvedRef(dr.ref).target : null;
149
150
  if (resolved) {
@@ -200,6 +201,8 @@ interface LangContent {
200
201
  renderedTerm: string;
201
202
  definition: string;
202
203
  renderedDefinition: string;
204
+ annotations: string[];
205
+ renderedAnnotations: string[];
203
206
  notes: string[];
204
207
  renderedNotes: string[];
205
208
  examples: string[];
@@ -224,6 +227,7 @@ const allLangContent = computed(() => {
224
227
 
225
228
  const definition = lc.definitions
226
229
  .map(d => d.content).filter(Boolean).join('\n\n');
230
+ const annotations = getAnnotations(lc).map(a => a.content).filter(Boolean);
227
231
  const notes = lc.notes.map(n => n.content).filter(Boolean);
228
232
  const examples = lc.examples.map(e => e.content).filter(Boolean);
229
233
 
@@ -233,6 +237,8 @@ const allLangContent = computed(() => {
233
237
  renderedTerm: renderMath(getPreferredTerm(lc, '')),
234
238
  definition,
235
239
  renderedDefinition: renderMath(definition, renderOpts),
240
+ annotations,
241
+ renderedAnnotations: annotations.map((a: string) => renderMath(a, renderOpts)),
236
242
  notes,
237
243
  renderedNotes: notes.map(n => renderMath(n, renderOpts)),
238
244
  examples,
@@ -259,7 +265,7 @@ const langContentMap = computed(() => {
259
265
  });
260
266
 
261
267
  function hasContent(lc: LangContent): boolean {
262
- return !!(lc.definition || lc.notes.length || lc.examples.length || lc.sources.length);
268
+ return !!(lc.definition || lc.annotations.length || lc.notes.length || lc.examples.length || lc.sources.length);
263
269
  }
264
270
 
265
271
  function initCollapsed() {
@@ -364,7 +370,7 @@ const edgeDisplayCache = computed(() => {
364
370
  });
365
371
 
366
372
  function getEdgeDisplay(uri: string): EdgeDisplay {
367
- return edgeDisplayCache.value.get(uri) ?? { uri, conceptId: uri, tooltip: uri, isLocal: false, badge: null };
373
+ return edgeDisplayCache.value.get(uri) ?? { uri, conceptId: uri, designation: "", tooltip: uri, isLocal: false, badge: null };
368
374
  }
369
375
 
370
376
  interface ResolvedRef {
@@ -402,7 +408,7 @@ async function navigateEdge(edge: GraphEdge) {
402
408
 
403
409
  function navigateDomain(domain: { slug: string; conceptId?: string }) {
404
410
  const sectionId = domain.conceptId || domain.slug;
405
- router.push({ name: 'dataset', params: { registerId: manifest.id }, query: { section: sectionId } });
411
+ router.push({ name: 'dataset', params: { registerId: props.manifest.id }, query: { section: sectionId } });
406
412
  }
407
413
 
408
414
  function getTermForLang(lang: string): string {
@@ -623,76 +629,38 @@ const nonVerbalReps = computed(() => {
623
629
  <div v-if="hasContent(lc) && collapsedLangs.has(lc.lang)" class="px-3 sm:px-4 pb-3 -mt-0.5">
624
630
  <p v-if="lc.definition" class="text-xs text-ink-300 leading-relaxed pl-[22px]">{{ plainTruncate(lc.definition) }}</p>
625
631
  <p v-else class="text-xs text-ink-200 leading-relaxed pl-[22px]">
632
+ <template v-if="lc.annotations.length">{{ lc.annotations.length }} annotation{{ lc.annotations.length > 1 ? 's' : '' }}</template>
633
+ <template v-if="lc.annotations.length && lc.notes.length"> &middot; </template>
626
634
  <template v-if="lc.notes.length">{{ lc.notes.length }} note{{ lc.notes.length > 1 ? 's' : '' }}</template>
627
- <template v-if="lc.notes.length && lc.examples.length"> &middot; </template>
635
+ <template v-if="(lc.annotations.length || lc.notes.length) && lc.examples.length"> &middot; </template>
628
636
  <template v-if="lc.examples.length">{{ lc.examples.length }} example{{ lc.examples.length > 1 ? 's' : '' }}</template>
629
637
  </p>
630
638
  </div>
631
639
 
632
640
  <!-- Expandable content -->
633
641
  <div v-if="hasContent(lc)" v-show="!collapsedLangs.has(lc.lang)" class="lang-content px-3 sm:px-4 pb-4 space-y-3">
634
- <!-- Designations (show all, with full metadata) -->
635
- <div v-if="lc.designations.length > 0" class="space-y-1.5 pl-[22px]">
636
- <div v-for="(d, i) in orderedDesignations(lc.lang)" :key="i">
637
- <div class="flex items-center gap-1.5 text-sm flex-wrap">
638
- <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>
639
- <span class="badge text-[10px] flex-shrink-0" :class="designationTypeInfo(d).color" :title="designationTypeInfo(d).definition ?? ''">{{ designationTypeInfo(d).label }}</span>
640
- <span class="badge text-[10px] flex-shrink-0" :class="normativeStatusInfo(d.normativeStatus).color" :title="normativeStatusInfo(d.normativeStatus).definition ?? ''">{{ normativeStatusInfo(d.normativeStatus).label }}</span>
641
- <!-- Abbreviation details -->
642
- <template v-if="abbreviationDetails(d).length">
643
- <span v-for="abbr in abbreviationDetails(d)" :key="abbr" class="badge text-[10px] bg-amber-50 text-amber-600">{{ abbr }}</span>
644
- </template>
645
- <!-- Term type (ISO 12620) -->
646
- <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>
647
- <!-- Grammar info -->
648
- <template v-if="d.type === 'expression' && (d as Expression).grammarInfo?.length">
649
- <template v-for="(gi, giIdx) in (d as Expression).grammarInfo" :key="giIdx">
650
- <span v-for="badge in grammarBadges(gi)" :key="giIdx + '-' + badge.label"
651
- class="badge text-[10px] bg-gray-50 text-gray-600" :title="badge.definition ?? ''">{{ badge.label }}</span>
652
- </template>
653
- </template>
654
- <!-- Pronunciation -->
655
- <template v-if="d.pronunciations?.length">
656
- <span v-for="(p, pi) in d.pronunciations" :key="'p'+pi"
657
- class="text-xs text-ink-400 font-mono" :title="pronunciationTooltip(p)">{{ pronunciationLabel(p) }}</span>
658
- </template>
659
- <!-- Flags -->
660
- <span v-if="d.international" class="badge text-[10px] bg-sky-50 text-sky-600">international</span>
661
- <span v-if="d.absent" class="badge text-[10px] bg-red-50 text-red-600">absent</span>
662
- <span v-if="d.geographicalArea" class="badge text-[10px] bg-gray-50 text-gray-600">{{ d.geographicalArea }}</span>
663
- <span v-if="d.usageInfo" class="text-xs text-ink-300">{{ d.usageInfo }}</span>
664
- <span v-if="d.fieldOfApplication" class="text-xs text-ink-300">field: {{ d.fieldOfApplication }}</span>
665
- <!-- Per-designation language/script/system overrides -->
666
- <template v-if="d.language && d.language !== lc.lang">
667
- <span class="badge text-[10px] bg-teal-50 text-teal-600">lang: {{ langName(d.language) }}</span>
668
- </template>
669
- <span v-if="d.script" class="badge text-[10px] bg-gray-50 text-gray-600">script: {{ d.script }}</span>
670
- <span v-if="d.system" class="badge text-[10px] bg-gray-50 text-gray-600">system: {{ d.system }}</span>
671
- </div>
672
- <!-- Designation sources -->
673
- <div v-if="d.sources?.length" class="mt-1 space-y-0.5">
674
- <div v-for="(ds, dsi) in d.sources" :key="'ds'+dsi" class="text-xs text-ink-400 flex items-center gap-1.5">
675
- <span v-if="ds.type" class="badge text-[9px]" :class="sourceTypeInfo(ds.type).color">{{ sourceTypeInfo(ds.type).label }}</span>
676
- <CitationDisplay v-if="ds.origin" :citation="ds.origin" :register-id="registerId" />
677
- <span v-else-if="ds.modification" class="text-ink-300">{{ ds.modification }}</span>
678
- </div>
679
- </div>
680
- <!-- Designation relationships -->
681
- <div v-if="d.related?.length" class="mt-0.5 space-y-0.5">
682
- <div v-for="(dr, dri) in d.related" :key="'dr'+dri" class="text-xs text-ink-400 flex items-center gap-1.5">
683
- <span class="badge text-[9px] bg-gray-50 text-gray-600">{{ relationshipLabel(dr.type) }}</span>
684
- <button v-if="getResolvedRef(dr.ref).target" @click="navigateRelated(dr.ref!)" class="concept-link">{{ relatedLabel(dr) }}</button>
685
- <span v-else>{{ relatedLabel(dr) }}</span>
686
- </div>
687
- </div>
688
- </div>
689
- </div>
642
+ <!-- Designations -->
643
+ <DesignationList
644
+ :designations="orderedDesignations(lc.lang)"
645
+ :rendered-designations="lc.renderedDesignations"
646
+ :lang="lc.lang"
647
+ :register-id="registerId"
648
+ @navigate-related="(ref) => navigateRelated(ref)"
649
+ />
690
650
 
691
651
  <!-- Definition -->
692
652
  <div v-if="lc.definition" class="p-4 rounded-lg bg-surface border-l-2" :style="{ borderLeftColor: getColor(manifest.id) }">
693
653
  <div class="text-ink-800 leading-relaxed" v-html="lc.renderedDefinition"></div>
694
654
  </div>
695
655
 
656
+ <!-- Annotations -->
657
+ <div v-if="lc.annotations.length" class="space-y-2">
658
+ <div v-for="(_, i) in lc.annotations" :key="'ann-'+i" class="text-ink-500 text-sm leading-relaxed italic pl-3 border-l-2 border-ink-200">
659
+ <span class="font-medium text-ink-400 text-xs uppercase tracking-wide not-italic">{{ t('concept.annotation') }}</span>
660
+ <div class="mt-1 not-italic" v-html="lc.renderedAnnotations[i]"></div>
661
+ </div>
662
+ </div>
663
+
696
664
  <!-- Notes -->
697
665
  <div v-if="lc.notes.length" class="space-y-2">
698
666
  <div v-for="(_, i) in lc.notes" :key="i" class="text-ink-600 text-sm leading-relaxed">