@eifi1/ui-kit 0.51.6 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -26
- package/dist/account/account-parts.d.ts +10 -8
- package/dist/account/account-parts.js +1 -18
- package/dist/account/account-parts.js.map +1 -1
- package/dist/account/data-export-setting-labels.js +1 -1
- package/dist/account/data-export-setting-labels.js.map +1 -1
- package/dist/account/delete-account-setting-labels.js +1 -1
- package/dist/account/delete-account-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting-labels.js +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/passkeys-setting.d.ts +9 -2
- package/dist/account/passkeys-setting.js +41 -29
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +3 -2
- package/dist/account/sessions-setting.js +10 -8
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +3 -7
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-parts.d.ts +6 -3
- package/dist/admin/admin-parts.js +6 -21
- package/dist/admin/admin-parts.js.map +1 -1
- package/dist/admin/mail-test-card-labels.js +1 -1
- package/dist/admin/mail-test-card-labels.js.map +1 -1
- package/dist/admin/plan-change-labels.d.ts +2 -2
- package/dist/admin/plan-change-labels.js +1 -17
- package/dist/admin/plan-change-labels.js.map +1 -1
- package/dist/admin/plan-change.d.ts +3 -42
- package/dist/admin/plan-change.js +1 -1
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +3 -3
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/auth/auth-errors-labels.d.ts +11 -0
- package/dist/auth/auth-errors-labels.js +13 -0
- package/dist/auth/auth-errors-labels.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +7 -23
- package/dist/auth/auth-errors.js +2 -15
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.d.ts +3 -2
- package/dist/auth/complete-name-dialog.js +3 -5
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/forgot-password-form-labels.js +1 -1
- package/dist/auth/forgot-password-form-labels.js.map +1 -1
- package/dist/auth/forgot-password-form.js +7 -2
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form-labels.js +1 -1
- package/dist/auth/register-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form-labels.js +1 -1
- package/dist/auth/reset-password-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form.js +5 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form-labels.js +1 -1
- package/dist/auth/sign-in-form-labels.js.map +1 -1
- package/dist/auth/status-parts.d.ts +0 -3
- package/dist/auth/status-parts.js +0 -4
- package/dist/auth/status-parts.js.map +1 -1
- package/dist/auth/verify-email-labels.js +1 -1
- package/dist/auth/verify-email-labels.js.map +1 -1
- package/dist/auth/verify-email.js +7 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/cancel-request.d.ts +11 -2
- package/dist/billing/cancel-request.js +15 -3
- package/dist/billing/cancel-request.js.map +1 -1
- package/dist/billing/cancel-subscription.d.ts +22 -10
- package/dist/billing/cancel-subscription.js +8 -11
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/dialog-close.d.ts +29 -0
- package/dist/billing/dialog-close.js +8 -0
- package/dist/billing/dialog-close.js.map +1 -0
- package/dist/billing/plan-card.d.ts +14 -4
- package/dist/billing/plan-card.js +15 -8
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-change-labels.d.ts +42 -0
- package/dist/billing/plan-change-labels.js +22 -0
- package/dist/billing/plan-change-labels.js.map +1 -0
- package/dist/billing/plan-switch.d.ts +21 -9
- package/dist/billing/plan-switch.js +10 -8
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +12 -0
- package/dist/chart.js +16 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +5 -5
- package/dist/components/account-chips.js +11 -13
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +16 -5
- package/dist/components/alert-banner.js +46 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +22 -4
- package/dist/components/amount-input.js +3 -3
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +57 -65
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +12 -2
- package/dist/components/bulk-action-bar.js +65 -21
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-look.d.ts +5 -5
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.d.ts +21 -2
- package/dist/components/calculator.js +53 -14
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-edge.d.ts +16 -0
- package/dist/components/chart-edge.js +14 -1
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-figure.d.ts +13 -0
- package/dist/components/chart-figure.js +15 -0
- package/dist/components/chart-figure.js.map +1 -0
- package/dist/components/chart-zoom-core.js +1 -4
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +8 -1
- package/dist/components/chart.js +19 -10
- package/dist/components/chart.js.map +1 -1
- package/dist/components/combobox-core.d.ts +119 -7
- package/dist/components/combobox-core.js +151 -46
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +142 -131
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +6 -5
- package/dist/components/copy-button.js +5 -3
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.js +2 -2
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +1 -1
- package/dist/components/data-table-column-settings.js +33 -7
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +4 -2
- package/dist/components/data-table-filters.js +2 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +6 -4
- package/dist/components/data-table-fit.js +4 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +1 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +1 -1
- package/dist/components/data-table-head.js +15 -12
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +36 -7
- package/dist/components/data-table-mobile.js +63 -31
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js +11 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-resize.d.ts +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +6 -2
- package/dist/components/data-table-row.js +17 -10
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-selection.d.ts +1 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +10 -2
- package/dist/components/data-table-shared.js +18 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +21 -2
- package/dist/components/data-table-sort.js +24 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +1 -1
- package/dist/components/data-table-virtual.js +18 -6
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +25 -8
- package/dist/components/data-table.js +154 -25
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +24 -2
- package/dist/components/date-field.js +33 -8
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +15 -2
- package/dist/components/date-picker.js +55 -26
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.d.ts +47 -20
- package/dist/components/date-range-picker.js +23 -7
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +2 -1
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/entity-combobox.js +4 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js +37 -30
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
- package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
- package/dist/components/field-base-labels.js.map +1 -0
- package/dist/components/field-base.d.ts +4 -3
- package/dist/components/field-base.js +3 -3
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-sync-labels.d.ts +1 -0
- package/dist/components/field-sync-labels.js.map +1 -1
- package/dist/components/field-sync.d.ts +23 -1
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/form-actions.js +11 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +5 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +14 -9
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.d.ts +43 -0
- package/dist/components/inline-confirm.js +67 -0
- package/dist/components/inline-confirm.js.map +1 -0
- package/dist/components/inline-entity-combobox.d.ts +4 -3
- package/dist/components/inline-entity-combobox.js +150 -141
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +28 -112
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-page.d.ts +3 -1
- package/dist/components/legal-page.js +3 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +4 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.d.ts +2 -2
- package/dist/components/lightbox.js +17 -6
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/list.d.ts +6 -2
- package/dist/components/list.js +4 -3
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +1 -1
- package/dist/components/measured-grid.js +2 -1
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/mini-calendar.js +3 -5
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +24 -0
- package/dist/components/modal.js +12 -4
- package/dist/components/modal.js.map +1 -1
- package/dist/components/money-draft.js +7 -5
- package/dist/components/money-draft.js.map +1 -1
- package/dist/components/money-field.d.ts +2 -2
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-grid.d.ts +55 -0
- package/dist/components/month-grid.js +148 -0
- package/dist/components/month-grid.js.map +1 -0
- package/dist/components/month-picker.d.ts +36 -4
- package/dist/components/month-picker.js +123 -146
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -1
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/multi-select.js +11 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +6 -4
- package/dist/components/number-field.js +3 -2
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +4 -1
- package/dist/components/number-input.js +3 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +7 -0
- package/dist/components/pie-chart-labels.js +1 -0
- package/dist/components/pie-chart-labels.js.map +1 -1
- package/dist/components/pie-chart.d.ts +5 -3
- package/dist/components/pie-chart.js +7 -22
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +3 -0
- package/dist/components/popover.js +7 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/reauth-dialog.d.ts +4 -5
- package/dist/components/reauth-dialog.js +1 -4
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/row-actions.js +2 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +7 -0
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +10 -3
- package/dist/components/series-chart-axes.js +1 -0
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +7 -4
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +31 -7
- package/dist/components/series-chart.js +72 -29
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +3 -3
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip-labels.d.ts +1 -0
- package/dist/components/sign-chip-labels.js.map +1 -1
- package/dist/components/sign-chip.d.ts +10 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/tabs-labels.d.ts +2 -0
- package/dist/components/tabs-labels.js +11 -0
- package/dist/components/tabs-labels.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/tabs.js +13 -11
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +2 -2
- package/dist/components/toggle-legend.js +6 -6
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -2
- package/dist/components/translation-review-editor.js +2 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.d.ts +2 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap-labels.d.ts +26 -0
- package/dist/components/treemap-labels.js +10 -0
- package/dist/components/treemap-labels.js.map +1 -0
- package/dist/components/treemap-layout.d.ts +62 -0
- package/dist/components/treemap-layout.js +126 -0
- package/dist/components/treemap-layout.js.map +1 -0
- package/dist/components/treemap.d.ts +140 -17
- package/dist/components/treemap.js +324 -97
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.d.ts +5 -5
- package/dist/components/use-table-state.js +4 -1
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +6 -2
- package/dist/data-table.js +7 -2
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +36 -7
- package/dist/eslint/role-utilities.js +42 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +25 -16
- package/dist/feedback/feedback-dialog.js +6 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-mutation.d.ts +30 -0
- package/dist/feedback/feedback-mutation.js +16 -0
- package/dist/feedback/feedback-mutation.js.map +1 -0
- package/dist/feedback/feedback-page-link.d.ts +53 -0
- package/dist/feedback/feedback-page-link.js +37 -0
- package/dist/feedback/feedback-page-link.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +12 -3
- package/dist/feedback/feedback-public-list.js +2 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +3 -1
- package/dist/feedback/feedback-replies.js +5 -4
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +5 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +64 -28
- package/dist/feedback/feedback-row-detail.js +30 -28
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +30 -11
- package/dist/feedback/feedback-status-undo.js +10 -15
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
- package/dist/feedback/feedback-submit-dialog.js +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.js +9 -3
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +1 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +21 -9
- package/dist/feedback/feedback-table.js +45 -25
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-labels.d.ts +2 -2
- package/dist/feedback/feedback-thread-labels.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +24 -23
- package/dist/feedback/feedback-thread.js +2 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +34 -0
- package/dist/feedback.js +42 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +35 -0
- package/dist/hooks/use-focus-trap.js +6 -4
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-hotkey.d.ts +5 -2
- package/dist/hooks/use-hotkey.js +5 -4
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +7 -4
- package/dist/hooks/use-long-press.js +3 -0
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-media-query.js +12 -4
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-mounted.d.ts +22 -0
- package/dist/hooks/use-mounted.js +24 -0
- package/dist/hooks/use-mounted.js.map +1 -0
- package/dist/hooks/use-overlay-history.d.ts +7 -1
- package/dist/hooks/use-overlay-history.js +35 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +4 -5
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +7 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +20 -41
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -10
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +8 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +5 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +64 -17
- package/dist/index.js +77 -12
- package/dist/index.js.map +1 -1
- package/dist/landing/routing.d.ts +3 -1
- package/dist/landing/routing.js +4 -7
- package/dist/landing/routing.js.map +1 -1
- package/dist/lib/broadcast.d.ts +17 -0
- package/dist/lib/broadcast.js +30 -0
- package/dist/lib/broadcast.js.map +1 -0
- package/dist/lib/calc.d.ts +7 -1
- package/dist/lib/calc.js +3 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.d.ts +2 -2
- package/dist/lib/clipboard-minus.js +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/date-format.d.ts +37 -2
- package/dist/lib/date-format.js +34 -8
- package/dist/lib/date-format.js.map +1 -1
- package/dist/lib/day-format.d.ts +46 -0
- package/dist/lib/day-format.js +46 -0
- package/dist/lib/day-format.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +7 -1
- package/dist/lib/decimal-marks.js +5 -2
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/describe-failure.d.ts +14 -0
- package/dist/lib/describe-failure.js +9 -0
- package/dist/lib/describe-failure.js.map +1 -0
- package/dist/lib/direction.d.ts +16 -0
- package/dist/lib/direction.js +9 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/focus-left.d.ts +63 -0
- package/dist/lib/focus-left.js +67 -0
- package/dist/lib/focus-left.js.map +1 -0
- package/dist/lib/format.d.ts +5 -3
- package/dist/lib/format.js +16 -13
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/http-status.d.ts +24 -0
- package/dist/lib/http-status.js +14 -0
- package/dist/lib/http-status.js.map +1 -0
- package/dist/lib/intl-cache.d.ts +14 -0
- package/dist/lib/intl-cache.js +15 -0
- package/dist/lib/intl-cache.js.map +1 -0
- package/dist/lib/isin.d.ts +10 -0
- package/dist/lib/isin.js.map +1 -1
- package/dist/lib/iso-day.d.ts +11 -0
- package/dist/lib/iso-day.js +8 -0
- package/dist/lib/iso-day.js.map +1 -0
- package/dist/lib/lazy-with-fallback.d.ts +31 -0
- package/dist/lib/lazy-with-fallback.js +40 -0
- package/dist/lib/lazy-with-fallback.js.map +1 -0
- package/dist/lib/path-pattern.d.ts +34 -0
- package/dist/lib/path-pattern.js +24 -0
- package/dist/lib/path-pattern.js.map +1 -0
- package/dist/lib/reduced-motion.d.ts +2 -1
- package/dist/lib/reduced-motion.js.map +1 -1
- package/dist/lib/round.d.ts +2 -1
- package/dist/lib/round.js +2 -1
- package/dist/lib/round.js.map +1 -1
- package/dist/lib/safe-storage.d.ts +24 -5
- package/dist/lib/safe-storage.js +27 -2
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/lib/session-refresh.js +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/share-format.d.ts +13 -0
- package/dist/lib/share-format.js +10 -0
- package/dist/lib/share-format.js.map +1 -0
- package/dist/lib/text-case.d.ts +17 -0
- package/dist/lib/text-case.js +8 -0
- package/dist/lib/text-case.js.map +1 -0
- package/dist/lib/text-match.d.ts +38 -0
- package/dist/lib/text-match.js +24 -2
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/thenable.d.ts +13 -0
- package/dist/lib/thenable.js +8 -0
- package/dist/lib/thenable.js.map +1 -0
- package/dist/lib/translation-review.d.ts +7 -2
- package/dist/lib/translation-review.js +4 -4
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/device-zone-report.js +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +2 -1
- package/dist/notifications/notification-bell.js +3 -3
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +10 -6
- package/dist/notifications/notification-inbox.js +26 -48
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +3 -2
- package/dist/notifications/notification-parts.js +1 -3
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.d.ts +6 -0
- package/dist/notifications/notifications-labels.js +1 -0
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +1 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +2 -1
- package/dist/notifications/push-devices-setting.js +27 -49
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +10 -3
- package/dist/notifications/quiet-hours-setting.js +65 -26
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/unsubscribe-panel.js +4 -7
- package/dist/notifications/unsubscribe-panel.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +39 -1
- package/dist/notifications/use-push-device.js +28 -7
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/query-persistence.d.ts +3 -1
- package/dist/offline/query-persistence.js +19 -2
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/rhf/fields.d.ts +11 -4
- package/dist/rhf/fields.js.map +1 -1
- package/dist/search/command-palette.js +1 -1
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +19 -9
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/use-command-key.d.ts +12 -1
- package/dist/search/use-command-key.js +3 -12
- package/dist/search/use-command-key.js.map +1 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.js +10 -0
- package/dist/search.js.map +1 -1
- package/dist/settings/use-settings-focus.js +1 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +21 -5
- package/dist/shell/account-menu-language.js +3 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +26 -9
- package/dist/shell/account-menu-text-size.js +3 -2
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/dev-menu.js +5 -18
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +7 -0
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +1 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +0 -2
- package/dist/shell/top-bar-parts-labels.js +0 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +27 -8
- package/dist/shell.js +28 -7
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +30 -4
- package/dist/sync/engine.js +172 -52
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +6 -2
- package/dist/sync.js +6 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js +3 -8
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -1
- package/dist/theme/contrast.js +3 -8
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-store.js +6 -6
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +3 -8
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +3 -6
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.d.ts +7 -4
- package/dist/tour/tour-catalogue.js +12 -10
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +0 -2
- package/dist/tour/tour-dock.js +0 -2
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-gates.d.ts +4 -0
- package/dist/tour/tour-gates.js +14 -0
- package/dist/tour/tour-gates.js.map +1 -1
- package/dist/tour/tour-overlay.d.ts +27 -0
- package/dist/tour/tour-overlay.js +741 -0
- package/dist/tour/tour-overlay.js.map +1 -0
- package/dist/tour/tour.d.ts +16 -4
- package/dist/tour/tour.js +62 -778
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js +2 -2
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +6 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.d.ts +3 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.d.ts +3 -1
- package/dist/wizard/validation.js.map +1 -1
- package/package.json +12 -8
- package/src/account/account-parts.ts +5 -25
- package/src/account/data-export-setting-labels.ts +1 -1
- package/src/account/delete-account-setting-labels.ts +1 -1
- package/src/account/email-change-setting-labels.ts +1 -1
- package/src/account/passkeys-setting.tsx +60 -38
- package/src/account/sessions-setting.tsx +18 -12
- package/src/admin/admin-action-confirm.tsx +5 -10
- package/src/admin/admin-parts.ts +11 -25
- package/src/admin/mail-test-card-labels.ts +1 -1
- package/src/admin/plan-change-labels.ts +5 -23
- package/src/admin/plan-change.tsx +7 -44
- package/src/admin/reviewer-scope-editor.tsx +3 -3
- package/src/auth/auth-errors-labels.ts +23 -0
- package/src/auth/auth-errors.ts +11 -38
- package/src/auth/complete-name-dialog.tsx +9 -8
- package/src/auth/forgot-password-form-labels.ts +1 -1
- package/src/auth/forgot-password-form.tsx +11 -6
- package/src/auth/register-form-labels.ts +1 -1
- package/src/auth/reset-password-form-labels.ts +1 -1
- package/src/auth/reset-password-form.tsx +8 -4
- package/src/auth/sign-in-form-labels.ts +1 -1
- package/src/auth/status-parts.tsx +0 -6
- package/src/auth/verify-email-labels.ts +1 -1
- package/src/auth/verify-email.tsx +11 -6
- package/src/billing/cancel-request.ts +28 -6
- package/src/billing/cancel-subscription.tsx +29 -22
- package/src/billing/dialog-close.ts +34 -0
- package/src/billing/plan-card.tsx +36 -11
- package/src/billing/plan-change-labels.ts +66 -0
- package/src/billing/plan-switch.tsx +31 -20
- package/src/chart.ts +21 -3
- package/src/components/account-chips.tsx +20 -23
- package/src/components/alert-banner.tsx +82 -5
- package/src/components/amount-input.tsx +32 -14
- package/src/components/autocomplete.tsx +64 -70
- package/src/components/bulk-action-bar.tsx +70 -2
- package/src/components/button-look.ts +5 -5
- package/src/components/button.tsx +2 -2
- package/src/components/calculator.tsx +98 -17
- package/src/components/chart-edge.ts +34 -1
- package/src/components/chart-figure.tsx +35 -0
- package/src/components/chart-zoom-core.tsx +6 -5
- package/src/components/chart.tsx +26 -5
- package/src/components/combobox-core.tsx +303 -69
- package/src/components/combobox.tsx +184 -155
- package/src/components/copy-button.tsx +34 -23
- package/src/components/country-select.tsx +16 -11
- package/src/components/data-table-column-settings.tsx +52 -10
- package/src/components/data-table-filters.ts +6 -4
- package/src/components/data-table-fit.ts +19 -5
- package/src/components/data-table-groups.tsx +1 -1
- package/src/components/data-table-head.tsx +32 -22
- package/src/components/data-table-mobile.tsx +110 -27
- package/src/components/data-table-pagination.tsx +21 -6
- package/src/components/data-table-resize.ts +1 -1
- package/src/components/data-table-row-actions.tsx +3 -3
- package/src/components/data-table-row.tsx +25 -14
- package/src/components/data-table-selection.ts +1 -1
- package/src/components/data-table-shared.tsx +33 -1
- package/src/components/data-table-sort.ts +57 -2
- package/src/components/data-table-virtual.ts +32 -8
- package/src/components/data-table.tsx +267 -41
- package/src/components/date-field.tsx +65 -8
- package/src/components/date-picker.tsx +125 -48
- package/src/components/date-range-picker.tsx +82 -31
- package/src/components/dialog-frame.tsx +2 -1
- package/src/components/entity-combobox.tsx +4 -1
- package/src/components/error-boundary.tsx +53 -39
- package/src/components/field-base-labels.ts +20 -0
- package/src/components/field-base.tsx +13 -7
- package/src/components/field-sync-labels.ts +1 -0
- package/src/components/field-sync.tsx +23 -1
- package/src/components/form-actions.tsx +20 -5
- package/src/components/hover-menu.tsx +18 -8
- package/src/components/icon-button.tsx +14 -9
- package/src/components/inline-confirm.tsx +116 -0
- package/src/components/inline-entity-combobox.tsx +198 -152
- package/src/components/input.tsx +1 -1
- package/src/components/jump-calendar.tsx +30 -146
- package/src/components/legal-page.tsx +10 -7
- package/src/components/legal.tsx +4 -2
- package/src/components/lightbox.tsx +21 -8
- package/src/components/list.tsx +14 -5
- package/src/components/measured-grid.tsx +8 -4
- package/src/components/mini-calendar.tsx +6 -5
- package/src/components/modal.tsx +45 -4
- package/src/components/money-draft.ts +16 -5
- package/src/components/money-field.tsx +2 -2
- package/src/components/month-grid.tsx +229 -0
- package/src/components/month-picker.tsx +192 -200
- package/src/components/multi-entity-combobox.tsx +1 -0
- package/src/components/multi-select.tsx +21 -9
- package/src/components/number-field.tsx +14 -7
- package/src/components/number-input.tsx +9 -3
- package/src/components/numpad-sheet.tsx +4 -3
- package/src/components/picker-sheet.tsx +12 -9
- package/src/components/pie-chart-labels.ts +8 -0
- package/src/components/pie-chart.tsx +22 -28
- package/src/components/popover.tsx +18 -1
- package/src/components/reauth-dialog.tsx +6 -10
- package/src/components/row-actions.tsx +2 -1
- package/src/components/section-nav.tsx +7 -0
- package/src/components/series-chart-axes.ts +11 -3
- package/src/components/series-chart-labels.ts +7 -4
- package/src/components/series-chart.tsx +178 -69
- package/src/components/share-card.tsx +6 -6
- package/src/components/sign-chip-labels.ts +1 -0
- package/src/components/sign-chip.tsx +10 -1
- package/src/components/tabs-labels.ts +13 -0
- package/src/components/tabs.tsx +35 -19
- package/src/components/toggle-legend.tsx +16 -11
- package/src/components/translation-review-editor.tsx +6 -5
- package/src/components/translation-review.tsx +4 -3
- package/src/components/treemap-labels.ts +39 -0
- package/src/components/treemap-layout.ts +210 -0
- package/src/components/treemap.tsx +722 -133
- package/src/components/use-table-state.ts +12 -9
- package/src/data-table.ts +9 -2
- package/src/eslint/role-utilities.ts +97 -19
- package/src/feedback/feedback-dialog.tsx +30 -19
- package/src/feedback/feedback-mutation.ts +41 -0
- package/src/feedback/feedback-page-link.ts +90 -0
- package/src/feedback/feedback-public-list.tsx +17 -5
- package/src/feedback/feedback-replies.tsx +13 -6
- package/src/feedback/feedback-row-detail-labels.ts +5 -0
- package/src/feedback/feedback-row-detail.tsx +103 -67
- package/src/feedback/feedback-status-undo.ts +41 -27
- package/src/feedback/feedback-submit-dialog.tsx +2 -2
- package/src/feedback/feedback-submit.tsx +21 -7
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/feedback/feedback-table.tsx +86 -38
- package/src/feedback/feedback-thread-labels.ts +2 -2
- package/src/feedback/feedback-thread.tsx +27 -25
- package/src/feedback.ts +42 -0
- package/src/hooks/use-dismiss.ts +3 -2
- package/src/hooks/use-focus-trap.ts +8 -24
- package/src/hooks/use-hotkey.ts +24 -7
- package/src/hooks/use-long-press.ts +16 -2
- package/src/hooks/use-media-query.ts +20 -4
- package/src/hooks/use-mounted.ts +39 -0
- package/src/hooks/use-overlay-history.ts +70 -5
- package/src/i18n/defaults.ts +4 -5
- package/src/i18n/german.ts +8 -2
- package/src/i18n/kit-labels.tsx +9 -2
- package/src/i18n/locales/es.ts +8 -2
- package/src/i18n/locales/fr.ts +21 -13
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +8 -2
- package/src/i18n/locales/zh.ts +9 -2
- package/src/i18n/review.ts +5 -0
- package/src/index.ts +103 -13
- package/src/landing/routing.tsx +14 -10
- package/src/lib/broadcast.ts +52 -0
- package/src/lib/calc.ts +13 -2
- package/src/lib/clipboard-minus.ts +14 -4
- package/src/lib/date-format.ts +89 -13
- package/src/lib/day-format.ts +96 -0
- package/src/lib/decimal-marks.ts +12 -3
- package/src/lib/describe-failure.ts +18 -0
- package/src/lib/direction.ts +39 -0
- package/src/lib/focus-left.ts +156 -0
- package/src/lib/format.ts +34 -19
- package/src/lib/http-status.ts +38 -0
- package/src/lib/intl-cache.ts +25 -0
- package/src/lib/isin.ts +10 -0
- package/src/lib/iso-day.ts +13 -0
- package/src/lib/lazy-with-fallback.ts +82 -0
- package/src/lib/path-pattern.ts +55 -0
- package/src/lib/reduced-motion.ts +2 -1
- package/src/lib/round.ts +7 -2
- package/src/lib/safe-storage.ts +48 -6
- package/src/lib/session-refresh.ts +1 -1
- package/src/lib/share-format.ts +18 -0
- package/src/lib/text-case.ts +19 -0
- package/src/lib/text-match.ts +69 -2
- package/src/lib/thenable.ts +19 -0
- package/src/lib/translation-review.ts +13 -5
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-bell.tsx +7 -4
- package/src/notifications/notification-inbox.tsx +40 -58
- package/src/notifications/notification-parts.tsx +3 -4
- package/src/notifications/notification-types-setting.tsx +1 -1
- package/src/notifications/notifications-labels.ts +7 -0
- package/src/notifications/push-device-setting.tsx +1 -1
- package/src/notifications/push-devices-setting.tsx +30 -55
- package/src/notifications/quiet-hours-setting.tsx +93 -31
- package/src/notifications/unsubscribe-panel.tsx +4 -7
- package/src/notifications/use-push-device.ts +76 -10
- package/src/offline/query-persistence.ts +35 -3
- package/src/rhf/fields.tsx +11 -4
- package/src/search/command-palette.tsx +2 -4
- package/src/search/global-search.tsx +36 -15
- package/src/search/use-command-key.ts +13 -12
- package/src/search.ts +9 -0
- package/src/settings/use-settings-focus.ts +3 -0
- package/src/shell/account-menu-language.tsx +27 -5
- package/src/shell/account-menu-text-size.tsx +32 -10
- package/src/shell/dev-menu.tsx +7 -21
- package/src/shell/role-switcher.tsx +7 -0
- package/src/shell/top-bar-actions.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +0 -3
- package/src/shell.ts +40 -9
- package/src/sync/engine.ts +339 -61
- package/src/sync.ts +8 -1
- package/src/testing/css-variables.ts +8 -9
- package/src/theme/contrast.ts +8 -9
- package/src/theme/palette-store.ts +9 -9
- package/src/theme/text-size.ts +6 -8
- package/src/theme/theme-store.ts +7 -9
- package/src/tour/tour-catalogue.tsx +26 -15
- package/src/tour/tour-dock.ts +0 -2
- package/src/tour/tour-gates.ts +23 -0
- package/src/tour/tour-overlay.tsx +1170 -0
- package/src/tour/tour.tsx +116 -1193
- package/src/tour/use-tour-progress.ts +1 -1
- package/src/tour/use-tour-url-sync.ts +7 -3
- package/src/wizard/stepper-nav.tsx +3 -1
- package/src/wizard/types.ts +11 -2
- package/src/wizard/use-wizard.ts +2 -1
- package/src/wizard/validation.ts +8 -2
- package/tokens.css +2 -1
- package/dist/components/ui-labels.js.map +0 -1
- package/dist/shell/dev-switchers.d.ts +0 -6
- package/dist/shell/dev-switchers.js +0 -12
- package/dist/shell/dev-switchers.js.map +0 -1
- package/src/components/ui-labels.ts +0 -24
- package/src/shell/dev-switchers.tsx +0 -18
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
/** What a {@link lazyWithFallback} fallback is given besides the component's own props. */
|
|
3
|
+
export interface LazyFallbackProps {
|
|
4
|
+
/** What the import rejected with. Already on `console.error`. */
|
|
5
|
+
error: unknown;
|
|
6
|
+
/** Fetch the module again and draw it here if it comes (back online, the deploy's
|
|
7
|
+
* new chunk). Call it on the person's next attempt, never at once: offline, a retry
|
|
8
|
+
* on mount would fetch in a loop. */
|
|
9
|
+
retry: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `React.lazy` for a module the kit loads late (refactor audit 2026-10, MOD-1: the tour
|
|
13
|
+
* overlay, the search palette, the feedback dialog), where an import that fails — a tab
|
|
14
|
+
* left open across a deploy, a local-first app offline — draws `Fallback` instead of
|
|
15
|
+
* throwing.
|
|
16
|
+
*
|
|
17
|
+
* `React.lazy` keeps what its import settled to, so one failed fetch threw on every render
|
|
18
|
+
* for as long as the component stayed mounted, to the nearest error boundary. All three
|
|
19
|
+
* sit above the routes — in the provider every app mounts, in the top bar — where that
|
|
20
|
+
* boundary is the whole app's: a white page because somebody pressed ⌘K or "Take the
|
|
21
|
+
* tour" (0.52 code review, tour.tsx: the tour's own fix, written by hand with a module-level
|
|
22
|
+
* `let`, made shared here).
|
|
23
|
+
*
|
|
24
|
+
* A failure is logged and the next mount (or the fallback's `retry`) fetches again; a
|
|
25
|
+
* re-render of a mounted fallback does not. `load` resolves to the component itself,
|
|
26
|
+
* through a LITERAL relative dynamic import of its module (as feedback-submit's), so an
|
|
27
|
+
* app's bundler can split it off. Render the result under a `Suspense`, as a lazy
|
|
28
|
+
* component. (No import call is spelled out here: the build's fix-esm-extensions step
|
|
29
|
+
* reads specifiers in the .d.ts comments too, and would try to resolve an example.)
|
|
30
|
+
*/
|
|
31
|
+
export declare function lazyWithFallback<P extends object>(load: () => Promise<ComponentType<P>>, Fallback: ComponentType<P & LazyFallbackProps>): ComponentType<P>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, createElement, lazy, useCallback, useContext, useLayoutEffect, useState } from "react";
|
|
3
|
+
const RetryContext = createContext(() => {
|
|
4
|
+
});
|
|
5
|
+
function lazyWithFallback(load, Fallback) {
|
|
6
|
+
const attempt = () => {
|
|
7
|
+
const made = {
|
|
8
|
+
spent: false,
|
|
9
|
+
Component: lazy(
|
|
10
|
+
() => load().then(
|
|
11
|
+
(Loaded) => ({ default: Loaded }),
|
|
12
|
+
(error) => {
|
|
13
|
+
console.error(error);
|
|
14
|
+
function Failed(props) {
|
|
15
|
+
const retry = useContext(RetryContext);
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
made.spent = true;
|
|
18
|
+
}, []);
|
|
19
|
+
return createElement(Fallback, { ...props, error, retry });
|
|
20
|
+
}
|
|
21
|
+
return { default: Failed };
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
};
|
|
26
|
+
return made;
|
|
27
|
+
};
|
|
28
|
+
let latest = attempt();
|
|
29
|
+
const take = () => latest.spent ? latest = attempt() : latest;
|
|
30
|
+
function LazyWithFallback(props) {
|
|
31
|
+
const [current, setCurrent] = useState(take);
|
|
32
|
+
const retry = useCallback(() => setCurrent(take), []);
|
|
33
|
+
return createElement(RetryContext.Provider, { value: retry }, createElement(current.Component, props));
|
|
34
|
+
}
|
|
35
|
+
return LazyWithFallback;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
lazyWithFallback
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=lazy-with-fallback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/lazy-with-fallback.ts"],"sourcesContent":["import { createContext, createElement, lazy, useCallback, useContext, useLayoutEffect, useState } from \"react\";\nimport type { ComponentType, LazyExoticComponent } from \"react\";\n\n/** What a {@link lazyWithFallback} fallback is given besides the component's own props. */\nexport interface LazyFallbackProps {\n /** What the import rejected with. Already on `console.error`. */\n error: unknown;\n /** Fetch the module again and draw it here if it comes (back online, the deploy's\n * new chunk). Call it on the person's next attempt, never at once: offline, a retry\n * on mount would fetch in a loop. */\n retry: () => void;\n}\n\ninterface Attempt<P> {\n Component: LazyExoticComponent<ComponentType<P>>;\n /** Its import failed and its fallback was drawn: the next mount, or `retry`, takes a\n * fresh attempt. Marked when the fallback COMMITS, not when the import rejects: a\n * first mount that suspended is thrown away and rendered anew, and an attempt marked\n * at the rejection would hand that re-render a fresh import — which fails, and so on. */\n spent: boolean;\n}\n\nconst RetryContext = createContext<() => void>(() => {});\n\n/**\n * `React.lazy` for a module the kit loads late (refactor audit 2026-10, MOD-1: the tour\n * overlay, the search palette, the feedback dialog), where an import that fails — a tab\n * left open across a deploy, a local-first app offline — draws `Fallback` instead of\n * throwing.\n *\n * `React.lazy` keeps what its import settled to, so one failed fetch threw on every render\n * for as long as the component stayed mounted, to the nearest error boundary. All three\n * sit above the routes — in the provider every app mounts, in the top bar — where that\n * boundary is the whole app's: a white page because somebody pressed ⌘K or \"Take the\n * tour\" (0.52 code review, tour.tsx: the tour's own fix, written by hand with a module-level\n * `let`, made shared here).\n *\n * A failure is logged and the next mount (or the fallback's `retry`) fetches again; a\n * re-render of a mounted fallback does not. `load` resolves to the component itself,\n * through a LITERAL relative dynamic import of its module (as feedback-submit's), so an\n * app's bundler can split it off. Render the result under a `Suspense`, as a lazy\n * component. (No import call is spelled out here: the build's fix-esm-extensions step\n * reads specifiers in the .d.ts comments too, and would try to resolve an example.)\n */\nexport function lazyWithFallback<P extends object>(\n load: () => Promise<ComponentType<P>>,\n Fallback: ComponentType<P & LazyFallbackProps>,\n): ComponentType<P> {\n const attempt = (): Attempt<P> => {\n const made: Attempt<P> = {\n spent: false,\n Component: lazy<ComponentType<P>>(() =>\n load().then(\n (Loaded) => ({ default: Loaded }),\n (error: unknown) => {\n console.error(error);\n function Failed(props: P) {\n const retry = useContext(RetryContext);\n useLayoutEffect(() => {\n made.spent = true;\n }, []);\n return createElement(Fallback, { ...props, error, retry });\n }\n return { default: Failed };\n },\n ),\n ),\n };\n return made;\n };\n let latest = attempt();\n /** The attempt a new mount takes: the latest, or a fresh one once that has failed. */\n const take = () => (latest.spent ? (latest = attempt()) : latest);\n\n function LazyWithFallback(props: P) {\n // Fixed per mount, so a re-render of a mounted fallback fetches nothing.\n const [current, setCurrent] = useState(take);\n const retry = useCallback(() => setCurrent(take), []);\n return createElement(RetryContext.Provider, { value: retry }, createElement(current.Component, props));\n }\n return LazyWithFallback;\n}\n"],"mappings":";AAAA,SAAS,eAAe,eAAe,MAAM,aAAa,YAAY,iBAAiB,gBAAgB;AAsBvG,MAAM,eAAe,cAA0B,MAAM;AAAC,CAAC;AAsBhD,SAAS,iBACd,MACA,UACkB;AAClB,QAAM,UAAU,MAAkB;AAChC,UAAM,OAAmB;AAAA,MACvB,OAAO;AAAA,MACP,WAAW;AAAA,QAAuB,MAChC,KAAK,EAAE;AAAA,UACL,CAAC,YAAY,EAAE,SAAS,OAAO;AAAA,UAC/B,CAAC,UAAmB;AAClB,oBAAQ,MAAM,KAAK;AACnB,qBAAS,OAAO,OAAU;AACxB,oBAAM,QAAQ,WAAW,YAAY;AACrC,8BAAgB,MAAM;AACpB,qBAAK,QAAQ;AAAA,cACf,GAAG,CAAC,CAAC;AACL,qBAAO,cAAc,UAAU,EAAE,GAAG,OAAO,OAAO,MAAM,CAAC;AAAA,YAC3D;AACA,mBAAO,EAAE,SAAS,OAAO;AAAA,UAC3B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,MAAI,SAAS,QAAQ;AAErB,QAAM,OAAO,MAAO,OAAO,QAAS,SAAS,QAAQ,IAAK;AAE1D,WAAS,iBAAiB,OAAU;AAElC,UAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,UAAM,QAAQ,YAAY,MAAM,WAAW,IAAI,GAAG,CAAC,CAAC;AACpD,WAAO,cAAc,aAAa,UAAU,EAAE,OAAO,MAAM,GAAG,cAAc,QAAQ,WAAW,KAAK,CAAC;AAAA,EACvG;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a pathname is one a `PathPattern` names — the rule `useLastVisitedPage`'s
|
|
3
|
+
* `exclude` reads (landing/routing.tsx) and the feedback guard's one-shot routes
|
|
4
|
+
* (refactor audit 2026-10-30, FLW-7), here without the router: `landing/routing` imports
|
|
5
|
+
* react-router, which `/feedback` must not drag in. The one copy: routing kept a private
|
|
6
|
+
* one, and the two had drifted within the round (0.52 code review, this file).
|
|
7
|
+
*
|
|
8
|
+
* A string is that path and everything under it (`/join` is `/join` and `/join/abc`, not
|
|
9
|
+
* `/joined`; `/` is only `/`); a RegExp is tested on the pathname (a `g` flag dropped, so
|
|
10
|
+
* `lastIndex` never carries over between calls).
|
|
11
|
+
*
|
|
12
|
+
* Case-INSENSITIVE, as React Router matches a route unless it sets `caseSensitive` (0.52
|
|
13
|
+
* code review, feedback/feedback-page-link.ts): the FLW-7 guard compared case-sensitively,
|
|
14
|
+
* and `/Join?token=` went out as a link the router still rendered as `/join`. A RegExp
|
|
15
|
+
* gets the `i` flag for the same reason. Matching more than a case-sensitive route would
|
|
16
|
+
* only turns a link into text, or leaves a page unrecorded as a resume target: both
|
|
17
|
+
* callers' safe side.
|
|
18
|
+
*
|
|
19
|
+
* @internal Package-private — not re-exported from the barrel.
|
|
20
|
+
*/
|
|
21
|
+
export declare function matchesPathPattern(pathname: string, pattern: string | RegExp): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The pathname of `href` (a path with its query and hash) as React Router matches it:
|
|
24
|
+
* each segment percent-decoded, an encoded `/` kept as `%2F` so it never splits a segment
|
|
25
|
+
* (react-router 8, `decodePath` in `matchRoutesImpl`). The 0.52 code review
|
|
26
|
+
* (feedback/feedback-page-link.ts) found `/j%6Fin?token=` passing the FLW-7 guard as a
|
|
27
|
+
* link that the router decoded to `/join`.
|
|
28
|
+
*
|
|
29
|
+
* A malformed escape leaves its own segment as written, where the router gives up on the
|
|
30
|
+
* whole path: the rest still decodes, so the answer is never less than the router's.
|
|
31
|
+
*
|
|
32
|
+
* @internal Package-private — not re-exported from the barrel.
|
|
33
|
+
*/
|
|
34
|
+
export declare function routerPathname(href: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
function matchesPathPattern(pathname, pattern) {
|
|
3
|
+
if (typeof pattern !== "string") {
|
|
4
|
+
return new RegExp(pattern.source, `${pattern.flags.replace(/[gi]/g, "")}i`).test(pathname);
|
|
5
|
+
}
|
|
6
|
+
const path = pathname.toLowerCase();
|
|
7
|
+
if (pattern === "/") return path === "/";
|
|
8
|
+
const base = (pattern.endsWith("/") ? pattern.slice(0, -1) : pattern).toLowerCase();
|
|
9
|
+
return path === base || path.startsWith(`${base}/`);
|
|
10
|
+
}
|
|
11
|
+
function routerPathname(href) {
|
|
12
|
+
return href.replace(/[?#].*$/, "").split("/").map((segment) => {
|
|
13
|
+
try {
|
|
14
|
+
return decodeURIComponent(segment).replace(/\//g, "%2F");
|
|
15
|
+
} catch {
|
|
16
|
+
return segment;
|
|
17
|
+
}
|
|
18
|
+
}).join("/");
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
matchesPathPattern,
|
|
22
|
+
routerPathname
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=path-pattern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/path-pattern.ts"],"sourcesContent":["/**\n * Whether a pathname is one a `PathPattern` names — the rule `useLastVisitedPage`'s\n * `exclude` reads (landing/routing.tsx) and the feedback guard's one-shot routes\n * (refactor audit 2026-10-30, FLW-7), here without the router: `landing/routing` imports\n * react-router, which `/feedback` must not drag in. The one copy: routing kept a private\n * one, and the two had drifted within the round (0.52 code review, this file).\n *\n * A string is that path and everything under it (`/join` is `/join` and `/join/abc`, not\n * `/joined`; `/` is only `/`); a RegExp is tested on the pathname (a `g` flag dropped, so\n * `lastIndex` never carries over between calls).\n *\n * Case-INSENSITIVE, as React Router matches a route unless it sets `caseSensitive` (0.52\n * code review, feedback/feedback-page-link.ts): the FLW-7 guard compared case-sensitively,\n * and `/Join?token=` went out as a link the router still rendered as `/join`. A RegExp\n * gets the `i` flag for the same reason. Matching more than a case-sensitive route would\n * only turns a link into text, or leaves a page unrecorded as a resume target: both\n * callers' safe side.\n *\n * @internal Package-private — not re-exported from the barrel.\n */\nexport function matchesPathPattern(pathname: string, pattern: string | RegExp): boolean {\n if (typeof pattern !== \"string\") {\n return new RegExp(pattern.source, `${pattern.flags.replace(/[gi]/g, \"\")}i`).test(pathname);\n }\n const path = pathname.toLowerCase();\n if (pattern === \"/\") return path === \"/\";\n const base = (pattern.endsWith(\"/\") ? pattern.slice(0, -1) : pattern).toLowerCase();\n return path === base || path.startsWith(`${base}/`);\n}\n\n/**\n * The pathname of `href` (a path with its query and hash) as React Router matches it:\n * each segment percent-decoded, an encoded `/` kept as `%2F` so it never splits a segment\n * (react-router 8, `decodePath` in `matchRoutesImpl`). The 0.52 code review\n * (feedback/feedback-page-link.ts) found `/j%6Fin?token=` passing the FLW-7 guard as a\n * link that the router decoded to `/join`.\n *\n * A malformed escape leaves its own segment as written, where the router gives up on the\n * whole path: the rest still decodes, so the answer is never less than the router's.\n *\n * @internal Package-private — not re-exported from the barrel.\n */\nexport function routerPathname(href: string): string {\n return href\n .replace(/[?#].*$/, \"\")\n .split(\"/\")\n .map((segment) => {\n try {\n return decodeURIComponent(segment).replace(/\\//g, \"%2F\");\n } catch {\n return segment;\n }\n })\n .join(\"/\");\n}\n"],"mappings":";AAoBO,SAAS,mBAAmB,UAAkB,SAAmC;AACtF,MAAI,OAAO,YAAY,UAAU;AAC/B,WAAO,IAAI,OAAO,QAAQ,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,EAAE,CAAC,GAAG,EAAE,KAAK,QAAQ;AAAA,EAC3F;AACA,QAAM,OAAO,SAAS,YAAY;AAClC,MAAI,YAAY,IAAK,QAAO,SAAS;AACrC,QAAM,QAAQ,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,EAAE,IAAI,SAAS,YAAY;AAClF,SAAO,SAAS,QAAQ,KAAK,WAAW,GAAG,IAAI,GAAG;AACpD;AAcO,SAAS,eAAe,MAAsB;AACnD,SAAO,KACJ,QAAQ,WAAW,EAAE,EACrB,MAAM,GAAG,EACT,IAAI,CAAC,YAAY;AAChB,QAAI;AACF,aAAO,mBAAmB,OAAO,EAAE,QAAQ,OAAO,KAAK;AAAA,IACzD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,CAAC,EACA,KAAK,GAAG;AACb;","names":[]}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* screen only delays its `onClose` — every test suite, the kit's and the apps', reads a
|
|
14
14
|
* closed overlay as gone at once.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* Public since 0.52 (DUP-8, refactor audit 2026-10-30): Kurvenschmiede's chain animation
|
|
17
|
+
* carried a copy of its own.
|
|
17
18
|
*/
|
|
18
19
|
export declare function prefersReducedMotion(whereUnknown?: boolean): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/reduced-motion.ts"],"sourcesContent":["/**\n * Whether the user has asked for no motion (`prefers-reduced-motion: reduce`), read NOW —\n * not at mount: the setting can change under a long-lived page. The kit's one copy\n * (DUP-11, refactor audit 2026-10: the settings focus and the exit timers each had their\n * own, with opposite answers where `matchMedia` is missing).\n *\n * Where there is no `matchMedia` to ask (SSR, jsdom) the answer is `whereUnknown`,\n * default `false`: motion allowed — the platform's own default, `no-preference`, which\n * is what a browser that cannot say otherwise has. A smooth scroll there is harmless.\n *\n * An exit animation passes `true` (`skipsExitAnimation` in `hooks/use-close-transition`):\n * without `matchMedia` nothing is painting, and a timer that holds an unpainted panel on\n * screen only delays its `onClose` — every test suite, the kit's and the apps', reads a\n * closed overlay as gone at once.\n *\n *
|
|
1
|
+
{"version":3,"sources":["../../src/lib/reduced-motion.ts"],"sourcesContent":["/**\n * Whether the user has asked for no motion (`prefers-reduced-motion: reduce`), read NOW —\n * not at mount: the setting can change under a long-lived page. The kit's one copy\n * (DUP-11, refactor audit 2026-10: the settings focus and the exit timers each had their\n * own, with opposite answers where `matchMedia` is missing).\n *\n * Where there is no `matchMedia` to ask (SSR, jsdom) the answer is `whereUnknown`,\n * default `false`: motion allowed — the platform's own default, `no-preference`, which\n * is what a browser that cannot say otherwise has. A smooth scroll there is harmless.\n *\n * An exit animation passes `true` (`skipsExitAnimation` in `hooks/use-close-transition`):\n * without `matchMedia` nothing is painting, and a timer that holds an unpainted panel on\n * screen only delays its `onClose` — every test suite, the kit's and the apps', reads a\n * closed overlay as gone at once.\n *\n * Public since 0.52 (DUP-8, refactor audit 2026-10-30): Kurvenschmiede's chain animation\n * carried a copy of its own.\n */\nexport function prefersReducedMotion(whereUnknown = false): boolean {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return whereUnknown;\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n"],"mappings":";AAkBO,SAAS,qBAAqB,eAAe,OAAgB;AAClE,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,WAAY,QAAO;AACrF,SAAO,OAAO,WAAW,kCAAkC,EAAE;AAC/D;","names":[]}
|
package/dist/lib/round.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* `n` rounded to `digits` decimals, half away from zero, in decimal arithmetic:
|
|
18
18
|
* 1.005 → 1.01, -2.5 at 0 → -3. Never NaN for a finite `n`, and never -0 — `formatMoney`
|
|
19
19
|
* prints -0 as "-0,00 €", so a negative that rounds away comes back as 0. A non-finite
|
|
20
|
-
* `n` comes back unchanged.
|
|
20
|
+
* `n` comes back unchanged. `digits` counts in whole decimals, as `toFixed` takes it: 2.5
|
|
21
|
+
* is 2, and NaN is 0.
|
|
21
22
|
*/
|
|
22
23
|
export declare function roundHalfAway(n: number, digits: number): number;
|
package/dist/lib/round.js
CHANGED
|
@@ -5,8 +5,9 @@ function shiftDecimal(value, by) {
|
|
|
5
5
|
}
|
|
6
6
|
function roundHalfAway(n, digits) {
|
|
7
7
|
if (!Number.isFinite(n)) return n;
|
|
8
|
+
const places = Math.trunc(digits) || 0;
|
|
8
9
|
const abs = Math.abs(n);
|
|
9
|
-
const magnitude = abs >= 2 ** 53 ? abs : shiftDecimal(Math.round(shiftDecimal(abs,
|
|
10
|
+
const magnitude = abs >= 2 ** 53 ? abs : shiftDecimal(Math.round(shiftDecimal(abs, places)), -places);
|
|
10
11
|
return (n < 0 ? -magnitude : magnitude) || 0;
|
|
11
12
|
}
|
|
12
13
|
export {
|
package/dist/lib/round.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/round.ts"],"sourcesContent":["/**\n * Decimal rounding, half away from zero — the one copy behind {@link roundToCurrency}\n * and AmountInput's settle (BUG-11, refactor audit 2026-10).\n *\n * Package-private — not re-exported from the barrel; `format.ts` and `calc.ts` are\n * re-exported wholesale, so a helper there would become API.\n *\n * WHY THE STRING SHIFT. `Math.round(1.005 * 100)` is 100, because 1.005 is 1.00499…\n * in binary. Shifting the decimal point in the number's SHORTEST spelling instead\n * (\"1.005e2\" is exactly 100.5) rounds the figure a person typed. Both helpers did that\n * by gluing `e${digits}` onto `String(n)` — but `String(n)` is itself in exponent form\n * below 1e-6 and at or above 1e21, and \"5.55e-17e2\" is NaN: `roundToCurrency(0.1 + 0.2\n * - 0.3, \"EUR\")` was NaN, and AmountInput blanked a typed \"0.0000001\" on blur (U-2's\n * class, 2026-08-24). {@link shiftDecimal} moves the exponent the string already has.\n */\n\n/** `value × 10^by`, by moving the exponent of its shortest spelling (\"1.005\" → \"1.005e2\",\n * \"5.55e-17\" → \"5.55e-15\"), so no binary multiplication creeps in. */\nfunction shiftDecimal(value: number, by: number): number {\n const [mantissa, exponent = \"0\"] = String(value).split(\"e\");\n return Number(`${mantissa}e${Number(exponent) + by}`);\n}\n\n/**\n * `n` rounded to `digits` decimals, half away from zero, in decimal arithmetic:\n * 1.005 → 1.01, -2.5 at 0 → -3. Never NaN for a finite `n`, and never -0 — `formatMoney`\n * prints -0 as \"-0,00 €\", so a negative that rounds away comes back as 0. A non-finite\n * `n` comes back unchanged.\n */\nexport function roundHalfAway(n: number, digits: number): number {\n if (!Number.isFinite(n)) return n;\n const abs = Math.abs(n);\n // Every double from 2^53 up is a whole number already, and scaling one near\n // Number.MAX_VALUE up would overflow to Infinity.\n const magnitude = abs >= 2 ** 53 ? abs : shiftDecimal(Math.round(shiftDecimal(abs,
|
|
1
|
+
{"version":3,"sources":["../../src/lib/round.ts"],"sourcesContent":["/**\n * Decimal rounding, half away from zero — the one copy behind {@link roundToCurrency}\n * and AmountInput's settle (BUG-11, refactor audit 2026-10).\n *\n * Package-private — not re-exported from the barrel; `format.ts` and `calc.ts` are\n * re-exported wholesale, so a helper there would become API.\n *\n * WHY THE STRING SHIFT. `Math.round(1.005 * 100)` is 100, because 1.005 is 1.00499…\n * in binary. Shifting the decimal point in the number's SHORTEST spelling instead\n * (\"1.005e2\" is exactly 100.5) rounds the figure a person typed. Both helpers did that\n * by gluing `e${digits}` onto `String(n)` — but `String(n)` is itself in exponent form\n * below 1e-6 and at or above 1e21, and \"5.55e-17e2\" is NaN: `roundToCurrency(0.1 + 0.2\n * - 0.3, \"EUR\")` was NaN, and AmountInput blanked a typed \"0.0000001\" on blur (U-2's\n * class, 2026-08-24). {@link shiftDecimal} moves the exponent the string already has.\n */\n\n/** `value × 10^by`, by moving the exponent of its shortest spelling (\"1.005\" → \"1.005e2\",\n * \"5.55e-17\" → \"5.55e-15\"), so no binary multiplication creeps in. */\nfunction shiftDecimal(value: number, by: number): number {\n const [mantissa, exponent = \"0\"] = String(value).split(\"e\");\n return Number(`${mantissa}e${Number(exponent) + by}`);\n}\n\n/**\n * `n` rounded to `digits` decimals, half away from zero, in decimal arithmetic:\n * 1.005 → 1.01, -2.5 at 0 → -3. Never NaN for a finite `n`, and never -0 — `formatMoney`\n * prints -0 as \"-0,00 €\", so a negative that rounds away comes back as 0. A non-finite\n * `n` comes back unchanged. `digits` counts in whole decimals, as `toFixed` takes it: 2.5\n * is 2, and NaN is 0.\n */\nexport function roundHalfAway(n: number, digits: number): number {\n if (!Number.isFinite(n)) return n;\n // \"1.2345e2.5\" is NaN, and `|| 0` below made it 0: MeasuredGrid's `digits`, which went to\n // `toFixed` before DUP-14 brought it here, wrote every computed cell of a table with a\n // fractional one as \"0\" (0.52 code review, deep, charts, finding 5).\n const places = Math.trunc(digits) || 0;\n const abs = Math.abs(n);\n // Every double from 2^53 up is a whole number already, and scaling one near\n // Number.MAX_VALUE up would overflow to Infinity.\n const magnitude = abs >= 2 ** 53 ? abs : shiftDecimal(Math.round(shiftDecimal(abs, places)), -places);\n // The sign goes back on after rounding, so `|| 0` must see the signed result.\n return (n < 0 ? -magnitude : magnitude) || 0;\n}\n"],"mappings":";AAkBA,SAAS,aAAa,OAAe,IAAoB;AACvD,QAAM,CAAC,UAAU,WAAW,GAAG,IAAI,OAAO,KAAK,EAAE,MAAM,GAAG;AAC1D,SAAO,OAAO,GAAG,QAAQ,IAAI,OAAO,QAAQ,IAAI,EAAE,EAAE;AACtD;AASO,SAAS,cAAc,GAAW,QAAwB;AAC/D,MAAI,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AAIhC,QAAM,SAAS,KAAK,MAAM,MAAM,KAAK;AACrC,QAAM,MAAM,KAAK,IAAI,CAAC;AAGtB,QAAM,YAAY,OAAO,KAAK,KAAK,MAAM,aAAa,KAAK,MAAM,aAAa,KAAK,MAAM,CAAC,GAAG,CAAC,MAAM;AAEpG,UAAQ,IAAI,IAAI,CAAC,YAAY,cAAc;AAC7C;","names":[]}
|
|
@@ -16,14 +16,21 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Since 0.34 (DUP-11, refactor audit 2026-10) it also removes, and guards
|
|
18
18
|
* `sessionStorage` the same way: `landing/routing`, `billing/checkout-processing` and
|
|
19
|
-
* the demo banner each wrote that guard again by hand.
|
|
20
|
-
*
|
|
19
|
+
* the demo banner each wrote that guard again by hand.
|
|
20
|
+
*
|
|
21
|
+
* Since 0.52 all of it but {@link storedKeys} is public, with a JSON pair beside it
|
|
22
|
+
* (DUP-8, refactor audit 2026-10-30). Kept package-private, the guard was written again
|
|
23
|
+
* four times in the kit's showcase and in 16 files across the three apps, some of them
|
|
24
|
+
* without it: a token read that throws where storage is blocked takes every request
|
|
25
|
+
* down with it. (The kit's own DevMenu, 0.51, wrote the session pair a second time too.)
|
|
21
26
|
*/
|
|
22
27
|
/** The stored string for `key`, or null when absent, unreadable, or blocked. */
|
|
23
28
|
export declare function readStored(key: string): string | null;
|
|
24
29
|
/** Store `value` under `key`, silently doing nothing when storage is unavailable
|
|
25
|
-
* or full. A preference that cannot be persisted is not worth an exception.
|
|
26
|
-
|
|
30
|
+
* or full. A preference that cannot be persisted is not worth an exception. `null`
|
|
31
|
+
* removes the key, as {@link writeSessionStored}'s does (0.52; the two pairs wrote
|
|
32
|
+
* differently, and `null` past the types stored the string "null" — 0.52 code review). */
|
|
33
|
+
export declare function writeStored(key: string, value: string | null): void;
|
|
27
34
|
/** Forget `key`, silently doing nothing when storage is blocked — there is nothing to
|
|
28
35
|
* forget then. */
|
|
29
36
|
export declare function removeStored(key: string): void;
|
|
@@ -31,9 +38,21 @@ export declare function removeStored(key: string): void;
|
|
|
31
38
|
export declare function readSessionStored(key: string): string | null;
|
|
32
39
|
/** {@link writeStored} for `sessionStorage`; `null` removes the key. */
|
|
33
40
|
export declare function writeSessionStored(key: string, value: string | null): void;
|
|
41
|
+
/**
|
|
42
|
+
* The JSON stored under `key`, when it parses and `isShape` accepts it; else `null`. Absent,
|
|
43
|
+
* blocked, malformed and another shape all read as "never set" — the caller's default —
|
|
44
|
+
* which is what the six hand-written `JSON.parse(readStored(key))` reads in the kit each
|
|
45
|
+
* answered in their own try/catch (DUP-8). `isShape` gets the parsed value as `unknown`:
|
|
46
|
+
* the shape check is the one part that is the caller's. A shape check that throws reads
|
|
47
|
+
* as another shape: null.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readStoredJson<T>(key: string, isShape: (value: unknown) => value is T): T | null;
|
|
50
|
+
/** Store `value` as JSON under `key`, silently doing nothing when storage is unavailable
|
|
51
|
+
* or full, or the value has no JSON form (a cycle, a BigInt) — {@link writeStored}'s rule. */
|
|
52
|
+
export declare function writeStoredJson(key: string, value: unknown): void;
|
|
34
53
|
/**
|
|
35
54
|
* Every key in `area` that starts with `prefix`, or `[]` where storage refuses — for the
|
|
36
55
|
* reset of this device's settings, which removes a table's views by their prefix
|
|
37
|
-
* (docs/settings-harmonization.md §11.4). Package-private
|
|
56
|
+
* (docs/settings-harmonization.md §11.4). Package-private: the reset's alone.
|
|
38
57
|
*/
|
|
39
58
|
export declare function storedKeys(prefix: string, area?: "local" | "session"): string[];
|
package/dist/lib/safe-storage.js
CHANGED
|
@@ -11,7 +11,11 @@ function readStored(key) {
|
|
|
11
11
|
return guarded("localStorage", null, (storage) => storage.getItem(key));
|
|
12
12
|
}
|
|
13
13
|
function writeStored(key, value) {
|
|
14
|
-
guarded(
|
|
14
|
+
guarded(
|
|
15
|
+
"localStorage",
|
|
16
|
+
void 0,
|
|
17
|
+
(storage) => value === null ? storage.removeItem(key) : storage.setItem(key, value)
|
|
18
|
+
);
|
|
15
19
|
}
|
|
16
20
|
function removeStored(key) {
|
|
17
21
|
guarded("localStorage", void 0, (storage) => storage.removeItem(key));
|
|
@@ -26,6 +30,25 @@ function writeSessionStored(key, value) {
|
|
|
26
30
|
(storage) => value === null ? storage.removeItem(key) : storage.setItem(key, value)
|
|
27
31
|
);
|
|
28
32
|
}
|
|
33
|
+
function readStoredJson(key, isShape) {
|
|
34
|
+
const text = readStored(key);
|
|
35
|
+
if (text === null) return null;
|
|
36
|
+
try {
|
|
37
|
+
const value = JSON.parse(text);
|
|
38
|
+
return isShape(value) ? value : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeStoredJson(key, value) {
|
|
44
|
+
let text;
|
|
45
|
+
try {
|
|
46
|
+
text = JSON.stringify(value);
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (text !== void 0) writeStored(key, text);
|
|
51
|
+
}
|
|
29
52
|
function storedKeys(prefix, area = "local") {
|
|
30
53
|
return guarded(area === "session" ? "sessionStorage" : "localStorage", [], (storage) => {
|
|
31
54
|
const keys = [];
|
|
@@ -39,9 +62,11 @@ function storedKeys(prefix, area = "local") {
|
|
|
39
62
|
export {
|
|
40
63
|
readSessionStored,
|
|
41
64
|
readStored,
|
|
65
|
+
readStoredJson,
|
|
42
66
|
removeStored,
|
|
43
67
|
storedKeys,
|
|
44
68
|
writeSessionStored,
|
|
45
|
-
writeStored
|
|
69
|
+
writeStored,
|
|
70
|
+
writeStoredJson
|
|
46
71
|
};
|
|
47
72
|
//# sourceMappingURL=safe-storage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/safe-storage.ts"],"sourcesContent":["/**\n * `localStorage` that cannot take the page down with it.\n *\n * Accessing `window.localStorage` **throws** — rather than returning null — when a\n * browser has site data blocked: Safari private browsing, Chrome with third-party\n * site data disabled, a partitioned webview. Three places in this package already\n * knew that and wrapped their calls in try/catch (`logger.ts`, and the DataTable's\n * `loadPersisted`/`savePersisted`). One did not: `AppShell` read the sidebar's\n * collapse flag straight out of a `useState` initialiser, so the throw happened\n * during render of the application's top-level shell. That is not a lost preference\n * — nothing mounts, in every consumer of this package at once.\n *\n * So the guard lives here, once, and the next storage-backed preference cannot\n * forget it. A read that cannot happen is indistinguishable from a key that was\n * never set, which is the right answer for every caller: the default.\n *\n * Since 0.34 (DUP-11, refactor audit 2026-10) it also removes, and guards\n * `sessionStorage` the same way: `landing/routing`, `billing/checkout-processing` and\n * the demo banner each wrote that guard again by hand.
|
|
1
|
+
{"version":3,"sources":["../../src/lib/safe-storage.ts"],"sourcesContent":["/**\n * `localStorage` that cannot take the page down with it.\n *\n * Accessing `window.localStorage` **throws** — rather than returning null — when a\n * browser has site data blocked: Safari private browsing, Chrome with third-party\n * site data disabled, a partitioned webview. Three places in this package already\n * knew that and wrapped their calls in try/catch (`logger.ts`, and the DataTable's\n * `loadPersisted`/`savePersisted`). One did not: `AppShell` read the sidebar's\n * collapse flag straight out of a `useState` initialiser, so the throw happened\n * during render of the application's top-level shell. That is not a lost preference\n * — nothing mounts, in every consumer of this package at once.\n *\n * So the guard lives here, once, and the next storage-backed preference cannot\n * forget it. A read that cannot happen is indistinguishable from a key that was\n * never set, which is the right answer for every caller: the default.\n *\n * Since 0.34 (DUP-11, refactor audit 2026-10) it also removes, and guards\n * `sessionStorage` the same way: `landing/routing`, `billing/checkout-processing` and\n * the demo banner each wrote that guard again by hand.\n *\n * Since 0.52 all of it but {@link storedKeys} is public, with a JSON pair beside it\n * (DUP-8, refactor audit 2026-10-30). Kept package-private, the guard was written again\n * four times in the kit's showcase and in 16 files across the three apps, some of them\n * without it: a token read that throws where storage is blocked takes every request\n * down with it. (The kit's own DevMenu, 0.51, wrote the session pair a second time too.)\n */\n\n/** `access` on the storage area, or `fallback` where there is none (SSR) or it throws. */\nfunction guarded<T>(area: \"localStorage\" | \"sessionStorage\", fallback: T, access: (storage: Storage) => T): T {\n if (typeof window === \"undefined\") return fallback;\n try {\n return access(window[area]);\n } catch {\n // private mode, quota, partitioned or blocked storage\n return fallback;\n }\n}\n\n/** The stored string for `key`, or null when absent, unreadable, or blocked. */\nexport function readStored(key: string): string | null {\n return guarded(\"localStorage\", null, (storage) => storage.getItem(key));\n}\n\n/** Store `value` under `key`, silently doing nothing when storage is unavailable\n * or full. A preference that cannot be persisted is not worth an exception. `null`\n * removes the key, as {@link writeSessionStored}'s does (0.52; the two pairs wrote\n * differently, and `null` past the types stored the string \"null\" — 0.52 code review). */\nexport function writeStored(key: string, value: string | null): void {\n guarded(\"localStorage\", undefined, (storage) =>\n value === null ? storage.removeItem(key) : storage.setItem(key, value),\n );\n}\n\n/** Forget `key`, silently doing nothing when storage is blocked — there is nothing to\n * forget then. */\nexport function removeStored(key: string): void {\n guarded(\"localStorage\", undefined, (storage) => storage.removeItem(key));\n}\n\n/** {@link readStored} for `sessionStorage`: this tab's value, or null. */\nexport function readSessionStored(key: string): string | null {\n return guarded(\"sessionStorage\", null, (storage) => storage.getItem(key));\n}\n\n/** {@link writeStored} for `sessionStorage`; `null` removes the key. */\nexport function writeSessionStored(key: string, value: string | null): void {\n guarded(\"sessionStorage\", undefined, (storage) =>\n value === null ? storage.removeItem(key) : storage.setItem(key, value),\n );\n}\n\n/**\n * The JSON stored under `key`, when it parses and `isShape` accepts it; else `null`. Absent,\n * blocked, malformed and another shape all read as \"never set\" — the caller's default —\n * which is what the six hand-written `JSON.parse(readStored(key))` reads in the kit each\n * answered in their own try/catch (DUP-8). `isShape` gets the parsed value as `unknown`:\n * the shape check is the one part that is the caller's. A shape check that throws reads\n * as another shape: null.\n */\nexport function readStoredJson<T>(key: string, isShape: (value: unknown) => value is T): T | null {\n const text = readStored(key);\n if (text === null) return null;\n try {\n const value: unknown = JSON.parse(text);\n return isShape(value) ? value : null;\n } catch {\n // not JSON, or a shape check that read past what is there\n return null;\n }\n}\n\n/** Store `value` as JSON under `key`, silently doing nothing when storage is unavailable\n * or full, or the value has no JSON form (a cycle, a BigInt) — {@link writeStored}'s rule. */\nexport function writeStoredJson(key: string, value: unknown): void {\n let text: string | undefined;\n try {\n text = JSON.stringify(value);\n } catch {\n return;\n }\n // `undefined` (and a function) has no JSON form either: stringify answers undefined.\n if (text !== undefined) writeStored(key, text);\n}\n\n/**\n * Every key in `area` that starts with `prefix`, or `[]` where storage refuses — for the\n * reset of this device's settings, which removes a table's views by their prefix\n * (docs/settings-harmonization.md §11.4). Package-private: the reset's alone.\n */\nexport function storedKeys(prefix: string, area: \"local\" | \"session\" = \"local\"): string[] {\n return guarded(area === \"session\" ? \"sessionStorage\" : \"localStorage\", [], (storage) => {\n const keys: string[] = [];\n for (let i = 0; i < storage.length; i++) {\n const key = storage.key(i);\n if (key !== null && key.startsWith(prefix)) keys.push(key);\n }\n return keys;\n });\n}\n"],"mappings":";AA4BA,SAAS,QAAW,MAAyC,UAAa,QAAoC;AAC5G,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,OAAO,IAAI,CAAC;AAAA,EAC5B,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,WAAW,KAA4B;AACrD,SAAO,QAAQ,gBAAgB,MAAM,CAAC,YAAY,QAAQ,QAAQ,GAAG,CAAC;AACxE;AAMO,SAAS,YAAY,KAAa,OAA4B;AACnE;AAAA,IAAQ;AAAA,IAAgB;AAAA,IAAW,CAAC,YAClC,UAAU,OAAO,QAAQ,WAAW,GAAG,IAAI,QAAQ,QAAQ,KAAK,KAAK;AAAA,EACvE;AACF;AAIO,SAAS,aAAa,KAAmB;AAC9C,UAAQ,gBAAgB,QAAW,CAAC,YAAY,QAAQ,WAAW,GAAG,CAAC;AACzE;AAGO,SAAS,kBAAkB,KAA4B;AAC5D,SAAO,QAAQ,kBAAkB,MAAM,CAAC,YAAY,QAAQ,QAAQ,GAAG,CAAC;AAC1E;AAGO,SAAS,mBAAmB,KAAa,OAA4B;AAC1E;AAAA,IAAQ;AAAA,IAAkB;AAAA,IAAW,CAAC,YACpC,UAAU,OAAO,QAAQ,WAAW,GAAG,IAAI,QAAQ,QAAQ,KAAK,KAAK;AAAA,EACvE;AACF;AAUO,SAAS,eAAkB,KAAa,SAAmD;AAChG,QAAM,OAAO,WAAW,GAAG;AAC3B,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI;AACF,UAAM,QAAiB,KAAK,MAAM,IAAI;AACtC,WAAO,QAAQ,KAAK,IAAI,QAAQ;AAAA,EAClC,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAIO,SAAS,gBAAgB,KAAa,OAAsB;AACjE,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AACN;AAAA,EACF;AAEA,MAAI,SAAS,OAAW,aAAY,KAAK,IAAI;AAC/C;AAOO,SAAS,WAAW,QAAgB,OAA4B,SAAmB;AACxF,SAAO,QAAQ,SAAS,YAAY,mBAAmB,gBAAgB,CAAC,GAAG,CAAC,YAAY;AACtF,UAAM,OAAiB,CAAC;AACxB,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,MAAM,QAAQ,IAAI,CAAC;AACzB,UAAI,QAAQ,QAAQ,IAAI,WAAW,MAAM,EAAG,MAAK,KAAK,GAAG;AAAA,IAC3D;AACA,WAAO;AAAA,EACT,CAAC;AACH;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/session-refresh.ts"],"sourcesContent":["/**\n * Renewing a session when the access token runs out: the client half of\n * docs/auth-harmonization.md §11 (round 0.34 C7, KS U6), lifted from keksdose's\n * `refreshOnce` (Kurvenschmiede's is the same, line for line) and corrected where the\n * three apps' reviews found it wrong. It ends kastlan's sign-out over a network blip (B3).\n *\n * THE KIT OWNS THE RULE, THE APP OWNS THE CALL (§11.2, decision 1). The kit decides when\n * to refresh, who waits, what a failure means, when the session is over and whether a\n * renewal may still be stored. The app keeps its endpoint, its token storage, its\n * sign-out and where it navigates, and hands them in as {@link SessionRefreshOptions}.\n *\n * - **One flight per tab** (decision 2): every 401 that arrives while a refresh runs\n * waits for it, and so does `session.refresh()` (kastlan's boot).\n * - **Definitive** (decision 3): the device holds no refresh token (`refresh` resolves\n * `null`), the refresh answers 401 or 403, or the request replayed with the fresh\n * token gets a 401 again. Only then does the app sign out (`onEnded`), and only while\n * that session is still the app's.\n * - **Transient** (decision 4): everything else. The session stays: nothing is signed\n * out, stored or redirected, and the kit shows nothing. The request rejects with its\n * OWN 401, marked ({@link isTransientRefreshFailure}), so the app's message can say\n * \"can't reach the server\" instead of \"not signed in\".\n * - **Retries only where waiting helps** (decision 5, KS 7): no answer, the kit's own\n * timeout, 408, 429 and 5xx get up to three attempts, 1 s and then 3 s apart, each cut\n * off after 15 s; none while the browser reports itself offline. Any other answer, and\n * anything that is not an HTTP error, ends the flight at the first attempt.\n * - **A stale session is never written back** (§11.7, ka 3, KS 3, KS 4): every request\n * is stamped with `currentToken()` when it goes out, and every flight notes it when it\n * starts. A 401 to a request sent with an older token replays it with the app's token\n * now, without a refresh; a flight whose session changed while it ran stores nothing\n * and ends nothing. The access token is the session's identity because every change\n * the apps make replaces it — a refresh, another tab, a company switch, a password\n * change, a sign-in, a demo start, a sign-out.\n *\n * Framework-free like `server-wake.ts`: {@link attachSessionRefresh} takes the app's\n * axios instance as an ARGUMENT, typed structurally, so the kit never imports axios.\n */\n\nimport type { AxiosLikeInstance, ServerWakeAxiosConfig } from \"./server-wake\";\nimport { httpStatusOf } from \"../auth/auth-errors\";\n\n/** What the app's `refresh` resolves: the new access token, plus whatever its `store`\n * needs (its refresh token, the user). */\nexport interface SessionRenewal {\n /** The new access token; the waiting requests are replayed with it. */\n token: string;\n /**\n * `false`: store the renewal, but replay nothing — each waiting request rejects with\n * its 401, unmarked. kastlan's refresh can land in another company (§11.5, ka 3): a\n * waiting write, such as the handover's `POST /sync`, must not be replayed into it.\n */\n replay?: false;\n}\n\n/** What the app hands {@link createSessionRefresh} (§11.4). */\nexport interface SessionRefreshOptions<R extends SessionRenewal> {\n /**\n * The app's own `POST /auth/refresh`, on a BARE client — never the instance the kit is\n * attached to, so the refresh cannot re-enter the interceptor. It **writes nothing**:\n * the kit hands the answer to `store`, and only while the session is unchanged.\n *\n * Resolves the answer as a renewal, or `null` when the device holds no refresh token.\n * Throws on failure, and the kit classifies the throw: a 401 or 403 is definitive; no\n * answer (an axios error without a `response`), the kit's own timeout, 408, 429 and\n * 5xx are transient and retried; any other answer, and anything that is not an HTTP\n * error (a bug in this function), is transient and not retried. The status is read\n * from `err.response.status` (axios), `err.status` or `err.statusCode`.\n *\n * `signal` aborts when the attempt runs past `timeoutMs`; pass it to the request.\n */\n refresh: (signal: AbortSignal) => Promise<R | null>;\n /** Writes the renewal: the tokens and the user (a missing refresh token keeps the\n * stored one). Called only while the session the flight renews is still the app's,\n * and before the waiting requests are replayed — so it writes the token SYNCHRONOUSLY\n * where `currentToken` reads it: the replay goes through the app's own request\n * interceptor again, and is stamped then. A throw is the app's bug: the waiting\n * requests and `session.refresh()` reject with it. */\n store: (renewal: R) => void;\n /** The access token the app sends NOW, read where its request interceptor reads it;\n * `null` when there is none. */\n currentToken: () => string | null;\n /** Whether the session is a demo: a demo's 401 ends it, with no refresh tried\n * (landing-demo §5.5). */\n isDemo: () => boolean;\n /**\n * The session is over: drop the tokens and the user, then navigate — `/demo/ended` for\n * a demo, else the sign-in with the page it came from (auth §5.2). Called once per\n * session, however many requests found out (KS 8), and never by `session.refresh()`.\n */\n onEnded: (ended: { demo: boolean }) => void;\n /** Whether a URL is a signed-out step, whose 401 judges the form and never the\n * session. Replaces the default list, {@link isSignedOutStep}. */\n isSignedOutStep?: (url: string) => boolean;\n /** Attempts for a refresh that got no answer, timed out, or got a 408, 429 or 5xx.\n * Default 3. */\n attempts?: number;\n /** The waits before the second and later attempts; the last one repeats. Default\n * `[1000, 3000]`. */\n backoffMs?: readonly number[];\n /** Each attempt is cut off after this, and its `signal` aborted. Default 15 000. */\n timeoutMs?: number;\n}\n\n/** What `session.refresh()` resolves (§11.4), by `result`:\n *\n * | `result` | When | What the kit has done |\n * |---|---|---|\n * | `\"renewed\"` | a renewal while the session was unchanged | `store(renewal)` |\n * | `\"ended\"` | definitive | nothing; `onEnded` only for requests that waited on the same flight |\n * | `\"transient\"` | transient | nothing |\n * | `\"changed\"` | the session changed while it ran | nothing; read the app's own state |\n */\nexport type SessionRefreshOutcome<R extends SessionRenewal = SessionRenewal> =\n { result: \"renewed\"; renewal: R } | { result: \"ended\" } | { result: \"transient\" } | { result: \"changed\" };\n\n/** The flight {@link createSessionRefresh} makes; {@link attachSessionRefresh} hooks it\n * to an axios instance. */\nexport interface SessionRefresh<R extends SessionRenewal = SessionRenewal> {\n /**\n * The same flight, for a caller outside the interceptor — kastlan's boot. Joins a\n * running flight, or starts one. Never calls `onEnded`: the caller decides what an\n * `\"ended\"` means for it.\n */\n refresh(): Promise<SessionRefreshOutcome<R>>;\n}\n\n// ── The signed-out steps ──────────────────────────────────────────────────────\n\n/**\n * Every `/auth/` call made without a session, the union of the three apps' lists\n * (§11.3): signing in (with its `2fa` and `set-password` steps, and a passkey's), up,\n * into a demo, a password reset, the verification link, the invitation check, the email\n * change's confirmation link, and the refresh itself. NOT `resend-verification` (KS 1):\n * in keksdose and Kurvenschmiede it needs a session; in kastlan it never answers 401.\n */\nconst SIGNED_OUT_STEPS = [\n \"login\",\n \"passkey/login\",\n \"register\",\n \"demo-session\",\n \"password-reset\",\n \"verify-email\",\n \"invitations/check\",\n \"me/email/confirm\",\n \"refresh\",\n] as const;\n\n/**\n * Whether a request's URL is a signed-out step — the kit's default list (§11.3), exported\n * for an app's tests and for an app's own `isSignedOutStep` that adds to it.\n *\n * **Whole steps only** (KS 2): the part of the path after its `auth/` segment, without\n * the query string, is the entry itself or starts with the entry and a `/`. So\n * `login/2fa` is `login`'s, but `register` never catches `/auth/passkey/register/begin`\n * (adding a passkey, which needs a session), `invitations/check` never\n * `/auth/invitations/accept`, and `me/email/confirm` never `/auth/me/email`. Relative and\n * absolute URLs alike; a URL with no `auth` segment is no step.\n */\nexport function isSignedOutStep(url: string): boolean {\n const path = url.replace(/[?#].*$/s, \"\").replace(/^([a-z][a-z\\d+.-]*:)?\\/\\/[^/]*/i, \"\");\n const segments = path.split(\"/\");\n const at = segments.indexOf(\"auth\");\n if (at < 0) return false;\n const step = segments.slice(at + 1).join(\"/\");\n return SIGNED_OUT_STEPS.some((entry) => step === entry || step.startsWith(`${entry}/`));\n}\n\n// ── The marker ────────────────────────────────────────────────────────────────\n\n/** On the request's own axios error, after a transient failure (decision 8). A\n * `Symbol.for` key: shared by every copy of the kit in a page, and left out of\n * `JSON.stringify`, so a log that serialises the error doesn't print it (kk 2). */\nconst TRANSIENT = Symbol.for(\"@eifi1/ui-kit:session-refresh.transient\");\n\n/**\n * Whether a request was refused because the refresh behind it failed TRANSIENTLY —\n * offline, a timeout, a 5xx — and not because the session is over (decision 8). The\n * error is the request's own 401, `err.response?.status` and every check an app already\n * has keep working; this reads the mark the kit put on it, so the app's message can say\n * \"can't reach the server\" instead of \"not signed in\". Read it FIRST: keksdose's sync\n * chip, kastlan's handover sync and Kurvenschmiede's `apiErrorMessage` count it as\n * offline (§11.5).\n *\n * Not the refresh's own error: its config carries the refresh token in its body, into any\n * log or crash report that keeps the error.\n */\nexport function isTransientRefreshFailure(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && (error as Record<symbol, unknown>)[TRANSIENT] === true;\n}\n\n// ── The flight ────────────────────────────────────────────────────────────────\n\n/** A flight's result, as the interceptor needs it: `\"ended\"` keeps the session it ended. */\ntype Flight<R extends SessionRenewal> =\n | { result: \"renewed\"; renewal: R }\n | { result: \"ended\"; token: string | null }\n | { result: \"transient\" }\n | { result: \"changed\" };\n\n/** What one attempt came to. */\ntype Attempt<R> = { renewal: R } | \"definitive\" | \"retry\" | \"transient\";\n\n/** What {@link attachSessionRefresh} needs of a session. Kept off the public interface:\n * only what {@link createSessionRefresh} made can be attached. */\ninterface Engine {\n token(): string | null;\n isDemo(): boolean;\n isSignedOutStep(url: string): boolean;\n join(): Promise<Flight<SessionRenewal>>;\n end(token: string | null, demo: boolean): void;\n}\n\nconst engines = new WeakMap<SessionRefresh<SessionRenewal>, Engine>();\n\n/** The kit's own timeout, told apart from anything the app's `refresh` throws. */\nconst TIMED_OUT = Symbol(\"timed out\");\n\nfunction isOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/** A refresh's throw, classified (§11.4). The status is read where `isRateLimited` reads\n * it — `httpStatusOf`, one probe for axios, ofetch and Node errors (DUP-11). */\nfunction classify(error: unknown): \"definitive\" | \"retry\" | \"transient\" {\n if (error === TIMED_OUT) return \"retry\";\n if (typeof error !== \"object\" || error === null) return \"transient\";\n const status = httpStatusOf(error);\n if (status === undefined) {\n // No answer: axios marks its own errors, and one without a response never reached a\n // server (offline, DNS, CORS, its own timeout). Anything else is a bug in `refresh`.\n return (error as { isAxiosError?: unknown }).isAxiosError === true ? \"retry\" : \"transient\";\n }\n if (status === 401 || status === 403) return \"definitive\";\n if (status === 408 || status === 429 || status >= 500) return \"retry\";\n return \"transient\";\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/**\n * Make the flight: `const session = createSessionRefresh({ refresh, store, currentToken,\n * isDemo, onEnded })`, then {@link attachSessionRefresh}`(api, session)`. One per tab.\n */\nexport function createSessionRefresh<R extends SessionRenewal>(options: SessionRefreshOptions<R>): SessionRefresh<R> {\n const { attempts = 3, backoffMs = [1000, 3000], timeoutMs = 15_000 } = options;\n // An empty string is no token: the stamp and the flight compare tokens, and \"\" against\n // `null` must not read as a change of session.\n const token = () => options.currentToken() || null;\n\n /** One call of the app's `refresh`, cut off after `timeoutMs` even if it ignores the\n * signal. */\n const attempt = async (): Promise<Attempt<R>> => {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(TIMED_OUT);\n controller.abort();\n }, timeoutMs);\n });\n try {\n // Through `then`, so a `refresh` that throws before it returns a promise is\n // classified like one that rejects.\n const renewal = await Promise.race([Promise.resolve().then(() => options.refresh(controller.signal)), timeout]);\n return renewal ? { renewal } : \"definitive\";\n } catch (error) {\n return classify(error);\n } finally {\n clearTimeout(timer);\n }\n };\n\n const run = async (): Promise<Flight<R>> => {\n // The session this flight renews (§11.3 step 5).\n const noted = token();\n const changed = () => token() !== noted;\n let outcome = await attempt();\n for (let made = 1; outcome === \"retry\"; made += 1) {\n // No further attempt while the browser says it is offline, nor for a session that\n // is no longer the app's: whatever the answer, it would be \"changed\".\n if (made >= attempts || isOffline() || changed()) break;\n await sleep(backoffMs[made - 1] ?? backoffMs[backoffMs.length - 1] ?? 0);\n if (isOffline() || changed()) break;\n outcome = await attempt();\n }\n // A late answer never writes the old session back over a sign-out, a demo start or a\n // password change, and the old session's refusal never signs anyone out (KS 3).\n if (changed()) return { result: \"changed\" };\n if (outcome === \"definitive\") return { result: \"ended\", token: noted };\n if (outcome === \"retry\" || outcome === \"transient\") return { result: \"transient\" };\n options.store(outcome.renewal);\n return { result: \"renewed\", renewal: outcome.renewal };\n };\n\n let flight: Promise<Flight<R>> | null = null;\n const join = () => {\n // Cleared before any waiter resumes (`finally` settles first), so a waiter that\n // needs another flight starts a new one.\n flight ??= run().finally(() => {\n flight = null;\n });\n return flight;\n };\n\n /** The session `onEnded` was last called for: once per session (KS 8). */\n let ended: { token: string | null } | undefined;\n\n const session: SessionRefresh<R> = {\n async refresh() {\n const outcome = await join();\n return outcome.result === \"ended\" ? { result: \"ended\" } : outcome;\n },\n };\n engines.set(session, {\n token,\n isDemo: () => options.isDemo(),\n isSignedOutStep: options.isSignedOutStep ?? isSignedOutStep,\n join,\n end(endedToken, demo) {\n if (ended && ended.token === endedToken) return;\n ended = { token: endedToken };\n options.onEnded({ demo });\n },\n });\n return session;\n}\n\n// ── axios ─────────────────────────────────────────────────────────────────────\n\n/** The config fields the kit reads and writes — what axios's request config has. */\ninterface SessionRefreshAxiosConfig extends ServerWakeAxiosConfig {\n /** axios's `AxiosHeaders` (written through its `set`), or a plain record. */\n headers?: unknown;\n}\n\n/** An axios instance, structurally: {@link AxiosLikeInstance}, plus `request(config)` for\n * the replay. `axios.create()`'s result fits it as is. `NoInfer`, because axios's\n * `request` takes the WIDER `AxiosRequestConfig`: inferred from there, the config type\n * no longer fits the interceptors, which hand out `InternalAxiosRequestConfig`. */\ninterface SessionRefreshInstance<\n C extends SessionRefreshAxiosConfig,\n R extends { config: C },\n> extends AxiosLikeInstance<C, R> {\n request(config: NoInfer<C>): Promise<unknown>;\n}\n\n/** The keys the kit keeps on a request's config, as `attachServerWake` keeps\n * `_serverWake`: never a header. Plain string keys holding primitives, because axios\n * copies the config on a replay (`mergeConfig`) and a symbol key would be dropped. */\nconst STAMP = \"_sessionStamp\";\nconst REFRESHED = \"_sessionRefreshed\";\nconst RESTAMPED = \"_sessionRestamped\";\ntype Marked = {\n /** `currentToken()` when the request went out. */\n [STAMP]?: string | null;\n /** Replayed after a refresh: a 401 now is definitive (decision 3). */\n [REFRESHED]?: boolean;\n /** Replayed because it was sent with an older token: once for this reason. */\n [RESTAMPED]?: boolean;\n};\n\n/** `Authorization: Bearer <token>` on a config about to be replayed. The app's own\n * request interceptor runs again on the replay and writes the same, from the same\n * source. */\nfunction authorize(config: SessionRefreshAxiosConfig, token: string): void {\n const value = `Bearer ${token}`;\n const headers = config.headers;\n if (typeof headers !== \"object\" || headers === null) {\n config.headers = { Authorization: value };\n } else if (typeof (headers as { set?: unknown }).set === \"function\") {\n (headers as { set(name: string, value: string): unknown }).set(\"Authorization\", value);\n } else {\n (headers as Record<string, unknown>).Authorization = value;\n }\n}\n\n/**\n * Hook a session to an axios instance: `attachSessionRefresh(api, session)`. Adds a\n * request interceptor (the stamp) and a response interceptor (the 401), and returns a\n * function that ejects both.\n *\n * Attach it LAST (§11.3, kk 4): after `attachServerWake`, which asks to be first, and\n * after the app's own response interceptors (logging, a watchdog of its own). axios runs\n * response interceptors in the order they were added, and each of those must see the 401\n * settle before the refresh is awaited. The request side's order does not matter.\n *\n * On a 401, in this order (§11.3):\n *\n * 1. a signed-out step → reject, nothing else;\n * 2. sent with an older token → replayed with the app's token now, with no refresh, once\n * for this reason; with no token now, reject;\n * 3. a demo → `onEnded({ demo: true })`, reject;\n * 4. already replayed after a refresh → definitive: `onEnded({ demo: false })`, reject;\n * 5. otherwise it joins the flight, or starts it. A renewal replays it once with the\n * renewal's token (`replay: false`: rejects, unmarked); definitive ends the session;\n * transient rejects, marked; a session that changed meanwhile sends it through step\n * 2 again.\n *\n * Every rejection is the request's OWN error. Any other status passes through untouched.\n */\nexport function attachSessionRefresh<C extends SessionRefreshAxiosConfig, R extends { config: C }>(\n instance: SessionRefreshInstance<C, R>,\n session: SessionRefresh<SessionRenewal>,\n): () => void {\n const engine = engines.get(session);\n if (!engine) throw new TypeError(\"attachSessionRefresh takes the session createSessionRefresh made\");\n\n const onUnauthorized = async (error: object, config: C & Marked): Promise<unknown> => {\n if (engine.isSignedOutStep(config.url ?? \"\")) throw error;\n // A config the kit never stamped (sent before it was attached) counts as current.\n const stamp = config[STAMP] === undefined ? engine.token() : config[STAMP];\n const replay = (token: string) => {\n authorize(config, token);\n return instance.request(config);\n };\n for (;;) {\n const now = engine.token();\n // 2. The session it was sent for is gone, and whatever ended it has already done\n // what follows (ka 3, KS 3, KS 4).\n if (stamp !== now) {\n if (now === null || config[RESTAMPED]) throw error;\n config[RESTAMPED] = true;\n return replay(now);\n }\n // 3. A demo has nothing to refresh: its token lives as long as its account\n // (landing-demo §5.5).\n if (engine.isDemo()) {\n engine.end(now, true);\n throw error;\n }\n // 4. A replay for an older token doesn't count here: the app's current token may\n // simply have expired too, which needs a refresh, not a sign-out.\n if (config[REFRESHED]) {\n engine.end(now, false);\n throw error;\n }\n // 5. The flight.\n const flight = await engine.join();\n if (flight.result === \"changed\") continue;\n if (flight.result === \"ended\") {\n engine.end(flight.token, false);\n throw error;\n }\n if (flight.result === \"transient\") {\n (error as Record<symbol, unknown>)[TRANSIENT] = true;\n throw error;\n }\n if (flight.renewal.replay === false) throw error;\n config[REFRESHED] = true;\n return replay(flight.renewal.token);\n }\n };\n\n const request = instance.interceptors.request.use(\n (config) => {\n (config as C & Marked)[STAMP] = engine.token();\n return config;\n },\n (error) => Promise.reject(error),\n );\n const response = instance.interceptors.response.use(\n (res) => res,\n (error) => {\n const failed = error as { config?: C & Marked; response?: { status?: unknown } } | null;\n if (failed?.response?.status !== 401 || !failed.config) return Promise.reject(error);\n return onUnauthorized(failed, failed.config);\n },\n );\n return () => {\n instance.interceptors.request.eject(request);\n instance.interceptors.response.eject(response);\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/session-refresh.ts"],"sourcesContent":["/**\n * Renewing a session when the access token runs out: the client half of\n * docs/auth-harmonization.md §11 (round 0.34 C7, KS U6), lifted from keksdose's\n * `refreshOnce` (Kurvenschmiede's is the same, line for line) and corrected where the\n * three apps' reviews found it wrong. It ends kastlan's sign-out over a network blip (B3).\n *\n * THE KIT OWNS THE RULE, THE APP OWNS THE CALL (§11.2, decision 1). The kit decides when\n * to refresh, who waits, what a failure means, when the session is over and whether a\n * renewal may still be stored. The app keeps its endpoint, its token storage, its\n * sign-out and where it navigates, and hands them in as {@link SessionRefreshOptions}.\n *\n * - **One flight per tab** (decision 2): every 401 that arrives while a refresh runs\n * waits for it, and so does `session.refresh()` (kastlan's boot).\n * - **Definitive** (decision 3): the device holds no refresh token (`refresh` resolves\n * `null`), the refresh answers 401 or 403, or the request replayed with the fresh\n * token gets a 401 again. Only then does the app sign out (`onEnded`), and only while\n * that session is still the app's.\n * - **Transient** (decision 4): everything else. The session stays: nothing is signed\n * out, stored or redirected, and the kit shows nothing. The request rejects with its\n * OWN 401, marked ({@link isTransientRefreshFailure}), so the app's message can say\n * \"can't reach the server\" instead of \"not signed in\".\n * - **Retries only where waiting helps** (decision 5, KS 7): no answer, the kit's own\n * timeout, 408, 429 and 5xx get up to three attempts, 1 s and then 3 s apart, each cut\n * off after 15 s; none while the browser reports itself offline. Any other answer, and\n * anything that is not an HTTP error, ends the flight at the first attempt.\n * - **A stale session is never written back** (§11.7, ka 3, KS 3, KS 4): every request\n * is stamped with `currentToken()` when it goes out, and every flight notes it when it\n * starts. A 401 to a request sent with an older token replays it with the app's token\n * now, without a refresh; a flight whose session changed while it ran stores nothing\n * and ends nothing. The access token is the session's identity because every change\n * the apps make replaces it — a refresh, another tab, a company switch, a password\n * change, a sign-in, a demo start, a sign-out.\n *\n * Framework-free like `server-wake.ts`: {@link attachSessionRefresh} takes the app's\n * axios instance as an ARGUMENT, typed structurally, so the kit never imports axios.\n */\n\nimport type { AxiosLikeInstance, ServerWakeAxiosConfig } from \"./server-wake\";\nimport { httpStatusOf } from \"./http-status\";\n\n/** What the app's `refresh` resolves: the new access token, plus whatever its `store`\n * needs (its refresh token, the user). */\nexport interface SessionRenewal {\n /** The new access token; the waiting requests are replayed with it. */\n token: string;\n /**\n * `false`: store the renewal, but replay nothing — each waiting request rejects with\n * its 401, unmarked. kastlan's refresh can land in another company (§11.5, ka 3): a\n * waiting write, such as the handover's `POST /sync`, must not be replayed into it.\n */\n replay?: false;\n}\n\n/** What the app hands {@link createSessionRefresh} (§11.4). */\nexport interface SessionRefreshOptions<R extends SessionRenewal> {\n /**\n * The app's own `POST /auth/refresh`, on a BARE client — never the instance the kit is\n * attached to, so the refresh cannot re-enter the interceptor. It **writes nothing**:\n * the kit hands the answer to `store`, and only while the session is unchanged.\n *\n * Resolves the answer as a renewal, or `null` when the device holds no refresh token.\n * Throws on failure, and the kit classifies the throw: a 401 or 403 is definitive; no\n * answer (an axios error without a `response`), the kit's own timeout, 408, 429 and\n * 5xx are transient and retried; any other answer, and anything that is not an HTTP\n * error (a bug in this function), is transient and not retried. The status is read\n * from `err.response.status` (axios), `err.status` or `err.statusCode`.\n *\n * `signal` aborts when the attempt runs past `timeoutMs`; pass it to the request.\n */\n refresh: (signal: AbortSignal) => Promise<R | null>;\n /** Writes the renewal: the tokens and the user (a missing refresh token keeps the\n * stored one). Called only while the session the flight renews is still the app's,\n * and before the waiting requests are replayed — so it writes the token SYNCHRONOUSLY\n * where `currentToken` reads it: the replay goes through the app's own request\n * interceptor again, and is stamped then. A throw is the app's bug: the waiting\n * requests and `session.refresh()` reject with it. */\n store: (renewal: R) => void;\n /** The access token the app sends NOW, read where its request interceptor reads it;\n * `null` when there is none. */\n currentToken: () => string | null;\n /** Whether the session is a demo: a demo's 401 ends it, with no refresh tried\n * (landing-demo §5.5). */\n isDemo: () => boolean;\n /**\n * The session is over: drop the tokens and the user, then navigate — `/demo/ended` for\n * a demo, else the sign-in with the page it came from (auth §5.2). Called once per\n * session, however many requests found out (KS 8), and never by `session.refresh()`.\n */\n onEnded: (ended: { demo: boolean }) => void;\n /** Whether a URL is a signed-out step, whose 401 judges the form and never the\n * session. Replaces the default list, {@link isSignedOutStep}. */\n isSignedOutStep?: (url: string) => boolean;\n /** Attempts for a refresh that got no answer, timed out, or got a 408, 429 or 5xx.\n * Default 3. */\n attempts?: number;\n /** The waits before the second and later attempts; the last one repeats. Default\n * `[1000, 3000]`. */\n backoffMs?: readonly number[];\n /** Each attempt is cut off after this, and its `signal` aborted. Default 15 000. */\n timeoutMs?: number;\n}\n\n/** What `session.refresh()` resolves (§11.4), by `result`:\n *\n * | `result` | When | What the kit has done |\n * |---|---|---|\n * | `\"renewed\"` | a renewal while the session was unchanged | `store(renewal)` |\n * | `\"ended\"` | definitive | nothing; `onEnded` only for requests that waited on the same flight |\n * | `\"transient\"` | transient | nothing |\n * | `\"changed\"` | the session changed while it ran | nothing; read the app's own state |\n */\nexport type SessionRefreshOutcome<R extends SessionRenewal = SessionRenewal> =\n { result: \"renewed\"; renewal: R } | { result: \"ended\" } | { result: \"transient\" } | { result: \"changed\" };\n\n/** The flight {@link createSessionRefresh} makes; {@link attachSessionRefresh} hooks it\n * to an axios instance. */\nexport interface SessionRefresh<R extends SessionRenewal = SessionRenewal> {\n /**\n * The same flight, for a caller outside the interceptor — kastlan's boot. Joins a\n * running flight, or starts one. Never calls `onEnded`: the caller decides what an\n * `\"ended\"` means for it.\n */\n refresh(): Promise<SessionRefreshOutcome<R>>;\n}\n\n// ── The signed-out steps ──────────────────────────────────────────────────────\n\n/**\n * Every `/auth/` call made without a session, the union of the three apps' lists\n * (§11.3): signing in (with its `2fa` and `set-password` steps, and a passkey's), up,\n * into a demo, a password reset, the verification link, the invitation check, the email\n * change's confirmation link, and the refresh itself. NOT `resend-verification` (KS 1):\n * in keksdose and Kurvenschmiede it needs a session; in kastlan it never answers 401.\n */\nconst SIGNED_OUT_STEPS = [\n \"login\",\n \"passkey/login\",\n \"register\",\n \"demo-session\",\n \"password-reset\",\n \"verify-email\",\n \"invitations/check\",\n \"me/email/confirm\",\n \"refresh\",\n] as const;\n\n/**\n * Whether a request's URL is a signed-out step — the kit's default list (§11.3), exported\n * for an app's tests and for an app's own `isSignedOutStep` that adds to it.\n *\n * **Whole steps only** (KS 2): the part of the path after its `auth/` segment, without\n * the query string, is the entry itself or starts with the entry and a `/`. So\n * `login/2fa` is `login`'s, but `register` never catches `/auth/passkey/register/begin`\n * (adding a passkey, which needs a session), `invitations/check` never\n * `/auth/invitations/accept`, and `me/email/confirm` never `/auth/me/email`. Relative and\n * absolute URLs alike; a URL with no `auth` segment is no step.\n */\nexport function isSignedOutStep(url: string): boolean {\n const path = url.replace(/[?#].*$/s, \"\").replace(/^([a-z][a-z\\d+.-]*:)?\\/\\/[^/]*/i, \"\");\n const segments = path.split(\"/\");\n const at = segments.indexOf(\"auth\");\n if (at < 0) return false;\n const step = segments.slice(at + 1).join(\"/\");\n return SIGNED_OUT_STEPS.some((entry) => step === entry || step.startsWith(`${entry}/`));\n}\n\n// ── The marker ────────────────────────────────────────────────────────────────\n\n/** On the request's own axios error, after a transient failure (decision 8). A\n * `Symbol.for` key: shared by every copy of the kit in a page, and left out of\n * `JSON.stringify`, so a log that serialises the error doesn't print it (kk 2). */\nconst TRANSIENT = Symbol.for(\"@eifi1/ui-kit:session-refresh.transient\");\n\n/**\n * Whether a request was refused because the refresh behind it failed TRANSIENTLY —\n * offline, a timeout, a 5xx — and not because the session is over (decision 8). The\n * error is the request's own 401, `err.response?.status` and every check an app already\n * has keep working; this reads the mark the kit put on it, so the app's message can say\n * \"can't reach the server\" instead of \"not signed in\". Read it FIRST: keksdose's sync\n * chip, kastlan's handover sync and Kurvenschmiede's `apiErrorMessage` count it as\n * offline (§11.5).\n *\n * Not the refresh's own error: its config carries the refresh token in its body, into any\n * log or crash report that keeps the error.\n */\nexport function isTransientRefreshFailure(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && (error as Record<symbol, unknown>)[TRANSIENT] === true;\n}\n\n// ── The flight ────────────────────────────────────────────────────────────────\n\n/** A flight's result, as the interceptor needs it: `\"ended\"` keeps the session it ended. */\ntype Flight<R extends SessionRenewal> =\n | { result: \"renewed\"; renewal: R }\n | { result: \"ended\"; token: string | null }\n | { result: \"transient\" }\n | { result: \"changed\" };\n\n/** What one attempt came to. */\ntype Attempt<R> = { renewal: R } | \"definitive\" | \"retry\" | \"transient\";\n\n/** What {@link attachSessionRefresh} needs of a session. Kept off the public interface:\n * only what {@link createSessionRefresh} made can be attached. */\ninterface Engine {\n token(): string | null;\n isDemo(): boolean;\n isSignedOutStep(url: string): boolean;\n join(): Promise<Flight<SessionRenewal>>;\n end(token: string | null, demo: boolean): void;\n}\n\nconst engines = new WeakMap<SessionRefresh<SessionRenewal>, Engine>();\n\n/** The kit's own timeout, told apart from anything the app's `refresh` throws. */\nconst TIMED_OUT = Symbol(\"timed out\");\n\nfunction isOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/** A refresh's throw, classified (§11.4). The status is read where `isRateLimited` reads\n * it — `httpStatusOf`, one probe for axios, ofetch and Node errors (DUP-11). */\nfunction classify(error: unknown): \"definitive\" | \"retry\" | \"transient\" {\n if (error === TIMED_OUT) return \"retry\";\n if (typeof error !== \"object\" || error === null) return \"transient\";\n const status = httpStatusOf(error);\n if (status === undefined) {\n // No answer: axios marks its own errors, and one without a response never reached a\n // server (offline, DNS, CORS, its own timeout). Anything else is a bug in `refresh`.\n return (error as { isAxiosError?: unknown }).isAxiosError === true ? \"retry\" : \"transient\";\n }\n if (status === 401 || status === 403) return \"definitive\";\n if (status === 408 || status === 429 || status >= 500) return \"retry\";\n return \"transient\";\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/**\n * Make the flight: `const session = createSessionRefresh({ refresh, store, currentToken,\n * isDemo, onEnded })`, then {@link attachSessionRefresh}`(api, session)`. One per tab.\n */\nexport function createSessionRefresh<R extends SessionRenewal>(options: SessionRefreshOptions<R>): SessionRefresh<R> {\n const { attempts = 3, backoffMs = [1000, 3000], timeoutMs = 15_000 } = options;\n // An empty string is no token: the stamp and the flight compare tokens, and \"\" against\n // `null` must not read as a change of session.\n const token = () => options.currentToken() || null;\n\n /** One call of the app's `refresh`, cut off after `timeoutMs` even if it ignores the\n * signal. */\n const attempt = async (): Promise<Attempt<R>> => {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(TIMED_OUT);\n controller.abort();\n }, timeoutMs);\n });\n try {\n // Through `then`, so a `refresh` that throws before it returns a promise is\n // classified like one that rejects.\n const renewal = await Promise.race([Promise.resolve().then(() => options.refresh(controller.signal)), timeout]);\n return renewal ? { renewal } : \"definitive\";\n } catch (error) {\n return classify(error);\n } finally {\n clearTimeout(timer);\n }\n };\n\n const run = async (): Promise<Flight<R>> => {\n // The session this flight renews (§11.3 step 5).\n const noted = token();\n const changed = () => token() !== noted;\n let outcome = await attempt();\n for (let made = 1; outcome === \"retry\"; made += 1) {\n // No further attempt while the browser says it is offline, nor for a session that\n // is no longer the app's: whatever the answer, it would be \"changed\".\n if (made >= attempts || isOffline() || changed()) break;\n await sleep(backoffMs[made - 1] ?? backoffMs[backoffMs.length - 1] ?? 0);\n if (isOffline() || changed()) break;\n outcome = await attempt();\n }\n // A late answer never writes the old session back over a sign-out, a demo start or a\n // password change, and the old session's refusal never signs anyone out (KS 3).\n if (changed()) return { result: \"changed\" };\n if (outcome === \"definitive\") return { result: \"ended\", token: noted };\n if (outcome === \"retry\" || outcome === \"transient\") return { result: \"transient\" };\n options.store(outcome.renewal);\n return { result: \"renewed\", renewal: outcome.renewal };\n };\n\n let flight: Promise<Flight<R>> | null = null;\n const join = () => {\n // Cleared before any waiter resumes (`finally` settles first), so a waiter that\n // needs another flight starts a new one.\n flight ??= run().finally(() => {\n flight = null;\n });\n return flight;\n };\n\n /** The session `onEnded` was last called for: once per session (KS 8). */\n let ended: { token: string | null } | undefined;\n\n const session: SessionRefresh<R> = {\n async refresh() {\n const outcome = await join();\n return outcome.result === \"ended\" ? { result: \"ended\" } : outcome;\n },\n };\n engines.set(session, {\n token,\n isDemo: () => options.isDemo(),\n isSignedOutStep: options.isSignedOutStep ?? isSignedOutStep,\n join,\n end(endedToken, demo) {\n if (ended && ended.token === endedToken) return;\n ended = { token: endedToken };\n options.onEnded({ demo });\n },\n });\n return session;\n}\n\n// ── axios ─────────────────────────────────────────────────────────────────────\n\n/** The config fields the kit reads and writes — what axios's request config has. */\ninterface SessionRefreshAxiosConfig extends ServerWakeAxiosConfig {\n /** axios's `AxiosHeaders` (written through its `set`), or a plain record. */\n headers?: unknown;\n}\n\n/** An axios instance, structurally: {@link AxiosLikeInstance}, plus `request(config)` for\n * the replay. `axios.create()`'s result fits it as is. `NoInfer`, because axios's\n * `request` takes the WIDER `AxiosRequestConfig`: inferred from there, the config type\n * no longer fits the interceptors, which hand out `InternalAxiosRequestConfig`. */\ninterface SessionRefreshInstance<\n C extends SessionRefreshAxiosConfig,\n R extends { config: C },\n> extends AxiosLikeInstance<C, R> {\n request(config: NoInfer<C>): Promise<unknown>;\n}\n\n/** The keys the kit keeps on a request's config, as `attachServerWake` keeps\n * `_serverWake`: never a header. Plain string keys holding primitives, because axios\n * copies the config on a replay (`mergeConfig`) and a symbol key would be dropped. */\nconst STAMP = \"_sessionStamp\";\nconst REFRESHED = \"_sessionRefreshed\";\nconst RESTAMPED = \"_sessionRestamped\";\ntype Marked = {\n /** `currentToken()` when the request went out. */\n [STAMP]?: string | null;\n /** Replayed after a refresh: a 401 now is definitive (decision 3). */\n [REFRESHED]?: boolean;\n /** Replayed because it was sent with an older token: once for this reason. */\n [RESTAMPED]?: boolean;\n};\n\n/** `Authorization: Bearer <token>` on a config about to be replayed. The app's own\n * request interceptor runs again on the replay and writes the same, from the same\n * source. */\nfunction authorize(config: SessionRefreshAxiosConfig, token: string): void {\n const value = `Bearer ${token}`;\n const headers = config.headers;\n if (typeof headers !== \"object\" || headers === null) {\n config.headers = { Authorization: value };\n } else if (typeof (headers as { set?: unknown }).set === \"function\") {\n (headers as { set(name: string, value: string): unknown }).set(\"Authorization\", value);\n } else {\n (headers as Record<string, unknown>).Authorization = value;\n }\n}\n\n/**\n * Hook a session to an axios instance: `attachSessionRefresh(api, session)`. Adds a\n * request interceptor (the stamp) and a response interceptor (the 401), and returns a\n * function that ejects both.\n *\n * Attach it LAST (§11.3, kk 4): after `attachServerWake`, which asks to be first, and\n * after the app's own response interceptors (logging, a watchdog of its own). axios runs\n * response interceptors in the order they were added, and each of those must see the 401\n * settle before the refresh is awaited. The request side's order does not matter.\n *\n * On a 401, in this order (§11.3):\n *\n * 1. a signed-out step → reject, nothing else;\n * 2. sent with an older token → replayed with the app's token now, with no refresh, once\n * for this reason; with no token now, reject;\n * 3. a demo → `onEnded({ demo: true })`, reject;\n * 4. already replayed after a refresh → definitive: `onEnded({ demo: false })`, reject;\n * 5. otherwise it joins the flight, or starts it. A renewal replays it once with the\n * renewal's token (`replay: false`: rejects, unmarked); definitive ends the session;\n * transient rejects, marked; a session that changed meanwhile sends it through step\n * 2 again.\n *\n * Every rejection is the request's OWN error. Any other status passes through untouched.\n */\nexport function attachSessionRefresh<C extends SessionRefreshAxiosConfig, R extends { config: C }>(\n instance: SessionRefreshInstance<C, R>,\n session: SessionRefresh<SessionRenewal>,\n): () => void {\n const engine = engines.get(session);\n if (!engine) throw new TypeError(\"attachSessionRefresh takes the session createSessionRefresh made\");\n\n const onUnauthorized = async (error: object, config: C & Marked): Promise<unknown> => {\n if (engine.isSignedOutStep(config.url ?? \"\")) throw error;\n // A config the kit never stamped (sent before it was attached) counts as current.\n const stamp = config[STAMP] === undefined ? engine.token() : config[STAMP];\n const replay = (token: string) => {\n authorize(config, token);\n return instance.request(config);\n };\n for (;;) {\n const now = engine.token();\n // 2. The session it was sent for is gone, and whatever ended it has already done\n // what follows (ka 3, KS 3, KS 4).\n if (stamp !== now) {\n if (now === null || config[RESTAMPED]) throw error;\n config[RESTAMPED] = true;\n return replay(now);\n }\n // 3. A demo has nothing to refresh: its token lives as long as its account\n // (landing-demo §5.5).\n if (engine.isDemo()) {\n engine.end(now, true);\n throw error;\n }\n // 4. A replay for an older token doesn't count here: the app's current token may\n // simply have expired too, which needs a refresh, not a sign-out.\n if (config[REFRESHED]) {\n engine.end(now, false);\n throw error;\n }\n // 5. The flight.\n const flight = await engine.join();\n if (flight.result === \"changed\") continue;\n if (flight.result === \"ended\") {\n engine.end(flight.token, false);\n throw error;\n }\n if (flight.result === \"transient\") {\n (error as Record<symbol, unknown>)[TRANSIENT] = true;\n throw error;\n }\n if (flight.renewal.replay === false) throw error;\n config[REFRESHED] = true;\n return replay(flight.renewal.token);\n }\n };\n\n const request = instance.interceptors.request.use(\n (config) => {\n (config as C & Marked)[STAMP] = engine.token();\n return config;\n },\n (error) => Promise.reject(error),\n );\n const response = instance.interceptors.response.use(\n (res) => res,\n (error) => {\n const failed = error as { config?: C & Marked; response?: { status?: unknown } } | null;\n if (failed?.response?.status !== 401 || !failed.config) return Promise.reject(error);\n return onUnauthorized(failed, failed.config);\n },\n );\n return () => {\n instance.interceptors.request.eject(request);\n instance.interceptors.response.eject(response);\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a chart says a share of its whole — PieChart's slices and centre, Treemap's tiles
|
|
3
|
+
* and groups. Internal, not exported from the barrel.
|
|
4
|
+
*
|
|
5
|
+
* Each chart held a copy of the same two `Intl` formats and the same switch between them
|
|
6
|
+
* (0.52 code review of treemap.tsx, finding 9); one copy is the rule both say.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* A formatter for shares (0–1) in `locale`, as a percent: one decimal under 10 %, where
|
|
10
|
+
* rounding to whole percents starts to lie — a slice of 0.4 % said as "0%" says it is not
|
|
11
|
+
* there — and whole percents above. Make it once per locale (it builds two `Intl` formats).
|
|
12
|
+
*/
|
|
13
|
+
export declare function shareFormat(locale: string | undefined): (share: number) => string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
function shareFormat(locale) {
|
|
3
|
+
const whole = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 });
|
|
4
|
+
const fine = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 });
|
|
5
|
+
return (share) => (share < 0.1 ? fine : whole).format(share);
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
shareFormat
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=share-format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/share-format.ts"],"sourcesContent":["/**\n * How a chart says a share of its whole — PieChart's slices and centre, Treemap's tiles\n * and groups. Internal, not exported from the barrel.\n *\n * Each chart held a copy of the same two `Intl` formats and the same switch between them\n * (0.52 code review of treemap.tsx, finding 9); one copy is the rule both say.\n */\n\n/**\n * A formatter for shares (0–1) in `locale`, as a percent: one decimal under 10 %, where\n * rounding to whole percents starts to lie — a slice of 0.4 % said as \"0%\" says it is not\n * there — and whole percents above. Make it once per locale (it builds two `Intl` formats).\n */\nexport function shareFormat(locale: string | undefined): (share: number) => string {\n const whole = new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 });\n const fine = new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 1 });\n return (share) => (share < 0.1 ? fine : whole).format(share);\n}\n"],"mappings":";AAaO,SAAS,YAAY,QAAuD;AACjF,QAAM,QAAQ,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC;AAC1F,QAAM,OAAO,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC;AACzF,SAAO,CAAC,WAAW,QAAQ,MAAM,OAAO,OAAO,OAAO,KAAK;AAC7D;","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upper-case the first letter only, in `locale` — the one copy of the rule (refactor audit
|
|
3
|
+
* 2026-10-30, DUP-11: MonthPicker and JumpCalendar each had it, and MiniCalendar's caption
|
|
4
|
+
* still used the CSS `capitalize` it was written against).
|
|
5
|
+
*
|
|
6
|
+
* Several locales (fr, es, it, pt, …) write month names in lower case, which at the head
|
|
7
|
+
* of a field or a panel reads as a typo. But only the FIRST letter is ours to change:
|
|
8
|
+
* `capitalize` upper-cases every word, so Spanish read "Septiembre De 2026", and a
|
|
9
|
+
* caption that does not start with the month kept its wrong capital — Hungarian read
|
|
10
|
+
* "2026. Szeptember". This gives "Septiembre de 2026" and "2026. szeptember".
|
|
11
|
+
*
|
|
12
|
+
* In `locale` because the case mapping is the language's: Turkish upper-cases "i" to "İ".
|
|
13
|
+
* `undefined` is the runtime's default, as everywhere `Intl` takes one.
|
|
14
|
+
*
|
|
15
|
+
* Package-private — not re-exported from the barrel.
|
|
16
|
+
*/
|
|
17
|
+
export declare function upperFirst(text: string, locale: string | undefined): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/text-case.ts"],"sourcesContent":["/**\n * Upper-case the first letter only, in `locale` — the one copy of the rule (refactor audit\n * 2026-10-30, DUP-11: MonthPicker and JumpCalendar each had it, and MiniCalendar's caption\n * still used the CSS `capitalize` it was written against).\n *\n * Several locales (fr, es, it, pt, …) write month names in lower case, which at the head\n * of a field or a panel reads as a typo. But only the FIRST letter is ours to change:\n * `capitalize` upper-cases every word, so Spanish read \"Septiembre De 2026\", and a\n * caption that does not start with the month kept its wrong capital — Hungarian read\n * \"2026. Szeptember\". This gives \"Septiembre de 2026\" and \"2026. szeptember\".\n *\n * In `locale` because the case mapping is the language's: Turkish upper-cases \"i\" to \"İ\".\n * `undefined` is the runtime's default, as everywhere `Intl` takes one.\n *\n * Package-private — not re-exported from the barrel.\n */\nexport function upperFirst(text: string, locale: string | undefined): string {\n return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);\n}\n"],"mappings":";AAgBO,SAAS,WAAW,MAAc,QAAoC;AAC3E,SAAO,KAAK,OAAO,CAAC,EAAE,kBAAkB,MAAM,IAAI,KAAK,MAAM,CAAC;AAChE;","names":[]}
|
package/dist/lib/text-match.d.ts
CHANGED
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
* `locale` left out is the runtime's default; a malformed tag folds as if left out.
|
|
20
20
|
*/
|
|
21
21
|
export declare function foldForSearch(text: string, locale?: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Whether two names are one name as a picker's search reads them: folded
|
|
24
|
+
* ({@link foldForSearch}) and trimmed — "cafe" is Café, " MIGROS " is Migros. For "does
|
|
25
|
+
* the typed text name a record that exists" (a create row, InlineEntityCombobox's blur
|
|
26
|
+
* commit), which compared with plain `toLowerCase` while the list beside it folded, so
|
|
27
|
+
* "cafe" listed Café as a hit and offered "Create “cafe”" too — a duplicate payee or shop
|
|
28
|
+
* chain one Enter away (refactor audit 2026-10-30, DUP-5). Internal: not in the barrel.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sameName(a: string, b: string, locale?: string): boolean;
|
|
22
31
|
/**
|
|
23
32
|
* A picker's query as its words, each folded (0.51.4, keksdose's /prices shop picker):
|
|
24
33
|
* "Migros Zürich" → `["migros", "zurich"]`. An empty or blank query has no words.
|
|
@@ -59,3 +68,32 @@ export declare function rankMatch(label: string, query: string, { code, locale }
|
|
|
59
68
|
code?: string;
|
|
60
69
|
locale?: string;
|
|
61
70
|
}): MatchRank;
|
|
71
|
+
/** A row {@link searchRanked} kept, and how well it answers the WHOLE query:
|
|
72
|
+
* {@link rankMatch}'s rank, or `3` for a row that holds the query's words apart
|
|
73
|
+
* ("Zürich Migros" for "migros zürich"). */
|
|
74
|
+
export interface RankedHit<T> {
|
|
75
|
+
row: T;
|
|
76
|
+
rank: Exclude<MatchRank, -1> | 3;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The rows that hold every word of `query`, each ranked for the WHOLE query — for the
|
|
80
|
+
* lists that FILTER by words ({@link matchesSearch}) and RANK by the whole query
|
|
81
|
+
* ({@link rankMatch}): Combobox, CountrySelect and InlineEntityCombobox (refactor audit
|
|
82
|
+
* 2026-10-30, DUP-4). In the order given; sort by `rank` with a stable sort, and the rows
|
|
83
|
+
* of one rank keep it. No words keep every row, at `0`.
|
|
84
|
+
*
|
|
85
|
+
* Each row's texts are folded ONCE, and the query once. The lists asked `matchesSearch`
|
|
86
|
+
* and then `rankMatch` of each row they kept, which folded the row three times and the
|
|
87
|
+
* query once per row, per keystroke — InlineEntityCombobox asked `rankMatch` twice of
|
|
88
|
+
* most rows (0.52 code review, combobox.tsx).
|
|
89
|
+
*
|
|
90
|
+
* `texts(row)`: where a word may stand; the FIRST is the label the rank is for.
|
|
91
|
+
* `code(row)`: a key that answers only WHOLE — a word that IS the code matches the row,
|
|
92
|
+
* and the code as the whole query ranks `0` ("ch" is Switzerland before it is Chad) —
|
|
93
|
+
* never a letter of it ("w" is not Aruba, AW: 0.52 code review, country-select.tsx).
|
|
94
|
+
* Internal: not in the barrel.
|
|
95
|
+
*/
|
|
96
|
+
export declare function searchRanked<T>(rows: readonly T[], query: string, texts: (row: T) => readonly (string | undefined)[], { code, locale }?: {
|
|
97
|
+
code?: (row: T) => string;
|
|
98
|
+
locale?: string;
|
|
99
|
+
}): RankedHit<T>[];
|