conductor-figma 1.0.2 → 3.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.
@@ -1,341 +1,320 @@
1
1
  // ═══════════════════════════════════════════
2
- // CONDUCTOR — Design Intelligence
2
+ // CONDUCTOR v3 — Design Intelligence Engine
3
3
  // ═══════════════════════════════════════════
4
- // Pure functions for design decisions. No Figma dependency.
5
- // These encode the rules a senior designer applies instinctively.
4
+ // This is what separates Conductor from every other Figma MCP.
5
+ // Every tool has design intelligence built in.
6
6
 
7
7
  // ─── 8px Grid ───
8
-
9
- export const BASE_UNITS = [4, 8];
10
-
11
- export function snapToGrid(value, base = 8) {
12
- return Math.round(value / base) * base;
13
- }
14
-
15
- export function isOnGrid(value, base = 8) {
16
- return value % base === 0;
17
- }
18
-
19
- export function generateSpacingScale(base = 8, steps = 12) {
20
- const scale = [];
21
- for (let i = 1; i <= steps; i++) {
22
- scale.push(base * i);
23
- }
24
- return scale;
25
- }
26
-
27
- export function findNearestGridValue(value, base = 8) {
28
- const snapped = snapToGrid(value, base);
29
- return { original: value, snapped, diff: Math.abs(value - snapped), onGrid: value === snapped };
30
- }
31
-
32
- export function auditSpacing(values, base = 8) {
33
- const results = values.map(v => findNearestGridValue(v, base));
34
- const onGrid = results.filter(r => r.onGrid).length;
35
- return {
36
- total: values.length,
37
- onGrid,
38
- offGrid: values.length - onGrid,
39
- adherence: values.length > 0 ? onGrid / values.length : 1,
40
- issues: results.filter(r => !r.onGrid),
41
- fixes: results.filter(r => !r.onGrid).map(r => ({ from: r.original, to: r.snapped })),
42
- };
43
- }
8
+ export function snap(v, grid = 8) { return Math.round(v / grid) * grid }
9
+ export function snapUp(v, grid = 8) { return Math.ceil(v / grid) * grid }
44
10
 
45
11
  // ─── Type Scale ───
46
-
47
- export const TYPE_SCALES = {
48
- 'minor-second': { name: 'Minor Second', ratio: 1.067 },
49
- 'major-second': { name: 'Major Second', ratio: 1.125 },
50
- 'minor-third': { name: 'Minor Third', ratio: 1.2 },
51
- 'major-third': { name: 'Major Third', ratio: 1.25 },
52
- 'perfect-fourth': { name: 'Perfect Fourth', ratio: 1.333 },
53
- 'augmented-fourth':{ name: 'Augmented Fourth', ratio: 1.414 },
54
- 'perfect-fifth': { name: 'Perfect Fifth', ratio: 1.5 },
55
- 'golden-ratio': { name: 'Golden Ratio', ratio: 1.618 },
56
- };
57
-
58
- export function generateTypeScale(baseFontSize = 16, scaleKey = 'major-third', steps = { down: 2, up: 6 }) {
59
- const scale = TYPE_SCALES[scaleKey] || TYPE_SCALES['major-third'];
60
- const sizes = [];
61
-
62
- for (let i = -steps.down; i <= steps.up; i++) {
63
- const size = Math.round(baseFontSize * Math.pow(scale.ratio, i) * 100) / 100;
64
- const snapped = Math.round(size);
65
- sizes.push({ step: i, raw: size, size: snapped, label: getTypeLabel(i) });
66
- }
67
-
68
- return { scale: scale.name, ratio: scale.ratio, baseFontSize, sizes };
69
- }
70
-
71
- function getTypeLabel(step) {
72
- const labels = { '-2': 'xs', '-1': 'sm', 0: 'base', 1: 'md', 2: 'lg', 3: 'xl', 4: '2xl', 5: '3xl', 6: '4xl' };
73
- return labels[String(step)] || `step-${step}`;
12
+ const TYPE_SCALES = {
13
+ minor2: 1.067, major2: 1.125, minor3: 1.200,
14
+ major3: 1.250, perfect4: 1.333, aug4: 1.414,
15
+ perfect5: 1.500, golden: 1.618,
74
16
  }
75
17
 
76
- export function detectTypeScale(fontSizes) {
77
- if (fontSizes.length < 3) return { detected: false, scale: null, ratio: null };
78
- const sorted = [...fontSizes].sort((a, b) => a - b);
79
- const ratios = [];
80
- for (let i = 1; i < sorted.length; i++) {
81
- ratios.push(sorted[i] / sorted[i - 1]);
18
+ export function typeScale(base = 16, ratio = 'major2', steps = 8) {
19
+ const r = TYPE_SCALES[ratio] || parseFloat(ratio) || 1.125
20
+ const scale = {}
21
+ const names = ['xs','sm','base','md','lg','xl','2xl','3xl','4xl','5xl']
22
+ for (let i = -2; i < steps; i++) {
23
+ const size = Math.round(base * Math.pow(r, i))
24
+ scale[names[i + 2] || `${i + 2}xl`] = size
82
25
  }
83
- const avgRatio = ratios.reduce((s, r) => s + r, 0) / ratios.length;
26
+ return scale
27
+ }
84
28
 
85
- let closest = null;
86
- let closestDist = Infinity;
87
- for (const [key, val] of Object.entries(TYPE_SCALES)) {
88
- const dist = Math.abs(avgRatio - val.ratio);
89
- if (dist < closestDist) { closestDist = dist; closest = { key, ...val }; }
29
+ // ─── Semantic Colors ───
30
+ export function semanticColors(brand = '#6366f1', mode = 'dark') {
31
+ const hex2rgb = h => { const n = parseInt(h.replace('#',''), 16); return [(n>>16)&255,(n>>8)&255,n&255] }
32
+ const rgb2hex = (r,g,b) => '#' + [r,g,b].map(x => x.toString(16).padStart(2,'0')).join('')
33
+ const [br,bg,bb] = hex2rgb(brand)
34
+
35
+ if (mode === 'dark') return {
36
+ bg: '#09090f', bg2: '#0f0f1c', bg3: '#14142a',
37
+ surface: '#12122a', surface2: '#16163a', surface3: '#1a1a42',
38
+ border: '#1e1e3a', border2: '#282850', border3: '#323268',
39
+ text1: '#f0f0f8', text2: '#a0a0b8', text3: '#686880',
40
+ brand, brandDim: rgb2hex(Math.round(br*.3), Math.round(bg*.3), Math.round(bb*.3)),
41
+ success: '#4ade80', warning: '#fbbf24', error: '#f87171', info: '#60a5fa',
90
42
  }
91
-
92
43
  return {
93
- detected: closestDist < 0.1,
94
- scale: closest,
95
- avgRatio: Math.round(avgRatio * 1000) / 1000,
96
- sizes: sorted,
97
- };
98
- }
99
-
100
- export function getLineHeight(fontSize) {
101
- if (fontSize <= 14) return 1.6;
102
- if (fontSize <= 20) return 1.5;
103
- if (fontSize <= 32) return 1.3;
104
- if (fontSize <= 48) return 1.15;
105
- return 1.1;
106
- }
107
-
108
- export function getFontWeight(level) {
109
- const weights = { heading: 700, subheading: 600, body: 400, caption: 400, label: 500 };
110
- return weights[level] || 400;
111
- }
112
-
113
- export function checkMeasure(charCount) {
114
- return { charCount, optimal: charCount >= 45 && charCount <= 75, tooNarrow: charCount < 45, tooWide: charCount > 75 };
115
- }
116
-
117
- // ─── Color ───
118
-
119
- export function hexToRgb(hex) {
120
- hex = hex.replace('#', '');
121
- if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
122
- return { r: parseInt(hex.substr(0,2),16), g: parseInt(hex.substr(2,2),16), b: parseInt(hex.substr(4,2),16) };
123
- }
124
-
125
- export function rgbToHex(r, g, b) {
126
- return '#' + [r, g, b].map(c => Math.max(0, Math.min(255, Math.round(c))).toString(16).padStart(2, '0')).join('');
127
- }
128
-
129
- export function hexToHsl(hex) {
130
- const { r, g, b } = hexToRgb(hex);
131
- const rf = r/255, gf = g/255, bf = b/255;
132
- const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
133
- let h, s, l = (max + min) / 2;
134
- if (max === min) { h = s = 0; }
135
- else {
136
- const d = max - min;
137
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
138
- if (max === rf) h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
139
- else if (max === gf) h = ((bf - rf) / d + 2) / 6;
140
- else h = ((rf - gf) / d + 4) / 6;
44
+ bg: '#ffffff', bg2: '#f9f9fb', bg3: '#f3f3f7',
45
+ surface: '#ffffff', surface2: '#f5f5fa', surface3: '#ededf5',
46
+ border: '#e4e4ec', border2: '#d0d0dd', border3: '#b8b8cc',
47
+ text1: '#111118', text2: '#55556a', text3: '#88889a',
48
+ brand, brandDim: rgb2hex(Math.min(255,br+180), Math.min(255,bg+180), Math.min(255,bb+180)),
49
+ success: '#16a34a', warning: '#d97706', error: '#dc2626', info: '#2563eb',
141
50
  }
142
- return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
143
51
  }
144
52
 
145
- export function hslToHex(h, s, l) {
146
- s /= 100; l /= 100;
147
- const a = s * Math.min(l, 1 - l);
148
- const f = (n) => {
149
- const k = (n + h / 30) % 12;
150
- return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
151
- };
152
- return rgbToHex(Math.round(f(0)*255), Math.round(f(8)*255), Math.round(f(4)*255));
153
- }
53
+ // ─── Spacing System ───
54
+ export const SPACING = { none:0, xs:4, sm:8, md:16, lg:24, xl:32, '2xl':48, '3xl':64, '4xl':96 }
154
55
 
155
- export function generatePalette(baseHex, steps = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]) {
156
- const { h, s } = hexToHsl(baseHex);
157
- return steps.map(step => {
158
- const l = step <= 50 ? 97 : step >= 950 ? 8 : Math.round(100 - (step / 10));
159
- const satAdj = step <= 100 || step >= 900 ? Math.max(0, s - 10) : s;
160
- return { step, hex: hslToHex(h, satAdj, l) };
161
- });
162
- }
56
+ // ─── Corner Radius ───
57
+ export const RADIUS = { none:0, xs:4, sm:6, md:8, lg:12, xl:16, '2xl':20, '3xl':24, full:9999 }
163
58
 
164
- export function generateSemanticColors(brandHex) {
165
- const { h } = hexToHsl(brandHex);
166
- return {
167
- primary: brandHex,
168
- surface: hslToHex(h, 5, 98),
169
- surfaceAlt:hslToHex(h, 5, 95),
170
- border: hslToHex(h, 8, 88),
171
- text: hslToHex(h, 10, 15),
172
- textMuted: hslToHex(h, 6, 45),
173
- accent: brandHex,
174
- success: '#16a34a',
175
- warning: '#d97706',
176
- danger: '#dc2626',
177
- info: '#2563eb',
178
- };
59
+ // ─── Shadows ───
60
+ export const SHADOWS = {
61
+ sm: { color:'#00000015', offset:{x:0,y:1}, blur:3, spread:0 },
62
+ md: { color:'#00000020', offset:{x:0,y:4}, blur:8, spread:-2 },
63
+ lg: { color:'#00000025', offset:{x:0,y:8}, blur:24, spread:-4 },
64
+ xl: { color:'#00000030', offset:{x:0,y:20}, blur:48, spread:-8 },
179
65
  }
180
66
 
181
- export function generateDarkMode(lightColors) {
182
- const result = {};
183
- for (const [key, hex] of Object.entries(lightColors)) {
184
- if (typeof hex !== 'string' || !hex.startsWith('#')) { result[key] = hex; continue; }
185
- const { h, s, l } = hexToHsl(hex);
186
- result[key] = hslToHex(h, Math.min(s, 80), 100 - l);
67
+ // ─── Component Intelligence ───
68
+ // When an AI says "create a button", this knows what that means.
69
+ export function componentDefaults(type, variant = 'default') {
70
+ const defs = {
71
+ button: {
72
+ default: { h:44, px:20, py:0, radius:10, fontSize:15, fontWeight:'Semi Bold', minW:100, touchTarget:44 },
73
+ sm: { h:36, px:14, py:0, radius:8, fontSize:13, fontWeight:'Medium', minW:72, touchTarget:44 },
74
+ lg: { h:52, px:28, py:0, radius:12, fontSize:17, fontWeight:'Semi Bold', minW:120, touchTarget:52 },
75
+ icon: { h:40, px:10, py:0, radius:10, fontSize:18, fontWeight:'Medium', minW:40, touchTarget:44 },
76
+ },
77
+ input: {
78
+ default: { h:44, px:14, py:0, radius:8, fontSize:15, fontWeight:'Regular', borderW:1.5 },
79
+ sm: { h:36, px:12, py:0, radius:6, fontSize:13, fontWeight:'Regular', borderW:1 },
80
+ lg: { h:52, px:16, py:0, radius:10, fontSize:17, fontWeight:'Regular', borderW:1.5 },
81
+ },
82
+ card: {
83
+ default: { px:24, py:24, radius:16, gap:16, borderW:1 },
84
+ compact: { px:16, py:16, radius:12, gap:12, borderW:1 },
85
+ spacious: { px:32, py:32, radius:20, gap:20, borderW:1 },
86
+ },
87
+ avatar: {
88
+ default: { size:40, radius:9999, fontSize:16, fontWeight:'Semi Bold' },
89
+ sm: { size:32, radius:9999, fontSize:13, fontWeight:'Semi Bold' },
90
+ lg: { size:56, radius:9999, fontSize:22, fontWeight:'Semi Bold' },
91
+ },
92
+ badge: {
93
+ default: { h:24, px:10, py:0, radius:6, fontSize:11, fontWeight:'Semi Bold' },
94
+ pill: { h:24, px:10, py:0, radius:9999, fontSize:11, fontWeight:'Semi Bold' },
95
+ },
96
+ nav: {
97
+ default: { h:64, px:24, gap:24, fontSize:14, fontWeight:'Medium' },
98
+ compact: { h:52, px:16, gap:16, fontSize:13, fontWeight:'Medium' },
99
+ },
100
+ section: {
101
+ default: { py:96, px:48, gap:48, maxW:1120 },
102
+ compact: { py:64, px:32, gap:32, maxW:1120 },
103
+ hero: { py:120, px:48, gap:36, maxW:1120 },
104
+ },
105
+ modal: {
106
+ default: { px:28, py:28, radius:20, gap:20, maxW:480, borderW:1 },
107
+ lg: { px:36, py:36, radius:24, gap:24, maxW:640, borderW:1 },
108
+ },
109
+ sidebar: {
110
+ default: { w:260, px:16, py:20, gap:4, fontSize:14 },
111
+ compact: { w:220, px:12, py:16, gap:2, fontSize:13 },
112
+ wide: { w:300, px:20, py:24, gap:4, fontSize:14 },
113
+ },
114
+ toast: {
115
+ default: { h:48, px:16, py:0, radius:10, fontSize:14, fontWeight:'Medium', gap:10 },
116
+ },
117
+ tooltip: {
118
+ default: { px:10, py:6, radius:6, fontSize:12, fontWeight:'Medium' },
119
+ },
120
+ chip: {
121
+ default: { h:32, px:12, py:0, radius:8, fontSize:13, fontWeight:'Medium', gap:6 },
122
+ },
123
+ divider: {
124
+ default: { h:1, color:'border' },
125
+ },
126
+ skeleton: {
127
+ default: { radius:8, color:'surface2' },
128
+ },
129
+ progress: {
130
+ default: { h:8, radius:4 },
131
+ thin: { h:4, radius:2 },
132
+ },
133
+ switch: {
134
+ default: { w:44, h:24, radius:12, thumbSize:20, touchTarget:44 },
135
+ },
136
+ checkbox: {
137
+ default: { size:20, radius:4, borderW:2, touchTarget:44 },
138
+ },
139
+ radio: {
140
+ default: { size:20, radius:9999, borderW:2, touchTarget:44 },
141
+ },
142
+ table: {
143
+ default: { cellPx:16, cellPy:12, headerFontSize:12, bodyFontSize:14, headerWeight:'Semi Bold', borderW:1 },
144
+ },
145
+ tabs: {
146
+ default: { h:44, px:16, gap:4, fontSize:14, fontWeight:'Medium', indicatorH:2, indicatorRadius:1 },
147
+ },
148
+ dropdown: {
149
+ default: { itemH:40, px:12, py:8, radius:12, fontSize:14, gap:2, shadow:'lg' },
150
+ },
187
151
  }
188
- return result;
152
+ const comp = defs[type]
153
+ if (!comp) return null
154
+ return comp[variant] || comp.default || null
189
155
  }
190
156
 
191
- export function relativeLuminance(hex) {
192
- const { r, g, b } = hexToRgb(hex);
193
- const [rs, gs, bs] = [r, g, b].map(c => {
194
- c = c / 255;
195
- return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
196
- });
197
- return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
198
- }
199
-
200
- export function contrastRatio(hex1, hex2) {
201
- const l1 = relativeLuminance(hex1);
202
- const l2 = relativeLuminance(hex2);
203
- const lighter = Math.max(l1, l2);
204
- const darker = Math.min(l1, l2);
205
- return (lighter + 0.05) / (darker + 0.05);
157
+ // ─── Layout Intelligence ───
158
+ export function suggestAutoLayout(intent) {
159
+ const layouts = {
160
+ 'row': { direction:'HORIZONTAL', gap:snap(12), align:'CENTER' },
161
+ 'column': { direction:'VERTICAL', gap:snap(16), align:'STRETCH' },
162
+ 'center': { direction:'VERTICAL', gap:snap(16), align:'CENTER', justify:'CENTER' },
163
+ 'spread': { direction:'HORIZONTAL', gap:0, align:'CENTER', justify:'SPACE_BETWEEN' },
164
+ 'stack': { direction:'VERTICAL', gap:0, align:'STRETCH' },
165
+ 'wrap': { direction:'HORIZONTAL', gap:snap(12), align:'CENTER', wrap:true },
166
+ 'grid-2': { direction:'HORIZONTAL', gap:snap(16), align:'STRETCH', childWidth:'FILL' },
167
+ 'grid-3': { direction:'HORIZONTAL', gap:snap(16), align:'STRETCH', childWidth:'FILL' },
168
+ 'grid-4': { direction:'HORIZONTAL', gap:snap(16), align:'STRETCH', childWidth:'FILL' },
169
+ 'sidebar': { direction:'HORIZONTAL', gap:0, align:'STRETCH' },
170
+ 'header': { direction:'HORIZONTAL', gap:snap(16), align:'CENTER', px:snap(24) },
171
+ 'card-row': { direction:'HORIZONTAL', gap:snap(20), align:'STRETCH' },
172
+ 'form': { direction:'VERTICAL', gap:snap(16), align:'STRETCH' },
173
+ }
174
+ return layouts[intent] || layouts.column
206
175
  }
207
176
 
208
- export function checkContrast(fgHex, bgHex) {
209
- const ratio = contrastRatio(fgHex, bgHex);
177
+ // ─── Accessibility ───
178
+ export function checkContrast(fg, bg) {
179
+ const hex2lum = h => {
180
+ const [r,g,b] = [1,3,5].map(i => { let c = parseInt(h.slice(i,i+2),16)/255; return c<=0.03928?c/12.92:Math.pow((c+0.055)/1.055,2.4) })
181
+ return 0.2126*r + 0.7152*g + 0.0722*b
182
+ }
183
+ const l1 = hex2lum(fg.replace('#',''))
184
+ const l2 = hex2lum(bg.replace('#',''))
185
+ const ratio = (Math.max(l1,l2)+0.05)/(Math.min(l1,l2)+0.05)
210
186
  return {
211
- ratio: Math.round(ratio * 100) / 100,
187
+ ratio: Math.round(ratio*100)/100,
212
188
  aa: ratio >= 4.5,
213
- aaLarge: ratio >= 3,
214
189
  aaa: ratio >= 7,
215
- aaaLarge: ratio >= 4.5,
216
- };
217
- }
218
-
219
- // ─── Shadow / Elevation ───
220
-
221
- export function generateElevation(steps = ['sm', 'md', 'lg', 'xl', '2xl']) {
222
- const configs = {
223
- sm: { y: 1, blur: 2, spread: 0, opacity: 0.05 },
224
- md: { y: 2, blur: 4, spread: -1, opacity: 0.06 },
225
- lg: { y: 4, blur: 8, spread: -2, opacity: 0.08 },
226
- xl: { y: 8, blur: 16, spread: -4, opacity: 0.1 },
227
- '2xl': { y: 16, blur: 32, spread: -8, opacity: 0.12 },
228
- };
229
- return steps.map(step => ({ step, ...configs[step], css: `0 ${configs[step].y}px ${configs[step].blur}px ${configs[step].spread}px rgba(0,0,0,${configs[step].opacity})` }));
230
- }
231
-
232
- // ─── Border Radius ───
233
-
234
- export function generateRadiusScale(base = 4) {
235
- return [
236
- { name: 'none', value: 0 },
237
- { name: 'sm', value: base },
238
- { name: 'md', value: base * 2 },
239
- { name: 'lg', value: base * 3 },
240
- { name: 'xl', value: base * 4 },
241
- { name: '2xl', value: base * 6 },
242
- { name: 'full', value: 9999 },
243
- ];
190
+ aaLarge: ratio >= 3,
191
+ }
244
192
  }
245
193
 
246
- // ─── Hierarchy ───
247
-
248
- export function assessHierarchy(elements) {
249
- // elements: [{ type: 'heading'|'subheading'|'body'|'button'|'caption', fontSize, fontWeight, color }]
250
- const sorted = [...elements].sort((a, b) => {
251
- const weightA = (a.fontSize || 16) * (a.fontWeight || 400) / 400;
252
- const weightB = (b.fontSize || 16) * (b.fontWeight || 400) / 400;
253
- return weightB - weightA;
254
- });
255
-
256
- const issues = [];
257
- for (let i = 0; i < sorted.length - 1; i++) {
258
- const curr = sorted[i];
259
- const next = sorted[i + 1];
260
- const currWeight = (curr.fontSize || 16) * (curr.fontWeight || 400) / 400;
261
- const nextWeight = (next.fontSize || 16) * (next.fontWeight || 400) / 400;
262
- if (currWeight / nextWeight < 1.1) {
263
- issues.push({ a: curr, b: next, reason: 'Insufficient visual weight difference between levels' });
264
- }
194
+ export function auditAccessibility(node) {
195
+ const issues = []
196
+ // Touch target
197
+ if (node.type === 'button' || node.type === 'input' || node.type === 'link') {
198
+ if ((node.height || 0) < 44) issues.push({ severity:'error', rule:'touch-target', message:`Touch target ${node.height}px is below 44px minimum`, fix:{ height:44 } })
265
199
  }
266
-
267
- return { elements: sorted, issues, score: issues.length === 0 ? 100 : Math.max(0, 100 - issues.length * 20) };
200
+ // Text contrast
201
+ if (node.type === 'text' && node.color && node.bgColor) {
202
+ const c = checkContrast(node.color, node.bgColor)
203
+ if (!c.aa && (node.fontSize || 16) < 18) issues.push({ severity:'error', rule:'contrast-aa', message:`Contrast ratio ${c.ratio}:1 fails WCAG AA (needs 4.5:1)`, ratio:c.ratio })
204
+ else if (!c.aaLarge && (node.fontSize || 16) >= 18) issues.push({ severity:'warning', rule:'contrast-aa-large', message:`Large text contrast ${c.ratio}:1 fails AA (needs 3:1)`, ratio:c.ratio })
205
+ }
206
+ // Font size
207
+ if (node.type === 'text' && (node.fontSize || 16) < 12) {
208
+ issues.push({ severity:'warning', rule:'min-font-size', message:`Font size ${node.fontSize}px is below 12px minimum`, fix:{ fontSize:12 } })
209
+ }
210
+ return issues
268
211
  }
269
212
 
270
- // ─── Accessibility ───
271
-
272
- export function checkTouchTarget(width, height, minSize = 44) {
273
- return { width, height, passes: width >= minSize && height >= minSize, minSize };
213
+ // ─── Font Weight Map ───
214
+ export const FONT_WEIGHTS = {
215
+ '100':'Thin','200':'Extra Light','300':'Light','400':'Regular',
216
+ '500':'Medium','600':'Semi Bold','700':'Bold','800':'Extra Bold','900':'Black',
217
+ thin:'Thin', light:'Light', regular:'Regular', normal:'Regular',
218
+ medium:'Medium', semibold:'Semi Bold', bold:'Bold', extrabold:'Extra Bold', black:'Black',
274
219
  }
275
220
 
276
- // ─── Auto-Layout ───
277
-
278
- export function inferLayoutDirection(children) {
279
- if (children.length < 2) return 'vertical';
280
- const firstTwo = children.slice(0, 2);
281
- const xDiff = Math.abs(firstTwo[0].x - firstTwo[1].x);
282
- const yDiff = Math.abs(firstTwo[0].y - firstTwo[1].y);
283
- return xDiff > yDiff ? 'horizontal' : 'vertical';
221
+ export function resolveFontWeight(w) {
222
+ if (!w) return 'Regular'
223
+ return FONT_WEIGHTS[String(w).toLowerCase()] || FONT_WEIGHTS[w] || w
284
224
  }
285
225
 
286
- export function inferGap(children, direction = 'vertical') {
287
- if (children.length < 2) return 8;
288
- const gaps = [];
289
- const sorted = [...children].sort((a, b) => direction === 'vertical' ? a.y - b.y : a.x - b.x);
290
- for (let i = 1; i < sorted.length; i++) {
291
- const gap = direction === 'vertical'
292
- ? sorted[i].y - (sorted[i-1].y + sorted[i-1].height)
293
- : sorted[i].x - (sorted[i-1].x + sorted[i-1].width);
294
- if (gap > 0) gaps.push(gap);
226
+ // ─── Gradient Helpers ───
227
+ export function linearGradient(angle, stops) {
228
+ const rad = (angle - 90) * Math.PI / 180
229
+ return {
230
+ type: 'GRADIENT_LINEAR',
231
+ gradientTransform: [[Math.cos(rad), Math.sin(rad), 0.5 - 0.5*Math.cos(rad) - 0.5*Math.sin(rad)], [-Math.sin(rad), Math.cos(rad), 0.5 + 0.5*Math.sin(rad) - 0.5*Math.cos(rad)]],
232
+ gradientStops: stops.map(s => ({ position: s.position, color: hexToFigmaColor(s.color) })),
295
233
  }
296
- if (gaps.length === 0) return 8;
297
- const avg = gaps.reduce((s, g) => s + g, 0) / gaps.length;
298
- return snapToGrid(avg);
299
234
  }
300
235
 
301
- export function inferPadding(parent, children) {
302
- if (children.length === 0) return { top: 16, right: 16, bottom: 16, left: 16 };
303
- const minX = Math.min(...children.map(c => c.x));
304
- const minY = Math.min(...children.map(c => c.y));
305
- const maxX = Math.max(...children.map(c => c.x + c.width));
306
- const maxY = Math.max(...children.map(c => c.y + c.height));
236
+ export function radialGradient(stops) {
307
237
  return {
308
- top: snapToGrid(minY - parent.y),
309
- right: snapToGrid((parent.x + parent.width) - maxX),
310
- bottom: snapToGrid((parent.y + parent.height) - maxY),
311
- left: snapToGrid(minX - parent.x),
312
- };
238
+ type: 'GRADIENT_RADIAL',
239
+ gradientTransform: [[0.5,0,0.25],[0,0.5,0.25]],
240
+ gradientStops: stops.map(s => ({ position: s.position, color: hexToFigmaColor(s.color) })),
241
+ }
313
242
  }
314
243
 
315
- // ─── Responsive ───
316
-
317
- export const BREAKPOINTS = {
318
- mobile: { name: 'Mobile', width: 375, height: 812 },
319
- tablet: { name: 'Tablet', width: 768, height: 1024 },
320
- desktop: { name: 'Desktop', width: 1440, height: 900 },
321
- };
322
-
323
- export function scaleForBreakpoint(value, fromWidth, toWidth) {
324
- return Math.round(value * (toWidth / fromWidth));
244
+ export function hexToFigmaColor(hex) {
245
+ hex = hex.replace('#','')
246
+ const hasAlpha = hex.length === 8
247
+ const r = parseInt(hex.slice(0,2),16) / 255
248
+ const g = parseInt(hex.slice(2,4),16) / 255
249
+ const b = parseInt(hex.slice(4,6),16) / 255
250
+ const a = hasAlpha ? parseInt(hex.slice(6,8),16) / 255 : 1
251
+ return { r, g, b, a }
325
252
  }
326
253
 
327
- // ─── Design Score ───
328
-
329
- export function computeDesignScore(audit) {
330
- const weights = { spacing: 25, typography: 20, color: 15, components: 15, accessibility: 15, hierarchy: 10 };
331
- let total = 0;
332
- let maxTotal = 0;
254
+ export function figmaColorToHex({r,g,b,a}) {
255
+ const h = [r,g,b].map(v => Math.round(v*255).toString(16).padStart(2,'0')).join('')
256
+ return a !== undefined && a < 1 ? '#' + h + Math.round(a*255).toString(16).padStart(2,'0') : '#' + h
257
+ }
333
258
 
334
- for (const [category, weight] of Object.entries(weights)) {
335
- const score = audit[category] ?? 100;
336
- total += score * weight;
337
- maxTotal += 100 * weight;
259
+ // ─── Design Craft Guide ───
260
+ export function getDesignCraftGuide() {
261
+ return {
262
+ typography: {
263
+ rules: [
264
+ 'Use 2-3 font weights maximum (Regular + Semi Bold, or Regular + Medium + Bold)',
265
+ 'Body text: 15-17px, line-height 1.5-1.7',
266
+ 'Headings: Use type scale ratios, not arbitrary sizes',
267
+ 'Never use font size below 12px',
268
+ 'Label/caption text: 11-13px, uppercase + letter-spacing for differentiation',
269
+ ],
270
+ scale: typeScale(16, 'major2'),
271
+ },
272
+ spacing: {
273
+ rules: [
274
+ 'Use 8px grid for all spacing',
275
+ 'Content padding: 24-48px',
276
+ 'Card padding: 20-32px',
277
+ 'Section vertical padding: 64-120px',
278
+ 'Gap between related items: 8-16px',
279
+ 'Gap between groups: 24-48px',
280
+ ],
281
+ system: SPACING,
282
+ },
283
+ color: {
284
+ rules: [
285
+ 'Maximum 3 brand colors',
286
+ 'Use opacity for hierarchy, not different grays',
287
+ 'Text hierarchy: 3 levels (primary, secondary, muted)',
288
+ 'Always check contrast ratios (WCAG AA minimum)',
289
+ 'Semantic colors for status: success/warning/error/info',
290
+ ],
291
+ },
292
+ layout: {
293
+ rules: [
294
+ 'Content max-width: 1120-1200px',
295
+ 'Use auto-layout for everything (no absolute positioning)',
296
+ 'Consistent alignment (CENTER for hero sections, LEFT for content)',
297
+ 'Visual hierarchy through size contrast, not just weight',
298
+ 'Generous whitespace signals quality',
299
+ ],
300
+ },
301
+ components: {
302
+ rules: [
303
+ 'Buttons: minimum 44px touch target',
304
+ 'Inputs: match button height for visual rhythm',
305
+ 'Cards: consistent padding and corner radius',
306
+ 'Icons: 20-24px for UI, 16-18px inline with text',
307
+ 'Avatars: always circular (border-radius: 9999)',
308
+ ],
309
+ },
310
+ antiPatterns: [
311
+ 'Random font sizes not on a scale',
312
+ 'Spacing values not on 4/8px grid',
313
+ 'Too many colors competing for attention',
314
+ 'Tiny click targets on interactive elements',
315
+ 'Inconsistent corner radii across components',
316
+ 'Text on images without proper contrast overlay',
317
+ 'More than 3 font weights on one screen',
318
+ ],
338
319
  }
339
-
340
- return Math.round((total / maxTotal) * 100);
341
320
  }