Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_types_match_build.json
Original file line number Diff line number Diff line change
@@ -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"
}
16 changes: 16 additions & 0 deletions packages/plugin-utils/src/__tests__/theme-type.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
3 changes: 3 additions & 0 deletions packages/plugin-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
68 changes: 62 additions & 6 deletions packages/react/src/compat/emotion.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<P extends { theme?: Theme }>(
component: ComponentType<P>,
): ForwardRefExoticComponent<
PropsWithoutRef<Omit<P, 'theme'> & { theme?: Theme }> &
RefAttributes<unknown>
>
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'
}
80 changes: 73 additions & 7 deletions packages/react/src/compat/styled-components.d.ts
Original file line number Diff line number Diff line change
@@ -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<P extends { theme?: DefaultTheme }>(
component: ComponentType<P>,
): ForwardRefExoticComponent<
PropsWithoutRef<Omit<P, 'theme'> & { theme?: DefaultTheme }> &
RefAttributes<unknown>
>
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<DefaultTheme | undefined>

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 }
Expand Down
31 changes: 26 additions & 5 deletions packages/react/src/compat/vanilla-extract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ declare module '@vanilla-extract/css' {

type VanillaRule = Record<string, unknown>

/** 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
Expand All @@ -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<T extends Record<string, VanillaRule>>(
export function styleVariants<T extends Record<string, VanillaStyle>>(
variants: T,
): Record<keyof T, string>
export function styleVariants<
T extends Record<string, unknown>,
Key extends keyof T = keyof T,
>(
data: T,
mapData: (value: T[Key], key: Key) => VanillaStyle,
): Record<keyof T, string>
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<T>(contract: T, values: unknown): string
export function createTheme<T extends VanillaRule>(values: T): [string, T]
export function createGlobalTheme<T extends VanillaRule>(
selector: string,
values: T,
): T
export function createGlobalTheme<T>(
selector: string,
contract: T,
values: unknown,
): void
export function createThemeContract<T>(shape: T): T
export function createGlobalThemeContract<T extends VanillaRule>(
tokens: T,
mapFn?: (value: unknown, path: string[]) => string,
): T
export function assignVars(
contract: unknown,
values: unknown,
): Record<string, string>
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
}
3 changes: 3 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/types/props/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface DevupComponentProps<
T extends React.ElementType,
> extends DevupPropsWithTheme {
as?: T
styleVars?: Record<string, string | undefined>
styleVars?: Record<string, string | number | undefined>
}

export type DevupComponentMergedProps<T extends React.ElementType> = Merge<
Expand Down
10 changes: 9 additions & 1 deletion packages/react/src/types/props/selector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<N, [...Acc, Acc['length']]>

/** 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<Range<255>, 0>

export type CamelCase<S extends string> =
S extends Lowercase<S>
? S extends `${infer F}-${infer RF}${infer R}`
Expand Down Expand Up @@ -108,5 +116,5 @@ export interface DevupSelectorProps

selectors?: Selectors

styleOrder?: number
styleOrder?: StyleOrder | `${StyleOrder}`
}
18 changes: 13 additions & 5 deletions packages/react/src/utils/css.ts
Original file line number Diff line number Diff line change
@@ -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')
}
7 changes: 6 additions & 1 deletion packages/react/src/utils/global-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
MediaShorthand,
SimpleSelector,
} from '../types/props/selector'
import type { StyleTemplateValue } from './css'

type GlobalCssKeys<T extends string> =
| `*${T}`
Expand Down Expand Up @@ -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

Expand All @@ -99,6 +103,7 @@ export function globalCss(
string,
DevupCommonProps & DevupSelectorProps & DevupThemeSelectorProps
>,
..._values: StyleTemplateValue[]
): void {
throw new Error('Cannot run on the runtime')
}
Loading
Loading