diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index cad2f823..d7377c93 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import type { Preview } from '@storybook/react';
-
+import '../src/tailwind.css';
/** @type { import('@storybook/react').Preview } */
-import '../dist/style.css';
+
const preview: Preview = {
parameters: {
controls: {
@@ -20,7 +20,7 @@ const preview: Preview = {
},
decorators: [
(Story) => (
-
+
),
diff --git a/README.md b/README.md
index 81ffbcb6..00889988 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Using Force UI as a dependency in package.json -
```json
"dependencies": {
- "@bsf/force-ui": "git+https://github.com/brainstormforce/force-ui#1.2.2"
+ "@bsf/force-ui": "git+https://github.com/brainstormforce/force-ui#1.3.0"
}
```
@@ -25,6 +25,12 @@ And run the following command to install the package -
npm install
```
+Or you can directly run the following command to install the package -
+
+```bash
+npm i -S @bsf/force-ui@git+https://github.com/brainstormforce/force-ui.git#1.3.0
+```
+
2. Once you install @bsf/force-ui you need to wrap your tailwind css configurations with the `withTW()` function coming from @bsf/force-ui/withTW.
diff --git a/changelog.txt b/changelog.txt
index 62b39fe6..b6356c0b 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,7 @@
+Version 1.3.0 - 16th December, 2024
+- New - Table component.
+- Fixed - Asterisk missing on required input label.
+
Version 1.2.2 - 4th December, 2024
- Improvement - Removed margin and added new props to the Line Chart component for customizability.
diff --git a/package.json b/package.json
index 28c9c5b9..a0f65682 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@bsf/force-ui",
- "version": "1.2.2",
+ "version": "1.3.0",
"description": "Library of components for the BSF project",
"main": "./dist/force-ui.js",
"module": "./dist/force-ui.js",
diff --git a/src/components/checkbox/checkbox.tsx b/src/components/checkbox/checkbox.tsx
index b3054fc9..1c6e547e 100644
--- a/src/components/checkbox/checkbox.tsx
+++ b/src/components/checkbox/checkbox.tsx
@@ -145,14 +145,14 @@ export const CheckboxComponent = (
return (
>
+ className={ cn( labelClasses[ size ] ) }
htmlFor={ inputId }
+ { ...( props?.required && { required: true } ) }
>
{ label }
-
+
);
}, [ label, size, inputId ] );
diff --git a/src/components/label/label.tsx b/src/components/label/label.tsx
index b9fd168e..7a261cad 100644
--- a/src/components/label/label.tsx
+++ b/src/components/label/label.tsx
@@ -17,7 +17,7 @@ export interface LabelProps {
}
const Label = forwardRef(
- (
+
(
{
children = null,
tag: Tag = 'label',
@@ -26,7 +26,7 @@ const Label = forwardRef(
variant = 'neutral', // neutral, help, error, disabled
required = false,
...props
- }: LabelProps,
+ }: LabelProps & T,
ref: React.Ref
) => {
// Base classes. - Mandatory classes.
@@ -78,4 +78,7 @@ const Label = forwardRef(
}
);
-export default Label;
+export default Label as (
+ props: LabelProps & T,
+ ref: React.Ref
+) => React.ReactNode;
diff --git a/src/components/pagination/pagination.tsx b/src/components/pagination/pagination.tsx
index ef8c61b2..824f15d3 100644
--- a/src/components/pagination/pagination.tsx
+++ b/src/components/pagination/pagination.tsx
@@ -1,10 +1,4 @@
-import {
- createContext,
- useContext,
- forwardRef,
- type ReactNode,
- type ElementType,
-} from 'react';
+import { createContext, useContext, forwardRef, type ReactNode } from 'react';
import { cn, callAll } from '@/utilities/functions';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { disabledClassNames, sizeClassNames } from './component-style';
@@ -33,14 +27,14 @@ export interface PaginationProps extends PaginationCommonProps {
disabled?: boolean;
}
-export interface PaginationItemProps extends PaginationCommonProps {
+export interface PaginationItemProps
+ extends PaginationCommonProps,
+ PaginationButtonProps {
/** Marks the pagination item as active. */
isActive?: boolean;
}
export interface PaginationButtonProps extends PaginationCommonProps {
- /** The element type of the pagination button. */
- as?: ElementType;
/** Marks the button as active. */
isActive?: boolean;
/** Disables the button. */
diff --git a/src/components/table/index.ts b/src/components/table/index.ts
new file mode 100644
index 00000000..a62247d8
--- /dev/null
+++ b/src/components/table/index.ts
@@ -0,0 +1 @@
+export { default } from './table';
diff --git a/src/components/table/table.stories.tsx b/src/components/table/table.stories.tsx
new file mode 100644
index 00000000..9723f129
--- /dev/null
+++ b/src/components/table/table.stories.tsx
@@ -0,0 +1,286 @@
+import { useState, type ComponentType } from 'react';
+import { StoryFn, Meta } from '@storybook/react';
+import Table from './table';
+import { Button, Container, Pagination, Tooltip } from '@/components';
+import { Edit, Trash } from 'lucide-react';
+
+const meta = {
+ title: 'Atoms/Table',
+ component: Table,
+ subcomponents: {
+ 'Table.Head': Table.Head,
+ 'Table.HeadCell': Table.HeadCell,
+ 'Table.Body': Table.Body,
+ 'Table.Row': Table.Row,
+ 'Table.Cell': Table.Cell,
+ 'Table.Footer': Table.Footer,
+ } as Record>,
+ tags: [ 'autodocs' ],
+} satisfies Meta;
+export default meta;
+
+type Story = StoryFn;
+
+const data = [
+ {
+ name: 'John Doe',
+ age: 30,
+ email: 'KXk7g@example.com',
+ phone: '1234567890',
+ },
+ {
+ name: 'Jane Doe',
+ age: 25,
+ email: 'oXHsO@example.com',
+ phone: '1234567890',
+ },
+ {
+ name: 'Bob Smith',
+ age: 40,
+ email: 'oXHsO@example.com',
+ phone: '1234567890',
+ },
+ {
+ name: 'Alice Johnson',
+ age: 35,
+ email: 'oXHsO@example.com',
+ phone: '1234567890',
+ },
+];
+
+const Template: Story = ( { checkboxSelection } ) => {
+ const [ selected, setSelected ] = useState( [] );
+
+ const handleCheckboxChange = (
+ checked: boolean,
+ value: ( typeof data )[number]
+ ) => {
+ if ( checked ) {
+ setSelected( [ ...selected, value.name ] );
+ } else {
+ setSelected( selected.filter( ( item: string ) => item !== value.name ) );
+ }
+ };
+
+ const toggleSelectAll = ( checked: boolean ) => {
+ if ( checked ) {
+ setSelected( data.map( ( item ) => item.name ) );
+ } else {
+ setSelected( [] );
+ }
+ };
+
+ return (
+
+ 0 }
+ onChangeSelection={ toggleSelectAll }
+ indeterminate={
+ selected.length > 0 && selected.length < data.length
+ }
+ >
+ Name
+ Age
+ Email
+ Phone
+
+ Actions
+
+
+
+ { data.map( ( item, index ) => (
+
+ { item.name }
+ { item.age }
+ { item.email }
+ { item.phone }
+
+
+
+ }
+ size="xs"
+ className="text-icon-secondary hover:text-icon-primary"
+ aria-label="Delete"
+ />
+
+
+ }
+ size="xs"
+ className="text-icon-secondary hover:text-icon-primary"
+ aria-label="Edit"
+ />
+
+
+
+
+ ) ) }
+
+
+
+
+ Page 1 out of 9
+
+
+
+
+ 1
+ 2
+ 3
+
+ 7
+ 8
+ 9
+
+
+
+
+
+
+ );
+};
+
+export const Default = Template.bind( {} );
+Default.args = {
+ checkboxSelection: false,
+};
+
+export const WithCheckboxSelection = Template.bind( {} );
+WithCheckboxSelection.args = {
+ checkboxSelection: true,
+};
+WithCheckboxSelection.parameters = {
+ docs: {
+ source: {
+ code: `
+import { useState } from 'react';
+import { Table, Button, Pagination, Tooltip } from '@bsf/force-ui';
+import { Edit, Trash } from 'lucide-react';
+
+const data = [
+ {
+ name: 'John Doe',
+ age: 30,
+ email: 'KXk7g@example.com',
+ phone: '1234567890',
+ },
+ {
+ name: 'Jane Doe',
+ age: 25,
+ email: 'oXHsO@example.com',
+ phone: '1234567890',
+ },
+ {
+ name: 'Bob Smith',
+ age: 40,
+ email: 'oXHsO@example.com',
+ phone: '1234567890',
+ },
+ {
+ name: 'Alice Johnson',
+ age: 35,
+ email: 'oXHsO@example.com',
+ phone: '1234567890',
+ },
+];
+
+const App = () => {
+ const [ selected, setSelected ] = useState( [] );
+
+ const handleCheckboxChange = ( checked, value ) => {
+ if ( checked ) {
+ setSelected( [ ...selected, value.name ] );
+ } else {
+ setSelected( selected.filter( ( item ) => item !== value.name ) );
+ }
+ };
+
+ const toggleSelectAll = ( checked ) => {
+ if ( checked ) {
+ setSelected( data.map( ( item ) => item.name ) );
+ } else {
+ setSelected( [] );
+ }
+ };
+
+ return (
+
+ 0 }
+ onChangeSelection={ toggleSelectAll }
+ indeterminate={ selected.length > 0 && selected.length < data.length }
+ >
+ Name
+ Age
+ Email
+ Phone
+
+ Actions
+
+
+
+ { data.map( ( item, index ) => (
+
+ { item.name }
+ { item.age }
+ { item.email }
+ { item.phone }
+
+
+
+ } size="xs" className="text-icon-secondary hover:text-icon-primary" aria-label="Delete" />
+
+
+ } size="xs" className="text-icon-secondary hover:text-icon-primary" aria-label="Edit" />
+
+
+
+
+ ) ) }
+
+
+
+
+ Page 1 out of 9
+
+
+
+
+ 1
+ 2
+ 3
+
+ 7
+ 8
+ 9
+
+
+
+
+
+
+ );
+}
+ `,
+ },
+ },
+};
diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx
new file mode 100644
index 00000000..24a85ddd
--- /dev/null
+++ b/src/components/table/table.tsx
@@ -0,0 +1,425 @@
+import { cn } from '@/utilities/functions';
+import React, {
+ Children,
+ createContext,
+ useContext,
+ type ReactNode,
+} from 'react';
+import { Checkbox } from '@/components';
+
+/**
+ * Common props for all table components.
+ */
+export interface TableCommonProps {
+ /**
+ * Children to render within the component.
+ */
+ children?: ReactNode;
+ /**
+ * Class name to apply to the component.
+ */
+ className?: string;
+}
+
+/**
+ * Interface for table context.
+ */
+export interface TableContextType {
+ /**
+ * Set of indices for selected rows.
+ */
+ selectedRows?: Set;
+ /**
+ * Whether to show checkboxes for row selection.
+ */
+ checkboxSelection?: boolean;
+ /**
+ * On checkbox selection change.
+ */
+ onChangeSelection?: ( checked: boolean, value: T ) => void;
+}
+
+/**
+ * Interface for base table props.
+ */
+export interface BaseTableProps
+ extends TableCommonProps,
+ Omit, 'className' | 'children'> {
+ /**
+ * Child components to render within the table.
+ *
+ * @default undefined
+ */
+ children?: ReactNode;
+ /**
+ * Whether to show checkboxes for row selection.
+ */
+ checkboxSelection?: boolean;
+}
+
+/**
+ * Interface for table head props.
+ */
+export interface TableHeadProps
+ extends TableCommonProps,
+ Omit<
+ React.HTMLAttributes,
+ 'className' | 'children'
+ > {
+ /**
+ * Child components to render within the table head.
+ */
+ children?: ReactNode;
+ /**
+ * Whether any of the rows are selected.
+ */
+ selected?: boolean;
+
+ /**
+ * Whether the checkbox is indeterminate.
+ */
+ indeterminate?: boolean;
+
+ /**
+ * Whether the checkbox is disabled.
+ */
+ disabled?: boolean;
+
+ /**
+ * On checkbox change for bulk selection/deselection.
+ *
+ * @default undefined
+ */
+ onChangeSelection?: ( checked: boolean ) => void;
+}
+
+/**
+ * Interface for table head cell props.
+ */
+export interface TableHeadCellProps
+ extends TableCommonProps,
+ Omit<
+ React.HTMLAttributes,
+ 'className' | 'children'
+ > {
+ /**
+ * Content to display in the header cell.
+ */
+ children?: ReactNode;
+}
+
+/**
+ * Interface for table body props.
+ */
+export interface TableBodyProps
+ extends TableCommonProps,
+ Omit<
+ React.HTMLAttributes,
+ 'className' | 'children'
+ > {
+ /**
+ * Child components to render within the table body.
+ */
+ children?: ReactNode;
+}
+
+/**
+ * Interface for table row props.
+ */
+export interface TableRowProps
+ extends TableCommonProps,
+ Omit<
+ React.HTMLAttributes,
+ 'className' | 'children'
+ > {
+ /**
+ * Child components to render within the table row.
+ */
+ children?: ReactNode;
+ /**
+ * value of the row.
+ */
+ value?: T | undefined;
+ /**
+ * Whether the row is selected.
+ */
+ selected?: boolean;
+
+ /**
+ * On checkbox selection change.
+ */
+ onChangeSelection?: ( checked: boolean, value: T ) => void;
+
+ /**
+ * Whether the row is disabled.
+ */
+ disabled?: boolean;
+}
+
+/**
+ * Interface for table cell props.
+ */
+export interface TableCellProps
+ extends TableCommonProps,
+ Omit<
+ React.HTMLAttributes,
+ 'className' | 'children'
+ > {
+ /**
+ * Content to display in the table cell.
+ */
+ children?: ReactNode;
+}
+
+/**
+ * Interface for table footer props.
+ */
+export interface TableFooterProps
+ extends TableCommonProps,
+ Omit, 'className' | 'children'> {
+ /**
+ * Child components to render within the table footer.
+ */
+ children?: ReactNode;
+}
+
+const TableContext = createContext | undefined>(
+ undefined
+);
+
+const useTableContext = () => {
+ const context = useContext( TableContext );
+ if ( ! context ) {
+ throw new Error( 'Table components must be used within Table component' );
+ }
+ return context;
+};
+
+export const Table = ( {
+ children,
+ className,
+ checkboxSelection = false,
+ ...props
+}: BaseTableProps ) => {
+ const contextValue: TableContextType = {
+ checkboxSelection,
+ };
+
+ // Extract footer from children
+ const footer = Children.toArray( children ).find(
+ ( child ) => React.isValidElement( child ) && child.type === TableFooter
+ );
+ const restChildren = Children.toArray( children ).filter(
+ ( child ) => React.isValidElement( child ) && child.type !== TableFooter
+ );
+ return (
+ }
+ >
+
+
+ );
+};
+
+// Head Components
+export const TableHead: React.FC = ( {
+ children,
+ className,
+ selected,
+ onChangeSelection,
+ indeterminate,
+ disabled,
+ ...props
+} ) => {
+ const { checkboxSelection } = useTableContext();
+
+ const handleCheckboxChange = ( checked: boolean ) => {
+ if ( typeof onChangeSelection !== 'function' ) {
+ return;
+ }
+ onChangeSelection( checked );
+ };
+
+ return (
+
+
+ { checkboxSelection && (
+
+
+
+
+
+ ) }
+ { children }
+
+
+ );
+};
+
+export const TableHeadCell: React.FC = ( {
+ children,
+ className,
+ ...props
+} ) => {
+ return (
+
+ { children }
+
+ );
+};
+
+// Body Components
+export const TableBody: React.FC = ( {
+ children,
+ className,
+ ...props
+} ) => {
+ return (
+
+ { children }
+
+ );
+};
+
+export const TableRow = ( {
+ children,
+ selected,
+ value,
+ className,
+ onChangeSelection,
+ ...props
+}: TableRowProps ) => {
+ const { checkboxSelection } = useTableContext();
+
+ const handleCheckboxChange = ( checked: boolean ) => {
+ if ( typeof onChangeSelection !== 'function' ) {
+ return;
+ }
+ onChangeSelection( checked, value as T );
+ };
+
+ return (
+
+ { checkboxSelection && (
+
+
+
+
+
+ ) }
+ { children }
+
+ );
+};
+
+export const TableCell: React.FC = ( {
+ children,
+ className,
+ ...props
+} ) => {
+ return (
+
+ { children }
+
+ );
+};
+
+// Table Footer
+export const TableFooter: React.FC = ( {
+ children,
+ className,
+ ...props
+} ) => {
+ const { checkboxSelection } = useTableContext();
+ return (
+
+ { children }
+
+ );
+};
+
+// Update display name
+Table.displayName = 'Table';
+TableHead.displayName = 'Table.Head';
+TableHeadCell.displayName = 'Table.HeadCell';
+TableBody.displayName = 'Table.Body';
+TableRow.displayName = 'Table.Row';
+TableCell.displayName = 'Table.Cell';
+TableFooter.displayName = 'Table.Footer';
+
+// Assign compound components
+Table.Head = TableHead;
+Table.HeadCell = TableHeadCell;
+Table.Body = TableBody;
+Table.Row = TableRow;
+Table.Cell = TableCell;
+Table.Footer = TableFooter;
+
+export default Table;
diff --git a/src/theme/default-config.js b/src/theme/default-config.js
index aa5153cd..c797dba2 100644
--- a/src/theme/default-config.js
+++ b/src/theme/default-config.js
@@ -197,6 +197,8 @@ const defaultTheme = {
tiny: '0.625rem',
},
spacing: {
+ 4.5: '1.125rem', // 18px
+ 5.5: '1.375rem', // 22px
120: '30rem', // 480px
95: '23.75rem', // 380px
141.5: '35.375rem', // 566px
diff --git a/version.json b/version.json
index 7fd3c879..675a1b15 100644
--- a/version.json
+++ b/version.json
@@ -1,3 +1,3 @@
{
- "force-ui": "1.2.2"
+ "force-ui": "1.3.0"
}