v0-ui-reviewer
Version:
Next-gen UI/UX reviewer with multi-model AI support (OpenAI, Claude, v0), style extraction, and live preview sandbox
336 lines (335 loc) • 13.5 kB
JavaScript
export class StyleExtractor {
page;
constructor(page) {
this.page = page;
}
/**
* Extract computed styles from a specific coordinate
*/
async extractStyleAtPoint(x, y) {
try {
const extractedData = await this.page.evaluate((x, y) => {
const element = document.elementFromPoint(x, y);
if (!element)
return null;
const computed = window.getComputedStyle(element);
// Helper to get all border colors
const getBorderColors = () => {
const top = computed.getPropertyValue('border-top-color');
const right = computed.getPropertyValue('border-right-color');
const bottom = computed.getPropertyValue('border-bottom-color');
const left = computed.getPropertyValue('border-left-color');
// If all are the same, return a single value
if (top === right && right === bottom && bottom === left) {
return top;
}
return `${top} ${right} ${bottom} ${left}`;
};
return {
element: {
tagName: element.tagName.toLowerCase(),
className: element.className || '',
id: element.id || ''
},
computed: {
// Colors
backgroundColor: computed.backgroundColor,
color: computed.color,
borderColor: getBorderColors(),
borderTopColor: computed.borderTopColor,
borderRightColor: computed.borderRightColor,
borderBottomColor: computed.borderBottomColor,
borderLeftColor: computed.borderLeftColor,
// Typography
fontFamily: computed.fontFamily,
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
lineHeight: computed.lineHeight,
letterSpacing: computed.letterSpacing,
textAlign: computed.textAlign,
// Spacing
padding: computed.padding,
paddingTop: computed.paddingTop,
paddingRight: computed.paddingRight,
paddingBottom: computed.paddingBottom,
paddingLeft: computed.paddingLeft,
margin: computed.margin,
marginTop: computed.marginTop,
marginRight: computed.marginRight,
marginBottom: computed.marginBottom,
marginLeft: computed.marginLeft,
// Borders
borderRadius: computed.borderRadius,
borderTopLeftRadius: computed.borderTopLeftRadius,
borderTopRightRadius: computed.borderTopRightRadius,
borderBottomRightRadius: computed.borderBottomRightRadius,
borderBottomLeftRadius: computed.borderBottomLeftRadius,
borderWidth: computed.borderWidth,
borderStyle: computed.borderStyle,
// Effects
boxShadow: computed.boxShadow,
opacity: computed.opacity,
// Layout
display: computed.display,
position: computed.position,
width: computed.width,
height: computed.height
}
};
}, x, y);
if (!extractedData)
return null;
return {
x,
y,
...extractedData
};
}
catch (error) {
console.error(`Error extracting style at point (${x}, ${y}):`, error);
return null;
}
}
/**
* Extract styles from multiple points
*/
async extractStylesFromPoints(points) {
const results = [];
for (const point of points) {
const style = await this.extractStyleAtPoint(point.x, point.y);
if (style) {
results.push(style);
}
}
return results;
}
/**
* Extract styles from a grid pattern across the page
*/
async extractStylesFromGrid(gridSize = 10) {
const viewport = await this.page.viewport();
if (!viewport)
throw new Error('No viewport defined');
const points = [];
const stepX = viewport.width / gridSize;
const stepY = viewport.height / gridSize;
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
points.push({
x: Math.round(stepX * i + stepX / 2),
y: Math.round(stepY * j + stepY / 2)
});
}
}
return this.extractStylesFromPoints(points);
}
/**
* Create design tokens from extracted styles
*/
createDesignTokens(styles) {
const tokens = {
colors: {
primary: new Set(),
secondary: new Set(),
background: new Set(),
text: new Set(),
border: new Set()
},
typography: {
fontFamilies: new Set(),
fontSizes: new Set(),
fontWeights: new Set(),
lineHeights: new Set()
},
spacing: {
padding: new Set(),
margin: new Set()
},
borders: {
radius: new Set(),
width: new Set()
},
effects: {
shadows: new Set()
}
};
for (const style of styles) {
const { computed } = style;
// Colors
if (computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)') {
tokens.colors.background.add(computed.backgroundColor);
}
if (computed.color) {
tokens.colors.text.add(computed.color);
}
if (computed.borderColor && computed.borderColor !== 'rgba(0, 0, 0, 0)') {
tokens.colors.border.add(computed.borderColor);
}
// Typography
if (computed.fontFamily) {
tokens.typography.fontFamilies.add(computed.fontFamily);
}
if (computed.fontSize) {
tokens.typography.fontSizes.add(computed.fontSize);
}
if (computed.fontWeight) {
tokens.typography.fontWeights.add(computed.fontWeight);
}
if (computed.lineHeight && computed.lineHeight !== 'normal') {
tokens.typography.lineHeights.add(computed.lineHeight);
}
// Spacing
['Top', 'Right', 'Bottom', 'Left'].forEach(side => {
const paddingKey = `padding${side}`;
const marginKey = `margin${side}`;
if (computed[paddingKey] && computed[paddingKey] !== '0px') {
tokens.spacing.padding.add(computed[paddingKey]);
}
if (computed[marginKey] && computed[marginKey] !== '0px') {
tokens.spacing.margin.add(computed[marginKey]);
}
});
// Borders
if (computed.borderRadius && computed.borderRadius !== '0px') {
tokens.borders.radius.add(computed.borderRadius);
}
if (computed.borderWidth && computed.borderWidth !== '0px') {
tokens.borders.width.add(computed.borderWidth);
}
// Effects
if (computed.boxShadow && computed.boxShadow !== 'none') {
tokens.effects.shadows.add(computed.boxShadow);
}
}
return tokens;
}
/**
* Export styles as CSS variables
*/
exportAsCSSVariables(tokens) {
let css = ':root {\n';
// Colors
const colorCategories = ['background', 'text', 'border'];
colorCategories.forEach(category => {
const colors = Array.from(tokens.colors[category]);
colors.forEach((color, index) => {
css += ` --color-${category}-${index + 1}: ${color};\n`;
});
});
// Typography
Array.from(tokens.typography.fontFamilies).forEach((font, index) => {
css += ` --font-family-${index + 1}: ${font};\n`;
});
Array.from(tokens.typography.fontSizes).forEach((size, index) => {
css += ` --font-size-${index + 1}: ${size};\n`;
});
Array.from(tokens.typography.fontWeights).forEach((weight, index) => {
css += ` --font-weight-${index + 1}: ${weight};\n`;
});
// Spacing
Array.from(tokens.spacing.padding).forEach((value, index) => {
css += ` --spacing-padding-${index + 1}: ${value};\n`;
});
Array.from(tokens.spacing.margin).forEach((value, index) => {
css += ` --spacing-margin-${index + 1}: ${value};\n`;
});
// Borders
Array.from(tokens.borders.radius).forEach((value, index) => {
css += ` --border-radius-${index + 1}: ${value};\n`;
});
// Effects
Array.from(tokens.effects.shadows).forEach((value, index) => {
css += ` --shadow-${index + 1}: ${value};\n`;
});
css += '}';
return css;
}
/**
* Export styles as Tailwind config
*/
exportAsTailwindConfig(tokens) {
const config = {
theme: {
extend: {
colors: {
background: {},
text: {},
border: {}
},
fontFamily: {},
fontSize: {},
fontWeight: {},
spacing: {},
borderRadius: {},
boxShadow: {}
}
}
};
// Colors
Array.from(tokens.colors.background).forEach((color, index) => {
config.theme.extend.colors.background[`${index + 1}`] = color;
});
Array.from(tokens.colors.text).forEach((color, index) => {
config.theme.extend.colors.text[`${index + 1}`] = color;
});
Array.from(tokens.colors.border).forEach((color, index) => {
config.theme.extend.colors.border[`${index + 1}`] = color;
});
// Typography
Array.from(tokens.typography.fontFamilies).forEach((font, index) => {
config.theme.extend.fontFamily[`custom-${index + 1}`] = font.split(',').map(f => f.trim());
});
Array.from(tokens.typography.fontSizes).forEach((size, index) => {
config.theme.extend.fontSize[`custom-${index + 1}`] = size;
});
Array.from(tokens.typography.fontWeights).forEach((weight, index) => {
config.theme.extend.fontWeight[`custom-${index + 1}`] = weight;
});
// Spacing
const allSpacing = new Set([...tokens.spacing.padding, ...tokens.spacing.margin]);
Array.from(allSpacing).forEach((value, index) => {
config.theme.extend.spacing[`custom-${index + 1}`] = value;
});
// Border radius
Array.from(tokens.borders.radius).forEach((value, index) => {
config.theme.extend.borderRadius[`custom-${index + 1}`] = value;
});
// Shadows
Array.from(tokens.effects.shadows).forEach((value, index) => {
config.theme.extend.boxShadow[`custom-${index + 1}`] = value;
});
return `module.exports = ${JSON.stringify(config, null, 2)}`;
}
/**
* Export styles as JSON
*/
exportAsJSON(styles, tokens) {
return JSON.stringify({
extractedStyles: styles,
designTokens: {
colors: {
background: Array.from(tokens.colors.background),
text: Array.from(tokens.colors.text),
border: Array.from(tokens.colors.border)
},
typography: {
fontFamilies: Array.from(tokens.typography.fontFamilies),
fontSizes: Array.from(tokens.typography.fontSizes),
fontWeights: Array.from(tokens.typography.fontWeights),
lineHeights: Array.from(tokens.typography.lineHeights)
},
spacing: {
padding: Array.from(tokens.spacing.padding),
margin: Array.from(tokens.spacing.margin)
},
borders: {
radius: Array.from(tokens.borders.radius),
width: Array.from(tokens.borders.width)
},
effects: {
shadows: Array.from(tokens.effects.shadows)
}
}
}, null, 2);
}
}