Skip to content

Commit 7ca186c

Browse files
Horizontal card layout (#1199)
* Support vertical layout * Make horizontal layout more robust * Remove redundant styles * Add horizontal examples * Add changeset * Fix horizontal media card layout * Make right aligned Media clickable * Add example * Update example texts * Make example outlined * FIx Badge radius in horizontal Media
1 parent 00edb02 commit 7ca186c

File tree

3 files changed

+150
-4
lines changed

3 files changed

+150
-4
lines changed

.changeset/polite-candies-draw.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@obosbbl/grunnmuren-react": minor
3+
---
4+
5+
New `layout` prop in `<Card>` to support for responsive horizontal layout.

packages/react/src/card/card.stories.tsx

+86
Original file line numberDiff line numberDiff line change
@@ -468,3 +468,89 @@ export const ClickableWithBadgeRight = () => (
468468
</Footer>
469469
</Card>
470470
);
471+
472+
export const HorizontalLeft = () => (
473+
<Card layout="horizontal">
474+
<Media>
475+
<img
476+
alt=""
477+
src="https://res.cloudinary.com/obosit-prd-ch-clry/image/upload/obos-logo-socialmeta.jpg"
478+
/>
479+
</Media>
480+
<Content>
481+
<Heading level={3}>Med bilde til venstre</Heading>
482+
<p>
483+
Dette kortet har bilde til venstre på større skjermer og er klikkbart
484+
mot en CTA-lenke
485+
</p>
486+
<CardLink className="group/cta">
487+
<Button href="#cta" variant="tertiary">
488+
Les mer
489+
<ArrowRight className="transition-transform group-hover/cta:motion-safe:translate-x-1" />
490+
</Button>
491+
</CardLink>
492+
</Content>
493+
</Card>
494+
);
495+
496+
export const HorizontalRight = () => (
497+
<Card layout="horizontal">
498+
<Content>
499+
<Heading level={3}>Med bilde til høyre</Heading>
500+
<p>
501+
Dette kortet har bilde til høyre på større skjermer og er klikkbart mot
502+
en CTA-lenke
503+
</p>
504+
<CardLink className="group/cta">
505+
<Button href="#cta" variant="tertiary">
506+
Les mer
507+
<ArrowRight className="transition-transform group-hover/cta:motion-safe:translate-x-1" />
508+
</Button>
509+
</CardLink>
510+
</Content>
511+
<Media>
512+
<img
513+
alt=""
514+
src="https://res.cloudinary.com/obosit-prd-ch-clry/image/upload/obos-logo-socialmeta.jpg"
515+
/>
516+
</Media>
517+
</Card>
518+
);
519+
520+
export const HorizontalWithIconLeft = () => (
521+
<Card layout="horizontal" variant="outlined">
522+
<PiggyBank />
523+
<Content>
524+
<Heading level={3}>Med ikon til venstre</Heading>
525+
<p>
526+
Dette kortet er liggende, har et ikon til venstre og er klikkbart mot en
527+
CTA-lenke
528+
</p>
529+
<CardLink className="group/cta">
530+
<Button href="#cta" variant="tertiary">
531+
Les mer
532+
<ArrowRight className="transition-transform group-hover/cta:motion-safe:translate-x-1" />
533+
</Button>
534+
</CardLink>
535+
</Content>
536+
</Card>
537+
);
538+
539+
export const HorizontalWithIconRight = () => (
540+
<Card layout="horizontal" variant="outlined">
541+
<Content>
542+
<Heading level={3}>Med ikon til høyre</Heading>
543+
<p>
544+
Dette kortet er liggende, har et ikon til høyre og er klikkbart mot en
545+
CTA-lenke
546+
</p>
547+
<CardLink className="group/cta">
548+
<Button href="#cta" variant="tertiary">
549+
Les mer
550+
<ArrowRight className="transition-transform group-hover/cta:motion-safe:translate-x-1" />
551+
</Button>
552+
</CardLink>
553+
</Content>
554+
<PiggyBank />
555+
</Card>
556+
);

packages/react/src/card/card.tsx

+59-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const cardVariants = cva({
1010
base: [
1111
'group/card',
1212
'rounded-2xl border p-3',
13-
'flex flex-col gap-y-4',
13+
'flex gap-y-4', // y-gap ensures a vertical spacing for both verical layout and responsive horizontal layout
1414
'relative', // Needed for positiong of the clickable pseudo-element (and can also be used for other absolute positioned elements the consumer might add)
1515

1616
// **** Heading ****
@@ -28,9 +28,9 @@ const cardVariants = cva({
2828
// **** Media ****
2929
'[&_[data-slot="media"]]:overflow-hidden', // Prevent content from overflowing the rounded corners
3030
'[&_[data-slot="media"]]:relative', // Needed for positioning the <Badge> component (if present)
31-
'[&_[data-slot="media"]]:rounded-t-2xl', // Top corners are always rounded
3231
// Position media at the edges of the card (because of these negative margins the media-element must be a wrapper around the actual image or other media content)
3332
'[&_[data-slot="media"]]:mx-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))] [&_[data-slot="media"]]:mt-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
33+
3434
// Sets the aspect ratio of the media content (width: 100% is necessary to make aspect ratio work in FF)
3535
'[&_[data-slot="media"]>*:not([data-slot="badge"])]:aspect-[3/2] [&_[data-slot="media"]>*:not([data-slot="badge"])]:w-full [&_[data-slot="media"]_img]:object-cover',
3636
// Prepare zoom animation for hover effects. The hover effect can also be enabled by classes on the parent component, so it is always prepared here.
@@ -91,26 +91,81 @@ const cardVariants = cva({
9191
variant: {
9292
subtle: [
9393
'border-transparent',
94-
// Media styles:
95-
'[&_[data-slot="media"]]:rounded-b-2xl',
94+
// **** Media styles ****
95+
'[&_[data-slot="media"]]:rounded-2xl', // All corners are rounded
9696
],
9797
outlined: 'border border-black',
9898
},
99+
/**
100+
* The layout of the card
101+
* @default vertical
102+
*/
103+
layout: {
104+
vertical: [
105+
'flex-col',
106+
// **** Media ****
107+
'[&_[data-slot="media"]]:rounded-t-2xl', // Both Top corners are rounded
108+
],
109+
horizontal: [
110+
'gap-x-4', // Since this does not affect the layout before the flex direction is set (at breakpoint md for Card with Media), we can set it here
111+
// **** With Media ****
112+
'[&:has(>[data-slot="media"]:first-child)]:flex-col',
113+
'[&:has(>[data-slot="media"]:last-child)]:flex-col-reverse', // Always display the media at the top of the card
114+
'[&:has(>[data-slot="media"])]:md:!flex-row', // When need !important to override the specificity (first-/last-child) of the flex-col-reverse and flex-col classes
115+
116+
'[&:has(>[data-slot="media"])>*]:md:basis-1/2', // Ensures a 50/50 split of the media and content on medium screens
117+
// Position media at the edges of the card
118+
'[&_[data-slot="media"]]:md:mb-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
119+
'[&_[data-slot="media"]:first-child]:md:mr-0',
120+
'[&_[data-slot="media"]:last-child]:md:ml-0',
121+
122+
// Make sure the card link is clickable when the media is on the right side
123+
// This i necessary because the media content is positioned after the card link in the DOM
124+
'[&:has(>[data-slot="media"]:last-child)_[data-slot="card-link"]]:z-[1]',
125+
126+
// **** Without Media ****
127+
'[&:not(:has(>[data-slot="media"]))]:flex-row',
128+
// Make the layout responsive: when the Content reaches a minimum width of 18rem, the layout switches to vertical. Also makes sure Content takes up the remaining space available.
129+
'[&:not(:has(>[data-slot="media"]))]:flex-wrap [&:not(:has(>[data-slot="media"]))_[data-slot="content"]]:grow [&:not(:has(>[data-slot="media"]))_[data-slot="content"]]:basis-[18rem]',
130+
// Make sure svg's etc. are not shrinkable
131+
'[&>:not([data-slot="content"],[data-slot="media"])]:shrink-0',
132+
],
133+
},
99134
},
100135
defaultVariants: {
101136
variant: 'subtle',
137+
layout: 'vertical',
102138
},
139+
compoundVariants: [
140+
{
141+
variant: 'outlined',
142+
layout: 'horizontal',
143+
className: [
144+
// **** Media ****
145+
// Some rounded corners are removed when the card is outlined
146+
'[&_[data-slot="media"]]:rounded-t-2xl', // On small screens, the top corners are rounded
147+
'[&_[data-slot="media"]:first-child]:md:rounded-tr-none [&_[data-slot="media"]:first-child]:md:rounded-bl-2xl', // Both left corners are rounded when media is on the left side
148+
'[&_[data-slot="media"]:last-child]:md:rounded-tl-none [&_[data-slot="media"]:last-child]:md:rounded-br-2xl', // Both right corners are rounded when media is on the right side
149+
// **** Badge ****
150+
// Override default corner radius of the badge to match the media border radius
151+
'[&_[data-slot="media"]:first-child_[data-slot="badge"]:last-child]:md:rounded-tr-none',
152+
'[&_[data-slot="media"]:last-child_[data-slot="badge"]:first-child]:md:rounded-tl-none',
153+
],
154+
},
155+
],
103156
});
104157

105158
const Card = ({
106159
children,
107160
className: _className,
108161
variant,
162+
layout,
109163
...restProps
110164
}: CardProps) => {
111165
const className = cardVariants({
112166
className: _className,
113167
variant,
168+
layout,
114169
});
115170
return (
116171
<div className={className} {...restProps}>

0 commit comments

Comments
 (0)