@glossarist/concept-browser 0.7.85 → 0.7.87

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.
@@ -1,25 +1,25 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
3
  * PartitiveRelationList — renders one-to-many partitive decompositions
4
- * as ISO 704 rake diagrams. Each relation shows:
5
- * - completeness badge (complete / partial)
6
- * - plurality badge (shared / uncertain / sharedType) when present
7
- * - criterion as italic text under the header
8
- * - the rake diagram itself (PartitiveRelationDiagram)
4
+ * as ISO 704 rake diagrams. Each relation card shows:
5
+ * - completeness badge (Complete / Partial)
6
+ * - the comprehensive's designation as the card title
7
+ * - optional criterion as italic text
8
+ * - the rake diagram itself (PartitiveRelationDiagram), where each
9
+ * drop's line style encodes its multiplicity + is_delimiting per
10
+ * ISO 704:2022 (5 multiplicity values × 2 delimiting states).
9
11
  *
10
- * The diagram is the primary renderer. Badges in the header communicate
11
- * the metadata that the diagram's line styles also encode (so users who
12
- * can't perceive the visual difference still get the info).
12
+ * Header badges communicate metadata that the diagram's line styles
13
+ * also encode — for users who can't perceive the visual difference
14
+ * (screen readers, low vision, monochrome displays).
13
15
  */
14
16
  import type { PartitiveRelationWire, Manifest } from '../adapters/types';
15
17
  import { useRouter } from 'vue-router';
16
18
  import { useVocabularyStore } from '../stores/vocabulary';
17
19
  import { getFactory } from '../adapters/factory';
18
20
  import { useI18n, locale } from '../i18n';
19
- import {
20
- completenessLabel,
21
- certaintyLabel,
22
- } from '../utils/partitive-relation-styling';
21
+ import { completenessLabel } from '../utils/partitive-relation-styling';
22
+ import { presenceLabel, countLabel } from '../utils/partitive-multiplicity';
23
23
  import PartitiveRelationDiagram, {
24
24
  type PartitiveMemberLabeled,
25
25
  } from './PartitiveRelationDiagram.vue';
@@ -77,25 +77,25 @@ function criterionText(criterion?: Record<string, string>): string | null {
77
77
  ?? null;
78
78
  }
79
79
 
80
- function pluralityBadge(plurality: NonNullable<PartitiveRelationWire['plurality']>): string {
81
- if (!plurality.isShared) return '';
82
- let label = t('partitive.plurality.shared') || 'shared type';
83
- if (plurality.isUncertain) {
84
- label = `${label} (${t('partitive.plurality.uncertain') || 'uncertain'})`;
85
- }
86
- if (plurality.sharedType) {
87
- label = `${label}: ${plurality.sharedType}`;
88
- }
89
- return label;
90
- }
91
-
80
+ /**
81
+ * Build a per-member view-model carrying the resolved designation plus
82
+ * the multiplicity + is_delimiting fields the diagram needs.
83
+ */
92
84
  function labeledMembers(rel: PartitiveRelationWire): PartitiveMemberLabeled[] {
93
85
  return rel.partitives.map(m => ({
94
86
  uri: m.uri,
95
87
  label: designationFor(m.uri),
96
- certainty: m.certainty,
88
+ presence: m.presence,
89
+ count: m.count,
90
+ isDelimiting: m.isDelimiting,
97
91
  }));
98
92
  }
93
+
94
+ /** Tooltip text for a member, shown on hover. */
95
+ function memberTooltip(m: { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple'; isDelimiting: boolean }): string {
96
+ const label = `${presenceLabel(m.presence)} · ${countLabel(m.count).toLowerCase()}`;
97
+ return m.isDelimiting ? `Delimiting · ${label}` : label;
98
+ }
99
99
  </script>
100
100
 
101
101
  <template>
@@ -113,13 +113,6 @@ function labeledMembers(rel: PartitiveRelationWire): PartitiveMemberLabeled[] {
113
113
  <span class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-200 dark:bg-emerald-800 text-emerald-900 dark:text-emerald-100 uppercase">
114
114
  {{ completenessLabel(rel.completeness) }}
115
115
  </span>
116
- <span
117
- v-if="rel.plurality && rel.plurality.isShared"
118
- class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-amber-200 dark:bg-amber-800 text-amber-900 dark:text-amber-100 uppercase"
119
- :title="pluralityBadge(rel.plurality)"
120
- >
121
- {{ pluralityBadge(rel.plurality) }}
122
- </span>
123
116
  <button
124
117
  @click="navigate(rel.comprehensive)"
125
118
  class="ml-auto text-sm font-medium text-emerald-900 dark:text-emerald-100 hover:underline truncate"
@@ -133,14 +126,14 @@ function labeledMembers(rel: PartitiveRelationWire): PartitiveMemberLabeled[] {
133
126
  >
134
127
  {{ criterionText(rel.criterion) }}
135
128
  </div>
136
- <!-- Rake diagram (ISO 704 partitive-relation convention) -->
129
+ <!-- Rake diagram (ISO 704:2022 partitive-relation convention) -->
137
130
  <div class="px-3 py-3 overflow-x-auto">
138
131
  <PartitiveRelationDiagram
139
132
  :comprehensive-label="designationFor(rel.comprehensive)"
140
133
  :partitives="labeledMembers(rel)"
141
134
  :completeness="rel.completeness"
142
- :plurality="rel.plurality"
143
135
  :criterion="null"
136
+ :member-tooltip="memberTooltip"
144
137
  @navigate="navigate"
145
138
  />
146
139
  </div>
@@ -25,7 +25,8 @@ import { hashSeed, expandParams, portSide, portPoint, idToUriGet } from '../util
25
25
  import { useI18n } from '../i18n';
26
26
 
27
27
  const { t, locale } = useI18n();
28
- import type { Concept, Manifest, GraphEdge, PartitiveRelationWire } from '../adapters/types';
28
+ import type { Concept, Manifest, GraphEdge, PartitiveRelationWire, PartitiveMemberWire } from '../adapters/types';
29
+ import { rakeStrokeStyle } from '../utils/partitive-multiplicity';
29
30
  import { conceptUri } from '../adapters/model-bridge';
30
31
 
31
32
  const props = defineProps<{
@@ -760,41 +761,33 @@ function drawEdges(cx: number, cy: number) {
760
761
  }
761
762
 
762
763
  /**
763
- * Render PartitiveRelations as ISO 704 right-angle rake brackets.
764
+ * Render PartitiveRelations as ISO 704:2022 rake brackets in the sphere.
764
765
  *
765
- * Each relation renders as a true bracket with 90° angles:
766
+ * Each relation renders as a right-angle bracket with stem + spine + drops
767
+ * (geometry described in detail above). Stem and spine are the structural
768
+ * frame — always single solid at FRAME_STROKE_WIDTH.
766
769
  *
767
- * [comprehensive]
768
- * │
769
- * │ ← stem (along stem-axis)
770
- * │
771
- * ┌──────────●──────────┐ ← spine (perpendicular to stem-axis; right angle at junction)
772
- * │ │ │
773
- * │ │ │ ← drops (parallel to stem-axis; right angle at each tooth)
774
- * [p1] [p2] [p3]
770
+ * Per-member multiplicity + is_delimiting is encoded on each DROP via
771
+ * rakeStrokeStyle() (see utils/partitive-multiplicity.ts):
775
772
  *
776
- * Geometry:
777
- * 1. stem-axis = direction from comprehensive to centroid of partitives
778
- * 2. spine-axis = perpendicular to stem-axis
779
- * 3. spine-center = point along stem-axis at SPINE_FRACTION (60%) from comp
780
- * 4. For each partitive Pi:
781
- * - Project Pi onto spine-axis → tooth position
782
- * - Drop goes from tooth straight to Pi (parallel to stem-axis by construction)
773
+ * multiplicity is_delimiting drop visual
774
+ * ------------------- -------------- --------------------------------
775
+ * compulsory false 1 solid @ 1.5px
776
+ * compulsory true 1 solid @ 4.5px (3× bold)
777
+ * optional false 1 dashed @ 1.5px
778
+ * optional true 1 dashed @ 4.5px
779
+ * compulsory_multiple false 2 solid @ 1.5px
780
+ * compulsory_multiple true 2 solid @ 4.5px
781
+ * optional_multiple false 2 dashed @ 1.5px
782
+ * optional_multiple true 2 dashed @ 4.5px
783
+ * compulsory_at_least_one false 1 solid + 1 dashed @ 1.5px
784
+ * compulsory_at_least_one true 1 solid + 1 dashed @ 4.5px
783
785
  *
784
- * All bends are exactly 90° because tooth_i is the foot of perpendicular
785
- * from Pi to the spine line.
786
- *
787
- * ISO 704 line conventions:
788
- * - default : single solid line per segment
789
- * - plurality.isShared && !isUncertain : TWO parallel solid lines (close-set)
790
- * - plurality.isShared && isUncertain : one solid + one dashed line
791
- * - completeness === 'partial' : spine extends past last tooth
792
- * (continued backline — "more exist")
793
- *
794
- * Per-member certainty 'possible' → that drop is dashed + reduced opacity
795
- * (stem and spine keep relation style).
786
+ * completeness === 'partial' : spine extends past last tooth (continued
787
+ * backline — "more exist but not shown")
796
788
  *
797
789
  * Diamond marker at the comprehensive end signals the hyperedge origin.
790
+ * Junction marker at the spine midpoint signals the right-angle corner.
798
791
  */
799
792
  function drawRakeBundles(
800
793
  svg: SVGSVGElement,
@@ -806,6 +799,7 @@ function drawRakeBundles(
806
799
  const color = uiStore.isDark ? '#2dd4bf' : '#0d9488';
807
800
  const DOUBLE_GAP = 4;
808
801
  const DASH = '4 3';
802
+ const FRAME_STROKE_WIDTH = 1.4;
809
803
  const SPINE_FRACTION = 0.55; // spine sits 55% from comp toward centroid
810
804
  const PARTIAL_TAIL = 28; // px extension for completeness: 'partial'
811
805
 
@@ -824,7 +818,7 @@ function drawRakeBundles(
824
818
  const p = pos.get(nid);
825
819
  return p ? { member, pos: p } : null;
826
820
  })
827
- .filter((x): x is { member: { uri: string; certainty: 'confirmed' | 'possible' }; pos: {x: number; y: number} } => x !== null);
821
+ .filter((x): x is { member: PartitiveMemberWire; pos: {x: number; y: number} } => x !== null);
828
822
 
829
823
  if (memberPositions.length === 0) continue;
830
824
 
@@ -850,7 +844,14 @@ function drawRakeBundles(
850
844
  const along = vx * spineDirX + vy * spineDirY;
851
845
  const toothX = spineCx + spineDirX * along;
852
846
  const toothY = spineCy + spineDirY * along;
853
- return { member, partitivePos: pPos, toothX, toothY, along };
847
+ return {
848
+ member,
849
+ partitivePos: pPos,
850
+ toothX,
851
+ toothY,
852
+ along,
853
+ style: rakeStrokeStyle(member.presence, member.count, member.isDelimiting),
854
+ };
854
855
  });
855
856
 
856
857
  // 4. Spine extents (leftmost to rightmost tooth along spine-axis)
@@ -867,23 +868,22 @@ function drawRakeBundles(
867
868
  const spineEndX = spineCx + spineDirX * maxAlong;
868
869
  const spineEndY = spineCy + spineDirY * maxAlong;
869
870
 
870
- const isShared = !!rel.plurality?.isShared;
871
- const isUncertain = !!rel.plurality?.isUncertain;
872
-
873
871
  /**
874
- * Draw a single straight segment with optional parallel offset
875
- * (used to render the close-set double line) and dash style.
872
+ * Draw a single straight segment.
873
+ *
874
+ * Stem + spine use FRAME_STROKE_WIDTH (single solid frame).
875
+ * Drops use the per-member multiplicity + delimiting style from
876
+ * rakeStrokeStyle().
876
877
  */
877
878
  const drawSegment = (
878
879
  ax: number, ay: number, bx: number, by: number,
879
- opts: { offset?: number; dashed?: boolean; opacity?: number } = {},
880
+ opts: { width: number; dashed?: boolean; offset?: number; opacity?: number },
880
881
  ) => {
882
+ const opacity = opts.opacity ?? 0.85;
881
883
  const offset = opts.offset ?? 0;
882
884
  const dashed = opts.dashed ?? false;
883
- const opacity = opts.opacity ?? 0.85;
884
885
  const dx = bx - ax, dy = by - ay;
885
886
  const len = Math.hypot(dx, dy) || 1;
886
- // Perpendicular to THIS segment (not the spine-axis) for parallel offset
887
887
  const px = -dy / len, py = dx / len;
888
888
  const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
889
889
  line.setAttribute('class', 'rake-seg');
@@ -892,50 +892,35 @@ function drawRakeBundles(
892
892
  line.setAttribute('x2', String(bx + px * offset));
893
893
  line.setAttribute('y2', String(by + py * offset));
894
894
  line.setAttribute('stroke', color);
895
- line.setAttribute('stroke-width', '1.6');
895
+ line.setAttribute('stroke-width', String(opts.width));
896
896
  line.setAttribute('opacity', String(opacity));
897
- line.setAttribute('stroke-linecap', 'square'); // sharp corners at right angles
897
+ line.setAttribute('stroke-linecap', 'square');
898
898
  if (dashed) line.setAttribute('stroke-dasharray', DASH);
899
899
  svg.appendChild(line);
900
900
  };
901
901
 
902
- /**
903
- * Draw a segment with the relation's plurality style.
904
- * Single solid (default), double solid (isShared),
905
- * or solid + dashed (isShared + isUncertain).
906
- */
907
- const drawSegmentWithPlurality = (
908
- ax: number, ay: number, bx: number, by: number,
909
- opts: { opacity?: number; perMemberPossible?: boolean } = {},
910
- ) => {
911
- const opacity = opts.opacity ?? 0.85;
912
- if (!isShared) {
913
- drawSegment(ax, ay, bx, by, { opacity, dashed: opts.perMemberPossible });
914
- } else {
915
- drawSegment(ax, ay, bx, by, { offset: -DOUBLE_GAP / 2, opacity });
916
- drawSegment(ax, ay, bx, by, {
917
- offset: DOUBLE_GAP / 2,
918
- opacity,
919
- dashed: isUncertain || opts.perMemberPossible,
920
- });
921
- }
922
- };
923
-
924
- // 5. Stem: comp → spine center (along stem-axis)
925
- drawSegmentWithPlurality(compPos.x, compPos.y, spineCx, spineCy);
902
+ // 5. Stem: comp → spine center (frame — single solid)
903
+ drawSegment(compPos.x, compPos.y, spineCx, spineCy, { width: FRAME_STROKE_WIDTH });
926
904
 
927
- // 6. Spine: leftmost tooth → rightmost tooth (along spine-axis)
928
- // (skip if only one tooth — no horizontal extent)
905
+ // 6. Spine: leftmost tooth → rightmost tooth (frame — single solid)
929
906
  if (Math.abs(maxAlong - minAlong) > 1) {
930
- drawSegmentWithPlurality(spineStartX, spineStartY, spineEndX, spineEndY);
907
+ drawSegment(spineStartX, spineStartY, spineEndX, spineEndY, { width: FRAME_STROKE_WIDTH });
931
908
  }
932
909
 
933
- // 7. Drops: tooth → partitive (parallel to stem-axis by construction)
910
+ // 7. Drops: tooth → partitive (per-member multiplicity + delimiting)
911
+ // Draw 1 or 2 parallel lines per the style.
934
912
  for (const tooth of teeth) {
935
- drawSegmentWithPlurality(tooth.toothX, tooth.toothY, tooth.partitivePos.x, tooth.partitivePos.y, {
936
- opacity: tooth.member.certainty === 'possible' ? 0.5 : 0.85,
937
- perMemberPossible: tooth.member.certainty === 'possible',
913
+ drawSegment(tooth.toothX, tooth.toothY, tooth.partitivePos.x, tooth.partitivePos.y, {
914
+ width: tooth.style.strokeWidth,
915
+ dashed: tooth.style.primaryDashed,
938
916
  });
917
+ if (tooth.style.lineCount === 2) {
918
+ drawSegment(tooth.toothX, tooth.toothY, tooth.partitivePos.x, tooth.partitivePos.y, {
919
+ width: tooth.style.strokeWidth,
920
+ dashed: tooth.style.secondaryDashed,
921
+ offset: DOUBLE_GAP,
922
+ });
923
+ }
939
924
  }
940
925
 
941
926
  // 8. Junction marker at spine center (the right-angle corner)
@@ -1,19 +1,18 @@
1
1
  import { computed, type ComputedRef } from 'vue';
2
2
  import type { Router } from 'vue-router';
3
+ // glossarist-js's top-level d.ts is incomplete for v3 model fields.
4
+ // Import the MECE-augmented types from glossarist/models (extended by
5
+ // src/adapters/non-verbal/glossarist-augment.d.ts).
6
+ import type { Concept, RelatedConcept, ConceptRef } from 'glossarist';
3
7
  import type {
4
- Concept,
5
- RelatedConcept,
6
8
  PartitiveRelation as GlsPartitiveRelation,
7
9
  PartitiveMember as GlsPartitiveMember,
8
- TypeSharedPlurality as GlsTypeSharedPlurality,
9
- ConceptRef,
10
- } from 'glossarist';
10
+ } from 'glossarist/models';
11
11
  import type {
12
12
  Manifest,
13
13
  GraphEdge,
14
14
  PartitiveRelationWire,
15
15
  PartitiveMemberWire,
16
- TypeSharedPluralityWire,
17
16
  } from '../adapters/types';
18
17
  import { getFactory } from '../adapters/factory';
19
18
  import { conceptUri } from '../adapters/model-bridge';
@@ -24,6 +23,7 @@ import { categorizeRelationship, relationshipLabel, INVERSE_RELATIONSHIPS } from
24
23
  import { langLabel } from '../utils/lang';
25
24
  import { escapeAttr } from '../utils/escape';
26
25
  import { useI18n } from '../i18n';
26
+ import { type PartitivePresence, type PartitiveCount } from '../utils/partitive-multiplicity';
27
27
 
28
28
  export interface EdgeDisplay {
29
29
  uri: string;
@@ -190,7 +190,12 @@ export function useConceptEdges(
190
190
  .map((m: GlsPartitiveMember): PartitiveMemberWire | null => {
191
191
  const uri = resolveConceptRefUri(m.ref);
192
192
  if (!uri || uri === source) return null;
193
- return { uri, certainty: m.certainty };
193
+ return {
194
+ uri,
195
+ presence: readPresence(m),
196
+ count: readCount(m),
197
+ isDelimiting: readIsDelimiting(m),
198
+ };
194
199
  })
195
200
  .filter((m: PartitiveMemberWire | null): m is PartitiveMemberWire => m !== null);
196
201
  if (partitives.length === 0) return null;
@@ -200,7 +205,6 @@ export function useConceptEdges(
200
205
  comprehensive,
201
206
  partitives,
202
207
  completeness: rel.completeness,
203
- plurality: projectPlurality(rel.plurality),
204
208
  criterion: rel.criterion ?? undefined,
205
209
  register: registerId.value,
206
210
  };
@@ -208,24 +212,25 @@ export function useConceptEdges(
208
212
  .filter((r: PartitiveRelationWire | null): r is PartitiveRelationWire => r !== null);
209
213
  });
210
214
 
211
- function projectPlurality(p: GlsTypeSharedPlurality | null): TypeSharedPluralityWire | null {
212
- if (!p) return null;
213
- const sharedTypeRef: ConceptRef | null = p.sharedType;
214
- const sharedType = sharedTypeRef
215
- ? [sharedTypeRef.source, sharedTypeRef.id].filter(Boolean).join(':') || null
216
- : null;
217
- return {
218
- isShared: p.isShared,
219
- isUncertain: p.isUncertain,
220
- sharedType,
221
- };
222
- }
223
-
224
215
  function resolveConceptRefUri(ref: ConceptRef | null): string | null {
225
216
  if (!ref) return null;
226
217
  return resolveRefUri({ source: ref.source ?? null, id: ref.id ?? null });
227
218
  }
228
219
 
220
+ // Read presence + count directly from the glossarist-js model
221
+ // (MECE native, since glossarist 0.4.26).
222
+ function readPresence(m: GlsPartitiveMember): PartitivePresence {
223
+ return m.presence;
224
+ }
225
+
226
+ function readCount(m: GlsPartitiveMember): PartitiveCount {
227
+ return m.count;
228
+ }
229
+
230
+ function readIsDelimiting(m: GlsPartitiveMember): boolean {
231
+ return m.is_delimiting === true;
232
+ }
233
+
229
234
  function resolveRefUri(ref: { source: string | null; id: string | null } | null): string | null {
230
235
  const target = resolveRelatedRef(ref);
231
236
  if (!target) return null;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * ISO 704:2022 §5.5.4.2 — partitive multiplicity model (MECE).
3
+ *
4
+ * Two independent axes:
5
+ *
6
+ * presence: 'required' | 'optional'
7
+ * Is this part always present?
8
+ *
9
+ * count: 'exactly_one' | 'at_least_one' | 'multiple'
10
+ * How many instances?
11
+ *
12
+ * 6 combinations, 5 visually distinct (optional + at_least_one
13
+ * collapses to 2-dashed = same as optional + multiple) and the
14
+ * invalid combo (optional, at_least_one) is rejected at construction
15
+ * by glossarist-js.
16
+ *
17
+ * Types are re-exported from glossarist-js — the model is the SSOT.
18
+ * The render-ready `rakeStrokeStyle(presence, count, isDelimiting)`
19
+ * helper is purely a browser-rendering concern; everything else
20
+ * (presence, count, is_delimiting fields, MULTIPLICITY name lookup via
21
+ * multiplicityFromPair, validity via isValidPresence / isValidCount)
22
+ * comes from `glossarist/models`.
23
+ */
24
+
25
+ import {
26
+ PARTITIVE_PRESENCE,
27
+ PARTITIVE_COUNT,
28
+ multiplicityFromPair,
29
+ isValidPresence,
30
+ isValidCount,
31
+ type PartitivePresence,
32
+ type PartitiveCount,
33
+ } from 'glossarist/models';
34
+
35
+ // ── Two independent axes (re-exported SSOT) ────────────────────────────
36
+
37
+ export { PARTITIVE_PRESENCE, PARTITIVE_COUNT };
38
+ export type { PartitivePresence, PartitiveCount };
39
+ export { isValidPresence as isPartitivePresence, isValidCount as isPartitiveCount };
40
+
41
+ /** ISO 704 name for a (presence, count) pair. Throws on invalid combos. */
42
+ export const partitiveMultiplicityName = multiplicityFromPair;
43
+
44
+ // ── Render-ready stroke style (rendering-only concern) ─────────────────
45
+
46
+ export interface RakeStrokeStyle {
47
+ readonly lineCount: 1 | 2;
48
+ readonly primaryDashed: boolean;
49
+ readonly secondaryDashed: boolean;
50
+ readonly strokeWidth: number;
51
+ }
52
+
53
+ export const NORMAL_STROKE_WIDTH = 1.5;
54
+ export const DELIMITING_STROKE_WIDTH = 4.5;
55
+
56
+ /**
57
+ * Compute the render-ready stroke style.
58
+ *
59
+ * Pure function — consumed by both the sidebar rake diagram
60
+ * (PartitiveRelationDiagram) and the sphere rake bundles
61
+ * (RelationSphere.drawRakeBundles).
62
+ *
63
+ * Derivation:
64
+ * presence=required → primary line is solid
65
+ * presence=optional → primary line is dashed
66
+ * count=exactly_one → 1 line
67
+ * count=multiple → 2 lines, both same style as primary
68
+ * count=at_least_one → 2 lines: primary + dashed ("possibly more")
69
+ */
70
+ export function rakeStrokeStyle(
71
+ presence: PartitivePresence,
72
+ count: PartitiveCount,
73
+ isDelimiting: boolean,
74
+ ): RakeStrokeStyle {
75
+ const primaryDashed = presence === 'optional';
76
+ const width = isDelimiting ? DELIMITING_STROKE_WIDTH : NORMAL_STROKE_WIDTH;
77
+
78
+ if (count === 'exactly_one') {
79
+ return { lineCount: 1, primaryDashed, secondaryDashed: false, strokeWidth: width };
80
+ }
81
+ // multiple or at_least_one
82
+ const secondaryDashed = count === 'at_least_one' ? true : primaryDashed;
83
+ return { lineCount: 2, primaryDashed, secondaryDashed, strokeWidth: width };
84
+ }
85
+
86
+ // ── Human-readable labels (UI concern) ────────────────────────────────
87
+
88
+ export function presenceLabel(p: PartitivePresence): string {
89
+ return p === 'optional' ? 'Optional' : 'Required';
90
+ }
91
+
92
+ export function countLabel(c: PartitiveCount): string {
93
+ switch (c) {
94
+ case 'exactly_one': return 'Exactly one';
95
+ case 'at_least_one': return 'At least one';
96
+ case 'multiple': return 'Multiple';
97
+ default: return '';
98
+ }
99
+ }
@@ -1,23 +1,28 @@
1
1
  /**
2
- * Partitive relation styling — colors for one-to-many decompositions.
2
+ * Partitive relation styling — colors and labels for one-to-many
3
+ * partitive decompositions.
3
4
  *
4
5
  * Independent of relationship-categories.ts (which handles binary edges).
5
- * Partitive relations have their own visual language:
6
- * - completeness (complete/partial) affects opacity
7
- * - plurality (type-shared, uncertain) adds accent badges
6
+ * The visual encoding for PartitiveRelation comes from two orthogonal
7
+ * axes per ISO 704:2022:
8
8
  *
9
- * v2 redesign: replaces v1 hyperedge-styling.ts per
10
- * concept-model/TODO.partitive-relation-v2.
9
+ * 1. completeness (complete / partial) — relation-level metadata,
10
+ * affects overall opacity. Partial relations have a continued
11
+ * backline ("more exist but aren't shown").
12
+ * 2. multiplicity + is_delimiting (per-member) — see
13
+ * partitive-multiplicity.ts for the registry + rakeStrokeStyle().
14
+ *
15
+ * This module owns:
16
+ * - relation-level color/badge (completeness-driven)
17
+ * - human-readable labels for badges
18
+ *
19
+ * It does NOT encode multiplicity visuals — those live in
20
+ * partitive-multiplicity.ts (separated so renderers can consume the
21
+ * pure rakeStrokeStyle() helper without pulling badge strings).
11
22
  */
12
23
 
13
- import type { TypeSharedPluralityWire } from '../adapters/types';
14
-
15
24
  const BASE_LIGHT = '#0d9488';
16
25
  const BASE_DARK = '#2dd4bf';
17
- const ACCENT_SHARED_LIGHT = '#3b82f6';
18
- const ACCENT_SHARED_DARK = '#60a5fa';
19
- const ACCENT_UNCERTAIN_LIGHT = '#f59e0b';
20
- const ACCENT_UNCERTAIN_DARK = '#fbbf24';
21
26
 
22
27
  export interface PartitiveRelationStyle {
23
28
  color: string;
@@ -25,38 +30,24 @@ export interface PartitiveRelationStyle {
25
30
  opacity: number;
26
31
  }
27
32
 
33
+ /**
34
+ * Relation-level style (color + opacity) driven by completeness.
35
+ *
36
+ * Per-member multiplicity + delimiting rendering is via
37
+ * `rakeStrokeStyle()` from partitive-multiplicity.ts, not here —
38
+ * this helper is for the relation's overall frame + header badges.
39
+ */
28
40
  export function partitiveRelationStyle(
29
41
  completeness: 'complete' | 'partial',
30
- plurality: TypeSharedPluralityWire | null,
31
42
  isDark: boolean,
32
43
  ): PartitiveRelationStyle {
33
- const base = isDark ? BASE_DARK : BASE_LIGHT;
34
- const opacity = completeness === 'partial' ? 0.6 : 1.0;
35
-
36
- let badgeClass = 'badge-teal';
37
- if (plurality?.isShared) {
38
- badgeClass = 'badge-blue';
39
- if (plurality.isUncertain) {
40
- badgeClass = 'badge-yellow';
41
- }
42
- }
43
-
44
- return { color: base, badgeClass, opacity };
45
- }
46
-
47
- export function pluralityColor(
48
- plurality: TypeSharedPluralityWire,
49
- isDark: boolean,
50
- ): string {
51
- if (plurality.isUncertain) return isDark ? ACCENT_UNCERTAIN_DARK : ACCENT_UNCERTAIN_LIGHT;
52
- if (plurality.isShared) return isDark ? ACCENT_SHARED_DARK : ACCENT_SHARED_LIGHT;
53
- return isDark ? BASE_DARK : BASE_LIGHT;
44
+ return {
45
+ color: isDark ? BASE_DARK : BASE_LIGHT,
46
+ badgeClass: 'badge-teal',
47
+ opacity: completeness === 'partial' ? 0.6 : 1.0,
48
+ };
54
49
  }
55
50
 
56
51
  export function completenessLabel(completeness: 'complete' | 'partial'): string {
57
52
  return completeness === 'partial' ? 'Partial' : 'Complete';
58
53
  }
59
-
60
- export function certaintyLabel(certainty: 'confirmed' | 'possible'): string {
61
- return certainty === 'possible' ? 'Possible' : 'Confirmed';
62
- }
@@ -5,6 +5,8 @@ import { useVocabularyStore } from '../stores/vocabulary';
5
5
  import { conceptUri } from '../adapters/model-bridge';
6
6
  import { UriRouter } from '../adapters/UriRouter';
7
7
  import type { PartitiveRelationWire, PartitiveMemberWire } from '../adapters/types';
8
+ import type { PartitiveMember } from 'glossarist/models';
9
+ import type { PartitivePresence, PartitiveCount } from '../utils/partitive-multiplicity';
8
10
  import ConceptDetail from '../components/ConceptDetail.vue';
9
11
  import RelationSphere from '../components/RelationSphere.vue';
10
12
  import ShortcutsModal from '../components/ShortcutsModal.vue';
@@ -80,15 +82,15 @@ const partitiveRelationsForSphere = computed<PartitiveRelationWire[]>(() => {
80
82
  : null;
81
83
  if (!compRef.id || !compTarget) continue;
82
84
  const partitives: PartitiveMemberWire[] = rel.partitives
83
- .map(member => {
85
+ .map((member): PartitiveMemberWire | null => {
84
86
  const ref = member.ref;
85
87
  if (!ref?.id) return null;
86
- const targetRegister = store.manifests.has(activeRegisterId.value)
87
- ? activeRegisterId.value
88
- : activeRegisterId.value;
89
- const uri = UriRouter.buildConceptUri(m.uriBase, targetRegister, ref.id);
88
+ const uri = UriRouter.buildConceptUri(m.uriBase, activeRegisterId.value, ref.id);
90
89
  if (uri === sourceUri) return null;
91
- return { uri, certainty: member.certainty };
90
+ const presence: PartitivePresence = member.presence;
91
+ const count: PartitiveCount = member.count;
92
+ const isDelimiting = member.isDelimiting;
93
+ return { uri, presence, count, isDelimiting };
92
94
  })
93
95
  .filter((x): x is PartitiveMemberWire => x !== null);
94
96
  if (partitives.length === 0) continue;
@@ -97,13 +99,6 @@ const partitiveRelationsForSphere = computed<PartitiveRelationWire[]>(() => {
97
99
  comprehensive: compTarget,
98
100
  partitives,
99
101
  completeness: rel.completeness,
100
- plurality: rel.plurality
101
- ? {
102
- isShared: rel.plurality.isShared,
103
- isUncertain: rel.plurality.isUncertain,
104
- sharedType: null,
105
- }
106
- : null,
107
102
  register: activeRegisterId.value,
108
103
  });
109
104
  }