diff --git a/site/theme/__tests__/__snapshots__/doc-theme.test.ts.snap b/site/theme/__tests__/__snapshots__/doc-theme.test.ts.snap
index 275e76e52e..59c92ecd8f 100644
--- a/site/theme/__tests__/__snapshots__/doc-theme.test.ts.snap
+++ b/site/theme/__tests__/__snapshots__/doc-theme.test.ts.snap
@@ -855,7 +855,9 @@ Object {
},
"cardContainer": Object {
"base": Object {
+ "backgroundColor": "{{colors.interface010}}",
"borderRadius": false,
+ "color": "{{colors.inkBase}}",
},
},
"cardContainerActions": Object {
@@ -3615,7 +3617,9 @@ Object {
},
"cardContainer": Object {
"base": Object {
+ "backgroundColor": "{{colors.interface010}}",
"borderRadius": false,
+ "color": "{{colors.inkBase}}",
},
},
"cardContainerActions": Object {
@@ -6378,7 +6382,9 @@ Object {
},
"cardContainer": Object {
"base": Object {
+ "backgroundColor": "{{colors.interface010}}",
"borderRadius": false,
+ "color": "{{colors.inkBase}}",
},
},
"cardContainerActions": Object {
@@ -9138,7 +9144,9 @@ Object {
},
"cardContainer": Object {
"base": Object {
+ "backgroundColor": "{{colors.interface010}}",
"borderRadius": false,
+ "color": "{{colors.inkBase}}",
},
},
"cardContainerActions": Object {
@@ -11901,7 +11909,9 @@ Object {
},
"cardContainer": Object {
"base": Object {
+ "backgroundColor": "{{colors.interface010}}",
"borderRadius": false,
+ "color": "{{colors.inkBase}}",
},
},
"cardContainerActions": Object {
@@ -14661,7 +14671,9 @@ Object {
},
"cardContainer": Object {
"base": Object {
+ "backgroundColor": "{{colors.interface010}}",
"borderRadius": false,
+ "color": "{{colors.inkBase}}",
},
},
"cardContainerActions": Object {
@@ -17424,7 +17436,9 @@ Object {
},
"cardContainer": Object {
"base": Object {
+ "backgroundColor": "{{colors.interface010}}",
"borderRadius": false,
+ "color": "{{colors.inkBase}}",
},
},
"cardContainerActions": Object {
@@ -20184,7 +20198,9 @@ Object {
},
"cardContainer": Object {
"base": Object {
+ "backgroundColor": "{{colors.interface010}}",
"borderRadius": false,
+ "color": "{{colors.inkBase}}",
},
},
"cardContainerActions": Object {
diff --git a/src/__tests__/__snapshots__/index.test.ts.snap b/src/__tests__/__snapshots__/index.test.ts.snap
index dbb2db386d..97a0f47be5 100644
--- a/src/__tests__/__snapshots__/index.test.ts.snap
+++ b/src/__tests__/__snapshots__/index.test.ts.snap
@@ -27,7 +27,12 @@ Array [
"Byline",
"Caption",
"Card",
+ "CardActions",
+ "CardComposable",
+ "CardContent",
"CardInset",
+ "CardLink",
+ "CardMedia",
"Cell",
"CharacterCount",
"Checkbox",
diff --git a/src/card-composable/__tests__/__snapshots__/card-composable.test.tsx.snap b/src/card-composable/__tests__/__snapshots__/card-composable.test.tsx.snap
new file mode 100644
index 0000000000..02d6aee959
--- /dev/null
+++ b/src/card-composable/__tests__/__snapshots__/card-composable.test.tsx.snap
@@ -0,0 +1,530 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CardComposable renders defaults 1`] = `
+
+ .emotion-0 {
+ margin: 0;
+ padding: 0;
+ display: grid;
+ grid-template-areas: "media" "content" "actions";
+ color: #3B3B3B;
+ background-color: #FFFFFF;
+ position: relative;
+}
+
+@media screen and (prefers-reduced-motion: no-preference) {
+ .emotion-0 {
+ transition-property: background-color;
+ transition-duration: 200ms;
+ transition-timing-function: cubic-bezier(0, 0, .5, 1);
+ }
+}
+
+@media screen and (prefers-reduced-motion: reduce) {
+ .emotion-0 {
+ transition-property: background-color;
+ transition-duration: 0ms;
+ transition-timing-function: cubic-bezier(0, 0, .5, 1);
+ }
+}
+
+.emotion-1 {
+ margin: 0;
+ padding: 0;
+ display: grid;
+ justify-items: start;
+ -webkit-align-items: start;
+ -webkit-box-align: start;
+ -ms-flex-align: start;
+ align-items: start;
+ grid-area: content;
+}
+
+.emotion-2 {
+ display: inline-block;
+ color: #3358CC;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+@media screen and (prefers-reduced-motion: no-preference) {
+ .emotion-2 {
+ transition-property: color,fill;
+ transition-duration: 200ms,200ms;
+ transition-timing-function: cubic-bezier(0, 0, .5, 1),cubic-bezier(0, 0, .5, 1);
+ }
+}
+
+@media screen and (prefers-reduced-motion: reduce) {
+ .emotion-2 {
+ transition-property: color,fill;
+ transition-duration: 0ms;
+ transition-timing-function: cubic-bezier(0, 0, .5, 1),cubic-bezier(0, 0, .5, 1);
+ }
+}
+
+.emotion-2 svg {
+ fill: #3358CC;
+}
+
+.emotion-2:hover:not(:disabled) {
+ color: #254CAC;
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-2:hover:not(:disabled) svg {
+ fill: #254CAC;
+}
+
+.emotion-2:active:not(:disabled) {
+ color: #12387A;
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-2:active:not(:disabled) svg {
+ fill: #12387A;
+}
+
+.emotion-2:focus-visible:not(:disabled) {
+ outline-color: #3768FB;
+ outline-style: solid;
+ outline-width: 2px;
+ outline-offset: 2px;
+}
+
+@media not all and (min-resolution: 0.001dpcm) {
+ @supports (-webkit-appearance: none) and (stroke-color: transparent) {
+ .emotion-2:focus-visible:not(:disabled) {
+ outline-style: auto;
+ }
+ }
+}
+
+.emotion-2:before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+}
+
+.emotion-3 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ height: 100%;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ -webkit-justify-content: flex-start;
+ justify-content: flex-start;
+}
+
+.emotion-4 {
+ margin: 0;
+ font-family: "Poppins",sans-serif;
+ font-size: 14px;
+ line-height: 21px;
+ font-weight: 500;
+ letter-spacing: 0;
+ padding: 0.5px 0px;
+ display: inline-block;
+ display: block;
+}
+
+.emotion-4::before {
+ content: '';
+ margin-bottom: -0.403em;
+ display: block;
+}
+
+.emotion-4::after {
+ content: '';
+ margin-top: -0.4em;
+ display: block;
+}
+
+.emotion-5 {
+ margin: 0;
+ padding: 0;
+ display: grid;
+ grid-area: media;
+}
+
+.emotion-6 {
+ position: relative;
+ width: 100%;
+ display: block;
+ padding-top: 0;
+ height: 0;
+ width: 100%;
+ border-radius: 0;
+ background-color: #F1F1F1;
+}
+
+.emotion-6 svg {
+ fill: #ABABAB;
+}
+
+.emotion-7 {
+ top: 0;
+ left: 0;
+ position: absolute;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+.emotion-8 {
+ opacity: 0;
+ display: block;
+ border-radius: inherit;
+ height: auto;
+ width: 100%;
+ top: 0;
+ left: 0;
+ position: absolute;
+}
+
+.emotion-9 {
+ margin: 0;
+ padding: 0;
+ display: grid;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ -webkit-justify-content: start;
+ justify-content: start;
+ -webkit-align-items: start;
+ -webkit-box-align: start;
+ -ms-flex-align: start;
+ align-items: start;
+ grid-area: actions;
+ position: relative;
+ z-index: 2;
+}
+
+
+
+
+
+
+
+
+
+
+ actions
+
+
+
+`;
+
+exports[`CardComposable renders without areas 1`] = `
+
+ .emotion-0 {
+ margin: 0;
+ padding: 0;
+ display: grid;
+ color: #3B3B3B;
+ background-color: #FFFFFF;
+ position: relative;
+}
+
+@media screen and (prefers-reduced-motion: no-preference) {
+ .emotion-0 {
+ transition-property: background-color;
+ transition-duration: 200ms;
+ transition-timing-function: cubic-bezier(0, 0, .5, 1);
+ }
+}
+
+@media screen and (prefers-reduced-motion: reduce) {
+ .emotion-0 {
+ transition-property: background-color;
+ transition-duration: 0ms;
+ transition-timing-function: cubic-bezier(0, 0, .5, 1);
+ }
+}
+
+.emotion-1 {
+ margin: 0;
+ padding: 0;
+ display: grid;
+ justify-items: start;
+ -webkit-align-items: start;
+ -webkit-box-align: start;
+ -ms-flex-align: start;
+ align-items: start;
+}
+
+.emotion-2 {
+ display: inline-block;
+ color: #3358CC;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+@media screen and (prefers-reduced-motion: no-preference) {
+ .emotion-2 {
+ transition-property: color,fill;
+ transition-duration: 200ms,200ms;
+ transition-timing-function: cubic-bezier(0, 0, .5, 1),cubic-bezier(0, 0, .5, 1);
+ }
+}
+
+@media screen and (prefers-reduced-motion: reduce) {
+ .emotion-2 {
+ transition-property: color,fill;
+ transition-duration: 0ms;
+ transition-timing-function: cubic-bezier(0, 0, .5, 1),cubic-bezier(0, 0, .5, 1);
+ }
+}
+
+.emotion-2 svg {
+ fill: #3358CC;
+}
+
+.emotion-2:hover:not(:disabled) {
+ color: #254CAC;
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-2:hover:not(:disabled) svg {
+ fill: #254CAC;
+}
+
+.emotion-2:active:not(:disabled) {
+ color: #12387A;
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-2:active:not(:disabled) svg {
+ fill: #12387A;
+}
+
+.emotion-2:focus-visible:not(:disabled) {
+ outline-color: #3768FB;
+ outline-style: solid;
+ outline-width: 2px;
+ outline-offset: 2px;
+}
+
+@media not all and (min-resolution: 0.001dpcm) {
+ @supports (-webkit-appearance: none) and (stroke-color: transparent) {
+ .emotion-2:focus-visible:not(:disabled) {
+ outline-style: auto;
+ }
+ }
+}
+
+.emotion-3 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ height: 100%;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ -webkit-justify-content: flex-start;
+ justify-content: flex-start;
+}
+
+.emotion-4 {
+ margin: 0;
+ font-family: "Poppins",sans-serif;
+ font-size: 14px;
+ line-height: 21px;
+ font-weight: 500;
+ letter-spacing: 0;
+ padding: 0.5px 0px;
+ display: inline-block;
+ display: block;
+}
+
+.emotion-4::before {
+ content: '';
+ margin-bottom: -0.403em;
+ display: block;
+}
+
+.emotion-4::after {
+ content: '';
+ margin-top: -0.4em;
+ display: block;
+}
+
+.emotion-5 {
+ margin: 0;
+ padding: 0;
+ display: grid;
+}
+
+.emotion-6 {
+ position: relative;
+ width: 100%;
+ display: block;
+ padding-top: 0;
+ height: 0;
+ width: 100%;
+ border-radius: 0;
+ background-color: #F1F1F1;
+}
+
+.emotion-6 svg {
+ fill: #ABABAB;
+}
+
+.emotion-7 {
+ top: 0;
+ left: 0;
+ position: absolute;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+.emotion-8 {
+ opacity: 0;
+ display: block;
+ border-radius: inherit;
+ height: auto;
+ width: 100%;
+ top: 0;
+ left: 0;
+ position: absolute;
+}
+
+.emotion-9 {
+ margin: 0;
+ padding: 0;
+ display: grid;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ -webkit-justify-content: start;
+ justify-content: start;
+ -webkit-align-items: start;
+ -webkit-box-align: start;
+ -ms-flex-align: start;
+ align-items: start;
+ position: relative;
+ z-index: 2;
+}
+
+
+
+
+
+
+
+
+
+
+ actions
+
+
+
+`;
diff --git a/src/card-composable/__tests__/__stories/card-composable.stories.tsx b/src/card-composable/__tests__/__stories/card-composable.stories.tsx
new file mode 100644
index 0000000000..54d722c32d
--- /dev/null
+++ b/src/card-composable/__tests__/__stories/card-composable.stories.tsx
@@ -0,0 +1,948 @@
+/* eslint-disable no-script-url */
+import React from 'react';
+import {Story as StoryType} from '@storybook/react';
+import {
+ CardComposable,
+ CardActions,
+ CardContent,
+ CardLink,
+ CardMedia,
+} from '../../card-composable';
+import {Headline, HeadlineProps} from '../../../headline';
+import {CreateThemeArgs, ThemeProvider} from '../../../theme';
+import {createCustomThemeWithBaseThemeSwitch} from '../../../test/theme-select-object';
+import {Flag} from '../../../flag';
+import {StorybookCase, StorybookPage} from '../../../test/storybook-comps';
+import {Button} from '../../../button';
+import {UnorderedList} from '../../../unordered-list';
+import {TextBlock, TextBlockProps} from '../../../text-block';
+import {GridLayout} from '../../../grid-layout';
+import {Divider} from '../../../divider';
+import {Block} from '../../../block';
+import {
+ IconFilledStarOutline,
+ IconFilledStar,
+ IconFilledFigma,
+ IconFilledAccountBalance,
+ IconFilledAccountTree,
+ IconFilledBookmarkBorder,
+} from '../../../icons';
+import {LinkInline} from '../../../link';
+import {Tag} from '../../../tag';
+import {VideoPlayer} from '../../../video-player';
+import {DEFATULT_VIDEO_PLAYER_CONFIG} from '../../../video-player/__tests__/config';
+
+const H = ({overrides, ...props}: Omit) => (
+
+ Short title of the card describing the main content
+
+);
+
+const P = ({...props}: Omit) => (
+
+ Short paragraph description of the article, outlining main story and focus.
+
+);
+
+const cardCustomThemeObject: CreateThemeArgs = {
+ name: 'card-custom-theme',
+ overrides: {
+ stylePresets: {
+ // split stories
+ firstSplitBarCustom: {
+ base: {
+ color: '{{colors.inkBase}}',
+ backgroundColor: '{{colors.interactivePrimary010}}',
+ textAlign: 'center',
+ },
+ },
+ secondSplitBarCustom: {
+ base: {
+ color: '{{colors.inkBase}}',
+ backgroundColor: '{{colors.interactivePrimary020}}',
+ textAlign: 'center',
+ },
+ },
+ // Other stories
+ cardBook: {
+ base: {
+ borderStyle: 'solid',
+ borderColor: '{{colors.interface020}}',
+ borderWidth: '{{borders.borderWidth010}}',
+ boxShadow: '{{shadows.shadow030}}',
+ borderRadius: '{{borders.borderRadiusDefault}}',
+ backgroundColor: '{{colors.interfaceBackground}}',
+ },
+ hover: {
+ boxShadow: '{{shadows.shadow060}}',
+ backgroundColor: '{{colors.interface020}}',
+ borderColor: '{{colors.interface030}}',
+ },
+ },
+ // Other stories
+ cardBookActions: {
+ base: {
+ borderStyle: 'solid',
+ borderColor: '{{colors.interface020}}',
+ borderWidth:
+ '{{borders.borderWidth010}} {{borders.borderWidth000}} {{borders.borderWidth000}} {{borders.borderWidth000}}',
+ },
+ },
+ // Other stories
+ centered: {
+ base: {
+ textAlign: 'center',
+ },
+ },
+ // Whole card as a link by applying the 'expand' prop
+ cardContentSeparateColor: {
+ base: {
+ boxShadow: '{{shadows.shadow020}}',
+ backgroundColor: '{{colors.interface010}}',
+ },
+ hover: {
+ backgroundColor: '{{colors.interface020}}',
+ },
+ },
+ // CardInset & Padding overrides
+ cardInset: {
+ base: {
+ backgroundColor: '{{colors.interface020}}',
+ },
+ },
+ // Style preset - card and flag colours
+ cardContainerWithHover: {
+ base: {
+ backgroundColor: '{{colors.interfaceInformative020}}',
+ color: '{{colors.inkBrand010}}',
+ },
+ hover: {
+ boxShadow: '{{shadows.shadow030}}',
+ backgroundColor: '{{colors.interfaceInformative010}}',
+ color: '{{colors.inkInverse}}',
+ },
+ },
+ currentColor: {
+ base: {
+ color: 'currentColor',
+ },
+ },
+ currentColorTag: {
+ base: {
+ color: 'currentColor',
+ borderStyle: 'solid',
+ borderColor: 'currentColor',
+ borderWidth: '{{borders.borderWidth010}}',
+ },
+ },
+ },
+ },
+};
+
+const href = 'javascript:void(0);';
+const areasGap = 'space050';
+const contentGap = 'space040';
+
+export const StoryDefault = () => (
+
+
+
+ Flag
+
+
+
+
+
+
+
+ Tag
+
+
+
+);
+StoryDefault.storyName = 'Default';
+
+export const StoryCardAreas = () => (
+
+
+
+
+ Flag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tag
+
+
+
+
+
+
+
+
+ Flag
+
+
+
+
+
+
+ Tag
+
+
+
+
+);
+StoryCardAreas.storyName = 'Card areas';
+
+export const StoryVariations = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flag
+
+
+
+
+
+
+
+ Tag
+
+
+
+
+
+
+
+ Flag
+
+
+
+
+
+
+
+
+
+
+
+
+ Flag
+
+
+
+
+
+ Unordered list item
+
+
+ Unordered list item
+
+
+ Unordered list item
+
+
+
+
+
+
+
+
+ Flag
+
+
+
+
+ Tag
+
+
+
+
+
+
+ Flag
+
+
+
+
+
+
+
+ Tag
+
+
+
+
+);
+StoryVariations.storyName = 'Variations';
+
+export const StoryInsetCard = () => (
+
+
+
+
+
+ Flag
+
+
+
+
+ Tag
+
+
+
+
+);
+StoryInsetCard.storyName = 'Inset card';
+
+export const StoryLayout = () => (
+
+
+
+
+
+ Flag
+
+
+
+
+ Tag
+
+
+
+
+
+
+
+ Flag
+
+
+
+
+ Tag
+
+
+
+
+);
+StoryLayout.storyName = 'Layout';
+
+const SplitBars = ({
+ columns,
+ maxWidth,
+}: {
+ columns: string;
+ maxWidth: string;
+}) => {
+ const [first, second] = columns.split(' ');
+
+ return (
+
+
+ {first}
+
+
+ {second}
+
+
+ );
+};
+
+const SplitCard = ({columns}: {columns: string}) => {
+ const maxWidth = '600px';
+ return (
+
+
+
+
+
+
+
+
+
+ Tag
+
+
+
+ );
+};
+
+export const StorySpan = () => (
+
+
+
+
+
+
+
+
+
+
+
+);
+StorySpan.storyName = 'Span';
+
+export const StoryOrder = () => (
+
+
+
+
+
+
+
+
+
+ Tag
+
+
+
+
+);
+StoryOrder.storyName = 'Order';
+
+export const StoryResponsiveCard = () => (
+
+
+
+
+
+
+
+ {[
+ 'Unordered list item',
+ 'Unordered list item',
+ 'Unordered list item',
+ ]}
+
+
+
+
+
+
+);
+StoryResponsiveCard.storyName = 'Responsive card';
+
+export const StoryLogicalProps = () => (
+
+
+
+
+ Flag
+
+
+
+
+
+
+ Tag
+
+
+
+
+
+
+
+ Flag
+
+
+
+
+
+
+ Tag
+
+
+
+
+
+
+
+ Flag
+
+
+
+
+
+
+ Tag
+
+
+
+
+
+);
+StoryLogicalProps.storyName = 'Logical props';
+
+export const StoryOverrides = () => (
+
+
+
+
+
+ Flag
+
+ {/* Unfortunately in NewsKit there is not a way for parent hover to trigger the children one
+ the easiest way to do that is using CSS currentColor */}
+
+
+
+
+
+ Tag
+
+
+
+
+
+
+
+
+ Flag
+
+
+
+
+
+
+ Tag
+
+
+
+
+
+);
+StoryOverrides.storyName = 'Styling overrides';
+
+// https://www.linkedin.com/search/results/content/?keywords=design&origin=FACETED_SEARCH&postedBy=%5B%22first%22%2C%22following%22%5D&sid=xxV&sortBy=%22relevance%22
+export const ComplexStory = () => (
+
+
+
+
+
+
+ Mountain retreat
+
+
+ Snowy Peaks, Austria
+
+
+
+
+
+
+
+
+
+ 4.1 (38 reviews)
+
+
+
+
+
+
+
+
+
+ Wi-Fi
+
+
+
+
+
+ SPA
+
+
+
+
+
+ Air-co
+
+
+
+
+
+ 24/7
+
+
+
+
+
+ Short paragraph description of the article, outlining main story and
+ focus.
+
+
+
+
+
+
+ $299
+
+
+ /night
+
+
+
+
+
+
+);
+
+ComplexStory.storyName = 'Other story';
+
+export default {
+ title: 'Components/CardComposable',
+ component: () => 'None',
+ decorators: [
+ (
+ Story: StoryType,
+ context: {name: string; globals: {backgrounds: {value: string}}},
+ ) => (
+
+
+
+ ),
+ ],
+};
diff --git a/src/card-composable/__tests__/card-composable.test.tsx b/src/card-composable/__tests__/card-composable.test.tsx
new file mode 100644
index 0000000000..070574117d
--- /dev/null
+++ b/src/card-composable/__tests__/card-composable.test.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import {renderToFragmentWithTheme} from '../../test/test-utils';
+import {
+ CardComposable,
+ CardMedia,
+ CardContent,
+ CardActions,
+ CardLink,
+} from '..';
+
+describe('CardComposable', () => {
+ test('renders defaults', () => {
+ const fragment = renderToFragmentWithTheme(CardComposable, {
+ children: (
+ <>
+
+
+ content
+
+
+
+ actions
+ >
+ ),
+ });
+ expect(fragment).toMatchSnapshot();
+ });
+
+ test('renders without areas', () => {
+ const fragment = renderToFragmentWithTheme(CardComposable, {
+ areas: '',
+ children: (
+ <>
+
+ content
+
+
+ actions
+ >
+ ),
+ });
+ expect(fragment).toMatchSnapshot();
+ });
+});
diff --git a/src/card-composable/card-composable.tsx b/src/card-composable/card-composable.tsx
new file mode 100644
index 0000000000..dc38fb45e4
--- /dev/null
+++ b/src/card-composable/card-composable.tsx
@@ -0,0 +1,130 @@
+import * as React from 'react';
+import {Image} from '../image';
+
+import {
+ StyledActions,
+ StyledCard,
+ StyledContent,
+ StyledLink,
+ StyledMedia,
+} from './styled';
+
+import {
+ CardComposableProps,
+ CardMediaProps,
+ CardContentProps,
+ CardActionsProps,
+ CardLinkProps,
+ ComponentWithOverrides,
+} from './types';
+import {useTheme} from '../theme';
+import {filterOutFalsyProperties} from '../utils/filter-object';
+import {withOwnTheme} from '../utils/with-own-theme';
+import defaults from './defaults';
+import stylePresets from './style-presets';
+import {CardProvider, useCardContext} from './context';
+
+const useGetOverrides = (
+ {overrides}: TCO,
+ componentName: string,
+) => {
+ const theme = useTheme();
+
+ return {
+ ...theme.componentDefaults[componentName],
+ ...filterOutFalsyProperties(overrides),
+ };
+};
+
+const defaultAreas = `
+ media
+ content
+ actions
+ `;
+
+const ThemelessCardComposable = React.forwardRef<
+ HTMLDivElement,
+ CardComposableProps
+>(({children, areas = defaultAreas, ...props}, ref) => {
+ const overrides = useGetOverrides(
+ props,
+ 'cardComposable',
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+});
+
+export const CardComposable = withOwnTheme(ThemelessCardComposable)({
+ defaults,
+ stylePresets,
+});
+
+export const CardMedia = React.forwardRef(
+ ({media, children, ...props}, ref) => {
+ const {useAreas} = useCardContext();
+ const overrides = useGetOverrides(props, 'cardMedia');
+ return (
+
+ {children || }
+
+ );
+ },
+);
+
+export const CardContent = React.forwardRef(
+ (props, ref) => {
+ const {useAreas} = useCardContext();
+ const overrides = useGetOverrides(
+ props,
+ 'cardContent',
+ );
+ return (
+
+ );
+ },
+);
+
+export const CardActions = React.forwardRef(
+ (props, ref) => {
+ const {useAreas} = useCardContext();
+ const overrides = useGetOverrides(
+ props,
+ 'cardActions',
+ );
+ return (
+
+ );
+ },
+);
+
+export const CardLink = React.forwardRef(
+ (props, ref) => {
+ const overrides = useGetOverrides(props, 'cardLink');
+ return ;
+ },
+);
diff --git a/src/card-composable/context.ts b/src/card-composable/context.ts
new file mode 100644
index 0000000000..1d74d78fba
--- /dev/null
+++ b/src/card-composable/context.ts
@@ -0,0 +1,22 @@
+import {createContext, useContext} from 'react';
+
+const CardContext = createContext({useAreas: false});
+
+export const CardProvider = CardContext.Provider;
+
+export const useCardContext = () => {
+ const context = useContext(CardContext);
+
+ /* istanbul ignore if */
+ if (
+ process.env.NODE_ENV !== 'production' &&
+ Object.keys(context).length === 0
+ ) {
+ // eslint-disable-next-line no-console
+ console.error(
+ 'You are using a component which needs to be a child of ',
+ );
+ }
+
+ return context;
+};
diff --git a/src/card-composable/defaults.ts b/src/card-composable/defaults.ts
new file mode 100644
index 0000000000..6763a31421
--- /dev/null
+++ b/src/card-composable/defaults.ts
@@ -0,0 +1,10 @@
+export default {
+ cardComposable: {
+ stylePreset: 'cardContainer',
+ transitionPreset: 'backgroundColorChange',
+ },
+ cardMedia: {},
+ cardContent: {},
+ cardActions: {},
+ cardLink: {},
+};
diff --git a/src/card-composable/index.ts b/src/card-composable/index.ts
new file mode 100644
index 0000000000..5c422a56cf
--- /dev/null
+++ b/src/card-composable/index.ts
@@ -0,0 +1,8 @@
+export * from './card-composable';
+export type {
+ CardComposableProps,
+ CardMediaProps,
+ CardContentProps,
+ CardActionsProps,
+ CardLinkProps,
+} from './types';
diff --git a/src/card-composable/style-presets.ts b/src/card-composable/style-presets.ts
new file mode 100644
index 0000000000..31918ce12c
--- /dev/null
+++ b/src/card-composable/style-presets.ts
@@ -0,0 +1,3 @@
+import {StylePreset} from '../theme/types';
+
+export default {} as Record;
diff --git a/src/card-composable/styled.tsx b/src/card-composable/styled.tsx
new file mode 100644
index 0000000000..3721418512
--- /dev/null
+++ b/src/card-composable/styled.tsx
@@ -0,0 +1,41 @@
+import {getStylePreset, getTransitionPreset, styled} from '../utils/style';
+
+import {GridLayout} from '../grid-layout/grid-layout';
+import {CardLinkProps, StylableGridLayout} from './types';
+import {LinkStandalone} from '../link';
+
+type StyledGridLayoutProps = StylableGridLayout & {
+ areaName?: string;
+};
+
+const StyledGrid = styled(GridLayout)`
+ ${getStylePreset('', '')};
+ ${getTransitionPreset('', '')};
+ ${({areaName}) => areaName && `grid-area: ${areaName};`}
+`;
+
+export const StyledCard = styled(StyledGrid)`
+ position: relative;
+`;
+
+export const StyledMedia = StyledGrid;
+export const StyledContent = StyledGrid;
+
+export const StyledActions = styled(StyledGrid)`
+ position: relative;
+ z-index: 2;
+`;
+
+export const StyledLink = styled(LinkStandalone)`
+ text-decoration: none;
+ ${({expand}) =>
+ expand &&
+ `
+ &:before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ }
+ `}
+`;
diff --git a/src/card-composable/types.ts b/src/card-composable/types.ts
new file mode 100644
index 0000000000..a9433df545
--- /dev/null
+++ b/src/card-composable/types.ts
@@ -0,0 +1,32 @@
+import {ReactNode} from 'react';
+import {GridLayoutProps} from '../grid-layout/types';
+import {ImageProps} from '../image';
+import {MQ} from '../utils';
+import {LinkProps} from '../link';
+import {TransitionToken} from '../theme';
+
+export type StylableGridLayout = GridLayoutProps & {
+ overrides?: {
+ stylePreset?: MQ;
+ transitionPreset?: TransitionToken | TransitionToken[];
+ };
+};
+
+export type ComponentWithOverrides = {
+ overrides?: object;
+};
+
+export type CardComposableProps = StylableGridLayout;
+
+export type CardMediaProps = {
+ media?: ImageProps;
+ children?: ReactNode;
+};
+
+export type CardContentProps = StylableGridLayout;
+
+export type CardActionsProps = StylableGridLayout;
+
+export type CardLinkProps = LinkProps & {
+ expand?: boolean;
+};
diff --git a/src/card/style-presets.ts b/src/card/style-presets.ts
index c263651330..b7190bc926 100644
--- a/src/card/style-presets.ts
+++ b/src/card/style-presets.ts
@@ -2,12 +2,6 @@ import {StylePreset} from '../theme/types';
import {defaultFocusVisible} from '../utils/default-focus-visible';
export default {
- cardContainer: {
- base: {
- color: '{{colors.inkBase}}',
- backgroundColor: '{{colors.interface010}}',
- },
- },
headlineHeadingInteractive: {
base: {
color: '{{colors.inkContrast}}',
diff --git a/src/grid-layout/__tests__/stories/grid-card.tsx b/src/grid-layout/__tests__/stories/grid-card.tsx
deleted file mode 100644
index 35bdec0b14..0000000000
--- a/src/grid-layout/__tests__/stories/grid-card.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-import * as React from 'react';
-import {Headline} from '../../../headline';
-import {TextBlock} from '../../../text-block';
-import {Block} from '../../../block';
-import {styled} from '../../../utils/style';
-import {Button} from '../../../button';
-import {Image} from '../../../image';
-import {Flag, getMediaQueryFromTheme} from '../../..';
-import {IconFilledEmail} from '../../../icons';
-import {GridLayout} from '../../grid-layout';
-
-const StyledAdvancedCard = styled(GridLayout)`
- border: 1px solid gray;
-`;
-
-const StyledLink = styled.a`
- grid-area: 1 / 1 / 2 / 3;
- z-index: 3;
- ${getMediaQueryFromTheme('md')} {
- grid-area: 1 / 1 / 3 / 3;
- }
- &:hover {
- background: rgba(0, 0, 0, 0.1);
- }
-`;
-
-export const GridCard = ({title = '', teaser = '', image = '', href = ''}) => {
- const xsLayout = `
- "thumb content"
- "tags share"
- `;
- const mdLayout = `
- "thumb thumb"
- "content content"
- "tags share"
- `;
-
- return (
-
- {Areas => (
- <>
-
-
-
-
- Flag
-
-
-
-
- {title}
-
-
-
-
- {teaser}
-
-
-
-
-
-
-
-
- {href ? : null}
- >
- )}
-
- );
-};
-
-export const GridTeaser = ({
- title = '',
- teaser = '',
- image = '',
- href = '',
-}) => {
- const xsLayout = `
- "thumb"
- "tags"
- `;
-
- return (
-
- {Areas => (
- <>
-
-
-
-
-
-
- {title}
-
-
-
-
- {teaser}
-
-
-
-
-
- {href ? : null}
- >
- )}
-
- );
-};
diff --git a/src/grid-layout/__tests__/stories/grid-layout.stories.tsx b/src/grid-layout/__tests__/stories/grid-layout.stories.tsx
index c75e603285..5bf58a815f 100644
--- a/src/grid-layout/__tests__/stories/grid-layout.stories.tsx
+++ b/src/grid-layout/__tests__/stories/grid-layout.stories.tsx
@@ -3,7 +3,6 @@ import {styled} from '../../../utils';
import {Block} from '../../../block';
import {Divider} from '../../../divider';
import {GridLayout, GridLayoutItem} from '../../grid-layout';
-import {GridCard, GridTeaser} from './grid-card';
import {GridBox} from './common';
import {Grid, Cell} from '../../../grid';
import {Label} from '../../..';
@@ -412,27 +411,6 @@ export const StoryWithLogicalPropsOverrides = () => (
StoryWithLogicalPropsOverrides.storyName = 'with-logical-props';
-export const StoryCardWithGrid = () => (
- <>
- Card with grid
-
-
-
-
-
- >
-);
-StoryCardWithGrid.storyName = 'card-with-grid';
-
export * from './the-times';
export * from './the-sun';
diff --git a/src/index.ts b/src/index.ts
index e3cba68a5b..85e1f8c3c0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,6 +10,7 @@ export * from './button';
export * from './byline';
export * from './caption';
export * from './card';
+export * from './card-composable';
export * from './character-count';
export * from './checkbox';
export * from './consent';
diff --git a/src/theme/__tests__/__snapshots__/creator.test.ts.snap b/src/theme/__tests__/__snapshots__/creator.test.ts.snap
index 34b81d716f..cb9ccf37f9 100644
--- a/src/theme/__tests__/__snapshots__/creator.test.ts.snap
+++ b/src/theme/__tests__/__snapshots__/creator.test.ts.snap
@@ -413,6 +413,12 @@ Object {
"spaceInsetStretch060": "{{sizing.sizing080}} {{sizing.sizing060}}",
},
"stylePresets": Object {
+ "cardContainer": Object {
+ "base": Object {
+ "backgroundColor": "{{colors.interface010}}",
+ "color": "{{colors.inkBase}}",
+ },
+ },
"controlLabel": Object {
"base": Object {
"color": "{{colors.inkBase}}",
diff --git a/src/theme/__tests__/__snapshots__/theme.test.ts.snap b/src/theme/__tests__/__snapshots__/theme.test.ts.snap
index 5bd3a1d6c1..04036c550d 100644
--- a/src/theme/__tests__/__snapshots__/theme.test.ts.snap
+++ b/src/theme/__tests__/__snapshots__/theme.test.ts.snap
@@ -418,6 +418,12 @@ Object {
"spaceInsetStretch060": "{{sizing.sizing080}} {{sizing.sizing060}}",
},
"stylePresets": Object {
+ "cardContainer": Object {
+ "base": Object {
+ "backgroundColor": "{{colors.interface010}}",
+ "color": "{{colors.inkBase}}",
+ },
+ },
"controlLabel": Object {
"base": Object {
"color": "{{colors.inkBase}}",
@@ -1710,6 +1716,12 @@ Object {
"paddingInline": "space000",
},
},
+ "cardActions": Object {},
+ "cardComposable": Object {
+ "stylePreset": "cardContainer",
+ "transitionPreset": "backgroundColorChange",
+ },
+ "cardContent": Object {},
"cardInset": Object {
"actionsContainer": Object {
"minHeight": "sizing000",
@@ -1746,6 +1758,8 @@ Object {
},
},
},
+ "cardLink": Object {},
+ "cardMedia": Object {},
"characterCount": Object {
"large": Object {
"marginBlockEnd": "space020",
diff --git a/src/theme/compiler/__tests__/__snapshots__/index.test.ts.snap b/src/theme/compiler/__tests__/__snapshots__/index.test.ts.snap
index 761df18fc0..6a4509c7d2 100644
--- a/src/theme/compiler/__tests__/__snapshots__/index.test.ts.snap
+++ b/src/theme/compiler/__tests__/__snapshots__/index.test.ts.snap
@@ -414,6 +414,12 @@ Object {
"spaceInsetStretch060": "48px 32px",
},
"stylePresets": Object {
+ "cardContainer": Object {
+ "base": Object {
+ "backgroundColor": "#FFFFFF",
+ "color": "#3B3B3B",
+ },
+ },
"controlLabel": Object {
"base": Object {
"color": "#3B3B3B",
diff --git a/src/theme/presets/style-presets.ts b/src/theme/presets/style-presets.ts
index 3901941022..48153a283c 100644
--- a/src/theme/presets/style-presets.ts
+++ b/src/theme/presets/style-presets.ts
@@ -179,3 +179,10 @@ stylePresets.selectOptionItemIcon = {
iconColor: '{{colors.interactiveInput040}}',
},
};
+
+stylePresets.cardContainer = {
+ base: {
+ color: '{{colors.inkBase}}',
+ backgroundColor: '{{colors.interface010}}',
+ },
+};