designlang 1.0.0

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.
@@ -0,0 +1,31 @@
1
+ import { parseCSSValue, clusterValues } from '../utils.js';
2
+
3
+ export function extractBorders(computedStyles) {
4
+ const radiiSet = new Map(); // value -> count
5
+
6
+ for (const el of computedStyles) {
7
+ if (el.borderRadius && el.borderRadius !== '0px') {
8
+ const val = parseCSSValue(el.borderRadius.split(' ')[0]);
9
+ if (val && val.value > 0) {
10
+ const px = Math.round(val.value);
11
+ radiiSet.set(px, (radiiSet.get(px) || 0) + 1);
12
+ }
13
+ }
14
+ }
15
+
16
+ const sorted = [...radiiSet.keys()].sort((a, b) => a - b);
17
+ const clustered = clusterValues(sorted, 2);
18
+
19
+ const radii = clustered.map(v => {
20
+ let label;
21
+ if (v <= 2) label = 'xs';
22
+ else if (v <= 5) label = 'sm';
23
+ else if (v <= 10) label = 'md';
24
+ else if (v <= 16) label = 'lg';
25
+ else if (v <= 24) label = 'xl';
26
+ else label = 'full';
27
+ return { value: v, label, count: radiiSet.get(v) || 0 };
28
+ });
29
+
30
+ return { radii };
31
+ }
@@ -0,0 +1,33 @@
1
+ export function extractBreakpoints(mediaQueries) {
2
+ const bpSet = new Map(); // px value -> type
3
+
4
+ for (const query of mediaQueries) {
5
+ const minMatch = query.match(/min-width:\s*([\d.]+)(px|em|rem)/);
6
+ if (minMatch) {
7
+ let px = parseFloat(minMatch[1]);
8
+ if (minMatch[2] === 'em' || minMatch[2] === 'rem') px *= 16;
9
+ bpSet.set(Math.round(px), 'min-width');
10
+ }
11
+ const maxMatch = query.match(/max-width:\s*([\d.]+)(px|em|rem)/);
12
+ if (maxMatch) {
13
+ let px = parseFloat(maxMatch[1]);
14
+ if (maxMatch[2] === 'em' || maxMatch[2] === 'rem') px *= 16;
15
+ bpSet.set(Math.round(px), 'max-width');
16
+ }
17
+ }
18
+
19
+ const sorted = [...bpSet.entries()].sort((a, b) => a[0] - b[0]);
20
+
21
+ const labels = { 320: 'xs', 480: 'sm', 640: 'sm', 768: 'md', 1024: 'lg', 1280: 'xl', 1536: '2xl' };
22
+
23
+ return sorted.map(([value, type]) => {
24
+ // Find closest standard label
25
+ let label = `${value}px`;
26
+ let minDist = Infinity;
27
+ for (const [std, name] of Object.entries(labels)) {
28
+ const dist = Math.abs(value - parseInt(std));
29
+ if (dist < minDist && dist <= 64) { minDist = dist; label = name; }
30
+ }
31
+ return { value, label, type };
32
+ });
33
+ }
@@ -0,0 +1,87 @@
1
+ import { parseColor, rgbToHex, rgbToHsl, clusterColors, isSaturated } from '../utils.js';
2
+
3
+ export function extractColors(computedStyles) {
4
+ const colorMap = new Map(); // hex -> { hex, parsed, count, contexts: Set }
5
+
6
+ function addColor(value, context) {
7
+ const parsed = parseColor(value);
8
+ if (!parsed || parsed.a === 0) return;
9
+ const hex = rgbToHex(parsed);
10
+ if (!colorMap.has(hex)) {
11
+ colorMap.set(hex, { hex, parsed, count: 0, contexts: new Set() });
12
+ }
13
+ const entry = colorMap.get(hex);
14
+ entry.count++;
15
+ entry.contexts.add(context);
16
+ }
17
+
18
+ const gradients = new Set();
19
+
20
+ for (const el of computedStyles) {
21
+ addColor(el.color, 'text');
22
+ addColor(el.backgroundColor, 'background');
23
+ addColor(el.borderColor, 'border');
24
+
25
+ if (el.backgroundImage && el.backgroundImage !== 'none' && el.backgroundImage.includes('gradient')) {
26
+ gradients.add(el.backgroundImage);
27
+ }
28
+ }
29
+
30
+ const allColors = Array.from(colorMap.values());
31
+ const clusters = clusterColors(allColors, 15);
32
+
33
+ // Classify roles
34
+ const neutrals = [];
35
+ const chromatic = [];
36
+
37
+ for (const cluster of clusters) {
38
+ if (isSaturated(cluster.representative)) {
39
+ chromatic.push(cluster);
40
+ } else {
41
+ neutrals.push(cluster);
42
+ }
43
+ }
44
+
45
+ // Background colors: found on large-area elements
46
+ const bgColors = [];
47
+ for (const el of computedStyles) {
48
+ if (el.area > 50000) {
49
+ const parsed = parseColor(el.backgroundColor);
50
+ if (parsed && parsed.a > 0) bgColors.push(rgbToHex(parsed));
51
+ }
52
+ }
53
+
54
+ // Text colors: from color property
55
+ const textColors = [];
56
+ for (const el of computedStyles) {
57
+ const parsed = parseColor(el.color);
58
+ if (parsed && parsed.a > 0) {
59
+ const hex = rgbToHex(parsed);
60
+ if (!textColors.includes(hex)) textColors.push(hex);
61
+ }
62
+ }
63
+
64
+ const primary = chromatic[0] || null;
65
+ const secondary = chromatic[1] || null;
66
+ const accent = chromatic.find(c => {
67
+ const pct = c.count / allColors.reduce((s, a) => s + a.count, 0);
68
+ return pct < 0.05 && c.members.some(m => m.contexts.has('background'));
69
+ }) || chromatic[2] || null;
70
+
71
+ return {
72
+ primary: primary ? { hex: primary.hex, rgb: primary.representative, hsl: rgbToHsl(primary.representative), count: primary.count } : null,
73
+ secondary: secondary ? { hex: secondary.hex, rgb: secondary.representative, hsl: rgbToHsl(secondary.representative), count: secondary.count } : null,
74
+ accent: accent ? { hex: accent.hex, rgb: accent.representative, hsl: rgbToHsl(accent.representative), count: accent.count } : null,
75
+ neutrals: neutrals.map(c => ({ hex: c.hex, rgb: c.representative, hsl: rgbToHsl(c.representative), count: c.count })),
76
+ backgrounds: [...new Set(bgColors)],
77
+ text: textColors.slice(0, 10),
78
+ gradients: [...gradients],
79
+ all: clusters.map(c => ({
80
+ hex: c.hex,
81
+ rgb: c.representative,
82
+ hsl: rgbToHsl(c.representative),
83
+ count: c.count,
84
+ contexts: [...new Set(c.members.flatMap(m => [...m.contexts]))],
85
+ })),
86
+ };
87
+ }
@@ -0,0 +1,66 @@
1
+ export function extractComponents(computedStyles) {
2
+ const components = {};
3
+
4
+ // Buttons
5
+ const buttons = computedStyles.filter(el =>
6
+ el.tag === 'button' || el.role === 'button' ||
7
+ (el.tag === 'a' && /btn|button/i.test(el.classList))
8
+ );
9
+ if (buttons.length > 0) {
10
+ components.buttons = {
11
+ count: buttons.length,
12
+ baseStyle: mostCommonStyle(buttons, ['backgroundColor', 'color', 'fontSize', 'fontWeight', 'paddingTop', 'paddingRight', 'borderRadius']),
13
+ };
14
+ }
15
+
16
+ // Cards
17
+ const cards = computedStyles.filter(el =>
18
+ /card/i.test(el.classList) ||
19
+ (el.tag === 'div' && el.boxShadow !== 'none' && el.borderRadius !== '0px' && el.backgroundColor !== 'rgba(0, 0, 0, 0)')
20
+ );
21
+ if (cards.length > 0) {
22
+ components.cards = {
23
+ count: cards.length,
24
+ baseStyle: mostCommonStyle(cards, ['backgroundColor', 'borderRadius', 'boxShadow', 'paddingTop', 'paddingRight']),
25
+ };
26
+ }
27
+
28
+ // Inputs
29
+ const inputs = computedStyles.filter(el =>
30
+ ['input', 'textarea', 'select'].includes(el.tag)
31
+ );
32
+ if (inputs.length > 0) {
33
+ components.inputs = {
34
+ count: inputs.length,
35
+ baseStyle: mostCommonStyle(inputs, ['backgroundColor', 'color', 'borderColor', 'borderRadius', 'fontSize', 'paddingTop', 'paddingRight']),
36
+ };
37
+ }
38
+
39
+ // Links
40
+ const links = computedStyles.filter(el => el.tag === 'a');
41
+ if (links.length > 0) {
42
+ components.links = {
43
+ count: links.length,
44
+ baseStyle: mostCommonStyle(links, ['color', 'fontSize', 'fontWeight']),
45
+ };
46
+ }
47
+
48
+ return components;
49
+ }
50
+
51
+ function mostCommonStyle(elements, properties) {
52
+ const style = {};
53
+ for (const prop of properties) {
54
+ const counts = new Map();
55
+ for (const el of elements) {
56
+ const val = el[prop];
57
+ if (val && val !== 'none' && val !== 'auto' && val !== 'normal' && val !== 'rgba(0, 0, 0, 0)') {
58
+ counts.set(val, (counts.get(val) || 0) + 1);
59
+ }
60
+ }
61
+ if (counts.size > 0) {
62
+ style[prop] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
63
+ }
64
+ }
65
+ return style;
66
+ }
@@ -0,0 +1,27 @@
1
+ export function extractShadows(computedStyles) {
2
+ const shadowSet = new Set();
3
+
4
+ for (const el of computedStyles) {
5
+ if (el.boxShadow && el.boxShadow !== 'none') {
6
+ shadowSet.add(el.boxShadow);
7
+ }
8
+ }
9
+
10
+ const values = [...shadowSet].map(raw => {
11
+ // Parse: offset-x offset-y blur spread color [inset]
12
+ const inset = raw.includes('inset');
13
+ // Approximate blur from the string for classification
14
+ const nums = raw.match(/([\d.]+)px/g)?.map(n => parseFloat(n)) || [];
15
+ const blur = nums[2] || 0;
16
+ let label = 'md';
17
+ if (blur <= 2) label = 'xs';
18
+ else if (blur <= 6) label = 'sm';
19
+ else if (blur <= 15) label = 'md';
20
+ else if (blur <= 30) label = 'lg';
21
+ else label = 'xl';
22
+ return { raw, blur, inset, label };
23
+ });
24
+
25
+ values.sort((a, b) => a.blur - b.blur);
26
+ return { values };
27
+ }
@@ -0,0 +1,37 @@
1
+ import { parseCSSValue, clusterValues, detectScale } from '../utils.js';
2
+
3
+ export function extractSpacing(computedStyles) {
4
+ const allValues = new Set();
5
+
6
+ for (const el of computedStyles) {
7
+ for (const prop of ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'gap']) {
8
+ const val = parseCSSValue(el[prop]);
9
+ if (val && val.value > 0 && val.value < 500) {
10
+ allValues.add(Math.round(val.value));
11
+ }
12
+ }
13
+ }
14
+
15
+ const sorted = [...allValues].sort((a, b) => a - b);
16
+ const clustered = clusterValues(sorted, 2);
17
+ const { base, scale } = detectScale(clustered);
18
+
19
+ // Build named tokens
20
+ const tokens = {};
21
+ if (base) {
22
+ for (const v of scale) {
23
+ const step = v / base;
24
+ if (Number.isInteger(step)) {
25
+ tokens[String(step)] = `${v}px`;
26
+ } else {
27
+ tokens[`${v}px`] = `${v}px`;
28
+ }
29
+ }
30
+ } else {
31
+ for (let i = 0; i < scale.length; i++) {
32
+ tokens[String(i)] = `${scale[i]}px`;
33
+ }
34
+ }
35
+
36
+ return { base, scale, tokens, raw: sorted };
37
+ }
@@ -0,0 +1,72 @@
1
+ import { parseCSSValue } from '../utils.js';
2
+
3
+ export function extractTypography(computedStyles) {
4
+ const familyCount = new Map();
5
+ const sizeEntries = [];
6
+ const weightCount = new Map();
7
+
8
+ for (const el of computedStyles) {
9
+ // Font families
10
+ const family = el.fontFamily?.replace(/["']/g, '').split(',')[0]?.trim();
11
+ if (family) familyCount.set(family, (familyCount.get(family) || 0) + 1);
12
+
13
+ // Font sizes
14
+ const sizeVal = parseCSSValue(el.fontSize);
15
+ if (sizeVal) {
16
+ sizeEntries.push({
17
+ size: sizeVal.value,
18
+ weight: el.fontWeight,
19
+ lineHeight: el.lineHeight,
20
+ letterSpacing: el.letterSpacing,
21
+ tag: el.tag,
22
+ family,
23
+ });
24
+ }
25
+
26
+ // Weights
27
+ if (el.fontWeight) weightCount.set(el.fontWeight, (weightCount.get(el.fontWeight) || 0) + 1);
28
+ }
29
+
30
+ // Unique font families sorted by usage
31
+ const families = [...familyCount.entries()]
32
+ .sort((a, b) => b[1] - a[1])
33
+ .map(([name, count]) => {
34
+ const usedOn = computedStyles
35
+ .filter(el => el.fontFamily?.includes(name))
36
+ .map(el => el.tag);
37
+ const headingUse = usedOn.some(t => /^h[1-6]$/.test(t));
38
+ const bodyUse = usedOn.some(t => ['p', 'span', 'li', 'div'].includes(t));
39
+ return { name, count, usage: headingUse && bodyUse ? 'all' : headingUse ? 'headings' : 'body' };
40
+ });
41
+
42
+ // Build type scale from unique sizes
43
+ const sizeMap = new Map();
44
+ for (const entry of sizeEntries) {
45
+ const key = entry.size;
46
+ if (!sizeMap.has(key)) {
47
+ sizeMap.set(key, { size: entry.size, weight: entry.weight, lineHeight: entry.lineHeight, letterSpacing: entry.letterSpacing, tags: new Set(), count: 0 });
48
+ }
49
+ const s = sizeMap.get(key);
50
+ s.tags.add(entry.tag);
51
+ s.count++;
52
+ }
53
+
54
+ const scale = [...sizeMap.values()]
55
+ .sort((a, b) => b.size - a.size)
56
+ .map(s => ({ ...s, tags: [...s.tags] }));
57
+
58
+ // Identify heading sizes (from h1-h6 tags)
59
+ const headings = scale.filter(s => s.tags.some(t => /^h[1-6]$/.test(t)));
60
+
61
+ // Body text: most common size on p/span/li
62
+ const bodyEntries = sizeEntries.filter(e => ['p', 'span', 'li'].includes(e.tag));
63
+ const bodySizeCount = new Map();
64
+ for (const e of bodyEntries) bodySizeCount.set(e.size, (bodySizeCount.get(e.size) || 0) + 1);
65
+ const bodySize = [...bodySizeCount.entries()].sort((a, b) => b[1] - a[1])[0];
66
+ const body = bodySize ? scale.find(s => s.size === bodySize[0]) : null;
67
+
68
+ // Weights
69
+ const weights = [...weightCount.entries()].sort((a, b) => b[1] - a[1]).map(([w, count]) => ({ weight: w, count }));
70
+
71
+ return { families, scale, headings, body, weights };
72
+ }
@@ -0,0 +1,22 @@
1
+ export function extractVariables(cssVariables) {
2
+ const categories = { colors: {}, spacing: {}, typography: {}, shadows: {}, radii: {}, other: {} };
3
+
4
+ for (const [name, value] of Object.entries(cssVariables)) {
5
+ const lower = name.toLowerCase();
6
+ if (/color|bg|foreground|primary|secondary|accent|muted|border|ring|destructive|card|popover|chart/.test(lower)) {
7
+ categories.colors[name] = value;
8
+ } else if (/spacing|gap|padding|margin|space|size/.test(lower)) {
9
+ categories.spacing[name] = value;
10
+ } else if (/font|text|line-height|letter|tracking|leading/.test(lower)) {
11
+ categories.typography[name] = value;
12
+ } else if (/shadow/.test(lower)) {
13
+ categories.shadows[name] = value;
14
+ } else if (/radius|rounded/.test(lower)) {
15
+ categories.radii[name] = value;
16
+ } else {
17
+ categories.other[name] = value;
18
+ }
19
+ }
20
+
21
+ return categories;
22
+ }
@@ -0,0 +1,103 @@
1
+ import { pxToRem } from '../utils.js';
2
+
3
+ export function formatCssVars(design) {
4
+ const lines = [':root {'];
5
+
6
+ // Colors
7
+ lines.push(' /* Colors — Primary */');
8
+ if (design.colors.primary) lines.push(` --color-primary: ${design.colors.primary.hex};`);
9
+ if (design.colors.secondary) lines.push(` --color-secondary: ${design.colors.secondary.hex};`);
10
+ if (design.colors.accent) lines.push(` --color-accent: ${design.colors.accent.hex};`);
11
+ lines.push('');
12
+
13
+ if (design.colors.neutrals.length > 0) {
14
+ lines.push(' /* Colors — Neutrals */');
15
+ for (let i = 0; i < design.colors.neutrals.length && i < 10; i++) {
16
+ lines.push(` --color-neutral-${i * 100 || 50}: ${design.colors.neutrals[i].hex};`);
17
+ }
18
+ lines.push('');
19
+ }
20
+
21
+ if (design.colors.backgrounds.length > 0) {
22
+ lines.push(' /* Colors — Backgrounds */');
23
+ for (let i = 0; i < design.colors.backgrounds.length; i++) {
24
+ lines.push(` --color-bg${i > 0 ? `-${i}` : ''}: ${design.colors.backgrounds[i]};`);
25
+ }
26
+ lines.push('');
27
+ }
28
+
29
+ if (design.colors.text.length > 0) {
30
+ lines.push(' /* Colors — Text */');
31
+ for (let i = 0; i < design.colors.text.length && i < 5; i++) {
32
+ lines.push(` --color-text${i > 0 ? `-${i}` : ''}: ${design.colors.text[i]};`);
33
+ }
34
+ lines.push('');
35
+ }
36
+
37
+ // Typography
38
+ if (design.typography.families.length > 0) {
39
+ lines.push(' /* Typography — Families */');
40
+ for (let i = 0; i < design.typography.families.length; i++) {
41
+ const f = design.typography.families[i];
42
+ let key;
43
+ if (f.usage === 'headings') key = 'heading';
44
+ else if (f.usage === 'body') key = 'body';
45
+ else if (i === 0) key = 'sans';
46
+ else if (f.name.toLowerCase().includes('mono')) key = 'mono';
47
+ else key = i === 1 ? 'heading' : `font-${i}`;
48
+ lines.push(` --font-${key}: '${f.name}', sans-serif;`);
49
+ }
50
+ lines.push('');
51
+ }
52
+
53
+ if (design.typography.scale.length > 0) {
54
+ lines.push(' /* Typography — Scale */');
55
+ for (const s of design.typography.scale.slice(0, 12)) {
56
+ lines.push(` --font-size-${s.size}: ${s.size}px;`);
57
+ }
58
+ lines.push('');
59
+ }
60
+
61
+ // Spacing
62
+ if (design.spacing.scale.length > 0) {
63
+ lines.push(' /* Spacing */');
64
+ for (const v of design.spacing.scale.slice(0, 20)) {
65
+ lines.push(` --spacing-${v}: ${v}px;`);
66
+ }
67
+ lines.push('');
68
+ }
69
+
70
+ // Border radius
71
+ if (design.borders.radii.length > 0) {
72
+ lines.push(' /* Border Radius */');
73
+ for (const r of design.borders.radii) {
74
+ lines.push(` --radius-${r.label}: ${r.value}px;`);
75
+ }
76
+ lines.push('');
77
+ }
78
+
79
+ // Shadows
80
+ if (design.shadows.values.length > 0) {
81
+ lines.push(' /* Box Shadows */');
82
+ for (const s of design.shadows.values) {
83
+ lines.push(` --shadow-${s.label}: ${s.raw};`);
84
+ }
85
+ lines.push('');
86
+ }
87
+
88
+ // Site's own CSS variables
89
+ const siteVars = Object.entries(design.variables).filter(([, v]) => Object.keys(v).length > 0);
90
+ if (siteVars.length > 0) {
91
+ lines.push(' /* Original Site Variables */');
92
+ for (const [category, vars] of siteVars) {
93
+ lines.push(` /* — ${category} — */`);
94
+ for (const [name, value] of Object.entries(vars)) {
95
+ lines.push(` ${name}: ${value};`);
96
+ }
97
+ }
98
+ lines.push('');
99
+ }
100
+
101
+ lines.push('}');
102
+ return lines.join('\n');
103
+ }