@eifi1/ui-kit 0.53.0 → 0.55.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 (902) hide show
  1. package/README.md +96 -82
  2. package/dist/account/delete-account-setting.d.ts +3 -2
  3. package/dist/account/delete-account-setting.js.map +1 -1
  4. package/dist/admin/plan-change.js +2 -2
  5. package/dist/admin/plan-change.js.map +1 -1
  6. package/dist/admin/user-roster.d.ts +6 -0
  7. package/dist/admin/user-roster.js +4 -0
  8. package/dist/admin/user-roster.js.map +1 -1
  9. package/dist/auth/auth-errors.d.ts +36 -0
  10. package/dist/auth/auth-errors.js +20 -0
  11. package/dist/auth/auth-errors.js.map +1 -1
  12. package/dist/billing/billing-labels.d.ts +26 -0
  13. package/dist/billing/billing-labels.js +9 -0
  14. package/dist/billing/billing-labels.js.map +1 -1
  15. package/dist/billing/subscription-standing.d.ts +61 -0
  16. package/dist/billing/subscription-standing.js +51 -0
  17. package/dist/billing/subscription-standing.js.map +1 -0
  18. package/dist/billing/usage-meter.d.ts +52 -0
  19. package/dist/billing/usage-meter.js +52 -0
  20. package/dist/billing/usage-meter.js.map +1 -0
  21. package/dist/chart.d.ts +1 -0
  22. package/dist/chart.js +4 -0
  23. package/dist/chart.js.map +1 -1
  24. package/dist/components/alert-banner.js +2 -3
  25. package/dist/components/alert-banner.js.map +1 -1
  26. package/dist/components/amount-input.d.ts +2 -1
  27. package/dist/components/amount-input.js +3 -15
  28. package/dist/components/amount-input.js.map +1 -1
  29. package/dist/components/autocomplete.js +2 -2
  30. package/dist/components/autocomplete.js.map +1 -1
  31. package/dist/components/bulk-action-bar.js +2 -1
  32. package/dist/components/bulk-action-bar.js.map +1 -1
  33. package/dist/components/button.d.ts +35 -1
  34. package/dist/components/button.js +5 -9
  35. package/dist/components/button.js.map +1 -1
  36. package/dist/components/calendar-heatmap.js +6 -4
  37. package/dist/components/calendar-heatmap.js.map +1 -1
  38. package/dist/components/camera-button.d.ts +89 -0
  39. package/dist/components/camera-button.js +122 -0
  40. package/dist/components/camera-button.js.map +1 -0
  41. package/dist/components/camera-capture.d.ts +66 -0
  42. package/dist/components/camera-capture.js +190 -0
  43. package/dist/components/camera-capture.js.map +1 -0
  44. package/dist/components/camera-labels.d.ts +20 -0
  45. package/dist/components/camera-labels.js +15 -0
  46. package/dist/components/camera-labels.js.map +1 -0
  47. package/dist/components/chart-edge.d.ts +9 -0
  48. package/dist/components/chart-edge.js +14 -3
  49. package/dist/components/chart-edge.js.map +1 -1
  50. package/dist/components/chart-zoom-core.js +34 -29
  51. package/dist/components/chart-zoom-core.js.map +1 -1
  52. package/dist/components/checkbox-group.js +1 -1
  53. package/dist/components/checkbox-group.js.map +1 -1
  54. package/dist/components/combobox-core.js +3 -2
  55. package/dist/components/combobox-core.js.map +1 -1
  56. package/dist/components/combobox.js +2 -2
  57. package/dist/components/combobox.js.map +1 -1
  58. package/dist/components/consent-again-notice-labels.d.ts +25 -0
  59. package/dist/components/consent-again-notice-labels.js +15 -0
  60. package/dist/components/consent-again-notice-labels.js.map +1 -0
  61. package/dist/components/consent-again-notice.d.ts +85 -0
  62. package/dist/components/consent-again-notice.js +94 -0
  63. package/dist/components/consent-again-notice.js.map +1 -0
  64. package/dist/components/country-select.js +2 -7
  65. package/dist/components/country-select.js.map +1 -1
  66. package/dist/components/currency-option-list.d.ts +2 -1
  67. package/dist/components/currency-option-list.js +10 -3
  68. package/dist/components/currency-option-list.js.map +1 -1
  69. package/dist/components/currency-select.d.ts +3 -1
  70. package/dist/components/currency-select.js +2 -2
  71. package/dist/components/currency-select.js.map +1 -1
  72. package/dist/components/danger-confirm.d.ts +22 -1
  73. package/dist/components/danger-confirm.js +52 -30
  74. package/dist/components/danger-confirm.js.map +1 -1
  75. package/dist/components/data-table-column-settings.js +2 -2
  76. package/dist/components/data-table-column-settings.js.map +1 -1
  77. package/dist/components/data-table-filter-popover.d.ts +2 -1
  78. package/dist/components/data-table-filter-popover.js +3 -2
  79. package/dist/components/data-table-filter-popover.js.map +1 -1
  80. package/dist/components/data-table-filters.d.ts +33 -3
  81. package/dist/components/data-table-filters.js +6 -4
  82. package/dist/components/data-table-filters.js.map +1 -1
  83. package/dist/components/data-table-fit.js.map +1 -1
  84. package/dist/components/data-table-virtual.js +13 -3
  85. package/dist/components/data-table-virtual.js.map +1 -1
  86. package/dist/components/date-field.js +2 -2
  87. package/dist/components/date-field.js.map +1 -1
  88. package/dist/components/date-picker.js +2 -2
  89. package/dist/components/date-picker.js.map +1 -1
  90. package/dist/components/date-range-picker.js +2 -6
  91. package/dist/components/date-range-picker.js.map +1 -1
  92. package/dist/components/dialog-frame.d.ts +5 -1
  93. package/dist/components/dialog-frame.js +6 -2
  94. package/dist/components/dialog-frame.js.map +1 -1
  95. package/dist/components/dropdown.js +7 -3
  96. package/dist/components/dropdown.js.map +1 -1
  97. package/dist/components/entity-combobox.js +2 -2
  98. package/dist/components/entity-combobox.js.map +1 -1
  99. package/dist/components/field-parts.d.ts +108 -6
  100. package/dist/components/field-parts.js +59 -6
  101. package/dist/components/field-parts.js.map +1 -1
  102. package/dist/components/field-suggestion-labels.d.ts +2 -0
  103. package/dist/components/field-suggestion-labels.js +10 -0
  104. package/dist/components/field-suggestion-labels.js.map +1 -0
  105. package/dist/components/field.js +1 -1
  106. package/dist/components/field.js.map +1 -1
  107. package/dist/components/file-button.d.ts +1 -1
  108. package/dist/components/file-button.js.map +1 -1
  109. package/dist/components/floating-panel.d.ts +7 -6
  110. package/dist/components/floating-panel.js +3 -17
  111. package/dist/components/floating-panel.js.map +1 -1
  112. package/dist/components/form-actions.d.ts +3 -2
  113. package/dist/components/form-actions.js.map +1 -1
  114. package/dist/components/hover-menu.js +3 -2
  115. package/dist/components/hover-menu.js.map +1 -1
  116. package/dist/components/icon-button.d.ts +23 -5
  117. package/dist/components/icon-button.js +6 -8
  118. package/dist/components/icon-button.js.map +1 -1
  119. package/dist/components/inline-confirm.js +2 -1
  120. package/dist/components/inline-confirm.js.map +1 -1
  121. package/dist/components/inline-entity-combobox.js +2 -2
  122. package/dist/components/inline-entity-combobox.js.map +1 -1
  123. package/dist/components/input.js +2 -2
  124. package/dist/components/input.js.map +1 -1
  125. package/dist/components/jump-calendar.js +3 -3
  126. package/dist/components/jump-calendar.js.map +1 -1
  127. package/dist/components/legal.d.ts +13 -1
  128. package/dist/components/legal.js +12 -0
  129. package/dist/components/legal.js.map +1 -1
  130. package/dist/components/legended-series-chart.d.ts +80 -0
  131. package/dist/components/legended-series-chart.js +70 -0
  132. package/dist/components/legended-series-chart.js.map +1 -0
  133. package/dist/components/lightbox.js +2 -1
  134. package/dist/components/lightbox.js.map +1 -1
  135. package/dist/components/mini-calendar.js +16 -7
  136. package/dist/components/mini-calendar.js.map +1 -1
  137. package/dist/components/modal.d.ts +48 -0
  138. package/dist/components/modal.js +26 -6
  139. package/dist/components/modal.js.map +1 -1
  140. package/dist/components/month-picker.d.ts +1 -1
  141. package/dist/components/month-picker.js +6 -6
  142. package/dist/components/month-picker.js.map +1 -1
  143. package/dist/components/multi-entity-combobox.js +2 -2
  144. package/dist/components/multi-entity-combobox.js.map +1 -1
  145. package/dist/components/multi-select.js +2 -6
  146. package/dist/components/multi-select.js.map +1 -1
  147. package/dist/components/number-input.js +2 -2
  148. package/dist/components/number-input.js.map +1 -1
  149. package/dist/components/page-header-layout.d.ts +18 -0
  150. package/dist/components/page-header-layout.js +23 -0
  151. package/dist/components/page-header-layout.js.map +1 -0
  152. package/dist/components/page-header-skeleton.d.ts +41 -0
  153. package/dist/components/page-header-skeleton.js +52 -0
  154. package/dist/components/page-header-skeleton.js.map +1 -0
  155. package/dist/components/page-header.js +4 -17
  156. package/dist/components/page-header.js.map +1 -1
  157. package/dist/components/popover.js +7 -2
  158. package/dist/components/popover.js.map +1 -1
  159. package/dist/components/release-notes-labels.d.ts +16 -0
  160. package/dist/components/release-notes-labels.js +18 -0
  161. package/dist/components/release-notes-labels.js.map +1 -0
  162. package/dist/components/release-notes.d.ts +65 -0
  163. package/dist/components/release-notes.js +65 -0
  164. package/dist/components/release-notes.js.map +1 -0
  165. package/dist/components/select.js +2 -2
  166. package/dist/components/select.js.map +1 -1
  167. package/dist/components/series-chart-labels.d.ts +9 -0
  168. package/dist/components/series-chart-labels.js +3 -1
  169. package/dist/components/series-chart-labels.js.map +1 -1
  170. package/dist/components/series-chart.d.ts +5 -0
  171. package/dist/components/series-chart.js +89 -36
  172. package/dist/components/series-chart.js.map +1 -1
  173. package/dist/components/stat-tile.d.ts +21 -2
  174. package/dist/components/stat-tile.js +4 -2
  175. package/dist/components/stat-tile.js.map +1 -1
  176. package/dist/components/toggle-group.js +3 -2
  177. package/dist/components/toggle-group.js.map +1 -1
  178. package/dist/components/treemap-layout.js.map +1 -1
  179. package/dist/components/treemap.d.ts +17 -15
  180. package/dist/components/treemap.js +167 -85
  181. package/dist/components/treemap.js.map +1 -1
  182. package/dist/currencies.d.ts +16 -2
  183. package/dist/currencies.js +2 -1
  184. package/dist/currencies.js.map +1 -1
  185. package/dist/eslint/index.js +1 -1
  186. package/dist/eslint/role-utilities.d.ts +2 -2
  187. package/dist/eslint/role-utilities.js.map +1 -1
  188. package/dist/feedback/feedback-context-labels.js +2 -1
  189. package/dist/feedback/feedback-context-labels.js.map +1 -1
  190. package/dist/feedback/feedback-context.d.ts +19 -3
  191. package/dist/feedback/feedback-context.js +12 -1
  192. package/dist/feedback/feedback-context.js.map +1 -1
  193. package/dist/feedback/feedback-diagnostics.d.ts +17 -0
  194. package/dist/feedback/feedback-diagnostics.js +27 -0
  195. package/dist/feedback/feedback-diagnostics.js.map +1 -0
  196. package/dist/feedback/feedback-dialog.d.ts +10 -0
  197. package/dist/feedback/feedback-dialog.js +6 -4
  198. package/dist/feedback/feedback-dialog.js.map +1 -1
  199. package/dist/feedback/feedback-inbox-page.d.ts +260 -0
  200. package/dist/feedback/feedback-inbox-page.js +207 -0
  201. package/dist/feedback/feedback-inbox-page.js.map +1 -0
  202. package/dist/feedback/feedback-record.d.ts +5 -0
  203. package/dist/feedback/feedback-record.js.map +1 -1
  204. package/dist/feedback/feedback-row-detail-labels.js +1 -0
  205. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  206. package/dist/feedback/feedback-row-detail.d.ts +3 -0
  207. package/dist/feedback/feedback-row-detail.js +3 -0
  208. package/dist/feedback/feedback-row-detail.js.map +1 -1
  209. package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
  210. package/dist/feedback/feedback-submit-dialog.js +15 -3
  211. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  212. package/dist/feedback/feedback-submit.d.ts +30 -2
  213. package/dist/feedback/feedback-submit.js +5 -2
  214. package/dist/feedback/feedback-submit.js.map +1 -1
  215. package/dist/feedback/feedback-vocabulary.js +2 -2
  216. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  217. package/dist/feedback.d.ts +1 -0
  218. package/dist/feedback.js +6 -0
  219. package/dist/feedback.js.map +1 -1
  220. package/dist/hooks/use-debounce.d.ts +32 -5
  221. package/dist/hooks/use-debounce.js +6 -5
  222. package/dist/hooks/use-debounce.js.map +1 -1
  223. package/dist/hooks/use-file-picker.d.ts +3 -1
  224. package/dist/hooks/use-file-picker.js.map +1 -1
  225. package/dist/hooks/use-focus-trap.d.ts +19 -3
  226. package/dist/hooks/use-focus-trap.js +15 -5
  227. package/dist/hooks/use-focus-trap.js.map +1 -1
  228. package/dist/hooks/use-has-camera.d.ts +36 -0
  229. package/dist/hooks/use-has-camera.js +53 -0
  230. package/dist/hooks/use-has-camera.js.map +1 -0
  231. package/dist/hooks/use-hotkey.js +2 -1
  232. package/dist/hooks/use-hotkey.js.map +1 -1
  233. package/dist/hooks/use-overlay-history.d.ts +6 -1
  234. package/dist/hooks/use-overlay-history.js +10 -2
  235. package/dist/hooks/use-overlay-history.js.map +1 -1
  236. package/dist/i18n/defaults.js +11 -1
  237. package/dist/i18n/defaults.js.map +1 -1
  238. package/dist/i18n/german.js +78 -4
  239. package/dist/i18n/german.js.map +1 -1
  240. package/dist/i18n/kit-labels.d.ts +20 -0
  241. package/dist/i18n/kit-labels.js.map +1 -1
  242. package/dist/i18n/locales/en.js +95 -5
  243. package/dist/i18n/locales/en.js.map +3 -3
  244. package/dist/i18n/locales/es.js +74 -4
  245. package/dist/i18n/locales/es.js.map +1 -1
  246. package/dist/i18n/locales/fr.js +75 -3
  247. package/dist/i18n/locales/fr.js.map +1 -1
  248. package/dist/i18n/locales/hu.js +77 -4
  249. package/dist/i18n/locales/hu.js.map +1 -1
  250. package/dist/i18n/locales/it.js +77 -4
  251. package/dist/i18n/locales/it.js.map +1 -1
  252. package/dist/i18n/locales/zh.js +74 -4
  253. package/dist/i18n/locales/zh.js.map +1 -1
  254. package/dist/i18n/review.js +22 -1
  255. package/dist/i18n/review.js.map +1 -1
  256. package/dist/index.d.ts +27 -8
  257. package/dist/index.js +41 -2
  258. package/dist/index.js.map +1 -1
  259. package/dist/landing/landing-labels.d.ts +8 -0
  260. package/dist/landing/landing-labels.js +2 -1
  261. package/dist/landing/landing-labels.js.map +1 -1
  262. package/dist/lib/changelog.d.ts +130 -0
  263. package/dist/lib/changelog.js +137 -0
  264. package/dist/lib/changelog.js.map +1 -0
  265. package/dist/lib/clipboard-minus.js +2 -2
  266. package/dist/lib/clipboard-minus.js.map +1 -1
  267. package/dist/lib/dates.d.ts +36 -4
  268. package/dist/lib/dates.js +49 -13
  269. package/dist/lib/dates.js.map +1 -1
  270. package/dist/lib/editable-content.d.ts +33 -0
  271. package/dist/lib/editable-content.js +15 -0
  272. package/dist/lib/editable-content.js.map +1 -0
  273. package/dist/lib/focus-left.d.ts +47 -1
  274. package/dist/lib/focus-left.js +11 -2
  275. package/dist/lib/focus-left.js.map +1 -1
  276. package/dist/lib/format.d.ts +37 -2
  277. package/dist/lib/format.js +25 -1
  278. package/dist/lib/format.js.map +1 -1
  279. package/dist/lib/intl-cache.d.ts +4 -2
  280. package/dist/lib/intl-cache.js.map +1 -1
  281. package/dist/lib/server-wake.d.ts +49 -9
  282. package/dist/lib/server-wake.js +25 -14
  283. package/dist/lib/server-wake.js.map +1 -1
  284. package/dist/lib/session-refresh.d.ts +53 -0
  285. package/dist/lib/session-refresh.js +27 -1
  286. package/dist/lib/session-refresh.js.map +1 -1
  287. package/dist/lib/translation-review.d.ts +26 -0
  288. package/dist/lib/translation-review.js +14 -0
  289. package/dist/lib/translation-review.js.map +1 -1
  290. package/dist/rhf/fields.js +3 -3
  291. package/dist/rhf/fields.js.map +1 -1
  292. package/dist/shell/app-shell.d.ts +25 -1
  293. package/dist/shell/app-shell.js +3 -1
  294. package/dist/shell/app-shell.js.map +1 -1
  295. package/dist/shell/dev-menu.d.ts +3 -0
  296. package/dist/shell/dev-menu.js +4 -2
  297. package/dist/shell/dev-menu.js.map +1 -1
  298. package/dist/shell/held-changes-banner-labels.d.ts +23 -0
  299. package/dist/shell/held-changes-banner-labels.js +14 -0
  300. package/dist/shell/held-changes-banner-labels.js.map +1 -0
  301. package/dist/shell/held-changes-banner.d.ts +75 -0
  302. package/dist/shell/held-changes-banner.js +106 -0
  303. package/dist/shell/held-changes-banner.js.map +1 -0
  304. package/dist/shell/sync-status-indicator.d.ts +27 -12
  305. package/dist/shell/sync-status-indicator.js +13 -4
  306. package/dist/shell/sync-status-indicator.js.map +1 -1
  307. package/dist/shell/top-bar-parts-labels.d.ts +5 -0
  308. package/dist/shell/top-bar-parts-labels.js +3 -1
  309. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  310. package/dist/shell.d.ts +2 -0
  311. package/dist/shell.js +3 -0
  312. package/dist/shell.js.map +1 -1
  313. package/dist/sync/engine.d.ts +11 -2
  314. package/dist/sync/engine.js +13 -5
  315. package/dist/sync/engine.js.map +1 -1
  316. package/dist/sync/hooks.d.ts +3 -2
  317. package/dist/sync/hooks.js +4 -2
  318. package/dist/sync/hooks.js.map +1 -1
  319. package/dist/testing/css-variables.generated.d.ts +2 -2
  320. package/dist/testing/css-variables.generated.js +1 -1
  321. package/dist/testing/css-variables.generated.js.map +1 -1
  322. package/dist/theme/chart-palette.d.ts +18 -0
  323. package/dist/theme/chart-palette.js +17 -1
  324. package/dist/theme/chart-palette.js.map +1 -1
  325. package/dist/theme/contrast-tokens.d.ts +3 -3
  326. package/dist/theme/contrast-tokens.js.map +1 -1
  327. package/dist/theme/contrast.d.ts +4 -4
  328. package/dist/theme/contrast.js.map +1 -1
  329. package/dist/tour/tour-overlay.js +2 -1
  330. package/dist/tour/tour-overlay.js.map +1 -1
  331. package/package.json +5 -9
  332. package/tokens.css +19 -54
  333. package/src/account/account-parts.ts +0 -47
  334. package/src/account/account-settings-labels.ts +0 -240
  335. package/src/account/account-settings.tsx +0 -698
  336. package/src/account/data-export-setting-labels.ts +0 -20
  337. package/src/account/data-export-setting.tsx +0 -200
  338. package/src/account/delete-account-setting-labels.ts +0 -33
  339. package/src/account/delete-account-setting.tsx +0 -341
  340. package/src/account/email-change-setting-labels.ts +0 -31
  341. package/src/account/email-change-setting.tsx +0 -443
  342. package/src/account/passkeys-setting.tsx +0 -445
  343. package/src/account/sessions-setting-labels.ts +0 -24
  344. package/src/account/sessions-setting.tsx +0 -283
  345. package/src/admin/admin-action-confirm-labels.ts +0 -19
  346. package/src/admin/admin-action-confirm.tsx +0 -509
  347. package/src/admin/admin-action-log-labels.ts +0 -83
  348. package/src/admin/admin-action-log.tsx +0 -464
  349. package/src/admin/admin-area-labels.ts +0 -114
  350. package/src/admin/admin-forbidden.tsx +0 -75
  351. package/src/admin/admin-overview.tsx +0 -258
  352. package/src/admin/admin-parts.ts +0 -68
  353. package/src/admin/admin-sections.tsx +0 -208
  354. package/src/admin/admin-tool-links.tsx +0 -90
  355. package/src/admin/invitations-panel-labels.ts +0 -31
  356. package/src/admin/invitations-panel.tsx +0 -516
  357. package/src/admin/mail-test-card-labels.ts +0 -19
  358. package/src/admin/mail-test-card.tsx +0 -189
  359. package/src/admin/plan-change-labels.ts +0 -5
  360. package/src/admin/plan-change.tsx +0 -474
  361. package/src/admin/reviewer-scope-editor-labels.ts +0 -16
  362. package/src/admin/reviewer-scope-editor.tsx +0 -224
  363. package/src/admin/role-select-labels.ts +0 -16
  364. package/src/admin/role-select.tsx +0 -282
  365. package/src/admin/transfer-ownership-dialog-labels.ts +0 -20
  366. package/src/admin/transfer-ownership-dialog.tsx +0 -162
  367. package/src/admin/user-roster-labels.ts +0 -17
  368. package/src/admin/user-roster.tsx +0 -559
  369. package/src/auth/accept-invitation-labels.ts +0 -21
  370. package/src/auth/accept-invitation.tsx +0 -210
  371. package/src/auth/auth-errors-labels.ts +0 -23
  372. package/src/auth/auth-errors.ts +0 -370
  373. package/src/auth/complete-name-dialog-labels.ts +0 -16
  374. package/src/auth/complete-name-dialog.tsx +0 -253
  375. package/src/auth/email-tag.ts +0 -75
  376. package/src/auth/forgot-password-form-labels.ts +0 -20
  377. package/src/auth/forgot-password-form.tsx +0 -221
  378. package/src/auth/form-rules.ts +0 -40
  379. package/src/auth/new-password-fields.tsx +0 -85
  380. package/src/auth/not-found-page-labels.ts +0 -12
  381. package/src/auth/not-found-page.tsx +0 -95
  382. package/src/auth/register-form-labels.ts +0 -31
  383. package/src/auth/register-form.tsx +0 -456
  384. package/src/auth/reset-password-form-labels.ts +0 -27
  385. package/src/auth/reset-password-form.tsx +0 -341
  386. package/src/auth/sign-in-form-labels.ts +0 -44
  387. package/src/auth/sign-in-form.tsx +0 -858
  388. package/src/auth/status-parts.tsx +0 -132
  389. package/src/auth/verify-email-labels.ts +0 -26
  390. package/src/auth/verify-email.tsx +0 -445
  391. package/src/billing/billing-banner.tsx +0 -258
  392. package/src/billing/billing-labels.ts +0 -381
  393. package/src/billing/billing-lock.ts +0 -108
  394. package/src/billing/billing-overview.ts +0 -157
  395. package/src/billing/billing-standing.ts +0 -80
  396. package/src/billing/cancel-request.ts +0 -97
  397. package/src/billing/cancel-subscription.tsx +0 -98
  398. package/src/billing/checkout-processing.ts +0 -394
  399. package/src/billing/checkout-return.ts +0 -90
  400. package/src/billing/pay-page.ts +0 -330
  401. package/src/billing/plan-card.tsx +0 -562
  402. package/src/billing/plan-change-labels.ts +0 -66
  403. package/src/billing/plan-limit-notice.tsx +0 -148
  404. package/src/billing/plan-limit-toast.tsx +0 -90
  405. package/src/billing/plan-limit.ts +0 -52
  406. package/src/billing/plan-price.ts +0 -98
  407. package/src/billing/plan-switch.tsx +0 -216
  408. package/src/billing/provider-page.ts +0 -84
  409. package/src/billing/subscription-actions.tsx +0 -247
  410. package/src/billing/subscription-status-chip.tsx +0 -95
  411. package/src/chart.ts +0 -103
  412. package/src/components/account-chips-labels.ts +0 -16
  413. package/src/components/account-chips.tsx +0 -474
  414. package/src/components/alert-banner.tsx +0 -670
  415. package/src/components/amount-input.tsx +0 -815
  416. package/src/components/appearance-labels.ts +0 -63
  417. package/src/components/authed-image-labels.ts +0 -11
  418. package/src/components/authed-image.tsx +0 -209
  419. package/src/components/autocomplete.tsx +0 -645
  420. package/src/components/breadcrumbs-labels.ts +0 -10
  421. package/src/components/breadcrumbs.tsx +0 -205
  422. package/src/components/bulk-action-bar-labels.ts +0 -11
  423. package/src/components/bulk-action-bar.tsx +0 -443
  424. package/src/components/button-group.tsx +0 -223
  425. package/src/components/button-look.ts +0 -256
  426. package/src/components/button.tsx +0 -339
  427. package/src/components/calculator.tsx +0 -341
  428. package/src/components/calendar-heatmap-labels.ts +0 -15
  429. package/src/components/calendar-heatmap.tsx +0 -594
  430. package/src/components/card.tsx +0 -377
  431. package/src/components/chart-edge.ts +0 -106
  432. package/src/components/chart-figure.tsx +0 -35
  433. package/src/components/chart-height.ts +0 -30
  434. package/src/components/chart-zoom-core.tsx +0 -627
  435. package/src/components/chart-zoom.tsx +0 -22
  436. package/src/components/chart.tsx +0 -554
  437. package/src/components/checkbox-group.tsx +0 -247
  438. package/src/components/checkbox.tsx +0 -346
  439. package/src/components/chip-input.tsx +0 -370
  440. package/src/components/chip-labels.ts +0 -13
  441. package/src/components/chip.tsx +0 -843
  442. package/src/components/choice-card.tsx +0 -544
  443. package/src/components/column-mapper-labels.ts +0 -38
  444. package/src/components/column-mapper.tsx +0 -655
  445. package/src/components/combobox-core.tsx +0 -1321
  446. package/src/components/combobox-suggestions.tsx +0 -131
  447. package/src/components/combobox.tsx +0 -716
  448. package/src/components/confirm-context.ts +0 -7
  449. package/src/components/confirm-dialog-labels.ts +0 -13
  450. package/src/components/confirm-dialog.tsx +0 -341
  451. package/src/components/copy-button-labels.ts +0 -13
  452. package/src/components/copy-button.tsx +0 -192
  453. package/src/components/country-select-labels.ts +0 -11
  454. package/src/components/country-select.tsx +0 -539
  455. package/src/components/currency-option-list.tsx +0 -180
  456. package/src/components/currency-select.tsx +0 -341
  457. package/src/components/danger-confirm-labels.ts +0 -20
  458. package/src/components/danger-confirm.tsx +0 -934
  459. package/src/components/data-table-cells.tsx +0 -118
  460. package/src/components/data-table-column-settings.tsx +0 -198
  461. package/src/components/data-table-filter-popover.tsx +0 -281
  462. package/src/components/data-table-filters.ts +0 -287
  463. package/src/components/data-table-fit.ts +0 -301
  464. package/src/components/data-table-groups.tsx +0 -288
  465. package/src/components/data-table-head.tsx +0 -280
  466. package/src/components/data-table-labels.ts +0 -198
  467. package/src/components/data-table-leading-row.tsx +0 -58
  468. package/src/components/data-table-mobile.tsx +0 -1117
  469. package/src/components/data-table-pagination.tsx +0 -202
  470. package/src/components/data-table-resize.ts +0 -188
  471. package/src/components/data-table-row-actions.tsx +0 -302
  472. package/src/components/data-table-row.tsx +0 -496
  473. package/src/components/data-table-selection.ts +0 -71
  474. package/src/components/data-table-shared.tsx +0 -287
  475. package/src/components/data-table-sort.ts +0 -163
  476. package/src/components/data-table-virtual.ts +0 -576
  477. package/src/components/data-table.tsx +0 -2598
  478. package/src/components/date-field.tsx +0 -684
  479. package/src/components/date-format-setting.tsx +0 -64
  480. package/src/components/date-picker.tsx +0 -404
  481. package/src/components/date-range-picker.tsx +0 -518
  482. package/src/components/description-list-labels.ts +0 -9
  483. package/src/components/description-list.tsx +0 -386
  484. package/src/components/dialog-frame-labels.ts +0 -7
  485. package/src/components/dialog-frame.tsx +0 -276
  486. package/src/components/disclosure.tsx +0 -476
  487. package/src/components/dropdown.tsx +0 -461
  488. package/src/components/empty-state.tsx +0 -136
  489. package/src/components/entity-combobox.tsx +0 -422
  490. package/src/components/error-boundary-labels.ts +0 -20
  491. package/src/components/error-boundary.tsx +0 -675
  492. package/src/components/facing-pair.ts +0 -93
  493. package/src/components/field-base-labels.ts +0 -20
  494. package/src/components/field-base.tsx +0 -656
  495. package/src/components/field-parts.tsx +0 -825
  496. package/src/components/field-strip.tsx +0 -129
  497. package/src/components/field.tsx +0 -206
  498. package/src/components/file-button.tsx +0 -295
  499. package/src/components/file-dropzone.tsx +0 -559
  500. package/src/components/floating-panel-labels.ts +0 -10
  501. package/src/components/floating-panel.tsx +0 -828
  502. package/src/components/focus-ring.ts +0 -16
  503. package/src/components/form-actions-labels.ts +0 -11
  504. package/src/components/form-actions.tsx +0 -578
  505. package/src/components/full-bleed-dialog.tsx +0 -294
  506. package/src/components/grouped-picker.tsx +0 -184
  507. package/src/components/help-popover.tsx +0 -117
  508. package/src/components/hover-menu.tsx +0 -343
  509. package/src/components/iban-input-labels.ts +0 -14
  510. package/src/components/iban-input.tsx +0 -236
  511. package/src/components/icon-button.tsx +0 -959
  512. package/src/components/icon-picker-labels.ts +0 -13
  513. package/src/components/icon-picker.tsx +0 -232
  514. package/src/components/image-grid-labels.ts +0 -11
  515. package/src/components/image-grid.tsx +0 -202
  516. package/src/components/inline-confirm.tsx +0 -116
  517. package/src/components/inline-edit-field-labels.ts +0 -11
  518. package/src/components/inline-edit-field.tsx +0 -552
  519. package/src/components/inline-entity-combobox.tsx +0 -849
  520. package/src/components/input.tsx +0 -394
  521. package/src/components/jump-calendar.tsx +0 -265
  522. package/src/components/language-select.tsx +0 -93
  523. package/src/components/legal-labels.ts +0 -105
  524. package/src/components/legal-page.tsx +0 -711
  525. package/src/components/legal.tsx +0 -668
  526. package/src/components/lightbox-labels.ts +0 -18
  527. package/src/components/lightbox.tsx +0 -399
  528. package/src/components/line-items-labels.ts +0 -14
  529. package/src/components/line-items.tsx +0 -602
  530. package/src/components/list-labels.ts +0 -12
  531. package/src/components/list.tsx +0 -870
  532. package/src/components/loading-state.tsx +0 -78
  533. package/src/components/measured-grid-labels.ts +0 -25
  534. package/src/components/measured-grid.tsx +0 -837
  535. package/src/components/menu-item.tsx +0 -241
  536. package/src/components/mini-calendar-labels.ts +0 -17
  537. package/src/components/mini-calendar.tsx +0 -789
  538. package/src/components/modal.tsx +0 -505
  539. package/src/components/money-draft.ts +0 -83
  540. package/src/components/money-field.tsx +0 -76
  541. package/src/components/month-grid.tsx +0 -229
  542. package/src/components/month-picker-labels.ts +0 -22
  543. package/src/components/month-picker.tsx +0 -1087
  544. package/src/components/multi-entity-combobox.tsx +0 -369
  545. package/src/components/multi-select.tsx +0 -376
  546. package/src/components/nav-pills.tsx +0 -198
  547. package/src/components/number-field.tsx +0 -327
  548. package/src/components/number-input.tsx +0 -558
  549. package/src/components/numeric-field.ts +0 -169
  550. package/src/components/numpad-sheet.tsx +0 -285
  551. package/src/components/one-time-code-input.tsx +0 -270
  552. package/src/components/page-contents-labels.ts +0 -9
  553. package/src/components/page-contents.tsx +0 -282
  554. package/src/components/page-header-context.ts +0 -8
  555. package/src/components/page-header.tsx +0 -298
  556. package/src/components/password-strength-labels.ts +0 -22
  557. package/src/components/password-strength.tsx +0 -241
  558. package/src/components/phone-input-labels.ts +0 -10
  559. package/src/components/phone-input.tsx +0 -282
  560. package/src/components/picker-sheet.tsx +0 -330
  561. package/src/components/pie-chart-labels.ts +0 -40
  562. package/src/components/pie-chart.tsx +0 -480
  563. package/src/components/popover-labels.ts +0 -14
  564. package/src/components/popover.tsx +0 -268
  565. package/src/components/progress-bar-labels.ts +0 -10
  566. package/src/components/progress-bar.tsx +0 -479
  567. package/src/components/qr-code.tsx +0 -76
  568. package/src/components/reauth-dialog-labels.ts +0 -13
  569. package/src/components/reauth-dialog.tsx +0 -265
  570. package/src/components/row-actions-labels.ts +0 -12
  571. package/src/components/row-actions.tsx +0 -795
  572. package/src/components/scroll-area.tsx +0 -123
  573. package/src/components/search-field.tsx +0 -192
  574. package/src/components/select.tsx +0 -294
  575. package/src/components/separator.tsx +0 -33
  576. package/src/components/series-chart-axes.ts +0 -342
  577. package/src/components/series-chart-budget.ts +0 -150
  578. package/src/components/series-chart-labels.ts +0 -33
  579. package/src/components/series-chart-thin.ts +0 -180
  580. package/src/components/series-chart-ticks.ts +0 -194
  581. package/src/components/series-chart.tsx +0 -1906
  582. package/src/components/server-wake-labels.ts +0 -11
  583. package/src/components/server-wake.tsx +0 -129
  584. package/src/components/settings-fields.tsx +0 -237
  585. package/src/components/share-card-labels.ts +0 -30
  586. package/src/components/share-card.tsx +0 -675
  587. package/src/components/signature-pad-labels.ts +0 -24
  588. package/src/components/signature-pad.tsx +0 -747
  589. package/src/components/signed-amount-labels.ts +0 -10
  590. package/src/components/signed-amount.tsx +0 -336
  591. package/src/components/skeleton.tsx +0 -80
  592. package/src/components/slider.tsx +0 -338
  593. package/src/components/sparkline-labels.ts +0 -14
  594. package/src/components/sparkline.tsx +0 -322
  595. package/src/components/spinner.tsx +0 -81
  596. package/src/components/stat-tile-labels.ts +0 -15
  597. package/src/components/stat-tile.tsx +0 -725
  598. package/src/components/status-dot.tsx +0 -170
  599. package/src/components/swatch-picker-labels.ts +0 -10
  600. package/src/components/swatch-picker.tsx +0 -283
  601. package/src/components/swipeable-row.tsx +0 -386
  602. package/src/components/switch.tsx +0 -273
  603. package/src/components/table-skeleton.tsx +0 -136
  604. package/src/components/table.tsx +0 -647
  605. package/src/components/tabs-labels.ts +0 -13
  606. package/src/components/tabs.tsx +0 -693
  607. package/src/components/text-link.tsx +0 -359
  608. package/src/components/text.tsx +0 -150
  609. package/src/components/textarea.tsx +0 -330
  610. package/src/components/tile-radio.tsx +0 -332
  611. package/src/components/time-input.tsx +0 -121
  612. package/src/components/toast-labels.ts +0 -12
  613. package/src/components/toast.tsx +0 -519
  614. package/src/components/toggle-group.tsx +0 -752
  615. package/src/components/toggle-legend.tsx +0 -394
  616. package/src/components/tooltip.tsx +0 -1149
  617. package/src/components/translation-review-editor.tsx +0 -290
  618. package/src/components/translation-review-labels.ts +0 -181
  619. package/src/components/translation-review-parts.tsx +0 -272
  620. package/src/components/translation-review.tsx +0 -1240
  621. package/src/components/tree-view.tsx +0 -589
  622. package/src/components/treemap-labels.ts +0 -47
  623. package/src/components/treemap-layout.ts +0 -210
  624. package/src/components/treemap.tsx +0 -1184
  625. package/src/components/trigger-aria.ts +0 -44
  626. package/src/components/use-anchor-dir.ts +0 -20
  627. package/src/components/use-mobile-reveal.ts +0 -62
  628. package/src/components/use-table-state.ts +0 -753
  629. package/src/components/user-avatar.tsx +0 -116
  630. package/src/components/wizard-stepper.tsx +0 -42
  631. package/src/components/write-lock-labels.ts +0 -10
  632. package/src/components/write-lock.tsx +0 -253
  633. package/src/currencies.ts +0 -70
  634. package/src/data-table.ts +0 -42
  635. package/src/demo/demo-banner.tsx +0 -153
  636. package/src/demo/demo-countdown.ts +0 -136
  637. package/src/demo/demo-ended.tsx +0 -90
  638. package/src/demo/demo-labels.ts +0 -78
  639. package/src/demo/demo-session.ts +0 -32
  640. package/src/demo/demo-start.tsx +0 -156
  641. package/src/eslint/index.ts +0 -161
  642. package/src/eslint/role-utilities.generated.ts +0 -146
  643. package/src/eslint/role-utilities.ts +0 -233
  644. package/src/feedback/feedback-attachment-download.tsx +0 -77
  645. package/src/feedback/feedback-attachment-labels.ts +0 -37
  646. package/src/feedback/feedback-attachment.tsx +0 -1239
  647. package/src/feedback/feedback-capture.ts +0 -287
  648. package/src/feedback/feedback-comment-thread.tsx +0 -353
  649. package/src/feedback/feedback-context-labels.ts +0 -10
  650. package/src/feedback/feedback-context.tsx +0 -213
  651. package/src/feedback/feedback-crash.ts +0 -547
  652. package/src/feedback/feedback-dialog-labels.ts +0 -29
  653. package/src/feedback/feedback-dialog.tsx +0 -440
  654. package/src/feedback/feedback-entry-file.tsx +0 -150
  655. package/src/feedback/feedback-inbox.tsx +0 -552
  656. package/src/feedback/feedback-labels.ts +0 -144
  657. package/src/feedback/feedback-menu-labels.ts +0 -12
  658. package/src/feedback/feedback-menu.tsx +0 -177
  659. package/src/feedback/feedback-mutation.ts +0 -20
  660. package/src/feedback/feedback-page-link.ts +0 -90
  661. package/src/feedback/feedback-public-labels.ts +0 -81
  662. package/src/feedback/feedback-public-list.tsx +0 -524
  663. package/src/feedback/feedback-public-switch.tsx +0 -102
  664. package/src/feedback/feedback-public-types.ts +0 -101
  665. package/src/feedback/feedback-record.ts +0 -423
  666. package/src/feedback/feedback-replies-context.ts +0 -9
  667. package/src/feedback/feedback-replies.tsx +0 -343
  668. package/src/feedback/feedback-rework-entry-chip.tsx +0 -18
  669. package/src/feedback/feedback-row-detail-labels.ts +0 -30
  670. package/src/feedback/feedback-row-detail.tsx +0 -858
  671. package/src/feedback/feedback-status-undo.ts +0 -109
  672. package/src/feedback/feedback-submit-dialog.tsx +0 -118
  673. package/src/feedback/feedback-submit.tsx +0 -237
  674. package/src/feedback/feedback-swipe.tsx +0 -192
  675. package/src/feedback/feedback-table-labels.ts +0 -27
  676. package/src/feedback/feedback-table.tsx +0 -912
  677. package/src/feedback/feedback-thread-labels.ts +0 -22
  678. package/src/feedback/feedback-thread-parts-labels.ts +0 -41
  679. package/src/feedback/feedback-thread.tsx +0 -905
  680. package/src/feedback/feedback-vocabulary.ts +0 -263
  681. package/src/feedback/feedback-vote-button.tsx +0 -124
  682. package/src/feedback.ts +0 -116
  683. package/src/hooks/use-anchored-panel.ts +0 -396
  684. package/src/hooks/use-anchored-rect.ts +0 -77
  685. package/src/hooks/use-announce.ts +0 -85
  686. package/src/hooks/use-authed-src.ts +0 -167
  687. package/src/hooks/use-body-scroll-lock.ts +0 -119
  688. package/src/hooks/use-breakpoint.ts +0 -133
  689. package/src/hooks/use-close-transition.ts +0 -107
  690. package/src/hooks/use-copy-to-clipboard.ts +0 -122
  691. package/src/hooks/use-debounce.ts +0 -114
  692. package/src/hooks/use-dismiss.ts +0 -371
  693. package/src/hooks/use-document-title.ts +0 -33
  694. package/src/hooks/use-file-drop.ts +0 -121
  695. package/src/hooks/use-file-picker-labels.ts +0 -28
  696. package/src/hooks/use-file-picker.tsx +0 -359
  697. package/src/hooks/use-focus-trap.ts +0 -199
  698. package/src/hooks/use-hotkey.ts +0 -189
  699. package/src/hooks/use-large-text.ts +0 -41
  700. package/src/hooks/use-long-press.ts +0 -140
  701. package/src/hooks/use-media-query.ts +0 -39
  702. package/src/hooks/use-mounted.ts +0 -39
  703. package/src/hooks/use-noindex.ts +0 -37
  704. package/src/hooks/use-overlay-history.ts +0 -714
  705. package/src/hooks/use-row-swipe.ts +0 -207
  706. package/src/hooks/use-search-param-state.ts +0 -434
  707. package/src/hooks/use-windowed-rows.ts +0 -225
  708. package/src/i18n/defaults.ts +0 -267
  709. package/src/i18n/german.ts +0 -2057
  710. package/src/i18n/kit-labels.tsx +0 -1003
  711. package/src/i18n/languages.ts +0 -349
  712. package/src/i18n/locales/de-CH.ts +0 -24
  713. package/src/i18n/locales/en.ts +0 -267
  714. package/src/i18n/locales/es.ts +0 -1997
  715. package/src/i18n/locales/fr.ts +0 -2035
  716. package/src/i18n/locales/hu.ts +0 -2002
  717. package/src/i18n/locales/it.ts +0 -2012
  718. package/src/i18n/locales/zh.ts +0 -1910
  719. package/src/i18n/plurals.ts +0 -86
  720. package/src/i18n/review.ts +0 -475
  721. package/src/i18n/shared-labels.ts +0 -138
  722. package/src/i18n/swiss.ts +0 -29
  723. package/src/index.ts +0 -1179
  724. package/src/landing/access.ts +0 -93
  725. package/src/landing/landing-actions.tsx +0 -97
  726. package/src/landing/landing-labels.ts +0 -59
  727. package/src/landing/landing-sections.tsx +0 -341
  728. package/src/landing/page-seo.ts +0 -261
  729. package/src/landing/public-header.tsx +0 -121
  730. package/src/landing/public-page.tsx +0 -106
  731. package/src/landing/routing.tsx +0 -225
  732. package/src/lib/broadcast.ts +0 -52
  733. package/src/lib/calc.ts +0 -323
  734. package/src/lib/chip-edge-snap.ts +0 -212
  735. package/src/lib/clipboard-minus.ts +0 -107
  736. package/src/lib/clipping.ts +0 -34
  737. package/src/lib/cn.ts +0 -28
  738. package/src/lib/column-mapping.ts +0 -234
  739. package/src/lib/countries.ts +0 -91
  740. package/src/lib/data-attributes.ts +0 -14
  741. package/src/lib/date-format.ts +0 -227
  742. package/src/lib/dates.ts +0 -266
  743. package/src/lib/day-format.ts +0 -96
  744. package/src/lib/decimal-marks.ts +0 -108
  745. package/src/lib/describe-failure.ts +0 -18
  746. package/src/lib/device-settings.ts +0 -147
  747. package/src/lib/direction.ts +0 -74
  748. package/src/lib/document-navigation.ts +0 -17
  749. package/src/lib/document-title.ts +0 -41
  750. package/src/lib/download.ts +0 -172
  751. package/src/lib/float-clearance.ts +0 -70
  752. package/src/lib/floating-stack.ts +0 -70
  753. package/src/lib/focus-left.ts +0 -156
  754. package/src/lib/format.ts +0 -522
  755. package/src/lib/http-status.ts +0 -38
  756. package/src/lib/iban.ts +0 -251
  757. package/src/lib/ime.ts +0 -24
  758. package/src/lib/intl-cache.ts +0 -25
  759. package/src/lib/iso-day.ts +0 -13
  760. package/src/lib/lazy-with-fallback.ts +0 -82
  761. package/src/lib/logger.ts +0 -69
  762. package/src/lib/minus.ts +0 -50
  763. package/src/lib/path-pattern.ts +0 -55
  764. package/src/lib/person-name.ts +0 -155
  765. package/src/lib/phone.ts +0 -208
  766. package/src/lib/plain-text.ts +0 -22
  767. package/src/lib/presence.ts +0 -18
  768. package/src/lib/qr-encode.ts +0 -444
  769. package/src/lib/quiet-focus.ts +0 -32
  770. package/src/lib/reduced-motion.ts +0 -22
  771. package/src/lib/refs.ts +0 -22
  772. package/src/lib/round.ts +0 -43
  773. package/src/lib/safe-storage.ts +0 -119
  774. package/src/lib/server-wake.ts +0 -315
  775. package/src/lib/session-refresh.ts +0 -472
  776. package/src/lib/share-format.ts +0 -18
  777. package/src/lib/status-tone.ts +0 -43
  778. package/src/lib/strip-fade.ts +0 -287
  779. package/src/lib/table-text.ts +0 -554
  780. package/src/lib/text-case.ts +0 -19
  781. package/src/lib/text-match.ts +0 -169
  782. package/src/lib/thenable.ts +0 -19
  783. package/src/lib/translation-review.ts +0 -717
  784. package/src/notifications/device-zone-report.ts +0 -67
  785. package/src/notifications/feedback-notification-types-labels.ts +0 -33
  786. package/src/notifications/feedback-notification-types.ts +0 -71
  787. package/src/notifications/notification-bell.tsx +0 -119
  788. package/src/notifications/notification-inbox.tsx +0 -309
  789. package/src/notifications/notification-parts.tsx +0 -81
  790. package/src/notifications/notification-types-setting.tsx +0 -406
  791. package/src/notifications/notification-types.ts +0 -89
  792. package/src/notifications/notifications-labels.ts +0 -287
  793. package/src/notifications/push-device-setting.tsx +0 -230
  794. package/src/notifications/push-devices-setting.tsx +0 -374
  795. package/src/notifications/push-worker.ts +0 -168
  796. package/src/notifications/quiet-hours-setting.tsx +0 -238
  797. package/src/notifications/unsubscribe-panel.tsx +0 -200
  798. package/src/notifications/use-push-device.ts +0 -448
  799. package/src/offline/local-first.ts +0 -100
  800. package/src/offline/pwa-update-prompt-labels.ts +0 -9
  801. package/src/offline/pwa-update-prompt.tsx +0 -78
  802. package/src/offline/query-persistence.ts +0 -391
  803. package/src/offline/service-worker-updates.ts +0 -81
  804. package/src/offline/shared-computer-labels.ts +0 -8
  805. package/src/offline/shared-computer.tsx +0 -87
  806. package/src/offline/sign-out-pending-dialog-labels.ts +0 -21
  807. package/src/offline/sign-out-pending-dialog.tsx +0 -135
  808. package/src/offline.ts +0 -37
  809. package/src/push-worker.ts +0 -25
  810. package/src/rhf/fields.tsx +0 -2187
  811. package/src/rhf/form-core.tsx +0 -324
  812. package/src/rhf/form.tsx +0 -12
  813. package/src/rhf/line-items.tsx +0 -135
  814. package/src/rhf/use-rhf-wizard-step.ts +0 -104
  815. package/src/rhf.ts +0 -21
  816. package/src/search/command-palette-labels.ts +0 -16
  817. package/src/search/command-palette.tsx +0 -624
  818. package/src/search/global-search-labels.ts +0 -13
  819. package/src/search/global-search.tsx +0 -660
  820. package/src/search/search-index.ts +0 -283
  821. package/src/search/use-command-key.ts +0 -23
  822. package/src/search.ts +0 -14
  823. package/src/settings/device-reset-setting-labels.ts +0 -20
  824. package/src/settings/device-reset-setting.tsx +0 -116
  825. package/src/settings/settings-card-classes.ts +0 -11
  826. package/src/settings/settings-catalogue.ts +0 -150
  827. package/src/settings/settings-context.ts +0 -33
  828. package/src/settings/settings-heading.tsx +0 -67
  829. package/src/settings/settings-labels.ts +0 -87
  830. package/src/settings/settings-layout.tsx +0 -471
  831. package/src/settings/settings-section.tsx +0 -108
  832. package/src/settings/use-account-appearance.ts +0 -148
  833. package/src/settings/use-account-language.ts +0 -146
  834. package/src/settings/use-settings-focus.ts +0 -128
  835. package/src/settings/use-settings-route.ts +0 -258
  836. package/src/shell/account-menu-language.tsx +0 -130
  837. package/src/shell/account-menu-text-size.tsx +0 -80
  838. package/src/shell/admin-menu.tsx +0 -183
  839. package/src/shell/app-shell-labels.ts +0 -11
  840. package/src/shell/app-shell.tsx +0 -1560
  841. package/src/shell/auth-layout.tsx +0 -186
  842. package/src/shell/company-switcher-labels.ts +0 -13
  843. package/src/shell/company-switcher.tsx +0 -136
  844. package/src/shell/dev-menu.tsx +0 -257
  845. package/src/shell/fill-height.tsx +0 -45
  846. package/src/shell/language-code.ts +0 -5
  847. package/src/shell/option-switcher-menu.tsx +0 -84
  848. package/src/shell/sync-status-indicator.tsx +0 -303
  849. package/src/shell/top-bar-actions.tsx +0 -87
  850. package/src/shell/top-bar-brand.tsx +0 -78
  851. package/src/shell/top-bar-parts-labels.ts +0 -148
  852. package/src/shell/top-bar.tsx +0 -40
  853. package/src/shell/topbar-action-menu.tsx +0 -567
  854. package/src/shell/topbar-controls.tsx +0 -269
  855. package/src/shell/tour-menu.tsx +0 -187
  856. package/src/shell.ts +0 -108
  857. package/src/sync/db.ts +0 -129
  858. package/src/sync/engine.ts +0 -1011
  859. package/src/sync/hooks.ts +0 -98
  860. package/src/sync/testing.ts +0 -73
  861. package/src/sync/wire.ts +0 -61
  862. package/src/sync.ts +0 -37
  863. package/src/table-text.ts +0 -8
  864. package/src/testing/css-variables.generated.ts +0 -112
  865. package/src/testing/css-variables.ts +0 -189
  866. package/src/testing/index.ts +0 -29
  867. package/src/testing/react.ts +0 -58
  868. package/src/testing/tour.ts +0 -41
  869. package/src/testing/viewport.ts +0 -357
  870. package/src/theme/chart-palette.ts +0 -232
  871. package/src/theme/color.ts +0 -272
  872. package/src/theme/contrast-tokens.ts +0 -268
  873. package/src/theme/contrast.ts +0 -250
  874. package/src/theme/palette-derive.ts +0 -825
  875. package/src/theme/palette-presets.ts +0 -544
  876. package/src/theme/palette-store.ts +0 -108
  877. package/src/theme/text-size.ts +0 -289
  878. package/src/theme/theme-store.ts +0 -157
  879. package/src/tour/tour-catalogue-labels.ts +0 -75
  880. package/src/tour/tour-catalogue.tsx +0 -335
  881. package/src/tour/tour-dock.ts +0 -134
  882. package/src/tour/tour-engine.ts +0 -37
  883. package/src/tour/tour-gates.ts +0 -218
  884. package/src/tour/tour-guide-button.tsx +0 -64
  885. package/src/tour/tour-guide-labels.ts +0 -15
  886. package/src/tour/tour-info.ts +0 -144
  887. package/src/tour/tour-labels.ts +0 -23
  888. package/src/tour/tour-overlay.tsx +0 -1170
  889. package/src/tour/tour.tsx +0 -750
  890. package/src/tour/use-tour-progress.ts +0 -671
  891. package/src/tour/use-tour-url-sync.ts +0 -266
  892. package/src/tour.ts +0 -21
  893. package/src/vite-env.d.ts +0 -23
  894. package/src/wizard/stepper-nav.tsx +0 -360
  895. package/src/wizard/types.ts +0 -275
  896. package/src/wizard/use-wizard.ts +0 -560
  897. package/src/wizard/validation.ts +0 -39
  898. package/src/wizard/wizard-context.tsx +0 -74
  899. package/src/wizard/wizard-labels.ts +0 -29
  900. package/src/wizard/wizard-step.tsx +0 -76
  901. package/src/wizard/wizard-summary.tsx +0 -84
  902. package/src/wizard.ts +0 -15
@@ -1,1560 +0,0 @@
1
- import {
2
- createContext,
3
- useCallback,
4
- useContext,
5
- useEffect,
6
- useId,
7
- useLayoutEffect,
8
- useMemo,
9
- useRef,
10
- useState,
11
- } from "react";
12
- import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
13
- import { createPortal } from "react-dom";
14
- import { Link, NavLink, matchPath, useHref, useLocation } from "react-router";
15
- import { ChevronRight, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react";
16
- import type { LucideIcon } from "lucide-react";
17
- import { cn } from "../lib/cn";
18
- import { scrollIntoStrip, useStripFade } from "../lib/strip-fade";
19
- import { dirOf, type Direction } from "../lib/direction";
20
- import { readStored, writeStored } from "../lib/safe-storage";
21
- import { registerDeviceSetting } from "../lib/device-settings";
22
- import { Tooltip } from "../components/tooltip";
23
- import { pickLinkRenderer } from "../components/text-link";
24
- import { DialogFrame } from "../components/dialog-frame";
25
- import { CompactControls } from "../components/field-parts";
26
- import { mergeRefs } from "../components/field-parts";
27
- import { useAnchoredRect } from "../hooks/use-anchored-rect";
28
- import { anchoredInline } from "../hooks/use-anchored-panel";
29
- import { useDismissLayer } from "../hooks/use-dismiss";
30
- import { useBreakpoint } from "../hooks/use-breakpoint";
31
- import { TEXT_SIZES, useTextSize } from "../theme/text-size";
32
- import type { TextSize } from "../theme/text-size";
33
- import { DEFAULT_APP_SHELL_LABELS, useKitLabels, useKitLink, type LabelOverride } from "../i18n/kit-labels";
34
- import type { KitLinkComponent, KitLinkProps } from "../i18n/kit-labels";
35
- import { DEFAULT_APP_SHELL_MORE_LABELS } from "./app-shell-labels";
36
-
37
- export interface AppShellSubItem {
38
- to: string;
39
- label: string;
40
- icon: LucideIcon;
41
- /** Passed to NavLink's `end` (exact match). Defaults to true. */
42
- end?: boolean;
43
- }
44
-
45
- export interface AppShellNavItem {
46
- to: string;
47
- label: string;
48
- /** Optional shorter label for the MOBILE BOTTOM BAR only (the sidebar, its
49
- * tooltips and any palette built on this list keep `label`).
50
- *
51
- * The bar divides the viewport into `nav.length` equal cells, so a label's room
52
- * shrinks with every entry added — at six entries on a 406 px phone that is ~67 px,
53
- * and "Import & Export" wrapped to two lines and pushed the row taller than its
54
- * neighbours (keksdose feedback live #210). A per-item override is better than
55
- * shortening the real label, which the sidebar has ample room for and which is the
56
- * name the rest of the product uses. */
57
- shortLabel?: string;
58
- /** 0.48: the text size from which the bar uses {@link shortLabel} — `"large"` keeps the
59
- * full label at Normal, where five cells hold "Transactions" whole, and shortens it
60
- * from Large up (keksdose dev#613, which read `useTextSize()` in its layout to decide).
61
- * Default `"normal"`: the short label at every size, as before. */
62
- shortLabelFrom?: TextSize;
63
- /** 0.49: `shortLabel` is another word for the page, not a shortening of `label` —
64
- * keksdose's "Activity" for "Transactions". Without it a development build warns once
65
- * when `shortLabel` does not start with `label`'s first word (an abbreviation of that
66
- * word, "Trans." for "Transactions", counts), because a bar cell that names a page
67
- * differently from the sidebar, the title and the palette is a page the reader
68
- * looks for twice. */
69
- shortLabelIsAlias?: boolean;
70
- /** 0.49: which entries sit in the phone bar when they do not all fit
71
- * ({@link AppShellProps.mobileMaxItems}). Lower first: the entries with a priority take
72
- * the bar's cells in its order, then the rest in the list's order; the bar still shows
73
- * its entries in the list's order, and the others go under "More". Unset: the list's
74
- * order decides (as before). */
75
- mobilePriority?: number;
76
- /** 0.31.0: leave this entry out of the MOBILE BOTTOM BAR; the sidebar keeps it.
77
- * For an entry a phone reaches another way — the brand link to the start page —
78
- * when one cell fewer is what keeps the others' labels whole (the bar divides its
79
- * width evenly, see `shortLabel`). Its group's sub-row still shows while one of its
80
- * pages is open. Named like `DataTable`'s column `mobileHidden`. */
81
- mobileHidden?: boolean;
82
- icon: LucideIcon;
83
- /** Passed to NavLink's `end` (exact match). Defaults to true. */
84
- end?: boolean;
85
- /** Optional `data-tour` value rendered on this item's link (both the desktop
86
- * sidebar and the mobile bottom bar), so a guided tour can spotlight ONE nav
87
- * entry precisely instead of matching by href (feedback #338). */
88
- dataTour?: string;
89
- /** Optional sub-items. When present, hovering (or focusing) the item opens a
90
- * flyout to the right of the sidebar with a header (this item's label) and the
91
- * sub-items as icon + title links. Works collapsed or expanded. On the phone (0.49)
92
- * the bar's cell opens a sheet of them instead of linking to `to` — the sheet lists
93
- * `to` first as "Overview" when no sub-item is that page — and the pages of the group
94
- * the reader is in show as the row above the bar (`mobileSubNav`). */
95
- items?: AppShellSubItem[];
96
- }
97
-
98
- /**
99
- * One entry of the sidebar's bottom area ({@link AppShellProps.sidebarFooterItems}) —
100
- * the version link and the legal links kastlan hand-built in `sidebarFooter`.
101
- *
102
- * - `link` (default): one link. Expanded it shows `icon` + `text` (default `label`),
103
- * with `hint` as a tooltip if given; collapsed it is the icon alone, named and
104
- * tooltipped by `label`.
105
- * - `links`: a small group — the legal links. Expanded, a `<nav>` named `label` holding
106
- * the links as a wrapping row of small text; collapsed, ONE icon link to `to` (default
107
- * the first link's), named and tooltipped by `label`, since a column of icons for
108
- * "Imprint", "Privacy" and "Terms" would say nothing an icon can.
109
- */
110
- export type AppShellSidebarFooterItem =
111
- | {
112
- kind?: "link";
113
- key: string;
114
- to: string;
115
- icon: LucideIcon;
116
- /** The link's name, and the tooltip when collapsed ("What's new · v1.4.0"). */
117
- label: string;
118
- /** Visible text when expanded ("Kastlan v1.4.0"). Default `label`. */
119
- text?: ReactNode;
120
- /** A tooltip on the expanded link ("What's new"). */
121
- hint?: string;
122
- /** Leaves the app: a plain `<a target="_blank" rel="noopener noreferrer">`. */
123
- external?: boolean;
124
- /**
125
- * 0.42.0 (docs/admin-harmonization.md §6.9): a count of what waits behind the link —
126
- * kastlan's "Administration" with its To-triage count. A pill at the end of the
127
- * expanded link, a corner dot on the collapsed icon; `0`, `null` or none shows
128
- * nothing, and above 99 it reads "99+". The count joins the link's name, in
129
- * `badgeLabel`'s words when given ("Administration, 3 waiting").
130
- */
131
- badge?: number | null;
132
- /** The badge's words for a reader — `adminArea.waiting(count)`. Default: the number. */
133
- badgeLabel?: string;
134
- }
135
- | {
136
- kind: "links";
137
- key: string;
138
- icon: LucideIcon;
139
- /** The group's name — its `<nav>` label, and the collapsed icon's name ("Legal"). */
140
- label: string;
141
- /** The collapsed icon's target. Default the first link's `to`. */
142
- to?: string;
143
- links: { to: string; label: string; external?: boolean }[];
144
- };
145
-
146
- /**
147
- * The words of the phone bar's "More" cell and its sheet (0.32,
148
- * docs/text-size-harmonization.md §4 Navigation, §10.7) — the `appShellMore` namespace of
149
- * `<UiKitProvider labels>`, overridable per shell through `moreLabels`.
150
- */
151
- export interface AppShellMoreLabels {
152
- /** The cell's visible label, under its icon — one short word. */
153
- more: string;
154
- /** The sheet's heading, and so its accessible name. */
155
- moreTitle: string;
156
- /** 0.49: a parent entry's own page in the sheet its bar cell opens, above its pages,
157
- * when no page is that one — "Overview". */
158
- overview: string;
159
- }
160
-
161
- // Defined in ./app-shell-labels, which holds no component code (0.34, KS U8).
162
- export { DEFAULT_APP_SHELL_MORE_LABELS };
163
-
164
- /**
165
- * The most cells the phone bar holds, "More" included, per text size (0.49 KIT-6; 0.32
166
- * §10.7): five at Normal and Large, four at Extra large. Eight entries are four and
167
- * "More" at Normal; a fifth of a 390 px phone holds one word of the bar's type, and a
168
- * sixth no longer does once the type grows. A fifth of a 360 px phone at 150 % is 48 px
169
- * of Normal-sized room.
170
- */
171
- export const DEFAULT_MOBILE_MAX_ITEMS: Readonly<Record<TextSize, number>> = {
172
- normal: 5,
173
- large: 5,
174
- xlarge: 4,
175
- };
176
-
177
- /**
178
- * The phone bar's cells (0.49 KIT-6): every entry when there are at most `maxItems`,
179
- * else `maxItems − 1` entries and a "More" cell for the rest. Which entries stay is
180
- * `priority`'s call — lower first, entries without one after them in the list's order —
181
- * and both halves keep the list's order. Never fewer than one entry beside More.
182
- */
183
- export function pickMobileBar<T>(
184
- entries: readonly T[],
185
- maxItems: number,
186
- priority?: (entry: T) => number | undefined,
187
- ): { shown: T[]; hidden: T[] } {
188
- const cells = Math.max(1, Math.floor(maxItems));
189
- if (entries.length <= cells) return { shown: [...entries], hidden: [] };
190
- const keep = Math.max(1, cells - 1);
191
- const rank = (i: number) => priority?.(entries[i]!) ?? Number.POSITIVE_INFINITY;
192
- const chosen = new Set(
193
- entries
194
- .map((_, i) => i)
195
- .sort((a, b) => rank(a) - rank(b) || a - b)
196
- .slice(0, keep),
197
- );
198
- return {
199
- shown: entries.filter((_, i) => chosen.has(i)),
200
- hidden: entries.filter((_, i) => !chosen.has(i)),
201
- };
202
- }
203
-
204
- /** The bar's cell count at `size`: `mobileMaxItems`, else the default. */
205
- function mobileCells(maxItems: AppShellProps["mobileMaxItems"], size: TextSize): number {
206
- if (maxItems === undefined) return DEFAULT_MOBILE_MAX_ITEMS[size];
207
- return typeof maxItems === "number" ? maxItems : (maxItems[size] ?? DEFAULT_MOBILE_MAX_ITEMS[size]);
208
- }
209
-
210
- /** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a
211
- * production one, never under vitest. Read per call so a test can switch it on. */
212
- function devWarnings(): boolean {
213
- return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
214
- }
215
-
216
- const warnedNav = new Set<string>();
217
-
218
- function warnNavOnce(key: string, message: string): void {
219
- if (warnedNav.has(key)) return;
220
- warnedNav.add(key);
221
- console.warn(message);
222
- }
223
-
224
- /** The first word of a label, lower-cased: letters and digits up to the first other
225
- * character ("Import & Export" → "import", "Trans." → "trans"). */
226
- function firstWord(text: string): string {
227
- return (text.trim().match(/^[\p{L}\p{N}]+/u)?.[0] ?? "").toLocaleLowerCase();
228
- }
229
-
230
- /** Whether `short` names `label`: its first word is `label`'s first word or an
231
- * abbreviation of it ("Trans." for "Transactions"), or the other way round. */
232
- function shortLabelNamesLabel(short: string, label: string): boolean {
233
- const s = firstWord(short);
234
- const l = firstWord(label);
235
- return s === "" || l === "" || l.startsWith(s) || s.startsWith(l);
236
- }
237
-
238
- const TEXT_SIZE_WORDS: Record<TextSize, string> = { normal: "Normal", large: "Large", xlarge: "Extra large" };
239
-
240
- /** A development build's notes on `nav` (0.49 KIT-6): a `shortLabel` that is another word
241
- * than its `label` without `shortLabelIsAlias`. */
242
- function useNavDevWarnings(nav: AppShellNavItem[]): void {
243
- useEffect(() => {
244
- if (!devWarnings()) return;
245
- for (const item of nav) {
246
- if (item.shortLabel === undefined || item.shortLabelIsAlias) continue;
247
- if (shortLabelNamesLabel(item.shortLabel, item.label)) continue;
248
- const from = item.shortLabelFrom ?? "normal";
249
- const where = from === "normal" ? "at every text size" : `from ${TEXT_SIZE_WORDS[from]} text up`;
250
- warnNavOnce(
251
- `shortLabel|${item.to}|${item.shortLabel}|${item.label}`,
252
- `[ui-kit] AppShell nav "${item.to}": shortLabel "${item.shortLabel}" does not start with ` +
253
- `"${item.label.trim().split(/\s+/)[0]}", the first word of its label "${item.label}". The phone bar ` +
254
- `shows it ${where}, so the page has another name there than in the sidebar and its title. ` +
255
- "Shorten the label instead, or pass shortLabelIsAlias: true if the other word is meant.",
256
- );
257
- }
258
- }, [nav]);
259
- }
260
-
261
- /**
262
- * Exported and `<div>`-shaped. This is the outermost element of every consuming app, so
263
- * there is nothing above it to hang an id, a landmark label or a `data-tour` anchor on —
264
- * and nothing a consumer can wrap it in either, since the root owns the `h-dvh` and
265
- * `overflow-hidden` the whole layout is built on (audit §api-design).
266
- */
267
- export interface AppShellProps extends ComponentPropsWithoutRef<"div"> {
268
- /** Navigation entries — shared by the desktop sidebar and mobile bottom bar. */
269
- nav: AppShellNavItem[];
270
- /** The composed top bar (e.g. the shared TopBar with app-owned actions). */
271
- topBar: ReactNode;
272
- /** Main content — typically the router `<Outlet />`. */
273
- children: ReactNode;
274
- /** Desktop-only footer rendered below the content (hidden on mobile). */
275
- footer?: ReactNode;
276
- /** Extra sidebar content above the collapse toggle (e.g. a version link).
277
- * Receives the collapsed state so it can render compact vs. full. */
278
- sidebarFooter?: (collapsed: boolean) => ReactNode;
279
- /** The sidebar's bottom area as data — a version link, the legal links — drawn full
280
- * when expanded and as icon + tooltip when collapsed. Rendered above
281
- * `sidebarFooter`, which stays for anything the data cannot describe. */
282
- sidebarFooterItems?: AppShellSidebarFooterItem[];
283
- /** The router link for `sidebarFooterItems`. Default: the provider's
284
- * `linkComponent`, else react-router's `Link`. */
285
- renderLink?: KitLinkComponent;
286
- /** localStorage key for the persisted collapse state. */
287
- collapseStorageKey?: string;
288
- /** Default: `appShell.collapse` from the {@link UiKitProvider}, else English. */
289
- collapseLabel?: string;
290
- /** Default: `appShell.expand` from the {@link UiKitProvider}, else English. */
291
- expandLabel?: string;
292
- /** How an entry's `items` are revealed in the EXPANDED desktop sidebar.
293
- *
294
- * - `"flyout"` (default) — a panel beside the sidebar on hover/focus. Compact: the
295
- * sidebar stays one row per group however many pages a group holds.
296
- * - `"inline"` — a disclosure under the entry, the pattern of MUI's nested List and
297
- * most documentation sites. Every page is visible at once, and the group holding
298
- * the current page opens on its own, so the reader can see where they are without
299
- * hovering anything.
300
- *
301
- * The icon-only (collapsed) sidebar always uses the flyout: an indented list has no
302
- * room there. The mobile bottom bar ignores `items` in both modes. */
303
- subNav?: "flyout" | "inline";
304
- /** Accessible name of the inline disclosure button, given the group's label.
305
- * Only used with `subNav="inline"`. Its expanded state is carried by
306
- * `aria-expanded`, so one wording serves both directions. Default:
307
- * `appShell.toggleGroup` from the {@link UiKitProvider}, else English. */
308
- toggleGroupLabel?: (groupLabel: string) => string;
309
- /** Below `md`: show the current group's `items` as a scrollable row above the
310
- * bottom bar — the phone's counterpart of the sidebar's second level. Default true;
311
- * `false` keeps the single bar, where a group entry only links to its own `to`. */
312
- mobileSubNav?: boolean;
313
- /**
314
- * How the phone sub-nav lays out a group with more pages than fit on one row.
315
- * `"wrap"`: every page in view, on as many rows as it takes — the bar grows.
316
- * `"scroll"`: one row that scrolls sideways, faded at a cut edge, with the current
317
- * page scrolled into view. A long group took four rows (~130px) of a 390px screen
318
- * above the bottom bar (showcase audit); scrolling gives that back at the price of
319
- * pages hidden past the edge, so it is the app's call, per shell.
320
- *
321
- * Unset (0.32.2, kastlan's 0.32.1 report): `"wrap"` at Normal and `"scroll"` at Large
322
- * and Extra large, where a wrapped group of eight pages took six or seven rows and
323
- * covered the page — the other half of what `mobileMaxItems` does for the bar.
324
- */
325
- mobileSubNavLayout?: "wrap" | "scroll";
326
- /**
327
- * The most cells the phone bar holds, "More" included (0.49 KIT-6). When `nav` (after
328
- * `mobileHidden`) has more entries, the bar keeps `mobileMaxItems − 1` of them and a
329
- * last "More" cell opens a sheet with the rest, each group with its pages. Which
330
- * entries stay is the items' `mobilePriority`, else the list's order (so the app puts
331
- * its most used entries first, or orders by role: kastlan's manager and accountant see
332
- * different firsts).
333
- *
334
- * Default {@link DEFAULT_MOBILE_MAX_ITEMS}: 5 at Normal and Large, 4 at Extra large —
335
- * eight entries are four and "More". A number replaces the default at every size;
336
- * `{ xlarge: 5 }` replaces it at one size only. Never fewer than one entry beside More.
337
- */
338
- mobileMaxItems?: number | Partial<Record<TextSize, number>>;
339
- /** The More cell's words over the provider's `appShellMore`. */
340
- moreLabels?: Partial<AppShellMoreLabels>;
341
- /**
342
- * Lay the shell out inside its PARENT'S box instead of the viewport: `h-full` rather
343
- * than `h-dvh`, its own scroll container at every width, a bottom bar pinned to the
344
- * shell rather than the window, `--app-nav-h` published on the shell alone, and no
345
- * persisted collapse state unless `collapseStorageKey` is passed. For an app preview,
346
- * a device frame, a docs example.
347
- *
348
- * Implied for a shell rendered inside another AppShell, which is the case this
349
- * exists for: the inner one used to publish its own nav height on `<html>`, fight
350
- * the outer one over it, and remove it on unmount from under the outer app.
351
- */
352
- embedded?: boolean;
353
- /** The shell's root, where the rest of the props go (0.34, API-2, refactor audit
354
- * 2026-10) — merged with its own ref, which `--app-nav-h` is published on. */
355
- ref?: Ref<HTMLDivElement>;
356
- }
357
-
358
- /** True below an AppShell — how a nested shell knows it is one. */
359
- const AppShellNesting = createContext(false);
360
-
361
- /**
362
- * The bottom nav's own height, published as `--app-nav-h` so anything that has to sit
363
- * ON it can stop guessing.
364
- *
365
- * Keksdose live #314 is what this is for. Its rework — *"Stick it to the bottom
366
- * touching the bottom icon bar with budget, accounts, … Currently there is a small gap
367
- * which confuses"* — was a receipt footer pinned at `bottom-20`, because `<main>`
368
- * reserves `pb-20` for this nav and 20 looked like the nav's height. It is not: the
369
- * padding is generous CLEARANCE, and the nav is content-sized (icon + label + `py-2`,
370
- * about 56px), so the footer floated ~24px above the bar.
371
- *
372
- * Measured rather than named, because the height is not a constant anyone owns: it is
373
- * an icon, a translated label that can wrap or truncate, and whatever the platform does
374
- * with the safe-area inset. A hard-coded `bottom-14` would be the same guess one number
375
- * lower, and it would be wrong again the first time a label needed two lines.
376
- *
377
- * `0px` when the nav is not rendered — above `md` it is `display:none`, which observes
378
- * as a zero box — so a consumer can write `bottom-[var(--app-nav-h,0px)]` once and get
379
- * the right answer at both widths without a breakpoint of its own.
380
- */
381
- function useNavHeightVar(
382
- ref: React.RefObject<HTMLElement | null>,
383
- shellRef: React.RefObject<HTMLElement | null>,
384
- embedded: boolean,
385
- ): void {
386
- // Re-run when the breakpoint flips. A ResizeObserver SKIPS an element that is not
387
- // being rendered, so the nav going `display:none` above `md` fires no callback and
388
- // would leave the last phone height published — a desktop footer would then sit 56px
389
- // off the bottom after a live resize. `useBreakpoint("md")` is the nav's own `md:hidden`
390
- // at the text size in force (0.32), so the two cannot disagree about where the
391
- // boundary is — at Extra large both move to 72rem.
392
- const isMdUp = useBreakpoint("md", false);
393
- useEffect(() => {
394
- const node = ref.current;
395
- const shell = shellRef.current;
396
- // Always on the shell's own root, so everything inside it — a nested shell's
397
- // content included — reads the nearest bar's height. On <html> as well only for
398
- // the OUTERMOST shell, for what it portals to <body> (a sheet docking on the bar).
399
- // The DOM check backs up the context for a shell mounted in a separate React root.
400
- const global = !embedded && !shell?.parentElement?.closest("[data-app-shell]");
401
- const targets = [shell, global ? document.documentElement : null].filter((el): el is HTMLElement => el !== null);
402
- // `getBoundingClientRect().height`, rounded DOWN — not `offsetHeight`, and not
403
- // ceil. Measured on the running app at 406x816: the bar is **55.5px** tall, and
404
- // both `offsetHeight` and `Math.ceil` answer 56. That is why live #314 round four
405
- // was *"No change. Still a pixel inbetween"* — the previous fix was arithmetically
406
- // a no-op on the very value it was meant to correct.
407
- //
408
- // 56 is the wrong side. A sticky element with `bottom: X` puts its bottom edge X
409
- // above the viewport bottom, and the bar's TOP edge is at its own height: so
410
- // X = 56 lands the footer at y=760.0 against a bar starting at y=760.5, i.e. half
411
- // a CSS pixel of page showing through — about 1.4 device pixels on his phone,
412
- // which is exactly the hairline in the screenshot.
413
- //
414
- // The two errors are not symmetric, and I had them backwards the first time.
415
- // Too SMALL tucks the sticky element under a bar that is opaque and painted above
416
- // it (`z-30` against `z-10`): invisible. Too LARGE opens the seam. So floor, and
417
- // consumers subtract a further pixel (see `invoice-line-totals.tsx`) for the case
418
- // where the height lands on a whole pixel and floor leaves no overlap at all.
419
- const publish = () => {
420
- const value = `${node ? Math.floor(node.getBoundingClientRect().height) : 0}px`;
421
- for (const el of targets) el.style.setProperty("--app-nav-h", value);
422
- };
423
- publish();
424
- // The observer is the refinement, not the mechanism: `publish()` above is already
425
- // right for a static nav, and jsdom has no ResizeObserver unless a test stubs one.
426
- // ONE cleanup for every branch, though — an early `return` on the no-observer path
427
- // left the variable behind on unmount, which a stale `bottom:` offset on whatever
428
- // rendered next would have inherited.
429
- const ro = node && typeof ResizeObserver !== "undefined" ? new ResizeObserver(publish) : null;
430
- if (ro && node) ro.observe(node);
431
- return () => {
432
- ro?.disconnect();
433
- for (const el of targets) el.style.removeProperty("--app-nav-h");
434
- };
435
- }, [ref, shellRef, embedded, isMdUp]);
436
- }
437
-
438
- /**
439
- * The responsive application shell: a top bar, a collapsible desktop sidebar
440
- * (icon-only when collapsed, with tooltips), a mobile bottom nav bar, the main
441
- * content area, and an optional desktop footer. Router-aware via react-router
442
- * `NavLink`; domain-free — nav items, brand, footer and the top bar's actions
443
- * are all supplied by the app.
444
- */
445
- export function AppShell({
446
- nav,
447
- topBar,
448
- children,
449
- footer,
450
- sidebarFooter,
451
- sidebarFooterItems,
452
- renderLink,
453
- collapseStorageKey: collapseStorageKeyProp,
454
- collapseLabel,
455
- expandLabel,
456
- subNav = "flyout",
457
- mobileSubNav = true,
458
- mobileSubNavLayout: mobileSubNavLayoutProp,
459
- mobileMaxItems,
460
- moreLabels,
461
- toggleGroupLabel,
462
- embedded: embeddedProp,
463
- className,
464
- ref,
465
- ...rest
466
- }: AppShellProps) {
467
- const embedded = useContext(AppShellNesting) || !!embeddedProp;
468
- const { size: textSize } = useTextSize();
469
- const large = textSize !== "normal";
470
- const mobileSubNavLayout = mobileSubNavLayoutProp ?? (large ? "scroll" : "wrap");
471
- // The bottom bar's cells: every entry the phone does not reach another way, up to the
472
- // text size's limit, More included; the rest go under More (§10.7, KIT-6).
473
- const maxItems = mobileCells(mobileMaxItems, textSize);
474
- const { shown: barNav, hidden: moreNav } = pickMobileBar(
475
- nav.filter((item) => !item.mobileHidden),
476
- maxItems,
477
- (item) => item.mobilePriority,
478
- );
479
- useNavDevWarnings(nav);
480
- // An embedded shell sharing the default key would overwrite the outer app's sidebar
481
- // preference with its own — so it persists only when given a key of its own.
482
- const collapseStorageKey = collapseStorageKeyProp ?? (embedded ? null : "appLayout.sidebarCollapsed");
483
- // The three props are per-shell overrides of the provider's `appShell`; one left
484
- // undefined falls through to the provider rather than to English.
485
- const labels = useKitLabels("appShell", DEFAULT_APP_SHELL_LABELS, {
486
- collapse: collapseLabel,
487
- expand: expandLabel,
488
- toggleGroup: toggleGroupLabel,
489
- });
490
- // Through the guarded helpers, not `window.localStorage` directly. The read runs
491
- // inside a `useState` initialiser — i.e. during render of the top-level shell — and
492
- // `localStorage` THROWS where site data is blocked (Safari private browsing, a
493
- // partitioned webview). Unguarded, that was not a lost sidebar preference: nothing
494
- // in the application mounted at all.
495
- const [collapsed, setCollapsed] = useState(
496
- () => collapseStorageKey !== null && readStored(collapseStorageKey) === "1",
497
- );
498
- // The inline sidebar is an ACCORDION: one group open at a time, keyed by its `to`.
499
- // Held here rather than per group so opening one can close the other — two lists
500
- // open at once pushed every entry below them off a laptop screen.
501
- const [openGroup, setOpenGroup] = useState<string | null>(null);
502
- // Publishes `--app-nav-h` for anything that has to sit on the bottom bar.
503
- const navRef = useRef<HTMLDivElement>(null);
504
- const rootRef = useRef<HTMLDivElement>(null);
505
- const setRoot = useMemo(() => mergeRefs(rootRef, ref), [ref]);
506
- useNavHeightVar(navRef, rootRef, embedded);
507
- useEffect(() => {
508
- if (collapseStorageKey !== null) writeStored(collapseStorageKey, collapsed ? "1" : "0");
509
- }, [collapsed, collapseStorageKey]);
510
- // Reset this device's settings (docs/settings-harmonization.md §11.4): the sidebar
511
- // unfolds, live, and the effect above writes "0". An embedded shell without a key of
512
- // its own stores nothing and joins nothing.
513
- useEffect(() => {
514
- if (collapseStorageKey === null) return;
515
- return registerDeviceSetting({ key: collapseStorageKey, kind: "view", reset: () => setCollapsed(false) });
516
- }, [collapseStorageKey]);
517
- // §4: at Large and Extra large the sidebar never collapses to icons only — a glyph
518
- // with its name in a tooltip is the very thing a reader who asked for bigger type
519
- // cannot use. The persisted preference is left alone, so Normal gets it back, and the
520
- // toggle goes while it could not do anything.
521
- const iconOnly = collapsed && !large;
522
-
523
- return (
524
- // The caller's attributes go on the shell's own root, never on the `data-tour="nav"`
525
- // wrappers below — those are the kit's anchors for its own guided tour (#313/#322),
526
- // and a second element answering to the same selector is a tour that highlights the
527
- // wrong thing or nothing at all.
528
- <AppShellNesting.Provider value={true}>
529
- <div
530
- {...rest}
531
- ref={setRoot}
532
- data-app-shell=""
533
- className={cn(
534
- "relative flex flex-col",
535
- embedded
536
- ? // `contain: layout` makes this box the containing block for the `fixed`
537
- // bottom bar below, so it pins to the shell rather than to the window.
538
- "h-full min-h-0 overflow-hidden [contain:layout]"
539
- : "min-h-screen md:h-dvh md:overflow-hidden",
540
- className,
541
- )}
542
- >
543
- {/* §4: "the top bar keeps its essential icons" — its IconButtons stay icons at
544
- Large (their labels the names and tooltips) and a locked one keeps its reason
545
- in the tooltip: a bar 3rem tall has no room for words under every glyph. */}
546
- <CompactControls>{topBar}</CompactControls>
547
- <div className="flex flex-1 min-h-0">
548
- <aside
549
- className={cn(
550
- // `border-e`: the sidebar sits at the START, which is the right in RTL.
551
- "hidden md:flex md:flex-col border-e border-subtle bg-surface transition-[width] duration-200 ease-out overflow-hidden",
552
- // Embedded, the row it sits in is already the shell's height; the sticky
553
- // offsets below are measured against the window's top bar.
554
- embedded ? "md:self-stretch" : "md:sticky md:top-12 md:self-start md:h-[calc(100vh-3rem)]",
555
- iconOnly ? "md:w-14" : "md:w-60",
556
- )}
557
- >
558
- {/* The `<nav>` is flex-1 so it fills the sidebar's height (with empty
559
- space below the items); the data-tour marker goes on the INNER,
560
- fit-content wrapper so the guided-tour spotlight hugs the actual nav
561
- items instead of the whole tall column (feedback #322). The marker
562
- also tags the mobile bottom bar below; the tour targets
563
- `[data-tour="nav"]` and resolves to whichever is visible (#313). */}
564
- <nav className="flex-1 px-2 py-3 overflow-y-auto overflow-x-hidden">
565
- <div data-tour="nav" className="space-y-1">
566
- {nav.map((item) => (
567
- <SidebarNavItem
568
- key={item.to}
569
- item={item}
570
- collapsed={iconOnly}
571
- inline={subNav === "inline"}
572
- toggleGroupLabel={labels.toggleGroup}
573
- openGroup={openGroup}
574
- setOpenGroup={setOpenGroup}
575
- />
576
- ))}
577
- </div>
578
- </nav>
579
- {!!sidebarFooterItems?.length && (
580
- <SidebarFooterItems items={sidebarFooterItems} collapsed={iconOnly} renderLink={renderLink} />
581
- )}
582
- {sidebarFooter?.(iconOnly)}
583
- {/* No toggle at Large: the sidebar cannot collapse there (see `iconOnly`). */}
584
- {!large && (
585
- <div className="border-t border-subtle p-2">
586
- {collapsed ? (
587
- <Tooltip label={labels.expand} side="end" portal className="block">
588
- <button
589
- type="button"
590
- onClick={() => setCollapsed(false)}
591
- aria-label={labels.expand}
592
- className="flex w-full items-center justify-center min-h-9 rounded-md text-muted hover:bg-hover hover:text-secondary"
593
- >
594
- <PanelLeftOpen className="size-4 shrink-0 rtl:-scale-x-100" />
595
- </button>
596
- </Tooltip>
597
- ) : (
598
- <button
599
- type="button"
600
- onClick={() => setCollapsed(true)}
601
- aria-label={labels.collapse}
602
- className="flex w-full items-center gap-3 min-h-9 px-3 py-2 rounded-md text-sm text-muted hover:bg-hover hover:text-secondary"
603
- >
604
- <PanelLeftClose className="size-4 shrink-0 rtl:-scale-x-100" />
605
- {/* Never drawn at Large (the toggle is not), so the class matters at
606
- Normal only — the kit's one name for it all the same (round 0.34 T1). */}
607
- <span className="truncate-until-large">{labels.collapse}</span>
608
- </button>
609
- )}
610
- </div>
611
- )}
612
- </aside>
613
-
614
- {/* Content column beside the sidebar. main grows to fill so the footer
615
- sits at the bottom — at the viewport edge when content is short,
616
- beside the sidebar rather than under it. */}
617
- <div className="flex flex-1 flex-col min-w-0 md:min-h-0">
618
- {/* `scrollbar-gutter: stable` reserves the scrollbar's track whether or
619
- not it is currently needed. Without it this element — the app's only
620
- scroll container from md up — narrows its own client box by the
621
- scrollbar width the moment a page's content outgrows it, and widens
622
- it again when the next page fits. Every centred `mx-auto` container
623
- inside then jumps sideways by half a scrollbar, and any fluid-width
624
- content changes width outright. Keksdose feedback #403 caught it as
625
- "slight width increase/decrease" when switching to the notifications
626
- settings section and back: that section is the only one tall enough
627
- to scroll. Platform-dependent, which is why it is easy to miss —
628
- overlay scrollbars (macOS, most Linux builds) take no layout space,
629
- classic ones (Windows) take ~15px.
630
-
631
- `both-edges`, not the bare `stable`: a one-sided reservation keeps the
632
- width stable but moves the middle. On a page short enough not to
633
- scroll, nothing is painted into the reserved track, so every centred
634
- `mx-auto` container sits half a scrollbar left of the optical centre
635
- and every full-bleed child stops ~15px short on the right. Keksdose
636
- feedback #491 (/settings#data) and #498 (/budgets) both reported it as
637
- "the left boundary looks smaller / cut off compared to the right" on
638
- the one element per page whose frame is a saturated colour — the rose
639
- destructive-action cards. Reserving the track on both edges keeps the
640
- content centred whether or not the scrollbar is showing. */}
641
- {/* `relative` makes <main> the containing block for everything absolutely
642
- positioned inside it. Tailwind's `sr-only` is `position:absolute`; with no
643
- positioned ancestor it resolves against the INITIAL containing block, is
644
- laid out at its offset from the top of the DOCUMENT, and grows
645
- `<html>`'s scroll height past the viewport — a second scrollbar beside this
646
- one that scrolls into empty space. Guarding it per component (see
647
- sr-only-containment.test) could never be complete: any page's own
648
- `sr-only` span re-opened it. Here it is closed for every child at once. */}
649
- {/* Below `md` the bottom room is the nav plus the larger of 1.5rem and what the
650
- floating launchers take above it (`--app-float-clearance`, published by
651
- every kit FAB, FloatingPanel and FloatingActionGroup while mounted — keksdose
652
- dev#612: the last row ended under the help launcher). From `md` up, where
653
- there is no nav, the launchers' room alone (0.49 KIT-11): the last row
654
- scrolls clear of a corner button there too. */}
655
- <main
656
- className={cn(
657
- "relative flex-1 max-w-full overflow-x-clip pb-[calc(var(--app-nav-h,4rem)+max(1.5rem,var(--app-float-clearance,0px)))] md:pb-[var(--app-float-clearance,0px)] md:min-h-0 md:overflow-y-auto md:[scrollbar-gutter:stable_both-edges]",
658
- // The list page's fill (0.40, docs/page-layout-harmonization.md §1.3): while
659
- // a `fillHeight` DataTable is inside, a flex column — a banner above the
660
- // page keeps its own height and the page (a `FillHeight`) takes the rest,
661
- // so `<main>` no longer overflows by the banner. Its children do not shrink
662
- // (a banner of a fixed height or `overflow-hidden` would): the page grows
663
- // into the rest from a zero basis, and a window too short for the page's
664
- // floor scrolls `<main>`. Every other page lays out as before. Below `md`
665
- // the document scrolls and nothing fills.
666
- // The children's `shrink-0` carries no `:has`: `flex-shrink` does nothing
667
- // outside a flex container, and a `:has(…) > *` rule cost a style recalc of
668
- // the whole page on every DOM insertion under it (17–45 ms per scrolled step
669
- // of a `virtualize` table, 0.48.2). `[&&>*]` doubles the class, so it stays
670
- // as specific as FillHeight's `has-[…]:flex-1` (whose `flex: 1` would set
671
- // shrink 1) and, emitted after it, still wins.
672
- "md:has-[[data-fill-height]]:flex md:has-[[data-fill-height]]:flex-col md:[&&>*]:shrink-0",
673
- // Embedded there is no document scroll to lean on below `md` either.
674
- embedded && "min-h-0 overflow-y-auto",
675
- )}
676
- >
677
- {children}
678
- </main>
679
- {footer}
680
- </div>
681
- </div>
682
-
683
- {/* The phone navigation: the group bar, and above it — when the current page
684
- belongs to a group with pages of its own — the row of those pages. The pair
685
- is the phone's version of the sidebar's two levels; without the upper row a
686
- group's `items` were simply unreachable below `md`. ONE measured box, so
687
- `--app-nav-h` covers both rows and whatever docks on the bar sits on the
688
- whole of it. */}
689
- <div ref={navRef} className="md:hidden fixed bottom-0 inset-x-0 z-30 border-t border-subtle bg-surface">
690
- {mobileSubNav && <MobileSubNav nav={nav} layout={mobileSubNavLayout} />}
691
- <nav
692
- data-tour="nav"
693
- className="grid"
694
- style={{ gridTemplateColumns: `repeat(${barNav.length + (moreNav.length ? 1 : 0)}, minmax(0, 1fr))` }}
695
- >
696
- {barNav.map((item) =>
697
- item.items?.length ? (
698
- <MobileGroupCell key={item.to} item={item} labels={moreLabels} />
699
- ) : (
700
- <MobileNavItem key={item.to} item={item} />
701
- ),
702
- )}
703
- {moreNav.length > 0 && <MobileMoreCell hidden={moreNav} labels={moreLabels} />}
704
- </nav>
705
- </div>
706
- </div>
707
- </AppShellNesting.Provider>
708
- );
709
- }
710
-
711
- const routerLink: KitLinkComponent = ({ href, ...p }) => <Link to={href} {...p} />;
712
- const plainLink: KitLinkComponent = ({ children, ...p }) => <a {...p}>{children}</a>;
713
-
714
- const FOOTER_ICON_LINK =
715
- "flex min-h-8 items-center justify-center rounded-md text-muted hover:bg-hover hover:text-secondary";
716
-
717
- /** A footer link's count as drawn: nothing for none, "99+" past 99. */
718
- function footerBadgeText(badge: number | null | undefined): string | null {
719
- if (typeof badge !== "number" || !(badge > 0)) return null;
720
- return badge > 99 ? "99+" : String(Math.floor(badge));
721
- }
722
-
723
- /** The count's pill — the solid "count" look of `Chip` (warning, the pending count's
724
- * tone, as `FloatingActionGroup`'s badge), sized for the footer's small text. */
725
- const FOOTER_BADGE =
726
- "inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-warning px-1 text-micro font-semibold leading-4 tabular-nums text-warning-contrast";
727
-
728
- /** Calls the link renderer as a component, so a router link's hooks are its own. */
729
- function RenderedLink({ render, ...props }: KitLinkProps & { render: KitLinkComponent }) {
730
- return render(props);
731
- }
732
-
733
- /** The sidebar's bottom area, from {@link AppShellSidebarFooterItem}s. */
734
- function SidebarFooterItems({
735
- items,
736
- collapsed,
737
- renderLink,
738
- }: {
739
- items: AppShellSidebarFooterItem[];
740
- collapsed: boolean;
741
- renderLink?: KitLinkComponent;
742
- }) {
743
- const kitLink = useKitLink();
744
- const linkTo = (to: string, external: boolean | undefined, { children, ...props }: Omit<KitLinkProps, "href">) => {
745
- // The kit's one link rule (`pickLinkRenderer`): an external `to` or an in-page
746
- // `#anchor` is a plain `<a>` even without `external`; only `external` opens a tab.
747
- const InApp = pickLinkRenderer(renderLink, kitLink ?? routerLink, to) ?? plainLink;
748
- return external ? (
749
- <a href={to} target="_blank" rel="noopener noreferrer" {...props}>
750
- {children}
751
- </a>
752
- ) : (
753
- <RenderedLink render={InApp} href={to} {...props}>
754
- {children}
755
- </RenderedLink>
756
- );
757
- };
758
-
759
- return (
760
- <div data-slot="sidebar-footer-items" className={cn("px-2 py-1", collapsed ? "space-y-1" : "space-y-1.5")}>
761
- {items.map((item) => {
762
- if (collapsed) {
763
- const to = item.kind === "links" ? (item.to ?? item.links[0]?.to) : item.to;
764
- if (!to) return null;
765
- const external = item.kind === "links" ? item.links.find((l) => l.to === to)?.external : item.external;
766
- const count = item.kind === "links" ? null : footerBadgeText(item.badge);
767
- const spoken = count === null || item.kind === "links" ? null : (item.badgeLabel ?? count);
768
- return (
769
- <Tooltip key={item.key} label={item.label} side="end" portal className="block">
770
- {linkTo(to, external, {
771
- "aria-label": spoken === null ? item.label : `${item.label}, ${spoken}`,
772
- className: cn(FOOTER_ICON_LINK, count !== null && "relative"),
773
- children: (
774
- <>
775
- <item.icon className="size-4" aria-hidden />
776
- {count !== null && (
777
- <span aria-hidden data-badge="" className={cn(FOOTER_BADGE, "absolute -top-0.5 end-0.5")}>
778
- {count}
779
- </span>
780
- )}
781
- </>
782
- ),
783
- })}
784
- </Tooltip>
785
- );
786
- }
787
- if (item.kind === "links") {
788
- return (
789
- <nav
790
- key={item.key}
791
- aria-label={item.label}
792
- className="flex flex-wrap justify-center gap-x-3 gap-y-0.5 text-caption text-muted"
793
- >
794
- {item.links.map((l) => (
795
- <span key={l.to}>
796
- {linkTo(l.to, l.external, {
797
- className: "rounded-sm hover:text-secondary hover:underline",
798
- children: l.label,
799
- })}
800
- </span>
801
- ))}
802
- </nav>
803
- );
804
- }
805
- const count = footerBadgeText(item.badge);
806
- const link = linkTo(item.to, item.external, {
807
- className: cn(FOOTER_ICON_LINK, "gap-1.5 text-xs font-medium"),
808
- children: (
809
- <>
810
- <item.icon className="size-3.5 shrink-0" aria-hidden />
811
- {/* Wraps from Large up (text size §10.17, round 0.34 T1): the link is
812
- `min-h-8`, so it grows a line rather than cut "Feedb…". */}
813
- <span className="min-w-0 truncate-until-large">{item.text ?? item.label}</span>
814
- {count !== null && (
815
- <>
816
- <span aria-hidden data-badge="" className={FOOTER_BADGE}>
817
- {count}
818
- </span>
819
- <span className="sr-only">, {item.badgeLabel ?? count}</span>
820
- </>
821
- )}
822
- </>
823
- ),
824
- });
825
- return item.hint ? (
826
- <Tooltip key={item.key} label={item.hint} side="top" portal className="block">
827
- {link}
828
- </Tooltip>
829
- ) : (
830
- <div key={item.key}>{link}</div>
831
- );
832
- })}
833
- </div>
834
- );
835
- }
836
-
837
- /**
838
- * A bar cell's look, shared by the entries and the More cell. The active page needs a
839
- * clear marker, not just a subtle text shade (feedback #327): brand-accent icon + label,
840
- * bolder weight, and a top accent bar spanning the cell so it reads at a glance.
841
- */
842
- const mobileCellClass = (active: boolean) =>
843
- cn(
844
- // `h-full`: the grid row stretches every cell to the tallest one's height, so a
845
- // label on two lines never leaves its neighbours a different shape.
846
- "relative flex h-full w-full flex-col items-center justify-start py-2 text-caption gap-0.5 transition-colors",
847
- "before:absolute before:content-[''] before:inset-x-4 before:top-0 before:h-0.5 before:rounded-full before:transition-colors",
848
- active ? "font-semibold text-brand before:bg-brand" : "text-muted before:bg-transparent",
849
- );
850
-
851
- /**
852
- * A bar cell's label (0.32, §10.7): up to TWO lines, never an ellipsis on one. At Large
853
- * a cell is a fifth of a phone holding type a quarter bigger, and one line truncated
854
- * "Transactions" to "Trans…" in every app (Kurvenschmiede). Two lines and then a clamp,
855
- * centred, a long word broken (and hyphenated where the page's `lang` allows) rather
856
- * than overflowing into the next cell. `shortLabel` still wins where the app has one.
857
- *
858
- * `data-truncate-ok` (text size §10.17, round 0.34 T1): the clamp is that decision, not
859
- * an accident — the size sweep finds "Sowing calen…" in a 65 px cell at 360 px / Extra
860
- * large, and a third line would make every cell of the bar taller for one long word.
861
- * The whole label stays the link's accessible name; an app with a long one gives the bar
862
- * a `shortLabel`.
863
- */
864
- function MobileCellLabel({ children }: { children: ReactNode }) {
865
- return (
866
- <span
867
- data-truncate-ok=""
868
- className="line-clamp-2 max-w-full break-words px-0.5 text-center leading-tight hyphens-auto"
869
- >
870
- {children}
871
- </span>
872
- );
873
- }
874
-
875
- /** One cell of the phone's group bar. Marked while the group's own page OR any of its
876
- * pages is current — NavLink alone only knows its own `to`, which left the bar with
877
- * nothing marked on every sub-page (the same defect the sidebar had). */
878
- function MobileNavItem({ item }: { item: AppShellNavItem }) {
879
- const subActive = useSubItemActive(item);
880
- const label = useCellLabel(item);
881
- return (
882
- <NavLink
883
- to={item.to}
884
- end={item.end ?? true}
885
- data-tour={item.dataTour}
886
- className={({ isActive }) => mobileCellClass(isActive || subActive)}
887
- >
888
- <span className="relative">
889
- <item.icon className="size-5" />
890
- </span>
891
- <MobileCellLabel>{label}</MobileCellLabel>
892
- </NavLink>
893
- );
894
- }
895
-
896
- /** The bar cell's label: `shortLabel` from its text size up (0.48), else `label`. */
897
- function useCellLabel(item: AppShellNavItem): string {
898
- const { size } = useTextSize();
899
- return item.shortLabel !== undefined &&
900
- TEXT_SIZES.indexOf(size) >= TEXT_SIZES.indexOf(item.shortLabelFrom ?? "normal")
901
- ? item.shortLabel
902
- : item.label;
903
- }
904
-
905
- /** A link in one of the phone's sheets — an entry (`level="entry"`) or a page under one. */
906
- function SheetLink({
907
- to,
908
- end,
909
- icon: Icon,
910
- label,
911
- onClick,
912
- level,
913
- }: {
914
- to: string;
915
- end?: boolean;
916
- icon: LucideIcon;
917
- label: string;
918
- onClick: () => void;
919
- level: "entry" | "page";
920
- }) {
921
- return (
922
- <NavLink
923
- to={to}
924
- end={end ?? true}
925
- onClick={onClick}
926
- className={({ isActive }) =>
927
- cn(
928
- level === "entry"
929
- ? "flex min-h-11 items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors"
930
- : "flex min-h-11 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",
931
- isActive
932
- ? "bg-inverse text-inverse"
933
- : level === "entry"
934
- ? "text-primary hover:bg-hover"
935
- : "text-secondary hover:bg-hover",
936
- )
937
- }
938
- >
939
- <Icon aria-hidden className={level === "entry" ? "size-5 shrink-0" : "size-4 shrink-0 opacity-70"} />
940
- <span className="min-w-0 break-words leading-snug">{label}</span>
941
- </NavLink>
942
- );
943
- }
944
-
945
- /**
946
- * A bar cell for an entry with pages (0.49 KIT-6): on a phone the entry opens a sheet of
947
- * its pages instead of linking to its own `to` — a tap on "Reports" used to land on the
948
- * overview with the pages one more step away, in the row above the bar, which only
949
- * appears once you are in the group. The sheet lists the entry's own page first, as
950
- * "Overview", when none of the pages is that page. Marked while the entry's page or one
951
- * of its pages is current, as the link was.
952
- *
953
- * Tours find the entry by `a[href]` or its `dataTour`, as on the More cell: an inert,
954
- * pointer-transparent anchor over the button carries them (see {@link MobileMoreCell}).
955
- */
956
- function MobileGroupCell({
957
- item,
958
- labels: labelsProp,
959
- }: {
960
- item: AppShellNavItem;
961
- labels?: LabelOverride<AppShellMoreLabels>;
962
- }) {
963
- const labels: AppShellMoreLabels = useKitLabels("appShellMore", DEFAULT_APP_SHELL_MORE_LABELS, labelsProp);
964
- const { pathname } = useLocation();
965
- const [open, setOpen] = useState(false);
966
- const label = useCellLabel(item);
967
- const active = entryIsCurrent(item, pathname);
968
- const close = () => setOpen(false);
969
- const pages = item.items ?? [];
970
- const ownPageListed = pages.some((sub) => sub.to === item.to);
971
- return (
972
- <div data-slot="app-shell-group" className="relative min-w-0">
973
- <button
974
- type="button"
975
- aria-haspopup="dialog"
976
- aria-expanded={open}
977
- // The cell may show a shortLabel; the button's name is the entry's.
978
- aria-label={label === item.label ? undefined : item.label}
979
- onClick={() => setOpen(true)}
980
- className={mobileCellClass(active)}
981
- >
982
- <span className="relative">
983
- <item.icon aria-hidden className="size-5" />
984
- </span>
985
- <MobileCellLabel>{label}</MobileCellLabel>
986
- </button>
987
- <TourAnchor to={item.to} dataTour={item.dataTour} />
988
- {pages
989
- .filter((sub) => sub.to !== item.to)
990
- .map((sub) => (
991
- <TourAnchor key={sub.to} to={sub.to} />
992
- ))}
993
- <DialogFrame open={open} onClose={close} title={item.label} closeButton bodyClassName="space-y-1">
994
- <ul className="space-y-1">
995
- {!ownPageListed && (
996
- <li>
997
- <SheetLink
998
- to={item.to}
999
- end={item.end}
1000
- icon={item.icon}
1001
- label={labels.overview}
1002
- onClick={close}
1003
- level="entry"
1004
- />
1005
- </li>
1006
- )}
1007
- {pages.map((sub) => (
1008
- <li key={sub.to}>
1009
- <SheetLink to={sub.to} end={sub.end} icon={sub.icon} label={sub.label} onClick={close} level="entry" />
1010
- </li>
1011
- ))}
1012
- </ul>
1013
- </DialogFrame>
1014
- </div>
1015
- );
1016
- }
1017
-
1018
- /** Whether the current route is `item`'s page or one of its pages. */
1019
- function entryIsCurrent(item: AppShellNavItem, pathname: string): boolean {
1020
- return (
1021
- !!matchPath({ path: item.to, end: item.end ?? true }, pathname) ||
1022
- !!item.items?.some((sub) => matchPath({ path: sub.to, end: sub.end ?? true }, pathname))
1023
- );
1024
- }
1025
-
1026
- /** A hidden entry's link, resolved as the router would write it (basename included). */
1027
- function TourAnchor({ to, dataTour }: { to: string; dataTour?: string }) {
1028
- const href = useHref(to);
1029
- return (
1030
- // eslint-disable-next-line jsx-a11y/anchor-has-content -- a tour anchor, aria-hidden and inert; the More button under it is the control (see MobileMoreCell)
1031
- <a
1032
- href={href}
1033
- data-tour={dataTour}
1034
- // Not a control: no name, no tab stop, no pointer — the More button under it
1035
- // takes the tap. It exists only to be FOUND (see MobileMoreCell).
1036
- aria-hidden
1037
- tabIndex={-1}
1038
- className="pointer-events-none absolute inset-0"
1039
- />
1040
- );
1041
- }
1042
-
1043
- /**
1044
- * The bar's "More" cell (0.32, docs/text-size-harmonization.md §4, §10.7): the entries
1045
- * past `mobileMaxItems`, in a sheet — each entry as a link, and under a group its pages,
1046
- * since a group the bar no longer shows has no sub-row to reach them by either. Marked
1047
- * active while the current page is one of them, as the cell it replaced would be.
1048
- *
1049
- * **Tour anchors.** A guided tour finds a nav entry by `[data-tour="nav"] a[href="/x"]`
1050
- * (keksdose's tours) or by the entry's own `dataTour`. An entry under More has no link
1051
- * in the bar, and the sidebar's copy is `display:none` on a phone, so such a step would
1052
- * spotlight nothing. So the More cell carries the hidden entries' links — one `<a>` per
1053
- * entry and per page of a hidden group, with the entry's `href` and `dataTour` — laid
1054
- * over the cell (`absolute inset-0`): both selectors resolve to a visible box, and that
1055
- * box is the More cell, which is where the reader has to go next. They are
1056
- * `aria-hidden`, out of the tab order and pointer-transparent, so neither a screen
1057
- * reader nor a tap ever meets them; the button under them is the one control.
1058
- */
1059
- function MobileMoreCell({
1060
- hidden,
1061
- labels: labelsProp,
1062
- }: {
1063
- hidden: AppShellNavItem[];
1064
- labels?: LabelOverride<AppShellMoreLabels>;
1065
- }) {
1066
- const labels: AppShellMoreLabels = useKitLabels("appShellMore", DEFAULT_APP_SHELL_MORE_LABELS, labelsProp);
1067
- const { pathname } = useLocation();
1068
- const [open, setOpen] = useState(false);
1069
- const active = hidden.some((item) => entryIsCurrent(item, pathname));
1070
- const close = () => setOpen(false);
1071
- return (
1072
- <div data-slot="app-shell-more" className="relative min-w-0">
1073
- <button
1074
- type="button"
1075
- aria-haspopup="dialog"
1076
- aria-expanded={open}
1077
- onClick={() => setOpen(true)}
1078
- className={mobileCellClass(active)}
1079
- >
1080
- <span className="relative">
1081
- <MoreHorizontal aria-hidden className="size-5" />
1082
- </span>
1083
- <MobileCellLabel>{labels.more}</MobileCellLabel>
1084
- </button>
1085
- {hidden.flatMap((item) => [
1086
- <TourAnchor key={item.to} to={item.to} dataTour={item.dataTour} />,
1087
- ...(item.items ?? []).map((sub) => <TourAnchor key={`${item.to} ${sub.to}`} to={sub.to} />),
1088
- ])}
1089
- <DialogFrame open={open} onClose={close} title={labels.moreTitle} closeButton bodyClassName="space-y-1">
1090
- <ul className="space-y-1">
1091
- {hidden.map((item) => (
1092
- <li key={item.to}>
1093
- <SheetLink
1094
- to={item.to}
1095
- end={item.end}
1096
- icon={item.icon}
1097
- label={item.label}
1098
- onClick={close}
1099
- level="entry"
1100
- />
1101
- {!!item.items?.length && (
1102
- <ul aria-label={item.label} className="ms-[1.3rem] mt-0.5 space-y-0.5 border-s border-subtle ps-2">
1103
- {item.items.map((sub) => (
1104
- <li key={sub.to}>
1105
- <SheetLink
1106
- to={sub.to}
1107
- end={sub.end}
1108
- icon={sub.icon}
1109
- label={sub.label}
1110
- onClick={close}
1111
- level="page"
1112
- />
1113
- </li>
1114
- ))}
1115
- </ul>
1116
- )}
1117
- </li>
1118
- ))}
1119
- </ul>
1120
- </DialogFrame>
1121
- </div>
1122
- );
1123
- }
1124
-
1125
- /**
1126
- * The upper row of the phone navigation: the pages of the group the reader is in, as
1127
- * a sideways-scrolling row of pills. Rendered only while such a group is current —
1128
- * on a page outside every group the bar stays one row, rather than showing an empty
1129
- * shelf or guessing a group.
1130
- *
1131
- * It slides in over 200ms (fade only under reduced motion) and wraps onto more rows
1132
- * rather than scrolling, so every page of the group is in view at once.
1133
- */
1134
- function MobileSubNav({ nav, layout }: { nav: AppShellNavItem[]; layout: "wrap" | "scroll" }) {
1135
- const { pathname } = useLocation();
1136
- const scroll = layout === "scroll";
1137
- const listRef = useRef<HTMLUListElement>(null);
1138
- const fade = useStripFade(listRef);
1139
- // The current page into view — the row only, never the page.
1140
- useLayoutEffect(() => {
1141
- const list = listRef.current;
1142
- const current = list?.querySelector<HTMLElement>('[aria-current="page"]');
1143
- if (scroll && list && current) scrollIntoStrip(list, current);
1144
- }, [scroll, pathname]);
1145
- const group = nav.find(
1146
- (item) =>
1147
- !!item.items?.length &&
1148
- (matchPath({ path: item.to, end: item.end ?? true }, pathname) ||
1149
- item.items.some((sub) => matchPath({ path: sub.to, end: sub.end ?? true }, pathname))),
1150
- );
1151
-
1152
- if (!group) return null;
1153
- return (
1154
- <nav
1155
- aria-label={group.label}
1156
- // Keyed by group so switching groups replays the entrance.
1157
- key={group.to}
1158
- className="animate-subnav border-b border-subtle"
1159
- >
1160
- <ul
1161
- ref={listRef}
1162
- // `wrap` (default) takes as many rows as the group needs: a sideways row hides
1163
- // pages past the edge, and every page of the group in view is the point of the
1164
- // row. The bar grows with it — `--app-nav-h` measures it. `scroll` is the app's
1165
- // opt-in trade: one row, the cut edge faded (see mobileSubNavLayout).
1166
- className={cn(
1167
- "flex gap-1.5 px-3 py-2",
1168
- scroll ? "overflow-x-auto overscroll-x-contain [scrollbar-width:none]" : "flex-wrap",
1169
- )}
1170
- data-overflow={scroll ? fade.overflow : undefined}
1171
- style={scroll && fade.mask ? { maskImage: fade.mask, WebkitMaskImage: fade.mask } : undefined}
1172
- >
1173
- {group.items!.map((sub) => (
1174
- <li key={sub.to} className="shrink-0">
1175
- <NavLink
1176
- to={sub.to}
1177
- end={sub.end ?? true}
1178
- className={({ isActive }) =>
1179
- cn(
1180
- "flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs transition-colors",
1181
- isActive
1182
- ? "border-brand bg-brand font-medium text-brand-contrast"
1183
- : "border-subtle text-secondary hover:bg-hover",
1184
- )
1185
- }
1186
- >
1187
- <sub.icon className="size-3.5 shrink-0" aria-hidden />
1188
- {sub.label}
1189
- </NavLink>
1190
- </li>
1191
- ))}
1192
- </ul>
1193
- </nav>
1194
- );
1195
- }
1196
-
1197
- /** `strong` is the current page; `trail` is the group that CONTAINS the current page
1198
- * while the page itself is shown elsewhere (the inline list below it). */
1199
- const navLinkClass =
1200
- (collapsed: boolean, forceActive: boolean, trail = false) =>
1201
- ({ isActive }: { isActive: boolean }) =>
1202
- `flex items-center gap-3 min-h-9 ${collapsed ? "justify-center px-2" : "px-3"} py-2 rounded-md text-sm transition-colors duration-150 ${
1203
- isActive || (forceActive && !trail)
1204
- ? "bg-inverse text-inverse"
1205
- : forceActive
1206
- ? "font-medium text-primary hover:bg-hover"
1207
- : "text-secondary hover:bg-hover"
1208
- }`;
1209
-
1210
- /** Whether any of an entry's sub-items is the current route. NavLink only knows about
1211
- * its OWN `to`, so without this a group whose pages live at sibling paths
1212
- * (`/fields`, `/numbers`, …) lost its highlight the moment the reader left the one
1213
- * page the group linked to — the sidebar then marked nothing at all, and the only
1214
- * "you are here" left was inside a flyout that is closed unless hovered. */
1215
- function useSubItemActive(item: AppShellNavItem): boolean {
1216
- const { pathname } = useLocation();
1217
- return !!item.items?.some((sub) => matchPath({ path: sub.to, end: sub.end ?? true }, pathname));
1218
- }
1219
-
1220
- /** A single desktop sidebar entry. Plain link (with a tooltip when collapsed) —
1221
- * unless it has sub-items, in which case hovering/focusing opens a flyout, or (with
1222
- * `inline`) a disclosure lists them underneath. */
1223
- function SidebarNavItem({
1224
- item,
1225
- collapsed,
1226
- inline,
1227
- toggleGroupLabel,
1228
- openGroup,
1229
- setOpenGroup,
1230
- }: {
1231
- item: AppShellNavItem;
1232
- collapsed: boolean;
1233
- inline: boolean;
1234
- toggleGroupLabel: (groupLabel: string) => string;
1235
- openGroup: string | null;
1236
- setOpenGroup: (to: string | null) => void;
1237
- }) {
1238
- const hasSub = !!item.items?.length;
1239
- const subActive = useSubItemActive(item);
1240
-
1241
- // In the EXPANDED sidebar a group is one component in both styles, so switching
1242
- // between them is a transition of the same element (the page list folding away or
1243
- // unfolding) rather than one tree replaced by another. The icon-only sidebar keeps
1244
- // the plain flyout below: an indented list has no room there.
1245
- if (hasSub && !collapsed) {
1246
- return (
1247
- <SidebarGroup
1248
- item={item}
1249
- subActive={subActive}
1250
- inline={inline}
1251
- toggleGroupLabel={toggleGroupLabel}
1252
- open={openGroup === item.to}
1253
- setOpenGroup={setOpenGroup}
1254
- />
1255
- );
1256
- }
1257
-
1258
- const link = (
1259
- <NavLink
1260
- to={item.to}
1261
- end={item.end ?? true}
1262
- data-tour={item.dataTour}
1263
- className={navLinkClass(collapsed, subActive)}
1264
- aria-haspopup={hasSub ? "menu" : undefined}
1265
- >
1266
- <span className="relative shrink-0">
1267
- <item.icon className="size-4" />
1268
- </span>
1269
- {/* Wraps rather than truncates: in the sidebar a label cut to "Checkbox, switch
1270
- & sl…" hides the one word that tells two pages apart, and there is height
1271
- to spare. (The phone bar clamps at two lines instead, since 0.32 §10.7.) */}
1272
- {!collapsed && <span className="min-w-0 flex-1 break-words leading-snug">{item.label}</span>}
1273
- {!collapsed && hasSub && <ChevronRight className="size-3.5 shrink-0 opacity-60 rtl:-scale-x-100" />}
1274
- </NavLink>
1275
- );
1276
-
1277
- if (hasSub) return <SidebarFlyout item={item}>{link}</SidebarFlyout>;
1278
- return collapsed ? (
1279
- <Tooltip label={item.label} side="end" portal className="block">
1280
- {link}
1281
- </Tooltip>
1282
- ) : (
1283
- link
1284
- );
1285
- }
1286
-
1287
- /**
1288
- * An entry with sub-items in the expanded sidebar.
1289
- *
1290
- * - `inline`: the entry's own link (its overview page) plus a chevron button that
1291
- * shows or hides the sub-items indented beneath it. Two controls rather than one
1292
- * because they do two different things — a row that both navigated AND toggled
1293
- * would make "open the overview" and "peek at the pages" the same click.
1294
- * - flyout: the link alone, with the sub-items in a panel beside the sidebar on
1295
- * hover/focus. The inline list is still rendered, folded shut, so that switching
1296
- * styles animates it rather than swapping it.
1297
- *
1298
- * THE FOLD is a `grid-template-rows` transition from `0fr` to `1fr` — the one way to
1299
- * animate to a content-sized height without measuring it, so a translated label that
1300
- * wraps to two lines unfolds exactly as far as it needs. `visibility` rides the same
1301
- * transition so a shut list drops out of the accessibility tree only once it has
1302
- * finished closing, and `inert` takes its links out of the tab order immediately.
1303
- */
1304
- function SidebarGroup({
1305
- item,
1306
- subActive,
1307
- inline,
1308
- toggleGroupLabel,
1309
- open,
1310
- setOpenGroup,
1311
- }: {
1312
- item: AppShellNavItem;
1313
- subActive: boolean;
1314
- inline: boolean;
1315
- toggleGroupLabel: (groupLabel: string) => string;
1316
- /** Whether this is the sidebar's one open group (see AppShell's `openGroup`). */
1317
- open: boolean;
1318
- setOpenGroup: (to: string | null) => void;
1319
- }) {
1320
- const listId = useId();
1321
- const { pathname } = useLocation();
1322
- const onOwnPage = !!matchPath({ path: item.to, end: item.end ?? true }, pathname);
1323
- // Opening follows the reader: arriving on this group's overview or one of its pages
1324
- // (by link, by the pager, by Back) opens it — and, the sidebar holding ONE open
1325
- // group, closes whichever was open before. Both folds run at once, so the eye sees
1326
- // one list hand over to the other. Run in an effect after mount, so the current
1327
- // group UNFOLDS into view on load rather than appearing already open.
1328
- const here = subActive || onOwnPage;
1329
- useEffect(() => {
1330
- if (here) setOpenGroup(item.to);
1331
- }, [here, item.to, setOpenGroup]);
1332
- const expanded = inline && open;
1333
-
1334
- const link = (
1335
- <NavLink
1336
- to={item.to}
1337
- end={item.end ?? true}
1338
- data-tour={item.dataTour}
1339
- aria-haspopup={inline ? undefined : "menu"}
1340
- // `trail` (inline only): while a page of the group is current, the group row is
1341
- // the breadcrumb, not the destination — the page's own row below carries the
1342
- // strong marker, and two inverse rows would say "you are here" twice. In the
1343
- // flyout style the pages are out of sight, so the group row IS the marker.
1344
- className={(state) => cn(navLinkClass(false, subActive, inline)(state), "min-w-0 flex-1")}
1345
- >
1346
- <span className="relative shrink-0">
1347
- <item.icon className="size-4" />
1348
- </span>
1349
- <span className="min-w-0 flex-1 break-words leading-snug">{item.label}</span>
1350
- {!inline && <ChevronRight className="size-3.5 shrink-0 opacity-60 rtl:-scale-x-100" />}
1351
- </NavLink>
1352
- );
1353
-
1354
- return (
1355
- <div>
1356
- {inline ? (
1357
- <div className="flex items-center gap-0.5">
1358
- {link}
1359
- <button
1360
- type="button"
1361
- aria-expanded={open}
1362
- aria-controls={listId}
1363
- aria-label={toggleGroupLabel(item.label)}
1364
- onClick={() => setOpenGroup(open ? null : item.to)}
1365
- className="flex size-9 shrink-0 items-center justify-center rounded-md text-muted hover:bg-hover hover:text-secondary"
1366
- >
1367
- <ChevronRight
1368
- aria-hidden
1369
- className={cn(
1370
- "size-3.5 transition-transform duration-200 ease-out motion-reduce:transition-none rtl:-scale-x-100",
1371
- // Under dir="rtl" the chevron is MIRRORED first (scale applies before rotate), so
1372
- // it starts pointing left and a +90° turn would point it UP — hence −90° there.
1373
- open && "rotate-90 rtl:-rotate-90",
1374
- )}
1375
- />
1376
- </button>
1377
- </div>
1378
- ) : (
1379
- <SidebarFlyout item={item}>{link}</SidebarFlyout>
1380
- )}
1381
- <div
1382
- id={listId}
1383
- inert={!expanded}
1384
- className="grid transition-[grid-template-rows,visibility] duration-200 ease-out motion-reduce:transition-none"
1385
- style={{
1386
- gridTemplateRows: expanded ? "1fr" : "0fr",
1387
- visibility: expanded ? "visible" : "hidden",
1388
- }}
1389
- >
1390
- <div
1391
- className={cn(
1392
- "min-h-0 overflow-hidden transition-opacity duration-200 ease-out motion-reduce:transition-none",
1393
- expanded ? "opacity-100" : "opacity-0",
1394
- )}
1395
- >
1396
- {/* The rule and indent live on the list INSIDE the clipping box: margin or
1397
- padding on the element whose row is collapsing to 0fr would hold it open
1398
- by that much. */}
1399
- <ul className="ms-[1.3rem] mt-0.5 space-y-0.5 border-s border-subtle ps-2">
1400
- {item.items!.map((sub) => (
1401
- <li key={sub.to}>
1402
- <NavLink
1403
- to={sub.to}
1404
- end={sub.end ?? true}
1405
- className={({ isActive }) =>
1406
- cn(
1407
- "flex min-h-8 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",
1408
- isActive ? "bg-inverse text-inverse" : "text-secondary hover:bg-hover",
1409
- )
1410
- }
1411
- >
1412
- <sub.icon className="size-4 shrink-0 opacity-70" aria-hidden />
1413
- <span className="min-w-0 break-words leading-snug">{sub.label}</span>
1414
- </NavLink>
1415
- </li>
1416
- ))}
1417
- </ul>
1418
- </div>
1419
- </div>
1420
- </div>
1421
- );
1422
- }
1423
-
1424
- // Only ONE sidebar flyout should be on screen at a time. Each open flyout parks its
1425
- // "close now" here so the next item to open can dismiss it immediately, instead of
1426
- // leaving the previous panel up for the 140ms travel delay while the cursor moves
1427
- // down the nav list — which showed two panels at once.
1428
- let activeFlyoutClose: (() => void) | null = null;
1429
-
1430
- /**
1431
- * Wraps a nav item that has sub-items: on hover or keyboard focus it opens a
1432
- * portalled flyout to the right of the sidebar (portalled so the sidebar's
1433
- * `overflow-hidden` can't clip it) with the item's label as a header and each
1434
- * sub-item as an icon + title link. A short close delay lets the cursor travel
1435
- * from the item onto the panel; Escape / outside-move close it.
1436
- */
1437
- function SidebarFlyout({ item, children }: { item: AppShellNavItem; children: ReactNode }) {
1438
- const wrapperRef = useRef<HTMLDivElement>(null);
1439
- const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
1440
- const [open, setOpen] = useState(false);
1441
- // The flyout is portalled out of the subtree whose `dir` it inherited; read on open.
1442
- const [dir, setDir] = useState<Direction>("ltr");
1443
-
1444
- // Anchor the flyout to the item's END edge — its right, or its left in RTL, where
1445
- // the sidebar sits on the right of the window — and keep it on the glass (Wave 2,
1446
- // DUP-7). Right to left it hangs by its `right`, which a fixed box measures from
1447
- // inside the scrollbar: `innerWidth` counted the bar and put the flyout a bar's width
1448
- // off its item. The hook re-measures on scroll/resize (the nav list can scroll); the
1449
- // panel's own width is measured once it is up, before paint.
1450
- const rect = useAnchoredRect(wrapperRef, open);
1451
- const panelRef = useRef<HTMLDivElement>(null);
1452
- const [panelWidth, setPanelWidth] = useState(0);
1453
- const placed = rect !== null;
1454
- useLayoutEffect(() => {
1455
- if (open && placed) setPanelWidth(panelRef.current?.offsetWidth ?? 0);
1456
- }, [open, placed]);
1457
- const at = rect ? anchoredInline(rect, panelWidth, { dir, align: "after" }) : null;
1458
- const pos = rect && at ? { top: rect.top, ...(dir === "rtl" ? { right: at.right } : { left: at.left }) } : null;
1459
-
1460
- const cancelClose = useCallback(() => {
1461
- if (closeTimer.current) {
1462
- clearTimeout(closeTimer.current);
1463
- closeTimer.current = null;
1464
- }
1465
- }, []);
1466
- // Stable identity: the coordinator above holds this so a sibling item can close
1467
- // THIS flyout without waiting out its travel delay.
1468
- const closeNow = useCallback(() => {
1469
- cancelClose();
1470
- setOpen(false);
1471
- }, [cancelClose]);
1472
- const openMenu = useCallback(() => {
1473
- cancelClose();
1474
- if (activeFlyoutClose && activeFlyoutClose !== closeNow) activeFlyoutClose();
1475
- activeFlyoutClose = closeNow;
1476
- setDir(dirOf(wrapperRef.current));
1477
- setOpen(true);
1478
- }, [cancelClose, closeNow]);
1479
- const scheduleClose = useCallback(() => {
1480
- cancelClose();
1481
- closeTimer.current = setTimeout(() => {
1482
- setOpen(false);
1483
- if (activeFlyoutClose === closeNow) activeFlyoutClose = null;
1484
- }, 140);
1485
- }, [cancelClose, closeNow]);
1486
-
1487
- // Close on Escape while open — a layer on the kit's Escape stack (Wave 2, DUP-1).
1488
- useDismissLayer(open, () => setOpen(false));
1489
-
1490
- useEffect(
1491
- () => () => {
1492
- cancelClose();
1493
- if (activeFlyoutClose === closeNow) activeFlyoutClose = null;
1494
- },
1495
- [cancelClose, closeNow],
1496
- );
1497
-
1498
- return (
1499
- // Not a control: hover and focus-within on the nav link inside. Focus (the
1500
- // keyboard path) opens it exactly as hover does, Escape closes it.
1501
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- hover/focus-within wrapper around a real link
1502
- <div
1503
- ref={wrapperRef}
1504
- className="relative"
1505
- onMouseEnter={openMenu}
1506
- onMouseLeave={scheduleClose}
1507
- onFocus={openMenu}
1508
- onBlur={scheduleClose}
1509
- >
1510
- {children}
1511
- {open &&
1512
- pos &&
1513
- createPortal(
1514
- // Keeps the flyout open while the pointer crosses into it; keyboard users
1515
- // reach it by focus, which the wrapper above already handles.
1516
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- pointer-only hover bridge
1517
- <div
1518
- ref={panelRef}
1519
- dir={dir}
1520
- style={{ position: "fixed", ...pos }}
1521
- className="z-40 ps-1"
1522
- onMouseEnter={cancelClose}
1523
- onMouseLeave={scheduleClose}
1524
- >
1525
- <div
1526
- role="menu"
1527
- aria-label={item.label}
1528
- className="animate-flyout min-w-52 overflow-hidden rounded-md border border-subtle bg-surface shadow-lg"
1529
- >
1530
- <div className="border-b border-subtle px-3 py-2 text-caption font-semibold uppercase tracking-wide text-muted">
1531
- {item.label}
1532
- </div>
1533
- <ul className="py-1">
1534
- {item.items!.map((sub) => (
1535
- <li key={sub.to}>
1536
- <NavLink
1537
- to={sub.to}
1538
- end={sub.end ?? true}
1539
- role="menuitem"
1540
- onClick={() => setOpen(false)}
1541
- className={({ isActive }) =>
1542
- cn(
1543
- "flex items-center gap-2.5 px-3 py-2 text-sm",
1544
- isActive ? "bg-active font-medium text-primary" : "text-secondary hover:bg-hover",
1545
- )
1546
- }
1547
- >
1548
- <sub.icon className="size-4 shrink-0 text-placeholder" />
1549
- <span className="min-w-0 break-words leading-snug">{sub.label}</span>
1550
- </NavLink>
1551
- </li>
1552
- ))}
1553
- </ul>
1554
- </div>
1555
- </div>,
1556
- document.body,
1557
- )}
1558
- </div>
1559
- );
1560
- }