@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,298 +0,0 @@
1
- import { FileIcon, UploadIcon, XIcon } from "lucide-react";
2
- import {
3
- useCallback,
4
- useId,
5
- useRef,
6
- useState,
7
- type ChangeEvent,
8
- type DragEvent,
9
- type KeyboardEvent,
10
- type ReactNode,
11
- } from "react";
12
-
13
- import { cn } from "../../lib/utils";
14
- import { Progress } from "../feedback/progress";
15
- import { Button } from "../primitives/button";
16
-
17
- export type DropZoneStatus = "empty" | "armed" | "busy" | "failed";
18
-
19
- export type DropZoneFileInfo = {
20
- name: string;
21
- size: number;
22
- };
23
-
24
- export type DropZoneProps = {
25
- status: DropZoneStatus;
26
- /** Selected file metadata for armed / busy / failed (with file retained). */
27
- file?: DropZoneFileInfo | null;
28
- error?: string | null;
29
- notice?: string | null;
30
- /** File input accept attribute, e.g. `.pdf,application/pdf`. */
31
- accept?: string;
32
- multiple?: boolean;
33
- disabled?: boolean;
34
- className?: string;
35
- /** Empty idle copy */
36
- emptyLabel?: ReactNode;
37
- emptyHint?: ReactNode;
38
- draggingLabel?: ReactNode;
39
- /** Busy copy */
40
- busyLabel?: ReactNode;
41
- busyDescription?: ReactNode;
42
- /** Action labels — parent owns domain language */
43
- submitLabel?: string;
44
- retryLabel?: string;
45
- clearLabel?: string;
46
- cancelLabel?: string;
47
- /** Extra content under error (e.g. sign-in link). */
48
- failedActions?: ReactNode;
49
- onFiles: (files: File[]) => void;
50
- onSubmit?: () => void;
51
- onClear?: () => void;
52
- onCancel?: () => void;
53
- onRetry?: () => void;
54
- };
55
-
56
- function formatFileSize(bytes: number) {
57
- if (bytes < 1024) return `${bytes} B`;
58
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
59
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
60
- }
61
-
62
- /**
63
- * Generic file drop surface. Parent owns validation, network, and domain copy.
64
- * Modes: empty | armed | busy | failed, plus an internal drag overlay.
65
- */
66
- export function DropZone({
67
- status,
68
- file = null,
69
- error = null,
70
- notice = null,
71
- accept,
72
- multiple = false,
73
- disabled = false,
74
- className,
75
- emptyLabel = (
76
- <>
77
- Drop a file here, or <span className="text-accent-text font-medium">browse</span>
78
- </>
79
- ),
80
- emptyHint,
81
- draggingLabel = "Release to attach",
82
- busyLabel = "Processing…",
83
- busyDescription,
84
- submitLabel = "Upload",
85
- retryLabel = "Try again",
86
- clearLabel = "Use a different file",
87
- cancelLabel = "Cancel",
88
- failedActions,
89
- onFiles,
90
- onSubmit,
91
- onClear,
92
- onCancel,
93
- onRetry,
94
- }: DropZoneProps) {
95
- const inputId = useId();
96
- const inputRef = useRef<HTMLInputElement>(null);
97
- const zoneRef = useRef<HTMLDivElement>(null);
98
- const [dragging, setDragging] = useState(false);
99
-
100
- const isBusy = status === "busy";
101
- const isInteractive = !disabled && !isBusy;
102
-
103
- const openPicker = useCallback(() => {
104
- if (!isInteractive) return;
105
- inputRef.current?.click();
106
- }, [isInteractive]);
107
-
108
- const handleFiles = useCallback(
109
- (list: FileList | File[] | null) => {
110
- if (!list || !isInteractive) return;
111
- const files = Array.from(list);
112
- if (files.length === 0) return;
113
- onFiles(files);
114
- },
115
- [isInteractive, onFiles],
116
- );
117
-
118
- const onDragEnter = (event: DragEvent) => {
119
- event.preventDefault();
120
- event.stopPropagation();
121
- if (!isInteractive) return;
122
- setDragging(true);
123
- };
124
-
125
- const onDragOver = (event: DragEvent) => {
126
- event.preventDefault();
127
- event.stopPropagation();
128
- if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
129
- };
130
-
131
- const onDragLeave = (event: DragEvent) => {
132
- event.preventDefault();
133
- event.stopPropagation();
134
- const next = event.relatedTarget;
135
- if (next instanceof Node && zoneRef.current?.contains(next)) return;
136
- setDragging(false);
137
- };
138
-
139
- const onDrop = (event: DragEvent) => {
140
- event.preventDefault();
141
- event.stopPropagation();
142
- setDragging(false);
143
- if (!isInteractive) return;
144
- handleFiles(event.dataTransfer.files);
145
- };
146
-
147
- const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
148
- handleFiles(event.target.files);
149
- event.target.value = "";
150
- };
151
-
152
- const onKeyDown = (event: KeyboardEvent) => {
153
- if (event.key === "Enter" || event.key === " ") {
154
- event.preventDefault();
155
- openPicker();
156
- }
157
- };
158
-
159
- const showDragging = dragging && isInteractive;
160
- const frameTone = cn(
161
- showDragging && "border-ring bg-primary/5",
162
- status === "failed" && !showDragging && "border-destructive bg-destructive/5",
163
- status === "busy" && "border-border bg-muted/30",
164
- status !== "failed" && status !== "busy" && !showDragging && "border-border bg-card",
165
- );
166
-
167
- return (
168
- <div className={cn("w-full space-y-3", className)}>
169
- <input
170
- ref={inputRef}
171
- id={inputId}
172
- type="file"
173
- accept={accept}
174
- multiple={multiple}
175
- disabled={!isInteractive}
176
- className="pointer-events-none sr-only"
177
- onChange={onInputChange}
178
- tabIndex={-1}
179
- aria-hidden
180
- />
181
-
182
- <div
183
- ref={zoneRef}
184
- role="button"
185
- tabIndex={isInteractive ? 0 : -1}
186
- aria-disabled={!isInteractive}
187
- aria-busy={isBusy}
188
- aria-label="File upload drop zone"
189
- className={cn(
190
- "relative flex min-h-44 w-full flex-col items-center justify-center overflow-hidden rounded-2xl border border-dashed p-6 text-center outline-none",
191
- "focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2",
192
- "motion-safe:transition-[transform,opacity] motion-safe:duration-200 motion-safe:ease-in-out motion-reduce:transition-none",
193
- isInteractive ? "cursor-pointer" : "cursor-default",
194
- showDragging ? "scale-[1.01] motion-reduce:scale-100" : "scale-100",
195
- frameTone,
196
- )}
197
- onClick={isInteractive ? openPicker : undefined}
198
- onKeyDown={isInteractive ? onKeyDown : undefined}
199
- onDragEnter={onDragEnter}
200
- onDragOver={onDragOver}
201
- onDragLeave={onDragLeave}
202
- onDrop={onDrop}
203
- >
204
- <div className="flex w-full max-w-sm flex-col items-center gap-3">
205
- {showDragging ? (
206
- <>
207
- <UploadIcon
208
- className="text-accent-text size-8 motion-safe:transition-[transform,opacity] motion-safe:duration-200 motion-safe:ease-in-out motion-reduce:transition-none motion-safe:-translate-y-0.5 motion-reduce:translate-y-0"
209
- aria-hidden
210
- />
211
- <p className="body-3 text-foreground">{draggingLabel}</p>
212
- </>
213
- ) : null}
214
-
215
- {!showDragging && status === "empty" ? (
216
- <>
217
- <UploadIcon className="size-8 text-muted-foreground" aria-hidden />
218
- <div className="space-y-1">
219
- <p className="body-3 text-foreground">{emptyLabel}</p>
220
- {emptyHint ? <p className="body-4 text-muted-foreground">{emptyHint}</p> : null}
221
- </div>
222
- </>
223
- ) : null}
224
-
225
- {!showDragging && status === "armed" && file ? (
226
- <>
227
- <FileIcon className="size-8 text-muted-foreground" aria-hidden />
228
- <div className="min-w-0 max-w-full space-y-1">
229
- <p className="subheading-4 truncate text-foreground">{file.name}</p>
230
- <p className="body-4 text-muted-foreground">{formatFileSize(file.size)}</p>
231
- </div>
232
- </>
233
- ) : null}
234
-
235
- {!showDragging && status === "busy" ? (
236
- <div className="w-full space-y-3">
237
- <div className="space-y-1">
238
- <p className="subheading-4 text-foreground">{busyLabel}</p>
239
- {busyDescription ? (
240
- <p className="body-4 text-muted-foreground">{busyDescription}</p>
241
- ) : null}
242
- {file ? <p className="body-4 truncate text-muted-foreground">{file.name}</p> : null}
243
- </div>
244
- <Progress
245
- className="h-1.5 opacity-80 motion-safe:animate-pulse"
246
- aria-label={typeof busyLabel === "string" ? busyLabel : "Processing"}
247
- value={50}
248
- />
249
- </div>
250
- ) : null}
251
-
252
- {!showDragging && status === "failed" ? (
253
- <div className="w-full space-y-2">
254
- {file ? <p className="subheading-4 truncate text-foreground">{file.name}</p> : null}
255
- {error ? <p className="body-3 text-destructive-text">{error}</p> : null}
256
- {failedActions}
257
- </div>
258
- ) : null}
259
- </div>
260
- </div>
261
-
262
- {notice ? <p className="body-4 text-muted-foreground">{notice}</p> : null}
263
-
264
- <div className="grid gap-2">
265
- {status === "armed" && onSubmit ? (
266
- <Button type="button" className="w-full" onClick={onSubmit}>
267
- <UploadIcon className="size-4" />
268
- {submitLabel}
269
- </Button>
270
- ) : null}
271
- {status === "failed" && onRetry ? (
272
- <Button type="button" className="w-full" onClick={onRetry}>
273
- {retryLabel}
274
- </Button>
275
- ) : null}
276
- {status === "busy" && onCancel ? (
277
- <Button type="button" variant="outline" className="w-full" onClick={onCancel}>
278
- <XIcon className="size-4" />
279
- {cancelLabel}
280
- </Button>
281
- ) : null}
282
- {(status === "armed" || status === "failed") && onClear ? (
283
- <Button
284
- type="button"
285
- variant="ghost"
286
- className="w-full"
287
- onClick={(event) => {
288
- event.stopPropagation();
289
- onClear();
290
- }}
291
- >
292
- {clearLabel}
293
- </Button>
294
- ) : null}
295
- </div>
296
- </div>
297
- );
298
- }
@@ -1,47 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { booleanControl, selectControl, textControl } from "../../storybook/controls";
4
- import { inputTypeOptions } from "../../storybook/options";
5
- import { Input } from "./input";
6
-
7
- const meta = {
8
- title: "Forms/Input",
9
- component: Input,
10
- tags: ["autodocs"],
11
- argTypes: {
12
- type: selectControl(inputTypeOptions, "HTML input type attribute", {
13
- defaultValue: "text",
14
- }),
15
- placeholder: textControl("Placeholder text shown when the input is empty"),
16
- disabled: booleanControl("Prevents user interaction and applies disabled styling", false),
17
- required: booleanControl("Marks the input as required for form submission", false),
18
- },
19
- } satisfies Meta<typeof Input>;
20
-
21
- export default meta;
22
- type Story = StoryObj<typeof meta>;
23
-
24
- export const Default: Story = {
25
- args: { placeholder: "Enter text…" },
26
- };
27
-
28
- export const WithLabel: Story = {
29
- render: () => (
30
- <div className="flex w-72 flex-col gap-2">
31
- <label htmlFor="email" className="text-foreground text-sm leading-none font-medium">
32
- Email
33
- </label>
34
- <Input id="email" type="email" placeholder="you@example.com" />
35
- </div>
36
- ),
37
- };
38
-
39
- export const States: Story = {
40
- render: () => (
41
- <div className="flex w-72 flex-col gap-4">
42
- <Input aria-label="Default input" placeholder="Default" />
43
- <Input aria-label="Disabled input" placeholder="Disabled" disabled />
44
- <Input aria-label="Input with value" defaultValue="With value" />
45
- </div>
46
- ),
47
- };
@@ -1,24 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
-
5
- function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
- return (
7
- <input
8
- type={type}
9
- data-slot="input"
10
- className={cn(
11
- "bg-background text-foreground border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base transition-colors md:text-sm",
12
- "file:text-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium",
13
- "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground",
14
- "focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
15
- "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
16
- "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
17
- className,
18
- )}
19
- {...props}
20
- />
21
- );
22
- }
23
-
24
- export { Input };
@@ -1,16 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Label } from "./label";
4
-
5
- const meta = {
6
- title: "Forms/Label",
7
- component: Label,
8
- tags: ["autodocs"],
9
- } satisfies Meta<typeof Label>;
10
-
11
- export default meta;
12
- type Story = StoryObj<typeof meta>;
13
-
14
- export const Default: Story = {
15
- args: { children: "Email address", htmlFor: "email" },
16
- };
@@ -1,25 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
-
5
- import { cn } from "../../lib/utils";
6
-
7
- function Label({ className, ...props }: React.ComponentProps<"label">) {
8
- return (
9
- // Association happens at usage (htmlFor or wrapping); the wrapper
10
- // itself cannot know its control.
11
- // oxlint-disable-next-line jsx-a11y/label-has-associated-control
12
- <label
13
- data-slot="label"
14
- className={cn(
15
- "text-sm leading-none font-medium select-none",
16
- "peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
- "group-has-disabled:cursor-not-allowed group-has-disabled:opacity-50",
18
- className,
19
- )}
20
- {...props}
21
- />
22
- );
23
- }
24
-
25
- export { Label };
@@ -1,85 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import { PasskeyManagement, type PasskeyRecord } from "./passkey-management";
5
-
6
- const meta = {
7
- title: "Forms/PasskeyManagement",
8
- component: PasskeyManagement,
9
- tags: ["autodocs"],
10
- } satisfies Meta<typeof PasskeyManagement>;
11
-
12
- export default meta;
13
- type Story = StoryObj<typeof meta>;
14
-
15
- const initialPasskeys: PasskeyRecord[] = [
16
- { id: "1", name: "MacBook Pro", createdAt: "2025-01-15T00:00:00.000Z" },
17
- { id: "2", name: "iPhone", createdAt: "2025-03-02T00:00:00.000Z" },
18
- ];
19
-
20
- function PasskeyManagementPreview({ withDeleteError = false }: { withDeleteError?: boolean }) {
21
- const [passkeys, setPasskeys] = useState(initialPasskeys);
22
-
23
- return (
24
- <div className="w-96">
25
- <PasskeyManagement
26
- passkeys={passkeys}
27
- onAdd={async () => {
28
- setPasskeys((prev) => [
29
- ...prev,
30
- {
31
- id: String(prev.length + 1),
32
- name: "New device",
33
- createdAt: new Date().toISOString(),
34
- },
35
- ]);
36
- return true;
37
- }}
38
- onDelete={async (id) => {
39
- if (withDeleteError && id === "2") return false;
40
- setPasskeys((prev) => prev.filter((pk) => pk.id !== id));
41
- return true;
42
- }}
43
- />
44
- </div>
45
- );
46
- }
47
-
48
- export const Default: Story = {
49
- args: {
50
- passkeys: initialPasskeys,
51
- onAdd: async () => true,
52
- onDelete: async () => true,
53
- },
54
- render: () => <PasskeyManagementPreview />,
55
- };
56
-
57
- export const Empty: Story = {
58
- args: {
59
- passkeys: [],
60
- onAdd: async () => true,
61
- onDelete: async () => true,
62
- },
63
- };
64
-
65
- export const Loading: Story = {
66
- args: {
67
- isLoading: true,
68
- onAdd: async () => true,
69
- onDelete: async () => true,
70
- },
71
- render: (args) => (
72
- <div className="w-[calc(100vw-2rem)] max-w-md">
73
- <PasskeyManagement {...args} />
74
- </div>
75
- ),
76
- };
77
-
78
- export const WithError: Story = {
79
- args: {
80
- passkeys: initialPasskeys,
81
- onAdd: async () => true,
82
- onDelete: async () => true,
83
- },
84
- render: () => <PasskeyManagementPreview withDeleteError />,
85
- };
@@ -1,168 +0,0 @@
1
- import { KeyRound, Plus, Trash2, X } from "lucide-react";
2
- import { useCallback, useState } from "react";
3
-
4
- import { Spinner } from "../feedback/spinner";
5
- import { Button } from "../primitives/button";
6
- import { Card, CardHeader } from "../primitives/card";
7
-
8
- export interface PasskeyRecord {
9
- id: string;
10
- name?: string;
11
- createdAt?: string;
12
- }
13
-
14
- export interface PasskeyManagementProps {
15
- passkeys?: PasskeyRecord[];
16
- isLoading?: boolean;
17
- error?: string | null;
18
- onAdd: () => Promise<boolean>;
19
- onDelete: (id: string) => Promise<boolean>;
20
- }
21
-
22
- type PasskeyActionError = {
23
- message: string;
24
- passkeyId?: string;
25
- };
26
-
27
- /** List, add, and remove passkeys. The host app owns the WebAuthn ceremony via `onAdd`/`onDelete`. */
28
- export function PasskeyManagement({
29
- passkeys: passkeysProp,
30
- isLoading = false,
31
- error: externalError,
32
- onAdd,
33
- onDelete,
34
- }: PasskeyManagementProps) {
35
- const [adding, setAdding] = useState(false);
36
- const [deletingId, setDeletingId] = useState<string | null>(null);
37
- const [actionError, setActionError] = useState<PasskeyActionError | null>(null);
38
-
39
- const passkeys = passkeysProp ?? [];
40
- const error = externalError ?? (actionError?.passkeyId ? null : actionError?.message);
41
-
42
- const handleAdd = useCallback(async () => {
43
- setAdding(true);
44
- setActionError(null);
45
- try {
46
- const success = await onAdd();
47
- if (!success) {
48
- setActionError({ message: "Passkey registration was cancelled or failed." });
49
- }
50
- } catch {
51
- setActionError({ message: "An error occurred during passkey registration." });
52
- } finally {
53
- setAdding(false);
54
- }
55
- }, [onAdd]);
56
-
57
- const handleDelete = useCallback(
58
- async (id: string) => {
59
- setDeletingId(id);
60
- setActionError(null);
61
- try {
62
- const success = await onDelete(id);
63
- if (!success) {
64
- throw new Error("Failed to delete passkey");
65
- }
66
- } catch {
67
- setActionError({
68
- message: "Could not delete passkey. Please try again.",
69
- passkeyId: id,
70
- });
71
- } finally {
72
- setDeletingId(null);
73
- }
74
- },
75
- [onDelete],
76
- );
77
-
78
- return (
79
- <Card aria-labelledby="passkey-heading" className="gap-0 overflow-hidden w-full">
80
- <CardHeader className="flex-row items-center border-b p-4">
81
- <h2 id="passkey-heading" className="text-foreground text-sm font-medium">
82
- Passkeys
83
- </h2>
84
- <Button
85
- type="button"
86
- size="sm"
87
- onClick={() => void handleAdd()}
88
- isLoading={adding}
89
- loadingLabel="Adding passkey"
90
- aria-label="Add a passkey"
91
- className="ml-auto"
92
- >
93
- <Plus className="size-4" aria-hidden />
94
- </Button>
95
- </CardHeader>
96
-
97
- {error ? (
98
- <p role="alert" className="text-destructive-text border-b px-4 py-3 text-sm">
99
- {error}
100
- </p>
101
- ) : null}
102
-
103
- {isLoading ? (
104
- <Spinner presentation="centered" size="sm" label="Loading passkeys" />
105
- ) : passkeys.length === 0 ? (
106
- <div className="text-muted-foreground flex items-center gap-3 p-4 text-sm">
107
- <KeyRound className="size-4 shrink-0" aria-hidden />
108
- <span>No passkeys registered. Add one to sign in faster.</span>
109
- </div>
110
- ) : (
111
- <ul className="divide-border divide-y" aria-label="Registered passkeys">
112
- {passkeys.map((pk) => {
113
- const rowError = actionError?.passkeyId === pk.id ? actionError.message : null;
114
-
115
- return (
116
- <li key={pk.id} className="flex flex-col gap-2 px-4 py-3 text-sm">
117
- <div className="flex items-center justify-between gap-4">
118
- <div className="flex min-w-0 items-center gap-3">
119
- <KeyRound className="text-muted-foreground size-4 shrink-0" aria-hidden />
120
- <div className="min-w-0">
121
- <span className="font-medium">{pk.name ?? "Passkey"}</span>
122
- {pk.createdAt ? (
123
- <span className="text-muted-foreground ml-2 text-xs">
124
- Added {new Date(pk.createdAt).toLocaleDateString()}
125
- </span>
126
- ) : null}
127
- </div>
128
- </div>
129
- <button
130
- type="button"
131
- onClick={() => void handleDelete(pk.id)}
132
- disabled={deletingId === pk.id}
133
- aria-busy={deletingId === pk.id}
134
- aria-label={`Remove passkey ${pk.name ?? pk.id}`}
135
- className="hover:text-destructive-text text-muted-foreground focus-visible:ring-ring rounded-sm focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
136
- >
137
- {deletingId === pk.id ? (
138
- <Spinner size="sm" aria-hidden />
139
- ) : (
140
- <Trash2 className="size-4" aria-hidden />
141
- )}
142
- </button>
143
- </div>
144
- {rowError ? (
145
- <div
146
- role="alert"
147
- className="bg-destructive/5 text-destructive-text flex items-start justify-between gap-3 rounded-md px-3 py-2 text-sm"
148
- >
149
- <span>{rowError}</span>
150
- <button
151
- type="button"
152
- onClick={() => setActionError(null)}
153
- aria-label="Dismiss passkey error"
154
- title="Dismiss error"
155
- className="hover:text-destructive focus-visible:ring-ring -m-1 shrink-0 rounded-sm p-1 focus-visible:ring-2 focus-visible:outline-none"
156
- >
157
- <X className="size-4" aria-hidden />
158
- </button>
159
- </div>
160
- ) : null}
161
- </li>
162
- );
163
- })}
164
- </ul>
165
- )}
166
- </Card>
167
- );
168
- }