tharaday 0.8.2 → 0.8.4

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 (240) hide show
  1. package/.gitignore +3 -0
  2. package/.storybook/preview.ts +2 -2
  3. package/dist/{src/components → components}/Accordion/Accordion.d.ts +0 -1
  4. package/dist/{src/components → components}/Avatar/Avatar.d.ts +0 -1
  5. package/dist/{src/components → components}/Badge/Badge.d.ts +0 -1
  6. package/dist/{src/components → components}/Box/Box.d.ts +0 -1
  7. package/dist/{src/components → components}/Box/Box.types.d.ts +11 -11
  8. package/dist/{src/components → components}/Box/helpers/getSpacingStyles.d.ts +2 -2
  9. package/dist/{src/components → components}/Breadcrumbs/Breadcrumbs.d.ts +0 -1
  10. package/dist/{src/components → components}/Button/Button.d.ts +0 -1
  11. package/dist/{src/components → components}/Card/Card.d.ts +0 -1
  12. package/dist/{src/components → components}/Checkbox/Checkbox.d.ts +0 -1
  13. package/dist/components/DatePicker/DatePicker.d.ts +1 -0
  14. package/dist/{src/components → components}/Divider/Divider.d.ts +0 -1
  15. package/dist/components/Drawer/Drawer.d.ts +1 -0
  16. package/dist/{src/components → components}/Dropdown/Dropdown.d.ts +0 -1
  17. package/dist/components/EmptyState/EmptyState.d.ts +1 -0
  18. package/dist/{src/components → components}/Header/Header.d.ts +0 -1
  19. package/dist/{src/components → components}/Input/Input.d.ts +0 -1
  20. package/dist/{src/components → components}/List/List.d.ts +2 -2
  21. package/dist/{src/components → components}/List/List.types.d.ts +3 -3
  22. package/dist/{src/components → components}/List/ListItem.d.ts +1 -1
  23. package/dist/{src/components → components}/List/ListItem.types.d.ts +1 -1
  24. package/dist/{src/components → components}/Loader/Loader.d.ts +0 -1
  25. package/dist/{src/components → components}/Modal/Modal.d.ts +1 -2
  26. package/dist/{src/components → components}/NavBar/NavBar.d.ts +0 -1
  27. package/dist/{src/components → components}/Notification/Notification.d.ts +0 -1
  28. package/dist/{src/components → components}/Pagination/Pagination.d.ts +0 -1
  29. package/dist/components/Popover/Popover.d.ts +1 -0
  30. package/dist/{src/components → components}/ProgressBar/ProgressBar.d.ts +0 -1
  31. package/dist/{src/components → components}/RadioButton/RadioButton.d.ts +0 -1
  32. package/dist/{src/components → components}/Select/Select.d.ts +0 -1
  33. package/dist/{src/components → components}/Skeleton/Skeleton.d.ts +0 -1
  34. package/dist/{src/components → components}/Slider/Slider.d.ts +0 -1
  35. package/dist/{src/components → components}/Stepper/Step.d.ts +0 -1
  36. package/dist/{src/components → components}/Stepper/Stepper.d.ts +0 -1
  37. package/dist/{src/components → components}/Stepper/stepper.utils.d.ts +2 -2
  38. package/dist/{src/components → components}/Switch/Switch.d.ts +0 -1
  39. package/dist/{src/components → components}/Table/Table.d.ts +0 -1
  40. package/dist/{src/components → components}/Tabs/Tabs.d.ts +0 -1
  41. package/dist/components/Tag/Tag.d.ts +1 -0
  42. package/dist/{src/components → components}/Text/Text.d.ts +0 -1
  43. package/dist/{src/components → components}/Textarea/Textarea.d.ts +0 -1
  44. package/dist/{src/components → components}/Tooltip/Tooltip.d.ts +0 -1
  45. package/dist/{src/components → components}/Tree/Tree.d.ts +2 -2
  46. package/dist/{src/components → components}/Tree/Tree.types.d.ts +1 -1
  47. package/dist/{src/components → components}/Tree/TreeItem.d.ts +1 -1
  48. package/dist/{src/components → components}/Tree/TreeItem.types.d.ts +1 -1
  49. package/dist/ds.css +1 -1
  50. package/dist/ds.js +1351 -1206
  51. package/dist/ds.umd.cjs +1 -1
  52. package/dist/hooks/useClickOutside.d.ts +6 -0
  53. package/dist/{src/hooks → hooks}/useComponentId.d.ts +1 -1
  54. package/dist/hooks/useFocusTrap.d.ts +17 -0
  55. package/dist/{src/index.d.ts → index.d.ts} +38 -28
  56. package/dist/{src/layouts → layouts}/AppLayout/AppLayout.d.ts +0 -1
  57. package/dist/{src/layouts → layouts}/AuthLayout/AuthLayout.d.ts +0 -1
  58. package/dist/{src/layouts → layouts}/DashboardLayout/DashboardLayout.d.ts +0 -1
  59. package/dist/{src/layouts → layouts}/SettingsLayout/SettingsLayout.d.ts +0 -1
  60. package/eslint.config.js +15 -0
  61. package/package.json +13 -11
  62. package/src/components/Accordion/Accordion.stories.tsx +1 -1
  63. package/src/components/Accordion/Accordion.tsx +1 -1
  64. package/src/components/Avatar/Avatar.stories.tsx +1 -1
  65. package/src/components/Avatar/Avatar.test.tsx +1 -1
  66. package/src/components/Badge/Badge.stories.tsx +1 -1
  67. package/src/components/Box/Box.module.css +0 -557
  68. package/src/components/Box/Box.test.tsx +4 -4
  69. package/src/components/Box/Box.tsx +8 -16
  70. package/src/components/Box/Box.types.ts +1 -1
  71. package/src/components/Box/helpers/getSpacingStyles.ts +24 -17
  72. package/src/components/Breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  73. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +1 -1
  74. package/src/components/Breadcrumbs/Breadcrumbs.tsx +1 -1
  75. package/src/components/Breadcrumbs/Breadcrumbs.types.ts +1 -1
  76. package/src/components/Button/Button.stories.tsx +1 -1
  77. package/src/components/Card/Card.stories.tsx +1 -1
  78. package/src/components/Card/Card.test.tsx +1 -1
  79. package/src/components/Card/Card.tsx +2 -2
  80. package/src/components/Checkbox/Checkbox.stories.tsx +1 -1
  81. package/src/components/Checkbox/Checkbox.tsx +1 -1
  82. package/src/components/DatePicker/DatePicker.module.css +212 -0
  83. package/src/components/DatePicker/DatePicker.stories.tsx +56 -0
  84. package/src/components/DatePicker/DatePicker.test.tsx +61 -0
  85. package/src/components/DatePicker/DatePicker.tsx +271 -0
  86. package/src/components/DatePicker/DatePicker.types.ts +11 -0
  87. package/src/components/Divider/Divider.stories.tsx +1 -1
  88. package/src/components/Drawer/Drawer.module.css +126 -0
  89. package/src/components/Drawer/Drawer.stories.tsx +71 -0
  90. package/src/components/Drawer/Drawer.test.tsx +49 -0
  91. package/src/components/Drawer/Drawer.tsx +77 -0
  92. package/src/components/Drawer/Drawer.types.ts +17 -0
  93. package/src/components/Dropdown/Dropdown.tsx +7 -3
  94. package/src/components/EmptyState/EmptyState.module.css +73 -0
  95. package/src/components/EmptyState/EmptyState.stories.tsx +65 -0
  96. package/src/components/EmptyState/EmptyState.test.tsx +30 -0
  97. package/src/components/EmptyState/EmptyState.tsx +29 -0
  98. package/src/components/EmptyState/EmptyState.types.ts +12 -0
  99. package/src/components/Header/Header.test.tsx +5 -5
  100. package/src/components/Header/Header.tsx +2 -2
  101. package/src/components/Input/Input.stories.tsx +1 -1
  102. package/src/components/Input/Input.tsx +1 -1
  103. package/src/components/List/List.stories.tsx +1 -0
  104. package/src/components/List/List.tsx +1 -1
  105. package/src/components/List/List.types.ts +3 -2
  106. package/src/components/List/ListItem.tsx +2 -1
  107. package/src/components/List/ListItem.types.ts +1 -1
  108. package/src/components/Loader/Loader.stories.tsx +1 -1
  109. package/src/components/Modal/Modal.stories.tsx +1 -1
  110. package/src/components/Modal/Modal.tsx +4 -65
  111. package/src/components/NavBar/NavBar.stories.tsx +1 -1
  112. package/src/components/Notification/Notification.stories.tsx +1 -1
  113. package/src/components/Notification/Notification.tsx +1 -1
  114. package/src/components/Pagination/Pagination.tsx +2 -2
  115. package/src/components/Popover/Popover.module.css +52 -0
  116. package/src/components/Popover/Popover.stories.tsx +67 -0
  117. package/src/components/Popover/Popover.test.tsx +40 -0
  118. package/src/components/Popover/Popover.tsx +78 -0
  119. package/src/components/Popover/Popover.types.ts +13 -0
  120. package/src/components/ProgressBar/ProgressBar.stories.tsx +1 -1
  121. package/src/components/ProgressBar/ProgressBar.tsx +1 -1
  122. package/src/components/RadioButton/RadioButton.stories.tsx +1 -1
  123. package/src/components/RadioButton/RadioButton.tsx +1 -1
  124. package/src/components/Select/Select.stories.tsx +1 -1
  125. package/src/components/Select/Select.tsx +1 -1
  126. package/src/components/Select/Select.types.ts +1 -1
  127. package/src/components/Skeleton/Skeleton.stories.tsx +1 -1
  128. package/src/components/Skeleton/Skeleton.tsx +1 -1
  129. package/src/components/Slider/Slider.tsx +4 -1
  130. package/src/components/Stepper/Stepper.stories.tsx +1 -1
  131. package/src/components/Stepper/Stepper.tsx +1 -1
  132. package/src/components/Stepper/stepper.utils.ts +4 -1
  133. package/src/components/Switch/Switch.stories.tsx +1 -1
  134. package/src/components/Switch/Switch.tsx +1 -1
  135. package/src/components/Table/Table.stories.tsx +1 -1
  136. package/src/components/Table/Table.test.tsx +3 -3
  137. package/src/components/Table/Table.tsx +4 -4
  138. package/src/components/Tabs/Tabs.tsx +2 -2
  139. package/src/components/Tag/Tag.module.css +115 -0
  140. package/src/components/Tag/Tag.stories.tsx +61 -0
  141. package/src/components/Tag/Tag.test.tsx +42 -0
  142. package/src/components/Tag/Tag.tsx +74 -0
  143. package/src/components/Tag/Tag.types.ts +15 -0
  144. package/src/components/Text/Text.module.css +0 -521
  145. package/src/components/Text/Text.stories.tsx +1 -1
  146. package/src/components/Text/Text.test.tsx +4 -4
  147. package/src/components/Text/Text.tsx +0 -14
  148. package/src/components/Text/Text.types.ts +3 -3
  149. package/src/components/Textarea/Textarea.stories.tsx +1 -1
  150. package/src/components/Textarea/Textarea.tsx +1 -1
  151. package/src/components/Tooltip/Tooltip.module.css +1 -1
  152. package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
  153. package/src/components/Tooltip/Tooltip.test.tsx +5 -5
  154. package/src/components/Tooltip/Tooltip.tsx +6 -9
  155. package/src/components/Tree/Tree.stories.tsx +1 -0
  156. package/src/components/Tree/Tree.tsx +3 -2
  157. package/src/components/Tree/TreeItem.tsx +5 -2
  158. package/src/hooks/useClickOutside.test.tsx +69 -0
  159. package/src/hooks/useClickOutside.ts +36 -0
  160. package/src/hooks/useComponentId.ts +1 -0
  161. package/src/hooks/useFocusTrap.test.tsx +97 -0
  162. package/src/hooks/useFocusTrap.ts +89 -0
  163. package/src/index.ts +43 -33
  164. package/src/layouts/AppLayout/AppLayout.stories.tsx +2 -2
  165. package/src/layouts/AppLayout/AppLayout.tsx +1 -1
  166. package/src/layouts/AuthLayout/AuthLayout.stories.tsx +1 -1
  167. package/src/layouts/AuthLayout/AuthLayout.tsx +2 -2
  168. package/src/layouts/AuthLayout/AuthLayout.types.tsx +2 -2
  169. package/src/layouts/DashboardLayout/DashboardLayout.stories.tsx +3 -3
  170. package/src/layouts/SettingsLayout/SettingsLayout.stories.tsx +3 -3
  171. package/src/styles/themes.browser.test.ts +76 -0
  172. package/vite.config.ts +2 -3
  173. package/dist/src/components/Accordion/Accordion.stories.d.ts +0 -14
  174. package/dist/src/components/Accordion/Accordion.types.d.ts +0 -18
  175. package/dist/src/components/Avatar/Avatar.stories.d.ts +0 -14
  176. package/dist/src/components/Avatar/Avatar.types.d.ts +0 -10
  177. package/dist/src/components/Badge/Badge.stories.d.ts +0 -33
  178. package/dist/src/components/Badge/Badge.types.d.ts +0 -10
  179. package/dist/src/components/Box/Box.stories.d.ts +0 -38
  180. package/dist/src/components/Breadcrumbs/Breadcrumbs.stories.d.ts +0 -13
  181. package/dist/src/components/Breadcrumbs/Breadcrumbs.types.d.ts +0 -11
  182. package/dist/src/components/Button/Button.stories.d.ts +0 -22
  183. package/dist/src/components/Button/Button.types.d.ts +0 -12
  184. package/dist/src/components/Card/Card.stories.d.ts +0 -27
  185. package/dist/src/components/Card/Card.types.d.ts +0 -16
  186. package/dist/src/components/Checkbox/Checkbox.stories.d.ts +0 -17
  187. package/dist/src/components/Checkbox/Checkbox.types.d.ts +0 -7
  188. package/dist/src/components/Divider/Divider.stories.d.ts +0 -15
  189. package/dist/src/components/Divider/Divider.types.d.ts +0 -10
  190. package/dist/src/components/Dropdown/Dropdown.stories.d.ts +0 -12
  191. package/dist/src/components/Dropdown/Dropdown.types.d.ts +0 -24
  192. package/dist/src/components/Header/Header.stories.d.ts +0 -20
  193. package/dist/src/components/Header/Header.types.d.ts +0 -16
  194. package/dist/src/components/Input/Input.stories.d.ts +0 -32
  195. package/dist/src/components/Input/Input.types.d.ts +0 -10
  196. package/dist/src/components/List/List.stories.d.ts +0 -25
  197. package/dist/src/components/Loader/Loader.stories.d.ts +0 -25
  198. package/dist/src/components/Loader/Loader.types.d.ts +0 -8
  199. package/dist/src/components/Modal/Modal.stories.d.ts +0 -28
  200. package/dist/src/components/Modal/Modal.types.d.ts +0 -12
  201. package/dist/src/components/NavBar/NavBar.stories.d.ts +0 -8
  202. package/dist/src/components/NavBar/NavBar.types.d.ts +0 -38
  203. package/dist/src/components/Notification/Notification.stories.d.ts +0 -26
  204. package/dist/src/components/Notification/Notification.types.d.ts +0 -9
  205. package/dist/src/components/Pagination/Pagination.stories.d.ts +0 -21
  206. package/dist/src/components/Pagination/Pagination.types.d.ts +0 -34
  207. package/dist/src/components/ProgressBar/ProgressBar.stories.d.ts +0 -32
  208. package/dist/src/components/ProgressBar/ProgressBar.types.d.ts +0 -12
  209. package/dist/src/components/RadioButton/RadioButton.stories.d.ts +0 -30
  210. package/dist/src/components/RadioButton/RadioButton.types.d.ts +0 -9
  211. package/dist/src/components/Select/Select.stories.d.ts +0 -32
  212. package/dist/src/components/Select/Select.types.d.ts +0 -23
  213. package/dist/src/components/Skeleton/Skeleton.stories.d.ts +0 -15
  214. package/dist/src/components/Skeleton/Skeleton.types.d.ts +0 -9
  215. package/dist/src/components/Slider/Slider.stories.d.ts +0 -23
  216. package/dist/src/components/Slider/Slider.types.d.ts +0 -15
  217. package/dist/src/components/Stepper/Step.types.d.ts +0 -18
  218. package/dist/src/components/Stepper/Stepper.stories.d.ts +0 -15
  219. package/dist/src/components/Stepper/Stepper.types.d.ts +0 -14
  220. package/dist/src/components/Switch/Switch.stories.d.ts +0 -16
  221. package/dist/src/components/Switch/Switch.types.d.ts +0 -6
  222. package/dist/src/components/Table/Table.stories.d.ts +0 -27
  223. package/dist/src/components/Table/Table.types.d.ts +0 -19
  224. package/dist/src/components/Tabs/Tabs.stories.d.ts +0 -19
  225. package/dist/src/components/Tabs/Tabs.types.d.ts +0 -16
  226. package/dist/src/components/Text/Text.stories.d.ts +0 -78
  227. package/dist/src/components/Text/Text.types.d.ts +0 -52
  228. package/dist/src/components/Textarea/Textarea.stories.d.ts +0 -32
  229. package/dist/src/components/Textarea/Textarea.types.d.ts +0 -11
  230. package/dist/src/components/Tooltip/Tooltip.stories.d.ts +0 -10
  231. package/dist/src/components/Tooltip/Tooltip.types.d.ts +0 -12
  232. package/dist/src/components/Tree/Tree.stories.d.ts +0 -27
  233. package/dist/src/layouts/AppLayout/AppLayout.stories.d.ts +0 -13
  234. package/dist/src/layouts/AppLayout/AppLayout.types.d.ts +0 -13
  235. package/dist/src/layouts/AuthLayout/AuthLayout.stories.d.ts +0 -12
  236. package/dist/src/layouts/AuthLayout/AuthLayout.types.d.ts +0 -8
  237. package/dist/src/layouts/DashboardLayout/DashboardLayout.stories.d.ts +0 -11
  238. package/dist/src/layouts/DashboardLayout/DashboardLayout.types.d.ts +0 -10
  239. package/dist/src/layouts/SettingsLayout/SettingsLayout.stories.d.ts +0 -11
  240. package/dist/src/layouts/SettingsLayout/SettingsLayout.types.d.ts +0 -9
@@ -1,6 +1,6 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
 
3
- import { Breadcrumbs, BreadcrumbItem } from './Breadcrumbs.tsx';
3
+ import { BreadcrumbItem, Breadcrumbs } from './Breadcrumbs.tsx';
4
4
 
5
5
  describe('Breadcrumbs', () => {
6
6
  it('renders a navigation landmark with label "Breadcrumbs"', () => {
@@ -2,7 +2,7 @@ import classnames from 'classnames';
2
2
  import { Children, Fragment, isValidElement } from 'react';
3
3
 
4
4
  import styles from './Breadcrumbs.module.css';
5
- import type { BreadcrumbsProps, BreadcrumbItemProps } from './Breadcrumbs.types.ts';
5
+ import type { BreadcrumbItemProps, BreadcrumbsProps } from './Breadcrumbs.types.ts';
6
6
 
7
7
  export const BreadcrumbItem = ({
8
8
  href,
@@ -1,4 +1,4 @@
1
- import type { ReactNode, AnchorHTMLAttributes, HTMLAttributes } from 'react';
1
+ import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from 'react';
2
2
 
3
3
  export interface BreadcrumbsProps extends HTMLAttributes<HTMLElement> {
4
4
  children?: ReactNode;
@@ -1,8 +1,8 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { fn } from 'storybook/test';
3
3
 
4
- import { Button } from './Button.tsx';
5
4
  import { Box } from '../Box/Box.tsx';
5
+ import { Button } from './Button.tsx';
6
6
 
7
7
  const meta = {
8
8
  title: 'Components/Button',
@@ -1,8 +1,8 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
 
3
- import { Card, CardHeader, CardContent, CardFooter } from './Card.tsx';
4
3
  import { Button } from '../Button/Button.tsx';
5
4
  import { Text } from '../Text/Text.tsx';
5
+ import { Card, CardContent, CardFooter, CardHeader } from './Card.tsx';
6
6
 
7
7
  const meta = {
8
8
  title: 'Components/Card',
@@ -1,6 +1,6 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
 
3
- import { Card, CardHeader, CardContent, CardFooter } from './Card.tsx';
3
+ import { Card, CardContent, CardFooter, CardHeader } from './Card.tsx';
4
4
 
5
5
  describe('Card', () => {
6
6
  it('renders children', () => {
@@ -2,10 +2,10 @@ import classnames from 'classnames';
2
2
 
3
3
  import styles from './Card.module.css';
4
4
  import type {
5
- CardProps,
6
- CardHeaderProps,
7
5
  CardContentProps,
8
6
  CardFooterProps,
7
+ CardHeaderProps,
8
+ CardProps,
9
9
  } from './Card.types.ts';
10
10
 
11
11
  export const Card = ({
@@ -1,7 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
 
3
- import { Checkbox } from './Checkbox.tsx';
4
3
  import { Box } from '../Box/Box.tsx';
4
+ import { Checkbox } from './Checkbox.tsx';
5
5
 
6
6
  const meta = {
7
7
  title: 'Components/Checkbox',
@@ -1,6 +1,6 @@
1
1
  import classnames from 'classnames';
2
- import { useComponentId } from '../../hooks/useComponentId.ts';
3
2
 
3
+ import { useComponentId } from '../../hooks/useComponentId.ts';
4
4
  import styles from './Checkbox.module.css';
5
5
  import type { CheckboxProps } from './Checkbox.types.ts';
6
6
 
@@ -0,0 +1,212 @@
1
+ .root {
2
+ position: relative;
3
+ display: inline-flex;
4
+ flex-direction: column;
5
+ gap: var(--ds-space-2);
6
+ font-family: var(--ds-font-family-base);
7
+ }
8
+
9
+ .label {
10
+ font-size: var(--ds-font-size-sm);
11
+ font-weight: var(--ds-font-weight-medium);
12
+ color: var(--ds-text-1);
13
+ }
14
+
15
+ .trigger {
16
+ all: unset;
17
+ display: inline-flex;
18
+ align-items: center;
19
+ justify-content: space-between;
20
+ gap: var(--ds-space-2);
21
+ height: var(--ds-space-10);
22
+ padding: 0 var(--ds-space-3);
23
+ border: 1px solid var(--ds-border-1);
24
+ border-radius: var(--ds-radius-md);
25
+ background-color: var(--ds-surface-0);
26
+ color: var(--ds-text-1);
27
+ font-size: var(--ds-font-size-sm);
28
+ font-family: var(--ds-font-family-base);
29
+ cursor: pointer;
30
+ min-width: 12rem;
31
+ box-sizing: border-box;
32
+ transition:
33
+ border-color 0.15s,
34
+ box-shadow 0.15s;
35
+ }
36
+ .trigger:hover:not(:disabled) {
37
+ border-color: var(--ds-neutral);
38
+ }
39
+ .trigger:focus-visible {
40
+ outline: none;
41
+ border-color: var(--ds-ring);
42
+ box-shadow:
43
+ 0 0 0 2px var(--ds-ring-offset),
44
+ 0 0 0 4px var(--ds-ring);
45
+ }
46
+ .trigger.disabled,
47
+ .trigger:disabled {
48
+ opacity: 0.5;
49
+ cursor: not-allowed;
50
+ }
51
+
52
+ .triggerText {
53
+ flex: 1;
54
+ text-align: left;
55
+ }
56
+ .placeholder {
57
+ color: var(--ds-text-disabled);
58
+ }
59
+
60
+ .calendarIcon {
61
+ color: var(--ds-text-2);
62
+ flex-shrink: 0;
63
+ }
64
+
65
+ /* Calendar panel */
66
+ .calendar {
67
+ position: absolute;
68
+ top: calc(100% + var(--ds-space-2));
69
+ left: 0;
70
+ z-index: 500;
71
+ background-color: var(--ds-surface-0);
72
+ border: 1px solid var(--ds-border-1);
73
+ border-radius: var(--ds-radius-md);
74
+ box-shadow: var(--ds-shadow-md);
75
+ padding: var(--ds-space-3);
76
+ min-width: 16rem;
77
+ }
78
+
79
+ .calendarHeader {
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: space-between;
83
+ margin-bottom: var(--ds-space-3);
84
+ }
85
+
86
+ .monthYear {
87
+ font-size: var(--ds-font-size-sm);
88
+ font-weight: var(--ds-font-weight-medium);
89
+ color: var(--ds-text-1);
90
+ }
91
+
92
+ .navButton {
93
+ all: unset;
94
+ display: inline-flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ width: 1.75rem;
98
+ height: 1.75rem;
99
+ border-radius: var(--ds-radius-sm);
100
+ cursor: pointer;
101
+ color: var(--ds-text-2);
102
+ transition:
103
+ background-color 0.15s,
104
+ color 0.15s;
105
+ }
106
+ .navButton:hover {
107
+ background-color: var(--ds-surface-1);
108
+ color: var(--ds-text-1);
109
+ }
110
+ .navButton:focus-visible {
111
+ outline: none;
112
+ box-shadow:
113
+ 0 0 0 2px var(--ds-ring-offset),
114
+ 0 0 0 4px var(--ds-ring);
115
+ }
116
+
117
+ .weekdays {
118
+ display: grid;
119
+ grid-template-columns: repeat(7, 1fr);
120
+ margin-bottom: var(--ds-space-1);
121
+ }
122
+
123
+ .weekday {
124
+ text-align: center;
125
+ font-size: var(--ds-font-size-xs);
126
+ font-weight: var(--ds-font-weight-medium);
127
+ color: var(--ds-text-2);
128
+ padding: var(--ds-space-1) 0;
129
+ }
130
+
131
+ .days {
132
+ display: grid;
133
+ grid-template-columns: repeat(7, 1fr);
134
+ gap: 2px;
135
+ }
136
+
137
+ .dayEmpty {
138
+ aspect-ratio: 1;
139
+ }
140
+
141
+ .day {
142
+ all: unset;
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ aspect-ratio: 1;
147
+ border-radius: var(--ds-radius-sm);
148
+ font-size: var(--ds-font-size-xs);
149
+ cursor: pointer;
150
+ color: var(--ds-text-1);
151
+ transition:
152
+ background-color 0.1s,
153
+ color 0.1s;
154
+ }
155
+ .day:hover:not(:disabled) {
156
+ background-color: var(--ds-surface-1);
157
+ }
158
+ .day:focus-visible {
159
+ outline: none;
160
+ box-shadow:
161
+ 0 0 0 2px var(--ds-ring-offset),
162
+ 0 0 0 4px var(--ds-ring);
163
+ }
164
+
165
+ .daySelected {
166
+ background-color: var(--ds-info);
167
+ color: var(--ds-text-on-brand);
168
+ font-weight: var(--ds-font-weight-medium);
169
+ }
170
+ .daySelected:hover {
171
+ background-color: var(--ds-info-hover);
172
+ }
173
+
174
+ .dayToday {
175
+ font-weight: var(--ds-font-weight-bold);
176
+ color: var(--ds-info);
177
+ }
178
+
179
+ .dayDisabled {
180
+ opacity: 0.35;
181
+ cursor: not-allowed;
182
+ }
183
+
184
+ .calendarFooter {
185
+ margin-top: var(--ds-space-3);
186
+ padding-top: var(--ds-space-2);
187
+ border-top: 1px solid var(--ds-border-2);
188
+ display: flex;
189
+ justify-content: flex-end;
190
+ }
191
+
192
+ .clearButton {
193
+ all: unset;
194
+ font-size: var(--ds-font-size-xs);
195
+ color: var(--ds-text-2);
196
+ cursor: pointer;
197
+ padding: var(--ds-space-1) var(--ds-space-2);
198
+ border-radius: var(--ds-radius-sm);
199
+ transition:
200
+ background-color 0.15s,
201
+ color 0.15s;
202
+ }
203
+ .clearButton:hover {
204
+ background-color: var(--ds-surface-1);
205
+ color: var(--ds-text-1);
206
+ }
207
+ .clearButton:focus-visible {
208
+ outline: none;
209
+ box-shadow:
210
+ 0 0 0 2px var(--ds-ring-offset),
211
+ 0 0 0 4px var(--ds-ring);
212
+ }
@@ -0,0 +1,56 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { useState } from 'react';
3
+
4
+ import { DatePicker } from './DatePicker.tsx';
5
+
6
+ const meta = {
7
+ title: 'Components/DatePicker',
8
+ component: DatePicker,
9
+ tags: ['autodocs'],
10
+ parameters: { layout: 'centered' },
11
+ } satisfies Meta<typeof DatePicker>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ const Controlled = (args: React.ComponentProps<typeof DatePicker>) => {
17
+ const [value, setValue] = useState<Date | null>(null);
18
+
19
+ return <DatePicker {...args} value={value} onChange={setValue} />;
20
+ };
21
+
22
+ const Preselected = (args: React.ComponentProps<typeof DatePicker>) => {
23
+ const [value, setValue] = useState<Date | null>(new Date(2026, 0, 15));
24
+
25
+ return <DatePicker {...args} value={value} onChange={setValue} />;
26
+ };
27
+
28
+ const WithMinMaxDemo = (args: React.ComponentProps<typeof DatePicker>) => {
29
+ const [value, setValue] = useState<Date | null>(null);
30
+
31
+ return <DatePicker {...args} value={value} onChange={setValue} />;
32
+ };
33
+
34
+ export const Default: Story = {
35
+ render: (args) => <Controlled {...args} />,
36
+ args: { label: 'Date', placeholder: 'Select date' },
37
+ };
38
+
39
+ export const WithPreselectedDate: Story = {
40
+ render: (args) => <Preselected {...args} />,
41
+ args: { label: 'Date' },
42
+ };
43
+
44
+ export const Disabled: Story = {
45
+ args: { label: 'Date', disabled: true },
46
+ };
47
+
48
+ export const WithMinMax: Story = {
49
+ render: (args) => <WithMinMaxDemo {...args} />,
50
+ args: {
51
+ label: 'Date',
52
+ min: new Date(2026, 3, 1),
53
+ max: new Date(2026, 3, 30),
54
+ placeholder: 'April 2026 only',
55
+ },
56
+ };
@@ -0,0 +1,61 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import { DatePicker } from './DatePicker.tsx';
5
+
6
+ describe('DatePicker', () => {
7
+ it('renders placeholder when no value', () => {
8
+ render(<DatePicker placeholder="Pick a date" />);
9
+ expect(screen.getByText('Pick a date')).toBeInTheDocument();
10
+ });
11
+
12
+ it('renders formatted value when provided', () => {
13
+ render(<DatePicker value={new Date(2026, 0, 15)} />);
14
+ expect(screen.getByText('Jan 15, 2026')).toBeInTheDocument();
15
+ });
16
+
17
+ it('renders label when provided', () => {
18
+ render(<DatePicker label="Due date" />);
19
+ expect(screen.getByText('Due date')).toBeInTheDocument();
20
+ });
21
+
22
+ it('opens calendar on trigger click', async () => {
23
+ render(<DatePicker />);
24
+ await userEvent.click(screen.getByRole('button'));
25
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
26
+ });
27
+
28
+ it('closes calendar on Escape', async () => {
29
+ render(<DatePicker />);
30
+ await userEvent.click(screen.getByRole('button'));
31
+ await userEvent.keyboard('{Escape}');
32
+ expect(screen.queryByRole('dialog')).toBeNull();
33
+ });
34
+
35
+ it('calls onChange when a day is selected', async () => {
36
+ const onChange = vi.fn();
37
+ render(<DatePicker onChange={onChange} />);
38
+ await userEvent.click(screen.getByRole('button'));
39
+ const dayButtons = screen
40
+ .getAllByRole('button')
41
+ .filter((b) => /^\d+$/.test(b.textContent ?? ''));
42
+ await userEvent.click(dayButtons[0]);
43
+ expect(onChange).toHaveBeenCalledOnce();
44
+ expect(onChange.mock.calls[0][0]).toBeInstanceOf(Date);
45
+ });
46
+
47
+ it('does not open when disabled', async () => {
48
+ render(<DatePicker disabled />);
49
+ await userEvent.click(screen.getByRole('button'));
50
+ expect(screen.queryByRole('dialog')).toBeNull();
51
+ });
52
+
53
+ it('navigates to previous month', async () => {
54
+ render(<DatePicker value={new Date(2026, 3, 1)} />);
55
+ await userEvent.click(screen.getByRole('button', { name: 'Apr 1, 2026' }));
56
+ const monthYear = screen.getByText('April 2026');
57
+ expect(monthYear).toBeInTheDocument();
58
+ await userEvent.click(screen.getByRole('button', { name: 'Previous month' }));
59
+ expect(screen.getByText('March 2026')).toBeInTheDocument();
60
+ });
61
+ });
@@ -0,0 +1,271 @@
1
+ import classnames from 'classnames';
2
+ import { useRef, useState } from 'react';
3
+
4
+ import { useClickOutside } from '../../hooks/useClickOutside.ts';
5
+ import { useComponentId } from '../../hooks/useComponentId.ts';
6
+ import styles from './DatePicker.module.css';
7
+ import type { DatePickerProps } from './DatePicker.types.ts';
8
+
9
+ const DAYS_OF_WEEK = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
10
+ const MONTHS = [
11
+ 'January',
12
+ 'February',
13
+ 'March',
14
+ 'April',
15
+ 'May',
16
+ 'June',
17
+ 'July',
18
+ 'August',
19
+ 'September',
20
+ 'October',
21
+ 'November',
22
+ 'December',
23
+ ];
24
+
25
+ const formatDate = (date: Date): string =>
26
+ date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
27
+
28
+ const isSameDay = (a: Date, b: Date): boolean =>
29
+ a.getFullYear() === b.getFullYear() &&
30
+ a.getMonth() === b.getMonth() &&
31
+ a.getDate() === b.getDate();
32
+
33
+ const isToday = (date: Date): boolean => isSameDay(date, new Date());
34
+
35
+ export const DatePicker = ({
36
+ value,
37
+ onChange,
38
+ placeholder = 'Select date',
39
+ disabled = false,
40
+ min,
41
+ max,
42
+ id,
43
+ label,
44
+ className,
45
+ }: DatePickerProps) => {
46
+ const today = new Date();
47
+ const [isOpen, setIsOpen] = useState(false);
48
+ const [viewYear, setViewYear] = useState((value ?? today).getFullYear());
49
+ const [viewMonth, setViewMonth] = useState((value ?? today).getMonth());
50
+ const rootRef = useRef<HTMLDivElement>(null);
51
+ const componentId = useComponentId('datepicker', id);
52
+ const inputId = `${componentId}-input`;
53
+ const calendarId = `${componentId}-calendar`;
54
+
55
+ useClickOutside(rootRef, () => setIsOpen(false), isOpen);
56
+
57
+ const open = () => {
58
+ if (disabled) {
59
+ return;
60
+ }
61
+ if (value) {
62
+ setViewYear(value.getFullYear());
63
+ setViewMonth(value.getMonth());
64
+ }
65
+ setIsOpen(true);
66
+ };
67
+
68
+ const selectDate = (date: Date) => {
69
+ onChange?.(date);
70
+ setIsOpen(false);
71
+ };
72
+
73
+ const prevMonth = () => {
74
+ if (viewMonth === 0) {
75
+ setViewMonth(11);
76
+ setViewYear((y) => y - 1);
77
+ } else {
78
+ setViewMonth((m) => m - 1);
79
+ }
80
+ };
81
+
82
+ const nextMonth = () => {
83
+ if (viewMonth === 11) {
84
+ setViewMonth(0);
85
+ setViewYear((y) => y + 1);
86
+ } else {
87
+ setViewMonth((m) => m + 1);
88
+ }
89
+ };
90
+
91
+ const isDisabledDate = (date: Date): boolean => {
92
+ const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
93
+ if (min) {
94
+ const minOnly = new Date(min.getFullYear(), min.getMonth(), min.getDate());
95
+ if (dateOnly < minOnly) {
96
+ return true;
97
+ }
98
+ }
99
+ if (max) {
100
+ const maxOnly = new Date(max.getFullYear(), max.getMonth(), max.getDate());
101
+ if (dateOnly > maxOnly) {
102
+ return true;
103
+ }
104
+ }
105
+
106
+ return false;
107
+ };
108
+
109
+ const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
110
+ const firstDayOfMonth = new Date(viewYear, viewMonth, 1).getDay();
111
+
112
+ const cells: Array<Date | null> = [
113
+ ...Array<null>(firstDayOfMonth).fill(null),
114
+ ...Array.from({ length: daysInMonth }, (_, i) => new Date(viewYear, viewMonth, i + 1)),
115
+ ];
116
+ while (cells.length % 7 !== 0) {
117
+ cells.push(null);
118
+ }
119
+
120
+ return (
121
+ <div ref={rootRef} className={classnames(styles.root, className)}>
122
+ {label && (
123
+ <label htmlFor={inputId} className={styles.label}>
124
+ {label}
125
+ </label>
126
+ )}
127
+ <button
128
+ type="button"
129
+ id={inputId}
130
+ className={classnames(styles.trigger, disabled && styles.disabled)}
131
+ onClick={open}
132
+ aria-haspopup="dialog"
133
+ aria-expanded={isOpen}
134
+ aria-controls={calendarId}
135
+ disabled={disabled}
136
+ >
137
+ <span className={classnames(styles.triggerText, !value && styles.placeholder)}>
138
+ {value ? formatDate(value) : placeholder}
139
+ </span>
140
+ <svg
141
+ width="14"
142
+ height="14"
143
+ viewBox="0 0 14 14"
144
+ fill="none"
145
+ aria-hidden="true"
146
+ className={styles.calendarIcon}
147
+ >
148
+ <rect
149
+ x="1"
150
+ y="2"
151
+ width="12"
152
+ height="11"
153
+ rx="1.5"
154
+ stroke="currentColor"
155
+ strokeWidth="1.5"
156
+ />
157
+ <path d="M1 5.5H13" stroke="currentColor" strokeWidth="1.5" />
158
+ <path d="M4.5 1V3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
159
+ <path d="M9.5 1V3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
160
+ </svg>
161
+ </button>
162
+
163
+ {isOpen && (
164
+ <div
165
+ id={calendarId}
166
+ role="dialog"
167
+ aria-label="Date picker calendar"
168
+ aria-modal="false"
169
+ className={styles.calendar}
170
+ >
171
+ <div className={styles.calendarHeader}>
172
+ <button
173
+ type="button"
174
+ className={styles.navButton}
175
+ onClick={prevMonth}
176
+ aria-label="Previous month"
177
+ >
178
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
179
+ <path
180
+ d="M6.5 2L3.5 5L6.5 8"
181
+ stroke="currentColor"
182
+ strokeWidth="1.5"
183
+ strokeLinecap="round"
184
+ strokeLinejoin="round"
185
+ />
186
+ </svg>
187
+ </button>
188
+ <span className={styles.monthYear}>
189
+ {MONTHS[viewMonth]} {viewYear}
190
+ </span>
191
+ <button
192
+ type="button"
193
+ className={styles.navButton}
194
+ onClick={nextMonth}
195
+ aria-label="Next month"
196
+ >
197
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
198
+ <path
199
+ d="M3.5 2L6.5 5L3.5 8"
200
+ stroke="currentColor"
201
+ strokeWidth="1.5"
202
+ strokeLinecap="round"
203
+ strokeLinejoin="round"
204
+ />
205
+ </svg>
206
+ </button>
207
+ </div>
208
+
209
+ <div className={styles.weekdays} aria-hidden="true">
210
+ {DAYS_OF_WEEK.map((d) => (
211
+ <span key={d} className={styles.weekday}>
212
+ {d}
213
+ </span>
214
+ ))}
215
+ </div>
216
+
217
+ <div className={styles.days}>
218
+ {cells.map((date, i) => {
219
+ if (!date) {
220
+ return <span key={`empty-${i}`} className={styles.dayEmpty} aria-hidden="true" />;
221
+ }
222
+ const selected = value ? isSameDay(date, value) : false;
223
+ const todayFlag = isToday(date);
224
+ const disabledFlag = isDisabledDate(date);
225
+
226
+ return (
227
+ <button
228
+ key={date.toISOString()}
229
+ type="button"
230
+ className={classnames(
231
+ styles.day,
232
+ selected && styles.daySelected,
233
+ todayFlag && !selected && styles.dayToday,
234
+ disabledFlag && styles.dayDisabled
235
+ )}
236
+ onClick={() => !disabledFlag && selectDate(date)}
237
+ disabled={disabledFlag}
238
+ aria-selected={selected}
239
+ aria-label={date.toLocaleDateString('en-US', {
240
+ weekday: 'long',
241
+ year: 'numeric',
242
+ month: 'long',
243
+ day: 'numeric',
244
+ })}
245
+ tabIndex={disabledFlag ? -1 : 0}
246
+ >
247
+ {date.getDate()}
248
+ </button>
249
+ );
250
+ })}
251
+ </div>
252
+
253
+ {value && (
254
+ <div className={styles.calendarFooter}>
255
+ <button
256
+ type="button"
257
+ className={styles.clearButton}
258
+ onClick={() => {
259
+ onChange?.(null);
260
+ setIsOpen(false);
261
+ }}
262
+ >
263
+ Clear
264
+ </button>
265
+ </div>
266
+ )}
267
+ </div>
268
+ )}
269
+ </div>
270
+ );
271
+ };
@@ -0,0 +1,11 @@
1
+ export interface DatePickerProps {
2
+ value?: Date | null;
3
+ onChange?: (date: Date | null) => void;
4
+ placeholder?: string;
5
+ disabled?: boolean;
6
+ min?: Date;
7
+ max?: Date;
8
+ id?: string;
9
+ label?: string;
10
+ className?: string;
11
+ }