@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,47 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
import type { LabelOverride } from "../i18n/kit-labels.js";
|
|
3
|
+
import type { NotificationsLabels } from "./notifications-labels.js";
|
|
4
|
+
import type { NotificationSettingsPatch } from "./notification-types.js";
|
|
5
|
+
export interface QuietHoursSettingProps extends Omit<ComponentPropsWithRef<"div">, "children" | "onChange"> {
|
|
6
|
+
/** The `GET`'s `quiet_hours_start`, `"HH:MM"`, or `null`. */
|
|
7
|
+
start: string | null;
|
|
8
|
+
/** The `GET`'s `quiet_hours_end`. */
|
|
9
|
+
end: string | null;
|
|
10
|
+
/** The `GET`'s `timezone`, an IANA name, or `null` (the app's zone applies). */
|
|
11
|
+
timezone: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Save one change — `PATCH /notifications/settings`: a time (`null` when cleared), with
|
|
14
|
+
* the device's zone the first time one is entered while `timezone` is `null`; or
|
|
15
|
+
* `{timezone}`. The field shows the new value while the call runs and goes back if it
|
|
16
|
+
* rejects.
|
|
17
|
+
*/
|
|
18
|
+
onChange: (patch: NotificationSettingsPatch) => Promise<unknown> | void;
|
|
19
|
+
/** The zone shown, and the one the server applies, while `timezone` is `null`.
|
|
20
|
+
* Default `"Europe/Zurich"`, every app's (decision 6). */
|
|
21
|
+
fallbackZone?: string;
|
|
22
|
+
/** The app keeps an inbox, and the description says a held push still reaches it. */
|
|
23
|
+
inbox?: boolean;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
/** The app's own words for a failed save, or `undefined` for the kit's `failed`. */
|
|
26
|
+
describeError?: (error: unknown) => ReactNode | undefined;
|
|
27
|
+
labels?: LabelOverride<NotificationsLabels>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Quiet hours (docs/notifications-harmonization.md §2.4, §2.6, §4.2): a window of local
|
|
31
|
+
* times in the account's zone, which may cross midnight, when no push of a machine-sent
|
|
32
|
+
* type is sent. A held push is dropped, not delayed; the inbox row and the mail go as
|
|
33
|
+
* usual. keksdose's block, as a card of its own.
|
|
34
|
+
*
|
|
35
|
+
* - **Two times**, each saved as it is entered; a cleared time sends `null`. While only
|
|
36
|
+
* one is set the card says so: the window fails open until both are.
|
|
37
|
+
* - **The zone** shows while a time is set. The first time entered while the account has
|
|
38
|
+
* none sends this device's zone with it, so "22:00" means the person's 22:00
|
|
39
|
+
* (keksdose `notifications-card.tsx:288-292`). With none stored, the card shows
|
|
40
|
+
* `fallbackZone` — the zone the server then applies — never the device's (keksdose's
|
|
41
|
+
* card showed the device's zone while its server applied UTC).
|
|
42
|
+
* - **"This device is in …"** with "Use it" when the device's zone is not the one in
|
|
43
|
+
* effect. Never written on its own: a zone is written by a person's pick (§2.6).
|
|
44
|
+
*
|
|
45
|
+
* Under a write lock the fields are `commit` controls exempt from billing.
|
|
46
|
+
*/
|
|
47
|
+
export declare function QuietHoursSetting({ start, end, timezone, onChange, fallbackZone, inbox, disabled, describeError, labels: labelsProp, className, ...rest }: QuietHoursSettingProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { Button } from "../components/button.js";
|
|
6
|
+
import { Card } from "../components/card.js";
|
|
7
|
+
import { TimeInput } from "../components/time-input.js";
|
|
8
|
+
import { COMMIT_EXCEPT_BILLING } from "../components/write-lock.js";
|
|
9
|
+
import { CARD_DESCRIPTION_CLASS } from "../settings/settings-card-classes.js";
|
|
10
|
+
import { SettingsCardTitle } from "../settings/settings-heading.js";
|
|
11
|
+
import { settle, useMounted } from "../account/account-parts.js";
|
|
12
|
+
import { describeFailure, FailureLine, useNotificationsLabels, useWriteOverlay } from "./notification-parts.js";
|
|
13
|
+
import { deviceTimeZone, TimeZoneSelect, zoneName } from "./time-zone-select.js";
|
|
14
|
+
const DEFAULT_FALLBACK_ZONE = "Europe/Zurich";
|
|
15
|
+
function QuietHoursSetting({
|
|
16
|
+
start,
|
|
17
|
+
end,
|
|
18
|
+
timezone,
|
|
19
|
+
onChange,
|
|
20
|
+
fallbackZone = DEFAULT_FALLBACK_ZONE,
|
|
21
|
+
inbox = false,
|
|
22
|
+
disabled = false,
|
|
23
|
+
describeError,
|
|
24
|
+
labels: labelsProp,
|
|
25
|
+
className,
|
|
26
|
+
...rest
|
|
27
|
+
}) {
|
|
28
|
+
const labels = useNotificationsLabels(labelsProp);
|
|
29
|
+
const overlay = useWriteOverlay(`${start}|${end}|${timezone}`);
|
|
30
|
+
const [failure, setFailure] = useState(null);
|
|
31
|
+
const mounted = useMounted();
|
|
32
|
+
const id = useId();
|
|
33
|
+
const shown = (field, server) => overlay.has(field) ? overlay.get(field) ?? null : server;
|
|
34
|
+
const startShown = shown("quiet_hours_start", start);
|
|
35
|
+
const endShown = shown("quiet_hours_end", end);
|
|
36
|
+
const zoneShown = shown("timezone", timezone);
|
|
37
|
+
const effectiveZone = zoneShown ?? fallbackZone;
|
|
38
|
+
const device = deviceTimeZone();
|
|
39
|
+
const save = (patch) => {
|
|
40
|
+
const fields = Object.keys(patch);
|
|
41
|
+
setFailure(null);
|
|
42
|
+
for (const field of fields) overlay.set(field, patch[field] ?? null);
|
|
43
|
+
settle(() => onChange(patch)).then(
|
|
44
|
+
() => {
|
|
45
|
+
if (mounted.current) for (const field of fields) overlay.settle(field, true);
|
|
46
|
+
},
|
|
47
|
+
(error) => {
|
|
48
|
+
if (!mounted.current) return;
|
|
49
|
+
for (const field of fields) overlay.settle(field, false);
|
|
50
|
+
setFailure(describeFailure(describeError?.(error), labels.failed));
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
const saveTime = (field, value) => {
|
|
55
|
+
const next = value || null;
|
|
56
|
+
if (next === shown(field, field === "quiet_hours_start" ? start : end)) return;
|
|
57
|
+
const patch = { [field]: next };
|
|
58
|
+
if (next && zoneShown === null) patch.timezone = device;
|
|
59
|
+
save(patch);
|
|
60
|
+
};
|
|
61
|
+
const anySet = Boolean(startShown || endShown);
|
|
62
|
+
const halfSet = Boolean(startShown) !== Boolean(endShown);
|
|
63
|
+
return /* @__PURE__ */ jsxs(Card, { ...rest, className: cn("p-4 space-y-3", className), children: [
|
|
64
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
65
|
+
/* @__PURE__ */ jsx(SettingsCardTitle, { children: labels.quietTitle }),
|
|
66
|
+
/* @__PURE__ */ jsx("div", { className: CARD_DESCRIPTION_CLASS, children: labels.quietDescription(inbox) })
|
|
67
|
+
] }),
|
|
68
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [
|
|
69
|
+
/* @__PURE__ */ jsx(
|
|
70
|
+
TimeInput,
|
|
71
|
+
{
|
|
72
|
+
id: `${id}-start`,
|
|
73
|
+
label: labels.quietFrom,
|
|
74
|
+
value: startShown ?? "",
|
|
75
|
+
disabled,
|
|
76
|
+
commit: COMMIT_EXCEPT_BILLING,
|
|
77
|
+
onValueChange: (value) => saveTime("quiet_hours_start", value),
|
|
78
|
+
className: "w-36"
|
|
79
|
+
}
|
|
80
|
+
),
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
TimeInput,
|
|
83
|
+
{
|
|
84
|
+
id: `${id}-end`,
|
|
85
|
+
label: labels.quietUntil,
|
|
86
|
+
value: endShown ?? "",
|
|
87
|
+
disabled,
|
|
88
|
+
commit: COMMIT_EXCEPT_BILLING,
|
|
89
|
+
onValueChange: (value) => saveTime("quiet_hours_end", value),
|
|
90
|
+
className: "w-36"
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
] }),
|
|
94
|
+
halfSet && /* @__PURE__ */ jsx("p", { className: "text-xs text-warning", "data-quiet-incomplete": "", children: labels.quietIncomplete }),
|
|
95
|
+
anySet && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
96
|
+
/* @__PURE__ */ jsx(
|
|
97
|
+
TimeZoneSelect,
|
|
98
|
+
{
|
|
99
|
+
id: `${id}-zone`,
|
|
100
|
+
value: effectiveZone,
|
|
101
|
+
disabled,
|
|
102
|
+
commit: COMMIT_EXCEPT_BILLING,
|
|
103
|
+
onChange: (zone) => save({ timezone: zone }),
|
|
104
|
+
className: "max-w-72",
|
|
105
|
+
labels: labelsProp
|
|
106
|
+
}
|
|
107
|
+
),
|
|
108
|
+
device !== effectiveZone && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [
|
|
109
|
+
/* @__PURE__ */ jsx("span", { id: `${id}-device`, className: CARD_DESCRIPTION_CLASS, children: labels.deviceZone(zoneName(device, labels.regions)) }),
|
|
110
|
+
/* @__PURE__ */ jsx(
|
|
111
|
+
Button,
|
|
112
|
+
{
|
|
113
|
+
type: "button",
|
|
114
|
+
size: "sm",
|
|
115
|
+
variant: "link",
|
|
116
|
+
disabled,
|
|
117
|
+
commit: COMMIT_EXCEPT_BILLING,
|
|
118
|
+
"aria-describedby": `${id}-device`,
|
|
119
|
+
onClick: () => save({ timezone: device }),
|
|
120
|
+
children: labels.useDeviceZone
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
] })
|
|
124
|
+
] }),
|
|
125
|
+
/* @__PURE__ */ jsx(FailureLine, { children: failure })
|
|
126
|
+
] });
|
|
127
|
+
}
|
|
128
|
+
export {
|
|
129
|
+
QuietHoursSetting
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=quiet-hours-setting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/notifications/quiet-hours-setting.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { TimeInput } from \"../components/time-input\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { CARD_DESCRIPTION_CLASS } from \"../settings/settings-card-classes\";\nimport { SettingsCardTitle } from \"../settings/settings-heading\";\nimport { settle, useMounted } from \"../account/account-parts\";\nimport { describeFailure, FailureLine, useNotificationsLabels, useWriteOverlay } from \"./notification-parts\";\nimport type { NotificationsLabels } from \"./notifications-labels\";\nimport type { NotificationSettingsPatch } from \"./notification-types\";\nimport { deviceTimeZone, TimeZoneSelect, zoneName } from \"./time-zone-select\";\n\n/** The zone the server applies while the account has none (§2.6): every app's. */\nconst DEFAULT_FALLBACK_ZONE = \"Europe/Zurich\";\n\nexport interface QuietHoursSettingProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onChange\"> {\n /** The `GET`'s `quiet_hours_start`, `\"HH:MM\"`, or `null`. */\n start: string | null;\n /** The `GET`'s `quiet_hours_end`. */\n end: string | null;\n /** The `GET`'s `timezone`, an IANA name, or `null` (the app's zone applies). */\n timezone: string | null;\n /**\n * Save one change — `PATCH /notifications/settings`: a time (`null` when cleared), with\n * the device's zone the first time one is entered while `timezone` is `null`; or\n * `{timezone}`. The field shows the new value while the call runs and goes back if it\n * rejects.\n */\n onChange: (patch: NotificationSettingsPatch) => Promise<unknown> | void;\n /** The zone shown, and the one the server applies, while `timezone` is `null`.\n * Default `\"Europe/Zurich\"`, every app's (decision 6). */\n fallbackZone?: string;\n /** The app keeps an inbox, and the description says a held push still reaches it. */\n inbox?: boolean;\n disabled?: boolean;\n /** The app's own words for a failed save, or `undefined` for the kit's `failed`. */\n describeError?: (error: unknown) => ReactNode | undefined;\n labels?: LabelOverride<NotificationsLabels>;\n}\n\ntype QuietField = \"quiet_hours_start\" | \"quiet_hours_end\" | \"timezone\";\n\n/**\n * Quiet hours (docs/notifications-harmonization.md §2.4, §2.6, §4.2): a window of local\n * times in the account's zone, which may cross midnight, when no push of a machine-sent\n * type is sent. A held push is dropped, not delayed; the inbox row and the mail go as\n * usual. keksdose's block, as a card of its own.\n *\n * - **Two times**, each saved as it is entered; a cleared time sends `null`. While only\n * one is set the card says so: the window fails open until both are.\n * - **The zone** shows while a time is set. The first time entered while the account has\n * none sends this device's zone with it, so \"22:00\" means the person's 22:00\n * (keksdose `notifications-card.tsx:288-292`). With none stored, the card shows\n * `fallbackZone` — the zone the server then applies — never the device's (keksdose's\n * card showed the device's zone while its server applied UTC).\n * - **\"This device is in …\"** with \"Use it\" when the device's zone is not the one in\n * effect. Never written on its own: a zone is written by a person's pick (§2.6).\n *\n * Under a write lock the fields are `commit` controls exempt from billing.\n */\nexport function QuietHoursSetting({\n start,\n end,\n timezone,\n onChange,\n fallbackZone = DEFAULT_FALLBACK_ZONE,\n inbox = false,\n disabled = false,\n describeError,\n labels: labelsProp,\n className,\n ...rest\n}: QuietHoursSettingProps) {\n const labels = useNotificationsLabels(labelsProp);\n const overlay = useWriteOverlay<string | null>(`${start}|${end}|${timezone}`);\n const [failure, setFailure] = useState<ReactNode>(null);\n const mounted = useMounted();\n const id = useId();\n\n const shown = (field: QuietField, server: string | null) =>\n overlay.has(field) ? (overlay.get(field) ?? null) : server;\n const startShown = shown(\"quiet_hours_start\", start);\n const endShown = shown(\"quiet_hours_end\", end);\n const zoneShown = shown(\"timezone\", timezone);\n const effectiveZone = zoneShown ?? fallbackZone;\n const device = deviceTimeZone();\n\n const save = (patch: NotificationSettingsPatch) => {\n const fields = Object.keys(patch) as QuietField[];\n setFailure(null);\n for (const field of fields) overlay.set(field, patch[field] ?? null);\n settle(() => onChange(patch)).then(\n () => {\n if (mounted.current) for (const field of fields) overlay.settle(field, true);\n },\n (error: unknown) => {\n if (!mounted.current) return;\n for (const field of fields) overlay.settle(field, false);\n setFailure(describeFailure(describeError?.(error), labels.failed));\n },\n );\n };\n\n const saveTime = (field: \"quiet_hours_start\" | \"quiet_hours_end\", value: string) => {\n const next = value || null;\n if (next === shown(field, field === \"quiet_hours_start\" ? start : end)) return;\n const patch: NotificationSettingsPatch = { [field]: next };\n if (next && zoneShown === null) patch.timezone = device;\n save(patch);\n };\n\n const anySet = Boolean(startShown || endShown);\n const halfSet = Boolean(startShown) !== Boolean(endShown);\n\n return (\n <Card {...rest} className={cn(\"p-4 space-y-3\", className)}>\n <div>\n <SettingsCardTitle>{labels.quietTitle}</SettingsCardTitle>\n <div className={CARD_DESCRIPTION_CLASS}>{labels.quietDescription(inbox)}</div>\n </div>\n <div className=\"flex flex-wrap items-end gap-3\">\n <TimeInput\n id={`${id}-start`}\n label={labels.quietFrom}\n value={startShown ?? \"\"}\n disabled={disabled}\n commit={COMMIT_EXCEPT_BILLING}\n onValueChange={(value) => saveTime(\"quiet_hours_start\", value)}\n className=\"w-36\"\n />\n <TimeInput\n id={`${id}-end`}\n label={labels.quietUntil}\n value={endShown ?? \"\"}\n disabled={disabled}\n commit={COMMIT_EXCEPT_BILLING}\n onValueChange={(value) => saveTime(\"quiet_hours_end\", value)}\n className=\"w-36\"\n />\n </div>\n {halfSet && (\n <p className=\"text-xs text-warning\" data-quiet-incomplete=\"\">\n {labels.quietIncomplete}\n </p>\n )}\n {anySet && (\n <div className=\"space-y-2\">\n <TimeZoneSelect\n id={`${id}-zone`}\n value={effectiveZone}\n disabled={disabled}\n commit={COMMIT_EXCEPT_BILLING}\n onChange={(zone) => save({ timezone: zone })}\n className=\"max-w-72\"\n labels={labelsProp}\n />\n {device !== effectiveZone && (\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <span id={`${id}-device`} className={CARD_DESCRIPTION_CLASS}>\n {labels.deviceZone(zoneName(device, labels.regions))}\n </span>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"link\"\n disabled={disabled}\n commit={COMMIT_EXCEPT_BILLING}\n aria-describedby={`${id}-device`}\n onClick={() => save({ timezone: device })}\n >\n {labels.useDeviceZone}\n </Button>\n </div>\n )}\n </div>\n )}\n <FailureLine>{failure}</FailureLine>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { LabelOverride } from "../i18n/kit-labels.js";
|
|
3
|
+
import type { ComboOption } from "../components/combobox-core.js";
|
|
4
|
+
import type { CommitScope } from "../components/write-lock.js";
|
|
5
|
+
import type { NotificationRegionLabels, NotificationsLabels } from "./notifications-labels.js";
|
|
6
|
+
/**
|
|
7
|
+
* This device's IANA zone ("Europe/Zurich"), or `"UTC"` where the runtime names none —
|
|
8
|
+
* the zone quiet hours are entered in the first time (§2.6), and the one the card offers
|
|
9
|
+
* when it differs from the account's.
|
|
10
|
+
*/
|
|
11
|
+
export declare function deviceTimeZone(): string;
|
|
12
|
+
/**
|
|
13
|
+
* The zone picker's rows (keksdose `notifications-card.tsx:131-146`): every zone `Intl`
|
|
14
|
+
* lists, as its city ("Argentina / Buenos Aires") under its region, the region named by
|
|
15
|
+
* `regionLabel` — which answers `undefined` for a region it does not know, and the region
|
|
16
|
+
* keeps its IANA name. A zone with no region ("UTC") comes first, ahead of every heading,
|
|
17
|
+
* rather than trailing the last group as if it were Pacific. `current` is kept even when
|
|
18
|
+
* the runtime does not list it (an old alias such as "Europe/Kiev"): a picker without its
|
|
19
|
+
* own value would show an empty field.
|
|
20
|
+
*/
|
|
21
|
+
export declare function timeZoneOptions(current: string, regionLabel: (region: string) => string | undefined): ComboOption<string>[];
|
|
22
|
+
/** The region's name in `regions`, or `undefined` for one the labels do not know.
|
|
23
|
+
* @internal Not part of the barrel. */
|
|
24
|
+
export declare function regionName(regions: NotificationRegionLabels, region: string): string | undefined;
|
|
25
|
+
/** A zone as the picker shows it, for a sentence: "Europe / Zurich", the region in the
|
|
26
|
+
* kit's words. @internal Not part of the barrel. */
|
|
27
|
+
export declare function zoneName(zone: string, regions: NotificationRegionLabels): string;
|
|
28
|
+
export interface TimeZoneSelectProps {
|
|
29
|
+
/** The IANA zone. Not clearable: a zone can be changed, never emptied. */
|
|
30
|
+
value: string;
|
|
31
|
+
/** The zone picked. Never called for the value it already has. */
|
|
32
|
+
onChange: (zone: string) => void;
|
|
33
|
+
/** Default `labels.timeZone`. */
|
|
34
|
+
label?: ReactNode;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** The input's id. */
|
|
37
|
+
id?: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
/** The picker SAVES on change (quiet hours do): locked under a write lock in scope. */
|
|
40
|
+
commit?: CommitScope;
|
|
41
|
+
labels?: LabelOverride<NotificationsLabels>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* An IANA time zone, searchable (docs/notifications-harmonization.md §4.2, keksdose's
|
|
45
|
+
* quiet-hours picker): type "Zur" for Europe/Zurich, and on a phone the kit's sheet with
|
|
46
|
+
* a search box instead of a 400-row wheel. Rows are the city under its region, the region
|
|
47
|
+
* in the kit's words (`labels.regions`); a zone with no region first; a stored zone the
|
|
48
|
+
* runtime does not list is kept. On the kit's {@link InlineEntityCombobox}.
|
|
49
|
+
*/
|
|
50
|
+
export declare function TimeZoneSelect({ value, onChange, label, disabled, id, className, commit, labels: labelsProp, }: TimeZoneSelectProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { InlineEntityCombobox } from "../components/inline-entity-combobox.js";
|
|
5
|
+
import { useNotificationsLabels } from "./notification-parts.js";
|
|
6
|
+
function deviceTimeZone() {
|
|
7
|
+
try {
|
|
8
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
9
|
+
} catch {
|
|
10
|
+
return "UTC";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function runtimeZones() {
|
|
14
|
+
const intl = Intl;
|
|
15
|
+
return typeof intl.supportedValuesOf === "function" ? intl.supportedValuesOf("timeZone") : [];
|
|
16
|
+
}
|
|
17
|
+
function cityOf(rest) {
|
|
18
|
+
return rest.replace(/_/g, " ").replace(/\//g, " / ");
|
|
19
|
+
}
|
|
20
|
+
function timeZoneOptions(current, regionLabel) {
|
|
21
|
+
const zones = runtimeZones();
|
|
22
|
+
const all = !current || zones.includes(current) ? zones : [...zones, current].sort();
|
|
23
|
+
const rows = all.map((zone) => {
|
|
24
|
+
const slash = zone.indexOf("/");
|
|
25
|
+
if (slash < 0) return { value: zone, label: zone };
|
|
26
|
+
const region = zone.slice(0, slash);
|
|
27
|
+
return { value: zone, label: cityOf(zone.slice(slash + 1)), group: regionLabel(region) ?? region };
|
|
28
|
+
});
|
|
29
|
+
return [...rows.filter((row) => !row.group), ...rows.filter((row) => row.group)];
|
|
30
|
+
}
|
|
31
|
+
function regionName(regions, region) {
|
|
32
|
+
return Object.prototype.hasOwnProperty.call(regions, region) ? regions[region] : void 0;
|
|
33
|
+
}
|
|
34
|
+
function zoneName(zone, regions) {
|
|
35
|
+
const slash = zone.indexOf("/");
|
|
36
|
+
if (slash < 0) return zone;
|
|
37
|
+
const region = zone.slice(0, slash);
|
|
38
|
+
return `${regionName(regions, region) ?? region} / ${cityOf(zone.slice(slash + 1))}`;
|
|
39
|
+
}
|
|
40
|
+
function TimeZoneSelect({
|
|
41
|
+
value,
|
|
42
|
+
onChange,
|
|
43
|
+
label,
|
|
44
|
+
disabled,
|
|
45
|
+
id,
|
|
46
|
+
className,
|
|
47
|
+
commit,
|
|
48
|
+
labels: labelsProp
|
|
49
|
+
}) {
|
|
50
|
+
const labels = useNotificationsLabels(labelsProp);
|
|
51
|
+
const { regions } = labels;
|
|
52
|
+
const options = useMemo(() => timeZoneOptions(value, (region) => regionName(regions, region)), [value, regions]);
|
|
53
|
+
return /* @__PURE__ */ jsx(
|
|
54
|
+
InlineEntityCombobox,
|
|
55
|
+
{
|
|
56
|
+
id,
|
|
57
|
+
label: label ?? labels.timeZone,
|
|
58
|
+
value,
|
|
59
|
+
options,
|
|
60
|
+
disabled,
|
|
61
|
+
commit,
|
|
62
|
+
onChange: (zone) => {
|
|
63
|
+
if (zone && zone !== value) onChange(zone);
|
|
64
|
+
},
|
|
65
|
+
className
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
TimeZoneSelect,
|
|
71
|
+
deviceTimeZone,
|
|
72
|
+
regionName,
|
|
73
|
+
timeZoneOptions,
|
|
74
|
+
zoneName
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=time-zone-select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/notifications/time-zone-select.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport type { ComboOption } from \"../components/combobox-core\";\nimport { InlineEntityCombobox } from \"../components/inline-entity-combobox\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useNotificationsLabels } from \"./notification-parts\";\nimport type { NotificationRegionLabels, NotificationsLabels } from \"./notifications-labels\";\n\n/**\n * This device's IANA zone (\"Europe/Zurich\"), or `\"UTC\"` where the runtime names none —\n * the zone quiet hours are entered in the first time (§2.6), and the one the card offers\n * when it differs from the account's.\n */\nexport function deviceTimeZone(): string {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || \"UTC\";\n } catch {\n return \"UTC\";\n }\n}\n\n/** Every zone the runtime lists; `[]` where `Intl.supportedValuesOf` is missing. */\nfunction runtimeZones(): string[] {\n const intl = Intl as typeof Intl & { supportedValuesOf?: (key: \"timeZone\") => string[] };\n return typeof intl.supportedValuesOf === \"function\" ? intl.supportedValuesOf(\"timeZone\") : [];\n}\n\n/** \"Argentina/Buenos_Aires\" → \"Argentina / Buenos Aires\": the zone's city, as a row. */\nfunction cityOf(rest: string): string {\n return rest.replace(/_/g, \" \").replace(/\\//g, \" / \");\n}\n\n/**\n * The zone picker's rows (keksdose `notifications-card.tsx:131-146`): every zone `Intl`\n * lists, as its city (\"Argentina / Buenos Aires\") under its region, the region named by\n * `regionLabel` — which answers `undefined` for a region it does not know, and the region\n * keeps its IANA name. A zone with no region (\"UTC\") comes first, ahead of every heading,\n * rather than trailing the last group as if it were Pacific. `current` is kept even when\n * the runtime does not list it (an old alias such as \"Europe/Kiev\"): a picker without its\n * own value would show an empty field.\n */\nexport function timeZoneOptions(\n current: string,\n regionLabel: (region: string) => string | undefined,\n): ComboOption<string>[] {\n const zones = runtimeZones();\n const all = !current || zones.includes(current) ? zones : [...zones, current].sort();\n const rows = all.map((zone): ComboOption<string> => {\n const slash = zone.indexOf(\"/\");\n if (slash < 0) return { value: zone, label: zone };\n const region = zone.slice(0, slash);\n return { value: zone, label: cityOf(zone.slice(slash + 1)), group: regionLabel(region) ?? region };\n });\n return [...rows.filter((row) => !row.group), ...rows.filter((row) => row.group)];\n}\n\n/** The region's name in `regions`, or `undefined` for one the labels do not know.\n * @internal Not part of the barrel. */\nexport function regionName(regions: NotificationRegionLabels, region: string): string | undefined {\n return Object.prototype.hasOwnProperty.call(regions, region)\n ? regions[region as keyof NotificationRegionLabels]\n : undefined;\n}\n\n/** A zone as the picker shows it, for a sentence: \"Europe / Zurich\", the region in the\n * kit's words. @internal Not part of the barrel. */\nexport function zoneName(zone: string, regions: NotificationRegionLabels): string {\n const slash = zone.indexOf(\"/\");\n if (slash < 0) return zone;\n const region = zone.slice(0, slash);\n return `${regionName(regions, region) ?? region} / ${cityOf(zone.slice(slash + 1))}`;\n}\n\nexport interface TimeZoneSelectProps {\n /** The IANA zone. Not clearable: a zone can be changed, never emptied. */\n value: string;\n /** The zone picked. Never called for the value it already has. */\n onChange: (zone: string) => void;\n /** Default `labels.timeZone`. */\n label?: ReactNode;\n disabled?: boolean;\n /** The input's id. */\n id?: string;\n className?: string;\n /** The picker SAVES on change (quiet hours do): locked under a write lock in scope. */\n commit?: CommitScope;\n labels?: LabelOverride<NotificationsLabels>;\n}\n\n/**\n * An IANA time zone, searchable (docs/notifications-harmonization.md §4.2, keksdose's\n * quiet-hours picker): type \"Zur\" for Europe/Zurich, and on a phone the kit's sheet with\n * a search box instead of a 400-row wheel. Rows are the city under its region, the region\n * in the kit's words (`labels.regions`); a zone with no region first; a stored zone the\n * runtime does not list is kept. On the kit's {@link InlineEntityCombobox}.\n */\nexport function TimeZoneSelect({\n value,\n onChange,\n label,\n disabled,\n id,\n className,\n commit,\n labels: labelsProp,\n}: TimeZoneSelectProps) {\n const labels = useNotificationsLabels(labelsProp);\n const { regions } = labels;\n const options = useMemo(() => timeZoneOptions(value, (region) => regionName(regions, region)), [value, regions]);\n return (\n <InlineEntityCombobox<string>\n id={id}\n label={label ?? labels.timeZone}\n value={value}\n options={options}\n disabled={disabled}\n commit={commit}\n onChange={(zone) => {\n if (zone && zone !== value) onChange(zone);\n }}\n className={className}\n />\n );\n}\n"],"mappings":";AAgHI;AAhHJ,SAAS,eAAe;AAKxB,SAAS,4BAA4B;AAErC,SAAS,8BAA8B;AAQhC,SAAS,iBAAyB;AACvC,MAAI;AACF,WAAO,KAAK,eAAe,EAAE,gBAAgB,EAAE,YAAY;AAAA,EAC7D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,eAAyB;AAChC,QAAM,OAAO;AACb,SAAO,OAAO,KAAK,sBAAsB,aAAa,KAAK,kBAAkB,UAAU,IAAI,CAAC;AAC9F;AAGA,SAAS,OAAO,MAAsB;AACpC,SAAO,KAAK,QAAQ,MAAM,GAAG,EAAE,QAAQ,OAAO,KAAK;AACrD;AAWO,SAAS,gBACd,SACA,aACuB;AACvB,QAAM,QAAQ,aAAa;AAC3B,QAAM,MAAM,CAAC,WAAW,MAAM,SAAS,OAAO,IAAI,QAAQ,CAAC,GAAG,OAAO,OAAO,EAAE,KAAK;AACnF,QAAM,OAAO,IAAI,IAAI,CAAC,SAA8B;AAClD,UAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,QAAI,QAAQ,EAAG,QAAO,EAAE,OAAO,MAAM,OAAO,KAAK;AACjD,UAAM,SAAS,KAAK,MAAM,GAAG,KAAK;AAClC,WAAO,EAAE,OAAO,MAAM,OAAO,OAAO,KAAK,MAAM,QAAQ,CAAC,CAAC,GAAG,OAAO,YAAY,MAAM,KAAK,OAAO;AAAA,EACnG,CAAC;AACD,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,GAAG,GAAG,KAAK,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAC;AACjF;AAIO,SAAS,WAAW,SAAmC,QAAoC;AAChG,SAAO,OAAO,UAAU,eAAe,KAAK,SAAS,MAAM,IACvD,QAAQ,MAAwC,IAChD;AACN;AAIO,SAAS,SAAS,MAAc,SAA2C;AAChF,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,SAAS,KAAK,MAAM,GAAG,KAAK;AAClC,SAAO,GAAG,WAAW,SAAS,MAAM,KAAK,MAAM,MAAM,OAAO,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AACpF;AAyBO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAAwB;AACtB,QAAM,SAAS,uBAAuB,UAAU;AAChD,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,UAAU,QAAQ,MAAM,gBAAgB,OAAO,CAAC,WAAW,WAAW,SAAS,MAAM,CAAC,GAAG,CAAC,OAAO,OAAO,CAAC;AAC/G,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,SAAS,OAAO;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,SAAS;AAClB,YAAI,QAAQ,SAAS,MAAO,UAAS,IAAI;AAAA,MAC3C;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
import type { LabelOverride } from "../i18n/kit-labels.js";
|
|
3
|
+
import type { AuthHeadingLevel } from "../auth/status-parts.js";
|
|
4
|
+
import type { NotificationsLabels } from "./notifications-labels.js";
|
|
5
|
+
/**
|
|
6
|
+
* The notification type an unsubscribe token names, read from its `<data>` half —
|
|
7
|
+
* `v1.<base64url("<user_id>:<type id>")>.<mac>` (docs/notifications-harmonization.md
|
|
8
|
+
* §3.6) — or `null` for a token that does not parse. Only the claim is read: the MAC is
|
|
9
|
+
* the server's to check, so a forged token still names a type here and the server
|
|
10
|
+
* refuses it. For the page's words ("You get e-mails about: Overdue invoices").
|
|
11
|
+
*/
|
|
12
|
+
export declare function unsubscribeTokenType(token: string): string | null;
|
|
13
|
+
export interface UnsubscribePanelProps extends Omit<ComponentPropsWithRef<"div">, "children" | "title"> {
|
|
14
|
+
/** The `token` from the page's query (`/unsubscribe?token=…`). */
|
|
15
|
+
token: string;
|
|
16
|
+
/** The app's name, for "Stop all notification e-mails from {app}". */
|
|
17
|
+
appName: string;
|
|
18
|
+
/** The type's name in the app's words, from {@link unsubscribeTokenType}. Without it
|
|
19
|
+
* the page says only that notification mail comes. */
|
|
20
|
+
typeLabel?: string;
|
|
21
|
+
/** `POST /notifications/unsubscribe/{token}`: that type's e-mail off. */
|
|
22
|
+
onStop: (token: string) => Promise<unknown>;
|
|
23
|
+
/** `POST /notifications/unsubscribe/{token}/all`: every e-mail switch off. */
|
|
24
|
+
onStopAll: (token: string) => Promise<unknown>;
|
|
25
|
+
/** Settings → Notifications, through the provider's link component. */
|
|
26
|
+
settingsHref: string;
|
|
27
|
+
/** Whether a rejection means the link is no good. Default: a 400 with
|
|
28
|
+
* `code: "unsubscribe_invalid"`. Anything else is "try again". */
|
|
29
|
+
isInvalid?: (error: unknown) => boolean;
|
|
30
|
+
/** The heading. Default `labels.unsubscribeTitle`; `null` draws none, for a page that
|
|
31
|
+
* puts it in `AuthLayout`'s `title`. */
|
|
32
|
+
title?: ReactNode;
|
|
33
|
+
/** The heading's level. Default `h2`, under the page's `h1`. */
|
|
34
|
+
headingAs?: AuthHeadingLevel;
|
|
35
|
+
labels?: LabelOverride<NotificationsLabels>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The body of an app's signed-out `/unsubscribe` page (docs/notifications-harmonization.md
|
|
39
|
+
* §3.6, §4.2) — the page a notification mail's footer links to. It works signed in or
|
|
40
|
+
* out (auth §7), and is not a card.
|
|
41
|
+
*
|
|
42
|
+
* **Nothing happens on a GET**: link scanners fetch every URL, so the page asks first —
|
|
43
|
+
* "Stop these e-mails", or "Stop all notification e-mails from {app}" — and only a press
|
|
44
|
+
* POSTs. A mail client's one-click POST goes to the server's route directly, without
|
|
45
|
+
* this page. Account mail is not a notification and keeps coming; the page says so, and
|
|
46
|
+
* then how to turn the mail back on (Settings → Notifications).
|
|
47
|
+
*
|
|
48
|
+
* A token that does not parse is "this link doesn't work" at once, with no request; the
|
|
49
|
+
* server's refusal (`unsubscribe_invalid`) says the same. Any other failure is "try
|
|
50
|
+
* again", the buttons still there. A valid token always answers 200 — also when the
|
|
51
|
+
* switch was already off or the account is gone — so the done state reveals nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function UnsubscribePanel({ token, appName, typeLabel, onStop, onStopAll, settingsHref, isInvalid, title, headingAs: Heading, labels: labelsProp, className, ...rest }: UnsubscribePanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { CheckCircle2, Link2Off } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { errorBodyWithCode, httpStatusOf } from "../auth/auth-errors.js";
|
|
7
|
+
import { INTRO_CLASS, OUTCOME_CLASS, STATUS_HEADING_CLASS, useFocusWhen } from "../auth/status-parts.js";
|
|
8
|
+
import { Button } from "../components/button.js";
|
|
9
|
+
import { EmptyState } from "../components/empty-state.js";
|
|
10
|
+
import { useMounted } from "../account/account-parts.js";
|
|
11
|
+
import { FailureLine, useNotificationsLabels } from "./notification-parts.js";
|
|
12
|
+
const TYPE_ID = /^[a-z][a-z0-9_]{1,39}$/;
|
|
13
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
14
|
+
function unsubscribeTokenType(token) {
|
|
15
|
+
const parts = token.split(".");
|
|
16
|
+
if (parts.length !== 3) return null;
|
|
17
|
+
const [version, data, mac] = parts;
|
|
18
|
+
if (version !== "v1" || !BASE64URL.test(data) || !BASE64URL.test(mac) || data.length % 4 === 1) return null;
|
|
19
|
+
let claim;
|
|
20
|
+
try {
|
|
21
|
+
claim = atob(data.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - data.length % 4) % 4));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const colon = claim.indexOf(":");
|
|
26
|
+
if (colon < 1) return null;
|
|
27
|
+
const type = claim.slice(colon + 1);
|
|
28
|
+
return TYPE_ID.test(type) ? type : null;
|
|
29
|
+
}
|
|
30
|
+
function defaultIsInvalid(error) {
|
|
31
|
+
const coded = errorBodyWithCode(error, (code) => code === "unsubscribe_invalid") !== void 0;
|
|
32
|
+
const status = httpStatusOf(error);
|
|
33
|
+
return coded && (status === void 0 || status === 400);
|
|
34
|
+
}
|
|
35
|
+
function UnsubscribePanel({
|
|
36
|
+
token,
|
|
37
|
+
appName,
|
|
38
|
+
typeLabel,
|
|
39
|
+
onStop,
|
|
40
|
+
onStopAll,
|
|
41
|
+
settingsHref,
|
|
42
|
+
isInvalid = defaultIsInvalid,
|
|
43
|
+
title,
|
|
44
|
+
headingAs: Heading = "h2",
|
|
45
|
+
labels: labelsProp,
|
|
46
|
+
className,
|
|
47
|
+
...rest
|
|
48
|
+
}) {
|
|
49
|
+
const labels = useNotificationsLabels(labelsProp);
|
|
50
|
+
const parses = unsubscribeTokenType(token) !== null;
|
|
51
|
+
const [phase, setPhase] = useState("ready");
|
|
52
|
+
const mounted = useMounted();
|
|
53
|
+
const outcomeRef = useRef(null);
|
|
54
|
+
const shown = parses ? phase : "invalid";
|
|
55
|
+
const done = shown === "stopped-these" || shown === "stopped-all";
|
|
56
|
+
useFocusWhen(done, outcomeRef);
|
|
57
|
+
const stop = (which) => {
|
|
58
|
+
if (shown !== "ready" && shown !== "failed") return;
|
|
59
|
+
setPhase(which);
|
|
60
|
+
const call = which === "these" ? onStop : onStopAll;
|
|
61
|
+
call(token).then(
|
|
62
|
+
() => {
|
|
63
|
+
if (mounted.current) setPhase(which === "these" ? "stopped-these" : "stopped-all");
|
|
64
|
+
},
|
|
65
|
+
(error) => {
|
|
66
|
+
if (mounted.current) setPhase(isInvalid(error) ? "invalid" : "failed");
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
const settingsLink = /* @__PURE__ */ jsx(Button, { href: settingsHref, variant: "secondary", children: labels.openSettings });
|
|
71
|
+
let body;
|
|
72
|
+
if (shown === "invalid") {
|
|
73
|
+
body = /* @__PURE__ */ jsx(
|
|
74
|
+
EmptyState,
|
|
75
|
+
{
|
|
76
|
+
tone: "danger",
|
|
77
|
+
icon: /* @__PURE__ */ jsx(Link2Off, {}),
|
|
78
|
+
title: labels.invalidLink,
|
|
79
|
+
action: settingsLink,
|
|
80
|
+
className: OUTCOME_CLASS
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
} else if (done) {
|
|
84
|
+
body = /* @__PURE__ */ jsx("div", { ref: outcomeRef, tabIndex: -1, className: "outline-none", children: /* @__PURE__ */ jsx(
|
|
85
|
+
EmptyState,
|
|
86
|
+
{
|
|
87
|
+
tone: "success",
|
|
88
|
+
icon: /* @__PURE__ */ jsx(CheckCircle2, {}),
|
|
89
|
+
title: shown === "stopped-these" ? labels.stoppedThese : labels.stoppedAll(appName),
|
|
90
|
+
hint: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
91
|
+
labels.accountMail,
|
|
92
|
+
" ",
|
|
93
|
+
labels.turnBackOn
|
|
94
|
+
] }),
|
|
95
|
+
action: settingsLink,
|
|
96
|
+
className: OUTCOME_CLASS
|
|
97
|
+
}
|
|
98
|
+
) });
|
|
99
|
+
} else {
|
|
100
|
+
const working = shown === "these" || shown === "all";
|
|
101
|
+
body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
102
|
+
/* @__PURE__ */ jsx("p", { className: INTRO_CLASS, children: typeLabel ? labels.unsubscribeAbout(typeLabel) : labels.unsubscribeGeneric }),
|
|
103
|
+
/* @__PURE__ */ jsx("p", { className: INTRO_CLASS, children: labels.accountMail }),
|
|
104
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:flex-wrap", children: [
|
|
105
|
+
/* @__PURE__ */ jsx(
|
|
106
|
+
Button,
|
|
107
|
+
{
|
|
108
|
+
type: "button",
|
|
109
|
+
pending: shown === "these",
|
|
110
|
+
disabled: working && shown !== "these",
|
|
111
|
+
onClick: () => stop("these"),
|
|
112
|
+
children: labels.stopThese
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
/* @__PURE__ */ jsx(
|
|
116
|
+
Button,
|
|
117
|
+
{
|
|
118
|
+
type: "button",
|
|
119
|
+
variant: "secondary",
|
|
120
|
+
pending: shown === "all",
|
|
121
|
+
disabled: working && shown !== "all",
|
|
122
|
+
onClick: () => stop("all"),
|
|
123
|
+
children: labels.stopAll(appName)
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
] }),
|
|
127
|
+
shown === "failed" && /* @__PURE__ */ jsx(FailureLine, { children: labels.failed })
|
|
128
|
+
] });
|
|
129
|
+
}
|
|
130
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, "data-state": shown, className: cn("space-y-3", className), children: [
|
|
131
|
+
title === null ? null : /* @__PURE__ */ jsx(Heading, { className: STATUS_HEADING_CLASS, children: title ?? labels.unsubscribeTitle }),
|
|
132
|
+
body
|
|
133
|
+
] });
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
UnsubscribePanel,
|
|
137
|
+
unsubscribeTokenType
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=unsubscribe-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/notifications/unsubscribe-panel.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CheckCircle2, Link2Off } from \"lucide-react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { errorBodyWithCode, httpStatusOf } from \"../auth/auth-errors\";\nimport { INTRO_CLASS, OUTCOME_CLASS, STATUS_HEADING_CLASS, useFocusWhen } from \"../auth/status-parts\";\nimport type { AuthHeadingLevel } from \"../auth/status-parts\";\nimport { Button } from \"../components/button\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { useMounted } from \"../account/account-parts\";\nimport { FailureLine, useNotificationsLabels } from \"./notification-parts\";\nimport type { NotificationsLabels } from \"./notifications-labels\";\n\n/** A type id, as the server's registry allows it (§2.1). */\nconst TYPE_ID = /^[a-z][a-z0-9_]{1,39}$/;\n/** base64url, unpadded, as the token's `<data>` is written (§3.6). */\nconst BASE64URL = /^[A-Za-z0-9_-]+$/;\n\n/**\n * The notification type an unsubscribe token names, read from its `<data>` half —\n * `v1.<base64url(\"<user_id>:<type id>\")>.<mac>` (docs/notifications-harmonization.md\n * §3.6) — or `null` for a token that does not parse. Only the claim is read: the MAC is\n * the server's to check, so a forged token still names a type here and the server\n * refuses it. For the page's words (\"You get e-mails about: Overdue invoices\").\n */\nexport function unsubscribeTokenType(token: string): string | null {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n const [version, data, mac] = parts;\n if (version !== \"v1\" || !BASE64URL.test(data) || !BASE64URL.test(mac) || data.length % 4 === 1) return null;\n let claim: string;\n try {\n claim = atob(data.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"=\".repeat((4 - (data.length % 4)) % 4));\n } catch {\n return null;\n }\n const colon = claim.indexOf(\":\");\n if (colon < 1) return null;\n const type = claim.slice(colon + 1);\n return TYPE_ID.test(type) ? type : null;\n}\n\n/** server-kit's `UnsubscribeTokenError`: 400 `{code: \"unsubscribe_invalid\"}`. A body\n * thrown as it came, with no status, counts on its code. */\nfunction defaultIsInvalid(error: unknown): boolean {\n const coded = errorBodyWithCode(error, (code) => code === \"unsubscribe_invalid\") !== undefined;\n const status = httpStatusOf(error);\n return coded && (status === undefined || status === 400);\n}\n\nexport interface UnsubscribePanelProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The `token` from the page's query (`/unsubscribe?token=…`). */\n token: string;\n /** The app's name, for \"Stop all notification e-mails from {app}\". */\n appName: string;\n /** The type's name in the app's words, from {@link unsubscribeTokenType}. Without it\n * the page says only that notification mail comes. */\n typeLabel?: string;\n /** `POST /notifications/unsubscribe/{token}`: that type's e-mail off. */\n onStop: (token: string) => Promise<unknown>;\n /** `POST /notifications/unsubscribe/{token}/all`: every e-mail switch off. */\n onStopAll: (token: string) => Promise<unknown>;\n /** Settings → Notifications, through the provider's link component. */\n settingsHref: string;\n /** Whether a rejection means the link is no good. Default: a 400 with\n * `code: \"unsubscribe_invalid\"`. Anything else is \"try again\". */\n isInvalid?: (error: unknown) => boolean;\n /** The heading. Default `labels.unsubscribeTitle`; `null` draws none, for a page that\n * puts it in `AuthLayout`'s `title`. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<NotificationsLabels>;\n}\n\ntype Phase = \"ready\" | \"these\" | \"all\" | \"stopped-these\" | \"stopped-all\" | \"invalid\" | \"failed\";\n\n/**\n * The body of an app's signed-out `/unsubscribe` page (docs/notifications-harmonization.md\n * §3.6, §4.2) — the page a notification mail's footer links to. It works signed in or\n * out (auth §7), and is not a card.\n *\n * **Nothing happens on a GET**: link scanners fetch every URL, so the page asks first —\n * \"Stop these e-mails\", or \"Stop all notification e-mails from {app}\" — and only a press\n * POSTs. A mail client's one-click POST goes to the server's route directly, without\n * this page. Account mail is not a notification and keeps coming; the page says so, and\n * then how to turn the mail back on (Settings → Notifications).\n *\n * A token that does not parse is \"this link doesn't work\" at once, with no request; the\n * server's refusal (`unsubscribe_invalid`) says the same. Any other failure is \"try\n * again\", the buttons still there. A valid token always answers 200 — also when the\n * switch was already off or the account is gone — so the done state reveals nothing.\n */\nexport function UnsubscribePanel({\n token,\n appName,\n typeLabel,\n onStop,\n onStopAll,\n settingsHref,\n isInvalid = defaultIsInvalid,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: UnsubscribePanelProps) {\n const labels = useNotificationsLabels(labelsProp);\n const parses = unsubscribeTokenType(token) !== null;\n const [phase, setPhase] = useState<Phase>(\"ready\");\n const mounted = useMounted();\n const outcomeRef = useRef<HTMLDivElement>(null);\n const shown: Phase = parses ? phase : \"invalid\";\n const done = shown === \"stopped-these\" || shown === \"stopped-all\";\n useFocusWhen(done, outcomeRef);\n\n const stop = (which: \"these\" | \"all\") => {\n if (shown !== \"ready\" && shown !== \"failed\") return;\n setPhase(which);\n const call = which === \"these\" ? onStop : onStopAll;\n call(token).then(\n () => {\n if (mounted.current) setPhase(which === \"these\" ? \"stopped-these\" : \"stopped-all\");\n },\n (error: unknown) => {\n if (mounted.current) setPhase(isInvalid(error) ? \"invalid\" : \"failed\");\n },\n );\n };\n\n const settingsLink = (\n <Button href={settingsHref} variant=\"secondary\">\n {labels.openSettings}\n </Button>\n );\n\n let body: ReactNode;\n if (shown === \"invalid\") {\n body = (\n <EmptyState\n tone=\"danger\"\n icon={<Link2Off />}\n title={labels.invalidLink}\n action={settingsLink}\n className={OUTCOME_CLASS}\n />\n );\n } else if (done) {\n body = (\n <div ref={outcomeRef} tabIndex={-1} className=\"outline-none\">\n <EmptyState\n tone=\"success\"\n icon={<CheckCircle2 />}\n title={shown === \"stopped-these\" ? labels.stoppedThese : labels.stoppedAll(appName)}\n hint={\n <>\n {labels.accountMail} {labels.turnBackOn}\n </>\n }\n action={settingsLink}\n className={OUTCOME_CLASS}\n />\n </div>\n );\n } else {\n const working = shown === \"these\" || shown === \"all\";\n body = (\n <>\n <p className={INTRO_CLASS}>{typeLabel ? labels.unsubscribeAbout(typeLabel) : labels.unsubscribeGeneric}</p>\n <p className={INTRO_CLASS}>{labels.accountMail}</p>\n <div className=\"flex flex-col gap-2 sm:flex-row sm:flex-wrap\">\n <Button\n type=\"button\"\n pending={shown === \"these\"}\n disabled={working && shown !== \"these\"}\n onClick={() => stop(\"these\")}\n >\n {labels.stopThese}\n </Button>\n <Button\n type=\"button\"\n variant=\"secondary\"\n pending={shown === \"all\"}\n disabled={working && shown !== \"all\"}\n onClick={() => stop(\"all\")}\n >\n {labels.stopAll(appName)}\n </Button>\n </div>\n {shown === \"failed\" && <FailureLine>{labels.failed}</FailureLine>}\n </>\n );\n }\n\n return (\n <div {...rest} data-state={shown} className={cn(\"space-y-3\", className)}>\n {title === null ? null : <Heading className={STATUS_HEADING_CLASS}>{title ?? labels.unsubscribeTitle}</Heading>}\n {body}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|