@eifi1/ui-kit 0.51.7 → 0.53.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 +178 -67
- 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 +1 -3
- 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.js +2 -1
- 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-subscription.d.ts +10 -7
- package/dist/billing/cancel-subscription.js +6 -6
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/plan-card.d.ts +5 -3
- package/dist/billing/plan-card.js +7 -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 +9 -6
- package/dist/billing/plan-switch.js +7 -5
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +4 -3
- package/dist/chart.js +72 -4
- 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 +13 -0
- package/dist/components/amount-input.js +1 -0
- 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/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 +111 -2
- package/dist/components/combobox-core.js +148 -42
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +141 -130
- 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 +12 -7
- package/dist/components/data-table-mobile.js +43 -30
- 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 +13 -4
- package/dist/components/data-table.js +139 -36
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +25 -3
- package/dist/components/date-field.js +33 -8
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +16 -3
- 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 +38 -24
- package/dist/components/date-range-picker.js +23 -7
- package/dist/components/date-range-picker.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/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 +148 -140
- 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 +3 -2
- 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.js +3 -1
- package/dist/components/modal.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-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/nav-pills.d.ts +1 -1
- package/dist/components/nav-pills.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/row-actions.js +2 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +4 -4
- 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 +92 -23
- package/dist/components/series-chart.js +74 -30
- 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/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 +1 -1
- 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 +31 -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 +167 -19
- package/dist/components/treemap.js +340 -100
- 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 +1 -2
- package/dist/data-table.js +1 -4
- package/dist/data-table.js.map +1 -1
- package/dist/demo/demo-countdown.d.ts +0 -5
- package/dist/demo/demo-countdown.js +0 -4
- package/dist/demo/demo-countdown.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 +18 -0
- package/dist/feedback/feedback-mutation.js +8 -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 +8 -4
- package/dist/feedback/feedback-public-list.js +2 -2
- 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 +0 -3
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +33 -44
- package/dist/feedback/feedback-row-detail.js +28 -34
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +17 -14
- 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 +15 -23
- package/dist/feedback/feedback-thread.js +2 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +6 -6
- package/dist/feedback.js +90 -6
- package/dist/feedback.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.js +19 -3
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +4 -9
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +7 -18
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -11
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +15 -64
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +7 -18
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -27
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +9 -18
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -19
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +8 -18
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +12 -2
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +0 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +21 -23
- package/dist/index.js +50 -65
- 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/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/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.js +5 -14
- 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/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 +29 -0
- package/dist/lib/text-match.js +20 -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/search-index.d.ts +0 -6
- package/dist/search/search-index.js +0 -4
- package/dist/search/search-index.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 +1 -1
- package/dist/search.js +6 -1
- 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 +5 -3
- package/dist/shell/account-menu-language.js +2 -2
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +9 -6
- package/dist/shell/account-menu-text-size.js +2 -2
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/app-shell.d.ts +0 -10
- package/dist/shell/app-shell.js +1 -7
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/company-switcher.d.ts +1 -1
- package/dist/shell/company-switcher.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +1 -1
- package/dist/shell/dev-menu.js +5 -18
- package/dist/shell/dev-menu.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 +1 -3
- 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 +7 -10
- package/dist/shell.js +14 -23
- 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 +2 -2
- package/dist/sync.js +2 -10
- 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/chart-palette.d.ts +0 -5
- package/dist/theme/chart-palette.js +0 -2
- package/dist/theme/chart-palette.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 +13 -3
- package/dist/tour/tour.js +59 -781
- 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/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 +10 -6
- 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 +1 -5
- 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 +4 -1
- 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-subscription.tsx +15 -12
- package/src/billing/plan-card.tsx +18 -10
- package/src/billing/plan-change-labels.ts +66 -0
- package/src/billing/plan-switch.tsx +15 -12
- package/src/chart.ts +73 -6
- package/src/components/account-chips.tsx +20 -23
- package/src/components/alert-banner.tsx +82 -5
- package/src/components/amount-input.tsx +18 -7
- 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/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 +293 -62
- package/src/components/combobox.tsx +183 -153
- 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 +61 -26
- 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 +239 -68
- package/src/components/date-field.tsx +66 -9
- package/src/components/date-picker.tsx +125 -49
- package/src/components/date-range-picker.tsx +71 -36
- 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/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 +197 -153
- 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 +10 -4
- package/src/components/measured-grid.tsx +8 -4
- package/src/components/mini-calendar.tsx +6 -5
- package/src/components/modal.tsx +6 -1
- package/src/components/month-grid.tsx +229 -0
- package/src/components/month-picker.tsx +192 -200
- package/src/components/multi-select.tsx +21 -9
- package/src/components/nav-pills.tsx +1 -1
- 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/row-actions.tsx +2 -1
- package/src/components/series-chart-axes.ts +9 -4
- package/src/components/series-chart-labels.ts +7 -4
- package/src/components/series-chart.tsx +253 -87
- package/src/components/share-card.tsx +6 -6
- package/src/components/tabs-labels.ts +13 -0
- package/src/components/tabs.tsx +2 -2
- 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 +47 -0
- package/src/components/treemap-layout.ts +210 -0
- package/src/components/treemap.tsx +780 -138
- package/src/components/use-table-state.ts +12 -9
- package/src/data-table.ts +1 -4
- package/src/demo/demo-countdown.ts +3 -8
- package/src/eslint/role-utilities.ts +97 -19
- package/src/feedback/feedback-dialog.tsx +30 -19
- package/src/feedback/feedback-mutation.ts +20 -0
- package/src/feedback/feedback-page-link.ts +90 -0
- package/src/feedback/feedback-public-list.tsx +13 -7
- package/src/feedback/feedback-replies.tsx +13 -6
- package/src/feedback/feedback-row-detail-labels.ts +0 -3
- package/src/feedback/feedback-row-detail.tsx +70 -84
- package/src/feedback/feedback-status-undo.ts +28 -28
- 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 +18 -26
- package/src/feedback.ts +89 -6
- 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 +32 -3
- package/src/i18n/defaults.ts +4 -9
- package/src/i18n/german.ts +8 -19
- package/src/i18n/kit-labels.tsx +9 -11
- package/src/i18n/locales/es.ts +8 -19
- package/src/i18n/locales/fr.ts +21 -30
- package/src/i18n/locales/hu.ts +10 -19
- package/src/i18n/locales/it.ts +8 -20
- package/src/i18n/locales/zh.ts +9 -19
- package/src/i18n/review.ts +12 -2
- package/src/i18n/shared-labels.ts +0 -1
- package/src/index.ts +57 -49
- package/src/landing/routing.tsx +14 -10
- package/src/lib/broadcast.ts +52 -0
- package/src/lib/date-format.ts +89 -13
- package/src/lib/day-format.ts +96 -0
- 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 +5 -17
- package/src/lib/http-status.ts +38 -0
- package/src/lib/intl-cache.ts +25 -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 +57 -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/search-index.ts +0 -15
- package/src/search/use-command-key.ts +13 -12
- package/src/search.ts +10 -1
- package/src/settings/use-settings-focus.ts +3 -0
- package/src/shell/account-menu-language.tsx +6 -4
- package/src/shell/account-menu-text-size.tsx +10 -7
- package/src/shell/app-shell.tsx +0 -13
- package/src/shell/company-switcher.tsx +1 -1
- package/src/shell/dev-menu.tsx +8 -22
- package/src/shell/top-bar-actions.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +1 -4
- package/src/shell.ts +21 -17
- package/src/sync/engine.ts +339 -61
- package/src/sync.ts +2 -8
- package/src/testing/css-variables.ts +8 -9
- package/src/theme/chart-palette.ts +0 -2
- 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 +108 -1202
- package/src/tour/use-tour-progress.ts +1 -1
- 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 +6 -5
- package/dist/components/field-sync-labels.d.ts +0 -2
- package/dist/components/field-sync-labels.js +0 -12
- package/dist/components/field-sync-labels.js.map +0 -1
- package/dist/components/field-sync.d.ts +0 -164
- package/dist/components/field-sync.js +0 -269
- package/dist/components/field-sync.js.map +0 -1
- package/dist/components/section-nav.d.ts +0 -37
- package/dist/components/section-nav.js +0 -33
- package/dist/components/section-nav.js.map +0 -1
- package/dist/components/sign-chip-labels.d.ts +0 -2
- package/dist/components/sign-chip-labels.js +0 -11
- package/dist/components/sign-chip-labels.js.map +0 -1
- package/dist/components/sign-chip.d.ts +0 -68
- package/dist/components/sign-chip.js +0 -47
- package/dist/components/sign-chip.js.map +0 -1
- package/dist/components/ui-labels.js.map +0 -1
- package/dist/lib/isin.d.ts +0 -28
- package/dist/lib/isin.js +0 -36
- package/dist/lib/isin.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/dist/shell/role-switcher.d.ts +0 -21
- package/dist/shell/role-switcher.js +0 -31
- package/dist/shell/role-switcher.js.map +0 -1
- package/src/components/field-sync-labels.ts +0 -13
- package/src/components/field-sync.tsx +0 -482
- package/src/components/section-nav.tsx +0 -65
- package/src/components/sign-chip-labels.ts +0 -12
- package/src/components/sign-chip.tsx +0 -124
- package/src/components/ui-labels.ts +0 -24
- package/src/lib/isin.ts +0 -62
- package/src/shell/dev-switchers.tsx +0 -18
- package/src/shell/role-switcher.tsx +0 -46
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/admin-action-confirm.tsx"],"sourcesContent":["import { Children, useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Checkbox } from \"../components/checkbox\";\nimport { DEFAULT_DANGER_CONFIRM_LABELS, TypedConfirmField, typedMatches } from \"../components/danger-confirm\";\nimport type { DangerConsequence } from \"../components/danger-confirm\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { ModalCloseContext } from \"../components/modal\";\nimport { Button } from \"../components/button\";\nimport { authErrorCode } from \"../auth/auth-errors\";\nimport type { KitErrorCode } from \"../auth/auth-errors\";\nimport { usePersonLabel } from \"./admin-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport type { AdminPerson, MaybePromise } from \"./admin-parts\";\nimport { isGroupTarget } from \"./admin-action-log\";\nimport type { AdminGroupTarget } from \"./admin-action-log\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_ADMIN_ACTION_LABELS } from \"./admin-action-confirm-labels\";\n\n/**\n * The confirmation in front of an admin action, at the level the SERVER states\n * (docs/user-admin-harmonization.md §4.2):\n *\n * | level | what it asks | actions |\n * |---|---|---|\n * | `none` | nothing: it confirms at once and renders nothing | resend a mail, reactivate, the reviewer scope… |\n * | `acknowledge` | a tick under the consequence | force a password change, change a role, delete a group |\n * | `type_email` | the target's address, typed | deactivate, erase now, transfer |\n *\n * A GROUP (round 0.34, §11.8) is a target at `none` and `acknowledge` only, said by its\n * name: it has no address to type, and no level table gives a group's action\n * `type_email` (server-kit's `require_confirmation` raises for one). The props' types\n * say so. Deleting one (`group_delete`) is `acknowledge` — a tick under the app's own\n * sentence of what goes with it, Kurvenschmiede's \"3 members, 2 grants and 1 invitation\n * go with it\" (decision 5): the cascade is what nobody can see afterwards.\n *\n * keksdose decides the level per action and per account on the server\n * (`admin_user_actions_service`) and Kurvenschmiede asks for the typed address on its\n * two hard-to-undo actions; both drew the dialog by hand beside the kit's `DangerConfirm`\n * and `useConfirm`. The level is a PROP, never derived here: the server enforces it and\n * a hand-written request never sees this dialog, so a page that guessed its own level\n * could only disagree with the server. The comparison (caseless), the field and the tick\n * are `DangerConfirm`'s (`TypedConfirmField`, the `dangerConfirm` words), so the three\n * confirmations in the kit cannot disagree about what counts as the address.\n *\n * WHY TYPE THE TARGET'S ADDRESS and not the admin's password: the admin proved who they\n * are at the admin gate. What is unproven is which ROW they are on (keksdose's\n * `TYPE_EMAIL` rule, Kurvenschmiede's `TypedConfirm`).\n *\n * THE REFUSALS (§4.1, §9.6) are read by code — `authErrorCode`, which knows the account\n * codes beside the sign-in ones — and stay in the dialog, which stays open so the admin\n * can read them: `last_admin`, `self_action`, `confirmation_mismatch`, and kastlan's\n * `other_companies` — with \"Remove from company\" offered in its place when the app passes\n * `onRemoveFromCompany`. Anything else is `describeError`'s words, else \"That didn't work\".\n * At level `none` nothing is drawn while the action runs; a refusal opens a dialog of its\n * own to say it.\n *\n * It never sends a request: `onConfirm` does, with what the guards were answered with.\n */\n\n/** How much confirmation an action needs — the server's `confirmation_level(action)`. */\nexport type AdminConfirmLevel = \"none\" | \"acknowledge\" | \"type_email\";\n\n/** What {@link AdminActionConfirmProps.onConfirm} receives — the guards' answers, for\n * the request body (the server re-checks both). */\nexport interface AdminActionConfirmValues {\n /** `type_email`: the address as typed — trimmed, its case the admin's; the body's\n * `confirm_email`, which the server compares with the account's (server-kit's\n * `confirm_email_matches`). */\n confirmEmail?: string;\n /** `acknowledge` and `type_email`: the action was confirmed past a guard — the body's\n * `acknowledged`, which every admin action carries (server-kit 0.4). */\n acknowledged?: true;\n}\n\n/** The account the action is about. A group is an {@link AdminGroupTarget}. */\nexport type AdminActionTarget = AdminPerson & { email: string };\n\n/** The `adminAction` namespace. The tick, the typed field and their held-button reasons\n * are the `dangerConfirm` namespace's, shared with `DangerConfirm`. */\nexport interface AdminActionLabels {\n /** The confirm button when the action names none. */\n confirm: string;\n cancel: string;\n /** The refusal dialog's only button (level `none`). */\n close: string;\n /** `409 last_admin`. */\n lastAdmin: string;\n /** `409 self_action`: the admin's own account. */\n self: string;\n /** `confirmation_mismatch`: the typed address is not this account's — the page and\n * the server disagree about which row this is. */\n confirmationMismatch: string;\n /** kastlan's `409 other_companies` (§9.6). */\n otherCompanies: string;\n /** The alternative offered with it. */\n removeFromCompany: string;\n /** Any other failure, unless `describeError` says better. */\n failed: string;\n}\n\n// Defined in ./admin-action-confirm-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ADMIN_ACTION_LABELS };\n\n/** What the action is about, and the level the server states for it. A union, so a group\n * can't be given `type_email` (§11.8). */\ntype AdminActionConfirmTarget =\n | {\n /** The level the server states for this action on this account (§4.2). */\n level: AdminConfirmLevel;\n /** Whom it is about: the address (typed at `type_email`) and the name parts. */\n target: AdminActionTarget;\n }\n | {\n /** A group's action: `none`, or `acknowledge` for `group_delete` (decision 5). */\n level: Exclude<AdminConfirmLevel, \"type_email\">;\n /** The group it is about, said by its name (0.34, §11.8). */\n target: AdminGroupTarget;\n };\n\n/** The props whatever the target. */\ninterface AdminActionConfirmCommonProps {\n /** The confirm button — \"Deactivate\". Also the title when no `title` is given. */\n confirmLabel?: ReactNode;\n /** The dialog's heading — \"Deactivate Ada Example?\". Default `confirmLabel`. */\n title?: ReactNode;\n /**\n * What the action does to THIS account, said before it happens: a sentence, or lines\n * (`DangerConsequence`, a `severe` one in the danger colour). Derive them from the\n * row's facts rather than reciting the worst case (keksdose dev#488).\n */\n consequence?: ReactNode | readonly (string | DangerConsequence)[];\n /** The tick's words at `acknowledge`. Default `dangerConfirm.acknowledge` — \"I have\n * read what this does and want to continue.\" */\n acknowledgeLabel?: ReactNode;\n /** `danger` (default at `type_email`): a red confirm. `warning` (default at\n * `acknowledge`) and `neutral`: the primary confirm. */\n tone?: \"danger\" | \"warning\" | \"neutral\";\n /**\n * Runs the action. Return a promise and the dialog manages itself: busy until it\n * settles, closed (`onClose(true)`) when it resolves, open with the refusal when it\n * rejects. Return nothing and the app owns the rest — `busy`, `error`, closing it.\n */\n onConfirm: (values: AdminActionConfirmValues) => MaybePromise;\n /** The dialog has closed: `true` after the action (or the alternative) went through,\n * `false` for Cancel, Escape, the backdrop or Back. */\n onClose: (done: boolean) => void;\n /** kastlan: offered with an `other_companies` refusal — `DELETE …/membership`,\n * logged as `membership_remove` (§4.1). Promise handling as `onConfirm`. */\n onRemoveFromCompany?: () => MaybePromise;\n /** The app's words for a failure the kit has no code for. Asked first. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** A refusal the app caught itself, when `onConfirm` returns nothing. */\n error?: unknown;\n /** The action is running, for an app that tracks it itself. */\n busy?: boolean;\n /**\n * Fields of the action's own, above the guard — the transfer's recipient, kastlan's\n * roles (`RolesEditor`). Their values are the app's; hold the confirm with\n * `confirmDisabledReason` until they are complete.\n */\n children?: ReactNode;\n /** Why the confirm is held for a reason of the app's (\"Choose who receives it\").\n * Named before the guard's own, since the fields are above it. */\n confirmDisabledReason?: ReactNode;\n /** Kept-mounted mode, as on `DialogFrame`; left out, open while mounted. */\n open?: boolean;\n /** The confirm COMMITS: under a locked `WriteLockProvider` it is held with the lock's\n * reason. Off by default — an admin action is not a write to the open record. */\n commit?: CommitScope;\n labels?: LabelOverride<AdminActionLabels>;\n}\n\n/** A type since 0.34 (an interface before): a person at any level, or a group at `none`\n * and `acknowledge`. */\nexport type AdminActionConfirmProps = AdminActionConfirmCommonProps & AdminActionConfirmTarget;\n\n/** Whether a callback answered a promise. */\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return typeof value === \"object\" && value !== null && typeof (value as PromiseLike<unknown>).then === \"function\";\n}\n\n/** The refusal's words and code. */\nfunction refusalOf(\n error: unknown,\n present: boolean,\n describeError: AdminActionConfirmProps[\"describeError\"],\n labels: AdminActionLabels,\n): { text: ReactNode; code: KitErrorCode | undefined } | null {\n if (!present) return null;\n const code = authErrorCode(error);\n const own = describeError?.(error);\n if (hasContent(own)) return { text: own, code };\n const text =\n code === \"last_admin\"\n ? labels.lastAdmin\n : code === \"self_action\"\n ? labels.self\n : code === \"confirmation_mismatch\"\n ? labels.confirmationMismatch\n : code === \"other_companies\"\n ? labels.otherCompanies\n : labels.failed;\n return { text, code };\n}\n\n/** The selector for the first field among the caller's `children` — what opening focuses. */\nconst FIELD =\n 'input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [role=\"combobox\"]';\n\nexport function AdminActionConfirm({\n level,\n target,\n confirmLabel,\n title,\n consequence,\n acknowledgeLabel,\n tone: toneProp,\n onConfirm,\n onClose,\n onRemoveFromCompany,\n describeError,\n error: errorProp,\n busy: busyProp,\n children,\n confirmDisabledReason,\n open,\n commit,\n labels: labelsProp,\n}: AdminActionConfirmProps) {\n const labels = useKitLabels(\"adminAction\", DEFAULT_ADMIN_ACTION_LABELS, labelsProp);\n const guardLabels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS);\n const nameOf = usePersonLabel();\n const isOpen = open ?? true;\n const [typed, setTyped] = useState(\"\");\n const [ticked, setTicked] = useState(false);\n const [pending, setPending] = useState(false);\n // The last rejection, boxed so a rejection with `undefined` still counts as one.\n const [failure, setFailure] = useState<{ error: unknown } | null>(null);\n // Resolved: the actions row lowers the panel, and `onClose` answers true.\n const [done, setDone] = useState(false);\n const confirmed = useRef(false);\n const mounted = useRef(true);\n const started = useRef(false);\n const formId = useId();\n const slotRef = useRef<HTMLDivElement>(null);\n const guardRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n // A close (kept-mounted) wipes the guards and the refusal: a reopened dialog starts\n // over. During render, like ReauthDialog's, so no frame shows the old answers.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (!isOpen) {\n setTyped(\"\");\n setTicked(false);\n setFailure(null);\n setDone(false);\n }\n }\n\n const busy = Boolean(busyProp) || pending;\n\n /** Runs `call`, managing busy, the refusal and the close — or not, when it answers\n * no promise (the app owns it then). */\n const perform = (call: () => MaybePromise) => {\n let result: MaybePromise;\n try {\n result = call();\n } catch (caught) {\n setFailure({ error: caught });\n return;\n }\n if (!isThenable(result)) return;\n setFailure(null);\n setPending(true);\n result.then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n confirmed.current = true;\n setDone(true);\n },\n (caught: unknown) => {\n if (!mounted.current) return;\n setPending(false);\n setFailure({ error: caught });\n },\n );\n };\n\n // Level `none`: confirmed the moment it opens, once per opening. Nothing is drawn\n // while it runs; the promise's answer comes back asynchronously.\n useEffect(() => {\n if (level !== \"none\") return;\n if (!isOpen) {\n started.current = false;\n return;\n }\n if (started.current) return;\n started.current = true;\n confirmed.current = false;\n let result: unknown;\n try {\n result = onConfirm({});\n } catch (caught) {\n result = Promise.reject(caught);\n }\n // No promise: the app owns the rest (it closes the part, or passes `error`).\n if (!isThenable(result)) return;\n result.then(\n () => {\n if (mounted.current) onClose(true);\n },\n (caught: unknown) => {\n if (mounted.current) setFailure({ error: caught });\n },\n );\n }, [level, isOpen, onConfirm, onClose]);\n\n // Focus the first thing the dialog waits for, after Modal's own effect has recorded\n // the trigger to return to (a parent's effects run after its children's).\n useEffect(() => {\n if (!isOpen || level === \"none\") return;\n const field = slotRef.current?.querySelector<HTMLElement>(FIELD) ?? guardRef.current;\n field?.focus();\n }, [isOpen, level]);\n\n const hasRefusal = failure !== null || errorProp !== undefined;\n const refusal = refusalOf(failure ? failure.error : errorProp, hasRefusal, describeError, labels);\n const offerRemoval = refusal?.code === \"other_companies\" && onRemoveFromCompany !== undefined;\n\n // A group has no address: it is said by its name, and nothing is typed for it — not\n // even when a caller outside the types passes `type_email` (the server refuses that).\n const group = isGroupTarget(target);\n const email = group ? \"\" : target.email;\n const name = group ? (target.name ?? \"\").trim() : nameOf(target);\n const said = !name || name === email ? email : email ? `${name} · ${email}` : name;\n const identity = <span data-private>{said}</span>;\n const heading = title ?? confirmLabel ?? labels.confirm;\n\n const refusalBanner = refusal && (\n <AlertBanner\n tone=\"danger\"\n size=\"sm\"\n action={\n offerRemoval ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n disabled={busy}\n onClick={() => perform(() => onRemoveFromCompany!())}\n >\n {labels.removeFromCompany}\n </Button>\n ) : undefined\n }\n >\n {refusal.text}\n </AlertBanner>\n );\n\n if (level === \"none\") {\n // Nothing to ask: only a refusal is worth a dialog.\n if (!isOpen || !refusal) return null;\n return (\n <DialogFrame\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n // The removal offered with the refusal is a request too.\n busy={busy}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"secondary\" disabled={busy} onClick={close}>\n {labels.close}\n </Button>\n </CloseOnDone>\n )}\n >\n {refusalBanner}\n </DialogFrame>\n );\n }\n\n const tone = toneProp ?? (level === \"type_email\" ? \"danger\" : \"warning\");\n const askTyped = level === \"type_email\" && !group;\n const typedOk = !askTyped || typedMatches(typed, email, \"caseless\");\n const tickOk = askTyped || ticked;\n const guardReason: ReactNode = hasContent(confirmDisabledReason)\n ? confirmDisabledReason\n : !tickOk\n ? guardLabels.needsAcknowledge\n : !typedOk\n ? typeof guardLabels.needsPhrase === \"function\"\n ? guardLabels.needsPhrase(email)\n : guardLabels.needsPhrase\n : undefined;\n\n const submit = () => {\n if (busy || guardReason !== undefined) return;\n perform(() => onConfirm(askTyped ? { confirmEmail: typed.trim(), acknowledged: true } : { acknowledged: true }));\n };\n\n const lines = Array.isArray(consequence)\n ? (consequence as readonly (string | DangerConsequence)[]).map((item, index) =>\n typeof item === \"string\"\n ? { key: item, text: item as ReactNode, severe: false }\n : { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) },\n )\n : null;\n\n return (\n <DialogFrame\n open={open}\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n // A request that has left cannot be called back by closing the dialog.\n busy={busy}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={close}>\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n form={formId}\n variant={tone === \"danger\" ? \"danger\" : \"default\"}\n pending={busy}\n commit={commit}\n disabledReason={busy ? undefined : guardReason}\n >\n {confirmLabel ?? labels.confirm}\n </Button>\n </CloseOnDone>\n )}\n >\n <form\n id={formId}\n noValidate\n className=\"space-y-3\"\n onSubmit={(event) => {\n event.preventDefault();\n submit();\n }}\n >\n {lines ? (\n <ul className=\"list-disc space-y-1 ps-5 text-sm text-secondary\">\n {lines.map((line) => (\n <li key={line.key} className={line.severe ? \"text-danger\" : undefined}>\n {line.text}\n </li>\n ))}\n </ul>\n ) : (\n hasContent(consequence as ReactNode) && <p className=\"text-sm text-secondary\">{consequence as ReactNode}</p>\n )}\n {Children.toArray(children).some(hasContent) && (\n <div ref={slotRef} className=\"space-y-3\">\n {children}\n </div>\n )}\n {askTyped ? (\n <TypedConfirmField\n ref={guardRef}\n target={email}\n match=\"caseless\"\n value={typed}\n onValueChange={setTyped}\n busy={busy}\n data-private\n />\n ) : (\n <Checkbox\n ref={guardRef}\n label={acknowledgeLabel ?? guardLabels.acknowledge}\n checked={ticked}\n // Not `disabled` while busy: the box may hold the focus.\n onCheckedChange={(next) => {\n if (!busy) setTicked(next);\n }}\n />\n )}\n {refusalBanner}\n </form>\n </DialogFrame>\n );\n}\n\n/**\n * The actions row's wrapper: it renders INSIDE the Modal, where the panel's animated\n * close is in context, so a resolved action lowers the panel the way Cancel does and\n * Modal then calls `onClose` (ReauthDialog's pattern).\n */\nfunction CloseOnDone({ done, children }: { done: boolean; children: ReactNode }) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return <>{children}</>;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/admin-action-confirm.tsx"],"sourcesContent":["import { Children, useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Checkbox } from \"../components/checkbox\";\nimport { DEFAULT_DANGER_CONFIRM_LABELS, TypedConfirmField, typedMatches } from \"../components/danger-confirm\";\nimport type { DangerConsequence } from \"../components/danger-confirm\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { ModalCloseContext } from \"../components/modal\";\nimport { Button } from \"../components/button\";\nimport { authErrorCode } from \"../auth/auth-errors\";\nimport type { KitErrorCode } from \"../auth/auth-errors\";\nimport { usePersonLabel } from \"./admin-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport { isThenable } from \"../lib/thenable\";\nimport type { AdminPerson, MaybePromise } from \"./admin-parts\";\nimport { isGroupTarget } from \"./admin-action-log\";\nimport type { AdminGroupTarget } from \"./admin-action-log\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_ADMIN_ACTION_LABELS } from \"./admin-action-confirm-labels\";\n\n/**\n * The confirmation in front of an admin action, at the level the SERVER states\n * (docs/user-admin-harmonization.md §4.2):\n *\n * | level | what it asks | actions |\n * |---|---|---|\n * | `none` | nothing: it confirms at once and renders nothing | resend a mail, reactivate, the reviewer scope… |\n * | `acknowledge` | a tick under the consequence | force a password change, change a role, delete a group |\n * | `type_email` | the target's address, typed | deactivate, erase now, transfer |\n *\n * A GROUP (round 0.34, §11.8) is a target at `none` and `acknowledge` only, said by its\n * name: it has no address to type, and no level table gives a group's action\n * `type_email` (server-kit's `require_confirmation` raises for one). The props' types\n * say so. Deleting one (`group_delete`) is `acknowledge` — a tick under the app's own\n * sentence of what goes with it, Kurvenschmiede's \"3 members, 2 grants and 1 invitation\n * go with it\" (decision 5): the cascade is what nobody can see afterwards.\n *\n * keksdose decides the level per action and per account on the server\n * (`admin_user_actions_service`) and Kurvenschmiede asks for the typed address on its\n * two hard-to-undo actions; both drew the dialog by hand beside the kit's `DangerConfirm`\n * and `useConfirm`. The level is a PROP, never derived here: the server enforces it and\n * a hand-written request never sees this dialog, so a page that guessed its own level\n * could only disagree with the server. The comparison (caseless), the field and the tick\n * are `DangerConfirm`'s (`TypedConfirmField`, the `dangerConfirm` words), so the three\n * confirmations in the kit cannot disagree about what counts as the address.\n *\n * WHY TYPE THE TARGET'S ADDRESS and not the admin's password: the admin proved who they\n * are at the admin gate. What is unproven is which ROW they are on (keksdose's\n * `TYPE_EMAIL` rule, Kurvenschmiede's `TypedConfirm`).\n *\n * THE REFUSALS (§4.1, §9.6) are read by code — `authErrorCode`, which knows the account\n * codes beside the sign-in ones — and stay in the dialog, which stays open so the admin\n * can read them: `last_admin`, `self_action`, `confirmation_mismatch`, and kastlan's\n * `other_companies` — with \"Remove from company\" offered in its place when the app passes\n * `onRemoveFromCompany`. Anything else is `describeError`'s words, else \"That didn't work\".\n * At level `none` nothing is drawn while the action runs; a refusal opens a dialog of its\n * own to say it.\n *\n * It never sends a request: `onConfirm` does, with what the guards were answered with.\n */\n\n/** How much confirmation an action needs — the server's `confirmation_level(action)`. */\nexport type AdminConfirmLevel = \"none\" | \"acknowledge\" | \"type_email\";\n\n/** What {@link AdminActionConfirmProps.onConfirm} receives — the guards' answers, for\n * the request body (the server re-checks both). */\nexport interface AdminActionConfirmValues {\n /** `type_email`: the address as typed — trimmed, its case the admin's; the body's\n * `confirm_email`, which the server compares with the account's (server-kit's\n * `confirm_email_matches`). */\n confirmEmail?: string;\n /** `acknowledge` and `type_email`: the action was confirmed past a guard — the body's\n * `acknowledged`, which every admin action carries (server-kit 0.4). */\n acknowledged?: true;\n}\n\n/** The account the action is about. A group is an {@link AdminGroupTarget}. */\nexport type AdminActionTarget = AdminPerson & { email: string };\n\n/** The `adminAction` namespace. The tick, the typed field and their held-button reasons\n * are the `dangerConfirm` namespace's, shared with `DangerConfirm`. */\nexport interface AdminActionLabels {\n /** The confirm button when the action names none. */\n confirm: string;\n cancel: string;\n /** The refusal dialog's only button (level `none`). */\n close: string;\n /** `409 last_admin`. */\n lastAdmin: string;\n /** `409 self_action`: the admin's own account. */\n self: string;\n /** `confirmation_mismatch`: the typed address is not this account's — the page and\n * the server disagree about which row this is. */\n confirmationMismatch: string;\n /** kastlan's `409 other_companies` (§9.6). */\n otherCompanies: string;\n /** The alternative offered with it. */\n removeFromCompany: string;\n /** Any other failure, unless `describeError` says better. */\n failed: string;\n}\n\n// Defined in ./admin-action-confirm-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ADMIN_ACTION_LABELS };\n\n/** What the action is about, and the level the server states for it. A union, so a group\n * can't be given `type_email` (§11.8). */\ntype AdminActionConfirmTarget =\n | {\n /** The level the server states for this action on this account (§4.2). */\n level: AdminConfirmLevel;\n /** Whom it is about: the address (typed at `type_email`) and the name parts. */\n target: AdminActionTarget;\n }\n | {\n /** A group's action: `none`, or `acknowledge` for `group_delete` (decision 5). */\n level: Exclude<AdminConfirmLevel, \"type_email\">;\n /** The group it is about, said by its name (0.34, §11.8). */\n target: AdminGroupTarget;\n };\n\n/** The props whatever the target. */\ninterface AdminActionConfirmCommonProps {\n /** The confirm button — \"Deactivate\". Also the title when no `title` is given. */\n confirmLabel?: ReactNode;\n /** The dialog's heading — \"Deactivate Ada Example?\". Default `confirmLabel`. */\n title?: ReactNode;\n /**\n * What the action does to THIS account, said before it happens: a sentence, or lines\n * (`DangerConsequence`, a `severe` one in the danger colour). Derive them from the\n * row's facts rather than reciting the worst case (keksdose dev#488).\n */\n consequence?: ReactNode | readonly (string | DangerConsequence)[];\n /** The tick's words at `acknowledge`. Default `dangerConfirm.acknowledge` — \"I have\n * read what this does and want to continue.\" */\n acknowledgeLabel?: ReactNode;\n /** `danger` (default at `type_email`): a red confirm. `warning` (default at\n * `acknowledge`) and `neutral`: the primary confirm. */\n tone?: \"danger\" | \"warning\" | \"neutral\";\n /**\n * Runs the action. Return a promise and the dialog manages itself: busy until it\n * settles, closed (`onClose(true)`) when it resolves, open with the refusal when it\n * rejects. Return nothing and the app owns the rest — `busy`, `error`, closing it.\n */\n onConfirm: (values: AdminActionConfirmValues) => MaybePromise;\n /** The dialog has closed: `true` after the action (or the alternative) went through,\n * `false` for Cancel, Escape, the backdrop or Back. */\n onClose: (done: boolean) => void;\n /** kastlan: offered with an `other_companies` refusal — `DELETE …/membership`,\n * logged as `membership_remove` (§4.1). Promise handling as `onConfirm`. */\n onRemoveFromCompany?: () => MaybePromise;\n /** The app's words for a failure the kit has no code for. Asked first. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** A refusal the app caught itself, when `onConfirm` returns nothing. */\n error?: unknown;\n /** The action is running, for an app that tracks it itself. */\n busy?: boolean;\n /**\n * Fields of the action's own, above the guard — the transfer's recipient, kastlan's\n * roles (`RolesEditor`). Their values are the app's; hold the confirm with\n * `confirmDisabledReason` until they are complete.\n */\n children?: ReactNode;\n /** Why the confirm is held for a reason of the app's (\"Choose who receives it\").\n * Named before the guard's own, since the fields are above it. */\n confirmDisabledReason?: ReactNode;\n /** Kept-mounted mode, as on `DialogFrame`; left out, open while mounted. */\n open?: boolean;\n /** The confirm COMMITS: under a locked `WriteLockProvider` it is held with the lock's\n * reason. Off by default — an admin action is not a write to the open record. */\n commit?: CommitScope;\n labels?: LabelOverride<AdminActionLabels>;\n}\n\n/** A type since 0.34 (an interface before): a person at any level, or a group at `none`\n * and `acknowledge`. */\nexport type AdminActionConfirmProps = AdminActionConfirmCommonProps & AdminActionConfirmTarget;\n\n/** The refusal's words and code. */\nfunction refusalOf(\n error: unknown,\n present: boolean,\n describeError: AdminActionConfirmProps[\"describeError\"],\n labels: AdminActionLabels,\n): { text: ReactNode; code: KitErrorCode | undefined } | null {\n if (!present) return null;\n const code = authErrorCode(error);\n const own = describeError?.(error);\n if (hasContent(own)) return { text: own, code };\n const text =\n code === \"last_admin\"\n ? labels.lastAdmin\n : code === \"self_action\"\n ? labels.self\n : code === \"confirmation_mismatch\"\n ? labels.confirmationMismatch\n : code === \"other_companies\"\n ? labels.otherCompanies\n : labels.failed;\n return { text, code };\n}\n\n/** The selector for the first field among the caller's `children` — what opening focuses. */\nconst FIELD =\n 'input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [role=\"combobox\"]';\n\nexport function AdminActionConfirm({\n level,\n target,\n confirmLabel,\n title,\n consequence,\n acknowledgeLabel,\n tone: toneProp,\n onConfirm,\n onClose,\n onRemoveFromCompany,\n describeError,\n error: errorProp,\n busy: busyProp,\n children,\n confirmDisabledReason,\n open,\n commit,\n labels: labelsProp,\n}: AdminActionConfirmProps) {\n const labels = useKitLabels(\"adminAction\", DEFAULT_ADMIN_ACTION_LABELS, labelsProp);\n const guardLabels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS);\n const nameOf = usePersonLabel();\n const isOpen = open ?? true;\n const [typed, setTyped] = useState(\"\");\n const [ticked, setTicked] = useState(false);\n const [pending, setPending] = useState(false);\n // The last rejection, boxed so a rejection with `undefined` still counts as one.\n const [failure, setFailure] = useState<{ error: unknown } | null>(null);\n // Resolved: the actions row lowers the panel, and `onClose` answers true.\n const [done, setDone] = useState(false);\n const confirmed = useRef(false);\n const mounted = useRef(true);\n const started = useRef(false);\n const formId = useId();\n const slotRef = useRef<HTMLDivElement>(null);\n const guardRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n // A close (kept-mounted) wipes the guards and the refusal: a reopened dialog starts\n // over. During render, like ReauthDialog's, so no frame shows the old answers.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (!isOpen) {\n setTyped(\"\");\n setTicked(false);\n setFailure(null);\n setDone(false);\n }\n }\n\n const busy = Boolean(busyProp) || pending;\n\n /** Runs `call`, managing busy, the refusal and the close — or not, when it answers\n * no promise (the app owns it then). */\n const perform = (call: () => MaybePromise) => {\n let result: MaybePromise;\n try {\n result = call();\n } catch (caught) {\n setFailure({ error: caught });\n return;\n }\n if (!isThenable(result)) return;\n setFailure(null);\n setPending(true);\n result.then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n confirmed.current = true;\n setDone(true);\n },\n (caught: unknown) => {\n if (!mounted.current) return;\n setPending(false);\n setFailure({ error: caught });\n },\n );\n };\n\n // Level `none`: confirmed the moment it opens, once per opening. Nothing is drawn\n // while it runs; the promise's answer comes back asynchronously.\n useEffect(() => {\n if (level !== \"none\") return;\n if (!isOpen) {\n started.current = false;\n return;\n }\n if (started.current) return;\n started.current = true;\n confirmed.current = false;\n let result: unknown;\n try {\n result = onConfirm({});\n } catch (caught) {\n result = Promise.reject(caught);\n }\n // No promise: the app owns the rest (it closes the part, or passes `error`).\n if (!isThenable(result)) return;\n result.then(\n () => {\n if (mounted.current) onClose(true);\n },\n (caught: unknown) => {\n if (mounted.current) setFailure({ error: caught });\n },\n );\n }, [level, isOpen, onConfirm, onClose]);\n\n // Focus the first thing the dialog waits for, after Modal's own effect has recorded\n // the trigger to return to (a parent's effects run after its children's).\n useEffect(() => {\n if (!isOpen || level === \"none\") return;\n const field = slotRef.current?.querySelector<HTMLElement>(FIELD) ?? guardRef.current;\n field?.focus();\n }, [isOpen, level]);\n\n const hasRefusal = failure !== null || errorProp !== undefined;\n const refusal = refusalOf(failure ? failure.error : errorProp, hasRefusal, describeError, labels);\n const offerRemoval = refusal?.code === \"other_companies\" && onRemoveFromCompany !== undefined;\n\n // A group has no address: it is said by its name, and nothing is typed for it — not\n // even when a caller outside the types passes `type_email` (the server refuses that).\n const group = isGroupTarget(target);\n const email = group ? \"\" : target.email;\n const name = group ? (target.name ?? \"\").trim() : nameOf(target);\n const said = !name || name === email ? email : email ? `${name} · ${email}` : name;\n const identity = <span data-private>{said}</span>;\n const heading = title ?? confirmLabel ?? labels.confirm;\n\n const refusalBanner = refusal && (\n <AlertBanner\n tone=\"danger\"\n size=\"sm\"\n action={\n offerRemoval ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n disabled={busy}\n onClick={() => perform(() => onRemoveFromCompany!())}\n >\n {labels.removeFromCompany}\n </Button>\n ) : undefined\n }\n >\n {refusal.text}\n </AlertBanner>\n );\n\n if (level === \"none\") {\n // Nothing to ask: only a refusal is worth a dialog.\n if (!isOpen || !refusal) return null;\n return (\n <DialogFrame\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n // The removal offered with the refusal is a request too.\n busy={busy}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"secondary\" disabled={busy} onClick={close}>\n {labels.close}\n </Button>\n </CloseOnDone>\n )}\n >\n {refusalBanner}\n </DialogFrame>\n );\n }\n\n const tone = toneProp ?? (level === \"type_email\" ? \"danger\" : \"warning\");\n const askTyped = level === \"type_email\" && !group;\n const typedOk = !askTyped || typedMatches(typed, email, \"caseless\");\n const tickOk = askTyped || ticked;\n const guardReason: ReactNode = hasContent(confirmDisabledReason)\n ? confirmDisabledReason\n : !tickOk\n ? guardLabels.needsAcknowledge\n : !typedOk\n ? typeof guardLabels.needsPhrase === \"function\"\n ? guardLabels.needsPhrase(email)\n : guardLabels.needsPhrase\n : undefined;\n\n const submit = () => {\n if (busy || guardReason !== undefined) return;\n perform(() => onConfirm(askTyped ? { confirmEmail: typed.trim(), acknowledged: true } : { acknowledged: true }));\n };\n\n const lines = Array.isArray(consequence)\n ? (consequence as readonly (string | DangerConsequence)[]).map((item, index) =>\n typeof item === \"string\"\n ? { key: item, text: item as ReactNode, severe: false }\n : { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) },\n )\n : null;\n\n return (\n <DialogFrame\n open={open}\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n // A request that has left cannot be called back by closing the dialog.\n busy={busy}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={close}>\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n form={formId}\n variant={tone === \"danger\" ? \"danger\" : \"default\"}\n pending={busy}\n commit={commit}\n disabledReason={busy ? undefined : guardReason}\n >\n {confirmLabel ?? labels.confirm}\n </Button>\n </CloseOnDone>\n )}\n >\n <form\n id={formId}\n noValidate\n className=\"space-y-3\"\n onSubmit={(event) => {\n event.preventDefault();\n submit();\n }}\n >\n {lines ? (\n <ul className=\"list-disc space-y-1 ps-5 text-sm text-secondary\">\n {lines.map((line) => (\n <li key={line.key} className={line.severe ? \"text-danger\" : undefined}>\n {line.text}\n </li>\n ))}\n </ul>\n ) : (\n hasContent(consequence as ReactNode) && <p className=\"text-sm text-secondary\">{consequence as ReactNode}</p>\n )}\n {Children.toArray(children).some(hasContent) && (\n <div ref={slotRef} className=\"space-y-3\">\n {children}\n </div>\n )}\n {askTyped ? (\n <TypedConfirmField\n ref={guardRef}\n target={email}\n match=\"caseless\"\n value={typed}\n onValueChange={setTyped}\n busy={busy}\n data-private\n />\n ) : (\n <Checkbox\n ref={guardRef}\n label={acknowledgeLabel ?? guardLabels.acknowledge}\n checked={ticked}\n // Not `disabled` while busy: the box may hold the focus.\n onCheckedChange={(next) => {\n if (!busy) setTicked(next);\n }}\n />\n )}\n {refusalBanner}\n </form>\n </DialogFrame>\n );\n}\n\n/**\n * The actions row's wrapper: it renders INSIDE the Modal, where the panel's animated\n * close is in context, so a resolved action lowers the panel the way Cancel does and\n * Modal then calls `onClose` (ReauthDialog's pattern).\n */\nfunction CloseOnDone({ done, children }: { done: boolean; children: ReactNode }) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return <>{children}</>;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -33,8 +33,11 @@ export declare function usePersonLabel(localeProp?: string): (person: AdminPerso
|
|
|
33
33
|
/**
|
|
34
34
|
* A day as words in a sentence ("6 Nov 2026"): the provider's `formatDate` when the app
|
|
35
35
|
* set one (keksdose K12), asked for no weekday — a date in a sentence carries none — else
|
|
36
|
-
* `Intl`'s medium date
|
|
36
|
+
* `Intl`'s medium date, in the provider's locale and `timeZone` (0.52: the kit's one day
|
|
37
|
+
* ladder, `useDayFormatter` — refactor audit 2026-10-30, DUP-2). `""` for no date or one
|
|
38
|
+
* that does not parse: the caller leaves the sentence out.
|
|
37
39
|
*/
|
|
38
40
|
export declare function useDayText(localeProp?: string): (value: DateInput) => string;
|
|
39
|
-
/** Call a callback that may throw or answer a promise, as a promise
|
|
40
|
-
|
|
41
|
+
/** Call a callback that may throw or answer a promise, as a promise — the kit's one
|
|
42
|
+
* (hooks/use-mounted), not a copy of it (0.52 code review, this file). */
|
|
43
|
+
export { settle } from "../hooks/use-mounted.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useCallback } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { useKitLocale } from "../i18n/kit-labels.js";
|
|
4
|
+
import { useDayFormatter } from "../lib/day-format.js";
|
|
5
|
+
import { toDate } from "../lib/format.js";
|
|
6
6
|
import { formatPersonName } from "../lib/person-name.js";
|
|
7
7
|
function personLabel(person, locale) {
|
|
8
8
|
if (!person) return "";
|
|
@@ -13,25 +13,10 @@ function usePersonLabel(localeProp) {
|
|
|
13
13
|
return useCallback((person) => personLabel(person, locale), [locale]);
|
|
14
14
|
}
|
|
15
15
|
function useDayText(localeProp) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
return useCallback(
|
|
19
|
-
(value) => {
|
|
20
|
-
const date = toDate(value);
|
|
21
|
-
if (!date) return "";
|
|
22
|
-
const own = fromProvider ? fromProvider(toLocalIso(date), { unit: "day", source: "dateMark", locale, weekday: false }) : "";
|
|
23
|
-
return own || formatDate(date, "medium", { locale });
|
|
24
|
-
},
|
|
25
|
-
[fromProvider, locale]
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
function settle(call) {
|
|
29
|
-
try {
|
|
30
|
-
return Promise.resolve(call());
|
|
31
|
-
} catch (error) {
|
|
32
|
-
return Promise.reject(error);
|
|
33
|
-
}
|
|
16
|
+
const day = useDayFormatter(void 0, localeProp);
|
|
17
|
+
return useCallback((value) => toDate(value) ? day(value) : "", [day]);
|
|
34
18
|
}
|
|
19
|
+
import { settle } from "../hooks/use-mounted.js";
|
|
35
20
|
export {
|
|
36
21
|
personLabel,
|
|
37
22
|
settle,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/admin-parts.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/admin/admin-parts.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { useDayFormatter } from \"../lib/day-format\";\nimport { toDate } from \"../lib/format\";\nimport type { DateInput } from \"../lib/format\";\nimport { formatPersonName } from \"../lib/person-name\";\n\n/**\n * What the admin parts share and do not export (0.30.0). Internal to `src/admin`.\n *\n * @internal\n */\n\n/** A callback that may answer a promise: resolve = done, reject = stay and say why. */\nexport type MaybePromise<T = unknown> = void | Promise<T>;\n\n/** A person as the admin parts show one: the two name parts of the auth contract\n * (§3.2), an older whole `name` as the fallback, and the address. The name parts are\n * read in either spelling — the kit's `first` / `last`, or server-kit's `PersonRef`\n * as it serialises (`first_name` / `last_name`), so an app hands the server's object\n * over as it came (keksdose's and kastlan's 0.30 adoptions mapped one to the other). */\nexport interface AdminPerson {\n id?: string | number | null;\n first?: string | null;\n last?: string | null;\n /** server-kit `PersonRef.first_name`; `first` wins when both are given. */\n first_name?: string | null;\n /** server-kit `PersonRef.last_name`; `last` wins when both are given. */\n last_name?: string | null;\n /** A whole name, for an API that still sends `display_name` only. */\n name?: string | null;\n email?: string | null;\n}\n\n/**\n * The name to say for a person, in the READER's order (auth §3.2,\n * {@link formatPersonName}), else the whole `name`, else the address, else `\"\"`.\n */\nexport function personLabel(person: AdminPerson | null | undefined, locale: string | undefined): string {\n if (!person) return \"\";\n return (\n formatPersonName({ first: person.first ?? person.first_name, last: person.last ?? person.last_name }, locale) ||\n (person.name ?? \"\").trim() ||\n (person.email ?? \"\").trim()\n );\n}\n\n/** {@link personLabel} in the provider's locale. */\nexport function usePersonLabel(localeProp?: string): (person: AdminPerson | null | undefined) => string {\n const locale = useKitLocale(localeProp);\n return useCallback((person) => personLabel(person, locale), [locale]);\n}\n\n/**\n * A day as words in a sentence (\"6 Nov 2026\"): the provider's `formatDate` when the app\n * set one (keksdose K12), asked for no weekday — a date in a sentence carries none — else\n * `Intl`'s medium date, in the provider's locale and `timeZone` (0.52: the kit's one day\n * ladder, `useDayFormatter` — refactor audit 2026-10-30, DUP-2). `\"\"` for no date or one\n * that does not parse: the caller leaves the sentence out.\n */\nexport function useDayText(localeProp?: string): (value: DateInput) => string {\n const day = useDayFormatter(undefined, localeProp);\n return useCallback((value) => (toDate(value) ? day(value) : \"\"), [day]);\n}\n\n/** Call a callback that may throw or answer a promise, as a promise — the kit's one\n * (hooks/use-mounted), not a copy of it (0.52 code review, this file). */\nexport { settle } from \"../hooks/use-mounted\";\n"],"mappings":";AAAA,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC,SAAS,cAAc;AAEvB,SAAS,wBAAwB;AAiC1B,SAAS,YAAY,QAAwC,QAAoC;AACtG,MAAI,CAAC,OAAQ,QAAO;AACpB,SACE,iBAAiB,EAAE,OAAO,OAAO,SAAS,OAAO,YAAY,MAAM,OAAO,QAAQ,OAAO,UAAU,GAAG,MAAM,MAC3G,OAAO,QAAQ,IAAI,KAAK,MACxB,OAAO,SAAS,IAAI,KAAK;AAE9B;AAGO,SAAS,eAAe,YAAyE;AACtG,QAAM,SAAS,aAAa,UAAU;AACtC,SAAO,YAAY,CAAC,WAAW,YAAY,QAAQ,MAAM,GAAG,CAAC,MAAM,CAAC;AACtE;AASO,SAAS,WAAW,YAAmD;AAC5E,QAAM,MAAM,gBAAgB,QAAW,UAAU;AACjD,SAAO,YAAY,CAAC,UAAW,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,IAAK,CAAC,GAAG,CAAC;AACxE;AAIA,SAAS,cAAc;","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { englishRateLimited } from "../auth/auth-errors.js";
|
|
2
|
+
import { englishRateLimited } from "../auth/auth-errors-labels.js";
|
|
3
3
|
const DEFAULT_MAIL_TEST_CARD_LABELS = {
|
|
4
4
|
title: "Test mail",
|
|
5
5
|
description: "Sends one mail to your own address, through the transport this installation sends all its mail with.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/mail-test-card-labels.ts"],"sourcesContent":["// The English words of ./mail-test-card, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./mail-test-card re-exports them.\n\nimport { englishRateLimited } from \"../auth/auth-errors\";\nimport type { MailTestCardLabels } from \"./mail-test-card\";\n\n/** docs/admin-harmonization.md §9.2.4, word for word. */\nexport const DEFAULT_MAIL_TEST_CARD_LABELS: MailTestCardLabels = {\n title: \"Test mail\",\n description: \"Sends one mail to your own address, through the transport this installation sends all its mail with.\",\n transport: (backend) => `Transport: ${backend}`,\n action: \"Send a test mail to me\",\n sent: (address) => `Sent to ${address}. Check the inbox.`,\n console: \"The console transport only logs mail; nothing was sent.\",\n failed: \"Could not send. The server log has the provider’s answer.\",\n rateLimited: englishRateLimited,\n keywords: \"test mail email e-mail transport smtp resend console send deliver\",\n};\n"],"mappings":";AAIA,SAAS,0BAA0B;AAI5B,MAAM,gCAAoD;AAAA,EAC/D,OAAO;AAAA,EACP,aAAa;AAAA,EACb,WAAW,CAAC,YAAY,cAAc,OAAO;AAAA,EAC7C,QAAQ;AAAA,EACR,MAAM,CAAC,YAAY,WAAW,OAAO;AAAA,EACrC,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,UAAU;AACZ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/mail-test-card-labels.ts"],"sourcesContent":["// The English words of ./mail-test-card, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./mail-test-card re-exports them.\n\nimport { englishRateLimited } from \"../auth/auth-errors-labels\";\nimport type { MailTestCardLabels } from \"./mail-test-card\";\n\n/** docs/admin-harmonization.md §9.2.4, word for word. */\nexport const DEFAULT_MAIL_TEST_CARD_LABELS: MailTestCardLabels = {\n title: \"Test mail\",\n description: \"Sends one mail to your own address, through the transport this installation sends all its mail with.\",\n transport: (backend) => `Transport: ${backend}`,\n action: \"Send a test mail to me\",\n sent: (address) => `Sent to ${address}. Check the inbox.`,\n console: \"The console transport only logs mail; nothing was sent.\",\n failed: \"Could not send. The server log has the provider’s answer.\",\n rateLimited: englishRateLimited,\n keywords: \"test mail email e-mail transport smtp resend console send deliver\",\n};\n"],"mappings":";AAIA,SAAS,0BAA0B;AAI5B,MAAM,gCAAoD;AAAA,EAC/D,OAAO;AAAA,EACP,aAAa;AAAA,EACb,WAAW,CAAC,YAAY,cAAc,OAAO;AAAA,EAC7C,QAAQ;AAAA,EACR,MAAM,CAAC,YAAY,WAAW,OAAO;AAAA,EACrC,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,UAAU;AACZ;","names":[]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
export { DEFAULT_PLAN_CHANGE_LABELS } from "../billing/plan-change-labels.js";
|
|
2
|
+
export type { PlanChangeLabels } from "../billing/plan-change-labels.js";
|
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
column: "Plan",
|
|
4
|
-
title: "Change plan",
|
|
5
|
-
confirm: "Change plan",
|
|
6
|
-
plan: "Plan",
|
|
7
|
-
current: (plan) => `Current plan: ${plan}`,
|
|
8
|
-
keepsItems: "A smaller plan only blocks creating more; nothing is deleted.",
|
|
9
|
-
until: "Free until",
|
|
10
|
-
untilHint: "Leave empty for no end. A running beta keeps its own end.",
|
|
11
|
-
needsChange: "Pick another plan or an end date.",
|
|
12
|
-
changed: (from, to) => `Plan changed from ${from} to ${to}.`,
|
|
13
|
-
set: (to) => `Plan set to ${to}.`,
|
|
14
|
-
keptBeta: "The beta keeps its end; only the plan changed.",
|
|
15
|
-
untilDone: (date) => `Free until ${date}.`,
|
|
16
|
-
overLimit: "Above the new plan\u2019s limit: nothing is removed, new items are blocked.",
|
|
17
|
-
usageOf: (used, limit, dimension) => `${used} of ${limit} ${dimension}`
|
|
18
|
-
};
|
|
2
|
+
import { DEFAULT_PLAN_CHANGE_LABELS } from "../billing/plan-change-labels.js";
|
|
19
3
|
export {
|
|
20
4
|
DEFAULT_PLAN_CHANGE_LABELS
|
|
21
5
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/plan-change-labels.ts"],"sourcesContent":["//
|
|
1
|
+
{"version":3,"sources":["../../src/admin/plan-change-labels.ts"],"sourcesContent":["// Moved to billing/plan-change-labels in 0.52 (refactor audit 2026-10-30, MOD-6): the\n// billing and admin folders imported each other through it. Kept so the old path still\n// answers; nothing in the kit imports it any more but the English catalogue.\nexport { DEFAULT_PLAN_CHANGE_LABELS } from \"../billing/plan-change-labels\";\nexport type { PlanChangeLabels } from \"../billing/plan-change-labels\";\n"],"mappings":";AAGA,SAAS,kCAAkC;","names":[]}
|
|
@@ -5,7 +5,8 @@ import type { DataTableColumn } from "../components/data-table.js";
|
|
|
5
5
|
import type { SubscriptionStatus } from "../billing/billing-labels.js";
|
|
6
6
|
import type { AdminActionConfirmValues } from "./admin-action-confirm.js";
|
|
7
7
|
import type { AdminPerson, MaybePromise } from "./admin-parts.js";
|
|
8
|
-
import { DEFAULT_PLAN_CHANGE_LABELS } from "
|
|
8
|
+
import { DEFAULT_PLAN_CHANGE_LABELS } from "../billing/plan-change-labels.js";
|
|
9
|
+
import type { PlanChangeLabels } from "../billing/plan-change-labels.js";
|
|
9
10
|
/**
|
|
10
11
|
* The operator's plan parts (docs/billing-harmonization.md §14.12, decision 27):
|
|
11
12
|
* {@link planColumn} for the roster or the companies list, {@link PlanChangeConfirm}
|
|
@@ -19,47 +20,7 @@ import { DEFAULT_PLAN_CHANGE_LABELS } from "./plan-change-labels.js";
|
|
|
19
20
|
* codes come from `GET /admin/plans` (kastlan `GET /platform/plans`), which answers
|
|
20
21
|
* whatever the billing switch says.
|
|
21
22
|
*/
|
|
22
|
-
|
|
23
|
-
export interface PlanChangeLabels {
|
|
24
|
-
/** The column's header: "Plan". */
|
|
25
|
-
column: string;
|
|
26
|
-
/** The dialog's title: "Change plan". */
|
|
27
|
-
title: string;
|
|
28
|
-
/** Its confirm button: "Change plan". */
|
|
29
|
-
confirm: string;
|
|
30
|
-
/** The plan select's label: "Plan". */
|
|
31
|
-
plan: string;
|
|
32
|
-
/** The line over the select: "Current plan: Free". */
|
|
33
|
-
current: (plan: string) => string;
|
|
34
|
-
/** Under the select — the fact that is not on the button. */
|
|
35
|
-
keepsItems: string;
|
|
36
|
-
/** The end of a free grant's field: "Free until". */
|
|
37
|
-
until: string;
|
|
38
|
-
/** Under it. */
|
|
39
|
-
untilHint: string;
|
|
40
|
-
/** Why the confirm is held while nothing would change. */
|
|
41
|
-
needsChange: string;
|
|
42
|
-
/** The result: the plan moved from one to another. */
|
|
43
|
-
changed: (from: string, to: string) => string;
|
|
44
|
-
/** The result: an account with no plan before, or the same plan again. */
|
|
45
|
-
set: (to: string) => string;
|
|
46
|
-
/** The result: a running beta kept its own end (§12.34); only the plan moved. */
|
|
47
|
-
keptBeta: string;
|
|
48
|
-
/** The result: the grant runs until `date` (formatted). */
|
|
49
|
-
untilDone: (date: string) => string;
|
|
50
|
-
/** The result: the account owns more than the new plan allows. */
|
|
51
|
-
overLimit: string;
|
|
52
|
-
/**
|
|
53
|
-
* The column's usage, spelled out (its tooltip): "1 of 5 budgets". Figures arrive
|
|
54
|
-
* formatted; `dimension` is the app's word, as `usage.label` gives it — since 0.33.1
|
|
55
|
-
* in the form the LIMIT takes when the label gives its plural forms ("1 of 1 budget",
|
|
56
|
-
* "1 of 5 budgets"): in every shipped language the word follows the limit, or heads
|
|
57
|
-
* the line in German, whose `von` would want a dative plural no app form carries
|
|
58
|
-
* ("Budgets: 1 von 5"). An override of yours gets the same word. Since 0.34 the grant
|
|
59
|
-
* dialog's usage line too, when `usage` comes as the column's shape.
|
|
60
|
-
*/
|
|
61
|
-
usageOf: (used: string, limit: string, dimension: string) => string;
|
|
62
|
-
}
|
|
23
|
+
export type { PlanChangeLabels };
|
|
63
24
|
export { DEFAULT_PLAN_CHANGE_LABELS };
|
|
64
25
|
/** What a row's usage is, for the line under its plan: "1/5" — and since 0.34 what
|
|
65
26
|
* {@link PlanChangeConfirm}'s `usage` takes, for the same sentence in the dialog. */
|
|
@@ -13,7 +13,7 @@ import { useBillingLabels } from "../billing/billing-labels.js";
|
|
|
13
13
|
import { SubscriptionStatusChip } from "../billing/subscription-status-chip.js";
|
|
14
14
|
import { AdminActionConfirm } from "./admin-action-confirm.js";
|
|
15
15
|
import { personLabel, useDayText } from "./admin-parts.js";
|
|
16
|
-
import { DEFAULT_PLAN_CHANGE_LABELS } from "
|
|
16
|
+
import { DEFAULT_PLAN_CHANGE_LABELS } from "../billing/plan-change-labels.js";
|
|
17
17
|
function codeOf(raw) {
|
|
18
18
|
const code = (raw ?? "").trim().toLowerCase();
|
|
19
19
|
return code === "" ? null : code;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/plan-change.tsx"],"sourcesContent":["import { isValidElement, useCallback, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { pluralForm } from \"../i18n/plurals\";\nimport type { PluralForms } from \"../i18n/plurals\";\nimport { formatNumber, EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport { toLocalIso } from \"../lib/dates\";\nimport type { DataTableColumn } from \"../components/data-table\";\nimport { DatePicker } from \"../components/date-picker\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { Select } from \"../components/select\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { useBillingLabels } from \"../billing/billing-labels\";\nimport type { SubscriptionStatus } from \"../billing/billing-labels\";\nimport { SubscriptionStatusChip } from \"../billing/subscription-status-chip\";\nimport { AdminActionConfirm } from \"./admin-action-confirm\";\nimport type { AdminActionConfirmValues } from \"./admin-action-confirm\";\nimport { personLabel, useDayText } from \"./admin-parts\";\nimport type { AdminPerson, MaybePromise } from \"./admin-parts\";\nimport { DEFAULT_PLAN_CHANGE_LABELS } from \"./plan-change-labels\";\n\n/**\n * The operator's plan parts (docs/billing-harmonization.md §14.12, decision 27):\n * {@link planColumn} for the roster or the companies list, {@link PlanChangeConfirm}\n * for the grant, and {@link usePlanChangeResult} for what the answer says.\n *\n * All three apps have the route (`POST /admin/users/{id}/plan`, kastlan's\n * `POST /platform/companies/{id}/plan`) and server-kit's `PlanChangeRequest` /\n * `PlanChangeResponse`; only keksdose had a UI (`users-panel.tsx`, `PlanConfirm` in\n * `user-admin-confirm.tsx`), and these are its parts, lifted. The plan names are the\n * app's (§3.1): every part takes `planName` or the plans with their names. The plan\n * codes come from `GET /admin/plans` (kastlan `GET /platform/plans`), which answers\n * whatever the billing switch says.\n */\n\n/** The `planChange` namespace: the column, the grant dialog and the result's lines. */\nexport interface PlanChangeLabels {\n /** The column's header: \"Plan\". */\n column: string;\n /** The dialog's title: \"Change plan\". */\n title: string;\n /** Its confirm button: \"Change plan\". */\n confirm: string;\n /** The plan select's label: \"Plan\". */\n plan: string;\n /** The line over the select: \"Current plan: Free\". */\n current: (plan: string) => string;\n /** Under the select — the fact that is not on the button. */\n keepsItems: string;\n /** The end of a free grant's field: \"Free until\". */\n until: string;\n /** Under it. */\n untilHint: string;\n /** Why the confirm is held while nothing would change. */\n needsChange: string;\n /** The result: the plan moved from one to another. */\n changed: (from: string, to: string) => string;\n /** The result: an account with no plan before, or the same plan again. */\n set: (to: string) => string;\n /** The result: a running beta kept its own end (§12.34); only the plan moved. */\n keptBeta: string;\n /** The result: the grant runs until `date` (formatted). */\n untilDone: (date: string) => string;\n /** The result: the account owns more than the new plan allows. */\n overLimit: string;\n /**\n * The column's usage, spelled out (its tooltip): \"1 of 5 budgets\". Figures arrive\n * formatted; `dimension` is the app's word, as `usage.label` gives it — since 0.33.1\n * in the form the LIMIT takes when the label gives its plural forms (\"1 of 1 budget\",\n * \"1 of 5 budgets\"): in every shipped language the word follows the limit, or heads\n * the line in German, whose `von` would want a dative plural no app form carries\n * (\"Budgets: 1 von 5\"). An override of yours gets the same word. Since 0.34 the grant\n * dialog's usage line too, when `usage` comes as the column's shape.\n */\n usageOf: (used: string, limit: string, dimension: string) => string;\n}\n\n// Defined in ./plan-change-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_PLAN_CHANGE_LABELS };\n\n/** A plan code as the parts compare it: lowercase (§12.16 — an older row's \"FREE\" is\n * the same plan as \"free\"), and no code at all as null. */\nfunction codeOf(raw: string | null | undefined): string | null {\n const code = (raw ?? \"\").trim().toLowerCase();\n return code === \"\" ? null : code;\n}\n\n/* ── The column ────────────────────────────────────────────────────────────── */\n\n/** What a row's usage is, for the line under its plan: \"1/5\" — and since 0.34 what\n * {@link PlanChangeConfirm}'s `usage` takes, for the same sentence in the dialog. */\nexport interface PlanColumnUsage {\n used: number;\n /** The plan's limit for it; `null` (unlimited, or none known): no line. */\n limit: number | null;\n /**\n * The app's word for what is counted, as it reads after a number: \"budgets\" in \"1 of 5\n * budgets\" — the word in a sentence, not a heading's \"Budgets\".\n *\n * Its plural forms (0.33.1, keksdose): `{ one: \"budget\", other: \"budgets\" }` — your\n * catalogue's `_one` / `_other` pair, any CLDR category allowed, `other` for the rest\n * ({@link PluralForms}). The column picks the form for the limit in the provider's\n * locale, so a plan of one reads \"1 of 1 budget\". A plain string is used as it is,\n * whatever the count.\n */\n label: string | PluralForms;\n}\n\nexport interface PlanColumnOptions<T> {\n /** The column's key. Default `\"plan\"`. */\n key?: string;\n /** The row's plan code, read without case (§12.16); null for none. */\n plan: (row: T) => string | null;\n /** The plan's display name, the app's (§3.1). Gets the lowercase code. */\n planName: (code: string) => string;\n /** The row's standing, as a `SubscriptionStatusChip` beside the name. Left out, or\n * undefined for a row: no chip — billing off, where a status says nothing. */\n status?: (row: T) => SubscriptionStatus | undefined;\n /** The row's usage of its plan's limit: \"1/5\" under the name, \"1 of 5 budgets\" in\n * the tooltip (`planChange.usageOf`, with the label's form for the limit). Undefined\n * for a row, or a `null` limit: no line. */\n usage?: (row: T) => PlanColumnUsage | undefined;\n /** A select filter over these codes, each named by `planName`. Left out: none. */\n filterPlans?: readonly string[];\n /**\n * The sort. Default: by the plan code (keksdose's, `users-panel.tsx:739`). A function:\n * by its value — the catalogue's `sort` for rank order. `false`: not sortable.\n *\n * Client-side only, as every column's `sortBy`: a `DataTable` with `serverPagination`\n * holds one page, so it sorts nothing itself — with `onSortsChange` it reports the\n * column's `key` (default `\"plan\"`), and your server orders the rows by it.\n */\n sortBy?: false | ((row: T) => string | number | null | undefined);\n /** The header's text. Default `planChange.column`. */\n headerText?: string;\n /** The words, already resolved. Default English; {@link usePlanColumn} reads the\n * provider's `planChange` namespace. */\n labels?: LabelOverride<PlanChangeLabels>;\n}\n\n/**\n * A usage spelled out — `planChange.usageOf` with the figures in `locale` and the word in\n * the form the limit takes: \"1 of 1 budget\", \"Budgets: 1 von 5\". The column's tooltip\n * and the grant dialog's line both read it, so the rule lives once (round 0.34 C15).\n */\nfunction usageSentence(\n usage: PlanColumnUsage & { limit: number },\n usageOf: PlanChangeLabels[\"usageOf\"],\n locale: string | undefined,\n): string {\n const figure = (value: number) => formatNumber(value, { locale });\n const word = typeof usage.label === \"string\" ? usage.label : pluralForm(usage.limit, usage.label, locale);\n return usageOf(figure(usage.used), figure(usage.limit), word);\n}\n\n/** The cell: a component, so the figures follow the provider's locale. */\nfunction PlanCell({\n code,\n name,\n status,\n usage,\n usageOf,\n}: {\n code: string | null;\n name: string;\n status: SubscriptionStatus | undefined;\n usage: PlanColumnUsage | undefined;\n usageOf: PlanChangeLabels[\"usageOf\"];\n}) {\n const locale = useKitLocale();\n const figure = (value: number) => formatNumber(value, { locale });\n return (\n <div className=\"whitespace-nowrap\" data-plan={code ?? undefined}>\n <div className=\"flex items-center gap-1.5\">\n <span className={code ? \"font-medium\" : \"text-muted\"}>{name}</span>\n {/* The standing at a glance (§6): the kit's word and tone, never colour alone. */}\n {status !== undefined && <SubscriptionStatusChip status={status} size=\"xs\" />}\n </div>\n {usage && usage.limit !== null && (\n // \"1/5\" under the plan, \"1 of 5 budgets\" in the tooltip: the sentence spelled out\n // inline was the widest thing in keksdose's table.\n <Tooltip label={usageSentence({ ...usage, limit: usage.limit }, usageOf, locale)}>\n <span className=\"block text-xs tabular-nums text-muted\">\n {figure(usage.used)}/{figure(usage.limit)}\n </span>\n </Tooltip>\n )}\n </div>\n );\n}\n\n/**\n * The roster's (or the companies list's) plan column: the plan's display name, the\n * standing chip beside it while billing is on, and the usage under it — keksdose's\n * column, `users-panel.tsx:706-745`. Pass it in `userRosterColumns`' `extra`, or in\n * any `DataTable`'s columns:\n *\n * extra: [planColumn<AdminUserRow>({\n * plan: (u) => u.plan,\n * planName,\n * status: billingOn ? (u) => u.subscription?.status : undefined,\n * usage: (u) => ({\n * used: u.budgets_owned,\n * limit: u.limits.budgets,\n * label: { one: t(\"planUsage.budget_one\"), other: t(\"planUsage.budget_other\") },\n * }),\n * filterPlans: plans.map((p) => p.code),\n * })]\n *\n * Pure: hand it resolved words, or use {@link usePlanColumn}.\n */\nexport function planColumn<T>(options: PlanColumnOptions<T>): DataTableColumn<T> {\n const { key = \"plan\", plan, planName, status, usage, filterPlans, sortBy, headerText, labels: labelsProp } = options;\n const labels: PlanChangeLabels = { ...DEFAULT_PLAN_CHANGE_LABELS, ...labelsProp };\n const header = headerText ?? labels.column;\n const nameOf = (code: string | null) => (code ? planName(code) : EMPTY_FORMATTED_VALUE);\n return {\n key,\n header,\n headerText: header,\n cell: (row) => {\n const code = codeOf(plan(row));\n return (\n <PlanCell\n code={code}\n name={nameOf(code)}\n status={status?.(row)}\n usage={usage?.(row)}\n usageOf={labels.usageOf}\n />\n );\n },\n sortBy: sortBy === false ? undefined : (sortBy ?? ((row) => codeOf(plan(row)) ?? \"\")),\n filter: filterPlans\n ? {\n type: \"select\",\n getValue: (row) => codeOf(plan(row)) ?? \"\",\n options: [...new Set(filterPlans.map((code) => codeOf(code)).filter((code) => code !== null))].map(\n (code) => ({ value: code, label: planName(code) }),\n ),\n }\n : undefined,\n };\n}\n\n/**\n * {@link planColumn} with the `planChange` words from the provider, memoised on the\n * options — pass stable callbacks (`useCallback`), or the column is rebuilt on every\n * render.\n */\nexport function usePlanColumn<T>(options: PlanColumnOptions<T>): DataTableColumn<T> {\n const { key, plan, planName, status, usage, filterPlans, sortBy, headerText, labels: labelsProp } = options;\n const labels = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS, labelsProp);\n return useMemo(\n () => planColumn<T>({ key, plan, planName, status, usage, filterPlans, sortBy, headerText, labels }),\n [key, plan, planName, status, usage, filterPlans, sortBy, headerText, labels],\n );\n}\n\n/* ── The grant ─────────────────────────────────────────────────────────────── */\n\n/** What {@link PlanChangeConfirmProps.onConfirm} sends: server-kit's\n * `PlanChangeRequest` without its `acknowledged`, which comes in `values`. */\nexport interface PlanChange {\n /** The picked plan's code. */\n plan: string;\n /** The end of the free grant, as ISO UTC — only when one was picked. */\n comped_until?: string;\n}\n\nexport interface PlanChangeConfirmProps {\n /**\n * Whom it is about — a user (`AdminActionTarget`), or kastlan's company, which has no\n * address: its name is said instead (the level is `acknowledge`, so nothing is typed).\n */\n target: AdminPerson & { email?: string | null };\n /** The plans to offer, in the catalogue's order, by their display names (§3.1) — from\n * `GET /admin/plans`. */\n plans: readonly { code: string; name: string }[];\n /** The account's plan now; null for none. The select opens on it. */\n current: string | null;\n /**\n * What the account uses. The column's shape (0.34, keksdose N1) — `{ used, limit, label }`,\n * the label a word or its plural forms — is spelled out as the column's tooltip is\n * (`planChange.usageOf`, the word in the limit's form: \"1 of 1 budget\"); a `null` limit\n * gives no line. Or the app's own sentence, as it is: \"3 of 5 budgets\".\n */\n usage?: PlanColumnUsage | ReactNode;\n /** Offer \"Free until\" — only meaningful while billing is on, where a grant's end\n * means something to the payer. */\n grantEnd?: boolean;\n /**\n * Sends it: `{plan}`, and `comped_until` only when an end was picked — the END of the\n * picked day in the operator's time zone, as ISO UTC (a grant \"until 31 January\" lasts\n * through it). No end: a grant without one, or on a running beta only the plan moves\n * and the beta keeps its own end (§12.34). `values.acknowledged` is the body's\n * `acknowledged`. A promise is handled as `AdminActionConfirm`'s.\n */\n onConfirm: (change: PlanChange, values: AdminActionConfirmValues) => MaybePromise;\n onClose: (done: boolean) => void;\n /** The app's words for a failure the kit has no code for. */\n describeError?: (error: unknown) => ReactNode | undefined;\n labels?: LabelOverride<PlanChangeLabels>;\n}\n\n/** The column's usage shape, told from a node: a plain object with the three fields. */\nfunction isPlanUsage(value: PlanChangeConfirmProps[\"usage\"]): value is PlanColumnUsage {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !isValidElement(value) &&\n \"used\" in value &&\n \"limit\" in value &&\n \"label\" in value\n );\n}\n\n/** The end of the day `date` (YYYY-MM-DD) names, in this device's time zone, as ISO\n * UTC — keksdose's `endOfDay` (`user-admin-confirm.tsx:44-46`). */\nfunction endOfDay(date: string): string {\n return new Date(`${date}T23:59:59`).toISOString();\n}\n\n/**\n * An operator's plan change (§14.12): `AdminActionConfirm` at `acknowledge`, the line\n * with the current plan and the app's usage sentence, a plan select, and — with\n * `grantEnd` — the end of a free grant. The confirm is held while nothing would change.\n *\n * Exempt from a BILLING lock, built in (`COMMIT_EXCEPT_BILLING`, §12.36): a plan change\n * is an admin route (§12.13), and the operator's own account or company may be lapsed —\n * kastlan's review. A demo lock still holds it.\n */\nexport function PlanChangeConfirm({\n target,\n plans,\n current: currentProp,\n usage,\n grantEnd = false,\n onConfirm,\n onClose,\n describeError,\n labels: labelsProp,\n}: PlanChangeConfirmProps) {\n const labels = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS, labelsProp);\n const locale = useKitLocale();\n // Seeded from the account, so the picker opens on the truth and confirming without\n // touching it is held rather than a silent free → free in the audit.\n const current = codeOf(currentProp);\n const [plan, setPlan] = useState(current ?? \"\");\n const [until, setUntil] = useState(\"\");\n // A grant ends today at the earliest; read once, not on every render.\n const [today] = useState(() => toLocalIso(new Date()));\n const options = plans.map((p) => ({ code: codeOf(p.code) ?? p.code, name: p.name }));\n const nameOf = (code: string) => options.find((p) => p.code === code)?.name ?? code;\n // An account on a plan the catalogue no longer lists still opens on it.\n if (current && !options.some((p) => p.code === current)) options.unshift({ code: current, name: current });\n const unchanged = plan === \"\" || (plan === current && !until);\n // A company has no address: the dialog says its name in the address's place.\n const who = { ...target, email: target.email || personLabel(target, locale) };\n // The column's shape in the column's sentence; the app's own sentence as it is.\n const usageLine: ReactNode | undefined = isPlanUsage(usage)\n ? usage.limit === null\n ? undefined\n : usageSentence({ ...usage, limit: usage.limit }, labels.usageOf, locale)\n : usage;\n\n return (\n <AdminActionConfirm\n level=\"acknowledge\"\n tone=\"warning\"\n target={who}\n title={labels.title}\n confirmLabel={labels.confirm}\n confirmDisabledReason={unchanged ? labels.needsChange : undefined}\n describeError={describeError}\n commit={COMMIT_EXCEPT_BILLING}\n onConfirm={(values) => onConfirm({ plan, ...(until ? { comped_until: endOfDay(until) } : {}) }, values)}\n onClose={onClose}\n >\n {(current !== null || usageLine !== undefined) && (\n <div className=\"space-y-0.5 text-xs text-muted\" data-plan-current={current ?? undefined}>\n {current !== null && <p>{labels.current(nameOf(current))}</p>}\n {usageLine !== undefined && <p>{usageLine}</p>}\n </div>\n )}\n <Select label={labels.plan} hint={labels.keepsItems} value={plan} onChange={(e) => setPlan(e.target.value)}>\n {plan === \"\" && (\n <option value=\"\" disabled>\n {EMPTY_FORMATTED_VALUE}\n </option>\n )}\n {options.map((option) => (\n <option key={option.code} value={option.code}>\n {option.name}\n </option>\n ))}\n </Select>\n {grantEnd && (\n <DatePicker\n label={labels.until}\n hint={labels.untilHint}\n value={until}\n onChange={setUntil}\n min={today}\n clearable\n />\n )}\n </AdminActionConfirm>\n );\n}\n\n/* ── The answer ────────────────────────────────────────────────────────────── */\n\n/**\n * A `PlanChangeResponse` as the page reads it — server-kit 0.7's (§14.12), which gained\n * `previous_plan` nullable, `usage`, `kept_beta` and `comped_until`.\n */\nexport interface PlanChangeOutcome {\n /** null: the account had no subscription. */\n previous_plan: string | null;\n plan: string;\n over_limit: boolean;\n /** The new plan's limits; null for unlimited. */\n limits: Readonly<Record<string, number | null>>;\n /** What the payer has, per dimension — the figures for the over-limit lines:\n * `{ budgets: 7 }`. */\n usage: Readonly<Record<string, number>>;\n /** A running beta kept its own end (§12.34). */\n kept_beta?: boolean;\n /** The grant's end as the change wrote it; null: no end, or no grant. */\n comped_until?: string | null;\n}\n\n/** A line's tone: the app's toast kind. */\nexport type PlanChangeTone = \"success\" | \"info\" | \"warning\";\n\nexport interface PlanChangeLine {\n tone: PlanChangeTone;\n text: string;\n}\n\nexport interface PlanChangeResultOptions {\n /** The app's word per dimension, for the over-limit figures: `{ budgets: \"Budgets\" }`.\n * A dimension without one is named by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a figure is written. Default: a number in the kit's locale. */\n formatValue?: (dimension: string, value: number) => string;\n}\n\n/**\n * The lines for a plan change's answer, each with its tone, for the app's toasts —\n * echoing the SERVER's before and after, not what was picked: a toast that reported the\n * request would say \"done\" to a write that landed differently (keksdose's rule).\n *\n * const lines = usePlanChangeResult();\n * for (const line of lines(res, planName)) toast[line.tone](line.text);\n *\n * - `changed(from, to)`, or `set(to)` with no previous plan (or the same one): success;\n * - a running beta that kept its end: `keptBeta`, info; else a grant's end:\n * `untilDone(date)`, success;\n * - over the new limit: `overLimit`, then one figure line per dimension over it (\"Budgets:\n * 7 of 5\", `billing.limitUsageLine`), from `usage` and `limits`: warning. A `null`\n * limit gives no figure line.\n */\nexport function usePlanChangeResult(): (\n res: PlanChangeOutcome,\n planName: (code: string) => string,\n options?: PlanChangeResultOptions,\n) => PlanChangeLine[] {\n const labels = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS);\n const billing = useBillingLabels();\n const locale = useKitLocale();\n const dayText = useDayText();\n return useCallback(\n (res, planName, options = {}) => {\n const { dimensionLabels, formatValue } = options;\n const name = (raw: string) => planName(codeOf(raw) ?? raw);\n const to = name(res.plan);\n const from = codeOf(res.previous_plan);\n const lines: PlanChangeLine[] = [\n {\n tone: \"success\",\n text: from !== null && from !== codeOf(res.plan) ? labels.changed(name(from), to) : labels.set(to),\n },\n ];\n if (res.kept_beta) lines.push({ tone: \"info\", text: labels.keptBeta });\n else if (res.comped_until) lines.push({ tone: \"success\", text: labels.untilDone(dayText(res.comped_until)) });\n if (res.over_limit) {\n lines.push({ tone: \"warning\", text: labels.overLimit });\n const figure = (dimension: string, value: number) =>\n formatValue ? formatValue(dimension, value) : formatNumber(value, { locale });\n // Read defensively: an app's server from before 0.7 sent neither.\n for (const [dimension, limit] of Object.entries(res.limits ?? {})) {\n const used = res.usage?.[dimension];\n if (limit === null || used === undefined || used <= limit) continue;\n lines.push({\n tone: \"warning\",\n text: billing.limitUsageLine(\n dimensionLabels?.[dimension] ?? dimension,\n figure(dimension, used),\n figure(dimension, limit),\n ),\n });\n }\n }\n return lines;\n },\n [labels, billing, locale, dayText],\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/plan-change.tsx"],"sourcesContent":["import { isValidElement, useCallback, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { pluralForm } from \"../i18n/plurals\";\nimport type { PluralForms } from \"../i18n/plurals\";\nimport { formatNumber, EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport { toLocalIso } from \"../lib/dates\";\nimport type { DataTableColumn } from \"../components/data-table\";\nimport { DatePicker } from \"../components/date-picker\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { Select } from \"../components/select\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { useBillingLabels } from \"../billing/billing-labels\";\nimport type { SubscriptionStatus } from \"../billing/billing-labels\";\nimport { SubscriptionStatusChip } from \"../billing/subscription-status-chip\";\nimport { AdminActionConfirm } from \"./admin-action-confirm\";\nimport type { AdminActionConfirmValues } from \"./admin-action-confirm\";\nimport { personLabel, useDayText } from \"./admin-parts\";\nimport type { AdminPerson, MaybePromise } from \"./admin-parts\";\nimport { DEFAULT_PLAN_CHANGE_LABELS } from \"../billing/plan-change-labels\";\nimport type { PlanChangeLabels } from \"../billing/plan-change-labels\";\n\n/**\n * The operator's plan parts (docs/billing-harmonization.md §14.12, decision 27):\n * {@link planColumn} for the roster or the companies list, {@link PlanChangeConfirm}\n * for the grant, and {@link usePlanChangeResult} for what the answer says.\n *\n * All three apps have the route (`POST /admin/users/{id}/plan`, kastlan's\n * `POST /platform/companies/{id}/plan`) and server-kit's `PlanChangeRequest` /\n * `PlanChangeResponse`; only keksdose had a UI (`users-panel.tsx`, `PlanConfirm` in\n * `user-admin-confirm.tsx`), and these are its parts, lifted. The plan names are the\n * app's (§3.1): every part takes `planName` or the plans with their names. The plan\n * codes come from `GET /admin/plans` (kastlan `GET /platform/plans`), which answers\n * whatever the billing switch says.\n */\n\n// The words and their type live in billing/plan-change-labels, which holds no component\n// code (0.34, KS U8): PlanSwitchConfirm reads them too, and billing importing admin while\n// this module imports billing made the two folders import each other (refactor audit\n// 2026-10-30, MOD-6).\nexport type { PlanChangeLabels };\nexport { DEFAULT_PLAN_CHANGE_LABELS };\n\n/** A plan code as the parts compare it: lowercase (§12.16 — an older row's \"FREE\" is\n * the same plan as \"free\"), and no code at all as null. */\nfunction codeOf(raw: string | null | undefined): string | null {\n const code = (raw ?? \"\").trim().toLowerCase();\n return code === \"\" ? null : code;\n}\n\n/* ── The column ────────────────────────────────────────────────────────────── */\n\n/** What a row's usage is, for the line under its plan: \"1/5\" — and since 0.34 what\n * {@link PlanChangeConfirm}'s `usage` takes, for the same sentence in the dialog. */\nexport interface PlanColumnUsage {\n used: number;\n /** The plan's limit for it; `null` (unlimited, or none known): no line. */\n limit: number | null;\n /**\n * The app's word for what is counted, as it reads after a number: \"budgets\" in \"1 of 5\n * budgets\" — the word in a sentence, not a heading's \"Budgets\".\n *\n * Its plural forms (0.33.1, keksdose): `{ one: \"budget\", other: \"budgets\" }` — your\n * catalogue's `_one` / `_other` pair, any CLDR category allowed, `other` for the rest\n * ({@link PluralForms}). The column picks the form for the limit in the provider's\n * locale, so a plan of one reads \"1 of 1 budget\". A plain string is used as it is,\n * whatever the count.\n */\n label: string | PluralForms;\n}\n\nexport interface PlanColumnOptions<T> {\n /** The column's key. Default `\"plan\"`. */\n key?: string;\n /** The row's plan code, read without case (§12.16); null for none. */\n plan: (row: T) => string | null;\n /** The plan's display name, the app's (§3.1). Gets the lowercase code. */\n planName: (code: string) => string;\n /** The row's standing, as a `SubscriptionStatusChip` beside the name. Left out, or\n * undefined for a row: no chip — billing off, where a status says nothing. */\n status?: (row: T) => SubscriptionStatus | undefined;\n /** The row's usage of its plan's limit: \"1/5\" under the name, \"1 of 5 budgets\" in\n * the tooltip (`planChange.usageOf`, with the label's form for the limit). Undefined\n * for a row, or a `null` limit: no line. */\n usage?: (row: T) => PlanColumnUsage | undefined;\n /** A select filter over these codes, each named by `planName`. Left out: none. */\n filterPlans?: readonly string[];\n /**\n * The sort. Default: by the plan code (keksdose's, `users-panel.tsx:739`). A function:\n * by its value — the catalogue's `sort` for rank order. `false`: not sortable.\n *\n * Client-side only, as every column's `sortBy`: a `DataTable` with `serverPagination`\n * holds one page, so it sorts nothing itself — with `onSortsChange` it reports the\n * column's `key` (default `\"plan\"`), and your server orders the rows by it.\n */\n sortBy?: false | ((row: T) => string | number | null | undefined);\n /** The header's text. Default `planChange.column`. */\n headerText?: string;\n /** The words, already resolved. Default English; {@link usePlanColumn} reads the\n * provider's `planChange` namespace. */\n labels?: LabelOverride<PlanChangeLabels>;\n}\n\n/**\n * A usage spelled out — `planChange.usageOf` with the figures in `locale` and the word in\n * the form the limit takes: \"1 of 1 budget\", \"Budgets: 1 von 5\". The column's tooltip\n * and the grant dialog's line both read it, so the rule lives once (round 0.34 C15).\n */\nfunction usageSentence(\n usage: PlanColumnUsage & { limit: number },\n usageOf: PlanChangeLabels[\"usageOf\"],\n locale: string | undefined,\n): string {\n const figure = (value: number) => formatNumber(value, { locale });\n const word = typeof usage.label === \"string\" ? usage.label : pluralForm(usage.limit, usage.label, locale);\n return usageOf(figure(usage.used), figure(usage.limit), word);\n}\n\n/** The cell: a component, so the figures follow the provider's locale. */\nfunction PlanCell({\n code,\n name,\n status,\n usage,\n usageOf,\n}: {\n code: string | null;\n name: string;\n status: SubscriptionStatus | undefined;\n usage: PlanColumnUsage | undefined;\n usageOf: PlanChangeLabels[\"usageOf\"];\n}) {\n const locale = useKitLocale();\n const figure = (value: number) => formatNumber(value, { locale });\n return (\n <div className=\"whitespace-nowrap\" data-plan={code ?? undefined}>\n <div className=\"flex items-center gap-1.5\">\n <span className={code ? \"font-medium\" : \"text-muted\"}>{name}</span>\n {/* The standing at a glance (§6): the kit's word and tone, never colour alone. */}\n {status !== undefined && <SubscriptionStatusChip status={status} size=\"xs\" />}\n </div>\n {usage && usage.limit !== null && (\n // \"1/5\" under the plan, \"1 of 5 budgets\" in the tooltip: the sentence spelled out\n // inline was the widest thing in keksdose's table.\n <Tooltip label={usageSentence({ ...usage, limit: usage.limit }, usageOf, locale)}>\n <span className=\"block text-xs tabular-nums text-muted\">\n {figure(usage.used)}/{figure(usage.limit)}\n </span>\n </Tooltip>\n )}\n </div>\n );\n}\n\n/**\n * The roster's (or the companies list's) plan column: the plan's display name, the\n * standing chip beside it while billing is on, and the usage under it — keksdose's\n * column, `users-panel.tsx:706-745`. Pass it in `userRosterColumns`' `extra`, or in\n * any `DataTable`'s columns:\n *\n * extra: [planColumn<AdminUserRow>({\n * plan: (u) => u.plan,\n * planName,\n * status: billingOn ? (u) => u.subscription?.status : undefined,\n * usage: (u) => ({\n * used: u.budgets_owned,\n * limit: u.limits.budgets,\n * label: { one: t(\"planUsage.budget_one\"), other: t(\"planUsage.budget_other\") },\n * }),\n * filterPlans: plans.map((p) => p.code),\n * })]\n *\n * Pure: hand it resolved words, or use {@link usePlanColumn}.\n */\nexport function planColumn<T>(options: PlanColumnOptions<T>): DataTableColumn<T> {\n const { key = \"plan\", plan, planName, status, usage, filterPlans, sortBy, headerText, labels: labelsProp } = options;\n const labels: PlanChangeLabels = { ...DEFAULT_PLAN_CHANGE_LABELS, ...labelsProp };\n const header = headerText ?? labels.column;\n const nameOf = (code: string | null) => (code ? planName(code) : EMPTY_FORMATTED_VALUE);\n return {\n key,\n header,\n headerText: header,\n cell: (row) => {\n const code = codeOf(plan(row));\n return (\n <PlanCell\n code={code}\n name={nameOf(code)}\n status={status?.(row)}\n usage={usage?.(row)}\n usageOf={labels.usageOf}\n />\n );\n },\n sortBy: sortBy === false ? undefined : (sortBy ?? ((row) => codeOf(plan(row)) ?? \"\")),\n filter: filterPlans\n ? {\n type: \"select\",\n getValue: (row) => codeOf(plan(row)) ?? \"\",\n options: [...new Set(filterPlans.map((code) => codeOf(code)).filter((code) => code !== null))].map(\n (code) => ({ value: code, label: planName(code) }),\n ),\n }\n : undefined,\n };\n}\n\n/**\n * {@link planColumn} with the `planChange` words from the provider, memoised on the\n * options — pass stable callbacks (`useCallback`), or the column is rebuilt on every\n * render.\n */\nexport function usePlanColumn<T>(options: PlanColumnOptions<T>): DataTableColumn<T> {\n const { key, plan, planName, status, usage, filterPlans, sortBy, headerText, labels: labelsProp } = options;\n const labels = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS, labelsProp);\n return useMemo(\n () => planColumn<T>({ key, plan, planName, status, usage, filterPlans, sortBy, headerText, labels }),\n [key, plan, planName, status, usage, filterPlans, sortBy, headerText, labels],\n );\n}\n\n/* ── The grant ─────────────────────────────────────────────────────────────── */\n\n/** What {@link PlanChangeConfirmProps.onConfirm} sends: server-kit's\n * `PlanChangeRequest` without its `acknowledged`, which comes in `values`. */\nexport interface PlanChange {\n /** The picked plan's code. */\n plan: string;\n /** The end of the free grant, as ISO UTC — only when one was picked. */\n comped_until?: string;\n}\n\nexport interface PlanChangeConfirmProps {\n /**\n * Whom it is about — a user (`AdminActionTarget`), or kastlan's company, which has no\n * address: its name is said instead (the level is `acknowledge`, so nothing is typed).\n */\n target: AdminPerson & { email?: string | null };\n /** The plans to offer, in the catalogue's order, by their display names (§3.1) — from\n * `GET /admin/plans`. */\n plans: readonly { code: string; name: string }[];\n /** The account's plan now; null for none. The select opens on it. */\n current: string | null;\n /**\n * What the account uses. The column's shape (0.34, keksdose N1) — `{ used, limit, label }`,\n * the label a word or its plural forms — is spelled out as the column's tooltip is\n * (`planChange.usageOf`, the word in the limit's form: \"1 of 1 budget\"); a `null` limit\n * gives no line. Or the app's own sentence, as it is: \"3 of 5 budgets\".\n */\n usage?: PlanColumnUsage | ReactNode;\n /** Offer \"Free until\" — only meaningful while billing is on, where a grant's end\n * means something to the payer. */\n grantEnd?: boolean;\n /**\n * Sends it: `{plan}`, and `comped_until` only when an end was picked — the END of the\n * picked day in the operator's time zone, as ISO UTC (a grant \"until 31 January\" lasts\n * through it). No end: a grant without one, or on a running beta only the plan moves\n * and the beta keeps its own end (§12.34). `values.acknowledged` is the body's\n * `acknowledged`. A promise is handled as `AdminActionConfirm`'s.\n */\n onConfirm: (change: PlanChange, values: AdminActionConfirmValues) => MaybePromise;\n onClose: (done: boolean) => void;\n /** The app's words for a failure the kit has no code for. */\n describeError?: (error: unknown) => ReactNode | undefined;\n labels?: LabelOverride<PlanChangeLabels>;\n}\n\n/** The column's usage shape, told from a node: a plain object with the three fields. */\nfunction isPlanUsage(value: PlanChangeConfirmProps[\"usage\"]): value is PlanColumnUsage {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !isValidElement(value) &&\n \"used\" in value &&\n \"limit\" in value &&\n \"label\" in value\n );\n}\n\n/** The end of the day `date` (YYYY-MM-DD) names, in this device's time zone, as ISO\n * UTC — keksdose's `endOfDay` (`user-admin-confirm.tsx:44-46`). */\nfunction endOfDay(date: string): string {\n return new Date(`${date}T23:59:59`).toISOString();\n}\n\n/**\n * An operator's plan change (§14.12): `AdminActionConfirm` at `acknowledge`, the line\n * with the current plan and the app's usage sentence, a plan select, and — with\n * `grantEnd` — the end of a free grant. The confirm is held while nothing would change.\n *\n * Exempt from a BILLING lock, built in (`COMMIT_EXCEPT_BILLING`, §12.36): a plan change\n * is an admin route (§12.13), and the operator's own account or company may be lapsed —\n * kastlan's review. A demo lock still holds it.\n */\nexport function PlanChangeConfirm({\n target,\n plans,\n current: currentProp,\n usage,\n grantEnd = false,\n onConfirm,\n onClose,\n describeError,\n labels: labelsProp,\n}: PlanChangeConfirmProps) {\n const labels = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS, labelsProp);\n const locale = useKitLocale();\n // Seeded from the account, so the picker opens on the truth and confirming without\n // touching it is held rather than a silent free → free in the audit.\n const current = codeOf(currentProp);\n const [plan, setPlan] = useState(current ?? \"\");\n const [until, setUntil] = useState(\"\");\n // A grant ends today at the earliest; read once, not on every render.\n const [today] = useState(() => toLocalIso(new Date()));\n const options = plans.map((p) => ({ code: codeOf(p.code) ?? p.code, name: p.name }));\n const nameOf = (code: string) => options.find((p) => p.code === code)?.name ?? code;\n // An account on a plan the catalogue no longer lists still opens on it.\n if (current && !options.some((p) => p.code === current)) options.unshift({ code: current, name: current });\n const unchanged = plan === \"\" || (plan === current && !until);\n // A company has no address: the dialog says its name in the address's place.\n const who = { ...target, email: target.email || personLabel(target, locale) };\n // The column's shape in the column's sentence; the app's own sentence as it is.\n const usageLine: ReactNode | undefined = isPlanUsage(usage)\n ? usage.limit === null\n ? undefined\n : usageSentence({ ...usage, limit: usage.limit }, labels.usageOf, locale)\n : usage;\n\n return (\n <AdminActionConfirm\n level=\"acknowledge\"\n tone=\"warning\"\n target={who}\n title={labels.title}\n confirmLabel={labels.confirm}\n confirmDisabledReason={unchanged ? labels.needsChange : undefined}\n describeError={describeError}\n commit={COMMIT_EXCEPT_BILLING}\n onConfirm={(values) => onConfirm({ plan, ...(until ? { comped_until: endOfDay(until) } : {}) }, values)}\n onClose={onClose}\n >\n {(current !== null || usageLine !== undefined) && (\n <div className=\"space-y-0.5 text-xs text-muted\" data-plan-current={current ?? undefined}>\n {current !== null && <p>{labels.current(nameOf(current))}</p>}\n {usageLine !== undefined && <p>{usageLine}</p>}\n </div>\n )}\n <Select label={labels.plan} hint={labels.keepsItems} value={plan} onChange={(e) => setPlan(e.target.value)}>\n {plan === \"\" && (\n <option value=\"\" disabled>\n {EMPTY_FORMATTED_VALUE}\n </option>\n )}\n {options.map((option) => (\n <option key={option.code} value={option.code}>\n {option.name}\n </option>\n ))}\n </Select>\n {grantEnd && (\n <DatePicker\n label={labels.until}\n hint={labels.untilHint}\n value={until}\n onChange={setUntil}\n min={today}\n clearable\n />\n )}\n </AdminActionConfirm>\n );\n}\n\n/* ── The answer ────────────────────────────────────────────────────────────── */\n\n/**\n * A `PlanChangeResponse` as the page reads it — server-kit 0.7's (§14.12), which gained\n * `previous_plan` nullable, `usage`, `kept_beta` and `comped_until`.\n */\nexport interface PlanChangeOutcome {\n /** null: the account had no subscription. */\n previous_plan: string | null;\n plan: string;\n over_limit: boolean;\n /** The new plan's limits; null for unlimited. */\n limits: Readonly<Record<string, number | null>>;\n /** What the payer has, per dimension — the figures for the over-limit lines:\n * `{ budgets: 7 }`. */\n usage: Readonly<Record<string, number>>;\n /** A running beta kept its own end (§12.34). */\n kept_beta?: boolean;\n /** The grant's end as the change wrote it; null: no end, or no grant. */\n comped_until?: string | null;\n}\n\n/** A line's tone: the app's toast kind. */\nexport type PlanChangeTone = \"success\" | \"info\" | \"warning\";\n\nexport interface PlanChangeLine {\n tone: PlanChangeTone;\n text: string;\n}\n\nexport interface PlanChangeResultOptions {\n /** The app's word per dimension, for the over-limit figures: `{ budgets: \"Budgets\" }`.\n * A dimension without one is named by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a figure is written. Default: a number in the kit's locale. */\n formatValue?: (dimension: string, value: number) => string;\n}\n\n/**\n * The lines for a plan change's answer, each with its tone, for the app's toasts —\n * echoing the SERVER's before and after, not what was picked: a toast that reported the\n * request would say \"done\" to a write that landed differently (keksdose's rule).\n *\n * const lines = usePlanChangeResult();\n * for (const line of lines(res, planName)) toast[line.tone](line.text);\n *\n * - `changed(from, to)`, or `set(to)` with no previous plan (or the same one): success;\n * - a running beta that kept its end: `keptBeta`, info; else a grant's end:\n * `untilDone(date)`, success;\n * - over the new limit: `overLimit`, then one figure line per dimension over it (\"Budgets:\n * 7 of 5\", `billing.limitUsageLine`), from `usage` and `limits`: warning. A `null`\n * limit gives no figure line.\n */\nexport function usePlanChangeResult(): (\n res: PlanChangeOutcome,\n planName: (code: string) => string,\n options?: PlanChangeResultOptions,\n) => PlanChangeLine[] {\n const labels = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS);\n const billing = useBillingLabels();\n const locale = useKitLocale();\n const dayText = useDayText();\n return useCallback(\n (res, planName, options = {}) => {\n const { dimensionLabels, formatValue } = options;\n const name = (raw: string) => planName(codeOf(raw) ?? raw);\n const to = name(res.plan);\n const from = codeOf(res.previous_plan);\n const lines: PlanChangeLine[] = [\n {\n tone: \"success\",\n text: from !== null && from !== codeOf(res.plan) ? labels.changed(name(from), to) : labels.set(to),\n },\n ];\n if (res.kept_beta) lines.push({ tone: \"info\", text: labels.keptBeta });\n else if (res.comped_until) lines.push({ tone: \"success\", text: labels.untilDone(dayText(res.comped_until)) });\n if (res.over_limit) {\n lines.push({ tone: \"warning\", text: labels.overLimit });\n const figure = (dimension: string, value: number) =>\n formatValue ? formatValue(dimension, value) : formatNumber(value, { locale });\n // Read defensively: an app's server from before 0.7 sent neither.\n for (const [dimension, limit] of Object.entries(res.limits ?? {})) {\n const used = res.usage?.[dimension];\n if (limit === null || used === undefined || used <= limit) continue;\n lines.push({\n tone: \"warning\",\n text: billing.limitUsageLine(\n dimensionLabels?.[dimension] ?? dimension,\n figure(dimension, used),\n figure(dimension, limit),\n ),\n });\n }\n }\n return lines;\n },\n [labels, billing, locale, dayText],\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -7,6 +7,7 @@ import { AlertBanner } from "../components/alert-banner.js";
|
|
|
7
7
|
import { CheckboxGroup } from "../components/checkbox-group.js";
|
|
8
8
|
import { FormActions } from "../components/form-actions.js";
|
|
9
9
|
import { Caption } from "../components/text.js";
|
|
10
|
+
import { describeFailure } from "../lib/describe-failure.js";
|
|
10
11
|
import { hasContent } from "../lib/presence.js";
|
|
11
12
|
import { DEFAULT_REVIEWER_SCOPE_LABELS } from "./reviewer-scope-editor-labels.js";
|
|
12
13
|
function languageOptions(languages) {
|
|
@@ -63,7 +64,7 @@ function ReviewerScopeEditor({
|
|
|
63
64
|
try {
|
|
64
65
|
result = onSave(scope);
|
|
65
66
|
} catch (error) {
|
|
66
|
-
setFailure(describeError?.(error)
|
|
67
|
+
setFailure(describeFailure(describeError?.(error), labels.failed));
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
70
|
if (!result || typeof result.then !== "function") return;
|
|
@@ -72,8 +73,7 @@ function ReviewerScopeEditor({
|
|
|
72
73
|
() => setPending(false),
|
|
73
74
|
(error) => {
|
|
74
75
|
setPending(false);
|
|
75
|
-
|
|
76
|
-
setFailure(hasContent(own) ? own : labels.failed);
|
|
76
|
+
setFailure(describeFailure(describeError?.(error), labels.failed));
|
|
77
77
|
}
|
|
78
78
|
);
|
|
79
79
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/reviewer-scope-editor.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { KIT_LANGUAGES } from \"../i18n/languages\";\nimport type { KitLanguageCode } from \"../i18n/languages\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { CheckboxGroup } from \"../components/checkbox-group\";\nimport type { CheckboxGroupOption } from \"../components/checkbox-group\";\nimport { FormActions } from \"../components/form-actions\";\nimport { Caption } from \"../components/text\";\nimport { hasContent } from \"../lib/presence\";\nimport type { MaybePromise } from \"./admin-parts\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_REVIEWER_SCOPE_LABELS } from \"./reviewer-scope-editor-labels\";\n\n/**\n * Who may review which translations: the REVIEWER role and its scope, in one editor\n * (docs/user-admin-harmonization.md §4.1 `PUT /admin/users/{id}/reviewer {locales, areas}`,\n * §7). It replaces three: keksdose's `user-reviewer-role.tsx` (a checkbox per language,\n * saved on each click), Kurvenschmiede's `reviewer-dialog.tsx` (a `CheckboxGroup` in a\n * dialog with Save) and kastlan's platform column.\n *\n * - **Languages** are the scope. Ticking none takes the role away — that is how both\n * apps remove a reviewer — and the editor says so under the boxes.\n * - **Areas** narrow it: \"Only the legal pages\" (Imprint, Privacy Policy, Terms) for a\n * lawyer, who reads law, not French. An area only means something beside at least one\n * language, so the areas wait for one. Saved as `areas: null` when none is ticked —\n * server-kit's \"every area\" — never `[]`, which would be \"no area at all\".\n *\n * A draft with Save, not a save per click: keksdose saved every tick, and two quick ticks\n * could land as a set nobody chose; one Save sends the whole scope, in the languages'\n * order (CheckboxGroup's), so the answer equals the request and the row does not\n * reshuffle on refetch. The server keeps its rules (an admin reviews everything already,\n * a customer is not given the role): `disabledReason` says them here.\n */\n\n/** A reviewer's scope — the request body of `PUT …/reviewer`. */\nexport interface ReviewerScope {\n /** Language codes, in the editor's order. Empty: not a reviewer. */\n locales: string[];\n /** The areas the scope is narrowed to; `null` = every area (server-kit's rule). */\n areas: string[] | null;\n}\n\n/** One area the scope can be narrowed to. */\nexport interface ReviewerScopeArea {\n value: string;\n label: ReactNode;\n hint?: ReactNode;\n}\n\n/** One language the editor offers, when the app's list is not the kit's codes. */\nexport interface ReviewerScopeLanguage {\n value: string;\n label: ReactNode;\n}\n\n/** The `reviewerScope` namespace. */\nexport interface ReviewerScopeLabels {\n /** The language group's legend. */\n languages: string;\n /** Under the languages. */\n hint: string;\n /** Under the languages while none is ticked. */\n none: string;\n /** The area group's legend. */\n areas: string;\n /** The default area, `legal`. */\n legalOnly: string;\n legalOnlyHint: string;\n /** A rejected save, unless `describeError` says better. */\n failed: string;\n}\n\n// Defined in ./reviewer-scope-editor-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_REVIEWER_SCOPE_LABELS };\n\nexport interface ReviewerScopeEditorProps {\n /** The scope the server has — the draft starts from it. Key the editor by the\n * account (`key={user.id}`) so another account starts a fresh draft. */\n value: { locales?: readonly string[] | null; areas?: readonly string[] | null };\n /** Sends the scope. A promise keeps the editor busy, and a rejection keeps the draft\n * and shows why. */\n onSave: (scope: ReviewerScope) => MaybePromise;\n /** A Cancel beside Save — a dialog's way out. */\n onCancel?: () => void;\n /**\n * The languages offered: kit codes (named by the kit's registry, each in its own\n * language — \"Deutsch\", \"Français\") or the app's own `{ value, label }` rows. Default:\n * all seven of the kit's.\n */\n languages?: readonly KitLanguageCode[] | readonly ReviewerScopeLanguage[];\n /** The areas the scope can be narrowed to. Default: `legal` alone. `[]` for none. */\n areas?: readonly ReviewerScopeArea[];\n /** Why the scope cannot be changed for this account (\"Administrators review every\n * language already\"). Every box says it; there is no Save. */\n disabledReason?: ReactNode;\n /** The app's words for a failed save. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** Over the languages: what the role is, in the app's words. */\n intro?: ReactNode;\n /** Columns for the languages from `sm` up. Default 2 (Kurvenschmiede's dialog). */\n columns?: 1 | 2 | 3 | 4;\n /** Save COMMITS: under a locked `WriteLockProvider` it is held with the lock's reason. */\n commit?: CommitScope;\n labels?: LabelOverride<ReviewerScopeLabels>;\n}\n\nfunction languageOptions(languages: ReviewerScopeEditorProps[\"languages\"]): CheckboxGroupOption[] {\n const rows = languages ?? KIT_LANGUAGES.map((language) => language.code);\n return rows.map((row) => {\n if (typeof row !== \"string\") return { value: row.value, label: row.label };\n const language = KIT_LANGUAGES.find((entry) => entry.code === row);\n return {\n value: row,\n // Each name in its own language, marked so a screen reader says it so.\n label: language ? <span lang={language.formatLocale}>{language.nativeName}</span> : row,\n };\n });\n}\n\n/** The same set, regardless of order. */\nfunction sameSet(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n const seen = new Set(a);\n return b.every((x) => seen.has(x));\n}\n\nexport function ReviewerScopeEditor({\n value,\n onSave,\n onCancel,\n languages,\n areas: areasProp,\n disabledReason,\n describeError,\n intro,\n columns = 2,\n commit,\n labels: labelsProp,\n}: ReviewerScopeEditorProps) {\n const labels = useKitLabels(\"reviewerScope\", DEFAULT_REVIEWER_SCOPE_LABELS, labelsProp);\n const areaOptions: readonly ReviewerScopeArea[] = areasProp ?? [\n { value: \"legal\", label: labels.legalOnly, hint: labels.legalOnlyHint },\n ];\n const savedLocales = [...(value.locales ?? [])];\n const savedAreas = [...(value.areas ?? [])];\n const [locales, setLocales] = useState<string[]>(savedLocales);\n const [areas, setAreas] = useState<string[]>(savedAreas);\n const [pending, setPending] = useState(false);\n const [failure, setFailure] = useState<ReactNode>(null);\n const locked = hasContent(disabledReason);\n\n const nextAreas = locales.length === 0 ? [] : areas;\n const dirty = !sameSet(locales, savedLocales) || !sameSet(nextAreas, locales.length === 0 ? [] : savedAreas);\n\n const save = () => {\n if (pending || locked || !dirty) return;\n const scope: ReviewerScope = {\n locales,\n areas: locales.length > 0 && areas.length > 0 ? areas : null,\n };\n setFailure(null);\n let result: MaybePromise;\n try {\n result = onSave(scope);\n } catch (error) {\n setFailure(describeError?.(error) ?? labels.failed);\n return;\n }\n if (!result || typeof result.then !== \"function\") return;\n setPending(true);\n result.then(\n () => setPending(false),\n (error: unknown) => {\n setPending(false);\n const own = describeError?.(error);\n setFailure(hasContent(own) ? own : labels.failed);\n },\n );\n };\n\n return (\n <div className=\"space-y-3\">\n {intro && <div className=\"text-sm text-secondary\">{intro}</div>}\n <CheckboxGroup\n legend={labels.languages}\n options={languageOptions(languages)}\n value={locales}\n onChange={(next) => {\n setFailure(null);\n setLocales(next);\n }}\n columns={columns}\n hint={locked ? undefined : locales.length === 0 ? labels.none : labels.hint}\n disabledReason={disabledReason}\n />\n {areaOptions.length > 0 && (\n <CheckboxGroup\n legend={labels.areas}\n options={areaOptions.map((area) => ({ value: area.value, label: area.label, hint: area.hint }))}\n value={nextAreas}\n onChange={(next) => {\n setFailure(null);\n setAreas(next);\n }}\n // An area narrows a language scope; with no language there is none to narrow.\n disabled={locales.length === 0}\n disabledReason={disabledReason}\n />\n )}\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\">\n {failure}\n </AlertBanner>\n )}\n {locked ? (\n <Caption>{disabledReason}</Caption>\n ) : (\n <FormActions onSubmit={save} onCancel={onCancel} pending={pending} submitDisabled={!dirty} commit={commit} />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/reviewer-scope-editor.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { KIT_LANGUAGES } from \"../i18n/languages\";\nimport type { KitLanguageCode } from \"../i18n/languages\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { CheckboxGroup } from \"../components/checkbox-group\";\nimport type { CheckboxGroupOption } from \"../components/checkbox-group\";\nimport { FormActions } from \"../components/form-actions\";\nimport { Caption } from \"../components/text\";\nimport { describeFailure } from \"../lib/describe-failure\";\nimport { hasContent } from \"../lib/presence\";\nimport type { MaybePromise } from \"./admin-parts\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_REVIEWER_SCOPE_LABELS } from \"./reviewer-scope-editor-labels\";\n\n/**\n * Who may review which translations: the REVIEWER role and its scope, in one editor\n * (docs/user-admin-harmonization.md §4.1 `PUT /admin/users/{id}/reviewer {locales, areas}`,\n * §7). It replaces three: keksdose's `user-reviewer-role.tsx` (a checkbox per language,\n * saved on each click), Kurvenschmiede's `reviewer-dialog.tsx` (a `CheckboxGroup` in a\n * dialog with Save) and kastlan's platform column.\n *\n * - **Languages** are the scope. Ticking none takes the role away — that is how both\n * apps remove a reviewer — and the editor says so under the boxes.\n * - **Areas** narrow it: \"Only the legal pages\" (Imprint, Privacy Policy, Terms) for a\n * lawyer, who reads law, not French. An area only means something beside at least one\n * language, so the areas wait for one. Saved as `areas: null` when none is ticked —\n * server-kit's \"every area\" — never `[]`, which would be \"no area at all\".\n *\n * A draft with Save, not a save per click: keksdose saved every tick, and two quick ticks\n * could land as a set nobody chose; one Save sends the whole scope, in the languages'\n * order (CheckboxGroup's), so the answer equals the request and the row does not\n * reshuffle on refetch. The server keeps its rules (an admin reviews everything already,\n * a customer is not given the role): `disabledReason` says them here.\n */\n\n/** A reviewer's scope — the request body of `PUT …/reviewer`. */\nexport interface ReviewerScope {\n /** Language codes, in the editor's order. Empty: not a reviewer. */\n locales: string[];\n /** The areas the scope is narrowed to; `null` = every area (server-kit's rule). */\n areas: string[] | null;\n}\n\n/** One area the scope can be narrowed to. */\nexport interface ReviewerScopeArea {\n value: string;\n label: ReactNode;\n hint?: ReactNode;\n}\n\n/** One language the editor offers, when the app's list is not the kit's codes. */\nexport interface ReviewerScopeLanguage {\n value: string;\n label: ReactNode;\n}\n\n/** The `reviewerScope` namespace. */\nexport interface ReviewerScopeLabels {\n /** The language group's legend. */\n languages: string;\n /** Under the languages. */\n hint: string;\n /** Under the languages while none is ticked. */\n none: string;\n /** The area group's legend. */\n areas: string;\n /** The default area, `legal`. */\n legalOnly: string;\n legalOnlyHint: string;\n /** A rejected save, unless `describeError` says better. */\n failed: string;\n}\n\n// Defined in ./reviewer-scope-editor-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_REVIEWER_SCOPE_LABELS };\n\nexport interface ReviewerScopeEditorProps {\n /** The scope the server has — the draft starts from it. Key the editor by the\n * account (`key={user.id}`) so another account starts a fresh draft. */\n value: { locales?: readonly string[] | null; areas?: readonly string[] | null };\n /** Sends the scope. A promise keeps the editor busy, and a rejection keeps the draft\n * and shows why. */\n onSave: (scope: ReviewerScope) => MaybePromise;\n /** A Cancel beside Save — a dialog's way out. */\n onCancel?: () => void;\n /**\n * The languages offered: kit codes (named by the kit's registry, each in its own\n * language — \"Deutsch\", \"Français\") or the app's own `{ value, label }` rows. Default:\n * all seven of the kit's.\n */\n languages?: readonly KitLanguageCode[] | readonly ReviewerScopeLanguage[];\n /** The areas the scope can be narrowed to. Default: `legal` alone. `[]` for none. */\n areas?: readonly ReviewerScopeArea[];\n /** Why the scope cannot be changed for this account (\"Administrators review every\n * language already\"). Every box says it; there is no Save. */\n disabledReason?: ReactNode;\n /** The app's words for a failed save. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** Over the languages: what the role is, in the app's words. */\n intro?: ReactNode;\n /** Columns for the languages from `sm` up. Default 2 (Kurvenschmiede's dialog). */\n columns?: 1 | 2 | 3 | 4;\n /** Save COMMITS: under a locked `WriteLockProvider` it is held with the lock's reason. */\n commit?: CommitScope;\n labels?: LabelOverride<ReviewerScopeLabels>;\n}\n\nfunction languageOptions(languages: ReviewerScopeEditorProps[\"languages\"]): CheckboxGroupOption[] {\n const rows = languages ?? KIT_LANGUAGES.map((language) => language.code);\n return rows.map((row) => {\n if (typeof row !== \"string\") return { value: row.value, label: row.label };\n const language = KIT_LANGUAGES.find((entry) => entry.code === row);\n return {\n value: row,\n // Each name in its own language, marked so a screen reader says it so.\n label: language ? <span lang={language.formatLocale}>{language.nativeName}</span> : row,\n };\n });\n}\n\n/** The same set, regardless of order. */\nfunction sameSet(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n const seen = new Set(a);\n return b.every((x) => seen.has(x));\n}\n\nexport function ReviewerScopeEditor({\n value,\n onSave,\n onCancel,\n languages,\n areas: areasProp,\n disabledReason,\n describeError,\n intro,\n columns = 2,\n commit,\n labels: labelsProp,\n}: ReviewerScopeEditorProps) {\n const labels = useKitLabels(\"reviewerScope\", DEFAULT_REVIEWER_SCOPE_LABELS, labelsProp);\n const areaOptions: readonly ReviewerScopeArea[] = areasProp ?? [\n { value: \"legal\", label: labels.legalOnly, hint: labels.legalOnlyHint },\n ];\n const savedLocales = [...(value.locales ?? [])];\n const savedAreas = [...(value.areas ?? [])];\n const [locales, setLocales] = useState<string[]>(savedLocales);\n const [areas, setAreas] = useState<string[]>(savedAreas);\n const [pending, setPending] = useState(false);\n const [failure, setFailure] = useState<ReactNode>(null);\n const locked = hasContent(disabledReason);\n\n const nextAreas = locales.length === 0 ? [] : areas;\n const dirty = !sameSet(locales, savedLocales) || !sameSet(nextAreas, locales.length === 0 ? [] : savedAreas);\n\n const save = () => {\n if (pending || locked || !dirty) return;\n const scope: ReviewerScope = {\n locales,\n areas: locales.length > 0 && areas.length > 0 ? areas : null,\n };\n setFailure(null);\n let result: MaybePromise;\n try {\n result = onSave(scope);\n } catch (error) {\n setFailure(describeFailure(describeError?.(error), labels.failed));\n return;\n }\n if (!result || typeof result.then !== \"function\") return;\n setPending(true);\n result.then(\n () => setPending(false),\n (error: unknown) => {\n setPending(false);\n setFailure(describeFailure(describeError?.(error), labels.failed));\n },\n );\n };\n\n return (\n <div className=\"space-y-3\">\n {intro && <div className=\"text-sm text-secondary\">{intro}</div>}\n <CheckboxGroup\n legend={labels.languages}\n options={languageOptions(languages)}\n value={locales}\n onChange={(next) => {\n setFailure(null);\n setLocales(next);\n }}\n columns={columns}\n hint={locked ? undefined : locales.length === 0 ? labels.none : labels.hint}\n disabledReason={disabledReason}\n />\n {areaOptions.length > 0 && (\n <CheckboxGroup\n legend={labels.areas}\n options={areaOptions.map((area) => ({ value: area.value, label: area.label, hint: area.hint }))}\n value={nextAreas}\n onChange={(next) => {\n setFailure(null);\n setAreas(next);\n }}\n // An area narrows a language scope; with no language there is none to narrow.\n disabled={locales.length === 0}\n disabledReason={disabledReason}\n />\n )}\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\">\n {failure}\n </AlertBanner>\n )}\n {locked ? (\n <Caption>{disabledReason}</Caption>\n ) : (\n <FormActions onSubmit={save} onCancel={onCancel} pending={pending} submitDisabled={!dirty} commit={commit} />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The English of every `rateLimited` label (0.30.0): "Too many attempts. Try again in
|
|
3
|
+
* 30 s." with a wait under a minute, in whole minutes from one minute up (keksdose's
|
|
4
|
+
* reset throttle answers in hours, and "3600 s" is a number nobody reads), and "Wait a
|
|
5
|
+
* moment and try again." without one — or with a `0`.
|
|
6
|
+
*
|
|
7
|
+
* @internal The namespaces' English; not part of the barrel.
|
|
8
|
+
*/
|
|
9
|
+
export declare function englishRateLimited(seconds?: number): string;
|
|
10
|
+
/** "30 s", "2 min" — a wait in English, rounded up. @internal */
|
|
11
|
+
export declare function englishWait(seconds: number): string;
|