brandpull 0.1.2
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/LICENSE +21 -0
- package/README.md +116 -0
- package/assets/brandpull-preview-exa.png +0 -0
- package/bin/brandpull +2 -0
- package/package.json +58 -0
- package/src/branding/colors.ts +82 -0
- package/src/branding/index.ts +107 -0
- package/src/branding/llm.ts +269 -0
- package/src/branding/logo.ts +249 -0
- package/src/branding/page-script.ts +860 -0
- package/src/branding/preview.ts +583 -0
- package/src/branding/processor.ts +382 -0
- package/src/branding/types.ts +279 -0
- package/src/index.ts +226 -0
- package/src/ui.ts +45 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { contrastYiq, hexify, isGrayish, isVibrant } from "./colors"
|
|
2
|
+
import { selectLogoWithConfidence } from "./logo"
|
|
3
|
+
import type { BrandingProfile, ButtonSnapshot, InputSnapshot, RawBrandingData, StyleSnapshot } from "./types"
|
|
4
|
+
|
|
5
|
+
function inferPalette(raw: RawBrandingData) {
|
|
6
|
+
const freq = new Map<string, number>()
|
|
7
|
+
const bump = (color: string | null, weight = 1) => {
|
|
8
|
+
if (!color) return
|
|
9
|
+
freq.set(color, (freq.get(color) ?? 0) + weight)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (raw.pageBackground) bump(hexify(raw.pageBackground), 1000)
|
|
13
|
+
for (const snapshot of raw.snapshots) {
|
|
14
|
+
const area = Math.max(1, snapshot.rect.w * snapshot.rect.h)
|
|
15
|
+
bump(hexify(snapshot.colors.background, raw.pageBackground), 0.5 + Math.log10(area + 10))
|
|
16
|
+
bump(hexify(snapshot.colors.text, raw.pageBackground), 1)
|
|
17
|
+
bump(hexify(snapshot.colors.border, raw.pageBackground), 0.3)
|
|
18
|
+
}
|
|
19
|
+
for (const color of raw.cssData.colors) bump(hexify(color, raw.pageBackground), 0.5)
|
|
20
|
+
|
|
21
|
+
const ranked = Array.from(freq.entries())
|
|
22
|
+
.sort((a, b) => b[1] - a[1])
|
|
23
|
+
.map(([color]) => color)
|
|
24
|
+
|
|
25
|
+
let background = "#FFFFFF"
|
|
26
|
+
const pageBackground = raw.pageBackground ? hexify(raw.pageBackground) : null
|
|
27
|
+
if (pageBackground && isGrayish(pageBackground)) background = pageBackground
|
|
28
|
+
|
|
29
|
+
if (background === "#FFFFFF" || (!raw.pageBackground && ranked.length > 0)) {
|
|
30
|
+
if (raw.colorScheme === "dark") {
|
|
31
|
+
background =
|
|
32
|
+
ranked.find((h) => isGrayish(h) && contrastYiq(h) < 128 && contrastYiq(h) > 0) ||
|
|
33
|
+
ranked.find((h) => isGrayish(h) && contrastYiq(h) < 180) ||
|
|
34
|
+
"#1A1A1A"
|
|
35
|
+
} else {
|
|
36
|
+
background = ranked.find((h) => isGrayish(h) && contrastYiq(h) > 180) || "#FFFFFF"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const textPrimary =
|
|
41
|
+
raw.colorScheme === "dark"
|
|
42
|
+
? ranked.find((h) => h !== background && contrastYiq(h) > 180) || "#FFFFFF"
|
|
43
|
+
: ranked.find((h) => h.toUpperCase() !== "#FFFFFF" && contrastYiq(h) < 160) || "#111111"
|
|
44
|
+
const chromatic = ranked.filter((h) => isVibrant(h) && h !== background && h !== textPrimary)
|
|
45
|
+
const primary =
|
|
46
|
+
chromatic[0] ||
|
|
47
|
+
ranked.find((h) => !isGrayish(h) && h !== textPrimary && h !== background) ||
|
|
48
|
+
(raw.colorScheme === "dark" ? "#FFFFFF" : "#000000")
|
|
49
|
+
const accent = chromatic.find((h) => h !== primary) || primary
|
|
50
|
+
const secondary = chromatic.find((h) => h !== primary && h !== accent)
|
|
51
|
+
const textSecondary =
|
|
52
|
+
raw.colorScheme === "dark"
|
|
53
|
+
? ranked.find((h) => h !== textPrimary && contrastYiq(h) > 140 && isGrayish(h))
|
|
54
|
+
: ranked.find((h) => h !== textPrimary && contrastYiq(h) < 180 && isGrayish(h))
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
primary,
|
|
58
|
+
secondary,
|
|
59
|
+
accent,
|
|
60
|
+
background,
|
|
61
|
+
textPrimary,
|
|
62
|
+
textSecondary,
|
|
63
|
+
link: accent,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function inferBaseUnit(values: number[]): number {
|
|
68
|
+
const normalized = values.filter((v) => Number.isFinite(v) && v > 0 && v <= 128).map((v) => Math.round(v))
|
|
69
|
+
if (!normalized.length) return 8
|
|
70
|
+
for (const candidate of [4, 6, 8, 10, 12]) {
|
|
71
|
+
const share =
|
|
72
|
+
normalized.filter((v) => v % candidate === 0 || Math.abs((v % candidate) - candidate) <= 1 || v % candidate <= 1)
|
|
73
|
+
.length / normalized.length
|
|
74
|
+
if (share >= 0.6) return candidate
|
|
75
|
+
}
|
|
76
|
+
normalized.sort((a, b) => a - b)
|
|
77
|
+
const median = normalized[Math.floor(normalized.length / 2)] ?? 8
|
|
78
|
+
return Math.max(2, Math.min(12, Math.round(median / 2) * 2))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pickBorderRadius(values: Array<number | null>): string {
|
|
82
|
+
const radii = values.filter((v): v is number => Number.isFinite(v))
|
|
83
|
+
if (!radii.length) return "8px"
|
|
84
|
+
radii.sort((a, b) => a - b)
|
|
85
|
+
return `${Math.round(radii[Math.floor(radii.length / 2)] ?? 8)}px`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function fontsFromStacks(stacks: string[][]) {
|
|
89
|
+
const ignored =
|
|
90
|
+
/^(system-ui|sans-serif|serif|monospace|ui-sans-serif|ui-serif|arial|helvetica|times new roman|inherit|initial|unset)$/i
|
|
91
|
+
const freq = new Map<string, number>()
|
|
92
|
+
for (const stack of stacks) {
|
|
93
|
+
for (const font of stack) {
|
|
94
|
+
const cleaned = cleanFontName(font)
|
|
95
|
+
if (!cleaned || ignored.test(cleaned) || /^var\(/i.test(cleaned)) continue
|
|
96
|
+
freq.set(cleaned, (freq.get(cleaned) ?? 0) + 1)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Array.from(freq.entries())
|
|
100
|
+
.sort((a, b) => b[1] - a[1])
|
|
101
|
+
.slice(0, 10)
|
|
102
|
+
.map(([family, count]) => ({ family, count }))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function cleanFontName(font: string): string {
|
|
106
|
+
const noQuotes = font.replace(/["']/g, "").trim()
|
|
107
|
+
const next = noQuotes.match(/^__(.+?)(?:_Fallback)?_[a-f0-9]+$/i)
|
|
108
|
+
if (next?.[1]) return next[1].replace(/_/g, " ").trim()
|
|
109
|
+
return noQuotes
|
|
110
|
+
.replace(/_Fallback_[a-f0-9]+$/i, "")
|
|
111
|
+
.replace(/_/g, " ")
|
|
112
|
+
.trim()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function representativeBorderRadius(radius?: StyleSnapshot["borderRadius"]): string {
|
|
116
|
+
const max = Math.max(radius?.topLeft || 0, radius?.topRight || 0, radius?.bottomRight || 0, radius?.bottomLeft || 0)
|
|
117
|
+
return max > 0 ? `${max}px` : "0px"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function corners(radius?: StyleSnapshot["borderRadius"]) {
|
|
121
|
+
return {
|
|
122
|
+
topLeft: `${radius?.topLeft ?? 0}px`,
|
|
123
|
+
topRight: `${radius?.topRight ?? 0}px`,
|
|
124
|
+
bottomRight: `${radius?.bottomRight ?? 0}px`,
|
|
125
|
+
bottomLeft: `${radius?.bottomLeft ?? 0}px`,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function snapshotToButton(snapshot: StyleSnapshot, index: number, raw: RawBrandingData, score: number): ButtonSnapshot {
|
|
130
|
+
const borderHex =
|
|
131
|
+
snapshot.colors.borderWidth && snapshot.colors.borderWidth > 0
|
|
132
|
+
? hexify(snapshot.colors.border, raw.pageBackground)
|
|
133
|
+
: null
|
|
134
|
+
return {
|
|
135
|
+
index,
|
|
136
|
+
text: snapshot.text || "",
|
|
137
|
+
classes: snapshot.classes || "",
|
|
138
|
+
background: hexify(snapshot.colors.background, raw.pageBackground) || "transparent",
|
|
139
|
+
textColor: hexify(snapshot.colors.text, raw.pageBackground) || "#000000",
|
|
140
|
+
borderColor: borderHex,
|
|
141
|
+
borderRadius: representativeBorderRadius(snapshot.borderRadius),
|
|
142
|
+
borderRadiusCorners: corners(snapshot.borderRadius),
|
|
143
|
+
shadow: snapshot.shadow,
|
|
144
|
+
score,
|
|
145
|
+
originalBackgroundColor: snapshot.colors.background || undefined,
|
|
146
|
+
originalTextColor: snapshot.colors.text || undefined,
|
|
147
|
+
originalBorderColor: snapshot.colors.border || undefined,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractButtons(raw: RawBrandingData): ButtonSnapshot[] {
|
|
152
|
+
const candidates = raw.snapshots
|
|
153
|
+
.filter((snapshot) => {
|
|
154
|
+
if (!snapshot.isButton) return false
|
|
155
|
+
if (snapshot.rect.w < 30 || snapshot.rect.h < 25) return false
|
|
156
|
+
if (!snapshot.text?.trim()) return false
|
|
157
|
+
const bg = hexify(snapshot.colors.background, raw.pageBackground)
|
|
158
|
+
const hasBorder = !!snapshot.colors.borderWidth && snapshot.colors.borderWidth > 0
|
|
159
|
+
return !!bg || hasBorder
|
|
160
|
+
})
|
|
161
|
+
.map((snapshot) => {
|
|
162
|
+
let score = 0
|
|
163
|
+
if (snapshot.hasCTAIndicator) score += 1000
|
|
164
|
+
const text = snapshot.text.toLowerCase()
|
|
165
|
+
if (
|
|
166
|
+
[
|
|
167
|
+
"sign up",
|
|
168
|
+
"get started",
|
|
169
|
+
"start",
|
|
170
|
+
"try",
|
|
171
|
+
"demo",
|
|
172
|
+
"contact",
|
|
173
|
+
"buy",
|
|
174
|
+
"subscribe",
|
|
175
|
+
"join",
|
|
176
|
+
"register",
|
|
177
|
+
"download",
|
|
178
|
+
"book",
|
|
179
|
+
].some((kw) => text.includes(kw))
|
|
180
|
+
) {
|
|
181
|
+
score += 500
|
|
182
|
+
}
|
|
183
|
+
const bg = hexify(snapshot.colors.background, raw.pageBackground)
|
|
184
|
+
const border =
|
|
185
|
+
snapshot.colors.borderWidth && snapshot.colors.borderWidth > 0
|
|
186
|
+
? hexify(snapshot.colors.border, raw.pageBackground)
|
|
187
|
+
: null
|
|
188
|
+
if (bg && bg !== "#FFFFFF" && bg !== "#FAFAFA" && bg !== "#F5F5F5") score += 300
|
|
189
|
+
if (border && !bg) score += 200
|
|
190
|
+
if (text.length > 0 && text.length < 50) score += 100
|
|
191
|
+
score += Math.log10(snapshot.rect.w * snapshot.rect.h + 1) * 10
|
|
192
|
+
return { snapshot, score }
|
|
193
|
+
})
|
|
194
|
+
.sort((a, b) => b.score - a.score)
|
|
195
|
+
|
|
196
|
+
const seen = new Set<string>()
|
|
197
|
+
const unique: ButtonSnapshot[] = []
|
|
198
|
+
for (const item of candidates) {
|
|
199
|
+
const bg = hexify(item.snapshot.colors.background, raw.pageBackground) || "transparent"
|
|
200
|
+
const border =
|
|
201
|
+
item.snapshot.colors.borderWidth && item.snapshot.colors.borderWidth > 0
|
|
202
|
+
? hexify(item.snapshot.colors.border, raw.pageBackground) || "transparent-border"
|
|
203
|
+
: "no-border"
|
|
204
|
+
const key = `${item.snapshot.text.trim().toLowerCase().slice(0, 50)}|${bg}|${border}|${item.snapshot.classes.split(/\s+/).slice(0, 5).join(" ")}`
|
|
205
|
+
if (seen.has(key)) continue
|
|
206
|
+
seen.add(key)
|
|
207
|
+
unique.push(snapshotToButton(item.snapshot, unique.length, raw, item.score))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return unique.slice(0, 80)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function classifyButtons(buttons: ButtonSnapshot[]) {
|
|
214
|
+
if (!buttons.length) return { primary: undefined, secondary: undefined, confidence: 0 }
|
|
215
|
+
const scored = buttons
|
|
216
|
+
.map((button) => {
|
|
217
|
+
let score = button.score ?? 0
|
|
218
|
+
if (isVibrant(button.background)) score += 400
|
|
219
|
+
if (/primary|brand|cta|accent|bg-brand|bg-primary/i.test(button.classes)) score += 250
|
|
220
|
+
if (/get started|sign up|start|try|buy|book|demo|download|join/i.test(button.text)) score += 200
|
|
221
|
+
if (button.background === "transparent" || button.background === "#FFFFFF") score -= 80
|
|
222
|
+
return { button, score }
|
|
223
|
+
})
|
|
224
|
+
.sort((a, b) => b.score - a.score)
|
|
225
|
+
const primary = scored[0]?.button
|
|
226
|
+
const secondary = scored.find(
|
|
227
|
+
(item) => item.button.index !== primary?.index && item.button.background !== primary?.background,
|
|
228
|
+
)?.button
|
|
229
|
+
return { primary, secondary, confidence: primary ? 0.65 : 0 }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function extractInputs(raw: RawBrandingData): InputSnapshot[] {
|
|
233
|
+
const candidates = raw.snapshots
|
|
234
|
+
.filter((snapshot) => snapshot.isInput && snapshot.inputMetadata && snapshot.rect.w >= 50 && snapshot.rect.h >= 20)
|
|
235
|
+
.map((snapshot) => {
|
|
236
|
+
const meta = snapshot.inputMetadata!
|
|
237
|
+
let score = 0
|
|
238
|
+
if (meta.type === "email") score += 100
|
|
239
|
+
else if (meta.type === "text") score += 80
|
|
240
|
+
else if (meta.type === "password") score += 70
|
|
241
|
+
else if (meta.type === "search") score += 60
|
|
242
|
+
if (meta.required) score += 50
|
|
243
|
+
if (meta.placeholder) score += 30
|
|
244
|
+
if (meta.label) score += 40
|
|
245
|
+
const words = `${meta.placeholder} ${meta.label} ${meta.name}`.toLowerCase()
|
|
246
|
+
if (words.includes("email")) score += 80
|
|
247
|
+
if (words.includes("search")) score += 60
|
|
248
|
+
return { snapshot, score }
|
|
249
|
+
})
|
|
250
|
+
.sort((a, b) => b.score - a.score)
|
|
251
|
+
|
|
252
|
+
return candidates.slice(0, 20).map(({ snapshot }) => {
|
|
253
|
+
const meta = snapshot.inputMetadata!
|
|
254
|
+
const border =
|
|
255
|
+
snapshot.colors.borderWidth && snapshot.colors.borderWidth > 0
|
|
256
|
+
? hexify(snapshot.colors.border, raw.pageBackground)
|
|
257
|
+
: null
|
|
258
|
+
return {
|
|
259
|
+
type: meta.type,
|
|
260
|
+
placeholder: meta.placeholder,
|
|
261
|
+
label: meta.label,
|
|
262
|
+
name: meta.name,
|
|
263
|
+
required: meta.required,
|
|
264
|
+
classes: snapshot.classes,
|
|
265
|
+
background: hexify(snapshot.colors.background, raw.pageBackground) || "transparent",
|
|
266
|
+
textColor: hexify(snapshot.colors.text, raw.pageBackground),
|
|
267
|
+
borderColor: border,
|
|
268
|
+
borderRadius: representativeBorderRadius(snapshot.borderRadius),
|
|
269
|
+
borderRadiusCorners: corners(snapshot.borderRadius),
|
|
270
|
+
shadow: snapshot.shadow,
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function pickLogo(raw: RawBrandingData, profile: BrandingProfile) {
|
|
276
|
+
const heuristic = selectLogoWithConfidence(raw.logoCandidates, raw.brandName)
|
|
277
|
+
if (heuristic.selectedIndex >= 0) {
|
|
278
|
+
const selected = raw.logoCandidates[heuristic.selectedIndex]
|
|
279
|
+
if (selected) {
|
|
280
|
+
profile.logo = selected.src
|
|
281
|
+
profile.images = {
|
|
282
|
+
...profile.images,
|
|
283
|
+
logo: selected.src,
|
|
284
|
+
logoHref: selected.href ?? null,
|
|
285
|
+
logoAlt: selected.alt || null,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
profile.diagnostics = {
|
|
290
|
+
...profile.diagnostics,
|
|
291
|
+
logo: {
|
|
292
|
+
source: heuristic.source,
|
|
293
|
+
selectedIndex: heuristic.selectedIndex,
|
|
294
|
+
reasoning: heuristic.reasoning,
|
|
295
|
+
confidence: heuristic.confidence,
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
profile.confidence = { ...profile.confidence, logo: heuristic.confidence }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function processRawBranding(
|
|
302
|
+
raw: RawBrandingData,
|
|
303
|
+
options: { debug?: boolean; url?: string } = {},
|
|
304
|
+
): BrandingProfile {
|
|
305
|
+
const palette = inferPalette(raw)
|
|
306
|
+
const allFontStacks = [
|
|
307
|
+
raw.typography.stacks.body,
|
|
308
|
+
raw.typography.stacks.heading,
|
|
309
|
+
raw.typography.stacks.paragraph,
|
|
310
|
+
...raw.snapshots.map((snapshot) => snapshot.typography.fontStack),
|
|
311
|
+
]
|
|
312
|
+
const fonts = fontsFromStacks(allFontStacks)
|
|
313
|
+
const buttons = extractButtons(raw)
|
|
314
|
+
const inputs = extractInputs(raw)
|
|
315
|
+
const classified = classifyButtons(buttons)
|
|
316
|
+
const profile: BrandingProfile = {
|
|
317
|
+
url: options.url,
|
|
318
|
+
finalUrl: raw.pageUrl,
|
|
319
|
+
brandName: raw.brandName,
|
|
320
|
+
pageTitle: raw.pageTitle,
|
|
321
|
+
colorScheme: raw.colorScheme,
|
|
322
|
+
fonts,
|
|
323
|
+
colors: palette,
|
|
324
|
+
typography: {
|
|
325
|
+
fontFamilies: {
|
|
326
|
+
primary: cleanFontName(raw.typography.stacks.body[0] || fonts[0]?.family || "system-ui"),
|
|
327
|
+
heading: cleanFontName(
|
|
328
|
+
raw.typography.stacks.heading[0] || raw.typography.stacks.body[0] || fonts[0]?.family || "system-ui",
|
|
329
|
+
),
|
|
330
|
+
},
|
|
331
|
+
fontStacks: raw.typography.stacks,
|
|
332
|
+
fontSizes: raw.typography.sizes,
|
|
333
|
+
},
|
|
334
|
+
spacing: {
|
|
335
|
+
baseUnit: inferBaseUnit(raw.cssData.spacings),
|
|
336
|
+
borderRadius: pickBorderRadius([...raw.snapshots.map((snapshot) => snapshot.radius), ...raw.cssData.radii]),
|
|
337
|
+
},
|
|
338
|
+
components: {},
|
|
339
|
+
images: {
|
|
340
|
+
logo:
|
|
341
|
+
raw.images.find((image) => image.type === "logo")?.src ||
|
|
342
|
+
raw.images.find((image) => image.type === "logo-svg")?.src ||
|
|
343
|
+
null,
|
|
344
|
+
favicon: raw.images.find((image) => image.type === "favicon")?.src || null,
|
|
345
|
+
ogImage:
|
|
346
|
+
raw.images.find((image) => image.type === "og")?.src ||
|
|
347
|
+
raw.images.find((image) => image.type === "twitter")?.src ||
|
|
348
|
+
null,
|
|
349
|
+
},
|
|
350
|
+
confidence: {
|
|
351
|
+
colors: 0.65,
|
|
352
|
+
buttons: classified.confidence,
|
|
353
|
+
},
|
|
354
|
+
diagnostics: {
|
|
355
|
+
errors: raw.errors,
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (inputs[0]) profile.components!.input = inputs[0]
|
|
360
|
+
if (classified.primary) profile.components!.buttonPrimary = classified.primary
|
|
361
|
+
if (classified.secondary) profile.components!.buttonSecondary = classified.secondary
|
|
362
|
+
|
|
363
|
+
pickLogo(raw, profile)
|
|
364
|
+
|
|
365
|
+
profile.confidence = {
|
|
366
|
+
...profile.confidence,
|
|
367
|
+
overall:
|
|
368
|
+
((profile.confidence?.logo ?? 0) + (profile.confidence?.colors ?? 0) + (profile.confidence?.buttons ?? 0)) / 3,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (options.debug) {
|
|
372
|
+
profile.debug = {
|
|
373
|
+
buttons,
|
|
374
|
+
inputs,
|
|
375
|
+
logoCandidates: raw.logoCandidates,
|
|
376
|
+
frameworkHints: raw.frameworkHints,
|
|
377
|
+
backgroundCandidates: raw.backgroundCandidates,
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return profile
|
|
382
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
export type ColorScheme = "light" | "dark"
|
|
2
|
+
|
|
3
|
+
export interface CSSData {
|
|
4
|
+
colors: string[]
|
|
5
|
+
spacings: number[]
|
|
6
|
+
radii: number[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StyleSnapshot {
|
|
10
|
+
tag: string
|
|
11
|
+
classes: string
|
|
12
|
+
text: string
|
|
13
|
+
rect: { w: number; h: number }
|
|
14
|
+
colors: {
|
|
15
|
+
text: string
|
|
16
|
+
background: string
|
|
17
|
+
border: string
|
|
18
|
+
borderWidth: number | null
|
|
19
|
+
borderTop?: string
|
|
20
|
+
borderTopWidth?: number | null
|
|
21
|
+
borderRight?: string
|
|
22
|
+
borderRightWidth?: number | null
|
|
23
|
+
borderBottom?: string
|
|
24
|
+
borderBottomWidth?: number | null
|
|
25
|
+
borderLeft?: string
|
|
26
|
+
borderLeftWidth?: number | null
|
|
27
|
+
}
|
|
28
|
+
typography: {
|
|
29
|
+
fontStack: string[]
|
|
30
|
+
size: string | null
|
|
31
|
+
weight: number | null
|
|
32
|
+
}
|
|
33
|
+
radius: number | null
|
|
34
|
+
borderRadius: {
|
|
35
|
+
topLeft: number | null
|
|
36
|
+
topRight: number | null
|
|
37
|
+
bottomRight: number | null
|
|
38
|
+
bottomLeft: number | null
|
|
39
|
+
}
|
|
40
|
+
shadow: string | null
|
|
41
|
+
isButton: boolean
|
|
42
|
+
isNavigation: boolean
|
|
43
|
+
hasCTAIndicator: boolean
|
|
44
|
+
isInput: boolean
|
|
45
|
+
inputMetadata: {
|
|
46
|
+
type: string
|
|
47
|
+
placeholder: string
|
|
48
|
+
value: string
|
|
49
|
+
required: boolean
|
|
50
|
+
disabled: boolean
|
|
51
|
+
name: string
|
|
52
|
+
id: string
|
|
53
|
+
label: string
|
|
54
|
+
} | null
|
|
55
|
+
isLink: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface LogoCandidate {
|
|
59
|
+
src: string
|
|
60
|
+
alt: string
|
|
61
|
+
ariaLabel?: string
|
|
62
|
+
title?: string
|
|
63
|
+
isSvg: boolean
|
|
64
|
+
isVisible: boolean
|
|
65
|
+
location: "header" | "body" | "footer"
|
|
66
|
+
position: { top: number; left: number; width: number; height: number }
|
|
67
|
+
indicators: {
|
|
68
|
+
inHeader: boolean
|
|
69
|
+
altMatch: boolean
|
|
70
|
+
srcMatch: boolean
|
|
71
|
+
classMatch: boolean
|
|
72
|
+
hrefMatch: boolean
|
|
73
|
+
}
|
|
74
|
+
href?: string
|
|
75
|
+
source: string
|
|
76
|
+
logoSvgScore?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface RawImage {
|
|
80
|
+
type: "favicon" | "og" | "twitter" | "logo" | "logo-svg" | string
|
|
81
|
+
src: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface BackgroundCandidate {
|
|
85
|
+
color: string
|
|
86
|
+
source: string
|
|
87
|
+
priority: number
|
|
88
|
+
area?: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TypographyData {
|
|
92
|
+
stacks: {
|
|
93
|
+
body: string[]
|
|
94
|
+
heading: string[]
|
|
95
|
+
paragraph: string[]
|
|
96
|
+
}
|
|
97
|
+
sizes: {
|
|
98
|
+
h1: string
|
|
99
|
+
h2: string
|
|
100
|
+
body: string
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ExtractionDiagnostic {
|
|
105
|
+
context: string
|
|
106
|
+
message: string
|
|
107
|
+
timestamp: number
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface RawBrandingData {
|
|
111
|
+
cssData: CSSData
|
|
112
|
+
snapshots: StyleSnapshot[]
|
|
113
|
+
images: RawImage[]
|
|
114
|
+
logoCandidates: LogoCandidate[]
|
|
115
|
+
brandName: string
|
|
116
|
+
pageTitle: string
|
|
117
|
+
pageUrl: string
|
|
118
|
+
typography: TypographyData
|
|
119
|
+
frameworkHints: string[]
|
|
120
|
+
colorScheme: ColorScheme
|
|
121
|
+
pageBackground: string | null
|
|
122
|
+
backgroundCandidates: BackgroundCandidate[]
|
|
123
|
+
errors?: ExtractionDiagnostic[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ButtonSnapshot {
|
|
127
|
+
index: number
|
|
128
|
+
text: string
|
|
129
|
+
classes: string
|
|
130
|
+
background: string
|
|
131
|
+
textColor: string
|
|
132
|
+
borderColor?: string | null
|
|
133
|
+
borderRadius?: string
|
|
134
|
+
borderRadiusCorners?: {
|
|
135
|
+
topLeft?: string
|
|
136
|
+
topRight?: string
|
|
137
|
+
bottomRight?: string
|
|
138
|
+
bottomLeft?: string
|
|
139
|
+
}
|
|
140
|
+
shadow?: string | null
|
|
141
|
+
score?: number
|
|
142
|
+
originalBackgroundColor?: string
|
|
143
|
+
originalTextColor?: string
|
|
144
|
+
originalBorderColor?: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface InputSnapshot {
|
|
148
|
+
type: string
|
|
149
|
+
placeholder: string
|
|
150
|
+
label: string
|
|
151
|
+
name: string
|
|
152
|
+
required: boolean
|
|
153
|
+
classes: string
|
|
154
|
+
background: string
|
|
155
|
+
textColor: string | null
|
|
156
|
+
borderColor?: string | null
|
|
157
|
+
borderRadius?: string
|
|
158
|
+
borderRadiusCorners?: {
|
|
159
|
+
topLeft?: string
|
|
160
|
+
topRight?: string
|
|
161
|
+
bottomRight?: string
|
|
162
|
+
bottomLeft?: string
|
|
163
|
+
}
|
|
164
|
+
shadow?: string | null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface BrandingProfile {
|
|
168
|
+
url?: string
|
|
169
|
+
finalUrl?: string
|
|
170
|
+
brandName?: string
|
|
171
|
+
pageTitle?: string
|
|
172
|
+
colorScheme?: ColorScheme
|
|
173
|
+
logo?: string | null
|
|
174
|
+
fonts?: Array<{ family: string; count?: number; role?: string }>
|
|
175
|
+
colors?: {
|
|
176
|
+
primary?: string
|
|
177
|
+
secondary?: string
|
|
178
|
+
accent?: string
|
|
179
|
+
background?: string
|
|
180
|
+
textPrimary?: string
|
|
181
|
+
textSecondary?: string
|
|
182
|
+
link?: string
|
|
183
|
+
[key: string]: string | undefined
|
|
184
|
+
}
|
|
185
|
+
typography?: {
|
|
186
|
+
fontFamilies?: {
|
|
187
|
+
primary?: string
|
|
188
|
+
heading?: string
|
|
189
|
+
code?: string
|
|
190
|
+
}
|
|
191
|
+
fontStacks?: {
|
|
192
|
+
body?: string[]
|
|
193
|
+
heading?: string[]
|
|
194
|
+
paragraph?: string[]
|
|
195
|
+
primary?: string[]
|
|
196
|
+
}
|
|
197
|
+
fontSizes?: {
|
|
198
|
+
h1?: string
|
|
199
|
+
h2?: string
|
|
200
|
+
body?: string
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
spacing?: {
|
|
204
|
+
baseUnit?: number
|
|
205
|
+
borderRadius?: string
|
|
206
|
+
}
|
|
207
|
+
components?: {
|
|
208
|
+
buttonPrimary?: ComponentStyle
|
|
209
|
+
buttonSecondary?: ComponentStyle
|
|
210
|
+
input?: ComponentStyle
|
|
211
|
+
}
|
|
212
|
+
images?: {
|
|
213
|
+
logo?: string | null
|
|
214
|
+
logoHref?: string | null
|
|
215
|
+
logoAlt?: string | null
|
|
216
|
+
favicon?: string | null
|
|
217
|
+
ogImage?: string | null
|
|
218
|
+
}
|
|
219
|
+
personality?: {
|
|
220
|
+
tone: "professional" | "playful" | "modern" | "traditional" | "minimalist" | "bold"
|
|
221
|
+
energy: "low" | "medium" | "high"
|
|
222
|
+
targetAudience: string
|
|
223
|
+
}
|
|
224
|
+
designSystem?: {
|
|
225
|
+
framework: "tailwind" | "bootstrap" | "material" | "chakra" | "custom" | "unknown"
|
|
226
|
+
componentLibrary: string
|
|
227
|
+
}
|
|
228
|
+
confidence?: {
|
|
229
|
+
logo?: number
|
|
230
|
+
colors?: number
|
|
231
|
+
buttons?: number
|
|
232
|
+
overall?: number
|
|
233
|
+
}
|
|
234
|
+
diagnostics?: {
|
|
235
|
+
llm?: {
|
|
236
|
+
enabled: boolean
|
|
237
|
+
used: boolean
|
|
238
|
+
error?: string
|
|
239
|
+
model?: string
|
|
240
|
+
}
|
|
241
|
+
logo?: {
|
|
242
|
+
source: "heuristic" | "llm" | "fallback" | "none"
|
|
243
|
+
selectedIndex: number
|
|
244
|
+
reasoning: string
|
|
245
|
+
confidence: number
|
|
246
|
+
}
|
|
247
|
+
errors?: ExtractionDiagnostic[]
|
|
248
|
+
}
|
|
249
|
+
debug?: {
|
|
250
|
+
buttons?: ButtonSnapshot[]
|
|
251
|
+
inputs?: InputSnapshot[]
|
|
252
|
+
logoCandidates?: LogoCandidate[]
|
|
253
|
+
frameworkHints?: string[]
|
|
254
|
+
backgroundCandidates?: BackgroundCandidate[]
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface ComponentStyle {
|
|
259
|
+
background?: string
|
|
260
|
+
textColor?: string | null
|
|
261
|
+
borderColor?: string | null
|
|
262
|
+
borderRadius?: string
|
|
263
|
+
borderRadiusCorners?: {
|
|
264
|
+
topLeft?: string
|
|
265
|
+
topRight?: string
|
|
266
|
+
bottomRight?: string
|
|
267
|
+
bottomLeft?: string
|
|
268
|
+
}
|
|
269
|
+
shadow?: string | null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface BrandingExtractionOptions {
|
|
273
|
+
debug?: boolean
|
|
274
|
+
includeRaw?: boolean
|
|
275
|
+
llm?: boolean
|
|
276
|
+
waitMs?: number
|
|
277
|
+
timeoutMs?: number
|
|
278
|
+
model?: string
|
|
279
|
+
}
|