feat(frontend): document typography tokens + Text component (#10132)

### Changes 🏗️

<img width="700" alt="Screenshot 2025-06-09 at 17 01 59"
src="https://github.com/user-attachments/assets/f2b0a3a6-fdf1-4e3e-9caa-d2bf03543dab"
/>

<img width="700" alt="Screenshot 2025-06-09 at 17 02 06"
src="https://github.com/user-attachments/assets/36e27a0b-07f2-4074-8628-cb236d75e4c4"
/>

This PR introduces a comprehensive Typography System for our design
system with improved documentation and developer experience [matching
what we have on
Figma](https://www.figma.com/design/nO9NFynNuicLtkiwvOxrbz/AutoGPT-Design-System?m=dev).

#### **Typography System**

- Created `<Text />` component
- Enforce its usage to ensure consistent typographic styles across the
app
```tsx
<Text variant="h1">Heading 1</Text>
<Text variant="h2">Heading 2</Text>
<Text variant="body">hello world</Text>
<Text variant="small">smol text</Text>
```
- Created `Typography.stories.tsx` on Storybook
- Complete typography overview with font showcases and usage guidelines

#### **Storybook Improvements**
- **Updated TypeScript docgen** configuration for better prop extraction
- **Cleaned up story rendering** to prevent MDX styling pollution
- **Split large story files** into focused, maintainable components

### Checklist 📋

#### For code changes:

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  
**Test Plan:**
- [x] Typography stories render correctly in Storybook
- [x] All Text component variants display properly
- [x] Interactive playground controls function correctly
- [x] No TypeScript or linting errors

---------

Co-authored-by: Swifty <craigswift13@gmail.com>
This commit is contained in:
Ubbe
2025-06-10 18:57:13 +04:00
committed by GitHub
parent 210d457ecd
commit 5385520c53
9 changed files with 475 additions and 8 deletions

View File

@@ -8,13 +8,14 @@ const config: StorybookConfig = {
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-docs",
],
features: {
experimentalRSC: true,
},
framework: {
name: "@storybook/nextjs",
options: {},
options: { builder: { useSWC: true } },
},
staticDirs: ["../public"],
};

View File

@@ -3,6 +3,15 @@ import type { Preview } from "@storybook/react";
import { initialize, mswLoader } from "msw-storybook-addon";
import "../src/app/globals.css";
import "../src/components/styles/fonts.css";
import {
Controls,
Description,
Primary,
Source,
Stories,
Subtitle,
Title,
} from "@storybook/blocks";
// Initialize MSW
initialize();
@@ -12,19 +21,26 @@ const preview: Preview = {
nextjs: {
appDirectory: true,
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
docs: {
page: () => (
<>
<Title />
<Subtitle />
<Description />
<Primary />
<Source />
<Stories />
<Controls />
</>
),
},
},
loaders: [mswLoader],
decorators: [
(Story) => (
<>
<div className="bg-background p-8">
<Story />
</>
</div>
),
],
};

View File

@@ -92,6 +92,7 @@
"@chromatic-com/storybook": "3.2.6",
"@playwright/test": "1.52.0",
"@storybook/addon-a11y": "8.6.14",
"@storybook/addon-docs": "8.6.14",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "8.6.14",

View File

@@ -207,6 +207,9 @@ importers:
'@storybook/addon-a11y':
specifier: 8.6.14
version: 8.6.14(storybook@8.6.14(prettier@3.5.3))
'@storybook/addon-docs':
specifier: 8.6.14
version: 8.6.14(@types/react@18.3.17)(storybook@8.6.14(prettier@3.5.3))
'@storybook/addon-essentials':
specifier: 8.6.14
version: 8.6.14(@types/react@18.3.17)(storybook@8.6.14(prettier@3.5.3))

View File

@@ -0,0 +1,169 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Text, textVariants, type TextVariant } from "./Text";
import { StoryCode } from "@/stories/helpers/StoryCode";
const meta: Meta<typeof Text> = {
title: "Design System/Atoms/Text",
component: Text,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
controls: { hideNoControlsWarning: true },
docs: {
description: {
component:
"A flexible Text component that supports all typography variants from our design system. Uses Poppins for headings and Geist Sans for body text.",
},
source: {
state: "open",
},
},
},
argTypes: {
variant: {
control: { type: "select" },
options: textVariants,
description: "Typography variant to apply",
},
as: {
control: { type: "select" },
options: ["h1", "h2", "h3", "h4", "h5", "h6", "p", "span", "div", "code"],
description: "HTML element to render as",
},
children: {
control: "text",
description: "Text content",
},
},
};
export default meta;
type Story = StoryObj<typeof Text>;
//=============================================================================
// All Variants Overview
//=============================================================================
export function AllVariants() {
return (
<div className="space-y-8">
{/* Headings */}
<div className="mb-19 mb-20 space-y-6">
<h2 className="mb-4 border-b border-border pb-2 text-xl text-zinc-500">
Headings
</h2>
<Text variant="h1">Heading 1</Text>
<Text variant="h2">Heading 2</Text>
<Text variant="h3">Heading 3</Text>
<Text variant="h4">Heading 4</Text>
<StoryCode
code={`<Text variant="h1">Heading 1</Text>
<Text variant="h2">Heading 2</Text>
<Text variant="h3">Heading 3</Text>
<Text variant="h4">Heading 4</Text>`}
/>
</div>
{/* Body Text */}
<h2 className="mb-4 border-b border-border pb-2 text-xl text-zinc-500">
Body Text
</h2>
<Text variant="lead">Lead</Text>
<StoryCode code={`<Text variant="lead">Lead</Text>`} />
<div className="flex flex-row gap-8">
<Text variant="large">Large</Text>
<Text variant="large-medium">Large Medium</Text>
<Text variant="large-semibold">Large Semibold</Text>
</div>
<StoryCode
code={`<Text variant="large">Large</Text>
<Text variant="large-medium">Large Medium</Text>
<Text variant="large-semibold">Large Semibold</Text>`}
/>
<div className="flex flex-row gap-8">
<Text variant="body">Body</Text>
<Text variant="body-medium">Body Medium</Text>
</div>
<StoryCode
code={`<Text variant="body">Body</Text>
<Text variant="body-medium">Body Medium</Text>`}
/>
<div className="flex flex-row gap-8">
<Text variant="small">Small</Text>
<Text variant="small-medium">Small Medium</Text>
</div>
<StoryCode
code={`<Text variant="small">Small</Text>
<Text variant="small-medium">Small Medium</Text>`}
/>
<Text variant="subtle">Subtle</Text>
<StoryCode code={`<Text variant="subtle">Subtle</Text>`} />
</div>
);
}
//=============================================================================
// Headings Only
//=============================================================================
export function Headings() {
return (
<div className="space-y-8">
<Text variant="h1">Heading 1</Text>
<Text variant="h2">Heading 2</Text>
<Text variant="h3">Heading 3</Text>
<Text variant="h4">Heading 4</Text>
</div>
);
}
//=============================================================================
// Body Text Only
//=============================================================================
export function BodyText() {
return (
<div className="space-y-8">
<Text variant="lead">Lead</Text>
<Text variant="large">Large</Text>
<Text variant="large-medium">Large Medium</Text>
<Text variant="large-semibold">Large Semibold</Text>
<Text variant="body">Body</Text>
<Text variant="body-medium">Body Medium</Text>
<Text variant="small">Small</Text>
<Text variant="small-medium">Small Medium</Text>
<Text variant="subtle">Subtle</Text>
</div>
);
}
//=============================================================================
// Interactive Playground
//=============================================================================
export const Playground: Story = {
args: {
variant: "body",
children:
"Edit this text and try different variants via the controls below",
},
parameters: {
controls: { include: ["variant", "as", "children"] },
},
render: (args) => (
<div className="space-y-8">
<Text {...args} />
</div>
),
};
//=============================================================================
// Custom Element
//=============================================================================
export function CustomElement() {
return (
<div className="space-y-8">
<Text variant="h3" as="div">
H3 size rendered as div
</Text>
<Text variant="body" as="h2">
Body size rendered as h2
</Text>
</div>
);
}

View File

@@ -0,0 +1,37 @@
import React from "react";
import { As, Variant, variantElementMap, variants } from "./helpers";
type CustomProps = {
variant: Variant;
as?: As;
className?: string;
};
export type TextProps = React.PropsWithChildren<
CustomProps & React.ComponentPropsWithoutRef<"p">
>;
export function Text({
children,
variant,
as: outerAs,
className = "",
...rest
}: TextProps) {
const variantClasses = variants[variant] || variants.body;
const Element = outerAs || variantElementMap[variant];
const combinedClassName = `${variantClasses} ${className}`.trim();
return React.createElement(
Element,
{
className: combinedClassName,
...rest,
},
children,
);
}
// Export variant names for use in stories
export const textVariants = Object.keys(variants) as Variant[];
export type TextVariant = Variant;

View File

@@ -0,0 +1,53 @@
export type As =
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "p"
| "span"
| "div"
| "code"
| "label"
| "kbd";
export const variants = {
// Headings
h1: "font-poppins text-5xl font-semibold leading-[56px] text-zinc-800",
h2: "font-poppins text-4xl font-normal leading-[52px] text-zinc-800",
h3: "font-poppins text-3xl font-medium leading-10 text-zinc-800",
h4: "font-poppins text-base font-medium leading-normal text-zinc-800",
// Body Text
lead: "font-sans text-xl font-normal leading-loose text-muted-zinc-800",
large: "font-sans text-base font-normal leading-normal text-zinc-800",
"large-medium":
"font-sans text-base font-medium leading-normal text-zinc-800",
"large-semibold":
"font-sans text-base font-semibold leading-normal text-zinc-800",
body: "font-sans text-sm font-normal leading-snug text-zinc-800",
"body-medium": "font-sans text-sm font-medium leading-snug text-zinc-800",
small: "font-sans text-xs font-normal leading-tight text-zinc-800",
"small-medium": "font-sans text-xs font-medium leading-tight text-zinc-800",
subtle:
"font-sans text-xs font-medium uppercase leading-tight tracking-wide text-zinc-800",
} as const;
export type Variant = keyof typeof variants;
export const variantElementMap: Record<Variant, As> = {
h1: "h1",
h2: "h2",
h3: "h3",
h4: "h4",
lead: "p",
large: "p",
"large-medium": "p",
"large-semibold": "p",
body: "p",
"body-medium": "p",
small: "p",
"small-medium": "p",
subtle: "p",
};

View File

@@ -0,0 +1,176 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Text } from "@/components/_new/Text/Text";
import { StoryCode } from "@/stories/helpers/StoryCode";
const meta: Meta<typeof Text> = {
title: "Design System/ Tokens /Typography",
component: Text,
parameters: {
layout: "fullscreen",
},
};
export default meta;
export function AllVariants() {
return (
<div className="space-y-12">
{/* Typography System Documentation */}
<div className="space-y-8">
<div>
<h1 className="mb-4 text-4xl font-bold text-zinc-800">
Typography System
</h1>
<p className="text-lg leading-relaxed text-zinc-600">
Our typography system uses two carefully selected fonts to create a
clear hierarchy and excellent readability across all interfaces.
</p>
</div>
<div className="grid gap-8 md:grid-cols-2">
<div>
<h2 className="mb-4 text-2xl font-semibold text-zinc-800">
Font Families
</h2>
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold text-zinc-800">
<a
href="https://fonts.google.com/specimen/Poppins"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Poppins
</a>
</h3>
<p className="mb-2 text-sm text-zinc-600">
Used for all headings and display text
</p>
<div className="font-poppins text-2xl text-zinc-800">
The quick brown fox
</div>
</div>
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold text-zinc-800">
<a
href="https://github.com/vercel/geist-font"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Geist Sans
</a>
</h3>
<p className="mb-2 text-sm text-zinc-600">
Used for all body text, labels, and UI elements
</p>
<div className="font-sans text-base text-zinc-800">
The quick brown fox jumps over the lazy dog
</div>
</div>
</div>
</div>
<div>
<h2 className="mb-4 text-2xl font-semibold text-zinc-800">FAQ</h2>
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-4">
<h3 className="mb-2 font-semibold text-zinc-800">
🤔 Why can&apos;t I use &lt;p&gt; tags directly?
</h3>
<div className="space-y-3 text-zinc-600">
<p className="text-sm">
Always use the{" "}
<code className="rounded bg-gray-100 px-2 py-1 text-xs">
&lt;Text /&gt;
</code>{" "}
component instead of plain HTML elements like{" "}
<code className="rounded bg-gray-100 px-2 py-1 text-xs">
&lt;h1&gt;
</code>
,{" "}
<code className="rounded bg-gray-100 px-2 py-1 text-xs">
&lt;p&gt;
</code>
,{" "}
<code className="rounded bg-gray-100 px-2 py-1 text-xs">
&lt;span&gt;
</code>
, etc... Reasons:
</p>
<ul className="ml-4 list-inside list-disc space-y-1 text-sm">
<li>Ensures consistent typography across the entire app</li>
<li>
Makes future design updates easier (change once, update
everywhere)
</li>
<li>Provides TypeScript safety for typography variants</li>
<li>
Automatically maps to correct HTML elements for
accessibility
</li>
<li>Prevents styling inconsistencies and design drift</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Typography Examples */}
<div className="space-y-8">
<div className="mb-19 mb-20 space-y-6">
<h2 className="mb-4 border-b border-border pb-2 text-xl text-zinc-500">
Headings (Poppins)
</h2>
<Text variant="h1">Heading 1</Text>
<Text variant="h2">Heading 2</Text>
<Text variant="h3">Heading 3</Text>
<Text variant="h4">Heading 4</Text>
<StoryCode
code={`<Text variant="h1">Heading 1</Text>
<Text variant="h2">Heading 2</Text>
<Text variant="h3">Heading 3</Text>
<Text variant="h4">Heading 4</Text>`}
/>
</div>
<h2 className="mb-4 border-b border-border pb-2 text-xl text-zinc-500">
Body Text (Geist Sans)
</h2>
<Text variant="lead">Lead</Text>
<StoryCode code="<Text variant='lead'>Lead</Text>" />
<div className="flex flex-row gap-8">
<Text variant="large">Large</Text>
<Text variant="large-medium">Large Medium</Text>
<Text variant="large-semibold">Large Semibold</Text>
</div>
<StoryCode
code={`<Text variant="large">Large</Text>
<Text variant="large-medium">Large Medium</Text>
<Text variant="large-semibold">Large Semibold</Text>`}
/>
<div className="flex flex-row gap-8">
<Text variant="body">Body</Text>
<Text variant="body-medium">Body Medium</Text>
</div>
<StoryCode
code={`<Text variant="body">Body</Text>
<Text variant="body-medium">Body Medium</Text>`}
/>
<div className="flex flex-row gap-8">
<Text variant="small">Small</Text>
<Text variant="small-medium">Small Medium</Text>
</div>
<StoryCode
code={`<Text variant="small">Small</Text>
<Text variant="small-medium">Small Medium</Text>`}
/>
<Text variant="subtle">Subtle</Text>
<StoryCode code={`<Text variant="subtle">Subtle</Text>`} />
</div>
</div>
);
}

View File

@@ -0,0 +1,11 @@
type Props = {
code: string;
};
export function StoryCode(props: Props) {
return (
<pre className="block rounded border bg-zinc-100 px-3 py-2 font-mono text-xs text-indigo-800 shadow-sm">
{props.code}
</pre>
);
}