Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: flowbite theme context provider #61

Merged
merged 18 commits into from
May 8, 2022
4 changes: 2 additions & 2 deletions src/docs/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export const Root: FC = () => {
<Sidebar collapsed={collapsed}>
<Sidebar.Items>
<Sidebar.ItemGroup>
{routes.map(({ href, icon, title }) => (
<Sidebar.Item key={title} href={href} icon={icon}>
{routes.map(({ href, icon, title }, key) => (
<Sidebar.Item key={key} href={href} icon={icon}>
{title}
</Sidebar.Item>
))}
Expand Down
7 changes: 5 additions & 2 deletions src/docs/pages/DemoPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ SyntaxHighlighter.registerLanguage('tsx', tsx);

export type CodeExample = {
title: string;
content?: ReactNode;
code: ReactNode;
showCode?: boolean;
codeClassName?: string;
codeStringifierOptions?: Options;
};
Expand All @@ -24,11 +26,12 @@ export const DemoPage: FC<DemoPageProps> = ({ examples }) => {

return (
<div className="mx-auto flex max-w-4xl flex-col gap-8 dark:text-white">
{examples.map(({ title, code, codeClassName, codeStringifierOptions }, index) => (
{examples.map(({ title, content, code, showCode = true, codeClassName, codeStringifierOptions }, index) => (
<div key={index} className="flex flex-col gap-2">
<span className="text-2xl font-bold">{title}</span>
{content && <div className="py-4">{content}</div>}
<Card className={codeClassName}>
{code}
{showCode && code}
<SyntaxHighlighterFix language="tsx" style={dracula}>
{reactElementToJSXString(code, {
showFunctions: true,
Expand Down
64 changes: 64 additions & 0 deletions src/docs/pages/ThemePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FC } from 'react';
import reactElementToJSXString from 'react-element-to-jsx-string';
import { HiInformationCircle } from 'react-icons/hi';
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Alert, Card, DarkThemeToggle } from '../../lib';
import { Flowbite } from '../../lib/components';

const ThemePage: FC = () => {
const theme = { config: { button: { base: 'py-5 px-5' } } };

return (
<div className="mx-auto flex max-w-4xl flex-col gap-8 dark:text-white">
<div className="flex flex-col gap-2">
<span className="text-2xl font-bold">Theme</span>
<div className="py-4">
<Alert color="yellow" Icon={HiInformationCircle}>
This feature is highly experimental. In the future, it could be deprecated or even suffer several changes.
</Alert>
<p className="mt-4">
Sometimes you want to give your web application a little more personality and customize some aspects of
Flowbite. This is possible thanks to the support we offer for themes. To use our theme support, your
application needs to be surrounded by the Flowbite component, and you must tell this component which theme
you want to load for your application.
</p>
</div>
<Card>
<SyntaxHighlighter language="tsx" style={dracula}>
{reactElementToJSXString(<Flowbite theme={theme}>...</Flowbite>, {
showFunctions: true,
functionValue: (fn) => fn.name,
sortProps: false,
useBooleanShorthandSyntax: false,
useFragmentShortSyntax: false,
})}
</SyntaxHighlighter>
</Card>
</div>
<span className="text-xl font-bold">Switch to dark theme</span>
<p>
Since the Flowbite component creates and context to manage the theme, it also enables your application to use
the <strong>DarkThemeToggle</strong> component.
</p>
<Card>
<SyntaxHighlighter language="tsx" style={dracula}>
{reactElementToJSXString(
<Flowbite>
<DarkThemeToggle />
</Flowbite>,
{
showFunctions: true,
functionValue: (fn) => fn.name,
sortProps: false,
useBooleanShorthandSyntax: false,
useFragmentShortSyntax: false,
},
)}
</SyntaxHighlighter>
</Card>
</div>
);
};

export default ThemePage;
14 changes: 13 additions & 1 deletion src/docs/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
HiMinus,
} from 'react-icons/hi';
import { AiOutlineLoading3Quarters } from 'react-icons/ai';
import { MdColorLens, MdTab } from 'react-icons/md';

import AccordionPage from './pages/AccordionPage';
import AlertsPage from './pages/AlertsPage';
Expand All @@ -50,7 +51,7 @@ import ToastPage from './pages/ToastPage';
import TooltipsPage from './pages/TooltipsPage';
import SidebarPage from './pages/SidebarPage';
import TabsPage from './pages/TabsPage';
import { MdTab } from 'react-icons/md';
import ThemePage from './pages/ThemePage';

export type ComponentCardItem = {
className: string;
Expand Down Expand Up @@ -312,6 +313,17 @@ export const routes: RouteProps[] = [
images: { light: 'tabs-light.svg', dark: 'tabs-dark.svg' },
},
},
{
title: 'Theme',
icon: MdColorLens,
href: '/theme',
component: <ThemePage />,
group: false,
card: {
className: 'w-64',
images: { light: 'tabs-light.svg', dark: 'tabs-dark.svg' },
},
},
{
title: 'Toast',
icon: BiNotification,
Expand Down
9 changes: 6 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { createRoot } from 'react-dom/client';
import { Root } from './docs/Root';
import { BrowserRouter } from 'react-router-dom';
import { Flowbite } from './lib/components';

import './index.css';
import 'flowbite';

const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<BrowserRouter>
<Root />
</BrowserRouter>,
<Flowbite>
<BrowserRouter>
<Root />
</BrowserRouter>
</Flowbite>,
);
14 changes: 11 additions & 3 deletions src/lib/components/Accordion/AccordionContent.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { ComponentProps, FC } from 'react';
import classNames from 'classnames';
import { ComponentProps, FC } from 'react';
import { useTheme } from '../Flowbite/ThemeContext';

import { useAccordionContext } from './AccordionPanelContext';

export const AccordionContent: FC<ComponentProps<'div'>> = ({ children, className, ...props }) => {
export const AccordionContent: FC<ComponentProps<'div'>> = ({ children, ...props }) => {
const {
theme: {
accordion: { content },
},
} = useTheme();
const { isOpen } = useAccordionContext();

const baseStyle = classNames('first:rounded-t-lg', content.base);

return isOpen ? (
<div {...props} className={classNames('py-5 dark:bg-gray-900', className)}>
<div {...props} className={baseStyle}>
{children}
</div>
) : null;
Expand Down
11 changes: 2 additions & 9 deletions src/lib/components/Accordion/AccordionPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Children, cloneElement, ComponentProps, FC, PropsWithChildren, ReactElement, useMemo, useState } from 'react';
import classNames from 'classnames';

import { AccordionPanelContext } from './AccordionPanelContext';

export type AccordionPanelProps = PropsWithChildren<{
Expand All @@ -11,13 +9,8 @@ export type AccordionPanelProps = PropsWithChildren<{
export const AccordionPanel: FC<AccordionPanelProps> = ({ children, open, flush }) => {
const [isOpen, setIsOpen] = useState(open);
const items = useMemo(
() =>
Children.map(children as ReactElement<ComponentProps<'div' | 'button'>>[], (child) =>
cloneElement(child, {
className: classNames(child.props.className, { 'px-5 first:rounded-t-lg last:rounded-b-lg': !flush }),
}),
),
[children, flush],
() => Children.map(children as ReactElement<ComponentProps<'div' | 'button'>>[], (child) => cloneElement(child)),
[children],
);

return <AccordionPanelContext.Provider value={{ flush, isOpen, setIsOpen }}>{items}</AccordionPanelContext.Provider>;
Expand Down
30 changes: 18 additions & 12 deletions src/lib/components/Accordion/AccordionTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
import { ComponentProps, FC } from 'react';
import classNames from 'classnames';
import { HiChevronDown } from 'react-icons/hi';

import { useAccordionContext } from './AccordionPanelContext';
import { useTheme } from '../Flowbite/ThemeContext';

export type AccordionTitleProps = ComponentProps<'button'> & {
arrowIcon?: FC<ComponentProps<'svg'>>;
};

export const AccordionTitle: FC<AccordionTitleProps> = ({
children,
className,
arrowIcon: ArrowIcon = HiChevronDown,
...props
}) => {
const { flush, isOpen, setIsOpen } = useAccordionContext();

const onClick = () => setIsOpen(!isOpen);

const {
theme: {
accordion: { title },
},
} = useTheme();

const baseStyle = classNames(
'flex w-full items-center justify-between first:rounded-t-lg last:rounded-b-lg',
title.base,
);
const buttonStateStyle = classNames({
[title.notFlushed]: !flush,
[title.isOpen]: isOpen,
[title.isOpenNotFlushed]: isOpen && !flush,
});

return (
<button
data-testid="accordion-title-element"
{...props}
type="button"
className={classNames(
'flex w-full items-center justify-between py-5 text-left font-medium text-gray-500 dark:text-gray-400',
{
'hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:hover:bg-gray-800 dark:focus:ring-gray-800': !flush,
'text-gray-900 dark:text-white': isOpen,
'bg-gray-100 dark:bg-gray-800': isOpen && !flush,
},
className,
)}
className={classNames(baseStyle, buttonStateStyle)}
onClick={onClick}
>
<h2>{children}</h2>
Expand Down
21 changes: 13 additions & 8 deletions src/lib/components/Accordion/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,31 @@ import { Children, cloneElement, FC, PropsWithChildren, ReactElement, useMemo }
import { AccordionPanel, AccordionPanelProps } from './AccordionPanel';
import { AccordionTitle } from './AccordionTitle';
import { AccordionContent } from './AccordionContent';
import classNames from 'classnames';
import cn from 'classnames';
import { useTheme } from '../Flowbite/ThemeContext';

export type AccordionProps = PropsWithChildren<{
flush?: boolean;
}>;

const AccordionComponent: FC<AccordionProps> = ({ children, flush }) => {
const {
theme: { accordion },
} = useTheme();

const baseStyle = accordion.base;
const flushStyle = {
'rounded-lg border': !flush,
'border-b': flush,
};

const panels = useMemo(
() => Children.map(children as ReactElement<AccordionPanelProps>[], (child) => cloneElement(child, { flush })),
[children, flush],
);

return (
<div
data-testid="accordion-element"
className={classNames('divide-y divide-gray-200 border-gray-200 dark:divide-gray-700 dark:border-gray-700', {
'rounded-lg border': !flush,
'border-b': flush,
})}
>
<div data-testid="accordion-element" className={cn(baseStyle, flushStyle)}>
{panels}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Meta, Story } from '@storybook/react/types-6-0';

import { DarkThemeToggle } from '.';
import { Flowbite } from '../Flowbite';

export default {
title: 'Components/DarkThemeToggle',
component: DarkThemeToggle,
} as Meta;

const Template: Story = (args) => <DarkThemeToggle {...args} />;
const Template: Story = (args) => (
<Flowbite>
<DarkThemeToggle {...args} />
</Flowbite>
);

export const DefaultDarkThemeToggle = Template.bind({});
DefaultDarkThemeToggle.storyName = 'Default';
Expand Down
35 changes: 5 additions & 30 deletions src/lib/components/DarkThemeToggle/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,17 @@
import { FC, useEffect, useState } from 'react';
import { FC, useContext } from 'react';
import { HiMoon, HiSun } from 'react-icons/hi';

export type Theme = 'dark' | 'light';
import { ThemeContext } from '../Flowbite/ThemeContext';

export const DarkThemeToggle: FC = () => {
const [theme, setTheme] = useState<Theme>();

useEffect(() => {
if (
localStorage.getItem('color-theme') === 'dark' ||
(!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
setTheme('dark');
} else {
setTheme('light');
}
}, []);

useEffect(() => {
if (theme) {
localStorage.setItem('color-theme', theme);
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
}, [theme]);

const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark');
const { mode, toggleMode } = useContext(ThemeContext);

return (
<button
type="button"
className="rounded-lg p-2.5 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-700"
onClick={toggleTheme}
onClick={toggleMode}
>
{theme === 'light' ? <HiMoon className="h-5 w-5" /> : <HiSun className="h-5 w-5" />}
{mode === 'dark' ? <HiSun className="h-5 w-5" /> : <HiMoon className="h-5 w-5" />}
</button>
);
};
Loading