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,344 +1,1152 @@
1
1
  // ═══════════════════════════════════════════
2
- // CONDUCTOR — Tool Registry
2
+ // CONDUCTOR v3 — Tool Registry (150+ Tools)
3
3
  // ═══════════════════════════════════════════
4
- // All 61 MCP tools, organized by category.
5
- // Each tool has: name, description, category, inputSchema, handler reference.
6
4
 
7
- export const CATEGORIES = {
8
- craft: { label: 'Design Craft', icon: '✦', count: 1 },
9
- create: { label: 'Create', icon: '⊞', count: 12 },
10
- layout: { label: 'Layout', icon: '▤', count: 7 },
11
- typography: { label: 'Typography', icon: '◆', count: 7 },
12
- color: { label: 'Color & Style', icon: '◑', count: 7 },
13
- components: { label: 'Components', icon: '◎', count: 6 },
14
- spacing: { label: 'Spacing & Grid', icon: '◧', count: 6 },
15
- audit: { label: 'Audit & Critique', icon: '◉', count: 6 },
16
- accessibility:{ label: 'Accessibility', icon: '♿', count: 5 },
17
- responsive: { label: 'Responsive', icon: '↔', count: 4 },
18
- export: { label: 'Export & Handoff', icon: '⤓', count: 6 },
19
- };
20
-
21
- function str(desc) { return { type: 'string', description: desc }; }
22
- function num(desc) { return { type: 'number', description: desc }; }
23
- function bool(desc) { return { type: 'boolean', description: desc }; }
24
- function arr(desc, items) { return { type: 'array', description: desc, items: items || { type: 'string' } }; }
25
- function opt(schema) { return { ...schema, optional: true }; }
26
-
27
- export const TOOLS = [
28
- // ═══ CREATE ═══
29
- { name: 'create_frame', category: 'create',
30
- description: 'Create a single auto-layout frame. IMPORTANT: For multi-element designs (pages, sections, dashboards), use create_page or create_section instead — they handle all nesting automatically. If using create_frame directly, ALWAYS set parentId to nest inside a parent frame, and ALWAYS set direction, padding, and gap for auto-layout.',
31
- inputSchema: { type: 'object', properties: { name: str('Frame name'), width: opt(num('Width')), height: opt(num('Height')), direction: opt(str('VERTICAL or HORIZONTAL')), padding: opt(num('Padding in px (use multiples of 8)')), paddingTop: opt(num('Top padding')), paddingBottom: opt(num('Bottom padding')), paddingLeft: opt(num('Left padding')), paddingRight: opt(num('Right padding')), gap: opt(num('Gap between children (use multiples of 8)')), fill: opt(str('Background color hex')), cornerRadius: opt(num('Corner radius')), parentId: opt(str('REQUIRED for nesting: parent frame ID')), primaryAxisAlignItems: opt(str('MIN, CENTER, MAX, SPACE_BETWEEN')), counterAxisAlignItems: opt(str('MIN, CENTER, MAX, STRETCH')), primaryAxisSizingMode: opt(str('FIXED, HUG, FILL')), counterAxisSizingMode: opt(str('FIXED, HUG, FILL')) }, required: ['name'] } },
32
-
33
- { name: 'create_page', category: 'create',
34
- description: 'THE PRIMARY TOOL FOR DESIGNING PAGES. Creates a complete, polished, production-ready page with all sections, components, and content — fully nested with auto-layout, proper spacing, and design-intelligent values. One call generates 40-80 Figma elements. Use this INSTEAD of calling create_frame/create_text individually. Supports: landing, pricing, dashboard page types. Pass brand color, title, features, stats, and other content as parameters.',
35
- inputSchema: { type: 'object', properties: {
36
- pageType: str('Page type: landing, pricing, dashboard'),
37
- title: opt(str('Hero heading text')),
38
- subtitle: opt(str('Hero subtitle text')),
39
- brand: opt(str('Brand/company name (appears in nav and footer)')),
40
- brandColor: opt(str('Primary brand color hex (default #6366f1)')),
41
- ctaText: opt(str('Primary CTA button text')),
42
- darkMode: opt(bool('Dark mode (default true)')),
43
- width: opt(num('Page width (default 1440)')),
44
- navItems: opt(arr('Navigation link labels')),
45
- features: opt(arr('Feature objects with icon, title, desc fields')),
46
- stats: opt(arr('Stat objects with value and label fields')),
47
- tiers: opt(arr('Pricing tier objects with name, price, period, desc, features, cta, highlighted fields')),
48
- metrics: opt(arr('Dashboard metric objects with label, value, change, positive fields')),
49
- }, required: ['pageType'] } },
50
-
51
- { name: 'create_section', category: 'create',
52
- description: 'Creates a complete page section with all child elements, auto-layout, and proper nesting. One call generates 5-20 Figma elements. Use this INSTEAD of manually building sections. Supports: hero, features, pricing, cta, testimonials, faq section types.',
53
- inputSchema: { type: 'object', properties: {
54
- sectionType: str('Section type: hero, features, testimonials, faq, cta, pricing'),
55
- heading: opt(str('Section heading')),
56
- subheading: opt(str('Section subtitle')),
57
- brandColor: opt(str('Brand color hex')),
58
- ctaText: opt(str('CTA button text')),
59
- width: opt(num('Section width (default 1440)')),
60
- features: opt(arr('Feature objects for features section')),
61
- testimonials: opt(arr('Testimonial objects with quote, author, role')),
62
- faqs: opt(arr('FAQ objects with q and a fields')),
63
- }, required: ['sectionType'] } },
64
-
65
- { name: 'create_card', category: 'create',
66
- description: 'Create a card with proper padding, radius, shadow depth, and content hierarchy.',
67
- inputSchema: { type: 'object', properties: { variant: opt(str('Card variant: default, elevated, outlined, filled')), title: opt(str('Card title')), description: opt(str('Card description')), hasImage: opt(bool('Include image placeholder')), hasAction: opt(bool('Include action button')), width: opt(num('Card width')) }, required: [] } },
68
-
69
- { name: 'create_form', category: 'create',
70
- description: 'Create a form layout with labels, inputs, validation states, help text, and submit flow.',
71
- inputSchema: { type: 'object', properties: { fields: arr('Field definitions', { type: 'object', properties: { name: str('Field name'), type: str('text, email, password, select, textarea, checkbox, radio'), label: str('Field label'), required: bool('Required field') } }), submitLabel: opt(str('Submit button text')), layout: opt(str('vertical or horizontal')) }, required: ['fields'] } },
72
-
73
- { name: 'create_table', category: 'create',
74
- description: 'Create a data table with header row, data rows, sorting indicators, and pagination controls.',
75
- inputSchema: { type: 'object', properties: { columns: arr('Column definitions', { type: 'object', properties: { name: str('Column name'), width: num('Column width') } }), rows: opt(num('Number of data rows')), hasPagination: opt(bool('Include pagination')), hasSorting: opt(bool('Include sort indicators')), hasCheckbox: opt(bool('Include row checkboxes')) }, required: ['columns'] } },
76
-
77
- { name: 'create_modal', category: 'create',
78
- description: 'Create a modal with overlay, header, content area, and action buttons. Proper sizing and constraints.',
79
- inputSchema: { type: 'object', properties: { title: str('Modal title'), size: opt(str('sm, md, lg, xl')), hasCloseButton: opt(bool('Include close button')), actions: opt(arr('Action button labels')) }, required: ['title'] } },
80
-
81
- { name: 'create_nav', category: 'create',
82
- description: 'Create navigation: topbar, sidebar, breadcrumbs, or tabs. Includes active states and responsive structure.',
83
- inputSchema: { type: 'object', properties: { navType: str('topbar, sidebar, breadcrumbs, tabs, bottom-nav'), items: arr('Navigation items'), logoText: opt(str('Logo text')), hasSearch: opt(bool('Include search')), hasAvatar: opt(bool('Include user avatar')) }, required: ['navType', 'items'] } },
84
-
85
- // ═══ LAYOUT ═══
86
- { name: 'layout_auto', category: 'layout',
87
- description: 'Convert any frame to auto-layout with inferred direction, gap, and padding. Removes absolute positioning.',
88
- inputSchema: { type: 'object', properties: { nodeId: str('Figma node ID to convert'), direction: opt(str('horizontal or vertical (auto-detected if omitted)')), gap: opt(num('Gap in px (auto-detected if omitted)')), padding: opt(num('Padding in px')) }, required: ['nodeId'] } },
89
-
90
- { name: 'layout_grid', category: 'layout',
91
- description: 'Apply a column grid to a frame: 12-col, 8-col, or custom with gutters and margins.',
92
- inputSchema: { type: 'object', properties: { nodeId: str('Figma node ID'), columns: opt(num('Number of columns (default 12)')), gutter: opt(num('Gutter width')), margin: opt(num('Outer margin')), type: opt(str('stretch, center, left, right')) }, required: ['nodeId'] } },
93
-
94
- { name: 'layout_stack', category: 'layout',
95
- description: 'Stack children vertically or horizontally with consistent grid-aligned spacing.',
96
- inputSchema: { type: 'object', properties: { nodeId: str('Parent node ID'), direction: str('horizontal or vertical'), gap: opt(num('Gap (snapped to grid)')), align: opt(str('start, center, end, stretch')) }, required: ['nodeId', 'direction'] } },
97
-
98
- { name: 'layout_wrap', category: 'layout',
99
- description: 'Create a wrap-layout for tag clouds, pill groups, or flexible card grids.',
100
- inputSchema: { type: 'object', properties: { nodeId: str('Parent node ID'), gap: opt(num('Gap between items')), maxWidth: opt(num('Max width before wrapping')) }, required: ['nodeId'] } },
101
-
102
- { name: 'layout_constrain', category: 'layout',
103
- description: 'Set responsive constraints on a frame: fill, hug, fixed, min/max width and height.',
104
- inputSchema: { type: 'object', properties: { nodeId: str('Node ID'), horizontal: opt(str('fill, hug, fixed')), vertical: opt(str('fill, hug, fixed')), minWidth: opt(num('Min width')), maxWidth: opt(num('Max width')), minHeight: opt(num('Min height')), maxHeight: opt(num('Max height')) }, required: ['nodeId'] } },
105
-
106
- { name: 'layout_align', category: 'layout',
107
- description: 'Align and distribute selected frames on the grid. Handles both alignment and even distribution.',
108
- inputSchema: { type: 'object', properties: { nodeIds: arr('Node IDs to align'), alignment: str('left, center, right, top, middle, bottom, distribute-h, distribute-v') }, required: ['nodeIds', 'alignment'] } },
109
-
110
- { name: 'layout_nest', category: 'layout',
111
- description: 'Restructure flat, absolute-positioned layers into a proper auto-layout tree based on spatial relationships.',
112
- inputSchema: { type: 'object', properties: { nodeId: str('Parent frame to restructure'), depth: opt(num('Max nesting depth (default 3)')) }, required: ['nodeId'] } },
113
-
114
- // ═══ TYPOGRAPHY ═══
115
- { name: 'type_scale', category: 'typography',
116
- description: 'Generate or detect a type scale. Supports minor third (1.2), major third (1.25), perfect fourth (1.333), and more.',
117
- inputSchema: { type: 'object', properties: { action: str('generate or detect'), baseSize: opt(num('Base font size (default 16)')), scaleRatio: opt(str('Scale name: minor-third, major-third, perfect-fourth, etc.')), nodeId: opt(str('Node ID to detect scale from')) }, required: ['action'] } },
118
-
119
- { name: 'type_hierarchy', category: 'typography',
120
- description: 'Set heading levels with proper size, weight, and line-height ratios across a frame.',
121
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to apply hierarchy to'), levels: opt(num('Number of heading levels (default 4)')), baseSize: opt(num('Body text size')) }, required: ['nodeId'] } },
122
-
123
- { name: 'type_pair', category: 'typography',
124
- description: 'Suggest font pairings from loaded fonts or recommend Google Fonts combinations.',
125
- inputSchema: { type: 'object', properties: { headingFont: opt(str('Heading font family')), bodyFont: opt(str('Body font family')), style: opt(str('modern, classic, technical, editorial, playful')) }, required: [] } },
126
-
127
- { name: 'type_measure', category: 'typography',
128
- description: 'Check line length (45-75 chars optimal), line-height ratios, and letter-spacing for readability.',
129
- inputSchema: { type: 'object', properties: { nodeId: str('Text node or frame to check') }, required: ['nodeId'] } },
130
-
131
- { name: 'type_apply', category: 'typography',
132
- description: 'Apply text styles consistently to all matching text elements across frames.',
133
- inputSchema: { type: 'object', properties: { nodeId: str('Root frame'), styles: { type: 'object', description: 'Text styles to apply by element type' } }, required: ['nodeId'] } },
134
-
135
- { name: 'type_audit', category: 'typography',
136
- description: 'Find every unique text style in a page. Flag off-scale sizes, inconsistent weights, and orphaned styles.',
137
- inputSchema: { type: 'object', properties: { nodeId: str('Frame or page to audit') }, required: ['nodeId'] } },
138
-
139
- // ═══ COLOR & STYLE ═══
140
- { name: 'color_palette', category: 'color',
141
- description: 'Generate a full color palette (50-950 shades) from a single brand color.',
142
- inputSchema: { type: 'object', properties: { baseColor: str('Brand color hex'), steps: opt(arr('Shade steps', { type: 'number' })) }, required: ['baseColor'] } },
143
-
144
- { name: 'color_semantic', category: 'color',
145
- description: 'Create semantic color tokens: surface, text, border, accent, danger, success, warning, info.',
146
- inputSchema: { type: 'object', properties: { brandColor: str('Primary brand color hex') }, required: ['brandColor'] } },
147
-
148
- { name: 'color_darkmode', category: 'color',
149
- description: 'Generate a dark mode variant of any frame or color set, preserving WCAG contrast ratios.',
150
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to generate dark mode for')), colors: opt({ type: 'object', description: 'Color map to invert' }) }, required: [] } },
151
-
152
- { name: 'color_contrast', category: 'color',
153
- description: 'Check WCAG contrast for all text/background pairs in a frame. Reports AA and AAA compliance.',
154
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to check')), foreground: opt(str('Foreground color hex')), background: opt(str('Background color hex')) }, required: [] } },
155
-
156
- { name: 'color_apply', category: 'color',
157
- description: 'Apply color styles to elements by semantic role: primary, secondary, muted, accent.',
158
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to apply colors to'), colorMap: { type: 'object', description: 'Role-to-color mapping' } }, required: ['nodeId'] } },
159
-
160
- { name: 'style_shadow', category: 'color',
161
- description: 'Generate an elevation/shadow system: sm, md, lg, xl, 2xl with consistent blur, spread, and opacity.',
162
- inputSchema: { type: 'object', properties: { levels: opt(arr('Shadow levels to generate')), color: opt(str('Shadow color hex')) }, required: [] } },
163
-
164
- { name: 'style_radius', category: 'color',
165
- description: 'Generate and apply a consistent border-radius scale across all frames.',
166
- inputSchema: { type: 'object', properties: { base: opt(num('Base radius (default 4)')), nodeId: opt(str('Frame to apply to')) }, required: [] } },
167
-
168
- // ═══ COMPONENTS ═══
169
- { name: 'component_list', category: 'components',
170
- description: 'List all components in the current file or team library, with variant info.',
171
- inputSchema: { type: 'object', properties: { source: opt(str('file or library')), filter: opt(str('Search filter')) }, required: [] } },
172
-
173
- { name: 'component_use', category: 'components',
174
- description: 'Instantiate a component by name with variant properties set.',
175
- inputSchema: { type: 'object', properties: { componentName: str('Component to instantiate'), variants: opt({ type: 'object', description: 'Variant property values' }), x: opt(num('X position')), y: opt(num('Y position')) }, required: ['componentName'] } },
176
-
177
- { name: 'component_create', category: 'components',
178
- description: 'Create a new component from a frame with proper variant structure.',
179
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to convert to component'), name: str('Component name'), variants: opt(arr('Variant property definitions')) }, required: ['nodeId', 'name'] } },
180
-
181
- { name: 'component_swap', category: 'components',
182
- description: 'Swap one component instance for another across an entire frame tree.',
183
- inputSchema: { type: 'object', properties: { nodeId: str('Root frame to search'), fromComponent: str('Component to replace'), toComponent: str('Component to replace with') }, required: ['nodeId', 'fromComponent', 'toComponent'] } },
184
-
185
- { name: 'component_detach', category: 'components',
186
- description: 'Detach component instances and flatten to editable frames.',
187
- inputSchema: { type: 'object', properties: { nodeId: str('Instance or frame to detach') }, required: ['nodeId'] } },
188
-
189
- { name: 'component_audit', category: 'components',
190
- description: 'Find detached instances, missing components, unused variants, and inconsistent overrides.',
191
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to audit (entire page if omitted)')) }, required: [] } },
192
-
193
- // ═══ SPACING & GRID ═══
194
- { name: 'spacing_scale', category: 'spacing',
195
- description: 'Define a base spacing unit (4px or 8px) and generate the full scale.',
196
- inputSchema: { type: 'object', properties: { base: opt(num('Base unit (default 8)')), steps: opt(num('Number of steps (default 12)')) }, required: [] } },
197
-
198
- { name: 'spacing_fix', category: 'spacing',
199
- description: 'Snap all off-grid spacing values in a frame to the nearest scale value.',
200
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to fix'), base: opt(num('Base unit (default 8)')) }, required: ['nodeId'] } },
201
-
202
- { name: 'spacing_audit', category: 'spacing',
203
- description: 'Report every spacing value in a frame: padding, margin, gap. Flag inconsistencies and off-grid values.',
204
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to audit'), base: opt(num('Base unit (default 8)')) }, required: ['nodeId'] } },
205
-
206
- { name: 'spacing_rhythm', category: 'spacing',
207
- description: 'Check vertical rhythm: are section gaps consistent and proportional? Flags irregular spacing patterns.',
208
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to check') }, required: ['nodeId'] } },
209
-
210
- { name: 'grid_apply', category: 'spacing',
211
- description: 'Apply a layout grid to a frame: columns, rows, or custom grid with configurable gutter and margin.',
212
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to apply grid to'), type: str('columns, rows, grid'), count: opt(num('Column/row count')), gutter: opt(num('Gutter size')), margin: opt(num('Margin')) }, required: ['nodeId', 'type'] } },
213
-
214
- { name: 'grid_check', category: 'spacing',
215
- description: 'Verify all children of a frame align to the parent layout grid.',
216
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to check') }, required: ['nodeId'] } },
217
-
218
- // ═══ AUDIT & CRITIQUE ═══
219
- { name: 'audit_full', category: 'audit',
220
- description: 'Full design audit: spacing, typography, color, components, accessibility, hierarchy. Returns a 0-100 score with per-category breakdown.',
221
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to audit (current page if omitted)')) }, required: [] } },
222
-
223
- { name: 'audit_hierarchy', category: 'audit',
224
- description: 'Check visual hierarchy: are primary elements dominant? Are heading levels properly differentiated?',
225
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to check') }, required: ['nodeId'] } },
226
-
227
- { name: 'audit_consistency', category: 'audit',
228
- description: 'Find elements that look similar but use different styles. Flags inconsistent text, colors, spacing, and radii.',
229
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to check') }, required: ['nodeId'] } },
230
-
231
- { name: 'audit_alignment', category: 'audit',
232
- description: 'Flag misaligned elements, uneven margins, off-center content, and broken visual axes.',
233
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to check'), tolerance: opt(num('Pixel tolerance (default 2)')) }, required: ['nodeId'] } },
234
-
235
- { name: 'audit_density', category: 'audit',
236
- description: 'Check information density: too cramped or too sparse? Measures whitespace ratio and content distribution.',
237
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to check') }, required: ['nodeId'] } },
238
-
239
- { name: 'audit_score', category: 'audit',
240
- description: 'Generate a 0-100 design health score with per-category breakdown and actionable improvement suggestions.',
241
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to score')) }, required: [] } },
242
-
243
- // ═══ ACCESSIBILITY ═══
244
- { name: 'a11y_contrast', category: 'accessibility',
245
- description: 'Check all text/background color pairs against WCAG AA (4.5:1) and AAA (7:1) standards.',
246
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to check')), level: opt(str('aa or aaa (default aa)')) }, required: [] } },
247
-
248
- { name: 'a11y_touch', category: 'accessibility',
249
- description: 'Verify all interactive elements meet the 44x44px minimum touch target size.',
250
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to check'), minSize: opt(num('Minimum size in px (default 44)')) }, required: ['nodeId'] } },
251
-
252
- { name: 'a11y_focus', category: 'accessibility',
253
- description: 'Generate focus ring styles for all interactive elements: buttons, inputs, links, toggles.',
254
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to add focus states to'), color: opt(str('Focus ring color hex')), offset: opt(num('Ring offset in px')) }, required: ['nodeId'] } },
255
-
256
- { name: 'a11y_labels', category: 'accessibility',
257
- description: 'Check for missing labels on inputs, buttons, and icon-only elements. Flags unlabeled interactive content.',
258
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to check') }, required: ['nodeId'] } },
259
-
260
- { name: 'a11y_fix', category: 'accessibility',
261
- description: 'Auto-fix accessibility issues: bump insufficient contrast, enlarge small touch targets, add visible focus states.',
262
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to fix'), fixes: opt(arr('Specific fixes: contrast, touch, focus, labels')) }, required: ['nodeId'] } },
263
-
264
- // ═══ RESPONSIVE ═══
265
- { name: 'responsive_variant', category: 'responsive',
266
- description: 'Generate mobile (375px), tablet (768px), and desktop (1440px) variants of any frame.',
267
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to generate variants from'), breakpoints: opt(arr('Breakpoint widths', { type: 'number' })) }, required: ['nodeId'] } },
268
-
269
- { name: 'responsive_reflow', category: 'responsive',
270
- description: 'Reflow a desktop layout into mobile: stack columns, resize typography, adjust spacing.',
271
- inputSchema: { type: 'object', properties: { nodeId: str('Desktop frame to reflow'), targetWidth: num('Target width in px') }, required: ['nodeId', 'targetWidth'] } },
272
-
273
- { name: 'responsive_breakpoints', category: 'responsive',
274
- description: 'Set up breakpoint frames side by side: 375, 768, 1024, 1440. Ready for responsive design.',
275
- inputSchema: { type: 'object', properties: { widths: opt(arr('Breakpoint widths', { type: 'number' })), height: opt(num('Frame height')) }, required: [] } },
276
-
277
- { name: 'responsive_check', category: 'responsive',
278
- description: 'Verify text doesn\'t overflow, images maintain aspect ratio, and touch targets remain accessible at each breakpoint.',
279
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to check') }, required: ['nodeId'] } },
280
-
281
- // ═══ EXPORT & HANDOFF ═══
282
- { name: 'export_tokens_css', category: 'export',
283
- description: 'Export all design tokens as CSS custom properties.',
284
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to extract tokens from')), includeColors: opt(bool('Include colors')), includeSpacing: opt(bool('Include spacing')), includeTypography: opt(bool('Include typography')) }, required: [] } },
285
-
286
- { name: 'export_tokens_tailwind', category: 'export',
287
- description: 'Export tokens as a Tailwind CSS config theme extension.',
288
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to extract tokens from')) }, required: [] } },
289
-
290
- { name: 'export_tokens_json', category: 'export',
291
- description: 'Export tokens in W3C Design Tokens format (JSON).',
292
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to extract tokens from')) }, required: [] } },
293
-
294
- { name: 'export_tokens_scss', category: 'export',
295
- description: 'Export tokens as SCSS variables and maps.',
296
- inputSchema: { type: 'object', properties: { nodeId: opt(str('Frame to extract tokens from')) }, required: [] } },
297
-
298
- { name: 'export_spec', category: 'export',
299
- description: 'Generate a design spec: measurements, spacing tokens, color values, component annotations.',
300
- inputSchema: { type: 'object', properties: { nodeId: str('Frame to generate spec for'), format: opt(str('markdown or json')) }, required: ['nodeId'] } },
301
-
302
- { name: 'export_changelog', category: 'export',
303
- description: 'Diff two frames and export what changed as structured markdown or JSON.',
304
- inputSchema: { type: 'object', properties: { nodeIdA: str('First frame (before)'), nodeIdB: str('Second frame (after)'), format: opt(str('markdown or json')) }, required: ['nodeIdA', 'nodeIdB'] } },
305
-
306
- // ═══ DESIGN CRAFT ═══
307
- { name: 'get_design_craft_guide', category: 'craft',
308
- description: 'CALL THIS FIRST before any design work. Returns professional design rules: typography scales, spacing systems (8px grid), color palettes, component patterns, and anti-patterns. Following these rules is the difference between amateur and production-quality output. Read the full guide before calling any create_ tools.',
309
- inputSchema: { type: 'object', properties: {}, required: [] } },
310
-
311
- // ═══ CREATE (additional) ═══
312
- { name: 'create_ellipse', category: 'create',
313
- description: 'Create a circle or oval. Use for avatars, status indicators, decorative elements, and icon backgrounds. Set equal width and height for a perfect circle.',
314
- inputSchema: { type: 'object', properties: { name: opt(str('Element name')), width: num('Width in px'), height: num('Height in px'), fill: opt(str('Fill color hex')), x: opt(num('X position')), y: opt(num('Y position')), parentId: opt(str('Parent frame ID')), opacity: opt(num('Opacity 0-1')) }, required: ['width', 'height'] } },
315
-
316
- { name: 'create_line', category: 'create',
317
- description: 'Create a horizontal or vertical line. Use for dividers between sections, separators in navigation, and visual breaks. A line is a rectangle with 1px height (horizontal) or 1px width (vertical).',
318
- inputSchema: { type: 'object', properties: { name: opt(str('Element name')), length: num('Length in px'), direction: opt(str('horizontal or vertical (default horizontal)')), color: opt(str('Line color hex')), parentId: opt(str('Parent frame ID')) }, required: ['length'] } },
319
-
320
- { name: 'create_svg_node', category: 'create',
321
- description: 'Create a vector graphic from raw SVG markup. Use for icons, logos, illustrations, and any custom vector shape. Pass the SVG string directly.',
322
- inputSchema: { type: 'object', properties: { name: opt(str('Element name')), svg: str('Raw SVG markup string'), x: opt(num('X position')), y: opt(num('Y position')), parentId: opt(str('Parent frame ID')) }, required: ['svg'] } },
323
-
324
- { name: 'find_nodes', category: 'create',
325
- description: 'Search for elements by name or type within a frame or the entire page. Returns matching nodes with their IDs, positions, and basic properties. Use to locate elements before editing them.',
326
- inputSchema: { type: 'object', properties: { query: opt(str('Search by name (partial match)')), type: opt(str('Filter by type: FRAME, TEXT, RECTANGLE, ELLIPSE, COMPONENT, INSTANCE')), parentId: opt(str('Search within this frame only')) }, required: [] } },
327
-
328
- // ═══ TYPOGRAPHY (additional) ═══
329
- { name: 'style_text_range', category: 'typography',
330
- description: 'Apply different styles to specific character ranges within a text node. Use for mixed-weight text (e.g., bold product name within a regular sentence), colored keywords, or size variations within a single text element.',
331
- inputSchema: { type: 'object', properties: { nodeId: str('Text node ID'), start: num('Start character index'), end: num('End character index'), fontSize: opt(num('Font size for range')), fontWeight: opt(num('Font weight: 400, 500, 600, 700')), color: opt(str('Color hex for range')) }, required: ['nodeId', 'start', 'end'] } },
332
- ];
333
-
334
- export function getToolsByCategory(category) {
335
- return TOOLS.filter(t => t.category === category);
5
+ function tool(name, desc, params, category) {
6
+ return { name, description: desc, inputSchema: { type: 'object', properties: params, required: Object.keys(params).filter(k => params[k]._required) }, category }
7
+ }
8
+ function req(type, desc) { return { type, description: desc, _required: true } }
9
+ function opt(type, desc, def) { return { type, description: desc + (def !== undefined ? ` (default: ${def})` : '') } }
10
+ function enm(values, desc, def) { return { type: 'string', enum: values, description: desc + (def ? ` (default: ${def})` : '') } }
11
+
12
+ // ═══ CREATE & LAYOUT (25) ═══
13
+ const CREATE = {
14
+ create_frame: tool('create_frame', 'Create a frame (screen, section, card, container). Automatically applies auto-layout with design-intelligent defaults.', {
15
+ name: req('string', 'Frame name (semantic: "Hero Section", "Card", "Sidebar")'),
16
+ width: opt('number', 'Width in pixels. Omit for hug-contents.'),
17
+ height: opt('number', 'Height in pixels. Omit for hug-contents.'),
18
+ direction: enm(['VERTICAL','HORIZONTAL'], 'Layout direction', 'VERTICAL'),
19
+ padding: opt('number', 'Equal padding all sides (8px grid snapped)'),
20
+ paddingTop: opt('number', 'Top padding'), paddingRight: opt('number', 'Right padding'),
21
+ paddingBottom: opt('number', 'Bottom padding'), paddingLeft: opt('number', 'Left padding'),
22
+ gap: opt('number', 'Space between children (8px grid)'),
23
+ fill: opt('string', 'Background color hex'),
24
+ cornerRadius: opt('number', 'Corner radius'),
25
+ primaryAxisAlignItems: enm(['MIN','CENTER','MAX','SPACE_BETWEEN'], 'Main axis alignment'),
26
+ counterAxisAlignItems: enm(['MIN','CENTER','MAX','STRETCH'], 'Cross axis alignment'),
27
+ primaryAxisSizingMode: enm(['FIXED','HUG','FILL'], 'Main axis sizing'),
28
+ counterAxisSizingMode: enm(['FIXED','HUG','FILL'], 'Cross axis sizing'),
29
+ parentId: opt('string', 'Parent node ID to nest inside'),
30
+ x: opt('number', 'X position on canvas'), y: opt('number', 'Y position on canvas'),
31
+ }, 'create'),
32
+
33
+ create_text: tool('create_text', 'Create text with design-intelligent typography. Font weight is auto-resolved (e.g. "bold" → "Bold", "600" → "Semi Bold").', {
34
+ text: req('string', 'Text content (use \\n for line breaks)'),
35
+ fontSize: opt('number', 'Font size in px', 16),
36
+ color: opt('string', 'Text color hex', '#ffffff'),
37
+ fontFamily: opt('string', 'Font family', 'Inter'),
38
+ fontWeight: opt('string', 'Weight: thin/light/regular/medium/semibold/bold/extrabold or 100-900', 'Regular'),
39
+ textAlignHorizontal: enm(['LEFT','CENTER','RIGHT','JUSTIFIED'], 'Horizontal alignment'),
40
+ textAlignVertical: enm(['TOP','CENTER','BOTTOM'], 'Vertical alignment'),
41
+ lineHeight: opt('number', 'Line height in px or ratio (1.5 = 150%)'),
42
+ letterSpacing: opt('number', 'Letter spacing in px'),
43
+ textDecoration: enm(['NONE','UNDERLINE','STRIKETHROUGH'], 'Text decoration'),
44
+ textCase: enm(['ORIGINAL','UPPER','LOWER','TITLE'], 'Text case transform'),
45
+ maxWidth: opt('number', 'Maximum text width for wrapping'),
46
+ parentId: opt('string', 'Parent node ID'),
47
+ }, 'create'),
48
+
49
+ create_rectangle: tool('create_rectangle', 'Create a rectangle shape.', {
50
+ name: opt('string', 'Name', 'Rectangle'),
51
+ width: req('number', 'Width'), height: req('number', 'Height'),
52
+ fill: opt('string', 'Fill color hex'),
53
+ cornerRadius: opt('number', 'Corner radius'), opacity: opt('number', 'Opacity 0-1'),
54
+ parentId: opt('string', 'Parent node ID'),
55
+ }, 'create'),
56
+
57
+ create_ellipse: tool('create_ellipse', 'Create a circle or oval.', {
58
+ name: opt('string', 'Name', 'Ellipse'),
59
+ width: req('number', 'Width'), height: req('number', 'Height'),
60
+ fill: opt('string', 'Fill color hex'), opacity: opt('number', 'Opacity 0-1'),
61
+ parentId: opt('string', 'Parent node ID'),
62
+ }, 'create'),
63
+
64
+ create_line: tool('create_line', 'Create a line or divider.', {
65
+ name: opt('string', 'Name', 'Line'),
66
+ length: req('number', 'Line length'), direction: enm(['HORIZONTAL','VERTICAL'], 'Direction', 'HORIZONTAL'),
67
+ strokeColor: opt('string', 'Stroke color'), strokeWeight: opt('number', 'Stroke weight', 1),
68
+ parentId: opt('string', 'Parent node ID'),
69
+ }, 'create'),
70
+
71
+ create_svg_node: tool('create_svg_node', 'Create a vector graphic from SVG markup. Use for icons, logos, illustrations.', {
72
+ svg: req('string', 'SVG markup string'),
73
+ name: opt('string', 'Node name'),
74
+ width: opt('number', 'Target width'), height: opt('number', 'Target height'),
75
+ parentId: opt('string', 'Parent node ID'),
76
+ }, 'create'),
77
+
78
+ create_component: tool('create_component', 'Create a reusable component from the current selection or a new frame.', {
79
+ name: req('string', 'Component name'),
80
+ description: opt('string', 'Component description'),
81
+ fromNodeId: opt('string', 'Node ID to convert to component'),
82
+ width: opt('number', 'Width'), height: opt('number', 'Height'),
83
+ parentId: opt('string', 'Parent node ID'),
84
+ }, 'create'),
85
+
86
+ create_component_instance: tool('create_component_instance', 'Instantiate an existing component.', {
87
+ componentId: req('string', 'Component node ID to instantiate'),
88
+ parentId: opt('string', 'Parent to place instance in'),
89
+ overrides: opt('object', 'Property overrides: { "Text Label": "New text", "fill": "#ff0000" }'),
90
+ }, 'create'),
91
+
92
+ create_component_set: tool('create_component_set', 'Combine component variants into a variant set.', {
93
+ name: req('string', 'Component set name'),
94
+ componentIds: { type:'array', items:{type:'string'}, description:'Array of component IDs to combine', _required:true },
95
+ }, 'create'),
96
+
97
+ create_smart_component: tool('create_smart_component', 'Create a design-intelligent component with proper auto-layout, padding, and sizing. Uses component intelligence defaults.', {
98
+ type: req('string', 'Component type: button, input, card, avatar, badge, chip, switch, checkbox, radio, toast, tooltip, modal, dropdown, tabs, table, progress, skeleton, divider'),
99
+ variant: opt('string', 'Variant: default, sm, lg, icon, compact, spacious, pill, thin'),
100
+ label: opt('string', 'Text label for the component'),
101
+ brandColor: opt('string', 'Brand/accent color hex'),
102
+ mode: enm(['dark','light'], 'Color mode', 'dark'),
103
+ parentId: opt('string', 'Parent node ID'),
104
+ }, 'create'),
105
+
106
+ set_auto_layout: tool('set_auto_layout', 'Configure auto-layout on a frame. All spacing is 8px grid-snapped.', {
107
+ nodeId: req('string', 'Target node ID'),
108
+ direction: enm(['VERTICAL','HORIZONTAL'], 'Direction'),
109
+ padding: opt('number', 'Uniform padding'),
110
+ paddingTop: opt('number',''), paddingRight: opt('number',''), paddingBottom: opt('number',''), paddingLeft: opt('number',''),
111
+ gap: opt('number', 'Gap between children'),
112
+ primaryAxisAlignItems: enm(['MIN','CENTER','MAX','SPACE_BETWEEN'], 'Main axis'),
113
+ counterAxisAlignItems: enm(['MIN','CENTER','MAX','STRETCH'], 'Cross axis'),
114
+ primaryAxisSizingMode: enm(['FIXED','HUG','FILL'], 'Main sizing'),
115
+ counterAxisSizingMode: enm(['FIXED','HUG','FILL'], 'Cross sizing'),
116
+ }, 'create'),
117
+
118
+ create_section: tool('create_section', 'Create a design-intelligent page section (hero, features, pricing, CTA, testimonials, FAQ, footer, stats, logos, comparison, team).', {
119
+ type: req('string', 'Section type: hero, features, pricing, cta, testimonials, faq, footer, stats, logos, comparison, team, newsletter'),
120
+ content: opt('object', 'Content data: { title, subtitle, items[], ... }'),
121
+ brandColor: opt('string', 'Brand color hex'),
122
+ mode: enm(['dark','light'], 'Color mode', 'dark'),
123
+ width: opt('number', 'Frame width', 1440),
124
+ parentId: opt('string', 'Parent node ID'),
125
+ }, 'create'),
126
+
127
+ create_page: tool('create_page', 'Create a complete page design with multiple sections. Design-intelligent layout, spacing, and typography throughout.', {
128
+ type: req('string', 'Page type: landing, pricing, dashboard, settings, login, signup, profile, blog, docs, 404'),
129
+ content: opt('object', 'Page content: { brand, title, features[], stats[], tiers[], ... }'),
130
+ brandColor: opt('string', 'Brand color hex'),
131
+ mode: enm(['dark','light'], 'Color mode', 'dark'),
132
+ width: opt('number', 'Frame width', 1440),
133
+ }, 'create'),
134
+
135
+ create_table_frame: tool('create_table_frame', 'Create a data table with headers, rows, and proper spacing.', {
136
+ columns: { type:'array', items:{type:'string'}, description:'Column headers', _required:true },
137
+ rows: opt('array', 'Row data: arrays of cell values'),
138
+ brandColor: opt('string', 'Accent color'),
139
+ parentId: opt('string', 'Parent node'),
140
+ }, 'create'),
141
+
142
+ create_form: tool('create_form', 'Create a form layout with labeled inputs, validation states, and submit button.', {
143
+ fields: { type:'array', items:{type:'object'}, description:'Fields: [{ label, type: "text|email|password|select|textarea", required }]', _required:true },
144
+ submitLabel: opt('string', 'Submit button text', 'Submit'),
145
+ brandColor: opt('string', 'Accent color'),
146
+ parentId: opt('string', 'Parent node'),
147
+ }, 'create'),
148
+
149
+ create_nav_bar: tool('create_nav_bar', 'Create a navigation bar with logo, links, and CTA button.', {
150
+ brand: opt('string', 'Brand name', 'acme'),
151
+ items: opt('array', 'Nav items: ["Features", "Pricing", "Docs"]'),
152
+ ctaText: opt('string', 'CTA button text'),
153
+ width: opt('number', 'Width', 1440),
154
+ brandColor: opt('string', 'Accent color'),
155
+ parentId: opt('string', 'Parent node'),
156
+ }, 'create'),
157
+
158
+ create_card_grid: tool('create_card_grid', 'Create a grid of cards with proper spacing and alignment.', {
159
+ columns: opt('number', 'Number of columns', 3),
160
+ cards: opt('array', 'Card data: [{ title, description, icon }]'),
161
+ brandColor: opt('string', 'Accent color'),
162
+ parentId: opt('string', 'Parent node'),
163
+ }, 'create'),
164
+
165
+ create_sidebar_layout: tool('create_sidebar_layout', 'Create a sidebar + main content layout with proper proportions.', {
166
+ sidebarWidth: opt('number', 'Sidebar width', 260),
167
+ totalWidth: opt('number', 'Total width', 1440),
168
+ brandColor: opt('string', 'Accent color'),
169
+ parentId: opt('string', 'Parent node'),
170
+ }, 'create'),
171
+
172
+ create_footer: tool('create_footer', 'Create a page footer with columns, links, and copyright.', {
173
+ brand: opt('string', 'Brand name'),
174
+ columns: opt('array', 'Footer columns: [{ title, links: ["Link 1", "Link 2"] }]'),
175
+ width: opt('number', 'Width', 1440),
176
+ brandColor: opt('string', 'Accent color'),
177
+ parentId: opt('string', 'Parent node'),
178
+ }, 'create'),
179
+
180
+ create_header: tool('create_header', 'Create a page header/hero with heading, subheading, and CTA.', {
181
+ heading: opt('string', 'Main heading'),
182
+ subheading: opt('string', 'Subheading text'),
183
+ ctaText: opt('string', 'CTA button text'),
184
+ width: opt('number', 'Width', 1440),
185
+ brandColor: opt('string', 'Accent color'),
186
+ parentId: opt('string', 'Parent node'),
187
+ }, 'create'),
188
+ }
189
+
190
+ // ═══ MODIFY & STYLE (25) ═══
191
+ const MODIFY = {
192
+ modify_node: tool('modify_node', 'Modify properties of any existing node.', {
193
+ nodeId: req('string', 'Target node ID'),
194
+ x: opt('number','X'), y: opt('number','Y'), width: opt('number','Width'), height: opt('number','Height'),
195
+ name: opt('string','Name'), visible: opt('boolean','Visibility'), locked: opt('boolean','Lock'),
196
+ opacity: opt('number','Opacity 0-1'), rotation: opt('number','Rotation degrees'),
197
+ cornerRadius: opt('number','Corner radius'), fill: opt('string','Fill color'),
198
+ }, 'modify'),
199
+
200
+ set_fill: tool('set_fill', 'Set fill on a node. Supports solid, linear gradient, radial gradient, and multiple fills.', {
201
+ nodeId: req('string', 'Target node ID'),
202
+ type: enm(['SOLID','LINEAR','RADIAL','ANGULAR','DIAMOND'], 'Fill type', 'SOLID'),
203
+ color: opt('string', 'Solid fill hex color'),
204
+ gradient: opt('object', '{ angle, stops: [{ position: 0-1, color: "#hex" }] }'),
205
+ opacity: opt('number', 'Fill opacity 0-1'),
206
+ fills: opt('array', 'Array of fill objects for multiple fills'),
207
+ }, 'modify'),
208
+
209
+ set_stroke: tool('set_stroke', 'Add border/stroke to a node.', {
210
+ nodeId: req('string', 'Target node ID'),
211
+ color: opt('string', 'Stroke color hex'), weight: opt('number', 'Stroke weight', 1),
212
+ align: enm(['INSIDE','OUTSIDE','CENTER'], 'Stroke alignment', 'INSIDE'),
213
+ dashPattern: opt('array', 'Dash pattern array, e.g. [4, 4]'),
214
+ opacity: opt('number', 'Stroke opacity'),
215
+ }, 'modify'),
216
+
217
+ set_effects: tool('set_effects', 'Add shadows, blur, or background blur effects.', {
218
+ nodeId: req('string', 'Target node ID'),
219
+ shadow: opt('object', '{ color, offsetX, offsetY, blur, spread }'),
220
+ innerShadow: opt('object', '{ color, offsetX, offsetY, blur, spread }'),
221
+ blur: opt('number', 'Layer blur amount'),
222
+ backgroundBlur: opt('number', 'Background blur (glassmorphism)'),
223
+ preset: enm(['sm','md','lg','xl'], 'Use preset shadow size'),
224
+ }, 'modify'),
225
+
226
+ set_image_fill: tool('set_image_fill', 'Set an image fill on a node from URL.', {
227
+ nodeId: req('string', 'Target node ID'),
228
+ url: req('string', 'Image URL'),
229
+ scaleMode: enm(['FILL','FIT','CROP','TILE'], 'Scale mode', 'FILL'),
230
+ }, 'modify'),
231
+
232
+ style_text_range: tool('style_text_range', 'Apply mixed styling within a text node. Style specific character ranges differently.', {
233
+ nodeId: req('string', 'Text node ID'),
234
+ ranges: { type:'array', items:{type:'object'}, description:'Array of { start, end, fontSize, fontWeight, color, textDecoration }', _required:true },
235
+ }, 'modify'),
236
+
237
+ set_constraints: tool('set_constraints', 'Set responsive constraints on a node.', {
238
+ nodeId: req('string', 'Target node ID'),
239
+ horizontal: enm(['MIN','CENTER','MAX','STRETCH','SCALE'], 'Horizontal constraint'),
240
+ vertical: enm(['MIN','CENTER','MAX','STRETCH','SCALE'], 'Vertical constraint'),
241
+ }, 'modify'),
242
+
243
+ delete_node: tool('delete_node', 'Remove a node from the canvas.', {
244
+ nodeId: req('string', 'Node ID to delete'),
245
+ }, 'modify'),
246
+
247
+ move_to_parent: tool('move_to_parent', 'Move a node into a different parent frame.', {
248
+ nodeId: req('string', 'Node to move'),
249
+ parentId: req('string', 'New parent ID'),
250
+ index: opt('number', 'Position index within parent'),
251
+ }, 'modify'),
252
+
253
+ duplicate_node: tool('duplicate_node', 'Duplicate a node.', {
254
+ nodeId: req('string', 'Node to duplicate'),
255
+ count: opt('number', 'Number of duplicates', 1),
256
+ offsetX: opt('number', 'X offset per copy'), offsetY: opt('number', 'Y offset per copy'),
257
+ }, 'modify'),
258
+
259
+ group_nodes: tool('group_nodes', 'Group multiple nodes together.', {
260
+ nodeIds: { type:'array', items:{type:'string'}, description:'Node IDs to group', _required:true },
261
+ name: opt('string', 'Group name'),
262
+ }, 'modify'),
263
+
264
+ ungroup_nodes: tool('ungroup_nodes', 'Ungroup a group node.', {
265
+ nodeId: req('string', 'Group node to ungroup'),
266
+ }, 'modify'),
267
+
268
+ resize_node: tool('resize_node', 'Resize a node with optional constraint preservation.', {
269
+ nodeId: req('string', 'Node to resize'),
270
+ width: opt('number', 'New width'), height: opt('number', 'New height'),
271
+ preserveAspect: opt('boolean', 'Maintain aspect ratio'),
272
+ }, 'modify'),
273
+
274
+ align_nodes: tool('align_nodes', 'Align multiple nodes relative to each other.', {
275
+ nodeIds: { type:'array', items:{type:'string'}, description:'Nodes to align', _required:true },
276
+ alignment: enm(['LEFT','CENTER','RIGHT','TOP','MIDDLE','BOTTOM','DISTRIBUTE_H','DISTRIBUTE_V'], 'Alignment type'),
277
+ }, 'modify'),
278
+
279
+ set_corner_radius: tool('set_corner_radius', 'Set corner radius with per-corner control.', {
280
+ nodeId: req('string', 'Target node'),
281
+ radius: opt('number', 'Uniform radius'),
282
+ topLeft: opt('number',''), topRight: opt('number',''), bottomRight: opt('number',''), bottomLeft: opt('number',''),
283
+ }, 'modify'),
284
+
285
+ set_opacity: tool('set_opacity', 'Set node opacity.', {
286
+ nodeId: req('string', 'Target node'), opacity: req('number', 'Opacity 0-1'),
287
+ }, 'modify'),
288
+
289
+ set_blend_mode: tool('set_blend_mode', 'Set blend mode.', {
290
+ nodeId: req('string', 'Target node'),
291
+ blendMode: enm(['NORMAL','MULTIPLY','SCREEN','OVERLAY','DARKEN','LIGHTEN','COLOR_DODGE','COLOR_BURN','HARD_LIGHT','SOFT_LIGHT','DIFFERENCE','EXCLUSION','HUE','SATURATION','COLOR','LUMINOSITY'], 'Blend mode'),
292
+ }, 'modify'),
293
+
294
+ set_clip_content: tool('set_clip_content', 'Toggle clip content on a frame.', {
295
+ nodeId: req('string', 'Frame node'), clip: req('boolean', 'Clip content'),
296
+ }, 'modify'),
297
+
298
+ rename_node: tool('rename_node', 'Rename a node.', {
299
+ nodeId: req('string', 'Target node'), name: req('string', 'New name'),
300
+ }, 'modify'),
301
+
302
+ lock_node: tool('lock_node', 'Lock/unlock a node.', {
303
+ nodeId: req('string', 'Target node'), locked: req('boolean', 'Lock state'),
304
+ }, 'modify'),
305
+
306
+ set_visibility: tool('set_visibility', 'Show/hide a node.', {
307
+ nodeId: req('string', 'Target node'), visible: req('boolean', 'Visibility'),
308
+ }, 'modify'),
309
+
310
+ reorder_node: tool('reorder_node', 'Change z-order of a node.', {
311
+ nodeId: req('string', 'Target node'),
312
+ direction: enm(['FRONT','BACK','FORWARD','BACKWARD'], 'Reorder direction'),
313
+ }, 'modify'),
314
+
315
+ set_layout_sizing: tool('set_layout_sizing', 'Set how a child behaves in auto-layout.', {
316
+ nodeId: req('string', 'Child node'),
317
+ horizontal: enm(['FIXED','HUG','FILL'], 'Horizontal sizing'),
318
+ vertical: enm(['FIXED','HUG','FILL'], 'Vertical sizing'),
319
+ }, 'modify'),
320
+
321
+ flatten_node: tool('flatten_node', 'Flatten a node into a single vector.', {
322
+ nodeId: req('string', 'Node to flatten'),
323
+ }, 'modify'),
324
+
325
+ set_rotation: tool('set_rotation', 'Rotate a node.', {
326
+ nodeId: req('string', 'Target node'), angle: req('number', 'Rotation in degrees'),
327
+ }, 'modify'),
328
+ }
329
+
330
+ // ═══ VECTOR & BOOLEAN (8) ═══
331
+ const VECTOR = {
332
+ create_vector: tool('create_vector', 'Draw custom vector paths using SVG-like path data.', {
333
+ name: opt('string', 'Vector name'),
334
+ pathData: req('string', 'SVG path data (M, L, C, Q, Z commands)'),
335
+ fill: opt('string', 'Fill color'), stroke: opt('string', 'Stroke color'),
336
+ strokeWeight: opt('number', 'Stroke weight'),
337
+ parentId: opt('string', 'Parent node ID'),
338
+ }, 'vector'),
339
+
340
+ boolean_operation: tool('boolean_operation', 'Perform boolean operations on shapes.', {
341
+ operation: enm(['UNION','SUBTRACT','INTERSECT','EXCLUDE'], 'Boolean operation type'),
342
+ nodeIds: { type:'array', items:{type:'string'}, description:'Node IDs (first is base shape)', _required:true },
343
+ }, 'vector'),
344
+
345
+ create_polygon: tool('create_polygon', 'Create a regular polygon.', {
346
+ sides: req('number', 'Number of sides (3=triangle, 5=pentagon, 6=hexagon, etc.)'),
347
+ size: req('number', 'Diameter'),
348
+ fill: opt('string', 'Fill color'), name: opt('string', 'Name'),
349
+ parentId: opt('string', 'Parent node ID'),
350
+ }, 'vector'),
351
+
352
+ create_star: tool('create_star', 'Create a star shape.', {
353
+ points: opt('number', 'Number of points', 5),
354
+ outerRadius: req('number', 'Outer radius'),
355
+ innerRadius: opt('number', 'Inner radius (ratio to outer)'),
356
+ fill: opt('string', 'Fill color'), name: opt('string', 'Name'),
357
+ parentId: opt('string', 'Parent node ID'),
358
+ }, 'vector'),
359
+
360
+ offset_path: tool('offset_path', 'Offset/expand a vector path.', {
361
+ nodeId: req('string', 'Vector node'), offset: req('number', 'Offset amount'),
362
+ }, 'vector'),
363
+
364
+ create_arrow: tool('create_arrow', 'Create an arrow shape.', {
365
+ length: req('number', 'Arrow length'), direction: enm(['RIGHT','LEFT','UP','DOWN'], 'Direction', 'RIGHT'),
366
+ strokeWeight: opt('number', 'Weight', 2), color: opt('string', 'Color'),
367
+ parentId: opt('string', 'Parent node ID'),
368
+ }, 'vector'),
369
+
370
+ create_icon: tool('create_icon', 'Create a common UI icon from built-in set.', {
371
+ icon: req('string', 'Icon name: arrow-right, arrow-left, arrow-up, arrow-down, check, x, plus, minus, search, menu, settings, user, heart, star, home, mail, phone, calendar, clock, bell, lock, unlock, eye, eye-off, edit, trash, download, upload, link, external-link, copy, share, filter, sort, grid, list, chevron-right, chevron-left, chevron-down, chevron-up'),
372
+ size: opt('number', 'Icon size', 24), color: opt('string', 'Color'),
373
+ parentId: opt('string', 'Parent node ID'),
374
+ }, 'vector'),
375
+
376
+ create_divider: tool('create_divider', 'Create a horizontal or vertical divider line.', {
377
+ direction: enm(['HORIZONTAL','VERTICAL'], 'Direction', 'HORIZONTAL'),
378
+ length: opt('number', 'Length (auto-fills parent if omitted)'),
379
+ color: opt('string', 'Color'), thickness: opt('number', 'Thickness', 1),
380
+ parentId: opt('string', 'Parent node ID'),
381
+ }, 'vector'),
382
+ }
383
+
384
+ // ═══ READ & INSPECT (18) ═══
385
+ const READ = {
386
+ get_selection: tool('get_selection', 'Get the currently selected nodes with full properties.', {}, 'read'),
387
+
388
+ get_page_structure: tool('get_page_structure', 'Get the full page layer tree with types, names, and hierarchy.', {
389
+ depth: opt('number', 'Max depth to traverse', 3),
390
+ pageId: opt('string', 'Page ID (uses current page if omitted)'),
391
+ }, 'read'),
392
+
393
+ get_node_info: tool('get_node_info', 'Get detailed properties of a specific node including fills, effects, auto-layout, and text styles.', {
394
+ nodeId: req('string', 'Node ID to inspect'),
395
+ }, 'read'),
396
+
397
+ get_nodes_info: tool('get_nodes_info', 'Get detailed properties of multiple nodes.', {
398
+ nodeIds: { type:'array', items:{type:'string'}, description:'Array of node IDs', _required:true },
399
+ }, 'read'),
400
+
401
+ find_nodes: tool('find_nodes', 'Search for nodes by name, type, or properties.', {
402
+ query: opt('string', 'Search by name (partial match)'),
403
+ type: enm(['FRAME','TEXT','RECTANGLE','ELLIPSE','LINE','COMPONENT','INSTANCE','GROUP','VECTOR','BOOLEAN_OPERATION','SECTION'], 'Filter by type'),
404
+ withinId: opt('string', 'Search within this node'),
405
+ }, 'read'),
406
+
407
+ get_local_styles: tool('get_local_styles', 'Get all local paint, text, and effect styles in the file.', {}, 'read'),
408
+
409
+ get_local_variables: tool('get_local_variables', 'Get all local variable collections and variables.', {}, 'read'),
410
+
411
+ list_components: tool('list_components', 'List all components in the file.', {
412
+ pageId: opt('string', 'Filter to specific page'),
413
+ }, 'read'),
414
+
415
+ list_pages: tool('list_pages', 'List all pages in the document.', {}, 'read'),
416
+
417
+ get_document_info: tool('get_document_info', 'Get document name, pages, and metadata.', {}, 'read'),
418
+
419
+ set_selection: tool('set_selection', 'Select nodes and optionally scroll viewport to them.', {
420
+ nodeIds: { type:'array', items:{type:'string'}, description:'Node IDs to select', _required:true },
421
+ zoomToFit: opt('boolean', 'Scroll and zoom to show selection', true),
422
+ }, 'read'),
423
+
424
+ set_focus: tool('set_focus', 'Focus viewport on a specific node.', {
425
+ nodeId: req('string', 'Node to focus on'),
426
+ zoom: opt('number', 'Zoom level 0.1-10'),
427
+ }, 'read'),
428
+
429
+ get_annotations: tool('get_annotations', 'Get all annotations/comments on the document or a specific node.', {
430
+ nodeId: opt('string', 'Node to get annotations for'),
431
+ }, 'read'),
432
+
433
+ set_annotation: tool('set_annotation', 'Create or update an annotation with markdown support.', {
434
+ nodeId: req('string', 'Node to annotate'),
435
+ text: req('string', 'Annotation text (markdown supported)'),
436
+ }, 'read'),
437
+
438
+ list_available_fonts: tool('list_available_fonts', 'Get all fonts available in the Figma file.', {}, 'read'),
439
+
440
+ read_node_css: tool('read_node_css', 'Get CSS representation of a node (for developer handoff).', {
441
+ nodeId: req('string', 'Node to get CSS for'),
442
+ format: enm(['css','tailwind','react-inline'], 'Output format', 'css'),
443
+ }, 'read'),
444
+
445
+ get_selection_colors: tool('get_selection_colors', 'Extract all colors used in the current selection.', {}, 'read'),
446
+
447
+ measure_distance: tool('measure_distance', 'Measure distance between two nodes.', {
448
+ nodeId1: req('string', 'First node'), nodeId2: req('string', 'Second node'),
449
+ }, 'read'),
450
+ }
451
+
452
+ // ═══ VARIABLES & TOKENS (10) ═══
453
+ const VARIABLES = {
454
+ create_variable_collection: tool('create_variable_collection', 'Create a design token collection with modes (e.g. Light/Dark).', {
455
+ name: req('string', 'Collection name (e.g. "Colors", "Spacing")'),
456
+ modes: opt('array', 'Mode names, e.g. ["Light", "Dark"]'),
457
+ }, 'variables'),
458
+
459
+ create_variable: tool('create_variable', 'Create a design variable/token.', {
460
+ name: req('string', 'Variable name (e.g. "primary-500", "spacing-md")'),
461
+ collectionId: req('string', 'Collection ID'),
462
+ type: enm(['COLOR','FLOAT','STRING','BOOLEAN'], 'Variable type'),
463
+ value: req('string', 'Value (hex for COLOR, number for FLOAT, etc.)'),
464
+ modeValues: opt('object', 'Values per mode: { "Light": "#000", "Dark": "#fff" }'),
465
+ }, 'variables'),
466
+
467
+ bind_variable: tool('bind_variable', 'Bind a variable to a node property.', {
468
+ nodeId: req('string', 'Target node'),
469
+ property: req('string', 'Property to bind: fills, strokes, cornerRadius, paddingTop, etc.'),
470
+ variableId: req('string', 'Variable ID to bind'),
471
+ }, 'variables'),
472
+
473
+ get_variables: tool('get_variables', 'List all variable collections and their variables.', {}, 'variables'),
474
+
475
+ update_variable: tool('update_variable', 'Update a variable value.', {
476
+ variableId: req('string', 'Variable ID'),
477
+ value: req('string', 'New value'),
478
+ mode: opt('string', 'Mode to update (updates default if omitted)'),
479
+ }, 'variables'),
480
+
481
+ delete_variable: tool('delete_variable', 'Delete a variable.', {
482
+ variableId: req('string', 'Variable to delete'),
483
+ }, 'variables'),
484
+
485
+ create_design_tokens: tool('create_design_tokens', 'Generate a complete design token system from a brand color. Creates color, spacing, radius, and typography collections.', {
486
+ brandColor: req('string', 'Primary brand color hex'),
487
+ name: opt('string', 'Token set name', 'Design System'),
488
+ withModes: opt('boolean', 'Create Light + Dark modes', true),
489
+ }, 'variables'),
490
+
491
+ import_tokens: tool('import_tokens', 'Import design tokens from JSON (W3C Design Token format).', {
492
+ json: req('string', 'JSON string of design tokens'),
493
+ collectionName: opt('string', 'Collection name'),
494
+ }, 'variables'),
495
+
496
+ export_tokens: tool('export_tokens', 'Export all design tokens as JSON, CSS custom properties, SCSS, or Tailwind config.', {
497
+ format: enm(['json','css','scss','tailwind'], 'Export format', 'json'),
498
+ }, 'variables'),
499
+
500
+ swap_mode: tool('swap_mode', 'Switch the active mode on a variable collection.', {
501
+ collectionId: req('string', 'Collection ID'),
502
+ mode: req('string', 'Mode name to activate'),
503
+ }, 'variables'),
336
504
  }
337
505
 
338
- export function getToolByName(name) {
339
- return TOOLS.find(t => t.name === name);
506
+ // ═══ EXPORT & CODE (10) ═══
507
+ const EXPORT = {
508
+ export_as_svg: tool('export_as_svg', 'Export a node as SVG markup.', {
509
+ nodeId: req('string', 'Node to export'),
510
+ }, 'export'),
511
+
512
+ export_as_png: tool('export_as_png', 'Export a node as PNG.', {
513
+ nodeId: req('string', 'Node to export'),
514
+ scale: opt('number', 'Scale factor', 2),
515
+ }, 'export'),
516
+
517
+ export_to_react: tool('export_to_react', 'Generate React + Tailwind code from a Figma node tree.', {
518
+ nodeId: req('string', 'Root node to convert'),
519
+ framework: enm(['react-tailwind','react-css','html-css','vue','svelte'], 'Output framework', 'react-tailwind'),
520
+ componentName: opt('string', 'Root component name'),
521
+ }, 'export'),
522
+
523
+ export_design_specs: tool('export_design_specs', 'Generate design specifications document for developer handoff.', {
524
+ nodeId: req('string', 'Node to document'),
525
+ format: enm(['markdown','json','html'], 'Spec format', 'markdown'),
526
+ }, 'export'),
527
+
528
+ export_assets: tool('export_assets', 'Batch export all exportable assets (icons, images) from a node tree.', {
529
+ nodeId: req('string', 'Root node'),
530
+ format: enm(['svg','png','pdf'], 'Export format', 'svg'),
531
+ scale: opt('number', 'Scale factor', 1),
532
+ }, 'export'),
533
+
534
+ screenshot: tool('screenshot', 'Take a screenshot of the current canvas view or a specific node.', {
535
+ nodeId: opt('string', 'Node to screenshot (uses viewport if omitted)'),
536
+ scale: opt('number', 'Scale factor', 2),
537
+ }, 'export'),
538
+
539
+ copy_css: tool('copy_css', 'Copy CSS properties of a node to clipboard.', {
540
+ nodeId: req('string', 'Node to get CSS for'),
541
+ }, 'export'),
542
+
543
+ generate_stylesheet: tool('generate_stylesheet', 'Generate a complete stylesheet from a design file.', {
544
+ pageId: opt('string', 'Page ID'),
545
+ format: enm(['css','scss','tailwind'], 'Output format', 'css'),
546
+ }, 'export'),
547
+
548
+ export_color_palette: tool('export_color_palette', 'Export all colors used as a palette.', {
549
+ format: enm(['json','css','scss','tailwind','figma-tokens'], 'Format', 'json'),
550
+ }, 'export'),
551
+
552
+ export_typography: tool('export_typography', 'Export all text styles as a typography system.', {
553
+ format: enm(['json','css','scss','tailwind'], 'Format', 'json'),
554
+ }, 'export'),
555
+
556
+ export_component_inventory: tool('export_component_inventory', 'Export a complete inventory of all components with usage counts.', {
557
+ format: enm(['json','markdown','csv'], 'Format', 'json'),
558
+ }, 'export'),
559
+
560
+ export_spacing_tokens: tool('export_spacing_tokens', 'Export all spacing values used as a spacing token system.', {
561
+ format: enm(['json','css','scss','tailwind'], 'Format', 'json'),
562
+ }, 'export'),
340
563
  }
341
564
 
342
- export function getAllToolNames() {
343
- return TOOLS.map(t => t.name);
565
+ // ═══ ACCESSIBILITY & LINT (12) ═══
566
+ const ACCESSIBILITY = {
567
+ audit_accessibility: tool('audit_accessibility', 'Run a full accessibility audit on a node tree. Checks contrast, touch targets, font sizes, focus indicators.', {
568
+ nodeId: req('string', 'Root node to audit'),
569
+ }, 'accessibility'),
570
+
571
+ check_contrast: tool('check_contrast', 'Check color contrast ratio between two colors.', {
572
+ foreground: req('string', 'Foreground color hex'),
573
+ background: req('string', 'Background color hex'),
574
+ }, 'accessibility'),
575
+
576
+ fix_touch_targets: tool('fix_touch_targets', 'Auto-fix all touch targets below 44px in a node tree.', {
577
+ nodeId: req('string', 'Root node to fix'),
578
+ }, 'accessibility'),
579
+
580
+ lint_design: tool('lint_design', 'Run design lint rules: spacing consistency, naming conventions, color usage, font sizes, alignment.', {
581
+ nodeId: req('string', 'Root node to lint'),
582
+ rules: opt('array', 'Specific rules to check. Omit for all.'),
583
+ }, 'accessibility'),
584
+
585
+ fix_spacing: tool('fix_spacing', 'Auto-fix all spacing values to nearest 8px grid value.', {
586
+ nodeId: req('string', 'Root node to fix'),
587
+ grid: opt('number', 'Grid size', 8),
588
+ }, 'accessibility'),
589
+
590
+ check_naming: tool('check_naming', 'Check layer naming conventions. Flags generic names like "Frame 123", "Group 5".', {
591
+ nodeId: req('string', 'Root node to check'),
592
+ }, 'accessibility'),
593
+
594
+ suggest_improvements: tool('suggest_improvements', 'AI-powered design improvement suggestions based on the design intelligence engine.', {
595
+ nodeId: req('string', 'Node to analyze'),
596
+ }, 'accessibility'),
597
+
598
+ validate_component: tool('validate_component', 'Validate a component follows design system rules.', {
599
+ nodeId: req('string', 'Component to validate'),
600
+ }, 'accessibility'),
601
+
602
+ check_consistency: tool('check_consistency', 'Check for inconsistent colors, fonts, spacing, and radii across a design.', {
603
+ nodeId: req('string', 'Root node to check'),
604
+ }, 'accessibility'),
605
+
606
+ generate_a11y_report: tool('generate_a11y_report', 'Generate a detailed accessibility compliance report.', {
607
+ nodeId: req('string', 'Root node'),
608
+ standard: enm(['WCAG-AA','WCAG-AAA'], 'Compliance standard', 'WCAG-AA'),
609
+ format: enm(['markdown','json','html'], 'Report format', 'markdown'),
610
+ }, 'accessibility'),
611
+
612
+ color_blindness_check: tool('color_blindness_check', 'Simulate color blindness on a design to check for issues.', {
613
+ nodeId: req('string', 'Node to check'),
614
+ type: enm(['protanopia','deuteranopia','tritanopia','achromatopsia'], 'Color blindness type'),
615
+ }, 'accessibility'),
616
+
617
+ responsive_check: tool('responsive_check', 'Check if a design handles different viewport widths correctly.', {
618
+ nodeId: req('string', 'Root frame to check'),
619
+ breakpoints: opt('array', 'Widths to check, e.g. [375, 768, 1024, 1440]'),
620
+ }, 'accessibility'),
344
621
  }
622
+
623
+ // ═══ BATCH OPERATIONS (12) ═══
624
+ const BATCH = {
625
+ batch_rename: tool('batch_rename', 'Rename multiple nodes using a pattern.', {
626
+ nodeIds: { type:'array', items:{type:'string'}, description:'Nodes to rename', _required:true },
627
+ pattern: req('string', 'Name pattern. Use {n} for number, {name} for current name. E.g. "Card {n}"'),
628
+ startNumber: opt('number', 'Starting number', 1),
629
+ }, 'batch'),
630
+
631
+ batch_style: tool('batch_style', 'Apply style changes to multiple nodes at once.', {
632
+ nodeIds: { type:'array', items:{type:'string'}, description:'Target nodes', _required:true },
633
+ changes: req('object', 'Style changes: { fill, opacity, cornerRadius, fontSize, fontWeight, ... }'),
634
+ }, 'batch'),
635
+
636
+ batch_replace_text: tool('batch_replace_text', 'Find and replace text across multiple text nodes.', {
637
+ find: req('string', 'Text to find'),
638
+ replace: req('string', 'Replacement text'),
639
+ nodeId: opt('string', 'Scope to search within (uses whole page if omitted)'),
640
+ matchCase: opt('boolean', 'Case sensitive', false),
641
+ }, 'batch'),
642
+
643
+ batch_replace_color: tool('batch_replace_color', 'Replace a color across all nodes.', {
644
+ find: req('string', 'Color to find (hex)'),
645
+ replace: req('string', 'Replacement color (hex)'),
646
+ nodeId: opt('string', 'Scope node'),
647
+ }, 'batch'),
648
+
649
+ batch_resize: tool('batch_resize', 'Resize multiple nodes.', {
650
+ nodeIds: { type:'array', items:{type:'string'}, description:'Target nodes', _required:true },
651
+ width: opt('number','New width'), height: opt('number','New height'),
652
+ scale: opt('number', 'Scale factor'),
653
+ }, 'batch'),
654
+
655
+ batch_align: tool('batch_align', 'Align multiple nodes.', {
656
+ nodeIds: { type:'array', items:{type:'string'}, description:'Nodes to align', _required:true },
657
+ horizontal: enm(['LEFT','CENTER','RIGHT','DISTRIBUTE'], 'H alignment'),
658
+ vertical: enm(['TOP','MIDDLE','BOTTOM','DISTRIBUTE'], 'V alignment'),
659
+ }, 'batch'),
660
+
661
+ batch_delete: tool('batch_delete', 'Delete multiple nodes.', {
662
+ nodeIds: { type:'array', items:{type:'string'}, description:'Nodes to delete', _required:true },
663
+ }, 'batch'),
664
+
665
+ batch_duplicate: tool('batch_duplicate', 'Duplicate multiple nodes.', {
666
+ nodeIds: { type:'array', items:{type:'string'}, description:'Nodes to duplicate', _required:true },
667
+ offsetX: opt('number', 'X offset per copy'), offsetY: opt('number', 'Y offset per copy'),
668
+ }, 'batch'),
669
+
670
+ batch_set_visibility: tool('batch_set_visibility', 'Show/hide multiple nodes.', {
671
+ nodeIds: { type:'array', items:{type:'string'}, description:'Target nodes', _required:true },
672
+ visible: req('boolean', 'Visibility state'),
673
+ }, 'batch'),
674
+
675
+ batch_lock: tool('batch_lock', 'Lock/unlock multiple nodes.', {
676
+ nodeIds: { type:'array', items:{type:'string'}, description:'Target nodes', _required:true },
677
+ locked: req('boolean', 'Lock state'),
678
+ }, 'batch'),
679
+
680
+ select_all_by_type: tool('select_all_by_type', 'Select all nodes of a specific type.', {
681
+ type: enm(['FRAME','TEXT','RECTANGLE','ELLIPSE','COMPONENT','INSTANCE','GROUP'], 'Node type'),
682
+ withinId: opt('string', 'Scope node'),
683
+ }, 'batch'),
684
+
685
+ clean_hidden_layers: tool('clean_hidden_layers', 'Remove all hidden layers from a node tree.', {
686
+ nodeId: req('string', 'Root node to clean'),
687
+ dryRun: opt('boolean', 'Preview changes without deleting', false),
688
+ }, 'batch'),
689
+
690
+ batch_set_font: tool('batch_set_font', 'Change font family on all text nodes in a scope.', {
691
+ fontFamily: req('string', 'New font family'),
692
+ nodeId: opt('string', 'Scope'),
693
+ }, 'batch'),
694
+
695
+ batch_round_values: tool('batch_round_values', 'Round all dimensions, positions, and spacing to whole pixels.', {
696
+ nodeId: req('string', 'Root node'),
697
+ }, 'batch'),
698
+
699
+ batch_remove_strokes: tool('batch_remove_strokes', 'Remove all strokes from nodes in a scope.', {
700
+ nodeId: req('string', 'Root node'),
701
+ }, 'batch'),
702
+
703
+ batch_remove_effects: tool('batch_remove_effects', 'Remove all effects from nodes in a scope.', {
704
+ nodeId: req('string', 'Root node'),
705
+ }, 'batch'),
706
+
707
+ batch_set_corner_radius: tool('batch_set_corner_radius', 'Set corner radius on all frames/rectangles in scope.', {
708
+ nodeId: req('string', 'Root node'),
709
+ radius: req('number', 'Corner radius'),
710
+ }, 'batch'),
711
+ }
712
+
713
+ // ═══ DESIGN SYSTEM (10) ═══
714
+ const DESIGN_SYSTEM = {
715
+ scan_design_system: tool('scan_design_system', 'Scan a file and extract the implied design system: colors, fonts, spacing, components.', {
716
+ pageId: opt('string', 'Page to scan'),
717
+ }, 'design-system'),
718
+
719
+ create_style_guide: tool('create_style_guide', 'Generate a visual style guide page from the current design system.', {
720
+ brandColor: opt('string', 'Brand color to use'),
721
+ mode: enm(['dark','light'], 'Mode', 'dark'),
722
+ }, 'design-system'),
723
+
724
+ detect_inconsistencies: tool('detect_inconsistencies', 'Find design inconsistencies: off-grid spacing, non-standard colors, mismatched fonts.', {
725
+ nodeId: opt('string', 'Scope node'),
726
+ }, 'design-system'),
727
+
728
+ normalize_design: tool('normalize_design', 'Auto-fix a design to match the implied design system. Snaps spacing, normalizes colors, fixes font weights.', {
729
+ nodeId: req('string', 'Root node to normalize'),
730
+ dryRun: opt('boolean', 'Preview changes', false),
731
+ }, 'design-system'),
732
+
733
+ extract_components: tool('extract_components', 'Find repeated patterns and suggest component extraction.', {
734
+ nodeId: req('string', 'Root node to analyze'),
735
+ }, 'design-system'),
736
+
737
+ get_design_craft_guide: tool('get_design_craft_guide', 'Get the professional design rules: typography, color, spacing, anti-AI-slop patterns. Use this before creating any design.', {}, 'design-system'),
738
+
739
+ suggest_color_palette: tool('suggest_color_palette', 'Generate a color palette from a single brand color.', {
740
+ brandColor: req('string', 'Brand color hex'),
741
+ mode: enm(['dark','light','both'], 'Mode', 'both'),
742
+ }, 'design-system'),
743
+
744
+ suggest_type_scale: tool('suggest_type_scale', 'Generate a typography scale.', {
745
+ baseSize: opt('number', 'Base font size', 16),
746
+ ratio: enm(['minor2','major2','minor3','major3','perfect4','aug4','perfect5','golden'], 'Scale ratio', 'major2'),
747
+ }, 'design-system'),
748
+
749
+ import_design_system: tool('import_design_system', 'Import a design system from JSON config and create all tokens, styles, and components.', {
750
+ config: req('string', 'JSON config string with colors, fonts, spacing, components'),
751
+ }, 'design-system'),
752
+
753
+ compare_to_system: tool('compare_to_system', 'Compare a design to the established design system and flag deviations.', {
754
+ nodeId: req('string', 'Node to compare'),
755
+ }, 'design-system'),
756
+ }
757
+
758
+ // ═══ RESPONSIVE (5) ═══
759
+ const RESPONSIVE = {
760
+ create_responsive_variant: tool('create_responsive_variant', 'Create mobile/tablet/desktop variants of a frame.', {
761
+ nodeId: req('string', 'Source frame'),
762
+ breakpoints: opt('array', 'Target widths', [375, 768, 1440]),
763
+ }, 'responsive'),
764
+
765
+ set_breakpoint: tool('set_breakpoint', 'Resize a frame to a standard breakpoint.', {
766
+ nodeId: req('string', 'Frame to resize'),
767
+ breakpoint: enm(['mobile','tablet','desktop','wide'], 'Breakpoint'),
768
+ }, 'responsive'),
769
+
770
+ convert_to_responsive: tool('convert_to_responsive', 'Convert fixed-width designs to responsive auto-layout.', {
771
+ nodeId: req('string', 'Root frame to convert'),
772
+ }, 'responsive'),
773
+
774
+ generate_mobile: tool('generate_mobile', 'Generate a mobile-optimized version of a desktop design.', {
775
+ nodeId: req('string', 'Desktop frame'),
776
+ width: opt('number', 'Mobile width', 375),
777
+ }, 'responsive'),
778
+
779
+ stack_for_mobile: tool('stack_for_mobile', 'Convert horizontal layouts to vertical stacking for mobile.', {
780
+ nodeId: req('string', 'Frame with horizontal layout'),
781
+ }, 'responsive'),
782
+ }
783
+
784
+ // ═══ TYPOGRAPHY (10) ═══
785
+ const TYPOGRAPHY = {
786
+ type_scale_apply: tool('type_scale_apply', 'Apply a type scale to all text in a frame. Maps headings, body, and caption sizes to the scale.', {
787
+ nodeId: req('string', 'Root frame'),
788
+ ratio: enm(['minor2','major2','minor3','major3','perfect4','aug4','perfect5','golden'], 'Scale ratio', 'major2'),
789
+ baseSize: opt('number', 'Base font size', 16),
790
+ }, 'typography'),
791
+
792
+ type_audit: tool('type_audit', 'Find every unique text style in a page. Flag off-scale sizes, inconsistent weights, and orphaned styles.', {
793
+ nodeId: opt('string', 'Scope node (page if omitted)'),
794
+ }, 'typography'),
795
+
796
+ type_set_hierarchy: tool('type_set_hierarchy', 'Set heading levels with proper size, weight, and line-height ratios.', {
797
+ nodeId: req('string', 'Root frame'),
798
+ levels: opt('number', 'Number of heading levels', 6),
799
+ }, 'typography'),
800
+
801
+ type_check_measure: tool('type_check_measure', 'Check line length (45-75 chars optimal), line-height, and letter-spacing for readability.', {
802
+ nodeId: req('string', 'Text node or frame to check'),
803
+ }, 'typography'),
804
+
805
+ type_normalize: tool('type_normalize', 'Normalize all text to the nearest type scale value. Fix off-scale sizes.', {
806
+ nodeId: req('string', 'Root frame'),
807
+ ratio: enm(['minor2','major2','minor3','major3','perfect4'], 'Scale ratio', 'major2'),
808
+ }, 'typography'),
809
+
810
+ type_list_styles: tool('type_list_styles', 'List all text styles with usage count.', {
811
+ nodeId: opt('string', 'Scope'),
812
+ }, 'typography'),
813
+
814
+ type_pair_suggest: tool('type_pair_suggest', 'Suggest font pairings based on currently loaded fonts.', {
815
+ primaryFont: opt('string', 'Primary font family'),
816
+ }, 'typography'),
817
+
818
+ type_replace_font: tool('type_replace_font', 'Replace one font family with another across all text nodes.', {
819
+ find: req('string', 'Font to replace'),
820
+ replace: req('string', 'Replacement font'),
821
+ nodeId: opt('string', 'Scope'),
822
+ }, 'typography'),
823
+
824
+ set_text_content: tool('set_text_content', 'Update text content of a text node without changing styles.', {
825
+ nodeId: req('string', 'Text node ID'),
826
+ text: req('string', 'New text content'),
827
+ }, 'typography'),
828
+
829
+ type_create_style: tool('type_create_style', 'Create a local text style from a text node.', {
830
+ nodeId: req('string', 'Text node to create style from'),
831
+ name: req('string', 'Style name'),
832
+ }, 'typography'),
833
+ }
834
+
835
+ // ═══ COLOR (10) ═══
836
+ const COLOR = {
837
+ color_palette_generate: tool('color_palette_generate', 'Generate a full color palette (50-950 shades) from a base color.', {
838
+ baseColor: req('string', 'Base color hex'),
839
+ steps: opt('number', 'Number of shade steps', 10),
840
+ }, 'color'),
841
+
842
+ color_extract: tool('color_extract', 'Extract all unique colors from a frame and organize by usage frequency.', {
843
+ nodeId: req('string', 'Root node'),
844
+ }, 'color'),
845
+
846
+ color_harmonize: tool('color_harmonize', 'Generate harmonious colors: complementary, triadic, analogous, split-complementary.', {
847
+ baseColor: req('string', 'Base color hex'),
848
+ scheme: enm(['complementary','triadic','analogous','split-complementary','tetradic','monochromatic'], 'Color scheme'),
849
+ }, 'color'),
850
+
851
+ color_darkmode: tool('color_darkmode', 'Generate a dark mode variant of a frame, mapping all colors intelligently.', {
852
+ nodeId: req('string', 'Frame to convert'),
853
+ brandColor: opt('string', 'Brand color to preserve'),
854
+ }, 'color'),
855
+
856
+ color_lightmode: tool('color_lightmode', 'Generate a light mode variant of a dark frame.', {
857
+ nodeId: req('string', 'Frame to convert'),
858
+ brandColor: opt('string', 'Brand color to preserve'),
859
+ }, 'color'),
860
+
861
+ color_check_all: tool('color_check_all', 'Check WCAG contrast for every text/background pair in a frame.', {
862
+ nodeId: req('string', 'Root frame'),
863
+ standard: enm(['AA','AAA'], 'WCAG standard', 'AA'),
864
+ }, 'color'),
865
+
866
+ color_create_style: tool('color_create_style', 'Create a local color style.', {
867
+ name: req('string', 'Style name (e.g. "Primary/500")'),
868
+ color: req('string', 'Color hex'),
869
+ }, 'color'),
870
+
871
+ color_apply_style: tool('color_apply_style', 'Apply a color style to a node.', {
872
+ nodeId: req('string', 'Target node'),
873
+ styleName: req('string', 'Style name to apply'),
874
+ property: enm(['fill','stroke'], 'Property to apply to', 'fill'),
875
+ }, 'color'),
876
+
877
+ color_replace_global: tool('color_replace_global', 'Replace a color across the entire document (all pages).', {
878
+ find: req('string', 'Color to find (hex)'),
879
+ replace: req('string', 'Replacement color (hex)'),
880
+ }, 'color'),
881
+
882
+ color_generate_semantic: tool('color_generate_semantic', 'Generate a full semantic color system (bg, surface, border, text, brand, status) from one brand color.', {
883
+ brandColor: req('string', 'Brand color hex'),
884
+ mode: enm(['dark','light','both'], 'Color mode', 'both'),
885
+ }, 'color'),
886
+ }
887
+
888
+ // ═══ PROTOTYPE & INTERACTION (10) ═══
889
+ const PROTOTYPE = {
890
+ create_prototype_link: tool('create_prototype_link', 'Create a prototype navigation link between two frames.', {
891
+ fromNodeId: req('string', 'Source node (trigger)'),
892
+ toNodeId: req('string', 'Destination frame'),
893
+ trigger: enm(['ON_CLICK','ON_HOVER','ON_PRESS','ON_DRAG','AFTER_TIMEOUT','MOUSE_ENTER','MOUSE_LEAVE','MOUSE_DOWN','MOUSE_UP'], 'Interaction trigger', 'ON_CLICK'),
894
+ transition: enm(['INSTANT','DISSOLVE','SLIDE_IN','SLIDE_OUT','PUSH','MOVE_IN','MOVE_OUT','SMART_ANIMATE'], 'Transition type', 'DISSOLVE'),
895
+ duration: opt('number', 'Transition duration ms', 300),
896
+ }, 'prototype'),
897
+
898
+ create_scroll_behavior: tool('create_scroll_behavior', 'Set scroll behavior on a frame.', {
899
+ nodeId: req('string', 'Frame node'),
900
+ direction: enm(['HORIZONTAL','VERTICAL','BOTH','NONE'], 'Scroll direction'),
901
+ overflow: enm(['VISIBLE','HIDDEN','SCROLL'], 'Overflow behavior', 'SCROLL'),
902
+ }, 'prototype'),
903
+
904
+ set_overflow: tool('set_overflow', 'Set overflow clipping on a frame.', {
905
+ nodeId: req('string', 'Frame node'),
906
+ clip: req('boolean', 'Clip content'),
907
+ }, 'prototype'),
908
+
909
+ create_overlay: tool('create_overlay', 'Set up a frame as a modal/overlay in prototype mode.', {
910
+ nodeId: req('string', 'Overlay frame'),
911
+ position: enm(['CENTER','TOP_LEFT','TOP_CENTER','TOP_RIGHT','BOTTOM_LEFT','BOTTOM_CENTER','BOTTOM_RIGHT','MANUAL'], 'Overlay position', 'CENTER'),
912
+ closeOnClickOutside: opt('boolean', 'Close when clicking outside', true),
913
+ backgroundDim: opt('number', 'Background dim opacity 0-1', 0.5),
914
+ }, 'prototype'),
915
+
916
+ set_fixed_position: tool('set_fixed_position', 'Pin a layer so it stays fixed during scroll (sticky nav, floating button).', {
917
+ nodeId: req('string', 'Node to pin'),
918
+ position: enm(['TOP','BOTTOM','LEFT','RIGHT'], 'Fixed position'),
919
+ }, 'prototype'),
920
+
921
+ create_hover_state: tool('create_hover_state', 'Create a hover variant interaction on a component.', {
922
+ nodeId: req('string', 'Component or instance'),
923
+ hoverVariant: req('string', 'Variant name for hover state'),
924
+ }, 'prototype'),
925
+
926
+ create_flow: tool('create_flow', 'Create a prototype flow starting point.', {
927
+ nodeId: req('string', 'Starting frame'),
928
+ name: req('string', 'Flow name'),
929
+ }, 'prototype'),
930
+
931
+ list_flows: tool('list_flows', 'List all prototype flows in the file.', {}, 'prototype'),
932
+
933
+ remove_prototype_link: tool('remove_prototype_link', 'Remove a prototype connection.', {
934
+ nodeId: req('string', 'Node to remove connection from'),
935
+ }, 'prototype'),
936
+
937
+ set_transition: tool('set_transition', 'Set the default transition for all prototype links on a frame.', {
938
+ nodeId: req('string', 'Frame node'),
939
+ transition: enm(['INSTANT','DISSOLVE','SLIDE_IN','SMART_ANIMATE'], 'Transition'),
940
+ duration: opt('number', 'Duration ms', 300),
941
+ easing: enm(['LINEAR','EASE_IN','EASE_OUT','EASE_IN_OUT','EASE_IN_BACK','EASE_OUT_BACK','CUSTOM_SPRING'], 'Easing', 'EASE_OUT'),
942
+ }, 'prototype'),
943
+ }
944
+
945
+ // ═══ PAGE MANAGEMENT (8) ═══
946
+ const PAGE = {
947
+ create_new_page: tool('create_new_page', 'Create a new page in the document.', {
948
+ name: req('string', 'Page name'),
949
+ }, 'page'),
950
+
951
+ switch_page: tool('switch_page', 'Switch to a different page.', {
952
+ pageId: req('string', 'Page ID or name'),
953
+ }, 'page'),
954
+
955
+ duplicate_page: tool('duplicate_page', 'Duplicate an entire page.', {
956
+ pageId: req('string', 'Page to duplicate'),
957
+ name: opt('string', 'New page name'),
958
+ }, 'page'),
959
+
960
+ delete_page: tool('delete_page', 'Delete a page.', {
961
+ pageId: req('string', 'Page to delete'),
962
+ }, 'page'),
963
+
964
+ rename_page: tool('rename_page', 'Rename a page.', {
965
+ pageId: req('string', 'Page ID'),
966
+ name: req('string', 'New name'),
967
+ }, 'page'),
968
+
969
+ sort_pages: tool('sort_pages', 'Sort pages alphabetically or by custom order.', {
970
+ order: enm(['ALPHABETICAL','REVERSE','CUSTOM'], 'Sort order'),
971
+ customOrder: opt('array', 'Array of page IDs in desired order'),
972
+ }, 'page'),
973
+
974
+ merge_pages: tool('merge_pages', 'Move all content from one page into another.', {
975
+ sourcePageId: req('string', 'Page to merge from'),
976
+ targetPageId: req('string', 'Page to merge into'),
977
+ }, 'page'),
978
+
979
+ page_overview: tool('page_overview', 'Get an overview of all pages: name, frame count, component count.', {}, 'page'),
980
+ }
981
+
982
+ // ═══ LIBRARY & COMPONENTS EXTENDED (8) ═══
983
+ const LIBRARY = {
984
+ search_library: tool('search_library', 'Search for components across local file and team libraries.', {
985
+ query: req('string', 'Search query'),
986
+ scope: enm(['LOCAL','TEAM','ALL'], 'Search scope', 'ALL'),
987
+ }, 'library'),
988
+
989
+ list_team_libraries: tool('list_team_libraries', 'List all available team libraries.', {}, 'library'),
990
+
991
+ swap_component: tool('swap_component', 'Swap one component instance for another.', {
992
+ instanceId: req('string', 'Instance to swap'),
993
+ newComponentId: req('string', 'New component ID'),
994
+ preserveOverrides: opt('boolean', 'Keep existing overrides', true),
995
+ }, 'library'),
996
+
997
+ detach_instance: tool('detach_instance', 'Detach a component instance to a regular frame.', {
998
+ nodeId: req('string', 'Instance to detach'),
999
+ }, 'library'),
1000
+
1001
+ reset_overrides: tool('reset_overrides', 'Reset all overrides on a component instance.', {
1002
+ nodeId: req('string', 'Instance node'),
1003
+ }, 'library'),
1004
+
1005
+ component_audit: tool('component_audit', 'Audit components: find detached instances, missing components, unused variants.', {
1006
+ nodeId: opt('string', 'Scope'),
1007
+ }, 'library'),
1008
+
1009
+ batch_swap_component: tool('batch_swap_component', 'Swap all instances of one component for another across the file.', {
1010
+ oldComponentId: req('string', 'Component to replace'),
1011
+ newComponentId: req('string', 'Replacement component'),
1012
+ }, 'library'),
1013
+
1014
+ publish_components: tool('publish_components', 'Mark components as ready to publish to team library.', {
1015
+ componentIds: { type:'array', items:{type:'string'}, description:'Components to mark', _required:true },
1016
+ }, 'library'),
1017
+ }
1018
+
1019
+ // ═══ ANNOTATION & HANDOFF (10) ═══
1020
+ const ANNOTATION = {
1021
+ annotate_spacing: tool('annotate_spacing', 'Add visual spacing annotations (redlines) to a frame.', {
1022
+ nodeId: req('string', 'Frame to annotate'),
1023
+ showPadding: opt('boolean', 'Show padding values', true),
1024
+ showGap: opt('boolean', 'Show gap values', true),
1025
+ showMargin: opt('boolean', 'Show margin values', true),
1026
+ }, 'annotation'),
1027
+
1028
+ annotate_colors: tool('annotate_colors', 'Add color swatch annotations to a frame.', {
1029
+ nodeId: req('string', 'Frame to annotate'),
1030
+ }, 'annotation'),
1031
+
1032
+ annotate_typography: tool('annotate_typography', 'Add typography annotations (font, size, weight, line-height) to text nodes.', {
1033
+ nodeId: req('string', 'Frame to annotate'),
1034
+ }, 'annotation'),
1035
+
1036
+ create_measurement: tool('create_measurement', 'Create a measurement line between two nodes showing distance.', {
1037
+ nodeId1: req('string', 'First node'),
1038
+ nodeId2: req('string', 'Second node'),
1039
+ direction: enm(['HORIZONTAL','VERTICAL','AUTO'], 'Measurement direction', 'AUTO'),
1040
+ }, 'annotation'),
1041
+
1042
+ create_spec_sheet: tool('create_spec_sheet', 'Generate a design specification sheet next to a frame with all measurements, colors, and typography.', {
1043
+ nodeId: req('string', 'Frame to spec'),
1044
+ position: enm(['RIGHT','BELOW'], 'Where to place spec', 'RIGHT'),
1045
+ }, 'annotation'),
1046
+
1047
+ annotate_grid: tool('annotate_grid', 'Visualize the underlying grid and spacing system of a frame.', {
1048
+ nodeId: req('string', 'Frame to annotate'),
1049
+ gridSize: opt('number', 'Grid size to overlay', 8),
1050
+ }, 'annotation'),
1051
+
1052
+ annotate_hierarchy: tool('annotate_hierarchy', 'Annotate the visual hierarchy: heading levels, reading order, focal points.', {
1053
+ nodeId: req('string', 'Frame to annotate'),
1054
+ }, 'annotation'),
1055
+
1056
+ create_component_docs: tool('create_component_docs', 'Generate documentation frames for a component showing all variants, props, and usage.', {
1057
+ componentId: req('string', 'Component to document'),
1058
+ }, 'annotation'),
1059
+
1060
+ annotate_responsive: tool('annotate_responsive', 'Annotate breakpoint behavior and responsive rules on a frame.', {
1061
+ nodeId: req('string', 'Frame to annotate'),
1062
+ breakpoints: opt('array', 'Breakpoints to annotate'),
1063
+ }, 'annotation'),
1064
+
1065
+ create_changelog: tool('create_changelog', 'Compare two frames and generate a visual changelog showing what changed.', {
1066
+ beforeNodeId: req('string', 'Before frame'),
1067
+ afterNodeId: req('string', 'After frame'),
1068
+ }, 'annotation'),
1069
+ }
1070
+
1071
+ // ═══ EFFECTS & STYLES EXTENDED (8) ═══
1072
+ const EFFECTS = {
1073
+ create_glassmorphism: tool('create_glassmorphism', 'Apply glassmorphism effect: background blur, semi-transparent fill, subtle border.', {
1074
+ nodeId: req('string', 'Target frame'),
1075
+ blur: opt('number', 'Blur amount', 16),
1076
+ opacity: opt('number', 'Background opacity', 0.1),
1077
+ }, 'effects'),
1078
+
1079
+ create_neumorphism: tool('create_neumorphism', 'Apply neumorphism effect: dual shadows (light + dark) with matching background.', {
1080
+ nodeId: req('string', 'Target node'),
1081
+ intensity: opt('number', 'Effect intensity 0-1', 0.5),
1082
+ }, 'effects'),
1083
+
1084
+ create_noise_texture: tool('create_noise_texture', 'Add a subtle noise/grain texture overlay to a frame.', {
1085
+ nodeId: req('string', 'Target frame'),
1086
+ opacity: opt('number', 'Noise opacity', 0.05),
1087
+ scale: opt('number', 'Noise scale', 1),
1088
+ }, 'effects'),
1089
+
1090
+ set_gradient_fill: tool('set_gradient_fill', 'Set a gradient fill with a simple angle + 2 colors API.', {
1091
+ nodeId: req('string', 'Target node'),
1092
+ startColor: req('string', 'Start color hex'),
1093
+ endColor: req('string', 'End color hex'),
1094
+ angle: opt('number', 'Gradient angle in degrees', 180),
1095
+ }, 'effects'),
1096
+
1097
+ create_shadow_system: tool('create_shadow_system', 'Generate a consistent shadow elevation system (sm, md, lg, xl) as effect styles.', {
1098
+ baseColor: opt('string', 'Shadow base color', '#000000'),
1099
+ scale: enm(['subtle','medium','dramatic'], 'Shadow intensity', 'medium'),
1100
+ }, 'effects'),
1101
+
1102
+ apply_backdrop_blur: tool('apply_backdrop_blur', 'Apply background blur (frosted glass effect) to a frame.', {
1103
+ nodeId: req('string', 'Target frame'),
1104
+ amount: opt('number', 'Blur amount', 16),
1105
+ }, 'effects'),
1106
+
1107
+ create_border_gradient: tool('create_border_gradient', 'Create a gradient border effect using a slightly larger frame behind.', {
1108
+ nodeId: req('string', 'Target node'),
1109
+ startColor: req('string', 'Start color hex'),
1110
+ endColor: req('string', 'End color hex'),
1111
+ width: opt('number', 'Border width', 1),
1112
+ }, 'effects'),
1113
+
1114
+ remove_all_effects: tool('remove_all_effects', 'Remove all effects (shadows, blurs) from a node.', {
1115
+ nodeId: req('string', 'Target node'),
1116
+ }, 'effects'),
1117
+ }
1118
+
1119
+ // ═══ ASSEMBLE ALL TOOLS ═══
1120
+ export const ALL_TOOLS = {
1121
+ ...CREATE, ...MODIFY, ...VECTOR, ...READ,
1122
+ ...VARIABLES, ...EXPORT, ...ACCESSIBILITY,
1123
+ ...BATCH, ...DESIGN_SYSTEM, ...RESPONSIVE,
1124
+ ...TYPOGRAPHY, ...COLOR, ...PROTOTYPE,
1125
+ ...PAGE, ...LIBRARY, ...ANNOTATION, ...EFFECTS,
1126
+ }
1127
+
1128
+ export const TOOL_LIST = Object.values(ALL_TOOLS)
1129
+ export const TOOL_COUNT = Object.keys(ALL_TOOLS).length
1130
+
1131
+ export const CATEGORIES = {
1132
+ create: Object.keys(CREATE),
1133
+ modify: Object.keys(MODIFY),
1134
+ vector: Object.keys(VECTOR),
1135
+ read: Object.keys(READ),
1136
+ variables: Object.keys(VARIABLES),
1137
+ export: Object.keys(EXPORT),
1138
+ accessibility: Object.keys(ACCESSIBILITY),
1139
+ batch: Object.keys(BATCH),
1140
+ 'design-system': Object.keys(DESIGN_SYSTEM),
1141
+ responsive: Object.keys(RESPONSIVE),
1142
+ typography: Object.keys(TYPOGRAPHY),
1143
+ color: Object.keys(COLOR),
1144
+ prototype: Object.keys(PROTOTYPE),
1145
+ page: Object.keys(PAGE),
1146
+ library: Object.keys(LIBRARY),
1147
+ annotation: Object.keys(ANNOTATION),
1148
+ effects: Object.keys(EFFECTS),
1149
+ }
1150
+
1151
+ export function getTool(name) { return ALL_TOOLS[name] || null }
1152
+ export function getToolsByCategory(cat) { return CATEGORIES[cat] || [] }