# WindRecipe A `tv()` (tailwind-variants) equivalent for Wind. Composes a className from a `base`, a set of `variants` (one axis per key, each mapping a value to a className), optional `compoundVariants` (multi-condition rules), and `defaultVariants` (the selection applied when an axis is not passed by the caller). `WindSlotRecipe` extends the same model to multi-part components: instead of a single string, each call returns a `Map` of slot-name to className. ## Table of Contents - [Basic Usage](#basic-usage) - [Props: WindRecipe](#props-windrecipe) - [Props: WindSlotRecipe](#props-windslotrecipe) - [Constructor: WindRecipe](#constructor-windrecipe) - [Constructor: WindSlotRecipe](#constructor-windslotrecipe) - [Resolution Order](#resolution-order) - [Same-Granularity Override Contract](#same-granularity-override-contract) - [Styling Examples](#styling-examples) - [Related Documentation](#related-documentation) ## Basic Usage ```dart final button = WindRecipe( base: 'inline-flex items-center rounded-lg font-medium', variants: { 'intent': { 'primary': 'bg-blue-600 dark:bg-blue-500 text-white', 'ghost': 'bg-transparent text-blue-600 dark:text-blue-400', }, 'size': { 'sm': 'px-3 py-1.5 text-sm', 'md': 'px-4 py-2 text-base', }, }, defaultVariants: {'intent': 'primary', 'size': 'md'}, ); // Full defaults: "inline-flex items-center rounded-lg font-medium bg-blue-600 dark:bg-blue-500 text-white px-4 py-2 text-base" button() // Override intent, keep size default: button(variants: {'intent': 'ghost'}) // Append extra classes: button(className: 'w-full') ``` ## Props: WindRecipe | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `base` | `String` | `''` | Always-applied className. | | `variants` | `Map>` | `{}` | Variant axes: axis -> (value -> className). Iteration order fixes emission order. | | `compoundVariants` | `List` | `[]` | Multi-condition rules applied in list order after variant classes. | | `defaultVariants` | `Map` | `{}` | Default selection per axis. Overridden by the `variants:` argument at call site; a `null` value clears the default. | **Call signature** (`WindRecipe` is callable): | Param | Type | Default | Description | |:------|:-----|:--------|:------------| | `variants` | `Map?` | `null` | Caller's axis selections. `null` for an axis clears its default (no class emitted). Unknown axis throws `ArgumentError`. | | `className` | `String?` | `null` | Extra caller className appended last. | ## Props: WindSlotRecipe | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `slots` | `Map` | `{}` | Base className per slot. Slot identity comes from these keys. | | `variants` | `Map>>` | `{}` | Variant axes: axis -> (value -> (slot -> className)). | | `compoundVariants` | `List` | `[]` | Multi-condition rules applied per slot in list order. | | `defaultVariants` | `Map` | `{}` | Default selection per axis. | **Call signature** (`WindSlotRecipe` is callable): | Param | Type | Default | Description | |:------|:-----|:--------|:------------| | `variants` | `Map?` | `null` | Caller's axis selections. Unknown axis throws `ArgumentError`. | | `classNames` | `Map?` | `null` | Extra caller className per slot, appended last for that slot. | ## Constructor: WindRecipe ```dart WindRecipe({ String base = '', Map> variants = const {}, List compoundVariants = const [], Map defaultVariants = const {}, }) ``` `WindCompoundVariant` constructor: ```dart WindCompoundVariant({ required Map conditions, required String className, }) ``` A condition value is a `String` (exact match) or `List` (OR match). ## Constructor: WindSlotRecipe ```dart WindSlotRecipe({ Map slots = const {}, Map>> variants = const {}, List compoundVariants = const [], Map defaultVariants = const {}, }) ``` `WindSlotCompoundVariant` constructor: ```dart WindSlotCompoundVariant({ required Map conditions, required Map classNames, }) ``` ## Resolution Order Every call resolves in this strict order: no dedupe, no sort, no twMerge: ```text base ++ variant-classes(definition order) ++ matched-compound(array order) ++ caller ``` The caller's `className` / `classNames` always arrive last, so they override earlier tokens via Wind's per-family last-wins rule. A `null` value in `variants:` clears the default for that axis (no class emitted for it), matching `tv()`'s `undefined`-clears semantics. ### The Caller-Append Contract (No twMerge) `WindRecipe` and `WindSlotRecipe` only APPEND the caller's `className` / `classNames`; they never dedupe, sort, or resolve a conflict themselves. A caller passing a className that conflicts with the base (e.g. `WindRecipe(base: 'w-1/2')(className: 'w-full')`) gets both tokens in the emitted string, in that order: `'w-1/2 w-full'`. The conflict is resolved one layer down, when the string reaches `WindParser`: `WindParser.findAndGroupClasses` groups same-family classes together WITHOUT deduping, and each parser (e.g. `SizingParser.parse`) resolves them with last-class-wins. So the appended `w-full` wins over the base `w-1/2` at parse time, not at recipe-call time. There is intentionally no Dart port of `tailwind-merge`: wind's parse-time per-family last-wins already achieves the same outcome for every token family the parsers understand, which is why `WindRecipe` deliberately only appends (see the no-twMerge rationale in the `WindRecipe` doc comment in `lib/src/recipe/wind_recipe.dart`). This is verified by `test/recipe/wind_recipe_test.dart` (proves the append, order preserved) and `test/parser/parsers/sizing_parser_test.dart` (proves the appended `w-full` resolves to the winning style). Note: the className-REPLACE anti-pattern (a component treating an incoming `className` as a full recipe bypass instead of appending it) is a consumer-component bug, not a wind defect; wind's own recipes always append. When it appears, it lives in a downstream consumer's component layer, outside this package. ## Same-Granularity Override Contract Because Wind's parser resolves conflicts per-family with last-class-wins at parse time, a variant must override a base token at the **same granularity**. Mixing a shorthand and a longhand of the same family silently keeps the shorthand: ```dart // Correct: override px-* with px-* WindRecipe( base: 'px-4', variants: {'size': {'sm': 'px-2'}}, // overrides px-4 correctly ) // Incorrect: base p-* vs variant px-* (same family, different granularity) WindRecipe( base: 'p-4', variants: {'size': {'sm': 'px-2'}}, // px-2 does NOT override p-4; p-4 wins silently ) ``` In debug builds, `WindRecipe` logs a warning the first time it detects a shorthand/longhand mix. ## Styling Examples ### Button with Compound Variants Compound variants contribute a className only when every condition is satisfied: ```dart final button = WindRecipe( base: 'inline-flex items-center rounded-lg font-medium', variants: { 'intent': { 'primary': 'bg-blue-600 dark:bg-blue-500 text-white', 'destructive': 'bg-red-600 dark:bg-red-500 text-white', }, 'size': {'sm': 'px-3 py-1.5 text-sm', 'lg': 'px-6 py-3 text-lg'}, }, compoundVariants: [ WindCompoundVariant( // Activates when intent is 'primary' OR 'destructive', AND size is 'lg'. conditions: {'intent': ['primary', 'destructive'], 'size': 'lg'}, className: 'shadow-lg', ), ], defaultVariants: {'intent': 'primary', 'size': 'sm'}, ); ``` ### Multi-Slot Card Recipe ```dart final card = WindSlotRecipe( slots: { 'root': 'rounded-xl border overflow-hidden', 'header': 'px-5 py-4 border-b', 'body': 'px-5 py-4', }, variants: { 'tone': { 'default': { 'root': 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700', 'header': 'border-gray-200 dark:border-gray-700', 'body': '', }, 'highlighted': { 'root': 'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800', 'header': 'border-blue-200 dark:border-blue-800', 'body': '', }, }, }, defaultVariants: {'tone': 'default'}, ); final classes = card(variants: {'tone': 'highlighted'}); WCard( className: classes['root'], header: WDiv(className: classes['header'], child: WText('Summary')), child: WDiv(className: classes['body'], child: WText('Body')), ) ``` ## Related Documentation - [WBadge](../widgets/w-badge.md): a natural fit for a `WindRecipe` badge tone axis. - [WCard](../widgets/w-card.md): pairs with `WindSlotRecipe` for multi-slot card variants. - [WButton](../widgets/w-button.md): commonly driven by a `WindRecipe` for intent + size axes.