@eifi1/ui-kit 0.10.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 (382) hide show
  1. package/README.md +66 -37
  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/alert-banner.d.ts +26 -1
  12. package/dist/components/alert-banner.js +10 -7
  13. package/dist/components/alert-banner.js.map +1 -1
  14. package/dist/components/amount-input.d.ts +26 -4
  15. package/dist/components/amount-input.js +5 -2
  16. package/dist/components/amount-input.js.map +1 -1
  17. package/dist/components/authed-image.d.ts +68 -0
  18. package/dist/components/authed-image.js +117 -0
  19. package/dist/components/authed-image.js.map +1 -0
  20. package/dist/components/breadcrumbs.d.ts +2 -1
  21. package/dist/components/breadcrumbs.js +5 -2
  22. package/dist/components/breadcrumbs.js.map +1 -1
  23. package/dist/components/bulk-action-bar.d.ts +51 -5
  24. package/dist/components/bulk-action-bar.js +88 -41
  25. package/dist/components/bulk-action-bar.js.map +1 -1
  26. package/dist/components/button-group.d.ts +107 -3
  27. package/dist/components/button-group.js +60 -5
  28. package/dist/components/button-group.js.map +1 -1
  29. package/dist/components/calculator.d.ts +17 -4
  30. package/dist/components/calendar-heatmap.d.ts +162 -0
  31. package/dist/components/calendar-heatmap.js +336 -0
  32. package/dist/components/calendar-heatmap.js.map +1 -0
  33. package/dist/components/chip.d.ts +2 -1
  34. package/dist/components/chip.js +6 -3
  35. package/dist/components/chip.js.map +1 -1
  36. package/dist/components/choice-card.d.ts +66 -2
  37. package/dist/components/choice-card.js +108 -1
  38. package/dist/components/choice-card.js.map +1 -1
  39. package/dist/components/combobox-core.js +2 -0
  40. package/dist/components/combobox-core.js.map +1 -1
  41. package/dist/components/combobox.d.ts +2 -2
  42. package/dist/components/combobox.js +31 -9
  43. package/dist/components/combobox.js.map +1 -1
  44. package/dist/components/copy-button.d.ts +52 -70
  45. package/dist/components/copy-button.js +1 -1
  46. package/dist/components/copy-button.js.map +1 -1
  47. package/dist/components/data-table-cells.d.ts +63 -0
  48. package/dist/components/data-table-cells.js +64 -0
  49. package/dist/components/data-table-cells.js.map +1 -0
  50. package/dist/components/data-table-filter-popover.d.ts +1 -1
  51. package/dist/components/data-table-filters.d.ts +1 -1
  52. package/dist/components/data-table-filters.js +31 -1
  53. package/dist/components/data-table-filters.js.map +1 -1
  54. package/dist/components/data-table-labels.d.ts +13 -0
  55. package/dist/components/data-table-labels.js +8 -1
  56. package/dist/components/data-table-labels.js.map +1 -1
  57. package/dist/components/data-table.d.ts +1 -1
  58. package/dist/components/data-table.js +104 -12
  59. package/dist/components/data-table.js.map +1 -1
  60. package/dist/components/date-picker.d.ts +3 -1
  61. package/dist/components/date-picker.js +15 -2
  62. package/dist/components/date-picker.js.map +1 -1
  63. package/dist/components/description-list.d.ts +23 -3
  64. package/dist/components/description-list.js +23 -3
  65. package/dist/components/description-list.js.map +1 -1
  66. package/dist/components/dialog-frame.d.ts +1 -0
  67. package/dist/components/entity-combobox.d.ts +1 -1
  68. package/dist/components/entity-combobox.js +18 -3
  69. package/dist/components/entity-combobox.js.map +1 -1
  70. package/dist/components/error-boundary.d.ts +92 -0
  71. package/dist/components/error-boundary.js +111 -0
  72. package/dist/components/error-boundary.js.map +1 -0
  73. package/dist/components/field.d.ts +69 -0
  74. package/dist/components/field.js +75 -0
  75. package/dist/components/field.js.map +1 -0
  76. package/dist/components/file-button.d.ts +51 -208
  77. package/dist/components/file-dropzone.d.ts +50 -2
  78. package/dist/components/floating-panel.d.ts +156 -4
  79. package/dist/components/floating-panel.js +193 -29
  80. package/dist/components/floating-panel.js.map +1 -1
  81. package/dist/components/form-actions.d.ts +53 -0
  82. package/dist/components/form-actions.js +98 -0
  83. package/dist/components/form-actions.js.map +1 -0
  84. package/dist/components/image-grid.d.ts +67 -0
  85. package/dist/components/image-grid.js +112 -0
  86. package/dist/components/image-grid.js.map +1 -0
  87. package/dist/components/lightbox.d.ts +104 -0
  88. package/dist/components/lightbox.js +229 -0
  89. package/dist/components/lightbox.js.map +1 -0
  90. package/dist/components/line-items.d.ts +114 -0
  91. package/dist/components/line-items.js +238 -0
  92. package/dist/components/line-items.js.map +1 -0
  93. package/dist/components/list.d.ts +37 -3
  94. package/dist/components/list.js +39 -14
  95. package/dist/components/list.js.map +1 -1
  96. package/dist/components/loading-state.d.ts +24 -0
  97. package/dist/components/loading-state.js +32 -0
  98. package/dist/components/loading-state.js.map +1 -0
  99. package/dist/components/menu-item.d.ts +13 -2
  100. package/dist/components/menu-item.js +11 -2
  101. package/dist/components/menu-item.js.map +1 -1
  102. package/dist/components/mini-calendar.d.ts +82 -5
  103. package/dist/components/mini-calendar.js +212 -64
  104. package/dist/components/mini-calendar.js.map +1 -1
  105. package/dist/components/modal.d.ts +20 -1
  106. package/dist/components/modal.js +26 -2
  107. package/dist/components/modal.js.map +1 -1
  108. package/dist/components/month-picker.d.ts +21 -1
  109. package/dist/components/month-picker.js +139 -26
  110. package/dist/components/month-picker.js.map +1 -1
  111. package/dist/components/multi-entity-combobox.d.ts +1 -1
  112. package/dist/components/multi-entity-combobox.js +19 -3
  113. package/dist/components/multi-entity-combobox.js.map +1 -1
  114. package/dist/components/nav-pills.d.ts +67 -0
  115. package/dist/components/nav-pills.js +69 -0
  116. package/dist/components/nav-pills.js.map +1 -0
  117. package/dist/components/number-field.d.ts +26 -5
  118. package/dist/components/number-field.js +5 -7
  119. package/dist/components/number-field.js.map +1 -1
  120. package/dist/components/number-input.d.ts +27 -5
  121. package/dist/components/number-input.js +7 -1
  122. package/dist/components/number-input.js.map +1 -1
  123. package/dist/components/numpad-sheet.d.ts +17 -4
  124. package/dist/components/passkeys-setting.d.ts +67 -0
  125. package/dist/components/passkeys-setting.js +214 -0
  126. package/dist/components/passkeys-setting.js.map +1 -0
  127. package/dist/components/picker-sheet.js +2 -1
  128. package/dist/components/picker-sheet.js.map +1 -1
  129. package/dist/components/pie-chart-labels.d.ts +20 -0
  130. package/dist/components/pie-chart-labels.js +12 -0
  131. package/dist/components/pie-chart-labels.js.map +1 -0
  132. package/dist/components/pie-chart.d.ts +101 -0
  133. package/dist/components/pie-chart.js +283 -0
  134. package/dist/components/pie-chart.js.map +1 -0
  135. package/dist/components/progress-bar.d.ts +73 -3
  136. package/dist/components/progress-bar.js +110 -8
  137. package/dist/components/progress-bar.js.map +1 -1
  138. package/dist/components/qr-code.d.ts +34 -0
  139. package/dist/components/qr-code.js +57 -0
  140. package/dist/components/qr-code.js.map +1 -0
  141. package/dist/components/search-field.d.ts +15 -3
  142. package/dist/components/search-field.js +5 -1
  143. package/dist/components/search-field.js.map +1 -1
  144. package/dist/components/series-chart.d.ts +16 -0
  145. package/dist/components/series-chart.js +4 -1
  146. package/dist/components/series-chart.js.map +1 -1
  147. package/dist/components/settings-fields.d.ts +50 -1
  148. package/dist/components/signed-amount.d.ts +121 -0
  149. package/dist/components/signed-amount.js +152 -0
  150. package/dist/components/signed-amount.js.map +1 -0
  151. package/dist/components/skeleton.d.ts +10 -3
  152. package/dist/components/skeleton.js +12 -2
  153. package/dist/components/skeleton.js.map +1 -1
  154. package/dist/components/stat-tile.d.ts +2 -1
  155. package/dist/components/stat-tile.js +5 -2
  156. package/dist/components/stat-tile.js.map +1 -1
  157. package/dist/components/status-dot.d.ts +6 -2
  158. package/dist/components/status-dot.js +6 -1
  159. package/dist/components/status-dot.js.map +1 -1
  160. package/dist/components/table.d.ts +77 -11
  161. package/dist/components/table.js +85 -17
  162. package/dist/components/table.js.map +1 -1
  163. package/dist/components/text-link.d.ts +176 -0
  164. package/dist/components/text-link.js +109 -0
  165. package/dist/components/text-link.js.map +1 -0
  166. package/dist/components/text.d.ts +8 -1
  167. package/dist/components/text.js +1 -0
  168. package/dist/components/text.js.map +1 -1
  169. package/dist/components/time-input.d.ts +50 -1
  170. package/dist/components/toggle-group.d.ts +28 -0
  171. package/dist/components/toggle-group.js +58 -9
  172. package/dist/components/toggle-group.js.map +1 -1
  173. package/dist/components/toggle-legend.d.ts +36 -3
  174. package/dist/components/toggle-legend.js +49 -0
  175. package/dist/components/toggle-legend.js.map +1 -1
  176. package/dist/components/tooltip.d.ts +11 -3
  177. package/dist/components/tooltip.js +2 -0
  178. package/dist/components/tooltip.js.map +1 -1
  179. package/dist/components/ui.d.ts +52 -729
  180. package/dist/components/ui.js +296 -27
  181. package/dist/components/ui.js.map +1 -1
  182. package/dist/components/use-table-state.d.ts +3 -52
  183. package/dist/components/use-table-state.js +128 -30
  184. package/dist/components/use-table-state.js.map +1 -1
  185. package/dist/components/user-avatar.d.ts +25 -3
  186. package/dist/components/user-avatar.js +32 -3
  187. package/dist/components/user-avatar.js.map +1 -1
  188. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  189. package/dist/data-table.d.ts +2 -1
  190. package/dist/data-table.js +5 -1
  191. package/dist/data-table.js.map +1 -1
  192. package/dist/feedback/feedback-dialog.d.ts +26 -3
  193. package/dist/feedback/feedback-dialog.js +16 -2
  194. package/dist/feedback/feedback-dialog.js.map +1 -1
  195. package/dist/feedback/feedback-thread.d.ts +133 -0
  196. package/dist/feedback/feedback-thread.js +237 -0
  197. package/dist/feedback/feedback-thread.js.map +1 -0
  198. package/dist/feedback.d.ts +2 -1
  199. package/dist/feedback.js +1 -0
  200. package/dist/feedback.js.map +1 -1
  201. package/dist/hooks/use-authed-src.d.ts +71 -0
  202. package/dist/hooks/use-authed-src.js +63 -0
  203. package/dist/hooks/use-authed-src.js.map +1 -0
  204. package/dist/hooks/use-file-drop.d.ts +50 -2
  205. package/dist/hooks/use-hotkey.d.ts +59 -0
  206. package/dist/hooks/use-hotkey.js +105 -0
  207. package/dist/hooks/use-hotkey.js.map +1 -0
  208. package/dist/hooks/use-search-param-state.d.ts +83 -0
  209. package/dist/hooks/use-search-param-state.js +94 -0
  210. package/dist/hooks/use-search-param-state.js.map +1 -0
  211. package/dist/i18n/defaults.d.ts +17 -4
  212. package/dist/i18n/defaults.js +33 -1
  213. package/dist/i18n/defaults.js.map +1 -1
  214. package/dist/i18n/kit-labels.d.ts +47 -310
  215. package/dist/i18n/kit-labels.js +20 -5
  216. package/dist/i18n/kit-labels.js.map +1 -1
  217. package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
  218. package/dist/i18n/locales/de-CH.d.ts +17 -4
  219. package/dist/i18n/locales/de-informal.d.ts +17 -4
  220. package/dist/i18n/locales/de.d.ts +17 -4
  221. package/dist/i18n/locales/de.js +165 -4
  222. package/dist/i18n/locales/de.js.map +1 -1
  223. package/dist/i18n/locales/es.d.ts +17 -4
  224. package/dist/i18n/locales/es.js +162 -4
  225. package/dist/i18n/locales/es.js.map +1 -1
  226. package/dist/i18n/locales/fr.d.ts +17 -4
  227. package/dist/i18n/locales/fr.js +162 -4
  228. package/dist/i18n/locales/fr.js.map +1 -1
  229. package/dist/i18n/locales/hu.d.ts +17 -4
  230. package/dist/i18n/locales/hu.js +162 -4
  231. package/dist/i18n/locales/hu.js.map +1 -1
  232. package/dist/i18n/locales/it.d.ts +17 -4
  233. package/dist/i18n/locales/it.js +162 -4
  234. package/dist/i18n/locales/it.js.map +1 -1
  235. package/dist/i18n/locales/zh.d.ts +17 -4
  236. package/dist/i18n/locales/zh.js +162 -4
  237. package/dist/i18n/locales/zh.js.map +1 -1
  238. package/dist/index.d.ts +45 -21
  239. package/dist/index.js +43 -1
  240. package/dist/index.js.map +1 -1
  241. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  242. package/dist/lib/clipping.d.ts +21 -5
  243. package/dist/lib/clipping.js +3 -0
  244. package/dist/lib/clipping.js.map +1 -1
  245. package/dist/lib/dates.d.ts +17 -1
  246. package/dist/lib/dates.js +19 -0
  247. package/dist/lib/dates.js.map +1 -1
  248. package/dist/lib/format.d.ts +144 -0
  249. package/dist/lib/format.js +149 -0
  250. package/dist/lib/format.js.map +1 -0
  251. package/dist/lib/qr-encode.d.ts +44 -0
  252. package/dist/lib/qr-encode.js +338 -0
  253. package/dist/lib/qr-encode.js.map +1 -0
  254. package/dist/rhf/fields.d.ts +241 -0
  255. package/dist/rhf/fields.js +601 -0
  256. package/dist/rhf/fields.js.map +1 -0
  257. package/dist/rhf/form.d.ts +50 -1
  258. package/dist/rhf/line-items.d.ts +31 -0
  259. package/dist/rhf/line-items.js +59 -0
  260. package/dist/rhf/line-items.js.map +1 -0
  261. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  262. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  263. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  264. package/dist/rhf.d.ts +63 -1
  265. package/dist/rhf.js +3 -0
  266. package/dist/rhf.js.map +1 -1
  267. package/dist/shell/app-shell.d.ts +99 -2
  268. package/dist/shell/app-shell.js +58 -3
  269. package/dist/shell/app-shell.js.map +1 -1
  270. package/dist/shell/auth-layout.d.ts +74 -0
  271. package/dist/shell/auth-layout.js +97 -0
  272. package/dist/shell/auth-layout.js.map +1 -0
  273. package/dist/shell/top-bar-brand.d.ts +81 -0
  274. package/dist/shell/top-bar-brand.js +47 -0
  275. package/dist/shell/top-bar-brand.js.map +1 -0
  276. package/dist/shell/topbar-action-menu.d.ts +84 -3
  277. package/dist/shell/topbar-action-menu.js +115 -27
  278. package/dist/shell/topbar-action-menu.js.map +1 -1
  279. package/dist/shell.d.ts +58 -2
  280. package/dist/shell.js +2 -0
  281. package/dist/shell.js.map +1 -1
  282. package/dist/wizard/stepper-nav.d.ts +49 -1
  283. package/dist/wizard/wizard-context.d.ts +30 -1
  284. package/dist/wizard/wizard-context.js +23 -2
  285. package/dist/wizard/wizard-context.js.map +1 -1
  286. package/dist/wizard/wizard-step.d.ts +20 -5
  287. package/dist/wizard/wizard-step.js +16 -3
  288. package/dist/wizard/wizard-step.js.map +1 -1
  289. package/dist/wizard.d.ts +51 -3
  290. package/package.json +1 -1
  291. package/src/chart.ts +4 -0
  292. package/src/components/account-settings-labels.ts +190 -0
  293. package/src/components/account-settings.tsx +110 -63
  294. package/src/components/alert-banner.tsx +36 -7
  295. package/src/components/amount-input.tsx +13 -1
  296. package/src/components/authed-image.tsx +190 -0
  297. package/src/components/breadcrumbs.tsx +8 -2
  298. package/src/components/bulk-action-bar.tsx +163 -57
  299. package/src/components/button-group.tsx +129 -4
  300. package/src/components/calendar-heatmap.tsx +599 -0
  301. package/src/components/chip.tsx +8 -4
  302. package/src/components/choice-card.tsx +208 -1
  303. package/src/components/combobox-core.tsx +5 -0
  304. package/src/components/combobox.tsx +31 -9
  305. package/src/components/copy-button.tsx +3 -2
  306. package/src/components/data-table-cells.tsx +119 -0
  307. package/src/components/data-table-filters.ts +77 -0
  308. package/src/components/data-table-labels.ts +23 -0
  309. package/src/components/data-table.tsx +231 -13
  310. package/src/components/date-picker.tsx +19 -3
  311. package/src/components/description-list.tsx +52 -3
  312. package/src/components/entity-combobox.tsx +18 -3
  313. package/src/components/error-boundary.tsx +216 -0
  314. package/src/components/field.tsx +188 -0
  315. package/src/components/floating-panel.tsx +380 -16
  316. package/src/components/form-actions.tsx +191 -0
  317. package/src/components/image-grid.tsx +181 -0
  318. package/src/components/lightbox.tsx +374 -0
  319. package/src/components/line-items.tsx +385 -0
  320. package/src/components/list.tsx +93 -18
  321. package/src/components/loading-state.tsx +47 -0
  322. package/src/components/menu-item.tsx +29 -3
  323. package/src/components/mini-calendar.tsx +317 -57
  324. package/src/components/modal.tsx +61 -3
  325. package/src/components/month-picker.tsx +181 -26
  326. package/src/components/multi-entity-combobox.tsx +19 -3
  327. package/src/components/nav-pills.tsx +152 -0
  328. package/src/components/number-field.tsx +15 -10
  329. package/src/components/number-input.tsx +16 -1
  330. package/src/components/passkeys-setting.tsx +331 -0
  331. package/src/components/picker-sheet.tsx +7 -1
  332. package/src/components/pie-chart-labels.ts +32 -0
  333. package/src/components/pie-chart.tsx +479 -0
  334. package/src/components/progress-bar.tsx +241 -13
  335. package/src/components/qr-code.tsx +83 -0
  336. package/src/components/search-field.tsx +20 -4
  337. package/src/components/series-chart.tsx +21 -0
  338. package/src/components/signed-amount.tsx +286 -0
  339. package/src/components/skeleton.tsx +21 -3
  340. package/src/components/stat-tile.tsx +8 -2
  341. package/src/components/status-dot.tsx +10 -2
  342. package/src/components/table.tsx +186 -23
  343. package/src/components/text-link.tsx +248 -0
  344. package/src/components/text.tsx +9 -1
  345. package/src/components/toggle-group.tsx +106 -13
  346. package/src/components/toggle-legend.tsx +87 -2
  347. package/src/components/tooltip.tsx +17 -3
  348. package/src/components/ui.tsx +572 -44
  349. package/src/components/use-table-state.ts +265 -33
  350. package/src/components/user-avatar.tsx +57 -3
  351. package/src/data-table.ts +10 -0
  352. package/src/feedback/feedback-dialog.tsx +42 -3
  353. package/src/feedback/feedback-thread.tsx +434 -0
  354. package/src/feedback.ts +1 -0
  355. package/src/hooks/use-authed-src.ts +161 -0
  356. package/src/hooks/use-hotkey.ts +172 -0
  357. package/src/hooks/use-search-param-state.ts +187 -0
  358. package/src/i18n/defaults.ts +32 -0
  359. package/src/i18n/kit-labels.tsx +104 -8
  360. package/src/i18n/locales/de.ts +162 -0
  361. package/src/i18n/locales/es.ts +159 -0
  362. package/src/i18n/locales/fr.ts +162 -0
  363. package/src/i18n/locales/hu.ts +160 -0
  364. package/src/i18n/locales/it.ts +160 -0
  365. package/src/i18n/locales/zh.ts +159 -0
  366. package/src/index.ts +64 -0
  367. package/src/lib/clipping.ts +22 -4
  368. package/src/lib/dates.ts +37 -0
  369. package/src/lib/format.ts +325 -0
  370. package/src/lib/qr-encode.ts +420 -0
  371. package/src/rhf/fields.tsx +1054 -0
  372. package/src/rhf/line-items.tsx +130 -0
  373. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  374. package/src/rhf.ts +8 -0
  375. package/src/shell/app-shell.tsx +147 -2
  376. package/src/shell/auth-layout.tsx +190 -0
  377. package/src/shell/top-bar-brand.tsx +76 -0
  378. package/src/shell/topbar-action-menu.tsx +260 -49
  379. package/src/shell.ts +2 -0
  380. package/src/wizard/wizard-context.tsx +50 -1
  381. package/src/wizard/wizard-step.tsx +43 -6
  382. package/tokens.css +12 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * {@link LineItems} over react-hook-form's `useFieldArray`. `@eifi1/ui-kit/rhf`.
3
+ *
4
+ * ```tsx
5
+ * <RhfLineItems
6
+ * name="components"
7
+ * newItem={() => ({ type: "net_rent", amount: null, validFrom: "" })}
8
+ * minItems={1}
9
+ * columns={[
10
+ * { key: "type", header: "Type", render: ({ name, label }) => (
11
+ * <RhfSelect name={`${name}.type`} options={TYPES} aria-label={label} />
12
+ * ) },
13
+ * { key: "amount", header: "Amount", width: "8rem", align: "end", render: ({ name, label }) => (
14
+ * <RhfMoneyField name={`${name}.amount`} ariaLabel={label} />
15
+ * ) },
16
+ * ]}
17
+ * totals={{ amount: formatMoney(total) }}
18
+ * />
19
+ * ```
20
+ *
21
+ * The rows are the array's `fields` (keyed by their `id`, as react-hook-form asks),
22
+ * Add appends `newItem()`, remove is `remove(index)`, and an error on the array
23
+ * itself — the `rules` here, or a resolver's `lines: "At least two lines"` — is the
24
+ * list's `error`. Each cell's `render` gets `name`, the row's path
25
+ * (`"components.2"`), to bind its field to.
26
+ */
27
+ import type { ReactNode } from "react";
28
+ import {
29
+ useFieldArray,
30
+ useFormState,
31
+ type FieldArray,
32
+ type FieldArrayPath,
33
+ type FieldArrayWithId,
34
+ type FieldValues,
35
+ type UseFieldArrayProps,
36
+ } from "react-hook-form";
37
+ import {
38
+ LineItems,
39
+ type LineItemCellContext,
40
+ type LineItemsColumn,
41
+ type LineItemsProps,
42
+ } from "../components/line-items";
43
+
44
+ /** A cell's context, with the row's path in the form. */
45
+ export interface RhfLineItemCellContext<TItem, TArrayName extends string>
46
+ extends LineItemCellContext<TItem> {
47
+ /** The row's path: `${name}.${index}` — bind the cell's field to
48
+ * `` `${name}.amount` ``. */
49
+ name: `${TArrayName}.${number}`;
50
+ }
51
+
52
+ export interface RhfLineItemsColumn<TItem, TArrayName extends string>
53
+ extends Omit<LineItemsColumn<TItem>, "render"> {
54
+ render: (ctx: RhfLineItemCellContext<TItem, TArrayName>) => ReactNode;
55
+ }
56
+
57
+ export interface RhfLineItemsProps<
58
+ TFieldValues extends FieldValues = FieldValues,
59
+ TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,
60
+ TTransformed = TFieldValues,
61
+ > extends Omit<
62
+ LineItemsProps<FieldArrayWithId<TFieldValues, TArrayName, "id">>,
63
+ "items" | "columns" | "onAdd" | "onRemove" | "getKey"
64
+ > {
65
+ name: TArrayName;
66
+ /** `form.control`. Optional under `<Form {...form}>`. */
67
+ control?: UseFieldArrayProps<TFieldValues, TArrayName, "id", TTransformed>["control"];
68
+ /** `useFieldArray`'s `rules` (`minLength`, `validate` …); its message is the list's
69
+ * error. */
70
+ rules?: UseFieldArrayProps<TFieldValues, TArrayName, "id", TTransformed>["rules"];
71
+ columns: readonly RhfLineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, "id">, TArrayName>[];
72
+ /** A new row's values. Omitted: no add button. */
73
+ newItem?: () => FieldArray<TFieldValues, TArrayName>;
74
+ /** `false`: no remove buttons. Default `true`. */
75
+ removable?: boolean;
76
+ }
77
+
78
+ /** The message of an error at `path` in react-hook-form's errors tree: the array's
79
+ * own (`root`, from `rules`) or one a resolver put on the array. */
80
+ function arrayError(errors: unknown, path: string): string | undefined {
81
+ let node: unknown = errors;
82
+ for (const key of path.split(".")) {
83
+ if (node === null || typeof node !== "object") return undefined;
84
+ node = (node as Record<string, unknown>)[key];
85
+ }
86
+ if (node === null || typeof node !== "object") return undefined;
87
+ const { root, message } = node as { root?: { message?: unknown }; message?: unknown };
88
+ const text = root?.message ?? message;
89
+ return typeof text === "string" && text !== "" ? text : undefined;
90
+ }
91
+
92
+ /** {@link LineItems} bound to a field array. See the module note. */
93
+ export function RhfLineItems<
94
+ TFieldValues extends FieldValues = FieldValues,
95
+ TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,
96
+ TTransformed = TFieldValues,
97
+ >({
98
+ name,
99
+ control,
100
+ rules,
101
+ columns,
102
+ newItem,
103
+ removable = true,
104
+ error,
105
+ ...rest
106
+ }: RhfLineItemsProps<TFieldValues, TArrayName, TTransformed>) {
107
+ const { fields, append, remove } = useFieldArray<TFieldValues, TArrayName, "id", TTransformed>({
108
+ control,
109
+ name,
110
+ rules,
111
+ });
112
+ const { errors } = useFormState({ control, name: name as never });
113
+ const bound: LineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, "id">>[] = columns.map(
114
+ ({ render, ...column }) => ({
115
+ ...column,
116
+ render: (ctx) => render({ ...ctx, name: `${name}.${ctx.index}` as `${TArrayName}.${number}` }),
117
+ }),
118
+ );
119
+ return (
120
+ <LineItems
121
+ {...rest}
122
+ items={fields}
123
+ columns={bound}
124
+ getKey={(field) => field.id}
125
+ onAdd={newItem ? () => append(newItem()) : undefined}
126
+ onRemove={removable ? (index) => remove(index) : undefined}
127
+ error={error ?? arrayError(errors, name)}
128
+ />
129
+ );
130
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The bridge between a react-hook-form step and the wizard's Next gate.
3
+ * `@eifi1/ui-kit/rhf`.
4
+ *
5
+ * ```tsx
6
+ * function AddressStep({ onUpdate }: { onUpdate: (v: Address) => void }) {
7
+ * const form = useForm<Address>({ defaultValues });
8
+ * useRhfWizardStep(form, { onValid: onUpdate });
9
+ * return <Form {...form}>…</Form>;
10
+ * }
11
+ * ```
12
+ *
13
+ * Inside a `<StepperNav>` the hook registers the step's validator with the wizard, so
14
+ * Next (and Finish on a form-bearing last step) runs it: on success the values go to
15
+ * `onValid` and the wizard advances; on failure react-hook-form shows its own inline
16
+ * `<FormMessage>`s, focuses the first field in error, and the wizard stays put — and,
17
+ * since the result carries no per-field errors, says its "fill in the required
18
+ * fields" line so the button did not seem to do nothing. It also RETURNS the
19
+ * validator, for a nav of your own outside `<StepperNav>` (where there is no wizard
20
+ * context to register with, so nothing is registered).
21
+ *
22
+ * Two modes:
23
+ *
24
+ * - **The whole form** (no `fields`): `form.handleSubmit`, exactly as kastlan's
25
+ * `use-rhf-wizard-step.ts` — every rule and the resolver run, `onValid` receives
26
+ * the TRANSFORMED values (a zod `.transform()` included), and focus follows the
27
+ * form's own `shouldFocusError` (on by default).
28
+ * - **Some fields** (`fields: ["iban", "bic"]`): `form.trigger(fields)`, for a wizard
29
+ * that keeps one form across several steps, each of which validates only what it
30
+ * shows. The first of those fields in error is focused (`shouldFocus`; the control
31
+ * has to take `field.ref`), errors on fields of later steps are not raised, and
32
+ * `onValid` receives `form.getValues()` — the untransformed values, because a
33
+ * resolver's output is only produced for the whole form.
34
+ *
35
+ * Both callbacks are read through refs, so the validator is registered once per form
36
+ * however the caller writes them (an inline `(v) => onUpdate(v)` is the usual case).
37
+ *
38
+ * The positional `useRhfWizardStep(form, onValid)` of the hook kastlan kept is the
39
+ * whole-form mode, and still accepted.
40
+ *
41
+ * Why it lives here and not in `@eifi1/ui-kit/wizard`: it imports react-hook-form,
42
+ * which only this entry may (see packaging-contract).
43
+ */
44
+ import { useCallback, useEffect, useRef } from "react";
45
+ import type { FieldPath, FieldValues, UseFormReturn } from "react-hook-form";
46
+ import { useOptionalWizardContext } from "../wizard/wizard-context";
47
+
48
+ export type RhfWizardStepOptions<
49
+ TFieldValues extends FieldValues,
50
+ TTransformed = TFieldValues,
51
+ > =
52
+ | {
53
+ /** Validate only these fields — the step's share of a form that spans steps. */
54
+ fields: readonly FieldPath<TFieldValues>[];
55
+ /** Called with `form.getValues()` when the fields pass, before the wizard
56
+ * advances. */
57
+ onValid?: (values: TFieldValues) => void;
58
+ }
59
+ | {
60
+ /** Omitted: validate the whole form through `handleSubmit`. */
61
+ fields?: undefined;
62
+ /** Called with the form's (transformed) values when it passes, before the
63
+ * wizard advances. */
64
+ onValid?: (values: TTransformed) => void;
65
+ };
66
+
67
+ /**
68
+ * Connects `form` to the wizard's step validation and returns the step's validator.
69
+ * See the module note for the two modes.
70
+ */
71
+ export function useRhfWizardStep<
72
+ TFieldValues extends FieldValues,
73
+ TContext = unknown,
74
+ TTransformed = TFieldValues,
75
+ >(
76
+ form: UseFormReturn<TFieldValues, TContext, TTransformed>,
77
+ options?:
78
+ | RhfWizardStepOptions<TFieldValues, TTransformed>
79
+ | ((values: TTransformed) => void),
80
+ ): () => Promise<boolean> {
81
+ const opts = typeof options === "function" ? { onValid: options } : (options ?? {});
82
+ const optsRef = useRef(opts);
83
+ useEffect(() => {
84
+ optsRef.current = opts;
85
+ });
86
+
87
+ const validate = useCallback(async (): Promise<boolean> => {
88
+ const current = optsRef.current;
89
+ if (current.fields) {
90
+ const ok = await form.trigger([...current.fields], { shouldFocus: true });
91
+ if (ok) current.onValid?.(form.getValues());
92
+ return ok;
93
+ }
94
+ const onValid = current.onValid as ((values: TTransformed) => void) | undefined;
95
+ let ok = false;
96
+ await form.handleSubmit(
97
+ (values) => {
98
+ ok = true;
99
+ onValid?.(values);
100
+ },
101
+ () => {
102
+ ok = false;
103
+ },
104
+ )();
105
+ return ok;
106
+ }, [form]);
107
+
108
+ const wizard = useOptionalWizardContext();
109
+ const register = wizard?.registerStepValidate;
110
+ useEffect(() => register?.(validate), [register, validate]);
111
+
112
+ return validate;
113
+ }
package/src/rhf.ts CHANGED
@@ -7,3 +7,11 @@
7
7
  // the barrel dropped its last react-hook-form import (see the wizard section of
8
8
  // src/index.ts). The packaging contract test holds that line.
9
9
  export * from "./rhf/form";
10
+ // The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).
11
+ export * from "./rhf/use-rhf-wizard-step";
12
+ // Bound fields — one line per field (RhfTextField, RhfNumberField, RhfMoneyField,
13
+ // RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox)
14
+ // and RhfField, the shell they are built on.
15
+ export * from "./rhf/fields";
16
+ // LineItems over useFieldArray.
17
+ export * from "./rhf/line-items";
@@ -1,17 +1,19 @@
1
1
  import { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
2
2
  import type { ComponentPropsWithoutRef, ReactNode } from "react";
3
3
  import { createPortal } from "react-dom";
4
- import { NavLink, matchPath, useLocation } from "react-router";
4
+ import { Link, NavLink, matchPath, useLocation } from "react-router";
5
5
  import { ChevronRight, PanelLeftClose, PanelLeftOpen } from "lucide-react";
6
6
  import type { LucideIcon } from "lucide-react";
7
7
  import { cn } from "../lib/cn";
8
8
  import { dirOf, type Direction } from "../lib/direction";
9
9
  import { readStored, writeStored } from "../lib/safe-storage";
10
10
  import { Tooltip } from "../components/tooltip";
11
+ import { pickLinkRenderer } from "../components/text-link";
11
12
  import { useAnchoredRect } from "../hooks/use-anchored-rect";
12
13
  import { useEscapeKey } from "../hooks/use-dismiss";
13
14
  import { useMediaQuery } from "../hooks/use-media-query";
14
- import { DEFAULT_APP_SHELL_LABELS, useKitLabels } from "../i18n/kit-labels";
15
+ import { DEFAULT_APP_SHELL_LABELS, useKitLabels, useKitLink } from "../i18n/kit-labels";
16
+ import type { KitLinkComponent, KitLinkProps } from "../i18n/kit-labels";
15
17
 
16
18
  export interface AppShellSubItem {
17
19
  to: string;
@@ -48,6 +50,44 @@ export interface AppShellNavItem {
48
50
  items?: AppShellSubItem[];
49
51
  }
50
52
 
53
+ /**
54
+ * One entry of the sidebar's bottom area ({@link AppShellProps.sidebarFooterItems}) —
55
+ * the version link and the legal links kastlan hand-built in `sidebarFooter`.
56
+ *
57
+ * - `link` (default): one link. Expanded it shows `icon` + `text` (default `label`),
58
+ * with `hint` as a tooltip if given; collapsed it is the icon alone, named and
59
+ * tooltipped by `label`.
60
+ * - `links`: a small group — the legal links. Expanded, a `<nav>` named `label` holding
61
+ * the links as a wrapping row of small text; collapsed, ONE icon link to `to` (default
62
+ * the first link's), named and tooltipped by `label`, since a column of icons for
63
+ * "Imprint", "Privacy" and "Terms" would say nothing an icon can.
64
+ */
65
+ export type AppShellSidebarFooterItem =
66
+ | {
67
+ kind?: "link";
68
+ key: string;
69
+ to: string;
70
+ icon: LucideIcon;
71
+ /** The link's name, and the tooltip when collapsed ("What's new · v1.4.0"). */
72
+ label: string;
73
+ /** Visible text when expanded ("Kastlan v1.4.0"). Default `label`. */
74
+ text?: ReactNode;
75
+ /** A tooltip on the expanded link ("What's new"). */
76
+ hint?: string;
77
+ /** Leaves the app: a plain `<a target="_blank" rel="noopener noreferrer">`. */
78
+ external?: boolean;
79
+ }
80
+ | {
81
+ kind: "links";
82
+ key: string;
83
+ icon: LucideIcon;
84
+ /** The group's name — its `<nav>` label, and the collapsed icon's name ("Legal"). */
85
+ label: string;
86
+ /** The collapsed icon's target. Default the first link's `to`. */
87
+ to?: string;
88
+ links: { to: string; label: string; external?: boolean }[];
89
+ };
90
+
51
91
  /**
52
92
  * Exported and `<div>`-shaped. This is the outermost element of every consuming app, so
53
93
  * there is nothing above it to hang an id, a landmark label or a `data-tour` anchor on —
@@ -66,6 +106,13 @@ export interface AppShellProps extends ComponentPropsWithoutRef<"div"> {
66
106
  /** Extra sidebar content above the collapse toggle (e.g. a version link).
67
107
  * Receives the collapsed state so it can render compact vs. full. */
68
108
  sidebarFooter?: (collapsed: boolean) => ReactNode;
109
+ /** The sidebar's bottom area as data — a version link, the legal links — drawn full
110
+ * when expanded and as icon + tooltip when collapsed. Rendered above
111
+ * `sidebarFooter`, which stays for anything the data cannot describe. */
112
+ sidebarFooterItems?: AppShellSidebarFooterItem[];
113
+ /** The router link for `sidebarFooterItems`. Default: the provider's
114
+ * `linkComponent`, else react-router's `Link`. */
115
+ renderLink?: KitLinkComponent;
69
116
  /** localStorage key for the persisted collapse state. */
70
117
  collapseStorageKey?: string;
71
118
  /** Default: `appShell.collapse` from the {@link UiKitProvider}, else English. */
@@ -201,6 +248,8 @@ export function AppShell({
201
248
  children,
202
249
  footer,
203
250
  sidebarFooter,
251
+ sidebarFooterItems,
252
+ renderLink,
204
253
  collapseStorageKey: collapseStorageKeyProp,
205
254
  collapseLabel,
206
255
  expandLabel,
@@ -296,6 +345,9 @@ export function AppShell({
296
345
  ))}
297
346
  </div>
298
347
  </nav>
348
+ {!!sidebarFooterItems?.length && (
349
+ <SidebarFooterItems items={sidebarFooterItems} collapsed={collapsed} renderLink={renderLink} />
350
+ )}
299
351
  {sidebarFooter?.(collapsed)}
300
352
  <div className="border-t border-[var(--border)] p-2">
301
353
  {collapsed ? (
@@ -397,6 +449,99 @@ export function AppShell({
397
449
  );
398
450
  }
399
451
 
452
+ const routerLink: KitLinkComponent = ({ href, ...p }) => <Link to={href} {...p} />;
453
+ const plainLink: KitLinkComponent = ({ children, ...p }) => <a {...p}>{children}</a>;
454
+
455
+ const FOOTER_ICON_LINK =
456
+ "flex min-h-8 items-center justify-center rounded-md text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]";
457
+
458
+ /** Calls the link renderer as a component, so a router link's hooks are its own. */
459
+ function RenderedLink({ render, ...props }: KitLinkProps & { render: KitLinkComponent }) {
460
+ return render(props);
461
+ }
462
+
463
+ /** The sidebar's bottom area, from {@link AppShellSidebarFooterItem}s. */
464
+ function SidebarFooterItems({
465
+ items,
466
+ collapsed,
467
+ renderLink,
468
+ }: {
469
+ items: AppShellSidebarFooterItem[];
470
+ collapsed: boolean;
471
+ renderLink?: KitLinkComponent;
472
+ }) {
473
+ const kitLink = useKitLink();
474
+ const linkTo = (to: string, external: boolean | undefined, { children, ...props }: Omit<KitLinkProps, "href">) => {
475
+ // The kit's one link rule (`pickLinkRenderer`): an external `to` or an in-page
476
+ // `#anchor` is a plain `<a>` even without `external`; only `external` opens a tab.
477
+ const InApp = pickLinkRenderer(renderLink, kitLink ?? routerLink, to) ?? plainLink;
478
+ return external ? (
479
+ <a href={to} target="_blank" rel="noopener noreferrer" {...props}>
480
+ {children}
481
+ </a>
482
+ ) : (
483
+ <RenderedLink render={InApp} href={to} {...props}>
484
+ {children}
485
+ </RenderedLink>
486
+ );
487
+ };
488
+
489
+ return (
490
+ <div data-slot="sidebar-footer-items" className={cn("px-2 py-1", collapsed ? "space-y-1" : "space-y-1.5")}>
491
+ {items.map((item) => {
492
+ if (collapsed) {
493
+ const to = item.kind === "links" ? (item.to ?? item.links[0]?.to) : item.to;
494
+ if (!to) return null;
495
+ const external = item.kind === "links" ? item.links.find((l) => l.to === to)?.external : item.external;
496
+ return (
497
+ <Tooltip key={item.key} label={item.label} side="end" portal className="block">
498
+ {linkTo(to, external, {
499
+ "aria-label": item.label,
500
+ className: FOOTER_ICON_LINK,
501
+ children: <item.icon className="size-4" aria-hidden />,
502
+ })}
503
+ </Tooltip>
504
+ );
505
+ }
506
+ if (item.kind === "links") {
507
+ return (
508
+ <nav
509
+ key={item.key}
510
+ aria-label={item.label}
511
+ className="flex flex-wrap justify-center gap-x-3 gap-y-0.5 text-[11px] text-[var(--text-muted)]"
512
+ >
513
+ {item.links.map((l) => (
514
+ <span key={l.to}>
515
+ {linkTo(l.to, l.external, {
516
+ className: "rounded-sm hover:text-[var(--text-secondary)] hover:underline",
517
+ children: l.label,
518
+ })}
519
+ </span>
520
+ ))}
521
+ </nav>
522
+ );
523
+ }
524
+ const link = linkTo(item.to, item.external, {
525
+ className: cn(FOOTER_ICON_LINK, "gap-1.5 text-xs font-medium"),
526
+ children: (
527
+ <>
528
+ <item.icon className="size-3.5 shrink-0" aria-hidden />
529
+ <span className="min-w-0 truncate">{item.text ?? item.label}</span>
530
+ </>
531
+ ),
532
+ });
533
+ return item.hint ? (
534
+ <Tooltip key={item.key} label={item.hint} side="top" portal className="block">
535
+ {link}
536
+ </Tooltip>
537
+ ) : (
538
+ <div key={item.key}>{link}</div>
539
+ );
540
+ })}
541
+ </div>
542
+ );
543
+ }
544
+
400
545
  /** One cell of the phone's group bar. Marked while the group's own page OR any of its
401
546
  * pages is current — NavLink alone only knows its own `to`, which left the bar with
402
547
  * nothing marked on every sub-page (the same defect the sidebar had). */
@@ -0,0 +1,190 @@
1
+ import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+ import { Card } from "../components/ui";
4
+
5
+ /**
6
+ * `extends ComponentPropsWithoutRef<"div">` because this is the page's outermost element
7
+ * — the one that owns `min-h-dvh` and the safe-area padding — so there is nothing a
8
+ * consumer could wrap it in to add an id or a `data-*` hook without breaking that.
9
+ */
10
+ export interface AuthLayoutProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
11
+ /** The brand mark (an SVG), drawn in a brand-coloured tile above the title. */
12
+ logo?: ReactNode;
13
+ /** `false` draws `logo` as it is, without the tile — for a full-colour logo image. */
14
+ logoTile?: boolean;
15
+ /** The page's `<h1>` (see `headingAs`): the product name on sign-in, "Verify email",
16
+ * "Imprint". */
17
+ title?: ReactNode;
18
+ /** The muted line under the title. */
19
+ description?: ReactNode;
20
+ /** The card's content — the form, the status, the legal text. */
21
+ children: ReactNode;
22
+ /**
23
+ * Above the header, outside the card — the legal pages' "← Back to sign in" link, or
24
+ * a notice banner. Start-aligned in both widths.
25
+ */
26
+ back?: ReactNode;
27
+ /** The top-end corner: a `LanguageMenu`. */
28
+ languageMenu?: ReactNode;
29
+ /** The top-end corner, before the language menu: a `ThemeToggle`. */
30
+ themeToggle?: ReactNode;
31
+ /** Below the card, centred — the legal links. */
32
+ footer?: ReactNode;
33
+ /** Wraps the footer in a `<nav>` with this name ("Legal"). Without it, a plain block. */
34
+ footerLabel?: string;
35
+ /**
36
+ * - `"narrow"` (default) — a `max-w-sm` card centred on the page, header centred:
37
+ * sign-in, register, verify email, two-factor.
38
+ * - `"wide"` — a `max-w-3xl` column from the top of the page, header start-aligned:
39
+ * running text (imprint, privacy, terms), which reads badly centred.
40
+ */
41
+ width?: "narrow" | "wide";
42
+ /** `false` lays the header and content on the page itself, without the card's
43
+ * surface and border — the legal pages. Default true. */
44
+ card?: boolean;
45
+ /** Extra classes for the card (or, with `card={false}`, the content column). */
46
+ cardClassName?: string;
47
+ /**
48
+ * `false` draws the content column and the footer as plain `<div>`s instead of
49
+ * `<main>` and `<footer>` — for an AuthLayout embedded in a page that already has its
50
+ * landmarks (a preview, a docs page, a dialog), where a second `<main>` would nest
51
+ * one inside the other. Default `true`: on its own route it IS the page.
52
+ */
53
+ landmark?: boolean;
54
+ /** The title's element. Default `h1` — the page's heading; an embedded layout takes
55
+ * the level of where it sits (`h2`, `h3`). */
56
+ headingAs?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
57
+ }
58
+
59
+ /** Keeps the page clear of a notch, the rounded corners and the home indicator on a
60
+ * phone in `viewport-fit=cover`; 1rem where the platform reports no inset. Physical
61
+ * left/right, because that is what `env()` reports. */
62
+ const SAFE_AREA: CSSProperties = {
63
+ paddingTop: "max(1rem, env(safe-area-inset-top, 0px))",
64
+ paddingBottom: "max(1rem, env(safe-area-inset-bottom, 0px))",
65
+ paddingLeft: "max(1rem, env(safe-area-inset-left, 0px))",
66
+ paddingRight: "max(1rem, env(safe-area-inset-right, 0px))",
67
+ };
68
+
69
+ const FOOTER_CLASS =
70
+ "flex flex-wrap justify-center gap-x-4 gap-y-1 text-xs text-[var(--text-muted)] [&_a:hover]:text-[var(--text-primary)] [&_a:hover]:underline [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-2 [&_a:focus-visible]:ring-[var(--brand)]";
71
+
72
+ /**
73
+ * The public page frame outside the app shell: sign-in, registration, email
74
+ * verification, two-factor — and, `width="wide"`, the legal pages.
75
+ *
76
+ * kastlan's login, register, verify-email and two-factor pages each repeat the same
77
+ * `min-h-screen` centring, the absolutely placed language switcher, the brand tile, the
78
+ * centred title + subtitle and an absolutely placed legal footer. Absolutely placed,
79
+ * the footer and the switcher sat ON the card on a short phone screen (a tall register
80
+ * form, or the software keyboard up); here they are rows of a column that grows with
81
+ * its content, so the page scrolls instead of overlapping.
82
+ *
83
+ * The corner controls, the `<main>` and the footer are in reading order: a screen
84
+ * reader meets the language switch first, which is where someone who cannot read the
85
+ * page's language needs it.
86
+ */
87
+ export function AuthLayout({
88
+ logo,
89
+ logoTile = true,
90
+ title,
91
+ description,
92
+ children,
93
+ back,
94
+ languageMenu,
95
+ themeToggle,
96
+ footer,
97
+ footerLabel,
98
+ width = "narrow",
99
+ card = true,
100
+ cardClassName,
101
+ landmark = true,
102
+ headingAs: Heading = "h1",
103
+ className,
104
+ style,
105
+ ...rest
106
+ }: AuthLayoutProps) {
107
+ const Main = landmark ? "main" : "div";
108
+ const Footer = landmark ? "footer" : "div";
109
+ const wide = width === "wide";
110
+ const hasCorner = languageMenu != null || themeToggle != null;
111
+ const hasHeader = logo != null || title != null || description != null;
112
+
113
+ const header = hasHeader && (
114
+ <div
115
+ data-slot="auth-layout-header"
116
+ className={cn("flex flex-col gap-1.5", wide ? "items-start text-start" : "items-center text-center")}
117
+ >
118
+ {logo != null &&
119
+ (logoTile ? (
120
+ <div className="mb-2 flex size-12 items-center justify-center rounded-lg bg-[var(--brand)] text-[var(--brand-contrast)] [&_svg]:size-6">
121
+ {logo}
122
+ </div>
123
+ ) : (
124
+ <div className="mb-2">{logo}</div>
125
+ ))}
126
+ {title != null && (
127
+ <Heading
128
+ className={cn(
129
+ "font-semibold leading-tight text-[var(--text-primary)]",
130
+ wide ? "text-2xl md:text-3xl" : "text-2xl",
131
+ )}
132
+ >
133
+ {title}
134
+ </Heading>
135
+ )}
136
+ {description != null && <p className="text-sm text-[var(--text-muted)]">{description}</p>}
137
+ </div>
138
+ );
139
+
140
+ const body = (
141
+ <>
142
+ {header}
143
+ <div className={cn(hasHeader && (wide ? "mt-6" : "mt-5"))}>{children}</div>
144
+ </>
145
+ );
146
+
147
+ return (
148
+ <div
149
+ {...rest}
150
+ data-auth-layout={width}
151
+ style={{ ...SAFE_AREA, ...style }}
152
+ className={cn("flex min-h-dvh flex-col bg-[var(--bg-page)] text-[var(--text-primary)]", className)}
153
+ >
154
+ {hasCorner && (
155
+ <div data-slot="auth-layout-corner" className="flex items-center justify-end gap-1">
156
+ {themeToggle}
157
+ {languageMenu}
158
+ </div>
159
+ )}
160
+ <Main
161
+ data-slot="auth-layout-main"
162
+ className={cn(
163
+ // `relative` contains any `sr-only` child (see AppShell's <main>).
164
+ "relative flex w-full flex-1 flex-col",
165
+ wide ? "py-6 md:py-10" : "items-center justify-center py-8",
166
+ )}
167
+ >
168
+ <div className={cn("mx-auto w-full", wide ? "max-w-3xl" : "max-w-sm")}>
169
+ {back != null && <div className="mb-4">{back}</div>}
170
+ {card ? (
171
+ <Card className={cn(wide ? "p-6 md:p-8" : "p-6", cardClassName)}>{body}</Card>
172
+ ) : (
173
+ <div className={cardClassName}>{body}</div>
174
+ )}
175
+ </div>
176
+ </Main>
177
+ {footer != null && footer !== false && (
178
+ <Footer data-slot="auth-layout-footer" className="pt-4">
179
+ {footerLabel ? (
180
+ <nav aria-label={footerLabel} className={FOOTER_CLASS}>
181
+ {footer}
182
+ </nav>
183
+ ) : (
184
+ <div className={FOOTER_CLASS}>{footer}</div>
185
+ )}
186
+ </Footer>
187
+ )}
188
+ </div>
189
+ );
190
+ }