@mmtitanl/tablets-core 0.3.0 → 0.4.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/index.cjs CHANGED
@@ -691,6 +691,14 @@ function ensureBuiltinsRegistered() {
691
691
  }
692
692
  didAutoRegister = true;
693
693
  }
694
+ var ROTATE_MS = 450;
695
+ var EASE = "cubic-bezier(0.4, 0, 0.2, 1)";
696
+ var SVG_FADE_MS = 240;
697
+ var SVG_FADE_DELAY_MS = 100;
698
+ var CONTENT_FADE_OUT_MS = 140;
699
+ var CONTENT_SWAP_MS = 180;
700
+ var CONTENT_FADE_IN_DELAY_MS = 220;
701
+ var CONTENT_FADE_IN_MS = 230;
694
702
  function sendDeviceInfo(iframe, contract, orientation) {
695
703
  if (!iframe?.contentWindow) return;
696
704
  iframe.contentWindow.postMessage(
@@ -728,7 +736,24 @@ function DeviceFrame({
728
736
  const resolvedId = device ?? deviceId;
729
737
  if (!resolvedId) throw new Error("DeviceFrame requires `device` or `deviceId`");
730
738
  const meta = (0, import_tablets3.getDeviceMetadata)(resolvedId);
731
- const contract = (0, import_react11.useMemo)(() => (0, import_tablets3.getDeviceContract)(resolvedId, orientation), [resolvedId, orientation]);
739
+ const [contentOrientation, setContentOrientation] = (0, import_react11.useState)(orientation);
740
+ const [contentVisible, setContentVisible] = (0, import_react11.useState)(true);
741
+ const animTimers = (0, import_react11.useRef)([]);
742
+ const prevOrientation = (0, import_react11.useRef)(orientation);
743
+ (0, import_react11.useEffect)(() => {
744
+ if (prevOrientation.current === orientation) return;
745
+ prevOrientation.current = orientation;
746
+ for (const t of animTimers.current) window.clearTimeout(t);
747
+ setContentVisible(false);
748
+ animTimers.current = [
749
+ window.setTimeout(() => setContentOrientation(orientation), CONTENT_SWAP_MS),
750
+ window.setTimeout(() => setContentVisible(true), CONTENT_FADE_IN_DELAY_MS)
751
+ ];
752
+ }, [orientation]);
753
+ (0, import_react11.useEffect)(() => () => {
754
+ for (const t of animTimers.current) window.clearTimeout(t);
755
+ }, []);
756
+ const contract = (0, import_react11.useMemo)(() => (0, import_tablets3.getDeviceContract)(resolvedId, contentOrientation), [resolvedId, contentOrientation]);
732
757
  const portW = meta.screen.width;
733
758
  const portH = meta.screen.height;
734
759
  const rotateFrame = orientation === "landscape";
@@ -737,12 +762,36 @@ function DeviceFrame({
737
762
  const sentinelRef = (0, import_react11.useRef)(null);
738
763
  const frameContainerRef = (0, import_react11.useRef)(null);
739
764
  const containerSize = useContainerSize(sentinelRef);
740
- const svgEntryEarly = getDeviceSVG(resolvedId);
741
- const portraitFrameEarly = svgEntryEarly?.frame;
742
- const landscapeFrameEarly = svgEntryEarly?.landscapeFrame;
743
- const hasLandscapeSVGEarly = !!svgEntryEarly?.landscapeComponent && !!landscapeFrameEarly;
744
- const fitW = portraitFrameEarly ? rotateFrame ? hasLandscapeSVGEarly ? landscapeFrameEarly.totalWidth : portraitFrameEarly.totalHeight : portraitFrameEarly.totalWidth : rotateFrame ? portH : portW;
745
- const fitH = portraitFrameEarly ? rotateFrame ? hasLandscapeSVGEarly ? landscapeFrameEarly.totalHeight : portraitFrameEarly.totalWidth : portraitFrameEarly.totalHeight : rotateFrame ? portW : portH;
765
+ const svgEntry = getDeviceSVG(resolvedId);
766
+ const SVGComponent = svgEntry?.component ?? null;
767
+ const LandscapeSVGComponent = svgEntry?.landscapeComponent ?? null;
768
+ const registeredLandscapeFrame = svgEntry?.landscapeFrame;
769
+ const hasLandscapeSVG = !!LandscapeSVGComponent && !!registeredLandscapeFrame;
770
+ const portFrame = svgEntry?.frame ?? {
771
+ bezelTop: 0,
772
+ bezelBottom: 0,
773
+ bezelLeft: 0,
774
+ bezelRight: 0,
775
+ totalWidth: portW,
776
+ totalHeight: portH,
777
+ screenWidth: portW,
778
+ screenHeight: portH,
779
+ screenRadius: 0
780
+ };
781
+ const landFrame = hasLandscapeSVG && registeredLandscapeFrame ? registeredLandscapeFrame : {
782
+ bezelTop: portFrame.totalWidth - portFrame.bezelLeft - portFrame.screenWidth,
783
+ bezelBottom: portFrame.bezelLeft,
784
+ bezelLeft: portFrame.bezelTop,
785
+ bezelRight: portFrame.totalHeight - portFrame.bezelTop - portFrame.screenHeight,
786
+ totalWidth: portFrame.totalHeight,
787
+ totalHeight: portFrame.totalWidth,
788
+ screenWidth: portFrame.screenHeight,
789
+ screenHeight: portFrame.screenWidth,
790
+ screenRadius: portFrame.screenRadius
791
+ };
792
+ const activeFrame = rotateFrame ? landFrame : portFrame;
793
+ const fitW = activeFrame.totalWidth;
794
+ const fitH = activeFrame.totalHeight;
746
795
  const fitResult = (0, import_react11.useMemo)(
747
796
  () => computeFullScale(fitW, fitH, containerSize.width, containerSize.height, {
748
797
  snapToSteps: scaleMode === "steps"
@@ -765,41 +814,32 @@ function DeviceFrame({
765
814
  }, [scale, onScaleChange]);
766
815
  (0, import_react11.useEffect)(() => {
767
816
  if (!iframeRef?.current) return;
768
- sendDeviceInfo(iframeRef.current, contract, orientation);
769
- const onLoad = () => sendDeviceInfo(iframeRef.current, contract, orientation);
817
+ sendDeviceInfo(iframeRef.current, contract, contentOrientation);
818
+ const onLoad = () => sendDeviceInfo(iframeRef.current, contract, contentOrientation);
770
819
  iframeRef.current.addEventListener("load", onLoad);
771
820
  return () => iframeRef.current?.removeEventListener("load", onLoad);
772
- }, [iframeRef, contract, orientation]);
821
+ }, [iframeRef, contract, contentOrientation]);
773
822
  (0, import_react11.useEffect)(() => {
774
823
  if (!iframeRef) return;
775
824
  const handler = (event) => {
776
825
  const data = event.data;
777
826
  if (!data || typeof data !== "object") return;
778
827
  if (data.type === "biela:requestDeviceInfo") {
779
- sendDeviceInfo(iframeRef.current, contract, orientation);
828
+ sendDeviceInfo(iframeRef.current, contract, contentOrientation);
780
829
  } else if (data.type === "biela:colorScheme" && data.payload?.scheme) {
781
830
  onColorSchemeChange?.(data.payload.scheme);
782
831
  }
783
832
  };
784
833
  window.addEventListener("message", handler);
785
834
  return () => window.removeEventListener("message", handler);
786
- }, [iframeRef, contract, orientation, onColorSchemeChange]);
787
- const svgEntry = getDeviceSVG(resolvedId);
788
- const SVGComponent = svgEntry?.component ?? null;
789
- const portraitFrame = svgEntry?.frame;
790
- const LandscapeSVGComponent = svgEntry?.landscapeComponent ?? null;
791
- const landscapeFrame = svgEntry?.landscapeFrame;
792
- const hasLandscapeSVG = !!LandscapeSVGComponent && !!landscapeFrame;
835
+ }, [iframeRef, contract, contentOrientation, onColorSchemeChange]);
793
836
  const cssVarsStyle = contract.cssVariables;
794
- const activeFrame = hasLandscapeSVG && rotateFrame ? landscapeFrame : portraitFrame;
795
- const scalerW = hasLandscapeSVG ? Math.max(portraitFrame?.totalWidth ?? dw, landscapeFrame?.totalWidth ?? dh) : activeFrame?.totalWidth ?? (rotateFrame ? portW : dw);
796
- const scalerH = hasLandscapeSVG ? Math.max(portraitFrame?.totalHeight ?? dh, landscapeFrame?.totalHeight ?? dw) : activeFrame?.totalHeight ?? (rotateFrame ? portH : dh);
797
- const contentBezelLeft = activeFrame?.bezelLeft ?? 0;
798
- const contentBezelTop = activeFrame?.bezelTop ?? 0;
799
- const contentScreenW = activeFrame?.screenWidth ?? dw;
800
- const contentScreenH = activeFrame?.screenHeight ?? dh;
801
- const useRotationFallback = rotateFrame && !hasLandscapeSVG;
802
- const scalerTransform = useRotationFallback ? `scale(${scale}) translate(0px, ${scalerW}px) rotate(-90deg)` : `scale(${scale})`;
837
+ const scalerW = portFrame.totalWidth;
838
+ const scalerH = portFrame.totalHeight;
839
+ const scalerTransform = rotateFrame ? `scale(${scale}) translate(0px, ${landFrame.totalHeight}px) rotate(-90deg)` : `scale(${scale})`;
840
+ const contentLandscape = contentOrientation === "landscape";
841
+ const contentBox = contentLandscape ? { left: landFrame.totalHeight - landFrame.bezelTop, top: landFrame.bezelLeft, width: landFrame.screenWidth, height: landFrame.screenHeight } : { left: portFrame.bezelLeft, top: portFrame.bezelTop, width: portFrame.screenWidth, height: portFrame.screenHeight };
842
+ const contentRadius = contentLandscape ? landFrame.screenRadius : portFrame.screenRadius;
803
843
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
804
844
  "div",
805
845
  {
@@ -816,8 +856,10 @@ function DeviceFrame({
816
856
  height: hostHeight,
817
857
  position: "relative",
818
858
  flexShrink: 0,
819
- overflow: "hidden",
820
- transition: "width 400ms cubic-bezier(0.4, 0, 0.2, 1), height 400ms cubic-bezier(0.4, 0, 0.2, 1)"
859
+ // No overflow clipping: at rest the frame fits the host exactly; while
860
+ // rotating, the corners sweep outside and clipping them flat looks bad.
861
+ // The sentinel still clips at the component boundary.
862
+ transition: `width ${ROTATE_MS}ms ${EASE}, height ${ROTATE_MS}ms ${EASE}`
821
863
  },
822
864
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
823
865
  "div",
@@ -833,7 +875,7 @@ function DeviceFrame({
833
875
  transform: scalerTransform,
834
876
  transformOrigin: "top left",
835
877
  willChange: "transform",
836
- transition: "transform 400ms cubic-bezier(0.4, 0, 0.2, 1)"
878
+ transition: `transform ${ROTATE_MS}ms ${EASE}`
837
879
  },
838
880
  children: [
839
881
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -842,24 +884,27 @@ function DeviceFrame({
842
884
  className: "bielaframe-content",
843
885
  style: {
844
886
  position: "absolute",
845
- left: `${contentBezelLeft / scalerW * 100}%`,
846
- top: `${contentBezelTop / scalerH * 100}%`,
847
- width: `${contentScreenW / scalerW * 100}%`,
848
- height: `${contentScreenH / scalerH * 100}%`,
887
+ left: contentBox.left,
888
+ top: contentBox.top,
889
+ width: contentBox.width,
890
+ height: contentBox.height,
891
+ ...contentLandscape ? { transform: "rotate(90deg)", transformOrigin: "top left" } : null,
849
892
  overflow: "hidden",
850
893
  zIndex: 0,
851
894
  background: colorScheme === "dark" ? "#000" : "#fff",
852
- borderRadius: activeFrame?.screenRadius ?? 0,
895
+ borderRadius: contentRadius,
896
+ opacity: contentVisible ? 1 : 0,
897
+ transition: `opacity ${contentVisible ? CONTENT_FADE_IN_MS : CONTENT_FADE_OUT_MS}ms ${EASE}`,
853
898
  ...cssVarsStyle
854
899
  },
855
900
  children: [
856
901
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DeviceErrorBoundary, { children }),
857
- showStatusBar && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DynamicStatusBar, { contract, orientation, colorScheme }),
858
- showSafeAreaOverlay && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SafeAreaOverlay, { contract, orientation })
902
+ showStatusBar && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DynamicStatusBar, { contract, orientation: contentOrientation, colorScheme }),
903
+ showSafeAreaOverlay && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SafeAreaOverlay, { contract, orientation: contentOrientation })
859
904
  ]
860
905
  }
861
906
  ),
862
- SVGComponent && portraitFrame && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
907
+ SVGComponent && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
863
908
  "div",
864
909
  {
865
910
  "aria-hidden": true,
@@ -867,12 +912,12 @@ function DeviceFrame({
867
912
  position: "absolute",
868
913
  top: 0,
869
914
  left: 0,
870
- width: portraitFrame.totalWidth,
871
- height: portraitFrame.totalHeight,
915
+ width: portFrame.totalWidth,
916
+ height: portFrame.totalHeight,
872
917
  pointerEvents: "none",
873
918
  zIndex: 1,
874
919
  opacity: hasLandscapeSVG && rotateFrame ? 0 : 1,
875
- transition: "opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)"
920
+ transition: `opacity ${SVG_FADE_MS}ms ${EASE} ${SVG_FADE_DELAY_MS}ms`
876
921
  },
877
922
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
878
923
  SVGComponent,
@@ -883,20 +928,22 @@ function DeviceFrame({
883
928
  )
884
929
  }
885
930
  ),
886
- hasLandscapeSVG && LandscapeSVGComponent && landscapeFrame && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
931
+ hasLandscapeSVG && LandscapeSVGComponent && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
887
932
  "div",
888
933
  {
889
934
  "aria-hidden": true,
890
935
  style: {
891
936
  position: "absolute",
937
+ left: landFrame.totalHeight,
892
938
  top: 0,
893
- left: 0,
894
- width: landscapeFrame.totalWidth,
895
- height: landscapeFrame.totalHeight,
939
+ width: landFrame.totalWidth,
940
+ height: landFrame.totalHeight,
941
+ transform: "rotate(90deg)",
942
+ transformOrigin: "top left",
896
943
  pointerEvents: "none",
897
944
  zIndex: 1,
898
945
  opacity: rotateFrame ? 1 : 0,
899
- transition: "opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)"
946
+ transition: `opacity ${SVG_FADE_MS}ms ${EASE} ${SVG_FADE_DELAY_MS}ms`
900
947
  },
901
948
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
902
949
  LandscapeSVGComponent,