@mmtitanl/tablets-core 0.2.1 → 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.js CHANGED
@@ -271,7 +271,40 @@ function registerDeviceSVG(deviceId, component, frame, screenRect, landscape) {
271
271
  function getDeviceSVG(deviceId) {
272
272
  return SVG_REGISTRY.get(deviceId);
273
273
  }
274
- function buildCustomComponent(deviceId, svgString, cropViewBox, screenRect, suffix) {
274
+ function readSVGViewBox(svg) {
275
+ const vb = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i);
276
+ if (vb) {
277
+ const parts = vb[1].split(/[\s,]+/).map(Number);
278
+ if (parts.length === 4 && parts.every((n) => Number.isFinite(n))) {
279
+ return { x: parts[0], y: parts[1], w: parts[2], h: parts[3] };
280
+ }
281
+ }
282
+ const wm = svg.match(/<svg\b[^>]*\swidth\s*=\s*["']([\d.]+)/i);
283
+ const hm = svg.match(/<svg\b[^>]*\sheight\s*=\s*["']([\d.]+)/i);
284
+ if (wm && hm) return { x: 0, y: 0, w: parseFloat(wm[1]), h: parseFloat(hm[1]) };
285
+ return null;
286
+ }
287
+ function injectScreenMask(svg, frame, suffix) {
288
+ if (frame.totalWidth <= 0 || frame.totalHeight <= 0) return svg;
289
+ if (frame.screenWidth <= 0 || frame.screenHeight <= 0) return svg;
290
+ const vb = readSVGViewBox(svg);
291
+ if (!vb) return svg;
292
+ const sx = vb.w / frame.totalWidth;
293
+ const sy = vb.h / frame.totalHeight;
294
+ const x = vb.x + frame.bezelLeft * sx;
295
+ const y = vb.y + frame.bezelTop * sy;
296
+ const w = frame.screenWidth * sx;
297
+ const h = frame.screenHeight * sy;
298
+ const rt = Math.max(0, frame.screenRadiusTop ?? frame.screenRadius) * Math.min(sx, sy);
299
+ const rb = Math.max(0, frame.screenRadiusBottom ?? frame.screenRadius) * Math.min(sx, sy);
300
+ const maskId = `biela-screen-mask-${suffix || "default"}`;
301
+ const innerPath = `M${x + rt},${y} H${x + w - rt} a${rt},${rt} 0 0 1 ${rt},${rt} V${y + h - rb} a${rb},${rb} 0 0 1 ${-rb},${rb} H${x + rb} a${rb},${rb} 0 0 1 ${-rb},${-rb} V${y + rt} a${rt},${rt} 0 0 1 ${rt},${-rt} Z`;
302
+ const maskDef = `<defs><mask id="${maskId}" maskUnits="userSpaceOnUse" x="${vb.x}" y="${vb.y}" width="${vb.w}" height="${vb.h}"><rect x="${vb.x}" y="${vb.y}" width="${vb.w}" height="${vb.h}" fill="white"/><path d="${innerPath}" fill="black"/></mask></defs>`;
303
+ let result = svg.replace(/(<svg\b[^>]*>)/i, (m) => `${m}${maskDef}<g mask="url(#${maskId})">`);
304
+ result = result.replace(/<\/svg\s*>/i, `</g></svg>`);
305
+ return result;
306
+ }
307
+ function buildCustomComponent(deviceId, svgString, cropViewBox, suffix, frame, dieCut = false) {
275
308
  const scopeKey = suffix ? `${deviceId}${suffix}` : deviceId;
276
309
  let svg = scopeSVGIds(svgString, scopeKey);
277
310
  if (cropViewBox) {
@@ -291,7 +324,7 @@ function buildCustomComponent(deviceId, svgString, cropViewBox, screenRect, suff
291
324
  }
292
325
  );
293
326
  }
294
- void screenRect;
327
+ if (frame && dieCut) svg = injectScreenMask(svg, frame, scopeKey);
295
328
  const Component2 = ({ style }) => /* @__PURE__ */ jsx(
296
329
  "span",
297
330
  {
@@ -302,9 +335,10 @@ function buildCustomComponent(deviceId, svgString, cropViewBox, screenRect, suff
302
335
  Component2.displayName = `CustomDeviceSVG(${scopeKey})`;
303
336
  return Component2;
304
337
  }
305
- function registerCustomDeviceSVG(deviceId, svgString, frame, cropViewBox, screenRect, landscape) {
306
- const portraitComponent = buildCustomComponent(deviceId, svgString, cropViewBox, screenRect, "");
307
- const landscapeComponent = landscape ? buildCustomComponent(deviceId, landscape.svgString, landscape.cropViewBox, landscape.screenRect, "-landscape") : void 0;
338
+ function registerCustomDeviceSVG(deviceId, svgString, frame, cropViewBox, screenRect, landscape, options) {
339
+ const dieCut = options?.dieCutScreen ?? false;
340
+ const portraitComponent = buildCustomComponent(deviceId, svgString, cropViewBox, "", frame, dieCut);
341
+ const landscapeComponent = landscape ? buildCustomComponent(deviceId, landscape.svgString, landscape.cropViewBox, "-landscape", landscape.frame, dieCut) : void 0;
308
342
  SVG_REGISTRY.set(deviceId, {
309
343
  component: portraitComponent,
310
344
  frame,
@@ -666,6 +700,14 @@ function ensureBuiltinsRegistered() {
666
700
  }
667
701
  didAutoRegister = true;
668
702
  }
703
+ var ROTATE_MS = 450;
704
+ var EASE = "cubic-bezier(0.4, 0, 0.2, 1)";
705
+ var SVG_FADE_MS = 240;
706
+ var SVG_FADE_DELAY_MS = 100;
707
+ var CONTENT_FADE_OUT_MS = 140;
708
+ var CONTENT_SWAP_MS = 180;
709
+ var CONTENT_FADE_IN_DELAY_MS = 220;
710
+ var CONTENT_FADE_IN_MS = 230;
669
711
  function sendDeviceInfo(iframe, contract, orientation) {
670
712
  if (!iframe?.contentWindow) return;
671
713
  iframe.contentWindow.postMessage(
@@ -689,7 +731,9 @@ function DeviceFrame({
689
731
  manualScale = 1,
690
732
  showSafeAreaOverlay = false,
691
733
  showScaleBar = true,
692
- showStatusBar = true,
734
+ // Implicit oprit: SVG-urile device-urilor au status bar-ul desenat în ramă
735
+ // (ceas, semnal, baterie); cel sintetic ar apărea dublat peste conținut.
736
+ showStatusBar = false,
693
737
  colorScheme = "light",
694
738
  iframeRef,
695
739
  onColorSchemeChange,
@@ -701,7 +745,24 @@ function DeviceFrame({
701
745
  const resolvedId = device ?? deviceId;
702
746
  if (!resolvedId) throw new Error("DeviceFrame requires `device` or `deviceId`");
703
747
  const meta = getDeviceMetadata(resolvedId);
704
- const contract = useMemo3(() => getDeviceContract2(resolvedId, orientation), [resolvedId, orientation]);
748
+ const [contentOrientation, setContentOrientation] = useState8(orientation);
749
+ const [contentVisible, setContentVisible] = useState8(true);
750
+ const animTimers = useRef2([]);
751
+ const prevOrientation = useRef2(orientation);
752
+ useEffect5(() => {
753
+ if (prevOrientation.current === orientation) return;
754
+ prevOrientation.current = orientation;
755
+ for (const t of animTimers.current) window.clearTimeout(t);
756
+ setContentVisible(false);
757
+ animTimers.current = [
758
+ window.setTimeout(() => setContentOrientation(orientation), CONTENT_SWAP_MS),
759
+ window.setTimeout(() => setContentVisible(true), CONTENT_FADE_IN_DELAY_MS)
760
+ ];
761
+ }, [orientation]);
762
+ useEffect5(() => () => {
763
+ for (const t of animTimers.current) window.clearTimeout(t);
764
+ }, []);
765
+ const contract = useMemo3(() => getDeviceContract2(resolvedId, contentOrientation), [resolvedId, contentOrientation]);
705
766
  const portW = meta.screen.width;
706
767
  const portH = meta.screen.height;
707
768
  const rotateFrame = orientation === "landscape";
@@ -710,12 +771,36 @@ function DeviceFrame({
710
771
  const sentinelRef = useRef2(null);
711
772
  const frameContainerRef = useRef2(null);
712
773
  const containerSize = useContainerSize(sentinelRef);
713
- const svgEntryEarly = getDeviceSVG(resolvedId);
714
- const portraitFrameEarly = svgEntryEarly?.frame;
715
- const landscapeFrameEarly = svgEntryEarly?.landscapeFrame;
716
- const hasLandscapeSVGEarly = !!svgEntryEarly?.landscapeComponent && !!landscapeFrameEarly;
717
- const fitW = portraitFrameEarly ? rotateFrame ? hasLandscapeSVGEarly ? landscapeFrameEarly.totalWidth : portraitFrameEarly.totalHeight : portraitFrameEarly.totalWidth : rotateFrame ? portH : portW;
718
- const fitH = portraitFrameEarly ? rotateFrame ? hasLandscapeSVGEarly ? landscapeFrameEarly.totalHeight : portraitFrameEarly.totalWidth : portraitFrameEarly.totalHeight : rotateFrame ? portW : portH;
774
+ const svgEntry = getDeviceSVG(resolvedId);
775
+ const SVGComponent = svgEntry?.component ?? null;
776
+ const LandscapeSVGComponent = svgEntry?.landscapeComponent ?? null;
777
+ const registeredLandscapeFrame = svgEntry?.landscapeFrame;
778
+ const hasLandscapeSVG = !!LandscapeSVGComponent && !!registeredLandscapeFrame;
779
+ const portFrame = svgEntry?.frame ?? {
780
+ bezelTop: 0,
781
+ bezelBottom: 0,
782
+ bezelLeft: 0,
783
+ bezelRight: 0,
784
+ totalWidth: portW,
785
+ totalHeight: portH,
786
+ screenWidth: portW,
787
+ screenHeight: portH,
788
+ screenRadius: 0
789
+ };
790
+ const landFrame = hasLandscapeSVG && registeredLandscapeFrame ? registeredLandscapeFrame : {
791
+ bezelTop: portFrame.totalWidth - portFrame.bezelLeft - portFrame.screenWidth,
792
+ bezelBottom: portFrame.bezelLeft,
793
+ bezelLeft: portFrame.bezelTop,
794
+ bezelRight: portFrame.totalHeight - portFrame.bezelTop - portFrame.screenHeight,
795
+ totalWidth: portFrame.totalHeight,
796
+ totalHeight: portFrame.totalWidth,
797
+ screenWidth: portFrame.screenHeight,
798
+ screenHeight: portFrame.screenWidth,
799
+ screenRadius: portFrame.screenRadius
800
+ };
801
+ const activeFrame = rotateFrame ? landFrame : portFrame;
802
+ const fitW = activeFrame.totalWidth;
803
+ const fitH = activeFrame.totalHeight;
719
804
  const fitResult = useMemo3(
720
805
  () => computeFullScale(fitW, fitH, containerSize.width, containerSize.height, {
721
806
  snapToSteps: scaleMode === "steps"
@@ -738,44 +823,32 @@ function DeviceFrame({
738
823
  }, [scale, onScaleChange]);
739
824
  useEffect5(() => {
740
825
  if (!iframeRef?.current) return;
741
- sendDeviceInfo(iframeRef.current, contract, orientation);
742
- const onLoad = () => sendDeviceInfo(iframeRef.current, contract, orientation);
826
+ sendDeviceInfo(iframeRef.current, contract, contentOrientation);
827
+ const onLoad = () => sendDeviceInfo(iframeRef.current, contract, contentOrientation);
743
828
  iframeRef.current.addEventListener("load", onLoad);
744
829
  return () => iframeRef.current?.removeEventListener("load", onLoad);
745
- }, [iframeRef, contract, orientation]);
830
+ }, [iframeRef, contract, contentOrientation]);
746
831
  useEffect5(() => {
747
832
  if (!iframeRef) return;
748
833
  const handler = (event) => {
749
834
  const data = event.data;
750
835
  if (!data || typeof data !== "object") return;
751
836
  if (data.type === "biela:requestDeviceInfo") {
752
- sendDeviceInfo(iframeRef.current, contract, orientation);
837
+ sendDeviceInfo(iframeRef.current, contract, contentOrientation);
753
838
  } else if (data.type === "biela:colorScheme" && data.payload?.scheme) {
754
839
  onColorSchemeChange?.(data.payload.scheme);
755
840
  }
756
841
  };
757
842
  window.addEventListener("message", handler);
758
843
  return () => window.removeEventListener("message", handler);
759
- }, [iframeRef, contract, orientation, onColorSchemeChange]);
760
- const svgEntry = getDeviceSVG(resolvedId);
761
- const SVGComponent = svgEntry?.component ?? null;
762
- const portraitFrame = svgEntry?.frame;
763
- const LandscapeSVGComponent = svgEntry?.landscapeComponent ?? null;
764
- const landscapeFrame = svgEntry?.landscapeFrame;
765
- const hasLandscapeSVG = !!LandscapeSVGComponent && !!landscapeFrame;
844
+ }, [iframeRef, contract, contentOrientation, onColorSchemeChange]);
766
845
  const cssVarsStyle = contract.cssVariables;
767
- const activeFrame = hasLandscapeSVG && rotateFrame ? landscapeFrame : portraitFrame;
768
- const scalerW = hasLandscapeSVG ? Math.max(portraitFrame?.totalWidth ?? dw, landscapeFrame?.totalWidth ?? dh) : activeFrame?.totalWidth ?? (rotateFrame ? portW : dw);
769
- const scalerH = hasLandscapeSVG ? Math.max(portraitFrame?.totalHeight ?? dh, landscapeFrame?.totalHeight ?? dw) : activeFrame?.totalHeight ?? (rotateFrame ? portH : dh);
770
- const contentBezelLeft = activeFrame?.bezelLeft ?? 0;
771
- const contentBezelTop = activeFrame?.bezelTop ?? 0;
772
- const contentScreenW = activeFrame?.screenWidth ?? dw;
773
- const contentScreenH = activeFrame?.screenHeight ?? dh;
774
- const baseRadius = activeFrame?.screenRadius ?? meta.screen.cornerRadius ?? 0;
775
- const radiusTop = activeFrame?.screenRadiusTop ?? baseRadius;
776
- const radiusBottom = activeFrame?.screenRadiusBottom ?? baseRadius;
777
- const useRotationFallback = rotateFrame && !hasLandscapeSVG;
778
- const scalerTransform = useRotationFallback ? `scale(${scale}) translate(0px, ${scalerW}px) rotate(-90deg)` : `scale(${scale})`;
846
+ const scalerW = portFrame.totalWidth;
847
+ const scalerH = portFrame.totalHeight;
848
+ const scalerTransform = rotateFrame ? `scale(${scale}) translate(0px, ${landFrame.totalHeight}px) rotate(-90deg)` : `scale(${scale})`;
849
+ const contentLandscape = contentOrientation === "landscape";
850
+ 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 };
851
+ const contentRadius = contentLandscape ? landFrame.screenRadius : portFrame.screenRadius;
779
852
  return /* @__PURE__ */ jsxs4(
780
853
  "div",
781
854
  {
@@ -792,8 +865,10 @@ function DeviceFrame({
792
865
  height: hostHeight,
793
866
  position: "relative",
794
867
  flexShrink: 0,
795
- overflow: "hidden",
796
- transition: "width 400ms cubic-bezier(0.4, 0, 0.2, 1), height 400ms cubic-bezier(0.4, 0, 0.2, 1)"
868
+ // No overflow clipping: at rest the frame fits the host exactly; while
869
+ // rotating, the corners sweep outside and clipping them flat looks bad.
870
+ // The sentinel still clips at the component boundary.
871
+ transition: `width ${ROTATE_MS}ms ${EASE}, height ${ROTATE_MS}ms ${EASE}`
797
872
  },
798
873
  children: /* @__PURE__ */ jsxs4(
799
874
  "div",
@@ -809,10 +884,36 @@ function DeviceFrame({
809
884
  transform: scalerTransform,
810
885
  transformOrigin: "top left",
811
886
  willChange: "transform",
812
- transition: "transform 400ms cubic-bezier(0.4, 0, 0.2, 1)"
887
+ transition: `transform ${ROTATE_MS}ms ${EASE}`
813
888
  },
814
889
  children: [
815
- SVGComponent && portraitFrame && /* @__PURE__ */ jsx6(
890
+ /* @__PURE__ */ jsxs4(
891
+ "div",
892
+ {
893
+ className: "bielaframe-content",
894
+ style: {
895
+ position: "absolute",
896
+ left: contentBox.left,
897
+ top: contentBox.top,
898
+ width: contentBox.width,
899
+ height: contentBox.height,
900
+ ...contentLandscape ? { transform: "rotate(90deg)", transformOrigin: "top left" } : null,
901
+ overflow: "hidden",
902
+ zIndex: 0,
903
+ background: colorScheme === "dark" ? "#000" : "#fff",
904
+ borderRadius: contentRadius,
905
+ opacity: contentVisible ? 1 : 0,
906
+ transition: `opacity ${contentVisible ? CONTENT_FADE_IN_MS : CONTENT_FADE_OUT_MS}ms ${EASE}`,
907
+ ...cssVarsStyle
908
+ },
909
+ children: [
910
+ /* @__PURE__ */ jsx6(DeviceErrorBoundary, { children }),
911
+ showStatusBar && /* @__PURE__ */ jsx6(DynamicStatusBar, { contract, orientation: contentOrientation, colorScheme }),
912
+ showSafeAreaOverlay && /* @__PURE__ */ jsx6(SafeAreaOverlay, { contract, orientation: contentOrientation })
913
+ ]
914
+ }
915
+ ),
916
+ SVGComponent && /* @__PURE__ */ jsx6(
816
917
  "div",
817
918
  {
818
919
  "aria-hidden": true,
@@ -820,12 +921,12 @@ function DeviceFrame({
820
921
  position: "absolute",
821
922
  top: 0,
822
923
  left: 0,
823
- width: portraitFrame.totalWidth,
824
- height: portraitFrame.totalHeight,
924
+ width: portFrame.totalWidth,
925
+ height: portFrame.totalHeight,
825
926
  pointerEvents: "none",
826
927
  zIndex: 1,
827
928
  opacity: hasLandscapeSVG && rotateFrame ? 0 : 1,
828
- transition: "opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)"
929
+ transition: `opacity ${SVG_FADE_MS}ms ${EASE} ${SVG_FADE_DELAY_MS}ms`
829
930
  },
830
931
  children: /* @__PURE__ */ jsx6(
831
932
  SVGComponent,
@@ -836,20 +937,22 @@ function DeviceFrame({
836
937
  )
837
938
  }
838
939
  ),
839
- hasLandscapeSVG && LandscapeSVGComponent && landscapeFrame && /* @__PURE__ */ jsx6(
940
+ hasLandscapeSVG && LandscapeSVGComponent && /* @__PURE__ */ jsx6(
840
941
  "div",
841
942
  {
842
943
  "aria-hidden": true,
843
944
  style: {
844
945
  position: "absolute",
946
+ left: landFrame.totalHeight,
845
947
  top: 0,
846
- left: 0,
847
- width: landscapeFrame.totalWidth,
848
- height: landscapeFrame.totalHeight,
948
+ width: landFrame.totalWidth,
949
+ height: landFrame.totalHeight,
950
+ transform: "rotate(90deg)",
951
+ transformOrigin: "top left",
849
952
  pointerEvents: "none",
850
953
  zIndex: 1,
851
954
  opacity: rotateFrame ? 1 : 0,
852
- transition: "opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)"
955
+ transition: `opacity ${SVG_FADE_MS}ms ${EASE} ${SVG_FADE_DELAY_MS}ms`
853
956
  },
854
957
  children: /* @__PURE__ */ jsx6(
855
958
  LandscapeSVGComponent,
@@ -859,38 +962,6 @@ function DeviceFrame({
859
962
  }
860
963
  )
861
964
  }
862
- ),
863
- /* @__PURE__ */ jsxs4(
864
- "div",
865
- {
866
- className: "bielaframe-content",
867
- style: {
868
- position: "absolute",
869
- left: contentBezelLeft,
870
- top: contentBezelTop,
871
- width: contentScreenW,
872
- height: contentScreenH,
873
- // `border-radius + overflow:hidden` clips the <iframe> child
874
- // in every browser. clip-path is kept as belt-and-braces — some
875
- // engines apply it to the iframe, some don't. Per-edge radii
876
- // (top vs bottom) let devices with asymmetric corners — e.g.
877
- // flat-bottom — clip correctly.
878
- borderRadius: `${radiusTop}px ${radiusTop}px ${radiusBottom}px ${radiusBottom}px`,
879
- clipPath: `inset(0 round ${radiusTop}px ${radiusTop}px ${radiusBottom}px ${radiusBottom}px)`,
880
- overflow: "hidden",
881
- isolation: "isolate",
882
- backfaceVisibility: "hidden",
883
- transform: "translateZ(0)",
884
- background: colorScheme === "dark" ? "#000" : "#fff",
885
- zIndex: 5,
886
- ...cssVarsStyle
887
- },
888
- children: [
889
- /* @__PURE__ */ jsx6(DeviceErrorBoundary, { children }),
890
- showStatusBar && /* @__PURE__ */ jsx6(DynamicStatusBar, { contract, orientation, colorScheme }),
891
- showSafeAreaOverlay && /* @__PURE__ */ jsx6(SafeAreaOverlay, { contract, orientation })
892
- ]
893
- }
894
965
  )
895
966
  ]
896
967
  }
@@ -1205,7 +1276,8 @@ var CustomSVGStore = class {
1205
1276
  },
1206
1277
  void 0,
1207
1278
  sr,
1208
- landscape
1279
+ landscape,
1280
+ { dieCutScreen: entry.dieCutScreen ?? false }
1209
1281
  );
1210
1282
  }
1211
1283
  persist(all) {