diff --git a/.changepacks/changepack_log_types_match_build.json b/.changepacks/changepack_log_types_match_build.json
new file mode 100644
index 000000000..2865577c2
--- /dev/null
+++ b/.changepacks/changepack_log_types_match_build.json
@@ -0,0 +1,8 @@
+{
+ "changes": {
+ "packages/react/package.json": "Patch",
+ "packages/plugin-utils/package.json": "Patch"
+ },
+ "note": "Types now accept what the build compiles and nothing it rejects. css() takes classes, style objects, arrays and falsy parts, and the css/keyframes/globalCss tags take the static interpolations the build evaluates. styled gets .attrs(), .withConfig() and .withComponent(), and its result renders another tag. styleVars takes numbers; styleOrder is 1 to 254; ResponsiveValue is exported for hand-written custom shorthands. The compat declarations name what Emotion, styled-components and vanilla-extract give (SerializedStyles, Keyframes, RuleSet, an augmentable Theme and DefaultTheme, CSSObject and the other styled-components types) and follow the vanilla-extract APIs the evaluator registers (style arrays, the styleVariants mapper, createGlobalTheme, createGlobalThemeContract; globalKeyframes, composeStyles and generateIdentifier are gone). DevupTheme takes shadow as devup.json documents it. No new build errors.",
+ "date": "2026-10-01T00:00:00.000Z"
+}
\ No newline at end of file
diff --git a/packages/plugin-utils/src/__tests__/theme-type.test.ts b/packages/plugin-utils/src/__tests__/theme-type.test.ts
new file mode 100644
index 000000000..1c495e626
--- /dev/null
+++ b/packages/plugin-utils/src/__tests__/theme-type.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from 'bun:test'
+
+import type { DevupConfig, DevupTheme } from '../types'
+
+describe('DevupTheme', () => {
+ it('spells the shadow tokens as the docs and the engine do', () => {
+ const shadow = { default: { card: '0 1px 2px #0003' } }
+ const documented: DevupTheme = { shadow }
+ const alias: DevupTheme = { shadows: shadow }
+ const config: DevupConfig = { theme: { shadow } }
+
+ expect(documented.shadow).toBe(shadow)
+ expect(alias.shadows).toBe(shadow)
+ expect(config.theme?.shadow).toBe(shadow)
+ })
+})
diff --git a/packages/plugin-utils/src/types.ts b/packages/plugin-utils/src/types.ts
index 2a441a433..f0c251b4b 100644
--- a/packages/plugin-utils/src/types.ts
+++ b/packages/plugin-utils/src/types.ts
@@ -57,6 +57,9 @@ export interface DevupTheme {
breakpoints?: number[]
typography?: ThemeTypography
length?: ThemeLength
+ /** Shadow tokens, as the docs and devup.json schema spell them */
+ shadow?: ThemeShadows
+ /** The engine also reads this spelling; prefer shadow */
shadows?: ThemeShadows
}
diff --git a/packages/react/package.json b/packages/react/package.json
index 9422f7176..b451f1692 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -21,7 +21,7 @@
"scripts": {
"lint": "eslint && bun ../../node_modules/@typescript/native/bin/tsc --noEmit && bun run test:types",
"build": "bun run test:types && bun ../../node_modules/@typescript/native/bin/tsc && vite build",
- "test:types": "bun ../../node_modules/@typescript/native/bin/tsc --ignoreConfig --noEmit --strict --jsx react-jsx --moduleResolution bundler --module esnext --target esnext --types bun --skipLibCheck type-tests/custom-shorthand.ts type-tests/class-names.ts"
+ "test:types": "bun ../../node_modules/@typescript/native/bin/tsc -p type-tests/tsconfig.json"
},
"publishConfig": {
"access": "public"
diff --git a/packages/react/src/compat/emotion.d.ts b/packages/react/src/compat/emotion.d.ts
index 591f8f076..af350741b 100644
--- a/packages/react/src/compat/emotion.d.ts
+++ b/packages/react/src/compat/emotion.d.ts
@@ -13,17 +13,73 @@ declare module '@emotion/css' {
}
declare module '@emotion/react' {
- export { css, keyframes } from '@devup-ui/react'
- export type { StyledTheme as Theme } from '@devup-ui/react/compat'
- export type { ClassNamesArg, ClassNamesContent } from '@devup-ui/react/compat'
+ import type {
+ CssPart,
+ KeyframesProps,
+ StyleTemplateValue,
+ } from '@devup-ui/react'
+ import type {
+ ClassNamesArg,
+ ClassNamesContent,
+ StyledTheme,
+ } from '@devup-ui/react/compat'
+ import type {
+ ComponentType,
+ ForwardRefExoticComponent,
+ PropsWithoutRef,
+ ReactNode,
+ RefAttributes,
+ } from 'react'
+
+ const serializedStyles: unique symbol
+ const keyframesName: unique symbol
+
+ /**
+ * What `css` gives: the build writes the class it makes of the styles in its
+ * place, so the value is that class, not an object with `name` and `styles`.
+ */
+ export type SerializedStyles = string & {
+ readonly [serializedStyles]: true
+ }
+
+ /** What `keyframes` gives: the name the build gives the animation. */
+ export type Keyframes = string & { readonly [keyframesName]: true }
+
+ /** The theme `ThemeProvider` publishes as CSS variables; augment it as in Emotion. */
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
+ export interface Theme extends StyledTheme {}
+
+ export type { ClassNamesArg, ClassNamesContent }
+
+ export function css(
+ strings: TemplateStringsArray,
+ ...values: StyleTemplateValue[]
+ ): SerializedStyles
+ export function css(...parts: CssPart[]): SerializedStyles
+
+ export function keyframes(props: KeyframesProps): Keyframes
+ export function keyframes(
+ strings: TemplateStringsArray,
+ ...values: StyleTemplateValue[]
+ ): Keyframes
+
+ export function useTheme(): Theme
+ export function withTheme
(
+ component: ComponentType
,
+ ): ForwardRefExoticComponent<
+ PropsWithoutRef & { theme?: Theme }> &
+ RefAttributes
+ >
+ export function ThemeProvider(props: {
+ theme?: Theme | ((outer: Theme | undefined) => Theme)
+ children?: ReactNode
+ }): ReactNode
+
export {
CacheProvider,
ClassNames,
jsx as createElement,
Global,
jsx,
- ThemeProvider,
- useTheme,
- withTheme,
} from '@devup-ui/react/compat'
}
diff --git a/packages/react/src/compat/styled-components.d.ts b/packages/react/src/compat/styled-components.d.ts
index abf6a3f06..e1c85a9d0 100644
--- a/packages/react/src/compat/styled-components.d.ts
+++ b/packages/react/src/compat/styled-components.d.ts
@@ -1,18 +1,84 @@
declare module 'styled-components' {
- import { styled } from '@devup-ui/react'
+ import {
+ type CssPart,
+ type DevupProps,
+ type KeyframesProps,
+ styled,
+ type StyleTemplateValue,
+ } from '@devup-ui/react'
+ import type { StyledTheme } from '@devup-ui/react/compat'
+ import type { Properties } from 'csstype-extra'
+ import type {
+ ComponentType,
+ Context,
+ ForwardRefExoticComponent,
+ PropsWithoutRef,
+ ReactNode,
+ RefAttributes,
+ } from 'react'
+
+ const ruleSet: unique symbol
+ const keyframesName: unique symbol
+
+ /**
+ * What `css` gives: the build writes the class it makes of the rules in its
+ * place, so the value is that class and only the build-known parts compose it.
+ */
+ export type RuleSet = string & { readonly [ruleSet]: true }
+
+ /** What `keyframes` gives: the name the build gives the animation. */
+ export type Keyframes = string & { readonly [keyframesName]: true }
+
+ /** The theme `ThemeProvider` publishes as CSS variables; augment it as in styled-components. */
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
+ export interface DefaultTheme extends StyledTheme {}
+
+ export type CSSProperties = Properties
+ export type CSSObject = DevupProps
+ export type CSSPseudos = Record<`:${string}`, CSSObject>
+ export type CSSKeyframes = KeyframesProps
+ export type CSSProp = CssPart
+
+ export function css(props: DevupProps): RuleSet
+ export function css(
+ strings: TemplateStringsArray,
+ ...values: StyleTemplateValue[]
+ ): RuleSet
+ export function css(...parts: CssPart[]): RuleSet
+
+ export function keyframes(props: KeyframesProps): Keyframes
+ export function keyframes(
+ strings: TemplateStringsArray,
+ ...values: StyleTemplateValue[]
+ ): Keyframes
+
+ export function useTheme(): DefaultTheme
+ export function withTheme
(
+ component: ComponentType
,
+ ): ForwardRefExoticComponent<
+ PropsWithoutRef & { theme?: DefaultTheme }> &
+ RefAttributes
+ >
+ export function ThemeProvider(props: {
+ theme?: DefaultTheme | ((outer: DefaultTheme | undefined) => DefaultTheme)
+ children?: ReactNode
+ }): ReactNode
+ export function ThemeConsumer(props: {
+ children: (theme: DefaultTheme) => ReactNode
+ }): ReactNode
+
+ /**
+ * Not compiled: the build keeps this import on `styled-components`, which
+ * must then be installed.
+ */
+ export const ThemeContext: Context
- export { css, keyframes } from '@devup-ui/react'
- export type { StyledTheme as DefaultTheme } from '@devup-ui/react/compat'
export {
createGlobalStyle,
createTheme,
isStyledComponent,
ServerStyleSheet,
StyleSheetManager,
- ThemeConsumer,
- ThemeProvider,
- useTheme,
- withTheme,
} from '@devup-ui/react/compat'
export { styled }
diff --git a/packages/react/src/compat/vanilla-extract.d.ts b/packages/react/src/compat/vanilla-extract.d.ts
index 13ddd2357..802791f4f 100644
--- a/packages/react/src/compat/vanilla-extract.d.ts
+++ b/packages/react/src/compat/vanilla-extract.d.ts
@@ -3,6 +3,9 @@ declare module '@vanilla-extract/css' {
type VanillaRule = Record
+ /** A rule, or rules composed in order: later declarations replace earlier ones */
+ type VanillaStyle = VanillaRule | string | readonly VanillaStyle[]
+
/**
* Keeps vanilla-extract's own two-argument shape. The extractor folds the
* selector back into the object `globalCss` takes, so the call compiles even
@@ -13,26 +16,44 @@ declare module '@vanilla-extract/css' {
/**
* Declared but not rewritten: these only run inside `.css.ts` / `.css.js`
* stylesheets, where the extractor evaluates the module and replaces every
- * call with its generated output.
+ * call with its generated output. Each is one of the APIs the evaluator
+ * registers.
*/
- export function styleVariants>(
+ export function styleVariants>(
variants: T,
): Record
+ export function styleVariants<
+ T extends Record,
+ Key extends keyof T = keyof T,
+ >(
+ data: T,
+ mapData: (value: T[Key], key: Key) => VanillaStyle,
+ ): Record
export function createVar(): string
export function fallbackVar(...values: string[]): string
export function fontFace(rule: VanillaRule): string
export function globalFontFace(name: string, rule: VanillaRule): void
- export function globalKeyframes(name: string, frames: VanillaRule): void
export function createTheme(contract: T, values: unknown): string
export function createTheme(values: T): [string, T]
+ export function createGlobalTheme(
+ selector: string,
+ values: T,
+ ): T
+ export function createGlobalTheme(
+ selector: string,
+ contract: T,
+ values: unknown,
+ ): void
export function createThemeContract(shape: T): T
+ export function createGlobalThemeContract(
+ tokens: T,
+ mapFn?: (value: unknown, path: string[]) => string,
+ ): T
export function assignVars(
contract: unknown,
values: unknown,
): Record
- export function composeStyles(...classNames: string[]): string
export function layer(options?: unknown): string
export function globalLayer(options: unknown, name?: string): string
export function createContainer(): string
- export function generateIdentifier(debugId?: string): string
}
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index f81ed198a..1de5d171a 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -10,16 +10,19 @@ export { ThemeScript } from './components/ThemeScript'
export { VStack } from './components/VStack'
export { useTheme } from './hooks/use-theme'
export type { DevupProps } from './types/props'
+export type { ResponsiveValue } from './types/responsive-value'
export type { DevupCustomShorthands } from './types/shorthand'
export type { DevupTheme } from './types/theme'
export type {
DevupThemeTypography,
DevupThemeTypographyKeys,
} from './types/typography'
+export type { CssPart, StyleTemplateValue } from './utils/css'
export { css } from './utils/css'
export { getTheme } from './utils/get-theme'
export { globalCss } from './utils/global-css'
export { initTheme } from './utils/init-theme'
+export type { KeyframesProps } from './utils/keyframes'
export { keyframes } from './utils/keyframes'
export { setTheme } from './utils/set-theme'
export { styled } from './utils/styled'
diff --git a/packages/react/src/types/props/index.ts b/packages/react/src/types/props/index.ts
index 2972d17aa..c2646b9b5 100644
--- a/packages/react/src/types/props/index.ts
+++ b/packages/react/src/types/props/index.ts
@@ -44,7 +44,7 @@ export interface DevupComponentProps<
T extends React.ElementType,
> extends DevupPropsWithTheme {
as?: T
- styleVars?: Record
+ styleVars?: Record
}
export type DevupComponentMergedProps = Merge<
diff --git a/packages/react/src/types/props/selector/index.ts b/packages/react/src/types/props/selector/index.ts
index 81d19ad0c..f8ab9c576 100644
--- a/packages/react/src/types/props/selector/index.ts
+++ b/packages/react/src/types/props/selector/index.ts
@@ -4,6 +4,14 @@ import type { ResponsiveValue } from '../../responsive-value'
import type { DevupTheme } from '../../theme'
import type { DevupProps } from '../index'
+type Range<
+ N extends number,
+ Acc extends number[] = [],
+> = Acc['length'] extends N ? Acc[number] : Range
+
+/** The cascade layer orders the build places a style in: 1 to 254, written as a number or as its digits. Any other value is ignored by the build. */
+export type StyleOrder = Exclude, 0>
+
export type CamelCase =
S extends Lowercase
? S extends `${infer F}-${infer RF}${infer R}`
@@ -108,5 +116,5 @@ export interface DevupSelectorProps
selectors?: Selectors
- styleOrder?: number
+ styleOrder?: StyleOrder | `${StyleOrder}`
}
diff --git a/packages/react/src/utils/css.ts b/packages/react/src/utils/css.ts
index 5ae6ffaea..5f5100daf 100644
--- a/packages/react/src/utils/css.ts
+++ b/packages/react/src/utils/css.ts
@@ -1,11 +1,19 @@
import type { DevupPropsWithTheme } from '../types/props'
-export function css(props: DevupPropsWithTheme): string
-export function css(strings: TemplateStringsArray): string
-export function css(): string
+/** What a tagged `css`, `keyframes` or `globalCss` template takes between its text: values the build knows, and what `css()` and `keyframes()` give */
+export type StyleTemplateValue = string | number | boolean | null | undefined
+
+/** A part `css()` composes: a style object, a class (another `css()` result or a plain class), a falsy part the build skips, or an array of parts */
+export type CssPart =
+ DevupPropsWithTheme | string | false | null | undefined | readonly CssPart[]
+export function css(props: DevupPropsWithTheme): string
export function css(
- _strings?: TemplateStringsArray | DevupPropsWithTheme,
-): string {
+ strings: TemplateStringsArray,
+ ...values: StyleTemplateValue[]
+): string
+export function css(...parts: CssPart[]): string
+
+export function css(..._parts: unknown[]): string {
throw new Error('Cannot run on the runtime')
}
diff --git a/packages/react/src/utils/global-css.ts b/packages/react/src/utils/global-css.ts
index dd86476c8..6a6a5757b 100644
--- a/packages/react/src/utils/global-css.ts
+++ b/packages/react/src/utils/global-css.ts
@@ -9,6 +9,7 @@ import type {
MediaShorthand,
SimpleSelector,
} from '../types/props/selector'
+import type { StyleTemplateValue } from './css'
type GlobalCssKeys =
| `*${T}`
@@ -87,7 +88,10 @@ export function globalCss(
>,
): void
-export function globalCss(strings?: TemplateStringsArray): void
+export function globalCss(
+ strings?: TemplateStringsArray,
+ ...values: StyleTemplateValue[]
+): void
export function globalCss(): void
@@ -99,6 +103,7 @@ export function globalCss(
string,
DevupCommonProps & DevupSelectorProps & DevupThemeSelectorProps
>,
+ ..._values: StyleTemplateValue[]
): void {
throw new Error('Cannot run on the runtime')
}
diff --git a/packages/react/src/utils/keyframes.ts b/packages/react/src/utils/keyframes.ts
index a2f365aa4..c15fc19b1 100644
--- a/packages/react/src/utils/keyframes.ts
+++ b/packages/react/src/utils/keyframes.ts
@@ -1,6 +1,7 @@
import type { DevupCommonProps } from '../types/props'
+import type { StyleTemplateValue } from './css'
-interface KeyframesProps {
+export interface KeyframesProps {
from?: DevupCommonProps | string
to?: DevupCommonProps | string
[key: `${number}%`]: DevupCommonProps | string
@@ -8,11 +9,15 @@ interface KeyframesProps {
export function keyframes(props: KeyframesProps): string
export function keyframes(props: Record): string
-export function keyframes(strings: TemplateStringsArray): string
+export function keyframes(
+ strings: TemplateStringsArray,
+ ...values: StyleTemplateValue[]
+): string
export function keyframes(): string
export function keyframes(
_strings?: TemplateStringsArray | KeyframesProps,
+ ..._values: StyleTemplateValue[]
): string {
throw new Error('Cannot run on the runtime')
}
diff --git a/packages/react/src/utils/styled.ts b/packages/react/src/utils/styled.ts
index 66d22c84a..de00527b8 100644
--- a/packages/react/src/utils/styled.ts
+++ b/packages/react/src/utils/styled.ts
@@ -18,24 +18,54 @@ type Interpolation
=
| null
| undefined
+type StyledProps
= P &
+ React.ComponentProps
+
+/** What `.withConfig()` takes: the props to forward, and names the build ignores */
+export interface StyledConfig {
+ shouldForwardProp?: (
+ prop: string,
+ defaultValidatorFn: (prop: string) => boolean,
+ ) => boolean
+ displayName?: string
+ componentId?: string
+}
+
+/** A component the build generates from `styled`, which can render another tag with the same styles */
+export interface StyledComponent
{
+ (props: P): React.ReactElement
+ withComponent(
+ tag: U,
+ ): StyledComponent>
+}
+
+/** What `styled.div`, `styled('div')` and what `.attrs()` and `.withConfig()` give: rules or CSS text come next */
+export interface StyledTemplate<
+ T extends React.ElementType | React.ComponentType,
+ B = unknown,
+> {
+