diff --git a/.eslintrc.js b/.eslintrc.js index 80026054a..476cae43a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,5 +22,6 @@ module.exports = { // TypeScript checks this 'no-undef': 'off', + 'no-lone-blocks': 'off', }, } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ecf3d4aa6..5d4792a8e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,50 +1,52 @@ -import { ThemeUIStyleObject } from '@theme-ui/css' - -import { ThemeUIJSX } from './jsx-namespace' - -export interface SxProp { - /** - * The sx prop lets you style elements inline, using values from your - * theme. To use the sx prop, add the custom pragma as a comment to the - * top of your module and import the jsx function. - * - * ```ts - * // @jsx jsx - * - * import { jsx } from 'theme-ui' - * ``` - */ - sx?: ThemeUIStyleObject -} - -export interface IntrinsicSxElements { - p: ThemeUIJSX.IntrinsicElements['p'] - b: ThemeUIJSX.IntrinsicElements['b'] - i: ThemeUIJSX.IntrinsicElements['i'] - a: ThemeUIJSX.IntrinsicElements['a'] - h1: ThemeUIJSX.IntrinsicElements['h1'] - h2: ThemeUIJSX.IntrinsicElements['h2'] - h3: ThemeUIJSX.IntrinsicElements['h3'] - h4: ThemeUIJSX.IntrinsicElements['h4'] - h5: ThemeUIJSX.IntrinsicElements['h5'] - h6: ThemeUIJSX.IntrinsicElements['h6'] - img: ThemeUIJSX.IntrinsicElements['img'] - pre: ThemeUIJSX.IntrinsicElements['pre'] - code: ThemeUIJSX.IntrinsicElements['code'] - ol: ThemeUIJSX.IntrinsicElements['ol'] - ul: ThemeUIJSX.IntrinsicElements['ul'] - li: ThemeUIJSX.IntrinsicElements['li'] - blockquote: ThemeUIJSX.IntrinsicElements['blockquote'] - hr: ThemeUIJSX.IntrinsicElements['hr'] - table: ThemeUIJSX.IntrinsicElements['table'] - tr: ThemeUIJSX.IntrinsicElements['tr'] - th: ThemeUIJSX.IntrinsicElements['th'] - td: ThemeUIJSX.IntrinsicElements['td'] - em: ThemeUIJSX.IntrinsicElements['em'] - strong: ThemeUIJSX.IntrinsicElements['strong'] - div: ThemeUIJSX.IntrinsicElements['div'] - del: ThemeUIJSX.IntrinsicElements['div'] - inlineCode: ThemeUIJSX.IntrinsicElements['div'] - thematicBreak: ThemeUIJSX.IntrinsicElements['div'] - root: ThemeUIJSX.IntrinsicElements['div'] -} +import { Interpolation } from '@emotion/react' +import { ThemeUIStyleObject, Theme as ThemeUITheme } from '@theme-ui/css' + +import { ThemeUIJSX } from './jsx-namespace' + +export interface SxProp { + /** + * The sx prop lets you style elements inline, using values from your + * theme. + * + * @see https://theme-ui.com/sx-prop/ + */ + sx?: ThemeUIStyleObject + /** + * Theme UI uses Emotion's JSX function. You can pass styles to it directly + * using `css` prop. + * @see https://theme-ui.com/sx-prop/#raw-css + */ + css?: Interpolation +} + +export interface IntrinsicSxElements { + p: ThemeUIJSX.IntrinsicElements['p'] + b: ThemeUIJSX.IntrinsicElements['b'] + i: ThemeUIJSX.IntrinsicElements['i'] + a: ThemeUIJSX.IntrinsicElements['a'] + h1: ThemeUIJSX.IntrinsicElements['h1'] + h2: ThemeUIJSX.IntrinsicElements['h2'] + h3: ThemeUIJSX.IntrinsicElements['h3'] + h4: ThemeUIJSX.IntrinsicElements['h4'] + h5: ThemeUIJSX.IntrinsicElements['h5'] + h6: ThemeUIJSX.IntrinsicElements['h6'] + img: ThemeUIJSX.IntrinsicElements['img'] + pre: ThemeUIJSX.IntrinsicElements['pre'] + code: ThemeUIJSX.IntrinsicElements['code'] + ol: ThemeUIJSX.IntrinsicElements['ol'] + ul: ThemeUIJSX.IntrinsicElements['ul'] + li: ThemeUIJSX.IntrinsicElements['li'] + blockquote: ThemeUIJSX.IntrinsicElements['blockquote'] + hr: ThemeUIJSX.IntrinsicElements['hr'] + table: ThemeUIJSX.IntrinsicElements['table'] + tr: ThemeUIJSX.IntrinsicElements['tr'] + th: ThemeUIJSX.IntrinsicElements['th'] + td: ThemeUIJSX.IntrinsicElements['td'] + em: ThemeUIJSX.IntrinsicElements['em'] + strong: ThemeUIJSX.IntrinsicElements['strong'] + div: ThemeUIJSX.IntrinsicElements['div'] + del: ThemeUIJSX.IntrinsicElements['div'] + inlineCode: ThemeUIJSX.IntrinsicElements['div'] + thematicBreak: ThemeUIJSX.IntrinsicElements['div'] + root: ThemeUIJSX.IntrinsicElements['div'] +} diff --git a/packages/core/test/react-jsx.tsx b/packages/core/test/react-jsx.tsx index c716a1a2e..7e1279045 100644 --- a/packages/core/test/react-jsx.tsx +++ b/packages/core/test/react-jsx.tsx @@ -1,7 +1,7 @@ /* eslint-disable no-lone-blocks */ /** @jsx jsx */ -import { renderJSON, NotHas, Assert } from '@theme-ui/test-utils' +import { renderJSON, NotHas, Assert, expecter } from '@theme-ui/test-utils' import { jsx, SxProp, ThemeUIJSX } from '../src' @@ -26,6 +26,40 @@ describe('JSX', () => { ) ).toMatchSnapshot() }) + + test('accepts css prop', () => { + const expectSnippet = expecter( + `/** @jsxImportSource @theme-ui/core */ + + export {}`, + { jsx: false } + ) + + expectSnippet(`const _1 =
`).toSucceed() + + // Theme UI theme can be injected to @emotion/react module in userspace + expectSnippet( + ` + import { Theme as ThemeUITheme } from '@theme-ui/css' + + declare module '@emotion/react' { + export interface Theme extends ThemeUITheme {} + } + +
{ + const _t = t; + return {} + }} + />` + ).toInfer('_t', 'Theme') + + expectSnippet( + `import { css } from '@emotion/react' + + const TestComponent = () =>
` + ).toSucceed() + }) }) { diff --git a/packages/css/emotion-theme.d.ts b/packages/css/emotion-theme.d.ts deleted file mode 100644 index e6575bbbb..000000000 --- a/packages/css/emotion-theme.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import '@emotion/react' - -import { Theme } from '@theme-ui/css' - -interface ThemeUITheme extends Theme {} - -declare module '@emotion/react' { - export interface Theme extends ThemeUITheme {} -} diff --git a/packages/css/src/index.ts b/packages/css/src/index.ts index ef6d2bf51..554a2a0e3 100644 --- a/packages/css/src/index.ts +++ b/packages/css/src/index.ts @@ -302,99 +302,100 @@ const transforms = [ {} ) -const responsive = ( - styles: Exclude -) => (theme?: Theme) => { - const next: Exclude = {} - const breakpoints = - (theme && (theme.breakpoints as string[])) || defaultBreakpoints - const mediaQueries = [ - null, - ...breakpoints.map((n) => - n.includes('@media') ? n : `@media screen and (min-width: ${n})` - ), - ] +const responsive = + (styles: Exclude) => + (theme?: Theme) => { + const next: Exclude = {} + const breakpoints = + (theme && (theme.breakpoints as string[])) || defaultBreakpoints + const mediaQueries = [ + null, + ...breakpoints.map((n) => + n.includes('@media') ? n : `@media screen and (min-width: ${n})` + ), + ] - for (const k in styles) { - const key = k as keyof typeof styles - let value = styles[key] - if (typeof value === 'function') { - value = value(theme || {}) - } + for (const k in styles) { + const key = k as keyof typeof styles + let value = styles[key] + if (typeof value === 'function') { + value = value(theme || {}) + } - if (value === false || value == null) { - continue - } - if (!Array.isArray(value)) { - next[key] = value - continue - } - for (let i = 0; i < value.slice(0, mediaQueries.length).length; i++) { - const media = mediaQueries[i] - if (!media) { - next[key] = value[i] + if (value === false || value == null) { + continue + } + if (!Array.isArray(value)) { + next[key] = value continue } - next[media] = next[media] || {} - if (value[i] == null) continue - ;(next[media] as Record)[key] = value[i] + for (let i = 0; i < value.slice(0, mediaQueries.length).length; i++) { + const media = mediaQueries[i] + if (!media) { + next[key] = value[i] + continue + } + next[media] = next[media] || {} + if (value[i] == null) continue + ;(next[media] as Record)[key] = value[i] + } } + return next } - return next -} type CssPropsArgument = { theme: Theme } | Theme -export const css = (args: ThemeUIStyleObject = {}) => ( - props: CssPropsArgument = {} -): CSSObject => { - const theme: Theme = { - ...defaultTheme, - ...('theme' in props ? props.theme : props), - } - // insert variant props before responsive styles, so they can be merged - // we need to maintain order of the style props, so if a variant is place in the middle - // of other props, it will extends its props at that same location order. +export const css = + (args: ThemeUIStyleObject = {}) => + (props: CssPropsArgument = {}): CSSObject => { + const theme: Theme = { + ...defaultTheme, + ...('theme' in props ? props.theme : props), + } + // insert variant props before responsive styles, so they can be merged + // we need to maintain order of the style props, so if a variant is place in the middle + // of other props, it will extends its props at that same location order. - const obj: CSSObject = getObjectWithVariants( - typeof args === 'function' ? args(theme) : args, - theme - ) + const obj: CSSObject = getObjectWithVariants( + typeof args === 'function' ? args(theme) : args, + theme + ) - const styles = responsive(obj as ThemeUICSSObject)(theme) - let result: CSSObject = {} - for (const key in styles) { - const x = styles[key as keyof typeof styles] - const val = typeof x === 'function' ? x(theme) : x + const styles = responsive(obj as ThemeUICSSObject)(theme) + let result: CSSObject = {} + for (const key in styles) { + const x = styles[key as keyof typeof styles] + const val = typeof x === 'function' ? x(theme) : x + + if (val && typeof val === 'object') { + if (hasDefault(val)) { + result[key] = val[THEME_UI_DEFAULT_KEY] + continue + } - if (val && typeof val === 'object') { - if (hasDefault(val)) { - result[key] = val[THEME_UI_DEFAULT_KEY] + // On type level, val can also be an array here, + // but we transform all arrays in `responsive` function. + result[key] = css(val as ThemeUICSSObject)(theme) continue } - // On type level, val can also be an array here, - // but we transform all arrays in `responsive` function. - result[key] = css(val as ThemeUICSSObject)(theme) - continue - } + const prop = key in aliases ? aliases[key as keyof Aliases] : key + const scaleName = + prop in scales ? scales[prop as keyof Scales] : undefined + const scale = scaleName ? theme?.[scaleName] : get(theme, prop, {}) + const transform = get(transforms, prop, get) + const value = transform(scale, val, val) - const prop = key in aliases ? aliases[key as keyof Aliases] : key - const scaleName = prop in scales ? scales[prop as keyof Scales] : undefined - const scale = scaleName ? theme?.[scaleName] : get(theme, prop, {}) - const transform = get(transforms, prop, get) - const value = transform(scale, val, val) + if (prop in multiples) { + const dirs = multiples[prop as keyof typeof multiples] - if (prop in multiples) { - const dirs = multiples[prop as keyof typeof multiples] - - for (let i = 0; i < dirs.length; i++) { - result[dirs[i]] = value + for (let i = 0; i < dirs.length; i++) { + result[dirs[i]] = value + } + } else { + result[prop] = value } - } else { - result[prop] = value } - } - return result -} + return result + } diff --git a/packages/docs/src/pages/sx-prop.mdx b/packages/docs/src/pages/sx-prop.mdx index 39a626a89..7e5250cbc 100644 --- a/packages/docs/src/pages/sx-prop.mdx +++ b/packages/docs/src/pages/sx-prop.mdx @@ -6,12 +6,15 @@ import { Note } from '..' # The `sx` Prop -The `sx` prop lets you style elements inline, using values from your theme. -To use the `sx` prop, add the custom `/** @jsxImportSource theme-ui */` pragma comment to the top of your module. +The `sx` prop lets you style elements inline, using values from your theme. To +use the `sx` prop, add the custom `/** @jsxImportSource theme-ui */` pragma +comment to the top of your module or configure +[automatic JSX runtime](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) +in your transpiler. -The `sx` prop lets you add any valid CSS to an element, -while using values from your theme to keep styles consistent. -You can think of the style object that the `sx` prop accepts as a _superset_ of CSS. +The `sx` prop lets you add any valid CSS to an element, while using values from +your theme to keep styles consistent. You can think of the style object that the +`sx` prop accepts as a _superset_ of CSS. ```jsx /** @jsxImportSource theme-ui */ @@ -33,16 +36,20 @@ export default (props) => ( -Under the hood, Theme UI uses a [custom pragma comment](/guides/jsx-pragma) that converts a theme-aware `sx` prop into a style object and passes it to Emotion's `jsx` functions. -The `sx` prop only works in modules that have defined a custom pragma at the top of the file, which replaces the default React `jsx` functions. -This means you can control which modules in your application opt into this feature without the need for a Babel plugin or additional configuration. -This is intended as a complete replacement for the Emotion custom JSX pragma. +Under the hood, Theme UI uses a [custom pragma comment](/guides/jsx-pragma) that +converts a theme-aware `sx` prop into a style object and passes it to Emotion's +`jsx` functions. The `sx` prop only works in modules that have defined a custom +pragma at the top of the file, which replaces the default React `jsx` functions. +This means you can control which modules in your application opt into this +feature without the need for a Babel plugin or additional configuration. This is +intended as a complete replacement for the Emotion custom JSX pragma. ## Theme-Aware Properties -The following CSS properties will use values defined in the theme, when available. +The following CSS properties will use values defined in the theme, when +available. | Property | Theme Key | | ------------------------- | ---------------- | @@ -138,7 +145,8 @@ The following CSS properties will use values defined in the theme, when availabl | `fill` | `colors` | | `stroke` | `colors` | -Theme UI also supports [CSS Logical Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties), +Theme UI also supports +[CSS Logical Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties), which follow this structure: - `margin-{block,inline}-{start,end}` @@ -209,8 +217,10 @@ which follow this structure: ## Responsive Values -Theme UI, like Styled System, includes a shorthand syntax for writing mobile-first responsive styles using arrays as values. -This is useful when you want to change a single property across multiple breakpoints without needing to write verbose media query syntax. +Theme UI, like Styled System, includes a shorthand syntax for writing +mobile-first responsive styles using arrays as values. This is useful when you +want to change a single property across multiple breakpoints without needing to +write verbose media query syntax. ```jsx /** @jsxImportSource theme-ui */ @@ -227,7 +237,8 @@ export default (props) => ( ### Skipping Breakpoints -If you want to skip a breakpoint, you can use the value `null`. This is useful if you want to set a value for only the largest breakpoint, for example. +If you want to skip a breakpoint, you can use the value `null`. This is useful +if you want to set a value for only the largest breakpoint, for example. ```jsx /** @jsxImportSource theme-ui */ @@ -244,7 +255,8 @@ export default (props) => ( ### Media Queries -If you prefer standard CSS media query syntax, you can use nested objects for responsive styles as well. +If you prefer standard CSS media query syntax, you can use nested objects for +responsive styles as well. ```jsx
( -If you’re looking to style text content like Markdown or from a CMS using your theme, -check out the [`BaseStyles`](/api#basestyles) component. +If you’re looking to style text content like Markdown or from a CMS using your +theme, check out the [`BaseStyles`](/api#basestyles) component. -If you want to reference the current class selector of the component, such as for +If you want to reference the current class selector of the component, such as +for [psuedo-classes & psuedo-elements](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors/Pseudo-classes_and_pseudo-elements), -you can use the [`&`](https://emotion.sh/docs/object-styles#child-selectors) symbol. +you can use the [`&`](https://emotion.sh/docs/object-styles#child-selectors) +symbol. ```jsx /** @jsx jsx */ @@ -350,7 +367,8 @@ export default (props) => ( ## Raw CSS -To opt-out of using theme-based CSS, use the `css` prop to render raw CSS values. +To opt-out of using theme-based CSS, use the `css` prop to render raw CSS +values. ```jsx /** @jsxImportSource theme-ui */ @@ -369,9 +387,10 @@ export default (props) => ( ## Using the `sx` Prop in MDX -Because MDX uses its own custom pragma and `createElement` function, the Theme UI pragma will not work in MDX files. -You can use any of the [Theme UI components](/components), -which support the `sx` prop, in an MDX file as a workaround. +Because MDX uses its own custom pragma and `createElement` function, the Theme +UI pragma will not work in MDX files. You can use any of the +[Theme UI components](/components), which support the `sx` prop, in an MDX file +as a workaround. ```mdx import { Box } from 'theme-ui' @@ -390,4 +409,5 @@ import { Box } from 'theme-ui' ## Object Styles -If you're new to using JavaScript object notation for authoring styles, see the [guide to using objects for styling](/guides/object-styles). +If you're new to using JavaScript object notation for authoring styles, see the +[guide to using objects for styling](/guides/object-styles).