diff --git a/packages/web/src/scss/components/Card/README.md b/packages/web/src/scss/components/Card/README.md index 355144d523..fb1f833041 100644 --- a/packages/web/src/scss/components/Card/README.md +++ b/packages/web/src/scss/components/Card/README.md @@ -69,6 +69,17 @@ Card can be displayed in a vertical, horizontal, or reversed horizontal layout. 👉 Keep in mind that, no matter the layout, the Card subcomponents must be arranged in the order [specified above](#card-1). +### Responsive Card Layout + +Card layout can be adjusted based on the breakpoint. To create a responsive layout, use the `tablet` and `desktop` +infixes, e.g. `Card--tablet--horizontal` or `Card--desktop--vertical`. + +```html +
+ +
+``` + ### Boxed Cards Card can be displayed with a border and a box shadow on hover. diff --git a/packages/web/src/scss/components/Card/_Card.scss b/packages/web/src/scss/components/Card/_Card.scss index 08f6101a47..42eb36ec74 100644 --- a/packages/web/src/scss/components/Card/_Card.scss +++ b/packages/web/src/scss/components/Card/_Card.scss @@ -1,67 +1,42 @@ // 1. Get ready for card link overlay. -// 2. If there is a expanded CardMedia in a boxed vertical card, replace card padding with empty grid columns/rows. -// 3. Allow shrinking in grid layouts. -// 4. Make sure links and buttons are clickable. -// 5. Make text content selectable when there is a stretched CardLink. - +// 2. Allow shrinking in grid layouts. +// 3. Make sure links and buttons are clickable. +// 4. Make text content selectable when there is a stretched CardLink. +// 5. Prepare card layouts for different breakpoints. +// a) Layouts are common for all breakpoints, so they are defined once in the theme file. +// b) Layout settings are applied to the grid via custom properties. +// c) If there is an expanded CardMedia in a boxed card, replace card padding with empty grid columns/rows. +// d) Padding is adjusted for different breakpoints. + +@use 'sass:map'; @use '@tokens' as tokens; @use '../../tools/breakpoint'; @use '../../tools/typography'; @use 'theme'; .Card { - --#{tokens.$css-variable-prefix}card-padding: #{theme.$padding}; - @include typography.generate(theme.$typography); position: relative; // 1. display: grid; - min-width: 0; // 3. + grid-template-columns: var(--#{tokens.$css-variable-prefix}card-columns); // 5.b + grid-template-rows: var(--#{tokens.$css-variable-prefix}card-rows); // 5.b + grid-template-areas: var(--#{tokens.$css-variable-prefix}card-areas); // 5.b + column-gap: var(--#{tokens.$css-variable-prefix}card-column-gap); // 5.b + min-width: 0; // 2. color: theme.$color; - - @include breakpoint.up(tokens.$breakpoint-tablet) { - --#{tokens.$css-variable-prefix}card-padding: #{theme.$padding-tablet}; - } } -// 4., 5. +// 3., 4. .Card:has(.CardLink) :where(a:not(.CardLink), button), .Card:has(.CardLink):has(.CardBody--selectable) :where(p, ul, ol, dl) { position: relative; z-index: 1; } -.Card--vertical { - grid-template-rows: auto 1fr auto; - grid-template-areas: - 'media' - 'body' - 'footer'; -} - -.Card--horizontal { - grid-template-columns: auto 1fr; - grid-template-rows: auto 1fr auto; - grid-template-areas: - 'media logo' - 'media body' - 'media footer'; -} - -.Card--horizontalReversed { - grid-template-columns: 1fr auto; - grid-template-rows: auto 1fr auto; - grid-template-areas: - 'logo media' - 'body media' - 'footer media'; -} - -:is(.Card--horizontal, .Card--horizontalReversed):has(.CardArtwork:not(:only-child), .CardMedia:not(:only-child)) { - column-gap: theme.$gap; -} - .Card--boxed { + --#{tokens.$css-variable-prefix}card-padding: #{theme.$padding}; // 5.d + border: theme.$border-width theme.$border-style theme.$border-color; border-radius: theme.$border-radius; background-color: theme.$background-color; @@ -76,77 +51,97 @@ } } - &:not(:has(.CardMedia--expanded)) { - padding: var(--#{tokens.$css-variable-prefix}card-padding); + &:has(.CardMedia--expanded) { + padding: var(--#{tokens.$css-variable-prefix}card-padding-shorthand); // 5.b } -} -.Card--boxed.Card--vertical { - &:has(.CardMedia--expanded) { - grid-template-columns: - var(--#{tokens.$css-variable-prefix}card-padding) - 1fr - var(--#{tokens.$css-variable-prefix}card-padding); // 2. - - grid-template-rows: - var(--#{tokens.$css-variable-prefix}card-padding) - auto - 1fr - auto; - grid-template-areas: - 'media media media' - 'media media media' - '. body .' - '. footer .'; + &:not(:has(.CardMedia--expanded)) { + padding: var(--#{tokens.$css-variable-prefix}card-padding); } - &:has(.CardMedia:not(:only-child)) { - padding-bottom: var(--#{tokens.$css-variable-prefix}card-padding); + @include breakpoint.up(tokens.$breakpoint-tablet) { + --#{tokens.$css-variable-prefix}card-padding: #{theme.$padding-tablet}; // 5.d } } -.Card--boxed.Card--horizontal { - &:has(.CardMedia--expanded) { - grid-template-columns: auto 1fr; - grid-template-rows: - var(--#{tokens.$css-variable-prefix}card-padding) - auto - 1fr - auto - var(--#{tokens.$css-variable-prefix}card-padding); // 2. - - grid-template-areas: - 'media .' - 'media logo' - 'media body' - 'media footer' - 'media .'; - } +// 5. +@each $breakpoint-name, $breakpoint-value in theme.$breakpoints { + $infix: breakpoint.get-modifier(infix, $breakpoint-name, $breakpoint-value); - &:has(.CardMedia:not(:only-child)) { - padding-right: var(--#{tokens.$css-variable-prefix}card-padding); - } -} + @include breakpoint.up($breakpoint-value) { + .Card--#{$infix}vertical { + --#{tokens.$css-variable-prefix}card-columns: #{map.get(theme.$layouts, vertical, columns)}; + --#{tokens.$css-variable-prefix}card-rows: #{map.get(theme.$layouts, vertical, rows)}; + --#{tokens.$css-variable-prefix}card-areas: #{map.get(theme.$layouts, vertical, areas)}; + } -.Card--boxed.Card--horizontalReversed { - &:has(.CardMedia--expanded) { - grid-template-columns: 1fr auto; - grid-template-rows: - var(--#{tokens.$css-variable-prefix}card-padding) - auto - 1fr - auto - var(--#{tokens.$css-variable-prefix}card-padding); // 2. - - grid-template-areas: - '. media' - 'logo media' - 'body media' - 'footer media' - '. media'; - } + .Card--#{$infix}horizontal { + --#{tokens.$css-variable-prefix}card-columns: #{map.get(theme.$layouts, horizontal, columns)}; + --#{tokens.$css-variable-prefix}card-rows: #{map.get(theme.$layouts, horizontal, rows)}; + --#{tokens.$css-variable-prefix}card-areas: #{map.get(theme.$layouts, horizontal, areas)}; + } + + .Card--#{$infix}horizontalReversed { + --#{tokens.$css-variable-prefix}card-columns: #{map.get(theme.$layouts, horizontal-reversed, columns)}; + --#{tokens.$css-variable-prefix}card-rows: #{map.get(theme.$layouts, horizontal-reversed, rows)}; + --#{tokens.$css-variable-prefix}card-areas: #{map.get(theme.$layouts, horizontal-reversed, areas)}; + } + + .Card--#{$infix}vertical:has(.CardArtwork:not(:only-child), .CardMedia:not(:only-child)) { + --#{tokens.$css-variable-prefix}card-column-gap: 0; + } + + :is(.Card--#{$infix}horizontal, .Card--#{$infix}horizontalReversed):has( + .CardArtwork:not(:only-child), + .CardMedia:not(:only-child) + ) { + --#{tokens.$css-variable-prefix}card-column-gap: #{theme.$gap}; + } + + // 5.c + .Card--boxed.Card--#{$infix}vertical { + &:has(.CardMedia--expanded) { + --#{tokens.$css-variable-prefix}card-columns: #{map.get(theme.$layouts-boxed, vertical, columns)}; + --#{tokens.$css-variable-prefix}card-rows: #{map.get(theme.$layouts-boxed, vertical, rows)}; + --#{tokens.$css-variable-prefix}card-areas: #{map.get(theme.$layouts-boxed, vertical, areas)}; + } - &:has(.CardMedia:not(:only-child)) { - padding-left: var(--#{tokens.$css-variable-prefix}card-padding); + &:has(.CardMedia:not(:only-child)) { + --#{tokens.$css-variable-prefix}card-padding-shorthand: 0 0 + var(--#{tokens.$css-variable-prefix}card-padding); + } + } + + // 5.c + .Card--boxed.Card--#{$infix}horizontal { + &:has(.CardMedia--expanded) { + --#{tokens.$css-variable-prefix}card-columns: #{map.get(theme.$layouts-boxed, horizontal, columns)}; + --#{tokens.$css-variable-prefix}card-rows: #{map.get(theme.$layouts-boxed, horizontal, rows)}; + --#{tokens.$css-variable-prefix}card-areas: #{map.get(theme.$layouts-boxed, horizontal, areas)}; + } + + &:has(.CardMedia:not(:only-child)) { + --#{tokens.$css-variable-prefix}card-padding-shorthand: 0 + var(--#{tokens.$css-variable-prefix}card-padding) 0 0; + } + } + + // 5.c + .Card--boxed.Card--#{$infix}horizontalReversed { + &:has(.CardMedia--expanded) { + --#{tokens.$css-variable-prefix}card-columns: #{map.get( + theme.$layouts-boxed, + horizontal-reversed, + columns + )}; + --#{tokens.$css-variable-prefix}card-rows: #{map.get(theme.$layouts-boxed, horizontal-reversed, rows)}; + --#{tokens.$css-variable-prefix}card-areas: #{map.get(theme.$layouts-boxed, horizontal-reversed, areas)}; + } + + &:has(.CardMedia:not(:only-child)) { + --#{tokens.$css-variable-prefix}card-padding-shorthand: 0 0 0 + var(--#{tokens.$css-variable-prefix}card-padding); + } + } } } diff --git a/packages/web/src/scss/components/Card/_CardArtwork.scss b/packages/web/src/scss/components/Card/_CardArtwork.scss index d5f18f3358..30899e1582 100644 --- a/packages/web/src/scss/components/Card/_CardArtwork.scss +++ b/packages/web/src/scss/components/Card/_CardArtwork.scss @@ -1,9 +1,15 @@ +@use '@tokens' as tokens; +@use '../../tools/breakpoint'; @use '../../tools/dictionaries'; @use 'theme'; .CardArtwork { display: grid; grid-area: media; + + &:not(:last-child) { + margin-bottom: var(--#{tokens.$css-variable-prefix}card-artwork-margin-bottom); + } } @include dictionaries.generate-alignments( @@ -12,6 +18,16 @@ $axis: 'x' ); -.Card--vertical > .CardArtwork:not(:last-child) { - margin-bottom: theme.$gap; +@each $breakpoint-name, $breakpoint-value in theme.$breakpoints { + @include breakpoint.up($breakpoint-value) { + $infix: breakpoint.get-modifier(infix, $breakpoint-name, $breakpoint-value); + + .Card--#{$infix}vertical > .CardArtwork { + --#{tokens.$css-variable-prefix}card-artwork-margin-bottom: #{theme.$gap}; + } + + :is(.Card--#{$infix}horizontal, .Card--#{$infix}horizontalReversed) > .CardArtwork { + --#{tokens.$css-variable-prefix}card-artwork-margin-bottom: 0; + } + } } diff --git a/packages/web/src/scss/components/Card/_CardLogo.scss b/packages/web/src/scss/components/Card/_CardLogo.scss index 36b14e4894..94d72dbc32 100644 --- a/packages/web/src/scss/components/Card/_CardLogo.scss +++ b/packages/web/src/scss/components/Card/_CardLogo.scss @@ -1,25 +1,39 @@ +@use '@tokens' as tokens; +@use '../../tools/breakpoint'; @use 'theme'; .CardLogo { display: inline-flex; + grid-area: var(--#{tokens.$css-variable-prefix}card-logo-grid-area); + place-self: var(--#{tokens.$css-variable-prefix}card-logo-place-self); align-items: center; justify-content: center; + margin-right: var(--#{tokens.$css-variable-prefix}card-logo-margin-right); border: theme.$logo-border-width theme.$logo-border-style theme.$logo-border-color; border-radius: theme.$logo-border-radius; background-color: theme.$logo-background-color; -} -.Card--vertical > .CardLogo { - grid-area: media; - place-self: end; - margin-right: theme.$logo-offset-horizontal; + &:not(:last-child) { + margin-bottom: var(--#{tokens.$css-variable-prefix}card-logo-margin-bottom); + } } -:is(.Card--horizontal, .Card--horizontalReversed) > .CardLogo { - grid-area: logo; - justify-self: start; +@each $breakpoint-name, $breakpoint-value in theme.$breakpoints { + @include breakpoint.up($breakpoint-value) { + $infix: breakpoint.get-modifier(infix, $breakpoint-name, $breakpoint-value); - &:not(:last-child) { - margin-bottom: theme.$gap; + .Card--#{$infix}vertical > .CardLogo { + --#{tokens.$css-variable-prefix}card-logo-grid-area: media; + --#{tokens.$css-variable-prefix}card-logo-place-self: end; + --#{tokens.$css-variable-prefix}card-logo-margin-right: #{theme.$logo-offset-horizontal}; + --#{tokens.$css-variable-prefix}card-logo-margin-bottom: 0; + } + + :is(.Card--#{$infix}horizontal, .Card--#{$infix}horizontalReversed) > .CardLogo { + --#{tokens.$css-variable-prefix}card-logo-grid-area: logo; + --#{tokens.$css-variable-prefix}card-logo-place-self: start; + --#{tokens.$css-variable-prefix}card-logo-margin-right: 0; + --#{tokens.$css-variable-prefix}card-logo-margin-bottom: #{theme.$gap}; + } } } diff --git a/packages/web/src/scss/components/Card/_CardMedia.scss b/packages/web/src/scss/components/Card/_CardMedia.scss index aa81c7271a..7218392bf1 100644 --- a/packages/web/src/scss/components/Card/_CardMedia.scss +++ b/packages/web/src/scss/components/Card/_CardMedia.scss @@ -2,26 +2,22 @@ // when the media is wrapped, for example in a link. // 2. Make sure rounded corners are applied to video player too. // 3. Do not let the media push the body content out of the horizontal card. -// 4. We need the specific combination of classes, expanded CardMedia is designed to work only with boxed cards. @use '@tokens' as tokens; +@use '../../tools/breakpoint'; @use '../../tools/dictionaries'; @use 'theme'; .CardMedia { - --#{tokens.$css-variable-prefix}card-media-border-radius-top-left: #{theme.$border-radius}; - --#{tokens.$css-variable-prefix}card-media-border-radius-top-right: #{theme.$border-radius}; - --#{tokens.$css-variable-prefix}card-media-border-radius-bottom-left: #{theme.$border-radius}; - --#{tokens.$css-variable-prefix}card-media-border-radius-bottom-right: #{theme.$border-radius}; - --#{tokens.$css-variable-prefix}card-media-canvas-width: auto; - --#{tokens.$css-variable-prefix}card-media-canvas-height: auto; - grid-area: media; + align-self: var(--#{tokens.$css-variable-prefix}card-media-align-self); + min-width: 0; // 3. overflow: hidden; - border-radius: var(--#{tokens.$css-variable-prefix}card-media-border-radius-top-left) - var(--#{tokens.$css-variable-prefix}card-media-border-radius-top-right) - var(--#{tokens.$css-variable-prefix}card-media-border-radius-bottom-right) - var(--#{tokens.$css-variable-prefix}card-media-border-radius-bottom-left); + border-radius: var(--#{tokens.$css-variable-prefix}card-media-border-radius-shorthand, #{theme.$border-radius}); + + &:not(:last-child) { + margin-bottom: var(--#{tokens.$css-variable-prefix}card-media-margin-bottom); + } } .CardMedia__canvas { @@ -38,48 +34,50 @@ object-fit: cover; } -.Card--vertical > { - @include dictionaries.generate-sizes($class-name: 'CardMedia', $sizes: theme.$media-sizes-vertical); -} - -:is(.Card--horizontal, .Card--horizontalReversed) > { - @include dictionaries.generate-sizes($class-name: 'CardMedia', $sizes: theme.$media-sizes-horizontal); -} - -:is(.Card--horizontal, .Card--horizontalReversed) > .CardMedia { - --#{tokens.$css-variable-prefix}card-media-canvas-width: var(--spirit-card-media-size); - - align-self: start; - min-width: 0; // 3. -} - -:is(.Card--horizontal, .Card--horizontalReversed) > :is(.CardMedia--expanded, .CardMedia--filledHeight) { - --#{tokens.$css-variable-prefix}card-media-canvas-height: 100%; - - align-self: stretch; -} - -.Card--vertical > .CardMedia { - --#{tokens.$css-variable-prefix}card-media-canvas-height: var(--spirit-card-media-size); - - &:not(:last-child) { - margin-bottom: theme.$gap-dense; +@each $breakpoint-name, $breakpoint-value in theme.$breakpoints { + @include breakpoint.up($breakpoint-value) { + $infix: breakpoint.get-modifier(infix, $breakpoint-name, $breakpoint-value); + + .Card--#{$infix}vertical > { + @include dictionaries.generate-sizes($class-name: 'CardMedia', $sizes: theme.$media-sizes-vertical); + } + + :is(.Card--#{$infix}horizontal, .Card--#{$infix}horizontalReversed) > { + @include dictionaries.generate-sizes($class-name: 'CardMedia', $sizes: theme.$media-sizes-horizontal); + } + + .Card--#{$infix}vertical > .CardMedia { + --#{tokens.$css-variable-prefix}card-media-canvas-width: auto; + --#{tokens.$css-variable-prefix}card-media-canvas-height: var(--spirit-card-media-size); + --#{tokens.$css-variable-prefix}card-media-align-self: initial; + --#{tokens.$css-variable-prefix}card-media-margin-bottom: #{theme.$gap-dense}; + } + + :is(.Card--#{$infix}horizontal, .Card--#{$infix}horizontalReversed) > .CardMedia { + --#{tokens.$css-variable-prefix}card-media-canvas-width: var(--spirit-card-media-size); + --#{tokens.$css-variable-prefix}card-media-canvas-height: auto; + --#{tokens.$css-variable-prefix}card-media-align-self: start; + --#{tokens.$css-variable-prefix}card-media-margin-bottom: 0; + } + + :is(.Card--#{$infix}horizontal, .Card--#{$infix}horizontalReversed) + > :is(.CardMedia--expanded, .CardMedia--filledHeight) { + --#{tokens.$css-variable-prefix}card-media-canvas-height: 100%; + --#{tokens.$css-variable-prefix}card-media-align-self: stretch; + } + + .Card--boxed.Card--#{$infix}vertical > .CardMedia--expanded:not(:last-child) { + --#{tokens.$css-variable-prefix}card-media-border-radius-shorthand: #{theme.$border-radius} #{theme.$border-radius} + 0 0; + } + + .Card--boxed.Card--#{$infix}horizontal > .CardMedia--expanded:not(:last-child) { + --#{tokens.$css-variable-prefix}card-media-border-radius-shorthand: #{theme.$border-radius} 0 0 #{theme.$border-radius}; + } + + .Card--boxed.Card--#{$infix}horizontalReversed > .CardMedia--expanded:not(:last-child) { + --#{tokens.$css-variable-prefix}card-media-border-radius-shorthand: 0 #{theme.$border-radius} #{theme.$border-radius} + 0; + } } } - -// stylelint-disable selector-max-class -- 4. -.Card--vertical.Card--boxed > .CardMedia--expanded:not(:last-child) { - --#{tokens.$css-variable-prefix}card-media-border-radius-bottom-left: 0; - --#{tokens.$css-variable-prefix}card-media-border-radius-bottom-right: 0; -} - -.Card--horizontal.Card--boxed > .CardMedia--expanded:not(:last-child) { - --#{tokens.$css-variable-prefix}card-media-border-radius-top-right: 0; - --#{tokens.$css-variable-prefix}card-media-border-radius-bottom-right: 0; -} - -.Card--horizontalReversed.Card--boxed > .CardMedia--expanded:not(:last-child) { - --#{tokens.$css-variable-prefix}card-media-border-radius-top-left: 0; - --#{tokens.$css-variable-prefix}card-media-border-radius-bottom-left: 0; -} -// stylelint-enable diff --git a/packages/web/src/scss/components/Card/_theme.scss b/packages/web/src/scss/components/Card/_theme.scss index 33b36ba6ce..1ffe08e355 100644 --- a/packages/web/src/scss/components/Card/_theme.scss +++ b/packages/web/src/scss/components/Card/_theme.scss @@ -4,6 +4,44 @@ $breakpoints: tokens.$breakpoints; +$layouts: ( + vertical: ( + columns: 1fr, + rows: auto 1fr auto, + areas: "'media' 'body' 'footer'", + ), + horizontal: ( + columns: auto 1fr, + rows: auto 1fr auto, + areas: "'media logo' 'media body' 'media footer'", + ), + horizontal-reversed: ( + columns: 1fr auto, + rows: auto 1fr auto, + areas: "'logo media' 'body media' 'footer media'", + ), +); + +$layouts-boxed: ( + vertical: ( + columns: var(--#{tokens.$css-variable-prefix}card-padding) 1fr var(--#{tokens.$css-variable-prefix}card-padding), + rows: var(--#{tokens.$css-variable-prefix}card-padding) auto 1fr auto, + areas: "'media media media' 'media media media' '. body .' '. footer .'", + ), + horizontal: ( + columns: auto 1fr, + rows: var(--#{tokens.$css-variable-prefix}card-padding) auto 1fr auto + var(--#{tokens.$css-variable-prefix}card-padding), + areas: "'media .' 'media logo' 'media body' 'media footer' 'media .'", + ), + horizontal-reversed: ( + columns: 1fr auto, + rows: var(--#{tokens.$css-variable-prefix}card-padding) auto 1fr auto + var(--#{tokens.$css-variable-prefix}card-padding), + areas: "'. media' 'logo media' 'body media' 'footer media' '. media'", + ), +); + $gap: tokens.$space-900; $gap-dense: tokens.$space-700; diff --git a/packages/web/src/scss/components/Card/index.html b/packages/web/src/scss/components/Card/index.html index 87e0d725f6..9d4b927f90 100644 --- a/packages/web/src/scss/components/Card/index.html +++ b/packages/web/src/scss/components/Card/index.html @@ -562,6 +562,77 @@

+
+

Responsive Card Layout

+
+ +
+ + + + + +
+ +
+
+

Media Options