sonance-brand-mcp 1.3.110 → 1.3.112

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 (84) hide show
  1. package/dist/assets/api/sonance-ai-edit/route.ts +30 -7
  2. package/dist/assets/api/sonance-save-image/route.ts +625 -0
  3. package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
  4. package/dist/assets/api/sonance-vision-apply/route.ts +1020 -64
  5. package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
  6. package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
  7. package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
  8. package/dist/assets/brand-system.ts +13 -12
  9. package/dist/assets/components/accordion.tsx +15 -7
  10. package/dist/assets/components/alert-dialog.tsx +35 -10
  11. package/dist/assets/components/alert.tsx +11 -10
  12. package/dist/assets/components/avatar.tsx +4 -4
  13. package/dist/assets/components/badge.tsx +16 -12
  14. package/dist/assets/components/button.stories.tsx +3 -3
  15. package/dist/assets/components/button.tsx +50 -31
  16. package/dist/assets/components/calendar.tsx +12 -8
  17. package/dist/assets/components/card.tsx +35 -29
  18. package/dist/assets/components/checkbox.tsx +9 -8
  19. package/dist/assets/components/code.tsx +19 -11
  20. package/dist/assets/components/command.tsx +32 -13
  21. package/dist/assets/components/context-menu.tsx +37 -16
  22. package/dist/assets/components/dialog.tsx +8 -5
  23. package/dist/assets/components/divider.tsx +15 -5
  24. package/dist/assets/components/drawer.tsx +4 -3
  25. package/dist/assets/components/dropdown-menu.tsx +15 -13
  26. package/dist/assets/components/hover-card.tsx +4 -1
  27. package/dist/assets/components/image.tsx +1 -1
  28. package/dist/assets/components/input.tsx +29 -14
  29. package/dist/assets/components/kbd.stories.tsx +3 -3
  30. package/dist/assets/components/kbd.tsx +29 -13
  31. package/dist/assets/components/listbox.tsx +8 -8
  32. package/dist/assets/components/menubar.tsx +50 -23
  33. package/dist/assets/components/navbar.stories.tsx +140 -13
  34. package/dist/assets/components/navbar.tsx +22 -5
  35. package/dist/assets/components/navigation-menu.tsx +28 -6
  36. package/dist/assets/components/pagination.tsx +10 -10
  37. package/dist/assets/components/popover.tsx +10 -8
  38. package/dist/assets/components/progress.tsx +6 -4
  39. package/dist/assets/components/radio-group.tsx +5 -5
  40. package/dist/assets/components/select.tsx +49 -29
  41. package/dist/assets/components/separator.tsx +3 -3
  42. package/dist/assets/components/sheet.tsx +4 -4
  43. package/dist/assets/components/sidebar.tsx +10 -10
  44. package/dist/assets/components/skeleton.tsx +13 -5
  45. package/dist/assets/components/slider.tsx +12 -10
  46. package/dist/assets/components/switch.tsx +4 -4
  47. package/dist/assets/components/table.tsx +5 -5
  48. package/dist/assets/components/tabs.tsx +8 -8
  49. package/dist/assets/components/textarea.tsx +11 -9
  50. package/dist/assets/components/toast.tsx +7 -7
  51. package/dist/assets/components/toggle.tsx +27 -7
  52. package/dist/assets/components/tooltip.tsx +10 -8
  53. package/dist/assets/components/user.tsx +8 -6
  54. package/dist/assets/dev-tools/SonanceDevTools.tsx +851 -708
  55. package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
  56. package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
  57. package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
  58. package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
  59. package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
  60. package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
  61. package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
  62. package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
  63. package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
  64. package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
  65. package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
  66. package/dist/assets/dev-tools/components/VisionModeBorder.tsx +12 -63
  67. package/dist/assets/dev-tools/constants.ts +38 -6
  68. package/dist/assets/dev-tools/hooks/index.ts +69 -0
  69. package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
  70. package/dist/assets/dev-tools/hooks/useComputedStyles.ts +471 -0
  71. package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
  72. package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
  73. package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
  74. package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
  75. package/dist/assets/dev-tools/index.ts +3 -0
  76. package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
  77. package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
  78. package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
  79. package/dist/assets/dev-tools/types.ts +93 -2
  80. package/dist/assets/globals.css +225 -9
  81. package/dist/assets/styles/brand-overrides.css +3 -2
  82. package/dist/assets/utils.ts +2 -1
  83. package/dist/index.js +22 -3
  84. package/package.json +2 -1
@@ -0,0 +1,398 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react";
4
+ import { DetectedElement, ElementFilters, OriginalLogoState, OriginalTextState } from "../types";
5
+ import { getActiveModalContent } from "../utils";
6
+ import { detectComponents } from "./useComponentDetection";
7
+ import { detectImages, extractLogoName } from "./useImageDetection";
8
+ import { detectTextElements } from "./useTextDetection";
9
+ import {
10
+ migrateLegacyIds,
11
+ isMigrationComplete,
12
+ markMigrationComplete,
13
+ } from "./useContentHash";
14
+
15
+ /**
16
+ * Scanner configuration
17
+ */
18
+ export interface ElementScannerOptions {
19
+ /** Whether scanning is enabled */
20
+ enabled: boolean;
21
+ /** Element type filters */
22
+ filters: ElementFilters;
23
+ /** Whether inspector highlighting is active (affects text detection) */
24
+ inspectorEnabled?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Scanner result
29
+ */
30
+ export interface ElementScannerResult {
31
+ /** All detected elements */
32
+ elements: DetectedElement[];
33
+ /** Original logo states (for reset functionality) */
34
+ originalLogoStates: Record<string, OriginalLogoState>;
35
+ /** Original text states (for reset functionality) */
36
+ originalTextStates: Record<string, OriginalTextState>;
37
+ /** Trigger a manual re-scan */
38
+ rescan: () => void;
39
+ /** Reset all original states */
40
+ resetStates: () => void;
41
+ }
42
+
43
+ /**
44
+ * Debounce delay for mutation observer (ms)
45
+ */
46
+ const MUTATION_DEBOUNCE_MS = 100;
47
+
48
+ /**
49
+ * Throttle limit for scans per second
50
+ */
51
+ const MAX_SCANS_PER_SECOND = 5;
52
+
53
+ /**
54
+ * Helper: Check if element is inside a scroll container and clip rect to visible bounds
55
+ */
56
+ function getVisibleRect(el: Element, rect: DOMRect): DOMRect | null {
57
+ const scrollParent = el.closest("aside, nav, [data-sidebar]");
58
+
59
+ if (scrollParent) {
60
+ const parentRect = scrollParent.getBoundingClientRect();
61
+
62
+ // Check if element is fully outside the scroll container
63
+ if (
64
+ rect.bottom < parentRect.top ||
65
+ rect.top > parentRect.bottom ||
66
+ rect.right < parentRect.left ||
67
+ rect.left > parentRect.right
68
+ ) {
69
+ return null;
70
+ }
71
+
72
+ // Clip the rect to parent's visible bounds
73
+ const clippedTop = Math.max(rect.top, parentRect.top);
74
+ const clippedBottom = Math.min(rect.bottom, parentRect.bottom);
75
+ const clippedLeft = Math.max(rect.left, parentRect.left);
76
+ const clippedRight = Math.min(rect.right, parentRect.right);
77
+
78
+ // If clipped area is too small, skip it
79
+ if (clippedBottom - clippedTop < 10 || clippedRight - clippedLeft < 10) {
80
+ return null;
81
+ }
82
+
83
+ return new DOMRect(
84
+ clippedLeft,
85
+ clippedTop,
86
+ clippedRight - clippedLeft,
87
+ clippedBottom - clippedTop
88
+ );
89
+ }
90
+
91
+ return rect;
92
+ }
93
+
94
+ /**
95
+ * Helper: Check if element is in the active layer (not behind a modal)
96
+ */
97
+ function createActiveLayerChecker(): (el: Element) => boolean {
98
+ const activeModalContent = getActiveModalContent();
99
+
100
+ return (el: Element): boolean => {
101
+ // Always exclude DevTools panel
102
+ if (el.closest("[data-sonance-devtools]")) return false;
103
+
104
+ // If a modal is active, only include elements inside the modal
105
+ if (activeModalContent) {
106
+ return activeModalContent.contains(el) || el === activeModalContent;
107
+ }
108
+
109
+ return true;
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Perform a one-time migration of legacy sequential IDs in localStorage
115
+ */
116
+ function performLegacyMigration(): void {
117
+ if (isMigrationComplete()) return;
118
+
119
+ try {
120
+ // Migrate text overrides
121
+ const textOverridesRaw = localStorage.getItem("sonance-text-overrides");
122
+ if (textOverridesRaw) {
123
+ const textOverrides = JSON.parse(textOverridesRaw);
124
+ const { migrated, mappings } = migrateLegacyIds("text", textOverrides);
125
+
126
+ if (mappings.length > 0) {
127
+ console.log("[Sonance] Migrated text element IDs:", mappings);
128
+ localStorage.setItem("sonance-text-overrides", JSON.stringify(migrated));
129
+ }
130
+ }
131
+
132
+ // Migrate logo configs
133
+ const logoConfigsRaw = localStorage.getItem("sonance-logo-configs");
134
+ if (logoConfigsRaw) {
135
+ const logoConfigs = JSON.parse(logoConfigsRaw);
136
+ const { migrated, mappings } = migrateLegacyIds("logo", logoConfigs);
137
+
138
+ if (mappings.length > 0) {
139
+ console.log("[Sonance] Migrated logo element IDs:", mappings);
140
+ localStorage.setItem("sonance-logo-configs", JSON.stringify(migrated));
141
+ }
142
+ }
143
+
144
+ markMigrationComplete();
145
+ } catch (e) {
146
+ console.warn("[Sonance] Migration failed:", e);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Main element scanner hook
152
+ *
153
+ * Uses MutationObserver for efficient change detection instead of
154
+ * continuous requestAnimationFrame polling.
155
+ */
156
+ export function useElementScanner(options: ElementScannerOptions): ElementScannerResult {
157
+ const { enabled, filters, inspectorEnabled = false } = options;
158
+
159
+ // State
160
+ const [elements, setElements] = useState<DetectedElement[]>([]);
161
+
162
+ // Refs for original states (avoid re-renders on state changes)
163
+ const originalLogoStatesRef = useRef<Record<string, OriginalLogoState>>({});
164
+ const originalTextStatesRef = useRef<Record<string, OriginalTextState>>({});
165
+
166
+ // Throttling refs
167
+ const lastScanTimeRef = useRef<number>(0);
168
+ const pendingScanRef = useRef<ReturnType<typeof setTimeout> | null>(null);
169
+ const rafIdRef = useRef<number | null>(null);
170
+
171
+ // Mounted ref for cleanup
172
+ const mountedRef = useRef(true);
173
+
174
+ /**
175
+ * Core scan function - detects all elements based on current filters
176
+ */
177
+ const performScan = useCallback(() => {
178
+ if (!mountedRef.current) return;
179
+
180
+ const now = Date.now();
181
+ const timeSinceLastScan = now - lastScanTimeRef.current;
182
+ const minInterval = 1000 / MAX_SCANS_PER_SECOND;
183
+
184
+ // Throttle if scanning too frequently
185
+ if (timeSinceLastScan < minInterval) {
186
+ if (!pendingScanRef.current) {
187
+ pendingScanRef.current = setTimeout(() => {
188
+ pendingScanRef.current = null;
189
+ performScan();
190
+ }, minInterval - timeSinceLastScan);
191
+ }
192
+ return;
193
+ }
194
+
195
+ lastScanTimeRef.current = now;
196
+
197
+ const allElements: DetectedElement[] = [];
198
+ const isInActiveLayer = createActiveLayerChecker();
199
+
200
+ // Scan components
201
+ if (filters.components) {
202
+ const componentConfig = {
203
+ includeModals: true,
204
+ isInActiveLayer,
205
+ getVisibleRect,
206
+ };
207
+ const componentElements = detectComponents(componentConfig);
208
+ allElements.push(...componentElements);
209
+ }
210
+
211
+ // Scan images/logos
212
+ if (filters.images) {
213
+ const imageConfig = {
214
+ isInActiveLayer,
215
+ existingOriginalStates: originalLogoStatesRef.current,
216
+ };
217
+ const { elements: imageElements, originalStates: newLogoStates } = detectImages(imageConfig);
218
+ allElements.push(...imageElements);
219
+ originalLogoStatesRef.current = newLogoStates;
220
+ }
221
+
222
+ // Scan text elements (only when inspector is enabled)
223
+ if (filters.text && inspectorEnabled) {
224
+ const textConfig = {
225
+ isInActiveLayer,
226
+ existingOriginalStates: originalTextStatesRef.current,
227
+ };
228
+ const { elements: textElements, originalStates: newTextStates } = detectTextElements(textConfig);
229
+ allElements.push(...textElements);
230
+ originalTextStatesRef.current = newTextStates;
231
+ }
232
+
233
+ setElements(allElements);
234
+ }, [filters, inspectorEnabled]);
235
+
236
+ /**
237
+ * Position update function - uses RAF for smooth position tracking
238
+ * Only updates rects, doesn't re-detect elements
239
+ */
240
+ const updatePositions = useCallback(() => {
241
+ if (!mountedRef.current || !enabled) return;
242
+
243
+ setElements((prevElements) => {
244
+ if (prevElements.length === 0) return prevElements;
245
+
246
+ let hasChanges = false;
247
+ const updated = prevElements.map((el) => {
248
+ // Find the actual DOM element
249
+ let domEl: Element | null = null;
250
+
251
+ if (el.type === "logo" && el.logoId) {
252
+ domEl = document.querySelector(`[data-sonance-logo-id="${el.logoId}"]`);
253
+ } else if (el.type === "text" && el.textId) {
254
+ domEl = document.querySelector(`[data-sonance-text-id="${el.textId}"]`);
255
+ } else if (el.type === "component" && el.variantId) {
256
+ domEl = document.querySelector(`[data-sonance-variant="${el.variantId}"]`);
257
+ }
258
+
259
+ if (!domEl) return el;
260
+
261
+ const newRect = domEl.getBoundingClientRect();
262
+ const visibleRect = getVisibleRect(domEl, newRect);
263
+
264
+ if (!visibleRect) {
265
+ // Element is no longer visible
266
+ hasChanges = true;
267
+ return null;
268
+ }
269
+
270
+ // Check if position actually changed
271
+ if (
272
+ Math.abs(el.rect.left - visibleRect.left) > 1 ||
273
+ Math.abs(el.rect.top - visibleRect.top) > 1 ||
274
+ Math.abs(el.rect.width - visibleRect.width) > 1 ||
275
+ Math.abs(el.rect.height - visibleRect.height) > 1
276
+ ) {
277
+ hasChanges = true;
278
+ return { ...el, rect: visibleRect };
279
+ }
280
+
281
+ return el;
282
+ }).filter((el): el is DetectedElement => el !== null);
283
+
284
+ return hasChanges ? updated : prevElements;
285
+ });
286
+ }, [enabled]);
287
+
288
+ /**
289
+ * Rescan trigger (exposed to parent)
290
+ */
291
+ const rescan = useCallback(() => {
292
+ performScan();
293
+ }, [performScan]);
294
+
295
+ /**
296
+ * Reset all original states
297
+ */
298
+ const resetStates = useCallback(() => {
299
+ originalLogoStatesRef.current = {};
300
+ originalTextStatesRef.current = {};
301
+ }, []);
302
+
303
+ // Perform legacy ID migration on mount
304
+ useEffect(() => {
305
+ performLegacyMigration();
306
+ }, []);
307
+
308
+ // Main effect: Set up MutationObserver and position tracking
309
+ useEffect(() => {
310
+ mountedRef.current = true;
311
+
312
+ if (!enabled) {
313
+ setElements([]);
314
+ return;
315
+ }
316
+
317
+ // Initial scan
318
+ performScan();
319
+
320
+ // Set up MutationObserver for DOM changes
321
+ let debounceTimeout: ReturnType<typeof setTimeout> | null = null;
322
+
323
+ const observer = new MutationObserver((mutations) => {
324
+ // Skip mutations inside DevTools
325
+ const hasRelevantMutation = mutations.some((mutation) => {
326
+ const target = mutation.target as Element;
327
+ return !target.closest?.("[data-sonance-devtools]");
328
+ });
329
+
330
+ if (!hasRelevantMutation) return;
331
+
332
+ // Debounce to batch rapid changes
333
+ if (debounceTimeout) {
334
+ clearTimeout(debounceTimeout);
335
+ }
336
+
337
+ debounceTimeout = setTimeout(() => {
338
+ debounceTimeout = null;
339
+ performScan();
340
+ }, MUTATION_DEBOUNCE_MS);
341
+ });
342
+
343
+ observer.observe(document.body, {
344
+ childList: true,
345
+ subtree: true,
346
+ attributes: true,
347
+ attributeFilter: ["class", "style", "src", "data-state"],
348
+ });
349
+
350
+ // Set up scroll/resize listeners for position updates
351
+ const handlePositionUpdate = () => {
352
+ if (rafIdRef.current) {
353
+ cancelAnimationFrame(rafIdRef.current);
354
+ }
355
+ rafIdRef.current = requestAnimationFrame(updatePositions);
356
+ };
357
+
358
+ window.addEventListener("scroll", handlePositionUpdate, true);
359
+ window.addEventListener("resize", handlePositionUpdate);
360
+
361
+ // Cleanup
362
+ return () => {
363
+ mountedRef.current = false;
364
+ observer.disconnect();
365
+
366
+ if (debounceTimeout) {
367
+ clearTimeout(debounceTimeout);
368
+ }
369
+ if (pendingScanRef.current) {
370
+ clearTimeout(pendingScanRef.current);
371
+ }
372
+ if (rafIdRef.current) {
373
+ cancelAnimationFrame(rafIdRef.current);
374
+ }
375
+
376
+ window.removeEventListener("scroll", handlePositionUpdate, true);
377
+ window.removeEventListener("resize", handlePositionUpdate);
378
+ };
379
+ }, [enabled, performScan, updatePositions]);
380
+
381
+ // Re-scan when filters change
382
+ useEffect(() => {
383
+ if (enabled) {
384
+ performScan();
385
+ }
386
+ }, [enabled, filters, inspectorEnabled, performScan]);
387
+
388
+ return {
389
+ elements,
390
+ originalLogoStates: originalLogoStatesRef.current,
391
+ originalTextStates: originalTextStatesRef.current,
392
+ rescan,
393
+ resetStates,
394
+ };
395
+ }
396
+
397
+ // Re-export utilities for use in other components
398
+ export { extractLogoName };
@@ -0,0 +1,162 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef } from "react";
4
+ import { DetectedElement, OriginalLogoState } from "../types";
5
+ import { generateImageElementId, isLegacyId } from "./useContentHash";
6
+
7
+ /**
8
+ * Image detection configuration
9
+ */
10
+ export interface ImageDetectionConfig {
11
+ /** Callback to check if element is in active layer */
12
+ isInActiveLayer: (el: Element) => boolean;
13
+ /** Current original states (for preserving existing state) */
14
+ existingOriginalStates?: Record<string, OriginalLogoState>;
15
+ }
16
+
17
+ /**
18
+ * Image detection result
19
+ */
20
+ export interface ImageDetectionResult {
21
+ elements: DetectedElement[];
22
+ originalStates: Record<string, OriginalLogoState>;
23
+ }
24
+
25
+ /**
26
+ * Extract logo name from image src
27
+ * Returns name if src contains "logo", null otherwise
28
+ */
29
+ export function extractLogoName(src: string): string | null {
30
+ if (!src) return null;
31
+
32
+ const lowerSrc = src.toLowerCase();
33
+ if (!lowerSrc.includes("logo")) return null;
34
+
35
+ // Extract filename without extension
36
+ const filename = src.split("/").pop()?.split("?")[0] || "";
37
+ const nameWithoutExt = filename.replace(/\.[^/.]+$/, "");
38
+
39
+ // Clean up the name
40
+ return nameWithoutExt
41
+ .replace(/[-_]/g, " ")
42
+ .replace(/\s+/g, " ")
43
+ .trim();
44
+ }
45
+
46
+ /**
47
+ * Detect images/logos on the page
48
+ * Identifies logo images and tracks their original state
49
+ */
50
+ export function detectImages(config: ImageDetectionConfig): ImageDetectionResult {
51
+ const { isInActiveLayer, existingOriginalStates = {} } = config;
52
+ const detected: DetectedElement[] = [];
53
+ const originalStates: Record<string, OriginalLogoState> = { ...existingOriginalStates };
54
+
55
+ const images = document.querySelectorAll("img");
56
+ images.forEach((img) => {
57
+ if (!isInActiveLayer(img)) return;
58
+
59
+ const src = img.src || img.getAttribute("src") || "";
60
+ const alt = img.alt || "";
61
+
62
+ // Check if this is a logo (src or alt contains "logo")
63
+ const logoName = extractLogoName(src);
64
+ const altContainsLogo = alt.toLowerCase().includes("logo");
65
+
66
+ if (!logoName && !altContainsLogo) return;
67
+
68
+ const rect = img.getBoundingClientRect();
69
+ if (rect.width <= 0 || rect.height <= 0) return;
70
+
71
+ // Get or generate stable ID
72
+ let logoId = img.getAttribute("data-sonance-logo-id");
73
+
74
+ // Check if using legacy sequential ID and migrate if needed
75
+ if (logoId && isLegacyId(logoId)) {
76
+ const newId = generateImageElementId(img);
77
+ img.setAttribute("data-sonance-logo-id", newId);
78
+
79
+ // Migrate original state if exists
80
+ if (originalStates[logoId]) {
81
+ originalStates[newId] = originalStates[logoId];
82
+ delete originalStates[logoId];
83
+ }
84
+
85
+ logoId = newId;
86
+ }
87
+
88
+ if (!logoId) {
89
+ logoId = generateImageElementId(img);
90
+ img.setAttribute("data-sonance-logo-id", logoId);
91
+ }
92
+
93
+ // Store original state if not already stored
94
+ if (!originalStates[logoId]) {
95
+ const originalSrc = img.getAttribute("data-original-src") || src;
96
+ const originalSrcset = img.getAttribute("data-original-srcset") || img.srcset || "";
97
+
98
+ originalStates[logoId] = {
99
+ src: originalSrc,
100
+ width: img.naturalWidth || img.width,
101
+ height: img.naturalHeight || img.height,
102
+ srcset: originalSrcset || undefined,
103
+ };
104
+
105
+ // Store original src/srcset as data attributes for reset
106
+ if (!img.getAttribute("data-original-src")) {
107
+ img.setAttribute("data-original-src", src);
108
+ }
109
+ if (!img.getAttribute("data-original-srcset") && img.srcset) {
110
+ img.setAttribute("data-original-srcset", img.srcset);
111
+ }
112
+ }
113
+
114
+ const displayName = logoName || alt || "Logo";
115
+ const imageSrc = img.getAttribute("data-original-src") || src;
116
+
117
+ detected.push({
118
+ name: displayName,
119
+ rect,
120
+ type: "logo",
121
+ logoId,
122
+ imageSrc,
123
+ });
124
+ });
125
+
126
+ return { elements: detected, originalStates };
127
+ }
128
+
129
+ /**
130
+ * Hook for image detection
131
+ * Maintains original state across scans
132
+ */
133
+ export function useImageDetection() {
134
+ const originalStatesRef = useRef<Record<string, OriginalLogoState>>({});
135
+
136
+ const detect = useCallback((config: Omit<ImageDetectionConfig, "existingOriginalStates">): ImageDetectionResult => {
137
+ const result = detectImages({
138
+ ...config,
139
+ existingOriginalStates: originalStatesRef.current,
140
+ });
141
+
142
+ // Update ref with new states
143
+ originalStatesRef.current = result.originalStates;
144
+
145
+ return result;
146
+ }, []);
147
+
148
+ const resetOriginalStates = useCallback(() => {
149
+ originalStatesRef.current = {};
150
+ }, []);
151
+
152
+ const getOriginalStates = useCallback(() => {
153
+ return originalStatesRef.current;
154
+ }, []);
155
+
156
+ return {
157
+ detectImages: detect,
158
+ resetOriginalStates,
159
+ getOriginalStates,
160
+ originalStatesRef,
161
+ };
162
+ }