@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.cjs CHANGED
@@ -329,7 +329,40 @@ function registerDeviceSVG(deviceId, component, frame, screenRect, landscape) {
329
329
  function getDeviceSVG(deviceId) {
330
330
  return SVG_REGISTRY.get(deviceId);
331
331
  }
332
- function buildCustomComponent(deviceId, svgString, cropViewBox, screenRect, suffix) {
332
+ function readSVGViewBox(svg) {
333
+ const vb = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i);
334
+ if (vb) {
335
+ const parts = vb[1].split(/[\s,]+/).map(Number);
336
+ if (parts.length === 4 && parts.every((n) => Number.isFinite(n))) {
337
+ return { x: parts[0], y: parts[1], w: parts[2], h: parts[3] };
338
+ }
339
+ }
340
+ const wm = svg.match(/<svg\b[^>]*\swidth\s*=\s*["']([\d.]+)/i);
341
+ const hm = svg.match(/<svg\b[^>]*\sheight\s*=\s*["']([\d.]+)/i);
342
+ if (wm && hm) return { x: 0, y: 0, w: parseFloat(wm[1]), h: parseFloat(hm[1]) };
343
+ return null;
344
+ }
345
+ function injectScreenMask(svg, frame, suffix) {
346
+ if (frame.totalWidth <= 0 || frame.totalHeight <= 0) return svg;
347
+ if (frame.screenWidth <= 0 || frame.screenHeight <= 0) return svg;
348
+ const vb = readSVGViewBox(svg);
349
+ if (!vb) return svg;
350
+ const sx = vb.w / frame.totalWidth;
351
+ const sy = vb.h / frame.totalHeight;
352
+ const x = vb.x + frame.bezelLeft * sx;
353
+ const y = vb.y + frame.bezelTop * sy;
354
+ const w = frame.screenWidth * sx;
355
+ const h = frame.screenHeight * sy;
356
+ const rt = Math.max(0, frame.screenRadiusTop ?? frame.screenRadius) * Math.min(sx, sy);
357
+ const rb = Math.max(0, frame.screenRadiusBottom ?? frame.screenRadius) * Math.min(sx, sy);
358
+ const maskId = `biela-screen-mask-${suffix || "default"}`;
359
+ 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`;
360
+ 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>`;
361
+ let result = svg.replace(/(<svg\b[^>]*>)/i, (m) => `${m}${maskDef}<g mask="url(#${maskId})">`);
362
+ result = result.replace(/<\/svg\s*>/i, `</g></svg>`);
363
+ return result;
364
+ }
365
+ function buildCustomComponent(deviceId, svgString, cropViewBox, suffix, frame, dieCut = false) {
333
366
  const scopeKey = suffix ? `${deviceId}${suffix}` : deviceId;
334
367
  let svg = (0, import_tablets2.scopeSVGIds)(svgString, scopeKey);
335
368
  if (cropViewBox) {
@@ -349,7 +382,7 @@ function buildCustomComponent(deviceId, svgString, cropViewBox, screenRect, suff
349
382
  }
350
383
  );
351
384
  }
352
- void screenRect;
385
+ if (frame && dieCut) svg = injectScreenMask(svg, frame, scopeKey);
353
386
  const Component2 = ({ style }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
354
387
  "span",
355
388
  {
@@ -360,9 +393,10 @@ function buildCustomComponent(deviceId, svgString, cropViewBox, screenRect, suff
360
393
  Component2.displayName = `CustomDeviceSVG(${scopeKey})`;
361
394
  return Component2;
362
395
  }
363
- function registerCustomDeviceSVG(deviceId, svgString, frame, cropViewBox, screenRect, landscape) {
364
- const portraitComponent = buildCustomComponent(deviceId, svgString, cropViewBox, screenRect, "");
365
- const landscapeComponent = landscape ? buildCustomComponent(deviceId, landscape.svgString, landscape.cropViewBox, landscape.screenRect, "-landscape") : void 0;
396
+ function registerCustomDeviceSVG(deviceId, svgString, frame, cropViewBox, screenRect, landscape, options) {
397
+ const dieCut = options?.dieCutScreen ?? false;
398
+ const portraitComponent = buildCustomComponent(deviceId, svgString, cropViewBox, "", frame, dieCut);
399
+ const landscapeComponent = landscape ? buildCustomComponent(deviceId, landscape.svgString, landscape.cropViewBox, "-landscape", landscape.frame, dieCut) : void 0;
366
400
  SVG_REGISTRY.set(deviceId, {
367
401
  component: portraitComponent,
368
402
  frame,
@@ -657,6 +691,14 @@ function ensureBuiltinsRegistered() {
657
691
  }
658
692
  didAutoRegister = true;
659
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;
660
702
  function sendDeviceInfo(iframe, contract, orientation) {
661
703
  if (!iframe?.contentWindow) return;
662
704
  iframe.contentWindow.postMessage(
@@ -680,7 +722,9 @@ function DeviceFrame({
680
722
  manualScale = 1,
681
723
  showSafeAreaOverlay = false,
682
724
  showScaleBar = true,
683
- showStatusBar = true,
725
+ // Implicit oprit: SVG-urile device-urilor au status bar-ul desenat în ramă
726
+ // (ceas, semnal, baterie); cel sintetic ar apărea dublat peste conținut.
727
+ showStatusBar = false,
684
728
  colorScheme = "light",
685
729
  iframeRef,
686
730
  onColorSchemeChange,
@@ -692,7 +736,24 @@ function DeviceFrame({
692
736
  const resolvedId = device ?? deviceId;
693
737
  if (!resolvedId) throw new Error("DeviceFrame requires `device` or `deviceId`");
694
738
  const meta = (0, import_tablets3.getDeviceMetadata)(resolvedId);
695
- 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]);
696
757
  const portW = meta.screen.width;
697
758
  const portH = meta.screen.height;
698
759
  const rotateFrame = orientation === "landscape";
@@ -701,12 +762,36 @@ function DeviceFrame({
701
762
  const sentinelRef = (0, import_react11.useRef)(null);
702
763
  const frameContainerRef = (0, import_react11.useRef)(null);
703
764
  const containerSize = useContainerSize(sentinelRef);
704
- const svgEntryEarly = getDeviceSVG(resolvedId);
705
- const portraitFrameEarly = svgEntryEarly?.frame;
706
- const landscapeFrameEarly = svgEntryEarly?.landscapeFrame;
707
- const hasLandscapeSVGEarly = !!svgEntryEarly?.landscapeComponent && !!landscapeFrameEarly;
708
- const fitW = portraitFrameEarly ? rotateFrame ? hasLandscapeSVGEarly ? landscapeFrameEarly.totalWidth : portraitFrameEarly.totalHeight : portraitFrameEarly.totalWidth : rotateFrame ? portH : portW;
709
- 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;
710
795
  const fitResult = (0, import_react11.useMemo)(
711
796
  () => computeFullScale(fitW, fitH, containerSize.width, containerSize.height, {
712
797
  snapToSteps: scaleMode === "steps"
@@ -729,44 +814,32 @@ function DeviceFrame({
729
814
  }, [scale, onScaleChange]);
730
815
  (0, import_react11.useEffect)(() => {
731
816
  if (!iframeRef?.current) return;
732
- sendDeviceInfo(iframeRef.current, contract, orientation);
733
- const onLoad = () => sendDeviceInfo(iframeRef.current, contract, orientation);
817
+ sendDeviceInfo(iframeRef.current, contract, contentOrientation);
818
+ const onLoad = () => sendDeviceInfo(iframeRef.current, contract, contentOrientation);
734
819
  iframeRef.current.addEventListener("load", onLoad);
735
820
  return () => iframeRef.current?.removeEventListener("load", onLoad);
736
- }, [iframeRef, contract, orientation]);
821
+ }, [iframeRef, contract, contentOrientation]);
737
822
  (0, import_react11.useEffect)(() => {
738
823
  if (!iframeRef) return;
739
824
  const handler = (event) => {
740
825
  const data = event.data;
741
826
  if (!data || typeof data !== "object") return;
742
827
  if (data.type === "biela:requestDeviceInfo") {
743
- sendDeviceInfo(iframeRef.current, contract, orientation);
828
+ sendDeviceInfo(iframeRef.current, contract, contentOrientation);
744
829
  } else if (data.type === "biela:colorScheme" && data.payload?.scheme) {
745
830
  onColorSchemeChange?.(data.payload.scheme);
746
831
  }
747
832
  };
748
833
  window.addEventListener("message", handler);
749
834
  return () => window.removeEventListener("message", handler);
750
- }, [iframeRef, contract, orientation, onColorSchemeChange]);
751
- const svgEntry = getDeviceSVG(resolvedId);
752
- const SVGComponent = svgEntry?.component ?? null;
753
- const portraitFrame = svgEntry?.frame;
754
- const LandscapeSVGComponent = svgEntry?.landscapeComponent ?? null;
755
- const landscapeFrame = svgEntry?.landscapeFrame;
756
- const hasLandscapeSVG = !!LandscapeSVGComponent && !!landscapeFrame;
835
+ }, [iframeRef, contract, contentOrientation, onColorSchemeChange]);
757
836
  const cssVarsStyle = contract.cssVariables;
758
- const activeFrame = hasLandscapeSVG && rotateFrame ? landscapeFrame : portraitFrame;
759
- const scalerW = hasLandscapeSVG ? Math.max(portraitFrame?.totalWidth ?? dw, landscapeFrame?.totalWidth ?? dh) : activeFrame?.totalWidth ?? (rotateFrame ? portW : dw);
760
- const scalerH = hasLandscapeSVG ? Math.max(portraitFrame?.totalHeight ?? dh, landscapeFrame?.totalHeight ?? dw) : activeFrame?.totalHeight ?? (rotateFrame ? portH : dh);
761
- const contentBezelLeft = activeFrame?.bezelLeft ?? 0;
762
- const contentBezelTop = activeFrame?.bezelTop ?? 0;
763
- const contentScreenW = activeFrame?.screenWidth ?? dw;
764
- const contentScreenH = activeFrame?.screenHeight ?? dh;
765
- const baseRadius = activeFrame?.screenRadius ?? meta.screen.cornerRadius ?? 0;
766
- const radiusTop = activeFrame?.screenRadiusTop ?? baseRadius;
767
- const radiusBottom = activeFrame?.screenRadiusBottom ?? baseRadius;
768
- const useRotationFallback = rotateFrame && !hasLandscapeSVG;
769
- 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;
770
843
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
771
844
  "div",
772
845
  {
@@ -783,8 +856,10 @@ function DeviceFrame({
783
856
  height: hostHeight,
784
857
  position: "relative",
785
858
  flexShrink: 0,
786
- overflow: "hidden",
787
- 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}`
788
863
  },
789
864
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
790
865
  "div",
@@ -800,10 +875,36 @@ function DeviceFrame({
800
875
  transform: scalerTransform,
801
876
  transformOrigin: "top left",
802
877
  willChange: "transform",
803
- transition: "transform 400ms cubic-bezier(0.4, 0, 0.2, 1)"
878
+ transition: `transform ${ROTATE_MS}ms ${EASE}`
804
879
  },
805
880
  children: [
806
- SVGComponent && portraitFrame && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
881
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
882
+ "div",
883
+ {
884
+ className: "bielaframe-content",
885
+ style: {
886
+ position: "absolute",
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,
892
+ overflow: "hidden",
893
+ zIndex: 0,
894
+ background: colorScheme === "dark" ? "#000" : "#fff",
895
+ borderRadius: contentRadius,
896
+ opacity: contentVisible ? 1 : 0,
897
+ transition: `opacity ${contentVisible ? CONTENT_FADE_IN_MS : CONTENT_FADE_OUT_MS}ms ${EASE}`,
898
+ ...cssVarsStyle
899
+ },
900
+ children: [
901
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DeviceErrorBoundary, { children }),
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 })
904
+ ]
905
+ }
906
+ ),
907
+ SVGComponent && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
807
908
  "div",
808
909
  {
809
910
  "aria-hidden": true,
@@ -811,12 +912,12 @@ function DeviceFrame({
811
912
  position: "absolute",
812
913
  top: 0,
813
914
  left: 0,
814
- width: portraitFrame.totalWidth,
815
- height: portraitFrame.totalHeight,
915
+ width: portFrame.totalWidth,
916
+ height: portFrame.totalHeight,
816
917
  pointerEvents: "none",
817
918
  zIndex: 1,
818
919
  opacity: hasLandscapeSVG && rotateFrame ? 0 : 1,
819
- transition: "opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)"
920
+ transition: `opacity ${SVG_FADE_MS}ms ${EASE} ${SVG_FADE_DELAY_MS}ms`
820
921
  },
821
922
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
822
923
  SVGComponent,
@@ -827,20 +928,22 @@ function DeviceFrame({
827
928
  )
828
929
  }
829
930
  ),
830
- hasLandscapeSVG && LandscapeSVGComponent && landscapeFrame && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
931
+ hasLandscapeSVG && LandscapeSVGComponent && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
831
932
  "div",
832
933
  {
833
934
  "aria-hidden": true,
834
935
  style: {
835
936
  position: "absolute",
937
+ left: landFrame.totalHeight,
836
938
  top: 0,
837
- left: 0,
838
- width: landscapeFrame.totalWidth,
839
- height: landscapeFrame.totalHeight,
939
+ width: landFrame.totalWidth,
940
+ height: landFrame.totalHeight,
941
+ transform: "rotate(90deg)",
942
+ transformOrigin: "top left",
840
943
  pointerEvents: "none",
841
944
  zIndex: 1,
842
945
  opacity: rotateFrame ? 1 : 0,
843
- transition: "opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)"
946
+ transition: `opacity ${SVG_FADE_MS}ms ${EASE} ${SVG_FADE_DELAY_MS}ms`
844
947
  },
845
948
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
846
949
  LandscapeSVGComponent,
@@ -850,38 +953,6 @@ function DeviceFrame({
850
953
  }
851
954
  )
852
955
  }
853
- ),
854
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
855
- "div",
856
- {
857
- className: "bielaframe-content",
858
- style: {
859
- position: "absolute",
860
- left: contentBezelLeft,
861
- top: contentBezelTop,
862
- width: contentScreenW,
863
- height: contentScreenH,
864
- // `border-radius + overflow:hidden` clips the <iframe> child
865
- // in every browser. clip-path is kept as belt-and-braces — some
866
- // engines apply it to the iframe, some don't. Per-edge radii
867
- // (top vs bottom) let devices with asymmetric corners — e.g.
868
- // flat-bottom — clip correctly.
869
- borderRadius: `${radiusTop}px ${radiusTop}px ${radiusBottom}px ${radiusBottom}px`,
870
- clipPath: `inset(0 round ${radiusTop}px ${radiusTop}px ${radiusBottom}px ${radiusBottom}px)`,
871
- overflow: "hidden",
872
- isolation: "isolate",
873
- backfaceVisibility: "hidden",
874
- transform: "translateZ(0)",
875
- background: colorScheme === "dark" ? "#000" : "#fff",
876
- zIndex: 5,
877
- ...cssVarsStyle
878
- },
879
- children: [
880
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DeviceErrorBoundary, { children }),
881
- showStatusBar && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DynamicStatusBar, { contract, orientation, colorScheme }),
882
- showSafeAreaOverlay && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SafeAreaOverlay, { contract, orientation })
883
- ]
884
- }
885
956
  )
886
957
  ]
887
958
  }
@@ -1196,7 +1267,8 @@ var CustomSVGStore = class {
1196
1267
  },
1197
1268
  void 0,
1198
1269
  sr,
1199
- landscape
1270
+ landscape,
1271
+ { dieCutScreen: entry.dieCutScreen ?? false }
1200
1272
  );
1201
1273
  }
1202
1274
  persist(all) {