@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.
Files changed (230) hide show
  1. package/dist/index.cjs +7729 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.cts +677 -0
  4. package/dist/index.d.ts +677 -0
  5. package/dist/index.js +7654 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/theme.css +2 -0
  8. package/dist/tokens.cjs +78 -0
  9. package/dist/tokens.cjs.map +1 -0
  10. package/dist/tokens.d.cts +67 -0
  11. package/dist/tokens.d.ts +67 -0
  12. package/dist/tokens.js +70 -0
  13. package/dist/tokens.js.map +1 -0
  14. package/package.json +103 -0
  15. package/src/data-display/activity-feed/ActivityFeed.stories.tsx +16 -0
  16. package/src/data-display/activity-feed/ActivityFeed.test.tsx +11 -0
  17. package/src/data-display/activity-feed/ActivityFeed.tsx +34 -0
  18. package/src/data-display/activity-feed/index.ts +2 -0
  19. package/src/data-display/circular-progress/CircularProgress.stories.tsx +10 -0
  20. package/src/data-display/circular-progress/CircularProgress.test.tsx +11 -0
  21. package/src/data-display/circular-progress/CircularProgress.tsx +35 -0
  22. package/src/data-display/circular-progress/index.ts +2 -0
  23. package/src/data-display/empty-state/EmptyState.stories.tsx +15 -0
  24. package/src/data-display/empty-state/EmptyState.test.tsx +18 -0
  25. package/src/data-display/empty-state/EmptyState.tsx +34 -0
  26. package/src/data-display/empty-state/index.ts +2 -0
  27. package/src/data-display/progress-bar/ProgressBar.stories.tsx +10 -0
  28. package/src/data-display/progress-bar/ProgressBar.test.tsx +15 -0
  29. package/src/data-display/progress-bar/ProgressBar.tsx +38 -0
  30. package/src/data-display/progress-bar/index.ts +2 -0
  31. package/src/data-display/stat-card/StatCard.stories.tsx +10 -0
  32. package/src/data-display/stat-card/StatCard.test.tsx +15 -0
  33. package/src/data-display/stat-card/StatCard.tsx +40 -0
  34. package/src/data-display/stat-card/index.ts +2 -0
  35. package/src/data-display/table/Table.stories.tsx +44 -0
  36. package/src/data-display/table/Table.test.tsx +16 -0
  37. package/src/data-display/table/Table.tsx +71 -0
  38. package/src/data-display/table/index.ts +1 -0
  39. package/src/data-display/timeline/Timeline.stories.tsx +16 -0
  40. package/src/data-display/timeline/Timeline.test.tsx +11 -0
  41. package/src/data-display/timeline/Timeline.tsx +44 -0
  42. package/src/data-display/timeline/index.ts +2 -0
  43. package/src/docs/ComponentOverview.mdx +192 -0
  44. package/src/docs/DesignTokens.mdx +235 -0
  45. package/src/docs/GettingStarted.mdx +145 -0
  46. package/src/feedback/alert-banner/AlertBanner.stories.tsx +10 -0
  47. package/src/feedback/alert-banner/AlertBanner.test.tsx +16 -0
  48. package/src/feedback/alert-banner/AlertBanner.tsx +47 -0
  49. package/src/feedback/alert-banner/index.ts +2 -0
  50. package/src/feedback/modal/Modal.stories.tsx +31 -0
  51. package/src/feedback/modal/Modal.test.tsx +33 -0
  52. package/src/feedback/modal/Modal.tsx +88 -0
  53. package/src/feedback/modal/index.ts +2 -0
  54. package/src/feedback/skeleton-loader/SkeletonLoader.stories.tsx +23 -0
  55. package/src/feedback/skeleton-loader/SkeletonLoader.test.tsx +14 -0
  56. package/src/feedback/skeleton-loader/SkeletonLoader.tsx +61 -0
  57. package/src/feedback/skeleton-loader/index.ts +2 -0
  58. package/src/feedback/toast/Toast.stories.tsx +27 -0
  59. package/src/feedback/toast/Toast.test.tsx +32 -0
  60. package/src/feedback/toast/Toast.tsx +106 -0
  61. package/src/feedback/toast/index.ts +2 -0
  62. package/src/hooks/index.ts +3 -0
  63. package/src/hooks/use-controllable-state.ts +34 -0
  64. package/src/hooks/use-focus-trap.ts +56 -0
  65. package/src/hooks/use-reduced-motion.ts +17 -0
  66. package/src/index.ts +148 -0
  67. package/src/layout/card/Card.stories.tsx +45 -0
  68. package/src/layout/card/Card.test.tsx +23 -0
  69. package/src/layout/card/Card.tsx +42 -0
  70. package/src/layout/card/Card.types.ts +6 -0
  71. package/src/layout/card/index.ts +2 -0
  72. package/src/layout/command-palette/CommandPalette.stories.tsx +34 -0
  73. package/src/layout/command-palette/CommandPalette.test.tsx +43 -0
  74. package/src/layout/command-palette/CommandPalette.tsx +188 -0
  75. package/src/layout/command-palette/CommandPalette.types.ts +18 -0
  76. package/src/layout/command-palette/index.ts +2 -0
  77. package/src/layout/sidebar/Sidebar.stories.tsx +60 -0
  78. package/src/layout/sidebar/Sidebar.test.tsx +27 -0
  79. package/src/layout/sidebar/Sidebar.tsx +57 -0
  80. package/src/layout/sidebar/Sidebar.types.ts +14 -0
  81. package/src/layout/sidebar/index.ts +2 -0
  82. package/src/layout/top-bar/TopBar.stories.tsx +51 -0
  83. package/src/layout/top-bar/TopBar.test.tsx +18 -0
  84. package/src/layout/top-bar/TopBar.tsx +19 -0
  85. package/src/layout/top-bar/TopBar.types.ts +10 -0
  86. package/src/layout/top-bar/index.ts +2 -0
  87. package/src/navigation/breadcrumbs/Breadcrumbs.stories.tsx +30 -0
  88. package/src/navigation/breadcrumbs/Breadcrumbs.test.tsx +43 -0
  89. package/src/navigation/breadcrumbs/Breadcrumbs.tsx +45 -0
  90. package/src/navigation/breadcrumbs/Breadcrumbs.types.ts +12 -0
  91. package/src/navigation/breadcrumbs/index.ts +2 -0
  92. package/src/navigation/sidebar-nav-item/SidebarNavItem.stories.tsx +41 -0
  93. package/src/navigation/sidebar-nav-item/SidebarNavItem.test.tsx +46 -0
  94. package/src/navigation/sidebar-nav-item/SidebarNavItem.tsx +38 -0
  95. package/src/navigation/sidebar-nav-item/SidebarNavItem.types.ts +12 -0
  96. package/src/navigation/sidebar-nav-item/index.ts +2 -0
  97. package/src/navigation/tabs/Tabs.stories.tsx +47 -0
  98. package/src/navigation/tabs/Tabs.test.tsx +67 -0
  99. package/src/navigation/tabs/Tabs.tsx +111 -0
  100. package/src/navigation/tabs/Tabs.types.ts +36 -0
  101. package/src/navigation/tabs/index.ts +2 -0
  102. package/src/patterns/accordion/Accordion.stories.tsx +25 -0
  103. package/src/patterns/accordion/Accordion.test.tsx +44 -0
  104. package/src/patterns/accordion/Accordion.tsx +92 -0
  105. package/src/patterns/accordion/index.ts +2 -0
  106. package/src/patterns/action-menu/ActionMenu.stories.tsx +18 -0
  107. package/src/patterns/action-menu/ActionMenu.test.tsx +18 -0
  108. package/src/patterns/action-menu/ActionMenu.tsx +41 -0
  109. package/src/patterns/action-menu/index.ts +2 -0
  110. package/src/patterns/carousel/Carousel.stories.tsx +16 -0
  111. package/src/patterns/carousel/Carousel.test.tsx +16 -0
  112. package/src/patterns/carousel/Carousel.tsx +69 -0
  113. package/src/patterns/carousel/index.ts +2 -0
  114. package/src/patterns/image-placeholder/ImagePlaceholder.stories.tsx +9 -0
  115. package/src/patterns/image-placeholder/ImagePlaceholder.test.tsx +10 -0
  116. package/src/patterns/image-placeholder/ImagePlaceholder.tsx +21 -0
  117. package/src/patterns/image-placeholder/index.ts +2 -0
  118. package/src/patterns/notification-dot/NotificationDot.stories.tsx +17 -0
  119. package/src/patterns/notification-dot/NotificationDot.test.tsx +14 -0
  120. package/src/patterns/notification-dot/NotificationDot.tsx +18 -0
  121. package/src/patterns/notification-dot/index.ts +2 -0
  122. package/src/patterns/pagination/Pagination.stories.tsx +14 -0
  123. package/src/patterns/pagination/Pagination.test.tsx +22 -0
  124. package/src/patterns/pagination/Pagination.tsx +67 -0
  125. package/src/patterns/pagination/index.ts +2 -0
  126. package/src/primitives/avatar/Avatar.stories.tsx +46 -0
  127. package/src/primitives/avatar/Avatar.test.tsx +35 -0
  128. package/src/primitives/avatar/Avatar.tsx +49 -0
  129. package/src/primitives/avatar/Avatar.types.ts +21 -0
  130. package/src/primitives/avatar/AvatarGroup.tsx +27 -0
  131. package/src/primitives/avatar/index.ts +3 -0
  132. package/src/primitives/badge/Badge.stories.tsx +28 -0
  133. package/src/primitives/badge/Badge.test.tsx +23 -0
  134. package/src/primitives/badge/Badge.tsx +44 -0
  135. package/src/primitives/badge/Badge.types.ts +14 -0
  136. package/src/primitives/badge/index.ts +2 -0
  137. package/src/primitives/button/Button.stories.tsx +81 -0
  138. package/src/primitives/button/Button.test.tsx +64 -0
  139. package/src/primitives/button/Button.tsx +85 -0
  140. package/src/primitives/button/Button.types.ts +17 -0
  141. package/src/primitives/button/index.ts +2 -0
  142. package/src/primitives/checkbox/Checkbox.stories.tsx +27 -0
  143. package/src/primitives/checkbox/Checkbox.test.tsx +30 -0
  144. package/src/primitives/checkbox/Checkbox.tsx +79 -0
  145. package/src/primitives/checkbox/Checkbox.types.ts +12 -0
  146. package/src/primitives/checkbox/index.ts +2 -0
  147. package/src/primitives/combobox/Combobox.stories.tsx +44 -0
  148. package/src/primitives/combobox/Combobox.test.tsx +44 -0
  149. package/src/primitives/combobox/Combobox.tsx +201 -0
  150. package/src/primitives/combobox/Combobox.types.ts +25 -0
  151. package/src/primitives/combobox/index.ts +2 -0
  152. package/src/primitives/date-input/DateInput.stories.tsx +23 -0
  153. package/src/primitives/date-input/DateInput.test.tsx +22 -0
  154. package/src/primitives/date-input/DateInput.tsx +66 -0
  155. package/src/primitives/date-input/DateInput.types.ts +10 -0
  156. package/src/primitives/date-input/index.ts +2 -0
  157. package/src/primitives/file-upload/FileUploadDropzone.stories.tsx +27 -0
  158. package/src/primitives/file-upload/FileUploadDropzone.test.tsx +26 -0
  159. package/src/primitives/file-upload/FileUploadDropzone.tsx +99 -0
  160. package/src/primitives/file-upload/FileUploadDropzone.types.ts +14 -0
  161. package/src/primitives/file-upload/index.ts +2 -0
  162. package/src/primitives/input/InputGroup.stories.tsx +31 -0
  163. package/src/primitives/input/InputGroup.test.tsx +40 -0
  164. package/src/primitives/input/InputGroup.tsx +65 -0
  165. package/src/primitives/input/InputGroup.types.ts +12 -0
  166. package/src/primitives/input/index.ts +2 -0
  167. package/src/primitives/link/Link.stories.tsx +28 -0
  168. package/src/primitives/link/Link.test.tsx +23 -0
  169. package/src/primitives/link/Link.tsx +28 -0
  170. package/src/primitives/link/Link.types.ts +8 -0
  171. package/src/primitives/link/index.ts +2 -0
  172. package/src/primitives/radio/Radio.stories.tsx +29 -0
  173. package/src/primitives/radio/Radio.test.tsx +32 -0
  174. package/src/primitives/radio/Radio.tsx +59 -0
  175. package/src/primitives/radio/Radio.types.ts +6 -0
  176. package/src/primitives/radio/index.ts +2 -0
  177. package/src/primitives/select/SelectGroup.stories.tsx +33 -0
  178. package/src/primitives/select/SelectGroup.test.tsx +34 -0
  179. package/src/primitives/select/SelectGroup.tsx +72 -0
  180. package/src/primitives/select/SelectGroup.types.ts +12 -0
  181. package/src/primitives/select/index.ts +2 -0
  182. package/src/primitives/slider/Slider.stories.tsx +23 -0
  183. package/src/primitives/slider/Slider.test.tsx +28 -0
  184. package/src/primitives/slider/Slider.tsx +80 -0
  185. package/src/primitives/slider/Slider.types.ts +22 -0
  186. package/src/primitives/slider/index.ts +2 -0
  187. package/src/primitives/textarea/TextareaGroup.stories.tsx +27 -0
  188. package/src/primitives/textarea/TextareaGroup.test.tsx +24 -0
  189. package/src/primitives/textarea/TextareaGroup.tsx +59 -0
  190. package/src/primitives/textarea/TextareaGroup.types.ts +10 -0
  191. package/src/primitives/textarea/index.ts +2 -0
  192. package/src/primitives/toggle/Toggle.stories.tsx +27 -0
  193. package/src/primitives/toggle/Toggle.test.tsx +31 -0
  194. package/src/primitives/toggle/Toggle.tsx +65 -0
  195. package/src/primitives/toggle/Toggle.types.ts +16 -0
  196. package/src/primitives/toggle/index.ts +2 -0
  197. package/src/primitives/tooltip/Tooltip.stories.tsx +45 -0
  198. package/src/primitives/tooltip/Tooltip.test.tsx +28 -0
  199. package/src/primitives/tooltip/Tooltip.tsx +94 -0
  200. package/src/primitives/tooltip/Tooltip.types.ts +16 -0
  201. package/src/primitives/tooltip/index.ts +2 -0
  202. package/src/productivity/comment-thread/CommentThread.stories.tsx +20 -0
  203. package/src/productivity/comment-thread/CommentThread.test.tsx +21 -0
  204. package/src/productivity/comment-thread/CommentThread.tsx +47 -0
  205. package/src/productivity/comment-thread/index.ts +2 -0
  206. package/src/productivity/kanban-board/KanbanBoard.tsx +41 -0
  207. package/src/productivity/kanban-board/index.ts +2 -0
  208. package/src/productivity/kanban-column/KanbanColumn.stories.tsx +131 -0
  209. package/src/productivity/kanban-column/KanbanColumn.test.tsx +18 -0
  210. package/src/productivity/kanban-column/KanbanColumn.tsx +58 -0
  211. package/src/productivity/kanban-column/SortableTaskCard.tsx +46 -0
  212. package/src/productivity/kanban-column/index.ts +4 -0
  213. package/src/productivity/priority-selector/PrioritySelector.stories.tsx +14 -0
  214. package/src/productivity/priority-selector/PrioritySelector.test.tsx +18 -0
  215. package/src/productivity/priority-selector/PrioritySelector.tsx +40 -0
  216. package/src/productivity/priority-selector/index.ts +2 -0
  217. package/src/productivity/rich-text-toolbar/RichTextToolbar.stories.tsx +19 -0
  218. package/src/productivity/rich-text-toolbar/RichTextToolbar.test.tsx +21 -0
  219. package/src/productivity/rich-text-toolbar/RichTextToolbar.tsx +50 -0
  220. package/src/productivity/rich-text-toolbar/index.ts +2 -0
  221. package/src/productivity/task-card/TaskCard.stories.tsx +20 -0
  222. package/src/productivity/task-card/TaskCard.test.tsx +21 -0
  223. package/src/productivity/task-card/TaskCard.tsx +76 -0
  224. package/src/productivity/task-card/index.ts +2 -0
  225. package/src/test-setup.ts +1 -0
  226. package/src/tokens/index.ts +1 -0
  227. package/src/tokens/tokens.ts +71 -0
  228. package/src/tokens/weave-theme.css +168 -0
  229. package/src/utils/cn.ts +6 -0
  230. 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,2 @@
1
+ export { AlertBanner } from './AlertBanner';
2
+ export type { AlertBannerProps, AlertBannerVariant } from './AlertBanner';
@@ -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,2 @@
1
+ export { Modal } from './Modal';
2
+ export type { ModalProps } from './Modal';
@@ -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
+ });