diff --git a/content/docs/typography/list.mdx b/content/docs/typography/list.mdx new file mode 100644 index 0000000000..a927f07940 --- /dev/null +++ b/content/docs/typography/list.mdx @@ -0,0 +1,46 @@ +--- +title: React Lists - Flowbite +description: Use the list component to show an unordred or ordered list of items based on multiple styles, layouts, and variants built with Tailwind CSS and Flowbite +--- + +Get started with a collection of list components built with Tailwind CSS for ordered and unordered lists with bullets, numbers, or icons and other styles and layouts to show a list of items inside an article or throughout your web page. + +Start using the list component by first importing it from Flowbite React: + +```jsx +import { List } from 'flowbite-react'; +``` + +## Default list + +Use this example to create a default unordered list of items using the `List` component with `List.Item` child components inside of it. + + + +## Nested + +Use this example to nested another list of items inside the parent list element. + + + +## Unstyled + +Use the `unstyled` prop to disable the list style bullets or numbers. + + + +## Ordered list + +Use the `ordered` prop tag to create an ordered list of items with numbers. + + + +## Theme + +To learn more about how to customize the appearance of components, please see the [Theme docs](/docs/customize/theme). + + + +## References + +- [Flowbite List](https://flowbite.com/docs/typography/list/) diff --git a/data/components.tsx b/data/components.tsx index c3d0bcfe83..d1c601003c 100644 --- a/data/components.tsx +++ b/data/components.tsx @@ -343,13 +343,13 @@ export const COMPONENTS_DATA: Component[] = [ // link: `/docs/typography/images`, // classes: 'w-64' // }, - // { - // name: 'List', - // image: '/images/components/list.svg', - // imageDark: '/images/components/list-dark.svg', - // link: `/docs/typography/lists`, - // classes: 'w-64' - // }, + { + name: 'List', + image: '/images/components/list.svg', + imageDark: '/images/components/list-dark.svg', + link: `/docs/typography/list`, + classes: 'w-64' + }, // { // name: 'Link', // image: '/images/components/link.svg', diff --git a/examples/index.ts b/examples/index.ts index a24c763b89..504aaea506 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -16,6 +16,7 @@ export * as footer from './footer'; export * as forms from './forms'; export * as kbd from './kbd'; export * as listGroup from './listGroup'; +export * as list from './list'; export * as modal from './modal'; export * as navbar from './navbar'; export * as pagination from './pagination'; diff --git a/examples/list/index.ts b/examples/list/index.ts new file mode 100644 index 0000000000..7688903704 --- /dev/null +++ b/examples/list/index.ts @@ -0,0 +1,4 @@ +export { root } from './list.root'; +export { unstyled } from './list.unstyled'; +export { nested } from './list.nested'; +export { ordered } from './list.ordered'; diff --git a/examples/list/list.nested.tsx b/examples/list/list.nested.tsx new file mode 100644 index 0000000000..22e448f5bc --- /dev/null +++ b/examples/list/list.nested.tsx @@ -0,0 +1,87 @@ +import { type CodeData } from '~/components/code-demo'; +import { List, ListItem } from '~/src'; + +const code = ` +'use client'; + +import { List } from 'flowbite-react'; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} +`; + +const codeRSC = ` +import { List, ListItem } from 'flowbite-react'; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} +`; + +function Component() { + return ( + <> + + + List item one + + You might feel like you are being really "organized" o + Nested navigation in UIs is a bad idea too, keep things as flat as possible. + Nesting tons of folders in your source code is also not helpful. + + + + List item two + + I'm not sure if we'll bother styling more than two levels deep. + Two is already too much, three is guaranteed to be a bad idea. + If you nest four levels deep you belong in prison. + + + + List item three + + Again please don't nest lists if you want + Nobody wants to look at this. + I'm upset that we even have to bother styling this. + + + + + ); +} + +export const nested: CodeData = { + type: 'single', + code: [ + { + fileName: 'client', + language: 'tsx', + code, + }, + { + fileName: 'server', + language: 'tsx', + code: codeRSC, + }, + ], + githubSlug: 'list/list.nested.tsx', + component: , +}; diff --git a/examples/list/list.ordered.tsx b/examples/list/list.ordered.tsx new file mode 100644 index 0000000000..8e938432e1 --- /dev/null +++ b/examples/list/list.ordered.tsx @@ -0,0 +1,66 @@ +import { type CodeData } from '~/components/code-demo'; +import { List, ListItem } from '~/src'; + +const code = ` +'use client'; + +import { List } from 'flowbite-react'; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} +`; + +const codeRSC = ` +import { List, ListItem } from 'flowbite-react'; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} +`; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} + +export const ordered: CodeData = { + type: 'single', + code: [ + { + fileName: 'client', + language: 'tsx', + code, + }, + { + fileName: 'server', + language: 'tsx', + code: codeRSC, + }, + ], + githubSlug: 'list/list.ordered.tsx', + component: , +}; diff --git a/examples/list/list.root.tsx b/examples/list/list.root.tsx new file mode 100644 index 0000000000..17decbad42 --- /dev/null +++ b/examples/list/list.root.tsx @@ -0,0 +1,66 @@ +import { type CodeData } from '~/components/code-demo'; +import { List, ListItem } from '~/src'; + +const code = ` +'use client'; + +import { List } from 'flowbite-react'; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} +`; + +const codeRSC = ` +import { List, ListItem } from 'flowbite-react'; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} +`; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} + +export const root: CodeData = { + type: 'single', + code: [ + { + fileName: 'client', + language: 'tsx', + code, + }, + { + fileName: 'server', + language: 'tsx', + code: codeRSC, + }, + ], + githubSlug: 'list/list.root.tsx', + component: , +}; diff --git a/examples/list/list.unstyled.tsx b/examples/list/list.unstyled.tsx new file mode 100644 index 0000000000..c2fc58f81f --- /dev/null +++ b/examples/list/list.unstyled.tsx @@ -0,0 +1,66 @@ +import { type CodeData } from '~/components/code-demo'; +import { List, ListItem } from '~/src'; + +const code = ` +'use client'; + +import { List } from 'flowbite-react'; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} +`; + +const codeRSC = ` +import { List, ListItem } from 'flowbite-react'; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} +`; + +function Component() { + return ( + <> + + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + + ); +} + +export const unstyled: CodeData = { + type: 'single', + code: [ + { + fileName: 'client', + language: 'tsx', + code, + }, + { + fileName: 'server', + language: 'tsx', + code: codeRSC, + }, + ], + githubSlug: 'list/list.unstyled.tsx', + component: , +}; diff --git a/public/images/components/list-dark.svg b/public/images/components/list-dark.svg new file mode 100644 index 0000000000..41c16691fd --- /dev/null +++ b/public/images/components/list-dark.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/components/list.svg b/public/images/components/list.svg new file mode 100644 index 0000000000..e900ccc04e --- /dev/null +++ b/public/images/components/list.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/Flowbite/FlowbiteTheme.ts b/src/components/Flowbite/FlowbiteTheme.ts index 05d3fa0054..5578fac31a 100644 --- a/src/components/Flowbite/FlowbiteTheme.ts +++ b/src/components/Flowbite/FlowbiteTheme.ts @@ -19,6 +19,7 @@ import type { FlowbiteHelperTextTheme } from '../HelperText'; import type { FlowbiteKbdTheme } from '../Kbd'; import type { FlowbiteLabelTheme } from '../Label'; import type { FlowbiteListGroupTheme } from '../ListGroup'; +import type { FlowbiteListTheme } from '../List'; import type { FlowbiteModalTheme } from '../Modal'; import type { FlowbiteNavbarTheme } from '../Navbar'; import type { FlowbitePaginationTheme } from '../Pagination'; @@ -56,6 +57,7 @@ export interface FlowbiteTheme { footer: FlowbiteFooterTheme; kbd: FlowbiteKbdTheme; listGroup: FlowbiteListGroupTheme; + list: FlowbiteListTheme; modal: FlowbiteModalTheme; navbar: FlowbiteNavbarTheme; rating: FlowbiteRatingTheme; diff --git a/src/components/List/List.spec.tsx b/src/components/List/List.spec.tsx new file mode 100644 index 0000000000..5015b05159 --- /dev/null +++ b/src/components/List/List.spec.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Flowbite } from '../../'; +import { List } from './List'; + +describe('Components / List group', () => { + describe('Theme', () => { + it('should use custom classes', () => { + const theme = { + list: { + root: { + base: 'asd', + }, + }, + }; + + render( + + + , + ), + expect(listGroup()).toHaveClass('asd'); + }); + }); +}); + +const TestList = (): JSX.Element => { + return ( + + Settings + Messages + Download + + ); +}; + +const listGroup = () => screen.getByRole('list'); \ No newline at end of file diff --git a/src/components/List/List.stories.tsx b/src/components/List/List.stories.tsx new file mode 100644 index 0000000000..a326dd993e --- /dev/null +++ b/src/components/List/List.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryFn } from '@storybook/react'; +import type { ListProps } from './List'; +import { List } from './List'; + +export default { + title: 'Components/List', + component: List, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const DefaultList = Template.bind({}); +DefaultList.storyName = 'Default'; +DefaultList.args = { + children: ( + <> + At least 10 characters (and up to 100 characters) + At least one lowercase character + Inclusion of at least one special character, e.g., ! @ # ? + + ), +}; \ No newline at end of file diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx new file mode 100644 index 0000000000..38975d93fc --- /dev/null +++ b/src/components/List/List.tsx @@ -0,0 +1,58 @@ +import type { ComponentProps, FC, PropsWithChildren } from 'react'; +import { twMerge } from 'tailwind-merge'; +import type { FlowbiteStateColors } from '../..'; +import { mergeDeep } from '../../helpers/merge-deep'; +import { getTheme } from '../../theme-store'; +import type { DeepPartial } from '../../types'; +import { ListItem } from './ListItem'; + +export interface FlowbiteListTheme { + root: FlowbiteListRootTheme; +} + +export interface FlowbiteListRootTheme { + base: string; + ordered: { + on: string; + off: string; + }; + unstyled: string; + nested: string; +} + +export interface ListColors extends FlowbiteStateColors { + [key: string]: string; + default: string; +} + +export interface ListProps extends PropsWithChildren & ComponentProps<'ol'>> { + theme?: DeepPartial; + ordered?: boolean; + unstyled?: boolean; + nested?: boolean; +} + +const ListComponent: FC = ({ children, className, theme: customTheme = {}, ...props }) => { + const theme = mergeDeep(getTheme().list, customTheme); + const Component = props.ordered ? 'ol' : 'ul'; + + return ( + + {children} + + ); +}; + +ListComponent.displayName = 'List'; +ListItem.displayName = 'List.Item'; + +export const List = Object.assign(ListComponent, { Item: ListItem }); diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx new file mode 100644 index 0000000000..a4c817a7ff --- /dev/null +++ b/src/components/List/ListItem.tsx @@ -0,0 +1,20 @@ +import type { FC, PropsWithChildren } from 'react'; +import { twMerge } from 'tailwind-merge'; +import type { DeepPartial } from '../../types'; +import { getTheme } from '../../theme-store'; +import { mergeDeep } from '../../helpers/merge-deep'; + +export interface FlowbiteListItemTheme { + base: string; +} + +export interface ListItemProps extends PropsWithChildren { + theme?: DeepPartial; + className?: string; +} + +export const ListItem: FC = ({ children, className, theme: customTheme = {} }) => { + const theme = mergeDeep(getTheme().listGroup.item, customTheme); + + return
  • {children}
  • ; +}; \ No newline at end of file diff --git a/src/components/List/index.ts b/src/components/List/index.ts new file mode 100644 index 0000000000..a27d90b1b2 --- /dev/null +++ b/src/components/List/index.ts @@ -0,0 +1,4 @@ +export { List } from './List'; +export type { FlowbiteListRootTheme, FlowbiteListTheme, ListProps } from './List'; +export { ListItem } from './ListItem'; +export type { FlowbiteListItemTheme, ListItemProps } from './ListItem'; diff --git a/src/components/List/theme.ts b/src/components/List/theme.ts new file mode 100644 index 0000000000..421dc3209b --- /dev/null +++ b/src/components/List/theme.ts @@ -0,0 +1,13 @@ +import type { FlowbiteListTheme } from './List'; + +export const listTheme: FlowbiteListTheme = { + root: { + base: 'space-y-1 text-gray-500 list-inside dark:text-gray-400', + ordered: { + off: 'list-disc', + on: 'list-decimal', + }, + unstyled: 'list-none', + nested: 'ps-5 mt-2 space-y-1', + }, +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 85dbe05bb0..3a5f80280d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ export * from './components/HelperText'; export * from './components/Kbd'; export * from './components/Label'; export * from './components/ListGroup'; +export * from './components/List'; export * from './components/Modal'; export * from './components/Navbar'; export * from './components/Pagination'; diff --git a/src/theme.ts b/src/theme.ts index 18e4f0915c..d6ffd837c2 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -19,6 +19,7 @@ import { helperTextTheme } from './components/HelperText/theme'; import { kbdTheme } from './components/Kbd/theme'; import { labelTheme } from './components/Label/theme'; import { listGroupTheme } from './components/ListGroup/theme'; +import { listTheme } from './components/List/theme'; import { modalTheme } from './components/Modal/theme'; import { navbarTheme } from './components/Navbar/theme'; import { paginationTheme } from './components/Pagination/theme'; @@ -60,6 +61,7 @@ export const theme: FlowbiteTheme = { kbd: kbdTheme, label: labelTheme, listGroup: listGroupTheme, + list: listTheme, modal: modalTheme, navbar: navbarTheme, pagination: paginationTheme,