@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/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("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap", ...isFullscreen ? { flex: "1 1 auto", minHeight: 0 } : null }, children: [
764
- unsupported ? /* @__PURE__ */ jsx(
765
- "div",
766
- {
767
- "data-testid": "bodymap3d-unsupported",
768
- 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 },
769
- children: "3D kr\xE6ver WebGL, som ikke er tilg\xE6ngeligt her."
770
- }
771
- ) : /* @__PURE__ */ jsx(
772
- "div",
773
- {
774
- ref: mountRef,
775
- "data-testid": "bodymap3d-canvas",
776
- style: {
777
- flex: "1 1 520px",
778
- // In fullscreen the box takes the height it is GIVEN rather than a
779
- // fixed `canvasHeight`; the ResizeObserver above is what makes the
780
- // picture follow it.
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
- type: "button",
802
- "data-testid": "bodymap3d-close",
803
- "aria-label": L.close,
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
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
811
- /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => (
812
- /* F052.34 — CIRCLES, and all eleven the same size.
813
- Dimensioned by «10», the widest label: sizing by the average
814
- would make the two-digit one an ellipse among circles, which
815
- is worse than eleven rounded squares because the odd one out
816
- becomes a different SHAPE rather than slightly wider.
817
- 46px is the SIBLING's touch size, not a new number: the 2D
818
- renderer already used it and this one was on 30px, under the
819
- >=44px floor F052.8 set. See INTENSITY_SIZE. */
820
- /* @__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)
821
- )) }),
822
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
823
- /* @__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)) }),
824
- current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }, children: L.remove })
825
- ] }) : 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 })
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