@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,306 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { ComponentPropsWithRef, KeyboardEvent, ReactNode } from "react";
3
+ import { MonitorSmartphone } from "lucide-react";
4
+
5
+ import { useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
6
+ import { DEFAULT_COMMON_LABELS } from "../i18n/shared-labels";
7
+ import { cn } from "../lib/cn";
8
+ import { formatDate } from "../lib/format";
9
+ import { Button } from "../components/button";
10
+ import { Card } from "../components/card";
11
+ import { Chip } from "../components/chip";
12
+ import { Skeleton } from "../components/skeleton";
13
+ import { COMMIT_EXCEPT_BILLING } from "../components/write-lock";
14
+ import { CARD_DESCRIPTION_CLASS } from "../settings/settings-card-classes";
15
+ import { SettingsCardTitle } from "../settings/settings-heading";
16
+ import { settle, useMounted } from "../account/account-parts";
17
+ import { describeFailure, FailureLine, useNotificationsLabels } from "./notification-parts";
18
+ import type { NotificationsLabels } from "./notifications-labels";
19
+ import type { PushDeviceItem } from "./notification-types";
20
+ import { notifyPushSubscriptionChanged } from "./use-push-device";
21
+
22
+ /**
23
+ * A name for a stored user agent: "Chrome · Android", "Firefox · Windows" (keksdose
24
+ * `push-devices-card.tsx:19-45`). Best effort: an agent it does not recognise comes back
25
+ * as it is, cut at 60 characters so a token soup cannot blow up the row; `null` for none.
26
+ * The order matters — Edge and Opera say "Chrome" too, and Chrome says "Safari".
27
+ */
28
+ export function describeUserAgent(ua: string | null): string | null {
29
+ if (!ua) return null;
30
+ const browser = /edg(?:e|a|ios)?\//i.test(ua)
31
+ ? "Edge"
32
+ : /opr\/|opera/i.test(ua)
33
+ ? "Opera"
34
+ : /firefox|fxios/i.test(ua)
35
+ ? "Firefox"
36
+ : /chrome|crios/i.test(ua)
37
+ ? "Chrome"
38
+ : /safari/i.test(ua)
39
+ ? "Safari"
40
+ : null;
41
+ const os = /windows/i.test(ua)
42
+ ? "Windows"
43
+ : /android/i.test(ua)
44
+ ? "Android"
45
+ : /iphone|ipad|ipod/i.test(ua)
46
+ ? "iOS"
47
+ : /mac os x|macintosh/i.test(ua)
48
+ ? "macOS"
49
+ : /linux/i.test(ua)
50
+ ? "Linux"
51
+ : null;
52
+ if (!browser && !os) return ua.length > 60 ? `${ua.slice(0, 60)}…` : ua;
53
+ return [browser, os].filter(Boolean).join(" · ");
54
+ }
55
+
56
+ export interface PushDevicesSettingProps<Id extends string | number = string>
57
+ extends Omit<ComponentPropsWithRef<"div">, "children"> {
58
+ /** `GET /notifications/devices`, oldest first; `undefined` while the first load runs. */
59
+ devices: readonly PushDeviceItem<Id>[] | undefined;
60
+ loading?: boolean;
61
+ /** This browser's endpoint (`usePushDevice().currentEndpoint`): its row says "This
62
+ * device", and removing it also unsubscribes the browser. */
63
+ currentEndpoint?: string | null;
64
+ /** `DELETE /notifications/devices/{id}`. Asked first; refresh `devices` once it
65
+ * resolves. */
66
+ onRemove: (id: Id) => Promise<unknown> | void;
67
+ /** The dates' text. Default: the provider's locale, medium date. */
68
+ formatDate?: (date: Date) => string;
69
+ /** Overrides the provider's locale for the default format. */
70
+ locale?: string;
71
+ /** The app's own words for a failed removal, or `undefined` for the kit's `failed`. */
72
+ describeError?: (error: unknown) => ReactNode | undefined;
73
+ labels?: LabelOverride<NotificationsLabels>;
74
+ }
75
+
76
+ /** Tear down this browser's subscription after its server row went: otherwise the
77
+ * browser still holds one, and the push card would say "on" for a device nothing can
78
+ * reach. Best effort — the row is already gone. */
79
+ async function unsubscribeThisBrowser(): Promise<void> {
80
+ try {
81
+ const registration = await navigator.serviceWorker?.getRegistration();
82
+ const subscription = await registration?.pushManager.getSubscription();
83
+ await subscription?.unsubscribe();
84
+ } catch {
85
+ /* the server row is gone either way */
86
+ }
87
+ notifyPushSubscriptionChanged();
88
+ }
89
+
90
+ /**
91
+ * Every device that gets push for the account (docs/notifications-harmonization.md §3.5,
92
+ * §4.2) — keksdose's devices card: the browser and system read from the stored user
93
+ * agent, "This device" on this browser's row, when it was added and last delivered to,
94
+ * the failures in a row in the warning tone, and "Remove".
95
+ *
96
+ * **Remove asks first, in the row**, as `PasskeysSetting` does: the card must work in an
97
+ * app without a `ConfirmProvider`, and the question belongs beside the device it names.
98
+ * Focus moves to Cancel. **Removing this device** also unsubscribes the browser and fires
99
+ * `PUSH_SUBSCRIPTION_CHANGED` (keksdose `push-devices-card.tsx:83-97`), so the push card
100
+ * stops saying "on".
101
+ *
102
+ * Under a write lock Remove is a `commit` control exempt from billing.
103
+ */
104
+ export function PushDevicesSetting<Id extends string | number = string>({
105
+ devices,
106
+ loading,
107
+ currentEndpoint,
108
+ onRemove,
109
+ formatDate: formatDateProp,
110
+ locale: localeProp,
111
+ describeError,
112
+ labels: labelsProp,
113
+ className,
114
+ ...rest
115
+ }: PushDevicesSettingProps<Id>) {
116
+ const labels = useNotificationsLabels(labelsProp);
117
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
118
+ const locale = useKitLocale(localeProp);
119
+ const [asking, setAsking] = useState<Id | null>(null);
120
+ const [busyId, setBusyId] = useState<Id | null>(null);
121
+ const [failure, setFailure] = useState<ReactNode>(null);
122
+ const mounted = useMounted();
123
+
124
+ const dateText = (iso: string): string => {
125
+ const date = new Date(iso);
126
+ if (Number.isNaN(date.getTime())) return iso;
127
+ return formatDateProp ? formatDateProp(date) : formatDate(date, "medium", { locale });
128
+ };
129
+
130
+ const remove = (device: PushDeviceItem<Id>) => {
131
+ if (busyId !== null) return;
132
+ setBusyId(device.id);
133
+ setFailure(null);
134
+ const isThis = Boolean(currentEndpoint) && device.endpoint === currentEndpoint;
135
+ settle(() => onRemove(device.id))
136
+ .then(async () => {
137
+ if (isThis) await unsubscribeThisBrowser();
138
+ if (!mounted.current) return;
139
+ setBusyId(null);
140
+ setAsking(null);
141
+ })
142
+ .catch((error: unknown) => {
143
+ if (!mounted.current) return;
144
+ setBusyId(null);
145
+ setFailure(describeFailure(describeError?.(error), labels.failed));
146
+ });
147
+ };
148
+
149
+ let body: ReactNode;
150
+ if (loading || devices === undefined) {
151
+ body = <Skeleton lines={2} label={labels.devicesLoading} />;
152
+ } else if (devices.length === 0) {
153
+ body = <p className={CARD_DESCRIPTION_CLASS}>{labels.devicesEmpty}</p>;
154
+ } else {
155
+ body = (
156
+ <ul aria-label={labels.devicesTitle} className="divide-y divide-subtle">
157
+ {devices.map((device) => (
158
+ <DeviceRow
159
+ key={device.id}
160
+ device={device}
161
+ name={describeUserAgent(device.user_agent) ?? labels.unknownDevice}
162
+ current={Boolean(currentEndpoint) && device.endpoint === currentEndpoint}
163
+ added={dateText(device.created_at)}
164
+ lastDelivery={device.last_success_at ? dateText(device.last_success_at) : null}
165
+ asking={asking === device.id}
166
+ busy={busyId === device.id}
167
+ blocked={busyId !== null && busyId !== device.id}
168
+ onAsk={() => setAsking(device.id)}
169
+ onCancel={() => setAsking(null)}
170
+ onConfirm={() => remove(device)}
171
+ labels={labels}
172
+ cancelLabel={common.cancel}
173
+ />
174
+ ))}
175
+ </ul>
176
+ );
177
+ }
178
+
179
+ return (
180
+ <Card {...rest} className={cn("p-4 space-y-3", className)}>
181
+ <div>
182
+ <SettingsCardTitle>{labels.devicesTitle}</SettingsCardTitle>
183
+ <div className={CARD_DESCRIPTION_CLASS}>{labels.devicesDescription}</div>
184
+ </div>
185
+ {body}
186
+ <FailureLine>{failure}</FailureLine>
187
+ </Card>
188
+ );
189
+ }
190
+
191
+ function DeviceRow<Id extends string | number>({
192
+ device,
193
+ name,
194
+ current,
195
+ added,
196
+ lastDelivery,
197
+ asking,
198
+ busy,
199
+ blocked,
200
+ onAsk,
201
+ onCancel,
202
+ onConfirm,
203
+ labels,
204
+ cancelLabel,
205
+ }: {
206
+ device: PushDeviceItem<Id>;
207
+ name: string;
208
+ current: boolean;
209
+ added: string;
210
+ lastDelivery: string | null;
211
+ asking: boolean;
212
+ busy: boolean;
213
+ blocked: boolean;
214
+ onAsk: () => void;
215
+ onCancel: () => void;
216
+ onConfirm: () => void;
217
+ labels: NotificationsLabels;
218
+ cancelLabel: string;
219
+ }) {
220
+ const cancelRef = useRef<HTMLButtonElement>(null);
221
+ const removeRef = useRef<HTMLButtonElement>(null);
222
+ // Which way the confirm closed: back to Remove, so focus does not fall to <body>.
223
+ const [returning, setReturning] = useState(false);
224
+ useEffect(() => {
225
+ if (asking) cancelRef.current?.focus();
226
+ else if (returning) removeRef.current?.focus();
227
+ }, [asking, returning]);
228
+
229
+ const leave = () => {
230
+ setReturning(true);
231
+ onCancel();
232
+ };
233
+ const escapeLeaves = (event: KeyboardEvent<HTMLButtonElement>) => {
234
+ if (event.key === "Escape") {
235
+ event.preventDefault();
236
+ event.stopPropagation();
237
+ leave();
238
+ }
239
+ };
240
+
241
+ const row = { "data-push-device-id": String(device.id), "data-current": current || undefined };
242
+
243
+ if (asking) {
244
+ return (
245
+ <li {...row} className="space-y-2 py-2">
246
+ <p role="alert" className="text-sm">
247
+ {labels.removePrompt}
248
+ </p>
249
+ <div className="flex flex-wrap gap-2">
250
+ <Button
251
+ type="button"
252
+ size="sm"
253
+ variant="danger"
254
+ commit={COMMIT_EXCEPT_BILLING}
255
+ pending={busy}
256
+ aria-label={labels.removeItem(name)}
257
+ onClick={onConfirm}
258
+ onKeyDown={escapeLeaves}
259
+ >
260
+ {labels.remove}
261
+ </Button>
262
+ <Button ref={cancelRef} type="button" size="sm" variant="ghost" onClick={leave} onKeyDown={escapeLeaves}>
263
+ {cancelLabel}
264
+ </Button>
265
+ </div>
266
+ </li>
267
+ );
268
+ }
269
+
270
+ return (
271
+ <li {...row} className="flex items-center gap-3 py-2">
272
+ <MonitorSmartphone aria-hidden className="size-4 shrink-0 text-muted" />
273
+ <div className="min-w-0 flex-1">
274
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
275
+ <span className="min-w-0 break-words text-sm font-medium">{name}</span>
276
+ {current && (
277
+ <Chip size="sm" tone="brand">
278
+ {labels.thisDevice}
279
+ </Chip>
280
+ )}
281
+ </div>
282
+ <div className={CARD_DESCRIPTION_CLASS}>
283
+ {labels.added(added)}
284
+ {" · "}
285
+ {lastDelivery ? labels.lastDelivery(lastDelivery) : labels.noDelivery}
286
+ </div>
287
+ {device.failure_count > 0 && <div className="text-xs text-warning">{labels.failures(device.failure_count)}</div>}
288
+ </div>
289
+ <Button
290
+ ref={removeRef}
291
+ type="button"
292
+ size="sm"
293
+ variant="ghost"
294
+ commit={COMMIT_EXCEPT_BILLING}
295
+ disabled={blocked}
296
+ aria-label={labels.removeItem(name)}
297
+ onClick={() => {
298
+ setReturning(false);
299
+ onAsk();
300
+ }}
301
+ >
302
+ {labels.remove}
303
+ </Button>
304
+ </li>
305
+ );
306
+ }
@@ -0,0 +1,185 @@
1
+ import { useId, useState } from "react";
2
+ import type { ComponentPropsWithRef, ReactNode } from "react";
3
+
4
+ import type { LabelOverride } from "../i18n/kit-labels";
5
+ import { cn } from "../lib/cn";
6
+ import { Button } from "../components/button";
7
+ import { Card } from "../components/card";
8
+ import { TimeInput } from "../components/time-input";
9
+ import { COMMIT_EXCEPT_BILLING } from "../components/write-lock";
10
+ import { CARD_DESCRIPTION_CLASS } from "../settings/settings-card-classes";
11
+ import { SettingsCardTitle } from "../settings/settings-heading";
12
+ import { settle, useMounted } from "../account/account-parts";
13
+ import { describeFailure, FailureLine, useNotificationsLabels, useWriteOverlay } from "./notification-parts";
14
+ import type { NotificationsLabels } from "./notifications-labels";
15
+ import type { NotificationSettingsPatch } from "./notification-types";
16
+ import { deviceTimeZone, TimeZoneSelect, zoneName } from "./time-zone-select";
17
+
18
+ /** The zone the server applies while the account has none (§2.6): every app's. */
19
+ const DEFAULT_FALLBACK_ZONE = "Europe/Zurich";
20
+
21
+ export interface QuietHoursSettingProps extends Omit<ComponentPropsWithRef<"div">, "children" | "onChange"> {
22
+ /** The `GET`'s `quiet_hours_start`, `"HH:MM"`, or `null`. */
23
+ start: string | null;
24
+ /** The `GET`'s `quiet_hours_end`. */
25
+ end: string | null;
26
+ /** The `GET`'s `timezone`, an IANA name, or `null` (the app's zone applies). */
27
+ timezone: string | null;
28
+ /**
29
+ * Save one change — `PATCH /notifications/settings`: a time (`null` when cleared), with
30
+ * the device's zone the first time one is entered while `timezone` is `null`; or
31
+ * `{timezone}`. The field shows the new value while the call runs and goes back if it
32
+ * rejects.
33
+ */
34
+ onChange: (patch: NotificationSettingsPatch) => Promise<unknown> | void;
35
+ /** The zone shown, and the one the server applies, while `timezone` is `null`.
36
+ * Default `"Europe/Zurich"`, every app's (decision 6). */
37
+ fallbackZone?: string;
38
+ /** The app keeps an inbox, and the description says a held push still reaches it. */
39
+ inbox?: boolean;
40
+ disabled?: boolean;
41
+ /** The app's own words for a failed save, or `undefined` for the kit's `failed`. */
42
+ describeError?: (error: unknown) => ReactNode | undefined;
43
+ labels?: LabelOverride<NotificationsLabels>;
44
+ }
45
+
46
+ type QuietField = "quiet_hours_start" | "quiet_hours_end" | "timezone";
47
+
48
+ /**
49
+ * Quiet hours (docs/notifications-harmonization.md §2.4, §2.6, §4.2): a window of local
50
+ * times in the account's zone, which may cross midnight, when no push of a machine-sent
51
+ * type is sent. A held push is dropped, not delayed; the inbox row and the mail go as
52
+ * usual. keksdose's block, as a card of its own.
53
+ *
54
+ * - **Two times**, each saved as it is entered; a cleared time sends `null`. While only
55
+ * one is set the card says so: the window fails open until both are.
56
+ * - **The zone** shows while a time is set. The first time entered while the account has
57
+ * none sends this device's zone with it, so "22:00" means the person's 22:00
58
+ * (keksdose `notifications-card.tsx:288-292`). With none stored, the card shows
59
+ * `fallbackZone` — the zone the server then applies — never the device's (keksdose's
60
+ * card showed the device's zone while its server applied UTC).
61
+ * - **"This device is in …"** with "Use it" when the device's zone is not the one in
62
+ * effect. Never written on its own: a zone is written by a person's pick (§2.6).
63
+ *
64
+ * Under a write lock the fields are `commit` controls exempt from billing.
65
+ */
66
+ export function QuietHoursSetting({
67
+ start,
68
+ end,
69
+ timezone,
70
+ onChange,
71
+ fallbackZone = DEFAULT_FALLBACK_ZONE,
72
+ inbox = false,
73
+ disabled = false,
74
+ describeError,
75
+ labels: labelsProp,
76
+ className,
77
+ ...rest
78
+ }: QuietHoursSettingProps) {
79
+ const labels = useNotificationsLabels(labelsProp);
80
+ const overlay = useWriteOverlay<string | null>(`${start}|${end}|${timezone}`);
81
+ const [failure, setFailure] = useState<ReactNode>(null);
82
+ const mounted = useMounted();
83
+ const id = useId();
84
+
85
+ const shown = (field: QuietField, server: string | null) =>
86
+ overlay.has(field) ? (overlay.get(field) ?? null) : server;
87
+ const startShown = shown("quiet_hours_start", start);
88
+ const endShown = shown("quiet_hours_end", end);
89
+ const zoneShown = shown("timezone", timezone);
90
+ const effectiveZone = zoneShown ?? fallbackZone;
91
+ const device = deviceTimeZone();
92
+
93
+ const save = (patch: NotificationSettingsPatch) => {
94
+ const fields = Object.keys(patch) as QuietField[];
95
+ setFailure(null);
96
+ for (const field of fields) overlay.set(field, patch[field] ?? null);
97
+ settle(() => onChange(patch)).then(
98
+ () => {
99
+ if (mounted.current) for (const field of fields) overlay.settle(field, true);
100
+ },
101
+ (error: unknown) => {
102
+ if (!mounted.current) return;
103
+ for (const field of fields) overlay.settle(field, false);
104
+ setFailure(describeFailure(describeError?.(error), labels.failed));
105
+ },
106
+ );
107
+ };
108
+
109
+ const saveTime = (field: "quiet_hours_start" | "quiet_hours_end", value: string) => {
110
+ const next = value || null;
111
+ if (next === shown(field, field === "quiet_hours_start" ? start : end)) return;
112
+ const patch: NotificationSettingsPatch = { [field]: next };
113
+ if (next && zoneShown === null) patch.timezone = device;
114
+ save(patch);
115
+ };
116
+
117
+ const anySet = Boolean(startShown || endShown);
118
+ const halfSet = Boolean(startShown) !== Boolean(endShown);
119
+
120
+ return (
121
+ <Card {...rest} className={cn("p-4 space-y-3", className)}>
122
+ <div>
123
+ <SettingsCardTitle>{labels.quietTitle}</SettingsCardTitle>
124
+ <div className={CARD_DESCRIPTION_CLASS}>{labels.quietDescription(inbox)}</div>
125
+ </div>
126
+ <div className="flex flex-wrap items-end gap-3">
127
+ <TimeInput
128
+ id={`${id}-start`}
129
+ label={labels.quietFrom}
130
+ value={startShown ?? ""}
131
+ disabled={disabled}
132
+ commit={COMMIT_EXCEPT_BILLING}
133
+ onValueChange={(value) => saveTime("quiet_hours_start", value)}
134
+ className="w-36"
135
+ />
136
+ <TimeInput
137
+ id={`${id}-end`}
138
+ label={labels.quietUntil}
139
+ value={endShown ?? ""}
140
+ disabled={disabled}
141
+ commit={COMMIT_EXCEPT_BILLING}
142
+ onValueChange={(value) => saveTime("quiet_hours_end", value)}
143
+ className="w-36"
144
+ />
145
+ </div>
146
+ {halfSet && (
147
+ <p className="text-xs text-warning" data-quiet-incomplete="">
148
+ {labels.quietIncomplete}
149
+ </p>
150
+ )}
151
+ {anySet && (
152
+ <div className="space-y-2">
153
+ <TimeZoneSelect
154
+ id={`${id}-zone`}
155
+ value={effectiveZone}
156
+ disabled={disabled}
157
+ commit={COMMIT_EXCEPT_BILLING}
158
+ onChange={(zone) => save({ timezone: zone })}
159
+ className="max-w-72"
160
+ labels={labelsProp}
161
+ />
162
+ {device !== effectiveZone && (
163
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
164
+ <span id={`${id}-device`} className={CARD_DESCRIPTION_CLASS}>
165
+ {labels.deviceZone(zoneName(device, labels.regions))}
166
+ </span>
167
+ <Button
168
+ type="button"
169
+ size="sm"
170
+ variant="link"
171
+ disabled={disabled}
172
+ commit={COMMIT_EXCEPT_BILLING}
173
+ aria-describedby={`${id}-device`}
174
+ onClick={() => save({ timezone: device })}
175
+ >
176
+ {labels.useDeviceZone}
177
+ </Button>
178
+ </div>
179
+ )}
180
+ </div>
181
+ )}
182
+ <FailureLine>{failure}</FailureLine>
183
+ </Card>
184
+ );
185
+ }
@@ -0,0 +1,126 @@
1
+ import { useMemo } from "react";
2
+ import type { ReactNode } from "react";
3
+
4
+ import type { LabelOverride } from "../i18n/kit-labels";
5
+ import type { ComboOption } from "../components/combobox-core";
6
+ import { InlineEntityCombobox } from "../components/inline-entity-combobox";
7
+ import type { CommitScope } from "../components/write-lock";
8
+ import { useNotificationsLabels } from "./notification-parts";
9
+ import type { NotificationRegionLabels, NotificationsLabels } from "./notifications-labels";
10
+
11
+ /**
12
+ * This device's IANA zone ("Europe/Zurich"), or `"UTC"` where the runtime names none —
13
+ * the zone quiet hours are entered in the first time (§2.6), and the one the card offers
14
+ * when it differs from the account's.
15
+ */
16
+ export function deviceTimeZone(): string {
17
+ try {
18
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
19
+ } catch {
20
+ return "UTC";
21
+ }
22
+ }
23
+
24
+ /** Every zone the runtime lists; `[]` where `Intl.supportedValuesOf` is missing. */
25
+ function runtimeZones(): string[] {
26
+ const intl = Intl as typeof Intl & { supportedValuesOf?: (key: "timeZone") => string[] };
27
+ return typeof intl.supportedValuesOf === "function" ? intl.supportedValuesOf("timeZone") : [];
28
+ }
29
+
30
+ /** "Argentina/Buenos_Aires" → "Argentina / Buenos Aires": the zone's city, as a row. */
31
+ function cityOf(rest: string): string {
32
+ return rest.replace(/_/g, " ").replace(/\//g, " / ");
33
+ }
34
+
35
+ /**
36
+ * The zone picker's rows (keksdose `notifications-card.tsx:131-146`): every zone `Intl`
37
+ * lists, as its city ("Argentina / Buenos Aires") under its region, the region named by
38
+ * `regionLabel` — which answers `undefined` for a region it does not know, and the region
39
+ * keeps its IANA name. A zone with no region ("UTC") comes first, ahead of every heading,
40
+ * rather than trailing the last group as if it were Pacific. `current` is kept even when
41
+ * the runtime does not list it (an old alias such as "Europe/Kiev"): a picker without its
42
+ * own value would show an empty field.
43
+ */
44
+ export function timeZoneOptions(
45
+ current: string,
46
+ regionLabel: (region: string) => string | undefined,
47
+ ): ComboOption<string>[] {
48
+ const zones = runtimeZones();
49
+ const all = !current || zones.includes(current) ? zones : [...zones, current].sort();
50
+ const rows = all.map((zone): ComboOption<string> => {
51
+ const slash = zone.indexOf("/");
52
+ if (slash < 0) return { value: zone, label: zone };
53
+ const region = zone.slice(0, slash);
54
+ return { value: zone, label: cityOf(zone.slice(slash + 1)), group: regionLabel(region) ?? region };
55
+ });
56
+ return [...rows.filter((row) => !row.group), ...rows.filter((row) => row.group)];
57
+ }
58
+
59
+ /** The region's name in `regions`, or `undefined` for one the labels do not know.
60
+ * @internal Not part of the barrel. */
61
+ export function regionName(regions: NotificationRegionLabels, region: string): string | undefined {
62
+ return Object.prototype.hasOwnProperty.call(regions, region)
63
+ ? regions[region as keyof NotificationRegionLabels]
64
+ : undefined;
65
+ }
66
+
67
+ /** A zone as the picker shows it, for a sentence: "Europe / Zurich", the region in the
68
+ * kit's words. @internal Not part of the barrel. */
69
+ export function zoneName(zone: string, regions: NotificationRegionLabels): string {
70
+ const slash = zone.indexOf("/");
71
+ if (slash < 0) return zone;
72
+ const region = zone.slice(0, slash);
73
+ return `${regionName(regions, region) ?? region} / ${cityOf(zone.slice(slash + 1))}`;
74
+ }
75
+
76
+ export interface TimeZoneSelectProps {
77
+ /** The IANA zone. Not clearable: a zone can be changed, never emptied. */
78
+ value: string;
79
+ /** The zone picked. Never called for the value it already has. */
80
+ onChange: (zone: string) => void;
81
+ /** Default `labels.timeZone`. */
82
+ label?: ReactNode;
83
+ disabled?: boolean;
84
+ /** The input's id. */
85
+ id?: string;
86
+ className?: string;
87
+ /** The picker SAVES on change (quiet hours do): locked under a write lock in scope. */
88
+ commit?: CommitScope;
89
+ labels?: LabelOverride<NotificationsLabels>;
90
+ }
91
+
92
+ /**
93
+ * An IANA time zone, searchable (docs/notifications-harmonization.md §4.2, keksdose's
94
+ * quiet-hours picker): type "Zur" for Europe/Zurich, and on a phone the kit's sheet with
95
+ * a search box instead of a 400-row wheel. Rows are the city under its region, the region
96
+ * in the kit's words (`labels.regions`); a zone with no region first; a stored zone the
97
+ * runtime does not list is kept. On the kit's {@link InlineEntityCombobox}.
98
+ */
99
+ export function TimeZoneSelect({
100
+ value,
101
+ onChange,
102
+ label,
103
+ disabled,
104
+ id,
105
+ className,
106
+ commit,
107
+ labels: labelsProp,
108
+ }: TimeZoneSelectProps) {
109
+ const labels = useNotificationsLabels(labelsProp);
110
+ const { regions } = labels;
111
+ const options = useMemo(() => timeZoneOptions(value, (region) => regionName(regions, region)), [value, regions]);
112
+ return (
113
+ <InlineEntityCombobox<string>
114
+ id={id}
115
+ label={label ?? labels.timeZone}
116
+ value={value}
117
+ options={options}
118
+ disabled={disabled}
119
+ commit={commit}
120
+ onChange={(zone) => {
121
+ if (zone && zone !== value) onChange(zone);
122
+ }}
123
+ className={className}
124
+ />
125
+ );
126
+ }