@vertis-components/components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+
5
+ // Auto-load theme.js from the consumer's project root.
6
+
7
+ // Usage in consumer's tailwind.config.js:
8
+
9
+ // const { loadTheme } = require('@vertis/components/tailwind-helper');
10
+ // const theme = loadTheme();
11
+
12
+ // module.exports = {
13
+ // darkMode: 'class',
14
+ // content: [
15
+ // './src/**/*.{js,ts,jsx,tsx}',
16
+ // './node_modules/@vertis/components/dist/**/*.{js,mjs}',
17
+ // ],
18
+ // theme: {
19
+ // extend: {
20
+ // colors: theme.colors,
21
+ // spacing: theme.spacing,
22
+ // borderRadius: theme.borderRadius,
23
+ // },
24
+ // },
25
+ // };
26
+
27
+ function loadTheme() {
28
+ // Find project root (where node_modules is)
29
+ let currentDir = __dirname;
30
+
31
+ // Walk up until we find node_modules or hit root
32
+ while (currentDir !== path.dirname(currentDir)) {
33
+ const nodeModulesPath = path.join(currentDir, 'node_modules');
34
+ if (fs.existsSync(nodeModulesPath)) {
35
+ // Found node_modules, so parent is project root
36
+ currentDir = path.dirname(nodeModulesPath);
37
+ break;
38
+ }
39
+ currentDir = path.dirname(currentDir);
40
+ }
41
+
42
+ const themePath = path.join(currentDir, 'theme.js');
43
+
44
+ if (!fs.existsSync(themePath)) {
45
+ console.warn('\n⚠️ No theme.js found in project root.');
46
+ console.warn(' Run "npx vertis-sync-figma && npx vertis-generate-theme" to create it.\n');
47
+ console.warn(' Falling back to empty theme (components may not display correctly).\n');
48
+
49
+ return {
50
+ colors: {},
51
+ spacing: {},
52
+ borderRadius: {},
53
+ };
54
+ }
55
+
56
+ console.log(`✅ Loaded theme from: ${themePath}`);
57
+ return require(themePath);
58
+ }
59
+
60
+ module.exports = { loadTheme };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@vertis-components/components",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.mjs",
6
+ "types": "./dist/index.d.ts",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./styles.css": "./dist/styles.css",
15
+ "./tailwind-helper": "./dist/tailwind-helper.js",
16
+ "./scripts/*": "./scripts/*"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "scripts",
21
+ "README.md",
22
+ "USAGE.md"
23
+ ],
24
+ "bin": {
25
+ "vertis-sync-figma": "./scripts/sync-figma.js",
26
+ "vertis-generate-theme": "./scripts/convert-tailwind.js"
27
+ },
28
+ "dependencies": {
29
+ "axios": "^1.6.0",
30
+ "clsx": "^2.1.0",
31
+ "dotenv": "^16.0.0",
32
+ "react-icons": "^5.5.0",
33
+ "swiper": "^12.0.3",
34
+ "tailwind-merge": "^2.2.0"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup && node scripts/build-css.js && cp tailwind-helper.js dist/",
38
+ "dev": "tsup --watch",
39
+ "lint": "eslint \"src/**/*.ts*\"",
40
+ "storybook": "storybook dev -p 6006 -h 0.0.0.0 --ci",
41
+ "build-storybook": "storybook build"
42
+ },
43
+ "peerDependencies": {
44
+ "react": "^18.0.0",
45
+ "react-dom": "^18.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@storybook/addon-essentials": "^7.6.0",
49
+ "@storybook/addon-interactions": "^7.6.0",
50
+ "@storybook/addon-links": "^7.6.0",
51
+ "@storybook/blocks": "^7.6.0",
52
+ "@storybook/react": "^7.6.0",
53
+ "@storybook/react-vite": "^7.6.0",
54
+ "@storybook/testing-library": "^0.2.2",
55
+ "@types/react": "^18.0.0",
56
+ "@types/react-dom": "^18.0.0",
57
+ "autoprefixer": "^10.0.0",
58
+ "postcss": "^8.0.0",
59
+ "react": "^18.0.0",
60
+ "react-dom": "^18.0.0",
61
+ "storybook": "^7.6.0",
62
+ "tailwindcss": "^3.0.0",
63
+ "tsup": "^8.0.0",
64
+ "typescript": "^5.0.0"
65
+ }
66
+ }
@@ -0,0 +1,54 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const postcss = require('postcss');
4
+ const tailwindcss = require('tailwindcss');
5
+ const autoprefixer = require('autoprefixer');
6
+
7
+ async function buildCSS() {
8
+ console.log('🔄 Building styles.css with default theme...\n');
9
+
10
+ // Read source CSS files
11
+ const indexCSS = fs.readFileSync(path.join(__dirname, '../src/index.css'), 'utf-8');
12
+ const carouselCSS = fs.readFileSync(path.join(__dirname, '../src/components/Carousel.css'), 'utf-8');
13
+
14
+ // Combine CSS
15
+ const combinedCSS = indexCSS + '\n\n' + carouselCSS;
16
+
17
+ // Use root default theme
18
+ const defaultTheme = require(path.join(__dirname, '../../../theme.js'));
19
+
20
+ // Create temporary Tailwind config with default theme
21
+ const tailwindConfig = {
22
+ content: [
23
+ { raw: '', extension: 'html' } // No content scanning needed
24
+ ],
25
+ darkMode: 'class',
26
+ theme: {
27
+ extend: {
28
+ colors: defaultTheme.colors,
29
+ spacing: defaultTheme.spacing,
30
+ borderRadius: defaultTheme.borderRadius,
31
+ },
32
+ },
33
+ };
34
+
35
+ // Process CSS
36
+ const result = await postcss([
37
+ tailwindcss(tailwindConfig),
38
+ autoprefixer,
39
+ ]).process(combinedCSS, {
40
+ from: undefined,
41
+ });
42
+
43
+ // Ensure dist directory exists
44
+ const distDir = path.join(__dirname, '../dist');
45
+ if (!fs.existsSync(distDir)) {
46
+ fs.mkdirSync(distDir, { recursive: true });
47
+ }
48
+
49
+ // Write output
50
+ fs.writeFileSync(path.join(distDir, 'styles.css'), result.css, 'utf-8');
51
+ console.log('✅ Built dist/styles.css with default theme');
52
+ }
53
+
54
+ buildCSS().catch(console.error);
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Compares two colors for approximate equality since converting between Figma RGBA objects (from 0 -> 1) and
3
+ * hex colors can result in slight differences.
4
+ */
5
+ function colorApproximatelyEqual(colorA, colorB) {
6
+ return rgbToHex(colorA) === rgbToHex(colorB)
7
+ }
8
+
9
+ function parseColor(color) {
10
+ color = color.trim()
11
+ const hexRegex = /^#([A-Fa-f0-9]{6})([A-Fa-f0-9]{2}){0,1}$/
12
+ const hexShorthandRegex = /^#([A-Fa-f0-9]{3})([A-Fa-f0-9]){0,1}$/
13
+
14
+ if (hexRegex.test(color) || hexShorthandRegex.test(color)) {
15
+ const hexValue = color.substring(1)
16
+ const expandedHex =
17
+ hexValue.length === 3 || hexValue.length === 4
18
+ ? hexValue
19
+ .split('')
20
+ .map((char) => char + char)
21
+ .join('')
22
+ : hexValue
23
+
24
+ const alphaValue = expandedHex.length === 8 ? expandedHex.slice(6, 8) : undefined
25
+
26
+ return {
27
+ r: parseInt(expandedHex.slice(0, 2), 16) / 255,
28
+ g: parseInt(expandedHex.slice(2, 4), 16) / 255,
29
+ b: parseInt(expandedHex.slice(4, 6), 16) / 255,
30
+ ...(alphaValue ? { a: parseInt(alphaValue, 16) / 255 } : {}),
31
+ }
32
+ } else {
33
+ throw new Error('Invalid color format')
34
+ }
35
+ }
36
+
37
+ function rgbToHex({ r, g, b, ...rest }) {
38
+ const a = 'a' in rest ? rest.a : 1
39
+
40
+ const toHex = (value) => {
41
+ const hex = Math.round(value * 255).toString(16)
42
+ return hex.length === 1 ? '0' + hex : hex
43
+ }
44
+
45
+ const hex = [toHex(r), toHex(g), toHex(b)].join('')
46
+ return `#${hex}` + (a !== 1 ? toHex(a) : '')
47
+ }
48
+
49
+ module.exports = { colorApproximatelyEqual, parseColor, rgbToHex }
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ /**
7
+ * Convert Figma design tokens to Tailwind CSS theme configuration.
8
+ *
9
+ * Generates:
10
+ * theme.js - Palette colors, spacing, and border radius for Tailwind
11
+ *
12
+ * Components use Tailwind's dark: variants directly:
13
+ * className="bg-secondary-300 dark:bg-secondary-300 text-grey-900 dark:text-white"
14
+ *
15
+ * Usage:
16
+ * npx vertis-generate-theme
17
+ */
18
+
19
+ const rootDir = process.cwd();
20
+ const tokensDir = path.join(rootDir, "tokens");
21
+
22
+ // Check if tokens directory exists
23
+ if (!fs.existsSync(tokensDir)) {
24
+ console.error('\n❌ Tokens directory not found!');
25
+ console.error('Run "npx vertis-sync-figma" first to fetch tokens from Figma.\n');
26
+ process.exit(1);
27
+ }
28
+
29
+ const tokenFiles = fs.readdirSync(tokensDir).filter(file => file.endsWith(".json"));
30
+
31
+ if (tokenFiles.length === 0) {
32
+ console.error('\n❌ No token files found in ./tokens/');
33
+ console.error('Run "npx vertis-sync-figma" first to fetch tokens from Figma.\n');
34
+ process.exit(1);
35
+ }
36
+
37
+ console.log('\n🔄 Converting tokens to Tailwind theme...\n');
38
+
39
+ const allData = {};
40
+ tokenFiles.forEach(file => {
41
+ const filePath = path.join(tokensDir, file);
42
+ allData[file] = JSON.parse(fs.readFileSync(filePath, "utf-8"));
43
+ console.log(` ✓ Loaded ${file}`);
44
+ });
45
+
46
+ // ============================================================================
47
+ // FILE DETECTION (Flexible pattern matching)
48
+ // ============================================================================
49
+
50
+ function findFileByPattern(patterns) {
51
+ const patternArray = Array.isArray(patterns) ? patterns : [patterns];
52
+
53
+ for (const fileName of Object.keys(allData)) {
54
+ const lowerName = fileName.toLowerCase();
55
+ for (const pattern of patternArray) {
56
+ if (lowerName.includes(pattern.toLowerCase())) {
57
+ return { fileName, data: allData[fileName] };
58
+ }
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ const colorFile = findFileByPattern(['colours', 'colors', 'colour', 'color']);
65
+ const spacingFile = findFileByPattern(['spacing']);
66
+ const radiusFile = findFileByPattern(['radius']);
67
+
68
+ if (colorFile) console.log(`\n 🎨 Using color file: ${colorFile.fileName}`);
69
+ if (spacingFile) console.log(` 📏 Using spacing file: ${spacingFile.fileName}`);
70
+ if (radiusFile) console.log(` ⭕ Using radius file: ${radiusFile.fileName}`);
71
+
72
+ // ============================================================================
73
+ // HELPERS
74
+ // ============================================================================
75
+
76
+ function isToken(obj) {
77
+ return obj && typeof obj === "object" && "$value" in obj;
78
+ }
79
+
80
+ function extractTokens(obj, prefix = "") {
81
+ const tokens = {};
82
+ for (const [key, value] of Object.entries(obj)) {
83
+ const tokenName = prefix ? `${prefix}-${key}` : key;
84
+ if (isToken(value)) {
85
+ tokens[tokenName] = value;
86
+ } else if (typeof value === "object" && value !== null) {
87
+ Object.assign(tokens, extractTokens(value, tokenName));
88
+ }
89
+ }
90
+ return tokens;
91
+ }
92
+
93
+ // ============================================================================
94
+ // TOKEN EXTRACTION - PALETTE COLORS
95
+ // ============================================================================
96
+
97
+ const paletteColors = {};
98
+
99
+ if (colorFile) {
100
+ const colorTokens = extractTokens(colorFile.data);
101
+
102
+ for (const [name, token] of Object.entries(colorTokens)) {
103
+ if (token.$type === "color") {
104
+ const parts = name.toLowerCase().split("-");
105
+
106
+ if (parts.length >= 2) {
107
+ const colorGroup = parts[0];
108
+ const shade = parts.slice(1).join("-");
109
+
110
+ if (!paletteColors[colorGroup]) {
111
+ paletteColors[colorGroup] = {};
112
+ }
113
+ paletteColors[colorGroup][shade] = token.$value;
114
+ } else {
115
+ paletteColors[name.toLowerCase()] = token.$value;
116
+ }
117
+ }
118
+ }
119
+ } else {
120
+ console.warn('\n ⚠️ No color file found.');
121
+ }
122
+
123
+ // Add Tailwind color aliases
124
+ const colorAliases = {
125
+ blue: 'primary',
126
+ green: 'secondary',
127
+ yellow: 'tertiary',
128
+ gray: 'grey',
129
+ };
130
+
131
+ for (const [alias, source] of Object.entries(colorAliases)) {
132
+ if (paletteColors[source]) {
133
+ paletteColors[alias] = { ...paletteColors[source] };
134
+ }
135
+ }
136
+
137
+ // ============================================================================
138
+ // PROCESS SPACING AND BORDER RADIUS
139
+ // ============================================================================
140
+
141
+ const spacing = {};
142
+ const borderRadius = {};
143
+
144
+ if (spacingFile) {
145
+ const spacingTokens = extractTokens(spacingFile.data);
146
+ for (const [name, token] of Object.entries(spacingTokens)) {
147
+ if (token.$type === "number") {
148
+ const match = name.match(/spacing-(\d+)/i);
149
+ if (match) {
150
+ spacing[match[1]] = `${token.$value}px`;
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ if (radiusFile) {
157
+ const radiusTokens = extractTokens(radiusFile.data);
158
+ for (const [name, token] of Object.entries(radiusTokens)) {
159
+ if (token.$type === "number") {
160
+ const match = name.match(/radius-(.+)/i);
161
+ if (match) {
162
+ let value = token.$value;
163
+ if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
164
+ value = value.slice(1, -1);
165
+ }
166
+ borderRadius[match[1]] = `${value}px`;
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ console.log(`\n 📦 Processed ${Object.keys(spacing).length} spacing tokens`);
173
+ console.log(` 📦 Processed ${Object.keys(borderRadius).length} border radius tokens`);
174
+
175
+ // ============================================================================
176
+ // GENERATE theme.js (Palette colors only)
177
+ // ============================================================================
178
+
179
+ const colorOrder = ['base', 'primary', 'secondary', 'tertiary', 'grey', 'blue', 'green', 'yellow', 'gray'];
180
+
181
+ function formatColors(colors) {
182
+ const lines = [];
183
+
184
+ function formatValue(value, indent = 6) {
185
+ const spaces = ' '.repeat(indent);
186
+ if (typeof value === 'object' && value !== null) {
187
+ const subLines = [];
188
+ subLines.push('{');
189
+ for (const [key, subValue] of Object.entries(value)) {
190
+ const keyStr = /[^a-zA-Z0-9_]/.test(key) ? `"${key}"` : key;
191
+ if (typeof subValue === 'object' && subValue !== null) {
192
+ subLines.push(`${spaces} ${keyStr}: ${formatValue(subValue, indent + 2)},`);
193
+ } else {
194
+ subLines.push(`${spaces} ${keyStr}: "${subValue}",`);
195
+ }
196
+ }
197
+ subLines.push(`${spaces}}`);
198
+ return subLines.join('\n');
199
+ }
200
+ return `"${value}"`;
201
+ }
202
+
203
+ // Output in preferred order
204
+ for (const group of colorOrder) {
205
+ if (colors[group] && typeof colors[group] === 'object') {
206
+ lines.push(` ${group}: ${formatValue(colors[group], 4)},`);
207
+ }
208
+ }
209
+
210
+ // Output remaining groups
211
+ for (const [group, shades] of Object.entries(colors)) {
212
+ if (!colorOrder.includes(group)) {
213
+ if (typeof shades === 'object') {
214
+ lines.push(` ${group}: ${formatValue(shades, 4)},`);
215
+ } else {
216
+ lines.push(` "${group}": "${shades}",`);
217
+ }
218
+ }
219
+ }
220
+
221
+ return lines.join('\n');
222
+ }
223
+
224
+ const themeJSOutput = `/**
225
+ * Generated Tailwind theme from Figma design tokens.
226
+ * DO NOT EDIT MANUALLY - regenerate with: npx vertis-generate-theme
227
+ *
228
+ * This file contains palette colors for Tailwind.
229
+ * Components use Tailwind's dark: variants directly.
230
+ *
231
+ * Usage in components:
232
+ * className="bg-secondary-300 text-grey-900"
233
+ * className="dark:bg-secondary-300 dark:text-white"
234
+ *
235
+ * To customize for your project, override colors in tailwind.config.js:
236
+ * theme: {
237
+ * extend: {
238
+ * colors: {
239
+ * secondary: { 300: '#FF6600' } // Your brand color
240
+ * }
241
+ * }
242
+ * }
243
+ *
244
+ * Generated: ${new Date().toISOString()}
245
+ */
246
+ module.exports = {
247
+ colors: {
248
+ ${formatColors(paletteColors)}
249
+ },
250
+
251
+ spacing: ${JSON.stringify(spacing, null, 4).replace(/\n/g, '\n ')},
252
+
253
+ borderRadius: ${JSON.stringify(borderRadius, null, 4).replace(/\n/g, '\n ')},
254
+ };
255
+ `;
256
+
257
+ fs.writeFileSync(path.join(rootDir, "theme.js"), themeJSOutput, "utf-8");
258
+
259
+ // ============================================================================
260
+ // SUMMARY
261
+ // ============================================================================
262
+
263
+ let totalPaletteColors = 0;
264
+ let paletteGroups = 0;
265
+ for (const group of Object.values(paletteColors)) {
266
+ if (typeof group === 'object') {
267
+ totalPaletteColors += Object.keys(group).length;
268
+ paletteGroups++;
269
+ } else {
270
+ totalPaletteColors += 1;
271
+ }
272
+ }
273
+
274
+ console.log('\n✅ Theme generation complete!\n');
275
+ console.log(' Output file:');
276
+ console.log(' ✓ ./theme.js - Palette colors for Tailwind\n');
277
+ console.log(' Summary:');
278
+ console.log(` - Palette colors: ${totalPaletteColors} in ${paletteGroups} groups`);
279
+ console.log(` - Spacing: ${Object.keys(spacing).length} tokens`);
280
+ console.log(` - Border radius: ${Object.keys(borderRadius).length} tokens`);
281
+ console.log('\n Usage in components (with dark mode):');
282
+ console.log(' bg-secondary-300 dark:bg-secondary-300');
283
+ console.log(' text-grey-900 dark:text-white');
284
+ console.log(' border-grey-300 dark:border-grey-700');
285
+ console.log('\n Dark mode toggle:');
286
+ console.log(' document.documentElement.classList.toggle("dark");');
287
+ console.log('\n Next step: Configure Tailwind to use this theme.');
288
+ console.log(' See the @vertis/components USAGE.md for details.\n');
@@ -0,0 +1,37 @@
1
+ const axios = require('axios')
2
+
3
+ class FigmaApi {
4
+ baseUrl = 'https://api.figma.com'
5
+
6
+ constructor(token) {
7
+ this.token = token
8
+ }
9
+
10
+ async getLocalVariables(fileKey) {
11
+ const resp = await axios.request({
12
+ url: `${this.baseUrl}/v1/files/${fileKey}/variables/local`,
13
+ headers: {
14
+ Accept: '*/*',
15
+ 'X-Figma-Token': this.token,
16
+ },
17
+ })
18
+
19
+ return resp.data
20
+ }
21
+
22
+ async postVariables(fileKey, payload) {
23
+ const resp = await axios.request({
24
+ url: `${this.baseUrl}/v1/files/${fileKey}/variables`,
25
+ method: 'POST',
26
+ headers: {
27
+ Accept: '*/*',
28
+ 'X-Figma-Token': this.token,
29
+ },
30
+ data: payload,
31
+ })
32
+
33
+ return resp.data
34
+ }
35
+ }
36
+
37
+ module.exports = FigmaApi
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ require('dotenv').config({ path: path.join(process.cwd(), '.env') });
5
+ const FigmaApi = require('./figma-api.js');
6
+ const fs = require('fs');
7
+ const { green } = require('./utils.js');
8
+ const { tokenFilesFromLocalVariables } = require('./token-export.js');
9
+
10
+ /**
11
+ * Sync design tokens from Figma to local JSON files.
12
+ *
13
+ * Prerequisites:
14
+ * Create .env in your project root with:
15
+ * FIGMA_PAT=your_personal_access_token
16
+ * FIGMA_FILE_KEY=your_figma_file_key
17
+ *
18
+ * Usage:
19
+ * npx vertis-sync-figma # Writes to ./tokens directory
20
+ * npx vertis-sync-figma -- --output dir # Writes to specified directory
21
+ */
22
+
23
+ async function main() {
24
+ if (!process.env.FIGMA_PAT || !process.env.FIGMA_FILE_KEY) {
25
+ console.error('\n❌ Missing Figma credentials!\n');
26
+ console.error('Create .env.local in your project root with:');
27
+ console.error(' FIGMA_PAT=your_personal_access_token');
28
+ console.error(' FIGMA_FILE_KEY=your_figma_file_key\n');
29
+ console.error('To get these values:');
30
+ console.error(' 1. FIGMA_PAT: Figma → Settings → Account → Personal access tokens');
31
+ console.error(' 2. FIGMA_FILE_KEY: From your Figma URL: https://www.figma.com/file/FILE_KEY/...\n');
32
+ throw new Error('FIGMA_PAT and FIGMA_FILE_KEY environment variables are required');
33
+ }
34
+
35
+ const fileKey = process.env.FIGMA_FILE_KEY;
36
+
37
+ console.log('🔄 Fetching tokens from Figma...');
38
+ const api = new FigmaApi(process.env.FIGMA_PAT);
39
+ const localVariables = await api.getLocalVariables(fileKey);
40
+
41
+ const tokensFiles = tokenFilesFromLocalVariables(localVariables);
42
+
43
+ // Default output to 'tokens' directory in consumer's project root
44
+ let outputDir = path.join(process.cwd(), 'tokens');
45
+ const outputArgIdx = process.argv.indexOf('--output');
46
+ if (outputArgIdx !== -1) {
47
+ outputDir = path.join(process.cwd(), process.argv[outputArgIdx + 1]);
48
+ }
49
+
50
+ if (!fs.existsSync(outputDir)) {
51
+ fs.mkdirSync(outputDir, { recursive: true });
52
+ }
53
+
54
+ Object.entries(tokensFiles).forEach(([fileName, fileContent]) => {
55
+ fs.writeFileSync(path.join(outputDir, fileName), JSON.stringify(fileContent, null, 2));
56
+ console.log(` ✓ Wrote ${fileName}`);
57
+ });
58
+
59
+ console.log(green(`\n✅ Tokens synced to ${outputDir}`));
60
+ console.log('\nNext step: Run "npx vertis-generate-theme" to create the Tailwind theme.');
61
+ }
62
+
63
+ main();
@@ -0,0 +1,79 @@
1
+ const { rgbToHex } = require('./color.js')
2
+
3
+ function tokenTypeFromVariable(variable) {
4
+ switch (variable.resolvedType) {
5
+ case 'BOOLEAN':
6
+ return 'boolean'
7
+ case 'COLOR':
8
+ return 'color'
9
+ case 'FLOAT':
10
+ return 'number'
11
+ case 'STRING':
12
+ return 'string'
13
+ }
14
+ }
15
+
16
+ function tokenValueFromVariable(variable, modeId, localVariables) {
17
+ const value = variable.valuesByMode[modeId]
18
+ if (typeof value === 'object') {
19
+ if ('type' in value && value.type === 'VARIABLE_ALIAS') {
20
+ const aliasedVariable = localVariables[value.id]
21
+ return `{${aliasedVariable.name.replace(/\//g, '.')}}`
22
+ } else if ('r' in value) {
23
+ return rgbToHex(value)
24
+ }
25
+
26
+ throw new Error(`Format of variable value is invalid: ${value}`)
27
+ } else {
28
+ return value
29
+ }
30
+ }
31
+
32
+ function tokenFilesFromLocalVariables(localVariablesResponse) {
33
+ const tokenFiles = {}
34
+ const localVariableCollections = localVariablesResponse.meta.variableCollections
35
+ const localVariables = localVariablesResponse.meta.variables
36
+
37
+ Object.values(localVariables).forEach((variable) => {
38
+ // Skip remote variables because we only want to generate tokens for local variables
39
+ if (variable.remote) {
40
+ return
41
+ }
42
+
43
+ const collection = localVariableCollections[variable.variableCollectionId]
44
+
45
+ collection.modes.forEach((mode) => {
46
+ const fileName = `${collection.name}.${mode.name}.json`
47
+
48
+ if (!tokenFiles[fileName]) {
49
+ tokenFiles[fileName] = {}
50
+ }
51
+
52
+ let obj = tokenFiles[fileName]
53
+
54
+ variable.name.split('/').forEach((groupName) => {
55
+ obj[groupName] = obj[groupName] || {}
56
+ obj = obj[groupName]
57
+ })
58
+
59
+ const token = {
60
+ $type: tokenTypeFromVariable(variable),
61
+ $value: tokenValueFromVariable(variable, mode.modeId, localVariables),
62
+ $description: variable.description,
63
+ $extensions: {
64
+ 'com.figma': {
65
+ hiddenFromPublishing: variable.hiddenFromPublishing,
66
+ scopes: variable.scopes,
67
+ codeSyntax: variable.codeSyntax,
68
+ },
69
+ },
70
+ }
71
+
72
+ Object.assign(obj, token)
73
+ })
74
+ })
75
+
76
+ return tokenFiles
77
+ }
78
+
79
+ module.exports = { tokenFilesFromLocalVariables }