@broberg/bodymap 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -128,6 +128,45 @@ var FILL = {
128
128
  foot_left: "#f0a5a5",
129
129
  foot_right: "#f0a5a5"
130
130
  };
131
+ var SHAPES_BACK = {
132
+ head: { el: "circle", cx: 130, cy: 40, r: 26 },
133
+ neck: { el: "rect", x: 120, y: 62, width: 20, height: 16, rx: 6 },
134
+ thora: { el: "rect", x: 100, y: 84, width: 60, height: 58, rx: 15 },
135
+ lumbar: { el: "rect", x: 104, y: 144, width: 52, height: 44, rx: 13 },
136
+ hip_left: { el: "rect", x: 100, y: 190, width: 27, height: 32, rx: 13 },
137
+ hip_right: { el: "rect", x: 133, y: 190, width: 27, height: 32, rx: 13 },
138
+ // arms — shared with the front (back of the arm sits at the same x/y)
139
+ shoulder_left: { el: "circle", cx: 88, cy: 98, r: 14 },
140
+ uarm_left: { el: "rect", x: 66, y: 104, width: 18, height: 54, rx: 9 },
141
+ elbow_left: { el: "circle", cx: 73, cy: 164, r: 10 },
142
+ farm_left: { el: "rect", x: 62, y: 174, width: 16, height: 50, rx: 8 },
143
+ wrist_left: { el: "circle", cx: 68, cy: 230, r: 8 },
144
+ hand_left: { el: "ellipse", cx: 67, cy: 250, rx: 11, ry: 13 },
145
+ shoulder_right: { el: "circle", cx: 172, cy: 98, r: 14 },
146
+ uarm_right: { el: "rect", x: 176, y: 104, width: 18, height: 54, rx: 9 },
147
+ elbow_right: { el: "circle", cx: 187, cy: 164, r: 10 },
148
+ farm_right: { el: "rect", x: 182, y: 174, width: 16, height: 50, rx: 8 },
149
+ wrist_right: { el: "circle", cx: 192, cy: 230, r: 8 },
150
+ hand_right: { el: "ellipse", cx: 193, cy: 250, rx: 11, ry: 13 },
151
+ // legs — back (thigh→calf→heel, same x/y as the front)
152
+ thigh_left: { el: "rect", x: 104, y: 224, width: 22, height: 80, rx: 11 },
153
+ thigh_right: { el: "rect", x: 134, y: 224, width: 22, height: 80, rx: 11 },
154
+ knee_left: { el: "circle", cx: 115, cy: 312, r: 13 },
155
+ knee_right: { el: "circle", cx: 145, cy: 312, r: 13 },
156
+ lowleg_left: { el: "rect", x: 106, y: 322, width: 18, height: 74, rx: 9 },
157
+ lowleg_right: { el: "rect", x: 136, y: 322, width: 18, height: 74, rx: 9 },
158
+ ankle_left: { el: "circle", cx: 115, cy: 404, r: 9 },
159
+ ankle_right: { el: "circle", cx: 145, cy: 404, r: 9 },
160
+ foot_left: { el: "ellipse", cx: 115, cy: 424, rx: 14, ry: 10 },
161
+ foot_right: { el: "ellipse", cx: 145, cy: 424, rx: 14, ry: 10 }
162
+ };
163
+ var FILL_BACK = {
164
+ ...FILL,
165
+ thora: "#e2896d",
166
+ lumbar: "#d97fa0",
167
+ hip_left: "#c99bd6",
168
+ hip_right: "#c99bd6"
169
+ };
131
170
  function center(s) {
132
171
  if (s.el === "rect") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };
133
172
  return { x: s.cx, y: s.cy };
@@ -140,6 +179,12 @@ var STYLE = `
140
179
  .bmap{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#64748b;
141
180
  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)}
142
181
  .bmap__svg{flex:0 0 auto}
182
+ .bmap__stage{display:flex;flex-direction:column;gap:12px;flex:0 0 auto}
183
+ .bmap__viewtoggle{display:inline-flex;align-self:flex-start;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}
184
+ .bmap__vbtn{font:inherit;font-size:13px;font-weight:600;color:var(--bmap-muted);background:none;border:0;border-radius:7px;padding:6px 16px;cursor:pointer;transition:color .12s,background .12s,transform .1s}
185
+ .bmap__vbtn:hover{color:var(--bmap-ink)}
186
+ .bmap__vbtn:active{transform:scale(.97)}
187
+ .bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}
143
188
  .bmap__region{cursor:pointer;stroke:#fff;stroke-width:2.4;transition:filter .12s}
144
189
  .bmap__region:hover{filter:brightness(1.08) saturate(1.15)}
145
190
  .bmap__region--locked{cursor:default;opacity:.4}
@@ -173,11 +218,29 @@ function ensureStyles() {
173
218
  el.textContent = STYLE;
174
219
  document.head.appendChild(el);
175
220
  }
176
- function BodyMap({ value, defaultValue, onChange, config = {}, className }) {
221
+ function BodyMap({
222
+ value,
223
+ defaultValue,
224
+ onChange,
225
+ config = {},
226
+ defaultView = "front",
227
+ onViewChange,
228
+ className
229
+ }) {
177
230
  react.useEffect(ensureStyles, []);
178
231
  const [internal, setInternal] = react.useState(defaultValue ?? []);
179
232
  const [selected, setSelected] = react.useState(null);
233
+ const [view, setView] = react.useState(defaultView);
180
234
  const report = value ?? internal;
235
+ const shapes = view === "back" ? SHAPES_BACK : SHAPES;
236
+ const fills = view === "back" ? FILL_BACK : FILL;
237
+ const changeView = (v) => {
238
+ if (v === view) return;
239
+ setView(v);
240
+ const sh = v === "back" ? SHAPES_BACK : SHAPES;
241
+ if (selected && !sh[selected]) setSelected(null);
242
+ onViewChange?.(v);
243
+ };
181
244
  const commit = (next) => {
182
245
  if (value === void 0) setInternal(next);
183
246
  onChange?.(next);
@@ -196,48 +259,74 @@ function BodyMap({ value, defaultValue, onChange, config = {}, className }) {
196
259
  const current = selected ? pointOf(selected) : void 0;
197
260
  const rootCls = ["bmap", className].filter(Boolean).join(" ");
198
261
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rootCls, "data-testid": "bodymap-root", children: [
199
- /* @__PURE__ */ jsxRuntime.jsxs(
200
- "svg",
201
- {
202
- className: "bmap__svg",
203
- viewBox: "0 0 260 470",
204
- width: 230,
205
- height: 416,
206
- role: "group",
207
- "aria-label": "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
208
- children: [
209
- regions.map((r) => {
210
- const s = SHAPES[r.key];
211
- if (!s) return null;
212
- const marked = pointOf(r.key);
213
- const selectable = isSelectable(r.key, config);
214
- const cls = [
215
- "bmap__region",
216
- !selectable && "bmap__region--locked",
217
- selected === r.key && "bmap__region--sel"
218
- ].filter(Boolean).join(" ");
219
- const fill = marked ? heat(marked.intensity) : FILL[r.key] ?? "#cbd5e1";
220
- const common = {
221
- className: cls,
222
- fill,
223
- "data-testid": `bodymap-region-${r.key}`,
224
- role: selectable ? "button" : void 0,
225
- "aria-label": r.label,
226
- onClick: selectable ? () => setSelected(r.key) : void 0
227
- };
228
- if (s.el === "circle") return /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...common }, r.key);
229
- if (s.el === "ellipse") return /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...common }, r.key);
230
- return /* @__PURE__ */ jsxRuntime.jsx("rect", { x: s.x, y: s.y, width: s.width, height: s.height, rx: s.rx, ...common }, r.key);
231
- }),
232
- report.map((p) => {
233
- const s = SHAPES[p.region];
234
- if (!s || config[p.region]?.visible === false) return null;
235
- const c = center(s);
236
- return /* @__PURE__ */ jsxRuntime.jsx("text", { x: c.x, y: c.y + 4, textAnchor: "middle", className: "bmap__heatn", children: p.intensity }, p.region);
237
- })
238
- ]
239
- }
240
- ),
262
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__stage", children: [
263
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__viewtoggle", role: "group", "aria-label": "Visning", children: [
264
+ /* @__PURE__ */ jsxRuntime.jsx(
265
+ "button",
266
+ {
267
+ type: "button",
268
+ className: "bmap__vbtn" + (view === "front" ? " bmap__vbtn--on" : ""),
269
+ "data-testid": "bodymap-view-front",
270
+ "aria-pressed": view === "front",
271
+ onClick: () => changeView("front"),
272
+ children: "Forfra"
273
+ }
274
+ ),
275
+ /* @__PURE__ */ jsxRuntime.jsx(
276
+ "button",
277
+ {
278
+ type: "button",
279
+ className: "bmap__vbtn" + (view === "back" ? " bmap__vbtn--on" : ""),
280
+ "data-testid": "bodymap-view-back",
281
+ "aria-pressed": view === "back",
282
+ onClick: () => changeView("back"),
283
+ children: "Bagfra"
284
+ }
285
+ )
286
+ ] }),
287
+ /* @__PURE__ */ jsxRuntime.jsxs(
288
+ "svg",
289
+ {
290
+ className: "bmap__svg",
291
+ viewBox: "0 0 260 470",
292
+ width: 230,
293
+ height: 416,
294
+ role: "group",
295
+ "aria-label": "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
296
+ children: [
297
+ regions.map((r) => {
298
+ const s = shapes[r.key];
299
+ if (!s) return null;
300
+ const marked = pointOf(r.key);
301
+ const selectable = isSelectable(r.key, config);
302
+ const cls = [
303
+ "bmap__region",
304
+ !selectable && "bmap__region--locked",
305
+ selected === r.key && "bmap__region--sel"
306
+ ].filter(Boolean).join(" ");
307
+ const fill = marked ? heat(marked.intensity) : fills[r.key] ?? "#cbd5e1";
308
+ const common = {
309
+ className: cls,
310
+ fill,
311
+ "data-testid": `bodymap-region-${r.key}`,
312
+ role: selectable ? "button" : void 0,
313
+ "aria-label": r.label,
314
+ onClick: selectable ? () => setSelected(r.key) : void 0
315
+ };
316
+ if (s.el === "circle") return /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...common }, r.key);
317
+ if (s.el === "ellipse") return /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...common }, r.key);
318
+ return /* @__PURE__ */ jsxRuntime.jsx("rect", { x: s.x, y: s.y, width: s.width, height: s.height, rx: s.rx, ...common }, r.key);
319
+ }),
320
+ report.map((p) => {
321
+ const s = shapes[p.region];
322
+ if (!s || config[p.region]?.visible === false) return null;
323
+ const c = center(s);
324
+ return /* @__PURE__ */ jsxRuntime.jsx("text", { x: c.x, y: c.y + 4, textAnchor: "middle", className: "bmap__heatn", children: p.intensity }, p.region);
325
+ })
326
+ ]
327
+ }
328
+ )
329
+ ] }),
241
330
  region ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__panel", "data-testid": "bodymap-panel", children: [
242
331
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bmap__ph", children: [
243
332
  /* @__PURE__ */ jsxRuntime.jsx("b", { children: region.label }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/react.tsx"],"names":["z","useEffect","useState","jsxs","jsx"],"mappings":"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","file":"react.cjs","sourcesContent":["// @broberg/bodymap — headless core (F052.1).\n//\n// Framework-neutral: the region taxonomy + the PainReport data model (zod) + a\n// selection engine + per-app region config. NO React/Preact/DOM import — the 2D\n// (SVG) and 3D (Three.js) renderers, and all three FD apps, share this one\n// contract. Output is a structured PainReport, never a bare image.\n\nimport { z } from \"zod\";\n\nexport type Side = \"left\" | \"right\";\n\nexport interface BodyRegion {\n /** Stable, unique identifier (snake_case) — the key used in a PainReport. */\n key: string;\n /** Human label (Danish). */\n label: string;\n /** Clinical short code (unique). */\n code: string;\n /** Body side, when the region is paired. */\n side?: Side;\n}\n\n/** The canonical body regions — the AUTHORITATIVE fd-sundhed clinical taxonomy\n * (docs/BODYMAP-TAKSONOMI.md, broberg-ai/fd-sundhed @360842f): 18 SIDE-LESS\n * clinical codes + a separate `side` field (L/R on limbs, C=center on axis\n * regions). The `key` is a unique per-side identifier; `code` is the side-less\n * clinical code that goes on the bodymap/v1 wire. NOT an anatomical atlas —\n * ~30 named surface regions for a pain-map. The 2D front renderer draws the\n * front-visible subset; the 3D body (F052.6) drives them all. */\nexport const REGIONS: readonly BodyRegion[] = [\n // axis / centre-line (serialised side \"center\")\n { key: \"head\", label: \"Hoved\", code: \"HEAD\" },\n { key: \"neck\", label: \"Nakke\", code: \"NECK\" },\n { key: \"chest\", label: \"Bryst\", code: \"CHEST\" },\n { key: \"thora\", label: \"Øvre ryg (thorakal)\", code: \"THORA\" },\n { key: \"lumbar\", label: \"Lænd (lumbal)\", code: \"LUMBAR\" },\n { key: \"groin\", label: \"Lyske\", code: \"GROIN\" },\n // paired limbs / sides (L / R)\n { key: \"shoulder_left\", label: \"Skulder, venstre\", code: \"SHOULDER\", side: \"left\" },\n { key: \"shoulder_right\", label: \"Skulder, højre\", code: \"SHOULDER\", side: \"right\" },\n { key: \"uarm_left\", label: \"Overarm, venstre\", code: \"UARM\", side: \"left\" },\n { key: \"uarm_right\", label: \"Overarm, højre\", code: \"UARM\", side: \"right\" },\n { key: \"elbow_left\", label: \"Albue, venstre\", code: \"ELBOW\", side: \"left\" },\n { key: \"elbow_right\", label: \"Albue, højre\", code: \"ELBOW\", side: \"right\" },\n { key: \"farm_left\", label: \"Underarm, venstre\", code: \"FARM\", side: \"left\" },\n { key: \"farm_right\", label: \"Underarm, højre\", code: \"FARM\", side: \"right\" },\n { key: \"wrist_left\", label: \"Håndled, venstre\", code: \"WRIST\", side: \"left\" },\n { key: \"wrist_right\", label: \"Håndled, højre\", code: \"WRIST\", side: \"right\" },\n { key: \"hand_left\", label: \"Hånd, venstre\", code: \"HAND\", side: \"left\" },\n { key: \"hand_right\", label: \"Hånd, højre\", code: \"HAND\", side: \"right\" },\n { key: \"hip_left\", label: \"Hofte, venstre\", code: \"HIP\", side: \"left\" },\n { key: \"hip_right\", label: \"Hofte, højre\", code: \"HIP\", side: \"right\" },\n { key: \"thigh_left\", label: \"Lår, venstre\", code: \"THIGH\", side: \"left\" },\n { key: \"thigh_right\", label: \"Lår, højre\", code: \"THIGH\", side: \"right\" },\n { key: \"knee_left\", label: \"Knæ, venstre\", code: \"KNEE\", side: \"left\" },\n { key: \"knee_right\", label: \"Knæ, højre\", code: \"KNEE\", side: \"right\" },\n { key: \"lowleg_left\", label: \"Underben, venstre\", code: \"LOWLEG\", side: \"left\" },\n { key: \"lowleg_right\", label: \"Underben, højre\", code: \"LOWLEG\", side: \"right\" },\n { key: \"ankle_left\", label: \"Ankel, venstre\", code: \"ANKLE\", side: \"left\" },\n { key: \"ankle_right\", label: \"Ankel, højre\", code: \"ANKLE\", side: \"right\" },\n { key: \"foot_left\", label: \"Fod, venstre\", code: \"FOOT\", side: \"left\" },\n { key: \"foot_right\", label: \"Fod, højre\", code: \"FOOT\", side: \"right\" },\n];\n\nconst REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));\nexport const REGION_KEYS: readonly string[] = REGIONS.map((r) => r.key);\n\n/** Look up a region by its key. */\nexport function getRegion(key: string): BodyRegion | undefined {\n return REGIONS.find((r) => r.key === key);\n}\n\n// ---- PainReport model ---------------------------------------------------\n\nexport const PAIN_TYPES = [\"stikkende\", \"dump\", \"konstant\", \"jagende\"] as const;\nexport type PainType = (typeof PAIN_TYPES)[number];\n\n/** One marked pain point. `region` MUST be a known region key; `intensity` is a\n * 0-10 integer; `type` is optional but constrained; `timestamp` is an ISO string. */\nexport const painPointSchema = z.object({\n region: z.string().refine((k) => REGION_KEY_SET.has(k), { message: \"unknown region\" }),\n intensity: z.number().int().min(0).max(10),\n type: z.enum(PAIN_TYPES).optional(),\n timestamp: z.string(),\n});\nexport type PainPoint = z.infer<typeof painPointSchema>;\n\nexport const painReportSchema = z.array(painPointSchema);\nexport type PainReport = PainPoint[];\n\n// ---- Selection engine (framework-agnostic) ------------------------------\n\nexport interface PainSelection {\n /** Mark (or update) pain on a region. One point per region — latest wins. */\n set(region: string, intensity: number, type?: PainType): PainPoint;\n remove(region: string): boolean;\n get(region: string): PainPoint | undefined;\n has(region: string): boolean;\n clear(): void;\n /** The current, validated PainReport. */\n getReport(): PainReport;\n}\n\nexport interface PainSelectionOptions {\n /** Injectable clock (ISO string) — defaults to `new Date().toISOString()`. */\n now?: () => string;\n}\n\n/** Create a selection engine seeded with an optional report. Pure state — no\n * DOM, no framework, no network. One point per region. */\nexport function createPainSelection(\n initial: PainReport = [],\n opts: PainSelectionOptions = {},\n): PainSelection {\n const now = opts.now ?? (() => new Date().toISOString());\n const map = new Map<string, PainPoint>();\n for (const p of initial) {\n const v = painPointSchema.parse(p);\n map.set(v.region, v);\n }\n return {\n set(region, intensity, type) {\n const point = painPointSchema.parse({ region, intensity, type, timestamp: now() });\n map.set(point.region, point);\n return point;\n },\n remove: (region) => map.delete(region),\n get: (region) => map.get(region),\n has: (region) => map.has(region),\n clear: () => map.clear(),\n getReport: () => painReportSchema.parse(Array.from(map.values())),\n };\n}\n\n// ---- Per-app region config (the toggle) ---------------------------------\n\nexport interface RegionSetting {\n /** Render this region at all. Default true. */\n visible?: boolean;\n /** Allow marking pain on this region. Default true. */\n selectable?: boolean;\n}\n\n/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */\nexport type RegionConfig = Record<string, RegionSetting>;\n\n/** The regions an app should render, honouring `visible` (default true). */\nexport function resolveRegions(config: RegionConfig = {}): BodyRegion[] {\n return REGIONS.filter((r) => config[r.key]?.visible ?? true);\n}\n\n/** Whether a region may be marked. A hidden region is never selectable. */\nexport function isSelectable(key: string, config: RegionConfig = {}): boolean {\n const s = config[key];\n if (s?.visible === false) return false;\n return s?.selectable ?? true;\n}\n\n// ---- palette (consumer-defined colours — shared by the 2D + 3D renderers) ---\n\n/** Colour control for the body renderers. Consumers pass a palette to theme the\n * body base colour, the hover + selected highlights, the pain-heat colours, and\n * optional per-region base colours. All values are CSS/hex colour strings. */\nexport interface BodymapPalette {\n /** Base body colour (an unmarked region). */\n body: string;\n /** Region highlight on hover (before click). */\n hover: string;\n /** A region selected (clicked) but not yet given an intensity. */\n selected: string;\n /** Pain-intensity heat colours: low (0-3), mid (4-6), high (7-10). */\n heat: { low: string; mid: string; high: string };\n /** Optional per-region base-colour overrides (region key → colour). */\n regions?: Record<string, string>;\n}\n\n/** The fleet default palette. Override any field per consumer. */\nexport const defaultPalette: BodymapPalette = {\n body: \"#d2d7de\",\n hover: \"#8fd0cd\",\n selected: \"#5cc4b7\",\n heat: { low: \"#fcd34d\", mid: \"#fb923c\", high: \"#ef4444\" },\n};\n\n/** The heat colour for a pain intensity, honouring the palette. */\nexport function heatFor(intensity: number, palette: BodymapPalette = defaultPalette): string {\n return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;\n}\n\n/** The base colour for a region (a per-region override, else the body colour). */\nexport function baseColorFor(regionKey: string, palette: BodymapPalette = defaultPalette): string {\n return palette.regions?.[regionKey] ?? palette.body;\n}\n\n// ---- bodymap/v1 serialization (the shared cross-app / native wire format) ---\n//\n// The shape every consumer + the native mobile apps read (aligned with\n// fd-sundhed's bodymap/v1: region CODE + side + intensity + quality + view).\n// The internal PainReport keys on the region KEY; this maps key -> clinical CODE\n// so the report is portable and human-readable on the wire.\n\nexport type BodyView = \"front\" | \"back\" | \"left\" | \"right\";\n/** Side in the serialized report — a midline region (no side) becomes \"center\". */\nexport type SerializedSide = \"left\" | \"right\" | \"center\";\n\nexport interface SerializedPainPoint {\n /** Clinical region CODE (e.g. \"LUMB\"). */\n region: string;\n side: SerializedSide;\n intensity: number;\n quality?: PainType;\n}\n\nexport interface BodymapReportV1 {\n schema: \"bodymap/v1\";\n view: BodyView;\n points: SerializedPainPoint[];\n}\n\nexport const bodymapReportV1Schema = z.object({\n schema: z.literal(\"bodymap/v1\"),\n view: z.enum([\"front\", \"back\", \"left\", \"right\"]),\n points: z.array(\n z.object({\n region: z.string(),\n side: z.enum([\"left\", \"right\", \"center\"]),\n intensity: z.number().int().min(0).max(10),\n quality: z.enum(PAIN_TYPES).optional(),\n }),\n ),\n});\n\n/** Serialize a PainReport to the shared `bodymap/v1` wire format. */\nexport function serializeReport(\n report: PainReport,\n opts: { view?: BodyView } = {},\n): BodymapReportV1 {\n return {\n schema: \"bodymap/v1\",\n view: opts.view ?? \"front\",\n points: report.map((p) => {\n const r = getRegion(p.region);\n return {\n region: r?.code ?? p.region,\n side: (r?.side ?? \"center\") as SerializedSide,\n intensity: p.intensity,\n quality: p.type,\n };\n }),\n };\n}\n\n/** Parse a `bodymap/v1` report back into an internal PainReport. Region CODE →\n * key; a point whose code is unknown to this taxonomy is dropped. */\nexport function deserializeReport(\n env: unknown,\n now: () => string = () => new Date().toISOString(),\n): PainReport {\n const parsed = bodymapReportV1Schema.parse(env);\n // code is side-less, so a point is identified by code + side.\n const byCodeSide = new Map(\n REGIONS.map((r) => [`${r.code}|${r.side ?? \"center\"}`, r.key] as const),\n );\n const out: PainReport = [];\n for (const sp of parsed.points) {\n const key = byCodeSide.get(`${sp.region}|${sp.side}`);\n if (!key) continue;\n out.push(\n painPointSchema.parse({\n region: key,\n intensity: sp.intensity,\n type: sp.quality,\n timestamp: now(),\n }),\n );\n }\n return out;\n}\n","// @broberg/bodymap/react — the 2D SVG renderer (F052.2).\n//\n// <BodyMap> is the shippable MVP: a genderless, colour-blocked body a patient\n// clicks to mark where it hurts. Selecting a region opens a custom intensity\n// (0-10) + quality control (NO native inputs) that upserts a validated\n// PainReport and fires onChange. Honours the per-app RegionConfig. The 3D\n// <BodyMap3D> (F052.6) will render the SAME core on the SAME onChange contract.\n\nimport { useEffect, useState } from \"react\";\nimport {\n REGIONS,\n PAIN_TYPES,\n painPointSchema,\n resolveRegions,\n isSelectable,\n type BodyRegion,\n type PainReport,\n type PainType,\n type RegionConfig,\n} from \"./index.js\";\n\n// ---- geometry: each region → an SVG shape (a neutral colour-blockout) --------\n\ntype Shape =\n | { el: \"circle\"; cx: number; cy: number; r: number }\n | { el: \"ellipse\"; cx: number; cy: number; rx: number; ry: number }\n | { el: \"rect\"; x: number; y: number; width: number; height: number; rx: number };\n\n// Front-view geometry (viewBox 260×470). The 2D MVP renders the front-visible\n// regions; back-only regions (thora, lumbar) + hip are driven by the 3D body /\n// a later back view. \"left\"/\"right\" are placed self-view (viewer's left = key\n// left) for the MVP — the facing-the-patient mirror is a later config option.\nconst SHAPES: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n chest: { el: \"rect\", x: 100, y: 84, width: 60, height: 104, rx: 16 },\n groin: { el: \"rect\", x: 106, y: 190, width: 48, height: 30, rx: 13 },\n // left arm (viewer-left)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n // right arm (viewer-right)\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL: Record<string, string> = {\n head: \"#cbb7ec\", neck: \"#b8a3e6\", chest: \"#e26d6d\", groin: \"#e88a52\",\n shoulder_left: \"#f6c667\", shoulder_right: \"#f6c667\",\n uarm_left: \"#7ececb\", uarm_right: \"#7ececb\",\n elbow_left: \"#86cfcc\", elbow_right: \"#86cfcc\",\n farm_left: \"#8fd0cd\", farm_right: \"#8fd0cd\",\n wrist_left: \"#9ad6d3\", wrist_right: \"#9ad6d3\",\n hand_left: \"#a7ddda\", hand_right: \"#a7ddda\",\n thigh_left: \"#7fb2e8\", thigh_right: \"#7fb2e8\",\n knee_left: \"#c79be0\", knee_right: \"#c79be0\",\n lowleg_left: \"#e39ec6\", lowleg_right: \"#e39ec6\",\n ankle_left: \"#eeaab9\", ankle_right: \"#eeaab9\",\n foot_left: \"#f0a5a5\", foot_right: \"#f0a5a5\",\n};\n\nfunction center(s: Shape): { x: number; y: number } {\n if (s.el === \"rect\") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };\n return { x: s.cx, y: s.cy };\n}\nfunction heat(v: number): string {\n return v >= 7 ? \"#ef4444\" : v >= 4 ? \"#fb923c\" : \"#fcd34d\";\n}\n\n// ---- one-time style injection (no external CSS; theme via CSS variables) -----\n\nconst STYLE_ID = \"broberg-bodymap-styles\";\nconst STYLE = `\n.bmap{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#64748b;\n 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)}\n.bmap__svg{flex:0 0 auto}\n.bmap__region{cursor:pointer;stroke:#fff;stroke-width:2.4;transition:filter .12s}\n.bmap__region:hover{filter:brightness(1.08) saturate(1.15)}\n.bmap__region--locked{cursor:default;opacity:.4}\n.bmap__region--sel{stroke:var(--bmap-accent);stroke-width:3.6}\n.bmap__heatn{font:700 11px ui-sans-serif;fill:#fff;pointer-events:none}\n.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)}\n.bmap__panel--empty{color:var(--bmap-muted);font-size:13.5px}\n.bmap__ph{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}\n.bmap__ph b{font-size:16px}\n.bmap__code{font:11px ui-monospace,monospace;color:var(--bmap-muted);background:#f1f5f9;border-radius:6px;padding:2px 7px}\n.bmap__lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bmap-muted);margin:0 0 7px}\n.bmap__scale{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:14px}\n.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}\n.bmap__i:hover{border-color:var(--bmap-accent)}\n.bmap__i:active{transform:scale(.9)}\n.bmap__i--on{background:var(--bmap-accent);border-color:var(--bmap-accent);color:#fff}\n.bmap__chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}\n.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}\n.bmap__chip:hover{border-color:var(--bmap-accent);color:var(--bmap-accent)}\n.bmap__chip:active{transform:scale(.96)}\n.bmap__chip--on{background:var(--bmap-ink);border-color:var(--bmap-ink);color:#fff}\n.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}\n.bmap__rm:hover{background:#fef2f2}\n.bmap__rm:active{transform:scale(.97)}\n`;\n\nfunction ensureStyles(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = STYLE;\n document.head.appendChild(el);\n}\n\n// ---- component ---------------------------------------------------------------\n\nexport interface BodyMapProps {\n /** Controlled report. Omit for an uncontrolled component (use defaultValue). */\n value?: PainReport;\n /** Initial report when uncontrolled. */\n defaultValue?: PainReport;\n /** Fired with the full validated PainReport on every change. */\n onChange?: (report: PainReport) => void;\n /** Per-app region config (visible / selectable). */\n config?: RegionConfig;\n className?: string;\n}\n\n/** 2D SVG body pain-map. Click a region → set intensity + quality → PainReport. */\nexport function BodyMap({ value, defaultValue, onChange, config = {}, className }: BodyMapProps) {\n useEffect(ensureStyles, []);\n const [internal, setInternal] = useState<PainReport>(defaultValue ?? []);\n const [selected, setSelected] = useState<string | null>(null);\n const report = value ?? internal;\n\n const commit = (next: PainReport) => {\n if (value === undefined) setInternal(next);\n onChange?.(next);\n };\n const pointOf = (key: string) => report.find((p) => p.region === key);\n const setPain = (key: string, intensity: number, type?: PainType) => {\n const point = painPointSchema.parse({ region: key, intensity, type, timestamp: new Date().toISOString() });\n commit([...report.filter((p) => p.region !== key), point]);\n };\n const removePain = (key: string) => {\n commit(report.filter((p) => p.region !== key));\n setSelected(null);\n };\n\n const regions = resolveRegions(config);\n const region: BodyRegion | undefined = selected ? REGIONS.find((r) => r.key === selected) : undefined;\n const current = selected ? pointOf(selected) : undefined;\n const rootCls = [\"bmap\", className].filter(Boolean).join(\" \");\n\n return (\n <div className={rootCls} data-testid=\"bodymap-root\">\n <svg\n className=\"bmap__svg\"\n viewBox=\"0 0 260 470\"\n width={230}\n height={416}\n role=\"group\"\n aria-label=\"Kropskort — vælg hvor det gør ondt\"\n >\n {regions.map((r) => {\n const s = SHAPES[r.key];\n if (!s) return null;\n const marked = pointOf(r.key);\n const selectable = isSelectable(r.key, config);\n const cls = [\n \"bmap__region\",\n !selectable && \"bmap__region--locked\",\n selected === r.key && \"bmap__region--sel\",\n ]\n .filter(Boolean)\n .join(\" \");\n const fill = marked ? heat(marked.intensity) : FILL[r.key] ?? \"#cbd5e1\";\n const common = {\n className: cls,\n fill,\n \"data-testid\": `bodymap-region-${r.key}`,\n role: selectable ? \"button\" : undefined,\n \"aria-label\": r.label,\n onClick: selectable ? () => setSelected(r.key) : undefined,\n };\n if (s.el === \"circle\") return <circle key={r.key} cx={s.cx} cy={s.cy} r={s.r} {...common} />;\n if (s.el === \"ellipse\") return <ellipse key={r.key} cx={s.cx} cy={s.cy} rx={s.rx} ry={s.ry} {...common} />;\n return <rect key={r.key} x={s.x} y={s.y} width={s.width} height={s.height} rx={s.rx} {...common} />;\n })}\n {/* intensity numbers on marked regions */}\n {report.map((p) => {\n const s = SHAPES[p.region];\n if (!s || (config[p.region]?.visible === false)) return null;\n const c = center(s);\n return (\n <text key={p.region} x={c.x} y={c.y + 4} textAnchor=\"middle\" className=\"bmap__heatn\">\n {p.intensity}\n </text>\n );\n })}\n </svg>\n\n {region ? (\n <div className=\"bmap__panel\" data-testid=\"bodymap-panel\">\n <div className=\"bmap__ph\">\n <b>{region.label}</b>\n <span className=\"bmap__code\">{region.code}</span>\n </div>\n <p className=\"bmap__lbl\">Intensitet (0-10)</p>\n <div className=\"bmap__scale\" role=\"group\" aria-label=\"Intensitet\">\n {Array.from({ length: 11 }, (_, n) => (\n <button\n key={n}\n type=\"button\"\n className={\"bmap__i\" + (current?.intensity === n ? \" bmap__i--on\" : \"\")}\n data-testid={`bodymap-intensity-${n}`}\n aria-pressed={current?.intensity === n}\n onClick={() => setPain(region.key, n, current?.type)}\n >\n {n}\n </button>\n ))}\n </div>\n <p className=\"bmap__lbl\">Kvalitet</p>\n <div className=\"bmap__chips\" role=\"group\" aria-label=\"Kvalitet\">\n {PAIN_TYPES.map((t) => (\n <button\n key={t}\n type=\"button\"\n className={\"bmap__chip\" + (current?.type === t ? \" bmap__chip--on\" : \"\")}\n data-testid={`bodymap-type-${t}`}\n aria-pressed={current?.type === t}\n onClick={() => setPain(region.key, current?.intensity ?? 5, t)}\n >\n {t}\n </button>\n ))}\n </div>\n {current && (\n <button type=\"button\" className=\"bmap__rm\" data-testid=\"bodymap-remove\" onClick={() => removePain(region.key)}>\n Fjern punkt\n </button>\n )}\n </div>\n ) : (\n <div className=\"bmap__panel bmap__panel--empty\" data-testid=\"bodymap-panel\">\n Klik en kropsdel for at markere smerte.\n </div>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/react.tsx"],"names":["z","useEffect","useState","jsxs","jsx"],"mappings":"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","file":"react.cjs","sourcesContent":["// @broberg/bodymap — headless core (F052.1).\n//\n// Framework-neutral: the region taxonomy + the PainReport data model (zod) + a\n// selection engine + per-app region config. NO React/Preact/DOM import — the 2D\n// (SVG) and 3D (Three.js) renderers, and all three FD apps, share this one\n// contract. Output is a structured PainReport, never a bare image.\n\nimport { z } from \"zod\";\n\nexport type Side = \"left\" | \"right\";\n\nexport interface BodyRegion {\n /** Stable, unique identifier (snake_case) — the key used in a PainReport. */\n key: string;\n /** Human label (Danish). */\n label: string;\n /** Clinical short code (unique). */\n code: string;\n /** Body side, when the region is paired. */\n side?: Side;\n}\n\n/** The canonical body regions — the AUTHORITATIVE fd-sundhed clinical taxonomy\n * (docs/BODYMAP-TAKSONOMI.md, broberg-ai/fd-sundhed @360842f): 18 SIDE-LESS\n * clinical codes + a separate `side` field (L/R on limbs, C=center on axis\n * regions). The `key` is a unique per-side identifier; `code` is the side-less\n * clinical code that goes on the bodymap/v1 wire. NOT an anatomical atlas —\n * ~30 named surface regions for a pain-map. The 2D front renderer draws the\n * front-visible subset; the 3D body (F052.6) drives them all. */\nexport const REGIONS: readonly BodyRegion[] = [\n // axis / centre-line (serialised side \"center\")\n { key: \"head\", label: \"Hoved\", code: \"HEAD\" },\n { key: \"neck\", label: \"Nakke\", code: \"NECK\" },\n { key: \"chest\", label: \"Bryst\", code: \"CHEST\" },\n { key: \"thora\", label: \"Øvre ryg (thorakal)\", code: \"THORA\" },\n { key: \"lumbar\", label: \"Lænd (lumbal)\", code: \"LUMBAR\" },\n { key: \"groin\", label: \"Lyske\", code: \"GROIN\" },\n // paired limbs / sides (L / R)\n { key: \"shoulder_left\", label: \"Skulder, venstre\", code: \"SHOULDER\", side: \"left\" },\n { key: \"shoulder_right\", label: \"Skulder, højre\", code: \"SHOULDER\", side: \"right\" },\n { key: \"uarm_left\", label: \"Overarm, venstre\", code: \"UARM\", side: \"left\" },\n { key: \"uarm_right\", label: \"Overarm, højre\", code: \"UARM\", side: \"right\" },\n { key: \"elbow_left\", label: \"Albue, venstre\", code: \"ELBOW\", side: \"left\" },\n { key: \"elbow_right\", label: \"Albue, højre\", code: \"ELBOW\", side: \"right\" },\n { key: \"farm_left\", label: \"Underarm, venstre\", code: \"FARM\", side: \"left\" },\n { key: \"farm_right\", label: \"Underarm, højre\", code: \"FARM\", side: \"right\" },\n { key: \"wrist_left\", label: \"Håndled, venstre\", code: \"WRIST\", side: \"left\" },\n { key: \"wrist_right\", label: \"Håndled, højre\", code: \"WRIST\", side: \"right\" },\n { key: \"hand_left\", label: \"Hånd, venstre\", code: \"HAND\", side: \"left\" },\n { key: \"hand_right\", label: \"Hånd, højre\", code: \"HAND\", side: \"right\" },\n { key: \"hip_left\", label: \"Hofte, venstre\", code: \"HIP\", side: \"left\" },\n { key: \"hip_right\", label: \"Hofte, højre\", code: \"HIP\", side: \"right\" },\n { key: \"thigh_left\", label: \"Lår, venstre\", code: \"THIGH\", side: \"left\" },\n { key: \"thigh_right\", label: \"Lår, højre\", code: \"THIGH\", side: \"right\" },\n { key: \"knee_left\", label: \"Knæ, venstre\", code: \"KNEE\", side: \"left\" },\n { key: \"knee_right\", label: \"Knæ, højre\", code: \"KNEE\", side: \"right\" },\n { key: \"lowleg_left\", label: \"Underben, venstre\", code: \"LOWLEG\", side: \"left\" },\n { key: \"lowleg_right\", label: \"Underben, højre\", code: \"LOWLEG\", side: \"right\" },\n { key: \"ankle_left\", label: \"Ankel, venstre\", code: \"ANKLE\", side: \"left\" },\n { key: \"ankle_right\", label: \"Ankel, højre\", code: \"ANKLE\", side: \"right\" },\n { key: \"foot_left\", label: \"Fod, venstre\", code: \"FOOT\", side: \"left\" },\n { key: \"foot_right\", label: \"Fod, højre\", code: \"FOOT\", side: \"right\" },\n];\n\nconst REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));\nexport const REGION_KEYS: readonly string[] = REGIONS.map((r) => r.key);\n\n/** Look up a region by its key. */\nexport function getRegion(key: string): BodyRegion | undefined {\n return REGIONS.find((r) => r.key === key);\n}\n\n// ---- PainReport model ---------------------------------------------------\n\nexport const PAIN_TYPES = [\"stikkende\", \"dump\", \"konstant\", \"jagende\"] as const;\nexport type PainType = (typeof PAIN_TYPES)[number];\n\n/** One marked pain point. `region` MUST be a known region key; `intensity` is a\n * 0-10 integer; `type` is optional but constrained; `timestamp` is an ISO string. */\nexport const painPointSchema = z.object({\n region: z.string().refine((k) => REGION_KEY_SET.has(k), { message: \"unknown region\" }),\n intensity: z.number().int().min(0).max(10),\n type: z.enum(PAIN_TYPES).optional(),\n timestamp: z.string(),\n});\nexport type PainPoint = z.infer<typeof painPointSchema>;\n\nexport const painReportSchema = z.array(painPointSchema);\nexport type PainReport = PainPoint[];\n\n// ---- Selection engine (framework-agnostic) ------------------------------\n\nexport interface PainSelection {\n /** Mark (or update) pain on a region. One point per region — latest wins. */\n set(region: string, intensity: number, type?: PainType): PainPoint;\n remove(region: string): boolean;\n get(region: string): PainPoint | undefined;\n has(region: string): boolean;\n clear(): void;\n /** The current, validated PainReport. */\n getReport(): PainReport;\n}\n\nexport interface PainSelectionOptions {\n /** Injectable clock (ISO string) — defaults to `new Date().toISOString()`. */\n now?: () => string;\n}\n\n/** Create a selection engine seeded with an optional report. Pure state — no\n * DOM, no framework, no network. One point per region. */\nexport function createPainSelection(\n initial: PainReport = [],\n opts: PainSelectionOptions = {},\n): PainSelection {\n const now = opts.now ?? (() => new Date().toISOString());\n const map = new Map<string, PainPoint>();\n for (const p of initial) {\n const v = painPointSchema.parse(p);\n map.set(v.region, v);\n }\n return {\n set(region, intensity, type) {\n const point = painPointSchema.parse({ region, intensity, type, timestamp: now() });\n map.set(point.region, point);\n return point;\n },\n remove: (region) => map.delete(region),\n get: (region) => map.get(region),\n has: (region) => map.has(region),\n clear: () => map.clear(),\n getReport: () => painReportSchema.parse(Array.from(map.values())),\n };\n}\n\n// ---- Per-app region config (the toggle) ---------------------------------\n\nexport interface RegionSetting {\n /** Render this region at all. Default true. */\n visible?: boolean;\n /** Allow marking pain on this region. Default true. */\n selectable?: boolean;\n}\n\n/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */\nexport type RegionConfig = Record<string, RegionSetting>;\n\n/** The regions an app should render, honouring `visible` (default true). */\nexport function resolveRegions(config: RegionConfig = {}): BodyRegion[] {\n return REGIONS.filter((r) => config[r.key]?.visible ?? true);\n}\n\n/** Whether a region may be marked. A hidden region is never selectable. */\nexport function isSelectable(key: string, config: RegionConfig = {}): boolean {\n const s = config[key];\n if (s?.visible === false) return false;\n return s?.selectable ?? true;\n}\n\n// ---- palette (consumer-defined colours — shared by the 2D + 3D renderers) ---\n\n/** Colour control for the body renderers. Consumers pass a palette to theme the\n * body base colour, the hover + selected highlights, the pain-heat colours, and\n * optional per-region base colours. All values are CSS/hex colour strings. */\nexport interface BodymapPalette {\n /** Base body colour (an unmarked region). */\n body: string;\n /** Region highlight on hover (before click). */\n hover: string;\n /** A region selected (clicked) but not yet given an intensity. */\n selected: string;\n /** Pain-intensity heat colours: low (0-3), mid (4-6), high (7-10). */\n heat: { low: string; mid: string; high: string };\n /** Optional per-region base-colour overrides (region key → colour). */\n regions?: Record<string, string>;\n}\n\n/** The fleet default palette. Override any field per consumer. */\nexport const defaultPalette: BodymapPalette = {\n body: \"#d2d7de\",\n hover: \"#8fd0cd\",\n selected: \"#5cc4b7\",\n heat: { low: \"#fcd34d\", mid: \"#fb923c\", high: \"#ef4444\" },\n};\n\n/** The heat colour for a pain intensity, honouring the palette. */\nexport function heatFor(intensity: number, palette: BodymapPalette = defaultPalette): string {\n return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;\n}\n\n/** The base colour for a region (a per-region override, else the body colour). */\nexport function baseColorFor(regionKey: string, palette: BodymapPalette = defaultPalette): string {\n return palette.regions?.[regionKey] ?? palette.body;\n}\n\n// ---- bodymap/v1 serialization (the shared cross-app / native wire format) ---\n//\n// The shape every consumer + the native mobile apps read (aligned with\n// fd-sundhed's bodymap/v1: region CODE + side + intensity + quality + view).\n// The internal PainReport keys on the region KEY; this maps key -> clinical CODE\n// so the report is portable and human-readable on the wire.\n\nexport type BodyView = \"front\" | \"back\" | \"left\" | \"right\";\n/** Side in the serialized report — a midline region (no side) becomes \"center\". */\nexport type SerializedSide = \"left\" | \"right\" | \"center\";\n\nexport interface SerializedPainPoint {\n /** Clinical region CODE (e.g. \"LUMB\"). */\n region: string;\n side: SerializedSide;\n intensity: number;\n quality?: PainType;\n}\n\nexport interface BodymapReportV1 {\n schema: \"bodymap/v1\";\n view: BodyView;\n points: SerializedPainPoint[];\n}\n\nexport const bodymapReportV1Schema = z.object({\n schema: z.literal(\"bodymap/v1\"),\n view: z.enum([\"front\", \"back\", \"left\", \"right\"]),\n points: z.array(\n z.object({\n region: z.string(),\n side: z.enum([\"left\", \"right\", \"center\"]),\n intensity: z.number().int().min(0).max(10),\n quality: z.enum(PAIN_TYPES).optional(),\n }),\n ),\n});\n\n/** Serialize a PainReport to the shared `bodymap/v1` wire format. */\nexport function serializeReport(\n report: PainReport,\n opts: { view?: BodyView } = {},\n): BodymapReportV1 {\n return {\n schema: \"bodymap/v1\",\n view: opts.view ?? \"front\",\n points: report.map((p) => {\n const r = getRegion(p.region);\n return {\n region: r?.code ?? p.region,\n side: (r?.side ?? \"center\") as SerializedSide,\n intensity: p.intensity,\n quality: p.type,\n };\n }),\n };\n}\n\n/** Parse a `bodymap/v1` report back into an internal PainReport. Region CODE →\n * key; a point whose code is unknown to this taxonomy is dropped. */\nexport function deserializeReport(\n env: unknown,\n now: () => string = () => new Date().toISOString(),\n): PainReport {\n const parsed = bodymapReportV1Schema.parse(env);\n // code is side-less, so a point is identified by code + side.\n const byCodeSide = new Map(\n REGIONS.map((r) => [`${r.code}|${r.side ?? \"center\"}`, r.key] as const),\n );\n const out: PainReport = [];\n for (const sp of parsed.points) {\n const key = byCodeSide.get(`${sp.region}|${sp.side}`);\n if (!key) continue;\n out.push(\n painPointSchema.parse({\n region: key,\n intensity: sp.intensity,\n type: sp.quality,\n timestamp: now(),\n }),\n );\n }\n return out;\n}\n","// @broberg/bodymap/react — the 2D SVG renderer (F052.2).\n//\n// <BodyMap> is the shippable MVP: a genderless, colour-blocked body a patient\n// clicks to mark where it hurts. Selecting a region opens a custom intensity\n// (0-10) + quality control (NO native inputs) that upserts a validated\n// PainReport and fires onChange. Honours the per-app RegionConfig. The 3D\n// <BodyMap3D> (F052.6) will render the SAME core on the SAME onChange contract.\n\nimport { useEffect, useState } from \"react\";\nimport {\n REGIONS,\n PAIN_TYPES,\n painPointSchema,\n resolveRegions,\n isSelectable,\n type BodyRegion,\n type PainReport,\n type PainType,\n type RegionConfig,\n} from \"./index.js\";\n\n// ---- geometry: each region → an SVG shape (a neutral colour-blockout) --------\n\ntype Shape =\n | { el: \"circle\"; cx: number; cy: number; r: number }\n | { el: \"ellipse\"; cx: number; cy: number; rx: number; ry: number }\n | { el: \"rect\"; x: number; y: number; width: number; height: number; rx: number };\n\n// Front-view geometry (viewBox 260×470). The 2D MVP renders the front-visible\n// regions; back-only regions (thora, lumbar) + hip are driven by the 3D body /\n// a later back view. \"left\"/\"right\" are placed self-view (viewer's left = key\n// left) for the MVP — the facing-the-patient mirror is a later config option.\nconst SHAPES: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n chest: { el: \"rect\", x: 100, y: 84, width: 60, height: 104, rx: 16 },\n groin: { el: \"rect\", x: 106, y: 190, width: 48, height: 30, rx: 13 },\n // left arm (viewer-left)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n // right arm (viewer-right)\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL: Record<string, string> = {\n head: \"#cbb7ec\", neck: \"#b8a3e6\", chest: \"#e26d6d\", groin: \"#e88a52\",\n shoulder_left: \"#f6c667\", shoulder_right: \"#f6c667\",\n uarm_left: \"#7ececb\", uarm_right: \"#7ececb\",\n elbow_left: \"#86cfcc\", elbow_right: \"#86cfcc\",\n farm_left: \"#8fd0cd\", farm_right: \"#8fd0cd\",\n wrist_left: \"#9ad6d3\", wrist_right: \"#9ad6d3\",\n hand_left: \"#a7ddda\", hand_right: \"#a7ddda\",\n thigh_left: \"#7fb2e8\", thigh_right: \"#7fb2e8\",\n knee_left: \"#c79be0\", knee_right: \"#c79be0\",\n lowleg_left: \"#e39ec6\", lowleg_right: \"#e39ec6\",\n ankle_left: \"#eeaab9\", ankle_right: \"#eeaab9\",\n foot_left: \"#f0a5a5\", foot_right: \"#f0a5a5\",\n};\n\n// Back-view geometry (same viewBox 260×470 + same limb/head geometry as the\n// front, so a marked limb sits at the same spot in both views). The torso swaps\n// front regions (chest, groin) for the posterior ones — THORA (upper back),\n// LUMBAR (lower back) and HIP (buttocks, paired) — the pilot's blocker regions.\n// Same self-view L/R convention as the front (patient-left = viewer-left).\nconst SHAPES_BACK: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n thora: { el: \"rect\", x: 100, y: 84, width: 60, height: 58, rx: 15 },\n lumbar: { el: \"rect\", x: 104, y: 144, width: 52, height: 44, rx: 13 },\n hip_left: { el: \"rect\", x: 100, y: 190, width: 27, height: 32, rx: 13 },\n hip_right: { el: \"rect\", x: 133, y: 190, width: 27, height: 32, rx: 13 },\n // arms — shared with the front (back of the arm sits at the same x/y)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs — back (thigh→calf→heel, same x/y as the front)\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL_BACK: Record<string, string> = {\n ...FILL,\n thora: \"#e2896d\",\n lumbar: \"#d97fa0\",\n hip_left: \"#c99bd6\", hip_right: \"#c99bd6\",\n};\n\nexport type BodyView2D = \"front\" | \"back\";\n\nfunction center(s: Shape): { x: number; y: number } {\n if (s.el === \"rect\") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };\n return { x: s.cx, y: s.cy };\n}\nfunction heat(v: number): string {\n return v >= 7 ? \"#ef4444\" : v >= 4 ? \"#fb923c\" : \"#fcd34d\";\n}\n\n// ---- one-time style injection (no external CSS; theme via CSS variables) -----\n\nconst STYLE_ID = \"broberg-bodymap-styles\";\nconst STYLE = `\n.bmap{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#64748b;\n 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)}\n.bmap__svg{flex:0 0 auto}\n.bmap__stage{display:flex;flex-direction:column;gap:12px;flex:0 0 auto}\n.bmap__viewtoggle{display:inline-flex;align-self:flex-start;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}\n.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}\n.bmap__vbtn:hover{color:var(--bmap-ink)}\n.bmap__vbtn:active{transform:scale(.97)}\n.bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}\n.bmap__region{cursor:pointer;stroke:#fff;stroke-width:2.4;transition:filter .12s}\n.bmap__region:hover{filter:brightness(1.08) saturate(1.15)}\n.bmap__region--locked{cursor:default;opacity:.4}\n.bmap__region--sel{stroke:var(--bmap-accent);stroke-width:3.6}\n.bmap__heatn{font:700 11px ui-sans-serif;fill:#fff;pointer-events:none}\n.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)}\n.bmap__panel--empty{color:var(--bmap-muted);font-size:13.5px}\n.bmap__ph{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}\n.bmap__ph b{font-size:16px}\n.bmap__code{font:11px ui-monospace,monospace;color:var(--bmap-muted);background:#f1f5f9;border-radius:6px;padding:2px 7px}\n.bmap__lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bmap-muted);margin:0 0 7px}\n.bmap__scale{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:14px}\n.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}\n.bmap__i:hover{border-color:var(--bmap-accent)}\n.bmap__i:active{transform:scale(.9)}\n.bmap__i--on{background:var(--bmap-accent);border-color:var(--bmap-accent);color:#fff}\n.bmap__chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}\n.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}\n.bmap__chip:hover{border-color:var(--bmap-accent);color:var(--bmap-accent)}\n.bmap__chip:active{transform:scale(.96)}\n.bmap__chip--on{background:var(--bmap-ink);border-color:var(--bmap-ink);color:#fff}\n.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}\n.bmap__rm:hover{background:#fef2f2}\n.bmap__rm:active{transform:scale(.97)}\n`;\n\nfunction ensureStyles(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = STYLE;\n document.head.appendChild(el);\n}\n\n// ---- component ---------------------------------------------------------------\n\nexport interface BodyMapProps {\n /** Controlled report. Omit for an uncontrolled component (use defaultValue). */\n value?: PainReport;\n /** Initial report when uncontrolled. */\n defaultValue?: PainReport;\n /** Fired with the full validated PainReport on every change. */\n onChange?: (report: PainReport) => void;\n /** Per-app region config (visible / selectable). */\n config?: RegionConfig;\n /** Initial body view (front / back). Default \"front\". */\n defaultView?: BodyView2D;\n /** Fired when the user toggles the front/back view — pass it to\n * serializeReport({ view }) so the wire report carries the active view. */\n onViewChange?: (view: BodyView2D) => void;\n className?: string;\n}\n\n/** 2D SVG body pain-map. Click a region → set intensity + quality → PainReport. */\nexport function BodyMap({\n value, defaultValue, onChange, config = {}, defaultView = \"front\", onViewChange, className,\n}: BodyMapProps) {\n useEffect(ensureStyles, []);\n const [internal, setInternal] = useState<PainReport>(defaultValue ?? []);\n const [selected, setSelected] = useState<string | null>(null);\n const [view, setView] = useState<BodyView2D>(defaultView);\n const report = value ?? internal;\n const shapes = view === \"back\" ? SHAPES_BACK : SHAPES;\n const fills = view === \"back\" ? FILL_BACK : FILL;\n const changeView = (v: BodyView2D) => {\n if (v === view) return;\n setView(v);\n const sh = v === \"back\" ? SHAPES_BACK : SHAPES;\n if (selected && !sh[selected]) setSelected(null); // selected region absent in the new view\n onViewChange?.(v);\n };\n\n const commit = (next: PainReport) => {\n if (value === undefined) setInternal(next);\n onChange?.(next);\n };\n const pointOf = (key: string) => report.find((p) => p.region === key);\n const setPain = (key: string, intensity: number, type?: PainType) => {\n const point = painPointSchema.parse({ region: key, intensity, type, timestamp: new Date().toISOString() });\n commit([...report.filter((p) => p.region !== key), point]);\n };\n const removePain = (key: string) => {\n commit(report.filter((p) => p.region !== key));\n setSelected(null);\n };\n\n const regions = resolveRegions(config);\n const region: BodyRegion | undefined = selected ? REGIONS.find((r) => r.key === selected) : undefined;\n const current = selected ? pointOf(selected) : undefined;\n const rootCls = [\"bmap\", className].filter(Boolean).join(\" \");\n\n return (\n <div className={rootCls} data-testid=\"bodymap-root\">\n <div className=\"bmap__stage\">\n <div className=\"bmap__viewtoggle\" role=\"group\" aria-label=\"Visning\">\n <button\n type=\"button\"\n className={\"bmap__vbtn\" + (view === \"front\" ? \" bmap__vbtn--on\" : \"\")}\n data-testid=\"bodymap-view-front\"\n aria-pressed={view === \"front\"}\n onClick={() => changeView(\"front\")}\n >\n Forfra\n </button>\n <button\n type=\"button\"\n className={\"bmap__vbtn\" + (view === \"back\" ? \" bmap__vbtn--on\" : \"\")}\n data-testid=\"bodymap-view-back\"\n aria-pressed={view === \"back\"}\n onClick={() => changeView(\"back\")}\n >\n Bagfra\n </button>\n </div>\n <svg\n className=\"bmap__svg\"\n viewBox=\"0 0 260 470\"\n width={230}\n height={416}\n role=\"group\"\n aria-label=\"Kropskort — vælg hvor det gør ondt\"\n >\n {regions.map((r) => {\n const s = shapes[r.key];\n if (!s) return null;\n const marked = pointOf(r.key);\n const selectable = isSelectable(r.key, config);\n const cls = [\n \"bmap__region\",\n !selectable && \"bmap__region--locked\",\n selected === r.key && \"bmap__region--sel\",\n ]\n .filter(Boolean)\n .join(\" \");\n const fill = marked ? heat(marked.intensity) : fills[r.key] ?? \"#cbd5e1\";\n const common = {\n className: cls,\n fill,\n \"data-testid\": `bodymap-region-${r.key}`,\n role: selectable ? \"button\" : undefined,\n \"aria-label\": r.label,\n onClick: selectable ? () => setSelected(r.key) : undefined,\n };\n if (s.el === \"circle\") return <circle key={r.key} cx={s.cx} cy={s.cy} r={s.r} {...common} />;\n if (s.el === \"ellipse\") return <ellipse key={r.key} cx={s.cx} cy={s.cy} rx={s.rx} ry={s.ry} {...common} />;\n return <rect key={r.key} x={s.x} y={s.y} width={s.width} height={s.height} rx={s.rx} {...common} />;\n })}\n {/* intensity numbers on marked regions visible in the current view */}\n {report.map((p) => {\n const s = shapes[p.region];\n if (!s || (config[p.region]?.visible === false)) return null;\n const c = center(s);\n return (\n <text key={p.region} x={c.x} y={c.y + 4} textAnchor=\"middle\" className=\"bmap__heatn\">\n {p.intensity}\n </text>\n );\n })}\n </svg>\n </div>\n\n {region ? (\n <div className=\"bmap__panel\" data-testid=\"bodymap-panel\">\n <div className=\"bmap__ph\">\n <b>{region.label}</b>\n <span className=\"bmap__code\">{region.code}</span>\n </div>\n <p className=\"bmap__lbl\">Intensitet (0-10)</p>\n <div className=\"bmap__scale\" role=\"group\" aria-label=\"Intensitet\">\n {Array.from({ length: 11 }, (_, n) => (\n <button\n key={n}\n type=\"button\"\n className={\"bmap__i\" + (current?.intensity === n ? \" bmap__i--on\" : \"\")}\n data-testid={`bodymap-intensity-${n}`}\n aria-pressed={current?.intensity === n}\n onClick={() => setPain(region.key, n, current?.type)}\n >\n {n}\n </button>\n ))}\n </div>\n <p className=\"bmap__lbl\">Kvalitet</p>\n <div className=\"bmap__chips\" role=\"group\" aria-label=\"Kvalitet\">\n {PAIN_TYPES.map((t) => (\n <button\n key={t}\n type=\"button\"\n className={\"bmap__chip\" + (current?.type === t ? \" bmap__chip--on\" : \"\")}\n data-testid={`bodymap-type-${t}`}\n aria-pressed={current?.type === t}\n onClick={() => setPain(region.key, current?.intensity ?? 5, t)}\n >\n {t}\n </button>\n ))}\n </div>\n {current && (\n <button type=\"button\" className=\"bmap__rm\" data-testid=\"bodymap-remove\" onClick={() => removePain(region.key)}>\n Fjern punkt\n </button>\n )}\n </div>\n ) : (\n <div className=\"bmap__panel bmap__panel--empty\" data-testid=\"bodymap-panel\">\n Klik en kropsdel for at markere smerte.\n </div>\n )}\n </div>\n );\n}\n"]}
package/dist/react.d.cts CHANGED
@@ -30,6 +30,7 @@ interface RegionSetting {
30
30
  /** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */
31
31
  type RegionConfig = Record<string, RegionSetting>;
32
32
 
33
+ type BodyView2D = "front" | "back";
33
34
  interface BodyMapProps {
34
35
  /** Controlled report. Omit for an uncontrolled component (use defaultValue). */
35
36
  value?: PainReport;
@@ -39,9 +40,14 @@ interface BodyMapProps {
39
40
  onChange?: (report: PainReport) => void;
40
41
  /** Per-app region config (visible / selectable). */
41
42
  config?: RegionConfig;
43
+ /** Initial body view (front / back). Default "front". */
44
+ defaultView?: BodyView2D;
45
+ /** Fired when the user toggles the front/back view — pass it to
46
+ * serializeReport({ view }) so the wire report carries the active view. */
47
+ onViewChange?: (view: BodyView2D) => void;
42
48
  className?: string;
43
49
  }
44
50
  /** 2D SVG body pain-map. Click a region → set intensity + quality → PainReport. */
45
- declare function BodyMap({ value, defaultValue, onChange, config, className }: BodyMapProps): react.JSX.Element;
51
+ declare function BodyMap({ value, defaultValue, onChange, config, defaultView, onViewChange, className, }: BodyMapProps): react.JSX.Element;
46
52
 
47
- export { BodyMap, type BodyMapProps };
53
+ export { BodyMap, type BodyMapProps, type BodyView2D };
package/dist/react.d.ts CHANGED
@@ -30,6 +30,7 @@ interface RegionSetting {
30
30
  /** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */
31
31
  type RegionConfig = Record<string, RegionSetting>;
32
32
 
33
+ type BodyView2D = "front" | "back";
33
34
  interface BodyMapProps {
34
35
  /** Controlled report. Omit for an uncontrolled component (use defaultValue). */
35
36
  value?: PainReport;
@@ -39,9 +40,14 @@ interface BodyMapProps {
39
40
  onChange?: (report: PainReport) => void;
40
41
  /** Per-app region config (visible / selectable). */
41
42
  config?: RegionConfig;
43
+ /** Initial body view (front / back). Default "front". */
44
+ defaultView?: BodyView2D;
45
+ /** Fired when the user toggles the front/back view — pass it to
46
+ * serializeReport({ view }) so the wire report carries the active view. */
47
+ onViewChange?: (view: BodyView2D) => void;
42
48
  className?: string;
43
49
  }
44
50
  /** 2D SVG body pain-map. Click a region → set intensity + quality → PainReport. */
45
- declare function BodyMap({ value, defaultValue, onChange, config, className }: BodyMapProps): react.JSX.Element;
51
+ declare function BodyMap({ value, defaultValue, onChange, config, defaultView, onViewChange, className, }: BodyMapProps): react.JSX.Element;
46
52
 
47
- export { BodyMap, type BodyMapProps };
53
+ export { BodyMap, type BodyMapProps, type BodyView2D };
package/dist/react.js CHANGED
@@ -126,6 +126,45 @@ var FILL = {
126
126
  foot_left: "#f0a5a5",
127
127
  foot_right: "#f0a5a5"
128
128
  };
129
+ var SHAPES_BACK = {
130
+ head: { el: "circle", cx: 130, cy: 40, r: 26 },
131
+ neck: { el: "rect", x: 120, y: 62, width: 20, height: 16, rx: 6 },
132
+ thora: { el: "rect", x: 100, y: 84, width: 60, height: 58, rx: 15 },
133
+ lumbar: { el: "rect", x: 104, y: 144, width: 52, height: 44, rx: 13 },
134
+ hip_left: { el: "rect", x: 100, y: 190, width: 27, height: 32, rx: 13 },
135
+ hip_right: { el: "rect", x: 133, y: 190, width: 27, height: 32, rx: 13 },
136
+ // arms — shared with the front (back of the arm sits at the same x/y)
137
+ shoulder_left: { el: "circle", cx: 88, cy: 98, r: 14 },
138
+ uarm_left: { el: "rect", x: 66, y: 104, width: 18, height: 54, rx: 9 },
139
+ elbow_left: { el: "circle", cx: 73, cy: 164, r: 10 },
140
+ farm_left: { el: "rect", x: 62, y: 174, width: 16, height: 50, rx: 8 },
141
+ wrist_left: { el: "circle", cx: 68, cy: 230, r: 8 },
142
+ hand_left: { el: "ellipse", cx: 67, cy: 250, rx: 11, ry: 13 },
143
+ shoulder_right: { el: "circle", cx: 172, cy: 98, r: 14 },
144
+ uarm_right: { el: "rect", x: 176, y: 104, width: 18, height: 54, rx: 9 },
145
+ elbow_right: { el: "circle", cx: 187, cy: 164, r: 10 },
146
+ farm_right: { el: "rect", x: 182, y: 174, width: 16, height: 50, rx: 8 },
147
+ wrist_right: { el: "circle", cx: 192, cy: 230, r: 8 },
148
+ hand_right: { el: "ellipse", cx: 193, cy: 250, rx: 11, ry: 13 },
149
+ // legs — back (thigh→calf→heel, same x/y as the front)
150
+ thigh_left: { el: "rect", x: 104, y: 224, width: 22, height: 80, rx: 11 },
151
+ thigh_right: { el: "rect", x: 134, y: 224, width: 22, height: 80, rx: 11 },
152
+ knee_left: { el: "circle", cx: 115, cy: 312, r: 13 },
153
+ knee_right: { el: "circle", cx: 145, cy: 312, r: 13 },
154
+ lowleg_left: { el: "rect", x: 106, y: 322, width: 18, height: 74, rx: 9 },
155
+ lowleg_right: { el: "rect", x: 136, y: 322, width: 18, height: 74, rx: 9 },
156
+ ankle_left: { el: "circle", cx: 115, cy: 404, r: 9 },
157
+ ankle_right: { el: "circle", cx: 145, cy: 404, r: 9 },
158
+ foot_left: { el: "ellipse", cx: 115, cy: 424, rx: 14, ry: 10 },
159
+ foot_right: { el: "ellipse", cx: 145, cy: 424, rx: 14, ry: 10 }
160
+ };
161
+ var FILL_BACK = {
162
+ ...FILL,
163
+ thora: "#e2896d",
164
+ lumbar: "#d97fa0",
165
+ hip_left: "#c99bd6",
166
+ hip_right: "#c99bd6"
167
+ };
129
168
  function center(s) {
130
169
  if (s.el === "rect") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };
131
170
  return { x: s.cx, y: s.cy };
@@ -138,6 +177,12 @@ var STYLE = `
138
177
  .bmap{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#64748b;
139
178
  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)}
140
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}
183
+ .bmap__vbtn:hover{color:var(--bmap-ink)}
184
+ .bmap__vbtn:active{transform:scale(.97)}
185
+ .bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}
141
186
  .bmap__region{cursor:pointer;stroke:#fff;stroke-width:2.4;transition:filter .12s}
142
187
  .bmap__region:hover{filter:brightness(1.08) saturate(1.15)}
143
188
  .bmap__region--locked{cursor:default;opacity:.4}
@@ -171,11 +216,29 @@ function ensureStyles() {
171
216
  el.textContent = STYLE;
172
217
  document.head.appendChild(el);
173
218
  }
174
- function BodyMap({ value, defaultValue, onChange, config = {}, className }) {
219
+ function BodyMap({
220
+ value,
221
+ defaultValue,
222
+ onChange,
223
+ config = {},
224
+ defaultView = "front",
225
+ onViewChange,
226
+ className
227
+ }) {
175
228
  useEffect(ensureStyles, []);
176
229
  const [internal, setInternal] = useState(defaultValue ?? []);
177
230
  const [selected, setSelected] = useState(null);
231
+ const [view, setView] = useState(defaultView);
178
232
  const report = value ?? internal;
233
+ const shapes = view === "back" ? SHAPES_BACK : SHAPES;
234
+ const fills = view === "back" ? FILL_BACK : FILL;
235
+ const changeView = (v) => {
236
+ if (v === view) return;
237
+ setView(v);
238
+ const sh = v === "back" ? SHAPES_BACK : SHAPES;
239
+ if (selected && !sh[selected]) setSelected(null);
240
+ onViewChange?.(v);
241
+ };
179
242
  const commit = (next) => {
180
243
  if (value === void 0) setInternal(next);
181
244
  onChange?.(next);
@@ -194,48 +257,74 @@ function BodyMap({ value, defaultValue, onChange, config = {}, className }) {
194
257
  const current = selected ? pointOf(selected) : void 0;
195
258
  const rootCls = ["bmap", className].filter(Boolean).join(" ");
196
259
  return /* @__PURE__ */ jsxs("div", { className: rootCls, "data-testid": "bodymap-root", children: [
197
- /* @__PURE__ */ jsxs(
198
- "svg",
199
- {
200
- className: "bmap__svg",
201
- viewBox: "0 0 260 470",
202
- width: 230,
203
- height: 416,
204
- role: "group",
205
- "aria-label": "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
206
- children: [
207
- regions.map((r) => {
208
- const s = SHAPES[r.key];
209
- if (!s) return null;
210
- const marked = pointOf(r.key);
211
- const selectable = isSelectable(r.key, config);
212
- const cls = [
213
- "bmap__region",
214
- !selectable && "bmap__region--locked",
215
- selected === r.key && "bmap__region--sel"
216
- ].filter(Boolean).join(" ");
217
- const fill = marked ? heat(marked.intensity) : FILL[r.key] ?? "#cbd5e1";
218
- const common = {
219
- className: cls,
220
- fill,
221
- "data-testid": `bodymap-region-${r.key}`,
222
- role: selectable ? "button" : void 0,
223
- "aria-label": r.label,
224
- onClick: selectable ? () => setSelected(r.key) : void 0
225
- };
226
- if (s.el === "circle") return /* @__PURE__ */ jsx("circle", { cx: s.cx, cy: s.cy, r: s.r, ...common }, r.key);
227
- if (s.el === "ellipse") return /* @__PURE__ */ jsx("ellipse", { cx: s.cx, cy: s.cy, rx: s.rx, ry: s.ry, ...common }, r.key);
228
- return /* @__PURE__ */ jsx("rect", { x: s.x, y: s.y, width: s.width, height: s.height, rx: s.rx, ...common }, r.key);
229
- }),
230
- report.map((p) => {
231
- const s = SHAPES[p.region];
232
- if (!s || config[p.region]?.visible === false) return null;
233
- const c = center(s);
234
- return /* @__PURE__ */ jsx("text", { x: c.x, y: c.y + 4, textAnchor: "middle", className: "bmap__heatn", children: p.intensity }, p.region);
235
- })
236
- ]
237
- }
238
- ),
260
+ /* @__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
+ )
284
+ ] }),
285
+ /* @__PURE__ */ jsxs(
286
+ "svg",
287
+ {
288
+ className: "bmap__svg",
289
+ viewBox: "0 0 260 470",
290
+ width: 230,
291
+ height: 416,
292
+ role: "group",
293
+ "aria-label": "Kropskort \u2014 v\xE6lg hvor det g\xF8r ondt",
294
+ children: [
295
+ regions.map((r) => {
296
+ const s = shapes[r.key];
297
+ if (!s) return null;
298
+ const marked = pointOf(r.key);
299
+ 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(" ");
305
+ 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);
317
+ }),
318
+ report.map((p) => {
319
+ const s = shapes[p.region];
320
+ if (!s || config[p.region]?.visible === false) return null;
321
+ const c = center(s);
322
+ return /* @__PURE__ */ jsx("text", { x: c.x, y: c.y + 4, textAnchor: "middle", className: "bmap__heatn", children: p.intensity }, p.region);
323
+ })
324
+ ]
325
+ }
326
+ )
327
+ ] }),
239
328
  region ? /* @__PURE__ */ jsxs("div", { className: "bmap__panel", "data-testid": "bodymap-panel", children: [
240
329
  /* @__PURE__ */ jsxs("div", { className: "bmap__ph", children: [
241
330
  /* @__PURE__ */ jsx("b", { children: region.label }),
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/react.tsx"],"names":[],"mappings":"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","file":"react.js","sourcesContent":["// @broberg/bodymap — headless core (F052.1).\n//\n// Framework-neutral: the region taxonomy + the PainReport data model (zod) + a\n// selection engine + per-app region config. NO React/Preact/DOM import — the 2D\n// (SVG) and 3D (Three.js) renderers, and all three FD apps, share this one\n// contract. Output is a structured PainReport, never a bare image.\n\nimport { z } from \"zod\";\n\nexport type Side = \"left\" | \"right\";\n\nexport interface BodyRegion {\n /** Stable, unique identifier (snake_case) — the key used in a PainReport. */\n key: string;\n /** Human label (Danish). */\n label: string;\n /** Clinical short code (unique). */\n code: string;\n /** Body side, when the region is paired. */\n side?: Side;\n}\n\n/** The canonical body regions — the AUTHORITATIVE fd-sundhed clinical taxonomy\n * (docs/BODYMAP-TAKSONOMI.md, broberg-ai/fd-sundhed @360842f): 18 SIDE-LESS\n * clinical codes + a separate `side` field (L/R on limbs, C=center on axis\n * regions). The `key` is a unique per-side identifier; `code` is the side-less\n * clinical code that goes on the bodymap/v1 wire. NOT an anatomical atlas —\n * ~30 named surface regions for a pain-map. The 2D front renderer draws the\n * front-visible subset; the 3D body (F052.6) drives them all. */\nexport const REGIONS: readonly BodyRegion[] = [\n // axis / centre-line (serialised side \"center\")\n { key: \"head\", label: \"Hoved\", code: \"HEAD\" },\n { key: \"neck\", label: \"Nakke\", code: \"NECK\" },\n { key: \"chest\", label: \"Bryst\", code: \"CHEST\" },\n { key: \"thora\", label: \"Øvre ryg (thorakal)\", code: \"THORA\" },\n { key: \"lumbar\", label: \"Lænd (lumbal)\", code: \"LUMBAR\" },\n { key: \"groin\", label: \"Lyske\", code: \"GROIN\" },\n // paired limbs / sides (L / R)\n { key: \"shoulder_left\", label: \"Skulder, venstre\", code: \"SHOULDER\", side: \"left\" },\n { key: \"shoulder_right\", label: \"Skulder, højre\", code: \"SHOULDER\", side: \"right\" },\n { key: \"uarm_left\", label: \"Overarm, venstre\", code: \"UARM\", side: \"left\" },\n { key: \"uarm_right\", label: \"Overarm, højre\", code: \"UARM\", side: \"right\" },\n { key: \"elbow_left\", label: \"Albue, venstre\", code: \"ELBOW\", side: \"left\" },\n { key: \"elbow_right\", label: \"Albue, højre\", code: \"ELBOW\", side: \"right\" },\n { key: \"farm_left\", label: \"Underarm, venstre\", code: \"FARM\", side: \"left\" },\n { key: \"farm_right\", label: \"Underarm, højre\", code: \"FARM\", side: \"right\" },\n { key: \"wrist_left\", label: \"Håndled, venstre\", code: \"WRIST\", side: \"left\" },\n { key: \"wrist_right\", label: \"Håndled, højre\", code: \"WRIST\", side: \"right\" },\n { key: \"hand_left\", label: \"Hånd, venstre\", code: \"HAND\", side: \"left\" },\n { key: \"hand_right\", label: \"Hånd, højre\", code: \"HAND\", side: \"right\" },\n { key: \"hip_left\", label: \"Hofte, venstre\", code: \"HIP\", side: \"left\" },\n { key: \"hip_right\", label: \"Hofte, højre\", code: \"HIP\", side: \"right\" },\n { key: \"thigh_left\", label: \"Lår, venstre\", code: \"THIGH\", side: \"left\" },\n { key: \"thigh_right\", label: \"Lår, højre\", code: \"THIGH\", side: \"right\" },\n { key: \"knee_left\", label: \"Knæ, venstre\", code: \"KNEE\", side: \"left\" },\n { key: \"knee_right\", label: \"Knæ, højre\", code: \"KNEE\", side: \"right\" },\n { key: \"lowleg_left\", label: \"Underben, venstre\", code: \"LOWLEG\", side: \"left\" },\n { key: \"lowleg_right\", label: \"Underben, højre\", code: \"LOWLEG\", side: \"right\" },\n { key: \"ankle_left\", label: \"Ankel, venstre\", code: \"ANKLE\", side: \"left\" },\n { key: \"ankle_right\", label: \"Ankel, højre\", code: \"ANKLE\", side: \"right\" },\n { key: \"foot_left\", label: \"Fod, venstre\", code: \"FOOT\", side: \"left\" },\n { key: \"foot_right\", label: \"Fod, højre\", code: \"FOOT\", side: \"right\" },\n];\n\nconst REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));\nexport const REGION_KEYS: readonly string[] = REGIONS.map((r) => r.key);\n\n/** Look up a region by its key. */\nexport function getRegion(key: string): BodyRegion | undefined {\n return REGIONS.find((r) => r.key === key);\n}\n\n// ---- PainReport model ---------------------------------------------------\n\nexport const PAIN_TYPES = [\"stikkende\", \"dump\", \"konstant\", \"jagende\"] as const;\nexport type PainType = (typeof PAIN_TYPES)[number];\n\n/** One marked pain point. `region` MUST be a known region key; `intensity` is a\n * 0-10 integer; `type` is optional but constrained; `timestamp` is an ISO string. */\nexport const painPointSchema = z.object({\n region: z.string().refine((k) => REGION_KEY_SET.has(k), { message: \"unknown region\" }),\n intensity: z.number().int().min(0).max(10),\n type: z.enum(PAIN_TYPES).optional(),\n timestamp: z.string(),\n});\nexport type PainPoint = z.infer<typeof painPointSchema>;\n\nexport const painReportSchema = z.array(painPointSchema);\nexport type PainReport = PainPoint[];\n\n// ---- Selection engine (framework-agnostic) ------------------------------\n\nexport interface PainSelection {\n /** Mark (or update) pain on a region. One point per region — latest wins. */\n set(region: string, intensity: number, type?: PainType): PainPoint;\n remove(region: string): boolean;\n get(region: string): PainPoint | undefined;\n has(region: string): boolean;\n clear(): void;\n /** The current, validated PainReport. */\n getReport(): PainReport;\n}\n\nexport interface PainSelectionOptions {\n /** Injectable clock (ISO string) — defaults to `new Date().toISOString()`. */\n now?: () => string;\n}\n\n/** Create a selection engine seeded with an optional report. Pure state — no\n * DOM, no framework, no network. One point per region. */\nexport function createPainSelection(\n initial: PainReport = [],\n opts: PainSelectionOptions = {},\n): PainSelection {\n const now = opts.now ?? (() => new Date().toISOString());\n const map = new Map<string, PainPoint>();\n for (const p of initial) {\n const v = painPointSchema.parse(p);\n map.set(v.region, v);\n }\n return {\n set(region, intensity, type) {\n const point = painPointSchema.parse({ region, intensity, type, timestamp: now() });\n map.set(point.region, point);\n return point;\n },\n remove: (region) => map.delete(region),\n get: (region) => map.get(region),\n has: (region) => map.has(region),\n clear: () => map.clear(),\n getReport: () => painReportSchema.parse(Array.from(map.values())),\n };\n}\n\n// ---- Per-app region config (the toggle) ---------------------------------\n\nexport interface RegionSetting {\n /** Render this region at all. Default true. */\n visible?: boolean;\n /** Allow marking pain on this region. Default true. */\n selectable?: boolean;\n}\n\n/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */\nexport type RegionConfig = Record<string, RegionSetting>;\n\n/** The regions an app should render, honouring `visible` (default true). */\nexport function resolveRegions(config: RegionConfig = {}): BodyRegion[] {\n return REGIONS.filter((r) => config[r.key]?.visible ?? true);\n}\n\n/** Whether a region may be marked. A hidden region is never selectable. */\nexport function isSelectable(key: string, config: RegionConfig = {}): boolean {\n const s = config[key];\n if (s?.visible === false) return false;\n return s?.selectable ?? true;\n}\n\n// ---- palette (consumer-defined colours — shared by the 2D + 3D renderers) ---\n\n/** Colour control for the body renderers. Consumers pass a palette to theme the\n * body base colour, the hover + selected highlights, the pain-heat colours, and\n * optional per-region base colours. All values are CSS/hex colour strings. */\nexport interface BodymapPalette {\n /** Base body colour (an unmarked region). */\n body: string;\n /** Region highlight on hover (before click). */\n hover: string;\n /** A region selected (clicked) but not yet given an intensity. */\n selected: string;\n /** Pain-intensity heat colours: low (0-3), mid (4-6), high (7-10). */\n heat: { low: string; mid: string; high: string };\n /** Optional per-region base-colour overrides (region key → colour). */\n regions?: Record<string, string>;\n}\n\n/** The fleet default palette. Override any field per consumer. */\nexport const defaultPalette: BodymapPalette = {\n body: \"#d2d7de\",\n hover: \"#8fd0cd\",\n selected: \"#5cc4b7\",\n heat: { low: \"#fcd34d\", mid: \"#fb923c\", high: \"#ef4444\" },\n};\n\n/** The heat colour for a pain intensity, honouring the palette. */\nexport function heatFor(intensity: number, palette: BodymapPalette = defaultPalette): string {\n return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;\n}\n\n/** The base colour for a region (a per-region override, else the body colour). */\nexport function baseColorFor(regionKey: string, palette: BodymapPalette = defaultPalette): string {\n return palette.regions?.[regionKey] ?? palette.body;\n}\n\n// ---- bodymap/v1 serialization (the shared cross-app / native wire format) ---\n//\n// The shape every consumer + the native mobile apps read (aligned with\n// fd-sundhed's bodymap/v1: region CODE + side + intensity + quality + view).\n// The internal PainReport keys on the region KEY; this maps key -> clinical CODE\n// so the report is portable and human-readable on the wire.\n\nexport type BodyView = \"front\" | \"back\" | \"left\" | \"right\";\n/** Side in the serialized report — a midline region (no side) becomes \"center\". */\nexport type SerializedSide = \"left\" | \"right\" | \"center\";\n\nexport interface SerializedPainPoint {\n /** Clinical region CODE (e.g. \"LUMB\"). */\n region: string;\n side: SerializedSide;\n intensity: number;\n quality?: PainType;\n}\n\nexport interface BodymapReportV1 {\n schema: \"bodymap/v1\";\n view: BodyView;\n points: SerializedPainPoint[];\n}\n\nexport const bodymapReportV1Schema = z.object({\n schema: z.literal(\"bodymap/v1\"),\n view: z.enum([\"front\", \"back\", \"left\", \"right\"]),\n points: z.array(\n z.object({\n region: z.string(),\n side: z.enum([\"left\", \"right\", \"center\"]),\n intensity: z.number().int().min(0).max(10),\n quality: z.enum(PAIN_TYPES).optional(),\n }),\n ),\n});\n\n/** Serialize a PainReport to the shared `bodymap/v1` wire format. */\nexport function serializeReport(\n report: PainReport,\n opts: { view?: BodyView } = {},\n): BodymapReportV1 {\n return {\n schema: \"bodymap/v1\",\n view: opts.view ?? \"front\",\n points: report.map((p) => {\n const r = getRegion(p.region);\n return {\n region: r?.code ?? p.region,\n side: (r?.side ?? \"center\") as SerializedSide,\n intensity: p.intensity,\n quality: p.type,\n };\n }),\n };\n}\n\n/** Parse a `bodymap/v1` report back into an internal PainReport. Region CODE →\n * key; a point whose code is unknown to this taxonomy is dropped. */\nexport function deserializeReport(\n env: unknown,\n now: () => string = () => new Date().toISOString(),\n): PainReport {\n const parsed = bodymapReportV1Schema.parse(env);\n // code is side-less, so a point is identified by code + side.\n const byCodeSide = new Map(\n REGIONS.map((r) => [`${r.code}|${r.side ?? \"center\"}`, r.key] as const),\n );\n const out: PainReport = [];\n for (const sp of parsed.points) {\n const key = byCodeSide.get(`${sp.region}|${sp.side}`);\n if (!key) continue;\n out.push(\n painPointSchema.parse({\n region: key,\n intensity: sp.intensity,\n type: sp.quality,\n timestamp: now(),\n }),\n );\n }\n return out;\n}\n","// @broberg/bodymap/react — the 2D SVG renderer (F052.2).\n//\n// <BodyMap> is the shippable MVP: a genderless, colour-blocked body a patient\n// clicks to mark where it hurts. Selecting a region opens a custom intensity\n// (0-10) + quality control (NO native inputs) that upserts a validated\n// PainReport and fires onChange. Honours the per-app RegionConfig. The 3D\n// <BodyMap3D> (F052.6) will render the SAME core on the SAME onChange contract.\n\nimport { useEffect, useState } from \"react\";\nimport {\n REGIONS,\n PAIN_TYPES,\n painPointSchema,\n resolveRegions,\n isSelectable,\n type BodyRegion,\n type PainReport,\n type PainType,\n type RegionConfig,\n} from \"./index.js\";\n\n// ---- geometry: each region → an SVG shape (a neutral colour-blockout) --------\n\ntype Shape =\n | { el: \"circle\"; cx: number; cy: number; r: number }\n | { el: \"ellipse\"; cx: number; cy: number; rx: number; ry: number }\n | { el: \"rect\"; x: number; y: number; width: number; height: number; rx: number };\n\n// Front-view geometry (viewBox 260×470). The 2D MVP renders the front-visible\n// regions; back-only regions (thora, lumbar) + hip are driven by the 3D body /\n// a later back view. \"left\"/\"right\" are placed self-view (viewer's left = key\n// left) for the MVP — the facing-the-patient mirror is a later config option.\nconst SHAPES: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n chest: { el: \"rect\", x: 100, y: 84, width: 60, height: 104, rx: 16 },\n groin: { el: \"rect\", x: 106, y: 190, width: 48, height: 30, rx: 13 },\n // left arm (viewer-left)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n // right arm (viewer-right)\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL: Record<string, string> = {\n head: \"#cbb7ec\", neck: \"#b8a3e6\", chest: \"#e26d6d\", groin: \"#e88a52\",\n shoulder_left: \"#f6c667\", shoulder_right: \"#f6c667\",\n uarm_left: \"#7ececb\", uarm_right: \"#7ececb\",\n elbow_left: \"#86cfcc\", elbow_right: \"#86cfcc\",\n farm_left: \"#8fd0cd\", farm_right: \"#8fd0cd\",\n wrist_left: \"#9ad6d3\", wrist_right: \"#9ad6d3\",\n hand_left: \"#a7ddda\", hand_right: \"#a7ddda\",\n thigh_left: \"#7fb2e8\", thigh_right: \"#7fb2e8\",\n knee_left: \"#c79be0\", knee_right: \"#c79be0\",\n lowleg_left: \"#e39ec6\", lowleg_right: \"#e39ec6\",\n ankle_left: \"#eeaab9\", ankle_right: \"#eeaab9\",\n foot_left: \"#f0a5a5\", foot_right: \"#f0a5a5\",\n};\n\nfunction center(s: Shape): { x: number; y: number } {\n if (s.el === \"rect\") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };\n return { x: s.cx, y: s.cy };\n}\nfunction heat(v: number): string {\n return v >= 7 ? \"#ef4444\" : v >= 4 ? \"#fb923c\" : \"#fcd34d\";\n}\n\n// ---- one-time style injection (no external CSS; theme via CSS variables) -----\n\nconst STYLE_ID = \"broberg-bodymap-styles\";\nconst STYLE = `\n.bmap{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#64748b;\n 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)}\n.bmap__svg{flex:0 0 auto}\n.bmap__region{cursor:pointer;stroke:#fff;stroke-width:2.4;transition:filter .12s}\n.bmap__region:hover{filter:brightness(1.08) saturate(1.15)}\n.bmap__region--locked{cursor:default;opacity:.4}\n.bmap__region--sel{stroke:var(--bmap-accent);stroke-width:3.6}\n.bmap__heatn{font:700 11px ui-sans-serif;fill:#fff;pointer-events:none}\n.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)}\n.bmap__panel--empty{color:var(--bmap-muted);font-size:13.5px}\n.bmap__ph{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}\n.bmap__ph b{font-size:16px}\n.bmap__code{font:11px ui-monospace,monospace;color:var(--bmap-muted);background:#f1f5f9;border-radius:6px;padding:2px 7px}\n.bmap__lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bmap-muted);margin:0 0 7px}\n.bmap__scale{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:14px}\n.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}\n.bmap__i:hover{border-color:var(--bmap-accent)}\n.bmap__i:active{transform:scale(.9)}\n.bmap__i--on{background:var(--bmap-accent);border-color:var(--bmap-accent);color:#fff}\n.bmap__chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}\n.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}\n.bmap__chip:hover{border-color:var(--bmap-accent);color:var(--bmap-accent)}\n.bmap__chip:active{transform:scale(.96)}\n.bmap__chip--on{background:var(--bmap-ink);border-color:var(--bmap-ink);color:#fff}\n.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}\n.bmap__rm:hover{background:#fef2f2}\n.bmap__rm:active{transform:scale(.97)}\n`;\n\nfunction ensureStyles(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = STYLE;\n document.head.appendChild(el);\n}\n\n// ---- component ---------------------------------------------------------------\n\nexport interface BodyMapProps {\n /** Controlled report. Omit for an uncontrolled component (use defaultValue). */\n value?: PainReport;\n /** Initial report when uncontrolled. */\n defaultValue?: PainReport;\n /** Fired with the full validated PainReport on every change. */\n onChange?: (report: PainReport) => void;\n /** Per-app region config (visible / selectable). */\n config?: RegionConfig;\n className?: string;\n}\n\n/** 2D SVG body pain-map. Click a region → set intensity + quality → PainReport. */\nexport function BodyMap({ value, defaultValue, onChange, config = {}, className }: BodyMapProps) {\n useEffect(ensureStyles, []);\n const [internal, setInternal] = useState<PainReport>(defaultValue ?? []);\n const [selected, setSelected] = useState<string | null>(null);\n const report = value ?? internal;\n\n const commit = (next: PainReport) => {\n if (value === undefined) setInternal(next);\n onChange?.(next);\n };\n const pointOf = (key: string) => report.find((p) => p.region === key);\n const setPain = (key: string, intensity: number, type?: PainType) => {\n const point = painPointSchema.parse({ region: key, intensity, type, timestamp: new Date().toISOString() });\n commit([...report.filter((p) => p.region !== key), point]);\n };\n const removePain = (key: string) => {\n commit(report.filter((p) => p.region !== key));\n setSelected(null);\n };\n\n const regions = resolveRegions(config);\n const region: BodyRegion | undefined = selected ? REGIONS.find((r) => r.key === selected) : undefined;\n const current = selected ? pointOf(selected) : undefined;\n const rootCls = [\"bmap\", className].filter(Boolean).join(\" \");\n\n return (\n <div className={rootCls} data-testid=\"bodymap-root\">\n <svg\n className=\"bmap__svg\"\n viewBox=\"0 0 260 470\"\n width={230}\n height={416}\n role=\"group\"\n aria-label=\"Kropskort — vælg hvor det gør ondt\"\n >\n {regions.map((r) => {\n const s = SHAPES[r.key];\n if (!s) return null;\n const marked = pointOf(r.key);\n const selectable = isSelectable(r.key, config);\n const cls = [\n \"bmap__region\",\n !selectable && \"bmap__region--locked\",\n selected === r.key && \"bmap__region--sel\",\n ]\n .filter(Boolean)\n .join(\" \");\n const fill = marked ? heat(marked.intensity) : FILL[r.key] ?? \"#cbd5e1\";\n const common = {\n className: cls,\n fill,\n \"data-testid\": `bodymap-region-${r.key}`,\n role: selectable ? \"button\" : undefined,\n \"aria-label\": r.label,\n onClick: selectable ? () => setSelected(r.key) : undefined,\n };\n if (s.el === \"circle\") return <circle key={r.key} cx={s.cx} cy={s.cy} r={s.r} {...common} />;\n if (s.el === \"ellipse\") return <ellipse key={r.key} cx={s.cx} cy={s.cy} rx={s.rx} ry={s.ry} {...common} />;\n return <rect key={r.key} x={s.x} y={s.y} width={s.width} height={s.height} rx={s.rx} {...common} />;\n })}\n {/* intensity numbers on marked regions */}\n {report.map((p) => {\n const s = SHAPES[p.region];\n if (!s || (config[p.region]?.visible === false)) return null;\n const c = center(s);\n return (\n <text key={p.region} x={c.x} y={c.y + 4} textAnchor=\"middle\" className=\"bmap__heatn\">\n {p.intensity}\n </text>\n );\n })}\n </svg>\n\n {region ? (\n <div className=\"bmap__panel\" data-testid=\"bodymap-panel\">\n <div className=\"bmap__ph\">\n <b>{region.label}</b>\n <span className=\"bmap__code\">{region.code}</span>\n </div>\n <p className=\"bmap__lbl\">Intensitet (0-10)</p>\n <div className=\"bmap__scale\" role=\"group\" aria-label=\"Intensitet\">\n {Array.from({ length: 11 }, (_, n) => (\n <button\n key={n}\n type=\"button\"\n className={\"bmap__i\" + (current?.intensity === n ? \" bmap__i--on\" : \"\")}\n data-testid={`bodymap-intensity-${n}`}\n aria-pressed={current?.intensity === n}\n onClick={() => setPain(region.key, n, current?.type)}\n >\n {n}\n </button>\n ))}\n </div>\n <p className=\"bmap__lbl\">Kvalitet</p>\n <div className=\"bmap__chips\" role=\"group\" aria-label=\"Kvalitet\">\n {PAIN_TYPES.map((t) => (\n <button\n key={t}\n type=\"button\"\n className={\"bmap__chip\" + (current?.type === t ? \" bmap__chip--on\" : \"\")}\n data-testid={`bodymap-type-${t}`}\n aria-pressed={current?.type === t}\n onClick={() => setPain(region.key, current?.intensity ?? 5, t)}\n >\n {t}\n </button>\n ))}\n </div>\n {current && (\n <button type=\"button\" className=\"bmap__rm\" data-testid=\"bodymap-remove\" onClick={() => removePain(region.key)}>\n Fjern punkt\n </button>\n )}\n </div>\n ) : (\n <div className=\"bmap__panel bmap__panel--empty\" data-testid=\"bodymap-panel\">\n Klik en kropsdel for at markere smerte.\n </div>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/react.tsx"],"names":[],"mappings":"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","file":"react.js","sourcesContent":["// @broberg/bodymap — headless core (F052.1).\n//\n// Framework-neutral: the region taxonomy + the PainReport data model (zod) + a\n// selection engine + per-app region config. NO React/Preact/DOM import — the 2D\n// (SVG) and 3D (Three.js) renderers, and all three FD apps, share this one\n// contract. Output is a structured PainReport, never a bare image.\n\nimport { z } from \"zod\";\n\nexport type Side = \"left\" | \"right\";\n\nexport interface BodyRegion {\n /** Stable, unique identifier (snake_case) — the key used in a PainReport. */\n key: string;\n /** Human label (Danish). */\n label: string;\n /** Clinical short code (unique). */\n code: string;\n /** Body side, when the region is paired. */\n side?: Side;\n}\n\n/** The canonical body regions — the AUTHORITATIVE fd-sundhed clinical taxonomy\n * (docs/BODYMAP-TAKSONOMI.md, broberg-ai/fd-sundhed @360842f): 18 SIDE-LESS\n * clinical codes + a separate `side` field (L/R on limbs, C=center on axis\n * regions). The `key` is a unique per-side identifier; `code` is the side-less\n * clinical code that goes on the bodymap/v1 wire. NOT an anatomical atlas —\n * ~30 named surface regions for a pain-map. The 2D front renderer draws the\n * front-visible subset; the 3D body (F052.6) drives them all. */\nexport const REGIONS: readonly BodyRegion[] = [\n // axis / centre-line (serialised side \"center\")\n { key: \"head\", label: \"Hoved\", code: \"HEAD\" },\n { key: \"neck\", label: \"Nakke\", code: \"NECK\" },\n { key: \"chest\", label: \"Bryst\", code: \"CHEST\" },\n { key: \"thora\", label: \"Øvre ryg (thorakal)\", code: \"THORA\" },\n { key: \"lumbar\", label: \"Lænd (lumbal)\", code: \"LUMBAR\" },\n { key: \"groin\", label: \"Lyske\", code: \"GROIN\" },\n // paired limbs / sides (L / R)\n { key: \"shoulder_left\", label: \"Skulder, venstre\", code: \"SHOULDER\", side: \"left\" },\n { key: \"shoulder_right\", label: \"Skulder, højre\", code: \"SHOULDER\", side: \"right\" },\n { key: \"uarm_left\", label: \"Overarm, venstre\", code: \"UARM\", side: \"left\" },\n { key: \"uarm_right\", label: \"Overarm, højre\", code: \"UARM\", side: \"right\" },\n { key: \"elbow_left\", label: \"Albue, venstre\", code: \"ELBOW\", side: \"left\" },\n { key: \"elbow_right\", label: \"Albue, højre\", code: \"ELBOW\", side: \"right\" },\n { key: \"farm_left\", label: \"Underarm, venstre\", code: \"FARM\", side: \"left\" },\n { key: \"farm_right\", label: \"Underarm, højre\", code: \"FARM\", side: \"right\" },\n { key: \"wrist_left\", label: \"Håndled, venstre\", code: \"WRIST\", side: \"left\" },\n { key: \"wrist_right\", label: \"Håndled, højre\", code: \"WRIST\", side: \"right\" },\n { key: \"hand_left\", label: \"Hånd, venstre\", code: \"HAND\", side: \"left\" },\n { key: \"hand_right\", label: \"Hånd, højre\", code: \"HAND\", side: \"right\" },\n { key: \"hip_left\", label: \"Hofte, venstre\", code: \"HIP\", side: \"left\" },\n { key: \"hip_right\", label: \"Hofte, højre\", code: \"HIP\", side: \"right\" },\n { key: \"thigh_left\", label: \"Lår, venstre\", code: \"THIGH\", side: \"left\" },\n { key: \"thigh_right\", label: \"Lår, højre\", code: \"THIGH\", side: \"right\" },\n { key: \"knee_left\", label: \"Knæ, venstre\", code: \"KNEE\", side: \"left\" },\n { key: \"knee_right\", label: \"Knæ, højre\", code: \"KNEE\", side: \"right\" },\n { key: \"lowleg_left\", label: \"Underben, venstre\", code: \"LOWLEG\", side: \"left\" },\n { key: \"lowleg_right\", label: \"Underben, højre\", code: \"LOWLEG\", side: \"right\" },\n { key: \"ankle_left\", label: \"Ankel, venstre\", code: \"ANKLE\", side: \"left\" },\n { key: \"ankle_right\", label: \"Ankel, højre\", code: \"ANKLE\", side: \"right\" },\n { key: \"foot_left\", label: \"Fod, venstre\", code: \"FOOT\", side: \"left\" },\n { key: \"foot_right\", label: \"Fod, højre\", code: \"FOOT\", side: \"right\" },\n];\n\nconst REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));\nexport const REGION_KEYS: readonly string[] = REGIONS.map((r) => r.key);\n\n/** Look up a region by its key. */\nexport function getRegion(key: string): BodyRegion | undefined {\n return REGIONS.find((r) => r.key === key);\n}\n\n// ---- PainReport model ---------------------------------------------------\n\nexport const PAIN_TYPES = [\"stikkende\", \"dump\", \"konstant\", \"jagende\"] as const;\nexport type PainType = (typeof PAIN_TYPES)[number];\n\n/** One marked pain point. `region` MUST be a known region key; `intensity` is a\n * 0-10 integer; `type` is optional but constrained; `timestamp` is an ISO string. */\nexport const painPointSchema = z.object({\n region: z.string().refine((k) => REGION_KEY_SET.has(k), { message: \"unknown region\" }),\n intensity: z.number().int().min(0).max(10),\n type: z.enum(PAIN_TYPES).optional(),\n timestamp: z.string(),\n});\nexport type PainPoint = z.infer<typeof painPointSchema>;\n\nexport const painReportSchema = z.array(painPointSchema);\nexport type PainReport = PainPoint[];\n\n// ---- Selection engine (framework-agnostic) ------------------------------\n\nexport interface PainSelection {\n /** Mark (or update) pain on a region. One point per region — latest wins. */\n set(region: string, intensity: number, type?: PainType): PainPoint;\n remove(region: string): boolean;\n get(region: string): PainPoint | undefined;\n has(region: string): boolean;\n clear(): void;\n /** The current, validated PainReport. */\n getReport(): PainReport;\n}\n\nexport interface PainSelectionOptions {\n /** Injectable clock (ISO string) — defaults to `new Date().toISOString()`. */\n now?: () => string;\n}\n\n/** Create a selection engine seeded with an optional report. Pure state — no\n * DOM, no framework, no network. One point per region. */\nexport function createPainSelection(\n initial: PainReport = [],\n opts: PainSelectionOptions = {},\n): PainSelection {\n const now = opts.now ?? (() => new Date().toISOString());\n const map = new Map<string, PainPoint>();\n for (const p of initial) {\n const v = painPointSchema.parse(p);\n map.set(v.region, v);\n }\n return {\n set(region, intensity, type) {\n const point = painPointSchema.parse({ region, intensity, type, timestamp: now() });\n map.set(point.region, point);\n return point;\n },\n remove: (region) => map.delete(region),\n get: (region) => map.get(region),\n has: (region) => map.has(region),\n clear: () => map.clear(),\n getReport: () => painReportSchema.parse(Array.from(map.values())),\n };\n}\n\n// ---- Per-app region config (the toggle) ---------------------------------\n\nexport interface RegionSetting {\n /** Render this region at all. Default true. */\n visible?: boolean;\n /** Allow marking pain on this region. Default true. */\n selectable?: boolean;\n}\n\n/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */\nexport type RegionConfig = Record<string, RegionSetting>;\n\n/** The regions an app should render, honouring `visible` (default true). */\nexport function resolveRegions(config: RegionConfig = {}): BodyRegion[] {\n return REGIONS.filter((r) => config[r.key]?.visible ?? true);\n}\n\n/** Whether a region may be marked. A hidden region is never selectable. */\nexport function isSelectable(key: string, config: RegionConfig = {}): boolean {\n const s = config[key];\n if (s?.visible === false) return false;\n return s?.selectable ?? true;\n}\n\n// ---- palette (consumer-defined colours — shared by the 2D + 3D renderers) ---\n\n/** Colour control for the body renderers. Consumers pass a palette to theme the\n * body base colour, the hover + selected highlights, the pain-heat colours, and\n * optional per-region base colours. All values are CSS/hex colour strings. */\nexport interface BodymapPalette {\n /** Base body colour (an unmarked region). */\n body: string;\n /** Region highlight on hover (before click). */\n hover: string;\n /** A region selected (clicked) but not yet given an intensity. */\n selected: string;\n /** Pain-intensity heat colours: low (0-3), mid (4-6), high (7-10). */\n heat: { low: string; mid: string; high: string };\n /** Optional per-region base-colour overrides (region key → colour). */\n regions?: Record<string, string>;\n}\n\n/** The fleet default palette. Override any field per consumer. */\nexport const defaultPalette: BodymapPalette = {\n body: \"#d2d7de\",\n hover: \"#8fd0cd\",\n selected: \"#5cc4b7\",\n heat: { low: \"#fcd34d\", mid: \"#fb923c\", high: \"#ef4444\" },\n};\n\n/** The heat colour for a pain intensity, honouring the palette. */\nexport function heatFor(intensity: number, palette: BodymapPalette = defaultPalette): string {\n return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;\n}\n\n/** The base colour for a region (a per-region override, else the body colour). */\nexport function baseColorFor(regionKey: string, palette: BodymapPalette = defaultPalette): string {\n return palette.regions?.[regionKey] ?? palette.body;\n}\n\n// ---- bodymap/v1 serialization (the shared cross-app / native wire format) ---\n//\n// The shape every consumer + the native mobile apps read (aligned with\n// fd-sundhed's bodymap/v1: region CODE + side + intensity + quality + view).\n// The internal PainReport keys on the region KEY; this maps key -> clinical CODE\n// so the report is portable and human-readable on the wire.\n\nexport type BodyView = \"front\" | \"back\" | \"left\" | \"right\";\n/** Side in the serialized report — a midline region (no side) becomes \"center\". */\nexport type SerializedSide = \"left\" | \"right\" | \"center\";\n\nexport interface SerializedPainPoint {\n /** Clinical region CODE (e.g. \"LUMB\"). */\n region: string;\n side: SerializedSide;\n intensity: number;\n quality?: PainType;\n}\n\nexport interface BodymapReportV1 {\n schema: \"bodymap/v1\";\n view: BodyView;\n points: SerializedPainPoint[];\n}\n\nexport const bodymapReportV1Schema = z.object({\n schema: z.literal(\"bodymap/v1\"),\n view: z.enum([\"front\", \"back\", \"left\", \"right\"]),\n points: z.array(\n z.object({\n region: z.string(),\n side: z.enum([\"left\", \"right\", \"center\"]),\n intensity: z.number().int().min(0).max(10),\n quality: z.enum(PAIN_TYPES).optional(),\n }),\n ),\n});\n\n/** Serialize a PainReport to the shared `bodymap/v1` wire format. */\nexport function serializeReport(\n report: PainReport,\n opts: { view?: BodyView } = {},\n): BodymapReportV1 {\n return {\n schema: \"bodymap/v1\",\n view: opts.view ?? \"front\",\n points: report.map((p) => {\n const r = getRegion(p.region);\n return {\n region: r?.code ?? p.region,\n side: (r?.side ?? \"center\") as SerializedSide,\n intensity: p.intensity,\n quality: p.type,\n };\n }),\n };\n}\n\n/** Parse a `bodymap/v1` report back into an internal PainReport. Region CODE →\n * key; a point whose code is unknown to this taxonomy is dropped. */\nexport function deserializeReport(\n env: unknown,\n now: () => string = () => new Date().toISOString(),\n): PainReport {\n const parsed = bodymapReportV1Schema.parse(env);\n // code is side-less, so a point is identified by code + side.\n const byCodeSide = new Map(\n REGIONS.map((r) => [`${r.code}|${r.side ?? \"center\"}`, r.key] as const),\n );\n const out: PainReport = [];\n for (const sp of parsed.points) {\n const key = byCodeSide.get(`${sp.region}|${sp.side}`);\n if (!key) continue;\n out.push(\n painPointSchema.parse({\n region: key,\n intensity: sp.intensity,\n type: sp.quality,\n timestamp: now(),\n }),\n );\n }\n return out;\n}\n","// @broberg/bodymap/react — the 2D SVG renderer (F052.2).\n//\n// <BodyMap> is the shippable MVP: a genderless, colour-blocked body a patient\n// clicks to mark where it hurts. Selecting a region opens a custom intensity\n// (0-10) + quality control (NO native inputs) that upserts a validated\n// PainReport and fires onChange. Honours the per-app RegionConfig. The 3D\n// <BodyMap3D> (F052.6) will render the SAME core on the SAME onChange contract.\n\nimport { useEffect, useState } from \"react\";\nimport {\n REGIONS,\n PAIN_TYPES,\n painPointSchema,\n resolveRegions,\n isSelectable,\n type BodyRegion,\n type PainReport,\n type PainType,\n type RegionConfig,\n} from \"./index.js\";\n\n// ---- geometry: each region → an SVG shape (a neutral colour-blockout) --------\n\ntype Shape =\n | { el: \"circle\"; cx: number; cy: number; r: number }\n | { el: \"ellipse\"; cx: number; cy: number; rx: number; ry: number }\n | { el: \"rect\"; x: number; y: number; width: number; height: number; rx: number };\n\n// Front-view geometry (viewBox 260×470). The 2D MVP renders the front-visible\n// regions; back-only regions (thora, lumbar) + hip are driven by the 3D body /\n// a later back view. \"left\"/\"right\" are placed self-view (viewer's left = key\n// left) for the MVP — the facing-the-patient mirror is a later config option.\nconst SHAPES: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n chest: { el: \"rect\", x: 100, y: 84, width: 60, height: 104, rx: 16 },\n groin: { el: \"rect\", x: 106, y: 190, width: 48, height: 30, rx: 13 },\n // left arm (viewer-left)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n // right arm (viewer-right)\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL: Record<string, string> = {\n head: \"#cbb7ec\", neck: \"#b8a3e6\", chest: \"#e26d6d\", groin: \"#e88a52\",\n shoulder_left: \"#f6c667\", shoulder_right: \"#f6c667\",\n uarm_left: \"#7ececb\", uarm_right: \"#7ececb\",\n elbow_left: \"#86cfcc\", elbow_right: \"#86cfcc\",\n farm_left: \"#8fd0cd\", farm_right: \"#8fd0cd\",\n wrist_left: \"#9ad6d3\", wrist_right: \"#9ad6d3\",\n hand_left: \"#a7ddda\", hand_right: \"#a7ddda\",\n thigh_left: \"#7fb2e8\", thigh_right: \"#7fb2e8\",\n knee_left: \"#c79be0\", knee_right: \"#c79be0\",\n lowleg_left: \"#e39ec6\", lowleg_right: \"#e39ec6\",\n ankle_left: \"#eeaab9\", ankle_right: \"#eeaab9\",\n foot_left: \"#f0a5a5\", foot_right: \"#f0a5a5\",\n};\n\n// Back-view geometry (same viewBox 260×470 + same limb/head geometry as the\n// front, so a marked limb sits at the same spot in both views). The torso swaps\n// front regions (chest, groin) for the posterior ones — THORA (upper back),\n// LUMBAR (lower back) and HIP (buttocks, paired) — the pilot's blocker regions.\n// Same self-view L/R convention as the front (patient-left = viewer-left).\nconst SHAPES_BACK: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n thora: { el: \"rect\", x: 100, y: 84, width: 60, height: 58, rx: 15 },\n lumbar: { el: \"rect\", x: 104, y: 144, width: 52, height: 44, rx: 13 },\n hip_left: { el: \"rect\", x: 100, y: 190, width: 27, height: 32, rx: 13 },\n hip_right: { el: \"rect\", x: 133, y: 190, width: 27, height: 32, rx: 13 },\n // arms — shared with the front (back of the arm sits at the same x/y)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs — back (thigh→calf→heel, same x/y as the front)\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL_BACK: Record<string, string> = {\n ...FILL,\n thora: \"#e2896d\",\n lumbar: \"#d97fa0\",\n hip_left: \"#c99bd6\", hip_right: \"#c99bd6\",\n};\n\nexport type BodyView2D = \"front\" | \"back\";\n\nfunction center(s: Shape): { x: number; y: number } {\n if (s.el === \"rect\") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };\n return { x: s.cx, y: s.cy };\n}\nfunction heat(v: number): string {\n return v >= 7 ? \"#ef4444\" : v >= 4 ? \"#fb923c\" : \"#fcd34d\";\n}\n\n// ---- one-time style injection (no external CSS; theme via CSS variables) -----\n\nconst STYLE_ID = \"broberg-bodymap-styles\";\nconst STYLE = `\n.bmap{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#64748b;\n 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)}\n.bmap__svg{flex:0 0 auto}\n.bmap__stage{display:flex;flex-direction:column;gap:12px;flex:0 0 auto}\n.bmap__viewtoggle{display:inline-flex;align-self:flex-start;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}\n.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}\n.bmap__vbtn:hover{color:var(--bmap-ink)}\n.bmap__vbtn:active{transform:scale(.97)}\n.bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}\n.bmap__region{cursor:pointer;stroke:#fff;stroke-width:2.4;transition:filter .12s}\n.bmap__region:hover{filter:brightness(1.08) saturate(1.15)}\n.bmap__region--locked{cursor:default;opacity:.4}\n.bmap__region--sel{stroke:var(--bmap-accent);stroke-width:3.6}\n.bmap__heatn{font:700 11px ui-sans-serif;fill:#fff;pointer-events:none}\n.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)}\n.bmap__panel--empty{color:var(--bmap-muted);font-size:13.5px}\n.bmap__ph{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}\n.bmap__ph b{font-size:16px}\n.bmap__code{font:11px ui-monospace,monospace;color:var(--bmap-muted);background:#f1f5f9;border-radius:6px;padding:2px 7px}\n.bmap__lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bmap-muted);margin:0 0 7px}\n.bmap__scale{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:14px}\n.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}\n.bmap__i:hover{border-color:var(--bmap-accent)}\n.bmap__i:active{transform:scale(.9)}\n.bmap__i--on{background:var(--bmap-accent);border-color:var(--bmap-accent);color:#fff}\n.bmap__chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}\n.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}\n.bmap__chip:hover{border-color:var(--bmap-accent);color:var(--bmap-accent)}\n.bmap__chip:active{transform:scale(.96)}\n.bmap__chip--on{background:var(--bmap-ink);border-color:var(--bmap-ink);color:#fff}\n.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}\n.bmap__rm:hover{background:#fef2f2}\n.bmap__rm:active{transform:scale(.97)}\n`;\n\nfunction ensureStyles(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = STYLE;\n document.head.appendChild(el);\n}\n\n// ---- component ---------------------------------------------------------------\n\nexport interface BodyMapProps {\n /** Controlled report. Omit for an uncontrolled component (use defaultValue). */\n value?: PainReport;\n /** Initial report when uncontrolled. */\n defaultValue?: PainReport;\n /** Fired with the full validated PainReport on every change. */\n onChange?: (report: PainReport) => void;\n /** Per-app region config (visible / selectable). */\n config?: RegionConfig;\n /** Initial body view (front / back). Default \"front\". */\n defaultView?: BodyView2D;\n /** Fired when the user toggles the front/back view — pass it to\n * serializeReport({ view }) so the wire report carries the active view. */\n onViewChange?: (view: BodyView2D) => void;\n className?: string;\n}\n\n/** 2D SVG body pain-map. Click a region → set intensity + quality → PainReport. */\nexport function BodyMap({\n value, defaultValue, onChange, config = {}, defaultView = \"front\", onViewChange, className,\n}: BodyMapProps) {\n useEffect(ensureStyles, []);\n const [internal, setInternal] = useState<PainReport>(defaultValue ?? []);\n const [selected, setSelected] = useState<string | null>(null);\n const [view, setView] = useState<BodyView2D>(defaultView);\n const report = value ?? internal;\n const shapes = view === \"back\" ? SHAPES_BACK : SHAPES;\n const fills = view === \"back\" ? FILL_BACK : FILL;\n const changeView = (v: BodyView2D) => {\n if (v === view) return;\n setView(v);\n const sh = v === \"back\" ? SHAPES_BACK : SHAPES;\n if (selected && !sh[selected]) setSelected(null); // selected region absent in the new view\n onViewChange?.(v);\n };\n\n const commit = (next: PainReport) => {\n if (value === undefined) setInternal(next);\n onChange?.(next);\n };\n const pointOf = (key: string) => report.find((p) => p.region === key);\n const setPain = (key: string, intensity: number, type?: PainType) => {\n const point = painPointSchema.parse({ region: key, intensity, type, timestamp: new Date().toISOString() });\n commit([...report.filter((p) => p.region !== key), point]);\n };\n const removePain = (key: string) => {\n commit(report.filter((p) => p.region !== key));\n setSelected(null);\n };\n\n const regions = resolveRegions(config);\n const region: BodyRegion | undefined = selected ? REGIONS.find((r) => r.key === selected) : undefined;\n const current = selected ? pointOf(selected) : undefined;\n const rootCls = [\"bmap\", className].filter(Boolean).join(\" \");\n\n return (\n <div className={rootCls} data-testid=\"bodymap-root\">\n <div className=\"bmap__stage\">\n <div className=\"bmap__viewtoggle\" role=\"group\" aria-label=\"Visning\">\n <button\n type=\"button\"\n className={\"bmap__vbtn\" + (view === \"front\" ? \" bmap__vbtn--on\" : \"\")}\n data-testid=\"bodymap-view-front\"\n aria-pressed={view === \"front\"}\n onClick={() => changeView(\"front\")}\n >\n Forfra\n </button>\n <button\n type=\"button\"\n className={\"bmap__vbtn\" + (view === \"back\" ? \" bmap__vbtn--on\" : \"\")}\n data-testid=\"bodymap-view-back\"\n aria-pressed={view === \"back\"}\n onClick={() => changeView(\"back\")}\n >\n Bagfra\n </button>\n </div>\n <svg\n className=\"bmap__svg\"\n viewBox=\"0 0 260 470\"\n width={230}\n height={416}\n role=\"group\"\n aria-label=\"Kropskort — vælg hvor det gør ondt\"\n >\n {regions.map((r) => {\n const s = shapes[r.key];\n if (!s) return null;\n const marked = pointOf(r.key);\n const selectable = isSelectable(r.key, config);\n const cls = [\n \"bmap__region\",\n !selectable && \"bmap__region--locked\",\n selected === r.key && \"bmap__region--sel\",\n ]\n .filter(Boolean)\n .join(\" \");\n const fill = marked ? heat(marked.intensity) : fills[r.key] ?? \"#cbd5e1\";\n const common = {\n className: cls,\n fill,\n \"data-testid\": `bodymap-region-${r.key}`,\n role: selectable ? \"button\" : undefined,\n \"aria-label\": r.label,\n onClick: selectable ? () => setSelected(r.key) : undefined,\n };\n if (s.el === \"circle\") return <circle key={r.key} cx={s.cx} cy={s.cy} r={s.r} {...common} />;\n if (s.el === \"ellipse\") return <ellipse key={r.key} cx={s.cx} cy={s.cy} rx={s.rx} ry={s.ry} {...common} />;\n return <rect key={r.key} x={s.x} y={s.y} width={s.width} height={s.height} rx={s.rx} {...common} />;\n })}\n {/* intensity numbers on marked regions visible in the current view */}\n {report.map((p) => {\n const s = shapes[p.region];\n if (!s || (config[p.region]?.visible === false)) return null;\n const c = center(s);\n return (\n <text key={p.region} x={c.x} y={c.y + 4} textAnchor=\"middle\" className=\"bmap__heatn\">\n {p.intensity}\n </text>\n );\n })}\n </svg>\n </div>\n\n {region ? (\n <div className=\"bmap__panel\" data-testid=\"bodymap-panel\">\n <div className=\"bmap__ph\">\n <b>{region.label}</b>\n <span className=\"bmap__code\">{region.code}</span>\n </div>\n <p className=\"bmap__lbl\">Intensitet (0-10)</p>\n <div className=\"bmap__scale\" role=\"group\" aria-label=\"Intensitet\">\n {Array.from({ length: 11 }, (_, n) => (\n <button\n key={n}\n type=\"button\"\n className={\"bmap__i\" + (current?.intensity === n ? \" bmap__i--on\" : \"\")}\n data-testid={`bodymap-intensity-${n}`}\n aria-pressed={current?.intensity === n}\n onClick={() => setPain(region.key, n, current?.type)}\n >\n {n}\n </button>\n ))}\n </div>\n <p className=\"bmap__lbl\">Kvalitet</p>\n <div className=\"bmap__chips\" role=\"group\" aria-label=\"Kvalitet\">\n {PAIN_TYPES.map((t) => (\n <button\n key={t}\n type=\"button\"\n className={\"bmap__chip\" + (current?.type === t ? \" bmap__chip--on\" : \"\")}\n data-testid={`bodymap-type-${t}`}\n aria-pressed={current?.type === t}\n onClick={() => setPain(region.key, current?.intensity ?? 5, t)}\n >\n {t}\n </button>\n ))}\n </div>\n {current && (\n <button type=\"button\" className=\"bmap__rm\" data-testid=\"bodymap-remove\" onClick={() => removePain(region.key)}>\n Fjern punkt\n </button>\n )}\n </div>\n ) : (\n <div className=\"bmap__panel bmap__panel--empty\" data-testid=\"bodymap-panel\">\n Klik en kropsdel for at markere smerte.\n </div>\n )}\n </div>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@broberg/bodymap",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Interactive body pain-map: a genderless body a patient clicks to mark where it hurts, producing a structured PainReport (bodymap/v1). Framework-neutral core (region taxonomy + zod PainReport model + selection) + a 2D SVG React renderer; 3D (vanilla Three.js) later on the same core.",
5
5
  "type": "module",
6
6
  "license": "MIT",