From 1c5a00dc5dd84950b54c8abc375ef001964aa1f3 Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Fri, 26 Jan 2024 17:56:21 +0100 Subject: [PATCH 1/8] :sparkles: #11 - feat: add datagrid component --- src/components/badge/badge.scss | 13 + src/components/badge/badge.stories.tsx | 17 ++ src/components/badge/badge.tsx | 19 ++ src/components/badge/index.ts | 1 + src/components/button/button.scss | 1 + src/components/datagrid/datagrid.scss | 143 +++++++++++ src/components/datagrid/datagrid.stories.tsx | 199 +++++++++++++++ src/components/datagrid/datagrid.tsx | 227 ++++++++++++++++++ src/components/datagrid/index.ts | 1 + src/components/form/input/input.scss | 1 + src/components/form/select/select.scss | 2 + src/components/index.ts | 2 + .../layout/container/container.scss | 2 + src/components/page/page.scss | 4 +- src/components/page/page.stories.tsx | 103 +++++++- src/components/paginator/paginator.scss | 10 +- src/components/paginator/paginator.tsx | 33 ++- src/components/toolbar/toolbar.scss | 5 + src/components/toolbar/toolbar.tsx | 6 + src/components/typography/h3/h3.tsx | 6 +- src/components/typography/p/p.scss | 10 +- src/components/typography/p/p.tsx | 8 +- src/settings/tokens.css | 2 +- 23 files changed, 786 insertions(+), 29 deletions(-) create mode 100644 src/components/badge/badge.scss create mode 100644 src/components/badge/badge.stories.tsx create mode 100644 src/components/badge/badge.tsx create mode 100644 src/components/badge/index.ts create mode 100644 src/components/datagrid/datagrid.scss create mode 100644 src/components/datagrid/datagrid.stories.tsx create mode 100644 src/components/datagrid/datagrid.tsx create mode 100644 src/components/datagrid/index.ts diff --git a/src/components/badge/badge.scss b/src/components/badge/badge.scss new file mode 100644 index 00000000..1815f50c --- /dev/null +++ b/src/components/badge/badge.scss @@ -0,0 +1,13 @@ +.mykn-badge { + background-color: var(--theme-color-primary-200); + align-items: center; + border-radius: var(--border-radus-m); + display: inline-flex; + font-family: Inter, sans-serif; + font-size: var(--typography-font-size-body-xs); + font-weight: var(--typography-font-weight-normal); + height: var(--typography-line-height-body-s); + justify-content: center; + line-height: var(--typography-line-height-body-xs); + padding: 0 var(--spacing-h-s); +} diff --git a/src/components/badge/badge.stories.tsx b/src/components/badge/badge.stories.tsx new file mode 100644 index 00000000..9384cd14 --- /dev/null +++ b/src/components/badge/badge.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Badge } from "./badge"; + +const meta = { + title: "Typography/Badge", + component: Badge, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const BadgeComponent: Story = { + args: { + children: "The quick brown fox jumps over the lazy dog.", + }, +}; diff --git a/src/components/badge/badge.tsx b/src/components/badge/badge.tsx new file mode 100644 index 00000000..790b0d42 --- /dev/null +++ b/src/components/badge/badge.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import "./badge.scss"; + +export type BadgeProps = React.PropsWithChildren<{ + // Props here. +}>; + +/** + * Badge component + * @param children + * @param props + * @constructor + */ +export const Badge: React.FC = ({ children, ...props }) => ( +
+ {children} +
+); diff --git a/src/components/badge/index.ts b/src/components/badge/index.ts new file mode 100644 index 00000000..80844a4e --- /dev/null +++ b/src/components/badge/index.ts @@ -0,0 +1 @@ +export * from "./badge"; diff --git a/src/components/button/button.scss b/src/components/button/button.scss index 7fadd48f..dfa316ac 100644 --- a/src/components/button/button.scss +++ b/src/components/button/button.scss @@ -45,6 +45,7 @@ ); --mykn-button-padding-v: 0; --mykn-button-padding-h: 0; + flex-shrink: 0; } &--variant-primary { diff --git a/src/components/datagrid/datagrid.scss b/src/components/datagrid/datagrid.scss new file mode 100644 index 00000000..903d5bf2 --- /dev/null +++ b/src/components/datagrid/datagrid.scss @@ -0,0 +1,143 @@ +@use "../../settings/constants"; + +.mykn-datagrid { + background-color: var(--typography-color-background); + border-radius: var(--border-radus-m); + + &__table { + border-spacing: 0; + width: 100%; + } + + &__caption { + padding: var(--spacing-v-m) var(--spacing-h-m); + text-align: start; + } + + &__head { + background-color: var(--typography-color-background); + position: sticky; + top: 0; + } + + &__head &__row:first-child &__cell { + border-top: 1px solid var(--theme-shade-300); + } + + &__cell { + border-bottom: 1px solid var(--theme-shade-300); + box-sizing: border-box; + padding: var(--spacing-v-m) var(--spacing-h-m); + } + + &__cell--type-boolean { + text-align: center; + } + + &__cell--type-number { + text-align: end; + } + + &__foot { + position: sticky; + bottom: 0; + } + + &__foot &__cell { + border-bottom: none; + padding-top: 0; + padding-bottom: 0; + } + + @media screen and (max-width: constants.$breakpoint-desktop - 1px) { + background-color: transparent; + overflow: visible; + + &__table { + display: block; + } + + &__caption { + background-color: var(--typography-color-background); + border-radius: var(--border-radus-m); + display: block; + } + + &__head { + display: none; + } + + &__body { + display: block; + } + + &__row { + background-color: var( + --typography-color-background + ); //border-radius: var(--border-radus-m); + display: flex; + flex-wrap: wrap; + + &:nth-child(even) { + background-color: var(--theme-shade-100); + } + } + + &__row:nth-child(even) &__cell { + border-bottom: 1px solid var(--typography-color-background); + } + + &__cell { + display: flex; + flex-direction: column; + gap: var(--spacing-h-m); + width: 100%; + position: relative; + + .mykn-p { + font-weight: var(--typography-font-weight-bold); + width: 100%; + } + + &:before { + color: var(--theme-shade-700); + content: attr(aria-description); + font-family: Inter, sans-serif; + font-size: var(--typography-font-size-body-xs); + font-weight: var(--typography-font-weight-normal); + line-height: var(--typography-line-height-body-xs); + display: block; + text-align: start; + width: 40%; + } + + &:first-child .mykn-a:has(.mykn-icon) { + float: right; + } + } + + &__foot { + display: flex; + } + + &__foot &__row { + width: 100%; + } + + &__foot &__cell { + padding: 0; + + &:before { + display: none; + } + } + + .mykn-toolbar { + border-radius: var(--border-radus-m); + } + + .mykn-paginator .mykn-icon--spin:first-child { + display: none; + } + } +} diff --git a/src/components/datagrid/datagrid.stories.tsx b/src/components/datagrid/datagrid.stories.tsx new file mode 100644 index 00000000..55017f0d --- /dev/null +++ b/src/components/datagrid/datagrid.stories.tsx @@ -0,0 +1,199 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React, { useEffect, useState } from "react"; + +import { Page } from "../page"; +import { PaginatorProps } from "../paginator"; +import { DataGrid } from "./datagrid"; + +const meta = { + title: "Data/DataGrid", + component: DataGrid, + decorators: [ + (Story) => ( + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const DataridComponent = { + args: { + booleanProps: { + labelTrue: "This value is true", + labelFalse: "This value is false", + }, + paginatorProps: { + count: 100, + page: 1, + pageSize: 10, + pageSizeOptions: [ + { label: 10 }, + { label: 20 }, + { label: 30 }, + { label: 40 }, + { label: 50 }, + ], + labelLoading: "Loading", + labelPagination: "pagination", + labelCurrentPageRange: "{pageStart} - {pageEnd} of {pageCount}", + labelGoToPage: "Go to", + labelPageSize: "Show rows", + labelPrevious: "Go to previous page", + labelNext: "Go to next page", + }, + results: [ + { + url: "https://www.example.com", + Omschrijving: "Afvalpas vervangen", + Versie: 2, + Actief: false, + Toekomstig: false, + Concept: true, + }, + { + url: "https://www.example.com", + Omschrijving: "Erfpacht wijzigen", + Versie: 4, + Actief: true, + Toekomstig: true, + Concept: true, + }, + { + url: "https://www.example.com", + Omschrijving: "Dakkapel vervangen", + Versie: 1, + Actief: false, + Toekomstig: false, + Concept: false, + }, + { + url: "https://www.example.com", + Omschrijving: "Dakkapel vervangen", + Versie: 4, + Actief: true, + Toekomstig: true, + Concept: true, + }, + { + url: "https://www.example.com", + Omschrijving: "Erfpacht wijzigen", + Versie: 2, + Actief: false, + Toekomstig: false, + Concept: true, + }, + { + url: "https://www.example.com", + Omschrijving: "Dakkapel vervangen", + Versie: 4, + Actief: true, + Toekomstig: true, + Concept: true, + }, + { + url: "https://www.example.com", + Omschrijving: "Erfpacht wijzigen", + Versie: 1, + Actief: false, + Toekomstig: false, + Concept: false, + }, + { + url: "https://www.example.com", + Omschrijving: "Dakkapel vervangen", + Versie: 1, + Actief: false, + Toekomstig: false, + Concept: false, + }, + ], + title: "Posts", + urlFields: ["url"], + }, +}; + +export const DatagridOnMobile: Story = { + ...DataridComponent, + parameters: { + viewport: { defaultViewport: "mobile1" }, + chromatic: { + viewports: ["mobile1"], + }, + }, +}; + +export const JSONPlaceholderExample: Story = { + args: { + booleanProps: { + labelTrue: "This value is true", + labelFalse: "This value is false", + }, + paginatorProps: { + count: 100, + page: 1, + pageSize: 10, + pageSizeOptions: [ + { label: 10 }, + { label: 20 }, + { label: 30 }, + { label: 40 }, + { label: 50 }, + ], + labelLoading: "Loading", + labelPagination: "pagination", + labelCurrentPageRange: "{pageStart} - {pageEnd} of {pageCount}", + labelGoToPage: "Go to", + labelPageSize: "Show rows", + labelPrevious: "Go to previous page", + labelNext: "Go to next page", + }, + results: [], + title: "Posts", + }, + render: (args) => { + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(args.paginatorProps?.page || 1); + const [pageSize, setPageSize] = useState( + args.paginatorProps?.pageSize || 10, + ); + const [results, setResults] = useState(args.results); + const paginatorProps = args.paginatorProps as PaginatorProps; + + paginatorProps.pageSize = pageSize; + + useEffect(() => { + setLoading(true); + const index = page - 1; + const abortController = new AbortController(); + + fetch("https://jsonplaceholder.typicode.com/posts", { + signal: abortController.signal, + }) + .then((response) => response.json()) + .then((data) => { + // Paginate locally for demonstration purposes. + const posts = data.slice( + index * pageSize, + index * pageSize + pageSize, + ); + setResults(posts); + setLoading(false); + }); + + return () => { + abortController.abort(); + setLoading(false); + }; + }, [page, pageSize]); + + paginatorProps.loading = loading; + paginatorProps.onPageChange = (page) => setPage(page); + paginatorProps.onPageSizeChange = async (pageSize) => setPageSize(pageSize); + + return ; + }, +}; diff --git a/src/components/datagrid/datagrid.tsx b/src/components/datagrid/datagrid.tsx new file mode 100644 index 00000000..6e374af6 --- /dev/null +++ b/src/components/datagrid/datagrid.tsx @@ -0,0 +1,227 @@ +import clsx from "clsx"; +import React, { useId } from "react"; + +import { Badge, BadgeProps } from "../badge"; +import { Boolean, BooleanProps } from "../boolean"; +import { Outline } from "../icon"; +import { Paginator, PaginatorProps } from "../paginator"; +import { Toolbar } from "../toolbar"; +import { A, AProps, H3, P } from "../typography"; +import "./datagrid.scss"; + +/** Matches a URL. */ +const REGEX_URL = /https?:\/\/[^\s]+$/; + +export type RowData = Record; + +export type DataGridProps = Omit< + React.HTMLAttributes, + "results" +> & { + /** The results (after pagination), only primitive types supported for now. */ + results: RowData[]; + + /** A `string[]` containing the keys in `results` to show data for. */ + fields?: string[]; + + /** + * A `string[]` containing the fields which should be used to detect the url + * of a row. Fields specified in this array won't be rendered, instead: the + * first (non url) field is rendered as link (`A`) with `href` set to the + * resolved url of the row. + */ + urlFields?: string[]; + + /** Props for A. */ + aProps?: Omit; + + /** Props for Badge. */ + badgeProps?: BadgeProps; + + /** Props for Boolean. */ + booleanProps?: Omit; + + /** If set, the paginator is enabled. */ + paginatorProps?: PaginatorProps; + + /** A title for the datagrid. */ + title?: string; +}; + +/** + * DataGrid component + * @param aProps + * @param badgeProps + * @param booleanProps + * @param paginatorProps + * @param results + * @param fields + * @param title + * @param urlFields + * @param props + * @constructor + */ +export const DataGrid: React.FC = ({ + aProps, + badgeProps, + booleanProps, + results, + fields = results?.length ? Object.keys(results[0]) : [], + paginatorProps, + title = "", + urlFields = [ + "absolute_url", + "get_absolute_url", + "href", + "get_href", + "url", + "get_url", + ], + ...props +}) => { + const id = useId(); + const renderableFields = fields.filter((f) => !urlFields.includes(f)); + const captions = renderableFields.map((f) => field2Caption(f as string)); + const titleId = title ? `${id}-caption` : undefined; + + /** + * Renders a cell based on type of `rowData[field]`. + * @param rowData + * @param field + * @param index + */ + const renderCell = (rowData: RowData, field: string, index: number) => { + const fieldIndex = renderableFields.indexOf(field); + const urlField = urlFields.find((f) => String(rowData[f]).match(REGEX_URL)); + const rowUrl = urlField ? rowData[urlField] : null; + const data = rowData[field]; + const type = typeof data; + + // Explicit link: passed as URL without being set as urlField. + const isExplicitLink = String(data).match(REGEX_URL); + + // Implicit link: first column and `rowUrl` resolved using `urlFields`. + const isImplicitLink = rowUrl && fieldIndex === 0; + + // Cell should be a link is truthy. + const link = isExplicitLink + ? String(data) + : isImplicitLink + ? String(rowUrl) + : ""; + + let contents: React.ReactNode = data; + switch (type) { + case "boolean": + contents = ( + + ); + break; + case "number": + contents = {data}; + } + + return ( + + {isExplicitLink ? ( + + {contents} + + ) : ( + <> + {isImplicitLink ? ( +

+ + + +   + {contents} +

+ ) : ( +

{contents}

+ )} + + )} + + ); + }; + + return ( +
+ {/* Caption */} + + {title && ( + + )} + + {/* Headings */} + + + {captions.map((caption) => ( + + ))} + + + + {/* Cells */} + + {results.map((rowData, index) => ( + /** + * FIXME: This effectively still uses index as keys which might lead to issues. + * @see {@link https://react.dev/learn/rendering-lists#rules-of-keys|Rules of keys} + */ + + {renderableFields.map((field) => + renderCell(rowData, String(field), index), + )} + + ))} + + + {/* Paginator */} + {paginatorProps && ( + + + + + + )} +
+

{title}

+
+

+ {caption} +

+
+ + + +
+
+ ); +}; + +/** + * Converts "field_name" to "FIELD NAME". + * @param field + */ +const field2Caption = (field: string): string => + String(field).replaceAll("_", " ").toUpperCase(); diff --git a/src/components/datagrid/index.ts b/src/components/datagrid/index.ts new file mode 100644 index 00000000..f32f9692 --- /dev/null +++ b/src/components/datagrid/index.ts @@ -0,0 +1 @@ +export * from "./datagrid"; diff --git a/src/components/form/input/input.scss b/src/components/form/input/input.scss index e4c1d9fe..b1986c03 100644 --- a/src/components/form/input/input.scss +++ b/src/components/form/input/input.scss @@ -8,6 +8,7 @@ color: var(--typography-color-body); font-family: Inter, sans-serif; font-size: var(--typography-font-size-body-s); + font-weight: var(--typography-font-weight-normal); line-height: var(--typography-line-height-body-s); padding: var(--spacing-v-s) var(--spacing-h-s); position: relative; diff --git a/src/components/form/select/select.scss b/src/components/form/select/select.scss index 36858e5f..6d165fee 100644 --- a/src/components/form/select/select.scss +++ b/src/components/form/select/select.scss @@ -10,6 +10,7 @@ justify-content: space-between; font-family: Inter, sans-serif; font-size: var(--typography-font-size-body-s); + font-weight: var(--typography-font-weight-normal); line-height: var(--typography-line-height-body-s); padding: var(--spacing-v-s) var(--spacing-h-s); width: min(320px, 100%); @@ -80,6 +81,7 @@ font-weight: var(--mykn-option-font-weight); line-height: var(--typography-line-height-body-s); padding: 0 var(--spacing-h-s); + text-align: start; white-space: nowrap; &[aria-selected="true"] { diff --git a/src/components/index.ts b/src/components/index.ts index acb9d523..40294d1f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,8 +1,10 @@ // Auto-generated file. Do not modify manually. +export * from "./badge"; export * from "./boolean"; export * from "./breadcrumbs"; export * from "./button"; export * from "./card"; +export * from "./datagrid"; export * from "./dropdown"; export * from "./form"; export * from "./icon"; diff --git a/src/components/layout/container/container.scss b/src/components/layout/container/container.scss index 926cde19..6f343203 100644 --- a/src/components/layout/container/container.scss +++ b/src/components/layout/container/container.scss @@ -1,4 +1,6 @@ .mykn-container { + container-name: container; + container-type: inline-size; margin: 0 auto; max-width: 1240px; width: 100%; diff --git a/src/components/page/page.scss b/src/components/page/page.scss index 57fd9f02..9bd01a32 100644 --- a/src/components/page/page.scss +++ b/src/components/page/page.scss @@ -3,11 +3,11 @@ .mykn-page { background-color: var(--theme-color-primary-200); container-name: page; - container-type: size; + container-type: inline-size; box-sizing: border-box; padding: var(--spacing-h-xl); width: 100%; - height: 100%; + min-height: 100%; @media screen and (min-width: constants.$breakpoint-desktop) { padding: var(--spacing-v-xl) var(--spacing-h-xl); diff --git a/src/components/page/page.stories.tsx b/src/components/page/page.stories.tsx index cf43aaf3..f4961eec 100644 --- a/src/components/page/page.stories.tsx +++ b/src/components/page/page.stories.tsx @@ -3,6 +3,7 @@ import React from "react"; import { Button, ButtonLink } from "../button"; import { Card } from "../card"; +import { DataGrid } from "../datagrid"; import { Select } from "../form"; import { Outline } from "../icon"; import { Container, Grid } from "../layout"; @@ -90,10 +91,106 @@ export const SamplePage: Story = { > - - - + + {}, + page: 3, + pageSize: 20, + pageSizeOptions: [ + { + label: 10, + }, + { + label: 20, + }, + { + label: 30, + }, + { + label: 40, + }, + { + label: 50, + }, + ], + }} + results={[ + { + Omschrijving: "Afvalpas vervangen", + Versie: 2, + Actief: false, + Concept: true, + Toekomstig: false, + }, + { + Omschrijving: "Erfpacht wijzigen", + Actief: true, + Versie: 4, + Concept: true, + Toekomstig: true, + }, + { + Omschrijving: "Dakkapel vervangen", + Actief: false, + Versie: 1, + Concept: false, + Toekomstig: false, + }, + { + Omschrijving: "Dakkapel vervangen", + Actief: true, + Versie: 4, + Concept: true, + Toekomstig: true, + }, + { + Omschrijving: "Erfpacht wijzigen", + Actief: false, + Versie: 2, + Concept: true, + Toekomstig: false, + }, + { + Omschrijving: "Dakkapel vervangen", + Actief: true, + Versie: 4, + Concept: true, + Toekomstig: true, + }, + { + Omschrijving: "Erfpacht wijzigen", + Actief: false, + Versie: 1, + Concept: false, + Toekomstig: false, + }, + { + Omschrijving: "Dakkapel vervangen", + Versie: 1, + Actief: false, + Concept: false, + Toekomstig: false, + }, + ]} + title="Zaaktypen" + /> + + diff --git a/src/components/paginator/paginator.scss b/src/components/paginator/paginator.scss index df0746cb..27b4372e 100644 --- a/src/components/paginator/paginator.scss +++ b/src/components/paginator/paginator.scss @@ -12,12 +12,16 @@ } @media screen and (max-width: constants.$breakpoint-desktop - 1px) { + &__section--form { + display: none; + } + &__section { width: 100%; + justify-content: flex-end; - > * { - width: 100% !important; - white-space: nowrap; + .mykn-button { + justify-content: center !important; } } } diff --git a/src/components/paginator/paginator.tsx b/src/components/paginator/paginator.tsx index 21590a82..e3cacf30 100644 --- a/src/components/paginator/paginator.tsx +++ b/src/components/paginator/paginator.tsx @@ -7,7 +7,7 @@ import { Outline } from "../icon"; import { P } from "../typography"; import "./paginator.scss"; -export type PaginatorProps = { +export type PaginatorProps = React.HTMLAttributes & { /** The total number of results (items not pages). */ count: number; @@ -35,15 +35,18 @@ export type PaginatorProps = { /** The go to next page (accessible) label. */ labelNext: string; - /** The options for the page size, can be omitted if no variable pages size is supported. */ - pageSizeOptions?: Option[]; - /** * The loading (accessible) label, * @see onPageChange */ labelLoading?: string; + /** Indicates whether the spinner should be shown (requires `labelLoading`). */ + loading?: boolean; + + /** The options for the page size, can be omitted if no variable pages size is supported. */ + pageSizeOptions?: Option[]; + /** * Gets called when the selected page is changed * @@ -89,6 +92,7 @@ export const Paginator: React.FC = ({ labelPrevious = "Go to previous page", labelNext = "Go to next page", labelLoading, + loading = undefined, page = 1, pageSize, pageSizeOptions = [], @@ -182,6 +186,9 @@ export const Paginator: React.FC = ({ setPageState(sanitizedValue); }; + // `loading` takes precedence over `loadingState` (derived from Promise). + const isLoading = typeof loading === "boolean" ? loading : loadingState; + return ( ); diff --git a/src/components/toolbar/toolbar.scss b/src/components/toolbar/toolbar.scss index 7b7a3de9..1916f60d 100644 --- a/src/components/toolbar/toolbar.scss +++ b/src/components/toolbar/toolbar.scss @@ -87,4 +87,9 @@ text-decoration: underline; } } + + &--pad-h { + padding-inline-start: var(--spacing-h-m); + padding-inline-end: var(--spacing-h-m); + } } diff --git a/src/components/toolbar/toolbar.tsx b/src/components/toolbar/toolbar.tsx index 0ff49b9e..fa74cd13 100644 --- a/src/components/toolbar/toolbar.tsx +++ b/src/components/toolbar/toolbar.tsx @@ -18,6 +18,9 @@ export type ToolbarProps = React.PropsWithChildren< /** When set to true, padding is applied to A components to match Button component's height. */ padA?: boolean; + /** When set to true, horizontal padding is applied to the toolbar. */ + padH?: boolean; + /** The variant (style) of the toolbar. */ variant?: "normal" | "transparent"; @@ -33,6 +36,7 @@ export type ToolbarProps = React.PropsWithChildren< * @param align * @param direction * @param padA + * @param padH * @param variant * @param items * @param props @@ -43,6 +47,7 @@ export const Toolbar: React.FC = ({ align = "start", direction = "horizontal", padA = false, + padH = false, variant = "normal", items = [], ...props @@ -92,6 +97,7 @@ export const Toolbar: React.FC = ({ `mykn-toolbar--variant-${variant}`, { "mykn-toolbar--pad-a": padA, + "mykn-toolbar--pad-h": padH, }, )} role="toolbar" diff --git a/src/components/typography/h3/h3.tsx b/src/components/typography/h3/h3.tsx index 206871b7..3f1bddd6 100644 --- a/src/components/typography/h3/h3.tsx +++ b/src/components/typography/h3/h3.tsx @@ -2,9 +2,9 @@ import React from "react"; import "./h3.scss"; -export type H3Props = React.PropsWithChildren<{ - // Props here. -}>; +export type H3Props = React.PropsWithChildren< + React.HTMLAttributes +>; /** * H3 component diff --git a/src/components/typography/p/p.scss b/src/components/typography/p/p.scss index 63ad0565..05350f36 100644 --- a/src/components/typography/p/p.scss +++ b/src/components/typography/p/p.scss @@ -7,12 +7,16 @@ margin-top: 0; margin-bottom: 0; - &--size-xs { - font-size: var(--typography-font-size-body-xs); - line-height: var(--typography-line-height-body-xs); + &--bold { + font-weight: var(--typography-font-weight-bold); } &--muted { color: var(--typography-color-muted); } + + &--size-xs { + font-size: var(--typography-font-size-body-xs); + line-height: var(--typography-line-height-body-xs); + } } diff --git a/src/components/typography/p/p.tsx b/src/components/typography/p/p.tsx index 20ce2055..18beeeae 100644 --- a/src/components/typography/p/p.tsx +++ b/src/components/typography/p/p.tsx @@ -4,6 +4,9 @@ import React from "react"; import "./p.scss"; export type PProps = React.PropsWithChildren<{ + /** Whether the text should be presented bold. */ + bold?: boolean; + /** Whether the text should be presented in a lighter color. */ muted?: boolean; @@ -13,6 +16,7 @@ export type PProps = React.PropsWithChildren<{ /** * Ul component + * @param bold * @param children * @param muted * @param size @@ -20,13 +24,15 @@ export type PProps = React.PropsWithChildren<{ * @constructor */ export const P: React.FC = ({ + bold = false, children, - muted, + muted = false, size = "s", ...props }) => (

Date: Fri, 2 Feb 2024 17:51:11 +0100 Subject: [PATCH 2/8] :recycle: #11 - refactor: move field2caption to libs --- src/components/datagrid/datagrid.tsx | 10 ++-------- src/lib/format/string.tsx | 6 ++++++ 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 src/lib/format/string.tsx diff --git a/src/components/datagrid/datagrid.tsx b/src/components/datagrid/datagrid.tsx index 6e374af6..7dd1353f 100644 --- a/src/components/datagrid/datagrid.tsx +++ b/src/components/datagrid/datagrid.tsx @@ -1,6 +1,7 @@ import clsx from "clsx"; import React, { useId } from "react"; +import { field2Caption } from "../../lib/format/string"; import { Badge, BadgeProps } from "../badge"; import { Boolean, BooleanProps } from "../boolean"; import { Outline } from "../icon"; @@ -81,7 +82,7 @@ export const DataGrid: React.FC = ({ }) => { const id = useId(); const renderableFields = fields.filter((f) => !urlFields.includes(f)); - const captions = renderableFields.map((f) => field2Caption(f as string)); + const captions = renderableFields.map(field2Caption); const titleId = title ? `${id}-caption` : undefined; /** @@ -218,10 +219,3 @@ export const DataGrid: React.FC = ({ ); }; - -/** - * Converts "field_name" to "FIELD NAME". - * @param field - */ -const field2Caption = (field: string): string => - String(field).replaceAll("_", " ").toUpperCase(); diff --git a/src/lib/format/string.tsx b/src/lib/format/string.tsx new file mode 100644 index 00000000..f4a0b4c6 --- /dev/null +++ b/src/lib/format/string.tsx @@ -0,0 +1,6 @@ +/** + * Converts "field_name" to "FIELD NAME". + * @param field + */ +export const field2Caption = (field: string): string => + String(field).replaceAll("_", " ").toUpperCase(); From 504e495d5ac855b57ad4822b80d9c1fb855c918f Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Fri, 2 Feb 2024 17:54:31 +0100 Subject: [PATCH 3/8] :bulb: #11 - docs: add comment about link construction in datagrid --- src/components/datagrid/datagrid.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/datagrid/datagrid.tsx b/src/components/datagrid/datagrid.tsx index 7dd1353f..6e575de9 100644 --- a/src/components/datagrid/datagrid.tsx +++ b/src/components/datagrid/datagrid.tsx @@ -104,7 +104,9 @@ export const DataGrid: React.FC = ({ // Implicit link: first column and `rowUrl` resolved using `urlFields`. const isImplicitLink = rowUrl && fieldIndex === 0; - // Cell should be a link is truthy. + // If isExplicitLink is truthy, link should be data (data is a link). + // If isImplicitLink is truthy, link should be rowUrl (rowUrl is resolved URL of row). + // Otherwise, link should be an empty string (no link was resolved). const link = isExplicitLink ? String(data) : isImplicitLink From 54dab29f3a54b0edd43d561c5d9a0f1ca4db3893 Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Fri, 2 Feb 2024 17:57:57 +0100 Subject: [PATCH 4/8] :recycle: #11 - refactor: move link detection logic to libs --- src/components/datagrid/datagrid.tsx | 9 +++------ src/lib/format/string.tsx | 10 ++++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/datagrid/datagrid.tsx b/src/components/datagrid/datagrid.tsx index 6e575de9..62242544 100644 --- a/src/components/datagrid/datagrid.tsx +++ b/src/components/datagrid/datagrid.tsx @@ -1,7 +1,7 @@ import clsx from "clsx"; import React, { useId } from "react"; -import { field2Caption } from "../../lib/format/string"; +import { field2Caption, isLink } from "../../lib/format/string"; import { Badge, BadgeProps } from "../badge"; import { Boolean, BooleanProps } from "../boolean"; import { Outline } from "../icon"; @@ -10,9 +10,6 @@ import { Toolbar } from "../toolbar"; import { A, AProps, H3, P } from "../typography"; import "./datagrid.scss"; -/** Matches a URL. */ -const REGEX_URL = /https?:\/\/[^\s]+$/; - export type RowData = Record; export type DataGridProps = Omit< @@ -93,13 +90,13 @@ export const DataGrid: React.FC = ({ */ const renderCell = (rowData: RowData, field: string, index: number) => { const fieldIndex = renderableFields.indexOf(field); - const urlField = urlFields.find((f) => String(rowData[f]).match(REGEX_URL)); + const urlField = urlFields.find((f) => isLink(String(rowData[f]))); const rowUrl = urlField ? rowData[urlField] : null; const data = rowData[field]; const type = typeof data; // Explicit link: passed as URL without being set as urlField. - const isExplicitLink = String(data).match(REGEX_URL); + const isExplicitLink = isLink(String(data)); // Implicit link: first column and `rowUrl` resolved using `urlFields`. const isImplicitLink = rowUrl && fieldIndex === 0; diff --git a/src/lib/format/string.tsx b/src/lib/format/string.tsx index f4a0b4c6..66c8c512 100644 --- a/src/lib/format/string.tsx +++ b/src/lib/format/string.tsx @@ -1,6 +1,16 @@ +/** Matches a URL. */ +export const REGEX_URL = /https?:\/\/[^\s]+$/; + /** * Converts "field_name" to "FIELD NAME". * @param field */ export const field2Caption = (field: string): string => String(field).replaceAll("_", " ").toUpperCase(); + +/** + * Returns whether `string` is a link according to `REGEX_URL`. + * @param string + */ +export const isLink = (string: string): boolean => + Boolean(string.match(REGEX_URL)); From 7c6cb589440ce3919e23bb3e68a1fd0501afdeae Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Mon, 5 Feb 2024 11:00:59 +0100 Subject: [PATCH 5/8] :lipstick: #11 - style: improve alignment on both mobile and desktop --- src/components/datagrid/datagrid.scss | 21 ++++++++++------- src/components/datagrid/datagrid.tsx | 33 +++++++++++++++++---------- src/settings/tokens.css | 3 ++- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/components/datagrid/datagrid.scss b/src/components/datagrid/datagrid.scss index 903d5bf2..dc0c7a84 100644 --- a/src/components/datagrid/datagrid.scss +++ b/src/components/datagrid/datagrid.scss @@ -30,12 +30,11 @@ padding: var(--spacing-v-m) var(--spacing-h-m); } - &__cell--type-boolean { - text-align: center; - } - + &__cell--type-boolean, &__cell--type-number { + padding: var(--spacing-v-m) var(--spacing-h-xxl); text-align: end; + width: 0; } &__foot { @@ -72,14 +71,12 @@ } &__row { - background-color: var( - --typography-color-background - ); //border-radius: var(--border-radus-m); + background-color: var(--typography-color-background); display: flex; flex-wrap: wrap; &:nth-child(even) { - background-color: var(--theme-shade-100); + background-color: var(--typography-color-background-dark); } } @@ -116,6 +113,14 @@ } } + &__cell--type-boolean, + &__cell--type-number { + flex-direction: row; + padding: var(--spacing-v-m) var(--spacing-h-m); + text-align: right; + width: 100%; + } + &__foot { display: flex; } diff --git a/src/components/datagrid/datagrid.tsx b/src/components/datagrid/datagrid.tsx index 62242544..46dc9e9c 100644 --- a/src/components/datagrid/datagrid.tsx +++ b/src/components/datagrid/datagrid.tsx @@ -79,7 +79,6 @@ export const DataGrid: React.FC = ({ }) => { const id = useId(); const renderableFields = fields.filter((f) => !urlFields.includes(f)); - const captions = renderableFields.map(field2Caption); const titleId = title ? `${id}-caption` : undefined; /** @@ -170,17 +169,27 @@ export const DataGrid: React.FC = ({ {/* Headings */} - {captions.map((caption) => ( - -

- {caption} -

- - ))} + {renderableFields.map((field) => { + const caption = field2Caption(field); + const data = results?.[0]?.[field]; + const type = typeof data; + + return ( + +

+ {caption} +

+ + ); + })} diff --git a/src/settings/tokens.css b/src/settings/tokens.css index 78c54ac6..cb1c1698 100644 --- a/src/settings/tokens.css +++ b/src/settings/tokens.css @@ -26,7 +26,7 @@ --theme-shade-400: #b3b3b3; --theme-shade-300: #eaeaea; --theme-shade-200: #e6e6e6; - --theme-shade-100: #f0f0f0; + --theme-shade-100: #f7f7f7; --theme-shade-50: #fcfcfc; --theme-shade-0: #fff; @@ -70,6 +70,7 @@ /* TYPOGRAPHY */ --typography-color-background: var(--theme-shade-0); + --typography-color-background-dark: var(--theme-shade-100); --typography-color-h: var(--theme-shade-900); --typography-color-body: var(--theme-shade-700); --typography-color-muted: var(--theme-shade-600); From 0247278ed213851ff6667b95cf986fce801390c1 Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Mon, 5 Feb 2024 11:44:13 +0100 Subject: [PATCH 6/8] :recycle: #11 - refactor: refactor color usage --- src/components/button/button.scss | 2 +- src/components/datagrid/datagrid.scss | 4 ++-- src/components/form/input/input.scss | 2 +- src/components/form/select/select.scss | 4 ++-- src/components/typography/hr/hr.scss | 2 +- src/settings/tokens.css | 6 +++++- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/button/button.scss b/src/components/button/button.scss index dfa316ac..3d4507bc 100644 --- a/src/components/button/button.scss +++ b/src/components/button/button.scss @@ -69,7 +69,7 @@ &--variant-outline { --mykn-button-color-background: transparent; - --mykn-button-color-border: var(--theme-shade-700); + --mykn-button-color-border: var(--form-color-border); --mykn-button-color-shadow: currentColor; --mykn-button-color-text: var(--typography-color-body); diff --git a/src/components/datagrid/datagrid.scss b/src/components/datagrid/datagrid.scss index dc0c7a84..a1033c1e 100644 --- a/src/components/datagrid/datagrid.scss +++ b/src/components/datagrid/datagrid.scss @@ -21,11 +21,11 @@ } &__head &__row:first-child &__cell { - border-top: 1px solid var(--theme-shade-300); + border-top: 1px solid var(--typography-color-border); } &__cell { - border-bottom: 1px solid var(--theme-shade-300); + border-bottom: 1px solid var(--typography-color-border); box-sizing: border-box; padding: var(--spacing-v-m) var(--spacing-h-m); } diff --git a/src/components/form/input/input.scss b/src/components/form/input/input.scss index b1986c03..736263d9 100644 --- a/src/components/form/input/input.scss +++ b/src/components/form/input/input.scss @@ -2,7 +2,7 @@ appearance: none; align-items: center; background: var(--typography-color-background); - border: 1px solid var(--theme-color-primary-800); + border: 1px solid var(--form-color-border); border-radius: 6px; box-sizing: border-box; color: var(--typography-color-body); diff --git a/src/components/form/select/select.scss b/src/components/form/select/select.scss index 6d165fee..5a992350 100644 --- a/src/components/form/select/select.scss +++ b/src/components/form/select/select.scss @@ -1,7 +1,7 @@ .mykn-select { align-items: center; background: var(--typography-color-background); - border: 1px solid var(--theme-color-primary-800); + border: 1px solid var(--form-color-border); border-radius: var(--border-radius-m); box-sizing: border-box; color: var(--typography-color-body); @@ -85,7 +85,7 @@ white-space: nowrap; &[aria-selected="true"] { - --mykn-option-color-background: var(--theme-shade-300); + --mykn-option-color-background: var(--typography-color-background-dark); --mykn-option-font-weight: var(--typography-font-weight-bold); } diff --git a/src/components/typography/hr/hr.scss b/src/components/typography/hr/hr.scss index b2199a1f..c135319c 100644 --- a/src/components/typography/hr/hr.scss +++ b/src/components/typography/hr/hr.scss @@ -1,5 +1,5 @@ .mykn-hr { border: 0; - border-bottom: 1px solid var(--theme-shade-300); + border-bottom: 1px solid var(--typography-color-border); margin: 0; } diff --git a/src/settings/tokens.css b/src/settings/tokens.css index cb1c1698..8e817263 100644 --- a/src/settings/tokens.css +++ b/src/settings/tokens.css @@ -34,7 +34,7 @@ --theme-color-success-background: #e9ffe9; --theme-color-danger-body: #ff4545; - --theme-color-danger-background: #{rgba(#ff4545, 0.1)}; + --theme-color-danger-background: #FFDFDF; /* SPACING */ --border-radius-l: 12px; @@ -68,11 +68,15 @@ --spacing-h-xxxl: 40px; --spacing-v-xxxl: 40px; + /* FORM */ + --form-color-border: var(--theme-shade-300); + /* TYPOGRAPHY */ --typography-color-background: var(--theme-shade-0); --typography-color-background-dark: var(--theme-shade-100); --typography-color-h: var(--theme-shade-900); --typography-color-body: var(--theme-shade-700); + --typography-color-border: var(--theme-shade-300); --typography-color-muted: var(--theme-shade-600); --typography-font-size-h1: 24px; From 7976b2c6bf5dcfeae6e13d8bb869a57a3a437502 Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Mon, 5 Feb 2024 11:52:27 +0100 Subject: [PATCH 7/8] :twisted_rightwards_arrows: #11 - fix: merge duplicate string formatting lib --- src/lib/format/string.ts | 17 +++++++++++++++++ src/lib/format/string.tsx | 16 ---------------- 2 files changed, 17 insertions(+), 16 deletions(-) delete mode 100644 src/lib/format/string.tsx diff --git a/src/lib/format/string.ts b/src/lib/format/string.ts index 9e3698c3..7aceb1bd 100644 --- a/src/lib/format/string.ts +++ b/src/lib/format/string.ts @@ -1,3 +1,20 @@ +/** Matches a URL. */ +export const REGEX_URL = /https?:\/\/[^\s]+$/; + +/** + * Returns whether `string` is a link according to `REGEX_URL`. + * @param string + */ +export const isLink = (string: string): boolean => + Boolean(string.match(REGEX_URL)); + +/** + * Converts "field_name" to "FIELD NAME". + * @param field + */ +export const field2Caption = (field: string): string => + String(field).replaceAll("_", " ").toUpperCase(); + /** * Converts "Some object name" to "some-object-name". * @param input diff --git a/src/lib/format/string.tsx b/src/lib/format/string.tsx deleted file mode 100644 index 66c8c512..00000000 --- a/src/lib/format/string.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/** Matches a URL. */ -export const REGEX_URL = /https?:\/\/[^\s]+$/; - -/** - * Converts "field_name" to "FIELD NAME". - * @param field - */ -export const field2Caption = (field: string): string => - String(field).replaceAll("_", " ").toUpperCase(); - -/** - * Returns whether `string` is a link according to `REGEX_URL`. - * @param string - */ -export const isLink = (string: string): boolean => - Boolean(string.match(REGEX_URL)); From 888dc0f83f5a7fb8cd80258fcda441c99eec175e Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Mon, 5 Feb 2024 11:53:24 +0100 Subject: [PATCH 8/8] :recycle: #11 - fix: fix incorrect variable name for border radius --- src/components/badge/badge.scss | 2 +- src/components/datagrid/datagrid.scss | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/badge/badge.scss b/src/components/badge/badge.scss index 1815f50c..950d1a9f 100644 --- a/src/components/badge/badge.scss +++ b/src/components/badge/badge.scss @@ -1,7 +1,7 @@ .mykn-badge { background-color: var(--theme-color-primary-200); align-items: center; - border-radius: var(--border-radus-m); + border-radius: var(--border-radius-m); display: inline-flex; font-family: Inter, sans-serif; font-size: var(--typography-font-size-body-xs); diff --git a/src/components/datagrid/datagrid.scss b/src/components/datagrid/datagrid.scss index a1033c1e..266b19f2 100644 --- a/src/components/datagrid/datagrid.scss +++ b/src/components/datagrid/datagrid.scss @@ -2,7 +2,7 @@ .mykn-datagrid { background-color: var(--typography-color-background); - border-radius: var(--border-radus-m); + border-radius: var(--border-radius-m); &__table { border-spacing: 0; @@ -58,7 +58,7 @@ &__caption { background-color: var(--typography-color-background); - border-radius: var(--border-radus-m); + border-radius: var(--border-radius-m); display: block; } @@ -138,7 +138,7 @@ } .mykn-toolbar { - border-radius: var(--border-radus-m); + border-radius: var(--border-radius-m); } .mykn-paginator .mykn-icon--spin:first-child {