diff --git a/packages/react-docs/pages/_app.page.js b/packages/react-docs/pages/_app.page.js index 20a615e877..212207afae 100644 --- a/packages/react-docs/pages/_app.page.js +++ b/packages/react-docs/pages/_app.page.js @@ -7,6 +7,7 @@ import { ToastManager, TonicProvider, colorStyle as defaultColorStyle, + createTheme, theme, useTheme, } from '@tonic-ui/react'; @@ -50,28 +51,24 @@ const EmotionCacheProvider = ({ }; const App = (props) => { - const customTheme = useConst(() => { - return { - ...theme, - components: { - // Set default props for components here. - // - // Example: - // ``` - // 'AccordionToggle': { - // defaultProps: { - // disabled: true, - // }, - // } - // ``` - }, - config: { - ...theme?.config, - // Enable CSS variables replacement - useCSSVariables: true, - }, - }; - }); + const customTheme = useConst(() => createTheme(theme, { + components: { + // Set default props for specific components + // + // Example: + // ``` + // 'ToastCloseButton': { + // defaultProps: { + // 'aria-label': 'Close toast', + // }, + // }, + // ``` + }, + config: { + // Enable CSS variables replacement + useCSSVariables: true, + }, + })); const [initialColorMode, setColorMode] = useState(null); const router = useRouter(); diff --git a/packages/react-docs/pages/getting-started/usage/index.page.mdx b/packages/react-docs/pages/getting-started/usage/index.page.mdx index 118dce13b8..93406bc491 100644 --- a/packages/react-docs/pages/getting-started/usage/index.page.mdx +++ b/packages/react-docs/pages/getting-started/usage/index.page.mdx @@ -14,32 +14,30 @@ import { Box, TonicProvider, colorStyle, // [optional] Required only for customizing color styles + createTheme, theme, // [optional] Required only for customizing the theme } from '@tonic-ui/react'; +import { useConst } from '@tonic-ui/react-hooks'; function App(props) { - const customTheme = { - ...theme, + const customTheme = useConst(() => createTheme(theme, { + components: { // Set default props for specific components // // Example: - // ```js - // components: { - // 'ToastCloseButton': { - // defaultProps: { - // 'aria-label': 'Close toast', - // }, + // ``` + // 'ToastCloseButton': { + // defaultProps: { + // 'aria-label': 'Close toast', // }, - // } + // }, // ``` - components: {}, - // Enable CSS variables - config: { - ...theme?.config, - useCSSVariables: true, - }, - }; - }; + }, + config: { + // Enable CSS variables replacement + useCSSVariables: true, + }, + })); return ( createTheme(theme, { + components: { // Set default props for specific components // // Example: - // ```js - // components: { - // 'ToastCloseButton': { - // defaultProps: { - // 'aria-label': 'Close toast', - // }, + // ``` + // 'ToastCloseButton': { + // defaultProps: { + // 'aria-label': 'Close toast', // }, - // } + // }, // ``` - components: {}, - // Enable CSS variables - config: { - ...theme?.config, - useCSSVariables: true, - }, - }; - }; + }, + config: { + // Enable CSS variables replacement + useCSSVariables: true, + }, + })); return ( ` component. ```js -import { theme } from '@tonic-ui/react'; +import { createTheme, theme } from '@tonic-ui/react'; // Let's say you want to add more colors -const customTheme = { - ...theme, +const customTheme = createTheme(theme, { config: { - ...theme?.config, useCSSVariables: true, }, colors: { - ...theme?.colors, brand: { 90: "#1a365d", 80: "#153e75", 70: "#2a69ac", }, }, -}; +}); ``` #### 2. Setting up the provider diff --git a/packages/react-docs/pages/theme/breakpoints/index.page.mdx b/packages/react-docs/pages/theme/breakpoints/index.page.mdx index 1feb2a62da..1e37bbb031 100644 --- a/packages/react-docs/pages/theme/breakpoints/index.page.mdx +++ b/packages/react-docs/pages/theme/breakpoints/index.page.mdx @@ -42,7 +42,7 @@ To do this, add a `breakpoints` array with additional aliases (e.g. `sm`, `md`, ```jsx disabled // 1. Import the theme -import { ThemeProvider, theme } from '@tonic-ui/react'; +import { ThemeProvider, createTheme, theme } from '@tonic-ui/react'; // 2. Update the breakpoints const breakpoints = [ @@ -59,10 +59,9 @@ breakpoints.xl = breakpoints[3]; breakpoints['2xl'] = breakpoints[4]; // 3. Extend the theme -const customTheme = { - ...theme, +const customTheme = createTheme(theme, { breakpoints, -}; +}); // 4. Pass the custom theme to the theme provider function App() { diff --git a/packages/react-docs/sandbox/create-react-app.js b/packages/react-docs/sandbox/create-react-app.js index d139ac7a84..c55790be80 100644 --- a/packages/react-docs/sandbox/create-react-app.js +++ b/packages/react-docs/sandbox/create-react-app.js @@ -28,20 +28,20 @@ import { ToastManager, TonicProvider, colorStyle, + createTheme, theme, useColorMode, useColorStyle, useTheme, } from '@tonic-ui/react'; +import { useConst } from '@tonic-ui/react-hooks'; +import { merge } from '@tonic-ui/utils'; import * as React from 'react'; import ReactDOM from 'react-dom/client'; import App from './app'; -const customColorStyle = { - ...colorStyle, +const customColorStyle = merge(colorStyle, { dark: { - ...colorStyle.dark, - // Add custom colors here risk: { high: 'red:50', @@ -58,8 +58,6 @@ const customColorStyle = { }, }, light: { - ...colorStyle.light, - // Add custom colors here risk: { high: 'red:60', @@ -75,30 +73,27 @@ const customColorStyle = { info: 'gray:50', }, }, -}; +}); const Root = (props) => { - const customTheme = { - ...theme, - // Set default props for specific components - // - // Example: - // \`\`\`js - // components: { - // 'ToastCloseButton': { - // defaultProps: { - // 'aria-label': 'Close toast', - // }, - // }, - // } - // \`\`\` - components: {}, - // Enable CSS variables + const customTheme = useConst(() => createTheme(theme, { + components: { + // Set default props for specific components + // + // Example: + // \`\`\` + // 'ToastCloseButton': { + // defaultProps: { + // 'aria-label': 'Close toast', + // }, + // }, + // \`\`\` + }, config: { - ...theme?.config, + // Enable CSS variables replacement useCSSVariables: true, }, - }; + })); return ( { // theme 'ThemeProvider', + 'createTheme', 'theme', 'useTheme', diff --git a/packages/react/src/provider/TonicProvider.js b/packages/react/src/provider/TonicProvider.js index ca81bcff32..3d682e3e85 100644 --- a/packages/react/src/provider/TonicProvider.js +++ b/packages/react/src/provider/TonicProvider.js @@ -6,14 +6,11 @@ import { CSSBaseline } from '../css-baseline'; const TonicProvider = ({ children, - colorMode: colorModeProps, - colorStyle: colorStyleProps, + colorMode: colorModeProps = {}, + colorStyle: colorStyleProps = {}, theme, useCSSBaseline = false, }) => { - colorModeProps = colorModeProps ?? {}; - colorStyleProps = colorStyleProps ?? {}; - if (typeof colorModeProps !== 'object') { console.error( 'TonicProvider: "colorMode" prop must be an object if provided.\n' + 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/createTheme.js b/packages/react/src/theme/createTheme.js new file mode 100644 index 0000000000..5e033c0e1d --- /dev/null +++ b/packages/react/src/theme/createTheme.js @@ -0,0 +1,57 @@ +import { merge } from '@tonic-ui/utils'; +import { ensurePlainObject } from 'ensure-type'; +import createCSSVariableMap from './utils/createCSSVariableMap'; + +const defaultCSSVariablePrefix = 'tonic'; + +const cssVariableScales = [ + 'borders', + 'breakpoints', + 'colors', + 'fonts', + 'fontSizes', + 'fontWeights', + 'letterSpacings', + 'lineHeights', + 'outlines', + 'radii', + 'shadows', + 'sizes', + 'space', + 'zIndices', +]; + +const createTheme = (options, ...args) => { + // Merge provided options with default configurations + let theme = merge(options, { + config: { + prefix: defaultCSSVariablePrefix, + useCSSVariables: false, + }, + }); + + // Ensure the components field is initialized + theme.components = theme.components ?? {}; + + // Merge additional arguments into the theme + theme = args.reduce((acc, arg) => merge(acc, arg), theme); + + // Generate a theme object filtered to include only scales supported by CSS variables + const cssVariableTheme = Object.fromEntries( + Object.entries(ensurePlainObject(theme)).filter( + ([key]) => cssVariableScales.includes(key) + ) + ); + + // Create a map of CSS variables with the appropriate prefix + const cssVariableMap = createCSSVariableMap(cssVariableTheme, { prefix: theme?.config?.prefix }); + + // Merge the CSS variable map into the theme + theme = merge(theme, { + __cssVariableMap: cssVariableMap, + }); + + 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..7c89066a20 100644 --- a/packages/react/src/theme/theme.js +++ b/packages/react/src/theme/theme.js @@ -1,13 +1,6 @@ -import originalTheme from '@tonic-ui/theme'; +import tonicTheme from '@tonic-ui/theme'; +import createTheme from './createTheme'; -const theme = { - ...originalTheme, - config: { - prefix: 'tonic', - useCSSVariables: false, - }, - components: {}, - icons: [], -}; +const theme = createTheme(tonicTheme); export default theme; diff --git a/packages/react/src/theme/utils/createCSSVariableMap.js b/packages/react/src/theme/utils/createCSSVariableMap.js new file mode 100644 index 0000000000..8fcf409fe4 --- /dev/null +++ b/packages/react/src/theme/utils/createCSSVariableMap.js @@ -0,0 +1,40 @@ +import { ensureString } from 'ensure-type'; +import flatten from './flatten'; +import toCSSVariable from './toCSSVariable'; + +/** + * 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; +}; + +export default createCSSVariableMap; 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');