@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/dist/three.cjs CHANGED
@@ -183,6 +183,7 @@ var LABELS_DA = {
183
183
  intensity: "Intensitet (0-10)",
184
184
  quality: "Kvalitet",
185
185
  remove: "Fjern punkt",
186
+ close: "Luk",
186
187
  front: "Forfra",
187
188
  back: "Bagfra",
188
189
  empty: "V\xE6lg en kropsdel for at markere smerte.",
@@ -205,6 +206,7 @@ var LABELS_EN = {
205
206
  intensity: "Intensity (0-10)",
206
207
  quality: "Quality",
207
208
  remove: "Remove point",
209
+ close: "Close",
208
210
  front: "Front",
209
211
  back: "Back",
210
212
  empty: "Pick a body part to mark pain.",
@@ -221,8 +223,8 @@ var LABELS_EN = {
221
223
  zoomOut: "Zoom out",
222
224
  zoomReset: "Reset zoom"
223
225
  };
224
- var UI_DA = { male: "Mand", female: "Kvinde", hoverHint: "Hover for at fremh\xE6ve \xB7 klik en kropsdel for at markere smerte." };
225
- var UI_EN = { male: "Male", female: "Female", hoverHint: "Hover to highlight \xB7 tap a body part to mark pain." };
226
+ 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" };
227
+ var UI_EN = { male: "Male", female: "Female", hoverHint: "Hover to highlight \xB7 tap a body part to mark pain.", expand: "Expand", collapse: "Close" };
226
228
  var ANCHORS = {
227
229
  head: [0, 1.79, 0.02],
228
230
  neck: [0, 1.57, 0],
@@ -309,6 +311,9 @@ function BodyMap3D(props) {
309
311
  onFeedback,
310
312
  haptics,
311
313
  seam = false,
314
+ fullscreen: fullscreenProp,
315
+ onFullscreenChange,
316
+ showFullscreenButton = true,
312
317
  className
313
318
  } = props;
314
319
  const chrome = uiColors(palette);
@@ -348,6 +353,12 @@ function BodyMap3D(props) {
348
353
  setReadyRef.current = setReady;
349
354
  const seamRef = react.useRef(seam);
350
355
  seamRef.current = seam;
356
+ const [internalFs, setInternalFs] = react.useState(false);
357
+ const isFullscreen = fullscreenProp ?? internalFs;
358
+ const setFullscreen = (v) => {
359
+ if (fullscreenProp === void 0) setInternalFs(v);
360
+ onFullscreenChange?.(v);
361
+ };
351
362
  const apiRef = react.useRef(null);
352
363
  react.useEffect(() => {
353
364
  if (!webglAvailable()) {
@@ -597,8 +608,11 @@ function BodyMap3D(props) {
597
608
  renderFrame();
598
609
  };
599
610
  window.addEventListener("resize", onResize);
611
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => onResize()) : null;
612
+ ro?.observe(el);
600
613
  return () => {
601
614
  cancelAnimationFrame(raf);
615
+ ro?.disconnect();
602
616
  window.removeEventListener("resize", onResize);
603
617
  document.removeEventListener("visibilitychange", onVis);
604
618
  controls.removeEventListener("change", kick);
@@ -610,6 +624,16 @@ function BodyMap3D(props) {
610
624
  if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
611
625
  };
612
626
  }, []);
627
+ react.useEffect(() => {
628
+ if (!isFullscreen && !selected) return;
629
+ const onKey = (e) => {
630
+ if (e.key !== "Escape") return;
631
+ if (selected) setSelected(null);
632
+ else setFullscreen(false);
633
+ };
634
+ window.addEventListener("keydown", onKey);
635
+ return () => window.removeEventListener("keydown", onKey);
636
+ });
613
637
  react.useEffect(() => {
614
638
  apiRef.current?.refresh();
615
639
  }, [selected, report, palette, seam]);
@@ -644,31 +668,98 @@ function BodyMap3D(props) {
644
668
  const current = selected ? pointOf(selected) : void 0;
645
669
  const anySelectable = REGIONS.some((r) => isSelectable(r.key, config ?? {}));
646
670
  const showEmptyHint = anySelectable || ui?.hoverHint !== void 0;
647
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "bodymap3d-root", className, style: { fontFamily: "system-ui, sans-serif", color: "#1e293b" }, children: [
648
- showSexToggle && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", marginBottom: 12, fontSize: 13 }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
649
- /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(sex === "male"), children: UI.male }),
650
- /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(sex === "female"), children: UI.female })
651
- ] }) }),
652
- /* @__PURE__ */ jsxRuntime.jsx("span", { ref: loadedRef, "data-testid": "bodymap3d-loaded", style: { display: "none" } }),
653
- ready && /* @__PURE__ */ jsxRuntime.jsx("span", { "data-testid": "bodymap3d-ready", style: { position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" } }),
654
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap" }, children: [
655
- unsupported ? /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("div", { ref: mountRef, "data-testid": "bodymap3d-canvas", style: { flex: "1 1 520px", height: canvasH, minWidth: 320, borderRadius: 16, overflow: "hidden", background: "#0e1424", touchAction: "none" } }),
656
- /* @__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: [
657
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
658
- /* @__PURE__ */ jsxRuntime.jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
659
- 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: [
660
- region.code,
661
- region.side ? " \xB7 " + region.side : ""
662
- ] })
663
- ] }),
664
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
665
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => /* @__PURE__ */ jsxRuntime.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)) }),
666
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
667
- /* @__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, borderRadius: 999, padding: "6px 12px", background: current?.type === t ? "#1e293b" : "#fff", color: current?.type === t ? "#fff" : "#64748b" }, children: L.qualities[t] ?? t }, t)) }),
668
- current && /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn, color: chrome.danger, borderColor: "#f6c9c9" }, children: L.remove })
669
- ] }) : showEmptyHint ? /* @__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: UI.hoverHint }) : null })
670
- ] })
671
- ] });
671
+ return /* @__PURE__ */ jsxRuntime.jsxs(
672
+ "div",
673
+ {
674
+ "data-testid": "bodymap3d-root",
675
+ className,
676
+ "data-fullscreen": isFullscreen ? "true" : void 0,
677
+ style: {
678
+ fontFamily: "system-ui, sans-serif",
679
+ color: "#1e293b",
680
+ // A viewport fill, not the Fullscreen API see the `fullscreen` prop.
681
+ // `fixed`+`inset:0` behaves the same on iOS Safari, where element
682
+ // fullscreen does not exist at all, as it does everywhere else.
683
+ ...isFullscreen ? {
684
+ position: "fixed",
685
+ inset: 0,
686
+ zIndex: 2147483e3,
687
+ background: chrome.panelBg,
688
+ padding: 12,
689
+ display: "flex",
690
+ flexDirection: "column",
691
+ overflow: "auto"
692
+ } : null
693
+ },
694
+ children: [
695
+ showSexToggle && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", marginBottom: 12, fontSize: 13 }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
696
+ /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(sex === "male"), children: UI.male }),
697
+ /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(sex === "female"), children: UI.female })
698
+ ] }) }),
699
+ /* @__PURE__ */ jsxRuntime.jsx("span", { ref: loadedRef, "data-testid": "bodymap3d-loaded", style: { display: "none" } }),
700
+ ready && /* @__PURE__ */ jsxRuntime.jsx("span", { "data-testid": "bodymap3d-ready", style: { position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" } }),
701
+ showFullscreenButton && !unsupported && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginBottom: 8 }, children: /* @__PURE__ */ jsxRuntime.jsx(
702
+ "button",
703
+ {
704
+ type: "button",
705
+ "data-testid": "bodymap3d-fullscreen",
706
+ "aria-pressed": isFullscreen,
707
+ "aria-label": isFullscreen ? UI.collapse ?? "Close" : UI.expand ?? "Expand",
708
+ onClick: () => setFullscreen(!isFullscreen),
709
+ style: { ...btn, color: chrome.text, borderColor: chrome.border, background: chrome.panelBg },
710
+ children: isFullscreen ? UI.collapse ?? "Close" : UI.expand ?? "Expand"
711
+ }
712
+ ) }),
713
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap", ...isFullscreen ? { flex: "1 1 auto", minHeight: 0 } : null }, children: [
714
+ unsupported ? /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
715
+ "div",
716
+ {
717
+ ref: mountRef,
718
+ "data-testid": "bodymap3d-canvas",
719
+ style: {
720
+ flex: "1 1 520px",
721
+ // In fullscreen the box takes the height it is GIVEN rather than a
722
+ // fixed `canvasHeight`; the ResizeObserver above is what makes the
723
+ // picture follow it.
724
+ height: isFullscreen ? "100%" : canvasH,
725
+ minWidth: 320,
726
+ minHeight: isFullscreen ? 0 : void 0,
727
+ borderRadius: 16,
728
+ overflow: "hidden",
729
+ background: "#0e1424",
730
+ touchAction: "none"
731
+ }
732
+ }
733
+ ),
734
+ /* @__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: [
735
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
736
+ /* @__PURE__ */ jsxRuntime.jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
737
+ 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: [
738
+ region.code,
739
+ region.side ? " \xB7 " + region.side : ""
740
+ ] }),
741
+ /* @__PURE__ */ jsxRuntime.jsx(
742
+ "button",
743
+ {
744
+ type: "button",
745
+ "data-testid": "bodymap3d-close",
746
+ "aria-label": L.close,
747
+ onClick: () => setSelected(null),
748
+ style: { ...btn, width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
749
+ children: "\xD7"
750
+ }
751
+ )
752
+ ] }),
753
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
754
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => /* @__PURE__ */ jsxRuntime.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)) }),
755
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
756
+ /* @__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, borderRadius: 999, padding: "6px 12px", background: current?.type === t ? "#1e293b" : "#fff", color: current?.type === t ? "#fff" : "#64748b" }, children: L.qualities[t] ?? t }, t)) }),
757
+ current && /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn, color: chrome.danger, borderColor: "#f6c9c9" }, children: L.remove })
758
+ ] }) : showEmptyHint ? /* @__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: UI.hoverHint }) : null })
759
+ ] })
760
+ ]
761
+ }
762
+ );
672
763
  }
673
764
 
674
765
  exports.BodyMap3D = BodyMap3D;