@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,76 @@
1
+ import type { ReactNode } from "react";
2
+ import { Link as RouterLink } from "react-router";
3
+ import { cn } from "../lib/cn";
4
+ import { useKitLink } from "../i18n/kit-labels";
5
+ import { pickLinkRenderer } from "../components/text-link";
6
+ import type { KitLinkComponent, KitLinkProps } from "../i18n/kit-labels";
7
+
8
+ const routerLink: KitLinkComponent = ({ href, ...p }) => <RouterLink to={href} {...p} />;
9
+ const plainLink: KitLinkComponent = ({ children, ...p }) => <a {...p}>{children}</a>;
10
+
11
+ /** Below which width only the logo shows. The name stays in the link's accessible
12
+ * name at every width. */
13
+ const COLLAPSE = {
14
+ sm: "sr-only sm:not-sr-only sm:truncate",
15
+ md: "sr-only md:not-sr-only md:truncate",
16
+ } as const;
17
+
18
+ export interface TopBarBrandProps extends Omit<KitLinkProps, "href" | "children"> {
19
+ /** The brand mark — an SVG, sized by the caller or `h-6 w-auto` by default. */
20
+ logo: ReactNode;
21
+ /** The product name beside the logo, and the link's accessible name. */
22
+ name: ReactNode;
23
+ /** Where the brand links to. Default `/`. */
24
+ to?: string;
25
+ /** Hide the name below `sm` (default) or `md`; `false` always shows it. It is
26
+ * visually hidden, not removed, so the link keeps its name on a phone. */
27
+ collapseBelow?: "sm" | "md" | false;
28
+ /** The app's router link for this one link. Default: the provider's
29
+ * `linkComponent`, else react-router's `Link`. */
30
+ renderLink?: KitLinkComponent;
31
+ }
32
+
33
+ /**
34
+ * The top bar's home link: logo + product name, collapsing to the logo on a phone.
35
+ * Pass it as {@link TopBar}'s `brand`.
36
+ *
37
+ * kastlan's (top-bar.tsx:66) hid nothing and truncated the name instead — on a phone
38
+ * with five actions that left "Ka…" beside the logo — and needed an `aria-label`
39
+ * because the name could be cut. Here the name is `sr-only` below the breakpoint, so
40
+ * the logo is the whole visible link there and the name is still what it is called.
41
+ */
42
+ export function TopBarBrand({
43
+ logo,
44
+ name,
45
+ to = "/",
46
+ collapseBelow = "sm",
47
+ renderLink,
48
+ className,
49
+ ...rest
50
+ }: TopBarBrandProps) {
51
+ const kitLink = useKitLink();
52
+ return (
53
+ <RenderedLink
54
+ // The kit's one link rule (`pickLinkRenderer`): an external `to` or an in-page
55
+ // `#anchor` is a plain `<a>`, never the router's link.
56
+ render={pickLinkRenderer(renderLink, kitLink ?? routerLink, to) ?? plainLink}
57
+ {...rest}
58
+ href={to}
59
+ className={cn(
60
+ // `relative` contains the sr-only name.
61
+ "relative flex min-w-0 items-center gap-2 rounded-md font-semibold text-[var(--text-primary)] hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
62
+ className,
63
+ )}
64
+ >
65
+ <span aria-hidden className="flex shrink-0 items-center [&_svg]:h-6 [&_svg]:w-auto">
66
+ {logo}
67
+ </span>
68
+ <span className={collapseBelow ? COLLAPSE[collapseBelow] : "truncate"}>{name}</span>
69
+ </RenderedLink>
70
+ );
71
+ }
72
+
73
+ /** Calls the link renderer as a component, so a router link's hooks are its own. */
74
+ function RenderedLink({ render, ...props }: KitLinkProps & { render: KitLinkComponent }) {
75
+ return render(props);
76
+ }
@@ -1,20 +1,118 @@
1
- import type { ReactNode } from "react";
1
+ import { useLayoutEffect, useRef } from "react";
2
+ import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
2
3
  import { Link } from "react-router";
3
4
  import { cn } from "../lib/cn";
4
5
  import { HoverMenu } from "../components/hover-menu";
5
- import { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from "./topbar-controls";
6
+ import { MenuItem } from "../components/menu-item";
7
+ import type { MenuItemTone } from "../components/menu-item";
8
+ import { StatusDot } from "../components/status-dot";
9
+ import type { UserAvatarBadge } from "../components/user-avatar";
10
+ import { TOPBAR_TRIGGER_CLASS } from "./topbar-controls";
6
11
 
7
12
  /**
8
13
  * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a
9
14
  * `link` navigates via react-router, and a `divider` draws a rule. Non-divider
10
15
  * rows close the menu automatically when chosen. `kind` may be omitted for
11
16
  * actions, keeping the common case terse.
17
+ *
18
+ * The rows are {@link MenuItem}s, so they take its options: `tone: "danger"` for the
19
+ * log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the
20
+ * page already open, `disabled`.
12
21
  */
13
22
  export type TopBarMenuEntry =
14
- | { kind?: "action"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; onSelect: () => void }
15
- | { kind: "link"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; to: string }
23
+ | {
24
+ kind?: "action";
25
+ key: string;
26
+ icon?: ReactNode;
27
+ label: ReactNode;
28
+ trailing?: ReactNode;
29
+ onSelect: () => void;
30
+ tone?: MenuItemTone;
31
+ /** Makes the row a choice (`menuitemradio`, or `menuitemcheckbox` with
32
+ * `checkable: "checkbox"`) and says whether it is chosen. */
33
+ checked?: boolean;
34
+ checkable?: "radio" | "checkbox";
35
+ disabled?: boolean;
36
+ }
37
+ | {
38
+ kind: "link";
39
+ key: string;
40
+ icon?: ReactNode;
41
+ label: ReactNode;
42
+ trailing?: ReactNode;
43
+ to: string;
44
+ tone?: MenuItemTone;
45
+ /** The link to the page already open: `aria-current="page"` and a check mark. */
46
+ current?: boolean;
47
+ disabled?: boolean;
48
+ }
16
49
  | { kind: "divider"; key: string };
17
50
 
51
+ /**
52
+ * The non-interactive identity block at the top of an account menu — kastlan's name +
53
+ * role, keksdose's display name + email + role chip. It is text, not a `menuitem`: the
54
+ * arrow keys pass over it.
55
+ */
56
+ export interface TopBarMenuHeader {
57
+ /** The first line — the user's name. */
58
+ title: ReactNode;
59
+ /** The muted second line — an email, a role. */
60
+ subtitle?: ReactNode;
61
+ /** Anything below — a role chip. A link or button here stays out of the arrow keys
62
+ * and in the Tab order, like the footer's. */
63
+ extra?: ReactNode;
64
+ }
65
+
66
+ /** What a control is, as far as `HoverMenu` is concerned, when it lives in the header
67
+ * or the footer: not a menu row. */
68
+ const SECTION_CONTROLS = "a[href]:not([role]),button:not([role])";
69
+ const TABBABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]';
70
+
71
+ /**
72
+ * A header or footer inside the menu panel.
73
+ *
74
+ * `HoverMenu` promotes EVERY role-less button and link in its panel to a `menuitem` out
75
+ * of the Tab order, and turns every list into `role="none"` — right for the rows, wrong
76
+ * for a footer of legal links, which would join the arrow-key cycle and drop out of Tab.
77
+ * So before it looks (a child's layout effect runs before its parent's), the controls
78
+ * in here get their own role spelled out, which `HoverMenu` leaves alone, and any list
79
+ * keeps its list semantics.
80
+ */
81
+ function MenuSection({ className, children }: { className: string; children: ReactNode }) {
82
+ const ref = useRef<HTMLLIElement>(null);
83
+ useLayoutEffect(() => {
84
+ const el = ref.current;
85
+ if (!el) return;
86
+ for (const c of el.querySelectorAll<HTMLElement>(SECTION_CONTROLS)) {
87
+ c.setAttribute("role", c.tagName === "A" ? "link" : "button");
88
+ }
89
+ for (const l of el.querySelectorAll<HTMLElement>("ul:not([role]),ol:not([role])")) l.setAttribute("role", "list");
90
+ for (const l of el.querySelectorAll<HTMLElement>("li:not([role])")) l.setAttribute("role", "listitem");
91
+ });
92
+ return (
93
+ <li ref={ref} role="none" className={className}>
94
+ {children}
95
+ </li>
96
+ );
97
+ }
98
+
99
+ /**
100
+ * `HoverMenu` closes on any Tab inside the panel — right when Tab leaves it, wrong when
101
+ * Tab is on its way from a row to a footer link, or from one footer link to the next:
102
+ * the panel would unmount under the focus. So a Tab whose next stop is still inside the
103
+ * panel stops here and the browser moves focus; one that leaves reaches `HoverMenu`,
104
+ * which closes.
105
+ */
106
+ function keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {
107
+ if (e.key !== "Tab") return;
108
+ const from = e.target as HTMLElement;
109
+ const stops = Array.from(e.currentTarget.querySelectorAll<HTMLElement>(TABBABLE)).filter(
110
+ (el) => el.getAttribute("tabindex") !== "-1",
111
+ );
112
+ const way = e.shiftKey ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING;
113
+ if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();
114
+ }
115
+
18
116
  /**
19
117
  * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,
20
118
  * with an optional uppercase heading and divider rules. Generic glue over the
@@ -22,70 +120,183 @@ export type TopBarMenuEntry =
22
120
  * so those call sites describe their rows as data instead of rebuilding the
23
121
  * trigger + list markup. For a checkmarked value-switcher use
24
122
  * {@link OptionSwitcherMenu} instead.
123
+ *
124
+ * It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a
125
+ * custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a
126
+ * free-content `footer` (the legal links).
25
127
  */
26
128
  export function TopBarActionMenu({
27
129
  icon,
130
+ iconBadge,
131
+ trigger,
132
+ triggerClassName,
28
133
  ariaLabel,
29
134
  heading,
135
+ header,
30
136
  entries,
137
+ children,
138
+ footer,
139
+ footerLabel,
140
+ align,
31
141
  panelClassName,
32
142
  }: {
33
- icon: ReactNode;
143
+ /** The trigger's face for the usual icon button. Ignored when `trigger` is given. */
144
+ icon?: ReactNode;
145
+ /**
146
+ * A status dot in the top-end corner of `icon` — keksdose's unread-support dot on the
147
+ * feedback trigger. Like `UserAvatar`'s `badge`, its `label` joins the trigger's name
148
+ * ("Send feedback 2 unread"), so the dot is not something only the eye is told about.
149
+ * Ignored with `trigger`: give the face its own badge there.
150
+ */
151
+ iconBadge?: UserAvatarBadge | null;
152
+ /**
153
+ * A face of your own for the trigger — the account menu's `UserAvatar`. It is drawn
154
+ * INSIDE the kit's button, which keeps the wiring: `aria-haspopup`, `aria-expanded`,
155
+ * the toggle, the keyboard. `open` is there for a chevron that turns.
156
+ *
157
+ * The button is named by `ariaLabel` followed by any readable text in the face — a
158
+ * `UserAvatar` `badge` label reaches a screen reader that way ("Account menu 3
159
+ * unread"), where an `aria-label` on the button would have hidden it. A
160
+ * `UserAvatar`'s initials are `aria-hidden`, so the person's name is only in the
161
+ * name if `ariaLabel` carries it ("Marcel Eifert, account menu").
162
+ */
163
+ trigger?: (state: { open: boolean }) => ReactNode;
164
+ /** Extra classes for the trigger button — `rounded-full` round an avatar. */
165
+ triggerClassName?: string;
166
+ /** Names the trigger and the menu. */
34
167
  ariaLabel: string;
35
168
  heading?: string;
169
+ /** A non-interactive identity block above the rows. */
170
+ header?: TopBarMenuHeader;
36
171
  entries: TopBarMenuEntry[];
172
+ /**
173
+ * Rows the entry data cannot describe (a `Disclosure` sub-list, an app's own
174
+ * section), after `entries`. Render `<li>`s holding `MenuItem`s or plain controls;
175
+ * `close` shuts the menu.
176
+ */
177
+ children?: (close: () => void) => ReactNode;
178
+ /**
179
+ * Free content below the rows, behind a rule — the legal links. Its links and
180
+ * buttons are NOT menu rows: the arrow keys skip them and Tab reaches them. Pass a
181
+ * function to get `close` for the links' `onClick`.
182
+ */
183
+ footer?: ReactNode | ((close: () => void) => ReactNode);
184
+ /** Wraps the footer in a `<nav>` with this name ("Legal"). Without it, a plain block. */
185
+ footerLabel?: string;
186
+ /** Which edge of the trigger the panel lines up with (see `HoverMenu`). Default `end`. */
187
+ align?: "start" | "end" | "left" | "right";
37
188
  panelClassName?: string;
38
189
  }) {
39
190
  return (
40
191
  <HoverMenu
41
192
  ariaLabel={ariaLabel}
42
- trigger={({ toggle }) => (
43
- <button type="button" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>
44
- {icon}
45
- </button>
46
- )}
193
+ align={align}
194
+ trigger={({ open, toggle }) =>
195
+ trigger ? (
196
+ <button type="button" onClick={toggle} className={cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName)}>
197
+ <span className="sr-only">{ariaLabel}</span>
198
+ {/* The space keeps the name from running into the face's text ("Account
199
+ menu3 unread"); as a flex item it takes no room. */}
200
+ {" "}
201
+ {trigger({ open })}
202
+ </button>
203
+ ) : iconBadge ? (
204
+ // Named by its content, like the custom-trigger branch, so the badge label is
205
+ // part of the name; an `aria-label` would have hidden it.
206
+ <button type="button" onClick={toggle} className={cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName)}>
207
+ <span className="sr-only">{ariaLabel}</span>{" "}
208
+ <span aria-hidden className="relative inline-flex">
209
+ {icon}
210
+ <StatusDot ring tone={iconBadge.tone ?? "danger"} className="absolute -end-0.5 -top-0.5" />
211
+ </span>{" "}
212
+ <span className="sr-only">{iconBadge.label}</span>
213
+ </button>
214
+ ) : (
215
+ <button
216
+ type="button"
217
+ onClick={toggle}
218
+ aria-label={ariaLabel}
219
+ className={cn(TOPBAR_TRIGGER_CLASS, triggerClassName)}
220
+ >
221
+ {icon}
222
+ </button>
223
+ )
224
+ }
47
225
  >
48
- {(close) => (
49
- <ul className={cn("py-1", panelClassName)}>
50
- {heading && (
51
- <li className="px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]">
52
- {heading}
53
- </li>
54
- )}
55
- {entries.map((entry) => {
56
- if (entry.kind === "divider") {
57
- return <li key={entry.key} className="my-1 border-t border-[var(--border)]" />;
58
- }
59
- const content = (
60
- <span className="flex w-full items-center gap-2">
61
- {entry.icon}
62
- {entry.label}
63
- {entry.trailing && <span className="ms-auto ps-2">{entry.trailing}</span>}
64
- </span>
65
- );
66
- return (
67
- <li key={entry.key}>
68
- {entry.kind === "link" ? (
69
- <Link to={entry.to} onClick={close} className={TOPBAR_MENU_ITEM_CLASS}>
70
- {content}
71
- </Link>
72
- ) : (
73
- <button
74
- type="button"
75
- onClick={() => {
76
- entry.onSelect();
77
- close();
78
- }}
79
- className={TOPBAR_MENU_ITEM_CLASS}
80
- >
81
- {content}
82
- </button>
226
+ {(close) => {
227
+ const footerBody = typeof footer === "function" ? footer(close) : footer;
228
+ const footerClass =
229
+ "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--text-muted)] [&_a:hover]:text-[var(--text-primary)] [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-2 [&_a:focus-visible]:ring-[var(--brand)]";
230
+ return (
231
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel
232
+ <ul className={cn("py-1", panelClassName)} onKeyDown={keepTabInsidePanel}>
233
+ {header && (
234
+ <MenuSection className="mb-1 border-b border-[var(--border)] px-3 py-2">
235
+ <div className="truncate text-sm font-medium text-[var(--text-primary)]">{header.title}</div>
236
+ {header.subtitle != null && (
237
+ <div className="truncate text-xs text-[var(--text-muted)]">{header.subtitle}</div>
83
238
  )}
239
+ {header.extra != null && <div className="mt-1">{header.extra}</div>}
240
+ </MenuSection>
241
+ )}
242
+ {heading && (
243
+ <li className="px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]">
244
+ {heading}
84
245
  </li>
85
- );
86
- })}
87
- </ul>
88
- )}
246
+ )}
247
+ {entries.map((entry) => {
248
+ if (entry.kind === "divider") {
249
+ return <li key={entry.key} className="my-1 border-t border-[var(--border)]" />;
250
+ }
251
+ return (
252
+ <li key={entry.key}>
253
+ {entry.kind === "link" ? (
254
+ <MenuItem
255
+ href={entry.to}
256
+ renderLink={({ href, ...p }) => <Link to={href} {...p} />}
257
+ onClick={close}
258
+ leading={entry.icon}
259
+ trailing={entry.trailing}
260
+ tone={entry.tone}
261
+ current={entry.current}
262
+ disabled={entry.disabled}
263
+ >
264
+ {entry.label}
265
+ </MenuItem>
266
+ ) : (
267
+ <MenuItem
268
+ onClick={() => {
269
+ entry.onSelect();
270
+ close();
271
+ }}
272
+ leading={entry.icon}
273
+ trailing={entry.trailing}
274
+ tone={entry.tone}
275
+ checked={entry.checked}
276
+ checkable={entry.checkable}
277
+ disabled={entry.disabled}
278
+ >
279
+ {entry.label}
280
+ </MenuItem>
281
+ )}
282
+ </li>
283
+ );
284
+ })}
285
+ {children?.(close)}
286
+ {footerBody != null && footerBody !== false && (
287
+ <MenuSection className="mt-1 border-t border-[var(--border)] px-3 py-2">
288
+ {footerLabel ? (
289
+ <nav aria-label={footerLabel} className={footerClass}>
290
+ {footerBody}
291
+ </nav>
292
+ ) : (
293
+ <div className={footerClass}>{footerBody}</div>
294
+ )}
295
+ </MenuSection>
296
+ )}
297
+ </ul>
298
+ );
299
+ }}
89
300
  </HoverMenu>
90
301
  );
91
302
  }
package/src/shell.ts CHANGED
@@ -8,3 +8,5 @@ export * from "./shell/app-shell";
8
8
  export * from "./shell/option-switcher-menu";
9
9
  export * from "./shell/role-switcher";
10
10
  export * from "./shell/topbar-action-menu";
11
+ export * from "./shell/top-bar-brand";
12
+ export * from "./shell/auth-layout";
@@ -1,4 +1,4 @@
1
- import { createContext, useContext } from "react";
1
+ import { createContext, useContext, useEffect, useRef } from "react";
2
2
  import type { ValidateResult } from "./types";
3
3
 
4
4
  export interface WizardContextValue {
@@ -27,3 +27,52 @@ export function useWizardContext(): WizardContextValue {
27
27
  }
28
28
  return ctx;
29
29
  }
30
+
31
+ /**
32
+ * The wizard context, or `null` outside a `<StepperNav>`. For a hook that registers
33
+ * with the wizard when there is one and works without it — `useRhfWizardStep` in
34
+ * `@eifi1/ui-kit/rhf`, whose returned `validate` a custom nav can call itself.
35
+ */
36
+ export function useOptionalWizardContext(): WizardContextValue | null {
37
+ return useContext(WizardContext);
38
+ }
39
+
40
+ /**
41
+ * Registers a plain validator for the mounted step — the counterpart to
42
+ * `useRhfWizardStep` (in `@eifi1/ui-kit/rhf`) for a step that gates on its own state
43
+ * (a hand-managed list of lines, a sum that has to balance) rather than a form.
44
+ *
45
+ * ```tsx
46
+ * useWizardStepValidate(() => ({ ok: lines.length >= 2, errors: { lines: t("…") } }));
47
+ * ```
48
+ *
49
+ * Next (and Finish) run it with the step's other validators; a `false` or a result
50
+ * with `ok: false` keeps the wizard where it is. `validate` is read through a ref, so
51
+ * an inline closure over the latest state registers exactly once. Throws outside a
52
+ * `<StepperNav>`, like {@link useWizardContext}: a validator with nothing to run it is
53
+ * a bug to hear about.
54
+ */
55
+ export function useWizardStepValidate(
56
+ validate: () => ValidateResult | Promise<ValidateResult>,
57
+ ): void {
58
+ const { registerStepValidate } = useWizardContext();
59
+ const ref = useRef(validate);
60
+ useEffect(() => {
61
+ ref.current = validate;
62
+ });
63
+ useEffect(() => registerStepValidate(() => ref.current()), [registerStepValidate]);
64
+ }
65
+
66
+ /**
67
+ * Disables the wizard's Next and Skip while `blocked` holds (tenant shares that do not
68
+ * add up to 100 %). Unlike {@link useWizardStepValidate}, which answers a click, this
69
+ * greys the button out before one. The gate is lifted again when the step unmounts, so
70
+ * the next step starts unblocked.
71
+ */
72
+ export function useWizardNextGate(blocked: boolean): void {
73
+ const { setNextBlocked } = useWizardContext();
74
+ useEffect(() => {
75
+ setNextBlocked(blocked);
76
+ return () => setNextBlocked(false);
77
+ }, [blocked, setNextBlocked]);
78
+ }
@@ -1,14 +1,51 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { cn } from "../lib/cn";
3
3
 
4
+ export interface WizardStepProps {
5
+ children: ReactNode;
6
+ className?: string;
7
+ /**
8
+ * The step's heading ("Choose a unit"), drawn in the one style every step shares.
9
+ * kastlan's steps each hand-wrote `<h3 className="text-lg font-semibold">` (about
10
+ * sixteen of them: lease-picker-step, lease-unit-step, …), and a hand-written
11
+ * heading is one that drifts — a size here, a colour there. Omitted: no heading.
12
+ */
13
+ title?: ReactNode;
14
+ /** The line under the title: what the step asks for, or why. Rendered only with a
15
+ * `title` or on its own when there is none. */
16
+ description?: ReactNode;
17
+ /** The heading's level. Default 3: the page has its `h1` and the wizard's own
18
+ * chrome sits at `h2` (the review step, the cancel dialog), so a step is under it. */
19
+ headingLevel?: 2 | 3 | 4;
20
+ }
21
+
22
+ const HEADING = { 2: "h2", 3: "h3", 4: "h4" } as const;
23
+
4
24
  /** The vertical rhythm one wizard step's content sits on. A component rather
5
- * than a class string so every step in every app spaces its fields the same. */
25
+ * than a class string so every step in every app spaces its fields the same —
26
+ * and, with `title` / `description`, heads them the same. */
6
27
  export function WizardStep({
7
28
  children,
8
29
  className,
9
- }: {
10
- children: ReactNode;
11
- className?: string;
12
- }) {
13
- return <div className={cn("space-y-6", className)}>{children}</div>;
30
+ title,
31
+ description,
32
+ headingLevel = 3,
33
+ }: WizardStepProps) {
34
+ const hasTitle = title !== undefined && title !== null && title !== false && title !== "";
35
+ const hasDescription =
36
+ description !== undefined && description !== null && description !== false && description !== "";
37
+ const Heading = HEADING[headingLevel];
38
+ return (
39
+ <div className={cn("space-y-6", className)}>
40
+ {(hasTitle || hasDescription) && (
41
+ <div data-slot="wizard-step-header" className="space-y-1">
42
+ {hasTitle && (
43
+ <Heading className="text-lg font-semibold text-[var(--text-primary)]">{title}</Heading>
44
+ )}
45
+ {hasDescription && <p className="text-sm text-[var(--text-muted)]">{description}</p>}
46
+ </div>
47
+ )}
48
+ {children}
49
+ </div>
50
+ );
14
51
  }
package/tokens.css CHANGED
@@ -233,6 +233,18 @@
233
233
  --success-bg: rgba(6, 78, 59, 0.25);
234
234
  }
235
235
 
236
+ /* ── Media ─────────────────────────────────────────────────────────────────
237
+ Controls drawn ON a photo or a live camera picture (the camera shutter), whose
238
+ background is the picture, not the page — so they must NOT follow the theme: a
239
+ dark-mode inverse pair would put a dark ring on a dark viewfinder. The same in both
240
+ themes, on purpose. */
241
+ :root,
242
+ .dark {
243
+ --media-ink: #ffffff;
244
+ --media-scrim: rgb(0 0 0 / 0.35);
245
+ --media-scrim-hover: rgb(0 0 0 / 0.5);
246
+ }
247
+
236
248
  /* ── Toasts ─────────────────────────────────────────────────────────────────
237
249
  The kit `<Toaster>` (components/toast.tsx) paints sonner with the AlertBanner
238
250
  tones. These are ALIASES, not new colours, and they exist for one reason: sonner