@eifi1/ui-kit 0.51.6 → 0.52.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 +146 -26
- package/dist/account/account-parts.d.ts +10 -8
- package/dist/account/account-parts.js +1 -18
- package/dist/account/account-parts.js.map +1 -1
- package/dist/account/data-export-setting-labels.js +1 -1
- package/dist/account/data-export-setting-labels.js.map +1 -1
- package/dist/account/delete-account-setting-labels.js +1 -1
- package/dist/account/delete-account-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting-labels.js +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/passkeys-setting.d.ts +9 -2
- package/dist/account/passkeys-setting.js +41 -29
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +3 -2
- package/dist/account/sessions-setting.js +10 -8
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +3 -7
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-parts.d.ts +6 -3
- package/dist/admin/admin-parts.js +6 -21
- package/dist/admin/admin-parts.js.map +1 -1
- package/dist/admin/mail-test-card-labels.js +1 -1
- package/dist/admin/mail-test-card-labels.js.map +1 -1
- package/dist/admin/plan-change-labels.d.ts +2 -2
- package/dist/admin/plan-change-labels.js +1 -17
- package/dist/admin/plan-change-labels.js.map +1 -1
- package/dist/admin/plan-change.d.ts +3 -42
- package/dist/admin/plan-change.js +1 -1
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +3 -3
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/auth/auth-errors-labels.d.ts +11 -0
- package/dist/auth/auth-errors-labels.js +13 -0
- package/dist/auth/auth-errors-labels.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +7 -23
- package/dist/auth/auth-errors.js +2 -15
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.d.ts +3 -2
- package/dist/auth/complete-name-dialog.js +3 -5
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/forgot-password-form-labels.js +1 -1
- package/dist/auth/forgot-password-form-labels.js.map +1 -1
- package/dist/auth/forgot-password-form.js +7 -2
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form-labels.js +1 -1
- package/dist/auth/register-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form-labels.js +1 -1
- package/dist/auth/reset-password-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form.js +5 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form-labels.js +1 -1
- package/dist/auth/sign-in-form-labels.js.map +1 -1
- package/dist/auth/status-parts.d.ts +0 -3
- package/dist/auth/status-parts.js +0 -4
- package/dist/auth/status-parts.js.map +1 -1
- package/dist/auth/verify-email-labels.js +1 -1
- package/dist/auth/verify-email-labels.js.map +1 -1
- package/dist/auth/verify-email.js +7 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/cancel-request.d.ts +11 -2
- package/dist/billing/cancel-request.js +15 -3
- package/dist/billing/cancel-request.js.map +1 -1
- package/dist/billing/cancel-subscription.d.ts +22 -10
- package/dist/billing/cancel-subscription.js +8 -11
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/dialog-close.d.ts +29 -0
- package/dist/billing/dialog-close.js +8 -0
- package/dist/billing/dialog-close.js.map +1 -0
- package/dist/billing/plan-card.d.ts +14 -4
- package/dist/billing/plan-card.js +15 -8
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-change-labels.d.ts +42 -0
- package/dist/billing/plan-change-labels.js +22 -0
- package/dist/billing/plan-change-labels.js.map +1 -0
- package/dist/billing/plan-switch.d.ts +21 -9
- package/dist/billing/plan-switch.js +10 -8
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +12 -0
- package/dist/chart.js +16 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +5 -5
- package/dist/components/account-chips.js +11 -13
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +16 -5
- package/dist/components/alert-banner.js +46 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +22 -4
- package/dist/components/amount-input.js +3 -3
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +57 -65
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +12 -2
- package/dist/components/bulk-action-bar.js +65 -21
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-look.d.ts +5 -5
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.d.ts +21 -2
- package/dist/components/calculator.js +53 -14
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-edge.d.ts +16 -0
- package/dist/components/chart-edge.js +14 -1
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-figure.d.ts +13 -0
- package/dist/components/chart-figure.js +15 -0
- package/dist/components/chart-figure.js.map +1 -0
- package/dist/components/chart-zoom-core.js +1 -4
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +8 -1
- package/dist/components/chart.js +19 -10
- package/dist/components/chart.js.map +1 -1
- package/dist/components/combobox-core.d.ts +119 -7
- package/dist/components/combobox-core.js +151 -46
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +142 -131
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +6 -5
- package/dist/components/copy-button.js +5 -3
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.js +2 -2
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +1 -1
- package/dist/components/data-table-column-settings.js +33 -7
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +4 -2
- package/dist/components/data-table-filters.js +2 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +6 -4
- package/dist/components/data-table-fit.js +4 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +1 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +1 -1
- package/dist/components/data-table-head.js +15 -12
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +36 -7
- package/dist/components/data-table-mobile.js +63 -31
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js +11 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-resize.d.ts +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +6 -2
- package/dist/components/data-table-row.js +17 -10
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-selection.d.ts +1 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +10 -2
- package/dist/components/data-table-shared.js +18 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +21 -2
- package/dist/components/data-table-sort.js +24 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +1 -1
- package/dist/components/data-table-virtual.js +18 -6
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +25 -8
- package/dist/components/data-table.js +154 -25
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +24 -2
- package/dist/components/date-field.js +33 -8
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +15 -2
- package/dist/components/date-picker.js +55 -26
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.d.ts +47 -20
- package/dist/components/date-range-picker.js +23 -7
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +2 -1
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/entity-combobox.js +4 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js +37 -30
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
- package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
- package/dist/components/field-base-labels.js.map +1 -0
- package/dist/components/field-base.d.ts +4 -3
- package/dist/components/field-base.js +3 -3
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-sync-labels.d.ts +1 -0
- package/dist/components/field-sync-labels.js.map +1 -1
- package/dist/components/field-sync.d.ts +23 -1
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/form-actions.js +11 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +5 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +14 -9
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.d.ts +43 -0
- package/dist/components/inline-confirm.js +67 -0
- package/dist/components/inline-confirm.js.map +1 -0
- package/dist/components/inline-entity-combobox.d.ts +4 -3
- package/dist/components/inline-entity-combobox.js +150 -141
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +28 -112
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-page.d.ts +3 -1
- package/dist/components/legal-page.js +3 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +4 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.d.ts +2 -2
- package/dist/components/lightbox.js +17 -6
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/list.d.ts +6 -2
- package/dist/components/list.js +4 -3
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +1 -1
- package/dist/components/measured-grid.js +2 -1
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/mini-calendar.js +3 -5
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +24 -0
- package/dist/components/modal.js +12 -4
- package/dist/components/modal.js.map +1 -1
- package/dist/components/money-draft.js +7 -5
- package/dist/components/money-draft.js.map +1 -1
- package/dist/components/money-field.d.ts +2 -2
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-grid.d.ts +55 -0
- package/dist/components/month-grid.js +148 -0
- package/dist/components/month-grid.js.map +1 -0
- package/dist/components/month-picker.d.ts +36 -4
- package/dist/components/month-picker.js +123 -146
- package/dist/components/month-picker.js.map +1 -1
- 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 +2 -2
- package/dist/components/multi-select.js +11 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +6 -4
- package/dist/components/number-field.js +3 -2
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +4 -1
- package/dist/components/number-input.js +3 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +7 -0
- package/dist/components/pie-chart-labels.js +1 -0
- package/dist/components/pie-chart-labels.js.map +1 -1
- package/dist/components/pie-chart.d.ts +5 -3
- package/dist/components/pie-chart.js +7 -22
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +3 -0
- package/dist/components/popover.js +7 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/reauth-dialog.d.ts +4 -5
- package/dist/components/reauth-dialog.js +1 -4
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/row-actions.js +2 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +7 -0
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +10 -3
- package/dist/components/series-chart-axes.js +1 -0
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +7 -4
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +31 -7
- package/dist/components/series-chart.js +72 -29
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +3 -3
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip-labels.d.ts +1 -0
- package/dist/components/sign-chip-labels.js.map +1 -1
- package/dist/components/sign-chip.d.ts +10 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/tabs-labels.d.ts +2 -0
- package/dist/components/tabs-labels.js +11 -0
- package/dist/components/tabs-labels.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/tabs.js +13 -11
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +2 -2
- package/dist/components/toggle-legend.js +6 -6
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -2
- package/dist/components/translation-review-editor.js +2 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.d.ts +2 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap-labels.d.ts +26 -0
- package/dist/components/treemap-labels.js +10 -0
- package/dist/components/treemap-labels.js.map +1 -0
- package/dist/components/treemap-layout.d.ts +62 -0
- package/dist/components/treemap-layout.js +126 -0
- package/dist/components/treemap-layout.js.map +1 -0
- package/dist/components/treemap.d.ts +140 -17
- package/dist/components/treemap.js +324 -97
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.d.ts +5 -5
- package/dist/components/use-table-state.js +4 -1
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +6 -2
- package/dist/data-table.js +7 -2
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +36 -7
- package/dist/eslint/role-utilities.js +42 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +25 -16
- package/dist/feedback/feedback-dialog.js +6 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-mutation.d.ts +30 -0
- package/dist/feedback/feedback-mutation.js +16 -0
- package/dist/feedback/feedback-mutation.js.map +1 -0
- package/dist/feedback/feedback-page-link.d.ts +53 -0
- package/dist/feedback/feedback-page-link.js +37 -0
- package/dist/feedback/feedback-page-link.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +12 -3
- package/dist/feedback/feedback-public-list.js +2 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +3 -1
- package/dist/feedback/feedback-replies.js +5 -4
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +5 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +64 -28
- package/dist/feedback/feedback-row-detail.js +30 -28
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +30 -11
- package/dist/feedback/feedback-status-undo.js +10 -15
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
- package/dist/feedback/feedback-submit-dialog.js +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.js +9 -3
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +1 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +21 -9
- package/dist/feedback/feedback-table.js +45 -25
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-labels.d.ts +2 -2
- package/dist/feedback/feedback-thread-labels.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +24 -23
- package/dist/feedback/feedback-thread.js +2 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +34 -0
- package/dist/feedback.js +42 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +35 -0
- package/dist/hooks/use-focus-trap.js +6 -4
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-hotkey.d.ts +5 -2
- package/dist/hooks/use-hotkey.js +5 -4
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +7 -4
- package/dist/hooks/use-long-press.js +3 -0
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-media-query.js +12 -4
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-mounted.d.ts +22 -0
- package/dist/hooks/use-mounted.js +24 -0
- package/dist/hooks/use-mounted.js.map +1 -0
- package/dist/hooks/use-overlay-history.d.ts +7 -1
- package/dist/hooks/use-overlay-history.js +35 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +4 -5
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +7 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +20 -41
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -10
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +8 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +5 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +64 -17
- package/dist/index.js +77 -12
- package/dist/index.js.map +1 -1
- package/dist/landing/routing.d.ts +3 -1
- package/dist/landing/routing.js +4 -7
- package/dist/landing/routing.js.map +1 -1
- package/dist/lib/broadcast.d.ts +17 -0
- package/dist/lib/broadcast.js +30 -0
- package/dist/lib/broadcast.js.map +1 -0
- package/dist/lib/calc.d.ts +7 -1
- package/dist/lib/calc.js +3 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.d.ts +2 -2
- package/dist/lib/clipboard-minus.js +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/date-format.d.ts +37 -2
- package/dist/lib/date-format.js +34 -8
- package/dist/lib/date-format.js.map +1 -1
- package/dist/lib/day-format.d.ts +46 -0
- package/dist/lib/day-format.js +46 -0
- package/dist/lib/day-format.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +7 -1
- package/dist/lib/decimal-marks.js +5 -2
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/describe-failure.d.ts +14 -0
- package/dist/lib/describe-failure.js +9 -0
- package/dist/lib/describe-failure.js.map +1 -0
- package/dist/lib/direction.d.ts +16 -0
- package/dist/lib/direction.js +9 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/focus-left.d.ts +63 -0
- package/dist/lib/focus-left.js +67 -0
- package/dist/lib/focus-left.js.map +1 -0
- package/dist/lib/format.d.ts +5 -3
- package/dist/lib/format.js +16 -13
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/http-status.d.ts +24 -0
- package/dist/lib/http-status.js +14 -0
- package/dist/lib/http-status.js.map +1 -0
- package/dist/lib/intl-cache.d.ts +14 -0
- package/dist/lib/intl-cache.js +15 -0
- package/dist/lib/intl-cache.js.map +1 -0
- package/dist/lib/isin.d.ts +10 -0
- package/dist/lib/isin.js.map +1 -1
- package/dist/lib/iso-day.d.ts +11 -0
- package/dist/lib/iso-day.js +8 -0
- package/dist/lib/iso-day.js.map +1 -0
- package/dist/lib/lazy-with-fallback.d.ts +31 -0
- package/dist/lib/lazy-with-fallback.js +40 -0
- package/dist/lib/lazy-with-fallback.js.map +1 -0
- package/dist/lib/path-pattern.d.ts +34 -0
- package/dist/lib/path-pattern.js +24 -0
- package/dist/lib/path-pattern.js.map +1 -0
- package/dist/lib/reduced-motion.d.ts +2 -1
- package/dist/lib/reduced-motion.js.map +1 -1
- package/dist/lib/round.d.ts +2 -1
- package/dist/lib/round.js +2 -1
- package/dist/lib/round.js.map +1 -1
- package/dist/lib/safe-storage.d.ts +24 -5
- package/dist/lib/safe-storage.js +27 -2
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/lib/session-refresh.js +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/share-format.d.ts +13 -0
- package/dist/lib/share-format.js +10 -0
- package/dist/lib/share-format.js.map +1 -0
- package/dist/lib/text-case.d.ts +17 -0
- package/dist/lib/text-case.js +8 -0
- package/dist/lib/text-case.js.map +1 -0
- package/dist/lib/text-match.d.ts +38 -0
- package/dist/lib/text-match.js +24 -2
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/thenable.d.ts +13 -0
- package/dist/lib/thenable.js +8 -0
- package/dist/lib/thenable.js.map +1 -0
- package/dist/lib/translation-review.d.ts +7 -2
- package/dist/lib/translation-review.js +4 -4
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/device-zone-report.js +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +2 -1
- package/dist/notifications/notification-bell.js +3 -3
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +10 -6
- package/dist/notifications/notification-inbox.js +26 -48
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +3 -2
- package/dist/notifications/notification-parts.js +1 -3
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.d.ts +6 -0
- package/dist/notifications/notifications-labels.js +1 -0
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +1 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +2 -1
- package/dist/notifications/push-devices-setting.js +27 -49
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +10 -3
- package/dist/notifications/quiet-hours-setting.js +65 -26
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/unsubscribe-panel.js +4 -7
- package/dist/notifications/unsubscribe-panel.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +39 -1
- package/dist/notifications/use-push-device.js +28 -7
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/query-persistence.d.ts +3 -1
- package/dist/offline/query-persistence.js +19 -2
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/rhf/fields.d.ts +11 -4
- package/dist/rhf/fields.js.map +1 -1
- package/dist/search/command-palette.js +1 -1
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +19 -9
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/use-command-key.d.ts +12 -1
- package/dist/search/use-command-key.js +3 -12
- package/dist/search/use-command-key.js.map +1 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.js +10 -0
- package/dist/search.js.map +1 -1
- package/dist/settings/use-settings-focus.js +1 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +21 -5
- package/dist/shell/account-menu-language.js +3 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +26 -9
- package/dist/shell/account-menu-text-size.js +3 -2
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/dev-menu.js +5 -18
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +7 -0
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +1 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +0 -2
- package/dist/shell/top-bar-parts-labels.js +0 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +27 -8
- package/dist/shell.js +28 -7
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +30 -4
- package/dist/sync/engine.js +172 -52
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +6 -2
- package/dist/sync.js +6 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js +3 -8
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -1
- package/dist/theme/contrast.js +3 -8
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-store.js +6 -6
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +3 -8
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +3 -6
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.d.ts +7 -4
- package/dist/tour/tour-catalogue.js +12 -10
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +0 -2
- package/dist/tour/tour-dock.js +0 -2
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-gates.d.ts +4 -0
- package/dist/tour/tour-gates.js +14 -0
- package/dist/tour/tour-gates.js.map +1 -1
- package/dist/tour/tour-overlay.d.ts +27 -0
- package/dist/tour/tour-overlay.js +741 -0
- package/dist/tour/tour-overlay.js.map +1 -0
- package/dist/tour/tour.d.ts +16 -4
- package/dist/tour/tour.js +62 -778
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js +2 -2
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +6 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.d.ts +3 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.d.ts +3 -1
- package/dist/wizard/validation.js.map +1 -1
- package/package.json +12 -8
- package/src/account/account-parts.ts +5 -25
- package/src/account/data-export-setting-labels.ts +1 -1
- package/src/account/delete-account-setting-labels.ts +1 -1
- package/src/account/email-change-setting-labels.ts +1 -1
- package/src/account/passkeys-setting.tsx +60 -38
- package/src/account/sessions-setting.tsx +18 -12
- package/src/admin/admin-action-confirm.tsx +5 -10
- package/src/admin/admin-parts.ts +11 -25
- package/src/admin/mail-test-card-labels.ts +1 -1
- package/src/admin/plan-change-labels.ts +5 -23
- package/src/admin/plan-change.tsx +7 -44
- package/src/admin/reviewer-scope-editor.tsx +3 -3
- package/src/auth/auth-errors-labels.ts +23 -0
- package/src/auth/auth-errors.ts +11 -38
- package/src/auth/complete-name-dialog.tsx +9 -8
- package/src/auth/forgot-password-form-labels.ts +1 -1
- package/src/auth/forgot-password-form.tsx +11 -6
- package/src/auth/register-form-labels.ts +1 -1
- package/src/auth/reset-password-form-labels.ts +1 -1
- package/src/auth/reset-password-form.tsx +8 -4
- package/src/auth/sign-in-form-labels.ts +1 -1
- package/src/auth/status-parts.tsx +0 -6
- package/src/auth/verify-email-labels.ts +1 -1
- package/src/auth/verify-email.tsx +11 -6
- package/src/billing/cancel-request.ts +28 -6
- package/src/billing/cancel-subscription.tsx +29 -22
- package/src/billing/dialog-close.ts +34 -0
- package/src/billing/plan-card.tsx +36 -11
- package/src/billing/plan-change-labels.ts +66 -0
- package/src/billing/plan-switch.tsx +31 -20
- package/src/chart.ts +21 -3
- package/src/components/account-chips.tsx +20 -23
- package/src/components/alert-banner.tsx +82 -5
- package/src/components/amount-input.tsx +32 -14
- package/src/components/autocomplete.tsx +64 -70
- package/src/components/bulk-action-bar.tsx +70 -2
- package/src/components/button-look.ts +5 -5
- package/src/components/button.tsx +2 -2
- package/src/components/calculator.tsx +98 -17
- package/src/components/chart-edge.ts +34 -1
- package/src/components/chart-figure.tsx +35 -0
- package/src/components/chart-zoom-core.tsx +6 -5
- package/src/components/chart.tsx +26 -5
- package/src/components/combobox-core.tsx +303 -69
- package/src/components/combobox.tsx +184 -155
- package/src/components/copy-button.tsx +34 -23
- package/src/components/country-select.tsx +16 -11
- package/src/components/data-table-column-settings.tsx +52 -10
- package/src/components/data-table-filters.ts +6 -4
- package/src/components/data-table-fit.ts +19 -5
- package/src/components/data-table-groups.tsx +1 -1
- package/src/components/data-table-head.tsx +32 -22
- package/src/components/data-table-mobile.tsx +110 -27
- package/src/components/data-table-pagination.tsx +21 -6
- package/src/components/data-table-resize.ts +1 -1
- package/src/components/data-table-row-actions.tsx +3 -3
- package/src/components/data-table-row.tsx +25 -14
- package/src/components/data-table-selection.ts +1 -1
- package/src/components/data-table-shared.tsx +33 -1
- package/src/components/data-table-sort.ts +57 -2
- package/src/components/data-table-virtual.ts +32 -8
- package/src/components/data-table.tsx +267 -41
- package/src/components/date-field.tsx +65 -8
- package/src/components/date-picker.tsx +125 -48
- package/src/components/date-range-picker.tsx +82 -31
- package/src/components/dialog-frame.tsx +2 -1
- package/src/components/entity-combobox.tsx +4 -1
- package/src/components/error-boundary.tsx +53 -39
- package/src/components/field-base-labels.ts +20 -0
- package/src/components/field-base.tsx +13 -7
- package/src/components/field-sync-labels.ts +1 -0
- package/src/components/field-sync.tsx +23 -1
- package/src/components/form-actions.tsx +20 -5
- package/src/components/hover-menu.tsx +18 -8
- package/src/components/icon-button.tsx +14 -9
- package/src/components/inline-confirm.tsx +116 -0
- package/src/components/inline-entity-combobox.tsx +198 -152
- package/src/components/input.tsx +1 -1
- package/src/components/jump-calendar.tsx +30 -146
- package/src/components/legal-page.tsx +10 -7
- package/src/components/legal.tsx +4 -2
- package/src/components/lightbox.tsx +21 -8
- package/src/components/list.tsx +14 -5
- package/src/components/measured-grid.tsx +8 -4
- package/src/components/mini-calendar.tsx +6 -5
- package/src/components/modal.tsx +45 -4
- package/src/components/money-draft.ts +16 -5
- package/src/components/money-field.tsx +2 -2
- package/src/components/month-grid.tsx +229 -0
- package/src/components/month-picker.tsx +192 -200
- package/src/components/multi-entity-combobox.tsx +1 -0
- package/src/components/multi-select.tsx +21 -9
- package/src/components/number-field.tsx +14 -7
- package/src/components/number-input.tsx +9 -3
- package/src/components/numpad-sheet.tsx +4 -3
- package/src/components/picker-sheet.tsx +12 -9
- package/src/components/pie-chart-labels.ts +8 -0
- package/src/components/pie-chart.tsx +22 -28
- package/src/components/popover.tsx +18 -1
- package/src/components/reauth-dialog.tsx +6 -10
- package/src/components/row-actions.tsx +2 -1
- package/src/components/section-nav.tsx +7 -0
- package/src/components/series-chart-axes.ts +11 -3
- package/src/components/series-chart-labels.ts +7 -4
- package/src/components/series-chart.tsx +178 -69
- package/src/components/share-card.tsx +6 -6
- package/src/components/sign-chip-labels.ts +1 -0
- package/src/components/sign-chip.tsx +10 -1
- package/src/components/tabs-labels.ts +13 -0
- package/src/components/tabs.tsx +35 -19
- package/src/components/toggle-legend.tsx +16 -11
- package/src/components/translation-review-editor.tsx +6 -5
- package/src/components/translation-review.tsx +4 -3
- package/src/components/treemap-labels.ts +39 -0
- package/src/components/treemap-layout.ts +210 -0
- package/src/components/treemap.tsx +722 -133
- package/src/components/use-table-state.ts +12 -9
- package/src/data-table.ts +9 -2
- package/src/eslint/role-utilities.ts +97 -19
- package/src/feedback/feedback-dialog.tsx +30 -19
- package/src/feedback/feedback-mutation.ts +41 -0
- package/src/feedback/feedback-page-link.ts +90 -0
- package/src/feedback/feedback-public-list.tsx +17 -5
- package/src/feedback/feedback-replies.tsx +13 -6
- package/src/feedback/feedback-row-detail-labels.ts +5 -0
- package/src/feedback/feedback-row-detail.tsx +103 -67
- package/src/feedback/feedback-status-undo.ts +41 -27
- package/src/feedback/feedback-submit-dialog.tsx +2 -2
- package/src/feedback/feedback-submit.tsx +21 -7
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/feedback/feedback-table.tsx +86 -38
- package/src/feedback/feedback-thread-labels.ts +2 -2
- package/src/feedback/feedback-thread.tsx +27 -25
- package/src/feedback.ts +42 -0
- package/src/hooks/use-dismiss.ts +3 -2
- package/src/hooks/use-focus-trap.ts +8 -24
- package/src/hooks/use-hotkey.ts +24 -7
- package/src/hooks/use-long-press.ts +16 -2
- package/src/hooks/use-media-query.ts +20 -4
- package/src/hooks/use-mounted.ts +39 -0
- package/src/hooks/use-overlay-history.ts +70 -5
- package/src/i18n/defaults.ts +4 -5
- package/src/i18n/german.ts +8 -2
- package/src/i18n/kit-labels.tsx +9 -2
- package/src/i18n/locales/es.ts +8 -2
- package/src/i18n/locales/fr.ts +21 -13
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +8 -2
- package/src/i18n/locales/zh.ts +9 -2
- package/src/i18n/review.ts +5 -0
- package/src/index.ts +103 -13
- package/src/landing/routing.tsx +14 -10
- package/src/lib/broadcast.ts +52 -0
- package/src/lib/calc.ts +13 -2
- package/src/lib/clipboard-minus.ts +14 -4
- package/src/lib/date-format.ts +89 -13
- package/src/lib/day-format.ts +96 -0
- package/src/lib/decimal-marks.ts +12 -3
- package/src/lib/describe-failure.ts +18 -0
- package/src/lib/direction.ts +39 -0
- package/src/lib/focus-left.ts +156 -0
- package/src/lib/format.ts +34 -19
- package/src/lib/http-status.ts +38 -0
- package/src/lib/intl-cache.ts +25 -0
- package/src/lib/isin.ts +10 -0
- package/src/lib/iso-day.ts +13 -0
- package/src/lib/lazy-with-fallback.ts +82 -0
- package/src/lib/path-pattern.ts +55 -0
- package/src/lib/reduced-motion.ts +2 -1
- package/src/lib/round.ts +7 -2
- package/src/lib/safe-storage.ts +48 -6
- package/src/lib/session-refresh.ts +1 -1
- package/src/lib/share-format.ts +18 -0
- package/src/lib/text-case.ts +19 -0
- package/src/lib/text-match.ts +69 -2
- package/src/lib/thenable.ts +19 -0
- package/src/lib/translation-review.ts +13 -5
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-bell.tsx +7 -4
- package/src/notifications/notification-inbox.tsx +40 -58
- package/src/notifications/notification-parts.tsx +3 -4
- package/src/notifications/notification-types-setting.tsx +1 -1
- package/src/notifications/notifications-labels.ts +7 -0
- package/src/notifications/push-device-setting.tsx +1 -1
- package/src/notifications/push-devices-setting.tsx +30 -55
- package/src/notifications/quiet-hours-setting.tsx +93 -31
- package/src/notifications/unsubscribe-panel.tsx +4 -7
- package/src/notifications/use-push-device.ts +76 -10
- package/src/offline/query-persistence.ts +35 -3
- package/src/rhf/fields.tsx +11 -4
- package/src/search/command-palette.tsx +2 -4
- package/src/search/global-search.tsx +36 -15
- package/src/search/use-command-key.ts +13 -12
- package/src/search.ts +9 -0
- package/src/settings/use-settings-focus.ts +3 -0
- package/src/shell/account-menu-language.tsx +27 -5
- package/src/shell/account-menu-text-size.tsx +32 -10
- package/src/shell/dev-menu.tsx +7 -21
- package/src/shell/role-switcher.tsx +7 -0
- package/src/shell/top-bar-actions.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +0 -3
- package/src/shell.ts +40 -9
- package/src/sync/engine.ts +339 -61
- package/src/sync.ts +8 -1
- package/src/testing/css-variables.ts +8 -9
- package/src/theme/contrast.ts +8 -9
- package/src/theme/palette-store.ts +9 -9
- package/src/theme/text-size.ts +6 -8
- package/src/theme/theme-store.ts +7 -9
- package/src/tour/tour-catalogue.tsx +26 -15
- package/src/tour/tour-dock.ts +0 -2
- package/src/tour/tour-gates.ts +23 -0
- package/src/tour/tour-overlay.tsx +1170 -0
- package/src/tour/tour.tsx +116 -1193
- package/src/tour/use-tour-progress.ts +1 -1
- package/src/tour/use-tour-url-sync.ts +7 -3
- package/src/wizard/stepper-nav.tsx +3 -1
- package/src/wizard/types.ts +11 -2
- package/src/wizard/use-wizard.ts +2 -1
- package/src/wizard/validation.ts +8 -2
- package/tokens.css +2 -1
- package/dist/components/ui-labels.js.map +0 -1
- package/dist/shell/dev-switchers.d.ts +0 -6
- package/dist/shell/dev-switchers.js +0 -12
- package/dist/shell/dev-switchers.js.map +0 -1
- package/src/components/ui-labels.ts +0 -24
- package/src/shell/dev-switchers.tsx +0 -18
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
4
|
import { MonitorSmartphone } from "lucide-react";
|
|
5
|
-
import { useKitLabels
|
|
5
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
6
6
|
import { DEFAULT_COMMON_LABELS } from "../i18n/shared-labels.js";
|
|
7
7
|
import { cn } from "../lib/cn.js";
|
|
8
|
-
import {
|
|
8
|
+
import { useDayFormatter } from "../lib/day-format.js";
|
|
9
|
+
import { toDate } from "../lib/format.js";
|
|
9
10
|
import { Button } from "../components/button.js";
|
|
10
11
|
import { Card } from "../components/card.js";
|
|
11
12
|
import { Chip } from "../components/chip.js";
|
|
12
13
|
import { Disclosure } from "../components/disclosure.js";
|
|
14
|
+
import { InlineConfirm } from "../components/inline-confirm.js";
|
|
13
15
|
import { Skeleton } from "../components/skeleton.js";
|
|
14
16
|
import { Switch } from "../components/switch.js";
|
|
15
17
|
import { COMMIT_EXCEPT_BILLING } from "../components/write-lock.js";
|
|
16
18
|
import { CARD_DESCRIPTION_CLASS } from "../settings/settings-card-classes.js";
|
|
17
19
|
import { SettingsCardTitle } from "../settings/settings-heading.js";
|
|
18
|
-
import { settle, useMounted } from "../
|
|
20
|
+
import { settle, useMounted } from "../hooks/use-mounted.js";
|
|
19
21
|
import { describeFailure, FailureLine, useNotificationsLabels, useWriteOverlay } from "./notification-parts.js";
|
|
20
22
|
import { notifyPushSubscriptionChanged } from "./use-push-device.js";
|
|
21
23
|
function describeUserAgent(ua) {
|
|
@@ -52,15 +54,14 @@ function PushDevicesSetting({
|
|
|
52
54
|
const labels = useNotificationsLabels(labelsProp);
|
|
53
55
|
const mutedOverlay = useWriteOverlay(devices);
|
|
54
56
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
55
|
-
const
|
|
57
|
+
const day = useDayFormatter(formatDateProp, localeProp);
|
|
56
58
|
const [asking, setAsking] = useState(null);
|
|
57
59
|
const [busyId, setBusyId] = useState(null);
|
|
58
60
|
const [failure, setFailure] = useState(null);
|
|
59
61
|
const mounted = useMounted();
|
|
60
62
|
const dateText = (iso) => {
|
|
61
|
-
const date =
|
|
62
|
-
|
|
63
|
-
return formatDateProp ? formatDateProp(date) : formatDate(date, "medium", { locale });
|
|
63
|
+
const date = toDate(iso);
|
|
64
|
+
return date ? day(date) : iso;
|
|
64
65
|
};
|
|
65
66
|
const remove = (device) => {
|
|
66
67
|
if (busyId !== null) return;
|
|
@@ -179,46 +180,26 @@ function DeviceRow({
|
|
|
179
180
|
cancelLabel,
|
|
180
181
|
types
|
|
181
182
|
}) {
|
|
182
|
-
const cancelRef = useRef(null);
|
|
183
183
|
const removeRef = useRef(null);
|
|
184
|
-
const [returning, setReturning] = useState(false);
|
|
185
|
-
useEffect(() => {
|
|
186
|
-
if (asking) cancelRef.current?.focus();
|
|
187
|
-
else if (returning) removeRef.current?.focus();
|
|
188
|
-
}, [asking, returning]);
|
|
189
|
-
const leave = () => {
|
|
190
|
-
setReturning(true);
|
|
191
|
-
onCancel();
|
|
192
|
-
};
|
|
193
|
-
const escapeLeaves = (event) => {
|
|
194
|
-
if (event.key === "Escape") {
|
|
195
|
-
event.preventDefault();
|
|
196
|
-
event.stopPropagation();
|
|
197
|
-
leave();
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
184
|
const row = { "data-push-device-id": String(device.id), "data-current": current || void 0 };
|
|
201
185
|
if (asking) {
|
|
202
|
-
return /* @__PURE__ */
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
/* @__PURE__ */ jsx(Button, { ref: cancelRef, type: "button", size: "sm", variant: "ghost", onClick: leave, onKeyDown: escapeLeaves, children: cancelLabel })
|
|
220
|
-
] })
|
|
221
|
-
] });
|
|
186
|
+
return /* @__PURE__ */ jsx(
|
|
187
|
+
InlineConfirm,
|
|
188
|
+
{
|
|
189
|
+
as: "li",
|
|
190
|
+
...row,
|
|
191
|
+
className: "py-2",
|
|
192
|
+
prompt: labels.removePrompt,
|
|
193
|
+
confirmLabel: labels.remove,
|
|
194
|
+
confirmName: labels.removeItem(name),
|
|
195
|
+
cancelLabel,
|
|
196
|
+
onConfirm,
|
|
197
|
+
onLeave: onCancel,
|
|
198
|
+
busy,
|
|
199
|
+
commit: COMMIT_EXCEPT_BILLING,
|
|
200
|
+
returnFocusRef: removeRef
|
|
201
|
+
}
|
|
202
|
+
);
|
|
222
203
|
}
|
|
223
204
|
return /* @__PURE__ */ jsxs("li", { ...row, className: "space-y-1 py-2", children: [
|
|
224
205
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
@@ -245,10 +226,7 @@ function DeviceRow({
|
|
|
245
226
|
commit: COMMIT_EXCEPT_BILLING,
|
|
246
227
|
disabled: blocked,
|
|
247
228
|
"aria-label": labels.removeItem(name),
|
|
248
|
-
onClick:
|
|
249
|
-
setReturning(false);
|
|
250
|
-
onAsk();
|
|
251
|
-
},
|
|
229
|
+
onClick: onAsk,
|
|
252
230
|
children: labels.remove
|
|
253
231
|
}
|
|
254
232
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/notifications/push-devices-setting.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, KeyboardEvent, ReactNode } from \"react\";\nimport { MonitorSmartphone } from \"lucide-react\";\n\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { cn } from \"../lib/cn\";\nimport { formatDate } from \"../lib/format\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { Chip } from \"../components/chip\";\nimport { Disclosure } from \"../components/disclosure\";\nimport { Skeleton } from \"../components/skeleton\";\nimport { Switch } from \"../components/switch\";\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 { NotificationTypeEntry, NotificationTypesValue, PushDeviceItem } from \"./notification-types\";\nimport { notifyPushSubscriptionChanged } from \"./use-push-device\";\n\n/**\n * A name for a stored user agent: \"Chrome · Android\", \"Firefox · Windows\" (keksdose\n * `push-devices-card.tsx:19-45`). Best effort: an agent it does not recognise comes back\n * as it is, cut at 60 characters so a token soup cannot blow up the row; `null` for none.\n * The order matters — Edge and Opera say \"Chrome\" too, and Chrome says \"Safari\".\n */\nexport function describeUserAgent(ua: string | null): string | null {\n if (!ua) return null;\n const browser = /edg(?:e|a|ios)?\\//i.test(ua)\n ? \"Edge\"\n : /opr\\/|opera/i.test(ua)\n ? \"Opera\"\n : /firefox|fxios/i.test(ua)\n ? \"Firefox\"\n : /chrome|crios/i.test(ua)\n ? \"Chrome\"\n : /safari/i.test(ua)\n ? \"Safari\"\n : null;\n const os = /windows/i.test(ua)\n ? \"Windows\"\n : /android/i.test(ua)\n ? \"Android\"\n : /iphone|ipad|ipod/i.test(ua)\n ? \"iOS\"\n : /mac os x|macintosh/i.test(ua)\n ? \"macOS\"\n : /linux/i.test(ua)\n ? \"Linux\"\n : null;\n if (!browser && !os) return ua.length > 60 ? `${ua.slice(0, 60)}…` : ua;\n return [browser, os].filter(Boolean).join(\" · \");\n}\n\nexport interface PushDevicesSettingProps<Id extends string | number = string> extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"children\"\n> {\n /** `GET /notifications/devices`, oldest first; `undefined` while the first load runs. */\n devices: readonly PushDeviceItem<Id>[] | undefined;\n loading?: boolean;\n /** This browser's endpoint (`usePushDevice().currentEndpoint`): its row says \"This\n * device\", and removing it also unsubscribes the browser. */\n currentEndpoint?: string | null;\n /** `DELETE /notifications/devices/{id}`. Asked first; refresh `devices` once it\n * resolves. */\n onRemove: (id: Id) => Promise<unknown> | void;\n /** The dates' text. Default: the provider's locale, medium date. */\n formatDate?: (date: Date) => string;\n /** Overrides the provider's locale for the default format. */\n locale?: string;\n /** The app's own words for a failed removal, or `undefined` for the kit's `failed`. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /**\n * 0.46 (§10.2): \"Types on this device\" on each row — a switch per type whose account\n * `push` is on, all on until the device mutes one. Save the device's WHOLE list:\n * `PATCH /notifications/devices/{id} {\"muted_types\": [...]}`; refresh `devices` once it\n * resolves. Left out, no row has the disclosure.\n */\n onMutedTypesChange?: (id: Id, mutedTypes: string[]) => Promise<unknown> | void;\n /** The app's words for its types (the types card's `types`), for the disclosure. */\n types?: readonly NotificationTypeEntry[];\n /** The settings `GET`'s `types` (the types card's `value`): which types have push on. */\n typeSwitches?: NotificationTypesValue;\n labels?: LabelOverride<NotificationsLabels>;\n}\n\n/** Tear down this browser's subscription after its server row went: otherwise the\n * browser still holds one, and the push card would say \"on\" for a device nothing can\n * reach. Best effort — the row is already gone. */\nasync function unsubscribeThisBrowser(): Promise<void> {\n try {\n const registration = await navigator.serviceWorker?.getRegistration();\n const subscription = await registration?.pushManager.getSubscription();\n await subscription?.unsubscribe();\n } catch {\n /* the server row is gone either way */\n }\n notifyPushSubscriptionChanged();\n}\n\n/**\n * Every device that gets push for the account (docs/notifications-harmonization.md §3.5,\n * §4.2) — keksdose's devices card: the browser and system read from the stored user\n * agent, \"This device\" on this browser's row, when it was added and last delivered to,\n * the failures in a row in the warning tone, and \"Remove\".\n *\n * **Remove asks first, in the row**, as `PasskeysSetting` does: the card must work in an\n * app without a `ConfirmProvider`, and the question belongs beside the device it names.\n * Focus moves to Cancel. **Removing this device** also unsubscribes the browser and fires\n * `PUSH_SUBSCRIPTION_CHANGED` (keksdose `push-devices-card.tsx:83-97`), so the push card\n * stops saying \"on\".\n *\n * **\"Types on this device\"** (0.46, §10.2, with `onMutedTypesChange`): a disclosure under\n * each row with a switch per type the account gets by push; turning one off mutes it on\n * that device only — a device can mute types, never add them. Each flip saves the\n * device's whole `muted_types` list.\n *\n * Under a write lock Remove and the type switches are `commit` controls exempt from\n * billing.\n */\nexport function PushDevicesSetting<Id extends string | number = string>({\n devices,\n loading,\n currentEndpoint,\n onRemove,\n formatDate: formatDateProp,\n locale: localeProp,\n describeError,\n onMutedTypesChange,\n types,\n typeSwitches,\n labels: labelsProp,\n className,\n ...rest\n}: PushDevicesSettingProps<Id>) {\n const labels = useNotificationsLabels(labelsProp);\n const mutedOverlay = useWriteOverlay<string[]>(devices);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n const [asking, setAsking] = useState<Id | null>(null);\n const [busyId, setBusyId] = useState<Id | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n const mounted = useMounted();\n\n const dateText = (iso: string): string => {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return iso;\n return formatDateProp ? formatDateProp(date) : formatDate(date, \"medium\", { locale });\n };\n\n const remove = (device: PushDeviceItem<Id>) => {\n if (busyId !== null) return;\n setBusyId(device.id);\n setFailure(null);\n const isThis = Boolean(currentEndpoint) && device.endpoint === currentEndpoint;\n settle(() => onRemove(device.id))\n .then(async () => {\n if (isThis) await unsubscribeThisBrowser();\n if (!mounted.current) return;\n setBusyId(null);\n setAsking(null);\n })\n .catch((error: unknown) => {\n if (!mounted.current) return;\n setBusyId(null);\n setFailure(describeFailure(describeError?.(error), labels.failed));\n });\n };\n\n // The types a device can mute: those the account gets by push, in the app's order.\n const pushTypes = (types ?? []).filter((entry) => typeSwitches?.[entry.id]?.push === true);\n const mutedOf = (device: PushDeviceItem<Id>): string[] =>\n mutedOverlay.get(String(device.id)) ??\n // Ids the registry dropped (no longer in the settings answer) are ignored.\n (device.muted_types ?? []).filter((id) => !typeSwitches || id in typeSwitches);\n const setMuted = (device: PushDeviceItem<Id>, id: string, on: boolean) => {\n const key = String(device.id);\n if (!onMutedTypesChange || mutedOverlay.pending(key)) return;\n const current = mutedOf(device);\n const next = on ? current.filter((muted) => muted !== id) : [...current.filter((muted) => muted !== id), id];\n setFailure(null);\n mutedOverlay.set(key, next);\n settle(() => onMutedTypesChange(device.id, next)).then(\n () => {\n if (mounted.current) mutedOverlay.settle(key, true);\n },\n (error: unknown) => {\n if (!mounted.current) return;\n mutedOverlay.settle(key, false);\n setFailure(describeFailure(describeError?.(error), labels.failed));\n },\n );\n };\n const typesOf = (device: PushDeviceItem<Id>, name: string): ReactNode => {\n if (!onMutedTypesChange) return null;\n const muted = mutedOf(device);\n return (\n <Disclosure\n variant=\"bare\"\n title={labels.deviceTypes}\n // Every row has one: the name says whose (it starts with the visible words).\n triggerProps={{ \"aria-label\": `${labels.deviceTypes}: ${name}` }}\n className=\"text-sm\"\n data-device-types=\"\"\n >\n <div className=\"space-y-2 pt-1 ps-6\">\n {pushTypes.length === 0 ? (\n <p className={CARD_DESCRIPTION_CLASS}>{labels.deviceTypesEmpty}</p>\n ) : (\n <>\n <p className={CARD_DESCRIPTION_CLASS}>{labels.deviceTypesDescription}</p>\n {pushTypes.map((entry) => (\n <Switch\n key={entry.id}\n size=\"sm\"\n label={entry.label}\n checked={!muted.includes(entry.id)}\n commit={COMMIT_EXCEPT_BILLING}\n onCheckedChange={(on) => setMuted(device, entry.id, on)}\n />\n ))}\n </>\n )}\n </div>\n </Disclosure>\n );\n };\n\n let body: ReactNode;\n if (loading || devices === undefined) {\n body = <Skeleton lines={2} label={labels.devicesLoading} />;\n } else if (devices.length === 0) {\n body = <p className={CARD_DESCRIPTION_CLASS}>{labels.devicesEmpty}</p>;\n } else {\n body = (\n <ul aria-label={labels.devicesTitle} className=\"divide-y divide-subtle\">\n {devices.map((device) => (\n <DeviceRow\n key={device.id}\n device={device}\n name={describeUserAgent(device.user_agent) ?? labels.unknownDevice}\n current={Boolean(currentEndpoint) && device.endpoint === currentEndpoint}\n added={dateText(device.created_at)}\n lastDelivery={device.last_success_at ? dateText(device.last_success_at) : null}\n asking={asking === device.id}\n busy={busyId === device.id}\n blocked={busyId !== null && busyId !== device.id}\n onAsk={() => setAsking(device.id)}\n onCancel={() => setAsking(null)}\n onConfirm={() => remove(device)}\n labels={labels}\n cancelLabel={common.cancel}\n types={typesOf(device, describeUserAgent(device.user_agent) ?? labels.unknownDevice)}\n />\n ))}\n </ul>\n );\n }\n\n return (\n <Card {...rest} className={cn(\"p-4 space-y-3\", className)}>\n <div>\n <SettingsCardTitle>{labels.devicesTitle}</SettingsCardTitle>\n <div className={CARD_DESCRIPTION_CLASS}>{labels.devicesDescription}</div>\n </div>\n {body}\n <FailureLine>{failure}</FailureLine>\n </Card>\n );\n}\n\nfunction DeviceRow<Id extends string | number>({\n device,\n name,\n current,\n added,\n lastDelivery,\n asking,\n busy,\n blocked,\n onAsk,\n onCancel,\n onConfirm,\n labels,\n cancelLabel,\n types,\n}: {\n device: PushDeviceItem<Id>;\n name: string;\n current: boolean;\n added: string;\n lastDelivery: string | null;\n asking: boolean;\n busy: boolean;\n blocked: boolean;\n onAsk: () => void;\n onCancel: () => void;\n onConfirm: () => void;\n labels: NotificationsLabels;\n cancelLabel: string;\n /** \"Types on this device\", under the row (0.46). */\n types: ReactNode;\n}) {\n const cancelRef = useRef<HTMLButtonElement>(null);\n const removeRef = useRef<HTMLButtonElement>(null);\n // Which way the confirm closed: back to Remove, so focus does not fall to <body>.\n const [returning, setReturning] = useState(false);\n useEffect(() => {\n if (asking) cancelRef.current?.focus();\n else if (returning) removeRef.current?.focus();\n }, [asking, returning]);\n\n const leave = () => {\n setReturning(true);\n onCancel();\n };\n const escapeLeaves = (event: KeyboardEvent<HTMLButtonElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n leave();\n }\n };\n\n const row = { \"data-push-device-id\": String(device.id), \"data-current\": current || undefined };\n\n if (asking) {\n return (\n <li {...row} className=\"space-y-2 py-2\">\n <p role=\"alert\" className=\"text-sm\">\n {labels.removePrompt}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"danger\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy}\n aria-label={labels.removeItem(name)}\n onClick={onConfirm}\n onKeyDown={escapeLeaves}\n >\n {labels.remove}\n </Button>\n <Button ref={cancelRef} type=\"button\" size=\"sm\" variant=\"ghost\" onClick={leave} onKeyDown={escapeLeaves}>\n {cancelLabel}\n </Button>\n </div>\n </li>\n );\n }\n\n return (\n <li {...row} className=\"space-y-1 py-2\">\n <div className=\"flex items-center gap-3\">\n <MonitorSmartphone aria-hidden className=\"size-4 shrink-0 text-muted\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <span className=\"min-w-0 break-words text-sm font-medium\">{name}</span>\n {current && (\n <Chip size=\"sm\" tone=\"brand\">\n {labels.thisDevice}\n </Chip>\n )}\n </div>\n <div className={CARD_DESCRIPTION_CLASS}>\n {labels.added(added)}\n {\" · \"}\n {lastDelivery ? labels.lastDelivery(lastDelivery) : labels.noDelivery}\n </div>\n {device.failure_count > 0 && (\n <div className=\"text-xs text-warning\">{labels.failures(device.failure_count)}</div>\n )}\n </div>\n <Button\n ref={removeRef}\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n commit={COMMIT_EXCEPT_BILLING}\n disabled={blocked}\n aria-label={labels.removeItem(name)}\n onClick={() => {\n setReturning(false);\n onAsk();\n }}\n >\n {labels.remove}\n </Button>\n </div>\n {types}\n </li>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/notifications/push-devices-setting.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { MonitorSmartphone } from \"lucide-react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { cn } from \"../lib/cn\";\nimport { useDayFormatter } from \"../lib/day-format\";\nimport { toDate } from \"../lib/format\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { Chip } from \"../components/chip\";\nimport { Disclosure } from \"../components/disclosure\";\nimport { InlineConfirm } from \"../components/inline-confirm\";\nimport { Skeleton } from \"../components/skeleton\";\nimport { Switch } from \"../components/switch\";\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 \"../hooks/use-mounted\";\nimport { describeFailure, FailureLine, useNotificationsLabels, useWriteOverlay } from \"./notification-parts\";\nimport type { NotificationsLabels } from \"./notifications-labels\";\nimport type { NotificationTypeEntry, NotificationTypesValue, PushDeviceItem } from \"./notification-types\";\nimport { notifyPushSubscriptionChanged } from \"./use-push-device\";\n\n/**\n * A name for a stored user agent: \"Chrome · Android\", \"Firefox · Windows\" (keksdose\n * `push-devices-card.tsx:19-45`). Best effort: an agent it does not recognise comes back\n * as it is, cut at 60 characters so a token soup cannot blow up the row; `null` for none.\n * The order matters — Edge and Opera say \"Chrome\" too, and Chrome says \"Safari\".\n */\nexport function describeUserAgent(ua: string | null): string | null {\n if (!ua) return null;\n const browser = /edg(?:e|a|ios)?\\//i.test(ua)\n ? \"Edge\"\n : /opr\\/|opera/i.test(ua)\n ? \"Opera\"\n : /firefox|fxios/i.test(ua)\n ? \"Firefox\"\n : /chrome|crios/i.test(ua)\n ? \"Chrome\"\n : /safari/i.test(ua)\n ? \"Safari\"\n : null;\n const os = /windows/i.test(ua)\n ? \"Windows\"\n : /android/i.test(ua)\n ? \"Android\"\n : /iphone|ipad|ipod/i.test(ua)\n ? \"iOS\"\n : /mac os x|macintosh/i.test(ua)\n ? \"macOS\"\n : /linux/i.test(ua)\n ? \"Linux\"\n : null;\n if (!browser && !os) return ua.length > 60 ? `${ua.slice(0, 60)}…` : ua;\n return [browser, os].filter(Boolean).join(\" · \");\n}\n\nexport interface PushDevicesSettingProps<Id extends string | number = string> extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"children\"\n> {\n /** `GET /notifications/devices`, oldest first; `undefined` while the first load runs. */\n devices: readonly PushDeviceItem<Id>[] | undefined;\n loading?: boolean;\n /** This browser's endpoint (`usePushDevice().currentEndpoint`): its row says \"This\n * device\", and removing it also unsubscribes the browser. */\n currentEndpoint?: string | null;\n /** `DELETE /notifications/devices/{id}`. Asked first; refresh `devices` once it\n * resolves. */\n onRemove: (id: Id) => Promise<unknown> | void;\n /** The dates' text. Default: the provider's `formatDate` (the app's date preference),\n * else a medium date in the provider's locale and `timeZone`. */\n formatDate?: (date: Date) => string;\n /** Overrides the provider's locale for the default format. */\n locale?: string;\n /** The app's own words for a failed removal, or `undefined` for the kit's `failed`. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /**\n * 0.46 (§10.2): \"Types on this device\" on each row — a switch per type whose account\n * `push` is on, all on until the device mutes one. Save the device's WHOLE list:\n * `PATCH /notifications/devices/{id} {\"muted_types\": [...]}`; refresh `devices` once it\n * resolves. Left out, no row has the disclosure.\n */\n onMutedTypesChange?: (id: Id, mutedTypes: string[]) => Promise<unknown> | void;\n /** The app's words for its types (the types card's `types`), for the disclosure. */\n types?: readonly NotificationTypeEntry[];\n /** The settings `GET`'s `types` (the types card's `value`): which types have push on. */\n typeSwitches?: NotificationTypesValue;\n labels?: LabelOverride<NotificationsLabels>;\n}\n\n/** Tear down this browser's subscription after its server row went: otherwise the\n * browser still holds one, and the push card would say \"on\" for a device nothing can\n * reach. Best effort — the row is already gone. */\nasync function unsubscribeThisBrowser(): Promise<void> {\n try {\n const registration = await navigator.serviceWorker?.getRegistration();\n const subscription = await registration?.pushManager.getSubscription();\n await subscription?.unsubscribe();\n } catch {\n /* the server row is gone either way */\n }\n notifyPushSubscriptionChanged();\n}\n\n/**\n * Every device that gets push for the account (docs/notifications-harmonization.md §3.5,\n * §4.2) — keksdose's devices card: the browser and system read from the stored user\n * agent, \"This device\" on this browser's row, when it was added and last delivered to,\n * the failures in a row in the warning tone, and \"Remove\".\n *\n * **Remove asks first, in the row**, as `PasskeysSetting` does: the card must work in an\n * app without a `ConfirmProvider`, and the question belongs beside the device it names.\n * Focus moves to Cancel. **Removing this device** also unsubscribes the browser and fires\n * `PUSH_SUBSCRIPTION_CHANGED` (keksdose `push-devices-card.tsx:83-97`), so the push card\n * stops saying \"on\".\n *\n * **\"Types on this device\"** (0.46, §10.2, with `onMutedTypesChange`): a disclosure under\n * each row with a switch per type the account gets by push; turning one off mutes it on\n * that device only — a device can mute types, never add them. Each flip saves the\n * device's whole `muted_types` list.\n *\n * Under a write lock Remove and the type switches are `commit` controls exempt from\n * billing.\n */\nexport function PushDevicesSetting<Id extends string | number = string>({\n devices,\n loading,\n currentEndpoint,\n onRemove,\n formatDate: formatDateProp,\n locale: localeProp,\n describeError,\n onMutedTypesChange,\n types,\n typeSwitches,\n labels: labelsProp,\n className,\n ...rest\n}: PushDevicesSettingProps<Id>) {\n const labels = useNotificationsLabels(labelsProp);\n const mutedOverlay = useWriteOverlay<string[]>(devices);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n // The prop, else the provider's `formatDate` (the app's date preference), else the kit's\n // in the provider's locale and `timeZone` (refactor audit 2026-10-30, DUP-2).\n const day = useDayFormatter(formatDateProp, localeProp);\n const [asking, setAsking] = useState<Id | null>(null);\n const [busyId, setBusyId] = useState<Id | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n const mounted = useMounted();\n\n const dateText = (iso: string): string => {\n const date = toDate(iso);\n return date ? day(date) : iso;\n };\n\n const remove = (device: PushDeviceItem<Id>) => {\n if (busyId !== null) return;\n setBusyId(device.id);\n setFailure(null);\n const isThis = Boolean(currentEndpoint) && device.endpoint === currentEndpoint;\n settle(() => onRemove(device.id))\n .then(async () => {\n if (isThis) await unsubscribeThisBrowser();\n if (!mounted.current) return;\n setBusyId(null);\n setAsking(null);\n })\n .catch((error: unknown) => {\n if (!mounted.current) return;\n setBusyId(null);\n setFailure(describeFailure(describeError?.(error), labels.failed));\n });\n };\n\n // The types a device can mute: those the account gets by push, in the app's order.\n const pushTypes = (types ?? []).filter((entry) => typeSwitches?.[entry.id]?.push === true);\n const mutedOf = (device: PushDeviceItem<Id>): string[] =>\n mutedOverlay.get(String(device.id)) ??\n // Ids the registry dropped (no longer in the settings answer) are ignored.\n (device.muted_types ?? []).filter((id) => !typeSwitches || id in typeSwitches);\n const setMuted = (device: PushDeviceItem<Id>, id: string, on: boolean) => {\n const key = String(device.id);\n if (!onMutedTypesChange || mutedOverlay.pending(key)) return;\n const current = mutedOf(device);\n const next = on ? current.filter((muted) => muted !== id) : [...current.filter((muted) => muted !== id), id];\n setFailure(null);\n mutedOverlay.set(key, next);\n settle(() => onMutedTypesChange(device.id, next)).then(\n () => {\n if (mounted.current) mutedOverlay.settle(key, true);\n },\n (error: unknown) => {\n if (!mounted.current) return;\n mutedOverlay.settle(key, false);\n setFailure(describeFailure(describeError?.(error), labels.failed));\n },\n );\n };\n const typesOf = (device: PushDeviceItem<Id>, name: string): ReactNode => {\n if (!onMutedTypesChange) return null;\n const muted = mutedOf(device);\n return (\n <Disclosure\n variant=\"bare\"\n title={labels.deviceTypes}\n // Every row has one: the name says whose (it starts with the visible words).\n triggerProps={{ \"aria-label\": `${labels.deviceTypes}: ${name}` }}\n className=\"text-sm\"\n data-device-types=\"\"\n >\n <div className=\"space-y-2 pt-1 ps-6\">\n {pushTypes.length === 0 ? (\n <p className={CARD_DESCRIPTION_CLASS}>{labels.deviceTypesEmpty}</p>\n ) : (\n <>\n <p className={CARD_DESCRIPTION_CLASS}>{labels.deviceTypesDescription}</p>\n {pushTypes.map((entry) => (\n <Switch\n key={entry.id}\n size=\"sm\"\n label={entry.label}\n checked={!muted.includes(entry.id)}\n commit={COMMIT_EXCEPT_BILLING}\n onCheckedChange={(on) => setMuted(device, entry.id, on)}\n />\n ))}\n </>\n )}\n </div>\n </Disclosure>\n );\n };\n\n let body: ReactNode;\n if (loading || devices === undefined) {\n body = <Skeleton lines={2} label={labels.devicesLoading} />;\n } else if (devices.length === 0) {\n body = <p className={CARD_DESCRIPTION_CLASS}>{labels.devicesEmpty}</p>;\n } else {\n body = (\n <ul aria-label={labels.devicesTitle} className=\"divide-y divide-subtle\">\n {devices.map((device) => (\n <DeviceRow\n key={device.id}\n device={device}\n name={describeUserAgent(device.user_agent) ?? labels.unknownDevice}\n current={Boolean(currentEndpoint) && device.endpoint === currentEndpoint}\n added={dateText(device.created_at)}\n lastDelivery={device.last_success_at ? dateText(device.last_success_at) : null}\n asking={asking === device.id}\n busy={busyId === device.id}\n blocked={busyId !== null && busyId !== device.id}\n onAsk={() => setAsking(device.id)}\n onCancel={() => setAsking(null)}\n onConfirm={() => remove(device)}\n labels={labels}\n cancelLabel={common.cancel}\n types={typesOf(device, describeUserAgent(device.user_agent) ?? labels.unknownDevice)}\n />\n ))}\n </ul>\n );\n }\n\n return (\n <Card {...rest} className={cn(\"p-4 space-y-3\", className)}>\n <div>\n <SettingsCardTitle>{labels.devicesTitle}</SettingsCardTitle>\n <div className={CARD_DESCRIPTION_CLASS}>{labels.devicesDescription}</div>\n </div>\n {body}\n <FailureLine>{failure}</FailureLine>\n </Card>\n );\n}\n\nfunction DeviceRow<Id extends string | number>({\n device,\n name,\n current,\n added,\n lastDelivery,\n asking,\n busy,\n blocked,\n onAsk,\n onCancel,\n onConfirm,\n labels,\n cancelLabel,\n types,\n}: {\n device: PushDeviceItem<Id>;\n name: string;\n current: boolean;\n added: string;\n lastDelivery: string | null;\n asking: boolean;\n busy: boolean;\n blocked: boolean;\n onAsk: () => void;\n onCancel: () => void;\n onConfirm: () => void;\n labels: NotificationsLabels;\n cancelLabel: string;\n /** \"Types on this device\", under the row (0.46). */\n types: ReactNode;\n}) {\n // Back to Remove when the confirm leaves, so focus does not fall to <body>.\n const removeRef = useRef<HTMLButtonElement>(null);\n\n const row = { \"data-push-device-id\": String(device.id), \"data-current\": current || undefined };\n\n if (asking) {\n return (\n <InlineConfirm\n as=\"li\"\n {...row}\n className=\"py-2\"\n prompt={labels.removePrompt}\n confirmLabel={labels.remove}\n confirmName={labels.removeItem(name)}\n cancelLabel={cancelLabel}\n onConfirm={onConfirm}\n onLeave={onCancel}\n busy={busy}\n commit={COMMIT_EXCEPT_BILLING}\n returnFocusRef={removeRef}\n />\n );\n }\n\n return (\n <li {...row} className=\"space-y-1 py-2\">\n <div className=\"flex items-center gap-3\">\n <MonitorSmartphone aria-hidden className=\"size-4 shrink-0 text-muted\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <span className=\"min-w-0 break-words text-sm font-medium\">{name}</span>\n {current && (\n <Chip size=\"sm\" tone=\"brand\">\n {labels.thisDevice}\n </Chip>\n )}\n </div>\n <div className={CARD_DESCRIPTION_CLASS}>\n {labels.added(added)}\n {\" · \"}\n {lastDelivery ? labels.lastDelivery(lastDelivery) : labels.noDelivery}\n </div>\n {device.failure_count > 0 && (\n <div className=\"text-xs text-warning\">{labels.failures(device.failure_count)}</div>\n )}\n </div>\n <Button\n ref={removeRef}\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n commit={COMMIT_EXCEPT_BILLING}\n disabled={blocked}\n aria-label={labels.removeItem(name)}\n onClick={onAsk}\n >\n {labels.remove}\n </Button>\n </div>\n {types}\n </li>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -20,7 +20,9 @@ export interface QuietHoursSettingProps extends Omit<ComponentPropsWithRef<"div"
|
|
|
20
20
|
held?: boolean;
|
|
21
21
|
/**
|
|
22
22
|
* Save one change — `PATCH /notifications/settings`: a time, or `null` when cleared.
|
|
23
|
-
* Never a zone.
|
|
23
|
+
* Never a zone. Called when a time is ENTERED — the field left, or Enter, or the card
|
|
24
|
+
* unmounted with a time still typed — never per keystroke, and one call per field at a
|
|
25
|
+
* time (0.52). The field shows the new value while the call runs and goes back if it
|
|
24
26
|
* rejects.
|
|
25
27
|
*/
|
|
26
28
|
onChange: (patch: NotificationSettingsPatch) => Promise<unknown> | void;
|
|
@@ -45,8 +47,13 @@ export interface QuietHoursSettingProps extends Omit<ComponentPropsWithRef<"div"
|
|
|
45
47
|
* only** — the zone of the device last used, else `Europe/Zurich`. The card names it
|
|
46
48
|
* read-only and never sends one; there is no picker and no "Use it".
|
|
47
49
|
*
|
|
48
|
-
* - **Two times**, each saved
|
|
49
|
-
*
|
|
50
|
+
* - **Two times**, each saved when it is entered — on leaving the field or on Enter, or
|
|
51
|
+
* when the card goes while it is still typed — and a cleared time sends `null`. While
|
|
52
|
+
* only one is set the card says so: the window fails open until both are.
|
|
53
|
+
* - **One request per field at a time** (0.52): a time entered while that field's save
|
|
54
|
+
* runs waits for it, and only the latest waiting one is sent. Each input event used to
|
|
55
|
+
* PATCH — typing "30" over 22:00's minutes sent 22:03 and 22:30, and whichever answer
|
|
56
|
+
* landed last stayed on the server (refactor audit 2026-10-30, FLW-4).
|
|
50
57
|
* - **The zone** shows while a time is set: `zone`, else `fallbackZone`.
|
|
51
58
|
*
|
|
52
59
|
* Under a write lock the fields are `commit` controls exempt from billing.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useId, useState } from "react";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
4
|
import { cn } from "../lib/cn.js";
|
|
5
5
|
import { Card } from "../components/card.js";
|
|
6
6
|
import { TimeInput } from "../components/time-input.js";
|
|
7
7
|
import { COMMIT_EXCEPT_BILLING } from "../components/write-lock.js";
|
|
8
8
|
import { CARD_DESCRIPTION_CLASS } from "../settings/settings-card-classes.js";
|
|
9
9
|
import { SettingsCardTitle } from "../settings/settings-heading.js";
|
|
10
|
-
import { settle, useMounted } from "../
|
|
10
|
+
import { settle, useMounted } from "../hooks/use-mounted.js";
|
|
11
11
|
import { describeFailure, FailureLine, useNotificationsLabels, useWriteOverlay } from "./notification-parts.js";
|
|
12
12
|
const DEFAULT_FALLBACK_ZONE = "Europe/Zurich";
|
|
13
13
|
function zoneName(zone, regions) {
|
|
@@ -36,29 +36,64 @@ function QuietHoursSetting({
|
|
|
36
36
|
const [failure, setFailure] = useState(null);
|
|
37
37
|
const mounted = useMounted();
|
|
38
38
|
const id = useId();
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
39
|
+
const [drafts, setDrafts] = useState({});
|
|
40
|
+
const running = useRef({});
|
|
41
|
+
const waiting = useRef({});
|
|
42
|
+
const server = (field) => field === "quiet_hours_start" ? start : end;
|
|
43
|
+
const saved = (field) => overlay.has(field) ? overlay.get(field) ?? null : server(field);
|
|
44
|
+
const shown = (field) => drafts[field] ?? saved(field) ?? "";
|
|
45
|
+
const startShown = shown("quiet_hours_start");
|
|
46
|
+
const endShown = shown("quiet_hours_end");
|
|
42
47
|
const accountZone = zone ?? fallbackZone;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
setFailure(null);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
() =>
|
|
49
|
-
if (mounted.current) for (const field of fields) overlay.settle(field, true);
|
|
50
|
-
},
|
|
51
|
-
(error) => {
|
|
52
|
-
if (!mounted.current) return;
|
|
53
|
-
for (const field of fields) overlay.settle(field, false);
|
|
54
|
-
setFailure(describeFailure(describeError?.(error), labels.failed));
|
|
55
|
-
}
|
|
48
|
+
const send = (field, value) => {
|
|
49
|
+
running.current[field] = true;
|
|
50
|
+
if (mounted.current) setFailure(null);
|
|
51
|
+
settle(() => onChange({ [field]: value })).then(
|
|
52
|
+
() => done(field, true),
|
|
53
|
+
(error) => done(field, false, error)
|
|
56
54
|
);
|
|
57
55
|
};
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
56
|
+
const done = (field, ok, error) => {
|
|
57
|
+
running.current[field] = false;
|
|
58
|
+
if (Object.prototype.hasOwnProperty.call(waiting.current, field)) {
|
|
59
|
+
const next = waiting.current[field] ?? null;
|
|
60
|
+
delete waiting.current[field];
|
|
61
|
+
send(field, next);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!mounted.current) return;
|
|
65
|
+
overlay.settle(field, ok);
|
|
66
|
+
if (!ok) setFailure(describeFailure(describeError?.(error), labels.failed));
|
|
67
|
+
};
|
|
68
|
+
const save = (field, draft) => {
|
|
69
|
+
const next = draft || null;
|
|
70
|
+
if (next === saved(field)) return void 0;
|
|
71
|
+
if (running.current[field]) waiting.current[field] = next;
|
|
72
|
+
else send(field, next);
|
|
73
|
+
return next;
|
|
74
|
+
};
|
|
75
|
+
const enter = (field) => {
|
|
76
|
+
const draft = drafts[field];
|
|
77
|
+
if (draft === void 0) return;
|
|
78
|
+
setDrafts((current) => {
|
|
79
|
+
const rest2 = { ...current };
|
|
80
|
+
delete rest2[field];
|
|
81
|
+
return rest2;
|
|
82
|
+
});
|
|
83
|
+
const next = save(field, draft);
|
|
84
|
+
if (next !== void 0) overlay.set(field, next);
|
|
85
|
+
};
|
|
86
|
+
const leave = useRef(() => {
|
|
87
|
+
});
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
leave.current = () => {
|
|
90
|
+
for (const [field, draft] of Object.entries(drafts)) save(field, draft);
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
useEffect(() => () => leave.current(), []);
|
|
94
|
+
const type = (field, value) => setDrafts((current) => ({ ...current, [field]: value }));
|
|
95
|
+
const enterOnEnter = (field, event) => {
|
|
96
|
+
if (event.key === "Enter") enter(field);
|
|
62
97
|
};
|
|
63
98
|
const anySet = Boolean(startShown || endShown);
|
|
64
99
|
const halfSet = Boolean(startShown) !== Boolean(endShown);
|
|
@@ -73,10 +108,12 @@ function QuietHoursSetting({
|
|
|
73
108
|
{
|
|
74
109
|
id: `${id}-start`,
|
|
75
110
|
label: labels.quietFrom,
|
|
76
|
-
value: startShown
|
|
111
|
+
value: startShown,
|
|
112
|
+
onValueChange: (value) => type("quiet_hours_start", value),
|
|
113
|
+
onBlur: () => enter("quiet_hours_start"),
|
|
114
|
+
onKeyDown: (event) => enterOnEnter("quiet_hours_start", event),
|
|
77
115
|
disabled,
|
|
78
116
|
commit: COMMIT_EXCEPT_BILLING,
|
|
79
|
-
onValueChange: (value) => saveTime("quiet_hours_start", value),
|
|
80
117
|
className: "w-36"
|
|
81
118
|
}
|
|
82
119
|
),
|
|
@@ -85,10 +122,12 @@ function QuietHoursSetting({
|
|
|
85
122
|
{
|
|
86
123
|
id: `${id}-end`,
|
|
87
124
|
label: labels.quietUntil,
|
|
88
|
-
value: endShown
|
|
125
|
+
value: endShown,
|
|
126
|
+
onValueChange: (value) => type("quiet_hours_end", value),
|
|
127
|
+
onBlur: () => enter("quiet_hours_end"),
|
|
128
|
+
onKeyDown: (event) => enterOnEnter("quiet_hours_end", event),
|
|
89
129
|
disabled,
|
|
90
130
|
commit: COMMIT_EXCEPT_BILLING,
|
|
91
|
-
onValueChange: (value) => saveTime("quiet_hours_end", value),
|
|
92
131
|
className: "w-36"
|
|
93
132
|
}
|
|
94
133
|
)
|
|
@@ -1 +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 { 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 { NotificationRegionLabels, NotificationsLabels } from \"./notifications-labels\";\nimport type { NotificationSettingsPatch } from \"./notification-types\";\n\n/** The zone the server applies while no device ever reported one (§10.3): every app's. */\nconst DEFAULT_FALLBACK_ZONE = \"Europe/Zurich\";\n\n/** A zone for a sentence: \"Europe / Zurich\", the region in the kit's words (`labels.regions`);\n * a region the labels do not know keeps its IANA name, a zone with none (\"UTC\") is itself. */\nfunction 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 const named = Object.prototype.hasOwnProperty.call(regions, region)\n ? regions[region as keyof NotificationRegionLabels]\n : region;\n return `${named} / ${zone\n .slice(slash + 1)\n .replace(/_/g, \" \")\n .replace(/\\//g, \" / \")}`;\n}\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 /**\n * 0.46 (§10.3, amended 2026-10-20: automatic only): the account's zone — `/auth/me`'s\n * `device_timezone`, the zone of the device last used; `null` while no device has\n * reported one, and the card then names `fallbackZone`, the zone the server applies.\n * Shown read-only once a time is set (\"In your time zone (Europe / Zurich), which follows\n * the device you last used.\"). No picker: the zone is never chosen.\n */\n zone?: string | null;\n /** 0.46 (§10.4): the app queues a held push and sends it when the window ends (it runs\n * the notifications tick); the description says so. Default `false`: dropped. */\n held?: boolean;\n /**\n * Save one change — `PATCH /notifications/settings`: a time, or `null` when cleared.\n * Never a zone. 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 named while `zone` names none: the one the server\n * applies. Default `\"Europe/Zurich\"`, every app's. */\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\";\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 — or, with `held` (0.46, §10.4), sent when the\n * window ends; the inbox row and the mail go as usual. keksdose's block, as a card of its\n * own.\n *\n * **0.46 (§10.3, amended 2026-10-20, Marcel): the zone is the account's, automatic\n * only** — the zone of the device last used, else `Europe/Zurich`. The card names it\n * read-only and never sends one; there is no picker and no \"Use it\".\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: `zone`, else `fallbackZone`.\n *\n * Under a write lock the fields are `commit` controls exempt from billing.\n */\nexport function QuietHoursSetting({\n start,\n end,\n zone,\n held = false,\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}`);\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 accountZone = zone ?? fallbackZone;\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 save({ [field]: next });\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, held)}</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 <p className={CARD_DESCRIPTION_CLASS} data-quiet-zone=\"\">\n {labels.quietZone(zoneName(accountZone, labels.regions))}\n </p>\n )}\n <FailureLine>{failure}</FailureLine>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/notifications/quiet-hours-setting.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, KeyboardEvent, ReactNode } from \"react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\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 \"../hooks/use-mounted\";\nimport { describeFailure, FailureLine, useNotificationsLabels, useWriteOverlay } from \"./notification-parts\";\nimport type { NotificationRegionLabels, NotificationsLabels } from \"./notifications-labels\";\nimport type { NotificationSettingsPatch } from \"./notification-types\";\n\n/** The zone the server applies while no device ever reported one (§10.3): every app's. */\nconst DEFAULT_FALLBACK_ZONE = \"Europe/Zurich\";\n\n/** A zone for a sentence: \"Europe / Zurich\", the region in the kit's words (`labels.regions`);\n * a region the labels do not know keeps its IANA name, a zone with none (\"UTC\") is itself. */\nfunction 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 const named = Object.prototype.hasOwnProperty.call(regions, region)\n ? regions[region as keyof NotificationRegionLabels]\n : region;\n return `${named} / ${zone\n .slice(slash + 1)\n .replace(/_/g, \" \")\n .replace(/\\//g, \" / \")}`;\n}\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 /**\n * 0.46 (§10.3, amended 2026-10-20: automatic only): the account's zone — `/auth/me`'s\n * `device_timezone`, the zone of the device last used; `null` while no device has\n * reported one, and the card then names `fallbackZone`, the zone the server applies.\n * Shown read-only once a time is set (\"In your time zone (Europe / Zurich), which follows\n * the device you last used.\"). No picker: the zone is never chosen.\n */\n zone?: string | null;\n /** 0.46 (§10.4): the app queues a held push and sends it when the window ends (it runs\n * the notifications tick); the description says so. Default `false`: dropped. */\n held?: boolean;\n /**\n * Save one change — `PATCH /notifications/settings`: a time, or `null` when cleared.\n * Never a zone. Called when a time is ENTERED — the field left, or Enter, or the card\n * unmounted with a time still typed — never per keystroke, and one call per field at a\n * time (0.52). 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 named while `zone` names none: the one the server\n * applies. Default `\"Europe/Zurich\"`, every app's. */\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\";\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 — or, with `held` (0.46, §10.4), sent when the\n * window ends; the inbox row and the mail go as usual. keksdose's block, as a card of its\n * own.\n *\n * **0.46 (§10.3, amended 2026-10-20, Marcel): the zone is the account's, automatic\n * only** — the zone of the device last used, else `Europe/Zurich`. The card names it\n * read-only and never sends one; there is no picker and no \"Use it\".\n *\n * - **Two times**, each saved when it is entered — on leaving the field or on Enter, or\n * when the card goes while it is still typed — and a cleared time sends `null`. While\n * only one is set the card says so: the window fails open until both are.\n * - **One request per field at a time** (0.52): a time entered while that field's save\n * runs waits for it, and only the latest waiting one is sent. Each input event used to\n * PATCH — typing \"30\" over 22:00's minutes sent 22:03 and 22:30, and whichever answer\n * landed last stayed on the server (refactor audit 2026-10-30, FLW-4).\n * - **The zone** shows while a time is set: `zone`, else `fallbackZone`.\n *\n * Under a write lock the fields are `commit` controls exempt from billing.\n */\nexport function QuietHoursSetting({\n start,\n end,\n zone,\n held = false,\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}`);\n const [failure, setFailure] = useState<ReactNode>(null);\n const mounted = useMounted();\n const id = useId();\n\n // What is typed and not yet entered, per field: the field shows it, nothing is sent.\n const [drafts, setDrafts] = useState<Partial<Record<QuietField, string>>>({});\n // Per field: whether a save runs, and the latest time entered while it did (own key =\n // one is waiting). Refs: a save's answer reads them after renders it did not see.\n const running = useRef<Partial<Record<QuietField, boolean>>>({});\n const waiting = useRef<Partial<Record<QuietField, string | null>>>({});\n\n const server = (field: QuietField) => (field === \"quiet_hours_start\" ? start : end);\n const saved = (field: QuietField) => (overlay.has(field) ? (overlay.get(field) ?? null) : server(field));\n const shown = (field: QuietField) => drafts[field] ?? saved(field) ?? \"\";\n const startShown = shown(\"quiet_hours_start\");\n const endShown = shown(\"quiet_hours_end\");\n const accountZone = zone ?? fallbackZone;\n\n const send = (field: QuietField, value: string | null) => {\n running.current[field] = true;\n if (mounted.current) setFailure(null);\n settle(() => onChange({ [field]: value })).then(\n () => done(field, true),\n (error: unknown) => done(field, false, error),\n );\n };\n\n /** A save answered: the waiting time goes next — even after an unmount, it is what the\n * person entered last — and supersedes this answer, a failure included. */\n const done = (field: QuietField, ok: boolean, error?: unknown) => {\n running.current[field] = false;\n if (Object.prototype.hasOwnProperty.call(waiting.current, field)) {\n const next = waiting.current[field] ?? null;\n delete waiting.current[field];\n send(field, next);\n return;\n }\n if (!mounted.current) return;\n overlay.settle(field, ok);\n if (!ok) setFailure(describeFailure(describeError?.(error), labels.failed));\n };\n\n /** `draft` entered: the time it names, or `undefined` when that is what the field\n * already holds and nothing is to be sent. Queued behind the field's running save. */\n const save = (field: QuietField, draft: string): string | null | undefined => {\n const next = draft || null;\n if (next === saved(field)) return undefined;\n if (running.current[field]) waiting.current[field] = next;\n else send(field, next);\n return next;\n };\n\n /** The typed time, entered: saved unless it is what the field already holds. */\n const enter = (field: QuietField) => {\n const draft = drafts[field];\n if (draft === undefined) return;\n setDrafts((current) => {\n const rest = { ...current };\n delete rest[field];\n return rest;\n });\n const next = save(field, draft);\n if (next !== undefined) overlay.set(field, next);\n };\n\n // Leaving the card is leaving the field. A focused input removed with its card fires no\n // blur — Escape on the settings sheet, a shortcut that navigates — and the time it showed\n // as typed was dropped (0.52 code review, quiet-hours-setting.tsx; save-on-blur is FLW-4's\n // decision). So whatever is still typed is entered as the card goes. A ref to this\n // render's drafts and saved values, assigned in an effect, never during render.\n const leave = useRef<() => void>(() => {});\n useEffect(() => {\n leave.current = () => {\n for (const [field, draft] of Object.entries(drafts) as [QuietField, string][]) save(field, draft);\n };\n });\n useEffect(() => () => leave.current(), []);\n\n const type = (field: QuietField, value: string) => setDrafts((current) => ({ ...current, [field]: value }));\n const enterOnEnter = (field: QuietField, event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === \"Enter\") enter(field);\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, held)}</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 onValueChange={(value) => type(\"quiet_hours_start\", value)}\n onBlur={() => enter(\"quiet_hours_start\")}\n onKeyDown={(event) => enterOnEnter(\"quiet_hours_start\", event)}\n disabled={disabled}\n commit={COMMIT_EXCEPT_BILLING}\n className=\"w-36\"\n />\n <TimeInput\n id={`${id}-end`}\n label={labels.quietUntil}\n value={endShown}\n onValueChange={(value) => type(\"quiet_hours_end\", value)}\n onBlur={() => enter(\"quiet_hours_end\")}\n onKeyDown={(event) => enterOnEnter(\"quiet_hours_end\", event)}\n disabled={disabled}\n commit={COMMIT_EXCEPT_BILLING}\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 <p className={CARD_DESCRIPTION_CLASS} data-quiet-zone=\"\">\n {labels.quietZone(zoneName(accountZone, labels.regions))}\n </p>\n )}\n <FailureLine>{failure}</FailureLine>\n </Card>\n );\n}\n"],"mappings":"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","names":["rest"]}
|
|
@@ -3,11 +3,12 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useRef, useState } from "react";
|
|
4
4
|
import { CheckCircle2, Link2Off } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
|
-
import { errorBodyWithCode
|
|
6
|
+
import { errorBodyWithCode } from "../auth/auth-errors.js";
|
|
7
|
+
import { httpStatusOf } from "../lib/http-status.js";
|
|
7
8
|
import { INTRO_CLASS, OUTCOME_CLASS, STATUS_HEADING_CLASS, useFocusWhen } from "../auth/status-parts.js";
|
|
8
9
|
import { Button } from "../components/button.js";
|
|
9
10
|
import { EmptyState } from "../components/empty-state.js";
|
|
10
|
-
import { useMounted } from "../
|
|
11
|
+
import { useMounted } from "../hooks/use-mounted.js";
|
|
11
12
|
import { FailureLine, useNotificationsLabels } from "./notification-parts.js";
|
|
12
13
|
const TYPE_ID = /^[a-z][a-z0-9_]{1,39}$/;
|
|
13
14
|
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
@@ -87,11 +88,7 @@ function UnsubscribePanel({
|
|
|
87
88
|
tone: "success",
|
|
88
89
|
icon: /* @__PURE__ */ jsx(CheckCircle2, {}),
|
|
89
90
|
title: shown === "stopped-these" ? labels.stoppedThese : labels.stoppedAll(appName),
|
|
90
|
-
hint:
|
|
91
|
-
labels.accountMail,
|
|
92
|
-
" ",
|
|
93
|
-
labels.turnBackOn
|
|
94
|
-
] }),
|
|
91
|
+
hint: labels.stoppedHint(labels.accountMail, labels.turnBackOn),
|
|
95
92
|
action: settingsLink,
|
|
96
93
|
className: OUTCOME_CLASS
|
|
97
94
|
}
|
|
@@ -1 +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":[]}
|
|
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 } from \"../auth/auth-errors\";\nimport { httpStatusOf } from \"../lib/http-status\";\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 \"../hooks/use-mounted\";\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={labels.stoppedHint(labels.accountMail, labels.turnBackOn)}\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":[]}
|
|
@@ -74,7 +74,8 @@ export interface PushDevice {
|
|
|
74
74
|
* the permission was not granted or push is off on the server; rejects with a
|
|
75
75
|
* {@link PushDeviceError} or the app's request's error. */
|
|
76
76
|
enable: () => Promise<boolean>;
|
|
77
|
-
/** Tell the server first, then the browser, so a dropped device stops at once.
|
|
77
|
+
/** Tell the server first, then the browser, so a dropped device stops at once. At a
|
|
78
|
+
* sign-out, with no card mounted: {@link releaseThisDevicePush}. */
|
|
78
79
|
disable: () => Promise<void>;
|
|
79
80
|
sendTest: () => Promise<{
|
|
80
81
|
sent: number;
|
|
@@ -85,6 +86,43 @@ export declare function notifyPushSubscriptionChanged(): void;
|
|
|
85
86
|
/** Whether this browser has the APIs push needs. iOS exposes them only to an INSTALLED
|
|
86
87
|
* app (Add to Home Screen). */
|
|
87
88
|
export declare function pushSupported(): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Release this browser's push at an EXPLICIT sign-out (0.52, docs/notifications-
|
|
91
|
+
* harmonization.md §10.6; refactor audit 2026-10-30, FLW-2) — without the settings card
|
|
92
|
+
* mounted, which `usePushDevice().disable` needs.
|
|
93
|
+
*
|
|
94
|
+
* A sign-out left the subscription registered to the account, so the device went on
|
|
95
|
+
* showing that account's notifications, title and body (keksdose: invoices, bookings,
|
|
96
|
+
* shares), to whoever used it next — while the same sign-out wiped the mirror, the outbox
|
|
97
|
+
* and the query cache "for the next person on this device".
|
|
98
|
+
*
|
|
99
|
+
* Call it with the app's `unregister` (`POST /notifications/devices/unregister
|
|
100
|
+
* {endpoint}`) BEFORE the session is cleared, while the token is still valid:
|
|
101
|
+
*
|
|
102
|
+
* async function logOut() {
|
|
103
|
+
* await releaseThisDevicePush(api.unregisterDevice).catch(() => undefined);
|
|
104
|
+
* clear();
|
|
105
|
+
* }
|
|
106
|
+
*
|
|
107
|
+
* The server first, then the browser — and the browser EVEN WHEN THE SERVER REFUSED (0.52
|
|
108
|
+
* code review, this file): `disable`'s order left a sign-out on a flaky network, or one
|
|
109
|
+
* that met a 5xx, with the device still subscribed. Unsubscribed, the push service answers
|
|
110
|
+
* the server's next send with 410 and the server prunes the row. The browser's unsubscribe
|
|
111
|
+
* is cut at 15 s (`PushDeviceError("push_timeout")`). Then it tells every mounted
|
|
112
|
+
* {@link usePushDevice} ({@link PUSH_SUBSCRIPTION_CHANGED}).
|
|
113
|
+
*
|
|
114
|
+
* Resolves whether this browser held a subscription; `false` at once where push is
|
|
115
|
+
* unsupported or was never set up — no service worker registration, or none ever active
|
|
116
|
+
* (0.52 code review: most devices, which used to reject with `no_service_worker`, after
|
|
117
|
+
* 10 s for a worker stuck installing). It asks the registration, not `ready`: a
|
|
118
|
+
* subscription belongs to the registration, and a sign-out waits for no worker. Rejects
|
|
119
|
+
* as the app's request did — once the browser has let go — or with `push_timeout`. Best
|
|
120
|
+
* effort at a sign-out: catch it, a sign-out must not stop on push.
|
|
121
|
+
*
|
|
122
|
+
* Only an explicit sign-out (Marcel's decision 2 of the 2026-10-30 audit): a session that
|
|
123
|
+
* merely expired keeps its device, and whoever signs back in turns push on again.
|
|
124
|
+
*/
|
|
125
|
+
export declare function releaseThisDevicePush(unregister: (endpoint: string) => Promise<unknown>): Promise<boolean>;
|
|
88
126
|
/**
|
|
89
127
|
* This browser's push for the signed-in account. Hand it one function per endpoint
|
|
90
128
|
* (§3.8); {@link PushDeviceSetting} draws it, and an app's own card may use it too.
|