@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.
- package/README.md +115 -41
- package/dist/admin/role-select.d.ts +3 -2
- package/dist/admin/role-select.js +2 -0
- package/dist/admin/role-select.js.map +1 -1
- package/dist/billing/billing-banner.d.ts +5 -2
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/plan-limit-notice.d.ts +5 -2
- package/dist/billing/plan-limit-notice.js.map +1 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -5
- package/dist/components/amount-input.js +2 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +2 -2
- package/dist/components/autocomplete.js +2 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-look.d.ts +1 -1
- package/dist/components/button-look.js +9 -4
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/calculator.d.ts +1 -4
- package/dist/components/calculator.js +1 -3
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.d.ts +0 -37
- package/dist/components/chart-zoom.js +1 -28
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/checkbox-group.d.ts +5 -2
- package/dist/components/checkbox-group.js +15 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/chip-input.d.ts +0 -4
- package/dist/components/chip-input.js +1 -5
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/column-mapper.d.ts +5 -3
- package/dist/components/column-mapper.js +2 -0
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox.d.ts +3 -3
- package/dist/components/combobox.js +2 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +5 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.d.ts +3 -3
- package/dist/components/country-select.js +3 -0
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.d.ts +3 -8
- package/dist/components/currency-option-list.js +1 -29
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.d.ts +3 -2
- package/dist/components/currency-select.js +10 -1
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +3 -3
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +1 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +1 -42
- package/dist/components/data-table-labels.js +2 -31
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +17 -1
- package/dist/components/data-table-shared.js +22 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table.d.ts +12 -2
- package/dist/components/data-table.js +18 -14
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +2 -1
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.js +3 -0
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js +3 -0
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +3 -1
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/dropdown.d.ts +18 -15
- package/dist/components/dropdown.js +11 -3
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +3 -3
- package/dist/components/entity-combobox.js +2 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/field-base.d.ts +0 -5
- package/dist/components/field-base.js +0 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +69 -22
- package/dist/components/field-parts.js +33 -19
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-strip.d.ts +9 -23
- package/dist/components/field-strip.js +26 -23
- package/dist/components/field-strip.js.map +1 -1
- package/dist/components/field-sync.d.ts +0 -4
- package/dist/components/field-sync.js +0 -4
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/field.d.ts +7 -6
- package/dist/components/field.js +24 -15
- package/dist/components/field.js.map +1 -1
- package/dist/components/full-bleed-dialog.js +2 -2
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/help-popover.d.ts +9 -7
- package/dist/components/help-popover.js.map +1 -1
- package/dist/components/hover-menu.d.ts +6 -13
- package/dist/components/hover-menu.js +3 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +3 -2
- package/dist/components/icon-picker.js +3 -0
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-entity-combobox.d.ts +3 -3
- package/dist/components/inline-entity-combobox.js +2 -1
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.d.ts +2 -2
- package/dist/components/input.js +2 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/modal.d.ts +9 -1
- package/dist/components/modal.js +7 -4
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +2 -1
- package/dist/components/month-picker.js +8 -1
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +2 -2
- package/dist/components/multi-entity-combobox.js +2 -1
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +7 -18
- package/dist/components/multi-select.js +8 -10
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-input.d.ts +2 -5
- package/dist/components/number-input.js +2 -4
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numeric-field.d.ts +5 -2
- package/dist/components/numeric-field.js +4 -1
- package/dist/components/numeric-field.js.map +1 -1
- package/dist/components/one-time-code-input.d.ts +2 -1
- package/dist/components/one-time-code-input.js +2 -1
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/phone-input.d.ts +1 -1
- package/dist/components/phone-input.js +4 -0
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/popover.js +3 -3
- package/dist/components/popover.js.map +1 -1
- package/dist/components/select.d.ts +4 -3
- package/dist/components/select.js +2 -1
- package/dist/components/select.js.map +1 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/server-wake.d.ts +0 -6
- package/dist/components/server-wake.js.map +1 -1
- package/dist/components/settings-fields.d.ts +4 -2
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/slider.d.ts +3 -2
- package/dist/components/slider.js +7 -1
- package/dist/components/slider.js.map +1 -1
- package/dist/components/stat-tile.d.ts +8 -2
- package/dist/components/stat-tile.js +2 -11
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +3 -2
- package/dist/components/swatch-picker.js +3 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +2 -17
- package/dist/components/swipeable-row.js +11 -16
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/text-link.js +3 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/textarea.d.ts +2 -1
- package/dist/components/textarea.js +9 -3
- package/dist/components/textarea.js.map +1 -1
- package/dist/components/toggle-group.d.ts +2 -2
- package/dist/components/toggle-group.js +7 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +9 -1
- package/dist/components/tooltip.js +20 -5
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review.d.ts +10 -2
- package/dist/components/translation-review.js +11 -3
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/use-table-state.d.ts +31 -3
- package/dist/components/use-table-state.js +53 -8
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +2 -2
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.d.ts +1 -4
- package/dist/components/wizard-stepper.js +2 -3
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/currencies.d.ts +23 -0
- package/dist/currencies.js +38 -0
- package/dist/currencies.js.map +1 -0
- package/dist/demo/demo-banner.d.ts +7 -2
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/eslint/index.d.ts +83 -0
- package/dist/eslint/index.js +69 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/eslint/role-utilities.d.ts +48 -0
- package/dist/eslint/role-utilities.generated.d.ts +2 -0
- package/dist/eslint/role-utilities.generated.js +143 -0
- package/dist/eslint/role-utilities.generated.js.map +1 -0
- package/dist/eslint/role-utilities.js +43 -0
- package/dist/eslint/role-utilities.js.map +1 -0
- package/dist/feedback/feedback-menu.d.ts +2 -2
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +3 -8
- package/dist/feedback/feedback-thread.js +0 -2
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +16 -3
- package/dist/hooks/use-dismiss.js +14 -2
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +97 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +29 -4
- package/dist/i18n/kit-labels.js +21 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +104 -3
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +95 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +99 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +95 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +99 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +95 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +13 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +3 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/device-settings.d.ts +72 -0
- package/dist/lib/device-settings.js +64 -0
- package/dist/lib/device-settings.js.map +1 -0
- package/dist/lib/plain-text.d.ts +11 -0
- package/dist/lib/plain-text.js +16 -0
- package/dist/lib/plain-text.js.map +1 -0
- package/dist/lib/refs.d.ts +13 -0
- package/dist/lib/refs.js +15 -0
- package/dist/lib/refs.js.map +1 -0
- package/dist/lib/safe-storage.d.ts +6 -0
- package/dist/lib/safe-storage.js +11 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +39 -0
- package/dist/notifications/notification-parts.js +47 -0
- package/dist/notifications/notification-parts.js.map +1 -0
- package/dist/notifications/notification-types-setting.d.ts +62 -0
- package/dist/notifications/notification-types-setting.js +192 -0
- package/dist/notifications/notification-types-setting.js.map +1 -0
- package/dist/notifications/notification-types.d.ts +75 -0
- package/dist/notifications/notification-types.js +2 -0
- package/dist/notifications/notification-types.js.map +1 -0
- package/dist/notifications/notifications-labels.d.ts +131 -0
- package/dist/notifications/notifications-labels.js +83 -0
- package/dist/notifications/notifications-labels.js.map +1 -0
- package/dist/notifications/push-device-setting.d.ts +37 -0
- package/dist/notifications/push-device-setting.js +164 -0
- package/dist/notifications/push-device-setting.js.map +1 -0
- package/dist/notifications/push-devices-setting.d.ts +44 -0
- package/dist/notifications/push-devices-setting.js +203 -0
- package/dist/notifications/push-devices-setting.js.map +1 -0
- package/dist/notifications/quiet-hours-setting.d.ts +47 -0
- package/dist/notifications/quiet-hours-setting.js +131 -0
- package/dist/notifications/quiet-hours-setting.js.map +1 -0
- package/dist/notifications/time-zone-select.d.ts +50 -0
- package/dist/notifications/time-zone-select.js +76 -0
- package/dist/notifications/time-zone-select.js.map +1 -0
- package/dist/notifications/unsubscribe-panel.d.ts +53 -0
- package/dist/notifications/unsubscribe-panel.js +139 -0
- package/dist/notifications/unsubscribe-panel.js.map +1 -0
- package/dist/notifications/use-push-device.d.ts +122 -0
- package/dist/notifications/use-push-device.js +230 -0
- package/dist/notifications/use-push-device.js.map +1 -0
- package/dist/rhf/fields.d.ts +36 -29
- package/dist/rhf/fields.js +51 -22
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.d.ts +5 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/form.d.ts +7 -36
- package/dist/rhf/form.js +1 -16
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf/line-items.d.ts +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/rhf.js.map +1 -1
- package/dist/settings/device-reset-setting-labels.d.ts +3 -0
- package/dist/settings/device-reset-setting-labels.js +15 -0
- package/dist/settings/device-reset-setting-labels.js.map +1 -0
- package/dist/settings/device-reset-setting.d.ts +58 -0
- package/dist/settings/device-reset-setting.js +51 -0
- package/dist/settings/device-reset-setting.js.map +1 -0
- package/dist/shell/app-shell.js +5 -0
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.d.ts +5 -17
- package/dist/shell/option-switcher-menu.js +1 -3
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +1 -4
- package/dist/shell/role-switcher.js +2 -3
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +7 -19
- package/dist/shell/topbar-action-menu.js +1 -3
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +3 -12
- package/dist/shell/topbar-controls.js +6 -9
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/viewport.d.ts +25 -2
- package/dist/testing/viewport.js +61 -10
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/contrast-tokens.d.ts +42 -4
- package/dist/theme/contrast-tokens.js +56 -9
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +4 -2
- package/dist/theme/contrast.js +13 -0
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-derive.d.ts +15 -1
- package/dist/theme/palette-derive.js +37 -8
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +2 -8
- package/dist/theme/palette-presets.js +5 -284
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +6 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +6 -0
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +6 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/wizard/types.d.ts +0 -5
- package/dist/wizard/types.js +1 -5
- package/dist/wizard/types.js.map +1 -1
- package/package.json +9 -1
- package/src/admin/role-select.tsx +4 -1
- package/src/billing/billing-banner.tsx +5 -2
- package/src/billing/plan-limit-notice.tsx +5 -2
- package/src/chart.ts +5 -3
- package/src/components/amount-input.tsx +4 -7
- package/src/components/autocomplete.tsx +5 -2
- package/src/components/button-look.ts +15 -5
- package/src/components/calculator.tsx +2 -7
- package/src/components/chart-zoom.tsx +4 -47
- package/src/components/checkbox-group.tsx +22 -4
- package/src/components/chip-input.tsx +0 -7
- package/src/components/column-mapper.tsx +6 -2
- package/src/components/combobox.tsx +6 -3
- package/src/components/copy-button.tsx +5 -1
- package/src/components/country-select.tsx +6 -1
- package/src/components/currency-option-list.tsx +8 -42
- package/src/components/currency-select.tsx +12 -3
- package/src/components/danger-confirm.tsx +3 -3
- package/src/components/data-table-filter-popover.tsx +2 -1
- package/src/components/data-table-labels.ts +2 -84
- package/src/components/data-table-shared.tsx +55 -2
- package/src/components/data-table.tsx +29 -17
- package/src/components/date-field.tsx +2 -2
- package/src/components/date-picker.tsx +3 -0
- package/src/components/date-range-picker.tsx +3 -0
- package/src/components/dialog-frame.tsx +3 -1
- package/src/components/dropdown.tsx +42 -18
- package/src/components/entity-combobox.tsx +6 -3
- package/src/components/field-base.tsx +0 -11
- package/src/components/field-parts.tsx +112 -38
- package/src/components/field-strip.tsx +12 -27
- package/src/components/field-sync.tsx +0 -7
- package/src/components/field.tsx +32 -19
- package/src/components/full-bleed-dialog.tsx +4 -3
- package/src/components/help-popover.tsx +9 -7
- package/src/components/hover-menu.tsx +12 -16
- package/src/components/icon-picker.tsx +8 -3
- package/src/components/inline-entity-combobox.tsx +6 -3
- package/src/components/input.tsx +4 -2
- package/src/components/modal.tsx +20 -4
- package/src/components/month-picker.tsx +10 -3
- package/src/components/multi-entity-combobox.tsx +5 -2
- package/src/components/multi-select.tsx +19 -31
- package/src/components/number-input.tsx +5 -10
- package/src/components/numeric-field.ts +10 -2
- package/src/components/one-time-code-input.tsx +5 -3
- package/src/components/phone-input.tsx +4 -0
- package/src/components/popover.tsx +8 -4
- package/src/components/select.tsx +6 -3
- package/src/components/series-chart.tsx +1 -1
- package/src/components/server-wake.tsx +0 -6
- package/src/components/settings-fields.tsx +4 -2
- package/src/components/slider.tsx +10 -3
- package/src/components/stat-tile.tsx +11 -20
- package/src/components/swatch-picker.tsx +9 -4
- package/src/components/swipeable-row.tsx +15 -37
- package/src/components/text-link.tsx +3 -1
- package/src/components/textarea.tsx +11 -4
- package/src/components/toggle-group.tsx +21 -5
- package/src/components/tooltip.tsx +37 -6
- package/src/components/translation-review.tsx +25 -7
- package/src/components/use-table-state.ts +91 -10
- package/src/components/user-avatar.tsx +2 -2
- package/src/components/wizard-stepper.tsx +2 -6
- package/src/currencies.ts +70 -0
- package/src/demo/demo-banner.tsx +7 -2
- package/src/eslint/index.ts +161 -0
- package/src/eslint/role-utilities.generated.ts +146 -0
- package/src/eslint/role-utilities.ts +99 -0
- package/src/feedback/feedback-menu.tsx +2 -2
- package/src/feedback/feedback-thread.tsx +3 -8
- package/src/hooks/use-dismiss.ts +39 -4
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/german.ts +113 -0
- package/src/i18n/kit-labels.tsx +66 -6
- package/src/i18n/locales/en.ts +6 -0
- package/src/i18n/locales/es.ts +106 -0
- package/src/i18n/locales/fr.ts +109 -2
- package/src/i18n/locales/hu.ts +104 -0
- package/src/i18n/locales/it.ts +108 -2
- package/src/i18n/locales/zh.ts +96 -0
- package/src/i18n/review.ts +13 -0
- package/src/i18n/shared-labels.ts +2 -0
- package/src/index.ts +53 -1
- package/src/lib/device-settings.ts +146 -0
- package/src/lib/plain-text.ts +22 -0
- package/src/lib/refs.ts +22 -0
- package/src/lib/safe-storage.ts +16 -0
- package/src/notifications/notification-parts.tsx +83 -0
- package/src/notifications/notification-types-setting.tsx +309 -0
- package/src/notifications/notification-types.ts +80 -0
- package/src/notifications/notifications-labels.ts +238 -0
- package/src/notifications/push-device-setting.tsx +224 -0
- package/src/notifications/push-devices-setting.tsx +306 -0
- package/src/notifications/quiet-hours-setting.tsx +185 -0
- package/src/notifications/time-zone-select.tsx +126 -0
- package/src/notifications/unsubscribe-panel.tsx +203 -0
- package/src/notifications/use-push-device.ts +392 -0
- package/src/rhf/fields.tsx +87 -39
- package/src/rhf/form-core.tsx +5 -4
- package/src/rhf/form.tsx +7 -43
- package/src/rhf/line-items.tsx +1 -1
- package/src/rhf/use-rhf-wizard-step.ts +4 -4
- package/src/rhf.ts +3 -2
- package/src/settings/device-reset-setting-labels.ts +20 -0
- package/src/settings/device-reset-setting.tsx +119 -0
- package/src/shell/app-shell.tsx +8 -0
- package/src/shell/option-switcher-menu.tsx +5 -22
- package/src/shell/role-switcher.tsx +2 -6
- package/src/shell/topbar-action-menu.tsx +7 -24
- package/src/shell/topbar-controls.tsx +6 -18
- package/src/testing/index.ts +3 -1
- package/src/testing/viewport.ts +125 -19
- package/src/theme/contrast-tokens.ts +106 -12
- package/src/theme/contrast.ts +30 -4
- package/src/theme/palette-derive.ts +68 -7
- package/src/theme/palette-presets.ts +16 -299
- package/src/theme/palette-store.ts +12 -0
- package/src/theme/text-size.ts +8 -0
- package/src/theme/theme-store.ts +10 -0
- package/src/wizard/types.ts +0 -8
- 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
|
+
}
|