@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.js CHANGED
@@ -39,6 +39,9 @@ var REGIONS = [
39
39
  ];
40
40
  var REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));
41
41
  REGIONS.map((r) => r.key);
42
+ function getRegion(key) {
43
+ return REGIONS.find((r) => r.key === key);
44
+ }
42
45
  var PAIN_TYPES = ["stikkende", "dump", "konstant", "jagende"];
43
46
  var painPointSchema = z.object({
44
47
  region: z.string().refine((k) => REGION_KEY_SET.has(k), { message: "unknown region" }),
@@ -55,6 +58,18 @@ function isSelectable(key, config = {}) {
55
58
  if (s?.visible === false) return false;
56
59
  return s?.selectable ?? true;
57
60
  }
61
+ var defaultPalette = {
62
+ body: "#d2d7de",
63
+ hover: "#8fd0cd",
64
+ selected: "#5cc4b7",
65
+ heat: { low: "#fcd34d", mid: "#fb923c", high: "#ef4444" }
66
+ };
67
+ function heatFor(intensity, palette = defaultPalette) {
68
+ return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;
69
+ }
70
+ function baseColorFor(regionKey, palette = defaultPalette) {
71
+ return palette.regions?.[regionKey] ?? palette.body;
72
+ }
58
73
  z.object({
59
74
  schema: z.literal("bodymap/v1"),
60
75
  view: z.enum(["front", "back", "left", "right"]),
@@ -165,6 +180,76 @@ var FILL_BACK = {
165
180
  hip_left: "#c99bd6",
166
181
  hip_right: "#c99bd6"
167
182
  };
183
+ var daRegions = Object.fromEntries(REGIONS.map((r) => [r.key, r.label]));
184
+ var EN_CODE = {
185
+ HEAD: "Head",
186
+ NECK: "Neck",
187
+ CHEST: "Chest",
188
+ THORA: "Upper back",
189
+ LUMBAR: "Lower back",
190
+ GROIN: "Groin",
191
+ SHOULDER: "Shoulder",
192
+ UARM: "Upper arm",
193
+ ELBOW: "Elbow",
194
+ FARM: "Forearm",
195
+ WRIST: "Wrist",
196
+ HAND: "Hand",
197
+ HIP: "Hip",
198
+ THIGH: "Thigh",
199
+ KNEE: "Knee",
200
+ LOWLEG: "Lower leg",
201
+ ANKLE: "Ankle",
202
+ FOOT: "Foot"
203
+ };
204
+ var enRegions = Object.fromEntries(
205
+ REGIONS.map((r) => [r.key, EN_CODE[r.code] + (r.side ? r.side === "left" ? ", left" : ", right" : "")])
206
+ );
207
+ var LABELS_DA = {
208
+ regions: daRegions,
209
+ qualities: { stikkende: "stikkende", dump: "dump", konstant: "konstant", jagende: "jagende" },
210
+ intensity: "Intensitet (0-10)",
211
+ quality: "Kvalitet",
212
+ remove: "Fjern punkt",
213
+ front: "Forfra",
214
+ back: "Bagfra",
215
+ empty: "V\xE6lg en kropsdel for at markere smerte.",
216
+ ariaMarked: (n, i, q) => `${n}, smerte ${i} af 10${q ? ", " + q : ""}`,
217
+ ariaUnmarked: (n) => `${n}, ikke markeret. Aktiv\xE9r for at markere smerte.`,
218
+ svgLabel: "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
219
+ viewLabel: "Visning",
220
+ zoomLabel: "Zoom",
221
+ zoomIn: "Zoom ind",
222
+ zoomOut: "Zoom ud",
223
+ zoomReset: "Nulstil zoom"
224
+ };
225
+ var LABELS_EN = {
226
+ regions: enRegions,
227
+ qualities: { stikkende: "stabbing", dump: "dull", konstant: "constant", jagende: "shooting" },
228
+ intensity: "Intensity (0-10)",
229
+ quality: "Quality",
230
+ remove: "Remove point",
231
+ front: "Front",
232
+ back: "Back",
233
+ empty: "Pick a body part to mark pain.",
234
+ ariaMarked: (n, i, q) => `${n}, pain ${i} of 10${q ? ", " + q : ""}`,
235
+ ariaUnmarked: (n) => `${n}, not marked. Activate to mark pain.`,
236
+ svgLabel: "Body map \u2014 pick where it hurts",
237
+ viewLabel: "View",
238
+ zoomLabel: "Zoom",
239
+ zoomIn: "Zoom in",
240
+ zoomOut: "Zoom out",
241
+ zoomReset: "Reset zoom"
242
+ };
243
+ function resolveLabels(locale, overrides) {
244
+ const base = locale === "en" ? LABELS_EN : LABELS_DA;
245
+ if (!overrides) return base;
246
+ return {
247
+ ...base,
248
+ ...overrides,
249
+ regions: { ...base.regions, ...overrides.regions },
250
+ qualities: { ...base.qualities, ...overrides.qualities }
251
+ };
252
+ }
168
253
  var VBW = 260;
169
254
  var VBH = 470;
170
255
  var MIN_HIT = 18;
@@ -184,6 +269,15 @@ function hitShape(s) {
184
269
  function heat(v) {
185
270
  return v >= 7 ? "#ef4444" : v >= 4 ? "#fb923c" : "#fcd34d";
186
271
  }
272
+ function heatTier(v) {
273
+ return v >= 7 ? "high" : v >= 4 ? "mid" : "low";
274
+ }
275
+ function baseFill(key, rainbow, palette) {
276
+ return palette ? baseColorFor(key, palette) : `var(--bmap-region-${key}, var(--bmap-body, ${rainbow}))`;
277
+ }
278
+ function markedFill(v, palette) {
279
+ return palette ? heatFor(v, palette) : `var(--bmap-heat-${heatTier(v)}, ${heat(v)})`;
280
+ }
187
281
  function shapeEl(s, props) {
188
282
  if (s.el === "circle") return /* @__PURE__ */ jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...props });
189
283
  if (s.el === "ellipse") return /* @__PURE__ */ jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...props });
@@ -264,6 +358,10 @@ function BodyMap({
264
358
  config = {},
265
359
  defaultView = "front",
266
360
  onViewChange,
361
+ palette,
362
+ locale = "da",
363
+ labels,
364
+ readOnly = false,
267
365
  className
268
366
  }) {
269
367
  useEffect(ensureStyles, []);
@@ -275,6 +373,8 @@ function BodyMap({
275
373
  const report = value ?? internal;
276
374
  const shapes = view === "back" ? SHAPES_BACK : SHAPES;
277
375
  const fills = view === "back" ? FILL_BACK : FILL;
376
+ const L = resolveLabels(locale, labels);
377
+ const nameOf = (key) => L.regions[key] ?? getRegion(key)?.label ?? key;
278
378
  const svgRef = useRef(null);
279
379
  const zoomRef = useRef(1);
280
380
  const panRef = useRef({ x: 0, y: 0 });
@@ -376,14 +476,15 @@ function BodyMap({
376
476
  const region = selected ? REGIONS.find((r) => r.key === selected) : void 0;
377
477
  const current = selected ? pointOf(selected) : void 0;
378
478
  const rootCls = ["bmap", className].filter(Boolean).join(" ");
379
- const ariaFor = (r, marked, selectable = true) => {
380
- if (marked) return `${r.label}, smerte ${marked.intensity} af 10${marked.type ? ", " + marked.type : ""}`;
381
- return selectable ? `${r.label}, ikke markeret. Aktiv\xE9r for at markere smerte.` : r.label;
479
+ const rootStyle = palette ? { "--bmap-accent": palette.selected } : void 0;
480
+ const ariaFor = (r, marked, interactive = true) => {
481
+ if (marked) return L.ariaMarked(nameOf(r.key), marked.intensity, marked.type ? L.qualities[marked.type] : void 0);
482
+ return interactive ? L.ariaUnmarked(nameOf(r.key)) : nameOf(r.key);
382
483
  };
383
- return /* @__PURE__ */ jsxs("div", { className: rootCls, "data-testid": "bodymap-root", children: [
484
+ return /* @__PURE__ */ jsxs("div", { className: rootCls, "data-testid": "bodymap-root", style: rootStyle, children: [
384
485
  /* @__PURE__ */ jsxs("div", { className: "bmap__stage", children: [
385
486
  /* @__PURE__ */ jsxs("div", { className: "bmap__bar", children: [
386
- /* @__PURE__ */ jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": "Visning", children: [
487
+ /* @__PURE__ */ jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": L.viewLabel, children: [
387
488
  /* @__PURE__ */ jsx(
388
489
  "button",
389
490
  {
@@ -392,7 +493,7 @@ function BodyMap({
392
493
  "data-testid": "bodymap-view-front",
393
494
  "aria-pressed": view === "front",
394
495
  onClick: () => changeView("front"),
395
- children: "Forfra"
496
+ children: L.front
396
497
  }
397
498
  ),
398
499
  /* @__PURE__ */ jsx(
@@ -403,14 +504,14 @@ function BodyMap({
403
504
  "data-testid": "bodymap-view-back",
404
505
  "aria-pressed": view === "back",
405
506
  onClick: () => changeView("back"),
406
- children: "Bagfra"
507
+ children: L.back
407
508
  }
408
509
  )
409
510
  ] }),
410
- /* @__PURE__ */ jsxs("div", { className: "bmap__zoom", role: "group", "aria-label": "Zoom", children: [
411
- /* @__PURE__ */ jsx("button", { type: "button", "data-testid": "bodymap-zoom-out", "aria-label": "Zoom ud", disabled: zoom <= ZMIN, onClick: () => zoomBtn(1 / 1.4), children: "\u2212" }),
412
- /* @__PURE__ */ 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" }),
413
- /* @__PURE__ */ jsx("button", { type: "button", "data-testid": "bodymap-zoom-in", "aria-label": "Zoom ind", disabled: zoom >= ZMAX, onClick: () => zoomBtn(1.4), children: "+" })
511
+ /* @__PURE__ */ jsxs("div", { className: "bmap__zoom", role: "group", "aria-label": L.zoomLabel, children: [
512
+ /* @__PURE__ */ jsx("button", { type: "button", "data-testid": "bodymap-zoom-out", "aria-label": L.zoomOut, disabled: zoom <= ZMIN, onClick: () => zoomBtn(1 / 1.4), children: "\u2212" }),
513
+ /* @__PURE__ */ 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" }),
514
+ /* @__PURE__ */ jsx("button", { type: "button", "data-testid": "bodymap-zoom-in", "aria-label": L.zoomIn, disabled: zoom >= ZMAX, onClick: () => zoomBtn(1.4), children: "+" })
414
515
  ] })
415
516
  ] }),
416
517
  /* @__PURE__ */ jsx(
@@ -420,7 +521,7 @@ function BodyMap({
420
521
  className: "bmap__svg",
421
522
  viewBox: "0 0 260 470",
422
523
  role: "group",
423
- "aria-label": "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
524
+ "aria-label": L.svgLabel,
424
525
  onPointerDown,
425
526
  onPointerMove,
426
527
  onPointerUp,
@@ -431,21 +532,20 @@ function BodyMap({
431
532
  const s = shapes[r.key];
432
533
  if (!s) return null;
433
534
  const marked = pointOf(r.key);
434
- const selectable = isSelectable(r.key, config);
535
+ const interactive = isSelectable(r.key, config) && !readOnly;
435
536
  const visCls = ["bmap__vis", selected === r.key && "bmap__vis--sel"].filter(Boolean).join(" ");
436
- const fill = marked ? heat(marked.intensity) : fills[r.key] ?? "#cbd5e1";
537
+ const fill = marked ? markedFill(marked.intensity, palette) : baseFill(r.key, fills[r.key] ?? "#cbd5e1", palette);
437
538
  const hs = hitShape(s);
438
- const act = () => selectable ? pickRegion(r.key) : void 0;
439
539
  return /* @__PURE__ */ jsxs("g", { children: [
440
540
  shapeEl(hs, {
441
- className: "bmap__hit" + (selectable ? "" : " bmap__hit--locked"),
541
+ className: "bmap__hit" + (interactive ? "" : " bmap__hit--locked"),
442
542
  "data-testid": `bodymap-region-${r.key}`,
443
- role: selectable ? "button" : "img",
444
- tabIndex: selectable ? 0 : -1,
445
- "aria-label": ariaFor(r, marked, selectable),
446
- "aria-pressed": selectable ? selected === r.key : void 0,
447
- onClick: act,
448
- onKeyDown: selectable ? (e) => {
543
+ role: interactive ? "button" : "img",
544
+ tabIndex: interactive ? 0 : -1,
545
+ "aria-label": ariaFor(r, marked, interactive),
546
+ "aria-pressed": interactive ? selected === r.key : void 0,
547
+ onClick: interactive ? () => pickRegion(r.key) : void 0,
548
+ onKeyDown: interactive ? (e) => {
449
549
  if (e.key === "Enter" || e.key === " ") {
450
550
  e.preventDefault();
451
551
  suppressClick.current = false;
@@ -453,7 +553,7 @@ function BodyMap({
453
553
  }
454
554
  } : void 0
455
555
  }),
456
- shapeEl(s, { className: visCls, fill, opacity: selectable ? 1 : 0.4 })
556
+ shapeEl(s, { className: visCls, fill, opacity: !readOnly && !isSelectable(r.key, config) ? 0.4 : 1 })
457
557
  ] }, r.key);
458
558
  }),
459
559
  report.map((p) => {
@@ -466,26 +566,25 @@ function BodyMap({
466
566
  }
467
567
  )
468
568
  ] }),
469
- region ? /* @__PURE__ */ jsxs("div", { className: "bmap__panel", "data-testid": "bodymap-panel", children: [
569
+ readOnly ? null : region ? /* @__PURE__ */ jsxs("div", { className: "bmap__panel", "data-testid": "bodymap-panel", children: [
470
570
  /* @__PURE__ */ jsxs("div", { className: "bmap__ph", children: [
471
- /* @__PURE__ */ jsx("b", { children: region.label }),
571
+ /* @__PURE__ */ jsx("b", { children: nameOf(region.key) }),
472
572
  /* @__PURE__ */ jsx("span", { className: "bmap__code", children: region.code })
473
573
  ] }),
474
- /* @__PURE__ */ jsx("p", { className: "bmap__lbl", id: "bmap-intensity-lbl", children: "Intensitet (0-10)" }),
574
+ /* @__PURE__ */ jsx("p", { className: "bmap__lbl", id: "bmap-intensity-lbl", children: L.intensity }),
475
575
  /* @__PURE__ */ jsx("div", { className: "bmap__scale", role: "group", "aria-labelledby": "bmap-intensity-lbl", children: Array.from({ length: 11 }, (_, n) => /* @__PURE__ */ jsx(
476
576
  "button",
477
577
  {
478
578
  type: "button",
479
579
  className: "bmap__i" + (current?.intensity === n ? " bmap__i--on" : ""),
480
580
  "data-testid": `bodymap-intensity-${n}`,
481
- "aria-label": `Intensitet ${n}`,
482
581
  "aria-pressed": current?.intensity === n,
483
582
  onClick: () => setPain(region.key, n, current?.type),
484
583
  children: n
485
584
  },
486
585
  n
487
586
  )) }),
488
- /* @__PURE__ */ jsx("p", { className: "bmap__lbl", id: "bmap-quality-lbl", children: "Kvalitet" }),
587
+ /* @__PURE__ */ jsx("p", { className: "bmap__lbl", id: "bmap-quality-lbl", children: L.quality }),
489
588
  /* @__PURE__ */ jsx("div", { className: "bmap__chips", role: "group", "aria-labelledby": "bmap-quality-lbl", children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsx(
490
589
  "button",
491
590
  {
@@ -494,15 +593,15 @@ function BodyMap({
494
593
  "data-testid": `bodymap-type-${t}`,
495
594
  "aria-pressed": current?.type === t,
496
595
  onClick: () => setPain(region.key, current?.intensity ?? 5, t),
497
- children: t
596
+ children: L.qualities[t]
498
597
  },
499
598
  t
500
599
  )) }),
501
- current && /* @__PURE__ */ jsx("button", { type: "button", className: "bmap__rm", "data-testid": "bodymap-remove", onClick: () => removePain(region.key), children: "Fjern punkt" })
502
- ] }) : /* @__PURE__ */ jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children: "V\xE6lg en kropsdel for at markere smerte." })
600
+ current && /* @__PURE__ */ jsx("button", { type: "button", className: "bmap__rm", "data-testid": "bodymap-remove", onClick: () => removePain(region.key), children: L.remove })
601
+ ] }) : /* @__PURE__ */ jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children: L.empty })
503
602
  ] });
504
603
  }
505
604
 
506
- export { BodyMap };
605
+ export { BodyMap, LABELS_DA, LABELS_EN };
507
606
  //# sourceMappingURL=react.js.map
508
607
  //# sourceMappingURL=react.js.map