feat(table): big improvements

This commit is contained in:
Junior Garcia 2023-04-22 17:12:56 -03:00
parent 4309dbf161
commit fbbedfbd2d
43 changed files with 1867 additions and 306 deletions

View File

@ -124,6 +124,7 @@ export function useAvatar(props: UseAvatarProps) {
const {isHovered, hoverProps} = useHover({isDisabled});
const imageStatus = useImage({src, onError, ignoreFallback});
const isImgLoaded = imageStatus === "loaded";
/**
@ -149,13 +150,6 @@ export function useAvatar(props: UseAvatarProps) {
[color, radius, size, isBordered, isDisabled, isInGroup, groupContext?.isGrid],
);
const buttonStyles = useMemo(() => {
if (as !== "button") return "";
// reset button classNames
return "appearance-none outline-none border-none cursor-pointer";
}, [as]);
const baseStyles = clsx(classNames?.base, className);
const canBeFocused = useMemo(() => {
@ -163,25 +157,25 @@ export function useAvatar(props: UseAvatarProps) {
}, [isFocusable, as]);
const getAvatarProps = useCallback<PropGetter>(
() => ({
(props = {}) => ({
ref: domRef,
tabIndex: canBeFocused ? 0 : -1,
"data-hover": dataAttr(isHovered),
"data-focus": dataAttr(isFocused),
"data-focus-visible": dataAttr(isFocusVisible),
className: slots.base({
class: clsx(baseStyles, buttonStyles),
class: clsx(baseStyles, props?.className),
}),
...mergeProps(otherProps, hoverProps, canBeFocused ? focusProps : {}),
}),
[canBeFocused, slots, baseStyles, buttonStyles, focusProps, otherProps],
[canBeFocused, slots, baseStyles, focusProps, otherProps],
);
const getImageProps = useCallback<PropGetter>(
() => ({
ref: imgRef,
src: src,
"data-loaded": isImgLoaded,
"data-loaded": dataAttr(isImgLoaded),
className: slots.img({class: classNames?.img}),
}),
[slots, isImgLoaded, src, imgRef],

View File

@ -226,7 +226,7 @@ export function useCheckbox(props: UseCheckboxProps) {
ref: domRef,
className: slots.base({class: baseStyles}),
"data-disabled": dataAttr(isDisabled),
"data-checked": dataAttr(isSelected),
"data-checked": dataAttr(isSelected || isIndeterminate),
"data-invalid": dataAttr(isInvalid),
"data-hover": dataAttr(isHovered),
"data-focus": dataAttr(isFocused),

View File

@ -0,0 +1,24 @@
# @nextui-org/spacer
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/spacer
# or
npm i @nextui-org/spacer
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## Licence
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,19 @@
import * as React from "react";
import {render} from "@testing-library/react";
import {Spacer} from "../src";
describe("Spacer", () => {
it("should render correctly", () => {
const wrapper = render(<Spacer />);
expect(() => wrapper.unmount()).not.toThrow();
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(<Spacer ref={ref} />);
expect(ref.current).not.toBeNull();
});
});

View File

@ -0,0 +1,58 @@
{
"name": "@nextui-org/spacer",
"version": "2.0.0-beta.1",
"description": "A flexible spacer component designed to create consistent spacing and maintain alignment in your layout.",
"keywords": [
"spacer"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/components/spacer"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=18"
},
"dependencies": {
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/dom-utils": "workspace:*"
},
"devDependencies": {
"clean-package": "2.2.0",
"react": "^18.0.0"
},
"clean-package": "../../../clean-package.config.json",
"tsup": {
"clean": true,
"target": "es2019",
"format": [
"cjs",
"esm"
]
}
}

View File

@ -0,0 +1,10 @@
import Spacer from "./spacer";
// export types
export type {SpacerProps} from "./spacer";
// export hooks
export {useSpacer} from "./use-spacer";
// export component
export {Spacer};

View File

@ -0,0 +1,15 @@
import {forwardRef} from "@nextui-org/system";
import {UseSpacerProps, useSpacer} from "./use-spacer";
export interface SpacerProps extends Omit<UseSpacerProps, "ref"> {}
const Spacer = forwardRef<SpacerProps, "span">((props, ref) => {
const {Component, getSpacerProps} = useSpacer({ref, ...props});
return <Component {...getSpacerProps()} />;
});
Spacer.displayName = "NextUI.Spacer";
export default Spacer;

View File

@ -0,0 +1,68 @@
import type {SpacerVariantProps} from "@nextui-org/theme";
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
import {spacer} from "@nextui-org/theme";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, dataAttr, ReactRef} from "@nextui-org/shared-utils";
import {useMemo} from "react";
export interface UseSpacerProps extends HTMLNextUIProps<"span", SpacerVariantProps> {
/**
* Ref to the DOM node.
*/
ref?: ReactRef<HTMLElement | null>;
/**
* The x-axis margin.
* @default 1
*/
x?: number;
/**
* The y-axis margin.
* @default 1
*/
y?: number;
}
export const getMargin = (num: number): string => {
return `calc(${num * 15.25}pt + 1px * ${num - 1})`;
};
export function useSpacer(originalProps: UseSpacerProps) {
const [props, variantProps] = mapPropsVariants(originalProps, spacer.variantKeys);
const {ref, as, className, x = 1, y = 1, ...otherProps} = props;
const Component = as || "span";
const domRef = useDOMRef(ref);
const styles = useMemo(
() =>
spacer({
...variantProps,
className,
}),
[...Object.values(variantProps), className],
);
const marginLeft = getMargin(x);
const marginTop = getMargin(y);
const getSpacerProps: PropGetter = (props = {}) => ({
ref: domRef,
...props,
...otherProps,
"aria-hidden": dataAttr(true),
className: clsx(styles, props.className),
style: {
...props.style,
...otherProps.style,
marginLeft,
marginTop,
},
});
return {Component, getSpacerProps};
}
export type UseSpacerReturn = ReturnType<typeof useSpacer>;

View File

@ -0,0 +1,46 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {spacer} from "@nextui-org/theme";
import {Spacer, SpacerProps} from "../src";
export default {
title: "Components/Spacer",
component: Spacer,
argTypes: {
color: {
control: {
type: "select",
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
},
},
radius: {
control: {
type: "select",
options: ["none", "base", "sm", "md", "lg", "xl", "full"],
},
},
size: {
control: {
type: "select",
options: ["xs", "sm", "md", "lg", "xl"],
},
},
isDisabled: {
control: {
type: "boolean",
},
},
},
} as ComponentMeta<typeof Spacer>;
const defaultProps = {
...spacer.defaultVariants,
};
const Template: ComponentStory<typeof Spacer> = (args: SpacerProps) => <Spacer {...args} />;
export const Default = Template.bind({});
Default.args = {
...defaultProps,
};

View File

@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"tailwind-variants": ["../../../node_modules/tailwind-variants"]
}
},
"include": ["src", "index.ts"]
}

View File

@ -38,6 +38,7 @@
},
"dependencies": {
"@nextui-org/checkbox": "workspace:*",
"@nextui-org/spacer": "workspace:*",
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/system": "workspace:*",
@ -46,9 +47,14 @@
"@react-aria/table": "^3.9.0",
"@react-aria/utils": "^3.16.0",
"@react-aria/visually-hidden": "^3.8.0",
"@react-aria/interactions": "^3.15.0",
"@react-stately/table": "^3.9.0"
},
"devDependencies": {
"@nextui-org/user": "workspace:*",
"@nextui-org/tooltip": "workspace:*",
"@nextui-org/chip": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@react-types/grid": "^3.1.7",
"@react-types/table": "^3.6.0",
"clean-package": "2.2.0",

View File

@ -0,0 +1,11 @@
export {default as TableBody} from "./table-body";
export {default as TableCell} from "./table-cell";
export {default as TableColumn} from "./table-column";
export {default as TableHeader} from "./table-header";
export {default as TableRow} from "./table-row";
// export types
export type {TableBodyProps} from "./table-body";
export type {TableCellProps} from "./table-cell";
export type {TableColumnProps} from "./table-column";
export type {TableHeaderProps} from "./table-header";

View File

@ -0,0 +1,10 @@
import {HTMLNextUIProps} from "@nextui-org/system";
import {TableBody as TableBodyBase} from "@react-stately/table";
import {TableBodyProps as TableBodyBaseProps} from "@react-types/table";
export type TableBodyProps<T> = TableBodyBaseProps<T> &
Omit<HTMLNextUIProps<"tbody">, keyof TableBodyBaseProps<T>>;
const TableBody = TableBodyBase as <T>(props: TableBodyProps<T>) => JSX.Element;
export default TableBody;

View File

@ -0,0 +1,9 @@
import {HTMLNextUIProps} from "@nextui-org/system";
import {Cell} from "@react-stately/table";
import {CellProps} from "@react-types/table";
export type TableCellProps = CellProps & HTMLNextUIProps<"td">;
const TableCell = Cell as (props: TableCellProps) => JSX.Element;
export default TableCell;

View File

@ -0,0 +1,10 @@
import {HTMLNextUIProps} from "@nextui-org/system";
import {Column} from "@react-stately/table";
import {SpectrumColumnProps} from "@react-types/table";
export type TableColumnProps<T> = Omit<SpectrumColumnProps<T>, "showDivider"> &
Omit<HTMLNextUIProps<"th">, keyof SpectrumColumnProps<T>>;
const TableColumn = Column as <T>(props: TableColumnProps<T>) => JSX.Element;
export default TableColumn;

View File

@ -0,0 +1,10 @@
import {HTMLNextUIProps} from "@nextui-org/system";
import {TableHeader as TableHeaderBase} from "@react-stately/table";
import {TableHeaderProps as TableHeaderBaseProps} from "@react-types/table";
export type TableHeaderProps<T> = TableHeaderBaseProps<T> &
Omit<HTMLNextUIProps<"thead">, keyof TableHeaderBaseProps<T>>;
const TableHeader = TableHeaderBase as <T>(props: TableHeaderProps<T>) => JSX.Element;
export default TableHeader;

View File

@ -0,0 +1,9 @@
import {HTMLNextUIProps} from "@nextui-org/system";
import {Row} from "@react-stately/table";
import {RowProps} from "@react-types/table";
export type TableRowProps = RowProps & Omit<HTMLNextUIProps<"tr">, keyof RowProps>;
const TableRow = Row as (props: TableRowProps) => JSX.Element;
export default TableRow;

View File

@ -1,5 +1,3 @@
import Table from "./table";
// export types
export type {TableProps} from "./table";
@ -7,4 +5,5 @@ export type {TableProps} from "./table";
export {useTable} from "./use-table";
// export component
export {Table};
export {default as Table} from "./table";
export * from "./base";

View File

@ -0,0 +1,50 @@
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx} from "@nextui-org/shared-utils";
import {useMemo} from "react";
import TableRow from "./table-row";
import TableCell from "./table-cell";
import TableRowGroup from "./table-row-group";
import TableCheckboxCell from "./table-checkbox-cell";
import {useTableContext} from "./table-context";
const TableBody = forwardRef<HTMLNextUIProps, "tbody">((props, ref) => {
const {as: asProp, className, ...otherProps} = props;
const as = asProp || "tbody";
const domRef = useDOMRef(ref);
const {slots, collection, classNames} = useTableContext();
const tbodyStyles = clsx(classNames?.tbody, className);
const renderRows = useMemo(() => {
return [...collection.body.childNodes].map((row) => (
<TableRow key={row.key} node={row}>
{[...row.childNodes].map((cell) =>
cell.props.isSelectionCell ? (
<TableCheckboxCell key={cell.key} node={cell} rowKey={row.key} />
) : (
<TableCell key={cell.key} node={cell} rowKey={row.key} />
),
)}
</TableRow>
));
}, [collection.body.childNodes]);
return (
<TableRowGroup
ref={domRef}
as={as}
{...otherProps}
className={slots.tbody?.({class: tbodyStyles})}
>
{renderRows}
</TableRowGroup>
);
});
TableBody.displayName = "NextUI.TableBody";
export default TableBody;

View File

@ -0,0 +1,58 @@
import type {GridNode} from "@react-types/grid";
import {Key} from "react";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {useTableCell} from "@react-aria/table";
import {filterDOMProps, mergeProps} from "@react-aria/utils";
import {useFocusRing} from "@react-aria/focus";
import {useTableContext} from "./table-context";
export interface TableCellProps<T = object> extends HTMLNextUIProps<"td"> {
/**
* The key of the table row.
*/
rowKey: Key;
/**
* The table cell.
*/
node: GridNode<T>;
}
const TableCell = forwardRef<TableCellProps, "td">((props, ref) => {
const {as, className, node, rowKey, ...otherProps} = props;
const Component = as || "td";
const domRef = useDOMRef(ref);
const {slots, state, classNames} = useTableContext();
const {gridCellProps} = useTableCell({node}, state, domRef);
const tdStyles = clsx(classNames?.thead, className, node.props?.className);
const {isFocusVisible, focusProps} = useFocusRing();
const isRowSelected = state.selectionManager.isSelected(rowKey);
return (
<Component
ref={domRef}
data-focus-visible={dataAttr(isFocusVisible)}
data-selected={dataAttr(isRowSelected)}
{...mergeProps(
gridCellProps,
focusProps,
filterDOMProps(node.props, {labelable: true}),
otherProps,
)}
className={slots.td?.({class: tdStyles})}
>
{node.rendered}
</Component>
);
});
TableCell.displayName = "NextUI.TableCell";
export default TableCell;

View File

@ -0,0 +1,75 @@
import type {GridNode} from "@react-types/grid";
import type {Key} from "react";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {useTableCell, useTableSelectionCheckbox} from "@react-aria/table";
import {filterDOMProps, mergeProps} from "@react-aria/utils";
import {useFocusRing} from "@react-aria/focus";
import {Checkbox} from "@nextui-org/checkbox";
import {VisuallyHidden} from "@react-aria/visually-hidden";
import {useTableContext} from "./table-context";
export interface TableCheckboxCellProps<T = object> extends HTMLNextUIProps<"td"> {
/**
* The key of the table row.
*/
rowKey: Key;
/**
* The table cell.
*/
node: GridNode<T>;
}
const TableCheckboxCell = forwardRef<TableCheckboxCellProps, "td">((props, ref) => {
const {as, className, node, rowKey, ...otherProps} = props;
const Component = as || "td";
const domRef = useDOMRef(ref);
const {slots, state, color, disableAnimation, selectionMode, classNames} = useTableContext();
const {gridCellProps} = useTableCell({node}, state, domRef);
const {isFocusVisible, focusProps} = useFocusRing();
const {checkboxProps} = useTableSelectionCheckbox({key: node?.parentKey || node.key}, state);
const tdStyles = clsx(classNames?.td, className, node.props?.className);
const isSingleSelectionMode = selectionMode === "single";
const {onChange, ...otherCheckboxProps} = checkboxProps;
const isRowSelected = state.selectionManager.isSelected(rowKey);
return (
<Component
ref={domRef}
data-focus-visible={dataAttr(isFocusVisible)}
data-selected={dataAttr(isRowSelected)}
{...mergeProps(
gridCellProps,
focusProps,
filterDOMProps(node.props, {labelable: true}),
otherProps,
)}
className={slots.td?.({class: tdStyles})}
>
{isSingleSelectionMode ? (
<VisuallyHidden>{checkboxProps["aria-label"]}</VisuallyHidden>
) : (
<Checkbox
color={color}
disableAnimation={disableAnimation}
onValueChange={onChange}
{...otherCheckboxProps}
/>
)}
</Component>
);
});
TableCheckboxCell.displayName = "NextUI.TableCheckboxCell";
export default TableCheckboxCell;

View File

@ -0,0 +1,77 @@
import type {GridNode} from "@react-types/grid";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx} from "@nextui-org/shared-utils";
import {useTableColumnHeader} from "@react-aria/table";
import {filterDOMProps, mergeProps} from "@react-aria/utils";
import {useFocusRing} from "@react-aria/focus";
import {VisuallyHidden} from "@react-aria/visually-hidden";
import {useTableContext} from "./table-context";
export interface TableColumnHeaderProps<T = object> extends HTMLNextUIProps<"th"> {
/**
* The table node to render.
*/
node: GridNode<T>;
}
const TableColumnHeader = forwardRef<TableColumnHeaderProps, "th">((props, ref) => {
const {as, className, node, ...otherProps} = props;
const Component = as || "th";
const domRef = useDOMRef(ref);
const {slots, state, classNames} = useTableContext();
const {columnHeaderProps} = useTableColumnHeader({node}, state, domRef);
const thStyles = clsx(classNames?.th, className, node.props?.className);
const {isFocusVisible, focusProps} = useFocusRing();
const {hideHeader, ...columnProps} = node.props;
let arrowIcon = state.sortDescriptor?.direction === "ascending" ? "▲" : "▼";
return (
<Component
ref={domRef}
colSpan={node.colspan}
data-focus-visible={isFocusVisible}
{...mergeProps(
columnHeaderProps,
focusProps,
filterDOMProps(columnProps, {labelable: true}),
otherProps,
)}
className={slots.th?.({class: thStyles})}
>
{hideHeader ? <VisuallyHidden>{node.rendered}</VisuallyHidden> : node.rendered}
{columnProps.allowsSorting && (
<span
aria-hidden="true"
style={{
padding: "0 2px",
visibility: state.sortDescriptor?.column === node.key ? "visible" : "hidden",
}}
>
{arrowIcon}
</span>
// <TableSortIcon
// ascending={state.sortDescriptor?.direction === "ascending"}
// css={{
// position: "absolute",
// m: "0 $2",
// bottom: `calc(50% - ${ICON_SIZE / 2}px)`,
// }}
// visible={state.sortDescriptor?.column === column.key}
// />
)}
</Component>
);
});
TableColumnHeader.displayName = "NextUI.TableColumnHeader";
export default TableColumnHeader;

View File

@ -0,0 +1,43 @@
import type {GridNode} from "@react-types/grid";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx} from "@nextui-org/shared-utils";
import {useTableHeaderRow} from "@react-aria/table";
import {filterDOMProps, mergeProps} from "@react-aria/utils";
import {useTableContext} from "./table-context";
export interface TableHeaderRowProps<T = object> extends HTMLNextUIProps<"tr"> {
/**
* The table node to render.
*/
node: GridNode<T>;
}
const TableHeaderRow = forwardRef<TableHeaderRowProps, "tr">((props, ref) => {
const {as, className, children, node, ...otherProps} = props;
const Component = as || "tr";
const domRef = useDOMRef(ref);
const {slots, state, classNames} = useTableContext();
const {rowProps} = useTableHeaderRow({node}, state, domRef);
const trStyles = clsx(classNames?.tr, className, node.props?.className);
return (
<Component
ref={domRef}
{...mergeProps(rowProps, filterDOMProps(node.props, {labelable: true}), otherProps)}
className={slots.tr?.({class: trStyles})}
>
{children}
</Component>
);
});
TableHeaderRow.displayName = "NextUI.TableHeaderRow";
export default TableHeaderRow;

View File

@ -0,0 +1,34 @@
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx} from "@nextui-org/shared-utils";
import {useTableRowGroup} from "@react-aria/table";
import {mergeProps} from "@react-aria/utils";
import {useTableContext} from "./table-context";
const TableRowGroup = forwardRef<HTMLNextUIProps, "thead">((props, ref) => {
const {as, className, children, ...otherProps} = props;
const Component = as || "thead";
const domRef = useDOMRef(ref);
const {slots, classNames} = useTableContext();
const {rowGroupProps} = useTableRowGroup();
const theadStyles = clsx(classNames?.thead, className);
return (
<Component
ref={domRef}
className={slots.thead?.({class: theadStyles})}
{...mergeProps(rowGroupProps, otherProps)}
>
{children}
</Component>
);
});
TableRowGroup.displayName = "NextUI.TableRowGroup";
export default TableRowGroup;

View File

@ -0,0 +1,80 @@
import type {GridNode} from "@react-types/grid";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {useTableRow} from "@react-aria/table";
import {filterDOMProps, mergeProps} from "@react-aria/utils";
import {useFocusRing} from "@react-aria/focus";
import {useHover} from "@react-aria/interactions";
import {useMemo} from "react";
import {useTableContext} from "./table-context";
export interface TableRowProps<T = object> extends HTMLNextUIProps<"tr"> {
/**
* The table row.
*/
node: GridNode<T>;
}
const TableRow = forwardRef<TableRowProps, "tr">((props, ref) => {
const {as, className, children, node, ...otherProps} = props;
const Component = as || "tr";
const domRef = useDOMRef(ref);
const {slots, state, isSelectable, classNames} = useTableContext();
const {rowProps} = useTableRow({node}, state, domRef);
const trStyles = clsx(classNames?.thead, className, node.props?.className);
const {isFocusVisible, focusProps} = useFocusRing();
const isDisabled = state.disabledKeys.has(node.key);
const isSelected = state.selectionManager.isSelected(node.key);
const {isHovered, hoverProps} = useHover({isDisabled});
const {isFirst, isLast, isMiddle, isOdd} = useMemo(() => {
const isFirst = node.key === state.collection.getFirstKey();
const isLast = node.key === state.collection.getLastKey();
const isMiddle = !isFirst && !isLast;
const isOdd = node?.index ? (node.index + 1) % 2 === 0 : false;
return {
isFirst,
isLast,
isMiddle,
isOdd,
};
}, [node, state.collection]);
return (
<Component
ref={domRef}
data-disabled={dataAttr(isDisabled)}
data-first={dataAttr(isFirst)}
data-focus-visible={dataAttr(isFocusVisible)}
data-hover={dataAttr(isHovered)}
data-last={dataAttr(isLast)}
data-middle={dataAttr(isMiddle)}
data-odd={dataAttr(isOdd)}
data-selected={dataAttr(isSelected)}
{...mergeProps(
rowProps,
isSelectable ? {...hoverProps, ...focusProps} : {},
filterDOMProps(node.props, {labelable: true}),
otherProps,
)}
className={slots.tr?.({class: trStyles})}
>
{children}
</Component>
);
});
TableRow.displayName = "NextUI.TableRow";
export default TableRow;

View File

@ -0,0 +1,68 @@
import type {GridNode} from "@react-types/grid";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {useTableColumnHeader, useTableSelectAllCheckbox} from "@react-aria/table";
import {filterDOMProps, mergeProps} from "@react-aria/utils";
import {useFocusRing} from "@react-aria/focus";
import {Checkbox} from "@nextui-org/checkbox";
import {VisuallyHidden} from "@react-aria/visually-hidden";
import {useTableContext} from "./table-context";
export interface TableSelectAllCheckboxProps<T = object> extends HTMLNextUIProps<"th"> {
/**
* The table column.
*/
node: GridNode<T>;
}
const TableSelectAllCheckbox = forwardRef<TableSelectAllCheckboxProps, "th">((props, ref) => {
const {as, className, node, ...otherProps} = props;
const Component = as || "th";
const domRef = useDOMRef(ref);
const {slots, state, selectionMode, color, disableAnimation, classNames} = useTableContext();
const {columnHeaderProps} = useTableColumnHeader({node}, state, domRef);
const {isFocusVisible, focusProps} = useFocusRing();
const {checkboxProps} = useTableSelectAllCheckbox(state);
const thStyles = clsx(classNames?.th, className, node.props?.className);
const isSingleSelectionMode = selectionMode === "single";
const {onChange, ...otherCheckboxProps} = checkboxProps;
return (
<Component
ref={domRef}
data-focus-visible={dataAttr(isFocusVisible)}
{...mergeProps(
columnHeaderProps,
focusProps,
filterDOMProps(node.props, {labelable: true}),
otherProps,
)}
className={slots.th?.({class: thStyles})}
>
{isSingleSelectionMode ? (
<VisuallyHidden>{checkboxProps["aria-label"]}</VisuallyHidden>
) : (
<Checkbox
color={color}
disableAnimation={disableAnimation}
onValueChange={onChange}
{...otherCheckboxProps}
/>
)}
</Component>
);
});
TableSelectAllCheckbox.displayName = "NextUI.TableSelectAllCheckbox";
export default TableSelectAllCheckbox;

View File

@ -1,18 +1,56 @@
import {forwardRef} from "@nextui-org/system";
import {Spacer} from "@nextui-org/spacer";
import {useCallback} from "react";
import {TableProvider} from "./table-context";
import {UseTableProps, useTable} from "./use-table";
import TableRowGroup from "./table-row-group";
import TableHeaderRow from "./table-header-row";
import TableColumnHeader from "./table-column-header";
import TableSelectAllCheckbox from "./table-select-all-checkbox";
import TableBody from "./table-body";
export interface TableProps extends Omit<UseTableProps, "ref"> {}
export interface TableProps
extends Omit<UseTableProps, "ref" | "isSelectable" | "isMultiSelectable"> {}
const Table = forwardRef<TableProps, "div">((props, ref) => {
const {Component, children, context, getBaseProps, getTableProps} = useTable({ref, ...props});
const Table = forwardRef<TableProps, "table">((props, ref) => {
const {Component, collection, context, removeWrapper, getBaseProps, getTableProps} = useTable({
ref,
...props,
});
const Wrapper = useCallback(
({children}: {children: JSX.Element}) => {
if (removeWrapper) {
return children;
}
return <div {...getBaseProps()}>{children}</div>;
},
[removeWrapper, getBaseProps],
);
return (
<TableProvider value={context}>
<div {...getBaseProps()}>
<Component {...getTableProps()}>{children}</Component>
</div>
<Wrapper>
<Component {...getTableProps()}>
<TableRowGroup>
{collection.headerRows.map((headerRow) => (
<TableHeaderRow key={headerRow?.key} node={headerRow}>
{[...headerRow.childNodes].map((column) =>
column?.props?.isSelectionCell ? (
<TableSelectAllCheckbox key={column?.key} node={column} />
) : (
<TableColumnHeader key={column?.key} node={column} />
),
)}
</TableHeaderRow>
))}
<Spacer as="tr" y={0.4} />
</TableRowGroup>
<TableBody />
</Component>
</Wrapper>
</TableProvider>
);
});

View File

@ -5,16 +5,16 @@ import type {
TableSlots,
} from "@nextui-org/theme";
import type {SelectionBehavior, DisabledBehavior} from "@react-types/shared";
import type {ReactNode, Key} from "react";
import type {TableState, TableStateProps} from "@react-stately/table";
import type {TableCollection} from "@react-types/table";
import {ReactNode, Key, useCallback} from "react";
import {useTableState} from "@react-stately/table";
import {useTable as useReactAriaTable} from "@react-aria/table";
import {AriaTableProps, useTable as useReactAriaTable} from "@react-aria/table";
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
import {table} from "@nextui-org/theme";
import {useDOMRef} from "@nextui-org/dom-utils";
import {mergeProps} from "@react-aria/utils";
import {filterDOMProps, mergeProps} from "@react-aria/utils";
import {clsx, ReactRef} from "@nextui-org/shared-utils";
import {useMemo} from "react";
@ -25,6 +25,11 @@ interface Props extends HTMLNextUIProps<"table"> {
ref?: ReactRef<HTMLElement | null>;
/** The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows. */
children?: ReactNode;
/**
* Whether the table container should not be rendered.
* @default false
*/
removeWrapper?: boolean;
/**
* Classname or List of classes to change the classNames of the element.
* if `className` is passed, it will be added to the base slot.
@ -46,33 +51,46 @@ interface Props extends HTMLNextUIProps<"table"> {
classNames?: SlotsToClasses<TableSlots>;
/**
* How multiple selection should behave in the collection.
* The selection behavior for the table. If selectionMode is `"none"`, this will be `null`.
* otherwise, this will be `toggle` or `replace`.
* @default "toggle"
*/
selectionBehavior?: SelectionBehavior;
selectionBehavior?: SelectionBehavior | null;
/**
* Whether `disabledKeys` applies to all interactions, or only selection.
* @default "selection"
*/
disabledBehavior?: DisabledBehavior;
/**
* Whether to disabled the related animations such as checkbox animation.
* @default false
*/
disableAnimation?: boolean;
/** Handler that is called when a user performs an action on the row. */
onRowAction?: (key: Key) => void;
/** Handler that is called when a user performs an action on the cell. */
onCellAction?: (key: Key) => void;
}
export type UseTableProps<T = object> = Props & TableStateProps<T> & TableVariantProps;
export type UseTableProps<T = object> = Props &
TableStateProps<T> &
AriaTableProps<T> &
TableVariantProps;
export type ContextType<T = object> = {
state: TableState<T>;
slots: TableReturnType;
collection: TableCollection<T>;
color: TableVariantProps["color"];
isSelectable: boolean;
selectionMode: UseTableProps<T>["selectionMode"];
selectionBehavior: SelectionBehavior | null;
disabledBehavior: UseTableProps<T>["disabledBehavior"];
disableAnimation?: UseTableProps<T>["disableAnimation"];
showSelectionCheckboxes: UseTableProps<T>["showSelectionCheckboxes"];
classNames?: SlotsToClasses<TableSlots>;
selectionMode: UseTableProps["selectionMode"];
selectionBehavior: UseTableProps["selectionBehavior"];
disabledBehavior: UseTableProps["disabledBehavior"];
showSelectionCheckboxes: UseTableProps["showSelectionCheckboxes"];
onRowAction?: UseTableProps["onRowAction"];
onCellAction?: UseTableProps["onCellAction"];
onRowAction?: UseTableProps<T>["onRowAction"];
onCellAction?: UseTableProps<T>["onCellAction"];
};
export function useTable<T extends object>(originalProps: UseTableProps<T>) {
@ -83,13 +101,15 @@ export function useTable<T extends object>(originalProps: UseTableProps<T>) {
as,
children,
className,
classNames,
removeWrapper = false,
selectionMode = "none",
selectionBehavior = "replace",
selectionBehavior = selectionMode === "none" ? null : "toggle",
disabledBehavior = "selection",
showSelectionCheckboxes = selectionMode === "multiple" && selectionBehavior !== "replace",
disableAnimation = false,
onRowAction,
onCellAction,
classNames,
...otherProps
} = props;
@ -105,24 +125,32 @@ export function useTable<T extends object>(originalProps: UseTableProps<T>) {
const {collection} = state;
const {gridProps} = useReactAriaTable(originalProps, state, domRef);
const {gridProps} = useReactAriaTable<T>(originalProps, state, domRef);
const isSelectable = selectionMode !== "none";
const isMultiSelectable = selectionMode === "multiple";
const slots = useMemo(
() =>
table({
...variantProps,
isSelectable,
isMultiSelectable,
}),
[...Object.values(variantProps)],
[...Object.values(variantProps), isSelectable, isMultiSelectable],
);
const tableStyles = clsx(classNames?.table, className);
const baseStyles = clsx(classNames?.base, className);
const context = useMemo<ContextType<T>>(
() => ({
state,
slots,
isSelectable,
color: originalProps?.color,
collection,
classNames,
disableAnimation,
selectionMode,
selectionBehavior,
disabledBehavior,
@ -134,6 +162,9 @@ export function useTable<T extends object>(originalProps: UseTableProps<T>) {
slots,
state,
collection,
isSelectable,
disableAnimation,
originalProps?.color,
classNames,
selectionMode,
selectionBehavior,
@ -144,18 +175,31 @@ export function useTable<T extends object>(originalProps: UseTableProps<T>) {
],
);
const getBaseProps: PropGetter = (props) => ({
...props,
className: slots.base({class: props?.className}),
});
const getBaseProps: PropGetter = useCallback(
(props) => ({
...props,
className: slots.base({class: clsx(baseStyles, props?.className)}),
}),
[baseStyles, slots],
);
const getTableProps: PropGetter = (props) => ({
...mergeProps(gridProps, otherProps, props),
...mergeProps(gridProps, filterDOMProps(otherProps, {labelable: true}), props),
ref: domRef,
className: slots.base({class: clsx(tableStyles, props?.className)}),
className: slots.table({class: clsx(classNames?.table, props?.className)}),
});
return {Component, children, context, getBaseProps, getTableProps};
return {
Component,
children,
state,
collection,
context,
removeWrapper,
selectionMode,
getBaseProps,
getTableProps,
};
}
export type UseTableReturn = ReturnType<typeof useTable>;

View File

@ -1,8 +1,12 @@
import React from "react";
import React, {Key} from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {table} from "@nextui-org/theme";
import {User} from "@nextui-org/user";
import {Chip, ChipProps} from "@nextui-org/chip";
import {Tooltip} from "@nextui-org/tooltip";
import {EditIcon, DeleteIcon, EyeIcon} from "@nextui-org/shared-icons";
import {Table, TableProps} from "../src";
import {Table, TableHeader, TableColumn, TableBody, TableCell, TableRow, TableProps} from "../src";
export default {
title: "Components/Table",
@ -14,33 +18,352 @@ export default {
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
},
},
layout: {
control: {
type: "select",
options: ["auto", "fixed"],
},
},
radius: {
control: {
type: "select",
options: ["none", "base", "sm", "md", "lg", "xl", "full"],
},
},
size: {
shadow: {
control: {
type: "select",
options: ["xs", "sm", "md", "lg", "xl"],
options: ["none", "sm", "md", "lg", "xl", "2xl", "inner"],
},
},
isDisabled: {
selectionMode: {
control: {
type: "select",
options: ["none", "single", "multiple"],
},
},
isStriped: {
control: {
type: "boolean",
},
},
},
decorators: [
(Story) => (
<div className="flex items-center justify-center w-screen h-screen">
<Story />
</div>
),
],
} as ComponentMeta<typeof Table>;
const defaultProps = {
...table.defaultVariants,
className: "max-w-lg",
};
const Template: ComponentStory<typeof Table> = (args: TableProps) => <Table {...args} />;
const rows = [
{
key: "1",
name: "Tony Reichert",
role: "CEO",
status: "Active",
},
{
key: "2",
name: "Zoey Lang",
role: "Technical Lead",
status: "Paused",
},
{
key: "3",
name: "Jane Fisher",
role: "Senior Developer",
status: "Active",
},
{
key: "4",
name: "William Howard",
role: "Community Manager",
status: "Vacation",
},
];
export const Default = Template.bind({});
Default.args = {
const columns = [
{
key: "name",
label: "NAME",
},
{
key: "role",
label: "ROLE",
},
{
key: "status",
label: "STATUS",
},
];
const isObject = (target: unknown) => target && typeof target === "object";
const getKeyValue = (obj: any, key: Key) => {
if (!isObject(obj)) return obj;
if (obj instanceof Array) return [...obj];
return obj[key];
};
const StaticTemplate: ComponentStory<typeof Table> = (args: TableProps) => (
<Table aria-label="Example static collection table" {...args}>
<TableHeader>
<TableColumn>NAME</TableColumn>
<TableColumn>ROLE</TableColumn>
<TableColumn>STATUS</TableColumn>
</TableHeader>
<TableBody>
<TableRow key="1">
<TableCell>Tony Reichert</TableCell>
<TableCell>CEO</TableCell>
<TableCell>Active</TableCell>
</TableRow>
<TableRow key="2">
<TableCell>Zoey Lang</TableCell>
<TableCell>Technical Lead</TableCell>
<TableCell>Paused</TableCell>
</TableRow>
<TableRow key="3">
<TableCell>Jane Fisher</TableCell>
<TableCell>Senior Developer</TableCell>
<TableCell>Active</TableCell>
</TableRow>
<TableRow key="4">
<TableCell>William Howard</TableCell>
<TableCell>Community Manager</TableCell>
<TableCell>Vacation</TableCell>
</TableRow>
</TableBody>
</Table>
);
const DynamicTemplate: ComponentStory<typeof Table> = (args: TableProps) => (
<Table aria-label="Example table with dynamic content" {...args}>
<TableHeader columns={columns}>
{(column) => <TableColumn key={column.key}>{column.label}</TableColumn>}
</TableHeader>
<TableBody items={rows}>
{(item) => (
<TableRow key={item.key}>
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
);
const CustomCellTemplate: ComponentStory<typeof Table> = (args: TableProps) => {
const columns = [
{name: "NAME", uid: "name"},
{name: "ROLE", uid: "role"},
{name: "STATUS", uid: "status"},
{name: "ACTIONS", uid: "actions"},
];
const users = [
{
id: 1,
name: "Tony Reichert",
role: "CEO",
team: "Management",
status: "active",
age: "29",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
email: "tony.reichert@example.com",
},
{
id: 2,
name: "Zoey Lang",
role: "Technical Lead",
team: "Development",
status: "paused",
age: "25",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
email: "zoey.lang@example.com",
},
{
id: 3,
name: "Jane Fisher",
role: "Senior Developer",
team: "Development",
status: "active",
age: "22",
avatar: "https://i.pravatar.cc/150?u=a04258114e29026702d",
email: "jane.fisher@example.com",
},
{
id: 4,
name: "William Howard",
role: "Community Manager",
team: "Marketing",
status: "vacation",
age: "28",
avatar: "https://i.pravatar.cc/150?u=a048581f4e29026701d",
email: "william.howard@example.com",
},
{
id: 5,
name: "Kristen Copper",
role: "Sales Manager",
team: "Sales",
status: "active",
age: "24",
avatar: "https://i.pravatar.cc/150?u=a092581d4ef9026700d",
email: "kristen.cooper@example.com",
},
];
const statusColorMap: Record<string, ChipProps["color"]> = {
active: "success",
paused: "danger",
vacation: "warning",
};
const renderCell = React.useCallback((user, columnKey) => {
const cellValue = user[columnKey];
switch (columnKey) {
case "name":
return (
<User
avatarProps={{radius: "xl", src: user.avatar}}
description={user.email}
name={cellValue}
>
{user.email}
</User>
);
case "role":
return (
<div className="flex flex-col">
<p className="text-bold text-sm capitalize">{cellValue}</p>
<p className="text-bold text-sm capitalize text-neutral-400">{user.team}</p>
</div>
);
case "status":
return (
<Chip className="capitalize" color={statusColorMap[user.status]} size="sm" variant="flat">
{cellValue}
</Chip>
);
case "actions":
return (
<div className="relative flex items-center gap-2">
<Tooltip content="Details">
<span className="text-lg text-neutral-400 cursor-pointer active:opacity-50">
<EyeIcon />
</span>
</Tooltip>
<Tooltip content="Edit user">
<span className="text-lg text-neutral-400 cursor-pointer active:opacity-50">
<EditIcon />
</span>
</Tooltip>
<Tooltip color="danger" content="Delete user">
<span className="text-lg text-danger cursor-pointer active:opacity-50">
<DeleteIcon />
</span>
</Tooltip>
</div>
);
default:
return cellValue;
}
}, []);
return (
<Table aria-label="Example table with custom cells" {...args}>
<TableHeader columns={columns}>
{(column) => (
<TableColumn key={column.uid} align={column.uid === "actions" ? "center" : "start"}>
{column.name}
</TableColumn>
)}
</TableHeader>
<TableBody items={users}>
{(item) => (
<TableRow key={item.id}>
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
);
};
export const Static = StaticTemplate.bind({});
Static.args = {
...defaultProps,
};
export const Dynamic = DynamicTemplate.bind({});
Dynamic.args = {
...defaultProps,
};
export const CustomCells = CustomCellTemplate.bind({});
CustomCells.args = {
...defaultProps,
className: "max-w-3xl",
};
export const Striped = StaticTemplate.bind({});
Striped.args = {
...defaultProps,
isStriped: true,
};
export const RemoveWrapper = StaticTemplate.bind({});
RemoveWrapper.args = {
...defaultProps,
classNames: {
table: "max-w-lg",
},
removeWrapper: true,
};
export const SingleSelection = StaticTemplate.bind({});
SingleSelection.args = {
...defaultProps,
selectionMode: "single",
};
export const MultipleSelection = StaticTemplate.bind({});
MultipleSelection.args = {
...defaultProps,
selectionMode: "multiple",
color: "secondary",
};
export const DisabledKeys = StaticTemplate.bind({});
DisabledKeys.args = {
...defaultProps,
selectionMode: "multiple",
disabledKeys: ["2"],
color: "warning",
};
export const DisallowEmptySelection = StaticTemplate.bind({});
DisallowEmptySelection.args = {
...defaultProps,
disallowEmptySelection: true,
color: "primary",
defaultSelectedKeys: ["2"],
selectionMode: "multiple",
};
export const DisableAnimation = StaticTemplate.bind({});
DisableAnimation.args = {
...defaultProps,
selectionMode: "multiple",
color: "secondary",
disableAnimation: true,
};

View File

@ -78,22 +78,6 @@ export function useUser(props: UseUserProps) {
const baseStyles = clsx(classNames?.base, className);
const buttonStyles = useMemo(() => {
if (as !== "button") return "";
// reset button classNames
return [
"p-0",
"m-0",
"bg-none",
"radius-none",
"appearance-none",
"outline-none",
"border-none",
"cursor-pointer",
];
}, [as]);
const getUserProps = useCallback<PropGetter>(
() => ({
ref: domRef,
@ -101,7 +85,7 @@ export function useUser(props: UseUserProps) {
"data-focus-visible": dataAttr(isFocusVisible),
"data-focused": dataAttr(isFocused),
className: slots.base({
class: clsx(baseStyles, buttonStyles),
class: baseStyles,
}),
...mergeProps(otherProps, canBeFocused ? focusProps : {}),
}),

View File

@ -64,7 +64,9 @@
"@nextui-org/dropdown": "workspace:*",
"@nextui-org/image": "workspace:*",
"@nextui-org/modal": "workspace:*",
"@nextui-org/navbar": "workspace:*"
"@nextui-org/navbar": "workspace:*",
"@nextui-org/table": "workspace:*",
"@nextui-org/spacer": "workspace:*"
},
"peerDependencies": {
"react": ">=18",

View File

@ -24,3 +24,5 @@ export * from "@nextui-org/dropdown";
export * from "@nextui-org/image";
export * from "@nextui-org/modal";
export * from "@nextui-org/navbar";
export * from "@nextui-org/table";
export * from "@nextui-org/spacer";

View File

@ -53,7 +53,7 @@
"lodash.foreach": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.isempty": "^4.4.0",
"tailwind-variants": "^0.1.2",
"tailwind-variants": "^0.1.3",
"tailwindcss": "^3.2.7"
},
"peerDependencies": {

View File

@ -31,3 +31,4 @@ export * from "./image";
export * from "./modal";
export * from "./navbar";
export * from "./table";
export * from "./spacer";

View File

@ -0,0 +1,29 @@
import type {VariantProps} from "tailwind-variants";
import {tv} from "tailwind-variants";
/**
* Spacer wrapper **Tailwind Variants** component
*
* @example
*
* const styles = spacer()
*
* <span className={styles} />
*/
const spacer = tv({
base: "w-px h-px inline-block",
variants: {
isInline: {
true: "inline-block",
false: "block",
},
},
defaultVariants: {
isInline: false,
},
});
export type SpacerVariantProps = VariantProps<typeof spacer>;
export {spacer};

View File

@ -2,6 +2,15 @@ import type {VariantProps} from "tailwind-variants";
import {tv} from "tailwind-variants";
const focusRing = [
"data-[focus-visible=true]:outline-none",
"data-[focus-visible=true]:ring-2",
"data-[focus-visible=true]:ring-primary",
"data-[focus-visible=true]:ring-offset-2",
"data-[focus-visible=true]:ring-offset-background",
"data-[focus-visible=true]:dark:ring-offset-background-dark",
];
/**
* Table **Tailwind Variants** component
*
@ -39,17 +48,169 @@ import {tv} from "tailwind-variants";
*/
const table = tv({
slots: {
base: "",
table: "group",
base: "p-4 border border-neutral-100",
table: "",
thead: "",
tbody: "",
tr: "",
th: "",
td: "",
tr: ["group", "outline-none", ...focusRing],
th: [
"px-3",
"h-10",
"text-left",
"align-middle",
"bg-neutral-100",
"text-neutral-500",
"text-xs",
"font-semibold",
"first:rounded-l-lg",
"last:rounded-r-lg",
"outline-none",
...focusRing,
],
td: [
"py-2",
"px-3",
"relative",
"align-middle",
"whitespace-normal",
"text-base",
"font-normal",
"outline-none",
...focusRing,
// before content for selection
"before:content-['']",
"before:absolute",
"before:-z-[1]",
"before:inset-0",
"before:opacity-0",
"data-[selected=true]:before:opacity-100",
// disabled
"group-data-[disabled=true]:text-neutral-300",
],
tfoot: "",
},
variants: {},
defaultVariants: {},
variants: {
color: {
neutral: {
td:
"before:bg-neutral-200 dark:before:bg-neutral-100 data-[selected=true]:text-neutral-foreground",
},
primary: {
td: "before:bg-primary-50 data-[selected=true]:text-primary",
},
secondary: {
td: "before:bg-secondary-100 data-[selected=true]:text-secondary",
},
success: {
td: "before:bg-success-50 data-[selected=true]:text-success",
},
warning: {
td: "before:bg-warning-50 data-[selected=true]:text-warning",
},
danger: {
td: "before:bg-danger-50 data-[selected=true]:text-danger",
},
},
layout: {
auto: {
table: "table-auto",
},
fixed: {
table: "table-fixed",
},
},
radius: {
none: {
base: "rounded-none",
},
base: {
base: "rounded",
},
sm: {
base: "rounded-sm",
},
md: {
base: "rounded-md",
},
lg: {
base: "rounded-lg",
},
xl: {
base: "rounded-xl",
},
"2xl": {
base: "rounded-2xl",
},
},
shadow: {
none: {
base: "shadow-none",
},
sm: {
base: "shadow-sm",
},
md: {
base: "shadow-md",
},
lg: {
base: "shadow-lg",
},
xl: {
base: "shadow-xl",
},
"2xl": {
base: "shadow-2xl",
},
inner: {
base: "shadow-inner",
},
},
isStriped: {
true: {
td: [
"group-data-[odd=true]:before:bg-neutral-100",
"group-data-[odd=true]:before:opacity-100",
],
},
},
isSelectable: {
true: {
tr: "cursor-default",
td: ["group-data-[hover=true]:before:opacity-70"],
},
},
isMultiSelectable: {
true: {
td: [
// first
"group-data-[first=true]:first:before:rounded-tl-lg",
"group-data-[first=true]:last:before:rounded-tr-lg",
// middle
"group-data-[middle=true]:before:rounded-none",
// last
"group-data-[last=true]:first:before:rounded-bl-lg",
"group-data-[last=true]:last:before:rounded-br-lg",
],
},
false: {
td: ["first:before:rounded-l-lg", "last:before:rounded-r-lg"],
},
},
fullWidth: {
true: {
base: "w-full",
table: "w-full",
},
},
},
defaultVariants: {
layout: "auto",
shadow: "lg",
radius: "xl",
color: "neutral",
isStriped: false,
fullWidth: true,
},
});
export type TableVariantProps = VariantProps<typeof table>;

View File

@ -28,7 +28,7 @@ const user = tv({
],
wrapper: "inline-flex flex-col items-start",
name: "text-sm text-foreground dark:text-foreground-dark",
description: "text-xs text-neutral-500",
description: "text-xs text-neutral-400",
},
});

View File

@ -55,19 +55,19 @@ h3 {
color: #bcbcbc;
}
.dark tbody td {
.dark #docs-root tbody td {
background: #161616 !important;
color: #bcbcbc !important;
}
.dark tbody tr:first-child td:first-child {
.dark #docs-root tbody tr:first-child td:first-child {
border-top-left-radius: 0 !important;
}
.dark tbody tr:first-child td:last-child {
.dark #docs-root tbody tr:first-child td:last-child {
border-top-right-radius: 0 !important;
}
.dark tbody tr:not(:first-child) {
.dark #docs-root tbody tr:not(:first-child) {
border-top: 1px solid #292929 !important;
}

View File

@ -0,0 +1,50 @@
import {IconSvgProps} from "./types";
export const DeleteIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 20 20"
width="1em"
{...props}
>
<path
d="M17.5 4.98332C14.725 4.70832 11.9333 4.56665 9.15 4.56665C7.5 4.56665 5.85 4.64998 4.2 4.81665L2.5 4.98332"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
<path
d="M7.08331 4.14169L7.26665 3.05002C7.39998 2.25835 7.49998 1.66669 8.90831 1.66669H11.0916C12.5 1.66669 12.6083 2.29169 12.7333 3.05835L12.9166 4.14169"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
<path
d="M15.7084 7.61664L15.1667 16.0083C15.075 17.3166 15 18.3333 12.675 18.3333H7.32502C5.00002 18.3333 4.92502 17.3166 4.83335 16.0083L4.29169 7.61664"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
<path
d="M8.60834 13.75H11.3833"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
<path
d="M7.91669 10.4167H12.0834"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
</svg>
);

View File

@ -0,0 +1,39 @@
import {IconSvgProps} from "./types";
export const EditIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 20 20"
width="1em"
{...props}
>
<path
d="M11.05 3.00002L4.20835 10.2417C3.95002 10.5167 3.70002 11.0584 3.65002 11.4334L3.34169 14.1334C3.23335 15.1084 3.93335 15.775 4.90002 15.6084L7.58335 15.15C7.95835 15.0834 8.48335 14.8084 8.74168 14.525L15.5834 7.28335C16.7667 6.03335 17.3 4.60835 15.4583 2.86668C13.625 1.14168 12.2334 1.75002 11.05 3.00002Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
strokeWidth={1.5}
/>
<path
d="M9.90833 4.20831C10.2667 6.50831 12.1333 8.26665 14.45 8.49998"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
strokeWidth={1.5}
/>
<path
d="M2.5 18.3333H17.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
strokeWidth={1.5}
/>
</svg>
);

View File

@ -0,0 +1,29 @@
import {IconSvgProps} from "./types";
export const EyeIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 20 20"
width="1em"
{...props}
>
<path
d="M12.9833 10C12.9833 11.65 11.65 12.9833 10 12.9833C8.35 12.9833 7.01666 11.65 7.01666 10C7.01666 8.35 8.35 7.01666 10 7.01666C11.65 7.01666 12.9833 8.35 12.9833 10Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
<path
d="M9.99999 16.8916C12.9417 16.8916 15.6833 15.1583 17.5917 12.1583C18.3417 10.9833 18.3417 9.00831 17.5917 7.83331C15.6833 4.83331 12.9417 3.09998 9.99999 3.09998C7.05833 3.09998 4.31666 4.83331 2.40833 7.83331C1.65833 9.00831 1.65833 10.9833 2.40833 12.1583C4.31666 15.1583 7.05833 16.8916 9.99999 16.8916Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
</svg>
);

View File

@ -24,3 +24,6 @@ export * from "./eye-slash-filled";
export * from "./search";
export * from "./bulk";
export * from "./lock-filled";
export * from "./edit";
export * from "./delete";
export * from "./eye";

509
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff