From 92b9d63bc86fdb569247813e4b548b65dfff6861 Mon Sep 17 00:00:00 2001 From: Christopher Debove Date: Mon, 25 Jul 2022 02:55:23 +0200 Subject: [PATCH 1/3] feat: rework api to be more flexible --- README.md | 188 ++++++++++++++++++++++------ package.json | 6 +- src/css-variable-generator.test.ts | 51 -------- src/css-variable-generator.ts | 47 ------- src/css-variables-generator.test.ts | 101 +++++++++++++++ src/css-variables-generator.ts | 96 ++++++++++++++ src/helpers.test.ts | 83 +++++++----- src/helpers.ts | 113 +++++++++-------- src/index.ts | 2 +- src/token-box.ts | 47 +++---- src/type-tests/PartialTokenPath.ts | 16 +++ src/type-tests/TokenPath.ts | 19 +++ src/type-tests/utils.ts | 10 ++ tsconfig.build.json | 24 ++++ 14 files changed, 556 insertions(+), 247 deletions(-) delete mode 100644 src/css-variable-generator.test.ts delete mode 100644 src/css-variable-generator.ts create mode 100644 src/css-variables-generator.test.ts create mode 100644 src/css-variables-generator.ts create mode 100644 src/type-tests/PartialTokenPath.ts create mode 100644 src/type-tests/TokenPath.ts create mode 100644 src/type-tests/utils.ts create mode 100644 tsconfig.build.json diff --git a/README.md b/README.md index 1521513..79aa163 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,11 @@ In the root of your sources (eg: `./src`) create a `themthem-interfaces.d.ts` an /// interface GlobalDesignTokenBox { - colors: ['black', 'white', 'my-custom-color']; - sizes: ['sm', 'md', 'lg', 'xl'] + palette: ['black', 'white', 'my-custom-color']; + tokens: { + colors: ['default', 'accent'] + sizes: ['sm', 'md', 'lg', 'xl'] + } } ``` @@ -46,7 +49,12 @@ To define component token you need to augment `ComponentDesignTokenBox`. It's re /// interface ComponentDesignTokenBox { - Input: ['background-color', 'color', 'border-color', 'border-size'] + Input: { + border: { + default: ['color', 'size'], + focus: ['color', 'size'] + } + } } ``` @@ -55,24 +63,72 @@ interface ComponentDesignTokenBox { All the functions of the API is based on `GlobalDesignTokenBox` and `ComponentDesignTokenBox` by default. So the example on this section will all be based on the augmentation done in the [Usage](#usage) section. -### `cssVariable(type, key, token, options?)` +### `gVar(path)` + +Generate a CSS variable usage (`var(--variable)`) based on your `GlobalDesignTokenBox`. + +| Parameter | Type | Description | Default value | +|---|---|---|---| +| path | `string` | The path to your token || + +`@returns {string} The CSS variable usage of your token` + +```ts +import { gVar } from 'themthem'; + +gVar('palette.black'); // "var(--global-palette-black)" +gVar('tokens.colors.accent'); // "var(--global-tokens-colors-accent)" +``` + +### `gIdentifier(path)` + +Generate a CSS variable identifier (`--variable`) based on your `GlobalDesignTokenBox`. + +| Parameter | Type | Description | Default value | +|---|---|---|---| +| path | `string` | The path to your token || + +`@returns {string} The CSS variable identifier of your token` + +```ts +import { gIdentifier } from 'themthem'; + +gIdentifier('palette.black'); // "--global-palette-black" +gIdentifier('tokens.colors.accent'); // "--global-tokens-colors-accent" +``` -Generate a CSS Variable based on your theme. +### `cVar(path)` + +Generate a CSS variable usage (`var(--variable)`) based on your `ComponentDesignTokenBox`. | Parameter | Type | Description | Default value | |---|---|---|---| -| type | `'global' \| 'component'` | The type of token you want. `'global'` for GlobalDesignTokens or `'component'` for ComponentDesignToken || -| key | `keyof Themthem[typeof type]` | The global design token category or component name || -| token | `Themthem[typeof type][typeof key][number]` | The token token inside the category/component || -| options | `{ bare?: boolean } \| undefined` | Whether you want the bare css variable or it's usage | `{ bare: false }` | +| path | `string` | The path to your token || -`@returns {string} The bare CSS variable or CSS variable usage` +`@returns {string} The CSS Variable usage of your token` ```ts -import { cssVariable } from 'themthem'; +import { cVar } from 'themthem'; -cssVariable('global', 'color', 'black', { bare: true }); // "--global-color-black" -cssVariable('global', 'color', 'black'); // "var(--global-color-black)" +cVar('Input.border.default.color'); // "var(--component-Input-border-default-color)" +cVar('Input.border.focus.color'); // "var(--component-Input-border-focus-color)" +``` + +### `cIdentifier(path)` + +Generate a CSS variable identifier (`--variable`) based on your `ComponentDesignTokenBox`. + +| Parameter | Type | Description | Default value | +|---|---|---|---| +| path | `string` | The path to your token || + +`@returns {string} The CSS variable identifier of your token` + +```ts +import { cIdentifier } from 'themthem'; + +cIdentifier('Input.border.default.color'); // "--component-Input-border-default-color" +cIdentifier('Input.border.focus.color'); // "--component-Input-border-focus-color" ``` ### generateGlobalCSSVariables(config) @@ -81,7 +137,7 @@ Generate CSS variables assignments for you global design tokens based on your th | Parameter | Type | Description | Default value | |---|---|---|---| -| config | Object | A 2-level deep object assigning values to your global design tokens || +| config | Object | A config object assigning values to your global design tokens || `@returns {string[]} An array of CSS variables declarations` @@ -89,31 +145,81 @@ Generate CSS variables assignments for you global design tokens based on your th import { generateGlobalCSSVariables } from 'themthem'; const globalVariablesAssignments = generateGlobalCSSVariables({ - colors: { + palette: { black: '#000', white: '#fff', - 'my-custom-color': '#298af3' + 'my-custom-color': '#298af3', + }, + tokens: { + colors: { + default: gVar('palette.black'), + accent: gVar('palette.my-custom-color'), + }, + sizes: { + sm: '4px', + md: '8px', + lg: '12px', + xl: '20px', + }, }, - sizes: { - sm: '4px', - md: '8px', - lg: '12px', - xl: '20px', - } }); +// Returns: +// // [ -// '--global-colors-black: #000;', -// '--global-colors-white: #fff;', -// '--global-colors-my-custom-color: #298af3;', -// '--global-sizes-sm: 4px;', -// '--global-sizes-md: 8px;', -// '--global-sizes-lg: 12px;', -// '--global-sizes-xl: 20px;' +// '--global-palette-black: #000;', +// '--global-palette-white: #fff;', +// '--global-palette-my-custom-color: #298af3;', +// '--global-tokens-colors-default: var(--global-palette-black);', +// '--global-tokens-colors-accent: var(--global-palette-my-custom-color);', +// '--global-tokens-sizes-sm: 4px;', +// '--global-tokens-sizes-md: 8px;', +// '--global-tokens-sizes-lg: 12px;', +// '--global-tokens-sizes-xl: 20px;', // ] ``` -### createCSSVariableGenerator(component) +### createGlobalCSSVariablesGenerator(path) + +Create a function which lets you generate CSS variables assignments for a part of your global design tokens. + +| Parameter | Type | Description | Default value | +|---|---|---|---| +| path | `string` | The path to the part of the theme you want to configure || + +`@returns {(config): string[]} A function which generates CSS variables assignments for the specified part of your global design` + +```ts +import { createGlobalCSSVariableGenerator } from 'themthem'; + +const generateColorsTokensVariables = createGlobalCSSVariableGenerator('tokens.colors'); + +const lightThemeColorsTokens = generateGlobalCSSVariables({ + default: gVar('palette.black'), + accent: gVar('palette.my-custom-color'), +}); + +// Returns: +// +// [ +// '--global-tokens-colors-default: var(--global-palette-black);', +// '--global-tokens-colors-accent: var(--global-palette-my-custom-color);', +// ] + +const darkThemeColorsTokens = generateGlobalCSSVariables({ + default: gVar('palette.white'), + accent: gVar('palette.my-custom-color'), +}); + +// Returns: +// +// [ +// '--global-tokens-colors-default: var(--global-palette-white);', +// '--global-tokens-colors-accent: var(--global-palette-my-custom-color);', +// ] +``` + +### createComponentCSSVariableGenerator(component) Create a function which lets you generate CSS variables assignments for a component. @@ -129,16 +235,22 @@ import { createCSSVariablesGenerator } from 'themthem'; const generateInputCSSVariables = createCSSVariablesGenerator('Input'); const inputVariablesAssignments = generateInputCSSVariables({ - 'background-color': cssVariable('global', 'colors', 'white'), - 'color': 'black', - 'border-color': '#333', - 'border-size': '1px' + border: { + default: { + color: gVar('palette.black'), + size: '1px', + }, + focus: { + color: gVar('tokens.colors.accent'), + size: '2px' + } + }, }); // [ -// '--input-background-color: var(--global-colors-white);', -// '--input-color: black;', -// '--input-border-color: #333;', -// '--input-border-size: 1px;', +// '--component-Input-border-default-color: var(--global-palette-black);', +// '--component-Input-border-default-size: 1px;', +// '--component-Input-border-focus-color: var(--global-tokens-colors-accent);', +// '--component-Input-border-focus-size: 2px;', // ] ``` \ No newline at end of file diff --git a/package.json b/package.json index 023fb60..02cce6e 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "main": "./dist/themthem.umd.js", "module": "./dist/themthem.es.js", "files": [ - "./dist", - "./interfaces.d.ts" + "dist", + "interfaces.d.ts" ], "exports": { ".": { @@ -31,7 +31,7 @@ } }, "scripts": { - "build": "tsc --noEmit && vite build && tsc --emitDeclarationOnly && node ./scripts/typescript-replace.js", + "build": "tsc --noEmit && vite build && tsc -p tsconfig.build.json --emitDeclarationOnly", "test": "vitest --run --passWithNoTests", "prepare": "husky install" }, diff --git a/src/css-variable-generator.test.ts b/src/css-variable-generator.test.ts deleted file mode 100644 index 019c1df..0000000 --- a/src/css-variable-generator.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - createCSSVariablesGenerator, - generateGlobalCSSVariables, -} from './css-variable-generator'; - -interface TestThemthem { - global: { - foo: ['bar']; - bar: ['baz']; - }; - component: { - Foo: ['bar']; - Baz: ['baz']; - }; -} - -describe('generateGlobalCSSVariables', () => { - it('should returns an array of css variable affections', () => { - expect( - generateGlobalCSSVariables({ - foo: { - bar: 'value', - }, - bar: { - baz: 'other-value', - }, - }), - ).toEqual(['--global-foo-bar: value;', '--global-bar-baz: other-value;']); - }); -}); - -describe('createCSSVariableGenerator', () => { - it('should return a function', () => { - const result = createCSSVariablesGenerator<'Foo', TestThemthem>('Foo'); - expect(result).toBeInstanceOf(Function); - }); - - it('should returns an array of css variable affections', () => { - const generateCSSVariables = createCSSVariablesGenerator< - 'Foo', - TestThemthem - >('Foo'); - - expect( - generateCSSVariables({ - bar: 'value', - }), - ).toEqual(['--Foo-bar: value;']); - }); -}); diff --git a/src/css-variable-generator.ts b/src/css-variable-generator.ts deleted file mode 100644 index 78fc2be..0000000 --- a/src/css-variable-generator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { DTBoxKey, DesignToken, Themthem } from './token-box'; -import { cssVariable } from './helpers'; -import { getKeys } from './utils'; - -export function createCSSVariablesGenerator< - Key extends DTBoxKey<'component', ThemeInterface> & string, - ThemeInterface extends Themthem = Themthem, ->(component: Key) { - return function generateCSSVariables(config: { - [Token in DesignToken<'component', Key, ThemeInterface>]+?: string; - }) { - const variables: string[] = []; - for (const key of getKeys(config)) { - variables.push( - `${cssVariable<'component', Key, typeof key, ThemeInterface>( - 'component', - component, - key, - { bare: true }, - )}: ${config[key]};`, - ); - } - return variables; - }; -} - -export function generateGlobalCSSVariables< - ThemeInterface extends Themthem = Themthem, ->(config: { - [Key in DTBoxKey<'global', ThemeInterface> & string]+?: { - [Token in DesignToken<'global', Key, ThemeInterface>]+?: string; - }; -}) { - const variables: string[] = []; - for (const key of getKeys(config)) { - const configValue = config[key]!; - for (const token of getKeys(configValue)) { - const variable = configValue[token]; - variables.push( - `${cssVariable('global', key as never, token as never, { - bare: true, - })}: ${variable};`, - ); - } - } - return variables; -} diff --git a/src/css-variables-generator.test.ts b/src/css-variables-generator.test.ts new file mode 100644 index 0000000..799b16c --- /dev/null +++ b/src/css-variables-generator.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from 'vitest'; +import { + createComponentCSSVariablesGenerator, + createGlobalCSSVariablesGenerator, + generateGlobalCSSVariables, +} from './css-variables-generator'; +import { gVar } from './helpers'; + +interface TestThemthem { + global: { + foo: ['bar']; + bar: ['baz']; + }; + component: { + Foo: ['bar']; + Baz: ['baz']; + }; +} + +interface UsageTheme { + global: { + palette: ['black', 'white', 'my-custom-color']; + tokens: { + colors: ['default', 'accent']; + sizes: ['sm', 'md', 'lg', 'xl']; + }; + uniqueToken: ['']; + }; + component: {}; +} + +describe('generateGlobalCSSVariables', () => { + it('should returns an array of css variable affections', () => { + expect( + generateGlobalCSSVariables({ + foo: { + bar: 'value', + }, + bar: { + baz: 'other-value', + }, + }), + ).toEqual(['--global-foo-bar: value;', '--global-bar-baz: other-value;']); + + const expected = [ + '--global-palette-black: #000;', + '--global-palette-white: #fff;', + '--global-palette-my-custom-color: #298af3;', + '--global-tokens-colors-default: var(--global-palette-black);', + '--global-tokens-colors-accent: var(--global-palette-my-custom-color);', + '--global-tokens-sizes-sm: 4px;', + '--global-tokens-sizes-md: 8px;', + '--global-tokens-sizes-lg: 12px;', + '--global-tokens-sizes-xl: 20px;', + '--global-uniqueToken: #333;', + ]; + + expect( + generateGlobalCSSVariables({ + palette: { + black: '#000', + white: '#fff', + 'my-custom-color': '#298af3', + }, + tokens: { + colors: { + default: gVar('palette.black'), + accent: gVar('palette.my-custom-color'), + }, + sizes: { + sm: '4px', + md: '8px', + lg: '12px', + xl: '20px', + }, + }, + uniqueToken: { + '': '#333', + }, + }), + ).toEqual(expected); + }); +}); + +describe('createGlobalCSSVariableGenerator', () => { + it('should returns a variable generator scoped to the path specified', () => { + const fn = createGlobalCSSVariablesGenerator('foo'); + expect(fn).toBeInstanceOf(Function); + + expect(fn({ bar: 'value' })).toEqual(['--global-foo-bar: value;']); + }); +}); + +describe('createCSSVariableGenerator', () => { + it('should returns a variable generator scoped to the path specified', () => { + const fn = createComponentCSSVariablesGenerator('Foo'); + expect(fn).toBeInstanceOf(Function); + + expect(fn({ bar: 'value' })).toEqual(['--component-Foo-bar: value;']); + }); +}); diff --git a/src/css-variables-generator.ts b/src/css-variables-generator.ts new file mode 100644 index 0000000..0e1f239 --- /dev/null +++ b/src/css-variables-generator.ts @@ -0,0 +1,96 @@ +import type { PartialTokenPath, Themthem, TokenPath } from './token-box'; +import { cIdentifier, gIdentifier } from './helpers'; +import { getKeys } from './utils'; + +export type GeneratorConfig = Box extends string[] + ? { [Key in Box[number]]+?: string } + : { + [Key in keyof Box & string]+?: Box[Key] extends object ? GeneratorConfig : never; + }; + +export type CSSVariableGenerator = (config: GeneratorConfig) => string[]; + +export type TokenBoxFromPath< + Path extends string, + Box extends object, +> = Path extends `${infer Segment}.${infer Rest}` + ? Segment extends keyof Box + ? Box[Segment] extends object + ? TokenBoxFromPath + : never + : never + : Path extends keyof Box + ? Box[Path] + : never; + +function generateAssignments( + path: string, + box: Record | string, +): { path: string; value: string }[] { + if (typeof box === 'string') { + return [{ path, value: box }]; + } + + const assignments = []; + for (const key of getKeys(box)) { + assignments.push( + ...generateAssignments(path ? (key !== '' ? `${path}.${key}` : path) : key, box[key]), + ); + } + return assignments; +} + +export function generateGlobalCSSVariables( + config: GeneratorConfig, +): string[] { + const assignments = generateAssignments('', config); + const stringAssignments: string[] = []; + + for (const { path, value } of assignments) { + stringAssignments.push(`${gIdentifier(path as TokenPath)}: ${value};`); + } + + return stringAssignments; +} + +export function createGlobalCSSVariablesGenerator< + TI extends Themthem = Themthem, + Path extends PartialTokenPath = PartialTokenPath, +>( + p: Path, +): CSSVariableGenerator< + TokenBoxFromPath extends object ? TokenBoxFromPath : never +> { + return (config) => { + const assignments = generateAssignments(p, config); + const stringAssignments: string[] = []; + + for (const { path, value } of assignments) { + stringAssignments.push(`${gIdentifier(path as TokenPath)}: ${value};`); + } + + return stringAssignments; + }; +} + +export function createComponentCSSVariablesGenerator< + TI extends Themthem = Themthem, + Key extends keyof TI['component'] & string = keyof TI['component'] & string, +>( + key: Key, +): CSSVariableGenerator< + TokenBoxFromPath extends object + ? TokenBoxFromPath + : never +> { + return (config) => { + const assignments = generateAssignments(key, config); + const stringAssignments: string[] = []; + + for (const { path, value } of assignments) { + stringAssignments.push(`${cIdentifier(path as TokenPath)}: ${value};`); + } + + return stringAssignments; + }; +} diff --git a/src/helpers.test.ts b/src/helpers.test.ts index 8d2a009..732cdcf 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -1,48 +1,65 @@ import { describe, it, expect } from 'vitest'; -import { cssVariable } from './helpers'; +import { gIdentifier, gVar, cIdentifier, cVar } from './helpers'; interface TestThemthem { global: { foo: ['bar']; - bar: ['baz']; + bar: { baz: [''] }; + baz: { + test: ['foo']; + }; }; component: { Foo: ['bar']; - Baz: ['baz']; + Bar: { baz: [''] }; + Baz: { + test: ['foo']; + }; }; } -describe('cssVariable', () => { - it('should print the css variable usage by default', () => { - expect( - cssVariable<'global', 'foo', 'bar', TestThemthem>('global', 'foo', 'bar'), - ).toBe('var(--global-foo-bar)'); - expect( - cssVariable<'global', 'foo', 'bar', TestThemthem>( - 'global', - 'foo', - 'bar', - {}, - ), - ).toBe('var(--global-foo-bar)'); - expect( - cssVariable<'global', 'foo', 'bar', TestThemthem>( - 'global', - 'foo', - 'bar', - { bare: false }, - ), - ).toBe('var(--global-foo-bar)'); +describe('global', () => { + describe('globalCSSVariable', () => { + it('should return the variable usage formed from the path', () => { + expect(gVar('foo.bar')).toBe('var(--global-foo-bar)'); + expect(gVar('bar.baz')).toBe('var(--global-bar-baz)'); + expect(gVar('baz.test.foo')).toBe('var(--global-baz-test-foo)'); + expect(gVar('baz.test.foo', '22px')).toBe( + 'var(--global-baz-test-foo, 22px)', + ); + }); }); - it('should print the bare css variable when bare options is true', () => { - expect( - cssVariable<'global', 'foo', 'bar', TestThemthem>( - 'global', - 'foo', - 'bar', - { bare: true }, - ), - ).toBe('--global-foo-bar'); + describe('globalCSSVariableIdentifier', () => { + it('should return the variable usage formed from the path', () => { + expect(gIdentifier('foo.bar')).toBe('--global-foo-bar'); + expect(gIdentifier('baz.test.foo')).toBe( + '--global-baz-test-foo', + ); + }); + }); +}); + +describe('component', () => { + describe('componentCSSVariable', () => { + it('should return the variable usage formed from the path', () => { + expect(cVar('Foo.bar')).toBe('var(--component-Foo-bar)'); + expect(cVar('Bar.baz')).toBe('var(--component-Bar-baz)'); + expect(cVar('Baz.test.foo')).toBe( + 'var(--component-Baz-test-foo)', + ); + expect(cVar('Baz.test.foo', '10px')).toBe( + 'var(--component-Baz-test-foo, 10px)', + ); + }); + }); + + describe('componentCSSVariableIdentifier', () => { + it('should return the variable usage formed from the path', () => { + expect(cIdentifier('Foo.bar')).toBe('--component-Foo-bar'); + expect(cIdentifier('Baz.test.foo')).toBe( + '--component-Baz-test-foo', + ); + }); }); }); diff --git a/src/helpers.ts b/src/helpers.ts index 961e62e..0a282e7 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,57 +1,68 @@ -import { DTBoxType, DTBoxKey, DesignToken, Themthem } from './token-box'; +import type { Themthem, TokenPath } from './token-box'; -export type ThemthemVariable< - Type extends DTBoxType, - Key extends DTBoxKey & string, - Token extends DesignToken, - ThemeInterface extends Themthem = Themthem, -> = Type extends 'global' ? `--global-${Key}-${Token}` : `--${Key}-${Token}`; +export type Segments = Keys extends `${infer Key}.${infer RestKeys}` + ? `${Key}-${Segments}` + : Keys; + +export type ThemthemGlobalVariableName< + Box extends object, + Keys extends TokenPath, +> = `--global-${Segments}`; + +export type ThemthemComponentVariableName< + Box extends object, + Keys extends TokenPath, +> = `--component-${Segments}`; + +export type DefaultValuePart = DefaultValue extends + | '' + | undefined + ? '' + : `, ${DefaultValue}`; export type ThemthemVariableUsage< - Type extends DTBoxType, - Key extends DTBoxKey & string, - Token extends DesignToken, - ThemeInterface extends Themthem = Themthem, -> = `var(${ThemthemVariable})`; - -export function cssVariable< - Type extends DTBoxType, - Key extends DTBoxKey & string, - Token extends DesignToken, - ThemeInterface extends Themthem = Themthem, ->( - type: Type, - key: Key, - token: Token, - options: { bare: true }, -): ThemthemVariable; -export function cssVariable< - Type extends DTBoxType, - Key extends DTBoxKey & string, - Token extends DesignToken, - ThemeInterface extends Themthem = Themthem, + Variable extends string, + DefaultValue extends string | undefined, +> = `var(${Variable}${DefaultValuePart})`; + +export function gIdentifier< + TI extends Themthem = Themthem, + Path extends TokenPath = TokenPath, +>(path: Path): ThemthemGlobalVariableName { + const token = path.split('.').join('-') as Segments; + return `--global-${token}`; +} + +export function gVar< + TI extends Themthem = Themthem, + Path extends TokenPath = TokenPath, + DefaultValue extends string = string, >( - type: Type, - key: Key, - token: Token, - options?: { bare?: false }, -): ThemthemVariableUsage; -export function cssVariable< - Type extends DTBoxType, - Key extends DTBoxKey & string, - Token extends DesignToken, - ThemeInterface extends Themthem = Themthem, + path: Path, + defaultValue?: DefaultValue, +): ThemthemVariableUsage, DefaultValue> { + return `var(${gIdentifier(path)}${ + (defaultValue ? `, ${defaultValue}` : '') as DefaultValuePart + })`; +} + +export function cIdentifier< + TI extends Themthem = Themthem, + Path extends TokenPath = TokenPath, +>(path: Path): ThemthemComponentVariableName { + const token = path.split('.').join('-') as Segments; + return `--component-${token}`; +} + +export function cVar< + TI extends Themthem = Themthem, + Path extends TokenPath = TokenPath, + DefaultValue extends string = string, >( - type: Type, - key: Key, - token: Token, - { bare = false }: { bare?: boolean } = {}, -): - | ThemthemVariable - | ThemthemVariableUsage { - const variable = ( - type === 'global' ? `--global-${key}-${token}` : `--${key}-${token}` - ) as ThemthemVariable; - - return bare ? variable : `var(${variable})`; + path: Path, + defaultValue?: DefaultValue, +): ThemthemVariableUsage, DefaultValue> { + return `var(${cIdentifier(path)}${ + (defaultValue ? `, ${defaultValue}` : '') as DefaultValuePart + })`; } diff --git a/src/index.ts b/src/index.ts index 8f76fd6..b11018e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export * from './helpers'; -export * from './css-variable-generator'; +export * from './css-variables-generator'; export * from './token-box'; diff --git a/src/token-box.ts b/src/token-box.ts index 24dcf22..e7658d0 100644 --- a/src/token-box.ts +++ b/src/token-box.ts @@ -5,29 +5,30 @@ export interface Themthem { component: ComponentDesignTokenBox; } -export type DTBoxType = - keyof ThemeInterface; +export type BareToken = Box extends string[] ? Exclude : TokenPath; -export type DTBox< - Type extends DTBoxType, - ThemeInterface extends Themthem = Themthem, -> = ThemeInterface[Type]; - -export type DTBoxKey< - Type extends DTBoxType, - ThemeInterface extends Themthem = Themthem, -> = keyof DTBox; +export type TokenPath = Box extends string[] + ? Box[number] + : Box extends object + ? { + [Key in keyof Box]: Key extends string + ? '' extends TokenPath + ? `${Key}` | `${Key}.${BareToken}` + : `${Key}.${TokenPath}` + : never; + }[keyof Box] + : never; -export type DesignTokens< - Type extends DTBoxType, - Key extends DTBoxKey, - ThemeInterface extends Themthem = Themthem, -> = DTBox[Key]; +type Prepend = [Prefix] extends [never] + ? `${Path}` + : `${Prefix}.${Path}`; -export type DesignToken< - Type extends DTBoxType, - Key extends DTBoxKey, - ThemeInterface extends Themthem = Themthem, -> = DesignTokens extends string[] - ? DesignTokens[number] - : never; +export type PartialTokenPath = Box extends any[] + ? Prefix + : + | Prefix + | { + [Key in keyof Box & string]: Box[Key] extends object + ? PartialTokenPath> + : Prepend; + }[keyof Box & string]; diff --git a/src/type-tests/PartialTokenPath.ts b/src/type-tests/PartialTokenPath.ts new file mode 100644 index 0000000..c2b7cc7 --- /dev/null +++ b/src/type-tests/PartialTokenPath.ts @@ -0,0 +1,16 @@ +import { Expect, Equal } from './utils'; +import { PartialTokenPath } from '../token-box'; + +type interface1 = { test: ['a'] }; +type interface2 = { test: { foo: ['bar', 'baz'] } }; +type interface3 = { test: { foo: ['bar'] }; test2: { bar: ['baz'] } }; + +type expected1 = 'test'; +type expected2 = 'test' | 'test.foo'; +type expected3 = 'test' | 'test.foo' | 'test2' | 'test2.bar'; + +export type cases = [ + Expect, expected1>>, + Expect, expected2>>, + Expect, expected3>>, +]; diff --git a/src/type-tests/TokenPath.ts b/src/type-tests/TokenPath.ts new file mode 100644 index 0000000..da4ce8c --- /dev/null +++ b/src/type-tests/TokenPath.ts @@ -0,0 +1,19 @@ +import { Expect, Equal } from './utils'; +import { TokenPath } from '../token-box'; + +type interface1 = { test: ['a'] }; +type interface2 = { test: { foo: ['bar', 'baz'] } }; +type interface3 = { test: { foo: ['bar'] }; test2: { bar: ['baz'] } }; +type interface4 = { test: ['', 'abc'] }; + +type expected1 = 'test.a'; +type expected2 = 'test.foo.bar' | 'test.foo.baz'; +type expected3 = 'test.foo.bar' | 'test2.bar.baz'; +type expected4 = 'test' | 'test.abc'; + +export type cases = [ + Expect, expected1>>, + Expect, expected2>>, + Expect, expected3>>, + Expect, expected4>>, +]; diff --git a/src/type-tests/utils.ts b/src/type-tests/utils.ts new file mode 100644 index 0000000..28564f5 --- /dev/null +++ b/src/type-tests/utils.ts @@ -0,0 +1,10 @@ +export type Expect = T; +export type ExpectTrue = T; +export type ExpectFalse = T; +export type IsTrue = T; +export type IsFalse = T; + +export type NotEqual = true extends Equal ? false : true; +export type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 + ? true + : false; diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..7245423 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": "./src", + "outDir": "./dist", + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src"], + "exclude": ["src/*.test.ts", "src/type-tests/*"] + } + \ No newline at end of file From 316fce25b4fdb8a53c96036da88b44c861aa22c7 Mon Sep 17 00:00:00 2001 From: Christopher Debove Date: Mon, 5 Sep 2022 01:28:52 +0200 Subject: [PATCH 2/3] feat: rethink interface declaration --- README.md | 67 +++++++++++++++++------------ src/css-variables-generator.test.ts | 54 +++++++++++++++++------ src/css-variables-generator.ts | 27 +++++++++--- src/helpers.test.ts | 19 +++++--- src/helpers.ts | 18 +++++--- src/token-box.ts | 39 +++++++++++------ src/type-tests/PartialTokenPath.ts | 12 ++++-- src/type-tests/TokenPath.ts | 13 +++--- 8 files changed, 170 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 79aa163..f4ec144 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,11 @@ In the root of your sources (eg: `./src`) create a `themthem-interfaces.d.ts` an /// interface GlobalDesignTokenBox { - palette: ['black', 'white', 'my-custom-color']; + // This is a Values object and show that 'black', 'white' and 'my-custom-color' are values + palette: { $values: ['black', 'white', 'my-custom-color'] }; tokens: { - colors: ['default', 'accent'] - sizes: ['sm', 'md', 'lg', 'xl'] + colors: { $values: ['base', 'accent'] } + sizes: { $values: ['sm', 'md', 'lg', 'xl'] } } } ``` @@ -50,14 +51,24 @@ To define component token you need to augment `ComponentDesignTokenBox`. It's re interface ComponentDesignTokenBox { Input: { + // This is a Values object and show that 'color' is a value + $values: ['color']; border: { - default: ['color', 'size'], - focus: ['color', 'size'] + // This is a Modifiers object and show that 'size' + // can have a "$default" value and a "$focus" value + size: { + $modifiers: ['focus']; + }; + color: { + $modifiers: ['focus']; + }; } } } ``` +All the final tokens of your Token Box must be either a Value object or a Modifiers object. + ## API All the functions of the API is based on `GlobalDesignTokenBox` and `ComponentDesignTokenBox` by default. @@ -110,8 +121,8 @@ Generate a CSS variable usage (`var(--variable)`) based on your `ComponentDesign ```ts import { cVar } from 'themthem'; -cVar('Input.border.default.color'); // "var(--component-Input-border-default-color)" -cVar('Input.border.focus.color'); // "var(--component-Input-border-focus-color)" +cVar('Input.border.color'); // "var(--component-Input-border-color)" +cVar('Input.border.color.$focus'); // "var(--component-Input-border-color__focus)" ``` ### `cIdentifier(path)` @@ -127,8 +138,8 @@ Generate a CSS variable identifier (`--variable`) based on your `ComponentDesign ```ts import { cIdentifier } from 'themthem'; -cIdentifier('Input.border.default.color'); // "--component-Input-border-default-color" -cIdentifier('Input.border.focus.color'); // "--component-Input-border-focus-color" +cIdentifier('Input.border.color'); // "--component-Input-border-color" +cIdentifier('Input.border.color.$focus'); // "--component-Input-border-color__focus" ``` ### generateGlobalCSSVariables(config) @@ -152,7 +163,7 @@ const globalVariablesAssignments = generateGlobalCSSVariables({ }, tokens: { colors: { - default: gVar('palette.black'), + base: gVar('palette.black'), accent: gVar('palette.my-custom-color'), }, sizes: { @@ -170,7 +181,7 @@ const globalVariablesAssignments = generateGlobalCSSVariables({ // '--global-palette-black: #000;', // '--global-palette-white: #fff;', // '--global-palette-my-custom-color: #298af3;', -// '--global-tokens-colors-default: var(--global-palette-black);', +// '--global-tokens-colors-base: var(--global-palette-black);', // '--global-tokens-colors-accent: var(--global-palette-my-custom-color);', // '--global-tokens-sizes-sm: 4px;', // '--global-tokens-sizes-md: 8px;', @@ -194,27 +205,27 @@ import { createGlobalCSSVariableGenerator } from 'themthem'; const generateColorsTokensVariables = createGlobalCSSVariableGenerator('tokens.colors'); -const lightThemeColorsTokens = generateGlobalCSSVariables({ - default: gVar('palette.black'), +const lightThemeColorsTokens = generateColorsTokensVariables({ + base: gVar('palette.black'), accent: gVar('palette.my-custom-color'), }); // Returns: // // [ -// '--global-tokens-colors-default: var(--global-palette-black);', +// '--global-tokens-colors-base: var(--global-palette-black);', // '--global-tokens-colors-accent: var(--global-palette-my-custom-color);', // ] -const darkThemeColorsTokens = generateGlobalCSSVariables({ - default: gVar('palette.white'), +const darkThemeColorsTokens = generateColorsTokensVariables({ + base: gVar('palette.white'), accent: gVar('palette.my-custom-color'), }); // Returns: // // [ -// '--global-tokens-colors-default: var(--global-palette-white);', +// '--global-tokens-colors-base: var(--global-palette-white);', // '--global-tokens-colors-accent: var(--global-palette-my-custom-color);', // ] ``` @@ -236,21 +247,23 @@ const generateInputCSSVariables = createCSSVariablesGenerator('Input'); const inputVariablesAssignments = generateInputCSSVariables({ border: { - default: { - color: gVar('palette.black'), - size: '1px', + // Input.border.color -> { $modifiers: ['focus'] } + color: { + $default: gVar('palette.black'), + $focus: gVar('tokens.colors.accent') }, - focus: { - color: gVar('tokens.colors.accent'), - size: '2px' + // Input.border.size -> { $modifiers: ['focus'] } + size: { + $default: '1px', + $focus: '2px' } }, }); // [ -// '--component-Input-border-default-color: var(--global-palette-black);', -// '--component-Input-border-default-size: 1px;', -// '--component-Input-border-focus-color: var(--global-tokens-colors-accent);', -// '--component-Input-border-focus-size: 2px;', +// '--component-Input-border-color: var(--global-palette-black);', +// '--component-Input-border-color__focus: var(--global-tokens-colors-accent);', +// '--component-Input-border-size: 1px;', +// '--component-Input-border-size__focus: 2px;', // ] ``` \ No newline at end of file diff --git a/src/css-variables-generator.test.ts b/src/css-variables-generator.test.ts index 799b16c..d1e78b6 100644 --- a/src/css-variables-generator.test.ts +++ b/src/css-variables-generator.test.ts @@ -8,23 +8,33 @@ import { gVar } from './helpers'; interface TestThemthem { global: { - foo: ['bar']; - bar: ['baz']; + foo: { $values: ['bar'] }; + bar: { + $values: ['baz']; + color: { + $modifiers: ['hover', 'disabled']; + }; + }; }; component: { - Foo: ['bar']; - Baz: ['baz']; + Foo: { + $values: ['bar']; + color: { + $modifiers: ['hover', 'disabled']; + }; + }; + Baz: { $values: ['baz'] }; }; } interface UsageTheme { global: { - palette: ['black', 'white', 'my-custom-color']; + $values: ['uniqueToken']; + palette: { $values: ['black', 'white', 'my-custom-color'] }; tokens: { - colors: ['default', 'accent']; - sizes: ['sm', 'md', 'lg', 'xl']; + colors: { $values: ['default', 'accent'] }; + sizes: { $values: ['sm', 'md', 'lg', 'xl'] }; }; - uniqueToken: ['']; }; component: {}; } @@ -38,9 +48,20 @@ describe('generateGlobalCSSVariables', () => { }, bar: { baz: 'other-value', + color: { + $default: 'default-color', + $hover: 'hover-color', + $disabled: 'disabled-color', + }, }, }), - ).toEqual(['--global-foo-bar: value;', '--global-bar-baz: other-value;']); + ).toEqual([ + '--global-foo-bar: value;', + '--global-bar-baz: other-value;', + '--global-bar-color: default-color;', + '--global-bar-color__hover: hover-color;', + '--global-bar-color__disabled: disabled-color;', + ]); const expected = [ '--global-palette-black: #000;', @@ -74,9 +95,7 @@ describe('generateGlobalCSSVariables', () => { xl: '20px', }, }, - uniqueToken: { - '': '#333', - }, + uniqueToken: '#333', }), ).toEqual(expected); }); @@ -91,11 +110,18 @@ describe('createGlobalCSSVariableGenerator', () => { }); }); -describe('createCSSVariableGenerator', () => { +describe('createComponentCSSVariablesGenerator', () => { it('should returns a variable generator scoped to the path specified', () => { const fn = createComponentCSSVariablesGenerator('Foo'); expect(fn).toBeInstanceOf(Function); - expect(fn({ bar: 'value' })).toEqual(['--component-Foo-bar: value;']); + expect( + fn({ bar: 'value', color: { $default: 'default', $hover: 'hover', $disabled: 'disabled' } }), + ).toEqual([ + '--component-Foo-bar: value;', + '--component-Foo-color: default;', + '--component-Foo-color__hover: hover;', + '--component-Foo-color__disabled: disabled;', + ]); }); }); diff --git a/src/css-variables-generator.ts b/src/css-variables-generator.ts index 0e1f239..d8b897c 100644 --- a/src/css-variables-generator.ts +++ b/src/css-variables-generator.ts @@ -2,11 +2,28 @@ import type { PartialTokenPath, Themthem, TokenPath } from './token-box'; import { cIdentifier, gIdentifier } from './helpers'; import { getKeys } from './utils'; -export type GeneratorConfig = Box extends string[] - ? { [Key in Box[number]]+?: string } - : { - [Key in keyof Box & string]+?: Box[Key] extends object ? GeneratorConfig : never; - }; +type BoxValues = { + [k in '$values' extends keyof Box + ? Box['$values'] extends string[] + ? Box['$values'][number] + : never + : never]+?: string; +}; + +type BoxModifiers = { + [k in '$modifiers' extends keyof Box + ? Box['$modifiers'] extends string[] + ? `$${Box['$modifiers'][number]}` | '$default' + : never + : never]+?: string; +}; + +export type GeneratorConfig = BoxValues & + BoxModifiers & { + [k in keyof (Omit extends object + ? Omit + : never)]+?: GeneratorConfig; + }; export type CSSVariableGenerator = (config: GeneratorConfig) => string[]; diff --git a/src/helpers.test.ts b/src/helpers.test.ts index 732cdcf..842df63 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -3,17 +3,20 @@ import { gIdentifier, gVar, cIdentifier, cVar } from './helpers'; interface TestThemthem { global: { - foo: ['bar']; - bar: { baz: [''] }; + foo: { $values: ['bar'] }; + bar: { $values: ['baz'] }; baz: { - test: ['foo']; + test: { $values: ['foo'] }; }; }; component: { - Foo: ['bar']; - Bar: { baz: [''] }; + Foo: { $values: ['bar'] }; + Bar: { $values: ['baz'] }; Baz: { - test: ['foo']; + test: { $values: ['foo'] }; + foo: { + $modifiers: ['mod1', 'mod2']; + }; }; }; } @@ -48,6 +51,10 @@ describe('component', () => { expect(cVar('Baz.test.foo')).toBe( 'var(--component-Baz-test-foo)', ); + expect(cVar('Baz.foo.$mod1')).toBe( + 'var(--component-Baz-foo__mod1)', + ); + expect(cVar('Baz.foo')).toBe('var(--component-Baz-foo)'); expect(cVar('Baz.test.foo', '10px')).toBe( 'var(--component-Baz-test-foo, 10px)', ); diff --git a/src/helpers.ts b/src/helpers.ts index 0a282e7..ad8148b 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,7 +1,11 @@ import type { Themthem, TokenPath } from './token-box'; export type Segments = Keys extends `${infer Key}.${infer RestKeys}` - ? `${Key}-${Segments}` + ? RestKeys extends `$default` + ? Key + : RestKeys extends `$${infer R}` + ? `${Key}__${Segments}` + : `${Key}-${Segments}` : Keys; export type ThemthemGlobalVariableName< @@ -25,12 +29,17 @@ export type ThemthemVariableUsage< DefaultValue extends string | undefined, > = `var(${Variable}${DefaultValuePart})`; +function variabilize(path: Path): Segments { + return path.split('.').reduce((acc, s) => { + return s === '$default' ? acc : s.startsWith('$') ? `${acc}__${s.slice(1)}` : `${acc}-${s}`; + }) as Segments; +} + export function gIdentifier< TI extends Themthem = Themthem, Path extends TokenPath = TokenPath, >(path: Path): ThemthemGlobalVariableName { - const token = path.split('.').join('-') as Segments; - return `--global-${token}`; + return `--global-${variabilize(path)}`; } export function gVar< @@ -50,8 +59,7 @@ export function cIdentifier< TI extends Themthem = Themthem, Path extends TokenPath = TokenPath, >(path: Path): ThemthemComponentVariableName { - const token = path.split('.').join('-') as Segments; - return `--component-${token}`; + return `--component-${variabilize(path)}`; } export function cVar< diff --git a/src/token-box.ts b/src/token-box.ts index e7658d0..b898718 100644 --- a/src/token-box.ts +++ b/src/token-box.ts @@ -5,19 +5,30 @@ export interface Themthem { component: ComponentDesignTokenBox; } -export type BareToken = Box extends string[] ? Exclude : TokenPath; +export type SimpleBox = Omit; -export type TokenPath = Box extends string[] - ? Box[number] - : Box extends object - ? { - [Key in keyof Box]: Key extends string - ? '' extends TokenPath - ? `${Key}` | `${Key}.${BareToken}` - : `${Key}.${TokenPath}` - : never; - }[keyof Box] - : never; +export type TokenPath = + | ('$values' extends keyof Box + ? Box['$values'] extends string[] + ? Box['$values'][number] + : never + : never) + | ('$modifiers' extends keyof Box + ? Box['$modifiers'] extends string[] + ? `$${Box['$modifiers'][number]}` + : never + : never) + | (Box extends object + ? { + [Key in keyof SimpleBox]: Key extends string + ? SimpleBox[Key] extends object + ? TokenPath[Key]> extends `$${infer Modifier}` + ? Key | `${Key}.$${Modifier}` + : `${Key}.${TokenPath[Key]>}` + : never + : never; + }[keyof SimpleBox] + : never); type Prepend = [Prefix] extends [never] ? `${Path}` @@ -28,7 +39,7 @@ export type PartialTokenPath : | Prefix | { - [Key in keyof Box & string]: Box[Key] extends object + [Key in keyof Omit & string]: Box[Key] extends object ? PartialTokenPath> : Prepend; - }[keyof Box & string]; + }[keyof Omit & string]; diff --git a/src/type-tests/PartialTokenPath.ts b/src/type-tests/PartialTokenPath.ts index c2b7cc7..7484335 100644 --- a/src/type-tests/PartialTokenPath.ts +++ b/src/type-tests/PartialTokenPath.ts @@ -1,16 +1,22 @@ import { Expect, Equal } from './utils'; import { PartialTokenPath } from '../token-box'; -type interface1 = { test: ['a'] }; -type interface2 = { test: { foo: ['bar', 'baz'] } }; -type interface3 = { test: { foo: ['bar'] }; test2: { bar: ['baz'] } }; +type interface1 = { test: { $values: ['a'] } }; +type interface2 = { test: { foo: { $values: ['bar', 'baz'] } } }; +type interface3 = { test: { foo: { $values: ['bar'] } }; test2: { bar: { $values: ['baz'] } } }; +type interface4 = { test: { foo: { $modifiers: ['mod1', 'mod2'] } } }; +type interface5 = { test: { foo: { $modifiers: ['mod1', 'mod2'] }; bar: { $values: ['baz'] } } }; type expected1 = 'test'; type expected2 = 'test' | 'test.foo'; type expected3 = 'test' | 'test.foo' | 'test2' | 'test2.bar'; +type expected4 = 'test' | 'test.foo'; +type expected5 = 'test' | 'test.foo' | 'test.bar'; export type cases = [ Expect, expected1>>, Expect, expected2>>, Expect, expected3>>, + Expect, expected4>>, + Expect, expected5>>, ]; diff --git a/src/type-tests/TokenPath.ts b/src/type-tests/TokenPath.ts index da4ce8c..b9cf688 100644 --- a/src/type-tests/TokenPath.ts +++ b/src/type-tests/TokenPath.ts @@ -1,19 +1,22 @@ import { Expect, Equal } from './utils'; import { TokenPath } from '../token-box'; -type interface1 = { test: ['a'] }; -type interface2 = { test: { foo: ['bar', 'baz'] } }; -type interface3 = { test: { foo: ['bar'] }; test2: { bar: ['baz'] } }; -type interface4 = { test: ['', 'abc'] }; +type interface1 = { test: { $values: ['a'] } }; +type interface2 = { test: { foo: { $values: ['bar', 'baz'] } } }; +type interface3 = { test: { foo: { $values: ['bar'] } }; test2: { bar: { $values: ['baz'] } } }; +type interface4 = { test: { foo: { $modifiers: ['mod1', 'mod2'] } } }; +type interface5 = { test: { foo: { $modifiers: ['mod1', 'mod2'] }; bar: { $values: ['baz'] } } }; type expected1 = 'test.a'; type expected2 = 'test.foo.bar' | 'test.foo.baz'; type expected3 = 'test.foo.bar' | 'test2.bar.baz'; -type expected4 = 'test' | 'test.abc'; +type expected4 = 'test.foo' | 'test.foo.$mod1' | 'test.foo.$mod2'; +type expected5 = 'test.foo' | 'test.foo.$mod1' | 'test.foo.$mod2' | 'test.bar.baz'; export type cases = [ Expect, expected1>>, Expect, expected2>>, Expect, expected3>>, Expect, expected4>>, + Expect, expected5>>, ]; From e08743dd0f09b240f4837956d2f3bcfe864eef1e Mon Sep 17 00:00:00 2001 From: Christopher Debove Date: Sat, 17 Sep 2022 04:16:56 +0200 Subject: [PATCH 3/3] feat: rethink entry points and generator fn names --- .circleci/config.yml | 137 ++++++++-------- .gitignore | 3 + .prettierignore | 2 + .prettierrc.json | 10 +- README.md | 238 ++++++++++++++-------------- package.json | 18 ++- scripts/make-dts.js | 13 ++ src/component.ts | 2 + src/css-variables-generator.test.ts | 17 +- src/css-variables-generator.ts | 14 +- src/global.ts | 5 + src/utils.ts | 2 +- tsconfig.build.json | 45 +++--- vite.config.ts | 19 ++- 14 files changed, 294 insertions(+), 231 deletions(-) create mode 100644 .prettierignore create mode 100644 scripts/make-dts.js create mode 100644 src/component.ts create mode 100644 src/global.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index be9dac8..b11fca7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,7 +17,7 @@ executors: node: working_directory: ~/repo docker: - - image: circleci/node:14.18 + - image: circleci/node:14.18 # ------------------------------ # Commands @@ -26,15 +26,15 @@ executors: commands: checkout-and-restore-cache: steps: - - checkout - - restore_cache: - key: &cache-key << pipeline.parameters.cache-version >>-node14-dependencies-{{ arch }}-{{ checksum "./yarn.lock" }} + - checkout + - restore_cache: + key: &cache-key << pipeline.parameters.cache-version >>-node14-dependencies-{{ arch }}-{{ checksum "./yarn.lock" }} set-git-write-access: steps: - - run: - name: Setup git origin with write access - command: git remote set-url origin "https://$GITHUB_TOKEN@github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" + - run: + name: Setup git origin with write access + command: git remote set-url origin "https://$GITHUB_TOKEN@github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" # ------------------------------ # Jobs @@ -44,57 +44,56 @@ jobs: dependencies: executor: node steps: - - checkout-and-restore-cache - - run: - name: Install dependencies - command: yarn --prefer-offline --pure-lockfile --ignore-engines - - save_cache: - paths: - - ./node_modules - key: *cache-key + - checkout-and-restore-cache + - run: + name: Install dependencies + command: yarn --prefer-offline --pure-lockfile --ignore-engines + - save_cache: + paths: + - ./node_modules + key: *cache-key unit-test: executor: node steps: - - checkout-and-restore-cache - - run: - name: Run tests - command: yarn test + - checkout-and-restore-cache + - run: + name: Run tests + command: yarn test build: executor: node steps: - - checkout-and-restore-cache - - run: - name: Build sources - command: yarn build - - persist_to_workspace: - root: ~/repo - paths: - - dist - + - checkout-and-restore-cache + - run: + name: Build sources + command: yarn build + - persist_to_workspace: + root: ~/repo + paths: + - dist + dry-release: executor: node steps: - - checkout-and-restore-cache - - attach_workspace: - at: ~/repo - - set-git-write-access - - run: - name: Create release - command: yarn semantic-release --dry-run --branch ${CIRCLE_BRANCH} + - checkout-and-restore-cache + - attach_workspace: + at: ~/repo + - set-git-write-access + - run: + name: Create release + command: yarn semantic-release --dry-run --branch ${CIRCLE_BRANCH} release: executor: node steps: - - checkout-and-restore-cache - - attach_workspace: - at: ~/repo - - set-git-write-access - - run: - name: Create release - command: yarn semantic-release - + - checkout-and-restore-cache + - attach_workspace: + at: ~/repo + - set-git-write-access + - run: + name: Create release + command: yarn semantic-release # ------------------------------ # Workflow @@ -103,29 +102,29 @@ jobs: workflows: main-workflow: jobs: - - dependencies - - unit-test: - requires: - - dependencies - - build: - requires: - - dependencies - - dry-release: - context: NpmReleasePublish - requires: - - unit-test - - build - - hold-release: - type: approval - filters: - branches: - only: main - - release: - context: NpmReleasePublish - requires: - - unit-test - - build - - hold-release - filters: - branches: - only: main \ No newline at end of file + - dependencies + - unit-test: + requires: + - dependencies + - build: + requires: + - dependencies + - dry-release: + context: NpmReleasePublish + requires: + - unit-test + - build + - hold-release: + type: approval + filters: + branches: + only: main + - release: + context: NpmReleasePublish + requires: + - unit-test + - build + - hold-release + filters: + branches: + only: main diff --git a/.gitignore b/.gitignore index a547bf3..bce9e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +/*.d.ts +!/interfaces.d.ts \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9f31556 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +CHANGELOG.md +dist/ \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 0810850..6fc6d66 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,6 +1,6 @@ { - "singleQuote": true, - "trailingComma": "all", - "semi": true, - "printWidth": 100 -} \ No newline at end of file + "singleQuote": true, + "trailingComma": "all", + "semi": true, + "printWidth": 100 +} diff --git a/README.md b/README.md index f4ec144..d5f2959 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,12 @@ In the root of your sources (eg: `./src`) create a `themthem-interfaces.d.ts` an /// interface GlobalDesignTokenBox { - // This is a Values object and show that 'black', 'white' and 'my-custom-color' are values - palette: { $values: ['black', 'white', 'my-custom-color'] }; - tokens: { - colors: { $values: ['base', 'accent'] } - sizes: { $values: ['sm', 'md', 'lg', 'xl'] } - } + // This is a Values object and show that 'black', 'white' and 'my-custom-color' are values + palette: { $values: ['black', 'white', 'my-custom-color'] }; + tokens: { + colors: { $values: ['base', 'accent'] }; + sizes: { $values: ['sm', 'md', 'lg', 'xl'] }; + }; } ``` @@ -50,20 +50,20 @@ To define component token you need to augment `ComponentDesignTokenBox`. It's re /// interface ComponentDesignTokenBox { - Input: { - // This is a Values object and show that 'color' is a value - $values: ['color']; - border: { - // This is a Modifiers object and show that 'size' - // can have a "$default" value and a "$focus" value - size: { - $modifiers: ['focus']; - }; - color: { - $modifiers: ['focus']; - }; - } - } + Input: { + // This is a Values object and show that 'color' is a value + $values: ['color']; + border: { + // This is a Modifiers object and show that 'size' + // can have a "$default" value and a "$focus" value + size: { + $modifiers: ['focus']; + }; + color: { + $modifiers: ['focus']; + }; + }; + }; } ``` @@ -74,105 +74,73 @@ All the final tokens of your Token Box must be either a Value object or a Modifi All the functions of the API is based on `GlobalDesignTokenBox` and `ComponentDesignTokenBox` by default. So the example on this section will all be based on the augmentation done in the [Usage](#usage) section. -### `gVar(path)` +### Global theme + +#### `gVar(path)` Generate a CSS variable usage (`var(--variable)`) based on your `GlobalDesignTokenBox`. -| Parameter | Type | Description | Default value | -|---|---|---|---| -| path | `string` | The path to your token || +| Parameter | Type | Description | Default value | +| --------- | -------- | ---------------------- | ------------- | +| path | `string` | The path to your token | | `@returns {string} The CSS variable usage of your token` ```ts -import { gVar } from 'themthem'; +import { gVar } from 'themthem/global'; gVar('palette.black'); // "var(--global-palette-black)" gVar('tokens.colors.accent'); // "var(--global-tokens-colors-accent)" ``` -### `gIdentifier(path)` +#### `gIdentifier(path)` Generate a CSS variable identifier (`--variable`) based on your `GlobalDesignTokenBox`. -| Parameter | Type | Description | Default value | -|---|---|---|---| -| path | `string` | The path to your token || +| Parameter | Type | Description | Default value | +| --------- | -------- | ---------------------- | ------------- | +| path | `string` | The path to your token | | `@returns {string} The CSS variable identifier of your token` ```ts -import { gIdentifier } from 'themthem'; +import { gIdentifier } from 'themthem/global'; gIdentifier('palette.black'); // "--global-palette-black" gIdentifier('tokens.colors.accent'); // "--global-tokens-colors-accent" ``` -### `cVar(path)` - -Generate a CSS variable usage (`var(--variable)`) based on your `ComponentDesignTokenBox`. - -| Parameter | Type | Description | Default value | -|---|---|---|---| -| path | `string` | The path to your token || - -`@returns {string} The CSS Variable usage of your token` - -```ts -import { cVar } from 'themthem'; - -cVar('Input.border.color'); // "var(--component-Input-border-color)" -cVar('Input.border.color.$focus'); // "var(--component-Input-border-color__focus)" -``` - -### `cIdentifier(path)` - -Generate a CSS variable identifier (`--variable`) based on your `ComponentDesignTokenBox`. - -| Parameter | Type | Description | Default value | -|---|---|---|---| -| path | `string` | The path to your token || - -`@returns {string} The CSS variable identifier of your token` - -```ts -import { cIdentifier } from 'themthem'; - -cIdentifier('Input.border.color'); // "--component-Input-border-color" -cIdentifier('Input.border.color.$focus'); // "--component-Input-border-color__focus" -``` - -### generateGlobalCSSVariables(config) +#### `generateVars(config)` Generate CSS variables assignments for you global design tokens based on your theme. -| Parameter | Type | Description | Default value | -|---|---|---|---| -| config | Object | A config object assigning values to your global design tokens || +| Parameter | Type | Description | Default value | +| --------- | ------ | ------------------------------------------------------------- | ------------- | +| config | Object | A config object assigning values to your global design tokens | | `@returns {string[]} An array of CSS variables declarations` ```ts -import { generateGlobalCSSVariables } from 'themthem'; - -const globalVariablesAssignments = generateGlobalCSSVariables({ - palette: { - black: '#000', - white: '#fff', - 'my-custom-color': '#298af3', +import { generateVars } from 'themthem/global'; + +const globalVars = generateVars({ + palette: { + black: '#000', + white: '#fff', + 'my-custom-color': '#298af3', + }, + tokens: { + colors: { + base: gVar('palette.black'), + accent: gVar('palette.my-custom-color'), }, - tokens: { - colors: { - base: gVar('palette.black'), - accent: gVar('palette.my-custom-color'), - }, - sizes: { - sm: '4px', - md: '8px', - lg: '12px', - xl: '20px', - }, + sizes: { + sm: '4px', + md: '8px', + lg: '12px', + xl: '20px', }, + }, }); // Returns: @@ -190,24 +158,24 @@ const globalVariablesAssignments = generateGlobalCSSVariables({ // ] ``` -### createGlobalCSSVariablesGenerator(path) +#### `createGenerator(path)` Create a function which lets you generate CSS variables assignments for a part of your global design tokens. -| Parameter | Type | Description | Default value | -|---|---|---|---| -| path | `string` | The path to the part of the theme you want to configure || +| Parameter | Type | Description | Default value | +| --------- | -------- | ------------------------------------------------------- | ------------- | +| path | `string` | The path to the part of the theme you want to configure | | `@returns {(config): string[]} A function which generates CSS variables assignments for the specified part of your global design` ```ts -import { createGlobalCSSVariableGenerator } from 'themthem'; +import { createGenerator } from 'themthem/global'; -const generateColorsTokensVariables = createGlobalCSSVariableGenerator('tokens.colors'); +const generateColorsVars = createGenerator('tokens.colors'); -const lightThemeColorsTokens = generateColorsTokensVariables({ - base: gVar('palette.black'), - accent: gVar('palette.my-custom-color'), +const lightThemeColorsTokens = generateColorsVars({ + base: gVar('palette.black'), + accent: gVar('palette.my-custom-color'), }); // Returns: @@ -217,9 +185,9 @@ const lightThemeColorsTokens = generateColorsTokensVariables({ // '--global-tokens-colors-accent: var(--global-palette-my-custom-color);', // ] -const darkThemeColorsTokens = generateColorsTokensVariables({ - base: gVar('palette.white'), - accent: gVar('palette.my-custom-color'), +const darkThemeColorsTokens = generateColorsVars({ + base: gVar('palette.white'), + accent: gVar('palette.my-custom-color'), }); // Returns: @@ -230,34 +198,70 @@ const darkThemeColorsTokens = generateColorsTokensVariables({ // ] ``` -### createComponentCSSVariableGenerator(component) +### Component Theme + +#### `cVar(path)` + +Generate a CSS variable usage (`var(--variable)`) based on your `ComponentDesignTokenBox`. + +| Parameter | Type | Description | Default value | +| --------- | -------- | ---------------------- | ------------- | +| path | `string` | The path to your token | | + +`@returns {string} The CSS Variable usage of your token` + +```ts +import { cVar } from 'themthem/component'; + +cVar('Input.border.color'); // "var(--component-Input-border-color)" +cVar('Input.border.color.$focus'); // "var(--component-Input-border-color__focus)" +``` + +#### `cIdentifier(path)` + +Generate a CSS variable identifier (`--variable`) based on your `ComponentDesignTokenBox`. + +| Parameter | Type | Description | Default value | +| --------- | -------- | ---------------------- | ------------- | +| path | `string` | The path to your token | | + +`@returns {string} The CSS variable identifier of your token` + +```ts +import { cIdentifier } from 'themthem/component'; + +cIdentifier('Input.border.color'); // "--component-Input-border-color" +cIdentifier('Input.border.color.$focus'); // "--component-Input-border-color__focus" +``` + +#### `createGenerator(path)` Create a function which lets you generate CSS variables assignments for a component. -| Parameter | Type | Description | Default value | -|---|---|---|---| -| component | `keyof Themthem['component']` | The name of the component || +| Parameter | Type | Description | Default value | +| --------- | ----------------------------- | ------------------------- | ------------- | +| path | `string` | The path to the part of the component you want to configure | | `@returns {(config): string[]} A function which generates CSS variables assignments for the specified component` ```ts -import { createCSSVariablesGenerator } from 'themthem'; +import { createGenerator } from 'themthem/component'; -const generateInputCSSVariables = createCSSVariablesGenerator('Input'); +const generateInputVars = createGenerator('Input'); -const inputVariablesAssignments = generateInputCSSVariables({ - border: { - // Input.border.color -> { $modifiers: ['focus'] } - color: { - $default: gVar('palette.black'), - $focus: gVar('tokens.colors.accent') - }, - // Input.border.size -> { $modifiers: ['focus'] } - size: { - $default: '1px', - $focus: '2px' - } +const inputVars = generateInputVars({ + border: { + // Input.border.color -> { $modifiers: ['focus'] } + color: { + $default: gVar('palette.black'), + $focus: gVar('tokens.colors.accent'), + }, + // Input.border.size -> { $modifiers: ['focus'] } + size: { + $default: '1px', + $focus: '2px', }, + }, }); // [ @@ -266,4 +270,4 @@ const inputVariablesAssignments = generateInputCSSVariables({ // '--component-Input-border-size: 1px;', // '--component-Input-border-size__focus: 2px;', // ] -``` \ No newline at end of file +``` diff --git a/package.json b/package.json index 02cce6e..1ba5093 100644 --- a/package.json +++ b/package.json @@ -18,20 +18,32 @@ "url": "https://github.com/ChibiBlasphem/themthem.git" }, "types": "./dist/index.d.ts", - "main": "./dist/themthem.umd.js", + "main": "./dist/themthem.cjs.js", "module": "./dist/themthem.es.js", "files": [ "dist", - "interfaces.d.ts" + "*.d.ts" ], "exports": { ".": { + "types": "./dist/index.d.ts", "import": "./dist/themthem.es.js", - "require": "./dist/themthem.umd.js" + "require": "./dist/themthem.cjs.js" + }, + "./component": { + "types": "./dist/component.d.ts", + "import": "./dist/component.es.js", + "require": "./dist/component.cjs.js" + }, + "./global": { + "types": "./dist/global.d.ts", + "import": "./dist/global.es.js", + "require": "./dist/global.cjs.js" } }, "scripts": { "build": "tsc --noEmit && vite build && tsc -p tsconfig.build.json --emitDeclarationOnly", + "postbuild": "node ./scripts/make-dts", "test": "vitest --run --passWithNoTests", "prepare": "husky install" }, diff --git a/scripts/make-dts.js b/scripts/make-dts.js new file mode 100644 index 0000000..aef156a --- /dev/null +++ b/scripts/make-dts.js @@ -0,0 +1,13 @@ +const fs = require('fs'); +const path = require('path'); +const package = require('../package.json'); + +Object.keys(package.exports) + .map((entry) => entry.replace(/^\.\/?/, '')) + .filter(Boolean) + .forEach((entry) => { + fs.writeFileSync( + path.resolve(__dirname, '../', `${entry}.d.ts`), + `export * from './dist/${entry}';`, + ); + }); diff --git a/src/component.ts b/src/component.ts new file mode 100644 index 0000000..3046d6f --- /dev/null +++ b/src/component.ts @@ -0,0 +1,2 @@ +export { createComponentCSSVariablesGenerator as createGenerator } from './css-variables-generator'; +export { cVar, cIdentifier } from './helpers'; diff --git a/src/css-variables-generator.test.ts b/src/css-variables-generator.test.ts index d1e78b6..a693e96 100644 --- a/src/css-variables-generator.test.ts +++ b/src/css-variables-generator.test.ts @@ -112,16 +112,27 @@ describe('createGlobalCSSVariableGenerator', () => { describe('createComponentCSSVariablesGenerator', () => { it('should returns a variable generator scoped to the path specified', () => { - const fn = createComponentCSSVariablesGenerator('Foo'); - expect(fn).toBeInstanceOf(Function); + const fnComponent = createComponentCSSVariablesGenerator('Foo'); + expect(fnComponent).toBeInstanceOf(Function); expect( - fn({ bar: 'value', color: { $default: 'default', $hover: 'hover', $disabled: 'disabled' } }), + fnComponent({ + bar: 'value', + color: { $default: 'default', $hover: 'hover', $disabled: 'disabled' }, + }), ).toEqual([ '--component-Foo-bar: value;', '--component-Foo-color: default;', '--component-Foo-color__hover: hover;', '--component-Foo-color__disabled: disabled;', ]); + + const fnOnPath = createComponentCSSVariablesGenerator('Foo.color'); + expect(fnOnPath).toBeInstanceOf(Function); + + expect(fnOnPath({ $hover: 'hover', $disabled: 'disabled' })).toEqual([ + '--component-Foo-color__hover: hover;', + '--component-Foo-color__disabled: disabled;', + ]); }); }); diff --git a/src/css-variables-generator.ts b/src/css-variables-generator.ts index d8b897c..a03e819 100644 --- a/src/css-variables-generator.ts +++ b/src/css-variables-generator.ts @@ -74,12 +74,12 @@ export function createGlobalCSSVariablesGenerator< TI extends Themthem = Themthem, Path extends PartialTokenPath = PartialTokenPath, >( - p: Path, + path: Path, ): CSSVariableGenerator< TokenBoxFromPath extends object ? TokenBoxFromPath : never > { return (config) => { - const assignments = generateAssignments(p, config); + const assignments = generateAssignments(path, config); const stringAssignments: string[] = []; for (const { path, value } of assignments) { @@ -92,16 +92,16 @@ export function createGlobalCSSVariablesGenerator< export function createComponentCSSVariablesGenerator< TI extends Themthem = Themthem, - Key extends keyof TI['component'] & string = keyof TI['component'] & string, + Path extends PartialTokenPath = PartialTokenPath, >( - key: Key, + path: Path, ): CSSVariableGenerator< - TokenBoxFromPath extends object - ? TokenBoxFromPath + TokenBoxFromPath extends object + ? TokenBoxFromPath : never > { return (config) => { - const assignments = generateAssignments(key, config); + const assignments = generateAssignments(path, config); const stringAssignments: string[] = []; for (const { path, value } of assignments) { diff --git a/src/global.ts b/src/global.ts new file mode 100644 index 0000000..7237de3 --- /dev/null +++ b/src/global.ts @@ -0,0 +1,5 @@ +export { + createGlobalCSSVariablesGenerator as createGenerator, + generateGlobalCSSVariables as generateVars, +} from './css-variables-generator'; +export { gVar, gIdentifier } from './helpers'; diff --git a/src/utils.ts b/src/utils.ts index c7b0795..157b264 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,3 @@ -export function getKeys(o: O): (keyof O)[] { +export function getKeys(o: O): (keyof O)[] { return Object.keys(o) as (keyof O)[]; } diff --git a/tsconfig.build.json b/tsconfig.build.json index 7245423..f312220 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,24 +1,23 @@ { - "compilerOptions": { - "baseUrl": "./src", - "outDir": "./dist", - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "strict": true, - "sourceMap": true, - "resolveJsonModule": true, - "isolatedModules": true, - "esModuleInterop": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "skipLibCheck": true, - "declaration": true - }, - "include": ["src"], - "exclude": ["src/*.test.ts", "src/type-tests/*"] - } - \ No newline at end of file + "compilerOptions": { + "baseUrl": "./src", + "outDir": "./dist", + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src"], + "exclude": ["src/*.test.ts", "src/type-tests/*"] +} diff --git a/vite.config.ts b/vite.config.ts index b80c85b..9f2775a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,26 @@ /// +import { resolve } from 'path'; import { defineConfig } from 'vite'; export default defineConfig({ build: { lib: { - entry: './src/index.ts', - name: 'Themthem', - fileName: (format) => `themthem.${format}.js`, + entry: resolve(__dirname, 'src/index.ts'), + formats: ['cjs', 'es'], + }, + rollupOptions: { + input: { + themthem: resolve(__dirname, 'src/index.ts'), + component: resolve(__dirname, 'src/component.ts'), + global: resolve(__dirname, 'src/global.ts'), + }, + output: { + preserveModules: true, + entryFileNames: ({ name: filename }) => { + return `${filename}.[format].js`; + }, + }, }, }, });