@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/README.md
CHANGED
|
@@ -274,6 +274,14 @@ this component's primary surface. A `position: fixed; inset: 0` overlay behaves
|
|
|
274
274
|
identically on every platform, needs no user-gesture dance, and cannot leave you
|
|
275
275
|
with a control that silently does nothing on the one device that matters most.
|
|
276
276
|
|
|
277
|
+
**On a narrow screen the panel goes UNDER the body (0.14.0).** Side by side needs
|
|
278
|
+
`FS_ROW_MIN` (838px) of content width. Below that, fullscreen stacks: the body
|
|
279
|
+
shrinks to what the panel leaves (never under 160px) and the panel sits on screen
|
|
280
|
+
beneath it. Before 0.14.0 the panel wrapped below a full-height canvas — on an
|
|
281
|
+
iPhone every intensity button sat under the bottom edge, so a patient could mark
|
|
282
|
+
a body part and not say how much it hurt. Wider than 838px, nothing changed.
|
|
283
|
+
`stackInFullscreen(width)` is exported if you need the same decision.
|
|
284
|
+
|
|
277
285
|
### It ships with a `ResizeObserver`, and that is not incidental
|
|
278
286
|
|
|
279
287
|
Before 0.6.0 the canvas measured the element **once at mount** and only re-measured
|
package/dist/three.cjs
CHANGED
|
@@ -312,6 +312,11 @@ function webglAvailable() {
|
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
var INTENSITY_SIZE = 46;
|
|
315
|
+
var TOUCH_MIN = 44;
|
|
316
|
+
var FS_ROW_MIN = 520 + 18 + 300;
|
|
317
|
+
function stackInFullscreen(contentWidth) {
|
|
318
|
+
return contentWidth < FS_ROW_MIN;
|
|
319
|
+
}
|
|
315
320
|
function shouldShowHint(opts) {
|
|
316
321
|
if (opts.openRegion) return false;
|
|
317
322
|
if (opts.hint === false) return false;
|
|
@@ -404,6 +409,17 @@ function BodyMap3D(props) {
|
|
|
404
409
|
onFullscreenChange?.(v);
|
|
405
410
|
};
|
|
406
411
|
const apiRef = react.useRef(null);
|
|
412
|
+
const rowRef = react.useRef(null);
|
|
413
|
+
const [rowWidth, setRowWidth] = react.useState(0);
|
|
414
|
+
react.useEffect(() => {
|
|
415
|
+
const el = rowRef.current;
|
|
416
|
+
if (!isFullscreen || !el) return;
|
|
417
|
+
setRowWidth(el.clientWidth);
|
|
418
|
+
const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => setRowWidth(el.clientWidth)) : null;
|
|
419
|
+
ro?.observe(el);
|
|
420
|
+
return () => ro?.disconnect();
|
|
421
|
+
}, [isFullscreen]);
|
|
422
|
+
const stacked = isFullscreen && stackInFullscreen(rowWidth);
|
|
407
423
|
react.useEffect(() => {
|
|
408
424
|
if (!webglAvailable()) {
|
|
409
425
|
setUnsupported(true);
|
|
@@ -782,70 +798,90 @@ function BodyMap3D(props) {
|
|
|
782
798
|
children: isFullscreen ? "\xD7" : UI.expand ?? "Expand"
|
|
783
799
|
}
|
|
784
800
|
) }),
|
|
785
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
height: isFullscreen ? "100%" : canvasH,
|
|
804
|
-
minWidth: 320,
|
|
805
|
-
minHeight: isFullscreen ? 0 : void 0,
|
|
806
|
-
borderRadius: 16,
|
|
807
|
-
overflow: "hidden",
|
|
808
|
-
background: chrome.stageBg,
|
|
809
|
-
touchAction: "none"
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
),
|
|
813
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: "1 1 300px", minWidth: 260 }, children: region ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg }, children: [
|
|
814
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
|
|
815
|
-
/* @__PURE__ */ jsxRuntime.jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
|
|
816
|
-
showRegionCode && /* @__PURE__ */ jsxRuntime.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: [
|
|
817
|
-
region.code,
|
|
818
|
-
region.side ? " \xB7 " + region.side : ""
|
|
819
|
-
] }),
|
|
820
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
821
|
-
"button",
|
|
801
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
802
|
+
"div",
|
|
803
|
+
{
|
|
804
|
+
ref: rowRef,
|
|
805
|
+
"data-layout": stacked ? "stacked" : void 0,
|
|
806
|
+
style: {
|
|
807
|
+
display: "flex",
|
|
808
|
+
gap: 18,
|
|
809
|
+
alignItems: "flex-start",
|
|
810
|
+
flexWrap: "wrap",
|
|
811
|
+
...isFullscreen ? { flex: "1 1 auto", minHeight: 0 } : null,
|
|
812
|
+
// F052.35 — a column that never wraps: the stage takes what the panel
|
|
813
|
+
// leaves, so the panel is always inside the screen.
|
|
814
|
+
...stacked ? { flexDirection: "column", flexWrap: "nowrap", alignItems: "stretch", gap: 12 } : null
|
|
815
|
+
},
|
|
816
|
+
children: [
|
|
817
|
+
unsupported ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
818
|
+
"div",
|
|
822
819
|
{
|
|
823
|
-
|
|
824
|
-
"
|
|
825
|
-
|
|
826
|
-
onClick: () => setSelected(null),
|
|
827
|
-
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 },
|
|
828
|
-
children: "\xD7"
|
|
820
|
+
"data-testid": "bodymap3d-unsupported",
|
|
821
|
+
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 },
|
|
822
|
+
children: "3D kr\xE6ver WebGL, som ikke er tilg\xE6ngeligt her."
|
|
829
823
|
}
|
|
830
|
-
)
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
824
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
825
|
+
"div",
|
|
826
|
+
{
|
|
827
|
+
ref: mountRef,
|
|
828
|
+
"data-testid": "bodymap3d-canvas",
|
|
829
|
+
style: {
|
|
830
|
+
flex: "1 1 520px",
|
|
831
|
+
// In fullscreen the box takes the height it is GIVEN rather than a
|
|
832
|
+
// fixed `canvasHeight`; the ResizeObserver above is what makes the
|
|
833
|
+
// picture follow it.
|
|
834
|
+
height: isFullscreen ? "100%" : canvasH,
|
|
835
|
+
minWidth: 320,
|
|
836
|
+
minHeight: isFullscreen ? 0 : void 0,
|
|
837
|
+
// Stacked: the stage fills what is left above the panel, and
|
|
838
|
+
// keeps a floor so the body never disappears behind it.
|
|
839
|
+
...stacked ? { flex: "1 1 0", height: "auto", minWidth: 0, minHeight: 160 } : null,
|
|
840
|
+
borderRadius: 16,
|
|
841
|
+
overflow: "hidden",
|
|
842
|
+
background: chrome.stageBg,
|
|
843
|
+
touchAction: "none"
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
),
|
|
847
|
+
/* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { style: { border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg }, children: [
|
|
848
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
|
|
849
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
|
|
850
|
+
showRegionCode && /* @__PURE__ */ jsxRuntime.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: [
|
|
851
|
+
region.code,
|
|
852
|
+
region.side ? " \xB7 " + region.side : ""
|
|
853
|
+
] }),
|
|
854
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
855
|
+
"button",
|
|
856
|
+
{
|
|
857
|
+
type: "button",
|
|
858
|
+
"data-testid": "bodymap3d-close",
|
|
859
|
+
"aria-label": L.close,
|
|
860
|
+
onClick: () => setSelected(null),
|
|
861
|
+
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 },
|
|
862
|
+
children: "\xD7"
|
|
863
|
+
}
|
|
864
|
+
)
|
|
865
|
+
] }),
|
|
866
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
|
|
867
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => (
|
|
868
|
+
/* F052.34 — CIRCLES, and all eleven the same size.
|
|
869
|
+
Dimensioned by «10», the widest label: sizing by the average
|
|
870
|
+
would make the two-digit one an ellipse among circles, which
|
|
871
|
+
is worse than eleven rounded squares because the odd one out
|
|
872
|
+
becomes a different SHAPE rather than slightly wider.
|
|
873
|
+
46px is the SIBLING's touch size, not a new number: the 2D
|
|
874
|
+
renderer already used it and this one was on 30px, under the
|
|
875
|
+
>=44px floor F052.8 set. See INTENSITY_SIZE. */
|
|
876
|
+
/* @__PURE__ */ jsxRuntime.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)
|
|
877
|
+
)) }),
|
|
878
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
|
|
879
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 14 }, children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsxRuntime.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)) }),
|
|
880
|
+
current && /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }, children: L.remove })
|
|
881
|
+
] }) : showEmptyHint && !hintAtStage ? /* @__PURE__ */ jsxRuntime.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 })
|
|
882
|
+
]
|
|
883
|
+
}
|
|
884
|
+
),
|
|
849
885
|
showEmptyHint && hintAtStage && /* F052.31 — pinned INSIDE the fullscreen surface, not in the column that
|
|
850
886
|
wraps beneath a full-height canvas. That wrap is what put the note at
|
|
851
887
|
858-912 in an 852px window on a real iPhone: entirely offscreen, and
|
|
@@ -877,13 +913,16 @@ function BodyMap3D(props) {
|
|
|
877
913
|
}
|
|
878
914
|
|
|
879
915
|
exports.BodyMap3D = BodyMap3D;
|
|
916
|
+
exports.FS_ROW_MIN = FS_ROW_MIN;
|
|
880
917
|
exports.INTENSITY_SIZE = INTENSITY_SIZE;
|
|
881
918
|
exports.TAP_MAX_DISTANCE = TAP_MAX_DISTANCE;
|
|
919
|
+
exports.TOUCH_MIN = TOUCH_MIN;
|
|
882
920
|
exports.ensureNormals = ensureNormals;
|
|
883
921
|
exports.isTap = isTap;
|
|
884
922
|
exports.seamBlend = seamBlend;
|
|
885
923
|
exports.seamWeight = seamWeight;
|
|
886
924
|
exports.serializeReport = serializeReport;
|
|
887
925
|
exports.shouldShowHint = shouldShowHint;
|
|
926
|
+
exports.stackInFullscreen = stackInFullscreen;
|
|
888
927
|
//# sourceMappingURL=three.cjs.map
|
|
889
928
|
//# sourceMappingURL=three.cjs.map
|