@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.
- package/README.md +57 -0
- package/USAGE.md +236 -0
- package/dist/index.css +102 -0
- package/dist/index.d.mts +2196 -0
- package/dist/index.d.ts +2196 -0
- package/dist/index.js +10244 -0
- package/dist/index.mjs +10172 -0
- package/dist/styles.css +885 -0
- package/dist/tailwind-helper.js +60 -0
- package/package.json +66 -0
- package/scripts/build-css.js +54 -0
- package/scripts/color.js +49 -0
- package/scripts/convert-tailwind.js +288 -0
- package/scripts/figma-api.js +37 -0
- package/scripts/sync-figma.js +63 -0
- package/scripts/token-export.js +79 -0
- package/scripts/utils.js +11 -0
|
@@ -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);
|
package/scripts/color.js
ADDED
|
@@ -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 }
|