diff --git a/.changeset/friendly-sloths-jam.md b/.changeset/friendly-sloths-jam.md new file mode 100644 index 000000000..a3bad3007 --- /dev/null +++ b/.changeset/friendly-sloths-jam.md @@ -0,0 +1,8 @@ +--- +'@compiled/babel-plugin': minor +'@compiled/webpack-app': minor +'@compiled/parcel-app': minor +'@compiled/react': minor +--- + +Implement the `cssMap` API to enable library users to dynamically choose a varied set of CSS rules. diff --git a/.loki/reference/chrome_laptop_css_map_Conditional_Styles.png b/.loki/reference/chrome_laptop_css_map_Conditional_Styles.png new file mode 100644 index 000000000..85163862e Binary files /dev/null and b/.loki/reference/chrome_laptop_css_map_Conditional_Styles.png differ diff --git a/.loki/reference/chrome_laptop_css_map_Dynamic_Variant.png b/.loki/reference/chrome_laptop_css_map_Dynamic_Variant.png new file mode 100644 index 000000000..d7102c156 Binary files /dev/null and b/.loki/reference/chrome_laptop_css_map_Dynamic_Variant.png differ diff --git a/.loki/reference/chrome_laptop_css_map_Merge_Styles.png b/.loki/reference/chrome_laptop_css_map_Merge_Styles.png new file mode 100644 index 000000000..630d0d864 Binary files /dev/null and b/.loki/reference/chrome_laptop_css_map_Merge_Styles.png differ diff --git a/.loki/reference/chrome_laptop_css_map_Variant_As_Prop.png b/.loki/reference/chrome_laptop_css_map_Variant_As_Prop.png new file mode 100644 index 000000000..34213399a Binary files /dev/null and b/.loki/reference/chrome_laptop_css_map_Variant_As_Prop.png differ diff --git a/examples/parcel/src/app.jsx b/examples/parcel/src/app.jsx index 518bfba66..64c80178e 100644 --- a/examples/parcel/src/app.jsx +++ b/examples/parcel/src/app.jsx @@ -9,6 +9,7 @@ import '@compiled/react'; import { primary } from './constants'; import Annotated from './ui/annotated'; +import CSSMap from './ui/css-map'; import { CustomFileExtensionStyled, customFileExtensionCss, @@ -29,5 +30,6 @@ export const App = () => ( Loading...}> + CSS Map ); diff --git a/examples/parcel/src/ui/css-map.jsx b/examples/parcel/src/ui/css-map.jsx new file mode 100644 index 000000000..1c7f3bbc1 --- /dev/null +++ b/examples/parcel/src/ui/css-map.jsx @@ -0,0 +1,12 @@ +import { css, cssMap } from '@compiled/react'; + +const styles = cssMap({ + danger: { + color: 'red', + }, + success: { + color: 'green', + }, +}); + +export default ({ variant, children }) =>
{children}
; diff --git a/examples/webpack/src/app.jsx b/examples/webpack/src/app.jsx index 3933d1664..9e40ec9ee 100644 --- a/examples/webpack/src/app.jsx +++ b/examples/webpack/src/app.jsx @@ -4,6 +4,7 @@ import { Suspense, lazy } from 'react'; import { primary } from './common/constants'; import Annotated from './ui/annotated'; +import CSSMap from './ui/css-map'; import { CustomFileExtensionStyled, customFileExtensionCss, @@ -23,5 +24,6 @@ export const App = () => ( Custom File Extension Styled
Custom File Extension CSS
+ CSS Map ); diff --git a/examples/webpack/src/ui/css-map.jsx b/examples/webpack/src/ui/css-map.jsx new file mode 100644 index 000000000..1c7f3bbc1 --- /dev/null +++ b/examples/webpack/src/ui/css-map.jsx @@ -0,0 +1,12 @@ +import { css, cssMap } from '@compiled/react'; + +const styles = cssMap({ + danger: { + color: 'red', + }, + success: { + color: 'green', + }, +}); + +export default ({ variant, children }) =>
{children}
; diff --git a/packages/babel-plugin/src/babel-plugin.ts b/packages/babel-plugin/src/babel-plugin.ts index 3e4635315..10d2e0a1c 100644 --- a/packages/babel-plugin/src/babel-plugin.ts +++ b/packages/babel-plugin/src/babel-plugin.ts @@ -8,6 +8,7 @@ import * as t from '@babel/types'; import { unique, preserveLeadingComments } from '@compiled/utils'; import { visitClassNamesPath } from './class-names'; +import { visitCssMapPath } from './css-map'; import { visitCssPropPath } from './css-prop'; import { visitStyledPath } from './styled'; import type { State } from './types'; @@ -20,6 +21,7 @@ import { isCompiledKeyframesTaggedTemplateExpression, isCompiledStyledCallExpression, isCompiledStyledTaggedTemplateExpression, + isCompiledCSSMapCallExpression, } from './utils/is-compiled'; import { normalizePropsUsage } from './utils/normalize-props-usage'; @@ -39,6 +41,7 @@ export default declare((api) => { inherits: jsxSyntax, pre() { this.sheets = {}; + this.cssMap = {}; let cache: Cache; if (this.opts.cache === true) { @@ -150,7 +153,7 @@ export default declare((api) => { return; } - (['styled', 'ClassNames', 'css', 'keyframes'] as const).forEach((apiName) => { + (['styled', 'ClassNames', 'css', 'keyframes', 'cssMap'] as const).forEach((apiName) => { if ( state.compiledImports && t.isIdentifier(specifier.node?.imported) && @@ -171,6 +174,11 @@ export default declare((api) => { path: NodePath | NodePath, state: State ) { + if (isCompiledCSSMapCallExpression(path.node, state)) { + visitCssMapPath(path, { context: 'root', state, parentPath: path }); + return; + } + const hasStyles = isCompiledCSSTaggedTemplateExpression(path.node, state) || isCompiledStyledTaggedTemplateExpression(path.node, state) || diff --git a/packages/babel-plugin/src/css-map/__tests__/index.test.ts b/packages/babel-plugin/src/css-map/__tests__/index.test.ts new file mode 100644 index 000000000..d93df96f9 --- /dev/null +++ b/packages/babel-plugin/src/css-map/__tests__/index.test.ts @@ -0,0 +1,135 @@ +import type { TransformOptions } from '../../test-utils'; +import { transform as transformCode } from '../../test-utils'; +import { ErrorMessages } from '../index'; + +describe('css map', () => { + const transform = (code: string, opts: TransformOptions = {}) => + transformCode(code, { pretty: false, ...opts }); + + const styles = `{ + danger: { + color: 'red', + backgroundColor: 'red' + }, + success: { + color: 'green', + backgroundColor: 'green' + } + }`; + + it('should transform css map', () => { + const actual = transform(` + import { cssMap } from '@compiled/react'; + + const styles = cssMap(${styles}); + `); + + expect(actual).toInclude( + 'const styles={danger:"_syaz5scu _bfhk5scu",success:"_syazbf54 _bfhkbf54"};' + ); + }); + + it('should error out if variants are not defined at the top-most scope of the module.', () => { + expect(() => { + transform(` + import { cssMap } from '@compiled/react'; + + const styles = { + map1: cssMap(${styles}), + } + `); + }).toThrow(ErrorMessages.DEFINE_MAP); + + expect(() => { + transform(` + import { cssMap } from '@compiled/react'; + + const styles = () => cssMap(${styles}) + `); + }).toThrow(ErrorMessages.DEFINE_MAP); + }); + + it('should error out if cssMap receives more than one argument', () => { + expect(() => { + transform(` + import { cssMap } from '@compiled/react'; + + const styles = cssMap(${styles}, ${styles}) + `); + }).toThrow(ErrorMessages.NUMBER_OF_ARGUMENT); + }); + + it('should error out if cssMap does not receive an object', () => { + expect(() => { + transform(` + import { cssMap } from '@compiled/react'; + + const styles = cssMap('color: red') + `); + }).toThrow(ErrorMessages.ARGUMENT_TYPE); + }); + + it('should error out if spread element is used', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + ...base + }); + `); + }).toThrow(ErrorMessages.NO_SPREAD_ELEMENT); + }); + + it('should error out if object method is used', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + danger() {} + }); + `); + }).toThrow(ErrorMessages.NO_OBJECT_METHOD); + }); + + it('should error out if variant object is dynamic', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + danger: otherStyles + }); + `); + }).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT); + }); + + it('should error out if styles include runtime variables', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + danger: { + color: canNotBeStaticallyEvulated + } + }); + `); + }).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT); + }); + + it('should error out if styles include conditional CSS', () => { + expect(() => { + transform(` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + danger: { + color: canNotBeStaticallyEvulated ? 'red' : 'blue' + } + }); + `); + }).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT); + }); +}); diff --git a/packages/babel-plugin/src/css-map/index.ts b/packages/babel-plugin/src/css-map/index.ts new file mode 100644 index 000000000..21857c7c4 --- /dev/null +++ b/packages/babel-plugin/src/css-map/index.ts @@ -0,0 +1,153 @@ +import type { NodePath } from '@babel/core'; +import * as t from '@babel/types'; + +import type { Metadata } from '../types'; +import { buildCodeFrameError } from '../utils/ast'; +import { buildCss } from '../utils/css-builders'; +import { transformCssItems } from '../utils/transform-css-items'; + +// The messages are exported for testing. +export enum ErrorMessages { + NO_TAGGED_TEMPLATE = 'cssMap function cannot be used as a tagged template expression.', + NUMBER_OF_ARGUMENT = 'cssMap function can only receive one argument.', + ARGUMENT_TYPE = 'cssMap function can only receive an object.', + DEFINE_MAP = 'CSS Map must be declared at the top-most scope of the module.', + NO_SPREAD_ELEMENT = 'Spread element is not supported in CSS Map.', + NO_OBJECT_METHOD = 'Object method is not supported in CSS Map.', + STATIC_VARIANT_OBJECT = 'The variant object must be statically defined.', +} + +const createErrorMessage = (message: string): string => { + return ` +${message} +To correctly implement a CSS Map, follow the syntax below: + +\`\`\` +import { css, cssMap } from '@compiled/react'; +const borderStyleMap = cssMap({ + none: { borderStyle: 'none' }, + solid: { borderStyle: 'solid' }, +}); +const Component = ({ borderStyle }) =>
+\`\`\` + `; +}; + +/** + * Takes `cssMap` function expression and then transforms it to a record of class names and sheets. + * + * For example: + * ``` + * const styles = cssMap({ + * none: { color: 'red' }, + * solid: { color: 'green' }, + * }); + * ``` + * gets transformed to + * ``` + * const styles = { + * danger: "_syaz5scu", + * success: "_syazbf54", + * }; + * ``` + * + * @param path {NodePath} The path to be evaluated. + * @param meta {Metadata} Useful metadata that can be used during the transformation + */ +export const visitCssMapPath = ( + path: NodePath | NodePath, + meta: Metadata +): void => { + // We don't support tagged template expressions. + if (t.isTaggedTemplateExpression(path.node)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.DEFINE_MAP), + path.node, + meta.parentPath + ); + } + + // We need to ensure CSS Map is declared at the top-most scope of the module. + if (!t.isVariableDeclarator(path.parent) || !t.isIdentifier(path.parent.id)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.DEFINE_MAP), + path.node, + meta.parentPath + ); + } + + // We need to ensure cssMap receives only one argument. + if (path.node.arguments.length !== 1) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.NUMBER_OF_ARGUMENT), + path.node, + meta.parentPath + ); + } + + // We need to ensure the argument is an objectExpression. + if (!t.isObjectExpression(path.node.arguments[0])) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.ARGUMENT_TYPE), + path.node, + meta.parentPath + ); + } + + const totalSheets: string[] = []; + path.replaceWith( + t.objectExpression( + path.node.arguments[0].properties.map((property) => { + if (t.isSpreadElement(property)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.NO_SPREAD_ELEMENT), + property.argument, + meta.parentPath + ); + } + + if (t.isObjectMethod(property)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.NO_OBJECT_METHOD), + property.key, + meta.parentPath + ); + } + + if (!t.isObjectExpression(property.value)) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT), + property.value, + meta.parentPath + ); + } + + const { css, variables } = buildCss(property.value, meta); + + if (variables.length) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT), + property.value, + meta.parentPath + ); + } + + const { sheets, classNames } = transformCssItems(css, meta); + totalSheets.push(...sheets); + + if (classNames.length !== 1) { + throw buildCodeFrameError( + createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT), + property, + meta.parentPath + ); + } + + return t.objectProperty(property.key, classNames[0]); + }) + ) + ); + + // We store sheets in the meta state so that we can use it later to generate Compiled component. + meta.state.cssMap[path.parent.id.name] = totalSheets; +}; diff --git a/packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts b/packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts new file mode 100644 index 000000000..638a62d12 --- /dev/null +++ b/packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts @@ -0,0 +1,85 @@ +import type { TransformOptions } from '../../test-utils'; +import { transform as transformCode } from '../../test-utils'; + +describe('css map behaviour', () => { + beforeAll(() => { + process.env.AUTOPREFIXER = 'off'; + }); + + afterAll(() => { + delete process.env.AUTOPREFIXER; + }); + + const transform = (code: string, opts: TransformOptions = {}) => + transformCode(code, { pretty: false, ...opts }); + + const styles = ` + import { css, cssMap } from '@compiled/react'; + + const styles = cssMap({ + danger: { + color: 'red', + backgroundColor: 'red' + }, + success: { + color: 'green', + backgroundColor: 'green' + } + }); + `; + + it('should evaluate css map with various syntactic patterns', () => { + const actual = transform( + ` + ${styles} +
; + `, + { pretty: true } + ); + + expect(actual).toMatchInlineSnapshot(` + "import * as React from "react"; + import { ax, ix, CC, CS } from "@compiled/react/runtime"; + const _5 = "._syaz13q2{color:blue}"; + const _4 = "._bfhkbf54{background-color:green}"; + const _3 = "._syazbf54{color:green}"; + const _2 = "._bfhk5scu{background-color:red}"; + const _ = "._syaz5scu{color:red}"; + const styles = { + danger: "_syaz5scu _bfhk5scu", + success: "_syazbf54 _bfhkbf54", + }; + + {[_, _2, _3, _4, _5]} + { +
+ } + ; + " + `); + }); +}); diff --git a/packages/babel-plugin/src/types.ts b/packages/babel-plugin/src/types.ts index 0a6b2a95a..a796da317 100644 --- a/packages/babel-plugin/src/types.ts +++ b/packages/babel-plugin/src/types.ts @@ -98,6 +98,7 @@ export interface State extends PluginPass { css?: string; keyframes?: string; styled?: string; + cssMap?: string; }; importedCompiledImports?: { @@ -141,6 +142,11 @@ export interface State extends PluginPass { * Files that have been included in this pass. */ includedFiles: string[]; + + /** + * Holds a record of currently evaluated CSS Map and its sheets in the module. + */ + cssMap: Record; } interface CommonMetadata { diff --git a/packages/babel-plugin/src/utils/css-builders.ts b/packages/babel-plugin/src/utils/css-builders.ts index 062f6d7e5..0691ffe79 100644 --- a/packages/babel-plugin/src/utils/css-builders.ts +++ b/packages/babel-plugin/src/utils/css-builders.ts @@ -28,9 +28,33 @@ import type { CssItem, LogicalCssItem, SheetCssItem, + CssMapItem, PartialBindingWithMeta, } from './types'; +/** + * Retrieves the leftmost identity from a given expression. + * + * For example: + * Given a member expression "colors.primary.500", the function will return "colors". + * + * @param expression The expression to be evaluated. + * @returns {string} The leftmost identity in the expression. + */ +const findBindingIdentifier = ( + expression: t.Expression | t.V8IntrinsicIdentifier +): t.Identifier | undefined => { + if (t.isIdentifier(expression)) { + return expression; + } else if (t.isCallExpression(expression)) { + return findBindingIdentifier(expression.callee); + } else if (t.isMemberExpression(expression)) { + return findBindingIdentifier(expression.object); + } + + return undefined; +}; + /** * Will normalize the value of a `content` CSS property to ensure it has quotations around it. * This is done to replicate both how Styled Components behaves, @@ -804,6 +828,13 @@ export const buildCss = (node: t.Expression | t.Expression[], meta: Metadata): C } if (t.isMemberExpression(node)) { + const bindingIdentifier = findBindingIdentifier(node); + if (bindingIdentifier && meta.state.cssMap[bindingIdentifier.name]) { + return { + css: [{ type: 'map', expression: node, name: bindingIdentifier.name, css: '' }], + variables: [], + }; + } const { value, meta: updatedMeta } = evaluateExpression(node, meta); return buildCss(value, updatedMeta); } @@ -877,6 +908,13 @@ export const buildCss = (node: t.Expression | t.Expression[], meta: Metadata): C }; } + if (item.type === 'map') { + return { + ...item, + expression: t.logicalExpression(node.operator, expression, item.expression), + } as CssMapItem; + } + const logicalItem: LogicalCssItem = { type: 'logical', css: getItemCss(item), diff --git a/packages/babel-plugin/src/utils/is-compiled.ts b/packages/babel-plugin/src/utils/is-compiled.ts index 65a7ecf34..ddc022018 100644 --- a/packages/babel-plugin/src/utils/is-compiled.ts +++ b/packages/babel-plugin/src/utils/is-compiled.ts @@ -47,6 +47,21 @@ export const isCompiledKeyframesCallExpression = ( t.isIdentifier(node.callee) && node.callee.name === state.compiledImports?.keyframes; +/** + * Returns `true` if the node is using `cssMap` from `@compiled/react` as a call expression + * + * @param node {t.Node} The node that is being checked + * @param state {State} Plugin state + * @returns {boolean} Whether the node is a compiled cssMap + */ +export const isCompiledCSSMapCallExpression = ( + node: t.Node, + state: State +): node is t.CallExpression => + t.isCallExpression(node) && + t.isIdentifier(node.callee) && + node.callee.name === state.compiledImports?.cssMap; + /** * Returns `true` if the node is using `keyframes` from `@compiled/react` as a tagged template expression * diff --git a/packages/babel-plugin/src/utils/transform-css-items.ts b/packages/babel-plugin/src/utils/transform-css-items.ts index 02a1cc131..196a3e199 100644 --- a/packages/babel-plugin/src/utils/transform-css-items.ts +++ b/packages/babel-plugin/src/utils/transform-css-items.ts @@ -74,6 +74,12 @@ const transformCssItem = ( ), }; + case 'map': + return { + sheets: meta.state.cssMap[item.name], + classExpression: item.expression, + }; + default: const css = transformCss(getItemCss(item), meta.state.opts); const className = compressClassNamesForRuntime( diff --git a/packages/babel-plugin/src/utils/types.ts b/packages/babel-plugin/src/utils/types.ts index 0bbe4e1ef..f753012f3 100644 --- a/packages/babel-plugin/src/utils/types.ts +++ b/packages/babel-plugin/src/utils/types.ts @@ -27,7 +27,20 @@ export interface SheetCssItem { css: string; } -export type CssItem = UnconditionalCssItem | ConditionalCssItem | LogicalCssItem | SheetCssItem; +export interface CssMapItem { + name: string; + type: 'map'; + expression: t.Expression; + // We can remove this once we local transform other CssItem types + css: string; +} + +export type CssItem = + | UnconditionalCssItem + | ConditionalCssItem + | LogicalCssItem + | SheetCssItem + | CssMapItem; export type Variable = { name: string; diff --git a/packages/react/src/css-map/__tests__/index.test.tsx b/packages/react/src/css-map/__tests__/index.test.tsx new file mode 100644 index 000000000..34e4cd48c --- /dev/null +++ b/packages/react/src/css-map/__tests__/index.test.tsx @@ -0,0 +1,52 @@ +/** @jsxImportSource @compiled/react */ +// eslint-disable-next-line import/no-extraneous-dependencies +import { css, cssMap } from '@compiled/react'; +import { render } from '@testing-library/react'; + +describe('css map', () => { + const styles = cssMap({ + danger: { + color: 'red', + }, + success: { + color: 'green', + }, + }); + + it('should generate css based on the selected variant', () => { + const Foo = ({ variant }: { variant: keyof typeof styles }) => ( +
hello world
+ ); + const { getByText, rerender } = render(); + + expect(getByText('hello world')).toHaveCompiledCss('color', 'red'); + + rerender(); + expect(getByText('hello world')).toHaveCompiledCss('color', 'green'); + }); + + it('should statically access a variant', () => { + const Foo = () =>
hello world
; + const { getByText } = render(); + + expect(getByText('hello world')).toHaveCompiledCss('color', 'red'); + }); + + it('should merge styles', () => { + const hover = css({ ':hover': { color: 'red' } }); + const Foo = () =>
hello world
; + const { getByText } = render(); + + expect(getByText('hello world')).toHaveCompiledCss('color', 'green'); + expect(getByText('hello world')).toHaveCompiledCss('color', 'red', { target: ':hover' }); + }); + + it('should conditionally apply variant', () => { + const Foo = ({ isDanger }: { isDanger: boolean }) => ( +
hello world
+ ); + const { getByText } = render(); + + expect(getByText('hello world')).toHaveCompiledCss('color', 'red'); + }); +}); diff --git a/packages/react/src/css-map/index.js.flow b/packages/react/src/css-map/index.js.flow new file mode 100644 index 000000000..3569d3776 --- /dev/null +++ b/packages/react/src/css-map/index.js.flow @@ -0,0 +1,25 @@ +/** + * Flowtype definitions for index + * Generated by Flowgen from a Typescript Definition + * Flowgen v1.20.1 + * @flow + */ +import type { CSSProps, CssObject } from '../types'; +/** + * ## cssMap + * + * Creates a collection of named CSS rules that are statically typed and useable with other Compiled APIs. + * For further details [read the documentation](https://compiledcssinjs.com/docs/api-cssmap). + * @example ``` + * const borderStyleMap = cssMap({ + * none: { borderStyle: 'none' }, + * solid: { borderStyle: 'solid' }, + * }); + * const Component = ({ borderStyle }) =>
+ * + * + * ``` + */ +declare export default function cssMap(_styles: { + [key: T]: CssObject | CssObject[], +}): { [key: T]: CSSProps }; diff --git a/packages/react/src/css-map/index.ts b/packages/react/src/css-map/index.ts new file mode 100644 index 000000000..357e41457 --- /dev/null +++ b/packages/react/src/css-map/index.ts @@ -0,0 +1,25 @@ +import type { CSSProps, CssObject } from '../types'; +import { createSetupError } from '../utils/error'; + +/** + * ## cssMap + * + * Creates a collection of named CSS rules that are statically typed and useable with other Compiled APIs. + * For further details [read the documentation](https://compiledcssinjs.com/docs/api-cssmap). + * + * @example + * ``` + * const borderStyleMap = cssMap({ + * none: { borderStyle: 'none' }, + * solid: { borderStyle: 'solid' }, + * }); + * const Component = ({ borderStyle }) =>
+ * + * + * ``` + */ +export default function cssMap( + _styles: Record | CssObject[]> +): Record> { + throw createSetupError(); +} diff --git a/packages/react/src/index.js.flow b/packages/react/src/index.js.flow index d6183f0b2..1937810a7 100644 --- a/packages/react/src/index.js.flow +++ b/packages/react/src/index.js.flow @@ -10,3 +10,4 @@ declare export { keyframes } from './keyframes'; declare export { styled } from './styled'; declare export { ClassNames } from './class-names'; declare export { default as css } from './css'; +declare export { default as cssMap } from './css-map'; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 7c038675f..8b114db02 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -9,6 +9,7 @@ export { keyframes } from './keyframes'; export { styled } from './styled'; export { ClassNames } from './class-names'; export { default as css } from './css'; +export { default as cssMap } from './css-map'; // Pass through the (classic) jsx runtime. // Compiled currently doesn't define its own and uses this purely to enable a local jsx namespace. diff --git a/stories/css-map.tsx b/stories/css-map.tsx new file mode 100644 index 000000000..8559d1f11 --- /dev/null +++ b/stories/css-map.tsx @@ -0,0 +1,62 @@ +import { cssMap } from '@compiled/react'; +import { useState } from 'react'; + +export default { + title: 'css map', +}; + +const styles = cssMap({ + success: { + color: 'green', + ':hover': { + color: 'DarkGreen', + }, + '@media (max-width: 800px)': { + color: 'SpringGreen', + }, + }, + danger: { + color: 'red', + ':hover': { + color: 'DarkRed', + }, + '@media (max-width: 800px)': { + color: 'Crimson', + }, + }, +}); + +export const DynamicVariant = (): JSX.Element => { + const [variant, setVariant] = useState('success'); + + return ( + <> +
*': { + margin: '5px', + }, + }}> + + +
hello world
+
+ + ); +}; + +export const VariantAsProp = (): JSX.Element => { + const Component = ({ variant }: { variant: keyof typeof styles }) => ( +
hello world
+ ); + return ; +}; + +export const MergeStyles = (): JSX.Element => { + return
hello world
; +}; + +export const ConditionalStyles = (): JSX.Element => { + const isDanger = true; + return
hello world
; +};