Skip to content

Commit 55e7538

Browse files
committed
Create Link typography component
1 parent 92706da commit 55e7538

3 files changed

Lines changed: 232 additions & 0 deletions

File tree

src/components/link/index.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as React from 'react'
2+
import { Slot } from '@radix-ui/react-slot'
3+
import { cva, type VariantProps } from 'class-variance-authority'
4+
5+
import { cn } from '../../lib/utils'
6+
7+
const linkVariants = cva(
8+
'leading-tight inline-flex items-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9+
{
10+
variants: {
11+
variant: {
12+
default: '',
13+
underline: 'underline underline-offset-4',
14+
hover: 'hover:underline hover:underline-offset-4',
15+
},
16+
textColor: {
17+
primary: 'text-primary hover:text-primary/80',
18+
inherit: 'text-inherit hover:opacity-80',
19+
},
20+
},
21+
defaultVariants: {
22+
variant: 'default',
23+
textColor: 'primary',
24+
},
25+
}
26+
)
27+
28+
export interface LinkProps
29+
extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
30+
VariantProps<typeof linkVariants> {
31+
asChild?: boolean
32+
}
33+
34+
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
35+
({ className, variant, textColor, asChild = false, ...props }, ref) => {
36+
const Comp = asChild ? Slot : 'a'
37+
return <Comp className={cn(linkVariants({ variant, textColor, className }))} ref={ref} {...props} />
38+
}
39+
)
40+
Link.displayName = 'Link'
41+
42+
export { Link, linkVariants }
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
import { RouterTabs, RouterTab } from '../../components/tabs/router-tabs'
3+
import { expect, within } from 'storybook/test'
4+
5+
// Mock router link component for demonstration
6+
const MockRouterLink = ({
7+
to,
8+
className,
9+
children,
10+
...props
11+
}: {
12+
to: string
13+
className?: string
14+
children: React.ReactNode
15+
}) => (
16+
<a href={to} className={className} {...props}>
17+
{children}
18+
</a>
19+
)
20+
21+
const meta: Meta<typeof RouterTabs> = {
22+
title: 'Components/RouterTabs',
23+
component: RouterTabs,
24+
parameters: {
25+
docs: {
26+
description: {
27+
component:
28+
'A router-aware tabs component that mimics MUI tabs functionality. Supports routing with custom router link components and scrollable variants.',
29+
},
30+
},
31+
layout: 'centered',
32+
},
33+
tags: ['autodocs'],
34+
argTypes: {
35+
value: {
36+
control: 'text',
37+
description: 'The currently active tab value',
38+
},
39+
variant: {
40+
control: 'select',
41+
options: ['default', 'scrollable'],
42+
description: 'The variant of the tabs',
43+
},
44+
scrollButtons: {
45+
control: 'boolean',
46+
description: 'Whether to show scroll buttons in scrollable variant',
47+
},
48+
},
49+
}
50+
51+
export default meta
52+
type Story = StoryObj<typeof meta>
53+
54+
// Sample tabs data similar to MUI example
55+
const tabs = [
56+
{ to: '/dashboard', label: 'Dashboard', visible: true },
57+
{ to: '/analytics', label: 'Analytics', visible: true },
58+
{ to: '/reports', label: 'Reports', visible: true },
59+
{ to: '/settings', label: 'Settings', visible: false },
60+
{ to: '/profile', label: 'Profile', visible: true },
61+
]
62+
63+
const targetTab = tabs.find(tab => tab.to === '/analytics')
64+
65+
export const Default: Story = {
66+
args: {
67+
value: targetTab?.to,
68+
children: tabs
69+
.filter(tab => tab === targetTab || tab.visible !== false)
70+
.map(tab => (
71+
<RouterTab key={tab.to} component={MockRouterLink} value={tab.to} label={tab.label} to={tab.to} />
72+
)),
73+
},
74+
play: async ({ canvasElement }) => {
75+
const canvas = within(canvasElement)
76+
const tabsList = canvas.getByRole('tablist')
77+
await expect(tabsList).toBeInTheDocument()
78+
},
79+
}
80+
81+
export const ScrollableVariant: Story = {
82+
args: {
83+
value: '/analytics',
84+
variant: 'scrollable',
85+
scrollButtons: false,
86+
children: [
87+
{ to: '/dashboard', label: 'Dashboard' },
88+
{ to: '/analytics', label: 'Analytics' },
89+
{ to: '/reports', label: 'Reports' },
90+
{ to: '/users', label: 'User Management' },
91+
{ to: '/billing', label: 'Billing & Payments' },
92+
{ to: '/integrations', label: 'Integrations' },
93+
{ to: '/security', label: 'Security Settings' },
94+
{ to: '/notifications', label: 'Notifications' },
95+
].map(tab => (
96+
<RouterTab key={tab.to} component={MockRouterLink} value={tab.to} label={tab.label} to={tab.to} />
97+
)),
98+
},
99+
}
100+
101+
export const WithButtonTabs: Story = {
102+
args: {
103+
value: '/dashboard',
104+
children: [
105+
<RouterTab key="/dashboard" value="/dashboard" label="Dashboard" to="/dashboard" />,
106+
<RouterTab key="/analytics" value="/analytics" label="Analytics" to="/analytics" />,
107+
<RouterTab key="/reports" value="/reports" label="Reports" to="/reports" />,
108+
],
109+
},
110+
}
111+
112+
export const MUILikeExample: Story = {
113+
name: 'MUI-like Implementation',
114+
args: {
115+
value: targetTab?.to,
116+
variant: 'scrollable',
117+
scrollButtons: false,
118+
children: tabs
119+
.filter(tab => tab === targetTab || tab.visible !== false)
120+
.map(tab => (
121+
<RouterTab key={tab.to} component={MockRouterLink} value={tab.to} label={tab.label} to={tab.to} />
122+
)),
123+
},
124+
parameters: {
125+
docs: {
126+
description: {
127+
story: `This example demonstrates how to replicate the MUI tabs pattern:
128+
129+
\`\`\`tsx
130+
<RouterTabs value={targetTab?.to} variant="scrollable" scrollButtons={false}>
131+
{tabs
132+
.filter(tab => tab === targetTab || tab.visible !== false)
133+
.map(tab => (
134+
<RouterTab
135+
key={tab.to}
136+
component={NonScrollingRouterLink}
137+
value={tab.to}
138+
label={tab.label}
139+
to={tab.to}
140+
/>
141+
))}
142+
</RouterTabs>
143+
\`\`\``,
144+
},
145+
},
146+
},
147+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { Meta, StoryObj } from '@storybook/react'
2+
import { Link } from '../../components/link'
3+
4+
const meta = {
5+
title: 'Typography/Link',
6+
component: Link,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
tags: ['autodocs'],
11+
argTypes: {
12+
variant: {
13+
control: 'select',
14+
options: ['default', 'underline', 'hover'],
15+
description: 'The visual style variant of the link',
16+
},
17+
textColor: {
18+
control: 'select',
19+
options: ['primary', 'inherit'],
20+
description: 'The text color scheme of the link',
21+
defaultValue: 'primary',
22+
},
23+
asChild: {
24+
control: 'boolean',
25+
description: 'When true, renders the child component with Link styles and props merged.',
26+
},
27+
className: {
28+
control: 'text',
29+
description: 'Additional CSS classes for custom styling',
30+
},
31+
},
32+
} satisfies Meta<typeof Link>
33+
34+
export default meta
35+
type Story = StoryObj<typeof meta>
36+
37+
export const Default: Story = {
38+
args: {
39+
children: 'Oasis Explorer',
40+
href: '#',
41+
textColor: 'primary',
42+
},
43+
}

0 commit comments

Comments
 (0)