diff --git a/src/components/ui/__stories__/Breadcrumb.stories.tsx b/src/components/ui/__stories__/Breadcrumb.stories.tsx new file mode 100644 index 00000000000..e614134a567 --- /dev/null +++ b/src/components/ui/__stories__/Breadcrumb.stories.tsx @@ -0,0 +1,125 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { + Breadcrumb, + BreadcrumbEllipsis, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "../breadcrumb" + +const meta = { + title: "UI / Primitives / Breadcrumb", + component: Breadcrumb, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Hierarchy navigation. Compose `Breadcrumb` > `BreadcrumbList` > `BreadcrumbItem` with `BreadcrumbLink` (clickable) or `BreadcrumbPage` (current page, non-link). Separators default to a chevron icon.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const TwoLevel: Story = { + render: () => ( + + + + Home + + + + Layer 2 + + + + ), +} + +export const ThreeLevel: Story = { + render: () => ( + + + + Home + + + + Layer 2 + + + + Optimistic rollups + + + + ), +} + +export const WithCurrentPage: Story = { + parameters: { + docs: { + description: { + story: + "`BreadcrumbPage` marks the current page with `aria-current='page'` and renders in the primary color.", + }, + }, + }, + render: () => ( + + + + Home + + + + Learn + + + + What is Ethereum? + + + + ), +} + +export const WithEllipsis: Story = { + parameters: { + docs: { + description: { + story: + "Use `BreadcrumbEllipsis` to collapse intermediate levels in deep hierarchies.", + }, + }, + }, + render: () => ( + + + + Home + + + + + + + + Docs + + + + Smart contracts + + + + ), +} diff --git a/src/components/ui/__stories__/Chart.stories.tsx b/src/components/ui/__stories__/Chart.stories.tsx new file mode 100644 index 00000000000..fd83d7f0358 --- /dev/null +++ b/src/components/ui/__stories__/Chart.stories.tsx @@ -0,0 +1,232 @@ +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Cell, + Line, + LineChart, + Pie, + PieChart, + XAxis, + YAxis, +} from "recharts" +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { + type ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from "../chart" + +const meta = { + title: "UI / Chart", + component: ChartContainer, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Recharts wrapper. `ChartContainer` accepts a `config` map (`{ key: { label, color | theme, icon? } }`) and renders any Recharts chart inside a responsive container. `ChartTooltipContent` and `ChartLegendContent` adapt the default Recharts UI to the design system. Color tokens are exposed as CSS variables (`--color-`) you reference via `fill='var(--color-foo)'`.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +const monthlyData = [ + { month: "Jan", mainnet: 186, l2: 80 }, + { month: "Feb", mainnet: 305, l2: 200 }, + { month: "Mar", mainnet: 237, l2: 120 }, + { month: "Apr", mainnet: 73, l2: 190 }, + { month: "May", mainnet: 209, l2: 130 }, + { month: "Jun", mainnet: 214, l2: 140 }, +] + +const monthlyConfig = { + mainnet: { label: "Mainnet", color: "hsl(var(--primary))" }, + l2: { label: "Layer 2", color: "hsl(var(--accent-a))" }, +} satisfies ChartConfig + +export const BarUsage: Story = { + args: { config: monthlyConfig, children: <> }, + render: (args) => ( + + + + + + } /> + } /> + + + + + ), +} + +export const LineUsage: Story = { + args: { config: monthlyConfig, children: <> }, + render: (args) => ( + + + + + + } /> + } /> + + + + + ), +} + +export const AreaUsage: Story = { + args: { config: monthlyConfig, children: <> }, + render: (args) => ( + + + + + + } /> + } /> + + + + + ), +} + +const pieData = [ + { name: "ethereum", value: 60 }, + { name: "arbitrum", value: 18 }, + { name: "base", value: 12 }, + { name: "op", value: 10 }, +] + +const pieConfig = { + ethereum: { label: "Ethereum", color: "hsl(var(--primary))" }, + arbitrum: { label: "Arbitrum", color: "hsl(var(--accent-a))" }, + base: { label: "Base", color: "hsl(var(--accent-b))" }, + op: { label: "OP Mainnet", color: "hsl(var(--accent-c))" }, +} satisfies ChartConfig + +export const PieUsage: Story = { + args: { config: pieConfig, children: <> }, + render: (args) => ( + + + } /> + } /> + + {pieData.map((entry) => ( + + ))} + + + + ), +} + +export const TooltipIndicators: Story = { + args: { config: monthlyConfig, children: <> }, + parameters: { + docs: { + description: { + story: + "`ChartTooltipContent` accepts an `indicator` prop: `dot` (default), `line`, or `dashed`. Hover the bars to compare.", + }, + }, + }, + render: (args) => ( + + + + + } /> + } /> + + + + + ), +} + +export const HiddenLabel: Story = { + args: { config: monthlyConfig, children: <> }, + parameters: { + docs: { + description: { + story: + "`hideLabel` on `ChartTooltipContent` removes the X-axis category from the tooltip header. Useful for non-categorical charts.", + }, + }, + }, + render: (args) => ( + + + + + } /> + + + + ), +} diff --git a/src/components/ui/__stories__/Collapsible.stories.tsx b/src/components/ui/__stories__/Collapsible.stories.tsx new file mode 100644 index 00000000000..11863fc0abd --- /dev/null +++ b/src/components/ui/__stories__/Collapsible.stories.tsx @@ -0,0 +1,137 @@ +import { ChevronDown, ChevronUp } from "lucide-react" +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { Button } from "../buttons/Button" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "../collapsible" +import { HStack, VStack } from "../flex" + +const meta = { + title: "UI / Primitives / Collapsible", + component: Collapsible, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Disclosure primitive built on Radix Collapsible. Compose `Collapsible` > `CollapsibleTrigger` (use `asChild` to wrap any focusable element) + `CollapsibleContent`. For multi-section flows, prefer `Accordion`.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Closed: Story = { + render: () => ( + + + + + + Hidden content. The trigger toggles the open state. + + + ), +} + +export const Open: Story = { + render: () => ( + + + + + + Visible content. Use `defaultOpen` to start expanded. + + + ), +} + +export const WithCustomTrigger: Story = { + parameters: { + docs: { + description: { + story: + "`CollapsibleTrigger` renders as a `button` by default; pass `asChild` to render a custom element. Use `data-state` to swap the icon based on open state.", + }, + }, + }, + render: () => ( + + + + + +

Custom triggers can pull in any element via `asChild`.

+

The chevron flips using `data-state=open` on the trigger.

+
+
+ ), +} + +export const ListOfDetails: Story = { + parameters: { + docs: { + description: { + story: + "Multiple independent collapsibles in a list. Each manages its own state. For grouped behavior, use `Accordion` instead.", + }, + }, + }, + render: () => ( + + {[ + { + title: "What is a layer 2?", + body: "Layer 2 networks scale Ethereum by handling transactions off the main chain.", + }, + { + title: "What is a rollup?", + body: "Rollups bundle many transactions into a single proof posted to layer 1.", + }, + { + title: "What is a validator?", + body: "Validators secure the network by proposing and attesting to blocks.", + }, + ].map(({ title, body }) => ( + + + + + + {body} + + + ))} + + ), +} diff --git a/src/components/ui/__stories__/List.stories.tsx b/src/components/ui/__stories__/List.stories.tsx new file mode 100644 index 00000000000..c15ec27103e --- /dev/null +++ b/src/components/ui/__stories__/List.stories.tsx @@ -0,0 +1,88 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { List, ListItem, OrderedList, UnorderedList } from "../list" + +const meta = { + title: "UI / List", + component: List, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Prose lists. `UnorderedList` (alias of `List`) renders a `
    `, `OrderedList` renders an `
      `. Wrap items in `ListItem` for consistent spacing. Both lists use `asChild` via `Slot` to inherit semantics from the parent element.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Unordered: Story = { + render: () => ( + + Smart contracts run on the EVM. + Validators secure the network. + Layer 2 rollups scale throughput. + + ), +} + +export const Ordered: Story = { + render: () => ( + + Connect a wallet. + Pick a layer 2 network. + Bridge ETH to start transacting. + + ), +} + +export const Nested: Story = { + parameters: { + docs: { + description: { + story: + "Nested lists pick up `mt-3` between parent `ListItem` and the nested list via the `[&_ol]:mt-3 [&_ul]:mt-3` selector on `ListItem`.", + }, + }, + }, + render: () => ( + + + Layer 1 + + Ethereum mainnet + + + + Layer 2 + + Arbitrum One + Base + OP Mainnet + + + + ), +} + +export const PlainList: Story = { + parameters: { + docs: { + description: { + story: + "Without an explicit `list-*` class the markers are hidden, leaving plain stacked items.", + }, + }, + }, + render: () => ( + + First entry + Second entry + Third entry + + ), +} diff --git a/src/components/ui/__stories__/Progress.stories.tsx b/src/components/ui/__stories__/Progress.stories.tsx new file mode 100644 index 00000000000..f9bdb3b5b69 --- /dev/null +++ b/src/components/ui/__stories__/Progress.stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { VStack } from "../flex" +import { Progress } from "../progress" + +const meta = { + title: "UI / Primitives / Progress", + component: Progress, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Determinate progress bar built on Radix Progress. `value` is 0-100. `color: disabled` (default) renders a muted bar; `color: primary` switches the indicator and track to the brand pair.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + render: () => ( +
      + +
      + ), +} + +export const Colors: Story = { + parameters: { + docs: { + description: { + story: + "`color` prop variants. `disabled` is the default (muted), `primary` uses the brand color.", + }, + }, + }, + render: () => ( + + + + + ), +} + +export const Values: Story = { + parameters: { + docs: { + description: { + story: + "`value` is 0-100. The indicator translates with a CSS transition.", + }, + }, + }, + render: () => ( + + + + + + + + ), +} + +export const WithLabel: Story = { + render: () => ( + +
      + Uploading + 42% +
      + +
      + ), +} diff --git a/src/components/ui/__stories__/ScrollArea.stories.tsx b/src/components/ui/__stories__/ScrollArea.stories.tsx new file mode 100644 index 00000000000..f48681b206b --- /dev/null +++ b/src/components/ui/__stories__/ScrollArea.stories.tsx @@ -0,0 +1,101 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { ScrollArea, ScrollBar } from "../scroll-area" + +const meta = { + title: "UI / Primitives / ScrollArea", + component: ScrollArea, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Custom-styled scroll viewport built on Radix ScrollArea. Vertical scrollbar is wired by default; for horizontal scrolling, render an additional `ScrollBar orientation='horizontal'`.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +const NETWORKS = [ + "Ethereum", + "Arbitrum One", + "Base", + "OP Mainnet", + "zkSync Era", + "Linea", + "Scroll", + "Polygon zkEVM", + "Mantle", + "Mode", + "Blast", + "Manta Pacific", + "Taiko", + "Zora", + "Fraxtal", + "Lisk", + "Cyber", + "Mint", + "Re.al", + "Redstone", +] + +export const VerticalScroll: Story = { + render: () => ( + +
        + {NETWORKS.map((network) => ( +
      • {network}
      • + ))} +
      +
      + ), +} + +export const HorizontalScroll: Story = { + parameters: { + docs: { + description: { + story: + "Render an explicit `ScrollBar orientation='horizontal'` and wrap content in a single-row flex container wider than the viewport.", + }, + }, + }, + render: () => ( + +
      + {NETWORKS.map((network) => ( +
      + {network} +
      + ))} +
      + +
      + ), +} + +export const ShortContent: Story = { + parameters: { + docs: { + description: { + story: + "When content fits the viewport, the scrollbar is not visible. Useful as a sanity check for the `overflow-hidden` framing.", + }, + }, + }, + render: () => ( + +

      + Short content does not trigger the scrollbar. The viewport keeps its + rounded corners thanks to `rounded-[inherit]` on the inner viewport. +

      +
      + ), +} diff --git a/src/components/ui/__stories__/Skeleton.stories.tsx b/src/components/ui/__stories__/Skeleton.stories.tsx new file mode 100644 index 00000000000..2bb26e49612 --- /dev/null +++ b/src/components/ui/__stories__/Skeleton.stories.tsx @@ -0,0 +1,110 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { VStack } from "../flex" +import { + Skeleton, + SkeletonCard, + SkeletonCardContent, + SkeletonCardGrid, + SkeletonLines, +} from "../skeleton" + +const meta = { + title: "UI / Skeleton", + component: Skeleton, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Loading placeholders. Use `Skeleton` for arbitrary-shape blocks, `SkeletonLines` for paragraph-style stacks, and `SkeletonCard` / `SkeletonCardContent` / `SkeletonCardGrid` for card layouts.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + parameters: { + docs: { + description: { + story: + "Base `Skeleton` is a single block. Set width/height via Tailwind classes (`h-*`, `w-*`).", + }, + }, + }, + render: () => ( + + + + + + ), +} + +export const Lines: Story = { + parameters: { + docs: { + description: { + story: + "`SkeletonLines` renders multiple `Skeleton` blocks at varying widths. Control with `noOfLines`.", + }, + }, + }, + render: () => ( +
      + +
      + ), +} + +export const CardContent: Story = { + parameters: { + docs: { + description: { + story: + "`SkeletonCardContent` is the body half of a card placeholder, suitable when the consumer already provides the outer card frame.", + }, + }, + }, + render: () => ( +
      + +
      + ), +} + +export const Card: Story = { + parameters: { + docs: { + description: { + story: + "`SkeletonCard` is a full-card placeholder including a banner area and content lines.", + }, + }, + }, + render: () => ( +
      + +
      + ), +} + +export const CardGrid: Story = { + parameters: { + docs: { + description: { + story: + "`SkeletonCardGrid` renders 1, 2, or 3 cards depending on viewport width (responsive).", + }, + }, + }, + render: () => ( +
      + +
      + ), +} diff --git a/src/components/ui/__stories__/Spinner.stories.tsx b/src/components/ui/__stories__/Spinner.stories.tsx new file mode 100644 index 00000000000..cf921063ee7 --- /dev/null +++ b/src/components/ui/__stories__/Spinner.stories.tsx @@ -0,0 +1,89 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { HStack, VStack } from "../flex" +import { Spinner } from "../spinner" + +const meta = { + title: "UI / Spinner", + component: Spinner, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Indeterminate loading indicator. Renders a rotating `Loader2` icon sized at `1em`, so spinner size scales with the parent's `font-size`. Apply Tailwind `text-*` classes to size it. Pair with sibling text for a label.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + render: () => , +} + +export const Sizes: Story = { + parameters: { + docs: { + description: { + story: + "Size scales with `font-size`. Use Tailwind text-size utilities on the spinner element.", + }, + }, + }, + render: () => ( + + + + + + + + ), +} + +export const WithLabel: Story = { + parameters: { + docs: { + description: { + story: + "Spinner has no built-in label. Pair with sibling text and announce via aria-live for accessibility.", + }, + }, + }, + render: () => ( + + + + Loading... + + + + Fetching layer 2 networks + + + ), +} + +export const InsideButton: Story = { + parameters: { + docs: { + description: { + story: "Place inline with button text to indicate a pending action.", + }, + }, + }, + render: () => ( + + ), +} diff --git a/src/components/ui/__stories__/Tabs.stories.tsx b/src/components/ui/__stories__/Tabs.stories.tsx new file mode 100644 index 00000000000..393229afe98 --- /dev/null +++ b/src/components/ui/__stories__/Tabs.stories.tsx @@ -0,0 +1,98 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../tabs" + +const meta = { + title: "UI / Primitives / Tabs", + component: Tabs, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Tabbed navigation built on Radix Tabs. Wrap with `Tabs`, render labels in `TabsList` + `TabsTrigger`, and one `TabsContent` per tab keyed by `value`. Active state is driven by `value`/`defaultValue` on the root.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + render: () => ( + + + Overview + Details + History + + + Overview content. Switch tabs to see other panels. + + + Details content. Each tab has its own panel. + + + History content. Panels are siblings keyed by `value`. + + + ), +} + +export const WithDisabledTab: Story = { + parameters: { + docs: { + description: { + story: + "Mark a tab unavailable with `disabled`. Disabled triggers are skipped during keyboard navigation.", + }, + }, + }, + render: () => ( + + + Active + Archived + + Deleted + + + Showing active items. + Showing archived items. + Deleted items are not shown. + + ), +} + +export const ManyPanels: Story = { + parameters: { + docs: { + description: { + story: + "The tab list scrolls horizontally on overflow (`overflow-x-auto`).", + }, + }, + }, + render: () => ( + + + Ethereum + Arbitrum + Base + OP Mainnet + zkSync Era + Linea + Scroll + + Ethereum mainnet. + Arbitrum One details. + Base details. + OP Mainnet details. + zkSync Era details. + Linea details. + Scroll details. + + ), +} diff --git a/src/components/ui/__stories__/TruncatedText.stories.tsx b/src/components/ui/__stories__/TruncatedText.stories.tsx new file mode 100644 index 00000000000..6b63269dd29 --- /dev/null +++ b/src/components/ui/__stories__/TruncatedText.stories.tsx @@ -0,0 +1,101 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { VStack } from "../flex" +import TruncatedText from "../TruncatedText" + +const meta = { + title: "UI / TruncatedText", + component: TruncatedText, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Prose with a clamped line count and a Show more / Show less toggle. `maxLines` accepts 1-4 (the values supported by `LINE_CLAMP_CLASS_MAPPING`). The toggle button is always rendered; click to expand or collapse.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +const SHORT_TEXT = + "Ethereum is a decentralized, open-source blockchain featuring smart-contract functionality." + +const LONG_TEXT = + "Ethereum is a decentralized, open-source blockchain featuring smart-contract functionality. Ether is the native cryptocurrency of the platform. Among cryptocurrencies, ether is second only to bitcoin in market capitalization. Ethereum was conceived in 2013 by programmer Vitalik Buterin. Additional founders of Ethereum included Gavin Wood, Charles Hoskinson, Anthony Di Iorio, and Joseph Lubin. In 2014, development work began and was crowdfunded, and the network went live on 30 July 2015. Ethereum allows anyone to deploy permanent and immutable decentralized applications onto it, with which users can interact." + +export const Default: Story = { + args: { children: LONG_TEXT }, + render: (args) => ( +
      + +
      + ), +} + +export const OneLine: Story = { + args: { children: LONG_TEXT, maxLines: 1 }, + render: (args) => ( +
      + +
      + ), +} + +export const ThreeLines: Story = { + args: { children: LONG_TEXT, maxLines: 3 }, + render: (args) => ( +
      + +
      + ), +} + +export const FourLines: Story = { + args: { children: LONG_TEXT, maxLines: 4 }, + render: (args) => ( +
      + +
      + ), +} + +export const LineCounts: Story = { + parameters: { + docs: { + description: { + story: + "All four supported `maxLines` values side-by-side for visual comparison.", + }, + }, + }, + render: () => ( + + {[1, 2, 3, 4].map((n) => ( +
      +

      maxLines = {n}

      + {LONG_TEXT} +
      + ))} +
      + ), +} + +export const ShortText: Story = { + parameters: { + docs: { + description: { + story: + "When the content fits within `maxLines`, the prose is not clamped, but the toggle button is still rendered. Consumers may want to gate `TruncatedText` behind a length check.", + }, + }, + }, + render: () => ( +
      + {SHORT_TEXT} +
      + ), +}