diff --git a/src/components/ui/__stories__/Flex.stories.tsx b/src/components/ui/__stories__/Flex.stories.tsx new file mode 100644 index 00000000000..66ee63d291a --- /dev/null +++ b/src/components/ui/__stories__/Flex.stories.tsx @@ -0,0 +1,152 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { Center, Flex, HStack, Stack, VStack } from "../flex" + +const meta = { + title: "UI / Flex", + component: Flex, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Flex layout primitives. `Flex` is the base (no axis or alignment defaults beyond `display: flex`). `Center` aligns and centers with `gap-2`. `Stack` is `flex-col gap-2` and accepts a `separator` element rendered between children. `HStack` and `VStack` extend `Stack` with horizontal/vertical orientation and centered cross-axis alignment.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +const Box = ({ children }: { children: React.ReactNode }) => ( +
+ {children} +
+) + +export const FlexBase: Story = { + parameters: { + docs: { + description: { + story: + "`Flex` only applies `display: flex`. Add direction, alignment, and gap with Tailwind utilities.", + }, + }, + }, + render: () => ( + + One + Two + Three + + ), +} + +export const CenterBase: Story = { + parameters: { + docs: { + description: { + story: + "`Center` is `flex items-center justify-center gap-2`. Use for centered icon + text pairs or single-element centering.", + }, + }, + }, + render: () => ( +
+ Centered +
+ ), +} + +export const StackBase: Story = { + parameters: { + docs: { + description: { + story: "`Stack` is `flex flex-col gap-2` (vertical, no separator).", + }, + }, + }, + render: () => ( + + One + Two + Three + + ), +} + +export const StackWithSeparator: Story = { + parameters: { + docs: { + description: { + story: + "Pass a `separator` element to render it between children. The separator is cloned with `border self-stretch`.", + }, + }, + }, + render: () => ( + }> + One + Two + Three + + ), +} + +export const HStackBase: Story = { + parameters: { + docs: { + description: { + story: + "`HStack` lays out children horizontally with centered cross-axis alignment.", + }, + }, + }, + render: () => ( + + One + Two + Three + + ), +} + +export const VStackBase: Story = { + parameters: { + docs: { + description: { + story: + "`VStack` lays out children vertically with centered cross-axis alignment (each item is centered horizontally).", + }, + }, + }, + render: () => ( + + One + Two + Three + + ), +} + +export const HStackWithSeparator: Story = { + render: () => ( + }> + One + Two + Three + + ), +} + +export const VStackWithSeparator: Story = { + render: () => ( + }> + One + Two + Three + + ), +} diff --git a/src/components/ui/__stories__/LinkBox.stories.tsx b/src/components/ui/__stories__/LinkBox.stories.tsx new file mode 100644 index 00000000000..836ed119967 --- /dev/null +++ b/src/components/ui/__stories__/LinkBox.stories.tsx @@ -0,0 +1,90 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { BaseLink } from "../Link" +import { LinkBox, LinkOverlay } from "../link-box" + +const meta = { + title: "UI / LinkBox", + component: LinkBox, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Whole-card-clickable pattern. `LinkBox` is the positioned wrapper (`relative z-10`); `LinkOverlay` (built on `BaseLink`) places a `::before` pseudo-element across the entire box so any click within the card navigates. Other interactive children stay accessible because they have higher z-index.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + render: () => ( + +

+ Layer 2 networks +

+

+ Anywhere on this card is a click target. The overlay covers the whole + box via the `before:absolute` pseudo-element. +

+
+ ), +} + +export const WithNestedInteractive: Story = { + parameters: { + docs: { + description: { + story: + "Nested interactive elements (like a secondary link or button) need a higher stacking context to remain clickable. Use `relative z-10` on them.", + }, + }, + }, + render: () => ( + +
+

+ Decentralized applications +

+

+ Clicking the card navigates to dapps. The badge below stays + independently clickable because it sits above the overlay. +

+
+ + DeFi + +
+ ), +} + +export const ExternalLink: Story = { + parameters: { + docs: { + description: { + story: + "When `LinkOverlay`'s href is external, `BaseLink` would normally apply `relative` positioning; `LinkOverlay` overrides this with `!static` so the `::before` overlay still anchors to the parent `LinkBox`.", + }, + }, + }, + render: () => ( + +

+ + ethresear.ch (external) + +

+

+ Even with an external destination, the entire card is the click target. +

+
+ ), +} diff --git a/src/components/ui/__stories__/Section.stories.tsx b/src/components/ui/__stories__/Section.stories.tsx new file mode 100644 index 00000000000..09e704a8ba4 --- /dev/null +++ b/src/components/ui/__stories__/Section.stories.tsx @@ -0,0 +1,114 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import { + Section, + SectionBanner, + SectionContent, + SectionHeader, + SectionTag, +} from "../section" + +const meta = { + title: "UI / Section", + component: Section, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "Top-level page section. `variant: responsiveFlex` enables a column-on-mobile / row-on-desktop layout. `scrollMargin: tabNav` adds extra scroll-margin so sticky-nav layouts land below the nav. Sub-components: `SectionBanner`, `SectionTag`, `SectionHeader`, `SectionContent`.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + render: () => ( +
+ Overview + What is Ethereum? + +

+ Ethereum is a decentralized, open-source blockchain featuring smart + contract functionality. +

+
+
+ ), +} + +export const ResponsiveFlex: Story = { + parameters: { + docs: { + description: { + story: + "`variant: responsiveFlex` stacks on mobile and switches to a row at `md`.", + }, + }, + }, + render: () => ( +
+ +
+ Banner area +
+
+ + Layer 2 + Scaling Ethereum +

+ Layer 2 networks bundle transactions off-chain and post proofs to + mainnet, reducing fees and increasing throughput. +

+
+
+ ), +} + +export const TabNavScrollMargin: Story = { + parameters: { + docs: { + description: { + story: + "`scrollMargin: tabNav` adds the extra scroll-margin needed when the page has a sticky tab nav above the fold. The visual effect is invisible at rest; observe by hash-navigating to `#tab-nav-section`.", + }, + }, + }, + render: () => ( +
+ Sticky-nav layout + Lands below the tab nav + +

+ Linking to `#tab-nav-section` will scroll so the section starts below + the sticky nav, not under it. +

+
+
+ ), +} + +export const TagVariants: Story = { + parameters: { + docs: { + description: { + story: + "`SectionTag` has two variants: `pill` (default, low-contrast bg) and `plain` (semibold uppercase, no background).", + }, + }, + }, + render: () => ( +
+ Pill (default) + Plain +
+ ), +} diff --git a/src/components/ui/__stories__/TabNav.stories.tsx b/src/components/ui/__stories__/TabNav.stories.tsx new file mode 100644 index 00000000000..9c293c5078a --- /dev/null +++ b/src/components/ui/__stories__/TabNav.stories.tsx @@ -0,0 +1,111 @@ +import { useState } from "react" +import { Boxes, GraduationCap, Layers, Users } from "lucide-react" +import type { Meta, StoryObj } from "@storybook/nextjs" + +import type { SectionNavDetails } from "@/lib/types" + +import TabNav, { StickyContainer } from "../TabNav" + +const meta = { + title: "UI / TabNav", + component: TabNav, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + description: { + component: + "URL-fragment-driven section navigation. Each entry in `sections` becomes a `ButtonLink` with `href = '#' + key` (or its own `href`). Active section is auto-detected from the URL hash via `useActiveHash`; pass `activeSection` + `onSelect` for controlled use. Wrap in `StickyContainer` for sticky behavior.", + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +const SECTIONS: SectionNavDetails[] = [ + { key: "overview", label: "Overview" }, + { key: "developers", label: "Developers" }, + { key: "community", label: "Community" }, + { key: "research", label: "Research" }, +] + +const SECTIONS_WITH_ICONS: SectionNavDetails[] = [ + { key: "layers", label: "Layers", icon: }, + { key: "apps", label: "Apps", icon: }, + { key: "learn", label: "Learn", icon: }, + { key: "community", label: "Community", icon: }, +] + +export const Default: Story = { + args: { sections: SECTIONS, activeSection: "overview" }, +} + +export const WithIcons: Story = { + args: { sections: SECTIONS_WITH_ICONS, activeSection: "layers" }, +} + +const ControlledTabs = () => { + const [active, setActive] = useState("developers") + return ( + + ) +} + +export const Controlled: Story = { + args: { sections: SECTIONS }, + parameters: { + docs: { + description: { + story: + "Pass `onSelect` to render `Button`s instead of `ButtonLink`s and drive the active state from React state.", + }, + }, + }, + render: () => , +} + +export const WithMotion: Story = { + args: { + sections: SECTIONS, + activeSection: "developers", + useMotion: true, + }, + parameters: { + docs: { + description: { + story: + "`useMotion` swaps the active-tab highlight for a Framer Motion `layoutId` so the highlight animates between tabs.", + }, + }, + }, +} + +export const Sticky: Story = { + args: { sections: SECTIONS, activeSection: "overview" }, + parameters: { + docs: { + description: { + story: + "Wrap in `StickyContainer` to make the nav stick at `top-20` while the page scrolls.", + }, + }, + }, + render: (args) => ( +
+ + + +
+

+ Scroll to see the nav stay pinned. The actual active-section detection + is driven by the page URL hash. +

+ {Array.from({ length: 30 }).map((_, i) => ( +

Scrollable content row {i + 1}.

+ ))} +
+
+ ), +}