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.
- package/dist/assets/api/sonance-ai-edit/route.ts +30 -7
- package/dist/assets/api/sonance-save-image/route.ts +625 -0
- package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +1020 -64
- package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
- package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
- package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
- package/dist/assets/brand-system.ts +13 -12
- package/dist/assets/components/accordion.tsx +15 -7
- package/dist/assets/components/alert-dialog.tsx +35 -10
- package/dist/assets/components/alert.tsx +11 -10
- package/dist/assets/components/avatar.tsx +4 -4
- package/dist/assets/components/badge.tsx +16 -12
- package/dist/assets/components/button.stories.tsx +3 -3
- package/dist/assets/components/button.tsx +50 -31
- package/dist/assets/components/calendar.tsx +12 -8
- package/dist/assets/components/card.tsx +35 -29
- package/dist/assets/components/checkbox.tsx +9 -8
- package/dist/assets/components/code.tsx +19 -11
- package/dist/assets/components/command.tsx +32 -13
- package/dist/assets/components/context-menu.tsx +37 -16
- package/dist/assets/components/dialog.tsx +8 -5
- package/dist/assets/components/divider.tsx +15 -5
- package/dist/assets/components/drawer.tsx +4 -3
- package/dist/assets/components/dropdown-menu.tsx +15 -13
- package/dist/assets/components/hover-card.tsx +4 -1
- package/dist/assets/components/image.tsx +1 -1
- package/dist/assets/components/input.tsx +29 -14
- package/dist/assets/components/kbd.stories.tsx +3 -3
- package/dist/assets/components/kbd.tsx +29 -13
- package/dist/assets/components/listbox.tsx +8 -8
- package/dist/assets/components/menubar.tsx +50 -23
- package/dist/assets/components/navbar.stories.tsx +140 -13
- package/dist/assets/components/navbar.tsx +22 -5
- package/dist/assets/components/navigation-menu.tsx +28 -6
- package/dist/assets/components/pagination.tsx +10 -10
- package/dist/assets/components/popover.tsx +10 -8
- package/dist/assets/components/progress.tsx +6 -4
- package/dist/assets/components/radio-group.tsx +5 -5
- package/dist/assets/components/select.tsx +49 -29
- package/dist/assets/components/separator.tsx +3 -3
- package/dist/assets/components/sheet.tsx +4 -4
- package/dist/assets/components/sidebar.tsx +10 -10
- package/dist/assets/components/skeleton.tsx +13 -5
- package/dist/assets/components/slider.tsx +12 -10
- package/dist/assets/components/switch.tsx +4 -4
- package/dist/assets/components/table.tsx +5 -5
- package/dist/assets/components/tabs.tsx +8 -8
- package/dist/assets/components/textarea.tsx +11 -9
- package/dist/assets/components/toast.tsx +7 -7
- package/dist/assets/components/toggle.tsx +27 -7
- package/dist/assets/components/tooltip.tsx +10 -8
- package/dist/assets/components/user.tsx +8 -6
- package/dist/assets/dev-tools/SonanceDevTools.tsx +851 -708
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
- package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
- package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +12 -63
- package/dist/assets/dev-tools/constants.ts +38 -6
- package/dist/assets/dev-tools/hooks/index.ts +69 -0
- package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
- package/dist/assets/dev-tools/hooks/useComputedStyles.ts +471 -0
- package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
- package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
- package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
- package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
- package/dist/assets/dev-tools/index.ts +3 -0
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +93 -2
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- package/dist/index.js +22 -3
- 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
|
+
}
|