@weave-design-system/react 0.1.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/dist/index.cjs +7729 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +677 -0
- package/dist/index.d.ts +677 -0
- package/dist/index.js +7654 -0
- package/dist/index.js.map +1 -0
- package/dist/theme.css +2 -0
- package/dist/tokens.cjs +78 -0
- package/dist/tokens.cjs.map +1 -0
- package/dist/tokens.d.cts +67 -0
- package/dist/tokens.d.ts +67 -0
- package/dist/tokens.js +70 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +103 -0
- package/src/data-display/activity-feed/ActivityFeed.stories.tsx +16 -0
- package/src/data-display/activity-feed/ActivityFeed.test.tsx +11 -0
- package/src/data-display/activity-feed/ActivityFeed.tsx +34 -0
- package/src/data-display/activity-feed/index.ts +2 -0
- package/src/data-display/circular-progress/CircularProgress.stories.tsx +10 -0
- package/src/data-display/circular-progress/CircularProgress.test.tsx +11 -0
- package/src/data-display/circular-progress/CircularProgress.tsx +35 -0
- package/src/data-display/circular-progress/index.ts +2 -0
- package/src/data-display/empty-state/EmptyState.stories.tsx +15 -0
- package/src/data-display/empty-state/EmptyState.test.tsx +18 -0
- package/src/data-display/empty-state/EmptyState.tsx +34 -0
- package/src/data-display/empty-state/index.ts +2 -0
- package/src/data-display/progress-bar/ProgressBar.stories.tsx +10 -0
- package/src/data-display/progress-bar/ProgressBar.test.tsx +15 -0
- package/src/data-display/progress-bar/ProgressBar.tsx +38 -0
- package/src/data-display/progress-bar/index.ts +2 -0
- package/src/data-display/stat-card/StatCard.stories.tsx +10 -0
- package/src/data-display/stat-card/StatCard.test.tsx +15 -0
- package/src/data-display/stat-card/StatCard.tsx +40 -0
- package/src/data-display/stat-card/index.ts +2 -0
- package/src/data-display/table/Table.stories.tsx +44 -0
- package/src/data-display/table/Table.test.tsx +16 -0
- package/src/data-display/table/Table.tsx +71 -0
- package/src/data-display/table/index.ts +1 -0
- package/src/data-display/timeline/Timeline.stories.tsx +16 -0
- package/src/data-display/timeline/Timeline.test.tsx +11 -0
- package/src/data-display/timeline/Timeline.tsx +44 -0
- package/src/data-display/timeline/index.ts +2 -0
- package/src/docs/ComponentOverview.mdx +192 -0
- package/src/docs/DesignTokens.mdx +235 -0
- package/src/docs/GettingStarted.mdx +145 -0
- package/src/feedback/alert-banner/AlertBanner.stories.tsx +10 -0
- package/src/feedback/alert-banner/AlertBanner.test.tsx +16 -0
- package/src/feedback/alert-banner/AlertBanner.tsx +47 -0
- package/src/feedback/alert-banner/index.ts +2 -0
- package/src/feedback/modal/Modal.stories.tsx +31 -0
- package/src/feedback/modal/Modal.test.tsx +33 -0
- package/src/feedback/modal/Modal.tsx +88 -0
- package/src/feedback/modal/index.ts +2 -0
- package/src/feedback/skeleton-loader/SkeletonLoader.stories.tsx +23 -0
- package/src/feedback/skeleton-loader/SkeletonLoader.test.tsx +14 -0
- package/src/feedback/skeleton-loader/SkeletonLoader.tsx +61 -0
- package/src/feedback/skeleton-loader/index.ts +2 -0
- package/src/feedback/toast/Toast.stories.tsx +27 -0
- package/src/feedback/toast/Toast.test.tsx +32 -0
- package/src/feedback/toast/Toast.tsx +106 -0
- package/src/feedback/toast/index.ts +2 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-controllable-state.ts +34 -0
- package/src/hooks/use-focus-trap.ts +56 -0
- package/src/hooks/use-reduced-motion.ts +17 -0
- package/src/index.ts +148 -0
- package/src/layout/card/Card.stories.tsx +45 -0
- package/src/layout/card/Card.test.tsx +23 -0
- package/src/layout/card/Card.tsx +42 -0
- package/src/layout/card/Card.types.ts +6 -0
- package/src/layout/card/index.ts +2 -0
- package/src/layout/command-palette/CommandPalette.stories.tsx +34 -0
- package/src/layout/command-palette/CommandPalette.test.tsx +43 -0
- package/src/layout/command-palette/CommandPalette.tsx +188 -0
- package/src/layout/command-palette/CommandPalette.types.ts +18 -0
- package/src/layout/command-palette/index.ts +2 -0
- package/src/layout/sidebar/Sidebar.stories.tsx +60 -0
- package/src/layout/sidebar/Sidebar.test.tsx +27 -0
- package/src/layout/sidebar/Sidebar.tsx +57 -0
- package/src/layout/sidebar/Sidebar.types.ts +14 -0
- package/src/layout/sidebar/index.ts +2 -0
- package/src/layout/top-bar/TopBar.stories.tsx +51 -0
- package/src/layout/top-bar/TopBar.test.tsx +18 -0
- package/src/layout/top-bar/TopBar.tsx +19 -0
- package/src/layout/top-bar/TopBar.types.ts +10 -0
- package/src/layout/top-bar/index.ts +2 -0
- package/src/navigation/breadcrumbs/Breadcrumbs.stories.tsx +30 -0
- package/src/navigation/breadcrumbs/Breadcrumbs.test.tsx +43 -0
- package/src/navigation/breadcrumbs/Breadcrumbs.tsx +45 -0
- package/src/navigation/breadcrumbs/Breadcrumbs.types.ts +12 -0
- package/src/navigation/breadcrumbs/index.ts +2 -0
- package/src/navigation/sidebar-nav-item/SidebarNavItem.stories.tsx +41 -0
- package/src/navigation/sidebar-nav-item/SidebarNavItem.test.tsx +46 -0
- package/src/navigation/sidebar-nav-item/SidebarNavItem.tsx +38 -0
- package/src/navigation/sidebar-nav-item/SidebarNavItem.types.ts +12 -0
- package/src/navigation/sidebar-nav-item/index.ts +2 -0
- package/src/navigation/tabs/Tabs.stories.tsx +47 -0
- package/src/navigation/tabs/Tabs.test.tsx +67 -0
- package/src/navigation/tabs/Tabs.tsx +111 -0
- package/src/navigation/tabs/Tabs.types.ts +36 -0
- package/src/navigation/tabs/index.ts +2 -0
- package/src/patterns/accordion/Accordion.stories.tsx +25 -0
- package/src/patterns/accordion/Accordion.test.tsx +44 -0
- package/src/patterns/accordion/Accordion.tsx +92 -0
- package/src/patterns/accordion/index.ts +2 -0
- package/src/patterns/action-menu/ActionMenu.stories.tsx +18 -0
- package/src/patterns/action-menu/ActionMenu.test.tsx +18 -0
- package/src/patterns/action-menu/ActionMenu.tsx +41 -0
- package/src/patterns/action-menu/index.ts +2 -0
- package/src/patterns/carousel/Carousel.stories.tsx +16 -0
- package/src/patterns/carousel/Carousel.test.tsx +16 -0
- package/src/patterns/carousel/Carousel.tsx +69 -0
- package/src/patterns/carousel/index.ts +2 -0
- package/src/patterns/image-placeholder/ImagePlaceholder.stories.tsx +9 -0
- package/src/patterns/image-placeholder/ImagePlaceholder.test.tsx +10 -0
- package/src/patterns/image-placeholder/ImagePlaceholder.tsx +21 -0
- package/src/patterns/image-placeholder/index.ts +2 -0
- package/src/patterns/notification-dot/NotificationDot.stories.tsx +17 -0
- package/src/patterns/notification-dot/NotificationDot.test.tsx +14 -0
- package/src/patterns/notification-dot/NotificationDot.tsx +18 -0
- package/src/patterns/notification-dot/index.ts +2 -0
- package/src/patterns/pagination/Pagination.stories.tsx +14 -0
- package/src/patterns/pagination/Pagination.test.tsx +22 -0
- package/src/patterns/pagination/Pagination.tsx +67 -0
- package/src/patterns/pagination/index.ts +2 -0
- package/src/primitives/avatar/Avatar.stories.tsx +46 -0
- package/src/primitives/avatar/Avatar.test.tsx +35 -0
- package/src/primitives/avatar/Avatar.tsx +49 -0
- package/src/primitives/avatar/Avatar.types.ts +21 -0
- package/src/primitives/avatar/AvatarGroup.tsx +27 -0
- package/src/primitives/avatar/index.ts +3 -0
- package/src/primitives/badge/Badge.stories.tsx +28 -0
- package/src/primitives/badge/Badge.test.tsx +23 -0
- package/src/primitives/badge/Badge.tsx +44 -0
- package/src/primitives/badge/Badge.types.ts +14 -0
- package/src/primitives/badge/index.ts +2 -0
- package/src/primitives/button/Button.stories.tsx +81 -0
- package/src/primitives/button/Button.test.tsx +64 -0
- package/src/primitives/button/Button.tsx +85 -0
- package/src/primitives/button/Button.types.ts +17 -0
- package/src/primitives/button/index.ts +2 -0
- package/src/primitives/checkbox/Checkbox.stories.tsx +27 -0
- package/src/primitives/checkbox/Checkbox.test.tsx +30 -0
- package/src/primitives/checkbox/Checkbox.tsx +79 -0
- package/src/primitives/checkbox/Checkbox.types.ts +12 -0
- package/src/primitives/checkbox/index.ts +2 -0
- package/src/primitives/combobox/Combobox.stories.tsx +44 -0
- package/src/primitives/combobox/Combobox.test.tsx +44 -0
- package/src/primitives/combobox/Combobox.tsx +201 -0
- package/src/primitives/combobox/Combobox.types.ts +25 -0
- package/src/primitives/combobox/index.ts +2 -0
- package/src/primitives/date-input/DateInput.stories.tsx +23 -0
- package/src/primitives/date-input/DateInput.test.tsx +22 -0
- package/src/primitives/date-input/DateInput.tsx +66 -0
- package/src/primitives/date-input/DateInput.types.ts +10 -0
- package/src/primitives/date-input/index.ts +2 -0
- package/src/primitives/file-upload/FileUploadDropzone.stories.tsx +27 -0
- package/src/primitives/file-upload/FileUploadDropzone.test.tsx +26 -0
- package/src/primitives/file-upload/FileUploadDropzone.tsx +99 -0
- package/src/primitives/file-upload/FileUploadDropzone.types.ts +14 -0
- package/src/primitives/file-upload/index.ts +2 -0
- package/src/primitives/input/InputGroup.stories.tsx +31 -0
- package/src/primitives/input/InputGroup.test.tsx +40 -0
- package/src/primitives/input/InputGroup.tsx +65 -0
- package/src/primitives/input/InputGroup.types.ts +12 -0
- package/src/primitives/input/index.ts +2 -0
- package/src/primitives/link/Link.stories.tsx +28 -0
- package/src/primitives/link/Link.test.tsx +23 -0
- package/src/primitives/link/Link.tsx +28 -0
- package/src/primitives/link/Link.types.ts +8 -0
- package/src/primitives/link/index.ts +2 -0
- package/src/primitives/radio/Radio.stories.tsx +29 -0
- package/src/primitives/radio/Radio.test.tsx +32 -0
- package/src/primitives/radio/Radio.tsx +59 -0
- package/src/primitives/radio/Radio.types.ts +6 -0
- package/src/primitives/radio/index.ts +2 -0
- package/src/primitives/select/SelectGroup.stories.tsx +33 -0
- package/src/primitives/select/SelectGroup.test.tsx +34 -0
- package/src/primitives/select/SelectGroup.tsx +72 -0
- package/src/primitives/select/SelectGroup.types.ts +12 -0
- package/src/primitives/select/index.ts +2 -0
- package/src/primitives/slider/Slider.stories.tsx +23 -0
- package/src/primitives/slider/Slider.test.tsx +28 -0
- package/src/primitives/slider/Slider.tsx +80 -0
- package/src/primitives/slider/Slider.types.ts +22 -0
- package/src/primitives/slider/index.ts +2 -0
- package/src/primitives/textarea/TextareaGroup.stories.tsx +27 -0
- package/src/primitives/textarea/TextareaGroup.test.tsx +24 -0
- package/src/primitives/textarea/TextareaGroup.tsx +59 -0
- package/src/primitives/textarea/TextareaGroup.types.ts +10 -0
- package/src/primitives/textarea/index.ts +2 -0
- package/src/primitives/toggle/Toggle.stories.tsx +27 -0
- package/src/primitives/toggle/Toggle.test.tsx +31 -0
- package/src/primitives/toggle/Toggle.tsx +65 -0
- package/src/primitives/toggle/Toggle.types.ts +16 -0
- package/src/primitives/toggle/index.ts +2 -0
- package/src/primitives/tooltip/Tooltip.stories.tsx +45 -0
- package/src/primitives/tooltip/Tooltip.test.tsx +28 -0
- package/src/primitives/tooltip/Tooltip.tsx +94 -0
- package/src/primitives/tooltip/Tooltip.types.ts +16 -0
- package/src/primitives/tooltip/index.ts +2 -0
- package/src/productivity/comment-thread/CommentThread.stories.tsx +20 -0
- package/src/productivity/comment-thread/CommentThread.test.tsx +21 -0
- package/src/productivity/comment-thread/CommentThread.tsx +47 -0
- package/src/productivity/comment-thread/index.ts +2 -0
- package/src/productivity/kanban-board/KanbanBoard.tsx +41 -0
- package/src/productivity/kanban-board/index.ts +2 -0
- package/src/productivity/kanban-column/KanbanColumn.stories.tsx +131 -0
- package/src/productivity/kanban-column/KanbanColumn.test.tsx +18 -0
- package/src/productivity/kanban-column/KanbanColumn.tsx +58 -0
- package/src/productivity/kanban-column/SortableTaskCard.tsx +46 -0
- package/src/productivity/kanban-column/index.ts +4 -0
- package/src/productivity/priority-selector/PrioritySelector.stories.tsx +14 -0
- package/src/productivity/priority-selector/PrioritySelector.test.tsx +18 -0
- package/src/productivity/priority-selector/PrioritySelector.tsx +40 -0
- package/src/productivity/priority-selector/index.ts +2 -0
- package/src/productivity/rich-text-toolbar/RichTextToolbar.stories.tsx +19 -0
- package/src/productivity/rich-text-toolbar/RichTextToolbar.test.tsx +21 -0
- package/src/productivity/rich-text-toolbar/RichTextToolbar.tsx +50 -0
- package/src/productivity/rich-text-toolbar/index.ts +2 -0
- package/src/productivity/task-card/TaskCard.stories.tsx +20 -0
- package/src/productivity/task-card/TaskCard.test.tsx +21 -0
- package/src/productivity/task-card/TaskCard.tsx +76 -0
- package/src/productivity/task-card/index.ts +2 -0
- package/src/test-setup.ts +1 -0
- package/src/tokens/index.ts +1 -0
- package/src/tokens/tokens.ts +71 -0
- package/src/tokens/weave-theme.css +168 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
{/* @jsxImportSource react */}
|
|
2
|
+
import { Meta } from '@storybook/blocks';
|
|
3
|
+
|
|
4
|
+
<Meta title="Design Tokens" />
|
|
5
|
+
|
|
6
|
+
# Design Tokens
|
|
7
|
+
|
|
8
|
+
All tokens are available as CSS custom properties (via `theme.css`) and as JavaScript constants (via `@weave-ds/react/tokens`).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Colors
|
|
13
|
+
|
|
14
|
+
Every color has a physical-world reference — clay, linen, moss, honey, charcoal.
|
|
15
|
+
|
|
16
|
+
### Neutrals
|
|
17
|
+
|
|
18
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', gap: 12, marginTop: 16 }}>
|
|
19
|
+
<ColorSwatch color="#FAF7F2" name="Background" token="--color-bg" />
|
|
20
|
+
<ColorSwatch color="#F0EBE3" name="Surface" token="--color-surface" />
|
|
21
|
+
<ColorSwatch color="#D9D2C7" name="Border" token="--color-border" />
|
|
22
|
+
<ColorSwatch color="#2C2825" name="Text Primary" token="--color-text-primary" dark />
|
|
23
|
+
<ColorSwatch color="#6B635A" name="Text Secondary" token="--color-text-secondary" dark />
|
|
24
|
+
<ColorSwatch color="#FFFFFF" name="White" token="--color-white" border />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
### Primary (Rust)
|
|
28
|
+
|
|
29
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', gap: 12, marginTop: 16 }}>
|
|
30
|
+
<ColorSwatch color="#B85C38" name="Rust" token="--color-rust" dark />
|
|
31
|
+
<ColorSwatch color="#F0D5C5" name="Rust Light" token="--color-rust-light" />
|
|
32
|
+
<ColorSwatch color="#8E4429" name="Rust Dark" token="--color-rust-dark" dark />
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
### Secondary (Forest)
|
|
36
|
+
|
|
37
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', gap: 12, marginTop: 16 }}>
|
|
38
|
+
<ColorSwatch color="#3D6B5E" name="Forest" token="--color-forest" dark />
|
|
39
|
+
<ColorSwatch color="#D4E5DF" name="Forest Light" token="--color-forest-light" />
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
### Semantic
|
|
43
|
+
|
|
44
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', gap: 12, marginTop: 16 }}>
|
|
45
|
+
<ColorSwatch color="#5E8A6B" name="Success" token="--color-success" dark />
|
|
46
|
+
<ColorSwatch color="#DDE9E0" name="Success Light" token="--color-success-light" />
|
|
47
|
+
<ColorSwatch color="#C49A3C" name="Warning" token="--color-warning" dark />
|
|
48
|
+
<ColorSwatch color="#F5EDD4" name="Warning Light" token="--color-warning-light" />
|
|
49
|
+
<ColorSwatch color="#B84A3C" name="Error" token="--color-error" dark />
|
|
50
|
+
<ColorSwatch color="#F3D9D5" name="Error Light" token="--color-error-light" />
|
|
51
|
+
<ColorSwatch color="#7A8A9A" name="Info" token="--color-info" dark />
|
|
52
|
+
<ColorSwatch color="#E0E5EA" name="Info Light" token="--color-info-light" />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
### Delight
|
|
56
|
+
|
|
57
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', gap: 12, marginTop: 16 }}>
|
|
58
|
+
<ColorSwatch color="#D4923A" name="Saffron" token="--color-saffron" dark />
|
|
59
|
+
<ColorSwatch color="#F5E3C8" name="Saffron Light" token="--color-saffron-light" />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Typography
|
|
65
|
+
|
|
66
|
+
Two-tier type system: **Satoshi** (humanist sans-serif) + **JetBrains Mono** (code).
|
|
67
|
+
|
|
68
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, marginTop: 16 }}>
|
|
69
|
+
<TypeSample name="Display" size="40px" lineHeight="48px" weight="700" />
|
|
70
|
+
<TypeSample name="Heading 1" size="32px" lineHeight="40px" weight="700" />
|
|
71
|
+
<TypeSample name="Heading 2" size="26px" lineHeight="34px" weight="600" />
|
|
72
|
+
<TypeSample name="Heading 3" size="22px" lineHeight="30px" weight="600" />
|
|
73
|
+
<TypeSample name="Body Large" size="18px" lineHeight="28px" weight="400" />
|
|
74
|
+
<TypeSample name="Body" size="16px" lineHeight="24px" weight="400" />
|
|
75
|
+
<TypeSample name="Body Small" size="14px" lineHeight="20px" weight="400" />
|
|
76
|
+
<TypeSample name="Caption" size="12px" lineHeight="16px" weight="500" />
|
|
77
|
+
<TypeSample name="Code" size="14px" lineHeight="22px" weight="400" mono />
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Spacing
|
|
83
|
+
|
|
84
|
+
4px base grid. All spacing derives from `--spacing: 4px`.
|
|
85
|
+
|
|
86
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 16 }}>
|
|
87
|
+
<SpacingSample token="space-1" value="4px" />
|
|
88
|
+
<SpacingSample token="space-2" value="8px" />
|
|
89
|
+
<SpacingSample token="space-3" value="12px" />
|
|
90
|
+
<SpacingSample token="space-4" value="16px" />
|
|
91
|
+
<SpacingSample token="space-6" value="24px" />
|
|
92
|
+
<SpacingSample token="space-8" value="32px" />
|
|
93
|
+
<SpacingSample token="space-12" value="48px" />
|
|
94
|
+
<SpacingSample token="space-16" value="64px" />
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Border Radius
|
|
100
|
+
|
|
101
|
+
<div style={{ display: 'flex', gap: 16, alignItems: 'end', marginTop: 16 }}>
|
|
102
|
+
<RadiusSample name="sm" value="6px" />
|
|
103
|
+
<RadiusSample name="md" value="10px" />
|
|
104
|
+
<RadiusSample name="lg" value="16px" />
|
|
105
|
+
<RadiusSample name="xl" value="24px" />
|
|
106
|
+
<RadiusSample name="full" value="9999px" />
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Elevation
|
|
112
|
+
|
|
113
|
+
Warm-tinted shadows using `rgba(44, 40, 37, ...)` instead of pure black.
|
|
114
|
+
|
|
115
|
+
<div style={{ display: 'flex', gap: 24, marginTop: 16 }}>
|
|
116
|
+
<ElevationSample level={1} shadow="0 1px 3px rgba(44,40,37,0.06)" />
|
|
117
|
+
<ElevationSample level={2} shadow="0 4px 12px rgba(44,40,37,0.08)" />
|
|
118
|
+
<ElevationSample level={3} shadow="0 8px 24px rgba(44,40,37,0.12)" />
|
|
119
|
+
<ElevationSample level={4} shadow="0 16px 48px rgba(44,40,37,0.16)" />
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Motion
|
|
125
|
+
|
|
126
|
+
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
|
|
127
|
+
<thead>
|
|
128
|
+
<tr style={{ borderBottom: '2px solid #D9D2C7', textAlign: 'left' }}>
|
|
129
|
+
<th style={{ padding: '8px 12px', color: '#6B635A', fontWeight: 600 }}>Token</th>
|
|
130
|
+
<th style={{ padding: '8px 12px', color: '#6B635A', fontWeight: 600 }}>Value</th>
|
|
131
|
+
<th style={{ padding: '8px 12px', color: '#6B635A', fontWeight: 600 }}>Usage</th>
|
|
132
|
+
</tr>
|
|
133
|
+
</thead>
|
|
134
|
+
<tbody>
|
|
135
|
+
<tr style={{ borderBottom: '1px solid #D9D2C7' }}>
|
|
136
|
+
<td style={{ padding: '8px 12px' }}><code>--duration-micro</code></td>
|
|
137
|
+
<td style={{ padding: '8px 12px' }}>100ms</td>
|
|
138
|
+
<td style={{ padding: '8px 12px', color: '#6B635A' }}>Hover colors, focus rings</td>
|
|
139
|
+
</tr>
|
|
140
|
+
<tr style={{ borderBottom: '1px solid #D9D2C7' }}>
|
|
141
|
+
<td style={{ padding: '8px 12px' }}><code>--duration-fast</code></td>
|
|
142
|
+
<td style={{ padding: '8px 12px' }}>200ms</td>
|
|
143
|
+
<td style={{ padding: '8px 12px', color: '#6B635A' }}>Button presses, toggles</td>
|
|
144
|
+
</tr>
|
|
145
|
+
<tr style={{ borderBottom: '1px solid #D9D2C7' }}>
|
|
146
|
+
<td style={{ padding: '8px 12px' }}><code>--duration-normal</code></td>
|
|
147
|
+
<td style={{ padding: '8px 12px' }}>350ms</td>
|
|
148
|
+
<td style={{ padding: '8px 12px', color: '#6B635A' }}>Dropdowns, sidebar toggle</td>
|
|
149
|
+
</tr>
|
|
150
|
+
<tr style={{ borderBottom: '1px solid #D9D2C7' }}>
|
|
151
|
+
<td style={{ padding: '8px 12px' }}><code>--duration-slow</code></td>
|
|
152
|
+
<td style={{ padding: '8px 12px' }}>500ms</td>
|
|
153
|
+
<td style={{ padding: '8px 12px', color: '#6B635A' }}>Modals, page transitions</td>
|
|
154
|
+
</tr>
|
|
155
|
+
<tr style={{ borderBottom: '1px solid #D9D2C7' }}>
|
|
156
|
+
<td style={{ padding: '8px 12px' }}><code>--ease-out</code></td>
|
|
157
|
+
<td style={{ padding: '8px 12px' }}><code>cubic-bezier(0.22, 1, 0.36, 1)</code></td>
|
|
158
|
+
<td style={{ padding: '8px 12px', color: '#6B635A' }}>Entrances</td>
|
|
159
|
+
</tr>
|
|
160
|
+
<tr style={{ borderBottom: '1px solid #D9D2C7' }}>
|
|
161
|
+
<td style={{ padding: '8px 12px' }}><code>--ease-in</code></td>
|
|
162
|
+
<td style={{ padding: '8px 12px' }}><code>cubic-bezier(0.55, 0, 1, 0.45)</code></td>
|
|
163
|
+
<td style={{ padding: '8px 12px', color: '#6B635A' }}>Exits</td>
|
|
164
|
+
</tr>
|
|
165
|
+
<tr>
|
|
166
|
+
<td style={{ padding: '8px 12px' }}><code>--ease-spring</code></td>
|
|
167
|
+
<td style={{ padding: '8px 12px' }}><code>cubic-bezier(0.34, 1.56, 0.64, 1)</code></td>
|
|
168
|
+
<td style={{ padding: '8px 12px', color: '#6B635A' }}>Interactive feedback</td>
|
|
169
|
+
</tr>
|
|
170
|
+
</tbody>
|
|
171
|
+
</table>
|
|
172
|
+
|
|
173
|
+
<p style={{ marginTop: 12, fontSize: 14, color: '#6B635A' }}>
|
|
174
|
+
All durations collapse to <code>0ms</code> when <code>prefers-reduced-motion: reduce</code> is active.
|
|
175
|
+
</p>
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
export function ColorSwatch({ color, name, token, dark, border }) {
|
|
180
|
+
return (
|
|
181
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
182
|
+
<div style={{
|
|
183
|
+
width: '100%', height: 56, borderRadius: 10, backgroundColor: color,
|
|
184
|
+
border: border ? '1px solid #D9D2C7' : 'none',
|
|
185
|
+
}} />
|
|
186
|
+
<span style={{ fontSize: 13, fontWeight: 600, color: '#2C2825' }}>{name}</span>
|
|
187
|
+
<code style={{ fontSize: 11, color: '#6B635A' }}>{token}</code>
|
|
188
|
+
<code style={{ fontSize: 11, color: '#6B635A' }}>{color}</code>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function TypeSample({ name, size, lineHeight, weight, mono }) {
|
|
194
|
+
return (
|
|
195
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 16 }}>
|
|
196
|
+
<span style={{ width: 100, fontSize: 12, color: '#6B635A', flexShrink: 0 }}>{name}</span>
|
|
197
|
+
<span style={{
|
|
198
|
+
fontFamily: mono ? "'JetBrains Mono', monospace" : "'Satoshi', sans-serif",
|
|
199
|
+
fontSize: size, lineHeight, fontWeight: weight, color: '#2C2825',
|
|
200
|
+
}}>
|
|
201
|
+
The quick brown fox
|
|
202
|
+
</span>
|
|
203
|
+
<span style={{ fontSize: 11, color: '#6B635A', flexShrink: 0 }}>{size}/{lineHeight}</span>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function SpacingSample({ token, value }) {
|
|
209
|
+
return (
|
|
210
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
211
|
+
<code style={{ width: 80, fontSize: 11, color: '#6B635A' }}>{token}</code>
|
|
212
|
+
<div style={{ width: parseInt(value), height: 16, borderRadius: 4, backgroundColor: '#B85C38' }} />
|
|
213
|
+
<span style={{ fontSize: 11, color: '#6B635A' }}>{value}</span>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function RadiusSample({ name, value }) {
|
|
219
|
+
return (
|
|
220
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
|
|
221
|
+
<div style={{ width: 56, height: 56, borderRadius: value, backgroundColor: '#F0EBE3', border: '2px solid #D9D2C7' }} />
|
|
222
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: '#2C2825' }}>{name}</span>
|
|
223
|
+
<code style={{ fontSize: 11, color: '#6B635A' }}>{value}</code>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function ElevationSample({ level, shadow }) {
|
|
229
|
+
return (
|
|
230
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
|
|
231
|
+
<div style={{ width: 80, height: 64, borderRadius: 10, backgroundColor: '#F0EBE3', boxShadow: shadow }} />
|
|
232
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: '#2C2825' }}>elevation-{level}</span>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{/* @jsxImportSource react */}
|
|
2
|
+
import { Meta } from '@storybook/blocks';
|
|
3
|
+
|
|
4
|
+
<Meta title="Getting Started" />
|
|
5
|
+
|
|
6
|
+
# Getting Started with Weave
|
|
7
|
+
|
|
8
|
+
Weave is a human-centered design system for productivity software. Warm over cold. Fluid over rigid. Connected over isolated.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# npm
|
|
14
|
+
npm install @weave-ds/react
|
|
15
|
+
|
|
16
|
+
# pnpm
|
|
17
|
+
pnpm add @weave-ds/react
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Peer Dependencies
|
|
21
|
+
|
|
22
|
+
Weave requires React 18 or 19:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install react react-dom
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
### 1. Import the theme CSS
|
|
31
|
+
|
|
32
|
+
Add this import to your app's entry point. This loads all design tokens (colors, typography, spacing, shadows, motion) and Tailwind utilities used by Weave components.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// app.tsx or main.tsx
|
|
36
|
+
import '@weave-ds/react/theme.css';
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Load the fonts
|
|
40
|
+
|
|
41
|
+
Weave uses [Satoshi](https://www.fontshare.com/fonts/satoshi) as the primary typeface and [JetBrains Mono](https://www.jetbrains.com/lp/mono/) for code. Add these to your HTML `<head>`:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,600,700&display=swap" rel="stylesheet">
|
|
45
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. Use components
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { Button, Card, Badge } from '@weave-ds/react';
|
|
52
|
+
|
|
53
|
+
function App() {
|
|
54
|
+
return (
|
|
55
|
+
<Card className="max-w-md">
|
|
56
|
+
<Card.Header>
|
|
57
|
+
<h2>My Project</h2>
|
|
58
|
+
</Card.Header>
|
|
59
|
+
<Card.Content>
|
|
60
|
+
<p>A warm, human-centered interface.</p>
|
|
61
|
+
<Badge variant="success">Active</Badge>
|
|
62
|
+
</Card.Content>
|
|
63
|
+
<Card.Actions>
|
|
64
|
+
<Button variant="ghost">Cancel</Button>
|
|
65
|
+
<Button>Save</Button>
|
|
66
|
+
</Card.Actions>
|
|
67
|
+
</Card>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 4. Toast notifications (optional)
|
|
73
|
+
|
|
74
|
+
Wrap your app in `ToastProvider` to enable toast notifications:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { ToastProvider } from '@weave-ds/react';
|
|
78
|
+
|
|
79
|
+
function App() {
|
|
80
|
+
return (
|
|
81
|
+
<ToastProvider>
|
|
82
|
+
<YourApp />
|
|
83
|
+
</ToastProvider>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then use the `useToast` hook anywhere:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { useToast, Button } from '@weave-ds/react';
|
|
92
|
+
|
|
93
|
+
function SaveButton() {
|
|
94
|
+
const { toast } = useToast();
|
|
95
|
+
return (
|
|
96
|
+
<Button onClick={() => toast({ title: 'Saved!', variant: 'success' })}>
|
|
97
|
+
Save
|
|
98
|
+
</Button>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Tailwind CSS Integration
|
|
104
|
+
|
|
105
|
+
If your app uses Tailwind CSS v4, add a `@source` directive so Tailwind generates the utility classes used by Weave components:
|
|
106
|
+
|
|
107
|
+
```css
|
|
108
|
+
/* your app's main CSS file */
|
|
109
|
+
@import "tailwindcss";
|
|
110
|
+
@source "../node_modules/@weave-ds/react/src";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This enables you to extend and override Weave's styles using standard Tailwind utilities.
|
|
114
|
+
|
|
115
|
+
## Programmatic Token Access
|
|
116
|
+
|
|
117
|
+
For cases where you need token values in JavaScript (charting libraries, dynamic styles):
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { colors, spacing, radii } from '@weave-ds/react/tokens';
|
|
121
|
+
|
|
122
|
+
console.log(colors.rust); // '#B85C38'
|
|
123
|
+
console.log(spacing[6]); // '24px'
|
|
124
|
+
console.log(radii.lg); // '16px'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Design Philosophy
|
|
128
|
+
|
|
129
|
+
Every decision in Weave traces back to one question: **does this feel human?**
|
|
130
|
+
|
|
131
|
+
- **Warm Over Cold** — Rounded corners, warm neutrals, earthy accents
|
|
132
|
+
- **Motion Is Meaning** — Purposeful animations that guide attention
|
|
133
|
+
- **Breathing Space** — Generous whitespace, progressive disclosure
|
|
134
|
+
- **Connected, Not Cluttered** — Visual relationships through proximity
|
|
135
|
+
- **Personality in Details** — Satisfying micro-interactions
|
|
136
|
+
|
|
137
|
+
## Accessibility
|
|
138
|
+
|
|
139
|
+
All components target **WCAG 2.1 AA** compliance:
|
|
140
|
+
|
|
141
|
+
- 4.5:1+ contrast ratios on all text
|
|
142
|
+
- Visible focus rings (2px rust outline)
|
|
143
|
+
- Full keyboard navigation
|
|
144
|
+
- ARIA attributes and screen reader support
|
|
145
|
+
- `prefers-reduced-motion` respected automatically
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { AlertBanner } from './AlertBanner';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof AlertBanner> = { title: 'Feedback/AlertBanner', component: AlertBanner, tags: ['autodocs'] };
|
|
5
|
+
export default meta;
|
|
6
|
+
type Story = StoryObj<typeof AlertBanner>;
|
|
7
|
+
|
|
8
|
+
export const Info: Story = { args: { message: 'A new version is available. Refresh to update.', variant: 'info' } };
|
|
9
|
+
export const Warning: Story = { args: { message: 'Your trial expires in 3 days.', variant: 'warning' } };
|
|
10
|
+
export const Error: Story = { args: { message: 'Failed to save changes. Please try again.', variant: 'error' } };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { AlertBanner } from './AlertBanner';
|
|
5
|
+
|
|
6
|
+
describe('AlertBanner', () => {
|
|
7
|
+
it('renders message', () => {
|
|
8
|
+
render(<AlertBanner message="Update available" />);
|
|
9
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Update available');
|
|
10
|
+
});
|
|
11
|
+
it('dismisses on close', async () => {
|
|
12
|
+
render(<AlertBanner message="Warning" />);
|
|
13
|
+
await userEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
|
|
14
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Info, AlertTriangle, AlertCircle, X } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../utils/cn';
|
|
4
|
+
|
|
5
|
+
export type AlertBannerVariant = 'info' | 'warning' | 'error';
|
|
6
|
+
|
|
7
|
+
export interface AlertBannerProps {
|
|
8
|
+
message: string;
|
|
9
|
+
variant?: AlertBannerVariant;
|
|
10
|
+
dismissible?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const icons: Record<AlertBannerVariant, React.ReactNode> = {
|
|
15
|
+
info: <Info size={18} />,
|
|
16
|
+
warning: <AlertTriangle size={18} />,
|
|
17
|
+
error: <AlertCircle size={18} />,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const variantStyles: Record<AlertBannerVariant, string> = {
|
|
21
|
+
info: 'bg-rust-light text-rust-dark',
|
|
22
|
+
warning: 'bg-warning-light text-warning',
|
|
23
|
+
error: 'bg-error-light text-error',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function AlertBanner({ message, variant = 'info', dismissible = true, className }: AlertBannerProps) {
|
|
27
|
+
const [visible, setVisible] = useState(true);
|
|
28
|
+
if (!visible) return null;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={cn('flex items-center gap-3 px-4 py-3 rounded-lg', variantStyles[variant], className)} role="alert">
|
|
32
|
+
<span className="shrink-0">{icons[variant]}</span>
|
|
33
|
+
<p className="flex-1" style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}>{message}</p>
|
|
34
|
+
{dismissible && (
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={() => setVisible(false)}
|
|
38
|
+
className="shrink-0 p-1 rounded hover:bg-black/10 transition-colors"
|
|
39
|
+
aria-label="Dismiss"
|
|
40
|
+
style={{ transitionDuration: 'var(--duration-micro)' }}
|
|
41
|
+
>
|
|
42
|
+
<X size={14} />
|
|
43
|
+
</button>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { Modal } from './Modal';
|
|
4
|
+
import { Button } from '../../primitives/button';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Modal> = { title: 'Feedback/Modal', component: Modal, tags: ['autodocs'] };
|
|
7
|
+
export default meta;
|
|
8
|
+
type Story = StoryObj<typeof Modal>;
|
|
9
|
+
|
|
10
|
+
export const Default: Story = {
|
|
11
|
+
render: () => {
|
|
12
|
+
const [open, setOpen] = useState(false);
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<Button onClick={() => setOpen(true)}>Open Modal</Button>
|
|
16
|
+
<Modal open={open} onClose={() => setOpen(false)}>
|
|
17
|
+
<Modal.Header onClose={() => setOpen(false)}>Confirm Action</Modal.Header>
|
|
18
|
+
<Modal.Body>
|
|
19
|
+
<p className="text-text-secondary" style={{ fontSize: 'var(--text-body)' }}>
|
|
20
|
+
Are you sure you want to delete this project? This action cannot be undone.
|
|
21
|
+
</p>
|
|
22
|
+
</Modal.Body>
|
|
23
|
+
<Modal.Footer>
|
|
24
|
+
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
|
|
25
|
+
<Button variant="destructive" onClick={() => setOpen(false)}>Delete</Button>
|
|
26
|
+
</Modal.Footer>
|
|
27
|
+
</Modal>
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { Modal } from './Modal';
|
|
5
|
+
|
|
6
|
+
describe('Modal', () => {
|
|
7
|
+
it('renders when open', () => {
|
|
8
|
+
render(<Modal open onClose={() => {}}>Content</Modal>);
|
|
9
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
it('does not render when closed', () => {
|
|
12
|
+
render(<Modal open={false} onClose={() => {}}>Content</Modal>);
|
|
13
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
it('closes on Escape', async () => {
|
|
16
|
+
const onClose = vi.fn();
|
|
17
|
+
render(<Modal open onClose={onClose}><button>Focus</button></Modal>);
|
|
18
|
+
await userEvent.keyboard('{Escape}');
|
|
19
|
+
expect(onClose).toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
it('renders compound parts', () => {
|
|
22
|
+
render(
|
|
23
|
+
<Modal open onClose={() => {}}>
|
|
24
|
+
<Modal.Header>Title</Modal.Header>
|
|
25
|
+
<Modal.Body>Body</Modal.Body>
|
|
26
|
+
<Modal.Footer>Footer</Modal.Footer>
|
|
27
|
+
</Modal>,
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByText('Title')).toBeInTheDocument();
|
|
30
|
+
expect(screen.getByText('Body')).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText('Footer')).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { forwardRef, useEffect, type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../utils/cn';
|
|
4
|
+
import { useFocusTrap } from '../../hooks/use-focus-trap';
|
|
5
|
+
|
|
6
|
+
export interface ModalProps {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ModalRoot({ open, onClose, children, className }: ModalProps) {
|
|
14
|
+
const containerRef = useFocusTrap<HTMLDivElement>(open);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!open) return;
|
|
18
|
+
const handler = (e: KeyboardEvent) => {
|
|
19
|
+
if (e.key === 'Escape') onClose();
|
|
20
|
+
};
|
|
21
|
+
document.addEventListener('keydown', handler);
|
|
22
|
+
return () => document.removeEventListener('keydown', handler);
|
|
23
|
+
}, [open, onClose]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (open) {
|
|
27
|
+
document.body.style.overflow = 'hidden';
|
|
28
|
+
}
|
|
29
|
+
return () => { document.body.style.overflow = ''; };
|
|
30
|
+
}, [open]);
|
|
31
|
+
|
|
32
|
+
if (!open) return null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
36
|
+
<div
|
|
37
|
+
className="absolute inset-0 bg-text-primary/30 animate-fade-in"
|
|
38
|
+
onClick={onClose}
|
|
39
|
+
/>
|
|
40
|
+
<div
|
|
41
|
+
ref={containerRef}
|
|
42
|
+
role="dialog"
|
|
43
|
+
aria-modal="true"
|
|
44
|
+
className={cn(
|
|
45
|
+
'relative w-full max-w-lg rounded-xl bg-white shadow-3 overflow-hidden',
|
|
46
|
+
'animate-scale-in',
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ModalHeader = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'> & { onClose?: () => void }>(
|
|
57
|
+
({ className, children, onClose, ...props }, ref) => (
|
|
58
|
+
<div ref={ref} className={cn('flex items-center justify-between px-6 py-4 border-b border-border', className)} {...props}>
|
|
59
|
+
<h2 className="text-text-primary font-semibold" style={{ fontSize: 'var(--text-h3)', lineHeight: 'var(--leading-h3)' }}>{children}</h2>
|
|
60
|
+
{onClose && (
|
|
61
|
+
<button type="button" onClick={onClose} className="p-1.5 rounded-md text-text-secondary hover:bg-surface transition-colors" aria-label="Close" style={{ transitionDuration: 'var(--duration-micro)' }}>
|
|
62
|
+
<X size={18} />
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
ModalHeader.displayName = 'Modal.Header';
|
|
69
|
+
|
|
70
|
+
const ModalBody = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(
|
|
71
|
+
({ className, ...props }, ref) => (
|
|
72
|
+
<div ref={ref} className={cn('px-6 py-4', className)} {...props} />
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
ModalBody.displayName = 'Modal.Body';
|
|
76
|
+
|
|
77
|
+
const ModalFooter = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(
|
|
78
|
+
({ className, ...props }, ref) => (
|
|
79
|
+
<div ref={ref} className={cn('flex justify-end gap-3 px-6 py-4 border-t border-border', className)} {...props} />
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
ModalFooter.displayName = 'Modal.Footer';
|
|
83
|
+
|
|
84
|
+
export const Modal = Object.assign(ModalRoot, {
|
|
85
|
+
Header: ModalHeader,
|
|
86
|
+
Body: ModalBody,
|
|
87
|
+
Footer: ModalFooter,
|
|
88
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { SkeletonLoader } from './SkeletonLoader';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof SkeletonLoader> = { title: 'Feedback/SkeletonLoader', component: SkeletonLoader, tags: ['autodocs'] };
|
|
5
|
+
export default meta;
|
|
6
|
+
type Story = StoryObj<typeof SkeletonLoader>;
|
|
7
|
+
|
|
8
|
+
export const TextLines: Story = { args: { shape: 'text', lines: 3 } };
|
|
9
|
+
export const AvatarShape: Story = { args: { shape: 'avatar', width: 56, height: 56 } };
|
|
10
|
+
export const ImageShape: Story = { args: { shape: 'image', height: 160 } };
|
|
11
|
+
|
|
12
|
+
export const CardSkeleton: Story = {
|
|
13
|
+
render: () => (
|
|
14
|
+
<div className="max-w-sm rounded-lg bg-surface p-6 shadow-1 flex flex-col gap-4">
|
|
15
|
+
<div className="flex items-center gap-3">
|
|
16
|
+
<SkeletonLoader shape="avatar" />
|
|
17
|
+
<SkeletonLoader shape="text" lines={2} />
|
|
18
|
+
</div>
|
|
19
|
+
<SkeletonLoader shape="image" height={120} />
|
|
20
|
+
<SkeletonLoader shape="text" lines={3} />
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { SkeletonLoader } from './SkeletonLoader';
|
|
4
|
+
|
|
5
|
+
describe('SkeletonLoader', () => {
|
|
6
|
+
it('renders text lines', () => {
|
|
7
|
+
const { container } = render(<SkeletonLoader shape="text" lines={3} />);
|
|
8
|
+
expect(container.querySelectorAll('[aria-hidden] > div')).toHaveLength(3);
|
|
9
|
+
});
|
|
10
|
+
it('renders avatar shape', () => {
|
|
11
|
+
const { container } = render(<SkeletonLoader shape="avatar" />);
|
|
12
|
+
expect(container.firstChild).toHaveAttribute('aria-hidden', 'true');
|
|
13
|
+
});
|
|
14
|
+
});
|