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/changelog.txt b/changelog.txt index 62b39fe6..4a314439 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +Version x.x.x - x x, x +- 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/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 (
+ ); }, [ 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/table/index.ts b/src/components/table/index.ts new file mode 100644 index 00000000..1a876cfc --- /dev/null +++ b/src/components/table/index.ts @@ -0,0 +1 @@ +export { default as Table } 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 } + + + +
+ ); +}; + +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 } + +
+ +
+
+
+ ) ) } +
+ +
+ + 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..c37cc33d --- /dev/null +++ b/src/components/table/table.tsx @@ -0,0 +1,421 @@ +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 ( + } + > +
+ + { restChildren } +
+ { footer } +
+
+ ); +}; + +// 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