& { theme: StyledTheme }
+
+type Interpolation =
+ | ((props: InterpolationProps
) => unknown)
+ | string
+ | number
+ | boolean
+ | null
+ | undefined
interface StyledCreator {
+ (
+ tag: T,
+ styles: DevupPropsWithTheme,
+ ): (props: React.ComponentProps) => React.ReactElement
(
tag: T,
): (
strings: TemplateStringsArray | DevupPropsWithTheme,
- ...values: (
- | ((props: React.ComponentProps) => unknown)
- | string
- | number
- | boolean
- | null
- | undefined
- )[][]
+ ...values: Interpolation[][]
) => (props: React.ComponentProps) => React.ReactElement
}
type Styled = StyledCreator & {
- [T in keyof React.JSX.IntrinsicElements]: (
+ [T in keyof React.JSX.IntrinsicElements]:
>(
strings: TemplateStringsArray | DevupPropsWithTheme,
- ...values: (
- | ((props: P & React.ComponentProps) => unknown)
- | string
- | number
- | boolean
- | null
- | undefined
- )[]
+ ...values: Interpolation[]
) => (props: P & React.ComponentProps) => React.ReactElement
}
diff --git a/packages/react/src/utils/stylex.ts b/packages/react/src/utils/stylex.ts
index 373c2b0f0..e46905f6c 100644
--- a/packages/react/src/utils/stylex.ts
+++ b/packages/react/src/utils/stylex.ts
@@ -30,6 +30,12 @@ export function props(
throw new Error('Cannot run on the runtime')
}
+export function attrs(
+ ..._styles: ReadonlyArray
+): { class?: string; style?: Record } {
+ throw new Error('Cannot run on the runtime')
+}
+
export function keyframes(_frames: Record): string {
throw new Error('Cannot run on the runtime')
}
@@ -56,6 +62,26 @@ export function createTheme>(
throw new Error('Cannot run on the runtime')
}
+export function createThemeContract>(
+ _vars: V,
+): { readonly [K in keyof V]: string } {
+ throw new Error('Cannot run on the runtime')
+}
+
+export function defineConsts>(
+ _consts: V,
+): { readonly [K in keyof V]: V[K] } {
+ throw new Error('Cannot run on the runtime')
+}
+
+export function positionTry(_fallback: StyleProperties): string {
+ throw new Error('Cannot run on the runtime')
+}
+
+export function viewTransitionClass(_styles: StyleProperties): string {
+ throw new Error('Cannot run on the runtime')
+}
+
export const types: StyleXTypes = new Proxy({} as StyleXTypes, {
get() {
return () => {
diff --git a/packages/react/src/utils/theme-vars.ts b/packages/react/src/utils/theme-vars.ts
new file mode 100644
index 000000000..88f54d7a2
--- /dev/null
+++ b/packages/react/src/utils/theme-vars.ts
@@ -0,0 +1,59 @@
+import type { CSSProperties } from 'react'
+
+import type { StyledTheme } from '../types/props'
+
+export type { StyledTheme, StyledThemeValue } from '../types/props'
+
+/**
+ * Join a theme path into the CSS custom property that carries it, so
+ * `theme.colors.brand` and the `--colors-brand` variable always agree.
+ */
+export function themeVariableName(path: readonly string[]): string {
+ return `--${path.join('-')}`
+}
+
+/**
+ * Flatten a (possibly nested) theme into CSS custom property declarations.
+ *
+ * `display: contents` keeps the provider out of layout: the element exists only
+ * to scope the variables to its subtree, and the cascade handles nesting.
+ */
+export function themeToCssVariables(theme?: StyledTheme): CSSProperties {
+ const style: Record = { display: 'contents' }
+ const walk = (node: StyledTheme, path: string[]) => {
+ for (const [key, value] of Object.entries(node)) {
+ const next = [...path, key]
+ if (value !== null && typeof value === 'object') {
+ walk(value, next)
+ } else if (value !== undefined) {
+ style[themeVariableName(next)] = value
+ }
+ }
+ }
+ if (theme) walk(theme, [])
+ return style as CSSProperties
+}
+
+/**
+ * Theme accessor backed purely by CSS variables.
+ *
+ * Reading any path yields the `var(--path)` reference for it, so a value read in
+ * JS and a value the extractor inlined at build time resolve to the same custom
+ * property. Nesting works because each read returns another accessor.
+ */
+export function createThemeAccessor(
+ path: readonly string[] = [],
+): T {
+ const reference = path.length ? `var(${themeVariableName(path)})` : ''
+ return new Proxy(
+ {},
+ {
+ get(_target, key) {
+ if (key === Symbol.toPrimitive) return () => reference
+ if (typeof key !== 'string') return undefined
+ if (key === 'toString' || key === 'valueOf') return () => reference
+ return createThemeAccessor([...path, key])
+ },
+ },
+ ) as T
+}
diff --git a/packages/react/src/utils/with-theme.tsx b/packages/react/src/utils/with-theme.tsx
new file mode 100644
index 000000000..14f25c2bb
--- /dev/null
+++ b/packages/react/src/utils/with-theme.tsx
@@ -0,0 +1,12 @@
+import type { ComponentType } from 'react'
+
+import { useStyledTheme } from '../hooks/use-styled-theme'
+import type { StyledTheme } from './theme-vars'
+
+export function withTheme(
+ Component: ComponentType
,
+): ComponentType> {
+ return function WithTheme(props: Omit) {
+ return
+ }
+}
diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts
index c496cdf5b..17167e831 100644
--- a/packages/react/vite.config.ts
+++ b/packages/react/vite.config.ts
@@ -49,6 +49,7 @@ export default defineConfig({
formats: ['es', 'cjs'],
entry: {
index: 'src/index.ts',
+ 'compat/index': 'src/compat/index.ts',
},
},
outDir: 'dist',
diff --git a/packages/rsbuild-plugin/src/plugin.ts b/packages/rsbuild-plugin/src/plugin.ts
index fedc42e2d..16f618fd2 100644
--- a/packages/rsbuild-plugin/src/plugin.ts
+++ b/packages/rsbuild-plugin/src/plugin.ts
@@ -5,6 +5,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
import {
buildCanonicalMap,
computeFileReach,
+ createCompatTypes,
createNodeModulesExcludeRegex,
createThemeInterfaceArgs,
type CustomShorthands,
@@ -127,6 +128,11 @@ export const DevupUI = ({
if (!existsSync(distDir)) await mkdir(distDir, { recursive: true })
await writeFile(join(distDir, '.gitignore'), '*', 'utf-8')
+ await writeFile(
+ join(distDir, 'compat.d.ts'),
+ createCompatTypes(importAliases),
+ 'utf-8',
+ )
await writeDataFiles({
package: libPackage,
diff --git a/packages/vite-plugin/src/__tests__/plugin.test.ts b/packages/vite-plugin/src/__tests__/plugin.test.ts
index cb6582988..53399fe8d 100644
--- a/packages/vite-plugin/src/__tests__/plugin.test.ts
+++ b/packages/vite-plugin/src/__tests__/plugin.test.ts
@@ -1095,6 +1095,7 @@ describe('devupUIVitePlugin', () => {
true,
false,
{
+ '@emotion/react': null,
'@emotion/styled': 'styled',
'@vanilla-extract/css': null,
'styled-components': 'styled',
@@ -1112,6 +1113,7 @@ describe('devupUIVitePlugin', () => {
true,
false,
{
+ '@emotion/react': null,
'@emotion/styled': 'styled',
'@vanilla-extract/css': null,
'styled-components': 'styled',
diff --git a/packages/vite-plugin/src/plugin.ts b/packages/vite-plugin/src/plugin.ts
index 546dd81bf..4d39cb6a0 100644
--- a/packages/vite-plugin/src/plugin.ts
+++ b/packages/vite-plugin/src/plugin.ts
@@ -5,6 +5,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
import {
buildCanonicalMap,
computeFileReach,
+ createCompatTypes,
createNodeModulesExcludeRegex,
createThemeInterfaceArgs,
type CustomShorthands,
@@ -287,6 +288,11 @@ export function DevupUI({
}
if (!existsSync(distDir)) await mkdir(distDir, { recursive: true })
await writeFile(join(distDir, '.gitignore'), '*', 'utf-8')
+ await writeFile(
+ join(distDir, 'compat.d.ts'),
+ createCompatTypes(importAliases),
+ 'utf-8',
+ )
await writeDataFiles({
package: libPackage,
cssDir,
diff --git a/packages/webpack-plugin/src/plugin.ts b/packages/webpack-plugin/src/plugin.ts
index 1a43af8b6..c5dbc18aa 100644
--- a/packages/webpack-plugin/src/plugin.ts
+++ b/packages/webpack-plugin/src/plugin.ts
@@ -6,6 +6,7 @@ import { dirname, join, relative, resolve } from 'node:path'
import {
buildCanonicalMap,
computeFileReach,
+ createCompatTypes,
createNodeModulesExcludeRegex,
createThemeInterfaceArgs,
type CustomShorthands,
@@ -186,6 +187,11 @@ export class DevupUIWebpackPlugin {
if (!existsSync(this.options.distDir))
mkdirSync(this.options.distDir, { recursive: true })
writeFileSync(join(this.options.distDir, '.gitignore'), '*', 'utf-8')
+ writeFileSync(
+ join(this.options.distDir, 'compat.d.ts'),
+ createCompatTypes(this.importAliases),
+ 'utf-8',
+ )
if (this.options.watch) {
try {