@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
@@ -0,0 +1,126 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-HCA5ODCD.js";
4
+
5
+ // src/tabs.tsx
6
+ import * as React from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ function TabPanel({
9
+ id,
10
+ labelledBy,
11
+ active,
12
+ children,
13
+ className
14
+ }) {
15
+ return /* @__PURE__ */ jsx(
16
+ "div",
17
+ {
18
+ role: "tabpanel",
19
+ id,
20
+ "aria-labelledby": labelledBy,
21
+ hidden: !active,
22
+ className,
23
+ children: active ? children : null
24
+ }
25
+ );
26
+ }
27
+ function Tabs({
28
+ tabs,
29
+ value,
30
+ onChange,
31
+ ariaLabel,
32
+ className
33
+ }) {
34
+ const refs = React.useRef(/* @__PURE__ */ new Map());
35
+ const reactId = React.useId();
36
+ const hasInlinePanels = tabs.some((tab) => tab.panel != null);
37
+ function tabDomId(key) {
38
+ return `${reactId}-tab-${key}`;
39
+ }
40
+ function panelDomId(tab) {
41
+ return tab.panelId ?? `${reactId}-panel-${tab.key}`;
42
+ }
43
+ function focusTab(key) {
44
+ onChange(key);
45
+ refs.current.get(key)?.focus();
46
+ }
47
+ function onKeyDown(event) {
48
+ const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
49
+ if (step === 0) return;
50
+ const selectable = tabs.filter((tab) => !tab.disabled);
51
+ if (selectable.length < 2) return;
52
+ const current = selectable.findIndex((tab) => tab.key === value);
53
+ const next = selectable[(current + step + selectable.length) % selectable.length];
54
+ event.preventDefault();
55
+ focusTab(next.key);
56
+ }
57
+ const tablist = /* @__PURE__ */ jsx(
58
+ "div",
59
+ {
60
+ role: "tablist",
61
+ "aria-label": ariaLabel,
62
+ onKeyDown,
63
+ className: cn(
64
+ "border-border flex flex-wrap gap-1 border-b",
65
+ !hasInlinePanels ? className : void 0
66
+ ),
67
+ children: tabs.map((tab) => {
68
+ const active = tab.key === value;
69
+ const id = tabDomId(tab.key);
70
+ const controls = tab.panel != null || tab.panelId ? panelDomId(tab) : void 0;
71
+ return /* @__PURE__ */ jsxs(
72
+ "button",
73
+ {
74
+ id,
75
+ ref: (node) => {
76
+ refs.current.set(tab.key, node);
77
+ },
78
+ type: "button",
79
+ role: "tab",
80
+ "aria-selected": active,
81
+ "aria-controls": controls,
82
+ disabled: tab.disabled,
83
+ tabIndex: active ? 0 : -1,
84
+ onClick: () => onChange(tab.key),
85
+ className: cn(
86
+ "cursor-pointer rounded-t-md px-3 py-1.5 text-sm font-medium transition",
87
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
88
+ "disabled:cursor-not-allowed disabled:opacity-50",
89
+ // The filled-pill active state the five hand-rolled strips
90
+ // already used. Adopting it keeps this a deduplication rather
91
+ // than a restyle of every panel that has tabs.
92
+ active ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"
93
+ ),
94
+ children: [
95
+ tab.label,
96
+ tab.badge != null ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground ml-1.5 text-xs", children: tab.badge }) : null
97
+ ]
98
+ },
99
+ tab.key
100
+ );
101
+ })
102
+ }
103
+ );
104
+ if (!hasInlinePanels) return tablist;
105
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), children: [
106
+ tablist,
107
+ tabs.map(
108
+ (tab) => tab.panel != null ? /* @__PURE__ */ jsx(
109
+ TabPanel,
110
+ {
111
+ id: panelDomId(tab),
112
+ labelledBy: tabDomId(tab.key),
113
+ active: tab.key === value,
114
+ children: tab.panel
115
+ },
116
+ tab.key
117
+ ) : null
118
+ )
119
+ ] });
120
+ }
121
+
122
+ export {
123
+ TabPanel,
124
+ Tabs
125
+ };
126
+ //# sourceMappingURL=chunk-ZFSQVMLJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tabs.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"./utils\";\n\n/**\n * The tab strip five Managers were each drawing by hand, with the same\n * `rounded-t-md px-3 py-1.5 text-sm font-medium` and their own idea of what an\n * active tab looks like.\n *\n * It is a controlled component with no state of its own: every caller already\n * held the active key, usually because a URL or a query depends on it.\n *\n * Roles are the reason to use this rather than styling buttons again. A hand\n * -rolled strip is a row of buttons to a screen reader, which says nothing\n * about how many tabs there are or which is selected; `tablist` / `tab` /\n * `aria-selected` do. Arrow-key navigation comes with that expectation, so it\n * is implemented here too.\n *\n * Optional `panel` on a tab (or a sibling `TabPanel`) wires APG `aria-controls`\n * / `role=\"tabpanel\"`. Callers that already render content from `value` can\n * keep doing so and pass `panelId` for association, or switch to `panel`.\n */\nexport interface TabItem<Key extends string = string> {\n key: Key;\n label: React.ReactNode;\n /** Rendered after the label — a count, usually. */\n badge?: React.ReactNode;\n disabled?: boolean;\n /**\n * Optional panel body. When any tab provides `panel`, panels render under\n * the strip with `role=\"tabpanel\"`.\n */\n panel?: React.ReactNode;\n /**\n * External panel element id for `aria-controls` when panels live outside\n * this component.\n */\n panelId?: string;\n}\n\nexport interface TabsProps<Key extends string = string> {\n tabs: ReadonlyArray<TabItem<Key>>;\n value: Key;\n onChange: (key: Key) => void;\n /** Names the strip for screen readers. */\n ariaLabel?: string;\n className?: string;\n}\n\nexport interface TabPanelProps {\n /** Must match the controlling tab's generated or explicit id. */\n id: string;\n /** Id of the tab that labels this panel. */\n labelledBy: string;\n active: boolean;\n children: React.ReactNode;\n className?: string;\n}\n\n/** Companion panel for callers that keep content outside `Tabs`. */\nexport function TabPanel({\n id,\n labelledBy,\n active,\n children,\n className,\n}: TabPanelProps) {\n return (\n <div\n role=\"tabpanel\"\n id={id}\n aria-labelledby={labelledBy}\n hidden={!active}\n className={className}\n >\n {active ? children : null}\n </div>\n );\n}\n\nexport function Tabs<Key extends string = string>({\n tabs,\n value,\n onChange,\n ariaLabel,\n className,\n}: TabsProps<Key>) {\n const refs = React.useRef(new Map<Key, HTMLButtonElement | null>());\n const reactId = React.useId();\n const hasInlinePanels = tabs.some((tab) => tab.panel != null);\n\n function tabDomId(key: Key) {\n return `${reactId}-tab-${key}`;\n }\n\n function panelDomId(tab: TabItem<Key>) {\n return tab.panelId ?? `${reactId}-panel-${tab.key}`;\n }\n\n function focusTab(key: Key) {\n onChange(key);\n // The newly selected tab has to take focus, or the next arrow press is\n // still measured from the old one.\n refs.current.get(key)?.focus();\n }\n\n function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n const step =\n event.key === \"ArrowRight\" ? 1 : event.key === \"ArrowLeft\" ? -1 : 0;\n if (step === 0) return;\n\n const selectable = tabs.filter((tab) => !tab.disabled);\n if (selectable.length < 2) return;\n\n const current = selectable.findIndex((tab) => tab.key === value);\n // Wraps, which is what a tablist is expected to do.\n const next =\n selectable[(current + step + selectable.length) % selectable.length];\n\n event.preventDefault();\n focusTab(next.key);\n }\n\n const tablist = (\n <div\n role=\"tablist\"\n aria-label={ariaLabel}\n onKeyDown={onKeyDown}\n className={cn(\n \"border-border flex flex-wrap gap-1 border-b\",\n !hasInlinePanels ? className : undefined,\n )}\n >\n {tabs.map((tab) => {\n const active = tab.key === value;\n const id = tabDomId(tab.key);\n const controls =\n tab.panel != null || tab.panelId ? panelDomId(tab) : undefined;\n return (\n <button\n key={tab.key}\n id={id}\n ref={(node) => {\n refs.current.set(tab.key, node);\n }}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n aria-controls={controls}\n disabled={tab.disabled}\n // Only the active tab is reachable by Tab; arrows move within the\n // strip. Without this a keyboard user pages through every tab to\n // get past the strip.\n tabIndex={active ? 0 : -1}\n onClick={() => onChange(tab.key)}\n className={cn(\n \"cursor-pointer rounded-t-md px-3 py-1.5 text-sm font-medium transition\",\n \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n // The filled-pill active state the five hand-rolled strips\n // already used. Adopting it keeps this a deduplication rather\n // than a restyle of every panel that has tabs.\n active\n ? \"bg-primary text-primary-foreground\"\n : \"text-muted-foreground hover:bg-muted\",\n )}\n >\n {tab.label}\n {tab.badge != null ? (\n <span className=\"text-muted-foreground ml-1.5 text-xs\">\n {tab.badge}\n </span>\n ) : null}\n </button>\n );\n })}\n </div>\n );\n\n if (!hasInlinePanels) return tablist;\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {tablist}\n {tabs.map((tab) =>\n tab.panel != null ? (\n <TabPanel\n key={tab.key}\n id={panelDomId(tab)}\n labelledBy={tabDomId(tab.key)}\n active={tab.key === value}\n >\n {tab.panel}\n </TabPanel>\n ) : null,\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,46 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-HCA5ODCD.js";
4
+
5
+ // src/switch.tsx
6
+ import * as React from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ var Switch = React.forwardRef(
9
+ ({ checked, onCheckedChange, className, disabled, id, ...props }, ref) => /* @__PURE__ */ jsx(
10
+ "button",
11
+ {
12
+ ref,
13
+ id,
14
+ type: "button",
15
+ role: "switch",
16
+ "aria-checked": checked,
17
+ disabled,
18
+ onClick: () => onCheckedChange(!checked),
19
+ className: cn(
20
+ // 24×44px track meets DESIGN.md 24px minimum target size.
21
+ "relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border transition-colors",
22
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
23
+ "disabled:cursor-not-allowed disabled:opacity-50",
24
+ checked ? "border-primary bg-primary" : "border-border bg-muted",
25
+ className
26
+ ),
27
+ ...props,
28
+ children: /* @__PURE__ */ jsx(
29
+ "span",
30
+ {
31
+ "aria-hidden": true,
32
+ className: cn(
33
+ "bg-background pointer-events-none block size-5 rounded-full shadow-sm transition-transform",
34
+ checked ? "translate-x-5" : "translate-x-0.5"
35
+ )
36
+ }
37
+ )
38
+ }
39
+ )
40
+ );
41
+ Switch.displayName = "Switch";
42
+
43
+ export {
44
+ Switch
45
+ };
46
+ //# sourceMappingURL=chunk-ZHATD73Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/switch.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"./utils\";\n\nexport type SwitchProps = Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n \"onChange\"\n> & {\n checked: boolean;\n onCheckedChange: (checked: boolean) => void;\n};\n\n/** Compact accessible toggle — use instead of full-width checkbox inputs. */\nexport const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(\n ({ checked, onCheckedChange, className, disabled, id, ...props }, ref) => (\n <button\n ref={ref}\n id={id}\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n disabled={disabled}\n onClick={() => onCheckedChange(!checked)}\n className={cn(\n // 24×44px track meets DESIGN.md 24px minimum target size.\n \"relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border transition-colors\",\n \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n checked ? \"border-primary bg-primary\" : \"border-border bg-muted\",\n className,\n )}\n {...props}\n >\n <span\n aria-hidden\n className={cn(\n \"bg-background pointer-events-none block size-5 rounded-full shadow-sm transition-transform\",\n checked ? \"translate-x-5\" : \"translate-x-0.5\",\n )}\n />\n </button>\n ),\n);\n\nSwitch.displayName = \"Switch\";\n"],"mappings":";;;;;AAEA,YAAY,WAAW;AAiCjB;AApBC,IAAM,SAAe;AAAA,EAC1B,CAAC,EAAE,SAAS,iBAAiB,WAAW,UAAU,IAAI,GAAG,MAAM,GAAG,QAChE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,MAAK;AAAA,MACL,MAAK;AAAA,MACL,gBAAc;AAAA,MACd;AAAA,MACA,SAAS,MAAM,gBAAgB,CAAC,OAAO;AAAA,MACvC,WAAW;AAAA;AAAA,QAET;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,8BAA8B;AAAA,QACxC;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC;AAAA;AAAA,UACC,eAAW;AAAA,UACX,WAAW;AAAA,YACT;AAAA,YACA,UAAU,kBAAkB;AAAA,UAC9B;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAEA,OAAO,cAAc;","names":[]}
@@ -0,0 +1,111 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-HCA5ODCD.js";
4
+
5
+ // src/data-table.tsx
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ var HEADER_VARIANTS = {
8
+ uppercase: "border-border bg-muted/40 text-muted-foreground border-b text-xs tracking-wide uppercase",
9
+ plain: "text-muted-foreground border-border border-b text-left"
10
+ };
11
+ var DENSITY = {
12
+ comfortable: "px-4 py-3",
13
+ compact: "px-3 py-2"
14
+ };
15
+ var ALIGN = {
16
+ left: "text-left",
17
+ right: "text-right",
18
+ center: "text-center"
19
+ };
20
+ function DataTable({
21
+ columns,
22
+ rows,
23
+ rowKey,
24
+ variant = "uppercase",
25
+ density = "comfortable",
26
+ stickyHeader = false,
27
+ rowClassName,
28
+ emptyMessage,
29
+ footer,
30
+ ariaLabel,
31
+ className,
32
+ tableClassName
33
+ }) {
34
+ return /* @__PURE__ */ jsx(
35
+ "div",
36
+ {
37
+ className: cn(
38
+ "border-border bg-background/80 overflow-x-auto rounded-lg border",
39
+ className
40
+ ),
41
+ children: /* @__PURE__ */ jsxs(
42
+ "table",
43
+ {
44
+ "aria-label": ariaLabel,
45
+ className: cn("min-w-full text-left text-sm", tableClassName),
46
+ children: [
47
+ /* @__PURE__ */ jsx(
48
+ "thead",
49
+ {
50
+ className: cn(
51
+ HEADER_VARIANTS[variant],
52
+ stickyHeader && "bg-muted sticky top-0 z-10"
53
+ ),
54
+ children: /* @__PURE__ */ jsx("tr", { children: columns.map((column) => /* @__PURE__ */ jsx(
55
+ "th",
56
+ {
57
+ scope: "col",
58
+ "aria-sort": column.ariaSort,
59
+ className: cn(
60
+ DENSITY[density],
61
+ "font-medium",
62
+ ALIGN[column.align ?? "left"],
63
+ column.headerClassName
64
+ ),
65
+ children: column.title
66
+ },
67
+ column.key
68
+ )) })
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsx("tbody", { children: rows.length === 0 && emptyMessage != null ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
72
+ "td",
73
+ {
74
+ colSpan: columns.length,
75
+ className: "text-muted-foreground px-4 py-6 text-center text-sm",
76
+ children: emptyMessage
77
+ }
78
+ ) }) : rows.map((row, index) => /* @__PURE__ */ jsx(
79
+ "tr",
80
+ {
81
+ className: cn(
82
+ "border-border/70 border-b last:border-0",
83
+ rowClassName?.(row, index)
84
+ ),
85
+ children: columns.map((column) => /* @__PURE__ */ jsx(
86
+ "td",
87
+ {
88
+ className: cn(
89
+ DENSITY[density],
90
+ ALIGN[column.align ?? "left"],
91
+ column.className
92
+ ),
93
+ children: column.cell(row, index)
94
+ },
95
+ column.key
96
+ ))
97
+ },
98
+ rowKey(row, index)
99
+ )) }),
100
+ footer != null ? /* @__PURE__ */ jsx("tfoot", { children: footer }) : null
101
+ ]
102
+ }
103
+ )
104
+ }
105
+ );
106
+ }
107
+
108
+ export {
109
+ DataTable
110
+ };
111
+ //# sourceMappingURL=chunk-ZLAA4DGG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data-table.tsx"],"sourcesContent":["import * as React from \"react\";\n\nimport { cn } from \"./utils\";\n\n/**\n * The list table every feature panel was hand-rolling.\n *\n * Seventeen files repeated the same wrapper string\n * (`border-border bg-background/80 overflow-x-auto rounded-lg border`) and the\n * header had already drifted into two incompatible looks, so Campaigns and\n * Targets did not resemble each other. Both are kept as `variant` rather than\n * unified here: collapsing them is a visual change to ten panels and belongs in\n * its own reviewable diff, not smuggled in with a refactor.\n *\n * The column contract deliberately echoes `GridColumn` in\n * `spreadsheet-grid.tsx` — `key`, `title`, a renderer, an optional class — so\n * the repo has one shape to learn. It drops that type's `value: (row) => string`\n * because there is no clipboard or CSV here; nothing would keep it honest, and\n * a required field nobody reads gets filled in with lies.\n *\n * **Not for every table.** `GroupTable` renders parent and child rows with\n * different column counts, and `BusinessTable`'s rows are dnd-kit sortables that\n * own their own refs and transforms. `PublishModal`, `BulkCreateModal` and the\n * clone queue in `MetaAdSetsPanel` are grids of inputs rather than lists of\n * values — every cell is an editable control, and they size their header and\n * body rows differently on purpose. All five are legitimately tables of a\n * different kind; bending this component around them would cost more than it\n * saves.\n */\nexport type TableColumn<Row> = {\n key: string;\n /** Node, not string: a header can carry a select-all box or a sort control. */\n title: React.ReactNode;\n cell: (row: Row, index: number) => React.ReactNode;\n align?: \"left\" | \"right\" | \"center\";\n /**\n * Set on a sortable column so a screen reader announces the current sort.\n * Only the sorted column says `ascending`/`descending`; the rest say `none`,\n * which is what marks them as sortable at all.\n */\n ariaSort?: \"ascending\" | \"descending\" | \"none\";\n /** Extra classes for this column's `<td>`. */\n className?: string;\n /** Extra classes for this column's `<th>`. */\n headerClassName?: string;\n};\n\nexport interface DataTableProps<Row> {\n columns: TableColumn<Row>[];\n rows: Row[];\n /** Stable React key per row. */\n rowKey: (row: Row, index: number) => string;\n /**\n * `uppercase` — the tinted small-caps header (campaigns, showcase, filters).\n * `plain` — the quieter left-aligned header (ads, targets, creatives).\n */\n variant?: \"uppercase\" | \"plain\";\n /**\n * `comfortable` — the roomy default, for tables of a handful of columns.\n * `compact` — what every table of eight columns or more had already chosen by\n * hand, because the roomy padding pushes them into a horizontal scroll.\n */\n density?: \"comfortable\" | \"compact\";\n /** Pins the header for a table inside its own scroll container. */\n stickyHeader?: boolean;\n rowClassName?: (row: Row, index: number) => string | undefined;\n /** Rendered in place of `<tbody>` rows when there are none. */\n emptyMessage?: React.ReactNode;\n /**\n * Rows for a `<tfoot>` — a totals line, typically. The caller supplies the\n * `<tr>` itself because a summary row rarely lines up with the columns above\n * it; designer-counts spans its first two.\n */\n footer?: React.ReactNode;\n /** Names the table for screen readers when no visible caption fits. */\n ariaLabel?: string;\n /** Classes for the scroll wrapper. */\n className?: string;\n /** Classes for the `<table>`. */\n tableClassName?: string;\n}\n\nconst HEADER_VARIANTS = {\n uppercase:\n \"border-border bg-muted/40 text-muted-foreground border-b text-xs tracking-wide uppercase\",\n plain: \"text-muted-foreground border-border border-b text-left\",\n} as const;\n\nconst DENSITY = {\n comfortable: \"px-4 py-3\",\n compact: \"px-3 py-2\",\n} as const;\n\nconst ALIGN = {\n left: \"text-left\",\n right: \"text-right\",\n center: \"text-center\",\n} as const;\n\nexport function DataTable<Row>({\n columns,\n rows,\n rowKey,\n variant = \"uppercase\",\n density = \"comfortable\",\n stickyHeader = false,\n rowClassName,\n emptyMessage,\n footer,\n ariaLabel,\n className,\n tableClassName,\n}: DataTableProps<Row>) {\n return (\n <div\n className={cn(\n \"border-border bg-background/80 overflow-x-auto rounded-lg border\",\n className,\n )}\n >\n <table\n aria-label={ariaLabel}\n className={cn(\"min-w-full text-left text-sm\", tableClassName)}\n >\n <thead\n className={cn(\n HEADER_VARIANTS[variant],\n stickyHeader && \"bg-muted sticky top-0 z-10\",\n )}\n >\n <tr>\n {columns.map((column) => (\n <th\n key={column.key}\n scope=\"col\"\n aria-sort={column.ariaSort}\n className={cn(\n DENSITY[density],\n \"font-medium\",\n ALIGN[column.align ?? \"left\"],\n column.headerClassName,\n )}\n >\n {column.title}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.length === 0 && emptyMessage != null ? (\n <tr>\n {/* One cell across the whole width — a short row would otherwise\n leave the message sitting under the first column. */}\n <td\n colSpan={columns.length}\n className=\"text-muted-foreground px-4 py-6 text-center text-sm\"\n >\n {emptyMessage}\n </td>\n </tr>\n ) : (\n rows.map((row, index) => (\n <tr\n key={rowKey(row, index)}\n className={cn(\n \"border-border/70 border-b last:border-0\",\n rowClassName?.(row, index),\n )}\n >\n {columns.map((column) => (\n <td\n key={column.key}\n className={cn(\n DENSITY[density],\n ALIGN[column.align ?? \"left\"],\n column.className,\n )}\n >\n {column.cell(row, index)}\n </td>\n ))}\n </tr>\n ))\n )}\n </tbody>\n {footer != null ? <tfoot>{footer}</tfoot> : null}\n </table>\n </div>\n );\n}\n"],"mappings":";;;;;AAwHM,SAYQ,KAZR;AAtCN,IAAM,kBAAkB;AAAA,EACtB,WACE;AAAA,EACF,OAAO;AACT;AAEA,IAAM,UAAU;AAAA,EACd,aAAa;AAAA,EACb,SAAS;AACX;AAEA,IAAM,QAAQ;AAAA,EACZ,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AACV;AAEO,SAAS,UAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AAAA,EACV,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,cAAY;AAAA,UACZ,WAAW,GAAG,gCAAgC,cAAc;AAAA,UAE5D;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,WAAW;AAAA,kBACT,gBAAgB,OAAO;AAAA,kBACvB,gBAAgB;AAAA,gBAClB;AAAA,gBAEA,8BAAC,QACE,kBAAQ,IAAI,CAAC,WACZ;AAAA,kBAAC;AAAA;AAAA,oBAEC,OAAM;AAAA,oBACN,aAAW,OAAO;AAAA,oBAClB,WAAW;AAAA,sBACT,QAAQ,OAAO;AAAA,sBACf;AAAA,sBACA,MAAM,OAAO,SAAS,MAAM;AAAA,sBAC5B,OAAO;AAAA,oBACT;AAAA,oBAEC,iBAAO;AAAA;AAAA,kBAVH,OAAO;AAAA,gBAWd,CACD,GACH;AAAA;AAAA,YACF;AAAA,YACA,oBAAC,WACE,eAAK,WAAW,KAAK,gBAAgB,OACpC,oBAAC,QAGC;AAAA,cAAC;AAAA;AAAA,gBACC,SAAS,QAAQ;AAAA,gBACjB,WAAU;AAAA,gBAET;AAAA;AAAA,YACH,GACF,IAEA,KAAK,IAAI,CAAC,KAAK,UACb;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAW;AAAA,kBACT;AAAA,kBACA,eAAe,KAAK,KAAK;AAAA,gBAC3B;AAAA,gBAEC,kBAAQ,IAAI,CAAC,WACZ;AAAA,kBAAC;AAAA;AAAA,oBAEC,WAAW;AAAA,sBACT,QAAQ,OAAO;AAAA,sBACf,MAAM,OAAO,SAAS,MAAM;AAAA,sBAC5B,OAAO;AAAA,oBACT;AAAA,oBAEC,iBAAO,KAAK,KAAK,KAAK;AAAA;AAAA,kBAPlB,OAAO;AAAA,gBAQd,CACD;AAAA;AAAA,cAjBI,OAAO,KAAK,KAAK;AAAA,YAkBxB,CACD,GAEL;AAAA,YACC,UAAU,OAAO,oBAAC,WAAO,kBAAO,IAAW;AAAA;AAAA;AAAA,MAC9C;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,56 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-HCA5ODCD.js";
4
+
5
+ // src/alert.tsx
6
+ import * as React from "react";
7
+ import { cva } from "class-variance-authority";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ var alertVariants = cva("rounded-lg border px-3 py-2 text-sm", {
10
+ variants: {
11
+ variant: {
12
+ danger: "border-destructive/30 bg-destructive/5 text-destructive",
13
+ warning: "border-warning/30 bg-warning/5 text-warning",
14
+ success: "border-success/30 bg-success/5 text-success",
15
+ info: "border-info/30 bg-info/5 text-info",
16
+ /**
17
+ * The quiet state of a banner that also has loud ones — "no default
18
+ * campaign set" next to "this is the default". Without it those callers
19
+ * keep a ternary of raw class strings and the shared component only
20
+ * covers half of what they draw.
21
+ */
22
+ neutral: "border-border bg-muted/40 text-muted-foreground"
23
+ }
24
+ },
25
+ defaultVariants: {
26
+ // Failure is what a banner almost always reports here, and defaulting to
27
+ // it means the noisy case cannot be forgotten.
28
+ variant: "danger"
29
+ }
30
+ });
31
+ var Alert = React.forwardRef(
32
+ ({ className, variant, action, children, ...props }, ref) => /* @__PURE__ */ jsxs(
33
+ "div",
34
+ {
35
+ ref,
36
+ role: variant === "danger" || variant == null ? "alert" : "status",
37
+ className: cn(
38
+ alertVariants({ variant }),
39
+ action && "flex flex-wrap items-center justify-between gap-3",
40
+ className
41
+ ),
42
+ ...props,
43
+ children: [
44
+ action ? /* @__PURE__ */ jsx("span", { children }) : children,
45
+ action
46
+ ]
47
+ }
48
+ )
49
+ );
50
+ Alert.displayName = "Alert";
51
+
52
+ export {
53
+ alertVariants,
54
+ Alert
55
+ };
56
+ //# sourceMappingURL=chunk-ZP252OJZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alert.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./utils\";\n\n/**\n * The inline message banner — a failed load, a warning above a form, a note.\n *\n * This was the most duplicated markup in the panel: thirty blocks across\n * twenty-eight files, spelled seven different ways. `px-3 py-2` in most places\n * and `px-4 py-3` in others, `text-rose-700` here and `text-destructive`\n * there — all meaning \"something went wrong\".\n *\n * `role` is derived from the variant rather than left to callers. An error a\n * user needs to act on should interrupt a screen reader; a note should not.\n * Almost none of the hand-rolled copies set either.\n */\nconst alertVariants = cva(\"rounded-lg border px-3 py-2 text-sm\", {\n variants: {\n variant: {\n danger: \"border-destructive/30 bg-destructive/5 text-destructive\",\n warning: \"border-warning/30 bg-warning/5 text-warning\",\n success: \"border-success/30 bg-success/5 text-success\",\n info: \"border-info/30 bg-info/5 text-info\",\n /**\n * The quiet state of a banner that also has loud ones — \"no default\n * campaign set\" next to \"this is the default\". Without it those callers\n * keep a ternary of raw class strings and the shared component only\n * covers half of what they draw.\n */\n neutral: \"border-border bg-muted/40 text-muted-foreground\",\n },\n },\n defaultVariants: {\n // Failure is what a banner almost always reports here, and defaulting to\n // it means the noisy case cannot be forgotten.\n variant: \"danger\",\n },\n});\n\nexport interface AlertProps\n extends\n React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof alertVariants> {\n /**\n * A control belonging to the message, such as Retry. Given one, the banner\n * lays out as a row and keeps the action at its end.\n */\n action?: React.ReactNode;\n}\n\nexport const Alert = React.forwardRef<HTMLDivElement, AlertProps>(\n ({ className, variant, action, children, ...props }, ref) => (\n <div\n ref={ref}\n role={variant === \"danger\" || variant == null ? \"alert\" : \"status\"}\n className={cn(\n alertVariants({ variant }),\n action && \"flex flex-wrap items-center justify-between gap-3\",\n className,\n )}\n {...props}\n >\n {/* Wrapped so the action sits opposite the text rather than being\n pushed around by whatever the caller passed as children. */}\n {action ? <span>{children}</span> : children}\n {action}\n </div>\n ),\n);\n\nAlert.displayName = \"Alert\";\n\nexport { alertVariants };\n"],"mappings":";;;;;AAAA,YAAY,WAAW;AACvB,SAAS,WAA8B;AAoDnC,SAYY,KAZZ;AApCJ,IAAM,gBAAgB,IAAI,uCAAuC;AAAA,EAC/D,UAAU;AAAA,IACR,SAAS;AAAA,MACP,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,SAAS;AAAA,MACT,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAON,SAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA;AAAA;AAAA,IAGf,SAAS;AAAA,EACX;AACF,CAAC;AAaM,IAAM,QAAc;AAAA,EACzB,CAAC,EAAE,WAAW,SAAS,QAAQ,UAAU,GAAG,MAAM,GAAG,QACnD;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAM,YAAY,YAAY,WAAW,OAAO,UAAU;AAAA,MAC1D,WAAW;AAAA,QACT,cAAc,EAAE,QAAQ,CAAC;AAAA,QACzB,UAAU;AAAA,QACV;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAIH;AAAA,iBAAS,oBAAC,UAAM,UAAS,IAAU;AAAA,QACnC;AAAA;AAAA;AAAA,EACH;AAEJ;AAEA,MAAM,cAAc;","names":[]}
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/clipboard.ts
21
+ var clipboard_exports = {};
22
+ __export(clipboard_exports, {
23
+ CSV_DELIMITER: () => CSV_DELIMITER,
24
+ TSV_DELIMITER: () => TSV_DELIMITER,
25
+ buildRangeText: () => buildRangeText,
26
+ countCells: () => countCells,
27
+ describeRange: () => describeRange,
28
+ escapeCell: () => escapeCell,
29
+ fullRange: () => fullRange,
30
+ normalizeRange: () => normalizeRange,
31
+ writeToClipboard: () => writeToClipboard
32
+ });
33
+ module.exports = __toCommonJS(clipboard_exports);
34
+ var TSV_DELIMITER = " ";
35
+ var CSV_DELIMITER = ",";
36
+ function normalizeRange(anchor, focus) {
37
+ if (!anchor || !focus) return null;
38
+ return {
39
+ r1: Math.min(anchor.r, focus.r),
40
+ r2: Math.max(anchor.r, focus.r),
41
+ c1: Math.min(anchor.c, focus.c),
42
+ c2: Math.max(anchor.c, focus.c)
43
+ };
44
+ }
45
+ function escapeCell(raw, delimiter) {
46
+ const value = raw == null ? "" : String(raw);
47
+ if (value.includes(delimiter) || value.includes("\n") || value.includes("\r") || value.includes('"')) {
48
+ return `"${value.replace(/"/g, '""')}"`;
49
+ }
50
+ return value;
51
+ }
52
+ function buildRangeText(range, columns, rows, delimiter, includeHeaders) {
53
+ if (!range) return "";
54
+ const lines = [];
55
+ if (includeHeaders) {
56
+ const header = [];
57
+ for (let c = range.c1; c <= range.c2; c++) {
58
+ header.push(escapeCell(columns[c]?.title, delimiter));
59
+ }
60
+ lines.push(header.join(delimiter));
61
+ }
62
+ for (let r = range.r1; r <= range.r2; r++) {
63
+ const line = [];
64
+ const row = rows[r];
65
+ for (let c = range.c1; c <= range.c2; c++) {
66
+ const column = columns[c];
67
+ line.push(
68
+ escapeCell(
69
+ column && row !== void 0 ? column.value(row) : "",
70
+ delimiter
71
+ )
72
+ );
73
+ }
74
+ lines.push(line.join(delimiter));
75
+ }
76
+ return lines.join("\n");
77
+ }
78
+ function fullRange(columns, rows) {
79
+ if (!rows.length || !columns.length) return null;
80
+ return { r1: 0, r2: rows.length - 1, c1: 0, c2: columns.length - 1 };
81
+ }
82
+ function describeRange(range) {
83
+ if (!range) return "No selection";
84
+ const rowCount = range.r2 - range.r1 + 1;
85
+ const colCount = range.c2 - range.c1 + 1;
86
+ if (rowCount === 1 && colCount === 1) return "1 cell selected";
87
+ return `${rowCount} \xD7 ${colCount} selected`;
88
+ }
89
+ function countCells(range) {
90
+ return {
91
+ rows: range.r2 - range.r1 + 1,
92
+ columns: range.c2 - range.c1 + 1
93
+ };
94
+ }
95
+ async function writeToClipboard(text) {
96
+ if (navigator.clipboard?.writeText) {
97
+ return navigator.clipboard.writeText(text);
98
+ }
99
+ const textarea = document.createElement("textarea");
100
+ textarea.value = text;
101
+ textarea.style.position = "fixed";
102
+ textarea.style.top = "-1000px";
103
+ document.body.appendChild(textarea);
104
+ textarea.select();
105
+ try {
106
+ if (!document.execCommand("copy")) {
107
+ throw new Error("Could not write to the clipboard");
108
+ }
109
+ } finally {
110
+ document.body.removeChild(textarea);
111
+ }
112
+ }
113
+ // Annotate the CommonJS export names for ESM import in node:
114
+ 0 && (module.exports = {
115
+ CSV_DELIMITER,
116
+ TSV_DELIMITER,
117
+ buildRangeText,
118
+ countCells,
119
+ describeRange,
120
+ escapeCell,
121
+ fullRange,
122
+ normalizeRange,
123
+ writeToClipboard
124
+ });
125
+ //# sourceMappingURL=clipboard.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/clipboard.ts"],"sourcesContent":["/**\n * Range maths and clipboard text for the card table's spreadsheet selection.\n *\n * Ported from video-shorts-panel `app/src/common/components/ExcelTable.jsx`.\n * Kept free of React and the DOM (except `writeToClipboard`) so the escaping\n * and range rules are unit-testable.\n */\n\nexport type CellRef = { r: number; c: number };\n\nexport type CellRange = { r1: number; r2: number; c1: number; c2: number };\n\n/** Tab for the clipboard (Excel/Sheets split on it), comma for a .csv file. */\nexport const TSV_DELIMITER = \"\\t\";\nexport const CSV_DELIMITER = \",\";\n\n/**\n * Anchor and focus can be dragged in any direction, so the range is whichever\n * corners they happen to be — normalize to top-left / bottom-right.\n */\nexport function normalizeRange(\n anchor: CellRef | null | undefined,\n focus: CellRef | null | undefined,\n): CellRange | null {\n if (!anchor || !focus) return null;\n return {\n r1: Math.min(anchor.r, focus.r),\n r2: Math.max(anchor.r, focus.r),\n c1: Math.min(anchor.c, focus.c),\n c2: Math.max(anchor.c, focus.c),\n };\n}\n\n/**\n * A cell needs quoting once it carries the delimiters themselves — the same\n * rule Excel uses when it parses the clipboard. Without it, a title containing\n * a tab or a newline silently becomes extra columns or rows on paste.\n */\nexport function escapeCell(raw: unknown, delimiter: string): string {\n const value = raw == null ? \"\" : String(raw);\n if (\n value.includes(delimiter) ||\n value.includes(\"\\n\") ||\n value.includes(\"\\r\") ||\n value.includes('\"')\n ) {\n return `\"${value.replace(/\"/g, '\"\"')}\"`;\n }\n return value;\n}\n\nexport type TextColumn<Row> = {\n title: string;\n value: (row: Row) => string;\n};\n\n/** Renders a range as delimited text, optionally with a header line. */\nexport function buildRangeText<Row>(\n range: CellRange | null,\n columns: TextColumn<Row>[],\n rows: Row[],\n delimiter: string,\n includeHeaders: boolean,\n): string {\n if (!range) return \"\";\n const lines: string[] = [];\n\n if (includeHeaders) {\n const header: string[] = [];\n for (let c = range.c1; c <= range.c2; c++) {\n header.push(escapeCell(columns[c]?.title, delimiter));\n }\n lines.push(header.join(delimiter));\n }\n\n for (let r = range.r1; r <= range.r2; r++) {\n const line: string[] = [];\n const row = rows[r];\n for (let c = range.c1; c <= range.c2; c++) {\n const column = columns[c];\n line.push(\n escapeCell(\n column && row !== undefined ? column.value(row) : \"\",\n delimiter,\n ),\n );\n }\n lines.push(line.join(delimiter));\n }\n\n return lines.join(\"\\n\");\n}\n\n/** The whole grid, or null when there is nothing to select. */\nexport function fullRange<Row>(\n columns: TextColumn<Row>[],\n rows: Row[],\n): CellRange | null {\n if (!rows.length || !columns.length) return null;\n return { r1: 0, r2: rows.length - 1, c1: 0, c2: columns.length - 1 };\n}\n\n/** \"3 × 2 selected\" — what the toolbar reports back to the user. */\nexport function describeRange(range: CellRange | null): string {\n if (!range) return \"No selection\";\n const rowCount = range.r2 - range.r1 + 1;\n const colCount = range.c2 - range.c1 + 1;\n if (rowCount === 1 && colCount === 1) return \"1 cell selected\";\n return `${rowCount} × ${colCount} selected`;\n}\n\nexport function countCells(range: CellRange): {\n rows: number;\n columns: number;\n} {\n return {\n rows: range.r2 - range.r1 + 1,\n columns: range.c2 - range.c1 + 1,\n };\n}\n\n/**\n * `navigator.clipboard` is unavailable on http origins and older browsers, so\n * fall back to a hidden textarea + execCommand rather than failing the copy.\n */\nexport async function writeToClipboard(text: string): Promise<void> {\n if (navigator.clipboard?.writeText) {\n return navigator.clipboard.writeText(text);\n }\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.style.position = \"fixed\";\n textarea.style.top = \"-1000px\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n if (!document.execCommand(\"copy\")) {\n throw new Error(\"Could not write to the clipboard\");\n }\n } finally {\n document.body.removeChild(textarea);\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Range maths and clipboard text for the card table's spreadsheet selection.
3
+ *
4
+ * Ported from video-shorts-panel `app/src/common/components/ExcelTable.jsx`.
5
+ * Kept free of React and the DOM (except `writeToClipboard`) so the escaping
6
+ * and range rules are unit-testable.
7
+ */
8
+ type CellRef = {
9
+ r: number;
10
+ c: number;
11
+ };
12
+ type CellRange = {
13
+ r1: number;
14
+ r2: number;
15
+ c1: number;
16
+ c2: number;
17
+ };
18
+ /** Tab for the clipboard (Excel/Sheets split on it), comma for a .csv file. */
19
+ declare const TSV_DELIMITER = "\t";
20
+ declare const CSV_DELIMITER = ",";
21
+ /**
22
+ * Anchor and focus can be dragged in any direction, so the range is whichever
23
+ * corners they happen to be — normalize to top-left / bottom-right.
24
+ */
25
+ declare function normalizeRange(anchor: CellRef | null | undefined, focus: CellRef | null | undefined): CellRange | null;
26
+ /**
27
+ * A cell needs quoting once it carries the delimiters themselves — the same
28
+ * rule Excel uses when it parses the clipboard. Without it, a title containing
29
+ * a tab or a newline silently becomes extra columns or rows on paste.
30
+ */
31
+ declare function escapeCell(raw: unknown, delimiter: string): string;
32
+ type TextColumn<Row> = {
33
+ title: string;
34
+ value: (row: Row) => string;
35
+ };
36
+ /** Renders a range as delimited text, optionally with a header line. */
37
+ declare function buildRangeText<Row>(range: CellRange | null, columns: TextColumn<Row>[], rows: Row[], delimiter: string, includeHeaders: boolean): string;
38
+ /** The whole grid, or null when there is nothing to select. */
39
+ declare function fullRange<Row>(columns: TextColumn<Row>[], rows: Row[]): CellRange | null;
40
+ /** "3 × 2 selected" — what the toolbar reports back to the user. */
41
+ declare function describeRange(range: CellRange | null): string;
42
+ declare function countCells(range: CellRange): {
43
+ rows: number;
44
+ columns: number;
45
+ };
46
+ /**
47
+ * `navigator.clipboard` is unavailable on http origins and older browsers, so
48
+ * fall back to a hidden textarea + execCommand rather than failing the copy.
49
+ */
50
+ declare function writeToClipboard(text: string): Promise<void>;
51
+
52
+ export { CSV_DELIMITER, type CellRange, type CellRef, TSV_DELIMITER, type TextColumn, buildRangeText, countCells, describeRange, escapeCell, fullRange, normalizeRange, writeToClipboard };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Range maths and clipboard text for the card table's spreadsheet selection.
3
+ *
4
+ * Ported from video-shorts-panel `app/src/common/components/ExcelTable.jsx`.
5
+ * Kept free of React and the DOM (except `writeToClipboard`) so the escaping
6
+ * and range rules are unit-testable.
7
+ */
8
+ type CellRef = {
9
+ r: number;
10
+ c: number;
11
+ };
12
+ type CellRange = {
13
+ r1: number;
14
+ r2: number;
15
+ c1: number;
16
+ c2: number;
17
+ };
18
+ /** Tab for the clipboard (Excel/Sheets split on it), comma for a .csv file. */
19
+ declare const TSV_DELIMITER = "\t";
20
+ declare const CSV_DELIMITER = ",";
21
+ /**
22
+ * Anchor and focus can be dragged in any direction, so the range is whichever
23
+ * corners they happen to be — normalize to top-left / bottom-right.
24
+ */
25
+ declare function normalizeRange(anchor: CellRef | null | undefined, focus: CellRef | null | undefined): CellRange | null;
26
+ /**
27
+ * A cell needs quoting once it carries the delimiters themselves — the same
28
+ * rule Excel uses when it parses the clipboard. Without it, a title containing
29
+ * a tab or a newline silently becomes extra columns or rows on paste.
30
+ */
31
+ declare function escapeCell(raw: unknown, delimiter: string): string;
32
+ type TextColumn<Row> = {
33
+ title: string;
34
+ value: (row: Row) => string;
35
+ };
36
+ /** Renders a range as delimited text, optionally with a header line. */
37
+ declare function buildRangeText<Row>(range: CellRange | null, columns: TextColumn<Row>[], rows: Row[], delimiter: string, includeHeaders: boolean): string;
38
+ /** The whole grid, or null when there is nothing to select. */
39
+ declare function fullRange<Row>(columns: TextColumn<Row>[], rows: Row[]): CellRange | null;
40
+ /** "3 × 2 selected" — what the toolbar reports back to the user. */
41
+ declare function describeRange(range: CellRange | null): string;
42
+ declare function countCells(range: CellRange): {
43
+ rows: number;
44
+ columns: number;
45
+ };
46
+ /**
47
+ * `navigator.clipboard` is unavailable on http origins and older browsers, so
48
+ * fall back to a hidden textarea + execCommand rather than failing the copy.
49
+ */
50
+ declare function writeToClipboard(text: string): Promise<void>;
51
+
52
+ export { CSV_DELIMITER, type CellRange, type CellRef, TSV_DELIMITER, type TextColumn, buildRangeText, countCells, describeRange, escapeCell, fullRange, normalizeRange, writeToClipboard };
@@ -0,0 +1,23 @@
1
+ import {
2
+ CSV_DELIMITER,
3
+ TSV_DELIMITER,
4
+ buildRangeText,
5
+ countCells,
6
+ describeRange,
7
+ escapeCell,
8
+ fullRange,
9
+ normalizeRange,
10
+ writeToClipboard
11
+ } from "./chunk-Y5WCT6XN.js";
12
+ export {
13
+ CSV_DELIMITER,
14
+ TSV_DELIMITER,
15
+ buildRangeText,
16
+ countCells,
17
+ describeRange,
18
+ escapeCell,
19
+ fullRange,
20
+ normalizeRange,
21
+ writeToClipboard
22
+ };
23
+ //# sourceMappingURL=clipboard.js.map