@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.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
|
|
380
|
-
|
|
381
|
-
|
|
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":
|
|
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:
|
|
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:
|
|
507
|
+
children: L.back
|
|
407
508
|
}
|
|
408
509
|
)
|
|
409
510
|
] }),
|
|
410
|
-
/* @__PURE__ */ jsxs("div", { className: "bmap__zoom", role: "group", "aria-label":
|
|
411
|
-
/* @__PURE__ */ jsx("button", { type: "button", "data-testid": "bodymap-zoom-out", "aria-label":
|
|
412
|
-
/* @__PURE__ */ jsx("button", { type: "button", "data-testid": "bodymap-zoom-reset", "aria-label":
|
|
413
|
-
/* @__PURE__ */ jsx("button", { type: "button", "data-testid": "bodymap-zoom-in", "aria-label":
|
|
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":
|
|
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
|
|
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 ?
|
|
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" + (
|
|
541
|
+
className: "bmap__hit" + (interactive ? "" : " bmap__hit--locked"),
|
|
442
542
|
"data-testid": `bodymap-region-${r.key}`,
|
|
443
|
-
role:
|
|
444
|
-
tabIndex:
|
|
445
|
-
"aria-label": ariaFor(r, marked,
|
|
446
|
-
"aria-pressed":
|
|
447
|
-
onClick:
|
|
448
|
-
onKeyDown:
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
502
|
-
] }) : /* @__PURE__ */ jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children:
|
|
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
|