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