@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.
- 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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import * as icons from '@swisper.ai/icons';
|
|
4
|
+
import { CheckIcon, CopyIcon, SearchIcon } from '@swisper.ai/icons';
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogBody,
|
|
9
|
+
DialogClose,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogOverlay,
|
|
15
|
+
DialogRoot,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
EmptyState,
|
|
18
|
+
EmptyStateActionRow,
|
|
19
|
+
EmptyStateDescription,
|
|
20
|
+
EmptyStateIcon,
|
|
21
|
+
EmptyStateTitle,
|
|
22
|
+
TextField,
|
|
23
|
+
Typography,
|
|
24
|
+
VisuallyHidden,
|
|
25
|
+
} from '@swisper.ai/ui';
|
|
26
|
+
|
|
27
|
+
import { DocsPage } from './docs/docs-section';
|
|
28
|
+
|
|
29
|
+
const meta: Meta = {
|
|
30
|
+
title: 'Design System/Icons',
|
|
31
|
+
parameters: {
|
|
32
|
+
layout: 'padded',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj;
|
|
38
|
+
|
|
39
|
+
type IconComponent = React.ComponentType<{
|
|
40
|
+
fontSize?: 'small' | 'medium' | 'large' | 'inherit';
|
|
41
|
+
className?: string;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
interface IconEntry {
|
|
45
|
+
/** The export: `MapIcon`. */
|
|
46
|
+
exportName: string;
|
|
47
|
+
/** The export without its suffix, shown under the tile and matched by the search: `Map`. */
|
|
48
|
+
label: string;
|
|
49
|
+
Icon: IconComponent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ICONS: IconEntry[] = Object.entries(icons as Record<string, IconComponent>)
|
|
53
|
+
.filter(([exportName]) => exportName.endsWith('Icon'))
|
|
54
|
+
.map(([exportName, Icon]) => ({ exportName, label: exportName.replace(/Icon$/, ''), Icon }))
|
|
55
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
56
|
+
|
|
57
|
+
const SIZES = [
|
|
58
|
+
{ fontSize: 'small', px: 20 },
|
|
59
|
+
{ fontSize: 'medium', px: 24 },
|
|
60
|
+
{ fontSize: 'large', px: 35 },
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
// Each surface with the three text colours an icon usually takes on it.
|
|
64
|
+
const SURFACES = [
|
|
65
|
+
{
|
|
66
|
+
name: 'Dark surface',
|
|
67
|
+
className: 'bg-layout-background-base',
|
|
68
|
+
colors: [
|
|
69
|
+
'text-layout-foreground-base',
|
|
70
|
+
'text-layout-foreground-subtle',
|
|
71
|
+
'text-brand-primary-500',
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'Light surface',
|
|
76
|
+
className: 'bg-layout-foreground-base',
|
|
77
|
+
colors: [
|
|
78
|
+
'text-layout-foreground-inverse',
|
|
79
|
+
'text-layout-foreground-subtle',
|
|
80
|
+
'text-brand-primary-500',
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const CHECKERBOARD = {
|
|
86
|
+
backgroundImage:
|
|
87
|
+
'repeating-conic-gradient(var(--color-card-background-lvl-2) 0% 25%, transparent 0% 50%)',
|
|
88
|
+
backgroundSize: '24px 24px',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function ImportLine({ exportName }: { exportName: string }) {
|
|
92
|
+
const [copied, setCopied] = React.useState(false);
|
|
93
|
+
const code = `import { ${exportName} } from '@swisper.ai/icons';`;
|
|
94
|
+
|
|
95
|
+
const copy = async () => {
|
|
96
|
+
await navigator.clipboard.writeText(code);
|
|
97
|
+
setCopied(true);
|
|
98
|
+
setTimeout(() => setCopied(false), 1000);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={copy}
|
|
105
|
+
aria-label="Copy import"
|
|
106
|
+
className="flex w-full cursor-pointer items-center justify-between gap-16 rounded-[8px] bg-card-background-lvl-2 p-16 text-left focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
107
|
+
>
|
|
108
|
+
<code className="min-w-0 font-ibm-plex-mono text-[13px] break-all">{code}</code>
|
|
109
|
+
{copied ? <CheckIcon fontSize="small" /> : <CopyIcon fontSize="small" />}
|
|
110
|
+
<VisuallyHidden role="status">{copied ? 'Copied' : ''}</VisuallyHidden>
|
|
111
|
+
</button>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function IconDetails({ exportName, Icon }: IconEntry) {
|
|
116
|
+
const previewRef = React.useRef<HTMLDivElement>(null);
|
|
117
|
+
// Flags, logos and weather icons carry their own colours and ignore the text colour.
|
|
118
|
+
const [followsTextColor, setFollowsTextColor] = React.useState(true);
|
|
119
|
+
|
|
120
|
+
React.useLayoutEffect(() => {
|
|
121
|
+
setFollowsTextColor(
|
|
122
|
+
Boolean(previewRef.current?.querySelector('[fill="currentColor"], [stroke="currentColor"]')),
|
|
123
|
+
);
|
|
124
|
+
}, [exportName]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className="space-y-24">
|
|
128
|
+
<ImportLine exportName={exportName} />
|
|
129
|
+
<div className="grid grid-cols-1 gap-24 sm:grid-cols-2">
|
|
130
|
+
<div
|
|
131
|
+
ref={previewRef}
|
|
132
|
+
className="flex aspect-square items-center justify-center rounded-[8px] text-[144px]"
|
|
133
|
+
style={CHECKERBOARD}
|
|
134
|
+
>
|
|
135
|
+
<Icon fontSize="inherit" />
|
|
136
|
+
</div>
|
|
137
|
+
<div className="space-y-24">
|
|
138
|
+
<div className="flex items-end gap-24">
|
|
139
|
+
{SIZES.map(({ fontSize, px }) => (
|
|
140
|
+
<div key={fontSize} className="flex flex-col items-center gap-8">
|
|
141
|
+
<Icon fontSize={fontSize} />
|
|
142
|
+
<Typography variant="label-xs" className="text-text-secondary">
|
|
143
|
+
{fontSize} · {px}px
|
|
144
|
+
</Typography>
|
|
145
|
+
</div>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
{followsTextColor ? (
|
|
149
|
+
SURFACES.map((surface) => (
|
|
150
|
+
<div key={surface.name} className="space-y-8">
|
|
151
|
+
<Typography variant="label-xs" className="text-text-secondary">
|
|
152
|
+
{surface.name}
|
|
153
|
+
</Typography>
|
|
154
|
+
<div className={`flex gap-24 rounded-[8px] p-16 ${surface.className}`}>
|
|
155
|
+
{surface.colors.map((color) => (
|
|
156
|
+
<Icon key={color} className={color} />
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
))
|
|
161
|
+
) : (
|
|
162
|
+
<Typography variant="body-small-high" className="text-text-secondary">
|
|
163
|
+
This icon keeps its own colours, so the text colour around it has no effect.
|
|
164
|
+
</Typography>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function IconBrowser() {
|
|
173
|
+
const [query, setQuery] = React.useState('');
|
|
174
|
+
// Kept after the dialog closes so its content stays in place during the exit animation.
|
|
175
|
+
const [selected, setSelected] = React.useState<IconEntry | null>(null);
|
|
176
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
177
|
+
|
|
178
|
+
const needle = query.trim().toLowerCase();
|
|
179
|
+
const matches = ICONS.filter(({ label }) => label.toLowerCase().includes(needle));
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<DocsPage
|
|
183
|
+
title="Icons"
|
|
184
|
+
lead="Every icon in @swisper.ai/icons. Search by name, then select an icon to copy its import and see it at each size."
|
|
185
|
+
>
|
|
186
|
+
<div className="space-y-16">
|
|
187
|
+
<TextField
|
|
188
|
+
type="search"
|
|
189
|
+
size="lg"
|
|
190
|
+
aria-label="Search icons"
|
|
191
|
+
placeholder="Search icons"
|
|
192
|
+
startIcon={<SearchIcon />}
|
|
193
|
+
value={query}
|
|
194
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
195
|
+
/>
|
|
196
|
+
<Typography role="status" className="text-text-secondary">
|
|
197
|
+
{needle
|
|
198
|
+
? `${matches.length} matching ${matches.length === 1 ? 'result' : 'results'}`
|
|
199
|
+
: `${ICONS.length} icons`}
|
|
200
|
+
</Typography>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{matches.length === 0 ? (
|
|
204
|
+
<EmptyState>
|
|
205
|
+
<EmptyStateIcon>
|
|
206
|
+
<SearchIcon />
|
|
207
|
+
</EmptyStateIcon>
|
|
208
|
+
<EmptyStateTitle>No icons match “{query.trim()}”</EmptyStateTitle>
|
|
209
|
+
<EmptyStateDescription>Check the spelling or try a shorter word.</EmptyStateDescription>
|
|
210
|
+
<EmptyStateActionRow>
|
|
211
|
+
<Button variant="tertiary" onClick={() => setQuery('')}>
|
|
212
|
+
Clear search
|
|
213
|
+
</Button>
|
|
214
|
+
</EmptyStateActionRow>
|
|
215
|
+
</EmptyState>
|
|
216
|
+
) : (
|
|
217
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(112px,1fr))] gap-8">
|
|
218
|
+
{matches.map((entry) => (
|
|
219
|
+
<button
|
|
220
|
+
key={entry.exportName}
|
|
221
|
+
type="button"
|
|
222
|
+
title={entry.label}
|
|
223
|
+
onClick={() => {
|
|
224
|
+
setSelected(entry);
|
|
225
|
+
setIsOpen(true);
|
|
226
|
+
}}
|
|
227
|
+
className="flex cursor-pointer flex-col items-center gap-8 rounded-[8px] px-8 py-16 transition-colors hover:bg-card-background-lvl-2 focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
228
|
+
>
|
|
229
|
+
<entry.Icon fontSize="large" />
|
|
230
|
+
<Typography
|
|
231
|
+
variant="label-xs"
|
|
232
|
+
className="w-full truncate text-center text-text-secondary"
|
|
233
|
+
>
|
|
234
|
+
{entry.label}
|
|
235
|
+
</Typography>
|
|
236
|
+
</button>
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
|
|
241
|
+
<DialogRoot open={isOpen} onOpenChange={setIsOpen}>
|
|
242
|
+
<Dialog className="max-w-[640px]">
|
|
243
|
+
<DialogContent>
|
|
244
|
+
<DialogHeader>
|
|
245
|
+
<DialogTitle>{selected?.exportName}</DialogTitle>
|
|
246
|
+
<DialogClose />
|
|
247
|
+
</DialogHeader>
|
|
248
|
+
<DialogDescription>Select the import to copy it.</DialogDescription>
|
|
249
|
+
<DialogBody>{selected && <IconDetails {...selected} />}</DialogBody>
|
|
250
|
+
<DialogFooter>
|
|
251
|
+
<DialogClose asChild>
|
|
252
|
+
<Button variant="tertiary">Close</Button>
|
|
253
|
+
</DialogClose>
|
|
254
|
+
</DialogFooter>
|
|
255
|
+
</DialogContent>
|
|
256
|
+
</Dialog>
|
|
257
|
+
<DialogOverlay />
|
|
258
|
+
</DialogRoot>
|
|
259
|
+
</DocsPage>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export const Default: Story = {
|
|
264
|
+
// Named after the page, so the sidebar shows one entry instead of a folder with one story.
|
|
265
|
+
name: 'Icons',
|
|
266
|
+
render: () => <IconBrowser />,
|
|
267
|
+
};
|
package/src/introduction.mdx
CHANGED
|
@@ -24,13 +24,13 @@ Echo is the design system behind Swisper. It provides styling foundations with T
|
|
|
24
24
|
icon={<DeployedCodeIcon />}
|
|
25
25
|
title="Components"
|
|
26
26
|
subtitle="Explore the library of composable components"
|
|
27
|
-
storyId="components--
|
|
27
|
+
storyId="components--default"
|
|
28
28
|
/>
|
|
29
29
|
<LinkCard
|
|
30
30
|
icon={<FormatColorIcon />}
|
|
31
31
|
title="Colors"
|
|
32
32
|
subtitle="See all available color tokens"
|
|
33
|
-
storyId="design-system-colors--
|
|
33
|
+
storyId="design-system-colors--default"
|
|
34
34
|
/>
|
|
35
35
|
<LinkCard
|
|
36
36
|
icon={<LibraryIcon />}
|
|
@@ -42,7 +42,7 @@ Echo is the design system behind Swisper. It provides styling foundations with T
|
|
|
42
42
|
icon={<DashboardIcon />}
|
|
43
43
|
title="Icons"
|
|
44
44
|
subtitle="Browse and search the icon set"
|
|
45
|
-
storyId="design-system-icons--
|
|
45
|
+
storyId="design-system-icons--default"
|
|
46
46
|
/>
|
|
47
47
|
</div>
|
|
48
48
|
</Unstyled>
|