@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.
@@ -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
+ };
@@ -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--placeholder"
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--hello-world"
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--placeholder"
45
+ storyId="design-system-icons--default"
46
46
  />
47
47
  </div>
48
48
  </Unstyled>