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.
- package/LICENSE +20 -0
- package/README.md +59 -153
- package/bin/conductor.js +1 -75
- package/figma-plugin/code.js +755 -0
- package/figma-plugin/manifest.json +14 -0
- package/figma-plugin/ui.html +77 -0
- package/package.json +25 -16
- package/src/bridge.js +60 -0
- package/src/design/intelligence.js +273 -294
- package/src/server.js +82 -196
- package/src/tools/handlers.js +145 -463
- package/src/tools/registry.js +1144 -336
- package/src/blueprints.js +0 -775
- package/src/design/craftguide.js +0 -181
- package/src/design/exporter.js +0 -72
- package/src/index.js +0 -33
- package/src/orchestrator.js +0 -100
- package/src/relay.js +0 -176
|
@@ -1,341 +1,320 @@
|
|
|
1
1
|
// ═══════════════════════════════════════════
|
|
2
|
-
// CONDUCTOR — Design Intelligence
|
|
2
|
+
// CONDUCTOR v3 — Design Intelligence Engine
|
|
3
3
|
// ═══════════════════════════════════════════
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
for (let i =
|
|
81
|
-
|
|
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
|
-
|
|
26
|
+
return scale
|
|
27
|
+
}
|
|
84
28
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
152
|
+
const comp = defs[type]
|
|
153
|
+
if (!comp) return null
|
|
154
|
+
return comp[variant] || comp.default || null
|
|
189
155
|
}
|
|
190
156
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
|
187
|
+
ratio: Math.round(ratio*100)/100,
|
|
212
188
|
aa: ratio >= 4.5,
|
|
213
|
-
aaLarge: ratio >= 3,
|
|
214
189
|
aaa: ratio >= 7,
|
|
215
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
// ───
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
}
|