Build tokens for each theme (#2590)

This PR adds the ability to export tokens for each theme. 

You can export tokens by:

1. `cd styles`
2. `npm run build-tokens`
3. Tokens will be output in the target folder (`styles/target`)

The tokens match the ColorScheme object. In the future we may also
export tokens for our styleTrees.

Release Notes:

- N/A (No public facing changes)

---
TODO:

- [x] Generate Token Studio theme index file
- [x] ColorScheme
    - [x] name:
    - [x] isLight
    - [x] lowest
    - [x] middle
    - [x] highest
    - [x] popoverShadow
    - [x] modalShadow
    - [x] players
    - [x] syntax
This commit is contained in:
Nate Butler 2023-06-08 16:40:46 -04:00 committed by GitHub
commit 85b049f250
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 197 additions and 22 deletions

View file

@ -1,11 +1,13 @@
import * as fs from "fs"
import * as path from "path"
import { ColorScheme, createColorScheme } from "./common"
import { themes } from "./themes"
import { slugify } from "./utils/slugify"
import { colorSchemeTokens } from "./theme/tokens/colorScheme"
import * as fs from "fs";
import * as path from "path";
import { ColorScheme, createColorScheme } from "./common";
import { themes } from "./themes";
import { slugify } from "./utils/slugify";
import { colorSchemeTokens } from "./theme/tokens/colorScheme";
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens");
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json");
const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json");
function clearTokens(tokensDirectory: string) {
if (!fs.existsSync(tokensDirectory)) {
@ -19,21 +21,65 @@ function clearTokens(tokensDirectory: string) {
}
}
type TokenSet = {
id: string;
name: string;
selectedTokenSets: { [key: string]: "enabled" };
};
function buildTokenSetOrder(colorSchemes: ColorScheme[]): { tokenSetOrder: string[] } {
const tokenSetOrder: string[] = colorSchemes.map(
(scheme) => scheme.name.toLowerCase().replace(/\s+/g, "_")
);
return { tokenSetOrder };
}
function buildThemesIndex(colorSchemes: ColorScheme[]): TokenSet[] {
const themesIndex: TokenSet[] = colorSchemes.map((scheme, index) => {
const id = `${scheme.isLight ? "light" : "dark"}_${scheme.name
.toLowerCase()
.replace(/\s+/g, "_")}_${index}`;
const selectedTokenSets: { [key: string]: "enabled" } = {};
const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_");
selectedTokenSets[tokenSet] = "enabled";
return {
id,
name: `${scheme.name} - ${scheme.isLight ? "Light" : "Dark"}`,
selectedTokenSets,
};
});
return themesIndex;
}
function writeTokens(colorSchemes: ColorScheme[], tokensDirectory: string) {
clearTokens(tokensDirectory)
clearTokens(tokensDirectory);
for (const colorScheme of colorSchemes) {
const fileName = slugify(colorScheme.name)
const tokens = colorSchemeTokens(colorScheme)
const tokensJSON = JSON.stringify(tokens, null, 2)
const outPath = path.join(tokensDirectory, `${fileName}.json`)
fs.writeFileSync(outPath, tokensJSON)
console.log(`- ${outPath} created`)
const fileName = slugify(colorScheme.name) + ".json";
const tokens = colorSchemeTokens(colorScheme);
const tokensJSON = JSON.stringify(tokens, null, 2);
const outPath = path.join(tokensDirectory, fileName);
fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 });
console.log(`- ${outPath} created`);
}
const themeIndexData = buildThemesIndex(colorSchemes);
const themesJSON = JSON.stringify(themeIndexData, null, 2);
fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 });
console.log(`- ${TOKENS_FILE} created`);
const tokenSetOrderData = buildTokenSetOrder(colorSchemes);
const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2);
fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 });
console.log(`- ${METADATA_FILE} created`);
}
const colorSchemes: ColorScheme[] = themes.map((theme) =>
createColorScheme(theme)
)
);
writeTokens(colorSchemes, TOKENS_DIRECTORY)
writeTokens(colorSchemes, TOKENS_DIRECTORY);

View file

@ -1,12 +1,81 @@
import { ColorScheme } from "../colorScheme"
import { PlayerTokens, players } from "./players"
import { SingleBoxShadowToken, SingleColorToken, SingleOtherToken, TokenTypes } from "@tokens-studio/types"
import { ColorScheme, Shadow, SyntaxHighlightStyle, ThemeSyntax } from "../colorScheme"
import { LayerToken, layerToken } from "./layer"
import { PlayersToken, playersToken } from "./players"
import { colorToken } from "./token"
import { Syntax } from "../syntax";
import editor from "../../styleTree/editor"
interface ColorSchemeTokens {
players: PlayerTokens
name: SingleOtherToken
appearance: SingleOtherToken
lowest: LayerToken
middle: LayerToken
highest: LayerToken
players: PlayersToken
popoverShadow: SingleBoxShadowToken
modalShadow: SingleBoxShadowToken
syntax?: Partial<ThemeSyntaxColorTokens>
}
const createShadowToken = (shadow: Shadow, tokenName: string): SingleBoxShadowToken => {
return {
name: tokenName,
type: TokenTypes.BOX_SHADOW,
value: `${shadow.offset[0]}px ${shadow.offset[1]}px ${shadow.blur}px 0px ${shadow.color}`
};
};
const popoverShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => {
const shadow = colorScheme.popoverShadow;
return createShadowToken(shadow, "popoverShadow");
};
const modalShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => {
const shadow = colorScheme.modalShadow;
return createShadowToken(shadow, "modalShadow");
};
type ThemeSyntaxColorTokens = Record<keyof ThemeSyntax, SingleColorToken>
function syntaxHighlightStyleColorTokens(syntax: Syntax): ThemeSyntaxColorTokens {
const styleKeys = Object.keys(syntax) as (keyof Syntax)[]
return styleKeys.reduce((acc, styleKey) => {
// Hack: The type of a style could be "Function"
// This can happen because we have a "constructor" property on the syntax object
// and a "constructor" property on the prototype of the syntax object
// To work around this just assert that the type of the style is not a function
if (!syntax[styleKey] || typeof syntax[styleKey] === 'function') return acc;
const { color } = syntax[styleKey] as Required<SyntaxHighlightStyle>;
return { ...acc, [styleKey]: colorToken(styleKey, color) };
}, {} as ThemeSyntaxColorTokens);
}
const syntaxTokens = (colorScheme: ColorScheme): ColorSchemeTokens['syntax'] => {
const syntax = editor(colorScheme).syntax
return syntaxHighlightStyleColorTokens(syntax)
}
export function colorSchemeTokens(colorScheme: ColorScheme): ColorSchemeTokens {
return {
players: players(colorScheme),
name: {
name: "themeName",
value: colorScheme.name,
type: TokenTypes.OTHER,
},
appearance: {
name: "themeAppearance",
value: colorScheme.isLight ? "light" : "dark",
type: TokenTypes.OTHER,
},
lowest: layerToken(colorScheme.lowest, "lowest"),
middle: layerToken(colorScheme.middle, "middle"),
highest: layerToken(colorScheme.highest, "highest"),
popoverShadow: popoverShadowToken(colorScheme),
modalShadow: modalShadowToken(colorScheme),
players: playersToken(colorScheme),
syntax: syntaxTokens(colorScheme),
}
}

View file

@ -0,0 +1,60 @@
import { SingleColorToken } from "@tokens-studio/types";
import { Layer, Style, StyleSet } from "../colorScheme";
import { colorToken } from "./token";
interface StyleToken {
background: SingleColorToken,
border: SingleColorToken,
foreground: SingleColorToken,
}
interface StyleSetToken {
default: StyleToken
active: StyleToken
disabled: StyleToken
hovered: StyleToken
pressed: StyleToken
inverted: StyleToken
}
export interface LayerToken {
base: StyleSetToken
variant: StyleSetToken
on: StyleSetToken
accent: StyleSetToken
positive: StyleSetToken
warning: StyleSetToken
negative: StyleSetToken
}
export const styleToken = (style: Style, name: string): StyleToken => {
const token = {
background: colorToken(`${name}Background`, style.background),
border: colorToken(`${name}Border`, style.border),
foreground: colorToken(`${name}Foreground`, style.foreground),
}
return token
}
export const styleSetToken = (styleSet: StyleSet, name: string): StyleSetToken => {
const token: StyleSetToken = {} as StyleSetToken;
for (const style in styleSet) {
const s = style as keyof StyleSet;
token[s] = styleToken(styleSet[s], `${name}${style}`);
}
return token;
}
export const layerToken = (layer: Layer, name: string): LayerToken => {
const token: LayerToken = {} as LayerToken;
for (const styleSet in layer) {
const s = styleSet as keyof Layer;
token[s] = styleSetToken(layer[s], `${name}${styleSet}`);
}
return token;
}

View file

@ -4,7 +4,7 @@ import { colorToken } from "./token"
export type PlayerToken = Record<"selection" | "cursor", SingleColorToken>
export type PlayerTokens = Record<keyof Players, PlayerToken>
export type PlayersToken = Record<keyof Players, PlayerToken>
function buildPlayerToken(colorScheme: ColorScheme, index: number): PlayerToken {
@ -16,7 +16,7 @@ function buildPlayerToken(colorScheme: ColorScheme, index: number): PlayerToken
}
}
export const players = (colorScheme: ColorScheme): PlayerTokens => ({
export const playersToken = (colorScheme: ColorScheme): PlayersToken => ({
"0": buildPlayerToken(colorScheme, 0),
"1": buildPlayerToken(colorScheme, 1),
"2": buildPlayerToken(colorScheme, 2),