@broberg/bodymap 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,223 @@
1
+ import { VNode } from 'preact';
2
+ import { z } from 'zod';
3
+
4
+ /** One marked pain point. `region` MUST be a known region key; `intensity` is a
5
+ * 0-10 integer; `type` is optional but constrained; `timestamp` is an ISO string. */
6
+ declare const painPointSchema: z.ZodObject<{
7
+ region: z.ZodString;
8
+ intensity: z.ZodNumber;
9
+ type: z.ZodOptional<z.ZodEnum<{
10
+ stikkende: "stikkende";
11
+ dump: "dump";
12
+ konstant: "konstant";
13
+ jagende: "jagende";
14
+ }>>;
15
+ timestamp: z.ZodString;
16
+ }, z.core.$strip>;
17
+ type PainPoint = z.infer<typeof painPointSchema>;
18
+ type PainReport = PainPoint[];
19
+ interface RegionSetting {
20
+ /** Render this region at all. Default true. */
21
+ visible?: boolean;
22
+ /** Allow marking pain on this region. Default true. */
23
+ selectable?: boolean;
24
+ }
25
+ /** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */
26
+ type RegionConfig = Record<string, RegionSetting>;
27
+ /**
28
+ * What a pick on a region should DO (F052.20).
29
+ *
30
+ * Lives in the core because the 2D and 3D renderers share no click code, and a
31
+ * rule written twice is a rule that drifts. This repo measured the cost of that
32
+ * twice on 2026-08-28 alone: a fix applied to one half of a pair, and a sibling
33
+ * branch that carried the same defect with no test on it.
34
+ *
35
+ * "clear" the region is already marked → picking it again removes the mark
36
+ * "select" unmarked → open it for marking
37
+ * "ignore" not selectable (read-only or config) → nothing happens
38
+ *
39
+ * Three outcomes, not a boolean: "nothing happened because it is locked" and
40
+ * "nothing happened because we removed the mark" must never look alike to a
41
+ * caller.
42
+ */
43
+ type PickOutcome = "clear" | "select" | "ignore";
44
+ /**
45
+ * What a pick did, handed to the consuming app so it can make a sound or a buzz.
46
+ *
47
+ * The outcome is the one `decidePick` ACTUALLY returned, never the intent to tap
48
+ * — so a tap on a locked region can not announce itself as a removal, and a tap
49
+ * the pan/pinch guard swallowed emits nothing at all (it never gets here).
50
+ *
51
+ * It deliberately reuses `PickOutcome` rather than introducing a second
52
+ * three-word vocabulary. Two enums meaning the same thing is a drift bug waiting
53
+ * for the first person who adds a fourth outcome to only one of them.
54
+ */
55
+ interface FeedbackSignal {
56
+ outcome: PickOutcome;
57
+ /** The region key that was picked. */
58
+ region: string;
59
+ }
60
+ type FeedbackFn = (signal: FeedbackSignal) => void;
61
+ /** Colour control for the body renderers. Consumers pass a palette to theme the
62
+ * body base colour, the hover + selected highlights, the pain-heat colours, and
63
+ * optional per-region base colours. All values are CSS/hex colour strings. */
64
+ interface BodymapPalette {
65
+ /** Base body colour (an unmarked region). */
66
+ body: string;
67
+ /** Region highlight on hover (before click). */
68
+ hover: string;
69
+ /** A region selected (clicked) but not yet given an intensity. */
70
+ selected: string;
71
+ /** Pain-intensity heat colours: low (0-3), mid (4-6), high (7-10). */
72
+ heat: {
73
+ low: string;
74
+ mid: string;
75
+ high: string;
76
+ };
77
+ /** Optional per-region base-colour overrides (region key → colour). */
78
+ regions?: Record<string, string>;
79
+ /**
80
+ * Optional panel-chrome colours (the selection panel, labels, hint box) —
81
+ * NOT the body itself. All optional; anything omitted falls back to
82
+ * {@link defaultUi}. A palette that only themed the body was half a palette:
83
+ * a consumer passing brand colours still got hardcoded chrome. (F052.19)
84
+ */
85
+ ui?: BodymapUiColors;
86
+ }
87
+ /** Panel-chrome colours. Every default is WCAG-AA (>=4.5:1) on its own background. */
88
+ interface BodymapUiColors {
89
+ /** Primary text (headings, values). */
90
+ text?: string;
91
+ /** Secondary text — section labels, the empty-state hint. */
92
+ mutedText?: string;
93
+ /** Panel background — the card, its buttons, the empty hint. */
94
+ panelBg?: string;
95
+ /**
96
+ * THE STAGE: the fullscreen backdrop, the canvas frame, and the three.js
97
+ * scene behind the body. A different surface from `panelBg` with an opposite
98
+ * need — see F052.29, where one field paints both and no value is right for
99
+ * either.
100
+ *
101
+ * 3D only. The 2D renderer has no canvas and no fullscreen, so it ignores
102
+ * this; it is declared here because a stage is a shared idea, not because
103
+ * every renderer has one.
104
+ *
105
+ * Defaults to {@link STAGE_BG}. Pass `'#fff'` for the pre-0.8.0 white
106
+ * fullscreen backdrop.
107
+ */
108
+ stageBg?: string;
109
+ /** Panel + control borders. */
110
+ border?: string;
111
+ /** Background behind the region-code badge. */
112
+ badgeBg?: string;
113
+ /** The destructive action (remove a marked region). */
114
+ danger?: string;
115
+ /** Border on the destructive control. Its TEXT was already themeable and its
116
+ * border was not — half a control. (F052.30) */
117
+ dangerBorder?: string;
118
+ /**
119
+ * THE ACTIVE CONTROL colour — the chosen intensity, the active sex toggle.
120
+ *
121
+ * NOT `palette.selected`. That is the colour of a MARK ON SKIN; this is the
122
+ * colour of an active button. Collapsing them would stop a consumer having a
123
+ * teal mark and a navy button, which is a normal thing to want. (F052.30)
124
+ */
125
+ accent?: string;
126
+ /** Text ON the accent. Check it against your accent — see the README. */
127
+ accentText?: string;
128
+ }
129
+
130
+ type BodyView2D = "front" | "back";
131
+ interface BodyMapLabels {
132
+ /** Region key → display name. */
133
+ regions: Record<string, string>;
134
+ /** PainType → display word (e.g. "stikkende" → "Stabbing"). */
135
+ qualities: Record<string, string>;
136
+ intensity: string;
137
+ quality: string;
138
+ remove: string;
139
+ /** Close the picker without marking anything (F052.26). */
140
+ close: string;
141
+ front: string;
142
+ back: string;
143
+ empty: string;
144
+ /** Compare view (F052.13). */
145
+ before: string;
146
+ after: string;
147
+ noChange: string;
148
+ change: {
149
+ new: string;
150
+ resolved: string;
151
+ improved: string;
152
+ worse: string;
153
+ };
154
+ /** Accessible name for a marked region. */
155
+ ariaMarked: (name: string, intensity: number, quality?: string) => string;
156
+ /** Accessible name for an unmarked, selectable region. */
157
+ ariaUnmarked: (name: string) => string;
158
+ svgLabel: string;
159
+ viewLabel: string;
160
+ zoomLabel: string;
161
+ zoomIn: string;
162
+ zoomOut: string;
163
+ zoomReset: string;
164
+ }
165
+ type BodyMapLocale = "da" | "en";
166
+ interface BodyMapProps {
167
+ /** Controlled report. Omit for an uncontrolled component (use defaultValue). */
168
+ value?: PainReport;
169
+ /** Initial report when uncontrolled. */
170
+ defaultValue?: PainReport;
171
+ /** Fired with the full validated PainReport on every change. */
172
+ onChange?: (report: PainReport) => void;
173
+ /** Per-app region config (visible / selectable). */
174
+ config?: RegionConfig;
175
+ /** Initial body view (front / back). Default "front". */
176
+ defaultView?: BodyView2D;
177
+ /** Fired when the user toggles the front/back view — pass it to
178
+ * serializeReport({ view }) so the wire report carries the active view. */
179
+ onViewChange?: (view: BodyView2D) => void;
180
+ /** Colour theme (F052.10). Overrides the default rainbow: body/hover/selected/
181
+ * heat + optional per-region base colours. Without it, the rainbow is used
182
+ * (themeable via --bmap-body / --bmap-region-<key> / --bmap-heat-* CSS vars). */
183
+ palette?: BodymapPalette;
184
+ /** UI language (F052.12). "da" (default) or "en". */
185
+ locale?: BodyMapLocale;
186
+ /** Override individual UI strings (region names, headings, quality words, …).
187
+ * Merged over the locale set — region CODE + wire format are never affected. */
188
+ labels?: Partial<BodyMapLabels>;
189
+ /** Display-only mode (F052.9): render a saved report coloured by intensity
190
+ * with NO picker and no interactivity. For journals, PDFs, clinician views. */
191
+ readOnly?: boolean;
192
+ /** Fired after every pick with what ACTUALLY happened (F052.22): "select",
193
+ * "clear" or "ignore". Wire it to a sound (`@broberg/soundkit`) or to native
194
+ * haptics (Capacitor `Haptics.impact()` — the only route to a real buzz on an
195
+ * iPhone, where web vibration does not exist). A pick swallowed by the
196
+ * pan/pinch guard fires nothing at all. */
197
+ onFeedback?: FeedbackFn;
198
+ /** Web vibration on select/clear. Default true; silently inert where
199
+ * `navigator.vibrate` is absent (every browser on iPhone, most desktops).
200
+ * Set false to keep the signal but drop the buzz. */
201
+ haptics?: boolean;
202
+ className?: string;
203
+ }
204
+ interface BodyMapCompareProps {
205
+ /** The earlier report. */
206
+ before: PainReport;
207
+ /** The later report. */
208
+ after: PainReport;
209
+ palette?: BodymapPalette;
210
+ locale?: BodyMapLocale;
211
+ labels?: Partial<BodyMapLabels>;
212
+ defaultView?: BodyView2D;
213
+ className?: string;
214
+ }
215
+
216
+ /** 2D SVG body pain-map. Click/tap a region → set intensity + quality → PainReport. */
217
+ declare function BodyMap(props: BodyMapProps): VNode;
218
+ /** Read-only before/after view: two bodies side by side + a per-region change list. */
219
+ declare function BodyMapCompare(props: BodyMapCompareProps): VNode;
220
+ declare const LABELS_DA: BodyMapLabels;
221
+ declare const LABELS_EN: BodyMapLabels;
222
+
223
+ export { BodyMap, BodyMapCompare, type BodyMapCompareProps, type BodyMapLabels, type BodyMapLocale, type BodyMapProps, type BodyView2D, LABELS_DA, LABELS_EN };