Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/web/src/design-audit/foundations.css
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,20 @@
color: var(--color-text-tertiary);
}

.design-audit-tables {
display: grid;
gap: calc(var(--spacing) * 6);
}

.design-audit-table-narrow {
width: calc(var(--spacing) * 80);
max-width: 100%;
}

.design-audit-table-narrow .design-audit-table {
min-width: calc(var(--spacing) * 160);
}

.design-audit-icon-states [data-icon-state="default"] [data-nucleo-icon] {
color: var(--color-icon);
}
Expand Down
67 changes: 66 additions & 1 deletion apps/web/src/design-audit/foundations.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { CheckIcon, InfoIcon, SirenIcon, WarningIcon } from "@chopin/icons";
import { Badge, IconLabel, MiniBars, ProgressBar, Sparkline } from "@chopin/visuals";
import {
Badge,
IconLabel,
MiniBars,
ProgressBar,
Sparkline,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@chopin/visuals";

import { AuditPlate } from "./frame";
import { IconCatalogue } from "./icons";
Expand Down Expand Up @@ -76,6 +88,45 @@ const PROGRESS_BARS = [
["Complete", 100],
] as const;

const TABLE_ROWS = [
["Production", "Healthy", "2 minutes ago"],
["Preview", "Building", "12 minutes ago"],
] as const;

const OVERFLOW_TABLE_ROWS = [
["Production Europe West with an intentionally long name", "Healthy", "2 minutes ago"],
["Preview for the static presentation table branch", "Building", "12 minutes ago"],
] as const;

function TableExample({
rows,
variant = "plain",
}: {
rows: readonly (readonly [string, string, string])[];
variant?: "plain" | "contained";
}) {
return (
<Table aria-label="Deployment activity" className="design-audit-table" variant={variant}>
<TableHeader>
<TableRow>
<TableHead scope="col">Environment</TableHead>
<TableHead scope="col">Status</TableHead>
<TableHead scope="col">Updated</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map(([environment, status, updated]) => (
<TableRow key={environment}>
<TableCell>{environment}</TableCell>
<TableCell>{status}</TableCell>
<TableCell>{updated}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}

export function Foundations() {
return (
<>
Expand Down Expand Up @@ -226,6 +277,20 @@ export function Foundations() {
))}
</div>
</AuditPlate>
<AuditPlate
item="presentation-table"
title="Presentation tables"
description="Static native tables for compact structured data, including narrow overflow."
>
<div className="design-audit-tables">
<div data-table-example="plain">
<TableExample rows={TABLE_ROWS} />
</div>
<div className="design-audit-table-narrow" data-table-example="contained-overflow">
<TableExample rows={OVERFLOW_TABLE_ROWS} variant="contained" />
</div>
</div>
</AuditPlate>
</>
);
}
6 changes: 6 additions & 0 deletions apps/web/src/design-audit/inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ export const AUDIT_INVENTORY: readonly AuditGroup[] = [
source: "packages/visuals/src/ui/progress-bar.tsx",
states: ["zero", "in-progress", "complete"],
},
{
id: "presentation-table",
label: "Presentation tables",
source: "packages/visuals/src/ui/table.tsx",
states: ["plain", "contained", "narrow-overflow"],
},
],
},
{
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/design-audit/specimens.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,37 @@ describe("design audit specimens", () => {
expect(specimen.match(/role="progressbar"/g)).toHaveLength(3);
});

it("adds presentation tables without replacing the authored editor table", () => {
let items = AUDIT_INVENTORY.flatMap(group => group.items);
let presentationTable = items.find(candidate => candidate.id === "presentation-table");
let authoredTable = items.find(candidate => candidate.id === "table");

expect(presentationTable).toEqual({
id: "presentation-table",
label: "Presentation tables",
source: "packages/visuals/src/ui/table.tsx",
states: ["plain", "contained", "narrow-overflow"],
});
expect(authoredTable).toEqual({
id: "table",
label: "Tables",
source: "packages/editor/src/table/chrome.tsx",
states: ["default", "selected-cell", "toolbar", "overflow"],
});

let markup = renderToStaticMarkup(createElement(Foundations));
let specimen = plate(markup, "presentation-table");
expect(specimen).toContain('data-audit-item="presentation-table"');
expect(specimen).toContain('data-table-example="plain"');
expect(specimen).toContain('data-table-example="contained-overflow"');
expect(specimen).toContain('data-variant="plain"');
expect(specimen).toContain('data-variant="contained"');
expect(specimen.match(/data-slot="table"/g)).toHaveLength(2);
expect(specimen.match(/aria-label="Deployment activity"/g)).toHaveLength(2);
expect(specimen).not.toContain("<caption");
expect(specimen).toContain("Production Europe West with an intentionally long name");
});

it("renders controls with their native accessibility states", () => {
let markup = renderToStaticMarkup(createElement(Controls));

Expand Down
19 changes: 19 additions & 0 deletions packages/visuals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,22 @@ export type { ProgressBarProps } from "./ui/progress-bar";
export type { SemanticTone } from "./ui/semantic-tone";
export { Sparkline } from "./ui/sparkline";
export type { SparklineProps } from "./ui/sparkline";
export {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "./ui/table";
export type {
TableBodyProps,
TableCellProps,
TableFooterProps,
TableHeaderProps,
TableHeadProps,
TableProps,
TableRowProps,
TableVariant,
} from "./ui/table";
1 change: 1 addition & 0 deletions packages/visuals/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
@import "./ui/mini-bars.css";
@import "./ui/progress-bar.css";
@import "./ui/sparkline.css";
@import "./ui/table.css";
48 changes: 48 additions & 0 deletions packages/visuals/src/ui/table.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.cv-table-container {
width: 100%;
max-width: 100%;
overflow-x: auto;
}

.cv-table-container[data-variant="contained"] {
border: var(--edge-width) solid var(--color-edge);
border-radius: var(--radius-lg);
}

.cv-table {
width: 100%;
border-collapse: collapse;
color: var(--color-text-primary);
font-family: var(--font-sans);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
}

.cv-table-row {
border-bottom: var(--edge-width) solid var(--color-edge);
border-color: var(--color-edge);
}

.cv-table-body .cv-table-row:last-child,
.cv-table-footer .cv-table-row:last-child {
border-bottom: 0;
}

.cv-table-head,
.cv-table-cell {
padding-block: calc(var(--spacing) * 2);
padding-inline: calc(var(--spacing) * 3);
text-align: left;
vertical-align: top;
}

.cv-table-head {
color: var(--color-text-secondary);
font-weight: 500;
white-space: nowrap;
}

.cv-table-footer {
border-top: var(--edge-width) solid var(--color-edge);
font-weight: 500;
}
138 changes: 138 additions & 0 deletions packages/visuals/src/ui/table.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@chopin/visuals";
import type {
TableBodyProps,
TableCellProps,
TableFooterProps,
TableHeaderProps,
TableHeadProps,
TableProps,
TableRowProps,
} from "@chopin/visuals";
import { expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";

test("exports the complete native presentation table API", () => {
let table: TableProps = { variant: "contained" };
let header: TableHeaderProps = {};
let body: TableBodyProps = {};
let footer: TableFooterProps = {};
let row: TableRowProps = {};
let head: TableHeadProps = { scope: "col" };
let cell: TableCellProps = { headers: "status" };

expect([
Table,
TableHeader,
TableBody,
TableFooter,
TableRow,
TableHead,
TableCell,
].every(component => typeof component === "function")).toBe(true);
expect(table.variant).toBe("contained");
expect([header, body, footer, row, head, cell]).toHaveLength(6);
});

test(
"renders native table elements with authoritative slots and forwarded semantic attributes",
() => {
let markup = renderToStaticMarkup(
<Table
aria-label="Deployment activity"
className="custom-table"
data-example="activity"
data-slot="ignored-table"
data-variant="plain"
id="activity-table"
variant="contained"
>
<TableHeader className="custom-header" data-slot="ignored-header">
<TableRow className="custom-row" data-slot="ignored-row">
<TableHead data-slot="ignored-head" id="environment" scope="col">
Environment
</TableHead>
<TableHead id="status" scope="col">
Status
</TableHead>
</TableRow>
</TableHeader>
<TableBody className="custom-body" data-slot="ignored-body">
<TableRow>
<TableCell data-slot="ignored-cell" headers="environment">
Production
</TableCell>
<TableCell headers="status">Healthy</TableCell>
</TableRow>
</TableBody>
<TableFooter className="custom-footer" data-slot="ignored-footer">
<TableRow>
<TableCell colSpan={2}>2 environments</TableCell>
</TableRow>
</TableFooter>
</Table>,
);

expect(markup.startsWith('<div data-slot="table-container"')).toBe(true);
expect(markup).toContain('<table aria-label="Deployment activity"');
expect(markup).toContain('class="cv-table custom-table"');
expect(markup).toContain('data-example="activity"');
expect(markup).toContain('data-slot="table"');
expect(markup).not.toContain('data-slot="ignored-table"');
expect(markup).toContain('data-variant="contained"');
expect(markup).not.toContain('data-variant="plain"');
expect(markup).toContain('id="activity-table"');
expect(markup).toContain(
'<thead class="cv-table-header custom-header" data-slot="table-header"',
);
expect(markup).toContain('<tbody class="cv-table-body custom-body" data-slot="table-body"');
expect(markup).toContain(
'<tfoot class="cv-table-footer custom-footer" data-slot="table-footer"',
);
expect(markup).toContain('<tr class="cv-table-row custom-row" data-slot="table-row"');
expect(markup).toContain(
'<th class="cv-table-head" data-slot="table-head" id="environment" scope="col"',
);
expect(markup).toContain(
'<td class="cv-table-cell" data-slot="table-cell" headers="environment"',
);
expect(markup).toContain('colSpan="2"');
for (let ignored of ["header", "body", "footer", "row", "head", "cell"]) {
expect(markup).not.toContain(`data-slot="ignored-${ignored}"`);
}
},
);

test("defaults to plain and supports the contained presentation variant", () => {
let plain = renderToStaticMarkup(<Table />);
let contained = renderToStaticMarkup(<Table variant="contained" />);

expect(plain).toContain('data-slot="table"');
expect(plain).toContain('data-variant="plain"');
expect(contained).toContain('data-slot="table"');
expect(contained).toContain('data-variant="contained"');
});

test("uses the existing type and colour tokens in a horizontal overflow container", async () => {
let css = await Bun.file(new URL("./table.css", import.meta.url)).text();
let styles = await Bun.file(new URL("../styles.css", import.meta.url)).text();

expect(styles).toContain('@import "./ui/table.css";');
expect(css).toContain("max-width: 100%;");
expect(css).toContain("overflow-x: auto;");
expect(css).toContain("width: 100%;");
expect(css).toContain("border-collapse: collapse;");
expect(css).toContain("font-size: var(--text-sm);");
expect(css).toContain("line-height: var(--text-sm--line-height);");
expect(css).toContain("color: var(--color-text-primary);");
expect(css).toContain("color: var(--color-text-secondary);");
expect(css).toContain("border-color: var(--color-edge);");
expect(css).not.toMatch(/#[\da-f]{3,8}|(?:oklch|rgb|hsl)\(/i);
});
Loading
Loading