@broberg/bodymap 0.13.2 → 0.14.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 +8 -0
- package/dist/three.cjs +101 -62
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +19 -1
- package/dist/three.d.ts +19 -1
- package/dist/three.js +99 -63
- package/dist/three.js.map +1 -1
- package/package.json +1 -1
package/dist/three.d.cts
CHANGED
|
@@ -398,6 +398,24 @@ interface BodyMap3DProps {
|
|
|
398
398
|
* and all eleven take the same box so none is an ellipse among circles.
|
|
399
399
|
*/
|
|
400
400
|
declare const INTENSITY_SIZE = 46;
|
|
401
|
+
/**
|
|
402
|
+
* F052.34 — the touch floor F052.8 set for this component (a phone is its primary
|
|
403
|
+
* surface). The panel's close control was 40×40 — round, but under the floor; it now
|
|
404
|
+
* takes exactly this. INTENSITY_SIZE sits above it.
|
|
405
|
+
*/
|
|
406
|
+
declare const TOUCH_MIN = 44;
|
|
407
|
+
/**
|
|
408
|
+
* F052.35 — whether fullscreen must STACK the panel under the stage instead of
|
|
409
|
+
* beside it. The row lays the canvas (basis 520) and the panel (basis 300) side
|
|
410
|
+
* by side with an 18px gap and wraps when they do not fit — and in fullscreen
|
|
411
|
+
* the canvas is the full height, so a wrapped panel lands entirely below the
|
|
412
|
+
* screen. Measured on iphone-15 (369px of content): every intensity button and
|
|
413
|
+
* the close sat at top >= innerHeight, so a patient could mark a body part and
|
|
414
|
+
* not say how much it hurt. Stacking shrinks the stage instead, and the panel
|
|
415
|
+
* stays on screen. Pure so the threshold is asserted without a browser.
|
|
416
|
+
*/
|
|
417
|
+
declare const FS_ROW_MIN: number;
|
|
418
|
+
declare function stackInFullscreen(contentWidth: number): boolean;
|
|
401
419
|
/**
|
|
402
420
|
* F052.33 — should the help note be on screen at all?
|
|
403
421
|
*
|
|
@@ -418,4 +436,4 @@ declare function shouldShowHint(opts: {
|
|
|
418
436
|
}): boolean;
|
|
419
437
|
declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
|
|
420
438
|
|
|
421
|
-
export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, INTENSITY_SIZE, TAP_MAX_DISTANCE, ensureNormals, isTap, seamBlend, seamWeight, serializeReport, shouldShowHint };
|
|
439
|
+
export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, FS_ROW_MIN, INTENSITY_SIZE, TAP_MAX_DISTANCE, TOUCH_MIN, ensureNormals, isTap, seamBlend, seamWeight, serializeReport, shouldShowHint, stackInFullscreen };
|
package/dist/three.d.ts
CHANGED
|
@@ -398,6 +398,24 @@ interface BodyMap3DProps {
|
|
|
398
398
|
* and all eleven take the same box so none is an ellipse among circles.
|
|
399
399
|
*/
|
|
400
400
|
declare const INTENSITY_SIZE = 46;
|
|
401
|
+
/**
|
|
402
|
+
* F052.34 — the touch floor F052.8 set for this component (a phone is its primary
|
|
403
|
+
* surface). The panel's close control was 40×40 — round, but under the floor; it now
|
|
404
|
+
* takes exactly this. INTENSITY_SIZE sits above it.
|
|
405
|
+
*/
|
|
406
|
+
declare const TOUCH_MIN = 44;
|
|
407
|
+
/**
|
|
408
|
+
* F052.35 — whether fullscreen must STACK the panel under the stage instead of
|
|
409
|
+
* beside it. The row lays the canvas (basis 520) and the panel (basis 300) side
|
|
410
|
+
* by side with an 18px gap and wraps when they do not fit — and in fullscreen
|
|
411
|
+
* the canvas is the full height, so a wrapped panel lands entirely below the
|
|
412
|
+
* screen. Measured on iphone-15 (369px of content): every intensity button and
|
|
413
|
+
* the close sat at top >= innerHeight, so a patient could mark a body part and
|
|
414
|
+
* not say how much it hurt. Stacking shrinks the stage instead, and the panel
|
|
415
|
+
* stays on screen. Pure so the threshold is asserted without a browser.
|
|
416
|
+
*/
|
|
417
|
+
declare const FS_ROW_MIN: number;
|
|
418
|
+
declare function stackInFullscreen(contentWidth: number): boolean;
|
|
401
419
|
/**
|
|
402
420
|
* F052.33 — should the help note be on screen at all?
|
|
403
421
|
*
|
|
@@ -418,4 +436,4 @@ declare function shouldShowHint(opts: {
|
|
|
418
436
|
}): boolean;
|
|
419
437
|
declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
|
|
420
438
|
|
|
421
|
-
export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, INTENSITY_SIZE, TAP_MAX_DISTANCE, ensureNormals, isTap, seamBlend, seamWeight, serializeReport, shouldShowHint };
|
|
439
|
+
export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, FS_ROW_MIN, INTENSITY_SIZE, TAP_MAX_DISTANCE, TOUCH_MIN, ensureNormals, isTap, seamBlend, seamWeight, serializeReport, shouldShowHint, stackInFullscreen };
|
package/dist/three.js
CHANGED
|
@@ -290,6 +290,11 @@ function webglAvailable() {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
var INTENSITY_SIZE = 46;
|
|
293
|
+
var TOUCH_MIN = 44;
|
|
294
|
+
var FS_ROW_MIN = 520 + 18 + 300;
|
|
295
|
+
function stackInFullscreen(contentWidth) {
|
|
296
|
+
return contentWidth < FS_ROW_MIN;
|
|
297
|
+
}
|
|
293
298
|
function shouldShowHint(opts) {
|
|
294
299
|
if (opts.openRegion) return false;
|
|
295
300
|
if (opts.hint === false) return false;
|
|
@@ -382,6 +387,17 @@ function BodyMap3D(props) {
|
|
|
382
387
|
onFullscreenChange?.(v);
|
|
383
388
|
};
|
|
384
389
|
const apiRef = useRef(null);
|
|
390
|
+
const rowRef = useRef(null);
|
|
391
|
+
const [rowWidth, setRowWidth] = useState(0);
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
const el = rowRef.current;
|
|
394
|
+
if (!isFullscreen || !el) return;
|
|
395
|
+
setRowWidth(el.clientWidth);
|
|
396
|
+
const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => setRowWidth(el.clientWidth)) : null;
|
|
397
|
+
ro?.observe(el);
|
|
398
|
+
return () => ro?.disconnect();
|
|
399
|
+
}, [isFullscreen]);
|
|
400
|
+
const stacked = isFullscreen && stackInFullscreen(rowWidth);
|
|
385
401
|
useEffect(() => {
|
|
386
402
|
if (!webglAvailable()) {
|
|
387
403
|
setUnsupported(true);
|
|
@@ -760,70 +776,90 @@ function BodyMap3D(props) {
|
|
|
760
776
|
children: isFullscreen ? "\xD7" : UI.expand ?? "Expand"
|
|
761
777
|
}
|
|
762
778
|
) }),
|
|
763
|
-
/* @__PURE__ */ jsxs(
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
height: isFullscreen ? "100%" : canvasH,
|
|
782
|
-
minWidth: 320,
|
|
783
|
-
minHeight: isFullscreen ? 0 : void 0,
|
|
784
|
-
borderRadius: 16,
|
|
785
|
-
overflow: "hidden",
|
|
786
|
-
background: chrome.stageBg,
|
|
787
|
-
touchAction: "none"
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
),
|
|
791
|
-
/* @__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: [
|
|
792
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
|
|
793
|
-
/* @__PURE__ */ jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
|
|
794
|
-
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: [
|
|
795
|
-
region.code,
|
|
796
|
-
region.side ? " \xB7 " + region.side : ""
|
|
797
|
-
] }),
|
|
798
|
-
/* @__PURE__ */ jsx(
|
|
799
|
-
"button",
|
|
779
|
+
/* @__PURE__ */ jsxs(
|
|
780
|
+
"div",
|
|
781
|
+
{
|
|
782
|
+
ref: rowRef,
|
|
783
|
+
"data-layout": stacked ? "stacked" : void 0,
|
|
784
|
+
style: {
|
|
785
|
+
display: "flex",
|
|
786
|
+
gap: 18,
|
|
787
|
+
alignItems: "flex-start",
|
|
788
|
+
flexWrap: "wrap",
|
|
789
|
+
...isFullscreen ? { flex: "1 1 auto", minHeight: 0 } : null,
|
|
790
|
+
// F052.35 — a column that never wraps: the stage takes what the panel
|
|
791
|
+
// leaves, so the panel is always inside the screen.
|
|
792
|
+
...stacked ? { flexDirection: "column", flexWrap: "nowrap", alignItems: "stretch", gap: 12 } : null
|
|
793
|
+
},
|
|
794
|
+
children: [
|
|
795
|
+
unsupported ? /* @__PURE__ */ jsx(
|
|
796
|
+
"div",
|
|
800
797
|
{
|
|
801
|
-
|
|
802
|
-
"
|
|
803
|
-
|
|
804
|
-
onClick: () => setSelected(null),
|
|
805
|
-
style: { ...btn(chrome), width: 40, height: 40, minWidth: 40, padding: 0, borderRadius: "50%", fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
|
|
806
|
-
children: "\xD7"
|
|
798
|
+
"data-testid": "bodymap3d-unsupported",
|
|
799
|
+
style: { flex: "1 1 520px", minWidth: 320, height: canvasH, borderRadius: 16, background: chrome.panelBg, color: chrome.mutedText, border: `1px solid ${chrome.border}`, display: "flex", alignItems: "center", justifyContent: "center", padding: 24, textAlign: "center", fontSize: 14 },
|
|
800
|
+
children: "3D kr\xE6ver WebGL, som ikke er tilg\xE6ngeligt her."
|
|
807
801
|
}
|
|
808
|
-
)
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
802
|
+
) : /* @__PURE__ */ jsx(
|
|
803
|
+
"div",
|
|
804
|
+
{
|
|
805
|
+
ref: mountRef,
|
|
806
|
+
"data-testid": "bodymap3d-canvas",
|
|
807
|
+
style: {
|
|
808
|
+
flex: "1 1 520px",
|
|
809
|
+
// In fullscreen the box takes the height it is GIVEN rather than a
|
|
810
|
+
// fixed `canvasHeight`; the ResizeObserver above is what makes the
|
|
811
|
+
// picture follow it.
|
|
812
|
+
height: isFullscreen ? "100%" : canvasH,
|
|
813
|
+
minWidth: 320,
|
|
814
|
+
minHeight: isFullscreen ? 0 : void 0,
|
|
815
|
+
// Stacked: the stage fills what is left above the panel, and
|
|
816
|
+
// keeps a floor so the body never disappears behind it.
|
|
817
|
+
...stacked ? { flex: "1 1 0", height: "auto", minWidth: 0, minHeight: 160 } : null,
|
|
818
|
+
borderRadius: 16,
|
|
819
|
+
overflow: "hidden",
|
|
820
|
+
background: chrome.stageBg,
|
|
821
|
+
touchAction: "none"
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
),
|
|
825
|
+
/* @__PURE__ */ jsx("div", { "data-testid": "bodymap3d-panel-col", style: { flex: "1 1 300px", minWidth: 260, ...stacked ? { flex: "0 1 auto", minWidth: 0, minHeight: 0, overflowY: "auto" } : null }, children: region ? /* @__PURE__ */ jsxs("div", { style: { border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg }, children: [
|
|
826
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
|
|
827
|
+
/* @__PURE__ */ jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
|
|
828
|
+
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: [
|
|
829
|
+
region.code,
|
|
830
|
+
region.side ? " \xB7 " + region.side : ""
|
|
831
|
+
] }),
|
|
832
|
+
/* @__PURE__ */ jsx(
|
|
833
|
+
"button",
|
|
834
|
+
{
|
|
835
|
+
type: "button",
|
|
836
|
+
"data-testid": "bodymap3d-close",
|
|
837
|
+
"aria-label": L.close,
|
|
838
|
+
onClick: () => setSelected(null),
|
|
839
|
+
style: { ...btn(chrome), width: TOUCH_MIN, height: TOUCH_MIN, minWidth: TOUCH_MIN, padding: 0, borderRadius: "50%", fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
|
|
840
|
+
children: "\xD7"
|
|
841
|
+
}
|
|
842
|
+
)
|
|
843
|
+
] }),
|
|
844
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
|
|
845
|
+
/* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => (
|
|
846
|
+
/* F052.34 — CIRCLES, and all eleven the same size.
|
|
847
|
+
Dimensioned by «10», the widest label: sizing by the average
|
|
848
|
+
would make the two-digit one an ellipse among circles, which
|
|
849
|
+
is worse than eleven rounded squares because the odd one out
|
|
850
|
+
becomes a different SHAPE rather than slightly wider.
|
|
851
|
+
46px is the SIBLING's touch size, not a new number: the 2D
|
|
852
|
+
renderer already used it and this one was on 30px, under the
|
|
853
|
+
>=44px floor F052.8 set. See INTENSITY_SIZE. */
|
|
854
|
+
/* @__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", width: INTENSITY_SIZE, height: INTENSITY_SIZE, minWidth: INTENSITY_SIZE, padding: 0, borderRadius: "50%", background: current?.intensity === i ? chrome.accent : chrome.panelBg, color: current?.intensity === i ? chrome.accentText : chrome.text }, children: i }, i)
|
|
855
|
+
)) }),
|
|
856
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
|
|
857
|
+
/* @__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)) }),
|
|
858
|
+
current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }, children: L.remove })
|
|
859
|
+
] }) : 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 })
|
|
860
|
+
]
|
|
861
|
+
}
|
|
862
|
+
),
|
|
827
863
|
showEmptyHint && hintAtStage && /* F052.31 — pinned INSIDE the fullscreen surface, not in the column that
|
|
828
864
|
wraps beneath a full-height canvas. That wrap is what put the note at
|
|
829
865
|
858-912 in an 852px window on a real iPhone: entirely offscreen, and
|
|
@@ -854,6 +890,6 @@ function BodyMap3D(props) {
|
|
|
854
890
|
);
|
|
855
891
|
}
|
|
856
892
|
|
|
857
|
-
export { BodyMap3D, INTENSITY_SIZE, TAP_MAX_DISTANCE, ensureNormals, isTap, seamBlend, seamWeight, serializeReport, shouldShowHint };
|
|
893
|
+
export { BodyMap3D, FS_ROW_MIN, INTENSITY_SIZE, TAP_MAX_DISTANCE, TOUCH_MIN, ensureNormals, isTap, seamBlend, seamWeight, serializeReport, shouldShowHint, stackInFullscreen };
|
|
858
894
|
//# sourceMappingURL=three.js.map
|
|
859
895
|
//# sourceMappingURL=three.js.map
|