@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 +132 -31
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +60 -4
- package/dist/react.d.ts +60 -4
- package/dist/react.js +131 -32
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
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
|
|
382
|
-
|
|
383
|
-
|
|
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":
|
|
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:
|
|
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:
|
|
509
|
+
children: L.back
|
|
409
510
|
}
|
|
410
511
|
)
|
|
411
512
|
] }),
|
|
412
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__zoom", role: "group", "aria-label":
|
|
413
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-out", "aria-label":
|
|
414
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-reset", "aria-label":
|
|
415
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-in", "aria-label":
|
|
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":
|
|
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
|
|
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 ?
|
|
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" + (
|
|
543
|
+
className: "bmap__hit" + (interactive ? "" : " bmap__hit--locked"),
|
|
444
544
|
"data-testid": `bodymap-region-${r.key}`,
|
|
445
|
-
role:
|
|
446
|
-
tabIndex:
|
|
447
|
-
"aria-label": ariaFor(r, marked,
|
|
448
|
-
"aria-pressed":
|
|
449
|
-
onClick:
|
|
450
|
-
onKeyDown:
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
504
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children:
|
|
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
|