@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,59 @@
1
+ import { forwardRef, useId } from 'react';
2
+ import { AlertCircle } from 'lucide-react';
3
+ import { cn } from '../../utils/cn';
4
+ import type { TextareaGroupProps } from './TextareaGroup.types';
5
+
6
+ export const TextareaGroup = forwardRef<HTMLTextAreaElement, TextareaGroupProps>(
7
+ ({ label, helperText, error, className, disabled, id: idProp, ...props }, ref) => {
8
+ const autoId = useId();
9
+ const id = idProp ?? autoId;
10
+ const messageId = `${id}-message`;
11
+
12
+ return (
13
+ <div className={cn('flex flex-col gap-1.5', className)}>
14
+ <label
15
+ htmlFor={id}
16
+ className="text-text-primary font-medium"
17
+ style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}
18
+ >
19
+ {label}
20
+ </label>
21
+ <textarea
22
+ ref={ref}
23
+ id={id}
24
+ disabled={disabled}
25
+ aria-invalid={!!error}
26
+ aria-describedby={error || helperText ? messageId : undefined}
27
+ className={cn(
28
+ 'w-full rounded-md bg-white px-3 py-2 text-text-primary placeholder:text-text-secondary',
29
+ 'border outline-none transition-all min-h-[80px] resize-y',
30
+ 'focus:border-rust focus:ring-2 focus:ring-rust-light',
31
+ error ? 'border-error' : 'border-border',
32
+ disabled && 'bg-surface opacity-50 cursor-not-allowed',
33
+ )}
34
+ style={{
35
+ fontSize: 'var(--text-body)',
36
+ lineHeight: 'var(--leading-body)',
37
+ transitionDuration: 'var(--duration-fast)',
38
+ }}
39
+ {...props}
40
+ />
41
+ {(error || helperText) && (
42
+ <p
43
+ id={messageId}
44
+ className={cn(
45
+ 'flex items-center gap-1',
46
+ error ? 'text-error' : 'text-text-secondary',
47
+ )}
48
+ style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}
49
+ >
50
+ {error && <AlertCircle size={14} />}
51
+ {error || helperText}
52
+ </p>
53
+ )}
54
+ </div>
55
+ );
56
+ },
57
+ );
58
+
59
+ TextareaGroup.displayName = 'TextareaGroup';
@@ -0,0 +1,10 @@
1
+ import type { ComponentPropsWithoutRef } from 'react';
2
+
3
+ export interface TextareaGroupProps extends ComponentPropsWithoutRef<'textarea'> {
4
+ /** Label text displayed above the textarea */
5
+ label: string;
6
+ /** Helper text displayed below the textarea */
7
+ helperText?: string;
8
+ /** Error message — sets the textarea to error state */
9
+ error?: string;
10
+ }
@@ -0,0 +1,2 @@
1
+ export { TextareaGroup } from './TextareaGroup';
2
+ export type { TextareaGroupProps } from './TextareaGroup.types';
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Toggle } from './Toggle';
3
+
4
+ const meta: Meta<typeof Toggle> = {
5
+ title: 'Primitives/Toggle',
6
+ component: Toggle,
7
+ tags: ['autodocs'],
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof Toggle>;
12
+
13
+ export const Off: Story = {
14
+ args: { label: 'Enable notifications' },
15
+ };
16
+
17
+ export const On: Story = {
18
+ args: { label: 'Enable notifications', defaultChecked: true },
19
+ };
20
+
21
+ export const Disabled: Story = {
22
+ args: { label: 'Locked setting', disabled: true },
23
+ };
24
+
25
+ export const DisabledOn: Story = {
26
+ args: { label: 'Locked setting', disabled: true, checked: true },
27
+ };
@@ -0,0 +1,31 @@
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 { Toggle } from './Toggle';
5
+
6
+ describe('Toggle', () => {
7
+ it('renders with label', () => {
8
+ render(<Toggle label="Notifications" />);
9
+ expect(screen.getByRole('switch')).toBeInTheDocument();
10
+ expect(screen.getByText('Notifications')).toBeInTheDocument();
11
+ });
12
+
13
+ it('toggles on click', async () => {
14
+ const onChange = vi.fn();
15
+ render(<Toggle label="Notifications" onCheckedChange={onChange} />);
16
+ await userEvent.click(screen.getByRole('switch'));
17
+ expect(onChange).toHaveBeenCalledWith(true);
18
+ });
19
+
20
+ it('does not toggle when disabled', async () => {
21
+ const onChange = vi.fn();
22
+ render(<Toggle label="Notifications" disabled onCheckedChange={onChange} />);
23
+ await userEvent.click(screen.getByRole('switch'));
24
+ expect(onChange).not.toHaveBeenCalled();
25
+ });
26
+
27
+ it('reflects controlled state', () => {
28
+ render(<Toggle label="Notifications" checked={true} />);
29
+ expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true');
30
+ });
31
+ });
@@ -0,0 +1,65 @@
1
+ import { useId } from 'react';
2
+ import { cn } from '../../utils/cn';
3
+ import { useControllableState } from '../../hooks/use-controllable-state';
4
+ import type { ToggleProps } from './Toggle.types';
5
+
6
+ export function Toggle({
7
+ label,
8
+ checked: controlledChecked,
9
+ defaultChecked = false,
10
+ onCheckedChange,
11
+ disabled = false,
12
+ className,
13
+ 'aria-label': ariaLabel,
14
+ }: ToggleProps) {
15
+ const id = useId();
16
+ const [checked, setChecked] = useControllableState({
17
+ value: controlledChecked,
18
+ defaultValue: defaultChecked,
19
+ onChange: onCheckedChange,
20
+ });
21
+
22
+ return (
23
+ <label
24
+ htmlFor={id}
25
+ className={cn(
26
+ 'inline-flex items-center gap-2.5 cursor-pointer select-none',
27
+ disabled && 'opacity-40 cursor-not-allowed',
28
+ className,
29
+ )}
30
+ >
31
+ <button
32
+ id={id}
33
+ type="button"
34
+ role="switch"
35
+ aria-checked={checked}
36
+ aria-label={ariaLabel ?? label}
37
+ disabled={disabled}
38
+ onClick={() => setChecked(!checked)}
39
+ className={cn(
40
+ 'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors',
41
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rust',
42
+ checked ? 'bg-rust' : 'bg-border',
43
+ )}
44
+ style={{ transitionDuration: 'var(--duration-fast)' }}
45
+ >
46
+ <span
47
+ className={cn(
48
+ 'inline-block h-5 w-5 rounded-full bg-white shadow-1 transition-transform',
49
+ checked ? 'translate-x-[22px]' : 'translate-x-[2px]',
50
+ )}
51
+ style={{
52
+ transitionDuration: 'var(--duration-fast)',
53
+ transitionTimingFunction: 'var(--ease-spring)',
54
+ }}
55
+ />
56
+ </button>
57
+ <span
58
+ className="text-text-primary"
59
+ style={{ fontSize: 'var(--text-body)', lineHeight: 'var(--leading-body)' }}
60
+ >
61
+ {label}
62
+ </span>
63
+ </label>
64
+ );
65
+ }
@@ -0,0 +1,16 @@
1
+ export interface ToggleProps {
2
+ /** Label text displayed next to the toggle */
3
+ label: string;
4
+ /** Controlled on/off state */
5
+ checked?: boolean;
6
+ /** Default state for uncontrolled usage */
7
+ defaultChecked?: boolean;
8
+ /** Change handler */
9
+ onCheckedChange?: (checked: boolean) => void;
10
+ /** Whether the toggle is disabled */
11
+ disabled?: boolean;
12
+ /** Additional CSS classes */
13
+ className?: string;
14
+ /** Accessible name override */
15
+ 'aria-label'?: string;
16
+ }
@@ -0,0 +1,2 @@
1
+ export { Toggle } from './Toggle';
2
+ export type { ToggleProps } from './Toggle.types';
@@ -0,0 +1,45 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Tooltip } from './Tooltip';
3
+ import { Button } from '../button';
4
+
5
+ const meta: Meta<typeof Tooltip> = {
6
+ title: 'Primitives/Tooltip',
7
+ component: Tooltip,
8
+ tags: ['autodocs'],
9
+ decorators: [
10
+ (Story) => (
11
+ <div className="flex items-center justify-center p-20">
12
+ <Story />
13
+ </div>
14
+ ),
15
+ ],
16
+ };
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof Tooltip>;
20
+
21
+ export const Dark: Story = {
22
+ args: {
23
+ content: 'Create a new task',
24
+ children: <Button>Hover me</Button>,
25
+ },
26
+ };
27
+
28
+ export const Light: Story = {
29
+ args: {
30
+ content: 'Create a new task',
31
+ variant: 'light',
32
+ children: <Button variant="outline">Hover me</Button>,
33
+ },
34
+ };
35
+
36
+ export const Placements: Story = {
37
+ render: () => (
38
+ <div className="flex gap-4">
39
+ <Tooltip content="Top" placement="top"><Button variant="outline">Top</Button></Tooltip>
40
+ <Tooltip content="Bottom" placement="bottom"><Button variant="outline">Bottom</Button></Tooltip>
41
+ <Tooltip content="Left" placement="left"><Button variant="outline">Left</Button></Tooltip>
42
+ <Tooltip content="Right" placement="right"><Button variant="outline">Right</Button></Tooltip>
43
+ </div>
44
+ ),
45
+ };
@@ -0,0 +1,28 @@
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 { Tooltip } from './Tooltip';
5
+
6
+ describe('Tooltip', () => {
7
+ it('shows tooltip on hover', async () => {
8
+ render(
9
+ <Tooltip content="Help text" delayMs={0}>
10
+ <button>Hover me</button>
11
+ </Tooltip>,
12
+ );
13
+ await userEvent.hover(screen.getByRole('button', { name: 'Hover me' }));
14
+ expect(await screen.findByRole('tooltip')).toHaveTextContent('Help text');
15
+ });
16
+
17
+ it('hides tooltip on unhover', async () => {
18
+ render(
19
+ <Tooltip content="Help text" delayMs={0}>
20
+ <button>Hover me</button>
21
+ </Tooltip>,
22
+ );
23
+ await userEvent.hover(screen.getByRole('button'));
24
+ expect(await screen.findByRole('tooltip')).toBeInTheDocument();
25
+ await userEvent.unhover(screen.getByRole('button'));
26
+ // Tooltip should eventually be removed
27
+ });
28
+ });
@@ -0,0 +1,94 @@
1
+ import { useState, cloneElement } from 'react';
2
+ import {
3
+ useFloating,
4
+ useHover,
5
+ useFocus,
6
+ useDismiss,
7
+ useRole,
8
+ useInteractions,
9
+ offset,
10
+ flip,
11
+ shift,
12
+ arrow,
13
+ FloatingArrow,
14
+ FloatingPortal,
15
+ useTransitionStyles,
16
+ } from '@floating-ui/react';
17
+ import { useRef } from 'react';
18
+ import { cn } from '../../utils/cn';
19
+ import type { TooltipProps } from './Tooltip.types';
20
+
21
+ export function Tooltip({
22
+ content,
23
+ children,
24
+ variant = 'dark',
25
+ placement = 'top',
26
+ delayMs = 200,
27
+ }: TooltipProps) {
28
+ const [isOpen, setIsOpen] = useState(false);
29
+ const arrowRef = useRef(null);
30
+
31
+ const { refs, floatingStyles, context } = useFloating({
32
+ open: isOpen,
33
+ onOpenChange: setIsOpen,
34
+ placement,
35
+ middleware: [
36
+ offset(8),
37
+ flip(),
38
+ shift({ padding: 8 }),
39
+ arrow({ element: arrowRef }),
40
+ ],
41
+ });
42
+
43
+ const hover = useHover(context, { delay: { open: delayMs, close: 0 } });
44
+ const focus = useFocus(context);
45
+ const dismiss = useDismiss(context);
46
+ const role = useRole(context, { role: 'tooltip' });
47
+
48
+ const { getReferenceProps, getFloatingProps } = useInteractions([
49
+ hover,
50
+ focus,
51
+ dismiss,
52
+ role,
53
+ ]);
54
+
55
+ const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
56
+ duration: 150,
57
+ });
58
+
59
+ return (
60
+ <>
61
+ {cloneElement(children, {
62
+ ref: refs.setReference,
63
+ ...getReferenceProps(),
64
+ })}
65
+ {isMounted && (
66
+ <FloatingPortal>
67
+ <div
68
+ ref={refs.setFloating}
69
+ style={{ ...floatingStyles, ...transitionStyles }}
70
+ className={cn(
71
+ 'z-50 rounded-md px-3 py-1.5 max-w-xs',
72
+ variant === 'dark' && 'bg-text-primary text-white',
73
+ variant === 'light' && 'bg-white text-text-primary shadow-2 border border-border',
74
+ )}
75
+ {...getFloatingProps()}
76
+ >
77
+ <span style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}>
78
+ {content}
79
+ </span>
80
+ <FloatingArrow
81
+ ref={arrowRef}
82
+ context={context}
83
+ fill={variant === 'dark' ? 'var(--color-text-primary)' : '#fff'}
84
+ strokeWidth={variant === 'light' ? 1 : 0}
85
+ stroke={variant === 'light' ? 'var(--color-border)' : undefined}
86
+ width={12}
87
+ height={6}
88
+ />
89
+ </div>
90
+ </FloatingPortal>
91
+ )}
92
+ </>
93
+ );
94
+ }
@@ -0,0 +1,16 @@
1
+ import type { Placement } from '@floating-ui/react';
2
+
3
+ export type TooltipVariant = 'dark' | 'light';
4
+
5
+ export interface TooltipProps {
6
+ /** Tooltip content */
7
+ content: React.ReactNode;
8
+ /** The trigger element */
9
+ children: React.ReactElement;
10
+ /** Visual variant */
11
+ variant?: TooltipVariant;
12
+ /** Placement relative to trigger */
13
+ placement?: Placement;
14
+ /** Delay before showing (ms) */
15
+ delayMs?: number;
16
+ }
@@ -0,0 +1,2 @@
1
+ export { Tooltip } from './Tooltip';
2
+ export type { TooltipProps, TooltipVariant } from './Tooltip.types';
@@ -0,0 +1,20 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { CommentThread } from './CommentThread';
3
+
4
+ const meta: Meta<typeof CommentThread> = { title: 'Productivity/CommentThread', component: CommentThread, tags: ['autodocs'] };
5
+ export default meta;
6
+ type Story = StoryObj<typeof CommentThread>;
7
+
8
+ export const Default: Story = {
9
+ args: {
10
+ comments: [
11
+ {
12
+ id: '1', author: { name: 'Alice' }, content: 'The landing page design looks great! Just a few tweaks needed on the CTA.', timestamp: '2 hours ago',
13
+ replies: [
14
+ { id: '2', author: { name: 'Bob' }, content: "Thanks! I'll update the CTA button color.", timestamp: '1 hour ago' },
15
+ ],
16
+ },
17
+ { id: '3', author: { name: 'Charlie' }, content: 'Should we also update the mobile layout?', timestamp: '30 min ago' },
18
+ ],
19
+ },
20
+ };
@@ -0,0 +1,21 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { CommentThread } from './CommentThread';
4
+
5
+ describe('CommentThread', () => {
6
+ it('renders comments', () => {
7
+ render(<CommentThread comments={[{ id: '1', author: { name: 'Alice' }, content: 'Hello', timestamp: '1h ago' }]} />);
8
+ expect(screen.getByText('Alice')).toBeInTheDocument();
9
+ expect(screen.getByText('Hello')).toBeInTheDocument();
10
+ });
11
+ it('renders replies', () => {
12
+ render(
13
+ <CommentThread comments={[{
14
+ id: '1', author: { name: 'Alice' }, content: 'Hello', timestamp: '1h ago',
15
+ replies: [{ id: '2', author: { name: 'Bob' }, content: 'Hi!', timestamp: '30m ago' }],
16
+ }]} />,
17
+ );
18
+ expect(screen.getByText('Bob')).toBeInTheDocument();
19
+ expect(screen.getByText('Hi!')).toBeInTheDocument();
20
+ });
21
+ });
@@ -0,0 +1,47 @@
1
+ import { cn } from '../../utils/cn';
2
+ import { Avatar } from '../../primitives/avatar';
3
+
4
+ export interface Comment {
5
+ id: string;
6
+ author: { name: string; avatarSrc?: string };
7
+ content: string;
8
+ timestamp: string;
9
+ replies?: Comment[];
10
+ }
11
+
12
+ export interface CommentThreadProps {
13
+ comments: Comment[];
14
+ className?: string;
15
+ }
16
+
17
+ function CommentItem({ comment, depth = 0 }: { comment: Comment; depth?: number }) {
18
+ return (
19
+ <div className={cn('flex gap-3', depth > 0 && 'ml-10')}>
20
+ <Avatar name={comment.author.name} src={comment.author.avatarSrc} size="sm" />
21
+ <div className="flex-1 min-w-0">
22
+ <div className="flex items-center gap-2">
23
+ <span className="text-text-primary font-medium" style={{ fontSize: 'var(--text-body-sm)' }}>{comment.author.name}</span>
24
+ <span className="text-text-secondary" style={{ fontSize: 'var(--text-caption)' }}>{comment.timestamp}</span>
25
+ </div>
26
+ <p className="text-text-primary mt-1" style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}>
27
+ {comment.content}
28
+ </p>
29
+ {comment.replies?.map((reply) => (
30
+ <div key={reply.id} className="mt-3">
31
+ <CommentItem comment={reply} depth={depth + 1} />
32
+ </div>
33
+ ))}
34
+ </div>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export function CommentThread({ comments, className }: CommentThreadProps) {
40
+ return (
41
+ <div className={cn('flex flex-col gap-4', className)}>
42
+ {comments.map((comment) => (
43
+ <CommentItem key={comment.id} comment={comment} />
44
+ ))}
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,2 @@
1
+ export { CommentThread } from './CommentThread';
2
+ export type { CommentThreadProps, Comment } from './CommentThread';
@@ -0,0 +1,41 @@
1
+ import {
2
+ DndContext,
3
+ closestCorners,
4
+ KeyboardSensor,
5
+ PointerSensor,
6
+ useSensor,
7
+ useSensors,
8
+ type DragEndEvent,
9
+ type DragOverEvent,
10
+ } from '@dnd-kit/core';
11
+ import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
12
+ import { cn } from '../../utils/cn';
13
+
14
+ export interface KanbanBoardProps {
15
+ /** Called when an item is dropped */
16
+ onDragEnd?: (event: DragEndEvent) => void;
17
+ /** Called when dragging over a new container */
18
+ onDragOver?: (event: DragOverEvent) => void;
19
+ children: React.ReactNode;
20
+ className?: string;
21
+ }
22
+
23
+ export function KanbanBoard({ onDragEnd, onDragOver, children, className }: KanbanBoardProps) {
24
+ const sensors = useSensors(
25
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
26
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
27
+ );
28
+
29
+ return (
30
+ <DndContext
31
+ sensors={sensors}
32
+ collisionDetection={closestCorners}
33
+ onDragEnd={onDragEnd}
34
+ onDragOver={onDragOver}
35
+ >
36
+ <div className={cn('flex gap-4 overflow-x-auto p-4', className)}>
37
+ {children}
38
+ </div>
39
+ </DndContext>
40
+ );
41
+ }
@@ -0,0 +1,2 @@
1
+ export { KanbanBoard } from './KanbanBoard';
2
+ export type { KanbanBoardProps } from './KanbanBoard';