@glossarist/concept-browser 0.7.76 → 0.7.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/astro.config.mjs CHANGED
@@ -20,6 +20,7 @@ export default defineConfig({
20
20
  base,
21
21
  output: 'static',
22
22
  publicDir: resolve(process.cwd(), 'public'),
23
+ outDir: resolve(process.cwd(), 'dist'),
23
24
  integrations: [
24
25
  vue({ appEntrypoint: '/src/islands/app-entry' }),
25
26
  sitemap(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.76",
3
+ "version": "0.7.78",
4
4
  "description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,8 @@
1
+ function buildConceptUri(uriBase, registerId, conceptId) {
2
+ return `${uriBase}/${registerId}/concept/${conceptId}`;
3
+ }
4
+
5
+
1
6
  /**
2
7
  * Data bridge: converts public/data/*.json into Astro content collections.
3
8
  *
@@ -77,7 +82,7 @@ for (const reg of registry) {
77
82
  const conceptEntry = {
78
83
  registerId: id,
79
84
  conceptId: concept.id,
80
- uri: `${entry.uriBase || ''}/${id}/concept/${concept.id}`,
85
+ uri: buildConceptUri(entry.uriBase || '', id, concept.id),
81
86
  status: concept.status || 'valid',
82
87
  designations: concept.designations || {},
83
88
  eng: concept.eng || concept.designations?.eng || '',
@@ -1,3 +1,27 @@
1
+ function buildConceptUri(uriBase, registerId, conceptId) {
2
+ return `${uriBase}/${registerId}/concept/${conceptId}`;
3
+ }
4
+
5
+ const VALID_MARKERS = new Set(['double', 'dashed']);
6
+
7
+ function validateHyperedgeMarkers(markers) {
8
+ const out = [];
9
+ for (const m of markers) {
10
+ if (!VALID_MARKERS.has(m)) {
11
+ throw new Error(`Invalid partitive hyperedge marker: "${m}". Allowed: double, dashed`);
12
+ }
13
+ out.push(m);
14
+ }
15
+ return out;
16
+ }
17
+
18
+ function normalizeHyperedgeContent(content) {
19
+ if (content == null) return undefined;
20
+ if (typeof content === 'string') return { default: content };
21
+ return content;
22
+ }
23
+
24
+
1
25
  /**
2
26
  * Pre-computes cross-reference and domain edges for each dataset.
3
27
  * Reads all concept JSON files, extracts structured references and
@@ -108,7 +132,7 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
108
132
  const id = ref['gl:id'] || ref['id'];
109
133
  if (!source || !id) continue;
110
134
  const reg = urnMap.get(source) || source;
111
- const target = `${uriBase}/${reg}/concept/${id}`;
135
+ const target = buildConceptUri(uriBase, reg, id);
112
136
  if (target === sourceUri) continue;
113
137
  edges.push({
114
138
  source: sourceUri,
@@ -121,6 +145,52 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
121
145
  return edges;
122
146
  }
123
147
 
148
+ /**
149
+ * Extracts partitive hyperedges from a concept's gl:partitiveHyperedges
150
+ * array. Returns hyperedge objects (NOT binary GraphEdges) so the UI
151
+ * can render them with their full set-based semantics: comprehensive +
152
+ * parts together, enumeration completeness (open vs closed), and
153
+ * diagram plurality markers (double / dashed).
154
+ *
155
+ * Hyperedges are written to a separate hyperedges.json file, keeping
156
+ * edges.json binary-only.
157
+ */
158
+ function extractPartitiveHyperedges(concept, registerId, uriBase, urnMap) {
159
+ const hyperedges = [];
160
+ const sourceUri = concept['@id'];
161
+ for (const he of concept['gl:partitiveHyperedges'] || []) {
162
+ const compRef = he['gl:comprehensive'];
163
+ if (!compRef) continue;
164
+ const comprehensive = resolveConceptUri(compRef, uriBase, urnMap);
165
+ if (!comprehensive) continue;
166
+
167
+ const parts = (he['gl:hasPart'] || [])
168
+ .map(p => resolveConceptUri(p, uriBase, urnMap))
169
+ .filter(p => p && p !== sourceUri);
170
+
171
+ if (parts.length === 0) continue;
172
+
173
+ hyperedges.push({
174
+ source: sourceUri,
175
+ comprehensive,
176
+ parts,
177
+ enumeration: he['gl:enumeration'] || 'closed',
178
+ markers: validateHyperedgeMarkers(he['gl:hasPluralityMarker'] || []),
179
+ label: normalizeHyperedgeContent(he['gl:content']),
180
+ register: registerId,
181
+ });
182
+ }
183
+ return hyperedges;
184
+ }
185
+
186
+ function resolveConceptUri(ref, uriBase, urnMap) {
187
+ const source = ref['gl:source'] || ref['source'];
188
+ const id = ref['gl:id'] || ref['id'];
189
+ if (!source || !id) return null;
190
+ const reg = urnMap.get(source) || source;
191
+ return buildConceptUri(uriBase, reg, id);
192
+ }
193
+
124
194
  const EXTRACTORS = [extractReferences, extractRelated, extractDomains];
125
195
 
126
196
  function extractAllEdges(concept, registerId, uriBase, urnMap) {
@@ -140,6 +210,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
140
210
  console.log(` Processing ${files.length} concepts...`);
141
211
 
142
212
  const allEdges = [];
213
+ const allHyperedges = [];
143
214
  const allSourceRefs = [];
144
215
  const domainConceptCount = new Map();
145
216
  let processed = 0;
@@ -149,6 +220,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
149
220
  const data = JSON.parse(readFileSync(join(conceptsDir, file), 'utf-8'));
150
221
  const edges = extractAllEdges(data, registerId, uriBase, urnMap);
151
222
  allEdges.push(...edges);
223
+ allHyperedges.push(...extractPartitiveHyperedges(data, registerId, uriBase, urnMap));
152
224
  allSourceRefs.push(...extractSourceRefs(data, registerId));
153
225
 
154
226
  for (const edge of edges) {
@@ -186,6 +258,17 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
186
258
  writeFileSync(outputPath, JSON.stringify(output));
187
259
  console.log(` Written ${deduped.length} edges to edges.json (${(JSON.stringify(output).length / 1024).toFixed(1)} KB)`);
188
260
 
261
+ // Write hyperedges.json (parallel to edges.json) for one-to-many
262
+ // partitive decompositions. See TODO.hyperedge/00-design-overview.md.
263
+ const hyperedgesOutput = {
264
+ registerId,
265
+ hyperedgeCount: allHyperedges.length,
266
+ hyperedges: allHyperedges,
267
+ };
268
+ const hyperedgesPath = join(datasetDir, 'hyperedges.json');
269
+ writeFileSync(hyperedgesPath, JSON.stringify(hyperedgesOutput));
270
+ console.log(` Written ${allHyperedges.length} hyperedges to hyperedges.json (${(JSON.stringify(hyperedgesOutput).length / 1024).toFixed(1)} KB)`);
271
+
189
272
  // Build domain-nodes.json from manifest sections (authoritative source)
190
273
  const manifestSections = manifest.sections;
191
274
  if (manifestSections && manifestSections.length > 0) {
@@ -14,6 +14,11 @@ import { buildVersionHistoryTurtle } from './lib/version-turtle.mjs';
14
14
  import { buildBibliographyTurtle } from './lib/bibliography-turtle.mjs';
15
15
  import { ttlLit } from './lib/turtle-escape.mjs';
16
16
  import { firstNonEmpty } from './lib/first-non-empty.mjs';
17
+ function buildConceptUri(uriBase, registerId, conceptId) {
18
+ return `${uriBase}/${registerId}/concept/${conceptId}`;
19
+ }
20
+
21
+
17
22
  const __dirname = path.dirname(new URL(import.meta.url).pathname);
18
23
  const ROOT = process.cwd();
19
24
  const PUBLIC = path.join(ROOT, 'public');
@@ -60,6 +65,7 @@ function loadConceptFile(filePath) {
60
65
 
61
66
  // Managed concept-level fields
62
67
  if (mc.related) result._related = mc.related;
68
+ if (mc.partitive_hyperedges) result._partitiveHyperedges = mc.partitive_hyperedges;
63
69
  if (mc.data.domains) result._domains = mc.data.domains;
64
70
  if (mc.dates) result._dates = mc.dates;
65
71
  if (mc.sources) result._sources = mc.sources;
@@ -98,14 +104,14 @@ async function writeDatasetRdf(register, manifest, concepts, refMaps, opts) {
98
104
  const datasetIri = `${uriBase}/${register}/`;
99
105
  const topConceptUris = concepts
100
106
  .slice(0, 32)
101
- .map(c => `${uriBase}/${register}/concept/${c.id}`);
107
+ .map(c => buildConceptUri(uriBase, register, c.id));
102
108
 
103
109
  const sections = (manifest.sections ?? []).map(section => {
104
110
  const sectionId = section.id ?? section.slug ?? section.title;
105
111
  return {
106
112
  collectionIri: `${uriBase}/${register}/section/${sectionId}`,
107
113
  title: section.title ?? section.name ?? sectionId,
108
- memberUris: (section.members ?? []).map(id => `${uriBase}/${register}/concept/${id}`),
114
+ memberUris: (section.members ?? []).map(id => buildConceptUri(uriBase, register, id)),
109
115
  };
110
116
  });
111
117
 
@@ -242,15 +248,16 @@ function defsToJsonLd(defs) {
242
248
  .filter(d => d['gl:content']);
243
249
  }
244
250
 
245
- function refToJsonLd(ref) {
251
+ function refToJsonLd(ref, typeName = 'gl:Ref') {
246
252
  if (!ref) return undefined;
247
- const refObj = { '@type': 'gl:Ref' };
253
+ const refObj = { '@type': typeName };
248
254
  if (typeof ref === 'string') {
249
255
  refObj['gl:source'] = ref;
250
256
  } else {
251
257
  if (ref.source) refObj['gl:source'] = ref.source;
252
258
  if (ref.id) refObj['gl:id'] = ref.id;
253
259
  if (ref.version) refObj['gl:version'] = ref.version;
260
+ if (ref.text) refObj['gl:text'] = ref.text;
254
261
  }
255
262
  return refObj;
256
263
  }
@@ -366,12 +373,12 @@ function buildPatternIndex(datasets, registerCache) {
366
373
 
367
374
  function resolveRefUri(term, refMaps) {
368
375
  const resolved = refMaps.patternIndex.resolve(term);
369
- if (resolved) return `${refMaps.uriBase}/${resolved.datasetId}/concept/${resolved.conceptId}`;
376
+ if (resolved) return buildConceptUri(refMaps.uriBase, resolved.datasetId, resolved.conceptId);
370
377
 
371
378
  const ievMatch = term.match(/^IEV:(\d+[-\d]+)$/);
372
379
  if (ievMatch) {
373
380
  const dsId = refMaps.refPrefixMap['IEV'];
374
- if (dsId) return `${refMaps.uriBase}/${dsId}/concept/${ievMatch[1]}`;
381
+ if (dsId) return buildConceptUri(refMaps.uriBase, dsId, ievMatch[1]);
375
382
  }
376
383
  return null;
377
384
  }
@@ -405,7 +412,7 @@ function resolveIevRef(display, term, refPrefixMap, uriBase) {
405
412
  if (!display.startsWith('IEV:')) return null;
406
413
  const datasetId = refPrefixMap['IEV'];
407
414
  if (!datasetId) return null;
408
- return { id: `${uriBase}/${datasetId}/concept/${display.slice(4)}`, term };
415
+ return { id: buildConceptUri(uriBase, datasetId, display.slice(4)), term };
409
416
  }
410
417
 
411
418
  /**
@@ -414,7 +421,7 @@ function resolveIevRef(display, term, refPrefixMap, uriBase) {
414
421
  function resolvePatternRef(identifier, display, refPrefixMap, patternIndex, uriBase) {
415
422
  const r = patternIndex.resolve(identifier);
416
423
  if (!r) return null;
417
- return { id: `${uriBase}/${r.datasetId}/concept/${r.conceptId}`, term: display };
424
+ return { id: buildConceptUri(uriBase, r.datasetId, r.conceptId), term: display };
418
425
  }
419
426
 
420
427
  /**
@@ -444,7 +451,7 @@ function handleCiteRef(parsed, allSources) {
444
451
  function handleNumeric(parsed, register, uriBase) {
445
452
  if (!register) return null;
446
453
  const term = parsed.label ?? parsed.id;
447
- return { id: `${uriBase}/${register}/concept/${parsed.id}`, term };
454
+ return { id: buildConceptUri(uriBase, register, parsed.id), term };
448
455
  }
449
456
 
450
457
  /**
@@ -459,7 +466,7 @@ function handleDesignation(parsed, refMaps) {
459
466
  const display = parsed.label ?? designation;
460
467
  const conceptId = refMaps.designationLookup?.get(designation.toLowerCase());
461
468
  return {
462
- id: `${refMaps.uriBase}/${register}/concept/${conceptId ?? designation}`,
469
+ id: buildConceptUri(refMaps.uriBase, register, conceptId ?? designation),
463
470
  term: display,
464
471
  };
465
472
  }
@@ -485,7 +492,7 @@ function handleUnresolved(body, refMaps) {
485
492
  // Same-dataset: {{conceptId, displayTerm}} where conceptId is numeric/X.Y
486
493
  const register = refMaps.register;
487
494
  if (register && (/^\d/.test(identifier) || /^[A-Z]\.\d/.test(identifier))) {
488
- return { id: `${refMaps.uriBase}/${register}/concept/${identifier}`, term: display };
495
+ return { id: buildConceptUri(refMaps.uriBase, register, identifier), term: display };
489
496
  }
490
497
  return null;
491
498
  }
@@ -542,7 +549,7 @@ function extractInlineRefs(localizedData, refMaps, conceptSources = []) {
542
549
  const term = parsed.label ?? uri;
543
550
  const pattern = refMaps.patternIndex.resolve(uri);
544
551
  if (pattern) {
545
- ref = { id: `${refMaps.uriBase}/${pattern.datasetId}/concept/${pattern.conceptId}`, term };
552
+ ref = { id: buildConceptUri(refMaps.uriBase, pattern.datasetId, pattern.conceptId), term };
546
553
  } else {
547
554
  ref = { id: uri, term };
548
555
  }
@@ -571,7 +578,7 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
571
578
  const base = refMaps.uriBase;
572
579
  const doc = {
573
580
  '@context': 'https://glossarist.org/ns/context.jsonld',
574
- '@id': `${base}/${register}/concept/${termid}`,
581
+ '@id': buildConceptUri(base, register, termid),
575
582
  '@type': 'gl:Concept',
576
583
  'gl:identifier': termid,
577
584
  };
@@ -672,6 +679,28 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
672
679
  });
673
680
  }
674
681
 
682
+ if (conceptYaml._partitiveHyperedges?.length > 0) {
683
+ doc['gl:partitiveHyperedges'] = conceptYaml._partitiveHyperedges.map(he => {
684
+ const out = { '@type': 'gl:PartitiveHyperedge' };
685
+ if (he.comprehensive) {
686
+ out['gl:comprehensive'] = refToJsonLd(he.comprehensive, 'gl:ConceptRef');
687
+ }
688
+ if (Array.isArray(he.parts) && he.parts.length > 0) {
689
+ out['gl:hasPart'] = he.parts.map(p => refToJsonLd(p, 'gl:ConceptRef'));
690
+ }
691
+ if (he.enumeration) {
692
+ out['gl:enumeration'] = he.enumeration;
693
+ }
694
+ if (Array.isArray(he.markers) && he.markers.length > 0) {
695
+ out['gl:hasPluralityMarker'] = [...he.markers];
696
+ }
697
+ if (he.content) {
698
+ out['gl:content'] = typeof he.content === 'string' ? { default: he.content } : he.content;
699
+ }
700
+ return out;
701
+ });
702
+ }
703
+
675
704
  return doc;
676
705
  }
677
706
 
@@ -920,6 +949,61 @@ async function processDataset(dir, register, opts) {
920
949
 
921
950
  const dsRefMaps = { ...refMaps, register, designationLookup };
922
951
 
952
+ const stats = {
953
+ sourceMap: new Map(),
954
+ relTypeCounts: {},
955
+ partitiveHyperedges: { count: 0, byEnumeration: { closed: 0, open: 0 }, byMarker: { double: 0, dashed: 0, none: 0 } },
956
+ };
957
+
958
+ const STATS_PROCESSORS = [
959
+ function collectSources(cy, termid, s) {
960
+ const refs = new Set();
961
+ const allSources = [
962
+ ...(cy._sources || []),
963
+ ...opts.languages.flatMap(l => cy[l]?.sources || []),
964
+ ];
965
+ for (const src of allSources) {
966
+ const ref = src.origin?.ref?.source || src.origin?.ref?.id || 'Unknown';
967
+ refs.add(ref);
968
+ if (!s.sourceMap.has(ref)) s.sourceMap.set(ref, { ref, types: new Set(), conceptIds: [] });
969
+ if (src.type) s.sourceMap.get(ref).types.add(src.type);
970
+ }
971
+ for (const ref of refs) s.sourceMap.get(ref)?.conceptIds.push(termid);
972
+ },
973
+ function countRelationships(cy, _termid, s) {
974
+ for (const r of cy._related || []) {
975
+ const type = r.type || 'unknown';
976
+ s.relTypeCounts[type] = (s.relTypeCounts[type] || 0) + 1;
977
+ }
978
+ },
979
+ function countHyperedges(cy, _termid, s) {
980
+ for (const he of cy._partitiveHyperedges || []) {
981
+ s.partitiveHyperedges.count += 1;
982
+ const e = he.enumeration || 'closed';
983
+ s.partitiveHyperedges.byEnumeration[e] = (s.partitiveHyperedges.byEnumeration[e] || 0) + 1;
984
+ const markers = he.markers || [];
985
+ if (markers.length === 0) {
986
+ s.partitiveHyperedges.byMarker.none += 1;
987
+ } else {
988
+ for (const m of markers) {
989
+ s.partitiveHyperedges.byMarker[m] = (s.partitiveHyperedges.byMarker[m] || 0) + 1;
990
+ }
991
+ }
992
+ }
993
+ },
994
+ function countLanguages(cy, _termid, _s) {
995
+ for (const lang of opts.languages) {
996
+ const lc = cy[lang];
997
+ if (lc) {
998
+ if (lc.terms?.length > 0) langTermCounts[lang] = (langTermCounts[lang] || 0) + 1;
999
+ if (lc.definition && (Array.isArray(lc.definition) ? lc.definition.some(d => d.content) : lc.definition)) {
1000
+ langDefCounts[lang] = (langDefCounts[lang] || 0) + 1;
1001
+ }
1002
+ }
1003
+ }
1004
+ },
1005
+ ];
1006
+
923
1007
  for (let i = 0; i < files.length; i++) {
924
1008
  const file = files[i];
925
1009
  try {
@@ -930,21 +1014,17 @@ async function processDataset(dir, register, opts) {
930
1014
  const jsonld = yamlToJsonLd(conceptYaml, register, dsRefMaps);
931
1015
  writeJson(path.join(conceptsDir, `${termid}.json`), jsonld);
932
1016
 
933
- // Generate Turtle format
934
1017
  const ttlContent = conceptJsonToTurtle(jsonld);
935
1018
  fs.writeFileSync(path.join(conceptsDir, `${termid}.ttl`), ttlContent);
936
1019
 
937
- // Generate SKOS JSON-LD format
938
1020
  const skosJsonLd = conceptJsonToSkosJsonLd(jsonld);
939
1021
  fs.writeFileSync(path.join(conceptsDir, `${termid}.jsonld`), skosJsonLd);
940
1022
 
941
- // Generate TBX-XML format
942
1023
  const tbxContent = conceptJsonToTbx(jsonld);
943
1024
  if (tbxContent) {
944
1025
  fs.writeFileSync(path.join(conceptsDir, `${termid}.tbx`), tbxContent);
945
1026
  }
946
1027
 
947
- // Copy source YAML
948
1028
  fs.copyFileSync(path.join(dir, file), path.join(conceptsDir, `${termid}.yaml`));
949
1029
 
950
1030
  concepts.push({
@@ -954,16 +1034,8 @@ async function processDataset(dir, register, opts) {
954
1034
  status: conceptYaml.eng?.entry_status || 'valid',
955
1035
  });
956
1036
 
957
- for (const lang of opts.languages) {
958
- const lc = conceptYaml[lang];
959
- if (lc) {
960
- if (lc.terms && lc.terms.length > 0) {
961
- langTermCounts[lang] = (langTermCounts[lang] || 0) + 1;
962
- }
963
- if (lc.definition && (Array.isArray(lc.definition) ? lc.definition.some(d => d.content) : lc.definition)) {
964
- langDefCounts[lang] = (langDefCounts[lang] || 0) + 1;
965
- }
966
- }
1037
+ for (const processor of STATS_PROCESSORS) {
1038
+ processor(conceptYaml, termid, stats);
967
1039
  }
968
1040
  } catch (e) {
969
1041
  console.warn(` Skipping ${file}: ${e.message}`);
@@ -1072,6 +1144,24 @@ async function processDataset(dir, register, opts) {
1072
1144
  }
1073
1145
  }
1074
1146
 
1147
+ const sourceStats = Array.from(stats.sourceMap.values())
1148
+ .map(s => ({
1149
+ ref: s.ref,
1150
+ types: Array.from(s.types),
1151
+ conceptCount: s.conceptIds.length,
1152
+ conceptIds: s.conceptIds,
1153
+ }))
1154
+ .sort((a, b) => b.conceptCount - a.conceptCount);
1155
+ const totalRelationships = Object.values(stats.relTypeCounts).reduce((a, b) => a + b, 0);
1156
+
1157
+ writeJson(path.join(DATA, register, 'stats.json'), {
1158
+ sourceCount: sourceStats.length,
1159
+ sources: sourceStats,
1160
+ relationshipCount: totalRelationships,
1161
+ relationshipTypes: stats.relTypeCounts,
1162
+ partitiveHyperedges: stats.partitiveHyperedges,
1163
+ });
1164
+
1075
1165
  const manifest = {
1076
1166
  id: register,
1077
1167
  datasetUri: opts.datasetUri,
@@ -1082,6 +1172,8 @@ async function processDataset(dir, register, opts) {
1082
1172
  baseUrl: `/data/${register}`,
1083
1173
  languages: opts.languages,
1084
1174
  conceptCount: concepts.length,
1175
+ sourceCount: sourceStats.length,
1176
+ relationshipCount: totalRelationships,
1085
1177
  conceptUrlTemplate: '{baseUrl}/concepts/{conceptId}.json',
1086
1178
  indexUrl: '{baseUrl}/index.json',
1087
1179
  contextUrl: 'https://glossarist.org/ns/context.jsonld',
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { hyperedgeStyle, markerColor, enumerationLabel } from '../utils/hyperedge-styling';
3
+
4
+ describe('hyperedge-styling', () => {
5
+ describe('hyperedgeStyle', () => {
6
+ it('returns full opacity for closed enumeration', () => {
7
+ expect(hyperedgeStyle('closed', [], false).opacity).toBe(1.0);
8
+ });
9
+
10
+ it('returns reduced opacity for open enumeration', () => {
11
+ expect(hyperedgeStyle('open', [], false).opacity).toBe(0.6);
12
+ });
13
+
14
+ it('uses teal badge class for closed with no markers', () => {
15
+ expect(hyperedgeStyle('closed', [], false).badgeClass).toBe('badge-teal');
16
+ });
17
+
18
+ it('uses blue badge class for double marker', () => {
19
+ expect(hyperedgeStyle('closed', ['double'], false).badgeClass).toBe('badge-blue');
20
+ });
21
+
22
+ it('uses yellow badge class for dashed marker', () => {
23
+ expect(hyperedgeStyle('closed', ['dashed'], false).badgeClass).toBe('badge-yellow');
24
+ });
25
+
26
+ it('returns different colors in dark mode', () => {
27
+ const light = hyperedgeStyle('closed', [], false);
28
+ const dark = hyperedgeStyle('closed', [], true);
29
+ expect(light.color).not.toBe(dark.color);
30
+ });
31
+ });
32
+
33
+ describe('markerColor', () => {
34
+ it('returns blue for double marker', () => {
35
+ expect(markerColor('double', false)).toBe('#3b82f6');
36
+ });
37
+
38
+ it('returns amber for dashed marker', () => {
39
+ expect(markerColor('dashed', false)).toBe('#f59e0b');
40
+ });
41
+
42
+ it('returns dark-mode variants', () => {
43
+ expect(markerColor('double', true)).toBe('#60a5fa');
44
+ expect(markerColor('dashed', true)).toBe('#fbbf24');
45
+ });
46
+ });
47
+
48
+ describe('enumerationLabel', () => {
49
+ it('labels closed as "Closed (complete)"', () => {
50
+ expect(enumerationLabel('closed')).toBe('Closed (complete)');
51
+ });
52
+
53
+ it('labels open as "Open (partial)"', () => {
54
+ expect(enumerationLabel('open')).toBe('Open (partial)');
55
+ });
56
+ });
57
+ });