@broberg/bodymap 0.13.3 → 0.15.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
@@ -404,6 +404,18 @@ declare const INTENSITY_SIZE = 46;
404
404
  * takes exactly this. INTENSITY_SIZE sits above it.
405
405
  */
406
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;
407
419
  /**
408
420
  * F052.33 — should the help note be on screen at all?
409
421
  *
@@ -424,4 +436,4 @@ declare function shouldShowHint(opts: {
424
436
  }): boolean;
425
437
  declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
426
438
 
427
- export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, INTENSITY_SIZE, TAP_MAX_DISTANCE, TOUCH_MIN, 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
@@ -404,6 +404,18 @@ declare const INTENSITY_SIZE = 46;
404
404
  * takes exactly this. INTENSITY_SIZE sits above it.
405
405
  */
406
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;
407
419
  /**
408
420
  * F052.33 — should the help note be on screen at all?
409
421
  *
@@ -424,4 +436,4 @@ declare function shouldShowHint(opts: {
424
436
  }): boolean;
425
437
  declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
426
438
 
427
- export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, INTENSITY_SIZE, TAP_MAX_DISTANCE, TOUCH_MIN, 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
@@ -291,6 +291,10 @@ function webglAvailable() {
291
291
  }
292
292
  var INTENSITY_SIZE = 46;
293
293
  var TOUCH_MIN = 44;
294
+ var FS_ROW_MIN = 520 + 18 + 300;
295
+ function stackInFullscreen(contentWidth) {
296
+ return contentWidth < FS_ROW_MIN;
297
+ }
294
298
  function shouldShowHint(opts) {
295
299
  if (opts.openRegion) return false;
296
300
  if (opts.hint === false) return false;
@@ -383,6 +387,17 @@ function BodyMap3D(props) {
383
387
  onFullscreenChange?.(v);
384
388
  };
385
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);
386
401
  useEffect(() => {
387
402
  if (!webglAvailable()) {
388
403
  setUnsupported(true);
@@ -761,70 +776,90 @@ function BodyMap3D(props) {
761
776
  children: isFullscreen ? "\xD7" : UI.expand ?? "Expand"
762
777
  }
763
778
  ) }),
764
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap", ...isFullscreen ? { flex: "1 1 auto", minHeight: 0 } : null }, children: [
765
- unsupported ? /* @__PURE__ */ jsx(
766
- "div",
767
- {
768
- "data-testid": "bodymap3d-unsupported",
769
- 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 },
770
- children: "3D kr\xE6ver WebGL, som ikke er tilg\xE6ngeligt her."
771
- }
772
- ) : /* @__PURE__ */ jsx(
773
- "div",
774
- {
775
- ref: mountRef,
776
- "data-testid": "bodymap3d-canvas",
777
- style: {
778
- flex: "1 1 520px",
779
- // In fullscreen the box takes the height it is GIVEN rather than a
780
- // fixed `canvasHeight`; the ResizeObserver above is what makes the
781
- // picture follow it.
782
- height: isFullscreen ? "100%" : canvasH,
783
- minWidth: 320,
784
- minHeight: isFullscreen ? 0 : void 0,
785
- borderRadius: 16,
786
- overflow: "hidden",
787
- background: chrome.stageBg,
788
- touchAction: "none"
789
- }
790
- }
791
- ),
792
- /* @__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: [
793
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
794
- /* @__PURE__ */ jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
795
- 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: [
796
- region.code,
797
- region.side ? " \xB7 " + region.side : ""
798
- ] }),
799
- /* @__PURE__ */ jsx(
800
- "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",
801
797
  {
802
- type: "button",
803
- "data-testid": "bodymap3d-close",
804
- "aria-label": L.close,
805
- onClick: () => setSelected(null),
806
- 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 },
807
- 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."
808
801
  }
809
- )
810
- ] }),
811
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
812
- /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => (
813
- /* F052.34 — CIRCLES, and all eleven the same size.
814
- Dimensioned by «10», the widest label: sizing by the average
815
- would make the two-digit one an ellipse among circles, which
816
- is worse than eleven rounded squares because the odd one out
817
- becomes a different SHAPE rather than slightly wider.
818
- 46px is the SIBLING's touch size, not a new number: the 2D
819
- renderer already used it and this one was on 30px, under the
820
- >=44px floor F052.8 set. See INTENSITY_SIZE. */
821
- /* @__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)
822
- )) }),
823
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
824
- /* @__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)) }),
825
- current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }, children: L.remove })
826
- ] }) : 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 })
827
- ] }),
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
+ ),
828
863
  showEmptyHint && hintAtStage && /* F052.31 — pinned INSIDE the fullscreen surface, not in the column that
829
864
  wraps beneath a full-height canvas. That wrap is what put the note at
830
865
  858-912 in an 852px window on a real iPhone: entirely offscreen, and
@@ -855,6 +890,6 @@ function BodyMap3D(props) {
855
890
  );
856
891
  }
857
892
 
858
- export { BodyMap3D, INTENSITY_SIZE, TAP_MAX_DISTANCE, TOUCH_MIN, 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 };
859
894
  //# sourceMappingURL=three.js.map
860
895
  //# sourceMappingURL=three.js.map