diff --git a/.changeset/shiny-bees-rest.md b/.changeset/shiny-bees-rest.md new file mode 100644 index 000000000..ee30dfac5 --- /dev/null +++ b/.changeset/shiny-bees-rest.md @@ -0,0 +1,81 @@ +--- +'@vanilla-extract/css': minor +--- + +Add `createGlobalThemeContract` function + +Creates a contract of globally scoped variable names for themes to implement. + +> 💡 This is useful if you want to make your theme contract available to non-JavaScript environments. + +```ts +// themes.css.ts +import { + createGlobalThemeContract, + createGlobalTheme +} from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract({ + color: { + brand: 'color-brand' + }, + font: { + body: 'font-body' + } +}); + +createGlobalTheme(':root', vars, { + color: { + brand: 'blue' + }, + font: { + body: 'arial' + } +}); +``` + +You can also provide a map function as the second argument which has access to the value and the object path. + +For example, you can automatically prefix all variable names. + +```ts +// themes.css.ts +import { + createGlobalThemeContract, + createGlobalTheme +} from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract( + { + color: { + brand: 'color-brand' + }, + font: { + body: 'font-body' + } + }, + (value) => `prefix-${value}` +); +``` + +You can also use the map function to automatically generate names from the object path, joining keys with a hyphen. + +```ts +// themes.css.ts +import { + createGlobalThemeContract, + createGlobalTheme +} from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract( + { + color: { + brand: null + }, + font: { + body: null + } + }, + (_value, path) => `prefix-${path.join('-')}` +); +``` \ No newline at end of file diff --git a/README.md b/README.md index f33237ca8..dd973ffcb 100644 --- a/README.md +++ b/README.md @@ -581,7 +581,7 @@ createGlobalTheme(':root', vars, { ### createThemeContract -Creates a contract for themes to implement. +Creates a contract of locally scoped variable names for themes to implement. **Ensure this function is called within a `.css.ts` context, otherwise variable names will be mismatched between files.** @@ -623,6 +623,81 @@ export const themeB = createTheme(vars, { }); ``` +### createGlobalThemeContract + +Creates a contract of globally scoped variable names for themes to implement. + +> 💡 This is useful if you want to make your theme contract available to non-JavaScript environments. + +```ts +// themes.css.ts + +import { + createGlobalThemeContract, + createGlobalTheme +} from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract({ + color: { + brand: 'color-brand' + }, + font: { + body: 'font-body' + } +}); + +createGlobalTheme(':root', vars, { + color: { + brand: 'blue' + }, + font: { + body: 'arial' + } +}); +``` + +You can also provide a map function as the second argument which has access to the value and the object path. + +For example, you can automatically prefix all variable names. + +```ts +// themes.css.ts + +import { + createGlobalThemeContract, + createGlobalTheme +} from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract({ + color: { + brand: 'color-brand' + }, + font: { + body: 'font-body' + } +}, (value) => `prefix-${value}`); +``` + +You can also use the map function to automatically generate names from the object path, joining keys with a hyphen. + +```ts +// themes.css.ts + +import { + createGlobalThemeContract, + createGlobalTheme +} from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract({ + color: { + brand: null + }, + font: { + body: null + } +}, (_value, path) => `prefix-${path.join('-')}`); +``` + ### assignVars Assigns a collection of CSS Variables anywhere within a style block. diff --git a/packages/css/src/vars.test.ts b/packages/css/src/vars.test.ts index 57b41c48e..841be4838 100644 --- a/packages/css/src/vars.test.ts +++ b/packages/css/src/vars.test.ts @@ -1,4 +1,4 @@ -import { fallbackVar } from './vars'; +import { fallbackVar, createGlobalThemeContract } from './vars'; describe('fallbackVar', () => { it('supports a single string fallback', () => { @@ -67,3 +67,175 @@ describe('fallbackVar', () => { }).toThrowErrorMatchingInlineSnapshot(`"Invalid variable name: INVALID"`); }); }); + +describe('createGlobalThemeContract', () => { + it('supports defining css vars via object properties', () => { + expect( + createGlobalThemeContract({ + color: { + red: 'color-red', + blue: 'color-blue', + green: 'color-green', + }, + }), + ).toMatchInlineSnapshot(` + Object { + "color": Object { + "blue": "var(--color-blue)", + "green": "var(--color-green)", + "red": "var(--color-red)", + }, + } + `); + }); + + it('ignores leading double hyphen', () => { + expect( + createGlobalThemeContract({ + color: { + red: '--color-red', + blue: '--color-blue', + green: '--color-green', + }, + }), + ).toMatchInlineSnapshot(` + Object { + "color": Object { + "blue": "var(--color-blue)", + "green": "var(--color-green)", + "red": "var(--color-red)", + }, + } + `); + }); + + it('supports adding a prefix', () => { + expect( + createGlobalThemeContract( + { + color: { + red: 'color-red', + blue: 'color-blue', + green: 'color-green', + }, + }, + (value) => `prefix-${value}`, + ), + ).toMatchInlineSnapshot(` + Object { + "color": Object { + "blue": "var(--prefix-color-blue)", + "green": "var(--prefix-color-green)", + "red": "var(--prefix-color-red)", + }, + } + `); + }); + + it('ignores leading double hyphen when adding a prefix', () => { + expect( + createGlobalThemeContract( + { + color: { + red: 'color-red', + blue: 'color-blue', + green: 'color-green', + }, + }, + (value) => `--prefix-${value}`, + ), + ).toMatchInlineSnapshot(` + Object { + "color": Object { + "blue": "var(--prefix-color-blue)", + "green": "var(--prefix-color-green)", + "red": "var(--prefix-color-red)", + }, + } + `); + }); + + it('supports path based names', () => { + expect( + createGlobalThemeContract( + { + color: { + red: null, + blue: null, + green: null, + }, + }, + (_, path) => `prefix-${path.join('-')}`, + ), + ).toMatchInlineSnapshot(` + Object { + "color": Object { + "blue": "var(--prefix-color-blue)", + "green": "var(--prefix-color-green)", + "red": "var(--prefix-color-red)", + }, + } + `); + }); + + it('errors when invalid property value', () => { + expect(() => + createGlobalThemeContract({ + color: { + // @ts-expect-error + red: null, + blue: 'color-blue', + green: 'color-green', + }, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid variable name for \\"color.red\\": null"`, + ); + }); + + it('errors when escaped property value', () => { + expect(() => + createGlobalThemeContract({ + color: { + red: 'color-red', + blue: "color'blue", + green: 'color-green', + }, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid variable name for \\"color.blue\\": color'blue"`, + ); + }); + + it('errors when property value starts with a number', () => { + expect(() => + createGlobalThemeContract({ + color: { + red: 'color-red', + blue: 'color-blue', + green: '123-color-green', + }, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid variable name for \\"color.green\\": 123-color-green"`, + ); + }); + + it('errors when invalid map value', () => { + expect(() => + createGlobalThemeContract( + { + color: { + red: 'color-red', + blue: 'color-blue', + green: 'color-green', + }, + }, + // @ts-expect-error + () => null, + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid variable name for \\"color.red\\": null"`, + ); + }); +}); diff --git a/packages/css/src/vars.ts b/packages/css/src/vars.ts index 90e5a38b1..40d682a33 100644 --- a/packages/css/src/vars.ts +++ b/packages/css/src/vars.ts @@ -8,7 +8,7 @@ import { import hash from '@emotion/hash'; import cssesc from 'cssesc'; -import { NullableTokens, ThemeVars } from './types'; +import { Tokens, NullableTokens, ThemeVars } from './types'; import { getAndIncrementRefCounter, getFileScope } from './fileScope'; import { validateContract } from './validateContract'; @@ -76,3 +76,36 @@ export function createThemeContract( return createVar(path.join('-')); }); } + +export function createGlobalThemeContract( + tokens: ThemeTokens, +): ThemeVars; +export function createGlobalThemeContract( + tokens: ThemeTokens, + mapFn: (value: string | null, path: Array) => string, +): ThemeVars; +export function createGlobalThemeContract( + tokens: Tokens | NullableTokens, + mapFn?: (value: string | null, path: Array) => string, +) { + return walkObject(tokens, (value, path) => { + const rawVarName = + typeof mapFn === 'function' + ? mapFn(value as string | null, path) + : (value as string); + + const varName = + typeof rawVarName === 'string' ? rawVarName.replace(/^\-\-/, '') : null; + + if ( + typeof varName !== 'string' || + varName !== cssesc(varName, { isIdentifier: true }) + ) { + throw new Error( + `Invalid variable name for "${path.join('.')}": ${varName}`, + ); + } + + return `var(--${varName})`; + }); +} diff --git a/site/docs/styling-api.md b/site/docs/styling-api.md index 56a06c46e..74c8f81b0 100644 --- a/site/docs/styling-api.md +++ b/site/docs/styling-api.md @@ -293,7 +293,7 @@ createGlobalTheme(':root', vars, { ## createThemeContract -Creates a contract for themes to implement. +Creates a contract of locally scoped variable names for themes to implement. **Ensure this function is called within a `.css.ts` context, otherwise variable names will be mismatched between files.** @@ -334,6 +334,84 @@ export const themeB = createTheme(vars, { }); ``` +## createGlobalThemeContract + +Creates a contract of globally scoped variable names for themes to implement. + +> 💡 This is useful if you want to make your theme contract available to non-JavaScript environments. + +```ts +// themes.css.ts +import { + createGlobalThemeContract, + createGlobalTheme +} from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract({ + color: { + brand: 'color-brand' + }, + font: { + body: 'font-body' + } +}); + +createGlobalTheme(':root', vars, { + color: { + brand: 'blue' + }, + font: { + body: 'arial' + } +}); +``` + +You can also provide a map function as the second argument which has access to the value and the object path. + +For example, you can automatically prefix all variable names. + +```ts +// themes.css.ts +import { + createGlobalThemeContract, + createGlobalTheme +} from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract( + { + color: { + brand: 'color-brand' + }, + font: { + body: 'font-body' + } + }, + (value) => `prefix-${value}` +); +``` + +You can also use the map function to automatically generate names from the object path, joining keys with a hyphen. + +```ts +// themes.css.ts +import { + createGlobalThemeContract, + createGlobalTheme +} from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract( + { + color: { + brand: null + }, + font: { + body: null + } + }, + (_value, path) => `prefix-${path.join('-')}` +); +``` + ## assignVars Assigns a collection of CSS Variables anywhere within a style block. diff --git a/site/src/DocsPage/DocsPage.css.ts b/site/src/DocsPage/DocsPage.css.ts index 77fd38fe1..faad9f6a2 100644 --- a/site/src/DocsPage/DocsPage.css.ts +++ b/site/src/DocsPage/DocsPage.css.ts @@ -4,7 +4,7 @@ import { vars } from '../themes.css'; import { responsiveStyle } from '../themeUtils'; const headerHeight = '90px'; -const sidebarWidth = '265px'; +const sidebarWidth = '300px'; export const bodyLock = style({ overflow: 'hidden!important',