@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,843 @@
1
+ "use strict";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { View, Text, TextInput, TouchableOpacity, StyleSheet, Switch, Alert } from "react-native";
5
+ import { JsModal, ModalHeader, macOSColors, buoyColors, Trash2, SquareDashed, Image, PowerToggleButton } from "@buoy-gg/shared-ui";
6
+ import { ImageOverlayController } from "../utils/ImageOverlayController";
7
+ import { scanForImageTargets } from "../utils/fiberScanner";
8
+ import { TargetList } from "./TargetList";
9
+ import { OverlayControls } from "./OverlayControls";
10
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
+ // Module-level so it survives remounts from JsModal
12
+ let persistedImageUrl = "";
13
+ export function ImageOverlayModal({
14
+ visible,
15
+ onClose,
16
+ onBack,
17
+ onMinimize,
18
+ enableSharedModalDimensions = false,
19
+ initialModalState,
20
+ minimizeTargetPosition
21
+ }) {
22
+ console.log("[ImageOverlay] render", {
23
+ visible,
24
+ initialModalState: initialModalState ? JSON.stringify(initialModalState).slice(0, 200) : null,
25
+ minimizeTargetPosition
26
+ });
27
+ const [view, setView] = useState(() => {
28
+ const s = ImageOverlayController.getState();
29
+ if (s.mode === "free") return "free";
30
+ if (s.targetRect) return "controls";
31
+ return "idle";
32
+ });
33
+ const [targets, setTargets] = useState([]);
34
+ const [state, setState] = useState(() => ImageOverlayController.getState());
35
+ const [imageUrl, setImageUrl] = useState(persistedImageUrl);
36
+ const [autoTrack, setAutoTrack] = useState(() => ImageOverlayController.isAutoTracking());
37
+ useEffect(() => {
38
+ persistedImageUrl = imageUrl;
39
+ }, [imageUrl]);
40
+ useEffect(() => {
41
+ return ImageOverlayController.subscribe(setState);
42
+ }, []);
43
+
44
+ // Sync auto-track toggle with controller
45
+ useEffect(() => {
46
+ ImageOverlayController.setAutoTrack(autoTrack);
47
+ return () => {
48
+ // Don't stop tracking on unmount — let it persist while modal is minimized
49
+ };
50
+ }, [autoTrack]);
51
+ const handleScan = useCallback(() => {
52
+ const found = scanForImageTargets();
53
+ setTargets(found);
54
+ setView("targets");
55
+ }, []);
56
+ const handleSelectTarget = useCallback(async target => {
57
+ await ImageOverlayController.setTarget(target.instance, target.label, target.fiber);
58
+ if (persistedImageUrl.trim()) {
59
+ await ImageOverlayController.setImageUri(persistedImageUrl.trim());
60
+ ImageOverlayController.setEnabled(true);
61
+ }
62
+ setView("controls");
63
+ }, []);
64
+ const handleLoadImage = useCallback(async () => {
65
+ const url = persistedImageUrl.trim();
66
+ if (!url) return;
67
+ if (view === "free") {
68
+ await ImageOverlayController.startFreeMode(url);
69
+ } else {
70
+ await ImageOverlayController.setImageUri(url);
71
+ ImageOverlayController.setEnabled(true);
72
+ }
73
+ }, [view]);
74
+ const handlePasteImage = useCallback(async () => {
75
+ // Try expo-clipboard (supports images)
76
+ let expoClipboard = null;
77
+ try {
78
+ expoClipboard = require("expo-clipboard");
79
+ } catch {/* not installed */}
80
+
81
+ // Try @react-native-clipboard/clipboard (text only)
82
+ let rnClipboard = null;
83
+ try {
84
+ const mod = require("@react-native-clipboard/clipboard");
85
+ rnClipboard = mod.default || mod;
86
+ } catch {/* not installed */}
87
+
88
+ // No clipboard library at all
89
+ if (!expoClipboard && !rnClipboard) {
90
+ Alert.alert("Clipboard not available", "Install expo-clipboard or @react-native-clipboard/clipboard to use paste.");
91
+ return;
92
+ }
93
+ try {
94
+ // 1. Try image paste (expo-clipboard only)
95
+ if (expoClipboard && typeof expoClipboard.hasImageAsync === "function") {
96
+ const hasImage = await expoClipboard.hasImageAsync();
97
+ if (hasImage) {
98
+ const image = await expoClipboard.getImageAsync({
99
+ format: "png"
100
+ });
101
+ if (image?.data) {
102
+ if (view === "free") {
103
+ await ImageOverlayController.startFreeMode(image.data);
104
+ } else {
105
+ await ImageOverlayController.setImageUri(image.data);
106
+ ImageOverlayController.setEnabled(true);
107
+ }
108
+ return;
109
+ }
110
+ }
111
+ }
112
+
113
+ // 2. Try text paste as URL (either library)
114
+ let text = null;
115
+ if (expoClipboard && typeof expoClipboard.getStringAsync === "function") {
116
+ text = await expoClipboard.getStringAsync();
117
+ } else if (rnClipboard && typeof rnClipboard.getString === "function") {
118
+ text = await rnClipboard.getString();
119
+ }
120
+ if (text && (text.startsWith("http") || text.startsWith("data:"))) {
121
+ setImageUrl(text);
122
+ persistedImageUrl = text;
123
+ return;
124
+ }
125
+
126
+ // 3. Nothing useful found
127
+ if (!expoClipboard) {
128
+ // RN clipboard can't read images — tell them why
129
+ Alert.alert("Image paste requires expo-clipboard", "Your project uses @react-native-clipboard/clipboard which only supports text. Install expo-clipboard to paste images directly, or paste an image URL instead.");
130
+ } else {
131
+ Alert.alert("No image detected", "Try saving your design image, opening it in Photos or Preview, and copying it from there.");
132
+ }
133
+ } catch {
134
+ Alert.alert("Paste failed", "Something went wrong reading the clipboard. Try copying the image again.");
135
+ }
136
+ }, [view]);
137
+ const handleReset = useCallback(() => {
138
+ ImageOverlayController.reset();
139
+ persistedImageUrl = "";
140
+ setView("idle");
141
+ setImageUrl("");
142
+ setTargets([]);
143
+ }, []);
144
+ const handleBack = useCallback(() => {
145
+ if (view === "controls") {
146
+ const found = scanForImageTargets();
147
+ setTargets(found);
148
+ setView("targets");
149
+ } else if (view === "targets" || view === "free") {
150
+ ImageOverlayController.reset();
151
+ setView("idle");
152
+ } else {
153
+ setView("idle");
154
+ }
155
+ }, [view]);
156
+ const headerTitle = (() => {
157
+ switch (view) {
158
+ case "targets":
159
+ return `Targets (${targets.length})`;
160
+ case "controls":
161
+ return state.targetLabel || "Component Match";
162
+ case "free":
163
+ return "Free Placement";
164
+ default:
165
+ return "Image Overlay";
166
+ }
167
+ })();
168
+ const headerContent = {
169
+ showToggleButton: false,
170
+ customContent: /*#__PURE__*/_jsxs(ModalHeader, {
171
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
172
+ onBack: view !== "idle" ? handleBack : onBack
173
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
174
+ title: headerTitle
175
+ }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
176
+ children: [view === "targets" && /*#__PURE__*/_jsx(TouchableOpacity, {
177
+ style: styles.headerTextButton,
178
+ onPress: handleScan,
179
+ activeOpacity: 0.7,
180
+ children: /*#__PURE__*/_jsx(Text, {
181
+ style: styles.headerTextButtonLabel,
182
+ children: "Scan"
183
+ })
184
+ }), (view === "controls" || view === "free") && state.imageUri && /*#__PURE__*/_jsx(PowerToggleButton, {
185
+ isEnabled: state.enabled,
186
+ onToggle: () => ImageOverlayController.toggle(),
187
+ accessibilityLabel: "Toggle image overlay"
188
+ }), view !== "idle" && /*#__PURE__*/_jsx(TouchableOpacity, {
189
+ style: styles.headerActionButton,
190
+ onPress: handleReset,
191
+ activeOpacity: 0.7,
192
+ children: /*#__PURE__*/_jsx(Trash2, {
193
+ size: 14,
194
+ color: macOSColors.text.secondary
195
+ })
196
+ })]
197
+ })]
198
+ })
199
+ };
200
+ return /*#__PURE__*/_jsx(JsModal, {
201
+ visible: visible,
202
+ onClose: onClose,
203
+ onMinimize: onMinimize,
204
+ minimizeTargetPosition: minimizeTargetPosition,
205
+ initialModalState: initialModalState,
206
+ header: headerContent,
207
+ initialMode: "bottomSheet",
208
+ persistenceKey: "buoy-image-overlay-modal",
209
+ enablePersistence: true,
210
+ children: /*#__PURE__*/_jsxs(View, {
211
+ style: styles.container,
212
+ children: [view === "idle" && /*#__PURE__*/_jsxs(View, {
213
+ style: styles.homeContainer,
214
+ children: [/*#__PURE__*/_jsx(Text, {
215
+ style: styles.homeSubtitle,
216
+ children: "Choose a mode"
217
+ }), /*#__PURE__*/_jsxs(View, {
218
+ style: styles.modeGrid,
219
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
220
+ style: styles.modePanel,
221
+ onPress: handleScan,
222
+ activeOpacity: 0.7,
223
+ children: [/*#__PURE__*/_jsx(View, {
224
+ style: [styles.modeIconCircle, {
225
+ backgroundColor: macOSColors.semantic.success + "18"
226
+ }],
227
+ children: /*#__PURE__*/_jsx(SquareDashed, {
228
+ size: 24,
229
+ color: macOSColors.semantic.success
230
+ })
231
+ }), /*#__PURE__*/_jsxs(Text, {
232
+ style: styles.modePanelTitle,
233
+ children: ["Component", "\n", "Match"]
234
+ }), /*#__PURE__*/_jsx(Text, {
235
+ style: styles.modePanelDesc,
236
+ children: "Overlay on a tagged component"
237
+ })]
238
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
239
+ style: styles.modePanel,
240
+ onPress: () => setView("free"),
241
+ activeOpacity: 0.7,
242
+ children: [/*#__PURE__*/_jsx(View, {
243
+ style: [styles.modeIconCircle, {
244
+ backgroundColor: buoyColors.primary + "18"
245
+ }],
246
+ children: /*#__PURE__*/_jsx(Image, {
247
+ size: 24,
248
+ color: buoyColors.primary
249
+ })
250
+ }), /*#__PURE__*/_jsxs(Text, {
251
+ style: styles.modePanelTitle,
252
+ children: ["Free", "\n", "Placement"]
253
+ }), /*#__PURE__*/_jsx(Text, {
254
+ style: styles.modePanelDesc,
255
+ children: "Drag and resize anywhere"
256
+ })]
257
+ })]
258
+ })]
259
+ }), view === "targets" && /*#__PURE__*/_jsx(TargetList, {
260
+ targets: targets,
261
+ onSelect: handleSelectTarget
262
+ }), view === "controls" && /*#__PURE__*/_jsxs(View, {
263
+ style: styles.controlsContainer,
264
+ children: [state.targetRect && /*#__PURE__*/_jsxs(View, {
265
+ style: styles.infoCard,
266
+ children: [/*#__PURE__*/_jsxs(View, {
267
+ style: styles.infoRow,
268
+ children: [/*#__PURE__*/_jsx(Text, {
269
+ style: styles.infoLabel,
270
+ children: "Component"
271
+ }), /*#__PURE__*/_jsx(Text, {
272
+ style: styles.infoValue,
273
+ children: state.targetLabel
274
+ })]
275
+ }), /*#__PURE__*/_jsxs(View, {
276
+ style: styles.infoRow,
277
+ children: [/*#__PURE__*/_jsx(Text, {
278
+ style: styles.infoLabel,
279
+ children: "Size"
280
+ }), /*#__PURE__*/_jsxs(Text, {
281
+ style: styles.infoValueMono,
282
+ children: [Math.round(state.targetRect.width), " x ", Math.round(state.targetRect.height)]
283
+ })]
284
+ }), /*#__PURE__*/_jsxs(View, {
285
+ style: styles.infoRow,
286
+ children: [/*#__PURE__*/_jsx(Text, {
287
+ style: styles.infoLabel,
288
+ children: "Position"
289
+ }), /*#__PURE__*/_jsxs(Text, {
290
+ style: styles.infoValueMono,
291
+ children: [Math.round(state.targetRect.x), ", ", Math.round(state.targetRect.y)]
292
+ })]
293
+ }), state.imageWidth != null && state.imageHeight != null && /*#__PURE__*/_jsxs(View, {
294
+ style: styles.infoRow,
295
+ children: [/*#__PURE__*/_jsx(Text, {
296
+ style: styles.infoLabel,
297
+ children: "Image"
298
+ }), /*#__PURE__*/_jsxs(Text, {
299
+ style: styles.infoValueMono,
300
+ children: [state.imageWidth, " x ", state.imageHeight]
301
+ })]
302
+ })]
303
+ }), /*#__PURE__*/_jsxs(View, {
304
+ style: styles.inputSection,
305
+ children: [/*#__PURE__*/_jsx(Text, {
306
+ style: styles.sectionLabel,
307
+ children: "Image"
308
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
309
+ style: styles.pasteButton,
310
+ onPress: handlePasteImage,
311
+ activeOpacity: 0.7,
312
+ children: /*#__PURE__*/_jsx(Text, {
313
+ style: styles.pasteButtonText,
314
+ children: "Paste from Clipboard"
315
+ })
316
+ }), /*#__PURE__*/_jsxs(View, {
317
+ style: styles.urlRow,
318
+ children: [/*#__PURE__*/_jsx(TextInput, {
319
+ style: styles.urlInput,
320
+ value: imageUrl,
321
+ onChangeText: setImageUrl,
322
+ placeholder: "or enter URL...",
323
+ placeholderTextColor: macOSColors.text.muted,
324
+ autoCapitalize: "none",
325
+ autoCorrect: false
326
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
327
+ style: [styles.loadButton, !imageUrl.trim() && styles.loadButtonDisabled],
328
+ onPress: handleLoadImage,
329
+ disabled: !imageUrl.trim(),
330
+ activeOpacity: 0.7,
331
+ children: /*#__PURE__*/_jsx(Text, {
332
+ style: styles.loadButtonText,
333
+ children: "Load"
334
+ })
335
+ })]
336
+ })]
337
+ }), /*#__PURE__*/_jsxs(View, {
338
+ style: styles.toggleSection,
339
+ children: [/*#__PURE__*/_jsxs(View, {
340
+ style: styles.toggleRow,
341
+ children: [/*#__PURE__*/_jsx(Text, {
342
+ style: styles.toggleLabel,
343
+ children: "Show Outline"
344
+ }), /*#__PURE__*/_jsx(Switch, {
345
+ value: state.showOutline,
346
+ onValueChange: v => ImageOverlayController.setShowOutline(v),
347
+ trackColor: {
348
+ false: macOSColors.background.hover,
349
+ true: buoyColors.primary
350
+ }
351
+ })]
352
+ }), /*#__PURE__*/_jsxs(View, {
353
+ style: styles.toggleRow,
354
+ children: [/*#__PURE__*/_jsx(Text, {
355
+ style: styles.toggleLabel,
356
+ children: "Auto Track"
357
+ }), /*#__PURE__*/_jsx(Switch, {
358
+ value: autoTrack,
359
+ onValueChange: setAutoTrack,
360
+ trackColor: {
361
+ false: macOSColors.background.hover,
362
+ true: macOSColors.semantic.success
363
+ }
364
+ })]
365
+ }), state.imageUri && /*#__PURE__*/_jsxs(_Fragment, {
366
+ children: [/*#__PURE__*/_jsxs(View, {
367
+ style: styles.blendRow,
368
+ children: [/*#__PURE__*/_jsx(Text, {
369
+ style: styles.toggleLabel,
370
+ children: "Opacity"
371
+ }), /*#__PURE__*/_jsx(View, {
372
+ style: styles.blendButtons,
373
+ children: [0, 25, 50, 75, 100].map(pct => /*#__PURE__*/_jsx(TouchableOpacity, {
374
+ style: [styles.blendButton, Math.round(state.opacity * 100) === pct && styles.blendButtonActive],
375
+ onPress: () => ImageOverlayController.setOpacity(pct / 100),
376
+ activeOpacity: 0.7,
377
+ children: /*#__PURE__*/_jsxs(Text, {
378
+ style: [styles.blendButtonText, Math.round(state.opacity * 100) === pct && styles.blendButtonTextActive],
379
+ children: [pct, "%"]
380
+ })
381
+ }, pct))
382
+ })]
383
+ }), /*#__PURE__*/_jsxs(View, {
384
+ style: styles.blendRow,
385
+ children: [/*#__PURE__*/_jsx(Text, {
386
+ style: styles.toggleLabel,
387
+ children: "Flip"
388
+ }), /*#__PURE__*/_jsxs(View, {
389
+ style: styles.blendButtons,
390
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
391
+ style: [styles.blendButton, state.invertX && styles.blendButtonActive],
392
+ onPress: () => ImageOverlayController.toggleInvertX(),
393
+ activeOpacity: 0.7,
394
+ children: /*#__PURE__*/_jsx(Text, {
395
+ style: [styles.blendButtonText, state.invertX && styles.blendButtonTextActive],
396
+ children: "Horizontal"
397
+ })
398
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
399
+ style: [styles.blendButton, state.invertY && styles.blendButtonActive],
400
+ onPress: () => ImageOverlayController.toggleInvertY(),
401
+ activeOpacity: 0.7,
402
+ children: /*#__PURE__*/_jsx(Text, {
403
+ style: [styles.blendButtonText, state.invertY && styles.blendButtonTextActive],
404
+ children: "Vertical"
405
+ })
406
+ })]
407
+ })]
408
+ })]
409
+ })]
410
+ }), state.imageUri && /*#__PURE__*/_jsx(OverlayControls, {
411
+ opacity: state.opacity,
412
+ scale: state.scale,
413
+ offsetX: state.offsetX,
414
+ offsetY: state.offsetY,
415
+ onOpacityChange: v => ImageOverlayController.setOpacity(v),
416
+ onScaleChange: v => ImageOverlayController.setScale(v),
417
+ onOffsetChange: (x, y) => ImageOverlayController.setOffset(x, y)
418
+ }), /*#__PURE__*/_jsxs(View, {
419
+ style: styles.actions,
420
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
421
+ style: styles.actionButton,
422
+ onPress: () => ImageOverlayController.resetSettings(),
423
+ activeOpacity: 0.7,
424
+ children: /*#__PURE__*/_jsx(Text, {
425
+ style: styles.actionButtonText,
426
+ children: "Reset Settings"
427
+ })
428
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
429
+ style: styles.actionButton,
430
+ onPress: () => ImageOverlayController.remeasure(),
431
+ activeOpacity: 0.7,
432
+ children: /*#__PURE__*/_jsx(Text, {
433
+ style: styles.actionButtonText,
434
+ children: "Remeasure"
435
+ })
436
+ })]
437
+ })]
438
+ }), view === "free" && /*#__PURE__*/_jsxs(View, {
439
+ style: styles.controlsContainer,
440
+ children: [/*#__PURE__*/_jsxs(View, {
441
+ style: styles.inputSection,
442
+ children: [/*#__PURE__*/_jsx(Text, {
443
+ style: styles.sectionLabel,
444
+ children: "Image"
445
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
446
+ style: styles.pasteButton,
447
+ onPress: handlePasteImage,
448
+ activeOpacity: 0.7,
449
+ children: /*#__PURE__*/_jsx(Text, {
450
+ style: styles.pasteButtonText,
451
+ children: "Paste from Clipboard"
452
+ })
453
+ }), /*#__PURE__*/_jsxs(View, {
454
+ style: styles.urlRow,
455
+ children: [/*#__PURE__*/_jsx(TextInput, {
456
+ style: styles.urlInput,
457
+ value: imageUrl,
458
+ onChangeText: setImageUrl,
459
+ placeholder: "or enter URL...",
460
+ placeholderTextColor: macOSColors.text.muted,
461
+ autoCapitalize: "none",
462
+ autoCorrect: false
463
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
464
+ style: [styles.loadButton, !imageUrl.trim() && styles.loadButtonDisabled],
465
+ onPress: handleLoadImage,
466
+ disabled: !imageUrl.trim(),
467
+ activeOpacity: 0.7,
468
+ children: /*#__PURE__*/_jsx(Text, {
469
+ style: styles.loadButtonText,
470
+ children: "Load"
471
+ })
472
+ })]
473
+ })]
474
+ }), state.imageUri && state.mode === "free" && /*#__PURE__*/_jsxs(_Fragment, {
475
+ children: [/*#__PURE__*/_jsxs(View, {
476
+ style: styles.infoCard,
477
+ children: [/*#__PURE__*/_jsxs(View, {
478
+ style: styles.infoRow,
479
+ children: [/*#__PURE__*/_jsx(Text, {
480
+ style: styles.infoLabel,
481
+ children: "Position"
482
+ }), /*#__PURE__*/_jsxs(Text, {
483
+ style: styles.infoValueMono,
484
+ children: [Math.round(state.freeX), ", ", Math.round(state.freeY)]
485
+ })]
486
+ }), /*#__PURE__*/_jsxs(View, {
487
+ style: styles.infoRow,
488
+ children: [/*#__PURE__*/_jsx(Text, {
489
+ style: styles.infoLabel,
490
+ children: "Size"
491
+ }), /*#__PURE__*/_jsxs(Text, {
492
+ style: styles.infoValueMono,
493
+ children: [Math.round(state.freeWidth), " x ", Math.round(state.freeHeight)]
494
+ })]
495
+ }), state.imageWidth != null && state.imageHeight != null && /*#__PURE__*/_jsxs(View, {
496
+ style: styles.infoRow,
497
+ children: [/*#__PURE__*/_jsx(Text, {
498
+ style: styles.infoLabel,
499
+ children: "Original"
500
+ }), /*#__PURE__*/_jsxs(Text, {
501
+ style: styles.infoValueMono,
502
+ children: [state.imageWidth, " x ", state.imageHeight]
503
+ })]
504
+ })]
505
+ }), /*#__PURE__*/_jsx(View, {
506
+ style: styles.inputSection,
507
+ children: /*#__PURE__*/_jsx(OverlayControls, {
508
+ opacity: state.opacity,
509
+ scale: 1,
510
+ offsetX: 0,
511
+ offsetY: 0,
512
+ onOpacityChange: v => ImageOverlayController.setOpacity(v),
513
+ onScaleChange: () => {},
514
+ onOffsetChange: () => {},
515
+ opacityOnly: true
516
+ })
517
+ }), /*#__PURE__*/_jsxs(View, {
518
+ style: styles.toggleSection,
519
+ children: [/*#__PURE__*/_jsxs(View, {
520
+ style: styles.toggleRow,
521
+ children: [/*#__PURE__*/_jsx(Text, {
522
+ style: styles.toggleLabel,
523
+ children: "Lock Position"
524
+ }), /*#__PURE__*/_jsx(Switch, {
525
+ value: state.locked,
526
+ onValueChange: v => ImageOverlayController.setLocked(v),
527
+ trackColor: {
528
+ false: macOSColors.background.hover,
529
+ true: "#f59e0b"
530
+ }
531
+ })]
532
+ }), /*#__PURE__*/_jsxs(View, {
533
+ style: styles.blendRow,
534
+ children: [/*#__PURE__*/_jsx(Text, {
535
+ style: styles.toggleLabel,
536
+ children: "Opacity"
537
+ }), /*#__PURE__*/_jsx(View, {
538
+ style: styles.blendButtons,
539
+ children: [0, 25, 50, 75, 100].map(pct => /*#__PURE__*/_jsx(TouchableOpacity, {
540
+ style: [styles.blendButton, Math.round(state.opacity * 100) === pct && styles.blendButtonActive],
541
+ onPress: () => ImageOverlayController.setOpacity(pct / 100),
542
+ activeOpacity: 0.7,
543
+ children: /*#__PURE__*/_jsxs(Text, {
544
+ style: [styles.blendButtonText, Math.round(state.opacity * 100) === pct && styles.blendButtonTextActive],
545
+ children: [pct, "%"]
546
+ })
547
+ }, pct))
548
+ })]
549
+ }), /*#__PURE__*/_jsxs(View, {
550
+ style: styles.blendRow,
551
+ children: [/*#__PURE__*/_jsx(Text, {
552
+ style: styles.toggleLabel,
553
+ children: "Flip"
554
+ }), /*#__PURE__*/_jsxs(View, {
555
+ style: styles.blendButtons,
556
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
557
+ style: [styles.blendButton, state.invertX && styles.blendButtonActive],
558
+ onPress: () => ImageOverlayController.toggleInvertX(),
559
+ activeOpacity: 0.7,
560
+ children: /*#__PURE__*/_jsx(Text, {
561
+ style: [styles.blendButtonText, state.invertX && styles.blendButtonTextActive],
562
+ children: "Horizontal"
563
+ })
564
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
565
+ style: [styles.blendButton, state.invertY && styles.blendButtonActive],
566
+ onPress: () => ImageOverlayController.toggleInvertY(),
567
+ activeOpacity: 0.7,
568
+ children: /*#__PURE__*/_jsx(Text, {
569
+ style: [styles.blendButtonText, state.invertY && styles.blendButtonTextActive],
570
+ children: "Vertical"
571
+ })
572
+ })]
573
+ })]
574
+ })]
575
+ }), /*#__PURE__*/_jsx(View, {
576
+ style: styles.actions,
577
+ children: /*#__PURE__*/_jsx(TouchableOpacity, {
578
+ style: styles.actionButton,
579
+ onPress: () => ImageOverlayController.resetSettings(),
580
+ activeOpacity: 0.7,
581
+ children: /*#__PURE__*/_jsx(Text, {
582
+ style: styles.actionButtonText,
583
+ children: "Reset Position"
584
+ })
585
+ })
586
+ })]
587
+ })]
588
+ })]
589
+ })
590
+ });
591
+ }
592
+ const styles = StyleSheet.create({
593
+ container: {
594
+ flex: 1
595
+ },
596
+ headerTextButton: {
597
+ height: 32,
598
+ paddingHorizontal: 12,
599
+ borderRadius: 8,
600
+ backgroundColor: macOSColors.background.hover,
601
+ borderWidth: 1,
602
+ borderColor: macOSColors.border.default,
603
+ alignItems: "center",
604
+ justifyContent: "center"
605
+ },
606
+ headerTextButtonLabel: {
607
+ fontSize: 12,
608
+ fontWeight: "600",
609
+ color: buoyColors.primary
610
+ },
611
+ headerActionButton: {
612
+ width: 32,
613
+ height: 32,
614
+ borderRadius: 8,
615
+ backgroundColor: macOSColors.background.hover,
616
+ borderWidth: 1,
617
+ borderColor: macOSColors.border.default,
618
+ alignItems: "center",
619
+ justifyContent: "center"
620
+ },
621
+ // Home — mode selection
622
+ homeContainer: {
623
+ flex: 1,
624
+ paddingHorizontal: 12,
625
+ paddingTop: 16,
626
+ paddingBottom: 20,
627
+ gap: 12
628
+ },
629
+ homeSubtitle: {
630
+ fontSize: 12,
631
+ fontWeight: "600",
632
+ color: macOSColors.text.muted,
633
+ textTransform: "uppercase",
634
+ letterSpacing: 0.5,
635
+ textAlign: "center"
636
+ },
637
+ modeGrid: {
638
+ flexDirection: "row",
639
+ gap: 10
640
+ },
641
+ modePanel: {
642
+ flex: 1,
643
+ alignItems: "center",
644
+ paddingVertical: 20,
645
+ paddingHorizontal: 12,
646
+ backgroundColor: macOSColors.background.hover,
647
+ borderRadius: 12,
648
+ borderWidth: 1,
649
+ borderColor: macOSColors.border.default,
650
+ gap: 8
651
+ },
652
+ modeIconCircle: {
653
+ width: 48,
654
+ height: 48,
655
+ borderRadius: 24,
656
+ alignItems: "center",
657
+ justifyContent: "center",
658
+ marginBottom: 4
659
+ },
660
+ modePanelTitle: {
661
+ fontSize: 13,
662
+ fontWeight: "700",
663
+ color: macOSColors.text.primary,
664
+ textAlign: "center",
665
+ lineHeight: 17
666
+ },
667
+ modePanelDesc: {
668
+ fontSize: 11,
669
+ color: macOSColors.text.muted,
670
+ textAlign: "center",
671
+ lineHeight: 15
672
+ },
673
+ // Controls
674
+ controlsContainer: {
675
+ flex: 1,
676
+ paddingTop: 8,
677
+ paddingBottom: 24,
678
+ gap: 10
679
+ },
680
+ infoCard: {
681
+ marginHorizontal: 12,
682
+ backgroundColor: macOSColors.background.hover,
683
+ borderRadius: 8,
684
+ borderWidth: 1,
685
+ borderColor: macOSColors.border.default,
686
+ padding: 10,
687
+ gap: 4
688
+ },
689
+ infoRow: {
690
+ flexDirection: "row",
691
+ justifyContent: "space-between",
692
+ alignItems: "center"
693
+ },
694
+ infoLabel: {
695
+ fontSize: 11,
696
+ fontWeight: "500",
697
+ color: macOSColors.text.muted,
698
+ textTransform: "uppercase",
699
+ letterSpacing: 0.3
700
+ },
701
+ infoValue: {
702
+ fontSize: 12,
703
+ fontWeight: "600",
704
+ color: macOSColors.text.primary
705
+ },
706
+ infoValueMono: {
707
+ fontSize: 12,
708
+ fontWeight: "600",
709
+ color: macOSColors.text.primary,
710
+ fontFamily: "monospace"
711
+ },
712
+ inputSection: {
713
+ paddingHorizontal: 12,
714
+ gap: 6
715
+ },
716
+ sectionLabel: {
717
+ fontSize: 11,
718
+ fontWeight: "600",
719
+ color: macOSColors.text.muted,
720
+ textTransform: "uppercase",
721
+ letterSpacing: 0.5
722
+ },
723
+ pasteButton: {
724
+ paddingVertical: 10,
725
+ borderRadius: 8,
726
+ backgroundColor: buoyColors.primary + "15",
727
+ borderWidth: 1,
728
+ borderColor: buoyColors.primary + "40",
729
+ alignItems: "center"
730
+ },
731
+ pasteButtonText: {
732
+ fontSize: 13,
733
+ fontWeight: "600",
734
+ color: buoyColors.primary
735
+ },
736
+ urlRow: {
737
+ flexDirection: "row",
738
+ gap: 8
739
+ },
740
+ urlInput: {
741
+ flex: 1,
742
+ height: 36,
743
+ borderRadius: 10,
744
+ backgroundColor: macOSColors.background.input,
745
+ borderWidth: 1,
746
+ borderColor: macOSColors.border.default,
747
+ paddingHorizontal: 12,
748
+ fontSize: 13,
749
+ color: macOSColors.text.primary
750
+ },
751
+ loadButton: {
752
+ height: 36,
753
+ paddingHorizontal: 14,
754
+ borderRadius: 8,
755
+ backgroundColor: buoyColors.primary,
756
+ justifyContent: "center",
757
+ alignItems: "center"
758
+ },
759
+ loadButtonDisabled: {
760
+ opacity: 0.4
761
+ },
762
+ loadButtonText: {
763
+ fontSize: 13,
764
+ fontWeight: "600",
765
+ color: "#fff"
766
+ },
767
+ toggleSection: {
768
+ paddingHorizontal: 12,
769
+ gap: 6
770
+ },
771
+ toggleRow: {
772
+ flexDirection: "row",
773
+ alignItems: "center",
774
+ justifyContent: "space-between",
775
+ backgroundColor: macOSColors.background.hover,
776
+ borderRadius: 8,
777
+ borderWidth: 1,
778
+ borderColor: macOSColors.border.default,
779
+ paddingHorizontal: 12,
780
+ paddingVertical: 8
781
+ },
782
+ toggleLabel: {
783
+ fontSize: 13,
784
+ fontWeight: "500",
785
+ color: macOSColors.text.secondary
786
+ },
787
+ blendRow: {
788
+ flexDirection: "row",
789
+ alignItems: "center",
790
+ justifyContent: "space-between",
791
+ backgroundColor: macOSColors.background.hover,
792
+ borderRadius: 8,
793
+ borderWidth: 1,
794
+ borderColor: macOSColors.border.default,
795
+ paddingHorizontal: 12,
796
+ paddingVertical: 6
797
+ },
798
+ blendButtons: {
799
+ flexDirection: "row",
800
+ gap: 4
801
+ },
802
+ blendButton: {
803
+ paddingHorizontal: 10,
804
+ paddingVertical: 5,
805
+ borderRadius: 6,
806
+ backgroundColor: "transparent"
807
+ },
808
+ blendButtonActive: {
809
+ backgroundColor: buoyColors.primary + "20"
810
+ },
811
+ blendButtonText: {
812
+ fontSize: 12,
813
+ fontWeight: "600",
814
+ color: macOSColors.text.muted
815
+ },
816
+ blendButtonTextActive: {
817
+ color: buoyColors.primary
818
+ },
819
+ actions: {
820
+ flexDirection: "row",
821
+ paddingHorizontal: 12,
822
+ gap: 8,
823
+ marginTop: 4
824
+ },
825
+ actionButton: {
826
+ flex: 1,
827
+ flexDirection: "row",
828
+ alignItems: "center",
829
+ justifyContent: "center",
830
+ gap: 6,
831
+ paddingVertical: 8,
832
+ paddingHorizontal: 12,
833
+ borderRadius: 8,
834
+ backgroundColor: macOSColors.background.hover,
835
+ borderWidth: 1,
836
+ borderColor: macOSColors.border.default
837
+ },
838
+ actionButtonText: {
839
+ fontSize: 12,
840
+ fontWeight: "500",
841
+ color: macOSColors.text.secondary
842
+ }
843
+ });