figma-local 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "figma-local",
3
+ "version": "1.0.0",
4
+ "description": "Figma CLI — control Figma Desktop with Claude Code. Smart read, write, and AI-prompt export. No API key required.",
5
+ "author": "elvke",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "fig": "src/index.js",
10
+ "figma-cli": "src/index.js",
11
+ "fig-start": "bin/fig-start"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "bin"
16
+ ],
17
+ "keywords": [
18
+ "figma",
19
+ "design-system",
20
+ "design-tokens",
21
+ "cli",
22
+ "variables",
23
+ "claude-code",
24
+ "ai"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/thepreakerebi/figma-cli.git"
29
+ },
30
+ "homepage": "https://github.com/thepreakerebi/figma-cli",
31
+ "bugs": {
32
+ "url": "https://github.com/thepreakerebi/figma-cli/issues"
33
+ },
34
+ "scripts": {
35
+ "setup-alias": "bash bin/setup-alias.sh",
36
+ "test": "node --test tests/*.test.js"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "dependencies": {
42
+ "chalk": "^5.3.0",
43
+ "commander": "^12.0.0",
44
+ "ora": "^8.0.0",
45
+ "ws": "^8.16.0"
46
+ }
47
+ }
@@ -0,0 +1,379 @@
1
+ // Dashboard-01: Analytics dashboard with sidebar, stats, chart and data table
2
+ // Combines JSX template (sidebar with icons) + JS code (table, chart)
3
+ // Uses shadcn variable bindings (var:) for all colors - supports Light/Dark mode
4
+
5
+ // JSX template for the sidebar - uses Icon components for real Lucide icons
6
+ export const sidebarJsx = `
7
+ <Frame name="Dashboard" w={1440} h={820} flex="row" bg="var:background">
8
+ <Frame name="Sidebar" w={260} h="fill" flex="col" bg="var:muted" stroke="var:border" strokeWidth={1} strokeAlign="inside">
9
+ <Frame name="Logo" flex="row" gap={10} items="center" p={20} pb={8} w="fill">
10
+ <Frame w={28} h={28} bg="var:foreground" rounded={6} />
11
+ <Text size={15} weight="semibold" color="var:foreground">Acme Inc</Text>
12
+ </Frame>
13
+ <Frame name="Main Nav" flex="col" gap={2} px={12} py={8} w="fill">
14
+ <Frame name="Nav-Dashboard" flex="row" gap={10} items="center" px={12} py={8} rounded={8} bg="var:accent" w="fill">
15
+ <Icon name="lucide:layout-dashboard" size={18} color="var:foreground" />
16
+ <Text size={14} weight="medium" color="var:foreground" w="fill">Dashboard</Text>
17
+ </Frame>
18
+ <Frame name="Nav-Lifecycle" flex="row" gap={10} items="center" px={12} py={8} rounded={8} w="fill">
19
+ <Icon name="lucide:refresh-cw" size={18} color="var:muted-foreground" />
20
+ <Text size={14} color="var:muted-foreground" w="fill">Lifecycle</Text>
21
+ </Frame>
22
+ <Frame name="Nav-Analytics" flex="row" gap={10} items="center" px={12} py={8} rounded={8} w="fill">
23
+ <Icon name="lucide:bar-chart-3" size={18} color="var:muted-foreground" />
24
+ <Text size={14} color="var:muted-foreground" w="fill">Analytics</Text>
25
+ </Frame>
26
+ <Frame name="Nav-Projects" flex="row" gap={10} items="center" px={12} py={8} rounded={8} w="fill">
27
+ <Icon name="lucide:folder" size={18} color="var:muted-foreground" />
28
+ <Text size={14} color="var:muted-foreground" w="fill">Projects</Text>
29
+ </Frame>
30
+ <Frame name="Nav-Team" flex="row" gap={10} items="center" px={12} py={8} rounded={8} w="fill">
31
+ <Icon name="lucide:users" size={18} color="var:muted-foreground" />
32
+ <Text size={14} color="var:muted-foreground" w="fill">Team</Text>
33
+ </Frame>
34
+ </Frame>
35
+ <Frame name="Section Label" px={24} py={8} w="fill">
36
+ <Text size={12} color="var:muted-foreground">Documents</Text>
37
+ </Frame>
38
+ <Frame name="Doc Nav" flex="col" gap={2} px={12} w="fill">
39
+ <Frame flex="row" gap={10} items="center" px={12} py={6} w="fill">
40
+ <Icon name="lucide:file-text" size={16} color="var:muted-foreground" />
41
+ <Text size={13} color="var:muted-foreground" w="fill">Data Library</Text>
42
+ </Frame>
43
+ <Frame flex="row" gap={10} items="center" px={12} py={6} w="fill">
44
+ <Icon name="lucide:file-bar-chart" size={16} color="var:muted-foreground" />
45
+ <Text size={13} color="var:muted-foreground" w="fill">Reports</Text>
46
+ </Frame>
47
+ <Frame flex="row" gap={10} items="center" px={12} py={6} w="fill">
48
+ <Icon name="lucide:file-check" size={16} color="var:muted-foreground" />
49
+ <Text size={13} color="var:muted-foreground" w="fill">Word Assistant</Text>
50
+ </Frame>
51
+ </Frame>
52
+ <Frame name="User" flex="row" gap={10} items="center" px={20} py={16} w="fill" stroke="var:border" strokeWidth={1} strokeAlign="inside">
53
+ <Frame w={32} h={32} bg="var:border" rounded={16} />
54
+ <Frame flex="col" gap={2} grow={1}>
55
+ <Text size={13} weight="medium" color="var:foreground">Sofia Davis</Text>
56
+ <Text size={11} color="var:muted-foreground">sofia@acme.com</Text>
57
+ </Frame>
58
+ <Icon name="lucide:chevrons-up-down" size={16} color="var:muted-foreground" />
59
+ </Frame>
60
+ </Frame>
61
+ </Frame>
62
+ `.trim();
63
+
64
+ // JS code for main content (header, stats, chart, table)
65
+ // Uses variable binding for all colors
66
+ export function getMainContentCode(dashboardNodeId) {
67
+ return `(async function() {
68
+ var dashboard = await figma.getNodeByIdAsync('${dashboardNodeId}');
69
+ if (!dashboard) return 'Dashboard not found';
70
+
71
+ await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
72
+ await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
73
+ await figma.loadFontAsync({ family: 'Inter', style: 'Semi Bold' });
74
+ await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
75
+
76
+ // Load shadcn variables for binding
77
+ var collections = await figma.variables.getLocalVariableCollectionsAsync();
78
+ var shadcnCollections = collections.filter(function(c) { return c.name.startsWith('shadcn'); });
79
+ var varCache = {};
80
+
81
+ for (var ci = 0; ci < shadcnCollections.length; ci++) {
82
+ var col = shadcnCollections[ci];
83
+ for (var vi = 0; vi < col.variableIds.length; vi++) {
84
+ var v = await figma.variables.getVariableByIdAsync(col.variableIds[vi]);
85
+ if (v) varCache[v.name] = v;
86
+ }
87
+ }
88
+
89
+ var hasVars = Object.keys(varCache).length > 0;
90
+
91
+ function hexToRgb(hex) {
92
+ hex = hex.replace('#', '');
93
+ return {
94
+ r: parseInt(hex.substring(0, 2), 16) / 255,
95
+ g: parseInt(hex.substring(2, 4), 16) / 255,
96
+ b: parseInt(hex.substring(4, 6), 16) / 255
97
+ };
98
+ }
99
+
100
+ // Bind a fill to a variable, with hex fallback
101
+ function bindFill(node, varName, fallbackHex) {
102
+ if (hasVars && varCache[varName]) {
103
+ var paint = { type: 'SOLID', color: hexToRgb(fallbackHex) };
104
+ var bound = figma.variables.setBoundVariableForPaint(paint, 'color', varCache[varName]);
105
+ node.fills = [bound];
106
+ } else {
107
+ node.fills = [{ type: 'SOLID', color: hexToRgb(fallbackHex) }];
108
+ }
109
+ }
110
+
111
+ function bindStroke(node, varName, fallbackHex) {
112
+ if (hasVars && varCache[varName]) {
113
+ var paint = { type: 'SOLID', color: hexToRgb(fallbackHex) };
114
+ var bound = figma.variables.setBoundVariableForPaint(paint, 'color', varCache[varName]);
115
+ node.strokes = [bound];
116
+ } else {
117
+ node.strokes = [{ type: 'SOLID', color: hexToRgb(fallbackHex) }];
118
+ }
119
+ }
120
+
121
+ function bindTextFill(node, varName, fallbackHex) {
122
+ if (hasVars && varCache[varName]) {
123
+ var paint = { type: 'SOLID', color: hexToRgb(fallbackHex) };
124
+ var bound = figma.variables.setBoundVariableForPaint(paint, 'color', varCache[varName]);
125
+ node.fills = [bound];
126
+ } else {
127
+ node.fills = [{ type: 'SOLID', color: hexToRgb(fallbackHex) }];
128
+ }
129
+ }
130
+
131
+ function f(props) {
132
+ var frame = figma.createFrame();
133
+ frame.fills = [];
134
+ if (props.bg) bindFill(frame, props.bg, props.bgFallback || '#ffffff');
135
+ if (props.name) frame.name = props.name;
136
+ if (props.layout) {
137
+ frame.layoutMode = props.layout;
138
+ frame.primaryAxisSizingMode = 'AUTO';
139
+ frame.counterAxisSizingMode = 'AUTO';
140
+ }
141
+ if (props.gap !== undefined) frame.itemSpacing = props.gap;
142
+ if (props.p !== undefined) frame.paddingTop = frame.paddingBottom = frame.paddingLeft = frame.paddingRight = props.p;
143
+ if (props.px !== undefined) { frame.paddingLeft = frame.paddingRight = props.px; }
144
+ if (props.py !== undefined) { frame.paddingTop = frame.paddingBottom = props.py; }
145
+ if (props.w !== undefined) frame.resize(props.w, frame.height);
146
+ if (props.h !== undefined) frame.resize(frame.width, props.h);
147
+ if (props.rounded !== undefined) frame.cornerRadius = props.rounded;
148
+ if (props.stroke) {
149
+ bindStroke(frame, props.stroke, props.strokeFallback || '#e4e4e7');
150
+ frame.strokeWeight = props.strokeWeight || 1;
151
+ frame.strokeAlign = 'INSIDE';
152
+ }
153
+ if (props.clip) frame.clipsContent = true;
154
+ return frame;
155
+ }
156
+
157
+ function txt(str, props) {
158
+ var t = figma.createText();
159
+ t.fontName = { family: 'Inter', style: props.style || 'Regular' };
160
+ t.characters = str;
161
+ t.fontSize = props.size || 14;
162
+ bindTextFill(t, props.color, props.colorFallback || '#18181b');
163
+ if (props.name) t.name = props.name;
164
+ return t;
165
+ }
166
+
167
+ // Fix sidebar: left-align everything + spacer for user at bottom
168
+ var sidebar = dashboard.children.find(function(n) { return n.name === 'Sidebar'; });
169
+ if (sidebar) {
170
+ sidebar.counterAxisAlignItems = 'MIN';
171
+ sidebar.layoutSizingVertical = 'FILL';
172
+ for (var si = 0; si < sidebar.children.length; si++) {
173
+ var sc = sidebar.children[si];
174
+ if (sc.name === 'Logo') sc.layoutSizingHorizontal = 'FILL';
175
+ if (sc.name === 'Section Label') sc.primaryAxisAlignItems = 'MIN';
176
+ if (sc.name === 'Main Nav' || sc.name === 'Doc Nav') {
177
+ sc.counterAxisAlignItems = 'MIN';
178
+ sc.primaryAxisAlignItems = 'MIN';
179
+ }
180
+ }
181
+ var user = sidebar.children.find(function(n) { return n.name === 'User'; });
182
+ if (user) {
183
+ var spacer = figma.createFrame();
184
+ spacer.name = 'Spacer';
185
+ spacer.fills = [];
186
+ spacer.layoutMode = 'VERTICAL';
187
+ var idx = sidebar.children.indexOf(user);
188
+ sidebar.insertChild(idx, spacer);
189
+ spacer.layoutSizingHorizontal = 'FILL';
190
+ spacer.layoutGrow = 1;
191
+ }
192
+ }
193
+
194
+ // Main content area
195
+ var main = f({ name: 'Main Content', layout: 'VERTICAL', gap: 0, bg: 'background', bgFallback: '#ffffff' });
196
+ dashboard.appendChild(main);
197
+ main.layoutSizingHorizontal = 'FILL';
198
+ main.layoutSizingVertical = 'FILL';
199
+
200
+ // Header
201
+ var header = f({ name: 'Header', layout: 'HORIZONTAL', gap: 16, px: 32, py: 20, stroke: 'border', strokeFallback: '#e4e4e7' });
202
+ main.appendChild(header);
203
+ header.layoutSizingHorizontal = 'FILL';
204
+ header.counterAxisAlignItems = 'CENTER';
205
+ header.appendChild(txt('Documents', { size: 20, style: 'Semi Bold', color: 'foreground', colorFallback: '#18181b' }));
206
+
207
+ // Stats Row
208
+ var statsRow = f({ name: 'Stats Row', layout: 'HORIZONTAL', gap: 16, px: 24, py: 16 });
209
+ main.appendChild(statsRow);
210
+ statsRow.layoutSizingHorizontal = 'FILL';
211
+
212
+ var stats = [
213
+ { label: 'Total Revenue', value: '$1,250.00', change: '+12.5%', up: true },
214
+ { label: 'New Customers', value: '1,234', change: '-20%', up: false },
215
+ { label: 'Active Accounts', value: '45,678', change: '+12.5%', up: true },
216
+ { label: 'Growth Rate', value: '4.5%', change: '+4.5%', up: true }
217
+ ];
218
+
219
+ for (var i = 0; i < stats.length; i++) {
220
+ var s = stats[i];
221
+ var card = f({ name: s.label, layout: 'VERTICAL', gap: 8, p: 20, rounded: 12, bg: 'card', bgFallback: '#ffffff', stroke: 'border', strokeFallback: '#e4e4e7' });
222
+ statsRow.appendChild(card);
223
+ card.layoutSizingHorizontal = 'FILL';
224
+ card.appendChild(txt(s.label, { size: 13, style: 'Medium', color: 'muted-foreground', colorFallback: '#71717a' }));
225
+ var vr = f({ name: 'Value Row', layout: 'HORIZONTAL', gap: 8 });
226
+ card.appendChild(vr);
227
+ vr.layoutSizingHorizontal = 'FILL';
228
+ vr.counterAxisAlignItems = 'MAX';
229
+ vr.appendChild(txt(s.value, { size: 28, style: 'Bold', color: 'card-foreground', colorFallback: '#18181b' }));
230
+ vr.appendChild(txt((s.up ? '\\u2191 ' : '\\u2193 ') + s.change, { size: 12, color: s.up ? '#16a34a' : '#dc2626', colorFallback: s.up ? '#16a34a' : '#dc2626', style: 'Medium' }));
231
+ }
232
+
233
+ // Chart Card
234
+ var chartWrap = f({ name: 'Chart Wrapper', layout: 'VERTICAL', gap: 0, px: 24 });
235
+ main.appendChild(chartWrap);
236
+ chartWrap.layoutSizingHorizontal = 'FILL';
237
+
238
+ var chartCard = f({ name: 'Chart Card', layout: 'VERTICAL', gap: 12, p: 20, rounded: 12, bg: 'card', bgFallback: '#ffffff', stroke: 'border', strokeFallback: '#e4e4e7' });
239
+ chartWrap.appendChild(chartCard);
240
+ chartCard.layoutSizingHorizontal = 'FILL';
241
+ chartCard.appendChild(txt('Total Visitors', { size: 16, style: 'Semi Bold', color: 'card-foreground', colorFallback: '#18181b' }));
242
+ chartCard.appendChild(txt('Showing total visitors for the last 3 months', { size: 13, color: 'muted-foreground', colorFallback: '#a1a1aa' }));
243
+
244
+ // SVG Area Chart
245
+ var cW = 1100, cH = 180;
246
+ var d1 = [40, 85, 55, 120, 90, 145];
247
+ var d2 = [20, 50, 35, 75, 55, 95];
248
+ var maxV = 160;
249
+
250
+ function toPoints(data) {
251
+ var pts = [];
252
+ for (var j = 0; j < data.length; j++) {
253
+ pts.push(Math.round(j * cW / (data.length - 1)) + ',' + Math.round(cH - (data[j] / maxV) * cH));
254
+ }
255
+ return pts;
256
+ }
257
+
258
+ function toLine(pts) { return 'M ' + pts.join(' L '); }
259
+ function toArea(pts) { return toLine(pts) + ' L ' + cW + ',' + cH + ' L 0,' + cH + ' Z'; }
260
+
261
+ var p1 = toPoints(d1), p2 = toPoints(d2);
262
+ var grid = '';
263
+ for (var g = 0; g < 5; g++) {
264
+ var gy = Math.round(g / 4 * cH);
265
+ grid += '<line x1="0" y1="' + gy + '" x2="' + cW + '" y2="' + gy + '" stroke="#e4e4e7" stroke-width="1" stroke-dasharray="4,4"/>';
266
+ }
267
+
268
+ var svg = '<svg width="' + cW + '" height="' + cH + '" xmlns="http://www.w3.org/2000/svg">' + grid +
269
+ '<path d="' + toArea(p1) + '" fill="rgba(59,130,246,0.15)"/>' +
270
+ '<path d="' + toLine(p1) + '" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linejoin="round"/>' +
271
+ '<path d="' + toArea(p2) + '" fill="rgba(139,92,246,0.12)"/>' +
272
+ '<path d="' + toLine(p2) + '" fill="none" stroke="#8b5cf6" stroke-width="2" stroke-linejoin="round"/>' +
273
+ p1.map(function(p) { var xy = p.split(','); return '<circle cx="' + xy[0] + '" cy="' + xy[1] + '" r="3" fill="#3b82f6"/>'; }).join('') +
274
+ p2.map(function(p) { var xy = p.split(','); return '<circle cx="' + xy[0] + '" cy="' + xy[1] + '" r="3" fill="#8b5cf6"/>'; }).join('') +
275
+ '</svg>';
276
+
277
+ var svgNode = figma.createNodeFromSvg(svg);
278
+ svgNode.name = 'Area Chart';
279
+ chartCard.appendChild(svgNode);
280
+ svgNode.layoutSizingHorizontal = 'FILL';
281
+
282
+ // X-axis labels
283
+ var xAxis = f({ name: 'X Axis', layout: 'HORIZONTAL' });
284
+ chartCard.appendChild(xAxis);
285
+ xAxis.layoutSizingHorizontal = 'FILL';
286
+ xAxis.primaryAxisAlignItems = 'SPACE_BETWEEN';
287
+ ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'].forEach(function(m) {
288
+ xAxis.appendChild(txt(m, { size: 12, color: 'muted-foreground', colorFallback: '#a1a1aa' }));
289
+ });
290
+
291
+ // Data Table
292
+ var tableWrap = f({ name: 'Table Wrapper', layout: 'VERTICAL', gap: 0, px: 24, py: 8 });
293
+ main.appendChild(tableWrap);
294
+ tableWrap.layoutSizingHorizontal = 'FILL';
295
+
296
+ var table = f({ name: 'Data Table', layout: 'VERTICAL', gap: 0, rounded: 12, bg: 'card', bgFallback: '#ffffff', stroke: 'border', strokeFallback: '#e4e4e7', clip: true });
297
+ tableWrap.appendChild(table);
298
+ table.layoutSizingHorizontal = 'FILL';
299
+
300
+ function makeRow(cells, isHeader) {
301
+ var row = f({
302
+ name: isHeader ? 'Table Header' : 'Row',
303
+ layout: 'HORIZONTAL', gap: 0, px: 16, py: 10,
304
+ bg: isHeader ? 'muted' : undefined,
305
+ bgFallback: '#fafafa',
306
+ stroke: 'border', strokeFallback: '#f4f4f5'
307
+ });
308
+ table.appendChild(row);
309
+ row.layoutSizingHorizontal = 'FILL';
310
+ row.counterAxisAlignItems = 'CENTER';
311
+
312
+ // Checkbox
313
+ var chk = f({ name: 'Checkbox', w: 18, h: 18, rounded: 4, stroke: 'border', strokeFallback: '#d4d4d8' });
314
+ row.appendChild(chk);
315
+ row.appendChild(f({ w: 16, h: 1 }));
316
+
317
+ for (var j = 0; j < cells.length; j++) {
318
+ var style = isHeader ? 'Medium' : (j === 0 ? 'Medium' : 'Regular');
319
+ var varName = isHeader ? 'muted-foreground' : (j === 0 ? 'foreground' : 'muted-foreground');
320
+ var fallback = isHeader ? '#71717a' : (j === 0 ? '#18181b' : '#71717a');
321
+ var cell = txt(cells[j], { size: 13, color: varName, colorFallback: fallback, style: style });
322
+ row.appendChild(cell);
323
+ if (j === 0) {
324
+ cell.layoutSizingHorizontal = 'FILL';
325
+ } else {
326
+ cell.resize(90, cell.height);
327
+ }
328
+ }
329
+ return row;
330
+ }
331
+
332
+ makeRow(['Header', 'Type', 'Status', 'Target', 'Limit', 'Reviewer'], true);
333
+ makeRow(['Cover page', 'Introduction', 'Done', '80', '100', 'Eddie Lake'], false);
334
+ makeRow(['Table of contents', 'Narrative', 'In Process', '120', '150', 'Jamik Tashian'], false);
335
+ makeRow(['Executive summary', 'Technical', 'Done', '60', '80', 'Emily Zhang'], false);
336
+ makeRow(['Technical approach', 'Technical', 'Done', '100', '120', 'Alex Rivera'], false);
337
+ makeRow(['Design system', 'Design', 'In Process', '90', '110', 'Jordan Lee'], false);
338
+
339
+ // Footer
340
+ var footer = f({ name: 'Table Footer', layout: 'HORIZONTAL', gap: 8, px: 16, py: 10, bg: 'muted', bgFallback: '#fafafa' });
341
+ table.appendChild(footer);
342
+ footer.layoutSizingHorizontal = 'FILL';
343
+ footer.counterAxisAlignItems = 'CENTER';
344
+ footer.primaryAxisAlignItems = 'SPACE_BETWEEN';
345
+
346
+ footer.appendChild(txt('Showing 5 of 20 rows', { size: 12, color: 'muted-foreground', colorFallback: '#a1a1aa' }));
347
+
348
+ var btnGroup = f({ name: 'Buttons', layout: 'HORIZONTAL', gap: 8 });
349
+ footer.appendChild(btnGroup);
350
+
351
+ var prev = f({ name: 'Prev', layout: 'HORIZONTAL', px: 12, py: 6, rounded: 6, stroke: 'border', strokeFallback: '#e4e4e7' });
352
+ btnGroup.appendChild(prev);
353
+ prev.counterAxisAlignItems = 'CENTER';
354
+ prev.appendChild(txt('Previous', { size: 12, color: 'muted-foreground', colorFallback: '#71717a', style: 'Medium' }));
355
+
356
+ var next = f({ name: 'Next', layout: 'HORIZONTAL', px: 12, py: 6, rounded: 6, stroke: 'border', strokeFallback: '#e4e4e7' });
357
+ btnGroup.appendChild(next);
358
+ next.counterAxisAlignItems = 'CENTER';
359
+ next.appendChild(txt('Next', { size: 12, color: 'muted-foreground', colorFallback: '#71717a', style: 'Medium' }));
360
+
361
+ return { id: dashboard.id, name: dashboard.name, variables: hasVars ? 'bound' : 'fallback' };
362
+ })()`;
363
+ }
364
+
365
+ // Main create function - called by the blocks command
366
+ export async function dashboard01(context) {
367
+ const { renderJsx, evalFile, writeTemp } = context;
368
+
369
+ // Step 1: Render sidebar with JSX (gets real Lucide icons + var: bindings)
370
+ const result = await renderJsx(sidebarJsx);
371
+ const dashboardId = result.id;
372
+
373
+ // Step 2: Add main content via eval (tables, charts, data-driven + var bindings)
374
+ const code = getMainContentCode(dashboardId);
375
+ const tmpFile = writeTemp('dashboard-main.js', code);
376
+ await evalFile(tmpFile);
377
+
378
+ return dashboardId;
379
+ }
@@ -0,0 +1,27 @@
1
+ // Block registry - all available blocks
2
+ // Each block has: name, description, category, and a create function
3
+
4
+ import { dashboard01 } from './dashboard-01.js';
5
+
6
+ export const BLOCKS = [
7
+ {
8
+ id: 'dashboard-01',
9
+ name: 'Dashboard',
10
+ description: 'Analytics dashboard with sidebar, stats cards, area chart and data table',
11
+ category: 'Application',
12
+ create: dashboard01
13
+ }
14
+ ];
15
+
16
+ export function listBlocks() {
17
+ return BLOCKS.map(b => ({
18
+ id: b.id,
19
+ name: b.name,
20
+ description: b.description,
21
+ category: b.category
22
+ }));
23
+ }
24
+
25
+ export function getBlock(id) {
26
+ return BLOCKS.find(b => b.id === id);
27
+ }