@broberg/bodymap 0.14.0 → 0.16.0

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/preact.js ADDED
@@ -0,0 +1,779 @@
1
+ import { useEffect, useState, useRef } from 'preact/hooks';
2
+ import { z } from 'zod';
3
+ import { jsxs, jsx } from 'preact/jsx-runtime';
4
+
5
+ // src/react.tsx
6
+ var REGIONS = [
7
+ // axis / centre-line (serialised side "center")
8
+ { key: "head", label: "Hoved", code: "HEAD" },
9
+ { key: "neck", label: "Nakke", code: "NECK" },
10
+ { key: "chest", label: "Bryst", code: "CHEST" },
11
+ { key: "thora", label: "\xD8vre ryg (thorakal)", code: "THORA" },
12
+ { key: "lumbar", label: "L\xE6nd (lumbal)", code: "LUMBAR" },
13
+ { key: "groin", label: "Lyske", code: "GROIN" },
14
+ // paired limbs / sides (L / R)
15
+ { key: "shoulder_left", label: "Skulder, venstre", code: "SHOULDER", side: "left" },
16
+ { key: "shoulder_right", label: "Skulder, h\xF8jre", code: "SHOULDER", side: "right" },
17
+ { key: "uarm_left", label: "Overarm, venstre", code: "UARM", side: "left" },
18
+ { key: "uarm_right", label: "Overarm, h\xF8jre", code: "UARM", side: "right" },
19
+ { key: "elbow_left", label: "Albue, venstre", code: "ELBOW", side: "left" },
20
+ { key: "elbow_right", label: "Albue, h\xF8jre", code: "ELBOW", side: "right" },
21
+ { key: "farm_left", label: "Underarm, venstre", code: "FARM", side: "left" },
22
+ { key: "farm_right", label: "Underarm, h\xF8jre", code: "FARM", side: "right" },
23
+ { key: "wrist_left", label: "H\xE5ndled, venstre", code: "WRIST", side: "left" },
24
+ { key: "wrist_right", label: "H\xE5ndled, h\xF8jre", code: "WRIST", side: "right" },
25
+ { key: "hand_left", label: "H\xE5nd, venstre", code: "HAND", side: "left" },
26
+ { key: "hand_right", label: "H\xE5nd, h\xF8jre", code: "HAND", side: "right" },
27
+ { key: "hip_left", label: "Hofte, venstre", code: "HIP", side: "left" },
28
+ { key: "hip_right", label: "Hofte, h\xF8jre", code: "HIP", side: "right" },
29
+ { key: "thigh_left", label: "L\xE5r, venstre", code: "THIGH", side: "left" },
30
+ { key: "thigh_right", label: "L\xE5r, h\xF8jre", code: "THIGH", side: "right" },
31
+ { key: "knee_left", label: "Kn\xE6, venstre", code: "KNEE", side: "left" },
32
+ { key: "knee_right", label: "Kn\xE6, h\xF8jre", code: "KNEE", side: "right" },
33
+ { key: "lowleg_left", label: "Underben, venstre", code: "LOWLEG", side: "left" },
34
+ { key: "lowleg_right", label: "Underben, h\xF8jre", code: "LOWLEG", side: "right" },
35
+ { key: "ankle_left", label: "Ankel, venstre", code: "ANKLE", side: "left" },
36
+ { key: "ankle_right", label: "Ankel, h\xF8jre", code: "ANKLE", side: "right" },
37
+ { key: "foot_left", label: "Fod, venstre", code: "FOOT", side: "left" },
38
+ { key: "foot_right", label: "Fod, h\xF8jre", code: "FOOT", side: "right" }
39
+ ];
40
+ var REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));
41
+ REGIONS.map((r) => r.key);
42
+ function getRegion(key) {
43
+ return REGIONS.find((r) => r.key === key);
44
+ }
45
+ var PAIN_TYPES = ["stikkende", "dump", "konstant", "jagende"];
46
+ var painPointSchema = z.object({
47
+ region: z.string().refine((k) => REGION_KEY_SET.has(k), { message: "unknown region" }),
48
+ intensity: z.number().int().min(0).max(10),
49
+ type: z.enum(PAIN_TYPES).optional(),
50
+ timestamp: z.string()
51
+ });
52
+ z.array(painPointSchema).max(REGIONS.length);
53
+ function resolveRegions(config = {}) {
54
+ return REGIONS.filter((r) => config[r.key]?.visible ?? true);
55
+ }
56
+ function decidePick(region, report, config = {}) {
57
+ if (!isSelectable(region, config)) return "ignore";
58
+ return report.some((p) => p.region === region) ? "clear" : "select";
59
+ }
60
+ function isSelectable(key, config = {}) {
61
+ const s = config[key];
62
+ if (s?.visible === false) return false;
63
+ return s?.selectable ?? true;
64
+ }
65
+ var VIBRATION_PATTERNS = {
66
+ select: [12],
67
+ clear: [8, 40, 8],
68
+ ignore: []
69
+ };
70
+ function requestVibration(pattern, nav = globalThis.navigator) {
71
+ if (pattern.length === 0) return "skipped";
72
+ if (typeof nav?.vibrate !== "function") return "unsupported";
73
+ try {
74
+ return nav.vibrate([...pattern]) ? "requested" : "declined";
75
+ } catch {
76
+ return "declined";
77
+ }
78
+ }
79
+ function emitFeedback(outcome, region, opts = {}) {
80
+ opts.onFeedback?.({ outcome, region });
81
+ if (opts.haptics === false) return "skipped";
82
+ return requestVibration(VIBRATION_PATTERNS[outcome], opts.nav);
83
+ }
84
+ var defaultPalette = {
85
+ body: "#d2d7de",
86
+ hover: "#8fd0cd",
87
+ selected: "#5cc4b7",
88
+ heat: { low: "#fcd34d", mid: "#fb923c", high: "#ef4444" }
89
+ };
90
+ function heatFor(intensity, palette = defaultPalette) {
91
+ return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;
92
+ }
93
+ function baseColorFor(regionKey, palette = defaultPalette) {
94
+ return palette.regions?.[regionKey] ?? palette.body;
95
+ }
96
+ z.object({
97
+ schema: z.literal("bodymap/v1"),
98
+ view: z.enum(["front", "back", "left", "right"]),
99
+ points: z.array(
100
+ z.object({
101
+ region: z.string(),
102
+ side: z.enum(["left", "right", "center"]),
103
+ intensity: z.number().int().min(0).max(10),
104
+ quality: z.enum(PAIN_TYPES).optional()
105
+ })
106
+ ).max(REGIONS.length)
107
+ });
108
+ var SHAPES = {
109
+ head: { el: "circle", cx: 130, cy: 40, r: 26 },
110
+ neck: { el: "rect", x: 120, y: 62, width: 20, height: 16, rx: 6 },
111
+ chest: { el: "rect", x: 100, y: 84, width: 60, height: 104, rx: 16 },
112
+ groin: { el: "rect", x: 106, y: 190, width: 48, height: 30, rx: 13 },
113
+ // left arm (viewer-left)
114
+ shoulder_left: { el: "circle", cx: 88, cy: 98, r: 14 },
115
+ uarm_left: { el: "rect", x: 66, y: 104, width: 18, height: 54, rx: 9 },
116
+ elbow_left: { el: "circle", cx: 73, cy: 164, r: 10 },
117
+ farm_left: { el: "rect", x: 62, y: 174, width: 16, height: 50, rx: 8 },
118
+ wrist_left: { el: "circle", cx: 68, cy: 230, r: 8 },
119
+ hand_left: { el: "ellipse", cx: 67, cy: 250, rx: 11, ry: 13 },
120
+ // right arm (viewer-right)
121
+ shoulder_right: { el: "circle", cx: 172, cy: 98, r: 14 },
122
+ uarm_right: { el: "rect", x: 176, y: 104, width: 18, height: 54, rx: 9 },
123
+ elbow_right: { el: "circle", cx: 187, cy: 164, r: 10 },
124
+ farm_right: { el: "rect", x: 182, y: 174, width: 16, height: 50, rx: 8 },
125
+ wrist_right: { el: "circle", cx: 192, cy: 230, r: 8 },
126
+ hand_right: { el: "ellipse", cx: 193, cy: 250, rx: 11, ry: 13 },
127
+ // legs
128
+ thigh_left: { el: "rect", x: 104, y: 224, width: 22, height: 80, rx: 11 },
129
+ thigh_right: { el: "rect", x: 134, y: 224, width: 22, height: 80, rx: 11 },
130
+ knee_left: { el: "circle", cx: 115, cy: 312, r: 13 },
131
+ knee_right: { el: "circle", cx: 145, cy: 312, r: 13 },
132
+ lowleg_left: { el: "rect", x: 106, y: 322, width: 18, height: 74, rx: 9 },
133
+ lowleg_right: { el: "rect", x: 136, y: 322, width: 18, height: 74, rx: 9 },
134
+ ankle_left: { el: "circle", cx: 115, cy: 404, r: 9 },
135
+ ankle_right: { el: "circle", cx: 145, cy: 404, r: 9 },
136
+ foot_left: { el: "ellipse", cx: 115, cy: 424, rx: 14, ry: 10 },
137
+ foot_right: { el: "ellipse", cx: 145, cy: 424, rx: 14, ry: 10 }
138
+ };
139
+ var FILL = {
140
+ head: "#cbb7ec",
141
+ neck: "#b8a3e6",
142
+ chest: "#e26d6d",
143
+ groin: "#e88a52",
144
+ shoulder_left: "#f6c667",
145
+ shoulder_right: "#f6c667",
146
+ uarm_left: "#7ececb",
147
+ uarm_right: "#7ececb",
148
+ elbow_left: "#86cfcc",
149
+ elbow_right: "#86cfcc",
150
+ farm_left: "#8fd0cd",
151
+ farm_right: "#8fd0cd",
152
+ wrist_left: "#9ad6d3",
153
+ wrist_right: "#9ad6d3",
154
+ hand_left: "#a7ddda",
155
+ hand_right: "#a7ddda",
156
+ thigh_left: "#7fb2e8",
157
+ thigh_right: "#7fb2e8",
158
+ knee_left: "#c79be0",
159
+ knee_right: "#c79be0",
160
+ lowleg_left: "#e39ec6",
161
+ lowleg_right: "#e39ec6",
162
+ ankle_left: "#eeaab9",
163
+ ankle_right: "#eeaab9",
164
+ foot_left: "#f0a5a5",
165
+ foot_right: "#f0a5a5"
166
+ };
167
+ var SHAPES_BACK = {
168
+ head: { el: "circle", cx: 130, cy: 40, r: 26 },
169
+ neck: { el: "rect", x: 120, y: 62, width: 20, height: 16, rx: 6 },
170
+ thora: { el: "rect", x: 100, y: 84, width: 60, height: 58, rx: 15 },
171
+ lumbar: { el: "rect", x: 104, y: 144, width: 52, height: 44, rx: 13 },
172
+ hip_left: { el: "rect", x: 100, y: 190, width: 27, height: 32, rx: 13 },
173
+ hip_right: { el: "rect", x: 133, y: 190, width: 27, height: 32, rx: 13 },
174
+ // arms — shared with the front (back of the arm sits at the same x/y)
175
+ shoulder_left: { el: "circle", cx: 88, cy: 98, r: 14 },
176
+ uarm_left: { el: "rect", x: 66, y: 104, width: 18, height: 54, rx: 9 },
177
+ elbow_left: { el: "circle", cx: 73, cy: 164, r: 10 },
178
+ farm_left: { el: "rect", x: 62, y: 174, width: 16, height: 50, rx: 8 },
179
+ wrist_left: { el: "circle", cx: 68, cy: 230, r: 8 },
180
+ hand_left: { el: "ellipse", cx: 67, cy: 250, rx: 11, ry: 13 },
181
+ shoulder_right: { el: "circle", cx: 172, cy: 98, r: 14 },
182
+ uarm_right: { el: "rect", x: 176, y: 104, width: 18, height: 54, rx: 9 },
183
+ elbow_right: { el: "circle", cx: 187, cy: 164, r: 10 },
184
+ farm_right: { el: "rect", x: 182, y: 174, width: 16, height: 50, rx: 8 },
185
+ wrist_right: { el: "circle", cx: 192, cy: 230, r: 8 },
186
+ hand_right: { el: "ellipse", cx: 193, cy: 250, rx: 11, ry: 13 },
187
+ // legs — back (thigh→calf→heel, same x/y as the front)
188
+ thigh_left: { el: "rect", x: 104, y: 224, width: 22, height: 80, rx: 11 },
189
+ thigh_right: { el: "rect", x: 134, y: 224, width: 22, height: 80, rx: 11 },
190
+ knee_left: { el: "circle", cx: 115, cy: 312, r: 13 },
191
+ knee_right: { el: "circle", cx: 145, cy: 312, r: 13 },
192
+ lowleg_left: { el: "rect", x: 106, y: 322, width: 18, height: 74, rx: 9 },
193
+ lowleg_right: { el: "rect", x: 136, y: 322, width: 18, height: 74, rx: 9 },
194
+ ankle_left: { el: "circle", cx: 115, cy: 404, r: 9 },
195
+ ankle_right: { el: "circle", cx: 145, cy: 404, r: 9 },
196
+ foot_left: { el: "ellipse", cx: 115, cy: 424, rx: 14, ry: 10 },
197
+ foot_right: { el: "ellipse", cx: 145, cy: 424, rx: 14, ry: 10 }
198
+ };
199
+ var FILL_BACK = {
200
+ ...FILL,
201
+ thora: "#e2896d",
202
+ lumbar: "#d97fa0",
203
+ hip_left: "#c99bd6",
204
+ hip_right: "#c99bd6"
205
+ };
206
+ var daRegions = Object.fromEntries(REGIONS.map((r) => [r.key, r.label]));
207
+ var EN_CODE = {
208
+ HEAD: "Head",
209
+ NECK: "Neck",
210
+ CHEST: "Chest",
211
+ THORA: "Upper back",
212
+ LUMBAR: "Lower back",
213
+ GROIN: "Groin",
214
+ SHOULDER: "Shoulder",
215
+ UARM: "Upper arm",
216
+ ELBOW: "Elbow",
217
+ FARM: "Forearm",
218
+ WRIST: "Wrist",
219
+ HAND: "Hand",
220
+ HIP: "Hip",
221
+ THIGH: "Thigh",
222
+ KNEE: "Knee",
223
+ LOWLEG: "Lower leg",
224
+ ANKLE: "Ankle",
225
+ FOOT: "Foot"
226
+ };
227
+ var enRegions = Object.fromEntries(
228
+ REGIONS.map((r) => [r.key, EN_CODE[r.code] + (r.side ? r.side === "left" ? ", left" : ", right" : "")])
229
+ );
230
+ var LABELS_DA = {
231
+ regions: daRegions,
232
+ qualities: { stikkende: "stikkende", dump: "dump", konstant: "konstant", jagende: "jagende" },
233
+ intensity: "Intensitet (0-10)",
234
+ quality: "Kvalitet",
235
+ remove: "Fjern punkt",
236
+ ask: "Hvor ondt g\xF8r det? V\xE6lg et tal.",
237
+ close: "Luk",
238
+ front: "Forfra",
239
+ back: "Bagfra",
240
+ empty: "V\xE6lg en kropsdel for at markere smerte.",
241
+ before: "F\xF8r",
242
+ after: "Efter",
243
+ noChange: "Ingen \xE6ndring",
244
+ change: { new: "nyt", resolved: "forsvundet", improved: "bedre", worse: "v\xE6rre" },
245
+ ariaMarked: (n, i, q) => `${n}, smerte ${i} af 10${q ? ", " + q : ""}`,
246
+ ariaUnmarked: (n) => `${n}, ikke markeret. Aktiv\xE9r for at markere smerte.`,
247
+ svgLabel: "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
248
+ viewLabel: "Visning",
249
+ zoomLabel: "Zoom",
250
+ zoomIn: "Zoom ind",
251
+ zoomOut: "Zoom ud",
252
+ zoomReset: "Nulstil zoom"
253
+ };
254
+ var LABELS_EN = {
255
+ regions: enRegions,
256
+ qualities: { stikkende: "stabbing", dump: "dull", konstant: "constant", jagende: "shooting" },
257
+ intensity: "Intensity (0-10)",
258
+ quality: "Quality",
259
+ remove: "Remove point",
260
+ ask: "How much does it hurt? Pick a number.",
261
+ close: "Close",
262
+ front: "Front",
263
+ back: "Back",
264
+ empty: "Pick a body part to mark pain.",
265
+ before: "Before",
266
+ after: "After",
267
+ noChange: "No change",
268
+ change: { new: "new", resolved: "resolved", improved: "improved", worse: "worse" },
269
+ ariaMarked: (n, i, q) => `${n}, pain ${i} of 10${q ? ", " + q : ""}`,
270
+ ariaUnmarked: (n) => `${n}, not marked. Activate to mark pain.`,
271
+ svgLabel: "Body map \u2014 pick where it hurts",
272
+ viewLabel: "View",
273
+ zoomLabel: "Zoom",
274
+ zoomIn: "Zoom in",
275
+ zoomOut: "Zoom out",
276
+ zoomReset: "Reset zoom"
277
+ };
278
+ function resolveLabels(locale, overrides) {
279
+ const base = locale === "en" ? LABELS_EN : LABELS_DA;
280
+ if (!overrides) return base;
281
+ return {
282
+ ...base,
283
+ ...overrides,
284
+ regions: { ...base.regions, ...overrides.regions },
285
+ qualities: { ...base.qualities, ...overrides.qualities }
286
+ };
287
+ }
288
+ var VBW = 260;
289
+ var VBH = 470;
290
+ var MIN_HIT = 18;
291
+ function center(s) {
292
+ if (s.el === "rect") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };
293
+ return { x: s.cx, y: s.cy };
294
+ }
295
+ function hitShape(s) {
296
+ if (s.el === "circle") return { el: "circle", cx: s.cx, cy: s.cy, r: Math.max(s.r, MIN_HIT) };
297
+ if (s.el === "ellipse")
298
+ return { el: "ellipse", cx: s.cx, cy: s.cy, rx: Math.max(s.rx, MIN_HIT), ry: Math.max(s.ry, MIN_HIT) };
299
+ const c = center(s);
300
+ const w = Math.max(s.width, MIN_HIT * 2);
301
+ const h = Math.max(s.height, MIN_HIT * 2);
302
+ return { el: "rect", x: c.x - w / 2, y: c.y - h / 2, width: w, height: h, rx: s.rx };
303
+ }
304
+ function heat(v) {
305
+ return v >= 7 ? "#ef4444" : v >= 4 ? "#fb923c" : "#fcd34d";
306
+ }
307
+ function heatTier(v) {
308
+ return v >= 7 ? "high" : v >= 4 ? "mid" : "low";
309
+ }
310
+ function baseFill(key, rainbow, palette) {
311
+ return palette ? baseColorFor(key, palette) : `var(--bmap-region-${key}, var(--bmap-body, ${rainbow}))`;
312
+ }
313
+ function markedFill(v, palette) {
314
+ return palette ? heatFor(v, palette) : `var(--bmap-heat-${heatTier(v)}, ${heat(v)})`;
315
+ }
316
+ function shapeEl(s, props) {
317
+ if (s.el === "circle") return /* @__PURE__ */ jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...props });
318
+ if (s.el === "ellipse") return /* @__PURE__ */ jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...props });
319
+ return /* @__PURE__ */ jsx("rect", { x: s.x, y: s.y, width: s.width, height: s.height, rx: s.rx, ...props });
320
+ }
321
+ function chromeStyle(palette) {
322
+ if (!palette) return void 0;
323
+ const ui = palette.ui ?? {};
324
+ const vars = { "--bmap-accent": palette.selected };
325
+ if (ui.text) vars["--bmap-ink"] = ui.text;
326
+ if (ui.mutedText) vars["--bmap-muted"] = ui.mutedText;
327
+ if (ui.panelBg) vars["--bmap-panel"] = ui.panelBg;
328
+ if (ui.border) vars["--bmap-line"] = ui.border;
329
+ if (ui.badgeBg) vars["--bmap-badge-bg"] = ui.badgeBg;
330
+ if (ui.danger) vars["--bmap-danger"] = ui.danger;
331
+ return vars;
332
+ }
333
+ var STYLE_ID = "broberg-bodymap-styles";
334
+ var STYLE = `
335
+ .bmap{--bmap-accent:var(--primary,#0c7d77);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#475569;--bmap-badge-bg:#f1f5f9;--bmap-danger:#dc2626;
336
+ 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)}
337
+ .bmap__stage{display:flex;flex-direction:column;gap:10px;flex:0 1 340px;min-width:0;max-width:360px}
338
+ .bmap__bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
339
+ .bmap__viewtoggle{display:inline-flex;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}
340
+ .bmap__vbtn{font:inherit;font-size:13px;font-weight:600;color:#556274;background:none;border:0;border-radius:7px;padding:8px 16px;cursor:pointer;transition:color .12s,background .12s,transform .1s}
341
+ .bmap__vbtn:hover{color:var(--bmap-ink)}
342
+ .bmap__vbtn:active{transform:scale(.97)}
343
+ .bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}
344
+ .bmap__zoom{display:inline-flex;gap:4px;margin-left:auto}
345
+ .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}
346
+ .bmap__zoom button:hover{border-color:var(--bmap-accent)}
347
+ .bmap__zoom button:active{transform:scale(.9)}
348
+ .bmap__zoom button:disabled{opacity:.4;cursor:default}
349
+ .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}
350
+ .bmap__vis{stroke:#fff;stroke-width:2.4;pointer-events:none;transition:filter .12s,stroke .12s,stroke-width .12s}
351
+ .bmap__vis--sel{stroke:var(--bmap-accent);stroke-width:3.6}
352
+ .bmap__hit{fill:transparent;cursor:pointer;outline:none}
353
+ .bmap__hit--locked{cursor:default}
354
+ .bmap__hit:hover + .bmap__vis{filter:brightness(1.08) saturate(1.15)}
355
+ .bmap__hit:focus-visible + .bmap__vis{stroke:var(--bmap-accent);stroke-width:4;stroke-dasharray:4 2.5}
356
+ .bmap__heatn{font:700 12px ui-sans-serif;fill:#fff;pointer-events:none}
357
+ .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)}
358
+ .bmap__panel--empty{color:var(--bmap-muted);font-size:13.5px}
359
+ .bmap__ph{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}
360
+ .bmap__ph b{font-size:16px}
361
+ .bmap__close{margin-left:auto;font:inherit;font-size:20px;line-height:1;cursor:pointer;background:none;border:1px solid transparent;border-radius:8px;width:32px;height:32px;color:var(--bmap-muted,#475569)}
362
+ .bmap__close:hover{background:var(--bmap-badge-bg,#f1f5f9)}
363
+ .bmap__close:active{transform:scale(.94)}
364
+ .bmap__close:focus-visible{outline:2px solid var(--bmap-accent,#0c7d77);outline-offset:2px}
365
+ .bmap__code{font:11px ui-monospace,monospace;color:var(--bmap-muted);background:var(--bmap-badge-bg);border-radius:6px;padding:2px 7px}
366
+ .bmap__ask{font-size:15px;font-weight:600;color:var(--bmap-ink);margin:0 0 10px}
367
+ .bmap__lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bmap-muted);margin:0 0 7px}
368
+ .bmap__scale{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
369
+ .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}
370
+ .bmap__i:hover{border-color:var(--bmap-accent)}
371
+ .bmap__i:active{transform:scale(.9)}
372
+ .bmap__i--on{background:var(--bmap-accent);border-color:var(--bmap-accent);color:#fff}
373
+ .bmap__chips{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}
374
+ .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}
375
+ .bmap__chip:hover{border-color:var(--bmap-accent);color:var(--bmap-accent)}
376
+ .bmap__chip:active{transform:scale(.96)}
377
+ .bmap__chip--on{background:var(--bmap-ink);border-color:var(--bmap-ink);color:#fff}
378
+ .bmap__rm{font:inherit;font-size:14px;font-weight:600;border:1px solid #f6c9c9;background:#fff;color:var(--bmap-danger);border-radius:9px;padding:10px 15px;cursor:pointer;transition:.1s}
379
+ .bmap__rm:hover{background:#fef2f2}
380
+ .bmap__rm:active{transform:scale(.97)}
381
+ .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}
382
+ .bmapc{--bmap-accent:var(--primary,#0c7d77);--bmap-line:#e2e8f0;--bmap-ink:#1e293b;--bmap-muted:#475569;font:15px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--bmap-ink)}
383
+ .bmapc__bodies{display:flex;gap:14px;flex-wrap:wrap}
384
+ .bmapc__fig{flex:1 1 150px;min-width:130px;max-width:220px;margin:0;text-align:center}
385
+ .bmapc__cap{font-size:12px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#556274;margin-bottom:6px}
386
+ .bmapc__svg{width:100%;height:auto;display:block}
387
+ .bmapc__vis{stroke:#fff;stroke-width:2.4}
388
+ .bmapc__n{font:700 12px ui-sans-serif;fill:#fff;pointer-events:none}
389
+ .bmapc__delta{margin-top:16px;display:flex;flex-direction:column;gap:6px;max-width:460px}
390
+ .bmapc__row{display:flex;align-items:center;gap:10px;font-size:13.5px;border:1px solid var(--bmap-line);border-radius:9px;padding:7px 11px}
391
+ .bmapc__rname{font-weight:600;flex:1 1 auto}
392
+ .bmapc__rval{font:12px ui-monospace,monospace;color:#556274;white-space:nowrap}
393
+ .bmapc__badge{font-size:11px;font-weight:700;border-radius:6px;padding:2px 8px;white-space:nowrap}
394
+ .bmapc__badge--improved{background:#dcfce7;color:#166534}
395
+ .bmapc__badge--worse{background:#fee2e2;color:#991b1b}
396
+ .bmapc__badge--new{background:#fef3c7;color:#92400e}
397
+ .bmapc__badge--resolved{background:#e2e8f0;color:#475569}
398
+ .bmapc__empty{color:#556274;font-size:13.5px;margin-top:14px}
399
+ @media (max-width:560px){
400
+ .bmap{flex-direction:column;gap:14px}
401
+ .bmap__stage{max-width:100%;width:100%;flex-basis:auto}
402
+ .bmap__panel{width:100%}
403
+ .bmap__i{width:46px;height:46px;font-size:16px}
404
+ .bmap__chip{padding:11px 17px;font-size:15px}
405
+ .bmap__vbtn{padding:10px 18px;font-size:15px}
406
+ }
407
+ @media (prefers-reduced-motion:reduce){.bmap *{transition:none !important}}
408
+ `;
409
+ function ensureStyles() {
410
+ if (typeof document === "undefined") return;
411
+ if (document.getElementById(STYLE_ID)) return;
412
+ const el = document.createElement("style");
413
+ el.id = STYLE_ID;
414
+ el.textContent = STYLE;
415
+ document.head.appendChild(el);
416
+ }
417
+ var ZMIN = 1;
418
+ var ZMAX = 4;
419
+ var clampPan = (p, z2) => ({
420
+ x: Math.min(0, Math.max(VBW * (1 - z2), p.x)),
421
+ y: Math.min(0, Math.max(VBH * (1 - z2), p.y))
422
+ });
423
+ function BodyMap({
424
+ value,
425
+ defaultValue,
426
+ onChange,
427
+ config = {},
428
+ defaultView = "front",
429
+ onViewChange,
430
+ palette,
431
+ locale = "da",
432
+ labels,
433
+ readOnly = false,
434
+ onFeedback,
435
+ haptics,
436
+ className
437
+ }) {
438
+ useEffect(ensureStyles, []);
439
+ const [internal, setInternal] = useState(defaultValue ?? []);
440
+ const [selected, setSelected] = useState(null);
441
+ const [view, setView] = useState(defaultView);
442
+ const [zoom, setZoom] = useState(1);
443
+ const [pan, setPan] = useState({ x: 0, y: 0 });
444
+ const report = value ?? internal;
445
+ const shapes = view === "back" ? SHAPES_BACK : SHAPES;
446
+ const fills = view === "back" ? FILL_BACK : FILL;
447
+ const L = resolveLabels(locale, labels);
448
+ const nameOf = (key) => L.regions[key] ?? getRegion(key)?.label ?? key;
449
+ const svgRef = useRef(null);
450
+ const zoomRef = useRef(1);
451
+ const panRef = useRef({ x: 0, y: 0 });
452
+ const pointers = useRef(/* @__PURE__ */ new Map());
453
+ const gesture = useRef({ startDist: 0, startZoom: 1, midVB: { x: 0, y: 0 }, start: { x: 0, y: 0 }, moved: false });
454
+ const suppressClick = useRef(false);
455
+ const setZP = (z2, p) => {
456
+ const cz = Math.max(ZMIN, Math.min(ZMAX, z2));
457
+ const cp = clampPan(p, cz);
458
+ zoomRef.current = cz;
459
+ panRef.current = cp;
460
+ setZoom(cz);
461
+ setPan(cp);
462
+ };
463
+ const screenToVB = (cx, cy) => {
464
+ const el = svgRef.current;
465
+ if (!el) return { x: VBW / 2, y: VBH / 2 };
466
+ const r = el.getBoundingClientRect();
467
+ if (!r.width || !r.height) return { x: VBW / 2, y: VBH / 2 };
468
+ return { x: (cx - r.left) / r.width * VBW, y: (cy - r.top) / r.height * VBH };
469
+ };
470
+ const zoomTo = (nz, focalVB) => {
471
+ const z2 = Math.max(ZMIN, Math.min(ZMAX, nz));
472
+ const p = panRef.current;
473
+ setZP(z2, { x: focalVB.x - (focalVB.x - p.x) * (z2 / zoomRef.current), y: focalVB.y - (focalVB.y - p.y) * (z2 / zoomRef.current) });
474
+ };
475
+ const zoomBtn = (factor) => zoomTo(zoomRef.current * factor, { x: VBW / 2, y: VBH / 2 });
476
+ const resetZoom = () => setZP(1, { x: 0, y: 0 });
477
+ const onPointerDown = (e) => {
478
+ pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
479
+ const pts = [...pointers.current.values()];
480
+ if (pts.length === 2) {
481
+ gesture.current.startDist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
482
+ gesture.current.startZoom = zoomRef.current;
483
+ gesture.current.midVB = screenToVB((pts[0].x + pts[1].x) / 2, (pts[0].y + pts[1].y) / 2);
484
+ gesture.current.moved = true;
485
+ suppressClick.current = true;
486
+ } else {
487
+ gesture.current.start = { x: e.clientX, y: e.clientY };
488
+ gesture.current.moved = false;
489
+ }
490
+ };
491
+ const onPointerMove = (e) => {
492
+ const prev = pointers.current.get(e.pointerId);
493
+ if (!prev) return;
494
+ pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
495
+ const pts = [...pointers.current.values()];
496
+ if (pts.length >= 2) {
497
+ const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
498
+ if (gesture.current.startDist > 0) zoomTo(gesture.current.startZoom * (dist / gesture.current.startDist), gesture.current.midVB);
499
+ return;
500
+ }
501
+ const el = svgRef.current;
502
+ const rect = el?.getBoundingClientRect();
503
+ if (Math.hypot(e.clientX - gesture.current.start.x, e.clientY - gesture.current.start.y) > 6) {
504
+ gesture.current.moved = true;
505
+ suppressClick.current = true;
506
+ }
507
+ if (zoomRef.current > 1 && rect && rect.width) {
508
+ const dxVB = (e.clientX - prev.x) / rect.width * VBW;
509
+ const dyVB = (e.clientY - prev.y) / rect.height * VBH;
510
+ setZP(zoomRef.current, { x: panRef.current.x + dxVB, y: panRef.current.y + dyVB });
511
+ }
512
+ };
513
+ const onPointerUp = (e) => {
514
+ pointers.current.delete(e.pointerId);
515
+ if (pointers.current.size === 0) window.setTimeout(() => {
516
+ suppressClick.current = false;
517
+ }, 60);
518
+ };
519
+ const changeView = (v) => {
520
+ if (v === view) return;
521
+ setView(v);
522
+ const sh = v === "back" ? SHAPES_BACK : SHAPES;
523
+ if (selected && !sh[selected]) setSelected(null);
524
+ onViewChange?.(v);
525
+ };
526
+ const commit = (next) => {
527
+ if (value === void 0) setInternal(next);
528
+ onChange?.(next);
529
+ };
530
+ const pointOf = (key) => report.find((p) => p.region === key);
531
+ const setPain = (key, intensity, type) => {
532
+ const point = painPointSchema.parse({ region: key, intensity, type, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
533
+ commit([...report.filter((p) => p.region !== key), point]);
534
+ };
535
+ const removePain = (key) => {
536
+ commit(report.filter((p) => p.region !== key));
537
+ setSelected(null);
538
+ };
539
+ const applyPick = (key) => {
540
+ const what = decidePick(key, report, config);
541
+ emitFeedback(what, key, { onFeedback, haptics });
542
+ if (what === "ignore") return;
543
+ if (what === "clear") {
544
+ removePain(key);
545
+ return;
546
+ }
547
+ setSelected(key);
548
+ };
549
+ const pickRegion = (key) => {
550
+ if (suppressClick.current) {
551
+ suppressClick.current = false;
552
+ return;
553
+ }
554
+ applyPick(key);
555
+ };
556
+ const regions = resolveRegions(config);
557
+ const region = selected ? REGIONS.find((r) => r.key === selected) : void 0;
558
+ const current = selected ? pointOf(selected) : void 0;
559
+ const rootCls = ["bmap", className].filter(Boolean).join(" ");
560
+ const rootStyle = chromeStyle(palette);
561
+ const ariaFor = (r, marked, interactive = true) => {
562
+ if (marked) return L.ariaMarked(nameOf(r.key), marked.intensity, marked.type ? L.qualities[marked.type] : void 0);
563
+ return interactive ? L.ariaUnmarked(nameOf(r.key)) : nameOf(r.key);
564
+ };
565
+ return /* @__PURE__ */ jsxs("div", { className: rootCls, "data-testid": "bodymap-root", style: rootStyle, children: [
566
+ /* @__PURE__ */ jsxs("div", { className: "bmap__stage", children: [
567
+ /* @__PURE__ */ jsxs("div", { className: "bmap__bar", children: [
568
+ /* @__PURE__ */ jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": L.viewLabel, children: [
569
+ /* @__PURE__ */ jsx(
570
+ "button",
571
+ {
572
+ type: "button",
573
+ className: "bmap__vbtn" + (view === "front" ? " bmap__vbtn--on" : ""),
574
+ "data-testid": "bodymap-view-front",
575
+ "aria-pressed": view === "front",
576
+ onClick: () => changeView("front"),
577
+ children: L.front
578
+ }
579
+ ),
580
+ /* @__PURE__ */ jsx(
581
+ "button",
582
+ {
583
+ type: "button",
584
+ className: "bmap__vbtn" + (view === "back" ? " bmap__vbtn--on" : ""),
585
+ "data-testid": "bodymap-view-back",
586
+ "aria-pressed": view === "back",
587
+ onClick: () => changeView("back"),
588
+ children: L.back
589
+ }
590
+ )
591
+ ] }),
592
+ /* @__PURE__ */ jsxs("div", { className: "bmap__zoom", role: "group", "aria-label": L.zoomLabel, children: [
593
+ /* @__PURE__ */ jsx("button", { type: "button", "data-testid": "bodymap-zoom-out", "aria-label": L.zoomOut, disabled: zoom <= ZMIN, onClick: () => zoomBtn(1 / 1.4), children: "\u2212" }),
594
+ /* @__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" }),
595
+ /* @__PURE__ */ jsx("button", { type: "button", "data-testid": "bodymap-zoom-in", "aria-label": L.zoomIn, disabled: zoom >= ZMAX, onClick: () => zoomBtn(1.4), children: "+" })
596
+ ] })
597
+ ] }),
598
+ /* @__PURE__ */ jsx(
599
+ "svg",
600
+ {
601
+ ref: svgRef,
602
+ className: "bmap__svg",
603
+ "data-testid": "bodymap-svg",
604
+ viewBox: "0 0 260 470",
605
+ role: "group",
606
+ "aria-label": L.svgLabel,
607
+ onPointerDown,
608
+ onPointerMove,
609
+ onPointerUp,
610
+ onPointerCancel: onPointerUp,
611
+ onWheel: (e) => zoomTo(zoomRef.current * (e.deltaY < 0 ? 1.12 : 1 / 1.12), screenToVB(e.clientX, e.clientY)),
612
+ children: /* @__PURE__ */ jsxs("g", { transform: `translate(${pan.x} ${pan.y}) scale(${zoom})`, children: [
613
+ regions.map((r) => {
614
+ const s = shapes[r.key];
615
+ if (!s) return null;
616
+ const marked = pointOf(r.key);
617
+ const interactive = isSelectable(r.key, config) && !readOnly;
618
+ const visCls = ["bmap__vis", selected === r.key && "bmap__vis--sel"].filter(Boolean).join(" ");
619
+ const fill = marked ? markedFill(marked.intensity, palette) : baseFill(r.key, fills[r.key] ?? "#cbd5e1", palette);
620
+ const hs = hitShape(s);
621
+ return /* @__PURE__ */ jsxs("g", { children: [
622
+ shapeEl(hs, {
623
+ className: "bmap__hit" + (interactive ? "" : " bmap__hit--locked"),
624
+ "data-testid": `bodymap-region-${r.key}`,
625
+ role: interactive ? "button" : "img",
626
+ tabIndex: interactive ? 0 : -1,
627
+ "aria-label": ariaFor(r, marked, interactive),
628
+ "aria-pressed": interactive ? selected === r.key : void 0,
629
+ // Click is attached whenever the map is EDITABLE, even on a
630
+ // locked region — the pick returns "ignore", changes nothing
631
+ // and does not buzz, but it reaches onFeedback so an app can
632
+ // say WHY nothing happened. Found by measurement (F052.22):
633
+ // the 3D renderer raycasts every mesh and already reported
634
+ // `ignore`, so gating this on `interactive` made the two
635
+ // renderers answer the same tap differently. ARIA is
636
+ // deliberately NOT widened — a locked region must not become
637
+ // a focusable button that does nothing.
638
+ onClick: readOnly ? void 0 : () => pickRegion(r.key),
639
+ onKeyDown: interactive ? (e) => {
640
+ if (e.key === "Enter" || e.key === " ") {
641
+ e.preventDefault();
642
+ suppressClick.current = false;
643
+ applyPick(r.key);
644
+ }
645
+ } : void 0
646
+ }),
647
+ shapeEl(s, { className: visCls, fill, opacity: !readOnly && !isSelectable(r.key, config) ? 0.4 : 1 })
648
+ ] }, r.key);
649
+ }),
650
+ report.map((p) => {
651
+ const s = shapes[p.region];
652
+ if (!s || config[p.region]?.visible === false) return null;
653
+ const c = center(s);
654
+ return /* @__PURE__ */ jsx("text", { x: c.x, y: c.y + 4, textAnchor: "middle", className: "bmap__heatn", children: p.intensity }, p.region);
655
+ })
656
+ ] })
657
+ }
658
+ )
659
+ ] }),
660
+ readOnly ? null : region ? /* @__PURE__ */ jsxs("div", { className: "bmap__panel", "data-testid": "bodymap-panel", children: [
661
+ /* @__PURE__ */ jsxs("div", { className: "bmap__ph", children: [
662
+ /* @__PURE__ */ jsx("b", { children: nameOf(region.key) }),
663
+ /* @__PURE__ */ jsx("span", { className: "bmap__code", children: region.code }),
664
+ /* @__PURE__ */ jsx(
665
+ "button",
666
+ {
667
+ type: "button",
668
+ className: "bmap__close",
669
+ "data-testid": "bodymap-close",
670
+ "aria-label": L.close,
671
+ onClick: () => setSelected(null),
672
+ children: "\xD7"
673
+ }
674
+ )
675
+ ] }),
676
+ !current && L.ask && /* @__PURE__ */ jsx("p", { className: "bmap__ask", "data-testid": "bodymap-ask", "aria-live": "polite", children: L.ask }),
677
+ /* @__PURE__ */ jsx("p", { className: "bmap__lbl", id: "bmap-intensity-lbl", children: L.intensity }),
678
+ /* @__PURE__ */ jsx("div", { className: "bmap__scale", role: "group", "aria-labelledby": "bmap-intensity-lbl", children: Array.from({ length: 11 }, (_, n) => /* @__PURE__ */ jsx(
679
+ "button",
680
+ {
681
+ type: "button",
682
+ className: "bmap__i" + (current?.intensity === n ? " bmap__i--on" : ""),
683
+ "data-testid": `bodymap-intensity-${n}`,
684
+ "aria-pressed": current?.intensity === n,
685
+ onClick: () => setPain(region.key, n, current?.type),
686
+ children: n
687
+ },
688
+ n
689
+ )) }),
690
+ /* @__PURE__ */ jsx("p", { className: "bmap__lbl", id: "bmap-quality-lbl", children: L.quality }),
691
+ /* @__PURE__ */ jsx("div", { className: "bmap__chips", role: "group", "aria-labelledby": "bmap-quality-lbl", children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsx(
692
+ "button",
693
+ {
694
+ type: "button",
695
+ className: "bmap__chip" + (current?.type === t ? " bmap__chip--on" : ""),
696
+ "data-testid": `bodymap-type-${t}`,
697
+ "aria-pressed": current?.type === t,
698
+ onClick: () => setPain(region.key, current?.intensity ?? 5, t),
699
+ children: L.qualities[t]
700
+ },
701
+ t
702
+ )) }),
703
+ current && /* @__PURE__ */ jsx("button", { type: "button", className: "bmap__rm", "data-testid": "bodymap-remove", onClick: () => removePain(region.key), children: L.remove })
704
+ ] }) : /* @__PURE__ */ jsx("div", { className: "bmap__panel bmap__panel--empty", "data-testid": "bodymap-panel", children: L.empty })
705
+ ] });
706
+ }
707
+ function changeOf(b, a) {
708
+ if (b == null && a != null) return "new";
709
+ if (b != null && a == null) return "resolved";
710
+ if (b == null || a == null) return "same";
711
+ return a < b ? "improved" : a > b ? "worse" : "same";
712
+ }
713
+ function CompareFigure({ report, view, palette, caption, testid }) {
714
+ const shapes = view === "back" ? SHAPES_BACK : SHAPES;
715
+ const fills = view === "back" ? FILL_BACK : FILL;
716
+ return /* @__PURE__ */ jsxs("figure", { className: "bmapc__fig", "data-testid": testid, children: [
717
+ /* @__PURE__ */ jsx("figcaption", { className: "bmapc__cap", children: caption }),
718
+ /* @__PURE__ */ jsxs("svg", { className: "bmapc__svg", viewBox: "0 0 260 470", role: "img", "aria-label": caption, children: [
719
+ REGIONS.map((r) => {
720
+ const s = shapes[r.key];
721
+ if (!s) return null;
722
+ const m = report.find((p) => p.region === r.key);
723
+ const fill = m ? markedFill(m.intensity, palette) : baseFill(r.key, fills[r.key] ?? "#cbd5e1", palette);
724
+ return /* @__PURE__ */ jsx("g", { children: shapeEl(s, { className: "bmapc__vis", fill }) }, r.key);
725
+ }),
726
+ report.map((p) => {
727
+ const s = shapes[p.region];
728
+ if (!s) return null;
729
+ const c = center(s);
730
+ return /* @__PURE__ */ jsx("text", { x: c.x, y: c.y + 4, textAnchor: "middle", className: "bmapc__n", children: p.intensity }, p.region);
731
+ })
732
+ ] })
733
+ ] });
734
+ }
735
+ function BodyMapCompare({
736
+ before,
737
+ after,
738
+ palette,
739
+ locale = "da",
740
+ labels,
741
+ defaultView = "front",
742
+ className
743
+ }) {
744
+ useEffect(ensureStyles, []);
745
+ const [view, setView] = useState(defaultView);
746
+ const L = resolveLabels(locale, labels);
747
+ const nameOf = (k) => L.regions[k] ?? getRegion(k)?.label ?? k;
748
+ const rootStyle = chromeStyle(palette);
749
+ const keys = Array.from(new Set([...before, ...after].map((p) => p.region)));
750
+ const changes = keys.map((k) => ({
751
+ key: k,
752
+ b: before.find((p) => p.region === k)?.intensity,
753
+ a: after.find((p) => p.region === k)?.intensity,
754
+ status: changeOf(before.find((p) => p.region === k)?.intensity, after.find((p) => p.region === k)?.intensity)
755
+ })).filter((c) => c.status !== "same").sort((x, y) => x.key < y.key ? -1 : 1);
756
+ return /* @__PURE__ */ jsxs("div", { className: ["bmapc", className].filter(Boolean).join(" "), "data-testid": "bodymap-compare", style: rootStyle, children: [
757
+ /* @__PURE__ */ jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": L.viewLabel, style: { marginBottom: 12 }, children: [
758
+ /* @__PURE__ */ jsx("button", { type: "button", className: "bmap__vbtn" + (view === "front" ? " bmap__vbtn--on" : ""), "data-testid": "bodymap-compare-view-front", "aria-pressed": view === "front", onClick: () => setView("front"), children: L.front }),
759
+ /* @__PURE__ */ jsx("button", { type: "button", className: "bmap__vbtn" + (view === "back" ? " bmap__vbtn--on" : ""), "data-testid": "bodymap-compare-view-back", "aria-pressed": view === "back", onClick: () => setView("back"), children: L.back })
760
+ ] }),
761
+ /* @__PURE__ */ jsxs("div", { className: "bmapc__bodies", children: [
762
+ /* @__PURE__ */ jsx(CompareFigure, { report: before, view, palette, caption: L.before, testid: "bodymap-compare-before" }),
763
+ /* @__PURE__ */ jsx(CompareFigure, { report: after, view, palette, caption: L.after, testid: "bodymap-compare-after" })
764
+ ] }),
765
+ changes.length ? /* @__PURE__ */ jsx("div", { className: "bmapc__delta", "data-testid": "bodymap-compare-delta", children: changes.map((c) => /* @__PURE__ */ jsxs("div", { className: "bmapc__row", "data-testid": `bodymap-compare-delta-${c.key}`, children: [
766
+ /* @__PURE__ */ jsx("span", { className: "bmapc__rname", children: nameOf(c.key) }),
767
+ /* @__PURE__ */ jsxs("span", { className: "bmapc__rval", children: [
768
+ c.b ?? "\u2013",
769
+ " \u2192 ",
770
+ c.a ?? "\u2013"
771
+ ] }),
772
+ /* @__PURE__ */ jsx("span", { className: `bmapc__badge bmapc__badge--${c.status}`, children: L.change[c.status] })
773
+ ] }, c.key)) }) : /* @__PURE__ */ jsx("div", { className: "bmapc__empty", "data-testid": "bodymap-compare-delta", children: L.noChange })
774
+ ] });
775
+ }
776
+
777
+ export { BodyMap, BodyMapCompare, LABELS_DA, LABELS_EN };
778
+ //# sourceMappingURL=preact.js.map
779
+ //# sourceMappingURL=preact.js.map