@broberg/bodymap 0.3.0 → 0.4.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 +75 -0
- package/dist/index.cjs +22 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -1
- package/dist/index.d.ts +70 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +32 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +45 -1
- package/dist/react.d.ts +45 -1
- package/dist/react.js +32 -1
- package/dist/react.js.map +1 -1
- package/dist/three.cjs +22 -0
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +43 -0
- package/dist/three.d.ts +43 -0
- package/dist/three.js +22 -0
- package/dist/three.js.map +1 -1
- package/package.json +1 -1
package/dist/three.d.cts
CHANGED
|
@@ -31,6 +31,40 @@ interface RegionSetting {
|
|
|
31
31
|
}
|
|
32
32
|
/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */
|
|
33
33
|
type RegionConfig = Record<string, RegionSetting>;
|
|
34
|
+
/**
|
|
35
|
+
* What a pick on a region should DO (F052.20).
|
|
36
|
+
*
|
|
37
|
+
* Lives in the core because the 2D and 3D renderers share no click code, and a
|
|
38
|
+
* rule written twice is a rule that drifts. This repo measured the cost of that
|
|
39
|
+
* twice on 2026-08-28 alone: a fix applied to one half of a pair, and a sibling
|
|
40
|
+
* branch that carried the same defect with no test on it.
|
|
41
|
+
*
|
|
42
|
+
* "clear" the region is already marked → picking it again removes the mark
|
|
43
|
+
* "select" unmarked → open it for marking
|
|
44
|
+
* "ignore" not selectable (read-only or config) → nothing happens
|
|
45
|
+
*
|
|
46
|
+
* Three outcomes, not a boolean: "nothing happened because it is locked" and
|
|
47
|
+
* "nothing happened because we removed the mark" must never look alike to a
|
|
48
|
+
* caller.
|
|
49
|
+
*/
|
|
50
|
+
type PickOutcome = "clear" | "select" | "ignore";
|
|
51
|
+
/**
|
|
52
|
+
* What a pick did, handed to the consuming app so it can make a sound or a buzz.
|
|
53
|
+
*
|
|
54
|
+
* The outcome is the one `decidePick` ACTUALLY returned, never the intent to tap
|
|
55
|
+
* — so a tap on a locked region can not announce itself as a removal, and a tap
|
|
56
|
+
* the pan/pinch guard swallowed emits nothing at all (it never gets here).
|
|
57
|
+
*
|
|
58
|
+
* It deliberately reuses `PickOutcome` rather than introducing a second
|
|
59
|
+
* three-word vocabulary. Two enums meaning the same thing is a drift bug waiting
|
|
60
|
+
* for the first person who adds a fourth outcome to only one of them.
|
|
61
|
+
*/
|
|
62
|
+
interface FeedbackSignal {
|
|
63
|
+
outcome: PickOutcome;
|
|
64
|
+
/** The region key that was picked. */
|
|
65
|
+
region: string;
|
|
66
|
+
}
|
|
67
|
+
type FeedbackFn = (signal: FeedbackSignal) => void;
|
|
34
68
|
/** Colour control for the body renderers. Consumers pass a palette to theme the
|
|
35
69
|
* body base colour, the hover + selected highlights, the pain-heat colours, and
|
|
36
70
|
* optional per-region base colours. All values are CSS/hex colour strings. */
|
|
@@ -182,6 +216,15 @@ interface BodyMap3DProps {
|
|
|
182
216
|
* (default true). Set false for a patient/employee-facing flow where the code is
|
|
183
217
|
* internal jargon and the readable region name is enough. */
|
|
184
218
|
showRegionCode?: boolean;
|
|
219
|
+
/** Fired after every pick with what ACTUALLY happened (F052.22): "select",
|
|
220
|
+
* "clear" or "ignore". Wire it to a sound (`@broberg/soundkit`) or to native
|
|
221
|
+
* haptics (Capacitor `Haptics.impact()` — the only route to a real buzz on an
|
|
222
|
+
* iPhone, where web vibration does not exist). */
|
|
223
|
+
onFeedback?: FeedbackFn;
|
|
224
|
+
/** Web vibration on select/clear. Default true; silently inert where
|
|
225
|
+
* `navigator.vibrate` is absent (every browser on iPhone, most desktops).
|
|
226
|
+
* Set false to keep the signal but drop the buzz. */
|
|
227
|
+
haptics?: boolean;
|
|
185
228
|
className?: string;
|
|
186
229
|
}
|
|
187
230
|
declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
|
package/dist/three.d.ts
CHANGED
|
@@ -31,6 +31,40 @@ interface RegionSetting {
|
|
|
31
31
|
}
|
|
32
32
|
/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */
|
|
33
33
|
type RegionConfig = Record<string, RegionSetting>;
|
|
34
|
+
/**
|
|
35
|
+
* What a pick on a region should DO (F052.20).
|
|
36
|
+
*
|
|
37
|
+
* Lives in the core because the 2D and 3D renderers share no click code, and a
|
|
38
|
+
* rule written twice is a rule that drifts. This repo measured the cost of that
|
|
39
|
+
* twice on 2026-08-28 alone: a fix applied to one half of a pair, and a sibling
|
|
40
|
+
* branch that carried the same defect with no test on it.
|
|
41
|
+
*
|
|
42
|
+
* "clear" the region is already marked → picking it again removes the mark
|
|
43
|
+
* "select" unmarked → open it for marking
|
|
44
|
+
* "ignore" not selectable (read-only or config) → nothing happens
|
|
45
|
+
*
|
|
46
|
+
* Three outcomes, not a boolean: "nothing happened because it is locked" and
|
|
47
|
+
* "nothing happened because we removed the mark" must never look alike to a
|
|
48
|
+
* caller.
|
|
49
|
+
*/
|
|
50
|
+
type PickOutcome = "clear" | "select" | "ignore";
|
|
51
|
+
/**
|
|
52
|
+
* What a pick did, handed to the consuming app so it can make a sound or a buzz.
|
|
53
|
+
*
|
|
54
|
+
* The outcome is the one `decidePick` ACTUALLY returned, never the intent to tap
|
|
55
|
+
* — so a tap on a locked region can not announce itself as a removal, and a tap
|
|
56
|
+
* the pan/pinch guard swallowed emits nothing at all (it never gets here).
|
|
57
|
+
*
|
|
58
|
+
* It deliberately reuses `PickOutcome` rather than introducing a second
|
|
59
|
+
* three-word vocabulary. Two enums meaning the same thing is a drift bug waiting
|
|
60
|
+
* for the first person who adds a fourth outcome to only one of them.
|
|
61
|
+
*/
|
|
62
|
+
interface FeedbackSignal {
|
|
63
|
+
outcome: PickOutcome;
|
|
64
|
+
/** The region key that was picked. */
|
|
65
|
+
region: string;
|
|
66
|
+
}
|
|
67
|
+
type FeedbackFn = (signal: FeedbackSignal) => void;
|
|
34
68
|
/** Colour control for the body renderers. Consumers pass a palette to theme the
|
|
35
69
|
* body base colour, the hover + selected highlights, the pain-heat colours, and
|
|
36
70
|
* optional per-region base colours. All values are CSS/hex colour strings. */
|
|
@@ -182,6 +216,15 @@ interface BodyMap3DProps {
|
|
|
182
216
|
* (default true). Set false for a patient/employee-facing flow where the code is
|
|
183
217
|
* internal jargon and the readable region name is enough. */
|
|
184
218
|
showRegionCode?: boolean;
|
|
219
|
+
/** Fired after every pick with what ACTUALLY happened (F052.22): "select",
|
|
220
|
+
* "clear" or "ignore". Wire it to a sound (`@broberg/soundkit`) or to native
|
|
221
|
+
* haptics (Capacitor `Haptics.impact()` — the only route to a real buzz on an
|
|
222
|
+
* iPhone, where web vibration does not exist). */
|
|
223
|
+
onFeedback?: FeedbackFn;
|
|
224
|
+
/** Web vibration on select/clear. Default true; silently inert where
|
|
225
|
+
* `navigator.vibrate` is absent (every browser on iPhone, most desktops).
|
|
226
|
+
* Set false to keep the signal but drop the buzz. */
|
|
227
|
+
haptics?: boolean;
|
|
185
228
|
className?: string;
|
|
186
229
|
}
|
|
187
230
|
declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
|
package/dist/three.js
CHANGED
|
@@ -62,6 +62,25 @@ function isSelectable(key, config = {}) {
|
|
|
62
62
|
if (s?.visible === false) return false;
|
|
63
63
|
return s?.selectable ?? true;
|
|
64
64
|
}
|
|
65
|
+
var VIBRATION_PATTERNS = {
|
|
66
|
+
select: [12],
|
|
67
|
+
clear: [8, 40, 8],
|
|
68
|
+
ignore: []
|
|
69
|
+
};
|
|
70
|
+
function requestVibration(pattern, nav = globalThis.navigator) {
|
|
71
|
+
if (pattern.length === 0) return "skipped";
|
|
72
|
+
if (typeof nav?.vibrate !== "function") return "unsupported";
|
|
73
|
+
try {
|
|
74
|
+
return nav.vibrate([...pattern]) ? "requested" : "declined";
|
|
75
|
+
} catch {
|
|
76
|
+
return "declined";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function emitFeedback(outcome, region, opts = {}) {
|
|
80
|
+
opts.onFeedback?.({ outcome, region });
|
|
81
|
+
if (opts.haptics === false) return "skipped";
|
|
82
|
+
return requestVibration(VIBRATION_PATTERNS[outcome], opts.nav);
|
|
83
|
+
}
|
|
65
84
|
var defaultUi = {
|
|
66
85
|
text: "#1e293b",
|
|
67
86
|
mutedText: "#475569",
|
|
@@ -265,6 +284,8 @@ function BodyMap3D(props) {
|
|
|
265
284
|
autoRotate = true,
|
|
266
285
|
canvasHeight = "60vh",
|
|
267
286
|
showRegionCode = true,
|
|
287
|
+
onFeedback,
|
|
288
|
+
haptics,
|
|
268
289
|
className
|
|
269
290
|
} = props;
|
|
270
291
|
const chrome = uiColors(palette);
|
|
@@ -585,6 +606,7 @@ function BodyMap3D(props) {
|
|
|
585
606
|
};
|
|
586
607
|
pickRef.current = (k) => {
|
|
587
608
|
const what = decidePick(k, reportRef.current, configRef.current ?? {});
|
|
609
|
+
emitFeedback(what, k, { onFeedback, haptics });
|
|
588
610
|
if (what === "clear") removePain(k);
|
|
589
611
|
else if (what === "select") setSelected(k);
|
|
590
612
|
return what;
|