@inshorts/glint-ui 0.5.3 → 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 (341) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +3 -9
  3. package/dist/alert.cjs +96 -0
  4. package/dist/alert.cjs.map +1 -0
  5. package/dist/alert.d.cts +29 -0
  6. package/dist/alert.d.ts +29 -0
  7. package/dist/alert.js +10 -0
  8. package/dist/alert.js.map +1 -0
  9. package/dist/autocomplete.cjs +578 -0
  10. package/dist/autocomplete.cjs.map +1 -0
  11. package/dist/autocomplete.d.cts +56 -0
  12. package/dist/autocomplete.d.ts +56 -0
  13. package/dist/autocomplete.js +11 -0
  14. package/dist/autocomplete.js.map +1 -0
  15. package/dist/badge.cjs +89 -0
  16. package/dist/badge.cjs.map +1 -0
  17. package/dist/badge.d.cts +25 -0
  18. package/dist/badge.d.ts +25 -0
  19. package/dist/badge.js +10 -0
  20. package/dist/badge.js.map +1 -0
  21. package/dist/breadcrumb.cjs +88 -0
  22. package/dist/breadcrumb.cjs.map +1 -0
  23. package/dist/breadcrumb.d.cts +14 -0
  24. package/dist/breadcrumb.d.ts +14 -0
  25. package/dist/breadcrumb.js +8 -0
  26. package/dist/breadcrumb.js.map +1 -0
  27. package/dist/button.cjs +143 -0
  28. package/dist/button.cjs.map +1 -0
  29. package/dist/button.d.cts +34 -0
  30. package/dist/button.d.ts +34 -0
  31. package/dist/button.js +9 -0
  32. package/dist/button.js.map +1 -0
  33. package/dist/card.cjs +111 -0
  34. package/dist/card.cjs.map +1 -0
  35. package/dist/card.d.cts +15 -0
  36. package/dist/card.d.ts +15 -0
  37. package/dist/card.js +18 -0
  38. package/dist/card.js.map +1 -0
  39. package/dist/checkbox.cjs +66 -0
  40. package/dist/checkbox.cjs.map +1 -0
  41. package/{src/checkbox.tsx → dist/checkbox.d.cts} +5 -22
  42. package/dist/checkbox.d.ts +25 -0
  43. package/dist/checkbox.js +10 -0
  44. package/dist/checkbox.js.map +1 -0
  45. package/dist/chunk-43ZUFY63.js +85 -0
  46. package/dist/chunk-43ZUFY63.js.map +1 -0
  47. package/dist/chunk-67ACNS42.js +29 -0
  48. package/dist/chunk-67ACNS42.js.map +1 -0
  49. package/dist/chunk-6URKHMCF.js +76 -0
  50. package/dist/chunk-6URKHMCF.js.map +1 -0
  51. package/dist/chunk-ASU6CUTZ.js +85 -0
  52. package/dist/chunk-ASU6CUTZ.js.map +1 -0
  53. package/dist/chunk-EWSBFU54.js +53 -0
  54. package/dist/chunk-EWSBFU54.js.map +1 -0
  55. package/dist/chunk-G4ZQUSAJ.js +38 -0
  56. package/dist/chunk-G4ZQUSAJ.js.map +1 -0
  57. package/dist/chunk-GMCUXEFI.js +67 -0
  58. package/dist/chunk-GMCUXEFI.js.map +1 -0
  59. package/dist/chunk-HBBOR43A.js +85 -0
  60. package/dist/chunk-HBBOR43A.js.map +1 -0
  61. package/dist/chunk-HCA5ODCD.js +11 -0
  62. package/dist/chunk-HCA5ODCD.js.map +1 -0
  63. package/dist/chunk-HYRJYUPI.js +26 -0
  64. package/dist/chunk-HYRJYUPI.js.map +1 -0
  65. package/dist/chunk-IRCTGJQB.js +26 -0
  66. package/dist/chunk-IRCTGJQB.js.map +1 -0
  67. package/dist/chunk-JDE5XYPT.js +30 -0
  68. package/dist/chunk-JDE5XYPT.js.map +1 -0
  69. package/dist/chunk-JSJEEKR3.js +59 -0
  70. package/dist/chunk-JSJEEKR3.js.map +1 -0
  71. package/dist/chunk-KR3BCFP7.js +85 -0
  72. package/dist/chunk-KR3BCFP7.js.map +1 -0
  73. package/dist/chunk-MQT5RAQV.js +202 -0
  74. package/dist/chunk-MQT5RAQV.js.map +1 -0
  75. package/dist/chunk-MUPW2TFJ.js +102 -0
  76. package/dist/chunk-MUPW2TFJ.js.map +1 -0
  77. package/dist/chunk-PM2VE7DJ.js +100 -0
  78. package/dist/chunk-PM2VE7DJ.js.map +1 -0
  79. package/dist/chunk-PPYR3O3L.js +44 -0
  80. package/dist/chunk-PPYR3O3L.js.map +1 -0
  81. package/dist/chunk-PUTC52SI.js +98 -0
  82. package/dist/chunk-PUTC52SI.js.map +1 -0
  83. package/dist/chunk-PVK4WQJZ.js +513 -0
  84. package/dist/chunk-PVK4WQJZ.js.map +1 -0
  85. package/dist/chunk-PWAKTEXF.js +18 -0
  86. package/dist/chunk-PWAKTEXF.js.map +1 -0
  87. package/dist/chunk-QTFWORA7.js +26 -0
  88. package/dist/chunk-QTFWORA7.js.map +1 -0
  89. package/dist/chunk-RUPL2H6V.js +49 -0
  90. package/dist/chunk-RUPL2H6V.js.map +1 -0
  91. package/dist/chunk-S7PVZAPL.js +28 -0
  92. package/dist/chunk-S7PVZAPL.js.map +1 -0
  93. package/dist/chunk-SQDV2AKJ.js +49 -0
  94. package/dist/chunk-SQDV2AKJ.js.map +1 -0
  95. package/dist/chunk-T2KXVH3O.js +297 -0
  96. package/dist/chunk-T2KXVH3O.js.map +1 -0
  97. package/dist/chunk-T4WCCHJQ.js +29 -0
  98. package/dist/chunk-T4WCCHJQ.js.map +1 -0
  99. package/dist/chunk-UC3ZIQPE.js +93 -0
  100. package/dist/chunk-UC3ZIQPE.js.map +1 -0
  101. package/dist/chunk-UE7JY7VR.js +28 -0
  102. package/dist/chunk-UE7JY7VR.js.map +1 -0
  103. package/dist/chunk-WVNRFO5F.js +70 -0
  104. package/dist/chunk-WVNRFO5F.js.map +1 -0
  105. package/dist/chunk-Y5WCT6XN.js +93 -0
  106. package/dist/chunk-Y5WCT6XN.js.map +1 -0
  107. package/dist/chunk-YDICVETX.js +151 -0
  108. package/dist/chunk-YDICVETX.js.map +1 -0
  109. package/dist/chunk-YSU4VWRD.js +90 -0
  110. package/dist/chunk-YSU4VWRD.js.map +1 -0
  111. package/dist/chunk-ZFSQVMLJ.js +126 -0
  112. package/dist/chunk-ZFSQVMLJ.js.map +1 -0
  113. package/dist/chunk-ZHATD73Y.js +46 -0
  114. package/dist/chunk-ZHATD73Y.js.map +1 -0
  115. package/dist/chunk-ZLAA4DGG.js +111 -0
  116. package/dist/chunk-ZLAA4DGG.js.map +1 -0
  117. package/dist/chunk-ZP252OJZ.js +56 -0
  118. package/dist/chunk-ZP252OJZ.js.map +1 -0
  119. package/dist/clipboard.cjs +125 -0
  120. package/dist/clipboard.cjs.map +1 -0
  121. package/dist/clipboard.d.cts +52 -0
  122. package/dist/clipboard.d.ts +52 -0
  123. package/dist/clipboard.js +23 -0
  124. package/dist/clipboard.js.map +1 -0
  125. package/dist/collapsible.cjs +94 -0
  126. package/dist/collapsible.cjs.map +1 -0
  127. package/dist/collapsible.d.cts +21 -0
  128. package/dist/collapsible.d.ts +21 -0
  129. package/dist/collapsible.js +9 -0
  130. package/dist/collapsible.js.map +1 -0
  131. package/dist/confirm-dialog.cjs +369 -0
  132. package/dist/confirm-dialog.cjs.map +1 -0
  133. package/dist/confirm-dialog.d.cts +16 -0
  134. package/dist/confirm-dialog.d.ts +16 -0
  135. package/dist/confirm-dialog.js +13 -0
  136. package/dist/confirm-dialog.js.map +1 -0
  137. package/dist/copy-button.cjs +102 -0
  138. package/dist/copy-button.cjs.map +1 -0
  139. package/dist/copy-button.d.cts +18 -0
  140. package/dist/copy-button.d.ts +18 -0
  141. package/dist/copy-button.js +10 -0
  142. package/dist/copy-button.js.map +1 -0
  143. package/dist/data-table.cjs +140 -0
  144. package/dist/data-table.cjs.map +1 -0
  145. package/dist/data-table.d.cts +81 -0
  146. package/dist/data-table.d.ts +81 -0
  147. package/dist/data-table.js +8 -0
  148. package/dist/data-table.js.map +1 -0
  149. package/dist/date-range-picker/dateRange.cjs +138 -0
  150. package/dist/date-range-picker/dateRange.cjs.map +1 -0
  151. package/dist/date-range-picker/dateRange.d.cts +57 -0
  152. package/dist/date-range-picker/dateRange.d.ts +57 -0
  153. package/dist/date-range-picker/dateRange.js +31 -0
  154. package/dist/date-range-picker/dateRange.js.map +1 -0
  155. package/dist/date-range-picker/index.cjs +375 -0
  156. package/dist/date-range-picker/index.cjs.map +1 -0
  157. package/dist/date-range-picker/index.d.cts +18 -0
  158. package/dist/date-range-picker/index.d.ts +18 -0
  159. package/dist/date-range-picker/index.js +10 -0
  160. package/dist/date-range-picker/index.js.map +1 -0
  161. package/dist/dialogLayer.cjs +121 -0
  162. package/dist/dialogLayer.cjs.map +1 -0
  163. package/dist/dialogLayer.d.cts +11 -0
  164. package/dist/dialogLayer.d.ts +11 -0
  165. package/dist/dialogLayer.js +8 -0
  166. package/dist/dialogLayer.js.map +1 -0
  167. package/dist/feature-access-gate.cjs +160 -0
  168. package/dist/feature-access-gate.cjs.map +1 -0
  169. package/dist/feature-access-gate.d.cts +14 -0
  170. package/dist/feature-access-gate.d.ts +14 -0
  171. package/dist/feature-access-gate.js +11 -0
  172. package/dist/feature-access-gate.js.map +1 -0
  173. package/dist/field.cjs +58 -0
  174. package/dist/field.cjs.map +1 -0
  175. package/dist/field.d.cts +23 -0
  176. package/dist/field.d.ts +23 -0
  177. package/dist/field.js +8 -0
  178. package/dist/field.js.map +1 -0
  179. package/dist/image-uploader.cjs +293 -0
  180. package/dist/image-uploader.cjs.map +1 -0
  181. package/dist/image-uploader.d.cts +34 -0
  182. package/dist/image-uploader.d.ts +34 -0
  183. package/dist/image-uploader.js +12 -0
  184. package/dist/image-uploader.js.map +1 -0
  185. package/dist/index.cjs +3012 -0
  186. package/dist/index.cjs.map +1 -0
  187. package/dist/index.d.cts +41 -0
  188. package/dist/index.d.ts +41 -0
  189. package/dist/index.js +232 -0
  190. package/dist/index.js.map +1 -0
  191. package/dist/input.cjs +58 -0
  192. package/dist/input.cjs.map +1 -0
  193. package/dist/input.d.cts +7 -0
  194. package/dist/input.d.ts +7 -0
  195. package/dist/input.js +10 -0
  196. package/dist/input.js.map +1 -0
  197. package/dist/label.cjs +67 -0
  198. package/dist/label.cjs.map +1 -0
  199. package/dist/label.d.cts +7 -0
  200. package/dist/label.d.ts +7 -0
  201. package/dist/label.js +9 -0
  202. package/dist/label.js.map +1 -0
  203. package/dist/modal.cjs +212 -0
  204. package/dist/modal.cjs.map +1 -0
  205. package/dist/modal.d.cts +39 -0
  206. package/dist/modal.d.ts +39 -0
  207. package/dist/modal.js +12 -0
  208. package/dist/modal.js.map +1 -0
  209. package/dist/page-header.cjs +78 -0
  210. package/dist/page-header.cjs.map +1 -0
  211. package/dist/page-header.d.cts +35 -0
  212. package/dist/page-header.d.ts +35 -0
  213. package/dist/page-header.js +8 -0
  214. package/dist/page-header.js.map +1 -0
  215. package/dist/pagination.cjs +237 -0
  216. package/dist/pagination.cjs.map +1 -0
  217. package/dist/pagination.d.cts +15 -0
  218. package/dist/pagination.d.ts +15 -0
  219. package/dist/pagination.js +11 -0
  220. package/dist/pagination.js.map +1 -0
  221. package/dist/panel-brand-mark.cjs +59 -0
  222. package/dist/panel-brand-mark.cjs.map +1 -0
  223. package/dist/panel-brand-mark.d.cts +13 -0
  224. package/dist/panel-brand-mark.d.ts +13 -0
  225. package/dist/panel-brand-mark.js +8 -0
  226. package/dist/panel-brand-mark.js.map +1 -0
  227. package/dist/panel-sign-in.cjs +256 -0
  228. package/dist/panel-sign-in.cjs.map +1 -0
  229. package/dist/panel-sign-in.d.cts +31 -0
  230. package/dist/panel-sign-in.d.ts +31 -0
  231. package/dist/panel-sign-in.js +14 -0
  232. package/dist/panel-sign-in.js.map +1 -0
  233. package/dist/segmented-control.cjs +126 -0
  234. package/dist/segmented-control.cjs.map +1 -0
  235. package/dist/segmented-control.d.cts +60 -0
  236. package/dist/segmented-control.d.ts +60 -0
  237. package/dist/segmented-control.js +9 -0
  238. package/dist/segmented-control.js.map +1 -0
  239. package/dist/select.cjs +648 -0
  240. package/dist/select.cjs.map +1 -0
  241. package/dist/select.d.cts +30 -0
  242. package/dist/select.d.ts +30 -0
  243. package/dist/select.js +12 -0
  244. package/dist/select.js.map +1 -0
  245. package/dist/separator.cjs +70 -0
  246. package/dist/separator.cjs.map +1 -0
  247. package/dist/separator.d.cts +13 -0
  248. package/dist/separator.d.ts +13 -0
  249. package/dist/separator.js +9 -0
  250. package/dist/separator.js.map +1 -0
  251. package/dist/side-drawer.cjs +321 -0
  252. package/dist/side-drawer.cjs.map +1 -0
  253. package/dist/side-drawer.d.cts +21 -0
  254. package/dist/side-drawer.d.ts +21 -0
  255. package/dist/side-drawer.js +13 -0
  256. package/dist/side-drawer.js.map +1 -0
  257. package/dist/spinner.cjs +57 -0
  258. package/dist/spinner.cjs.map +1 -0
  259. package/{src/spinner.tsx → dist/spinner.d.cts} +11 -22
  260. package/dist/spinner.d.ts +27 -0
  261. package/dist/spinner.js +8 -0
  262. package/dist/spinner.js.map +1 -0
  263. package/dist/spreadsheet-grid.cjs +238 -0
  264. package/dist/spreadsheet-grid.cjs.map +1 -0
  265. package/dist/spreadsheet-grid.d.cts +48 -0
  266. package/dist/spreadsheet-grid.d.ts +48 -0
  267. package/dist/spreadsheet-grid.js +8 -0
  268. package/dist/spreadsheet-grid.js.map +1 -0
  269. package/dist/state-panel.cjs +86 -0
  270. package/dist/state-panel.cjs.map +1 -0
  271. package/dist/state-panel.d.cts +23 -0
  272. package/dist/state-panel.d.ts +23 -0
  273. package/dist/state-panel.js +9 -0
  274. package/dist/state-panel.js.map +1 -0
  275. package/dist/switch.cjs +87 -0
  276. package/dist/switch.cjs.map +1 -0
  277. package/dist/switch.d.cts +13 -0
  278. package/dist/switch.d.ts +13 -0
  279. package/dist/switch.js +9 -0
  280. package/dist/switch.js.map +1 -0
  281. package/dist/table.cjs +139 -0
  282. package/dist/table.cjs.map +1 -0
  283. package/dist/table.d.cts +16 -0
  284. package/dist/table.d.ts +16 -0
  285. package/dist/table.js +22 -0
  286. package/dist/table.js.map +1 -0
  287. package/dist/tabs.cjs +168 -0
  288. package/dist/tabs.cjs.map +1 -0
  289. package/dist/tabs.d.cts +59 -0
  290. package/dist/tabs.d.ts +59 -0
  291. package/dist/tabs.js +11 -0
  292. package/dist/tabs.js.map +1 -0
  293. package/dist/textarea.cjs +66 -0
  294. package/dist/textarea.cjs.map +1 -0
  295. package/dist/textarea.d.cts +18 -0
  296. package/dist/textarea.d.ts +18 -0
  297. package/dist/textarea.js +10 -0
  298. package/dist/textarea.js.map +1 -0
  299. package/dist/utils.cjs +35 -0
  300. package/dist/utils.cjs.map +1 -0
  301. package/dist/utils.d.cts +5 -0
  302. package/dist/utils.d.ts +5 -0
  303. package/dist/utils.js +7 -0
  304. package/dist/utils.js.map +1 -0
  305. package/package.json +395 -19
  306. package/src/alert.tsx +0 -74
  307. package/src/autocomplete.tsx +0 -666
  308. package/src/badge.tsx +0 -60
  309. package/src/breadcrumb.tsx +0 -71
  310. package/src/button.tsx +0 -113
  311. package/src/card.tsx +0 -79
  312. package/src/clipboard.ts +0 -144
  313. package/src/collapsible.tsx +0 -63
  314. package/src/confirm-dialog.tsx +0 -84
  315. package/src/copy-button.tsx +0 -58
  316. package/src/data-table.tsx +0 -190
  317. package/src/date-range-picker/dateRange.ts +0 -142
  318. package/src/date-range-picker/index.tsx +0 -366
  319. package/src/dialogLayer.ts +0 -142
  320. package/src/feature-access-gate.tsx +0 -38
  321. package/src/field.tsx +0 -56
  322. package/src/image-uploader.tsx +0 -181
  323. package/src/index.ts +0 -37
  324. package/src/input.tsx +0 -16
  325. package/src/label.tsx +0 -23
  326. package/src/modal.tsx +0 -117
  327. package/src/page-header.tsx +0 -75
  328. package/src/pagination.tsx +0 -116
  329. package/src/panel-brand-mark.tsx +0 -32
  330. package/src/panel-sign-in.tsx +0 -134
  331. package/src/segmented-control.tsx +0 -155
  332. package/src/select.tsx +0 -105
  333. package/src/separator.tsx +0 -32
  334. package/src/side-drawer.tsx +0 -87
  335. package/src/spreadsheet-grid.tsx +0 -264
  336. package/src/state-panel.tsx +0 -46
  337. package/src/switch.tsx +0 -46
  338. package/src/table.tsx +0 -110
  339. package/src/tabs.tsx +0 -201
  340. package/src/textarea.tsx +0 -32
  341. package/src/utils.ts +0 -6
@@ -1,142 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
-
5
- /**
6
- * The keyboard and focus behaviour every dialog needs, in one place.
7
- *
8
- * `Modal` and `SideDrawer` each had a copy of the scroll lock and an Escape
9
- * handler, and neither did anything about focus: opening a dialog left focus
10
- * on the button behind it, Tab walked the page underneath — through the
11
- * full-screen backdrop button, among other things — and closing left focus
12
- * wherever it had wandered. That is the whole keyboard story for thirty
13
- * dialogs, so it is fixed once here rather than thirty times.
14
- */
15
-
16
- /**
17
- * Open dialogs, innermost last.
18
- *
19
- * Nesting is real here — "Create audience group" opens on top of "Send
20
- * notification" — and without an ordering both would answer the same Escape
21
- * and fight over the same Tab. Only the top layer acts.
22
- */
23
- const layers: symbol[] = [];
24
-
25
- const FOCUSABLE = [
26
- "a[href]",
27
- "button:not([disabled])",
28
- "input:not([disabled])",
29
- "select:not([disabled])",
30
- "textarea:not([disabled])",
31
- '[tabindex]:not([tabindex="-1"])',
32
- ].join(",");
33
-
34
- /**
35
- * Deliberately not `offsetParent` or `getClientRects`: the first is null for
36
- * anything `position: fixed`, which a dialog panel usually is, and the second
37
- * is empty for *everything* under a test renderer that does no layout — which
38
- * would collapse the trap to "no focusable elements" in every test.
39
- */
40
- function isVisible(element: HTMLElement): boolean {
41
- if (element.closest("[hidden]")) return false;
42
- if (element.getAttribute("aria-hidden") === "true") return false;
43
- return typeof element.checkVisibility === "function"
44
- ? element.checkVisibility()
45
- : true;
46
- }
47
-
48
- function focusableWithin(container: HTMLElement): HTMLElement[] {
49
- return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
50
- isVisible,
51
- );
52
- }
53
-
54
- export function useDialogLayer({
55
- active,
56
- containerRef,
57
- onEscape,
58
- }: {
59
- active: boolean;
60
- /** The panel. Focus is moved into it and kept inside it. */
61
- containerRef: React.RefObject<HTMLElement | null>;
62
- /** Called on Escape, only while this is the top layer. Omit to ignore it. */
63
- onEscape?: () => void;
64
- }): void {
65
- // Read through a ref so a caller passing a fresh closure every render does
66
- // not tear the listener down and rebuild it on every keystroke.
67
- const escapeRef = React.useRef(onEscape);
68
- React.useEffect(() => {
69
- escapeRef.current = onEscape;
70
- });
71
-
72
- React.useEffect(() => {
73
- if (!active) return;
74
-
75
- const layer = Symbol("dialog-layer");
76
- layers.push(layer);
77
- const isTopLayer = () => layers[layers.length - 1] === layer;
78
-
79
- const previouslyFocused =
80
- document.activeElement instanceof HTMLElement
81
- ? document.activeElement
82
- : null;
83
-
84
- const container = containerRef.current;
85
- // Something inside may have claimed focus already — an `autoFocus` input —
86
- // and that choice is better than any this could make.
87
- if (container && !container.contains(document.activeElement)) {
88
- const [first] = focusableWithin(container);
89
- // The panel itself, not its first control: the first control is a Delete
90
- // button often enough that focusing it would put a destructive action
91
- // one Enter away from an accidental keypress.
92
- (container.hasAttribute("tabindex") ? container : first)?.focus();
93
- }
94
-
95
- function handleKeyDown(event: KeyboardEvent) {
96
- if (!isTopLayer()) return;
97
-
98
- if (event.key === "Escape") {
99
- escapeRef.current?.();
100
- return;
101
- }
102
-
103
- if (event.key !== "Tab") return;
104
- const panel = containerRef.current;
105
- if (!panel) return;
106
-
107
- const items = focusableWithin(panel);
108
- if (items.length === 0) {
109
- event.preventDefault();
110
- panel.focus();
111
- return;
112
- }
113
-
114
- const first = items[0]!;
115
- const last = items[items.length - 1]!;
116
- const active = document.activeElement;
117
- const outside = !panel.contains(active);
118
-
119
- if (event.shiftKey && (active === first || outside)) {
120
- event.preventDefault();
121
- last.focus();
122
- } else if (!event.shiftKey && (active === last || outside)) {
123
- event.preventDefault();
124
- first.focus();
125
- }
126
- }
127
-
128
- document.addEventListener("keydown", handleKeyDown);
129
- const previousOverflow = document.body.style.overflow;
130
- document.body.style.overflow = "hidden";
131
-
132
- return () => {
133
- document.removeEventListener("keydown", handleKeyDown);
134
- document.body.style.overflow = previousOverflow;
135
- const index = layers.indexOf(layer);
136
- if (index !== -1) layers.splice(index, 1);
137
- // Back where it came from, so closing a dialog opened by keyboard does
138
- // not drop the caret at the top of the document.
139
- if (previouslyFocused?.isConnected) previouslyFocused.focus();
140
- };
141
- }, [active, containerRef]);
142
- }
@@ -1,38 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { Alert } from "./alert";
4
- import { StatePanel } from "./state-panel";
5
-
6
- type FeatureAccessGateProps = {
7
- isLoading?: boolean;
8
- allowed: boolean;
9
- deniedMessage: React.ReactNode;
10
- children: React.ReactNode;
11
- };
12
-
13
- /**
14
- * Shared loading / denied shell for feature Managers (showcase, headers, …).
15
- */
16
- export function FeatureAccessGate({
17
- isLoading,
18
- allowed,
19
- deniedMessage,
20
- children,
21
- }: FeatureAccessGateProps) {
22
- if (isLoading) {
23
- return (
24
- <StatePanel loading className="p-6">
25
- Checking permissions…
26
- </StatePanel>
27
- );
28
- }
29
-
30
- if (!allowed) {
31
- // `Alert`, not a hand-rolled banner: this is the denial surface for
32
- // seventeen panels, and it was the one copy the Alert migration missed —
33
- // so being refused a panel announced nothing to a screen reader.
34
- return <Alert>{deniedMessage}</Alert>;
35
- }
36
-
37
- return <>{children}</>;
38
- }
package/src/field.tsx DELETED
@@ -1,56 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "./utils";
4
-
5
- interface FieldProps {
6
- label: React.ReactNode;
7
- required?: boolean;
8
- /**
9
- * Associates the label with a control via `htmlFor` / `id`. Prefer this for
10
- * native inputs rendered as siblings (not wrapped). For composite controls
11
- * (Autocomplete), omit and use `as="label"` wrapping or an `aria-label`.
12
- */
13
- htmlFor?: string;
14
- /**
15
- * Render as a `<label>` (wraps a native control so clicking the text focuses
16
- * it) or a `<div>` (for composite controls like Autocomplete that manage
17
- * their own focus / render in a portal). Defaults to `div`.
18
- */
19
- as?: "label" | "div";
20
- className?: string;
21
- children: React.ReactNode;
22
- }
23
-
24
- export function Field({
25
- label,
26
- required,
27
- htmlFor,
28
- as = "div",
29
- className,
30
- children,
31
- }: FieldProps) {
32
- const Wrapper = as;
33
- const labelNode = (
34
- <>
35
- {label}
36
- {required ? (
37
- <span className="text-destructive ml-0.5" aria-hidden="true">
38
- *
39
- </span>
40
- ) : null}
41
- </>
42
- );
43
-
44
- return (
45
- <Wrapper className={cn("space-y-1 text-sm", className)}>
46
- {htmlFor && as === "div" ? (
47
- <label htmlFor={htmlFor} className="text-muted-foreground">
48
- {labelNode}
49
- </label>
50
- ) : (
51
- <span className="text-muted-foreground">{labelNode}</span>
52
- )}
53
- {children}
54
- </Wrapper>
55
- );
56
- }
@@ -1,181 +0,0 @@
1
- "use client";
2
-
3
- import { ImagePlus, Loader2, X } from "lucide-react";
4
- import * as React from "react";
5
-
6
- import { Button } from "./button";
7
- import { Input } from "./input";
8
- import { cn } from "./utils";
9
-
10
- export type ImageUploaderProps = {
11
- value?: string;
12
- onChange: (url: string) => void;
13
- disabled?: boolean;
14
- label?: string;
15
- className?: string;
16
- /** Allow pasting a remote URL in addition to file upload. Default true. */
17
- allowUrlPaste?: boolean;
18
- accept?: string;
19
- /**
20
- * Upload a picked file and return its hosted URL.
21
- * Injected by the panel — Upload stays enabled either way; without a
22
- * handler, picking a file surfaces a configuration error.
23
- */
24
- uploadFile?: (file: File) => Promise<string>;
25
- /**
26
- * Optional transform for pasted values (e.g. host a relative path).
27
- * When omitted, any pasted non-empty string is applied as-is. Absolute
28
- * `http(s)` URLs never call this.
29
- */
30
- uploadFromUrl?: (imageUrl: string) => Promise<string>;
31
- };
32
-
33
- /**
34
- * Reusable image uploader.
35
- *
36
- * Upload and URL paste are both enabled by default. Hosting is injected:
37
- * each panel passes `uploadFile` / `uploadFromUrl` so the kit stays free of
38
- * panel-specific API routes.
39
- */
40
- export function ImageUploader({
41
- value = "",
42
- onChange,
43
- disabled,
44
- label,
45
- className,
46
- allowUrlPaste = true,
47
- accept = "image/*,.gif",
48
- uploadFile,
49
- uploadFromUrl,
50
- }: ImageUploaderProps) {
51
- const inputRef = React.useRef<HTMLInputElement>(null);
52
- const [uploading, setUploading] = React.useState(false);
53
- const [error, setError] = React.useState<string | null>(null);
54
- const busy = Boolean(disabled || uploading);
55
-
56
- const handleFile = async (file: File) => {
57
- if (!uploadFile) {
58
- setError("File upload is not configured for this panel.");
59
- return;
60
- }
61
- setUploading(true);
62
- setError(null);
63
- try {
64
- onChange(await uploadFile(file));
65
- } catch (err) {
66
- setError(err instanceof Error ? err.message : "Upload failed");
67
- } finally {
68
- setUploading(false);
69
- if (inputRef.current) inputRef.current.value = "";
70
- }
71
- };
72
-
73
- const handleUrl = async (imageUrl: string) => {
74
- const trimmed = imageUrl.trim();
75
- if (!trimmed) {
76
- onChange("");
77
- return;
78
- }
79
- if (trimmed === value) return;
80
-
81
- // Absolute URLs — and any paste when no transform is configured — apply
82
- // as-is. Paste is never gated behind a disabled input.
83
- if (!uploadFromUrl || /^https?:\/\//i.test(trimmed)) {
84
- setError(null);
85
- onChange(trimmed);
86
- return;
87
- }
88
-
89
- setUploading(true);
90
- setError(null);
91
- try {
92
- onChange(await uploadFromUrl(trimmed));
93
- } catch (err) {
94
- setError(err instanceof Error ? err.message : "Upload failed");
95
- } finally {
96
- setUploading(false);
97
- }
98
- };
99
-
100
- return (
101
- <div className={cn("space-y-2", className)}>
102
- {label ? (
103
- <div className="text-muted-foreground text-xs font-medium">{label}</div>
104
- ) : null}
105
-
106
- <div className="flex flex-wrap items-center gap-2">
107
- <input
108
- ref={inputRef}
109
- type="file"
110
- accept={accept}
111
- className="hidden"
112
- disabled={busy}
113
- onChange={(e) => {
114
- const file = e.target.files?.[0];
115
- if (file) void handleFile(file);
116
- }}
117
- />
118
- <Button
119
- type="button"
120
- variant="outline"
121
- size="sm"
122
- disabled={busy}
123
- onClick={() => inputRef.current?.click()}
124
- >
125
- {uploading ? (
126
- <Loader2 className="h-4 w-4 animate-spin" />
127
- ) : (
128
- <ImagePlus className="h-4 w-4" />
129
- )}
130
- {uploading ? "Uploading…" : "Upload"}
131
- </Button>
132
- {value ? (
133
- <Button
134
- type="button"
135
- variant="ghost"
136
- size="sm"
137
- disabled={busy}
138
- onClick={() => onChange("")}
139
- aria-label="Clear image"
140
- >
141
- <X className="h-4 w-4" />
142
- Clear
143
- </Button>
144
- ) : null}
145
- </div>
146
-
147
- {allowUrlPaste ? (
148
- <Input
149
- key={value || "empty"}
150
- defaultValue={value}
151
- disabled={busy}
152
- placeholder="Or paste image URL…"
153
- onBlur={(e) => {
154
- void handleUrl(e.target.value);
155
- }}
156
- onKeyDown={(e) => {
157
- if (e.key === "Enter") {
158
- e.preventDefault();
159
- void handleUrl((e.target as HTMLInputElement).value);
160
- }
161
- }}
162
- />
163
- ) : null}
164
-
165
- {value ? (
166
- // eslint-disable-next-line @next/next/no-img-element -- CDN URLs vary
167
- <img
168
- src={value}
169
- alt=""
170
- className="border-border max-h-24 rounded border bg-white object-contain p-1"
171
- />
172
- ) : null}
173
-
174
- {error ? (
175
- <p className="text-destructive text-xs" role="alert">
176
- {error}
177
- </p>
178
- ) : null}
179
- </div>
180
- );
181
- }
package/src/index.ts DELETED
@@ -1,37 +0,0 @@
1
- export * from "./alert";
2
- export * from "./autocomplete";
3
- export * from "./badge";
4
- export * from "./breadcrumb";
5
- export * from "./button";
6
- export * from "./card";
7
- export * from "./checkbox";
8
- export * from "./clipboard";
9
- export * from "./collapsible";
10
- export * from "./confirm-dialog";
11
- export * from "./copy-button";
12
- export * from "./data-table";
13
- export * from "./date-range-picker";
14
- export * from "./date-range-picker/dateRange";
15
- export * from "./dialogLayer";
16
- export * from "./feature-access-gate";
17
- export * from "./field";
18
- export * from "./image-uploader";
19
- export * from "./input";
20
- export * from "./label";
21
- export * from "./modal";
22
- export * from "./page-header";
23
- export * from "./panel-brand-mark";
24
- export * from "./panel-sign-in";
25
- export * from "./pagination";
26
- export * from "./segmented-control";
27
- export * from "./select";
28
- export * from "./separator";
29
- export * from "./side-drawer";
30
- export * from "./spinner";
31
- export * from "./spreadsheet-grid";
32
- export * from "./state-panel";
33
- export * from "./switch";
34
- export * from "./table";
35
- export * from "./tabs";
36
- export * from "./textarea";
37
- export * from "./utils";
package/src/input.tsx DELETED
@@ -1,16 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "./utils";
4
-
5
- export const inputClass =
6
- "h-9 w-full rounded-md border border-border bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
7
-
8
- export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
9
-
10
- export const Input = React.forwardRef<HTMLInputElement, InputProps>(
11
- ({ className, ...props }, ref) => (
12
- <input ref={ref} className={cn(inputClass, className)} {...props} />
13
- ),
14
- );
15
-
16
- Input.displayName = "Input";
package/src/label.tsx DELETED
@@ -1,23 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
-
5
- import { cn } from "./utils";
6
-
7
- export type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
8
-
9
- /** Form label — pair with a native control via `htmlFor`. */
10
- export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
11
- ({ className, ...props }, ref) => (
12
- <label
13
- ref={ref}
14
- className={cn(
15
- "text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
16
- className,
17
- )}
18
- {...props}
19
- />
20
- ),
21
- );
22
-
23
- Label.displayName = "Label";
package/src/modal.tsx DELETED
@@ -1,117 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { createPortal } from "react-dom";
5
-
6
- import { useDialogLayer } from "./dialogLayer";
7
- import { cn } from "./utils";
8
-
9
- const emptySubscribe = () => () => {};
10
-
11
- /**
12
- * Returns false during SSR and the first client render, then true once mounted.
13
- * `useSyncExternalStore` is the React-recommended way to read client-only state
14
- * without a setState-in-effect (which triggers cascading renders).
15
- */
16
- export function useHasMounted(): boolean {
17
- return React.useSyncExternalStore(
18
- emptySubscribe,
19
- () => true,
20
- () => false,
21
- );
22
- }
23
-
24
- interface ModalProps {
25
- isOpen: boolean;
26
- /** id of the element labelling the dialog; wired to aria-labelledby. */
27
- labelledBy: string;
28
- /**
29
- * Optional id(s) of elements describing the dialog (APG alertdialog body).
30
- * Space-separated when multiple.
31
- */
32
- describedBy?: string;
33
- /** Accessible label for the backdrop dismiss button. */
34
- closeLabel: string;
35
- onClose: () => void;
36
- children: React.ReactNode;
37
- /**
38
- * When false, Escape and backdrop clicks do not close the modal (e.g. while a
39
- * save is in flight). Defaults to true.
40
- */
41
- dismissible?: boolean;
42
- /** Use "alertdialog" for destructive confirmations. Defaults to "dialog". */
43
- role?: "dialog" | "alertdialog";
44
- /** Tailwind max-width class for the panel. Defaults to "max-w-md". */
45
- panelClassName?: string;
46
- }
47
-
48
- /**
49
- * Shared modal shell: renders a portal-backed, scroll-locked, Escape-dismissible
50
- * overlay with a centered panel. Callers provide the panel body (heading, form,
51
- * footer). Consolidates the boilerplate previously copied across every dialog.
52
- */
53
- export function Modal({
54
- isOpen,
55
- labelledBy,
56
- describedBy,
57
- closeLabel,
58
- onClose,
59
- children,
60
- dismissible = true,
61
- role = "dialog",
62
- panelClassName,
63
- }: ModalProps) {
64
- const mounted = useHasMounted();
65
- const panelRef = React.useRef<HTMLDivElement>(null);
66
-
67
- useDialogLayer({
68
- // Gated on `mounted` too: before that the panel is not rendered, so there
69
- // would be nothing to move focus into.
70
- active: isOpen && mounted,
71
- containerRef: panelRef,
72
- onEscape: dismissible ? onClose : undefined,
73
- });
74
-
75
- if (!mounted || !isOpen) return null;
76
-
77
- return createPortal(
78
- /*
79
- `overflow-y-auto` on the overlay, `my-auto` on the panel — not
80
- `items-center`. A flex item centred with `align-items` overflows equally
81
- in both directions once it is taller than its container, and the half
82
- above the top edge cannot be scrolled to: with the page behind it locked,
83
- an over-tall dialog was simply unreachable at both ends. Auto margins
84
- centre a short panel the same way and collapse to zero on a tall one,
85
- which scrolls.
86
- */
87
- <div className="fixed inset-0 z-[100] flex justify-center overflow-y-auto p-4">
88
- {/* `fixed`, not `absolute`: inside a scrolling overlay an absolute
89
- backdrop scrolls away with the content and leaves the rest of a tall
90
- dialog sitting on an undimmed page. */}
91
- <button
92
- type="button"
93
- aria-label={closeLabel}
94
- tabIndex={-1}
95
- className="fixed inset-0 bg-slate-950/50"
96
- onClick={() => {
97
- if (dismissible) onClose();
98
- }}
99
- />
100
- <div
101
- ref={panelRef}
102
- role={role}
103
- aria-modal="true"
104
- aria-labelledby={labelledBy}
105
- aria-describedby={describedBy}
106
- tabIndex={-1}
107
- className={cn(
108
- "border-border bg-background relative z-10 my-auto w-full rounded-lg border shadow-2xl outline-none",
109
- panelClassName ?? "max-w-md p-6",
110
- )}
111
- >
112
- {children}
113
- </div>
114
- </div>,
115
- document.body,
116
- );
117
- }
@@ -1,75 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "./utils";
4
-
5
- /**
6
- * The title block every panel opens with.
7
- *
8
- * Twenty Managers wrote the same `<h1>` and subtitle by hand, differing only in
9
- * whether the row said `gap-3` or `gap-x-4 gap-y-2` and whether the inner block
10
- * carried `min-w-0` — differences nobody chose, which is what made them worth
11
- * removing.
12
- *
13
- * `description` takes a node rather than a string because several panels put a
14
- * `<code>` in theirs, naming the Firebase field they write. That is the useful
15
- * kind of subtitle and it should not have to opt out of the shared component to
16
- * stay.
17
- *
18
- * `min-w-0` is not optional here. Without it a long title refuses to shrink and
19
- * pushes the actions off the row instead of wrapping — which is the bug the
20
- * three panels that set it had already hit, and the seven that did not had not
21
- * yet.
22
- */
23
- export interface PageHeaderProps {
24
- title: React.ReactNode;
25
- description?: React.ReactNode;
26
- /**
27
- * Sits above the title: a breadcrumb, or the back link the filter editors
28
- * use. Four panels had one, each spacing it by hand.
29
- */
30
- eyebrow?: React.ReactNode;
31
- /** Buttons belonging to the page, laid out at the end of the title row. */
32
- actions?: React.ReactNode;
33
- className?: string;
34
- }
35
-
36
- export function PageHeader({
37
- title,
38
- description,
39
- eyebrow,
40
- actions,
41
- className,
42
- }: PageHeaderProps) {
43
- const heading = (
44
- <div className="min-w-0">
45
- {eyebrow}
46
- <h1
47
- className={cn(
48
- "text-2xl font-semibold tracking-tight",
49
- eyebrow != null && "mt-1",
50
- )}
51
- >
52
- {title}
53
- </h1>
54
- {description != null ? (
55
- <p className="text-muted-foreground mt-1 text-sm">{description}</p>
56
- ) : null}
57
- </div>
58
- );
59
-
60
- if (!actions) {
61
- return <div className={className}>{heading}</div>;
62
- }
63
-
64
- return (
65
- <div
66
- className={cn(
67
- "flex flex-wrap items-center justify-between gap-3",
68
- className,
69
- )}
70
- >
71
- {heading}
72
- {actions}
73
- </div>
74
- );
75
- }