@ponti-studios/ui 2.0.1 → 3.0.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 (319) hide show
  1. package/README.md +36 -3
  2. package/dist/auth/index.d.ts +3 -0
  3. package/dist/auth/index.js +3 -0
  4. package/dist/components/data-display/accordion.d.ts +10 -0
  5. package/dist/components/data-display/accordion.js +53 -0
  6. package/{src/components/data-display/index.ts → dist/components/data-display/index.d.ts} +1 -10
  7. package/dist/components/data-display/index.js +8 -0
  8. package/dist/components/data-display/metric-card.d.ts +9 -0
  9. package/dist/components/data-display/metric-card.js +25 -0
  10. package/dist/components/data-display/sort-controls.d.ts +13 -0
  11. package/dist/components/data-display/sort-controls.js +57 -0
  12. package/dist/components/data-display/sort-row.d.ts +10 -0
  13. package/dist/components/data-display/sort-row.js +52 -0
  14. package/dist/components/data-display/table.d.ts +10 -0
  15. package/dist/components/data-display/table.js +57 -0
  16. package/dist/components/feedback/alert.d.ts +9 -0
  17. package/dist/components/feedback/alert.js +38 -0
  18. package/dist/components/feedback/empty-state.d.ts +18 -0
  19. package/dist/components/feedback/empty-state.js +36 -0
  20. package/{src/components/feedback/index.ts → dist/components/feedback/index.d.ts} +2 -0
  21. package/dist/components/feedback/index.js +7 -0
  22. package/dist/components/feedback/progress.d.ts +6 -0
  23. package/dist/components/feedback/progress.js +25 -0
  24. package/dist/components/feedback/skeleton.d.ts +2 -0
  25. package/dist/components/feedback/skeleton.js +11 -0
  26. package/dist/components/feedback/spinner.d.ts +14 -0
  27. package/dist/components/feedback/spinner.js +27 -0
  28. package/dist/components/feedback/update-guard.d.ts +13 -0
  29. package/dist/components/feedback/update-guard.js +38 -0
  30. package/dist/components/filters/active-filters-bar.d.ts +14 -0
  31. package/dist/components/filters/active-filters-bar.js +44 -0
  32. package/dist/components/filters/filter-chip.d.ts +11 -0
  33. package/dist/components/filters/filter-chip.js +33 -0
  34. package/dist/components/filters/filter-select.d.ts +17 -0
  35. package/dist/components/filters/filter-select.js +46 -0
  36. package/dist/components/filters/index.js +4 -0
  37. package/dist/components/forms/checkbox.d.ts +4 -0
  38. package/dist/components/forms/checkbox.js +24 -0
  39. package/dist/components/forms/date-month-select.d.ts +11 -0
  40. package/dist/components/forms/date-month-select.js +47 -0
  41. package/dist/components/forms/date-picker.d.ts +60 -0
  42. package/dist/components/forms/date-picker.js +193 -0
  43. package/dist/components/forms/date-utils.d.ts +14 -0
  44. package/dist/components/forms/date-utils.js +18 -0
  45. package/dist/components/forms/drop-zone.d.ts +42 -0
  46. package/dist/components/forms/drop-zone.js +184 -0
  47. package/{src/components/forms/index.ts → dist/components/forms/index.d.ts} +3 -9
  48. package/dist/components/forms/index.js +17 -0
  49. package/dist/components/forms/input.d.ts +3 -0
  50. package/dist/components/forms/input.js +15 -0
  51. package/dist/components/forms/label.d.ts +3 -0
  52. package/dist/components/forms/label.js +14 -0
  53. package/dist/components/forms/passkey-management.d.ts +14 -0
  54. package/dist/components/forms/passkey-management.js +132 -0
  55. package/dist/components/forms/radio-group.d.ts +6 -0
  56. package/dist/components/forms/radio-group.js +28 -0
  57. package/dist/components/forms/select.d.ts +18 -0
  58. package/dist/components/forms/select.js +103 -0
  59. package/dist/components/forms/slider.d.ts +9 -0
  60. package/dist/components/forms/slider.js +25 -0
  61. package/dist/components/forms/stepper.d.ts +15 -0
  62. package/dist/components/forms/stepper.js +41 -0
  63. package/dist/components/forms/switch.d.ts +6 -0
  64. package/dist/components/forms/switch.js +21 -0
  65. package/dist/components/forms/text-field.d.ts +15 -0
  66. package/dist/components/forms/text-field.js +46 -0
  67. package/dist/components/forms/text-field.types.d.ts +8 -0
  68. package/dist/components/forms/textarea.d.ts +3 -0
  69. package/dist/components/forms/textarea.js +14 -0
  70. package/dist/components/layout/index.js +5 -0
  71. package/dist/components/layout/particle-background.d.ts +19 -0
  72. package/dist/components/layout/particle-background.js +171 -0
  73. package/dist/components/layout/scroll-area.d.ts +9 -0
  74. package/dist/components/layout/scroll-area.js +35 -0
  75. package/dist/components/layout/section-intro.d.ts +12 -0
  76. package/dist/components/layout/section-intro.js +28 -0
  77. package/dist/components/layout/surface-panel.d.ts +7 -0
  78. package/dist/components/layout/surface-panel.js +11 -0
  79. package/dist/components/navigation/app-navigation.d.ts +67 -0
  80. package/dist/components/navigation/app-navigation.js +106 -0
  81. package/{src/components/navigation/index.ts → dist/components/navigation/index.d.ts} +1 -7
  82. package/dist/components/navigation/index.js +4 -0
  83. package/dist/components/navigation/pagination-controls.d.ts +7 -0
  84. package/dist/components/navigation/pagination-controls.js +50 -0
  85. package/dist/components/navigation/tabs.d.ts +7 -0
  86. package/dist/components/navigation/tabs.js +35 -0
  87. package/dist/components/overlays/bottom-sheet.d.ts +20 -0
  88. package/dist/components/overlays/bottom-sheet.js +101 -0
  89. package/dist/components/overlays/command.d.ts +29 -0
  90. package/dist/components/overlays/command.js +123 -0
  91. package/dist/components/overlays/dialog.d.ts +30 -0
  92. package/dist/components/overlays/dialog.js +105 -0
  93. package/dist/components/overlays/dropdown-menu.d.ts +29 -0
  94. package/dist/components/overlays/dropdown-menu.js +128 -0
  95. package/dist/components/overlays/index.d.ts +7 -0
  96. package/dist/components/overlays/index.js +8 -0
  97. package/dist/components/overlays/popover.d.ts +14 -0
  98. package/dist/components/overlays/popover.js +56 -0
  99. package/dist/components/overlays/sheet.d.ts +27 -0
  100. package/dist/components/overlays/sheet.js +81 -0
  101. package/dist/components/overlays/tooltip.d.ts +10 -0
  102. package/dist/components/overlays/tooltip.js +23 -0
  103. package/dist/components/primitives/avatar.d.ts +14 -0
  104. package/dist/components/primitives/avatar.js +72 -0
  105. package/dist/components/primitives/badge.d.ts +7 -0
  106. package/dist/components/primitives/badge.js +27 -0
  107. package/dist/components/primitives/button.d.ts +13 -0
  108. package/dist/components/primitives/button.js +113 -0
  109. package/{src/components/primitives/button.types.ts → dist/components/primitives/button.types.d.ts} +4 -6
  110. package/dist/components/primitives/card.d.ts +9 -0
  111. package/dist/components/primitives/card.js +56 -0
  112. package/{src/components/primitives/index.ts → dist/components/primitives/index.d.ts} +1 -9
  113. package/dist/components/primitives/index.js +7 -0
  114. package/dist/components/primitives/segmented-control.d.ts +20 -0
  115. package/dist/components/primitives/segmented-control.js +27 -0
  116. package/dist/components/primitives/separator.d.ts +4 -0
  117. package/dist/components/primitives/separator.js +16 -0
  118. package/dist/constants/chart-colors.d.ts +33 -0
  119. package/dist/constants/chart-colors.js +31 -0
  120. package/dist/hooks/index.js +9 -0
  121. package/{src/hooks/sort.types.ts → dist/hooks/sort.types.d.ts} +2 -4
  122. package/dist/hooks/use-api-client.d.ts +30 -0
  123. package/dist/hooks/use-api-client.js +106 -0
  124. package/dist/hooks/use-debounced-value.d.ts +1 -0
  125. package/dist/hooks/use-debounced-value.js +12 -0
  126. package/dist/hooks/use-derived-filter-state.d.ts +20 -0
  127. package/dist/hooks/use-derived-filter-state.js +39 -0
  128. package/dist/hooks/use-filter-state.d.ts +12 -0
  129. package/dist/hooks/use-filter-state.js +37 -0
  130. package/dist/hooks/use-media-query.d.ts +6 -0
  131. package/dist/hooks/use-media-query.js +27 -0
  132. package/dist/hooks/use-mobile.d.ts +1 -0
  133. package/dist/hooks/use-mobile.js +14 -0
  134. package/dist/hooks/use-sort.d.ts +11 -0
  135. package/dist/hooks/use-sort.js +14 -0
  136. package/{src/index.ts → dist/index.d.ts} +4 -32
  137. package/dist/index.js +15 -0
  138. package/dist/integrations/service-worker.d.ts +2 -0
  139. package/dist/integrations/service-worker.js +2 -0
  140. package/dist/lib/auth.d.ts +2 -0
  141. package/dist/lib/auth.js +7 -0
  142. package/dist/lib/clipboard.d.ts +1 -0
  143. package/dist/lib/clipboard.js +10 -0
  144. package/dist/lib/index.js +4 -0
  145. package/{src/lib/slot.tsx → dist/lib/slot.d.ts} +2 -15
  146. package/dist/lib/slot.js +14 -0
  147. package/dist/lib/utils.d.ts +2 -0
  148. package/dist/lib/utils.js +8 -0
  149. package/dist/native/components.d.ts +41 -0
  150. package/dist/native/components.js +135 -0
  151. package/dist/native/index.js +3 -0
  152. package/dist/native/theme.d.ts +19 -0
  153. package/dist/native/theme.js +58 -0
  154. package/dist/styles/tokens/css-vars.d.ts +7 -0
  155. package/dist/styles/tokens/css-vars.js +5 -0
  156. package/dist/styles/tokens/generated/dark.colors.generated.d.ts +40 -0
  157. package/dist/styles/tokens/generated/dark.colors.generated.js +39 -0
  158. package/dist/styles/tokens/generated/foundations.generated.d.ts +323 -0
  159. package/dist/styles/tokens/generated/foundations.generated.js +453 -0
  160. package/dist/styles/tokens/generated/light.colors.generated.d.ts +40 -0
  161. package/dist/styles/tokens/generated/light.colors.generated.js +39 -0
  162. package/dist/styles/tokens/index.d.ts +317 -0
  163. package/dist/styles/tokens/index.js +14 -0
  164. package/dist/styles.css +1 -1
  165. package/dist/translations/en.d.ts +34 -0
  166. package/dist/translations/index.d.ts +39 -0
  167. package/dist/translations/ui-provider.d.ts +9 -0
  168. package/dist/translations/ui-provider.js +16 -0
  169. package/dist/types/upload.d.ts +36 -0
  170. package/package.json +50 -33
  171. package/src/components/data-display/accordion.stories.tsx +0 -57
  172. package/src/components/data-display/accordion.tsx +0 -86
  173. package/src/components/data-display/metric-card.stories.tsx +0 -48
  174. package/src/components/data-display/metric-card.tsx +0 -20
  175. package/src/components/data-display/sort-controls.stories.tsx +0 -45
  176. package/src/components/data-display/sort-controls.tsx +0 -96
  177. package/src/components/data-display/sort-row.tsx +0 -88
  178. package/src/components/data-display/table.stories.tsx +0 -75
  179. package/src/components/data-display/table.tsx +0 -80
  180. package/src/components/feedback/alert.stories.tsx +0 -51
  181. package/src/components/feedback/alert.tsx +0 -64
  182. package/src/components/feedback/empty-state.tsx +0 -76
  183. package/src/components/feedback/progress.stories.tsx +0 -54
  184. package/src/components/feedback/progress.tsx +0 -41
  185. package/src/components/feedback/skeleton.stories.tsx +0 -36
  186. package/src/components/feedback/skeleton.tsx +0 -7
  187. package/src/components/feedback/spinner.stories.tsx +0 -52
  188. package/src/components/feedback/spinner.tsx +0 -47
  189. package/src/components/feedback/update-guard.stories.tsx +0 -28
  190. package/src/components/feedback/update-guard.tsx +0 -57
  191. package/src/components/filters/active-filters-bar.stories.tsx +0 -114
  192. package/src/components/filters/active-filters-bar.tsx +0 -61
  193. package/src/components/filters/filter-chip.tsx +0 -57
  194. package/src/components/filters/filter-select.stories.tsx +0 -71
  195. package/src/components/filters/filter-select.tsx +0 -68
  196. package/src/components/forms/checkbox.stories.tsx +0 -35
  197. package/src/components/forms/checkbox.tsx +0 -38
  198. package/src/components/forms/date-month-select.stories.tsx +0 -26
  199. package/src/components/forms/date-month-select.tsx +0 -79
  200. package/src/components/forms/date-picker.stories.tsx +0 -188
  201. package/src/components/forms/date-picker.tsx +0 -303
  202. package/src/components/forms/date-utils.ts +0 -37
  203. package/src/components/forms/drop-zone.stories.tsx +0 -140
  204. package/src/components/forms/drop-zone.tsx +0 -298
  205. package/src/components/forms/input.stories.tsx +0 -47
  206. package/src/components/forms/input.tsx +0 -24
  207. package/src/components/forms/label.stories.tsx +0 -16
  208. package/src/components/forms/label.tsx +0 -25
  209. package/src/components/forms/passkey-management.stories.tsx +0 -85
  210. package/src/components/forms/passkey-management.tsx +0 -168
  211. package/src/components/forms/radio-group.stories.tsx +0 -46
  212. package/src/components/forms/radio-group.tsx +0 -44
  213. package/src/components/forms/select.stories.tsx +0 -207
  214. package/src/components/forms/select.tsx +0 -182
  215. package/src/components/forms/slider.stories.tsx +0 -70
  216. package/src/components/forms/slider.tsx +0 -50
  217. package/src/components/forms/stepper.stories.tsx +0 -111
  218. package/src/components/forms/stepper.tsx +0 -66
  219. package/src/components/forms/switch.stories.tsx +0 -144
  220. package/src/components/forms/switch.tsx +0 -39
  221. package/src/components/forms/text-field.stories.tsx +0 -87
  222. package/src/components/forms/text-field.tsx +0 -75
  223. package/src/components/forms/text-field.types.ts +0 -9
  224. package/src/components/forms/textarea.stories.tsx +0 -150
  225. package/src/components/forms/textarea.tsx +0 -22
  226. package/src/components/layout/particle-background.stories.tsx +0 -38
  227. package/src/components/layout/particle-background.tsx +0 -378
  228. package/src/components/layout/scroll-area.stories.tsx +0 -91
  229. package/src/components/layout/scroll-area.tsx +0 -79
  230. package/src/components/layout/section-intro.stories.tsx +0 -29
  231. package/src/components/layout/section-intro.tsx +0 -31
  232. package/src/components/layout/surface-panel.stories.tsx +0 -32
  233. package/src/components/layout/surface-panel.tsx +0 -23
  234. package/src/components/navigation/app-navigation.stories.tsx +0 -217
  235. package/src/components/navigation/app-navigation.tsx +0 -234
  236. package/src/components/navigation/pagination-controls.stories.tsx +0 -41
  237. package/src/components/navigation/pagination-controls.tsx +0 -46
  238. package/src/components/navigation/tabs.stories.tsx +0 -48
  239. package/src/components/navigation/tabs.tsx +0 -52
  240. package/src/components/overlays/bottom-sheet.stories.tsx +0 -52
  241. package/src/components/overlays/bottom-sheet.tsx +0 -179
  242. package/src/components/overlays/command.stories.tsx +0 -65
  243. package/src/components/overlays/command.tsx +0 -227
  244. package/src/components/overlays/dialog.stories.tsx +0 -148
  245. package/src/components/overlays/dialog.tsx +0 -203
  246. package/src/components/overlays/dropdown-menu.stories.tsx +0 -74
  247. package/src/components/overlays/dropdown-menu.tsx +0 -236
  248. package/src/components/overlays/index.ts +0 -56
  249. package/src/components/overlays/popover.stories.tsx +0 -51
  250. package/src/components/overlays/popover.tsx +0 -78
  251. package/src/components/overlays/sheet.stories.tsx +0 -68
  252. package/src/components/overlays/sheet.tsx +0 -155
  253. package/src/components/overlays/tooltip.stories.tsx +0 -29
  254. package/src/components/overlays/tooltip.tsx +0 -41
  255. package/src/components/primitives/avatar-placeholder.svg +0 -13
  256. package/src/components/primitives/avatar.stories.tsx +0 -72
  257. package/src/components/primitives/avatar.tsx +0 -136
  258. package/src/components/primitives/badge.stories.tsx +0 -43
  259. package/src/components/primitives/badge.tsx +0 -43
  260. package/src/components/primitives/button.stories.tsx +0 -113
  261. package/src/components/primitives/button.tsx +0 -131
  262. package/src/components/primitives/card.stories.tsx +0 -59
  263. package/src/components/primitives/card.tsx +0 -66
  264. package/src/components/primitives/segmented-control.stories.tsx +0 -54
  265. package/src/components/primitives/segmented-control.tsx +0 -73
  266. package/src/components/primitives/separator.stories.tsx +0 -34
  267. package/src/components/primitives/separator.tsx +0 -27
  268. package/src/constants/chart-colors.ts +0 -39
  269. package/src/env.d.ts +0 -9
  270. package/src/hooks/use-api-client.ts +0 -122
  271. package/src/hooks/use-debounced-value.ts +0 -12
  272. package/src/hooks/use-derived-filter-state.ts +0 -70
  273. package/src/hooks/use-filter-state.ts +0 -66
  274. package/src/hooks/use-media-query.ts +0 -69
  275. package/src/hooks/use-mobile.tsx +0 -19
  276. package/src/hooks/use-sort.ts +0 -20
  277. package/src/lib/auth.ts +0 -5
  278. package/src/lib/clipboard.ts +0 -8
  279. package/src/lib/utils.ts +0 -6
  280. package/src/native/components.tsx +0 -198
  281. package/src/native/theme.tsx +0 -69
  282. package/src/storybook/controls.ts +0 -117
  283. package/src/storybook/options.ts +0 -52
  284. package/src/styles/build.ts +0 -1
  285. package/src/styles/index.css +0 -423
  286. package/src/styles/nokia.css +0 -36
  287. package/src/styles/package.css +0 -3
  288. package/src/styles/tokens/chart-colors.stories.tsx +0 -189
  289. package/src/styles/tokens/color-systems.stories.tsx +0 -190
  290. package/src/styles/tokens/css-vars.ts +0 -15
  291. package/src/styles/tokens/generated/dark-class.css +0 -39
  292. package/src/styles/tokens/generated/dark.colors.generated.ts +0 -39
  293. package/src/styles/tokens/generated/dark.css +0 -39
  294. package/src/styles/tokens/generated/foundations.css +0 -149
  295. package/src/styles/tokens/generated/foundations.generated.ts +0 -454
  296. package/src/styles/tokens/generated/light.colors.generated.ts +0 -39
  297. package/src/styles/tokens/generated/light.css +0 -39
  298. package/src/styles/tokens/index.ts +0 -70
  299. package/src/styles/tokens/layout.stories.tsx +0 -50
  300. package/src/styles/tokens/scripts/check-foundations.mjs +0 -126
  301. package/src/styles/tokens/scripts/check-source.mjs +0 -53
  302. package/src/styles/tokens/source/dark.tokens.json +0 -138
  303. package/src/styles/tokens/source/foundations.tokens.json +0 -344
  304. package/src/styles/tokens/source/light.tokens.json +0 -138
  305. package/src/styles/tokens/style-dictionary.dark-class.json +0 -16
  306. package/src/styles/tokens/style-dictionary.dark-storybook.json +0 -16
  307. package/src/styles/tokens/style-dictionary.dark.json +0 -21
  308. package/src/styles/tokens/style-dictionary.foundations.json +0 -21
  309. package/src/styles/tokens/style-dictionary.light-storybook.json +0 -16
  310. package/src/styles/tokens/style-dictionary.light.json +0 -21
  311. package/src/styles/tokens/typography.stories.tsx +0 -157
  312. package/src/translations/en.ts +0 -34
  313. package/src/translations/index.ts +0 -32
  314. package/src/types/upload.ts +0 -42
  315. /package/{src/components/filters/index.ts → dist/components/filters/index.d.ts} +0 -0
  316. /package/{src/components/layout/index.ts → dist/components/layout/index.d.ts} +0 -0
  317. /package/{src/hooks/index.ts → dist/hooks/index.d.ts} +0 -0
  318. /package/{src/lib/index.ts → dist/lib/index.d.ts} +0 -0
  319. /package/{src/native/index.ts → dist/native/index.d.ts} +0 -0
@@ -1,188 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { expect, userEvent } from "storybook/test";
3
- import { useState } from "react";
4
-
5
- import { DatePicker } from "./date-picker";
6
-
7
- const meta = {
8
- title: "Forms/DatePicker",
9
- component: DatePicker,
10
- tags: ["autodocs"],
11
- } satisfies Meta<typeof DatePicker>;
12
-
13
- export default meta;
14
- type Story = StoryObj<typeof meta>;
15
-
16
- function dateValue(canvasElement: HTMLElement, name: string): string {
17
- const input = canvasElement.querySelector<HTMLInputElement>(`input[name="${name}"]`);
18
- return input?.value ?? "";
19
- }
20
-
21
- // The native date input owns the picker UI, so stories assert on the input's
22
- // "YYYY-MM-DD" value — the same string a form would submit.
23
- export const Default: Story = {
24
- args: {
25
- name: "startDate",
26
- defaultValue: "2020-01-15",
27
- label: "Start date",
28
- },
29
- play: async ({ canvasElement }) => {
30
- const input = canvasElement.querySelector<HTMLInputElement>('input[name="startDate"]');
31
- await expect(input?.value).toBe("2020-01-15");
32
- },
33
- };
34
-
35
- export const WithDateObjectDefault: Story = {
36
- args: {
37
- name: "endDate",
38
- defaultValue: new Date(2020, 0, 15),
39
- label: "End date",
40
- },
41
- play: async ({ canvasElement }) => {
42
- const input = canvasElement.querySelector<HTMLInputElement>('input[name="endDate"]');
43
- await expect(input?.value).toBe("2020-01-15");
44
- },
45
- };
46
-
47
- export const Controlled: Story = {
48
- render: () => {
49
- const [value, setValue] = useState<string | null>("2026-08-20");
50
- return (
51
- <div className="flex flex-col gap-2">
52
- <DatePicker
53
- mode="single"
54
- label="Due date"
55
- value={value}
56
- onValueChange={setValue}
57
- helpText="Controlled — pick a new date"
58
- />
59
- <p className="text-sm">value: {value ?? "(none)"}</p>
60
- </div>
61
- );
62
- },
63
- play: async ({ canvasElement }) => {
64
- await expect(canvasElement.textContent).toContain("2026-08-20");
65
- },
66
- };
67
-
68
- export const Disabled: Story = {
69
- args: {
70
- name: "startDate",
71
- defaultValue: "2020-01-15",
72
- label: "Locked date",
73
- disabled: true,
74
- },
75
- play: async ({ canvasElement }) => {
76
- const input = canvasElement.querySelector<HTMLInputElement>('input[name="startDate"]');
77
- await expect(input?.disabled).toBe(true);
78
- await expect(input?.value).toBe("2020-01-15");
79
- },
80
- };
81
-
82
- export const WithHelpText: Story = {
83
- args: {
84
- name: "startDate",
85
- label: "Start date",
86
- helpText: "Format is YYYY-MM-DD.",
87
- },
88
- };
89
-
90
- export const WithError: Story = {
91
- args: {
92
- name: "startDate",
93
- label: "Start date",
94
- error: "A start date is required.",
95
- },
96
- play: async ({ canvasElement }) => {
97
- const input = canvasElement.querySelector<HTMLInputElement>('input[name="startDate"]');
98
- await expect(input?.getAttribute("aria-invalid")).toBe("true");
99
- await expect(canvasElement.textContent).toContain("A start date is required.");
100
- },
101
- };
102
-
103
- export const Range: Story = {
104
- args: {
105
- mode: "range",
106
- startName: "startDate",
107
- endName: "endDate",
108
- defaultValue: { from: "2020-01-15", to: "2020-06-30" },
109
- label: "Active period",
110
- },
111
- play: async ({ canvasElement }) => {
112
- await expect(dateValue(canvasElement, "startDate")).toBe("2020-01-15");
113
- await expect(dateValue(canvasElement, "endDate")).toBe("2020-06-30");
114
- },
115
- };
116
-
117
- export const RangeEmpty: Story = {
118
- args: {
119
- mode: "range",
120
- startName: "startDate",
121
- endName: "endDate",
122
- label: "Active period",
123
- },
124
- play: async ({ canvasElement }) => {
125
- await expect(dateValue(canvasElement, "startDate")).toBe("");
126
- await expect(dateValue(canvasElement, "endDate")).toBe("");
127
- },
128
- };
129
-
130
- export const RangeControlled: Story = {
131
- render: () => {
132
- const [range, setRange] = useState<{ from: string | null; to: string | null }>({
133
- from: "2026-08-01",
134
- to: "2026-08-15",
135
- });
136
- return (
137
- <div className="flex flex-col gap-2">
138
- <DatePicker
139
- mode="range"
140
- label="Stay dates"
141
- startName="stayStart"
142
- endName="stayEnd"
143
- value={range}
144
- onValueChange={setRange}
145
- fromLabel="Check-in"
146
- toLabel="Check-out"
147
- />
148
- <p className="text-sm">
149
- from: {range.from ?? "(none)"} · to: {range.to ?? "(none)"}
150
- </p>
151
- </div>
152
- );
153
- },
154
- play: async ({ canvasElement }) => {
155
- await expect(canvasElement.textContent).toContain("from: 2026-08-01");
156
- await expect(canvasElement.textContent).toContain("to: 2026-08-15");
157
-
158
- const end = canvasElement.querySelector<HTMLInputElement>('input[name="stayEnd"]');
159
- await userEvent.clear(end!);
160
- await userEvent.type(end!, "0002-08-01");
161
- await expect(end?.value).toBe("0002-08-01");
162
- await expect(canvasElement.textContent).toContain("to: 0002-08-01");
163
- },
164
- };
165
-
166
- // Two independent fields cross-wired via min/max: the end field can't go
167
- // before the start, and the start field can't go after the end.
168
- export const RangeMinMax: Story = {
169
- args: {
170
- mode: "range",
171
- startName: "startDate",
172
- endName: "endDate",
173
- min: "2026-01-01",
174
- max: "2026-12-31",
175
- label: "Active period",
176
- },
177
- play: async ({ canvasElement }) => {
178
- const start = canvasElement.querySelector<HTMLInputElement>('input[name="startDate"]');
179
- const end = canvasElement.querySelector<HTMLInputElement>('input[name="endDate"]');
180
- await expect(start?.min).toBe("2026-01-01");
181
- await expect(end?.max).toBe("2026-12-31");
182
-
183
- await userEvent.type(start!, "2026-03-15");
184
- await expect(start?.value).toBe("2026-03-15");
185
- // End field's floor is now the chosen start date.
186
- await expect(end?.min).toBe("2026-03-15");
187
- },
188
- };
@@ -1,303 +0,0 @@
1
- import { CalendarIcon } from "lucide-react";
2
- import { useId, useRef, useState } from "react";
3
- import type { ChangeEvent, RefObject } from "react";
4
-
5
- import { cn } from "../../lib/utils";
6
- import { formatDateInput } from "./date-utils";
7
-
8
- export interface DateRangeValue {
9
- from?: Date | string | null;
10
- to?: Date | string | null;
11
- }
12
-
13
- interface DatePickerBaseProps {
14
- label?: string;
15
- helpText?: string;
16
- error?: string;
17
- id?: string;
18
- disabled?: boolean;
19
- required?: boolean;
20
- className?: string;
21
- /** Earliest selectable date, "YYYY-MM-DD". */
22
- min?: string;
23
- /** Latest selectable date, "YYYY-MM-DD". */
24
- max?: string;
25
- }
26
-
27
- export interface SingleDatePickerProps extends DatePickerBaseProps {
28
- mode?: "single";
29
- /** Form field name — the native input submits "YYYY-MM-DD". */
30
- name?: string;
31
- value?: Date | string | null;
32
- defaultValue?: Date | string | null;
33
- onValueChange?: (value: string | null) => void;
34
- }
35
-
36
- export interface RangeDatePickerProps extends DatePickerBaseProps {
37
- mode: "range";
38
- /** Form field name for the range start — submits "YYYY-MM-DD". */
39
- startName?: string;
40
- /** Form field name for the range end — submits "YYYY-MM-DD". */
41
- endName?: string;
42
- value?: DateRangeValue;
43
- defaultValue?: DateRangeValue;
44
- onValueChange?: (value: { from: string | null; to: string | null }) => void;
45
- fromLabel?: string;
46
- toLabel?: string;
47
- }
48
-
49
- export type DatePickerProps = SingleDatePickerProps | RangeDatePickerProps;
50
-
51
- const LABEL_CLASS = "text-foreground text-sm font-medium";
52
- const REQUIRED_CLASS = "after:text-destructive-text after:ml-0.5 after:content-['*']";
53
- const SUBLABEL_CLASS = "text-muted-foreground text-xs font-medium";
54
- const FIELD_CLASS =
55
- "bg-background text-foreground border-input h-10 w-full min-w-0 appearance-none border px-3 py-1 text-sm transition-colors " +
56
- "[&::-webkit-calendar-picker-indicator]:appearance-none [&::-webkit-calendar-picker-indicator]:opacity-0 " +
57
- "placeholder:text-muted-foreground " +
58
- "focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none " +
59
- "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " +
60
- "aria-invalid:ring-destructive/20 aria-invalid:border-destructive";
61
-
62
- /**
63
- * DatePickerIcon — the brand calendar glyph. The browser's own picker
64
- * indicator is hidden, so this is the visible affordance: clicking it calls
65
- * the input's `showPicker()` (falling back to focus, which opens the native
66
- * picker on iOS). It is not a tab stop because the input itself is the
67
- * focusable control and opens the picker on focus.
68
- */
69
- function DatePickerIcon({
70
- inputRef,
71
- disabled,
72
- ariaLabel,
73
- }: {
74
- inputRef: RefObject<HTMLInputElement | null>;
75
- disabled?: boolean;
76
- ariaLabel: string;
77
- }) {
78
- return (
79
- <button
80
- type="button"
81
- tabIndex={-1}
82
- aria-label={ariaLabel}
83
- disabled={disabled}
84
- onClick={() => {
85
- const input = inputRef.current;
86
- if (!input) return;
87
- try {
88
- input.showPicker?.();
89
- } catch {
90
- input.focus();
91
- }
92
- }}
93
- className="border-border bg-background text-foreground flex size-10 shrink-0 items-center justify-center rounded-l-xl border"
94
- >
95
- <CalendarIcon className="size-4" aria-hidden />
96
- </button>
97
- );
98
- }
99
-
100
- function SingleDatePicker({
101
- id,
102
- name,
103
- label,
104
- helpText,
105
- error,
106
- className,
107
- disabled,
108
- required,
109
- min,
110
- max,
111
- value: controlledValue,
112
- defaultValue,
113
- onValueChange,
114
- }: SingleDatePickerProps) {
115
- const generatedId = useId();
116
- const inputId = id ?? generatedId;
117
- const descriptionId = `${inputId}-description`;
118
- const errorId = `${inputId}-error`;
119
- const inputRef = useRef<HTMLInputElement>(null);
120
- const isControlled = controlledValue !== undefined;
121
- const [internalValue, setInternalValue] = useState<string>(() =>
122
- formatDateInput(isControlled ? controlledValue : defaultValue),
123
- );
124
- const inputValue = (isControlled ? formatDateInput(controlledValue) : internalValue) || "";
125
-
126
- function handleChange(event: ChangeEvent<HTMLInputElement>) {
127
- const next = event.target.value || null;
128
- if (!isControlled) setInternalValue(next ?? "");
129
- onValueChange?.(next);
130
- }
131
-
132
- return (
133
- <div className={cn("flex flex-col gap-1.5", className)}>
134
- {label ? (
135
- <label htmlFor={inputId} className={cn(LABEL_CLASS, required && REQUIRED_CLASS)}>
136
- {label}
137
- </label>
138
- ) : null}
139
- <div className="flex w-full items-center">
140
- <DatePickerIcon inputRef={inputRef} disabled={disabled} ariaLabel="Open date picker" />
141
- <input
142
- ref={inputRef}
143
- id={inputId}
144
- name={name}
145
- type="date"
146
- value={isControlled ? inputValue : undefined}
147
- defaultValue={isControlled ? undefined : inputValue}
148
- onChange={handleChange}
149
- disabled={disabled}
150
- required={required}
151
- min={min}
152
- max={max}
153
- aria-invalid={error ? true : undefined}
154
- aria-describedby={error ? errorId : helpText ? descriptionId : undefined}
155
- className={cn(FIELD_CLASS, "rounded-l-none rounded-r-xl border-l-0")}
156
- />
157
- </div>
158
- {error ? (
159
- <p id={errorId} role="alert" className="text-destructive-text text-sm">
160
- {error}
161
- </p>
162
- ) : helpText ? (
163
- <p id={descriptionId} className="text-muted-foreground text-sm">
164
- {helpText}
165
- </p>
166
- ) : null}
167
- </div>
168
- );
169
- }
170
-
171
- function RangeDatePicker({
172
- id,
173
- label,
174
- helpText,
175
- error,
176
- className,
177
- disabled,
178
- required,
179
- min,
180
- max,
181
- startName,
182
- endName,
183
- value: controlledValue,
184
- defaultValue,
185
- onValueChange,
186
- fromLabel = "From",
187
- toLabel = "To",
188
- }: RangeDatePickerProps) {
189
- const generatedId = useId();
190
- const startId = id ?? generatedId;
191
- const endId = `${startId}-to`;
192
- const descriptionId = `${startId}-description`;
193
- const errorId = `${startId}-error`;
194
- const startRef = useRef<HTMLInputElement>(null);
195
- const endRef = useRef<HTMLInputElement>(null);
196
- const isControlled = controlledValue !== undefined;
197
- const [internal, setInternal] = useState<{ from: string; to: string }>(() => {
198
- const initial = (isControlled ? controlledValue : defaultValue) as DateRangeValue | undefined;
199
- return { from: formatDateInput(initial?.from), to: formatDateInput(initial?.to) };
200
- });
201
- const from = (isControlled ? formatDateInput(controlledValue?.from) : internal.from) || "";
202
- const to = (isControlled ? formatDateInput(controlledValue?.to) : internal.to) || "";
203
-
204
- function handleChange(which: "from" | "to") {
205
- return (event: ChangeEvent<HTMLInputElement>) => {
206
- const nextValue = event.target.value || null;
207
- const next = which === "from" ? { from: nextValue, to } : { from, to: nextValue };
208
- if (!isControlled) setInternal({ from: next.from ?? "", to: next.to ?? "" });
209
- onValueChange?.({ from: next.from, to: next.to });
210
- };
211
- }
212
-
213
- return (
214
- <div className={cn("flex flex-col gap-1.5", className)}>
215
- {label ? <span className={cn(LABEL_CLASS, required && REQUIRED_CLASS)}>{label}</span> : null}
216
- <div className="grid gap-3 sm:grid-cols-2">
217
- <div className="flex min-w-0 flex-col gap-1">
218
- <label htmlFor={startId} className={SUBLABEL_CLASS}>
219
- {fromLabel}
220
- </label>
221
- <div className="flex w-full items-center">
222
- <DatePickerIcon inputRef={startRef} disabled={disabled} ariaLabel={fromLabel} />
223
- <input
224
- ref={startRef}
225
- id={startId}
226
- name={startName}
227
- type="date"
228
- value={isControlled ? from : undefined}
229
- defaultValue={isControlled ? undefined : from}
230
- onChange={handleChange("from")}
231
- disabled={disabled}
232
- required={required}
233
- min={min}
234
- max={to || max || undefined}
235
- aria-invalid={error ? true : undefined}
236
- aria-describedby={error ? errorId : helpText ? descriptionId : undefined}
237
- className={cn(FIELD_CLASS, "rounded-l-none rounded-r-xl border-l-0")}
238
- />
239
- </div>
240
- </div>
241
- <div className="flex min-w-0 flex-col gap-1">
242
- <label htmlFor={endId} className={SUBLABEL_CLASS}>
243
- {toLabel}
244
- </label>
245
- <div className="flex w-full items-center">
246
- <DatePickerIcon inputRef={endRef} disabled={disabled} ariaLabel={toLabel} />
247
- <input
248
- ref={endRef}
249
- id={endId}
250
- name={endName}
251
- type="date"
252
- value={isControlled ? to : undefined}
253
- defaultValue={isControlled ? undefined : to}
254
- onChange={handleChange("to")}
255
- disabled={disabled}
256
- required={required}
257
- min={from || min || undefined}
258
- max={max}
259
- aria-invalid={error ? true : undefined}
260
- aria-describedby={error ? errorId : helpText ? descriptionId : undefined}
261
- className={cn(FIELD_CLASS, "rounded-l-none rounded-r-xl border-l-0")}
262
- />
263
- </div>
264
- </div>
265
- </div>
266
- {error ? (
267
- <p id={errorId} role="alert" className="text-destructive-text text-sm">
268
- {error}
269
- </p>
270
- ) : helpText ? (
271
- <p id={descriptionId} className="text-muted-foreground text-sm">
272
- {helpText}
273
- </p>
274
- ) : null}
275
- </div>
276
- );
277
- }
278
-
279
- /**
280
- * DatePicker — native date inputs styled to the design system. The browser
281
- * owns the picker UI (calendar on desktop, wheel on iOS, etc.) so there is no
282
- * popover, calendar grid, or positioning layer in the bundle. Both a single
283
- * date (`name`) and a range (`startName` + `endName`) are supported; the
284
- * native inputs submit "YYYY-MM-DD" local date strings directly.
285
- *
286
- * @example
287
- * <DatePicker name="startDate" defaultValue="2020-01-15" label="Start date" />
288
- * <DatePicker
289
- * mode="range"
290
- * startName="startDate"
291
- * endName="endDate"
292
- * defaultValue={{ from: "2020-01-15", to: "2020-06-30" }}
293
- * label="Active period"
294
- * />
295
- */
296
- function DatePicker(props: DatePickerProps) {
297
- if (props.mode === "range") {
298
- return <RangeDatePicker {...props} />;
299
- }
300
- return <SingleDatePicker {...props} />;
301
- }
302
-
303
- export { DatePicker };
@@ -1,37 +0,0 @@
1
- /**
2
- * Parse a date-only string ("YYYY-MM-DD") as a local calendar day instead of
3
- * UTC midnight, so negative-UTC-offset timezones don't render the previous day.
4
- * Accepts a `Date` (returned as-is when valid) and returns `undefined` for
5
- * null/undefined/invalid input.
6
- */
7
- export function parseDateInput(value: Date | string | null | undefined): Date | undefined {
8
- if (!value) return undefined;
9
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value;
10
- const [year, month, day] = value.split("-").map(Number);
11
- if (!year || !month || !day) return undefined;
12
- // `new Date(year, month, day)` treats years from 0 through 99 as
13
- // 1900-based years. Native date inputs can temporarily expose those years
14
- // while a user is editing the year segment, so construct the date first and
15
- // set the full year explicitly to avoid turning `0002` into `1902`.
16
- const date = new Date(0);
17
- date.setFullYear(year, month - 1, day);
18
- return Number.isNaN(date.getTime()) ? undefined : date;
19
- }
20
-
21
- /**
22
- * Serialize a date to its "YYYY-MM-DD" form using local components, so the
23
- * value round-trips without a UTC shift. Returns "" for invalid input.
24
- */
25
- export function formatDateInput(value: Date | string | null | undefined): string {
26
- const date = parseDateInput(value);
27
- if (!date) return "";
28
- const year = String(date.getFullYear()).padStart(4, "0");
29
- const month = String(date.getMonth() + 1).padStart(2, "0");
30
- const day = String(date.getDate()).padStart(2, "0");
31
- return `${year}-${month}-${day}`;
32
- }
33
-
34
- /** Today's date as a "YYYY-MM-DD" local string. */
35
- export function todayDateInput(): string {
36
- return formatDateInput(new Date());
37
- }
@@ -1,140 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
- import { expect, fireEvent, userEvent, within } from "storybook/test";
4
-
5
- import { DropZone, type DropZoneFileInfo, type DropZoneStatus } from "./drop-zone";
6
-
7
- const meta = {
8
- title: "Forms/DropZone",
9
- component: DropZone,
10
- tags: ["autodocs"],
11
- args: {
12
- status: "empty",
13
- onFiles: () => {},
14
- },
15
- } satisfies Meta<typeof DropZone>;
16
-
17
- export default meta;
18
- type Story = StoryObj<typeof meta>;
19
-
20
- function DropZonePreview({ initialStatus = "empty" }: { initialStatus?: DropZoneStatus }) {
21
- const [status, setStatus] = useState<DropZoneStatus>(initialStatus);
22
- const [file, setFile] = useState<DropZoneFileInfo | null>(
23
- initialStatus === "empty" ? null : { name: "applications.csv", size: 24_576 },
24
- );
25
-
26
- return (
27
- <div className="w-96">
28
- <DropZone
29
- status={status}
30
- file={file}
31
- accept=".csv"
32
- emptyHint="Supported formats: CSV"
33
- error={status === "failed" ? "The file could not be processed." : undefined}
34
- busyDescription="This usually takes a moment."
35
- onFiles={(files) => {
36
- const selectedFile = files[0];
37
- if (!selectedFile) return;
38
- setFile({ name: selectedFile.name, size: selectedFile.size });
39
- setStatus("armed");
40
- }}
41
- onSubmit={() => setStatus("busy")}
42
- onCancel={() => setStatus("armed")}
43
- onRetry={() => setStatus("busy")}
44
- onClear={() => {
45
- setFile(null);
46
- setStatus("empty");
47
- }}
48
- />
49
- </div>
50
- );
51
- }
52
-
53
- export const Default: Story = {
54
- render: () => <DropZonePreview />,
55
- play: async ({ canvasElement }) => {
56
- const canvas = within(canvasElement);
57
- const input = canvasElement.querySelector('input[type="file"]') as HTMLInputElement;
58
- const file = new File(["name,email\nAda,ada@example.com"], "applications.csv", {
59
- type: "text/csv",
60
- });
61
-
62
- await expect(canvas.getByText("Drop a file here, or")).toBeInTheDocument();
63
- await fireEvent.change(input, { target: { files: [file] } });
64
- await expect(canvas.getByText("applications.csv")).toBeInTheDocument();
65
- await expect(canvas.getByRole("button", { name: /Upload/ })).toBeInTheDocument();
66
-
67
- await userEvent.click(canvas.getByRole("button", { name: /Upload/ }));
68
- await expect(canvas.getByRole("progressbar", { name: "Processing…" })).toBeInTheDocument();
69
- await expect(canvas.getByRole("button", { name: /Cancel/ })).toBeInTheDocument();
70
- },
71
- };
72
-
73
- export const Armed: Story = {
74
- render: () => <DropZonePreview initialStatus="armed" />,
75
- play: async ({ canvasElement }) => {
76
- const canvas = within(canvasElement);
77
-
78
- await expect(canvas.getByText("applications.csv")).toBeInTheDocument();
79
- await expect(canvas.getByText("24.0 KB")).toBeInTheDocument();
80
- await userEvent.click(canvas.getByRole("button", { name: /Use a different file/ }));
81
- await expect(canvas.getByText("Drop a file here, or")).toBeInTheDocument();
82
- },
83
- };
84
-
85
- export const Busy: Story = {
86
- render: () => <DropZonePreview initialStatus="busy" />,
87
- play: async ({ canvasElement }) => {
88
- const canvas = within(canvasElement);
89
-
90
- await expect(canvas.getByRole("progressbar", { name: "Processing…" })).toBeInTheDocument();
91
- await expect(canvas.getByRole("button", { name: /Cancel/ })).toBeInTheDocument();
92
- await expect(canvas.queryByRole("button", { name: /Upload/ })).not.toBeInTheDocument();
93
- await userEvent.click(canvas.getByRole("button", { name: /Cancel/ }));
94
- await expect(canvas.getByRole("button", { name: /Upload/ })).toBeInTheDocument();
95
- },
96
- };
97
-
98
- export const Dragging: Story = {
99
- render: () => <DropZonePreview />,
100
- play: async ({ canvasElement }) => {
101
- const canvas = within(canvasElement);
102
- const zone = canvas.getByRole("button", { name: "File upload drop zone" });
103
- const file = new File(["data"], "applications.csv", { type: "text/csv" });
104
-
105
- await fireEvent.dragEnter(zone);
106
- await expect(canvas.getByText("Release to attach")).toBeInTheDocument();
107
- await fireEvent.dragLeave(zone);
108
- await expect(canvas.getByText("Drop a file here, or")).toBeInTheDocument();
109
- const dataTransfer = new DataTransfer();
110
- dataTransfer.items.add(file);
111
- Object.defineProperty(dataTransfer, "files", { value: [file] });
112
- await fireEvent.drop(zone, { dataTransfer });
113
- await expect(canvas.getByText("applications.csv")).toBeInTheDocument();
114
- },
115
- };
116
-
117
- export const Failed: Story = {
118
- render: () => <DropZonePreview initialStatus="failed" />,
119
- play: async ({ canvasElement }) => {
120
- const canvas = within(canvasElement);
121
-
122
- await expect(canvas.getByText("The file could not be processed.")).toBeInTheDocument();
123
- await expect(canvas.getByRole("button", { name: /Try again/ })).toBeInTheDocument();
124
- await userEvent.click(canvas.getByRole("button", { name: /Try again/ }));
125
- await expect(canvas.getByRole("progressbar", { name: "Processing…" })).toBeInTheDocument();
126
- },
127
- };
128
-
129
- export const Disabled: Story = {
130
- args: { status: "empty", disabled: true },
131
- play: async ({ canvasElement }) => {
132
- const canvas = within(canvasElement);
133
- const zone = canvas.getByRole("button", { name: "File upload drop zone" });
134
- const input = canvasElement.querySelector('input[type="file"]') as HTMLInputElement;
135
-
136
- await expect(zone).toHaveAttribute("aria-disabled", "true");
137
- await expect(zone).toHaveAttribute("tabindex", "-1");
138
- await expect(input).toBeDisabled();
139
- },
140
- };