@broberg/bodymap 0.1.1 → 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 +315 -72
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +61 -5
- package/dist/react.d.ts +61 -5
- package/dist/react.js +315 -74
- 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,48 +182,162 @@ 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
|
+
}
|
|
255
|
+
var VBW = 260;
|
|
256
|
+
var VBH = 470;
|
|
257
|
+
var MIN_HIT = 18;
|
|
170
258
|
function center(s) {
|
|
171
259
|
if (s.el === "rect") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };
|
|
172
260
|
return { x: s.cx, y: s.cy };
|
|
173
261
|
}
|
|
262
|
+
function hitShape(s) {
|
|
263
|
+
if (s.el === "circle") return { el: "circle", cx: s.cx, cy: s.cy, r: Math.max(s.r, MIN_HIT) };
|
|
264
|
+
if (s.el === "ellipse")
|
|
265
|
+
return { el: "ellipse", cx: s.cx, cy: s.cy, rx: Math.max(s.rx, MIN_HIT), ry: Math.max(s.ry, MIN_HIT) };
|
|
266
|
+
const c = center(s);
|
|
267
|
+
const w = Math.max(s.width, MIN_HIT * 2);
|
|
268
|
+
const h = Math.max(s.height, MIN_HIT * 2);
|
|
269
|
+
return { el: "rect", x: c.x - w / 2, y: c.y - h / 2, width: w, height: h, rx: s.rx };
|
|
270
|
+
}
|
|
174
271
|
function heat(v) {
|
|
175
272
|
return v >= 7 ? "#ef4444" : v >= 4 ? "#fb923c" : "#fcd34d";
|
|
176
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
|
+
}
|
|
283
|
+
function shapeEl(s, props) {
|
|
284
|
+
if (s.el === "circle") return /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...props });
|
|
285
|
+
if (s.el === "ellipse") return /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...props });
|
|
286
|
+
return /* @__PURE__ */ jsxRuntime.jsx("rect", { x: s.x, y: s.y, width: s.width, height: s.height, rx: s.rx, ...props });
|
|
287
|
+
}
|
|
177
288
|
var STYLE_ID = "broberg-bodymap-styles";
|
|
178
289
|
var STYLE = `
|
|
179
290
|
.bmap{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#64748b;
|
|
180
291
|
display:flex;gap:18px;flex-wrap:wrap;align-items:flex-start;font:15px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--bmap-ink)}
|
|
181
|
-
.
|
|
182
|
-
.
|
|
183
|
-
.bmap__viewtoggle{display:inline-flex;
|
|
184
|
-
.bmap__vbtn{font:inherit;font-size:13px;font-weight:600;color:var(--bmap-muted);background:none;border:0;border-radius:7px;padding:
|
|
292
|
+
.bmap__stage{display:flex;flex-direction:column;gap:10px;flex:0 1 340px;min-width:0;max-width:360px}
|
|
293
|
+
.bmap__bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
294
|
+
.bmap__viewtoggle{display:inline-flex;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}
|
|
295
|
+
.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}
|
|
185
296
|
.bmap__vbtn:hover{color:var(--bmap-ink)}
|
|
186
297
|
.bmap__vbtn:active{transform:scale(.97)}
|
|
187
298
|
.bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}
|
|
188
|
-
.
|
|
189
|
-
.
|
|
190
|
-
.
|
|
191
|
-
.
|
|
192
|
-
.
|
|
193
|
-
.
|
|
299
|
+
.bmap__zoom{display:inline-flex;gap:4px;margin-left:auto}
|
|
300
|
+
.bmap__zoom button{font:inherit;font-weight:700;font-size:16px;line-height:1;width:36px;height:36px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-ink);border-radius:8px;cursor:pointer;transition:border-color .12s,transform .1s;display:flex;align-items:center;justify-content:center}
|
|
301
|
+
.bmap__zoom button:hover{border-color:var(--bmap-accent)}
|
|
302
|
+
.bmap__zoom button:active{transform:scale(.9)}
|
|
303
|
+
.bmap__zoom button:disabled{opacity:.4;cursor:default}
|
|
304
|
+
.bmap__svg{width:100%;height:auto;display:block;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;background:transparent;border-radius:14px}
|
|
305
|
+
.bmap__vis{stroke:#fff;stroke-width:2.4;pointer-events:none;transition:filter .12s,stroke .12s,stroke-width .12s}
|
|
306
|
+
.bmap__vis--sel{stroke:var(--bmap-accent);stroke-width:3.6}
|
|
307
|
+
.bmap__hit{fill:transparent;cursor:pointer;outline:none}
|
|
308
|
+
.bmap__hit--locked{cursor:default}
|
|
309
|
+
.bmap__hit:hover + .bmap__vis{filter:brightness(1.08) saturate(1.15)}
|
|
310
|
+
.bmap__hit:focus-visible + .bmap__vis{stroke:var(--bmap-accent);stroke-width:4;stroke-dasharray:4 2.5}
|
|
311
|
+
.bmap__heatn{font:700 12px ui-sans-serif;fill:#fff;pointer-events:none}
|
|
312
|
+
.bmap__panel{flex:1 1 240px;min-width:220px;border:1px solid var(--bmap-line);border-radius:14px;padding:16px 18px;background:var(--bmap-panel)}
|
|
194
313
|
.bmap__panel--empty{color:var(--bmap-muted);font-size:13.5px}
|
|
195
314
|
.bmap__ph{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}
|
|
196
315
|
.bmap__ph b{font-size:16px}
|
|
197
316
|
.bmap__code{font:11px ui-monospace,monospace;color:var(--bmap-muted);background:#f1f5f9;border-radius:6px;padding:2px 7px}
|
|
198
317
|
.bmap__lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bmap-muted);margin:0 0 7px}
|
|
199
|
-
.bmap__scale{display:flex;gap:
|
|
200
|
-
.bmap__i{font:inherit;font-size:
|
|
318
|
+
.bmap__scale{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
|
|
319
|
+
.bmap__i{font:inherit;font-size:14px;font-weight:600;width:38px;height:38px;border-radius:9px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-ink);cursor:pointer;transition:border-color .1s,background .1s,transform .1s}
|
|
201
320
|
.bmap__i:hover{border-color:var(--bmap-accent)}
|
|
202
321
|
.bmap__i:active{transform:scale(.9)}
|
|
203
322
|
.bmap__i--on{background:var(--bmap-accent);border-color:var(--bmap-accent);color:#fff}
|
|
204
|
-
.bmap__chips{display:flex;gap:
|
|
205
|
-
.bmap__chip{font:inherit;font-size:
|
|
323
|
+
.bmap__chips{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}
|
|
324
|
+
.bmap__chip{font:inherit;font-size:13px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-muted);border-radius:999px;padding:9px 15px;cursor:pointer;transition:.1s}
|
|
206
325
|
.bmap__chip:hover{border-color:var(--bmap-accent);color:var(--bmap-accent)}
|
|
207
326
|
.bmap__chip:active{transform:scale(.96)}
|
|
208
327
|
.bmap__chip--on{background:var(--bmap-ink);border-color:var(--bmap-ink);color:#fff}
|
|
209
|
-
.bmap__rm{font:inherit;font-size:
|
|
328
|
+
.bmap__rm{font:inherit;font-size:14px;font-weight:600;border:1px solid #f6c9c9;background:#fff;color:#ef4444;border-radius:9px;padding:10px 15px;cursor:pointer;transition:.1s}
|
|
210
329
|
.bmap__rm:hover{background:#fef2f2}
|
|
211
330
|
.bmap__rm:active{transform:scale(.97)}
|
|
331
|
+
.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}
|
|
332
|
+
@media (max-width:560px){
|
|
333
|
+
.bmap{flex-direction:column;gap:14px}
|
|
334
|
+
.bmap__stage{max-width:100%;width:100%;flex-basis:auto}
|
|
335
|
+
.bmap__panel{width:100%}
|
|
336
|
+
.bmap__i{width:46px;height:46px;font-size:16px}
|
|
337
|
+
.bmap__chip{padding:11px 17px;font-size:15px}
|
|
338
|
+
.bmap__vbtn{padding:10px 18px;font-size:15px}
|
|
339
|
+
}
|
|
340
|
+
@media (prefers-reduced-motion:reduce){.bmap *{transition:none !important}}
|
|
212
341
|
`;
|
|
213
342
|
function ensureStyles() {
|
|
214
343
|
if (typeof document === "undefined") return;
|
|
@@ -218,6 +347,12 @@ function ensureStyles() {
|
|
|
218
347
|
el.textContent = STYLE;
|
|
219
348
|
document.head.appendChild(el);
|
|
220
349
|
}
|
|
350
|
+
var ZMIN = 1;
|
|
351
|
+
var ZMAX = 4;
|
|
352
|
+
var clampPan = (p, z2) => ({
|
|
353
|
+
x: Math.min(0, Math.max(VBW * (1 - z2), p.x)),
|
|
354
|
+
y: Math.min(0, Math.max(VBH * (1 - z2), p.y))
|
|
355
|
+
});
|
|
221
356
|
function BodyMap({
|
|
222
357
|
value,
|
|
223
358
|
defaultValue,
|
|
@@ -225,15 +360,93 @@ function BodyMap({
|
|
|
225
360
|
config = {},
|
|
226
361
|
defaultView = "front",
|
|
227
362
|
onViewChange,
|
|
363
|
+
palette,
|
|
364
|
+
locale = "da",
|
|
365
|
+
labels,
|
|
366
|
+
readOnly = false,
|
|
228
367
|
className
|
|
229
368
|
}) {
|
|
230
369
|
react.useEffect(ensureStyles, []);
|
|
231
370
|
const [internal, setInternal] = react.useState(defaultValue ?? []);
|
|
232
371
|
const [selected, setSelected] = react.useState(null);
|
|
233
372
|
const [view, setView] = react.useState(defaultView);
|
|
373
|
+
const [zoom, setZoom] = react.useState(1);
|
|
374
|
+
const [pan, setPan] = react.useState({ x: 0, y: 0 });
|
|
234
375
|
const report = value ?? internal;
|
|
235
376
|
const shapes = view === "back" ? SHAPES_BACK : SHAPES;
|
|
236
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;
|
|
380
|
+
const svgRef = react.useRef(null);
|
|
381
|
+
const zoomRef = react.useRef(1);
|
|
382
|
+
const panRef = react.useRef({ x: 0, y: 0 });
|
|
383
|
+
const pointers = react.useRef(/* @__PURE__ */ new Map());
|
|
384
|
+
const gesture = react.useRef({ startDist: 0, startZoom: 1, midVB: { x: 0, y: 0 }, start: { x: 0, y: 0 }, moved: false });
|
|
385
|
+
const suppressClick = react.useRef(false);
|
|
386
|
+
const setZP = (z2, p) => {
|
|
387
|
+
const cz = Math.max(ZMIN, Math.min(ZMAX, z2));
|
|
388
|
+
const cp = clampPan(p, cz);
|
|
389
|
+
zoomRef.current = cz;
|
|
390
|
+
panRef.current = cp;
|
|
391
|
+
setZoom(cz);
|
|
392
|
+
setPan(cp);
|
|
393
|
+
};
|
|
394
|
+
const screenToVB = (cx, cy) => {
|
|
395
|
+
const el = svgRef.current;
|
|
396
|
+
if (!el) return { x: VBW / 2, y: VBH / 2 };
|
|
397
|
+
const r = el.getBoundingClientRect();
|
|
398
|
+
if (!r.width || !r.height) return { x: VBW / 2, y: VBH / 2 };
|
|
399
|
+
return { x: (cx - r.left) / r.width * VBW, y: (cy - r.top) / r.height * VBH };
|
|
400
|
+
};
|
|
401
|
+
const zoomTo = (nz, focalVB) => {
|
|
402
|
+
const z2 = Math.max(ZMIN, Math.min(ZMAX, nz));
|
|
403
|
+
const p = panRef.current;
|
|
404
|
+
setZP(z2, { x: focalVB.x - (focalVB.x - p.x) * (z2 / zoomRef.current), y: focalVB.y - (focalVB.y - p.y) * (z2 / zoomRef.current) });
|
|
405
|
+
};
|
|
406
|
+
const zoomBtn = (factor) => zoomTo(zoomRef.current * factor, { x: VBW / 2, y: VBH / 2 });
|
|
407
|
+
const resetZoom = () => setZP(1, { x: 0, y: 0 });
|
|
408
|
+
const onPointerDown = (e) => {
|
|
409
|
+
pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
410
|
+
const pts = [...pointers.current.values()];
|
|
411
|
+
if (pts.length === 2) {
|
|
412
|
+
gesture.current.startDist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
|
413
|
+
gesture.current.startZoom = zoomRef.current;
|
|
414
|
+
gesture.current.midVB = screenToVB((pts[0].x + pts[1].x) / 2, (pts[0].y + pts[1].y) / 2);
|
|
415
|
+
gesture.current.moved = true;
|
|
416
|
+
suppressClick.current = true;
|
|
417
|
+
} else {
|
|
418
|
+
gesture.current.start = { x: e.clientX, y: e.clientY };
|
|
419
|
+
gesture.current.moved = false;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
const onPointerMove = (e) => {
|
|
423
|
+
const prev = pointers.current.get(e.pointerId);
|
|
424
|
+
if (!prev) return;
|
|
425
|
+
pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
426
|
+
const pts = [...pointers.current.values()];
|
|
427
|
+
if (pts.length >= 2) {
|
|
428
|
+
const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
|
429
|
+
if (gesture.current.startDist > 0) zoomTo(gesture.current.startZoom * (dist / gesture.current.startDist), gesture.current.midVB);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const el = svgRef.current;
|
|
433
|
+
const rect = el?.getBoundingClientRect();
|
|
434
|
+
if (Math.hypot(e.clientX - gesture.current.start.x, e.clientY - gesture.current.start.y) > 6) {
|
|
435
|
+
gesture.current.moved = true;
|
|
436
|
+
suppressClick.current = true;
|
|
437
|
+
}
|
|
438
|
+
if (zoomRef.current > 1 && rect && rect.width) {
|
|
439
|
+
const dxVB = (e.clientX - prev.x) / rect.width * VBW;
|
|
440
|
+
const dyVB = (e.clientY - prev.y) / rect.height * VBH;
|
|
441
|
+
setZP(zoomRef.current, { x: panRef.current.x + dxVB, y: panRef.current.y + dyVB });
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
const onPointerUp = (e) => {
|
|
445
|
+
pointers.current.delete(e.pointerId);
|
|
446
|
+
if (pointers.current.size === 0) window.setTimeout(() => {
|
|
447
|
+
suppressClick.current = false;
|
|
448
|
+
}, 60);
|
|
449
|
+
};
|
|
237
450
|
const changeView = (v) => {
|
|
238
451
|
if (v === view) return;
|
|
239
452
|
setView(v);
|
|
@@ -254,68 +467,96 @@ function BodyMap({
|
|
|
254
467
|
commit(report.filter((p) => p.region !== key));
|
|
255
468
|
setSelected(null);
|
|
256
469
|
};
|
|
470
|
+
const pickRegion = (key) => {
|
|
471
|
+
if (suppressClick.current) {
|
|
472
|
+
suppressClick.current = false;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
setSelected(key);
|
|
476
|
+
};
|
|
257
477
|
const regions = resolveRegions(config);
|
|
258
478
|
const region = selected ? REGIONS.find((r) => r.key === selected) : void 0;
|
|
259
479
|
const current = selected ? pointOf(selected) : void 0;
|
|
260
480
|
const rootCls = ["bmap", className].filter(Boolean).join(" ");
|
|
261
|
-
|
|
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);
|
|
485
|
+
};
|
|
486
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rootCls, "data-testid": "bodymap-root", style: rootStyle, children: [
|
|
262
487
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__stage", children: [
|
|
263
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "
|
|
264
|
-
/* @__PURE__ */ jsxRuntime.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
488
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__bar", children: [
|
|
489
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": L.viewLabel, children: [
|
|
490
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
491
|
+
"button",
|
|
492
|
+
{
|
|
493
|
+
type: "button",
|
|
494
|
+
className: "bmap__vbtn" + (view === "front" ? " bmap__vbtn--on" : ""),
|
|
495
|
+
"data-testid": "bodymap-view-front",
|
|
496
|
+
"aria-pressed": view === "front",
|
|
497
|
+
onClick: () => changeView("front"),
|
|
498
|
+
children: L.front
|
|
499
|
+
}
|
|
500
|
+
),
|
|
501
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
502
|
+
"button",
|
|
503
|
+
{
|
|
504
|
+
type: "button",
|
|
505
|
+
className: "bmap__vbtn" + (view === "back" ? " bmap__vbtn--on" : ""),
|
|
506
|
+
"data-testid": "bodymap-view-back",
|
|
507
|
+
"aria-pressed": view === "back",
|
|
508
|
+
onClick: () => changeView("back"),
|
|
509
|
+
children: L.back
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
] }),
|
|
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: "+" })
|
|
517
|
+
] })
|
|
286
518
|
] }),
|
|
287
|
-
/* @__PURE__ */ jsxRuntime.
|
|
519
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
288
520
|
"svg",
|
|
289
521
|
{
|
|
522
|
+
ref: svgRef,
|
|
290
523
|
className: "bmap__svg",
|
|
291
524
|
viewBox: "0 0 260 470",
|
|
292
|
-
width: 230,
|
|
293
|
-
height: 416,
|
|
294
525
|
role: "group",
|
|
295
|
-
"aria-label":
|
|
296
|
-
|
|
526
|
+
"aria-label": L.svgLabel,
|
|
527
|
+
onPointerDown,
|
|
528
|
+
onPointerMove,
|
|
529
|
+
onPointerUp,
|
|
530
|
+
onPointerCancel: onPointerUp,
|
|
531
|
+
onWheel: (e) => zoomTo(zoomRef.current * (e.deltaY < 0 ? 1.12 : 1 / 1.12), screenToVB(e.clientX, e.clientY)),
|
|
532
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${pan.x} ${pan.y}) scale(${zoom})`, children: [
|
|
297
533
|
regions.map((r) => {
|
|
298
534
|
const s = shapes[r.key];
|
|
299
535
|
if (!s) return null;
|
|
300
536
|
const marked = pointOf(r.key);
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
537
|
+
const interactive = isSelectable(r.key, config) && !readOnly;
|
|
538
|
+
const visCls = ["bmap__vis", selected === r.key && "bmap__vis--sel"].filter(Boolean).join(" ");
|
|
539
|
+
const fill = marked ? markedFill(marked.intensity, palette) : baseFill(r.key, fills[r.key] ?? "#cbd5e1", palette);
|
|
540
|
+
const hs = hitShape(s);
|
|
541
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
542
|
+
shapeEl(hs, {
|
|
543
|
+
className: "bmap__hit" + (interactive ? "" : " bmap__hit--locked"),
|
|
544
|
+
"data-testid": `bodymap-region-${r.key}`,
|
|
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) => {
|
|
551
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
552
|
+
e.preventDefault();
|
|
553
|
+
suppressClick.current = false;
|
|
554
|
+
setSelected(r.key);
|
|
555
|
+
}
|
|
556
|
+
} : void 0
|
|
557
|
+
}),
|
|
558
|
+
shapeEl(s, { className: visCls, fill, opacity: !readOnly && !isSelectable(r.key, config) ? 0.4 : 1 })
|
|
559
|
+
] }, r.key);
|
|
319
560
|
}),
|
|
320
561
|
report.map((p) => {
|
|
321
562
|
const s = shapes[p.region];
|
|
@@ -323,17 +564,17 @@ function BodyMap({
|
|
|
323
564
|
const c = center(s);
|
|
324
565
|
return /* @__PURE__ */ jsxRuntime.jsx("text", { x: c.x, y: c.y + 4, textAnchor: "middle", className: "bmap__heatn", children: p.intensity }, p.region);
|
|
325
566
|
})
|
|
326
|
-
]
|
|
567
|
+
] })
|
|
327
568
|
}
|
|
328
569
|
)
|
|
329
570
|
] }),
|
|
330
|
-
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: [
|
|
331
572
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__ph", children: [
|
|
332
|
-
/* @__PURE__ */ jsxRuntime.jsx("b", { children: region.
|
|
573
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: nameOf(region.key) }),
|
|
333
574
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "bmap__code", children: region.code })
|
|
334
575
|
] }),
|
|
335
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl",
|
|
336
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__scale", role: "group", "aria-
|
|
576
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-intensity-lbl", children: L.intensity }),
|
|
577
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__scale", role: "group", "aria-labelledby": "bmap-intensity-lbl", children: Array.from({ length: 11 }, (_, n) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
337
578
|
"button",
|
|
338
579
|
{
|
|
339
580
|
type: "button",
|
|
@@ -345,8 +586,8 @@ function BodyMap({
|
|
|
345
586
|
},
|
|
346
587
|
n
|
|
347
588
|
)) }),
|
|
348
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl",
|
|
349
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__chips", role: "group", "aria-
|
|
589
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-quality-lbl", children: L.quality }),
|
|
590
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__chips", role: "group", "aria-labelledby": "bmap-quality-lbl", children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
350
591
|
"button",
|
|
351
592
|
{
|
|
352
593
|
type: "button",
|
|
@@ -354,15 +595,17 @@ function BodyMap({
|
|
|
354
595
|
"data-testid": `bodymap-type-${t}`,
|
|
355
596
|
"aria-pressed": current?.type === t,
|
|
356
597
|
onClick: () => setPain(region.key, current?.intensity ?? 5, t),
|
|
357
|
-
children: t
|
|
598
|
+
children: L.qualities[t]
|
|
358
599
|
},
|
|
359
600
|
t
|
|
360
601
|
)) }),
|
|
361
|
-
current && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "bmap__rm", "data-testid": "bodymap-remove", onClick: () => removePain(region.key), children:
|
|
362
|
-
] }) : /* @__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 })
|
|
363
604
|
] });
|
|
364
605
|
}
|
|
365
606
|
|
|
366
607
|
exports.BodyMap = BodyMap;
|
|
608
|
+
exports.LABELS_DA = LABELS_DA;
|
|
609
|
+
exports.LABELS_EN = LABELS_EN;
|
|
367
610
|
//# sourceMappingURL=react.cjs.map
|
|
368
611
|
//# sourceMappingURL=react.cjs.map
|