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 1521513..d5f2959 100644
--- a/README.md
+++ b/README.md
@@ -33,8 +33,12 @@ 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']
+ // 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'] };
+ };
}
```
@@ -46,99 +50,224 @@ To define component token you need to augment `ComponentDesignTokenBox`. It's re
///
interface ComponentDesignTokenBox {
- Input: ['background-color', 'color', 'border-color', 'border-size']
+ 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'];
+ };
+ };
+ };
}
```
+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.
So the example on this section will all be based on the augmentation done in the [Usage](#usage) section.
-### `cssVariable(type, key, token, options?)`
+### Global theme
+
+#### `gVar(path)`
-Generate a CSS Variable based on your theme.
+Generate a CSS variable usage (`var(--variable)`) based on your `GlobalDesignTokenBox`.
-| 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 }` |
+| Parameter | Type | Description | Default value |
+| --------- | -------- | ---------------------- | ------------- |
+| 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 { gVar } from 'themthem/global';
-cssVariable('global', 'color', 'black', { bare: true }); // "--global-color-black"
-cssVariable('global', 'color', 'black'); // "var(--global-color-black)"
+gVar('palette.black'); // "var(--global-palette-black)"
+gVar('tokens.colors.accent'); // "var(--global-tokens-colors-accent)"
```
-### generateGlobalCSSVariables(config)
+#### `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/global';
+
+gIdentifier('palette.black'); // "--global-palette-black"
+gIdentifier('tokens.colors.accent'); // "--global-tokens-colors-accent"
+```
+
+#### `generateVars(config)`
Generate CSS variables assignments for you global design tokens based on your theme.
-| Parameter | Type | Description | Default value |
-|---|---|---|---|
-| config | Object | A 2-level deep 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({
+import { generateVars } from 'themthem/global';
+
+const globalVars = generateVars({
+ palette: {
+ black: '#000',
+ white: '#fff',
+ 'my-custom-color': '#298af3',
+ },
+ tokens: {
colors: {
- black: '#000',
- white: '#fff',
- 'my-custom-color': '#298af3'
+ base: gVar('palette.black'),
+ accent: gVar('palette.my-custom-color'),
},
sizes: {
- sm: '4px',
- md: '8px',
- lg: '12px',
- xl: '20px',
- }
+ sm: '4px',
+ md: '8px',
+ lg: '12px',
+ xl: '20px',
+ },
+ },
+});
+
+// Returns:
+//
+// [
+// '--global-palette-black: #000;',
+// '--global-palette-white: #fff;',
+// '--global-palette-my-custom-color: #298af3;',
+// '--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;',
+// '--global-tokens-sizes-lg: 12px;',
+// '--global-tokens-sizes-xl: 20px;',
+// ]
+```
+
+#### `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 | |
+
+`@returns {(config): string[]} A function which generates CSS variables assignments for the specified part of your global design`
+
+```ts
+import { createGenerator } from 'themthem/global';
+
+const generateColorsVars = createGenerator('tokens.colors');
+
+const lightThemeColorsTokens = generateColorsVars({
+ base: gVar('palette.black'),
+ accent: gVar('palette.my-custom-color'),
});
+// 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-tokens-colors-base: var(--global-palette-black);',
+// '--global-tokens-colors-accent: var(--global-palette-my-custom-color);',
+// ]
+
+const darkThemeColorsTokens = generateColorsVars({
+ base: gVar('palette.white'),
+ accent: gVar('palette.my-custom-color'),
+});
+
+// Returns:
+//
+// [
+// '--global-tokens-colors-base: var(--global-palette-white);',
+// '--global-tokens-colors-accent: var(--global-palette-my-custom-color);',
// ]
```
-### createCSSVariableGenerator(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({
- 'background-color': cssVariable('global', 'colors', 'white'),
- 'color': 'black',
- 'border-color': '#333',
- 'border-size': '1px'
+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',
+ },
+ },
});
// [
-// '--input-background-color: var(--global-colors-white);',
-// '--input-color: black;',
-// '--input-border-color: #333;',
-// '--input-border-size: 1px;',
+// '--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/package.json b/package.json
index 023fb60..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"
+ "dist",
+ "*.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 --emitDeclarationOnly && node ./scripts/typescript-replace.js",
+ "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-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..a693e96
--- /dev/null
+++ b/src/css-variables-generator.test.ts
@@ -0,0 +1,138 @@
+import { describe, it, expect } from 'vitest';
+import {
+ createComponentCSSVariablesGenerator,
+ createGlobalCSSVariablesGenerator,
+ generateGlobalCSSVariables,
+} from './css-variables-generator';
+import { gVar } from './helpers';
+
+interface TestThemthem {
+ global: {
+ foo: { $values: ['bar'] };
+ bar: {
+ $values: ['baz'];
+ color: {
+ $modifiers: ['hover', 'disabled'];
+ };
+ };
+ };
+ component: {
+ Foo: {
+ $values: ['bar'];
+ color: {
+ $modifiers: ['hover', 'disabled'];
+ };
+ };
+ Baz: { $values: ['baz'] };
+ };
+}
+
+interface UsageTheme {
+ global: {
+ $values: ['uniqueToken'];
+ palette: { $values: ['black', 'white', 'my-custom-color'] };
+ tokens: {
+ colors: { $values: ['default', 'accent'] };
+ sizes: { $values: ['sm', 'md', 'lg', 'xl'] };
+ };
+ };
+ component: {};
+}
+
+describe('generateGlobalCSSVariables', () => {
+ it('should returns an array of css variable affections', () => {
+ expect(
+ generateGlobalCSSVariables({
+ foo: {
+ bar: 'value',
+ },
+ bar: {
+ baz: 'other-value',
+ color: {
+ $default: 'default-color',
+ $hover: 'hover-color',
+ $disabled: 'disabled-color',
+ },
+ },
+ }),
+ ).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;',
+ '--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('createComponentCSSVariablesGenerator', () => {
+ it('should returns a variable generator scoped to the path specified', () => {
+ const fnComponent = createComponentCSSVariablesGenerator('Foo');
+ expect(fnComponent).toBeInstanceOf(Function);
+
+ expect(
+ 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
new file mode 100644
index 0000000..a03e819
--- /dev/null
+++ b/src/css-variables-generator.ts
@@ -0,0 +1,113 @@
+import type { PartialTokenPath, Themthem, TokenPath } from './token-box';
+import { cIdentifier, gIdentifier } from './helpers';
+import { getKeys } from './utils';
+
+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[];
+
+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,
+>(
+ path: Path,
+): CSSVariableGenerator<
+ TokenBoxFromPath extends object ? TokenBoxFromPath : never
+> {
+ return (config) => {
+ const assignments = generateAssignments(path, 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,
+ Path extends PartialTokenPath = PartialTokenPath,
+>(
+ path: Path,
+): CSSVariableGenerator<
+ TokenBoxFromPath extends object
+ ? TokenBoxFromPath
+ : never
+> {
+ return (config) => {
+ const assignments = generateAssignments(path, config);
+ const stringAssignments: string[] = [];
+
+ for (const { path, value } of assignments) {
+ stringAssignments.push(`${cIdentifier(path as TokenPath)}: ${value};`);
+ }
+
+ return stringAssignments;
+ };
+}
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/helpers.test.ts b/src/helpers.test.ts
index 8d2a009..842df63 100644
--- a/src/helpers.test.ts
+++ b/src/helpers.test.ts
@@ -1,48 +1,72 @@
import { describe, it, expect } from 'vitest';
-import { cssVariable } from './helpers';
+import { gIdentifier, gVar, cIdentifier, cVar } from './helpers';
interface TestThemthem {
global: {
- foo: ['bar'];
- bar: ['baz'];
+ foo: { $values: ['bar'] };
+ bar: { $values: ['baz'] };
+ baz: {
+ test: { $values: ['foo'] };
+ };
};
component: {
- Foo: ['bar'];
- Baz: ['baz'];
+ Foo: { $values: ['bar'] };
+ Bar: { $values: ['baz'] };
+ Baz: {
+ test: { $values: ['foo'] };
+ foo: {
+ $modifiers: ['mod1', 'mod2'];
+ };
+ };
};
}
-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.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)',
+ );
+ });
+ });
+
+ 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..ad8148b 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -1,57 +1,76 @@
-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}`
+ ? RestKeys extends `$default`
+ ? Key
+ : RestKeys extends `$${infer R}`
+ ? `${Key}__${Segments}`
+ : `${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})`;
+
+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 {
+ return `--global-${variabilize(path)}`;
+}
+
+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 {
+ return `--component-${variabilize(path)}`;
+}
+
+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..b898718 100644
--- a/src/token-box.ts
+++ b/src/token-box.ts
@@ -5,29 +5,41 @@ export interface Themthem {
component: ComponentDesignTokenBox;
}
-export type DTBoxType =
- keyof ThemeInterface;
+export type SimpleBox = Omit;
-export type DTBox<
- Type extends DTBoxType,
- ThemeInterface extends Themthem = Themthem,
-> = ThemeInterface[Type];
+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);
-export type DTBoxKey<
- Type extends DTBoxType,
- ThemeInterface extends Themthem = Themthem,
-> = keyof DTBox;
+type Prepend = [Prefix] extends [never]
+ ? `${Path}`
+ : `${Prefix}.${Path}`;
-export type DesignTokens<
- Type extends DTBoxType,
- Key extends DTBoxKey,
- ThemeInterface extends Themthem = Themthem,
-> = DTBox[Key];
-
-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 Omit & string]: Box[Key] extends object
+ ? PartialTokenPath>
+ : Prepend;
+ }[keyof Omit & string];
diff --git a/src/type-tests/PartialTokenPath.ts b/src/type-tests/PartialTokenPath.ts
new file mode 100644
index 0000000..7484335
--- /dev/null
+++ b/src/type-tests/PartialTokenPath.ts
@@ -0,0 +1,22 @@
+import { Expect, Equal } from './utils';
+import { PartialTokenPath } from '../token-box';
+
+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
new file mode 100644
index 0000000..b9cf688
--- /dev/null
+++ b/src/type-tests/TokenPath.ts
@@ -0,0 +1,22 @@
+import { Expect, Equal } from './utils';
+import { TokenPath } from '../token-box';
+
+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.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>>,
+];
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/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
new file mode 100644
index 0000000..f312220
--- /dev/null
+++ b/tsconfig.build.json
@@ -0,0 +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/*"]
+}
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`;
+ },
+ },
},
},
});