@broberg/bodymap 0.1.1 → 0.1.2

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 CHANGED
@@ -167,48 +167,83 @@ var FILL_BACK = {
167
167
  hip_left: "#c99bd6",
168
168
  hip_right: "#c99bd6"
169
169
  };
170
+ var VBW = 260;
171
+ var VBH = 470;
172
+ var MIN_HIT = 18;
170
173
  function center(s) {
171
174
  if (s.el === "rect") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };
172
175
  return { x: s.cx, y: s.cy };
173
176
  }
177
+ function hitShape(s) {
178
+ if (s.el === "circle") return { el: "circle", cx: s.cx, cy: s.cy, r: Math.max(s.r, MIN_HIT) };
179
+ if (s.el === "ellipse")
180
+ return { el: "ellipse", cx: s.cx, cy: s.cy, rx: Math.max(s.rx, MIN_HIT), ry: Math.max(s.ry, MIN_HIT) };
181
+ const c = center(s);
182
+ const w = Math.max(s.width, MIN_HIT * 2);
183
+ const h = Math.max(s.height, MIN_HIT * 2);
184
+ return { el: "rect", x: c.x - w / 2, y: c.y - h / 2, width: w, height: h, rx: s.rx };
185
+ }
174
186
  function heat(v) {
175
187
  return v >= 7 ? "#ef4444" : v >= 4 ? "#fb923c" : "#fcd34d";
176
188
  }
189
+ function shapeEl(s, props) {
190
+ if (s.el === "circle") return /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...props });
191
+ if (s.el === "ellipse") return /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...props });
192
+ return /* @__PURE__ */ jsxRuntime.jsx("rect", { x: s.x, y: s.y, width: s.width, height: s.height, rx: s.rx, ...props });
193
+ }
177
194
  var STYLE_ID = "broberg-bodymap-styles";
178
195
  var STYLE = `
179
196
  .bmap{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#64748b;
180
197
  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
- .bmap__svg{flex:0 0 auto}
182
- .bmap__stage{display:flex;flex-direction:column;gap:12px;flex:0 0 auto}
183
- .bmap__viewtoggle{display:inline-flex;align-self:flex-start;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}
184
- .bmap__vbtn{font:inherit;font-size:13px;font-weight:600;color:var(--bmap-muted);background:none;border:0;border-radius:7px;padding:6px 16px;cursor:pointer;transition:color .12s,background .12s,transform .1s}
198
+ .bmap__stage{display:flex;flex-direction:column;gap:10px;flex:0 1 340px;min-width:0;max-width:360px}
199
+ .bmap__bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
200
+ .bmap__viewtoggle{display:inline-flex;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}
201
+ .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
202
  .bmap__vbtn:hover{color:var(--bmap-ink)}
186
203
  .bmap__vbtn:active{transform:scale(.97)}
187
204
  .bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}
188
- .bmap__region{cursor:pointer;stroke:#fff;stroke-width:2.4;transition:filter .12s}
189
- .bmap__region:hover{filter:brightness(1.08) saturate(1.15)}
190
- .bmap__region--locked{cursor:default;opacity:.4}
191
- .bmap__region--sel{stroke:var(--bmap-accent);stroke-width:3.6}
192
- .bmap__heatn{font:700 11px ui-sans-serif;fill:#fff;pointer-events:none}
193
- .bmap__panel{flex:1 1 220px;min-width:220px;border:1px solid var(--bmap-line);border-radius:14px;padding:16px 18px;background:var(--bmap-panel)}
205
+ .bmap__zoom{display:inline-flex;gap:4px;margin-left:auto}
206
+ .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}
207
+ .bmap__zoom button:hover{border-color:var(--bmap-accent)}
208
+ .bmap__zoom button:active{transform:scale(.9)}
209
+ .bmap__zoom button:disabled{opacity:.4;cursor:default}
210
+ .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}
211
+ .bmap__vis{stroke:#fff;stroke-width:2.4;pointer-events:none;transition:filter .12s,stroke .12s,stroke-width .12s}
212
+ .bmap__vis--sel{stroke:var(--bmap-accent);stroke-width:3.6}
213
+ .bmap__hit{fill:transparent;cursor:pointer;outline:none}
214
+ .bmap__hit--locked{cursor:default}
215
+ .bmap__hit:hover + .bmap__vis{filter:brightness(1.08) saturate(1.15)}
216
+ .bmap__hit:focus-visible + .bmap__vis{stroke:var(--bmap-accent);stroke-width:4;stroke-dasharray:4 2.5}
217
+ .bmap__heatn{font:700 12px ui-sans-serif;fill:#fff;pointer-events:none}
218
+ .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
219
  .bmap__panel--empty{color:var(--bmap-muted);font-size:13.5px}
195
220
  .bmap__ph{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}
196
221
  .bmap__ph b{font-size:16px}
197
222
  .bmap__code{font:11px ui-monospace,monospace;color:var(--bmap-muted);background:#f1f5f9;border-radius:6px;padding:2px 7px}
198
223
  .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:4px;flex-wrap:wrap;margin-bottom:14px}
200
- .bmap__i{font:inherit;font-size:13px;font-weight:600;width:30px;height:30px;border-radius:8px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-ink);cursor:pointer;transition:.1s}
224
+ .bmap__scale{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
225
+ .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
226
  .bmap__i:hover{border-color:var(--bmap-accent)}
202
227
  .bmap__i:active{transform:scale(.9)}
203
228
  .bmap__i--on{background:var(--bmap-accent);border-color:var(--bmap-accent);color:#fff}
204
- .bmap__chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
205
- .bmap__chip{font:inherit;font-size:12.5px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-muted);border-radius:999px;padding:6px 12px;cursor:pointer;transition:.1s}
229
+ .bmap__chips{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}
230
+ .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
231
  .bmap__chip:hover{border-color:var(--bmap-accent);color:var(--bmap-accent)}
207
232
  .bmap__chip:active{transform:scale(.96)}
208
233
  .bmap__chip--on{background:var(--bmap-ink);border-color:var(--bmap-ink);color:#fff}
209
- .bmap__rm{font:inherit;font-size:13px;font-weight:600;border:1px solid #f6c9c9;background:#fff;color:#ef4444;border-radius:9px;padding:8px 13px;cursor:pointer;transition:.1s}
234
+ .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
235
  .bmap__rm:hover{background:#fef2f2}
211
236
  .bmap__rm:active{transform:scale(.97)}
237
+ .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}
238
+ @media (max-width:560px){
239
+ .bmap{flex-direction:column;gap:14px}
240
+ .bmap__stage{max-width:100%;width:100%;flex-basis:auto}
241
+ .bmap__panel{width:100%}
242
+ .bmap__i{width:46px;height:46px;font-size:16px}
243
+ .bmap__chip{padding:11px 17px;font-size:15px}
244
+ .bmap__vbtn{padding:10px 18px;font-size:15px}
245
+ }
246
+ @media (prefers-reduced-motion:reduce){.bmap *{transition:none !important}}
212
247
  `;
213
248
  function ensureStyles() {
214
249
  if (typeof document === "undefined") return;
@@ -218,6 +253,12 @@ function ensureStyles() {
218
253
  el.textContent = STYLE;
219
254
  document.head.appendChild(el);
220
255
  }
256
+ var ZMIN = 1;
257
+ var ZMAX = 4;
258
+ var clampPan = (p, z2) => ({
259
+ x: Math.min(0, Math.max(VBW * (1 - z2), p.x)),
260
+ y: Math.min(0, Math.max(VBH * (1 - z2), p.y))
261
+ });
221
262
  function BodyMap({
222
263
  value,
223
264
  defaultValue,
@@ -231,9 +272,81 @@ function BodyMap({
231
272
  const [internal, setInternal] = react.useState(defaultValue ?? []);
232
273
  const [selected, setSelected] = react.useState(null);
233
274
  const [view, setView] = react.useState(defaultView);
275
+ const [zoom, setZoom] = react.useState(1);
276
+ const [pan, setPan] = react.useState({ x: 0, y: 0 });
234
277
  const report = value ?? internal;
235
278
  const shapes = view === "back" ? SHAPES_BACK : SHAPES;
236
279
  const fills = view === "back" ? FILL_BACK : FILL;
280
+ const svgRef = react.useRef(null);
281
+ const zoomRef = react.useRef(1);
282
+ const panRef = react.useRef({ x: 0, y: 0 });
283
+ const pointers = react.useRef(/* @__PURE__ */ new Map());
284
+ const gesture = react.useRef({ startDist: 0, startZoom: 1, midVB: { x: 0, y: 0 }, start: { x: 0, y: 0 }, moved: false });
285
+ const suppressClick = react.useRef(false);
286
+ const setZP = (z2, p) => {
287
+ const cz = Math.max(ZMIN, Math.min(ZMAX, z2));
288
+ const cp = clampPan(p, cz);
289
+ zoomRef.current = cz;
290
+ panRef.current = cp;
291
+ setZoom(cz);
292
+ setPan(cp);
293
+ };
294
+ const screenToVB = (cx, cy) => {
295
+ const el = svgRef.current;
296
+ if (!el) return { x: VBW / 2, y: VBH / 2 };
297
+ const r = el.getBoundingClientRect();
298
+ if (!r.width || !r.height) return { x: VBW / 2, y: VBH / 2 };
299
+ return { x: (cx - r.left) / r.width * VBW, y: (cy - r.top) / r.height * VBH };
300
+ };
301
+ const zoomTo = (nz, focalVB) => {
302
+ const z2 = Math.max(ZMIN, Math.min(ZMAX, nz));
303
+ const p = panRef.current;
304
+ setZP(z2, { x: focalVB.x - (focalVB.x - p.x) * (z2 / zoomRef.current), y: focalVB.y - (focalVB.y - p.y) * (z2 / zoomRef.current) });
305
+ };
306
+ const zoomBtn = (factor) => zoomTo(zoomRef.current * factor, { x: VBW / 2, y: VBH / 2 });
307
+ const resetZoom = () => setZP(1, { x: 0, y: 0 });
308
+ const onPointerDown = (e) => {
309
+ pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
310
+ const pts = [...pointers.current.values()];
311
+ if (pts.length === 2) {
312
+ gesture.current.startDist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
313
+ gesture.current.startZoom = zoomRef.current;
314
+ gesture.current.midVB = screenToVB((pts[0].x + pts[1].x) / 2, (pts[0].y + pts[1].y) / 2);
315
+ gesture.current.moved = true;
316
+ suppressClick.current = true;
317
+ } else {
318
+ gesture.current.start = { x: e.clientX, y: e.clientY };
319
+ gesture.current.moved = false;
320
+ }
321
+ };
322
+ const onPointerMove = (e) => {
323
+ const prev = pointers.current.get(e.pointerId);
324
+ if (!prev) return;
325
+ pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
326
+ const pts = [...pointers.current.values()];
327
+ if (pts.length >= 2) {
328
+ const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
329
+ if (gesture.current.startDist > 0) zoomTo(gesture.current.startZoom * (dist / gesture.current.startDist), gesture.current.midVB);
330
+ return;
331
+ }
332
+ const el = svgRef.current;
333
+ const rect = el?.getBoundingClientRect();
334
+ if (Math.hypot(e.clientX - gesture.current.start.x, e.clientY - gesture.current.start.y) > 6) {
335
+ gesture.current.moved = true;
336
+ suppressClick.current = true;
337
+ }
338
+ if (zoomRef.current > 1 && rect && rect.width) {
339
+ const dxVB = (e.clientX - prev.x) / rect.width * VBW;
340
+ const dyVB = (e.clientY - prev.y) / rect.height * VBH;
341
+ setZP(zoomRef.current, { x: panRef.current.x + dxVB, y: panRef.current.y + dyVB });
342
+ }
343
+ };
344
+ const onPointerUp = (e) => {
345
+ pointers.current.delete(e.pointerId);
346
+ if (pointers.current.size === 0) window.setTimeout(() => {
347
+ suppressClick.current = false;
348
+ }, 60);
349
+ };
237
350
  const changeView = (v) => {
238
351
  if (v === view) return;
239
352
  setView(v);
@@ -254,68 +367,96 @@ function BodyMap({
254
367
  commit(report.filter((p) => p.region !== key));
255
368
  setSelected(null);
256
369
  };
370
+ const pickRegion = (key) => {
371
+ if (suppressClick.current) {
372
+ suppressClick.current = false;
373
+ return;
374
+ }
375
+ setSelected(key);
376
+ };
257
377
  const regions = resolveRegions(config);
258
378
  const region = selected ? REGIONS.find((r) => r.key === selected) : void 0;
259
379
  const current = selected ? pointOf(selected) : void 0;
260
380
  const rootCls = ["bmap", className].filter(Boolean).join(" ");
381
+ const ariaFor = (r, marked, selectable = true) => {
382
+ if (marked) return `${r.label}, smerte ${marked.intensity} af 10${marked.type ? ", " + marked.type : ""}`;
383
+ return selectable ? `${r.label}, ikke markeret. Aktiv\xE9r for at markere smerte.` : r.label;
384
+ };
261
385
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rootCls, "data-testid": "bodymap-root", children: [
262
386
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__stage", children: [
263
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": "Visning", children: [
264
- /* @__PURE__ */ jsxRuntime.jsx(
265
- "button",
266
- {
267
- type: "button",
268
- className: "bmap__vbtn" + (view === "front" ? " bmap__vbtn--on" : ""),
269
- "data-testid": "bodymap-view-front",
270
- "aria-pressed": view === "front",
271
- onClick: () => changeView("front"),
272
- children: "Forfra"
273
- }
274
- ),
275
- /* @__PURE__ */ jsxRuntime.jsx(
276
- "button",
277
- {
278
- type: "button",
279
- className: "bmap__vbtn" + (view === "back" ? " bmap__vbtn--on" : ""),
280
- "data-testid": "bodymap-view-back",
281
- "aria-pressed": view === "back",
282
- onClick: () => changeView("back"),
283
- children: "Bagfra"
284
- }
285
- )
387
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__bar", children: [
388
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": "Visning", children: [
389
+ /* @__PURE__ */ jsxRuntime.jsx(
390
+ "button",
391
+ {
392
+ type: "button",
393
+ className: "bmap__vbtn" + (view === "front" ? " bmap__vbtn--on" : ""),
394
+ "data-testid": "bodymap-view-front",
395
+ "aria-pressed": view === "front",
396
+ onClick: () => changeView("front"),
397
+ children: "Forfra"
398
+ }
399
+ ),
400
+ /* @__PURE__ */ jsxRuntime.jsx(
401
+ "button",
402
+ {
403
+ type: "button",
404
+ className: "bmap__vbtn" + (view === "back" ? " bmap__vbtn--on" : ""),
405
+ "data-testid": "bodymap-view-back",
406
+ "aria-pressed": view === "back",
407
+ onClick: () => changeView("back"),
408
+ children: "Bagfra"
409
+ }
410
+ )
411
+ ] }),
412
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__zoom", role: "group", "aria-label": "Zoom", children: [
413
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-out", "aria-label": "Zoom ud", disabled: zoom <= ZMIN, onClick: () => zoomBtn(1 / 1.4), children: "\u2212" }),
414
+ /* @__PURE__ */ jsxRuntime.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" }),
415
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "bodymap-zoom-in", "aria-label": "Zoom ind", disabled: zoom >= ZMAX, onClick: () => zoomBtn(1.4), children: "+" })
416
+ ] })
286
417
  ] }),
287
- /* @__PURE__ */ jsxRuntime.jsxs(
418
+ /* @__PURE__ */ jsxRuntime.jsx(
288
419
  "svg",
289
420
  {
421
+ ref: svgRef,
290
422
  className: "bmap__svg",
291
423
  viewBox: "0 0 260 470",
292
- width: 230,
293
- height: 416,
294
424
  role: "group",
295
425
  "aria-label": "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
296
- children: [
426
+ onPointerDown,
427
+ onPointerMove,
428
+ onPointerUp,
429
+ onPointerCancel: onPointerUp,
430
+ onWheel: (e) => zoomTo(zoomRef.current * (e.deltaY < 0 ? 1.12 : 1 / 1.12), screenToVB(e.clientX, e.clientY)),
431
+ children: /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${pan.x} ${pan.y}) scale(${zoom})`, children: [
297
432
  regions.map((r) => {
298
433
  const s = shapes[r.key];
299
434
  if (!s) return null;
300
435
  const marked = pointOf(r.key);
301
436
  const selectable = isSelectable(r.key, config);
302
- const cls = [
303
- "bmap__region",
304
- !selectable && "bmap__region--locked",
305
- selected === r.key && "bmap__region--sel"
306
- ].filter(Boolean).join(" ");
437
+ const visCls = ["bmap__vis", selected === r.key && "bmap__vis--sel"].filter(Boolean).join(" ");
307
438
  const fill = marked ? heat(marked.intensity) : fills[r.key] ?? "#cbd5e1";
308
- const common = {
309
- className: cls,
310
- fill,
311
- "data-testid": `bodymap-region-${r.key}`,
312
- role: selectable ? "button" : void 0,
313
- "aria-label": r.label,
314
- onClick: selectable ? () => setSelected(r.key) : void 0
315
- };
316
- if (s.el === "circle") return /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...common }, r.key);
317
- if (s.el === "ellipse") return /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...common }, r.key);
318
- return /* @__PURE__ */ jsxRuntime.jsx("rect", { x: s.x, y: s.y, width: s.width, height: s.height, rx: s.rx, ...common }, r.key);
439
+ const hs = hitShape(s);
440
+ const act = () => selectable ? pickRegion(r.key) : void 0;
441
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
442
+ shapeEl(hs, {
443
+ className: "bmap__hit" + (selectable ? "" : " bmap__hit--locked"),
444
+ "data-testid": `bodymap-region-${r.key}`,
445
+ role: selectable ? "button" : "img",
446
+ tabIndex: selectable ? 0 : -1,
447
+ "aria-label": ariaFor(r, marked, selectable),
448
+ "aria-pressed": selectable ? selected === r.key : void 0,
449
+ onClick: act,
450
+ onKeyDown: selectable ? (e) => {
451
+ if (e.key === "Enter" || e.key === " ") {
452
+ e.preventDefault();
453
+ suppressClick.current = false;
454
+ setSelected(r.key);
455
+ }
456
+ } : void 0
457
+ }),
458
+ shapeEl(s, { className: visCls, fill, opacity: selectable ? 1 : 0.4 })
459
+ ] }, r.key);
319
460
  }),
320
461
  report.map((p) => {
321
462
  const s = shapes[p.region];
@@ -323,7 +464,7 @@ function BodyMap({
323
464
  const c = center(s);
324
465
  return /* @__PURE__ */ jsxRuntime.jsx("text", { x: c.x, y: c.y + 4, textAnchor: "middle", className: "bmap__heatn", children: p.intensity }, p.region);
325
466
  })
326
- ]
467
+ ] })
327
468
  }
328
469
  )
329
470
  ] }),
@@ -332,21 +473,22 @@ function BodyMap({
332
473
  /* @__PURE__ */ jsxRuntime.jsx("b", { children: region.label }),
333
474
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bmap__code", children: region.code })
334
475
  ] }),
335
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", children: "Intensitet (0-10)" }),
336
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__scale", role: "group", "aria-label": "Intensitet", children: Array.from({ length: 11 }, (_, n) => /* @__PURE__ */ jsxRuntime.jsx(
476
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-intensity-lbl", children: "Intensitet (0-10)" }),
477
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__scale", role: "group", "aria-labelledby": "bmap-intensity-lbl", children: Array.from({ length: 11 }, (_, n) => /* @__PURE__ */ jsxRuntime.jsx(
337
478
  "button",
338
479
  {
339
480
  type: "button",
340
481
  className: "bmap__i" + (current?.intensity === n ? " bmap__i--on" : ""),
341
482
  "data-testid": `bodymap-intensity-${n}`,
483
+ "aria-label": `Intensitet ${n}`,
342
484
  "aria-pressed": current?.intensity === n,
343
485
  onClick: () => setPain(region.key, n, current?.type),
344
486
  children: n
345
487
  },
346
488
  n
347
489
  )) }),
348
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", children: "Kvalitet" }),
349
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__chips", role: "group", "aria-label": "Kvalitet", children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsxRuntime.jsx(
490
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bmap__lbl", id: "bmap-quality-lbl", children: "Kvalitet" }),
491
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__chips", role: "group", "aria-labelledby": "bmap-quality-lbl", children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsxRuntime.jsx(
350
492
  "button",
351
493
  {
352
494
  type: "button",
@@ -359,7 +501,7 @@ function BodyMap({
359
501
  t
360
502
  )) }),
361
503
  current && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "bmap__rm", "data-testid": "bodymap-remove", onClick: () => removePain(region.key), children: "Fjern punkt" })
362
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children: "Klik en kropsdel for at markere smerte." })
504
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children: "V\xE6lg en kropsdel for at markere smerte." })
363
505
  ] });
364
506
  }
365
507