@vaam-apps/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/LICENSE +21 -0
  3. package/README.md +286 -0
  4. package/dist/components/data/code.d.ts +37 -0
  5. package/dist/components/data/code.d.ts.map +1 -0
  6. package/dist/components/data/code.js +37 -0
  7. package/dist/components/data/code.js.map +1 -0
  8. package/dist/components/data/copy-button.d.ts +39 -0
  9. package/dist/components/data/copy-button.d.ts.map +1 -0
  10. package/dist/components/data/copy-button.js +50 -0
  11. package/dist/components/data/copy-button.js.map +1 -0
  12. package/dist/components/data/detail-row.d.ts +69 -0
  13. package/dist/components/data/detail-row.d.ts.map +1 -0
  14. package/dist/components/data/detail-row.js +27 -0
  15. package/dist/components/data/detail-row.js.map +1 -0
  16. package/dist/components/data/id-display.d.ts +33 -0
  17. package/dist/components/data/id-display.d.ts.map +1 -0
  18. package/dist/components/data/id-display.js +25 -0
  19. package/dist/components/data/id-display.js.map +1 -0
  20. package/dist/components/data/masked-value.d.ts +47 -0
  21. package/dist/components/data/masked-value.d.ts.map +1 -0
  22. package/dist/components/data/masked-value.js +46 -0
  23. package/dist/components/data/masked-value.js.map +1 -0
  24. package/dist/components/data/money.d.ts +28 -0
  25. package/dist/components/data/money.d.ts.map +1 -0
  26. package/dist/components/data/money.js +22 -0
  27. package/dist/components/data/money.js.map +1 -0
  28. package/dist/components/data/phone-display.d.ts +45 -0
  29. package/dist/components/data/phone-display.d.ts.map +1 -0
  30. package/dist/components/data/phone-display.js +23 -0
  31. package/dist/components/data/phone-display.js.map +1 -0
  32. package/dist/components/data/stat-tile.d.ts +33 -0
  33. package/dist/components/data/stat-tile.d.ts.map +1 -0
  34. package/dist/components/data/stat-tile.js +18 -0
  35. package/dist/components/data/stat-tile.js.map +1 -0
  36. package/dist/components/data/timestamp-display.d.ts +14 -0
  37. package/dist/components/data/timestamp-display.d.ts.map +1 -0
  38. package/dist/components/data/timestamp-display.js +77 -0
  39. package/dist/components/data/timestamp-display.js.map +1 -0
  40. package/dist/components/patterns/inline-banner.d.ts +22 -0
  41. package/dist/components/patterns/inline-banner.d.ts.map +1 -0
  42. package/dist/components/patterns/inline-banner.js +11 -0
  43. package/dist/components/patterns/inline-banner.js.map +1 -0
  44. package/dist/components/patterns/inline-empty-state.d.ts +20 -0
  45. package/dist/components/patterns/inline-empty-state.d.ts.map +1 -0
  46. package/dist/components/patterns/inline-empty-state.js +11 -0
  47. package/dist/components/patterns/inline-empty-state.js.map +1 -0
  48. package/dist/components/patterns/live-row.d.ts +23 -0
  49. package/dist/components/patterns/live-row.d.ts.map +1 -0
  50. package/dist/components/patterns/live-row.js +48 -0
  51. package/dist/components/patterns/live-row.js.map +1 -0
  52. package/dist/components/patterns/payload-inspector.d.ts +25 -0
  53. package/dist/components/patterns/payload-inspector.d.ts.map +1 -0
  54. package/dist/components/patterns/payload-inspector.js +33 -0
  55. package/dist/components/patterns/payload-inspector.js.map +1 -0
  56. package/dist/components/patterns/route-skeleton.d.ts +15 -0
  57. package/dist/components/patterns/route-skeleton.d.ts.map +1 -0
  58. package/dist/components/patterns/route-skeleton.js +7 -0
  59. package/dist/components/patterns/route-skeleton.js.map +1 -0
  60. package/dist/components/patterns/screen-layout.d.ts +14 -0
  61. package/dist/components/patterns/screen-layout.d.ts.map +1 -0
  62. package/dist/components/patterns/screen-layout.js +11 -0
  63. package/dist/components/patterns/screen-layout.js.map +1 -0
  64. package/dist/components/patterns/stale-write-banner.d.ts +26 -0
  65. package/dist/components/patterns/stale-write-banner.d.ts.map +1 -0
  66. package/dist/components/patterns/stale-write-banner.js +28 -0
  67. package/dist/components/patterns/stale-write-banner.js.map +1 -0
  68. package/dist/components/patterns/state-timeline.d.ts +58 -0
  69. package/dist/components/patterns/state-timeline.d.ts.map +1 -0
  70. package/dist/components/patterns/state-timeline.js +103 -0
  71. package/dist/components/patterns/state-timeline.js.map +1 -0
  72. package/dist/components/primitives/badge.d.ts +27 -0
  73. package/dist/components/primitives/badge.d.ts.map +1 -0
  74. package/dist/components/primitives/badge.js +33 -0
  75. package/dist/components/primitives/badge.js.map +1 -0
  76. package/dist/components/primitives/button.d.ts +12 -0
  77. package/dist/components/primitives/button.d.ts.map +1 -0
  78. package/dist/components/primitives/button.js +42 -0
  79. package/dist/components/primitives/button.js.map +1 -0
  80. package/dist/components/primitives/calendar.d.ts +39 -0
  81. package/dist/components/primitives/calendar.d.ts.map +1 -0
  82. package/dist/components/primitives/calendar.js +102 -0
  83. package/dist/components/primitives/calendar.js.map +1 -0
  84. package/dist/components/primitives/card.d.ts +24 -0
  85. package/dist/components/primitives/card.d.ts.map +1 -0
  86. package/dist/components/primitives/card.js +28 -0
  87. package/dist/components/primitives/card.js.map +1 -0
  88. package/dist/components/primitives/checkbox.d.ts +42 -0
  89. package/dist/components/primitives/checkbox.d.ts.map +1 -0
  90. package/dist/components/primitives/checkbox.js +31 -0
  91. package/dist/components/primitives/checkbox.js.map +1 -0
  92. package/dist/components/primitives/chip-select.d.ts +38 -0
  93. package/dist/components/primitives/chip-select.d.ts.map +1 -0
  94. package/dist/components/primitives/chip-select.js +25 -0
  95. package/dist/components/primitives/chip-select.js.map +1 -0
  96. package/dist/components/primitives/command-menu.d.ts +54 -0
  97. package/dist/components/primitives/command-menu.d.ts.map +1 -0
  98. package/dist/components/primitives/command-menu.js +24 -0
  99. package/dist/components/primitives/command-menu.js.map +1 -0
  100. package/dist/components/primitives/confirm-dialog.d.ts +52 -0
  101. package/dist/components/primitives/confirm-dialog.d.ts.map +1 -0
  102. package/dist/components/primitives/confirm-dialog.js +30 -0
  103. package/dist/components/primitives/confirm-dialog.js.map +1 -0
  104. package/dist/components/primitives/date-picker.d.ts +68 -0
  105. package/dist/components/primitives/date-picker.d.ts.map +1 -0
  106. package/dist/components/primitives/date-picker.js +107 -0
  107. package/dist/components/primitives/date-picker.js.map +1 -0
  108. package/dist/components/primitives/dialog.d.ts +50 -0
  109. package/dist/components/primitives/dialog.d.ts.map +1 -0
  110. package/dist/components/primitives/dialog.js +90 -0
  111. package/dist/components/primitives/dialog.js.map +1 -0
  112. package/dist/components/primitives/drawer.d.ts +71 -0
  113. package/dist/components/primitives/drawer.d.ts.map +1 -0
  114. package/dist/components/primitives/drawer.js +93 -0
  115. package/dist/components/primitives/drawer.js.map +1 -0
  116. package/dist/components/primitives/dropdown-menu.d.ts +40 -0
  117. package/dist/components/primitives/dropdown-menu.d.ts.map +1 -0
  118. package/dist/components/primitives/dropdown-menu.js +53 -0
  119. package/dist/components/primitives/dropdown-menu.js.map +1 -0
  120. package/dist/components/primitives/form-field.d.ts +94 -0
  121. package/dist/components/primitives/form-field.d.ts.map +1 -0
  122. package/dist/components/primitives/form-field.js +27 -0
  123. package/dist/components/primitives/form-field.js.map +1 -0
  124. package/dist/components/primitives/inline-confirm.d.ts +62 -0
  125. package/dist/components/primitives/inline-confirm.d.ts.map +1 -0
  126. package/dist/components/primitives/inline-confirm.js +11 -0
  127. package/dist/components/primitives/inline-confirm.js.map +1 -0
  128. package/dist/components/primitives/input.d.ts +4 -0
  129. package/dist/components/primitives/input.d.ts.map +1 -0
  130. package/dist/components/primitives/input.js +13 -0
  131. package/dist/components/primitives/input.js.map +1 -0
  132. package/dist/components/primitives/label.d.ts +26 -0
  133. package/dist/components/primitives/label.d.ts.map +1 -0
  134. package/dist/components/primitives/label.js +33 -0
  135. package/dist/components/primitives/label.js.map +1 -0
  136. package/dist/components/primitives/pagination.d.ts +54 -0
  137. package/dist/components/primitives/pagination.d.ts.map +1 -0
  138. package/dist/components/primitives/pagination.js +31 -0
  139. package/dist/components/primitives/pagination.js.map +1 -0
  140. package/dist/components/primitives/popover.d.ts +12 -0
  141. package/dist/components/primitives/popover.d.ts.map +1 -0
  142. package/dist/components/primitives/popover.js +22 -0
  143. package/dist/components/primitives/popover.js.map +1 -0
  144. package/dist/components/primitives/progress.d.ts +25 -0
  145. package/dist/components/primitives/progress.d.ts.map +1 -0
  146. package/dist/components/primitives/progress.js +25 -0
  147. package/dist/components/primitives/progress.js.map +1 -0
  148. package/dist/components/primitives/radio-group.d.ts +47 -0
  149. package/dist/components/primitives/radio-group.d.ts.map +1 -0
  150. package/dist/components/primitives/radio-group.js +11 -0
  151. package/dist/components/primitives/radio-group.js.map +1 -0
  152. package/dist/components/primitives/select.d.ts +39 -0
  153. package/dist/components/primitives/select.d.ts.map +1 -0
  154. package/dist/components/primitives/select.js +142 -0
  155. package/dist/components/primitives/select.js.map +1 -0
  156. package/dist/components/primitives/separator.d.ts +9 -0
  157. package/dist/components/primitives/separator.d.ts.map +1 -0
  158. package/dist/components/primitives/separator.js +16 -0
  159. package/dist/components/primitives/separator.js.map +1 -0
  160. package/dist/components/primitives/side-nav.d.ts +65 -0
  161. package/dist/components/primitives/side-nav.d.ts.map +1 -0
  162. package/dist/components/primitives/side-nav.js +100 -0
  163. package/dist/components/primitives/side-nav.js.map +1 -0
  164. package/dist/components/primitives/skeleton.d.ts +22 -0
  165. package/dist/components/primitives/skeleton.d.ts.map +1 -0
  166. package/dist/components/primitives/skeleton.js +25 -0
  167. package/dist/components/primitives/skeleton.js.map +1 -0
  168. package/dist/components/primitives/spinner.d.ts +20 -0
  169. package/dist/components/primitives/spinner.d.ts.map +1 -0
  170. package/dist/components/primitives/spinner.js +21 -0
  171. package/dist/components/primitives/spinner.js.map +1 -0
  172. package/dist/components/primitives/switch.d.ts +29 -0
  173. package/dist/components/primitives/switch.d.ts.map +1 -0
  174. package/dist/components/primitives/switch.js +24 -0
  175. package/dist/components/primitives/switch.js.map +1 -0
  176. package/dist/components/primitives/table.d.ts +23 -0
  177. package/dist/components/primitives/table.d.ts.map +1 -0
  178. package/dist/components/primitives/table.js +81 -0
  179. package/dist/components/primitives/table.js.map +1 -0
  180. package/dist/components/primitives/tabs.d.ts +38 -0
  181. package/dist/components/primitives/tabs.d.ts.map +1 -0
  182. package/dist/components/primitives/tabs.js +84 -0
  183. package/dist/components/primitives/tabs.js.map +1 -0
  184. package/dist/components/primitives/textarea.d.ts +4 -0
  185. package/dist/components/primitives/textarea.d.ts.map +1 -0
  186. package/dist/components/primitives/textarea.js +11 -0
  187. package/dist/components/primitives/textarea.js.map +1 -0
  188. package/dist/components/primitives/toast.d.ts +24 -0
  189. package/dist/components/primitives/toast.d.ts.map +1 -0
  190. package/dist/components/primitives/toast.js +58 -0
  191. package/dist/components/primitives/toast.js.map +1 -0
  192. package/dist/components/primitives/tooltip.d.ts +30 -0
  193. package/dist/components/primitives/tooltip.d.ts.map +1 -0
  194. package/dist/components/primitives/tooltip.js +6 -0
  195. package/dist/components/primitives/tooltip.js.map +1 -0
  196. package/dist/components/status/state-chip.d.ts +53 -0
  197. package/dist/components/status/state-chip.d.ts.map +1 -0
  198. package/dist/components/status/state-chip.js +8 -0
  199. package/dist/components/status/state-chip.js.map +1 -0
  200. package/dist/components/status/state-mark.d.ts +21 -0
  201. package/dist/components/status/state-mark.d.ts.map +1 -0
  202. package/dist/components/status/state-mark.js +90 -0
  203. package/dist/components/status/state-mark.js.map +1 -0
  204. package/dist/components/status/status-pill.d.ts +62 -0
  205. package/dist/components/status/status-pill.d.ts.map +1 -0
  206. package/dist/components/status/status-pill.js +65 -0
  207. package/dist/components/status/status-pill.js.map +1 -0
  208. package/dist/components/status/status-tokens.d.ts +110 -0
  209. package/dist/components/status/status-tokens.d.ts.map +1 -0
  210. package/dist/components/status/status-tokens.js +102 -0
  211. package/dist/components/status/status-tokens.js.map +1 -0
  212. package/dist/index.d.ts +76 -0
  213. package/dist/index.d.ts.map +1 -0
  214. package/dist/index.js +95 -0
  215. package/dist/index.js.map +1 -0
  216. package/dist/lib/cn.d.ts +11 -0
  217. package/dist/lib/cn.d.ts.map +1 -0
  218. package/dist/lib/cn.js +78 -0
  219. package/dist/lib/cn.js.map +1 -0
  220. package/dist/lib/money.d.ts +73 -0
  221. package/dist/lib/money.d.ts.map +1 -0
  222. package/dist/lib/money.js +123 -0
  223. package/dist/lib/money.js.map +1 -0
  224. package/dist/lib/omit-undefined.d.ts +17 -0
  225. package/dist/lib/omit-undefined.d.ts.map +1 -0
  226. package/dist/lib/omit-undefined.js +24 -0
  227. package/dist/lib/omit-undefined.js.map +1 -0
  228. package/package.json +100 -0
  229. package/src/components/data/code.tsx +43 -0
  230. package/src/components/data/copy-button.tsx +97 -0
  231. package/src/components/data/detail-row.tsx +113 -0
  232. package/src/components/data/id-display.tsx +49 -0
  233. package/src/components/data/masked-value.tsx +96 -0
  234. package/src/components/data/money.tsx +46 -0
  235. package/src/components/data/phone-display.tsx +60 -0
  236. package/src/components/data/stat-tile.tsx +68 -0
  237. package/src/components/data/timestamp-display.tsx +106 -0
  238. package/src/components/patterns/inline-banner.tsx +60 -0
  239. package/src/components/patterns/inline-empty-state.tsx +45 -0
  240. package/src/components/patterns/live-row.tsx +75 -0
  241. package/src/components/patterns/payload-inspector.tsx +157 -0
  242. package/src/components/patterns/route-skeleton.tsx +77 -0
  243. package/src/components/patterns/screen-layout.tsx +44 -0
  244. package/src/components/patterns/stale-write-banner.tsx +44 -0
  245. package/src/components/patterns/state-timeline.tsx +229 -0
  246. package/src/components/primitives/badge.tsx +40 -0
  247. package/src/components/primitives/button.tsx +54 -0
  248. package/src/components/primitives/calendar.tsx +136 -0
  249. package/src/components/primitives/card.tsx +70 -0
  250. package/src/components/primitives/checkbox.tsx +101 -0
  251. package/src/components/primitives/chip-select.tsx +105 -0
  252. package/src/components/primitives/command-menu.tsx +92 -0
  253. package/src/components/primitives/confirm-dialog.tsx +109 -0
  254. package/src/components/primitives/date-picker.tsx +271 -0
  255. package/src/components/primitives/dialog.tsx +195 -0
  256. package/src/components/primitives/drawer.tsx +326 -0
  257. package/src/components/primitives/dropdown-menu.tsx +128 -0
  258. package/src/components/primitives/form-field.tsx +136 -0
  259. package/src/components/primitives/inline-confirm.tsx +118 -0
  260. package/src/components/primitives/input.tsx +21 -0
  261. package/src/components/primitives/label.tsx +43 -0
  262. package/src/components/primitives/pagination.tsx +110 -0
  263. package/src/components/primitives/popover.tsx +41 -0
  264. package/src/components/primitives/progress.tsx +66 -0
  265. package/src/components/primitives/radio-group.tsx +90 -0
  266. package/src/components/primitives/select.tsx +254 -0
  267. package/src/components/primitives/separator.tsx +49 -0
  268. package/src/components/primitives/side-nav.tsx +253 -0
  269. package/src/components/primitives/skeleton.tsx +25 -0
  270. package/src/components/primitives/spinner.tsx +39 -0
  271. package/src/components/primitives/switch.tsx +74 -0
  272. package/src/components/primitives/table.tsx +157 -0
  273. package/src/components/primitives/tabs.tsx +163 -0
  274. package/src/components/primitives/textarea.tsx +21 -0
  275. package/src/components/primitives/toast.tsx +117 -0
  276. package/src/components/primitives/tooltip.tsx +38 -0
  277. package/src/components/status/state-chip.tsx +70 -0
  278. package/src/components/status/state-mark.tsx +154 -0
  279. package/src/components/status/status-pill.tsx +153 -0
  280. package/src/components/status/status-tokens.ts +173 -0
  281. package/src/index.ts +96 -0
  282. package/src/lib/cn.ts +168 -0
  283. package/src/lib/money.ts +164 -0
  284. package/src/lib/omit-undefined.ts +25 -0
  285. package/src/styles/theme.css +381 -0
@@ -0,0 +1,326 @@
1
+ "use client";
2
+
3
+ import { X } from "lucide-react";
4
+ import { forwardRef, type ReactNode } from "react";
5
+ import { Drawer as DrawerPrimitive } from "vaul";
6
+ import { cn } from "../../lib/cn";
7
+
8
+ // vaul is standalone. Direction defaults to "right" — a side drawer reads
9
+ // as the console's Sheet-equivalent (design doc T15: "extend, don't fork");
10
+ // pass `direction="bottom"` for the mobile/narrow-viewport case.
11
+ //
12
+ // This generic composition (`Drawer`/`DrawerTrigger`/`DrawerClose`/
13
+ // `DrawerContent`) is unchanged by console-redesign.md §6.4/D14 — it is
14
+ // still the right tool for a one-off drawer with no quick-vs-more
15
+ // distinction to encode (see the gallery's own "Open drawer" example).
16
+ // `QuickDetailDrawer`/`MoreDetailDrawer` below are a *second*, self-
17
+ // contained API for the specific §3 distinction; they do not replace this
18
+ // one.
19
+ export const Drawer = DrawerPrimitive.Root;
20
+ export const DrawerTrigger = DrawerPrimitive.Trigger;
21
+ export const DrawerClose = DrawerPrimitive.Close;
22
+ export const DrawerPortal = DrawerPrimitive.Portal;
23
+
24
+ export const DrawerOverlay = forwardRef<
25
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
26
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
27
+ >(({ className, ...props }, ref) => (
28
+ <DrawerPrimitive.Overlay
29
+ ref={ref}
30
+ className={cn("fixed inset-0 z-50 bg-black/50", className)}
31
+ {...props}
32
+ />
33
+ ));
34
+ DrawerOverlay.displayName = "DrawerOverlay";
35
+
36
+ export const DrawerContent = forwardRef<
37
+ React.ElementRef<typeof DrawerPrimitive.Content>,
38
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
39
+ >(({ className, children, ...props }, ref) => (
40
+ <DrawerPortal>
41
+ <DrawerOverlay />
42
+ <DrawerPrimitive.Content
43
+ ref={ref}
44
+ className={cn(
45
+ "fixed inset-y-0 right-0 z-50 flex h-full w-full max-w-[560px] flex-col",
46
+ "border-edge border-l bg-surface-2 shadow-[var(--shadow-dialog)]",
47
+ className,
48
+ )}
49
+ {...props}
50
+ >
51
+ {children}
52
+ </DrawerPrimitive.Content>
53
+ </DrawerPortal>
54
+ ));
55
+ DrawerContent.displayName = "DrawerContent";
56
+
57
+ /** Not previously exported — `vaul`'s `Content` renders Radix Dialog's
58
+ * `Content` underneath (`vaul/dist/index.js` imports `@radix-ui/react-
59
+ * dialog` directly and forwards its `Content`/`Title`/`Description`), so
60
+ * Radix's own "DialogContent requires a DialogTitle" dev warning applies
61
+ * here exactly as it does to `primitives/dialog.tsx`. `QuickDetailDrawer`/
62
+ * `MoreDetailDrawer` below always render one; exported separately too for
63
+ * any future one-off use of the generic `DrawerContent` above. */
64
+ export const DrawerTitle = forwardRef<
65
+ React.ElementRef<typeof DrawerPrimitive.Title>,
66
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
67
+ >(({ className, ...props }, ref) => (
68
+ <DrawerPrimitive.Title
69
+ ref={ref}
70
+ className={cn("font-medium text-foreground text-title-sm", className)}
71
+ {...props}
72
+ />
73
+ ));
74
+ DrawerTitle.displayName = "DrawerTitle";
75
+
76
+ /** See `DrawerTitle` — same "not previously exported, Radix warns without
77
+ * one" reasoning, for `aria-describedby` instead of the accessible name. */
78
+ export const DrawerDescription = forwardRef<
79
+ React.ElementRef<typeof DrawerPrimitive.Description>,
80
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
81
+ >(({ className, ...props }, ref) => (
82
+ <DrawerPrimitive.Description
83
+ ref={ref}
84
+ className={cn("text-muted-foreground text-prose", className)}
85
+ {...props}
86
+ />
87
+ ));
88
+ DrawerDescription.displayName = "DrawerDescription";
89
+
90
+ // ---------------------------------------------------------------------
91
+ // QuickDetailDrawer / MoreDetailDrawer (console-redesign.md §3, D14, §5)
92
+ // ---------------------------------------------------------------------
93
+ //
94
+ // The design doc's own §6.4 sketch of this file was explicitly flagged as
95
+ // unverified ("the mechanism is now known and worth checking before this
96
+ // sketch is trusted verbatim") for two independent reasons, both checked
97
+ // directly against `vaul@1.1.2`'s compiled source
98
+ // (`node_modules/vaul/dist/index.js`) before writing this, not assumed:
99
+ //
100
+ // 1. **The `useMediaQuery` call the sketch used is gone (D12).** No JS
101
+ // viewport read of any kind happens in this file — every breakpoint
102
+ // switch below is a plain Tailwind `md:` variant, so both variants
103
+ // render identically on the server and on the client's first paint.
104
+ //
105
+ // 2. **A single `vaul` `Drawer.Root` cannot itself be "bottom on phone,
106
+ // right on desktop" — its `direction` prop is not a CSS concern.** It
107
+ // drives real per-instance state: which axis (`translate3d(0,y,0)` vs
108
+ // `(x,0,0)`) the built-in open/close keyframes and the live drag-follow
109
+ // math use (`isVertical(direction)`, `getTranslate(el, direction)`,
110
+ // throughout `dist/index.js`). The sketch never set `direction` at all
111
+ // (it left every caller to supply it on a separately-rendered `<Drawer>`
112
+ // Root, which — combined with `direction`'s own default of `"bottom"` —
113
+ // means the sketch's "more details" `Content` would in practice always
114
+ // have inherited `"bottom"` regardless of viewport anyway). Rather than
115
+ // read the viewport to pick a direction (banned, and also the same
116
+ // hydration-mismatch shape D12 already fixed once — a deep-linked
117
+ // `?panel=<id>` drawer can be `open` on the very first server render,
118
+ // per D14, so the first paint has to be right, not just eventually
119
+ // correct after an effect), both drawers below **fix `direction` to
120
+ // `"bottom"` unconditionally** and let plain `md:` classes handle
121
+ // *position* — matching this file's own established, precedented
122
+ // pattern for a responsive split that can't be one reactive value
123
+ // (`side-nav.tsx`'s `GroupSection`: "two parallel trees... not one tree
124
+ // whose content changes based on a JS-read viewport width").
125
+ //
126
+ // The one real, accepted cost of fixing `direction`: `vaul`'s own
127
+ // open/close animation (`slideFromBottom`/`slideToBottom`, a vertical
128
+ // `translate3d`) plays at every breakpoint, including the `md:` right-
129
+ // panel position — so the panel's *entrance motion* is vertical even
130
+ // though its *resting position* is a right-hand edge. This was
131
+ // deliberately not "fixed" by layering a second, horizontal CSS
132
+ // `transition` on top keyed off `data-[state=]`: `vaul`'s animation is
133
+ // a real `@keyframes` `animation`, which (unlike a plain `transition`)
134
+ // correctly plays on a freshly-inserted node — Radix's `Content` isn't
135
+ // in the DOM at all until `open`, so a competing `transition`-based
136
+ // replacement would very likely not animate on *open* at all (no prior
137
+ // DOM node to transition *from*) while still fighting the real
138
+ // animation for the `transform` property. Keeping `vaul`'s own,
139
+ // already-correct mechanism and accepting a vertical entrance for the
140
+ // desktop panel is the smaller, more honest compromise, and it costs no
141
+ // extra CSS (constraint 7). If this reads as wrong once seen live,
142
+ // revisit by disabling it explicitly (`data-vaul-animate="false"` is
143
+ // force-overridable via a prop — `Content`'s `...rest` spreads *after*
144
+ // vaul's own default in `dist/index.js`) and shipping a real
145
+ // `@keyframes`-based replacement, not a `transition`.
146
+ //
147
+ // Fixing `direction` also fixes the *drag* gesture to the vertical
148
+ // axis everywhere. Left alone, that would make a click-drag inside the
149
+ // desktop panel (e.g. selecting text) register as a vertical swipe
150
+ // attempt. Closed with `handleOnly` on the `Root` (drag can only start
151
+ // from a `Drawer.Handle`) plus rendering that handle only below `md:` —
152
+ // so desktop has no drag surface at all (pointer/click/Escape/overlay
153
+ // close it instead), and phone keeps a real, correctly-axised drag
154
+ // handle, which is also what §3's own mobile paragraph asks for ("...
155
+ // with a drag handle").
156
+ //
157
+ // Both variants are otherwise identical in mechanism and differ only in
158
+ // the two things §3/D14 make load-bearing: `dimmed` (renders an
159
+ // `Overlay` and sets `vaul`'s own `modal` prop — verified live in
160
+ // `dist/index.js` that `modal` is what actually gates both the
161
+ // background `pointer-events`/scroll-lock behaviour *and*, independently,
162
+ // whether an `Overlay` should render at all; Radix's own focus trap and
163
+ // `aria-modal="true"` are unconditional either way — `vaul` never forwards
164
+ // its `modal` prop down to `@radix-ui/react-dialog`'s `Root`, which keeps
165
+ // its own default of `true` regardless. So "quick details" is visually and
166
+ // interactively lighter — no dim, background stays scrollable/clickable —
167
+ // but is exactly as keyboard-trapped and exactly as announced to a screen
168
+ // reader as "more details" is; that's a real, verified limitation of
169
+ // `vaul@1.1.2`, not a gap left open on purpose) and `contentClassName`
170
+ // (width/inset/radius/border, per D14's own two size ranges).
171
+ interface DetailDrawerProps {
172
+ open: boolean;
173
+ onOpenChange: (open: boolean) => void;
174
+ /** Rendered as the drawer's accessible name (`Drawer.Title`) — required,
175
+ * not optional, because both variants always render one and Radix warns
176
+ * loudly in dev without it. */
177
+ title: ReactNode;
178
+ /** Rendered as the drawer's accessible description (`Drawer.Description`).
179
+ * Optional for the *caller* — when omitted, a screen-reader-only
180
+ * fallback is still rendered so `aria-describedby` is always wired,
181
+ * matching `primitives/dialog.tsx`'s own precedent of never shipping a
182
+ * `Content` with no description at all. */
183
+ description?: ReactNode;
184
+ /** The scrollable body — a summary of fields, an edit form, whatever the
185
+ * calling screen owns. This file has no opinion on it. */
186
+ children: ReactNode;
187
+ /** Optional sticky action row (§1.5's Polar reference: "a fixed bottom
188
+ * action row... stays visible without the form needing to scroll"). */
189
+ footer?: ReactNode;
190
+ className?: string;
191
+ }
192
+
193
+ function DetailDrawerContent({
194
+ open,
195
+ onOpenChange,
196
+ title,
197
+ description,
198
+ children,
199
+ footer,
200
+ className,
201
+ dimmed,
202
+ contentClassName,
203
+ }: DetailDrawerProps & { dimmed: boolean; contentClassName: string }) {
204
+ return (
205
+ <DrawerPrimitive.Root
206
+ open={open}
207
+ onOpenChange={onOpenChange}
208
+ direction="bottom"
209
+ modal={dimmed}
210
+ dismissible
211
+ handleOnly
212
+ // vaul defaults this to `false` (`onOpenAutoFocus` is pre-vented
213
+ // unless the caller opts in) — verified in `dist/index.js`'s `Root`
214
+ // default parameters. Without it, opening either drawer would leave
215
+ // keyboard focus sitting on whatever triggered it instead of moving
216
+ // into the panel, which is a real, checkable regression from plain
217
+ // Radix `Dialog` behaviour (its own default *does* auto-focus
218
+ // `Content`) — set explicitly so focus genuinely lands inside the
219
+ // drawer the moment it opens, not just gets trapped there once the
220
+ // user starts tabbing.
221
+ autoFocus
222
+ >
223
+ <DrawerPrimitive.Portal>
224
+ {dimmed && <DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50" />}
225
+ <DrawerPrimitive.Content
226
+ className={cn(
227
+ "fixed z-50 flex flex-col bg-surface-2 shadow-[var(--shadow-dialog)] outline-none",
228
+ // Phone: bottom sheet.
229
+ "inset-x-0 bottom-0 rounded-t-box border-edge border-t",
230
+ // `md`+: right-hand panel — every sheet-only property above is
231
+ // reset explicitly (inset, rounding, border side, max-height),
232
+ // not just overridden by a wider max-width, per this file's
233
+ // own D12-correction note above.
234
+ "md:inset-x-auto md:inset-y-0 md:right-0 md:h-full md:max-h-none",
235
+ "md:w-full md:rounded-t-none md:rounded-l-box md:border-t-0 md:border-l",
236
+ contentClassName,
237
+ className,
238
+ )}
239
+ >
240
+ {/* Drag surface (D14's mobile "with a drag handle"), phone only —
241
+ `handleOnly` on Root means dragging is only possible starting
242
+ from this element, and it's hidden at `md:`, so desktop has
243
+ no drag surface at all (see this file's own header comment
244
+ for why: `direction` is fixed to "bottom", so a desktop drag
245
+ would otherwise compute against the wrong axis).
246
+ **`!` (Tailwind v4 important) is load-bearing here, not
247
+ decorative — found live, not assumed:** `vaul` injects
248
+ `[data-vaul-handle]{display:block; ...}` into a `<style>` tag
249
+ it appends to `<head>` itself (`dist/index.js`'s own
250
+ `__insertCSS`), at the same specificity as a plain `md:hidden`
251
+ utility and *after* Tailwind's build-time stylesheet in
252
+ cascade order — so a bare `md:hidden` lost the tie and the
253
+ handle stayed visible at desktop width, confirmed by
254
+ inspecting `getComputedStyle(...).display` in a real browser
255
+ at 1280px before this fix, and confirmed gone after it. */}
256
+ <DrawerPrimitive.Handle className="mt-2 shrink-0 bg-edge-strong md:hidden!" />
257
+
258
+ <div className="flex items-start justify-between gap-3 border-edge border-b px-5 py-4">
259
+ <div className="min-w-0">
260
+ <DrawerTitle className="truncate">{title}</DrawerTitle>
261
+ <DrawerDescription className={description == null ? "sr-only" : "mt-1"}>
262
+ {description ?? "Details panel."}
263
+ </DrawerDescription>
264
+ </div>
265
+ <DrawerPrimitive.Close
266
+ aria-label="Close"
267
+ className="shrink-0 text-subtle-foreground hover:text-foreground"
268
+ >
269
+ <X size={16} strokeWidth={1.5} />
270
+ </DrawerPrimitive.Close>
271
+ </div>
272
+
273
+ <div className="flex-1 overflow-y-auto px-5 py-4">{children}</div>
274
+
275
+ {footer != null && (
276
+ <div className="flex shrink-0 items-center justify-end gap-2 border-edge border-t px-5 py-4">
277
+ {footer}
278
+ </div>
279
+ )}
280
+ </DrawerPrimitive.Content>
281
+ </DrawerPrimitive.Portal>
282
+ </DrawerPrimitive.Root>
283
+ );
284
+ }
285
+
286
+ /**
287
+ * **Quick details** (§3, D14) — a peek at one row's state without leaving
288
+ * the list. Narrow (`420–480px` at `md`+), undimmed (background stays
289
+ * legible and, per `vaul`'s own `modal={false}` behaviour verified above,
290
+ * scrollable/clickable — matching the Mercury reference, §1.4), no route
291
+ * ownership: the caller owns `open`/`onOpenChange` from local state (e.g.
292
+ * "which row id is selected"), and losing it on refresh is expected and
293
+ * fine (reopening is one click on the same row).
294
+ *
295
+ * Not a `Dialog`, and not for anything destructive — §1.7/§3 reserve that
296
+ * for the centered `Dialog` primitive. This is a read-mostly summary with
297
+ * 1–2 actions, e.g. a "View full details" link that upgrades to
298
+ * `MoreDetailDrawer`.
299
+ */
300
+ export function QuickDetailDrawer(props: DetailDrawerProps) {
301
+ return (
302
+ <DetailDrawerContent
303
+ {...props}
304
+ dimmed={false}
305
+ contentClassName="max-h-[70vh] md:max-w-[440px]"
306
+ />
307
+ );
308
+ }
309
+
310
+ /**
311
+ * **More details** (§3, D14) — the full record: every field, an edit
312
+ * form, destructive actions, short nested history. Wide (`640–720px` at
313
+ * `md`+), dimmed (modal-weight, matching the Polar reference, §1.5). The
314
+ * *caller* is expected to own a shallow `?panel=<recordId>` route so this
315
+ * survives refresh and is linkable (D14) — this component only owns the
316
+ * visual/behavioural weight, never routing; pass `open` derived from that
317
+ * query param and `onOpenChange` wired to update it.
318
+ *
319
+ * A destructive-confirmation step opened *from inside* this drawer (e.g.
320
+ * webhook-secret rotation) stays a nested `Dialog` (§3's own footnote) —
321
+ * that dialog needs a z-index at or above this drawer's `z-50`; setting
322
+ * that is `primitives/dialog.tsx`'s job (§8 risk list), not this file's.
323
+ */
324
+ export function MoreDetailDrawer(props: DetailDrawerProps) {
325
+ return <DetailDrawerContent {...props} dimmed contentClassName="max-h-[92vh] md:max-w-[680px]" />;
326
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import {
4
+ Menu,
5
+ MenuButton,
6
+ MenuItem,
7
+ MenuItems,
8
+ MenuSection,
9
+ MenuSeparator,
10
+ } from "@headlessui/react";
11
+ import { Check } from "lucide-react";
12
+ import type { ComponentPropsWithoutRef } from "react";
13
+ import { cn } from "../../lib/cn";
14
+ import { omitUndefined } from "../../lib/omit-undefined";
15
+
16
+ // D3: Radix `DropdownMenu` → Headless UI `Menu`/`MenuButton`/`MenuItems`/`MenuItem`.
17
+ export const DropdownMenu = Menu;
18
+
19
+ // `MenuButton` is natively polymorphic (`as`), replacing Radix's `asChild`
20
+ // pattern (D4's same call, applied to a trigger rather than `Button`
21
+ // itself): `<DropdownMenuTrigger as={Button} variant="secondary">…`.
22
+ export const DropdownMenuTrigger = MenuButton;
23
+
24
+ // `DropdownMenuGroup`/`DropdownMenuCheckboxItem` have zero call sites in
25
+ // this codebase today (grepped across `admin/` and `packages/ui`) — kept
26
+ // for API parity rather than dropped silently, mapped onto Headless UI's
27
+ // closest equivalents.
28
+ export const DropdownMenuGroup = MenuSection;
29
+
30
+ export function DropdownMenuSeparator({
31
+ className,
32
+ ...props
33
+ }: ComponentPropsWithoutRef<typeof MenuSeparator>) {
34
+ return <MenuSeparator className={cn("my-1 h-px bg-edge", className)} {...props} />;
35
+ }
36
+
37
+ export function DropdownMenuContent({
38
+ className,
39
+ ...props
40
+ }: ComponentPropsWithoutRef<typeof MenuItems>) {
41
+ return (
42
+ <MenuItems
43
+ anchor="bottom start"
44
+ transition
45
+ className={cn(
46
+ "z-50 min-w-[180px] rounded-md border border-edge bg-surface-2 p-1 shadow-[var(--shadow-popover)] [--anchor-gap:4px] focus:outline-none",
47
+ "origin-top transition duration-100 ease-out data-closed:scale-95 data-closed:opacity-0",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ export function DropdownMenuItem({
56
+ className,
57
+ ...props
58
+ }: Omit<ComponentPropsWithoutRef<"button">, "type">) {
59
+ return (
60
+ <MenuItem
61
+ as="button"
62
+ type="button"
63
+ className={cn(
64
+ "flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-body text-foreground outline-none",
65
+ "data-focus:bg-surface-3",
66
+ "data-disabled:pointer-events-none data-disabled:opacity-50",
67
+ className,
68
+ )}
69
+ {...omitUndefined(props)}
70
+ />
71
+ );
72
+ }
73
+
74
+ /** Unconsumed today (see module doc above) — caller controls `checked`
75
+ * itself, since Headless UI's `Menu` has no built-in checkbox-item state
76
+ * the way Radix's `DropdownMenuCheckboxItem` did. */
77
+ export function DropdownMenuCheckboxItem({
78
+ className,
79
+ checked = false,
80
+ children,
81
+ ...props
82
+ }: Omit<ComponentPropsWithoutRef<"button">, "type"> & { checked?: boolean }) {
83
+ return (
84
+ <MenuItem
85
+ as="button"
86
+ type="button"
87
+ role="menuitemcheckbox"
88
+ aria-checked={checked}
89
+ className={cn(
90
+ "flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-body text-foreground outline-none",
91
+ "data-focus:bg-surface-3",
92
+ className,
93
+ )}
94
+ {...omitUndefined(props)}
95
+ >
96
+ <span className="flex h-3.5 w-3.5 items-center justify-center">
97
+ {checked && <Check size={14} strokeWidth={1.5} aria-hidden="true" />}
98
+ </span>
99
+ {children}
100
+ </MenuItem>
101
+ );
102
+ }
103
+
104
+ /**
105
+ * **Found live, not assumed**: Headless UI's own `MenuHeading` throws the
106
+ * identical "not inside a relevant parent" error `label.tsx`'s own module
107
+ * doc already found for the standalone `Label` — `MenuHeading` needs a
108
+ * `MenuSection` ancestor to supply that context (`MenuSection`'s own
109
+ * implementation calls `useLabels()` to *create* the provider; `MenuHeading`
110
+ * calls `useLabelContext()` to *consume* it), and every call site here
111
+ * (matching the original Radix `DropdownMenuLabel`'s own shape) renders a
112
+ * standalone label with no surrounding group. Radix's own `Label` was a
113
+ * plain, non-interactive `<div>` with no roving-focus/ARIA-grouping
114
+ * behaviour beyond being read as ordinary text inside the menu's
115
+ * accessible tree — so a plain `<div>` here loses nothing real and avoids
116
+ * a dependency this element doesn't actually need. Reproduced live: a real
117
+ * "Uncaught Error: You used a <Label /> component, but it is not inside a
118
+ * relevant parent" client-side exception, thrown the instant the gallery's
119
+ * demo menu opened, before this fix.
120
+ */
121
+ export function DropdownMenuLabel({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
122
+ return (
123
+ <div
124
+ className={cn("px-2 py-1.5 text-micro text-muted-foreground tracking-[0.03em]", className)}
125
+ {...props}
126
+ />
127
+ );
128
+ }
@@ -0,0 +1,136 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../../lib/cn";
3
+ import { Label } from "./label";
4
+
5
+ /**
6
+ * A labelled form control with an optional validation error beneath it.
7
+ *
8
+ * This replaces the console's two most-duplicated class strings after the
9
+ * table-column ones:
10
+ *
11
+ * - `"flex flex-col gap-1.5"` — **80 occurrences**, every one of them
12
+ * wrapping exactly `<Label htmlFor>` + one control (+ sometimes an
13
+ * error paragraph).
14
+ * - `"text-caption text-state-danger-fg"` — **35 occurrences**, almost all
15
+ * of them that error paragraph.
16
+ *
17
+ * The shape was identical at every site, checked before this component was
18
+ * designed rather than assumed:
19
+ *
20
+ * ```tsx
21
+ * <div className="flex flex-col gap-1.5">
22
+ * <Label htmlFor="record-msisdn">MSISDN</Label>
23
+ * <Input id="record-msisdn" {...form.register("msisdn")} />
24
+ * <p className="text-caption text-state-danger-fg">{errors.msisdn.message}</p>
25
+ * </div>
26
+ * ```
27
+ *
28
+ * becomes
29
+ *
30
+ * ```tsx
31
+ * <FormField label="MSISDN" htmlFor="record-msisdn" error={errors.msisdn?.message}>
32
+ * <Input id="record-msisdn" {...form.register("msisdn")} />
33
+ * </FormField>
34
+ * ```
35
+ *
36
+ * **Why a semantic component and not a generic `<Stack gap="1.5">`.** A
37
+ * layout primitive parameterised by its own CSS is a `<div>` with extra
38
+ * steps — it moves the class from the call site into a prop at the call
39
+ * site, which is not factorisation. `FormField` instead names the *thing*
40
+ * (a labelled control with an error slot), so the spacing, the label
41
+ * treatment and the error treatment are one decision in one place, and a
42
+ * caller cannot get the error styling wrong by writing the `<p>` itself.
43
+ *
44
+ * `htmlFor` is required rather than optional. Several of the 80 sites had
45
+ * a `<Label>` with no `htmlFor` at all, which silently breaks
46
+ * click-to-focus and screen-reader association; making it required turns
47
+ * that into a compile error instead of an accessibility bug nobody
48
+ * notices.
49
+ */
50
+ export interface FormFieldProps {
51
+ label: ReactNode;
52
+ /** The `id` of the control this labels. Required — see the module doc. */
53
+ htmlFor: string;
54
+ /** Validation message. `undefined` renders no error element at all. */
55
+ error?: string | undefined;
56
+ /** Help text rendered between the label and the control. */
57
+ hint?: ReactNode;
58
+ /**
59
+ * `"field"` (default) labels a single form control by `htmlFor`.
60
+ *
61
+ * `"group"` is for a `RadioGroup`/`ChipSelect`, where the control is a
62
+ * `role="radiogroup"`/`role="group"` wrapper rather than a labelable
63
+ * element. HTML's `for` only associates with labelable form controls, so
64
+ * pointing it at a `<div>` is invalid *and* dangling — which is exactly
65
+ * what the enum migration shipped: four `<Label for="…">` with no
66
+ * element carrying that id anywhere, found in review.
67
+ *
68
+ * In group mode no `for` is emitted; the label carries
69
+ * [`groupLabelId(htmlFor)`] instead, and the caller wires the group with
70
+ * `aria-labelledby={groupLabelId(htmlFor)}`. Both sides derive the id
71
+ * from the same function so they cannot drift.
72
+ */
73
+ control?: "field" | "group";
74
+ children: ReactNode;
75
+ className?: string | undefined;
76
+ }
77
+
78
+ /** The id a `control="group"` FormField puts on its label, and the value a
79
+ * grouped control must pass as `aria-labelledby`. One function so the two
80
+ * sides cannot disagree. */
81
+ export function groupLabelId(htmlFor: string): string {
82
+ return `${htmlFor}-label`;
83
+ }
84
+
85
+ export function FormField({
86
+ label,
87
+ htmlFor,
88
+ error,
89
+ hint,
90
+ children,
91
+ className,
92
+ control = "field",
93
+ }: FormFieldProps) {
94
+ return (
95
+ <div className={cn("flex flex-col gap-1.5", className)}>
96
+ {control === "group" ? (
97
+ // Not a <Label>: with no `for` it would associate with nothing, and
98
+ // a label wrapping no control is worse than a plain element with an
99
+ // id the group points back at.
100
+ <span id={groupLabelId(htmlFor)} className="font-medium text-body text-foreground">
101
+ {label}
102
+ </span>
103
+ ) : (
104
+ <Label htmlFor={htmlFor}>{label}</Label>
105
+ )}
106
+ {hint !== undefined && <p className="text-caption text-muted-foreground">{hint}</p>}
107
+ {children}
108
+ {error !== undefined && <FieldError>{error}</FieldError>}
109
+ </div>
110
+ );
111
+ }
112
+
113
+ /**
114
+ * A validation message.
115
+ *
116
+ * Exported separately because a handful of the 35 sites are not inside a
117
+ * `FormField` — a form-level error above the submit button, or an error
118
+ * attached to a control group rather than one input. Those should still
119
+ * use the same treatment rather than re-inlining the class.
120
+ *
121
+ * `role="alert"` so the message is announced when it appears, which none
122
+ * of the 35 hand-rolled `<p>` elements did.
123
+ */
124
+ export function FieldError({
125
+ children,
126
+ className,
127
+ }: {
128
+ children: ReactNode;
129
+ className?: string | undefined;
130
+ }) {
131
+ return (
132
+ <p role="alert" className={cn("text-caption text-state-danger-fg", className)}>
133
+ {children}
134
+ </p>
135
+ );
136
+ }