-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from qld-gov-au/feature/tokens2.0
Design Tokens 2.0
- Loading branch information
Showing
171 changed files
with
42,952 additions
and
1,781 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
import StyleDictionary from "style-dictionary"; | ||
import { getReferences, usesReferences } from "style-dictionary/utils"; | ||
import { | ||
register, | ||
permutateThemes, | ||
} from "@tokens-studio/sd-transforms"; | ||
import { promises } from "node:fs"; | ||
import { primitiveFilter } from "./sd-filters.mjs"; | ||
import { | ||
generateSemanticFiles, | ||
generateComponentFiles, | ||
} from "./sd-file-generators.mjs"; | ||
import { copyOriginalFile, emptyDir } from "./dir-and-files.mjs"; | ||
|
||
register(StyleDictionary); | ||
|
||
// list of components that we have tokens for, assume the tokenset path for it is tokens/${comp}.tokens.json | ||
const components = ["button", "card"]; | ||
|
||
const directory = "src"; | ||
const originalSrcIndex = ".build/original/index.ts" | ||
const finalDestIndex = "src/index.ts" | ||
const originalSrcGlobal = ".build/original/global.d.ts" | ||
const finalDestGlobal = "src/global.d.ts" | ||
|
||
async function beforeRun() { | ||
// clear | ||
emptyDir(directory); | ||
} | ||
|
||
async function afterRun() { | ||
// copy index.ts for dist | ||
copyOriginalFile(originalSrcIndex, finalDestIndex); | ||
copyOriginalFile(originalSrcGlobal, finalDestGlobal); | ||
} | ||
|
||
async function run() { | ||
const $themes = JSON.parse(await promises.readFile("tokens/$themes.tokens.json")); | ||
const themes = permutateThemes($themes); | ||
// collect all tokensets for all themes and dedupe | ||
const tokensets = [ | ||
...new Set( | ||
Object.values(themes).reduce((acc, sets) => [...acc, ...sets], []) | ||
), | ||
]; | ||
// figure out which tokensets are theme-specific | ||
// this is determined by checking if a certain tokenset is used for EVERY theme dimension variant | ||
// if it is, then it is not theme-specific | ||
const themeableSets = tokensets.filter((set) => { | ||
return !Object.values(themes).every((sets) => sets.includes(set)); | ||
}); | ||
|
||
const configs = Object.entries(themes).map(([theme, sets]) => ({ | ||
source: sets.map((tokenset) => `tokens/${tokenset}.tokens.json`), | ||
// these are the defaults | ||
log: { | ||
warnings: 'error', // 'warn' | 'error' | 'disabled' | ||
verbosity: 'default', // 'default' | 'silent' | 'verbose' | ||
errors: { | ||
brokenReferences: 'throw', // 'throw' | 'console' | ||
}, | ||
}, | ||
platforms: { | ||
|
||
android: { | ||
transformGroup: 'tokens-studio', | ||
transforms: [ | ||
'ts/descriptionToComment', | ||
'ts/size/px', | ||
'ts/opacity', | ||
'ts/size/lineheight', | ||
'ts/resolveMath', | ||
'ts/color/modifiers', | ||
'ts/typography/compose/shorthand', | ||
'ts/typography/fontWeight', | ||
'ts/size/css/letterspacing', | ||
'ts/color/css/hexrgba', | ||
'ts/color/modifiers', | ||
'attribute/themeable' | ||
], | ||
expand: true, | ||
files: [ | ||
// primitive tokens, e.g. for application developer | ||
{ | ||
destination: "src/android/styles/primitive.tokens.xml", | ||
format: "android/resources", | ||
filter: primitiveFilter, | ||
}, | ||
// semantic tokens, e.g. for application developer | ||
...generateSemanticFiles(components, theme, 'android', 'xml'), | ||
// component tokens, e.g. for design system developer | ||
...generateComponentFiles(components, theme, 'android', 'xml'), | ||
], | ||
}, | ||
js: { | ||
transformGroup: 'tokens-studio', | ||
transforms: [ | ||
'ts/descriptionToComment', | ||
'ts/size/px', | ||
'ts/opacity', | ||
'ts/size/lineheight', | ||
'ts/typography/fontWeight', | ||
'ts/resolveMath', | ||
'ts/size/css/letterspacing', | ||
'ts/color/css/hexrgba', | ||
'ts/color/modifiers', | ||
'attribute/themeable' | ||
], | ||
expand: true, | ||
files: [ | ||
// primitive tokens, e.g. for application developer | ||
{ | ||
destination: "src/js/styles/primitive.tokens.js", | ||
format: "javascript/es6", | ||
filter: primitiveFilter, | ||
}, | ||
// semantic tokens, e.g. for application developer | ||
...generateSemanticFiles(components, theme, 'js', 'js'), | ||
// component tokens, e.g. for design system developer | ||
...generateComponentFiles(components, theme, 'js', 'js'), | ||
], | ||
}, | ||
scss: { | ||
transformGroup: 'tokens-studio', | ||
transforms: [ | ||
'ts/descriptionToComment', | ||
'ts/size/px', | ||
'ts/opacity', | ||
'ts/size/lineheight', | ||
'ts/typography/fontWeight', | ||
'ts/resolveMath', | ||
'ts/size/css/letterspacing', | ||
'ts/color/css/hexrgba', | ||
'ts/color/modifiers', | ||
'attribute/themeable' | ||
], | ||
expand: true, | ||
files: [ | ||
// primitive tokens, e.g. for application developer | ||
{ | ||
destination: "src/scss/styles/primitive.tokens.scss", | ||
format: "scss/variables", | ||
filter: primitiveFilter, | ||
}, | ||
// semantic tokens, e.g. for application developer | ||
...generateSemanticFiles(components, theme, 'scss', 'scss'), | ||
// component tokens, e.g. for design system developer | ||
...generateComponentFiles(components, theme, 'scss', 'scss'), | ||
], | ||
}, | ||
|
||
css: { | ||
transformGroup: "tokens-studio", | ||
// transforms: ["attribute/themeable", "name/kebab"], | ||
transforms: [ | ||
'ts/descriptionToComment', | ||
'ts/size/px', | ||
'ts/opacity', | ||
'ts/size/lineheight', | ||
'ts/typography/fontWeight', | ||
'ts/resolveMath', | ||
'ts/size/css/letterspacing', | ||
'ts/color/css/hexrgba', | ||
'ts/color/modifiers', | ||
'attribute/themeable', | ||
'name/kebab', | ||
], | ||
expand: true, | ||
files: [ | ||
// primitive tokens, e.g. for application developer | ||
{ | ||
destination: "src/css/styles/primitive.tokens.css", | ||
format: "css/variables", | ||
filter: primitiveFilter, | ||
}, | ||
// semantic tokens, e.g. for application developer | ||
...generateSemanticFiles(components, theme, 'css', 'css'), | ||
// component tokens, e.g. for design system developer | ||
...generateComponentFiles(components, theme, 'css', 'css'), | ||
], | ||
}, | ||
}, | ||
})); | ||
|
||
for (const cfg of configs) { | ||
const sd = new StyleDictionary(cfg); | ||
|
||
/** | ||
* This transform checks for each token whether that token's value could change | ||
* due to Tokens Studio theming. | ||
* Any tokenset from Tokens Studio marked as "enabled" in the $themes.json is considered | ||
* a set in which any token could change if the theme changes. | ||
* Any token that is inside such a set or is a reference with a token in that reference chain | ||
* that is inside such a set, is considered "themeable", | ||
* which means it could change by theme switching. | ||
* | ||
* This metadata is applied to the token so we can use it as a way of filtering outputs | ||
* later in the "format" stage. | ||
*/ | ||
sd.registerTransform({ | ||
name: "attribute/themeable", | ||
type: "attribute", | ||
transform: (token) => { | ||
function isPartOfEnabledSet(token) { | ||
const set = token.filePath | ||
.replace(/^tokens\//g, "") | ||
.replace(/.tokens.json$/g, ""); | ||
return themeableSets.includes(set); | ||
} | ||
|
||
// Set token to themeable if it's part of an enabled set | ||
if (isPartOfEnabledSet(token)) { | ||
return { | ||
themeable: true, | ||
}; | ||
} | ||
|
||
// Set token to themeable if it's using a reference and inside the reference chain | ||
// any one of them is from a themeable set | ||
if (usesReferences(token.original.value)) { | ||
const refs = getReferences(token.original.value, sd.tokens); | ||
if (refs.some((ref) => isPartOfEnabledSet(ref))) { | ||
return { | ||
themeable: true, | ||
}; | ||
} | ||
} | ||
}, | ||
}); | ||
|
||
await sd.cleanAllPlatforms(); | ||
await sd.buildAllPlatforms(); | ||
} | ||
} | ||
beforeRun(); | ||
run(); | ||
afterRun(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import { copyFile } from 'copy-file'; | ||
|
||
export const copyOriginalFile = async (originalSrc, finalDest) => { | ||
try { | ||
await copyFile(originalSrc, finalDest); | ||
console.log('file copied'); | ||
} catch (err) { | ||
console.log(err); | ||
} | ||
}; | ||
|
||
export const emptyDir = async (dirPath) => { | ||
try { | ||
// list dir content | ||
const dirContents = fs.readdirSync(dirPath); | ||
|
||
for (const fileOrDirPath of dirContents) { | ||
try { | ||
// get full path | ||
const fullPath = path.join(dirPath, fileOrDirPath); | ||
const stat = fs.statSync(fullPath); | ||
|
||
if (stat.isDirectory()) { | ||
// it's a sub directory | ||
if (fs.readdirSync(fullPath).length) emptyDir(fullPath); | ||
|
||
// if the dir is not empty then remove it's contents too(recursively) | ||
fs.rmdirSync(fullPath); | ||
|
||
} else { | ||
// it's a file | ||
fs.unlinkSync(fullPath); | ||
} | ||
|
||
} catch (ex) { | ||
console.error(ex.message); | ||
} | ||
} | ||
} catch (err) { | ||
console.log(err); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Please edit original file located in .build/original/global.d.ts | ||
|
||
declare module '*.tokens.css' { | ||
const classes: { [key: string]: string }; | ||
export default classes; | ||
} | ||
declare module '*.tokens.scss' { | ||
const classes: { [key: string]: string }; | ||
export default classes; | ||
} | ||
declare module '*.tokens.js' { | ||
const classes: { [key: string]: string }; | ||
export default classes; | ||
} | ||
declare module '*.tokens.xml' { | ||
const classes: { [key: string]: string }; | ||
export default classes; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// Please edit original file located in .build/original/index.ts | ||
|
||
export default qgdsDesignTokens; | ||
import cssTokensPrimitive from './css/styles/primitive.tokens.css'; | ||
|
||
function qgdsDesignTokens() { | ||
return JSON.stringify(cssTokensPrimitive); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { semanticFilter, componentFilter } from "./sd-filters.mjs"; | ||
|
||
const commonFileOptions = { | ||
android: { | ||
format: "android/resources", | ||
}, | ||
js: { | ||
format: "javascript/es6", | ||
}, | ||
scss: { | ||
format: "scss/variables", | ||
options: { | ||
selector: ":host", | ||
}, | ||
}, | ||
css: { | ||
format: "css/variables", | ||
options: { | ||
selector: ":host", | ||
}, | ||
} | ||
}; | ||
|
||
export const generateSemanticFiles = (components, theme, platform, fileExtension) => { | ||
const filesArr = []; | ||
// theme-specific outputs | ||
filesArr.push({ | ||
...commonFileOptions[platform], | ||
filter: semanticFilter(components, true), | ||
destination: `src/${platform}/styles/qgds-${theme.toLowerCase()}.tokens.${fileExtension}`, | ||
}); | ||
|
||
// not theme-specific outputs | ||
filesArr.push({ | ||
...commonFileOptions[platform], | ||
filter: semanticFilter(components, false), | ||
destination: `src/${platform}/styles/qgds.tokens.${fileExtension}`, | ||
}); | ||
|
||
return filesArr; | ||
}; | ||
|
||
// for each component (currently only button), filter those specific component tokens and output them | ||
// to the component folder where the component source code will live | ||
export const generateComponentFiles = (components, theme, platform, fileExtension) => { | ||
const filesArr = []; | ||
|
||
for (const comp of components) { | ||
// theme-specific outputs | ||
filesArr.push({ | ||
...commonFileOptions[platform], | ||
filter: componentFilter(comp, true), | ||
destination: `src/${platform}/${comp}/${comp}-${theme.toLowerCase()}.tokens.${fileExtension}`, | ||
}); | ||
|
||
// not theme-specific outputs | ||
filesArr.push({ | ||
...commonFileOptions[platform], | ||
filter: componentFilter(comp, false), | ||
destination: `src/${platform}/${comp}/${comp}.tokens.${fileExtension}`, | ||
}); | ||
} | ||
return filesArr; | ||
}; |
Oops, something went wrong.