diff --git a/packages/ods/react/tests/_app/src/components.ts b/packages/ods/react/tests/_app/src/components.ts
index 1d4cd35ad1..a42d6746b6 100644
--- a/packages/ods/react/tests/_app/src/components.ts
+++ b/packages/ods/react/tests/_app/src/components.ts
@@ -35,6 +35,7 @@ const componentNames = [
'checkbox',
'select',
'modal',
+ 'card',
//--generator-anchor--
];
diff --git a/packages/ods/react/tests/_app/src/components/ods-card.tsx b/packages/ods/react/tests/_app/src/components/ods-card.tsx
new file mode 100644
index 0000000000..91ccd228c9
--- /dev/null
+++ b/packages/ods/react/tests/_app/src/components/ods-card.tsx
@@ -0,0 +1,12 @@
+import React from 'react-dom/client';
+import { OdsCard, OdsText } from 'ods-components-react';
+
+const Card = () => {
+ return (
+
+ Hello, world!
+
+ );
+};
+
+export default Card;
diff --git a/packages/ods/react/tests/e2e/ods-card.e2e.ts b/packages/ods/react/tests/e2e/ods-card.e2e.ts
new file mode 100644
index 0000000000..3d1a129b90
--- /dev/null
+++ b/packages/ods/react/tests/e2e/ods-card.e2e.ts
@@ -0,0 +1,23 @@
+import type { Page } from 'puppeteer';
+import { goToComponentPage, setupBrowser } from '../setup';
+
+describe('ods-card react', () => {
+ const setup = setupBrowser();
+ let page: Page;
+
+ beforeAll(async () => {
+ page = setup().page;
+ });
+
+ beforeEach(async () => {
+ await goToComponentPage(page, 'ods-card');
+ });
+
+ it('render the component correctly', async () => {
+ const elem = await page.$('ods-card');
+ const boundingBox = await elem?.boundingBox();
+
+ expect(boundingBox?.height).toBeGreaterThan(0);
+ expect(boundingBox?.width).toBeGreaterThan(0);
+ });
+});
diff --git a/packages/ods/src/components/card/.gitignore b/packages/ods/src/components/card/.gitignore
new file mode 100644
index 0000000000..7b15d7273d
--- /dev/null
+++ b/packages/ods/src/components/card/.gitignore
@@ -0,0 +1,5 @@
+# Local Stencil command generates external ods component build at the root of the project
+# Excluding them is a temporary solution to avoid pushing generated files
+# But the issue may cause main build (ods-component package) to fails, as it detects multiples occurences
+# of the same component and thus you have to delete all those generated dir manually
+*/src/
diff --git a/packages/ods/src/components/card/package.json b/packages/ods/src/components/card/package.json
new file mode 100644
index 0000000000..61f9cb2fae
--- /dev/null
+++ b/packages/ods/src/components/card/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@ovhcloud/ods-component-card",
+ "version": "17.1.0",
+ "private": true,
+ "description": "ODS Card component",
+ "main": "dist/index.cjs.js",
+ "collection": "dist/collection/collection-manifest.json",
+ "scripts": {
+ "clean": "rimraf .stencil coverage dist docs-api www",
+ "doc": "typedoc --pretty --plugin ../../../scripts/typedoc-plugin-decorator.js && node ../../../scripts/generate-typedoc-md.js",
+ "lint:scss": "stylelint 'src/components/**/*.scss'",
+ "lint:ts": "eslint '{src,tests}/**/*.{js,ts,tsx}'",
+ "start": "stencil build --dev --watch --serve",
+ "test:e2e": "stencil test --e2e --config stencil.config.ts",
+ "test:e2e:ci": "tsc --noEmit && stencil test --e2e --ci --runInBand --config stencil.config.ts",
+ "test:spec": "stencil test --spec --config stencil.config.ts --coverage",
+ "test:spec:ci": "tsc --noEmit && stencil test --config stencil.config.ts --spec --ci --coverage"
+ }
+}
diff --git a/packages/ods/src/components/card/src/components/ods-card/ods-card.scss b/packages/ods/src/components/card/src/components/ods-card/ods-card.scss
new file mode 100644
index 0000000000..0c5c6adf1f
--- /dev/null
+++ b/packages/ods/src/components/card/src/components/ods-card/ods-card.scss
@@ -0,0 +1,18 @@
+:host(.ods-card) {
+ display: inline-block;
+}
+
+.ods-card {
+ &__wrapper {
+ border: 1px solid;
+ border-radius: 8px;
+
+ &--neutral {
+ border-color: var(--ods-color-neutral-200);
+ }
+
+ &--primary {
+ border-color: var(--ods-color-primary-200);
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/ods/src/components/card/src/components/ods-card/ods-card.tsx b/packages/ods/src/components/card/src/components/ods-card/ods-card.tsx
new file mode 100644
index 0000000000..a3fcf38e25
--- /dev/null
+++ b/packages/ods/src/components/card/src/components/ods-card/ods-card.tsx
@@ -0,0 +1,22 @@
+import type { FunctionalComponent } from '@stencil/core';
+import { Component, Host, Prop, h } from '@stencil/core';
+import { ODS_CARD_COLOR, type OdsCardColor } from '../../constants/card-color';
+
+@Component({
+ shadow: true,
+ styleUrl: 'ods-card.scss',
+ tag: 'ods-card',
+})
+export class OdsCard {
+ @Prop({ reflect: true }) public color: OdsCardColor = ODS_CARD_COLOR.primary;
+
+ render(): FunctionalComponent {
+ return (
+
+
+
+
+
+ );
+ }
+}
diff --git a/packages/ods/src/components/card/src/constants/card-color.ts b/packages/ods/src/components/card/src/constants/card-color.ts
new file mode 100644
index 0000000000..8b830fd674
--- /dev/null
+++ b/packages/ods/src/components/card/src/constants/card-color.ts
@@ -0,0 +1,14 @@
+enum ODS_CARD_COLOR {
+ neutral = 'neutral',
+ primary = 'primary',
+}
+
+type OdsCardColor = `${ODS_CARD_COLOR}`;
+
+const ODS_CARD_COLORS = Object.freeze(Object.values(ODS_CARD_COLOR));
+
+export {
+ ODS_CARD_COLOR,
+ ODS_CARD_COLORS,
+ type OdsCardColor,
+};
\ No newline at end of file
diff --git a/packages/ods/src/components/card/src/globals.ts b/packages/ods/src/components/card/src/globals.ts
new file mode 100644
index 0000000000..a4fcf2c6ab
--- /dev/null
+++ b/packages/ods/src/components/card/src/globals.ts
@@ -0,0 +1,9 @@
+/**
+ * Import here all the external ODS component that you need to run the current component
+ * when running dev server (yarn start) or e2e tests
+ *
+ * ex:
+ * import '../../text/src';
+ */
+
+import '../../text/src';
\ No newline at end of file
diff --git a/packages/ods/src/components/card/src/index.html b/packages/ods/src/components/card/src/index.html
new file mode 100644
index 0000000000..a61c5f1020
--- /dev/null
+++ b/packages/ods/src/components/card/src/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+ Dev ods-card
+
+
+
+
+
+
+
+ Default
+
+ Hello, world!
+
+
+ Primary
+
+ Hello, world!
+
+
+ Neutral
+
+ Hello, world!
+
+
+
+
+
diff --git a/packages/ods/src/components/card/src/index.ts b/packages/ods/src/components/card/src/index.ts
new file mode 100644
index 0000000000..d54b53406b
--- /dev/null
+++ b/packages/ods/src/components/card/src/index.ts
@@ -0,0 +1,2 @@
+export { OdsCard } from './components/ods-card/ods-card';
+export { ODS_CARD_COLOR, ODS_CARD_COLORS, type OdsCardColor } from './constants/card-color';
\ No newline at end of file
diff --git a/packages/ods/src/components/card/stencil.config.ts b/packages/ods/src/components/card/stencil.config.ts
new file mode 100644
index 0000000000..f68caa4aab
--- /dev/null
+++ b/packages/ods/src/components/card/stencil.config.ts
@@ -0,0 +1,7 @@
+import { getStencilConfig } from '../../config/stencil';
+
+export const config = getStencilConfig({
+ args: process.argv.slice(2),
+ componentCorePackage: '@ovhcloud/ods-component-card',
+ namespace: 'ods-card',
+});
diff --git a/packages/ods/src/components/card/tests/rendering/ods-card.e2e.ts b/packages/ods/src/components/card/tests/rendering/ods-card.e2e.ts
new file mode 100644
index 0000000000..38f32c942c
--- /dev/null
+++ b/packages/ods/src/components/card/tests/rendering/ods-card.e2e.ts
@@ -0,0 +1,56 @@
+import type { E2EElement, E2EPage } from '@stencil/core/testing';
+import { newE2EPage } from '@stencil/core/testing';
+
+describe('ods-card rendering', () => {
+ let el: E2EElement;
+ let page: E2EPage;
+
+ async function setup(content: string, customStyle?: string): Promise {
+ page = await newE2EPage();
+
+ await page.setContent(content);
+ await page.evaluate(() => document.body.style.setProperty('margin', '0px'));
+
+ if (customStyle) {
+ await page.addStyleTag({ content: customStyle });
+ }
+
+ el = await page.find('ods-card');
+ }
+
+ it('should render the web component', async() => {
+ await setup('');
+
+ expect(el.shadowRoot).not.toBeNull();
+ });
+
+ describe('color', () => {
+ it('should render with correct color primary', async() => {
+ await setup(`
+
+
+ `);
+
+ const hasClassPrimary = await page.evaluate(() => {
+ const wrapper = document.querySelector('ods-card')?.shadowRoot?.querySelector('.ods-card__wrapper');
+ return wrapper?.classList.contains('ods-card__wrapper--primary');
+ });
+
+ expect(hasClassPrimary).toBe(true);
+ });
+
+ it('should render with correct color neutral', async() => {
+ await setup(`
+
+
+ `);
+
+ const hasClassNeutral = await page.evaluate(() => {
+ const wrapper = document.querySelector('ods-card')?.shadowRoot?.querySelector('.ods-card__wrapper');
+ return wrapper?.classList.contains('ods-card__wrapper--neutral');
+ });
+
+ expect(hasClassNeutral).toBe(true);
+ });
+ });
+});
diff --git a/packages/ods/src/components/card/tests/rendering/ods-card.spec.ts b/packages/ods/src/components/card/tests/rendering/ods-card.spec.ts
new file mode 100644
index 0000000000..a4780879e1
--- /dev/null
+++ b/packages/ods/src/components/card/tests/rendering/ods-card.spec.ts
@@ -0,0 +1,35 @@
+import type { SpecPage } from '@stencil/core/testing';
+import { newSpecPage } from '@stencil/core/testing';
+import { ODS_CARD_COLOR, OdsCard } from '../../src';
+
+describe('ods-card rendering', () => {
+ let page: SpecPage;
+ let root: HTMLElement | undefined;
+
+ async function setup(html: string): Promise {
+ page = await newSpecPage({
+ components: [OdsCard],
+ html,
+ });
+
+ root = page.root;
+ }
+
+ describe('attributes', () => {
+ describe('color', () => {
+ it('should be reflected', async() => {
+ const colorValue = ODS_CARD_COLOR.neutral;
+
+ await setup(``);
+
+ expect(root?.getAttribute('color')).toBe(colorValue);
+ });
+
+ it('should be set to its default value', async() => {
+ await setup('');
+
+ expect(root?.getAttribute('color')).toBe(ODS_CARD_COLOR.primary);
+ });
+ });
+ });
+});
diff --git a/packages/ods/src/components/card/tsconfig.json b/packages/ods/src/components/card/tsconfig.json
new file mode 100644
index 0000000000..e242da5e2f
--- /dev/null
+++ b/packages/ods/src/components/card/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../tsconfig.json",
+ "include": [
+ "src",
+ "tests"
+ ]
+}
diff --git a/packages/ods/src/components/card/typedoc.json b/packages/ods/src/components/card/typedoc.json
new file mode 100644
index 0000000000..74d6700e67
--- /dev/null
+++ b/packages/ods/src/components/card/typedoc.json
@@ -0,0 +1,10 @@
+{
+ "entryPoints": ["src/index.ts"],
+ "excludeInternal": true,
+ "excludePrivate": true,
+ "excludeProtected": true,
+ "hideGenerator": true,
+ "json": "dist/docs-api/typedoc.json",
+ "out": "dist/docs-api/",
+ "tsconfig":"tsconfig.json"
+}
diff --git a/packages/ods/src/components/index.ts b/packages/ods/src/components/index.ts
index 3ca65a17ca..2d80a06864 100644
--- a/packages/ods/src/components/index.ts
+++ b/packages/ods/src/components/index.ts
@@ -33,4 +33,5 @@ export * from './message/src';
export * from './radio/src';
export * from './checkbox/src';
export * from './select/src';
-export * from './modal/src';
\ No newline at end of file
+export * from './modal/src';
+export * from './card/src';
\ No newline at end of file
diff --git a/packages/ods/vue/tests/_app/src/components.ts b/packages/ods/vue/tests/_app/src/components.ts
index 1d4cd35ad1..a42d6746b6 100644
--- a/packages/ods/vue/tests/_app/src/components.ts
+++ b/packages/ods/vue/tests/_app/src/components.ts
@@ -35,6 +35,7 @@ const componentNames = [
'checkbox',
'select',
'modal',
+ 'card',
//--generator-anchor--
];
diff --git a/packages/ods/vue/tests/_app/src/components/ods-card.vue b/packages/ods/vue/tests/_app/src/components/ods-card.vue
new file mode 100644
index 0000000000..4d4f55097e
--- /dev/null
+++ b/packages/ods/vue/tests/_app/src/components/ods-card.vue
@@ -0,0 +1,18 @@
+
+
+ Hello, world!
+
+
+
+
diff --git a/packages/ods/vue/tests/e2e/ods-card.e2e.ts b/packages/ods/vue/tests/e2e/ods-card.e2e.ts
new file mode 100644
index 0000000000..1c0935855c
--- /dev/null
+++ b/packages/ods/vue/tests/e2e/ods-card.e2e.ts
@@ -0,0 +1,23 @@
+import type { Page } from 'puppeteer';
+import { goToComponentPage, setupBrowser } from '../setup';
+
+describe('ods-card vue', () => {
+ const setup = setupBrowser();
+ let page: Page;
+
+ beforeAll(async () => {
+ page = setup().page;
+ });
+
+ beforeEach(async () => {
+ await goToComponentPage(page, 'ods-card');
+ });
+
+ it('render the component correctly', async () => {
+ const elem = await page.$('ods-card');
+ const boundingBox = await elem?.boundingBox();
+
+ expect(boundingBox?.height).toBeGreaterThan(0);
+ expect(boundingBox?.width).toBeGreaterThan(0);
+ });
+});
diff --git a/packages/storybook/stories/components/card/card.stories.ts b/packages/storybook/stories/components/card/card.stories.ts
new file mode 100644
index 0000000000..6e7efc0276
--- /dev/null
+++ b/packages/storybook/stories/components/card/card.stories.ts
@@ -0,0 +1,108 @@
+import type { Meta, StoryObj } from '@storybook/web-components';
+import { defineCustomElement } from '@ovhcloud/ods-components/dist/components/ods-card';
+import { ODS_CARD_COLOR, ODS_CARD_COLORS } from '@ovhcloud/ods-components';
+import { html } from 'lit-html';
+import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
+import { CONTROL_CATEGORY, orderControls } from '../../control';
+
+defineCustomElement();
+
+const meta: Meta = {
+ title: 'ODS Components/Layout/Card',
+ component: 'ods-card',
+};
+
+export default meta;
+
+export const Demo: StoryObj = {
+ render: (args) => html`
+
+ ${unsafeHTML(args.content)}
+
+
+
+ `,
+ argTypes: orderControls({
+ color: {
+ table: {
+ category: CONTROL_CATEGORY.design,
+ defaultValue: { summary: ODS_CARD_COLOR.primary },
+ type: { summary: ODS_CARD_COLORS },
+ },
+ control: { type: 'select' },
+ options: ODS_CARD_COLORS,
+ },
+ content: {
+ table: {
+ category: CONTROL_CATEGORY.slot,
+ defaultValue: { summary: 'ø' },
+ },
+ control: 'text',
+ },
+ customCss: {
+ table: {
+ category: CONTROL_CATEGORY.design,
+ defaultValue: { summary: 'ø' },
+ },
+ control: 'text',
+ description: 'Set a custom style properties. Example: ".demo-card::part(card) { border: 1px red solid; }"',
+ },
+ }),
+ args: {
+ color: ODS_CARD_COLOR.primary,
+ content: 'Hello, world!',
+ customCss: '.demo-card::part(card) { padding: 0 24px; }',
+ },
+};
+
+export const Default: StoryObj = {
+ tags: ['isHidden'],
+ render: () => html`
+
+ `,
+};
+
+
+export const Overview: StoryObj = {
+ tags: ['isHidden'],
+ render: () => html`
+
+ Hello, world!
+
+
+
+ `,
+};
+
+export const CustomCSS: StoryObj = {
+ tags: ['isHidden'],
+ render: () => html`
+
+ Hello, world!
+
+
+
+ `,
+};
+
+export const Color: StoryObj = {
+ tags: ['isHidden'],
+ render: () => html`
+
+ Hello, world!
+
+ `,
+};
\ No newline at end of file
diff --git a/packages/storybook/stories/components/card/documentation.mdx b/packages/storybook/stories/components/card/documentation.mdx
new file mode 100644
index 0000000000..a334dda9d7
--- /dev/null
+++ b/packages/storybook/stories/components/card/documentation.mdx
@@ -0,0 +1,39 @@
+import { Canvas, Meta, Markdown } from '@storybook/blocks';
+import SpecificationsCard from '@ovhcloud/ods-components/src/components/card/documentation/spec.md?raw';
+import { Banner } from '../../banner';
+import { DocNavigator } from '../../doc-navigator';
+import * as CardStories from './card.stories';
+import { LINK_ID } from '../../zeroheight';
+
+
+
+
+
+# Overview
+
+Card is a component designed to be a container for displaying content and other elements to communicate information or feature a call-to-action within a page.
+
+
+
+
+
+{ SpecificationsCard }
+
+# Style customization
+
+You can add your own style on the card element using the part `card`.
+
+Custom Card CSS:
+
+
+
+# Examples
+
+## Default
+
+
+
+## Color
+
+
+...
diff --git a/packages/storybook/stories/components/card/migration.from.17.x.mdx b/packages/storybook/stories/components/card/migration.from.17.x.mdx
new file mode 100644
index 0000000000..0bf98e3c3c
--- /dev/null
+++ b/packages/storybook/stories/components/card/migration.from.17.x.mdx
@@ -0,0 +1,11 @@
+import { Meta } from '@storybook/blocks';
+import * as CardStories from './card.stories';
+
+
+
+# Card - migrate from v17 to v18
+----
+
+This component has been first released with the v18, there is no migration involved coming from v17.x.
+
+To learn how to use this component, please check the [documentation](?path=/docs/ods-components-layout-card--documentation).
\ No newline at end of file
diff --git a/packages/storybook/stories/zeroheight.ts b/packages/storybook/stories/zeroheight.ts
index c51623c9e7..840c90b106 100644
--- a/packages/storybook/stories/zeroheight.ts
+++ b/packages/storybook/stories/zeroheight.ts
@@ -7,6 +7,7 @@ enum LINK_ID {
BADGE = '879fff-badge',
BREADCRUMB = '6707ac-breadcrumb',
BUTTON = '794ee9-button',
+ CARD = '76f7e4-card',
CHECKBOX_BUTTON = '0166c5-checkbox-button',
CLIPBOARD = '25bcae-clipboard',
CODE = '07c385-code',
diff --git a/yarn.lock b/yarn.lock
index c870a72fdf..9800f1841e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3940,11 +3940,20 @@ __metadata:
languageName: unknown
linkType: soft
+<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
>>>>>>> ca6158dbb (feat(button): implement component)
=======
=======
+=======
+"@ovhcloud/ods-component-card@workspace:packages/ods/src/components/card":
+ version: 0.0.0-use.local
+ resolution: "@ovhcloud/ods-component-card@workspace:packages/ods/src/components/card"
+ languageName: unknown
+ linkType: soft
+
+>>>>>>> 085d24bf8 (feat(card): implement component)
"@ovhcloud/ods-component-checkbox@workspace:packages/ods/src/components/checkbox":
version: 0.0.0-use.local
resolution: "@ovhcloud/ods-component-checkbox@workspace:packages/ods/src/components/checkbox"