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