diff --git a/src/components/atoms/link/Link.stories.tsx b/src/components/atoms/link/Link.stories.tsx index a28affef..c34ae50f 100644 --- a/src/components/atoms/link/Link.stories.tsx +++ b/src/components/atoms/link/Link.stories.tsx @@ -1,181 +1,250 @@ +import { action } from '@storybook/addon-actions'; import type { Meta, StoryObj } from '@storybook/react'; -import { fn } from '@storybook/test'; -import Link from './Link'; +import type { ReactNode } from 'react'; +import { Link } from './Link'; + +const StoryCanvas = ({ children }: { children: ReactNode }) => ( +
{children}
+); + +const VariantRow = ({ children }: { children: ReactNode }) => ( +
{children}
+); /** + * ## Description + * Link renders navigation and action affordances with Stack-and-Flow typography, focus, and token styling. * - * ## DESCRIPTION - * Link component for navigation and actions. - * - * ## DEPENDENCIES - * - Icon: Uses Icon component from `lucide-react` for icons. + * ## Dependencies + * Uses `lucide-react/dynamic` when the optional `icon` prop is provided. * + * ## Usage Guide + * Use `regular` for inline navigation, `outlined` for secondary call-to-action links, and `button` when a link must visually match a primary action while preserving link semantics when `href` is present. When `href` is omitted, Link behaves as a local action control with button semantics and keyboard activation. */ - const meta: Meta = { title: 'Atoms/Link', component: Link, parameters: { + layout: 'fullscreen', docs: { autodocs: true } }, tags: ['autodocs'] }; + export default meta; type Story = StoryObj; +/** + * Shows the default inline link for body copy, release notes, and low-emphasis navigation. + */ export const Default: Story = { args: { - variant: 'regular', - href: '', - target: '_blank', - size: 'md', - children: 'Lorem ipsum', - className: '', - title: '' - } + children: 'Read the release notes', + href: 'https://github.com/egdev6' + }, + render: (args) => ( + +

+ Follow the product update in for migration notes and release context. +

+
+ ) }; /** - * - Regular: Default link style. - * - ⚠️ This variant uses a transparent background by default. To ensure proper visibility, place it inside a container with a solid or plain background. + * Shows the three visual hierarchy levels: inline, secondary CTA, and primary CTA. */ - -export const Regular: Story = { +export const Variants: Story = { render: () => ( -
- - Lorem Ipsum - - - Lorem Ipsum - - - Lorem Ipsum - -
+ + + Inline link + + Secondary CTA + + + Primary CTA + + + ) }; /** - * - Outlined: Link with outlined style. - * - ⚠️ This variant uses a transparent background by default. To ensure proper visibility, place it inside a container with a solid or plain background. + * Shows CTA-style links with their default glow and with glow disabled for quieter layouts. + * Regular inline links stay glowless regardless of this option. */ - -export const Outlined: Story = { +export const Shadow: Story = { render: () => ( -
- - Lorem Ipsum - - - Lorem Ipsum - - - Lorem Ipsum - -
+ +
+ + + Outlined with glow + + + Outlined without glow + + + + + Primary with glow + + + Primary without glow + + +
+
) }; + /** - * - Button: Link styled as a button. + * Shows how size changes typography and horizontal rhythm while preserving each variant treatment. */ - -export const Button: Story = { +export const Sizes: Story = { render: () => ( -
- - Lorem Ipsum - - - Lorem Ipsum - - - Lorem Ipsum - -
+ +
+ + + Small inline + + + Medium inline + + + Large inline + + + + + Small outlined + + + Medium outlined + + + Large outlined + + + + + Small button + + + Medium button + + + Large button + + +
+
) }; /** - * - With Icon: Link with an icon. + * Shows icons inheriting current text color and keeping alignment consistent across variants. */ - export const WithIcon: Story = { render: () => ( -
- - Lorem Ipsum - - - Lorem Ipsum - - - Lorem Ipsum - -
+ + + + External link + + + Continue + + + Download + + + ) }; /** - * - You can change the target of the link to open in a new tab or the same tab. - * - Use the `target` prop to specify the link behavior. + * Shows disabled links preserving their color treatment while reducing affordance through opacity and cursor state. */ -export const Target: Story = { +export const Disabled: Story = { render: () => ( -
- - Open in new tab - - - Open in same tab - - - Open in parent frame - - - Open in top frame - -
+ + + + Inline disabled + + + Outlined disabled + + + Button disabled + + + ) }; /** - * - Accessibility: The `title` prop is used to provide a tooltip or description for the link. - * - Use the `title` attribute to provide additional context for screen readers. + * Shows target values changing navigation behavior while preserving accessible link semantics. */ +export const Target: Story = { + render: () => ( + + + + New tab + + + Same tab + + + Parent frame + + + Top frame + + + + ) +}; +/** + * Shows title-backed accessible labels when visible text alone is not enough. + */ export const Accessibility: Story = { render: () => ( -
- - GitHub Profile - -
+ + + + GitHub profile + + + Documentation + + + ) }; /** - * - Custom Class: You can add custom classes to the link for additional styling. - * - You need to override the default classes using `!important` to ensure your styles take precedence. + * Shows local action behavior when no `href` is provided; these controls expose button semantics and support pointer plus keyboard activation. */ - -export const CustomClass: Story = { +export const ActionSemantics: Story = { render: () => ( -
- - Custom Class Link - - - Custom Class Link - -
+ + + + Run local action + + + Secondary action + + + ) }; diff --git a/src/components/atoms/link/Link.test.tsx b/src/components/atoms/link/Link.test.tsx new file mode 100644 index 00000000..c87b626e --- /dev/null +++ b/src/components/atoms/link/Link.test.tsx @@ -0,0 +1,271 @@ +/** + * Link.test.tsx — behavior tests for Stack-and-Flow Design System + * + * Strategy: + * - Hook (useLink): tested with renderHook for derived attributes and state. + * - Component (Link): tested with render/screen/userEvent for observable behavior. + * + * We test behavior, not internal CSS class strings. + */ + +import { render, renderHook, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; + +// --- Mocks (declared before component import) --- + +vi.mock('lucide-react/dynamic', () => ({ + // biome-ignore lint/style/useNamingConvention: must match library export name + DynamicIcon: ({ name }: { name: string }) =>