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,486 +1,168 @@
1
1
  // ═══════════════════════════════════════════
2
- // CONDUCTOR — Tool Handlers
2
+ // CONDUCTOR v3 — Tool Handlers
3
3
  // ═══════════════════════════════════════════
4
- // Executes tool calls using design intelligence.
5
- // Each handler returns { content: [{ type: 'text', text: string }] }
6
4
 
7
- import * as design from '../design/intelligence.js';
8
- import * as exporter from '../design/exporter.js';
9
- import { getDesignCraftGuide } from '../design/craftguide.js';
10
-
11
- function text(str) {
12
- return { content: [{ type: 'text', text: str }] };
13
- }
14
-
15
- function json(obj) {
16
- return text(JSON.stringify(obj, null, 2));
5
+ import {
6
+ snap, typeScale, semanticColors, componentDefaults, suggestAutoLayout,
7
+ checkContrast, auditAccessibility, getDesignCraftGuide, resolveFontWeight,
8
+ hexToFigmaColor, linearGradient, radialGradient, SPACING, RADIUS, SHADOWS,
9
+ } from '../design/intelligence.js'
10
+
11
+ // ─── Icon SVG Library ───
12
+ const ICONS = {
13
+ 'arrow-right': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>',
14
+ 'arrow-left': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>',
15
+ 'arrow-up': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 19V5M5 12l7-7 7 7"/></svg>',
16
+ 'arrow-down': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>',
17
+ 'check': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg>',
18
+ 'x': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>',
19
+ 'plus': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>',
20
+ 'minus': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>',
21
+ 'search': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>',
22
+ 'menu': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>',
23
+ 'settings': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>',
24
+ 'user': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
25
+ 'heart': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 000-7.78z"/></svg>',
26
+ 'star': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"/></svg>',
27
+ 'home': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg>',
28
+ 'mail': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 7l-10 7L2 7"/></svg>',
29
+ 'chevron-right': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>',
30
+ 'chevron-left': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>',
31
+ 'chevron-down': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>',
32
+ 'chevron-up': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"/></svg>',
33
+ 'external-link': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3"/></svg>',
34
+ 'copy': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>',
35
+ 'trash': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>',
36
+ 'edit': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
37
+ 'download': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>',
38
+ 'upload': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>',
39
+ 'eye': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
40
+ 'lock': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>',
41
+ 'bell': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 01-3.46 0"/></svg>',
42
+ 'filter': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22,3 2,3 10,12.46 10,19 14,21 14,12.46"/></svg>',
43
+ 'grid': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>',
44
+ 'list': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>',
45
+ 'link': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>',
46
+ 'share': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>',
47
+ 'sort': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19,12 12,19 5,12"/></svg>',
48
+ 'clock': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12,6 12,12 16,14"/></svg>',
49
+ 'calendar': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
50
+ 'phone': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6A19.79 19.79 0 012.12 4.18 2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z"/></svg>',
17
51
  }
18
52
 
19
- // ─── Handler Map ───
20
- // Returns a response for each tool. Tools that need Figma connection
21
- // return instructions; tools that are pure design logic execute immediately.
22
-
23
- export function handleTool(toolName, args, figmaState) {
24
- const handler = HANDLERS[toolName];
25
- if (!handler) return text(`Unknown tool: ${toolName}`);
26
- try {
27
- return handler(args, figmaState);
28
- } catch (err) {
29
- return text(`Error in ${toolName}: ${err.message}`);
53
+ // ─── Main Handler ───
54
+ export async function handleTool(name, args, bridge) {
55
+ // Tools that don't need Figma connection
56
+ switch (name) {
57
+ case 'get_design_craft_guide': return getDesignCraftGuide()
58
+ case 'check_contrast': return checkContrast(args.foreground, args.background)
59
+ case 'suggest_color_palette': return semanticColors(args.brandColor, args.mode || 'dark')
60
+ case 'suggest_type_scale': return typeScale(args.baseSize || 16, args.ratio || 'major2')
30
61
  }
31
- }
32
-
33
- const HANDLERS = {
34
- // ═══ CREATE ═══
35
- create_frame(args) {
36
- const padding = design.snapToGrid(args.padding || 16);
37
- const gap = design.snapToGrid(args.gap || 8);
38
- return json({
39
- action: 'create_frame',
40
- name: args.name,
41
- layoutMode: (args.direction || 'vertical').toUpperCase(),
42
- primaryAxisAlignItems: 'MIN',
43
- counterAxisAlignItems: 'MIN',
44
- paddingLeft: padding, paddingRight: padding, paddingTop: padding, paddingBottom: padding,
45
- itemSpacing: gap,
46
- width: args.width || null,
47
- height: args.height || null,
48
- fills: args.fill ? [{ type: 'SOLID', color: hexToFigmaColor(args.fill) }] : [],
49
- designNotes: `Padding: ${padding}px (grid-aligned), Gap: ${gap}px`,
50
- });
51
- },
52
-
53
- create_page(args) {
54
- const sections = args.sections || getDefaultSections(args.pageType);
55
- const colors = args.brandColor ? design.generateSemanticColors(args.brandColor) : null;
56
- const typeScale = design.generateTypeScale(16, 'major-third');
57
-
58
- return json({
59
- action: 'create_page',
60
- pageType: args.pageType,
61
- title: args.title || `${args.pageType} page`,
62
- sections,
63
- typeScale: typeScale.sizes.map(s => ({ label: s.label, size: s.size })),
64
- spacingScale: design.generateSpacingScale(8, 8),
65
- colors,
66
- darkMode: args.darkMode ? design.generateDarkMode(colors || {}) : null,
67
- designNotes: `Type scale: ${typeScale.scale} (${typeScale.ratio}). Grid: 8px. Sections: ${sections.join(', ')}`,
68
- });
69
- },
70
-
71
- create_section(args) {
72
- const patterns = {
73
- hero: { columns: 1, minHeight: 480, padding: 64, elements: ['heading', 'subheading', 'cta-group'] },
74
- features: { columns: args.columns || 3, padding: 48, elements: ['section-heading', 'feature-cards'] },
75
- testimonials: { columns: args.columns || 2, padding: 48, elements: ['section-heading', 'testimonial-cards'] },
76
- faq: { columns: 1, padding: 48, elements: ['section-heading', 'accordion-items'] },
77
- cta: { columns: 1, padding: 64, elements: ['heading', 'subheading', 'cta-button'] },
78
- pricing: { columns: args.columns || 3, padding: 48, elements: ['section-heading', 'pricing-cards'] },
79
- stats: { columns: args.columns || 4, padding: 48, elements: ['stat-items'] },
80
- team: { columns: args.columns || 3, padding: 48, elements: ['section-heading', 'team-cards'] },
81
- footer: { columns: 4, padding: 48, elements: ['logo', 'nav-groups', 'legal'] },
82
- header: { columns: 1, padding: 16, elements: ['logo', 'nav-links', 'cta-button'] },
83
- };
84
-
85
- const pattern = patterns[args.sectionType] || patterns.hero;
86
- return json({
87
- action: 'create_section',
88
- sectionType: args.sectionType,
89
- ...pattern,
90
- heading: args.heading,
91
- designNotes: `Section pattern: ${args.sectionType}. ${pattern.columns} columns, ${pattern.padding}px padding, grid-aligned.`,
92
- });
93
- },
94
-
95
- create_card(args) {
96
- const variant = args.variant || 'elevated';
97
- const shadows = design.generateElevation();
98
- const shadow = variant === 'elevated' ? shadows.find(s => s.step === 'md') : null;
99
- const radii = design.generateRadiusScale();
100
- const radius = radii.find(r => r.name === 'lg');
101
-
102
- return json({
103
- action: 'create_card',
104
- variant,
105
- width: args.width || 320,
106
- padding: 24,
107
- gap: 12,
108
- cornerRadius: radius?.value || 12,
109
- shadow: shadow?.css || 'none',
110
- title: args.title,
111
- description: args.description,
112
- hasImage: args.hasImage || false,
113
- hasAction: args.hasAction || false,
114
- designNotes: `${variant} card. 24px padding (3×8), 12px gap, ${radius?.value}px radius.`,
115
- });
116
- },
117
-
118
- create_form(args) {
119
- const fields = args.fields.map(f => ({
120
- ...f,
121
- inputHeight: 40,
122
- padding: { x: 12, y: 10 },
123
- fontSize: 14,
124
- labelSize: 13,
125
- gap: 4,
126
- }));
127
-
128
- return json({
129
- action: 'create_form',
130
- layout: args.layout || 'vertical',
131
- fieldGap: 20,
132
- submitLabel: args.submitLabel || 'Submit',
133
- fields,
134
- designNotes: `Form: ${fields.length} fields, ${args.layout || 'vertical'} layout. 40px input height, 14px text. All spacing grid-aligned.`,
135
- });
136
- },
137
-
138
- create_table(args) {
139
- return json({
140
- action: 'create_table',
141
- columns: args.columns,
142
- rows: args.rows || 5,
143
- headerHeight: 40,
144
- rowHeight: 48,
145
- cellPadding: { x: 16, y: 12 },
146
- hasPagination: args.hasPagination || false,
147
- hasSorting: args.hasSorting || false,
148
- hasCheckbox: args.hasCheckbox || false,
149
- designNotes: `Table: ${args.columns.length} cols × ${args.rows || 5} rows. Header: 40px, Row: 48px. 16px cell padding.`,
150
- });
151
- },
152
-
153
- create_modal(args) {
154
- const sizes = { sm: 400, md: 560, lg: 720, xl: 900 };
155
- const width = sizes[args.size || 'md'] || 560;
156
62
 
157
- return json({
158
- action: 'create_modal',
159
- title: args.title,
160
- width,
161
- padding: 24,
162
- headerHeight: 56,
163
- cornerRadius: 16,
164
- hasCloseButton: args.hasCloseButton !== false,
165
- actions: args.actions || ['Cancel', 'Confirm'],
166
- overlay: { color: '#000000', opacity: 0.5 },
167
- designNotes: `Modal: ${args.size || 'md'} (${width}px). 24px padding, 16px radius.`,
168
- });
169
- },
170
-
171
- create_nav(args) {
172
- return json({
173
- action: 'create_nav',
174
- navType: args.navType,
175
- items: args.items,
176
- logoText: args.logoText,
177
- hasSearch: args.hasSearch || false,
178
- hasAvatar: args.hasAvatar || false,
179
- height: args.navType === 'topbar' ? 56 : undefined,
180
- width: args.navType === 'sidebar' ? 240 : undefined,
181
- padding: { x: 16, y: 12 },
182
- itemGap: args.navType === 'tabs' ? 0 : 8,
183
- designNotes: `Nav: ${args.navType} with ${args.items.length} items. All spacing grid-aligned.`,
184
- });
185
- },
186
-
187
- // ═══ LAYOUT ═══
188
- layout_auto(args) {
189
- return json({
190
- action: 'set_auto_layout',
191
- nodeId: args.nodeId,
192
- direction: args.direction || 'auto-detect',
193
- gap: args.gap ? design.snapToGrid(args.gap) : 'auto-detect',
194
- padding: args.padding ? design.snapToGrid(args.padding) : 'auto-detect',
195
- designNotes: 'Converting to auto-layout. Direction and gap auto-detected from child positions if not specified.',
196
- });
197
- },
198
-
199
- layout_grid(args) {
200
- return json({
201
- action: 'apply_grid',
202
- nodeId: args.nodeId,
203
- columns: args.columns || 12,
204
- gutter: design.snapToGrid(args.gutter || 24),
205
- margin: design.snapToGrid(args.margin || 24),
206
- type: args.type || 'stretch',
207
- });
208
- },
209
-
210
- layout_stack(args) {
211
- return json({ action: 'stack', nodeId: args.nodeId, direction: args.direction, gap: design.snapToGrid(args.gap || 8), align: args.align || 'start' });
212
- },
213
-
214
- layout_wrap(args) {
215
- return json({ action: 'wrap_layout', nodeId: args.nodeId, gap: design.snapToGrid(args.gap || 8), maxWidth: args.maxWidth });
216
- },
217
-
218
- layout_constrain(args) {
219
- return json({ action: 'set_constraints', nodeId: args.nodeId, horizontal: args.horizontal, vertical: args.vertical, minWidth: args.minWidth, maxWidth: args.maxWidth, minHeight: args.minHeight, maxHeight: args.maxHeight });
220
- },
63
+ // Everything else needs Figma
64
+ if (!bridge || !bridge.isConnected()) {
65
+ throw new Error('Figma plugin not connected. Open Figma → Plugins → Development → Conductor, then try again.')
66
+ }
221
67
 
222
- layout_align(args) {
223
- return json({ action: 'align', nodeIds: args.nodeIds, alignment: args.alignment });
224
- },
68
+ // Apply design intelligence to args before sending
69
+ const enhanced = enhanceWithIntelligence(name, args)
225
70
 
226
- layout_nest(args) {
227
- return json({ action: 'restructure_to_autolayout', nodeId: args.nodeId, maxDepth: args.depth || 3, designNotes: 'Analyzing spatial relationships to build auto-layout tree. Groups detected by proximity and alignment.' });
228
- },
71
+ // Send to Figma plugin
72
+ return bridge.send(name, enhanced)
73
+ }
229
74
 
230
- // ═══ TYPOGRAPHY ═══
231
- type_scale(args) {
232
- if (args.action === 'generate') {
233
- const result = design.generateTypeScale(args.baseSize || 16, args.scaleRatio || 'major-third');
234
- return json({ action: 'type_scale_generated', ...result });
75
+ // ─── Design Intelligence Enhancement ───
76
+ function enhanceWithIntelligence(name, args) {
77
+ const a = { ...args }
78
+
79
+ switch (name) {
80
+ case 'create_frame': {
81
+ // Snap all spacing to grid
82
+ if (a.padding !== undefined) { const p = snap(a.padding); a.paddingTop = p; a.paddingRight = p; a.paddingBottom = p; a.paddingLeft = p; delete a.padding }
83
+ if (a.paddingTop !== undefined) a.paddingTop = snap(a.paddingTop)
84
+ if (a.paddingRight !== undefined) a.paddingRight = snap(a.paddingRight)
85
+ if (a.paddingBottom !== undefined) a.paddingBottom = snap(a.paddingBottom)
86
+ if (a.paddingLeft !== undefined) a.paddingLeft = snap(a.paddingLeft)
87
+ if (a.gap !== undefined) a.gap = snap(a.gap)
88
+ // Default auto-layout
89
+ if (!a.direction) a.direction = 'VERTICAL'
90
+ break
235
91
  }
236
- return json({ action: 'detect_type_scale', nodeId: args.nodeId, designNotes: 'Scanning all text nodes to detect current scale ratio.' });
237
- },
238
-
239
- type_hierarchy(args) {
240
- const scale = design.generateTypeScale(args.baseSize || 16, 'major-third', { down: 1, up: args.levels || 4 });
241
- return json({ action: 'apply_hierarchy', nodeId: args.nodeId, levels: scale.sizes.map(s => ({ label: s.label, size: s.size, lineHeight: design.getLineHeight(s.size), weight: s.step >= 1 ? 700 : s.step === 0 ? 400 : 400 })) });
242
- },
243
-
244
- type_pair(args) {
245
- const pairs = [
246
- { heading: 'Instrument Serif', body: 'Sora', style: 'editorial' },
247
- { heading: 'Space Grotesk', body: 'IBM Plex Sans', style: 'technical' },
248
- { heading: 'Fraunces', body: 'Commissioner', style: 'classic' },
249
- { heading: 'Cabinet Grotesk', body: 'Satoshi', style: 'modern' },
250
- { heading: 'Gloock', body: 'DM Sans', style: 'playful' },
251
- ];
252
- const match = args.style ? pairs.find(p => p.style === args.style) : pairs[0];
253
- return json({ suggestions: pairs, recommended: match || pairs[0] });
254
- },
255
-
256
- type_measure(args) {
257
- return json({ action: 'check_measure', nodeId: args.nodeId, optimalRange: '45-75 characters', designNotes: 'Measuring character count per line and checking line-height ratios.' });
258
- },
259
-
260
- type_apply(args) {
261
- return json({ action: 'apply_text_styles', nodeId: args.nodeId, styles: args.styles });
262
- },
263
-
264
- type_audit(args) {
265
- return json({ action: 'audit_typography', nodeId: args.nodeId, designNotes: 'Scanning all text nodes for unique styles. Will flag off-scale sizes and inconsistent weights.' });
266
- },
267
-
268
- // ═══ COLOR ═══
269
- color_palette(args) {
270
- const palette = design.generatePalette(args.baseColor, args.steps);
271
- return json({ action: 'palette_generated', baseColor: args.baseColor, shades: palette });
272
- },
273
-
274
- color_semantic(args) {
275
- const colors = design.generateSemanticColors(args.brandColor);
276
- return json({ action: 'semantic_colors_generated', ...colors });
277
- },
278
92
 
279
- color_darkmode(args) {
280
- if (args.colors) return json({ action: 'dark_mode_generated', colors: design.generateDarkMode(args.colors) });
281
- return json({ action: 'generate_dark_mode', nodeId: args.nodeId, designNotes: 'Reading frame colors and generating dark mode with preserved contrast.' });
282
- },
283
-
284
- color_contrast(args) {
285
- if (args.foreground && args.background) {
286
- return json({ action: 'contrast_checked', ...design.checkContrast(args.foreground, args.background) });
93
+ case 'create_text': {
94
+ // Resolve font weight
95
+ if (a.fontWeight) a.fontWeight = resolveFontWeight(a.fontWeight)
96
+ if (!a.fontFamily) a.fontFamily = 'Inter'
97
+ // Build fontName for Figma
98
+ a.fontName = { family: a.fontFamily, style: a.fontWeight || 'Regular' }
99
+ break
287
100
  }
288
- return json({ action: 'audit_contrast', nodeId: args.nodeId, designNotes: 'Checking all text/background pairs in frame.' });
289
- },
290
-
291
- color_apply(args) {
292
- return json({ action: 'apply_colors', nodeId: args.nodeId, colorMap: args.colorMap });
293
- },
294
-
295
- style_shadow(args) {
296
- const shadows = design.generateElevation(args.levels);
297
- return json({ action: 'elevation_generated', shadows });
298
- },
299
-
300
- style_radius(args) {
301
- const scale = design.generateRadiusScale(args.base || 4);
302
- return json({ action: 'radius_scale_generated', scale, nodeId: args.nodeId });
303
- },
304
-
305
- // ═══ COMPONENTS ═══
306
- component_list(args) { return json({ action: 'list_components', source: args.source || 'file', filter: args.filter }); },
307
- component_use(args) { return json({ action: 'instantiate_component', componentName: args.componentName, variants: args.variants, position: { x: args.x || 0, y: args.y || 0 } }); },
308
- component_create(args) { return json({ action: 'create_component', nodeId: args.nodeId, name: args.name, variants: args.variants }); },
309
- component_swap(args) { return json({ action: 'swap_component', nodeId: args.nodeId, from: args.fromComponent, to: args.toComponent }); },
310
- component_detach(args) { return json({ action: 'detach_instance', nodeId: args.nodeId }); },
311
- component_audit(args) { return json({ action: 'audit_components', nodeId: args.nodeId }); },
312
-
313
- // ═══ SPACING ═══
314
- spacing_scale(args) {
315
- const scale = design.generateSpacingScale(args.base || 8, args.steps || 12);
316
- return json({ action: 'spacing_scale_generated', base: args.base || 8, scale });
317
- },
318
-
319
- spacing_fix(args) {
320
- return json({ action: 'fix_spacing', nodeId: args.nodeId, base: args.base || 8, designNotes: 'Snapping all padding, margin, and gap values to nearest grid multiple.' });
321
- },
322
-
323
- spacing_audit(args) {
324
- return json({ action: 'audit_spacing', nodeId: args.nodeId, base: args.base || 8 });
325
- },
326
-
327
- spacing_rhythm(args) {
328
- return json({ action: 'check_rhythm', nodeId: args.nodeId, designNotes: 'Measuring vertical gaps between sections and checking for proportional consistency.' });
329
- },
330
-
331
- grid_apply(args) {
332
- return json({ action: 'apply_grid', nodeId: args.nodeId, type: args.type, count: args.count || 12, gutter: design.snapToGrid(args.gutter || 24), margin: design.snapToGrid(args.margin || 24) });
333
- },
334
-
335
- grid_check(args) {
336
- return json({ action: 'check_grid_alignment', nodeId: args.nodeId });
337
- },
338
-
339
- // ═══ AUDIT ═══
340
- audit_full(args) { return json({ action: 'full_audit', nodeId: args.nodeId, categories: ['spacing', 'typography', 'color', 'components', 'accessibility', 'hierarchy'] }); },
341
- audit_hierarchy(args) { return json({ action: 'audit_hierarchy', nodeId: args.nodeId }); },
342
- audit_consistency(args) { return json({ action: 'audit_consistency', nodeId: args.nodeId }); },
343
- audit_alignment(args) { return json({ action: 'audit_alignment', nodeId: args.nodeId, tolerance: args.tolerance || 2 }); },
344
- audit_density(args) { return json({ action: 'audit_density', nodeId: args.nodeId }); },
345
-
346
- audit_score(args) {
347
- return json({ action: 'compute_score', nodeId: args.nodeId, designNotes: 'Computing 0-100 score weighted: spacing 25%, typography 20%, color 15%, components 15%, accessibility 15%, hierarchy 10%.' });
348
- },
349
-
350
- // ═══ ACCESSIBILITY ═══
351
- a11y_contrast(args) {
352
- if (!args.nodeId) return json({ action: 'check_contrast_global', level: args.level || 'aa' });
353
- return json({ action: 'check_contrast', nodeId: args.nodeId, level: args.level || 'aa' });
354
- },
355
-
356
- a11y_touch(args) { return json({ action: 'check_touch_targets', nodeId: args.nodeId, minSize: args.minSize || 44 }); },
357
- a11y_focus(args) { return json({ action: 'generate_focus_states', nodeId: args.nodeId, color: args.color || '#2563eb', offset: args.offset || 2 }); },
358
- a11y_labels(args) { return json({ action: 'check_labels', nodeId: args.nodeId }); },
359
- a11y_fix(args) { return json({ action: 'auto_fix_a11y', nodeId: args.nodeId, fixes: args.fixes || ['contrast', 'touch', 'focus'] }); },
360
101
 
361
- // ═══ RESPONSIVE ═══
362
- responsive_variant(args) {
363
- const breakpoints = args.breakpoints || [375, 768, 1440];
364
- return json({ action: 'generate_variants', nodeId: args.nodeId, breakpoints: breakpoints.map(w => ({ width: w, name: w <= 375 ? 'mobile' : w <= 768 ? 'tablet' : 'desktop' })) });
365
- },
366
-
367
- responsive_reflow(args) { return json({ action: 'reflow', nodeId: args.nodeId, targetWidth: args.targetWidth }); },
368
- responsive_breakpoints(args) {
369
- const widths = args.widths || [375, 768, 1024, 1440];
370
- return json({ action: 'setup_breakpoints', frames: widths.map(w => ({ width: w, height: args.height || 900, name: design.BREAKPOINTS[w <= 375 ? 'mobile' : w <= 768 ? 'tablet' : 'desktop']?.name || `${w}px` })) });
371
- },
372
- responsive_check(args) { return json({ action: 'check_responsive', nodeId: args.nodeId }); },
373
-
374
- // ═══ EXPORT ═══
375
- export_tokens_css(args) {
376
- const tokens = buildTokenSet(args);
377
- return text(exporter.exportCSS(tokens));
378
- },
379
- export_tokens_tailwind(args) {
380
- const tokens = buildTokenSet(args);
381
- return text(exporter.exportTailwind(tokens));
382
- },
383
- export_tokens_json(args) {
384
- const tokens = buildTokenSet(args);
385
- return text(exporter.exportW3CTokens(tokens));
386
- },
387
- export_tokens_scss(args) {
388
- const tokens = buildTokenSet(args);
389
- return text(exporter.exportSCSS(tokens));
390
- },
391
- export_spec(args) { return json({ action: 'generate_spec', nodeId: args.nodeId, format: args.format || 'markdown' }); },
392
- export_changelog(args) { return json({ action: 'diff_frames', nodeIdA: args.nodeIdA, nodeIdB: args.nodeIdB, format: args.format || 'markdown' }); },
393
-
394
- // ═══ DESIGN CRAFT ═══
395
- get_design_craft_guide() {
396
- return text(getDesignCraftGuide());
397
- },
398
-
399
- // ═══ CREATE (additional) ═══
400
- create_ellipse(args) {
401
- return json({
402
- action: 'create_ellipse',
403
- name: args.name || 'Ellipse',
404
- width: args.width, height: args.height,
405
- fill: args.fill || '#ffffff',
406
- x: args.x, y: args.y,
407
- parentId: args.parentId,
408
- opacity: args.opacity,
409
- });
410
- },
102
+ case 'create_smart_component': {
103
+ const defaults = componentDefaults(a.type, a.variant)
104
+ if (!defaults) throw new Error(`Unknown component type: ${a.type}. Available: button, input, card, avatar, badge, chip, switch, checkbox, radio, toast, tooltip, modal, dropdown, tabs, table, progress, skeleton, divider`)
105
+ const colors = semanticColors(a.brandColor || '#6366f1', a.mode || 'dark')
106
+ a._defaults = defaults
107
+ a._colors = colors
108
+ break
109
+ }
411
110
 
412
- create_line(args) {
413
- var dir = args.direction || 'horizontal';
414
- return json({
415
- action: 'create_rect',
416
- name: args.name || 'Divider',
417
- width: dir === 'horizontal' ? args.length : 1,
418
- height: dir === 'horizontal' ? 1 : args.length,
419
- fill: args.color || '#1a1a32',
420
- parentId: args.parentId,
421
- });
422
- },
111
+ case 'set_effects': {
112
+ if (a.preset) {
113
+ const s = SHADOWS[a.preset]
114
+ if (s) a.shadow = s
115
+ }
116
+ break
117
+ }
423
118
 
424
- create_svg_node(args) {
425
- return json({
426
- action: 'create_svg_node',
427
- name: args.name || 'SVG',
428
- svg: args.svg,
429
- x: args.x, y: args.y,
430
- parentId: args.parentId,
431
- });
432
- },
119
+ case 'set_fill': {
120
+ if (a.type === 'LINEAR' && a.gradient) {
121
+ a._fill = linearGradient(a.gradient.angle || 180, a.gradient.stops)
122
+ } else if (a.type === 'RADIAL' && a.gradient) {
123
+ a._fill = radialGradient(a.gradient.stops)
124
+ } else if (a.color) {
125
+ a._fill = { type: 'SOLID', color: hexToFigmaColor(a.color), opacity: a.opacity !== undefined ? a.opacity : 1 }
126
+ }
127
+ break
128
+ }
433
129
 
434
- find_nodes(args) {
435
- return json({
436
- action: 'find_nodes',
437
- query: args.query,
438
- type: args.type,
439
- parentId: args.parentId,
440
- });
441
- },
130
+ case 'create_icon': {
131
+ const svg = ICONS[a.icon]
132
+ if (!svg) throw new Error(`Unknown icon: ${a.icon}. Available: ${Object.keys(ICONS).join(', ')}`)
133
+ a._svg = svg.replace('currentColor', a.color || '#ffffff')
134
+ a._size = a.size || 24
135
+ break
136
+ }
442
137
 
443
- // ═══ TYPOGRAPHY (additional) ═══
444
- style_text_range(args) {
445
- return json({
446
- action: 'style_text_range',
447
- nodeId: args.nodeId,
448
- start: args.start,
449
- end: args.end,
450
- fontSize: args.fontSize,
451
- fontWeight: args.fontWeight,
452
- color: args.color,
453
- });
454
- },
455
- };
138
+ case 'set_auto_layout': {
139
+ if (a.padding !== undefined) { const p = snap(a.padding); a.paddingTop = p; a.paddingRight = p; a.paddingBottom = p; a.paddingLeft = p; delete a.padding }
140
+ ;['paddingTop','paddingRight','paddingBottom','paddingLeft','gap'].forEach(k => { if (a[k] !== undefined) a[k] = snap(a[k]) })
141
+ break
142
+ }
456
143
 
457
- // ─── Helpers ───
144
+ case 'create_design_tokens': {
145
+ const colors = semanticColors(a.brandColor, 'dark')
146
+ const colorsLight = semanticColors(a.brandColor, 'light')
147
+ const scale = typeScale(16, 'major2')
148
+ a._darkColors = colors
149
+ a._lightColors = colorsLight
150
+ a._typeScale = scale
151
+ a._spacing = SPACING
152
+ a._radius = RADIUS
153
+ break
154
+ }
458
155
 
459
- function hexToFigmaColor(hex) {
460
- const { r, g, b } = design.hexToRgb(hex);
461
- return { r: r / 255, g: g / 255, b: b / 255 };
462
- }
156
+ case 'fix_spacing': {
157
+ a._grid = a.grid || 8
158
+ break
159
+ }
463
160
 
464
- function getDefaultSections(pageType) {
465
- const defaults = {
466
- landing: ['header', 'hero', 'features', 'testimonials', 'cta', 'footer'],
467
- pricing: ['header', 'hero', 'pricing', 'faq', 'cta', 'footer'],
468
- dashboard: ['sidebar', 'header', 'stats', 'table'],
469
- settings: ['sidebar', 'header', 'form'],
470
- auth: ['hero', 'form'],
471
- blog: ['header', 'hero', 'cards', 'footer'],
472
- portfolio: ['header', 'hero', 'cards', 'cta', 'footer'],
473
- docs: ['sidebar', 'header', 'content'],
474
- };
475
- return defaults[pageType] || defaults.landing;
476
- }
161
+ case 'lint_design': {
162
+ a._rules = a.rules || ['spacing', 'naming', 'colors', 'fonts', 'contrast', 'alignment', 'touch-targets']
163
+ break
164
+ }
165
+ }
477
166
 
478
- function buildTokenSet(args) {
479
- return {
480
- colors: design.generateSemanticColors(args.brandColor || '#6366f1'),
481
- spacing: design.generateSpacingScale(8, 8),
482
- fontSizes: design.generateTypeScale(16, 'major-third').sizes,
483
- radii: design.generateRadiusScale(4),
484
- shadows: design.generateElevation(),
485
- };
167
+ return a
486
168
  }