@biela.dev/core 1.5.0 → 1.6.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
@@ -1,20 +1,25 @@
1
- import { useState, useEffect, useMemo, useRef, useCallback, Component } from 'react';
2
- import { getDeviceContract, scopeSVGIds, getDeviceMetadata } from '@biela.dev/devices';
1
+ import { useState, useEffect, useMemo, Component, useRef, useCallback } from 'react';
2
+ import { scopeSVGIds, getDeviceMetadata, getDeviceContract, IPHONE_17_PRO_MAX_FRAME, IPhone17ProMaxSVG, IPHONE_17_PRO_FRAME, IPhone17ProSVG, IPHONE_AIR_FRAME, IPhoneAirSVG, IPHONE_16_FRAME, IPhone16SVG, IPHONE_16E_FRAME, IPhone16eSVG, IPHONE_SE_3_FRAME, IPhoneSE3SVG, GALAXY_S25_ULTRA_FRAME, GalaxyS25UltraSVG, GALAXY_S25_FRAME, GalaxyS25SVG, GALAXY_S25_EDGE_FRAME, GalaxyS25EdgeSVG, PIXEL_9_PRO_XL_FRAME, Pixel9ProXLSVG, PIXEL_9_PRO_FRAME, Pixel9ProSVG } from '@biela.dev/devices';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
 
5
- // src/math/conversions.ts
6
- function ptsToPx(pts, dpr) {
7
- return Math.round(pts * dpr);
8
- }
9
- function pxToPts(px, dpr) {
10
- return px / dpr;
11
- }
12
- function ptsToPercent(pts, total) {
13
- if (total === 0) return 0;
14
- return pts / total * 100;
15
- }
16
- function scaleValue(value, scaleFactor) {
17
- return value * scaleFactor;
5
+ // src/components/DeviceFrame.tsx
6
+ function useContainerSize(ref) {
7
+ const [size, setSize] = useState({ width: 0, height: 0 });
8
+ useEffect(() => {
9
+ const el = ref.current;
10
+ if (!el) return;
11
+ const observer = new ResizeObserver(([entry]) => {
12
+ if (!entry) return;
13
+ const { width, height } = entry.contentRect;
14
+ setSize((prev) => {
15
+ if (prev.width === width && prev.height === height) return prev;
16
+ return { width, height };
17
+ });
18
+ });
19
+ observer.observe(el);
20
+ return () => observer.disconnect();
21
+ }, [ref]);
22
+ return size;
18
23
  }
19
24
 
20
25
  // src/math/scale-engine.ts
@@ -69,24 +74,8 @@ function computeFullScale(deviceWidth, deviceHeight, containerWidth, containerHe
69
74
  scalePercent: `${Math.round(scale * 100)}%`
70
75
  };
71
76
  }
72
- function useContainerSize(ref) {
73
- const [size, setSize] = useState({ width: 0, height: 0 });
74
- useEffect(() => {
75
- const el = ref.current;
76
- if (!el) return;
77
- const observer = new ResizeObserver(([entry]) => {
78
- if (!entry) return;
79
- const { width, height } = entry.contentRect;
80
- setSize((prev) => {
81
- if (prev.width === width && prev.height === height) return prev;
82
- return { width, height };
83
- });
84
- });
85
- observer.observe(el);
86
- return () => observer.disconnect();
87
- }, [ref]);
88
- return size;
89
- }
77
+
78
+ // src/hooks/useAdaptiveScale.ts
90
79
  function useAdaptiveScale(options) {
91
80
  const {
92
81
  device,
@@ -107,72 +96,6 @@ function useAdaptiveScale(options) {
107
96
  [device.screen.width, device.screen.height, containerWidth, containerHeight, padding, maxScale, minScale, snapToSteps]
108
97
  );
109
98
  }
110
- function useDeviceContract(deviceId, orientation = "portrait") {
111
- return useMemo(() => {
112
- const contract = getDeviceContract(deviceId, orientation);
113
- return {
114
- contract,
115
- cssVariables: contract.cssVariables,
116
- contentZone: contract.contentZone[orientation]
117
- };
118
- }, [deviceId, orientation]);
119
- }
120
- var STEPS = 16;
121
- var STEP_SIZE = 1 / STEPS;
122
- var HUD_DISPLAY_MS = 1500;
123
- function useVolumeControl(initialVolume = 1) {
124
- const [level, setLevel] = useState(initialVolume);
125
- const [muted, setMuted] = useState(false);
126
- const [hudVisible, setHudVisible] = useState(false);
127
- const hudTimerRef = useRef(null);
128
- const showHud = useCallback(() => {
129
- setHudVisible(true);
130
- if (hudTimerRef.current) clearTimeout(hudTimerRef.current);
131
- hudTimerRef.current = setTimeout(() => setHudVisible(false), HUD_DISPLAY_MS);
132
- }, []);
133
- const volumeUp = useCallback(() => {
134
- setLevel((prev) => {
135
- const next = Math.min(1, Math.round((prev + STEP_SIZE) * STEPS) / STEPS);
136
- return next;
137
- });
138
- setMuted(false);
139
- showHud();
140
- }, [showHud]);
141
- const volumeDown = useCallback(() => {
142
- setLevel((prev) => {
143
- const next = Math.max(0, Math.round((prev - STEP_SIZE) * STEPS) / STEPS);
144
- return next;
145
- });
146
- setMuted(false);
147
- showHud();
148
- }, [showHud]);
149
- const toggleMute = useCallback(() => {
150
- setMuted((prev) => !prev);
151
- showHud();
152
- }, [showHud]);
153
- const effectiveVolume = muted ? 0 : level;
154
- useEffect(() => {
155
- const container = document.querySelector(".bielaframe-content");
156
- if (!container) return;
157
- const mediaEls = container.querySelectorAll("audio, video");
158
- mediaEls.forEach((el) => {
159
- el.volume = effectiveVolume;
160
- });
161
- }, [effectiveVolume]);
162
- useEffect(() => {
163
- return () => {
164
- if (hudTimerRef.current) clearTimeout(hudTimerRef.current);
165
- };
166
- }, []);
167
- return { level, muted, hudVisible, volumeUp, volumeDown, toggleMute };
168
- }
169
- function useScreenPower() {
170
- const [isOff, setIsOff] = useState(false);
171
- const toggle = useCallback(() => {
172
- setIsOff((prev) => !prev);
173
- }, []);
174
- return { isOff, toggle };
175
- }
176
99
  var DeviceErrorBoundary = class extends Component {
177
100
  constructor(props) {
178
101
  super(props);
@@ -557,7 +480,8 @@ function registerCustomDeviceSVG(deviceId, svgString, frame, cropViewBox, screen
557
480
  SVG_REGISTRY[deviceId] = { component: CustomSVGComponent, frame };
558
481
  }
559
482
  function DeviceFrame({
560
- device,
483
+ device: deviceProp,
484
+ deviceId,
561
485
  orientation = "portrait",
562
486
  scaleMode = "fit",
563
487
  manualScale,
@@ -568,23 +492,28 @@ function DeviceFrame({
568
492
  onScaleChange,
569
493
  children
570
494
  }) {
495
+ const device = deviceProp ?? deviceId ?? "";
571
496
  const sentinelRef = useRef(null);
572
497
  const frameOverlayRef = useRef(null);
573
498
  const hostRef = useRef(null);
574
499
  const scalerRef = useRef(null);
575
500
  const { width: containerW, height: containerH } = useContainerSize(sentinelRef);
576
501
  const deviceLookup = useMemo(() => {
502
+ if (!device) {
503
+ return { meta: null, contract: null, error: "(no device specified)" };
504
+ }
577
505
  try {
578
- return {
579
- meta: getDeviceMetadata(device),
580
- contract: getDeviceContract(device, orientation),
581
- error: null
582
- };
506
+ const meta = getDeviceMetadata(device);
507
+ const contract2 = getDeviceContract(device, orientation);
508
+ if (!meta || !meta.screen) {
509
+ return { meta: null, contract: null, error: device };
510
+ }
511
+ return { meta, contract: contract2, error: null };
583
512
  } catch {
584
513
  return { meta: null, contract: null, error: device };
585
514
  }
586
515
  }, [device, orientation]);
587
- if (deviceLookup.error) {
516
+ if (deviceLookup.error || !deviceLookup.meta || !deviceLookup.contract) {
588
517
  return /* @__PURE__ */ jsxs(
589
518
  "div",
590
519
  {
@@ -601,7 +530,7 @@ function DeviceFrame({
601
530
  },
602
531
  children: [
603
532
  'Device not found: "',
604
- deviceLookup.error,
533
+ deviceLookup.error || "(unknown)",
605
534
  '"'
606
535
  ]
607
536
  }
@@ -783,6 +712,303 @@ function DeviceFrame({
783
712
  }
784
713
  );
785
714
  }
715
+ var SVG_OVERRIDES_KEY = "bielaframe-svg-overrides";
716
+ var CustomSVGStore = class {
717
+ storage;
718
+ constructor(storage) {
719
+ if (storage !== void 0) {
720
+ this.storage = storage;
721
+ } else {
722
+ this.storage = typeof localStorage !== "undefined" ? localStorage : null;
723
+ }
724
+ }
725
+ /** Load all stored overrides */
726
+ getAll() {
727
+ if (!this.storage) return {};
728
+ try {
729
+ const raw = this.storage.getItem(SVG_OVERRIDES_KEY);
730
+ if (raw) return JSON.parse(raw);
731
+ } catch {
732
+ }
733
+ return {};
734
+ }
735
+ /** Save an override and register it in the SVG registry */
736
+ save(entry) {
737
+ const all = this.getAll();
738
+ all[entry.deviceId] = entry;
739
+ this.persist(all);
740
+ this.applyEntry(entry);
741
+ }
742
+ /** Remove an override (revert to built-in) */
743
+ remove(deviceId) {
744
+ const all = this.getAll();
745
+ delete all[deviceId];
746
+ this.persist(all);
747
+ }
748
+ /** Check if a device has a custom override */
749
+ has(deviceId) {
750
+ return this.getAll()[deviceId] !== void 0;
751
+ }
752
+ /** Get a single override by device ID */
753
+ get(deviceId) {
754
+ return this.getAll()[deviceId];
755
+ }
756
+ /**
757
+ * Apply all stored overrides to the SVG registry.
758
+ * Called during auto-registration to restore user customizations.
759
+ */
760
+ applyAll() {
761
+ const all = this.getAll();
762
+ for (const entry of Object.values(all)) {
763
+ this.applyEntry(entry);
764
+ }
765
+ }
766
+ applyEntry(entry) {
767
+ let screenW = 402;
768
+ let screenH = 874;
769
+ let screenR = 0;
770
+ try {
771
+ const meta = getDeviceMetadata(entry.deviceId);
772
+ screenW = meta.screen.width;
773
+ screenH = meta.screen.height;
774
+ screenR = meta.screen.cornerRadius;
775
+ } catch {
776
+ if (entry.screenRect) {
777
+ screenW = entry.screenRect.width;
778
+ screenH = entry.screenRect.height;
779
+ }
780
+ }
781
+ const frame = {
782
+ bezelTop: entry.bezelTop,
783
+ bezelBottom: entry.bezelBottom,
784
+ bezelLeft: entry.bezelLeft,
785
+ bezelRight: entry.bezelRight,
786
+ totalWidth: entry.bezelLeft + entry.bezelRight + screenW,
787
+ totalHeight: entry.bezelTop + entry.bezelBottom + screenH,
788
+ screenWidth: screenW,
789
+ screenHeight: screenH,
790
+ screenRadius: screenR
791
+ };
792
+ try {
793
+ registerCustomDeviceSVG(
794
+ entry.deviceId,
795
+ entry.svgString,
796
+ frame,
797
+ void 0,
798
+ entry.screenRect
799
+ );
800
+ } catch {
801
+ }
802
+ }
803
+ persist(all) {
804
+ if (!this.storage) return;
805
+ const json = JSON.stringify(all);
806
+ try {
807
+ this.storage.setItem(SVG_OVERRIDES_KEY, json);
808
+ } catch {
809
+ }
810
+ }
811
+ };
812
+ var _store = null;
813
+ function getCustomSVGStore() {
814
+ if (!_store) {
815
+ _store = new CustomSVGStore();
816
+ }
817
+ return _store;
818
+ }
819
+
820
+ // src/registration.ts
821
+ registerDeviceSVG("iphone-17-pro-max", IPhone17ProMaxSVG, IPHONE_17_PRO_MAX_FRAME);
822
+ registerDeviceSVG("iphone-17-pro", IPhone17ProSVG, IPHONE_17_PRO_FRAME);
823
+ registerDeviceSVG("iphone-air", IPhoneAirSVG, IPHONE_AIR_FRAME);
824
+ registerDeviceSVG("iphone-16", IPhone16SVG, IPHONE_16_FRAME);
825
+ registerDeviceSVG("iphone-16e", IPhone16eSVG, IPHONE_16E_FRAME);
826
+ registerDeviceSVG("iphone-se-3", IPhoneSE3SVG, IPHONE_SE_3_FRAME);
827
+ registerDeviceSVG("galaxy-s25-ultra", GalaxyS25UltraSVG, GALAXY_S25_ULTRA_FRAME);
828
+ registerDeviceSVG("galaxy-s25", GalaxyS25SVG, GALAXY_S25_FRAME);
829
+ registerDeviceSVG("galaxy-s25-edge", GalaxyS25EdgeSVG, GALAXY_S25_EDGE_FRAME);
830
+ registerDeviceSVG("pixel-9-pro-xl", Pixel9ProXLSVG, PIXEL_9_PRO_XL_FRAME);
831
+ registerDeviceSVG("pixel-9-pro", Pixel9ProSVG, PIXEL_9_PRO_FRAME);
832
+ getCustomSVGStore().applyAll();
833
+
834
+ // src/math/conversions.ts
835
+ function ptsToPx(pts, dpr) {
836
+ return Math.round(pts * dpr);
837
+ }
838
+ function pxToPts(px, dpr) {
839
+ return px / dpr;
840
+ }
841
+ function ptsToPercent(pts, total) {
842
+ if (total === 0) return 0;
843
+ return pts / total * 100;
844
+ }
845
+ function scaleValue(value, scaleFactor) {
846
+ return value * scaleFactor;
847
+ }
848
+ function useDeviceContract(deviceId, orientation = "portrait") {
849
+ return useMemo(() => {
850
+ const contract = getDeviceContract(deviceId, orientation);
851
+ return {
852
+ contract,
853
+ cssVariables: contract.cssVariables,
854
+ contentZone: contract.contentZone[orientation]
855
+ };
856
+ }, [deviceId, orientation]);
857
+ }
858
+ var STEPS = 16;
859
+ var STEP_SIZE = 1 / STEPS;
860
+ var HUD_DISPLAY_MS = 1500;
861
+ function useVolumeControl(initialVolume = 1) {
862
+ const [level, setLevel] = useState(initialVolume);
863
+ const [muted, setMuted] = useState(false);
864
+ const [hudVisible, setHudVisible] = useState(false);
865
+ const hudTimerRef = useRef(null);
866
+ const showHud = useCallback(() => {
867
+ setHudVisible(true);
868
+ if (hudTimerRef.current) clearTimeout(hudTimerRef.current);
869
+ hudTimerRef.current = setTimeout(() => setHudVisible(false), HUD_DISPLAY_MS);
870
+ }, []);
871
+ const volumeUp = useCallback(() => {
872
+ setLevel((prev) => {
873
+ const next = Math.min(1, Math.round((prev + STEP_SIZE) * STEPS) / STEPS);
874
+ return next;
875
+ });
876
+ setMuted(false);
877
+ showHud();
878
+ }, [showHud]);
879
+ const volumeDown = useCallback(() => {
880
+ setLevel((prev) => {
881
+ const next = Math.max(0, Math.round((prev - STEP_SIZE) * STEPS) / STEPS);
882
+ return next;
883
+ });
884
+ setMuted(false);
885
+ showHud();
886
+ }, [showHud]);
887
+ const toggleMute = useCallback(() => {
888
+ setMuted((prev) => !prev);
889
+ showHud();
890
+ }, [showHud]);
891
+ const effectiveVolume = muted ? 0 : level;
892
+ useEffect(() => {
893
+ const container = document.querySelector(".bielaframe-content");
894
+ if (!container) return;
895
+ const mediaEls = container.querySelectorAll("audio, video");
896
+ mediaEls.forEach((el) => {
897
+ el.volume = effectiveVolume;
898
+ });
899
+ }, [effectiveVolume]);
900
+ useEffect(() => {
901
+ return () => {
902
+ if (hudTimerRef.current) clearTimeout(hudTimerRef.current);
903
+ };
904
+ }, []);
905
+ return { level, muted, hudVisible, volumeUp, volumeDown, toggleMute };
906
+ }
907
+ function useScreenPower() {
908
+ const [isOff, setIsOff] = useState(false);
909
+ const toggle = useCallback(() => {
910
+ setIsOff((prev) => !prev);
911
+ }, []);
912
+ return { isOff, toggle };
913
+ }
914
+ function useOrientation(initial = "portrait") {
915
+ const [orientation, setOrientation] = useState(initial);
916
+ const toggle = useCallback(
917
+ () => setOrientation((o) => o === "portrait" ? "landscape" : "portrait"),
918
+ []
919
+ );
920
+ return {
921
+ orientation,
922
+ isLandscape: orientation === "landscape",
923
+ toggle,
924
+ setOrientation
925
+ };
926
+ }
927
+ function DeviceCompare({
928
+ deviceA,
929
+ deviceB,
930
+ orientation = "portrait",
931
+ colorScheme = "dark",
932
+ showSafeAreaOverlay = false,
933
+ showScaleBar = false,
934
+ layout = "auto",
935
+ gap = 24,
936
+ children,
937
+ childrenA,
938
+ childrenB,
939
+ onContractReadyA,
940
+ onContractReadyB
941
+ }) {
942
+ const isLandscape = orientation === "landscape";
943
+ const effectiveLayout = layout === "auto" ? isLandscape ? "vertical" : "horizontal" : layout;
944
+ const flexDirection = effectiveLayout === "horizontal" ? "row" : "column";
945
+ return /* @__PURE__ */ jsxs(
946
+ "div",
947
+ {
948
+ className: "bielaframe-compare",
949
+ style: {
950
+ width: "100%",
951
+ height: "100%",
952
+ display: "flex",
953
+ flexDirection,
954
+ alignItems: "center",
955
+ justifyContent: "center",
956
+ gap,
957
+ overflow: "hidden"
958
+ },
959
+ children: [
960
+ /* @__PURE__ */ jsx(
961
+ "div",
962
+ {
963
+ style: {
964
+ flex: 1,
965
+ width: effectiveLayout === "horizontal" ? 0 : "100%",
966
+ height: effectiveLayout === "vertical" ? 0 : "100%",
967
+ minWidth: 0,
968
+ minHeight: 0
969
+ },
970
+ children: /* @__PURE__ */ jsx(
971
+ DeviceFrame,
972
+ {
973
+ device: deviceA,
974
+ orientation,
975
+ colorScheme,
976
+ showSafeAreaOverlay,
977
+ showScaleBar,
978
+ onContractReady: onContractReadyA,
979
+ children: childrenA ?? children
980
+ }
981
+ )
982
+ }
983
+ ),
984
+ /* @__PURE__ */ jsx(
985
+ "div",
986
+ {
987
+ style: {
988
+ flex: 1,
989
+ width: effectiveLayout === "horizontal" ? 0 : "100%",
990
+ height: effectiveLayout === "vertical" ? 0 : "100%",
991
+ minWidth: 0,
992
+ minHeight: 0
993
+ },
994
+ children: /* @__PURE__ */ jsx(
995
+ DeviceFrame,
996
+ {
997
+ device: deviceB,
998
+ orientation,
999
+ colorScheme,
1000
+ showSafeAreaOverlay,
1001
+ showScaleBar,
1002
+ onContractReady: onContractReadyB,
1003
+ children: childrenB ?? children
1004
+ }
1005
+ )
1006
+ }
1007
+ )
1008
+ ]
1009
+ }
1010
+ );
1011
+ }
786
1012
  function SafeAreaView({ edges, children, style }) {
787
1013
  const allEdges = !edges || edges.length === 0;
788
1014
  const padding = {
@@ -1170,6 +1396,6 @@ var styles = {
1170
1396
  }
1171
1397
  };
1172
1398
 
1173
- export { DeviceErrorBoundary, DeviceFrame, DynamicStatusBar, HardwareButtons, SCALE_STEPS, SafeAreaOverlay, SafeAreaView, ScaleBar, StatusBarIndicators, VolumeHUD, computeAdaptiveScale, computeFullScale, computeHostSize, ptsToPercent, ptsToPx, pxToPts, registerCustomDeviceSVG, registerDeviceSVG, scaleValue, snapToStep, useAdaptiveScale, useContainerSize, useDeviceContract, useScreenPower, useVolumeControl };
1399
+ export { CustomSVGStore, DeviceCompare, DeviceErrorBoundary, DeviceFrame, DynamicStatusBar, HardwareButtons, SCALE_STEPS, SafeAreaOverlay, SafeAreaView, ScaleBar, StatusBarIndicators, VolumeHUD, computeAdaptiveScale, computeFullScale, computeHostSize, getCustomSVGStore, ptsToPercent, ptsToPx, pxToPts, registerCustomDeviceSVG, registerDeviceSVG, scaleValue, snapToStep, useAdaptiveScale, useContainerSize, useDeviceContract, useOrientation, useScreenPower, useVolumeControl };
1174
1400
  //# sourceMappingURL=index.js.map
1175
1401
  //# sourceMappingURL=index.js.map