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