@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.d.cts CHANGED
@@ -134,6 +134,8 @@ interface BodyMapLabels {
134
134
  intensity: string;
135
135
  quality: string;
136
136
  remove: string;
137
+ /** Close the picker without marking anything (F052.26). */
138
+ close: string;
137
139
  front: string;
138
140
  back: string;
139
141
  empty: string;
@@ -168,6 +170,10 @@ interface BodyMap3DModels {
168
170
  }
169
171
  /** The 3D-only control strings (the shared labels come from `locale`/`labels`). */
170
172
  interface BodyMap3DUiLabels {
173
+ /** Label for the built-in fullscreen control (F052.24). */
174
+ expand?: string;
175
+ /** Label for the same control while fullscreen. */
176
+ collapse?: string;
171
177
  male: string;
172
178
  female: string;
173
179
  hoverHint: string;
@@ -216,6 +222,19 @@ interface BodyMap3DProps {
216
222
  * (default true). Set false for a patient/employee-facing flow where the code is
217
223
  * internal jargon and the readable region name is enough. */
218
224
  showRegionCode?: boolean;
225
+ /** Fill the viewport with the body (F052.24). Controlled: pass it and you own
226
+ * it; `onFullscreenChange` still fires so your own button can drive it.
227
+ *
228
+ * This is a VIEWPORT FILL, not the browser Fullscreen API. iOS Safari does not
229
+ * support fullscreen on an arbitrary element at all, and the iPhone is this
230
+ * component's primary surface — so the mechanism that works everywhere is the
231
+ * one that ships, and it behaves identically on every platform. */
232
+ fullscreen?: boolean;
233
+ /** Fired when the built-in control or the Escape key changes it. */
234
+ onFullscreenChange?: (fullscreen: boolean) => void;
235
+ /** Show the built-in expand/collapse control (default true). Set false and
236
+ * drive `fullscreen` from your own button. */
237
+ showFullscreenButton?: boolean;
219
238
  /** Soften the colour transition between neighbouring regions (F052.21).
220
239
  *
221
240
  * DEFAULT FALSE, and the default is the accessible one (F052.23). The blend
package/dist/three.d.ts CHANGED
@@ -134,6 +134,8 @@ interface BodyMapLabels {
134
134
  intensity: string;
135
135
  quality: string;
136
136
  remove: string;
137
+ /** Close the picker without marking anything (F052.26). */
138
+ close: string;
137
139
  front: string;
138
140
  back: string;
139
141
  empty: string;
@@ -168,6 +170,10 @@ interface BodyMap3DModels {
168
170
  }
169
171
  /** The 3D-only control strings (the shared labels come from `locale`/`labels`). */
170
172
  interface BodyMap3DUiLabels {
173
+ /** Label for the built-in fullscreen control (F052.24). */
174
+ expand?: string;
175
+ /** Label for the same control while fullscreen. */
176
+ collapse?: string;
171
177
  male: string;
172
178
  female: string;
173
179
  hoverHint: string;
@@ -216,6 +222,19 @@ interface BodyMap3DProps {
216
222
  * (default true). Set false for a patient/employee-facing flow where the code is
217
223
  * internal jargon and the readable region name is enough. */
218
224
  showRegionCode?: boolean;
225
+ /** Fill the viewport with the body (F052.24). Controlled: pass it and you own
226
+ * it; `onFullscreenChange` still fires so your own button can drive it.
227
+ *
228
+ * This is a VIEWPORT FILL, not the browser Fullscreen API. iOS Safari does not
229
+ * support fullscreen on an arbitrary element at all, and the iPhone is this
230
+ * component's primary surface — so the mechanism that works everywhere is the
231
+ * one that ships, and it behaves identically on every platform. */
232
+ fullscreen?: boolean;
233
+ /** Fired when the built-in control or the Escape key changes it. */
234
+ onFullscreenChange?: (fullscreen: boolean) => void;
235
+ /** Show the built-in expand/collapse control (default true). Set false and
236
+ * drive `fullscreen` from your own button. */
237
+ showFullscreenButton?: boolean;
219
238
  /** Soften the colour transition between neighbouring regions (F052.21).
220
239
  *
221
240
  * DEFAULT FALSE, and the default is the accessible one (F052.23). The blend
package/dist/three.js CHANGED
@@ -161,6 +161,7 @@ var LABELS_DA = {
161
161
  intensity: "Intensitet (0-10)",
162
162
  quality: "Kvalitet",
163
163
  remove: "Fjern punkt",
164
+ close: "Luk",
164
165
  front: "Forfra",
165
166
  back: "Bagfra",
166
167
  empty: "V\xE6lg en kropsdel for at markere smerte.",
@@ -183,6 +184,7 @@ var LABELS_EN = {
183
184
  intensity: "Intensity (0-10)",
184
185
  quality: "Quality",
185
186
  remove: "Remove point",
187
+ close: "Close",
186
188
  front: "Front",
187
189
  back: "Back",
188
190
  empty: "Pick a body part to mark pain.",
@@ -199,8 +201,8 @@ var LABELS_EN = {
199
201
  zoomOut: "Zoom out",
200
202
  zoomReset: "Reset zoom"
201
203
  };
202
- var UI_DA = { male: "Mand", female: "Kvinde", hoverHint: "Hover for at fremh\xE6ve \xB7 klik en kropsdel for at markere smerte." };
203
- var UI_EN = { male: "Male", female: "Female", hoverHint: "Hover to highlight \xB7 tap a body part to mark pain." };
204
+ 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" };
205
+ var UI_EN = { male: "Male", female: "Female", hoverHint: "Hover to highlight \xB7 tap a body part to mark pain.", expand: "Expand", collapse: "Close" };
204
206
  var ANCHORS = {
205
207
  head: [0, 1.79, 0.02],
206
208
  neck: [0, 1.57, 0],
@@ -287,6 +289,9 @@ function BodyMap3D(props) {
287
289
  onFeedback,
288
290
  haptics,
289
291
  seam = false,
292
+ fullscreen: fullscreenProp,
293
+ onFullscreenChange,
294
+ showFullscreenButton = true,
290
295
  className
291
296
  } = props;
292
297
  const chrome = uiColors(palette);
@@ -326,6 +331,12 @@ function BodyMap3D(props) {
326
331
  setReadyRef.current = setReady;
327
332
  const seamRef = useRef(seam);
328
333
  seamRef.current = seam;
334
+ const [internalFs, setInternalFs] = useState(false);
335
+ const isFullscreen = fullscreenProp ?? internalFs;
336
+ const setFullscreen = (v) => {
337
+ if (fullscreenProp === void 0) setInternalFs(v);
338
+ onFullscreenChange?.(v);
339
+ };
329
340
  const apiRef = useRef(null);
330
341
  useEffect(() => {
331
342
  if (!webglAvailable()) {
@@ -575,8 +586,11 @@ function BodyMap3D(props) {
575
586
  renderFrame();
576
587
  };
577
588
  window.addEventListener("resize", onResize);
589
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => onResize()) : null;
590
+ ro?.observe(el);
578
591
  return () => {
579
592
  cancelAnimationFrame(raf);
593
+ ro?.disconnect();
580
594
  window.removeEventListener("resize", onResize);
581
595
  document.removeEventListener("visibilitychange", onVis);
582
596
  controls.removeEventListener("change", kick);
@@ -588,6 +602,16 @@ function BodyMap3D(props) {
588
602
  if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
589
603
  };
590
604
  }, []);
605
+ useEffect(() => {
606
+ if (!isFullscreen && !selected) return;
607
+ const onKey = (e) => {
608
+ if (e.key !== "Escape") return;
609
+ if (selected) setSelected(null);
610
+ else setFullscreen(false);
611
+ };
612
+ window.addEventListener("keydown", onKey);
613
+ return () => window.removeEventListener("keydown", onKey);
614
+ });
591
615
  useEffect(() => {
592
616
  apiRef.current?.refresh();
593
617
  }, [selected, report, palette, seam]);
@@ -622,31 +646,98 @@ function BodyMap3D(props) {
622
646
  const current = selected ? pointOf(selected) : void 0;
623
647
  const anySelectable = REGIONS.some((r) => isSelectable(r.key, config ?? {}));
624
648
  const showEmptyHint = anySelectable || ui?.hoverHint !== void 0;
625
- return /* @__PURE__ */ jsxs("div", { "data-testid": "bodymap3d-root", className, style: { fontFamily: "system-ui, sans-serif", color: "#1e293b" }, children: [
626
- showSexToggle && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", marginBottom: 12, fontSize: 13 }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
627
- /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(sex === "male"), children: UI.male }),
628
- /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(sex === "female"), children: UI.female })
629
- ] }) }),
630
- /* @__PURE__ */ jsx("span", { ref: loadedRef, "data-testid": "bodymap3d-loaded", style: { display: "none" } }),
631
- ready && /* @__PURE__ */ jsx("span", { "data-testid": "bodymap3d-ready", style: { position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" } }),
632
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap" }, children: [
633
- unsupported ? /* @__PURE__ */ 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__ */ 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" } }),
634
- /* @__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: [
635
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
636
- /* @__PURE__ */ jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
637
- 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: [
638
- region.code,
639
- region.side ? " \xB7 " + region.side : ""
640
- ] })
641
- ] }),
642
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
643
- /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => /* @__PURE__ */ 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)) }),
644
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
645
- /* @__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, borderRadius: 999, padding: "6px 12px", background: current?.type === t ? "#1e293b" : "#fff", color: current?.type === t ? "#fff" : "#64748b" }, children: L.qualities[t] ?? t }, t)) }),
646
- current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn, color: chrome.danger, borderColor: "#f6c9c9" }, children: L.remove })
647
- ] }) : showEmptyHint ? /* @__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: UI.hoverHint }) : null })
648
- ] })
649
- ] });
649
+ return /* @__PURE__ */ jsxs(
650
+ "div",
651
+ {
652
+ "data-testid": "bodymap3d-root",
653
+ className,
654
+ "data-fullscreen": isFullscreen ? "true" : void 0,
655
+ style: {
656
+ fontFamily: "system-ui, sans-serif",
657
+ color: "#1e293b",
658
+ // A viewport fill, not the Fullscreen API see the `fullscreen` prop.
659
+ // `fixed`+`inset:0` behaves the same on iOS Safari, where element
660
+ // fullscreen does not exist at all, as it does everywhere else.
661
+ ...isFullscreen ? {
662
+ position: "fixed",
663
+ inset: 0,
664
+ zIndex: 2147483e3,
665
+ background: chrome.panelBg,
666
+ padding: 12,
667
+ display: "flex",
668
+ flexDirection: "column",
669
+ overflow: "auto"
670
+ } : null
671
+ },
672
+ children: [
673
+ showSexToggle && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", marginBottom: 12, fontSize: 13 }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
674
+ /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(sex === "male"), children: UI.male }),
675
+ /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(sex === "female"), children: UI.female })
676
+ ] }) }),
677
+ /* @__PURE__ */ jsx("span", { ref: loadedRef, "data-testid": "bodymap3d-loaded", style: { display: "none" } }),
678
+ ready && /* @__PURE__ */ jsx("span", { "data-testid": "bodymap3d-ready", style: { position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" } }),
679
+ showFullscreenButton && !unsupported && /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginBottom: 8 }, children: /* @__PURE__ */ jsx(
680
+ "button",
681
+ {
682
+ type: "button",
683
+ "data-testid": "bodymap3d-fullscreen",
684
+ "aria-pressed": isFullscreen,
685
+ "aria-label": isFullscreen ? UI.collapse ?? "Close" : UI.expand ?? "Expand",
686
+ onClick: () => setFullscreen(!isFullscreen),
687
+ style: { ...btn, color: chrome.text, borderColor: chrome.border, background: chrome.panelBg },
688
+ children: isFullscreen ? UI.collapse ?? "Close" : UI.expand ?? "Expand"
689
+ }
690
+ ) }),
691
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap", ...isFullscreen ? { flex: "1 1 auto", minHeight: 0 } : null }, children: [
692
+ unsupported ? /* @__PURE__ */ 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__ */ jsx(
693
+ "div",
694
+ {
695
+ ref: mountRef,
696
+ "data-testid": "bodymap3d-canvas",
697
+ style: {
698
+ flex: "1 1 520px",
699
+ // In fullscreen the box takes the height it is GIVEN rather than a
700
+ // fixed `canvasHeight`; the ResizeObserver above is what makes the
701
+ // picture follow it.
702
+ height: isFullscreen ? "100%" : canvasH,
703
+ minWidth: 320,
704
+ minHeight: isFullscreen ? 0 : void 0,
705
+ borderRadius: 16,
706
+ overflow: "hidden",
707
+ background: "#0e1424",
708
+ touchAction: "none"
709
+ }
710
+ }
711
+ ),
712
+ /* @__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: [
713
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
714
+ /* @__PURE__ */ jsx("b", { style: { fontSize: 16 }, children: nameOf(region.key) }),
715
+ 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: [
716
+ region.code,
717
+ region.side ? " \xB7 " + region.side : ""
718
+ ] }),
719
+ /* @__PURE__ */ jsx(
720
+ "button",
721
+ {
722
+ type: "button",
723
+ "data-testid": "bodymap3d-close",
724
+ "aria-label": L.close,
725
+ onClick: () => setSelected(null),
726
+ style: { ...btn, width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
727
+ children: "\xD7"
728
+ }
729
+ )
730
+ ] }),
731
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
732
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => /* @__PURE__ */ 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)) }),
733
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
734
+ /* @__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, borderRadius: 999, padding: "6px 12px", background: current?.type === t ? "#1e293b" : "#fff", color: current?.type === t ? "#fff" : "#64748b" }, children: L.qualities[t] ?? t }, t)) }),
735
+ current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn, color: chrome.danger, borderColor: "#f6c9c9" }, children: L.remove })
736
+ ] }) : showEmptyHint ? /* @__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: UI.hoverHint }) : null })
737
+ ] })
738
+ ]
739
+ }
740
+ );
650
741
  }
651
742
 
652
743
  export { BodyMap3D, seamBlend, serializeReport };