@vettvangur/design-system 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1335 @@
1
+ #!/usr/bin/env node
2
+ import fs$1 from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import chalk from 'chalk';
5
+ import require$$0 from 'fs';
6
+ import require$$1 from 'path';
7
+ import require$$2 from 'os';
8
+ import require$$3 from 'crypto';
9
+ import fs from 'node:fs';
10
+ import { pathToFileURL } from 'node:url';
11
+ import process$1 from 'node:process';
12
+
13
+ /**
14
+ * Return a list of button names from an exporter payload.
15
+ * @param {{components?: Record<string, any>}} payload
16
+ * @param {{stripPrefix?: boolean, sort?: boolean}} opts
17
+ * @returns {string[]}
18
+ */
19
+ function parseButtons(payload, {
20
+ stripPrefix = false,
21
+ sort = true
22
+ } = {}) {
23
+ const src = payload?.components || {};
24
+ let names = Object.keys(src).filter(k => k.startsWith('button-'));
25
+ if (stripPrefix) names = names.map(k => k.slice('button-'.length));
26
+ if (sort) names.sort();
27
+ return names;
28
+ }
29
+
30
+ var config$1 = {};
31
+
32
+ var main$1 = {exports: {}};
33
+
34
+ var name = "dotenv";
35
+ var version = "16.4.5";
36
+ var description = "Loads environment variables from .env file";
37
+ var main = "lib/main.js";
38
+ var types = "lib/main.d.ts";
39
+ var exports = {
40
+ ".": {
41
+ types: "./lib/main.d.ts",
42
+ require: "./lib/main.js",
43
+ "default": "./lib/main.js"
44
+ },
45
+ "./config": "./config.js",
46
+ "./config.js": "./config.js",
47
+ "./lib/env-options": "./lib/env-options.js",
48
+ "./lib/env-options.js": "./lib/env-options.js",
49
+ "./lib/cli-options": "./lib/cli-options.js",
50
+ "./lib/cli-options.js": "./lib/cli-options.js",
51
+ "./package.json": "./package.json"
52
+ };
53
+ var scripts = {
54
+ "dts-check": "tsc --project tests/types/tsconfig.json",
55
+ lint: "standard",
56
+ "lint-readme": "standard-markdown",
57
+ pretest: "npm run lint && npm run dts-check",
58
+ test: "tap tests/*.js --100 -Rspec",
59
+ "test:coverage": "tap --coverage-report=lcov",
60
+ prerelease: "npm test",
61
+ release: "standard-version"
62
+ };
63
+ var repository = {
64
+ type: "git",
65
+ url: "git://github.com/motdotla/dotenv.git"
66
+ };
67
+ var funding = "https://dotenvx.com";
68
+ var keywords = [
69
+ "dotenv",
70
+ "env",
71
+ ".env",
72
+ "environment",
73
+ "variables",
74
+ "config",
75
+ "settings"
76
+ ];
77
+ var readmeFilename = "README.md";
78
+ var license = "BSD-2-Clause";
79
+ var devDependencies = {
80
+ "@definitelytyped/dtslint": "^0.0.133",
81
+ "@types/node": "^18.11.3",
82
+ decache: "^4.6.1",
83
+ sinon: "^14.0.1",
84
+ standard: "^17.0.0",
85
+ "standard-markdown": "^7.1.0",
86
+ "standard-version": "^9.5.0",
87
+ tap: "^16.3.0",
88
+ tar: "^6.1.11",
89
+ typescript: "^4.8.4"
90
+ };
91
+ var engines = {
92
+ node: ">=12"
93
+ };
94
+ var browser = {
95
+ fs: false
96
+ };
97
+ var require$$4 = {
98
+ name: name,
99
+ version: version,
100
+ description: description,
101
+ main: main,
102
+ types: types,
103
+ exports: exports,
104
+ scripts: scripts,
105
+ repository: repository,
106
+ funding: funding,
107
+ keywords: keywords,
108
+ readmeFilename: readmeFilename,
109
+ license: license,
110
+ devDependencies: devDependencies,
111
+ engines: engines,
112
+ browser: browser
113
+ };
114
+
115
+ var hasRequiredMain;
116
+ function requireMain() {
117
+ if (hasRequiredMain) return main$1.exports;
118
+ hasRequiredMain = 1;
119
+ const fs = require$$0;
120
+ const path = require$$1;
121
+ const os = require$$2;
122
+ const crypto = require$$3;
123
+ const packageJson = require$$4;
124
+ const version = packageJson.version;
125
+ const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
126
+
127
+ // Parse src into an Object
128
+ function parse(src) {
129
+ const obj = {};
130
+
131
+ // Convert buffer to string
132
+ let lines = src.toString();
133
+
134
+ // Convert line breaks to same format
135
+ lines = lines.replace(/\r\n?/mg, '\n');
136
+ let match;
137
+ while ((match = LINE.exec(lines)) != null) {
138
+ const key = match[1];
139
+
140
+ // Default undefined or null to empty string
141
+ let value = match[2] || '';
142
+
143
+ // Remove whitespace
144
+ value = value.trim();
145
+
146
+ // Check if double quoted
147
+ const maybeQuote = value[0];
148
+
149
+ // Remove surrounding quotes
150
+ value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2');
151
+
152
+ // Expand newlines if double quoted
153
+ if (maybeQuote === '"') {
154
+ value = value.replace(/\\n/g, '\n');
155
+ value = value.replace(/\\r/g, '\r');
156
+ }
157
+
158
+ // Add to object
159
+ obj[key] = value;
160
+ }
161
+ return obj;
162
+ }
163
+ function _parseVault(options) {
164
+ const vaultPath = _vaultPath(options);
165
+
166
+ // Parse .env.vault
167
+ const result = DotenvModule.configDotenv({
168
+ path: vaultPath
169
+ });
170
+ if (!result.parsed) {
171
+ const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
172
+ err.code = 'MISSING_DATA';
173
+ throw err;
174
+ }
175
+
176
+ // handle scenario for comma separated keys - for use with key rotation
177
+ // example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
178
+ const keys = _dotenvKey(options).split(',');
179
+ const length = keys.length;
180
+ let decrypted;
181
+ for (let i = 0; i < length; i++) {
182
+ try {
183
+ // Get full key
184
+ const key = keys[i].trim();
185
+
186
+ // Get instructions for decrypt
187
+ const attrs = _instructions(result, key);
188
+
189
+ // Decrypt
190
+ decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
191
+ break;
192
+ } catch (error) {
193
+ // last key
194
+ if (i + 1 >= length) {
195
+ throw error;
196
+ }
197
+ // try next key
198
+ }
199
+ }
200
+
201
+ // Parse decrypted .env string
202
+ return DotenvModule.parse(decrypted);
203
+ }
204
+ function _log(message) {
205
+ console.log(`[dotenv@${version}][INFO] ${message}`);
206
+ }
207
+ function _warn(message) {
208
+ console.log(`[dotenv@${version}][WARN] ${message}`);
209
+ }
210
+ function _debug(message) {
211
+ console.log(`[dotenv@${version}][DEBUG] ${message}`);
212
+ }
213
+ function _dotenvKey(options) {
214
+ // prioritize developer directly setting options.DOTENV_KEY
215
+ if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
216
+ return options.DOTENV_KEY;
217
+ }
218
+
219
+ // secondary infra already contains a DOTENV_KEY environment variable
220
+ if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
221
+ return process.env.DOTENV_KEY;
222
+ }
223
+
224
+ // fallback to empty string
225
+ return '';
226
+ }
227
+ function _instructions(result, dotenvKey) {
228
+ // Parse DOTENV_KEY. Format is a URI
229
+ let uri;
230
+ try {
231
+ uri = new URL(dotenvKey);
232
+ } catch (error) {
233
+ if (error.code === 'ERR_INVALID_URL') {
234
+ const err = new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development');
235
+ err.code = 'INVALID_DOTENV_KEY';
236
+ throw err;
237
+ }
238
+ throw error;
239
+ }
240
+
241
+ // Get decrypt key
242
+ const key = uri.password;
243
+ if (!key) {
244
+ const err = new Error('INVALID_DOTENV_KEY: Missing key part');
245
+ err.code = 'INVALID_DOTENV_KEY';
246
+ throw err;
247
+ }
248
+
249
+ // Get environment
250
+ const environment = uri.searchParams.get('environment');
251
+ if (!environment) {
252
+ const err = new Error('INVALID_DOTENV_KEY: Missing environment part');
253
+ err.code = 'INVALID_DOTENV_KEY';
254
+ throw err;
255
+ }
256
+
257
+ // Get ciphertext payload
258
+ const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
259
+ const ciphertext = result.parsed[environmentKey]; // DOTENV_VAULT_PRODUCTION
260
+ if (!ciphertext) {
261
+ const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
262
+ err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT';
263
+ throw err;
264
+ }
265
+ return {
266
+ ciphertext,
267
+ key
268
+ };
269
+ }
270
+ function _vaultPath(options) {
271
+ let possibleVaultPath = null;
272
+ if (options && options.path && options.path.length > 0) {
273
+ if (Array.isArray(options.path)) {
274
+ for (const filepath of options.path) {
275
+ if (fs.existsSync(filepath)) {
276
+ possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault`;
277
+ }
278
+ }
279
+ } else {
280
+ possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault`;
281
+ }
282
+ } else {
283
+ possibleVaultPath = path.resolve(process.cwd(), '.env.vault');
284
+ }
285
+ if (fs.existsSync(possibleVaultPath)) {
286
+ return possibleVaultPath;
287
+ }
288
+ return null;
289
+ }
290
+ function _resolveHome(envPath) {
291
+ return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath;
292
+ }
293
+ function _configVault(options) {
294
+ _log('Loading env from encrypted .env.vault');
295
+ const parsed = DotenvModule._parseVault(options);
296
+ let processEnv = process.env;
297
+ if (options && options.processEnv != null) {
298
+ processEnv = options.processEnv;
299
+ }
300
+ DotenvModule.populate(processEnv, parsed, options);
301
+ return {
302
+ parsed
303
+ };
304
+ }
305
+ function configDotenv(options) {
306
+ const dotenvPath = path.resolve(process.cwd(), '.env');
307
+ let encoding = 'utf8';
308
+ const debug = Boolean(options && options.debug);
309
+ if (options && options.encoding) {
310
+ encoding = options.encoding;
311
+ } else {
312
+ if (debug) {
313
+ _debug('No encoding is specified. UTF-8 is used by default');
314
+ }
315
+ }
316
+ let optionPaths = [dotenvPath]; // default, look for .env
317
+ if (options && options.path) {
318
+ if (!Array.isArray(options.path)) {
319
+ optionPaths = [_resolveHome(options.path)];
320
+ } else {
321
+ optionPaths = []; // reset default
322
+ for (const filepath of options.path) {
323
+ optionPaths.push(_resolveHome(filepath));
324
+ }
325
+ }
326
+ }
327
+
328
+ // Build the parsed data in a temporary object (because we need to return it). Once we have the final
329
+ // parsed data, we will combine it with process.env (or options.processEnv if provided).
330
+ let lastError;
331
+ const parsedAll = {};
332
+ for (const path of optionPaths) {
333
+ try {
334
+ // Specifying an encoding returns a string instead of a buffer
335
+ const parsed = DotenvModule.parse(fs.readFileSync(path, {
336
+ encoding
337
+ }));
338
+ DotenvModule.populate(parsedAll, parsed, options);
339
+ } catch (e) {
340
+ if (debug) {
341
+ _debug(`Failed to load ${path} ${e.message}`);
342
+ }
343
+ lastError = e;
344
+ }
345
+ }
346
+ let processEnv = process.env;
347
+ if (options && options.processEnv != null) {
348
+ processEnv = options.processEnv;
349
+ }
350
+ DotenvModule.populate(processEnv, parsedAll, options);
351
+ if (lastError) {
352
+ return {
353
+ parsed: parsedAll,
354
+ error: lastError
355
+ };
356
+ } else {
357
+ return {
358
+ parsed: parsedAll
359
+ };
360
+ }
361
+ }
362
+
363
+ // Populates process.env from .env file
364
+ function config(options) {
365
+ // fallback to original dotenv if DOTENV_KEY is not set
366
+ if (_dotenvKey(options).length === 0) {
367
+ return DotenvModule.configDotenv(options);
368
+ }
369
+ const vaultPath = _vaultPath(options);
370
+
371
+ // dotenvKey exists but .env.vault file does not exist
372
+ if (!vaultPath) {
373
+ _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
374
+ return DotenvModule.configDotenv(options);
375
+ }
376
+ return DotenvModule._configVault(options);
377
+ }
378
+ function decrypt(encrypted, keyStr) {
379
+ const key = Buffer.from(keyStr.slice(-64), 'hex');
380
+ let ciphertext = Buffer.from(encrypted, 'base64');
381
+ const nonce = ciphertext.subarray(0, 12);
382
+ const authTag = ciphertext.subarray(-16);
383
+ ciphertext = ciphertext.subarray(12, -16);
384
+ try {
385
+ const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce);
386
+ aesgcm.setAuthTag(authTag);
387
+ return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
388
+ } catch (error) {
389
+ const isRange = error instanceof RangeError;
390
+ const invalidKeyLength = error.message === 'Invalid key length';
391
+ const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data';
392
+ if (isRange || invalidKeyLength) {
393
+ const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)');
394
+ err.code = 'INVALID_DOTENV_KEY';
395
+ throw err;
396
+ } else if (decryptionFailed) {
397
+ const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY');
398
+ err.code = 'DECRYPTION_FAILED';
399
+ throw err;
400
+ } else {
401
+ throw error;
402
+ }
403
+ }
404
+ }
405
+
406
+ // Populate process.env with parsed values
407
+ function populate(processEnv, parsed, options = {}) {
408
+ const debug = Boolean(options && options.debug);
409
+ const override = Boolean(options && options.override);
410
+ if (typeof parsed !== 'object') {
411
+ const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate');
412
+ err.code = 'OBJECT_REQUIRED';
413
+ throw err;
414
+ }
415
+
416
+ // Set process.env
417
+ for (const key of Object.keys(parsed)) {
418
+ if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
419
+ if (override === true) {
420
+ processEnv[key] = parsed[key];
421
+ }
422
+ if (debug) {
423
+ if (override === true) {
424
+ _debug(`"${key}" is already defined and WAS overwritten`);
425
+ } else {
426
+ _debug(`"${key}" is already defined and was NOT overwritten`);
427
+ }
428
+ }
429
+ } else {
430
+ processEnv[key] = parsed[key];
431
+ }
432
+ }
433
+ }
434
+ const DotenvModule = {
435
+ configDotenv,
436
+ _configVault,
437
+ _parseVault,
438
+ config,
439
+ decrypt,
440
+ parse,
441
+ populate
442
+ };
443
+ main$1.exports.configDotenv = DotenvModule.configDotenv;
444
+ main$1.exports._configVault = DotenvModule._configVault;
445
+ main$1.exports._parseVault = DotenvModule._parseVault;
446
+ main$1.exports.config = DotenvModule.config;
447
+ main$1.exports.decrypt = DotenvModule.decrypt;
448
+ main$1.exports.parse = DotenvModule.parse;
449
+ main$1.exports.populate = DotenvModule.populate;
450
+ main$1.exports = DotenvModule;
451
+ return main$1.exports;
452
+ }
453
+
454
+ var envOptions;
455
+ var hasRequiredEnvOptions;
456
+ function requireEnvOptions() {
457
+ if (hasRequiredEnvOptions) return envOptions;
458
+ hasRequiredEnvOptions = 1;
459
+ // ../config.js accepts options via environment variables
460
+ const options = {};
461
+ if (process.env.DOTENV_CONFIG_ENCODING != null) {
462
+ options.encoding = process.env.DOTENV_CONFIG_ENCODING;
463
+ }
464
+ if (process.env.DOTENV_CONFIG_PATH != null) {
465
+ options.path = process.env.DOTENV_CONFIG_PATH;
466
+ }
467
+ if (process.env.DOTENV_CONFIG_DEBUG != null) {
468
+ options.debug = process.env.DOTENV_CONFIG_DEBUG;
469
+ }
470
+ if (process.env.DOTENV_CONFIG_OVERRIDE != null) {
471
+ options.override = process.env.DOTENV_CONFIG_OVERRIDE;
472
+ }
473
+ if (process.env.DOTENV_CONFIG_DOTENV_KEY != null) {
474
+ options.DOTENV_KEY = process.env.DOTENV_CONFIG_DOTENV_KEY;
475
+ }
476
+ envOptions = options;
477
+ return envOptions;
478
+ }
479
+
480
+ var cliOptions;
481
+ var hasRequiredCliOptions;
482
+ function requireCliOptions() {
483
+ if (hasRequiredCliOptions) return cliOptions;
484
+ hasRequiredCliOptions = 1;
485
+ const re = /^dotenv_config_(encoding|path|debug|override|DOTENV_KEY)=(.+)$/;
486
+ cliOptions = function optionMatcher(args) {
487
+ return args.reduce(function (acc, cur) {
488
+ const matches = cur.match(re);
489
+ if (matches) {
490
+ acc[matches[1]] = matches[2];
491
+ }
492
+ return acc;
493
+ }, {});
494
+ };
495
+ return cliOptions;
496
+ }
497
+
498
+ var hasRequiredConfig;
499
+ function requireConfig() {
500
+ if (hasRequiredConfig) return config$1;
501
+ hasRequiredConfig = 1;
502
+ (function () {
503
+ requireMain().config(Object.assign({}, requireEnvOptions(), requireCliOptions()(process.argv)));
504
+ })();
505
+ return config$1;
506
+ }
507
+
508
+ requireConfig();
509
+
510
+ // src/load-config.mjs
511
+ function req(name, val) {
512
+ if (val === undefined || val === null) {
513
+ throw new Error(`[vettvangur-styleguide :: load-config] Missing required config field: ${name}`);
514
+ }
515
+ return val;
516
+ }
517
+
518
+ // Also load .env.local if present (priority over .env)
519
+ const rootCwd = process$1.cwd();
520
+ const envLocal = path.join(rootCwd, ".env.local");
521
+ if (fs.existsSync(envLocal)) {
522
+ const dotenv = await import('dotenv');
523
+ dotenv.config({
524
+ path: envLocal,
525
+ override: true
526
+ });
527
+ }
528
+ const cfgFile = path.resolve(rootCwd, "vettvangur.config.mjs");
529
+ const mod = await import(pathToFileURL(cfgFile).href);
530
+ const config = mod.default ?? mod;
531
+ const root = rootCwd;
532
+
533
+ // Accept either config.figma.key or env (FIGMA_KEY/FIGMA_TOKEN); file from config or FIGMA_FILE
534
+ const figmaKey = config?.figma?.key ?? process$1.env.FIGMA_KEY ?? process$1.env.FIGMA_TOKEN ?? null;
535
+ const figmaFile = config?.figma?.file ?? process$1.env.FIGMA_FILE ?? null;
536
+ const paths = {
537
+ root,
538
+ dest: path.resolve(root, req("dest", config.dest)),
539
+ public: path.resolve(root, req("public", config.public)),
540
+ assets: path.resolve(root, req("assets", config.assets)),
541
+ styles: path.resolve(root, req("styles.path", config.styles.path)),
542
+ styleEntries: req("styles.entries", config.styles.entries).map(p => path.isAbsolute(p) ? p : path.resolve(root, p)),
543
+ scripts: path.resolve(root, req("scripts.path", config.scripts.path)),
544
+ scriptEntries: req("scripts.entries", config.scripts.entries).map(p => path.isAbsolute(p) ? p : path.resolve(root, p)),
545
+ views: path.resolve(root, req("views", config.views)),
546
+ watch: config.watch,
547
+ figma: {
548
+ key: req("figma.key", figmaKey),
549
+ file: req("figma.file", figmaFile)
550
+ }
551
+ };
552
+
553
+ // ---- HTTPS (safe, never throws) --------------------------------------------
554
+ function makeHttpsCfg(cfg) {
555
+ const ds = cfg?.devserver ?? {};
556
+ const h = ds.https;
557
+ if (h === false) return false;
558
+ if (h && typeof h === "object") {
559
+ if (h.key && h.cert) return {
560
+ key: h.key,
561
+ cert: h.cert
562
+ };
563
+ if (h.keyfile && h.certfile) {
564
+ try {
565
+ if (fs.existsSync(h.keyfile) && fs.existsSync(h.certfile)) {
566
+ return {
567
+ key: fs.readFileSync(h.keyfile),
568
+ cert: fs.readFileSync(h.certfile)
569
+ };
570
+ }
571
+ } catch {
572
+ /* swallow */
573
+ }
574
+ return false;
575
+ }
576
+ }
577
+ if (ds.keyfile && ds.certfile) {
578
+ try {
579
+ if (fs.existsSync(ds.keyfile) && fs.existsSync(ds.certfile)) {
580
+ return {
581
+ key: fs.readFileSync(ds.keyfile),
582
+ cert: fs.readFileSync(ds.certfile)
583
+ };
584
+ }
585
+ } catch {
586
+ /* swallow */
587
+ }
588
+ }
589
+ return false;
590
+ }
591
+ makeHttpsCfg(config);
592
+
593
+ // ---- flags -----------------------------------------------------------------
594
+ ({
595
+ useSass: config.styles?.type === "sass",
596
+ useTailwind: config.styles?.type === "tailwind"
597
+ });
598
+
599
+ // figma-exporter.mjs
600
+ // Usage: import exporter from './figma-exporter.mjs'; const json = await exporter('<FILE_KEY>');
601
+ // Requires: FIGMA_TOKEN env (or hardcode TOKEN below)
602
+ const tag$1 = chalk.cyan('[design-system]');
603
+
604
+ // ---------- utils ----------
605
+ const toKebab = s => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
606
+ const hex2 = n => n.toString(16).padStart(2, "0");
607
+ function rgba255(c) {
608
+ const a = typeof c.a === "number" ? c.a : 1;
609
+ return {
610
+ r: Math.round(c.r * 255),
611
+ g: Math.round(c.g * 255),
612
+ b: Math.round(c.b * 255),
613
+ a
614
+ };
615
+ }
616
+ function colorToHexRgba(c) {
617
+ const r = Math.round(c.r * 255),
618
+ g = Math.round(c.g * 255),
619
+ b = Math.round(c.b * 255);
620
+ const a = typeof c.a === "number" ? c.a : 1;
621
+ return `#${hex2(r)}${hex2(g)}${hex2(b)}${a < 1 ? hex2(Math.round(a * 255)) : ""}`;
622
+ }
623
+
624
+ // ---------- text metric shims ----------
625
+ function readLineHeight(style) {
626
+ const s = style || {};
627
+ // 1) New object form
628
+ if (s.lineHeight && typeof s.lineHeight === "object") {
629
+ const u = s.lineHeight.unit || s.lineHeight.lineHeightUnit;
630
+ const v = s.lineHeight.value || s.lineHeight.lineHeightPx || s.lineHeight.percent;
631
+ if ((u === "PERCENT" || u === "FONT_SIZE_%") && typeof v === "number" && v > 0) {
632
+ return {
633
+ unit: "%",
634
+ value: v
635
+ };
636
+ }
637
+ if (u === "PIXELS" && typeof v === "number" && v > 0) {
638
+ return {
639
+ unit: "px",
640
+ value: v
641
+ };
642
+ }
643
+ }
644
+
645
+ // 2) Legacy percent fields (prefer percent over px)
646
+ if (typeof s.lineHeightPercentFontSize === "number" && s.lineHeightPercentFontSize > 0) {
647
+ return {
648
+ unit: "%",
649
+ value: s.lineHeightPercentFontSize
650
+ };
651
+ }
652
+ if (typeof s.lineHeightPercent === "number" && s.lineHeightPercent > 0) {
653
+ return {
654
+ unit: "%",
655
+ value: s.lineHeightPercent
656
+ };
657
+ }
658
+
659
+ // 3) If unit is percent but only px is present, derive percent
660
+ if ((s.lineHeightUnit === "FONT_SIZE_%" || s.lineHeightUnit === "PERCENT") && typeof s.lineHeightPx === "number" && typeof s.fontSize === "number" && s.fontSize > 0) {
661
+ const pct = s.lineHeightPx / s.fontSize * 100;
662
+ return {
663
+ unit: "%",
664
+ value: Math.round(pct * 100) / 100
665
+ };
666
+ }
667
+
668
+ // 4) Plain px
669
+ if (typeof s.lineHeightPx === "number" && s.lineHeightPx > 0) {
670
+ return {
671
+ unit: "px",
672
+ value: s.lineHeightPx
673
+ };
674
+ }
675
+
676
+ // 5) Last-resort derivation from numeric lineHeight + fontSize
677
+ if (typeof s.lineHeight === "number" && typeof s.fontSize === "number" && s.fontSize > 0) {
678
+ const pct = s.lineHeight / s.fontSize * 100;
679
+ return {
680
+ unit: "%",
681
+ value: Math.round(pct * 100) / 100
682
+ };
683
+ }
684
+ return null;
685
+ }
686
+ function readLetterSpacing(style) {
687
+ if (style?.letterSpacing && typeof style.letterSpacing === "object") {
688
+ const u = style.letterSpacing.unit || style.letterSpacingUnit;
689
+ const v = style.letterSpacing.value || style.letterSpacingPx || style.letterSpacing.percent;
690
+ if (u === "PIXELS" && typeof v === "number") return {
691
+ unit: "px",
692
+ value: v
693
+ };
694
+ if (u === "PERCENT" && typeof v === "number") return {
695
+ unit: "%",
696
+ value: v
697
+ };
698
+ }
699
+ if (typeof style?.letterSpacing === "number" && style?.letterSpacingUnit === "PIXELS") return {
700
+ unit: "px",
701
+ value: style.letterSpacing
702
+ };
703
+ if (typeof style?.letterSpacingPercent === "number") return {
704
+ unit: "%",
705
+ value: style.letterSpacingPercent
706
+ };
707
+ if (typeof style?.letterSpacingPercentFontSize === "number") return {
708
+ unit: "%",
709
+ value: style.letterSpacingPercentFontSize
710
+ };
711
+ return null;
712
+ }
713
+
714
+ // ---------- HTTP ----------
715
+ async function jget(url) {
716
+ const HEADERS = {
717
+ "X-Figma-Token": paths.figma.key,
718
+ "Content-Type": "application/json"
719
+ };
720
+ const r = await fetch(url, {
721
+ headers: HEADERS
722
+ });
723
+ if (!r.ok) {
724
+ const body = await r.text().catch(() => "");
725
+ throw new Error(`${r.status} ${r.statusText} – ${url}\n${body}`);
726
+ }
727
+ return r.json();
728
+ }
729
+
730
+ // ---------- variables (optional; Enterprise + file_variables:read) ----------
731
+ async function fetchVariablesLocal(fileKey) {
732
+ console.log(`${tag$1} fetching variables...`);
733
+ try {
734
+ const res = await jget(`https://api.figma.com/v1/files/${fileKey}/variables/local`);
735
+ const meta = res?.meta ?? {};
736
+ const vars = meta.variables ?? {};
737
+ const cols = meta.variableCollections ?? {};
738
+ const byCollection = {};
739
+ for (const colId of Object.keys(cols)) {
740
+ const c = cols[colId];
741
+ const colKey = toKebab(c.name);
742
+ byCollection[colKey] = {
743
+ id: c.id,
744
+ name: c.name,
745
+ modes: (c.modes || []).map(m => ({
746
+ id: m.modeId,
747
+ name: m.name,
748
+ key: toKebab(m.name)
749
+ })),
750
+ tokens: {}
751
+ };
752
+ }
753
+ function resolveAlias(value, depth = 0) {
754
+ if (!value || typeof value !== "object") return value;
755
+ if (value.type === "VARIABLE_ALIAS" && value.id && depth < 20) {
756
+ const aliased = vars[value.id];
757
+ if (!aliased) return value;
758
+ const coll = cols[aliased.variableCollectionId];
759
+ const modeOrder = (coll?.modes || []).map(m => m.modeId);
760
+ const pick = modeOrder[0] || Object.keys(aliased.valuesByMode || {})[0];
761
+ return resolveAlias(aliased.valuesByMode?.[pick], depth + 1);
762
+ }
763
+ return value;
764
+ }
765
+ for (const vId of Object.keys(vars)) {
766
+ const v = vars[vId];
767
+ const coll = cols[v.variableCollectionId];
768
+ if (!coll) continue;
769
+ const colKey = toKebab(coll.name);
770
+ const nameKey = toKebab(v.name);
771
+ const token = {
772
+ id: v.id,
773
+ name: v.name,
774
+ type: v.resolvedType,
775
+ values: {}
776
+ };
777
+ for (const [modeId, raw] of Object.entries(v.valuesByMode || {})) {
778
+ const mode = (coll.modes || []).find(m => m.modeId === modeId);
779
+ if (!mode) continue;
780
+ const val = resolveAlias(raw);
781
+ if (v.resolvedType === "FLOAT" || v.resolvedType === "BOOLEAN" || v.resolvedType === "STRING") {
782
+ token.values[mode.name] = val ?? null;
783
+ } else if (v.resolvedType === "COLOR" && val && typeof val === "object" && "r" in val) {
784
+ token.values[mode.name] = {
785
+ hex: colorToHexRgba(val),
786
+ rgba: rgba255(val)
787
+ };
788
+ } else {
789
+ token.values[mode.name] = null;
790
+ }
791
+ }
792
+ byCollection[colKey].tokens[nameKey] = token;
793
+ }
794
+ return byCollection;
795
+ } catch {
796
+ return {};
797
+ }
798
+ }
799
+
800
+ // ---------- styles (two sources) ----------
801
+ async function fetchStyleNodeIds_fromFile(fileKey) {
802
+ const file = await jget(`https://api.figma.com/v1/files/${fileKey}`);
803
+ return file?.styles || {};
804
+ }
805
+ async function fetchStyleNodeIds_published(fileKey) {
806
+ try {
807
+ const res = await jget(`https://api.figma.com/v1/files/${fileKey}/styles`);
808
+ const arr = Array.isArray(res?.meta?.styles) ? res.meta.styles : [];
809
+ const map = {};
810
+ for (const s of arr) map[s.key] = s;
811
+ return map;
812
+ } catch {
813
+ return {};
814
+ }
815
+ }
816
+ async function fetchMergedStyleNodeIds(fileKey) {
817
+ const a = await fetchStyleNodeIds_fromFile(fileKey);
818
+ const b = await fetchStyleNodeIds_published(fileKey);
819
+ return {
820
+ ...a,
821
+ ...b
822
+ };
823
+ }
824
+
825
+ // ---------- component + component-set discovery ----------
826
+ function walk(node, fn) {
827
+ if (!node || typeof node !== 'object') return;
828
+ fn(node);
829
+ const children = Array.isArray(node.children) ? node.children : [];
830
+ for (const c of children) walk(c, fn);
831
+ }
832
+ async function fetchComponentAndSetIds(fileKey) {
833
+ console.log(`${tag$1} scanning for components...`);
834
+ const file = await jget(`https://api.figma.com/v1/files/${fileKey}`);
835
+ const ids = new Set();
836
+ walk(file?.document, n => {
837
+ if (n?.type === 'COMPONENT' || n?.type === 'COMPONENT_SET') {
838
+ ids.add(n.id);
839
+ }
840
+ });
841
+ return Array.from(ids);
842
+ }
843
+
844
+ // ---------- nodes (batched) ----------
845
+ async function fetchNodesByIds(fileKey, allIds) {
846
+ console.log(`${tag$1} reading nodes by id...`);
847
+ const ids = Array.from(new Set((allIds || []).filter(Boolean).map(String).map(s => s.trim()).filter(s => s.length > 0)));
848
+ const out = [];
849
+ const CHUNK = 50;
850
+ for (let i = 0; i < ids.length; i += CHUNK) {
851
+ const slice = ids.slice(i, i + CHUNK);
852
+ const qs = slice.map(encodeURIComponent).join(",");
853
+ const url = `https://api.figma.com/v1/files/${fileKey}/nodes?ids=${qs}`;
854
+ const res = await jget(url);
855
+ const nodes = res?.nodes || {};
856
+ for (const k of Object.keys(nodes)) {
857
+ const doc = nodes[k]?.document;
858
+ if (doc) out.push(doc);
859
+ }
860
+ }
861
+ return out;
862
+ }
863
+
864
+ // ---------- text props derivation ----------
865
+ function weightFromStyleName(s) {
866
+ if (!s) return undefined;
867
+ const x = s.toLowerCase();
868
+ if (/\b(100|thin)\b/.test(x)) return 100;
869
+ if (/\b(200|extralight|ultralight)\b/.test(x)) return 200;
870
+ if (/\b(300|light)\b/.test(x)) return 300;
871
+ if (/\b(400|regular|book|normal)\b/.test(x)) return 400;
872
+ if (/\b(500|medium)\b/.test(x)) return 500;
873
+ if (/\b(600|semibold|demibold)\b/.test(x)) return 600;
874
+ if (/\b(700|bold)\b/.test(x)) return 700;
875
+ if (/\b(800|extrabold|ultrabold|heavy)\b/.test(x)) return 800;
876
+ if (/\b(900|black|heavy)\b/.test(x)) return 900;
877
+ const m = s.match(/\b(100|200|300|400|500|600|700|800|900)\b/);
878
+ return m ? Number(m[1]) : undefined;
879
+ }
880
+ function deriveFontStyle(style) {
881
+ if (typeof style?.fontStyle === "string" && style.fontStyle.trim()) return style.fontStyle;
882
+ const ps = style?.fontPostScriptName || "";
883
+ const tail = ps.includes("-") ? ps.split("-")[1] : ps;
884
+ if (/italic/i.test(tail)) return "Italic";
885
+ if (/oblique/i.test(tail)) return "Oblique";
886
+ if (/regular|book|normal/i.test(tail)) return "Regular";
887
+ const token = (tail.match(/[A-Za-z]+$/) || [])[0];
888
+ return token ? token[0].toUpperCase() + token.slice(1) : undefined;
889
+ }
890
+ function readTextProps(n) {
891
+ const s = n.style || n;
892
+ const styleName = s.fontStyle || s.fontPostScriptName || "";
893
+ return {
894
+ fontFamily: s.fontFamily || n.fontFamily,
895
+ fontStyle: deriveFontStyle(s),
896
+ fontWeight: typeof s.fontWeight === "number" ? s.fontWeight : weightFromStyleName(styleName),
897
+ fontSize: s.fontSize ?? n.fontSize,
898
+ lineHeight: readLineHeight(s),
899
+ letterSpacing: readLetterSpacing(s),
900
+ paragraphSpacing: typeof s.paragraphSpacing === "number" ? s.paragraphSpacing : typeof n.paragraphSpacing === "number" ? n.paragraphSpacing : 0,
901
+ textCase: s.textCase || n.textCase || "ORIGINAL",
902
+ textDecoration: s.textDecoration || n.textDecoration || "NONE"
903
+ };
904
+ }
905
+
906
+ // ---------- normalizers: styles ----------
907
+ function emitPaintStylesJson(styleMetas, nodes) {
908
+ const byId = {};
909
+ for (const n of nodes) {
910
+ const paints = Array.isArray(n.paints) ? n.paints : Array.isArray(n.fills) ? n.fills : null;
911
+ if (!paints) continue;
912
+ const meta = Object.values(styleMetas).find(m => m.node_id === n.id && m.style_type === "FILL");
913
+ if (!meta) continue;
914
+ const solids = [];
915
+ for (const p of paints) {
916
+ if (p?.type !== "SOLID" || p?.visible === false) continue;
917
+ const a = typeof p.opacity === "number" ? p.opacity : 1;
918
+ solids.push({
919
+ hex: colorToHexRgba({
920
+ ...p.color,
921
+ a
922
+ }),
923
+ rgba: rgba255({
924
+ ...p.color,
925
+ a
926
+ })
927
+ });
928
+ }
929
+ if (solids.length) {
930
+ const key = toKebab(meta.name);
931
+ byId[key] = {
932
+ id: meta.key || meta.node_id,
933
+ name: meta.name,
934
+ paints: solids
935
+ };
936
+ }
937
+ }
938
+ console.log(`${tag$1} finished reading paint styles`);
939
+ return byId;
940
+ }
941
+ function emitTextStylesJson(styleMetas, nodes) {
942
+ const out = {};
943
+ for (const n of nodes) {
944
+ const meta = Object.values(styleMetas).find(m => m.node_id === n.id && m.style_type === "TEXT");
945
+ if (!meta) continue;
946
+ const key = toKebab(meta.name);
947
+ const p = readTextProps(n);
948
+ out[key] = {
949
+ id: meta.key || meta.node_id,
950
+ name: meta.name,
951
+ fontFamily: p.fontFamily,
952
+ fontStyle: p.fontStyle,
953
+ fontWeight: p.fontWeight,
954
+ fontSize: p.fontSize,
955
+ lineHeight: p.lineHeight,
956
+ letterSpacing: p.letterSpacing,
957
+ paragraphSpacing: p.paragraphSpacing,
958
+ textCase: p.textCase,
959
+ textDecoration: p.textDecoration
960
+ };
961
+ }
962
+ console.log(`${tag$1} finished reading text styles`);
963
+ return out;
964
+ }
965
+ function effectToJson(e) {
966
+ if (e?.visible === false) return null;
967
+ if (e?.type === "DROP_SHADOW" || e?.type === "INNER_SHADOW") {
968
+ const spread = typeof e.spread === "number" ? Math.round(e.spread) : 0;
969
+ return {
970
+ kind: e.type,
971
+ offset: {
972
+ x: Math.round(e.offset?.x ?? 0),
973
+ y: Math.round(e.offset?.y ?? 0)
974
+ },
975
+ radius: Math.round(e.radius ?? 0),
976
+ spread,
977
+ color: {
978
+ hex: colorToHexRgba(e.color),
979
+ rgba: rgba255(e.color)
980
+ }
981
+ };
982
+ }
983
+ if (e?.type === "LAYER_BLUR" || e?.type === "BACKGROUND_BLUR") {
984
+ return {
985
+ kind: e.type,
986
+ radius: Math.round(e.radius ?? 0)
987
+ };
988
+ }
989
+ return null;
990
+ }
991
+ function emitEffectStylesJson(styleMetas, nodes) {
992
+ const out = {};
993
+ for (const n of nodes) {
994
+ const meta = Object.values(styleMetas).find(m => m.node_id === n.id && m.style_type === "EFFECT");
995
+ if (!meta) continue;
996
+ const list = [];
997
+ const effects = Array.isArray(n.effects) ? n.effects : [];
998
+ for (const e of effects) {
999
+ const obj = effectToJson(e);
1000
+ if (obj) list.push(obj);
1001
+ }
1002
+ if (list.length) {
1003
+ const key = toKebab(meta.name);
1004
+ out[key] = {
1005
+ id: meta.key || meta.node_id,
1006
+ name: meta.name,
1007
+ effects: list
1008
+ };
1009
+ }
1010
+ }
1011
+ console.log(`${tag$1} finished reading effect styles`);
1012
+ return out;
1013
+ }
1014
+
1015
+ // ---------- normalizers: component properties ----------
1016
+ function normalizeComponentPropDef(def) {
1017
+ // Figma REST: { name, type, defaultValue, variantOptions?, preferredValues? ... }
1018
+ const base = {
1019
+ name: def?.name ?? '',
1020
+ type: def?.type ?? 'TEXT'
1021
+ };
1022
+ if (def?.type === 'TEXT') {
1023
+ return {
1024
+ ...base,
1025
+ default: def?.defaultValue ?? ''
1026
+ };
1027
+ }
1028
+ if (def?.type === 'BOOLEAN') {
1029
+ return {
1030
+ ...base,
1031
+ default: !!def?.defaultValue
1032
+ };
1033
+ }
1034
+ if (def?.type === 'VARIANT') {
1035
+ const opts = Array.isArray(def?.variantOptions) ? def.variantOptions : [];
1036
+ const defVal = typeof def?.defaultValue === 'string' ? def.defaultValue : opts[0] ?? '';
1037
+ return {
1038
+ ...base,
1039
+ options: opts,
1040
+ default: defVal
1041
+ };
1042
+ }
1043
+ if (def?.type === 'INSTANCE_SWAP') {
1044
+ // preferredValues is an array of component IDs/keys; Figma may return "preferredValues"
1045
+ const prefs = Array.isArray(def?.preferredValues) ? def.preferredValues : [];
1046
+ const defVal = def?.defaultValue ?? null;
1047
+ return {
1048
+ ...base,
1049
+ preferredValues: prefs,
1050
+ default: defVal
1051
+ };
1052
+ }
1053
+ return {
1054
+ ...base,
1055
+ default: def?.defaultValue ?? null
1056
+ };
1057
+ }
1058
+ function emitComponentPropsJson(nodes) {
1059
+ const out = {};
1060
+ for (const n of nodes) {
1061
+ if (n?.type !== 'COMPONENT' && n?.type !== 'COMPONENT_SET') continue;
1062
+ const defs = n.componentPropertyDefinitions || {};
1063
+ const props = {};
1064
+ for (const propId of Object.keys(defs)) {
1065
+ const def = defs[propId];
1066
+ const key = toKebab(def?.name ?? propId);
1067
+ props[key] = {
1068
+ id: propId,
1069
+ ...normalizeComponentPropDef(def)
1070
+ };
1071
+ }
1072
+ if (Object.keys(props).length > 0) {
1073
+ const key = toKebab(n.name || n.id);
1074
+ out[key] = {
1075
+ id: n.id,
1076
+ name: n.name,
1077
+ kind: n.type,
1078
+ // COMPONENT or COMPONENT_SET
1079
+ props
1080
+ };
1081
+ }
1082
+ }
1083
+ console.log(`${tag$1} finished reading component properties`);
1084
+ return out;
1085
+ }
1086
+
1087
+ // ---------- exporter ----------
1088
+ const exporter = async function (fileKey) {
1089
+ if (!fileKey) throw new Error("Missing FILE_KEY");
1090
+ console.log(`${tag$1} reading from figma`);
1091
+ const variablesByCollection = await fetchVariablesLocal(fileKey);
1092
+ const stylesMap = await fetchMergedStyleNodeIds(fileKey);
1093
+ const styleNodeIds = Object.values(stylesMap).filter(s => s && (s.style_type === "FILL" || s.style_type === "TEXT" || s.style_type === "EFFECT")).map(s => s.node_id);
1094
+ const componentIds = await fetchComponentAndSetIds(fileKey);
1095
+
1096
+ // Early-return case when no styles and no components
1097
+ if (styleNodeIds.length === 0 && componentIds.length === 0) {
1098
+ return {
1099
+ paint: {},
1100
+ text: {},
1101
+ effect: {},
1102
+ variables: variablesByCollection,
1103
+ components: {}
1104
+ };
1105
+ }
1106
+ const styleNodes = styleNodeIds.length ? await fetchNodesByIds(fileKey, styleNodeIds) : [];
1107
+ const componentNodes = componentIds.length ? await fetchNodesByIds(fileKey, componentIds) : [];
1108
+ const paint = emitPaintStylesJson(stylesMap, styleNodes);
1109
+ const text = emitTextStylesJson(stylesMap, styleNodes);
1110
+ const effect = emitEffectStylesJson(stylesMap, styleNodes);
1111
+ const components = emitComponentPropsJson(componentNodes);
1112
+ return {
1113
+ paint,
1114
+ text,
1115
+ effect,
1116
+ variables: variablesByCollection,
1117
+ components
1118
+ };
1119
+ };
1120
+
1121
+ const tag = chalk.cyan('[design-system]');
1122
+
1123
+ /*
1124
+ * This needs to genrate:
1125
+ * variables:
1126
+ * color.css
1127
+ * font.css
1128
+ * radius.css
1129
+ * shadow.css
1130
+ * typography.css
1131
+ * core:
1132
+ * body.css
1133
+ * headline.css
1134
+ */
1135
+
1136
+ async function readFigma() {
1137
+ const figmaStyles = await exporter(paths.figma.file);
1138
+ return figmaStyles;
1139
+ }
1140
+ async function generateTailwind() {
1141
+ console.log(`${tag} generating tailwind..`);
1142
+ const figmaStyles = await readFigma(); // await fix
1143
+
1144
+ await generateColors(figmaStyles.paint);
1145
+ await generateTypography(figmaStyles.text);
1146
+
1147
+ // core utilities
1148
+ await generateBodies(figmaStyles.text);
1149
+ await generateHeadlines(figmaStyles.text);
1150
+ await generateButtons();
1151
+ console.log(`${tag} finished generating tailwind`);
1152
+ }
1153
+
1154
+ // ------- VARIABLES -------
1155
+ async function generateColors(data) {
1156
+ console.log(`${tag} generating colors...`);
1157
+ const outDir = path.join(paths.styles, 'config');
1158
+ await fs$1.mkdir(outDir, {
1159
+ recursive: true
1160
+ });
1161
+ const lines = [];
1162
+ for (const [key, entry] of Object.entries(data ?? {})) {
1163
+ const paints = entry?.paints ?? [];
1164
+ const p = paints[0] ?? {};
1165
+ // exporter already normalizes; prefer .hex/.value, else stringify fallback
1166
+ const value = (p.hex && String(p.hex)) ?? (p.value && String(p.value)) ?? (p.css && String(p.css)) ?? String(p);
1167
+ if (!value) {
1168
+ continue;
1169
+ }
1170
+ lines.push(` --color-${key}: ${value};`);
1171
+ }
1172
+ const css = `/* Auto-generated. Do not edit by hand. */
1173
+ @theme {
1174
+ ${lines.join('\n')}
1175
+ }
1176
+ `;
1177
+ const filePath = path.join(outDir, 'color.css');
1178
+ await fs$1.writeFile(filePath, css, 'utf8');
1179
+ console.log(`${tag} finished generating colors`);
1180
+ }
1181
+ async function generateButtons() {
1182
+ //shadow.css
1183
+ console.log(`${tag} generating buttons...`);
1184
+ const outDir = path.join(paths.styles, 'core');
1185
+ const getButtons = await parseButtons();
1186
+ const buttons = getButtons.map(b => {
1187
+ return `
1188
+ @utility ${b} {
1189
+
1190
+ }`;
1191
+ }).join('\n');
1192
+ const out = `/* Auto-generated. Do not edit by hand. */
1193
+ ${buttons.join('\n\n')}
1194
+ `;
1195
+ await fs$1.writeFile(path.join(outDir, 'button-variants.css'), out, 'utf8');
1196
+ console.log(`${tag} finished generating buttons`);
1197
+ }
1198
+ async function generateTypography(data) {
1199
+ console.log(`${tag} generating typography...`);
1200
+ const outDir = path.join(paths.styles, 'config');
1201
+ await fs$1.mkdir(outDir, {
1202
+ recursive: true
1203
+ });
1204
+ const lines = [];
1205
+ for (const [key, entry] of Object.entries(data ?? {})) {
1206
+ const fontSize = entry?.fontSize ?? null;
1207
+ const lh = entry?.lineHeight ?? {};
1208
+ const lhVal = lh?.value ?? null;
1209
+ const lhUnit = lh?.unit ?? '';
1210
+ if (fontSize) lines.push(` --text-${key}: ${fontSize}px;`);
1211
+ if (lhVal) lines.push(` --leading-${key}: ${lhVal}${lhUnit};`);
1212
+ }
1213
+ const css = `/* Auto-generated. Do not edit by hand. */
1214
+ @theme {
1215
+ ${lines.join('\n')}
1216
+ }
1217
+ `;
1218
+ const filePath = path.join(outDir, 'typography.css');
1219
+ await fs$1.writeFile(filePath, css, 'utf8');
1220
+ console.log(`${tag} finished generating typography`);
1221
+ }
1222
+
1223
+ // ------- CORE (fixed grouping incl. semibold) -------
1224
+
1225
+ async function generateBodies(textMap = {}) {
1226
+ console.log(`${tag} generating bodies...`);
1227
+ const outDir = path.join(paths.styles, 'core');
1228
+ await fs$1.mkdir(outDir, {
1229
+ recursive: true
1230
+ });
1231
+ const keys = Object.keys(textMap || {}).filter(k => k.startsWith('body-'));
1232
+
1233
+ // Normalize base: remove a single "mobile" segment after "body-"
1234
+ function baseKey(k) {
1235
+ const parts = k.split('-');
1236
+ const idx = parts.indexOf('mobile');
1237
+ if (idx !== -1) parts.splice(idx, 1);
1238
+ return parts.join('-');
1239
+ }
1240
+
1241
+ /** @type {Map<string,{desktop:string|null,mobile:string|null}>} */
1242
+ const groups = new Map();
1243
+ for (const k of keys) {
1244
+ const base = baseKey(k);
1245
+ const g = groups.get(base) ?? {
1246
+ desktop: null,
1247
+ mobile: null
1248
+ };
1249
+ if (k.includes('-mobile-')) g.mobile = k;else g.desktop = k;
1250
+ groups.set(base, g);
1251
+ }
1252
+ const blocks = [];
1253
+ for (const [base, {
1254
+ desktop,
1255
+ mobile
1256
+ }] of [...groups.entries()].sort()) {
1257
+ const hasMobile = !!mobile;
1258
+ const hasDesktop = !!desktop;
1259
+ const mobileRef = hasMobile ? mobile : desktop;
1260
+ const desktopUpgrade = hasMobile && hasDesktop ? `\n @apply desktop-xs:text-${desktop} desktop-xs:leading-${desktop};` : ``;
1261
+
1262
+ // collapse “body-body-x” → “body-x”
1263
+ const isDouble = base.startsWith('body-body-');
1264
+ const utilName = isDouble ? base.replace(/^body-body-/, 'body-') : base;
1265
+
1266
+ // main utility
1267
+ blocks.push(`@utility ${utilName} {
1268
+ @apply text-${mobileRef} leading-${mobileRef};${desktopUpgrade}
1269
+ }`);
1270
+
1271
+ // only add numeric alias if no collapse happened
1272
+ if (!isDouble) {
1273
+ const m = /^body-body-(\d+)$/i.exec(base);
1274
+ if (m) {
1275
+ const n = m[1];
1276
+ blocks.push(`@utility body-${n} {
1277
+ @apply text-${mobileRef} leading-${mobileRef};${desktopUpgrade}
1278
+ }`);
1279
+ }
1280
+ }
1281
+ }
1282
+ const out = `/* Auto-generated. Do not edit by hand. */
1283
+ ${blocks.join('\n\n')}
1284
+ `;
1285
+ await fs$1.writeFile(path.join(outDir, 'body.css'), out, 'utf8');
1286
+ console.log(`${tag} finished generating bodies`);
1287
+ }
1288
+ async function generateHeadlines(textMap = {}) {
1289
+ console.log(`${tag} generating headlines...`);
1290
+ const outDir = path.join(paths.styles, 'core');
1291
+ await fs$1.mkdir(outDir, {
1292
+ recursive: true
1293
+ });
1294
+ const keys = Object.keys(textMap || {}).filter(k => k.startsWith('headline-'));
1295
+
1296
+ // collect ids present in either mobile or desktop
1297
+ const ids = new Set();
1298
+ for (const k of keys) {
1299
+ let m = /^headline-h(\d+)$/i.exec(k);
1300
+ if (m) {
1301
+ ids.add(+m[1]);
1302
+ continue;
1303
+ }
1304
+ m = /^headline-mobile-h(\d+)$/i.exec(k);
1305
+ if (m) {
1306
+ ids.add(+m[1]);
1307
+ continue;
1308
+ }
1309
+ }
1310
+ const blocks = [];
1311
+ for (const n of [...ids].sort((a, b) => a - b)) {
1312
+ const mobileKey = keys.includes(`headline-mobile-h${n}`) ? `headline-mobile-h${n}` : `headline-h${n}`;
1313
+ const hasDesktop = keys.includes(`headline-h${n}`);
1314
+ const desktopKey = hasDesktop ? `headline-h${n}` : null;
1315
+ const desktopUpgrade = hasDesktop ? `\n @apply desktop-xs:text-${desktopKey} desktop-xs:leading-${desktopKey};` : ``;
1316
+ blocks.push(`@utility headline-h${n} {
1317
+ @apply text-${mobileKey} leading-${mobileKey};
1318
+ @apply font-default;${desktopUpgrade}
1319
+ }`);
1320
+ }
1321
+ const out = `/* Auto-generated. Do not edit by hand. */
1322
+ ${blocks.join('\n\n')}
1323
+ `;
1324
+ await fs$1.writeFile(path.join(outDir, 'headline.css'), out, 'utf8');
1325
+ console.log(`${tag} finished generating headlines`);
1326
+ }
1327
+
1328
+ var generateTailwind$1 = /*#__PURE__*/Object.freeze({
1329
+ __proto__: null,
1330
+ default: generateTailwind,
1331
+ generateTailwind: generateTailwind,
1332
+ readFigma: readFigma
1333
+ });
1334
+
1335
+ export { generateTailwind$1 as g, parseButtons as p, readFigma as r };