@mmtitanl/tablets-core 0.1.1

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 ADDED
@@ -0,0 +1,1242 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ BIELA_PREFIX: () => BIELA_PREFIX,
24
+ CustomSVGStore: () => CustomSVGStore,
25
+ DeviceCompare: () => DeviceCompare,
26
+ DeviceErrorBoundary: () => DeviceErrorBoundary,
27
+ DeviceFrame: () => DeviceFrame,
28
+ DynamicStatusBar: () => DynamicStatusBar,
29
+ HardwareButtons: () => HardwareButtons,
30
+ SCALE_STEPS: () => SCALE_STEPS,
31
+ SafeAreaOverlay: () => SafeAreaOverlay,
32
+ SafeAreaView: () => SafeAreaView,
33
+ ScaleBar: () => ScaleBar,
34
+ StatusBarIndicators: () => StatusBarIndicators,
35
+ VolumeHUD: () => VolumeHUD,
36
+ computeAdaptiveScale: () => computeAdaptiveScale,
37
+ computeFullScale: () => computeFullScale,
38
+ computeHostSize: () => computeHostSize,
39
+ getCustomSVGStore: () => getCustomSVGStore,
40
+ isBielaMessage: () => isBielaMessage,
41
+ ptsToPercent: () => ptsToPercent,
42
+ ptsToPx: () => ptsToPx,
43
+ pxToPts: () => pxToPts,
44
+ registerCustomDeviceSVG: () => registerCustomDeviceSVG,
45
+ registerDeviceSVG: () => registerDeviceSVG,
46
+ scaleValue: () => scaleValue,
47
+ snapToStep: () => snapToStep,
48
+ useAdaptiveScale: () => useAdaptiveScale,
49
+ useContainerSize: () => useContainerSize,
50
+ useDeviceContract: () => useDeviceContract,
51
+ useDeviceFrame: () => useDeviceFrame,
52
+ useFoldState: () => useFoldState,
53
+ useOrientation: () => useOrientation,
54
+ useScreenPower: () => useScreenPower,
55
+ useVolumeControl: () => useVolumeControl
56
+ });
57
+ module.exports = __toCommonJS(src_exports);
58
+
59
+ // src/math/scale.ts
60
+ function ptsToPx(pts, dpr) {
61
+ return pts * dpr;
62
+ }
63
+ function pxToPts(px, dpr) {
64
+ return px / dpr;
65
+ }
66
+ function ptsToPercent(pts, total) {
67
+ return pts / total * 100;
68
+ }
69
+ function scaleValue(value, scaleFactor) {
70
+ return value * scaleFactor;
71
+ }
72
+ var SCALE_STEPS = [0.25, 0.33, 0.5, 0.75, 1];
73
+ function computeAdaptiveScale(deviceWidth, deviceHeight, containerWidth, containerHeight, padding = 24, maxScale = 1, minScale = 0.1) {
74
+ const availW = Math.max(0, containerWidth - padding * 2);
75
+ const availH = Math.max(0, containerHeight - padding * 2);
76
+ if (availW <= 0 || availH <= 0) return minScale;
77
+ const sx = availW / deviceWidth;
78
+ const sy = availH / deviceHeight;
79
+ const raw = Math.min(sx, sy, maxScale);
80
+ return Math.max(raw, minScale);
81
+ }
82
+ function snapToStep(raw) {
83
+ const allowed = SCALE_STEPS.filter((s) => s <= raw);
84
+ if (allowed.length === 0) return raw;
85
+ return Math.max(...allowed);
86
+ }
87
+ function computeHostSize(deviceWidth, deviceHeight, scale) {
88
+ return { width: deviceWidth * scale, height: deviceHeight * scale };
89
+ }
90
+ function computeFullScale(deviceWidth, deviceHeight, containerWidth, containerHeight, options = {}) {
91
+ const { padding = 24, maxScale = 1, minScale = 0.1, snapToSteps = false } = options;
92
+ const raw = computeAdaptiveScale(deviceWidth, deviceHeight, containerWidth, containerHeight, padding, maxScale, minScale);
93
+ const scale = snapToSteps ? snapToStep(raw) : raw;
94
+ const { width: scaledWidth, height: scaledHeight } = computeHostSize(deviceWidth, deviceHeight, scale);
95
+ return {
96
+ scale,
97
+ scaledWidth,
98
+ scaledHeight,
99
+ deviceWidth,
100
+ deviceHeight,
101
+ isAtMaxScale: scale >= maxScale - 1e-4,
102
+ isConstrained: scale < maxScale - 1e-4,
103
+ scalePercent: `${Math.round(scale * 100)}%`
104
+ };
105
+ }
106
+
107
+ // src/hooks/use-container-size.ts
108
+ var import_react = require("react");
109
+ function clampToViewport(w, h) {
110
+ if (typeof window === "undefined") return { width: w, height: h };
111
+ const vw = window.visualViewport?.width ?? window.innerWidth;
112
+ const vh = window.visualViewport?.height ?? window.innerHeight;
113
+ return { width: Math.min(w, vw), height: Math.min(h, vh) };
114
+ }
115
+ function viewportFallback() {
116
+ if (typeof window === "undefined") return { width: 800, height: 600 };
117
+ return {
118
+ width: window.visualViewport?.width ?? window.innerWidth,
119
+ height: window.visualViewport?.height ?? window.innerHeight
120
+ };
121
+ }
122
+ function useContainerSize(ref) {
123
+ const [size, setSize] = (0, import_react.useState)(() => viewportFallback());
124
+ (0, import_react.useEffect)(() => {
125
+ const el = ref.current;
126
+ if (!el) return;
127
+ let rafId = null;
128
+ const measure = () => {
129
+ if (rafId !== null) cancelAnimationFrame(rafId);
130
+ rafId = requestAnimationFrame(() => {
131
+ rafId = null;
132
+ const rect = el.getBoundingClientRect();
133
+ const raw = rect.height > 0 ? { width: rect.width, height: rect.height } : viewportFallback();
134
+ setSize(clampToViewport(raw.width, raw.height));
135
+ });
136
+ };
137
+ measure();
138
+ const ro = new ResizeObserver(measure);
139
+ ro.observe(el);
140
+ return () => {
141
+ ro.disconnect();
142
+ if (rafId !== null) cancelAnimationFrame(rafId);
143
+ };
144
+ }, [ref]);
145
+ return size;
146
+ }
147
+
148
+ // src/hooks/use-adaptive-scale.ts
149
+ var import_react2 = require("react");
150
+ function useAdaptiveScale(options) {
151
+ const {
152
+ device,
153
+ containerWidth,
154
+ containerHeight,
155
+ padding = 24,
156
+ maxScale = 1,
157
+ minScale = 0.1,
158
+ snapToSteps = false,
159
+ orientation = "portrait"
160
+ } = options;
161
+ return (0, import_react2.useMemo)(() => {
162
+ const dw = orientation === "portrait" ? device.screen.width : device.screen.height;
163
+ const dh = orientation === "portrait" ? device.screen.height : device.screen.width;
164
+ return computeFullScale(dw, dh, containerWidth, containerHeight, { padding, maxScale, minScale, snapToSteps });
165
+ }, [
166
+ device.screen.width,
167
+ device.screen.height,
168
+ containerWidth,
169
+ containerHeight,
170
+ padding,
171
+ maxScale,
172
+ minScale,
173
+ snapToSteps,
174
+ orientation
175
+ ]);
176
+ }
177
+
178
+ // src/hooks/use-device-contract.ts
179
+ var import_react3 = require("react");
180
+ var import_tablets = require("@mmtitanl/tablets");
181
+ function useDeviceContract(deviceId, orientation = "portrait") {
182
+ return (0, import_react3.useMemo)(() => {
183
+ const contract = (0, import_tablets.getDeviceContract)(deviceId, orientation);
184
+ return {
185
+ contract,
186
+ cssVariables: contract.cssVariables,
187
+ contentZone: orientation === "portrait" ? contract.contentZone.portrait : contract.contentZone.landscape
188
+ };
189
+ }, [deviceId, orientation]);
190
+ }
191
+
192
+ // src/hooks/use-orientation.ts
193
+ var import_react4 = require("react");
194
+ function useOrientation(initial = "portrait") {
195
+ const [orientation, setOrientation] = (0, import_react4.useState)(initial);
196
+ const toggle = (0, import_react4.useCallback)(() => {
197
+ setOrientation((o) => o === "portrait" ? "landscape" : "portrait");
198
+ }, []);
199
+ return { orientation, isLandscape: orientation === "landscape", toggle, setOrientation };
200
+ }
201
+
202
+ // src/hooks/use-volume-control.ts
203
+ var import_react5 = require("react");
204
+ var STEPS = 16;
205
+ function applyToMedia(level, muted) {
206
+ if (typeof document === "undefined") return;
207
+ const nodes = document.querySelectorAll(".bielaframe-content audio, .bielaframe-content video");
208
+ nodes.forEach((el) => {
209
+ el.volume = Math.max(0, Math.min(1, level));
210
+ el.muted = muted;
211
+ });
212
+ }
213
+ function useVolumeControl(initialVolume = 0.5) {
214
+ const [level, setLevel] = (0, import_react5.useState)(initialVolume);
215
+ const [muted, setMuted] = (0, import_react5.useState)(false);
216
+ const [hudVisible, setHudVisible] = (0, import_react5.useState)(false);
217
+ const timer = (0, import_react5.useRef)(null);
218
+ const flashHUD = (0, import_react5.useCallback)(() => {
219
+ setHudVisible(true);
220
+ if (timer.current) window.clearTimeout(timer.current);
221
+ timer.current = window.setTimeout(() => setHudVisible(false), 1500);
222
+ }, []);
223
+ const volumeUp = (0, import_react5.useCallback)(() => {
224
+ setLevel((l) => Math.min(1, l + 1 / STEPS));
225
+ flashHUD();
226
+ }, [flashHUD]);
227
+ const volumeDown = (0, import_react5.useCallback)(() => {
228
+ setLevel((l) => Math.max(0, l - 1 / STEPS));
229
+ flashHUD();
230
+ }, [flashHUD]);
231
+ const toggleMute = (0, import_react5.useCallback)(() => {
232
+ setMuted((m) => !m);
233
+ flashHUD();
234
+ }, [flashHUD]);
235
+ (0, import_react5.useEffect)(() => {
236
+ applyToMedia(level, muted);
237
+ }, [level, muted]);
238
+ return { level, muted, hudVisible, volumeUp, volumeDown, toggleMute };
239
+ }
240
+
241
+ // src/hooks/use-screen-power.ts
242
+ var import_react6 = require("react");
243
+ function useScreenPower() {
244
+ const [isOff, setIsOff] = (0, import_react6.useState)(false);
245
+ const toggle = (0, import_react6.useCallback)(() => setIsOff((v) => !v), []);
246
+ return { isOff, toggle };
247
+ }
248
+
249
+ // src/hooks/use-device-frame.ts
250
+ var import_react7 = require("react");
251
+
252
+ // src/messages.ts
253
+ var BIELA_PREFIX = "biela:";
254
+ function isBielaMessage(data) {
255
+ return typeof data === "object" && data !== null && "type" in data && typeof data.type === "string" && data.type.startsWith(BIELA_PREFIX);
256
+ }
257
+
258
+ // src/hooks/use-device-frame.ts
259
+ function parsePx(value) {
260
+ if (!value) return 0;
261
+ const n = parseFloat(value);
262
+ return Number.isFinite(n) ? n : 0;
263
+ }
264
+ function useDeviceFrame() {
265
+ const [vars, setVars] = (0, import_react7.useState)({});
266
+ const [platform, setPlatform] = (0, import_react7.useState)(null);
267
+ const [deviceId, setDeviceId] = (0, import_react7.useState)(null);
268
+ const [orientation, setOrientation] = (0, import_react7.useState)(null);
269
+ const [isReady, setIsReady] = (0, import_react7.useState)(false);
270
+ (0, import_react7.useEffect)(() => {
271
+ if (typeof window === "undefined") return;
272
+ const handler = (event) => {
273
+ if (!isBielaMessage(event.data)) return;
274
+ if (event.data.type === "biela:deviceInfo") {
275
+ const msg = event.data;
276
+ setVars(msg.payload.vars);
277
+ setPlatform(msg.payload.platform);
278
+ setDeviceId(msg.payload.deviceId);
279
+ setOrientation(msg.payload.orientation);
280
+ setIsReady(true);
281
+ }
282
+ };
283
+ window.addEventListener("message", handler);
284
+ window.parent?.postMessage({ type: "biela:requestDeviceInfo" }, "*");
285
+ return () => window.removeEventListener("message", handler);
286
+ }, []);
287
+ const reportColorScheme = (0, import_react7.useCallback)((scheme) => {
288
+ if (typeof window === "undefined") return;
289
+ window.parent?.postMessage({ type: "biela:colorScheme", payload: { scheme } }, "*");
290
+ }, []);
291
+ const insets = {
292
+ top: parsePx(vars["--safe-top"]),
293
+ bottom: parsePx(vars["--safe-bottom"]),
294
+ left: parsePx(vars["--safe-left"]),
295
+ right: parsePx(vars["--safe-right"])
296
+ };
297
+ return { insets, vars, platform, deviceId, orientation, isReady, reportColorScheme };
298
+ }
299
+
300
+ // src/hooks/use-fold-state.ts
301
+ var import_react8 = require("react");
302
+ function useFoldState(baseDeviceId, initial = "folded") {
303
+ const [foldState, setFoldStateRaw] = (0, import_react8.useState)(initial);
304
+ const toggle = (0, import_react8.useCallback)(() => {
305
+ setFoldStateRaw((s) => s === "folded" ? "open" : "folded");
306
+ }, []);
307
+ const setFoldState = (0, import_react8.useCallback)((state) => {
308
+ setFoldStateRaw(state);
309
+ }, []);
310
+ const isOpen = foldState === "open";
311
+ const deviceId = isOpen ? `${baseDeviceId}-open` : baseDeviceId;
312
+ return { foldState, isOpen, deviceId, toggle, setFoldState };
313
+ }
314
+
315
+ // src/registration.tsx
316
+ var import_tablets2 = require("@mmtitanl/tablets");
317
+ var import_jsx_runtime = require("react/jsx-runtime");
318
+ var SVG_REGISTRY = /* @__PURE__ */ new Map();
319
+ function registerDeviceSVG(deviceId, component, frame, screenRect, landscape) {
320
+ SVG_REGISTRY.set(deviceId, {
321
+ component,
322
+ frame,
323
+ screenRect,
324
+ landscapeComponent: landscape?.component,
325
+ landscapeFrame: landscape?.frame,
326
+ landscapeScreenRect: landscape?.screenRect
327
+ });
328
+ }
329
+ function getDeviceSVG(deviceId) {
330
+ return SVG_REGISTRY.get(deviceId);
331
+ }
332
+ function buildCustomComponent(deviceId, svgString, cropViewBox, screenRect, suffix) {
333
+ const scopeKey = suffix ? `${deviceId}${suffix}` : deviceId;
334
+ let svg = (0, import_tablets2.scopeSVGIds)(svgString, scopeKey);
335
+ if (cropViewBox) {
336
+ svg = svg.replace(
337
+ /<svg\b([^>]*)>/i,
338
+ (_m, attrs) => {
339
+ const stripped = attrs.replace(/\sviewBox\s*=\s*["'][^"']*["']/i, "").replace(/\swidth\s*=\s*["'][^"']*["']/i, "").replace(/\sheight\s*=\s*["'][^"']*["']/i, "");
340
+ return `<svg${stripped} viewBox="${cropViewBox.x} ${cropViewBox.y} ${cropViewBox.width} ${cropViewBox.height}" width="100%" height="100%">`;
341
+ }
342
+ );
343
+ } else {
344
+ svg = svg.replace(
345
+ /<svg\b([^>]*)>/i,
346
+ (_m, attrs) => {
347
+ const stripped = attrs.replace(/\swidth\s*=\s*["'][^"']*["']/i, "").replace(/\sheight\s*=\s*["'][^"']*["']/i, "");
348
+ return `<svg${stripped} width="100%" height="100%">`;
349
+ }
350
+ );
351
+ }
352
+ void screenRect;
353
+ const Component2 = ({ style }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
354
+ "span",
355
+ {
356
+ style: { display: "block", width: "100%", height: "100%", ...style },
357
+ dangerouslySetInnerHTML: { __html: svg }
358
+ }
359
+ );
360
+ Component2.displayName = `CustomDeviceSVG(${scopeKey})`;
361
+ return Component2;
362
+ }
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;
366
+ SVG_REGISTRY.set(deviceId, {
367
+ component: portraitComponent,
368
+ frame,
369
+ screenRect,
370
+ landscapeComponent,
371
+ landscapeFrame: landscape?.frame,
372
+ landscapeScreenRect: landscape?.screenRect
373
+ });
374
+ }
375
+
376
+ // src/components/DeviceFrame.tsx
377
+ var import_react11 = require("react");
378
+ var import_tablets3 = require("@mmtitanl/tablets");
379
+
380
+ // src/components/DeviceErrorBoundary.tsx
381
+ var import_react9 = require("react");
382
+ var import_jsx_runtime2 = require("react/jsx-runtime");
383
+ var DeviceErrorBoundary = class extends import_react9.Component {
384
+ constructor(props) {
385
+ super(props);
386
+ this.state = { hasError: false, error: null };
387
+ }
388
+ static getDerivedStateFromError(error) {
389
+ return { hasError: true, error };
390
+ }
391
+ componentDidCatch(error, errorInfo) {
392
+ console.error("[BielaFrame] device content crashed", error, errorInfo);
393
+ }
394
+ render() {
395
+ if (this.state.hasError) {
396
+ return this.props.fallback ?? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
397
+ "div",
398
+ {
399
+ style: {
400
+ padding: 20,
401
+ fontFamily: "system-ui",
402
+ color: "#ff453a",
403
+ background: "#1c1c1e",
404
+ height: "100%",
405
+ display: "flex",
406
+ alignItems: "center",
407
+ justifyContent: "center",
408
+ textAlign: "center"
409
+ },
410
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
411
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 14, fontWeight: 600, marginBottom: 8 }, children: "Component crashed" }),
412
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 12, opacity: 0.8 }, children: this.state.error?.message })
413
+ ] })
414
+ }
415
+ );
416
+ }
417
+ return this.props.children;
418
+ }
419
+ };
420
+
421
+ // src/components/SafeAreaOverlay.tsx
422
+ var import_jsx_runtime3 = require("react/jsx-runtime");
423
+ function SafeAreaOverlay({ contract, orientation }) {
424
+ const safe = orientation === "portrait" ? contract.safeArea.portrait : contract.safeArea.landscape;
425
+ const w = orientation === "portrait" ? contract.screen.width : contract.screen.height;
426
+ const h = orientation === "portrait" ? contract.screen.height : contract.screen.width;
427
+ const baseStyle = { position: "absolute", pointerEvents: "none" };
428
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
429
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...baseStyle, top: 0, left: 0, right: 0, height: safe.top, background: "rgba(255,69,58,0.25)" } }),
430
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
431
+ "div",
432
+ {
433
+ style: { ...baseStyle, bottom: 0, left: 0, right: 0, height: safe.bottom, background: "rgba(255,69,58,0.25)" }
434
+ }
435
+ ),
436
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...baseStyle, top: 0, left: 0, bottom: 0, width: safe.left, background: "rgba(255,69,58,0.25)" } }),
437
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
438
+ "div",
439
+ {
440
+ style: { ...baseStyle, top: 0, right: 0, bottom: 0, width: safe.right, background: "rgba(255,69,58,0.25)" }
441
+ }
442
+ ),
443
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
444
+ "div",
445
+ {
446
+ style: {
447
+ ...baseStyle,
448
+ top: safe.top,
449
+ bottom: safe.bottom,
450
+ left: safe.left,
451
+ right: safe.right,
452
+ outline: "1px dashed rgba(48,209,88,0.6)"
453
+ }
454
+ }
455
+ ),
456
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
457
+ "div",
458
+ {
459
+ style: {
460
+ ...baseStyle,
461
+ top: 4,
462
+ left: 8,
463
+ color: "white",
464
+ background: "rgba(0,0,0,0.6)",
465
+ padding: "2px 6px",
466
+ fontSize: 10,
467
+ fontFamily: "ui-monospace, monospace",
468
+ borderRadius: 4
469
+ },
470
+ children: [
471
+ w,
472
+ "\xD7",
473
+ h,
474
+ " \xB7 safe ",
475
+ safe.top,
476
+ "/",
477
+ safe.bottom,
478
+ "/",
479
+ safe.left,
480
+ "/",
481
+ safe.right
482
+ ]
483
+ }
484
+ )
485
+ ] });
486
+ }
487
+
488
+ // src/components/ScaleBar.tsx
489
+ var import_jsx_runtime4 = require("react/jsx-runtime");
490
+ function ScaleBar({
491
+ deviceName,
492
+ deviceWidth,
493
+ deviceHeight,
494
+ scale,
495
+ scalePercent,
496
+ isAtMaxScale,
497
+ onScaleChange,
498
+ onFit,
499
+ onRealSize
500
+ }) {
501
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
502
+ "div",
503
+ {
504
+ role: "region",
505
+ "aria-label": "Device scale controls",
506
+ style: {
507
+ display: "flex",
508
+ alignItems: "center",
509
+ gap: 12,
510
+ padding: "8px 12px",
511
+ background: "rgba(28,28,30,0.92)",
512
+ color: "white",
513
+ borderRadius: 8,
514
+ fontFamily: "ui-monospace, monospace",
515
+ fontSize: 12,
516
+ marginTop: 8
517
+ },
518
+ children: [
519
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: deviceName }),
520
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.6 }, children: "\xB7" }),
521
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
522
+ deviceWidth,
523
+ "\xD7",
524
+ deviceHeight,
525
+ "pt"
526
+ ] }),
527
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
528
+ "input",
529
+ {
530
+ type: "range",
531
+ min: 0.1,
532
+ max: 2,
533
+ step: 0.01,
534
+ value: scale,
535
+ "aria-label": "Scale",
536
+ "aria-valuenow": scale,
537
+ role: "slider",
538
+ onChange: (e) => onScaleChange?.(parseFloat(e.target.value)),
539
+ style: { flex: 1 }
540
+ }
541
+ ),
542
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-live": "polite", style: { width: 40, textAlign: "right" }, children: scalePercent }),
543
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: onFit, type: "button", style: btn, children: "Fit" }),
544
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: onRealSize, type: "button", style: { ...btn, opacity: isAtMaxScale ? 0.5 : 1 }, children: "1:1" })
545
+ ]
546
+ }
547
+ );
548
+ }
549
+ var btn = {
550
+ background: "transparent",
551
+ color: "white",
552
+ border: "1px solid rgba(255,255,255,0.25)",
553
+ padding: "2px 8px",
554
+ borderRadius: 4,
555
+ cursor: "pointer",
556
+ fontSize: 11
557
+ };
558
+
559
+ // src/components/DynamicStatusBar.tsx
560
+ var import_react10 = require("react");
561
+ var import_jsx_runtime5 = require("react/jsx-runtime");
562
+ function formatNow() {
563
+ const now = /* @__PURE__ */ new Date();
564
+ return `${now.getHours()}:${now.getMinutes().toString().padStart(2, "0")}`;
565
+ }
566
+ function DynamicStatusBar({
567
+ contract,
568
+ orientation,
569
+ colorScheme,
570
+ showLiveClock = true,
571
+ fixedTime
572
+ }) {
573
+ const [now, setNow] = (0, import_react10.useState)(formatNow);
574
+ (0, import_react10.useEffect)(() => {
575
+ if (!showLiveClock || fixedTime) return;
576
+ const id = window.setInterval(() => setNow(formatNow()), 3e4);
577
+ return () => window.clearInterval(id);
578
+ }, [showLiveClock, fixedTime]);
579
+ if (orientation === "landscape" && contract.device.platform === "ios") return null;
580
+ const time = fixedTime ?? (showLiveClock ? now : "9:41");
581
+ const platform = contract.device.platform;
582
+ const statusBarStyle = contract.statusBar.style;
583
+ const height = contract.statusBar.height;
584
+ const textColor = colorScheme === "dark" ? "#fff" : "#000";
585
+ const bgColor = colorScheme === "dark" ? "#000" : "#fff";
586
+ const fontFamily = platform === "ios" ? "-apple-system, 'SF Pro Text', 'Helvetica Neue', sans-serif" : "'Roboto', 'Google Sans', sans-serif";
587
+ const baseFontSize = platform === "ios" ? 15 : 12;
588
+ const clockStyle = {
589
+ color: textColor,
590
+ fontFamily,
591
+ fontSize: baseFontSize,
592
+ fontWeight: platform === "ios" ? 600 : 400,
593
+ letterSpacing: platform === "ios" ? 0.3 : 0,
594
+ lineHeight: `${height}px`,
595
+ whiteSpace: "nowrap"
596
+ };
597
+ const patchStyle = {
598
+ position: "absolute",
599
+ top: 0,
600
+ height,
601
+ display: "flex",
602
+ alignItems: "center",
603
+ background: bgColor,
604
+ pointerEvents: "none"
605
+ };
606
+ if (platform === "ios" && statusBarStyle !== "dynamic-island" && statusBarStyle !== "notch") {
607
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
608
+ "div",
609
+ {
610
+ "aria-hidden": true,
611
+ style: { ...patchStyle, left: "50%", transform: "translateX(-50%)", paddingLeft: 6, paddingRight: 6 },
612
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { ...clockStyle, fontWeight: 500, fontSize: baseFontSize - 1 }, children: time })
613
+ }
614
+ );
615
+ }
616
+ if (platform === "ios") {
617
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { "aria-hidden": true, style: { ...patchStyle, left: 0, paddingLeft: 20, paddingRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: clockStyle, children: time }) });
618
+ }
619
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { "aria-hidden": true, style: { ...patchStyle, left: 0, paddingLeft: 16, paddingRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: clockStyle, children: time }) });
620
+ }
621
+
622
+ // src/components/DeviceFrame.tsx
623
+ var import_tablets4 = require("@mmtitanl/tablets");
624
+ var import_jsx_runtime6 = require("react/jsx-runtime");
625
+ var didAutoRegister = false;
626
+ function ensureBuiltinsRegistered() {
627
+ if (didAutoRegister) return;
628
+ registerDeviceSVG("ipad-pro-13", import_tablets4.IPadPro13SVG, import_tablets4.IPAD_PRO_13_FRAME, import_tablets4.IPAD_PRO_13_SCREEN_RECT);
629
+ registerDeviceSVG("ipad-pro-11", import_tablets4.IPadPro11SVG, import_tablets4.IPAD_PRO_11_FRAME, import_tablets4.IPAD_PRO_11_SCREEN_RECT);
630
+ registerDeviceSVG("ipad-air-13", import_tablets4.IPadAir13SVG, import_tablets4.IPAD_AIR_13_FRAME, import_tablets4.IPAD_AIR_13_SCREEN_RECT);
631
+ registerDeviceSVG("ipad-air-11", import_tablets4.IPadAir11SVG, import_tablets4.IPAD_AIR_11_FRAME, import_tablets4.IPAD_AIR_11_SCREEN_RECT);
632
+ registerDeviceSVG("ipad-mini-7", import_tablets4.IPadMini7SVG, import_tablets4.IPAD_MINI_7_FRAME, import_tablets4.IPAD_MINI_7_SCREEN_RECT);
633
+ registerDeviceSVG("galaxy-tab-s10", import_tablets4.GalaxyTabS10SVG, import_tablets4.GALAXY_TAB_S10_FRAME, import_tablets4.GALAXY_TAB_S10_SCREEN_RECT);
634
+ registerDeviceSVG("galaxy-tab-s10-ultra", import_tablets4.GalaxyTabS10UltraSVG, import_tablets4.GALAXY_TAB_S10_ULTRA_FRAME, import_tablets4.GALAXY_TAB_S10_ULTRA_SCREEN_RECT);
635
+ registerDeviceSVG("galaxy-tab-s10-fe", import_tablets4.GalaxyTabS10FeSVG, import_tablets4.GALAXY_TAB_S10_FE_FRAME, import_tablets4.GALAXY_TAB_S10_FE_SCREEN_RECT);
636
+ registerDeviceSVG("iphone-17-pro-max", import_tablets4.IPhone17ProMaxSVG, import_tablets4.IPHONE_17_PRO_MAX_FRAME, import_tablets4.IPHONE_17_PRO_MAX_SCREEN_RECT);
637
+ registerDeviceSVG("iphone-17-pro", import_tablets4.IPhone17ProSVG, import_tablets4.IPHONE_17_PRO_FRAME, import_tablets4.IPHONE_17_PRO_SCREEN_RECT);
638
+ registerDeviceSVG("iphone-air", import_tablets4.IPhoneAirSVG, import_tablets4.IPHONE_AIR_FRAME, import_tablets4.IPHONE_AIR_SCREEN_RECT);
639
+ registerDeviceSVG("iphone-16", import_tablets4.IPhone16SVG, import_tablets4.IPHONE_16_FRAME, import_tablets4.IPHONE_16_SCREEN_RECT);
640
+ registerDeviceSVG("iphone-16e", import_tablets4.IPhone16eSVG, import_tablets4.IPHONE_16E_FRAME, import_tablets4.IPHONE_16E_SCREEN_RECT);
641
+ registerDeviceSVG("iphone-se-3", import_tablets4.IPhoneSE3SVG, import_tablets4.IPHONE_SE_3_FRAME, import_tablets4.IPHONE_SE_3_SCREEN_RECT);
642
+ registerDeviceSVG("galaxy-s25", import_tablets4.GalaxyS25SVG, import_tablets4.GALAXY_S25_FRAME, import_tablets4.GALAXY_S25_SCREEN_RECT);
643
+ registerDeviceSVG("galaxy-s25-edge", import_tablets4.GalaxyS25EdgeSVG, import_tablets4.GALAXY_S25_EDGE_FRAME);
644
+ registerDeviceSVG("galaxy-s25-ultra", import_tablets4.GalaxyS25UltraSVG, import_tablets4.GALAXY_S25_ULTRA_FRAME, import_tablets4.GALAXY_S25_ULTRA_SCREEN_RECT);
645
+ registerDeviceSVG("pixel-9-pro", import_tablets4.Pixel9ProSVG, import_tablets4.PIXEL_9_PRO_FRAME, import_tablets4.PIXEL_9_PRO_SCREEN_RECT);
646
+ registerDeviceSVG("pixel-9-pro-xl", import_tablets4.Pixel9ProXLSVG, import_tablets4.PIXEL_9_PRO_XL_FRAME, import_tablets4.PIXEL_9_PRO_XL_SCREEN_RECT);
647
+ registerDeviceSVG("galaxy-z-fold-7", import_tablets4.GalaxyZFold7SVG, import_tablets4.GALAXY_Z_FOLD_7_FRAME, import_tablets4.GALAXY_Z_FOLD_7_SCREEN_RECT);
648
+ registerDeviceSVG("galaxy-z-fold-7-open", import_tablets4.GalaxyZFold7OpenSVG, import_tablets4.GALAXY_Z_FOLD_7_OPEN_FRAME, import_tablets4.GALAXY_Z_FOLD_7_OPEN_SCREEN_RECT);
649
+ didAutoRegister = true;
650
+ }
651
+ function sendDeviceInfo(iframe, contract, orientation) {
652
+ if (!iframe?.contentWindow) return;
653
+ iframe.contentWindow.postMessage(
654
+ {
655
+ type: "biela:deviceInfo",
656
+ payload: {
657
+ vars: contract.cssVariables,
658
+ platform: contract.device.platform,
659
+ deviceId: contract.device.id,
660
+ orientation
661
+ }
662
+ },
663
+ "*"
664
+ );
665
+ }
666
+ function DeviceFrame({
667
+ device,
668
+ deviceId,
669
+ orientation = "portrait",
670
+ scaleMode = "fit",
671
+ manualScale = 1,
672
+ showSafeAreaOverlay = false,
673
+ showScaleBar = true,
674
+ showStatusBar = true,
675
+ colorScheme = "light",
676
+ iframeRef,
677
+ onColorSchemeChange,
678
+ onContractReady,
679
+ onScaleChange,
680
+ children
681
+ }) {
682
+ ensureBuiltinsRegistered();
683
+ const resolvedId = device ?? deviceId;
684
+ if (!resolvedId) throw new Error("DeviceFrame requires `device` or `deviceId`");
685
+ const meta = (0, import_tablets3.getDeviceMetadata)(resolvedId);
686
+ const contract = (0, import_react11.useMemo)(() => (0, import_tablets3.getDeviceContract)(resolvedId, orientation), [resolvedId, orientation]);
687
+ const portW = meta.screen.width;
688
+ const portH = meta.screen.height;
689
+ const rotateFrame = orientation === "landscape";
690
+ const dw = rotateFrame ? portH : portW;
691
+ const dh = rotateFrame ? portW : portH;
692
+ const sentinelRef = (0, import_react11.useRef)(null);
693
+ const frameContainerRef = (0, import_react11.useRef)(null);
694
+ const containerSize = useContainerSize(sentinelRef);
695
+ const svgEntryEarly = getDeviceSVG(resolvedId);
696
+ const portraitFrameEarly = svgEntryEarly?.frame;
697
+ const landscapeFrameEarly = svgEntryEarly?.landscapeFrame;
698
+ const hasLandscapeSVGEarly = !!svgEntryEarly?.landscapeComponent && !!landscapeFrameEarly;
699
+ const fitW = portraitFrameEarly ? rotateFrame ? hasLandscapeSVGEarly ? landscapeFrameEarly.totalWidth : portraitFrameEarly.totalHeight : portraitFrameEarly.totalWidth : rotateFrame ? portH : portW;
700
+ const fitH = portraitFrameEarly ? rotateFrame ? hasLandscapeSVGEarly ? landscapeFrameEarly.totalHeight : portraitFrameEarly.totalWidth : portraitFrameEarly.totalHeight : rotateFrame ? portW : portH;
701
+ const fitResult = (0, import_react11.useMemo)(
702
+ () => computeFullScale(fitW, fitH, containerSize.width, containerSize.height, {
703
+ snapToSteps: scaleMode === "steps"
704
+ }),
705
+ [fitW, fitH, containerSize.width, containerSize.height, scaleMode]
706
+ );
707
+ const [overrideScale, setOverrideScale] = (0, import_react11.useState)(null);
708
+ (0, import_react11.useEffect)(() => {
709
+ setOverrideScale(null);
710
+ }, [orientation]);
711
+ const scale = scaleMode === "manual" ? manualScale : overrideScale ?? fitResult.scale;
712
+ const hostWidth = fitW * scale;
713
+ const hostHeight = fitH * scale;
714
+ const isAtMaxScale = scale >= 0.9999;
715
+ (0, import_react11.useEffect)(() => {
716
+ onContractReady?.(contract);
717
+ }, [contract, onContractReady]);
718
+ (0, import_react11.useEffect)(() => {
719
+ onScaleChange?.(scale);
720
+ }, [scale, onScaleChange]);
721
+ (0, import_react11.useEffect)(() => {
722
+ if (!iframeRef?.current) return;
723
+ sendDeviceInfo(iframeRef.current, contract, orientation);
724
+ const onLoad = () => sendDeviceInfo(iframeRef.current, contract, orientation);
725
+ iframeRef.current.addEventListener("load", onLoad);
726
+ return () => iframeRef.current?.removeEventListener("load", onLoad);
727
+ }, [iframeRef, contract, orientation]);
728
+ (0, import_react11.useEffect)(() => {
729
+ if (!iframeRef) return;
730
+ const handler = (event) => {
731
+ const data = event.data;
732
+ if (!data || typeof data !== "object") return;
733
+ if (data.type === "biela:requestDeviceInfo") {
734
+ sendDeviceInfo(iframeRef.current, contract, orientation);
735
+ } else if (data.type === "biela:colorScheme" && data.payload?.scheme) {
736
+ onColorSchemeChange?.(data.payload.scheme);
737
+ }
738
+ };
739
+ window.addEventListener("message", handler);
740
+ return () => window.removeEventListener("message", handler);
741
+ }, [iframeRef, contract, orientation, onColorSchemeChange]);
742
+ const svgEntry = getDeviceSVG(resolvedId);
743
+ const SVGComponent = svgEntry?.component ?? null;
744
+ const portraitFrame = svgEntry?.frame;
745
+ const LandscapeSVGComponent = svgEntry?.landscapeComponent ?? null;
746
+ const landscapeFrame = svgEntry?.landscapeFrame;
747
+ const hasLandscapeSVG = !!LandscapeSVGComponent && !!landscapeFrame;
748
+ const cssVarsStyle = contract.cssVariables;
749
+ const activeFrame = hasLandscapeSVG && rotateFrame ? landscapeFrame : portraitFrame;
750
+ const scalerW = hasLandscapeSVG ? Math.max(portraitFrame?.totalWidth ?? dw, landscapeFrame?.totalWidth ?? dh) : activeFrame?.totalWidth ?? (rotateFrame ? portW : dw);
751
+ const scalerH = hasLandscapeSVG ? Math.max(portraitFrame?.totalHeight ?? dh, landscapeFrame?.totalHeight ?? dw) : activeFrame?.totalHeight ?? (rotateFrame ? portH : dh);
752
+ const contentBezelLeft = activeFrame?.bezelLeft ?? 0;
753
+ const contentBezelTop = activeFrame?.bezelTop ?? 0;
754
+ const contentScreenW = activeFrame?.screenWidth ?? dw;
755
+ const contentScreenH = activeFrame?.screenHeight ?? dh;
756
+ const baseRadius = activeFrame?.screenRadius ?? meta.screen.cornerRadius ?? 0;
757
+ const radiusTop = activeFrame?.screenRadiusTop ?? baseRadius;
758
+ const radiusBottom = activeFrame?.screenRadiusBottom ?? baseRadius;
759
+ const useRotationFallback = rotateFrame && !hasLandscapeSVG;
760
+ const scalerTransform = useRotationFallback ? `scale(${scale}) translate(0px, ${scalerW}px) rotate(-90deg)` : `scale(${scale})`;
761
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
762
+ "div",
763
+ {
764
+ ref: sentinelRef,
765
+ className: "bielaframe-sentinel",
766
+ style: { width: "100%", height: "100%", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", overflow: "hidden" },
767
+ children: [
768
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
769
+ "div",
770
+ {
771
+ className: "bielaframe-host",
772
+ style: {
773
+ width: hostWidth,
774
+ height: hostHeight,
775
+ position: "relative",
776
+ flexShrink: 0,
777
+ overflow: "hidden",
778
+ transition: "width 400ms cubic-bezier(0.4, 0, 0.2, 1), height 400ms cubic-bezier(0.4, 0, 0.2, 1)"
779
+ },
780
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
781
+ "div",
782
+ {
783
+ className: "bielaframe-scaler",
784
+ ref: frameContainerRef,
785
+ style: {
786
+ position: "absolute",
787
+ top: 0,
788
+ left: 0,
789
+ width: scalerW,
790
+ height: scalerH,
791
+ transform: scalerTransform,
792
+ transformOrigin: "top left",
793
+ willChange: "transform",
794
+ transition: "transform 400ms cubic-bezier(0.4, 0, 0.2, 1)"
795
+ },
796
+ children: [
797
+ SVGComponent && portraitFrame && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
798
+ "div",
799
+ {
800
+ "aria-hidden": true,
801
+ style: {
802
+ position: "absolute",
803
+ top: 0,
804
+ left: 0,
805
+ width: portraitFrame.totalWidth,
806
+ height: portraitFrame.totalHeight,
807
+ pointerEvents: "none",
808
+ zIndex: 1,
809
+ opacity: hasLandscapeSVG && rotateFrame ? 0 : 1,
810
+ transition: "opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)"
811
+ },
812
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
813
+ SVGComponent,
814
+ {
815
+ colorScheme,
816
+ style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" }
817
+ }
818
+ )
819
+ }
820
+ ),
821
+ hasLandscapeSVG && LandscapeSVGComponent && landscapeFrame && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
822
+ "div",
823
+ {
824
+ "aria-hidden": true,
825
+ style: {
826
+ position: "absolute",
827
+ top: 0,
828
+ left: 0,
829
+ width: landscapeFrame.totalWidth,
830
+ height: landscapeFrame.totalHeight,
831
+ pointerEvents: "none",
832
+ zIndex: 1,
833
+ opacity: rotateFrame ? 1 : 0,
834
+ transition: "opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)"
835
+ },
836
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
837
+ LandscapeSVGComponent,
838
+ {
839
+ colorScheme,
840
+ style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" }
841
+ }
842
+ )
843
+ }
844
+ ),
845
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
846
+ "div",
847
+ {
848
+ className: "bielaframe-content",
849
+ style: {
850
+ position: "absolute",
851
+ left: contentBezelLeft,
852
+ top: contentBezelTop,
853
+ width: contentScreenW,
854
+ height: contentScreenH,
855
+ // `border-radius + overflow:hidden` clips the <iframe> child
856
+ // in every browser. clip-path is kept as belt-and-braces — some
857
+ // engines apply it to the iframe, some don't. Per-edge radii
858
+ // (top vs bottom) let devices with asymmetric corners — e.g.
859
+ // flat-bottom — clip correctly.
860
+ borderRadius: `${radiusTop}px ${radiusTop}px ${radiusBottom}px ${radiusBottom}px`,
861
+ clipPath: `inset(0 round ${radiusTop}px ${radiusTop}px ${radiusBottom}px ${radiusBottom}px)`,
862
+ overflow: "hidden",
863
+ isolation: "isolate",
864
+ backfaceVisibility: "hidden",
865
+ transform: "translateZ(0)",
866
+ background: colorScheme === "dark" ? "#000" : "#fff",
867
+ zIndex: 5,
868
+ ...cssVarsStyle
869
+ },
870
+ children: [
871
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DeviceErrorBoundary, { children }),
872
+ showStatusBar && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DynamicStatusBar, { contract, orientation, colorScheme }),
873
+ showSafeAreaOverlay && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SafeAreaOverlay, { contract, orientation })
874
+ ]
875
+ }
876
+ )
877
+ ]
878
+ }
879
+ )
880
+ }
881
+ ),
882
+ showScaleBar && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
883
+ ScaleBar,
884
+ {
885
+ deviceName: meta.name,
886
+ deviceWidth: dw,
887
+ deviceHeight: dh,
888
+ scale,
889
+ scalePercent: `${Math.round(scale * 100)}%`,
890
+ isAtMaxScale,
891
+ isConstrained: !isAtMaxScale,
892
+ onScaleChange: (s) => setOverrideScale(s),
893
+ onFit: () => setOverrideScale(null),
894
+ onRealSize: () => setOverrideScale(1)
895
+ }
896
+ )
897
+ ]
898
+ }
899
+ );
900
+ }
901
+
902
+ // src/components/DeviceCompare.tsx
903
+ var import_jsx_runtime7 = require("react/jsx-runtime");
904
+ function DeviceCompare({
905
+ deviceA,
906
+ deviceB,
907
+ orientation = "portrait",
908
+ colorScheme = "light",
909
+ showSafeAreaOverlay,
910
+ showScaleBar,
911
+ layout = "auto",
912
+ gap = 24,
913
+ children,
914
+ childrenA,
915
+ childrenB,
916
+ onContractReadyA,
917
+ onContractReadyB
918
+ }) {
919
+ const direction = layout === "auto" ? orientation === "portrait" ? "row" : "column" : layout === "horizontal" ? "row" : "column";
920
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", flexDirection: direction, gap, width: "100%", height: "100%" }, children: [
921
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { flex: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
922
+ DeviceFrame,
923
+ {
924
+ deviceId: deviceA,
925
+ orientation,
926
+ colorScheme,
927
+ showSafeAreaOverlay,
928
+ showScaleBar,
929
+ onContractReady: onContractReadyA,
930
+ children: childrenA ?? children
931
+ }
932
+ ) }),
933
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { flex: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
934
+ DeviceFrame,
935
+ {
936
+ deviceId: deviceB,
937
+ orientation,
938
+ colorScheme,
939
+ showSafeAreaOverlay,
940
+ showScaleBar,
941
+ onContractReady: onContractReadyB,
942
+ children: childrenB ?? children
943
+ }
944
+ ) })
945
+ ] });
946
+ }
947
+
948
+ // src/components/SafeAreaView.tsx
949
+ var import_jsx_runtime8 = require("react/jsx-runtime");
950
+ function SafeAreaView({ edges = ["top", "bottom", "left", "right"], children, style }) {
951
+ const padding = {
952
+ paddingTop: edges.includes("top") ? "var(--safe-top, 0px)" : void 0,
953
+ paddingBottom: edges.includes("bottom") ? "var(--safe-bottom, 0px)" : void 0,
954
+ paddingLeft: edges.includes("left") ? "var(--safe-left, 0px)" : void 0,
955
+ paddingRight: edges.includes("right") ? "var(--safe-right, 0px)" : void 0
956
+ };
957
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { ...padding, ...style }, children });
958
+ }
959
+
960
+ // src/components/VolumeHUD.tsx
961
+ var import_jsx_runtime9 = require("react/jsx-runtime");
962
+ function VolumeHUD({ level, muted, visible, platform }) {
963
+ if (!visible) return null;
964
+ const isIOS = platform === "ios";
965
+ const pct = Math.round((muted ? 0 : level) * 100);
966
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
967
+ "div",
968
+ {
969
+ role: "status",
970
+ "aria-label": `Volume ${pct}%`,
971
+ style: {
972
+ position: "absolute",
973
+ ...isIOS ? { top: "12%", left: "50%", transform: "translateX(-50%)", width: 6, height: 220, borderRadius: 3 } : { top: 16, left: "50%", transform: "translateX(-50%)", width: 240, height: 6, borderRadius: 3 },
974
+ background: "rgba(255,255,255,0.2)",
975
+ overflow: "hidden",
976
+ pointerEvents: "none"
977
+ },
978
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
979
+ "div",
980
+ {
981
+ style: {
982
+ background: muted ? "#8e8e93" : "white",
983
+ ...isIOS ? { width: "100%", height: `${pct}%`, marginTop: `${100 - pct}%` } : { width: `${pct}%`, height: "100%" }
984
+ }
985
+ }
986
+ )
987
+ }
988
+ );
989
+ }
990
+
991
+ // src/components/HardwareButtons.tsx
992
+ var import_react12 = require("react");
993
+ var import_jsx_runtime10 = require("react/jsx-runtime");
994
+ function HardwareButtons({
995
+ frameContainerRef,
996
+ onButtonPress,
997
+ enabled = true,
998
+ orientation = "portrait"
999
+ }) {
1000
+ const [rects, setRects] = (0, import_react12.useState)([]);
1001
+ (0, import_react12.useEffect)(() => {
1002
+ if (!enabled) return;
1003
+ const container = frameContainerRef.current;
1004
+ if (!container) return;
1005
+ const found = [];
1006
+ const nodes = container.querySelectorAll("[data-button]");
1007
+ const containerRect = container.getBoundingClientRect();
1008
+ nodes.forEach((node) => {
1009
+ const name = node.getAttribute("data-button");
1010
+ if (!name) return;
1011
+ const r = node.getBoundingClientRect();
1012
+ found.push({
1013
+ name,
1014
+ left: r.left - containerRect.left,
1015
+ top: r.top - containerRect.top,
1016
+ width: r.width,
1017
+ height: r.height
1018
+ });
1019
+ });
1020
+ setRects(found);
1021
+ }, [frameContainerRef, enabled, orientation]);
1022
+ if (!enabled || rects.length === 0) return null;
1023
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children: rects.map((r) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1024
+ "div",
1025
+ {
1026
+ role: "button",
1027
+ "aria-label": r.name,
1028
+ onMouseDown: () => onButtonPress?.(r.name),
1029
+ onTouchStart: () => onButtonPress?.(r.name),
1030
+ style: {
1031
+ position: "absolute",
1032
+ left: r.left,
1033
+ top: r.top,
1034
+ width: r.width,
1035
+ height: r.height,
1036
+ cursor: "pointer",
1037
+ zIndex: 20
1038
+ }
1039
+ },
1040
+ r.name
1041
+ )) });
1042
+ }
1043
+
1044
+ // src/components/StatusBarIndicators.tsx
1045
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1046
+ function StatusBarIndicators({ platform, colorScheme }) {
1047
+ const fill = colorScheme === "dark" ? "white" : "black";
1048
+ if (platform === "ios") {
1049
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
1050
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("svg", { width: "18", height: "10", viewBox: "0 0 18 10", children: [
1051
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "0", y: "6", width: "3", height: "4", fill, rx: "1" }),
1052
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "5", y: "4", width: "3", height: "6", fill, rx: "1" }),
1053
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "10", y: "2", width: "3", height: "8", fill, rx: "1" }),
1054
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "15", y: "0", width: "3", height: "10", fill, rx: "1" })
1055
+ ] }),
1056
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("svg", { width: "14", height: "10", viewBox: "0 0 14 10", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M7 9.5l3-3a4 4 0 0 0-6 0z", fill }) }),
1057
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("svg", { width: "26", height: "12", viewBox: "0 0 26 12", children: [
1058
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "1", y: "1", width: "22", height: "10", rx: "2.5", fill: "none", stroke: fill, strokeWidth: "1" }),
1059
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "24", y: "4", width: "1.5", height: "4", rx: "0.5", fill }),
1060
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "3", y: "3", width: "18", height: "6", rx: "1.5", fill })
1061
+ ] })
1062
+ ] });
1063
+ }
1064
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
1065
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("svg", { width: "14", height: "10", viewBox: "0 0 14 10", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0 9 L14 9 L14 1 Z", fill }) }),
1066
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("svg", { width: "20", height: "10", viewBox: "0 0 20 10", children: [
1067
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "1", y: "1", width: "16", height: "8", rx: "1.5", fill: "none", stroke: fill, strokeWidth: "1" }),
1068
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "18", y: "3", width: "1.5", height: "4", rx: "0.5", fill }),
1069
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "3", y: "3", width: "12", height: "4", fill })
1070
+ ] })
1071
+ ] });
1072
+ }
1073
+
1074
+ // src/storage/custom-svg-store.ts
1075
+ var STORAGE_KEY = "bielaframe-custom-tablet-svgs";
1076
+ var CustomSVGStore = class {
1077
+ storage;
1078
+ constructor(storage) {
1079
+ this.storage = storage ?? (typeof localStorage !== "undefined" ? localStorage : null);
1080
+ }
1081
+ getAll() {
1082
+ if (!this.storage) return {};
1083
+ try {
1084
+ const raw = this.storage.getItem(STORAGE_KEY);
1085
+ return raw ? JSON.parse(raw) : {};
1086
+ } catch {
1087
+ return {};
1088
+ }
1089
+ }
1090
+ save(entry) {
1091
+ const all = this.getAll();
1092
+ all[entry.deviceId] = entry;
1093
+ this.persist(all);
1094
+ this.applyEntry(entry);
1095
+ }
1096
+ remove(deviceId) {
1097
+ const all = this.getAll();
1098
+ delete all[deviceId];
1099
+ this.persist(all);
1100
+ }
1101
+ has(deviceId) {
1102
+ return !!this.getAll()[deviceId];
1103
+ }
1104
+ get(deviceId) {
1105
+ return this.getAll()[deviceId];
1106
+ }
1107
+ applyAll() {
1108
+ const all = this.getAll();
1109
+ Object.values(all).forEach((entry) => this.applyEntry(entry));
1110
+ }
1111
+ applyEntry(entry) {
1112
+ const rawW = entry.screenRect?.width ?? 0;
1113
+ const rawH = entry.screenRect?.height ?? 0;
1114
+ const FORM_FACTOR_LOGICAL_W = {
1115
+ tablet: 834,
1116
+ phone: 390,
1117
+ foldable: 374
1118
+ };
1119
+ const logicalW = entry.logicalScreenWidth ?? (entry.formFactor ? FORM_FACTOR_LOGICAL_W[entry.formFactor] : rawW);
1120
+ const scale = rawW > 0 ? logicalW / rawW : 1;
1121
+ const sr = entry.screenRect ? {
1122
+ x: entry.screenRect.x * scale,
1123
+ y: entry.screenRect.y * scale,
1124
+ width: rawW * scale,
1125
+ height: rawH * scale,
1126
+ rx: (entry.screenRect.rx ?? 0) * scale,
1127
+ rxTop: entry.screenRect.rxTop !== void 0 ? entry.screenRect.rxTop * scale : void 0,
1128
+ rxBottom: entry.screenRect.rxBottom !== void 0 ? entry.screenRect.rxBottom * scale : void 0
1129
+ } : void 0;
1130
+ const portraitBezelTop = entry.bezelTop * scale;
1131
+ const portraitBezelBottom = entry.bezelBottom * scale;
1132
+ const portraitBezelLeft = entry.bezelLeft * scale;
1133
+ const portraitBezelRight = entry.bezelRight * scale;
1134
+ const portraitScreenW = sr?.width ?? logicalW;
1135
+ const portraitScreenH = sr?.height ?? (entry.logicalScreenHeight ?? rawH);
1136
+ let landscape;
1137
+ if (entry.svgStringLandscape && entry.screenRectLandscape) {
1138
+ const lRawW = entry.screenRectLandscape.width;
1139
+ const lRawH = entry.screenRectLandscape.height;
1140
+ const lScale = lRawW > 0 ? (entry.logicalScreenHeight ?? lRawW) / lRawW : 1;
1141
+ const lsr = {
1142
+ x: entry.screenRectLandscape.x * lScale,
1143
+ y: entry.screenRectLandscape.y * lScale,
1144
+ width: lRawW * lScale,
1145
+ height: lRawH * lScale,
1146
+ rx: (entry.screenRectLandscape.rx ?? 0) * lScale,
1147
+ rxTop: entry.screenRectLandscape.rxTop !== void 0 ? entry.screenRectLandscape.rxTop * lScale : void 0,
1148
+ rxBottom: entry.screenRectLandscape.rxBottom !== void 0 ? entry.screenRectLandscape.rxBottom * lScale : void 0
1149
+ };
1150
+ const lBezelTop = (entry.bezelTopLandscape ?? 0) * lScale;
1151
+ const lBezelBottom = (entry.bezelBottomLandscape ?? 0) * lScale;
1152
+ const lBezelLeft = (entry.bezelLeftLandscape ?? 0) * lScale;
1153
+ const lBezelRight = (entry.bezelRightLandscape ?? 0) * lScale;
1154
+ landscape = {
1155
+ svgString: entry.svgStringLandscape,
1156
+ frame: {
1157
+ bezelTop: lBezelTop,
1158
+ bezelBottom: lBezelBottom,
1159
+ bezelLeft: lBezelLeft,
1160
+ bezelRight: lBezelRight,
1161
+ totalWidth: lsr.width + lBezelLeft + lBezelRight,
1162
+ totalHeight: lsr.height + lBezelTop + lBezelBottom,
1163
+ screenWidth: lsr.width,
1164
+ screenHeight: lsr.height,
1165
+ screenRadius: lsr.rx,
1166
+ screenRadiusTop: lsr.rxTop,
1167
+ screenRadiusBottom: lsr.rxBottom
1168
+ },
1169
+ screenRect: lsr
1170
+ };
1171
+ }
1172
+ registerCustomDeviceSVG(
1173
+ entry.deviceId,
1174
+ entry.svgString,
1175
+ {
1176
+ bezelTop: portraitBezelTop,
1177
+ bezelBottom: portraitBezelBottom,
1178
+ bezelLeft: portraitBezelLeft,
1179
+ bezelRight: portraitBezelRight,
1180
+ totalWidth: portraitScreenW + portraitBezelLeft + portraitBezelRight,
1181
+ totalHeight: portraitScreenH + portraitBezelTop + portraitBezelBottom,
1182
+ screenWidth: portraitScreenW,
1183
+ screenHeight: portraitScreenH,
1184
+ screenRadius: sr?.rx ?? 0,
1185
+ screenRadiusTop: sr?.rxTop,
1186
+ screenRadiusBottom: sr?.rxBottom
1187
+ },
1188
+ void 0,
1189
+ sr,
1190
+ landscape
1191
+ );
1192
+ }
1193
+ persist(all) {
1194
+ if (!this.storage) return;
1195
+ try {
1196
+ this.storage.setItem(STORAGE_KEY, JSON.stringify(all));
1197
+ } catch {
1198
+ }
1199
+ }
1200
+ };
1201
+ var singleton = null;
1202
+ function getCustomSVGStore() {
1203
+ if (!singleton) singleton = new CustomSVGStore();
1204
+ return singleton;
1205
+ }
1206
+ // Annotate the CommonJS export names for ESM import in node:
1207
+ 0 && (module.exports = {
1208
+ BIELA_PREFIX,
1209
+ CustomSVGStore,
1210
+ DeviceCompare,
1211
+ DeviceErrorBoundary,
1212
+ DeviceFrame,
1213
+ DynamicStatusBar,
1214
+ HardwareButtons,
1215
+ SCALE_STEPS,
1216
+ SafeAreaOverlay,
1217
+ SafeAreaView,
1218
+ ScaleBar,
1219
+ StatusBarIndicators,
1220
+ VolumeHUD,
1221
+ computeAdaptiveScale,
1222
+ computeFullScale,
1223
+ computeHostSize,
1224
+ getCustomSVGStore,
1225
+ isBielaMessage,
1226
+ ptsToPercent,
1227
+ ptsToPx,
1228
+ pxToPts,
1229
+ registerCustomDeviceSVG,
1230
+ registerDeviceSVG,
1231
+ scaleValue,
1232
+ snapToStep,
1233
+ useAdaptiveScale,
1234
+ useContainerSize,
1235
+ useDeviceContract,
1236
+ useDeviceFrame,
1237
+ useFoldState,
1238
+ useOrientation,
1239
+ useScreenPower,
1240
+ useVolumeControl
1241
+ });
1242
+ //# sourceMappingURL=index.cjs.map