astryx-dracula 0.1.3 → 0.2.0
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/BRAND.md +1 -1
- package/README.md +2 -0
- package/USAGE.md +13 -0
- package/astryx-dracula.js +4 -4
- package/astryx.integration.mjs +3 -0
- package/package.json +4 -2
- package/templates/ai-chat-landing.template.mjs +6 -0
- package/templates/ai-chat-landing.tsx +476 -0
- package/templates/ai-chat.template.mjs +6 -0
- package/templates/ai-chat.tsx +762 -0
- package/templates/blank.template.mjs +6 -0
- package/templates/blank.tsx +16 -0
- package/templates/centered-hero.template.mjs +6 -0
- package/templates/centered-hero.tsx +188 -0
- package/templates/classic-gallery.template.mjs +6 -0
- package/templates/classic-gallery.tsx +181 -0
- package/templates/contact-form.template.mjs +6 -0
- package/templates/contact-form.tsx +292 -0
- package/templates/dashboard-portfolio.template.mjs +6 -0
- package/templates/dashboard-portfolio.tsx +800 -0
- package/templates/dashboard.template.mjs +6 -0
- package/templates/dashboard.tsx +774 -0
- package/templates/detail-page.template.mjs +6 -0
- package/templates/detail-page.tsx +695 -0
- package/templates/documentation-design.template.mjs +6 -0
- package/templates/documentation-design.tsx +751 -0
- package/templates/documentation-technical.template.mjs +6 -0
- package/templates/documentation-technical.tsx +292 -0
- package/templates/documentation.template.mjs +6 -0
- package/templates/documentation.tsx +347 -0
- package/templates/editor.template.mjs +6 -0
- package/templates/editor.tsx +959 -0
- package/templates/file-explorer.template.mjs +6 -0
- package/templates/file-explorer.tsx +577 -0
- package/templates/form-two-column.template.mjs +6 -0
- package/templates/form-two-column.tsx +314 -0
- package/templates/gallery-hero.template.mjs +6 -0
- package/templates/gallery-hero.tsx +287 -0
- package/templates/ide.template.mjs +6 -0
- package/templates/ide.tsx +533 -0
- package/templates/incident-console.template.mjs +6 -0
- package/templates/incident-console.tsx +584 -0
- package/templates/kanban-board.template.mjs +6 -0
- package/templates/kanban-board.tsx +746 -0
- package/templates/library.template.mjs +6 -0
- package/templates/library.tsx +489 -0
- package/templates/login-card.template.mjs +6 -0
- package/templates/login-card.tsx +200 -0
- package/templates/login-split.template.mjs +6 -0
- package/templates/login-split.tsx +318 -0
- package/templates/login-sso.template.mjs +6 -0
- package/templates/login-sso.tsx +277 -0
- package/templates/login.template.mjs +6 -0
- package/templates/login.tsx +98 -0
- package/templates/messaging-shell.template.mjs +6 -0
- package/templates/messaging-shell.tsx +675 -0
- package/templates/mixed-gallery.template.mjs +6 -0
- package/templates/mixed-gallery.tsx +190 -0
- package/templates/payment-form.template.mjs +6 -0
- package/templates/payment-form.tsx +990 -0
- package/templates/product-detail.template.mjs +6 -0
- package/templates/product-detail.tsx +366 -0
- package/templates/product-gallery.template.mjs +6 -0
- package/templates/product-gallery.tsx +173 -0
- package/templates/settings-dialog.template.mjs +6 -0
- package/templates/settings-dialog.tsx +920 -0
- package/templates/settings-sidebar.template.mjs +6 -0
- package/templates/settings-sidebar.tsx +952 -0
- package/templates/settings.template.mjs +6 -0
- package/templates/settings.tsx +235 -0
- package/templates/shell-nav.template.mjs +6 -0
- package/templates/shell-nav.tsx +306 -0
- package/templates/shell-side-nav.template.mjs +6 -0
- package/templates/shell-side-nav.tsx +238 -0
- package/templates/shell-top-nav.template.mjs +6 -0
- package/templates/shell-top-nav.tsx +243 -0
- package/templates/side-gallery.template.mjs +6 -0
- package/templates/side-gallery.tsx +179 -0
- package/templates/table-grouped.template.mjs +6 -0
- package/templates/table-grouped.tsx +1174 -0
- package/templates/table-page-chart.template.mjs +6 -0
- package/templates/table-page-chart.tsx +693 -0
- package/templates/table-page-heatmap-status.template.mjs +6 -0
- package/templates/table-page-heatmap-status.tsx +547 -0
- package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
- package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
- package/templates/table-page.template.mjs +6 -0
- package/templates/table-page.tsx +445 -0
- package/templates/table.template.mjs +6 -0
- package/templates/table.tsx +84 -0
- package/templates/theme-showcase.template.mjs +6 -0
- package/templates/theme-showcase.tsx +1282 -0
- package/theme.css +4 -4
- package/tokens.css +4 -4
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {useState, useMemo, type CSSProperties} from 'react';
|
|
4
|
+
import {Layout, LayoutHeader, LayoutContent} from '@astryxdesign/core/Layout';
|
|
5
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
6
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
7
|
+
import {ToggleButton, ToggleButtonGroup} from '@astryxdesign/core/ToggleButton';
|
|
8
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
9
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
10
|
+
import {Section} from '@astryxdesign/core/Section';
|
|
11
|
+
import {Grid} from '@astryxdesign/core/Grid';
|
|
12
|
+
import {HStack, VStack, StackItem} from '@astryxdesign/core/Stack';
|
|
13
|
+
import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
|
|
14
|
+
import {OverflowList} from '@astryxdesign/core/OverflowList';
|
|
15
|
+
import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
16
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
17
|
+
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
18
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
19
|
+
import {Search} from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
interface LibraryItem {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
category: string;
|
|
26
|
+
type: 'Component' | 'Pattern' | 'Utility';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Dracula accent per category shelf, drawn from the fixed badge vocabulary.
|
|
30
|
+
const CATEGORY_HUES: Record<string, string> = {
|
|
31
|
+
Layout: 'var(--dracula-purple)',
|
|
32
|
+
Forms: 'var(--dracula-cyan)',
|
|
33
|
+
Navigation: 'var(--dracula-pink)',
|
|
34
|
+
Feedback: 'var(--dracula-yellow)',
|
|
35
|
+
Data: 'var(--dracula-green)',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const CATEGORIES = ['All', 'Layout', 'Forms', 'Navigation', 'Feedback', 'Data'];
|
|
39
|
+
|
|
40
|
+
const ITEMS: LibraryItem[] = [
|
|
41
|
+
{
|
|
42
|
+
id: '1',
|
|
43
|
+
name: 'Stack',
|
|
44
|
+
description:
|
|
45
|
+
'Vertical and horizontal stack layouts with configurable gap and alignment.',
|
|
46
|
+
category: 'Layout',
|
|
47
|
+
type: 'Component',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: '2',
|
|
51
|
+
name: 'Grid',
|
|
52
|
+
description:
|
|
53
|
+
'Responsive grid container with auto-fit columns and gap control.',
|
|
54
|
+
category: 'Layout',
|
|
55
|
+
type: 'Component',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: '3',
|
|
59
|
+
name: 'Card',
|
|
60
|
+
description:
|
|
61
|
+
'Surface container with optional padding, border, and shadow variants.',
|
|
62
|
+
category: 'Layout',
|
|
63
|
+
type: 'Component',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: '4',
|
|
67
|
+
name: 'Center',
|
|
68
|
+
description: 'Centers its child both horizontally and vertically.',
|
|
69
|
+
category: 'Layout',
|
|
70
|
+
type: 'Utility',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: '5',
|
|
74
|
+
name: 'Section',
|
|
75
|
+
description: 'Semantic page section with optional heading and divider.',
|
|
76
|
+
category: 'Layout',
|
|
77
|
+
type: 'Pattern',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: '6',
|
|
81
|
+
name: 'Collapsible',
|
|
82
|
+
description: 'Expandable region with animated height transition.',
|
|
83
|
+
category: 'Layout',
|
|
84
|
+
type: 'Component',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: '7',
|
|
88
|
+
name: 'TextInput',
|
|
89
|
+
description:
|
|
90
|
+
'Single-line text field with label, placeholder, and validation states.',
|
|
91
|
+
category: 'Forms',
|
|
92
|
+
type: 'Component',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: '8',
|
|
96
|
+
name: 'TextArea',
|
|
97
|
+
description: 'Multi-line text field with auto-resize and character count.',
|
|
98
|
+
category: 'Forms',
|
|
99
|
+
type: 'Component',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: '9',
|
|
103
|
+
name: 'CheckboxInput',
|
|
104
|
+
description: 'Checkbox with label, indeterminate state, and group support.',
|
|
105
|
+
category: 'Forms',
|
|
106
|
+
type: 'Component',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: '10',
|
|
110
|
+
name: 'RadioList',
|
|
111
|
+
description: 'Group of radio buttons with accessible fieldset wrapper.',
|
|
112
|
+
category: 'Forms',
|
|
113
|
+
type: 'Component',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: '11',
|
|
117
|
+
name: 'Switch',
|
|
118
|
+
description: 'Toggle switch for binary on/off settings.',
|
|
119
|
+
category: 'Forms',
|
|
120
|
+
type: 'Component',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: '12',
|
|
124
|
+
name: 'Selector',
|
|
125
|
+
description:
|
|
126
|
+
'Dropdown or inline option selector with single and multi-select modes.',
|
|
127
|
+
category: 'Forms',
|
|
128
|
+
type: 'Component',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: '13',
|
|
132
|
+
name: 'TabList',
|
|
133
|
+
description:
|
|
134
|
+
'Horizontal tab navigation with underline indicator and keyboard support.',
|
|
135
|
+
category: 'Navigation',
|
|
136
|
+
type: 'Component',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: '14',
|
|
140
|
+
name: 'TopNav',
|
|
141
|
+
description: 'Application top bar with logo, nav links, and action slots.',
|
|
142
|
+
category: 'Navigation',
|
|
143
|
+
type: 'Pattern',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: '15',
|
|
147
|
+
name: 'SideNav',
|
|
148
|
+
description:
|
|
149
|
+
'Vertical sidebar navigation with collapsible groups and active states.',
|
|
150
|
+
category: 'Navigation',
|
|
151
|
+
type: 'Pattern',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: '16',
|
|
155
|
+
name: 'Breadcrumbs',
|
|
156
|
+
description: 'Path trail navigation with separator and truncation support.',
|
|
157
|
+
category: 'Navigation',
|
|
158
|
+
type: 'Component',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: '17',
|
|
162
|
+
name: 'Pagination',
|
|
163
|
+
description:
|
|
164
|
+
'Page navigation with prev/next controls and page count display.',
|
|
165
|
+
category: 'Navigation',
|
|
166
|
+
type: 'Component',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: '18',
|
|
170
|
+
name: 'MobileNav',
|
|
171
|
+
description:
|
|
172
|
+
'Bottom tab bar for mobile viewports with icon and label slots.',
|
|
173
|
+
category: 'Navigation',
|
|
174
|
+
type: 'Pattern',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: '19',
|
|
178
|
+
name: 'Badge',
|
|
179
|
+
description:
|
|
180
|
+
'Compact label for status, count, or category with semantic color variants.',
|
|
181
|
+
category: 'Feedback',
|
|
182
|
+
type: 'Component',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: '20',
|
|
186
|
+
name: 'Banner',
|
|
187
|
+
description:
|
|
188
|
+
'Full-width alert bar for info, success, warning, and error messages.',
|
|
189
|
+
category: 'Feedback',
|
|
190
|
+
type: 'Component',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: '21',
|
|
194
|
+
name: 'Spinner',
|
|
195
|
+
description: 'Animated loading indicator with size and color variants.',
|
|
196
|
+
category: 'Feedback',
|
|
197
|
+
type: 'Component',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: '22',
|
|
201
|
+
name: 'ProgressBar',
|
|
202
|
+
description: 'Horizontal bar indicating task completion percentage.',
|
|
203
|
+
category: 'Feedback',
|
|
204
|
+
type: 'Component',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: '23',
|
|
208
|
+
name: 'StatusDot',
|
|
209
|
+
description:
|
|
210
|
+
'Small dot indicator for presence, health, or pipeline status.',
|
|
211
|
+
category: 'Feedback',
|
|
212
|
+
type: 'Component',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: '24',
|
|
216
|
+
name: 'Tooltip',
|
|
217
|
+
description:
|
|
218
|
+
'Contextual label that appears on hover with configurable placement.',
|
|
219
|
+
category: 'Feedback',
|
|
220
|
+
type: 'Component',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: '25',
|
|
224
|
+
name: 'Table',
|
|
225
|
+
description:
|
|
226
|
+
'Feature-rich data table with sorting, selection, and column resizing.',
|
|
227
|
+
category: 'Data',
|
|
228
|
+
type: 'Component',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: '26',
|
|
232
|
+
name: 'Avatar',
|
|
233
|
+
description:
|
|
234
|
+
'User profile image with fallback initials and status dot support.',
|
|
235
|
+
category: 'Data',
|
|
236
|
+
type: 'Component',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: '27',
|
|
240
|
+
name: 'Skeleton',
|
|
241
|
+
description:
|
|
242
|
+
'Placeholder shimmer for loading states matching content shapes.',
|
|
243
|
+
category: 'Data',
|
|
244
|
+
type: 'Utility',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: '28',
|
|
248
|
+
name: 'HoverCard',
|
|
249
|
+
description: 'Rich popover that appears on hover with arbitrary content.',
|
|
250
|
+
category: 'Data',
|
|
251
|
+
type: 'Component',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: '29',
|
|
255
|
+
name: 'PowerSearch',
|
|
256
|
+
description:
|
|
257
|
+
'Command-palette style search with grouped results and keyboard nav.',
|
|
258
|
+
category: 'Data',
|
|
259
|
+
type: 'Pattern',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: '30',
|
|
263
|
+
name: 'Typeahead',
|
|
264
|
+
description:
|
|
265
|
+
'Autocomplete input with async suggestion loading and selection.',
|
|
266
|
+
category: 'Data',
|
|
267
|
+
type: 'Component',
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const thumbnailImage: CSSProperties = {
|
|
272
|
+
width: '100%',
|
|
273
|
+
height: '100%',
|
|
274
|
+
display: 'block',
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// =============================================================================
|
|
278
|
+
// Side Nav
|
|
279
|
+
// =============================================================================
|
|
280
|
+
|
|
281
|
+
function LibraryCard({item}: {item: LibraryItem}) {
|
|
282
|
+
const hue = CATEGORY_HUES[item.category] ?? 'var(--dracula-purple)';
|
|
283
|
+
return (
|
|
284
|
+
<Card padding={0}>
|
|
285
|
+
<AspectRatio ratio={16 / 9}>
|
|
286
|
+
<svg
|
|
287
|
+
viewBox="0 0 400 300"
|
|
288
|
+
preserveAspectRatio="xMidYMid slice"
|
|
289
|
+
style={thumbnailImage}
|
|
290
|
+
role="img"
|
|
291
|
+
aria-label={`${item.name} thumbnail`}>
|
|
292
|
+
<rect width="400" height="300" fill="var(--dracula-bg-light)" />
|
|
293
|
+
<g
|
|
294
|
+
transform="translate(200 150)"
|
|
295
|
+
fill="none"
|
|
296
|
+
stroke="var(--dracula-comment)"
|
|
297
|
+
strokeWidth="5"
|
|
298
|
+
strokeLinecap="round"
|
|
299
|
+
strokeLinejoin="round">
|
|
300
|
+
<rect x="-44" y="-44" width="88" height="88" rx="5" />
|
|
301
|
+
<circle cx="18" cy="-18" r="2.5" fill={hue} stroke="none" />
|
|
302
|
+
<path d="M-34 30 L-8 0 L10 18 L20 8 L34 24" />
|
|
303
|
+
</g>
|
|
304
|
+
</svg>
|
|
305
|
+
</AspectRatio>
|
|
306
|
+
<Section variant="transparent" padding={4}>
|
|
307
|
+
<VStack gap={1}>
|
|
308
|
+
<Heading level={3}>{item.name}</Heading>
|
|
309
|
+
<Text type="body" color="secondary">
|
|
310
|
+
{item.description}
|
|
311
|
+
</Text>
|
|
312
|
+
</VStack>
|
|
313
|
+
</Section>
|
|
314
|
+
</Card>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function LibrarySection({
|
|
319
|
+
category,
|
|
320
|
+
items,
|
|
321
|
+
}: {
|
|
322
|
+
category: string;
|
|
323
|
+
items: LibraryItem[];
|
|
324
|
+
}) {
|
|
325
|
+
return (
|
|
326
|
+
<VStack gap={6}>
|
|
327
|
+
<HStack justify="between" vAlign="center">
|
|
328
|
+
<Heading level={2}>{category}</Heading>
|
|
329
|
+
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
330
|
+
{items.length} {items.length === 1 ? 'item' : 'items'}
|
|
331
|
+
</Text>
|
|
332
|
+
</HStack>
|
|
333
|
+
<Grid columns={{minWidth: 320}} gap={4}>
|
|
334
|
+
{items.map(item => (
|
|
335
|
+
<LibraryCard key={item.id} item={item} />
|
|
336
|
+
))}
|
|
337
|
+
</Grid>
|
|
338
|
+
</VStack>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export default function LibraryGrid() {
|
|
343
|
+
const [activeTab, setActiveTab] = useState('All');
|
|
344
|
+
const [search, setSearch] = useState('');
|
|
345
|
+
const [sortOrder, setSortOrder] = useState('A-Z');
|
|
346
|
+
|
|
347
|
+
const filtered = useMemo(() => {
|
|
348
|
+
let items =
|
|
349
|
+
activeTab === 'All' ? ITEMS : ITEMS.filter(i => i.category === activeTab);
|
|
350
|
+
if (search.trim()) {
|
|
351
|
+
const q = search.toLowerCase();
|
|
352
|
+
items = items.filter(
|
|
353
|
+
i =>
|
|
354
|
+
i.name.toLowerCase().includes(q) ||
|
|
355
|
+
i.description.toLowerCase().includes(q),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const sorted = [...items];
|
|
359
|
+
if (sortOrder === 'A-Z') {
|
|
360
|
+
sorted.sort((a, b) => a.name.localeCompare(b.name));
|
|
361
|
+
} else if (sortOrder === 'Z-A') {
|
|
362
|
+
sorted.sort((a, b) => b.name.localeCompare(a.name));
|
|
363
|
+
} else if (sortOrder === 'Newest') {
|
|
364
|
+
sorted.sort((a, b) => Number(b.id) - Number(a.id));
|
|
365
|
+
}
|
|
366
|
+
return sorted;
|
|
367
|
+
}, [activeTab, search, sortOrder]);
|
|
368
|
+
|
|
369
|
+
const groupedSections = useMemo(() => {
|
|
370
|
+
if (activeTab !== 'All') {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const order = CATEGORIES.filter(c => c !== 'All');
|
|
374
|
+
const map = new Map<string, LibraryItem[]>();
|
|
375
|
+
for (const item of filtered) {
|
|
376
|
+
let group = map.get(item.category);
|
|
377
|
+
if (!group) {
|
|
378
|
+
group = [];
|
|
379
|
+
map.set(item.category, group);
|
|
380
|
+
}
|
|
381
|
+
group.push(item);
|
|
382
|
+
}
|
|
383
|
+
return order
|
|
384
|
+
.filter(cat => map.has(cat))
|
|
385
|
+
.map(cat => ({category: cat, items: map.get(cat) ?? []}));
|
|
386
|
+
}, [activeTab, filtered]);
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<Layout
|
|
390
|
+
header={
|
|
391
|
+
<LayoutHeader hasDivider padding={6}>
|
|
392
|
+
<Heading level={1}>Library</Heading>
|
|
393
|
+
</LayoutHeader>
|
|
394
|
+
}
|
|
395
|
+
content={
|
|
396
|
+
<LayoutContent padding={6}>
|
|
397
|
+
<VStack gap={6}>
|
|
398
|
+
<VStack gap={4}>
|
|
399
|
+
<TextInput
|
|
400
|
+
label="Search"
|
|
401
|
+
isLabelHidden
|
|
402
|
+
placeholder="Search the stacks..."
|
|
403
|
+
value={search}
|
|
404
|
+
onChange={setSearch}
|
|
405
|
+
startIcon={Search}
|
|
406
|
+
size="lg"
|
|
407
|
+
/>
|
|
408
|
+
<HStack vAlign="center" gap={4}>
|
|
409
|
+
<StackItem size="fill">
|
|
410
|
+
<VStack>
|
|
411
|
+
<ToggleButtonGroup
|
|
412
|
+
label="Filter by category"
|
|
413
|
+
value={activeTab}
|
|
414
|
+
onChange={v => setActiveTab(v ?? 'All')}>
|
|
415
|
+
<OverflowList
|
|
416
|
+
gap={1}
|
|
417
|
+
behavior="observeParent"
|
|
418
|
+
overflowRenderer={overflowItems => (
|
|
419
|
+
<DropdownMenu
|
|
420
|
+
button={{
|
|
421
|
+
label: `+${overflowItems.length}`,
|
|
422
|
+
variant: 'ghost',
|
|
423
|
+
size: 'lg',
|
|
424
|
+
}}
|
|
425
|
+
items={overflowItems.map(({index}) => ({
|
|
426
|
+
label: CATEGORIES[index],
|
|
427
|
+
onClick: () => setActiveTab(CATEGORIES[index]),
|
|
428
|
+
}))}
|
|
429
|
+
/>
|
|
430
|
+
)}>
|
|
431
|
+
{CATEGORIES.map(cat => (
|
|
432
|
+
<ToggleButton
|
|
433
|
+
key={cat}
|
|
434
|
+
label={cat}
|
|
435
|
+
value={cat}
|
|
436
|
+
size="lg"
|
|
437
|
+
/>
|
|
438
|
+
))}
|
|
439
|
+
</OverflowList>
|
|
440
|
+
</ToggleButtonGroup>
|
|
441
|
+
</VStack>
|
|
442
|
+
</StackItem>
|
|
443
|
+
<DropdownMenu
|
|
444
|
+
button={{label: sortOrder, size: 'lg'}}
|
|
445
|
+
items={[
|
|
446
|
+
{label: 'A-Z', onClick: () => setSortOrder('A-Z')},
|
|
447
|
+
{label: 'Z-A', onClick: () => setSortOrder('Z-A')},
|
|
448
|
+
{label: 'Newest', onClick: () => setSortOrder('Newest')},
|
|
449
|
+
]}
|
|
450
|
+
/>
|
|
451
|
+
</HStack>
|
|
452
|
+
</VStack>
|
|
453
|
+
|
|
454
|
+
{filtered.length === 0 ? (
|
|
455
|
+
<EmptyState
|
|
456
|
+
icon={<Icon icon={Search} size="lg" color="secondary" />}
|
|
457
|
+
title="Nothing stirs in the stacks"
|
|
458
|
+
description="No entries match this search and filter. Clear them to browse the full library."
|
|
459
|
+
actions={
|
|
460
|
+
<Button
|
|
461
|
+
label="Clear search & filters"
|
|
462
|
+
variant="primary"
|
|
463
|
+
onClick={() => {
|
|
464
|
+
setSearch('');
|
|
465
|
+
setActiveTab('All');
|
|
466
|
+
}}
|
|
467
|
+
/>
|
|
468
|
+
}
|
|
469
|
+
/>
|
|
470
|
+
) : (
|
|
471
|
+
<VStack gap={6}>
|
|
472
|
+
{(
|
|
473
|
+
groupedSections ?? [{category: activeTab, items: filtered}]
|
|
474
|
+
).flatMap(section => [
|
|
475
|
+
<Divider key={`d-${section.category}`} />,
|
|
476
|
+
<LibrarySection
|
|
477
|
+
key={section.category}
|
|
478
|
+
category={section.category}
|
|
479
|
+
items={section.items}
|
|
480
|
+
/>,
|
|
481
|
+
])}
|
|
482
|
+
</VStack>
|
|
483
|
+
)}
|
|
484
|
+
</VStack>
|
|
485
|
+
</LayoutContent>
|
|
486
|
+
}
|
|
487
|
+
/>
|
|
488
|
+
);
|
|
489
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {useState, type CSSProperties} from 'react';
|
|
4
|
+
import {Moon} from 'lucide-react';
|
|
5
|
+
import {VStack} from '@astryxdesign/core/Layout';
|
|
6
|
+
import {Center} from '@astryxdesign/core/Center';
|
|
7
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
8
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
9
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
10
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
11
|
+
import {Link} from '@astryxdesign/core/Link';
|
|
12
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
13
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
14
|
+
|
|
15
|
+
// Brand sign-in marks — bespoke inline glyphs, no icon-library equivalent.
|
|
16
|
+
const AppleIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
|
17
|
+
<svg
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
fill="currentColor"
|
|
20
|
+
width={16}
|
|
21
|
+
height={16}
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
{...props}>
|
|
24
|
+
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const GoogleIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
|
29
|
+
<svg
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
fill="currentColor"
|
|
32
|
+
width={16}
|
|
33
|
+
height={16}
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
{...props}>
|
|
36
|
+
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
|
|
37
|
+
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
|
38
|
+
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
|
39
|
+
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Standalone auth page paints its own body background (no host shell).
|
|
44
|
+
const pageStyle: CSSProperties = {
|
|
45
|
+
minHeight: '100%',
|
|
46
|
+
backgroundColor: 'var(--color-background-body)',
|
|
47
|
+
padding: 'var(--spacing-6)',
|
|
48
|
+
};
|
|
49
|
+
// Cap the column at 400px but let it shrink to fit narrow screens (Stack
|
|
50
|
+
// has no maxWidth prop, so it's set here).
|
|
51
|
+
const contentStyle: CSSProperties = {
|
|
52
|
+
width: '100%',
|
|
53
|
+
maxWidth: 400,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default function LoginCard() {
|
|
57
|
+
const [email, setEmail] = useState('');
|
|
58
|
+
const [password, setPassword] = useState('');
|
|
59
|
+
const [loginFailed, setLoginFailed] = useState(false);
|
|
60
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
61
|
+
|
|
62
|
+
const handleLogin = () => {
|
|
63
|
+
if (!email || !password) {
|
|
64
|
+
setLoginFailed(true);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setIsLoading(true);
|
|
68
|
+
setLoginFailed(false);
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
setIsLoading(false);
|
|
71
|
+
setLoginFailed(true);
|
|
72
|
+
}, 2000);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Center axis="both" style={pageStyle}>
|
|
77
|
+
<VStack gap={4} hAlign="center" style={contentStyle}>
|
|
78
|
+
{/* Logo */}
|
|
79
|
+
<VStack gap={2} hAlign="center">
|
|
80
|
+
<Icon icon={Moon} size="lg" color="accent" />
|
|
81
|
+
<Text type="body" weight="bold" size="lg">
|
|
82
|
+
Castle Dracula
|
|
83
|
+
</Text>
|
|
84
|
+
</VStack>
|
|
85
|
+
|
|
86
|
+
{/* Card */}
|
|
87
|
+
<Card padding={8} width="100%">
|
|
88
|
+
<VStack gap={4} hAlign="stretch">
|
|
89
|
+
{/* Header */}
|
|
90
|
+
<VStack gap={1} hAlign="center">
|
|
91
|
+
<Heading level={2}>Welcome back to the night</Heading>
|
|
92
|
+
<Text type="body" color="secondary">
|
|
93
|
+
Sign in to your crypt
|
|
94
|
+
</Text>
|
|
95
|
+
</VStack>
|
|
96
|
+
|
|
97
|
+
{/* Form fields */}
|
|
98
|
+
<VStack gap={2}>
|
|
99
|
+
<TextInput
|
|
100
|
+
label="Email"
|
|
101
|
+
isLabelHidden
|
|
102
|
+
type="email"
|
|
103
|
+
placeholder="you@castle.dracula"
|
|
104
|
+
value={email}
|
|
105
|
+
onChange={setEmail}
|
|
106
|
+
size="lg"
|
|
107
|
+
/>
|
|
108
|
+
<VStack gap={1}>
|
|
109
|
+
<TextInput
|
|
110
|
+
label="Password"
|
|
111
|
+
isLabelHidden
|
|
112
|
+
placeholder="Whisper your password"
|
|
113
|
+
type="password"
|
|
114
|
+
value={password}
|
|
115
|
+
onChange={(v: string) => {
|
|
116
|
+
setPassword(v);
|
|
117
|
+
setLoginFailed(false);
|
|
118
|
+
}}
|
|
119
|
+
size="lg"
|
|
120
|
+
status={
|
|
121
|
+
loginFailed
|
|
122
|
+
? {
|
|
123
|
+
type: 'error',
|
|
124
|
+
message: 'Wrong incantation. Try again.',
|
|
125
|
+
}
|
|
126
|
+
: undefined
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
{loginFailed && (
|
|
130
|
+
<VStack hAlign="end">
|
|
131
|
+
<Link
|
|
132
|
+
href="#/templates/login-card"
|
|
133
|
+
size="sm"
|
|
134
|
+
color="secondary"
|
|
135
|
+
type="supporting">
|
|
136
|
+
Forgot password?
|
|
137
|
+
</Link>
|
|
138
|
+
</VStack>
|
|
139
|
+
)}
|
|
140
|
+
</VStack>
|
|
141
|
+
</VStack>
|
|
142
|
+
|
|
143
|
+
{/* Login button */}
|
|
144
|
+
<Button
|
|
145
|
+
label="Enter the night"
|
|
146
|
+
variant="primary"
|
|
147
|
+
size="lg"
|
|
148
|
+
isLoading={isLoading}
|
|
149
|
+
onClick={handleLogin}
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
{/* Divider */}
|
|
153
|
+
<Divider label="Or continue with" />
|
|
154
|
+
|
|
155
|
+
{/* Social buttons */}
|
|
156
|
+
<VStack gap={3} hAlign="stretch">
|
|
157
|
+
<Button
|
|
158
|
+
label="Login with Apple"
|
|
159
|
+
variant="secondary"
|
|
160
|
+
icon={<AppleIcon />}
|
|
161
|
+
size="lg"
|
|
162
|
+
/>
|
|
163
|
+
<Button
|
|
164
|
+
label="Login with Google"
|
|
165
|
+
variant="secondary"
|
|
166
|
+
icon={<GoogleIcon />}
|
|
167
|
+
size="lg"
|
|
168
|
+
/>
|
|
169
|
+
</VStack>
|
|
170
|
+
|
|
171
|
+
{/* Sign up link */}
|
|
172
|
+
<VStack hAlign="center">
|
|
173
|
+
<Text type="supporting" color="secondary">
|
|
174
|
+
New to the castle?{' '}
|
|
175
|
+
<Link href="#/templates/login-card" type="supporting">
|
|
176
|
+
Sign up
|
|
177
|
+
</Link>
|
|
178
|
+
</Text>
|
|
179
|
+
</VStack>
|
|
180
|
+
</VStack>
|
|
181
|
+
</Card>
|
|
182
|
+
|
|
183
|
+
{/* Terms */}
|
|
184
|
+
<VStack hAlign="center" width="100%">
|
|
185
|
+
<Text type="supporting" color="secondary" justify="center">
|
|
186
|
+
By clicking continue, you agree to our{' '}
|
|
187
|
+
<Link href="#/templates/login-card" type="supporting">
|
|
188
|
+
Terms of Service
|
|
189
|
+
</Link>{' '}
|
|
190
|
+
and{' '}
|
|
191
|
+
<Link href="#/templates/login-card" type="supporting">
|
|
192
|
+
Privacy Policy
|
|
193
|
+
</Link>
|
|
194
|
+
.
|
|
195
|
+
</Text>
|
|
196
|
+
</VStack>
|
|
197
|
+
</VStack>
|
|
198
|
+
</Center>
|
|
199
|
+
);
|
|
200
|
+
}
|