@broberg/bodymap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/dist/index.cjs +157 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +178 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +279 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +47 -0
- package/dist/react.d.ts +47 -0
- package/dist/react.js +277 -0
- package/dist/react.js.map +1 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @broberg/bodymap
|
|
2
|
+
|
|
3
|
+
Interactive body **pain-map**: a genderless body a patient clicks to mark where it
|
|
4
|
+
hurts, producing a structured `PainReport` (the shared `bodymap/v1` wire format) —
|
|
5
|
+
never a bare image. The clinical region taxonomy, the data model and the selection
|
|
6
|
+
engine are **framework-neutral**; the renderer is swappable. This release ships the
|
|
7
|
+
headless core + a 2D SVG React renderer (front view). A rotatable 3D renderer
|
|
8
|
+
(vanilla Three.js) and a 2D back view land in follow-up `0.1.x` releases on the
|
|
9
|
+
same core.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @broberg/bodymap
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## The data model
|
|
16
|
+
|
|
17
|
+
One `PainReport` is an array of points — one per marked region, latest wins:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
type PainPoint = {
|
|
21
|
+
region: string; // a region key, e.g. "shoulder_left"
|
|
22
|
+
intensity: number; // 0–10
|
|
23
|
+
type?: "stikkende" | "dump" | "konstant" | "jagende";
|
|
24
|
+
timestamp: string; // ISO
|
|
25
|
+
};
|
|
26
|
+
type PainReport = PainPoint[];
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The taxonomy is the authoritative fd-sundhed clinical set: **18 side-less codes**
|
|
30
|
+
(HEAD, NECK, SHOULDER, UARM, ELBOW, FARM, WRIST, HAND, CHEST, THORA, LUMBAR, HIP,
|
|
31
|
+
GROIN, THIGH, KNEE, LOWLEG, ANKLE, FOOT) × a `side` (`left` / `right` / centre) →
|
|
32
|
+
30 concrete regions. `REGIONS` is the full list; `getRegion(key)` looks one up.
|
|
33
|
+
|
|
34
|
+
## Core (no React)
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createPainSelection, serializeReport, deserializeReport } from "@broberg/bodymap";
|
|
38
|
+
|
|
39
|
+
const sel = createPainSelection();
|
|
40
|
+
sel.set("shoulder_left", 7, "jagende");
|
|
41
|
+
sel.set("lumbar", 4);
|
|
42
|
+
sel.getReport(); // validated PainReport
|
|
43
|
+
|
|
44
|
+
const wire = serializeReport(sel.getReport(), { view: "front" });
|
|
45
|
+
// → { schema: "bodymap/v1", view: "front",
|
|
46
|
+
// points: [{ region: "SHOULDER", side: "left", intensity: 7, quality: "jagende" }, …] }
|
|
47
|
+
|
|
48
|
+
deserializeReport(wire); // bodymap/v1 → internal PainReport
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`serializeReport` maps each region key to its side-less clinical **code** + side, so
|
|
52
|
+
the report is portable across the web components and the native mobile apps.
|
|
53
|
+
`painReportSchema` (zod) is exported for validating untrusted input.
|
|
54
|
+
|
|
55
|
+
## 2D renderer (React)
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { BodyMap } from "@broberg/bodymap/react";
|
|
59
|
+
|
|
60
|
+
<BodyMap
|
|
61
|
+
defaultValue={[]} // or `value` for a controlled component
|
|
62
|
+
onChange={(report) => save(report)} // full validated PainReport on every change
|
|
63
|
+
config={{ groin: { visible: false } }} // per-app region toggles (optional)
|
|
64
|
+
/>;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Click a region → pick intensity (0–10) and quality → the region fills by intensity
|
|
68
|
+
and `onChange` fires. No native form controls; every control carries a
|
|
69
|
+
`data-testid` (`bodymap-region-<key>`) for E2E/visual testing. `react` is an
|
|
70
|
+
optional peer dependency — the core works without it.
|
|
71
|
+
|
|
72
|
+
### Per-app region config
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
type RegionConfig = Record<string, { visible?: boolean; selectable?: boolean }>;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
An absent key is visible + selectable. Hidden ⇒ never selectable. `resolveRegions`
|
|
79
|
+
and `isSelectable` apply the config for custom renderers.
|
|
80
|
+
|
|
81
|
+
## Colour control — `BodymapPalette`
|
|
82
|
+
|
|
83
|
+
Both renderers theme off one palette (consumer-defined):
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { defaultPalette, heatFor, baseColorFor, type BodymapPalette } from "@broberg/bodymap";
|
|
87
|
+
|
|
88
|
+
const palette: BodymapPalette = {
|
|
89
|
+
body: "#c8ccdd",
|
|
90
|
+
hover: "#5CC4B7",
|
|
91
|
+
selected: "#141969",
|
|
92
|
+
heat: { low: "#FFE049", mid: "#F09A3E", high: "#D61C64" },
|
|
93
|
+
regions: { chest: "#e6e9f2" }, // optional per-region base colours
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
heatFor(8, palette); // → "#D61C64" (intensity → heat colour)
|
|
97
|
+
baseColorFor("chest", palette); // → "#e6e9f2" (per-region override, else body)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Roadmap
|
|
101
|
+
|
|
102
|
+
- 2D **back view** (LUMBAR / THORA / HIP posterior) — `0.1.x`
|
|
103
|
+
- Rotatable **3D** renderer (vanilla Three.js, realistic Blender Studio CC0 body,
|
|
104
|
+
click-to-colour by pain) — `0.1.x`
|
|
105
|
+
- Preact adapter
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT · part of the [`@broberg/*`](https://discovery.broberg.ai) shared inventory.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var REGIONS = [
|
|
7
|
+
// axis / centre-line (serialised side "center")
|
|
8
|
+
{ key: "head", label: "Hoved", code: "HEAD" },
|
|
9
|
+
{ key: "neck", label: "Nakke", code: "NECK" },
|
|
10
|
+
{ key: "chest", label: "Bryst", code: "CHEST" },
|
|
11
|
+
{ key: "thora", label: "\xD8vre ryg (thorakal)", code: "THORA" },
|
|
12
|
+
{ key: "lumbar", label: "L\xE6nd (lumbal)", code: "LUMBAR" },
|
|
13
|
+
{ key: "groin", label: "Lyske", code: "GROIN" },
|
|
14
|
+
// paired limbs / sides (L / R)
|
|
15
|
+
{ key: "shoulder_left", label: "Skulder, venstre", code: "SHOULDER", side: "left" },
|
|
16
|
+
{ key: "shoulder_right", label: "Skulder, h\xF8jre", code: "SHOULDER", side: "right" },
|
|
17
|
+
{ key: "uarm_left", label: "Overarm, venstre", code: "UARM", side: "left" },
|
|
18
|
+
{ key: "uarm_right", label: "Overarm, h\xF8jre", code: "UARM", side: "right" },
|
|
19
|
+
{ key: "elbow_left", label: "Albue, venstre", code: "ELBOW", side: "left" },
|
|
20
|
+
{ key: "elbow_right", label: "Albue, h\xF8jre", code: "ELBOW", side: "right" },
|
|
21
|
+
{ key: "farm_left", label: "Underarm, venstre", code: "FARM", side: "left" },
|
|
22
|
+
{ key: "farm_right", label: "Underarm, h\xF8jre", code: "FARM", side: "right" },
|
|
23
|
+
{ key: "wrist_left", label: "H\xE5ndled, venstre", code: "WRIST", side: "left" },
|
|
24
|
+
{ key: "wrist_right", label: "H\xE5ndled, h\xF8jre", code: "WRIST", side: "right" },
|
|
25
|
+
{ key: "hand_left", label: "H\xE5nd, venstre", code: "HAND", side: "left" },
|
|
26
|
+
{ key: "hand_right", label: "H\xE5nd, h\xF8jre", code: "HAND", side: "right" },
|
|
27
|
+
{ key: "hip_left", label: "Hofte, venstre", code: "HIP", side: "left" },
|
|
28
|
+
{ key: "hip_right", label: "Hofte, h\xF8jre", code: "HIP", side: "right" },
|
|
29
|
+
{ key: "thigh_left", label: "L\xE5r, venstre", code: "THIGH", side: "left" },
|
|
30
|
+
{ key: "thigh_right", label: "L\xE5r, h\xF8jre", code: "THIGH", side: "right" },
|
|
31
|
+
{ key: "knee_left", label: "Kn\xE6, venstre", code: "KNEE", side: "left" },
|
|
32
|
+
{ key: "knee_right", label: "Kn\xE6, h\xF8jre", code: "KNEE", side: "right" },
|
|
33
|
+
{ key: "lowleg_left", label: "Underben, venstre", code: "LOWLEG", side: "left" },
|
|
34
|
+
{ key: "lowleg_right", label: "Underben, h\xF8jre", code: "LOWLEG", side: "right" },
|
|
35
|
+
{ key: "ankle_left", label: "Ankel, venstre", code: "ANKLE", side: "left" },
|
|
36
|
+
{ key: "ankle_right", label: "Ankel, h\xF8jre", code: "ANKLE", side: "right" },
|
|
37
|
+
{ key: "foot_left", label: "Fod, venstre", code: "FOOT", side: "left" },
|
|
38
|
+
{ key: "foot_right", label: "Fod, h\xF8jre", code: "FOOT", side: "right" }
|
|
39
|
+
];
|
|
40
|
+
var REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));
|
|
41
|
+
var REGION_KEYS = REGIONS.map((r) => r.key);
|
|
42
|
+
function getRegion(key) {
|
|
43
|
+
return REGIONS.find((r) => r.key === key);
|
|
44
|
+
}
|
|
45
|
+
var PAIN_TYPES = ["stikkende", "dump", "konstant", "jagende"];
|
|
46
|
+
var painPointSchema = zod.z.object({
|
|
47
|
+
region: zod.z.string().refine((k) => REGION_KEY_SET.has(k), { message: "unknown region" }),
|
|
48
|
+
intensity: zod.z.number().int().min(0).max(10),
|
|
49
|
+
type: zod.z.enum(PAIN_TYPES).optional(),
|
|
50
|
+
timestamp: zod.z.string()
|
|
51
|
+
});
|
|
52
|
+
var painReportSchema = zod.z.array(painPointSchema);
|
|
53
|
+
function createPainSelection(initial = [], opts = {}) {
|
|
54
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
55
|
+
const map = /* @__PURE__ */ new Map();
|
|
56
|
+
for (const p of initial) {
|
|
57
|
+
const v = painPointSchema.parse(p);
|
|
58
|
+
map.set(v.region, v);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
set(region, intensity, type) {
|
|
62
|
+
const point = painPointSchema.parse({ region, intensity, type, timestamp: now() });
|
|
63
|
+
map.set(point.region, point);
|
|
64
|
+
return point;
|
|
65
|
+
},
|
|
66
|
+
remove: (region) => map.delete(region),
|
|
67
|
+
get: (region) => map.get(region),
|
|
68
|
+
has: (region) => map.has(region),
|
|
69
|
+
clear: () => map.clear(),
|
|
70
|
+
getReport: () => painReportSchema.parse(Array.from(map.values()))
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function resolveRegions(config = {}) {
|
|
74
|
+
return REGIONS.filter((r) => config[r.key]?.visible ?? true);
|
|
75
|
+
}
|
|
76
|
+
function isSelectable(key, config = {}) {
|
|
77
|
+
const s = config[key];
|
|
78
|
+
if (s?.visible === false) return false;
|
|
79
|
+
return s?.selectable ?? true;
|
|
80
|
+
}
|
|
81
|
+
var defaultPalette = {
|
|
82
|
+
body: "#d2d7de",
|
|
83
|
+
hover: "#8fd0cd",
|
|
84
|
+
selected: "#5cc4b7",
|
|
85
|
+
heat: { low: "#fcd34d", mid: "#fb923c", high: "#ef4444" }
|
|
86
|
+
};
|
|
87
|
+
function heatFor(intensity, palette = defaultPalette) {
|
|
88
|
+
return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;
|
|
89
|
+
}
|
|
90
|
+
function baseColorFor(regionKey, palette = defaultPalette) {
|
|
91
|
+
return palette.regions?.[regionKey] ?? palette.body;
|
|
92
|
+
}
|
|
93
|
+
var bodymapReportV1Schema = zod.z.object({
|
|
94
|
+
schema: zod.z.literal("bodymap/v1"),
|
|
95
|
+
view: zod.z.enum(["front", "back", "left", "right"]),
|
|
96
|
+
points: zod.z.array(
|
|
97
|
+
zod.z.object({
|
|
98
|
+
region: zod.z.string(),
|
|
99
|
+
side: zod.z.enum(["left", "right", "center"]),
|
|
100
|
+
intensity: zod.z.number().int().min(0).max(10),
|
|
101
|
+
quality: zod.z.enum(PAIN_TYPES).optional()
|
|
102
|
+
})
|
|
103
|
+
)
|
|
104
|
+
});
|
|
105
|
+
function serializeReport(report, opts = {}) {
|
|
106
|
+
return {
|
|
107
|
+
schema: "bodymap/v1",
|
|
108
|
+
view: opts.view ?? "front",
|
|
109
|
+
points: report.map((p) => {
|
|
110
|
+
const r = getRegion(p.region);
|
|
111
|
+
return {
|
|
112
|
+
region: r?.code ?? p.region,
|
|
113
|
+
side: r?.side ?? "center",
|
|
114
|
+
intensity: p.intensity,
|
|
115
|
+
quality: p.type
|
|
116
|
+
};
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function deserializeReport(env, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
121
|
+
const parsed = bodymapReportV1Schema.parse(env);
|
|
122
|
+
const byCodeSide = new Map(
|
|
123
|
+
REGIONS.map((r) => [`${r.code}|${r.side ?? "center"}`, r.key])
|
|
124
|
+
);
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const sp of parsed.points) {
|
|
127
|
+
const key = byCodeSide.get(`${sp.region}|${sp.side}`);
|
|
128
|
+
if (!key) continue;
|
|
129
|
+
out.push(
|
|
130
|
+
painPointSchema.parse({
|
|
131
|
+
region: key,
|
|
132
|
+
intensity: sp.intensity,
|
|
133
|
+
type: sp.quality,
|
|
134
|
+
timestamp: now()
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
exports.PAIN_TYPES = PAIN_TYPES;
|
|
142
|
+
exports.REGIONS = REGIONS;
|
|
143
|
+
exports.REGION_KEYS = REGION_KEYS;
|
|
144
|
+
exports.baseColorFor = baseColorFor;
|
|
145
|
+
exports.bodymapReportV1Schema = bodymapReportV1Schema;
|
|
146
|
+
exports.createPainSelection = createPainSelection;
|
|
147
|
+
exports.defaultPalette = defaultPalette;
|
|
148
|
+
exports.deserializeReport = deserializeReport;
|
|
149
|
+
exports.getRegion = getRegion;
|
|
150
|
+
exports.heatFor = heatFor;
|
|
151
|
+
exports.isSelectable = isSelectable;
|
|
152
|
+
exports.painPointSchema = painPointSchema;
|
|
153
|
+
exports.painReportSchema = painReportSchema;
|
|
154
|
+
exports.resolveRegions = resolveRegions;
|
|
155
|
+
exports.serializeReport = serializeReport;
|
|
156
|
+
//# sourceMappingURL=index.cjs.map
|
|
157
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["z"],"mappings":"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","file":"index.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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
type Side = "left" | "right";
|
|
4
|
+
interface BodyRegion {
|
|
5
|
+
/** Stable, unique identifier (snake_case) — the key used in a PainReport. */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Human label (Danish). */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Clinical short code (unique). */
|
|
10
|
+
code: string;
|
|
11
|
+
/** Body side, when the region is paired. */
|
|
12
|
+
side?: Side;
|
|
13
|
+
}
|
|
14
|
+
/** The canonical body regions — the AUTHORITATIVE fd-sundhed clinical taxonomy
|
|
15
|
+
* (docs/BODYMAP-TAKSONOMI.md, broberg-ai/fd-sundhed @360842f): 18 SIDE-LESS
|
|
16
|
+
* clinical codes + a separate `side` field (L/R on limbs, C=center on axis
|
|
17
|
+
* regions). The `key` is a unique per-side identifier; `code` is the side-less
|
|
18
|
+
* clinical code that goes on the bodymap/v1 wire. NOT an anatomical atlas —
|
|
19
|
+
* ~30 named surface regions for a pain-map. The 2D front renderer draws the
|
|
20
|
+
* front-visible subset; the 3D body (F052.6) drives them all. */
|
|
21
|
+
declare const REGIONS: readonly BodyRegion[];
|
|
22
|
+
declare const REGION_KEYS: readonly string[];
|
|
23
|
+
/** Look up a region by its key. */
|
|
24
|
+
declare function getRegion(key: string): BodyRegion | undefined;
|
|
25
|
+
declare const PAIN_TYPES: readonly ["stikkende", "dump", "konstant", "jagende"];
|
|
26
|
+
type PainType = (typeof PAIN_TYPES)[number];
|
|
27
|
+
/** One marked pain point. `region` MUST be a known region key; `intensity` is a
|
|
28
|
+
* 0-10 integer; `type` is optional but constrained; `timestamp` is an ISO string. */
|
|
29
|
+
declare const painPointSchema: z.ZodObject<{
|
|
30
|
+
region: z.ZodEffects<z.ZodString, string, string>;
|
|
31
|
+
intensity: z.ZodNumber;
|
|
32
|
+
type: z.ZodOptional<z.ZodEnum<["stikkende", "dump", "konstant", "jagende"]>>;
|
|
33
|
+
timestamp: z.ZodString;
|
|
34
|
+
}, "strip", z.ZodTypeAny, {
|
|
35
|
+
region: string;
|
|
36
|
+
intensity: number;
|
|
37
|
+
timestamp: string;
|
|
38
|
+
type?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
39
|
+
}, {
|
|
40
|
+
region: string;
|
|
41
|
+
intensity: number;
|
|
42
|
+
timestamp: string;
|
|
43
|
+
type?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
44
|
+
}>;
|
|
45
|
+
type PainPoint = z.infer<typeof painPointSchema>;
|
|
46
|
+
declare const painReportSchema: z.ZodArray<z.ZodObject<{
|
|
47
|
+
region: z.ZodEffects<z.ZodString, string, string>;
|
|
48
|
+
intensity: z.ZodNumber;
|
|
49
|
+
type: z.ZodOptional<z.ZodEnum<["stikkende", "dump", "konstant", "jagende"]>>;
|
|
50
|
+
timestamp: z.ZodString;
|
|
51
|
+
}, "strip", z.ZodTypeAny, {
|
|
52
|
+
region: string;
|
|
53
|
+
intensity: number;
|
|
54
|
+
timestamp: string;
|
|
55
|
+
type?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
56
|
+
}, {
|
|
57
|
+
region: string;
|
|
58
|
+
intensity: number;
|
|
59
|
+
timestamp: string;
|
|
60
|
+
type?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
61
|
+
}>, "many">;
|
|
62
|
+
type PainReport = PainPoint[];
|
|
63
|
+
interface PainSelection {
|
|
64
|
+
/** Mark (or update) pain on a region. One point per region — latest wins. */
|
|
65
|
+
set(region: string, intensity: number, type?: PainType): PainPoint;
|
|
66
|
+
remove(region: string): boolean;
|
|
67
|
+
get(region: string): PainPoint | undefined;
|
|
68
|
+
has(region: string): boolean;
|
|
69
|
+
clear(): void;
|
|
70
|
+
/** The current, validated PainReport. */
|
|
71
|
+
getReport(): PainReport;
|
|
72
|
+
}
|
|
73
|
+
interface PainSelectionOptions {
|
|
74
|
+
/** Injectable clock (ISO string) — defaults to `new Date().toISOString()`. */
|
|
75
|
+
now?: () => string;
|
|
76
|
+
}
|
|
77
|
+
/** Create a selection engine seeded with an optional report. Pure state — no
|
|
78
|
+
* DOM, no framework, no network. One point per region. */
|
|
79
|
+
declare function createPainSelection(initial?: PainReport, opts?: PainSelectionOptions): PainSelection;
|
|
80
|
+
interface RegionSetting {
|
|
81
|
+
/** Render this region at all. Default true. */
|
|
82
|
+
visible?: boolean;
|
|
83
|
+
/** Allow marking pain on this region. Default true. */
|
|
84
|
+
selectable?: boolean;
|
|
85
|
+
}
|
|
86
|
+
/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */
|
|
87
|
+
type RegionConfig = Record<string, RegionSetting>;
|
|
88
|
+
/** The regions an app should render, honouring `visible` (default true). */
|
|
89
|
+
declare function resolveRegions(config?: RegionConfig): BodyRegion[];
|
|
90
|
+
/** Whether a region may be marked. A hidden region is never selectable. */
|
|
91
|
+
declare function isSelectable(key: string, config?: RegionConfig): boolean;
|
|
92
|
+
/** Colour control for the body renderers. Consumers pass a palette to theme the
|
|
93
|
+
* body base colour, the hover + selected highlights, the pain-heat colours, and
|
|
94
|
+
* optional per-region base colours. All values are CSS/hex colour strings. */
|
|
95
|
+
interface BodymapPalette {
|
|
96
|
+
/** Base body colour (an unmarked region). */
|
|
97
|
+
body: string;
|
|
98
|
+
/** Region highlight on hover (before click). */
|
|
99
|
+
hover: string;
|
|
100
|
+
/** A region selected (clicked) but not yet given an intensity. */
|
|
101
|
+
selected: string;
|
|
102
|
+
/** Pain-intensity heat colours: low (0-3), mid (4-6), high (7-10). */
|
|
103
|
+
heat: {
|
|
104
|
+
low: string;
|
|
105
|
+
mid: string;
|
|
106
|
+
high: string;
|
|
107
|
+
};
|
|
108
|
+
/** Optional per-region base-colour overrides (region key → colour). */
|
|
109
|
+
regions?: Record<string, string>;
|
|
110
|
+
}
|
|
111
|
+
/** The fleet default palette. Override any field per consumer. */
|
|
112
|
+
declare const defaultPalette: BodymapPalette;
|
|
113
|
+
/** The heat colour for a pain intensity, honouring the palette. */
|
|
114
|
+
declare function heatFor(intensity: number, palette?: BodymapPalette): string;
|
|
115
|
+
/** The base colour for a region (a per-region override, else the body colour). */
|
|
116
|
+
declare function baseColorFor(regionKey: string, palette?: BodymapPalette): string;
|
|
117
|
+
type BodyView = "front" | "back" | "left" | "right";
|
|
118
|
+
/** Side in the serialized report — a midline region (no side) becomes "center". */
|
|
119
|
+
type SerializedSide = "left" | "right" | "center";
|
|
120
|
+
interface SerializedPainPoint {
|
|
121
|
+
/** Clinical region CODE (e.g. "LUMB"). */
|
|
122
|
+
region: string;
|
|
123
|
+
side: SerializedSide;
|
|
124
|
+
intensity: number;
|
|
125
|
+
quality?: PainType;
|
|
126
|
+
}
|
|
127
|
+
interface BodymapReportV1 {
|
|
128
|
+
schema: "bodymap/v1";
|
|
129
|
+
view: BodyView;
|
|
130
|
+
points: SerializedPainPoint[];
|
|
131
|
+
}
|
|
132
|
+
declare const bodymapReportV1Schema: z.ZodObject<{
|
|
133
|
+
schema: z.ZodLiteral<"bodymap/v1">;
|
|
134
|
+
view: z.ZodEnum<["front", "back", "left", "right"]>;
|
|
135
|
+
points: z.ZodArray<z.ZodObject<{
|
|
136
|
+
region: z.ZodString;
|
|
137
|
+
side: z.ZodEnum<["left", "right", "center"]>;
|
|
138
|
+
intensity: z.ZodNumber;
|
|
139
|
+
quality: z.ZodOptional<z.ZodEnum<["stikkende", "dump", "konstant", "jagende"]>>;
|
|
140
|
+
}, "strip", z.ZodTypeAny, {
|
|
141
|
+
side: "left" | "right" | "center";
|
|
142
|
+
region: string;
|
|
143
|
+
intensity: number;
|
|
144
|
+
quality?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
145
|
+
}, {
|
|
146
|
+
side: "left" | "right" | "center";
|
|
147
|
+
region: string;
|
|
148
|
+
intensity: number;
|
|
149
|
+
quality?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
150
|
+
}>, "many">;
|
|
151
|
+
}, "strip", z.ZodTypeAny, {
|
|
152
|
+
schema: "bodymap/v1";
|
|
153
|
+
view: "left" | "right" | "front" | "back";
|
|
154
|
+
points: {
|
|
155
|
+
side: "left" | "right" | "center";
|
|
156
|
+
region: string;
|
|
157
|
+
intensity: number;
|
|
158
|
+
quality?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
159
|
+
}[];
|
|
160
|
+
}, {
|
|
161
|
+
schema: "bodymap/v1";
|
|
162
|
+
view: "left" | "right" | "front" | "back";
|
|
163
|
+
points: {
|
|
164
|
+
side: "left" | "right" | "center";
|
|
165
|
+
region: string;
|
|
166
|
+
intensity: number;
|
|
167
|
+
quality?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
168
|
+
}[];
|
|
169
|
+
}>;
|
|
170
|
+
/** Serialize a PainReport to the shared `bodymap/v1` wire format. */
|
|
171
|
+
declare function serializeReport(report: PainReport, opts?: {
|
|
172
|
+
view?: BodyView;
|
|
173
|
+
}): BodymapReportV1;
|
|
174
|
+
/** Parse a `bodymap/v1` report back into an internal PainReport. Region CODE →
|
|
175
|
+
* key; a point whose code is unknown to this taxonomy is dropped. */
|
|
176
|
+
declare function deserializeReport(env: unknown, now?: () => string): PainReport;
|
|
177
|
+
|
|
178
|
+
export { type BodyRegion, type BodyView, type BodymapPalette, type BodymapReportV1, PAIN_TYPES, type PainPoint, type PainReport, type PainSelection, type PainSelectionOptions, type PainType, REGIONS, REGION_KEYS, type RegionConfig, type RegionSetting, type SerializedPainPoint, type SerializedSide, type Side, baseColorFor, bodymapReportV1Schema, createPainSelection, defaultPalette, deserializeReport, getRegion, heatFor, isSelectable, painPointSchema, painReportSchema, resolveRegions, serializeReport };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
type Side = "left" | "right";
|
|
4
|
+
interface BodyRegion {
|
|
5
|
+
/** Stable, unique identifier (snake_case) — the key used in a PainReport. */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Human label (Danish). */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Clinical short code (unique). */
|
|
10
|
+
code: string;
|
|
11
|
+
/** Body side, when the region is paired. */
|
|
12
|
+
side?: Side;
|
|
13
|
+
}
|
|
14
|
+
/** The canonical body regions — the AUTHORITATIVE fd-sundhed clinical taxonomy
|
|
15
|
+
* (docs/BODYMAP-TAKSONOMI.md, broberg-ai/fd-sundhed @360842f): 18 SIDE-LESS
|
|
16
|
+
* clinical codes + a separate `side` field (L/R on limbs, C=center on axis
|
|
17
|
+
* regions). The `key` is a unique per-side identifier; `code` is the side-less
|
|
18
|
+
* clinical code that goes on the bodymap/v1 wire. NOT an anatomical atlas —
|
|
19
|
+
* ~30 named surface regions for a pain-map. The 2D front renderer draws the
|
|
20
|
+
* front-visible subset; the 3D body (F052.6) drives them all. */
|
|
21
|
+
declare const REGIONS: readonly BodyRegion[];
|
|
22
|
+
declare const REGION_KEYS: readonly string[];
|
|
23
|
+
/** Look up a region by its key. */
|
|
24
|
+
declare function getRegion(key: string): BodyRegion | undefined;
|
|
25
|
+
declare const PAIN_TYPES: readonly ["stikkende", "dump", "konstant", "jagende"];
|
|
26
|
+
type PainType = (typeof PAIN_TYPES)[number];
|
|
27
|
+
/** One marked pain point. `region` MUST be a known region key; `intensity` is a
|
|
28
|
+
* 0-10 integer; `type` is optional but constrained; `timestamp` is an ISO string. */
|
|
29
|
+
declare const painPointSchema: z.ZodObject<{
|
|
30
|
+
region: z.ZodEffects<z.ZodString, string, string>;
|
|
31
|
+
intensity: z.ZodNumber;
|
|
32
|
+
type: z.ZodOptional<z.ZodEnum<["stikkende", "dump", "konstant", "jagende"]>>;
|
|
33
|
+
timestamp: z.ZodString;
|
|
34
|
+
}, "strip", z.ZodTypeAny, {
|
|
35
|
+
region: string;
|
|
36
|
+
intensity: number;
|
|
37
|
+
timestamp: string;
|
|
38
|
+
type?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
39
|
+
}, {
|
|
40
|
+
region: string;
|
|
41
|
+
intensity: number;
|
|
42
|
+
timestamp: string;
|
|
43
|
+
type?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
44
|
+
}>;
|
|
45
|
+
type PainPoint = z.infer<typeof painPointSchema>;
|
|
46
|
+
declare const painReportSchema: z.ZodArray<z.ZodObject<{
|
|
47
|
+
region: z.ZodEffects<z.ZodString, string, string>;
|
|
48
|
+
intensity: z.ZodNumber;
|
|
49
|
+
type: z.ZodOptional<z.ZodEnum<["stikkende", "dump", "konstant", "jagende"]>>;
|
|
50
|
+
timestamp: z.ZodString;
|
|
51
|
+
}, "strip", z.ZodTypeAny, {
|
|
52
|
+
region: string;
|
|
53
|
+
intensity: number;
|
|
54
|
+
timestamp: string;
|
|
55
|
+
type?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
56
|
+
}, {
|
|
57
|
+
region: string;
|
|
58
|
+
intensity: number;
|
|
59
|
+
timestamp: string;
|
|
60
|
+
type?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
61
|
+
}>, "many">;
|
|
62
|
+
type PainReport = PainPoint[];
|
|
63
|
+
interface PainSelection {
|
|
64
|
+
/** Mark (or update) pain on a region. One point per region — latest wins. */
|
|
65
|
+
set(region: string, intensity: number, type?: PainType): PainPoint;
|
|
66
|
+
remove(region: string): boolean;
|
|
67
|
+
get(region: string): PainPoint | undefined;
|
|
68
|
+
has(region: string): boolean;
|
|
69
|
+
clear(): void;
|
|
70
|
+
/** The current, validated PainReport. */
|
|
71
|
+
getReport(): PainReport;
|
|
72
|
+
}
|
|
73
|
+
interface PainSelectionOptions {
|
|
74
|
+
/** Injectable clock (ISO string) — defaults to `new Date().toISOString()`. */
|
|
75
|
+
now?: () => string;
|
|
76
|
+
}
|
|
77
|
+
/** Create a selection engine seeded with an optional report. Pure state — no
|
|
78
|
+
* DOM, no framework, no network. One point per region. */
|
|
79
|
+
declare function createPainSelection(initial?: PainReport, opts?: PainSelectionOptions): PainSelection;
|
|
80
|
+
interface RegionSetting {
|
|
81
|
+
/** Render this region at all. Default true. */
|
|
82
|
+
visible?: boolean;
|
|
83
|
+
/** Allow marking pain on this region. Default true. */
|
|
84
|
+
selectable?: boolean;
|
|
85
|
+
}
|
|
86
|
+
/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */
|
|
87
|
+
type RegionConfig = Record<string, RegionSetting>;
|
|
88
|
+
/** The regions an app should render, honouring `visible` (default true). */
|
|
89
|
+
declare function resolveRegions(config?: RegionConfig): BodyRegion[];
|
|
90
|
+
/** Whether a region may be marked. A hidden region is never selectable. */
|
|
91
|
+
declare function isSelectable(key: string, config?: RegionConfig): boolean;
|
|
92
|
+
/** Colour control for the body renderers. Consumers pass a palette to theme the
|
|
93
|
+
* body base colour, the hover + selected highlights, the pain-heat colours, and
|
|
94
|
+
* optional per-region base colours. All values are CSS/hex colour strings. */
|
|
95
|
+
interface BodymapPalette {
|
|
96
|
+
/** Base body colour (an unmarked region). */
|
|
97
|
+
body: string;
|
|
98
|
+
/** Region highlight on hover (before click). */
|
|
99
|
+
hover: string;
|
|
100
|
+
/** A region selected (clicked) but not yet given an intensity. */
|
|
101
|
+
selected: string;
|
|
102
|
+
/** Pain-intensity heat colours: low (0-3), mid (4-6), high (7-10). */
|
|
103
|
+
heat: {
|
|
104
|
+
low: string;
|
|
105
|
+
mid: string;
|
|
106
|
+
high: string;
|
|
107
|
+
};
|
|
108
|
+
/** Optional per-region base-colour overrides (region key → colour). */
|
|
109
|
+
regions?: Record<string, string>;
|
|
110
|
+
}
|
|
111
|
+
/** The fleet default palette. Override any field per consumer. */
|
|
112
|
+
declare const defaultPalette: BodymapPalette;
|
|
113
|
+
/** The heat colour for a pain intensity, honouring the palette. */
|
|
114
|
+
declare function heatFor(intensity: number, palette?: BodymapPalette): string;
|
|
115
|
+
/** The base colour for a region (a per-region override, else the body colour). */
|
|
116
|
+
declare function baseColorFor(regionKey: string, palette?: BodymapPalette): string;
|
|
117
|
+
type BodyView = "front" | "back" | "left" | "right";
|
|
118
|
+
/** Side in the serialized report — a midline region (no side) becomes "center". */
|
|
119
|
+
type SerializedSide = "left" | "right" | "center";
|
|
120
|
+
interface SerializedPainPoint {
|
|
121
|
+
/** Clinical region CODE (e.g. "LUMB"). */
|
|
122
|
+
region: string;
|
|
123
|
+
side: SerializedSide;
|
|
124
|
+
intensity: number;
|
|
125
|
+
quality?: PainType;
|
|
126
|
+
}
|
|
127
|
+
interface BodymapReportV1 {
|
|
128
|
+
schema: "bodymap/v1";
|
|
129
|
+
view: BodyView;
|
|
130
|
+
points: SerializedPainPoint[];
|
|
131
|
+
}
|
|
132
|
+
declare const bodymapReportV1Schema: z.ZodObject<{
|
|
133
|
+
schema: z.ZodLiteral<"bodymap/v1">;
|
|
134
|
+
view: z.ZodEnum<["front", "back", "left", "right"]>;
|
|
135
|
+
points: z.ZodArray<z.ZodObject<{
|
|
136
|
+
region: z.ZodString;
|
|
137
|
+
side: z.ZodEnum<["left", "right", "center"]>;
|
|
138
|
+
intensity: z.ZodNumber;
|
|
139
|
+
quality: z.ZodOptional<z.ZodEnum<["stikkende", "dump", "konstant", "jagende"]>>;
|
|
140
|
+
}, "strip", z.ZodTypeAny, {
|
|
141
|
+
side: "left" | "right" | "center";
|
|
142
|
+
region: string;
|
|
143
|
+
intensity: number;
|
|
144
|
+
quality?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
145
|
+
}, {
|
|
146
|
+
side: "left" | "right" | "center";
|
|
147
|
+
region: string;
|
|
148
|
+
intensity: number;
|
|
149
|
+
quality?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
150
|
+
}>, "many">;
|
|
151
|
+
}, "strip", z.ZodTypeAny, {
|
|
152
|
+
schema: "bodymap/v1";
|
|
153
|
+
view: "left" | "right" | "front" | "back";
|
|
154
|
+
points: {
|
|
155
|
+
side: "left" | "right" | "center";
|
|
156
|
+
region: string;
|
|
157
|
+
intensity: number;
|
|
158
|
+
quality?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
159
|
+
}[];
|
|
160
|
+
}, {
|
|
161
|
+
schema: "bodymap/v1";
|
|
162
|
+
view: "left" | "right" | "front" | "back";
|
|
163
|
+
points: {
|
|
164
|
+
side: "left" | "right" | "center";
|
|
165
|
+
region: string;
|
|
166
|
+
intensity: number;
|
|
167
|
+
quality?: "stikkende" | "dump" | "konstant" | "jagende" | undefined;
|
|
168
|
+
}[];
|
|
169
|
+
}>;
|
|
170
|
+
/** Serialize a PainReport to the shared `bodymap/v1` wire format. */
|
|
171
|
+
declare function serializeReport(report: PainReport, opts?: {
|
|
172
|
+
view?: BodyView;
|
|
173
|
+
}): BodymapReportV1;
|
|
174
|
+
/** Parse a `bodymap/v1` report back into an internal PainReport. Region CODE →
|
|
175
|
+
* key; a point whose code is unknown to this taxonomy is dropped. */
|
|
176
|
+
declare function deserializeReport(env: unknown, now?: () => string): PainReport;
|
|
177
|
+
|
|
178
|
+
export { type BodyRegion, type BodyView, type BodymapPalette, type BodymapReportV1, PAIN_TYPES, type PainPoint, type PainReport, type PainSelection, type PainSelectionOptions, type PainType, REGIONS, REGION_KEYS, type RegionConfig, type RegionSetting, type SerializedPainPoint, type SerializedSide, type Side, baseColorFor, bodymapReportV1Schema, createPainSelection, defaultPalette, deserializeReport, getRegion, heatFor, isSelectable, painPointSchema, painReportSchema, resolveRegions, serializeReport };
|