From 570aac85d5342c72ba028df2a95e73fde5aa199d Mon Sep 17 00:00:00 2001 From: Artur Bien Date: Wed, 24 Apr 2024 18:25:37 +0200 Subject: [PATCH] feat(DataList): add DataList component --- .../stories/components/data-list.stories.tsx | 402 ++++++++++++++++++ .../frosted-ui/src/components/data-list.css | 256 +++++++++++ .../src/components/data-list.props.ts | 45 ++ .../frosted-ui/src/components/data-list.tsx | 149 +++++++ packages/frosted-ui/src/components/index.ts | 9 + packages/frosted-ui/src/styles/index.css | 1 + 6 files changed, 862 insertions(+) create mode 100644 packages/frosted-ui/.storybook/stories/components/data-list.stories.tsx create mode 100644 packages/frosted-ui/src/components/data-list.css create mode 100644 packages/frosted-ui/src/components/data-list.props.ts create mode 100644 packages/frosted-ui/src/components/data-list.tsx diff --git a/packages/frosted-ui/.storybook/stories/components/data-list.stories.tsx b/packages/frosted-ui/.storybook/stories/components/data-list.stories.tsx new file mode 100644 index 00000000..1584969c --- /dev/null +++ b/packages/frosted-ui/.storybook/stories/components/data-list.stories.tsx @@ -0,0 +1,402 @@ +import { Copy12 } from '@frosted-ui/icons'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { + Badge, + Code, + DataList, + Flex, + Heading, + IconButton, + Link, + Separator, + Text, + Tooltip, + dataListRootPropDefs, +} from '../../../src/components/'; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta = { + title: 'Data presentation/DataList', + component: DataList.Root, + args: { + size: dataListRootPropDefs.size.default, + orientation: dataListRootPropDefs.orientation.default, + trim: dataListRootPropDefs.trim.default, + }, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Default: Story = { + render: ({ children, ...args }) => ( +
+ + {''} component displays metadata as a list of + key-value pairs. + + + + + Status + + + + Active + + + + + + ID + + + + biz_AB23XH123A + + + {/* @ts-expect-error -- TODO: fix frosted icons types */} + + + + + + + + + Name + + Artur Bień + + + + Email + + + artur@whop.com + + + + + Company + + + + Whop + + + + +
+ ), +}; +export const Size: Story = { + render: ({ children, size, ...args }) => ( + + + + + Name + + Artur Bień + + + + Email + + + artur@whop.com + + + + + Company + + + + Whop + + + + + + + + Name + + Artur Bień + + + + Email + + + artur@whop.com + + + + + Company + + + + Whop + + + + + + + + Name + + Artur Bień + + + + Email + + + artur@whop.com + + + + + Company + + + + Whop + + + + + + ), +}; + +export const Orientation: Story = { + render: ({ children, orientation, ...args }) => ( + +
+ + Horizontal + + + + + + Name + + Artur Bień + + + + Email + + + artur@whop.com + + + + + Company + + + + Whop + + + + +
+
+ + Vertical + + + + + + Name + + Artur Bień + + + + Email + + + artur@whop.com + + + + + Company + + + + Whop + + + + +
+
+ ), +}; + +export const Color: Story = { + render: ({ children, ...args }) => ( + + + Use the color prop on the{' '} + {''} component to assign a specific + color. + + + + + Color: + + Iris + + + + Color: + + Cyan + + + + Color: + + Lime + + + + Color: + + Crimson + + + + ), +}; +export const HighContrast: Story = { + name: 'High Contrast', + render: ({ children, ...args }) => ( + + + Use the highContrast prop on the{' '} + {''} component
to increase color + contrast with the background. +
+ + + + Name + Iris + + + Name + Cyan + + + Name + Lime + + + Name + Crimson + + + + + + + Name + + Iris + + + + Name + + Cyan + + + + Name + + Lime + + + + Name + + Crimson + + + +
+ ), +}; diff --git a/packages/frosted-ui/src/components/data-list.css b/packages/frosted-ui/src/components/data-list.css new file mode 100644 index 00000000..b341915d --- /dev/null +++ b/packages/frosted-ui/src/components/data-list.css @@ -0,0 +1,256 @@ +.fui-DataListRoot { + font-family: var(--default-font-family); + font-weight: var(--font-weight-normal); + font-style: normal; + text-align: start; +} + +.fui-DataListLabel { + display: flex; + color: var(--gray-a11); + + &:where(.fui-high-contrast) { + color: var(--gray-12); + } + + &:where([data-accent-color]) { + color: var(--accent-a11); + + &:where(.fui-high-contrast) { + color: var(--accent-12); + } + } +} + +.fui-DataListValue { + display: flex; + margin: 0; + + /* Ensure value can be truncated */ + min-width: 0px; +} + +/*************************************************************************************************** + * * + * ORIENTATION * + * * + ***************************************************************************************************/ + +.fui-DataListItem { + /* The actual margins that value part gets. These are re-assigned to other vars depending on the orientation */ + --data-list-value-margin-top: 0px; + --data-list-value-margin-bottom: 0px; + --data-list-first-item-value-margin-top: 0px; + --data-list-last-item-value-margin-bottom: 0px; + + /* How much the value part can poke outside of the row when in a horizontal data list */ + --data-list-value-trim-start: -0.25em; + --data-list-value-trim-end: -0.25em; + --data-list-first-item-value-trim-start: 0px; + --data-list-last-item-value-trim-end: 0px; +} + +.fui-DataListValue { + margin-top: var(--data-list-value-margin-top); + margin-bottom: var(--data-list-value-margin-bottom); + + /* + * The first/last item should not poke out of the Root boundaries – + * unless it has "align-items: center", but that’s handled later. + */ + :where(.fui-DataListItem:first-child) & { + margin-top: var(--data-list-first-item-value-margin-top); + } + :where(.fui-DataListItem:last-child) & { + margin-bottom: var(--data-list-last-item-value-margin-bottom); + } +} + +/* * * * * * * * * * * * * * * * * * * */ +/* */ +/* Sizes */ +/* */ +/* * * * * * * * * * * * * * * * * * * */ + +@breakpoints { + .fui-DataListRoot { + &:where(.fui-r-size-1) { + gap: var(--space-3); + } + &:where(.fui-r-size-2) { + gap: var(--space-4); + } + &:where(.fui-r-size-3) { + gap: calc(var(--space-4) * 1.25); + } + } +} + +/* * * * * * * * * * * * * * * * * * * */ +/* */ +/* Orientation */ +/* */ +/* * * * * * * * * * * * * * * * * * * */ + +@breakpoints { + .fui-DataListRoot { + &:where(.fui-r-orientation-vertical) { + display: flex; + flex-direction: column; + + & :where(.fui-DataListItem) { + /* No poking out of the row when orientation is vertical */ + --data-list-value-margin-top: 0px; + --data-list-value-margin-bottom: 0px; + --data-list-first-item-value-margin-top: 0px; + --data-list-last-item-value-margin-bottom: 0px; + + display: flex; + flex-direction: column; + gap: var(--space-1); + } + + & :where(.fui-DataListLabel) { + /* Ensure label can be truncated */ + min-width: 0px; + } + } + + &:where(.fui-r-orientation-horizontal) { + display: grid; + grid-template-columns: auto 1fr; + + & :where(.fui-DataListItem) { + /* Allow the value to poke out of the row when orientation is horizontal */ + --data-list-value-margin-top: var(--data-list-value-trim-start); + --data-list-value-margin-bottom: var(--data-list-value-trim-end); + --data-list-first-item-value-margin-top: var( + --data-list-first-item-value-trim-start + ); + --data-list-last-item-value-margin-bottom: var( + --data-list-last-item-value-trim-end + ); + + display: grid; + /* Use subgrid so all the label columns remain aligned */ + grid-template-columns: inherit; /* Fallback */ + grid-template-columns: subgrid; + gap: inherit; + grid-column: span 2; + align-items: baseline; + } + + & :where(.fui-DataListLabel) { + /* Set an implicit min. width when orientation is horizontal */ + min-width: 120px; + } + } + } +} + +/* * * * * * * * * * * * * * * * * * * */ +/* */ +/* Alignment */ +/* */ +/* * * * * * * * * * * * * * * * * * * */ + +.fui-DataListLabel, +.fui-DataListValue { + &::before { + /* + * Zero-width joiner to establish a baseline. + * Allows Flex children with text to align automatically. + */ + content: '‍'; + } +} + +@breakpoints { + /* + * Make sure that the margin adjustments cooperate with "align-items". + * To do that, we need to remove the corresponding margin adjustment depending on the "align-items" value. + * We can't set `--data-list-value-margin-top` directly because at breakpoints it would lose the orientation value. + */ + .fui-DataListItem { + /* Match the default: poke out at the top and bottom, but not when it’s the first or last item */ + &:where(.fui-r-ai-baseline) { + --data-list-value-trim-start: -0.25em; + --data-list-value-trim-end: -0.25em; + --data-list-first-item-value-trim-start: 0px; + --data-list-last-item-value-trim-end: 0px; + } + /* No poking out at the top; and not at the bottom when it’s the first or last item */ + &:where(.fui-r-ai-start) { + --data-list-value-trim-start: 0px; + --data-list-value-trim-end: -0.25em; + --data-list-first-item-value-trim-start: 0px; + --data-list-last-item-value-trim-end: 0px; + } + /* Allow to poke out from any side, as for centering to work the top and bottom margins have to be always equal */ + &:where(.fui-r-ai-center) { + --data-list-value-trim-start: -0.25em; + --data-list-value-trim-end: -0.25em; + --data-list-first-item-value-trim-start: -0.25em; + --data-list-last-item-value-trim-end: -0.25em; + } + /* No poking out at the bottom; and not at the top when it’s the first or last item */ + &:where(.fui-r-ai-end) { + --data-list-value-trim-start: -0.25em; + --data-list-value-trim-end: 0px; + --data-list-first-item-value-trim-start: 0px; + --data-list-last-item-value-trim-end: 0px; + } + /* No poking out when stretched */ + &:where(.fui-r-ai-stretch) { + --data-list-value-trim-start: 0px; + --data-list-value-trim-end: 0px; + --data-list-first-item-value-trim-start: 0px; + --data-list-last-item-value-trim-end: 0px; + } + } +} + +/* * * * * * * * * * * * * * * * * * * */ +/* */ +/* Trim */ +/* */ +/* * * * * * * * * * * * * * * * * * * */ + +.fui-DataListRoot { + --data-list-leading-trim-start: calc( + var(--default-leading-trim-start) - var(--line-height) / 2 + ); + --data-list-leading-trim-end: calc( + var(--default-leading-trim-end) - var(--line-height) / 2 + ); +} + +.fui-DataListItem { + &:where(:first-child) { + margin-top: var(--leading-trim-start); + } + &:where(:last-child) { + margin-bottom: var(--leading-trim-end); + } +} + +@breakpoints { + .fui-DataListRoot { + &:where(.fui-r-trim-normal) { + --leading-trim-start: initial; + --leading-trim-end: initial; + } + &:where(.fui-r-trim-start) { + --leading-trim-start: var(--data-list-leading-trim-start); + --leading-trim-end: initial; + } + &:where(.fui-r-trim-end) { + --leading-trim-start: initial; + --leading-trim-end: var(--data-list-leading-trim-end); + } + &:where(.fui-r-trim-both) { + --leading-trim-start: var(--data-list-leading-trim-start); + --leading-trim-end: var(--data-list-leading-trim-end); + } + } +} diff --git a/packages/frosted-ui/src/components/data-list.props.ts b/packages/frosted-ui/src/components/data-list.props.ts new file mode 100644 index 00000000..835e34c9 --- /dev/null +++ b/packages/frosted-ui/src/components/data-list.props.ts @@ -0,0 +1,45 @@ +import { PropDef, colorProp, highContrastProp, trimProp } from '../helpers'; + +const alignValues = ['start', 'center', 'end', 'baseline', 'stretch'] as const; +const orientationValues = ['horizontal', 'vertical'] as const; +const sizes = ['1', '2', '3'] as const; + +const dataListRootPropDefs = { + orientation: { + type: 'enum', + values: orientationValues, + default: 'horizontal', + responsive: true, + }, + size: { + type: 'enum', + values: sizes, + default: '2', + responsive: true, + }, + trim: { + ...trimProp, + }, +} satisfies { + orientation: PropDef<(typeof orientationValues)[number]>; + size: PropDef<(typeof sizes)[number]>; + trim: typeof trimProp; +}; + +const dataListItemPropDefs = { + align: { + type: 'enum', + values: alignValues, + responsive: true, + default: undefined, + }, +} satisfies { + align: PropDef<(typeof alignValues)[number]>; +}; + +const dataListLabelPropDefs = { + color: colorProp, + highContrast: highContrastProp, +}; + +export { dataListItemPropDefs, dataListLabelPropDefs, dataListRootPropDefs }; diff --git a/packages/frosted-ui/src/components/data-list.tsx b/packages/frosted-ui/src/components/data-list.tsx new file mode 100644 index 00000000..9aa52149 --- /dev/null +++ b/packages/frosted-ui/src/components/data-list.tsx @@ -0,0 +1,149 @@ +import classNames from 'classnames'; +import * as React from 'react'; + +import { + dataListItemPropDefs, + dataListLabelPropDefs, + dataListRootPropDefs, +} from './data-list.props'; +import { Text } from './text'; + +import { + extractMarginProps, + withBreakpoints, + withMarginProps, + type GetPropDefTypes, + type MarginProps, + type PropsWithoutRefOrColor, +} from '../helpers'; + +type DataListRootElement = HTMLDListElement; +type DataListRootOwnProps = GetPropDefTypes; +interface DataListRootProps + extends PropsWithoutRefOrColor<'dl'>, + MarginProps, + DataListRootOwnProps {} +const DataListRoot = React.forwardRef( + (props, forwardedRef) => { + const { rest: marginRest, ...marginProps } = extractMarginProps(props); + + const { + className, + size = dataListRootPropDefs.size.default, + trim = dataListRootPropDefs.trim.default, + orientation = dataListRootPropDefs.orientation.default, + ...dataListProps + } = marginRest; + + return ( + +
+ + ); + }, +); +DataListRoot.displayName = 'DataList.Root'; + +type DataListItemElement = HTMLDivElement; +type DataListItemOwnProps = GetPropDefTypes; +interface DataListItemProps + extends PropsWithoutRefOrColor<'div'>, + DataListItemOwnProps {} +const DataListItem = React.forwardRef( + (props, forwardedRef) => { + const { + className, + align = dataListItemPropDefs.align.default, + ...dataListItemProps + } = props; + + return ( +
+ ); + }, +); +DataListItem.displayName = 'DataList.Item'; + +type DataListLabelElement = React.ElementRef<'dt'>; +type DataListLabelOwnProps = GetPropDefTypes; +interface DataListLabelProps + extends PropsWithoutRefOrColor<'dt'>, + DataListLabelOwnProps {} +const DataListLabel = React.forwardRef< + DataListLabelElement, + DataListLabelProps +>((props, forwardedRef) => { + const { + className, + color = dataListLabelPropDefs.color.default, + highContrast = dataListLabelPropDefs.highContrast.default, + ...dataListLabelProps + } = props; + + return ( +
+ ); +}); +DataListLabel.displayName = 'DataList.Label'; + +type DataListValueElement = React.ElementRef<'dd'>; +interface DataListValueProps extends PropsWithoutRefOrColor<'dd'> {} +const DataListValue = React.forwardRef< + DataListValueElement, + DataListValueProps +>(({ children, className, ...props }, forwardedRef) => ( +
+ {children} +
+)); +DataListValue.displayName = 'DataList.Value'; + +const DataList = Object.assign( + {}, + { + Root: DataListRoot, + Item: DataListItem, + Label: DataListLabel, + Value: DataListValue, + }, +); + +export { DataList, DataListItem, DataListLabel, DataListRoot, DataListValue }; +export type { + DataListItemProps, + DataListLabelProps, + DataListRootProps, + DataListValueProps, +}; diff --git a/packages/frosted-ui/src/components/index.ts b/packages/frosted-ui/src/components/index.ts index 87b5fc34..f0f5651e 100644 --- a/packages/frosted-ui/src/components/index.ts +++ b/packages/frosted-ui/src/components/index.ts @@ -248,7 +248,16 @@ export { SegmentedControlRadioGroupRoot, } from './segmented-control-radio-group'; +export { + DataList, + DataListItem, + DataListLabel, + DataListRoot, + DataListValue, +} from './data-list'; export * from './segmented-control-nav.props'; + +export * from './data-list.props'; export { Separator } from './separator'; export * from './separator.props'; diff --git a/packages/frosted-ui/src/styles/index.css b/packages/frosted-ui/src/styles/index.css index 690814a7..7f62a23c 100644 --- a/packages/frosted-ui/src/styles/index.css +++ b/packages/frosted-ui/src/styles/index.css @@ -27,6 +27,7 @@ @import '../components/code.css'; @import '../components/container.css'; @import '../components/context-menu.css'; +@import '../components/data-list.css'; @import '../components/dialog.css'; @import '../components/drawer.css'; @import '../components/dropdown-menu.css';