@broberg/bodymap 0.5.0 → 0.7.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 +64 -0
- package/dist/react.cjs +18 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +18 -1
- package/dist/react.js.map +1 -1
- package/dist/three.cjs +118 -27
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +19 -0
- package/dist/three.d.ts +19 -0
- package/dist/three.js +118 -27
- package/dist/three.js.map +1 -1
- package/package.json +1 -1
package/dist/three.d.cts
CHANGED
|
@@ -134,6 +134,8 @@ interface BodyMapLabels {
|
|
|
134
134
|
intensity: string;
|
|
135
135
|
quality: string;
|
|
136
136
|
remove: string;
|
|
137
|
+
/** Close the picker without marking anything (F052.26). */
|
|
138
|
+
close: string;
|
|
137
139
|
front: string;
|
|
138
140
|
back: string;
|
|
139
141
|
empty: string;
|
|
@@ -168,6 +170,10 @@ interface BodyMap3DModels {
|
|
|
168
170
|
}
|
|
169
171
|
/** The 3D-only control strings (the shared labels come from `locale`/`labels`). */
|
|
170
172
|
interface BodyMap3DUiLabels {
|
|
173
|
+
/** Label for the built-in fullscreen control (F052.24). */
|
|
174
|
+
expand?: string;
|
|
175
|
+
/** Label for the same control while fullscreen. */
|
|
176
|
+
collapse?: string;
|
|
171
177
|
male: string;
|
|
172
178
|
female: string;
|
|
173
179
|
hoverHint: string;
|
|
@@ -216,6 +222,19 @@ interface BodyMap3DProps {
|
|
|
216
222
|
* (default true). Set false for a patient/employee-facing flow where the code is
|
|
217
223
|
* internal jargon and the readable region name is enough. */
|
|
218
224
|
showRegionCode?: boolean;
|
|
225
|
+
/** Fill the viewport with the body (F052.24). Controlled: pass it and you own
|
|
226
|
+
* it; `onFullscreenChange` still fires so your own button can drive it.
|
|
227
|
+
*
|
|
228
|
+
* This is a VIEWPORT FILL, not the browser Fullscreen API. iOS Safari does not
|
|
229
|
+
* support fullscreen on an arbitrary element at all, and the iPhone is this
|
|
230
|
+
* component's primary surface — so the mechanism that works everywhere is the
|
|
231
|
+
* one that ships, and it behaves identically on every platform. */
|
|
232
|
+
fullscreen?: boolean;
|
|
233
|
+
/** Fired when the built-in control or the Escape key changes it. */
|
|
234
|
+
onFullscreenChange?: (fullscreen: boolean) => void;
|
|
235
|
+
/** Show the built-in expand/collapse control (default true). Set false and
|
|
236
|
+
* drive `fullscreen` from your own button. */
|
|
237
|
+
showFullscreenButton?: boolean;
|
|
219
238
|
/** Soften the colour transition between neighbouring regions (F052.21).
|
|
220
239
|
*
|
|
221
240
|
* DEFAULT FALSE, and the default is the accessible one (F052.23). The blend
|
package/dist/three.d.ts
CHANGED
|
@@ -134,6 +134,8 @@ interface BodyMapLabels {
|
|
|
134
134
|
intensity: string;
|
|
135
135
|
quality: string;
|
|
136
136
|
remove: string;
|
|
137
|
+
/** Close the picker without marking anything (F052.26). */
|
|
138
|
+
close: string;
|
|
137
139
|
front: string;
|
|
138
140
|
back: string;
|
|
139
141
|
empty: string;
|
|
@@ -168,6 +170,10 @@ interface BodyMap3DModels {
|
|
|
168
170
|
}
|
|
169
171
|
/** The 3D-only control strings (the shared labels come from `locale`/`labels`). */
|
|
170
172
|
interface BodyMap3DUiLabels {
|
|
173
|
+
/** Label for the built-in fullscreen control (F052.24). */
|
|
174
|
+
expand?: string;
|
|
175
|
+
/** Label for the same control while fullscreen. */
|
|
176
|
+
collapse?: string;
|
|
171
177
|
male: string;
|
|
172
178
|
female: string;
|
|
173
179
|
hoverHint: string;
|
|
@@ -216,6 +222,19 @@ interface BodyMap3DProps {
|
|
|
216
222
|
* (default true). Set false for a patient/employee-facing flow where the code is
|
|
217
223
|
* internal jargon and the readable region name is enough. */
|
|
218
224
|
showRegionCode?: boolean;
|
|
225
|
+
/** Fill the viewport with the body (F052.24). Controlled: pass it and you own
|
|
226
|
+
* it; `onFullscreenChange` still fires so your own button can drive it.
|
|
227
|
+
*
|
|
228
|
+
* This is a VIEWPORT FILL, not the browser Fullscreen API. iOS Safari does not
|
|
229
|
+
* support fullscreen on an arbitrary element at all, and the iPhone is this
|
|
230
|
+
* component's primary surface — so the mechanism that works everywhere is the
|
|
231
|
+
* one that ships, and it behaves identically on every platform. */
|
|
232
|
+
fullscreen?: boolean;
|
|
233
|
+
/** Fired when the built-in control or the Escape key changes it. */
|
|
234
|
+
onFullscreenChange?: (fullscreen: boolean) => void;
|
|
235
|
+
/** Show the built-in expand/collapse control (default true). Set false and
|
|
236
|
+
* drive `fullscreen` from your own button. */
|
|
237
|
+
showFullscreenButton?: boolean;
|
|
219
238
|
/** Soften the colour transition between neighbouring regions (F052.21).
|
|
220
239
|
*
|
|
221
240
|
* DEFAULT FALSE, and the default is the accessible one (F052.23). The blend
|
package/dist/three.js
CHANGED
|
@@ -161,6 +161,7 @@ var LABELS_DA = {
|
|
|
161
161
|
intensity: "Intensitet (0-10)",
|
|
162
162
|
quality: "Kvalitet",
|
|
163
163
|
remove: "Fjern punkt",
|
|
164
|
+
close: "Luk",
|
|
164
165
|
front: "Forfra",
|
|
165
166
|
back: "Bagfra",
|
|
166
167
|
empty: "V\xE6lg en kropsdel for at markere smerte.",
|
|
@@ -183,6 +184,7 @@ var LABELS_EN = {
|
|
|
183
184
|
intensity: "Intensity (0-10)",
|
|
184
185
|
quality: "Quality",
|
|
185
186
|
remove: "Remove point",
|
|
187
|
+
close: "Close",
|
|
186
188
|
front: "Front",
|
|
187
189
|
back: "Back",
|
|
188
190
|
empty: "Pick a body part to mark pain.",
|
|
@@ -199,8 +201,8 @@ var LABELS_EN = {
|
|
|
199
201
|
zoomOut: "Zoom out",
|
|
200
202
|
zoomReset: "Reset zoom"
|
|
201
203
|
};
|
|
202
|
-
var UI_DA = { male: "Mand", female: "Kvinde", hoverHint: "Hover for at fremh\xE6ve \xB7 klik en kropsdel for at markere smerte." };
|
|
203
|
-
var UI_EN = { male: "Male", female: "Female", hoverHint: "Hover to highlight \xB7 tap a body part to mark pain." };
|
|
204
|
+
var UI_DA = { male: "Mand", female: "Kvinde", hoverHint: "Hover for at fremh\xE6ve \xB7 klik en kropsdel for at markere smerte.", expand: "Vis stor", collapse: "Luk stor visning" };
|
|
205
|
+
var UI_EN = { male: "Male", female: "Female", hoverHint: "Hover to highlight \xB7 tap a body part to mark pain.", expand: "Expand", collapse: "Close" };
|
|
204
206
|
var ANCHORS = {
|
|
205
207
|
head: [0, 1.79, 0.02],
|
|
206
208
|
neck: [0, 1.57, 0],
|
|
@@ -287,6 +289,9 @@ function BodyMap3D(props) {
|
|
|
287
289
|
onFeedback,
|
|
288
290
|
haptics,
|
|
289
291
|
seam = false,
|
|
292
|
+
fullscreen: fullscreenProp,
|
|
293
|
+
onFullscreenChange,
|
|
294
|
+
showFullscreenButton = true,
|
|
290
295
|
className
|
|
291
296
|
} = props;
|
|
292
297
|
const chrome = uiColors(palette);
|
|
@@ -326,6 +331,12 @@ function BodyMap3D(props) {
|
|
|
326
331
|
setReadyRef.current = setReady;
|
|
327
332
|
const seamRef = useRef(seam);
|
|
328
333
|
seamRef.current = seam;
|
|
334
|
+
const [internalFs, setInternalFs] = useState(false);
|
|
335
|
+
const isFullscreen = fullscreenProp ?? internalFs;
|
|
336
|
+
const setFullscreen = (v) => {
|
|
337
|
+
if (fullscreenProp === void 0) setInternalFs(v);
|
|
338
|
+
onFullscreenChange?.(v);
|
|
339
|
+
};
|
|
329
340
|
const apiRef = useRef(null);
|
|
330
341
|
useEffect(() => {
|
|
331
342
|
if (!webglAvailable()) {
|
|
@@ -575,8 +586,11 @@ function BodyMap3D(props) {
|
|
|
575
586
|
renderFrame();
|
|
576
587
|
};
|
|
577
588
|
window.addEventListener("resize", onResize);
|
|
589
|
+
const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => onResize()) : null;
|
|
590
|
+
ro?.observe(el);
|
|
578
591
|
return () => {
|
|
579
592
|
cancelAnimationFrame(raf);
|
|
593
|
+
ro?.disconnect();
|
|
580
594
|
window.removeEventListener("resize", onResize);
|
|
581
595
|
document.removeEventListener("visibilitychange", onVis);
|
|
582
596
|
controls.removeEventListener("change", kick);
|
|
@@ -588,6 +602,16 @@ function BodyMap3D(props) {
|
|
|
588
602
|
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
|
|
589
603
|
};
|
|
590
604
|
}, []);
|
|
605
|
+
useEffect(() => {
|
|
606
|
+
if (!isFullscreen && !selected) return;
|
|
607
|
+
const onKey = (e) => {
|
|
608
|
+
if (e.key !== "Escape") return;
|
|
609
|
+
if (selected) setSelected(null);
|
|
610
|
+
else setFullscreen(false);
|
|
611
|
+
};
|
|
612
|
+
window.addEventListener("keydown", onKey);
|
|
613
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
614
|
+
});
|
|
591
615
|
useEffect(() => {
|
|
592
616
|
apiRef.current?.refresh();
|
|
593
617
|
}, [selected, report, palette, seam]);
|
|
@@ -622,31 +646,98 @@ function BodyMap3D(props) {
|
|
|
622
646
|
const current = selected ? pointOf(selected) : void 0;
|
|
623
647
|
const anySelectable = REGIONS.some((r) => isSelectable(r.key, config ?? {}));
|
|
624
648
|
const showEmptyHint = anySelectable || ui?.hoverHint !== void 0;
|
|
625
|
-
return /* @__PURE__ */ jsxs(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
649
|
+
return /* @__PURE__ */ jsxs(
|
|
650
|
+
"div",
|
|
651
|
+
{
|
|
652
|
+
"data-testid": "bodymap3d-root",
|
|
653
|
+
className,
|
|
654
|
+
"data-fullscreen": isFullscreen ? "true" : void 0,
|
|
655
|
+
style: {
|
|
656
|
+
fontFamily: "system-ui, sans-serif",
|
|
657
|
+
color: "#1e293b",
|
|
658
|
+
// A viewport fill, not the Fullscreen API — see the `fullscreen` prop.
|
|
659
|
+
// `fixed`+`inset:0` behaves the same on iOS Safari, where element
|
|
660
|
+
// fullscreen does not exist at all, as it does everywhere else.
|
|
661
|
+
...isFullscreen ? {
|
|
662
|
+
position: "fixed",
|
|
663
|
+
inset: 0,
|
|
664
|
+
zIndex: 2147483e3,
|
|
665
|
+
background: chrome.panelBg,
|
|
666
|
+
padding: 12,
|
|
667
|
+
display: "flex",
|
|
668
|
+
flexDirection: "column",
|
|
669
|
+
overflow: "auto"
|
|
670
|
+
} : null
|
|
671
|
+
},
|
|
672
|
+
children: [
|
|
673
|
+
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: [
|
|
674
|
+
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(sex === "male"), children: UI.male }),
|
|
675
|
+
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(sex === "female"), children: UI.female })
|
|
676
|
+
] }) }),
|
|
677
|
+
/* @__PURE__ */ jsx("span", { ref: loadedRef, "data-testid": "bodymap3d-loaded", style: { display: "none" } }),
|
|
678
|
+
ready && /* @__PURE__ */ jsx("span", { "data-testid": "bodymap3d-ready", style: { position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" } }),
|
|
679
|
+
showFullscreenButton && !unsupported && /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginBottom: 8 }, children: /* @__PURE__ */ jsx(
|
|
680
|
+
"button",
|
|
681
|
+
{
|
|
682
|
+
type: "button",
|
|
683
|
+
"data-testid": "bodymap3d-fullscreen",
|
|
684
|
+
"aria-pressed": isFullscreen,
|
|
685
|
+
"aria-label": isFullscreen ? UI.collapse ?? "Close" : UI.expand ?? "Expand",
|
|
686
|
+
onClick: () => setFullscreen(!isFullscreen),
|
|
687
|
+
style: { ...btn, color: chrome.text, borderColor: chrome.border, background: chrome.panelBg },
|
|
688
|
+
children: isFullscreen ? UI.collapse ?? "Close" : UI.expand ?? "Expand"
|
|
689
|
+
}
|
|
690
|
+
) }),
|
|
691
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap", ...isFullscreen ? { flex: "1 1 auto", minHeight: 0 } : null }, children: [
|
|
692
|
+
unsupported ? /* @__PURE__ */ jsx("div", { "data-testid": "bodymap3d-unsupported", style: { flex: "1 1 520px", minWidth: 320, height: canvasH, borderRadius: 16, background: "#0e1424", color: "#cbd5e1", display: "flex", alignItems: "center", justifyContent: "center", padding: 24, textAlign: "center", fontSize: 14 }, children: "3D kr\xE6ver WebGL, som ikke er tilg\xE6ngeligt her." }) : /* @__PURE__ */ jsx(
|
|
693
|
+
"div",
|
|
694
|
+
{
|
|
695
|
+
ref: mountRef,
|
|
696
|
+
"data-testid": "bodymap3d-canvas",
|
|
697
|
+
style: {
|
|
698
|
+
flex: "1 1 520px",
|
|
699
|
+
// In fullscreen the box takes the height it is GIVEN rather than a
|
|
700
|
+
// fixed `canvasHeight`; the ResizeObserver above is what makes the
|
|
701
|
+
// picture follow it.
|
|
702
|
+
height: isFullscreen ? "100%" : canvasH,
|
|
703
|
+
minWidth: 320,
|
|
704
|
+
minHeight: isFullscreen ? 0 : void 0,
|
|
705
|
+
borderRadius: 16,
|
|
706
|
+
overflow: "hidden",
|
|
707
|
+
background: "#0e1424",
|
|
708
|
+
touchAction: "none"
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
),
|
|
712
|
+
/* @__PURE__ */ jsx("div", { style: { flex: "1 1 300px", minWidth: 260 }, children: region ? /* @__PURE__ */ jsxs("div", { style: { border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg }, children: [
|
|
713
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
|
|
714
|
+
/* @__PURE__ */ jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
|
|
715
|
+
showRegionCode && /* @__PURE__ */ jsxs("span", { "data-testid": "bodymap3d-region-code", style: { font: "11px ui-monospace, monospace", color: chrome.mutedText, background: chrome.badgeBg, borderRadius: 6, padding: "2px 7px" }, children: [
|
|
716
|
+
region.code,
|
|
717
|
+
region.side ? " \xB7 " + region.side : ""
|
|
718
|
+
] }),
|
|
719
|
+
/* @__PURE__ */ jsx(
|
|
720
|
+
"button",
|
|
721
|
+
{
|
|
722
|
+
type: "button",
|
|
723
|
+
"data-testid": "bodymap3d-close",
|
|
724
|
+
"aria-label": L.close,
|
|
725
|
+
onClick: () => setSelected(null),
|
|
726
|
+
style: { ...btn, width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
|
|
727
|
+
children: "\xD7"
|
|
728
|
+
}
|
|
729
|
+
)
|
|
730
|
+
] }),
|
|
731
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
|
|
732
|
+
/* @__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 ? "#0e8f8a" : "#fff", color: current?.intensity === i ? "#fff" : "#1e293b" }, children: i }, i)) }),
|
|
733
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
|
|
734
|
+
/* @__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 ? "#1e293b" : "#fff", color: current?.type === t ? "#fff" : "#64748b" }, children: L.qualities[t] ?? t }, t)) }),
|
|
735
|
+
current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn, color: chrome.danger, borderColor: "#f6c9c9" }, children: L.remove })
|
|
736
|
+
] }) : 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: UI.hoverHint }) : null })
|
|
737
|
+
] })
|
|
738
|
+
]
|
|
739
|
+
}
|
|
740
|
+
);
|
|
650
741
|
}
|
|
651
742
|
|
|
652
743
|
export { BodyMap3D, seamBlend, serializeReport };
|