Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: recipe extension #1414

Closed
wants to merge 9 commits into from
Closed

feat: recipe extension #1414

wants to merge 9 commits into from

Conversation

astahmer
Copy link
Collaborator

@astahmer astahmer commented Sep 25, 2023

Add support for Config Recipe (and Slot) extensions

You can now extend config Recipes and config Slots Recipes to easily compose them together.

Added

New defineRecipeConfigs identity function that takes an object of RecipeConfig objects (or the new RecipeBuilder
interface !) and returns the same object with less strict typing. This can be useful to help Typescript checking
performance when using a large number of recipes or when using complex recipes, with a large number of variants and/or a
large number of styles.

Changed

Config Recipe

The defineRecipe method will now return a RecipeBuilder object instead of a RecipeConfig object. The
RecipeBuilder object has the following methods:

  • extend: add additional variants to or override variants of a recipe.
const button = defineRecipe({
  className: 'btn',
  variants: {
    variant: { primary: { color: 'red' } },
  },
}).config.extend({
  variant: {
    primary: { px: 2 },
    secondary: { color: 'blue' },
  },
})

resulting in:

{
  "className": "btn",
  "variants": {
    "variant": {
      "primary": { "color": "red", "px": 2 },
      "secondary": { "color": "blue" }
    }
  }
}
  • merge: deep merge a recipe with another recipe. It takes a partial RecipeConfig object as an argument, which can
    include new (or existing) variants, compound variants, and default variants.
const button = defineRecipe({
  className: 'btn',
  variants: {
    variant: { primary: { color: 'red' } },
  },
}).config.merge({
  className: 'custom-btn',
  variants: {
    secondary: { color: 'blue' },
  },
  defaultVariants: {
    variant: 'secondary',
  },
})

resulting in:

{
  "className": "custom-btn",
  "variants": {
    "variant": {
      "primary": { "color": "red" },
      "secondary": { "color": "blue" }
    }
  },
  "defaultVariants": {
    "variant": "secondary"
  }
}
  • pick: pick only specified variants from a recipe. It takes a list of variant keys as arguments and returns a new
    RecipeBuilder object with only the specified variants. This will also filter out any compound variants that include
    any of the omitted variants.
const button = defineRecipe({
  className: 'btn',
  variants: {
    variant: { primary: { color: 'red' } },
    size: { small: { px: 2 }, large: { px: 4 } },
  },
}).config.pick('size')

resulting in:

{
  "className": "btn",
  "variants": {
    "variant": {
      "size": {
        "small": { "px": 2 },
        "large": { "px": 4 }
      }
    }
  }
}
  • omit: omit specified variants from a recipe. It takes a list of variant keys as arguments and returns a new
    RecipeBuilder object without the specified variants. This will also filter out any compound variants that include
    any of the omitted variants.
const button = defineRecipe({
  className: 'btn',
  variants: {
    variant: { primary: { color: 'red' } },
    size: { small: { px: 2 }, large: { px: 4 } },
  },
}).config.omit('size')

resulting in:

{
  "className": "btn",
  "variants": {
    "variant": {
      "primary": { "color": "red" }
    }
  }
}
  • cast: make the recipe generic to simplify the typings. It returns a new RecipeConfig object with the final
    computed variants, without the RecipeBuilder methods.

Each of these methods return a new RecipeBuilder object, so they can be chained together.

Config Slot Recipe

The defineSlotRecipe method will now return a SlotRecipeBuilder object instead of a SlotRecipeConfig object. The
SlotRecipeBuilder object has the same following methods as the RecipeBuilder object: extend, merge, pick, and
omit.

In addition, the SlotRecipeBuilder object has an object property called slots that is a SlotRecipeBuilder, which
has the following methods:

  • add: add additional slots to a slot recipe. It takes a list of slot names as arguments and returns a new
    SlotRecipeBuilder object with the updated slots.
const card = defineSlotRecipe({
  className: 'card',
  slots: ['root', 'input', 'icon'],
  variants: {
    variant: {
      subtle: { root: { color: 'blue.100' } },
      solid: { root: { color: 'blue.100' } },
    },
    size: {
      sm: { root: { fontSize: 'sm' } },
      md: { root: { fontSize: 'md' } },
    },
  },
}).config.slots.add('label')

resulting in:

{
  "className": "card",
  "slots": ["root", "input", "icon", "label"],
  "variants": {
    "variant": {
      "subtle": { "root": { "color": "blue.100" } },
      "solid": { "root": { "color": "blue.100" } }
    },
    "size": {
      "sm": { "root": { "fontSize": "sm" } },
      "md": { "root": { "fontSize": "md" } }
    }
  }
}
  • pick: pick only specified slots from a slot recipe. It takes a list of slot keys as arguments and returns a new
    SlotRecipeBuilder object with only the specified slots. This will also filter out any styles defined in a slot that
    is not picked, as well as any compound variants that include any of the omitted slots.
const card = defineSlotRecipe({
  className: 'card',
  slots: ['root', 'input', 'icon'],
  variants: {
    variant: {
      subtle: { root: { color: 'blue.100' } },
      solid: { input: { color: 'blue.100' } },
    },
    size: {
      sm: { root: { fontSize: 'sm' } },
      md: { input: { fontSize: 'md' } },
    },
  },
}).config.slots.pick('input')

resulting in:

{
  "className": "card",
  "slots": ["input"],
  "variants": {
    "variant": {
      "solid": { "input": { "color": "blue.100" } }
    },
    "size": {
      "md": { "input": { "fontSize": "md" } }
    }
  }
}
  • omit: omit specified slots from a slot recipe. It takes a list of slot keys as arguments and returns a new
    SlotRecipeBuilder object without the specified slots. This will also filter out any styles defined in a slot that is
    not picked, as well as any compound variants that include any of the omitted slots.
const card = defineSlotRecipe({
  className: 'card',
  slots: ['root', 'input', 'icon'],
  variants: {
    variant: {
      subtle: { root: { color: 'blue.100' } },
      solid: { input: { color: 'blue.100' } },
    },
    size: {
      sm: { root: { fontSize: 'sm' } },
      md: { input: { fontSize: 'md' } },
    },
  },
}).config.slots.omit('input')

resulting in:

{
  "className": "card",
  "slots": ["root", "icon"],
  "variants": {
    "variant": {
      "subtle": { "root": { "color": "blue.100" } }
    },
    "size": {
      "sm": { "root": { "fontSize": "sm" } }
    }
  }
}
  • assignTo: assign a simple (without slots) recipe to a slot of the current slot recipe. It takes a slot name and a
    recipe config as arguments and returns a new SlotRecipeBuilder object with the updated slot recipe. If a slot name
    already has styles defined in a matching (both defined in the simple recipe to assign from and the current slot recipe
    being assigned to) variant, the styles will be merged with the existing slot recipe, with priority given to the styles
    defined in the simple recipe to assign from.
const button = defineRecipe({
  className: 'btn',
  variants: {
    variant: {
      outline: { color: 'green.100' },
      empty: { border: 'none' },
    },
    size: {
      lg: { fontSize: 'xl', h: '10' },
    },
  },
})

const card = defineSlotRecipe({
  className: 'card',
  slots: ['root', 'input', 'icon'],
  variants: {
    variant: {
      subtle: { root: { color: 'blue.100' } },
      solid: { input: { color: 'blue.100' } },
      outline: { input: { mx: 2 } },
      empty: { input: {} },
    },
    size: {
      sm: { root: { fontSize: 'sm' } },
      md: { input: { fontSize: 'md' } },
    },
  },
}).config.slots.assignTo('input', button)

resulting in:

{
  "className": "card",
  "slots": ["root", "input", "icon"],
  "variants": {
    "variant": {
      "subtle": { "root": { "color": "blue.100" } },
      "solid": { "input": { "color": "blue.100" } },
      "outline": { "input": { "mx": 2, "color": "green.100" } },
      "empty": { "input": {} }
    },
    "size": {
      "sm": { "root": { "fontSize": "sm" } },
      "md": { "input": { "fontSize": "md" } }
    }
  }
}

@vercel
Copy link

vercel bot commented Sep 25, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
panda-docs ❌ Failed (Inspect) Sep 25, 2023 11:51am
panda-playground ✅ Ready (Inspect) Visit Preview Sep 25, 2023 11:51am
panda-studio ✅ Ready (Inspect) Visit Preview Sep 25, 2023 11:51am

@changeset-bot
Copy link

changeset-bot bot commented Sep 25, 2023

🦋 Changeset detected

Latest commit: 8623ca4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 25 packages
Name Type
@pandacss/generator Patch
@pandacss/studio Patch
@pandacss/types Patch
@pandacss/dev Patch
@pandacss/node Patch
@pandacss/parser Patch
@pandacss/config Patch
@pandacss/core Patch
@pandacss/fixture Patch
@pandacss/preset-atlaskit Patch
@pandacss/preset-base Patch
@pandacss/preset-panda Patch
@pandacss/token-dictionary Patch
@pandacss/language-server Patch
panda-css-vscode Patch
@pandacss/postcss Patch
@pandacss/astro Patch
@pandacss/error Patch
@pandacss/extractor Patch
@pandacss/is-valid-prop Patch
@pandacss/logger Patch
@pandacss/shared Patch
@pandacss/extension-shared Patch
@pandacss/ts-plugin Patch
@pandacss/vsix-builder Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

so it can be used in outdir/types/global.d.ts
@anubra266
Copy link
Collaborator

Looks awesome. Could we make the changelog rich. i.e. live examples for all the usecases, and add it to the docs too.

test: expectTypeOf slots.assignTo
fix: defineSlotRecipe assignTo fallback typings
@segunadebayo
Copy link
Member

This PR and feature have grown stale over time. We still need more requests for this to be in the core library.

I'm closing this for now to keep the API surface as is. Another thing we can do is to ship this as a standalone library and see how folks adopt it.

Let's go ahead and revisit this in the future.

@apatrida
Copy link

apatrida commented Mar 8, 2024

That's a lot of work on the contribution to let slip away. Is this even possible as a standalone library, there are plugin interfaces and a way to make this happen?

We could use this as well, since we constantly have to cx(someRecipe, css({...something additional...})) to specialize for various uses. It's uglier than doing an extension to the recipe / slot recipe and then using that more cleanly.

@astahmer
Copy link
Collaborator Author

astahmer commented Mar 8, 2024

That's a lot of work on the contribution to let slip away. Is this even possible as a standalone library, there are plugin interfaces and a way to make this happen?

We could use this as well, since we constantly have to cx(someRecipe, css({...something additional...})) to specialize for various uses. It's uglier than doing an extension to the recipe / slot recipe and then using that more cleanly.

done & published here https://github.com/astahmer/pandabox/tree/main/packages/define-recipe

tho it sounds like you'd prefer using cva instead of config recipes

@apatrida
Copy link

apatrida commented Mar 8, 2024

Would be a cleaner PR if it didn't modify 70+ files if it wasn't necessary (was there a file rename in here?)

@apatrida
Copy link

apatrida commented Mar 8, 2024

That's a lot of work on the contribution to let slip away. Is this even possible as a standalone library, there are plugin interfaces and a way to make this happen?
We could use this as well, since we constantly have to cx(someRecipe, css({...something additional...})) to specialize for various uses. It's uglier than doing an extension to the recipe / slot recipe and then using that more cleanly.

done & published here astahmer/pandabox@main/packages/define-recipe

tho it sounds like you'd prefer using cva instead of config recipes

wow, cool. yes, cva ... we define them, for example to have Kobalte based modals with slot recipes that can be applied, but then we keep seeing small CSS tweaks for each use on top of the cva and the cx(cvaFunc().slotWhatever, css({ something: value, otherThing: value })) noise scattered around is less clean.

@apatrida
Copy link

apatrida commented Mar 8, 2024

It's basically a need to do small patches to different parts of the cva in a clean way.

const derivedCva = someCva.patchWithAFewThings{ ... patch ... )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants