@buoy-gg/image-overlay 2.1.11

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.
Files changed (46) hide show
  1. package/lib/commonjs/imageOverlay/components/ImageOverlayModal.js +847 -0
  2. package/lib/commonjs/imageOverlay/components/ImageOverlayStandalone.js +455 -0
  3. package/lib/commonjs/imageOverlay/components/OverlayControls.js +253 -0
  4. package/lib/commonjs/imageOverlay/components/TargetList.js +183 -0
  5. package/lib/commonjs/imageOverlay/index.js +33 -0
  6. package/lib/commonjs/imageOverlay/types/index.js +1 -0
  7. package/lib/commonjs/imageOverlay/utils/ImageOverlayController.js +392 -0
  8. package/lib/commonjs/imageOverlay/utils/componentMeasurement.js +106 -0
  9. package/lib/commonjs/imageOverlay/utils/fiberScanner.js +101 -0
  10. package/lib/commonjs/index.js +46 -0
  11. package/lib/commonjs/package.json +1 -0
  12. package/lib/commonjs/preset.js +59 -0
  13. package/lib/module/imageOverlay/components/ImageOverlayModal.js +843 -0
  14. package/lib/module/imageOverlay/components/ImageOverlayStandalone.js +451 -0
  15. package/lib/module/imageOverlay/components/OverlayControls.js +249 -0
  16. package/lib/module/imageOverlay/components/TargetList.js +179 -0
  17. package/lib/module/imageOverlay/index.js +6 -0
  18. package/lib/module/imageOverlay/types/index.js +1 -0
  19. package/lib/module/imageOverlay/utils/ImageOverlayController.js +388 -0
  20. package/lib/module/imageOverlay/utils/componentMeasurement.js +102 -0
  21. package/lib/module/imageOverlay/utils/fiberScanner.js +97 -0
  22. package/lib/module/index.js +34 -0
  23. package/lib/module/preset.js +53 -0
  24. package/lib/typescript/imageOverlay/components/ImageOverlayModal.d.ts +3 -0
  25. package/lib/typescript/imageOverlay/components/ImageOverlayModal.d.ts.map +1 -0
  26. package/lib/typescript/imageOverlay/components/ImageOverlayStandalone.d.ts +12 -0
  27. package/lib/typescript/imageOverlay/components/ImageOverlayStandalone.d.ts.map +1 -0
  28. package/lib/typescript/imageOverlay/components/OverlayControls.d.ts +18 -0
  29. package/lib/typescript/imageOverlay/components/OverlayControls.d.ts.map +1 -0
  30. package/lib/typescript/imageOverlay/components/TargetList.d.ts +12 -0
  31. package/lib/typescript/imageOverlay/components/TargetList.d.ts.map +1 -0
  32. package/lib/typescript/imageOverlay/index.d.ts +6 -0
  33. package/lib/typescript/imageOverlay/index.d.ts.map +1 -0
  34. package/lib/typescript/imageOverlay/types/index.d.ts +53 -0
  35. package/lib/typescript/imageOverlay/types/index.d.ts.map +1 -0
  36. package/lib/typescript/imageOverlay/utils/ImageOverlayController.d.ts +34 -0
  37. package/lib/typescript/imageOverlay/utils/ImageOverlayController.d.ts.map +1 -0
  38. package/lib/typescript/imageOverlay/utils/componentMeasurement.d.ts +13 -0
  39. package/lib/typescript/imageOverlay/utils/componentMeasurement.d.ts.map +1 -0
  40. package/lib/typescript/imageOverlay/utils/fiberScanner.d.ts +13 -0
  41. package/lib/typescript/imageOverlay/utils/fiberScanner.d.ts.map +1 -0
  42. package/lib/typescript/index.d.ts +14 -0
  43. package/lib/typescript/index.d.ts.map +1 -0
  44. package/lib/typescript/preset.d.ts +46 -0
  45. package/lib/typescript/preset.d.ts.map +1 -0
  46. package/package.json +79 -0
@@ -0,0 +1,451 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Standalone overlay that renders:
5
+ * - Component mode: highlight border + image overlay on a tagged component
6
+ * - Free mode: draggable, resizable image anywhere on screen (aspect-ratio locked)
7
+ *
8
+ * Supports Normal, Difference, and Multiply blend modes.
9
+ * Supports image inversion (flip X/Y) and position locking.
10
+ * Auto-mounted by FloatingDevTools alongside other standalone overlays.
11
+ */
12
+
13
+ import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
14
+ import { View, Text, Image as RNImage, StyleSheet, PanResponder, Dimensions } from "react-native";
15
+ import { ImageOverlayController } from "../utils/ImageOverlayController";
16
+
17
+ // ─── Overlay Image (handles flip transforms) ─────────────────────────────────
18
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
19
+ function OverlayImage({
20
+ uri,
21
+ style,
22
+ invertX,
23
+ invertY
24
+ }) {
25
+ const transform = [];
26
+ if (invertX) transform.push({
27
+ scaleX: -1
28
+ });
29
+ if (invertY) transform.push({
30
+ scaleY: -1
31
+ });
32
+ const flipStyle = transform.length > 0 ? {
33
+ transform
34
+ } : {};
35
+ return /*#__PURE__*/_jsx(RNImage, {
36
+ source: {
37
+ uri
38
+ },
39
+ style: [style, flipStyle]
40
+ // @ts-ignore
41
+ ,
42
+ pointerEvents: "none"
43
+ });
44
+ }
45
+
46
+ // ─── Free Placement ───────────────────────────────────────────────────────────
47
+
48
+ const FREE_MIN_SIZE = 30;
49
+ function FreePlacement({
50
+ imageUri,
51
+ x,
52
+ y,
53
+ width,
54
+ height,
55
+ opacity,
56
+ aspectRatio,
57
+ invertX,
58
+ invertY,
59
+ locked
60
+ }) {
61
+ const [isInteracting, setIsInteracting] = useState(false);
62
+ const startPosRef = useRef({
63
+ x: 0,
64
+ y: 0
65
+ });
66
+ const startDimsRef = useRef({
67
+ x: 0,
68
+ y: 0,
69
+ width: 0,
70
+ height: 0
71
+ });
72
+ const screen = Dimensions.get("window");
73
+
74
+ // ── Drag ──
75
+ const dragResponder = useMemo(() => PanResponder.create({
76
+ onStartShouldSetPanResponder: () => !locked,
77
+ onMoveShouldSetPanResponder: (_, g) => !locked && (Math.abs(g.dx) > 2 || Math.abs(g.dy) > 2),
78
+ onPanResponderTerminationRequest: () => false,
79
+ onPanResponderGrant: () => {
80
+ const s = ImageOverlayController.getState();
81
+ startPosRef.current = {
82
+ x: s.freeX,
83
+ y: s.freeY
84
+ };
85
+ setIsInteracting(true);
86
+ },
87
+ onPanResponderMove: (_, g) => {
88
+ const s = ImageOverlayController.getState();
89
+ const newX = Math.max(0, Math.min(startPosRef.current.x + g.dx, screen.width - s.freeWidth));
90
+ const newY = Math.max(0, Math.min(startPosRef.current.y + g.dy, screen.height - s.freeHeight));
91
+ ImageOverlayController.setFreePosition(newX, newY);
92
+ },
93
+ onPanResponderRelease: () => setIsInteracting(false),
94
+ onPanResponderTerminate: () => setIsInteracting(false)
95
+ }), [screen.width, screen.height, locked]);
96
+
97
+ // ── Aspect-ratio-locked corner resize ──
98
+ const createResizeHandler = useCallback(corner => PanResponder.create({
99
+ onStartShouldSetPanResponder: () => !locked,
100
+ onMoveShouldSetPanResponder: () => !locked,
101
+ onPanResponderTerminationRequest: () => false,
102
+ onPanResponderGrant: () => {
103
+ const s = ImageOverlayController.getState();
104
+ startDimsRef.current = {
105
+ x: s.freeX,
106
+ y: s.freeY,
107
+ width: s.freeWidth,
108
+ height: s.freeHeight
109
+ };
110
+ setIsInteracting(true);
111
+ },
112
+ onPanResponderMove: (_, g) => {
113
+ const {
114
+ dx,
115
+ dy
116
+ } = g;
117
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return;
118
+ const start = startDimsRef.current;
119
+ const ratio = aspectRatio > 0 ? aspectRatio : start.width / start.height;
120
+ const absDx = Math.abs(dx);
121
+ const absDy = Math.abs(dy);
122
+ const useDx = absDx >= absDy;
123
+ let newWidth;
124
+ let newHeight;
125
+ let newX = start.x;
126
+ let newY = start.y;
127
+ switch (corner) {
128
+ case "bottomRight":
129
+ {
130
+ newWidth = useDx ? start.width + dx : (start.height + dy) * ratio;
131
+ newWidth = Math.max(FREE_MIN_SIZE, Math.min(newWidth, screen.width - start.x));
132
+ newHeight = newWidth / ratio;
133
+ break;
134
+ }
135
+ case "bottomLeft":
136
+ {
137
+ newWidth = useDx ? start.width - dx : (start.height + dy) * ratio;
138
+ newWidth = Math.max(FREE_MIN_SIZE, newWidth);
139
+ newHeight = newWidth / ratio;
140
+ newX = start.x + start.width - newWidth;
141
+ newX = Math.max(0, newX);
142
+ newWidth = Math.min(newWidth, start.x + start.width);
143
+ newHeight = newWidth / ratio;
144
+ break;
145
+ }
146
+ case "topRight":
147
+ {
148
+ newWidth = useDx ? start.width + dx : (start.height - dy) * ratio;
149
+ newWidth = Math.max(FREE_MIN_SIZE, Math.min(newWidth, screen.width - start.x));
150
+ newHeight = newWidth / ratio;
151
+ newY = start.y + start.height - newHeight;
152
+ newY = Math.max(0, newY);
153
+ newHeight = Math.min(newHeight, start.y + start.height);
154
+ newWidth = newHeight * ratio;
155
+ break;
156
+ }
157
+ case "topLeft":
158
+ {
159
+ newWidth = useDx ? start.width - dx : (start.height - dy) * ratio;
160
+ newWidth = Math.max(FREE_MIN_SIZE, newWidth);
161
+ newHeight = newWidth / ratio;
162
+ newX = start.x + start.width - newWidth;
163
+ newY = start.y + start.height - newHeight;
164
+ newX = Math.max(0, newX);
165
+ newY = Math.max(0, newY);
166
+ newWidth = Math.min(newWidth, start.x + start.width);
167
+ newHeight = newWidth / ratio;
168
+ newY = start.y + start.height - newHeight;
169
+ break;
170
+ }
171
+ }
172
+ newWidth = Math.max(FREE_MIN_SIZE, Math.round(newWidth));
173
+ newHeight = Math.max(FREE_MIN_SIZE, Math.round(newHeight));
174
+ ImageOverlayController.setFreeDimensions(newWidth, newHeight, Math.round(newX), Math.round(newY));
175
+ },
176
+ onPanResponderRelease: () => setIsInteracting(false),
177
+ onPanResponderTerminate: () => setIsInteracting(false)
178
+ }), [screen.width, screen.height, aspectRatio, locked]);
179
+ const resizeHandlers = useMemo(() => ({
180
+ topLeft: createResizeHandler("topLeft"),
181
+ topRight: createResizeHandler("topRight"),
182
+ bottomLeft: createResizeHandler("bottomLeft"),
183
+ bottomRight: createResizeHandler("bottomRight")
184
+ }), [createResizeHandler]);
185
+ const imageStyle = {
186
+ ...StyleSheet.absoluteFillObject,
187
+ opacity
188
+ };
189
+ return /*#__PURE__*/_jsxs(View, {
190
+ style: {
191
+ position: "absolute",
192
+ left: x,
193
+ top: y,
194
+ width,
195
+ height
196
+ },
197
+ children: [/*#__PURE__*/_jsx(View, {
198
+ style: StyleSheet.absoluteFill,
199
+ ...(locked ? {} : dragResponder.panHandlers),
200
+ children: /*#__PURE__*/_jsx(OverlayImage, {
201
+ uri: imageUri,
202
+ style: imageStyle,
203
+ invertX: invertX,
204
+ invertY: invertY
205
+ })
206
+ }), /*#__PURE__*/_jsx(View, {
207
+ pointerEvents: "none",
208
+ style: [styles.freeBorder, isInteracting && styles.freeBorderActive, locked && styles.freeBorderLocked]
209
+ }), !locked && /*#__PURE__*/_jsxs(_Fragment, {
210
+ children: [/*#__PURE__*/_jsx(View, {
211
+ ...resizeHandlers.topLeft.panHandlers,
212
+ style: [styles.cornerWrapper, {
213
+ top: -20,
214
+ left: -20
215
+ }],
216
+ hitSlop: {
217
+ top: 10,
218
+ left: 10,
219
+ right: 10,
220
+ bottom: 10
221
+ },
222
+ children: /*#__PURE__*/_jsx(View, {
223
+ style: [styles.cornerHandle, isInteracting && styles.cornerHandleActive]
224
+ })
225
+ }), /*#__PURE__*/_jsx(View, {
226
+ ...resizeHandlers.topRight.panHandlers,
227
+ style: [styles.cornerWrapper, {
228
+ top: -20,
229
+ right: -20
230
+ }],
231
+ hitSlop: {
232
+ top: 10,
233
+ left: 10,
234
+ right: 10,
235
+ bottom: 10
236
+ },
237
+ children: /*#__PURE__*/_jsx(View, {
238
+ style: [styles.cornerHandle, isInteracting && styles.cornerHandleActive]
239
+ })
240
+ }), /*#__PURE__*/_jsx(View, {
241
+ ...resizeHandlers.bottomLeft.panHandlers,
242
+ style: [styles.cornerWrapper, {
243
+ bottom: -20,
244
+ left: -20
245
+ }],
246
+ hitSlop: {
247
+ top: 10,
248
+ left: 10,
249
+ right: 10,
250
+ bottom: 10
251
+ },
252
+ children: /*#__PURE__*/_jsx(View, {
253
+ style: [styles.cornerHandle, isInteracting && styles.cornerHandleActive]
254
+ })
255
+ }), /*#__PURE__*/_jsx(View, {
256
+ ...resizeHandlers.bottomRight.panHandlers,
257
+ style: [styles.cornerWrapper, {
258
+ bottom: -20,
259
+ right: -20
260
+ }],
261
+ hitSlop: {
262
+ top: 10,
263
+ left: 10,
264
+ right: 10,
265
+ bottom: 10
266
+ },
267
+ children: /*#__PURE__*/_jsx(View, {
268
+ style: [styles.cornerHandle, isInteracting && styles.cornerHandleActive]
269
+ })
270
+ })]
271
+ })]
272
+ });
273
+ }
274
+
275
+ // ─── Main Standalone Overlay ──────────────────────────────────────────────────
276
+
277
+ export function ImageOverlayStandalone() {
278
+ const [state, setState] = useState(() => ImageOverlayController.getState());
279
+ useEffect(() => {
280
+ return ImageOverlayController.subscribe(setState);
281
+ }, []);
282
+ const {
283
+ mode,
284
+ enabled,
285
+ imageUri,
286
+ imageWidth,
287
+ imageHeight,
288
+ targetRect,
289
+ targetLabel,
290
+ showOutline,
291
+ opacity,
292
+ scale,
293
+ offsetX,
294
+ offsetY,
295
+ invertX,
296
+ invertY,
297
+ locked,
298
+ freeX,
299
+ freeY,
300
+ freeWidth,
301
+ freeHeight
302
+ } = state;
303
+ const aspectRatio = imageWidth && imageHeight && imageHeight > 0 ? imageWidth / imageHeight : 0;
304
+
305
+ // ─── Free Placement Mode ───
306
+ if (mode === "free" && enabled && imageUri) {
307
+ return /*#__PURE__*/_jsx(View, {
308
+ style: styles.overlay,
309
+ pointerEvents: "box-none",
310
+ nativeID: "image-overlay-standalone",
311
+ children: /*#__PURE__*/_jsx(FreePlacement, {
312
+ imageUri: imageUri,
313
+ x: freeX,
314
+ y: freeY,
315
+ width: freeWidth,
316
+ height: freeHeight,
317
+ opacity: opacity,
318
+ aspectRatio: aspectRatio,
319
+ invertX: invertX,
320
+ invertY: invertY,
321
+ locked: locked
322
+ })
323
+ });
324
+ }
325
+
326
+ // ─── Component Match Mode ───
327
+ if (!targetRect) return null;
328
+ const showImage = enabled && imageUri;
329
+ return /*#__PURE__*/_jsxs(View, {
330
+ style: styles.overlay,
331
+ pointerEvents: "box-none",
332
+ nativeID: "image-overlay-standalone",
333
+ children: [showOutline && /*#__PURE__*/_jsxs(_Fragment, {
334
+ children: [/*#__PURE__*/_jsx(View, {
335
+ pointerEvents: "none",
336
+ nativeID: "__image_overlay_highlight__",
337
+ style: [styles.highlight, {
338
+ left: targetRect.x - 3,
339
+ top: targetRect.y - 3,
340
+ width: targetRect.width + 6,
341
+ height: targetRect.height + 6
342
+ }]
343
+ }), targetLabel && /*#__PURE__*/_jsx(View, {
344
+ pointerEvents: "none",
345
+ style: [styles.label, {
346
+ left: targetRect.x,
347
+ top: targetRect.y - 20
348
+ }],
349
+ children: /*#__PURE__*/_jsx(Text, {
350
+ style: styles.labelText,
351
+ children: targetLabel
352
+ })
353
+ })]
354
+ }), showImage && (() => {
355
+ const w = (imageWidth ?? targetRect.width) * scale;
356
+ const h = (imageHeight ?? targetRect.height) * scale;
357
+ const centerX = targetRect.x + targetRect.width / 2;
358
+ const centerY = targetRect.y + targetRect.height / 2;
359
+ const imgStyle = {
360
+ position: "absolute",
361
+ left: centerX - w / 2 + offsetX,
362
+ top: centerY - h / 2 + offsetY,
363
+ width: w,
364
+ height: h,
365
+ opacity
366
+ };
367
+ return /*#__PURE__*/_jsx(OverlayImage, {
368
+ uri: imageUri,
369
+ style: imgStyle,
370
+ invertX: invertX,
371
+ invertY: invertY
372
+ });
373
+ })()]
374
+ });
375
+ }
376
+ const styles = StyleSheet.create({
377
+ overlay: {
378
+ position: "absolute",
379
+ top: 0,
380
+ left: 0,
381
+ right: 0,
382
+ bottom: 0,
383
+ zIndex: 999999,
384
+ elevation: 999999,
385
+ borderWidth: 0
386
+ },
387
+ highlight: {
388
+ position: "absolute",
389
+ borderWidth: 2,
390
+ borderStyle: "dashed",
391
+ borderColor: "#20C997",
392
+ backgroundColor: "transparent"
393
+ },
394
+ label: {
395
+ position: "absolute",
396
+ backgroundColor: "#20C997",
397
+ paddingHorizontal: 6,
398
+ paddingVertical: 2,
399
+ borderRadius: 3
400
+ },
401
+ labelText: {
402
+ color: "#fff",
403
+ fontSize: 10,
404
+ fontWeight: "700",
405
+ fontFamily: "monospace"
406
+ },
407
+ freeBorder: {
408
+ position: "absolute",
409
+ top: -6,
410
+ left: -6,
411
+ right: -6,
412
+ bottom: -6,
413
+ borderWidth: 1,
414
+ borderColor: "transparent",
415
+ borderRadius: 4
416
+ },
417
+ freeBorderActive: {
418
+ borderColor: "#20C997",
419
+ borderStyle: "dashed"
420
+ },
421
+ freeBorderLocked: {
422
+ borderColor: "#f59e0b",
423
+ borderStyle: "solid"
424
+ },
425
+ cornerWrapper: {
426
+ position: "absolute",
427
+ width: 24,
428
+ height: 24,
429
+ zIndex: 1000,
430
+ alignItems: "center",
431
+ justifyContent: "center"
432
+ },
433
+ cornerHandle: {
434
+ width: 12,
435
+ height: 12,
436
+ borderRadius: 6,
437
+ backgroundColor: "#20C997",
438
+ borderWidth: 2,
439
+ borderColor: "#fff"
440
+ },
441
+ cornerHandleActive: {
442
+ backgroundColor: "#10b981",
443
+ shadowColor: "#10b981",
444
+ shadowOffset: {
445
+ width: 0,
446
+ height: 0
447
+ },
448
+ shadowOpacity: 0.8,
449
+ shadowRadius: 6
450
+ }
451
+ });
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Controls for adjusting the overlay: opacity, scale, offset.
5
+ * Each control has a slider track plus -/+ stepper buttons.
6
+ */
7
+
8
+ import { useRef, useCallback } from "react";
9
+ import { View, Text, TouchableOpacity, StyleSheet, PanResponder } from "react-native";
10
+ import { macOSColors, buoyColors } from "@buoy-gg/shared-ui";
11
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ function SliderStepper({
13
+ label,
14
+ value,
15
+ min,
16
+ max,
17
+ step,
18
+ displayValue,
19
+ onChange
20
+ }) {
21
+ const trackWidth = useRef(0);
22
+ const trackX = useRef(0);
23
+ const clamp = useCallback(v => Math.max(min, Math.min(max, v)), [min, max]);
24
+ const fraction = max > min ? (value - min) / (max - min) : 0;
25
+
26
+ // Keep current values in refs so PanResponder always uses latest props
27
+ const onChangeRef = useRef(onChange);
28
+ const clampRef = useRef(clamp);
29
+ const minRef = useRef(min);
30
+ const maxRef = useRef(max);
31
+ onChangeRef.current = onChange;
32
+ clampRef.current = clamp;
33
+ minRef.current = min;
34
+ maxRef.current = max;
35
+ const handleSliderEvent = useCallback(pageX => {
36
+ const x = pageX - trackX.current;
37
+ const frac = Math.max(0, Math.min(1, x / trackWidth.current));
38
+ const val = minRef.current + frac * (maxRef.current - minRef.current);
39
+ onChangeRef.current(clampRef.current(val));
40
+ }, []);
41
+ const panResponder = useRef(PanResponder.create({
42
+ onStartShouldSetPanResponder: () => true,
43
+ onMoveShouldSetPanResponder: () => true,
44
+ onPanResponderTerminationRequest: () => false,
45
+ onPanResponderGrant: evt => {
46
+ handleSliderEvent(evt.nativeEvent.pageX);
47
+ },
48
+ onPanResponderMove: evt => {
49
+ handleSliderEvent(evt.nativeEvent.pageX);
50
+ }
51
+ })).current;
52
+ const onTrackLayout = useCallback(e => {
53
+ trackWidth.current = e.nativeEvent.layout.width;
54
+ // We need pageX, measure the track view
55
+ }, []);
56
+ const trackRef = useRef(null);
57
+ const measureTrack = useCallback(() => {
58
+ trackRef.current?.measure?.((_x, _y, _w, _h, pageX) => {
59
+ trackX.current = pageX;
60
+ });
61
+ }, []);
62
+ return /*#__PURE__*/_jsxs(View, {
63
+ style: styles.controlRow,
64
+ children: [/*#__PURE__*/_jsx(Text, {
65
+ style: styles.label,
66
+ children: label
67
+ }), /*#__PURE__*/_jsxs(View, {
68
+ style: styles.controlRight,
69
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
70
+ style: styles.stepperButton,
71
+ onPress: () => onChange(clamp(value - step)),
72
+ activeOpacity: 0.6,
73
+ children: /*#__PURE__*/_jsx(Text, {
74
+ style: styles.stepperButtonText,
75
+ children: "-"
76
+ })
77
+ }), /*#__PURE__*/_jsx(View, {
78
+ style: styles.sliderHitArea,
79
+ ...panResponder.panHandlers,
80
+ children: /*#__PURE__*/_jsxs(View, {
81
+ ref: trackRef,
82
+ style: styles.sliderTrack,
83
+ onLayout: e => {
84
+ onTrackLayout(e);
85
+ measureTrack();
86
+ },
87
+ pointerEvents: "none",
88
+ children: [/*#__PURE__*/_jsx(View, {
89
+ style: [styles.sliderFill, {
90
+ width: `${fraction * 100}%`
91
+ }]
92
+ }), /*#__PURE__*/_jsx(View, {
93
+ style: [styles.sliderThumb, {
94
+ left: `${fraction * 100}%`
95
+ }]
96
+ })]
97
+ })
98
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
99
+ style: styles.stepperButton,
100
+ onPress: () => onChange(clamp(value + step)),
101
+ activeOpacity: 0.6,
102
+ children: /*#__PURE__*/_jsx(Text, {
103
+ style: styles.stepperButtonText,
104
+ children: "+"
105
+ })
106
+ }), /*#__PURE__*/_jsx(Text, {
107
+ style: styles.valueText,
108
+ children: displayValue
109
+ })]
110
+ })]
111
+ });
112
+ }
113
+ export function OverlayControls({
114
+ opacity,
115
+ scale,
116
+ offsetX,
117
+ offsetY,
118
+ onOpacityChange,
119
+ onScaleChange,
120
+ onOffsetChange,
121
+ opacityOnly
122
+ }) {
123
+ if (opacityOnly) {
124
+ return /*#__PURE__*/_jsx(SliderStepper, {
125
+ label: "Opacity",
126
+ value: opacity,
127
+ min: 0,
128
+ max: 1,
129
+ step: 0.05,
130
+ displayValue: `${Math.round(opacity * 100)}%`,
131
+ onChange: onOpacityChange
132
+ });
133
+ }
134
+ return /*#__PURE__*/_jsxs(View, {
135
+ style: styles.container,
136
+ children: [/*#__PURE__*/_jsx(SliderStepper, {
137
+ label: "Opacity",
138
+ value: opacity,
139
+ min: 0,
140
+ max: 1,
141
+ step: 0.05,
142
+ displayValue: `${Math.round(opacity * 100)}%`,
143
+ onChange: onOpacityChange
144
+ }), /*#__PURE__*/_jsx(SliderStepper, {
145
+ label: "Scale",
146
+ value: scale,
147
+ min: 0.05,
148
+ max: 3,
149
+ step: 0.05,
150
+ displayValue: `${Math.round(scale * 100)}%`,
151
+ onChange: onScaleChange
152
+ }), /*#__PURE__*/_jsx(SliderStepper, {
153
+ label: "Offset X",
154
+ value: offsetX,
155
+ min: -200,
156
+ max: 200,
157
+ step: 2,
158
+ displayValue: `${Math.round(offsetX)}px`,
159
+ onChange: v => onOffsetChange(Math.round(v), offsetY)
160
+ }), /*#__PURE__*/_jsx(SliderStepper, {
161
+ label: "Offset Y",
162
+ value: offsetY,
163
+ min: -200,
164
+ max: 200,
165
+ step: 2,
166
+ displayValue: `${Math.round(offsetY)}px`,
167
+ onChange: v => onOffsetChange(offsetX, Math.round(v))
168
+ })]
169
+ });
170
+ }
171
+ const THUMB_SIZE = 18;
172
+ const styles = StyleSheet.create({
173
+ container: {
174
+ gap: 8
175
+ },
176
+ controlRow: {
177
+ paddingHorizontal: 12,
178
+ gap: 4
179
+ },
180
+ label: {
181
+ fontSize: 11,
182
+ fontWeight: "600",
183
+ color: macOSColors.text.muted,
184
+ textTransform: "uppercase",
185
+ letterSpacing: 0.5
186
+ },
187
+ controlRight: {
188
+ flexDirection: "row",
189
+ alignItems: "center",
190
+ gap: 6
191
+ },
192
+ stepperButton: {
193
+ width: 28,
194
+ height: 28,
195
+ borderRadius: 6,
196
+ backgroundColor: macOSColors.background.hover,
197
+ borderWidth: 1,
198
+ borderColor: macOSColors.border.default,
199
+ justifyContent: "center",
200
+ alignItems: "center"
201
+ },
202
+ stepperButtonText: {
203
+ fontSize: 16,
204
+ fontWeight: "700",
205
+ color: macOSColors.text.primary,
206
+ lineHeight: 18
207
+ },
208
+ sliderHitArea: {
209
+ flex: 1,
210
+ paddingVertical: 20,
211
+ paddingHorizontal: 40,
212
+ marginVertical: -20,
213
+ marginHorizontal: -40
214
+ },
215
+ sliderTrack: {
216
+ height: 28,
217
+ justifyContent: "center",
218
+ borderRadius: 6,
219
+ backgroundColor: macOSColors.background.hover,
220
+ borderWidth: 1,
221
+ borderColor: macOSColors.border.default,
222
+ overflow: "hidden"
223
+ },
224
+ sliderFill: {
225
+ position: "absolute",
226
+ left: 0,
227
+ top: 0,
228
+ bottom: 0,
229
+ backgroundColor: buoyColors.primary + "30",
230
+ borderRadius: 5
231
+ },
232
+ sliderThumb: {
233
+ position: "absolute",
234
+ width: 4,
235
+ top: 4,
236
+ bottom: 4,
237
+ marginLeft: -2,
238
+ borderRadius: 2,
239
+ backgroundColor: buoyColors.primary
240
+ },
241
+ valueText: {
242
+ fontSize: 11,
243
+ fontWeight: "600",
244
+ color: macOSColors.text.primary,
245
+ width: 45,
246
+ textAlign: "right",
247
+ fontFamily: "monospace"
248
+ }
249
+ });