diff --git a/package-lock.json b/package-lock.json index 65b0aef..67620e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "lucide-react": "^0.428.0" + "lucide-react": "^0.428.0", + "pretty-ms": "^9.1.0" }, "devDependencies": { "@chromatic-com/storybook": "^1.6.1", @@ -7690,6 +7691,17 @@ "node": ">=6" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7973,6 +7985,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz", + "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", diff --git a/package.json b/package.json index 378e77e..dc8a412 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,11 @@ "React" ], "dependencies": { - "lucide-react": "^0.428.0" + "lucide-react": "^0.428.0", + "pretty-ms": "^9.1.0" }, "peerDependencies": { "@codex/sdk-js": "@codex/sdk-js#master", - "@tanstack/react-query": "^5.51.24", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/src/components/Backdrop/backdrop.css b/src/components/Backdrop/backdrop.css index 1a36b7d..8bb1052 100644 --- a/src/components/Backdrop/backdrop.css +++ b/src/components/Backdrop/backdrop.css @@ -10,3 +10,7 @@ display: block; z-index: 1; } + +.document-noOverflow { + overflow: hidden; +} diff --git a/src/components/SimpleText/SimpleText.tsx b/src/components/SimpleText/SimpleText.tsx index 21615f2..9d8cd11 100644 --- a/src/components/SimpleText/SimpleText.tsx +++ b/src/components/SimpleText/SimpleText.tsx @@ -32,6 +32,8 @@ type Props = { size?: "normal" | "small"; center?: boolean; + + bold?: boolean; }; export function SimpleText({ @@ -41,8 +43,9 @@ export function SimpleText({ size = "normal", style, children, + bold, }: Props) { - const c = `text text--${variant} ${className} ${center ? "text--center" : ""}`; + const c = `text text--${variant} ${className} ${center ? "text--center" : ""} ${bold ? "text--bold" : ""}`; if (size === "small") { return ( diff --git a/src/components/SimpleText/simpleText.css b/src/components/SimpleText/simpleText.css index 6712fef..fbb6201 100644 --- a/src/components/SimpleText/simpleText.css +++ b/src/components/SimpleText/simpleText.css @@ -21,3 +21,7 @@ .text--warning { color: var(--codex-color-warning); } + +.text--bold { + font-weight: bold; +} diff --git a/src/components/Table/ActionCellRender.tsx b/src/components/Table/ActionCellRender.tsx new file mode 100644 index 0000000..f0c6c03 --- /dev/null +++ b/src/components/Table/ActionCellRender.tsx @@ -0,0 +1,13 @@ +import { SimpleText } from "../SimpleText/SimpleText"; + +export const ActionCellRender = + (action: string, onClick: (row: string[]) => void) => + (_: string, row: string[]) => { + return ( + onClick(row)} className="cell--action"> + + {action} + + + ); + }; diff --git a/src/components/Table/BreakCellRender.tsx b/src/components/Table/BreakCellRender.tsx new file mode 100644 index 0000000..f0e2cd8 --- /dev/null +++ b/src/components/Table/BreakCellRender.tsx @@ -0,0 +1,3 @@ +export const BreakCellRender = (val: string) => ( + {val || " "} +); diff --git a/src/components/Table/CellRender.css b/src/components/Table/CellRender.css new file mode 100644 index 0000000..c490888 --- /dev/null +++ b/src/components/Table/CellRender.css @@ -0,0 +1,22 @@ +.cell--break { + word-break: break-all; +} + +.cell--action { + text-decoration: none; + transition: text-shadow 0.35s; +} + +.cell--action:hover { + text-shadow: var(--codex-color-primary) 0px 0 20px; +} + +.cell-state { + border-radius: var(--codex-border-radius); + padding: 0.5rem 0.75rem; +} + +.cell-state--error { + background-color: var(--codex-color-error); + mix-blend-mode: difference; +} diff --git a/src/components/Table/CellRender.tsx b/src/components/Table/CellRender.tsx new file mode 100644 index 0000000..c8809d2 --- /dev/null +++ b/src/components/Table/CellRender.tsx @@ -0,0 +1,10 @@ +import { ReactNode } from "react"; +import "./CellRender.css"; + +export type CellRender = ( + value: string, + row: string[], + index: number +) => ReactNode; + +export const DefaultCellRender = (val: string) => val; diff --git a/src/components/Table/DefaultCellRender.tsx b/src/components/Table/DefaultCellRender.tsx new file mode 100644 index 0000000..5ec8c84 --- /dev/null +++ b/src/components/Table/DefaultCellRender.tsx @@ -0,0 +1,3 @@ +import "./CellRender.css"; + +export const DefaultCellRender = (val: string) => val; diff --git a/src/components/Table/DurationCellRender.tsx b/src/components/Table/DurationCellRender.tsx new file mode 100644 index 0000000..a010b27 --- /dev/null +++ b/src/components/Table/DurationCellRender.tsx @@ -0,0 +1,10 @@ +import prettyMilliseconds from "pretty-ms"; + +export function DurationCellRender(value: string) { + const ms = parseInt(value, 10); + if (isNaN(ms)) { + return "Nan"; + } + + return prettyMilliseconds(ms, { compact: true }); +} diff --git a/src/components/Table/StateCellRender.tsx b/src/components/Table/StateCellRender.tsx new file mode 100644 index 0000000..653bbcf --- /dev/null +++ b/src/components/Table/StateCellRender.tsx @@ -0,0 +1,11 @@ +type Mapping = { [k: string]: "success" | "warning" | "error" | "default" }; + +export const StateCellRender = (mapping: Mapping) => (value: string) => { + return ( +

+ + {value} + +

+ ); +}; diff --git a/src/components/Table/Table.css b/src/components/Table/Table.css new file mode 100644 index 0000000..3bec4f4 --- /dev/null +++ b/src/components/Table/Table.css @@ -0,0 +1,34 @@ +.table { + border-collapse: collapse; + width: 100%; +} + +.table-container { + border: 1px solid var(--codex-border-color); + background-color: var(--codex-background-secondary); + padding: 2rem; + border-radius: var(--codex-border-radius); + overflow-x: auto; +} + +.table-theadTr { + border-bottom: 1px solid var(--codex-border-color); +} + +.table-theadTh { + color: var(--codex-color-light); + font-weight: normal; + text-transform: uppercase; + font-size: 0.9rem; + text-align: left; + padding: 1rem; +} + +.table-tbodyTr { + border-bottom: 1px solid var(--codex-border-color); +} + +.table-tbodyTd { + text-align: left; + padding: 1rem; +} diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx new file mode 100644 index 0000000..bdaca55 --- /dev/null +++ b/src/components/Table/Table.tsx @@ -0,0 +1,56 @@ +import { ReactNode } from "react"; +import "./Table.css"; +import { CellRender } from "./CellRender"; + +type Props = { + /** + * List of header names + */ + headers: string[]; + + /** + * The data are represented by a 2 dimensions array. + * Each row contains a dataset whose data structure is a string array. + */ + data: string[][]; + + /** + * The cell render is an array of function that returns the cell data. + */ + cells: CellRender[]; + + className?: string; +}; + +export function Table({ data, headers, cells, className }: Props) { + return ( +
+ + + + {headers.map((col) => ( + + ))} + + + + {data.map((row, index) => ( + + {headers.map((header, idx) => { + const render = cells[idx]; + + return ( + + ); + })} + + ))} + +
+ {col} +
+ {render(row[idx], row, index)} +
+
+ ); +} diff --git a/src/components/index.ts b/src/components/index.ts deleted file mode 100644 index ed17dc9..0000000 --- a/src/components/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export { Button } from "../components/Button/Button"; -export { ButtonIcon } from "../components/ButtonIcon/ButtonIcon"; -export { Input } from "../components/Input/Input"; -export { InputGroup } from "../components/InputGroup/InputGroup"; -export { SimpleText } from "../components/SimpleText/SimpleText"; -export { Upload } from "../components/Upload/Upload"; -export { Card } from "../components/Card/Card"; -export { Select } from "../components/Select/Select"; -export { Toast } from "../components/Toast/Toast"; -export { SpaceAllocation } from "../components/SpaceAllocation/SpaceAllocation"; -export { EmptyPlaceholder } from "../components/EmptyPlaceholder/EmptyPlaceholder"; -export { Dropdown, type DropdownOption } from "../components/Dropdown/Dropdown"; -export { Failure } from "../components/Failure/Failure"; -export { Alert } from "../components/Alert/Alert"; -export { Spinner } from "../components/Spinner/Spinner"; -export { WebFileIcon } from "../components/WebFileIcon/WebFileIcon"; -export { Stepper } from "../components/Stepper/Stepper"; -export { Backdrop } from "../components/Backdrop/Backdrop"; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8d7d6a5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,24 @@ +export { Button } from "./components/Button/Button"; +export { ButtonIcon } from "./components/ButtonIcon/ButtonIcon"; +export { Input } from "./components/Input/Input"; +export { InputGroup } from "./components/InputGroup/InputGroup"; +export { SimpleText } from "./components/SimpleText/SimpleText"; +export { Upload } from "./components/Upload/Upload"; +export { Card } from "./components/Card/Card"; +export { Select } from "./components/Select/Select"; +export { Toast } from "./components/Toast/Toast"; +export { SpaceAllocation } from "./components/SpaceAllocation/SpaceAllocation"; +export { EmptyPlaceholder } from "./components/EmptyPlaceholder/EmptyPlaceholder"; +export { Dropdown, type DropdownOption } from "./components/Dropdown/Dropdown"; +export { Failure } from "./components/Failure/Failure"; +export { Alert } from "./components/Alert/Alert"; +export { Spinner } from "./components/Spinner/Spinner"; +export { WebFileIcon } from "./components/WebFileIcon/WebFileIcon"; +export { Stepper } from "./components/Stepper/Stepper"; +export { Backdrop } from "./components/Backdrop/Backdrop"; +export { ActionCellRender } from "./components/Table/ActionCellRender"; +export { BreakCellRender } from "./components/Table/BreakCellRender"; +export { DefaultCellRender } from "./components/Table/CellRender"; +export { DurationCellRender } from "./components/Table/DurationCellRender"; +export { StateCellRender } from "./components/Table/StateCellRender"; +export { Table } from "./components/Table/Table"; diff --git a/stories/Table.stories.css b/stories/Table.stories.css new file mode 100644 index 0000000..5657725 --- /dev/null +++ b/stories/Table.stories.css @@ -0,0 +1,3 @@ +.tableSmall { + width: 350px; +} diff --git a/stories/Table.stories.tsx b/stories/Table.stories.tsx new file mode 100644 index 0000000..9d8d0dd --- /dev/null +++ b/stories/Table.stories.tsx @@ -0,0 +1,91 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Table } from "../src/components/Table/Table"; +import { DefaultCellRender } from "../src/components/Table/CellRender"; +import { ActionCellRender } from "../src/components/Table/ActionCellRender"; +import { BreakCellRender } from "../src/components/Table/BreakCellRender"; +import "./Table.stories.css"; +import { StateCellRender } from "../src/components/Table/StateCellRender"; +import prettyMilliseconds from "pretty-ms"; +import { DurationCellRender } from "../src/components/Table/DurationCellRender"; + +console.info(prettyMilliseconds(1337000000n, { compact: true })); + +const meta = { + title: "Components/Table", + component: Table, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + cells: [ + DefaultCellRender, + DefaultCellRender, + DefaultCellRender, + ActionCellRender("Action", (row) => console.info(row)), + ], + data: [["Ox45678FDGHJKL", "My file", "Some data"]], + headers: ["id", "title", "other", "actions"], + }, +}; + +export const Scroll: Story = { + args: { + className: "tableSmall", + cells: [ + DefaultCellRender, + DefaultCellRender, + DefaultCellRender, + ActionCellRender("Action", (row) => console.info(row)), + ], + data: [["Ox45678FDGHJKLBSA21", "My file", "Some data"]], + headers: ["id", "title", "other", "actions"], + }, +}; + +export const BreakableCell: Story = { + args: { + cells: [ + BreakCellRender, + DefaultCellRender, + DefaultCellRender, + ActionCellRender("Action", (row) => console.info(row)), + ], + data: [["veryverylongvaluetobreak", "My file", "Some data"]], + headers: ["break", "title", "other", "actions"], + className: "tableSmall", + }, +}; + +export const StateCell: Story = { + args: { + cells: [ + DefaultCellRender, + DefaultCellRender, + StateCellRender({ cancelled: "error" }), + ActionCellRender("Action", (row) => console.info(row)), + ], + data: [["Ox45678FDGHJKL", "My file", "cancelled", "Action"]], + headers: ["id", "title", "state", "actions"], + }, +}; + +export const DurationCell: Story = { + args: { + cells: [ + DefaultCellRender, + DefaultCellRender, + DurationCellRender, + ActionCellRender("Action", (row) => console.info(row)), + ], + data: [["Ox45678FDGHJKL", "My file", "3600000", "Action"]], + headers: ["id", "title", "duration", "actions"], + }, +}; diff --git a/stories/Upload.stories.tsx b/stories/Upload.stories.tsx index 67d5faf..df74034 100644 --- a/stories/Upload.stories.tsx +++ b/stories/Upload.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from "@storybook/react"; +import type { Meta } from "@storybook/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { UploadResponse } from "@codex/sdk-js"; import { Upload } from "../src/components/Upload/Upload";