Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { PhoneInput } from '@/components/PhoneInput';
import { Progress } from '@/components/Progress';
import { Radio } from '@/components/Radio';
import { Select } from '@/components/Select';
import { SegmentedNav } from '@/components/SegmentedNav';
import { Separator } from '@/components/Separator';
import { Sidebar } from '@/components/Sidebar';
import { Skeleton } from '@/components/Skeleton';
Expand Down Expand Up @@ -1708,6 +1709,78 @@ function DatePickerDemo() {
);
}

function SegmentedNavDemo({
ariaLabel,
items,
initialActive,
}: {
ariaLabel: string;
items: string[];
initialActive: string;
}) {
const [activeItem, setActiveItem] = React.useState(initialActive);

return (
<SegmentedNav aria-label={ariaLabel}>
{items.map((item) => (
<SegmentedNav.Link
key={item}
active={activeItem === item}
render={
<a
href="/"
onClick={(event) => {
event.preventDefault();
setActiveItem(item);
}}
/>
}
>
{item}
</SegmentedNav.Link>
))}
</SegmentedNav>
);
}

function GroupedSegmentedNavDemo({
ariaLabel,
groups,
initialActive,
}: {
ariaLabel: string;
groups: string[][];
initialActive: string;
}) {
const [activeItem, setActiveItem] = React.useState(initialActive);

return (
<SegmentedNav aria-label={ariaLabel}>
{groups.map((group, index) => (
<SegmentedNav.Group key={`group-${index}`}>
{group.map((item) => (
<SegmentedNav.Link
key={item}
active={activeItem === item}
render={
<a
href="/"
onClick={(event) => {
event.preventDefault();
setActiveItem(item);
}}
/>
}
>
{item}
</SegmentedNav.Link>
))}
</SegmentedNav.Group>
))}
</SegmentedNav>
);
}

export default function Home() {
return (
<main style={{ padding: '2rem', maxWidth: '600px' }}>
Expand Down Expand Up @@ -3478,6 +3551,39 @@ export default function Home() {
<Radio.Error match>Error text goes here.</Radio.Error>
</Radio.Field>
</div>
<h2 style={{ marginBottom: '1rem' }}>SegmentedNav Component</h2>

<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', marginBottom: '128px' }}>
<div>
<span style={{ fontSize: '14px', color: '#7c7c7c', marginBottom: '0.5rem', display: 'block' }}>Flat links</span>
<SegmentedNavDemo
ariaLabel="Payout sections"
items={['Overview', 'Activity', 'Recipients', 'Customers']}
initialActive="Activity"
/>
</div>

<div>
<span style={{ fontSize: '14px', color: '#7c7c7c', marginBottom: '0.5rem', display: 'block' }}>Grouped links</span>
<GroupedSegmentedNavDemo
ariaLabel="Grouped payout sections"
groups={[
['Overview', 'Platform payouts', 'Recipients'],
['Customer payouts'],
]}
initialActive="Platform payouts"
/>
</div>

<div>
<span style={{ fontSize: '14px', color: '#7c7c7c', marginBottom: '0.5rem', display: 'block' }}>Longer labels</span>
<SegmentedNavDemo
ariaLabel="Customer payout sections"
items={['Customer overview', 'Platform payouts', 'Reconciliation']}
initialActive="Platform payouts"
/>
</div>
</div>
<h2 style={{ marginBottom: '1rem' }}>Select Component</h2>

<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', marginBottom: '128px', maxWidth: '256px' }}>
Expand Down
82 changes: 82 additions & 0 deletions src/components/SegmentedNav/SegmentedNav.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
@use '../../tokens/text-styles' as *;
@use '../../tokens/mixins' as *;

.root {
display: inline-flex;
align-items: center;
}

.list {
display: inline-flex;
align-items: center;
padding: var(--spacing-3xs);
overflow: clip;
background: var(--surface-secondary);
@include smooth-corners(var(--corner-radius-sm));
}

.list > .link + .link {
margin-inline-start: var(--spacing-2xs);
}

.group {
position: relative;
display: inline-flex;
gap: var(--spacing-2xs);
align-items: center;
}

.group + .group {
margin-inline-start: var(--spacing-xs);
padding-inline-start: var(--spacing-xs);

&::before {
content: '';
position: absolute;
top: calc(-1 * var(--spacing-3xs));
bottom: calc(-1 * var(--spacing-3xs));
inset-inline-start: 0;
width: var(--stroke-xs);
background: var(--border-secondary);
}
}

.link {
@include label;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 32px;
padding: 0 var(--spacing-md);
border: var(--stroke-xs) solid transparent;
@include smooth-corners(var(--corner-radius-xs));
color: var(--text-primary);
text-decoration: none;
white-space: nowrap;
outline: none;
transition:
background-color 150ms ease,
border-color 150ms ease,
box-shadow 150ms ease,
color 150ms ease;

&[aria-current='page'] {
background: var(--surface-panel);
border-color: var(--border-primary);
box-shadow: var(--shadow-sm);
color: var(--text-primary);
}

&:focus-visible {
background: var(--surface-panel);
border-color: var(--border-secondary);
box-shadow: var(--input-focus);
color: var(--text-primary);
}
}

@media (prefers-reduced-motion: reduce) {
.link {
transition: none;
}
}
89 changes: 89 additions & 0 deletions src/components/SegmentedNav/SegmentedNav.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SegmentedNav } from './';

const meta = {
title: 'Components/SegmentedNav',
component: SegmentedNav,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
} satisfies Meta<typeof SegmentedNav>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: () => (
<SegmentedNav aria-label="Payout sections">
<SegmentedNav.Link render={<a href="/payouts" />}>
Overview
</SegmentedNav.Link>
<SegmentedNav.Link active render={<a href="/payouts/activity" />}>
Activity
</SegmentedNav.Link>
<SegmentedNav.Link render={<a href="/payouts/recipients" />}>
Recipients
</SegmentedNav.Link>
<SegmentedNav.Link render={<a href="/payouts/customers" />}>
Customers
</SegmentedNav.Link>
</SegmentedNav>
),
};

export const Grouped: Story = {
render: () => (
<SegmentedNav aria-label="Payout sections">
<SegmentedNav.Group>
<SegmentedNav.Link render={<a href="/payouts" />}>
Overview
</SegmentedNav.Link>
<SegmentedNav.Link active render={<a href="/payouts/activity" />}>
Platform payouts
</SegmentedNav.Link>
<SegmentedNav.Link render={<a href="/payouts/recipients" />}>
Recipients
</SegmentedNav.Link>
</SegmentedNav.Group>
<SegmentedNav.Group>
<SegmentedNav.Link render={<a href="/payouts/customers" />}>
Customer payouts
</SegmentedNav.Link>
</SegmentedNav.Group>
</SegmentedNav>
),
};

export const PlainAnchors: Story = {
render: () => (
<SegmentedNav aria-label="Balance sections">
<SegmentedNav.Link active render={<a href="/balances" />}>
Balances
</SegmentedNav.Link>
<SegmentedNav.Link render={<a href="/balances/activity" />}>
Activity
</SegmentedNav.Link>
<SegmentedNav.Link render={<a href="/balances/reports" />}>
Reports
</SegmentedNav.Link>
</SegmentedNav>
),
};

export const LongerLabels: Story = {
render: () => (
<SegmentedNav aria-label="Customer payout sections">
<SegmentedNav.Link render={<a href="/customers/overview" />}>
Customer overview
</SegmentedNav.Link>
<SegmentedNav.Link active render={<a href="/customers/platform-payouts" />}>
Platform payouts
</SegmentedNav.Link>
<SegmentedNav.Link render={<a href="/customers/reconciliation" />}>
Reconciliation
</SegmentedNav.Link>
</SegmentedNav>
),
};
Loading
Loading