@eifi1/ui-kit 0.31.1 → 0.32.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 (538) hide show
  1. package/README.md +51 -28
  2. package/dist/account/delete-account-setting.d.ts +8 -3
  3. package/dist/admin/admin-action-confirm.d.ts +8 -3
  4. package/dist/admin/invitations-panel.d.ts +8 -3
  5. package/dist/admin/invitations-panel.js +29 -26
  6. package/dist/admin/invitations-panel.js.map +1 -1
  7. package/dist/admin/reviewer-scope-editor.d.ts +8 -3
  8. package/dist/admin/role-select.d.ts +10 -5
  9. package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
  10. package/dist/admin/user-roster.d.ts +10 -5
  11. package/dist/admin/user-roster.js +8 -47
  12. package/dist/admin/user-roster.js.map +1 -1
  13. package/dist/auth/auth-errors.d.ts +43 -8
  14. package/dist/auth/auth-errors.js +24 -1
  15. package/dist/auth/auth-errors.js.map +1 -1
  16. package/dist/auth/register-form.d.ts +8 -3
  17. package/dist/billing/billing-banner.d.ts +184 -0
  18. package/dist/billing/billing-banner.js +98 -0
  19. package/dist/billing/billing-banner.js.map +1 -0
  20. package/dist/billing/billing-labels.d.ts +100 -0
  21. package/dist/billing/billing-labels.js +74 -0
  22. package/dist/billing/billing-labels.js.map +1 -0
  23. package/dist/billing/billing-lock.d.ts +158 -0
  24. package/dist/billing/billing-lock.js +17 -0
  25. package/dist/billing/billing-lock.js.map +1 -0
  26. package/dist/billing/billing-standing.d.ts +149 -0
  27. package/dist/billing/billing-standing.js +28 -0
  28. package/dist/billing/billing-standing.js.map +1 -0
  29. package/dist/billing/plan-card.d.ts +248 -0
  30. package/dist/billing/plan-card.js +248 -0
  31. package/dist/billing/plan-card.js.map +1 -0
  32. package/dist/billing/plan-limit-notice.d.ts +156 -0
  33. package/dist/billing/plan-limit-notice.js +76 -0
  34. package/dist/billing/plan-limit-notice.js.map +1 -0
  35. package/dist/billing/plan-limit.d.ts +34 -0
  36. package/dist/billing/plan-limit.js +20 -0
  37. package/dist/billing/plan-limit.js.map +1 -0
  38. package/dist/billing/plan-price.d.ts +61 -0
  39. package/dist/billing/plan-price.js +45 -0
  40. package/dist/billing/plan-price.js.map +1 -0
  41. package/dist/billing/subscription-actions.d.ts +133 -0
  42. package/dist/billing/subscription-actions.js +28 -0
  43. package/dist/billing/subscription-actions.js.map +1 -0
  44. package/dist/billing/subscription-status-chip.d.ts +149 -0
  45. package/dist/billing/subscription-status-chip.js +48 -0
  46. package/dist/billing/subscription-status-chip.js.map +1 -0
  47. package/dist/chart.d.ts +9 -3
  48. package/dist/components/account-chips.d.ts +16 -4
  49. package/dist/components/account-chips.js +21 -3
  50. package/dist/components/account-chips.js.map +1 -1
  51. package/dist/components/alert-banner.d.ts +6 -1
  52. package/dist/components/alert-banner.js +4 -3
  53. package/dist/components/alert-banner.js.map +1 -1
  54. package/dist/components/amount-input.d.ts +8 -3
  55. package/dist/components/amount-input.js +5 -5
  56. package/dist/components/amount-input.js.map +1 -1
  57. package/dist/components/appearance-labels.d.ts +32 -0
  58. package/dist/components/appearance-labels.js +18 -0
  59. package/dist/components/appearance-labels.js.map +1 -0
  60. package/dist/components/autocomplete.js +2 -2
  61. package/dist/components/autocomplete.js.map +1 -1
  62. package/dist/components/breadcrumbs.js +3 -2
  63. package/dist/components/breadcrumbs.js.map +1 -1
  64. package/dist/components/bulk-action-bar.d.ts +2 -2
  65. package/dist/components/bulk-action-bar.js +7 -12
  66. package/dist/components/bulk-action-bar.js.map +1 -1
  67. package/dist/components/button-group.d.ts +8 -3
  68. package/dist/components/calculator.d.ts +8 -3
  69. package/dist/components/calculator.js +4 -3
  70. package/dist/components/calculator.js.map +1 -1
  71. package/dist/components/calendar-heatmap.js +5 -5
  72. package/dist/components/calendar-heatmap.js.map +1 -1
  73. package/dist/components/chart-height.d.ts +23 -0
  74. package/dist/components/chart-height.js +15 -0
  75. package/dist/components/chart-height.js.map +1 -0
  76. package/dist/components/chart-zoom.js +6 -1
  77. package/dist/components/chart-zoom.js.map +1 -1
  78. package/dist/components/checkbox-group.js +2 -2
  79. package/dist/components/checkbox-group.js.map +1 -1
  80. package/dist/components/checkbox.d.ts +10 -0
  81. package/dist/components/checkbox.js +25 -10
  82. package/dist/components/checkbox.js.map +1 -1
  83. package/dist/components/chip.d.ts +19 -3
  84. package/dist/components/chip.js +47 -21
  85. package/dist/components/chip.js.map +1 -1
  86. package/dist/components/choice-card.d.ts +1 -1
  87. package/dist/components/choice-card.js +8 -7
  88. package/dist/components/choice-card.js.map +1 -1
  89. package/dist/components/column-mapper.js +1 -1
  90. package/dist/components/column-mapper.js.map +1 -1
  91. package/dist/components/combobox-core.js +8 -7
  92. package/dist/components/combobox-core.js.map +1 -1
  93. package/dist/components/combobox.js +8 -8
  94. package/dist/components/combobox.js.map +1 -1
  95. package/dist/components/confirm-dialog.d.ts +8 -3
  96. package/dist/components/copy-button.d.ts +10 -5
  97. package/dist/components/country-select.js +1 -1
  98. package/dist/components/country-select.js.map +1 -1
  99. package/dist/components/currency-select.js +3 -3
  100. package/dist/components/currency-select.js.map +1 -1
  101. package/dist/components/danger-confirm.d.ts +8 -3
  102. package/dist/components/data-table-cells.d.ts +1 -1
  103. package/dist/components/data-table-filter-popover.d.ts +1 -1
  104. package/dist/components/data-table-filter-popover.js +4 -1
  105. package/dist/components/data-table-filter-popover.js.map +1 -1
  106. package/dist/components/data-table-filters.d.ts +1 -1
  107. package/dist/components/data-table-labels.d.ts +1 -1
  108. package/dist/components/data-table-pagination.d.ts +1 -1
  109. package/dist/components/data-table.d.ts +1 -1
  110. package/dist/components/data-table.js +123 -52
  111. package/dist/components/data-table.js.map +1 -1
  112. package/dist/components/date-picker.d.ts +1 -1
  113. package/dist/components/date-picker.js +7 -5
  114. package/dist/components/date-picker.js.map +1 -1
  115. package/dist/components/description-list.js +2 -2
  116. package/dist/components/description-list.js.map +1 -1
  117. package/dist/components/dialog-frame.js +2 -1
  118. package/dist/components/dialog-frame.js.map +1 -1
  119. package/dist/components/disclosure.js +3 -2
  120. package/dist/components/disclosure.js.map +1 -1
  121. package/dist/components/dropdown.d.ts +1 -1
  122. package/dist/components/dropdown.js +7 -1
  123. package/dist/components/dropdown.js.map +1 -1
  124. package/dist/components/facing-pair.d.ts +19 -6
  125. package/dist/components/facing-pair.js +3 -2
  126. package/dist/components/facing-pair.js.map +1 -1
  127. package/dist/components/field-parts.d.ts +102 -6
  128. package/dist/components/field-parts.js +81 -6
  129. package/dist/components/field-parts.js.map +1 -1
  130. package/dist/components/field-sync.d.ts +4 -1
  131. package/dist/components/field-sync.js +12 -2
  132. package/dist/components/field-sync.js.map +1 -1
  133. package/dist/components/field.js +2 -2
  134. package/dist/components/field.js.map +1 -1
  135. package/dist/components/file-button.d.ts +8 -3
  136. package/dist/components/file-dropzone.d.ts +8 -3
  137. package/dist/components/floating-panel.js +7 -4
  138. package/dist/components/floating-panel.js.map +1 -1
  139. package/dist/components/focus-ring.d.ts +12 -0
  140. package/dist/components/focus-ring.js +8 -0
  141. package/dist/components/focus-ring.js.map +1 -0
  142. package/dist/components/form-actions.d.ts +8 -3
  143. package/dist/components/form-actions.js +33 -19
  144. package/dist/components/form-actions.js.map +1 -1
  145. package/dist/components/full-bleed-dialog.d.ts +11 -1
  146. package/dist/components/full-bleed-dialog.js +3 -1
  147. package/dist/components/full-bleed-dialog.js.map +1 -1
  148. package/dist/components/iban-input.d.ts +8 -3
  149. package/dist/components/image-grid.js +1 -1
  150. package/dist/components/image-grid.js.map +1 -1
  151. package/dist/components/inline-edit-field.js +6 -3
  152. package/dist/components/inline-edit-field.js.map +1 -1
  153. package/dist/components/language-select.d.ts +8 -3
  154. package/dist/components/legal-page.d.ts +26 -5
  155. package/dist/components/legal-page.js +3 -1
  156. package/dist/components/legal-page.js.map +1 -1
  157. package/dist/components/legal.d.ts +8 -3
  158. package/dist/components/legal.js +2 -1
  159. package/dist/components/legal.js.map +1 -1
  160. package/dist/components/lightbox.js +24 -2
  161. package/dist/components/lightbox.js.map +1 -1
  162. package/dist/components/line-items.js +2 -1
  163. package/dist/components/line-items.js.map +1 -1
  164. package/dist/components/list.d.ts +5 -0
  165. package/dist/components/list.js +13 -9
  166. package/dist/components/list.js.map +1 -1
  167. package/dist/components/measured-grid.d.ts +3 -1
  168. package/dist/components/measured-grid.js +38 -20
  169. package/dist/components/measured-grid.js.map +1 -1
  170. package/dist/components/menu-item.js +2 -1
  171. package/dist/components/menu-item.js.map +1 -1
  172. package/dist/components/mini-calendar.js +8 -4
  173. package/dist/components/mini-calendar.js.map +1 -1
  174. package/dist/components/money-field.d.ts +8 -3
  175. package/dist/components/month-picker.js +9 -5
  176. package/dist/components/month-picker.js.map +1 -1
  177. package/dist/components/nav-pills.js +1 -1
  178. package/dist/components/nav-pills.js.map +1 -1
  179. package/dist/components/number-field.d.ts +8 -3
  180. package/dist/components/number-input.d.ts +11 -4
  181. package/dist/components/number-input.js +8 -6
  182. package/dist/components/number-input.js.map +1 -1
  183. package/dist/components/numpad-sheet.d.ts +8 -3
  184. package/dist/components/numpad-sheet.js +2 -1
  185. package/dist/components/numpad-sheet.js.map +1 -1
  186. package/dist/components/one-time-code-input.js +2 -2
  187. package/dist/components/one-time-code-input.js.map +1 -1
  188. package/dist/components/page-contents.js +2 -2
  189. package/dist/components/page-contents.js.map +1 -1
  190. package/dist/components/phone-input.d.ts +8 -3
  191. package/dist/components/phone-input.js +2 -2
  192. package/dist/components/phone-input.js.map +1 -1
  193. package/dist/components/pie-chart.d.ts +8 -2
  194. package/dist/components/pie-chart.js +11 -7
  195. package/dist/components/pie-chart.js.map +1 -1
  196. package/dist/components/popover.d.ts +9 -1
  197. package/dist/components/popover.js +8 -4
  198. package/dist/components/popover.js.map +1 -1
  199. package/dist/components/row-actions.d.ts +100 -0
  200. package/dist/components/row-actions.js +165 -0
  201. package/dist/components/row-actions.js.map +1 -0
  202. package/dist/components/scroll-area.js +1 -1
  203. package/dist/components/scroll-area.js.map +1 -1
  204. package/dist/components/search-field.js +3 -2
  205. package/dist/components/search-field.js.map +1 -1
  206. package/dist/components/series-chart-budget.d.ts +30 -7
  207. package/dist/components/series-chart-budget.js +12 -4
  208. package/dist/components/series-chart-budget.js.map +1 -1
  209. package/dist/components/series-chart-ticks.d.ts +7 -1
  210. package/dist/components/series-chart-ticks.js +6 -0
  211. package/dist/components/series-chart-ticks.js.map +1 -1
  212. package/dist/components/series-chart.d.ts +58 -28
  213. package/dist/components/series-chart.js +42 -32
  214. package/dist/components/series-chart.js.map +1 -1
  215. package/dist/components/settings-fields.d.ts +51 -6
  216. package/dist/components/settings-fields.js +37 -0
  217. package/dist/components/settings-fields.js.map +1 -1
  218. package/dist/components/share-card.d.ts +11 -6
  219. package/dist/components/share-card.js +2 -0
  220. package/dist/components/share-card.js.map +1 -1
  221. package/dist/components/sign-chip.d.ts +4 -0
  222. package/dist/components/sign-chip.js +17 -4
  223. package/dist/components/sign-chip.js.map +1 -1
  224. package/dist/components/signature-pad.d.ts +3 -1
  225. package/dist/components/signature-pad.js +8 -5
  226. package/dist/components/signature-pad.js.map +1 -1
  227. package/dist/components/slider.js +7 -6
  228. package/dist/components/slider.js.map +1 -1
  229. package/dist/components/sparkline.d.ts +5 -1
  230. package/dist/components/sparkline.js +4 -2
  231. package/dist/components/sparkline.js.map +1 -1
  232. package/dist/components/stat-tile.d.ts +17 -1
  233. package/dist/components/stat-tile.js +48 -20
  234. package/dist/components/stat-tile.js.map +1 -1
  235. package/dist/components/status-dot.d.ts +4 -0
  236. package/dist/components/swipeable-row.js +2 -1
  237. package/dist/components/swipeable-row.js.map +1 -1
  238. package/dist/components/switch.d.ts +21 -7
  239. package/dist/components/switch.js +24 -11
  240. package/dist/components/switch.js.map +1 -1
  241. package/dist/components/table.js +2 -2
  242. package/dist/components/table.js.map +1 -1
  243. package/dist/components/text-link.d.ts +8 -3
  244. package/dist/components/text-link.js +1 -1
  245. package/dist/components/text-link.js.map +1 -1
  246. package/dist/components/text.d.ts +7 -2
  247. package/dist/components/text.js +5 -5
  248. package/dist/components/text.js.map +1 -1
  249. package/dist/components/tile-radio.js +2 -1
  250. package/dist/components/tile-radio.js.map +1 -1
  251. package/dist/components/time-input.d.ts +8 -3
  252. package/dist/components/toast.d.ts +18 -2
  253. package/dist/components/toast.js +8 -5
  254. package/dist/components/toast.js.map +1 -1
  255. package/dist/components/toggle-group.d.ts +16 -1
  256. package/dist/components/toggle-group.js +40 -11
  257. package/dist/components/toggle-group.js.map +1 -1
  258. package/dist/components/toggle-legend.d.ts +10 -1
  259. package/dist/components/toggle-legend.js +21 -7
  260. package/dist/components/toggle-legend.js.map +1 -1
  261. package/dist/components/translation-review-parts.d.ts +4 -0
  262. package/dist/components/translation-review.d.ts +4 -0
  263. package/dist/components/translation-review.js +3 -3
  264. package/dist/components/translation-review.js.map +1 -1
  265. package/dist/components/tree-view.js +3 -2
  266. package/dist/components/tree-view.js.map +1 -1
  267. package/dist/components/treemap.d.ts +5 -2
  268. package/dist/components/treemap.js +33 -16
  269. package/dist/components/treemap.js.map +1 -1
  270. package/dist/components/ui.d.ts +8 -3
  271. package/dist/components/ui.js +202 -114
  272. package/dist/components/ui.js.map +1 -1
  273. package/dist/components/use-table-state.d.ts +1 -1
  274. package/dist/components/user-avatar.d.ts +5 -1
  275. package/dist/components/user-avatar.js +1 -1
  276. package/dist/components/user-avatar.js.map +1 -1
  277. package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
  278. package/dist/data-table.d.ts +1 -1
  279. package/dist/{delete-account-setting-CpIhzxxZ.d.ts → delete-account-setting-BvdQdbv3.d.ts} +605 -58
  280. package/dist/demo/demo-banner.js +1 -0
  281. package/dist/demo/demo-banner.js.map +1 -1
  282. package/dist/feedback/feedback-attachment.d.ts +8 -3
  283. package/dist/feedback/feedback-context.d.ts +8 -3
  284. package/dist/feedback/feedback-crash.d.ts +8 -3
  285. package/dist/feedback/feedback-dialog.d.ts +8 -3
  286. package/dist/feedback/feedback-dialog.js +3 -3
  287. package/dist/feedback/feedback-dialog.js.map +1 -1
  288. package/dist/feedback/feedback-inbox.d.ts +8 -3
  289. package/dist/feedback/feedback-inbox.js +2 -2
  290. package/dist/feedback/feedback-inbox.js.map +1 -1
  291. package/dist/feedback/feedback-labels.d.ts +8 -3
  292. package/dist/feedback/feedback-menu.d.ts +9 -4
  293. package/dist/feedback/feedback-record.d.ts +8 -3
  294. package/dist/feedback/feedback-row-detail.d.ts +8 -3
  295. package/dist/feedback/feedback-status-undo.d.ts +8 -3
  296. package/dist/feedback/feedback-submit.d.ts +8 -3
  297. package/dist/feedback/feedback-swipe.d.ts +8 -3
  298. package/dist/feedback/feedback-table.d.ts +8 -3
  299. package/dist/feedback/feedback-thread.d.ts +8 -3
  300. package/dist/feedback/feedback-thread.js +3 -3
  301. package/dist/feedback/feedback-thread.js.map +1 -1
  302. package/dist/feedback.d.ts +8 -3
  303. package/dist/hooks/use-anchored-panel.d.ts +32 -3
  304. package/dist/hooks/use-anchored-panel.js +21 -3
  305. package/dist/hooks/use-anchored-panel.js.map +1 -1
  306. package/dist/hooks/use-breakpoint.d.ts +77 -0
  307. package/dist/hooks/use-breakpoint.js +50 -0
  308. package/dist/hooks/use-breakpoint.js.map +1 -0
  309. package/dist/hooks/use-file-drop.d.ts +8 -3
  310. package/dist/hooks/use-large-text.d.ts +29 -0
  311. package/dist/hooks/use-large-text.js +20 -0
  312. package/dist/hooks/use-large-text.js.map +1 -0
  313. package/dist/hooks/use-windowed-rows.d.ts +52 -8
  314. package/dist/hooks/use-windowed-rows.js +94 -5
  315. package/dist/hooks/use-windowed-rows.js.map +1 -1
  316. package/dist/i18n/defaults.d.ts +8 -3
  317. package/dist/i18n/defaults.js +8 -0
  318. package/dist/i18n/defaults.js.map +1 -1
  319. package/dist/i18n/german.d.ts +8 -3
  320. package/dist/i18n/german.js +89 -2
  321. package/dist/i18n/german.js.map +1 -1
  322. package/dist/i18n/kit-labels.d.ts +8 -3
  323. package/dist/i18n/kit-labels.js.map +1 -1
  324. package/dist/i18n/languages.d.ts +10 -5
  325. package/dist/i18n/locales/de-CH.d.ts +8 -3
  326. package/dist/i18n/locales/en.d.ts +8 -3
  327. package/dist/i18n/locales/es.d.ts +8 -3
  328. package/dist/i18n/locales/es.js +84 -2
  329. package/dist/i18n/locales/es.js.map +1 -1
  330. package/dist/i18n/locales/fr.d.ts +8 -3
  331. package/dist/i18n/locales/fr.js +86 -2
  332. package/dist/i18n/locales/fr.js.map +1 -1
  333. package/dist/i18n/locales/hu.d.ts +8 -3
  334. package/dist/i18n/locales/hu.js +86 -2
  335. package/dist/i18n/locales/hu.js.map +1 -1
  336. package/dist/i18n/locales/it.d.ts +8 -3
  337. package/dist/i18n/locales/it.js +87 -2
  338. package/dist/i18n/locales/it.js.map +1 -1
  339. package/dist/i18n/locales/zh.d.ts +8 -3
  340. package/dist/i18n/locales/zh.js +84 -2
  341. package/dist/i18n/locales/zh.js.map +1 -1
  342. package/dist/i18n/review.d.ts +8 -3
  343. package/dist/i18n/review.js +15 -1
  344. package/dist/i18n/review.js.map +1 -1
  345. package/dist/index.d.ts +27 -11
  346. package/dist/index.js +75 -4
  347. package/dist/index.js.map +1 -1
  348. package/dist/landing/landing-actions.d.ts +8 -3
  349. package/dist/landing/landing-sections.d.ts +8 -3
  350. package/dist/landing/public-header.d.ts +9 -4
  351. package/dist/landing/public-header.js +3 -3
  352. package/dist/landing/public-header.js.map +1 -1
  353. package/dist/landing/routing.d.ts +8 -3
  354. package/dist/lib/cn.js +3 -2
  355. package/dist/lib/cn.js.map +1 -1
  356. package/dist/rhf/fields.d.ts +8 -3
  357. package/dist/rhf/fields.js +1 -1
  358. package/dist/rhf/fields.js.map +1 -1
  359. package/dist/rhf/form.d.ts +8 -3
  360. package/dist/rhf/form.js +2 -2
  361. package/dist/rhf/form.js.map +1 -1
  362. package/dist/rhf.d.ts +8 -3
  363. package/dist/search/command-palette.d.ts +1 -1
  364. package/dist/search/command-palette.js +9 -8
  365. package/dist/search/command-palette.js.map +1 -1
  366. package/dist/settings/settings-layout.js +2 -1
  367. package/dist/settings/settings-layout.js.map +1 -1
  368. package/dist/settings/settings-section.d.ts +8 -3
  369. package/dist/settings/use-account-appearance.d.ts +92 -0
  370. package/dist/settings/use-account-appearance.js +63 -0
  371. package/dist/settings/use-account-appearance.js.map +1 -0
  372. package/dist/settings/use-settings-route.d.ts +1 -1
  373. package/dist/settings/use-settings-route.js +2 -3
  374. package/dist/settings/use-settings-route.js.map +1 -1
  375. package/dist/shell/app-shell.d.ts +12 -171
  376. package/dist/shell/app-shell.js +144 -28
  377. package/dist/shell/app-shell.js.map +1 -1
  378. package/dist/shell/auth-layout.js +1 -1
  379. package/dist/shell/auth-layout.js.map +1 -1
  380. package/dist/shell/option-switcher-menu.js +1 -1
  381. package/dist/shell/option-switcher-menu.js.map +1 -1
  382. package/dist/shell/top-bar-brand.d.ts +8 -3
  383. package/dist/shell/top-bar-brand.js +3 -1
  384. package/dist/shell/top-bar-brand.js.map +1 -1
  385. package/dist/shell/topbar-action-menu.d.ts +4 -0
  386. package/dist/shell/topbar-action-menu.js +3 -3
  387. package/dist/shell/topbar-action-menu.js.map +1 -1
  388. package/dist/shell/topbar-controls.js +4 -4
  389. package/dist/shell/topbar-controls.js.map +1 -1
  390. package/dist/shell.d.ts +9 -5
  391. package/dist/theme/contrast-tokens.d.ts +41 -0
  392. package/dist/theme/contrast-tokens.js +59 -0
  393. package/dist/theme/contrast-tokens.js.map +1 -0
  394. package/dist/theme/contrast.d.ts +109 -0
  395. package/dist/theme/contrast.js +128 -0
  396. package/dist/theme/contrast.js.map +1 -0
  397. package/dist/theme/palette-presets.d.ts +12 -1
  398. package/dist/theme/palette-presets.js +9 -4
  399. package/dist/theme/palette-presets.js.map +1 -1
  400. package/dist/theme/palette-store.js.map +1 -1
  401. package/dist/theme/text-size.d.ts +157 -0
  402. package/dist/theme/text-size.js +121 -0
  403. package/dist/theme/text-size.js.map +1 -0
  404. package/dist/wizard/stepper-nav.d.ts +8 -3
  405. package/dist/wizard.d.ts +8 -3
  406. package/package.json +1 -1
  407. package/src/admin/invitations-panel.tsx +36 -29
  408. package/src/admin/user-roster.tsx +17 -72
  409. package/src/auth/auth-errors.ts +68 -7
  410. package/src/billing/billing-banner.tsx +205 -0
  411. package/src/billing/billing-labels.ts +224 -0
  412. package/src/billing/billing-lock.ts +75 -0
  413. package/src/billing/billing-standing.ts +72 -0
  414. package/src/billing/plan-card.tsx +458 -0
  415. package/src/billing/plan-limit-notice.tsx +145 -0
  416. package/src/billing/plan-limit.ts +53 -0
  417. package/src/billing/plan-price.ts +101 -0
  418. package/src/billing/subscription-actions.tsx +76 -0
  419. package/src/billing/subscription-status-chip.tsx +95 -0
  420. package/src/components/account-chips.tsx +36 -10
  421. package/src/components/alert-banner.tsx +11 -6
  422. package/src/components/amount-input.tsx +5 -5
  423. package/src/components/appearance-labels.ts +40 -0
  424. package/src/components/autocomplete.tsx +2 -2
  425. package/src/components/breadcrumbs.tsx +3 -3
  426. package/src/components/bulk-action-bar.tsx +11 -14
  427. package/src/components/calculator.tsx +4 -3
  428. package/src/components/calendar-heatmap.tsx +5 -5
  429. package/src/components/chart-height.ts +30 -0
  430. package/src/components/chart-zoom.tsx +6 -1
  431. package/src/components/checkbox-group.tsx +2 -2
  432. package/src/components/checkbox.tsx +62 -20
  433. package/src/components/chip.tsx +105 -24
  434. package/src/components/choice-card.tsx +15 -10
  435. package/src/components/column-mapper.tsx +1 -1
  436. package/src/components/combobox-core.tsx +8 -7
  437. package/src/components/combobox.tsx +8 -8
  438. package/src/components/country-select.tsx +1 -1
  439. package/src/components/currency-select.tsx +3 -3
  440. package/src/components/data-table-filter-popover.tsx +7 -1
  441. package/src/components/data-table.tsx +160 -43
  442. package/src/components/date-picker.tsx +12 -9
  443. package/src/components/description-list.tsx +2 -2
  444. package/src/components/dialog-frame.tsx +2 -1
  445. package/src/components/disclosure.tsx +3 -2
  446. package/src/components/dropdown.tsx +10 -2
  447. package/src/components/facing-pair.ts +12 -5
  448. package/src/components/field-parts.tsx +190 -7
  449. package/src/components/field-sync.tsx +17 -3
  450. package/src/components/field.tsx +2 -2
  451. package/src/components/floating-panel.tsx +7 -4
  452. package/src/components/focus-ring.ts +12 -0
  453. package/src/components/form-actions.tsx +55 -18
  454. package/src/components/full-bleed-dialog.tsx +18 -4
  455. package/src/components/image-grid.tsx +1 -1
  456. package/src/components/inline-edit-field.tsx +6 -3
  457. package/src/components/legal-page.tsx +28 -2
  458. package/src/components/legal.tsx +19 -1
  459. package/src/components/lightbox.tsx +24 -2
  460. package/src/components/line-items.tsx +7 -1
  461. package/src/components/list.tsx +36 -10
  462. package/src/components/measured-grid.tsx +49 -24
  463. package/src/components/menu-item.tsx +2 -2
  464. package/src/components/mini-calendar.tsx +8 -4
  465. package/src/components/month-picker.tsx +9 -5
  466. package/src/components/nav-pills.tsx +1 -1
  467. package/src/components/number-input.tsx +14 -7
  468. package/src/components/numpad-sheet.tsx +2 -2
  469. package/src/components/one-time-code-input.tsx +2 -2
  470. package/src/components/page-contents.tsx +2 -2
  471. package/src/components/phone-input.tsx +2 -2
  472. package/src/components/pie-chart.tsx +19 -9
  473. package/src/components/popover.tsx +22 -5
  474. package/src/components/row-actions.tsx +344 -0
  475. package/src/components/scroll-area.tsx +1 -1
  476. package/src/components/search-field.tsx +4 -2
  477. package/src/components/series-chart-budget.ts +40 -8
  478. package/src/components/series-chart-ticks.ts +10 -0
  479. package/src/components/series-chart.tsx +123 -72
  480. package/src/components/settings-fields.tsx +81 -2
  481. package/src/components/share-card.tsx +10 -0
  482. package/src/components/sign-chip.tsx +28 -4
  483. package/src/components/signature-pad.tsx +13 -7
  484. package/src/components/slider.tsx +22 -9
  485. package/src/components/sparkline.tsx +11 -3
  486. package/src/components/stat-tile.tsx +70 -14
  487. package/src/components/swipeable-row.tsx +2 -1
  488. package/src/components/switch.tsx +57 -23
  489. package/src/components/table.tsx +2 -2
  490. package/src/components/text-link.tsx +1 -1
  491. package/src/components/text.tsx +11 -6
  492. package/src/components/tile-radio.tsx +4 -1
  493. package/src/components/toast.tsx +27 -7
  494. package/src/components/toggle-group.tsx +99 -12
  495. package/src/components/toggle-legend.tsx +53 -8
  496. package/src/components/translation-review.tsx +10 -5
  497. package/src/components/tree-view.tsx +3 -2
  498. package/src/components/treemap.tsx +27 -10
  499. package/src/components/ui.tsx +331 -145
  500. package/src/components/user-avatar.tsx +1 -1
  501. package/src/demo/demo-banner.tsx +5 -0
  502. package/src/feedback/feedback-dialog.tsx +3 -3
  503. package/src/feedback/feedback-inbox.tsx +2 -2
  504. package/src/feedback/feedback-thread.tsx +3 -3
  505. package/src/hooks/use-anchored-panel.ts +67 -5
  506. package/src/hooks/use-breakpoint.ts +121 -0
  507. package/src/hooks/use-large-text.ts +41 -0
  508. package/src/hooks/use-windowed-rows.ts +168 -12
  509. package/src/i18n/defaults.ts +8 -0
  510. package/src/i18n/german.ts +103 -0
  511. package/src/i18n/kit-labels.tsx +12 -0
  512. package/src/i18n/locales/es.ts +94 -0
  513. package/src/i18n/locales/fr.ts +98 -0
  514. package/src/i18n/locales/hu.ts +96 -0
  515. package/src/i18n/locales/it.ts +99 -0
  516. package/src/i18n/locales/zh.ts +90 -0
  517. package/src/i18n/review.ts +14 -0
  518. package/src/index.ts +74 -6
  519. package/src/landing/public-header.tsx +4 -4
  520. package/src/lib/cn.ts +13 -2
  521. package/src/rhf/fields.tsx +1 -1
  522. package/src/rhf/form.tsx +2 -2
  523. package/src/search/command-palette.tsx +10 -9
  524. package/src/settings/settings-layout.tsx +2 -1
  525. package/src/settings/use-account-appearance.ts +148 -0
  526. package/src/settings/use-settings-route.ts +3 -4
  527. package/src/shell/app-shell.tsx +278 -54
  528. package/src/shell/auth-layout.tsx +1 -1
  529. package/src/shell/option-switcher-menu.tsx +1 -1
  530. package/src/shell/top-bar-brand.tsx +3 -1
  531. package/src/shell/topbar-action-menu.tsx +3 -3
  532. package/src/shell/topbar-controls.tsx +4 -4
  533. package/src/theme/contrast-tokens.ts +112 -0
  534. package/src/theme/contrast.ts +224 -0
  535. package/src/theme/palette-presets.ts +24 -4
  536. package/src/theme/palette-store.ts +6 -1
  537. package/src/theme/text-size.ts +267 -0
  538. package/tokens.css +143 -1
@@ -0,0 +1,112 @@
1
+ import { contrast, oklchToRgb, parseHex, rgbToOklch, toHex } from "./color";
2
+ import type { TokenSet } from "./palette-presets";
3
+
4
+ /**
5
+ * The "More contrast" step on a token set (docs/text-size-harmonization.md §5, §10.5).
6
+ * INTERNAL — `applyTokenSet` and the contrast layer share it; it is not re-exported.
7
+ *
8
+ * The step happens HERE, in JS, because the tokens it changes are written inline on
9
+ * `<html>` by `applyTokenSet`: an inline value beats any `[data-contrast=more]` rule and
10
+ * any `@media (prefers-contrast: more)` block, so a stylesheet cannot do it (§10.5, the
11
+ * keksdose review). `--text-placeholder` is a CSS-derived token, and it is written inline
12
+ * here too rather than redefined per attribute in tokens.css.
13
+ *
14
+ * What moves (§5):
15
+ * - `--text-muted` becomes the set's `--text-secondary`;
16
+ * - `--text-secondary` moves halfway to `--text-primary`;
17
+ * - `--text-placeholder` becomes the set's `--text-muted` (4.6–5.5:1 on every shipped
18
+ * preset, where the CSS formula gives 2.4–2.8:1) — the order primary > secondary >
19
+ * muted > placeholder survives the step;
20
+ * - `--border` moves toward `--text-primary` until it clears 3:1 against the worst of the
21
+ * three surfaces (WCAG 1.4.11, a field's boundary), and never less far than
22
+ * `--border-strong`'s 45 % — so every border is at least as strong as a "strong" one was.
23
+ * `--border-strong` stays derived in tokens.css and grows with it.
24
+ */
25
+
26
+ /** The custom properties a contrast step writes. */
27
+ export const CONTRAST_STEPPED_VARS = ["--text-secondary", "--text-muted", "--text-placeholder", "--border"] as const;
28
+
29
+ export interface ContrastStep {
30
+ textSecondary: string;
31
+ textMuted: string;
32
+ textPlaceholder: string;
33
+ border: string;
34
+ }
35
+
36
+ interface Lab {
37
+ l: number;
38
+ a: number;
39
+ b: number;
40
+ }
41
+
42
+ function toLab(hex: string): Lab | null {
43
+ const rgb = parseHex(hex);
44
+ if (!rgb) return null;
45
+ const { l, c, h } = rgbToOklch(rgb);
46
+ const rad = (h * Math.PI) / 180;
47
+ return { l, a: c * Math.cos(rad), b: c * Math.sin(rad) };
48
+ }
49
+
50
+ /** `color-mix(in oklab, a p, b)` as a hex — the same formula tokens.css's derived roles
51
+ * use, so a stepped value sits where the stylesheet would have put it. A colour that is
52
+ * not hex (a consumer's own token set) comes back unmixed. */
53
+ function mix(a: string, b: string, p: number): string {
54
+ const A = toLab(a);
55
+ const B = toLab(b);
56
+ if (!A || !B) return a;
57
+ const l = A.l * p + B.l * (1 - p);
58
+ const x = A.a * p + B.a * (1 - p);
59
+ const y = A.b * p + B.b * (1 - p);
60
+ const c = Math.hypot(x, y);
61
+ const h = c < 1e-6 ? 0 : ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;
62
+ return toHex(oklchToRgb({ l, c, h }));
63
+ }
64
+
65
+ /** WCAG 1.4.11 for a component's boundary. */
66
+ const BORDER_TARGET = 3;
67
+ /** `--border-strong` in tokens.css: 55 % border, 45 % text. */
68
+ const BORDER_STRONG_SHARE = 0.45;
69
+
70
+ function worstContrast(color: string, t: TokenSet): number {
71
+ return Math.min(contrast(color, t.bgPage), contrast(color, t.bgSurface), contrast(color, t.bgSurface2));
72
+ }
73
+
74
+ function steppedBorder(t: TokenSet): string {
75
+ // The share of `--text-primary` in the border: at least --border-strong's, more until
76
+ // 3:1. Monotonic in the share (the mix walks from the border to the text, away from
77
+ // every surface), so a bisection finds the least that clears it.
78
+ let lo = BORDER_STRONG_SHARE;
79
+ if (worstContrast(mix(t.border, t.textPrimary, 1 - lo), t) >= BORDER_TARGET) return mix(t.border, t.textPrimary, 1 - lo);
80
+ let hi = 1;
81
+ for (let i = 0; i < 16; i++) {
82
+ const mid = (lo + hi) / 2;
83
+ if (worstContrast(mix(t.border, t.textPrimary, 1 - mid), t) >= BORDER_TARGET) hi = mid;
84
+ else lo = mid;
85
+ }
86
+ return mix(t.border, t.textPrimary, 1 - hi);
87
+ }
88
+
89
+ /** The four stepped values for `t`. Pure. */
90
+ export function contrastStep(t: TokenSet): ContrastStep {
91
+ return {
92
+ textSecondary: mix(t.textSecondary, t.textPrimary, 0.5),
93
+ textMuted: t.textSecondary,
94
+ textPlaceholder: t.textMuted,
95
+ border: steppedBorder(t),
96
+ };
97
+ }
98
+
99
+ /* ── The token set last written to an element ───────────────────────────── */
100
+
101
+ const lastWritten = new WeakMap<HTMLElement, TokenSet>();
102
+
103
+ /** Remember the set `applyTokenSet` wrote, so a contrast change can re-apply it. */
104
+ export function rememberTokenSet(el: HTMLElement, t: TokenSet): void {
105
+ lastWritten.set(el, t);
106
+ }
107
+
108
+ /** The set `applyTokenSet` last wrote to `el`, or undefined when nothing did — an app
109
+ * without the palette layer, whose base tokens come from tokens.css. */
110
+ export function rememberedTokenSet(el: HTMLElement): TokenSet | undefined {
111
+ return lastWritten.get(el);
112
+ }
@@ -0,0 +1,224 @@
1
+ import { useEffect } from "react";
2
+ import { create } from "zustand";
3
+ import { persist } from "zustand/middleware";
4
+ import { logger } from "../lib/logger";
5
+ import { readStored } from "../lib/safe-storage";
6
+ import { useMediaQuery } from "../hooks/use-media-query";
7
+ import { CONTRAST_STEPPED_VARS, contrastStep, rememberedTokenSet } from "./contrast-tokens";
8
+ import { applyTokenSet, DEFAULT_PRESET } from "./palette-presets";
9
+ import type { PersistedAppearanceOptions } from "./text-size";
10
+
11
+ /**
12
+ * "More contrast" (docs/text-size-harmonization.md §2.4, §5, §10.5, §10.6): a switch
13
+ * beside the text size, on by choice or — on "System" — when the device asks for more
14
+ * contrast (`prefers-contrast: more`). Any text size.
15
+ *
16
+ * In force it is `<html data-contrast="more">` (else `"standard"`), which does two things:
17
+ * - the inline colour tokens are re-written one step stronger (`applyTokenSet`'s
18
+ * contrast flag — see contrast-tokens.ts for what moves and why it is JS);
19
+ * - tokens.css raises `--focus-ring-width` from 2 to 3 px and turns on the `contrast:`
20
+ * variant for anything else a component wants to change.
21
+ *
22
+ * The names below are frozen like the text size's: server-kit's `CONTRAST_MODES`, the
23
+ * stored format, the attribute.
24
+ */
25
+
26
+ /** The stored choices (§10.6): server-kit's `CONTRAST_MODES`. "system" IS a stored value
27
+ * — the PATCH rule refuses an explicit null, so null means only "never chosen". */
28
+ export const CONTRAST_MODES = ["system", "standard", "more"] as const;
29
+
30
+ export type ContrastMode = (typeof CONTRAST_MODES)[number];
31
+
32
+ /** What is in force on `<html data-contrast>`: a mode with "system" resolved. */
33
+ export type ContrastLevel = "standard" | "more";
34
+
35
+ const CONTRAST_ATTRIBUTE = "data-contrast";
36
+ const PREFERS_MORE_QUERY = "(prefers-contrast: more)";
37
+
38
+ export function isContrastMode(value: unknown): value is ContrastMode {
39
+ return value === "system" || value === "standard" || value === "more";
40
+ }
41
+
42
+ /** The mode in force: **this device's own choice → the account's → "system"** (§2.6).
43
+ * The one order the pre-paint call, the apply hook and `useAccountAppearance` share. */
44
+ export function resolveContrastMode(device: unknown, account?: unknown): ContrastMode {
45
+ if (isContrastMode(device)) return device;
46
+ if (isContrastMode(account)) return account;
47
+ return "system";
48
+ }
49
+
50
+ /** Whether the device asks for more contrast now. False without `matchMedia`. */
51
+ export function prefersMoreContrast(): boolean {
52
+ try {
53
+ return typeof window !== "undefined" && typeof window.matchMedia === "function"
54
+ ? window.matchMedia(PREFERS_MORE_QUERY).matches
55
+ : false;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /** A mode to the level it puts on `<html>`; "system" asks the device (or takes
62
+ * `prefersMore`, when the caller already knows). */
63
+ export function contrastLevel(mode: ContrastMode, prefersMore: boolean = prefersMoreContrast()): ContrastLevel {
64
+ return mode === "more" || (mode === "system" && prefersMore) ? "more" : "standard";
65
+ }
66
+
67
+ /** The level `<html>` carries now; "standard" without one. */
68
+ export function readContrastLevel(): ContrastLevel {
69
+ if (typeof document === "undefined") return "standard";
70
+ return document.documentElement.getAttribute(CONTRAST_ATTRIBUTE) === "more" ? "more" : "standard";
71
+ }
72
+
73
+ /**
74
+ * Re-write the contrast-dependent tokens on `el` for its current `data-contrast`.
75
+ *
76
+ * With a palette layer (`applyTokenSet` has written a set here), that set is applied
77
+ * again and steps itself. Without one — an app whose base tokens come from tokens.css,
78
+ * like Kurvenschmiede's — the four stepped tokens are written over the stylesheet for
79
+ * the default preset in the current light/dark, or removed again.
80
+ */
81
+ function reapplyContrastTokens(el: HTMLElement): void {
82
+ const written = rememberedTokenSet(el);
83
+ if (written) {
84
+ applyTokenSet(el, written);
85
+ return;
86
+ }
87
+ for (const name of CONTRAST_STEPPED_VARS) el.style.removeProperty(name);
88
+ if (el.getAttribute(CONTRAST_ATTRIBUTE) !== "more") return;
89
+ const step = contrastStep(DEFAULT_PRESET[el.classList.contains("dark") ? "dark" : "light"]);
90
+ el.style.setProperty("--text-secondary", step.textSecondary);
91
+ el.style.setProperty("--text-muted", step.textMuted);
92
+ el.style.setProperty("--text-placeholder", step.textPlaceholder);
93
+ el.style.setProperty("--border", step.border);
94
+ }
95
+
96
+ /** Put `level` on `<html>` and re-write the colour tokens for it. */
97
+ export function applyContrast(level: ContrastLevel): void {
98
+ if (typeof document === "undefined") return;
99
+ const root = document.documentElement;
100
+ root.setAttribute(CONTRAST_ATTRIBUTE, level);
101
+ reapplyContrastTokens(root);
102
+ }
103
+
104
+ /* ── Storage: the frozen format, the pre-paint call ─────────────────────── */
105
+
106
+ /**
107
+ * What a contrast store persists under its key — FROZEN, like the text size's:
108
+ *
109
+ * {"state":{"contrast":"more"},"version":1}
110
+ *
111
+ * `contrast` is one of {@link CONTRAST_MODES}, or `null` / absent when this device never
112
+ * chose.
113
+ */
114
+ export interface PersistedContrast {
115
+ state?: { contrast?: ContrastMode | null };
116
+ version?: number;
117
+ }
118
+
119
+ /** This device's own choice under `storageKey`, or `null` when it never chose. */
120
+ export function readPersistedContrast(storageKey: string): ContrastMode | null {
121
+ try {
122
+ const blob = JSON.parse(readStored(storageKey) ?? "null") as PersistedContrast | null;
123
+ const mode = blob?.state?.contrast;
124
+ return isContrastMode(mode) ? mode : null;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Pre-paint (§10.3, §10.5): this device's choice under `storageKey` — else the account's
132
+ * last known value, else "system" — resolved against `prefers-contrast` and applied
133
+ * BEFORE `createRoot`. Call it next to `applyPersistedPalette`, in either order: the
134
+ * palette steps itself when `data-contrast` is already set, and this re-applies the
135
+ * palette's set when it is not. Returns the level applied.
136
+ */
137
+ export function applyPersistedContrast(storageKey: string, options: PersistedAppearanceOptions = {}): ContrastLevel {
138
+ let account: unknown;
139
+ try {
140
+ account = options.account?.();
141
+ } catch {
142
+ account = undefined;
143
+ }
144
+ const level = contrastLevel(resolveContrastMode(readPersistedContrast(storageKey), account));
145
+ applyContrast(level);
146
+ return level;
147
+ }
148
+
149
+ /* ── The store ──────────────────────────────────────────────────────────── */
150
+
151
+ export interface ContrastState {
152
+ /** This device's own choice (persisted), or `null` when it never chose — then the
153
+ * account's value applies, else "system". */
154
+ contrast: ContrastMode | null;
155
+ /** Store this device's choice; `null` forgets it. */
156
+ setContrast: (contrast: ContrastMode | null) => void;
157
+ }
158
+
159
+ export interface ApplyContrastOptions {
160
+ /** The account's value (`/auth/me`'s `contrast`): it applies while this device has no
161
+ * choice of its own. */
162
+ account?: string | null;
163
+ }
164
+
165
+ /**
166
+ * Build a contrast store bound to a consumer-supplied localStorage key, shaped like
167
+ * `createThemeStore` and {@link createTextSizeStore} (§6). It holds the DEVICE's choice;
168
+ * the account's value comes in through `useApplyContrast({ account })`.
169
+ *
170
+ * `useApplyContrast` — mount it once near the root, beside `useApplyPalette`:
171
+ * - puts the level on `<html>` and steps the tokens;
172
+ * - on "System", follows `prefers-contrast: more` live (§10.5);
173
+ * - without a palette layer, re-writes the stepped tokens when the light/dark class
174
+ * changes, since they differ per theme.
175
+ *
176
+ * The persisted format is {@link PersistedContrast}, and frozen.
177
+ */
178
+ export function createContrastStore(storageKey: string) {
179
+ const useContrastStore = create<ContrastState>()(
180
+ logger(
181
+ persist(
182
+ (set) => ({
183
+ contrast: null,
184
+ setContrast: (contrast) => set({ contrast: isContrastMode(contrast) ? contrast : null }),
185
+ }),
186
+ {
187
+ name: storageKey,
188
+ version: 1,
189
+ partialize: (s) => ({ contrast: s.contrast }),
190
+ merge: (persisted, current) => {
191
+ const value = (persisted as { contrast?: unknown } | null)?.contrast;
192
+ return { ...current, contrast: isContrastMode(value) ? value : null };
193
+ },
194
+ },
195
+ ),
196
+ "contrast",
197
+ ),
198
+ );
199
+
200
+ function useApplyContrast({ account }: ApplyContrastOptions = {}): ContrastLevel {
201
+ const device = useContrastStore((s) => s.contrast);
202
+ // Subscribed whatever the mode (hooks cannot be skipped); only "system" reads it.
203
+ const prefersMore = useMediaQuery(PREFERS_MORE_QUERY, false);
204
+ const level = contrastLevel(resolveContrastMode(device, account), prefersMore);
205
+ useEffect(() => {
206
+ applyContrast(level);
207
+ }, [level]);
208
+ // Without a palette layer the stepped tokens are per theme: follow `.dark`.
209
+ useEffect(() => {
210
+ if (typeof MutationObserver === "undefined") return;
211
+ const root = document.documentElement;
212
+ const observer = new MutationObserver(() => {
213
+ if (!rememberedTokenSet(root)) reapplyContrastTokens(root);
214
+ });
215
+ observer.observe(root, { attributes: true, attributeFilter: ["class"] });
216
+ return () => observer.disconnect();
217
+ }, []);
218
+ return level;
219
+ }
220
+
221
+ return { useContrastStore, useApplyContrast };
222
+ }
223
+
224
+ export type ContrastStore = ReturnType<typeof createContrastStore>;
@@ -15,6 +15,7 @@
15
15
  // (the default preset here mirrors those exactly).
16
16
 
17
17
  import type { HeatStops } from "./chart-palette";
18
+ import { contrastStep, rememberTokenSet } from "./contrast-tokens";
18
19
 
19
20
  export interface TokenSet {
20
21
  // Backgrounds & chrome
@@ -740,15 +741,34 @@ export function presetById(id: string): PalettePreset {
740
741
  * have been silent and only a dead function would have noticed. The list is gone;
741
742
  * the test beside this asserts instead that every field of a `TokenSet` reaches the
742
743
  * element, which is what that list was standing in for.
744
+ *
745
+ * **More contrast (0.32, docs/text-size-harmonization.md §5, §10.5).** With `contrast`
746
+ * the text and border tokens are written one step stronger, and `--text-placeholder` —
747
+ * otherwise derived in tokens.css — is written inline too; without it that inline
748
+ * placeholder is removed again, so the stylesheet's formula applies. The step is made
749
+ * HERE because these tokens are inline: no `[data-contrast=more]` rule could beat them.
750
+ *
751
+ * `contrast` defaults to the element's own `data-contrast="more"`, which the contrast
752
+ * layer (`applyContrast`, `useApplyContrast`) sets first. That is how the palette and the
753
+ * contrast cooperate without knowing each other: a palette change re-applies through
754
+ * here and keeps the contrast, and a contrast change re-applies the set remembered here.
743
755
  */
744
- export function applyTokenSet(el: HTMLElement, t: TokenSet): void {
756
+ export function applyTokenSet(
757
+ el: HTMLElement,
758
+ t: TokenSet,
759
+ contrast: boolean = el.getAttribute("data-contrast") === "more",
760
+ ): void {
761
+ rememberTokenSet(el, t);
762
+ const step = contrast ? contrastStep(t) : null;
745
763
  el.style.setProperty("--bg-page", t.bgPage);
746
764
  el.style.setProperty("--bg-surface", t.bgSurface);
747
765
  el.style.setProperty("--bg-surface-2", t.bgSurface2);
748
- el.style.setProperty("--border", t.border);
766
+ el.style.setProperty("--border", step?.border ?? t.border);
749
767
  el.style.setProperty("--text-primary", t.textPrimary);
750
- el.style.setProperty("--text-secondary", t.textSecondary);
751
- el.style.setProperty("--text-muted", t.textMuted);
768
+ el.style.setProperty("--text-secondary", step?.textSecondary ?? t.textSecondary);
769
+ el.style.setProperty("--text-muted", step?.textMuted ?? t.textMuted);
770
+ if (step) el.style.setProperty("--text-placeholder", step.textPlaceholder);
771
+ else el.style.removeProperty("--text-placeholder");
752
772
  el.style.setProperty("--brand", t.brand);
753
773
  el.style.setProperty("--brand-hover", t.brandHover);
754
774
  el.style.setProperty("--brand-contrast", t.brandContrast);
@@ -60,7 +60,12 @@ export function createPaletteStore(
60
60
  /** Apply the active preset's tokens to <html> as inline CSS custom properties,
61
61
  * re-applying when the preset or the light/dark theme changes. Tailwind v4
62
62
  * strips a bare root token block from the bundle, so this inline write from
63
- * the TS source of truth is what actually drives the colours. */
63
+ * the TS source of truth is what actually drives the colours.
64
+ *
65
+ * 0.32: keeps More contrast. `applyTokenSet` reads `<html data-contrast>`, which the
66
+ * contrast store sets, and steps the tokens itself; the contrast store re-applies
67
+ * the set written here when the contrast changes (docs/text-size-harmonization.md
68
+ * §10.5). Neither store needs the other. */
64
69
  function useApplyPalette(): void {
65
70
  const id = usePalette((s) => s.id);
66
71
  const mode = useTheme((s) => s.mode);
@@ -0,0 +1,267 @@
1
+ import { useEffect, useMemo, useSyncExternalStore } from "react";
2
+ import { create } from "zustand";
3
+ import { persist } from "zustand/middleware";
4
+ import { logger } from "../lib/logger";
5
+ import { readStored } from "../lib/safe-storage";
6
+
7
+ /**
8
+ * The text size (docs/text-size-harmonization.md §2.1, §3.1, §10.3, §10.6).
9
+ *
10
+ * ONE scale on `<html>`: `data-text-size="normal|large|xlarge"`, and `tokens.css` turns
11
+ * it into `font-size: 100% / 125% / 150%` — percent, never px, so "Normal" keeps
12
+ * whatever size the person set in the browser. Tailwind's spacing and type are rem, so
13
+ * about 90 % of every screen grows with that one declaration; the breakpoints follow too
14
+ * (tokens.css redefines `sm:` … `3xl:` per size, {@link useBreakpoint} answers the same
15
+ * in JS).
16
+ *
17
+ * The names below are the mechanism's public vocabulary and are FROZEN: an app's
18
+ * storage, its `/auth/me` column (server-kit's `TEXT_SIZES`) and the inline boot snippet
19
+ * all spell them this way.
20
+ */
21
+
22
+ /** The three steps, in order (§2.1, §10.6): server-kit's `TEXT_SIZES`, value for value. */
23
+ export const TEXT_SIZES = ["normal", "large", "xlarge"] as const;
24
+
25
+ export type TextSize = (typeof TEXT_SIZES)[number];
26
+
27
+ /** The root font size each step sets, as a factor of the browser's own (tokens.css). For
28
+ * what CSS cannot reach: a canvas or SVG chart's px constants, a windowed list's row
29
+ * estimate, a popover width in px (§3.2). */
30
+ export const TEXT_SCALE: Readonly<Record<TextSize, number>> = { normal: 1, large: 1.25, xlarge: 1.5 };
31
+
32
+ /** The attribute on `<html>` that carries the size. */
33
+ const TEXT_SIZE_ATTRIBUTE = "data-text-size";
34
+
35
+ export function isTextSize(value: unknown): value is TextSize {
36
+ return value === "normal" || value === "large" || value === "xlarge";
37
+ }
38
+
39
+ /**
40
+ * The size in force: **this device's own choice → the account's → Normal** (§2.2, §2.6).
41
+ * Each is taken only when it is one of {@link TEXT_SIZES}; anything else (a stale value,
42
+ * `null` = never chosen) falls through to the next. The one order the pre-paint call, the
43
+ * apply hook and `useAccountAppearance` all use, so the three can never disagree.
44
+ */
45
+ export function resolveTextSize(device: unknown, account?: unknown): TextSize {
46
+ if (isTextSize(device)) return device;
47
+ if (isTextSize(account)) return account;
48
+ return "normal";
49
+ }
50
+
51
+ /* ── <html data-text-size> and the subscribers that read it ─────────────── */
52
+
53
+ const listeners = new Set<() => void>();
54
+ let observer: MutationObserver | null = null;
55
+
56
+ function notify(): void {
57
+ for (const listener of listeners) listener();
58
+ }
59
+
60
+ function subscribe(listener: () => void): () => void {
61
+ listeners.add(listener);
62
+ // Also catches writers that are not this module — the inline boot snippet, an app
63
+ // setting the attribute by hand, a test.
64
+ if (!observer && typeof MutationObserver !== "undefined" && typeof document !== "undefined") {
65
+ observer = new MutationObserver(notify);
66
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: [TEXT_SIZE_ATTRIBUTE] });
67
+ }
68
+ return () => {
69
+ listeners.delete(listener);
70
+ if (listeners.size === 0) {
71
+ observer?.disconnect();
72
+ observer = null;
73
+ }
74
+ };
75
+ }
76
+
77
+ /** The size `<html>` carries now; Normal without one (or outside a browser). */
78
+ export function readTextSize(): TextSize {
79
+ if (typeof document === "undefined") return "normal";
80
+ const value = document.documentElement.getAttribute(TEXT_SIZE_ATTRIBUTE);
81
+ return isTextSize(value) ? value : "normal";
82
+ }
83
+
84
+ /** Put `size` on `<html>`. Every {@link useTextSize} re-renders with it. */
85
+ export function applyTextSize(size: TextSize): void {
86
+ if (typeof document === "undefined") return;
87
+ const root = document.documentElement;
88
+ if (root.getAttribute(TEXT_SIZE_ATTRIBUTE) === size) return;
89
+ root.setAttribute(TEXT_SIZE_ATTRIBUTE, size);
90
+ notify();
91
+ }
92
+
93
+ export interface TextSizeInfo {
94
+ size: TextSize;
95
+ /** {@link TEXT_SCALE} for `size`: 1, 1.25 or 1.5. */
96
+ scale: number;
97
+ }
98
+
99
+ /**
100
+ * The size in force on this page, and its scale (§3.1) — read from `<html>`, so it needs
101
+ * no store and works in every kit component: whoever applied the size (the app's store,
102
+ * the pre-paint call, the inline snippet), this follows it.
103
+ *
104
+ * Only for what CSS cannot do. A class that should differ at Large is a `large:`
105
+ * variant; a layout switch is a breakpoint, which already scales.
106
+ */
107
+ export function useTextSize(): TextSizeInfo {
108
+ const size = useSyncExternalStore(subscribe, readTextSize, () => "normal" as const);
109
+ return useMemo(() => ({ size, scale: TEXT_SCALE[size] }), [size]);
110
+ }
111
+
112
+ /* ── Storage: the frozen format, the pre-paint call ─────────────────────── */
113
+
114
+ /**
115
+ * What a text-size store persists under its key — zustand's persist envelope, and FROZEN
116
+ * (§10.3) because an app's inline `<head>` snippet reads it before any module loads:
117
+ *
118
+ * {"state":{"size":"large"},"version":1}
119
+ *
120
+ * `size` is one of {@link TEXT_SIZES}, or `null` / absent when this device never chose.
121
+ * A future change adds a key; it never renames `state.size` or changes its values.
122
+ */
123
+ export interface PersistedTextSize {
124
+ state?: { size?: TextSize | null };
125
+ version?: number;
126
+ }
127
+
128
+ /** This device's own choice under `storageKey`, or `null` when it never chose (or the
129
+ * storage is blocked, malformed, or holds a value that is not a size). */
130
+ export function readPersistedTextSize(storageKey: string): TextSize | null {
131
+ try {
132
+ const blob = JSON.parse(readStored(storageKey) ?? "null") as PersistedTextSize | null;
133
+ const size = blob?.state?.size;
134
+ return isTextSize(size) ? size : null;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ export interface PersistedAppearanceOptions {
141
+ /**
142
+ * The account's last known value, for a device without a choice of its own (§10.3):
143
+ * what the app persisted with its user (keksdose keeps `/auth/me` in localStorage), so
144
+ * a cold boot paints the account's size instead of Normal and then jumping — offline
145
+ * too. Called once, guarded: a throw counts as "no account value".
146
+ */
147
+ account?: () => string | null | undefined;
148
+ }
149
+
150
+ function readAccount(account: PersistedAppearanceOptions["account"]): unknown {
151
+ if (!account) return undefined;
152
+ try {
153
+ return account();
154
+ } catch {
155
+ return undefined;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Pre-paint (§3.1, §10.3): read this device's choice under `storageKey` — else the
161
+ * account's last known value — and put it on `<html>` BEFORE `createRoot`, next to
162
+ * `applyPersistedTheme`, so the first paint already has the right size. Returns the size
163
+ * applied.
164
+ *
165
+ * ```ts
166
+ * applyPersistedTextSize("keksdose-text-size", {
167
+ * account: () => readPersistedUser()?.text_size,
168
+ * });
169
+ * ```
170
+ */
171
+ export function applyPersistedTextSize(storageKey: string, options: PersistedAppearanceOptions = {}): TextSize {
172
+ const size = resolveTextSize(readPersistedTextSize(storageKey), readAccount(options.account));
173
+ applyTextSize(size);
174
+ return size;
175
+ }
176
+
177
+ /**
178
+ * The same pre-paint as {@link applyPersistedTextSize}, as an inline `<head>` script for
179
+ * a boot splash painted from `index.html` before any module runs (§10.3). A function
180
+ * EXPRESSION: call it with the storage key and, optionally, a function returning the
181
+ * account's last known value.
182
+ *
183
+ * ```html
184
+ * <script>
185
+ * (function(k,a){…})("keksdose-text-size", function () {
186
+ * return JSON.parse(localStorage.getItem("keksdose-auth")).state.user.text_size;
187
+ * });
188
+ * </script>
189
+ * ```
190
+ *
191
+ * Paste the constant's value in place of `(function(k,a){…})`. It reads the frozen
192
+ * {@link PersistedTextSize} format, never throws, and sets only `data-text-size`; the
193
+ * splash itself sizes in rem so the attribute reaches it (tokens.css, or the splash's own
194
+ * `html[data-text-size=large]{font-size:125%}` if it paints before the stylesheet).
195
+ */
196
+ export const TEXT_SIZE_INLINE_SCRIPT =
197
+ "(function(k,a){function ok(v){return v==='normal'||v==='large'||v==='xlarge'}" +
198
+ "var v=null;try{var s=JSON.parse(localStorage.getItem(k)||'null');v=s&&s.state&&s.state.size}catch(e){}" +
199
+ "if(!ok(v)){v=null;try{v=a?a():null}catch(e){}}" +
200
+ "try{document.documentElement.setAttribute('data-text-size',ok(v)?v:'normal')}catch(e){}})";
201
+
202
+ /* ── The store ──────────────────────────────────────────────────────────── */
203
+
204
+ export interface TextSizeState {
205
+ /** This device's own choice (persisted), or `null` when it never chose — then the
206
+ * account's value applies (§2.2). */
207
+ size: TextSize | null;
208
+ /** Store this device's choice; `null` forgets it, so the account's applies again. */
209
+ setSize: (size: TextSize | null) => void;
210
+ }
211
+
212
+ export interface ApplyTextSizeOptions {
213
+ /** The account's value (`/auth/me`'s `text_size`): it applies while this device has no
214
+ * choice of its own. Leave it out where there is no account. */
215
+ account?: string | null;
216
+ }
217
+
218
+ /**
219
+ * Build a text-size store bound to a consumer-supplied localStorage key, shaped like
220
+ * `createThemeStore` (§6). It holds the DEVICE's choice only; the account's value comes
221
+ * in through `useApplyTextSize({ account })` and `useAccountAppearance`, and the size in
222
+ * force is always {@link resolveTextSize} of the two.
223
+ *
224
+ * - `useTextSizeStore` — the zustand hook: `size` (device choice or null) and `setSize`.
225
+ * - `useApplyTextSize({ account })` — the effect that puts the size in force on `<html>`.
226
+ * Mount it once near the root, beside `useApplyTheme`.
227
+ *
228
+ * The persisted format is {@link PersistedTextSize}, and frozen.
229
+ */
230
+ export function createTextSizeStore(storageKey: string) {
231
+ const useTextSizeStore = create<TextSizeState>()(
232
+ logger(
233
+ persist(
234
+ (set) => ({
235
+ size: null,
236
+ setSize: (size) => set({ size: isTextSize(size) ? size : null }),
237
+ }),
238
+ {
239
+ name: storageKey,
240
+ version: 1,
241
+ partialize: (s) => ({ size: s.size }),
242
+ // A hand-edited or future value must not reach <html> as an unknown size.
243
+ merge: (persisted, current) => ({
244
+ ...current,
245
+ size: isTextSize((persisted as { size?: unknown } | null)?.size)
246
+ ? ((persisted as { size: TextSize }).size)
247
+ : null,
248
+ }),
249
+ },
250
+ ),
251
+ "text-size",
252
+ ),
253
+ );
254
+
255
+ function useApplyTextSize({ account }: ApplyTextSizeOptions = {}): TextSize {
256
+ const device = useTextSizeStore((s) => s.size);
257
+ const size = resolveTextSize(device, account);
258
+ useEffect(() => {
259
+ applyTextSize(size);
260
+ }, [size]);
261
+ return size;
262
+ }
263
+
264
+ return { useTextSizeStore, useApplyTextSize };
265
+ }
266
+
267
+ export type TextSizeStore = ReturnType<typeof createTextSizeStore>;