From db4ebc282f8d1ac1a486793eb0b8d79d3d1c94ae Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Mon, 10 Mar 2025 12:34:30 +0100 Subject: [PATCH] Improve README. --- CHANGELOG.md | 6 + README.md | 551 ++++++++++++++++++++-------------------------- package-lock.json | 4 +- package.json | 2 +- 4 files changed, 248 insertions(+), 315 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0b6ad0..e90b096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v1.5.2 (2024-03-10) + +### Changed + +- Improve README. + ## v1.5.1 (2024-03-13) ### Fixed diff --git a/README.md b/README.md index d226946..a64e13f 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,14 @@ # Build-variants -Declare and compose styles variants with ease. +Declaratively build style objects based on your React component props with a clean, type-safe API. -## Motivation +--- -CSS-in-JS (Javascript that applies the styles of your components at runtime) is a very powerful technique but is generally mixed into your components logic and is sometimes very combersome. +## Introduction -Defining variants (the different variations of styles) of your components is also a touchy approach that requires a lot of attention to be simple, flexible and meaningful for the developers that will use them. +**Build-variants** helps you organize and compose CSS (or any style values) based on component props. It separates styling logic from component logic, making your code easier to maintain and extend. Note that it is a *builder*—it doesn’t apply styles by itself but returns an object your CSS-in-JS library can use. -`Build-variants` offers a clean, declarative, and type-safe API to organize the different variants of your components allowing better maintainability and flexibility in time. - -## Prerequisites - -Typescript is not mandatory but highly recommended. Build-variants leverages a lot on Typescript generics and inference to provide types checking at every level. +--- ## Installation @@ -20,303 +16,232 @@ Typescript is not mandatory but highly recommended. Build-variants leverages a l npm install @productive-codebases/build-variants ``` -## Core concepts - -At its core, `build-variants` is a tool that is building an object which can be styles or anything else according to the values of an object. - -Applied to web development, `build-variants` can be used to build styles object from components props. - -`build-variants` is **not** a CSS-in-JS library and don't manage styles. It is only **a builder** for your styles library. - -`Build-variants` is totally agnostic and has no dependencies on a specific web framework or styles library. It can be use to build styles, animations or objects of a totally different context. It's a versatile tool! - -The API of `build-variants` exposes functions to add CSS (or `values` if we are in a different context), variant(s) and compound variant(s) which are variant(s) composed from existing ones. - -`Build-variants` introduces the concept of private and public variants, a way to differentiate the public interfaces from the intrinsic characteristics of your components. - -Both are exposed as standard props so developers can use public variants for the "official" use-cases but can still compose or override a defined behavior by using private variants. - -All those concepts provide the best maintainability and flexibility in the implementation of your variants. +--- ## Usage -### Create your build-variants factory function +### 1. Setup Your Factory Function -As `build-variants` is totally agnostic, you have to specify which interface you want to use through `build-variants`. - -In the context of styles/CSS, you may use the interface exposed by the library of the framework you are using to decorate your components. - -Example with `styled-components`: +Configure **build-variants** with your styling engine. For example, with _styled-components_: ```ts +import type { CSSObject } from '@emotion/react' import { newBuildVariants } from 'build-variants' -import { CSSObject } from 'styled-components' -/** - * Return configured newBuildVariants with CSSObject from styled-components. - */ export function buildVariants(props: TProps) { return newBuildVariants(props) } ``` -[See CodeSandbox example.](https://codesandbox.io/s/1-init-b5t24e?file=/src/buildVariants.ts) +*This sets up a function that accepts props and returns a builder configured for styled-components.* -### Decorate a component +--- -To decorate your component, use your own factory `buildVariants()` function when you are defining the styles of your component. +### 2. Decorate a Component -:warning: Please note that the library that you are using to apply styles needs to support object notation, as `build-variants` will return an object. +Integrate the builder with any styled function that accepts a CSSObject-like object. Whether you're using Emotion, styled-components, MUI, or any other library, the generated style object will work seamlessly. ```tsx -import styled from 'styled-components' +import styled from '@emotion/styled' +// Alternatively: +// import styled from 'styled-components' +// or import { styled } from '@mui/material', etc. import { buildVariants } from './buildVariants' -const Div = styled.div(props => { - return buildVariants(props).end() -}) +const Div = styled.div(props => buildVariants(props).end()) export default function Button() { return
My Button
} ``` -[See CodeSandbox example.](https://codesandbox.io/s/1-init-b5t24e?file=/src/Button.tsx) +*In this example, no extra styles are added; the builder returns an empty style object.* + +--- -### Add some CSS +### 3. Adding CSS Blocks -To add some CSS, proceed as it: +Chain CSS blocks to add styles: ```ts const Div = styled.div(props => { - return ( - buildVariants(props) - .css({ - display: 'inline-block', - padding: '10px' - }) - // you can add as many blocks as you want - .css({ - background: 'blue', - color: 'white' - }) - .end() - ) + return buildVariants(props) + .css({ + display: 'inline-block', + padding: '10px' + }) + .css({ + background: 'blue', + color: 'white' + }) + .end() }) ``` -[See CodeSandbox example.](https://codesandbox.io/s/2-add-css-0zmimn?file=/src/Button.tsx) +**Applied styles:** +- `display: inline-block` +- `padding: 10px` +- `background: blue` +- `color: white` -:arrow_right: If you are using `build-variants` in a different context than styles, you may prefer using the `value()` alias of `css()`. +--- -:arrow_right: See also how `build-variants` can be used for [global styles](https://codesandbox.io/s/add-css-0zmimn?file=/src/App.tsx) as well. +### 4. Declaring Variants -### Declare variants +#### Simple Variant -#### Simple variant - -A variant is a characteristic of your component, for example the "type" of a button which could be "primary" or "secondary" and needs to be declared as a union of strings. - -`Build-variants` will ensure that all values of the union are declared as a property of the object used to describe the styles of the variant. - -At runtime, `build-variants` will return the styles that match the current props value. - -Example: +Define a style variant based on a prop value: ```tsx -import styled from 'styled-components' +import styled from '@emotion/styled' import { buildVariants } from './buildVariants' interface IButtonProps { type: 'primary' | 'secondary' } -// Note how IButtonProps is passed here as a generic to type the props argument. -// This construction may be different according to the styles library you are using. const Div = styled.div(props => { - return ( - buildVariants(props) - .css({ - display: 'inline-block', - padding: '10px' - }) - // The first argument is the name of the prop and is used as a label inside - // build-variants. It's used when building compound variants. - - // The second argument is the value of the variant used to "select" the correct - // styles definition. You may use props directly here. - - // The third argument is your styles definition. - .variant('type', props.type, { - primary: { - background: 'blue', - color: 'white' - }, - secondary: { - background: 'silver', - color: 'black' - } - }) - // The end() function means the end of your build declaration. It triggers the whole styles build by deeply merging the differents styles values and return the final object - .end() - ) + return buildVariants(props) + .css({ + display: 'inline-block', + padding: '10px' + }) + .variant('type', props.type, { + primary: { + background: 'blue', + color: 'white' + }, + secondary: { + background: 'silver', + color: 'black' + } + }) + .end() }) export default function Button(props: IButtonProps) { - // `build-variants` needs to know the value of the `type` to apply the correct style. - // Thanks to the `IButtonProps` passed to the styled function, the type is now required - // and Typescript will ensure that all props are passed (if not declared as optional props). return
My Button
} ``` -[See CodeSandbox example.](https://codesandbox.io/s/3-add-variant-9b3bvh?file=/src/Button.tsx) +**Applied styles:** -#### Multiple variants +- **When `type="primary"`:** + - Common: `display: inline-block`, `padding: 10px` + - Variant: `background: blue`, `color: white` -A variant can be multiple meaning that different values of a same variant can be applied at the same time. It's pretty useful for a font that can be strong and green for example... +- **When `type="secondary"`:** + - Common: `display: inline-block`, `padding: 10px` + - Variant: `background: silver`, `color: black` -Consider this example: +--- + +#### Multiple Variants + +Allow multiple variant values (e.g., text styles): ```tsx interface IButtonProps { type: 'primary' | 'secondary' - // Declare an array of union values text?: Array<'strong' | 'success' | 'error'> } const Div = styled.div(props => { - return ( - buildVariants(props) - .css({ - display: 'inline-block', - padding: '10px' - }) - .variant('type', props.type, { - primary: { - background: 'blue', - color: 'white' - }, - secondary: { - background: 'silver', - color: 'black' - } - }) - // The variants() function is working exactly the same than variant(), expect that it requires an array of values. - // Build-variants will apply the styles definition of each value of the array. - .variants('text', props.text, { - strong: { - fontWeight: 'bold' - }, - success: { - color: 'green' - }, - error: { - color: 'red' - } - }) - .end() - ) + return buildVariants(props) + .css({ + display: 'inline-block', + padding: '10px' + }) + .variant('type', props.type, { + primary: { + background: 'blue', + color: 'white' + }, + secondary: { + background: 'silver', + color: 'black' + } + }) + .variants('text', props.text, { + strong: { fontWeight: 'bold' }, + success: { color: 'green' }, + error: { color: 'red' } + }) + .end() }) ``` +Usage example: + ```tsx -// Here an example to render a primary button with bolded red text +// Renders a primary button with both bold and red text styles @@ -325,17 +250,30 @@ Usage: ``` -[See Codesandox example.](https://codesandbox.io/s/5-variants-composition-m6b5zs?file=/src/Button.tsx) +**Applied styles:** + +- **Primary:** + - Private `_background: primary` → `background: blue` + - Private `_text: ['light']` → `color: white` -Note that `compoundVariants()` is also available and allows to apply multiple compound variants. +- **Secondary:** + - Private `_background: secondary` → `background: silver` + - Private `_text: ['dark']` → `color: black` -### Use private variants to override public ones +- **Success:** + - Private `_background: success` → `background: #eaff96` + - Private `_text: ['success']` → `color: green` -As mentioned in the previous section, private variants (props starting with an underscore) can be used to override public variants definitions, allowing to keep a maximum of flexibility. +- **Error:** + - Private `_background: error` → `background: #ffdbdb` + - Private `_text: ['error', 'strong']` → `color: red` and `fontWeight: bold` + - Additional style: `border: 1px solid red` -For example, if a very specific use-case is not covered by the Button public variant, it is possible to override a (private) behavior of our component. +--- -Example: +### 5. Overriding with Private Variants + +Private variants have a higher precedence than public ones, allowing you to override the default behavior for specific use cases. ```tsx @@ -345,66 +283,52 @@ Example: ``` -[See CodeSandbox example.](https://codesandbox.io/s/overrides-with-private-variants-w72ed1?file=/src/App.tsx) - -The more granular your variants, the more flexible your component API. By having a few style definitions applied for each private variant, you can compose your different public variants more precisely while offering the maximum flexibility in the use of your component. - -### Condition blocks +**Applied styles:** +- The first button applies the default compound variant for `error`. +- The second button overrides the `_background` variant to `"success"`, so it receives `background: #eaff96` (as defined in the success mapping) while keeping the other error-related styles. -If you want to disable an entire block (`css()`, `variant()`...), you can use the `if()` function. +--- -The main advantage is that you can condition a whole block easily without modifying the rest of your variants composition. +### 6. Conditional Blocks -Example: +Enable or skip blocks of styles based on a condition: ```tsx const Div = styled.div(props => { - return ( - buildVariants(props) - // ... - // Deactivate the _text variant according to the applyTextVariant prop - .if(props.applyTextVariant === true, builder_ => { - return builder_ - .variants('_text', props._text, { - dark: { - color: 'black' - }, - light: { - color: 'white' - }, - success: { - color: 'green' - }, - error: { - color: 'red' - }, - strong: { - fontWeight: 'bold' - } - }) - .end() - }) - .compoundVariant('type', props.type, { - // ... - }) - .end() - ) -}) -``` - -In this example, only the styles of the text is applyed according to the `applyTextVariant` prop, the rest (padding, background, border) is still applied. + return buildVariants(props) + // Other style blocks… + .if(props.applyTextVariant === true, builder_ => { + return builder_ + .variants('_text', props._text, { + dark: { color: 'black' }, + light: { color: 'white' }, + success: { color: 'green' }, + error: { color: 'red' }, + strong: { fontWeight: 'bold' } + }) + .end() + }) -:warning: Be careful to use the `builder_` instance returned by the `if` function. + // Alternatively, if you only need to add simple CSS: + // .if(props.applyTextVariant === true, { + // color: 'red' + // }) -[See CodeSandbox example.](https://codesandbox.io/s/7-condition-blocks-0xko7x?file=/src/Button.tsx) + .compoundVariant('type', props.type, { + // … + }) + .end() +}) +``` -### Blocks weight +**Applied styles:** +- If `applyTextVariant` is true, the text-related styles are applied. Otherwise, they are skipped. -Blocks are applied in the order of the declaration meaning that a `color` defined in a first `css` block would be overridden by a `color` applied lastly in a variant or compound variant definition. +--- -`Build-variants` offers a way to add a weight to each block so that you can force some style directives to be applied in a defined order regardless of its declaration position. +### 7. Blocks Weight -Example: +Control the order of style application by assigning a weight to each block: ```ts const Div = styled.div(props => { @@ -414,73 +338,76 @@ const Div = styled.div(props => { padding: '10px' }) .css( - { - color: 'silver' - }, - { - // `color: silver` applied lastly thanks to its weight (0 by default), - // so the final color of the button text will be `silver`. - weight: 10 - } + { color: 'silver' }, + { weight: 10 } // This block is applied later. ) .variants('_text', props._text, { - dark: { - color: 'black' - } - // ... + dark: { color: 'black' }, + // … }) .end() }) ``` -The `weight` option is available for css, variant(s) and compoundVariant(s). - -[See CodeSandbox example.](https://codesandbox.io/s/8-blocks-weight-d0fbz3?file=/src/Button.tsx) +**Applied styles:** +- The `color: silver` style with weight 10 overrides any earlier conflicting `color` from `_text` if applied later. -### Debugging +--- -On complex components, you may encounter issues to understand which styles are really applied by `build-variants`. A `debug()` function is available to log `build-variants` internals and final applied styles. +### 8. Debugging -Example: +Log internal builder state to help diagnose complex style applications: ```tsx const Div = styled.div(props => { - return ( - buildVariants(props) - // ... - - // Enable console debugging - .debug() - .end() - ) + return buildVariants(props) + // Other style definitions… + .debug() + .end() }) ``` -Note that the `debug` function accepts an optional predicate (boolean) value to enable or disable debugging. It is particularly useful to limit the logs to a specific use case. - -Example: +Or enable debugging conditionally: ```tsx interface IButtonProps { - // ... debug?: boolean } const Div = styled.div(props => { - return ( - buildVariants(props) - // ... - - // Enable console debugging only if the debug props has been passed to the component - .debug(props.debug === true) - .end() - ) + return buildVariants(props) + // Other style definitions… + .debug(props.debug === true) + .end() }) ``` +**Result:** Detailed logs in the console show which styles are applied and the builder's internal state. + +--- + +## Examples + +- https://codesandbox.io/s/1-init-b5t24e?file=/src/buildVariants.ts +- https://codesandbox.io/s/1-init-b5t24e?file=/src/Button.tsx +- https://codesandbox.io/s/2-add-css-0zmimn?file=/src/Button.tsx +- https://codesandbox.io/s/3-add-variant-9b3bvh?file=/src/Button.tsx +- https://codesandbox.io/s/4-multiple-variants-v9bxds?file=/src/Button.tsx +- https://codesandbox.io/s/5-variants-composition-m6b5zs?file=/src/Button.tsx +- https://codesandbox.io/s/overrides-with-private-variants-w72ed1?file=/src/App.tsx +- https://codesandbox.io/s/7-condition-blocks-0xko7x?file=/src/Button.tsx +- https://codesandbox.io/s/8-blocks-weight-d0fbz3?file=/src/Button.tsx +- https://codesandbox.io/s/9-debug-f6ozbu?file=/src/Button.tsx:463-2386 + +--- -Now in the browser console, you get some logs that shoud help to understand which styles are applied: +## Summary -![build-variants debugging](https://user-images.githubusercontent.com/446128/209950582-d7a7faf0-a496-4e3e-8469-d298ac09016b.png) +Build-variants empowers you to: +- **Declare and compose style variants** with a clean, declarative, and type-safe API. +- **Separate styling logic** from component code. +- **Support multiple, compound, and conditional variants** for flexible component design. +- **Control style precedence** with block weights. +- **Debug** style composition effortlessly. -[See CodeSandbox example.](https://codesandbox.io/s/9-debug-f6ozbu?file=/src/Button.tsx:463-2386) +Enjoy building maintainable, flexible UI components with Build-variants! diff --git a/package-lock.json b/package-lock.json index d4e3ce5..2ce72c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@productive-codebases/build-variants", - "version": "1.5.1", + "version": "1.5.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@productive-codebases/build-variants", - "version": "1.5.1", + "version": "1.5.2", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.3", diff --git a/package.json b/package.json index 97b3b21..6c85be0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@productive-codebases/build-variants", - "version": "1.5.1", + "version": "1.5.2", "description": "Declare and compose styles variants with ease.", "author": "Alexis MINEAUD", "license": "MIT",