@broberg/bodymap 0.8.0 → 0.10.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 +59 -0
- package/dist/index.cjs +10 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +3 -3
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +13 -0
- package/dist/react.d.ts +13 -0
- package/dist/react.js +3 -3
- package/dist/react.js.map +1 -1
- package/dist/three.cjs +65 -16
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +42 -1
- package/dist/three.d.ts +42 -1
- package/dist/three.js +65 -16
- package/dist/three.js.map +1 -1
- package/package.json +1 -1
package/dist/three.d.cts
CHANGED
|
@@ -119,6 +119,19 @@ interface BodymapUiColors {
|
|
|
119
119
|
badgeBg?: string;
|
|
120
120
|
/** The destructive action (remove a marked region). */
|
|
121
121
|
danger?: string;
|
|
122
|
+
/** Border on the destructive control. Its TEXT was already themeable and its
|
|
123
|
+
* border was not — half a control. (F052.30) */
|
|
124
|
+
dangerBorder?: string;
|
|
125
|
+
/**
|
|
126
|
+
* THE ACTIVE CONTROL colour — the chosen intensity, the active sex toggle.
|
|
127
|
+
*
|
|
128
|
+
* NOT `palette.selected`. That is the colour of a MARK ON SKIN; this is the
|
|
129
|
+
* colour of an active button. Collapsing them would stop a consumer having a
|
|
130
|
+
* teal mark and a navy button, which is a normal thing to want. (F052.30)
|
|
131
|
+
*/
|
|
132
|
+
accent?: string;
|
|
133
|
+
/** Text ON the accent. Check it against your accent — see the README. */
|
|
134
|
+
accentText?: string;
|
|
122
135
|
}
|
|
123
136
|
type BodyView = "front" | "back" | "left" | "right";
|
|
124
137
|
/** Side in the serialized report — a midline region (no side) becomes "center". */
|
|
@@ -192,6 +205,16 @@ interface BodyMap3DUiLabels {
|
|
|
192
205
|
female: string;
|
|
193
206
|
hoverHint: string;
|
|
194
207
|
}
|
|
208
|
+
interface BodyMap3DHint {
|
|
209
|
+
/** Overrides the localized default. `ui.hoverHint` still works too. */
|
|
210
|
+
text?: string;
|
|
211
|
+
/**
|
|
212
|
+
* `after-canvas` (default) keeps today's flow position.
|
|
213
|
+
* `stage-bottom` pins it inside the fullscreen surface, above the home
|
|
214
|
+
* indicator — and falls back to the flow position when not fullscreen.
|
|
215
|
+
*/
|
|
216
|
+
placement?: "after-canvas" | "stage-bottom";
|
|
217
|
+
}
|
|
195
218
|
/**
|
|
196
219
|
* How much of the RUNNER-UP region bleeds into this vertex (F052.21).
|
|
197
220
|
*
|
|
@@ -252,6 +275,24 @@ interface BodyMap3DProps {
|
|
|
252
275
|
palette?: BodymapPalette;
|
|
253
276
|
locale?: BodyMapLocale;
|
|
254
277
|
labels?: Partial<BodyMapLabels>;
|
|
278
|
+
/**
|
|
279
|
+
* The help note under the body. F052.31.
|
|
280
|
+
*
|
|
281
|
+
* `false` renders NOTHING — no box, no border, no empty element. That path did
|
|
282
|
+
* not exist before: `ui.hoverHint = ""` produced an empty bordered box, which
|
|
283
|
+
* is worse than the sentence it replaced.
|
|
284
|
+
*
|
|
285
|
+
* `placement` is what fixes the defect rather than moving it. In the boxed
|
|
286
|
+
* layout the note belongs after the canvas; in FULLSCREEN that column wraps
|
|
287
|
+
* beneath a full-height canvas and the note lands past the bottom edge —
|
|
288
|
+
* measured by a consumer at 858-912 in an 852px window, i.e. entirely
|
|
289
|
+
* offscreen. `stage-bottom` anchors it inside the fullscreen surface instead,
|
|
290
|
+
* clear of the home indicator.
|
|
291
|
+
*
|
|
292
|
+
* A consumer cannot make that call in CSS without knowing our internal
|
|
293
|
+
* structure, which is exactly why two of them had to write wedges against it.
|
|
294
|
+
*/
|
|
295
|
+
hint?: false | BodyMap3DHint;
|
|
255
296
|
/** Override the 3D-only control strings (male/female/hoverHint). */
|
|
256
297
|
ui?: Partial<BodyMap3DUiLabels>;
|
|
257
298
|
defaultSex?: BodyMap3DSex;
|
|
@@ -312,4 +353,4 @@ interface BodyMap3DProps {
|
|
|
312
353
|
}
|
|
313
354
|
declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
|
|
314
355
|
|
|
315
|
-
export { BodyMap3D, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
|
|
356
|
+
export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
|
package/dist/three.d.ts
CHANGED
|
@@ -119,6 +119,19 @@ interface BodymapUiColors {
|
|
|
119
119
|
badgeBg?: string;
|
|
120
120
|
/** The destructive action (remove a marked region). */
|
|
121
121
|
danger?: string;
|
|
122
|
+
/** Border on the destructive control. Its TEXT was already themeable and its
|
|
123
|
+
* border was not — half a control. (F052.30) */
|
|
124
|
+
dangerBorder?: string;
|
|
125
|
+
/**
|
|
126
|
+
* THE ACTIVE CONTROL colour — the chosen intensity, the active sex toggle.
|
|
127
|
+
*
|
|
128
|
+
* NOT `palette.selected`. That is the colour of a MARK ON SKIN; this is the
|
|
129
|
+
* colour of an active button. Collapsing them would stop a consumer having a
|
|
130
|
+
* teal mark and a navy button, which is a normal thing to want. (F052.30)
|
|
131
|
+
*/
|
|
132
|
+
accent?: string;
|
|
133
|
+
/** Text ON the accent. Check it against your accent — see the README. */
|
|
134
|
+
accentText?: string;
|
|
122
135
|
}
|
|
123
136
|
type BodyView = "front" | "back" | "left" | "right";
|
|
124
137
|
/** Side in the serialized report — a midline region (no side) becomes "center". */
|
|
@@ -192,6 +205,16 @@ interface BodyMap3DUiLabels {
|
|
|
192
205
|
female: string;
|
|
193
206
|
hoverHint: string;
|
|
194
207
|
}
|
|
208
|
+
interface BodyMap3DHint {
|
|
209
|
+
/** Overrides the localized default. `ui.hoverHint` still works too. */
|
|
210
|
+
text?: string;
|
|
211
|
+
/**
|
|
212
|
+
* `after-canvas` (default) keeps today's flow position.
|
|
213
|
+
* `stage-bottom` pins it inside the fullscreen surface, above the home
|
|
214
|
+
* indicator — and falls back to the flow position when not fullscreen.
|
|
215
|
+
*/
|
|
216
|
+
placement?: "after-canvas" | "stage-bottom";
|
|
217
|
+
}
|
|
195
218
|
/**
|
|
196
219
|
* How much of the RUNNER-UP region bleeds into this vertex (F052.21).
|
|
197
220
|
*
|
|
@@ -252,6 +275,24 @@ interface BodyMap3DProps {
|
|
|
252
275
|
palette?: BodymapPalette;
|
|
253
276
|
locale?: BodyMapLocale;
|
|
254
277
|
labels?: Partial<BodyMapLabels>;
|
|
278
|
+
/**
|
|
279
|
+
* The help note under the body. F052.31.
|
|
280
|
+
*
|
|
281
|
+
* `false` renders NOTHING — no box, no border, no empty element. That path did
|
|
282
|
+
* not exist before: `ui.hoverHint = ""` produced an empty bordered box, which
|
|
283
|
+
* is worse than the sentence it replaced.
|
|
284
|
+
*
|
|
285
|
+
* `placement` is what fixes the defect rather than moving it. In the boxed
|
|
286
|
+
* layout the note belongs after the canvas; in FULLSCREEN that column wraps
|
|
287
|
+
* beneath a full-height canvas and the note lands past the bottom edge —
|
|
288
|
+
* measured by a consumer at 858-912 in an 852px window, i.e. entirely
|
|
289
|
+
* offscreen. `stage-bottom` anchors it inside the fullscreen surface instead,
|
|
290
|
+
* clear of the home indicator.
|
|
291
|
+
*
|
|
292
|
+
* A consumer cannot make that call in CSS without knowing our internal
|
|
293
|
+
* structure, which is exactly why two of them had to write wedges against it.
|
|
294
|
+
*/
|
|
295
|
+
hint?: false | BodyMap3DHint;
|
|
255
296
|
/** Override the 3D-only control strings (male/female/hoverHint). */
|
|
256
297
|
ui?: Partial<BodyMap3DUiLabels>;
|
|
257
298
|
defaultSex?: BodyMap3DSex;
|
|
@@ -312,4 +353,4 @@ interface BodyMap3DProps {
|
|
|
312
353
|
}
|
|
313
354
|
declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
|
|
314
355
|
|
|
315
|
-
export { BodyMap3D, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
|
|
356
|
+
export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
|
package/dist/three.js
CHANGED
|
@@ -89,7 +89,16 @@ var defaultUi = {
|
|
|
89
89
|
stageBg: STAGE_BG,
|
|
90
90
|
border: "#e2e8f0",
|
|
91
91
|
badgeBg: "#f1f5f9",
|
|
92
|
-
danger: "#dc2626"
|
|
92
|
+
danger: "#dc2626",
|
|
93
|
+
dangerBorder: "#f6c9c9",
|
|
94
|
+
// F052.30 — was #0e8f8a, which measured 3.95:1 against white. AA needs 4.5,
|
|
95
|
+
// and this is the SELECTED intensity button on a surface where AA is a legal
|
|
96
|
+
// duty. It shipped from 0.2.0 and no check could see it: the colour was an
|
|
97
|
+
// inline literal, and the F052.19 contrast sweep only read this object.
|
|
98
|
+
// #0c7d77 is the same teal a shade deeper — 4.98:1, with headroom rather than
|
|
99
|
+
// sitting on the 4.5 boundary where any later tweak reopens it.
|
|
100
|
+
accent: "#0c7d77",
|
|
101
|
+
accentText: "#fff"
|
|
93
102
|
};
|
|
94
103
|
function uiColors(palette) {
|
|
95
104
|
return { ...defaultUi, ...palette?.ui ?? {} };
|
|
@@ -203,8 +212,8 @@ var LABELS_EN = {
|
|
|
203
212
|
zoomOut: "Zoom out",
|
|
204
213
|
zoomReset: "Reset zoom"
|
|
205
214
|
};
|
|
206
|
-
var UI_DA = { male: "Mand", female: "Kvinde", hoverHint: "
|
|
207
|
-
var UI_EN = { male: "Male", female: "Female", hoverHint: "
|
|
215
|
+
var UI_DA = { male: "Mand", female: "Kvinde", hoverHint: "Tr\xE6k for at dreje \xB7 tryk en kropsdel for at markere smerte.", expand: "Vis stor", collapse: "Luk stor visning" };
|
|
216
|
+
var UI_EN = { male: "Male", female: "Female", hoverHint: "Drag to rotate \xB7 tap a body part to mark pain.", expand: "Expand", collapse: "Close" };
|
|
208
217
|
var ANCHORS = {
|
|
209
218
|
head: [0, 1.79, 0.02],
|
|
210
219
|
neck: [0, 1.57, 0],
|
|
@@ -276,8 +285,20 @@ function webglAvailable() {
|
|
|
276
285
|
return false;
|
|
277
286
|
}
|
|
278
287
|
}
|
|
279
|
-
var btn =
|
|
280
|
-
|
|
288
|
+
var btn = (c) => ({
|
|
289
|
+
font: "inherit",
|
|
290
|
+
cursor: "pointer",
|
|
291
|
+
borderRadius: 8,
|
|
292
|
+
border: `1px solid ${c.border}`,
|
|
293
|
+
background: c.panelBg,
|
|
294
|
+
padding: "6px 9px"
|
|
295
|
+
});
|
|
296
|
+
var seg = (c, on) => ({
|
|
297
|
+
...btn(c),
|
|
298
|
+
background: on ? c.accent : c.panelBg,
|
|
299
|
+
color: on ? c.accentText : c.text,
|
|
300
|
+
fontWeight: 600
|
|
301
|
+
});
|
|
281
302
|
function BodyMap3D(props) {
|
|
282
303
|
const {
|
|
283
304
|
models,
|
|
@@ -299,6 +320,7 @@ function BodyMap3D(props) {
|
|
|
299
320
|
onFeedback,
|
|
300
321
|
haptics,
|
|
301
322
|
seam = false,
|
|
323
|
+
hint,
|
|
302
324
|
fullscreen: fullscreenProp,
|
|
303
325
|
onFullscreenChange,
|
|
304
326
|
showFullscreenButton = true,
|
|
@@ -658,7 +680,9 @@ function BodyMap3D(props) {
|
|
|
658
680
|
const region = selected ? REGIONS.find((r) => r.key === selected) : null;
|
|
659
681
|
const current = selected ? pointOf(selected) : void 0;
|
|
660
682
|
const anySelectable = REGIONS.some((r) => isSelectable(r.key, config ?? {}));
|
|
661
|
-
const showEmptyHint = anySelectable || ui?.hoverHint !== void 0;
|
|
683
|
+
const showEmptyHint = hint === false ? false : anySelectable || ui?.hoverHint !== void 0;
|
|
684
|
+
const hintText = (hint ? hint.text : void 0) ?? UI.hoverHint;
|
|
685
|
+
const hintAtStage = isFullscreen && !!hint && hint.placement === "stage-bottom";
|
|
662
686
|
return /* @__PURE__ */ jsxs(
|
|
663
687
|
"div",
|
|
664
688
|
{
|
|
@@ -667,7 +691,7 @@ function BodyMap3D(props) {
|
|
|
667
691
|
"data-fullscreen": isFullscreen ? "true" : void 0,
|
|
668
692
|
style: {
|
|
669
693
|
fontFamily: "system-ui, sans-serif",
|
|
670
|
-
color:
|
|
694
|
+
color: chrome.text,
|
|
671
695
|
// A viewport fill, not the Fullscreen API — see the `fullscreen` prop.
|
|
672
696
|
// `fixed`+`inset:0` behaves the same on iOS Safari, where element
|
|
673
697
|
// fullscreen does not exist at all, as it does everywhere else.
|
|
@@ -699,8 +723,8 @@ function BodyMap3D(props) {
|
|
|
699
723
|
},
|
|
700
724
|
children: [
|
|
701
725
|
showSexToggle && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", marginBottom: 12, fontSize: 13 }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
|
|
702
|
-
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(sex === "male"), children: UI.male }),
|
|
703
|
-
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(sex === "female"), children: UI.female })
|
|
726
|
+
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(chrome, sex === "male"), children: UI.male }),
|
|
727
|
+
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(chrome, sex === "female"), children: UI.female })
|
|
704
728
|
] }) }),
|
|
705
729
|
/* @__PURE__ */ jsx("span", { ref: loadedRef, "data-testid": "bodymap3d-loaded", style: { display: "none" } }),
|
|
706
730
|
ready && /* @__PURE__ */ jsx("span", { "data-testid": "bodymap3d-ready", style: { position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" } }),
|
|
@@ -713,7 +737,7 @@ function BodyMap3D(props) {
|
|
|
713
737
|
"aria-label": isFullscreen ? UI.collapse ?? "Close" : UI.expand ?? "Expand",
|
|
714
738
|
onClick: () => setFullscreen(!isFullscreen),
|
|
715
739
|
style: {
|
|
716
|
-
...btn,
|
|
740
|
+
...btn(chrome),
|
|
717
741
|
color: chrome.text,
|
|
718
742
|
borderColor: chrome.border,
|
|
719
743
|
background: chrome.panelBg,
|
|
@@ -764,18 +788,43 @@ function BodyMap3D(props) {
|
|
|
764
788
|
"data-testid": "bodymap3d-close",
|
|
765
789
|
"aria-label": L.close,
|
|
766
790
|
onClick: () => setSelected(null),
|
|
767
|
-
style: { ...btn, width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
|
|
791
|
+
style: { ...btn(chrome), width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
|
|
768
792
|
children: "\xD7"
|
|
769
793
|
}
|
|
770
794
|
)
|
|
771
795
|
] }),
|
|
772
796
|
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
|
|
773
|
-
/* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => /* @__PURE__ */ jsx("button", { "data-testid": `bodymap3d-intensity-${i}`, onClick: () => setPain(region.key, i, current?.type), style: { ...btn, display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 30, height: 30, padding: 0, background: current?.intensity === i ?
|
|
797
|
+
/* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => /* @__PURE__ */ jsx("button", { "data-testid": `bodymap3d-intensity-${i}`, onClick: () => setPain(region.key, i, current?.type), style: { ...btn(chrome), display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 30, height: 30, padding: 0, background: current?.intensity === i ? chrome.accent : chrome.panelBg, color: current?.intensity === i ? chrome.accentText : chrome.text }, children: i }, i)) }),
|
|
774
798
|
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
|
|
775
|
-
/* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 14 }, children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsx("button", { "data-testid": `bodymap3d-type-${t}`, onClick: () => setPain(region.key, current?.intensity ?? 5, t), style: { ...btn, borderRadius: 999, padding: "6px 12px", background: current?.type === t ?
|
|
776
|
-
current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn, color: chrome.danger, borderColor:
|
|
777
|
-
] }) : showEmptyHint ? /* @__PURE__ */ jsx("div", { "data-testid": "bodymap3d-empty", style: { border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg, color: chrome.mutedText, fontSize: 13.5 }, children:
|
|
778
|
-
] })
|
|
799
|
+
/* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 14 }, children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsx("button", { "data-testid": `bodymap3d-type-${t}`, onClick: () => setPain(region.key, current?.intensity ?? 5, t), style: { ...btn(chrome), borderRadius: 999, padding: "6px 12px", background: current?.type === t ? chrome.text : chrome.panelBg, color: current?.type === t ? chrome.panelBg : chrome.mutedText }, children: L.qualities[t] ?? t }, t)) }),
|
|
800
|
+
current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }, children: L.remove })
|
|
801
|
+
] }) : showEmptyHint && !hintAtStage ? /* @__PURE__ */ jsx("div", { "data-testid": "bodymap3d-empty", style: { border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg, color: chrome.mutedText, fontSize: 13.5 }, children: hintText }) : null })
|
|
802
|
+
] }),
|
|
803
|
+
showEmptyHint && hintAtStage && /* F052.31 — pinned INSIDE the fullscreen surface, not in the column that
|
|
804
|
+
wraps beneath a full-height canvas. That wrap is what put the note at
|
|
805
|
+
858-912 in an 852px window on a real iPhone: entirely offscreen, and
|
|
806
|
+
it reads as a bug in the consumer's app rather than in ours.
|
|
807
|
+
The bottom inset clears the home indicator, exactly as the exit
|
|
808
|
+
control's does (F052.27). */
|
|
809
|
+
/* @__PURE__ */ jsx(
|
|
810
|
+
"div",
|
|
811
|
+
{
|
|
812
|
+
"data-testid": "bodymap3d-empty",
|
|
813
|
+
"data-placement": "stage-bottom",
|
|
814
|
+
style: {
|
|
815
|
+
flex: "0 0 auto",
|
|
816
|
+
marginTop: 12,
|
|
817
|
+
marginBottom: "env(safe-area-inset-bottom)",
|
|
818
|
+
border: `1px solid ${chrome.border}`,
|
|
819
|
+
borderRadius: 14,
|
|
820
|
+
padding: 16,
|
|
821
|
+
background: chrome.panelBg,
|
|
822
|
+
color: chrome.mutedText,
|
|
823
|
+
fontSize: 13.5
|
|
824
|
+
},
|
|
825
|
+
children: hintText
|
|
826
|
+
}
|
|
827
|
+
)
|
|
779
828
|
]
|
|
780
829
|
}
|
|
781
830
|
);
|