@biela.dev/core 0.1.0

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