sonance-brand-mcp 1.3.111 → 1.3.113
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-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 +988 -57
- 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/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 +429 -362
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
- package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
- 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 +4 -64
- 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 +171 -65
- 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/panels/ComponentsPanel.tsx +160 -57
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +42 -0
- 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 +32 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef } from "react";
|
|
4
|
+
import { DetectedElement, OriginalTextState } from "../types";
|
|
5
|
+
import { generateTextElementId, isLegacyId } from "./useContentHash";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Text detection configuration
|
|
9
|
+
*/
|
|
10
|
+
export interface TextDetectionConfig {
|
|
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, OriginalTextState>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Text detection result
|
|
19
|
+
*/
|
|
20
|
+
export interface TextDetectionResult {
|
|
21
|
+
elements: DetectedElement[];
|
|
22
|
+
originalStates: Record<string, OriginalTextState>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Semantic text element selectors
|
|
27
|
+
* Only select meaningful text containers - exclude spans as they're usually nested
|
|
28
|
+
*/
|
|
29
|
+
const TEXT_SELECTORS = "h1, h2, h3, h4, h5, h6, p, a, label, blockquote, figcaption, li";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Minimum dimensions to filter out decorative/icon elements
|
|
33
|
+
*/
|
|
34
|
+
const MIN_WIDTH = 20;
|
|
35
|
+
const MIN_HEIGHT = 10;
|
|
36
|
+
const MIN_TEXT_LENGTH = 2;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if element has meaningful text content
|
|
40
|
+
*/
|
|
41
|
+
function hasMeaningfulContent(element: Element): boolean {
|
|
42
|
+
const textContent = element.textContent?.trim() || "";
|
|
43
|
+
|
|
44
|
+
if (textContent.length < MIN_TEXT_LENGTH) return false;
|
|
45
|
+
|
|
46
|
+
// Skip elements that only contain whitespace/invisible chars
|
|
47
|
+
if (/^[\s\u200B-\u200D\uFEFF]+$/.test(textContent)) return false;
|
|
48
|
+
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if element is nested inside another text element we'd track
|
|
54
|
+
*/
|
|
55
|
+
function isNestedTextElement(element: Element): boolean {
|
|
56
|
+
const parentTextEl = element.parentElement?.closest(TEXT_SELECTORS);
|
|
57
|
+
|
|
58
|
+
if (!parentTextEl) return false;
|
|
59
|
+
|
|
60
|
+
// Skip if parent is part of DevTools
|
|
61
|
+
if (parentTextEl.closest("[data-sonance-devtools]")) return false;
|
|
62
|
+
|
|
63
|
+
// Allow links inside paragraphs to be selectable
|
|
64
|
+
const tagName = element.tagName.toLowerCase();
|
|
65
|
+
const parentTagName = parentTextEl.tagName.toLowerCase();
|
|
66
|
+
|
|
67
|
+
if (tagName === "a" && parentTagName === "p") return false;
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get display name for text element
|
|
74
|
+
*/
|
|
75
|
+
function getDisplayName(element: Element, textContent: string): string {
|
|
76
|
+
const tagName = element.tagName.toLowerCase();
|
|
77
|
+
|
|
78
|
+
let displayName: string;
|
|
79
|
+
switch (tagName) {
|
|
80
|
+
case "a":
|
|
81
|
+
displayName = "Link";
|
|
82
|
+
break;
|
|
83
|
+
case "li":
|
|
84
|
+
displayName = "List Item";
|
|
85
|
+
break;
|
|
86
|
+
default:
|
|
87
|
+
displayName = tagName.toUpperCase();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const truncatedContent = textContent.length > 30
|
|
91
|
+
? textContent.substring(0, 30) + "..."
|
|
92
|
+
: textContent;
|
|
93
|
+
|
|
94
|
+
return `${displayName}: ${truncatedContent}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Detect text elements on the page
|
|
99
|
+
* Filters noise and captures original state
|
|
100
|
+
*/
|
|
101
|
+
export function detectTextElements(config: TextDetectionConfig): TextDetectionResult {
|
|
102
|
+
const { isInActiveLayer, existingOriginalStates = {} } = config;
|
|
103
|
+
const detected: DetectedElement[] = [];
|
|
104
|
+
const originalStates: Record<string, OriginalTextState> = { ...existingOriginalStates };
|
|
105
|
+
|
|
106
|
+
// Track added elements to avoid duplicates
|
|
107
|
+
const addedElements = new Set<Element>();
|
|
108
|
+
|
|
109
|
+
const textElements = document.querySelectorAll(TEXT_SELECTORS);
|
|
110
|
+
textElements.forEach((el) => {
|
|
111
|
+
// Skip elements outside active layer
|
|
112
|
+
if (!isInActiveLayer(el)) return;
|
|
113
|
+
|
|
114
|
+
// Skip nested text elements (avoid duplicates)
|
|
115
|
+
if (isNestedTextElement(el)) return;
|
|
116
|
+
|
|
117
|
+
// Skip already processed elements
|
|
118
|
+
if (addedElements.has(el)) return;
|
|
119
|
+
|
|
120
|
+
const rect = el.getBoundingClientRect();
|
|
121
|
+
|
|
122
|
+
// Filter out small/decorative elements
|
|
123
|
+
if (rect.width < MIN_WIDTH || rect.height < MIN_HEIGHT) return;
|
|
124
|
+
|
|
125
|
+
const textContent = el.textContent?.trim() || "";
|
|
126
|
+
|
|
127
|
+
// Skip elements without meaningful content
|
|
128
|
+
if (!hasMeaningfulContent(el)) return;
|
|
129
|
+
|
|
130
|
+
// Skip invisible elements
|
|
131
|
+
if (rect.width <= 0 || rect.height <= 0) return;
|
|
132
|
+
|
|
133
|
+
// Get or generate stable ID
|
|
134
|
+
let textId = el.getAttribute("data-sonance-text-id");
|
|
135
|
+
|
|
136
|
+
// Check if using legacy sequential ID and migrate if needed
|
|
137
|
+
if (textId && isLegacyId(textId)) {
|
|
138
|
+
const newId = generateTextElementId(el);
|
|
139
|
+
el.setAttribute("data-sonance-text-id", newId);
|
|
140
|
+
|
|
141
|
+
// Migrate original state if exists
|
|
142
|
+
if (originalStates[textId]) {
|
|
143
|
+
originalStates[newId] = originalStates[textId];
|
|
144
|
+
delete originalStates[textId];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
textId = newId;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!textId) {
|
|
151
|
+
textId = generateTextElementId(el);
|
|
152
|
+
el.setAttribute("data-sonance-text-id", textId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Capture original state if not already captured
|
|
156
|
+
if (!originalStates[textId]) {
|
|
157
|
+
const computed = window.getComputedStyle(el);
|
|
158
|
+
originalStates[textId] = {
|
|
159
|
+
textContent: el.textContent,
|
|
160
|
+
fontSize: computed.fontSize,
|
|
161
|
+
fontWeight: computed.fontWeight,
|
|
162
|
+
lineHeight: computed.lineHeight,
|
|
163
|
+
letterSpacing: computed.letterSpacing,
|
|
164
|
+
color: computed.color,
|
|
165
|
+
fontFamily: computed.fontFamily,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const displayName = getDisplayName(el, textContent);
|
|
170
|
+
|
|
171
|
+
addedElements.add(el);
|
|
172
|
+
detected.push({
|
|
173
|
+
name: displayName,
|
|
174
|
+
rect,
|
|
175
|
+
type: "text",
|
|
176
|
+
textId,
|
|
177
|
+
textContent,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return { elements: detected, originalStates };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Hook for text detection
|
|
186
|
+
* Maintains original state across scans
|
|
187
|
+
*/
|
|
188
|
+
export function useTextDetection() {
|
|
189
|
+
const originalStatesRef = useRef<Record<string, OriginalTextState>>({});
|
|
190
|
+
|
|
191
|
+
const detect = useCallback((config: Omit<TextDetectionConfig, "existingOriginalStates">): TextDetectionResult => {
|
|
192
|
+
const result = detectTextElements({
|
|
193
|
+
...config,
|
|
194
|
+
existingOriginalStates: originalStatesRef.current,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Update ref with new states
|
|
198
|
+
originalStatesRef.current = result.originalStates;
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
const resetOriginalStates = useCallback(() => {
|
|
204
|
+
originalStatesRef.current = {};
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
207
|
+
const getOriginalStates = useCallback(() => {
|
|
208
|
+
return originalStatesRef.current;
|
|
209
|
+
}, []);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
detectTextElements: detect,
|
|
213
|
+
resetOriginalStates,
|
|
214
|
+
getOriginalStates,
|
|
215
|
+
originalStatesRef,
|
|
216
|
+
};
|
|
217
|
+
}
|