@biela.dev/core 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lite.cjs ADDED
@@ -0,0 +1,1413 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var devices = require('@biela.dev/devices');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/math/conversions.ts
8
+ function ptsToPx(pts, dpr) {
9
+ return Math.round(pts * dpr);
10
+ }
11
+ function pxToPts(px, dpr) {
12
+ return px / dpr;
13
+ }
14
+ function ptsToPercent(pts, total) {
15
+ if (total === 0) return 0;
16
+ return pts / total * 100;
17
+ }
18
+ function scaleValue(value, scaleFactor) {
19
+ return value * scaleFactor;
20
+ }
21
+
22
+ // src/math/scale-engine.ts
23
+ var SCALE_STEPS = [0.25, 0.33, 0.5, 0.75, 1];
24
+ function computeAdaptiveScale(deviceWidth, deviceHeight, containerWidth, containerHeight, padding = 24, maxScale = 1, minScale = 0.1) {
25
+ const availW = containerWidth - padding * 2;
26
+ const availH = containerHeight - padding * 2;
27
+ if (availW <= 0 || availH <= 0) return minScale;
28
+ const scaleW = availW / deviceWidth;
29
+ const scaleH = availH / deviceHeight;
30
+ const raw = Math.min(scaleW, scaleH, maxScale);
31
+ return Math.max(raw, minScale);
32
+ }
33
+ function snapToStep(raw) {
34
+ const valid = SCALE_STEPS.filter((s) => s <= raw + 1e-3);
35
+ if (valid.length === 0) return raw;
36
+ return valid[valid.length - 1];
37
+ }
38
+ function computeHostSize(deviceWidth, deviceHeight, scale) {
39
+ return {
40
+ width: Math.round(deviceWidth * scale),
41
+ height: Math.round(deviceHeight * scale)
42
+ };
43
+ }
44
+ function computeFullScale(deviceWidth, deviceHeight, containerWidth, containerHeight, options = {}) {
45
+ const { padding = 24, maxScale = 1, minScale = 0.1, snapToSteps = false } = options;
46
+ let scale = computeAdaptiveScale(
47
+ deviceWidth,
48
+ deviceHeight,
49
+ containerWidth,
50
+ containerHeight,
51
+ padding,
52
+ maxScale,
53
+ minScale
54
+ );
55
+ if (snapToSteps) {
56
+ scale = snapToStep(scale);
57
+ }
58
+ const { width: scaledWidth, height: scaledHeight } = computeHostSize(
59
+ deviceWidth,
60
+ deviceHeight,
61
+ scale
62
+ );
63
+ return {
64
+ scale,
65
+ scaledWidth,
66
+ scaledHeight,
67
+ deviceWidth,
68
+ deviceHeight,
69
+ isAtMaxScale: scale >= maxScale - 1e-3,
70
+ isConstrained: scale < maxScale - 1e-3,
71
+ scalePercent: `${Math.round(scale * 100)}%`
72
+ };
73
+ }
74
+ function useContainerSize(ref) {
75
+ const [size, setSize] = react.useState({ width: 0, height: 0 });
76
+ react.useEffect(() => {
77
+ const el = ref.current;
78
+ if (!el) return;
79
+ const observer = new ResizeObserver(([entry]) => {
80
+ if (!entry) return;
81
+ const { width, height } = entry.contentRect;
82
+ setSize((prev) => {
83
+ if (prev.width === width && prev.height === height) return prev;
84
+ return { width, height };
85
+ });
86
+ });
87
+ observer.observe(el);
88
+ return () => observer.disconnect();
89
+ }, [ref]);
90
+ return size;
91
+ }
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
+ } = options;
102
+ return react.useMemo(
103
+ () => computeFullScale(device.screen.width, device.screen.height, containerWidth, containerHeight, {
104
+ padding,
105
+ maxScale,
106
+ minScale,
107
+ snapToSteps
108
+ }),
109
+ [device.screen.width, device.screen.height, containerWidth, containerHeight, padding, maxScale, minScale, snapToSteps]
110
+ );
111
+ }
112
+ function useDeviceContract(deviceId, orientation = "portrait") {
113
+ return react.useMemo(() => {
114
+ const contract = devices.getDeviceContract(deviceId, orientation);
115
+ return {
116
+ contract,
117
+ cssVariables: contract.cssVariables,
118
+ contentZone: contract.contentZone[orientation]
119
+ };
120
+ }, [deviceId, orientation]);
121
+ }
122
+ var STEPS = 16;
123
+ var STEP_SIZE = 1 / STEPS;
124
+ var HUD_DISPLAY_MS = 1500;
125
+ function useVolumeControl(initialVolume = 1) {
126
+ const [level, setLevel] = react.useState(initialVolume);
127
+ const [muted, setMuted] = react.useState(false);
128
+ const [hudVisible, setHudVisible] = react.useState(false);
129
+ const hudTimerRef = react.useRef(null);
130
+ const showHud = react.useCallback(() => {
131
+ setHudVisible(true);
132
+ if (hudTimerRef.current) clearTimeout(hudTimerRef.current);
133
+ hudTimerRef.current = setTimeout(() => setHudVisible(false), HUD_DISPLAY_MS);
134
+ }, []);
135
+ const volumeUp = react.useCallback(() => {
136
+ setLevel((prev) => {
137
+ const next = Math.min(1, Math.round((prev + STEP_SIZE) * STEPS) / STEPS);
138
+ return next;
139
+ });
140
+ setMuted(false);
141
+ showHud();
142
+ }, [showHud]);
143
+ const volumeDown = react.useCallback(() => {
144
+ setLevel((prev) => {
145
+ const next = Math.max(0, Math.round((prev - STEP_SIZE) * STEPS) / STEPS);
146
+ return next;
147
+ });
148
+ setMuted(false);
149
+ showHud();
150
+ }, [showHud]);
151
+ const toggleMute = react.useCallback(() => {
152
+ setMuted((prev) => !prev);
153
+ showHud();
154
+ }, [showHud]);
155
+ const effectiveVolume = muted ? 0 : level;
156
+ react.useEffect(() => {
157
+ const container = document.querySelector(".bielaframe-content");
158
+ if (!container) return;
159
+ const mediaEls = container.querySelectorAll("audio, video");
160
+ mediaEls.forEach((el) => {
161
+ el.volume = effectiveVolume;
162
+ });
163
+ }, [effectiveVolume]);
164
+ react.useEffect(() => {
165
+ return () => {
166
+ if (hudTimerRef.current) clearTimeout(hudTimerRef.current);
167
+ };
168
+ }, []);
169
+ return { level, muted, hudVisible, volumeUp, volumeDown, toggleMute };
170
+ }
171
+ function useScreenPower() {
172
+ const [isOff, setIsOff] = react.useState(false);
173
+ const toggle = react.useCallback(() => {
174
+ setIsOff((prev) => !prev);
175
+ }, []);
176
+ return { isOff, toggle };
177
+ }
178
+ function useOrientation(initial = "portrait") {
179
+ const [orientation, setOrientation] = react.useState(initial);
180
+ const toggle = react.useCallback(
181
+ () => setOrientation((o) => o === "portrait" ? "landscape" : "portrait"),
182
+ []
183
+ );
184
+ return {
185
+ orientation,
186
+ isLandscape: orientation === "landscape",
187
+ toggle,
188
+ setOrientation
189
+ };
190
+ }
191
+ var DeviceErrorBoundary = class extends react.Component {
192
+ constructor(props) {
193
+ super(props);
194
+ this.state = { hasError: false, error: null };
195
+ }
196
+ static getDerivedStateFromError(error) {
197
+ return { hasError: true, error };
198
+ }
199
+ componentDidCatch(error, errorInfo) {
200
+ console.error("[BielaFrame] Component error caught by boundary:", error, errorInfo);
201
+ }
202
+ render() {
203
+ if (this.state.hasError) {
204
+ if (this.props.fallback) return this.props.fallback;
205
+ return /* @__PURE__ */ jsxRuntime.jsxs(
206
+ "div",
207
+ {
208
+ style: {
209
+ display: "flex",
210
+ flexDirection: "column",
211
+ alignItems: "center",
212
+ justifyContent: "center",
213
+ width: "100%",
214
+ height: "100%",
215
+ padding: "24px",
216
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
217
+ color: "#ef4444",
218
+ backgroundColor: "#1a1a1a",
219
+ textAlign: "center"
220
+ },
221
+ children: [
222
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "32px", marginBottom: "12px" }, children: "!" }),
223
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "14px", fontWeight: 600, marginBottom: "8px" }, children: "Component Error" }),
224
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "11px", color: "#888", maxWidth: "280px", lineHeight: 1.4 }, children: this.state.error?.message ?? "An unexpected error occurred in the rendered component." })
225
+ ]
226
+ }
227
+ );
228
+ }
229
+ return this.props.children;
230
+ }
231
+ };
232
+ function SafeAreaOverlay({ contract, orientation }) {
233
+ const sa = contract.safeArea[orientation];
234
+ const cz = contract.contentZone[orientation];
235
+ const screen = contract.screen;
236
+ const overlay = contract.hardwareOverlays;
237
+ const base = {
238
+ position: "absolute",
239
+ pointerEvents: "none",
240
+ boxSizing: "border-box"
241
+ };
242
+ const labelStyle = {
243
+ position: "absolute",
244
+ fontSize: "9px",
245
+ fontFamily: "monospace",
246
+ color: "white",
247
+ textShadow: "0 1px 2px rgba(0,0,0,0.8)",
248
+ whiteSpace: "nowrap"
249
+ };
250
+ return /* @__PURE__ */ jsxRuntime.jsxs(
251
+ "div",
252
+ {
253
+ style: {
254
+ position: "absolute",
255
+ inset: 0,
256
+ pointerEvents: "none",
257
+ zIndex: 20
258
+ },
259
+ children: [
260
+ /* @__PURE__ */ jsxRuntime.jsx(
261
+ "div",
262
+ {
263
+ style: {
264
+ ...base,
265
+ top: 0,
266
+ left: 0,
267
+ right: 0,
268
+ height: `${contract.statusBar.height}px`,
269
+ backgroundColor: "rgba(239, 68, 68, 0.25)",
270
+ borderBottom: "1px dashed rgba(239, 68, 68, 0.6)"
271
+ },
272
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...labelStyle, top: "2px", left: "4px" }, children: [
273
+ "status: ",
274
+ contract.statusBar.height,
275
+ "pt"
276
+ ] })
277
+ }
278
+ ),
279
+ overlay.type !== "none" && /* @__PURE__ */ jsxRuntime.jsx(
280
+ "div",
281
+ {
282
+ style: {
283
+ ...base,
284
+ left: `${overlay.portrait.x}px`,
285
+ top: `${overlay.portrait.y}px`,
286
+ width: `${overlay.portrait.width}px`,
287
+ height: `${overlay.portrait.height}px`,
288
+ backgroundColor: "rgba(249, 115, 22, 0.35)",
289
+ border: "1px solid rgba(249, 115, 22, 0.7)",
290
+ borderRadius: overlay.portrait.shape === "pill" ? `${overlay.portrait.height / 2}px` : overlay.portrait.shape === "circle" ? "50%" : "4px"
291
+ },
292
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...labelStyle, top: "-14px", left: "0px" }, children: [
293
+ overlay.type,
294
+ ": ",
295
+ overlay.portrait.width,
296
+ "\xD7",
297
+ overlay.portrait.height
298
+ ] })
299
+ }
300
+ ),
301
+ sa.top > contract.statusBar.height && /* @__PURE__ */ jsxRuntime.jsx(
302
+ "div",
303
+ {
304
+ style: {
305
+ ...base,
306
+ top: `${contract.statusBar.height}px`,
307
+ left: 0,
308
+ right: 0,
309
+ height: `${sa.top - contract.statusBar.height}px`,
310
+ backgroundColor: "rgba(239, 68, 68, 0.15)",
311
+ borderBottom: "1px dashed rgba(239, 68, 68, 0.4)"
312
+ }
313
+ }
314
+ ),
315
+ /* @__PURE__ */ jsxRuntime.jsx(
316
+ "div",
317
+ {
318
+ style: {
319
+ ...base,
320
+ left: `${cz.x}px`,
321
+ top: `${cz.y}px`,
322
+ width: `${cz.width}px`,
323
+ height: `${cz.height}px`,
324
+ backgroundColor: "rgba(34, 197, 94, 0.08)",
325
+ border: "1px dashed rgba(34, 197, 94, 0.4)"
326
+ },
327
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...labelStyle, bottom: "2px", right: "4px" }, children: [
328
+ "content: ",
329
+ cz.width,
330
+ "\xD7",
331
+ cz.height,
332
+ "pt"
333
+ ] })
334
+ }
335
+ ),
336
+ sa.bottom > 0 && /* @__PURE__ */ jsxRuntime.jsx(
337
+ "div",
338
+ {
339
+ style: {
340
+ ...base,
341
+ bottom: 0,
342
+ left: 0,
343
+ right: 0,
344
+ height: `${sa.bottom}px`,
345
+ backgroundColor: "rgba(239, 68, 68, 0.25)",
346
+ borderTop: "1px dashed rgba(239, 68, 68, 0.6)"
347
+ },
348
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...labelStyle, bottom: "2px", right: "4px" }, children: [
349
+ "bottom: ",
350
+ sa.bottom,
351
+ "pt"
352
+ ] })
353
+ }
354
+ ),
355
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...labelStyle, top: `${sa.top + 2}px`, left: "4px" }, children: [
356
+ "safe-top: ",
357
+ sa.top,
358
+ "pt"
359
+ ] }),
360
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...labelStyle, bottom: `${sa.bottom + 2}px`, left: "4px" }, children: [
361
+ "safe-bottom: ",
362
+ sa.bottom,
363
+ "pt"
364
+ ] }),
365
+ /* @__PURE__ */ jsxRuntime.jsxs(
366
+ "span",
367
+ {
368
+ style: {
369
+ ...labelStyle,
370
+ top: "50%",
371
+ left: "50%",
372
+ transform: "translate(-50%, -50%)",
373
+ fontSize: "11px",
374
+ backgroundColor: "rgba(0,0,0,0.6)",
375
+ padding: "2px 6px",
376
+ borderRadius: "3px"
377
+ },
378
+ children: [
379
+ screen.width,
380
+ "\xD7",
381
+ screen.height,
382
+ "pt"
383
+ ]
384
+ }
385
+ )
386
+ ]
387
+ }
388
+ );
389
+ }
390
+ function ScaleBar({
391
+ deviceName,
392
+ deviceWidth,
393
+ deviceHeight,
394
+ scale,
395
+ scalePercent,
396
+ isAtMaxScale,
397
+ isConstrained,
398
+ onScaleChange,
399
+ onFit,
400
+ onRealSize
401
+ }) {
402
+ return /* @__PURE__ */ jsxRuntime.jsxs(
403
+ "div",
404
+ {
405
+ "aria-label": `Scale bar for ${deviceName}`,
406
+ style: {
407
+ display: "flex",
408
+ alignItems: "center",
409
+ gap: "12px",
410
+ padding: "6px 12px",
411
+ marginTop: "8px",
412
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
413
+ borderRadius: "8px",
414
+ fontSize: "11px",
415
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Mono', 'Fira Code', monospace",
416
+ color: "#ccc",
417
+ userSelect: "none",
418
+ width: "fit-content",
419
+ alignSelf: "center"
420
+ },
421
+ children: [
422
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#fff", fontWeight: 500 }, children: deviceName }),
423
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#666" }, children: "\xB7" }),
424
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
425
+ deviceWidth,
426
+ "\xD7",
427
+ deviceHeight,
428
+ "pt"
429
+ ] }),
430
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#666" }, children: "|" }),
431
+ /* @__PURE__ */ jsxRuntime.jsx(
432
+ "input",
433
+ {
434
+ type: "range",
435
+ role: "slider",
436
+ "aria-label": "Device scale",
437
+ "aria-valuenow": Math.round(scale * 100),
438
+ "aria-valuemin": 10,
439
+ "aria-valuemax": 100,
440
+ min: 10,
441
+ max: 100,
442
+ step: 1,
443
+ value: Math.round(scale * 100),
444
+ onChange: (e) => onScaleChange?.(Number(e.target.value) / 100),
445
+ style: {
446
+ width: "80px",
447
+ accentColor: "#3b82f6",
448
+ cursor: "pointer"
449
+ }
450
+ }
451
+ ),
452
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-live": "polite", style: { minWidth: "32px", textAlign: "center", fontWeight: 600 }, children: scalePercent }),
453
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#666" }, children: "|" }),
454
+ /* @__PURE__ */ jsxRuntime.jsx(
455
+ "button",
456
+ {
457
+ onClick: onFit,
458
+ "aria-label": "Fit device to container",
459
+ style: {
460
+ background: isConstrained ? "rgba(59, 130, 246, 0.2)" : "transparent",
461
+ border: "1px solid rgba(59, 130, 246, 0.4)",
462
+ borderRadius: "4px",
463
+ color: isConstrained ? "#60a5fa" : "#666",
464
+ fontSize: "10px",
465
+ padding: "2px 6px",
466
+ cursor: "pointer",
467
+ fontWeight: isConstrained ? 600 : 400
468
+ },
469
+ children: "Fit"
470
+ }
471
+ ),
472
+ /* @__PURE__ */ jsxRuntime.jsx(
473
+ "button",
474
+ {
475
+ onClick: onRealSize,
476
+ "aria-label": "Show at real device size",
477
+ style: {
478
+ background: isAtMaxScale ? "rgba(234, 179, 8, 0.2)" : "transparent",
479
+ border: `1px solid ${isAtMaxScale ? "rgba(234, 179, 8, 0.6)" : "rgba(102, 102, 102, 0.4)"}`,
480
+ borderRadius: "4px",
481
+ color: isAtMaxScale ? "#eab308" : "#666",
482
+ fontSize: "10px",
483
+ padding: "2px 6px",
484
+ cursor: "pointer",
485
+ fontWeight: isAtMaxScale ? 600 : 400
486
+ },
487
+ children: "1:1"
488
+ }
489
+ )
490
+ ]
491
+ }
492
+ );
493
+ }
494
+ var SVG_REGISTRY = {};
495
+ function registerDeviceSVG(deviceId, component, frame) {
496
+ SVG_REGISTRY[deviceId] = { component, frame };
497
+ }
498
+ function registerCustomDeviceSVG(deviceId, svgString, frame, cropViewBox, screenRect) {
499
+ const scopedSVG = devices.scopeSVGIds(svgString, deviceId);
500
+ let processedSVG = scopedSVG.replace(
501
+ /<svg\b([^>]*)>/i,
502
+ (_match, attrs) => {
503
+ if (cropViewBox) {
504
+ attrs = attrs.replace(/\bviewBox\s*=\s*["'][^"']*["']/gi, "");
505
+ attrs += ` viewBox="${cropViewBox.x} ${cropViewBox.y} ${cropViewBox.width} ${cropViewBox.height}"`;
506
+ } else if (!/viewBox/i.test(attrs)) {
507
+ const wMatch = attrs.match(/\bwidth\s*=\s*["']?(\d+\.?\d*)["']?/i);
508
+ const hMatch = attrs.match(/\bheight\s*=\s*["']?(\d+\.?\d*)["']?/i);
509
+ if (wMatch && hMatch) {
510
+ attrs += ` viewBox="0 0 ${wMatch[1]} ${hMatch[1]}"`;
511
+ }
512
+ }
513
+ let cleaned = attrs.replace(/\bwidth\s*=\s*["'][^"']*["']/gi, "").replace(/\bheight\s*=\s*["'][^"']*["']/gi, "");
514
+ if (!/preserveAspectRatio/i.test(cleaned)) {
515
+ cleaned += ` preserveAspectRatio="xMidYMid meet"`;
516
+ }
517
+ return `<svg${cleaned} width="100%" height="100%">`;
518
+ }
519
+ );
520
+ if (screenRect) {
521
+ const { x: sx, y: sy, width: sw, height: sh, rx: sr } = screenRect;
522
+ const r = sr ?? 0;
523
+ const vbMatch = processedSVG.match(/viewBox\s*=\s*["']([^"']+)["']/i);
524
+ let svgW = 1e4, svgH = 1e4;
525
+ if (vbMatch) {
526
+ const parts = vbMatch[1].split(/[\s,]+/).map(Number);
527
+ if (parts.length >= 4) {
528
+ svgW = parts[2];
529
+ svgH = parts[3];
530
+ }
531
+ }
532
+ const maskId = `${deviceId}__screen-mask`;
533
+ let innerPath;
534
+ if (r > 0) {
535
+ innerPath = `M${sx + r},${sy} H${sx + sw - r} A${r},${r} 0 0 1 ${sx + sw},${sy + r} V${sy + sh - r} A${r},${r} 0 0 1 ${sx + sw - r},${sy + sh} H${sx + r} A${r},${r} 0 0 1 ${sx},${sy + sh - r} V${sy + r} A${r},${r} 0 0 1 ${sx + r},${sy} Z`;
536
+ } else {
537
+ innerPath = `M${sx},${sy} V${sy + sh} H${sx + sw} V${sy} Z`;
538
+ }
539
+ const maskDef = `<mask id="${maskId}" maskUnits="userSpaceOnUse" x="0" y="0" width="${svgW}" height="${svgH}"><path fill-rule="evenodd" d="M0,0 H${svgW} V${svgH} H0 Z ${innerPath}" fill="white"/></mask>`;
540
+ if (/<defs[\s>]/i.test(processedSVG)) {
541
+ processedSVG = processedSVG.replace(/<\/defs>/i, `${maskDef}</defs>`);
542
+ } else {
543
+ processedSVG = processedSVG.replace(
544
+ /(<svg\b[^>]*>)/i,
545
+ `$1<defs>${maskDef}</defs>`
546
+ );
547
+ }
548
+ const defsMatch = processedSVG.match(/(<defs[\s>][\s\S]*?<\/defs>)/i);
549
+ const defsContent = defsMatch ? defsMatch[1] : "";
550
+ const defsPlaceholder = "<!--BIELAFRAME_DEFS_PLACEHOLDER-->";
551
+ let bodySVG = defsMatch ? processedSVG.replace(defsContent, defsPlaceholder) : processedSVG;
552
+ bodySVG = bodySVG.replace(/<rect\b([^>]*?)\/?>/gi, (fullMatch, attrs) => {
553
+ if (/\bmask\s*=/i.test(fullMatch)) return fullMatch;
554
+ const xVal = parseFloat(attrs.match(/\bx\s*=\s*["']?(-?\d+\.?\d*)["']?/i)?.[1] ?? "0");
555
+ const yVal = parseFloat(attrs.match(/\by\s*=\s*["']?(-?\d+\.?\d*)["']?/i)?.[1] ?? "0");
556
+ const wVal = parseFloat(attrs.match(/\bwidth\s*=\s*["']?(\d+\.?\d*)["']?/i)?.[1] ?? "0");
557
+ const hVal = parseFloat(attrs.match(/\bheight\s*=\s*["']?(\d+\.?\d*)["']?/i)?.[1] ?? "0");
558
+ if (wVal > 0 && hVal > 0 && xVal <= sx + 2 && yVal <= sy + 2 && xVal + wVal >= sx + sw - 2 && yVal + hVal >= sy + sh - 2) {
559
+ return fullMatch.replace(/(\/?>)$/, ` mask="url(#${maskId})"$1`);
560
+ }
561
+ return fullMatch;
562
+ });
563
+ processedSVG = bodySVG.replace(defsPlaceholder, defsContent);
564
+ }
565
+ const CustomSVGComponent = ({ style }) => /* @__PURE__ */ jsxRuntime.jsx(
566
+ "div",
567
+ {
568
+ style,
569
+ dangerouslySetInnerHTML: { __html: processedSVG }
570
+ }
571
+ );
572
+ SVG_REGISTRY[deviceId] = { component: CustomSVGComponent, frame };
573
+ }
574
+ function DeviceFrame({
575
+ device: deviceProp,
576
+ deviceId,
577
+ orientation = "portrait",
578
+ scaleMode = "fit",
579
+ manualScale,
580
+ showSafeAreaOverlay = false,
581
+ showScaleBar = true,
582
+ colorScheme = "dark",
583
+ onContractReady,
584
+ onScaleChange,
585
+ children
586
+ }) {
587
+ const device = deviceProp ?? deviceId ?? "";
588
+ const sentinelRef = react.useRef(null);
589
+ const frameOverlayRef = react.useRef(null);
590
+ const hostRef = react.useRef(null);
591
+ const scalerRef = react.useRef(null);
592
+ const { width: containerW, height: containerH } = useContainerSize(sentinelRef);
593
+ const deviceLookup = react.useMemo(() => {
594
+ if (!device) {
595
+ return { meta: null, contract: null, error: "(no device specified)" };
596
+ }
597
+ try {
598
+ const meta = devices.getDeviceMetadata(device);
599
+ const contract2 = devices.getDeviceContract(device, orientation);
600
+ if (!meta || !meta.screen) {
601
+ return { meta: null, contract: null, error: device };
602
+ }
603
+ return { meta, contract: contract2, error: null };
604
+ } catch {
605
+ return { meta: null, contract: null, error: device };
606
+ }
607
+ }, [device, orientation]);
608
+ if (deviceLookup.error || !deviceLookup.meta || !deviceLookup.contract) {
609
+ return /* @__PURE__ */ jsxRuntime.jsxs(
610
+ "div",
611
+ {
612
+ ref: sentinelRef,
613
+ style: {
614
+ width: "100%",
615
+ height: "100%",
616
+ display: "flex",
617
+ alignItems: "center",
618
+ justifyContent: "center",
619
+ color: "#ef4444",
620
+ fontFamily: "monospace",
621
+ fontSize: "14px"
622
+ },
623
+ children: [
624
+ 'Device not found: "',
625
+ deviceLookup.error || "(unknown)",
626
+ '"'
627
+ ]
628
+ }
629
+ );
630
+ }
631
+ const deviceMeta = deviceLookup.meta;
632
+ const contract = deviceLookup.contract;
633
+ const svgEntry = SVG_REGISTRY[device];
634
+ const [overrideScale, setOverrideScale] = react.useState(null);
635
+ const effectiveMaxScale = scaleMode === "manual" && manualScale != null ? manualScale : 1;
636
+ const frameInfo = svgEntry?.frame;
637
+ const frameTotalW = frameInfo?.totalWidth ?? deviceMeta.screen.width;
638
+ const frameTotalH = frameInfo?.totalHeight ?? deviceMeta.screen.height;
639
+ const frameBezelLeft = frameInfo?.bezelLeft ?? 0;
640
+ const frameBezelTop = frameInfo?.bezelTop ?? 0;
641
+ const isLandscape = orientation === "landscape";
642
+ const effectiveHostW = isLandscape ? frameTotalH : frameTotalW;
643
+ const effectiveHostH = isLandscape ? frameTotalW : frameTotalH;
644
+ const scaleDevice = react.useMemo(() => ({
645
+ ...deviceMeta,
646
+ screen: {
647
+ ...deviceMeta.screen,
648
+ width: effectiveHostW,
649
+ height: effectiveHostH
650
+ }
651
+ }), [device, effectiveHostW, effectiveHostH]);
652
+ const scaleResult = useAdaptiveScale({
653
+ device: scaleDevice,
654
+ containerWidth: scaleMode === "manual" ? Infinity : containerW,
655
+ containerHeight: scaleMode === "manual" ? Infinity : containerH,
656
+ maxScale: effectiveMaxScale,
657
+ snapToSteps: scaleMode === "steps"
658
+ });
659
+ const activeScale = overrideScale ?? scaleResult.scale;
660
+ const hostWidth = Math.round(effectiveHostW * activeScale);
661
+ const hostHeight = Math.round(effectiveHostH * activeScale);
662
+ const scalerTransform = isLandscape ? `scale(${activeScale}) translate(0px, ${frameTotalW}px) rotate(-90deg)` : `scale(${activeScale}) translate(0px, 0px) rotate(0deg)`;
663
+ const cssVarStyle = react.useMemo(() => {
664
+ const vars = {};
665
+ for (const [key, value] of Object.entries(contract.cssVariables)) {
666
+ vars[key] = value;
667
+ }
668
+ if (isLandscape) {
669
+ const lsa = contract.safeArea.landscape;
670
+ vars["--safe-top"] = `${lsa.top}px`;
671
+ vars["--safe-bottom"] = `${lsa.bottom}px`;
672
+ vars["--safe-left"] = `${lsa.left}px`;
673
+ vars["--safe-right"] = `${lsa.right}px`;
674
+ }
675
+ return vars;
676
+ }, [device, orientation]);
677
+ const onContractReadyRef = react.useRef(onContractReady);
678
+ onContractReadyRef.current = onContractReady;
679
+ const onScaleChangeRef = react.useRef(onScaleChange);
680
+ onScaleChangeRef.current = onScaleChange;
681
+ react.useEffect(() => {
682
+ onContractReadyRef.current?.(contract);
683
+ }, [device, orientation]);
684
+ react.useEffect(() => {
685
+ onScaleChangeRef.current?.(activeScale);
686
+ }, [activeScale]);
687
+ const DeviceSVGComponent = svgEntry?.component;
688
+ const hasMeasured = containerW > 0 && containerH > 0;
689
+ return /* @__PURE__ */ jsxRuntime.jsx(
690
+ "div",
691
+ {
692
+ ref: sentinelRef,
693
+ className: "bielaframe-sentinel",
694
+ style: {
695
+ width: "100%",
696
+ height: "100%",
697
+ display: "flex",
698
+ flexDirection: "column",
699
+ alignItems: "center",
700
+ justifyContent: "center",
701
+ overflow: "hidden"
702
+ },
703
+ children: hasMeasured && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
704
+ /* @__PURE__ */ jsxRuntime.jsx(
705
+ "div",
706
+ {
707
+ ref: hostRef,
708
+ className: "bielaframe-host",
709
+ "aria-label": `${contract.device.name} device frame at ${Math.round(activeScale * 100)}% scale`,
710
+ style: {
711
+ width: hostWidth,
712
+ height: hostHeight,
713
+ position: "relative",
714
+ flexShrink: 0,
715
+ transition: "width 400ms cubic-bezier(0.4, 0, 0.2, 1), height 400ms cubic-bezier(0.4, 0, 0.2, 1)"
716
+ },
717
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
718
+ "div",
719
+ {
720
+ ref: scalerRef,
721
+ className: "bielaframe-scaler",
722
+ style: {
723
+ width: frameTotalW,
724
+ height: frameTotalH,
725
+ transform: scalerTransform,
726
+ transformOrigin: "top left",
727
+ position: "absolute",
728
+ top: 0,
729
+ left: 0,
730
+ willChange: "transform",
731
+ transition: "transform 400ms cubic-bezier(0.4, 0, 0.2, 1)"
732
+ },
733
+ children: [
734
+ /* @__PURE__ */ jsxRuntime.jsx(
735
+ "div",
736
+ {
737
+ className: "bielaframe-content",
738
+ style: {
739
+ position: "absolute",
740
+ left: frameBezelLeft,
741
+ top: frameBezelTop,
742
+ width: deviceMeta.screen.width,
743
+ height: deviceMeta.screen.height,
744
+ clipPath: `inset(0 round ${contract.screen.cornerRadius}px)`,
745
+ overflow: "hidden",
746
+ backfaceVisibility: "hidden",
747
+ transform: "translateZ(0)",
748
+ ...cssVarStyle
749
+ },
750
+ children: /* @__PURE__ */ jsxRuntime.jsx(DeviceErrorBoundary, { children })
751
+ }
752
+ ),
753
+ DeviceSVGComponent && /* @__PURE__ */ jsxRuntime.jsx("div", { ref: frameOverlayRef, children: /* @__PURE__ */ jsxRuntime.jsx(
754
+ DeviceSVGComponent,
755
+ {
756
+ colorScheme,
757
+ style: {
758
+ position: "absolute",
759
+ top: 0,
760
+ left: 0,
761
+ width: frameTotalW,
762
+ height: frameTotalH,
763
+ pointerEvents: "none",
764
+ zIndex: 10
765
+ }
766
+ }
767
+ ) }),
768
+ showSafeAreaOverlay && /* @__PURE__ */ jsxRuntime.jsx(
769
+ "div",
770
+ {
771
+ style: {
772
+ position: "absolute",
773
+ left: frameBezelLeft,
774
+ top: frameBezelTop,
775
+ width: deviceMeta.screen.width,
776
+ height: deviceMeta.screen.height,
777
+ pointerEvents: "none",
778
+ zIndex: 15
779
+ },
780
+ children: /* @__PURE__ */ jsxRuntime.jsx(SafeAreaOverlay, { contract, orientation })
781
+ }
782
+ )
783
+ ]
784
+ }
785
+ )
786
+ }
787
+ ),
788
+ showScaleBar && /* @__PURE__ */ jsxRuntime.jsx(
789
+ ScaleBar,
790
+ {
791
+ deviceName: contract.device.name,
792
+ deviceWidth: deviceMeta.screen.width,
793
+ deviceHeight: deviceMeta.screen.height,
794
+ scale: activeScale,
795
+ scalePercent: `${Math.round(activeScale * 100)}%`,
796
+ isAtMaxScale: activeScale >= 0.999,
797
+ isConstrained: activeScale < 0.999,
798
+ onScaleChange: (s) => setOverrideScale(s),
799
+ onFit: () => setOverrideScale(null),
800
+ onRealSize: () => setOverrideScale(1)
801
+ }
802
+ )
803
+ ] })
804
+ }
805
+ );
806
+ }
807
+ function DeviceCompare({
808
+ deviceA,
809
+ deviceB,
810
+ orientation = "portrait",
811
+ colorScheme = "dark",
812
+ showSafeAreaOverlay = false,
813
+ showScaleBar = false,
814
+ layout = "auto",
815
+ gap = 24,
816
+ children,
817
+ childrenA,
818
+ childrenB,
819
+ onContractReadyA,
820
+ onContractReadyB
821
+ }) {
822
+ const isLandscape = orientation === "landscape";
823
+ const effectiveLayout = layout === "auto" ? isLandscape ? "vertical" : "horizontal" : layout;
824
+ const flexDirection = effectiveLayout === "horizontal" ? "row" : "column";
825
+ return /* @__PURE__ */ jsxRuntime.jsxs(
826
+ "div",
827
+ {
828
+ className: "bielaframe-compare",
829
+ style: {
830
+ width: "100%",
831
+ height: "100%",
832
+ display: "flex",
833
+ flexDirection,
834
+ alignItems: "center",
835
+ justifyContent: "center",
836
+ gap,
837
+ overflow: "hidden"
838
+ },
839
+ children: [
840
+ /* @__PURE__ */ jsxRuntime.jsx(
841
+ "div",
842
+ {
843
+ style: {
844
+ flex: 1,
845
+ width: effectiveLayout === "horizontal" ? 0 : "100%",
846
+ height: effectiveLayout === "vertical" ? 0 : "100%",
847
+ minWidth: 0,
848
+ minHeight: 0
849
+ },
850
+ children: /* @__PURE__ */ jsxRuntime.jsx(
851
+ DeviceFrame,
852
+ {
853
+ device: deviceA,
854
+ orientation,
855
+ colorScheme,
856
+ showSafeAreaOverlay,
857
+ showScaleBar,
858
+ onContractReady: onContractReadyA,
859
+ children: childrenA ?? children
860
+ }
861
+ )
862
+ }
863
+ ),
864
+ /* @__PURE__ */ jsxRuntime.jsx(
865
+ "div",
866
+ {
867
+ style: {
868
+ flex: 1,
869
+ width: effectiveLayout === "horizontal" ? 0 : "100%",
870
+ height: effectiveLayout === "vertical" ? 0 : "100%",
871
+ minWidth: 0,
872
+ minHeight: 0
873
+ },
874
+ children: /* @__PURE__ */ jsxRuntime.jsx(
875
+ DeviceFrame,
876
+ {
877
+ device: deviceB,
878
+ orientation,
879
+ colorScheme,
880
+ showSafeAreaOverlay,
881
+ showScaleBar,
882
+ onContractReady: onContractReadyB,
883
+ children: childrenB ?? children
884
+ }
885
+ )
886
+ }
887
+ )
888
+ ]
889
+ }
890
+ );
891
+ }
892
+ function SafeAreaView({ edges, children, style }) {
893
+ const allEdges = !edges || edges.length === 0;
894
+ const padding = {
895
+ paddingTop: allEdges || edges?.includes("top") ? "var(--safe-top)" : void 0,
896
+ paddingBottom: allEdges || edges?.includes("bottom") ? "var(--safe-bottom)" : void 0,
897
+ paddingLeft: allEdges || edges?.includes("left") ? "var(--safe-left)" : void 0,
898
+ paddingRight: allEdges || edges?.includes("right") ? "var(--safe-right)" : void 0
899
+ };
900
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: "100%", height: "100%", boxSizing: "border-box", ...padding, ...style }, children });
901
+ }
902
+ function VolumeHUD({ level, muted, visible, platform }) {
903
+ if (platform === "ios") {
904
+ return /* @__PURE__ */ jsxRuntime.jsx(IOSVolumeHUD, { level, muted, visible });
905
+ }
906
+ return /* @__PURE__ */ jsxRuntime.jsx(AndroidVolumeHUD, { level, muted, visible });
907
+ }
908
+ function IOSVolumeHUD({ level, muted, visible }) {
909
+ const fillPercent = muted ? 0 : level * 100;
910
+ return /* @__PURE__ */ jsxRuntime.jsxs(
911
+ "div",
912
+ {
913
+ style: {
914
+ ...iosStyles.container,
915
+ opacity: visible ? 1 : 0,
916
+ pointerEvents: "none",
917
+ transition: visible ? "opacity 100ms ease-in" : "opacity 300ms ease-out"
918
+ },
919
+ children: [
920
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: iosStyles.track, children: /* @__PURE__ */ jsxRuntime.jsx(
921
+ "div",
922
+ {
923
+ style: {
924
+ ...iosStyles.fill,
925
+ height: `${fillPercent}%`
926
+ }
927
+ }
928
+ ) }),
929
+ muted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: iosStyles.muteIcon, children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "10", height: "10", viewBox: "0 0 10 10", fill: "none", children: [
930
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "1", y1: "1", x2: "9", y2: "9", stroke: "white", strokeWidth: "1.5", strokeLinecap: "round" }),
931
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "1", x2: "1", y2: "9", stroke: "white", strokeWidth: "1.5", strokeLinecap: "round" })
932
+ ] }) })
933
+ ]
934
+ }
935
+ );
936
+ }
937
+ function AndroidVolumeHUD({ level, muted, visible }) {
938
+ const fillPercent = muted ? 0 : level * 100;
939
+ return /* @__PURE__ */ jsxRuntime.jsx(
940
+ "div",
941
+ {
942
+ style: {
943
+ ...androidStyles.container,
944
+ opacity: visible ? 1 : 0,
945
+ pointerEvents: "none",
946
+ transition: visible ? "opacity 100ms ease-in" : "opacity 300ms ease-out"
947
+ },
948
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: androidStyles.track, children: /* @__PURE__ */ jsxRuntime.jsx(
949
+ "div",
950
+ {
951
+ style: {
952
+ ...androidStyles.fill,
953
+ width: `${fillPercent}%`
954
+ }
955
+ }
956
+ ) })
957
+ }
958
+ );
959
+ }
960
+ var iosStyles = {
961
+ container: {
962
+ position: "absolute",
963
+ left: 8,
964
+ top: "50%",
965
+ transform: "translateY(-50%)",
966
+ zIndex: 60,
967
+ display: "flex",
968
+ flexDirection: "column",
969
+ alignItems: "center",
970
+ gap: 4
971
+ },
972
+ track: {
973
+ width: 6,
974
+ height: 160,
975
+ borderRadius: 3,
976
+ background: "rgba(255, 255, 255, 0.15)",
977
+ backdropFilter: "blur(20px)",
978
+ WebkitBackdropFilter: "blur(20px)",
979
+ overflow: "hidden",
980
+ position: "relative",
981
+ display: "flex",
982
+ flexDirection: "column",
983
+ justifyContent: "flex-end",
984
+ border: "0.5px solid rgba(255, 255, 255, 0.2)"
985
+ },
986
+ fill: {
987
+ width: "100%",
988
+ background: "rgba(255, 255, 255, 0.85)",
989
+ borderRadius: 3,
990
+ transition: "height 80ms ease-out"
991
+ },
992
+ muteIcon: {
993
+ marginTop: 2
994
+ }
995
+ };
996
+ var androidStyles = {
997
+ container: {
998
+ position: "absolute",
999
+ top: 8,
1000
+ left: "50%",
1001
+ transform: "translateX(-50%)",
1002
+ zIndex: 60,
1003
+ width: "60%"
1004
+ },
1005
+ track: {
1006
+ width: "100%",
1007
+ height: 4,
1008
+ borderRadius: 2,
1009
+ background: "rgba(255, 255, 255, 0.2)",
1010
+ overflow: "hidden"
1011
+ },
1012
+ fill: {
1013
+ height: "100%",
1014
+ background: "rgba(255, 255, 255, 0.85)",
1015
+ borderRadius: 2,
1016
+ transition: "width 80ms ease-out"
1017
+ }
1018
+ };
1019
+ var BUTTON_ATTR_MAP = {
1020
+ "volume-up": "volumeUp",
1021
+ "volume-down": "volumeDown",
1022
+ "power": "power",
1023
+ "action": "actionButton",
1024
+ "camera": "cameraControl"
1025
+ };
1026
+ var PRESS_ANIMATION_IN = 80;
1027
+ var PRESS_ANIMATION_OUT = 120;
1028
+ var REPEAT_INITIAL_DELAY = 500;
1029
+ var REPEAT_INTERVAL = 150;
1030
+ var REPEAT_FAST_INTERVAL = 80;
1031
+ var REPEAT_FAST_THRESHOLD = 1e3;
1032
+ function HardwareButtons({
1033
+ frameContainerRef,
1034
+ onButtonPress,
1035
+ enabled = true,
1036
+ orientation = "portrait"
1037
+ }) {
1038
+ const [hitTargets, setHitTargets] = react.useState([]);
1039
+ const repeatTimerRef = react.useRef(null);
1040
+ const repeatIntervalRef = react.useRef(null);
1041
+ const pressStartRef = react.useRef(0);
1042
+ const clearRepeat = react.useCallback(() => {
1043
+ if (repeatTimerRef.current) {
1044
+ clearTimeout(repeatTimerRef.current);
1045
+ repeatTimerRef.current = null;
1046
+ }
1047
+ if (repeatIntervalRef.current) {
1048
+ clearInterval(repeatIntervalRef.current);
1049
+ repeatIntervalRef.current = null;
1050
+ }
1051
+ }, []);
1052
+ const discoverButtons = react.useCallback(() => {
1053
+ const container = frameContainerRef.current;
1054
+ if (!container) return false;
1055
+ const buttonEls = container.querySelectorAll("[data-button]");
1056
+ if (buttonEls.length === 0) return false;
1057
+ const targets = [];
1058
+ const seen = /* @__PURE__ */ new Set();
1059
+ buttonEls.forEach((el) => {
1060
+ const attrValue = el.getAttribute("data-button");
1061
+ if (!attrValue) return;
1062
+ const buttonName = BUTTON_ATTR_MAP[attrValue];
1063
+ if (!buttonName) return;
1064
+ if (seen.has(buttonName)) return;
1065
+ seen.add(buttonName);
1066
+ const side = el.getAttribute("data-side") || "right";
1067
+ let localLeft, localTop, localWidth, localHeight;
1068
+ const tag = el.tagName.toLowerCase();
1069
+ if (tag === "circle") {
1070
+ const cx = parseFloat(el.getAttribute("cx") || "0");
1071
+ const cy = parseFloat(el.getAttribute("cy") || "0");
1072
+ const r = parseFloat(el.getAttribute("r") || "0");
1073
+ localLeft = cx - r;
1074
+ localTop = cy - r;
1075
+ localWidth = r * 2;
1076
+ localHeight = r * 2;
1077
+ } else {
1078
+ localLeft = parseFloat(el.getAttribute("x") || "0");
1079
+ localTop = parseFloat(el.getAttribute("y") || "0");
1080
+ localWidth = parseFloat(el.getAttribute("width") || "0");
1081
+ localHeight = parseFloat(el.getAttribute("height") || "0");
1082
+ }
1083
+ if (localWidth <= 0 && localHeight <= 0) return;
1084
+ const pad = 10;
1085
+ targets.push({
1086
+ name: buttonName,
1087
+ side,
1088
+ top: localTop - pad,
1089
+ left: localLeft - pad,
1090
+ width: localWidth + pad * 2,
1091
+ height: localHeight + pad * 2,
1092
+ svgEl: el
1093
+ });
1094
+ });
1095
+ setHitTargets(targets);
1096
+ return targets.length > 0;
1097
+ }, [frameContainerRef]);
1098
+ react.useEffect(() => {
1099
+ if (!enabled) return;
1100
+ if (discoverButtons()) return;
1101
+ let retryCount = 0;
1102
+ const maxRetries = 10;
1103
+ let timer;
1104
+ const retry = () => {
1105
+ if (retryCount >= maxRetries) return;
1106
+ retryCount++;
1107
+ timer = setTimeout(() => {
1108
+ if (!discoverButtons()) retry();
1109
+ }, retryCount < 3 ? 100 : 300);
1110
+ };
1111
+ retry();
1112
+ return () => clearTimeout(timer);
1113
+ }, [enabled, orientation, discoverButtons]);
1114
+ const applyPress = react.useCallback((target) => {
1115
+ const pressOffset = target.side === "left" ? "-2px" : "2px";
1116
+ const el = target.svgEl;
1117
+ el.style.transform = `translateX(${pressOffset})`;
1118
+ el.style.opacity = "0.7";
1119
+ el.style.transition = `transform ${PRESS_ANIMATION_IN}ms ease-out, opacity ${PRESS_ANIMATION_IN}ms ease-out`;
1120
+ }, []);
1121
+ const releasePress = react.useCallback((target) => {
1122
+ const el = target.svgEl;
1123
+ el.style.transform = "";
1124
+ el.style.opacity = "";
1125
+ el.style.transition = `transform ${PRESS_ANIMATION_OUT}ms ease-in, opacity ${PRESS_ANIMATION_OUT}ms ease-in`;
1126
+ }, []);
1127
+ const startRepeat = react.useCallback((target) => {
1128
+ const isVolumeButton = target.name === "volumeUp" || target.name === "volumeDown";
1129
+ if (!isVolumeButton) return;
1130
+ pressStartRef.current = Date.now();
1131
+ repeatTimerRef.current = setTimeout(() => {
1132
+ repeatIntervalRef.current = setInterval(() => {
1133
+ onButtonPress?.(target.name);
1134
+ const elapsed = Date.now() - pressStartRef.current;
1135
+ if (elapsed > REPEAT_FAST_THRESHOLD && repeatIntervalRef.current) {
1136
+ clearInterval(repeatIntervalRef.current);
1137
+ repeatIntervalRef.current = setInterval(() => {
1138
+ onButtonPress?.(target.name);
1139
+ }, REPEAT_FAST_INTERVAL);
1140
+ }
1141
+ }, REPEAT_INTERVAL);
1142
+ }, REPEAT_INITIAL_DELAY);
1143
+ }, [onButtonPress, clearRepeat]);
1144
+ if (!enabled || hitTargets.length === 0) return null;
1145
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: hitTargets.map((target) => /* @__PURE__ */ jsxRuntime.jsx(
1146
+ "div",
1147
+ {
1148
+ style: {
1149
+ position: "absolute",
1150
+ top: target.top,
1151
+ left: target.left,
1152
+ width: target.width,
1153
+ height: target.height,
1154
+ cursor: "pointer",
1155
+ zIndex: 11,
1156
+ // Transparent but clickable
1157
+ background: "transparent"
1158
+ },
1159
+ onMouseDown: (e) => {
1160
+ e.preventDefault();
1161
+ e.stopPropagation();
1162
+ applyPress(target);
1163
+ onButtonPress?.(target.name);
1164
+ startRepeat(target);
1165
+ },
1166
+ onMouseUp: (e) => {
1167
+ e.preventDefault();
1168
+ releasePress(target);
1169
+ clearRepeat();
1170
+ },
1171
+ onMouseLeave: () => {
1172
+ releasePress(target);
1173
+ clearRepeat();
1174
+ },
1175
+ onTouchStart: (e) => {
1176
+ e.preventDefault();
1177
+ e.stopPropagation();
1178
+ applyPress(target);
1179
+ onButtonPress?.(target.name);
1180
+ startRepeat(target);
1181
+ },
1182
+ onTouchEnd: (e) => {
1183
+ e.preventDefault();
1184
+ releasePress(target);
1185
+ clearRepeat();
1186
+ }
1187
+ },
1188
+ target.name
1189
+ )) });
1190
+ }
1191
+ function formatTime(date) {
1192
+ const h = date.getHours();
1193
+ const m = date.getMinutes();
1194
+ const hour = h % 12 || 12;
1195
+ return `${hour}:${m.toString().padStart(2, "0")}`;
1196
+ }
1197
+ function DynamicStatusBar({
1198
+ contract,
1199
+ orientation,
1200
+ colorScheme,
1201
+ showLiveClock = true,
1202
+ fixedTime
1203
+ }) {
1204
+ const [time, setTime] = react.useState(() => formatTime(/* @__PURE__ */ new Date()));
1205
+ react.useEffect(() => {
1206
+ if (fixedTime || !showLiveClock) return;
1207
+ const interval = setInterval(() => {
1208
+ setTime(formatTime(/* @__PURE__ */ new Date()));
1209
+ }, 1e3);
1210
+ return () => clearInterval(interval);
1211
+ }, [fixedTime, showLiveClock]);
1212
+ if (orientation === "landscape" && contract.device.platform === "ios") {
1213
+ return null;
1214
+ }
1215
+ const displayTime = fixedTime ?? time;
1216
+ const { platform } = contract.device;
1217
+ const statusBarHeight = contract.statusBar.height;
1218
+ const statusBarStyle = contract.statusBar.style;
1219
+ const textColor = colorScheme === "dark" ? "#fff" : "#000";
1220
+ const bgColor = colorScheme === "dark" ? "#000" : "#fff";
1221
+ const fontFamily = platform === "ios" ? "-apple-system, 'SF Pro Text', 'Helvetica Neue', sans-serif" : "'Roboto', 'Google Sans', sans-serif";
1222
+ const baseFontSize = platform === "ios" ? 15 : 12;
1223
+ const clockStyle = {
1224
+ color: textColor,
1225
+ fontFamily,
1226
+ fontSize: baseFontSize,
1227
+ fontWeight: platform === "ios" ? 600 : 400,
1228
+ letterSpacing: platform === "ios" ? 0.3 : 0,
1229
+ lineHeight: `${statusBarHeight}px`,
1230
+ whiteSpace: "nowrap"
1231
+ };
1232
+ const patchStyle = {
1233
+ position: "absolute",
1234
+ top: 0,
1235
+ height: statusBarHeight,
1236
+ display: "flex",
1237
+ alignItems: "center",
1238
+ background: bgColor,
1239
+ pointerEvents: "none"
1240
+ };
1241
+ if (platform === "ios" && statusBarStyle !== "dynamic-island" && statusBarStyle !== "notch") {
1242
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...patchStyle, left: "50%", transform: "translateX(-50%)", paddingLeft: 6, paddingRight: 6 }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...clockStyle, fontWeight: 500, fontSize: baseFontSize - 1 }, children: displayTime }) });
1243
+ }
1244
+ if (platform === "ios") {
1245
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...patchStyle, left: 0, paddingLeft: 20, paddingRight: 8 }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: clockStyle, children: displayTime }) });
1246
+ }
1247
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...patchStyle, left: 0, paddingLeft: 16, paddingRight: 8 }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: clockStyle, children: displayTime }) });
1248
+ }
1249
+ function StatusBarIndicators({ platform, colorScheme }) {
1250
+ const color = colorScheme === "dark" ? "#fff" : "#000";
1251
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.container, children: [
1252
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "17", height: "11", viewBox: "0 0 17 11", fill: "none", children: [
1253
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "0", y: "7", width: "3", height: "4", rx: "0.5", fill: color, opacity: "0.4" }),
1254
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "4.5", y: "5", width: "3", height: "6", rx: "0.5", fill: color, opacity: "0.6" }),
1255
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "9", y: "2.5", width: "3", height: "8.5", rx: "0.5", fill: color, opacity: "0.8" }),
1256
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "13.5", y: "0", width: "3", height: "11", rx: "0.5", fill: color })
1257
+ ] }),
1258
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "15", height: "11", viewBox: "0 0 15 11", fill: "none", style: { marginLeft: 5 }, children: [
1259
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7.5 10.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z", fill: color }),
1260
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4.5 7.5a4.2 4.2 0 0 1 6 0", stroke: color, strokeWidth: "1.2", strokeLinecap: "round", fill: "none" }),
1261
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 5a7.1 7.1 0 0 1 11 0", stroke: color, strokeWidth: "1.2", strokeLinecap: "round", fill: "none" }),
1262
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M0 2.5a10 10 0 0 1 15 0", stroke: color, strokeWidth: "1.2", strokeLinecap: "round", fill: "none" })
1263
+ ] }),
1264
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "25", height: "12", viewBox: "0 0 25 12", fill: "none", style: { marginLeft: 5 }, children: [
1265
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "0.5", y: "0.5", width: "21", height: "11", rx: "2", stroke: color, strokeWidth: "1", fill: "none" }),
1266
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "2", width: "15", height: "8", rx: "1", fill: color, opacity: platform === "ios" ? "0.85" : "0.7" }),
1267
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M22.5 4v4a1.5 1.5 0 0 0 0-4z", fill: color, opacity: "0.5" })
1268
+ ] })
1269
+ ] });
1270
+ }
1271
+ var styles = {
1272
+ container: {
1273
+ display: "flex",
1274
+ alignItems: "center",
1275
+ gap: 0
1276
+ }
1277
+ };
1278
+ var SVG_OVERRIDES_KEY = "bielaframe-svg-overrides";
1279
+ var CustomSVGStore = class {
1280
+ storage;
1281
+ constructor(storage) {
1282
+ if (storage !== void 0) {
1283
+ this.storage = storage;
1284
+ } else {
1285
+ this.storage = typeof localStorage !== "undefined" ? localStorage : null;
1286
+ }
1287
+ }
1288
+ /** Load all stored overrides */
1289
+ getAll() {
1290
+ if (!this.storage) return {};
1291
+ try {
1292
+ const raw = this.storage.getItem(SVG_OVERRIDES_KEY);
1293
+ if (raw) return JSON.parse(raw);
1294
+ } catch {
1295
+ }
1296
+ return {};
1297
+ }
1298
+ /** Save an override and register it in the SVG registry */
1299
+ save(entry) {
1300
+ const all = this.getAll();
1301
+ all[entry.deviceId] = entry;
1302
+ this.persist(all);
1303
+ this.applyEntry(entry);
1304
+ }
1305
+ /** Remove an override (revert to built-in) */
1306
+ remove(deviceId) {
1307
+ const all = this.getAll();
1308
+ delete all[deviceId];
1309
+ this.persist(all);
1310
+ }
1311
+ /** Check if a device has a custom override */
1312
+ has(deviceId) {
1313
+ return this.getAll()[deviceId] !== void 0;
1314
+ }
1315
+ /** Get a single override by device ID */
1316
+ get(deviceId) {
1317
+ return this.getAll()[deviceId];
1318
+ }
1319
+ /**
1320
+ * Apply all stored overrides to the SVG registry.
1321
+ * Called during auto-registration to restore user customizations.
1322
+ */
1323
+ applyAll() {
1324
+ const all = this.getAll();
1325
+ for (const entry of Object.values(all)) {
1326
+ this.applyEntry(entry);
1327
+ }
1328
+ }
1329
+ applyEntry(entry) {
1330
+ let screenW = 402;
1331
+ let screenH = 874;
1332
+ let screenR = 0;
1333
+ try {
1334
+ const meta = devices.getDeviceMetadata(entry.deviceId);
1335
+ screenW = meta.screen.width;
1336
+ screenH = meta.screen.height;
1337
+ screenR = meta.screen.cornerRadius;
1338
+ } catch {
1339
+ if (entry.screenRect) {
1340
+ screenW = entry.screenRect.width;
1341
+ screenH = entry.screenRect.height;
1342
+ }
1343
+ }
1344
+ const frame = {
1345
+ bezelTop: entry.bezelTop,
1346
+ bezelBottom: entry.bezelBottom,
1347
+ bezelLeft: entry.bezelLeft,
1348
+ bezelRight: entry.bezelRight,
1349
+ totalWidth: entry.bezelLeft + entry.bezelRight + screenW,
1350
+ totalHeight: entry.bezelTop + entry.bezelBottom + screenH,
1351
+ screenWidth: screenW,
1352
+ screenHeight: screenH,
1353
+ screenRadius: screenR
1354
+ };
1355
+ try {
1356
+ registerCustomDeviceSVG(
1357
+ entry.deviceId,
1358
+ entry.svgString,
1359
+ frame,
1360
+ void 0,
1361
+ entry.screenRect
1362
+ );
1363
+ } catch {
1364
+ }
1365
+ }
1366
+ persist(all) {
1367
+ if (!this.storage) return;
1368
+ const json = JSON.stringify(all);
1369
+ try {
1370
+ this.storage.setItem(SVG_OVERRIDES_KEY, json);
1371
+ } catch {
1372
+ }
1373
+ }
1374
+ };
1375
+ var _store = null;
1376
+ function getCustomSVGStore() {
1377
+ if (!_store) {
1378
+ _store = new CustomSVGStore();
1379
+ }
1380
+ return _store;
1381
+ }
1382
+
1383
+ exports.CustomSVGStore = CustomSVGStore;
1384
+ exports.DeviceCompare = DeviceCompare;
1385
+ exports.DeviceErrorBoundary = DeviceErrorBoundary;
1386
+ exports.DeviceFrame = DeviceFrame;
1387
+ exports.DynamicStatusBar = DynamicStatusBar;
1388
+ exports.HardwareButtons = HardwareButtons;
1389
+ exports.SCALE_STEPS = SCALE_STEPS;
1390
+ exports.SafeAreaOverlay = SafeAreaOverlay;
1391
+ exports.SafeAreaView = SafeAreaView;
1392
+ exports.ScaleBar = ScaleBar;
1393
+ exports.StatusBarIndicators = StatusBarIndicators;
1394
+ exports.VolumeHUD = VolumeHUD;
1395
+ exports.computeAdaptiveScale = computeAdaptiveScale;
1396
+ exports.computeFullScale = computeFullScale;
1397
+ exports.computeHostSize = computeHostSize;
1398
+ exports.getCustomSVGStore = getCustomSVGStore;
1399
+ exports.ptsToPercent = ptsToPercent;
1400
+ exports.ptsToPx = ptsToPx;
1401
+ exports.pxToPts = pxToPts;
1402
+ exports.registerCustomDeviceSVG = registerCustomDeviceSVG;
1403
+ exports.registerDeviceSVG = registerDeviceSVG;
1404
+ exports.scaleValue = scaleValue;
1405
+ exports.snapToStep = snapToStep;
1406
+ exports.useAdaptiveScale = useAdaptiveScale;
1407
+ exports.useContainerSize = useContainerSize;
1408
+ exports.useDeviceContract = useDeviceContract;
1409
+ exports.useOrientation = useOrientation;
1410
+ exports.useScreenPower = useScreenPower;
1411
+ exports.useVolumeControl = useVolumeControl;
1412
+ //# sourceMappingURL=lite.cjs.map
1413
+ //# sourceMappingURL=lite.cjs.map