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