diff --git a/apps/apollo-stories/src/components/Table/Table.mdx b/apps/apollo-stories/src/components/Table/Table.mdx new file mode 100644 index 000000000..774c75648 --- /dev/null +++ b/apps/apollo-stories/src/components/Table/Table.mdx @@ -0,0 +1,79 @@ +import { Canvas, Controls, Meta } from "@storybook/addon-docs"; +import * as TableStories from "./Table.stories"; + + + +# Table + +To use the table import it like that: + +```tsx +import { Table } from "@axa-fr/canopee-react/prospect"; + +const MyComponent = () => ( + + + + Nom + Email + + + + + Jean Dupont + jean.dupont@example.com + + +
+); +``` + +## Usage + +The Table component is a compound component with the following sub-components: + +- `Table.THead` - Table header with optional `variant` prop +- `Table.TBody` - Table body with optional `variant` prop +- `Table.Tr` - Table row with optional `size` and variant props +- `Table.Th` - Table header cell with optional `onSort`, `onCheck`, `checkboxPosition` props +- `Table.Td` - Table data cell with optional `position`, `verticalAlign`, `variant` and `size` props + +## Examples + +### Basic Table + + + +### Alternate Variants + +Use `variant="alternate"` on `Table.TBody` to get zebra-striped rows. + + + +### With Tags + +Tables work great with other components like Tags for status indicators. + + + +### With Buttons + +Tables can include interactive elements like buttons for actions. + + + +### Different Sizes + +Use the `size` prop on `Table.Tr` to control row height ("S", "M", "L"). + + + +### Text Alignment + +Use the `position` prop on `Table.Td` to align cell content ("left", "center", "right"). + + + +### Compact Table + + diff --git a/apps/apollo-stories/src/components/Table/Table.stories.tsx b/apps/apollo-stories/src/components/Table/Table.stories.tsx new file mode 100644 index 000000000..b814fae5b --- /dev/null +++ b/apps/apollo-stories/src/components/Table/Table.stories.tsx @@ -0,0 +1,486 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { action } from "@storybook/addon-actions"; +import { + Table, + Button, + Tag, + type HeadColorVariants, + type BodyColorVariants, + type RowSizeVariants, +} from "@axa-fr/canopee-react/prospect"; + +interface TableStoryArgs { + theadVariant?: HeadColorVariants; + tbodyVariant?: BodyColorVariants; + rowSize?: RowSizeVariants; + row1Size?: RowSizeVariants; + row2Size?: RowSizeVariants; + row3Size?: RowSizeVariants; + row4Size?: RowSizeVariants; +} + +const meta: Meta = { + title: "Components/Table", + component: Table, +}; + +export default meta; + +type Story = StoryObj; + +export const BasicTable: Story = { + name: "Tableau basique", + args: { + theadVariant: "blue", + tbodyVariant: undefined, + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Nom + Prénom + Email + Téléphone + + + + + Dupont + Jean + jean.dupont@example.com + 06 12 34 56 78 + + + Martin + Marie + marie.martin@example.com + 06 98 76 54 32 + + + Bernard + Pierre + pierre.bernard@example.com + 06 11 22 33 44 + + + Dubois + Sophie + sophie.dubois@example.com + 06 55 66 77 88 + + +
+ ), +}; + +export const AlternateVariantTable: Story = { + name: "Tableau avec couleurs alternées", + args: { + theadVariant: undefined, + tbodyVariant: "alternate", + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Produit + Catégorie + Prix + Stock + + + + + Ordinateur Portable + Électronique + 899,00 € + 15 + + + Souris sans fil + Accessoires + 29,99 € + 50 + + + Clavier mécanique + Accessoires + 89,00 € + 23 + + + Écran 27 + Électronique + 299,00 € + 8 + + +
+ ), +}; + +export const TableWithTags: Story = { + name: "Tableau avec tags, statuts et tri", + args: { + theadVariant: undefined, + tbodyVariant: "alternate", + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Référence + Statut + Client + Montant + + + + + REF-001 + + Validé + + Jean Dupont + 220,00 € + + + REF-002 + + En attente + + Marie Martin + 450,00 € + + + REF-003 + + Rejeté + + Pierre Bernard + 180,00 € + + + REF-004 + + En cours + + Sophie Dubois + 320,00 € + + +
+ ), +}; + +export const TableWithButtons: Story = { + name: "Tableau avec sélection et actions", + args: { + theadVariant: "gray", + tbodyVariant: undefined, + rowSize: "M", + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + rowSize: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille des lignes", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Utilisateur + Email + Rôle + Actions + + + + + Jean Dupont + jean.dupont@example.com + Administrateur + + + + + + Marie Martin + marie.martin@example.com + Éditeur + + + + + + Pierre Bernard + pierre.bernard@example.com + Lecteur + + + + + + Sophie Dubois + sophie.dubois@example.com + Éditeur + + + + + +
+ ), +}; + +export const TableWithDifferentSizes: Story = { + name: "Tableau avec tailles de lignes variées", + args: { + theadVariant: undefined, + tbodyVariant: "alternate", + row1Size: "S", + row2Size: "M", + row3Size: "L", + row4Size: undefined, + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + row1Size: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille de la ligne 1", + }, + row2Size: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille de la ligne 2", + }, + row3Size: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille de la ligne 3", + }, + row4Size: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille de la ligne 4", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Nom + Description + Prix + Disponibilité + + + + + Produit A + Description courte + 49,99 € + En stock + + + Produit B + Description de longueur moyenne pour ce produit + 79,99 € + En stock + + + Produit C + + Description très détaillée avec beaucoup informations + + 129,99 € + Stock limité + + + Produit D + Description standard + 99,99 € + Sur commande + + +
+ ), +}; + +export const TableWithAlignments: Story = { + name: "Tableau avec alignements différents", + args: { + theadVariant: undefined, + tbodyVariant: undefined, + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Article + Quantité + Prix unitaire + Total + + + + + Ordinateur + 1 + 899,00 € + 899,00 € + + + Souris + 2 + 29,99 € + 59,98 € + + + Clavier + 1 + 89,00 € + 89,00 € + + + Câble HDMI + 3 + 15,99 € + 47,97 € + + +
+ ), +}; + +export const CompactTable: Story = { + name: "Tableau compact (3 colonnes)", + args: { + theadVariant: undefined, + tbodyVariant: undefined, + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Nom + Statut + Date + + + + + Projet Alpha + + Terminé + + 10/01/2026 + + + Projet Beta + + En cours + + 15/01/2026 + + + Projet Gamma + + Planifié + + 20/01/2026 + + + Projet Delta + + Annulé + + 05/01/2026 + + +
+ ), +}; diff --git a/apps/apollo-stories/src/components/TableMobileCard/TableMobileCard.mdx b/apps/apollo-stories/src/components/TableMobileCard/TableMobileCard.mdx new file mode 100644 index 000000000..c4392287a --- /dev/null +++ b/apps/apollo-stories/src/components/TableMobileCard/TableMobileCard.mdx @@ -0,0 +1,47 @@ +import { Canvas, Controls, Meta } from "@storybook/addon-docs"; +import * as TableMobileCardStories from "./TableMobileCard.stories"; + + + +# TableMobileCard + +> ⚠️ This component should not be used alone. It is the mobile version for a table. + +To use the table mobile card import it like that: + +```tsx +import { TableMobileCard } from "@axa-fr/canopee-react/prospect"; + +const MyComponent = () => ( + + + Produit/Support + AB Sustainable Global Thematic A + + + Code Isin + LU0101010101 + + + Status + Ouvert + + +); +``` + +## Usage + +The TableMobileCard component is a compound component with the following sub-components: + +- `TableMobileCard.DRow` - TableMobileCard row wrapper with optional `direction` prop +- `TableMobileCard.Dt` - Table row title +- `TableMobileCard.Dd` - Table row description + +## Examples + +### Basic Table + + + + diff --git a/apps/apollo-stories/src/components/TableMobileCard/TableMobileCard.stories.tsx b/apps/apollo-stories/src/components/TableMobileCard/TableMobileCard.stories.tsx new file mode 100644 index 000000000..ca947cd23 --- /dev/null +++ b/apps/apollo-stories/src/components/TableMobileCard/TableMobileCard.stories.tsx @@ -0,0 +1,94 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { action } from "@storybook/addon-actions"; +import { Button, Icon, TableMobileCard } from "@axa-fr/canopee-react/prospect"; +import download from "@material-symbols/svg-400/outlined/download_2-fill.svg"; + +interface TableMobileCardStoryArgs { + variant?: "alternate" | "blue" | "white"; + direction?: "row" | "column"; +} + +const meta: Meta = { + title: "Components/TableMobileCard", + component: TableMobileCard, +}; + +export default meta; + +type Story = StoryObj; + +export const BasicTableMobileCard: Story = { + name: "Table Card basique", + args: { + variant: "alternate", + direction: "row", + }, + argTypes: { + variant: { + control: { type: "select" }, + options: ["alternate", "white", "blue"], + description: "Variant de la couleur des lignes", + }, + direction: { + control: { type: "select" }, + options: ["row", "column"], + description: "Variant de la disposition des éléments d'une ligne", + }, + }, + render: (args: TableMobileCardStoryArgs) => ( + + + Produit/Support + + AB Sustainable Global Thematic A + + + + Code Isin + LU0101010101 + + + Status + Ouvert + + + Qualification SFDR + 9 + + + DIC/DIS/DICI + + + + + + Prospectus + + + + + + Rapport annuelle + + + + + + ), +}; diff --git a/apps/apollo-stories/src/pages/Table/Table.css b/apps/apollo-stories/src/pages/Table/Table.css new file mode 100644 index 000000000..92bdd70b0 --- /dev/null +++ b/apps/apollo-stories/src/pages/Table/Table.css @@ -0,0 +1,32 @@ +.table-scroll { + margin-inline: calc(-1 * var(--margin-inline)); + padding-inline: var(--margin-inline); + overflow: auto hidden; +} + +main.table-page { + --font-size-base: 16; + + padding-block: calc(40 / var(--font-size-base) * 1rem); + + @media (width > 1023px) { + padding-block: calc(48 / var(--font-size-base) * 1rem); + } + + > section.subgrid { + row-gap: calc(28 / var(--font-size-base) * 1rem); + + @media (width > 1023px) { + --row-gap: calc(48 / var(--font-size-base) * 1rem); + } + + .table-page__dropdown { + --cols: 4; + } + + .table-page__pagination { + width: fit-content; + justify-self: center; + } + } +} \ No newline at end of file diff --git a/apps/apollo-stories/src/pages/Table/Table.stories.tsx b/apps/apollo-stories/src/pages/Table/Table.stories.tsx new file mode 100644 index 000000000..7096dfaed --- /dev/null +++ b/apps/apollo-stories/src/pages/Table/Table.stories.tsx @@ -0,0 +1,16 @@ +import "./Table.css"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { TablePage } from "./Table"; + +const meta: Meta = { + title: "Pages/Table", + parameters: { layout: "fullscreen" }, +}; + +export default meta; + +export const StepSortie: StoryObj = { + name: "Table", + render: TablePage, +}; diff --git a/apps/apollo-stories/src/pages/Table/Table.tsx b/apps/apollo-stories/src/pages/Table/Table.tsx new file mode 100644 index 000000000..16be11ce8 --- /dev/null +++ b/apps/apollo-stories/src/pages/Table/Table.tsx @@ -0,0 +1,450 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import React, { + useCallback, + useMemo, + useState, + useSyncExternalStore, +} from "react"; +import { + Button, + DebugGrid, + Dropdown, + Pagination, + Table, + TableMobileCard, + Tag, + Heading, +} from "@axa-fr/canopee-react/prospect"; + +const useIsSmallScreen = (breakPointToCheck: number) => { + const subscribe = useCallback((listener: () => void) => { + window.addEventListener("resize", listener); + return () => { + window.removeEventListener("resize", listener); + }; + }, []); + + const getSnapshot = useCallback(() => { + return window.innerWidth <= breakPointToCheck; + }, [breakPointToCheck]); + + const getServerSnapshot = useCallback(() => false, []); + + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); +}; + +interface DataRow { + id: number; + reference: string; + societe: string; + contact: string; + email: string; + telephone: string; + montant: number; + statut: "Validé" | "En attente" | "Rejeté" | "En cours"; + date: string; +} + +const ALL_DATA: DataRow[] = [ + { + id: 1, + reference: "CON-2024-001", + societe: "Société ABC", + contact: "Jean Dupont", + email: "jean.dupont@abc.fr", + telephone: "06 12 34 56 78", + montant: 1250.0, + statut: "Validé", + date: "15/01/2024", + }, + { + id: 2, + reference: "CON-2024-002", + societe: "Entreprise XYZ", + contact: "Marie Martin", + email: "marie.martin@xyz.fr", + telephone: "06 98 76 54 32", + montant: 2500.0, + statut: "En attente", + date: "22/03/2024", + }, + { + id: 3, + reference: "CON-2024-003", + societe: "Groupe DEF", + contact: "Pierre Bernard", + email: "pierre.bernard@def.fr", + telephone: "06 11 22 33 44", + montant: 3750.0, + statut: "Rejeté", + date: "10/06/2024", + }, + { + id: 4, + reference: "CON-2024-004", + societe: "Industries GHI", + contact: "Sophie Dubois", + email: "sophie.dubois@ghi.fr", + telephone: "06 55 66 77 88", + montant: 890.0, + statut: "En cours", + date: "05/09/2024", + }, + { + id: 5, + reference: "CON-2024-005", + societe: "Services JKL", + contact: "Luc Moreau", + email: "luc.moreau@jkl.fr", + telephone: "06 22 33 44 55", + montant: 1780.0, + statut: "Validé", + date: "12/11/2024", + }, + { + id: 6, + reference: "CON-2024-006", + societe: "Tech MNO", + contact: "Claire Petit", + email: "claire.petit@mno.fr", + telephone: "06 77 88 99 00", + montant: 4200.0, + statut: "En attente", + date: "28/12/2024", + }, + { + id: 7, + reference: "CON-2024-007", + societe: "Solutions PQR", + contact: "Marc Lefebvre", + email: "marc.lefebvre@pqr.fr", + telephone: "06 33 44 55 66", + montant: 920.0, + statut: "Validé", + date: "03/01/2025", + }, + { + id: 8, + reference: "CON-2024-008", + societe: "Digital STU", + contact: "Anne Rousseau", + email: "anne.rousseau@stu.fr", + telephone: "06 44 55 66 77", + montant: 1560.0, + statut: "En cours", + date: "18/02/2025", + }, + { + id: 9, + reference: "CON-2024-009", + societe: "Consulting VWX", + contact: "Thomas Garnier", + email: "thomas.garnier@vwx.fr", + telephone: "06 88 99 00 11", + montant: 3100.0, + statut: "Validé", + date: "25/03/2025", + }, + { + id: 10, + reference: "CON-2024-010", + societe: "Innovation YZ", + contact: "Julie Faure", + email: "julie.faure@yz.fr", + telephone: "06 99 00 11 22", + montant: 2890.0, + statut: "En attente", + date: "07/04/2025", + }, + { + id: 11, + reference: "CON-2024-011", + societe: "Partners AB", + contact: "Vincent Leroux", + email: "vincent.leroux@ab.fr", + telephone: "06 00 11 22 33", + montant: 1450.0, + statut: "Rejeté", + date: "14/05/2025", + }, + { + id: 12, + reference: "CON-2024-012", + societe: "Business CD", + contact: "Isabelle Simon", + email: "isabelle.simon@cd.fr", + telephone: "06 11 22 33 44", + montant: 3680.0, + statut: "Validé", + date: "22/06/2025", + }, + { + id: 13, + reference: "CON-2024-013", + societe: "Global EF", + contact: "Nicolas Laurent", + email: "nicolas.laurent@ef.fr", + telephone: "06 22 33 44 55", + montant: 2100.0, + statut: "En cours", + date: "30/07/2025", + }, + { + id: 14, + reference: "CON-2024-014", + societe: "Pro GH", + contact: "Sandrine Michel", + email: "sandrine.michel@gh.fr", + telephone: "06 33 44 55 66", + montant: 1890.0, + statut: "En attente", + date: "08/08/2025", + }, + { + id: 15, + reference: "CON-2024-015", + societe: "Expert IJ", + contact: "Julien Leroy", + email: "julien.leroy@ij.fr", + telephone: "06 44 55 66 77", + montant: 4500.0, + statut: "Validé", + date: "15/09/2025", + }, +]; + +const STATUS_VARIANTS: Record< + DataRow["statut"], + "success" | "warning" | "error" | "info" +> = { + Validé: "success", + "En attente": "warning", + Rejeté: "error", + "En cours": "info", +}; + +const getStatusTag = (statut: DataRow["statut"]) => ( + {statut} +); + +type SortConfig = { key: keyof DataRow; direction: "asc" | "desc" } | null; + +export const TablePage = () => { + const [sortConfig, setSortConfig] = useState(null); + const [checkedColumns, setCheckedColumns] = useState>( + () => new Set(), + ); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(5); + + const isMobile = useIsSmallScreen(667); + + const handleSort = useCallback((key: keyof DataRow) => { + setSortConfig((prev) => { + const direction: "asc" | "desc" = + prev?.key === key && prev.direction === "asc" ? "desc" : "asc"; + return { key, direction }; + }); + setCurrentPage(1); + }, []); + + const handleColumnCheck = useCallback((columnIndex: number) => { + setCheckedColumns((prev) => { + const next = new Set(prev); + next.has(columnIndex) ? next.delete(columnIndex) : next.add(columnIndex); + return next; + }); + }, []); + + const getColumnVariant = useCallback( + (columnIndex: number) => { + return checkedColumns.has(columnIndex) ? "blue" : undefined; + }, + [checkedColumns], + ); + + const sortedData = useMemo(() => { + if (!sortConfig) return ALL_DATA; + + const { key, direction } = sortConfig; + + return [...ALL_DATA].sort((a, b) => { + const aValue = a[key]; + const bValue = b[key]; + + // numbers + if (typeof aValue === "number" && typeof bValue === "number") { + return direction === "asc" ? aValue - bValue : bValue - aValue; + } + + // everything else as string + const aStr = String(aValue); + const bStr = String(bValue); + return direction === "asc" + ? aStr.localeCompare(bStr) + : bStr.localeCompare(aStr); + }); + }, [sortConfig]); + + const totalPages = useMemo( + () => Math.max(1, Math.ceil(sortedData.length / itemsPerPage)), + [sortedData.length, itemsPerPage], + ); + + const currentData = useMemo(() => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return sortedData.slice(startIndex, endIndex); + }, [sortedData, currentPage, itemsPerPage]); + + const handlePageChange = useCallback( + (page: number) => setCurrentPage(page), + [], + ); + + const handleItemsPerPageChange = useCallback( + (event: React.ChangeEvent) => { + setItemsPerPage(Number(event.target.value)); + setCurrentPage(1); + }, + [], + ); + + return ( + <> + +
+
+ Liste des produits + {!isMobile && ( + <> +
+ + + + + + +
+ +
+ + + + handleColumnCheck(0)} + onSort={() => handleSort("reference")} + > + Référence + + handleColumnCheck(1)} + onSort={() => handleSort("societe")} + > + Société + + handleColumnCheck(2)} + onSort={() => handleSort("contact")} + > + Contact + + handleColumnCheck(3)} + onSort={() => handleSort("montant")} + > + Montant + + handleColumnCheck(4)} + onSort={() => handleSort("statut")} + > + Statut + + + + + + {currentData.map((row) => ( + + + {row.reference} + + + {row.societe} + + + {row.contact} + + + {row.montant.toFixed(2)} € + + + {getStatusTag(row.statut)} + + + ))} + +
+
+ + + + )} + + {isMobile ? ( + <> + {sortedData.map((elem) => ( + + + Référence + {elem.reference} + + + + Société + {elem.societe} + + + + Contact + {elem.contact} + + + + Montant + + {elem.montant.toFixed(2)} € + + + + + Status + + {getStatusTag(elem.statut)} + + + + ))} + + ) : null} +
+
+ + ); +}; diff --git a/apps/look-and-feel-stories/src/components/Table/Table.mdx b/apps/look-and-feel-stories/src/components/Table/Table.mdx new file mode 100644 index 000000000..50a624a3b --- /dev/null +++ b/apps/look-and-feel-stories/src/components/Table/Table.mdx @@ -0,0 +1,79 @@ +import { Canvas, Controls, Meta } from "@storybook/addon-docs"; +import * as TableStories from "./Table.stories.tsx"; + + + +# Table + +To use the table import it like that: + +```tsx +import { Table } from "@axa-fr/canopee-react/client"; + +const MyComponent = () => ( + + + + Nom + Email + + + + + Jean Dupont + jean.dupont@example.com + + +
+); +``` + +## Usage + +The Table component is a compound component with the following sub-components: + +- `Table.THead` - Table header with optional `variant` prop +- `Table.TBody` - Table body with optional `variant` prop +- `Table.Tr` - Table row with optional `size` and variant props +- `Table.Th` - Table header cell with optional `onSort`, `onCheck`, `checkboxPosition` props +- `Table.Td` - Table data cell with optional `position`, `verticalAlign`, `variant` and `size` props + +## Examples + +### Basic Table + + + +### Alternate Variants + +Use `variant="alternate"` on `Table.TBody` to get zebra-striped rows. + + + +### With Tags + +Tables work great with other components like Tags for status indicators. + + + +### With Buttons + +Tables can include interactive elements like buttons for actions. + + + +### Different Sizes + +Use the `size` prop on `Table.Tr` to control row height ("S", "M", "L"). + + + +### Text Alignment + +Use the `position` prop on `Table.Td` to align cell content ("left", "center", "right"). + + + +### Compact Table + + diff --git a/apps/look-and-feel-stories/src/components/Table/Table.stories.tsx b/apps/look-and-feel-stories/src/components/Table/Table.stories.tsx new file mode 100644 index 000000000..d6f03f03c --- /dev/null +++ b/apps/look-and-feel-stories/src/components/Table/Table.stories.tsx @@ -0,0 +1,486 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { action } from "@storybook/addon-actions"; +import { + Table, + Button, + Tag, + type HeadColorVariants, + type BodyColorVariants, + type RowSizeVariants, +} from "@axa-fr/canopee-react/client"; + +interface TableStoryArgs { + theadVariant?: HeadColorVariants; + tbodyVariant?: BodyColorVariants; + rowSize?: RowSizeVariants; + row1Size?: RowSizeVariants; + row2Size?: RowSizeVariants; + row3Size?: RowSizeVariants; + row4Size?: RowSizeVariants; +} + +const meta: Meta = { + title: "Components/Table", + component: Table, +}; + +export default meta; + +type Story = StoryObj; + +export const BasicTable: Story = { + name: "Tableau basique", + args: { + theadVariant: undefined, + tbodyVariant: "alternate", + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Nom + Prénom + Email + Téléphone + + + + + Dupont + Jean + jean.dupont@example.com + 06 12 34 56 78 + + + Martin + Marie + marie.martin@example.com + 06 98 76 54 32 + + + Bernard + Pierre + pierre.bernard@example.com + 06 11 22 33 44 + + + Dubois + Sophie + sophie.dubois@example.com + 06 55 66 77 88 + + +
+ ), +}; + +export const AlternateVariantTable: Story = { + name: "Tableau avec couleurs alternées", + args: { + theadVariant: "gray", + tbodyVariant: "alternate", + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Produit + Catégorie + Prix + Stock + + + + + Ordinateur Portable + Électronique + 899,00 € + 15 + + + Souris sans fil + Accessoires + 29,99 € + 50 + + + Clavier mécanique + Accessoires + 89,00 € + 23 + + + Écran 27 + Électronique + 299,00 € + 8 + + +
+ ), +}; + +export const TableWithTags: Story = { + name: "Tableau avec tags, statuts et tri", + args: { + theadVariant: "gray", + tbodyVariant: "alternate", + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Référence + Statut + Client + Montant + + + + + REF-001 + + Validé + + Jean Dupont + 220,00 € + + + REF-002 + + En attente + + Marie Martin + 450,00 € + + + REF-003 + + Rejeté + + Pierre Bernard + 180,00 € + + + REF-004 + + En cours + + Sophie Dubois + 320,00 € + + +
+ ), +}; + +export const TableWithButtons: Story = { + name: "Tableau avec sélection et actions", + args: { + theadVariant: "gray", + tbodyVariant: undefined, + rowSize: "M", + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + rowSize: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille des lignes", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Utilisateur + Email + Rôle + Actions + + + + + Jean Dupont + jean.dupont@example.com + Administrateur + + + + + + Marie Martin + marie.martin@example.com + Éditeur + + + + + + Pierre Bernard + pierre.bernard@example.com + Lecteur + + + + + + Sophie Dubois + sophie.dubois@example.com + Éditeur + + + + + +
+ ), +}; + +export const TableWithDifferentSizes: Story = { + name: "Tableau avec tailles de lignes variées", + args: { + theadVariant: "gray", + tbodyVariant: "alternate", + row1Size: "S", + row2Size: "M", + row3Size: "L", + row4Size: undefined, + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + row1Size: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille de la ligne 1", + }, + row2Size: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille de la ligne 2", + }, + row3Size: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille de la ligne 3", + }, + row4Size: { + control: { type: "select" }, + options: ["S", "M", "L"], + description: "Taille de la ligne 4", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Nom + Description + Prix + Disponibilité + + + + + Produit A + Description courte + 49,99 € + En stock + + + Produit B + Description de longueur moyenne pour ce produit + 79,99 € + En stock + + + Produit C + + Description très détaillée avec beaucoup informations + + 129,99 € + Stock limité + + + Produit D + Description standard + 99,99 € + Sur commande + + +
+ ), +}; + +export const TableWithAlignments: Story = { + name: "Tableau avec alignements différents", + args: { + theadVariant: "gray", + tbodyVariant: undefined, + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Article + Quantité + Prix unitaire + Total + + + + + Ordinateur + 1 + 899,00 € + 899,00 € + + + Souris + 2 + 29,99 € + 59,98 € + + + Clavier + 1 + 89,00 € + 89,00 € + + + Câble HDMI + 3 + 15,99 € + 47,97 € + + +
+ ), +}; + +export const CompactTable: Story = { + name: "Tableau compact (3 colonnes)", + args: { + theadVariant: "gray", + tbodyVariant: undefined, + }, + argTypes: { + theadVariant: { + control: { type: "select" }, + options: ["gray", "blue"], + description: "Variant de l'en-tête du tableau", + }, + tbodyVariant: { + control: { type: "select" }, + options: ["white", "blue", "alternate"], + description: "Variant du corps du tableau", + }, + }, + render: (args: TableStoryArgs) => ( + + + + Nom + Statut + Date + + + + + Projet Alpha + + Terminé + + 10/01/2026 + + + Projet Beta + + En cours + + 15/01/2026 + + + Projet Gamma + + Planifié + + 20/01/2026 + + + Projet Delta + + Annulé + + 05/01/2026 + + +
+ ), +}; diff --git a/apps/look-and-feel-stories/src/components/TableMobileCard/TableMobileCard.mdx b/apps/look-and-feel-stories/src/components/TableMobileCard/TableMobileCard.mdx new file mode 100644 index 000000000..7dc240506 --- /dev/null +++ b/apps/look-and-feel-stories/src/components/TableMobileCard/TableMobileCard.mdx @@ -0,0 +1,47 @@ +import { Canvas, Controls, Meta } from "@storybook/addon-docs"; +import * as TableMobileCardStories from "./TableMobileCard.stories"; + + + +# TableMobileCard + +> ⚠️ This component should not be used alone. It is the mobile version for a table. + +To use the table mobile card import it like that: + +```tsx +import { TableMobileCard } from "@axa-fr/canopee-react/client"; + +const MyComponent = () => ( + + + Produit/Support + AB Sustainable Global Thematic A + + + Code Isin + LU0101010101 + + + Status + Ouvert + + +); +``` + +## Usage + +The TableMobileCard component is a compound component with the following sub-components: + +- `TableMobileCard.DRow` - TableMobileCard row wrapper with optional `direction` prop +- `TableMobileCard.Dt` - Table row title +- `TableMobileCard.Dd` - Table row description + +## Examples + +### Basic Table + + + + diff --git a/apps/look-and-feel-stories/src/components/TableMobileCard/TableMobileCard.stories.tsx b/apps/look-and-feel-stories/src/components/TableMobileCard/TableMobileCard.stories.tsx new file mode 100644 index 000000000..d64301dd8 --- /dev/null +++ b/apps/look-and-feel-stories/src/components/TableMobileCard/TableMobileCard.stories.tsx @@ -0,0 +1,94 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { action } from "@storybook/addon-actions"; +import { Button, Icon, TableMobileCard } from "@axa-fr/canopee-react/client"; +import download from "@material-symbols/svg-400/outlined/download_2-fill.svg"; + +interface TableMobileCardStoryArgs { + variant?: "alternate" | "blue" | "white"; + direction?: "row" | "column"; +} + +const meta: Meta = { + title: "Components/TableMobileCard", + component: TableMobileCard, +}; + +export default meta; + +type Story = StoryObj; + +export const BasicTableMobileCard: Story = { + name: "Table Card basique", + args: { + variant: "alternate", + direction: "row", + }, + argTypes: { + variant: { + control: { type: "select" }, + options: ["alternate", "white", "blue"], + description: "Variant de la couleur des lignes", + }, + direction: { + control: { type: "select" }, + options: ["row", "column"], + description: "Variant de la disposition des éléments d'une ligne", + }, + }, + render: (args: TableMobileCardStoryArgs) => ( + + + Produit/Support + + AB Sustainable Global Thematic A + + + + Code Isin + LU0101010101 + + + Status + Ouvert + + + Qualification SFDR + 9 + + + DIC/DIS/DICI + + + + + + Prospectus + + + + + + Rapport annuelle + + + + + + ), +}; diff --git a/apps/look-and-feel-stories/src/pages/Table/Table.css b/apps/look-and-feel-stories/src/pages/Table/Table.css new file mode 100644 index 000000000..92bdd70b0 --- /dev/null +++ b/apps/look-and-feel-stories/src/pages/Table/Table.css @@ -0,0 +1,32 @@ +.table-scroll { + margin-inline: calc(-1 * var(--margin-inline)); + padding-inline: var(--margin-inline); + overflow: auto hidden; +} + +main.table-page { + --font-size-base: 16; + + padding-block: calc(40 / var(--font-size-base) * 1rem); + + @media (width > 1023px) { + padding-block: calc(48 / var(--font-size-base) * 1rem); + } + + > section.subgrid { + row-gap: calc(28 / var(--font-size-base) * 1rem); + + @media (width > 1023px) { + --row-gap: calc(48 / var(--font-size-base) * 1rem); + } + + .table-page__dropdown { + --cols: 4; + } + + .table-page__pagination { + width: fit-content; + justify-self: center; + } + } +} \ No newline at end of file diff --git a/apps/look-and-feel-stories/src/pages/Table/Table.stories.tsx b/apps/look-and-feel-stories/src/pages/Table/Table.stories.tsx new file mode 100644 index 000000000..7096dfaed --- /dev/null +++ b/apps/look-and-feel-stories/src/pages/Table/Table.stories.tsx @@ -0,0 +1,16 @@ +import "./Table.css"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { TablePage } from "./Table"; + +const meta: Meta = { + title: "Pages/Table", + parameters: { layout: "fullscreen" }, +}; + +export default meta; + +export const StepSortie: StoryObj = { + name: "Table", + render: TablePage, +}; diff --git a/apps/look-and-feel-stories/src/pages/Table/Table.tsx b/apps/look-and-feel-stories/src/pages/Table/Table.tsx new file mode 100644 index 000000000..c10b7fd2e --- /dev/null +++ b/apps/look-and-feel-stories/src/pages/Table/Table.tsx @@ -0,0 +1,450 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import React, { + useCallback, + useMemo, + useState, + useSyncExternalStore, +} from "react"; +import { + Button, + DebugGrid, + Dropdown, + Pagination, + Table, + TableMobileCard, + Tag, + Heading, +} from "@axa-fr/canopee-react/client"; + +const useIsSmallScreen = (breakPointToCheck: number) => { + const subscribe = useCallback((listener: () => void) => { + window.addEventListener("resize", listener); + return () => { + window.removeEventListener("resize", listener); + }; + }, []); + + const getSnapshot = useCallback(() => { + return window.innerWidth <= breakPointToCheck; + }, [breakPointToCheck]); + + const getServerSnapshot = useCallback(() => false, []); + + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); +}; + +interface DataRow { + id: number; + reference: string; + societe: string; + contact: string; + email: string; + telephone: string; + montant: number; + statut: "Validé" | "En attente" | "Rejeté" | "En cours"; + date: string; +} + +const ALL_DATA: DataRow[] = [ + { + id: 1, + reference: "CON-2024-001", + societe: "Société ABC", + contact: "Jean Dupont", + email: "jean.dupont@abc.fr", + telephone: "06 12 34 56 78", + montant: 1250.0, + statut: "Validé", + date: "15/01/2024", + }, + { + id: 2, + reference: "CON-2024-002", + societe: "Entreprise XYZ", + contact: "Marie Martin", + email: "marie.martin@xyz.fr", + telephone: "06 98 76 54 32", + montant: 2500.0, + statut: "En attente", + date: "22/03/2024", + }, + { + id: 3, + reference: "CON-2024-003", + societe: "Groupe DEF", + contact: "Pierre Bernard", + email: "pierre.bernard@def.fr", + telephone: "06 11 22 33 44", + montant: 3750.0, + statut: "Rejeté", + date: "10/06/2024", + }, + { + id: 4, + reference: "CON-2024-004", + societe: "Industries GHI", + contact: "Sophie Dubois", + email: "sophie.dubois@ghi.fr", + telephone: "06 55 66 77 88", + montant: 890.0, + statut: "En cours", + date: "05/09/2024", + }, + { + id: 5, + reference: "CON-2024-005", + societe: "Services JKL", + contact: "Luc Moreau", + email: "luc.moreau@jkl.fr", + telephone: "06 22 33 44 55", + montant: 1780.0, + statut: "Validé", + date: "12/11/2024", + }, + { + id: 6, + reference: "CON-2024-006", + societe: "Tech MNO", + contact: "Claire Petit", + email: "claire.petit@mno.fr", + telephone: "06 77 88 99 00", + montant: 4200.0, + statut: "En attente", + date: "28/12/2024", + }, + { + id: 7, + reference: "CON-2024-007", + societe: "Solutions PQR", + contact: "Marc Lefebvre", + email: "marc.lefebvre@pqr.fr", + telephone: "06 33 44 55 66", + montant: 920.0, + statut: "Validé", + date: "03/01/2025", + }, + { + id: 8, + reference: "CON-2024-008", + societe: "Digital STU", + contact: "Anne Rousseau", + email: "anne.rousseau@stu.fr", + telephone: "06 44 55 66 77", + montant: 1560.0, + statut: "En cours", + date: "18/02/2025", + }, + { + id: 9, + reference: "CON-2024-009", + societe: "Consulting VWX", + contact: "Thomas Garnier", + email: "thomas.garnier@vwx.fr", + telephone: "06 88 99 00 11", + montant: 3100.0, + statut: "Validé", + date: "25/03/2025", + }, + { + id: 10, + reference: "CON-2024-010", + societe: "Innovation YZ", + contact: "Julie Faure", + email: "julie.faure@yz.fr", + telephone: "06 99 00 11 22", + montant: 2890.0, + statut: "En attente", + date: "07/04/2025", + }, + { + id: 11, + reference: "CON-2024-011", + societe: "Partners AB", + contact: "Vincent Leroux", + email: "vincent.leroux@ab.fr", + telephone: "06 00 11 22 33", + montant: 1450.0, + statut: "Rejeté", + date: "14/05/2025", + }, + { + id: 12, + reference: "CON-2024-012", + societe: "Business CD", + contact: "Isabelle Simon", + email: "isabelle.simon@cd.fr", + telephone: "06 11 22 33 44", + montant: 3680.0, + statut: "Validé", + date: "22/06/2025", + }, + { + id: 13, + reference: "CON-2024-013", + societe: "Global EF", + contact: "Nicolas Laurent", + email: "nicolas.laurent@ef.fr", + telephone: "06 22 33 44 55", + montant: 2100.0, + statut: "En cours", + date: "30/07/2025", + }, + { + id: 14, + reference: "CON-2024-014", + societe: "Pro GH", + contact: "Sandrine Michel", + email: "sandrine.michel@gh.fr", + telephone: "06 33 44 55 66", + montant: 1890.0, + statut: "En attente", + date: "08/08/2025", + }, + { + id: 15, + reference: "CON-2024-015", + societe: "Expert IJ", + contact: "Julien Leroy", + email: "julien.leroy@ij.fr", + telephone: "06 44 55 66 77", + montant: 4500.0, + statut: "Validé", + date: "15/09/2025", + }, +]; + +const STATUS_VARIANTS: Record< + DataRow["statut"], + "success" | "warning" | "error" | "info" +> = { + Validé: "success", + "En attente": "warning", + Rejeté: "error", + "En cours": "info", +}; + +const getStatusTag = (statut: DataRow["statut"]) => ( + {statut} +); + +type SortConfig = { key: keyof DataRow; direction: "asc" | "desc" } | null; + +export const TablePage = () => { + const [sortConfig, setSortConfig] = useState(null); + const [checkedColumns, setCheckedColumns] = useState>( + () => new Set(), + ); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(5); + + const isMobile = useIsSmallScreen(667); + + const handleSort = useCallback((key: keyof DataRow) => { + setSortConfig((prev) => { + const direction: "asc" | "desc" = + prev?.key === key && prev.direction === "asc" ? "desc" : "asc"; + return { key, direction }; + }); + setCurrentPage(1); + }, []); + + const handleColumnCheck = useCallback((columnIndex: number) => { + setCheckedColumns((prev) => { + const next = new Set(prev); + next.has(columnIndex) ? next.delete(columnIndex) : next.add(columnIndex); + return next; + }); + }, []); + + const getColumnVariant = useCallback( + (columnIndex: number) => { + return checkedColumns.has(columnIndex) ? "blue" : undefined; + }, + [checkedColumns], + ); + + const sortedData = useMemo(() => { + if (!sortConfig) return ALL_DATA; + + const { key, direction } = sortConfig; + + return [...ALL_DATA].sort((a, b) => { + const aValue = a[key]; + const bValue = b[key]; + + // numbers + if (typeof aValue === "number" && typeof bValue === "number") { + return direction === "asc" ? aValue - bValue : bValue - aValue; + } + + // everything else as string + const aStr = String(aValue); + const bStr = String(bValue); + return direction === "asc" + ? aStr.localeCompare(bStr) + : bStr.localeCompare(aStr); + }); + }, [sortConfig]); + + const totalPages = useMemo( + () => Math.max(1, Math.ceil(sortedData.length / itemsPerPage)), + [sortedData.length, itemsPerPage], + ); + + const currentData = useMemo(() => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return sortedData.slice(startIndex, endIndex); + }, [sortedData, currentPage, itemsPerPage]); + + const handlePageChange = useCallback( + (page: number) => setCurrentPage(page), + [], + ); + + const handleItemsPerPageChange = useCallback( + (event: React.ChangeEvent) => { + setItemsPerPage(Number(event.target.value)); + setCurrentPage(1); + }, + [], + ); + + return ( + <> + +
+
+ Liste des produits + {!isMobile && ( + <> +
+ + + + + + +
+ +
+ + + + handleColumnCheck(0)} + onSort={() => handleSort("reference")} + > + Référence + + handleColumnCheck(1)} + onSort={() => handleSort("societe")} + > + Société + + handleColumnCheck(2)} + onSort={() => handleSort("contact")} + > + Contact + + handleColumnCheck(3)} + onSort={() => handleSort("montant")} + > + Montant + + handleColumnCheck(4)} + onSort={() => handleSort("statut")} + > + Statut + + + + + + {currentData.map((row) => ( + + + {row.reference} + + + {row.societe} + + + {row.contact} + + + {row.montant.toFixed(2)} € + + + {getStatusTag(row.statut)} + + + ))} + +
+
+ + + + )} + + {isMobile ? ( + <> + {sortedData.map((elem) => ( + + + Référence + {elem.reference} + + + + Société + {elem.societe} + + + + Contact + {elem.contact} + + + + Montant + + {elem.montant.toFixed(2)} € + + + + + Status + + {getStatusTag(elem.statut)} + + + + ))} + + ) : null} +
+
+ + ); +}; diff --git a/package-lock.json b/package-lock.json index a7eca8a3c..3cef398ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4905,10 +4905,12 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.2", + "version": "19.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", + "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { @@ -7048,7 +7050,9 @@ "license": "MIT" }, "node_modules/csstype": { - "version": "3.1.3", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/damerau-levenshtein": { diff --git a/package.json b/package.json index 4daf6e124..1c3eb5808 100644 --- a/package.json +++ b/package.json @@ -61,4 +61,4 @@ "node": "22.21.0", "npm": "11.6.4" } -} \ No newline at end of file +} diff --git a/packages/canopee-css/src/prospect-client/Table/TableApollo.css b/packages/canopee-css/src/prospect-client/Table/TableApollo.css new file mode 100644 index 000000000..5fa17ad59 --- /dev/null +++ b/packages/canopee-css/src/prospect-client/Table/TableApollo.css @@ -0,0 +1,5 @@ +@import "./TableCommon.css"; + +.af-table { + --table-header-bg-blue: var(--blue-080); +} diff --git a/packages/canopee-css/src/prospect-client/Table/TableCommon.css b/packages/canopee-css/src/prospect-client/Table/TableCommon.css new file mode 100644 index 000000000..7418818c8 --- /dev/null +++ b/packages/canopee-css/src/prospect-client/Table/TableCommon.css @@ -0,0 +1,157 @@ +.af-table { + all: unset; + display: table; + box-sizing: border-box; + width: 100%; + border-collapse: collapse; + line-height: var(--rem-20); + + .af-table__th { + padding: 0; + + .af-table__th-wrapper { + display: flex; + box-sizing: border-box; + height: 100%; + padding: var(--rem-16); + place-items: center; + + .af-table__th-sort-icon { + width: 40px; + cursor: pointer; + } + } + + .af-table__th-content { + display: flex; + flex: 1; + align-items: center; + font-size: var(--rem-16ptable); + font-weight: 600; + text-align: left; + } + + &.af-table__th--center .af-table__th-content { + justify-content: center; + text-align: center; + } + + &.af-table__th--right .af-table__th-content { + justify-content: flex-end; + text-align: right; + } + + &:has(input[type="checkbox"]) { + .af-table__th-content { + padding-left: var(--rem-16); + } + + &.af-table__th--checkbox-right { + .af-table__th-wrapper { + flex-direction: row-reverse; + } + + .af-table__th-content { + padding-right: var(--rem-16); + padding-left: 0; + } + } + } + } + + .af-table__td { + box-sizing: border-box; + padding-inline: var(--rem-16); + white-space: normal; + + &.af-table__td--left { + text-align: left; + } + + &.af-table__td--center { + text-align: center; + } + + &.af-table__td--right { + text-align: right; + } + + &.af-table__td--top { + padding-top: var(--rem-16); + vertical-align: top; + } + + &.af-table__td--middle { + vertical-align: middle; + } + + &.af-table__td--small { + padding-block: calc(10 / var(--font-size-base) * 1rem); + } + + &.af-table__td--medium { + padding-block: calc(18 / var(--font-size-base) * 1rem); + } + + &.af-table__td--large { + padding-block: calc(30 / var(--font-size-base) * 1rem); + } + } + + .af-table__tr { + border-bottom: 1px solid var(--blue-200); + + .af-table__td { + padding-block: calc(10 / var(--font-size-base) * 1rem); + } + + &.af-table__tr--medium { + .af-table__td { + padding-block: calc(18 / var(--font-size-base) * 1rem); + } + } + + &.af-table__tr--large { + .af-table__td { + padding-block: calc(30 / var(--font-size-base) * 1rem); + } + } + } + + .af-table__thead { + &.af-table__thead--gray .af-table__th { + color: var(--gray-1000); + background-color: var(--gray-050); + } + + &.af-table__thead--blue .af-table__th { + color: var(--blue-1000); + background-color: var(--table-header-bg-blue); + } + } + + .af-table__tbody { + font-weight: 600; + color: var(--gray-1000); + + &.af-table__tbody--white .af-table__tr, + & .af-table__td--white { + background: var(--white-1000); + } + + &.af-table__tbody--blue .af-table__tr, + & .af-table__td--blue { + background: var(--blue-040); + } + + &.af-table__tbody--alternate .af-table__tr { + &:nth-child(even) { + background: var(--blue-040); + } + + &:nth-child(odd) { + background: var(--white-1000); + } + } + } +} diff --git a/packages/canopee-css/src/prospect-client/Table/TableLF.css b/packages/canopee-css/src/prospect-client/Table/TableLF.css new file mode 100644 index 000000000..e99ca9d00 --- /dev/null +++ b/packages/canopee-css/src/prospect-client/Table/TableLF.css @@ -0,0 +1,5 @@ +@import "./TableCommon.css"; + +.af-table { + --table-header-bg-blue: var(--blue-100); +} diff --git a/packages/canopee-css/src/prospect-client/TableMobileCard/TableMobileCardAll.css b/packages/canopee-css/src/prospect-client/TableMobileCard/TableMobileCardAll.css new file mode 100644 index 000000000..cb25f88e4 --- /dev/null +++ b/packages/canopee-css/src/prospect-client/TableMobileCard/TableMobileCardAll.css @@ -0,0 +1,64 @@ +.af-table-mobile-card { + border: 1px solid var(--gray-250); + border-radius: var(--radius-8); + overflow: hidden; + font-size: calc(16 / var(--font-size-base) * 1rem); + line-height: calc(20 / var(--font-size-base) * 1rem); + color: var(--gray-1000); + + .af-table-mobile-card__drow { + display: flex; + padding: var(--rem-16); + border-bottom: 1px solid var(--blue-200); + justify-content: space-between; + gap: var(--rem-16); + + &:last-child { + border-bottom: none; + } + + & dt.af-table-mobile-card__dt { + width: 50%; + font-weight: 400; + } + + & dd.af-table-mobile-card__dd { + width: 50%; + justify-items: end; + font-weight: 600; + text-align: right; + } + + &.af-table-mobile-card__drow--column { + flex-direction: column; + + & dt.af-table-mobile-card__dt { + width: 100%; + } + + & dd.af-table-mobile-card__dd { + width: 100%; + justify-items: start; + text-align: left; + } + } + } + + &.af-table-mobile-card--white .af-table-mobile-card__drow { + background-color: var(--white-1000); + } + + &.af-table-mobile-card--blue .af-table-mobile-card__drow { + background-color: var(--blue-040); + } + + &.af-table-mobile-card--alternate { + .af-table-mobile-card__drow:nth-child(even) { + background-color: var(--blue-040); + } + + .af-table-mobile-card__drow:nth-child(odd) { + background-color: var(--white-1000); + } + } +} diff --git a/packages/canopee-css/src/prospect-client/client.css b/packages/canopee-css/src/prospect-client/client.css index 0dadffd29..4cf4177a1 100644 --- a/packages/canopee-css/src/prospect-client/client.css +++ b/packages/canopee-css/src/prospect-client/client.css @@ -54,4 +54,6 @@ @import "./List/List/ListLF.css"; @import "./LevelSelector/LevelSelectorLF.css"; @import "./Skeleton/SkeletonLF.css"; +@import "./Table/TableLF.css"; @import "./Fieldset/FieldsetLF.css"; +@import "./TableMobileCard/TableMobileCardAll.css"; diff --git a/packages/canopee-css/src/prospect-client/prospect.css b/packages/canopee-css/src/prospect-client/prospect.css index c7b4d8a13..4ff48d26b 100644 --- a/packages/canopee-css/src/prospect-client/prospect.css +++ b/packages/canopee-css/src/prospect-client/prospect.css @@ -54,4 +54,6 @@ @import "./List/List/ListApollo.css"; @import "./LevelSelector/LevelSelectorApollo.css"; @import "./Skeleton/SkeletonApollo.css"; +@import "./Table/TableApollo.css"; @import "./Fieldset/FieldsetApollo.css"; +@import "./TableMobileCard/TableMobileCardAll.css"; diff --git a/packages/canopee-react/src/client.ts b/packages/canopee-react/src/client.ts index 8a4287d6e..d5a5e1dcd 100644 --- a/packages/canopee-react/src/client.ts +++ b/packages/canopee-react/src/client.ts @@ -162,3 +162,14 @@ export { } from "./prospect-client/Tag/TagLF"; export { TimelineVertical } from "./prospect-client/TimelineVertical/TimelineVerticalLF"; export { Toggle } from "./prospect-client/Toggle/ToggleLF"; +export { + Table, + type TableProps, + type HeadColorVariants, + type BodyColorVariants, + type RowSizeVariants, +} from "./prospect-client/Table/TableLF"; +export { + TableMobileCard, + type TableMobileCardProps, +} from "./prospect-client/TableMobileCard/TableMobileCard"; diff --git a/packages/canopee-react/src/prospect-client/Table/TBody.tsx b/packages/canopee-react/src/prospect-client/Table/TBody.tsx new file mode 100644 index 000000000..19e06e290 --- /dev/null +++ b/packages/canopee-react/src/prospect-client/Table/TBody.tsx @@ -0,0 +1,26 @@ +import { ComponentPropsWithRef } from "react"; +import { getClassName } from "../utilities/getClassName"; + +export type BodyColorVariants = "white" | "blue" | "alternate"; + +export type TBodyProps = ComponentPropsWithRef<"tbody"> & { + variant?: BodyColorVariants; +}; + +export const TBody = ({ + variant = "white", + className, + children, + ...tableBodyProps +}: TBodyProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table__tbody", + className, + modifiers: [variant], + }); + return ( + + {children} + + ); +}; diff --git a/packages/canopee-react/src/prospect-client/Table/THead.tsx b/packages/canopee-react/src/prospect-client/Table/THead.tsx new file mode 100644 index 000000000..2292d2f05 --- /dev/null +++ b/packages/canopee-react/src/prospect-client/Table/THead.tsx @@ -0,0 +1,26 @@ +import { type ComponentPropsWithRef } from "react"; +import { getClassName } from "../utilities/getClassName"; + +export type HeadColorVariants = "gray" | "blue"; + +export type THeadProps = ComponentPropsWithRef<"thead"> & { + variant?: HeadColorVariants; +}; + +export const THead = ({ + variant = "blue", + className, + children, + ...tableHeadProps +}: THeadProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table__thead", + className, + modifiers: [variant], + }); + return ( + + {children} + + ); +}; diff --git a/packages/canopee-react/src/prospect-client/Table/TableApollo.tsx b/packages/canopee-react/src/prospect-client/Table/TableApollo.tsx new file mode 100644 index 000000000..034b0d80d --- /dev/null +++ b/packages/canopee-react/src/prospect-client/Table/TableApollo.tsx @@ -0,0 +1,9 @@ +import "@axa-fr/canopee-css/prospect/Table/TableApollo.css"; + +export { + Table, + type TableProps, + type HeadColorVariants, + type BodyColorVariants, + type RowSizeVariants, +} from "./TableCommon"; diff --git a/packages/canopee-react/src/prospect-client/Table/TableCommon.tsx b/packages/canopee-react/src/prospect-client/Table/TableCommon.tsx new file mode 100644 index 000000000..9edfee9e7 --- /dev/null +++ b/packages/canopee-react/src/prospect-client/Table/TableCommon.tsx @@ -0,0 +1,28 @@ +import { type ComponentPropsWithRef } from "react"; +import { getClassName } from "../utilities/getClassName"; +import { Td } from "./Td"; +import { Tr, type RowSizeVariants } from "./Tr"; +import { Th } from "./Th"; +import { TBody, type BodyColorVariants } from "./TBody"; +import { THead, type HeadColorVariants } from "./THead"; + +export type { HeadColorVariants, BodyColorVariants, RowSizeVariants }; +export type TableProps = ComponentPropsWithRef<"table">; + +export const Table = ({ className, children, ...tableProps }: TableProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table", + className, + }); + return ( + + {children} +
+ ); +}; + +Table.THead = THead; +Table.TBody = TBody; +Table.Th = Th; +Table.Tr = Tr; +Table.Td = Td; diff --git a/packages/canopee-react/src/prospect-client/Table/TableLF.tsx b/packages/canopee-react/src/prospect-client/Table/TableLF.tsx new file mode 100644 index 000000000..0f4dae886 --- /dev/null +++ b/packages/canopee-react/src/prospect-client/Table/TableLF.tsx @@ -0,0 +1,9 @@ +import "@axa-fr/canopee-css/client/Table/TableLF.css"; + +export { + Table, + type TableProps, + type HeadColorVariants, + type BodyColorVariants, + type RowSizeVariants, +} from "./TableCommon"; diff --git a/packages/canopee-react/src/prospect-client/Table/Td.tsx b/packages/canopee-react/src/prospect-client/Table/Td.tsx new file mode 100644 index 000000000..c2b03c7dc --- /dev/null +++ b/packages/canopee-react/src/prospect-client/Table/Td.tsx @@ -0,0 +1,40 @@ +import { ComponentPropsWithRef } from "react"; +import { getClassName } from "../utilities/getClassName"; + +export type CellContentPositionVariants = "left" | "center" | "right"; +export type CellContentVerticalAlignVariants = "top" | "middle"; +export const tdSizeVariants = { + L: "large", + M: "medium", + S: "small", +} as const; +export type TdSizeVariants = keyof typeof tdSizeVariants; +export type CellColorVariant = "white" | "blue"; + +export type TdProps = ComponentPropsWithRef<"td"> & { + position?: CellContentPositionVariants; + verticalAlign?: CellContentVerticalAlignVariants; + size?: TdSizeVariants; + variant?: CellColorVariant; +}; + +export const Td = ({ + position = "left", + verticalAlign = "middle", + size, + variant, + className, + children, + ...tableCellProps +}: TdProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table__td", + className, + modifiers: [position, verticalAlign, size && tdSizeVariants[size], variant], + }); + return ( + + {children} + + ); +}; diff --git a/packages/canopee-react/src/prospect-client/Table/Th.tsx b/packages/canopee-react/src/prospect-client/Table/Th.tsx new file mode 100644 index 000000000..dd7aa07e7 --- /dev/null +++ b/packages/canopee-react/src/prospect-client/Table/Th.tsx @@ -0,0 +1,46 @@ +import { type ComponentPropsWithRef } from "react"; +import unfoldMore from "@material-symbols/svg-400/rounded/unfold_more-fill.svg"; +import { getClassName } from "../utilities/getClassName"; +import { Checkbox } from "../Form/Checkbox/Checkbox/CheckboxCommon"; +import { ClickIcon } from "../ClickIcon/ClickIconCommon"; + +export type HeaderCellPositionVariants = "left" | "center" | "right"; + +export type ThProps = ComponentPropsWithRef<"th"> & { + position?: HeaderCellPositionVariants; + checkboxPosition?: HeaderCellPositionVariants; + onCheck?: () => void; + onSort?: () => void; +}; + +export const Th = ({ + position = "left", + onCheck, + checkboxPosition = "left", + onSort, + className, + children, + ...tableHeaderProps +}: ThProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table__th", + className, + modifiers: [position, checkboxPosition && `checkbox-${checkboxPosition}`], + }); + return ( + +
+ {onCheck ? : null} + {children} + {onSort ? ( + + ) : null} +
+ + ); +}; diff --git a/packages/canopee-react/src/prospect-client/Table/Tr.tsx b/packages/canopee-react/src/prospect-client/Table/Tr.tsx new file mode 100644 index 000000000..3d49ed79b --- /dev/null +++ b/packages/canopee-react/src/prospect-client/Table/Tr.tsx @@ -0,0 +1,31 @@ +import { type ComponentPropsWithRef } from "react"; +import { getClassName } from "../utilities/getClassName"; + +export const rowSizeVariants = { + L: "large", + M: "medium", + S: "small", +} as const; +export type RowSizeVariants = keyof typeof rowSizeVariants; + +export type TrProps = ComponentPropsWithRef<"tr"> & { + size?: RowSizeVariants; +}; + +export const Tr = ({ + size = "S", + className, + children, + ...tableRowProps +}: TrProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table__tr", + className, + modifiers: [rowSizeVariants[size]], + }); + return ( + + {children} + + ); +}; diff --git a/packages/canopee-react/src/prospect-client/TableMobileCard/DRow.tsx b/packages/canopee-react/src/prospect-client/TableMobileCard/DRow.tsx new file mode 100644 index 000000000..1e1180948 --- /dev/null +++ b/packages/canopee-react/src/prospect-client/TableMobileCard/DRow.tsx @@ -0,0 +1,26 @@ +import { type ComponentPropsWithRef } from "react"; +import { getClassName } from "../utilities/getClassName"; + +export type DRowDirectionVariants = "row" | "column"; + +export type TrProps = ComponentPropsWithRef<"div"> & { + direction?: DRowDirectionVariants; +}; + +export const DRow = ({ + className, + children, + direction = "row", + ...dRowProps +}: TrProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table-mobile-card__drow", + className, + modifiers: [direction], + }); + return ( +
+ {children} +
+ ); +}; diff --git a/packages/canopee-react/src/prospect-client/TableMobileCard/Dd.tsx b/packages/canopee-react/src/prospect-client/TableMobileCard/Dd.tsx new file mode 100644 index 000000000..d73caf456 --- /dev/null +++ b/packages/canopee-react/src/prospect-client/TableMobileCard/Dd.tsx @@ -0,0 +1,16 @@ +import { type ComponentPropsWithRef } from "react"; +import { getClassName } from "../utilities/getClassName"; + +export type TrProps = ComponentPropsWithRef<"dd">; + +export const Dd = ({ className, children, ...ddProps }: TrProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table-mobile-card__dd", + className, + }); + return ( +
+ {children} +
+ ); +}; diff --git a/packages/canopee-react/src/prospect-client/TableMobileCard/Dt.tsx b/packages/canopee-react/src/prospect-client/TableMobileCard/Dt.tsx new file mode 100644 index 000000000..58c44989b --- /dev/null +++ b/packages/canopee-react/src/prospect-client/TableMobileCard/Dt.tsx @@ -0,0 +1,16 @@ +import { type ComponentPropsWithRef } from "react"; +import { getClassName } from "../utilities/getClassName"; + +export type TrProps = ComponentPropsWithRef<"dt">; + +export const Dt = ({ className, children, ...dtProps }: TrProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table-mobile-card__dt", + className, + }); + return ( +
+ {children} +
+ ); +}; diff --git a/packages/canopee-react/src/prospect-client/TableMobileCard/TableMobileCard.tsx b/packages/canopee-react/src/prospect-client/TableMobileCard/TableMobileCard.tsx new file mode 100644 index 000000000..243d72c97 --- /dev/null +++ b/packages/canopee-react/src/prospect-client/TableMobileCard/TableMobileCard.tsx @@ -0,0 +1,36 @@ +import { type ComponentPropsWithRef } from "react"; +import { getClassName } from "../utilities/getClassName"; +import { DRow } from "./DRow"; +import { Dt } from "./Dt"; +import { Dd } from "./Dd"; + +import "@axa-fr/canopee-css/prospect/TableMobileCard/TableMobileCardAll.css"; + +export type TableMobileCardVariants = "white" | "blue" | "alternate"; + +export type TableMobileCardProps = ComponentPropsWithRef<"dl"> & { + variant?: TableMobileCardVariants; +}; + +export const TableMobileCard = ({ + className, + children, + variant = "alternate", + ...tableCardProps +}: TableMobileCardProps) => { + const componentClassName = getClassName({ + baseClassName: "af-table-mobile-card", + className, + modifiers: [variant], + }); + + return ( +
+ {children} +
+ ); +}; + +TableMobileCard.DRow = DRow; +TableMobileCard.Dt = Dt; +TableMobileCard.Dd = Dd; diff --git a/packages/canopee-react/src/prospect-client/TableMobileCard/__test__/TableMobileCard.test.tsx b/packages/canopee-react/src/prospect-client/TableMobileCard/__test__/TableMobileCard.test.tsx new file mode 100644 index 000000000..9c6ad4b51 --- /dev/null +++ b/packages/canopee-react/src/prospect-client/TableMobileCard/__test__/TableMobileCard.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from "@testing-library/react"; +import { TableMobileCard } from "../TableMobileCard"; + +describe("TableMobileCard", () => { + it("should render correctly", () => { + render( + + + Nom + Dupont + + + Nom + Dupont + + , + ); + + const dt = screen.getByTestId("firstDRow"); + const dds = screen.getAllByRole("definition"); + + expect(dt).toHaveClass("af-table-mobile-card__drow--row"); + expect(dds).toHaveLength(2); + }); + + it("should render vertical row correctly", () => { + render( + + + Nom + Dupont + + , + ); + + const dt = screen.getByTestId("firstDRow"); + + expect(dt).toHaveClass("af-table-mobile-card__drow--column"); + }); +}); diff --git a/packages/canopee-react/src/prospect.ts b/packages/canopee-react/src/prospect.ts index ba9471a1b..019a08149 100644 --- a/packages/canopee-react/src/prospect.ts +++ b/packages/canopee-react/src/prospect.ts @@ -154,3 +154,11 @@ export { } from "./prospect-client/Tag/TagApollo"; export { TimelineVertical } from "./prospect-client/TimelineVertical/TimelineVerticalApollo"; export { Toggle } from "./prospect-client/Toggle/ToggleApollo"; +export { + Table, + type TableProps, + type HeadColorVariants, + type BodyColorVariants, + type RowSizeVariants, +} from "./prospect-client/Table/TableApollo"; +export { TableMobileCard } from "./prospect-client/TableMobileCard/TableMobileCard";