@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,191 @@
1
+ import { isValidElement } from "react";
2
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
3
+ import { cn } from "../lib/cn";
4
+ import { useKitLabels } from "../i18n/kit-labels";
5
+ import { Button, Spinner, type ButtonVariant } from "./ui";
6
+
7
+ /** The words {@link FormActions} renders on its own behalf — the `form` namespace. */
8
+ export interface FormActionsLabels {
9
+ save: string;
10
+ cancel: string;
11
+ }
12
+
13
+ export const DEFAULT_FORM_ACTIONS_LABELS: FormActionsLabels = {
14
+ save: "Save",
15
+ cancel: "Cancel",
16
+ };
17
+
18
+ export type FormActionsAlign = "start" | "center" | "end" | "between";
19
+
20
+ /**
21
+ * Where the row sits:
22
+ * - `inline` (default): under the form's last field, `pt-4` above it.
23
+ * - `sticky`: stuck to the bottom of the scrolling page or pane, on the surface
24
+ * colour with a rule above, clear of the phone's home indicator — for a long form
25
+ * whose Save should not be a scroll away.
26
+ * - `dialog`: no spacing of its own, for a dialog's footer slot (DialogFrame,
27
+ * FullBleedDialog's `footer`), which already pads and rules it.
28
+ */
29
+ export type FormActionsPlacement = "inline" | "sticky" | "dialog";
30
+
31
+ /** The start-side action of {@link FormActionsProps.destructive}, as data. */
32
+ export interface FormActionsDestructive {
33
+ label: ReactNode;
34
+ onClick: () => void;
35
+ disabled?: boolean;
36
+ /** See Button's `disabledReason`. */
37
+ disabledReason?: ReactNode;
38
+ }
39
+
40
+ const ALIGN_CLASS: Record<FormActionsAlign, string> = {
41
+ start: "justify-start",
42
+ center: "justify-center",
43
+ end: "justify-end",
44
+ between: "justify-between",
45
+ };
46
+
47
+ const PLACEMENT_CLASS: Record<FormActionsPlacement, string> = {
48
+ inline: "pt-4",
49
+ sticky: "sticky bottom-0 z-10 border-t border-[var(--border)] bg-[var(--bg-surface)] pt-3",
50
+ dialog: "",
51
+ };
52
+
53
+ export interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
54
+ /** Called by Cancel. Omitted: no Cancel button (a form that only saves). */
55
+ onCancel?: () => void;
56
+ /**
57
+ * Given: the save button is `type="button"` and calls this — for a dialog or a row
58
+ * that saves from a handler. Omitted: it is `type="submit"` and submits the
59
+ * enclosing `<form>` (or the one named by {@link form}).
60
+ */
61
+ onSubmit?: () => void;
62
+ /** The save button's text. Default `form.save` from the provider, else "Save". */
63
+ submitLabel?: ReactNode;
64
+ /** The cancel button's text. Default `form.cancel` from the provider, else "Cancel". */
65
+ cancelLabel?: ReactNode;
66
+ /** The save button's text while `pending` ("Saving…"). Default: `submitLabel`. */
67
+ pendingLabel?: ReactNode;
68
+ /**
69
+ * The save is running: a spinner in the save button, `aria-busy` on it, and the
70
+ * button disabled, so a second click cannot send the form twice. Cancel stays
71
+ * usable.
72
+ */
73
+ pending?: boolean;
74
+ /** Disable save for a reason of the form's own (an unbalanced entry). */
75
+ submitDisabled?: boolean;
76
+ /** Why save is disabled — see Button's `disabledReason`. Keeps it focusable. */
77
+ submitDisabledReason?: ReactNode;
78
+ /** The save button's variant. Default `brand`; `danger` for a save that destroys
79
+ * (kastlan's `destructive` flag on its own FormActions). */
80
+ submitVariant?: ButtonVariant;
81
+ /**
82
+ * A destructive action at the START of the row, apart from Save and Cancel — the
83
+ * "Delete" of an edit dialog. Pass `{ label, onClick }` for the kit's quiet danger
84
+ * button, or an element of your own (a {@link DangerConfirm}, say). With it the row
85
+ * is `between`-aligned unless `align` says otherwise.
86
+ */
87
+ destructive?: ReactNode | FormActionsDestructive;
88
+ /** Horizontal alignment. Default `end` (`between` with a `destructive` action). */
89
+ align?: FormActionsAlign;
90
+ /** See {@link FormActionsPlacement}. Default `inline`. */
91
+ placement?: FormActionsPlacement;
92
+ /** The `id` of the `<form>` the save button submits, when the row is rendered
93
+ * outside it (a dialog's footer slot). */
94
+ form?: string;
95
+ /** Extra actions, placed before Cancel. */
96
+ children?: ReactNode;
97
+ }
98
+
99
+ function isDestructiveData(value: unknown): value is FormActionsDestructive {
100
+ return typeof value === "object" && value !== null && !isValidElement(value) && "onClick" in value;
101
+ }
102
+
103
+ /**
104
+ * The Save / Cancel row every form, wizard-step form and dialog ends in — kastlan's
105
+ * `FormActions`, with the labels from the provider, a pending state and a place for
106
+ * the destructive action.
107
+ *
108
+ * ```tsx
109
+ * <form onSubmit={form.handleSubmit(save)}>
110
+ * …
111
+ * <FormActions onCancel={close} pending={mutation.isPending} />
112
+ * </form>
113
+ * ```
114
+ *
115
+ * The spinner is the kit's {@link Spinner}, drawn inside the button with `label={null}`
116
+ * so the button's name stays its text; `aria-busy` is what says it is working.
117
+ */
118
+ export function FormActions({
119
+ onCancel,
120
+ onSubmit,
121
+ submitLabel,
122
+ cancelLabel,
123
+ pendingLabel,
124
+ pending = false,
125
+ submitDisabled = false,
126
+ submitDisabledReason,
127
+ submitVariant = "brand",
128
+ destructive,
129
+ align,
130
+ placement = "inline",
131
+ form,
132
+ children,
133
+ className,
134
+ style,
135
+ ...rest
136
+ }: FormActionsProps) {
137
+ const labels = useKitLabels("form", DEFAULT_FORM_ACTIONS_LABELS);
138
+ const hasDestructive = destructive !== undefined && destructive !== null && destructive !== false;
139
+ const justify = align ?? (hasDestructive ? "between" : "end");
140
+ const start = !hasDestructive ? null : isDestructiveData(destructive) ? (
141
+ <Button
142
+ type="button"
143
+ variant="ghost"
144
+ tone="danger"
145
+ onClick={destructive.onClick}
146
+ disabled={destructive.disabled}
147
+ disabledReason={destructive.disabledReason}
148
+ >
149
+ {destructive.label}
150
+ </Button>
151
+ ) : (
152
+ destructive
153
+ );
154
+ const saveLabel = submitLabel ?? labels.save;
155
+
156
+ return (
157
+ <div
158
+ data-slot="form-actions"
159
+ data-placement={placement}
160
+ {...rest}
161
+ style={
162
+ placement === "sticky"
163
+ ? { paddingBottom: "max(0.75rem, env(safe-area-inset-bottom))", ...style }
164
+ : style
165
+ }
166
+ className={cn("flex flex-wrap items-center gap-2", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className)}
167
+ >
168
+ {start && <div className="me-auto flex items-center gap-2">{start}</div>}
169
+ <div className="flex flex-wrap items-center gap-2">
170
+ {children}
171
+ {onCancel && (
172
+ <Button type="button" variant="secondary" onClick={onCancel}>
173
+ {cancelLabel ?? labels.cancel}
174
+ </Button>
175
+ )}
176
+ <Button
177
+ type={onSubmit ? "button" : "submit"}
178
+ form={form}
179
+ onClick={onSubmit}
180
+ variant={submitVariant}
181
+ disabled={pending || submitDisabled}
182
+ disabledReason={pending ? undefined : submitDisabledReason}
183
+ aria-busy={pending || undefined}
184
+ >
185
+ {pending && <Spinner label={null} className="size-4" />}
186
+ {pending && pendingLabel !== undefined ? pendingLabel : saveLabel}
187
+ </Button>
188
+ </div>
189
+ </div>
190
+ );
191
+ }
@@ -0,0 +1,181 @@
1
+ import { useState } from "react";
2
+ import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react";
3
+ import { FileText } from "lucide-react";
4
+
5
+ import { cn } from "../lib/cn";
6
+ import { useKitLabels } from "../i18n/kit-labels";
7
+ import type { AuthedFetcher } from "../hooks/use-authed-src";
8
+ import { AuthedImage } from "./authed-image";
9
+ import { Lightbox, imageItemKind } from "./lightbox";
10
+ import type { ImageItem, LightboxLabels } from "./lightbox";
11
+
12
+ export interface ImageGridLabels {
13
+ /** The list's accessible name. */
14
+ list: string;
15
+ /** An open button's name when an item's `alt` is empty: "Image 3 of 7". */
16
+ item: (index: number, count: number) => string;
17
+ /** The name of the group of per-image actions, given the image's name. */
18
+ actions: (name: string) => string;
19
+ }
20
+
21
+ export const DEFAULT_IMAGE_GRID_LABELS: ImageGridLabels = {
22
+ list: "Images",
23
+ item: (index, count) => `Image ${index} of ${count}`,
24
+ actions: (name) => `Actions for ${name}`,
25
+ };
26
+
27
+ export interface ImageGridProps extends Omit<ComponentPropsWithoutRef<"ul">, "children"> {
28
+ items: readonly ImageItem[];
29
+ /** The app's authenticated GET — thumbnails and the lightbox fetch through it. Only
30
+ * images are fetched for the grid; a file (PDF) tile shows its icon and name. */
31
+ fetcher?: AuthedFetcher | null;
32
+ /**
33
+ * Per-image actions — a delete, an edit. Rendered BESIDE the open button (on top of
34
+ * the thumbnail's corner), never inside it: a button inside a button is invalid HTML
35
+ * and a screen reader flattens it into the outer one's name, where it cannot be
36
+ * reached. Use `IconButton variant="overlay"` for controls that sit on a photo.
37
+ */
38
+ renderActions?: (item: ImageItem, index: number) => ReactNode;
39
+ /**
40
+ * What a tile's click does. Left out, the grid opens its own {@link Lightbox} on that
41
+ * image. Given, the grid opens nothing — for a caller that keeps the lightbox's
42
+ * index in the URL, or that opens something else.
43
+ */
44
+ onOpen?: (index: number) => void;
45
+ /**
46
+ * A fixed column count. Left out, the grid is responsive: as many columns of at
47
+ * least {@link minTileSize} px as fit, so the same grid is three across in a card on
48
+ * a phone and eight across on a desktop page — no breakpoints to choose.
49
+ */
50
+ columns?: number;
51
+ /** The smallest a tile may be in the responsive layout. Default 96 (px). */
52
+ minTileSize?: number;
53
+ /** `square` (default) crops to a square; `video` to 16:9; `auto` keeps each image's
54
+ * own proportions (a floor plan), within a square-ish box. */
55
+ aspect?: "square" | "video" | "auto";
56
+ /** Props for the built-in lightbox (ignored with {@link onOpen}). */
57
+ lightbox?: {
58
+ loop?: boolean;
59
+ zoom?: boolean;
60
+ download?: boolean;
61
+ onDownload?: (item: ImageItem, index: number) => void;
62
+ labels?: Partial<LightboxLabels>;
63
+ };
64
+ labels?: Partial<ImageGridLabels>;
65
+ }
66
+
67
+ const ASPECT_CLASS = {
68
+ square: "aspect-square",
69
+ video: "aspect-video",
70
+ auto: "aspect-[4/3]",
71
+ } as const;
72
+
73
+ /**
74
+ * A grid of thumbnails that open in a {@link Lightbox} — kastlan's room and defect
75
+ * photos (room-inspector, unit-rooms-tab) and its floor plans (unit-floor-plan), each
76
+ * of which drew its own `grid grid-cols-N` of `<img>` that nothing could open, with no
77
+ * loading state and nowhere to put a delete.
78
+ *
79
+ * Each tile is a real `<button>` named by the image's alt, with the actions beside it
80
+ * and the caption below it — outside the button, so the name stays the alt and the
81
+ * caption is read once. Placeholders come from {@link AuthedImage}: a skeleton while
82
+ * loading, a "couldn't load" tile on failure.
83
+ */
84
+ export function ImageGrid({
85
+ items,
86
+ fetcher,
87
+ renderActions,
88
+ onOpen,
89
+ columns,
90
+ minTileSize = 96,
91
+ aspect = "square",
92
+ lightbox,
93
+ labels: labelsProp,
94
+ className,
95
+ style,
96
+ ...rest
97
+ }: ImageGridProps) {
98
+ const labels = useKitLabels("imageGrid", DEFAULT_IMAGE_GRID_LABELS, labelsProp);
99
+ const [openIndex, setOpenIndex] = useState<number | null>(null);
100
+ const count = items.length;
101
+
102
+ const gridStyle: CSSProperties = {
103
+ gridTemplateColumns:
104
+ columns !== undefined
105
+ ? `repeat(${columns}, minmax(0, 1fr))`
106
+ : `repeat(auto-fill, minmax(min(${minTileSize}px, 100%), 1fr))`,
107
+ ...style,
108
+ };
109
+
110
+ return (
111
+ <>
112
+ <ul
113
+ aria-label={labels.list}
114
+ {...rest}
115
+ className={cn("grid gap-2", className)}
116
+ style={gridStyle}
117
+ >
118
+ {items.map((item, i) => {
119
+ const name = item.alt || labels.item(i + 1, count);
120
+ const kind = imageItemKind(item);
121
+ const actions = renderActions?.(item, i);
122
+ return (
123
+ <li key={item.key ?? item.src} className="relative min-w-0">
124
+ <button
125
+ type="button"
126
+ aria-label={name}
127
+ onClick={() => (onOpen ? onOpen(i) : setOpenIndex(i))}
128
+ className={cn(
129
+ "block w-full overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-hover)]",
130
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]",
131
+ ASPECT_CLASS[aspect],
132
+ )}
133
+ >
134
+ {kind === "file" ? (
135
+ <span className="flex h-full w-full flex-col items-center justify-center gap-1 p-2 text-center text-xs text-[var(--text-secondary)]">
136
+ <FileText aria-hidden className="size-6 shrink-0 text-[var(--text-muted)]" />
137
+ <span aria-hidden className="line-clamp-2 break-all">{item.fileName ?? item.alt}</span>
138
+ </span>
139
+ ) : (
140
+ <AuthedImage
141
+ src={item.thumbnailSrc ?? item.src}
142
+ // The button is named already; the picture inside it is decoration
143
+ // to a reader, or the name would be read twice.
144
+ alt=""
145
+ fetcher={fetcher}
146
+ loading="lazy"
147
+ wrapperClassName="h-full w-full"
148
+ className={cn("h-full w-full", aspect === "auto" ? "object-contain" : "object-cover")}
149
+ />
150
+ )}
151
+ </button>
152
+ {actions !== undefined && actions !== null && actions !== false && (
153
+ <div
154
+ role="group"
155
+ aria-label={labels.actions(name)}
156
+ className="absolute end-1 top-1 flex gap-1"
157
+ >
158
+ {actions}
159
+ </div>
160
+ )}
161
+ {item.caption !== undefined && item.caption !== null && item.caption !== false && (
162
+ <div className="mt-1 line-clamp-2 text-xs text-[var(--text-secondary)]">{item.caption}</div>
163
+ )}
164
+ </li>
165
+ );
166
+ })}
167
+ </ul>
168
+ {!onOpen && (
169
+ <Lightbox
170
+ open={openIndex !== null}
171
+ onClose={() => setOpenIndex(null)}
172
+ items={items}
173
+ index={openIndex ?? 0}
174
+ onIndexChange={setOpenIndex}
175
+ fetcher={fetcher}
176
+ {...lightbox}
177
+ />
178
+ )}
179
+ </>
180
+ );
181
+ }