diff --git a/.changeset/chilled-dots-divide.md b/.changeset/chilled-dots-divide.md new file mode 100644 index 0000000000..a687358a76 --- /dev/null +++ b/.changeset/chilled-dots-divide.md @@ -0,0 +1,5 @@ +--- +"@channel.io/bezier-react": minor +--- + +Implement multi theme feature based on data attributes. diff --git a/packages/bezier-react/src/features/index.ts b/packages/bezier-react/src/features/index.ts index d63c91c461..1dc94e5a4e 100644 --- a/packages/bezier-react/src/features/index.ts +++ b/packages/bezier-react/src/features/index.ts @@ -1,3 +1,4 @@ +export type { Feature } from './Feature' export { FeatureType } from './Feature' export { diff --git a/packages/bezier-react/src/index.ts b/packages/bezier-react/src/index.ts index f867045692..c4efe51ee1 100644 --- a/packages/bezier-react/src/index.ts +++ b/packages/bezier-react/src/index.ts @@ -2,7 +2,19 @@ import '~/src/styles/index.scss' /* Provider */ export { default as BezierProvider } from '~/src/providers/BezierProvider' -export { default as WindowProvider, useWindow } from '~/src/providers/WindowProvider' +export * from '~/src/providers/WindowProvider' +export * from '~/src/providers/AlphaAppProvider' +export { + useThemeName, + useToken, + ThemeProvider, + LightThemeProvider, + DarkThemeProvider, + InvertedThemeProvider, + type ThemeName, + type ThemeProviderProps, + type FixedThemeProviderProps, +} from '~/src/providers/ThemeProvider' /* Foundation */ export * from '~/src/foundation' diff --git a/packages/bezier-react/src/providers/AlphaAppProvider.tsx b/packages/bezier-react/src/providers/AlphaAppProvider.tsx new file mode 100644 index 0000000000..37c687fad5 --- /dev/null +++ b/packages/bezier-react/src/providers/AlphaAppProvider.tsx @@ -0,0 +1,85 @@ +import React, { useEffect } from 'react' + +import { + type Feature, + FeatureProvider, +} from '~/src/features' +import { window as defaultWindow } from '~/src/utils/dom' + +import { TooltipProvider } from '~/src/components/Tooltip' + +import { + type ThemeName, + TokenProvider, +} from './ThemeProvider' +import { WindowProvider } from './WindowProvider' + +export interface AlphaAppProviderProps { + children: React.ReactNode + /** + * Name of the theme to use for the app. + * @default 'light' + */ + themeName?: ThemeName + /** + * List of features to enable for the app. + * @default [] + */ + features?: Feature[] + /** + * Window object to use for the app. + * @default window + */ + window?: Window +} + +/** + * `AlphaAppProvider` is a required wrapper component that provides context for the app. + * + * @example + * + * ```tsx + * import React from 'react' + * import { createRoot } from 'react-dom/client' + * import { AlphaAppProvider } from '@channel.io/bezier-react' + * + * const container = document.getElementById('root') + * const root = createRoot(container) + * + * root.render( + * + * + * , + * ) + * ``` + */ +export function AlphaAppProvider({ + children, + themeName = 'light', + features = [], + window = defaultWindow, +}: AlphaAppProviderProps) { + useEffect(function updateRootThemeDataAttribute() { + const rootElement = window.document.documentElement + // TODO: Change data attribute constant to import from bezier-tokens + rootElement.setAttribute('data-bezier-theme', themeName) + return function cleanup() { + rootElement.removeAttribute('data-bezier-theme') + } + }, [ + window, + themeName, + ]) + + return ( + + + + + { children } + + + + + ) +} diff --git a/packages/bezier-react/src/providers/BezierProvider.tsx b/packages/bezier-react/src/providers/BezierProvider.tsx index 88760007f3..d1543bb0b1 100644 --- a/packages/bezier-react/src/providers/BezierProvider.tsx +++ b/packages/bezier-react/src/providers/BezierProvider.tsx @@ -9,14 +9,11 @@ import { type ThemeVarsAdditionalType, } from '~/src/foundation' -import { - document as defaultDocument, - window as defaultWindow, -} from '~/src/utils/dom' +import { window as defaultWindow } from '~/src/utils/dom' import { TooltipProvider } from '~/src/components/Tooltip' -import WindowProvider from './WindowProvider' +import { WindowProvider } from './WindowProvider' interface BezierProviderProps { foundation: Foundation & GlobalStyleProp @@ -29,13 +26,10 @@ function BezierProvider({ foundation, children, themeVarsScope, - externalWindow, + externalWindow = defaultWindow, }: BezierProviderProps) { return ( - + diff --git a/packages/bezier-react/src/providers/ThemeProvider.tsx b/packages/bezier-react/src/providers/ThemeProvider.tsx new file mode 100644 index 0000000000..5a980b1c4d --- /dev/null +++ b/packages/bezier-react/src/providers/ThemeProvider.tsx @@ -0,0 +1,151 @@ +import React, { + forwardRef, + useMemo, +} from 'react' + +import { tokens } from '@channel.io/bezier-tokens' +import { Slot } from '@radix-ui/react-slot' + +import { createContext } from '~/src/utils/react' + +type Tokens = typeof tokens +type GlobalTokens = Tokens['global'] +type SemanticTokens = Omit + +interface ThemedTokenSet { + global: GlobalTokens + semantic: SemanticTokens[keyof SemanticTokens] +} + +// TODO: Change theme name constant to import from bezier-tokens +export type ThemeName = 'light' | 'dark' + +interface TokenContextValue { + themeName: ThemeName + tokens: ThemedTokenSet +} + +const [TokenContextProvider, useTokenContext] = createContext(null, 'TokenProvider') + +const tokenSet: Record = Object.freeze({ + light: { + global: tokens.global, + semantic: tokens.lightTheme, + }, + dark: { + global: tokens.global, + semantic: tokens.darkTheme, + }, +}) + +interface TokenProviderProps { + themeName: ThemeName + children: React.ReactNode +} + +/** + * @private For internal use only. + */ +export function TokenProvider({ + themeName, + children, +}: TokenProviderProps) { + return ( + ({ + themeName, + tokens: tokenSet[themeName], + }), [themeName])} + > + { children } + + ) +} + +/** + * `useThemeName` is a hook that returns the current theme name. + */ +export function useThemeName() { + return useTokenContext('useThemeName').themeName +} + +/** + * `useToken` is a hook that returns the design token for the current theme. + */ +export function useToken() { + return useTokenContext('useToken').tokens +} + +export interface ThemeProviderProps { + themeName: ThemeName + children: React.ReactElement +} + +export type FixedThemeProviderProps = Omit + +/** + * `ThemeProvider` is a wrapper component that provides theme context. + */ +export const ThemeProvider = forwardRef(function ThemeProvider({ + themeName, + children, +}, forwardedRef) { + return ( + + + { children } + + + ) +}) + +/** + * `LightThemeProvider` is a wrapper component that provides light theme context. + */ +export const LightThemeProvider = forwardRef(function LightTheme({ + children, +}, forwardedRef) { + return ( + + { children } + + ) +}) + +/** + * `DarkThemeProvider` is a wrapper component that provides dark theme context. + */ +export const DarkThemeProvider = forwardRef(function DarkTheme({ + children, +}, forwardedRef) { + return ( + + { children } + + ) +}) + +/** + * `InvertedThemeProvider` is a wrapper component that provides inverted theme context. + */ +export const InvertedThemeProvider = forwardRef(function InvertedTheme({ + children, +}, forwardedRef) { + return ( + + { children } + + ) +}) diff --git a/packages/bezier-react/src/providers/WindowProvider.tsx b/packages/bezier-react/src/providers/WindowProvider.tsx index 74033d08cb..e8e251660d 100644 --- a/packages/bezier-react/src/providers/WindowProvider.tsx +++ b/packages/bezier-react/src/providers/WindowProvider.tsx @@ -23,19 +23,14 @@ interface WindowProviderProps extends PropsWithChildren { * @required */ window: Window - - /** - * injected document - * @required - */ - document: Document } /** * A Provider that provides window and document object * you can use this provider to inject an external window */ -function WindowProvider({ window, document, children }: WindowProviderProps) { +export function WindowProvider({ window, children }: WindowProviderProps) { + const document = window.document const rootElement = document.body const value = useMemo(() => ({ @@ -43,8 +38,8 @@ function WindowProvider({ window, document, children }: WindowProviderProps) { document, rootElement, }), [ - document, window, + document, rootElement, ]) @@ -52,5 +47,3 @@ function WindowProvider({ window, document, children }: WindowProviderProps) { { children } ) } - -export default WindowProvider diff --git a/packages/bezier-react/src/styles/_base.scss b/packages/bezier-react/src/styles/_base.scss index 986b49235a..b265edfeef 100644 --- a/packages/bezier-react/src/styles/_base.scss +++ b/packages/bezier-react/src/styles/_base.scss @@ -1,6 +1,18 @@ -html { +:where(:root, :host) { font-size: 62.5%; // 10/16 = 0.625. Make REM calculations easier. font-family: var(--font-family-sans-kr); + color: var(--txt-black-darkest); + color-scheme: light; +} + +// TODO: Change data attribute constant to import from bezier-tokens +[data-bezier-theme='light'] { + color-scheme: light; +} + +// TODO: Change data attribute constant to import from bezier-tokens +[data-bezier-theme='dark'] { + color-scheme: dark; } :lang(ja) {