@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.
@@ -0,0 +1,408 @@
1
+ import { RefObject, ReactNode, Component, ErrorInfo, CSSProperties, ReactElement } from 'react';
2
+ import { DeviceMeta, DeviceLayoutContract } from '@biela.dev/devices';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+
5
+ /**
6
+ * Design token dimension math — conversion utilities.
7
+ * All device dimensions are in logical points (CSS pixels at 1x).
8
+ */
9
+ /** Convert logical points to physical pixels */
10
+ declare function ptsToPx(pts: number, dpr: number): number;
11
+ /** Convert physical pixels to logical points */
12
+ declare function pxToPts(px: number, dpr: number): number;
13
+ /** Convert points to percentage of a total dimension */
14
+ declare function ptsToPercent(pts: number, total: number): number;
15
+ /** Apply uniform scale factor to any dimension value */
16
+ declare function scaleValue(value: number, scaleFactor: number): number;
17
+
18
+ /**
19
+ * Scale Engine Math — mirrors Xcode Simulator "fit to screen" behavior.
20
+ *
21
+ * Rules:
22
+ * 1. Inner content ALWAYS at 1:1 logical point size
23
+ * 2. Scale derived from available container space, never hardcoded
24
+ * 3. SVG frame chrome scales WITH content as one atomic unit
25
+ * 4. Host layout reserves SCALED dimensions
26
+ * 5. Maximum scale always 1.0 — never larger than the real device
27
+ */
28
+ /** Discrete scale steps matching Xcode Simulator presets */
29
+ declare const SCALE_STEPS: readonly [0.25, 0.33, 0.5, 0.75, 1];
30
+ /**
31
+ * Compute the adaptive scale factor for a device in a given container.
32
+ * Mirrors Xcode "fit to screen" exactly.
33
+ *
34
+ * @param deviceWidth - Logical points width of the device screen
35
+ * @param deviceHeight - Logical points height of the device screen
36
+ * @param containerWidth - Available CSS px width of the host container
37
+ * @param containerHeight - Available CSS px height of the host container
38
+ * @param padding - Breathing room in px (subtracted from each side)
39
+ * @param maxScale - Upper cap — never exceed 1.0 (real device size)
40
+ * @param minScale - Lower floor — prevent invisibly small render
41
+ */
42
+ declare function computeAdaptiveScale(deviceWidth: number, deviceHeight: number, containerWidth: number, containerHeight: number, padding?: number, maxScale?: number, minScale?: number): number;
43
+ /**
44
+ * Snap a raw scale to the nearest discrete step that does not exceed it.
45
+ * Fixed version: filters steps to only those <= raw, takes max.
46
+ * Falls back to raw if no step qualifies.
47
+ */
48
+ declare function snapToStep(raw: number): number;
49
+ /**
50
+ * Derive host container size from device dimensions + scale.
51
+ * The host div occupies these dimensions in normal document flow.
52
+ */
53
+ declare function computeHostSize(deviceWidth: number, deviceHeight: number, scale: number): {
54
+ width: number;
55
+ height: number;
56
+ };
57
+ /** Result returned by the scale computation */
58
+ interface AdaptiveScaleResult {
59
+ /** Computed scale factor e.g. 0.735 */
60
+ scale: number;
61
+ /** Host container width = deviceWidth * scale */
62
+ scaledWidth: number;
63
+ /** Host container height = deviceHeight * scale */
64
+ scaledHeight: number;
65
+ /** Raw logical pts — scaler div width */
66
+ deviceWidth: number;
67
+ /** Raw logical pts — scaler div height */
68
+ deviceHeight: number;
69
+ /** True when scale === maxScale (showing at real size) */
70
+ isAtMaxScale: boolean;
71
+ /** True when scale < maxScale (container is limiting) */
72
+ isConstrained: boolean;
73
+ /** Display string e.g. "73%" */
74
+ scalePercent: string;
75
+ }
76
+ /**
77
+ * Full scale computation returning all derived values.
78
+ */
79
+ declare function computeFullScale(deviceWidth: number, deviceHeight: number, containerWidth: number, containerHeight: number, options?: {
80
+ padding?: number;
81
+ maxScale?: number;
82
+ minScale?: number;
83
+ snapToSteps?: boolean;
84
+ }): AdaptiveScaleResult;
85
+
86
+ interface ContainerSize {
87
+ width: number;
88
+ height: number;
89
+ }
90
+ /**
91
+ * Observes an element's content box dimensions via ResizeObserver.
92
+ * Foundation of adaptive scaling — same mechanism as Xcode's window resize detection.
93
+ * Uses the actual canvas div, not the window, so it's panel-independent.
94
+ */
95
+ declare function useContainerSize(ref: RefObject<HTMLElement | null>): ContainerSize;
96
+
97
+ interface UseAdaptiveScaleOptions {
98
+ /** Device metadata (screen dimensions) */
99
+ device: DeviceMeta;
100
+ /** Container width from useContainerSize */
101
+ containerWidth: number;
102
+ /** Container height from useContainerSize */
103
+ containerHeight: number;
104
+ /** Breathing room in px (default: 24) */
105
+ padding?: number;
106
+ /** Upper cap — never exceed 1.0 (default: 1.0) */
107
+ maxScale?: number;
108
+ /** Lower floor (default: 0.1) */
109
+ minScale?: number;
110
+ /** Snap to discrete 25/33/50/75/100% steps (default: false) */
111
+ snapToSteps?: boolean;
112
+ }
113
+ /**
114
+ * Full adaptive scale hook — mirrors Xcode "fit to screen".
115
+ * Recalculates live as container resizes via ResizeObserver.
116
+ */
117
+ declare function useAdaptiveScale(options: UseAdaptiveScaleOptions): AdaptiveScaleResult;
118
+
119
+ interface UseDeviceContractResult {
120
+ contract: DeviceLayoutContract;
121
+ cssVariables: DeviceLayoutContract["cssVariables"];
122
+ contentZone: DeviceLayoutContract["contentZone"]["portrait"];
123
+ }
124
+ /**
125
+ * Hook to access the Device Layout Contract for a given device.
126
+ * Returns the contract, CSS variables, and content zone for the current orientation.
127
+ */
128
+ declare function useDeviceContract(deviceId: string, orientation?: "portrait" | "landscape"): UseDeviceContractResult;
129
+
130
+ interface VolumeState {
131
+ /** Current volume level 0–1 (16 steps, each = 1/16) */
132
+ level: number;
133
+ /** Is audio muted? */
134
+ muted: boolean;
135
+ /** Should the HUD be visible? */
136
+ hudVisible: boolean;
137
+ /** Increase volume by one step */
138
+ volumeUp: () => void;
139
+ /** Decrease volume by one step */
140
+ volumeDown: () => void;
141
+ /** Toggle mute */
142
+ toggleMute: () => void;
143
+ }
144
+ /**
145
+ * Volume control hook — 16-step volume with auto-hiding HUD.
146
+ * Applies volume to all <audio> and <video> elements inside `.bielaframe-content`.
147
+ */
148
+ declare function useVolumeControl(initialVolume?: number): VolumeState;
149
+
150
+ interface ScreenPowerState {
151
+ /** Is the screen off? */
152
+ isOff: boolean;
153
+ /** Toggle screen on/off */
154
+ toggle: () => void;
155
+ }
156
+ /**
157
+ * Screen power hook — manages on/off state for the power button.
158
+ */
159
+ declare function useScreenPower(): ScreenPowerState;
160
+
161
+ /**
162
+ * Resolve device SVG component by ID.
163
+ * This is a lookup registry — devices register their SVG components here.
164
+ */
165
+ type DeviceSVGComponent = React.ComponentType<{
166
+ colorScheme?: "light" | "dark";
167
+ style?: React.CSSProperties;
168
+ }>;
169
+ interface FrameInfo {
170
+ bezelTop: number;
171
+ bezelBottom: number;
172
+ bezelLeft: number;
173
+ bezelRight: number;
174
+ totalWidth: number;
175
+ totalHeight: number;
176
+ screenWidth: number;
177
+ screenHeight: number;
178
+ screenRadius: number;
179
+ }
180
+ /** Register a device SVG component for use with DeviceFrame */
181
+ declare function registerDeviceSVG(deviceId: string, component: DeviceSVGComponent, frame: FrameInfo): void;
182
+ /** Optional crop area to rewrite the SVG viewBox (e.g., to exclude side buttons) */
183
+ interface SVGCropArea {
184
+ x: number;
185
+ y: number;
186
+ width: number;
187
+ height: number;
188
+ }
189
+ /** Screen rectangle in SVG native coordinates — used to punch a transparent hole */
190
+ interface SVGScreenRect {
191
+ x: number;
192
+ y: number;
193
+ width: number;
194
+ height: number;
195
+ rx?: number;
196
+ }
197
+ /**
198
+ * Register a custom device SVG from a raw SVG string.
199
+ *
200
+ * Creates a React component that renders the SVG with scoped IDs
201
+ * and registers it in the SVG_REGISTRY.
202
+ *
203
+ * The inner <svg> element is rewritten to use width="100%" height="100%"
204
+ * so it fills the parent container (which is sized by the frame info).
205
+ *
206
+ * If `cropViewBox` is provided, the SVG viewBox is set to that area,
207
+ * cropping out external elements like side buttons.
208
+ *
209
+ * If `screenRect` is provided, a mask is injected to punch a transparent
210
+ * hole where the screen is, so content underneath shows through.
211
+ */
212
+ declare function registerCustomDeviceSVG(deviceId: string, svgString: string, frame: FrameInfo, cropViewBox?: SVGCropArea, screenRect?: SVGScreenRect): void;
213
+ interface DeviceFrameProps {
214
+ /** Device ID e.g. "iphone-16-pro" */
215
+ device: string;
216
+ /** Orientation */
217
+ orientation?: "portrait" | "landscape";
218
+ /** Scale mode: "fit" auto-scales, "manual" uses manualScale value */
219
+ scaleMode?: "fit" | "manual" | "steps";
220
+ /** Manual scale override (0.1–1.0), only when scaleMode="manual" */
221
+ manualScale?: number;
222
+ /** Show safe zone overlay (dev tool) */
223
+ showSafeAreaOverlay?: boolean;
224
+ /** Show DLC JSON panel */
225
+ showDLCPanel?: boolean;
226
+ /** Show scale bar (default: true) */
227
+ showScaleBar?: boolean;
228
+ /** Frame color scheme */
229
+ colorScheme?: "light" | "dark";
230
+ /** Callback when DLC is ready */
231
+ onContractReady?: (dlc: DeviceLayoutContract) => void;
232
+ /** Callback when scale changes */
233
+ onScaleChange?: (scale: number) => void;
234
+ /** Content rendered inside the device screen at real resolution */
235
+ children?: ReactNode;
236
+ }
237
+ /**
238
+ * DeviceFrame — the main BielaFrame component.
239
+ *
240
+ * Implements the Two-World Rendering Model:
241
+ * - Inner content renders at 1:1 device resolution (e.g. 402×874px)
242
+ * - Visual presentation scales via CSS transform: scale()
243
+ * - The AI component always thinks it's on a real device
244
+ *
245
+ * DOM structure:
246
+ * bielaframe-sentinel (fills parent, ResizeObserver target)
247
+ * └── bielaframe-host (scaled dimensions in flow)
248
+ * └── bielaframe-scaler (real device dims, transform: scale(N))
249
+ * ├── bielaframe-content (clipped to screen, CSS vars injected)
250
+ * │ └── children (AI-generated component)
251
+ * ├── SVG frame overlay (pointer-events: none)
252
+ * └── SafeAreaOverlay (optional dev tool)
253
+ * └── ScaleBar (outside scaler, in normal flow)
254
+ */
255
+ declare function DeviceFrame({ device, orientation, scaleMode, manualScale, showSafeAreaOverlay, showScaleBar, colorScheme, onContractReady, onScaleChange, children, }: DeviceFrameProps): react_jsx_runtime.JSX.Element;
256
+
257
+ interface Props {
258
+ children: ReactNode;
259
+ fallback?: ReactNode;
260
+ }
261
+ interface State {
262
+ hasError: boolean;
263
+ error: Error | null;
264
+ }
265
+ /**
266
+ * Error Boundary wrapping the children slot in DeviceFrame.
267
+ * AI-generated components will crash — this catches them gracefully
268
+ * and renders a fallback inside the device screen area.
269
+ */
270
+ declare class DeviceErrorBoundary extends Component<Props, State> {
271
+ constructor(props: Props);
272
+ static getDerivedStateFromError(error: Error): State;
273
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
274
+ render(): ReactNode;
275
+ }
276
+
277
+ interface SafeAreaViewProps {
278
+ /** Which edges to apply safe area padding to (default: all) */
279
+ edges?: Array<"top" | "bottom" | "left" | "right">;
280
+ children: ReactNode;
281
+ style?: CSSProperties;
282
+ }
283
+ /**
284
+ * Convenience wrapper that applies safe area padding via CSS variables.
285
+ * Uses var(--safe-top), var(--safe-bottom), etc. — injected by DeviceFrame.
286
+ *
287
+ * Usage inside a generated app:
288
+ * <SafeAreaView edges={["top", "bottom"]}>
289
+ * {content that never overlaps hardware elements}
290
+ * </SafeAreaView>
291
+ */
292
+ declare function SafeAreaView({ edges, children, style }: SafeAreaViewProps): react_jsx_runtime.JSX.Element;
293
+
294
+ interface SafeAreaOverlayProps {
295
+ contract: DeviceLayoutContract;
296
+ orientation: "portrait" | "landscape";
297
+ }
298
+ /**
299
+ * Dev overlay showing safe zones when showSafeAreaOverlay={true}.
300
+ * Does NOT affect layout — position: absolute, pointerEvents: none.
301
+ *
302
+ * Colors:
303
+ * - Red: status bar zone + home indicator zone (restricted)
304
+ * - Orange: hardware overlay (Dynamic Island / punch-hole)
305
+ * - Green: content zone (usable area)
306
+ * - White dimension labels at each edge
307
+ */
308
+ declare function SafeAreaOverlay({ contract, orientation }: SafeAreaOverlayProps): react_jsx_runtime.JSX.Element;
309
+
310
+ interface ScaleBarProps {
311
+ deviceName: string;
312
+ deviceWidth: number;
313
+ deviceHeight: number;
314
+ scale: number;
315
+ scalePercent: string;
316
+ isAtMaxScale: boolean;
317
+ isConstrained: boolean;
318
+ onScaleChange?: (scale: number) => void;
319
+ onFit?: () => void;
320
+ onRealSize?: () => void;
321
+ }
322
+ /**
323
+ * Persistent footer below device showing scale state:
324
+ * [ iPhone 16 Pro · 402×874pt | ━━━●━━━━━ 66% | Fit ↗ | 1:1 ]
325
+ *
326
+ * Accessibility:
327
+ * - role="slider" with aria-valuenow on scrubber
328
+ * - aria-label on the component
329
+ * - aria-live="polite" on scale announcements
330
+ */
331
+ declare function ScaleBar({ deviceName, deviceWidth, deviceHeight, scale, scalePercent, isAtMaxScale, isConstrained, onScaleChange, onFit, onRealSize, }: ScaleBarProps): react_jsx_runtime.JSX.Element;
332
+
333
+ interface VolumeHUDProps {
334
+ level: number;
335
+ muted: boolean;
336
+ visible: boolean;
337
+ platform: "ios" | "android";
338
+ }
339
+ /**
340
+ * Volume HUD overlay — iOS-style vertical pill or Android-style horizontal bar.
341
+ */
342
+ declare function VolumeHUD({ level, muted, visible, platform }: VolumeHUDProps): react_jsx_runtime.JSX.Element;
343
+
344
+ type ButtonName = "volumeUp" | "volumeDown" | "power" | "actionButton" | "cameraControl";
345
+ interface HardwareButtonsProps {
346
+ /** The container element that holds the SVG frame overlay */
347
+ frameContainerRef: React.RefObject<HTMLDivElement | null>;
348
+ /** Callbacks for button actions */
349
+ onButtonPress?: (button: ButtonName) => void;
350
+ /** Whether interactive buttons are enabled */
351
+ enabled?: boolean;
352
+ /** Current orientation — triggers button position re-discovery */
353
+ orientation?: "portrait" | "landscape";
354
+ }
355
+ /**
356
+ * HardwareButtons — creates invisible click-target overlays positioned
357
+ * over the SVG button elements.
358
+ *
359
+ * Because the SVG frame overlay has pointer-events: none (so content
360
+ * underneath remains interactive), we can't attach event listeners directly
361
+ * to SVG elements. Instead, we:
362
+ * 1. Find all [data-button] elements in the SVG
363
+ * 2. Read their bounding rects relative to the scaler container
364
+ * 3. Render transparent absolutely-positioned divs at those positions
365
+ * 4. Handle mouse/touch events on those divs
366
+ * 5. Apply press animation to the original SVG elements
367
+ */
368
+ declare function HardwareButtons({ frameContainerRef, onButtonPress, enabled, orientation, }: HardwareButtonsProps): ReactElement | null;
369
+
370
+ interface DynamicStatusBarProps {
371
+ contract: DeviceLayoutContract;
372
+ orientation: "portrait" | "landscape";
373
+ colorScheme: "light" | "dark";
374
+ /** Show a live updating clock (default: true) */
375
+ showLiveClock?: boolean;
376
+ /** Fixed time string override (e.g. "9:41") — disables live clock */
377
+ fixedTime?: string;
378
+ }
379
+ /**
380
+ * DynamicStatusBar — renders ONLY a live clock overlay.
381
+ *
382
+ * The SVG frame already provides all decorative status bar elements
383
+ * (signal bars, wifi, battery). This component only replaces the
384
+ * static time text (e.g. "9:41") with a live-updating clock.
385
+ *
386
+ * It renders a small opaque patch behind the clock text so the
387
+ * SVG's baked-in static time is fully covered, then draws the
388
+ * live time on top.
389
+ *
390
+ * Platform-specific clock positions:
391
+ * - iOS Dynamic Island: left side, paddingLeft ~20
392
+ * - iOS Notch: left side, paddingLeft ~20
393
+ * - iOS SE: centered
394
+ * - Android: left side, paddingLeft ~16
395
+ */
396
+ declare function DynamicStatusBar({ contract, orientation, colorScheme, showLiveClock, fixedTime, }: DynamicStatusBarProps): react_jsx_runtime.JSX.Element | null;
397
+
398
+ interface StatusBarIndicatorsProps {
399
+ platform: "ios" | "android";
400
+ colorScheme: "light" | "dark";
401
+ }
402
+ /**
403
+ * Static decorative status bar indicators — signal, wifi, battery.
404
+ * Rendered as small inline SVGs, platform-specific styling.
405
+ */
406
+ declare function StatusBarIndicators({ platform, colorScheme }: StatusBarIndicatorsProps): react_jsx_runtime.JSX.Element;
407
+
408
+ export { type AdaptiveScaleResult, type ButtonName, DeviceErrorBoundary, DeviceFrame, type DeviceFrameProps, DynamicStatusBar, HardwareButtons, SCALE_STEPS, type SVGCropArea, type SVGScreenRect, SafeAreaOverlay, SafeAreaView, ScaleBar, type ScreenPowerState, StatusBarIndicators, type UseAdaptiveScaleOptions, VolumeHUD, type VolumeState, computeAdaptiveScale, computeFullScale, computeHostSize, ptsToPercent, ptsToPx, pxToPts, registerCustomDeviceSVG, registerDeviceSVG, scaleValue, snapToStep, useAdaptiveScale, useContainerSize, useDeviceContract, useScreenPower, useVolumeControl };