TheMightyPenguin / dessert-box

An utility to create a Box component from your vanilla-extract + sprinkles tokens.
MIT License
343 stars 20 forks source link

Support variant styles #3

Closed alexandernanberg closed 2 years ago

alexandernanberg commented 3 years ago

From what I can tell dessert-box doesn't support variant styles yet, and I think it would be a great addition.

The API I'm looking for is something like

// Button
<Button variant="primary">Call to action</Button>
<Button variant="tertiary">Cancel</Button>

// Text
<Text variant="h1">Lorem ipsum dolor</Text>
<Text variant="h3">Lorem ipsum dolor sit amet</Text>

The variant prop would need to be able to set multiple style properties, e.g. setting variant=primary could change fontSize, backgroundColor etc

graup commented 3 years ago

Yeah, this would be nice. Maybe

// Box.styles.ts
import { styleVariants } from '@vanilla-extract/css';
variants = styleVariants(...);

// Box.ts
export const Box = createBox({
  atoms,
  variants
});

Similarly, I'd like to add extra styles. For example I could make a "Box" called Button that always includes some base styles. This way dessert-box would become similar to styled components but for Vanilla-extract/Sprinkles. Basically we'd extend the concept from "A single Box of all atom props" to "Boxes of composed atoms, variants, and styles".

Example code: https://github.com/graup/nf-new/blob/4fd97b7ef418787409704e95bd2522fbf84bdb04/src/foundation/Button/Button.tsx

This code seems like it could be replaced by createBox({ atoms, variants, classNames })

TheMightyPenguin commented 3 years ago

@alexandernanberg @graup awesome, got it! Thank you so much for the feedback and sorry for the delay here. I'll hack something together in the weekend and notify here with some proposals or once it's released, but I like the idea of the proposed API @graup, thanks!

TheMightyPenguin commented 3 years ago

Oops didn't mean to close.

TheMightyPenguin commented 3 years ago

@graup I have something working with this API:

tsx

const colorStyles = createAtomicStyles({
  conditions: {
    lightMode: { '@media': '(prefers-color-scheme: light)' },
    darkMode: { '@media': '(prefers-color-scheme: dark)' }
  },
  defaultCondition: false,
  properties: {
    backgroundColor: { green: 'green', red: 'red' }
  }
});

const space = {
  none: '0',
  small: '4px',
  medium: '8px',
  large: '16px',
  extraLarge: '32px'
};

const layoutStyles = createAtomicStyles({
  conditions: {
    mobile: {},
    tablet: { '@media': 'screen and (min-width: 768px)' },
    desktop: { '@media': 'screen and (min-width: 1024px)' }
  },
  defaultCondition: 'mobile',
  properties: {
    padding: space,
  },
  shorthands: {
  }
});

const atoms = createAtomsFn(layoutStyles);

const Button = createBox({
  atoms,
  variants: {
    type: {
      success: { backgroundColor: 'green' },
      warning: { backgroundColor: 'red' },
    },
    size: {
      small: {
        padding: { mobile: 'small', desktop: 'medium' }
      },
      large: {
        padding: { mobile: 'medium', desktop: 'large' }
      }
    }
  }
})

<Button type="success" size="small" />

The idea is that variants can be defined as a group of box props with a certain values, and you'll be able to pass multiple variants (e.g. type and size in the example above). This will be completely typed, so autocompletion will be there.

What do you think of this API?

graup commented 3 years ago

@TheMightyPenguin I like this, seems to match Sprinkle's design better than what I suggested with styleVariants.

tmm commented 3 years ago

This looks great!

@TheMightyPenguin I'm up for testing this if you create a release.

TheMightyPenguin commented 3 years ago

Awesome, @graup @tmm thanks for the feedback, expect a new release over the upcoming weekend 🙌

tmm commented 3 years ago

@TheMightyPenguin any luck? I'm so excited for this.

TheMightyPenguin commented 3 years ago

Hi all, sorry for the long delay, but here's the long awaited Variant API!

It's quite different from what we discussed here in this issue, but it goes more aligned with the vision for this library. I spend some time thinking what this library meant and what's the vision I had initially.

This library is meant to be used to create a Box component, and then to use that Box component as a low-level utility to consume design tokens, meaning it could be used once to create that Box, and then use that Box component to compose and build other components across your project. With this in mind, this is the API I ended up with:

  1. Create your Box, which can be used across your whole project to build UIs using your design tokens and also to create more components on top of it, notice how the dessert-box API now also returns createVariants:
    
    // Box.tsx
    import { createBox } from 'dessert-box';
    import { atoms } from './sprinkles.css';

// notice we export the createVariants function const { Box, createVariants } = createBox({ atoms });

export default Box


2. Use your Box together with `createVariants` to implement new components, like a Text one:

// Text.tsx import { Box, createVariants } from "./Box";

const variants = createVariants({ h1: { fontSize: "extraLarge", fontWeight: "600", }, h2: { // any design token from your sprinkles atoms can be used here fontSize: "large", fontWeight: "400", }, p: { fontSize: { // Including conditional properties. All of this autocompletes with TS desktop: "medium", mobile: "large", }, }, });

type Props = { variant: keyof typeof variants; children: React.ReactNode; };

const Text = ({ variant, children }: Props) => { return <Box {...variants[variant]}>{children}; };

export default Text;



You can check the docs here: https://github.com/TheMightyPenguin/dessert-box#variants

Please let me know how it works for you and any feedback, I hope this also works for your use cases! If there's some friction moving to this API or any questions let me know.

CC @tmm @graup @alexandernanberg 
tmm commented 3 years ago

@TheMightyPenguin looks great!

Might be worth noting in the docs that you can apply multiple variants since it's not immediately obvious. Something like this:

// Button.tsx
import { Box, createVariants } from "./Box"

const variants = createVariants({
  primary: {
    background: 'blue',
  },
  secondary: {
    background: 'gray',
  },
})

const sizes = createVariants({
  md: {
    fontSize: 'large',
  },
  lg: {
    fontSize: 'extraLarge',
  },
})

type Props = {
  children: React.ReactNode;
  size?: keyof typeof sizes
  variant?: keyof typeof variants
  // More props...
}

export const Button = ({
  children,
  size = 'md',
  variant = 'secondary',
  // More props...
}: Props) => {
  return (
    <Box as="button" {...sizes[size]} {...variants[variant]}>
      {children}
    </Box>
  )
}

When trying it out, the as prop on Box was creating a type error (see below) when I spread any of the createVariants values (i.e. {...variants[variant]}):

Type '{ children: ReactNode; background?: ConditionalStyle<Values<MapLeafNodes<{ black: string; blue: string; current: string; transparent: string; white: string; }, CSSVarFunction>, { ...; }>>; ... 50 more ...; as: "button"; }' is not assignable to type 'Omit<AllHTMLAttributes<HTMLElement>, "as" | "width" | "height">'.
  Types of property 'color' are incompatible.
    Type 'ConditionalStyle<Values<MapLeafNodes<{ black: string; blue: string; current: string; transparent: string; white: string; }, CSSVarFunction>, { defaultClass: string; conditions: { ...; }; }>>' is not assignable to type 'string'.
      Type '{ sm?: "transparent" | "black" | "blue" | "current" | "white"; md?: "transparent" | "black" | "blue" | "current" | "white"; lg?: "transparent" | "black" | "blue" | "current" | "white"; xl?: "transparent" | ... 3 more ... | "white"; }' is not assignable to type 'string'.ts(2322)
TheMightyPenguin commented 3 years ago

@tmm great idea, added that into the example in the docs!

Thanks for pointing this out, working on fixing this type error.

tmm commented 3 years ago

@TheMightyPenguin fix for this: https://github.com/TheMightyPenguin/dessert-box/issues/3#issuecomment-907887884

Filtered out the color prop from React.AllHTMLAttributes

TheMightyPenguin commented 3 years ago

@tmm released 0.3.3, thank you so much for the contribution! (Btw, are you using this at https://mirror.xyz? fan of that product!)

tmm commented 3 years ago

@TheMightyPenguin np!

Maybe at some point. Looking into vanilla-extract more generally, but lack of simple responsive variants is sort of a blocker.

TheMightyPenguin commented 3 years ago

@tmm what variant API would help you to use this? I'm up for a meeting sometime as well btw, I love working on Design Systems (working at one at work at the moment and it uses dessert-box to build our foundational Box component), so I can share some lessons learned and we can talk about your use case!

TheMightyPenguin commented 3 years ago

@tmm @graup @alexandernanberg so as @vanilla-extract/recipes was released, that does provide a great API for variants when using vanilla extract. So with that in mind the createVariants API was removed, and instead I documented how to use this library with recipes:

https://github.com/TheMightyPenguin/dessert-box#variants

I'll be documenting more patterns, and common use cases in the README!

TheMightyPenguin commented 2 years ago

Closing as @vanilla-extract/recipes solves this use case!