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