@inshorts/glint-ui 0.5.4 → 0.6.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 (340) hide show
  1. package/README.md +3 -1
  2. package/dist/alert.cjs +96 -0
  3. package/dist/alert.cjs.map +1 -0
  4. package/dist/alert.d.cts +29 -0
  5. package/dist/alert.d.ts +29 -0
  6. package/dist/alert.js +10 -0
  7. package/dist/alert.js.map +1 -0
  8. package/dist/autocomplete.cjs +578 -0
  9. package/dist/autocomplete.cjs.map +1 -0
  10. package/dist/autocomplete.d.cts +56 -0
  11. package/dist/autocomplete.d.ts +56 -0
  12. package/dist/autocomplete.js +11 -0
  13. package/dist/autocomplete.js.map +1 -0
  14. package/dist/badge.cjs +89 -0
  15. package/dist/badge.cjs.map +1 -0
  16. package/dist/badge.d.cts +25 -0
  17. package/dist/badge.d.ts +25 -0
  18. package/dist/badge.js +10 -0
  19. package/dist/badge.js.map +1 -0
  20. package/dist/breadcrumb.cjs +88 -0
  21. package/dist/breadcrumb.cjs.map +1 -0
  22. package/dist/breadcrumb.d.cts +14 -0
  23. package/dist/breadcrumb.d.ts +14 -0
  24. package/dist/breadcrumb.js +8 -0
  25. package/dist/breadcrumb.js.map +1 -0
  26. package/dist/button.cjs +143 -0
  27. package/dist/button.cjs.map +1 -0
  28. package/dist/button.d.cts +34 -0
  29. package/dist/button.d.ts +34 -0
  30. package/dist/button.js +9 -0
  31. package/dist/button.js.map +1 -0
  32. package/dist/card.cjs +111 -0
  33. package/dist/card.cjs.map +1 -0
  34. package/dist/card.d.cts +15 -0
  35. package/dist/card.d.ts +15 -0
  36. package/dist/card.js +18 -0
  37. package/dist/card.js.map +1 -0
  38. package/dist/checkbox.cjs +66 -0
  39. package/dist/checkbox.cjs.map +1 -0
  40. package/{src/checkbox.tsx → dist/checkbox.d.cts} +5 -22
  41. package/dist/checkbox.d.ts +25 -0
  42. package/dist/checkbox.js +10 -0
  43. package/dist/checkbox.js.map +1 -0
  44. package/dist/chunk-43ZUFY63.js +85 -0
  45. package/dist/chunk-43ZUFY63.js.map +1 -0
  46. package/dist/chunk-67ACNS42.js +29 -0
  47. package/dist/chunk-67ACNS42.js.map +1 -0
  48. package/dist/chunk-6URKHMCF.js +76 -0
  49. package/dist/chunk-6URKHMCF.js.map +1 -0
  50. package/dist/chunk-ASU6CUTZ.js +85 -0
  51. package/dist/chunk-ASU6CUTZ.js.map +1 -0
  52. package/dist/chunk-EWSBFU54.js +53 -0
  53. package/dist/chunk-EWSBFU54.js.map +1 -0
  54. package/dist/chunk-G4ZQUSAJ.js +38 -0
  55. package/dist/chunk-G4ZQUSAJ.js.map +1 -0
  56. package/dist/chunk-GMCUXEFI.js +67 -0
  57. package/dist/chunk-GMCUXEFI.js.map +1 -0
  58. package/dist/chunk-HBBOR43A.js +85 -0
  59. package/dist/chunk-HBBOR43A.js.map +1 -0
  60. package/dist/chunk-HCA5ODCD.js +11 -0
  61. package/dist/chunk-HCA5ODCD.js.map +1 -0
  62. package/dist/chunk-HYRJYUPI.js +26 -0
  63. package/dist/chunk-HYRJYUPI.js.map +1 -0
  64. package/dist/chunk-IRCTGJQB.js +26 -0
  65. package/dist/chunk-IRCTGJQB.js.map +1 -0
  66. package/dist/chunk-JDE5XYPT.js +30 -0
  67. package/dist/chunk-JDE5XYPT.js.map +1 -0
  68. package/dist/chunk-JSJEEKR3.js +59 -0
  69. package/dist/chunk-JSJEEKR3.js.map +1 -0
  70. package/dist/chunk-KR3BCFP7.js +85 -0
  71. package/dist/chunk-KR3BCFP7.js.map +1 -0
  72. package/dist/chunk-MQT5RAQV.js +202 -0
  73. package/dist/chunk-MQT5RAQV.js.map +1 -0
  74. package/dist/chunk-MUPW2TFJ.js +102 -0
  75. package/dist/chunk-MUPW2TFJ.js.map +1 -0
  76. package/dist/chunk-PM2VE7DJ.js +100 -0
  77. package/dist/chunk-PM2VE7DJ.js.map +1 -0
  78. package/dist/chunk-PPYR3O3L.js +44 -0
  79. package/dist/chunk-PPYR3O3L.js.map +1 -0
  80. package/dist/chunk-PUTC52SI.js +98 -0
  81. package/dist/chunk-PUTC52SI.js.map +1 -0
  82. package/dist/chunk-PVK4WQJZ.js +513 -0
  83. package/dist/chunk-PVK4WQJZ.js.map +1 -0
  84. package/dist/chunk-PWAKTEXF.js +18 -0
  85. package/dist/chunk-PWAKTEXF.js.map +1 -0
  86. package/dist/chunk-QTFWORA7.js +26 -0
  87. package/dist/chunk-QTFWORA7.js.map +1 -0
  88. package/dist/chunk-RUPL2H6V.js +49 -0
  89. package/dist/chunk-RUPL2H6V.js.map +1 -0
  90. package/dist/chunk-S7PVZAPL.js +28 -0
  91. package/dist/chunk-S7PVZAPL.js.map +1 -0
  92. package/dist/chunk-SQDV2AKJ.js +49 -0
  93. package/dist/chunk-SQDV2AKJ.js.map +1 -0
  94. package/dist/chunk-T2KXVH3O.js +297 -0
  95. package/dist/chunk-T2KXVH3O.js.map +1 -0
  96. package/dist/chunk-T4WCCHJQ.js +29 -0
  97. package/dist/chunk-T4WCCHJQ.js.map +1 -0
  98. package/dist/chunk-UC3ZIQPE.js +93 -0
  99. package/dist/chunk-UC3ZIQPE.js.map +1 -0
  100. package/dist/chunk-UE7JY7VR.js +28 -0
  101. package/dist/chunk-UE7JY7VR.js.map +1 -0
  102. package/dist/chunk-WVNRFO5F.js +70 -0
  103. package/dist/chunk-WVNRFO5F.js.map +1 -0
  104. package/dist/chunk-Y5WCT6XN.js +93 -0
  105. package/dist/chunk-Y5WCT6XN.js.map +1 -0
  106. package/dist/chunk-YDICVETX.js +151 -0
  107. package/dist/chunk-YDICVETX.js.map +1 -0
  108. package/dist/chunk-YSU4VWRD.js +90 -0
  109. package/dist/chunk-YSU4VWRD.js.map +1 -0
  110. package/dist/chunk-ZFSQVMLJ.js +126 -0
  111. package/dist/chunk-ZFSQVMLJ.js.map +1 -0
  112. package/dist/chunk-ZHATD73Y.js +46 -0
  113. package/dist/chunk-ZHATD73Y.js.map +1 -0
  114. package/dist/chunk-ZLAA4DGG.js +111 -0
  115. package/dist/chunk-ZLAA4DGG.js.map +1 -0
  116. package/dist/chunk-ZP252OJZ.js +56 -0
  117. package/dist/chunk-ZP252OJZ.js.map +1 -0
  118. package/dist/clipboard.cjs +125 -0
  119. package/dist/clipboard.cjs.map +1 -0
  120. package/dist/clipboard.d.cts +52 -0
  121. package/dist/clipboard.d.ts +52 -0
  122. package/dist/clipboard.js +23 -0
  123. package/dist/clipboard.js.map +1 -0
  124. package/dist/collapsible.cjs +94 -0
  125. package/dist/collapsible.cjs.map +1 -0
  126. package/dist/collapsible.d.cts +21 -0
  127. package/dist/collapsible.d.ts +21 -0
  128. package/dist/collapsible.js +9 -0
  129. package/dist/collapsible.js.map +1 -0
  130. package/dist/confirm-dialog.cjs +369 -0
  131. package/dist/confirm-dialog.cjs.map +1 -0
  132. package/dist/confirm-dialog.d.cts +16 -0
  133. package/dist/confirm-dialog.d.ts +16 -0
  134. package/dist/confirm-dialog.js +13 -0
  135. package/dist/confirm-dialog.js.map +1 -0
  136. package/dist/copy-button.cjs +102 -0
  137. package/dist/copy-button.cjs.map +1 -0
  138. package/dist/copy-button.d.cts +18 -0
  139. package/dist/copy-button.d.ts +18 -0
  140. package/dist/copy-button.js +10 -0
  141. package/dist/copy-button.js.map +1 -0
  142. package/dist/data-table.cjs +140 -0
  143. package/dist/data-table.cjs.map +1 -0
  144. package/dist/data-table.d.cts +81 -0
  145. package/dist/data-table.d.ts +81 -0
  146. package/dist/data-table.js +8 -0
  147. package/dist/data-table.js.map +1 -0
  148. package/dist/date-range-picker/dateRange.cjs +138 -0
  149. package/dist/date-range-picker/dateRange.cjs.map +1 -0
  150. package/dist/date-range-picker/dateRange.d.cts +57 -0
  151. package/dist/date-range-picker/dateRange.d.ts +57 -0
  152. package/dist/date-range-picker/dateRange.js +31 -0
  153. package/dist/date-range-picker/dateRange.js.map +1 -0
  154. package/dist/date-range-picker/index.cjs +375 -0
  155. package/dist/date-range-picker/index.cjs.map +1 -0
  156. package/dist/date-range-picker/index.d.cts +18 -0
  157. package/dist/date-range-picker/index.d.ts +18 -0
  158. package/dist/date-range-picker/index.js +10 -0
  159. package/dist/date-range-picker/index.js.map +1 -0
  160. package/dist/dialogLayer.cjs +121 -0
  161. package/dist/dialogLayer.cjs.map +1 -0
  162. package/dist/dialogLayer.d.cts +11 -0
  163. package/dist/dialogLayer.d.ts +11 -0
  164. package/dist/dialogLayer.js +8 -0
  165. package/dist/dialogLayer.js.map +1 -0
  166. package/dist/feature-access-gate.cjs +160 -0
  167. package/dist/feature-access-gate.cjs.map +1 -0
  168. package/dist/feature-access-gate.d.cts +14 -0
  169. package/dist/feature-access-gate.d.ts +14 -0
  170. package/dist/feature-access-gate.js +11 -0
  171. package/dist/feature-access-gate.js.map +1 -0
  172. package/dist/field.cjs +58 -0
  173. package/dist/field.cjs.map +1 -0
  174. package/dist/field.d.cts +23 -0
  175. package/dist/field.d.ts +23 -0
  176. package/dist/field.js +8 -0
  177. package/dist/field.js.map +1 -0
  178. package/dist/image-uploader.cjs +293 -0
  179. package/dist/image-uploader.cjs.map +1 -0
  180. package/dist/image-uploader.d.cts +34 -0
  181. package/dist/image-uploader.d.ts +34 -0
  182. package/dist/image-uploader.js +12 -0
  183. package/dist/image-uploader.js.map +1 -0
  184. package/dist/index.cjs +3012 -0
  185. package/dist/index.cjs.map +1 -0
  186. package/dist/index.d.cts +41 -0
  187. package/dist/index.d.ts +41 -0
  188. package/dist/index.js +232 -0
  189. package/dist/index.js.map +1 -0
  190. package/dist/input.cjs +58 -0
  191. package/dist/input.cjs.map +1 -0
  192. package/dist/input.d.cts +7 -0
  193. package/dist/input.d.ts +7 -0
  194. package/dist/input.js +10 -0
  195. package/dist/input.js.map +1 -0
  196. package/dist/label.cjs +67 -0
  197. package/dist/label.cjs.map +1 -0
  198. package/dist/label.d.cts +7 -0
  199. package/dist/label.d.ts +7 -0
  200. package/dist/label.js +9 -0
  201. package/dist/label.js.map +1 -0
  202. package/dist/modal.cjs +212 -0
  203. package/dist/modal.cjs.map +1 -0
  204. package/dist/modal.d.cts +39 -0
  205. package/dist/modal.d.ts +39 -0
  206. package/dist/modal.js +12 -0
  207. package/dist/modal.js.map +1 -0
  208. package/dist/page-header.cjs +78 -0
  209. package/dist/page-header.cjs.map +1 -0
  210. package/dist/page-header.d.cts +35 -0
  211. package/dist/page-header.d.ts +35 -0
  212. package/dist/page-header.js +8 -0
  213. package/dist/page-header.js.map +1 -0
  214. package/dist/pagination.cjs +237 -0
  215. package/dist/pagination.cjs.map +1 -0
  216. package/dist/pagination.d.cts +15 -0
  217. package/dist/pagination.d.ts +15 -0
  218. package/dist/pagination.js +11 -0
  219. package/dist/pagination.js.map +1 -0
  220. package/dist/panel-brand-mark.cjs +59 -0
  221. package/dist/panel-brand-mark.cjs.map +1 -0
  222. package/dist/panel-brand-mark.d.cts +13 -0
  223. package/dist/panel-brand-mark.d.ts +13 -0
  224. package/dist/panel-brand-mark.js +8 -0
  225. package/dist/panel-brand-mark.js.map +1 -0
  226. package/dist/panel-sign-in.cjs +256 -0
  227. package/dist/panel-sign-in.cjs.map +1 -0
  228. package/dist/panel-sign-in.d.cts +31 -0
  229. package/dist/panel-sign-in.d.ts +31 -0
  230. package/dist/panel-sign-in.js +14 -0
  231. package/dist/panel-sign-in.js.map +1 -0
  232. package/dist/segmented-control.cjs +126 -0
  233. package/dist/segmented-control.cjs.map +1 -0
  234. package/dist/segmented-control.d.cts +60 -0
  235. package/dist/segmented-control.d.ts +60 -0
  236. package/dist/segmented-control.js +9 -0
  237. package/dist/segmented-control.js.map +1 -0
  238. package/dist/select.cjs +648 -0
  239. package/dist/select.cjs.map +1 -0
  240. package/dist/select.d.cts +30 -0
  241. package/dist/select.d.ts +30 -0
  242. package/dist/select.js +12 -0
  243. package/dist/select.js.map +1 -0
  244. package/dist/separator.cjs +70 -0
  245. package/dist/separator.cjs.map +1 -0
  246. package/dist/separator.d.cts +13 -0
  247. package/dist/separator.d.ts +13 -0
  248. package/dist/separator.js +9 -0
  249. package/dist/separator.js.map +1 -0
  250. package/dist/side-drawer.cjs +321 -0
  251. package/dist/side-drawer.cjs.map +1 -0
  252. package/dist/side-drawer.d.cts +21 -0
  253. package/dist/side-drawer.d.ts +21 -0
  254. package/dist/side-drawer.js +13 -0
  255. package/dist/side-drawer.js.map +1 -0
  256. package/dist/spinner.cjs +57 -0
  257. package/dist/spinner.cjs.map +1 -0
  258. package/{src/spinner.tsx → dist/spinner.d.cts} +11 -22
  259. package/dist/spinner.d.ts +27 -0
  260. package/dist/spinner.js +8 -0
  261. package/dist/spinner.js.map +1 -0
  262. package/dist/spreadsheet-grid.cjs +238 -0
  263. package/dist/spreadsheet-grid.cjs.map +1 -0
  264. package/dist/spreadsheet-grid.d.cts +48 -0
  265. package/dist/spreadsheet-grid.d.ts +48 -0
  266. package/dist/spreadsheet-grid.js +8 -0
  267. package/dist/spreadsheet-grid.js.map +1 -0
  268. package/dist/state-panel.cjs +86 -0
  269. package/dist/state-panel.cjs.map +1 -0
  270. package/dist/state-panel.d.cts +23 -0
  271. package/dist/state-panel.d.ts +23 -0
  272. package/dist/state-panel.js +9 -0
  273. package/dist/state-panel.js.map +1 -0
  274. package/dist/switch.cjs +87 -0
  275. package/dist/switch.cjs.map +1 -0
  276. package/dist/switch.d.cts +13 -0
  277. package/dist/switch.d.ts +13 -0
  278. package/dist/switch.js +9 -0
  279. package/dist/switch.js.map +1 -0
  280. package/dist/table.cjs +139 -0
  281. package/dist/table.cjs.map +1 -0
  282. package/dist/table.d.cts +16 -0
  283. package/dist/table.d.ts +16 -0
  284. package/dist/table.js +22 -0
  285. package/dist/table.js.map +1 -0
  286. package/dist/tabs.cjs +168 -0
  287. package/dist/tabs.cjs.map +1 -0
  288. package/dist/tabs.d.cts +59 -0
  289. package/dist/tabs.d.ts +59 -0
  290. package/dist/tabs.js +11 -0
  291. package/dist/tabs.js.map +1 -0
  292. package/dist/textarea.cjs +66 -0
  293. package/dist/textarea.cjs.map +1 -0
  294. package/dist/textarea.d.cts +18 -0
  295. package/dist/textarea.d.ts +18 -0
  296. package/dist/textarea.js +10 -0
  297. package/dist/textarea.js.map +1 -0
  298. package/dist/utils.cjs +35 -0
  299. package/dist/utils.cjs.map +1 -0
  300. package/dist/utils.d.cts +5 -0
  301. package/dist/utils.d.ts +5 -0
  302. package/dist/utils.js +7 -0
  303. package/dist/utils.js.map +1 -0
  304. package/package.json +390 -13
  305. package/src/alert.tsx +0 -74
  306. package/src/autocomplete.tsx +0 -666
  307. package/src/badge.tsx +0 -60
  308. package/src/breadcrumb.tsx +0 -71
  309. package/src/button.tsx +0 -113
  310. package/src/card.tsx +0 -79
  311. package/src/clipboard.ts +0 -144
  312. package/src/collapsible.tsx +0 -63
  313. package/src/confirm-dialog.tsx +0 -84
  314. package/src/copy-button.tsx +0 -58
  315. package/src/data-table.tsx +0 -190
  316. package/src/date-range-picker/dateRange.ts +0 -142
  317. package/src/date-range-picker/index.tsx +0 -366
  318. package/src/dialogLayer.ts +0 -142
  319. package/src/feature-access-gate.tsx +0 -38
  320. package/src/field.tsx +0 -56
  321. package/src/image-uploader.tsx +0 -181
  322. package/src/index.ts +0 -37
  323. package/src/input.tsx +0 -16
  324. package/src/label.tsx +0 -23
  325. package/src/modal.tsx +0 -117
  326. package/src/page-header.tsx +0 -75
  327. package/src/pagination.tsx +0 -116
  328. package/src/panel-brand-mark.tsx +0 -32
  329. package/src/panel-sign-in.tsx +0 -134
  330. package/src/segmented-control.tsx +0 -155
  331. package/src/select.tsx +0 -105
  332. package/src/separator.tsx +0 -32
  333. package/src/side-drawer.tsx +0 -87
  334. package/src/spreadsheet-grid.tsx +0 -264
  335. package/src/state-panel.tsx +0 -46
  336. package/src/switch.tsx +0 -47
  337. package/src/table.tsx +0 -110
  338. package/src/tabs.tsx +0 -201
  339. package/src/textarea.tsx +0 -32
  340. package/src/utils.ts +0 -6
@@ -1,190 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "./utils";
4
-
5
- /**
6
- * The list table every feature panel was hand-rolling.
7
- *
8
- * Seventeen files repeated the same wrapper string
9
- * (`border-border bg-background/80 overflow-x-auto rounded-lg border`) and the
10
- * header had already drifted into two incompatible looks, so Campaigns and
11
- * Targets did not resemble each other. Both are kept as `variant` rather than
12
- * unified here: collapsing them is a visual change to ten panels and belongs in
13
- * its own reviewable diff, not smuggled in with a refactor.
14
- *
15
- * The column contract deliberately echoes `GridColumn` in
16
- * `spreadsheet-grid.tsx` — `key`, `title`, a renderer, an optional class — so
17
- * the repo has one shape to learn. It drops that type's `value: (row) => string`
18
- * because there is no clipboard or CSV here; nothing would keep it honest, and
19
- * a required field nobody reads gets filled in with lies.
20
- *
21
- * **Not for every table.** `GroupTable` renders parent and child rows with
22
- * different column counts, and `BusinessTable`'s rows are dnd-kit sortables that
23
- * own their own refs and transforms. `PublishModal`, `BulkCreateModal` and the
24
- * clone queue in `MetaAdSetsPanel` are grids of inputs rather than lists of
25
- * values — every cell is an editable control, and they size their header and
26
- * body rows differently on purpose. All five are legitimately tables of a
27
- * different kind; bending this component around them would cost more than it
28
- * saves.
29
- */
30
- export type TableColumn<Row> = {
31
- key: string;
32
- /** Node, not string: a header can carry a select-all box or a sort control. */
33
- title: React.ReactNode;
34
- cell: (row: Row, index: number) => React.ReactNode;
35
- align?: "left" | "right" | "center";
36
- /**
37
- * Set on a sortable column so a screen reader announces the current sort.
38
- * Only the sorted column says `ascending`/`descending`; the rest say `none`,
39
- * which is what marks them as sortable at all.
40
- */
41
- ariaSort?: "ascending" | "descending" | "none";
42
- /** Extra classes for this column's `<td>`. */
43
- className?: string;
44
- /** Extra classes for this column's `<th>`. */
45
- headerClassName?: string;
46
- };
47
-
48
- export interface DataTableProps<Row> {
49
- columns: TableColumn<Row>[];
50
- rows: Row[];
51
- /** Stable React key per row. */
52
- rowKey: (row: Row, index: number) => string;
53
- /**
54
- * `uppercase` — the tinted small-caps header (campaigns, showcase, filters).
55
- * `plain` — the quieter left-aligned header (ads, targets, creatives).
56
- */
57
- variant?: "uppercase" | "plain";
58
- /**
59
- * `comfortable` — the roomy default, for tables of a handful of columns.
60
- * `compact` — what every table of eight columns or more had already chosen by
61
- * hand, because the roomy padding pushes them into a horizontal scroll.
62
- */
63
- density?: "comfortable" | "compact";
64
- /** Pins the header for a table inside its own scroll container. */
65
- stickyHeader?: boolean;
66
- rowClassName?: (row: Row, index: number) => string | undefined;
67
- /** Rendered in place of `<tbody>` rows when there are none. */
68
- emptyMessage?: React.ReactNode;
69
- /**
70
- * Rows for a `<tfoot>` — a totals line, typically. The caller supplies the
71
- * `<tr>` itself because a summary row rarely lines up with the columns above
72
- * it; designer-counts spans its first two.
73
- */
74
- footer?: React.ReactNode;
75
- /** Names the table for screen readers when no visible caption fits. */
76
- ariaLabel?: string;
77
- /** Classes for the scroll wrapper. */
78
- className?: string;
79
- /** Classes for the `<table>`. */
80
- tableClassName?: string;
81
- }
82
-
83
- const HEADER_VARIANTS = {
84
- uppercase:
85
- "border-border bg-muted/40 text-muted-foreground border-b text-xs tracking-wide uppercase",
86
- plain: "text-muted-foreground border-border border-b text-left",
87
- } as const;
88
-
89
- const DENSITY = {
90
- comfortable: "px-4 py-3",
91
- compact: "px-3 py-2",
92
- } as const;
93
-
94
- const ALIGN = {
95
- left: "text-left",
96
- right: "text-right",
97
- center: "text-center",
98
- } as const;
99
-
100
- export function DataTable<Row>({
101
- columns,
102
- rows,
103
- rowKey,
104
- variant = "uppercase",
105
- density = "comfortable",
106
- stickyHeader = false,
107
- rowClassName,
108
- emptyMessage,
109
- footer,
110
- ariaLabel,
111
- className,
112
- tableClassName,
113
- }: DataTableProps<Row>) {
114
- return (
115
- <div
116
- className={cn(
117
- "border-border bg-background/80 overflow-x-auto rounded-lg border",
118
- className,
119
- )}
120
- >
121
- <table
122
- aria-label={ariaLabel}
123
- className={cn("min-w-full text-left text-sm", tableClassName)}
124
- >
125
- <thead
126
- className={cn(
127
- HEADER_VARIANTS[variant],
128
- stickyHeader && "bg-muted sticky top-0 z-10",
129
- )}
130
- >
131
- <tr>
132
- {columns.map((column) => (
133
- <th
134
- key={column.key}
135
- scope="col"
136
- aria-sort={column.ariaSort}
137
- className={cn(
138
- DENSITY[density],
139
- "font-medium",
140
- ALIGN[column.align ?? "left"],
141
- column.headerClassName,
142
- )}
143
- >
144
- {column.title}
145
- </th>
146
- ))}
147
- </tr>
148
- </thead>
149
- <tbody>
150
- {rows.length === 0 && emptyMessage != null ? (
151
- <tr>
152
- {/* One cell across the whole width — a short row would otherwise
153
- leave the message sitting under the first column. */}
154
- <td
155
- colSpan={columns.length}
156
- className="text-muted-foreground px-4 py-6 text-center text-sm"
157
- >
158
- {emptyMessage}
159
- </td>
160
- </tr>
161
- ) : (
162
- rows.map((row, index) => (
163
- <tr
164
- key={rowKey(row, index)}
165
- className={cn(
166
- "border-border/70 border-b last:border-0",
167
- rowClassName?.(row, index),
168
- )}
169
- >
170
- {columns.map((column) => (
171
- <td
172
- key={column.key}
173
- className={cn(
174
- DENSITY[density],
175
- ALIGN[column.align ?? "left"],
176
- column.className,
177
- )}
178
- >
179
- {column.cell(row, index)}
180
- </td>
181
- ))}
182
- </tr>
183
- ))
184
- )}
185
- </tbody>
186
- {footer != null ? <tfoot>{footer}</tfoot> : null}
187
- </table>
188
- </div>
189
- );
190
- }
@@ -1,142 +0,0 @@
1
- /**
2
- * Date helpers for the range picker, all in local time.
3
- *
4
- * Deliberately not `toISOString()`: that converts to UTC first, so on any
5
- * negative-offset day it returns yesterday's date and the picker highlights the
6
- * wrong cell. Every value here is built and read as the local calendar day the
7
- * user is actually looking at.
8
- */
9
-
10
- /** `YYYY-MM-DD`, the value shape the filters store. */
11
- export type IsoDate = string;
12
-
13
- const pad = (n: number) => String(n).padStart(2, "0");
14
-
15
- export function toIsoDate(date: Date): IsoDate {
16
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
17
- }
18
-
19
- /** Parses `YYYY-MM-DD` into a local midnight Date; null for anything else. */
20
- export function fromIsoDate(value: string | null | undefined): Date | null {
21
- if (!value) return null;
22
- const trimmed = value.trim();
23
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
24
- if (!match) return null;
25
- const date = new Date(
26
- Number(match[1]),
27
- Number(match[2]) - 1,
28
- Number(match[3]),
29
- );
30
- // Rejects 2026-02-31, which would otherwise roll silently into March.
31
- return toIsoDate(date) === trimmed ? date : null;
32
- }
33
-
34
- export function startOfMonth(date: Date): Date {
35
- return new Date(date.getFullYear(), date.getMonth(), 1);
36
- }
37
-
38
- export function addMonths(date: Date, count: number): Date {
39
- return new Date(date.getFullYear(), date.getMonth() + count, 1);
40
- }
41
-
42
- export function addDays(date: Date, count: number): Date {
43
- return new Date(date.getFullYear(), date.getMonth(), date.getDate() + count);
44
- }
45
-
46
- export function isSameDay(a: Date | null, b: Date | null): boolean {
47
- if (!a || !b) return false;
48
- return toIsoDate(a) === toIsoDate(b);
49
- }
50
-
51
- /**
52
- * The six-week grid for a month, Sunday-first, padded with the neighbouring
53
- * months so every row has seven cells.
54
- *
55
- * Always six rows, never five: a grid that changes height as you page through
56
- * months makes everything below it jump.
57
- */
58
- export function monthGrid(month: Date): Date[] {
59
- const first = startOfMonth(month);
60
- const start = addDays(first, -first.getDay());
61
- return Array.from({ length: 42 }, (_, index) => addDays(start, index));
62
- }
63
-
64
- export function monthLabel(month: Date, locale = "en-IN"): string {
65
- return month.toLocaleDateString(locale, { month: "long", year: "numeric" });
66
- }
67
-
68
- /** Human form for the trigger — "22 Aug 2026". */
69
- export function formatDisplay(value: string, locale = "en-IN"): string {
70
- const date = fromIsoDate(value);
71
- if (!date) return "";
72
- return date.toLocaleDateString(locale, {
73
- day: "numeric",
74
- month: "short",
75
- year: "numeric",
76
- });
77
- }
78
-
79
- export function startOfDayMs(value: string): number {
80
- const date = fromIsoDate(value);
81
- return date ? date.getTime() : 0;
82
- }
83
-
84
- /**
85
- * The end of a day as milliseconds.
86
- *
87
- * A range picked as whole days has to include everything created on the last
88
- * day, so "to" means 23:59:59.999 rather than that morning's midnight — which
89
- * would drop a whole day of cards without saying so.
90
- */
91
- export function endOfDayMs(value: string): number {
92
- const date = fromIsoDate(value);
93
- if (!date) return 0;
94
- return new Date(
95
- date.getFullYear(),
96
- date.getMonth(),
97
- date.getDate(),
98
- 23,
99
- 59,
100
- 59,
101
- 999,
102
- ).getTime();
103
- }
104
-
105
- /**
106
- * 11:59 PM on that local day, as 23:59:59.
107
- *
108
- * The next midnight is already the following date, and the feed includes that
109
- * whole day — so the 28th through the 28th came back with the 29th. Stopping
110
- * at 11:59 PM keeps the end on the day that was picked.
111
- */
112
- export function endAt2359Ms(value: string): number {
113
- const date = fromIsoDate(value);
114
- if (!date) return 0;
115
- return new Date(
116
- date.getFullYear(),
117
- date.getMonth(),
118
- date.getDate(),
119
- 23,
120
- 59,
121
- 59,
122
- 0,
123
- ).getTime();
124
- }
125
-
126
- /**
127
- * Created-date window for the feed.
128
- *
129
- * A blank end means the start day, so one selected date is that whole day.
130
- * `end_time` is 11:59 PM on the end date.
131
- */
132
- export function createdRangeMs(
133
- from: string,
134
- to: string,
135
- ): { start_time: number; end_time: number } {
136
- const startDate = from.trim() || to.trim();
137
- const endDate = to.trim() || from.trim();
138
- return {
139
- start_time: startOfDayMs(startDate),
140
- end_time: endAt2359Ms(endDate),
141
- };
142
- }
@@ -1,366 +0,0 @@
1
- "use client";
2
-
3
- import { CalendarDays, ChevronLeft, ChevronRight, X } from "lucide-react";
4
- import * as React from "react";
5
-
6
- import { cn } from "../utils";
7
- import {
8
- addDays,
9
- addMonths,
10
- formatDisplay,
11
- fromIsoDate,
12
- isSameDay,
13
- monthGrid,
14
- monthLabel,
15
- startOfMonth,
16
- toIsoDate,
17
- } from "./dateRange";
18
-
19
- export type DateRangeValue = { from: string; to: string };
20
-
21
- /**
22
- * A two-month range calendar.
23
- *
24
- * Two separate date fields cost two columns and read as two unrelated
25
- * questions when they are one: which window. They also let the pair be set
26
- * backwards, which returns nothing and looks like a broken filter.
27
- *
28
- * Two months rather than one because almost every range anyone picks here
29
- * crosses a month boundary — "last week of July into the first of August" is a
30
- * single gesture with two months visible and a paging exercise with one.
31
- *
32
- * Presets are the useful half. The common ranges are the same handful every
33
- * time, and picking "Last 7 days" out of a list beats counting backwards on a
34
- * grid.
35
- */
36
- const PRESETS: { label: string; range: () => DateRangeValue }[] = [
37
- {
38
- label: "Today",
39
- range: () => {
40
- const today = toIsoDate(new Date());
41
- return { from: today, to: today };
42
- },
43
- },
44
- {
45
- label: "Yesterday",
46
- range: () => {
47
- const day = toIsoDate(addDays(new Date(), -1));
48
- return { from: day, to: day };
49
- },
50
- },
51
- {
52
- label: "Last 7 days",
53
- range: () => ({
54
- from: toIsoDate(addDays(new Date(), -6)),
55
- to: toIsoDate(new Date()),
56
- }),
57
- },
58
- {
59
- label: "Last 30 days",
60
- range: () => ({
61
- from: toIsoDate(addDays(new Date(), -29)),
62
- to: toIsoDate(new Date()),
63
- }),
64
- },
65
- {
66
- label: "This month",
67
- range: () => ({
68
- from: toIsoDate(startOfMonth(new Date())),
69
- to: toIsoDate(new Date()),
70
- }),
71
- },
72
- ];
73
-
74
- const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
75
-
76
- export interface DateRangePickerProps {
77
- value: DateRangeValue;
78
- onChange: (next: DateRangeValue) => void;
79
- disabled?: boolean;
80
- placeholder?: string;
81
- className?: string;
82
- /** Names the trigger for screen readers when there is no visible label. */
83
- ariaLabel?: string;
84
- }
85
-
86
- export function DateRangePicker({
87
- value,
88
- onChange,
89
- disabled,
90
- placeholder = "Any date",
91
- className,
92
- ariaLabel = "Date range",
93
- }: DateRangePickerProps) {
94
- const [open, setOpen] = React.useState(false);
95
- const [hovered, setHovered] = React.useState<Date | null>(null);
96
- /** Set while choosing the second end, so the range can preview under the cursor. */
97
- const [pendingFrom, setPendingFrom] = React.useState<Date | null>(null);
98
- const rootRef = React.useRef<HTMLDivElement>(null);
99
-
100
- const from = fromIsoDate(value.from);
101
- const to = fromIsoDate(value.to);
102
-
103
- const [viewMonth, setViewMonth] = React.useState<Date>(() =>
104
- startOfMonth(from ?? new Date()),
105
- );
106
-
107
- React.useEffect(() => {
108
- if (!open) return undefined;
109
- const onPointerDown = (event: MouseEvent) => {
110
- if (!rootRef.current?.contains(event.target as Node)) setOpen(false);
111
- };
112
- const onKeyDown = (event: KeyboardEvent) => {
113
- if (event.key === "Escape") setOpen(false);
114
- };
115
- document.addEventListener("mousedown", onPointerDown);
116
- document.addEventListener("keydown", onKeyDown);
117
- return () => {
118
- document.removeEventListener("mousedown", onPointerDown);
119
- document.removeEventListener("keydown", onKeyDown);
120
- };
121
- }, [open]);
122
-
123
- /**
124
- * Reopening lands on the current selection rather than wherever the view was
125
- * left. Done here rather than in an effect: it is a response to an event, and
126
- * setting state from an effect is what `react-hooks/set-state-in-effect`
127
- * rejects — correctly, since it costs an extra render every open.
128
- */
129
- function openPicker() {
130
- setViewMonth(startOfMonth(fromIsoDate(value.from) ?? new Date()));
131
- setPendingFrom(null);
132
- setHovered(null);
133
- setOpen(true);
134
- }
135
-
136
- const anchor = pendingFrom ?? (to ? null : from);
137
- const previewEnd = pendingFrom ? hovered : null;
138
-
139
- function rangeBounds(): [Date | null, Date | null] {
140
- if (pendingFrom) {
141
- if (!previewEnd) return [pendingFrom, pendingFrom];
142
- return previewEnd.getTime() < pendingFrom.getTime()
143
- ? [previewEnd, pendingFrom]
144
- : [pendingFrom, previewEnd];
145
- }
146
- return [from, to];
147
- }
148
-
149
- const [rangeStart, rangeEnd] = rangeBounds();
150
-
151
- function selectDay(day: Date) {
152
- if (!anchor) {
153
- // Starting a new range. Keeping only `from` means the next click closes
154
- // it, whichever side of the anchor it lands on.
155
- setPendingFrom(day);
156
- onChange({ from: toIsoDate(day), to: "" });
157
- return;
158
- }
159
- const [start, end] =
160
- day.getTime() < anchor.getTime() ? [day, anchor] : [anchor, day];
161
- onChange({ from: toIsoDate(start), to: toIsoDate(end) });
162
- setPendingFrom(null);
163
- setHovered(null);
164
- setOpen(false);
165
- }
166
-
167
- /** Arrow keys move by day/week across both months, as a calendar should. */
168
- function onGridKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
169
- const offsets: Record<string, number> = {
170
- ArrowLeft: -1,
171
- ArrowRight: 1,
172
- ArrowUp: -7,
173
- ArrowDown: 7,
174
- };
175
- const offset = offsets[event.key];
176
- if (offset === undefined) return;
177
- const focused = document.activeElement as HTMLElement | null;
178
- const iso = focused?.dataset?.day;
179
- const current = fromIsoDate(iso);
180
- if (!current) return;
181
- event.preventDefault();
182
- const next = addDays(current, offset);
183
- const nextIso = toIsoDate(next);
184
- // Page the view when the move lands outside what is rendered.
185
- if (!rootRef.current?.querySelector(`[data-day="${nextIso}"]`)) {
186
- setViewMonth(startOfMonth(offset < 0 ? next : addMonths(next, -1)));
187
- }
188
- window.requestAnimationFrame(() => {
189
- rootRef.current
190
- ?.querySelector<HTMLElement>(`[data-day="${nextIso}"]`)
191
- ?.focus();
192
- });
193
- }
194
-
195
- const label =
196
- value.from || value.to
197
- ? `${formatDisplay(value.from) || "Any"} → ${formatDisplay(value.to) || "Any"}`
198
- : placeholder;
199
-
200
- const today = new Date();
201
-
202
- function renderMonth(month: Date) {
203
- return (
204
- <div key={monthLabel(month)} className="w-[15.75rem] shrink-0">
205
- <div className="text-foreground mb-2 text-center text-sm font-medium whitespace-nowrap">
206
- {monthLabel(month)}
207
- </div>
208
- <div className="text-muted-foreground grid grid-cols-7 text-center text-[11px]">
209
- {WEEKDAYS.map((day) => (
210
- <div key={day} className="grid size-9 place-items-center">
211
- {day}
212
- </div>
213
- ))}
214
- </div>
215
- <div className="grid grid-cols-7">
216
- {monthGrid(month).map((day) => {
217
- const iso = toIsoDate(day);
218
- const outside = day.getMonth() !== month.getMonth();
219
- const isStart = isSameDay(day, rangeStart);
220
- const isEnd = isSameDay(day, rangeEnd);
221
- const inRange =
222
- rangeStart != null &&
223
- rangeEnd != null &&
224
- day.getTime() > rangeStart.getTime() &&
225
- day.getTime() < rangeEnd.getTime();
226
- const isToday = isSameDay(day, today);
227
-
228
- return (
229
- <button
230
- key={iso}
231
- type="button"
232
- data-day={iso}
233
- // Outside days stay clickable — clicking one is how you reach
234
- // the neighbouring month without paging.
235
- onClick={() => selectDay(day)}
236
- onMouseEnter={() => setHovered(day)}
237
- aria-label={day.toDateString()}
238
- aria-pressed={isStart || isEnd}
239
- className={cn(
240
- "focus-visible:ring-ring relative grid size-9 place-items-center rounded-md text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none",
241
- outside ? "text-muted-foreground/50" : "text-foreground",
242
- inRange && "bg-primary/10 rounded-none",
243
- (isStart || isEnd) &&
244
- "bg-primary text-primary-foreground font-medium",
245
- // Square off the inner edge so a range reads as one bar
246
- // rather than two pills with a gap.
247
- isStart && rangeEnd && !isEnd && "rounded-r-none",
248
- isEnd && rangeStart && !isStart && "rounded-l-none",
249
- !isStart && !isEnd && "hover:bg-muted",
250
- isToday &&
251
- !isStart &&
252
- !isEnd &&
253
- "ring-border font-semibold ring-1",
254
- )}
255
- >
256
- {day.getDate()}
257
- </button>
258
- );
259
- })}
260
- </div>
261
- </div>
262
- );
263
- }
264
-
265
- return (
266
- <div ref={rootRef} className={cn("relative", className)}>
267
- <div
268
- className={cn(
269
- "border-border bg-background flex items-center rounded-md border",
270
- "focus-within:ring-ring/50 focus-within:border-ring focus-within:ring-2",
271
- disabled && "opacity-50",
272
- )}
273
- >
274
- <button
275
- type="button"
276
- disabled={disabled}
277
- aria-haspopup="dialog"
278
- aria-expanded={open}
279
- aria-label={ariaLabel}
280
- onClick={() => (open ? setOpen(false) : openPicker())}
281
- className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left text-sm"
282
- >
283
- <CalendarDays
284
- className="text-muted-foreground size-4 shrink-0"
285
- aria-hidden
286
- />
287
- <span
288
- className={cn(
289
- "truncate",
290
- !value.from && !value.to && "text-muted-foreground",
291
- )}
292
- >
293
- {label}
294
- </span>
295
- </button>
296
- {(value.from || value.to) && !disabled ? (
297
- <button
298
- type="button"
299
- aria-label="Clear date range"
300
- onClick={() => {
301
- onChange({ from: "", to: "" });
302
- setPendingFrom(null);
303
- }}
304
- className="text-muted-foreground hover:text-foreground shrink-0 px-2"
305
- >
306
- <X className="size-3.5" aria-hidden />
307
- </button>
308
- ) : null}
309
- </div>
310
-
311
- {open ? (
312
- <div
313
- role="dialog"
314
- aria-label={ariaLabel}
315
- // `w-max` so the calendar sets its own width instead of inheriting
316
- // the filter column, which squeezed the months until the numbers ran
317
- // together. Anchored right because this sits in the last column and
318
- // would otherwise open off the edge of the viewport.
319
- className="border-border bg-background absolute top-full right-0 z-50 mt-1 flex w-max rounded-lg border p-3 shadow-lg"
320
- >
321
- <div className="border-border mr-3 flex w-32 shrink-0 flex-col gap-0.5 border-r pr-3">
322
- {PRESETS.map((preset) => (
323
- <button
324
- key={preset.label}
325
- type="button"
326
- className="hover:bg-muted text-foreground rounded-md px-2 py-1.5 text-left text-xs"
327
- onClick={() => {
328
- onChange(preset.range());
329
- setPendingFrom(null);
330
- setOpen(false);
331
- }}
332
- >
333
- {preset.label}
334
- </button>
335
- ))}
336
- </div>
337
-
338
- <div onKeyDown={onGridKeyDown}>
339
- <div className="mb-2 flex items-center justify-between">
340
- <button
341
- type="button"
342
- aria-label="Previous month"
343
- className="hover:bg-muted text-muted-foreground hover:text-foreground rounded-md p-1.5"
344
- onClick={() => setViewMonth(addMonths(viewMonth, -1))}
345
- >
346
- <ChevronLeft className="size-4" aria-hidden />
347
- </button>
348
- <button
349
- type="button"
350
- aria-label="Next month"
351
- className="hover:bg-muted text-muted-foreground hover:text-foreground rounded-md p-1.5"
352
- onClick={() => setViewMonth(addMonths(viewMonth, 1))}
353
- >
354
- <ChevronRight className="size-4" aria-hidden />
355
- </button>
356
- </div>
357
- <div className="flex gap-5" onMouseLeave={() => setHovered(null)}>
358
- {renderMonth(viewMonth)}
359
- {renderMonth(addMonths(viewMonth, 1))}
360
- </div>
361
- </div>
362
- </div>
363
- ) : null}
364
- </div>
365
- );
366
- }