@glossarist/concept-browser 0.7.91 → 0.7.93

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.
@@ -10,7 +10,7 @@ import type { ConceptSummary } from '../types';
10
10
  import { ConceptIdentity } from '../concept-identity';
11
11
  import { GL } from '../wire-keys';
12
12
  import { isJsonLd, mapLocalizedFromJsonLd, mapRelatedFromJsonLd } from './mappers';
13
- import { mapPartitiveRelationFromJsonLd } from './partitive';
13
+ import { mapHyperedgeFromJsonLd } from './hyperedge';
14
14
  import { normalizeEntityRefs } from './entity-refs';
15
15
  import { attachBridges, attachSourcedFromToSources } from './bridges';
16
16
  import type { JsonLdConcept } from './jsonld-types';
@@ -27,9 +27,18 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
27
27
  }
28
28
 
29
29
  const related = (doc[GL.RELATED] ?? []).map(mapRelatedFromJsonLd);
30
+
31
+ // Unified hyperedge mapping: both partitive and generic relations
32
+ // share the same JSON-LD wire shape. glossarist-js dispatches by
33
+ // wire key (`partitive_relations` / `generic_relations`).
30
34
  const partitiveRelations = (doc[GL.PARTITIVE_RELATIONS] ?? [])
31
- .map(mapPartitiveRelationFromJsonLd)
35
+ .map((r: any) => mapHyperedgeFromJsonLd(r, 'gl:hasPartitive'))
36
+ .filter((r): r is Record<string, unknown> => r !== null);
37
+
38
+ const genericRelations = (doc[GL.GENERIC_RELATIONS] ?? [])
39
+ .map((r: any) => mapHyperedgeFromJsonLd(r, 'gl:hasGeneric'))
32
40
  .filter((r): r is Record<string, unknown> => r !== null);
41
+
33
42
  const tagsArr = doc[GL.TAGS];
34
43
  const tags = Array.isArray(tagsArr) ? [...tagsArr] : [];
35
44
 
@@ -40,6 +49,7 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
40
49
  localizations,
41
50
  related,
42
51
  partitive_relations: partitiveRelations,
52
+ generic_relations: genericRelations,
43
53
  tags,
44
54
  figures: normalizeEntityRefs(doc[GL.FIGURE_REF]),
45
55
  tables: normalizeEntityRefs(doc[GL.TABLE_REF]),
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Hyperedge (n-ary relation) mapping: JSON-LD → glossarist-js model.
3
+ *
4
+ * Handles ISO 704:2022 n-ary relations — both PartitiveHyperedge and
5
+ * GenericHyperedge share the same JSON-LD wire shape. The mapper is
6
+ * identical for both; the glossarist-js Concept constructor dispatches
7
+ * by wire key (`partitive_relations` vs `generic_relations`).
8
+ *
9
+ * MECE migration: reads presence × count directly (ISO 704:2022 v3).
10
+ * Falls back to migrating the legacy 5-value `multiplicity` or v2
11
+ * `certainty` field.
12
+ */
13
+ import { GL } from '../wire-keys';
14
+ import { isPartitivePresence, isPartitiveCount } from '../../utils/partitive-multiplicity';
15
+ import { mapRefFromJsonLd } from './mappers';
16
+ import type { JsonLdPartitiveRelation } from './jsonld-types';
17
+
18
+ /**
19
+ * Map a JSON-LD relation entry to the raw dict shape that
20
+ * glossarist-js's AbstractHyperedge subclasses accept.
21
+ *
22
+ * Works for both PartitiveHyperedge and GenericHyperedge — the only
23
+ * difference at the wire level is which wire key the parent dict
24
+ * uses (`partitive_relations` vs `generic_relations`).
25
+ *
26
+ * Reads `gl:hasPartitive`, `gl:hasGeneric`, or `gl:members` for
27
+ * the member array (in order of preference).
28
+ */
29
+ export function mapHyperedgeFromJsonLd(
30
+ r: JsonLdPartitiveRelation,
31
+ memberKey: 'gl:hasPartitive' | 'gl:hasGeneric' | 'gl:members' = 'gl:hasPartitive',
32
+ ): Record<string, unknown> | null {
33
+ const comprehensive = r[GL.COMPREHENSIVE] ? mapRefFromJsonLd(r[GL.COMPREHENSIVE]) : null;
34
+ if (!comprehensive) return null;
35
+
36
+ const rawMembers = r[GL.HAS_PARTITIVE] ?? r[GL.HAS_GENERIC] ?? r[GL.HAS_MEMBER] ?? r[memberKey] ?? [];
37
+ const partitives = (rawMembers as any[])
38
+ .map((m): Record<string, unknown> | null => {
39
+ const ref = m[GL.REF] ? mapRefFromJsonLd(m[GL.REF]) : null;
40
+ if (!ref) return null;
41
+ const out: Record<string, unknown> = { ref };
42
+ const presence = m[GL.PRESENCE];
43
+ const count = m[GL.COUNT];
44
+ if (isPartitivePresence(presence) && isPartitiveCount(count)) {
45
+ out.presence = presence;
46
+ out.count = count;
47
+ } else {
48
+ const raw = m[GL.MULTIPLICITY] ?? splitLegacyCertainty(m[GL.CERTAINTY]);
49
+ if (raw) {
50
+ const parts = splitMultiplicity(raw);
51
+ out.presence = parts.presence;
52
+ out.count = parts.count;
53
+ }
54
+ }
55
+ if (m[GL.IS_DELIMITING] === true) out.is_delimiting = true;
56
+ // ISO 704:2022 §5.5.4.2.1 — GenericMember carries a required
57
+ // delimitingCharacteristic (the intension difference between
58
+ // this specific concept and its coordinate concepts).
59
+ const dc = m[GL.DELIMITING_CHARACTERISTIC];
60
+ if (dc) {
61
+ out.delimitingCharacteristic = typeof dc === 'string'
62
+ ? { default: dc }
63
+ : dc;
64
+ }
65
+ return out;
66
+ })
67
+ .filter((m): m is Record<string, unknown> => m !== null);
68
+
69
+ if (partitives.length < 2) return null;
70
+
71
+ const out: Record<string, unknown> = {
72
+ comprehensive,
73
+ members: partitives,
74
+ };
75
+
76
+ if (r[GL.COMPLETENESS]) out.completeness = r[GL.COMPLETENESS];
77
+
78
+ if (r[GL.CRITERION]) {
79
+ out.criterion = typeof r[GL.CRITERION] === 'string'
80
+ ? { default: r[GL.CRITERION] }
81
+ : r[GL.CRITERION];
82
+ }
83
+
84
+ return out;
85
+ }
86
+
87
+ /** Migrate legacy v2 certainty → old v3 multiplicity string. */
88
+ function splitLegacyCertainty(certainty: string | undefined): string | null {
89
+ if (!certainty) return null;
90
+ return certainty === 'possible' ? 'optional' : 'compulsory';
91
+ }
92
+
93
+ /** Split the legacy one-string multiplicity into MECE presence × count. */
94
+ function splitMultiplicity(m: string): { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' } {
95
+ const LEGACY_MAP: Record<string, { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' }> = {
96
+ compulsory: { presence: 'required', count: 'exactly_one' },
97
+ optional: { presence: 'optional', count: 'exactly_one' },
98
+ compulsory_multiple: { presence: 'required', count: 'multiple' },
99
+ optional_multiple: { presence: 'optional', count: 'multiple' },
100
+ compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
101
+ };
102
+ return LEGACY_MAP[m] ?? { presence: 'required', count: 'exactly_one' };
103
+ }
@@ -150,6 +150,8 @@ export interface JsonLdPartitiveMember {
150
150
  export interface JsonLdPartitiveRelation {
151
151
  'gl:comprehensive'?: JsonLdRef;
152
152
  'gl:hasPartitive'?: JsonLdPartitiveMember[];
153
+ 'gl:hasGeneric'?: JsonLdPartitiveMember[];
154
+ 'gl:members'?: JsonLdPartitiveMember[];
153
155
  'gl:completeness'?: string;
154
156
  'gl:criterion'?: Record<string, string> | string;
155
157
  }
@@ -162,6 +164,7 @@ export interface JsonLdConcept {
162
164
  'gl:localizedConcept'?: Record<string, JsonLdLocalizedConcept>;
163
165
  'gl:related'?: JsonLdRelated[];
164
166
  'gl:partitiveRelations'?: JsonLdPartitiveRelation[];
167
+ 'gl:genericRelations'?: JsonLdPartitiveRelation[];
165
168
  'gl:tags'?: string[];
166
169
  'gl:figureRef'?: unknown[];
167
170
  'gl:tableRef'?: unknown[];
@@ -170,55 +170,79 @@ declare module 'glossarist' {
170
170
  function pairFromMultiplicity(name: Multiplicity): { presence: PartitivePresence; count: PartitiveCount };
171
171
 
172
172
  interface Concept {
173
- readonly partitiveRelations: PartitiveRelation[];
173
+ readonly relations: AbstractHyperedge[];
174
+ readonly partitiveRelations: PartitiveHyperedge[];
175
+ readonly genericRelations: GenericHyperedge[];
174
176
  }
175
177
 
176
178
  // Re-exposed here so callers importing from the top-level `glossarist`
177
179
  // entry get the MECE-augmented shape, not the stale d.ts type.
178
- export class PartitiveMember extends GlossaristModel {
180
+ export class HyperedgeMember extends GlossaristModel {
179
181
  constructor(data?: {
180
182
  ref?: ConceptRef;
181
183
  presence?: PartitivePresence;
182
184
  count?: PartitiveCount;
183
- is_delimiting?: boolean;
184
185
  });
185
186
  readonly ref: ConceptRef;
186
187
  readonly presence: PartitivePresence;
187
188
  readonly count: PartitiveCount;
189
+ required(): boolean;
190
+ optional(): boolean;
191
+ toJSON(): Record<string, unknown>;
192
+ static fromJSON(data: Record<string, unknown>): HyperedgeMember;
193
+ static identityOf(value: unknown): string;
194
+ }
195
+
196
+ export class PartitiveMember extends HyperedgeMember {
197
+ constructor(data?: {
198
+ ref?: ConceptRef;
199
+ presence?: PartitivePresence;
200
+ count?: PartitiveCount;
201
+ is_delimiting?: boolean;
202
+ });
188
203
  readonly is_delimiting: boolean;
189
- readonly isRequired: boolean;
190
- readonly isOptional: boolean;
191
204
  get isDelimiting(): boolean;
192
- toJSON(): { ref: ReturnType<ConceptRef['toJSON']>; presence?: PartitivePresence; count?: PartitiveCount; is_delimiting?: boolean };
193
- static fromJSON(data: Record<string, unknown>): PartitiveMember;
194
- static identityOf(value: unknown): string;
195
205
  }
196
206
 
197
- export class PartitiveRelation extends GlossaristModel {
207
+ export class GenericMember extends HyperedgeMember {
198
208
  constructor(data?: {
199
- comprehensive?: ConceptRef;
200
- partitives?: PartitiveMember[];
201
- completeness?: Completeness;
202
- criterion?: Record<string, string> | string;
209
+ ref?: ConceptRef;
210
+ presence?: PartitivePresence;
211
+ count?: PartitiveCount;
212
+ delimitingCharacteristic?: Record<string, string> | string;
203
213
  });
214
+ readonly delimitingCharacteristic: Record<string, string>;
215
+ }
216
+
217
+ export class AbstractHyperedge extends GlossaristModel {
204
218
  readonly comprehensive: ConceptRef;
205
- readonly partitives: PartitiveMember[];
219
+ readonly members: HyperedgeMember[];
206
220
  readonly completeness: Completeness;
207
221
  readonly criterion: Record<string, string> | null;
222
+ readonly sources: ConceptSource[];
223
+ readonly notes: Record<string, string> | null;
224
+ readonly status: string | null;
208
225
  readonly isComplete: boolean;
209
226
  readonly isPartial: boolean;
210
227
  readonly isCoordinate: boolean;
211
- hasPlurality(): boolean;
212
- hasCriterion(): boolean;
213
- toJSON(): {
214
- comprehensive: ReturnType<ConceptRef['toJSON']>;
215
- partitives: ReturnType<PartitiveMember['toJSON']>[];
216
- completeness: Completeness;
217
- criterion?: Record<string, string>;
218
- };
219
- static fromJSON(data: Record<string, unknown>): PartitiveRelation;
228
+ toJSON(): Record<string, unknown>;
229
+ static fromJSON(data: Record<string, unknown>): AbstractHyperedge;
230
+ static identityOf(value: unknown): string;
231
+ }
232
+
233
+ export class PartitiveHyperedge extends AbstractHyperedge {
234
+ get partitives(): HyperedgeMember[];
235
+ static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
236
+ static identityOf(value: unknown): string;
237
+ }
238
+
239
+ export class GenericHyperedge extends AbstractHyperedge {
240
+ static fromJSON(data: Record<string, unknown>): GenericHyperedge;
220
241
  static identityOf(value: unknown): string;
221
242
  }
243
+
244
+ // Legacy aliases (deprecated — use Hyperedge-based names)
245
+ export class PartitiveRelation extends PartitiveHyperedge {}
222
246
  }
223
247
 
224
248
  // Re-declare the same exports under the 'glossarist/models' subpath so
@@ -261,9 +285,9 @@ declare module 'glossarist/models' {
261
285
  // (TypeSharedPlurality removed — superseded by presence/count on
262
286
  // PartitiveMember per MECE refactor. Was dead code.)
263
287
 
264
- // ── PartitiveRelation (glossarist 0.4.26 — MECE presence × count) ─
288
+ // ── Hyperedge classes (glossarist 0.4.33 — unified n-ary model) ─
265
289
 
266
- export class PartitiveMember extends GlossaristModel {
290
+ export class HyperedgeMember extends GlossaristModel {
267
291
  constructor(data?: {
268
292
  ref?: ConceptRef;
269
293
  presence?: PartitivePresence;
@@ -274,37 +298,44 @@ declare module 'glossarist/models' {
274
298
  readonly presence: PartitivePresence;
275
299
  readonly count: PartitiveCount;
276
300
  readonly is_delimiting: boolean;
277
- readonly isRequired: boolean;
278
- readonly isOptional: boolean;
301
+ required(): boolean;
302
+ optional(): boolean;
279
303
  get isDelimiting(): boolean;
280
- toJSON(): { ref: ReturnType<ConceptRef['toJSON']>; presence?: PartitivePresence; count?: PartitiveCount; is_delimiting?: boolean };
281
- static fromJSON(data: Record<string, unknown>): PartitiveMember;
304
+ toJSON(): Record<string, unknown>;
305
+ static fromJSON(data: Record<string, unknown>): HyperedgeMember;
282
306
  static identityOf(value: unknown): string;
283
307
  }
284
308
 
285
- export class PartitiveRelation extends GlossaristModel {
286
- constructor(data?: {
287
- comprehensive?: ConceptRef;
288
- partitives?: PartitiveMember[];
289
- completeness?: Completeness;
290
- criterion?: Record<string, string> | string;
291
- });
309
+ export class PartitiveMember extends HyperedgeMember {}
310
+ export class GenericMember extends HyperedgeMember {}
311
+
312
+ export class AbstractHyperedge extends GlossaristModel {
292
313
  readonly comprehensive: ConceptRef;
293
- readonly partitives: PartitiveMember[];
314
+ readonly members: HyperedgeMember[];
294
315
  readonly completeness: Completeness;
295
316
  readonly criterion: Record<string, string> | null;
317
+ readonly sources: ConceptSource[];
318
+ readonly notes: Record<string, string> | null;
319
+ readonly status: string | null;
296
320
  readonly isComplete: boolean;
297
321
  readonly isPartial: boolean;
298
322
  readonly isCoordinate: boolean;
299
- hasPlurality(): boolean;
300
- hasCriterion(): boolean;
301
- toJSON(): {
302
- comprehensive: ReturnType<ConceptRef['toJSON']>;
303
- partitives: ReturnType<PartitiveMember['toJSON']>[];
304
- completeness: Completeness;
305
- criterion?: Record<string, string>;
306
- };
307
- static fromJSON(data: Record<string, unknown>): PartitiveRelation;
323
+ toJSON(): Record<string, unknown>;
324
+ static fromJSON(data: Record<string, unknown>): AbstractHyperedge;
325
+ static identityOf(value: unknown): string;
326
+ }
327
+
328
+ export class PartitiveHyperedge extends AbstractHyperedge {
329
+ get partitives(): HyperedgeMember[];
330
+ static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
308
331
  static identityOf(value: unknown): string;
309
332
  }
333
+
334
+ export class GenericHyperedge extends AbstractHyperedge {
335
+ static fromJSON(data: Record<string, unknown>): GenericHyperedge;
336
+ static identityOf(value: unknown): string;
337
+ }
338
+
339
+ // Legacy aliases (deprecated)
340
+ export class PartitiveRelation extends PartitiveHyperedge {}
310
341
  }
@@ -179,6 +179,33 @@ export interface PartitiveMemberWire {
179
179
  isDelimiting: boolean;
180
180
  }
181
181
 
182
+ /**
183
+ * GenericRelationWire — ISO 704 / 1087-1 / 12620 generic (genus/
184
+ * species) decomposition, mirror of PartitiveRelationWire.
185
+ *
186
+ * The OIML V 2-200:2010 dataset has ~8 generic hyperedges with
187
+ * multiple criterion groups per comprehensive; this shape supports
188
+ * that natively. See docs/design/generic-relation.md (concept-model).
189
+ */
190
+ export interface GenericRelationWire {
191
+ source: string;
192
+ comprehensive: string;
193
+ members: GenericMemberWire[];
194
+ completeness: 'complete' | 'partial';
195
+ criterion?: Record<string, string>;
196
+ register: string;
197
+ }
198
+
199
+ export interface GenericMemberWire {
200
+ uri: string;
201
+ presence: 'required' | 'optional';
202
+ count: 'exactly_one' | 'at_least_one' | 'multiple';
203
+ /** ISO 704:2022 §5.5.4.2.1 — the delimiting characteristic that
204
+ * distinguishes this specific concept from its coordinate concepts.
205
+ * Required on every GenericMember. LocalizedString shape. */
206
+ delimitingCharacteristic: Record<string, string>;
207
+ }
208
+
182
209
  export interface GraphNode {
183
210
  uri: string;
184
211
  register: string;
@@ -37,6 +37,7 @@ export const GL = {
37
37
  LOCALIZED_CONCEPT: 'gl:localizedConcept',
38
38
  RELATED: 'gl:related',
39
39
  PARTITIVE_RELATIONS: 'gl:partitiveRelations',
40
+ GENERIC_RELATIONS: 'gl:genericRelations',
40
41
  TAGS: 'gl:tags',
41
42
 
42
43
  // LocalizedConcept
@@ -133,12 +134,15 @@ export const GL = {
133
134
  PRESENCE: 'gl:presence',
134
135
  COUNT: 'gl:count',
135
136
  IS_DELIMITING: 'gl:isDelimiting',
137
+ DELIMITING_CHARACTERISTIC: 'gl:delimitingCharacteristic',
136
138
  MULTIPLICITY: 'gl:multiplicity',
137
139
  CERTAINTY: 'gl:certainty',
138
140
 
139
141
  // PartitiveRelation
140
142
  COMPREHENSIVE: 'gl:comprehensive',
141
143
  HAS_PARTITIVE: 'gl:hasPartitive',
144
+ HAS_GENERIC: 'gl:hasGeneric',
145
+ HAS_MEMBER: 'gl:members',
142
146
  COMPLETENESS: 'gl:completeness',
143
147
  CRITERION: 'gl:criterion',
144
148
 
@@ -28,6 +28,7 @@ import ConceptEditionRail from './ConceptEditionRail.vue';
28
28
  import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
29
29
  import NonVerbalList from './non-verbal/NonVerbalList.vue';
30
30
  import PartitiveRelationList from './PartitiveRelationList.vue';
31
+ import GenericRelationList from './GenericRelationList.vue';
31
32
  import ConceptDiffModal from './ConceptDiffModal.vue';
32
33
  import CitationDisplay from './CitationDisplay.vue';
33
34
  import DesignationList from './DesignationList.vue';
@@ -75,6 +76,7 @@ const {
75
76
  inverseEdgeType,
76
77
  conceptRelated,
77
78
  conceptPartitiveRelations,
79
+ conceptGenericRelations,
78
80
  getResolvedRef,
79
81
  relatedLabel,
80
82
  navigateEdge,
@@ -577,6 +579,14 @@ const nonVerbalReps = computed(() => {
577
579
  :register-id="registerId"
578
580
  />
579
581
 
582
+ <!-- Generic relations — genus/species decompositions (ISO 704) -->
583
+ <GenericRelationList
584
+ v-if="conceptGenericRelations.length"
585
+ :relations="conceptGenericRelations"
586
+ :manifest="manifest"
587
+ :register-id="registerId"
588
+ />
589
+
580
590
  <!-- Edition series — supersession chain across vocabulary editions -->
581
591
  <ConceptEditionRail
582
592
  :concept-uri="conceptUri(props.concept, props.registerId, props.manifest.uriBase)"
@@ -0,0 +1,176 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * GenericRelationList — renders one-to-many generic (genus/species)
4
+ * decompositions as ISO 704 rake diagrams. Mirror of
5
+ * PartitiveRelationList with type label swapped.
6
+ *
7
+ * Each relation card shows:
8
+ * - completeness badge (Complete / Partial)
9
+ * - the comprehensive (genus) designation as the card title
10
+ * - optional criterion as italic text
11
+ * - the rake diagram itself (PartitiveRelationDiagram, reused —
12
+ * the line notation is identical for generic rakes)
13
+ *
14
+ * OIML V 2-200:2010 has ~8 generic hyperedges; 5.1 measurement
15
+ * standard alone has 6 criterion groups. This component is the
16
+ * primary rendering surface for those datasets.
17
+ */
18
+ import type { GenericRelationWire, Manifest } from '../adapters/types';
19
+ import { useRouter } from 'vue-router';
20
+ import { useVocabularyStore } from '../stores/vocabulary';
21
+ import { getFactory } from '../adapters/factory';
22
+ import { useI18n, locale } from '../i18n';
23
+ import { completenessLabel } from '../utils/partitive-relation-styling';
24
+ import { presenceLabel, countLabel } from '../utils/partitive-multiplicity';
25
+ import { resolveDesignation } from '../utils/resolve-designation';
26
+ import PartitiveRelationDiagram, {
27
+ type PartitiveMemberLabeled,
28
+ } from './PartitiveRelationDiagram.vue';
29
+
30
+ const props = defineProps<{
31
+ relations: GenericRelationWire[];
32
+ manifest: Manifest;
33
+ registerId: string;
34
+ }>();
35
+
36
+ const router = useRouter();
37
+ const store = useVocabularyStore();
38
+ const factory = getFactory();
39
+ const { t } = useI18n();
40
+
41
+ function designationFor(uri: string): string {
42
+ return resolveDesignation(uri, store, factory, locale.value);
43
+ }
44
+
45
+ /** Resolve a LocalizedString delimitingCharacteristic to the current locale. */
46
+ function characteristicText(dc: Record<string, string>): string {
47
+ return dc[locale.value] ?? dc.default ?? dc.eng ?? Object.values(dc)[0] ?? '';
48
+ }
49
+
50
+ function memberLabel(member: GenericRelationWire['members'][number]): PartitiveMemberLabeled {
51
+ return {
52
+ uri: member.uri,
53
+ presence: member.presence,
54
+ count: member.count,
55
+ isDelimiting: false,
56
+ label: designationFor(member.uri),
57
+ };
58
+ }
59
+ </script>
60
+
61
+ <template>
62
+ <section class="generic-relations" aria-label="Generic relations">
63
+ <div
64
+ v-for="(rel, i) in relations"
65
+ :key="`generic-${i}-${rel.comprehensive}`"
66
+ class="generic-relation-card"
67
+ >
68
+ <header class="relation-header">
69
+ <span class="badge badge-completeness" :class="`completeness-${rel.completeness}`">
70
+ {{ completenessLabel(rel.completeness) }}
71
+ </span>
72
+ <span class="badge badge-type">{{ t('relations.generic') }}</span>
73
+ <h4 class="comprehensive">{{ designationFor(rel.comprehensive) }}</h4>
74
+ </header>
75
+
76
+ <p v-if="rel.criterion" class="criterion">
77
+ <em>{{ rel.criterion[locale] || rel.criterion.eng || Object.values(rel.criterion)[0] }}</em>
78
+ </p>
79
+
80
+ <PartitiveRelationDiagram
81
+ :partitives="rel.members.map(memberLabel)"
82
+ :comprehensive-label="designationFor(rel.comprehensive)"
83
+ :completeness="rel.completeness"
84
+ />
85
+
86
+ <ul class="member-presence-list" aria-label="Member delimiting characteristics">
87
+ <li v-for="(m, j) in rel.members" :key="j">
88
+ <span class="member-label">{{ designationFor(m.uri) }}</span>
89
+ <span
90
+ v-if="m.delimitingCharacteristic"
91
+ class="member-delimiting-characteristic"
92
+ >{{ characteristicText(m.delimitingCharacteristic) }}</span>
93
+ <span v-if="m.presence && m.presence !== 'required'" class="member-presence">{{ presenceLabel(m.presence) }}</span>
94
+ <span v-if="m.count && m.count !== 'exactly_one'" class="member-count">{{ countLabel(m.count) }}</span>
95
+ </li>
96
+ </ul>
97
+ </div>
98
+ </section>
99
+ </template>
100
+
101
+ <style scoped>
102
+ .generic-relation-card {
103
+ border: 1px solid var(--color-border, #ccc);
104
+ border-radius: 6px;
105
+ padding: 1rem;
106
+ margin-bottom: 1rem;
107
+ background: var(--color-surface, #fff);
108
+ }
109
+
110
+ .relation-header {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 0.5rem;
114
+ flex-wrap: wrap;
115
+ }
116
+
117
+ .badge {
118
+ font-size: 0.75rem;
119
+ font-weight: 600;
120
+ padding: 0.15rem 0.5rem;
121
+ border-radius: 4px;
122
+ text-transform: uppercase;
123
+ letter-spacing: 0.05em;
124
+ }
125
+
126
+ .badge-completeness.completeness-complete {
127
+ background: var(--color-success-soft, #d4edda);
128
+ color: var(--color-success-fg, #155724);
129
+ }
130
+
131
+ .badge-completeness.completeness-partial {
132
+ background: var(--color-warn-soft, #fff3cd);
133
+ color: var(--color-warn-fg, #856404);
134
+ }
135
+
136
+ .badge-type {
137
+ background: var(--color-accent-soft, #d1ecf1);
138
+ color: var(--color-accent-fg, #0c5460);
139
+ }
140
+
141
+ .comprehensive {
142
+ margin: 0;
143
+ font-size: 1.1rem;
144
+ flex: 1 1 auto;
145
+ }
146
+
147
+ .criterion {
148
+ color: var(--color-text-muted, #6c757d);
149
+ margin: 0.5rem 0;
150
+ font-size: 0.95rem;
151
+ }
152
+
153
+ .member-presence-list {
154
+ list-style: none;
155
+ padding: 0;
156
+ margin: 0.5rem 0 0;
157
+ font-size: 0.85rem;
158
+ }
159
+
160
+ .member-presence-list li {
161
+ display: flex;
162
+ gap: 0.5rem;
163
+ align-items: center;
164
+ padding: 0.15rem 0;
165
+ }
166
+
167
+ .member-presence-list .member-presence,
168
+ .member-presence-list .member-count,
169
+ .member-presence-list .member-delimiting {
170
+ color: var(--color-text-muted, #6c757d);
171
+ font-size: 0.75rem;
172
+ border: 1px solid var(--color-border-light, #eee);
173
+ padding: 0.05rem 0.4rem;
174
+ border-radius: 3px;
175
+ }
176
+ </style>
@@ -20,6 +20,7 @@ import { getFactory } from '../adapters/factory';
20
20
  import { useI18n, locale } from '../i18n';
21
21
  import { completenessLabel } from '../utils/partitive-relation-styling';
22
22
  import { presenceLabel, countLabel } from '../utils/partitive-multiplicity';
23
+ import { resolveDesignation } from '../utils/resolve-designation';
23
24
  import PartitiveRelationDiagram, {
24
25
  type PartitiveMemberLabeled,
25
26
  } from './PartitiveRelationDiagram.vue';
@@ -36,25 +37,7 @@ const factory = getFactory();
36
37
  const { t } = useI18n();
37
38
 
38
39
  function designationFor(uri: string): string {
39
- const node = store.graph.getNode(uri);
40
- if (node) {
41
- const des = node.designations[locale.value]
42
- || node.designations.eng
43
- || Object.values(node.designations)[0];
44
- if (des) return des;
45
- }
46
- const resolution = factory.resolve(uri);
47
- if (resolution.type === 'internal') {
48
- const adapter = store.datasets.get(resolution.registerId);
49
- const entry = adapter?.getIndexEntry(resolution.conceptId);
50
- if (entry) {
51
- const des = entry.designations[locale.value]
52
- || entry.designations.eng
53
- || Object.values(entry.designations)[0];
54
- if (des) return des;
55
- }
56
- }
57
- return resolution.type === 'internal' ? resolution.conceptId : uri;
40
+ return resolveDesignation(uri, store, factory, locale.value);
58
41
  }
59
42
 
60
43
  function navigate(uri: string) {