@broberg/bodymap 0.1.2 → 0.1.4

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/dist/react.cjs CHANGED
@@ -41,6 +41,9 @@ var REGIONS = [
41
41
  ];
42
42
  var REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));
43
43
  REGIONS.map((r) => r.key);
44
+ function getRegion(key) {
45
+ return REGIONS.find((r) => r.key === key);
46
+ }
44
47
  var PAIN_TYPES = ["stikkende", "dump", "konstant", "jagende"];
45
48
  var painPointSchema = zod.z.object({
46
49
  region: zod.z.string().refine((k) => REGION_KEY_SET.has(k), { message: "unknown region" }),
@@ -57,6 +60,18 @@ function isSelectable(key, config = {}) {
57
60
  if (s?.visible === false) return false;
58
61
  return s?.selectable ?? true;
59
62
  }
63
+ var defaultPalette = {
64
+ body: "#d2d7de",
65
+ hover: "#8fd0cd",
66
+ selected: "#5cc4b7",
67
+ heat: { low: "#fcd34d", mid: "#fb923c", high: "#ef4444" }
68
+ };
69
+ function heatFor(intensity, palette = defaultPalette) {
70
+ return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;
71
+ }
72
+ function baseColorFor(regionKey, palette = defaultPalette) {
73
+ return palette.regions?.[regionKey] ?? palette.body;
74
+ }
60
75
  zod.z.object({
61
76
  schema: zod.z.literal("bodymap/v1"),
62
77
  view: zod.z.enum(["front", "back", "left", "right"]),
@@ -167,6 +182,84 @@ var FILL_BACK = {
167
182
  hip_left: "#c99bd6",
168
183
  hip_right: "#c99bd6"
169
184
  };
185
+ var daRegions = Object.fromEntries(REGIONS.map((r) => [r.key, r.label]));
186
+ var EN_CODE = {
187
+ HEAD: "Head",
188
+ NECK: "Neck",
189
+ CHEST: "Chest",
190
+ THORA: "Upper back",
191
+ LUMBAR: "Lower back",
192
+ GROIN: "Groin",
193
+ SHOULDER: "Shoulder",
194
+ UARM: "Upper arm",
195
+ ELBOW: "Elbow",
196
+ FARM: "Forearm",
197
+ WRIST: "Wrist",
198
+ HAND: "Hand",
199
+ HIP: "Hip",
200
+ THIGH: "Thigh",
201
+ KNEE: "Knee",
202
+ LOWLEG: "Lower leg",
203
+ ANKLE: "Ankle",
204
+ FOOT: "Foot"
205
+ };
206
+ var enRegions = Object.fromEntries(
207
+ REGIONS.map((r) => [r.key, EN_CODE[r.code] + (r.side ? r.side === "left" ? ", left" : ", right" : "")])
208
+ );
209
+ var LABELS_DA = {
210
+ regions: daRegions,
211
+ qualities: { stikkende: "stikkende", dump: "dump", konstant: "konstant", jagende: "jagende" },
212
+ intensity: "Intensitet (0-10)",
213
+ quality: "Kvalitet",
214
+ remove: "Fjern punkt",
215
+ front: "Forfra",
216
+ back: "Bagfra",
217
+ empty: "V\xE6lg en kropsdel for at markere smerte.",
218
+ before: "F\xF8r",
219
+ after: "Efter",
220
+ noChange: "Ingen \xE6ndring",
221
+ change: { new: "nyt", resolved: "forsvundet", improved: "bedre", worse: "v\xE6rre" },
222
+ ariaMarked: (n, i, q) => `${n}, smerte ${i} af 10${q ? ", " + q : ""}`,
223
+ ariaUnmarked: (n) => `${n}, ikke markeret. Aktiv\xE9r for at markere smerte.`,
224
+ svgLabel: "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
225
+ viewLabel: "Visning",
226
+ zoomLabel: "Zoom",
227
+ zoomIn: "Zoom ind",
228
+ zoomOut: "Zoom ud",
229
+ zoomReset: "Nulstil zoom"
230
+ };
231
+ var LABELS_EN = {
232
+ regions: enRegions,
233
+ qualities: { stikkende: "stabbing", dump: "dull", konstant: "constant", jagende: "shooting" },
234
+ intensity: "Intensity (0-10)",
235
+ quality: "Quality",
236
+ remove: "Remove point",
237
+ front: "Front",
238
+ back: "Back",
239
+ empty: "Pick a body part to mark pain.",
240
+ before: "Before",
241
+ after: "After",
242
+ noChange: "No change",
243
+ change: { new: "new", resolved: "resolved", improved: "improved", worse: "worse" },
244
+ ariaMarked: (n, i, q) => `${n}, pain ${i} of 10${q ? ", " + q : ""}`,
245
+ ariaUnmarked: (n) => `${n}, not marked. Activate to mark pain.`,
246
+ svgLabel: "Body map \u2014 pick where it hurts",
247
+ viewLabel: "View",
248
+ zoomLabel: "Zoom",
249
+ zoomIn: "Zoom in",
250
+ zoomOut: "Zoom out",
251
+ zoomReset: "Reset zoom"
252
+ };
253
+ function resolveLabels(locale, overrides) {
254
+ const base = locale === "en" ? LABELS_EN : LABELS_DA;
255
+ if (!overrides) return base;
256
+ return {
257
+ ...base,
258
+ ...overrides,
259
+ regions: { ...base.regions, ...overrides.regions },
260
+ qualities: { ...base.qualities, ...overrides.qualities }
261
+ };
262
+ }
170
263
  var VBW = 260;
171
264
  var VBH = 470;
172
265
  var MIN_HIT = 18;
@@ -186,6 +279,15 @@ function hitShape(s) {
186
279
  function heat(v) {
187
280
  return v >= 7 ? "#ef4444" : v >= 4 ? "#fb923c" : "#fcd34d";
188
281
  }
282
+ function heatTier(v) {
283
+ return v >= 7 ? "high" : v >= 4 ? "mid" : "low";
284
+ }
285
+ function baseFill(key, rainbow, palette) {
286
+ return palette ? baseColorFor(key, palette) : `var(--bmap-region-${key}, var(--bmap-body, ${rainbow}))`;
287
+ }
288
+ function markedFill(v, palette) {
289
+ return palette ? heatFor(v, palette) : `var(--bmap-heat-${heatTier(v)}, ${heat(v)})`;
290
+ }
189
291
  function shapeEl(s, props) {
190
292
  if (s.el === "circle") return /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...props });
191
293
  if (s.el === "ellipse") return /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...props });
@@ -198,7 +300,7 @@ var STYLE = `
198
300
  .bmap__stage{display:flex;flex-direction:column;gap:10px;flex:0 1 340px;min-width:0;max-width:360px}
199
301
  .bmap__bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
200
302
  .bmap__viewtoggle{display:inline-flex;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}
201
- .bmap__vbtn{font:inherit;font-size:13px;font-weight:600;color:var(--bmap-muted);background:none;border:0;border-radius:7px;padding:8px 16px;cursor:pointer;transition:color .12s,background .12s,transform .1s}
303
+ .bmap__vbtn{font:inherit;font-size:13px;font-weight:600;color:#556274;background:none;border:0;border-radius:7px;padding:8px 16px;cursor:pointer;transition:color .12s,background .12s,transform .1s}
202
304
  .bmap__vbtn:hover{color:var(--bmap-ink)}
203
305
  .bmap__vbtn:active{transform:scale(.97)}
204
306
  .bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}
@@ -235,6 +337,23 @@ var STYLE = `
235
337
  .bmap__rm:hover{background:#fef2f2}
236
338
  .bmap__rm:active{transform:scale(.97)}
237
339
  .bmap__i:focus-visible,.bmap__chip:focus-visible,.bmap__vbtn:focus-visible,.bmap__rm:focus-visible,.bmap__zoom button:focus-visible{outline:2px solid var(--bmap-accent);outline-offset:2px}
340
+ .bmapc{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-ink:#1e293b;--bmap-muted:#64748b;font:15px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--bmap-ink)}
341
+ .bmapc__bodies{display:flex;gap:14px;flex-wrap:wrap}
342
+ .bmapc__fig{flex:1 1 150px;min-width:130px;max-width:220px;margin:0;text-align:center}
343
+ .bmapc__cap{font-size:12px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#556274;margin-bottom:6px}
344
+ .bmapc__svg{width:100%;height:auto;display:block}
345
+ .bmapc__vis{stroke:#fff;stroke-width:2.4}
346
+ .bmapc__n{font:700 12px ui-sans-serif;fill:#fff;pointer-events:none}
347
+ .bmapc__delta{margin-top:16px;display:flex;flex-direction:column;gap:6px;max-width:460px}
348
+ .bmapc__row{display:flex;align-items:center;gap:10px;font-size:13.5px;border:1px solid var(--bmap-line);border-radius:9px;padding:7px 11px}
349
+ .bmapc__rname{font-weight:600;flex:1 1 auto}
350
+ .bmapc__rval{font:12px ui-monospace,monospace;color:#556274;white-space:nowrap}
351
+ .bmapc__badge{font-size:11px;font-weight:700;border-radius:6px;padding:2px 8px;white-space:nowrap}
352
+ .bmapc__badge--improved{background:#dcfce7;color:#166534}
353
+ .bmapc__badge--worse{background:#fee2e2;color:#991b1b}
354
+ .bmapc__badge--new{background:#fef3c7;color:#92400e}
355
+ .bmapc__badge--resolved{background:#e2e8f0;color:#475569}
356
+ .bmapc__empty{color:#556274;font-size:13.5px;margin-top:14px}
238
357
  @media (max-width:560px){
239
358
  .bmap{flex-direction:column;gap:14px}
240
359
  .bmap__stage{max-width:100%;width:100%;flex-basis:auto}
@@ -266,6 +385,10 @@ function BodyMap({
266
385
  config = {},
267
386
  defaultView = "front",
268
387
  onViewChange,
388
+ palette,
389
+ locale = "da",
390
+ labels,
391
+ readOnly = false,
269
392
  className
270
393
  }) {
271
394
  react.useEffect(ensureStyles, []);
@@ -277,6 +400,8 @@ function BodyMap({
277
400
  const report = value ?? internal;
278
401
  const shapes = view === "back" ? SHAPES_BACK : SHAPES;
279
402
  const fills = view === "back" ? FILL_BACK : FILL;
403
+ const L = resolveLabels(locale, labels);
404
+ const nameOf = (key) => L.regions[key] ?? getRegion(key)?.label ?? key;
280
405
  const svgRef = react.useRef(null);
281
406
  const zoomRef = react.useRef(1);
282
407
  const panRef = react.useRef({ x: 0, y: 0 });
@@ -378,14 +503,15 @@ function BodyMap({
378
503
  const region = selected ? REGIONS.find((r) => r.key === selected) : void 0;
379
504
  const current = selected ? pointOf(selected) : void 0;
380
505
  const rootCls = ["bmap", className].filter(Boolean).join(" ");
381
- const ariaFor = (r, marked, selectable = true) => {
382
- if (marked) return `${r.label}, smerte ${marked.intensity} af 10${marked.type ? ", " + marked.type : ""}`;
383
- return selectable ? `${r.label}, ikke markeret. Aktiv\xE9r for at markere smerte.` : r.label;
506
+ const rootStyle = palette ? { "--bmap-accent": palette.selected } : void 0;
507
+ const ariaFor = (r, marked, interactive = true) => {
508
+ if (marked) return L.ariaMarked(nameOf(r.key), marked.intensity, marked.type ? L.qualities[marked.type] : void 0);
509
+ return interactive ? L.ariaUnmarked(nameOf(r.key)) : nameOf(r.key);
384
510
  };
385
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rootCls, "data-testid": "bodymap-root", children: [
511
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rootCls, "data-testid": "bodymap-root", style: rootStyle, children: [
386
512
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__stage", children: [
387
513
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__bar", children: [
388
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": "Visning", children: [
514
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": L.viewLabel, children: [
389
515
  /* @__PURE__ */ jsxRuntime.jsx(
390
516
  "button",
391
517
  {
@@ -394,7 +520,7 @@ function BodyMap({
394
520
  "data-testid": "bodymap-view-front",
395
521
  "aria-pressed": view === "front",
396
522
  onClick: () => changeView("front"),
397
- children: "Forfra"
523
+ children: L.front
398
524
  }
399
525
  ),
400
526
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -405,14 +531,14 @@ function BodyMap({
405
531
  "data-testid": "bodymap-view-back",
406
532
  "aria-pressed": view === "back",
407
533
  onClick: () => changeView("back"),
408
- children: "Bagfra"
534
+ children: L.back
409
535
  }
410
536
  )
411
537
  ] }),
412
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__zoom", role: "group", "aria-label": "Zoom", children: [
413
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-out", "aria-label": "Zoom ud", disabled: zoom <= ZMIN, onClick: () => zoomBtn(1 / 1.4), children: "\u2212" }),
414
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-reset", "aria-label": "Nulstil zoom", disabled: zoom === 1 && pan.x === 0 && pan.y === 0, onClick: resetZoom, children: "\u2922" }),
415
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-in", "aria-label": "Zoom ind", disabled: zoom >= ZMAX, onClick: () => zoomBtn(1.4), children: "+" })
538
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__zoom", role: "group", "aria-label": L.zoomLabel, children: [
539
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-out", "aria-label": L.zoomOut, disabled: zoom <= ZMIN, onClick: () => zoomBtn(1 / 1.4), children: "\u2212" }),
540
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-reset", "aria-label": L.zoomReset, disabled: zoom === 1 && pan.x === 0 && pan.y === 0, onClick: resetZoom, children: "\u2922" }),
541
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-in", "aria-label": L.zoomIn, disabled: zoom >= ZMAX, onClick: () => zoomBtn(1.4), children: "+" })
416
542
  ] })
417
543
  ] }),
418
544
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -422,7 +548,7 @@ function BodyMap({
422
548
  className: "bmap__svg",
423
549
  viewBox: "0 0 260 470",
424
550
  role: "group",
425
- "aria-label": "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
551
+ "aria-label": L.svgLabel,
426
552
  onPointerDown,
427
553
  onPointerMove,
428
554
  onPointerUp,
@@ -433,21 +559,20 @@ function BodyMap({
433
559
  const s = shapes[r.key];
434
560
  if (!s) return null;
435
561
  const marked = pointOf(r.key);
436
- const selectable = isSelectable(r.key, config);
562
+ const interactive = isSelectable(r.key, config) && !readOnly;
437
563
  const visCls = ["bmap__vis", selected === r.key && "bmap__vis--sel"].filter(Boolean).join(" ");
438
- const fill = marked ? heat(marked.intensity) : fills[r.key] ?? "#cbd5e1";
564
+ const fill = marked ? markedFill(marked.intensity, palette) : baseFill(r.key, fills[r.key] ?? "#cbd5e1", palette);
439
565
  const hs = hitShape(s);
440
- const act = () => selectable ? pickRegion(r.key) : void 0;
441
566
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
442
567
  shapeEl(hs, {
443
- className: "bmap__hit" + (selectable ? "" : " bmap__hit--locked"),
568
+ className: "bmap__hit" + (interactive ? "" : " bmap__hit--locked"),
444
569
  "data-testid": `bodymap-region-${r.key}`,
445
- role: selectable ? "button" : "img",
446
- tabIndex: selectable ? 0 : -1,
447
- "aria-label": ariaFor(r, marked, selectable),
448
- "aria-pressed": selectable ? selected === r.key : void 0,
449
- onClick: act,
450
- onKeyDown: selectable ? (e) => {
570
+ role: interactive ? "button" : "img",
571
+ tabIndex: interactive ? 0 : -1,
572
+ "aria-label": ariaFor(r, marked, interactive),
573
+ "aria-pressed": interactive ? selected === r.key : void 0,
574
+ onClick: interactive ? () => pickRegion(r.key) : void 0,
575
+ onKeyDown: interactive ? (e) => {
451
576
  if (e.key === "Enter" || e.key === " ") {
452
577
  e.preventDefault();
453
578
  suppressClick.current = false;
@@ -455,7 +580,7 @@ function BodyMap({
455
580
  }
456
581
  } : void 0
457
582
  }),
458
- shapeEl(s, { className: visCls, fill, opacity: selectable ? 1 : 0.4 })
583
+ shapeEl(s, { className: visCls, fill, opacity: !readOnly && !isSelectable(r.key, config) ? 0.4 : 1 })
459
584
  ] }, r.key);
460
585
  }),
461
586
  report.map((p) => {
@@ -468,26 +593,25 @@ function BodyMap({
468
593
  }
469
594
  )
470
595
  ] }),
471
- region ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__panel", "data-testid": "bodymap-panel", children: [
596
+ readOnly ? null : region ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__panel", "data-testid": "bodymap-panel", children: [
472
597
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__ph", children: [
473
- /* @__PURE__ */ jsxRuntime.jsx("b", { children: region.label }),
598
+ /* @__PURE__ */ jsxRuntime.jsx("b", { children: nameOf(region.key) }),
474
599
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bmap__code", children: region.code })
475
600
  ] }),
476
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-intensity-lbl", children: "Intensitet (0-10)" }),
601
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-intensity-lbl", children: L.intensity }),
477
602
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__scale", role: "group", "aria-labelledby": "bmap-intensity-lbl", children: Array.from({ length: 11 }, (_, n) => /* @__PURE__ */ jsxRuntime.jsx(
478
603
  "button",
479
604
  {
480
605
  type: "button",
481
606
  className: "bmap__i" + (current?.intensity === n ? " bmap__i--on" : ""),
482
607
  "data-testid": `bodymap-intensity-${n}`,
483
- "aria-label": `Intensitet ${n}`,
484
608
  "aria-pressed": current?.intensity === n,
485
609
  onClick: () => setPain(region.key, n, current?.type),
486
610
  children: n
487
611
  },
488
612
  n
489
613
  )) }),
490
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-quality-lbl", children: "Kvalitet" }),
614
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-quality-lbl", children: L.quality }),
491
615
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__chips", role: "group", "aria-labelledby": "bmap-quality-lbl", children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsxRuntime.jsx(
492
616
  "button",
493
617
  {
@@ -496,15 +620,87 @@ function BodyMap({
496
620
  "data-testid": `bodymap-type-${t}`,
497
621
  "aria-pressed": current?.type === t,
498
622
  onClick: () => setPain(region.key, current?.intensity ?? 5, t),
499
- children: t
623
+ children: L.qualities[t]
500
624
  },
501
625
  t
502
626
  )) }),
503
- current && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "bmap__rm", "data-testid": "bodymap-remove", onClick: () => removePain(region.key), children: "Fjern punkt" })
504
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children: "V\xE6lg en kropsdel for at markere smerte." })
627
+ current && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "bmap__rm", "data-testid": "bodymap-remove", onClick: () => removePain(region.key), children: L.remove })
628
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children: L.empty })
629
+ ] });
630
+ }
631
+ function changeOf(b, a) {
632
+ if (b == null && a != null) return "new";
633
+ if (b != null && a == null) return "resolved";
634
+ if (b == null || a == null) return "same";
635
+ return a < b ? "improved" : a > b ? "worse" : "same";
636
+ }
637
+ function CompareFigure({ report, view, palette, caption, testid }) {
638
+ const shapes = view === "back" ? SHAPES_BACK : SHAPES;
639
+ const fills = view === "back" ? FILL_BACK : FILL;
640
+ return /* @__PURE__ */ jsxRuntime.jsxs("figure", { className: "bmapc__fig", "data-testid": testid, children: [
641
+ /* @__PURE__ */ jsxRuntime.jsx("figcaption", { className: "bmapc__cap", children: caption }),
642
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "bmapc__svg", viewBox: "0 0 260 470", role: "img", "aria-label": caption, children: [
643
+ REGIONS.map((r) => {
644
+ const s = shapes[r.key];
645
+ if (!s) return null;
646
+ const m = report.find((p) => p.region === r.key);
647
+ const fill = m ? markedFill(m.intensity, palette) : baseFill(r.key, fills[r.key] ?? "#cbd5e1", palette);
648
+ return /* @__PURE__ */ jsxRuntime.jsx("g", { children: shapeEl(s, { className: "bmapc__vis", fill }) }, r.key);
649
+ }),
650
+ report.map((p) => {
651
+ const s = shapes[p.region];
652
+ if (!s) return null;
653
+ const c = center(s);
654
+ return /* @__PURE__ */ jsxRuntime.jsx("text", { x: c.x, y: c.y + 4, textAnchor: "middle", className: "bmapc__n", children: p.intensity }, p.region);
655
+ })
656
+ ] })
657
+ ] });
658
+ }
659
+ function BodyMapCompare({
660
+ before,
661
+ after,
662
+ palette,
663
+ locale = "da",
664
+ labels,
665
+ defaultView = "front",
666
+ className
667
+ }) {
668
+ react.useEffect(ensureStyles, []);
669
+ const [view, setView] = react.useState(defaultView);
670
+ const L = resolveLabels(locale, labels);
671
+ const nameOf = (k) => L.regions[k] ?? getRegion(k)?.label ?? k;
672
+ const rootStyle = palette ? { "--bmap-accent": palette.selected } : void 0;
673
+ const keys = Array.from(new Set([...before, ...after].map((p) => p.region)));
674
+ const changes = keys.map((k) => ({
675
+ key: k,
676
+ b: before.find((p) => p.region === k)?.intensity,
677
+ a: after.find((p) => p.region === k)?.intensity,
678
+ status: changeOf(before.find((p) => p.region === k)?.intensity, after.find((p) => p.region === k)?.intensity)
679
+ })).filter((c) => c.status !== "same").sort((x, y) => x.key < y.key ? -1 : 1);
680
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["bmapc", className].filter(Boolean).join(" "), "data-testid": "bodymap-compare", style: rootStyle, children: [
681
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": L.viewLabel, style: { marginBottom: 12 }, children: [
682
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "bmap__vbtn" + (view === "front" ? " bmap__vbtn--on" : ""), "data-testid": "bodymap-compare-view-front", "aria-pressed": view === "front", onClick: () => setView("front"), children: L.front }),
683
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "bmap__vbtn" + (view === "back" ? " bmap__vbtn--on" : ""), "data-testid": "bodymap-compare-view-back", "aria-pressed": view === "back", onClick: () => setView("back"), children: L.back })
684
+ ] }),
685
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmapc__bodies", children: [
686
+ /* @__PURE__ */ jsxRuntime.jsx(CompareFigure, { report: before, view, palette, caption: L.before, testid: "bodymap-compare-before" }),
687
+ /* @__PURE__ */ jsxRuntime.jsx(CompareFigure, { report: after, view, palette, caption: L.after, testid: "bodymap-compare-after" })
688
+ ] }),
689
+ changes.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmapc__delta", "data-testid": "bodymap-compare-delta", children: changes.map((c) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmapc__row", "data-testid": `bodymap-compare-delta-${c.key}`, children: [
690
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bmapc__rname", children: nameOf(c.key) }),
691
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "bmapc__rval", children: [
692
+ c.b ?? "\u2013",
693
+ " \u2192 ",
694
+ c.a ?? "\u2013"
695
+ ] }),
696
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `bmapc__badge bmapc__badge--${c.status}`, children: L.change[c.status] })
697
+ ] }, c.key)) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmapc__empty", "data-testid": "bodymap-compare-delta", children: L.noChange })
505
698
  ] });
506
699
  }
507
700
 
508
701
  exports.BodyMap = BodyMap;
702
+ exports.BodyMapCompare = BodyMapCompare;
703
+ exports.LABELS_DA = LABELS_DA;
704
+ exports.LABELS_EN = LABELS_EN;
509
705
  //# sourceMappingURL=react.cjs.map
510
706
  //# sourceMappingURL=react.cjs.map