diff --git a/.storybook/preview.ts b/.storybook/preview.ts deleted file mode 100644 index 37914b1..0000000 --- a/.storybook/preview.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Preview } from "@storybook/react"; - -const preview: Preview = { - parameters: { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - }, -}; - -export default preview; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000..cfa88da --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,54 @@ +import React, { useEffect } from "react"; +import clsx from "clsx"; + +import type { Preview } from "@storybook/react"; + +import "../src/styles/global.css"; + +export type IPreviewArgs = { + themeUI: "light" | "dark"; + backgroundUI: "white" | "light"; +}; + +const preview: Preview = { + decorators: [ + (Story, { args }) => { + const { themeUI, backgroundUI } = args; + useEffect(() => { + if (themeUI === "dark") document.documentElement.classList.add("dark"); + else document.documentElement.classList.remove("dark"); + }, [themeUI]); + return ( +
+ +
+ ); + }, + ], + args: { + themeUI: "light", + backgroundUI: "white", + }, + argTypes: { + themeUI: { + options: ["light", "dark"], + control: { type: "radio" }, + }, + backgroundUI: { + options: ["white", "light"], + control: { type: "radio" }, + }, + }, + parameters: { + layout: "centered", + }, +}; + +export default preview; diff --git a/eslint.config.mjs b/eslint.config.mjs index 543549d..83053fb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -67,6 +67,11 @@ export default [ react: { version: "^16.12.0", }, + "import/resolver": { + node: { + extensions: [".js", ".jsx", ".ts", ".tsx"], + }, + }, }, rules: { diff --git a/package.json b/package.json index a817ae4..7617832 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "simplebar": "^5.3.6", "simplebar-react": "^2.3.6", "smooth-scroll-into-view-if-needed": "^1.1.33", + "tailwind-merge": "^3.0.2", "usehooks-ts": "^2.9.1" }, "lint-staged": { diff --git a/src/lib/accordion/accordion-item.tsx b/src/lib/accordion/accordion-item.tsx index d4168f3..ae91b8c 100644 --- a/src/lib/accordion/accordion-item.tsx +++ b/src/lib/accordion/accordion-item.tsx @@ -3,8 +3,8 @@ import { useElementSize } from "../../hooks/useElementSize"; import Plus from "../../assets/svgs/accordion/plus.svg"; import Minus from "../../assets/svgs/accordion/minus.svg"; -import clsx from "clsx"; import { Button } from "react-aria-components"; +import { cn } from "../../utils"; interface AccordionItemProps { setExpanded: React.Dispatch>; @@ -25,7 +25,7 @@ const AccordionItem: React.FC = ({ return (
= ({ const [expanded, setExpanded] = useState(-1); return (
{items.map((item, index) => ( diff --git a/src/lib/accordion/index.tsx b/src/lib/accordion/index.tsx index 2d4e35e..cfff220 100644 --- a/src/lib/accordion/index.tsx +++ b/src/lib/accordion/index.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, useState } from "react"; import AccordionItem from "./accordion-item"; -import clsx from "clsx"; +import { cn } from "../../utils"; interface AccordionItem { title: string; @@ -31,7 +31,7 @@ const Accordion: React.FC = ({ const [expanded, setExpanded] = useState(defaultExpanded ?? -1); return (
{items.map((item, index) => ( diff --git a/src/stories/Button.stories.ts b/src/stories/Button.stories.ts deleted file mode 100644 index 07c1f38..0000000 --- a/src/stories/Button.stories.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { fn } from "@storybook/test"; - -import { Button } from "./Button"; - -// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export -const meta = { - title: "Example/Button", - component: Button, - parameters: { - // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - layout: "centered", - }, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ["autodocs"], - // More on argTypes: https://storybook.js.org/docs/api/argtypes - argTypes: { - backgroundColor: { control: "color" }, - }, - // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args - args: { onClick: fn() }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -export const Primary: Story = { - args: { - primary: true, - label: "Button", - }, -}; - -export const Secondary: Story = { - args: { - label: "Button", - }, -}; - -export const Large: Story = { - args: { - size: "large", - label: "Button", - }, -}; - -export const Small: Story = { - args: { - size: "small", - label: "Button", - }, -}; diff --git a/src/stories/Button.tsx b/src/stories/Button.tsx deleted file mode 100644 index 242d660..0000000 --- a/src/stories/Button.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; - -import "./button.css"; - -export interface ButtonProps { - /** Is this the principal call to action on the page? */ - primary?: boolean; - /** What background color to use */ - backgroundColor?: string; - /** How large should the button be? */ - size?: "small" | "medium" | "large"; - /** Button contents */ - label: string; - /** Optional click handler */ - onClick?: () => void; -} - -/** Primary UI component for user interaction */ -export const Button = ({ - primary = false, - size = "medium", - backgroundColor, - label, - ...props -}: ButtonProps) => { - const mode = primary - ? "storybook-button--primary" - : "storybook-button--secondary"; - return ( - - ); -}; diff --git a/src/stories/Header.stories.ts b/src/stories/Header.stories.ts deleted file mode 100644 index eb3e277..0000000 --- a/src/stories/Header.stories.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { fn } from "@storybook/test"; - -import { Header } from "./Header"; - -const meta = { - title: "Example/Header", - component: Header, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ["autodocs"], - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: "fullscreen", - }, - args: { - onLogin: fn(), - onLogout: fn(), - onCreateAccount: fn(), - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const LoggedIn: Story = { - args: { - user: { - name: "Jane Doe", - }, - }, -}; - -export const LoggedOut: Story = {}; diff --git a/src/stories/Header.tsx b/src/stories/Header.tsx deleted file mode 100644 index 9b143da..0000000 --- a/src/stories/Header.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; - -import { Button } from "./Button"; -import "./header.css"; - -type User = { - name: string; -}; - -export interface HeaderProps { - user?: User; - onLogin?: () => void; - onLogout?: () => void; - onCreateAccount?: () => void; -} - -export const Header = ({ - user, - onLogin, - onLogout, - onCreateAccount, -}: HeaderProps) => ( -
-
-
- - - - - - - -

Acme

-
-
- {user ? ( - <> - - Welcome, {user.name}! - -
-
-
-); diff --git a/src/stories/Page.stories.ts b/src/stories/Page.stories.ts deleted file mode 100644 index adcad1f..0000000 --- a/src/stories/Page.stories.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { expect, userEvent, within } from "@storybook/test"; - -import { Page } from "./Page"; - -const meta = { - title: "Example/Page", - component: Page, - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: "fullscreen", - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const LoggedOut: Story = {}; - -// More on component testing: https://storybook.js.org/docs/writing-tests/component-testing -export const LoggedIn: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const loginButton = canvas.getByRole("button", { name: /Log in/i }); - await expect(loginButton).toBeInTheDocument(); - await userEvent.click(loginButton); - await expect(loginButton).not.toBeInTheDocument(); - - const logoutButton = canvas.getByRole("button", { name: /Log out/i }); - await expect(logoutButton).toBeInTheDocument(); - }, -}; diff --git a/src/stories/Page.tsx b/src/stories/Page.tsx deleted file mode 100644 index a294b19..0000000 --- a/src/stories/Page.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from "react"; - -import { Header } from "./Header"; -import "./page.css"; - -type User = { - name: string; -}; - -export const Page: React.FC = () => { - const [user, setUser] = React.useState(); - - return ( -
-
setUser({ name: "Jane Doe" })} - onLogout={() => setUser(undefined)} - onCreateAccount={() => setUser({ name: "Jane Doe" })} - /> - -
-

Pages in Storybook

-

- We recommend building UIs with a{" "} - - component-driven - {" "} - process starting with atomic components and ending with pages. -

-

- Render pages with mock data. This makes it easy to build and review - page states without needing to navigate to them in your app. Here are - some handy patterns for managing page data in Storybook: -

-
    -
  • - Use a higher-level connected component. Storybook helps you compose - such data from the "args" of child component stories -
  • -
  • - Assemble data in the page component from your services. You can mock - these services out using Storybook. -
  • -
-

- Get a guided tutorial on component-driven development at{" "} - - Storybook tutorials - - . Read more in the{" "} - - docs - - . -

-
- Tip Adjust the width of the canvas with - the{" "} - - - - - - Viewports addon in the toolbar -
-
-
- ); -}; diff --git a/src/stories/accordion.stories.tsx b/src/stories/accordion.stories.tsx new file mode 100644 index 0000000..a1db66b --- /dev/null +++ b/src/stories/accordion.stories.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { IPreviewArgs } from "./utils"; + +import Accordion from "../lib/accordion/index"; + +const meta = { + component: Accordion, + title: "Accordion", + tags: ["autodocs"], +} satisfies Meta; + +export default meta; + +type Story = StoryObj & IPreviewArgs; + +export const DarkTheme: Story = { + args: { + className: "max-w-[80dvw]", + + items: [ + { + title: "How it works?", + body: ( + + {"hello\nhello\n\n\n\n\nhello"} + + ), + }, + { + title: "How it works?", + body: ( + hello + ), + }, + ], + + themeUI: "dark", + backgroundUI: "light", + }, +}; diff --git a/src/stories/button.css b/src/stories/button.css deleted file mode 100644 index 4e3620b..0000000 --- a/src/stories/button.css +++ /dev/null @@ -1,30 +0,0 @@ -.storybook-button { - display: inline-block; - cursor: pointer; - border: 0; - border-radius: 3em; - font-weight: 700; - line-height: 1; - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -} -.storybook-button--primary { - background-color: #555ab9; - color: white; -} -.storybook-button--secondary { - box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; - background-color: transparent; - color: #333; -} -.storybook-button--small { - padding: 10px 16px; - font-size: 12px; -} -.storybook-button--medium { - padding: 11px 20px; - font-size: 14px; -} -.storybook-button--large { - padding: 12px 24px; - font-size: 16px; -} diff --git a/src/stories/header.css b/src/stories/header.css deleted file mode 100644 index 5efd46c..0000000 --- a/src/stories/header.css +++ /dev/null @@ -1,32 +0,0 @@ -.storybook-header { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - padding: 15px 20px; - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -.storybook-header svg { - display: inline-block; - vertical-align: top; -} - -.storybook-header h1 { - display: inline-block; - vertical-align: top; - margin: 6px 0 6px 10px; - font-weight: 700; - font-size: 20px; - line-height: 1; -} - -.storybook-header button + button { - margin-left: 10px; -} - -.storybook-header .welcome { - margin-right: 10px; - color: #333; - font-size: 14px; -} diff --git a/src/stories/page.css b/src/stories/page.css deleted file mode 100644 index 77c81d2..0000000 --- a/src/stories/page.css +++ /dev/null @@ -1,68 +0,0 @@ -.storybook-page { - margin: 0 auto; - padding: 48px 20px; - max-width: 600px; - color: #333; - font-size: 14px; - line-height: 24px; - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -.storybook-page h2 { - display: inline-block; - vertical-align: top; - margin: 0 0 4px; - font-weight: 700; - font-size: 32px; - line-height: 1; -} - -.storybook-page p { - margin: 1em 0; -} - -.storybook-page a { - color: inherit; -} - -.storybook-page ul { - margin: 1em 0; - padding-left: 30px; -} - -.storybook-page li { - margin-bottom: 8px; -} - -.storybook-page .tip { - display: inline-block; - vertical-align: top; - margin-right: 10px; - border-radius: 1em; - background: #e7fdd8; - padding: 4px 12px; - color: #357a14; - font-weight: 700; - font-size: 11px; - line-height: 12px; -} - -.storybook-page .tip-wrapper { - margin-top: 40px; - margin-bottom: 40px; - font-size: 13px; - line-height: 20px; -} - -.storybook-page .tip-wrapper svg { - display: inline-block; - vertical-align: top; - margin-top: 3px; - margin-right: 4px; - width: 12px; - height: 12px; -} - -.storybook-page .tip-wrapper svg path { - fill: #1ea7fd; -} diff --git a/src/stories/utils.tsx b/src/stories/utils.tsx new file mode 100644 index 0000000..a7ed1d2 --- /dev/null +++ b/src/stories/utils.tsx @@ -0,0 +1,6 @@ +export type IPreviewArgs = { + args: { + themeUI: "light" | "dark"; + backgroundUI: "white" | "light"; + }; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..c71067e --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,6 @@ +import { twMerge } from "tailwind-merge"; +import clsx, { ClassValue } from "clsx"; + +export const cn = (...inputs: ClassValue[]) => { + return twMerge(clsx(inputs)); +}; diff --git a/yarn.lock b/yarn.lock index 44bcd5e..f0bc78c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1250,6 +1250,7 @@ __metadata: smooth-scroll-into-view-if-needed: "npm:^1.1.33" storybook: "npm:^8.6.4" styled-components: "npm:^5.3.3" + tailwind-merge: "npm:^3.0.2" tailwindcss: "npm:^4.0.11" typescript: "npm:^5.8.2" usehooks-ts: "npm:^2.9.1" @@ -10394,6 +10395,13 @@ __metadata: languageName: node linkType: hard +"tailwind-merge@npm:^3.0.2": + version: 3.0.2 + resolution: "tailwind-merge@npm:3.0.2" + checksum: 10c0/73f528345e5130e7690c5c57701155849ae4f0834e0a4bf56bc03e25d4563d45d4905ff9c487a7dce9efdfa18689a223e78365962105ad0e2f2d1226216c66ef + languageName: node + linkType: hard + "tailwindcss@npm:4.0.11, tailwindcss@npm:^4.0.11": version: 4.0.11 resolution: "tailwindcss@npm:4.0.11"