@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.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);
@@ -559,7 +482,8 @@ function registerCustomDeviceSVG(deviceId, svgString, frame, cropViewBox, screen
559
482
  SVG_REGISTRY[deviceId] = { component: CustomSVGComponent, frame };
560
483
  }
561
484
  function DeviceFrame({
562
- device,
485
+ device: deviceProp,
486
+ deviceId,
563
487
  orientation = "portrait",
564
488
  scaleMode = "fit",
565
489
  manualScale,
@@ -570,23 +494,28 @@ function DeviceFrame({
570
494
  onScaleChange,
571
495
  children
572
496
  }) {
497
+ const device = deviceProp ?? deviceId ?? "";
573
498
  const sentinelRef = react.useRef(null);
574
499
  const frameOverlayRef = react.useRef(null);
575
500
  const hostRef = react.useRef(null);
576
501
  const scalerRef = react.useRef(null);
577
502
  const { width: containerW, height: containerH } = useContainerSize(sentinelRef);
578
503
  const deviceLookup = react.useMemo(() => {
504
+ if (!device) {
505
+ return { meta: null, contract: null, error: "(no device specified)" };
506
+ }
579
507
  try {
580
- return {
581
- meta: devices.getDeviceMetadata(device),
582
- contract: devices.getDeviceContract(device, orientation),
583
- error: null
584
- };
508
+ const meta = devices.getDeviceMetadata(device);
509
+ const contract2 = devices.getDeviceContract(device, orientation);
510
+ if (!meta || !meta.screen) {
511
+ return { meta: null, contract: null, error: device };
512
+ }
513
+ return { meta, contract: contract2, error: null };
585
514
  } catch {
586
515
  return { meta: null, contract: null, error: device };
587
516
  }
588
517
  }, [device, orientation]);
589
- if (deviceLookup.error) {
518
+ if (deviceLookup.error || !deviceLookup.meta || !deviceLookup.contract) {
590
519
  return /* @__PURE__ */ jsxRuntime.jsxs(
591
520
  "div",
592
521
  {
@@ -603,7 +532,7 @@ function DeviceFrame({
603
532
  },
604
533
  children: [
605
534
  'Device not found: "',
606
- deviceLookup.error,
535
+ deviceLookup.error || "(unknown)",
607
536
  '"'
608
537
  ]
609
538
  }
@@ -785,6 +714,303 @@ function DeviceFrame({
785
714
  }
786
715
  );
787
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
+ }
788
1014
  function SafeAreaView({ edges, children, style }) {
789
1015
  const allEdges = !edges || edges.length === 0;
790
1016
  const padding = {
@@ -1172,6 +1398,8 @@ var styles = {
1172
1398
  }
1173
1399
  };
1174
1400
 
1401
+ exports.CustomSVGStore = CustomSVGStore;
1402
+ exports.DeviceCompare = DeviceCompare;
1175
1403
  exports.DeviceErrorBoundary = DeviceErrorBoundary;
1176
1404
  exports.DeviceFrame = DeviceFrame;
1177
1405
  exports.DynamicStatusBar = DynamicStatusBar;
@@ -1185,6 +1413,7 @@ exports.VolumeHUD = VolumeHUD;
1185
1413
  exports.computeAdaptiveScale = computeAdaptiveScale;
1186
1414
  exports.computeFullScale = computeFullScale;
1187
1415
  exports.computeHostSize = computeHostSize;
1416
+ exports.getCustomSVGStore = getCustomSVGStore;
1188
1417
  exports.ptsToPercent = ptsToPercent;
1189
1418
  exports.ptsToPx = ptsToPx;
1190
1419
  exports.pxToPts = pxToPts;
@@ -1195,6 +1424,7 @@ exports.snapToStep = snapToStep;
1195
1424
  exports.useAdaptiveScale = useAdaptiveScale;
1196
1425
  exports.useContainerSize = useContainerSize;
1197
1426
  exports.useDeviceContract = useDeviceContract;
1427
+ exports.useOrientation = useOrientation;
1198
1428
  exports.useScreenPower = useScreenPower;
1199
1429
  exports.useVolumeControl = useVolumeControl;
1200
1430
  //# sourceMappingURL=index.cjs.map