@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,61 @@
1
+ import { cn } from '../../utils/cn';
2
+
3
+ export type SkeletonShape = 'text' | 'avatar' | 'image' | 'custom';
4
+
5
+ export interface SkeletonLoaderProps {
6
+ shape?: SkeletonShape;
7
+ width?: string | number;
8
+ height?: string | number;
9
+ lines?: number;
10
+ className?: string;
11
+ }
12
+
13
+ export function SkeletonLoader({ shape = 'text', width, height, lines = 3, className }: SkeletonLoaderProps) {
14
+ const baseClass = 'bg-surface animate-skeleton-pulse rounded';
15
+
16
+ if (shape === 'avatar') {
17
+ return (
18
+ <div
19
+ className={cn(baseClass, 'rounded-full', className)}
20
+ style={{ width: width ?? 40, height: height ?? 40 }}
21
+ aria-hidden
22
+ />
23
+ );
24
+ }
25
+
26
+ if (shape === 'image') {
27
+ return (
28
+ <div
29
+ className={cn(baseClass, 'rounded-lg', className)}
30
+ style={{ width: width ?? '100%', height: height ?? 200 }}
31
+ aria-hidden
32
+ />
33
+ );
34
+ }
35
+
36
+ if (shape === 'custom') {
37
+ return (
38
+ <div
39
+ className={cn(baseClass, className)}
40
+ style={{ width, height }}
41
+ aria-hidden
42
+ />
43
+ );
44
+ }
45
+
46
+ // Text lines
47
+ return (
48
+ <div className={cn('flex flex-col gap-2', className)} aria-hidden>
49
+ {Array.from({ length: lines }).map((_, i) => (
50
+ <div
51
+ key={i}
52
+ className={baseClass}
53
+ style={{
54
+ height: height ?? 16,
55
+ width: i === lines - 1 ? '60%' : (width ?? '100%'),
56
+ }}
57
+ />
58
+ ))}
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,2 @@
1
+ export { SkeletonLoader } from './SkeletonLoader';
2
+ export type { SkeletonLoaderProps, SkeletonShape } from './SkeletonLoader';
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ToastProvider, useToast } from './Toast';
3
+ import { Button } from '../../primitives/button';
4
+
5
+ const meta: Meta = { title: 'Feedback/Toast', tags: ['autodocs'] };
6
+ export default meta;
7
+ type Story = StoryObj;
8
+
9
+ function ToastDemo() {
10
+ const { toast } = useToast();
11
+ return (
12
+ <div className="flex flex-wrap gap-3">
13
+ <Button variant="secondary" onClick={() => toast({ title: 'Task saved', message: 'Your changes have been saved.', variant: 'success' })}>Success</Button>
14
+ <Button variant="destructive" onClick={() => toast({ title: 'Error', message: 'Something went wrong.', variant: 'error' })}>Error</Button>
15
+ <Button variant="outline" onClick={() => toast({ title: 'Warning', message: 'Check your input.', variant: 'warning' })}>Warning</Button>
16
+ <Button variant="ghost" onClick={() => toast({ title: 'Info', message: 'New update available.', variant: 'info' })}>Info</Button>
17
+ </div>
18
+ );
19
+ }
20
+
21
+ export const Default: Story = {
22
+ render: () => (
23
+ <ToastProvider>
24
+ <ToastDemo />
25
+ </ToastProvider>
26
+ ),
27
+ };
@@ -0,0 +1,32 @@
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 { ToastProvider, useToast } from './Toast';
5
+
6
+ function ToastTrigger() {
7
+ const { toast } = useToast();
8
+ return <button onClick={() => toast({ title: 'Saved', variant: 'success' })}>Show</button>;
9
+ }
10
+
11
+ describe('Toast', () => {
12
+ it('shows toast on trigger', async () => {
13
+ render(
14
+ <ToastProvider>
15
+ <ToastTrigger />
16
+ </ToastProvider>,
17
+ );
18
+ await userEvent.click(screen.getByText('Show'));
19
+ expect(screen.getByRole('alert')).toHaveTextContent('Saved');
20
+ });
21
+
22
+ it('dismisses on close click', async () => {
23
+ render(
24
+ <ToastProvider>
25
+ <ToastTrigger />
26
+ </ToastProvider>,
27
+ );
28
+ await userEvent.click(screen.getByText('Show'));
29
+ await userEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
30
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
31
+ });
32
+ });
@@ -0,0 +1,106 @@
1
+ import { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
2
+ import { CheckCircle2, AlertCircle, AlertTriangle, Info, X } from 'lucide-react';
3
+ import { cn } from '../../utils/cn';
4
+
5
+ export type ToastVariant = 'success' | 'error' | 'warning' | 'info';
6
+
7
+ export interface ToastData {
8
+ id: string;
9
+ title: string;
10
+ message?: string;
11
+ variant: ToastVariant;
12
+ duration?: number;
13
+ }
14
+
15
+ interface ToastContextValue {
16
+ toast: (data: Omit<ToastData, 'id'>) => void;
17
+ }
18
+
19
+ const ToastContext = createContext<ToastContextValue>({ toast: () => {} });
20
+ export const useToast = () => useContext(ToastContext);
21
+
22
+ const icons: Record<ToastVariant, React.ReactNode> = {
23
+ success: <CheckCircle2 size={18} />,
24
+ error: <AlertCircle size={18} />,
25
+ warning: <AlertTriangle size={18} />,
26
+ info: <Info size={18} />,
27
+ };
28
+
29
+ const variantStyles: Record<ToastVariant, string> = {
30
+ success: 'border-success text-success',
31
+ error: 'border-error text-error',
32
+ warning: 'border-warning text-warning',
33
+ info: 'border-info text-info',
34
+ };
35
+
36
+ let toastId = 0;
37
+
38
+ export function ToastProvider({ children }: { children: React.ReactNode }) {
39
+ const [toasts, setToasts] = useState<ToastData[]>([]);
40
+ const timers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
41
+
42
+ const removeToast = useCallback((id: string) => {
43
+ setToasts((prev) => prev.filter((t) => t.id !== id));
44
+ const timer = timers.current.get(id);
45
+ if (timer) {
46
+ clearTimeout(timer);
47
+ timers.current.delete(id);
48
+ }
49
+ }, []);
50
+
51
+ const toast = useCallback(
52
+ (data: Omit<ToastData, 'id'>) => {
53
+ const id = `toast-${++toastId}`;
54
+ setToasts((prev) => [...prev, { ...data, id }]);
55
+ const duration = data.duration ?? 5000;
56
+ if (duration > 0) {
57
+ const timer = setTimeout(() => removeToast(id), duration);
58
+ timers.current.set(id, timer);
59
+ }
60
+ },
61
+ [removeToast],
62
+ );
63
+
64
+ // Cleanup timers on unmount
65
+ useEffect(() => {
66
+ return () => {
67
+ timers.current.forEach((timer) => clearTimeout(timer));
68
+ };
69
+ }, []);
70
+
71
+ return (
72
+ <ToastContext.Provider value={{ toast }}>
73
+ {children}
74
+ <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm" aria-live="polite">
75
+ {toasts.map((t) => (
76
+ <div
77
+ key={t.id}
78
+ className={cn(
79
+ 'flex items-start gap-3 rounded-lg bg-white border-l-4 p-4 shadow-2',
80
+ 'animate-slide-in-right',
81
+ variantStyles[t.variant],
82
+ )}
83
+ role="alert"
84
+ >
85
+ <span className="shrink-0 mt-0.5">{icons[t.variant]}</span>
86
+ <div className="flex-1 min-w-0">
87
+ <p className="text-text-primary font-medium" style={{ fontSize: 'var(--text-body-sm)' }}>{t.title}</p>
88
+ {t.message && (
89
+ <p className="text-text-secondary mt-0.5" style={{ fontSize: 'var(--text-body-sm)' }}>{t.message}</p>
90
+ )}
91
+ </div>
92
+ <button
93
+ type="button"
94
+ onClick={() => removeToast(t.id)}
95
+ className="shrink-0 p-1 rounded text-text-secondary hover:bg-surface transition-colors"
96
+ aria-label="Dismiss"
97
+ style={{ transitionDuration: 'var(--duration-micro)' }}
98
+ >
99
+ <X size={14} />
100
+ </button>
101
+ </div>
102
+ ))}
103
+ </div>
104
+ </ToastContext.Provider>
105
+ );
106
+ }
@@ -0,0 +1,2 @@
1
+ export { ToastProvider, useToast } from './Toast';
2
+ export type { ToastData, ToastVariant } from './Toast';
@@ -0,0 +1,3 @@
1
+ export { useReducedMotion } from './use-reduced-motion';
2
+ export { useControllableState } from './use-controllable-state';
3
+ export { useFocusTrap } from './use-focus-trap';
@@ -0,0 +1,34 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+
3
+ interface UseControllableStateParams<T> {
4
+ value?: T;
5
+ defaultValue: T;
6
+ onChange?: (value: T) => void;
7
+ }
8
+
9
+ export function useControllableState<T>({
10
+ value: controlledValue,
11
+ defaultValue,
12
+ onChange,
13
+ }: UseControllableStateParams<T>): [T, (value: T) => void] {
14
+ const [internalValue, setInternalValue] = useState(defaultValue);
15
+ const isControlled = controlledValue !== undefined;
16
+ const value = isControlled ? controlledValue : internalValue;
17
+
18
+ const onChangeRef = useRef(onChange);
19
+ useEffect(() => {
20
+ onChangeRef.current = onChange;
21
+ }, [onChange]);
22
+
23
+ const setValue = useCallback(
24
+ (next: T) => {
25
+ if (!isControlled) {
26
+ setInternalValue(next);
27
+ }
28
+ onChangeRef.current?.(next);
29
+ },
30
+ [isControlled],
31
+ );
32
+
33
+ return [value, setValue];
34
+ }
@@ -0,0 +1,56 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ const FOCUSABLE_SELECTOR = [
4
+ 'a[href]',
5
+ 'button:not([disabled])',
6
+ 'input:not([disabled])',
7
+ 'select:not([disabled])',
8
+ 'textarea:not([disabled])',
9
+ '[tabindex]:not([tabindex="-1"])',
10
+ ].join(', ');
11
+
12
+ export function useFocusTrap<T extends HTMLElement>(active: boolean = true) {
13
+ const containerRef = useRef<T>(null);
14
+
15
+ useEffect(() => {
16
+ if (!active) return;
17
+ const container = containerRef.current;
18
+ if (!container) return;
19
+
20
+ const focusableElements = () =>
21
+ Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR));
22
+
23
+ const handleKeyDown = (event: KeyboardEvent) => {
24
+ if (event.key !== 'Tab') return;
25
+
26
+ const elements = focusableElements();
27
+ if (elements.length === 0) return;
28
+
29
+ const first = elements[0];
30
+ const last = elements[elements.length - 1];
31
+
32
+ if (event.shiftKey) {
33
+ if (document.activeElement === first) {
34
+ event.preventDefault();
35
+ last.focus();
36
+ }
37
+ } else {
38
+ if (document.activeElement === last) {
39
+ event.preventDefault();
40
+ first.focus();
41
+ }
42
+ }
43
+ };
44
+
45
+ // Focus the first focusable element on mount
46
+ const elements = focusableElements();
47
+ if (elements.length > 0) {
48
+ elements[0].focus();
49
+ }
50
+
51
+ container.addEventListener('keydown', handleKeyDown);
52
+ return () => container.removeEventListener('keydown', handleKeyDown);
53
+ }, [active]);
54
+
55
+ return containerRef;
56
+ }
@@ -0,0 +1,17 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ export function useReducedMotion(): boolean {
4
+ const [reducedMotion, setReducedMotion] = useState(() => {
5
+ if (typeof window === 'undefined') return false;
6
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
7
+ });
8
+
9
+ useEffect(() => {
10
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
11
+ const handler = (event: MediaQueryListEvent) => setReducedMotion(event.matches);
12
+ mq.addEventListener('change', handler);
13
+ return () => mq.removeEventListener('change', handler);
14
+ }, []);
15
+
16
+ return reducedMotion;
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,148 @@
1
+ // Primitives
2
+ export { Button } from './primitives/button';
3
+ export type { ButtonProps, ButtonVariant, ButtonSize } from './primitives/button';
4
+
5
+ export { InputGroup } from './primitives/input';
6
+ export type { InputGroupProps } from './primitives/input';
7
+
8
+ export { TextareaGroup } from './primitives/textarea';
9
+ export type { TextareaGroupProps } from './primitives/textarea';
10
+
11
+ export { SelectGroup } from './primitives/select';
12
+ export type { SelectGroupProps } from './primitives/select';
13
+
14
+ export { Checkbox } from './primitives/checkbox';
15
+ export type { CheckboxProps } from './primitives/checkbox';
16
+
17
+ export { Radio } from './primitives/radio';
18
+ export type { RadioProps } from './primitives/radio';
19
+
20
+ export { Toggle } from './primitives/toggle';
21
+ export type { ToggleProps } from './primitives/toggle';
22
+
23
+ export { Badge } from './primitives/badge';
24
+ export type { BadgeProps, BadgeVariant } from './primitives/badge';
25
+
26
+ export { Avatar, AvatarGroup } from './primitives/avatar';
27
+ export type { AvatarProps, AvatarGroupProps, AvatarSize } from './primitives/avatar';
28
+
29
+ export { Link } from './primitives/link';
30
+ export type { LinkProps, LinkVariant } from './primitives/link';
31
+
32
+ export { Tooltip } from './primitives/tooltip';
33
+ export type { TooltipProps, TooltipVariant } from './primitives/tooltip';
34
+
35
+ export { Slider } from './primitives/slider';
36
+ export type { SliderProps } from './primitives/slider';
37
+
38
+ export { DateInput } from './primitives/date-input';
39
+ export type { DateInputProps } from './primitives/date-input';
40
+
41
+ export { Combobox } from './primitives/combobox';
42
+ export type { ComboboxProps, ComboboxOption } from './primitives/combobox';
43
+
44
+ export { FileUploadDropzone } from './primitives/file-upload';
45
+ export type { FileUploadDropzoneProps } from './primitives/file-upload';
46
+
47
+ // Layout
48
+ export { Card } from './layout/card';
49
+ export type { CardProps, CardHeaderProps, CardContentProps, CardActionsProps } from './layout/card';
50
+
51
+ export { Sidebar, SidebarContext, useSidebar } from './layout/sidebar';
52
+ export type { SidebarProps } from './layout/sidebar';
53
+
54
+ export { TopBar } from './layout/top-bar';
55
+ export type { TopBarProps } from './layout/top-bar';
56
+
57
+ export { CommandPalette } from './layout/command-palette';
58
+ export type { CommandPaletteProps, CommandItem } from './layout/command-palette';
59
+
60
+ // Navigation
61
+ export { SidebarNavItem } from './navigation/sidebar-nav-item';
62
+ export type { SidebarNavItemProps } from './navigation/sidebar-nav-item';
63
+
64
+ export { Tabs } from './navigation/tabs';
65
+ export type { TabsProps, TabsListProps, TabsTriggerProps, TabsPanelProps, TabsVariant } from './navigation/tabs';
66
+
67
+ export { Breadcrumbs } from './navigation/breadcrumbs';
68
+ export type { BreadcrumbsProps, BreadcrumbItem } from './navigation/breadcrumbs';
69
+
70
+ // Data Display
71
+ export { StatCard } from './data-display/stat-card';
72
+ export type { StatCardProps } from './data-display/stat-card';
73
+
74
+ export { ProgressBar } from './data-display/progress-bar';
75
+ export type { ProgressBarProps } from './data-display/progress-bar';
76
+
77
+ export { CircularProgress } from './data-display/circular-progress';
78
+ export type { CircularProgressProps } from './data-display/circular-progress';
79
+
80
+ export { Table } from './data-display/table';
81
+
82
+ export { Timeline } from './data-display/timeline';
83
+ export type { TimelineProps, TimelineItem, TimelineColor } from './data-display/timeline';
84
+
85
+ export { ActivityFeed } from './data-display/activity-feed';
86
+ export type { ActivityFeedProps, ActivityItem } from './data-display/activity-feed';
87
+
88
+ export { EmptyState } from './data-display/empty-state';
89
+ export type { EmptyStateProps } from './data-display/empty-state';
90
+
91
+ // Feedback
92
+ export { ToastProvider, useToast } from './feedback/toast';
93
+ export type { ToastData, ToastVariant } from './feedback/toast';
94
+
95
+ export { Modal } from './feedback/modal';
96
+ export type { ModalProps } from './feedback/modal';
97
+
98
+ export { AlertBanner } from './feedback/alert-banner';
99
+ export type { AlertBannerProps, AlertBannerVariant } from './feedback/alert-banner';
100
+
101
+ export { SkeletonLoader } from './feedback/skeleton-loader';
102
+ export type { SkeletonLoaderProps, SkeletonShape } from './feedback/skeleton-loader';
103
+
104
+ // Patterns
105
+ export { Accordion } from './patterns/accordion';
106
+ export type { AccordionProps, AccordionItemProps, AccordionTriggerProps, AccordionContentProps } from './patterns/accordion';
107
+
108
+ export { ActionMenu } from './patterns/action-menu';
109
+ export type { ActionMenuProps, ActionMenuItem } from './patterns/action-menu';
110
+
111
+ export { Pagination } from './patterns/pagination';
112
+ export type { PaginationProps } from './patterns/pagination';
113
+
114
+ export { NotificationDot } from './patterns/notification-dot';
115
+ export type { NotificationDotProps } from './patterns/notification-dot';
116
+
117
+ export { Carousel } from './patterns/carousel';
118
+ export type { CarouselProps } from './patterns/carousel';
119
+
120
+ export { ImagePlaceholder } from './patterns/image-placeholder';
121
+ export type { ImagePlaceholderProps } from './patterns/image-placeholder';
122
+
123
+ // Productivity
124
+ export { TaskCard } from './productivity/task-card';
125
+ export type { TaskCardProps } from './productivity/task-card';
126
+
127
+ export { KanbanColumn, SortableTaskCard } from './productivity/kanban-column';
128
+ export type { KanbanColumnProps, SortableTaskCardProps } from './productivity/kanban-column';
129
+
130
+ export { KanbanBoard } from './productivity/kanban-board';
131
+ export type { KanbanBoardProps } from './productivity/kanban-board';
132
+
133
+ export { PrioritySelector } from './productivity/priority-selector';
134
+ export type { PrioritySelectorProps, Priority } from './productivity/priority-selector';
135
+
136
+ export { CommentThread } from './productivity/comment-thread';
137
+ export type { CommentThreadProps, Comment } from './productivity/comment-thread';
138
+
139
+ export { RichTextToolbar } from './productivity/rich-text-toolbar';
140
+ export type { RichTextToolbarProps } from './productivity/rich-text-toolbar';
141
+
142
+ // Hooks
143
+ export { useReducedMotion } from './hooks/use-reduced-motion';
144
+ export { useControllableState } from './hooks/use-controllable-state';
145
+ export { useFocusTrap } from './hooks/use-focus-trap';
146
+
147
+ // Utils
148
+ export { cn } from './utils/cn';
@@ -0,0 +1,45 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Card } from './Card';
3
+ import { Button } from '../../primitives/button';
4
+
5
+ const meta: Meta<typeof Card> = {
6
+ title: 'Layout/Card',
7
+ component: Card,
8
+ tags: ['autodocs'],
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof Card>;
13
+
14
+ export const Default: Story = {
15
+ render: () => (
16
+ <Card className="max-w-md">
17
+ <Card.Header>
18
+ <h3 className="text-text-primary font-semibold" style={{ fontSize: 'var(--text-h3)' }}>
19
+ Project Settings
20
+ </h3>
21
+ </Card.Header>
22
+ <Card.Content>
23
+ <p className="text-text-secondary" style={{ fontSize: 'var(--text-body)' }}>
24
+ Configure your project settings and preferences. Changes will be saved automatically.
25
+ </p>
26
+ </Card.Content>
27
+ <Card.Actions>
28
+ <Button variant="ghost">Cancel</Button>
29
+ <Button>Save Changes</Button>
30
+ </Card.Actions>
31
+ </Card>
32
+ ),
33
+ };
34
+
35
+ export const Simple: Story = {
36
+ render: () => (
37
+ <Card className="max-w-sm">
38
+ <Card.Content>
39
+ <p className="text-text-primary" style={{ fontSize: 'var(--text-body)' }}>
40
+ A simple card with just content — no header or footer.
41
+ </p>
42
+ </Card.Content>
43
+ </Card>
44
+ ),
45
+ };
@@ -0,0 +1,23 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { Card } from './Card';
4
+
5
+ describe('Card', () => {
6
+ it('renders compound structure', () => {
7
+ render(
8
+ <Card>
9
+ <Card.Header>Header</Card.Header>
10
+ <Card.Content>Content</Card.Content>
11
+ <Card.Actions>Actions</Card.Actions>
12
+ </Card>,
13
+ );
14
+ expect(screen.getByText('Header')).toBeInTheDocument();
15
+ expect(screen.getByText('Content')).toBeInTheDocument();
16
+ expect(screen.getByText('Actions')).toBeInTheDocument();
17
+ });
18
+
19
+ it('applies custom className', () => {
20
+ const { container } = render(<Card className="max-w-md">Content</Card>);
21
+ expect(container.firstChild).toHaveClass('max-w-md');
22
+ });
23
+ });
@@ -0,0 +1,42 @@
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '../../utils/cn';
3
+ import type { CardProps, CardHeaderProps, CardContentProps, CardActionsProps } from './Card.types';
4
+
5
+ const CardRoot = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => (
6
+ <div
7
+ ref={ref}
8
+ className={cn(
9
+ 'rounded-lg bg-surface shadow-1 overflow-hidden',
10
+ 'transition-all hover:shadow-2 hover:-translate-y-0.5',
11
+ className,
12
+ )}
13
+ style={{ transitionDuration: 'var(--duration-fast)', transitionTimingFunction: 'var(--ease-out)' }}
14
+ {...props}
15
+ />
16
+ ));
17
+ CardRoot.displayName = 'Card';
18
+
19
+ const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(({ className, ...props }, ref) => (
20
+ <div ref={ref} className={cn('px-6 py-4 border-b border-border', className)} {...props} />
21
+ ));
22
+ CardHeader.displayName = 'Card.Header';
23
+
24
+ const CardContent = forwardRef<HTMLDivElement, CardContentProps>(({ className, ...props }, ref) => (
25
+ <div ref={ref} className={cn('px-6 py-4', className)} {...props} />
26
+ ));
27
+ CardContent.displayName = 'Card.Content';
28
+
29
+ const CardActions = forwardRef<HTMLDivElement, CardActionsProps>(({ className, ...props }, ref) => (
30
+ <div
31
+ ref={ref}
32
+ className={cn('px-6 py-4 border-t border-border flex justify-end gap-3', className)}
33
+ {...props}
34
+ />
35
+ ));
36
+ CardActions.displayName = 'Card.Actions';
37
+
38
+ export const Card = Object.assign(CardRoot, {
39
+ Header: CardHeader,
40
+ Content: CardContent,
41
+ Actions: CardActions,
42
+ });
@@ -0,0 +1,6 @@
1
+ import type { ComponentPropsWithoutRef } from 'react';
2
+
3
+ export interface CardProps extends ComponentPropsWithoutRef<'div'> {}
4
+ export interface CardHeaderProps extends ComponentPropsWithoutRef<'div'> {}
5
+ export interface CardContentProps extends ComponentPropsWithoutRef<'div'> {}
6
+ export interface CardActionsProps extends ComponentPropsWithoutRef<'div'> {}
@@ -0,0 +1,2 @@
1
+ export { Card } from './Card';
2
+ export type { CardProps, CardHeaderProps, CardContentProps, CardActionsProps } from './Card.types';
@@ -0,0 +1,34 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { Home, Settings, Users, FileText, Plus } from 'lucide-react';
4
+ import { CommandPalette } from './CommandPalette';
5
+ import { Button } from '../../primitives/button';
6
+
7
+ const meta: Meta<typeof CommandPalette> = {
8
+ title: 'Layout/CommandPalette',
9
+ component: CommandPalette,
10
+ tags: ['autodocs'],
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof CommandPalette>;
15
+
16
+ const items = [
17
+ { id: '1', label: 'Go to Dashboard', icon: <Home size={16} />, section: 'Navigation', onSelect: () => {} },
18
+ { id: '2', label: 'Go to Settings', icon: <Settings size={16} />, section: 'Navigation', onSelect: () => {} },
19
+ { id: '3', label: 'Go to Team', icon: <Users size={16} />, section: 'Navigation', onSelect: () => {} },
20
+ { id: '4', label: 'Create new task', icon: <Plus size={16} />, section: 'Actions', onSelect: () => {} },
21
+ { id: '5', label: 'Create new document', icon: <FileText size={16} />, section: 'Actions', onSelect: () => {} },
22
+ ];
23
+
24
+ export const Default: Story = {
25
+ render: () => {
26
+ const [open, setOpen] = useState(false);
27
+ return (
28
+ <>
29
+ <Button onClick={() => setOpen(true)}>Open Command Palette (⌘K)</Button>
30
+ <CommandPalette open={open} onOpenChange={setOpen} items={items} />
31
+ </>
32
+ );
33
+ },
34
+ };