diff --git a/packages/react/src/search-input/__tests__/SearchInput.test.js b/packages/react/src/search-input/__tests__/SearchInput.test.js
index 320ff502c3..cd5ac422a0 100644
--- a/packages/react/src/search-input/__tests__/SearchInput.test.js
+++ b/packages/react/src/search-input/__tests__/SearchInput.test.js
@@ -9,9 +9,7 @@ describe('SearchInput', () => {
it('should render correctly', async () => {
const sizes = ['sm', 'md', 'lg'];
const variants = ['outline', 'filled', 'flush', 'unstyled'];
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
<>
{sizes.map(size => (
@@ -94,9 +92,9 @@ describe('SearchInput', () => {
);
const inputControl = screen.getByTestId('input');
const input = inputControl.querySelector('input');
- const defaultBorderColor = '#5e5e5e'; // [dark] gray:60 / [light] gray:30
- const focusBorderColor = '#1e5ede'; // [dark] blue:60 / [light] blue:60
- const hoverBorderColor = '#578aef'; // [dark] blue:50 / [light] blue:50
+ const defaultBorderColor = 'var(--tonic-colors-gray-60)'; // [dark] gray:60 / [light] gray:30
+ const focusBorderColor = 'var(--tonic-colors-blue-60)'; // [dark] blue:60 / [light] blue:60
+ const hoverBorderColor = 'var(--tonic-colors-blue-50)'; // [dark] blue:50 / [light] blue:50
expect(input).toBeValid();
expect(document.body).toHaveFocus();
@@ -121,8 +119,8 @@ describe('SearchInput', () => {
);
const inputControl = screen.getByTestId('input');
const input = inputControl.querySelector('input');
- const errorBorderColor = '#f24c4f'; // [dark] red:50 / [light] red:60
- const focusBorderColor = '#1e5ede'; // [dark] blue:60 / [light] blue:60
+ const errorBorderColor = 'var(--tonic-colors-red-50)'; // [dark] red:50 / [light] red:60
+ const focusBorderColor = 'var(--tonic-colors-blue-60)'; // [dark] blue:60 / [light] blue:60
expect(input).toBeInvalid();
expect(document.body).toHaveFocus();
diff --git a/packages/react/src/stack/__tests__/Stack.test.js b/packages/react/src/stack/__tests__/Stack.test.js
index 51e9e28866..750eb4a347 100644
--- a/packages/react/src/stack/__tests__/Stack.test.js
+++ b/packages/react/src/stack/__tests__/Stack.test.js
@@ -6,9 +6,7 @@ import React from 'react';
describe('Stack', () => {
it('should render correctly', async () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
@@ -53,9 +51,7 @@ describe('Stack', () => {
});
it('should apply spacing between child elements', () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
render((
diff --git a/packages/react/src/switch/__tests__/Switch.test.js b/packages/react/src/switch/__tests__/Switch.test.js
index 7c76750937..26cc8cadac 100644
--- a/packages/react/src/switch/__tests__/Switch.test.js
+++ b/packages/react/src/switch/__tests__/Switch.test.js
@@ -5,9 +5,7 @@ import React, { useEffect, useRef } from 'react';
describe('Switch', () => {
it('should render correctly', async () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
<>
{/* Sizes */}
diff --git a/packages/react/src/table/__tests__/Table.test.js b/packages/react/src/table/__tests__/Table.test.js
index 2e78179726..c4fffcdd6f 100644
--- a/packages/react/src/table/__tests__/Table.test.js
+++ b/packages/react/src/table/__tests__/Table.test.js
@@ -60,9 +60,7 @@ describe('Table', () => {
];
it('should render correctly with `flexbox` layout', async () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
@@ -103,9 +101,7 @@ describe('Table', () => {
});
it('should render correctly with `table` layout', async () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
diff --git a/packages/react/src/tabs/__tests__/Tabs.test.js b/packages/react/src/tabs/__tests__/Tabs.test.js
index dd2304cd7d..1d541db517 100644
--- a/packages/react/src/tabs/__tests__/Tabs.test.js
+++ b/packages/react/src/tabs/__tests__/Tabs.test.js
@@ -5,9 +5,7 @@ import React from 'react';
describe('Tabs', () => {
it('should render correctly', async () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
diff --git a/packages/react/src/text/__tests__/Text.test.js b/packages/react/src/text/__tests__/Text.test.js
index 155d947726..295f694411 100644
--- a/packages/react/src/text/__tests__/Text.test.js
+++ b/packages/react/src/text/__tests__/Text.test.js
@@ -6,9 +6,7 @@ import React from 'react';
describe('TextLabel', () => {
it('should render correctly', async () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
Text
diff --git a/packages/react/src/text/__tests__/TextLabel.test.js b/packages/react/src/text/__tests__/TextLabel.test.js
index 502ca46a5d..e1d2eea2f7 100644
--- a/packages/react/src/text/__tests__/TextLabel.test.js
+++ b/packages/react/src/text/__tests__/TextLabel.test.js
@@ -6,9 +6,7 @@ import React from 'react';
describe('TextLabel', () => {
it('should render correctly', async () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
TextLabel
diff --git a/packages/react/src/textarea/__tests__/Textarea.test.js b/packages/react/src/textarea/__tests__/Textarea.test.js
index 0b078bb0cc..f66d1f5b26 100644
--- a/packages/react/src/textarea/__tests__/Textarea.test.js
+++ b/packages/react/src/textarea/__tests__/Textarea.test.js
@@ -8,9 +8,7 @@ import React from 'react';
describe('Textarea', () => {
it('should render correctly', async () => {
const variants = ['outline', 'filled', 'unstyled'];
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
<>
{variants.map(variant => (
@@ -113,9 +111,9 @@ describe('Textarea', () => {
);
const textarea = screen.getByTestId('textarea');
- const defaultBorderColor = '#5e5e5e'; // [dark] gray:60 / [light] gray:30
- const focusBorderColor = '#1e5ede'; // [dark] blue:60 / [light] blue:60
- const hoverBorderColor = '#578aef'; // [dark] blue:50 / [light] blue:50
+ const defaultBorderColor = 'var(--tonic-colors-gray-60)'; // [dark] gray:60 / [light] gray:30
+ const focusBorderColor = 'var(--tonic-colors-blue-60)'; // [dark] blue:60 / [light] blue:60
+ const hoverBorderColor = 'var(--tonic-colors-blue-50)'; // [dark] blue:50 / [light] blue:50
expect(textarea).toBeValid();
expect(document.body).toHaveFocus();
diff --git a/packages/react/src/theme/CSSVariables.js b/packages/react/src/theme/CSSVariables.js
index cedc64b7eb..22dd8f09dd 100644
--- a/packages/react/src/theme/CSSVariables.js
+++ b/packages/react/src/theme/CSSVariables.js
@@ -1,16 +1,18 @@
import { Global } from '@emotion/react';
-import { ensureArray } from 'ensure-type';
+import { ensurePlainObject } from 'ensure-type';
import React, { useCallback } from 'react';
-const CSSVariables = ({
- root = [':root', ':host'],
-}) => {
+const CSSVariables = () => {
const styles = useCallback((theme) => {
- const selector = ensureArray(root).join(',');
+ const rootSelector = theme?.rootSelector;
+ const cssVariables = ensurePlainObject(theme?.cssVariables);
+ if (!rootSelector || Object.keys(cssVariables) === 0) {
+ return {};
+ }
return {
- [selector]: { ...theme?.__cssVariableMap },
+ [rootSelector]: cssVariables,
};
- }, [root]);
+ }, []);
return (
diff --git a/packages/react/src/theme/ThemeProvider.js b/packages/react/src/theme/ThemeProvider.js
index 8bfbbefa80..4394c2dc69 100644
--- a/packages/react/src/theme/ThemeProvider.js
+++ b/packages/react/src/theme/ThemeProvider.js
@@ -1,83 +1,20 @@
import {
ThemeProvider as StyledEngineThemeProvider,
} from '@emotion/react';
-import originalTheme from '@tonic-ui/theme';
-import { ensurePlainObject, ensureString } from 'ensure-type';
-import React, { useMemo } from 'react';
+import React from 'react';
import { DefaultPropsProvider } from '../default-props';
import CSSVariables from './CSSVariables';
import defaultTheme from './theme';
-import flatten from './utils/flatten';
-import toCSSVariable from './utils/toCSSVariable';
-
-const originalThemeScales = Object.keys(originalTheme);
-
-/**
- * Generate CSS variable map for a given theme object.
- *
- * @param {object} theme - The object containing the theme values.
- * @param {object} [options] - The options object.
- * @param {string} [options.prefix] - A prefix to prepend to each generated CSS variable.
- *
- * @example
- * ```js
- * const theme = {
- * colors: {
- * 'blue:50': '#578aef',
- * },
- * };
- * createCSSVariableMap(theme, { prefix: 'tonic' });
- * // => {
- * // '--tonic-colors-blue-50': '#578aef'
- * // }
- * ```
- */
-const createCSSVariableMap = (theme, options) => {
- const prefix = ensureString(options?.prefix);
- const tokens = flatten(theme);
- const cssVariableMap = {};
-
- for (const [name, value] of Object.entries(tokens)) {
- // name='colors.blue:50', prefix='tonic'
- // => '--tonic-colors-blue-50'
- const variable = toCSSVariable(name, { prefix });
- cssVariableMap[variable] = value;
- }
-
- return cssVariableMap;
-};
const ThemeProvider = ({
children,
theme: themeProp,
}) => {
const theme = themeProp ?? defaultTheme;
- const computedTheme = useMemo(() => {
- const themeConfig = {
- ...defaultTheme.config,
- ...theme?.config,
- };
-
- // Filter only the theme scales that are supported by the original theme
- const normalizedTheme = Object.fromEntries(
- Object.entries(ensurePlainObject(theme)).filter(
- ([key]) => originalThemeScales.includes(key)
- )
- );
-
- // Create CSS variable map for the theme
- const cssVariableMap = createCSSVariableMap(normalizedTheme, { prefix: themeConfig.prefix });
-
- return {
- ...theme,
- config: themeConfig,
- __cssVariableMap: cssVariableMap,
- };
- }, [theme]);
return (
-
-
+
+
{children}
diff --git a/packages/react/src/theme/__tests__/__snapshots__/createTheme.test.js.snap b/packages/react/src/theme/__tests__/__snapshots__/createTheme.test.js.snap
new file mode 100644
index 0000000000..4c7cf12202
--- /dev/null
+++ b/packages/react/src/theme/__tests__/__snapshots__/createTheme.test.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`createTheme should apply custom \`rootSelector\` for CSS variables 1`] = `
+
+`;
diff --git a/packages/react/src/theme/__tests__/createTheme.test.js b/packages/react/src/theme/__tests__/createTheme.test.js
new file mode 100644
index 0000000000..8d0c7f6c2b
--- /dev/null
+++ b/packages/react/src/theme/__tests__/createTheme.test.js
@@ -0,0 +1,102 @@
+import { Box } from '@tonic-ui/react/src';
+import { render } from '@tonic-ui/react/test-utils/render';
+import createTheme from '../createTheme';
+
+describe('createTheme', () => {
+ const defaultThemeScales = [
+ 'borders',
+ 'breakpoints',
+ 'colors',
+ 'fonts',
+ 'fontSizes',
+ 'fontWeights',
+ 'letterSpacings',
+ 'lineHeights',
+ 'outlines',
+ 'radii',
+ 'shadows',
+ 'sizes',
+ 'space',
+ 'zIndices',
+ ];
+
+ it('should create a default theme', () => {
+ const theme = createTheme();
+ defaultThemeScales.forEach(scale => expect(theme).toHaveProperty(scale));
+ });
+
+ it('should merge custom theme options', () => {
+ const theme = createTheme();
+ const customTheme = createTheme(theme, {
+ colors: {
+ 'black:emphasis': 'rgb(0, 0, 0)',
+ 'white:emphasis': 'rgb(255, 255, 255)',
+ },
+ });
+ expect(theme.colors['black:emphasis']).toBe('rgba(0, 0, 0, 1.0)');
+ expect(theme.colors['white:emphasis']).toBe('rgba(255, 255, 255, 1.0)');
+ expect(customTheme.colors['black:emphasis']).toBe('rgb(0, 0, 0)');
+ expect(customTheme.colors['white:emphasis']).toBe('rgb(255, 255, 255)');
+ });
+
+ it('should merge additional arguments into the theme', () => {
+ const additionalConfig = { components: { Button: { defaultProps: { size: 'large' } } } };
+ const theme = createTheme({}, additionalConfig);
+ expect(theme.components.Button.defaultProps.size).toBe('large');
+ });
+
+ it('should not generate CSS variables with default configuration', () => {
+ const theme = createTheme();
+ expect(theme.cssVariables).not.toBeDefined();
+ });
+
+ it('should generate CSS variables', () => {
+ const theme = createTheme({ cssVariables: true });
+ expect(theme.cssVariables).toBeDefined();
+ expect(theme.cssVariablePrefix).toBe('tonic');
+ const cssVariableKeys = Object.keys(theme.cssVariables).filter(x => x.startsWith('--'));
+ expect(cssVariableKeys.length).toBeGreaterThan(0);
+ expect(cssVariableKeys[0]).toMatch(/^--tonic-/);
+ });
+
+ it('should apply custom prefix to CSS variables', () => {
+ const theme = createTheme({ cssVariables: { prefix: 'custom' } });
+ expect(theme.cssVariables).toBeDefined();
+ expect(theme.cssVariablePrefix).toBe('custom');
+ const cssVariableKeys = Object.keys(theme.cssVariables).filter(x => x.startsWith('--'));
+ expect(cssVariableKeys.length).toBeGreaterThan(0);
+ expect(cssVariableKeys[0]).toMatch(/^--custom-/);
+ });
+
+ it('should allow an empty prefix for CSS variables', () => {
+ const theme = createTheme({ cssVariables: { prefix: '' } });
+ expect(theme.cssVariables).toBeDefined();
+ expect(theme.cssVariablePrefix).toBe('');
+ const cssVariableKeys = Object.keys(theme.cssVariables).filter(x => x.startsWith('--'));
+ expect(cssVariableKeys.length).toBeGreaterThan(0);
+ cssVariableKeys.forEach(key => {
+ const isValid = defaultThemeScales.some(scale => key.startsWith(`--${scale}`));
+ expect(isValid).toBe(true);
+ });
+ });
+
+ it('should apply custom `rootSelector` for CSS variables', () => {
+ const customRootSelector = ':root[data-color-scheme]';
+ const themeOptions = {
+ cssVariables: {
+ rootSelector: customRootSelector,
+ },
+ };
+
+ const theme = createTheme(themeOptions);
+ expect(theme.rootSelector).toBe(customRootSelector);
+
+ const TestComponent = (props) => (
+
+ );
+ render(, { theme: themeOptions });
+
+ const styleElement = document.querySelector('style[data-emotion="css-global"]');
+ expect(styleElement).toMatchSnapshot();
+ });
+});
diff --git a/packages/react/src/theme/createTheme.js b/packages/react/src/theme/createTheme.js
new file mode 100644
index 0000000000..692a2164a5
--- /dev/null
+++ b/packages/react/src/theme/createTheme.js
@@ -0,0 +1,64 @@
+import tonicTheme from '@tonic-ui/theme';
+import { merge } from '@tonic-ui/utils';
+import { ensurePlainObject } from 'ensure-type';
+import { mapThemeToCSSVariables } from './utils/css-vars';
+
+const defaultCSSVariablePrefix = 'tonic';
+const defaultCSSVariableRootSelector = ':root';
+
+const createTheme = (options = {}, ...args) => {
+ const {
+ // CSS variables configuration for the theme:
+ // - false (default): Disables CSS variables.
+ // - true: Enables CSS variables with default settings.
+ // - Object: Enables CSS variables with custom settings:
+ // - prefix: 'tonic' (default) — Custom prefix for CSS variables.
+ // - rootSelector: ':root' (default) — Root selector where CSS variables are defined.
+ cssVariables: cssVariableConfig = false,
+ ...rest
+ } = options;
+
+ let theme = merge(
+ {
+ ...tonicTheme,
+ },
+ rest,
+ );
+
+ // Merge additional arguments into the theme
+ theme = args.reduce((acc, arg) => merge(acc, arg), theme);
+
+ if (cssVariableConfig) {
+ if (typeof cssVariableConfig !== 'boolean' && typeof cssVariableConfig !== 'object') {
+ throw new Error('The `cssVariables` config must be a boolean or an object');
+ }
+
+ // Configure the prefix and root selector for CSS variables:
+ // - `cssVariablePrefix`: Uses `cssVariables.prefix` if available; otherwise, defaults to 'tonic'.
+ // - `rootSelector`: Uses `cssVariables.rootSelector` if available; otherwise, defaults to ':root'.
+ const cssVariablePrefix = cssVariableConfig?.prefix ?? defaultCSSVariablePrefix;
+ const rootSelector = cssVariableConfig?.rootSelector ?? defaultCSSVariableRootSelector;
+
+ // Generate a theme object filtered to include only scales supported by CSS variables
+ const cssVariableScales = Object.keys(tonicTheme);
+ const cssVariableTheme = Object.fromEntries(
+ Object.entries(ensurePlainObject(theme)).filter(
+ ([key]) => cssVariableScales.includes(key)
+ )
+ );
+
+ // Create CSS variables with the appropriate prefix
+ const cssVariables = mapThemeToCSSVariables(cssVariableTheme, { prefix: cssVariablePrefix });
+
+ // Merge CSS variables into the theme
+ theme = merge(theme, {
+ cssVariablePrefix,
+ cssVariables,
+ rootSelector,
+ });
+ }
+
+ return theme;
+};
+
+export default createTheme;
diff --git a/packages/react/src/theme/index.js b/packages/react/src/theme/index.js
index 2e19cb1818..bc7f208afc 100644
--- a/packages/react/src/theme/index.js
+++ b/packages/react/src/theme/index.js
@@ -1,9 +1,11 @@
import ThemeProvider from './ThemeProvider';
-import theme from './theme';
+import createTheme from './createTheme';
import useTheme from './useTheme';
+import theme from './theme';
export {
ThemeProvider,
+ createTheme,
theme,
useTheme,
};
diff --git a/packages/react/src/theme/theme.js b/packages/react/src/theme/theme.js
index 21dd1e5e8a..0c3fe92d5e 100644
--- a/packages/react/src/theme/theme.js
+++ b/packages/react/src/theme/theme.js
@@ -1,13 +1,5 @@
-import originalTheme from '@tonic-ui/theme';
+import createTheme from './createTheme';
-const theme = {
- ...originalTheme,
- config: {
- prefix: 'tonic',
- useCSSVariables: false,
- },
- components: {},
- icons: [],
-};
+const theme = createTheme();
export default theme;
diff --git a/packages/react/src/theme/utils/css-vars.js b/packages/react/src/theme/utils/css-vars.js
new file mode 100644
index 0000000000..7990d6fba6
--- /dev/null
+++ b/packages/react/src/theme/utils/css-vars.js
@@ -0,0 +1,63 @@
+import { ensureString } from 'ensure-type';
+import flatten from './flatten';
+
+/**
+ * Returns a CSS variable name formatted from the given name and options.
+ *
+ * @param {string} name - The name of the variable.
+ * @param {object} [options] - The options object.
+ * @param {string} [options.prefix=''] - The prefix to use for the variable name.
+ * @param {string} [options.delimiter='-'] - The delimiter to use between the prefix and name.
+ *
+ * @return {string} The CSS variable name.
+*/
+export const toCSSVariable = (name, options) => {
+ const {
+ prefix = '',
+ delimiter = '-',
+ } = { ...options };
+ const variableName = ([prefix, name].filter(Boolean).join(delimiter))
+ .replace(/\s+/g, delimiter) // replace whitespace characters
+ .replace(/[^a-zA-Z0-9-_]/g, delimiter) // replace non-alphanumeric, non-hyphen, non-underscore characters
+ .replace(/^-+|-+$/g, ''); // trim hyphens from beginning and end of string
+ return `--${variableName}`;
+};
+
+/**
+ * Generates CSS variables from a theme object.
+ *
+ * @param {object} theme - The theme object containing key-value pairs for the variables.
+ * @param {object} [options] - Optional configuration for variable generation.
+ * @param {string} [options.prefix] - A prefix to prepend to each generated CSS variable.
+ *
+ * @example
+ * ```js
+ * const theme = {
+ * colors: {
+ * 'blue:50': '#578aef',
+ * },
+ * };
+ * mapThemeToCSSVariables(theme, { prefix: 'tonic' });
+ * // => {
+ * // '--tonic-colors-blue-50': '#578aef'
+ * // }
+ * ```
+ */
+export const mapThemeToCSSVariables = (theme, options) => {
+ const prefix = ensureString(options?.prefix);
+ const tokens = flatten(theme);
+ const cssVariables = {};
+
+ for (const [name, value] of Object.entries(tokens)) {
+ // name='colors.blue:50', prefix='tonic'
+ // => '--tonic-colors-blue-50'
+ if (!name) {
+ // Skip if name is empty
+ continue;
+ }
+ const variable = toCSSVariable(name, { prefix });
+ cssVariables[variable] = value;
+ }
+
+ return cssVariables;
+};
diff --git a/packages/react/src/tooltip/__tests__/Tooltip.test.js b/packages/react/src/tooltip/__tests__/Tooltip.test.js
index dc18f571d7..079517ed8d 100644
--- a/packages/react/src/tooltip/__tests__/Tooltip.test.js
+++ b/packages/react/src/tooltip/__tests__/Tooltip.test.js
@@ -16,9 +16,7 @@ describe('Tooltip', () => {
it('should render correctly', async () => {
const user = userEvent.setup();
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
), renderOptions);
diff --git a/packages/react/src/tree/__tests__/Tree.test.js b/packages/react/src/tree/__tests__/Tree.test.js
index 164ca1dcbb..a2c434e8ea 100644
--- a/packages/react/src/tree/__tests__/Tree.test.js
+++ b/packages/react/src/tree/__tests__/Tree.test.js
@@ -181,9 +181,7 @@ const TreeItemRender = ({
describe('Tree', () => {
it('should render correctly', async () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const treeNodes = buildTreeNodes();
const treeMap = buildTreeMap(treeNodes);
const expandableNodeIds = findExpandableNodeIds(treeNodes);
diff --git a/packages/react/src/truncate/__tests__/Truncate.test.js b/packages/react/src/truncate/__tests__/Truncate.test.js
index ad93b56201..36fb90d963 100644
--- a/packages/react/src/truncate/__tests__/Truncate.test.js
+++ b/packages/react/src/truncate/__tests__/Truncate.test.js
@@ -5,9 +5,7 @@ import React from 'react';
describe('Truncate', () => {
it('should render correctly', async () => {
- const renderOptions = {
- useCSSVariables: true,
- };
+ const renderOptions = {};
const { container } = render((
This is a very long text that will be truncated
diff --git a/packages/react/test-utils/render.js b/packages/react/test-utils/render.js
index 51f7ccd1a4..a6cb8e8f01 100644
--- a/packages/react/test-utils/render.js
+++ b/packages/react/test-utils/render.js
@@ -6,7 +6,7 @@ import {
PortalManager,
ToastManager,
TonicProvider,
- theme,
+ createTheme,
} from '../src';
// https://emotion.sh/docs/@emotion/jest#tohavestylerule
@@ -15,18 +15,22 @@ import {
expect.extend(matchers);
const customRender = (ui, options) => {
+ const {
+ theme: themeOptions = {
+ cssVariables: {
+ prefix: 'tonic',
+ rootSelector: ':root',
+ },
+ },
+ ...rest
+ } = { ...options };
+
const wrapper = ({ children }) => (
@@ -36,7 +40,7 @@ const customRender = (ui, options) => {
);
- return render(ui, { wrapper, ...options });
+ return render(ui, { wrapper, ...rest });
};
// re-export everything
diff --git a/packages/styled-system/README.md b/packages/styled-system/README.md
index 8c53a03b90..41c4387b61 100644
--- a/packages/styled-system/README.md
+++ b/packages/styled-system/README.md
@@ -1,6 +1,6 @@
# @tonic-ui/styled-system
-The framework agnostic styling engine for Tonic UI.
+A framework-independent styling engine for Tonic UI.
## Installation
diff --git a/packages/styled-system/package.json b/packages/styled-system/package.json
index 56c9f277aa..da12db9812 100644
--- a/packages/styled-system/package.json
+++ b/packages/styled-system/package.json
@@ -1,7 +1,7 @@
{
"name": "@tonic-ui/styled-system",
"version": "2.0.2",
- "description": "The framework agnostic styling engine for Tonic UI.",
+ "description": "A framework-independent styling engine for Tonic UI.",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"files": [
diff --git a/packages/styled-system/src/__tests__/sx.test.js b/packages/styled-system/src/__tests__/sx.test.js
index 62007ae858..2496ebe21f 100644
--- a/packages/styled-system/src/__tests__/sx.test.js
+++ b/packages/styled-system/src/__tests__/sx.test.js
@@ -280,11 +280,8 @@ const defaultTheme = {
test('should pass', () => {
const theme = {
...defaultTheme,
- config: {
- prefix: 'tonic',
- useCSSVariables: true,
- },
- __cssVariableMap: {
+ cssVariablePrefix: 'tonic',
+ cssVariables: {
'--tonic-borders-1': '.0625rem solid',
'--tonic-borders-2': '.125rem solid',
'--tonic-borders-3': '.1875rem solid',
diff --git a/packages/styled-system/src/config/__tests__/margin.test.js b/packages/styled-system/src/config/__tests__/margin.test.js
index 8084feaf1a..1ace6fe9fb 100644
--- a/packages/styled-system/src/config/__tests__/margin.test.js
+++ b/packages/styled-system/src/config/__tests__/margin.test.js
@@ -7,11 +7,8 @@ const defaultTheme = {
const defaultThemeWithCSSVariables = {
...defaultTheme,
- config: {
- prefix: 'tonic',
- useCSSVariables: true,
- },
- __cssVariableMap: {
+ cssVariablePrefix: 'tonic',
+ cssVariables: {
'--tonic-breakpoints-0': '40em',
'--tonic-breakpoints-1': '52em',
'--tonic-breakpoints-2': '64em',
diff --git a/packages/styled-system/src/config/__tests__/position.test.js b/packages/styled-system/src/config/__tests__/position.test.js
index 39752d7b02..3c03a873d0 100644
--- a/packages/styled-system/src/config/__tests__/position.test.js
+++ b/packages/styled-system/src/config/__tests__/position.test.js
@@ -6,11 +6,8 @@ const defaultTheme = {
const defaultThemeWithCSSVariables = {
...defaultTheme,
- config: {
- prefix: 'tonic',
- useCSSVariables: true,
- },
- __cssVariableMap: {
+ cssVariablePrefix: 'tonic',
+ cssVariables: {
'--tonic-space-0': 0,
'--tonic-space-1': 4,
'--tonic-space-2': 8,
diff --git a/packages/react/src/theme/utils/toCSSVariable.js b/packages/styled-system/src/utils/css-vars.js
similarity index 91%
rename from packages/react/src/theme/utils/toCSSVariable.js
rename to packages/styled-system/src/utils/css-vars.js
index 9c1a94d225..b7c0dfd441 100644
--- a/packages/react/src/theme/utils/toCSSVariable.js
+++ b/packages/styled-system/src/utils/css-vars.js
@@ -8,7 +8,7 @@
*
* @return {string} The CSS variable name.
*/
-const toCSSVariable = (name, options) => {
+export const toCSSVariable = (name, options) => {
const {
prefix = '',
delimiter = '-',
@@ -19,5 +19,3 @@ const toCSSVariable = (name, options) => {
.replace(/^-+|-+$/g, ''); // trim hyphens from beginning and end of string
return `--${variableName}`;
};
-
-export default toCSSVariable;
diff --git a/packages/styled-system/src/utils/toCSSVariable.js b/packages/styled-system/src/utils/toCSSVariable.js
deleted file mode 100644
index 9c1a94d225..0000000000
--- a/packages/styled-system/src/utils/toCSSVariable.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Returns a CSS variable name formatted from the given name and options.
- *
- * @param {string} name - The name of the variable.
- * @param {object} [options] - The options object.
- * @param {string} [options.prefix=''] - The prefix to use for the variable name.
- * @param {string} [options.delimiter='-'] - The delimiter to use between the prefix and name.
- *
- * @return {string} The CSS variable name.
-*/
-const toCSSVariable = (name, options) => {
- const {
- prefix = '',
- delimiter = '-',
- } = { ...options };
- const variableName = ([prefix, name].filter(Boolean).join(delimiter))
- .replace(/\s+/g, delimiter) // replace whitespace characters
- .replace(/[^a-zA-Z0-9-_]/g, delimiter) // replace non-alphanumeric, non-hyphen, non-underscore characters
- .replace(/^-+|-+$/g, ''); // trim hyphens from beginning and end of string
- return `--${variableName}`;
-};
-
-export default toCSSVariable;
diff --git a/packages/styled-system/src/utils/transforms.js b/packages/styled-system/src/utils/transforms.js
index 4f7e816df5..544a54be8c 100644
--- a/packages/styled-system/src/utils/transforms.js
+++ b/packages/styled-system/src/utils/transforms.js
@@ -1,5 +1,5 @@
import get from './get';
-import toCSSVariable from './toCSSVariable';
+import { toCSSVariable } from './css-vars';
// Check if a value is a simple CSS variable
// e.g. var(--tonic-spacing-1)
@@ -11,11 +11,10 @@ const isSimpleCSSVariable = (value) => {
// Negate the value, handling CSS variables and numeric values
const toNegativeValue = (scale, absoluteValue, options) => {
const theme = options?.props?.theme;
- const useCSSVariables = !!(theme?.config?.useCSSVariables); // defaults to false
const n = getter(scale, absoluteValue, options);
// Handle CSS variables for negative values
- if (useCSSVariables && isSimpleCSSVariable(n)) {
+ if (!!theme?.cssVariables && isSimpleCSSVariable(n)) {
// https://stackoverflow.com/questions/49469344/using-negative-css-custom-properties
return `calc(0px - ${n})`;
}
@@ -29,12 +28,17 @@ const toNegativeValue = (scale, absoluteValue, options) => {
};
export const getter = (scale, value, options) => {
- const theme = options?.props?.theme;
- const prefix = theme?.config?.prefix; // defaults to 'tonic'
- const useCSSVariables = !!(theme?.config?.useCSSVariables); // defaults to false
const result = get(scale, value);
+ if (result === undefined) {
+ return value; // fallback to value if result is undefined
+ }
- if (result !== undefined && useCSSVariables) {
+ const theme = options?.props?.theme;
+ // FIXME: `theme.config.prefix` and `theme.__cssVariableMap` are deprecated and will be removed in the next major release
+ const hasCSSVariables = !!(theme?.cssVariables ?? theme?.__cssVariableMap);
+ if (hasCSSVariables) {
+ const cssVariablePrefix = (theme?.cssVariablePrefix) ?? (theme?.config?.prefix);
+ const cssVariables = (theme?.cssVariables) ?? (theme?.__cssVariableMap);
const contextScale = options?.context?.scale;
const cssVariable = toCSSVariable(
// | contextScale | value |
@@ -42,17 +46,17 @@ export const getter = (scale, value, options) => {
// | colors | 'blue:50' |
// | space | 0 |
[contextScale, String(value ?? '')].filter(Boolean).join('.'), // => 'colors.blue:50'
- { prefix, delimiter: '-' },
+ { prefix: cssVariablePrefix, delimiter: '-' },
); // => '--tonic-colors-blue-50'
- const cssVariableValue = theme?.__cssVariableMap?.[cssVariable]; // => '#578aef'
+ const cssVariableValue = cssVariables?.[cssVariable]; // => '#578aef'
if (cssVariableValue !== undefined) {
// => Replace '#578aef' with 'var(--tonic-colors-blue-50)'
return String(result ?? '').replaceAll(cssVariableValue, `var(${cssVariable})`);
}
- // fallback to the original value
+ // fallback to the original result
}
- return result ?? value; // fallback to value if result is null or undefined
+ return result;
};
export const positiveOrNegative = (scale, value, options) => {
diff --git a/packages/theme/README.md b/packages/theme/README.md
index 2fa8b4f711..0b1c0ee478 100644
--- a/packages/theme/README.md
+++ b/packages/theme/README.md
@@ -1,6 +1,6 @@
# @tonic-ui/theme
-The default theme package for Tonic UI components.
+The default theme configuration for Tonic UI components.
## Installation
diff --git a/packages/theme/package.json b/packages/theme/package.json
index 57589500af..d61a56fe9d 100644
--- a/packages/theme/package.json
+++ b/packages/theme/package.json
@@ -1,7 +1,7 @@
{
"name": "@tonic-ui/theme",
"version": "2.0.0",
- "description": "The default theme package for Tonic UI components.",
+ "description": "The default theme configuration for Tonic UI components.",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"files": [
diff --git a/packages/theme/src/createTheme.js b/packages/theme/src/createTheme.js
index 04e41841ad..3f228c448c 100644
--- a/packages/theme/src/createTheme.js
+++ b/packages/theme/src/createTheme.js
@@ -168,6 +168,7 @@ const createTheme = (unit) => {
'px': _px(foundation),
}[unit] ?? foundation;
+ // TODO: Consider adding `Object.freeze(theme)` in a future major release
return theme;
};
diff --git a/packages/theme/src/index.js b/packages/theme/src/index.js
index f02767257e..d7c1833c03 100644
--- a/packages/theme/src/index.js
+++ b/packages/theme/src/index.js
@@ -1,4 +1,4 @@
-import createTheme from './createTheme';
+import createTheme from './createTheme'; // deprecated
const theme = createTheme('rem');
diff --git a/packages/utils/README.md b/packages/utils/README.md
index c25888dc53..45e18adc32 100644
--- a/packages/utils/README.md
+++ b/packages/utils/README.md
@@ -1,6 +1,6 @@
# @tonic-ui/utils
-Common utilities for various Tonic UI components and packages.
+A set of utility functions shared across Tonic UI components and packages.
## Installation
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 5e24cb4123..6bd71c6f00 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@tonic-ui/utils",
"version": "2.1.0",
- "description": "Common utilities for various Tonic UI components and packages.",
+ "description": "A set of utility functions shared across Tonic UI components and packages.",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"files": [