Repository: SonarSource/echoes-react
Pull request #303: ECHOES-654 WIP implement card component
Author: login: robert-hoover-sonarsource
Created: 2025-03-26T22:06:39Z | Merged: 2025-03-31T16:12:10Z

[ECHOES-654](https://sonarsource.atlassian.net/browse/ECHOES-654)

Part of 
<!-- 
  Only for standalone PRs without Jira issue in the PR title: 
    * Replace this comment with Epic ID to create a new Task in Jira
    * Replace this comment with Issue ID to create a new Sub-Task in Jira
    * Ignore or delete this note to create a new Task in Jira without a parent 
-->


[ECHOES-654]: https://sonarsource.atlassian.net/browse/ECHOES-654?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/src/components/card/CardBody.tsx b/src/components/card/CardBody.tsx
new file mode 100644
index 000000000..177ba1e07
--- /dev/null
+++ b/src/components/card/CardBody.tsx
@@ -0,0 +1,48 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import React, { useMemo } from 'react';
+import { useCardSize } from './CardRoot';
+import { CARD_SIZE_STYLES, CardBodyStyled } from './CardStyles';
+
+export interface CardBodyProps {
+  children: React.ReactNode;
+  className?: string;
+  insetContent?: boolean;
+}
+
+export const CardBody = React.forwardRef<HTMLDivElement, Readonly<CardBodyProps>>(
+  ({ children, className, insetContent = false }, ref) => {
+    const size = useCardSize();
+
+    return (
+      <CardBodyStyled
+        className={className}
+        css={useMemo(() => ({ ...CARD_SIZE_STYLES[size] }), [size])}
+        insetContent={insetContent}
+        ref={ref}
+        size={size}>
+        {children}
+      </CardBodyStyled>
+    );
+  },
+);
+
+CardBody.displayName = 'CardBody';
diff --git a/src/components/card/CardHeader.tsx b/src/components/card/CardHeader.tsx
new file mode 100644
index 000000000..02767407f
--- /dev/null
+++ b/src/components/card/CardHeader.tsx
@@ -0,0 +1,108 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import React, { useMemo } from 'react';
+import { Heading } from '../typography';
+import { useCardSize } from './CardRoot';
+import { CARD_HEADER_SIZE_STYLES, CardHeaderStyled } from './CardStyles';
+import { Divider } from '../divider';
+import { CardSize } from './CardSize';
+
+export interface CardHeaderProps {
+  title: React.ReactNode;
+  description?: React.ReactNode;
+  hasDivider?: boolean;
+  rightContent?: React.ReactNode;
+  className?: string;
+}
+
+const CARD_SIZE_CONFIG = {
+  [CardSize.Small]: {
+    headingLevel: 'h4' as const,
+    styles: CARD_HEADER_SIZE_STYLES[CardSize.Small],
+  },
+  [CardSize.Medium]: {
+    headingLevel: 'h3' as const,
+    styles: CARD_HEADER_SIZE_STYLES[CardSize.Medium],
+  },
+  [CardSize.Large]: {
+    headingLevel: 'h2' as const,
+    styles: CARD_HEADER_SIZE_STYLES[CardSize.Large],
+  },
+};
+
+export const CardHeader = React.forwardRef<HTMLDivElement, Readonly<CardHeaderProps>>(
+  ({ className, description, hasDivider = false, rightContent, title }, ref) => {
+    const size = useCardSize();
+
+    const sizeConfig = CARD_SIZE_CONFIG[size];
+
+    return (
+      <>
+        <CardHeaderStyled
+          className={className}
+          css={useMemo(
+            () => ({
+              ...sizeConfig.styles,
+            }),
+            [sizeConfig.styles],
+          )}
+          ref={ref}>
+          <CardHeaderContentStyled>
+            <CardHeaderTextStyled>
+              <Heading as={sizeConfig.headingLevel}>{title}</Heading>
+              {rightContent && <RightContentStyled>{rightContent}</RightContentStyled>}
+            </CardHeaderTextStyled>
+
+            {description && <DescriptionStyled>{description}</DescriptionStyled>}
+          </CardHeaderContentStyled>
+        </CardHeaderStyled>
+
+        {hasDivider && <Divider />}
+      </>
+    );
+  },
+);
+
+CardHeader.displayName = 'CardHeader';
+
+const CardHeaderContentStyled = styled.div`
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+`;
+
+const CardHeaderTextStyled = styled.div`
+  align-items: center;
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+`;
+
+const RightContentStyled = styled.div`
+  display: flex;
+`;
+
+const DescriptionStyled = styled.div`
+  align-items: center;
+  diplay: flex;
+  justify-content: space-between;
+`;
diff --git a/src/components/card/CardRoot.tsx b/src/components/card/CardRoot.tsx
new file mode 100644
index 000000000..11e8aef0d
--- /dev/null
+++ b/src/components/card/CardRoot.tsx
@@ -0,0 +1,47 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import React, { createContext } from 'react';
+import { CardStyled } from './CardStyles';
+import { CardSize } from './CardSize';
+
+export interface CardProps {
+  children: React.ReactNode;
+  className?: string;
+  size?: CardSize;
+}
+
+const CardContext = createContext<CardSize>(CardSize.Medium);
+
+export function useCardSize() {
+  return React.useContext(CardContext);
+}
+
+export const CardRoot = React.forwardRef<HTMLDivElement, Readonly<CardProps>>(
+  ({ children, className, size = CardSize.Medium }, ref) => (
+    <CardContext.Provider value={size}>
+      <CardStyled className={className} ref={ref}>
+        {children}
+      </CardStyled>
+    </CardContext.Provider>
+  ),
+);
+
+CardRoot.displayName = 'CardRoot';
diff --git a/src/components/card/CardSize.ts b/src/components/card/CardSize.ts
new file mode 100644
index 000000000..67cdc954d
--- /dev/null
+++ b/src/components/card/CardSize.ts
@@ -0,0 +1,25 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+export enum CardSize {
+  Small = 'small',
+  Medium = 'medium',
+  Large = 'large',
+}
diff --git a/src/components/card/CardStyles.tsx b/src/components/card/CardStyles.tsx
new file mode 100644
index 000000000..1a88ea4c3
--- /dev/null
+++ b/src/components/card/CardStyles.tsx
@@ -0,0 +1,84 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { CardSize } from './CardSize';
+
+export const CardStyled = styled.div`
+  background-color: var(--echoes-color-background-default);
+  border: var(--echoes-border-width-default) solid var(--echoes-color-border-weak);
+  border-radius: var(--echoes-border-radius-400);
+  box-shadow: var(--echoes-box-shadow-xsmall);
+
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  min-width: 470px;
+  width: 100%;
+`;
+
+export const CardHeaderStyled = styled.header`
+  align-items: center;
+  display: flex;
+  min-height: var(--card-header-min-height);
+  padding: var(--card-header-padding);
+`;
+
+export const CardBodyStyled = styled.div<{ size: CardSize; insetContent: boolean }>`
+  box-sizing: border-box;
+  min-height: var(--card-body-min-height);
+  padding: ${(props) => (props.insetContent ? '0' : 'var(--card-padding)')};
+  width: 100%;
+
+  & > * {
+    height: 100%;
+  }
+`;
+
+export const CARD_HEADER_SIZE_STYLES = {
+  [CardSize.Small]: {
+    '--card-header-padding': `var(--echoes-dimension-space-100) var(--echoes-dimension-space-150)`,
+    '--card-header-min-height': '36px',
+  },
+  [CardSize.Medium]: {
+    '--card-header-padding': `var(--echoes-dimension-space-150) var(--echoes-dimension-space-200)`,
+    '--card-header-min-height': '45px',
+  },
+  [CardSize.Large]: {
+    '--card-header-padding': `var(--echoes-dimension-space-200) var(--echoes-dimension-space-300)`,
+    '--card-header-min-height': '58px',
+  },
+};
+
+export const CARD_SIZE_STYLES = {
+  [CardSize.Small]: {
+    '--card-padding': 'var(--echoes-dimension-space-150)',
+    '--card-body-min-height': '84px',
+  },
+  [CardSize.Medium]: {
+    '--card-padding': 'var(--echoes-dimension-space-200)',
+    '--card-body-min-height': '92px',
+  },
+  [CardSize.Large]: {
+    '--card-padding': 'var(--echoes-dimension-space-300)',
+    '--card-body-min-height': '108px',
+  },
+};
diff --git a/src/components/card/__tests__/Card-test.tsx b/src/components/card/__tests__/Card-test.tsx
new file mode 100644
index 000000000..e9b96132e
--- /dev/null
+++ b/src/components/card/__tests__/Card-test.tsx
@@ -0,0 +1,199 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import React from 'react';
+
+import { CardRoot } from '../CardRoot';
+import { CardHeader } from '../CardHeader';
+import { CardBody } from '../CardBody';
+import { render } from '~common/helpers/test-utils';
+import { CardSize } from '../CardSize';
+
+describe('Card components', () => {
+  describe('CardRoot', () => {
+    it('renders children correctly', () => {
+      render(<CardRoot>Test Content</CardRoot>);
+      expect(screen.getByText('Test Content')).toBeInTheDocument();
+    });
+
+    it('applies custom className', () => {
+      render(<CardRoot className="custom-class">Content</CardRoot>);
+      const card = screen.getByText('Content');
+      expect(card).toBeInTheDocument();
+      expect(card).toHaveClass('custom-class');
+    });
+
+    it('forwards ref correctly', () => {
+      const ref = React.createRef<HTMLDivElement>();
+      render(<CardRoot ref={ref}>Content</CardRoot>);
+      expect(ref.current).not.toBeNull();
+    });
+
+    it('uses medium size by default', () => {
+      render(
+        <CardRoot>
+          <CardHeader title="Default Size Card" />
+        </CardRoot>,
+      );
+      expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Default Size Card');
+    });
+  });
+
+  describe('CardHeader', () => {
+    it('renders title correctly', () => {
+      render(
+        <CardRoot>
+          <CardHeader title="Card Title" />
+        </CardRoot>,
+      );
+      expect(screen.getByText('Card Title')).toBeInTheDocument();
+    });
+
+    it('renders description when provided', () => {
+      render(
+        <CardRoot>
+          <CardHeader description="Card Description" title="Card Title" />
+        </CardRoot>,
+      );
+      expect(screen.getByText('Card Description')).toBeInTheDocument();
+    });
+
+    it('renders right content when provided', () => {
+      render(
+        <CardRoot>
+          <CardHeader rightContent={<button type="button">Action</button>} title="Card Title" />
+        </CardRoot>,
+      );
+      expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument();
+    });
+
+    it('uses correct heading level based on card size', () => {
+      const { rerender } = render(
+        <CardRoot size={CardSize.Small}>
+          <CardHeader title="Small Card" />
+        </CardRoot>,
+      );
+
+      expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Small Card');
+
+      rerender(
+        <CardRoot size={CardSize.Medium}>
+          <CardHeader title="Medium Card" />
+        </CardRoot>,
+      );
+      expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Medium Card');
+
+      rerender(
+        <CardRoot size={CardSize.Large}>
+          <CardHeader title="Large Card" />
+        </CardRoot>,
+      );
+      expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Large Card');
+    });
+  });
+
+  describe('CardBody', () => {
+    it('renders children correctly', () => {
+      render(
+        <CardRoot>
+          <CardBody>Body Content</CardBody>
+        </CardRoot>,
+      );
+      expect(screen.getByText('Body Content')).toBeInTheDocument();
+    });
+
+    it('applies custom className', () => {
+      render(
+        <CardRoot>
+          <CardBody className="custom-body-class">Content</CardBody>
+        </CardRoot>,
+      );
+      const body = screen.getByText('Content');
+      expect(body).toHaveClass('custom-body-class');
+    });
+
+    it('forwards ref correctly', () => {
+      const ref = React.createRef<HTMLDivElement>();
+      render(
+        <CardRoot>
+          <CardBody ref={ref}>Content</CardBody>
+        </CardRoot>,
+      );
+      expect(ref.current).not.toBeNull();
+    });
+
+    it('applies insetContent prop correctly', () => {
+      const { rerender } = render(
+        <CardRoot>
+          <CardBody>Content</CardBody>
+        </CardRoot>,
+      );
+
+      const bodyWithPadding = screen.getByText('Content');
+      const styleWithPadding = window.getComputedStyle(bodyWithPadding);
+
+      expect(styleWithPadding.padding).not.toBe('0px');
+
+      rerender(
+        <CardRoot>
+          <CardBody insetContent>Content</CardBody>
+        </CardRoot>,
+      );
+
+      const bodyWithoutPadding = screen.getByText('Content');
+      const styleWithoutPadding = window.getComputedStyle(bodyWithoutPadding);
+
+      expect(styleWithoutPadding.padding).toBe('0px');
+    });
+  });
+
+  describe('Card integrated tests', () => {
+    it('renders a complete card with header and body', () => {
+      render(
+        <CardRoot>
+          <CardHeader description="Card Description" title="Card Title" />
+          <CardBody>Card Content</CardBody>
+        </CardRoot>,
+      );
+
+      expect(screen.getByText('Card Title')).toBeInTheDocument();
+      expect(screen.getByText('Card Description')).toBeInTheDocument();
+      expect(screen.getByText('Card Content')).toBeInTheDocument();
+    });
+
+    it('maintains the expected structure with all components', () => {
+      render(
+        <CardRoot className="card-root">
+          <CardHeader
+            description="Card Description"
+            hasDivider
+            rightContent={<span>Actions</span>}
+            title="Card Title"
+          />
+          <CardBody>Card Content</CardBody>
+        </CardRoot>,
+      );
+
+      expect(screen.getByText('Card Title')).toBeInTheDocument();
+      expect(screen.getByText('Card Description')).toBeInTheDocument();
+      expect(screen.getByText('Actions')).toBeInTheDocument();
+    });
+  });
+});
diff --git a/src/components/card/index.ts b/src/components/card/index.ts
new file mode 100644
index 000000000..d531f33ed
--- /dev/null
+++ b/src/components/card/index.ts
@@ -0,0 +1,123 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { CardBody } from './CardBody';
+import { CardHeader } from './CardHeader';
+import { CardRoot } from './CardRoot';
+
+export { CardSize } from './CardSize';
+
+/**
+ * {@link CardRoot | Card} is a component for displaying content in a contained, styled container.
+ * It provides a flexible card layout system.
+ *
+ * **Available sizes**
+ * - SMALL: Compact card with minimal padding
+ * - MEDIUM: Standard card size (default)
+ * - LARGE: Expanded card with increased padding
+ *
+ * **Permitted Content**
+ *
+ * Exactly one {@link CardBody | Card.Body} component and optionally one
+ * {@link CardHeader | Card.Header} component.
+ *
+ * **Example**
+ *
+ * ```tsx
+ * <Card>
+ *   <Card.Header>Card Title</Card.Header>
+ *   <Card.Body>Card Content</Card.Body>
+ * </Card>
+ * ```
+ *
+ * **Example with size**
+ *
+ * ```tsx
+ * <Card size={CardSize.LARGE}>
+ *   <Card.Header>Large Card</Card.Header>
+ *   <Card.Body>Content for a large card</Card.Body>
+ * </Card>
+ * ```
+ *
+ * **Example without header**
+ *
+ * ```tsx
+ * <Card>
+ *   <Card.Body>Card content without a header</Card.Body>
+ * </Card>
+ * ```
+ */
+export const Card = Object.assign(CardRoot, {
+  /**
+   * {@link CardHeader | Card.Header} provides a consistently styled header section for the Card component.
+   * Automatically inherits size context from parent Card component.
+   *
+   * **Props**
+   * - `title`: React.ReactNode - Required title content
+   * - `description?`: React.ReactNode - Optional description text below the title
+   * - `hasDivider?`: boolean - When true, renders a divider below the header (default: false)
+   * - `rightContent?`: React.ReactNode - Optional content to display on the right side
+   * - `className?`: string - Optional CSS class name
+   *
+   * **Example with description and right content**
+   *
+   * ```tsx
+   * <Card.Header
+   *   title="Card Title"
+   *   description="Additional information about this card"
+   *   rightContent={<Button>Action</Button>}
+   * />
+   * ```
+   *
+   * **Example with divider**
+   *
+   * ```tsx
+   * <Card.Header title="Card Title" hasDivider />
+   * ```
+   */
+  Header: CardHeader,
+
+  /**
+   * {@link CardBody | Card.Body} is the container for the main content of a Card.
+   * Automatically inherits size context from parent Card component.
+   *
+   * **Props**
+   * - `children`: React.ReactNode - Required content to display in the body
+   * - `className?`: string - Optional CSS class name
+   * - `insetContent?`: boolean - When true, removes padding from the body (default: false)
+   *
+   * **Example standard usage**
+   *
+   * ```tsx
+   * <Card.Body>
+   *   <p>This is the content of the card.</p>
+   * </Card.Body>
+   * ```
+   *
+   * **Example with inset content**
+   *
+   * ```tsx
+   * <Card.Body insetContent>
+   *   <div style={{ padding: '8px' }}>Custom padded content</div>
+   * </Card.Body>
+   * ```
+   */
+  Body: CardBody,
+});
diff --git a/src/components/index.ts b/src/components/index.ts
index ae1f7e04b..f9deee11f 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -21,6 +21,7 @@
 export * from './badges';
 export * from './breadcrumbs';
 export * from './buttons';
+export * from './card';
 export * from './checkbox';
 export * from './checkbox-group';
 export * from './divider';
diff --git a/stories/Card-stories.tsx b/stories/Card-stories.tsx
new file mode 100644
index 000000000..addd54168
--- /dev/null
+++ b/stories/Card-stories.tsx
@@ -0,0 +1,235 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+  Button,
+  ButtonVariety,
+  Card,
+  CardSize,
+  IconCheckCircle,
+  IconSparkleInShieldDisabled,
+  IconStar,
+  Label,
+  LinkStandalone,
+  RatingBadge,
+  Text,
+  TextSize,
+  Tooltip,
+} from '../src';
+import { basicWrapperDecorator } from './helpers/BasicWrapper';
+
+const meta: Meta<typeof Card> = {
+  title: 'Echoes/Card',
+  component: Card,
+  argTypes: {
+    size: {
+      control: 'select',
+      options: Object.values(CardSize),
+      defaultValue: CardSize.Medium,
+    },
+  },
+  decorators: [basicWrapperDecorator],
+};
+
+export default meta;
+type Story = StoryObj<typeof Card>;
+
+const CardBodyStyled = styled(Card.Body)`
+  display: flex;
+  align-items: center;
+`;
+
+export const Default: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+              <Text>Super awesome description</Text>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <IconSparkleInShieldDisabled />
+                <Text as="p" size={TextSize.Small}>
+                  Helpful text
+                </Text>
+              </div>
+            </div>
+          }
+          hasDivider
+          rightContent={<IconCheckCircle color="echoes-color-icon-success" />}
+          title="Card Title"
+        />
+        <CardBodyStyled>
+          <Text as="div" className="sw-p-10">
+            This is the content of the card.
+          </Text>
+        </CardBodyStyled>
+      </Card>
+    </div>
+  ),
+};
+
+export const WithRightContent: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <span>
+              <strong>Last analysis:</strong>
+              3/21/2025, 3:32 PM
+            </span>
+          }
+          hasDivider
+          rightContent={
+            <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
+              <Button variety={ButtonVariety.Primary}>Action</Button>
+            </div>
+          }
+          title={
+            <div style={{ display: 'flex', gap: '4px' }}>
+              <Tooltip content="This is a tooltip">
+                <IconStar color="echoes-color-icon-info" isFilled />
+              </Tooltip>
+              <LinkStandalone to="/">Organization</LinkStandalone>
+              {'/'}
+              <LinkStandalone title="Cool Project" to="/">
+                Cool Project
+              </LinkStandalone>
+            </div>
+          }
+        />
+        <CardBodyStyled>
+          <div
+            style={{
+              display: 'flex',
+              gap: '2rem',
+              width: '100%',
+            }}>
+            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+              <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                <RatingBadge rating="B" size="md" />
+                <strong>0</strong>
+              </div>
+              <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Security</Label>
+            </div>
+            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+              <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                <RatingBadge rating="C" size="md" />
+                <strong>1</strong>
+              </div>
+              <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Reliability</Label>
+            </div>
+            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+              <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                <RatingBadge rating="A" size="md" />
+                <strong>100%</strong>
+              </div>
+              <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Maintainability</Label>
+            </div>
+          </div>
+        </CardBodyStyled>
+      </Card>
+    </div>
+  ),
+};
+
+export const Sizes: Story = {
+  render: () => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      {Object.values(CardSize).map((size) => (
+        <Card key={size} size={size}>
+          <Card.Header
+            description={`A ${size} card`}
+            hasDivider
+            rightContent="some right content"
+            title={`${size.charAt(0).toUpperCase() + size.slice(1)} Card`}
+          />
+          <CardBodyStyled>
+            <Text as="div">Content for the {size} card.</Text>
+          </CardBodyStyled>
+        </Card>
+      ))}
+    </div>
+  ),
+};
+
+export const NoDescription: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header hasDivider title="Header Without Description" />
+        <CardBodyStyled>
+          <Text as="div">This card has a header with title but no description.</Text>
+        </CardBodyStyled>
+      </Card>
+    </div>
+  ),
+};
+
+export const BodyOnly: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <CardBodyStyled>
+          <Text as="div">This card has no header, only a body.</Text>
+        </CardBodyStyled>
+      </Card>
+    </div>
+  ),
+};
+
+export const NoDivider: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description="This card has a title and description but no divider between header and body"
+          title="Card Without Divider"
+        />
+        <CardBodyStyled>
+          <div style={{ display: 'flex', alignItems: 'center' }}>
+            <Text as="div">
+              Without a divider, the header and body content flow together visually. This works well
+              for simpler cards where visual separation isnt needed.
+            </Text>
+          </div>
+        </CardBodyStyled>
+      </Card>
+    </div>
+  ),
+};


Reviews:
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T10:39:28Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T10:42:04Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T10:43:32Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T10:45:36Z
user: login: robert-hoover-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T19:43:26Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T09:31:46Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T09:33:23Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T09:34:49Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T09:37:10Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T09:38:18Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T09:38:33Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T09:46:20Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T10:00:27Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T10:03:47Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T10:05:31Z
user: login: david-cho-lerat-sonarsource
body: Good job, but I still have a few questions/concerns :sweat_smile: 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T10:06:39Z
user: login: robert-hoover-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T14:19:38Z
user: login: robert-hoover-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T14:24:25Z
user: login: robert-hoover-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T14:31:44Z
user: login: robert-hoover-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T14:46:47Z
user: login: robert-hoover-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T14:47:24Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T14:48:33Z
user: login: robert-hoover-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T15:12:31Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T15:26:15Z
user: login: robert-hoover-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T15:34:32Z
user: login: robert-hoover-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T16:03:27Z
user: login: david-cho-lerat-sonarsource
body: Looks great, good job! :muscle: 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T16:08:50Z

Review comments:
diff_hunk: @@ -0,0 +1,57 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import React from 'react';
+import { CardSize } from './CardRoot';
+
+export interface CardBodyProps {
+  children: React.ReactNode;
+  className?: string;
+  insetContent?: boolean;
+  size?: CardSize;
+}
+
+const paddingMap = {
+  [CardSize.Small]: 'var(--echoes-dimension-space-150)',
+  [CardSize.Medium]: 'var(--echoes-dimension-space-200)',
+  [CardSize.Large]: 'var(--echoes-dimension-space-300)',
+};
+
+export const CardBody = React.forwardRef<HTMLDivElement, Readonly<CardBodyProps>>(
+  ({ children, className, insetContent = false, size = CardSize.Medium }, ref) => {
+    return (
+      <StyledBody className={className} insetContent={insetContent} ref={ref} size={size}>
path: src/components/card/CardBody.tsx
user: login: david-cho-lerat-sonarsource
body: NP: could be: `{...{ className, insetContent, ref, size }}`
created_at: 2025-03-27T10:39:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,107 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import React from 'react';
+import { Heading, Text, TextSize } from '../typography';
+import { CardSize } from './CardRoot';
+
+export interface CardHeaderProps {
+  title: React.ReactNode;
+  description?: React.ReactNode;
+  hasDivider?: boolean;
+  rightContent?: React.ReactNode;
+  className?: string;
+  size?: CardSize;
+}
+
+const paddingMap = {
+  [CardSize.Small]: {
+    vertical: 'var(--echoes-dimension-space-100)',
+    horizontal: 'var(--echoes-dimension-space-150)',
+  },
+  [CardSize.Medium]: {
+    vertical: 'var(--echoes-dimension-space-150)',
+    horizontal: 'var(--echoes-dimension-space-200)',
+  },
+  [CardSize.Large]: {
+    vertical: 'var(--echoes-dimension-space-200)',
+    horizontal: 'var(--echoes-dimension-space-300)',
+  },
+};
+
+const textSizeMap: Record<CardSize, TextSize> = {
+  [CardSize.Small]: TextSize.Small,
+  [CardSize.Medium]: TextSize.Default,
+  [CardSize.Large]: TextSize.Large,
+};
+
+export const CardHeader = React.forwardRef<HTMLDivElement, Readonly<CardHeaderProps>>(
+  (
+    { className, description, hasDivider = false, rightContent, size = CardSize.Medium, title },
+    ref,
+  ) => {
+    const headerLevel: Record<CardSize, 'h2' | 'h3' | 'h4'> = {
+      [CardSize.Small]: 'h4',
+      [CardSize.Medium]: 'h3',
+      [CardSize.Large]: 'h2',
+    };
+    return (
path: src/components/card/CardHeader.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

    return (
```
created_at: 2025-03-27T10:42:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,57 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import React from 'react';
+import { CardSize } from './CardRoot';
+
+export interface CardBodyProps {
+  children: React.ReactNode;
+  className?: string;
+  insetContent?: boolean;
+  size?: CardSize;
+}
+
+const paddingMap = {
+  [CardSize.Small]: 'var(--echoes-dimension-space-150)',
+  [CardSize.Medium]: 'var(--echoes-dimension-space-200)',
+  [CardSize.Large]: 'var(--echoes-dimension-space-300)',
+};
+
+export const CardBody = React.forwardRef<HTMLDivElement, Readonly<CardBodyProps>>(
+  ({ children, className, insetContent = false, size = CardSize.Medium }, ref) => {
+    return (
path: src/components/card/CardBody.tsx
user: login: david-cho-lerat-sonarsource
body: you don't need `=> { return (<StyleBody`, this is an arrow function, just use `=> (<StyleBody`
created_at: 2025-03-27T10:43:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { CardBody } from './CardBody';
+import { CardHeader } from './CardHeader';
+import { CardRoot } from './CardRoot';
+
+export { CardSize } from './CardRoot';
+
+export const Card = Object.assign(CardRoot, {
path: src/components/card/index.ts
user: login: david-cho-lerat-sonarsource
body: `Object.assign`??
created_at: 2025-03-27T10:45:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { CardBody } from './CardBody';
+import { CardHeader } from './CardHeader';
+import { CardRoot } from './CardRoot';
+
+export { CardSize } from './CardRoot';
+
+export const Card = Object.assign(CardRoot, {
path: src/components/card/index.ts
user: login: robert-hoover-sonarsource
body: This is a pattern that was used in the Form component, and Jay and I also used in the GlobalNavigation. I believe the benefit is to have all of the TSDocs in one location. @jeremy-davis-sonarsource is that right?
created_at: 2025-03-28T19:43:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { CardBody } from './CardBody';
+import { CardHeader } from './CardHeader';
+import { CardRoot } from './CardRoot';
+
+export { CardSize } from './CardRoot';
+
+export const Card = Object.assign(CardRoot, {
path: src/components/card/index.ts
user: login: david-cho-lerat-sonarsource
body: OK, but why `Object.assign`, though? Wouldn't `export const Card = { ...CardRoot, Header: CardHeader,` work?
created_at: 2025-03-31T09:31:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+export enum CardSize {
path: src/components/card/CardTypes.ts
user: login: david-cho-lerat-sonarsource
body: the enum is called CardSize and the file CardTypes.ts, this is slightly confusing
created_at: 2025-03-31T09:33:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+export enum CardSize {
path: src/components/card/CardTypes.ts
user: login: david-cho-lerat-sonarsource
body: it's not a type per se, and there's only one export, maybe the file name should be CardSize.ts, or even there shouldn't be a file at all and the enum should be part of the CardRoot file, like we did for other components?
created_at: 2025-03-31T09:34:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import React, { createContext } from 'react';
+import { CardStyled } from './CardStyles';
+import { CardSize } from './CardTypes';
+
+export interface CardProps {
+  children: React.ReactNode;
+  className?: string;
+  size?: CardSize;
+}
+
+const CardContext = createContext<CardSize>(CardSize.Medium);
+
+export function useCardContext() {
path: src/components/card/CardRoot.tsx
user: login: david-cho-lerat-sonarsource
body: there's only one piece of information: the card size. I'd say this should be called useCardSize
created_at: 2025-03-31T09:37:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import React, { useMemo } from 'react';
+import { Heading } from '../typography';
+import { useCardContext } from './CardRoot';
+import { CARD_HEADER_SIZE_STYLES, CardHeaderStyled } from './CardStyles';
+import { Divider } from '../divider';
+import { CardSize } from './CardTypes';
+
+export interface CardHeaderProps {
+  title: React.ReactNode;
+  description?: React.ReactNode;
+  hasDivider?: boolean;
+  rightContent?: React.ReactNode;
+  className?: string;
+}
+
+const CARD_SIZE_CONFIG = {
+  [CardSize.Small]: {
+    headingLevel: 'h4' as const,
+    styles: CARD_HEADER_SIZE_STYLES[CardSize.Small],
+  },
+  [CardSize.Medium]: {
+    headingLevel: 'h3' as const,
+    styles: CARD_HEADER_SIZE_STYLES[CardSize.Medium],
+  },
+  [CardSize.Large]: {
+    headingLevel: 'h2' as const,
+    styles: CARD_HEADER_SIZE_STYLES[CardSize.Large],
+  },
+};
+
+export const CardHeader = React.forwardRef<HTMLDivElement, Readonly<CardHeaderProps>>(
+  ({ className, description, hasDivider = false, rightContent, title }, ref) => {
+    const size = useCardContext();
+
+    const sizeConfig = CARD_SIZE_CONFIG[size];
+
+    return (
+      <>
+        <CardHeaderStyled
+          className={className}
+          css={useMemo(
+            () => ({
+              ...sizeConfig.styles,
+            }),
+            [sizeConfig.styles],
+          )}
+          ref={ref}>
+          <CardHeaderContentStyled>
+            <CardHeaderTextStyled>
+              <Heading as={sizeConfig.headingLevel}>{title}</Heading>
+              {rightContent && <RightContentStyled>{rightContent}</RightContentStyled>}
+            </CardHeaderTextStyled>
+            {description && <DescriptionStyled>{description}</DescriptionStyled>}
path: src/components/card/CardHeader.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

            {description && <DescriptionStyled>{description}</DescriptionStyled>}
```
created_at: 2025-03-31T09:38:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import React, { useMemo } from 'react';
+import { Heading } from '../typography';
+import { useCardContext } from './CardRoot';
+import { CARD_HEADER_SIZE_STYLES, CardHeaderStyled } from './CardStyles';
+import { Divider } from '../divider';
+import { CardSize } from './CardTypes';
+
+export interface CardHeaderProps {
+  title: React.ReactNode;
+  description?: React.ReactNode;
+  hasDivider?: boolean;
+  rightContent?: React.ReactNode;
+  className?: string;
+}
+
+const CARD_SIZE_CONFIG = {
+  [CardSize.Small]: {
+    headingLevel: 'h4' as const,
+    styles: CARD_HEADER_SIZE_STYLES[CardSize.Small],
+  },
+  [CardSize.Medium]: {
+    headingLevel: 'h3' as const,
+    styles: CARD_HEADER_SIZE_STYLES[CardSize.Medium],
+  },
+  [CardSize.Large]: {
+    headingLevel: 'h2' as const,
+    styles: CARD_HEADER_SIZE_STYLES[CardSize.Large],
+  },
+};
+
+export const CardHeader = React.forwardRef<HTMLDivElement, Readonly<CardHeaderProps>>(
+  ({ className, description, hasDivider = false, rightContent, title }, ref) => {
+    const size = useCardContext();
+
+    const sizeConfig = CARD_SIZE_CONFIG[size];
+
+    return (
+      <>
+        <CardHeaderStyled
+          className={className}
+          css={useMemo(
+            () => ({
+              ...sizeConfig.styles,
+            }),
+            [sizeConfig.styles],
+          )}
+          ref={ref}>
+          <CardHeaderContentStyled>
+            <CardHeaderTextStyled>
+              <Heading as={sizeConfig.headingLevel}>{title}</Heading>
+              {rightContent && <RightContentStyled>{rightContent}</RightContentStyled>}
+            </CardHeaderTextStyled>
+            {description && <DescriptionStyled>{description}</DescriptionStyled>}
+          </CardHeaderContentStyled>
+        </CardHeaderStyled>
+        {hasDivider && <Divider />}
path: src/components/card/CardHeader.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

        {hasDivider && <Divider />}
```
created_at: 2025-03-31T09:38:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+  Button,
+  ButtonVariety,
+  Card,
+  CardSize,
+  IconCheckCircle,
+  IconSparkleInShieldDisabled,
+  IconStar,
+  Label,
+  LinkStandalone,
+  RatingBadge,
+  Text,
+  TextSize,
+  Tooltip,
+} from '../src';
+import { basicWrapperDecorator } from './helpers/BasicWrapper';
+
+const meta: Meta<typeof Card> = {
+  title: 'Echoes/Card',
+  component: Card,
+  argTypes: {
+    size: {
+      control: 'select',
+      options: Object.values(CardSize),
+      defaultValue: CardSize.Medium,
+    },
+  },
+  decorators: [basicWrapperDecorator],
+};
+
+export default meta;
+type Story = StoryObj<typeof Card>;
+
+const BodyContainer = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+export const Default: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+              <Text>Super awesome description</Text>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <IconSparkleInShieldDisabled />
+                <Text as="p" size={TextSize.Small}>
+                  Helpful text
+                </Text>
+              </div>
+            </div>
+          }
+          hasDivider
+          rightContent={<IconCheckCircle color="echoes-color-icon-success" />}
+          title="Card Title"
+        />
+        <Card.Body>
+          <BodyContainer>
path: stories/Card-stories.tsx
user: login: david-cho-lerat-sonarsource
body: this seems a bit overkill to have an extra layer here, we should have a `styled(Card.Body)` instead with the flex props
created_at: 2025-03-31T09:46:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+  Button,
+  ButtonVariety,
+  Card,
+  CardSize,
+  IconCheckCircle,
+  IconSparkleInShieldDisabled,
+  IconStar,
+  Label,
+  LinkStandalone,
+  RatingBadge,
+  Text,
+  TextSize,
+  Tooltip,
+} from '../src';
+import { basicWrapperDecorator } from './helpers/BasicWrapper';
+
+const meta: Meta<typeof Card> = {
+  title: 'Echoes/Card',
+  component: Card,
+  argTypes: {
+    size: {
+      control: 'select',
+      options: Object.values(CardSize),
+      defaultValue: CardSize.Medium,
+    },
+  },
+  decorators: [basicWrapperDecorator],
+};
+
+export default meta;
+type Story = StoryObj<typeof Card>;
+
+const BodyContainer = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+export const Default: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+              <Text>Super awesome description</Text>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <IconSparkleInShieldDisabled />
+                <Text as="p" size={TextSize.Small}>
+                  Helpful text
+                </Text>
+              </div>
+            </div>
+          }
+          hasDivider
+          rightContent={<IconCheckCircle color="echoes-color-icon-success" />}
+          title="Card Title"
+        />
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div" className="sw-p-10">
+              This is the content of the card.
+            </Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const WithRightContent: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <span>
+              <strong>Last analysis:</strong>
+              3/21/2025, 3:32 PM
+            </span>
+          }
+          hasDivider
+          rightContent={
+            <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
+              <Button variety={ButtonVariety.Primary}>Action</Button>
+            </div>
+          }
+          title={
+            <div style={{ display: 'flex', gap: '4px' }}>
+              <Tooltip content="This is a tooltip">
+                <IconStar color="echoes-color-icon-info" isFilled />
+              </Tooltip>
+              <LinkStandalone to="/">Organization</LinkStandalone>
+              {'/'}
+              <LinkStandalone title="Cool Project" to="/">
+                Cool Project
+              </LinkStandalone>
+            </div>
+          }
+        />
+        <Card.Body>
+          <BodyContainer>
+            <div
+              style={{
+                display: 'flex',
+                gap: '2rem',
+                width: '100%',
+              }}>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="B" size="md" />
+                  <strong>0</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Security</Label>
+              </div>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="C" size="md" />
+                  <strong>1</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Reliability</Label>
+              </div>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="A" size="md" />
+                  <strong>100%</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Maintainability</Label>
+              </div>
+            </div>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const Sizes: Story = {
+  render: () => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      {Object.values(CardSize).map((size) => (
+        <Card key={size} size={size}>
+          <Card.Header
+            description={`A ${size} card`}
+            hasDivider
+            rightContent="some right content"
+            title={`${size.charAt(0).toUpperCase() + size.slice(1)} Card`}
+          />
+          <Card.Body>
+            <BodyContainer>
+              <Text as="div">Content for the {size} card.</Text>
+            </BodyContainer>
+          </Card.Body>
+        </Card>
+      ))}
+    </div>
+  ),
+};
+
+export const NoDescription: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header hasDivider title="Header Without Description" />
path: stories/Card-stories.tsx
user: login: david-cho-lerat-sonarsource
body: Shouldn't the header be vertically aligned automatically in this case? The spacing looks weird, here:

![image](https://github.com/user-attachments/assets/d834b25c-00a5-48c7-a4ed-ff18b8a240ab)

created_at: 2025-03-31T10:00:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+  Button,
+  ButtonVariety,
+  Card,
+  CardSize,
+  IconCheckCircle,
+  IconSparkleInShieldDisabled,
+  IconStar,
+  Label,
+  LinkStandalone,
+  RatingBadge,
+  Text,
+  TextSize,
+  Tooltip,
+} from '../src';
+import { basicWrapperDecorator } from './helpers/BasicWrapper';
+
+const meta: Meta<typeof Card> = {
+  title: 'Echoes/Card',
+  component: Card,
+  argTypes: {
+    size: {
+      control: 'select',
+      options: Object.values(CardSize),
+      defaultValue: CardSize.Medium,
+    },
+  },
+  decorators: [basicWrapperDecorator],
+};
+
+export default meta;
+type Story = StoryObj<typeof Card>;
+
+const BodyContainer = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+export const Default: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+              <Text>Super awesome description</Text>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <IconSparkleInShieldDisabled />
+                <Text as="p" size={TextSize.Small}>
+                  Helpful text
+                </Text>
+              </div>
+            </div>
+          }
+          hasDivider
+          rightContent={<IconCheckCircle color="echoes-color-icon-success" />}
+          title="Card Title"
+        />
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div" className="sw-p-10">
+              This is the content of the card.
+            </Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const WithRightContent: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <span>
+              <strong>Last analysis:</strong>
+              3/21/2025, 3:32 PM
+            </span>
+          }
+          hasDivider
+          rightContent={
+            <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
+              <Button variety={ButtonVariety.Primary}>Action</Button>
+            </div>
+          }
+          title={
+            <div style={{ display: 'flex', gap: '4px' }}>
+              <Tooltip content="This is a tooltip">
+                <IconStar color="echoes-color-icon-info" isFilled />
+              </Tooltip>
+              <LinkStandalone to="/">Organization</LinkStandalone>
+              {'/'}
+              <LinkStandalone title="Cool Project" to="/">
+                Cool Project
+              </LinkStandalone>
+            </div>
+          }
+        />
+        <Card.Body>
+          <BodyContainer>
+            <div
+              style={{
+                display: 'flex',
+                gap: '2rem',
+                width: '100%',
+              }}>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="B" size="md" />
+                  <strong>0</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Security</Label>
+              </div>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="C" size="md" />
+                  <strong>1</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Reliability</Label>
+              </div>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="A" size="md" />
+                  <strong>100%</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Maintainability</Label>
+              </div>
+            </div>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const Sizes: Story = {
+  render: () => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      {Object.values(CardSize).map((size) => (
+        <Card key={size} size={size}>
+          <Card.Header
+            description={`A ${size} card`}
+            hasDivider
+            rightContent="some right content"
+            title={`${size.charAt(0).toUpperCase() + size.slice(1)} Card`}
+          />
+          <Card.Body>
+            <BodyContainer>
+              <Text as="div">Content for the {size} card.</Text>
+            </BodyContainer>
+          </Card.Body>
+        </Card>
+      ))}
+    </div>
+  ),
+};
+
+export const NoDescription: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header hasDivider title="Header Without Description" />
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div">This card has a header with title but no description.</Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const BodyOnly: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div">This card has no header, only a body.</Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const NoDivider: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description="This card has a title and description but no divider between header and body"
+          title="Card Without Divider"
+        />
+        <Card.Body insetContent>
+          <div style={{ padding: '0 16px' }}>
path: stories/Card-stories.tsx
user: login: david-cho-lerat-sonarsource
body: again, we shouldn't need an extra layer here, but apply whatever extra style to the parent element
created_at: 2025-03-31T10:03:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+  Button,
+  ButtonVariety,
+  Card,
+  CardSize,
+  IconCheckCircle,
+  IconSparkleInShieldDisabled,
+  IconStar,
+  Label,
+  LinkStandalone,
+  RatingBadge,
+  Text,
+  TextSize,
+  Tooltip,
+} from '../src';
+import { basicWrapperDecorator } from './helpers/BasicWrapper';
+
+const meta: Meta<typeof Card> = {
+  title: 'Echoes/Card',
+  component: Card,
+  argTypes: {
+    size: {
+      control: 'select',
+      options: Object.values(CardSize),
+      defaultValue: CardSize.Medium,
+    },
+  },
+  decorators: [basicWrapperDecorator],
+};
+
+export default meta;
+type Story = StoryObj<typeof Card>;
+
+const BodyContainer = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+export const Default: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+              <Text>Super awesome description</Text>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <IconSparkleInShieldDisabled />
+                <Text as="p" size={TextSize.Small}>
+                  Helpful text
+                </Text>
+              </div>
+            </div>
+          }
+          hasDivider
+          rightContent={<IconCheckCircle color="echoes-color-icon-success" />}
+          title="Card Title"
+        />
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div" className="sw-p-10">
+              This is the content of the card.
+            </Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const WithRightContent: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <span>
+              <strong>Last analysis:</strong>
+              3/21/2025, 3:32 PM
+            </span>
+          }
+          hasDivider
+          rightContent={
+            <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
+              <Button variety={ButtonVariety.Primary}>Action</Button>
+            </div>
+          }
+          title={
+            <div style={{ display: 'flex', gap: '4px' }}>
+              <Tooltip content="This is a tooltip">
+                <IconStar color="echoes-color-icon-info" isFilled />
+              </Tooltip>
+              <LinkStandalone to="/">Organization</LinkStandalone>
+              {'/'}
+              <LinkStandalone title="Cool Project" to="/">
+                Cool Project
+              </LinkStandalone>
+            </div>
+          }
+        />
+        <Card.Body>
+          <BodyContainer>
+            <div
+              style={{
+                display: 'flex',
+                gap: '2rem',
+                width: '100%',
+              }}>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="B" size="md" />
+                  <strong>0</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Security</Label>
+              </div>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="C" size="md" />
+                  <strong>1</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Reliability</Label>
+              </div>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="A" size="md" />
+                  <strong>100%</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Maintainability</Label>
+              </div>
+            </div>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const Sizes: Story = {
+  render: () => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      {Object.values(CardSize).map((size) => (
+        <Card key={size} size={size}>
+          <Card.Header
+            description={`A ${size} card`}
+            hasDivider
+            rightContent="some right content"
+            title={`${size.charAt(0).toUpperCase() + size.slice(1)} Card`}
+          />
+          <Card.Body>
+            <BodyContainer>
+              <Text as="div">Content for the {size} card.</Text>
+            </BodyContainer>
+          </Card.Body>
+        </Card>
+      ))}
+    </div>
+  ),
+};
+
+export const NoDescription: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header hasDivider title="Header Without Description" />
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div">This card has a header with title but no description.</Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const BodyOnly: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div">This card has no header, only a body.</Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const NoDivider: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description="This card has a title and description but no divider between header and body"
+          title="Card Without Divider"
+        />
+        <Card.Body insetContent>
+          <div style={{ padding: '0 16px' }}>
+            <Text as="div">
+              Without a divider, the header and body content flow together visually. This works well
path: stories/Card-stories.tsx
user: login: david-cho-lerat-sonarsource
body: in all the other examples, the card content seems vertically align, but not for this one. Is there a reason for this?:

![image](https://github.com/user-attachments/assets/d5a7995c-1913-417f-87cc-5a0cccb44596)

created_at: 2025-03-31T10:05:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import React, { createContext } from 'react';
+import { CardStyled } from './CardStyles';
+import { CardSize } from './CardTypes';
+
+export interface CardProps {
+  children: React.ReactNode;
+  className?: string;
+  size?: CardSize;
+}
+
+const CardContext = createContext<CardSize>(CardSize.Medium);
+
+export function useCardContext() {
path: src/components/card/CardRoot.tsx
user: login: robert-hoover-sonarsource
body: That makes sense. I'll change the naming.
created_at: 2025-03-31T14:19:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+export enum CardSize {
path: src/components/card/CardTypes.ts
user: login: robert-hoover-sonarsource
body: Yes, I think having it as part of the CardRoot is better. I'll update this.
created_at: 2025-03-31T14:24:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { CardBody } from './CardBody';
+import { CardHeader } from './CardHeader';
+import { CardRoot } from './CardRoot';
+
+export { CardSize } from './CardRoot';
+
+export const Card = Object.assign(CardRoot, {
path: src/components/card/index.ts
user: login: robert-hoover-sonarsource
body: That also works :)
created_at: 2025-03-31T14:31:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { CardBody } from './CardBody';
+import { CardHeader } from './CardHeader';
+import { CardRoot } from './CardRoot';
+
+export { CardSize } from './CardRoot';
+
+export const Card = Object.assign(CardRoot, {
path: src/components/card/index.ts
user: login: robert-hoover-sonarsource
body: Actually, I take that back. When I don't use Object.assign, storybook breaks. My assumption is it's because Object.assign preserves the function identity. So when we create a plain object, although it contains all of CardRoots enumerable properties, it loses its identity as a React component function. 
created_at: 2025-03-31T14:46:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { CardBody } from './CardBody';
+import { CardHeader } from './CardHeader';
+import { CardRoot } from './CardRoot';
+
+export { CardSize } from './CardRoot';
+
+export const Card = Object.assign(CardRoot, {
path: src/components/card/index.ts
user: login: robert-hoover-sonarsource
body: `JSX element type 'Card' does not have any construct or call signatures.ts(2604)
'Card' cannot be used as a JSX component.`

created_at: 2025-03-31T14:47:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { CardBody } from './CardBody';
+import { CardHeader } from './CardHeader';
+import { CardRoot } from './CardRoot';
+
+export { CardSize } from './CardRoot';
+
+export const Card = Object.assign(CardRoot, {
path: src/components/card/index.ts
user: login: david-cho-lerat-sonarsource
body: huh, that's a shame, `Object.assign` feels outdated :sweat_smile: 
created_at: 2025-03-31T14:48:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+export enum CardSize {
path: src/components/card/CardTypes.ts
user: login: robert-hoover-sonarsource
body: In order to avoid a circular dependency, I can either keep the enum as a separate file or export it from the `CardStyles` file. WDYT
created_at: 2025-03-31T15:12:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+export enum CardSize {
path: src/components/card/CardTypes.ts
user: login: david-cho-lerat-sonarsource
body: I don't know where the circular dependency is, but having it in CardStyles sounds wrong as it's not a style. Keeping it as a separate file with a relevant name is fine then I guess :+1: 
created_at: 2025-03-31T15:26:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+  Button,
+  ButtonVariety,
+  Card,
+  CardSize,
+  IconCheckCircle,
+  IconSparkleInShieldDisabled,
+  IconStar,
+  Label,
+  LinkStandalone,
+  RatingBadge,
+  Text,
+  TextSize,
+  Tooltip,
+} from '../src';
+import { basicWrapperDecorator } from './helpers/BasicWrapper';
+
+const meta: Meta<typeof Card> = {
+  title: 'Echoes/Card',
+  component: Card,
+  argTypes: {
+    size: {
+      control: 'select',
+      options: Object.values(CardSize),
+      defaultValue: CardSize.Medium,
+    },
+  },
+  decorators: [basicWrapperDecorator],
+};
+
+export default meta;
+type Story = StoryObj<typeof Card>;
+
+const BodyContainer = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+export const Default: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+              <Text>Super awesome description</Text>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <IconSparkleInShieldDisabled />
+                <Text as="p" size={TextSize.Small}>
+                  Helpful text
+                </Text>
+              </div>
+            </div>
+          }
+          hasDivider
+          rightContent={<IconCheckCircle color="echoes-color-icon-success" />}
+          title="Card Title"
+        />
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div" className="sw-p-10">
+              This is the content of the card.
+            </Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const WithRightContent: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div>
+      <Card {...args}>
+        <Card.Header
+          description={
+            <span>
+              <strong>Last analysis:</strong>
+              3/21/2025, 3:32 PM
+            </span>
+          }
+          hasDivider
+          rightContent={
+            <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
+              <Button variety={ButtonVariety.Primary}>Action</Button>
+            </div>
+          }
+          title={
+            <div style={{ display: 'flex', gap: '4px' }}>
+              <Tooltip content="This is a tooltip">
+                <IconStar color="echoes-color-icon-info" isFilled />
+              </Tooltip>
+              <LinkStandalone to="/">Organization</LinkStandalone>
+              {'/'}
+              <LinkStandalone title="Cool Project" to="/">
+                Cool Project
+              </LinkStandalone>
+            </div>
+          }
+        />
+        <Card.Body>
+          <BodyContainer>
+            <div
+              style={{
+                display: 'flex',
+                gap: '2rem',
+                width: '100%',
+              }}>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="B" size="md" />
+                  <strong>0</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Security</Label>
+              </div>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="C" size="md" />
+                  <strong>1</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Reliability</Label>
+              </div>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <RatingBadge rating="A" size="md" />
+                  <strong>100%</strong>
+                </div>
+                <Label style={{ fontSize: '0.875rem', marginTop: '8px' }}>Maintainability</Label>
+              </div>
+            </div>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const Sizes: Story = {
+  render: () => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      {Object.values(CardSize).map((size) => (
+        <Card key={size} size={size}>
+          <Card.Header
+            description={`A ${size} card`}
+            hasDivider
+            rightContent="some right content"
+            title={`${size.charAt(0).toUpperCase() + size.slice(1)} Card`}
+          />
+          <Card.Body>
+            <BodyContainer>
+              <Text as="div">Content for the {size} card.</Text>
+            </BodyContainer>
+          </Card.Body>
+        </Card>
+      ))}
+    </div>
+  ),
+};
+
+export const NoDescription: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header hasDivider title="Header Without Description" />
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div">This card has a header with title but no description.</Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const BodyOnly: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Body>
+          <BodyContainer>
+            <Text as="div">This card has no header, only a body.</Text>
+          </BodyContainer>
+        </Card.Body>
+      </Card>
+    </div>
+  ),
+};
+
+export const NoDivider: Story = {
+  args: {
+    size: CardSize.Medium,
+  },
+  render: (args) => (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' }}>
+      <Card {...args}>
+        <Card.Header
+          description="This card has a title and description but no divider between header and body"
+          title="Card Without Divider"
+        />
+        <Card.Body insetContent>
+          <div style={{ padding: '0 16px' }}>
+            <Text as="div">
+              Without a divider, the header and body content flow together visually. This works well
path: stories/Card-stories.tsx
user: login: robert-hoover-sonarsource
body: No particular reason. I'll change it to be consistent with the other examples.
created_at: 2025-03-31T15:34:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+export enum CardSize {
path: src/components/card/CardTypes.ts
user: login: robert-hoover-sonarsource
body: Sounds good!
created_at: 2025-03-31T16:03:27Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-03-26T22:07:01Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *echoes-react* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 697ead2afe0455f0d01bce5ba79ce35f61317d92 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/echoes-react/deploys/67eabb714df9b500098ca59c |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-303--echoes-react.netlify.app](https://deploy-preview-303--echoes-react.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTMwMy0tZWNob2VzLXJlYWN0Lm5ldGxpZnkuYXBwIn0.rrpjv9MhoXFfY9C65BWDElqT3QjbZV3esrSKCLSvws8)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [echoes-react Preview](https://deploy-preview-303--echoes-react.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/echoes-react/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: sonarqube-next[bot]
created_at: 2025-03-31T15:59:47Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://next.sonarqube.com/sonarqube/dashboard?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303&resolved=false) [0 New issues](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&fixedInPullRequest=303) [0 Fixed issues](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&fixedInPullRequest=303)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303&issueStatuses=ACCEPTED) [1 Accepted issue](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/security_hotspots?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303) [0 Security Hotspots](https://next.sonarqube.com/sonarqube/security_hotspots?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303&metric=new_coverage&view=list) [97.8% Coverage on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://next.sonarqube.com/sonarqube/dashboard?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=303)


author_association: NONE