astryx-dracula 0.2.0 → 0.3.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 +30 -18
- package/README.md +22 -11
- package/USAGE.md +29 -10
- package/astryx-dracula.js +19 -13
- package/astryx-dracula.variants.d.ts +15 -0
- package/package.json +59 -17
- package/shared/auth-chrome-config.ts +18 -0
- package/shared/auth-chrome.tsx +25 -0
- package/shared/auth-copy.ts +22 -0
- package/shared/chaptered-doc-config.ts +92 -0
- package/shared/chaptered-doc.tsx +332 -0
- package/shared/chart-hues.ts +27 -0
- package/shared/chart-labels.tsx +44 -0
- package/shared/chart-legend.tsx +100 -0
- package/shared/chart-panel-style.ts +13 -0
- package/shared/data-bar.tsx +160 -0
- package/shared/gallery-image.ts +20 -0
- package/shared/login-demo.ts +7 -0
- package/shared/metric-delta.tsx +36 -0
- package/shared/order-desk.tsx +273 -0
- package/shared/revenue-chart.tsx +174 -0
- package/shared/scene-castle.tsx +322 -0
- package/shared/scene-frame.tsx +88 -0
- package/shared/scene-hues.ts +49 -0
- package/shared/scene-tile.tsx +128 -0
- package/shared/settings-data.ts +125 -0
- package/shared/settings-rows.tsx +179 -0
- package/shared/sparkline.tsx +94 -0
- package/shared/sso-icons.tsx +30 -0
- package/templates/ai-chat-landing.tsx +67 -32
- package/templates/ai-chat.tsx +41 -20
- package/templates/blank.tsx +5 -2
- package/templates/centered-hero.tsx +15 -124
- package/templates/classic-gallery.tsx +21 -65
- package/templates/contact-form.tsx +62 -40
- package/templates/dashboard-portfolio.tsx +146 -227
- package/templates/dashboard.tsx +123 -121
- package/templates/detail-page.tsx +101 -95
- package/templates/documentation-design.template.mjs +1 -1
- package/templates/documentation-design.tsx +85 -96
- package/templates/documentation-technical.template.mjs +1 -1
- package/templates/documentation-technical.tsx +33 -21
- package/templates/documentation.template.mjs +1 -1
- package/templates/documentation.tsx +54 -74
- package/templates/editor.tsx +119 -89
- package/templates/file-explorer.tsx +133 -84
- package/templates/form-two-column.tsx +62 -124
- package/templates/gallery-hero.tsx +17 -171
- package/templates/ide.tsx +27 -21
- package/templates/incident-console.tsx +105 -69
- package/templates/kanban-board.tsx +100 -41
- package/templates/library.template.mjs +2 -2
- package/templates/library.tsx +38 -41
- package/templates/login-card.tsx +59 -86
- package/templates/login-split.tsx +88 -128
- package/templates/login-sso.tsx +107 -68
- package/templates/login.tsx +108 -52
- package/templates/messaging-shell.tsx +172 -143
- package/templates/mixed-gallery.tsx +31 -130
- package/templates/payment-form.tsx +369 -289
- package/templates/product-detail.tsx +40 -57
- package/templates/product-gallery.tsx +31 -40
- package/templates/product-tour.template.mjs +6 -0
- package/templates/product-tour.tsx +916 -0
- package/templates/settings-dialog.tsx +145 -286
- package/templates/settings-sidebar.tsx +179 -267
- package/templates/settings.tsx +42 -13
- package/templates/shell-nav.tsx +32 -23
- package/templates/shell-side-nav.tsx +30 -19
- package/templates/shell-top-nav.tsx +81 -43
- package/templates/side-gallery.tsx +39 -93
- package/templates/table-grouped.tsx +63 -33
- package/templates/table-page-chart.tsx +24 -297
- package/templates/table-page-heatmap-status.tsx +157 -91
- package/templates/table-page-shoe-store-heatmap.tsx +19 -288
- package/templates/table-page.tsx +24 -6
- package/templates/table.tsx +24 -7
- package/templates/tech-report.template.mjs +6 -0
- package/templates/tech-report.tsx +1486 -0
- package/templates/theme-showcase.template.mjs +1 -1
- package/templates/theme-showcase.tsx +313 -249
- package/theme.css +76 -14
- package/tokens.css +425 -30
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
+
// S > L > (LH[divider] > Tbar > (Hd"Sprint Board"[level=1] + Bd.neutral"8") + (H[g=2] > SE + D.strong + IB"Sort" + IB"Filter" + IB"Search" + B.primary"Add task")) + (LC[p=0] > G[c={min:280,max:4} g=4] > (C.muted[p=0] > L > (LH[divider] > H[j=between a=center] > (H[g=2 a=center] > SD + Hd"To-do"[level=2] + IB) + Tx) + (LC[p=3] > V[g=2] > (C[p=3] > V[g=2] > (H[j=between] > (H[g=2] > Tk + Bd) + MM) + (V[g=1] > Hd[level=3] + Tx) + Tx)*2))*4)
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
6
|
useEffect,
|
|
5
|
-
useMemo,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
8
9
|
type CSSProperties,
|
|
9
10
|
type PointerEvent as ReactPointerEvent,
|
|
11
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
10
12
|
type ReactNode,
|
|
11
13
|
} from 'react';
|
|
12
14
|
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
21
23
|
import {Card} from '@astryxdesign/core/Card';
|
|
22
24
|
import {Badge} from '@astryxdesign/core/Badge';
|
|
25
|
+
import {Token} from '@astryxdesign/core/Token';
|
|
23
26
|
import {Button} from '@astryxdesign/core/Button';
|
|
24
27
|
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
25
28
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
@@ -27,7 +30,7 @@ import {StatusDot} from '@astryxdesign/core/StatusDot';
|
|
|
27
30
|
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
28
31
|
import {MoreMenu} from '@astryxdesign/core/MoreMenu';
|
|
29
32
|
import {Selector} from '@astryxdesign/core/Selector';
|
|
30
|
-
import {
|
|
33
|
+
import {Popover} from '@astryxdesign/core/Popover';
|
|
31
34
|
import {Divider} from '@astryxdesign/core/Divider';
|
|
32
35
|
import {Toolbar} from '@astryxdesign/core/Toolbar';
|
|
33
36
|
import {Section} from '@astryxdesign/core/Section';
|
|
@@ -64,7 +67,7 @@ interface WorkItem {
|
|
|
64
67
|
interface ColumnMeta {
|
|
65
68
|
id: ColumnId;
|
|
66
69
|
title: string;
|
|
67
|
-
variant: 'neutral' | '
|
|
70
|
+
variant: 'neutral' | 'info' | 'warning' | 'success';
|
|
68
71
|
tooltip: string;
|
|
69
72
|
emptyTitle: string;
|
|
70
73
|
emptyDescription: string;
|
|
@@ -105,7 +108,7 @@ const COLUMNS: ColumnMeta[] = [
|
|
|
105
108
|
{
|
|
106
109
|
id: 'in-progress',
|
|
107
110
|
title: 'In progress',
|
|
108
|
-
variant: '
|
|
111
|
+
variant: 'info',
|
|
109
112
|
tooltip: 'Theme work currently on the easel.',
|
|
110
113
|
emptyTitle: 'Nothing in progress',
|
|
111
114
|
emptyDescription: 'Tasks being worked on appear here.',
|
|
@@ -114,7 +117,7 @@ const COLUMNS: ColumnMeta[] = [
|
|
|
114
117
|
{
|
|
115
118
|
id: 'in-review',
|
|
116
119
|
title: 'In review',
|
|
117
|
-
variant: '
|
|
120
|
+
variant: 'info',
|
|
118
121
|
tooltip: 'Shades waiting for your contrast review.',
|
|
119
122
|
emptyTitle: 'Nothing in review',
|
|
120
123
|
emptyDescription: 'Tasks awaiting your review appear here.',
|
|
@@ -133,13 +136,26 @@ const COLUMNS: ColumnMeta[] = [
|
|
|
133
136
|
|
|
134
137
|
const PRIORITY_META: Record<
|
|
135
138
|
Priority,
|
|
136
|
-
{label: string; variant: 'red' | 'yellow' | '
|
|
139
|
+
{label: string; variant: 'red' | 'yellow' | 'neutral'}
|
|
137
140
|
> = {
|
|
138
141
|
high: {label: 'High', variant: 'red'},
|
|
139
142
|
medium: {label: 'Medium', variant: 'yellow'},
|
|
140
|
-
low: {label: 'Low', variant: '
|
|
143
|
+
low: {label: 'Low', variant: 'neutral'}, // neutral: low priority is not info
|
|
141
144
|
};
|
|
142
145
|
|
|
146
|
+
function groupByColumn(items: WorkItem[]): Record<ColumnId, WorkItem[]> {
|
|
147
|
+
const map: Record<ColumnId, WorkItem[]> = {
|
|
148
|
+
todo: [],
|
|
149
|
+
'in-progress': [],
|
|
150
|
+
'in-review': [],
|
|
151
|
+
done: [],
|
|
152
|
+
};
|
|
153
|
+
for (const item of items) {
|
|
154
|
+
map[item.column].push(item);
|
|
155
|
+
}
|
|
156
|
+
return map;
|
|
157
|
+
}
|
|
158
|
+
|
|
143
159
|
const INITIAL_ITEMS: WorkItem[] = [
|
|
144
160
|
{
|
|
145
161
|
id: 't1',
|
|
@@ -244,7 +260,7 @@ const boardColumnsStyle: CSSProperties = {
|
|
|
244
260
|
overflowX: 'auto',
|
|
245
261
|
overflowY: 'auto',
|
|
246
262
|
height: '100%',
|
|
247
|
-
padding: 'var(--
|
|
263
|
+
padding: 'var(--spacing-6)',
|
|
248
264
|
};
|
|
249
265
|
const columnShellStyle: CSSProperties = {
|
|
250
266
|
minWidth: 0,
|
|
@@ -258,14 +274,17 @@ const cardStyle: CSSProperties = {
|
|
|
258
274
|
touchAction: 'none',
|
|
259
275
|
};
|
|
260
276
|
// The dragged card is lifted out of flow and follows the pointer. It ignores
|
|
261
|
-
// pointer events so hit-testing reads the columns underneath it.
|
|
277
|
+
// pointer events so hit-testing reads the columns underneath it. The clone
|
|
278
|
+
// rides above shell chrome (layered 0-3) but stays under overlay surfaces
|
|
279
|
+
// (menus, dialogs layer at 500+ plus the top layer), so an open menu wins.
|
|
280
|
+
const DRAG_CLONE_LAYER = 100;
|
|
262
281
|
const floatingStyle: CSSProperties = {
|
|
263
282
|
position: 'fixed',
|
|
264
283
|
insetBlockStart: 0,
|
|
265
284
|
insetInlineStart: 0,
|
|
266
285
|
pointerEvents: 'none',
|
|
267
286
|
cursor: 'grabbing',
|
|
268
|
-
zIndex:
|
|
287
|
+
zIndex: DRAG_CLONE_LAYER,
|
|
269
288
|
};
|
|
270
289
|
// Placeholder marking the landing slot; matches the dragged card's height.
|
|
271
290
|
const ghostStyle = (height: number): CSSProperties => ({
|
|
@@ -277,7 +296,13 @@ const toolbarDividerStyle: CSSProperties = {
|
|
|
277
296
|
alignSelf: 'stretch',
|
|
278
297
|
};
|
|
279
298
|
const columnEmptyStateStyle: CSSProperties = {
|
|
280
|
-
|
|
299
|
+
// 12px, not 24. This applies to an EmptyState inside a LayoutContent
|
|
300
|
+
// padding={3} (12px), so 24 rendered as 36px of block air on the empty
|
|
301
|
+
// branch against 12px on the populated one -- a 3x difference between two
|
|
302
|
+
// states of the same column, and the only nested inset in the kit that
|
|
303
|
+
// exceeded its container. The block padding is the CONTAINER's job; this
|
|
304
|
+
// one is the placeholder's room to breathe inside it.
|
|
305
|
+
paddingBlock: 'var(--spacing-3)',
|
|
281
306
|
};
|
|
282
307
|
|
|
283
308
|
// ============= CARD BODY =============
|
|
@@ -300,8 +325,8 @@ function BoardCardBody({
|
|
|
300
325
|
return (
|
|
301
326
|
<VStack gap={2}>
|
|
302
327
|
<HStack hAlign="between" vAlign="start">
|
|
303
|
-
<HStack gap={
|
|
304
|
-
<
|
|
328
|
+
<HStack gap={2} vAlign="center" wrap="wrap">
|
|
329
|
+
<Token label={item.ref} size="sm" />
|
|
305
330
|
<Badge label={priority.label} variant={priority.variant} />
|
|
306
331
|
</HStack>
|
|
307
332
|
<MoreMenu
|
|
@@ -317,7 +342,7 @@ function BoardCardBody({
|
|
|
317
342
|
</HStack>
|
|
318
343
|
|
|
319
344
|
<VStack gap={1}>
|
|
320
|
-
<Heading level={
|
|
345
|
+
<Heading level={3}>{item.title}</Heading>
|
|
321
346
|
<Text type="body" color="secondary" maxLines={2}>
|
|
322
347
|
{item.description}
|
|
323
348
|
</Text>
|
|
@@ -343,12 +368,33 @@ function BoardCard({
|
|
|
343
368
|
onPointerDown: (e: ReactPointerEvent, id: string) => void;
|
|
344
369
|
onMove: (id: string, to: ColumnId) => void;
|
|
345
370
|
}) {
|
|
371
|
+
// Keyboard-move path for the core drag interaction: arrow keys move the
|
|
372
|
+
// focused card across columns. The pointer DnD below stays quarantined and
|
|
373
|
+
// untouched; this only reuses its moveItem commit.
|
|
374
|
+
const onKeyDown = (e: ReactKeyboardEvent) => {
|
|
375
|
+
const at = COLUMNS.findIndex(c => c.id === item.column);
|
|
376
|
+
const to =
|
|
377
|
+
e.key === 'ArrowRight'
|
|
378
|
+
? COLUMNS[Math.min(COLUMNS.length - 1, at + 1)]
|
|
379
|
+
: e.key === 'ArrowLeft'
|
|
380
|
+
? COLUMNS[Math.max(0, at - 1)]
|
|
381
|
+
: undefined;
|
|
382
|
+
if (!to || to.id === item.column) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
onMove(item.id, to.id);
|
|
387
|
+
};
|
|
346
388
|
return (
|
|
347
389
|
<Card
|
|
348
390
|
ref={cardRef}
|
|
349
391
|
padding={3}
|
|
350
392
|
style={cardStyle}
|
|
351
|
-
|
|
393
|
+
tabIndex={0}
|
|
394
|
+
role="button"
|
|
395
|
+
aria-label={`${item.title}. In ${COLUMNS.find(c => c.id === item.column)?.title}. Press left or right arrow to move across columns.`}
|
|
396
|
+
onPointerDown={e => onPointerDown(e, item.id)}
|
|
397
|
+
onKeyDown={onKeyDown}>
|
|
352
398
|
<BoardCardBody item={item} onMove={onMove} />
|
|
353
399
|
</Card>
|
|
354
400
|
);
|
|
@@ -374,15 +420,29 @@ function BoardColumn({
|
|
|
374
420
|
header={
|
|
375
421
|
<LayoutHeader hasDivider padding={3}>
|
|
376
422
|
<HStack hAlign="between" vAlign="center">
|
|
377
|
-
<HStack gap={
|
|
423
|
+
<HStack gap={1} vAlign="center">
|
|
378
424
|
<StatusDot
|
|
379
425
|
variant={meta.variant}
|
|
380
426
|
label={`${meta.title} status`}
|
|
427
|
+
isPulsing={meta.id === 'in-progress' || meta.id === 'in-review'}
|
|
381
428
|
/>
|
|
382
|
-
<Heading level={
|
|
383
|
-
<
|
|
384
|
-
|
|
385
|
-
|
|
429
|
+
<Heading level={2}>{meta.title}</Heading>
|
|
430
|
+
<Popover
|
|
431
|
+
placement="below"
|
|
432
|
+
width="min(20rem, calc(100vw - 2 * var(--space-viewport)))"
|
|
433
|
+
label={`${meta.title} column help`}
|
|
434
|
+
content={
|
|
435
|
+
<Text type="body" color="secondary">
|
|
436
|
+
{meta.tooltip}
|
|
437
|
+
</Text>
|
|
438
|
+
}>
|
|
439
|
+
<IconButton
|
|
440
|
+
label={`About the ${meta.title} column`}
|
|
441
|
+
icon={<Icon icon={Info} size="sm" />}
|
|
442
|
+
variant="ghost"
|
|
443
|
+
size="sm"
|
|
444
|
+
/>
|
|
445
|
+
</Popover>
|
|
386
446
|
</HStack>
|
|
387
447
|
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
388
448
|
{count}
|
|
@@ -391,7 +451,7 @@ function BoardColumn({
|
|
|
391
451
|
</LayoutHeader>
|
|
392
452
|
}
|
|
393
453
|
content={
|
|
394
|
-
<LayoutContent ref={contentRef} padding={
|
|
454
|
+
<LayoutContent ref={contentRef} padding={3}>
|
|
395
455
|
{children ?? (
|
|
396
456
|
<EmptyState
|
|
397
457
|
isCompact
|
|
@@ -459,18 +519,7 @@ export default function KanbanBoard() {
|
|
|
459
519
|
return cb;
|
|
460
520
|
};
|
|
461
521
|
|
|
462
|
-
const itemsByColumn =
|
|
463
|
-
const map: Record<ColumnId, WorkItem[]> = {
|
|
464
|
-
todo: [],
|
|
465
|
-
'in-progress': [],
|
|
466
|
-
'in-review': [],
|
|
467
|
-
done: [],
|
|
468
|
-
};
|
|
469
|
-
for (const item of items) {
|
|
470
|
-
map[item.column].push(item);
|
|
471
|
-
}
|
|
472
|
-
return map;
|
|
473
|
-
}, [items]);
|
|
522
|
+
const itemsByColumn = groupByColumn(items);
|
|
474
523
|
|
|
475
524
|
const moveItem = (id: string, to: ColumnId) => {
|
|
476
525
|
setItems(prev =>
|
|
@@ -491,9 +540,12 @@ export default function KanbanBoard() {
|
|
|
491
540
|
continue;
|
|
492
541
|
}
|
|
493
542
|
|
|
494
|
-
const ids =
|
|
495
|
-
|
|
496
|
-
|
|
543
|
+
const ids: string[] = [];
|
|
544
|
+
for (const it of itemsByColumn[colId]) {
|
|
545
|
+
if (it.id !== draggedId) {
|
|
546
|
+
ids.push(it.id);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
497
549
|
|
|
498
550
|
let index = ids.length;
|
|
499
551
|
for (let i = 0; i < ids.length; i++) {
|
|
@@ -659,13 +711,13 @@ export default function KanbanBoard() {
|
|
|
659
711
|
<Layout
|
|
660
712
|
height="fill"
|
|
661
713
|
header={
|
|
662
|
-
<LayoutHeader hasDivider padding={
|
|
714
|
+
<LayoutHeader hasDivider padding={6}>
|
|
663
715
|
<Toolbar
|
|
664
716
|
label="Board actions"
|
|
665
717
|
gap={2}
|
|
666
718
|
startContent={
|
|
667
719
|
<>
|
|
668
|
-
<Heading level={
|
|
720
|
+
<Heading level={1} type="display-2">Sprint Board</Heading>
|
|
669
721
|
<Badge label={items.length} variant="neutral" />
|
|
670
722
|
</>
|
|
671
723
|
}
|
|
@@ -714,8 +766,14 @@ export default function KanbanBoard() {
|
|
|
714
766
|
</LayoutHeader>
|
|
715
767
|
}
|
|
716
768
|
content={
|
|
717
|
-
<LayoutContent padding={0}>
|
|
718
|
-
<Grid
|
|
769
|
+
<LayoutContent padding={0} isScrollable={false}>
|
|
770
|
+
<Grid
|
|
771
|
+
columns={{ minWidth: 280, max: 4 }}
|
|
772
|
+
gap={4}
|
|
773
|
+
style={boardColumnsStyle}
|
|
774
|
+
tabIndex={0}
|
|
775
|
+
role="region"
|
|
776
|
+
aria-label="Sprint board columns">
|
|
719
777
|
{COLUMNS.map(meta => (
|
|
720
778
|
<BoardColumn
|
|
721
779
|
key={meta.id}
|
|
@@ -732,10 +790,11 @@ export default function KanbanBoard() {
|
|
|
732
790
|
{drag && draggedItem ? (
|
|
733
791
|
<Card
|
|
734
792
|
padding={3}
|
|
735
|
-
elevation="
|
|
793
|
+
elevation="none"
|
|
736
794
|
style={{
|
|
737
795
|
...floatingStyle,
|
|
738
796
|
width: drag.width,
|
|
797
|
+
borderColor: 'var(--color-border-emphasized)',
|
|
739
798
|
transform: `translate(${drag.pointerX - drag.offsetX}px, ${drag.pointerY - drag.offsetY}px)`,
|
|
740
799
|
}}>
|
|
741
800
|
<BoardCardBody item={draggedItem} onMove={() => {}} />
|
package/templates/library.tsx
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
+
// L > (LH[divider] > (V[g=1] > Hd"Library"[level=1] + Tx"30 components across 5 categories"[t=body])) + (LC[p=6] > V[g=8] > (V[g=4] > TI"Search the stacks…" + (H[a=center g=3] > (SI[fill] > TgG"Filter" > OFL > Tg"All"! + Tg"Layout" + Tg"Forms" + Tg"Navigation" + Tg"Feedback" + Tg"Data") + DM"Sort")) + D + (V[g=8] > (H[j=between a=center] > Hd"Layout"[level=2] + Tx"6 items"[t=body]) + (G[c={min:280} g=4] > (C[p=0] > AR + (S.transparent[p=4] > V[g=1] > Hd"Card"[level=3] + Tx"Description"[t=supporting]))*4)))
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Library — a browsable grid of design-system entries grouped by category. Frame: page header (title) | content column (search, filter row, s (Frame/responsive/container: see XLE header above.)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {useState, useMemo} from 'react';
|
|
4
10
|
import {Layout, LayoutHeader, LayoutContent} from '@astryxdesign/core/Layout';
|
|
5
11
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
6
12
|
import {Card} from '@astryxdesign/core/Card';
|
|
@@ -17,6 +23,8 @@ import {Button} from '@astryxdesign/core/Button';
|
|
|
17
23
|
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
18
24
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
19
25
|
import {Search} from 'lucide-react';
|
|
26
|
+
import {SceneTile} from 'astryx-dracula/shared/scene-tile';
|
|
27
|
+
import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
|
|
20
28
|
|
|
21
29
|
interface LibraryItem {
|
|
22
30
|
id: string;
|
|
@@ -26,9 +34,11 @@ interface LibraryItem {
|
|
|
26
34
|
type: 'Component' | 'Pattern' | 'Utility';
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
// Dracula accent per category shelf
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
// One Dracula accent per category shelf. Purple stays out of the map: it is
|
|
38
|
+
// reserved for interactive elements, and these hues only tint the thumbnail
|
|
39
|
+
// dot, so each shelf takes a non-purple accent instead.
|
|
40
|
+
const CATEGORY_HUES: Record<string, SceneHue> = {
|
|
41
|
+
Layout: 'var(--dracula-orange)',
|
|
32
42
|
Forms: 'var(--dracula-cyan)',
|
|
33
43
|
Navigation: 'var(--dracula-pink)',
|
|
34
44
|
Feedback: 'var(--dracula-yellow)',
|
|
@@ -268,40 +278,16 @@ const ITEMS: LibraryItem[] = [
|
|
|
268
278
|
},
|
|
269
279
|
];
|
|
270
280
|
|
|
271
|
-
const thumbnailImage: CSSProperties = {
|
|
272
|
-
width: '100%',
|
|
273
|
-
height: '100%',
|
|
274
|
-
display: 'block',
|
|
275
|
-
};
|
|
276
|
-
|
|
277
281
|
// =============================================================================
|
|
278
282
|
// Side Nav
|
|
279
283
|
// =============================================================================
|
|
280
284
|
|
|
281
285
|
function LibraryCard({item}: {item: LibraryItem}) {
|
|
282
|
-
const hue = CATEGORY_HUES[item.category] ?? 'var(--dracula-
|
|
286
|
+
const hue = CATEGORY_HUES[item.category] ?? 'var(--dracula-comment)';
|
|
283
287
|
return (
|
|
284
288
|
<Card padding={0}>
|
|
285
289
|
<AspectRatio ratio={16 / 9}>
|
|
286
|
-
<
|
|
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>
|
|
290
|
+
<SceneTile label={`${item.name} thumbnail`} hue={hue} size="lg" />
|
|
305
291
|
</AspectRatio>
|
|
306
292
|
<Section variant="transparent" padding={4}>
|
|
307
293
|
<VStack gap={1}>
|
|
@@ -323,14 +309,14 @@ function LibrarySection({
|
|
|
323
309
|
items: LibraryItem[];
|
|
324
310
|
}) {
|
|
325
311
|
return (
|
|
326
|
-
<VStack gap={
|
|
312
|
+
<VStack gap={8}>
|
|
327
313
|
<HStack justify="between" vAlign="center">
|
|
328
314
|
<Heading level={2}>{category}</Heading>
|
|
329
|
-
<Text type="
|
|
315
|
+
<Text type="body" color="secondary" hasTabularNumbers>
|
|
330
316
|
{items.length} {items.length === 1 ? 'item' : 'items'}
|
|
331
317
|
</Text>
|
|
332
318
|
</HStack>
|
|
333
|
-
<Grid columns={{minWidth:
|
|
319
|
+
<Grid columns={{minWidth: 280}} gap={4}>
|
|
334
320
|
{items.map(item => (
|
|
335
321
|
<LibraryCard key={item.id} item={item} />
|
|
336
322
|
))}
|
|
@@ -380,32 +366,43 @@ export default function LibraryGrid() {
|
|
|
380
366
|
}
|
|
381
367
|
group.push(item);
|
|
382
368
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
369
|
+
const result: Array<{category: string; items: LibraryItem[]}> = [];
|
|
370
|
+
for (const cat of order) {
|
|
371
|
+
const items = map.get(cat);
|
|
372
|
+
if (items) {
|
|
373
|
+
result.push({category: cat, items});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
386
377
|
}, [activeTab, filtered]);
|
|
387
378
|
|
|
388
379
|
return (
|
|
389
380
|
<Layout
|
|
381
|
+
height="fill"
|
|
390
382
|
header={
|
|
391
383
|
<LayoutHeader hasDivider padding={6}>
|
|
392
|
-
<
|
|
384
|
+
<VStack gap={1}>
|
|
385
|
+
<Heading level={1}>Library</Heading>
|
|
386
|
+
<Text type="body" color="secondary">
|
|
387
|
+
30 components across 5 categories
|
|
388
|
+
</Text>
|
|
389
|
+
</VStack>
|
|
393
390
|
</LayoutHeader>
|
|
394
391
|
}
|
|
395
392
|
content={
|
|
396
393
|
<LayoutContent padding={6}>
|
|
397
|
-
<VStack gap={
|
|
394
|
+
<VStack gap={8}>
|
|
398
395
|
<VStack gap={4}>
|
|
399
396
|
<TextInput
|
|
400
397
|
label="Search"
|
|
401
398
|
isLabelHidden
|
|
402
|
-
placeholder="Search the stacks
|
|
399
|
+
placeholder="Search the stacks…"
|
|
403
400
|
value={search}
|
|
404
401
|
onChange={setSearch}
|
|
405
402
|
startIcon={Search}
|
|
406
403
|
size="lg"
|
|
407
404
|
/>
|
|
408
|
-
<HStack vAlign="center" gap={
|
|
405
|
+
<HStack vAlign="center" gap={3}>
|
|
409
406
|
<StackItem size="fill">
|
|
410
407
|
<VStack>
|
|
411
408
|
<ToggleButtonGroup
|
|
@@ -468,7 +465,7 @@ export default function LibraryGrid() {
|
|
|
468
465
|
}
|
|
469
466
|
/>
|
|
470
467
|
) : (
|
|
471
|
-
<VStack gap={
|
|
468
|
+
<VStack gap={8}>
|
|
472
469
|
{(
|
|
473
470
|
groupedSections ?? [{category: activeTab, items: filtered}]
|
|
474
471
|
).flatMap(section => [
|