@swisper.ai/design-system 0.0.2 → 0.0.4
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.
- package/README.md +6 -0
- package/dist/css/components.css +79 -79
- package/dist/css/debug-tokens.json +99 -99
- package/dist/css/semantic.css +20 -20
- package/dist/tailwind.css +1 -1
- package/package.json +1 -1
- package/src/colors.stories.tsx +118 -25
- package/src/components.stories.tsx +142 -9
- package/src/docs/docs-section.tsx +58 -0
- package/src/docs/link-card.tsx +1 -1
- package/src/docs/token-scale.tsx +21 -0
- package/src/foundation.stories.tsx +201 -11
- package/src/icons.stories.tsx +267 -0
- package/src/introduction.mdx +3 -3
- package/src/tailwind.css +5 -0
package/src/colors.stories.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
77
|
+
setIsOpen(true);
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
setCopied(false);
|
|
80
|
+
setIsOpen(false);
|
|
81
|
+
}, 1000);
|
|
56
82
|
};
|
|
57
83
|
|
|
58
84
|
return (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
</
|
|
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
|
-
|
|
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
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
package/src/docs/link-card.tsx
CHANGED
|
@@ -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--
|
|
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
|
-
|
|
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
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
};
|