diff --git a/src/components/Table.tsx b/src/components/Table.tsx new file mode 100644 index 00000000..3d8e14af --- /dev/null +++ b/src/components/Table.tsx @@ -0,0 +1,159 @@ +import styled, { css } from 'styled-components'; + +const maxCols = 12; + +interface GapProps { + /** Offset between child elements */ + gap?: number; +} + +interface AlignProps { + /** As `align-items` for flexible row container */ + align: 'start' | 'center' | 'end' | 'baseline'; +} + +interface JustifyProps { + /** As `justify-content` for flexible container */ + justify: 'start' | 'center' | 'end' | 'between' | 'around' | 'baseline'; +} + +export type TableCellProps = + | { + /** Size of column width in range from `1` to `12` */ + col: number; + /** Minimum size column, use instead of `col` prop */ + min?: never; + /** Absolute or relative width, ex. `10ch` or `100` */ + width?: never; + } + | { + col?: never; + min: boolean; + width?: never; + } + | { + col?: never; + min?: never; + width: number | string; + } + | { + col?: never; + min?: never; + width?: never; + }; + +export interface TableProps extends GapProps { + /** Container width */ + width?: number; +} + +const mapRuleSet: { [key in AlignProps['align'] | JustifyProps['justify']]: string } = { + baseline: 'baseline', + start: 'flex-start', + end: 'flex-end', + center: 'center', + around: 'space-around', + between: 'space-between', +}; + +export const Table = styled.div` + display: flex; + flex-direction: column; + + ${({ width }) => + width && + css` + width: ${width}px; + `} + + ${({ gap }) => + gap && + css` + gap: ${gap}px; + `} +`; + +export const TableRow = styled.div & GapProps>` + display: flex; + flex-basis: 100%; + align-items: flex-start; + justify-content: flex-start; + + ${({ align }) => + align && + css` + align-items: ${mapRuleSet[align]}; + `} + + ${({ justify }) => + justify && + css` + justify-content: ${mapRuleSet[justify]}; + `} + + ${({ gap }) => + gap && + css` + gap: ${gap}px; + `} +`; + +export const TableCell = styled.div>` + display: inline-flex; + align-items: baseline; + flex-wrap: wrap; + + ${({ col }) => { + if (!col) { + return css` + flex: 1 0 0%; + max-width: 100%; + `; + } + + if (col > maxCols) { + return css` + flex: 0 0 100%; + max-width: 100%; + `; + } + + const width = (col / maxCols) * 100; + + return css` + flex: 0 0 ${width}%; + max-width: {width}%; + `; + }} + + ${({ width }) => { + if (width == null) { + return; + } + const unit = typeof width === 'number' ? 'px' : ''; + + return css` + flex: 0 0 ${width}${unit}; + max-width: ${width}${unit}; + `; + }} + + ${({ min }) => + min && + css` + flex-basis: 0%; + max-width: max-content; + `} + + ${({ justify }) => + justify && + css` + justify-content: ${mapRuleSet[justify]}; + `} + + ${({ align }) => + align && + css` + align-self: ${mapRuleSet[align]}; + `} +`; diff --git a/src/components/index.tsx b/src/components/index.tsx index 6a103c1b..00289d16 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -48,3 +48,4 @@ export * from './FiltersContainers'; export * from './FiltersCounter'; export * from './FiltersDropdown'; export * from './UserMenuItem'; +export * from './Table'; diff --git a/src/stories/Table.stories.tsx b/src/stories/Table.stories.tsx new file mode 100644 index 00000000..3b085536 --- /dev/null +++ b/src/stories/Table.stories.tsx @@ -0,0 +1,139 @@ +import React, { useState } from 'react'; +import type { Meta, StoryFn } from '@storybook/react'; +import { gray5, gray3 } from '@taskany/colors'; +import styled from 'styled-components'; + +import { Table, TableRow, TableCell } from '../components/Table'; +import { GoalIcon } from '../components/Icon/GoalIcon'; +import { Text } from '../components/Text'; +import { UserPic } from '../components/UserPic'; +import { Dot } from '../components/Dot'; +import { Tag } from '../components/Tag'; +import { CircleProgressBar } from '../components/CircleProgressBar'; +import { Button } from '../components/Button'; + +const meta: Meta = { + title: 'Table', + component: Table, +}; + +export default meta; +type Story = StoryFn; + +const data = Array.from({ length: 10 }, (_, i) => ({ + title: `Title ${i + 1}`, + projectId: (Math.random() * 10).toString(16).slice(2, 8).padEnd(6, 'AB').toUpperCase(), + tags: Array.from({ length: Math.ceil(Math.random() * 5) }, (_, i) => `Tag ${i + 1}`), + progress: Math.round(Math.random() * 100), + children: Array.from({ length: 3 }, (_, i) => ({ + title: `Title ${i + 1}`, + projectId: (Math.random() * 10).toString(16).slice(2, 8).padEnd(6, 'AB').toUpperCase(), + tags: Array.from({ length: Math.ceil(Math.random() * 5) }, (_, i) => `Tag ${i + 1}`), + progress: Math.round(Math.random() * 100), + children: Array.from({ length: 3 }, (_, i) => ({ + title: `Title ${i + 1}`, + projectId: (Math.random() * 10).toString(16).slice(2, 8).padEnd(6, 'AB').toUpperCase(), + tags: Array.from({ length: Math.ceil(Math.random() * 5) }, (_, i) => `Tag ${i + 1}`), + progress: Math.round(Math.random() * 100), + children: undefined, + })), + })), +})); + +const StyledTableRow = styled(TableRow)<{ depth?: number }>` + padding: 5px 0; + border-bottom: 1px solid ${gray5}; + border-top: 1px solid ${gray3}; + + &:first-child { + border-top: 0; + } + + &:last-child { + border-bottom: 0; + } + + & > ${TableCell}:first-child { + padding-left: calc(1rem * ${({ depth = 0 }) => depth}); + box-sizing: border-box; + } +`; + +export const Default: Story = () => { + return ( + + {data.map(({ title, projectId, tags, progress }) => ( + + + + + + + + {title} + + + + + + + + {projectId} + + + + {tags.map((t) => ( + + ))} + + + + + + ))} +
+ ); +}; + +const CollapseRow = ({ rowData, childData, depth = 0 }) => { + const [showChilds, setShowChilds] = useState(false); + return ( + <> + + + + + {rowData.title} + + + + + + {childData?.length ? ( + +