@eifi1/ui-kit 0.34.0 → 0.36.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 (446) hide show
  1. package/README.md +115 -41
  2. package/dist/admin/role-select.d.ts +3 -2
  3. package/dist/admin/role-select.js +2 -0
  4. package/dist/admin/role-select.js.map +1 -1
  5. package/dist/billing/billing-banner.d.ts +5 -2
  6. package/dist/billing/billing-banner.js.map +1 -1
  7. package/dist/billing/plan-limit-notice.d.ts +5 -2
  8. package/dist/billing/plan-limit-notice.js.map +1 -1
  9. package/dist/chart.js.map +1 -1
  10. package/dist/components/amount-input.d.ts +2 -5
  11. package/dist/components/amount-input.js +2 -2
  12. package/dist/components/amount-input.js.map +1 -1
  13. package/dist/components/autocomplete.d.ts +2 -2
  14. package/dist/components/autocomplete.js +2 -1
  15. package/dist/components/autocomplete.js.map +1 -1
  16. package/dist/components/button-look.d.ts +1 -1
  17. package/dist/components/button-look.js +9 -4
  18. package/dist/components/button-look.js.map +1 -1
  19. package/dist/components/calculator.d.ts +1 -4
  20. package/dist/components/calculator.js +1 -3
  21. package/dist/components/calculator.js.map +1 -1
  22. package/dist/components/chart-zoom.d.ts +0 -37
  23. package/dist/components/chart-zoom.js +1 -28
  24. package/dist/components/chart-zoom.js.map +1 -1
  25. package/dist/components/checkbox-group.d.ts +5 -2
  26. package/dist/components/checkbox-group.js +15 -4
  27. package/dist/components/checkbox-group.js.map +1 -1
  28. package/dist/components/chip-input.d.ts +0 -4
  29. package/dist/components/chip-input.js +1 -5
  30. package/dist/components/chip-input.js.map +1 -1
  31. package/dist/components/column-mapper.d.ts +5 -3
  32. package/dist/components/column-mapper.js +2 -0
  33. package/dist/components/column-mapper.js.map +1 -1
  34. package/dist/components/combobox.d.ts +3 -3
  35. package/dist/components/combobox.js +2 -1
  36. package/dist/components/combobox.js.map +1 -1
  37. package/dist/components/copy-button.d.ts +5 -1
  38. package/dist/components/copy-button.js.map +1 -1
  39. package/dist/components/country-select.d.ts +3 -3
  40. package/dist/components/country-select.js +3 -0
  41. package/dist/components/country-select.js.map +1 -1
  42. package/dist/components/currency-option-list.d.ts +3 -8
  43. package/dist/components/currency-option-list.js +1 -29
  44. package/dist/components/currency-option-list.js.map +1 -1
  45. package/dist/components/currency-select.d.ts +3 -2
  46. package/dist/components/currency-select.js +10 -1
  47. package/dist/components/currency-select.js.map +1 -1
  48. package/dist/components/danger-confirm.d.ts +3 -3
  49. package/dist/components/danger-confirm.js.map +1 -1
  50. package/dist/components/data-table-filter-popover.d.ts +1 -1
  51. package/dist/components/data-table-filter-popover.js +1 -1
  52. package/dist/components/data-table-filter-popover.js.map +1 -1
  53. package/dist/components/data-table-labels.d.ts +1 -42
  54. package/dist/components/data-table-labels.js +2 -31
  55. package/dist/components/data-table-labels.js.map +1 -1
  56. package/dist/components/data-table-shared.d.ts +17 -1
  57. package/dist/components/data-table-shared.js +22 -0
  58. package/dist/components/data-table-shared.js.map +1 -1
  59. package/dist/components/data-table.d.ts +12 -2
  60. package/dist/components/data-table.js +18 -14
  61. package/dist/components/data-table.js.map +1 -1
  62. package/dist/components/date-field.d.ts +2 -1
  63. package/dist/components/date-field.js.map +1 -1
  64. package/dist/components/date-picker.js +3 -0
  65. package/dist/components/date-picker.js.map +1 -1
  66. package/dist/components/date-range-picker.js +3 -0
  67. package/dist/components/date-range-picker.js.map +1 -1
  68. package/dist/components/dialog-frame.d.ts +3 -1
  69. package/dist/components/dialog-frame.js.map +1 -1
  70. package/dist/components/dropdown.d.ts +18 -15
  71. package/dist/components/dropdown.js +11 -3
  72. package/dist/components/dropdown.js.map +1 -1
  73. package/dist/components/entity-combobox.d.ts +3 -3
  74. package/dist/components/entity-combobox.js +2 -1
  75. package/dist/components/entity-combobox.js.map +1 -1
  76. package/dist/components/field-base.d.ts +0 -5
  77. package/dist/components/field-base.js +0 -5
  78. package/dist/components/field-base.js.map +1 -1
  79. package/dist/components/field-parts.d.ts +69 -22
  80. package/dist/components/field-parts.js +33 -19
  81. package/dist/components/field-parts.js.map +1 -1
  82. package/dist/components/field-strip.d.ts +9 -23
  83. package/dist/components/field-strip.js +26 -23
  84. package/dist/components/field-strip.js.map +1 -1
  85. package/dist/components/field-sync.d.ts +0 -4
  86. package/dist/components/field-sync.js +0 -4
  87. package/dist/components/field-sync.js.map +1 -1
  88. package/dist/components/field.d.ts +7 -6
  89. package/dist/components/field.js +24 -15
  90. package/dist/components/field.js.map +1 -1
  91. package/dist/components/full-bleed-dialog.js +2 -2
  92. package/dist/components/full-bleed-dialog.js.map +1 -1
  93. package/dist/components/help-popover.d.ts +9 -7
  94. package/dist/components/help-popover.js.map +1 -1
  95. package/dist/components/hover-menu.d.ts +6 -13
  96. package/dist/components/hover-menu.js +3 -3
  97. package/dist/components/hover-menu.js.map +1 -1
  98. package/dist/components/icon-picker.d.ts +3 -2
  99. package/dist/components/icon-picker.js +3 -0
  100. package/dist/components/icon-picker.js.map +1 -1
  101. package/dist/components/inline-entity-combobox.d.ts +3 -3
  102. package/dist/components/inline-entity-combobox.js +2 -1
  103. package/dist/components/inline-entity-combobox.js.map +1 -1
  104. package/dist/components/input.d.ts +2 -2
  105. package/dist/components/input.js +2 -1
  106. package/dist/components/input.js.map +1 -1
  107. package/dist/components/modal.d.ts +9 -1
  108. package/dist/components/modal.js +7 -4
  109. package/dist/components/modal.js.map +1 -1
  110. package/dist/components/month-picker.d.ts +2 -1
  111. package/dist/components/month-picker.js +8 -1
  112. package/dist/components/month-picker.js.map +1 -1
  113. package/dist/components/multi-entity-combobox.d.ts +2 -2
  114. package/dist/components/multi-entity-combobox.js +2 -1
  115. package/dist/components/multi-entity-combobox.js.map +1 -1
  116. package/dist/components/multi-select.d.ts +7 -18
  117. package/dist/components/multi-select.js +8 -10
  118. package/dist/components/multi-select.js.map +1 -1
  119. package/dist/components/number-input.d.ts +2 -5
  120. package/dist/components/number-input.js +2 -4
  121. package/dist/components/number-input.js.map +1 -1
  122. package/dist/components/numeric-field.d.ts +5 -2
  123. package/dist/components/numeric-field.js +4 -1
  124. package/dist/components/numeric-field.js.map +1 -1
  125. package/dist/components/one-time-code-input.d.ts +2 -1
  126. package/dist/components/one-time-code-input.js +2 -1
  127. package/dist/components/one-time-code-input.js.map +1 -1
  128. package/dist/components/phone-input.d.ts +1 -1
  129. package/dist/components/phone-input.js +4 -0
  130. package/dist/components/phone-input.js.map +1 -1
  131. package/dist/components/popover.js +3 -3
  132. package/dist/components/popover.js.map +1 -1
  133. package/dist/components/select.d.ts +4 -3
  134. package/dist/components/select.js +2 -1
  135. package/dist/components/select.js.map +1 -1
  136. package/dist/components/series-chart.js.map +1 -1
  137. package/dist/components/server-wake.d.ts +0 -6
  138. package/dist/components/server-wake.js.map +1 -1
  139. package/dist/components/settings-fields.d.ts +4 -2
  140. package/dist/components/settings-fields.js.map +1 -1
  141. package/dist/components/slider.d.ts +3 -2
  142. package/dist/components/slider.js +7 -1
  143. package/dist/components/slider.js.map +1 -1
  144. package/dist/components/stat-tile.d.ts +8 -2
  145. package/dist/components/stat-tile.js +2 -11
  146. package/dist/components/stat-tile.js.map +1 -1
  147. package/dist/components/swatch-picker.d.ts +3 -2
  148. package/dist/components/swatch-picker.js +3 -0
  149. package/dist/components/swatch-picker.js.map +1 -1
  150. package/dist/components/swipeable-row.d.ts +2 -17
  151. package/dist/components/swipeable-row.js +11 -16
  152. package/dist/components/swipeable-row.js.map +1 -1
  153. package/dist/components/text-link.js +3 -1
  154. package/dist/components/text-link.js.map +1 -1
  155. package/dist/components/textarea.d.ts +2 -1
  156. package/dist/components/textarea.js +9 -3
  157. package/dist/components/textarea.js.map +1 -1
  158. package/dist/components/toggle-group.d.ts +2 -2
  159. package/dist/components/toggle-group.js +7 -3
  160. package/dist/components/toggle-group.js.map +1 -1
  161. package/dist/components/tooltip.d.ts +9 -1
  162. package/dist/components/tooltip.js +20 -5
  163. package/dist/components/tooltip.js.map +1 -1
  164. package/dist/components/translation-review.d.ts +10 -2
  165. package/dist/components/translation-review.js +11 -3
  166. package/dist/components/translation-review.js.map +1 -1
  167. package/dist/components/use-table-state.d.ts +31 -3
  168. package/dist/components/use-table-state.js +53 -8
  169. package/dist/components/use-table-state.js.map +1 -1
  170. package/dist/components/user-avatar.d.ts +2 -2
  171. package/dist/components/user-avatar.js.map +1 -1
  172. package/dist/components/wizard-stepper.d.ts +1 -4
  173. package/dist/components/wizard-stepper.js +2 -3
  174. package/dist/components/wizard-stepper.js.map +1 -1
  175. package/dist/currencies.d.ts +23 -0
  176. package/dist/currencies.js +38 -0
  177. package/dist/currencies.js.map +1 -0
  178. package/dist/demo/demo-banner.d.ts +7 -2
  179. package/dist/demo/demo-banner.js.map +1 -1
  180. package/dist/eslint/index.d.ts +83 -0
  181. package/dist/eslint/index.js +69 -0
  182. package/dist/eslint/index.js.map +1 -0
  183. package/dist/eslint/role-utilities.d.ts +48 -0
  184. package/dist/eslint/role-utilities.generated.d.ts +2 -0
  185. package/dist/eslint/role-utilities.generated.js +143 -0
  186. package/dist/eslint/role-utilities.generated.js.map +1 -0
  187. package/dist/eslint/role-utilities.js +43 -0
  188. package/dist/eslint/role-utilities.js.map +1 -0
  189. package/dist/feedback/feedback-menu.d.ts +2 -2
  190. package/dist/feedback/feedback-menu.js.map +1 -1
  191. package/dist/feedback/feedback-thread.d.ts +3 -8
  192. package/dist/feedback/feedback-thread.js +0 -2
  193. package/dist/feedback/feedback-thread.js.map +1 -1
  194. package/dist/hooks/use-dismiss.d.ts +16 -3
  195. package/dist/hooks/use-dismiss.js +14 -2
  196. package/dist/hooks/use-dismiss.js.map +1 -1
  197. package/dist/i18n/defaults.js +5 -1
  198. package/dist/i18n/defaults.js.map +1 -1
  199. package/dist/i18n/german.js +97 -1
  200. package/dist/i18n/german.js.map +1 -1
  201. package/dist/i18n/kit-labels.d.ts +29 -4
  202. package/dist/i18n/kit-labels.js +21 -1
  203. package/dist/i18n/kit-labels.js.map +1 -1
  204. package/dist/i18n/locales/en.js +104 -3
  205. package/dist/i18n/locales/en.js.map +3 -3
  206. package/dist/i18n/locales/es.js +95 -1
  207. package/dist/i18n/locales/es.js.map +1 -1
  208. package/dist/i18n/locales/fr.js +99 -3
  209. package/dist/i18n/locales/fr.js.map +1 -1
  210. package/dist/i18n/locales/hu.js +95 -1
  211. package/dist/i18n/locales/hu.js.map +1 -1
  212. package/dist/i18n/locales/it.js +99 -3
  213. package/dist/i18n/locales/it.js.map +1 -1
  214. package/dist/i18n/locales/zh.js +95 -1
  215. package/dist/i18n/locales/zh.js.map +1 -1
  216. package/dist/i18n/review.js +13 -0
  217. package/dist/i18n/review.js.map +1 -1
  218. package/dist/i18n/shared-labels.js +3 -1
  219. package/dist/i18n/shared-labels.js.map +1 -1
  220. package/dist/index.d.ts +23 -1
  221. package/dist/index.js +40 -2
  222. package/dist/index.js.map +1 -1
  223. package/dist/lib/device-settings.d.ts +72 -0
  224. package/dist/lib/device-settings.js +64 -0
  225. package/dist/lib/device-settings.js.map +1 -0
  226. package/dist/lib/plain-text.d.ts +11 -0
  227. package/dist/lib/plain-text.js +16 -0
  228. package/dist/lib/plain-text.js.map +1 -0
  229. package/dist/lib/refs.d.ts +13 -0
  230. package/dist/lib/refs.js +15 -0
  231. package/dist/lib/refs.js.map +1 -0
  232. package/dist/lib/safe-storage.d.ts +6 -0
  233. package/dist/lib/safe-storage.js +11 -0
  234. package/dist/lib/safe-storage.js.map +1 -1
  235. package/dist/notifications/notification-parts.d.ts +39 -0
  236. package/dist/notifications/notification-parts.js +47 -0
  237. package/dist/notifications/notification-parts.js.map +1 -0
  238. package/dist/notifications/notification-types-setting.d.ts +62 -0
  239. package/dist/notifications/notification-types-setting.js +192 -0
  240. package/dist/notifications/notification-types-setting.js.map +1 -0
  241. package/dist/notifications/notification-types.d.ts +75 -0
  242. package/dist/notifications/notification-types.js +2 -0
  243. package/dist/notifications/notification-types.js.map +1 -0
  244. package/dist/notifications/notifications-labels.d.ts +131 -0
  245. package/dist/notifications/notifications-labels.js +83 -0
  246. package/dist/notifications/notifications-labels.js.map +1 -0
  247. package/dist/notifications/push-device-setting.d.ts +37 -0
  248. package/dist/notifications/push-device-setting.js +164 -0
  249. package/dist/notifications/push-device-setting.js.map +1 -0
  250. package/dist/notifications/push-devices-setting.d.ts +44 -0
  251. package/dist/notifications/push-devices-setting.js +203 -0
  252. package/dist/notifications/push-devices-setting.js.map +1 -0
  253. package/dist/notifications/quiet-hours-setting.d.ts +47 -0
  254. package/dist/notifications/quiet-hours-setting.js +131 -0
  255. package/dist/notifications/quiet-hours-setting.js.map +1 -0
  256. package/dist/notifications/time-zone-select.d.ts +50 -0
  257. package/dist/notifications/time-zone-select.js +76 -0
  258. package/dist/notifications/time-zone-select.js.map +1 -0
  259. package/dist/notifications/unsubscribe-panel.d.ts +53 -0
  260. package/dist/notifications/unsubscribe-panel.js +139 -0
  261. package/dist/notifications/unsubscribe-panel.js.map +1 -0
  262. package/dist/notifications/use-push-device.d.ts +122 -0
  263. package/dist/notifications/use-push-device.js +230 -0
  264. package/dist/notifications/use-push-device.js.map +1 -0
  265. package/dist/rhf/fields.d.ts +36 -29
  266. package/dist/rhf/fields.js +51 -22
  267. package/dist/rhf/fields.js.map +1 -1
  268. package/dist/rhf/form-core.d.ts +5 -4
  269. package/dist/rhf/form-core.js.map +1 -1
  270. package/dist/rhf/form.d.ts +7 -36
  271. package/dist/rhf/form.js +1 -16
  272. package/dist/rhf/form.js.map +1 -1
  273. package/dist/rhf/line-items.d.ts +1 -1
  274. package/dist/rhf/line-items.js.map +1 -1
  275. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  276. package/dist/rhf.js.map +1 -1
  277. package/dist/settings/device-reset-setting-labels.d.ts +3 -0
  278. package/dist/settings/device-reset-setting-labels.js +15 -0
  279. package/dist/settings/device-reset-setting-labels.js.map +1 -0
  280. package/dist/settings/device-reset-setting.d.ts +58 -0
  281. package/dist/settings/device-reset-setting.js +51 -0
  282. package/dist/settings/device-reset-setting.js.map +1 -0
  283. package/dist/shell/app-shell.js +5 -0
  284. package/dist/shell/app-shell.js.map +1 -1
  285. package/dist/shell/option-switcher-menu.d.ts +5 -17
  286. package/dist/shell/option-switcher-menu.js +1 -3
  287. package/dist/shell/option-switcher-menu.js.map +1 -1
  288. package/dist/shell/role-switcher.d.ts +1 -4
  289. package/dist/shell/role-switcher.js +2 -3
  290. package/dist/shell/role-switcher.js.map +1 -1
  291. package/dist/shell/topbar-action-menu.d.ts +7 -19
  292. package/dist/shell/topbar-action-menu.js +1 -3
  293. package/dist/shell/topbar-action-menu.js.map +1 -1
  294. package/dist/shell/topbar-controls.d.ts +3 -12
  295. package/dist/shell/topbar-controls.js +6 -9
  296. package/dist/shell/topbar-controls.js.map +1 -1
  297. package/dist/testing/index.d.ts +1 -1
  298. package/dist/testing/index.js +2 -0
  299. package/dist/testing/index.js.map +1 -1
  300. package/dist/testing/viewport.d.ts +25 -2
  301. package/dist/testing/viewport.js +61 -10
  302. package/dist/testing/viewport.js.map +1 -1
  303. package/dist/theme/contrast-tokens.d.ts +42 -4
  304. package/dist/theme/contrast-tokens.js +56 -9
  305. package/dist/theme/contrast-tokens.js.map +1 -1
  306. package/dist/theme/contrast.d.ts +4 -2
  307. package/dist/theme/contrast.js +13 -0
  308. package/dist/theme/contrast.js.map +1 -1
  309. package/dist/theme/palette-derive.d.ts +15 -1
  310. package/dist/theme/palette-derive.js +37 -8
  311. package/dist/theme/palette-derive.js.map +1 -1
  312. package/dist/theme/palette-presets.d.ts +2 -8
  313. package/dist/theme/palette-presets.js +5 -284
  314. package/dist/theme/palette-presets.js.map +1 -1
  315. package/dist/theme/palette-store.js +6 -0
  316. package/dist/theme/palette-store.js.map +1 -1
  317. package/dist/theme/text-size.js +6 -0
  318. package/dist/theme/text-size.js.map +1 -1
  319. package/dist/theme/theme-store.js +6 -0
  320. package/dist/theme/theme-store.js.map +1 -1
  321. package/dist/wizard/types.d.ts +0 -5
  322. package/dist/wizard/types.js +1 -5
  323. package/dist/wizard/types.js.map +1 -1
  324. package/package.json +9 -1
  325. package/src/admin/role-select.tsx +4 -1
  326. package/src/billing/billing-banner.tsx +5 -2
  327. package/src/billing/plan-limit-notice.tsx +5 -2
  328. package/src/chart.ts +5 -3
  329. package/src/components/amount-input.tsx +4 -7
  330. package/src/components/autocomplete.tsx +5 -2
  331. package/src/components/button-look.ts +15 -5
  332. package/src/components/calculator.tsx +2 -7
  333. package/src/components/chart-zoom.tsx +4 -47
  334. package/src/components/checkbox-group.tsx +22 -4
  335. package/src/components/chip-input.tsx +0 -7
  336. package/src/components/column-mapper.tsx +6 -2
  337. package/src/components/combobox.tsx +6 -3
  338. package/src/components/copy-button.tsx +5 -1
  339. package/src/components/country-select.tsx +6 -1
  340. package/src/components/currency-option-list.tsx +8 -42
  341. package/src/components/currency-select.tsx +12 -3
  342. package/src/components/danger-confirm.tsx +3 -3
  343. package/src/components/data-table-filter-popover.tsx +2 -1
  344. package/src/components/data-table-labels.ts +2 -84
  345. package/src/components/data-table-shared.tsx +55 -2
  346. package/src/components/data-table.tsx +29 -17
  347. package/src/components/date-field.tsx +2 -2
  348. package/src/components/date-picker.tsx +3 -0
  349. package/src/components/date-range-picker.tsx +3 -0
  350. package/src/components/dialog-frame.tsx +3 -1
  351. package/src/components/dropdown.tsx +42 -18
  352. package/src/components/entity-combobox.tsx +6 -3
  353. package/src/components/field-base.tsx +0 -11
  354. package/src/components/field-parts.tsx +112 -38
  355. package/src/components/field-strip.tsx +12 -27
  356. package/src/components/field-sync.tsx +0 -7
  357. package/src/components/field.tsx +32 -19
  358. package/src/components/full-bleed-dialog.tsx +4 -3
  359. package/src/components/help-popover.tsx +9 -7
  360. package/src/components/hover-menu.tsx +12 -16
  361. package/src/components/icon-picker.tsx +8 -3
  362. package/src/components/inline-entity-combobox.tsx +6 -3
  363. package/src/components/input.tsx +4 -2
  364. package/src/components/modal.tsx +20 -4
  365. package/src/components/month-picker.tsx +10 -3
  366. package/src/components/multi-entity-combobox.tsx +5 -2
  367. package/src/components/multi-select.tsx +19 -31
  368. package/src/components/number-input.tsx +5 -10
  369. package/src/components/numeric-field.ts +10 -2
  370. package/src/components/one-time-code-input.tsx +5 -3
  371. package/src/components/phone-input.tsx +4 -0
  372. package/src/components/popover.tsx +8 -4
  373. package/src/components/select.tsx +6 -3
  374. package/src/components/series-chart.tsx +1 -1
  375. package/src/components/server-wake.tsx +0 -6
  376. package/src/components/settings-fields.tsx +4 -2
  377. package/src/components/slider.tsx +10 -3
  378. package/src/components/stat-tile.tsx +11 -20
  379. package/src/components/swatch-picker.tsx +9 -4
  380. package/src/components/swipeable-row.tsx +15 -37
  381. package/src/components/text-link.tsx +3 -1
  382. package/src/components/textarea.tsx +11 -4
  383. package/src/components/toggle-group.tsx +21 -5
  384. package/src/components/tooltip.tsx +37 -6
  385. package/src/components/translation-review.tsx +25 -7
  386. package/src/components/use-table-state.ts +91 -10
  387. package/src/components/user-avatar.tsx +2 -2
  388. package/src/components/wizard-stepper.tsx +2 -6
  389. package/src/currencies.ts +70 -0
  390. package/src/demo/demo-banner.tsx +7 -2
  391. package/src/eslint/index.ts +161 -0
  392. package/src/eslint/role-utilities.generated.ts +146 -0
  393. package/src/eslint/role-utilities.ts +99 -0
  394. package/src/feedback/feedback-menu.tsx +2 -2
  395. package/src/feedback/feedback-thread.tsx +3 -8
  396. package/src/hooks/use-dismiss.ts +39 -4
  397. package/src/i18n/defaults.ts +4 -0
  398. package/src/i18n/german.ts +113 -0
  399. package/src/i18n/kit-labels.tsx +66 -6
  400. package/src/i18n/locales/en.ts +6 -0
  401. package/src/i18n/locales/es.ts +106 -0
  402. package/src/i18n/locales/fr.ts +109 -2
  403. package/src/i18n/locales/hu.ts +104 -0
  404. package/src/i18n/locales/it.ts +108 -2
  405. package/src/i18n/locales/zh.ts +96 -0
  406. package/src/i18n/review.ts +13 -0
  407. package/src/i18n/shared-labels.ts +2 -0
  408. package/src/index.ts +53 -1
  409. package/src/lib/device-settings.ts +146 -0
  410. package/src/lib/plain-text.ts +22 -0
  411. package/src/lib/refs.ts +22 -0
  412. package/src/lib/safe-storage.ts +16 -0
  413. package/src/notifications/notification-parts.tsx +83 -0
  414. package/src/notifications/notification-types-setting.tsx +309 -0
  415. package/src/notifications/notification-types.ts +80 -0
  416. package/src/notifications/notifications-labels.ts +238 -0
  417. package/src/notifications/push-device-setting.tsx +224 -0
  418. package/src/notifications/push-devices-setting.tsx +306 -0
  419. package/src/notifications/quiet-hours-setting.tsx +185 -0
  420. package/src/notifications/time-zone-select.tsx +126 -0
  421. package/src/notifications/unsubscribe-panel.tsx +203 -0
  422. package/src/notifications/use-push-device.ts +392 -0
  423. package/src/rhf/fields.tsx +87 -39
  424. package/src/rhf/form-core.tsx +5 -4
  425. package/src/rhf/form.tsx +7 -43
  426. package/src/rhf/line-items.tsx +1 -1
  427. package/src/rhf/use-rhf-wizard-step.ts +4 -4
  428. package/src/rhf.ts +3 -2
  429. package/src/settings/device-reset-setting-labels.ts +20 -0
  430. package/src/settings/device-reset-setting.tsx +119 -0
  431. package/src/shell/app-shell.tsx +8 -0
  432. package/src/shell/option-switcher-menu.tsx +5 -22
  433. package/src/shell/role-switcher.tsx +2 -6
  434. package/src/shell/topbar-action-menu.tsx +7 -24
  435. package/src/shell/topbar-controls.tsx +6 -18
  436. package/src/testing/index.ts +3 -1
  437. package/src/testing/viewport.ts +125 -19
  438. package/src/theme/contrast-tokens.ts +106 -12
  439. package/src/theme/contrast.ts +30 -4
  440. package/src/theme/palette-derive.ts +68 -7
  441. package/src/theme/palette-presets.ts +16 -299
  442. package/src/theme/palette-store.ts +12 -0
  443. package/src/theme/text-size.ts +8 -0
  444. package/src/theme/theme-store.ts +10 -0
  445. package/src/wizard/types.ts +0 -8
  446. package/tokens.css +38 -4
@@ -0,0 +1,119 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
4
+ import { resetDeviceSettings } from "../lib/device-settings";
5
+ import { Button } from "../components/button";
6
+ import { useConfirm } from "../components/confirm-dialog";
7
+ import { toast } from "../components/toast";
8
+ import { SettingsSection, type SettingsSectionProps } from "./settings-section";
9
+ import { DEFAULT_DEVICE_RESET_LABELS } from "./device-reset-setting-labels";
10
+
11
+ /* ── Labels ──────────────────────────────────────────────────────────────── */
12
+
13
+ /** Every string {@link DeviceResetSetting} renders — the `deviceReset` namespace of
14
+ * `<UiKitProvider labels>`, overridable per instance through `labels`
15
+ * (docs/settings-harmonization.md §11.3). The confirm's Cancel is `confirmDialog.cancel`. */
16
+ export interface DeviceResetLabels {
17
+ /** The card's title, and the app's catalogue entry's. */
18
+ title: string;
19
+ /** The line under it: what goes back, and that the account is not touched. */
20
+ description: string;
21
+ /** The button that asks first. */
22
+ action: string;
23
+ /** The question it asks. */
24
+ confirmTitle: string;
25
+ /** What goes, what follows the account again, and what stays. */
26
+ confirmBody: string;
27
+ /** The button that answers it. */
28
+ confirmAction: string;
29
+ /** The toast once it is done. */
30
+ done: string;
31
+ /** For the app's catalogue entry (`keywords`); the card never reads it. */
32
+ keywords: string;
33
+ }
34
+
35
+ // Defined in ./device-reset-setting-labels, which holds no component code.
36
+ export { DEFAULT_DEVICE_RESET_LABELS };
37
+
38
+ /* ── The card ────────────────────────────────────────────────────────────── */
39
+
40
+ /** The card's DOM id: its catalogue entry's `anchor`, the `?focus=` target. */
41
+ export const DEVICE_RESET_ANCHOR = "settings-device-reset";
42
+
43
+ // `onReset` is the card's own: the DOM's form-reset handler never fires on a div.
44
+ export interface DeviceResetSettingProps
45
+ extends Omit<SettingsSectionProps, "anchor" | "title" | "description" | "children" | "onReset"> {
46
+ /** The app's sentence after the kit's description: what of its own goes back, and what
47
+ * stays (keksdose: the date format, the feature set and the gestures go; presentation
48
+ * mode and the home position stay). */
49
+ description?: ReactNode;
50
+ labels?: LabelOverride<DeviceResetLabels>;
51
+ /** Runs after the reset and before the toast, with the keys reset or removed. */
52
+ onReset?: (keys: string[]) => void;
53
+ }
54
+
55
+ /**
56
+ * "Reset this device's settings" (docs/settings-harmonization.md §11): the LAST card of
57
+ * the appearance group. It puts this device's appearance — theme, palette, language, text
58
+ * size, contrast and the app's own appearance choices — and its views — table views, the
59
+ * sidebar, folded groups, saved filters — back to what a new device gets, through
60
+ * {@link resetDeviceSettings}. The account, the sign-in, the session and anything waiting
61
+ * to be sent are never touched: they never join the registry.
62
+ *
63
+ * - **A confirm, not an undo** (§11.2 decision 9): `useConfirm` in the neutral tone,
64
+ * then a toast. An app mounts a `ConfirmProvider` and a `Toaster` already.
65
+ * - **Not a commit**: nothing is written to the account, so a demo or a lapsed plan's
66
+ * write lock does not disable it (decision 3).
67
+ * - **Live, no reload** (decision 10): each registered store resets through its own
68
+ * setter; the card stays where it is.
69
+ *
70
+ * The app adds the catalogue entry, `{ id: "device_reset", group: "appearance", anchor:
71
+ * DEVICE_RESET_ANCHOR, title, keywords }` (`title` and `keywords` from these labels), last
72
+ * in its appearance group: inside a `SettingsLayout`, `SettingsSection` throws in
73
+ * development without it.
74
+ */
75
+ export function DeviceResetSetting({
76
+ description,
77
+ labels: labelsProp,
78
+ onReset,
79
+ ...rest
80
+ }: DeviceResetSettingProps) {
81
+ const labels = useKitLabels("deviceReset", DEFAULT_DEVICE_RESET_LABELS, labelsProp);
82
+ const confirm = useConfirm();
83
+
84
+ const reset = async () => {
85
+ const ok = await confirm({
86
+ title: labels.confirmTitle,
87
+ body: labels.confirmBody,
88
+ confirmLabel: labels.confirmAction,
89
+ tone: "neutral",
90
+ });
91
+ if (!ok) return;
92
+ const keys = resetDeviceSettings();
93
+ onReset?.(keys);
94
+ toast.success(labels.done);
95
+ };
96
+
97
+ return (
98
+ <SettingsSection
99
+ {...rest}
100
+ anchor={DEVICE_RESET_ANCHOR}
101
+ title={labels.title}
102
+ description={
103
+ description != null ? (
104
+ <>
105
+ {labels.description} {description}
106
+ </>
107
+ ) : (
108
+ labels.description
109
+ )
110
+ }
111
+ >
112
+ <div>
113
+ <Button variant="secondary" onClick={() => void reset()}>
114
+ {labels.action}
115
+ </Button>
116
+ </div>
117
+ </SettingsSection>
118
+ );
119
+ }
@@ -8,6 +8,7 @@ import { cn } from "../lib/cn";
8
8
  import { scrollIntoStrip, useStripFade } from "../lib/strip-fade";
9
9
  import { dirOf, type Direction } from "../lib/direction";
10
10
  import { readStored, writeStored } from "../lib/safe-storage";
11
+ import { registerDeviceSetting } from "../lib/device-settings";
11
12
  import { Tooltip } from "../components/tooltip";
12
13
  import { pickLinkRenderer } from "../components/text-link";
13
14
  import { DialogFrame } from "../components/dialog-frame";
@@ -394,6 +395,13 @@ export function AppShell({
394
395
  useEffect(() => {
395
396
  if (collapseStorageKey !== null) writeStored(collapseStorageKey, collapsed ? "1" : "0");
396
397
  }, [collapsed, collapseStorageKey]);
398
+ // Reset this device's settings (docs/settings-harmonization.md §11.4): the sidebar
399
+ // unfolds, live, and the effect above writes "0". An embedded shell without a key of
400
+ // its own stores nothing and joins nothing.
401
+ useEffect(() => {
402
+ if (collapseStorageKey === null) return;
403
+ return registerDeviceSetting({ key: collapseStorageKey, kind: "view", reset: () => setCollapsed(false) });
404
+ }, [collapseStorageKey]);
397
405
  // §4: at Large and Extra large the sidebar never collapses to icons only — a glyph
398
406
  // with its name in a tooltip is the very thing a reader who asked for bigger type
399
407
  // cannot use. The persisted preference is left alone, so Normal gets it back, and the
@@ -9,23 +9,6 @@ export interface OptionSwitcherOption<T extends string> {
9
9
  label: ReactNode;
10
10
  }
11
11
 
12
- /**
13
- * Names the trigger and the menu: one of the two spellings is required (API-1, refactor
14
- * audit 2026-10). `aria-label` wins when both are given. TopBarActionMenu has the same
15
- * type; both become a plain required `aria-label` when `ariaLabel` goes in 0.35.
16
- */
17
- type RequiredMenuName =
18
- | {
19
- "aria-label": string;
20
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
21
- ariaLabel?: string;
22
- }
23
- | {
24
- "aria-label"?: string;
25
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
26
- ariaLabel: string;
27
- };
28
-
29
12
  /**
30
13
  * A top-bar hover menu that switches between a small set of options, with a
31
14
  * check on the active one and an optional heading. Generic glue over
@@ -34,8 +17,7 @@ type RequiredMenuName =
34
17
  */
35
18
  export function OptionSwitcherMenu<T extends string>({
36
19
  icon,
37
- "aria-label": ariaLabelAttr,
38
- ariaLabel: ariaLabelProp,
20
+ "aria-label": ariaLabel,
39
21
  title,
40
22
  heading,
41
23
  options,
@@ -43,14 +25,15 @@ export function OptionSwitcherMenu<T extends string>({
43
25
  onSelect,
44
26
  }: {
45
27
  icon: ReactNode;
28
+ /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit
29
+ * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */
30
+ "aria-label": string;
46
31
  title?: string;
47
32
  heading?: string;
48
33
  options: OptionSwitcherOption<T>[];
49
34
  value: T;
50
35
  onSelect: (value: T) => void;
51
- } & RequiredMenuName) {
52
- // The DOM spelling wins; `ariaLabel` is the fallback for call sites not moved yet.
53
- const ariaLabel = ariaLabelAttr ?? ariaLabelProp;
36
+ }) {
54
37
  return (
55
38
  <HoverMenu
56
39
  aria-label={ariaLabel}
@@ -8,9 +8,6 @@ export interface RoleSwitcherProps<R extends string> {
8
8
  onChange: (role: R) => void;
9
9
  /** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */
10
10
  "aria-label"?: string;
11
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
12
- * Names the switcher only when `aria-label` is absent. */
13
- ariaLabel?: string;
14
11
  heading?: string;
15
12
  /** The trigger's tooltip. Default: `topBar.role(value)`. */
16
13
  title?: string;
@@ -30,12 +27,11 @@ export function RoleSwitcher<R extends string>({
30
27
  roles,
31
28
  value,
32
29
  onChange,
33
- "aria-label": ariaLabelAttr,
34
- ariaLabel,
30
+ "aria-label": ariaLabel,
35
31
  heading,
36
32
  title,
37
33
  }: RoleSwitcherProps<R>) {
38
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabelAttr ?? ariaLabel });
34
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabel });
39
35
  return (
40
36
  <OptionSwitcherMenu<R>
41
37
  aria-label={labels.switchRole}
@@ -179,23 +179,6 @@ function keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {
179
179
  if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();
180
180
  }
181
181
 
182
- /**
183
- * Names the trigger and the menu: one of the two spellings is required (API-1, refactor
184
- * audit 2026-10). `aria-label` wins when both are given. OptionSwitcherMenu has the same
185
- * type; both become a plain required `aria-label` when `ariaLabel` goes in 0.35.
186
- */
187
- type RequiredMenuName =
188
- | {
189
- "aria-label": string;
190
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
191
- ariaLabel?: string;
192
- }
193
- | {
194
- "aria-label"?: string;
195
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
196
- ariaLabel: string;
197
- };
198
-
199
182
  /**
200
183
  * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,
201
184
  * with an optional uppercase heading and divider rules. Generic glue over the
@@ -219,8 +202,7 @@ export function TopBarActionMenu({
219
202
  trigger,
220
203
  triggerClassName,
221
204
  href,
222
- "aria-label": ariaLabelAttr,
223
- ariaLabel: ariaLabelProp,
205
+ "aria-label": ariaLabel,
224
206
  heading,
225
207
  header,
226
208
  entries,
@@ -265,6 +247,9 @@ export function TopBarActionMenu({
265
247
  * inside interactive content.
266
248
  */
267
249
  href?: string;
250
+ /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit
251
+ * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */
252
+ "aria-label": string;
268
253
  heading?: string;
269
254
  /** A non-interactive identity block above the rows. */
270
255
  header?: TopBarMenuHeader;
@@ -283,13 +268,11 @@ export function TopBarActionMenu({
283
268
  footer?: ReactNode | ((close: () => void) => ReactNode);
284
269
  /** Wraps the footer in a `<nav>` with this name ("Legal"). Without it, a plain block. */
285
270
  footerLabel?: string;
286
- /** Which edge of the trigger the panel lines up with (see `HoverMenu`, whose type this
287
- * is: `left`/`right` are deprecated 0.34). Default `end`. */
271
+ /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading
272
+ * direction (see `HoverMenu`, whose type this is). Default `end`. */
288
273
  align?: HoverMenuProps["align"];
289
274
  panelClassName?: string;
290
- } & RequiredMenuName) {
291
- // The DOM spelling wins; `ariaLabel` is the fallback for call sites not moved yet.
292
- const ariaLabel = ariaLabelAttr ?? ariaLabelProp;
275
+ }) {
293
276
  // Names the groups' headings for `aria-labelledby`. Positional rather than from the
294
277
  // entry's `key`, which is the caller's text and may hold a space — and a space in
295
278
  // `aria-labelledby` is a list separator.
@@ -29,8 +29,7 @@ function MenuHeading({ children }: { children: string }) {
29
29
  export function ThemeToggle({
30
30
  mode,
31
31
  onToggle,
32
- "aria-label": ariaLabelAttr,
33
- ariaLabel,
32
+ "aria-label": ariaLabel,
34
33
  title,
35
34
  className,
36
35
  }: {
@@ -38,15 +37,12 @@ export function ThemeToggle({
38
37
  onToggle: () => void;
39
38
  /** Default: `topBar.theme` from the {@link UiKitProvider}, else English. */
40
39
  "aria-label"?: string;
41
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
42
- * Names the control only when `aria-label` is absent. */
43
- ariaLabel?: string;
44
40
  title?: string;
45
41
  className?: string;
46
42
  }) {
47
43
  // This had no default at all, so a toggle rendered without `aria-label` was an
48
44
  // icon button with no name — a reader heard "button" and nothing else.
49
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabelAttr ?? ariaLabel });
45
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });
50
46
  const Icon = mode === "dark" ? Moon : Sun;
51
47
  return (
52
48
  <Tooltip label={title} portal>
@@ -69,8 +65,7 @@ export function PaletteMenu({
69
65
  activeId,
70
66
  mode,
71
67
  onSelect,
72
- "aria-label": ariaLabelAttr,
73
- ariaLabel,
68
+ "aria-label": ariaLabel,
74
69
  heading,
75
70
  }: {
76
71
  palettes: PalettePreset[];
@@ -79,13 +74,10 @@ export function PaletteMenu({
79
74
  onSelect: (id: string) => void;
80
75
  /** Default: `topBar.palette` from the {@link UiKitProvider}, else English. */
81
76
  "aria-label"?: string;
82
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
83
- * Names the control only when `aria-label` is absent. */
84
- ariaLabel?: string;
85
77
  /** Optional small heading row above the list. */
86
78
  heading?: string;
87
79
  }) {
88
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabelAttr ?? ariaLabel });
80
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabel });
89
81
  const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;
90
82
  return (
91
83
  <HoverMenu
@@ -177,8 +169,7 @@ export function LanguageMenu({
177
169
  options,
178
170
  current,
179
171
  onChange,
180
- "aria-label": ariaLabelAttr,
181
- ariaLabel,
172
+ "aria-label": ariaLabel,
182
173
  heading,
183
174
  }: {
184
175
  options: LanguageOption[];
@@ -186,14 +177,11 @@ export function LanguageMenu({
186
177
  onChange: (code: string) => void;
187
178
  /** Default: `topBar.language` from the {@link UiKitProvider}, else English. */
188
179
  "aria-label"?: string;
189
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
190
- * Names the control only when `aria-label` is absent. */
191
- ariaLabel?: string;
192
180
  /** Optional small heading row above the list — as on `TopBarActionMenu`,
193
181
  * `OptionSwitcherMenu` and `PaletteMenu`. */
194
182
  heading?: string;
195
183
  }) {
196
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { language: ariaLabelAttr ?? ariaLabel });
184
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });
197
185
  const active = options.find((o) => o.code === current);
198
186
  return (
199
187
  <HoverMenu
@@ -13,8 +13,10 @@ export {
13
13
  } from "./css-variables";
14
14
  // 0.34 (refactor audit 2026-10, TST-10): the environment jsdom lacks — a `matchMedia`
15
15
  // that answers from a viewport width (right at every text size) and a `ResizeObserver`
16
- // the test drives. Framework-free: each returns its own undo.
16
+ // the test drives. Framework-free: each returns its own undo. 0.35: `setViewportWidth`,
17
+ // a live resize of the stub in force — its lists flip and fire `change` (keksdose).
17
18
  export {
19
+ setViewportWidth,
18
20
  stubResizeObserver,
19
21
  stubViewportWidth,
20
22
  viewportMatchMedia,
@@ -85,14 +85,91 @@ function condition(part: string, width: number, options: ViewportOptions): boole
85
85
  }
86
86
  }
87
87
 
88
+ /** A whole query: comma lists are alternatives, `and` joins conditions, as in CSS. */
89
+ function answer(query: string, width: number, options: ViewportOptions): boolean {
90
+ return query
91
+ .split(",")
92
+ .some((alternative) =>
93
+ alternative.split(/\s+and\s+/).every((part) => condition(part, width, options)),
94
+ );
95
+ }
96
+
97
+ /** A list {@link viewportMatchMedia} handed out: what {@link setViewportWidth} flips. */
98
+ interface StubMediaQueryList {
99
+ matches: boolean;
100
+ readonly media: string;
101
+ onchange: ((this: MediaQueryList, event: MediaQueryListEvent) => unknown) | null;
102
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject | null): void;
103
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null): void;
104
+ addListener(listener: EventListenerOrEventListenerObject | null): void;
105
+ removeListener(listener: EventListenerOrEventListenerObject | null): void;
106
+ dispatchEvent(event: Event): boolean;
107
+ }
108
+
109
+ /** One stub's viewport: its width now, and every list it has handed out. */
110
+ interface StubViewport {
111
+ width: number;
112
+ readonly options: ViewportOptions;
113
+ readonly lists: Set<StubMediaQueryList>;
114
+ }
115
+
116
+ /** Each `matchMedia` {@link viewportMatchMedia} made → its viewport. Keyed by the function,
117
+ * so {@link setViewportWidth} finds whichever stub is in force however it was put in. */
118
+ const VIEWPORTS = new WeakMap<object, StubViewport>();
119
+
120
+ function mediaQueryList(query: string, viewport: StubViewport): StubMediaQueryList {
121
+ // One set for both APIs: in a browser `addListener(fn)` IS `addEventListener("change",
122
+ // fn)`, so adding the same function through both registers it once.
123
+ const listeners = new Set<EventListenerOrEventListenerObject>();
124
+ const list: StubMediaQueryList = {
125
+ matches: answer(query, viewport.width, viewport.options),
126
+ media: query,
127
+ onchange: null,
128
+ addEventListener(type, listener) {
129
+ if (type === "change" && listener) listeners.add(listener);
130
+ },
131
+ removeEventListener(type, listener) {
132
+ if (type === "change" && listener) listeners.delete(listener);
133
+ },
134
+ addListener(listener) {
135
+ if (listener) listeners.add(listener);
136
+ },
137
+ removeListener(listener) {
138
+ if (listener) listeners.delete(listener);
139
+ },
140
+ dispatchEvent(event) {
141
+ if (event.type !== "change") return true;
142
+ const self = list as unknown as MediaQueryList;
143
+ list.onchange?.call(self, event as MediaQueryListEvent);
144
+ for (const listener of [...listeners]) {
145
+ if (typeof listener === "function") listener.call(self, event);
146
+ else listener.handleEvent(event);
147
+ }
148
+ return !event.defaultPrevented;
149
+ },
150
+ };
151
+ return list;
152
+ }
153
+
154
+ /** The `change` a browser fires at a list whose answer flipped: a `MediaQueryListEvent`'s
155
+ * `matches` and `media`, on an `Event` (jsdom has no `MediaQueryListEvent`). */
156
+ function changeEvent(list: StubMediaQueryList): Event {
157
+ const event = new Event("change");
158
+ for (const [key, value] of Object.entries({ matches: list.matches, media: list.media, target: list, currentTarget: list })) {
159
+ Object.defineProperty(event, key, { value, enumerable: true });
160
+ }
161
+ return event;
162
+ }
163
+
88
164
  /**
89
165
  * A `window.matchMedia` for a viewport `width` CSS px wide.
90
166
  *
91
167
  * Width queries answer from `width`: the range syntax the kit writes for `max-*`
92
168
  * (`(width < 768px)`) and every other comparison, `(min-width: …)` and `(max-width: …)`,
93
169
  * in px or rem; `and` and comma lists combine as in CSS. The features in `options` answer
94
- * as set there; every other query is false. The lists it returns never change, so
95
- * nothing fires `change`: a test that changes the width stubs again and renders again.
170
+ * as set there; every other query is false. The lists keep their listeners, and
171
+ * {@link setViewportWidth} resizes the viewport under them the way a browser window
172
+ * does: each list whose answer flips fires `change`.
96
173
  *
97
174
  * Hand it to the runner's own global stub where the suite undoes stubs that way —
98
175
  * `vi.stubGlobal("matchMedia", viewportMatchMedia(375))` — or use
@@ -102,23 +179,52 @@ export function viewportMatchMedia(
102
179
  width: number,
103
180
  options: ViewportOptions = {},
104
181
  ): (query: string) => MediaQueryList {
105
- const answer = (query: string) =>
106
- query
107
- .split(",")
108
- .some((alternative) =>
109
- alternative.split(/\s+and\s+/).every((part) => condition(part, width, options)),
110
- );
111
- return (query: string) =>
112
- ({
113
- matches: answer(query),
114
- media: query,
115
- onchange: null,
116
- addEventListener: () => {},
117
- removeEventListener: () => {},
118
- addListener: () => {},
119
- removeListener: () => {},
120
- dispatchEvent: () => false,
121
- }) as unknown as MediaQueryList;
182
+ const viewport: StubViewport = { width, options, lists: new Set() };
183
+ const matchMedia = (query: string) => {
184
+ const list = mediaQueryList(query, viewport);
185
+ viewport.lists.add(list);
186
+ return list as unknown as MediaQueryList;
187
+ };
188
+ VIEWPORTS.set(matchMedia, viewport);
189
+ return matchMedia;
190
+ }
191
+
192
+ /**
193
+ * Resize the stubbed viewport to `width` CSS px, live — the window being dragged narrower,
194
+ * a phone turned (0.35, keksdose's 0.34 report). Every list the stub in force has handed
195
+ * out answers from the new width, and each one whose answer flipped fires `change` to its
196
+ * `addEventListener` listeners, its legacy `addListener` ones and its `onchange`, as in a
197
+ * browser; a list whose answer stayed the same tells nobody. So a mounted component that
198
+ * listens to a breakpoint (`usePhoneLayout()`, `useBreakpoint()`) switches without being
199
+ * rendered again.
200
+ *
201
+ * The stub in force is whatever `window.matchMedia` is now — one {@link stubViewportWidth}
202
+ * or `vi.stubGlobal("matchMedia", viewportMatchMedia(…))` put in; its features
203
+ * (`options`) stay as they were. Throws when `matchMedia` is not one of these stubs, rather
204
+ * than resize nothing. Synchronous: wrap it in `act()` when a listener sets state.
205
+ *
206
+ * ```ts
207
+ * undo = stubViewportWidth(1280);
208
+ * render(<Page />);
209
+ * act(() => setViewportWidth(375));
210
+ * expect(screen.getByRole("list")).toBeInTheDocument(); // the cards
211
+ * ```
212
+ */
213
+ export function setViewportWidth(width: number): void {
214
+ const viewport = typeof window === "undefined" ? undefined : VIEWPORTS.get(window.matchMedia);
215
+ if (!viewport) {
216
+ throw new Error(
217
+ "setViewportWidth: no viewport stub is in force — put one in first with " +
218
+ "stubViewportWidth(width), or vi.stubGlobal(\"matchMedia\", viewportMatchMedia(width)).",
219
+ );
220
+ }
221
+ viewport.width = width;
222
+ for (const list of [...viewport.lists]) {
223
+ const matches = answer(list.media, width, viewport.options);
224
+ if (matches === list.matches) continue;
225
+ list.matches = matches;
226
+ list.dispatchEvent(changeEvent(list));
227
+ }
122
228
  }
123
229
 
124
230
  /**