a33c5e1b05
Resolve dor real: clonar estilos visuais de sites e slides para propostas. Tentativas anteriores (Penpot, scrapers HTML, Figma import) falharam porque tentavam reproduzir layouts. Esta skill extrai tokens (cores, fontes, espaçamento, raios, sombras) e alimenta gerador (Stitch / design-engine). Modos: - /clone-style web <url>: extract-web-tokens.js via chrome real (CSS computado) - /clone-style slides <pptx>: extract-pptx-theme.py (theme1.xml + slideMasters) - /clone-style apply <tokens.json>: mapeia para Stitch / design-engine / pptx Validado: PPTX (Calibri/Calibri Light + 6 accent colors do Office default theme). Web: aguarda primeiro teste end-to-end com browser real. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
123 lines
4.0 KiB
JavaScript
123 lines
4.0 KiB
JavaScript
// extract-web-tokens.js
|
|
// Injectar via mcp__claude-in-chrome__javascript_tool ou mcp__chrome-devtools__evaluate_script
|
|
// Devolve JSON com cores, fontes, espaçamentos, raios, sombras e amostras de componentes.
|
|
// Funciona em qualquer site (incluindo JS-rendered) porque usa getComputedStyle().
|
|
|
|
(() => {
|
|
const all = document.querySelectorAll('*');
|
|
const colorCounts = {};
|
|
const fontFamilies = {};
|
|
const fontSizes = {};
|
|
const fontWeights = {};
|
|
const lineHeights = {};
|
|
const radii = {};
|
|
const shadows = {};
|
|
const spacings = {};
|
|
const bgColors = {};
|
|
|
|
const norm = (v) => (v || '').trim();
|
|
const inc = (obj, k) => { if (!k || k === 'none' || k === '0px') return; obj[k] = (obj[k] || 0) + 1; };
|
|
|
|
// Helper: rgb(a) → hex (ignora transparente)
|
|
const toHex = (rgb) => {
|
|
if (!rgb || rgb === 'transparent' || rgb === 'rgba(0, 0, 0, 0)') return null;
|
|
const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
if (!m) return null;
|
|
const a = m[4] !== undefined ? parseFloat(m[4]) : 1;
|
|
if (a < 0.1) return null;
|
|
const h = (n) => parseInt(n).toString(16).padStart(2, '0');
|
|
return '#' + h(m[1]) + h(m[2]) + h(m[3]);
|
|
};
|
|
|
|
for (const el of all) {
|
|
if (!(el instanceof Element)) continue;
|
|
const cs = getComputedStyle(el);
|
|
|
|
// Cores
|
|
inc(colorCounts, toHex(cs.color));
|
|
const bg = toHex(cs.backgroundColor);
|
|
if (bg) {
|
|
inc(bgColors, bg);
|
|
inc(colorCounts, bg);
|
|
}
|
|
inc(colorCounts, toHex(cs.borderColor));
|
|
|
|
// Fontes
|
|
inc(fontFamilies, norm(cs.fontFamily));
|
|
inc(fontSizes, norm(cs.fontSize));
|
|
inc(fontWeights, norm(cs.fontWeight));
|
|
inc(lineHeights, norm(cs.lineHeight));
|
|
|
|
// Raios
|
|
inc(radii, norm(cs.borderRadius));
|
|
|
|
// Sombras
|
|
if (cs.boxShadow && cs.boxShadow !== 'none') inc(shadows, cs.boxShadow);
|
|
|
|
// Espaçamentos (paddings + margins + gap)
|
|
['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'gap', 'rowGap', 'columnGap'].forEach((p) => {
|
|
inc(spacings, norm(cs[p]));
|
|
});
|
|
}
|
|
|
|
// Top N por frequência
|
|
const topN = (obj, n) => Object.entries(obj).sort((a, b) => b[1] - a[1]).slice(0, n).map(([k, v]) => ({ value: k, count: v }));
|
|
|
|
// Detectar grid base (espaçamento mais comum)
|
|
const numericSpacings = Object.entries(spacings)
|
|
.map(([k, v]) => [parseFloat(k), v])
|
|
.filter(([n]) => !isNaN(n) && n > 0 && n < 200)
|
|
.sort((a, b) => b[1] - a[1]);
|
|
const gridBase = numericSpacings[0]?.[0] || 8;
|
|
|
|
// Capturar samples HTML de componentes-chave (pelo selector)
|
|
const sample = (sel) => {
|
|
const el = document.querySelector(sel);
|
|
if (!el) return null;
|
|
return {
|
|
html: el.outerHTML.substring(0, 800),
|
|
computed: {
|
|
bg: toHex(getComputedStyle(el).backgroundColor),
|
|
color: toHex(getComputedStyle(el).color),
|
|
radius: getComputedStyle(el).borderRadius,
|
|
padding: getComputedStyle(el).padding,
|
|
font: getComputedStyle(el).fontFamily,
|
|
}
|
|
};
|
|
};
|
|
|
|
return {
|
|
url: location.href,
|
|
title: document.title,
|
|
timestamp: new Date().toISOString(),
|
|
colors: {
|
|
top: topN(colorCounts, 12),
|
|
backgrounds: topN(bgColors, 6),
|
|
dominant: topN(colorCounts, 1)[0]?.value || null,
|
|
},
|
|
fonts: {
|
|
families: topN(fontFamilies, 5),
|
|
sizes: topN(fontSizes, 8),
|
|
weights: topN(fontWeights, 6),
|
|
lineHeights: topN(lineHeights, 6),
|
|
},
|
|
borderRadius: topN(radii, 6),
|
|
shadows: topN(shadows, 5),
|
|
spacing: {
|
|
gridBaseGuess: gridBase,
|
|
top: topN(spacings, 12),
|
|
},
|
|
components: {
|
|
button: sample('button, .btn, [class*="button" i]'),
|
|
card: sample('.card, [class*="card" i]'),
|
|
nav: sample('nav, header, [class*="nav" i]'),
|
|
hero: sample('main > section:first-child, .hero, [class*="hero" i]'),
|
|
},
|
|
meta: {
|
|
colorMode: getComputedStyle(document.body).backgroundColor.includes('255') ? 'LIGHT' : 'DARK',
|
|
lang: document.documentElement.lang || 'unknown',
|
|
viewport: document.querySelector('meta[name=viewport]')?.content || null,
|
|
}
|
|
};
|
|
})();
|