Skip to content

Commit

Permalink
feat(ui-kit): add Design Tokens synchronization automation from Figma…
Browse files Browse the repository at this point in the history
… to UI Kit
  • Loading branch information
sashathor committed Feb 26, 2024
1 parent 162ba08 commit 270935b
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/shiny-tables-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@propeldata/ui-kit': patch
---

Added Design Tokens synchronization automation from Figma to UI Kit
7 changes: 5 additions & 2 deletions packages/ui-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
"module": "dist/esm/index.module.js",
"scripts": {
"typecheck": "tsc --noEmit",
"build": "rm -rf dist && yarn graphql:build && yarn typecheck && rollup -c",
"build": "rm -rf dist && yarn graphql:build && yarn parse-design-tokens && yarn validate-design-tokens && yarn typecheck && rollup -c",
"graphql:build": "rm -rf src/helpers/graphql/generated && yarn graphql:gen",
"graphql:gen": "graphql-codegen --config src/helpers/graphql/codegen.yml && node src/helpers/graphql/script.cjs src/helpers/graphql/generated/index.ts",
"test": "yarn graphql:build && jest",
"test:coverage": "jest --ci --coverage --json --outputFile=coverage/coverage.json"
"test:coverage": "jest --ci --coverage --json --outputFile=coverage/coverage.json",
"parse-design-tokens": "node ./scripts/parse-design-tokens.js",
"validate-design-tokens": "yarn build-css && node ./scripts/validate-design-tokens.js",
"build-css": "sass src:src/generated"
},
"publishConfig": {
"access": "public",
Expand Down
88 changes: 88 additions & 0 deletions packages/ui-kit/scripts/parse-design-tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const fs = require('fs')
const path = require('path')

function kebabCaseToCamelCase(kebabStr) {
return kebabStr
.split('-')
.map((word, index) => (index === 0 ? word : word[0].toUpperCase() + word.slice(1)))
.join('')
}

function writeToFile(fileName, content) {
try {
fs.writeFileSync(`./src/themes/generated/${fileName}`, content)
} catch (err) {
console.error(err)
}
}

const data = fs.readFileSync('./src/themes/variables.json')
const variablesJSON = JSON.parse(data)

if (!variablesJSON) {
console.error('Error parsing JSON')
return
}

const primitives = variablesJSON.collections.find(({ name }) => name === 'Primitives').modes[0].variables
const tokens = variablesJSON.collections.find(({ name }) => name === 'Tokens').modes[0].variables

const variables = tokens.map((token) => {
const keys = token.name.split('/')
const key = keys.at(keys.length - 1).toLowerCase()
const primitiveValue = primitives.find((item) => item.name === token.value.name)
let value = primitiveValue.value
if (primitiveValue.type === 'number') {
value = `${primitiveValue.value}px`
}

return {
cssName: `--propel-${key}`,
jsName: kebabCaseToCamelCase(key),
type: primitiveValue.type === 'number' ? 'number' : 'string',
value
}
})

// Define the directory path you want to create
const dirPath = path.join(__dirname, '../src/themes/generated')
// Check if the directory exists
if (!fs.existsSync(dirPath)) {
// Create the directory if it does not exist
fs.mkdirSync(dirPath, { recursive: true })
}

// Generate _tokens.scss
writeToFile(
'_tokens.scss',
`// This file is generated automatically by scripts/parse-design-tokens.js. Do not edit manually.\n
.tokens {
${variables.map(({ cssName, value }) => ` ${cssName}: ${value};`).join('\n')}
}\n`
)

// Generate theme.types.ts
writeToFile(
'theme.types.ts',
`// This file is generated automatically by scripts/parse-design-tokens.js. Do not edit manually.\n
export type ThemeTokenGeneratedProps = {
${variables.map(({ jsName, type }) => ` ${jsName}?: ${type};`).join('\n')}
}
export type ThemeCSSTokenGeneratedProps = {
${variables.map(({ cssName, type }) => ` '${cssName}'?: ${type};`).join('\n')}
}\n`
)

// Generate themeTokens.ts
writeToFile(
'themeTokens.ts',
`// This file is generated automatically by scripts/parse-design-tokens.js. Do not edit manually.\n
import type { ThemeTokenGeneratedProps } from './theme.types'
export const themeTokensGenerated: (keyof ThemeTokenGeneratedProps)[] = [
${variables.map(({ jsName }) => ` '${jsName}',`).join('\n')}
]\n`
)

console.log('🎨 Desgin Tokens: parsed ok')
62 changes: 62 additions & 0 deletions packages/ui-kit/scripts/validate-design-tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require('fs')
const path = require('path')

// Function to list all css and scss files
function getAllFiles(dirPath, type = 'current', arrayOfFiles = []) {
const files = fs.readdirSync(dirPath)

files.forEach((file) => {
const fullPath = path.join(dirPath, file)
if (fs.statSync(fullPath).isDirectory()) {
if (type === 'current' && file !== 'themes') {
arrayOfFiles = getAllFiles(fullPath, type, arrayOfFiles)
}
} else if (file.endsWith('.css') || (type === 'new' && file.endsWith('.scss'))) {
arrayOfFiles.push(fullPath)
}
})

return arrayOfFiles
}

// Function to extract CSS variables
function extractCSSVariables(file) {
const fileContent = fs.readFileSync(file, 'utf8')
const regex = /--propel-[\w-]+/g // Regex updated to match only variables starting with --propel-
return fileContent.match(regex) || []
}

function getCSSVariables(dirPath, type) {
const files = getAllFiles(dirPath, type)
const allVariables = []

files.forEach((file) => {
const variables = extractCSSVariables(file)
variables.forEach((variable) => {
if (!allVariables.includes(variable)) {
allVariables.push(variable)
}
})
})

return allVariables
}

const currentVariablesList = getCSSVariables('./src', 'current')
const newVariablesList = getCSSVariables('./src/themes', 'new')

const failedVariables = []

currentVariablesList.forEach((variable) => {
if (!newVariablesList.includes(variable)) {
failedVariables.push(`⛔️ Variable '${variable}' is not defined in Design Tokens list`)
// console.log(`⛔️ Variable '${variable}' is not defined in Design Tokens list`)
}
})

if (failedVariables.length > 0) {
console.error(failedVariables.join('\n'))
throw new Error('🎨 Desgin Tokens: validation failed')
}

console.log('🎨 Desgin Tokens: validated ok')
5 changes: 3 additions & 2 deletions packages/ui-kit/src/themes/theme.types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { CSSProperties } from 'react'
import type { DefaultThemes } from '../components/ThemeProvider/ThemeProvider.types'
import { ThemeTokenGeneratedProps, ThemeCSSTokenGeneratedProps } from './generated/theme.types'

export type ThemeTokenProps = {
export interface ThemeTokenProps extends ThemeTokenGeneratedProps {
baseTheme?: DefaultThemes

fontFamily?: CSSProperties['fontFamily']
Expand Down Expand Up @@ -73,7 +74,7 @@ export type ThemeTokenProps = {
colorBlue25?: CSSProperties['color']
}

export type ThemeCSSTokenProps = {
export interface ThemeCSSTokenProps extends ThemeCSSTokenGeneratedProps {
'--propel-font-family'?: CSSProperties['fontFamily']
'--propel-font-size'?: CSSProperties['fontSize']
'--propel-font-weight'?: CSSProperties['fontWeight']
Expand Down
131 changes: 131 additions & 0 deletions packages/ui-kit/src/themes/variables.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
{
"version": "1.0.4",
"metadata": {},
"collections": [
{
"name": "Primitives",
"modes": [
{
"name": "Mode 1",
"variables": [
{
"name": "color/blue/25",
"type": "color",
"isAlias": false,
"value": "#F5FAFF"
},
{
"name": "color/blue/50",
"type": "color",
"isAlias": false,
"value": "#EFF8FF"
},
{
"name": "spacing/0",
"type": "number",
"isAlias": false,
"value": 0
},
{
"name": "spacing/0_5",
"type": "number",
"isAlias": false,
"value": 2
},
{
"name": "spacing/1",
"type": "number",
"isAlias": false,
"value": 4
},
{
"name": "color/warning/25",
"type": "color",
"isAlias": false,
"value": "#FFFBFA"
},
{
"name": "color/warning/50",
"type": "color",
"isAlias": false,
"value": "#FEF3F2"
}
]
}
]
},
{
"name": "Tokens",
"modes": [
{
"name": "Mode 1",
"variables": [
{
"name": "text/text-primary",
"type": "color",
"isAlias": true,
"value": {
"collection": "Primitives",
"name": "color/blue/25"
}
},
{
"name": "border/border-primary",
"type": "color",
"isAlias": true,
"value": {
"collection": "Primitives",
"name": "color/warning/25"
}
},
{
"name": "border/border-secondary",
"type": "color",
"isAlias": true,
"value": {
"collection": "Primitives",
"name": "color/warning/50"
}
},
{
"name": "text/text-secondary",
"type": "color",
"isAlias": true,
"value": {
"collection": "Primitives",
"name": "color/blue/50"
}
},
{
"name": "spacing-none",
"type": "number",
"isAlias": true,
"value": {
"collection": "Primitives",
"name": "spacing/0"
}
},
{
"name": "spacing-xxs",
"type": "number",
"isAlias": true,
"value": {
"collection": "Primitives",
"name": "spacing/0_5"
}
},
{
"name": "spacing-xs",
"type": "number",
"isAlias": true,
"value": {
"collection": "Primitives",
"name": "spacing/1"
}
}
]
}
]
}
]
}

0 comments on commit 270935b

Please sign in to comment.