conductor-figma 0.1.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,341 @@
1
+ // ═══════════════════════════════════════════
2
+ // CONDUCTOR — Design Intelligence
3
+ // ═══════════════════════════════════════════
4
+ // Pure functions for design decisions. No Figma dependency.
5
+ // These encode the rules a senior designer applies instinctively.
6
+
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
+ }
44
+
45
+ // ─── 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}`;
74
+ }
75
+
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]);
82
+ }
83
+ const avgRatio = ratios.reduce((s, r) => s + r, 0) / ratios.length;
84
+
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 }; }
90
+ }
91
+
92
+ 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;
141
+ }
142
+ return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
143
+ }
144
+
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
+ }
154
+
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
+ }
163
+
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
+ };
179
+ }
180
+
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);
187
+ }
188
+ return result;
189
+ }
190
+
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);
206
+ }
207
+
208
+ export function checkContrast(fgHex, bgHex) {
209
+ const ratio = contrastRatio(fgHex, bgHex);
210
+ return {
211
+ ratio: Math.round(ratio * 100) / 100,
212
+ aa: ratio >= 4.5,
213
+ aaLarge: ratio >= 3,
214
+ 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
+ ];
244
+ }
245
+
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
+ }
265
+ }
266
+
267
+ return { elements: sorted, issues, score: issues.length === 0 ? 100 : Math.max(0, 100 - issues.length * 20) };
268
+ }
269
+
270
+ // ─── Accessibility ───
271
+
272
+ export function checkTouchTarget(width, height, minSize = 44) {
273
+ return { width, height, passes: width >= minSize && height >= minSize, minSize };
274
+ }
275
+
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';
284
+ }
285
+
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);
295
+ }
296
+ if (gaps.length === 0) return 8;
297
+ const avg = gaps.reduce((s, g) => s + g, 0) / gaps.length;
298
+ return snapToGrid(avg);
299
+ }
300
+
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));
307
+ 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
+ };
313
+ }
314
+
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));
325
+ }
326
+
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;
333
+
334
+ for (const [category, weight] of Object.entries(weights)) {
335
+ const score = audit[category] ?? 100;
336
+ total += score * weight;
337
+ maxTotal += 100 * weight;
338
+ }
339
+
340
+ return Math.round((total / maxTotal) * 100);
341
+ }
package/src/index.js ADDED
@@ -0,0 +1,29 @@
1
+ // ═══════════════════════════════════════════
2
+ // CONDUCTOR — Public API
3
+ // ═══════════════════════════════════════════
4
+
5
+ export { startServer } from './server.js';
6
+ export { TOOLS, CATEGORIES, getToolByName, getToolsByCategory, getAllToolNames } from './tools/registry.js';
7
+ export { handleTool } from './tools/handlers.js';
8
+
9
+ export {
10
+ // Grid
11
+ snapToGrid, isOnGrid, generateSpacingScale, findNearestGridValue, auditSpacing,
12
+ // Type
13
+ TYPE_SCALES, generateTypeScale, detectTypeScale, getLineHeight, getFontWeight, checkMeasure,
14
+ // Color
15
+ hexToRgb, rgbToHex, hexToHsl, hslToHex, generatePalette, generateSemanticColors,
16
+ generateDarkMode, relativeLuminance, contrastRatio, checkContrast,
17
+ // Shadow & Radius
18
+ generateElevation, generateRadiusScale,
19
+ // Hierarchy
20
+ assessHierarchy,
21
+ // Accessibility
22
+ checkTouchTarget,
23
+ // Layout
24
+ inferLayoutDirection, inferGap, inferPadding, BREAKPOINTS, scaleForBreakpoint,
25
+ // Score
26
+ computeDesignScore,
27
+ } from './design/intelligence.js';
28
+
29
+ export { exportCSS, exportTailwind, exportSCSS, exportJSON, exportW3CTokens } from './design/exporter.js';
package/src/server.js ADDED
@@ -0,0 +1,115 @@
1
+ // ═══════════════════════════════════════════
2
+ // CONDUCTOR — MCP Server
3
+ // ═══════════════════════════════════════════
4
+ // Model Context Protocol server over stdio.
5
+ // Registers 61 design-intelligent tools for AI editors.
6
+
7
+ import { TOOLS } from './tools/registry.js';
8
+ import { handleTool } from './tools/handlers.js';
9
+
10
+ const SERVER_INFO = {
11
+ name: 'conductor-figma',
12
+ version: '0.1.0',
13
+ };
14
+
15
+ const CAPABILITIES = {
16
+ tools: {},
17
+ };
18
+
19
+ /**
20
+ * Start the MCP server on stdio.
21
+ * Reads JSON-RPC messages from stdin, writes responses to stdout.
22
+ */
23
+ export function startServer() {
24
+ let buffer = '';
25
+
26
+ process.stdin.setEncoding('utf-8');
27
+ process.stdin.on('data', (chunk) => {
28
+ buffer += chunk;
29
+
30
+ // Process complete lines
31
+ const lines = buffer.split('\n');
32
+ buffer = lines.pop() || '';
33
+
34
+ for (const line of lines) {
35
+ const trimmed = line.trim();
36
+ if (!trimmed) continue;
37
+
38
+ try {
39
+ const message = JSON.parse(trimmed);
40
+ handleMessage(message);
41
+ } catch (err) {
42
+ sendError(null, -32700, 'Parse error');
43
+ }
44
+ }
45
+ });
46
+
47
+ process.stdin.on('end', () => {
48
+ process.exit(0);
49
+ });
50
+
51
+ // Log startup to stderr (not stdout — that's for MCP protocol)
52
+ process.stderr.write(`CONDUCTOR MCP server started (${TOOLS.length} tools)\n`);
53
+ }
54
+
55
+ function handleMessage(msg) {
56
+ // JSON-RPC 2.0
57
+ const { id, method, params } = msg;
58
+
59
+ switch (method) {
60
+ case 'initialize':
61
+ sendResult(id, {
62
+ protocolVersion: '2024-11-05',
63
+ serverInfo: SERVER_INFO,
64
+ capabilities: CAPABILITIES,
65
+ });
66
+ break;
67
+
68
+ case 'initialized':
69
+ // Notification, no response needed
70
+ break;
71
+
72
+ case 'tools/list':
73
+ sendResult(id, {
74
+ tools: TOOLS.map(t => ({
75
+ name: t.name,
76
+ description: t.description,
77
+ inputSchema: t.inputSchema,
78
+ })),
79
+ });
80
+ break;
81
+
82
+ case 'tools/call': {
83
+ const toolName = params?.name;
84
+ const toolArgs = params?.arguments || {};
85
+
86
+ if (!toolName) {
87
+ sendError(id, -32602, 'Missing tool name');
88
+ return;
89
+ }
90
+
91
+ const result = handleTool(toolName, toolArgs, null);
92
+ sendResult(id, result);
93
+ break;
94
+ }
95
+
96
+ case 'ping':
97
+ sendResult(id, {});
98
+ break;
99
+
100
+ default:
101
+ if (id !== undefined) {
102
+ sendError(id, -32601, `Method not found: ${method}`);
103
+ }
104
+ }
105
+ }
106
+
107
+ function sendResult(id, result) {
108
+ const response = { jsonrpc: '2.0', id, result };
109
+ process.stdout.write(JSON.stringify(response) + '\n');
110
+ }
111
+
112
+ function sendError(id, code, message) {
113
+ const response = { jsonrpc: '2.0', id, error: { code, message } };
114
+ process.stdout.write(JSON.stringify(response) + '\n');
115
+ }