@eifi1/ui-kit 0.11.0 → 0.12.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 (342) hide show
  1. package/README.md +57 -32
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/amount-input.d.ts +24 -4
  12. package/dist/components/amount-input.js +5 -2
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/button-group.d.ts +51 -2
  21. package/dist/components/button-group.js +5 -1
  22. package/dist/components/button-group.js.map +1 -1
  23. package/dist/components/calculator.d.ts +15 -4
  24. package/dist/components/calendar-heatmap.d.ts +41 -3
  25. package/dist/components/calendar-heatmap.js +48 -7
  26. package/dist/components/calendar-heatmap.js.map +1 -1
  27. package/dist/components/chip.d.ts +2 -1
  28. package/dist/components/chip.js +6 -3
  29. package/dist/components/chip.js.map +1 -1
  30. package/dist/components/choice-card.d.ts +35 -3
  31. package/dist/components/choice-card.js +83 -25
  32. package/dist/components/choice-card.js.map +1 -1
  33. package/dist/components/combobox-core.js +2 -0
  34. package/dist/components/combobox-core.js.map +1 -1
  35. package/dist/components/combobox.d.ts +2 -2
  36. package/dist/components/combobox.js +31 -9
  37. package/dist/components/combobox.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +51 -70
  39. package/dist/components/copy-button.js +1 -1
  40. package/dist/components/copy-button.js.map +1 -1
  41. package/dist/components/data-table-cells.d.ts +63 -0
  42. package/dist/components/data-table-cells.js +64 -0
  43. package/dist/components/data-table-cells.js.map +1 -0
  44. package/dist/components/data-table-filter-popover.d.ts +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +31 -1
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +13 -0
  49. package/dist/components/data-table-labels.js +8 -1
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table.d.ts +1 -1
  52. package/dist/components/data-table.js +102 -12
  53. package/dist/components/data-table.js.map +1 -1
  54. package/dist/components/date-picker.d.ts +3 -1
  55. package/dist/components/date-picker.js +15 -2
  56. package/dist/components/date-picker.js.map +1 -1
  57. package/dist/components/description-list.d.ts +23 -3
  58. package/dist/components/description-list.js +23 -3
  59. package/dist/components/description-list.js.map +1 -1
  60. package/dist/components/dialog-frame.d.ts +1 -0
  61. package/dist/components/entity-combobox.d.ts +1 -1
  62. package/dist/components/entity-combobox.js +18 -3
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/error-boundary.d.ts +92 -0
  65. package/dist/components/error-boundary.js +111 -0
  66. package/dist/components/error-boundary.js.map +1 -0
  67. package/dist/components/field.d.ts +30 -4
  68. package/dist/components/field.js +40 -17
  69. package/dist/components/field.js.map +1 -1
  70. package/dist/components/file-button.d.ts +50 -208
  71. package/dist/components/file-dropzone.d.ts +49 -2
  72. package/dist/components/floating-panel.d.ts +13 -4
  73. package/dist/components/floating-panel.js +7 -3
  74. package/dist/components/floating-panel.js.map +1 -1
  75. package/dist/components/form-actions.d.ts +53 -0
  76. package/dist/components/form-actions.js +98 -0
  77. package/dist/components/form-actions.js.map +1 -0
  78. package/dist/components/image-grid.d.ts +67 -0
  79. package/dist/components/image-grid.js +112 -0
  80. package/dist/components/image-grid.js.map +1 -0
  81. package/dist/components/lightbox.d.ts +104 -0
  82. package/dist/components/lightbox.js +229 -0
  83. package/dist/components/lightbox.js.map +1 -0
  84. package/dist/components/line-items.d.ts +114 -0
  85. package/dist/components/line-items.js +238 -0
  86. package/dist/components/line-items.js.map +1 -0
  87. package/dist/components/list.d.ts +23 -2
  88. package/dist/components/list.js +32 -13
  89. package/dist/components/list.js.map +1 -1
  90. package/dist/components/loading-state.d.ts +24 -0
  91. package/dist/components/loading-state.js +32 -0
  92. package/dist/components/loading-state.js.map +1 -0
  93. package/dist/components/menu-item.d.ts +2 -1
  94. package/dist/components/menu-item.js +5 -1
  95. package/dist/components/menu-item.js.map +1 -1
  96. package/dist/components/mini-calendar.d.ts +13 -1
  97. package/dist/components/mini-calendar.js +46 -2
  98. package/dist/components/mini-calendar.js.map +1 -1
  99. package/dist/components/modal.d.ts +20 -1
  100. package/dist/components/modal.js +26 -2
  101. package/dist/components/modal.js.map +1 -1
  102. package/dist/components/month-picker.d.ts +21 -1
  103. package/dist/components/month-picker.js +139 -26
  104. package/dist/components/month-picker.js.map +1 -1
  105. package/dist/components/multi-entity-combobox.d.ts +1 -1
  106. package/dist/components/multi-entity-combobox.js +19 -3
  107. package/dist/components/multi-entity-combobox.js.map +1 -1
  108. package/dist/components/nav-pills.d.ts +2 -1
  109. package/dist/components/nav-pills.js +5 -1
  110. package/dist/components/nav-pills.js.map +1 -1
  111. package/dist/components/number-field.d.ts +24 -5
  112. package/dist/components/number-field.js +5 -7
  113. package/dist/components/number-field.js.map +1 -1
  114. package/dist/components/number-input.d.ts +25 -5
  115. package/dist/components/number-input.js +7 -1
  116. package/dist/components/number-input.js.map +1 -1
  117. package/dist/components/numpad-sheet.d.ts +15 -4
  118. package/dist/components/passkeys-setting.d.ts +67 -0
  119. package/dist/components/passkeys-setting.js +214 -0
  120. package/dist/components/passkeys-setting.js.map +1 -0
  121. package/dist/components/picker-sheet.js +2 -1
  122. package/dist/components/picker-sheet.js.map +1 -1
  123. package/dist/components/pie-chart-labels.d.ts +20 -0
  124. package/dist/components/pie-chart-labels.js +12 -0
  125. package/dist/components/pie-chart-labels.js.map +1 -0
  126. package/dist/components/pie-chart.d.ts +101 -0
  127. package/dist/components/pie-chart.js +283 -0
  128. package/dist/components/pie-chart.js.map +1 -0
  129. package/dist/components/progress-bar.d.ts +44 -3
  130. package/dist/components/progress-bar.js +58 -22
  131. package/dist/components/progress-bar.js.map +1 -1
  132. package/dist/components/qr-code.d.ts +34 -0
  133. package/dist/components/qr-code.js +57 -0
  134. package/dist/components/qr-code.js.map +1 -0
  135. package/dist/components/search-field.d.ts +15 -3
  136. package/dist/components/search-field.js +5 -1
  137. package/dist/components/search-field.js.map +1 -1
  138. package/dist/components/settings-fields.d.ts +49 -1
  139. package/dist/components/signed-amount.d.ts +121 -0
  140. package/dist/components/signed-amount.js +152 -0
  141. package/dist/components/signed-amount.js.map +1 -0
  142. package/dist/components/skeleton.d.ts +10 -3
  143. package/dist/components/skeleton.js +12 -2
  144. package/dist/components/skeleton.js.map +1 -1
  145. package/dist/components/stat-tile.d.ts +2 -1
  146. package/dist/components/stat-tile.js +5 -2
  147. package/dist/components/stat-tile.js.map +1 -1
  148. package/dist/components/table.d.ts +39 -5
  149. package/dist/components/table.js +45 -11
  150. package/dist/components/table.js.map +1 -1
  151. package/dist/components/text-link.d.ts +176 -0
  152. package/dist/components/text-link.js +109 -0
  153. package/dist/components/text-link.js.map +1 -0
  154. package/dist/components/time-input.d.ts +49 -1
  155. package/dist/components/toggle-group.d.ts +13 -0
  156. package/dist/components/toggle-group.js +35 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +36 -3
  159. package/dist/components/toggle-legend.js +49 -0
  160. package/dist/components/toggle-legend.js.map +1 -1
  161. package/dist/components/ui.d.ts +51 -779
  162. package/dist/components/ui.js +215 -23
  163. package/dist/components/ui.js.map +1 -1
  164. package/dist/components/use-table-state.d.ts +3 -52
  165. package/dist/components/use-table-state.js +128 -30
  166. package/dist/components/use-table-state.js.map +1 -1
  167. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  168. package/dist/data-table.d.ts +2 -1
  169. package/dist/data-table.js +5 -1
  170. package/dist/data-table.js.map +1 -1
  171. package/dist/feedback/feedback-dialog.d.ts +26 -3
  172. package/dist/feedback/feedback-dialog.js +16 -2
  173. package/dist/feedback/feedback-dialog.js.map +1 -1
  174. package/dist/feedback/feedback-thread.d.ts +133 -0
  175. package/dist/feedback/feedback-thread.js +237 -0
  176. package/dist/feedback/feedback-thread.js.map +1 -0
  177. package/dist/feedback.d.ts +2 -1
  178. package/dist/feedback.js +1 -0
  179. package/dist/feedback.js.map +1 -1
  180. package/dist/hooks/use-authed-src.d.ts +71 -0
  181. package/dist/hooks/use-authed-src.js +63 -0
  182. package/dist/hooks/use-authed-src.js.map +1 -0
  183. package/dist/hooks/use-file-drop.d.ts +49 -2
  184. package/dist/hooks/use-hotkey.d.ts +59 -0
  185. package/dist/hooks/use-hotkey.js +105 -0
  186. package/dist/hooks/use-hotkey.js.map +1 -0
  187. package/dist/hooks/use-search-param-state.d.ts +83 -0
  188. package/dist/hooks/use-search-param-state.js +94 -0
  189. package/dist/hooks/use-search-param-state.js.map +1 -0
  190. package/dist/i18n/defaults.d.ts +15 -4
  191. package/dist/i18n/defaults.js +31 -1
  192. package/dist/i18n/defaults.js.map +1 -1
  193. package/dist/i18n/kit-labels.d.ts +46 -312
  194. package/dist/i18n/kit-labels.js +20 -5
  195. package/dist/i18n/kit-labels.js.map +1 -1
  196. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  197. package/dist/i18n/locales/de-CH.d.ts +15 -4
  198. package/dist/i18n/locales/de-informal.d.ts +15 -4
  199. package/dist/i18n/locales/de.d.ts +15 -4
  200. package/dist/i18n/locales/de.js +156 -3
  201. package/dist/i18n/locales/de.js.map +1 -1
  202. package/dist/i18n/locales/es.d.ts +15 -4
  203. package/dist/i18n/locales/es.js +153 -3
  204. package/dist/i18n/locales/es.js.map +1 -1
  205. package/dist/i18n/locales/fr.d.ts +15 -4
  206. package/dist/i18n/locales/fr.js +153 -3
  207. package/dist/i18n/locales/fr.js.map +1 -1
  208. package/dist/i18n/locales/hu.d.ts +15 -4
  209. package/dist/i18n/locales/hu.js +153 -3
  210. package/dist/i18n/locales/hu.js.map +1 -1
  211. package/dist/i18n/locales/it.d.ts +15 -4
  212. package/dist/i18n/locales/it.js +153 -3
  213. package/dist/i18n/locales/it.js.map +1 -1
  214. package/dist/i18n/locales/zh.d.ts +15 -4
  215. package/dist/i18n/locales/zh.js +153 -3
  216. package/dist/i18n/locales/zh.js.map +1 -1
  217. package/dist/index.d.ts +37 -17
  218. package/dist/index.js +40 -1
  219. package/dist/index.js.map +1 -1
  220. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  221. package/dist/lib/format.d.ts +144 -0
  222. package/dist/lib/format.js +149 -0
  223. package/dist/lib/format.js.map +1 -0
  224. package/dist/lib/qr-encode.d.ts +44 -0
  225. package/dist/lib/qr-encode.js +338 -0
  226. package/dist/lib/qr-encode.js.map +1 -0
  227. package/dist/rhf/fields.d.ts +241 -0
  228. package/dist/rhf/fields.js +601 -0
  229. package/dist/rhf/fields.js.map +1 -0
  230. package/dist/rhf/form.d.ts +49 -1
  231. package/dist/rhf/line-items.d.ts +31 -0
  232. package/dist/rhf/line-items.js +59 -0
  233. package/dist/rhf/line-items.js.map +1 -0
  234. package/dist/rhf.d.ts +61 -1
  235. package/dist/rhf.js +2 -0
  236. package/dist/rhf.js.map +1 -1
  237. package/dist/shell/app-shell.d.ts +99 -2
  238. package/dist/shell/app-shell.js +58 -3
  239. package/dist/shell/app-shell.js.map +1 -1
  240. package/dist/shell/auth-layout.d.ts +74 -0
  241. package/dist/shell/auth-layout.js +97 -0
  242. package/dist/shell/auth-layout.js.map +1 -0
  243. package/dist/shell/top-bar-brand.d.ts +81 -0
  244. package/dist/shell/top-bar-brand.js +47 -0
  245. package/dist/shell/top-bar-brand.js.map +1 -0
  246. package/dist/shell/topbar-action-menu.d.ts +12 -1
  247. package/dist/shell/topbar-action-menu.js +16 -1
  248. package/dist/shell/topbar-action-menu.js.map +1 -1
  249. package/dist/shell.d.ts +56 -1
  250. package/dist/shell.js +2 -0
  251. package/dist/shell.js.map +1 -1
  252. package/dist/wizard/stepper-nav.d.ts +48 -1
  253. package/dist/wizard/wizard-context.d.ts +24 -1
  254. package/dist/wizard/wizard-context.js +19 -2
  255. package/dist/wizard/wizard-context.js.map +1 -1
  256. package/dist/wizard/wizard-step.d.ts +20 -5
  257. package/dist/wizard/wizard-step.js +16 -3
  258. package/dist/wizard/wizard-step.js.map +1 -1
  259. package/dist/wizard.d.ts +50 -3
  260. package/package.json +1 -1
  261. package/src/chart.ts +4 -0
  262. package/src/components/account-settings-labels.ts +190 -0
  263. package/src/components/account-settings.tsx +110 -63
  264. package/src/components/amount-input.tsx +13 -1
  265. package/src/components/authed-image.tsx +190 -0
  266. package/src/components/breadcrumbs.tsx +8 -2
  267. package/src/components/button-group.tsx +7 -2
  268. package/src/components/calendar-heatmap.tsx +105 -10
  269. package/src/components/chip.tsx +8 -4
  270. package/src/components/choice-card.tsx +136 -25
  271. package/src/components/combobox-core.tsx +5 -0
  272. package/src/components/combobox.tsx +31 -9
  273. package/src/components/copy-button.tsx +3 -2
  274. package/src/components/data-table-cells.tsx +119 -0
  275. package/src/components/data-table-filters.ts +77 -0
  276. package/src/components/data-table-labels.ts +23 -0
  277. package/src/components/data-table.tsx +224 -13
  278. package/src/components/date-picker.tsx +19 -3
  279. package/src/components/description-list.tsx +52 -3
  280. package/src/components/entity-combobox.tsx +18 -3
  281. package/src/components/error-boundary.tsx +216 -0
  282. package/src/components/field.tsx +56 -5
  283. package/src/components/floating-panel.tsx +20 -7
  284. package/src/components/form-actions.tsx +191 -0
  285. package/src/components/image-grid.tsx +181 -0
  286. package/src/components/lightbox.tsx +374 -0
  287. package/src/components/line-items.tsx +385 -0
  288. package/src/components/list.tsx +65 -16
  289. package/src/components/loading-state.tsx +47 -0
  290. package/src/components/menu-item.tsx +7 -2
  291. package/src/components/mini-calendar.tsx +72 -3
  292. package/src/components/modal.tsx +61 -3
  293. package/src/components/month-picker.tsx +181 -26
  294. package/src/components/multi-entity-combobox.tsx +19 -3
  295. package/src/components/nav-pills.tsx +8 -1
  296. package/src/components/number-field.tsx +15 -10
  297. package/src/components/number-input.tsx +16 -1
  298. package/src/components/passkeys-setting.tsx +331 -0
  299. package/src/components/picker-sheet.tsx +7 -1
  300. package/src/components/pie-chart-labels.ts +32 -0
  301. package/src/components/pie-chart.tsx +479 -0
  302. package/src/components/progress-bar.tsx +129 -19
  303. package/src/components/qr-code.tsx +83 -0
  304. package/src/components/search-field.tsx +20 -4
  305. package/src/components/signed-amount.tsx +286 -0
  306. package/src/components/skeleton.tsx +21 -3
  307. package/src/components/stat-tile.tsx +8 -2
  308. package/src/components/table.tsx +102 -13
  309. package/src/components/text-link.tsx +248 -0
  310. package/src/components/toggle-group.tsx +58 -10
  311. package/src/components/toggle-legend.tsx +87 -2
  312. package/src/components/ui.tsx +426 -36
  313. package/src/components/use-table-state.ts +265 -33
  314. package/src/data-table.ts +10 -0
  315. package/src/feedback/feedback-dialog.tsx +42 -3
  316. package/src/feedback/feedback-thread.tsx +434 -0
  317. package/src/feedback.ts +1 -0
  318. package/src/hooks/use-authed-src.ts +161 -0
  319. package/src/hooks/use-hotkey.ts +172 -0
  320. package/src/hooks/use-search-param-state.ts +187 -0
  321. package/src/i18n/defaults.ts +30 -0
  322. package/src/i18n/kit-labels.tsx +102 -8
  323. package/src/i18n/locales/de.ts +153 -0
  324. package/src/i18n/locales/es.ts +150 -0
  325. package/src/i18n/locales/fr.ts +153 -0
  326. package/src/i18n/locales/hu.ts +151 -0
  327. package/src/i18n/locales/it.ts +151 -0
  328. package/src/i18n/locales/zh.ts +150 -0
  329. package/src/index.ts +59 -0
  330. package/src/lib/format.ts +325 -0
  331. package/src/lib/qr-encode.ts +420 -0
  332. package/src/rhf/fields.tsx +1054 -0
  333. package/src/rhf/line-items.tsx +130 -0
  334. package/src/rhf.ts +6 -0
  335. package/src/shell/app-shell.tsx +147 -2
  336. package/src/shell/auth-layout.tsx +190 -0
  337. package/src/shell/top-bar-brand.tsx +76 -0
  338. package/src/shell/topbar-action-menu.tsx +21 -0
  339. package/src/shell.ts +2 -0
  340. package/src/wizard/wizard-context.tsx +41 -1
  341. package/src/wizard/wizard-step.tsx +43 -6
  342. package/tokens.css +12 -0
@@ -0,0 +1,229 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { ChevronLeft, ChevronRight, Download, ExternalLink, FileText, ZoomIn } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { horizontalStep } from "../lib/direction.js";
7
+ import { useKitLabels } from "../i18n/kit-labels.js";
8
+ import { useAuthedSrc } from "../hooks/use-authed-src.js";
9
+ import { FullBleedDialog } from "./full-bleed-dialog.js";
10
+ import { IconButton, buttonClasses } from "./ui.js";
11
+ import { ImageStates, DEFAULT_AUTHED_IMAGE_LABELS } from "./authed-image.js";
12
+ import { useAnchorDir } from "./use-anchor-dir.js";
13
+ const FILE_EXTENSION = /\.(pdf|docx?|xlsx?|pptx?|odt|ods|zip|txt|csv|rtf)(?:[?#]|$)/i;
14
+ function imageItemKind(item, fetchedType) {
15
+ if (item.kind) return item.kind;
16
+ if (item.mimeType) return item.mimeType.startsWith("image/") ? "image" : "file";
17
+ if (FILE_EXTENSION.test(item.src)) return "file";
18
+ if (fetchedType && !fetchedType.startsWith("image/")) return "file";
19
+ return "image";
20
+ }
21
+ const DEFAULT_LIGHTBOX_LABELS = {
22
+ dialog: "Image viewer",
23
+ close: "Close",
24
+ previous: "Previous image",
25
+ next: "Next image",
26
+ counter: (index, count) => `${index} / ${count}`,
27
+ position: (index, count) => `Image ${index} of ${count}`,
28
+ download: "Download",
29
+ zoom: "Zoom",
30
+ noPreview: "No preview available for this file",
31
+ openInNewTab: "Open in new tab"
32
+ };
33
+ const SWIPE_MIN_PX = 50;
34
+ function Lightbox({
35
+ open,
36
+ onClose,
37
+ items,
38
+ index: indexProp,
39
+ onIndexChange,
40
+ fetcher,
41
+ loop = false,
42
+ zoom: zoomEnabled = true,
43
+ download = true,
44
+ onDownload,
45
+ backCloses,
46
+ labels: labelsProp
47
+ }) {
48
+ const labels = useKitLabels("lightbox", DEFAULT_LIGHTBOX_LABELS, labelsProp);
49
+ const imageLabels = useKitLabels("authedImage", DEFAULT_AUTHED_IMAGE_LABELS);
50
+ const count = items.length;
51
+ const index = count === 0 ? 0 : Math.min(Math.max(indexProp, 0), count - 1);
52
+ const item = items[index];
53
+ const anchorRef = useRef(null);
54
+ const dir = useAnchorDir(anchorRef, open);
55
+ const fetched = useAuthedSrc(item?.src, { fetcher, enabled: open });
56
+ const kind = item ? imageItemKind(item, fetched.type) : "image";
57
+ const [zoomedKey, setZoomedKey] = useState(null);
58
+ const itemKey = item ? String(item.key ?? item.src) : null;
59
+ const zoomed = zoomEnabled && kind === "image" && zoomedKey !== null && zoomedKey === itemKey;
60
+ const toggleZoom = () => setZoomedKey(zoomed ? null : itemKey);
61
+ const canPrev = loop ? count > 1 : index > 0;
62
+ const canNext = loop ? count > 1 : index < count - 1;
63
+ const go = (step) => {
64
+ if (count === 0) return;
65
+ let next = index + step;
66
+ if (loop) next = (next + count) % count;
67
+ if (next < 0 || next >= count || next === index) return;
68
+ onIndexChange(next);
69
+ };
70
+ const onKeyDown = (e) => {
71
+ const target = e.target;
72
+ if (target.closest("input, textarea, select, [contenteditable='true']")) return;
73
+ const step = horizontalStep(e.key, e.currentTarget);
74
+ if (step !== 0) {
75
+ e.preventDefault();
76
+ go(step);
77
+ } else if (e.key === "Home") {
78
+ e.preventDefault();
79
+ if (index !== 0) onIndexChange(0);
80
+ } else if (e.key === "End") {
81
+ e.preventDefault();
82
+ if (index !== count - 1) onIndexChange(count - 1);
83
+ }
84
+ };
85
+ const touchStart = useRef(null);
86
+ const onTouchStart = (e) => {
87
+ const t = e.touches[0];
88
+ touchStart.current = e.touches.length === 1 && t ? { x: t.clientX, y: t.clientY } : null;
89
+ };
90
+ const onTouchEnd = (e) => {
91
+ const start = touchStart.current;
92
+ touchStart.current = null;
93
+ const t = e.changedTouches[0];
94
+ if (!start || !t || zoomed) return;
95
+ const dx = t.clientX - start.x;
96
+ const dy = t.clientY - start.y;
97
+ if (Math.abs(dx) < SWIPE_MIN_PX || Math.abs(dx) < Math.abs(dy)) return;
98
+ const towardStart = dir === "rtl" ? dx > 0 : dx < 0;
99
+ go(towardStart ? 1 : -1);
100
+ };
101
+ const bodyRef = useRef(null);
102
+ useEffect(() => {
103
+ bodyRef.current?.scrollTo?.({ top: 0, left: 0 });
104
+ }, [index]);
105
+ const downloadHref = fetched.src;
106
+ const downloadControl = download && item ? onDownload ? /* @__PURE__ */ jsx(IconButton, { size: "sm", label: labels.download, onClick: () => onDownload(item, index), children: /* @__PURE__ */ jsx(Download, {}) }) : downloadHref ? /* @__PURE__ */ jsxs(
107
+ "a",
108
+ {
109
+ href: downloadHref,
110
+ download: item.fileName ?? "",
111
+ className: buttonClasses("ghost", { size: "sm" }),
112
+ children: [
113
+ /* @__PURE__ */ jsx(Download, { "aria-hidden": true, className: "size-4" }),
114
+ /* @__PURE__ */ jsx("span", { className: "max-sm:sr-only", children: labels.download })
115
+ ]
116
+ }
117
+ ) : null : null;
118
+ const header = item ? /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
119
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "shrink-0 text-sm tabular-nums text-[var(--text-secondary)]", children: labels.counter(index + 1, count) }),
120
+ /* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: `${labels.position(index + 1, count)}${item.alt ? `: ${item.alt}` : ""}` }),
121
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
122
+ zoomEnabled && kind === "image" && /* @__PURE__ */ jsx(IconButton, { size: "sm", label: labels.zoom, pressed: zoomed, onClick: toggleZoom, children: /* @__PURE__ */ jsx(ZoomIn, {}) }),
123
+ downloadControl
124
+ ] }) : null;
125
+ const slide = !item ? null : kind === "file" ? /* @__PURE__ */ jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 rounded-lg border border-[var(--border)] p-6 text-center", children: [
126
+ /* @__PURE__ */ jsx(FileText, { "aria-hidden": true, className: "size-10 text-[var(--text-muted)]" }),
127
+ /* @__PURE__ */ jsx("p", { className: "break-all font-medium text-[var(--text-primary)]", children: item.fileName ?? item.alt }),
128
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--text-muted)]", children: labels.noPreview }),
129
+ fetched.src && /* @__PURE__ */ jsxs(
130
+ "a",
131
+ {
132
+ href: fetched.src,
133
+ target: "_blank",
134
+ rel: "noreferrer noopener",
135
+ className: buttonClasses("secondary", { size: "sm" }),
136
+ children: [
137
+ /* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": true, className: "size-4" }),
138
+ labels.openInNewTab
139
+ ]
140
+ }
141
+ )
142
+ ] }) : /* @__PURE__ */ jsx(
143
+ ImageStates,
144
+ {
145
+ fetched,
146
+ alt: item.alt,
147
+ labels: imageLabels,
148
+ wrapperClassName: zoomed ? "max-w-none shrink-0" : "flex h-full w-full items-center justify-center",
149
+ className: cn(
150
+ zoomed ? "max-w-none cursor-zoom-out" : "max-h-full max-w-full object-contain",
151
+ !zoomed && zoomEnabled && "cursor-zoom-in"
152
+ ),
153
+ imgProps: {
154
+ draggable: false,
155
+ onDoubleClick: zoomEnabled ? toggleZoom : void 0
156
+ }
157
+ },
158
+ itemKey ?? void 0
159
+ );
160
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
161
+ /* @__PURE__ */ jsx("span", { ref: anchorRef, hidden: true }),
162
+ /* @__PURE__ */ jsxs(
163
+ FullBleedDialog,
164
+ {
165
+ open: open && !!item,
166
+ onClose,
167
+ closeLabel: labels.close,
168
+ "aria-label": labels.dialog,
169
+ dir,
170
+ header,
171
+ backCloses,
172
+ onKeyDown,
173
+ "data-lightbox": "",
174
+ footer: item?.caption !== void 0 && item.caption !== null && item.caption !== false ? /* @__PURE__ */ jsx("p", { className: "me-auto min-w-0 text-sm text-[var(--text-secondary)]", children: item.caption }) : void 0,
175
+ children: [
176
+ /* @__PURE__ */ jsx(
177
+ "div",
178
+ {
179
+ ref: bodyRef,
180
+ "data-lightbox-stage": "",
181
+ "data-zoomed": zoomed ? "" : void 0,
182
+ onTouchStart,
183
+ onTouchEnd,
184
+ className: cn(
185
+ "relative h-full min-h-48 w-full",
186
+ zoomed ? "overflow-auto" : "flex items-center justify-center overflow-hidden"
187
+ ),
188
+ children: slide
189
+ }
190
+ ),
191
+ count > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
192
+ /* @__PURE__ */ jsx(
193
+ IconButton,
194
+ {
195
+ variant: "overlay",
196
+ size: "lg",
197
+ label: labels.previous,
198
+ disabled: !canPrev,
199
+ onClick: () => go(-1),
200
+ className: "absolute start-3 top-1/2 -translate-y-1/2",
201
+ tooltip: false,
202
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "rtl:-scale-x-100" })
203
+ }
204
+ ),
205
+ /* @__PURE__ */ jsx(
206
+ IconButton,
207
+ {
208
+ variant: "overlay",
209
+ size: "lg",
210
+ label: labels.next,
211
+ disabled: !canNext,
212
+ onClick: () => go(1),
213
+ className: "absolute end-3 top-1/2 -translate-y-1/2",
214
+ tooltip: false,
215
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "rtl:-scale-x-100" })
216
+ }
217
+ )
218
+ ] })
219
+ ]
220
+ }
221
+ )
222
+ ] });
223
+ }
224
+ export {
225
+ DEFAULT_LIGHTBOX_LABELS,
226
+ Lightbox,
227
+ imageItemKind
228
+ };
229
+ //# sourceMappingURL=lightbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/lightbox.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode, TouchEvent } from \"react\";\nimport { ChevronLeft, ChevronRight, Download, ExternalLink, FileText, ZoomIn } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useAuthedSrc } from \"../hooks/use-authed-src\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { IconButton, buttonClasses } from \"./ui\";\nimport { ImageStates, DEFAULT_AUTHED_IMAGE_LABELS } from \"./authed-image\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\n\n/* ── The item both the grid and the lightbox show ────────────────────────── */\n\n/** `image`: shown as a picture. `file`: anything a browser cannot put in an `<img>` —\n * a floor plan uploaded as a PDF — shown as a file card with open and download. */\nexport type ImageItemKind = \"image\" | \"file\";\n\nexport interface ImageItem {\n /** React key. Defaults to `src`. */\n key?: string | number;\n /** The full-size file — what the lightbox shows and downloads. An API path when the\n * component is given a `fetcher`. */\n src: string;\n /** A smaller rendition for the grid, when the server has one. Default: `src`. */\n thumbnailSrc?: string;\n /** Required: the picture's text alternative, and the open button's name. */\n alt: string;\n /** Shown under the thumbnail and at the foot of the lightbox. */\n caption?: ReactNode;\n /** The file's MIME type, when known (`\"application/pdf\"`). Decides {@link kind}. */\n mimeType?: string;\n /** Say it outright. Default: from {@link mimeType}, else from the file extension in\n * `src`, else — for a fetched file — from the type the server sent back. */\n kind?: ImageItemKind;\n /** The name a download is saved under, and a file card's heading. */\n fileName?: string;\n}\n\nconst FILE_EXTENSION = /\\.(pdf|docx?|xlsx?|pptx?|odt|ods|zip|txt|csv|rtf)(?:[?#]|$)/i;\n\n/** What an item is, from what is known about it WITHOUT fetching it — see\n * {@link ImageItem.kind}. `fetchedType` is the blob's type once it is in. */\nexport function imageItemKind(item: ImageItem, fetchedType?: string): ImageItemKind {\n if (item.kind) return item.kind;\n if (item.mimeType) return item.mimeType.startsWith(\"image/\") ? \"image\" : \"file\";\n if (FILE_EXTENSION.test(item.src)) return \"file\";\n if (fetchedType && !fetchedType.startsWith(\"image/\")) return \"file\";\n return \"image\";\n}\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\nexport interface LightboxLabels {\n /** The dialog's accessible name. */\n dialog: string;\n close: string;\n previous: string;\n next: string;\n /** The visible counter: \"3 / 7\". */\n counter: (index: number, count: number) => string;\n /** What a screen reader hears on each page turn: \"Image 3 of 7\". The alt follows. */\n position: (index: number, count: number) => string;\n download: string;\n /** The zoom toggle (a pressed/not-pressed button — its name does not change). */\n zoom: string;\n /** A file card's note: the file has no picture to show. */\n noPreview: string;\n /** A file card's link to the browser's own viewer (a PDF opens in its viewer). */\n openInNewTab: string;\n}\n\nexport const DEFAULT_LIGHTBOX_LABELS: LightboxLabels = {\n dialog: \"Image viewer\",\n close: \"Close\",\n previous: \"Previous image\",\n next: \"Next image\",\n counter: (index, count) => `${index} / ${count}`,\n position: (index, count) => `Image ${index} of ${count}`,\n download: \"Download\",\n zoom: \"Zoom\",\n noPreview: \"No preview available for this file\",\n openInNewTab: \"Open in new tab\",\n};\n\n/* ── The component ───────────────────────────────────────────────────────── */\n\nexport interface LightboxProps {\n open: boolean;\n /** The X, Escape and the Back gesture all call this. */\n onClose: () => void;\n items: readonly ImageItem[];\n /** The item on screen (0-based). Controlled, so the grid — or a URL — can own it. */\n index: number;\n onIndexChange: (index: number) => void;\n /** The app's authenticated GET (see {@link useAuthedSrc}); left out, `src` is\n * loaded by the browser as usual. */\n fetcher?: AuthedFetcher | null;\n /** Previous from the first goes to the last, and next from the last to the first.\n * Default `false`: the buttons at the ends are disabled, which says where the set\n * ends. */\n loop?: boolean;\n /** Double-click (or double-tap) the picture, or the zoom button, to see it at full\n * size and pan by scrolling. Default `true`. Pinch zoom is the browser's own. */\n zoom?: boolean;\n /** A download link in the header. Default `true`. */\n download?: boolean;\n /**\n * Download through the app instead — kastlan's floor plans have `openDocument(id)`,\n * which knows the stored file name. Replaces the link with a button that calls this.\n */\n onDownload?: (item: ImageItem, index: number) => void;\n /** Push a history entry so Back closes the viewer. Default `true`; see\n * `FullBleedDialog`'s `backCloses` for when to turn it off. */\n backCloses?: boolean;\n labels?: Partial<LightboxLabels>;\n}\n\n/** Swipes shorter than this are taps, and a drag that is mostly vertical is a scroll. */\nconst SWIPE_MIN_PX = 50;\n\n/**\n * A full-screen viewer for a set of images (and files that are not images).\n *\n * Built on {@link FullBleedDialog} rather than beside it, so the viewer has the kit's\n * one implementation of everything an overlay owes the page: the focus trap, Escape,\n * the Back gesture closing it instead of leaving the page (`useOverlayHistory`), the\n * body-scroll lock and the enter/exit motion.\n *\n * Paging: the previous/next buttons, ←/→ (which swap in a right-to-left page — the\n * direction is read at the point the viewer is rendered, since the dialog itself is\n * portalled out of it), Home/End, and a horizontal swipe on a touch screen.\n *\n * Zoom is a TOGGLE, not a gesture: double-click (or the zoom button) shows the picture\n * at its natural size and the body scrolls to pan. Pinch-to-zoom is not implemented —\n * the browser's own page pinch still works — because a pinch handler would have to own\n * every touch on the picture, and with it the swipe.\n *\n * A non-image (a PDF floor plan) is shown as a file card: its name, \"no preview\", a\n * link that opens it in the browser's own viewer, and the download. An `<iframe>`\n * preview was the alternative; phone browsers render a PDF in one inconsistently or\n * not at all, and a card works everywhere.\n */\nexport function Lightbox({\n open,\n onClose,\n items,\n index: indexProp,\n onIndexChange,\n fetcher,\n loop = false,\n zoom: zoomEnabled = true,\n download = true,\n onDownload,\n backCloses,\n labels: labelsProp,\n}: LightboxProps) {\n const labels = useKitLabels(\"lightbox\", DEFAULT_LIGHTBOX_LABELS, labelsProp);\n const imageLabels = useKitLabels(\"authedImage\", DEFAULT_AUTHED_IMAGE_LABELS);\n const count = items.length;\n // Clamped: the set can shrink under an open viewer (the photo on screen was deleted).\n const index = count === 0 ? 0 : Math.min(Math.max(indexProp, 0), count - 1);\n const item = items[index] as ImageItem | undefined;\n\n // The dialog is portalled to <body>, out of the subtree whose `dir` it would have\n // inherited; this marker stays where the viewer was rendered and reads it there.\n const anchorRef = useRef<HTMLSpanElement>(null);\n const dir = useAnchorDir(anchorRef, open);\n\n const fetched = useAuthedSrc(item?.src, { fetcher, enabled: open });\n const kind = item ? imageItemKind(item, fetched.type) : \"image\";\n\n const [zoomedKey, setZoomedKey] = useState<string | null>(null);\n const itemKey = item ? String(item.key ?? item.src) : null;\n // Zoom belongs to the picture it was turned on for: paging on resets it.\n const zoomed = zoomEnabled && kind === \"image\" && zoomedKey !== null && zoomedKey === itemKey;\n const toggleZoom = () => setZoomedKey(zoomed ? null : itemKey);\n\n const canPrev = loop ? count > 1 : index > 0;\n const canNext = loop ? count > 1 : index < count - 1;\n const go = (step: number) => {\n if (count === 0) return;\n let next = index + step;\n if (loop) next = (next + count) % count;\n if (next < 0 || next >= count || next === index) return;\n onIndexChange(next);\n };\n\n const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A key meant for a control stays with it: Space/Enter on a button, typing in any\n // field a caller's caption might hold.\n const target = e.target as HTMLElement;\n if (target.closest(\"input, textarea, select, [contenteditable='true']\")) return;\n const step = horizontalStep(e.key, e.currentTarget);\n if (step !== 0) {\n e.preventDefault();\n go(step);\n } else if (e.key === \"Home\") {\n e.preventDefault();\n if (index !== 0) onIndexChange(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n if (index !== count - 1) onIndexChange(count - 1);\n }\n };\n\n const touchStart = useRef<{ x: number; y: number } | null>(null);\n const onTouchStart = (e: TouchEvent) => {\n const t = e.touches[0];\n touchStart.current = e.touches.length === 1 && t ? { x: t.clientX, y: t.clientY } : null;\n };\n const onTouchEnd = (e: TouchEvent) => {\n const start = touchStart.current;\n touchStart.current = null;\n const t = e.changedTouches[0];\n // Zoomed, a drag is a PAN of the picture, not a page turn.\n if (!start || !t || zoomed) return;\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n if (Math.abs(dx) < SWIPE_MIN_PX || Math.abs(dx) < Math.abs(dy)) return;\n // Swiping toward the START edge brings in the next picture from the end edge.\n const towardStart = dir === \"rtl\" ? dx > 0 : dx < 0;\n go(towardStart ? 1 : -1);\n };\n\n // Paging the viewer while zoomed would leave the new picture scrolled to where the\n // last one was panned; start each at its top-left.\n const bodyRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n bodyRef.current?.scrollTo?.({ top: 0, left: 0 });\n }, [index]);\n\n const downloadHref = fetched.src;\n const downloadControl =\n download && item ? (\n onDownload ? (\n <IconButton size=\"sm\" label={labels.download} onClick={() => onDownload(item, index)}>\n <Download />\n </IconButton>\n ) : downloadHref ? (\n <a\n href={downloadHref}\n // `download` names the saved file; a blob: URL has no name of its own.\n download={item.fileName ?? \"\"}\n className={buttonClasses(\"ghost\", { size: \"sm\" })}\n >\n <Download aria-hidden className=\"size-4\" />\n <span className=\"max-sm:sr-only\">{labels.download}</span>\n </a>\n ) : null\n ) : null;\n\n const header = item ? (\n <div className=\"flex min-w-0 items-center gap-2\">\n <span aria-hidden className=\"shrink-0 text-sm tabular-nums text-[var(--text-secondary)]\">\n {labels.counter(index + 1, count)}\n </span>\n {/* The page turn, said — focus does not move when the picture changes, so\n without this a reader pressing → hears nothing at all. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {`${labels.position(index + 1, count)}${item.alt ? `: ${item.alt}` : \"\"}`}\n </span>\n <span className=\"min-w-0 flex-1\" />\n {zoomEnabled && kind === \"image\" && (\n <IconButton size=\"sm\" label={labels.zoom} pressed={zoomed} onClick={toggleZoom}>\n <ZoomIn />\n </IconButton>\n )}\n {downloadControl}\n </div>\n ) : null;\n\n const slide = !item ? null : kind === \"file\" ? (\n <div className=\"flex max-w-sm flex-col items-center gap-3 rounded-lg border border-[var(--border)] p-6 text-center\">\n <FileText aria-hidden className=\"size-10 text-[var(--text-muted)]\" />\n <p className=\"break-all font-medium text-[var(--text-primary)]\">{item.fileName ?? item.alt}</p>\n <p className=\"text-sm text-[var(--text-muted)]\">{labels.noPreview}</p>\n {fetched.src && (\n <a\n href={fetched.src}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className={buttonClasses(\"secondary\", { size: \"sm\" })}\n >\n <ExternalLink aria-hidden className=\"size-4\" />\n {labels.openInNewTab}\n </a>\n )}\n </div>\n ) : (\n <ImageStates\n // Keyed by item: a new picture is a new decode, never the old one's \"loaded\".\n key={itemKey ?? undefined}\n fetched={fetched}\n alt={item.alt}\n labels={imageLabels}\n wrapperClassName={\n zoomed ? \"max-w-none shrink-0\" : \"flex h-full w-full items-center justify-center\"\n }\n className={cn(\n zoomed ? \"max-w-none cursor-zoom-out\" : \"max-h-full max-w-full object-contain\",\n !zoomed && zoomEnabled && \"cursor-zoom-in\",\n )}\n imgProps={{\n draggable: false,\n onDoubleClick: zoomEnabled ? toggleZoom : undefined,\n }}\n />\n );\n\n return (\n <>\n <span ref={anchorRef} hidden />\n <FullBleedDialog\n open={open && !!item}\n onClose={onClose}\n closeLabel={labels.close}\n aria-label={labels.dialog}\n dir={dir}\n header={header}\n backCloses={backCloses}\n onKeyDown={onKeyDown}\n data-lightbox=\"\"\n footer={\n item?.caption !== undefined && item.caption !== null && item.caption !== false ? (\n <p className=\"me-auto min-w-0 text-sm text-[var(--text-secondary)]\">{item.caption}</p>\n ) : undefined\n }\n >\n <div\n ref={bodyRef}\n data-lightbox-stage=\"\"\n data-zoomed={zoomed ? \"\" : undefined}\n onTouchStart={onTouchStart}\n onTouchEnd={onTouchEnd}\n className={cn(\n \"relative h-full min-h-48 w-full\",\n zoomed ? \"overflow-auto\" : \"flex items-center justify-center overflow-hidden\",\n )}\n >\n {slide}\n </div>\n {count > 1 && (\n <>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.previous}\n disabled={!canPrev}\n onClick={() => go(-1)}\n className=\"absolute start-3 top-1/2 -translate-y-1/2\"\n tooltip={false}\n >\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.next}\n disabled={!canNext}\n onClick={() => go(1)}\n className=\"absolute end-3 top-1/2 -translate-y-1/2\"\n tooltip={false}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </>\n )}\n </FullBleedDialog>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,114 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode, Key } from 'react';
3
+
4
+ /** The words {@link LineItems} renders on its own behalf. Row numbers are 1-based. */
5
+ interface LineItemsLabels {
6
+ /** The dashed add button. */
7
+ add: string;
8
+ /** The remove button's name for one row. */
9
+ remove: (row: number) => string;
10
+ /** Its name while it waits for the second press (`confirmRemove`). */
11
+ confirmRemove: (row: number) => string;
12
+ /** A row's group name, when the caller gives no `rowLabel`. */
13
+ row: (row: number) => string;
14
+ /** The name a cell's field should carry: the column, and which row. */
15
+ cell: (column: string, row: number) => string;
16
+ /** The totals row's label, and its group name. */
17
+ totals: string;
18
+ }
19
+ declare const DEFAULT_LINE_ITEMS_LABELS: LineItemsLabels;
20
+ /** What a column's `render` receives for one row. */
21
+ interface LineItemCellContext<T> {
22
+ item: T;
23
+ /** 0-based. */
24
+ index: number;
25
+ /** The accessible name for the cell's field — "Debit, row 2". The header is
26
+ * hidden from assistive tech (each row repeats it), so the field has to carry it:
27
+ * `<Input aria-label={label} … />`. */
28
+ label: string;
29
+ }
30
+ interface LineItemsColumn<T> {
31
+ /** Stable id: the React key, and the key into `totals`. */
32
+ key: string;
33
+ /** The column header (and the small label over the cell once the rows stack). */
34
+ header: ReactNode;
35
+ /** The column's name for {@link LineItemCellContext.label}, when `header` is not a
36
+ * string. */
37
+ label?: string;
38
+ /** The grid track once the rows are wide enough to be a table. Default
39
+ * `minmax(0,1fr)`; `"7rem"` for an amount. */
40
+ width?: string;
41
+ /** `end` for figures: the header and the totals cell end-align with the field. */
42
+ align?: "start" | "end";
43
+ render: (ctx: LineItemCellContext<T>) => ReactNode;
44
+ }
45
+ interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
46
+ items: readonly T[];
47
+ columns: readonly LineItemsColumn<T>[];
48
+ /** The row's React key. Default: the item's `id` (react-hook-form's `useFieldArray`
49
+ * `fields` carry one), else its index. */
50
+ getKey?: (item: T, index: number) => Key;
51
+ /** Append a row. Omitted: no add button. The first field of the new row is focused. */
52
+ onAdd?: () => void;
53
+ /** Remove the row at `index`. Omitted: no remove buttons. */
54
+ onRemove?: (index: number) => void;
55
+ /** Remove is disabled at or below this many rows (a journal entry needs two).
56
+ * Default 0. */
57
+ minItems?: number;
58
+ /** Add is disabled at this many rows. */
59
+ maxItems?: number;
60
+ /**
61
+ * Ask before a row goes:
62
+ * - `true`: the remove button asks for a second press — it fills red and its name
63
+ * becomes `lineItems.confirmRemove`; Escape, or leaving it, calls it off.
64
+ * - a function: called with the row, and the row goes only if it answers `true`
65
+ * (`(item, i) => confirm({ title: … , tone: "danger" })` with `useConfirm`).
66
+ */
67
+ confirmRemove?: boolean | ((item: T, index: number) => boolean | Promise<boolean>);
68
+ /** The add button's text. Default `lineItems.add`. */
69
+ addLabel?: ReactNode;
70
+ /** A row's group name ("Line 2"). Default `lineItems.row`. */
71
+ rowLabel?: (index: number, item: T) => string;
72
+ /** A totals row under the lines, one entry per column `key`. The first column
73
+ * without an entry carries {@link totalsLabel}. Omitted: no totals row. */
74
+ totals?: Partial<Record<string, ReactNode>>;
75
+ /** Default `lineItems.totals`, "Total". */
76
+ totalsLabel?: ReactNode;
77
+ /** Shown in place of the rows when there are none. */
78
+ empty?: ReactNode;
79
+ /** A message about the list as a whole ("The entry does not balance"). */
80
+ error?: ReactNode;
81
+ /** Disables add and remove (the fields are the caller's to disable). */
82
+ disabled?: boolean;
83
+ labels?: Partial<LineItemsLabels>;
84
+ }
85
+ /**
86
+ * An editable list of lines — journal entry lines, lease components, invoice
87
+ * positions, budget rows: column headers, a row of fields per item, a remove button
88
+ * per row, a dashed "add" button under them, and an optional totals row.
89
+ *
90
+ * Presentation only: the rows are the caller's `items` and the fields are the
91
+ * caller's, rendered per column. So it works over plain state —
92
+ *
93
+ * ```tsx
94
+ * <LineItems items={lines} columns={cols} onAdd={() => setLines([...lines, blank()])}
95
+ * onRemove={(i) => setLines(lines.filter((_, j) => j !== i))} minItems={2} />
96
+ * ```
97
+ *
98
+ * — and over react-hook-form's `useFieldArray` alike (`items={fields}`, `onAdd={() =>
99
+ * append(blank())}`, `onRemove={remove}`), for which `@eifi1/ui-kit/rhf` has
100
+ * `RhfLineItems`.
101
+ *
102
+ * On a wide pane the rows are a grid under the headers; on a narrow one (a container
103
+ * query, so a dialog stacks where it is cramped) each row stacks its fields, each
104
+ * under a small copy of its header. The headers are `aria-hidden`: every row is a
105
+ * named group and every field is named by `render`'s `label` ("Debit, row 2"), which
106
+ * is what a screen reader needs — a header row it would read once, out of context.
107
+ *
108
+ * Keyboard: after Add, focus lands in the new row's first field; after a remove, on
109
+ * the row that took its place (else the add button). Ctrl/⌘+Enter in any row adds a
110
+ * row.
111
+ */
112
+ declare function LineItems<T>({ items, columns, getKey, onAdd, onRemove, minItems, maxItems, confirmRemove, addLabel, rowLabel, totals, totalsLabel, empty, error, disabled, labels: labelsProp, className, style, ...rest }: LineItemsProps<T>): react.JSX.Element;
113
+
114
+ export { DEFAULT_LINE_ITEMS_LABELS, type LineItemCellContext, LineItems, type LineItemsColumn, type LineItemsLabels, type LineItemsProps };
@@ -0,0 +1,238 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
4
+ import { Plus, Trash2 } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { useKitLabels } from "../i18n/kit-labels.js";
7
+ import { Button, IconButton } from "./ui.js";
8
+ const DEFAULT_LINE_ITEMS_LABELS = {
9
+ add: "Add row",
10
+ remove: (row) => `Remove row ${row}`,
11
+ confirmRemove: (row) => `Remove row ${row}? Press again to confirm`,
12
+ row: (row) => `Row ${row}`,
13
+ cell: (column, row) => `${column}, row ${row}`,
14
+ totals: "Total"
15
+ };
16
+ const REMOVE_TRACK = "2.25rem";
17
+ const FOCUSABLE = "input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex='-1'])";
18
+ function defaultKey(item, index) {
19
+ if (typeof item === "object" && item !== null && "id" in item) {
20
+ const id = item.id;
21
+ if (typeof id === "string" || typeof id === "number") return id;
22
+ }
23
+ return index;
24
+ }
25
+ function hasContent(node) {
26
+ return node !== void 0 && node !== null && node !== false && node !== "";
27
+ }
28
+ function LineItems({
29
+ items,
30
+ columns,
31
+ getKey = defaultKey,
32
+ onAdd,
33
+ onRemove,
34
+ minItems = 0,
35
+ maxItems,
36
+ confirmRemove,
37
+ addLabel,
38
+ rowLabel,
39
+ totals,
40
+ totalsLabel,
41
+ empty,
42
+ error,
43
+ disabled = false,
44
+ labels: labelsProp,
45
+ className,
46
+ style,
47
+ ...rest
48
+ }) {
49
+ const labels = useKitLabels("lineItems", DEFAULT_LINE_ITEMS_LABELS, labelsProp);
50
+ const rowRefs = useRef([]);
51
+ const addRef = useRef(null);
52
+ const pendingFocus = useRef(null);
53
+ const [confirmingAt, setConfirming] = useState(null);
54
+ const confirming = confirmingAt !== null && confirmingAt < items.length ? confirmingAt : null;
55
+ const tracks = [...columns.map((c) => c.width ?? "minmax(0,1fr)"), ...onRemove ? [REMOVE_TRACK] : []];
56
+ const gridStyle = { "--line-items-cols": tracks.join(" ") };
57
+ const canAdd = Boolean(onAdd) && !disabled && (maxItems === void 0 || items.length < maxItems);
58
+ const canRemove = Boolean(onRemove) && !disabled && items.length > minItems;
59
+ const hasError = hasContent(error);
60
+ useLayoutEffect(() => {
61
+ const pending = pendingFocus.current;
62
+ if (!pending) return;
63
+ pendingFocus.current = null;
64
+ if (pending.kind === "added") {
65
+ const row2 = rowRefs.current[items.length - 1];
66
+ row2?.querySelector(FOCUSABLE)?.focus();
67
+ return;
68
+ }
69
+ const row = rowRefs.current[Math.min(pending.index, items.length - 1)];
70
+ const target = row?.querySelector("[data-line-items-remove]:not([disabled])") ?? row?.querySelector(FOCUSABLE) ?? addRef.current;
71
+ target?.focus();
72
+ }, [items.length]);
73
+ const add = useCallback(() => {
74
+ if (!canAdd || !onAdd) return;
75
+ pendingFocus.current = { kind: "added" };
76
+ onAdd();
77
+ }, [canAdd, onAdd]);
78
+ const removeAt = async (index) => {
79
+ if (!canRemove || !onRemove) return;
80
+ if (confirmRemove === true && confirming !== index) {
81
+ setConfirming(index);
82
+ return;
83
+ }
84
+ if (typeof confirmRemove === "function") {
85
+ const ok = await confirmRemove(items[index], index);
86
+ if (!ok) return;
87
+ }
88
+ setConfirming(null);
89
+ pendingFocus.current = { kind: "removed", index };
90
+ onRemove(index);
91
+ };
92
+ const onRowKeyDown = (e) => {
93
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && canAdd) {
94
+ e.preventDefault();
95
+ add();
96
+ }
97
+ };
98
+ const columnName = (column) => column.label ?? (typeof column.header === "string" ? column.header : column.key);
99
+ const headerCell = (column) => /* @__PURE__ */ jsx(
100
+ "span",
101
+ {
102
+ className: cn(
103
+ "min-w-0 truncate text-xs font-medium text-[var(--text-muted)]",
104
+ column.align === "end" && "text-end"
105
+ ),
106
+ children: column.header
107
+ },
108
+ column.key
109
+ );
110
+ const showTotals = totals !== void 0;
111
+ const labelColumn = showTotals ? columns.find((c) => !hasContent(totals[c.key]))?.key : void 0;
112
+ return /* @__PURE__ */ jsxs(
113
+ "div",
114
+ {
115
+ "data-slot": "line-items",
116
+ ...rest,
117
+ style: { ...gridStyle, ...style },
118
+ className: cn("@container space-y-2", className),
119
+ children: [
120
+ /* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "hidden gap-2 @lg:grid @lg:grid-cols-[var(--line-items-cols)]", children: [
121
+ columns.map(headerCell),
122
+ onRemove && /* @__PURE__ */ jsx("span", {})
123
+ ] }),
124
+ items.length === 0 && hasContent(empty) && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-[var(--border)] px-3 py-4 text-center text-sm text-[var(--text-muted)]", children: empty }),
125
+ items.map((item, index) => {
126
+ const row = index + 1;
127
+ const isConfirming = confirming === index;
128
+ return (
129
+ // The row's Ctrl/⌘+Enter is a shortcut for the add button, which is itself
130
+ // reachable by Tab; the group only listens for it bubbling from its fields.
131
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
132
+ /* @__PURE__ */ jsxs(
133
+ "div",
134
+ {
135
+ ref: (el) => {
136
+ rowRefs.current[index] = el;
137
+ },
138
+ role: "group",
139
+ "aria-label": rowLabel ? rowLabel(index, item) : labels.row(row),
140
+ "data-line-items-row": "",
141
+ onKeyDown: onRowKeyDown,
142
+ className: cn(
143
+ "grid grid-cols-1 gap-2 @lg:grid-cols-[var(--line-items-cols)] @lg:items-start",
144
+ // Stacked, each row is a card of its own so the rows stay apart.
145
+ "@max-lg:rounded-md @max-lg:border @max-lg:border-[var(--border)] @max-lg:p-3"
146
+ ),
147
+ children: [
148
+ columns.map((column) => /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1 @lg:space-y-0", children: [
149
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "block text-xs font-medium text-[var(--text-muted)] @lg:hidden", children: column.header }),
150
+ column.render({ item, index, label: labels.cell(columnName(column), row) })
151
+ ] }, column.key)),
152
+ onRemove && /* @__PURE__ */ jsx("div", { className: "flex justify-end @lg:justify-center", children: /* @__PURE__ */ jsx(
153
+ IconButton,
154
+ {
155
+ type: "button",
156
+ "data-line-items-remove": "",
157
+ variant: isConfirming ? "danger" : "ghost",
158
+ tone: isConfirming ? void 0 : "danger",
159
+ label: isConfirming ? labels.confirmRemove(row) : labels.remove(row),
160
+ disabled: !canRemove,
161
+ onClick: () => void removeAt(index),
162
+ onBlur: () => {
163
+ if (isConfirming) setConfirming(null);
164
+ },
165
+ onKeyDown: (e) => {
166
+ if (e.key === "Escape" && isConfirming) {
167
+ e.stopPropagation();
168
+ setConfirming(null);
169
+ }
170
+ },
171
+ children: /* @__PURE__ */ jsx(Trash2, {})
172
+ }
173
+ ) })
174
+ ]
175
+ },
176
+ getKey(item, index)
177
+ )
178
+ );
179
+ }),
180
+ showTotals && /* @__PURE__ */ jsxs(
181
+ "div",
182
+ {
183
+ role: "group",
184
+ "aria-label": typeof totalsLabel === "string" ? totalsLabel : labels.totals,
185
+ "data-line-items-totals": "",
186
+ className: "grid grid-cols-1 gap-2 border-t border-[var(--border)] pt-2 text-sm font-semibold tabular-nums text-[var(--text-primary)] @lg:grid-cols-[var(--line-items-cols)]",
187
+ children: [
188
+ columns.map((column) => {
189
+ const value = totals[column.key];
190
+ if (column.key === labelColumn) {
191
+ return /* @__PURE__ */ jsx("span", { className: "min-w-0", children: totalsLabel ?? labels.totals }, column.key);
192
+ }
193
+ return /* @__PURE__ */ jsx(
194
+ "span",
195
+ {
196
+ className: cn(
197
+ "min-w-0",
198
+ column.align === "end" && "@lg:text-end",
199
+ // Stacked, a column with no total takes no line.
200
+ !hasContent(value) && "@max-lg:hidden"
201
+ ),
202
+ children: hasContent(value) && /* @__PURE__ */ jsxs("span", { className: "flex justify-between gap-2 @lg:block", children: [
203
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "font-normal text-[var(--text-muted)] @lg:hidden", children: column.header }),
204
+ value
205
+ ] })
206
+ },
207
+ column.key
208
+ );
209
+ }),
210
+ onRemove && /* @__PURE__ */ jsx("span", { className: "@max-lg:hidden" })
211
+ ]
212
+ }
213
+ ),
214
+ hasError && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-tight text-[var(--danger)]", children: error }),
215
+ onAdd && /* @__PURE__ */ jsxs(
216
+ Button,
217
+ {
218
+ ref: addRef,
219
+ type: "button",
220
+ variant: "secondary",
221
+ className: "w-full border-dashed",
222
+ disabled: !canAdd,
223
+ onClick: add,
224
+ children: [
225
+ /* @__PURE__ */ jsx(Plus, { "aria-hidden": true, className: "size-4" }),
226
+ addLabel ?? labels.add
227
+ ]
228
+ }
229
+ )
230
+ ]
231
+ }
232
+ );
233
+ }
234
+ export {
235
+ DEFAULT_LINE_ITEMS_LABELS,
236
+ LineItems
237
+ };
238
+ //# sourceMappingURL=line-items.js.map