@swisper.ai/design-system 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,29 @@
1
1
  import * as React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
 
4
+ import { SearchIcon } from '@swisper.ai/icons';
5
+ import {
6
+ Button,
7
+ EmptyState,
8
+ EmptyStateActionRow,
9
+ EmptyStateDescription,
10
+ EmptyStateIcon,
11
+ EmptyStateTitle,
12
+ TextField,
13
+ Tooltip,
14
+ TooltipProvider,
15
+ Typography,
16
+ } from '@swisper.ai/ui';
17
+
4
18
  import semanticCss from '../dist/css/semantic.css?raw';
5
19
 
20
+ import { DocsPage } from './docs/docs-section';
21
+
6
22
  const meta: Meta = {
7
23
  title: 'Design System/Colors',
8
24
  parameters: {
9
25
  layout: 'padded',
10
26
  },
11
- tags: ['autodocs'],
12
27
  };
13
28
 
14
29
  export default meta;
@@ -44,43 +59,121 @@ function parseSemanticColors(css: string): Map<string, ColorToken[]> {
44
59
 
45
60
  const SEMANTIC_COLORS = parseSemanticColors(semanticCss);
46
61
 
62
+ const TOKEN_COUNT = [...SEMANTIC_COLORS.values()].reduce((sum, tokens) => sum + tokens.length, 0);
63
+
47
64
  // Every `--st-colour-<name>` is exposed in the Tailwind theme as `--color-<name>`.
65
+ function classOf(name: string): string {
66
+ return `bg-${name.replace('--st-colour-', '')}`;
67
+ }
68
+
48
69
  function ColorBox({ name, value }: ColorToken) {
49
70
  const [copied, setCopied] = React.useState(false);
50
- const className = `bg-${name.replace('--st-colour-', '')}`;
71
+ const [isOpen, setIsOpen] = React.useState(false);
72
+ const className = classOf(name);
51
73
 
52
74
  const copy = async () => {
53
75
  await navigator.clipboard.writeText(className);
54
76
  setCopied(true);
55
- setTimeout(() => setCopied(false), 1000);
77
+ setIsOpen(true);
78
+ setTimeout(() => {
79
+ setCopied(false);
80
+ setIsOpen(false);
81
+ }, 1000);
56
82
  };
57
83
 
58
84
  return (
59
- <button
60
- type="button"
61
- onClick={copy}
62
- title={copied ? 'Copied!' : `${className} (${value})`}
63
- aria-label={`Copy ${className}`}
64
- className={`h-24 w-24 cursor-pointer rounded-full border ${copied ? 'ring-2 ring-offset-2' : ''}`}
65
- style={{ backgroundColor: value }}
66
- />
85
+ // Pressing the trigger closes a tooltip, so it is held open while it says "Copied".
86
+ <Tooltip
87
+ open={isOpen}
88
+ onOpenChange={(next) => setIsOpen(next || copied)}
89
+ tooltip={copied ? 'Copied' : `${className} · ${value}`}
90
+ asChild
91
+ >
92
+ <button
93
+ type="button"
94
+ onClick={copy}
95
+ aria-label={`Copy ${className}`}
96
+ className={`size-[36px] cursor-pointer rounded-full border border-divider-border-base focus-visible:outline-2 focus-visible:outline-offset-2 ${copied ? 'ring-2 ring-offset-2' : ''}`}
97
+ style={{ backgroundColor: value }}
98
+ />
99
+ </Tooltip>
67
100
  );
68
101
  }
69
102
 
70
- export const Semantic: Story = {
71
- render: () => (
72
- <div className="space-y-16">
73
- <h1 className="text-2xl font-bold">Colors</h1>
74
- {[...SEMANTIC_COLORS].map(([group, tokens]) => (
75
- <section key={group} className="space-y-8">
76
- <h2 className="text-lg font-semibold">{group}</h2>
77
- <div className="flex flex-wrap gap-6">
78
- {tokens.map((token) => (
79
- <ColorBox key={token.name} {...token} />
103
+ function ColorBrowser() {
104
+ const [query, setQuery] = React.useState('');
105
+
106
+ const needle = query.trim().toLowerCase();
107
+ const groups = [...SEMANTIC_COLORS]
108
+ .map(([group, tokens]) => ({
109
+ group,
110
+ tokens: tokens.filter(
111
+ ({ name, value }) =>
112
+ classOf(name).toLowerCase().includes(needle) || value.toLowerCase().includes(needle),
113
+ ),
114
+ }))
115
+ .filter(({ tokens }) => tokens.length > 0);
116
+ const matchCount = groups.reduce((sum, { tokens }) => sum + tokens.length, 0);
117
+
118
+ return (
119
+ <TooltipProvider>
120
+ <DocsPage
121
+ title="Colors"
122
+ lead="The semantic colour tokens. Search by name or value, then select a colour to copy its Tailwind class."
123
+ >
124
+ <div className="space-y-16">
125
+ <TextField
126
+ type="search"
127
+ size="lg"
128
+ aria-label="Search colors"
129
+ placeholder="Search by name or value"
130
+ startIcon={<SearchIcon />}
131
+ value={query}
132
+ onChange={(event) => setQuery(event.target.value)}
133
+ />
134
+ <Typography role="status" className="text-text-secondary">
135
+ {needle
136
+ ? `${matchCount} matching ${matchCount === 1 ? 'result' : 'results'}`
137
+ : `${TOKEN_COUNT} colors`}
138
+ </Typography>
139
+ </div>
140
+
141
+ {groups.length === 0 ? (
142
+ <EmptyState>
143
+ <EmptyStateIcon>
144
+ <SearchIcon />
145
+ </EmptyStateIcon>
146
+ <EmptyStateTitle>No colors match “{query.trim()}”</EmptyStateTitle>
147
+ <EmptyStateDescription>
148
+ Check the spelling, or search for part of a name such as “feedback”.
149
+ </EmptyStateDescription>
150
+ <EmptyStateActionRow>
151
+ <Button variant="tertiary" onClick={() => setQuery('')}>
152
+ Clear search
153
+ </Button>
154
+ </EmptyStateActionRow>
155
+ </EmptyState>
156
+ ) : (
157
+ <div className="space-y-24">
158
+ {groups.map(({ group, tokens }) => (
159
+ <section key={group} className="space-y-12">
160
+ <Typography variant="title-2x-small">{group}</Typography>
161
+ <div className="flex flex-wrap gap-8">
162
+ {tokens.map((token) => (
163
+ <ColorBox key={token.name} {...token} />
164
+ ))}
165
+ </div>
166
+ </section>
80
167
  ))}
81
168
  </div>
82
- </section>
83
- ))}
84
- </div>
85
- ),
169
+ )}
170
+ </DocsPage>
171
+ </TooltipProvider>
172
+ );
173
+ }
174
+
175
+ export const Default: Story = {
176
+ // Named after the page, so the sidebar shows one entry instead of a folder with one story.
177
+ name: 'Colors',
178
+ render: () => <ColorBrowser />,
86
179
  };
@@ -1,5 +1,16 @@
1
1
  import * as React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import {
4
+ DashboardIcon,
5
+ DeployedCodeIcon,
6
+ DescriptionIcon,
7
+ MapIcon,
8
+ NewChatIcon,
9
+ } from '@swisper.ai/icons';
10
+ import { Typography } from '@swisper.ai/ui';
11
+
12
+ import { Code, CodeBlock, DocsPage, DocsSection } from './docs/docs-section';
13
+ import { LinkCard } from './docs/link-card';
3
14
 
4
15
  const meta: Meta = {
5
16
  title: 'Components',
@@ -12,15 +23,137 @@ const meta: Meta = {
12
23
  export default meta;
13
24
  type Story = StoryObj;
14
25
 
15
- export const Placeholder: Story = {
26
+ const SECTIONS = [
27
+ {
28
+ icon: <DeployedCodeIcon />,
29
+ title: 'Components',
30
+ subtitle: '@swisper.ai/ui: buttons, forms, overlays, lists, tables and layout',
31
+ storyId: 'components-button--docs',
32
+ },
33
+ {
34
+ icon: <NewChatIcon />,
35
+ title: 'Chat',
36
+ subtitle: '@swisper.ai/web-chat-ui: messages, the composer and chat cards',
37
+ storyId: 'chat-overview--docs',
38
+ },
39
+ {
40
+ icon: <DashboardIcon />,
41
+ title: 'Cards',
42
+ subtitle: '@swisper.ai/cards-ui: card components built on ui',
43
+ storyId: 'cards-card--docs',
44
+ },
45
+ {
46
+ icon: <DescriptionIcon />,
47
+ title: 'Files',
48
+ subtitle:
49
+ '@swisper.ai/web-file-preview: PDF, Office, CSV, image, audio and video in the browser',
50
+ storyId: 'files-filepreview--docs',
51
+ },
52
+ {
53
+ icon: <MapIcon />,
54
+ title: 'Map',
55
+ subtitle: '@swisper.ai/web-map: MapLibre maps with selectable markers',
56
+ storyId: 'map-mapview--docs',
57
+ },
58
+ ];
59
+
60
+ const INSTALL = `npm install @swisper.ai/ui`;
61
+
62
+ const SETUP = `import '@swisper.ai/ui/index.css';
63
+
64
+ import { Button, Tooltip, TooltipProvider } from '@swisper.ai/ui';
65
+ import { createRoot } from 'react-dom/client';
66
+
67
+ function App() {
68
+ return (
69
+ <TooltipProvider>
70
+ <Tooltip tooltip="Saves the draft without sending it">
71
+ <Button>Save draft</Button>
72
+ </Tooltip>
73
+ </TooltipProvider>
74
+ );
75
+ }
76
+
77
+ createRoot(document.getElementById('root')!).render(<App />);`;
78
+
79
+ const OWN_TAILWIND = `@import 'tw-animate-css';
80
+ @import '@swisper.ai/design-system/src/tailwind.css';
81
+ @source '../node_modules/@swisper.ai/ui/src';`;
82
+
83
+ export const Default: Story = {
16
84
  render: () => (
17
- <div className="space-y-16">
18
- <h1 className="text-2xl font-bold">Components</h1>
19
- <div className="space-y-8">
20
- <p className="text-body">
21
- This section will contain reusable UI components built on top of the foundation elements.
22
- </p>
23
- </div>
24
- </div>
85
+ <DocsPage
86
+ title="Components"
87
+ lead="Accessible React components styled with the Echo tokens. The general-purpose ones are in @swisper.ai/ui; chat, cards, file preview and maps each have their own package built on it."
88
+ >
89
+ <DocsSection title="Browse">
90
+ <div className="grid grid-cols-1 gap-24 sm:grid-cols-2">
91
+ {SECTIONS.map((section) => (
92
+ <LinkCard key={section.storyId} {...section} />
93
+ ))}
94
+ </div>
95
+ </DocsSection>
96
+
97
+ <DocsSection title="Install and set up">
98
+ <Typography>
99
+ Install the package, then load its stylesheet once in the app's entry file, before your
100
+ own CSS. React, MUI and Emotion are peer dependencies.
101
+ </Typography>
102
+ <CodeBlock>{INSTALL}</CodeBlock>
103
+ <Typography>
104
+ Wrap the app in <Code>TooltipProvider</Code> if you use tooltips, and render a component:
105
+ </Typography>
106
+ <CodeBlock>{SETUP}</CodeBlock>
107
+ <Typography>
108
+ If the app compiles its own Tailwind v4, build the Echo classes into it instead of loading{' '}
109
+ <Code>index.css</Code>. Install <Code>@swisper.ai/design-system</Code> and{' '}
110
+ <Code>tw-animate-css</Code>, then in your Tailwind entry:
111
+ </Typography>
112
+ <CodeBlock>{OWN_TAILWIND}</CodeBlock>
113
+ <Typography>
114
+ The other packages follow the same steps: install <Code>@swisper.ai/web-chat-ui</Code>,{' '}
115
+ <Code>cards-ui</Code>, <Code>web-file-preview</Code> or <Code>web-map</Code> next to{' '}
116
+ <Code>ui</Code> and load its <Code>index.css</Code> too. Each package's npm page lists its
117
+ peers and setup.
118
+ </Typography>
119
+ </DocsSection>
120
+
121
+ <DocsSection title="Reading a component page">
122
+ <Typography>Every component has the same entries in the sidebar, in this order:</Typography>
123
+ <ul className="list-disc space-y-8 pl-24">
124
+ <li>
125
+ <Typography asChild>
126
+ <span>
127
+ <strong>Docs</strong>: what the component is for, how its parts nest, when to use it
128
+ and when to pick another one, its accessibility contract and the tokens it reads.
129
+ </span>
130
+ </Typography>
131
+ </li>
132
+ <li>
133
+ <Typography asChild>
134
+ <span>
135
+ <strong>Default</strong>: the component as you get it with no props changed.
136
+ </span>
137
+ </Typography>
138
+ </li>
139
+ <li>
140
+ <Typography asChild>
141
+ <span>
142
+ <strong>Stories</strong>: one per variant, state or composition, with realistic
143
+ content. <strong>Examples</strong> shows the component inside the parents it is
144
+ meant for.
145
+ </span>
146
+ </Typography>
147
+ </li>
148
+ <li>
149
+ <Typography asChild>
150
+ <span>
151
+ <strong>Playground</strong>: every prop on the controls panel.
152
+ </span>
153
+ </Typography>
154
+ </li>
155
+ </ul>
156
+ </DocsSection>
157
+ </DocsPage>
25
158
  ),
26
159
  };
@@ -0,0 +1,58 @@
1
+ import * as React from 'react';
2
+
3
+ import { Typography } from '@swisper.ai/ui';
4
+
5
+ interface DocsPageProps {
6
+ title: string;
7
+ /** One or two sentences under the title that say what the page covers. */
8
+ lead: React.ReactNode;
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ /** Page frame for the hand-written overview stories: title, lead, then the sections. */
13
+ export function DocsPage({ title, lead, children }: DocsPageProps) {
14
+ return (
15
+ <div className="max-w-[960px] space-y-40">
16
+ <header className="space-y-8">
17
+ <Typography variant="title-large">{title}</Typography>
18
+ <Typography variant="body-large" className="text-text-secondary">
19
+ {lead}
20
+ </Typography>
21
+ </header>
22
+ {children}
23
+ </div>
24
+ );
25
+ }
26
+
27
+ interface DocsSectionProps {
28
+ title: string;
29
+ children: React.ReactNode;
30
+ }
31
+
32
+ /** One headed section of a `DocsPage`. */
33
+ export function DocsSection({ title, children }: DocsSectionProps) {
34
+ return (
35
+ <section className="space-y-16">
36
+ <Typography variant="title-x-small">{title}</Typography>
37
+ {children}
38
+ </section>
39
+ );
40
+ }
41
+
42
+ /** Inline code: a class, token or package name inside a sentence. */
43
+ export function Code({ children }: { children: React.ReactNode }) {
44
+ return (
45
+ <code className="rounded-[4px] bg-card-background-lvl-2 px-4 py-1 font-ibm-plex-mono text-[0.875em]">
46
+ {children}
47
+ </code>
48
+ );
49
+ }
50
+
51
+ /** A snippet to copy, shown as written. */
52
+ export function CodeBlock({ children }: { children: string }) {
53
+ return (
54
+ <pre className="overflow-x-auto rounded-[8px] bg-card-background-lvl-2 p-16 font-ibm-plex-mono text-[13px] leading-[1.6]">
55
+ <code>{children}</code>
56
+ </pre>
57
+ );
58
+ }
@@ -8,7 +8,7 @@ interface LinkCardProps {
8
8
  icon: React.ReactNode;
9
9
  title: string;
10
10
  subtitle: string;
11
- /** Storybook id of the page to open, e.g. `design-system-colors--hello-world`. */
11
+ /** Storybook id of the page to open, e.g. `design-system-colors--default`. */
12
12
  storyId: string;
13
13
  }
14
14
 
@@ -0,0 +1,21 @@
1
+ export interface Token {
2
+ /** The part of the variable name after the prefix: `8` for `--st-spacing-8`. */
3
+ name: string;
4
+ value: string;
5
+ }
6
+
7
+ /**
8
+ * Reads every `<prefix><name>: <value>;` declaration from a stylesheet's text, smallest value
9
+ * first. A token declared more than once (one per theme) keeps its last value.
10
+ */
11
+ export function parseTokens(css: string, prefix: string): Token[] {
12
+ const unique = new Map<string, string>();
13
+ const pattern = new RegExp(`${prefix}([\\w-]+):\\s*([^;]+);`, 'g');
14
+ for (const [, name, value] of css.matchAll(pattern)) {
15
+ unique.set(name, value.trim());
16
+ }
17
+
18
+ return [...unique]
19
+ .map(([name, value]) => ({ name, value }))
20
+ .sort((a, b) => parseFloat(a.value) - parseFloat(b.value));
21
+ }
@@ -1,27 +1,217 @@
1
1
  import * as React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { DashboardIcon, FormatColorIcon, LibraryIcon } from '@swisper.ai/icons';
4
+ import { Typography } from '@swisper.ai/ui';
5
+
6
+ import semanticCss from '../dist/css/semantic.css?raw';
7
+
8
+ import { Code, CodeBlock, DocsPage, DocsSection } from './docs/docs-section';
9
+ import { LinkCard } from './docs/link-card';
10
+ import { parseTokens } from './docs/token-scale';
11
+ import tailwindCss from './tailwind.css?raw';
3
12
 
4
13
  const meta: Meta = {
5
14
  title: 'Design System/Foundation',
6
15
  parameters: {
7
16
  layout: 'padded',
8
17
  },
9
- tags: ['autodocs'],
10
18
  };
11
19
 
12
20
  export default meta;
13
21
  type Story = StoryObj;
14
22
 
15
- export const Placeholder: Story = {
23
+ // The numbered steps only: `--st-spacing-neg-*` are offsets, not part of the class scale.
24
+ const SPACING = parseTokens(semanticCss, '--st-spacing-').filter(({ name }) => /^\d+$/.test(name));
25
+ // The named steps; the numbered `--st-border-radius-<px>` tokens are the same values again.
26
+ const RADII = parseTokens(semanticCss, '--st-border-radius-').filter(
27
+ ({ name }) => !/^\d+$/.test(name),
28
+ );
29
+ const BREAKPOINTS = parseTokens(tailwindCss, '--breakpoint-');
30
+ const FONTS = parseTokens(semanticCss, '--st-font-family-')
31
+ .map(({ name, value }) => ({ name, family: value.replace(/^["']|["']$/g, '') }))
32
+ .sort((a, b) => a.name.localeCompare(b.name));
33
+
34
+ const LAYERS = [
35
+ {
36
+ prefix: '--pt-*',
37
+ name: 'Primitive',
38
+ description: 'The raw palette, dimensions and font values. Never used directly in a component.',
39
+ },
40
+ {
41
+ prefix: '--st-*',
42
+ name: 'Semantic',
43
+ description: 'What a value is for: a surface, a foreground, feedback, a spacing step.',
44
+ },
45
+ {
46
+ prefix: '--ct-*',
47
+ name: 'Component',
48
+ description:
49
+ 'One component part in one state: the padding of a small button, the border of a hovered chip.',
50
+ },
51
+ ];
52
+
53
+ const THEME_LIGHT = `<div data-theme="light" className="bg-layout-foreground-base">
54
+ <p className="text-layout-foreground-base theme-light:text-layout-foreground-inverse">
55
+ Readable on both surfaces
56
+ </p>
57
+ </div>`;
58
+
59
+ export const Default: Story = {
60
+ // Named after the page, so the sidebar shows one entry instead of a folder with one story.
61
+ name: 'Foundation',
16
62
  render: () => (
17
- <div className="space-y-16">
18
- <h1 className="text-2xl font-bold">Foundation</h1>
19
- <div className="space-y-8">
20
- <p className="text-body">
21
- This section will contain foundation elements such as colors, typography, spacing, and
22
- other design tokens.
23
- </p>
24
- </div>
25
- </div>
63
+ <DocsPage
64
+ title="Foundation"
65
+ lead="The design tokens every Echo component is built from. They are exported from Figma, shipped by @swisper.ai/design-system as CSS variables, and exposed as Tailwind classes."
66
+ >
67
+ <DocsSection title="Token layers">
68
+ <Typography>
69
+ Tokens come in three layers, each defined by the one before it. Write Tailwind classes,
70
+ not the variables: <Code>bg-layout-surface-base</Code> for a semantic colour,{' '}
71
+ <Code>rounded-button</Code> for a component radius.
72
+ </Typography>
73
+ <div className="grid grid-cols-1 gap-16 sm:grid-cols-3">
74
+ {LAYERS.map((layer) => (
75
+ <div
76
+ key={layer.prefix}
77
+ className="space-y-8 rounded-[8px] bg-card-background-lvl-2 p-16"
78
+ >
79
+ <Typography variant="title-2x-small">{layer.name}</Typography>
80
+ <Code>{layer.prefix}</Code>
81
+ <Typography variant="body-small-high" className="text-text-secondary">
82
+ {layer.description}
83
+ </Typography>
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </DocsSection>
88
+
89
+ <DocsSection title="Themes">
90
+ <Typography>
91
+ The tokens hold the dark values, and dark is the default. A light surface is marked with{' '}
92
+ <Code>data-theme="light"</Code>; inside it, the <Code>theme-light:</Code> variant applies
93
+ the classes that differ.
94
+ </Typography>
95
+ <CodeBlock>{THEME_LIGHT}</CodeBlock>
96
+ </DocsSection>
97
+
98
+ <DocsSection title="Spacing">
99
+ <Typography>
100
+ A number in a spacing class is pixels, not Tailwind's 4px steps: <Code>gap-8</Code> is 8px
101
+ and <Code>p-16</Code> is 16px. A number off this scale falls back to Tailwind's default,
102
+ so <Code>p-5</Code> is 20px; stay on the scale.
103
+ </Typography>
104
+ <div className="space-y-8">
105
+ {SPACING.map(({ name, value }) => (
106
+ <div key={name} className="flex items-center gap-16">
107
+ <Typography variant="label-sm" className="w-48 shrink-0 font-ibm-plex-mono">
108
+ {name}
109
+ </Typography>
110
+ <Typography variant="label-sm" className="w-48 shrink-0 text-text-secondary">
111
+ {value}
112
+ </Typography>
113
+ <div className="h-12 rounded-[2px] bg-brand-primary-500" style={{ width: value }} />
114
+ </div>
115
+ ))}
116
+ </div>
117
+ </DocsSection>
118
+
119
+ <DocsSection title="Radius">
120
+ <Typography>
121
+ The radius scale below is what component radii are picked from. In code, use the
122
+ component's own class, such as <Code>rounded-button</Code>,{' '}
123
+ <Code>rounded-card-lvl-1</Code> or <Code>rounded-dialog</Code>, so a change in Figma
124
+ reaches every instance.
125
+ </Typography>
126
+ <div className="flex flex-wrap gap-24">
127
+ {RADII.map(({ name, value }) => (
128
+ <div key={name} className="flex flex-col items-center gap-8">
129
+ <div className="size-[72px] bg-brand-primary-500" style={{ borderRadius: value }} />
130
+ <Typography variant="label-sm">{name}</Typography>
131
+ <Typography variant="label-xs" className="text-text-secondary">
132
+ {value}
133
+ </Typography>
134
+ </div>
135
+ ))}
136
+ </div>
137
+ </DocsSection>
138
+
139
+ <DocsSection title="Breakpoints">
140
+ <Typography>
141
+ Min-width breakpoints for the responsive variants: <Code>md:grid-cols-2</Code> applies
142
+ from 820px up.
143
+ </Typography>
144
+ <table className="w-full max-w-[420px] border-collapse text-left">
145
+ <thead>
146
+ <tr className="border-b border-divider-border-base">
147
+ <th className="py-8 pr-16">
148
+ <Typography variant="label-sm" className="text-text-secondary">
149
+ Variant
150
+ </Typography>
151
+ </th>
152
+ <th className="py-8">
153
+ <Typography variant="label-sm" className="text-text-secondary">
154
+ From
155
+ </Typography>
156
+ </th>
157
+ </tr>
158
+ </thead>
159
+ <tbody>
160
+ {BREAKPOINTS.map(({ name, value }) => (
161
+ <tr key={name} className="border-b border-divider-border-base">
162
+ <td className="py-8 pr-16">
163
+ <Code>{name}:</Code>
164
+ </td>
165
+ <td className="py-8">
166
+ <Typography variant="body-small-high">{value}</Typography>
167
+ </td>
168
+ </tr>
169
+ ))}
170
+ </tbody>
171
+ </table>
172
+ </DocsSection>
173
+
174
+ <DocsSection title="Fonts">
175
+ <Typography>
176
+ The package names the font families but ships no font files: load them in the app. Text
177
+ styles are applied with the Typography component, not with font classes.
178
+ </Typography>
179
+ <div className="space-y-16">
180
+ {FONTS.map(({ name, family }) => (
181
+ <div key={name} className="flex flex-wrap items-baseline gap-x-24 gap-y-4">
182
+ <Typography variant="label-sm" className="w-48 shrink-0 text-text-secondary">
183
+ {name}
184
+ </Typography>
185
+ <span className="text-[24px]" style={{ fontFamily: `"${family}"` }}>
186
+ {family}
187
+ </span>
188
+ </div>
189
+ ))}
190
+ </div>
191
+ </DocsSection>
192
+
193
+ <DocsSection title="More">
194
+ <div className="grid grid-cols-1 gap-24 sm:grid-cols-3">
195
+ <LinkCard
196
+ icon={<FormatColorIcon />}
197
+ title="Colors"
198
+ subtitle="Every semantic color token, click to copy its class"
199
+ storyId="design-system-colors--default"
200
+ />
201
+ <LinkCard
202
+ icon={<LibraryIcon />}
203
+ title="Typography"
204
+ subtitle="Font families, weights and text variants"
205
+ storyId="design-system-typography--all-variants"
206
+ />
207
+ <LinkCard
208
+ icon={<DashboardIcon />}
209
+ title="Icons"
210
+ subtitle="Browse and search the icon set"
211
+ storyId="design-system-icons--default"
212
+ />
213
+ </div>
214
+ </DocsSection>
215
+ </DocsPage>
26
216
  ),
27
217
  };