Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/card-part-borders.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
Add per-part borders with adjacency collapsing to Card recipe

- Add independent `borderTop` and `borderBottom` to each card part (header, body, footer) with compound variants per color×variant
- Add `:has(+ .card-*)` collapse selectors to header and body recipes to remove duplicate borders between adjacent parts
- Add `:first-child` / `:last-child` collapse selectors to header and footer recipes to remove duplicate borders at card edges
- Extend `createUseRecipe` with an optional `setup` callback for registering selectors alongside recipes
- Add `color` and `variant` props to `CardBody` component
45 changes: 17 additions & 28 deletions apps/docs/content/docs/06.components/02.composables/05.card.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The Card recipes integrate directly with the default [design tokens preset](/doc

The Card recipe helps you:

- **Ship faster with sensible defaults**: Get 3 colors, 4 visual styles, and 3 sizes out of the box with a single set of composable calls.
- **Ship faster with sensible defaults**: Get 3 colors, 3 visual styles, and 3 sizes out of the box with a single set of composable calls.
- **Compose structured layouts**: Four coordinated recipes (container, header, body, footer) share the same variant axes, so your cards stay internally consistent.
- **Maintain consistency**: Compound variants ensure every color-variant combination follows the same design rules, including separator colors and dark mode overrides.
- **Customize without forking**: Override base styles, default variants, or filter out options you don't need — all through the options API.
Expand Down Expand Up @@ -75,7 +75,7 @@ import { card, cardHeader, cardBody, cardFooter } from "virtual:styleframe";

interface CardProps {
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle";
variant?: "solid" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
title?: string;
description?: string;
Expand Down Expand Up @@ -126,7 +126,7 @@ const {
size = "md",
} = defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle";
variant?: "solid" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
}>();
</script>
Expand Down Expand Up @@ -194,7 +194,7 @@ height: 600

## Variants

Four visual style variants control how the card is rendered. Each variant is combined with the selected color through [compound variants](/docs/api/recipes#compound-variants), so you always get the correct background, text, border, and separator colors for your chosen color.
Three visual style variants control how the card is rendered. Each variant is combined with the selected color through [compound variants](/docs/api/recipes#compound-variants), so you always get the correct background, text, border, and separator colors for your chosen color.

### Solid

Expand All @@ -207,17 +207,6 @@ panel: true
---
::

### Outline

Transparent background with a colored border. Useful for secondary content groups that shouldn't dominate the visual hierarchy.

::story-preview
---
story: theme-recipes-card--outline
panel: true
---
::

### Soft

Light tinted background with no visible border. A gentle, borderless style that works well for grouped content in dense layouts.
Expand All @@ -231,7 +220,7 @@ panel: true

### Subtle

Light tinted background with a matching border. Combines the softness of the `soft` variant with the definition of `outline`.
Light tinted background with a matching border. Combines the softness of the `soft` variant with added visual definition from a border.

::story-preview
---
Expand Down Expand Up @@ -334,15 +323,15 @@ const card = useCardRecipe(s, {
},
defaultVariants: {
color: 'neutral',
variant: 'outline',
variant: 'subtle',
size: 'lg',
},
});

const cardHeader = useCardHeaderRecipe(s, {
defaultVariants: {
color: 'neutral',
variant: 'outline',
variant: 'subtle',
size: 'lg',
},
});
Expand All @@ -356,7 +345,7 @@ const cardBody = useCardBodyRecipe(s, {
const cardFooter = useCardFooterRecipe(s, {
defaultVariants: {
color: 'neutral',
variant: 'outline',
variant: 'subtle',
size: 'lg',
},
});
Expand All @@ -374,11 +363,11 @@ import { useCardRecipe } from '@styleframe/theme';

const s = styleframe();

// Only generate neutral color with solid and outline styles
// Only generate neutral color with solid and subtle styles
const card = useCardRecipe(s, {
filter: {
color: ['neutral'],
variant: ['solid', 'outline'],
variant: ['solid', 'subtle'],
},
});

Expand Down Expand Up @@ -412,7 +401,7 @@ Creates the card container recipe with background, border, border radius, and sh
| Variant | Options | Default |
|---------|---------|---------|
| `color` | `light`, `dark`, `neutral` | `neutral` |
| `variant` | `solid`, `outline`, `soft`, `subtle` | `solid` |
| `variant` | `solid`, `soft`, `subtle` | `solid` |
| `size` | `sm`, `md`, `lg` | `md` |

### `useCardHeaderRecipe(s, options?)`
Expand All @@ -424,7 +413,7 @@ Creates the card header recipe with a bottom separator border. Accepts the same
| Variant | Options | Default |
|---------|---------|---------|
| `color` | `light`, `dark`, `neutral` | `neutral` |
| `variant` | `solid`, `outline`, `soft`, `subtle` | `solid` |
| `variant` | `solid`, `soft`, `subtle` | `solid` |
| `size` | `sm`, `md`, `lg` | `md` |

### `useCardBodyRecipe(s, options?)`
Expand All @@ -436,7 +425,7 @@ Creates the card body recipe for the main content area. Accepts the same paramet
| Variant | Options | Default |
|---------|---------|---------|
| `color` | `light`, `dark`, `neutral` | `neutral` |
| `variant` | `solid`, `outline`, `soft`, `subtle` | `solid` |
| `variant` | `solid`, `soft`, `subtle` | `solid` |
| `size` | `sm`, `md`, `lg` | `md` |

### `useCardFooterRecipe(s, options?)`
Expand All @@ -448,7 +437,7 @@ Creates the card footer recipe with a top separator border. Accepts the same par
| Variant | Options | Default |
|---------|---------|---------|
| `color` | `light`, `dark`, `neutral` | `neutral` |
| `variant` | `solid`, `outline`, `soft`, `subtle` | `solid` |
| `variant` | `solid`, `soft`, `subtle` | `solid` |
| `size` | `sm`, `md`, `lg` | `md` |

[Learn more about recipes &rarr;](/docs/api/recipes)
Expand All @@ -458,7 +447,7 @@ Creates the card footer recipe with a top separator border. Accepts the same par
- **Pass `color` and `variant` consistently**: The container, header, and footer all need the same `color` and `variant` values so that separator borders match the card's visual style.
- **Pass `size` to each sub-recipe**: The card container controls the border radius, but each section (header, body, footer) manages its own padding and gap based on the `size` prop.
- **Use `neutral` for general-purpose cards**: The neutral color adapts to light and dark mode automatically, making it the safest default.
- **Prefer `solid` or `outline` for primary content**: Reserve `soft` and `subtle` for secondary or nested cards to create visual hierarchy.
- **Prefer `solid` for primary content**: Reserve `soft` and `subtle` for secondary or nested cards to create visual hierarchy.
- **Don't use all sections if you don't need them**: A card with only a body is perfectly valid. Add headers and footers only when your content has distinct sections.
- **Filter what you don't need**: If your component only uses one color, pass a `filter` option to reduce generated CSS.
- **Override defaults at the recipe level**: Set your most common variant combination as `defaultVariants` so component consumers write less code.
Expand Down Expand Up @@ -488,13 +477,13 @@ Both use a light tinted background. The difference is that `subtle` also adds a
:::

:::accordion-item{label="How do compound variants work in the Card recipe?" icon="i-lucide-circle-help"}
The Card recipe uses compound variants to map each color-variant combination to specific styles. For example, when `color` is `neutral` and `variant` is `solid`, the compound variant applies `background: @color.white`, `color: @color.text`, and `borderColor: @color.gray-200`, along with dark mode overrides. The header and footer recipes use compound variants to set separator border colors that match the card's visual style. This approach keeps the individual `color` and `variant` definitions clean while handling all 12 combinations (3 colors &times; 4 variants) automatically.
The Card recipe uses compound variants to map each color-variant combination to specific styles. For example, when `color` is `neutral` and `variant` is `solid`, the compound variant applies `background: @color.white`, `color: @color.text`, and `borderColor: @color.gray-200`, along with dark mode overrides. The header and footer recipes use compound variants to set separator border colors that match the card's visual style. This approach keeps the individual `color` and `variant` definitions clean while handling all 9 combinations (3 colors &times; 3 variants) automatically.

[Learn more about compound variants &rarr;](/docs/api/recipes#compound-variants)
:::

:::accordion-item{label="How does filtering affect compound variants?" icon="i-lucide-circle-help"}
When you use the `filter` option, compound variants that reference filtered-out values are automatically removed. For example, if you filter `variant` to only `['solid', 'outline']`, all compound variants matching `soft` or `subtle` are excluded from the generated output. Default variants are also adjusted if they reference a removed value.
When you use the `filter` option, compound variants that reference filtered-out values are automatically removed. For example, if you filter `variant` to only `['solid', 'subtle']`, all compound variants matching `soft` are excluded from the generated output. Default variants are also adjusted if they reference a removed value.
:::

:::accordion-item{label="Can I use the Card recipe without the design tokens preset?" icon="i-lucide-circle-help"}
Expand Down
2 changes: 1 addition & 1 deletion apps/storybook/src/components/components/card/Card.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { card } from "virtual:styleframe";
const props = withDefaults(
defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle";
variant?: "solid" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
}>(),
{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cardBody } from "virtual:styleframe";
const props = withDefaults(
defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle";
variant?: "solid" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
}>(),
{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cardFooter } from "virtual:styleframe";
const props = withDefaults(
defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle";
variant?: "solid" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
}>(),
{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cardHeader } from "virtual:styleframe";
const props = withDefaults(
defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle";
variant?: "solid" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
}>(),
{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CardTitle from "../CardTitle.vue";
import CardDescription from "../CardDescription.vue";

const colors = ["neutral", "light", "dark"] as const;
const variants = ["solid", "outline", "soft", "subtle"] as const;
const variants = ["solid", "soft", "subtle"] as const;
</script>

<template>
Expand Down
8 changes: 1 addition & 7 deletions apps/storybook/stories/components/card.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import CardGrid from "@/components/components/card/preview/CardGrid.vue";
import CardSizeGrid from "@/components/components/card/preview/CardSizeGrid.vue";

const colors = ["neutral", "light", "dark"] as const;
const variants = ["solid", "outline", "soft", "subtle"] as const;
const variants = ["solid", "soft", "subtle"] as const;
const sizes = ["sm", "md", "lg"] as const;

const meta = {
Expand Down Expand Up @@ -116,12 +116,6 @@ export const Solid: Story = {
},
};

export const Outline: Story = {
args: {
variant: "outline",
},
};

export const Soft: Story = {
args: {
variant: "soft",
Expand Down
59 changes: 4 additions & 55 deletions theme/src/recipes/card/useCardBodyRecipe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,6 @@ describe("useCardBodyRecipe", () => {
paddingBottom: "@0.75",
paddingLeft: "@1",
paddingRight: "@1",
borderTopWidth: "@border-width.thin",
borderTopStyle: "@border-style.solid",
borderTopColor: "transparent",
borderBottomWidth: "@border-width.thin",
borderBottomStyle: "@border-style.solid",
borderBottomColor: "transparent",
});
});

Expand Down Expand Up @@ -107,55 +101,10 @@ describe("useCardBodyRecipe", () => {
});
});

describe("compound variants", () => {
it("should have 12 compound variants total", () => {
const s = createInstance();
const recipe = useCardBodyRecipe(s);

// 3 colors × 4 variants = 12
expect(recipe.compoundVariants).toHaveLength(12);
});

it("should have correct neutral solid compound variant", () => {
const s = createInstance();
const recipe = useCardBodyRecipe(s);

const neutralSolid = recipe.compoundVariants!.find(
(cv) => cv.match.color === "neutral" && cv.match.variant === "solid",
);

expect(neutralSolid).toEqual({
match: { color: "neutral", variant: "solid" },
css: {
borderTopColor: "@color.gray-200",
borderBottomColor: "@color.gray-200",
"&:dark": {
borderTopColor: "@color.gray-800",
borderBottomColor: "@color.gray-800",
},
},
});
});

it("should have transparent borders for soft variants", () => {
const s = createInstance();
const recipe = useCardBodyRecipe(s);

const neutralSoft = recipe.compoundVariants!.find(
(cv) => cv.match.color === "neutral" && cv.match.variant === "soft",
);
it("should not have compound variants", () => {
const s = createInstance();
const recipe = useCardBodyRecipe(s);

expect(neutralSoft).toEqual({
match: { color: "neutral", variant: "soft" },
css: {
borderTopColor: "transparent",
borderBottomColor: "transparent",
"&:dark": {
borderTopColor: "transparent",
borderBottomColor: "transparent",
},
},
});
});
expect(recipe.compoundVariants).toBeUndefined();
});
});
Loading
Loading