@biela.dev/core 1.5.1 → 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.d.cts CHANGED
@@ -158,6 +158,27 @@ interface ScreenPowerState {
158
158
  */
159
159
  declare function useScreenPower(): ScreenPowerState;
160
160
 
161
+ interface UseOrientationResult {
162
+ /** Current orientation */
163
+ orientation: "portrait" | "landscape";
164
+ /** Convenience boolean */
165
+ isLandscape: boolean;
166
+ /** Toggle between portrait and landscape */
167
+ toggle: () => void;
168
+ /** Set orientation directly */
169
+ setOrientation: (o: "portrait" | "landscape") => void;
170
+ }
171
+ /**
172
+ * Hook for managing device orientation state with a toggle function.
173
+ *
174
+ * ```tsx
175
+ * const { orientation, toggle } = useOrientation();
176
+ * <button onClick={toggle}>Rotate</button>
177
+ * <DeviceFrame deviceId="iphone-17-pro" orientation={orientation} />
178
+ * ```
179
+ */
180
+ declare function useOrientation(initial?: "portrait" | "landscape"): UseOrientationResult;
181
+
161
182
  /**
162
183
  * Resolve device SVG component by ID.
163
184
  * This is a lookup registry — devices register their SVG components here.
@@ -256,6 +277,44 @@ interface DeviceFrameProps {
256
277
  */
257
278
  declare function DeviceFrame({ device: deviceProp, deviceId, orientation, scaleMode, manualScale, showSafeAreaOverlay, showScaleBar, colorScheme, onContractReady, onScaleChange, children, }: DeviceFrameProps): react_jsx_runtime.JSX.Element;
258
279
 
280
+ interface DeviceCompareProps {
281
+ /** First device ID */
282
+ deviceA: string;
283
+ /** Second device ID */
284
+ deviceB: string;
285
+ /** Orientation for both devices */
286
+ orientation?: "portrait" | "landscape";
287
+ /** Frame color scheme */
288
+ colorScheme?: "light" | "dark";
289
+ /** Show safe area overlays */
290
+ showSafeAreaOverlay?: boolean;
291
+ /** Show scale bars */
292
+ showScaleBar?: boolean;
293
+ /** Layout direction — "auto" picks row for portrait, column for landscape */
294
+ layout?: "horizontal" | "vertical" | "auto";
295
+ /** Gap between the two frames in px (default: 24) */
296
+ gap?: number;
297
+ /** Content rendered inside BOTH device frames */
298
+ children?: ReactNode;
299
+ /** Content for device A only (overrides children for A) */
300
+ childrenA?: ReactNode;
301
+ /** Content for device B only (overrides children for B) */
302
+ childrenB?: ReactNode;
303
+ /** Callback when device A contract is ready */
304
+ onContractReadyA?: (dlc: DeviceLayoutContract) => void;
305
+ /** Callback when device B contract is ready */
306
+ onContractReadyB?: (dlc: DeviceLayoutContract) => void;
307
+ }
308
+ /**
309
+ * DeviceCompare — renders two device frames side-by-side for comparison.
310
+ *
311
+ * Layout defaults to "auto": horizontal (row) in portrait, vertical (column) in landscape.
312
+ * Each frame auto-scales independently to fit its half of the container.
313
+ *
314
+ * Must be placed inside a container with explicit width and height.
315
+ */
316
+ declare function DeviceCompare({ deviceA, deviceB, orientation, colorScheme, showSafeAreaOverlay, showScaleBar, layout, gap, children, childrenA, childrenB, onContractReadyA, onContractReadyB, }: DeviceCompareProps): react_jsx_runtime.JSX.Element;
317
+
259
318
  interface Props {
260
319
  children: ReactNode;
261
320
  fallback?: ReactNode;
@@ -407,4 +466,46 @@ interface StatusBarIndicatorsProps {
407
466
  */
408
467
  declare function StatusBarIndicators({ platform, colorScheme }: StatusBarIndicatorsProps): react_jsx_runtime.JSX.Element;
409
468
 
410
- export { type AdaptiveScaleResult, type ButtonName, DeviceErrorBoundary, DeviceFrame, type DeviceFrameProps, DynamicStatusBar, HardwareButtons, SCALE_STEPS, type SVGCropArea, type SVGScreenRect, SafeAreaOverlay, SafeAreaView, ScaleBar, type ScreenPowerState, StatusBarIndicators, type UseAdaptiveScaleOptions, VolumeHUD, type VolumeState, computeAdaptiveScale, computeFullScale, computeHostSize, ptsToPercent, ptsToPx, pxToPts, registerCustomDeviceSVG, registerDeviceSVG, scaleValue, snapToStep, useAdaptiveScale, useContainerSize, useDeviceContract, useScreenPower, useVolumeControl };
469
+ interface SVGOverrideEntry {
470
+ deviceId: string;
471
+ svgString: string;
472
+ bezelTop: number;
473
+ bezelBottom: number;
474
+ bezelLeft: number;
475
+ bezelRight: number;
476
+ screenRect?: SVGScreenRect;
477
+ updatedAt: string;
478
+ }
479
+ /**
480
+ * Persistent storage for custom SVG overrides.
481
+ *
482
+ * When a user uploads a custom SVG frame for a device, it's stored in
483
+ * localStorage and automatically applied on subsequent imports.
484
+ *
485
+ * SSR-safe: falls back to no-op storage when localStorage is unavailable.
486
+ */
487
+ declare class CustomSVGStore {
488
+ private storage;
489
+ constructor(storage?: Storage | null);
490
+ /** Load all stored overrides */
491
+ getAll(): Record<string, SVGOverrideEntry>;
492
+ /** Save an override and register it in the SVG registry */
493
+ save(entry: SVGOverrideEntry): void;
494
+ /** Remove an override (revert to built-in) */
495
+ remove(deviceId: string): void;
496
+ /** Check if a device has a custom override */
497
+ has(deviceId: string): boolean;
498
+ /** Get a single override by device ID */
499
+ get(deviceId: string): SVGOverrideEntry | undefined;
500
+ /**
501
+ * Apply all stored overrides to the SVG registry.
502
+ * Called during auto-registration to restore user customizations.
503
+ */
504
+ applyAll(): void;
505
+ private applyEntry;
506
+ private persist;
507
+ }
508
+ /** Get the default CustomSVGStore instance (singleton, uses localStorage) */
509
+ declare function getCustomSVGStore(): CustomSVGStore;
510
+
511
+ export { type AdaptiveScaleResult, type ButtonName, CustomSVGStore, DeviceCompare, type DeviceCompareProps, DeviceErrorBoundary, DeviceFrame, type DeviceFrameProps, DynamicStatusBar, HardwareButtons, SCALE_STEPS, type SVGCropArea, type SVGOverrideEntry, type SVGScreenRect, SafeAreaOverlay, SafeAreaView, ScaleBar, type ScreenPowerState, StatusBarIndicators, type UseAdaptiveScaleOptions, type UseOrientationResult, VolumeHUD, type VolumeState, computeAdaptiveScale, computeFullScale, computeHostSize, getCustomSVGStore, ptsToPercent, ptsToPx, pxToPts, registerCustomDeviceSVG, registerDeviceSVG, scaleValue, snapToStep, useAdaptiveScale, useContainerSize, useDeviceContract, useOrientation, useScreenPower, useVolumeControl };
package/dist/index.d.ts CHANGED
@@ -158,6 +158,27 @@ interface ScreenPowerState {
158
158
  */
159
159
  declare function useScreenPower(): ScreenPowerState;
160
160
 
161
+ interface UseOrientationResult {
162
+ /** Current orientation */
163
+ orientation: "portrait" | "landscape";
164
+ /** Convenience boolean */
165
+ isLandscape: boolean;
166
+ /** Toggle between portrait and landscape */
167
+ toggle: () => void;
168
+ /** Set orientation directly */
169
+ setOrientation: (o: "portrait" | "landscape") => void;
170
+ }
171
+ /**
172
+ * Hook for managing device orientation state with a toggle function.
173
+ *
174
+ * ```tsx
175
+ * const { orientation, toggle } = useOrientation();
176
+ * <button onClick={toggle}>Rotate</button>
177
+ * <DeviceFrame deviceId="iphone-17-pro" orientation={orientation} />
178
+ * ```
179
+ */
180
+ declare function useOrientation(initial?: "portrait" | "landscape"): UseOrientationResult;
181
+
161
182
  /**
162
183
  * Resolve device SVG component by ID.
163
184
  * This is a lookup registry — devices register their SVG components here.
@@ -256,6 +277,44 @@ interface DeviceFrameProps {
256
277
  */
257
278
  declare function DeviceFrame({ device: deviceProp, deviceId, orientation, scaleMode, manualScale, showSafeAreaOverlay, showScaleBar, colorScheme, onContractReady, onScaleChange, children, }: DeviceFrameProps): react_jsx_runtime.JSX.Element;
258
279
 
280
+ interface DeviceCompareProps {
281
+ /** First device ID */
282
+ deviceA: string;
283
+ /** Second device ID */
284
+ deviceB: string;
285
+ /** Orientation for both devices */
286
+ orientation?: "portrait" | "landscape";
287
+ /** Frame color scheme */
288
+ colorScheme?: "light" | "dark";
289
+ /** Show safe area overlays */
290
+ showSafeAreaOverlay?: boolean;
291
+ /** Show scale bars */
292
+ showScaleBar?: boolean;
293
+ /** Layout direction — "auto" picks row for portrait, column for landscape */
294
+ layout?: "horizontal" | "vertical" | "auto";
295
+ /** Gap between the two frames in px (default: 24) */
296
+ gap?: number;
297
+ /** Content rendered inside BOTH device frames */
298
+ children?: ReactNode;
299
+ /** Content for device A only (overrides children for A) */
300
+ childrenA?: ReactNode;
301
+ /** Content for device B only (overrides children for B) */
302
+ childrenB?: ReactNode;
303
+ /** Callback when device A contract is ready */
304
+ onContractReadyA?: (dlc: DeviceLayoutContract) => void;
305
+ /** Callback when device B contract is ready */
306
+ onContractReadyB?: (dlc: DeviceLayoutContract) => void;
307
+ }
308
+ /**
309
+ * DeviceCompare — renders two device frames side-by-side for comparison.
310
+ *
311
+ * Layout defaults to "auto": horizontal (row) in portrait, vertical (column) in landscape.
312
+ * Each frame auto-scales independently to fit its half of the container.
313
+ *
314
+ * Must be placed inside a container with explicit width and height.
315
+ */
316
+ declare function DeviceCompare({ deviceA, deviceB, orientation, colorScheme, showSafeAreaOverlay, showScaleBar, layout, gap, children, childrenA, childrenB, onContractReadyA, onContractReadyB, }: DeviceCompareProps): react_jsx_runtime.JSX.Element;
317
+
259
318
  interface Props {
260
319
  children: ReactNode;
261
320
  fallback?: ReactNode;
@@ -407,4 +466,46 @@ interface StatusBarIndicatorsProps {
407
466
  */
408
467
  declare function StatusBarIndicators({ platform, colorScheme }: StatusBarIndicatorsProps): react_jsx_runtime.JSX.Element;
409
468
 
410
- export { type AdaptiveScaleResult, type ButtonName, DeviceErrorBoundary, DeviceFrame, type DeviceFrameProps, DynamicStatusBar, HardwareButtons, SCALE_STEPS, type SVGCropArea, type SVGScreenRect, SafeAreaOverlay, SafeAreaView, ScaleBar, type ScreenPowerState, StatusBarIndicators, type UseAdaptiveScaleOptions, VolumeHUD, type VolumeState, computeAdaptiveScale, computeFullScale, computeHostSize, ptsToPercent, ptsToPx, pxToPts, registerCustomDeviceSVG, registerDeviceSVG, scaleValue, snapToStep, useAdaptiveScale, useContainerSize, useDeviceContract, useScreenPower, useVolumeControl };
469
+ interface SVGOverrideEntry {
470
+ deviceId: string;
471
+ svgString: string;
472
+ bezelTop: number;
473
+ bezelBottom: number;
474
+ bezelLeft: number;
475
+ bezelRight: number;
476
+ screenRect?: SVGScreenRect;
477
+ updatedAt: string;
478
+ }
479
+ /**
480
+ * Persistent storage for custom SVG overrides.
481
+ *
482
+ * When a user uploads a custom SVG frame for a device, it's stored in
483
+ * localStorage and automatically applied on subsequent imports.
484
+ *
485
+ * SSR-safe: falls back to no-op storage when localStorage is unavailable.
486
+ */
487
+ declare class CustomSVGStore {
488
+ private storage;
489
+ constructor(storage?: Storage | null);
490
+ /** Load all stored overrides */
491
+ getAll(): Record<string, SVGOverrideEntry>;
492
+ /** Save an override and register it in the SVG registry */
493
+ save(entry: SVGOverrideEntry): void;
494
+ /** Remove an override (revert to built-in) */
495
+ remove(deviceId: string): void;
496
+ /** Check if a device has a custom override */
497
+ has(deviceId: string): boolean;
498
+ /** Get a single override by device ID */
499
+ get(deviceId: string): SVGOverrideEntry | undefined;
500
+ /**
501
+ * Apply all stored overrides to the SVG registry.
502
+ * Called during auto-registration to restore user customizations.
503
+ */
504
+ applyAll(): void;
505
+ private applyEntry;
506
+ private persist;
507
+ }
508
+ /** Get the default CustomSVGStore instance (singleton, uses localStorage) */
509
+ declare function getCustomSVGStore(): CustomSVGStore;
510
+
511
+ export { type AdaptiveScaleResult, type ButtonName, CustomSVGStore, DeviceCompare, type DeviceCompareProps, DeviceErrorBoundary, DeviceFrame, type DeviceFrameProps, DynamicStatusBar, HardwareButtons, SCALE_STEPS, type SVGCropArea, type SVGOverrideEntry, type SVGScreenRect, SafeAreaOverlay, SafeAreaView, ScaleBar, type ScreenPowerState, StatusBarIndicators, type UseAdaptiveScaleOptions, type UseOrientationResult, VolumeHUD, type VolumeState, computeAdaptiveScale, computeFullScale, computeHostSize, getCustomSVGStore, ptsToPercent, ptsToPx, pxToPts, registerCustomDeviceSVG, registerDeviceSVG, scaleValue, snapToStep, useAdaptiveScale, useContainerSize, useDeviceContract, useOrientation, useScreenPower, useVolumeControl };
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);
@@ -789,6 +712,303 @@ function DeviceFrame({
789
712
  }
790
713
  );
791
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
+ }
792
1012
  function SafeAreaView({ edges, children, style }) {
793
1013
  const allEdges = !edges || edges.length === 0;
794
1014
  const padding = {
@@ -1176,6 +1396,6 @@ var styles = {
1176
1396
  }
1177
1397
  };
1178
1398
 
1179
- 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 };
1180
1400
  //# sourceMappingURL=index.js.map
1181
1401
  //# sourceMappingURL=index.js.map