@broberg/bodymap 0.1.2 → 0.1.3

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,76 @@ 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
+ ariaMarked: (n, i, q) => `${n}, smerte ${i} af 10${q ? ", " + q : ""}`,
219
+ ariaUnmarked: (n) => `${n}, ikke markeret. Aktiv\xE9r for at markere smerte.`,
220
+ svgLabel: "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
221
+ viewLabel: "Visning",
222
+ zoomLabel: "Zoom",
223
+ zoomIn: "Zoom ind",
224
+ zoomOut: "Zoom ud",
225
+ zoomReset: "Nulstil zoom"
226
+ };
227
+ var LABELS_EN = {
228
+ regions: enRegions,
229
+ qualities: { stikkende: "stabbing", dump: "dull", konstant: "constant", jagende: "shooting" },
230
+ intensity: "Intensity (0-10)",
231
+ quality: "Quality",
232
+ remove: "Remove point",
233
+ front: "Front",
234
+ back: "Back",
235
+ empty: "Pick a body part to mark pain.",
236
+ ariaMarked: (n, i, q) => `${n}, pain ${i} of 10${q ? ", " + q : ""}`,
237
+ ariaUnmarked: (n) => `${n}, not marked. Activate to mark pain.`,
238
+ svgLabel: "Body map \u2014 pick where it hurts",
239
+ viewLabel: "View",
240
+ zoomLabel: "Zoom",
241
+ zoomIn: "Zoom in",
242
+ zoomOut: "Zoom out",
243
+ zoomReset: "Reset zoom"
244
+ };
245
+ function resolveLabels(locale, overrides) {
246
+ const base = locale === "en" ? LABELS_EN : LABELS_DA;
247
+ if (!overrides) return base;
248
+ return {
249
+ ...base,
250
+ ...overrides,
251
+ regions: { ...base.regions, ...overrides.regions },
252
+ qualities: { ...base.qualities, ...overrides.qualities }
253
+ };
254
+ }
170
255
  var VBW = 260;
171
256
  var VBH = 470;
172
257
  var MIN_HIT = 18;
@@ -186,6 +271,15 @@ function hitShape(s) {
186
271
  function heat(v) {
187
272
  return v >= 7 ? "#ef4444" : v >= 4 ? "#fb923c" : "#fcd34d";
188
273
  }
274
+ function heatTier(v) {
275
+ return v >= 7 ? "high" : v >= 4 ? "mid" : "low";
276
+ }
277
+ function baseFill(key, rainbow, palette) {
278
+ return palette ? baseColorFor(key, palette) : `var(--bmap-region-${key}, var(--bmap-body, ${rainbow}))`;
279
+ }
280
+ function markedFill(v, palette) {
281
+ return palette ? heatFor(v, palette) : `var(--bmap-heat-${heatTier(v)}, ${heat(v)})`;
282
+ }
189
283
  function shapeEl(s, props) {
190
284
  if (s.el === "circle") return /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...props });
191
285
  if (s.el === "ellipse") return /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...props });
@@ -266,6 +360,10 @@ function BodyMap({
266
360
  config = {},
267
361
  defaultView = "front",
268
362
  onViewChange,
363
+ palette,
364
+ locale = "da",
365
+ labels,
366
+ readOnly = false,
269
367
  className
270
368
  }) {
271
369
  react.useEffect(ensureStyles, []);
@@ -277,6 +375,8 @@ function BodyMap({
277
375
  const report = value ?? internal;
278
376
  const shapes = view === "back" ? SHAPES_BACK : SHAPES;
279
377
  const fills = view === "back" ? FILL_BACK : FILL;
378
+ const L = resolveLabels(locale, labels);
379
+ const nameOf = (key) => L.regions[key] ?? getRegion(key)?.label ?? key;
280
380
  const svgRef = react.useRef(null);
281
381
  const zoomRef = react.useRef(1);
282
382
  const panRef = react.useRef({ x: 0, y: 0 });
@@ -378,14 +478,15 @@ function BodyMap({
378
478
  const region = selected ? REGIONS.find((r) => r.key === selected) : void 0;
379
479
  const current = selected ? pointOf(selected) : void 0;
380
480
  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;
481
+ const rootStyle = palette ? { "--bmap-accent": palette.selected } : void 0;
482
+ const ariaFor = (r, marked, interactive = true) => {
483
+ if (marked) return L.ariaMarked(nameOf(r.key), marked.intensity, marked.type ? L.qualities[marked.type] : void 0);
484
+ return interactive ? L.ariaUnmarked(nameOf(r.key)) : nameOf(r.key);
384
485
  };
385
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rootCls, "data-testid": "bodymap-root", children: [
486
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rootCls, "data-testid": "bodymap-root", style: rootStyle, children: [
386
487
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__stage", children: [
387
488
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__bar", children: [
388
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": "Visning", children: [
489
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": L.viewLabel, children: [
389
490
  /* @__PURE__ */ jsxRuntime.jsx(
390
491
  "button",
391
492
  {
@@ -394,7 +495,7 @@ function BodyMap({
394
495
  "data-testid": "bodymap-view-front",
395
496
  "aria-pressed": view === "front",
396
497
  onClick: () => changeView("front"),
397
- children: "Forfra"
498
+ children: L.front
398
499
  }
399
500
  ),
400
501
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -405,14 +506,14 @@ function BodyMap({
405
506
  "data-testid": "bodymap-view-back",
406
507
  "aria-pressed": view === "back",
407
508
  onClick: () => changeView("back"),
408
- children: "Bagfra"
509
+ children: L.back
409
510
  }
410
511
  )
411
512
  ] }),
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: "+" })
513
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__zoom", role: "group", "aria-label": L.zoomLabel, children: [
514
+ /* @__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" }),
515
+ /* @__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" }),
516
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-in", "aria-label": L.zoomIn, disabled: zoom >= ZMAX, onClick: () => zoomBtn(1.4), children: "+" })
416
517
  ] })
417
518
  ] }),
418
519
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -422,7 +523,7 @@ function BodyMap({
422
523
  className: "bmap__svg",
423
524
  viewBox: "0 0 260 470",
424
525
  role: "group",
425
- "aria-label": "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
526
+ "aria-label": L.svgLabel,
426
527
  onPointerDown,
427
528
  onPointerMove,
428
529
  onPointerUp,
@@ -433,21 +534,20 @@ function BodyMap({
433
534
  const s = shapes[r.key];
434
535
  if (!s) return null;
435
536
  const marked = pointOf(r.key);
436
- const selectable = isSelectable(r.key, config);
537
+ const interactive = isSelectable(r.key, config) && !readOnly;
437
538
  const visCls = ["bmap__vis", selected === r.key && "bmap__vis--sel"].filter(Boolean).join(" ");
438
- const fill = marked ? heat(marked.intensity) : fills[r.key] ?? "#cbd5e1";
539
+ const fill = marked ? markedFill(marked.intensity, palette) : baseFill(r.key, fills[r.key] ?? "#cbd5e1", palette);
439
540
  const hs = hitShape(s);
440
- const act = () => selectable ? pickRegion(r.key) : void 0;
441
541
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
442
542
  shapeEl(hs, {
443
- className: "bmap__hit" + (selectable ? "" : " bmap__hit--locked"),
543
+ className: "bmap__hit" + (interactive ? "" : " bmap__hit--locked"),
444
544
  "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) => {
545
+ role: interactive ? "button" : "img",
546
+ tabIndex: interactive ? 0 : -1,
547
+ "aria-label": ariaFor(r, marked, interactive),
548
+ "aria-pressed": interactive ? selected === r.key : void 0,
549
+ onClick: interactive ? () => pickRegion(r.key) : void 0,
550
+ onKeyDown: interactive ? (e) => {
451
551
  if (e.key === "Enter" || e.key === " ") {
452
552
  e.preventDefault();
453
553
  suppressClick.current = false;
@@ -455,7 +555,7 @@ function BodyMap({
455
555
  }
456
556
  } : void 0
457
557
  }),
458
- shapeEl(s, { className: visCls, fill, opacity: selectable ? 1 : 0.4 })
558
+ shapeEl(s, { className: visCls, fill, opacity: !readOnly && !isSelectable(r.key, config) ? 0.4 : 1 })
459
559
  ] }, r.key);
460
560
  }),
461
561
  report.map((p) => {
@@ -468,26 +568,25 @@ function BodyMap({
468
568
  }
469
569
  )
470
570
  ] }),
471
- region ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__panel", "data-testid": "bodymap-panel", children: [
571
+ readOnly ? null : region ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__panel", "data-testid": "bodymap-panel", children: [
472
572
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__ph", children: [
473
- /* @__PURE__ */ jsxRuntime.jsx("b", { children: region.label }),
573
+ /* @__PURE__ */ jsxRuntime.jsx("b", { children: nameOf(region.key) }),
474
574
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bmap__code", children: region.code })
475
575
  ] }),
476
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-intensity-lbl", children: "Intensitet (0-10)" }),
576
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-intensity-lbl", children: L.intensity }),
477
577
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__scale", role: "group", "aria-labelledby": "bmap-intensity-lbl", children: Array.from({ length: 11 }, (_, n) => /* @__PURE__ */ jsxRuntime.jsx(
478
578
  "button",
479
579
  {
480
580
  type: "button",
481
581
  className: "bmap__i" + (current?.intensity === n ? " bmap__i--on" : ""),
482
582
  "data-testid": `bodymap-intensity-${n}`,
483
- "aria-label": `Intensitet ${n}`,
484
583
  "aria-pressed": current?.intensity === n,
485
584
  onClick: () => setPain(region.key, n, current?.type),
486
585
  children: n
487
586
  },
488
587
  n
489
588
  )) }),
490
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-quality-lbl", children: "Kvalitet" }),
589
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-quality-lbl", children: L.quality }),
491
590
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__chips", role: "group", "aria-labelledby": "bmap-quality-lbl", children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsxRuntime.jsx(
492
591
  "button",
493
592
  {
@@ -496,15 +595,17 @@ function BodyMap({
496
595
  "data-testid": `bodymap-type-${t}`,
497
596
  "aria-pressed": current?.type === t,
498
597
  onClick: () => setPain(region.key, current?.intensity ?? 5, t),
499
- children: t
598
+ children: L.qualities[t]
500
599
  },
501
600
  t
502
601
  )) }),
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." })
602
+ current && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "bmap__rm", "data-testid": "bodymap-remove", onClick: () => removePain(region.key), children: L.remove })
603
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children: L.empty })
505
604
  ] });
506
605
  }
507
606
 
508
607
  exports.BodyMap = BodyMap;
608
+ exports.LABELS_DA = LABELS_DA;
609
+ exports.LABELS_EN = LABELS_EN;
509
610
  //# sourceMappingURL=react.cjs.map
510
611
  //# sourceMappingURL=react.cjs.map