design-lazyyy-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +291 -0
- package/package.json +30 -0
- package/src/figjam-client.js +310 -0
- package/src/index.js +2528 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,2528 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { execSync, spawn } from 'child_process';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
import { createInterface } from 'readline';
|
|
11
|
+
import { homedir, platform } from 'os';
|
|
12
|
+
import { FigJamClient } from './figjam-client.js';
|
|
13
|
+
|
|
14
|
+
// Platform detection
|
|
15
|
+
const IS_WINDOWS = platform() === 'win32';
|
|
16
|
+
const IS_MAC = platform() === 'darwin';
|
|
17
|
+
const IS_LINUX = platform() === 'linux';
|
|
18
|
+
|
|
19
|
+
// Platform-specific Figma paths and commands
|
|
20
|
+
function getFigmaPath() {
|
|
21
|
+
if (IS_MAC) {
|
|
22
|
+
return '/Applications/Figma.app/Contents/MacOS/Figma';
|
|
23
|
+
} else if (IS_WINDOWS) {
|
|
24
|
+
const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local');
|
|
25
|
+
return join(localAppData, 'Figma', 'Figma.exe');
|
|
26
|
+
} else {
|
|
27
|
+
// Linux
|
|
28
|
+
return '/usr/bin/figma';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function startFigma() {
|
|
33
|
+
const figmaPath = getFigmaPath();
|
|
34
|
+
if (IS_MAC) {
|
|
35
|
+
execSync('open -a Figma --args --remote-debugging-port=9222', { stdio: 'pipe' });
|
|
36
|
+
} else if (IS_WINDOWS) {
|
|
37
|
+
spawn(figmaPath, ['--remote-debugging-port=9222'], { detached: true, stdio: 'ignore' }).unref();
|
|
38
|
+
} else {
|
|
39
|
+
spawn(figmaPath, ['--remote-debugging-port=9222'], { detached: true, stdio: 'ignore' }).unref();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function killFigma() {
|
|
44
|
+
try {
|
|
45
|
+
if (IS_MAC) {
|
|
46
|
+
execSync('pkill -x Figma 2>/dev/null || true', { stdio: 'pipe' });
|
|
47
|
+
} else if (IS_WINDOWS) {
|
|
48
|
+
execSync('taskkill /IM Figma.exe /F 2>nul', { stdio: 'pipe' });
|
|
49
|
+
} else {
|
|
50
|
+
execSync('pkill -x figma 2>/dev/null || true', { stdio: 'pipe' });
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// Ignore errors if Figma wasn't running
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getManualStartCommand() {
|
|
58
|
+
if (IS_MAC) {
|
|
59
|
+
return 'open -a Figma --args --remote-debugging-port=9222';
|
|
60
|
+
} else if (IS_WINDOWS) {
|
|
61
|
+
return '"%LOCALAPPDATA%\\Figma\\Figma.exe" --remote-debugging-port=9222';
|
|
62
|
+
} else {
|
|
63
|
+
return 'figma --remote-debugging-port=9222';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
68
|
+
const __dirname = dirname(__filename);
|
|
69
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
70
|
+
|
|
71
|
+
const CONFIG_DIR = join(homedir(), '.design-lazyyy-cli');
|
|
72
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
73
|
+
|
|
74
|
+
const program = new Command();
|
|
75
|
+
|
|
76
|
+
// Helper: Prompt user
|
|
77
|
+
function prompt(question) {
|
|
78
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
79
|
+
return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer); }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Helper: Load config
|
|
83
|
+
function loadConfig() {
|
|
84
|
+
try {
|
|
85
|
+
if (existsSync(CONFIG_FILE)) {
|
|
86
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Helper: Save config
|
|
93
|
+
function saveConfig(config) {
|
|
94
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
95
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Helper: Run figma-use command
|
|
99
|
+
function figmaUse(args, options = {}) {
|
|
100
|
+
try {
|
|
101
|
+
const result = execSync(`figma-use ${args}`, {
|
|
102
|
+
encoding: 'utf8',
|
|
103
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
104
|
+
...options
|
|
105
|
+
});
|
|
106
|
+
return result;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (options.silent) return null;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Helper: Check connection
|
|
114
|
+
function checkConnection() {
|
|
115
|
+
const result = figmaUse('status', { silent: true });
|
|
116
|
+
if (!result || result.includes('Not connected')) {
|
|
117
|
+
console.log(chalk.red('\n✗ Not connected to Figma\n'));
|
|
118
|
+
console.log(chalk.white(' Make sure Figma is running with remote debugging:'));
|
|
119
|
+
console.log(chalk.cyan(' design-lazyyy-cli connect\n'));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Helper: Check if figma-use is installed
|
|
126
|
+
function checkDependencies(silent = false) {
|
|
127
|
+
try {
|
|
128
|
+
execSync('which figma-use', { stdio: 'pipe' });
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
if (!silent) {
|
|
132
|
+
console.log(chalk.yellow(' Installing figma-use...'));
|
|
133
|
+
execSync('npm install -g figma-use', { stdio: 'inherit' });
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Helper: Check if Figma is patched
|
|
140
|
+
function isFigmaPatched() {
|
|
141
|
+
const config = loadConfig();
|
|
142
|
+
return config.patched === true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Helper: Hex to Figma RGB
|
|
146
|
+
function hexToRgb(hex) {
|
|
147
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
148
|
+
return {
|
|
149
|
+
r: parseInt(result[1], 16) / 255,
|
|
150
|
+
g: parseInt(result[2], 16) / 255,
|
|
151
|
+
b: parseInt(result[3], 16) / 255
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Helper: Smart positioning code (returns JS to get next free X position)
|
|
156
|
+
function smartPosCode(gap = 100) {
|
|
157
|
+
return `
|
|
158
|
+
const children = figma.currentPage.children;
|
|
159
|
+
let smartX = 0;
|
|
160
|
+
if (children.length > 0) {
|
|
161
|
+
children.forEach(n => { smartX = Math.max(smartX, n.x + n.width); });
|
|
162
|
+
smartX += ${gap};
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
program
|
|
168
|
+
.name('design-lazyyy-cli')
|
|
169
|
+
.description('CLI for managing Figma design systems')
|
|
170
|
+
.version(pkg.version);
|
|
171
|
+
|
|
172
|
+
// Default action when no command is given
|
|
173
|
+
program.action(async () => {
|
|
174
|
+
const config = loadConfig();
|
|
175
|
+
|
|
176
|
+
// First time? Run init
|
|
177
|
+
if (!config.patched || !checkDependencies(true)) {
|
|
178
|
+
showBanner();
|
|
179
|
+
console.log(chalk.white(' Welcome! Let\'s get you set up.\n'));
|
|
180
|
+
console.log(chalk.gray(' This takes about 30 seconds. No API key needed.\n'));
|
|
181
|
+
|
|
182
|
+
// Step 1: Check Node version
|
|
183
|
+
console.log(chalk.blue('Step 1/4: ') + 'Checking Node.js...');
|
|
184
|
+
const nodeVersion = process.version;
|
|
185
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
186
|
+
if (nodeMajor < 18) {
|
|
187
|
+
console.log(chalk.red(` ✗ Node.js ${nodeVersion} is too old. Please upgrade to Node 18+`));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
console.log(chalk.green(` ✓ Node.js ${nodeVersion}`));
|
|
191
|
+
|
|
192
|
+
// Step 2: Install figma-use
|
|
193
|
+
console.log(chalk.blue('\nStep 2/4: ') + 'Installing dependencies...');
|
|
194
|
+
if (checkDependencies(true)) {
|
|
195
|
+
console.log(chalk.green(' ✓ figma-use already installed'));
|
|
196
|
+
} else {
|
|
197
|
+
const spinner = ora(' Installing figma-use...').start();
|
|
198
|
+
try {
|
|
199
|
+
execSync('npm install -g figma-use', { stdio: 'pipe' });
|
|
200
|
+
spinner.succeed('figma-use installed');
|
|
201
|
+
} catch (error) {
|
|
202
|
+
spinner.fail('Failed to install figma-use');
|
|
203
|
+
console.log(chalk.gray(' Try manually: npm install -g figma-use'));
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Step 3: Patch Figma
|
|
209
|
+
console.log(chalk.blue('\nStep 3/4: ') + 'Patching Figma Desktop...');
|
|
210
|
+
if (config.patched) {
|
|
211
|
+
console.log(chalk.green(' ✓ Figma already patched'));
|
|
212
|
+
} else {
|
|
213
|
+
console.log(chalk.gray(' (This allows CLI to connect to Figma)'));
|
|
214
|
+
const spinner = ora(' Patching...').start();
|
|
215
|
+
try {
|
|
216
|
+
execSync('figma-use patch', { stdio: 'pipe' });
|
|
217
|
+
config.patched = true;
|
|
218
|
+
saveConfig(config);
|
|
219
|
+
spinner.succeed('Figma patched');
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error.message?.includes('already patched') || error.stderr?.includes('already patched')) {
|
|
222
|
+
config.patched = true;
|
|
223
|
+
saveConfig(config);
|
|
224
|
+
spinner.succeed('Figma already patched');
|
|
225
|
+
} else {
|
|
226
|
+
spinner.fail('Patch failed');
|
|
227
|
+
console.log(chalk.gray(' Try manually: figma-use patch'));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Step 4: Start Figma
|
|
233
|
+
console.log(chalk.blue('\nStep 4/4: ') + 'Starting Figma...');
|
|
234
|
+
try {
|
|
235
|
+
killFigma();
|
|
236
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
237
|
+
startFigma();
|
|
238
|
+
console.log(chalk.green(' ✓ Figma started'));
|
|
239
|
+
|
|
240
|
+
// Wait for connection
|
|
241
|
+
const spinner = ora(' Waiting for connection...').start();
|
|
242
|
+
let connected = false;
|
|
243
|
+
for (let i = 0; i < 10; i++) {
|
|
244
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
245
|
+
const result = figmaUse('status', { silent: true });
|
|
246
|
+
if (result && result.includes('Connected')) {
|
|
247
|
+
connected = true;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (connected) {
|
|
253
|
+
spinner.succeed('Connected to Figma');
|
|
254
|
+
} else {
|
|
255
|
+
spinner.warn('Connection pending - open a file in Figma');
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.log(chalk.yellow(' ! Could not start Figma automatically'));
|
|
259
|
+
console.log(chalk.gray(' Start manually: ' + getManualStartCommand()));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Done!
|
|
263
|
+
console.log(chalk.green('\n ✓ Setup complete!\n'));
|
|
264
|
+
showQuickStart();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Already set up - check connection and show status
|
|
269
|
+
showBanner();
|
|
270
|
+
|
|
271
|
+
const result = figmaUse('status', { silent: true });
|
|
272
|
+
if (result && result.includes('Connected')) {
|
|
273
|
+
console.log(chalk.green(' ✓ Connected to Figma\n'));
|
|
274
|
+
console.log(chalk.gray(result.trim().split('\n').map(l => ' ' + l).join('\n')));
|
|
275
|
+
console.log();
|
|
276
|
+
showQuickStart();
|
|
277
|
+
} else {
|
|
278
|
+
console.log(chalk.yellow(' ⚠ Figma not connected\n'));
|
|
279
|
+
console.log(chalk.white(' Starting Figma...'));
|
|
280
|
+
try {
|
|
281
|
+
killFigma();
|
|
282
|
+
await new Promise(r => setTimeout(r, 500));
|
|
283
|
+
startFigma();
|
|
284
|
+
console.log(chalk.green(' ✓ Figma started\n'));
|
|
285
|
+
|
|
286
|
+
const spinner = ora(' Waiting for connection...').start();
|
|
287
|
+
for (let i = 0; i < 8; i++) {
|
|
288
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
289
|
+
const res = figmaUse('status', { silent: true });
|
|
290
|
+
if (res && res.includes('Connected')) {
|
|
291
|
+
spinner.succeed('Connected to Figma\n');
|
|
292
|
+
showQuickStart();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
spinner.warn('Open a file in Figma to connect\n');
|
|
297
|
+
showQuickStart();
|
|
298
|
+
} catch {
|
|
299
|
+
console.log(chalk.gray(' Start manually: ' + getManualStartCommand() + '\n'));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
function showQuickStart() {
|
|
305
|
+
console.log(chalk.white(' Quick start:\n'));
|
|
306
|
+
console.log(chalk.gray(' Create design system ') + chalk.cyan('design-lazyyy-cli tokens ds'));
|
|
307
|
+
console.log(chalk.gray(' Create Tailwind colors ') + chalk.cyan('design-lazyyy-cli tokens tailwind'));
|
|
308
|
+
console.log(chalk.gray(' List all variables ') + chalk.cyan('design-lazyyy-cli var list'));
|
|
309
|
+
console.log(chalk.gray(' Render JSX to Figma ') + chalk.cyan('design-lazyyy-cli render \'<Frame>...</Frame>\''));
|
|
310
|
+
console.log(chalk.gray(' See all commands ') + chalk.cyan('design-lazyyy-cli --help'));
|
|
311
|
+
console.log();
|
|
312
|
+
console.log();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ============ WELCOME BANNER ============
|
|
316
|
+
|
|
317
|
+
function showBanner() {
|
|
318
|
+
console.log(chalk.cyan(`
|
|
319
|
+
██████╗ ███████╗███████╗██╗ ██████╗ ███╗ ██╗ ██╗ █████╗ ███████╗██╗ ██╗██╗ ██╗██╗ ██╗
|
|
320
|
+
██╔══██╗██╔════╝██╔════╝██║██╔════╝ ████╗ ██║ ██║ ██╔══██╗╚══███╔╝╚██╗ ██╔╝╚██╗ ██╔╝╚██╗ ██╔╝
|
|
321
|
+
██║ ██║█████╗ ███████╗██║██║ ███╗██╔██╗ ██║█████╗██║ ███████║ ███╔╝ ╚████╔╝ ╚████╔╝ ╚████╔╝
|
|
322
|
+
██║ ██║██╔══╝ ╚════██║██║██║ ██║██║╚██╗██║╚════╝██║ ██╔══██║ ███╔╝ ╚██╔╝ ╚██╔╝ ╚██╔╝
|
|
323
|
+
██████╔╝███████╗███████║██║╚██████╔╝██║ ╚████║ ███████╗██║ ██║███████╗ ██║ ██║ ██║
|
|
324
|
+
╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝
|
|
325
|
+
`));
|
|
326
|
+
console.log(chalk.white(` Design System CLI for Figma ${chalk.gray('v' + pkg.version)}`));
|
|
327
|
+
console.log(chalk.gray(` by plugin87\n`));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============ INIT (Interactive Onboarding) ============
|
|
331
|
+
|
|
332
|
+
program
|
|
333
|
+
.command('init')
|
|
334
|
+
.description('Interactive setup wizard')
|
|
335
|
+
.action(async () => {
|
|
336
|
+
showBanner();
|
|
337
|
+
|
|
338
|
+
console.log(chalk.white(' Welcome! Let\'s get you set up.\n'));
|
|
339
|
+
console.log(chalk.gray(' This takes about 30 seconds. No API key needed.\n'));
|
|
340
|
+
|
|
341
|
+
// Step 1: Check Node version
|
|
342
|
+
console.log(chalk.blue('Step 1/4: ') + 'Checking Node.js...');
|
|
343
|
+
const nodeVersion = process.version;
|
|
344
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
345
|
+
if (nodeMajor < 18) {
|
|
346
|
+
console.log(chalk.red(` ✗ Node.js ${nodeVersion} is too old. Please upgrade to Node 18+`));
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
console.log(chalk.green(` ✓ Node.js ${nodeVersion}`));
|
|
350
|
+
|
|
351
|
+
// Step 2: Install figma-use
|
|
352
|
+
console.log(chalk.blue('\nStep 2/4: ') + 'Installing dependencies...');
|
|
353
|
+
if (checkDependencies(true)) {
|
|
354
|
+
console.log(chalk.green(' ✓ figma-use already installed'));
|
|
355
|
+
} else {
|
|
356
|
+
const spinner = ora(' Installing figma-use...').start();
|
|
357
|
+
try {
|
|
358
|
+
execSync('npm install -g figma-use', { stdio: 'pipe' });
|
|
359
|
+
spinner.succeed('figma-use installed');
|
|
360
|
+
} catch (error) {
|
|
361
|
+
spinner.fail('Failed to install figma-use');
|
|
362
|
+
console.log(chalk.gray(' Try manually: npm install -g figma-use'));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Step 3: Patch Figma
|
|
368
|
+
console.log(chalk.blue('\nStep 3/4: ') + 'Patching Figma Desktop...');
|
|
369
|
+
const config = loadConfig();
|
|
370
|
+
if (config.patched) {
|
|
371
|
+
console.log(chalk.green(' ✓ Figma already patched'));
|
|
372
|
+
} else {
|
|
373
|
+
console.log(chalk.gray(' (This allows CLI to connect to Figma)'));
|
|
374
|
+
const spinner = ora(' Patching...').start();
|
|
375
|
+
try {
|
|
376
|
+
execSync('figma-use patch', { stdio: 'pipe' });
|
|
377
|
+
config.patched = true;
|
|
378
|
+
saveConfig(config);
|
|
379
|
+
spinner.succeed('Figma patched');
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error.message?.includes('already patched')) {
|
|
382
|
+
config.patched = true;
|
|
383
|
+
saveConfig(config);
|
|
384
|
+
spinner.succeed('Figma already patched');
|
|
385
|
+
} else {
|
|
386
|
+
spinner.fail('Patch failed');
|
|
387
|
+
console.log(chalk.gray(' Try manually: figma-use patch'));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Step 4: Start Figma
|
|
393
|
+
console.log(chalk.blue('\nStep 4/4: ') + 'Starting Figma...');
|
|
394
|
+
try {
|
|
395
|
+
killFigma();
|
|
396
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
397
|
+
startFigma();
|
|
398
|
+
console.log(chalk.green(' ✓ Figma started'));
|
|
399
|
+
|
|
400
|
+
// Wait for connection
|
|
401
|
+
const spinner = ora(' Waiting for connection...').start();
|
|
402
|
+
let connected = false;
|
|
403
|
+
for (let i = 0; i < 10; i++) {
|
|
404
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
405
|
+
const result = figmaUse('status', { silent: true });
|
|
406
|
+
if (result && result.includes('Connected')) {
|
|
407
|
+
connected = true;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (connected) {
|
|
413
|
+
spinner.succeed('Connected to Figma');
|
|
414
|
+
} else {
|
|
415
|
+
spinner.warn('Connection pending - open a file in Figma');
|
|
416
|
+
}
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.log(chalk.yellow(' ! Could not start Figma automatically'));
|
|
419
|
+
console.log(chalk.gray(' Start manually: ' + getManualStartCommand()));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Done!
|
|
423
|
+
console.log(chalk.green('\n ✓ Setup complete!\n'));
|
|
424
|
+
|
|
425
|
+
console.log(chalk.white(' Quick start:\n'));
|
|
426
|
+
console.log(chalk.gray(' Create Tailwind colors ') + chalk.cyan('design-lazyyy-cli tokens tailwind'));
|
|
427
|
+
console.log(chalk.gray(' Create spacing scale ') + chalk.cyan('design-lazyyy-cli tokens spacing'));
|
|
428
|
+
console.log(chalk.gray(' List all variables ') + chalk.cyan('design-lazyyy-cli var list'));
|
|
429
|
+
console.log(chalk.gray(' Render JSX to Figma ') + chalk.cyan('design-lazyyy-cli render \'<Frame>...</Frame>\''));
|
|
430
|
+
console.log(chalk.gray(' See all commands ') + chalk.cyan('design-lazyyy-cli --help'));
|
|
431
|
+
console.log();
|
|
432
|
+
console.log();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// ============ SETUP (alias for init) ============
|
|
436
|
+
|
|
437
|
+
program
|
|
438
|
+
.command('setup')
|
|
439
|
+
.description('Setup Figma for CLI access (alias for init)')
|
|
440
|
+
.action(() => {
|
|
441
|
+
// Redirect to init
|
|
442
|
+
execSync('design-lazyyy-cli init', { stdio: 'inherit' });
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ============ STATUS ============
|
|
446
|
+
|
|
447
|
+
program
|
|
448
|
+
.command('status')
|
|
449
|
+
.description('Check connection to Figma')
|
|
450
|
+
.action(() => {
|
|
451
|
+
// Check if first run
|
|
452
|
+
const config = loadConfig();
|
|
453
|
+
if (!config.patched && !checkDependencies(true)) {
|
|
454
|
+
console.log(chalk.yellow('\n⚠ First time? Run the setup wizard:\n'));
|
|
455
|
+
console.log(chalk.cyan(' design-lazyyy-cli init\n'));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
figmaUse('status');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// ============ CONNECT ============
|
|
462
|
+
|
|
463
|
+
program
|
|
464
|
+
.command('connect')
|
|
465
|
+
.description('Start Figma with remote debugging enabled')
|
|
466
|
+
.action(async () => {
|
|
467
|
+
// Check if first run
|
|
468
|
+
const config = loadConfig();
|
|
469
|
+
if (!config.patched) {
|
|
470
|
+
console.log(chalk.yellow('\n⚠ First time? Run the setup wizard:\n'));
|
|
471
|
+
console.log(chalk.cyan(' design-lazyyy-cli init\n'));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
console.log(chalk.blue('Starting Figma...'));
|
|
476
|
+
try {
|
|
477
|
+
killFigma();
|
|
478
|
+
await new Promise(r => setTimeout(r, 500));
|
|
479
|
+
} catch {}
|
|
480
|
+
|
|
481
|
+
startFigma();
|
|
482
|
+
console.log(chalk.green('✓ Figma started\n'));
|
|
483
|
+
|
|
484
|
+
// Wait and check connection
|
|
485
|
+
const spinner = ora('Waiting for connection...').start();
|
|
486
|
+
for (let i = 0; i < 8; i++) {
|
|
487
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
488
|
+
const result = figmaUse('status', { silent: true });
|
|
489
|
+
if (result && result.includes('Connected')) {
|
|
490
|
+
spinner.succeed('Connected to Figma');
|
|
491
|
+
console.log(chalk.gray(result.trim()));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
spinner.warn('Open a file in Figma to connect');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// ============ VARIABLES ============
|
|
499
|
+
|
|
500
|
+
const variables = program
|
|
501
|
+
.command('variables')
|
|
502
|
+
.alias('var')
|
|
503
|
+
.description('Manage design tokens/variables');
|
|
504
|
+
|
|
505
|
+
variables
|
|
506
|
+
.command('list')
|
|
507
|
+
.description('List all variables')
|
|
508
|
+
.action(() => {
|
|
509
|
+
checkConnection();
|
|
510
|
+
figmaUse('variable list');
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
variables
|
|
514
|
+
.command('create <name>')
|
|
515
|
+
.description('Create a variable')
|
|
516
|
+
.requiredOption('-c, --collection <id>', 'Collection ID')
|
|
517
|
+
.requiredOption('-t, --type <type>', 'Type: COLOR, FLOAT, STRING, BOOLEAN')
|
|
518
|
+
.option('-v, --value <value>', 'Initial value')
|
|
519
|
+
.action((name, options) => {
|
|
520
|
+
checkConnection();
|
|
521
|
+
let cmd = `variable create "${name}" --collection "${options.collection}" --type ${options.type}`;
|
|
522
|
+
if (options.value) cmd += ` --value "${options.value}"`;
|
|
523
|
+
figmaUse(cmd);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
variables
|
|
527
|
+
.command('find <pattern>')
|
|
528
|
+
.description('Find variables by name pattern')
|
|
529
|
+
.action((pattern) => {
|
|
530
|
+
checkConnection();
|
|
531
|
+
figmaUse(`variable find "${pattern}"`);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// ============ COLLECTIONS ============
|
|
535
|
+
|
|
536
|
+
const collections = program
|
|
537
|
+
.command('collections')
|
|
538
|
+
.alias('col')
|
|
539
|
+
.description('Manage variable collections');
|
|
540
|
+
|
|
541
|
+
collections
|
|
542
|
+
.command('list')
|
|
543
|
+
.description('List all collections')
|
|
544
|
+
.action(() => {
|
|
545
|
+
checkConnection();
|
|
546
|
+
figmaUse('collection list');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
collections
|
|
550
|
+
.command('create <name>')
|
|
551
|
+
.description('Create a collection')
|
|
552
|
+
.action((name) => {
|
|
553
|
+
checkConnection();
|
|
554
|
+
figmaUse(`collection create "${name}"`);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// ============ TOKENS (PRESETS) ============
|
|
558
|
+
|
|
559
|
+
const tokens = program
|
|
560
|
+
.command('tokens')
|
|
561
|
+
.description('Create design token presets');
|
|
562
|
+
|
|
563
|
+
tokens
|
|
564
|
+
.command('tailwind')
|
|
565
|
+
.description('Create Tailwind CSS color palette')
|
|
566
|
+
.option('-c, --collection <name>', 'Collection name', 'Color - Primitive')
|
|
567
|
+
.action((options) => {
|
|
568
|
+
checkConnection();
|
|
569
|
+
const spinner = ora('Creating Tailwind color palette...').start();
|
|
570
|
+
|
|
571
|
+
const tailwindColors = {
|
|
572
|
+
slate: { 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', 950: '#020617' },
|
|
573
|
+
gray: { 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827', 950: '#030712' },
|
|
574
|
+
zinc: { 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' },
|
|
575
|
+
neutral: { 50: '#fafafa', 100: '#f5f5f5', 200: '#e5e5e5', 300: '#d4d4d4', 400: '#a3a3a3', 500: '#737373', 600: '#525252', 700: '#404040', 800: '#262626', 900: '#171717', 950: '#0a0a0a' },
|
|
576
|
+
stone: { 50: '#fafaf9', 100: '#f5f5f4', 200: '#e7e5e4', 300: '#d6d3d1', 400: '#a8a29e', 500: '#78716c', 600: '#57534e', 700: '#44403c', 800: '#292524', 900: '#1c1917', 950: '#0c0a09' },
|
|
577
|
+
red: { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', 950: '#450a0a' },
|
|
578
|
+
orange: { 50: '#fff7ed', 100: '#ffedd5', 200: '#fed7aa', 300: '#fdba74', 400: '#fb923c', 500: '#f97316', 600: '#ea580c', 700: '#c2410c', 800: '#9a3412', 900: '#7c2d12', 950: '#431407' },
|
|
579
|
+
amber: { 50: '#fffbeb', 100: '#fef3c7', 200: '#fde68a', 300: '#fcd34d', 400: '#fbbf24', 500: '#f59e0b', 600: '#d97706', 700: '#b45309', 800: '#92400e', 900: '#78350f', 950: '#451a03' },
|
|
580
|
+
yellow: { 50: '#fefce8', 100: '#fef9c3', 200: '#fef08a', 300: '#fde047', 400: '#facc15', 500: '#eab308', 600: '#ca8a04', 700: '#a16207', 800: '#854d0e', 900: '#713f12', 950: '#422006' },
|
|
581
|
+
lime: { 50: '#f7fee7', 100: '#ecfccb', 200: '#d9f99d', 300: '#bef264', 400: '#a3e635', 500: '#84cc16', 600: '#65a30d', 700: '#4d7c0f', 800: '#3f6212', 900: '#365314', 950: '#1a2e05' },
|
|
582
|
+
green: { 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac', 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d', 800: '#166534', 900: '#14532d', 950: '#052e16' },
|
|
583
|
+
emerald: { 50: '#ecfdf5', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' },
|
|
584
|
+
teal: { 50: '#f0fdfa', 100: '#ccfbf1', 200: '#99f6e4', 300: '#5eead4', 400: '#2dd4bf', 500: '#14b8a6', 600: '#0d9488', 700: '#0f766e', 800: '#115e59', 900: '#134e4a', 950: '#042f2e' },
|
|
585
|
+
cyan: { 50: '#ecfeff', 100: '#cffafe', 200: '#a5f3fc', 300: '#67e8f9', 400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2', 700: '#0e7490', 800: '#155e75', 900: '#164e63', 950: '#083344' },
|
|
586
|
+
sky: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', 950: '#082f49' },
|
|
587
|
+
blue: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' },
|
|
588
|
+
indigo: { 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc', 400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca', 800: '#3730a3', 900: '#312e81', 950: '#1e1b4b' },
|
|
589
|
+
violet: { 50: '#f5f3ff', 100: '#ede9fe', 200: '#ddd6fe', 300: '#c4b5fd', 400: '#a78bfa', 500: '#8b5cf6', 600: '#7c3aed', 700: '#6d28d9', 800: '#5b21b6', 900: '#4c1d95', 950: '#2e1065' },
|
|
590
|
+
purple: { 50: '#faf5ff', 100: '#f3e8ff', 200: '#e9d5ff', 300: '#d8b4fe', 400: '#c084fc', 500: '#a855f7', 600: '#9333ea', 700: '#7e22ce', 800: '#6b21a8', 900: '#581c87', 950: '#3b0764' },
|
|
591
|
+
fuchsia: { 50: '#fdf4ff', 100: '#fae8ff', 200: '#f5d0fe', 300: '#f0abfc', 400: '#e879f9', 500: '#d946ef', 600: '#c026d3', 700: '#a21caf', 800: '#86198f', 900: '#701a75', 950: '#4a044e' },
|
|
592
|
+
pink: { 50: '#fdf2f8', 100: '#fce7f3', 200: '#fbcfe8', 300: '#f9a8d4', 400: '#f472b6', 500: '#ec4899', 600: '#db2777', 700: '#be185d', 800: '#9d174d', 900: '#831843', 950: '#500724' },
|
|
593
|
+
rose: { 50: '#fff1f2', 100: '#ffe4e6', 200: '#fecdd3', 300: '#fda4af', 400: '#fb7185', 500: '#f43f5e', 600: '#e11d48', 700: '#be123c', 800: '#9f1239', 900: '#881337', 950: '#4c0519' }
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const code = `
|
|
597
|
+
const colors = ${JSON.stringify(tailwindColors)};
|
|
598
|
+
function hexToRgb(hex) {
|
|
599
|
+
const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
|
|
600
|
+
return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
|
|
601
|
+
}
|
|
602
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
|
|
603
|
+
if (!col) col = figma.variables.createVariableCollection('${options.collection}');
|
|
604
|
+
const modeId = col.modes[0].modeId;
|
|
605
|
+
let count = 0;
|
|
606
|
+
Object.entries(colors).forEach(([colorName, shades]) => {
|
|
607
|
+
Object.entries(shades).forEach(([shade, hex]) => {
|
|
608
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === colorName + '/' + shade);
|
|
609
|
+
if (!existing) {
|
|
610
|
+
const v = figma.variables.createVariable(colorName + '/' + shade, col.id, 'COLOR');
|
|
611
|
+
v.setValueForMode(modeId, hexToRgb(hex));
|
|
612
|
+
count++;
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
'Created ' + count + ' color variables in ${options.collection}'
|
|
617
|
+
`;
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
621
|
+
spinner.succeed(result?.trim() || 'Created Tailwind palette');
|
|
622
|
+
} catch (error) {
|
|
623
|
+
spinner.fail('Failed to create palette');
|
|
624
|
+
console.error(error.message);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
tokens
|
|
629
|
+
.command('shadcn')
|
|
630
|
+
.description('Create shadcn/ui color primitives (from v3.shadcn.com/colors)')
|
|
631
|
+
.option('-c, --collection <name>', 'Collection name', 'shadcn/primitives')
|
|
632
|
+
.action((options) => {
|
|
633
|
+
checkConnection();
|
|
634
|
+
const spinner = ora('Creating shadcn color primitives...').start();
|
|
635
|
+
|
|
636
|
+
// All colors from https://v3.shadcn.com/colors
|
|
637
|
+
const shadcnColors = {
|
|
638
|
+
slate: { 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', 950: '#020617' },
|
|
639
|
+
gray: { 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827', 950: '#030712' },
|
|
640
|
+
zinc: { 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' },
|
|
641
|
+
neutral: { 50: '#fafafa', 100: '#f5f5f5', 200: '#e5e5e5', 300: '#d4d4d4', 400: '#a3a3a3', 500: '#737373', 600: '#525252', 700: '#404040', 800: '#262626', 900: '#171717', 950: '#0a0a0a' },
|
|
642
|
+
stone: { 50: '#fafaf9', 100: '#f5f5f4', 200: '#e7e5e4', 300: '#d6d3d1', 400: '#a8a29e', 500: '#78716c', 600: '#57534e', 700: '#44403c', 800: '#292524', 900: '#1c1917', 950: '#0c0a09' },
|
|
643
|
+
red: { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', 950: '#450a0a' },
|
|
644
|
+
orange: { 50: '#fff7ed', 100: '#ffedd5', 200: '#fed7aa', 300: '#fdba74', 400: '#fb923c', 500: '#f97316', 600: '#ea580c', 700: '#c2410c', 800: '#9a3412', 900: '#7c2d12', 950: '#431407' },
|
|
645
|
+
amber: { 50: '#fffbeb', 100: '#fef3c7', 200: '#fde68a', 300: '#fcd34d', 400: '#fbbf24', 500: '#f59e0b', 600: '#d97706', 700: '#b45309', 800: '#92400e', 900: '#78350f', 950: '#451a03' },
|
|
646
|
+
yellow: { 50: '#fefce8', 100: '#fef9c3', 200: '#fef08a', 300: '#fde047', 400: '#facc15', 500: '#eab308', 600: '#ca8a04', 700: '#a16207', 800: '#854d0e', 900: '#713f12', 950: '#422006' },
|
|
647
|
+
lime: { 50: '#f7fee7', 100: '#ecfccb', 200: '#d9f99d', 300: '#bef264', 400: '#a3e635', 500: '#84cc16', 600: '#65a30d', 700: '#4d7c0f', 800: '#3f6212', 900: '#365314', 950: '#1a2e05' },
|
|
648
|
+
green: { 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac', 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d', 800: '#166534', 900: '#14532d', 950: '#052e16' },
|
|
649
|
+
emerald: { 50: '#ecfdf5', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' },
|
|
650
|
+
teal: { 50: '#f0fdfa', 100: '#ccfbf1', 200: '#99f6e4', 300: '#5eead4', 400: '#2dd4bf', 500: '#14b8a6', 600: '#0d9488', 700: '#0f766e', 800: '#115e59', 900: '#134e4a', 950: '#042f2e' },
|
|
651
|
+
cyan: { 50: '#ecfeff', 100: '#cffafe', 200: '#a5f3fc', 300: '#67e8f9', 400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2', 700: '#0e7490', 800: '#155e75', 900: '#164e63', 950: '#083344' },
|
|
652
|
+
sky: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', 950: '#082f49' },
|
|
653
|
+
blue: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' },
|
|
654
|
+
indigo: { 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc', 400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca', 800: '#3730a3', 900: '#312e81', 950: '#1e1b4b' },
|
|
655
|
+
violet: { 50: '#f5f3ff', 100: '#ede9fe', 200: '#ddd6fe', 300: '#c4b5fd', 400: '#a78bfa', 500: '#8b5cf6', 600: '#7c3aed', 700: '#6d28d9', 800: '#5b21b6', 900: '#4c1d95', 950: '#2e1065' },
|
|
656
|
+
purple: { 50: '#faf5ff', 100: '#f3e8ff', 200: '#e9d5ff', 300: '#d8b4fe', 400: '#c084fc', 500: '#a855f7', 600: '#9333ea', 700: '#7e22ce', 800: '#6b21a8', 900: '#581c87', 950: '#3b0764' },
|
|
657
|
+
fuchsia: { 50: '#fdf4ff', 100: '#fae8ff', 200: '#f5d0fe', 300: '#f0abfc', 400: '#e879f9', 500: '#d946ef', 600: '#c026d3', 700: '#a21caf', 800: '#86198f', 900: '#701a75', 950: '#4a044e' },
|
|
658
|
+
pink: { 50: '#fdf2f8', 100: '#fce7f3', 200: '#fbcfe8', 300: '#f9a8d4', 400: '#f472b6', 500: '#ec4899', 600: '#db2777', 700: '#be185d', 800: '#9d174d', 900: '#831843', 950: '#500724' },
|
|
659
|
+
rose: { 50: '#fff1f2', 100: '#ffe4e6', 200: '#fecdd3', 300: '#fda4af', 400: '#fb7185', 500: '#f43f5e', 600: '#e11d48', 700: '#be123c', 800: '#9f1239', 900: '#881337', 950: '#4c0519' }
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const code = `
|
|
663
|
+
const colors = ${JSON.stringify(shadcnColors)};
|
|
664
|
+
function hexToRgb(hex) {
|
|
665
|
+
const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
|
|
666
|
+
return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
|
|
667
|
+
}
|
|
668
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
|
|
669
|
+
if (!col) col = figma.variables.createVariableCollection('${options.collection}');
|
|
670
|
+
const modeId = col.modes[0].modeId;
|
|
671
|
+
let count = 0;
|
|
672
|
+
Object.entries(colors).forEach(([colorName, shades]) => {
|
|
673
|
+
Object.entries(shades).forEach(([shade, hex]) => {
|
|
674
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === colorName + '/' + shade);
|
|
675
|
+
if (!existing) {
|
|
676
|
+
const v = figma.variables.createVariable(colorName + '/' + shade, col.id, 'COLOR');
|
|
677
|
+
v.setValueForMode(modeId, hexToRgb(hex));
|
|
678
|
+
count++;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
'Created ' + count + ' shadcn color variables in ${options.collection}'
|
|
683
|
+
`;
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
687
|
+
spinner.succeed(result?.trim() || 'Created shadcn primitives (231 colors)');
|
|
688
|
+
} catch (error) {
|
|
689
|
+
spinner.fail('Failed to create shadcn colors');
|
|
690
|
+
console.error(error.message);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
tokens
|
|
695
|
+
.command('spacing')
|
|
696
|
+
.description('Create spacing scale (4px base)')
|
|
697
|
+
.option('-c, --collection <name>', 'Collection name', 'Spacing')
|
|
698
|
+
.action((options) => {
|
|
699
|
+
checkConnection();
|
|
700
|
+
const spinner = ora('Creating spacing scale...').start();
|
|
701
|
+
|
|
702
|
+
const spacings = {
|
|
703
|
+
'0': 0, '0.5': 2, '1': 4, '1.5': 6, '2': 8, '2.5': 10,
|
|
704
|
+
'3': 12, '3.5': 14, '4': 16, '5': 20, '6': 24, '7': 28,
|
|
705
|
+
'8': 32, '9': 36, '10': 40, '11': 44, '12': 48,
|
|
706
|
+
'14': 56, '16': 64, '20': 80, '24': 96, '28': 112,
|
|
707
|
+
'32': 128, '36': 144, '40': 160, '44': 176, '48': 192
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const code = `
|
|
711
|
+
const spacings = ${JSON.stringify(spacings)};
|
|
712
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
|
|
713
|
+
if (!col) col = figma.variables.createVariableCollection('${options.collection}');
|
|
714
|
+
const modeId = col.modes[0].modeId;
|
|
715
|
+
let count = 0;
|
|
716
|
+
Object.entries(spacings).forEach(([name, value]) => {
|
|
717
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === 'spacing/' + name);
|
|
718
|
+
if (!existing) {
|
|
719
|
+
const v = figma.variables.createVariable('spacing/' + name, col.id, 'FLOAT');
|
|
720
|
+
v.setValueForMode(modeId, value);
|
|
721
|
+
count++;
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
'Created ' + count + ' spacing variables'
|
|
725
|
+
`;
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
729
|
+
spinner.succeed(result?.trim() || 'Created spacing scale');
|
|
730
|
+
} catch (error) {
|
|
731
|
+
spinner.fail('Failed to create spacing scale');
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
tokens
|
|
736
|
+
.command('radii')
|
|
737
|
+
.description('Create border radius scale')
|
|
738
|
+
.option('-c, --collection <name>', 'Collection name', 'Radii')
|
|
739
|
+
.action((options) => {
|
|
740
|
+
checkConnection();
|
|
741
|
+
const spinner = ora('Creating border radii...').start();
|
|
742
|
+
|
|
743
|
+
const radii = {
|
|
744
|
+
'none': 0, 'sm': 2, 'default': 4, 'md': 6, 'lg': 8,
|
|
745
|
+
'xl': 12, '2xl': 16, '3xl': 24, 'full': 9999
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const code = `
|
|
749
|
+
const radii = ${JSON.stringify(radii)};
|
|
750
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
|
|
751
|
+
if (!col) col = figma.variables.createVariableCollection('${options.collection}');
|
|
752
|
+
const modeId = col.modes[0].modeId;
|
|
753
|
+
let count = 0;
|
|
754
|
+
Object.entries(radii).forEach(([name, value]) => {
|
|
755
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === 'radius/' + name);
|
|
756
|
+
if (!existing) {
|
|
757
|
+
const v = figma.variables.createVariable('radius/' + name, col.id, 'FLOAT');
|
|
758
|
+
v.setValueForMode(modeId, value);
|
|
759
|
+
count++;
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
'Created ' + count + ' radius variables'
|
|
763
|
+
`;
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
767
|
+
spinner.succeed(result?.trim() || 'Created border radii');
|
|
768
|
+
} catch (error) {
|
|
769
|
+
spinner.fail('Failed to create radii');
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
tokens
|
|
774
|
+
.command('import <file>')
|
|
775
|
+
.description('Import tokens from JSON file')
|
|
776
|
+
.option('-c, --collection <name>', 'Collection name')
|
|
777
|
+
.action((file, options) => {
|
|
778
|
+
checkConnection();
|
|
779
|
+
|
|
780
|
+
// Read JSON file
|
|
781
|
+
let tokensData;
|
|
782
|
+
try {
|
|
783
|
+
const content = readFileSync(file, 'utf8');
|
|
784
|
+
tokensData = JSON.parse(content);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
console.log(chalk.red(`✗ Could not read file: ${file}`));
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const spinner = ora('Importing tokens...').start();
|
|
791
|
+
|
|
792
|
+
// Detect format and convert
|
|
793
|
+
// Support: { "colors": { "primary": "#xxx" } } or { "primary": { "value": "#xxx", "type": "color" } }
|
|
794
|
+
const collectionName = options.collection || 'Imported Tokens';
|
|
795
|
+
|
|
796
|
+
const code = `
|
|
797
|
+
const data = ${JSON.stringify(tokensData)};
|
|
798
|
+
const collectionName = '${collectionName}';
|
|
799
|
+
|
|
800
|
+
function hexToRgb(hex) {
|
|
801
|
+
const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
|
|
802
|
+
if (!r) return null;
|
|
803
|
+
return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function detectType(value) {
|
|
807
|
+
if (typeof value === 'string' && value.startsWith('#')) return 'COLOR';
|
|
808
|
+
if (typeof value === 'number') return 'FLOAT';
|
|
809
|
+
if (typeof value === 'boolean') return 'BOOLEAN';
|
|
810
|
+
return 'STRING';
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function flattenTokens(obj, prefix = '') {
|
|
814
|
+
const result = [];
|
|
815
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
816
|
+
const name = prefix ? prefix + '/' + key : key;
|
|
817
|
+
if (val && typeof val === 'object' && !val.value && !val.type) {
|
|
818
|
+
result.push(...flattenTokens(val, name));
|
|
819
|
+
} else {
|
|
820
|
+
const value = val?.value ?? val;
|
|
821
|
+
const type = val?.type?.toUpperCase() || detectType(value);
|
|
822
|
+
result.push({ name, value, type });
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === collectionName);
|
|
829
|
+
if (!col) col = figma.variables.createVariableCollection(collectionName);
|
|
830
|
+
const modeId = col.modes[0].modeId;
|
|
831
|
+
|
|
832
|
+
const tokens = flattenTokens(data);
|
|
833
|
+
let count = 0;
|
|
834
|
+
|
|
835
|
+
tokens.forEach(({ name, value, type }) => {
|
|
836
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === name);
|
|
837
|
+
if (!existing) {
|
|
838
|
+
try {
|
|
839
|
+
const figmaType = type === 'COLOR' ? 'COLOR' : type === 'FLOAT' || type === 'NUMBER' ? 'FLOAT' : type === 'BOOLEAN' ? 'BOOLEAN' : 'STRING';
|
|
840
|
+
const v = figma.variables.createVariable(name, col.id, figmaType);
|
|
841
|
+
let figmaValue = value;
|
|
842
|
+
if (figmaType === 'COLOR') figmaValue = hexToRgb(value);
|
|
843
|
+
if (figmaValue !== null) {
|
|
844
|
+
v.setValueForMode(modeId, figmaValue);
|
|
845
|
+
count++;
|
|
846
|
+
}
|
|
847
|
+
} catch (e) {}
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
'Imported ' + count + ' tokens into ' + collectionName
|
|
852
|
+
`;
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
856
|
+
spinner.succeed(result?.trim() || 'Tokens imported');
|
|
857
|
+
} catch (error) {
|
|
858
|
+
spinner.fail('Failed to import tokens');
|
|
859
|
+
console.error(error.message);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
tokens
|
|
864
|
+
.command('ds')
|
|
865
|
+
.description('Create IDS Base Design System (complete starter kit)')
|
|
866
|
+
.action(async () => {
|
|
867
|
+
checkConnection();
|
|
868
|
+
|
|
869
|
+
console.log(chalk.cyan('\n IDS Base Design System'));
|
|
870
|
+
console.log(chalk.gray(' by Into Design Systems\n'));
|
|
871
|
+
|
|
872
|
+
// IDS Base values
|
|
873
|
+
const idsColors = {
|
|
874
|
+
gray: { 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' },
|
|
875
|
+
primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' },
|
|
876
|
+
accent: { 50: '#fdf4ff', 100: '#fae8ff', 200: '#f5d0fe', 300: '#f0abfc', 400: '#e879f9', 500: '#d946ef', 600: '#c026d3', 700: '#a21caf', 800: '#86198f', 900: '#701a75', 950: '#4a044e' }
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
const idsSemanticColors = {
|
|
880
|
+
'background/default': '#ffffff',
|
|
881
|
+
'background/muted': '#f4f4f5',
|
|
882
|
+
'background/emphasis': '#18181b',
|
|
883
|
+
'foreground/default': '#18181b',
|
|
884
|
+
'foreground/muted': '#71717a',
|
|
885
|
+
'foreground/emphasis': '#ffffff',
|
|
886
|
+
'border/default': '#e4e4e7',
|
|
887
|
+
'border/focus': '#3b82f6',
|
|
888
|
+
'action/primary': '#3b82f6',
|
|
889
|
+
'action/primary-hover': '#2563eb',
|
|
890
|
+
'feedback/success': '#22c55e',
|
|
891
|
+
'feedback/success-muted': '#dcfce7',
|
|
892
|
+
'feedback/warning': '#f59e0b',
|
|
893
|
+
'feedback/warning-muted': '#fef3c7',
|
|
894
|
+
'feedback/error': '#ef4444',
|
|
895
|
+
'feedback/error-muted': '#fee2e2'
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const idsSpacing = {
|
|
899
|
+
'xs': 4, 'sm': 8, 'md': 16, 'lg': 24, 'xl': 32, '2xl': 48, '3xl': 64
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const idsTypography = {
|
|
903
|
+
'size/xs': 12, 'size/sm': 14, 'size/base': 16, 'size/lg': 18,
|
|
904
|
+
'size/xl': 20, 'size/2xl': 24, 'size/3xl': 30, 'size/4xl': 36,
|
|
905
|
+
'weight/normal': 400, 'weight/medium': 500, 'weight/semibold': 600, 'weight/bold': 700
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
const idsRadii = {
|
|
909
|
+
'none': 0, 'sm': 4, 'md': 8, 'lg': 12, 'xl': 16, 'full': 9999
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// Create Color - Primitives
|
|
913
|
+
let spinner = ora('Creating Color - Primitives...').start();
|
|
914
|
+
const primitivesCode = `
|
|
915
|
+
const colors = ${JSON.stringify(idsColors)};
|
|
916
|
+
function hexToRgb(hex) {
|
|
917
|
+
const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
|
|
918
|
+
return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
|
|
919
|
+
}
|
|
920
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Color - Primitives');
|
|
921
|
+
if (!col) col = figma.variables.createVariableCollection('Color - Primitives');
|
|
922
|
+
const modeId = col.modes[0].modeId;
|
|
923
|
+
let count = 0;
|
|
924
|
+
Object.entries(colors).forEach(([colorName, shades]) => {
|
|
925
|
+
Object.entries(shades).forEach(([shade, hex]) => {
|
|
926
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === colorName + '/' + shade);
|
|
927
|
+
if (!existing) {
|
|
928
|
+
const v = figma.variables.createVariable(colorName + '/' + shade, col.id, 'COLOR');
|
|
929
|
+
v.setValueForMode(modeId, hexToRgb(hex));
|
|
930
|
+
count++;
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
count
|
|
935
|
+
`;
|
|
936
|
+
try {
|
|
937
|
+
const result = figmaUse(`eval "${primitivesCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
938
|
+
spinner.succeed(`Color - Primitives (${result?.trim() || '33'} variables)`);
|
|
939
|
+
} catch { spinner.fail('Color - Primitives failed'); }
|
|
940
|
+
|
|
941
|
+
// Create Color - Semantic
|
|
942
|
+
spinner = ora('Creating Color - Semantic...').start();
|
|
943
|
+
const semanticCode = `
|
|
944
|
+
const colors = ${JSON.stringify(idsSemanticColors)};
|
|
945
|
+
function hexToRgb(hex) {
|
|
946
|
+
const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
|
|
947
|
+
return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
|
|
948
|
+
}
|
|
949
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Color - Semantic');
|
|
950
|
+
if (!col) col = figma.variables.createVariableCollection('Color - Semantic');
|
|
951
|
+
const modeId = col.modes[0].modeId;
|
|
952
|
+
let count = 0;
|
|
953
|
+
Object.entries(colors).forEach(([name, hex]) => {
|
|
954
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === name);
|
|
955
|
+
if (!existing) {
|
|
956
|
+
const v = figma.variables.createVariable(name, col.id, 'COLOR');
|
|
957
|
+
v.setValueForMode(modeId, hexToRgb(hex));
|
|
958
|
+
count++;
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
count
|
|
962
|
+
`;
|
|
963
|
+
try {
|
|
964
|
+
const result = figmaUse(`eval "${semanticCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
965
|
+
spinner.succeed(`Color - Semantic (${result?.trim() || '13'} variables)`);
|
|
966
|
+
} catch { spinner.fail('Color - Semantic failed'); }
|
|
967
|
+
|
|
968
|
+
// Create Spacing
|
|
969
|
+
spinner = ora('Creating Spacing...').start();
|
|
970
|
+
const spacingCode = `
|
|
971
|
+
const spacings = ${JSON.stringify(idsSpacing)};
|
|
972
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Spacing');
|
|
973
|
+
if (!col) col = figma.variables.createVariableCollection('Spacing');
|
|
974
|
+
const modeId = col.modes[0].modeId;
|
|
975
|
+
let count = 0;
|
|
976
|
+
Object.entries(spacings).forEach(([name, value]) => {
|
|
977
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === name);
|
|
978
|
+
if (!existing) {
|
|
979
|
+
const v = figma.variables.createVariable(name, col.id, 'FLOAT');
|
|
980
|
+
v.setValueForMode(modeId, value);
|
|
981
|
+
count++;
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
count
|
|
985
|
+
`;
|
|
986
|
+
try {
|
|
987
|
+
const result = figmaUse(`eval "${spacingCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
988
|
+
spinner.succeed(`Spacing (${result?.trim() || '7'} variables)`);
|
|
989
|
+
} catch { spinner.fail('Spacing failed'); }
|
|
990
|
+
|
|
991
|
+
// Create Typography
|
|
992
|
+
spinner = ora('Creating Typography...').start();
|
|
993
|
+
const typographyCode = `
|
|
994
|
+
const typography = ${JSON.stringify(idsTypography)};
|
|
995
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Typography');
|
|
996
|
+
if (!col) col = figma.variables.createVariableCollection('Typography');
|
|
997
|
+
const modeId = col.modes[0].modeId;
|
|
998
|
+
let count = 0;
|
|
999
|
+
Object.entries(typography).forEach(([name, value]) => {
|
|
1000
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === name);
|
|
1001
|
+
if (!existing) {
|
|
1002
|
+
const v = figma.variables.createVariable(name, col.id, 'FLOAT');
|
|
1003
|
+
v.setValueForMode(modeId, value);
|
|
1004
|
+
count++;
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
count
|
|
1008
|
+
`;
|
|
1009
|
+
try {
|
|
1010
|
+
const result = figmaUse(`eval "${typographyCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
1011
|
+
spinner.succeed(`Typography (${result?.trim() || '12'} variables)`);
|
|
1012
|
+
} catch { spinner.fail('Typography failed'); }
|
|
1013
|
+
|
|
1014
|
+
// Create Border Radii
|
|
1015
|
+
spinner = ora('Creating Border Radii...').start();
|
|
1016
|
+
const radiiCode = `
|
|
1017
|
+
const radii = ${JSON.stringify(idsRadii)};
|
|
1018
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Border Radii');
|
|
1019
|
+
if (!col) col = figma.variables.createVariableCollection('Border Radii');
|
|
1020
|
+
const modeId = col.modes[0].modeId;
|
|
1021
|
+
let count = 0;
|
|
1022
|
+
Object.entries(radii).forEach(([name, value]) => {
|
|
1023
|
+
const existing = figma.variables.getLocalVariables().find(v => v.name === name);
|
|
1024
|
+
if (!existing) {
|
|
1025
|
+
const v = figma.variables.createVariable(name, col.id, 'FLOAT');
|
|
1026
|
+
v.setValueForMode(modeId, value);
|
|
1027
|
+
count++;
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
count
|
|
1031
|
+
`;
|
|
1032
|
+
try {
|
|
1033
|
+
const result = figmaUse(`eval "${radiiCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
1034
|
+
spinner.succeed(`Border Radii (${result?.trim() || '6'} variables)`);
|
|
1035
|
+
} catch { spinner.fail('Border Radii failed'); }
|
|
1036
|
+
|
|
1037
|
+
// Small delay to let spinner render
|
|
1038
|
+
await new Promise(r => setTimeout(r, 100));
|
|
1039
|
+
|
|
1040
|
+
// Summary
|
|
1041
|
+
console.log(chalk.green('\n ✓ IDS Base Design System created!\n'));
|
|
1042
|
+
console.log(chalk.white(' Collections:'));
|
|
1043
|
+
console.log(chalk.gray(' • Color - Primitives (gray, primary, accent)'));
|
|
1044
|
+
console.log(chalk.gray(' • Color - Semantic (background, foreground, border, action, feedback)'));
|
|
1045
|
+
console.log(chalk.gray(' • Spacing (xs to 3xl, 4px base)'));
|
|
1046
|
+
console.log(chalk.gray(' • Typography (sizes + weights)'));
|
|
1047
|
+
console.log(chalk.gray(' • Border Radii (none to full)'));
|
|
1048
|
+
console.log();
|
|
1049
|
+
console.log(chalk.gray(' Total: ~74 variables across 5 collections\n'));
|
|
1050
|
+
console.log(chalk.gray(' Next: ') + chalk.cyan('design-lazyyy-cli tokens components') + chalk.gray(' to add UI components\n'));
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
tokens
|
|
1054
|
+
.command('components')
|
|
1055
|
+
.description('Create IDS Base Components (Button, Input, Card, Badge)')
|
|
1056
|
+
.action(async () => {
|
|
1057
|
+
checkConnection();
|
|
1058
|
+
|
|
1059
|
+
console.log(chalk.cyan('\n IDS Base Components'));
|
|
1060
|
+
console.log(chalk.gray(' by Into Design Systems\n'));
|
|
1061
|
+
|
|
1062
|
+
// Component colors (using IDS Base values)
|
|
1063
|
+
const colors = {
|
|
1064
|
+
primary500: '#3b82f6',
|
|
1065
|
+
primary600: '#2563eb',
|
|
1066
|
+
gray100: '#f4f4f5',
|
|
1067
|
+
gray200: '#e4e4e7',
|
|
1068
|
+
gray500: '#71717a',
|
|
1069
|
+
gray900: '#18181b',
|
|
1070
|
+
white: '#ffffff',
|
|
1071
|
+
success: '#22c55e',
|
|
1072
|
+
warning: '#f59e0b',
|
|
1073
|
+
error: '#ef4444'
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// First, clean up any existing IDS components
|
|
1077
|
+
let spinner = ora('Cleaning up existing components...').start();
|
|
1078
|
+
const cleanupCode = `
|
|
1079
|
+
const names = ['Button / Primary', 'Button / Secondary', 'Button / Outline', 'Input', 'Card', 'Badge / Default', 'Badge / Success', 'Badge / Warning', 'Badge / Error'];
|
|
1080
|
+
let removed = 0;
|
|
1081
|
+
figma.currentPage.children.forEach(n => {
|
|
1082
|
+
if (names.includes(n.name)) { n.remove(); removed++; }
|
|
1083
|
+
});
|
|
1084
|
+
removed
|
|
1085
|
+
`;
|
|
1086
|
+
try {
|
|
1087
|
+
const removed = figmaUse(`eval "${cleanupCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
1088
|
+
spinner.succeed(`Cleaned up ${removed?.trim() || '0'} old elements`);
|
|
1089
|
+
} catch { spinner.succeed('Ready'); }
|
|
1090
|
+
|
|
1091
|
+
// Step 1: Create frames using JSX render (handles fonts)
|
|
1092
|
+
spinner = ora('Creating frames...').start();
|
|
1093
|
+
const jsxComponents = [
|
|
1094
|
+
{ jsx: `<Frame name="Button / Primary" bg="${colors.primary500}" px={16} py={10} rounded={8} flex="row"><Text size={14} weight="semibold" color="#ffffff">Button</Text></Frame>` },
|
|
1095
|
+
{ jsx: `<Frame name="Button / Secondary" bg="${colors.gray100}" px={16} py={10} rounded={8} flex="row"><Text size={14} weight="semibold" color="${colors.gray900}">Button</Text></Frame>` },
|
|
1096
|
+
{ jsx: `<Frame name="Button / Outline" bg="#ffffff" stroke="${colors.gray200}" px={16} py={10} rounded={8} flex="row"><Text size={14} weight="semibold" color="${colors.gray900}">Button</Text></Frame>` },
|
|
1097
|
+
{ jsx: `<Frame name="Input" w={200} bg="#ffffff" stroke="${colors.gray200}" px={12} py={10} rounded={8} flex="row"><Text size={14} color="${colors.gray500}">Placeholder</Text></Frame>` },
|
|
1098
|
+
{ jsx: `<Frame name="Card" bg="#ffffff" stroke="${colors.gray200}" p={24} rounded={12} flex="col" gap={8}><Text size={18} weight="semibold" color="${colors.gray900}">Card Title</Text><Text size={14} color="${colors.gray500}">Card description goes here.</Text></Frame>` },
|
|
1099
|
+
{ jsx: `<Frame name="Badge / Default" bg="${colors.gray100}" px={10} py={4} rounded={9999} flex="row"><Text size={12} weight="medium" color="${colors.gray900}">Badge</Text></Frame>` },
|
|
1100
|
+
{ jsx: `<Frame name="Badge / Success" bg="#dcfce7" px={10} py={4} rounded={9999} flex="row"><Text size={12} weight="medium" color="#166534">Success</Text></Frame>` },
|
|
1101
|
+
{ jsx: `<Frame name="Badge / Warning" bg="#fef3c7" px={10} py={4} rounded={9999} flex="row"><Text size={12} weight="medium" color="#92400e">Warning</Text></Frame>` },
|
|
1102
|
+
{ jsx: `<Frame name="Badge / Error" bg="#fee2e2" px={10} py={4} rounded={9999} flex="row"><Text size={12} weight="medium" color="#991b1b">Error</Text></Frame>` }
|
|
1103
|
+
];
|
|
1104
|
+
|
|
1105
|
+
try {
|
|
1106
|
+
for (const { jsx } of jsxComponents) {
|
|
1107
|
+
execSync(`echo '${jsx}' | figma-use render --stdin`, { stdio: 'pipe' });
|
|
1108
|
+
}
|
|
1109
|
+
spinner.succeed('9 frames created');
|
|
1110
|
+
} catch (e) { spinner.fail('Frame creation failed'); }
|
|
1111
|
+
|
|
1112
|
+
// Step 2: Convert to components one by one with positioning
|
|
1113
|
+
spinner = ora('Converting to components...').start();
|
|
1114
|
+
|
|
1115
|
+
const componentOrder = [
|
|
1116
|
+
{ name: 'Button / Primary', row: 0, width: 80, varFill: 'action/primary' },
|
|
1117
|
+
{ name: 'Button / Secondary', row: 0, width: 80, varFill: 'background/muted' },
|
|
1118
|
+
{ name: 'Button / Outline', row: 0, width: 80, varFill: 'background/default', varStroke: 'border/default' },
|
|
1119
|
+
{ name: 'Input', row: 0, width: 200, varFill: 'background/default', varStroke: 'border/default' },
|
|
1120
|
+
{ name: 'Card', row: 0, width: 240, varFill: 'background/default', varStroke: 'border/default' },
|
|
1121
|
+
{ name: 'Badge / Default', row: 1, width: 60, varFill: 'background/muted' },
|
|
1122
|
+
{ name: 'Badge / Success', row: 1, width: 70, varFill: 'feedback/success-muted' },
|
|
1123
|
+
{ name: 'Badge / Warning', row: 1, width: 70, varFill: 'feedback/warning-muted' },
|
|
1124
|
+
{ name: 'Badge / Error', row: 1, width: 50, varFill: 'feedback/error-muted' }
|
|
1125
|
+
];
|
|
1126
|
+
|
|
1127
|
+
let row0X = 0, row1X = 0;
|
|
1128
|
+
const gap = 32;
|
|
1129
|
+
|
|
1130
|
+
for (const comp of componentOrder) {
|
|
1131
|
+
const convertSingle = `
|
|
1132
|
+
const f = figma.currentPage.children.find(n => n.name === '${comp.name}' && n.type === 'FRAME');
|
|
1133
|
+
if (f) {
|
|
1134
|
+
const vars = figma.variables.getLocalVariables();
|
|
1135
|
+
const findVar = (name) => vars.find(v => v.name === name);
|
|
1136
|
+
${comp.varFill ? `
|
|
1137
|
+
const vFill = findVar('${comp.varFill}');
|
|
1138
|
+
if (vFill && f.fills && f.fills.length > 0) {
|
|
1139
|
+
const fills = JSON.parse(JSON.stringify(f.fills));
|
|
1140
|
+
fills[0] = figma.variables.setBoundVariableForPaint(fills[0], 'color', vFill);
|
|
1141
|
+
f.fills = fills;
|
|
1142
|
+
}` : ''}
|
|
1143
|
+
${comp.varStroke ? `
|
|
1144
|
+
const vStroke = findVar('${comp.varStroke}');
|
|
1145
|
+
if (vStroke && f.strokes && f.strokes.length > 0) {
|
|
1146
|
+
const strokes = JSON.parse(JSON.stringify(f.strokes));
|
|
1147
|
+
strokes[0] = figma.variables.setBoundVariableForPaint(strokes[0], 'color', vStroke);
|
|
1148
|
+
f.strokes = strokes;
|
|
1149
|
+
}` : ''}
|
|
1150
|
+
const c = figma.createComponentFromNode(f);
|
|
1151
|
+
c.x = ${comp.row === 0 ? row0X : row1X};
|
|
1152
|
+
c.y = ${comp.row === 0 ? 0 : 80};
|
|
1153
|
+
}
|
|
1154
|
+
`;
|
|
1155
|
+
try {
|
|
1156
|
+
figmaUse(`eval "${convertSingle.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
1157
|
+
if (comp.row === 0) row0X += comp.width + gap;
|
|
1158
|
+
else row1X += comp.width + 24;
|
|
1159
|
+
} catch {}
|
|
1160
|
+
}
|
|
1161
|
+
spinner.succeed('9 components with variables');
|
|
1162
|
+
|
|
1163
|
+
await new Promise(r => setTimeout(r, 100));
|
|
1164
|
+
|
|
1165
|
+
console.log(chalk.green('\n ✓ IDS Base Components created!\n'));
|
|
1166
|
+
console.log(chalk.white(' Components:'));
|
|
1167
|
+
console.log(chalk.gray(' • Button (Primary, Secondary, Outline)'));
|
|
1168
|
+
console.log(chalk.gray(' • Input'));
|
|
1169
|
+
console.log(chalk.gray(' • Card'));
|
|
1170
|
+
console.log(chalk.gray(' • Badge (Default, Success, Warning, Error)'));
|
|
1171
|
+
console.log();
|
|
1172
|
+
console.log(chalk.gray(' Total: 9 components on canvas\n'));
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
tokens
|
|
1176
|
+
.command('add <name> <value>')
|
|
1177
|
+
.description('Add a single token')
|
|
1178
|
+
.option('-c, --collection <name>', 'Collection name', 'Tokens')
|
|
1179
|
+
.option('-t, --type <type>', 'Type: COLOR, FLOAT, STRING, BOOLEAN (auto-detected if not set)')
|
|
1180
|
+
.action((name, value, options) => {
|
|
1181
|
+
checkConnection();
|
|
1182
|
+
|
|
1183
|
+
const code = `
|
|
1184
|
+
function hexToRgb(hex) {
|
|
1185
|
+
const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
|
|
1186
|
+
if (!r) return null;
|
|
1187
|
+
return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const value = '${value}';
|
|
1191
|
+
let type = '${options.type || ''}';
|
|
1192
|
+
if (!type) {
|
|
1193
|
+
if (value.startsWith('#')) type = 'COLOR';
|
|
1194
|
+
else if (!isNaN(parseFloat(value))) type = 'FLOAT';
|
|
1195
|
+
else if (value === 'true' || value === 'false') type = 'BOOLEAN';
|
|
1196
|
+
else type = 'STRING';
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
|
|
1200
|
+
if (!col) col = figma.variables.createVariableCollection('${options.collection}');
|
|
1201
|
+
const modeId = col.modes[0].modeId;
|
|
1202
|
+
|
|
1203
|
+
const v = figma.variables.createVariable('${name}', col.id, type);
|
|
1204
|
+
let figmaValue = value;
|
|
1205
|
+
if (type === 'COLOR') figmaValue = hexToRgb(value);
|
|
1206
|
+
else if (type === 'FLOAT') figmaValue = parseFloat(value);
|
|
1207
|
+
else if (type === 'BOOLEAN') figmaValue = value === 'true';
|
|
1208
|
+
v.setValueForMode(modeId, figmaValue);
|
|
1209
|
+
|
|
1210
|
+
'Created ' + type.toLowerCase() + ' token: ${name}'
|
|
1211
|
+
`;
|
|
1212
|
+
|
|
1213
|
+
try {
|
|
1214
|
+
const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
1215
|
+
console.log(chalk.green(result?.trim() || `✓ Created token: ${name}`));
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
console.log(chalk.red(`✗ Failed to create token: ${name}`));
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// ============ CREATE ============
|
|
1222
|
+
|
|
1223
|
+
const create = program
|
|
1224
|
+
.command('create')
|
|
1225
|
+
.description('Create Figma elements');
|
|
1226
|
+
|
|
1227
|
+
create
|
|
1228
|
+
.command('frame <name>')
|
|
1229
|
+
.description('Create a frame')
|
|
1230
|
+
.option('-w, --width <n>', 'Width', '100')
|
|
1231
|
+
.option('-h, --height <n>', 'Height', '100')
|
|
1232
|
+
.option('-x <n>', 'X position')
|
|
1233
|
+
.option('-y <n>', 'Y position', '0')
|
|
1234
|
+
.option('--fill <color>', 'Fill color')
|
|
1235
|
+
.option('--radius <n>', 'Corner radius')
|
|
1236
|
+
.option('--smart', 'Auto-position to avoid overlaps (default if no -x)')
|
|
1237
|
+
.option('-g, --gap <n>', 'Gap for smart positioning', '100')
|
|
1238
|
+
.action((name, options) => {
|
|
1239
|
+
checkConnection();
|
|
1240
|
+
// Smart positioning: if no X specified, auto-position
|
|
1241
|
+
const useSmartPos = options.smart || options.x === undefined;
|
|
1242
|
+
if (useSmartPos) {
|
|
1243
|
+
const { r, g, b } = options.fill ? hexToRgb(options.fill) : { r: 1, g: 1, b: 1 };
|
|
1244
|
+
let code = `
|
|
1245
|
+
${smartPosCode(options.gap)}
|
|
1246
|
+
const frame = figma.createFrame();
|
|
1247
|
+
frame.name = '${name}';
|
|
1248
|
+
frame.x = smartX;
|
|
1249
|
+
frame.y = ${options.y};
|
|
1250
|
+
frame.resize(${options.width}, ${options.height});
|
|
1251
|
+
${options.fill ? `frame.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];` : ''}
|
|
1252
|
+
${options.radius ? `frame.cornerRadius = ${options.radius};` : ''}
|
|
1253
|
+
figma.currentPage.selection = [frame];
|
|
1254
|
+
'${name} created at (' + smartX + ', ${options.y})'
|
|
1255
|
+
`;
|
|
1256
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1257
|
+
} else {
|
|
1258
|
+
let cmd = `create frame --name "${name}" --x ${options.x} --y ${options.y} --width ${options.width} --height ${options.height}`;
|
|
1259
|
+
if (options.fill) cmd += ` --fill "${options.fill}"`;
|
|
1260
|
+
if (options.radius) cmd += ` --radius ${options.radius}`;
|
|
1261
|
+
figmaUse(cmd);
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
create
|
|
1266
|
+
.command('icon <name>')
|
|
1267
|
+
.description('Create an icon from Iconify (e.g., lucide:star, mdi:home) - auto-positions')
|
|
1268
|
+
.option('-s, --size <n>', 'Size', '24')
|
|
1269
|
+
.option('-c, --color <color>', 'Color', '#000000')
|
|
1270
|
+
.option('-x <n>', 'X position (auto if not set)')
|
|
1271
|
+
.option('-y <n>', 'Y position', '0')
|
|
1272
|
+
.option('--spacing <n>', 'Gap from other elements', '100')
|
|
1273
|
+
.action((name, options) => {
|
|
1274
|
+
checkConnection();
|
|
1275
|
+
const useSmartPos = options.x === undefined;
|
|
1276
|
+
if (useSmartPos) {
|
|
1277
|
+
// First create icon at 0,0, then move it to smart position
|
|
1278
|
+
const code = `
|
|
1279
|
+
${smartPosCode(options.spacing)}
|
|
1280
|
+
const icon = figma.currentPage.selection[0];
|
|
1281
|
+
if (icon) {
|
|
1282
|
+
icon.x = smartX;
|
|
1283
|
+
icon.y = ${options.y};
|
|
1284
|
+
'Icon moved to (' + smartX + ', ${options.y})';
|
|
1285
|
+
} else {
|
|
1286
|
+
'No icon selected';
|
|
1287
|
+
}
|
|
1288
|
+
`;
|
|
1289
|
+
figmaUse(`create icon ${name} --size ${options.size} --color "${options.color}"`);
|
|
1290
|
+
// Move to smart position after creation
|
|
1291
|
+
setTimeout(() => {
|
|
1292
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
1293
|
+
}, 500);
|
|
1294
|
+
} else {
|
|
1295
|
+
figmaUse(`create icon ${name} --size ${options.size} --color "${options.color}" -x ${options.x} -y ${options.y}`);
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
create
|
|
1300
|
+
.command('rect [name]')
|
|
1301
|
+
.alias('rectangle')
|
|
1302
|
+
.description('Create a rectangle (auto-positions to avoid overlap)')
|
|
1303
|
+
.option('-w, --width <n>', 'Width', '100')
|
|
1304
|
+
.option('-h, --height <n>', 'Height', '100')
|
|
1305
|
+
.option('-x <n>', 'X position (auto if not set)')
|
|
1306
|
+
.option('-y <n>', 'Y position', '0')
|
|
1307
|
+
.option('--fill <color>', 'Fill color', '#D9D9D9')
|
|
1308
|
+
.option('--stroke <color>', 'Stroke color')
|
|
1309
|
+
.option('--radius <n>', 'Corner radius')
|
|
1310
|
+
.option('--opacity <n>', 'Opacity 0-1')
|
|
1311
|
+
.action((name, options) => {
|
|
1312
|
+
checkConnection();
|
|
1313
|
+
const rectName = name || 'Rectangle';
|
|
1314
|
+
const { r, g, b } = hexToRgb(options.fill);
|
|
1315
|
+
const useSmartPos = options.x === undefined;
|
|
1316
|
+
let code = `
|
|
1317
|
+
${useSmartPos ? smartPosCode(100) : `const smartX = ${options.x};`}
|
|
1318
|
+
const rect = figma.createRectangle();
|
|
1319
|
+
rect.name = '${rectName}';
|
|
1320
|
+
rect.x = smartX;
|
|
1321
|
+
rect.y = ${options.y};
|
|
1322
|
+
rect.resize(${options.width}, ${options.height});
|
|
1323
|
+
rect.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];
|
|
1324
|
+
${options.radius ? `rect.cornerRadius = ${options.radius};` : ''}
|
|
1325
|
+
${options.opacity ? `rect.opacity = ${options.opacity};` : ''}
|
|
1326
|
+
${options.stroke ? `rect.strokes = [{ type: 'SOLID', color: { r: ${hexToRgb(options.stroke).r}, g: ${hexToRgb(options.stroke).g}, b: ${hexToRgb(options.stroke).b} } }]; rect.strokeWeight = 1;` : ''}
|
|
1327
|
+
figma.currentPage.selection = [rect];
|
|
1328
|
+
'${rectName} created at (' + smartX + ', ${options.y})'
|
|
1329
|
+
`;
|
|
1330
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
create
|
|
1334
|
+
.command('ellipse [name]')
|
|
1335
|
+
.alias('circle')
|
|
1336
|
+
.description('Create an ellipse/circle (auto-positions to avoid overlap)')
|
|
1337
|
+
.option('-w, --width <n>', 'Width (diameter)', '100')
|
|
1338
|
+
.option('-h, --height <n>', 'Height (same as width for circle)')
|
|
1339
|
+
.option('-x <n>', 'X position (auto if not set)')
|
|
1340
|
+
.option('-y <n>', 'Y position', '0')
|
|
1341
|
+
.option('--fill <color>', 'Fill color', '#D9D9D9')
|
|
1342
|
+
.option('--stroke <color>', 'Stroke color')
|
|
1343
|
+
.action((name, options) => {
|
|
1344
|
+
checkConnection();
|
|
1345
|
+
const ellipseName = name || 'Ellipse';
|
|
1346
|
+
const height = options.height || options.width;
|
|
1347
|
+
const { r, g, b } = hexToRgb(options.fill);
|
|
1348
|
+
const useSmartPos = options.x === undefined;
|
|
1349
|
+
let code = `
|
|
1350
|
+
${useSmartPos ? smartPosCode(100) : `const smartX = ${options.x};`}
|
|
1351
|
+
const ellipse = figma.createEllipse();
|
|
1352
|
+
ellipse.name = '${ellipseName}';
|
|
1353
|
+
ellipse.x = smartX;
|
|
1354
|
+
ellipse.y = ${options.y};
|
|
1355
|
+
ellipse.resize(${options.width}, ${height});
|
|
1356
|
+
ellipse.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];
|
|
1357
|
+
${options.stroke ? `ellipse.strokes = [{ type: 'SOLID', color: { r: ${hexToRgb(options.stroke).r}, g: ${hexToRgb(options.stroke).g}, b: ${hexToRgb(options.stroke).b} } }]; ellipse.strokeWeight = 1;` : ''}
|
|
1358
|
+
figma.currentPage.selection = [ellipse];
|
|
1359
|
+
'${ellipseName} created at (' + smartX + ', ${options.y})'
|
|
1360
|
+
`;
|
|
1361
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
create
|
|
1365
|
+
.command('text <content>')
|
|
1366
|
+
.description('Create a text layer (smart positions by default)')
|
|
1367
|
+
.option('-x <n>', 'X position (auto if not set)')
|
|
1368
|
+
.option('-y <n>', 'Y position', '0')
|
|
1369
|
+
.option('-s, --size <n>', 'Font size', '16')
|
|
1370
|
+
.option('-c, --color <color>', 'Text color', '#000000')
|
|
1371
|
+
.option('-w, --weight <weight>', 'Font weight: regular, medium, semibold, bold', 'regular')
|
|
1372
|
+
.option('--font <family>', 'Font family', 'Inter')
|
|
1373
|
+
.option('--width <n>', 'Text box width (auto-width if not set)')
|
|
1374
|
+
.option('--spacing <n>', 'Gap from other elements', '100')
|
|
1375
|
+
.action((content, options) => {
|
|
1376
|
+
checkConnection();
|
|
1377
|
+
const { r, g, b } = hexToRgb(options.color);
|
|
1378
|
+
const weightMap = { regular: 'Regular', medium: 'Medium', semibold: 'Semi Bold', bold: 'Bold' };
|
|
1379
|
+
const fontStyle = weightMap[options.weight.toLowerCase()] || 'Regular';
|
|
1380
|
+
const useSmartPos = options.x === undefined;
|
|
1381
|
+
let code = `
|
|
1382
|
+
(async function() {
|
|
1383
|
+
${useSmartPos ? smartPosCode(options.spacing) : `const smartX = ${options.x};`}
|
|
1384
|
+
await figma.loadFontAsync({ family: '${options.font}', style: '${fontStyle}' });
|
|
1385
|
+
const text = figma.createText();
|
|
1386
|
+
text.fontName = { family: '${options.font}', style: '${fontStyle}' };
|
|
1387
|
+
text.characters = '${content.replace(/'/g, "\\'")}';
|
|
1388
|
+
text.fontSize = ${options.size};
|
|
1389
|
+
text.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];
|
|
1390
|
+
text.x = smartX;
|
|
1391
|
+
text.y = ${options.y};
|
|
1392
|
+
${options.width ? `text.resize(${options.width}, text.height); text.textAutoResize = 'HEIGHT';` : ''}
|
|
1393
|
+
figma.currentPage.selection = [text];
|
|
1394
|
+
return 'Text created at (' + smartX + ', ${options.y})';
|
|
1395
|
+
})()
|
|
1396
|
+
`;
|
|
1397
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
create
|
|
1401
|
+
.command('line')
|
|
1402
|
+
.description('Create a line (smart positions by default)')
|
|
1403
|
+
.option('--x1 <n>', 'Start X (auto if not set)')
|
|
1404
|
+
.option('--y1 <n>', 'Start Y', '0')
|
|
1405
|
+
.option('--x2 <n>', 'End X (auto + length if x1 not set)')
|
|
1406
|
+
.option('--y2 <n>', 'End Y', '0')
|
|
1407
|
+
.option('-l, --length <n>', 'Line length', '100')
|
|
1408
|
+
.option('-c, --color <color>', 'Line color', '#000000')
|
|
1409
|
+
.option('-w, --weight <n>', 'Stroke weight', '1')
|
|
1410
|
+
.option('--spacing <n>', 'Gap from other elements', '100')
|
|
1411
|
+
.action((options) => {
|
|
1412
|
+
checkConnection();
|
|
1413
|
+
const { r, g, b } = hexToRgb(options.color);
|
|
1414
|
+
const useSmartPos = options.x1 === undefined;
|
|
1415
|
+
const lineLength = parseFloat(options.length);
|
|
1416
|
+
let code = `
|
|
1417
|
+
${useSmartPos ? smartPosCode(options.spacing) : `const smartX = ${options.x1};`}
|
|
1418
|
+
const line = figma.createLine();
|
|
1419
|
+
line.x = smartX;
|
|
1420
|
+
line.y = ${options.y1};
|
|
1421
|
+
line.resize(${useSmartPos ? lineLength : `Math.abs(${options.x2 || options.x1 + '+' + lineLength} - ${options.x1}) || ${lineLength}`}, 0);
|
|
1422
|
+
${options.x2 && options.x1 ? `line.rotation = Math.atan2(${options.y2} - ${options.y1}, ${options.x2} - ${options.x1}) * 180 / Math.PI;` : ''}
|
|
1423
|
+
line.strokes = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];
|
|
1424
|
+
line.strokeWeight = ${options.weight};
|
|
1425
|
+
figma.currentPage.selection = [line];
|
|
1426
|
+
'Line created at (' + smartX + ', ${options.y1}) with length ${lineLength}'
|
|
1427
|
+
`;
|
|
1428
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
create
|
|
1432
|
+
.command('component [name]')
|
|
1433
|
+
.description('Convert selection to component')
|
|
1434
|
+
.action((name) => {
|
|
1435
|
+
checkConnection();
|
|
1436
|
+
const compName = name || 'Component';
|
|
1437
|
+
let code = `
|
|
1438
|
+
const sel = figma.currentPage.selection;
|
|
1439
|
+
if (sel.length === 0) 'No selection';
|
|
1440
|
+
else if (sel.length === 1) {
|
|
1441
|
+
const comp = figma.createComponentFromNode(sel[0]);
|
|
1442
|
+
comp.name = '${compName}';
|
|
1443
|
+
figma.currentPage.selection = [comp];
|
|
1444
|
+
'Component created: ' + comp.name;
|
|
1445
|
+
} else {
|
|
1446
|
+
const group = figma.group(sel, figma.currentPage);
|
|
1447
|
+
const comp = figma.createComponentFromNode(group);
|
|
1448
|
+
comp.name = '${compName}';
|
|
1449
|
+
figma.currentPage.selection = [comp];
|
|
1450
|
+
'Component created from ' + sel.length + ' elements: ' + comp.name;
|
|
1451
|
+
}
|
|
1452
|
+
`;
|
|
1453
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
create
|
|
1457
|
+
.command('group [name]')
|
|
1458
|
+
.description('Group current selection')
|
|
1459
|
+
.action((name) => {
|
|
1460
|
+
checkConnection();
|
|
1461
|
+
const groupName = name || 'Group';
|
|
1462
|
+
let code = `
|
|
1463
|
+
const sel = figma.currentPage.selection;
|
|
1464
|
+
if (sel.length < 2) 'Select 2+ elements to group';
|
|
1465
|
+
else {
|
|
1466
|
+
const group = figma.group(sel, figma.currentPage);
|
|
1467
|
+
group.name = '${groupName}';
|
|
1468
|
+
figma.currentPage.selection = [group];
|
|
1469
|
+
'Grouped ' + sel.length + ' elements';
|
|
1470
|
+
}
|
|
1471
|
+
`;
|
|
1472
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
create
|
|
1476
|
+
.command('autolayout [name]')
|
|
1477
|
+
.alias('al')
|
|
1478
|
+
.description('Create an auto-layout frame (smart positions by default)')
|
|
1479
|
+
.option('-d, --direction <dir>', 'Direction: row, col', 'row')
|
|
1480
|
+
.option('-g, --gap <n>', 'Gap between items', '8')
|
|
1481
|
+
.option('-p, --padding <n>', 'Padding', '16')
|
|
1482
|
+
.option('-x <n>', 'X position (auto if not set)')
|
|
1483
|
+
.option('-y <n>', 'Y position', '0')
|
|
1484
|
+
.option('--fill <color>', 'Fill color')
|
|
1485
|
+
.option('--radius <n>', 'Corner radius')
|
|
1486
|
+
.option('--spacing <n>', 'Gap from other elements', '100')
|
|
1487
|
+
.action((name, options) => {
|
|
1488
|
+
checkConnection();
|
|
1489
|
+
const frameName = name || 'Auto Layout';
|
|
1490
|
+
const layoutMode = options.direction === 'col' ? 'VERTICAL' : 'HORIZONTAL';
|
|
1491
|
+
const useSmartPos = options.x === undefined;
|
|
1492
|
+
let code = `
|
|
1493
|
+
${useSmartPos ? smartPosCode(options.spacing) : `const smartX = ${options.x};`}
|
|
1494
|
+
const frame = figma.createFrame();
|
|
1495
|
+
frame.name = '${frameName}';
|
|
1496
|
+
frame.x = smartX;
|
|
1497
|
+
frame.y = ${options.y};
|
|
1498
|
+
frame.layoutMode = '${layoutMode}';
|
|
1499
|
+
frame.primaryAxisSizingMode = 'AUTO';
|
|
1500
|
+
frame.counterAxisSizingMode = 'AUTO';
|
|
1501
|
+
frame.itemSpacing = ${options.gap};
|
|
1502
|
+
frame.paddingTop = ${options.padding};
|
|
1503
|
+
frame.paddingRight = ${options.padding};
|
|
1504
|
+
frame.paddingBottom = ${options.padding};
|
|
1505
|
+
frame.paddingLeft = ${options.padding};
|
|
1506
|
+
${options.fill ? `frame.fills = [{ type: 'SOLID', color: { r: ${hexToRgb(options.fill).r}, g: ${hexToRgb(options.fill).g}, b: ${hexToRgb(options.fill).b} } }];` : 'frame.fills = [];'}
|
|
1507
|
+
${options.radius ? `frame.cornerRadius = ${options.radius};` : ''}
|
|
1508
|
+
figma.currentPage.selection = [frame];
|
|
1509
|
+
'Auto-layout frame created at (' + smartX + ', ${options.y})'
|
|
1510
|
+
`;
|
|
1511
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// ============ CANVAS ============
|
|
1515
|
+
|
|
1516
|
+
const canvas = program
|
|
1517
|
+
.command('canvas')
|
|
1518
|
+
.description('Canvas awareness and smart positioning');
|
|
1519
|
+
|
|
1520
|
+
canvas
|
|
1521
|
+
.command('info')
|
|
1522
|
+
.description('Show canvas info (bounds, element count, free space)')
|
|
1523
|
+
.action(() => {
|
|
1524
|
+
checkConnection();
|
|
1525
|
+
let code = `
|
|
1526
|
+
const children = figma.currentPage.children;
|
|
1527
|
+
if (children.length === 0) {
|
|
1528
|
+
JSON.stringify({ empty: true, message: 'Canvas is empty', nextX: 0, nextY: 0 });
|
|
1529
|
+
} else {
|
|
1530
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1531
|
+
children.forEach(n => {
|
|
1532
|
+
minX = Math.min(minX, n.x);
|
|
1533
|
+
minY = Math.min(minY, n.y);
|
|
1534
|
+
maxX = Math.max(maxX, n.x + n.width);
|
|
1535
|
+
maxY = Math.max(maxY, n.y + n.height);
|
|
1536
|
+
});
|
|
1537
|
+
JSON.stringify({
|
|
1538
|
+
elements: children.length,
|
|
1539
|
+
bounds: { x: Math.round(minX), y: Math.round(minY), width: Math.round(maxX - minX), height: Math.round(maxY - minY) },
|
|
1540
|
+
nextX: Math.round(maxX + 100),
|
|
1541
|
+
nextY: 0,
|
|
1542
|
+
frames: children.filter(n => n.type === 'FRAME').length,
|
|
1543
|
+
components: children.filter(n => n.type === 'COMPONENT').length
|
|
1544
|
+
}, null, 2);
|
|
1545
|
+
}
|
|
1546
|
+
`;
|
|
1547
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
canvas
|
|
1551
|
+
.command('next')
|
|
1552
|
+
.description('Get next free position on canvas (no overlap)')
|
|
1553
|
+
.option('-g, --gap <n>', 'Gap from existing elements', '100')
|
|
1554
|
+
.option('-d, --direction <dir>', 'Direction: right, below', 'right')
|
|
1555
|
+
.action((options) => {
|
|
1556
|
+
checkConnection();
|
|
1557
|
+
let code = `
|
|
1558
|
+
const children = figma.currentPage.children;
|
|
1559
|
+
const gap = ${options.gap};
|
|
1560
|
+
if (children.length === 0) {
|
|
1561
|
+
JSON.stringify({ x: 0, y: 0 });
|
|
1562
|
+
} else {
|
|
1563
|
+
${options.direction === 'below' ? `
|
|
1564
|
+
let maxY = -Infinity;
|
|
1565
|
+
children.forEach(n => { maxY = Math.max(maxY, n.y + n.height); });
|
|
1566
|
+
JSON.stringify({ x: 0, y: Math.round(maxY + gap) });
|
|
1567
|
+
` : `
|
|
1568
|
+
let maxX = -Infinity;
|
|
1569
|
+
children.forEach(n => { maxX = Math.max(maxX, n.x + n.width); });
|
|
1570
|
+
JSON.stringify({ x: Math.round(maxX + gap), y: 0 });
|
|
1571
|
+
`}
|
|
1572
|
+
}
|
|
1573
|
+
`;
|
|
1574
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
// ============ BIND (Variables) ============
|
|
1578
|
+
|
|
1579
|
+
const bind = program
|
|
1580
|
+
.command('bind')
|
|
1581
|
+
.description('Bind variables to node properties');
|
|
1582
|
+
|
|
1583
|
+
bind
|
|
1584
|
+
.command('fill <varName>')
|
|
1585
|
+
.description('Bind color variable to fill')
|
|
1586
|
+
.option('-n, --node <id>', 'Node ID (uses selection if not set)')
|
|
1587
|
+
.action((varName, options) => {
|
|
1588
|
+
checkConnection();
|
|
1589
|
+
const nodeSelector = options.node
|
|
1590
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
1591
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
1592
|
+
let code = `
|
|
1593
|
+
${nodeSelector}
|
|
1594
|
+
const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
|
|
1595
|
+
if (!v) 'Variable not found: ${varName}';
|
|
1596
|
+
else if (nodes.length === 0) 'No node selected';
|
|
1597
|
+
else {
|
|
1598
|
+
nodes.forEach(n => {
|
|
1599
|
+
if ('fills' in n && n.fills.length > 0) {
|
|
1600
|
+
const newFill = figma.variables.setBoundVariableForPaint(n.fills[0], 'color', v);
|
|
1601
|
+
n.fills = [newFill];
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
'Bound ' + v.name + ' to fill on ' + nodes.length + ' elements';
|
|
1605
|
+
}
|
|
1606
|
+
`;
|
|
1607
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
bind
|
|
1611
|
+
.command('stroke <varName>')
|
|
1612
|
+
.description('Bind color variable to stroke')
|
|
1613
|
+
.option('-n, --node <id>', 'Node ID')
|
|
1614
|
+
.action((varName, options) => {
|
|
1615
|
+
checkConnection();
|
|
1616
|
+
const nodeSelector = options.node
|
|
1617
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
1618
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
1619
|
+
let code = `
|
|
1620
|
+
${nodeSelector}
|
|
1621
|
+
const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
|
|
1622
|
+
if (!v) 'Variable not found: ${varName}';
|
|
1623
|
+
else if (nodes.length === 0) 'No node selected';
|
|
1624
|
+
else {
|
|
1625
|
+
nodes.forEach(n => {
|
|
1626
|
+
if ('strokes' in n) {
|
|
1627
|
+
const stroke = n.strokes[0] || { type: 'SOLID', color: {r:0,g:0,b:0} };
|
|
1628
|
+
const newStroke = figma.variables.setBoundVariableForPaint(stroke, 'color', v);
|
|
1629
|
+
n.strokes = [newStroke];
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
'Bound ' + v.name + ' to stroke on ' + nodes.length + ' elements';
|
|
1633
|
+
}
|
|
1634
|
+
`;
|
|
1635
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
bind
|
|
1639
|
+
.command('radius <varName>')
|
|
1640
|
+
.description('Bind number variable to corner radius')
|
|
1641
|
+
.option('-n, --node <id>', 'Node ID')
|
|
1642
|
+
.action((varName, options) => {
|
|
1643
|
+
checkConnection();
|
|
1644
|
+
const nodeSelector = options.node
|
|
1645
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
1646
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
1647
|
+
let code = `
|
|
1648
|
+
${nodeSelector}
|
|
1649
|
+
const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
|
|
1650
|
+
if (!v) 'Variable not found: ${varName}';
|
|
1651
|
+
else if (nodes.length === 0) 'No node selected';
|
|
1652
|
+
else {
|
|
1653
|
+
nodes.forEach(n => {
|
|
1654
|
+
if ('cornerRadius' in n) n.setBoundVariable('cornerRadius', v);
|
|
1655
|
+
});
|
|
1656
|
+
'Bound ' + v.name + ' to radius on ' + nodes.length + ' elements';
|
|
1657
|
+
}
|
|
1658
|
+
`;
|
|
1659
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
bind
|
|
1663
|
+
.command('gap <varName>')
|
|
1664
|
+
.description('Bind number variable to auto-layout gap')
|
|
1665
|
+
.option('-n, --node <id>', 'Node ID')
|
|
1666
|
+
.action((varName, options) => {
|
|
1667
|
+
checkConnection();
|
|
1668
|
+
const nodeSelector = options.node
|
|
1669
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
1670
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
1671
|
+
let code = `
|
|
1672
|
+
${nodeSelector}
|
|
1673
|
+
const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
|
|
1674
|
+
if (!v) 'Variable not found: ${varName}';
|
|
1675
|
+
else if (nodes.length === 0) 'No node selected';
|
|
1676
|
+
else {
|
|
1677
|
+
nodes.forEach(n => {
|
|
1678
|
+
if ('itemSpacing' in n) n.setBoundVariable('itemSpacing', v);
|
|
1679
|
+
});
|
|
1680
|
+
'Bound ' + v.name + ' to gap on ' + nodes.length + ' elements';
|
|
1681
|
+
}
|
|
1682
|
+
`;
|
|
1683
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
bind
|
|
1687
|
+
.command('padding <varName>')
|
|
1688
|
+
.description('Bind number variable to padding')
|
|
1689
|
+
.option('-n, --node <id>', 'Node ID')
|
|
1690
|
+
.option('-s, --side <side>', 'Side: top, right, bottom, left, all', 'all')
|
|
1691
|
+
.action((varName, options) => {
|
|
1692
|
+
checkConnection();
|
|
1693
|
+
const nodeSelector = options.node
|
|
1694
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
1695
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
1696
|
+
const sides = options.side === 'all'
|
|
1697
|
+
? ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft']
|
|
1698
|
+
: [`padding${options.side.charAt(0).toUpperCase() + options.side.slice(1)}`];
|
|
1699
|
+
let code = `
|
|
1700
|
+
${nodeSelector}
|
|
1701
|
+
const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
|
|
1702
|
+
if (!v) 'Variable not found: ${varName}';
|
|
1703
|
+
else if (nodes.length === 0) 'No node selected';
|
|
1704
|
+
else {
|
|
1705
|
+
const sides = ${JSON.stringify(sides)};
|
|
1706
|
+
nodes.forEach(n => {
|
|
1707
|
+
sides.forEach(side => { if (side in n) n.setBoundVariable(side, v); });
|
|
1708
|
+
});
|
|
1709
|
+
'Bound ' + v.name + ' to padding on ' + nodes.length + ' elements';
|
|
1710
|
+
}
|
|
1711
|
+
`;
|
|
1712
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
bind
|
|
1716
|
+
.command('list')
|
|
1717
|
+
.description('List available variables for binding')
|
|
1718
|
+
.option('-t, --type <type>', 'Filter: COLOR, FLOAT')
|
|
1719
|
+
.action((options) => {
|
|
1720
|
+
checkConnection();
|
|
1721
|
+
let code = `
|
|
1722
|
+
const vars = figma.variables.getLocalVariables();
|
|
1723
|
+
const filtered = vars${options.type ? `.filter(v => v.resolvedType === '${options.type.toUpperCase()}')` : ''};
|
|
1724
|
+
filtered.map(v => v.resolvedType.padEnd(8) + ' ' + v.name).join('\\n') || 'No variables';
|
|
1725
|
+
`;
|
|
1726
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
// ============ SIZING ============
|
|
1730
|
+
|
|
1731
|
+
const sizing = program
|
|
1732
|
+
.command('sizing')
|
|
1733
|
+
.description('Control sizing in auto-layout');
|
|
1734
|
+
|
|
1735
|
+
sizing
|
|
1736
|
+
.command('hug')
|
|
1737
|
+
.description('Set to hug contents')
|
|
1738
|
+
.option('-a, --axis <axis>', 'Axis: both, h, v', 'both')
|
|
1739
|
+
.action((options) => {
|
|
1740
|
+
checkConnection();
|
|
1741
|
+
let code = `
|
|
1742
|
+
const nodes = figma.currentPage.selection;
|
|
1743
|
+
if (nodes.length === 0) 'No selection';
|
|
1744
|
+
else {
|
|
1745
|
+
nodes.forEach(n => {
|
|
1746
|
+
${options.axis === 'h' || options.axis === 'both' ? `if ('layoutSizingHorizontal' in n) n.layoutSizingHorizontal = 'HUG';` : ''}
|
|
1747
|
+
${options.axis === 'v' || options.axis === 'both' ? `if ('layoutSizingVertical' in n) n.layoutSizingVertical = 'HUG';` : ''}
|
|
1748
|
+
if (n.layoutMode) { n.primaryAxisSizingMode = 'AUTO'; n.counterAxisSizingMode = 'AUTO'; }
|
|
1749
|
+
});
|
|
1750
|
+
'Set hug on ' + nodes.length + ' elements';
|
|
1751
|
+
}
|
|
1752
|
+
`;
|
|
1753
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
sizing
|
|
1757
|
+
.command('fill')
|
|
1758
|
+
.description('Set to fill container')
|
|
1759
|
+
.option('-a, --axis <axis>', 'Axis: both, h, v', 'both')
|
|
1760
|
+
.action((options) => {
|
|
1761
|
+
checkConnection();
|
|
1762
|
+
let code = `
|
|
1763
|
+
const nodes = figma.currentPage.selection;
|
|
1764
|
+
if (nodes.length === 0) 'No selection';
|
|
1765
|
+
else {
|
|
1766
|
+
nodes.forEach(n => {
|
|
1767
|
+
${options.axis === 'h' || options.axis === 'both' ? `if ('layoutSizingHorizontal' in n) n.layoutSizingHorizontal = 'FILL';` : ''}
|
|
1768
|
+
${options.axis === 'v' || options.axis === 'both' ? `if ('layoutSizingVertical' in n) n.layoutSizingVertical = 'FILL';` : ''}
|
|
1769
|
+
});
|
|
1770
|
+
'Set fill on ' + nodes.length + ' elements';
|
|
1771
|
+
}
|
|
1772
|
+
`;
|
|
1773
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1776
|
+
sizing
|
|
1777
|
+
.command('fixed <width> [height]')
|
|
1778
|
+
.description('Set to fixed size')
|
|
1779
|
+
.action((width, height) => {
|
|
1780
|
+
checkConnection();
|
|
1781
|
+
const h = height || width;
|
|
1782
|
+
let code = `
|
|
1783
|
+
const nodes = figma.currentPage.selection;
|
|
1784
|
+
if (nodes.length === 0) 'No selection';
|
|
1785
|
+
else {
|
|
1786
|
+
nodes.forEach(n => {
|
|
1787
|
+
if ('layoutSizingHorizontal' in n) n.layoutSizingHorizontal = 'FIXED';
|
|
1788
|
+
if ('layoutSizingVertical' in n) n.layoutSizingVertical = 'FIXED';
|
|
1789
|
+
if ('resize' in n) n.resize(${width}, ${h});
|
|
1790
|
+
});
|
|
1791
|
+
'Set fixed ${width}x${h} on ' + nodes.length + ' elements';
|
|
1792
|
+
}
|
|
1793
|
+
`;
|
|
1794
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
// ============ LAYOUT SHORTCUTS ============
|
|
1798
|
+
|
|
1799
|
+
program
|
|
1800
|
+
.command('padding <value> [r] [b] [l]')
|
|
1801
|
+
.alias('pad')
|
|
1802
|
+
.description('Set padding (CSS-style: 1-4 values)')
|
|
1803
|
+
.action((value, r, b, l) => {
|
|
1804
|
+
checkConnection();
|
|
1805
|
+
let top = value, right = r || value, bottom = b || value, left = l || r || value;
|
|
1806
|
+
if (!r) { right = value; bottom = value; left = value; }
|
|
1807
|
+
else if (!b) { bottom = value; left = r; }
|
|
1808
|
+
else if (!l) { left = r; }
|
|
1809
|
+
let code = `
|
|
1810
|
+
const nodes = figma.currentPage.selection;
|
|
1811
|
+
if (nodes.length === 0) 'No selection';
|
|
1812
|
+
else {
|
|
1813
|
+
nodes.forEach(n => {
|
|
1814
|
+
if ('paddingTop' in n) {
|
|
1815
|
+
n.paddingTop = ${top}; n.paddingRight = ${right};
|
|
1816
|
+
n.paddingBottom = ${bottom}; n.paddingLeft = ${left};
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
'Set padding on ' + nodes.length + ' elements';
|
|
1820
|
+
}
|
|
1821
|
+
`;
|
|
1822
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
program
|
|
1826
|
+
.command('gap <value>')
|
|
1827
|
+
.description('Set auto-layout gap')
|
|
1828
|
+
.action((value) => {
|
|
1829
|
+
checkConnection();
|
|
1830
|
+
let code = `
|
|
1831
|
+
const nodes = figma.currentPage.selection;
|
|
1832
|
+
if (nodes.length === 0) 'No selection';
|
|
1833
|
+
else {
|
|
1834
|
+
nodes.forEach(n => { if ('itemSpacing' in n) n.itemSpacing = ${value}; });
|
|
1835
|
+
'Set gap ${value} on ' + nodes.length + ' elements';
|
|
1836
|
+
}
|
|
1837
|
+
`;
|
|
1838
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
program
|
|
1842
|
+
.command('align <alignment>')
|
|
1843
|
+
.description('Align items: start, center, end, stretch')
|
|
1844
|
+
.action((alignment) => {
|
|
1845
|
+
checkConnection();
|
|
1846
|
+
const map = { start: 'MIN', center: 'CENTER', end: 'MAX', stretch: 'STRETCH' };
|
|
1847
|
+
const val = map[alignment.toLowerCase()] || 'CENTER';
|
|
1848
|
+
let code = `
|
|
1849
|
+
const nodes = figma.currentPage.selection;
|
|
1850
|
+
if (nodes.length === 0) 'No selection';
|
|
1851
|
+
else {
|
|
1852
|
+
nodes.forEach(n => {
|
|
1853
|
+
if ('primaryAxisAlignItems' in n) n.primaryAxisAlignItems = '${val}';
|
|
1854
|
+
if ('counterAxisAlignItems' in n) n.counterAxisAlignItems = '${val}';
|
|
1855
|
+
});
|
|
1856
|
+
'Aligned ' + nodes.length + ' elements to ${alignment}';
|
|
1857
|
+
}
|
|
1858
|
+
`;
|
|
1859
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
// ============ SELECT ============
|
|
1863
|
+
|
|
1864
|
+
program
|
|
1865
|
+
.command('select <nodeId>')
|
|
1866
|
+
.description('Select a node by ID')
|
|
1867
|
+
.action((nodeId) => {
|
|
1868
|
+
checkConnection();
|
|
1869
|
+
figmaUse(`select "${nodeId}"`);
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
// ============ DELETE ============
|
|
1873
|
+
|
|
1874
|
+
program
|
|
1875
|
+
.command('delete [nodeId]')
|
|
1876
|
+
.alias('remove')
|
|
1877
|
+
.description('Delete node by ID or current selection')
|
|
1878
|
+
.action((nodeId) => {
|
|
1879
|
+
checkConnection();
|
|
1880
|
+
if (nodeId) {
|
|
1881
|
+
let code = `
|
|
1882
|
+
const node = figma.getNodeById('${nodeId}');
|
|
1883
|
+
if (node) { node.remove(); 'Deleted: ${nodeId}'; } else { 'Node not found: ${nodeId}'; }
|
|
1884
|
+
`;
|
|
1885
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1886
|
+
} else {
|
|
1887
|
+
let code = `
|
|
1888
|
+
const sel = figma.currentPage.selection;
|
|
1889
|
+
if (sel.length === 0) 'No selection';
|
|
1890
|
+
else { const count = sel.length; sel.forEach(n => n.remove()); 'Deleted ' + count + ' elements'; }
|
|
1891
|
+
`;
|
|
1892
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
// ============ DUPLICATE ============
|
|
1897
|
+
|
|
1898
|
+
program
|
|
1899
|
+
.command('duplicate [nodeId]')
|
|
1900
|
+
.alias('dup')
|
|
1901
|
+
.description('Duplicate node by ID or current selection')
|
|
1902
|
+
.option('--offset <n>', 'Offset from original', '20')
|
|
1903
|
+
.action((nodeId, options) => {
|
|
1904
|
+
checkConnection();
|
|
1905
|
+
if (nodeId) {
|
|
1906
|
+
let code = `
|
|
1907
|
+
const node = figma.getNodeById('${nodeId}');
|
|
1908
|
+
if (node) { const clone = node.clone(); clone.x += ${options.offset}; clone.y += ${options.offset}; figma.currentPage.selection = [clone]; 'Duplicated: ' + clone.id; } else { 'Node not found'; }
|
|
1909
|
+
`;
|
|
1910
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1911
|
+
} else {
|
|
1912
|
+
let code = `
|
|
1913
|
+
const sel = figma.currentPage.selection;
|
|
1914
|
+
if (sel.length === 0) 'No selection';
|
|
1915
|
+
else { const clones = sel.map(n => { const c = n.clone(); c.x += ${options.offset}; c.y += ${options.offset}; return c; }); figma.currentPage.selection = clones; 'Duplicated ' + clones.length + ' elements'; }
|
|
1916
|
+
`;
|
|
1917
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
// ============ SET ============
|
|
1922
|
+
|
|
1923
|
+
const set = program
|
|
1924
|
+
.command('set')
|
|
1925
|
+
.description('Set properties on selection or node');
|
|
1926
|
+
|
|
1927
|
+
set
|
|
1928
|
+
.command('fill <color>')
|
|
1929
|
+
.description('Set fill color')
|
|
1930
|
+
.option('-n, --node <id>', 'Node ID (uses selection if not set)')
|
|
1931
|
+
.action((color, options) => {
|
|
1932
|
+
checkConnection();
|
|
1933
|
+
const { r, g, b } = hexToRgb(color);
|
|
1934
|
+
const nodeSelector = options.node
|
|
1935
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
1936
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
1937
|
+
let code = `
|
|
1938
|
+
${nodeSelector}
|
|
1939
|
+
if (nodes.length === 0) 'No node found';
|
|
1940
|
+
else { nodes.forEach(n => { if ('fills' in n) n.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }]; }); 'Fill set on ' + nodes.length + ' elements'; }
|
|
1941
|
+
`;
|
|
1942
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
set
|
|
1946
|
+
.command('stroke <color>')
|
|
1947
|
+
.description('Set stroke color')
|
|
1948
|
+
.option('-n, --node <id>', 'Node ID')
|
|
1949
|
+
.option('-w, --weight <n>', 'Stroke weight', '1')
|
|
1950
|
+
.action((color, options) => {
|
|
1951
|
+
checkConnection();
|
|
1952
|
+
const { r, g, b } = hexToRgb(color);
|
|
1953
|
+
const nodeSelector = options.node
|
|
1954
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
1955
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
1956
|
+
let code = `
|
|
1957
|
+
${nodeSelector}
|
|
1958
|
+
if (nodes.length === 0) 'No node found';
|
|
1959
|
+
else { nodes.forEach(n => { if ('strokes' in n) { n.strokes = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }]; n.strokeWeight = ${options.weight}; } }); 'Stroke set on ' + nodes.length + ' elements'; }
|
|
1960
|
+
`;
|
|
1961
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
set
|
|
1965
|
+
.command('radius <value>')
|
|
1966
|
+
.description('Set corner radius')
|
|
1967
|
+
.option('-n, --node <id>', 'Node ID')
|
|
1968
|
+
.action((value, options) => {
|
|
1969
|
+
checkConnection();
|
|
1970
|
+
const nodeSelector = options.node
|
|
1971
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
1972
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
1973
|
+
let code = `
|
|
1974
|
+
${nodeSelector}
|
|
1975
|
+
if (nodes.length === 0) 'No node found';
|
|
1976
|
+
else { nodes.forEach(n => { if ('cornerRadius' in n) n.cornerRadius = ${value}; }); 'Radius set on ' + nodes.length + ' elements'; }
|
|
1977
|
+
`;
|
|
1978
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
set
|
|
1982
|
+
.command('size <width> <height>')
|
|
1983
|
+
.description('Set size')
|
|
1984
|
+
.option('-n, --node <id>', 'Node ID')
|
|
1985
|
+
.action((width, height, options) => {
|
|
1986
|
+
checkConnection();
|
|
1987
|
+
const nodeSelector = options.node
|
|
1988
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
1989
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
1990
|
+
let code = `
|
|
1991
|
+
${nodeSelector}
|
|
1992
|
+
if (nodes.length === 0) 'No node found';
|
|
1993
|
+
else { nodes.forEach(n => { if ('resize' in n) n.resize(${width}, ${height}); }); 'Size set on ' + nodes.length + ' elements'; }
|
|
1994
|
+
`;
|
|
1995
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1998
|
+
set
|
|
1999
|
+
.command('pos <x> <y>')
|
|
2000
|
+
.alias('position')
|
|
2001
|
+
.description('Set position')
|
|
2002
|
+
.option('-n, --node <id>', 'Node ID')
|
|
2003
|
+
.action((x, y, options) => {
|
|
2004
|
+
checkConnection();
|
|
2005
|
+
const nodeSelector = options.node
|
|
2006
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
2007
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
2008
|
+
let code = `
|
|
2009
|
+
${nodeSelector}
|
|
2010
|
+
if (nodes.length === 0) 'No node found';
|
|
2011
|
+
else { nodes.forEach(n => { n.x = ${x}; n.y = ${y}; }); 'Position set on ' + nodes.length + ' elements'; }
|
|
2012
|
+
`;
|
|
2013
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
set
|
|
2017
|
+
.command('opacity <value>')
|
|
2018
|
+
.description('Set opacity (0-1)')
|
|
2019
|
+
.option('-n, --node <id>', 'Node ID')
|
|
2020
|
+
.action((value, options) => {
|
|
2021
|
+
checkConnection();
|
|
2022
|
+
const nodeSelector = options.node
|
|
2023
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
2024
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
2025
|
+
let code = `
|
|
2026
|
+
${nodeSelector}
|
|
2027
|
+
if (nodes.length === 0) 'No node found';
|
|
2028
|
+
else { nodes.forEach(n => { if ('opacity' in n) n.opacity = ${value}; }); 'Opacity set on ' + nodes.length + ' elements'; }
|
|
2029
|
+
`;
|
|
2030
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
set
|
|
2034
|
+
.command('name <name>')
|
|
2035
|
+
.description('Rename node')
|
|
2036
|
+
.option('-n, --node <id>', 'Node ID')
|
|
2037
|
+
.action((name, options) => {
|
|
2038
|
+
checkConnection();
|
|
2039
|
+
const nodeSelector = options.node
|
|
2040
|
+
? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
|
|
2041
|
+
: `const nodes = figma.currentPage.selection;`;
|
|
2042
|
+
let code = `
|
|
2043
|
+
${nodeSelector}
|
|
2044
|
+
if (nodes.length === 0) 'No node found';
|
|
2045
|
+
else { nodes.forEach(n => { n.name = '${name}'; }); 'Renamed ' + nodes.length + ' elements to ${name}'; }
|
|
2046
|
+
`;
|
|
2047
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
set
|
|
2051
|
+
.command('autolayout <direction>')
|
|
2052
|
+
.alias('al')
|
|
2053
|
+
.description('Apply auto-layout to selection (row/col)')
|
|
2054
|
+
.option('-g, --gap <n>', 'Gap between items', '8')
|
|
2055
|
+
.option('-p, --padding <n>', 'Padding')
|
|
2056
|
+
.action((direction, options) => {
|
|
2057
|
+
checkConnection();
|
|
2058
|
+
const layoutMode = direction === 'col' || direction === 'vertical' ? 'VERTICAL' : 'HORIZONTAL';
|
|
2059
|
+
let code = `
|
|
2060
|
+
const sel = figma.currentPage.selection;
|
|
2061
|
+
if (sel.length === 0) 'No selection';
|
|
2062
|
+
else {
|
|
2063
|
+
sel.forEach(n => {
|
|
2064
|
+
if (n.type === 'FRAME' || n.type === 'COMPONENT') {
|
|
2065
|
+
n.layoutMode = '${layoutMode}';
|
|
2066
|
+
n.primaryAxisSizingMode = 'AUTO';
|
|
2067
|
+
n.counterAxisSizingMode = 'AUTO';
|
|
2068
|
+
n.itemSpacing = ${options.gap};
|
|
2069
|
+
${options.padding ? `n.paddingTop = n.paddingRight = n.paddingBottom = n.paddingLeft = ${options.padding};` : ''}
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
'Auto-layout applied to ' + sel.length + ' frames';
|
|
2073
|
+
}
|
|
2074
|
+
`;
|
|
2075
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
// ============ ARRANGE ============
|
|
2079
|
+
|
|
2080
|
+
program
|
|
2081
|
+
.command('arrange')
|
|
2082
|
+
.description('Arrange frames on canvas')
|
|
2083
|
+
.option('-g, --gap <n>', 'Gap between frames', '100')
|
|
2084
|
+
.option('-c, --cols <n>', 'Number of columns (0 = single row)', '0')
|
|
2085
|
+
.action((options) => {
|
|
2086
|
+
checkConnection();
|
|
2087
|
+
let code = `
|
|
2088
|
+
const frames = figma.currentPage.children.filter(n => n.type === 'FRAME' || n.type === 'COMPONENT');
|
|
2089
|
+
if (frames.length === 0) 'No frames to arrange';
|
|
2090
|
+
else {
|
|
2091
|
+
frames.sort((a, b) => a.name.localeCompare(b.name));
|
|
2092
|
+
let x = 0, y = 0, rowHeight = 0, col = 0;
|
|
2093
|
+
const gap = ${options.gap};
|
|
2094
|
+
const cols = ${options.cols};
|
|
2095
|
+
frames.forEach((f, i) => {
|
|
2096
|
+
f.x = x;
|
|
2097
|
+
f.y = y;
|
|
2098
|
+
rowHeight = Math.max(rowHeight, f.height);
|
|
2099
|
+
if (cols > 0 && ++col >= cols) {
|
|
2100
|
+
col = 0;
|
|
2101
|
+
x = 0;
|
|
2102
|
+
y += rowHeight + gap;
|
|
2103
|
+
rowHeight = 0;
|
|
2104
|
+
} else {
|
|
2105
|
+
x += f.width + gap;
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
'Arranged ' + frames.length + ' frames';
|
|
2109
|
+
}
|
|
2110
|
+
`;
|
|
2111
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
// ============ GET ============
|
|
2115
|
+
|
|
2116
|
+
program
|
|
2117
|
+
.command('get [nodeId]')
|
|
2118
|
+
.description('Get properties of node or selection')
|
|
2119
|
+
.action((nodeId) => {
|
|
2120
|
+
checkConnection();
|
|
2121
|
+
const nodeSelector = nodeId
|
|
2122
|
+
? `const node = figma.getNodeById('${nodeId}');`
|
|
2123
|
+
: `const node = figma.currentPage.selection[0];`;
|
|
2124
|
+
let code = `
|
|
2125
|
+
${nodeSelector}
|
|
2126
|
+
if (!node) 'No node found';
|
|
2127
|
+
else JSON.stringify({
|
|
2128
|
+
id: node.id,
|
|
2129
|
+
name: node.name,
|
|
2130
|
+
type: node.type,
|
|
2131
|
+
x: node.x,
|
|
2132
|
+
y: node.y,
|
|
2133
|
+
width: node.width,
|
|
2134
|
+
height: node.height,
|
|
2135
|
+
visible: node.visible,
|
|
2136
|
+
locked: node.locked,
|
|
2137
|
+
opacity: node.opacity,
|
|
2138
|
+
rotation: node.rotation,
|
|
2139
|
+
cornerRadius: node.cornerRadius,
|
|
2140
|
+
layoutMode: node.layoutMode,
|
|
2141
|
+
fills: node.fills?.length,
|
|
2142
|
+
strokes: node.strokes?.length,
|
|
2143
|
+
children: node.children?.length
|
|
2144
|
+
}, null, 2)
|
|
2145
|
+
`;
|
|
2146
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
2147
|
+
});
|
|
2148
|
+
|
|
2149
|
+
// ============ FIND ============
|
|
2150
|
+
|
|
2151
|
+
program
|
|
2152
|
+
.command('find <name>')
|
|
2153
|
+
.description('Find nodes by name (partial match)')
|
|
2154
|
+
.option('-t, --type <type>', 'Filter by type (FRAME, TEXT, RECTANGLE, etc.)')
|
|
2155
|
+
.option('-l, --limit <n>', 'Limit results', '20')
|
|
2156
|
+
.action((name, options) => {
|
|
2157
|
+
checkConnection();
|
|
2158
|
+
let code = `
|
|
2159
|
+
const results = [];
|
|
2160
|
+
function search(node) {
|
|
2161
|
+
if (node.name && node.name.toLowerCase().includes('${name.toLowerCase()}')) {
|
|
2162
|
+
${options.type ? `if (node.type === '${options.type.toUpperCase()}')` : ''}
|
|
2163
|
+
results.push({ id: node.id, name: node.name, type: node.type });
|
|
2164
|
+
}
|
|
2165
|
+
if (node.children && results.length < ${options.limit}) {
|
|
2166
|
+
node.children.forEach(search);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
search(figma.currentPage);
|
|
2170
|
+
results.length === 0 ? 'No nodes found matching "${name}"' : results.slice(0, ${options.limit}).map(r => r.id + ' [' + r.type + '] ' + r.name).join('\\n')
|
|
2171
|
+
`;
|
|
2172
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
// ============ RENDER ============
|
|
2176
|
+
|
|
2177
|
+
program
|
|
2178
|
+
.command('render <jsx>')
|
|
2179
|
+
.description('Render JSX to Figma')
|
|
2180
|
+
.action((jsx) => {
|
|
2181
|
+
checkConnection();
|
|
2182
|
+
execSync(`echo '${jsx}' | figma-use render --stdin`, { stdio: 'inherit' });
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
// ============ EXPORT ============
|
|
2186
|
+
|
|
2187
|
+
const exp = program
|
|
2188
|
+
.command('export')
|
|
2189
|
+
.description('Export from Figma');
|
|
2190
|
+
|
|
2191
|
+
exp
|
|
2192
|
+
.command('screenshot')
|
|
2193
|
+
.description('Take a screenshot')
|
|
2194
|
+
.option('-o, --output <file>', 'Output file', 'screenshot.png')
|
|
2195
|
+
.action((options) => {
|
|
2196
|
+
checkConnection();
|
|
2197
|
+
figmaUse(`export screenshot --output "${options.output}"`);
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
exp
|
|
2201
|
+
.command('css')
|
|
2202
|
+
.description('Export variables as CSS custom properties')
|
|
2203
|
+
.action(() => {
|
|
2204
|
+
checkConnection();
|
|
2205
|
+
const code = `
|
|
2206
|
+
const vars = figma.variables.getLocalVariables();
|
|
2207
|
+
const css = vars.map(v => {
|
|
2208
|
+
const val = Object.values(v.valuesByMode)[0];
|
|
2209
|
+
if (v.resolvedType === 'COLOR') {
|
|
2210
|
+
const hex = '#' + [val.r, val.g, val.b].map(n => Math.round(n*255).toString(16).padStart(2,'0')).join('');
|
|
2211
|
+
return ' --' + v.name.replace(/\\//g, '-') + ': ' + hex + ';';
|
|
2212
|
+
}
|
|
2213
|
+
return ' --' + v.name.replace(/\\//g, '-') + ': ' + val + (v.resolvedType === 'FLOAT' ? 'px' : '') + ';';
|
|
2214
|
+
}).join('\\n');
|
|
2215
|
+
':root {\\n' + css + '\\n}'
|
|
2216
|
+
`;
|
|
2217
|
+
const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
2218
|
+
console.log(result);
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
exp
|
|
2222
|
+
.command('tailwind')
|
|
2223
|
+
.description('Export color variables as Tailwind config')
|
|
2224
|
+
.action(() => {
|
|
2225
|
+
checkConnection();
|
|
2226
|
+
const code = `
|
|
2227
|
+
const vars = figma.variables.getLocalVariables().filter(v => v.resolvedType === 'COLOR');
|
|
2228
|
+
const colors = {};
|
|
2229
|
+
vars.forEach(v => {
|
|
2230
|
+
const val = Object.values(v.valuesByMode)[0];
|
|
2231
|
+
const hex = '#' + [val.r, val.g, val.b].map(n => Math.round(n*255).toString(16).padStart(2,'0')).join('');
|
|
2232
|
+
const parts = v.name.split('/');
|
|
2233
|
+
if (parts.length === 2) {
|
|
2234
|
+
if (!colors[parts[0]]) colors[parts[0]] = {};
|
|
2235
|
+
colors[parts[0]][parts[1]] = hex;
|
|
2236
|
+
} else {
|
|
2237
|
+
colors[v.name.replace(/\\//g, '-')] = hex;
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
JSON.stringify({ theme: { extend: { colors } } }, null, 2)
|
|
2241
|
+
`;
|
|
2242
|
+
const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
|
|
2243
|
+
console.log(result);
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
// ============ EVAL ============
|
|
2247
|
+
|
|
2248
|
+
program
|
|
2249
|
+
.command('eval <code>')
|
|
2250
|
+
.description('Execute JavaScript in Figma plugin context')
|
|
2251
|
+
.action((code) => {
|
|
2252
|
+
checkConnection();
|
|
2253
|
+
figmaUse(`eval "${code.replace(/"/g, '\\"')}"`);
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
// ============ PASSTHROUGH ============
|
|
2257
|
+
|
|
2258
|
+
program
|
|
2259
|
+
.command('raw <command...>')
|
|
2260
|
+
.description('Run raw figma-use command')
|
|
2261
|
+
.action((command) => {
|
|
2262
|
+
checkConnection();
|
|
2263
|
+
figmaUse(command.join(' '));
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
// ============ FIGJAM ============
|
|
2267
|
+
|
|
2268
|
+
const figjam = program
|
|
2269
|
+
.command('figjam')
|
|
2270
|
+
.alias('fj')
|
|
2271
|
+
.description('FigJam commands (sticky notes, shapes, connectors)');
|
|
2272
|
+
|
|
2273
|
+
// Helper: Get FigJam client
|
|
2274
|
+
async function getFigJamClient(pageTitle) {
|
|
2275
|
+
const client = new FigJamClient();
|
|
2276
|
+
try {
|
|
2277
|
+
const pages = await FigJamClient.listPages();
|
|
2278
|
+
if (pages.length === 0) {
|
|
2279
|
+
console.log(chalk.red('\n✗ No FigJam pages open\n'));
|
|
2280
|
+
console.log(chalk.gray(' Open a FigJam file in Figma Desktop first.\n'));
|
|
2281
|
+
process.exit(1);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
const targetPage = pageTitle || pages[0].title;
|
|
2285
|
+
await client.connect(targetPage);
|
|
2286
|
+
return client;
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
console.log(chalk.red('\n✗ ' + error.message + '\n'));
|
|
2289
|
+
process.exit(1);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
figjam
|
|
2294
|
+
.command('list')
|
|
2295
|
+
.description('List open FigJam pages')
|
|
2296
|
+
.action(async () => {
|
|
2297
|
+
try {
|
|
2298
|
+
const pages = await FigJamClient.listPages();
|
|
2299
|
+
if (pages.length === 0) {
|
|
2300
|
+
console.log(chalk.yellow('\n No FigJam pages open\n'));
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
console.log(chalk.cyan('\n Open FigJam Pages:\n'));
|
|
2304
|
+
pages.forEach((p, i) => {
|
|
2305
|
+
console.log(chalk.white(` ${i + 1}. ${p.title}`));
|
|
2306
|
+
});
|
|
2307
|
+
console.log();
|
|
2308
|
+
} catch (error) {
|
|
2309
|
+
console.log(chalk.red('\n✗ Could not connect to Figma\n'));
|
|
2310
|
+
console.log(chalk.gray(' Make sure Figma is running with: design-lazyyy-cli connect\n'));
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
figjam
|
|
2315
|
+
.command('info')
|
|
2316
|
+
.description('Show current FigJam page info')
|
|
2317
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2318
|
+
.action(async (options) => {
|
|
2319
|
+
const client = await getFigJamClient(options.page);
|
|
2320
|
+
try {
|
|
2321
|
+
const info = await client.getPageInfo();
|
|
2322
|
+
console.log(chalk.cyan('\n FigJam Page Info:\n'));
|
|
2323
|
+
console.log(chalk.white(` Name: ${info.name}`));
|
|
2324
|
+
console.log(chalk.white(` ID: ${info.id}`));
|
|
2325
|
+
console.log(chalk.white(` Elements: ${info.childCount}`));
|
|
2326
|
+
console.log();
|
|
2327
|
+
} finally {
|
|
2328
|
+
client.close();
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
figjam
|
|
2333
|
+
.command('nodes')
|
|
2334
|
+
.description('List nodes on current FigJam page')
|
|
2335
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2336
|
+
.option('-l, --limit <n>', 'Limit number of nodes', '20')
|
|
2337
|
+
.action(async (options) => {
|
|
2338
|
+
const client = await getFigJamClient(options.page);
|
|
2339
|
+
try {
|
|
2340
|
+
const nodes = await client.listNodes(parseInt(options.limit));
|
|
2341
|
+
if (nodes.length === 0) {
|
|
2342
|
+
console.log(chalk.yellow('\n No elements on this page\n'));
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
console.log(chalk.cyan('\n FigJam Elements:\n'));
|
|
2346
|
+
nodes.forEach(n => {
|
|
2347
|
+
const type = n.type.padEnd(16);
|
|
2348
|
+
const name = (n.name || '(unnamed)').substring(0, 30);
|
|
2349
|
+
console.log(chalk.gray(` ${n.id.padEnd(8)}`), chalk.white(type), chalk.gray(name), chalk.gray(`(${n.x}, ${n.y})`));
|
|
2350
|
+
});
|
|
2351
|
+
console.log();
|
|
2352
|
+
} finally {
|
|
2353
|
+
client.close();
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
|
|
2357
|
+
figjam
|
|
2358
|
+
.command('sticky <text>')
|
|
2359
|
+
.description('Create a sticky note')
|
|
2360
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2361
|
+
.option('-x <n>', 'X position', '0')
|
|
2362
|
+
.option('-y <n>', 'Y position', '0')
|
|
2363
|
+
.option('-c, --color <hex>', 'Background color')
|
|
2364
|
+
.action(async (text, options) => {
|
|
2365
|
+
const client = await getFigJamClient(options.page);
|
|
2366
|
+
const spinner = ora('Creating sticky note...').start();
|
|
2367
|
+
try {
|
|
2368
|
+
const result = await client.createSticky(text, parseFloat(options.x), parseFloat(options.y), options.color);
|
|
2369
|
+
spinner.succeed(`Sticky created: ${result.id} at (${result.x}, ${result.y})`);
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
spinner.fail('Failed to create sticky: ' + error.message);
|
|
2372
|
+
} finally {
|
|
2373
|
+
client.close();
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
figjam
|
|
2378
|
+
.command('shape <text>')
|
|
2379
|
+
.description('Create a shape with text')
|
|
2380
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2381
|
+
.option('-x <n>', 'X position', '0')
|
|
2382
|
+
.option('-y <n>', 'Y position', '0')
|
|
2383
|
+
.option('-w, --width <n>', 'Width', '200')
|
|
2384
|
+
.option('-h, --height <n>', 'Height', '100')
|
|
2385
|
+
.option('-t, --type <type>', 'Shape type (ROUNDED_RECTANGLE, RECTANGLE, ELLIPSE, DIAMOND)', 'ROUNDED_RECTANGLE')
|
|
2386
|
+
.action(async (text, options) => {
|
|
2387
|
+
const client = await getFigJamClient(options.page);
|
|
2388
|
+
const spinner = ora('Creating shape...').start();
|
|
2389
|
+
try {
|
|
2390
|
+
const result = await client.createShape(
|
|
2391
|
+
text,
|
|
2392
|
+
parseFloat(options.x),
|
|
2393
|
+
parseFloat(options.y),
|
|
2394
|
+
parseFloat(options.width),
|
|
2395
|
+
parseFloat(options.height),
|
|
2396
|
+
options.type
|
|
2397
|
+
);
|
|
2398
|
+
spinner.succeed(`Shape created: ${result.id} at (${result.x}, ${result.y})`);
|
|
2399
|
+
} catch (error) {
|
|
2400
|
+
spinner.fail('Failed to create shape: ' + error.message);
|
|
2401
|
+
} finally {
|
|
2402
|
+
client.close();
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
figjam
|
|
2407
|
+
.command('text <content>')
|
|
2408
|
+
.description('Create a text node')
|
|
2409
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2410
|
+
.option('-x <n>', 'X position', '0')
|
|
2411
|
+
.option('-y <n>', 'Y position', '0')
|
|
2412
|
+
.option('-s, --size <n>', 'Font size', '16')
|
|
2413
|
+
.action(async (content, options) => {
|
|
2414
|
+
const client = await getFigJamClient(options.page);
|
|
2415
|
+
const spinner = ora('Creating text...').start();
|
|
2416
|
+
try {
|
|
2417
|
+
const result = await client.createText(content, parseFloat(options.x), parseFloat(options.y), parseFloat(options.size));
|
|
2418
|
+
spinner.succeed(`Text created: ${result.id} at (${result.x}, ${result.y})`);
|
|
2419
|
+
} catch (error) {
|
|
2420
|
+
spinner.fail('Failed to create text: ' + error.message);
|
|
2421
|
+
} finally {
|
|
2422
|
+
client.close();
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2425
|
+
|
|
2426
|
+
figjam
|
|
2427
|
+
.command('connect <startId> <endId>')
|
|
2428
|
+
.description('Create a connector between two nodes')
|
|
2429
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2430
|
+
.action(async (startId, endId, options) => {
|
|
2431
|
+
const client = await getFigJamClient(options.page);
|
|
2432
|
+
const spinner = ora('Creating connector...').start();
|
|
2433
|
+
try {
|
|
2434
|
+
const result = await client.createConnector(startId, endId);
|
|
2435
|
+
if (result.error) {
|
|
2436
|
+
spinner.fail(result.error);
|
|
2437
|
+
} else {
|
|
2438
|
+
spinner.succeed(`Connector created: ${result.id}`);
|
|
2439
|
+
}
|
|
2440
|
+
} catch (error) {
|
|
2441
|
+
spinner.fail('Failed to create connector: ' + error.message);
|
|
2442
|
+
} finally {
|
|
2443
|
+
client.close();
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
figjam
|
|
2448
|
+
.command('delete <nodeId>')
|
|
2449
|
+
.description('Delete a node by ID')
|
|
2450
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2451
|
+
.action(async (nodeId, options) => {
|
|
2452
|
+
const client = await getFigJamClient(options.page);
|
|
2453
|
+
const spinner = ora('Deleting node...').start();
|
|
2454
|
+
try {
|
|
2455
|
+
const result = await client.deleteNode(nodeId);
|
|
2456
|
+
if (result.deleted) {
|
|
2457
|
+
spinner.succeed(`Node ${nodeId} deleted`);
|
|
2458
|
+
} else {
|
|
2459
|
+
spinner.fail(result.error || 'Node not found');
|
|
2460
|
+
}
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
spinner.fail('Failed to delete node: ' + error.message);
|
|
2463
|
+
} finally {
|
|
2464
|
+
client.close();
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
figjam
|
|
2469
|
+
.command('move <nodeId> <x> <y>')
|
|
2470
|
+
.description('Move a node to a new position')
|
|
2471
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2472
|
+
.action(async (nodeId, x, y, options) => {
|
|
2473
|
+
const client = await getFigJamClient(options.page);
|
|
2474
|
+
const spinner = ora('Moving node...').start();
|
|
2475
|
+
try {
|
|
2476
|
+
const result = await client.moveNode(nodeId, parseFloat(x), parseFloat(y));
|
|
2477
|
+
if (result.error) {
|
|
2478
|
+
spinner.fail(result.error);
|
|
2479
|
+
} else {
|
|
2480
|
+
spinner.succeed(`Node ${result.id} moved to (${result.x}, ${result.y})`);
|
|
2481
|
+
}
|
|
2482
|
+
} catch (error) {
|
|
2483
|
+
spinner.fail('Failed to move node: ' + error.message);
|
|
2484
|
+
} finally {
|
|
2485
|
+
client.close();
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
figjam
|
|
2490
|
+
.command('update <nodeId> <text>')
|
|
2491
|
+
.description('Update text content of a node')
|
|
2492
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2493
|
+
.action(async (nodeId, text, options) => {
|
|
2494
|
+
const client = await getFigJamClient(options.page);
|
|
2495
|
+
const spinner = ora('Updating text...').start();
|
|
2496
|
+
try {
|
|
2497
|
+
const result = await client.updateText(nodeId, text);
|
|
2498
|
+
if (result.error) {
|
|
2499
|
+
spinner.fail(result.error);
|
|
2500
|
+
} else {
|
|
2501
|
+
spinner.succeed(`Node ${result.id} text updated`);
|
|
2502
|
+
}
|
|
2503
|
+
} catch (error) {
|
|
2504
|
+
spinner.fail('Failed to update text: ' + error.message);
|
|
2505
|
+
} finally {
|
|
2506
|
+
client.close();
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
|
|
2510
|
+
figjam
|
|
2511
|
+
.command('eval <code>')
|
|
2512
|
+
.description('Execute JavaScript in FigJam context')
|
|
2513
|
+
.option('-p, --page <title>', 'Page title (partial match)')
|
|
2514
|
+
.action(async (code, options) => {
|
|
2515
|
+
const client = await getFigJamClient(options.page);
|
|
2516
|
+
try {
|
|
2517
|
+
const result = await client.eval(code);
|
|
2518
|
+
if (result !== undefined) {
|
|
2519
|
+
console.log(typeof result === 'object' ? JSON.stringify(result, null, 2) : result);
|
|
2520
|
+
}
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
console.log(chalk.red('Error: ' + error.message));
|
|
2523
|
+
} finally {
|
|
2524
|
+
client.close();
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
|
|
2528
|
+
program.parse();
|