@eifi1/ui-kit 0.51.6 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -26
- package/dist/account/account-parts.d.ts +10 -8
- package/dist/account/account-parts.js +1 -18
- package/dist/account/account-parts.js.map +1 -1
- package/dist/account/data-export-setting-labels.js +1 -1
- package/dist/account/data-export-setting-labels.js.map +1 -1
- package/dist/account/delete-account-setting-labels.js +1 -1
- package/dist/account/delete-account-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting-labels.js +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/passkeys-setting.d.ts +9 -2
- package/dist/account/passkeys-setting.js +41 -29
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +3 -2
- package/dist/account/sessions-setting.js +10 -8
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +3 -7
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-parts.d.ts +6 -3
- package/dist/admin/admin-parts.js +6 -21
- package/dist/admin/admin-parts.js.map +1 -1
- package/dist/admin/mail-test-card-labels.js +1 -1
- package/dist/admin/mail-test-card-labels.js.map +1 -1
- package/dist/admin/plan-change-labels.d.ts +2 -2
- package/dist/admin/plan-change-labels.js +1 -17
- package/dist/admin/plan-change-labels.js.map +1 -1
- package/dist/admin/plan-change.d.ts +3 -42
- package/dist/admin/plan-change.js +1 -1
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +3 -3
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/auth/auth-errors-labels.d.ts +11 -0
- package/dist/auth/auth-errors-labels.js +13 -0
- package/dist/auth/auth-errors-labels.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +7 -23
- package/dist/auth/auth-errors.js +2 -15
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.d.ts +3 -2
- package/dist/auth/complete-name-dialog.js +3 -5
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/forgot-password-form-labels.js +1 -1
- package/dist/auth/forgot-password-form-labels.js.map +1 -1
- package/dist/auth/forgot-password-form.js +7 -2
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form-labels.js +1 -1
- package/dist/auth/register-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form-labels.js +1 -1
- package/dist/auth/reset-password-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form.js +5 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form-labels.js +1 -1
- package/dist/auth/sign-in-form-labels.js.map +1 -1
- package/dist/auth/status-parts.d.ts +0 -3
- package/dist/auth/status-parts.js +0 -4
- package/dist/auth/status-parts.js.map +1 -1
- package/dist/auth/verify-email-labels.js +1 -1
- package/dist/auth/verify-email-labels.js.map +1 -1
- package/dist/auth/verify-email.js +7 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/cancel-request.d.ts +11 -2
- package/dist/billing/cancel-request.js +15 -3
- package/dist/billing/cancel-request.js.map +1 -1
- package/dist/billing/cancel-subscription.d.ts +22 -10
- package/dist/billing/cancel-subscription.js +8 -11
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/dialog-close.d.ts +29 -0
- package/dist/billing/dialog-close.js +8 -0
- package/dist/billing/dialog-close.js.map +1 -0
- package/dist/billing/plan-card.d.ts +14 -4
- package/dist/billing/plan-card.js +15 -8
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-change-labels.d.ts +42 -0
- package/dist/billing/plan-change-labels.js +22 -0
- package/dist/billing/plan-change-labels.js.map +1 -0
- package/dist/billing/plan-switch.d.ts +21 -9
- package/dist/billing/plan-switch.js +10 -8
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +12 -0
- package/dist/chart.js +16 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +5 -5
- package/dist/components/account-chips.js +11 -13
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +16 -5
- package/dist/components/alert-banner.js +46 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +22 -4
- package/dist/components/amount-input.js +3 -3
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +57 -65
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +12 -2
- package/dist/components/bulk-action-bar.js +65 -21
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-look.d.ts +5 -5
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.d.ts +21 -2
- package/dist/components/calculator.js +53 -14
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-edge.d.ts +16 -0
- package/dist/components/chart-edge.js +14 -1
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-figure.d.ts +13 -0
- package/dist/components/chart-figure.js +15 -0
- package/dist/components/chart-figure.js.map +1 -0
- package/dist/components/chart-zoom-core.js +1 -4
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +8 -1
- package/dist/components/chart.js +19 -10
- package/dist/components/chart.js.map +1 -1
- package/dist/components/combobox-core.d.ts +119 -7
- package/dist/components/combobox-core.js +151 -46
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +142 -131
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +6 -5
- package/dist/components/copy-button.js +5 -3
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.js +2 -2
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +1 -1
- package/dist/components/data-table-column-settings.js +33 -7
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +4 -2
- package/dist/components/data-table-filters.js +2 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +6 -4
- package/dist/components/data-table-fit.js +4 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +1 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +1 -1
- package/dist/components/data-table-head.js +15 -12
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +36 -7
- package/dist/components/data-table-mobile.js +63 -31
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js +11 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-resize.d.ts +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +6 -2
- package/dist/components/data-table-row.js +17 -10
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-selection.d.ts +1 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +10 -2
- package/dist/components/data-table-shared.js +18 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +21 -2
- package/dist/components/data-table-sort.js +24 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +1 -1
- package/dist/components/data-table-virtual.js +18 -6
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +25 -8
- package/dist/components/data-table.js +154 -25
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +24 -2
- package/dist/components/date-field.js +33 -8
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +15 -2
- package/dist/components/date-picker.js +55 -26
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.d.ts +47 -20
- package/dist/components/date-range-picker.js +23 -7
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +2 -1
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/entity-combobox.js +4 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js +37 -30
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
- package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
- package/dist/components/field-base-labels.js.map +1 -0
- package/dist/components/field-base.d.ts +4 -3
- package/dist/components/field-base.js +3 -3
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-sync-labels.d.ts +1 -0
- package/dist/components/field-sync-labels.js.map +1 -1
- package/dist/components/field-sync.d.ts +23 -1
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/form-actions.js +11 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +5 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +14 -9
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.d.ts +43 -0
- package/dist/components/inline-confirm.js +67 -0
- package/dist/components/inline-confirm.js.map +1 -0
- package/dist/components/inline-entity-combobox.d.ts +4 -3
- package/dist/components/inline-entity-combobox.js +150 -141
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +28 -112
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-page.d.ts +3 -1
- package/dist/components/legal-page.js +3 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +4 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.d.ts +2 -2
- package/dist/components/lightbox.js +17 -6
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/list.d.ts +6 -2
- package/dist/components/list.js +4 -3
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +1 -1
- package/dist/components/measured-grid.js +2 -1
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/mini-calendar.js +3 -5
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +24 -0
- package/dist/components/modal.js +12 -4
- package/dist/components/modal.js.map +1 -1
- package/dist/components/money-draft.js +7 -5
- package/dist/components/money-draft.js.map +1 -1
- package/dist/components/money-field.d.ts +2 -2
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-grid.d.ts +55 -0
- package/dist/components/month-grid.js +148 -0
- package/dist/components/month-grid.js.map +1 -0
- package/dist/components/month-picker.d.ts +36 -4
- package/dist/components/month-picker.js +123 -146
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -1
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/multi-select.js +11 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +6 -4
- package/dist/components/number-field.js +3 -2
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +4 -1
- package/dist/components/number-input.js +3 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +7 -0
- package/dist/components/pie-chart-labels.js +1 -0
- package/dist/components/pie-chart-labels.js.map +1 -1
- package/dist/components/pie-chart.d.ts +5 -3
- package/dist/components/pie-chart.js +7 -22
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +3 -0
- package/dist/components/popover.js +7 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/reauth-dialog.d.ts +4 -5
- package/dist/components/reauth-dialog.js +1 -4
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/row-actions.js +2 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +7 -0
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +10 -3
- package/dist/components/series-chart-axes.js +1 -0
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +7 -4
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +31 -7
- package/dist/components/series-chart.js +72 -29
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +3 -3
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip-labels.d.ts +1 -0
- package/dist/components/sign-chip-labels.js.map +1 -1
- package/dist/components/sign-chip.d.ts +10 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/tabs-labels.d.ts +2 -0
- package/dist/components/tabs-labels.js +11 -0
- package/dist/components/tabs-labels.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/tabs.js +13 -11
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +2 -2
- package/dist/components/toggle-legend.js +6 -6
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -2
- package/dist/components/translation-review-editor.js +2 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.d.ts +2 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap-labels.d.ts +26 -0
- package/dist/components/treemap-labels.js +10 -0
- package/dist/components/treemap-labels.js.map +1 -0
- package/dist/components/treemap-layout.d.ts +62 -0
- package/dist/components/treemap-layout.js +126 -0
- package/dist/components/treemap-layout.js.map +1 -0
- package/dist/components/treemap.d.ts +140 -17
- package/dist/components/treemap.js +324 -97
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.d.ts +5 -5
- package/dist/components/use-table-state.js +4 -1
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +6 -2
- package/dist/data-table.js +7 -2
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +36 -7
- package/dist/eslint/role-utilities.js +42 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +25 -16
- package/dist/feedback/feedback-dialog.js +6 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-mutation.d.ts +30 -0
- package/dist/feedback/feedback-mutation.js +16 -0
- package/dist/feedback/feedback-mutation.js.map +1 -0
- package/dist/feedback/feedback-page-link.d.ts +53 -0
- package/dist/feedback/feedback-page-link.js +37 -0
- package/dist/feedback/feedback-page-link.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +12 -3
- package/dist/feedback/feedback-public-list.js +2 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +3 -1
- package/dist/feedback/feedback-replies.js +5 -4
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +5 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +64 -28
- package/dist/feedback/feedback-row-detail.js +30 -28
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +30 -11
- package/dist/feedback/feedback-status-undo.js +10 -15
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
- package/dist/feedback/feedback-submit-dialog.js +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.js +9 -3
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +1 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +21 -9
- package/dist/feedback/feedback-table.js +45 -25
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-labels.d.ts +2 -2
- package/dist/feedback/feedback-thread-labels.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +24 -23
- package/dist/feedback/feedback-thread.js +2 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +34 -0
- package/dist/feedback.js +42 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +35 -0
- package/dist/hooks/use-focus-trap.js +6 -4
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-hotkey.d.ts +5 -2
- package/dist/hooks/use-hotkey.js +5 -4
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +7 -4
- package/dist/hooks/use-long-press.js +3 -0
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-media-query.js +12 -4
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-mounted.d.ts +22 -0
- package/dist/hooks/use-mounted.js +24 -0
- package/dist/hooks/use-mounted.js.map +1 -0
- package/dist/hooks/use-overlay-history.d.ts +7 -1
- package/dist/hooks/use-overlay-history.js +35 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +4 -5
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +7 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +20 -41
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -10
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +8 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +5 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +64 -17
- package/dist/index.js +77 -12
- package/dist/index.js.map +1 -1
- package/dist/landing/routing.d.ts +3 -1
- package/dist/landing/routing.js +4 -7
- package/dist/landing/routing.js.map +1 -1
- package/dist/lib/broadcast.d.ts +17 -0
- package/dist/lib/broadcast.js +30 -0
- package/dist/lib/broadcast.js.map +1 -0
- package/dist/lib/calc.d.ts +7 -1
- package/dist/lib/calc.js +3 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.d.ts +2 -2
- package/dist/lib/clipboard-minus.js +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/date-format.d.ts +37 -2
- package/dist/lib/date-format.js +34 -8
- package/dist/lib/date-format.js.map +1 -1
- package/dist/lib/day-format.d.ts +46 -0
- package/dist/lib/day-format.js +46 -0
- package/dist/lib/day-format.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +7 -1
- package/dist/lib/decimal-marks.js +5 -2
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/describe-failure.d.ts +14 -0
- package/dist/lib/describe-failure.js +9 -0
- package/dist/lib/describe-failure.js.map +1 -0
- package/dist/lib/direction.d.ts +16 -0
- package/dist/lib/direction.js +9 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/focus-left.d.ts +63 -0
- package/dist/lib/focus-left.js +67 -0
- package/dist/lib/focus-left.js.map +1 -0
- package/dist/lib/format.d.ts +5 -3
- package/dist/lib/format.js +16 -13
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/http-status.d.ts +24 -0
- package/dist/lib/http-status.js +14 -0
- package/dist/lib/http-status.js.map +1 -0
- package/dist/lib/intl-cache.d.ts +14 -0
- package/dist/lib/intl-cache.js +15 -0
- package/dist/lib/intl-cache.js.map +1 -0
- package/dist/lib/isin.d.ts +10 -0
- package/dist/lib/isin.js.map +1 -1
- package/dist/lib/iso-day.d.ts +11 -0
- package/dist/lib/iso-day.js +8 -0
- package/dist/lib/iso-day.js.map +1 -0
- package/dist/lib/lazy-with-fallback.d.ts +31 -0
- package/dist/lib/lazy-with-fallback.js +40 -0
- package/dist/lib/lazy-with-fallback.js.map +1 -0
- package/dist/lib/path-pattern.d.ts +34 -0
- package/dist/lib/path-pattern.js +24 -0
- package/dist/lib/path-pattern.js.map +1 -0
- package/dist/lib/reduced-motion.d.ts +2 -1
- package/dist/lib/reduced-motion.js.map +1 -1
- package/dist/lib/round.d.ts +2 -1
- package/dist/lib/round.js +2 -1
- package/dist/lib/round.js.map +1 -1
- package/dist/lib/safe-storage.d.ts +24 -5
- package/dist/lib/safe-storage.js +27 -2
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/lib/session-refresh.js +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/share-format.d.ts +13 -0
- package/dist/lib/share-format.js +10 -0
- package/dist/lib/share-format.js.map +1 -0
- package/dist/lib/text-case.d.ts +17 -0
- package/dist/lib/text-case.js +8 -0
- package/dist/lib/text-case.js.map +1 -0
- package/dist/lib/text-match.d.ts +38 -0
- package/dist/lib/text-match.js +24 -2
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/thenable.d.ts +13 -0
- package/dist/lib/thenable.js +8 -0
- package/dist/lib/thenable.js.map +1 -0
- package/dist/lib/translation-review.d.ts +7 -2
- package/dist/lib/translation-review.js +4 -4
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/device-zone-report.js +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +2 -1
- package/dist/notifications/notification-bell.js +3 -3
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +10 -6
- package/dist/notifications/notification-inbox.js +26 -48
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +3 -2
- package/dist/notifications/notification-parts.js +1 -3
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.d.ts +6 -0
- package/dist/notifications/notifications-labels.js +1 -0
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +1 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +2 -1
- package/dist/notifications/push-devices-setting.js +27 -49
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +10 -3
- package/dist/notifications/quiet-hours-setting.js +65 -26
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/unsubscribe-panel.js +4 -7
- package/dist/notifications/unsubscribe-panel.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +39 -1
- package/dist/notifications/use-push-device.js +28 -7
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/query-persistence.d.ts +3 -1
- package/dist/offline/query-persistence.js +19 -2
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/rhf/fields.d.ts +11 -4
- package/dist/rhf/fields.js.map +1 -1
- package/dist/search/command-palette.js +1 -1
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +19 -9
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/use-command-key.d.ts +12 -1
- package/dist/search/use-command-key.js +3 -12
- package/dist/search/use-command-key.js.map +1 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.js +10 -0
- package/dist/search.js.map +1 -1
- package/dist/settings/use-settings-focus.js +1 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +21 -5
- package/dist/shell/account-menu-language.js +3 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +26 -9
- package/dist/shell/account-menu-text-size.js +3 -2
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/dev-menu.js +5 -18
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +7 -0
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +1 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +0 -2
- package/dist/shell/top-bar-parts-labels.js +0 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +27 -8
- package/dist/shell.js +28 -7
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +30 -4
- package/dist/sync/engine.js +172 -52
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +6 -2
- package/dist/sync.js +6 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js +3 -8
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -1
- package/dist/theme/contrast.js +3 -8
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-store.js +6 -6
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +3 -8
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +3 -6
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.d.ts +7 -4
- package/dist/tour/tour-catalogue.js +12 -10
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +0 -2
- package/dist/tour/tour-dock.js +0 -2
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-gates.d.ts +4 -0
- package/dist/tour/tour-gates.js +14 -0
- package/dist/tour/tour-gates.js.map +1 -1
- package/dist/tour/tour-overlay.d.ts +27 -0
- package/dist/tour/tour-overlay.js +741 -0
- package/dist/tour/tour-overlay.js.map +1 -0
- package/dist/tour/tour.d.ts +16 -4
- package/dist/tour/tour.js +62 -778
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js +2 -2
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +6 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.d.ts +3 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.d.ts +3 -1
- package/dist/wizard/validation.js.map +1 -1
- package/package.json +12 -8
- package/src/account/account-parts.ts +5 -25
- package/src/account/data-export-setting-labels.ts +1 -1
- package/src/account/delete-account-setting-labels.ts +1 -1
- package/src/account/email-change-setting-labels.ts +1 -1
- package/src/account/passkeys-setting.tsx +60 -38
- package/src/account/sessions-setting.tsx +18 -12
- package/src/admin/admin-action-confirm.tsx +5 -10
- package/src/admin/admin-parts.ts +11 -25
- package/src/admin/mail-test-card-labels.ts +1 -1
- package/src/admin/plan-change-labels.ts +5 -23
- package/src/admin/plan-change.tsx +7 -44
- package/src/admin/reviewer-scope-editor.tsx +3 -3
- package/src/auth/auth-errors-labels.ts +23 -0
- package/src/auth/auth-errors.ts +11 -38
- package/src/auth/complete-name-dialog.tsx +9 -8
- package/src/auth/forgot-password-form-labels.ts +1 -1
- package/src/auth/forgot-password-form.tsx +11 -6
- package/src/auth/register-form-labels.ts +1 -1
- package/src/auth/reset-password-form-labels.ts +1 -1
- package/src/auth/reset-password-form.tsx +8 -4
- package/src/auth/sign-in-form-labels.ts +1 -1
- package/src/auth/status-parts.tsx +0 -6
- package/src/auth/verify-email-labels.ts +1 -1
- package/src/auth/verify-email.tsx +11 -6
- package/src/billing/cancel-request.ts +28 -6
- package/src/billing/cancel-subscription.tsx +29 -22
- package/src/billing/dialog-close.ts +34 -0
- package/src/billing/plan-card.tsx +36 -11
- package/src/billing/plan-change-labels.ts +66 -0
- package/src/billing/plan-switch.tsx +31 -20
- package/src/chart.ts +21 -3
- package/src/components/account-chips.tsx +20 -23
- package/src/components/alert-banner.tsx +82 -5
- package/src/components/amount-input.tsx +32 -14
- package/src/components/autocomplete.tsx +64 -70
- package/src/components/bulk-action-bar.tsx +70 -2
- package/src/components/button-look.ts +5 -5
- package/src/components/button.tsx +2 -2
- package/src/components/calculator.tsx +98 -17
- package/src/components/chart-edge.ts +34 -1
- package/src/components/chart-figure.tsx +35 -0
- package/src/components/chart-zoom-core.tsx +6 -5
- package/src/components/chart.tsx +26 -5
- package/src/components/combobox-core.tsx +303 -69
- package/src/components/combobox.tsx +184 -155
- package/src/components/copy-button.tsx +34 -23
- package/src/components/country-select.tsx +16 -11
- package/src/components/data-table-column-settings.tsx +52 -10
- package/src/components/data-table-filters.ts +6 -4
- package/src/components/data-table-fit.ts +19 -5
- package/src/components/data-table-groups.tsx +1 -1
- package/src/components/data-table-head.tsx +32 -22
- package/src/components/data-table-mobile.tsx +110 -27
- package/src/components/data-table-pagination.tsx +21 -6
- package/src/components/data-table-resize.ts +1 -1
- package/src/components/data-table-row-actions.tsx +3 -3
- package/src/components/data-table-row.tsx +25 -14
- package/src/components/data-table-selection.ts +1 -1
- package/src/components/data-table-shared.tsx +33 -1
- package/src/components/data-table-sort.ts +57 -2
- package/src/components/data-table-virtual.ts +32 -8
- package/src/components/data-table.tsx +267 -41
- package/src/components/date-field.tsx +65 -8
- package/src/components/date-picker.tsx +125 -48
- package/src/components/date-range-picker.tsx +82 -31
- package/src/components/dialog-frame.tsx +2 -1
- package/src/components/entity-combobox.tsx +4 -1
- package/src/components/error-boundary.tsx +53 -39
- package/src/components/field-base-labels.ts +20 -0
- package/src/components/field-base.tsx +13 -7
- package/src/components/field-sync-labels.ts +1 -0
- package/src/components/field-sync.tsx +23 -1
- package/src/components/form-actions.tsx +20 -5
- package/src/components/hover-menu.tsx +18 -8
- package/src/components/icon-button.tsx +14 -9
- package/src/components/inline-confirm.tsx +116 -0
- package/src/components/inline-entity-combobox.tsx +198 -152
- package/src/components/input.tsx +1 -1
- package/src/components/jump-calendar.tsx +30 -146
- package/src/components/legal-page.tsx +10 -7
- package/src/components/legal.tsx +4 -2
- package/src/components/lightbox.tsx +21 -8
- package/src/components/list.tsx +14 -5
- package/src/components/measured-grid.tsx +8 -4
- package/src/components/mini-calendar.tsx +6 -5
- package/src/components/modal.tsx +45 -4
- package/src/components/money-draft.ts +16 -5
- package/src/components/money-field.tsx +2 -2
- package/src/components/month-grid.tsx +229 -0
- package/src/components/month-picker.tsx +192 -200
- package/src/components/multi-entity-combobox.tsx +1 -0
- package/src/components/multi-select.tsx +21 -9
- package/src/components/number-field.tsx +14 -7
- package/src/components/number-input.tsx +9 -3
- package/src/components/numpad-sheet.tsx +4 -3
- package/src/components/picker-sheet.tsx +12 -9
- package/src/components/pie-chart-labels.ts +8 -0
- package/src/components/pie-chart.tsx +22 -28
- package/src/components/popover.tsx +18 -1
- package/src/components/reauth-dialog.tsx +6 -10
- package/src/components/row-actions.tsx +2 -1
- package/src/components/section-nav.tsx +7 -0
- package/src/components/series-chart-axes.ts +11 -3
- package/src/components/series-chart-labels.ts +7 -4
- package/src/components/series-chart.tsx +178 -69
- package/src/components/share-card.tsx +6 -6
- package/src/components/sign-chip-labels.ts +1 -0
- package/src/components/sign-chip.tsx +10 -1
- package/src/components/tabs-labels.ts +13 -0
- package/src/components/tabs.tsx +35 -19
- package/src/components/toggle-legend.tsx +16 -11
- package/src/components/translation-review-editor.tsx +6 -5
- package/src/components/translation-review.tsx +4 -3
- package/src/components/treemap-labels.ts +39 -0
- package/src/components/treemap-layout.ts +210 -0
- package/src/components/treemap.tsx +722 -133
- package/src/components/use-table-state.ts +12 -9
- package/src/data-table.ts +9 -2
- package/src/eslint/role-utilities.ts +97 -19
- package/src/feedback/feedback-dialog.tsx +30 -19
- package/src/feedback/feedback-mutation.ts +41 -0
- package/src/feedback/feedback-page-link.ts +90 -0
- package/src/feedback/feedback-public-list.tsx +17 -5
- package/src/feedback/feedback-replies.tsx +13 -6
- package/src/feedback/feedback-row-detail-labels.ts +5 -0
- package/src/feedback/feedback-row-detail.tsx +103 -67
- package/src/feedback/feedback-status-undo.ts +41 -27
- package/src/feedback/feedback-submit-dialog.tsx +2 -2
- package/src/feedback/feedback-submit.tsx +21 -7
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/feedback/feedback-table.tsx +86 -38
- package/src/feedback/feedback-thread-labels.ts +2 -2
- package/src/feedback/feedback-thread.tsx +27 -25
- package/src/feedback.ts +42 -0
- package/src/hooks/use-dismiss.ts +3 -2
- package/src/hooks/use-focus-trap.ts +8 -24
- package/src/hooks/use-hotkey.ts +24 -7
- package/src/hooks/use-long-press.ts +16 -2
- package/src/hooks/use-media-query.ts +20 -4
- package/src/hooks/use-mounted.ts +39 -0
- package/src/hooks/use-overlay-history.ts +70 -5
- package/src/i18n/defaults.ts +4 -5
- package/src/i18n/german.ts +8 -2
- package/src/i18n/kit-labels.tsx +9 -2
- package/src/i18n/locales/es.ts +8 -2
- package/src/i18n/locales/fr.ts +21 -13
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +8 -2
- package/src/i18n/locales/zh.ts +9 -2
- package/src/i18n/review.ts +5 -0
- package/src/index.ts +103 -13
- package/src/landing/routing.tsx +14 -10
- package/src/lib/broadcast.ts +52 -0
- package/src/lib/calc.ts +13 -2
- package/src/lib/clipboard-minus.ts +14 -4
- package/src/lib/date-format.ts +89 -13
- package/src/lib/day-format.ts +96 -0
- package/src/lib/decimal-marks.ts +12 -3
- package/src/lib/describe-failure.ts +18 -0
- package/src/lib/direction.ts +39 -0
- package/src/lib/focus-left.ts +156 -0
- package/src/lib/format.ts +34 -19
- package/src/lib/http-status.ts +38 -0
- package/src/lib/intl-cache.ts +25 -0
- package/src/lib/isin.ts +10 -0
- package/src/lib/iso-day.ts +13 -0
- package/src/lib/lazy-with-fallback.ts +82 -0
- package/src/lib/path-pattern.ts +55 -0
- package/src/lib/reduced-motion.ts +2 -1
- package/src/lib/round.ts +7 -2
- package/src/lib/safe-storage.ts +48 -6
- package/src/lib/session-refresh.ts +1 -1
- package/src/lib/share-format.ts +18 -0
- package/src/lib/text-case.ts +19 -0
- package/src/lib/text-match.ts +69 -2
- package/src/lib/thenable.ts +19 -0
- package/src/lib/translation-review.ts +13 -5
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-bell.tsx +7 -4
- package/src/notifications/notification-inbox.tsx +40 -58
- package/src/notifications/notification-parts.tsx +3 -4
- package/src/notifications/notification-types-setting.tsx +1 -1
- package/src/notifications/notifications-labels.ts +7 -0
- package/src/notifications/push-device-setting.tsx +1 -1
- package/src/notifications/push-devices-setting.tsx +30 -55
- package/src/notifications/quiet-hours-setting.tsx +93 -31
- package/src/notifications/unsubscribe-panel.tsx +4 -7
- package/src/notifications/use-push-device.ts +76 -10
- package/src/offline/query-persistence.ts +35 -3
- package/src/rhf/fields.tsx +11 -4
- package/src/search/command-palette.tsx +2 -4
- package/src/search/global-search.tsx +36 -15
- package/src/search/use-command-key.ts +13 -12
- package/src/search.ts +9 -0
- package/src/settings/use-settings-focus.ts +3 -0
- package/src/shell/account-menu-language.tsx +27 -5
- package/src/shell/account-menu-text-size.tsx +32 -10
- package/src/shell/dev-menu.tsx +7 -21
- package/src/shell/role-switcher.tsx +7 -0
- package/src/shell/top-bar-actions.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +0 -3
- package/src/shell.ts +40 -9
- package/src/sync/engine.ts +339 -61
- package/src/sync.ts +8 -1
- package/src/testing/css-variables.ts +8 -9
- package/src/theme/contrast.ts +8 -9
- package/src/theme/palette-store.ts +9 -9
- package/src/theme/text-size.ts +6 -8
- package/src/theme/theme-store.ts +7 -9
- package/src/tour/tour-catalogue.tsx +26 -15
- package/src/tour/tour-dock.ts +0 -2
- package/src/tour/tour-gates.ts +23 -0
- package/src/tour/tour-overlay.tsx +1170 -0
- package/src/tour/tour.tsx +116 -1193
- package/src/tour/use-tour-progress.ts +1 -1
- package/src/tour/use-tour-url-sync.ts +7 -3
- package/src/wizard/stepper-nav.tsx +3 -1
- package/src/wizard/types.ts +11 -2
- package/src/wizard/use-wizard.ts +2 -1
- package/src/wizard/validation.ts +8 -2
- package/tokens.css +2 -1
- package/dist/components/ui-labels.js.map +0 -1
- package/dist/shell/dev-switchers.d.ts +0 -6
- package/dist/shell/dev-switchers.js +0 -12
- package/dist/shell/dev-switchers.js.map +0 -1
- package/src/components/ui-labels.ts +0 -24
- package/src/shell/dev-switchers.tsx +0 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/direction.ts"],"sourcesContent":["/**\n * Reading direction, as the DOM has it — internal, not exported from the barrel.\n *\n * The kit lays itself out with logical classes (`ps-*`, `end-*`, `text-start`), which\n * need no JavaScript. These helpers are for the two things CSS cannot do:\n *\n * - **Arrow keys.** ←/→ mean previous/next in LTR and the reverse in RTL. Resolve the\n * key through {@link horizontalStep} instead of comparing to \"ArrowRight\".\n * - **Portals.** A panel portalled to `document.body` leaves the subtree whose `dir`\n * it inherited. Read the anchor's direction with {@link dirOf} and put it on the\n * portalled root, and position it from the anchor's START edge, not `rect.left`.\n */\n\nexport type Direction = \"ltr\" | \"rtl\";\n\n/** The effective direction at `el`: its nearest `[dir]` ancestor (or itself), else the\n * document's, else LTR. Attribute-based on purpose — jsdom computes no `direction`. */\nexport function dirOf(el: Element | null | undefined): Direction {\n const source = el?.closest(\"[dir]\") ?? (typeof document !== \"undefined\" ? document.documentElement : null);\n return source?.getAttribute(\"dir\") === \"rtl\" ? \"rtl\" : \"ltr\";\n}\n\nexport function isRtl(el: Element | null | undefined): boolean {\n return dirOf(el) === \"rtl\";\n}\n\n/**\n * +1 for a key that moves FORWARD along the reading direction, -1 for backward, 0 for\n * any other key. ArrowRight is forward in LTR and backward in RTL.\n */\nexport function horizontalStep(key: string, el: Element | null | undefined): 1 | -1 | 0 {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = (key === \"ArrowRight\") !== isRtl(el);\n return forward ? 1 : -1;\n}\n"],"mappings":";AAiBO,SAAS,MAAM,IAA2C;AAC/D,QAAM,SAAS,IAAI,QAAQ,OAAO,MAAM,OAAO,aAAa,cAAc,SAAS,kBAAkB;AACrG,SAAO,QAAQ,aAAa,KAAK,MAAM,QAAQ,QAAQ;AACzD;AAEO,SAAS,MAAM,IAAyC;AAC7D,SAAO,MAAM,EAAE,MAAM;AACvB;AAMO,SAAS,eAAe,KAAa,IAA4C;AACtF,MAAI,QAAQ,gBAAgB,QAAQ,YAAa,QAAO;AACxD,QAAM,UAAW,QAAQ,iBAAkB,MAAM,EAAE;AACnD,SAAO,UAAU,IAAI;AACvB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/direction.ts"],"sourcesContent":["/**\n * Reading direction, as the DOM has it — internal, not exported from the barrel.\n *\n * The kit lays itself out with logical classes (`ps-*`, `end-*`, `text-start`), which\n * need no JavaScript. These helpers are for the two things CSS cannot do:\n *\n * - **Arrow keys.** ←/→ mean previous/next in LTR and the reverse in RTL. Resolve the\n * key through {@link horizontalStep} instead of comparing to \"ArrowRight\".\n * - **Portals.** A panel portalled to `document.body` leaves the subtree whose `dir`\n * it inherited. Read the anchor's direction with {@link dirOf} and put it on the\n * portalled root, and position it from the anchor's START edge, not `rect.left`.\n */\n\nexport type Direction = \"ltr\" | \"rtl\";\n\n/** The effective direction at `el`: its nearest `[dir]` ancestor (or itself), else the\n * document's, else LTR. Attribute-based on purpose — jsdom computes no `direction`. */\nexport function dirOf(el: Element | null | undefined): Direction {\n const source = el?.closest(\"[dir]\") ?? (typeof document !== \"undefined\" ? document.documentElement : null);\n return source?.getAttribute(\"dir\") === \"rtl\" ? \"rtl\" : \"ltr\";\n}\n\nexport function isRtl(el: Element | null | undefined): boolean {\n return dirOf(el) === \"rtl\";\n}\n\n/**\n * +1 for a key that moves FORWARD along the reading direction, -1 for backward, 0 for\n * any other key. ArrowRight is forward in LTR and backward in RTL.\n */\nexport function horizontalStep(key: string, el: Element | null | undefined): 1 | -1 | 0 {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = (key === \"ArrowRight\") !== isRtl(el);\n return forward ? 1 : -1;\n}\n\n/**\n * Where a key moves the focus in a list with ONE tab stop (a roving tabindex): the index\n * to focus from `at` among `count` items, or `undefined` for a key that moves nothing.\n * ←/→ along `dir` — → is next in `ltr` and previous in `rtl` — ↓/↑ as next and previous,\n * Home and End to the ends. With `wrap`, past the last is the first and before the first\n * the last (a ring: PieChart's slices); without, the ends hold (a plane: Treemap's tiles).\n *\n * The charts each mapped the keys themselves, and differently at the ends (0.52 code\n * review of treemap.tsx, finding 9); the end rule is now a word at the call.\n *\n * `dir` and not an element to read it off (0.52 code review, deep, charts, findings 1 and\n * 14): a list in the page's flow takes the page's (`dirOf(el)`), but a plot is laid out\n * left to right in any page (see `ChartContainer`) and passes `\"ltr\"` — read off the\n * element, Treemap's → walked an RTL page's tiles backwards, against the picture.\n */\nexport function rovingTarget(\n key: string,\n dir: Direction,\n at: number,\n count: number,\n wrap: boolean,\n): number | undefined {\n const physical = key === \"ArrowRight\" ? 1 : key === \"ArrowLeft\" ? -1 : 0;\n const step = dir === \"rtl\" ? -physical : physical;\n const to = step\n ? at + step\n : key === \"ArrowDown\"\n ? at + 1\n : key === \"ArrowUp\"\n ? at - 1\n : key === \"Home\"\n ? 0\n : key === \"End\"\n ? count - 1\n : undefined;\n if (to === undefined || count <= 0) return undefined;\n return wrap ? ((to % count) + count) % count : Math.max(0, Math.min(count - 1, to));\n}\n"],"mappings":";AAiBO,SAAS,MAAM,IAA2C;AAC/D,QAAM,SAAS,IAAI,QAAQ,OAAO,MAAM,OAAO,aAAa,cAAc,SAAS,kBAAkB;AACrG,SAAO,QAAQ,aAAa,KAAK,MAAM,QAAQ,QAAQ;AACzD;AAEO,SAAS,MAAM,IAAyC;AAC7D,SAAO,MAAM,EAAE,MAAM;AACvB;AAMO,SAAS,eAAe,KAAa,IAA4C;AACtF,MAAI,QAAQ,gBAAgB,QAAQ,YAAa,QAAO;AACxD,QAAM,UAAW,QAAQ,iBAAkB,MAAM,EAAE;AACnD,SAAO,UAAU,IAAI;AACvB;AAiBO,SAAS,aACd,KACA,KACA,IACA,OACA,MACoB;AACpB,QAAM,WAAW,QAAQ,eAAe,IAAI,QAAQ,cAAc,KAAK;AACvE,QAAM,OAAO,QAAQ,QAAQ,CAAC,WAAW;AACzC,QAAM,KAAK,OACP,KAAK,OACL,QAAQ,cACN,KAAK,IACL,QAAQ,YACN,KAAK,IACL,QAAQ,SACN,IACA,QAAQ,QACN,QAAQ,IACR;AACZ,MAAI,OAAO,UAAa,SAAS,EAAG,QAAO;AAC3C,SAAO,QAAS,KAAK,QAAS,SAAS,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,GAAG,EAAE,CAAC;AACpF;","names":[]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface TabStopOptions {
|
|
2
|
+
/** Whether `el` itself counts, first, when it is a stop. Default true: the tour's
|
|
3
|
+
* target may BE the field (refactor audit 2026-10-30, DUP-9). False for "a control
|
|
4
|
+
* inside it" — a row, a drawer, a trap's container, a toolbar. */
|
|
5
|
+
includeSelf?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** The tab stops in `el`, in DOM order — `el` itself first when it is one, unless
|
|
8
|
+
* `includeSelf: false`. */
|
|
9
|
+
export declare function tabStops(el: Element, { includeSelf }?: TabStopOptions): HTMLElement[];
|
|
10
|
+
/** The first of {@link tabStops} — the control a person types into or presses — or null. */
|
|
11
|
+
export declare function firstTabStop(el: Element, options?: TabStopOptions): HTMLElement | null;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the focus has fallen out of the page: onto `<body>`, onto nothing, or onto a
|
|
14
|
+
* node no longer in the document. False on the server, where nothing has focus to lose.
|
|
15
|
+
* A recovery asks this first: focus that went somewhere real (a dialog, the next field,
|
|
16
|
+
* an app's own `focus()`) is the user's or the app's, and is left where it is.
|
|
17
|
+
*/
|
|
18
|
+
export declare function focusIsLost(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Calls `then` when a `blur`/`focusout` was the user's own move to nothing — a click on
|
|
21
|
+
* the page's background, or a click Safari answers by focusing nothing (it focuses no
|
|
22
|
+
* button or link on a click, and blurs what had the focus) — after which a note of where
|
|
23
|
+
* the focus was is stale (0.52 code review: DataTable's row, BulkActionBar's and
|
|
24
|
+
* AlertBanner's notes outlived it, and a removal much later pulled the focus, and the
|
|
25
|
+
* scroll, back to them).
|
|
26
|
+
*
|
|
27
|
+
* No element took the focus, the document holds none, and the node that lost it could
|
|
28
|
+
* still hold it. Not when it was removed, hidden, made inert or disabled: that is the page
|
|
29
|
+
* taking the focus away — the very case those notes are kept for. Not a window losing the
|
|
30
|
+
* focus either, which leaves the node the document's active element. Decided a microtask
|
|
31
|
+
* later, once the event's own work is done: a browser that blurs a node it is removing
|
|
32
|
+
* (or hiding) may do it while the node is still in place, and only after it is gone is
|
|
33
|
+
* that told apart from a click.
|
|
34
|
+
*/
|
|
35
|
+
export declare function whenFocusLeftForNothing(e: {
|
|
36
|
+
target: EventTarget | null;
|
|
37
|
+
relatedTarget: EventTarget | null;
|
|
38
|
+
}, then: () => void): void;
|
|
39
|
+
/**
|
|
40
|
+
* The tab stops around `frame`, outside it, nearest first on each side — where focus
|
|
41
|
+
* goes once `frame` is gone. Read BEFORE it goes: a removed node has no place in the
|
|
42
|
+
* document to measure from. `limit` per side keeps a long page's stops out of memory.
|
|
43
|
+
*
|
|
44
|
+
* Walked outwards from the frame, `limit` stops each way (0.52 code review,
|
|
45
|
+
* lib/focus-left.ts): read from every stop in the page, a banner under a 2000-row table
|
|
46
|
+
* read all 2000 to keep the nearest five before it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function tabStopsAround(frame: Element, limit?: number): {
|
|
49
|
+
after: HTMLElement[];
|
|
50
|
+
before: HTMLElement[];
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Focus the first of `candidates` that is still in the page and takes the focus; true
|
|
54
|
+
* when one did. Each is tried and checked, since `focus()` on a node that cannot hold it
|
|
55
|
+
* (detached, `display: none`, disabled since) fails without a word.
|
|
56
|
+
*/
|
|
57
|
+
export declare function focusFirst(candidates: Iterable<Element | null | undefined>): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Focus a container that is not a tab stop of its own — a table, a list — as the place
|
|
60
|
+
* the user is in, without making it one: `tabindex="-1"` for as long as it holds the
|
|
61
|
+
* focus, unless it already carries a `tabindex` of its own.
|
|
62
|
+
*/
|
|
63
|
+
export declare function focusContainer(el: HTMLElement): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const TAB_STOP = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]):not([type="hidden"]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
3
|
+
function isTabStop(el) {
|
|
4
|
+
return el.tabIndex >= 0 && !el.closest("[hidden],[inert]");
|
|
5
|
+
}
|
|
6
|
+
function tabStops(el, { includeSelf = true } = {}) {
|
|
7
|
+
const inside = Array.from(el.querySelectorAll(TAB_STOP)).filter(isTabStop);
|
|
8
|
+
const own = includeSelf && el instanceof HTMLElement && el.matches(TAB_STOP) && isTabStop(el);
|
|
9
|
+
return own ? [el, ...inside] : inside;
|
|
10
|
+
}
|
|
11
|
+
function firstTabStop(el, options) {
|
|
12
|
+
return tabStops(el, options)[0] ?? null;
|
|
13
|
+
}
|
|
14
|
+
function focusIsLost() {
|
|
15
|
+
if (typeof document === "undefined") return false;
|
|
16
|
+
const el = document.activeElement;
|
|
17
|
+
return !el || el === document.body || !el.isConnected;
|
|
18
|
+
}
|
|
19
|
+
function whenFocusLeftForNothing(e, then) {
|
|
20
|
+
const el = e.target;
|
|
21
|
+
if (e.relatedTarget !== null || !(el instanceof Element)) return;
|
|
22
|
+
queueMicrotask(() => {
|
|
23
|
+
if (!el.isConnected || el.matches(":disabled") || el.closest("[hidden],[inert]")) return;
|
|
24
|
+
if (typeof el.checkVisibility === "function" && !el.checkVisibility()) return;
|
|
25
|
+
if (focusIsLost()) then();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function tabStopsAround(frame, limit = 5) {
|
|
29
|
+
const doc = frame.ownerDocument;
|
|
30
|
+
const walker = doc.createTreeWalker(doc.documentElement, NodeFilter.SHOW_ELEMENT);
|
|
31
|
+
const isStop = (node2) => node2 instanceof HTMLElement && node2.matches(TAB_STOP) && isTabStop(node2);
|
|
32
|
+
const after = [];
|
|
33
|
+
walker.currentNode = frame;
|
|
34
|
+
let node = walker.nextSibling();
|
|
35
|
+
while (!node && walker.parentNode()) node = walker.nextSibling();
|
|
36
|
+
for (; node && after.length < limit; node = walker.nextNode()) if (isStop(node)) after.push(node);
|
|
37
|
+
const before = [];
|
|
38
|
+
walker.currentNode = frame;
|
|
39
|
+
for (node = walker.previousNode(); node && before.length < limit; node = walker.previousNode())
|
|
40
|
+
if (isStop(node)) before.push(node);
|
|
41
|
+
return { after, before };
|
|
42
|
+
}
|
|
43
|
+
function focusFirst(candidates) {
|
|
44
|
+
for (const el of candidates) {
|
|
45
|
+
if (!(el instanceof HTMLElement) || !el.isConnected) continue;
|
|
46
|
+
el.focus();
|
|
47
|
+
if (document.activeElement === el) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function focusContainer(el) {
|
|
52
|
+
if (!el.hasAttribute("tabindex")) {
|
|
53
|
+
el.setAttribute("tabindex", "-1");
|
|
54
|
+
el.addEventListener("blur", () => el.removeAttribute("tabindex"), { once: true });
|
|
55
|
+
}
|
|
56
|
+
el.focus();
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
firstTabStop,
|
|
60
|
+
focusContainer,
|
|
61
|
+
focusFirst,
|
|
62
|
+
focusIsLost,
|
|
63
|
+
tabStops,
|
|
64
|
+
tabStopsAround,
|
|
65
|
+
whenFocusLeftForNothing
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=focus-left.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/focus-left.ts"],"sourcesContent":["// \"The focused node left\" (refactor audit 2026-10-30, Wave 1: DSP-4, DSP-15). When the\n// element holding the keyboard's focus is removed — a table row deleted, filtered out or\n// approved; a bulk bar gone with its Clear; a banner its × took away — the browser puts\n// focus on <body> and says nothing: no blur in Chromium, none in jsdom. A keyboard or\n// screen-reader user then starts again from the top of the page. The overlays give focus\n// back to their opener (use-focus-trap); these helpers are for the controls that leave\n// the page while the focus is IN them, and decide where it goes instead.\n\n/**\n * Candidate tab stops, in DOM order — narrowed by {@link isTabStop} to the ones a Tab\n * really reaches. The kit's one copy of the rule (0.52 code review, lib/focus-left.ts):\n * this file and use-focus-trap each had it, with an exported `firstTabStop` apiece that\n * disagreed about the element itself, and a combobox panel's toolbar had a third\n * selector. The trap, the focus a removed node drops (below), the tour's field ring and\n * the toolbar all read it here, so \"the next stop\" is the same stop everywhere.\n *\n * The selector is the one `Modal` used. On its own it was not enough (round 0.34\n * BUG-4): only the bare `[tabindex]` clause excluded `-1`, so a `button`, an `input` or\n * a link taken out of the tab order still counted. A panel that ends in a roving grid —\n * the calendar's days, the month grid, the picker sheet's rows, every one of them `-1`\n * but the active cell — then had a \"last\" stop no Tab ever reaches: Tab from the real\n * last stop was not wrapped and walked out of the open panel, and Shift+Tab from the\n * first landed on a cell that is not a stop at all.\n */\n// `input[type=hidden]` is no stop: a Tab never lands on it and focus() on it does nothing,\n// but its tabIndex reads 0, so a trap could take a form's hidden field for its first or last\n// stop and Shift+Tab left the dialog (0.52 code review).\nconst TAB_STOP =\n 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]):not([type=\"hidden\"]),select:not([disabled]),[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * A stop the browser's own Tab would land on: in the tab order (`tabIndex` ≥ 0) and not\n * inside something `hidden` or `inert`. Removes only elements a native Tab never\n * reaches, so no correct order changes. `visibility: hidden` and closed `<details>` are\n * still not walked — they need layout, which this rule has never read.\n */\nfunction isTabStop(el: HTMLElement): boolean {\n return el.tabIndex >= 0 && !el.closest(\"[hidden],[inert]\");\n}\n\nexport interface TabStopOptions {\n /** Whether `el` itself counts, first, when it is a stop. Default true: the tour's\n * target may BE the field (refactor audit 2026-10-30, DUP-9). False for \"a control\n * inside it\" — a row, a drawer, a trap's container, a toolbar. */\n includeSelf?: boolean;\n}\n\n/** The tab stops in `el`, in DOM order — `el` itself first when it is one, unless\n * `includeSelf: false`. */\nexport function tabStops(el: Element, { includeSelf = true }: TabStopOptions = {}): HTMLElement[] {\n const inside = Array.from(el.querySelectorAll<HTMLElement>(TAB_STOP)).filter(isTabStop);\n const own = includeSelf && el instanceof HTMLElement && el.matches(TAB_STOP) && isTabStop(el);\n return own ? [el, ...inside] : inside;\n}\n\n/** The first of {@link tabStops} — the control a person types into or presses — or null. */\nexport function firstTabStop(el: Element, options?: TabStopOptions): HTMLElement | null {\n return tabStops(el, options)[0] ?? null;\n}\n\n/**\n * Whether the focus has fallen out of the page: onto `<body>`, onto nothing, or onto a\n * node no longer in the document. False on the server, where nothing has focus to lose.\n * A recovery asks this first: focus that went somewhere real (a dialog, the next field,\n * an app's own `focus()`) is the user's or the app's, and is left where it is.\n */\nexport function focusIsLost(): boolean {\n if (typeof document === \"undefined\") return false;\n const el = document.activeElement;\n return !el || el === document.body || !el.isConnected;\n}\n\n/**\n * Calls `then` when a `blur`/`focusout` was the user's own move to nothing — a click on\n * the page's background, or a click Safari answers by focusing nothing (it focuses no\n * button or link on a click, and blurs what had the focus) — after which a note of where\n * the focus was is stale (0.52 code review: DataTable's row, BulkActionBar's and\n * AlertBanner's notes outlived it, and a removal much later pulled the focus, and the\n * scroll, back to them).\n *\n * No element took the focus, the document holds none, and the node that lost it could\n * still hold it. Not when it was removed, hidden, made inert or disabled: that is the page\n * taking the focus away — the very case those notes are kept for. Not a window losing the\n * focus either, which leaves the node the document's active element. Decided a microtask\n * later, once the event's own work is done: a browser that blurs a node it is removing\n * (or hiding) may do it while the node is still in place, and only after it is gone is\n * that told apart from a click.\n */\nexport function whenFocusLeftForNothing(\n e: { target: EventTarget | null; relatedTarget: EventTarget | null },\n then: () => void,\n): void {\n const el = e.target;\n if (e.relatedTarget !== null || !(el instanceof Element)) return;\n queueMicrotask(() => {\n if (!el.isConnected || el.matches(\":disabled\") || el.closest(\"[hidden],[inert]\")) return;\n // `display: none`, where the browser can say (jsdom cannot).\n if (typeof el.checkVisibility === \"function\" && !el.checkVisibility()) return;\n if (focusIsLost()) then();\n });\n}\n\n/**\n * The tab stops around `frame`, outside it, nearest first on each side — where focus\n * goes once `frame` is gone. Read BEFORE it goes: a removed node has no place in the\n * document to measure from. `limit` per side keeps a long page's stops out of memory.\n *\n * Walked outwards from the frame, `limit` stops each way (0.52 code review,\n * lib/focus-left.ts): read from every stop in the page, a banner under a 2000-row table\n * read all 2000 to keep the nearest five before it.\n */\nexport function tabStopsAround(frame: Element, limit = 5): { after: HTMLElement[]; before: HTMLElement[] } {\n const doc = frame.ownerDocument;\n const walker = doc.createTreeWalker(doc.documentElement, NodeFilter.SHOW_ELEMENT);\n const isStop = (node: Node): node is HTMLElement =>\n node instanceof HTMLElement && node.matches(TAB_STOP) && isTabStop(node);\n // After: from the first element past the frame's own subtree, in document order.\n const after: HTMLElement[] = [];\n walker.currentNode = frame;\n let node: Node | null = walker.nextSibling();\n while (!node && walker.parentNode()) node = walker.nextSibling();\n for (; node && after.length < limit; node = walker.nextNode()) if (isStop(node)) after.push(node);\n // Before: back from the frame, nearest first (its ancestors among them, as before).\n const before: HTMLElement[] = [];\n walker.currentNode = frame;\n for (node = walker.previousNode(); node && before.length < limit; node = walker.previousNode())\n if (isStop(node)) before.push(node);\n return { after, before };\n}\n\n/**\n * Focus the first of `candidates` that is still in the page and takes the focus; true\n * when one did. Each is tried and checked, since `focus()` on a node that cannot hold it\n * (detached, `display: none`, disabled since) fails without a word.\n */\nexport function focusFirst(candidates: Iterable<Element | null | undefined>): boolean {\n for (const el of candidates) {\n if (!(el instanceof HTMLElement) || !el.isConnected) continue;\n el.focus();\n if (document.activeElement === el) return true;\n }\n return false;\n}\n\n/**\n * Focus a container that is not a tab stop of its own — a table, a list — as the place\n * the user is in, without making it one: `tabindex=\"-1\"` for as long as it holds the\n * focus, unless it already carries a `tabindex` of its own.\n */\nexport function focusContainer(el: HTMLElement): void {\n if (!el.hasAttribute(\"tabindex\")) {\n el.setAttribute(\"tabindex\", \"-1\");\n el.addEventListener(\"blur\", () => el.removeAttribute(\"tabindex\"), { once: true });\n }\n el.focus();\n}\n"],"mappings":";AA2BA,MAAM,WACJ;AAQF,SAAS,UAAU,IAA0B;AAC3C,SAAO,GAAG,YAAY,KAAK,CAAC,GAAG,QAAQ,kBAAkB;AAC3D;AAWO,SAAS,SAAS,IAAa,EAAE,cAAc,KAAK,IAAoB,CAAC,GAAkB;AAChG,QAAM,SAAS,MAAM,KAAK,GAAG,iBAA8B,QAAQ,CAAC,EAAE,OAAO,SAAS;AACtF,QAAM,MAAM,eAAe,cAAc,eAAe,GAAG,QAAQ,QAAQ,KAAK,UAAU,EAAE;AAC5F,SAAO,MAAM,CAAC,IAAI,GAAG,MAAM,IAAI;AACjC;AAGO,SAAS,aAAa,IAAa,SAA8C;AACtF,SAAO,SAAS,IAAI,OAAO,EAAE,CAAC,KAAK;AACrC;AAQO,SAAS,cAAuB;AACrC,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,KAAK,SAAS;AACpB,SAAO,CAAC,MAAM,OAAO,SAAS,QAAQ,CAAC,GAAG;AAC5C;AAkBO,SAAS,wBACd,GACA,MACM;AACN,QAAM,KAAK,EAAE;AACb,MAAI,EAAE,kBAAkB,QAAQ,EAAE,cAAc,SAAU;AAC1D,iBAAe,MAAM;AACnB,QAAI,CAAC,GAAG,eAAe,GAAG,QAAQ,WAAW,KAAK,GAAG,QAAQ,kBAAkB,EAAG;AAElF,QAAI,OAAO,GAAG,oBAAoB,cAAc,CAAC,GAAG,gBAAgB,EAAG;AACvE,QAAI,YAAY,EAAG,MAAK;AAAA,EAC1B,CAAC;AACH;AAWO,SAAS,eAAe,OAAgB,QAAQ,GAAoD;AACzG,QAAM,MAAM,MAAM;AAClB,QAAM,SAAS,IAAI,iBAAiB,IAAI,iBAAiB,WAAW,YAAY;AAChF,QAAM,SAAS,CAACA,UACdA,iBAAgB,eAAeA,MAAK,QAAQ,QAAQ,KAAK,UAAUA,KAAI;AAEzE,QAAM,QAAuB,CAAC;AAC9B,SAAO,cAAc;AACrB,MAAI,OAAoB,OAAO,YAAY;AAC3C,SAAO,CAAC,QAAQ,OAAO,WAAW,EAAG,QAAO,OAAO,YAAY;AAC/D,SAAO,QAAQ,MAAM,SAAS,OAAO,OAAO,OAAO,SAAS,EAAG,KAAI,OAAO,IAAI,EAAG,OAAM,KAAK,IAAI;AAEhG,QAAM,SAAwB,CAAC;AAC/B,SAAO,cAAc;AACrB,OAAK,OAAO,OAAO,aAAa,GAAG,QAAQ,OAAO,SAAS,OAAO,OAAO,OAAO,aAAa;AAC3F,QAAI,OAAO,IAAI,EAAG,QAAO,KAAK,IAAI;AACpC,SAAO,EAAE,OAAO,OAAO;AACzB;AAOO,SAAS,WAAW,YAA2D;AACpF,aAAW,MAAM,YAAY;AAC3B,QAAI,EAAE,cAAc,gBAAgB,CAAC,GAAG,YAAa;AACrD,OAAG,MAAM;AACT,QAAI,SAAS,kBAAkB,GAAI,QAAO;AAAA,EAC5C;AACA,SAAO;AACT;AAOO,SAAS,eAAe,IAAuB;AACpD,MAAI,CAAC,GAAG,aAAa,UAAU,GAAG;AAChC,OAAG,aAAa,YAAY,IAAI;AAChC,OAAG,iBAAiB,QAAQ,MAAM,GAAG,gBAAgB,UAAU,GAAG,EAAE,MAAM,KAAK,CAAC;AAAA,EAClF;AACA,KAAG,MAAM;AACX;","names":["node"]}
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -142,8 +142,8 @@ export declare function toDate(value: DateInput): Date | null;
|
|
|
142
142
|
*/
|
|
143
143
|
export declare function formatDate(value: DateInput, style?: FormatDateStyle | Intl.DateTimeFormatOptions, options?: FormatDateOptions): string;
|
|
144
144
|
export interface FormatRelativeTimeOptions extends BaseOptions {
|
|
145
|
-
/** The zone of the date printed past `absoluteAfterDays` (0.46)
|
|
146
|
-
* device's. */
|
|
145
|
+
/** The zone of the date printed past `absoluteAfterDays` (0.46), and (0.51.7) the zone
|
|
146
|
+
* whose calendar days "yesterday" and "2 days ago" count. Left out: the device's. */
|
|
147
147
|
timeZone?: string;
|
|
148
148
|
/** The moment it is measured from. Default: now. */
|
|
149
149
|
now?: DateInput;
|
|
@@ -164,7 +164,9 @@ export interface FormatRelativeTimeOptions extends BaseOptions {
|
|
|
164
164
|
* How long ago (or until) `value` is, in `locale`'s words, through
|
|
165
165
|
* `Intl.RelativeTimeFormat`: "3 hours ago", "vor 3 Stunden", "il y a 3 heures",
|
|
166
166
|
* "in 2 days", and under 45 seconds "now". The unit is picked by size — seconds,
|
|
167
|
-
* minutes, hours, days, weeks, months, years — and the count is rounded
|
|
167
|
+
* minutes, hours, days, weeks, months, years — and the count is rounded; days are
|
|
168
|
+
* CALENDAR days (0.51.7), in `timeZone` when given: "yesterday" is the day before today,
|
|
169
|
+
* however few hours ago that was.
|
|
168
170
|
*/
|
|
169
171
|
export declare function formatRelativeTime(value: DateInput, options?: FormatRelativeTimeOptions): string;
|
|
170
172
|
type WithoutLocale<T> = Omit<T, "locale"> & {
|
package/dist/lib/format.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { useKitLocale, useKitTimeZone } from "../i18n/kit-labels.js";
|
|
4
4
|
import { parseIsoDate } from "./dates.js";
|
|
5
|
+
import { dateFormatter } from "./intl-cache.js";
|
|
6
|
+
import { isBareIsoDay } from "./iso-day.js";
|
|
5
7
|
import { formatWithMinus } from "./minus.js";
|
|
6
8
|
import { roundHalfAway } from "./round.js";
|
|
7
9
|
import { formatWithMinus as formatWithMinus2 } from "./minus.js";
|
|
@@ -111,28 +113,17 @@ const DATE_STYLES = {
|
|
|
111
113
|
time: { timeStyle: "short" },
|
|
112
114
|
monthYear: { month: "long", year: "numeric" }
|
|
113
115
|
};
|
|
114
|
-
const BARE_ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
115
116
|
function toDate(value) {
|
|
116
117
|
if (value === null || value === void 0 || value === "") return null;
|
|
117
|
-
if (
|
|
118
|
+
if (isBareIsoDay(value)) return parseIsoDate(value);
|
|
118
119
|
const d = value instanceof Date ? value : new Date(value);
|
|
119
120
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
120
121
|
}
|
|
121
|
-
const dateFormatters = /* @__PURE__ */ new Map();
|
|
122
|
-
function dateFormatter(locale, options) {
|
|
123
|
-
const key = `${locale ?? ""}|${JSON.stringify(options)}`;
|
|
124
|
-
let f = dateFormatters.get(key);
|
|
125
|
-
if (!f) {
|
|
126
|
-
f = new Intl.DateTimeFormat(locale, options);
|
|
127
|
-
dateFormatters.set(key, f);
|
|
128
|
-
}
|
|
129
|
-
return f;
|
|
130
|
-
}
|
|
131
122
|
function formatDate(value, style = "medium", options = {}) {
|
|
132
123
|
const d = toDate(value);
|
|
133
124
|
if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
134
125
|
const base = typeof style === "string" ? DATE_STYLES[style] : style;
|
|
135
|
-
if (
|
|
126
|
+
if (isBareIsoDay(value)) {
|
|
136
127
|
const day = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
137
128
|
return dateFormatter(options.locale, { ...base, timeZone: "UTC" }).format(day);
|
|
138
129
|
}
|
|
@@ -151,6 +142,13 @@ const RELATIVE_STEPS = [
|
|
|
151
142
|
[320 * DAY, "month", 30.44 * DAY],
|
|
152
143
|
[Infinity, "year", 365.25 * DAY]
|
|
153
144
|
];
|
|
145
|
+
function dayNumber(d, timeZone) {
|
|
146
|
+
if (!timeZone) return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY;
|
|
147
|
+
const fields = { timeZone, year: "numeric", month: "numeric", day: "numeric" };
|
|
148
|
+
const parts = dateFormatter("en-US", fields).formatToParts(d);
|
|
149
|
+
const part = (type) => Number(parts.find((p) => p.type === type)?.value);
|
|
150
|
+
return Date.UTC(part("year"), part("month") - 1, part("day")) / DAY;
|
|
151
|
+
}
|
|
154
152
|
const relativeFormatters = /* @__PURE__ */ new Map();
|
|
155
153
|
function relativeFormatter(locale, numeric, style) {
|
|
156
154
|
const key = `${locale ?? ""}|${numeric}|${style}`;
|
|
@@ -172,6 +170,11 @@ function formatRelativeTime(value, options = {}) {
|
|
|
172
170
|
}
|
|
173
171
|
const rtf = relativeFormatter(options.locale, options.numeric ?? "auto", options.style ?? "long");
|
|
174
172
|
const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit);
|
|
173
|
+
if (unit === "day") {
|
|
174
|
+
const bareDay = isBareIsoDay(value);
|
|
175
|
+
const days = dayNumber(d, bareDay ? void 0 : options.timeZone) - dayNumber(now, options.timeZone);
|
|
176
|
+
return days === 0 ? rtf.format(Math.round(diff / HOUR), "hour") : rtf.format(days, "day");
|
|
177
|
+
}
|
|
175
178
|
const count = unit === "second" ? 0 : Math.round(diff / size);
|
|
176
179
|
return rtf.format(count + 0, unit);
|
|
177
180
|
}
|
package/dist/lib/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/format.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLocale, useKitTimeZone } from \"../i18n/kit-labels\";\nimport { parseIsoDate } from \"./dates\";\nimport { formatWithMinus, type MinusStyle } from \"./minus\";\nimport { roundHalfAway } from \"./round\";\n\nexport type { MinusStyle } from \"./minus\";\n// 0.50 (KIT-1 follow-up): the kit's minus for an app's OWN `Intl.NumberFormat` wrapper.\nexport { formatWithMinus } from \"./minus\";\n\n/**\n * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,\n * for the figures an app prints OUTSIDE a kit component.\n *\n * Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned\n * `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`\n * whose \"5m ago\" / \"3h ago\" / \"2d ago\" stayed English in every language), its tenancy\n * `format.ts` (`formatChf`, a `formatPct` gluing a \"+\" onto `toFixed(2)`), keksdose's\n * own set. Every one of them is `Intl` with a locale bolted on, so the only thing\n * worth sharing is the locale — and the kit already has one, the provider's.\n *\n * The plain functions take a `locale` (none: the runtime default, as every `Intl` API\n * reads `undefined`). Inside a component, {@link useKitFormat} hands back the same\n * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the\n * kit components on it.\n *\n * A missing value — `null`, `undefined`, an unparseable date — formats as `empty`\n * (default \"—\", the dash `StatTile` shows for no value), never as \"NaN\" or \"Invalid\n * Date\". A number may also arrive as a numeric STRING (0.49, KIT-2) — \"1850.00\", \"-12.5\",\n * \"0\": a Decimal field off a Python API — and is formatted like the number. Anything\n * else (a non-numeric string, an object, `NaN`, ±`Infinity`) prints `empty` too, but is\n * a bug upstream rather than a gap in the data, so a development build says so on the\n * console, once per call site. Zero is a figure: `0` and `\"0\"` print \"CHF 0.00\", never\n * the dash.\n *\n * A negative figure carries the typographic minus, U+2212 (0.49, KIT-1), in every\n * locale — `Intl` writes a hyphen in most. `minus: \"ascii\"` for text a machine reads\n * back (a CSV, the clipboard).\n */\n\n/** What every formatter here prints for a missing value, unless told otherwise. */\nexport const EMPTY_FORMATTED_VALUE = \"—\";\n\n/** Fraction digits: a number pins both the minimum and the maximum (`2` → \"3.10\");\n * an object sets them apart (a meter reading, 1–3). */\nexport type FormatDigits = number | { min?: number; max?: number };\n\ninterface BaseOptions {\n /** BCP 47 tag. Left out: the runtime default (the hook: the provider's). */\n locale?: string;\n /** Printed for a missing value. Default {@link EMPTY_FORMATTED_VALUE}. */\n empty?: string;\n}\n\n/** The options every number formatter shares (money and percent carry their own unit). */\nexport interface FormatNumberBaseOptions extends BaseOptions {\n digits?: FormatDigits;\n /** Compact notation: 12 400 → \"12K\". */\n compact?: boolean;\n /** `\"exceptZero\"` for a change (\"+3\", \"−3\", \"0\"). Default: the minus only. */\n signDisplay?: Intl.NumberFormatOptions[\"signDisplay\"];\n /** Any further `Intl.NumberFormat` options, merged over the ones above. */\n options?: Intl.NumberFormatOptions;\n /**\n * The minus sign of a negative figure (0.49, KIT-1). `\"typographic\"` (default):\n * U+2212, as wide as \"+\" and the digits, as `SignedAmount` prints it — in every\n * locale, also where `Intl` writes a hyphen. `\"ascii\"`: U+002D, for an export a\n * machine parses back (a CSV download, the clipboard) — there, also pass\n * `options: { useGrouping: false }` or write the raw number.\n */\n minus?: MinusStyle;\n}\n\n/** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */\nexport interface FormatNumberOptions extends FormatNumberBaseOptions {\n /**\n * A unit printed after the figure, joined by a narrow no-break space (U+202F):\n * \"3.400 N\", \"12 kg\", \"4,5 m²\". lenkbank (P8) prints forces, masses and lengths in\n * every table and summary, and each call site glued `${n} N` together with a plain\n * space — which wraps the \"N\" onto the next line in a narrow column and reads loose\n * beside the digit grouping. The narrow no-break space is the SI brochure's and the\n * Swiss typesetting rule's separator between value and unit, and it never breaks.\n *\n * Free text, not `Intl`'s `style: \"unit\"`: that one only knows the ECMA-402\n * sanctioned units (no \"N\", no \"kN\", no \"m³/h\"), so an app would still be gluing.\n * A missing value prints `empty` ALONE — \"— N\" says there is a newton-shaped\n * nothing, which is noise in a column that already heads its unit.\n */\n unit?: string;\n}\n\n/** Narrow no-break space (U+202F): between a figure and its {@link FormatNumberOptions.unit}. */\nconst UNIT_SEPARATOR = \"\\u202F\";\n\nexport interface FormatMoneyOptions extends FormatNumberBaseOptions {\n /** `\"symbol\"` (default), `\"narrowSymbol\"`, `\"code\"` (\"CHF 12.00\") or `\"name\"`. */\n currencyDisplay?: Intl.NumberFormatOptions[\"currencyDisplay\"];\n}\n\nexport interface FormatPercentOptions extends FormatNumberBaseOptions {\n /**\n * `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read\n * it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %\n * (kastlan's rent `change_pct`).\n */\n ratio?: boolean;\n}\n\n// One formatter per option set: a table formats the same way thousands of times, and\n// constructing an Intl formatter is the expensive part.\nconst numberFormatters = new Map<string, Intl.NumberFormat>();\nfunction numberFormatter(locale: string | undefined, options: Intl.NumberFormatOptions): Intl.NumberFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = numberFormatters.get(key);\n if (!f) {\n f = new Intl.NumberFormat(locale, options);\n numberFormatters.set(key, f);\n }\n return f;\n}\n\nfunction digitOptions(digits: FormatDigits | undefined): Intl.NumberFormatOptions {\n if (digits === undefined) return {};\n if (typeof digits === \"number\") return { minimumFractionDigits: digits, maximumFractionDigits: digits };\n return {\n ...(digits.min !== undefined ? { minimumFractionDigits: digits.min } : null),\n ...(digits.max !== undefined ? { maximumFractionDigits: digits.max } : null),\n };\n}\n\nfunction numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {\n return {\n ...digitOptions(o.digits),\n ...(o.compact ? { notation: \"compact\" } : null),\n ...(o.signDisplay ? { signDisplay: o.signDisplay } : null),\n ...o.options,\n };\n}\n\n/**\n * What the number formatters take (0.49, KIT-2): a number, a numeric string (\"1850.00\",\n * \"-12.5\", \"1e3\" — what a Decimal arrives as in JSON), or `null` / `undefined` for no\n * value. A string is read with `Number()`, so a figure with more significant digits than\n * a double holds (about 15–17) is printed as its nearest double.\n */\nexport type FormattableNumber = number | string | null | undefined;\n\n/** A plain decimal: optional sign, digits with an optional point, an optional exponent.\n * No grouping marks, no decimal comma, no currency — those are formatted text, and\n * reading them back is a locale's guess. */\nconst NUMERIC_STRING = /^[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?$/;\n\n/** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a\n * production one (the consumer's bundler makes `import.meta.env.DEV` false) and never\n * under vitest. Read per call so a test can switch it on with `vi.stubEnv`. */\nfunction devWarnings(): boolean {\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n}\n\nconst warnedSites = new Set<string>();\n/** This module's own frames, skipped to find the caller (\"Object.formatMoney\" is a\n * `useKitFormat` wrapper). */\nconst OWN_FRAME = /\\b(?:formatNumber|formatMoney|formatPercent|toFormattable|warnInvalid|callSite)\\b/;\n\n/** The first stack frame outside the formatters: where the bad value was passed. */\nfunction callSite(): string {\n const frames = (new Error().stack ?? \"\").split(\"\\n\").slice(1);\n return frames.find((frame) => !OWN_FRAME.test(frame))?.trim() ?? \"\";\n}\n\nfunction describeValue(value: unknown): string {\n if (typeof value === \"number\" || typeof value === \"bigint\" || typeof value === \"boolean\") return String(value);\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return Object.prototype.toString.call(value);\n }\n}\n\n/** A development build's console note about an invalid value — once per call site. */\nfunction warnInvalid(fn: string, value: unknown): void {\n if (!devWarnings()) return;\n const site = callSite();\n const key = `${fn}|${site || typeof value}`;\n if (warnedSites.has(key)) return;\n warnedSites.add(key);\n const type = Array.isArray(value) ? \"array\" : typeof value;\n console.warn(\n `[ui-kit] ${fn} got ${describeValue(value)} (${type}), which is not a number: printed the empty value. ` +\n \"Pass a number, a numeric string, or null/undefined for a missing value.\" +\n (site ? `\\n at ${site.replace(/^at /, \"\")}` : \"\"),\n );\n}\n\n/**\n * `value` as a finite number to format, or `undefined` to print `empty`. `null` and\n * `undefined` are a missing value and pass quietly; anything that is neither a finite\n * number nor a numeric string is an invalid one — ±Infinity included (lenkbank, adopting\n * 0.13: a division by zero upstream; \"∞\" in a money column reads like data) — and a\n * development build warns. `-0` is folded into `0`: zero carries no sign.\n */\nfunction toFormattable(fn: string, value: unknown): number | undefined {\n if (value === null || value === undefined) return undefined;\n let n: number | undefined;\n if (typeof value === \"number\") n = value;\n else if (typeof value === \"string\" && NUMERIC_STRING.test(value.trim())) n = Number(value.trim());\n if (n === undefined || !Number.isFinite(n)) {\n warnInvalid(fn, value);\n return undefined;\n }\n return n + 0;\n}\n\n/** A number in `locale`: \"1,234.5\", \"1.234,5\", \"1’234.5\" — with `unit`, \"3.400 N\". */\nexport function formatNumber(value: FormattableNumber, options: FormatNumberOptions = {}): string {\n const n = toFormattable(\"formatNumber\", value);\n if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const figure = formatWithMinus(numberFormatter(options.locale, numberOptions(options)), n, options.minus);\n return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;\n}\n\n/** An amount of `currency` (ISO 4217) in `locale`: \"CHF 1’234.50\", \"1.234,50 €\". */\nexport function formatMoney(value: FormattableNumber, currency: string, options: FormatMoneyOptions = {}): string {\n const n = toFormattable(\"formatMoney\", value);\n if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const formatter = numberFormatter(options.locale, {\n style: \"currency\",\n currency,\n ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),\n ...numberOptions(options),\n });\n return formatWithMinus(formatter, n, options.minus);\n}\n\n/** A percentage in `locale` (\"12%\", \"12 %\", \"+1.25%\"). See {@link FormatPercentOptions.ratio}\n * for what `value` means. Default digits: at most one. */\nexport function formatPercent(value: FormattableNumber, options: FormatPercentOptions = {}): string {\n const n = toFormattable(\"formatPercent\", value);\n if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const ratio = options.ratio === false ? n / 100 : n;\n const formatter = numberFormatter(options.locale, {\n style: \"percent\",\n ...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),\n ...numberOptions(options),\n });\n return formatWithMinus(formatter, ratio, options.minus);\n}\n\n/* ── Dates ─────────────────────────────────────────────────────────────────── */\n\n/**\n * The shapes a date is printed in, each a set of `Intl.DateTimeFormat` options:\n * - `short` / `medium` / `long` / `full` — the date alone, at that `dateStyle`\n * (\"08/07/2026\", \"8 Jul 2026\", \"8 July 2026\", \"Wednesday, 8 July 2026\" in en-GB).\n * - `dateTime` — medium date and short time: a timestamp (\"8 Jul 2026, 14:05\").\n * - `time` — the short time alone (\"14:05\").\n * - `monthYear` — \"July 2026\".\n *\n * Or pass `Intl.DateTimeFormatOptions` for anything else. A date-fns PATTERN\n * (\"dd.MM.yyyy\") is deliberately not accepted: a pattern is one locale's order baked\n * into a string, which is how an English page ends up with German dates.\n */\nexport type FormatDateStyle = \"short\" | \"medium\" | \"long\" | \"full\" | \"dateTime\" | \"time\" | \"monthYear\";\n\nconst DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {\n short: { dateStyle: \"short\" },\n medium: { dateStyle: \"medium\" },\n long: { dateStyle: \"long\" },\n full: { dateStyle: \"full\" },\n dateTime: { dateStyle: \"medium\", timeStyle: \"short\" },\n time: { timeStyle: \"short\" },\n monthYear: { month: \"long\", year: \"numeric\" },\n};\n\nexport interface FormatDateOptions extends BaseOptions {\n /** An IANA zone (\"Europe/Zurich\") for a timestamp. Left out: the device's. A bare\n * \"YYYY-MM-DD\" ignores it: a calendar day is the day it names in every zone. */\n timeZone?: string;\n}\n\n/** A bare ISO calendar day, `\"YYYY-MM-DD\"`. */\nconst BARE_ISO_DAY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/** What {@link formatDate} and {@link formatRelativeTime} accept. */\nexport type DateInput = string | number | Date | null | undefined;\n\n/**\n * `value` as a Date, or null. A bare \"YYYY-MM-DD\" is a CALENDAR day and is read on the\n * local calendar (`parseIsoDate`) — `new Date(\"2026-07-08\")` is UTC midnight, which is\n * the 7th anywhere west of Greenwich. Anything else (a full ISO timestamp, epoch\n * milliseconds, a Date) is an instant and goes through `Date` as is.\n */\nexport function toDate(value: DateInput): Date | null {\n if (value === null || value === undefined || value === \"\") return null;\n if (typeof value === \"string\" && BARE_ISO_DAY.test(value)) return parseIsoDate(value);\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\nconst dateFormatters = new Map<string, Intl.DateTimeFormat>();\nfunction dateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = dateFormatters.get(key);\n if (!f) {\n f = new Intl.DateTimeFormat(locale, options);\n dateFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * A date or timestamp in `locale`. Takes an ISO date (\"2026-07-08\", read on the local\n * calendar), an ISO timestamp (\"2026-07-08T14:05:00Z\"), epoch milliseconds or a Date.\n * `style` defaults to `medium`; see {@link FormatDateStyle}.\n *\n * `formatIsoDate` (`@eifi1/ui-kit/dates`) remains for a plain \"YYYY-MM-DD\" with an\n * explicit locale; this is its superset for timestamps and the named styles.\n */\nexport function formatDate(\n value: DateInput,\n style: FormatDateStyle | Intl.DateTimeFormatOptions = \"medium\",\n options: FormatDateOptions = {},\n): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const base = typeof style === \"string\" ? DATE_STYLES[style] : style;\n // A bare \"YYYY-MM-DD\" is a calendar day, which has no zone: it is the day it names\n // wherever it is read, so a `timeZone` (in `options` or the style) never moves it — the\n // device's local midnight shown in a zone west of it was the day before (round 0.45, N9).\n if (typeof value === \"string\" && BARE_ISO_DAY.test(value)) {\n const day = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));\n return dateFormatter(options.locale, { ...base, timeZone: \"UTC\" }).format(day);\n }\n return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);\n}\n\nexport interface FormatRelativeTimeOptions extends BaseOptions {\n /** The zone of the date printed past `absoluteAfterDays` (0.46). Left out: the\n * device's. */\n timeZone?: string;\n /** The moment it is measured from. Default: now. */\n now?: DateInput;\n /** `\"auto\"` (default): \"yesterday\", \"now\", \"next week\". `\"always\"`: \"1 day ago\". */\n numeric?: Intl.RelativeTimeFormatNumeric;\n /** `\"long\"` (default) \"3 hours ago\", `\"short\"` \"3 hr. ago\", `\"narrow\"` \"3h ago\". */\n style?: Intl.RelativeTimeFormatStyle;\n /**\n * Past this many days either way, print the date instead ({@link formatDate} in\n * `absoluteStyle`) — kastlan's feed switches to the date after a week, because \"5\n * weeks ago\" makes a reader do sums. Default: never.\n */\n absoluteAfterDays?: number;\n /** The style of that date. Default `medium`. */\n absoluteStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n}\n\nconst SECOND = 1000;\nconst MINUTE = 60 * SECOND;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\n/** [the largest |difference| a unit is used for, the unit, its length]. */\nconst RELATIVE_STEPS: Array<[number, Intl.RelativeTimeFormatUnit, number]> = [\n [45 * SECOND, \"second\", SECOND],\n [45 * MINUTE, \"minute\", MINUTE],\n [22 * HOUR, \"hour\", HOUR],\n [6.5 * DAY, \"day\", DAY],\n [26 * DAY, \"week\", 7 * DAY],\n [320 * DAY, \"month\", 30.44 * DAY],\n [Infinity, \"year\", 365.25 * DAY],\n];\n\nconst relativeFormatters = new Map<string, Intl.RelativeTimeFormat>();\nfunction relativeFormatter(\n locale: string | undefined,\n numeric: Intl.RelativeTimeFormatNumeric,\n style: Intl.RelativeTimeFormatStyle,\n): Intl.RelativeTimeFormat {\n const key = `${locale ?? \"\"}|${numeric}|${style}`;\n let f = relativeFormatters.get(key);\n if (!f) {\n f = new Intl.RelativeTimeFormat(locale, { numeric, style });\n relativeFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * How long ago (or until) `value` is, in `locale`'s words, through\n * `Intl.RelativeTimeFormat`: \"3 hours ago\", \"vor 3 Stunden\", \"il y a 3 heures\",\n * \"in 2 days\", and under 45 seconds \"now\". The unit is picked by size — seconds,\n * minutes, hours, days, weeks, months, years — and the count is rounded.\n */\nexport function formatRelativeTime(value: DateInput, options: FormatRelativeTimeOptions = {}): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const now = toDate(options.now ?? new Date()) ?? new Date();\n const diff = d.getTime() - now.getTime();\n const abs = Math.abs(diff);\n if (options.absoluteAfterDays !== undefined && abs > options.absoluteAfterDays * DAY) {\n return formatDate(d, options.absoluteStyle ?? \"medium\", options);\n }\n const rtf = relativeFormatter(options.locale, options.numeric ?? \"auto\", options.style ?? \"long\");\n const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit)!;\n // Under 45 seconds is \"now\" rather than \"12 seconds ago\": a feed that ticks through\n // the seconds is noise, and nothing reads a timestamp to the second.\n const count = unit === \"second\" ? 0 : Math.round(diff / size);\n // `+ 0` folds -0 into 0: `format(-0, …)` says \"0 seconds ago\" instead of \"now\".\n return rtf.format(count + 0, unit);\n}\n\n/* ── Bound to the provider ─────────────────────────────────────────────────── */\n\ntype WithoutLocale<T> = Omit<T, \"locale\"> & { locale?: string };\n\n/** What {@link useKitFormat} returns: the formatters above with the provider's locale\n * as their default (an explicit `locale` in the options still wins). */\nexport interface KitFormat {\n /** The locale they format in — the provider's, or `undefined` (runtime default). */\n locale: string | undefined;\n formatNumber: (value: FormattableNumber, options?: WithoutLocale<FormatNumberOptions>) => string;\n formatMoney: (value: FormattableNumber, currency: string, options?: WithoutLocale<FormatMoneyOptions>) => string;\n formatPercent: (value: FormattableNumber, options?: WithoutLocale<FormatPercentOptions>) => string;\n formatDate: (\n value: DateInput,\n style?: FormatDateStyle | Intl.DateTimeFormatOptions,\n options?: WithoutLocale<FormatDateOptions>,\n ) => string;\n formatRelativeTime: (value: DateInput, options?: WithoutLocale<FormatRelativeTimeOptions>) => string;\n}\n\n/**\n * The formatters, bound to the `<UiKitProvider locale>` (or to `locale`, which wins\n * over it — the same precedence as a component's `locale` prop):\n *\n * const { formatMoney, formatRelativeTime } = useKitFormat();\n * formatMoney(row.balance, \"CHF\");\n */\nexport function useKitFormat(locale?: string): KitFormat {\n const kitLocale = useKitLocale(locale);\n // 0.46 (§10.3): the provider's `timeZone` for timestamps; a call's own wins, and a bare\n // \"YYYY-MM-DD\" ignores it (formatDate).\n const timeZone = useKitTimeZone();\n return useMemo<KitFormat>(\n () => ({\n locale: kitLocale,\n formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),\n formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),\n formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),\n formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, timeZone, ...options }),\n formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, timeZone, ...options }),\n }),\n [kitLocale, timeZone],\n );\n}\n\n/* ── Currency minor units ──────────────────────────────────────────────────── */\n\n/**\n * The decimals a currency is written with — its minor unit: CHF and EUR 2, JPY 0,\n * KWD 3 — from `Intl`, or `undefined` for a code `Intl` does not accept. The rule\n * AmountInput settles a typed figure by, for a host that needs it outside the field.\n */\nexport function currencyMinorDigits(currency: string | null | undefined): number | undefined {\n if (!currency) return undefined;\n try {\n return new Intl.NumberFormat(\"en\", { style: \"currency\", currency }).resolvedOptions().maximumFractionDigits;\n } catch {\n return undefined;\n }\n}\n\n/** The options form of {@link roundToCurrency}'s third argument. */\nexport interface RoundToCurrencyOptions {\n /** Decimals to round to, whatever the currency — the plain `digits` argument. */\n digits?: number;\n /**\n * Decimals for a currency `Intl` cannot resolve — no code yet (`null`, `\"\"`), or a\n * malformed one (\"EURO\", \"€\"). Left out, such a value comes back unchanged, as\n * before. Note that `Intl` resolves ANY well-formed three-letter code, a made-up\n * \"XYZ\" included (to 2), so this is the case of a missing or broken code, not of a\n * rare currency.\n */\n fallbackDigits?: number;\n}\n\n/**\n * `value` rounded to the currency's minor unit (or to `digits`), half away from zero,\n * in decimal arithmetic: 93.4213 CHF → 93.42, 1.005 EUR → 1.01 (`Math.round(1.005 *\n * 100)` is 100, since 1.005 is 1.00499… in binary). A non-finite value, or a currency\n * `Intl` does not know without `digits`, comes back unchanged (keksdose live #356: an\n * FX estimate set into an AmountInput showed four decimals) — unless the options form\n * gives `fallbackDigits`: `roundToCurrency(v, code, { fallbackDigits: 2 })` rounds a\n * figure whose currency is not chosen yet to cents rather than leaving 0.1 + 0.2 as is.\n * A finite value never comes back NaN or -0, however small or large (BUG-11):\n * 0.1 + 0.2 - 0.3 → 0, -0.004 → 0.\n */\nexport function roundToCurrency(\n value: number,\n currency: string | null | undefined,\n digits?: number | RoundToCurrencyOptions,\n): number {\n const options = typeof digits === \"object\" && digits !== null ? digits : { digits };\n const places = options.digits ?? currencyMinorDigits(currency) ?? options.fallbackDigits;\n if (places === undefined) return value;\n return roundHalfAway(value, places);\n}\n"],"mappings":";AAAA,SAAS,eAAe;AACxB,SAAS,cAAc,sBAAsB;AAC7C,SAAS,oBAAoB;AAC7B,SAAS,uBAAwC;AACjD,SAAS,qBAAqB;AAI9B,SAAS,mBAAAA,wBAAuB;AAiCzB,MAAM,wBAAwB;AAmDrC,MAAM,iBAAiB;AAkBvB,MAAM,mBAAmB,oBAAI,IAA+B;AAC5D,SAAS,gBAAgB,QAA4B,SAAsD;AACzG,QAAM,MAAM,GAAG,UAAU,EAAE,IAAI,KAAK,UAAU,OAAO,CAAC;AACtD,MAAI,IAAI,iBAAiB,IAAI,GAAG;AAChC,MAAI,CAAC,GAAG;AACN,QAAI,IAAI,KAAK,aAAa,QAAQ,OAAO;AACzC,qBAAiB,IAAI,KAAK,CAAC;AAAA,EAC7B;AACA,SAAO;AACT;AAEA,SAAS,aAAa,QAA4D;AAChF,MAAI,WAAW,OAAW,QAAO,CAAC;AAClC,MAAI,OAAO,WAAW,SAAU,QAAO,EAAE,uBAAuB,QAAQ,uBAAuB,OAAO;AACtG,SAAO;AAAA,IACL,GAAI,OAAO,QAAQ,SAAY,EAAE,uBAAuB,OAAO,IAAI,IAAI;AAAA,IACvE,GAAI,OAAO,QAAQ,SAAY,EAAE,uBAAuB,OAAO,IAAI,IAAI;AAAA,EACzE;AACF;AAEA,SAAS,cAAc,GAAsD;AAC3E,SAAO;AAAA,IACL,GAAG,aAAa,EAAE,MAAM;AAAA,IACxB,GAAI,EAAE,UAAU,EAAE,UAAU,UAAU,IAAI;AAAA,IAC1C,GAAI,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,IAAI;AAAA,IACrD,GAAG,EAAE;AAAA,EACP;AACF;AAaA,MAAM,iBAAiB;AAKvB,SAAS,cAAuB;AAC9B,SAAO,QAAQ,YAAY,KAAK,GAAG,KAAK,CAAC,YAAY,KAAK;AAC5D;AAEA,MAAM,cAAc,oBAAI,IAAY;AAGpC,MAAM,YAAY;AAGlB,SAAS,WAAmB;AAC1B,QAAM,UAAU,IAAI,MAAM,EAAE,SAAS,IAAI,MAAM,IAAI,EAAE,MAAM,CAAC;AAC5D,SAAO,OAAO,KAAK,CAAC,UAAU,CAAC,UAAU,KAAK,KAAK,CAAC,GAAG,KAAK,KAAK;AACnE;AAEA,SAAS,cAAc,OAAwB;AAC7C,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,UAAW,QAAO,OAAO,KAAK;AAC7G,MAAI;AACF,WAAO,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;AAAA,EAC9C,QAAQ;AACN,WAAO,OAAO,UAAU,SAAS,KAAK,KAAK;AAAA,EAC7C;AACF;AAGA,SAAS,YAAY,IAAY,OAAsB;AACrD,MAAI,CAAC,YAAY,EAAG;AACpB,QAAM,OAAO,SAAS;AACtB,QAAM,MAAM,GAAG,EAAE,IAAI,QAAQ,OAAO,KAAK;AACzC,MAAI,YAAY,IAAI,GAAG,EAAG;AAC1B,cAAY,IAAI,GAAG;AACnB,QAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,UAAU,OAAO;AACrD,UAAQ;AAAA,IACN,YAAY,EAAE,QAAQ,cAAc,KAAK,CAAC,KAAK,IAAI,gIAEhD,OAAO;AAAA,OAAU,KAAK,QAAQ,QAAQ,EAAE,CAAC,KAAK;AAAA,EACnD;AACF;AASA,SAAS,cAAc,IAAY,OAAoC;AACrE,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI;AACJ,MAAI,OAAO,UAAU,SAAU,KAAI;AAAA,WAC1B,OAAO,UAAU,YAAY,eAAe,KAAK,MAAM,KAAK,CAAC,EAAG,KAAI,OAAO,MAAM,KAAK,CAAC;AAChG,MAAI,MAAM,UAAa,CAAC,OAAO,SAAS,CAAC,GAAG;AAC1C,gBAAY,IAAI,KAAK;AACrB,WAAO;AAAA,EACT;AACA,SAAO,IAAI;AACb;AAGO,SAAS,aAAa,OAA0B,UAA+B,CAAC,GAAW;AAChG,QAAM,IAAI,cAAc,gBAAgB,KAAK;AAC7C,MAAI,MAAM,OAAW,QAAO,QAAQ,SAAS;AAC7C,QAAM,SAAS,gBAAgB,gBAAgB,QAAQ,QAAQ,cAAc,OAAO,CAAC,GAAG,GAAG,QAAQ,KAAK;AACxG,SAAO,QAAQ,OAAO,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,IAAI,KAAK;AACtE;AAGO,SAAS,YAAY,OAA0B,UAAkB,UAA8B,CAAC,GAAW;AAChH,QAAM,IAAI,cAAc,eAAe,KAAK;AAC5C,MAAI,MAAM,OAAW,QAAO,QAAQ,SAAS;AAC7C,QAAM,YAAY,gBAAgB,QAAQ,QAAQ;AAAA,IAChD,OAAO;AAAA,IACP;AAAA,IACA,GAAI,QAAQ,kBAAkB,EAAE,iBAAiB,QAAQ,gBAAgB,IAAI;AAAA,IAC7E,GAAG,cAAc,OAAO;AAAA,EAC1B,CAAC;AACD,SAAO,gBAAgB,WAAW,GAAG,QAAQ,KAAK;AACpD;AAIO,SAAS,cAAc,OAA0B,UAAgC,CAAC,GAAW;AAClG,QAAM,IAAI,cAAc,iBAAiB,KAAK;AAC9C,MAAI,MAAM,OAAW,QAAO,QAAQ,SAAS;AAC7C,QAAM,QAAQ,QAAQ,UAAU,QAAQ,IAAI,MAAM;AAClD,QAAM,YAAY,gBAAgB,QAAQ,QAAQ;AAAA,IAChD,OAAO;AAAA,IACP,GAAI,QAAQ,WAAW,SAAY,EAAE,uBAAuB,EAAE,IAAI;AAAA,IAClE,GAAG,cAAc,OAAO;AAAA,EAC1B,CAAC;AACD,SAAO,gBAAgB,WAAW,OAAO,QAAQ,KAAK;AACxD;AAkBA,MAAM,cAAmE;AAAA,EACvE,OAAO,EAAE,WAAW,QAAQ;AAAA,EAC5B,QAAQ,EAAE,WAAW,SAAS;AAAA,EAC9B,MAAM,EAAE,WAAW,OAAO;AAAA,EAC1B,MAAM,EAAE,WAAW,OAAO;AAAA,EAC1B,UAAU,EAAE,WAAW,UAAU,WAAW,QAAQ;AAAA,EACpD,MAAM,EAAE,WAAW,QAAQ;AAAA,EAC3B,WAAW,EAAE,OAAO,QAAQ,MAAM,UAAU;AAC9C;AASA,MAAM,eAAe;AAWd,SAAS,OAAO,OAA+B;AACpD,MAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,GAAI,QAAO;AAClE,MAAI,OAAO,UAAU,YAAY,aAAa,KAAK,KAAK,EAAG,QAAO,aAAa,KAAK;AACpF,QAAM,IAAI,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;AACxD,SAAO,OAAO,MAAM,EAAE,QAAQ,CAAC,IAAI,OAAO;AAC5C;AAEA,MAAM,iBAAiB,oBAAI,IAAiC;AAC5D,SAAS,cAAc,QAA4B,SAA0D;AAC3G,QAAM,MAAM,GAAG,UAAU,EAAE,IAAI,KAAK,UAAU,OAAO,CAAC;AACtD,MAAI,IAAI,eAAe,IAAI,GAAG;AAC9B,MAAI,CAAC,GAAG;AACN,QAAI,IAAI,KAAK,eAAe,QAAQ,OAAO;AAC3C,mBAAe,IAAI,KAAK,CAAC;AAAA,EAC3B;AACA,SAAO;AACT;AAUO,SAAS,WACd,OACA,QAAsD,UACtD,UAA6B,CAAC,GACtB;AACR,QAAM,IAAI,OAAO,KAAK;AACtB,MAAI,CAAC,EAAG,QAAO,QAAQ,SAAS;AAChC,QAAM,OAAO,OAAO,UAAU,WAAW,YAAY,KAAK,IAAI;AAI9D,MAAI,OAAO,UAAU,YAAY,aAAa,KAAK,KAAK,GAAG;AACzD,UAAM,MAAM,IAAI,KAAK,KAAK,IAAI,EAAE,YAAY,GAAG,EAAE,SAAS,GAAG,EAAE,QAAQ,CAAC,CAAC;AACzE,WAAO,cAAc,QAAQ,QAAQ,EAAE,GAAG,MAAM,UAAU,MAAM,CAAC,EAAE,OAAO,GAAG;AAAA,EAC/E;AACA,SAAO,cAAc,QAAQ,QAAQ,QAAQ,WAAW,EAAE,GAAG,MAAM,UAAU,QAAQ,SAAS,IAAI,IAAI,EAAE,OAAO,CAAC;AAClH;AAsBA,MAAM,SAAS;AACf,MAAM,SAAS,KAAK;AACpB,MAAM,OAAO,KAAK;AAClB,MAAM,MAAM,KAAK;AAGjB,MAAM,iBAAuE;AAAA,EAC3E,CAAC,KAAK,QAAQ,UAAU,MAAM;AAAA,EAC9B,CAAC,KAAK,QAAQ,UAAU,MAAM;AAAA,EAC9B,CAAC,KAAK,MAAM,QAAQ,IAAI;AAAA,EACxB,CAAC,MAAM,KAAK,OAAO,GAAG;AAAA,EACtB,CAAC,KAAK,KAAK,QAAQ,IAAI,GAAG;AAAA,EAC1B,CAAC,MAAM,KAAK,SAAS,QAAQ,GAAG;AAAA,EAChC,CAAC,UAAU,QAAQ,SAAS,GAAG;AACjC;AAEA,MAAM,qBAAqB,oBAAI,IAAqC;AACpE,SAAS,kBACP,QACA,SACA,OACyB;AACzB,QAAM,MAAM,GAAG,UAAU,EAAE,IAAI,OAAO,IAAI,KAAK;AAC/C,MAAI,IAAI,mBAAmB,IAAI,GAAG;AAClC,MAAI,CAAC,GAAG;AACN,QAAI,IAAI,KAAK,mBAAmB,QAAQ,EAAE,SAAS,MAAM,CAAC;AAC1D,uBAAmB,IAAI,KAAK,CAAC;AAAA,EAC/B;AACA,SAAO;AACT;AAQO,SAAS,mBAAmB,OAAkB,UAAqC,CAAC,GAAW;AACpG,QAAM,IAAI,OAAO,KAAK;AACtB,MAAI,CAAC,EAAG,QAAO,QAAQ,SAAS;AAChC,QAAM,MAAM,OAAO,QAAQ,OAAO,oBAAI,KAAK,CAAC,KAAK,oBAAI,KAAK;AAC1D,QAAM,OAAO,EAAE,QAAQ,IAAI,IAAI,QAAQ;AACvC,QAAM,MAAM,KAAK,IAAI,IAAI;AACzB,MAAI,QAAQ,sBAAsB,UAAa,MAAM,QAAQ,oBAAoB,KAAK;AACpF,WAAO,WAAW,GAAG,QAAQ,iBAAiB,UAAU,OAAO;AAAA,EACjE;AACA,QAAM,MAAM,kBAAkB,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,QAAQ,SAAS,MAAM;AAChG,QAAM,CAAC,EAAE,MAAM,IAAI,IAAI,eAAe,KAAK,CAAC,CAAC,KAAK,MAAM,MAAM,KAAK;AAGnE,QAAM,QAAQ,SAAS,WAAW,IAAI,KAAK,MAAM,OAAO,IAAI;AAE5D,SAAO,IAAI,OAAO,QAAQ,GAAG,IAAI;AACnC;AA6BO,SAAS,aAAa,QAA4B;AACvD,QAAM,YAAY,aAAa,MAAM;AAGrC,QAAM,WAAW,eAAe;AAChC,SAAO;AAAA,IACL,OAAO;AAAA,MACL,QAAQ;AAAA,MACR,cAAc,CAAC,OAAO,YAAY,aAAa,OAAO,EAAE,QAAQ,WAAW,GAAG,QAAQ,CAAC;AAAA,MACvF,aAAa,CAAC,OAAO,UAAU,YAAY,YAAY,OAAO,UAAU,EAAE,QAAQ,WAAW,GAAG,QAAQ,CAAC;AAAA,MACzG,eAAe,CAAC,OAAO,YAAY,cAAc,OAAO,EAAE,QAAQ,WAAW,GAAG,QAAQ,CAAC;AAAA,MACzF,YAAY,CAAC,OAAO,OAAO,YAAY,WAAW,OAAO,OAAO,EAAE,QAAQ,WAAW,UAAU,GAAG,QAAQ,CAAC;AAAA,MAC3G,oBAAoB,CAAC,OAAO,YAAY,mBAAmB,OAAO,EAAE,QAAQ,WAAW,UAAU,GAAG,QAAQ,CAAC;AAAA,IAC/G;AAAA,IACA,CAAC,WAAW,QAAQ;AAAA,EACtB;AACF;AASO,SAAS,oBAAoB,UAAyD;AAC3F,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,MAAM,EAAE,OAAO,YAAY,SAAS,CAAC,EAAE,gBAAgB,EAAE;AAAA,EACxF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AA2BO,SAAS,gBACd,OACA,UACA,QACQ;AACR,QAAM,UAAU,OAAO,WAAW,YAAY,WAAW,OAAO,SAAS,EAAE,OAAO;AAClF,QAAM,SAAS,QAAQ,UAAU,oBAAoB,QAAQ,KAAK,QAAQ;AAC1E,MAAI,WAAW,OAAW,QAAO;AACjC,SAAO,cAAc,OAAO,MAAM;AACpC;","names":["formatWithMinus"]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/format.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLocale, useKitTimeZone } from \"../i18n/kit-labels\";\nimport { parseIsoDate } from \"./dates\";\nimport { dateFormatter } from \"./intl-cache\";\nimport { isBareIsoDay } from \"./iso-day\";\nimport { formatWithMinus, type MinusStyle } from \"./minus\";\nimport { roundHalfAway } from \"./round\";\n\nexport type { MinusStyle } from \"./minus\";\n// 0.50 (KIT-1 follow-up): the kit's minus for an app's OWN `Intl.NumberFormat` wrapper.\nexport { formatWithMinus } from \"./minus\";\n\n/**\n * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,\n * for the figures an app prints OUTSIDE a kit component.\n *\n * Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned\n * `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`\n * whose \"5m ago\" / \"3h ago\" / \"2d ago\" stayed English in every language), its tenancy\n * `format.ts` (`formatChf`, a `formatPct` gluing a \"+\" onto `toFixed(2)`), keksdose's\n * own set. Every one of them is `Intl` with a locale bolted on, so the only thing\n * worth sharing is the locale — and the kit already has one, the provider's.\n *\n * The plain functions take a `locale` (none: the runtime default, as every `Intl` API\n * reads `undefined`). Inside a component, {@link useKitFormat} hands back the same\n * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the\n * kit components on it.\n *\n * A missing value — `null`, `undefined`, an unparseable date — formats as `empty`\n * (default \"—\", the dash `StatTile` shows for no value), never as \"NaN\" or \"Invalid\n * Date\". A number may also arrive as a numeric STRING (0.49, KIT-2) — \"1850.00\", \"-12.5\",\n * \"0\": a Decimal field off a Python API — and is formatted like the number. Anything\n * else (a non-numeric string, an object, `NaN`, ±`Infinity`) prints `empty` too, but is\n * a bug upstream rather than a gap in the data, so a development build says so on the\n * console, once per call site. Zero is a figure: `0` and `\"0\"` print \"CHF 0.00\", never\n * the dash.\n *\n * A negative figure carries the typographic minus, U+2212 (0.49, KIT-1), in every\n * locale — `Intl` writes a hyphen in most. `minus: \"ascii\"` for text a machine reads\n * back (a CSV, the clipboard).\n */\n\n/** What every formatter here prints for a missing value, unless told otherwise. */\nexport const EMPTY_FORMATTED_VALUE = \"—\";\n\n/** Fraction digits: a number pins both the minimum and the maximum (`2` → \"3.10\");\n * an object sets them apart (a meter reading, 1–3). */\nexport type FormatDigits = number | { min?: number; max?: number };\n\ninterface BaseOptions {\n /** BCP 47 tag. Left out: the runtime default (the hook: the provider's). */\n locale?: string;\n /** Printed for a missing value. Default {@link EMPTY_FORMATTED_VALUE}. */\n empty?: string;\n}\n\n/** The options every number formatter shares (money and percent carry their own unit). */\nexport interface FormatNumberBaseOptions extends BaseOptions {\n digits?: FormatDigits;\n /** Compact notation: 12 400 → \"12K\". */\n compact?: boolean;\n /** `\"exceptZero\"` for a change (\"+3\", \"−3\", \"0\"). Default: the minus only. */\n signDisplay?: Intl.NumberFormatOptions[\"signDisplay\"];\n /** Any further `Intl.NumberFormat` options, merged over the ones above. */\n options?: Intl.NumberFormatOptions;\n /**\n * The minus sign of a negative figure (0.49, KIT-1). `\"typographic\"` (default):\n * U+2212, as wide as \"+\" and the digits, as `SignedAmount` prints it — in every\n * locale, also where `Intl` writes a hyphen. `\"ascii\"`: U+002D, for an export a\n * machine parses back (a CSV download, the clipboard) — there, also pass\n * `options: { useGrouping: false }` or write the raw number.\n */\n minus?: MinusStyle;\n}\n\n/** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */\nexport interface FormatNumberOptions extends FormatNumberBaseOptions {\n /**\n * A unit printed after the figure, joined by a narrow no-break space (U+202F):\n * \"3.400 N\", \"12 kg\", \"4,5 m²\". lenkbank (P8) prints forces, masses and lengths in\n * every table and summary, and each call site glued `${n} N` together with a plain\n * space — which wraps the \"N\" onto the next line in a narrow column and reads loose\n * beside the digit grouping. The narrow no-break space is the SI brochure's and the\n * Swiss typesetting rule's separator between value and unit, and it never breaks.\n *\n * Free text, not `Intl`'s `style: \"unit\"`: that one only knows the ECMA-402\n * sanctioned units (no \"N\", no \"kN\", no \"m³/h\"), so an app would still be gluing.\n * A missing value prints `empty` ALONE — \"— N\" says there is a newton-shaped\n * nothing, which is noise in a column that already heads its unit.\n */\n unit?: string;\n}\n\n/** Narrow no-break space (U+202F): between a figure and its {@link FormatNumberOptions.unit}. */\nconst UNIT_SEPARATOR = \"\\u202F\";\n\nexport interface FormatMoneyOptions extends FormatNumberBaseOptions {\n /** `\"symbol\"` (default), `\"narrowSymbol\"`, `\"code\"` (\"CHF 12.00\") or `\"name\"`. */\n currencyDisplay?: Intl.NumberFormatOptions[\"currencyDisplay\"];\n}\n\nexport interface FormatPercentOptions extends FormatNumberBaseOptions {\n /**\n * `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read\n * it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %\n * (kastlan's rent `change_pct`).\n */\n ratio?: boolean;\n}\n\n// One formatter per option set: a table formats the same way thousands of times, and\n// constructing an Intl formatter is the expensive part.\nconst numberFormatters = new Map<string, Intl.NumberFormat>();\nfunction numberFormatter(locale: string | undefined, options: Intl.NumberFormatOptions): Intl.NumberFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = numberFormatters.get(key);\n if (!f) {\n f = new Intl.NumberFormat(locale, options);\n numberFormatters.set(key, f);\n }\n return f;\n}\n\nfunction digitOptions(digits: FormatDigits | undefined): Intl.NumberFormatOptions {\n if (digits === undefined) return {};\n if (typeof digits === \"number\") return { minimumFractionDigits: digits, maximumFractionDigits: digits };\n return {\n ...(digits.min !== undefined ? { minimumFractionDigits: digits.min } : null),\n ...(digits.max !== undefined ? { maximumFractionDigits: digits.max } : null),\n };\n}\n\nfunction numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {\n return {\n ...digitOptions(o.digits),\n ...(o.compact ? { notation: \"compact\" } : null),\n ...(o.signDisplay ? { signDisplay: o.signDisplay } : null),\n ...o.options,\n };\n}\n\n/**\n * What the number formatters take (0.49, KIT-2): a number, a numeric string (\"1850.00\",\n * \"-12.5\", \"1e3\" — what a Decimal arrives as in JSON), or `null` / `undefined` for no\n * value. A string is read with `Number()`, so a figure with more significant digits than\n * a double holds (about 15–17) is printed as its nearest double.\n */\nexport type FormattableNumber = number | string | null | undefined;\n\n/** A plain decimal: optional sign, digits with an optional point, an optional exponent.\n * No grouping marks, no decimal comma, no currency — those are formatted text, and\n * reading them back is a locale's guess. */\nconst NUMERIC_STRING = /^[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?$/;\n\n/** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a\n * production one (the consumer's bundler makes `import.meta.env.DEV` false) and never\n * under vitest. Read per call so a test can switch it on with `vi.stubEnv`. */\nfunction devWarnings(): boolean {\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n}\n\nconst warnedSites = new Set<string>();\n/** This module's own frames, skipped to find the caller (\"Object.formatMoney\" is a\n * `useKitFormat` wrapper). */\nconst OWN_FRAME = /\\b(?:formatNumber|formatMoney|formatPercent|toFormattable|warnInvalid|callSite)\\b/;\n\n/** The first stack frame outside the formatters: where the bad value was passed. */\nfunction callSite(): string {\n const frames = (new Error().stack ?? \"\").split(\"\\n\").slice(1);\n return frames.find((frame) => !OWN_FRAME.test(frame))?.trim() ?? \"\";\n}\n\nfunction describeValue(value: unknown): string {\n if (typeof value === \"number\" || typeof value === \"bigint\" || typeof value === \"boolean\") return String(value);\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return Object.prototype.toString.call(value);\n }\n}\n\n/** A development build's console note about an invalid value — once per call site. */\nfunction warnInvalid(fn: string, value: unknown): void {\n if (!devWarnings()) return;\n const site = callSite();\n const key = `${fn}|${site || typeof value}`;\n if (warnedSites.has(key)) return;\n warnedSites.add(key);\n const type = Array.isArray(value) ? \"array\" : typeof value;\n console.warn(\n `[ui-kit] ${fn} got ${describeValue(value)} (${type}), which is not a number: printed the empty value. ` +\n \"Pass a number, a numeric string, or null/undefined for a missing value.\" +\n (site ? `\\n at ${site.replace(/^at /, \"\")}` : \"\"),\n );\n}\n\n/**\n * `value` as a finite number to format, or `undefined` to print `empty`. `null` and\n * `undefined` are a missing value and pass quietly; anything that is neither a finite\n * number nor a numeric string is an invalid one — ±Infinity included (lenkbank, adopting\n * 0.13: a division by zero upstream; \"∞\" in a money column reads like data) — and a\n * development build warns. `-0` is folded into `0`: zero carries no sign.\n */\nfunction toFormattable(fn: string, value: unknown): number | undefined {\n if (value === null || value === undefined) return undefined;\n let n: number | undefined;\n if (typeof value === \"number\") n = value;\n else if (typeof value === \"string\" && NUMERIC_STRING.test(value.trim())) n = Number(value.trim());\n if (n === undefined || !Number.isFinite(n)) {\n warnInvalid(fn, value);\n return undefined;\n }\n return n + 0;\n}\n\n/** A number in `locale`: \"1,234.5\", \"1.234,5\", \"1’234.5\" — with `unit`, \"3.400 N\". */\nexport function formatNumber(value: FormattableNumber, options: FormatNumberOptions = {}): string {\n const n = toFormattable(\"formatNumber\", value);\n if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const figure = formatWithMinus(numberFormatter(options.locale, numberOptions(options)), n, options.minus);\n return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;\n}\n\n/** An amount of `currency` (ISO 4217) in `locale`: \"CHF 1’234.50\", \"1.234,50 €\". */\nexport function formatMoney(value: FormattableNumber, currency: string, options: FormatMoneyOptions = {}): string {\n const n = toFormattable(\"formatMoney\", value);\n if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const formatter = numberFormatter(options.locale, {\n style: \"currency\",\n currency,\n ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),\n ...numberOptions(options),\n });\n return formatWithMinus(formatter, n, options.minus);\n}\n\n/** A percentage in `locale` (\"12%\", \"12 %\", \"+1.25%\"). See {@link FormatPercentOptions.ratio}\n * for what `value` means. Default digits: at most one. */\nexport function formatPercent(value: FormattableNumber, options: FormatPercentOptions = {}): string {\n const n = toFormattable(\"formatPercent\", value);\n if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const ratio = options.ratio === false ? n / 100 : n;\n const formatter = numberFormatter(options.locale, {\n style: \"percent\",\n ...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),\n ...numberOptions(options),\n });\n return formatWithMinus(formatter, ratio, options.minus);\n}\n\n/* ── Dates ─────────────────────────────────────────────────────────────────── */\n\n/**\n * The shapes a date is printed in, each a set of `Intl.DateTimeFormat` options:\n * - `short` / `medium` / `long` / `full` — the date alone, at that `dateStyle`\n * (\"08/07/2026\", \"8 Jul 2026\", \"8 July 2026\", \"Wednesday, 8 July 2026\" in en-GB).\n * - `dateTime` — medium date and short time: a timestamp (\"8 Jul 2026, 14:05\").\n * - `time` — the short time alone (\"14:05\").\n * - `monthYear` — \"July 2026\".\n *\n * Or pass `Intl.DateTimeFormatOptions` for anything else. A date-fns PATTERN\n * (\"dd.MM.yyyy\") is deliberately not accepted: a pattern is one locale's order baked\n * into a string, which is how an English page ends up with German dates.\n */\nexport type FormatDateStyle = \"short\" | \"medium\" | \"long\" | \"full\" | \"dateTime\" | \"time\" | \"monthYear\";\n\nconst DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {\n short: { dateStyle: \"short\" },\n medium: { dateStyle: \"medium\" },\n long: { dateStyle: \"long\" },\n full: { dateStyle: \"full\" },\n dateTime: { dateStyle: \"medium\", timeStyle: \"short\" },\n time: { timeStyle: \"short\" },\n monthYear: { month: \"long\", year: \"numeric\" },\n};\n\nexport interface FormatDateOptions extends BaseOptions {\n /** An IANA zone (\"Europe/Zurich\") for a timestamp. Left out: the device's. A bare\n * \"YYYY-MM-DD\" ignores it: a calendar day is the day it names in every zone. */\n timeZone?: string;\n}\n\n/** What {@link formatDate} and {@link formatRelativeTime} accept. */\nexport type DateInput = string | number | Date | null | undefined;\n\n/**\n * `value` as a Date, or null. A bare \"YYYY-MM-DD\" is a CALENDAR day and is read on the\n * local calendar (`parseIsoDate`) — `new Date(\"2026-07-08\")` is UTC midnight, which is\n * the 7th anywhere west of Greenwich. Anything else (a full ISO timestamp, epoch\n * milliseconds, a Date) is an instant and goes through `Date` as is.\n */\nexport function toDate(value: DateInput): Date | null {\n if (value === null || value === undefined || value === \"\") return null;\n if (isBareIsoDay(value)) return parseIsoDate(value);\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\n/**\n * A date or timestamp in `locale`. Takes an ISO date (\"2026-07-08\", read on the local\n * calendar), an ISO timestamp (\"2026-07-08T14:05:00Z\"), epoch milliseconds or a Date.\n * `style` defaults to `medium`; see {@link FormatDateStyle}.\n *\n * `formatIsoDate` (`@eifi1/ui-kit/dates`) remains for a plain \"YYYY-MM-DD\" with an\n * explicit locale; this is its superset for timestamps and the named styles.\n */\nexport function formatDate(\n value: DateInput,\n style: FormatDateStyle | Intl.DateTimeFormatOptions = \"medium\",\n options: FormatDateOptions = {},\n): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const base = typeof style === \"string\" ? DATE_STYLES[style] : style;\n // A bare \"YYYY-MM-DD\" is a calendar day, which has no zone: it is the day it names\n // wherever it is read, so a `timeZone` (in `options` or the style) never moves it — the\n // device's local midnight shown in a zone west of it was the day before (round 0.45, N9).\n if (isBareIsoDay(value)) {\n const day = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));\n return dateFormatter(options.locale, { ...base, timeZone: \"UTC\" }).format(day);\n }\n return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);\n}\n\nexport interface FormatRelativeTimeOptions extends BaseOptions {\n /** The zone of the date printed past `absoluteAfterDays` (0.46), and (0.51.7) the zone\n * whose calendar days \"yesterday\" and \"2 days ago\" count. Left out: the device's. */\n timeZone?: string;\n /** The moment it is measured from. Default: now. */\n now?: DateInput;\n /** `\"auto\"` (default): \"yesterday\", \"now\", \"next week\". `\"always\"`: \"1 day ago\". */\n numeric?: Intl.RelativeTimeFormatNumeric;\n /** `\"long\"` (default) \"3 hours ago\", `\"short\"` \"3 hr. ago\", `\"narrow\"` \"3h ago\". */\n style?: Intl.RelativeTimeFormatStyle;\n /**\n * Past this many days either way, print the date instead ({@link formatDate} in\n * `absoluteStyle`) — kastlan's feed switches to the date after a week, because \"5\n * weeks ago\" makes a reader do sums. Default: never.\n */\n absoluteAfterDays?: number;\n /** The style of that date. Default `medium`. */\n absoluteStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n}\n\nconst SECOND = 1000;\nconst MINUTE = 60 * SECOND;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\n/** [the largest |difference| a unit is used for, the unit, its length]. */\nconst RELATIVE_STEPS: Array<[number, Intl.RelativeTimeFormatUnit, number]> = [\n [45 * SECOND, \"second\", SECOND],\n [45 * MINUTE, \"minute\", MINUTE],\n [22 * HOUR, \"hour\", HOUR],\n [6.5 * DAY, \"day\", DAY],\n [26 * DAY, \"week\", 7 * DAY],\n [320 * DAY, \"month\", 30.44 * DAY],\n [Infinity, \"year\", 365.25 * DAY],\n];\n\n/**\n * `d`'s calendar day in `timeZone` (left out: the device's), as a whole number of days:\n * that day's UTC midnight over a day's length. Two of them subtract to whole calendar\n * days — a 23- or 25-hour DST day still counts one.\n */\nfunction dayNumber(d: Date, timeZone: string | undefined): number {\n if (!timeZone) return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY;\n // `en-US`: Gregorian, Latin digits — the parts are read back as numbers.\n const fields: Intl.DateTimeFormatOptions = { timeZone, year: \"numeric\", month: \"numeric\", day: \"numeric\" };\n const parts = dateFormatter(\"en-US\", fields).formatToParts(d);\n const part = (type: Intl.DateTimeFormatPartTypes) => Number(parts.find((p) => p.type === type)?.value);\n return Date.UTC(part(\"year\"), part(\"month\") - 1, part(\"day\")) / DAY;\n}\n\nconst relativeFormatters = new Map<string, Intl.RelativeTimeFormat>();\nfunction relativeFormatter(\n locale: string | undefined,\n numeric: Intl.RelativeTimeFormatNumeric,\n style: Intl.RelativeTimeFormatStyle,\n): Intl.RelativeTimeFormat {\n const key = `${locale ?? \"\"}|${numeric}|${style}`;\n let f = relativeFormatters.get(key);\n if (!f) {\n f = new Intl.RelativeTimeFormat(locale, { numeric, style });\n relativeFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * How long ago (or until) `value` is, in `locale`'s words, through\n * `Intl.RelativeTimeFormat`: \"3 hours ago\", \"vor 3 Stunden\", \"il y a 3 heures\",\n * \"in 2 days\", and under 45 seconds \"now\". The unit is picked by size — seconds,\n * minutes, hours, days, weeks, months, years — and the count is rounded; days are\n * CALENDAR days (0.51.7), in `timeZone` when given: \"yesterday\" is the day before today,\n * however few hours ago that was.\n */\nexport function formatRelativeTime(value: DateInput, options: FormatRelativeTimeOptions = {}): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const now = toDate(options.now ?? new Date()) ?? new Date();\n const diff = d.getTime() - now.getTime();\n const abs = Math.abs(diff);\n if (options.absoluteAfterDays !== undefined && abs > options.absoluteAfterDays * DAY) {\n return formatDate(d, options.absoluteStyle ?? \"medium\", options);\n }\n const rtf = relativeFormatter(options.locale, options.numeric ?? \"auto\", options.style ?? \"long\");\n const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit)!;\n if (unit === \"day\") {\n // By the calendar, not in 24-hour units (refactor audit 2026-10-30, DSP-7): rounded\n // elapsed time made 00:10 seen at 23:50 \"yesterday\" and 01:00 yesterday seen at 23:30\n // \"2 days ago\", beside an exact date (DateMark's second line, the feedback thread's\n // tooltip) that said otherwise. Counted in `timeZone`, the zone that date is printed\n // in; a bare \"YYYY-MM-DD\" is the day it names wherever it is read, as in formatDate.\n // Still the same day (22 hours and more since just after midnight): in hours.\n const bareDay = isBareIsoDay(value);\n const days = dayNumber(d, bareDay ? undefined : options.timeZone) - dayNumber(now, options.timeZone);\n return days === 0 ? rtf.format(Math.round(diff / HOUR), \"hour\") : rtf.format(days, \"day\");\n }\n // Under 45 seconds is \"now\" rather than \"12 seconds ago\": a feed that ticks through\n // the seconds is noise, and nothing reads a timestamp to the second.\n const count = unit === \"second\" ? 0 : Math.round(diff / size);\n // `+ 0` folds -0 into 0: `format(-0, …)` says \"0 seconds ago\" instead of \"now\".\n return rtf.format(count + 0, unit);\n}\n\n/* ── Bound to the provider ─────────────────────────────────────────────────── */\n\ntype WithoutLocale<T> = Omit<T, \"locale\"> & { locale?: string };\n\n/** What {@link useKitFormat} returns: the formatters above with the provider's locale\n * as their default (an explicit `locale` in the options still wins). */\nexport interface KitFormat {\n /** The locale they format in — the provider's, or `undefined` (runtime default). */\n locale: string | undefined;\n formatNumber: (value: FormattableNumber, options?: WithoutLocale<FormatNumberOptions>) => string;\n formatMoney: (value: FormattableNumber, currency: string, options?: WithoutLocale<FormatMoneyOptions>) => string;\n formatPercent: (value: FormattableNumber, options?: WithoutLocale<FormatPercentOptions>) => string;\n formatDate: (\n value: DateInput,\n style?: FormatDateStyle | Intl.DateTimeFormatOptions,\n options?: WithoutLocale<FormatDateOptions>,\n ) => string;\n formatRelativeTime: (value: DateInput, options?: WithoutLocale<FormatRelativeTimeOptions>) => string;\n}\n\n/**\n * The formatters, bound to the `<UiKitProvider locale>` (or to `locale`, which wins\n * over it — the same precedence as a component's `locale` prop):\n *\n * const { formatMoney, formatRelativeTime } = useKitFormat();\n * formatMoney(row.balance, \"CHF\");\n */\nexport function useKitFormat(locale?: string): KitFormat {\n const kitLocale = useKitLocale(locale);\n // 0.46 (§10.3): the provider's `timeZone` for timestamps; a call's own wins, and a bare\n // \"YYYY-MM-DD\" ignores it (formatDate).\n const timeZone = useKitTimeZone();\n return useMemo<KitFormat>(\n () => ({\n locale: kitLocale,\n formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),\n formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),\n formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),\n formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, timeZone, ...options }),\n formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, timeZone, ...options }),\n }),\n [kitLocale, timeZone],\n );\n}\n\n/* ── Currency minor units ──────────────────────────────────────────────────── */\n\n/**\n * The decimals a currency is written with — its minor unit: CHF and EUR 2, JPY 0,\n * KWD 3 — from `Intl`, or `undefined` for a code `Intl` does not accept. The rule\n * AmountInput settles a typed figure by, for a host that needs it outside the field.\n */\nexport function currencyMinorDigits(currency: string | null | undefined): number | undefined {\n if (!currency) return undefined;\n try {\n return new Intl.NumberFormat(\"en\", { style: \"currency\", currency }).resolvedOptions().maximumFractionDigits;\n } catch {\n return undefined;\n }\n}\n\n/** The options form of {@link roundToCurrency}'s third argument. */\nexport interface RoundToCurrencyOptions {\n /** Decimals to round to, whatever the currency — the plain `digits` argument. */\n digits?: number;\n /**\n * Decimals for a currency `Intl` cannot resolve — no code yet (`null`, `\"\"`), or a\n * malformed one (\"EURO\", \"€\"). Left out, such a value comes back unchanged, as\n * before. Note that `Intl` resolves ANY well-formed three-letter code, a made-up\n * \"XYZ\" included (to 2), so this is the case of a missing or broken code, not of a\n * rare currency.\n */\n fallbackDigits?: number;\n}\n\n/**\n * `value` rounded to the currency's minor unit (or to `digits`), half away from zero,\n * in decimal arithmetic: 93.4213 CHF → 93.42, 1.005 EUR → 1.01 (`Math.round(1.005 *\n * 100)` is 100, since 1.005 is 1.00499… in binary). A non-finite value, or a currency\n * `Intl` does not know without `digits`, comes back unchanged (keksdose live #356: an\n * FX estimate set into an AmountInput showed four decimals) — unless the options form\n * gives `fallbackDigits`: `roundToCurrency(v, code, { fallbackDigits: 2 })` rounds a\n * figure whose currency is not chosen yet to cents rather than leaving 0.1 + 0.2 as is.\n * A finite value never comes back NaN or -0, however small or large (BUG-11):\n * 0.1 + 0.2 - 0.3 → 0, -0.004 → 0.\n */\nexport function roundToCurrency(\n value: number,\n currency: string | null | undefined,\n digits?: number | RoundToCurrencyOptions,\n): number {\n const options = typeof digits === \"object\" && digits !== null ? digits : { digits };\n const places = options.digits ?? currencyMinorDigits(currency) ?? options.fallbackDigits;\n if (places === undefined) return value;\n return roundHalfAway(value, places);\n}\n"],"mappings":"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","names":["formatWithMinus"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP status of a thrown error, as one probe (DUP-11, refactor audit 2026-10: the
|
|
3
|
+
* session refresh carried its own copy).
|
|
4
|
+
*
|
|
5
|
+
* It lived in `auth/auth-errors.ts` until 0.52, which made `lib/session-refresh` and
|
|
6
|
+
* `/sync`'s engine import the auth folder — and put auth's English error words in the
|
|
7
|
+
* `/sync` entry's closure — for a three-line reader of `status` (refactor audit
|
|
8
|
+
* 2026-10-30, MOD-6). `auth-errors.ts` re-exports it, so nothing that imported it there
|
|
9
|
+
* moved.
|
|
10
|
+
*
|
|
11
|
+
* @internal Package-private — not re-exported from the barrel.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The HTTP status a thrown error carries, wherever the apps' HTTP clients put it —
|
|
15
|
+
* without depending on any of them:
|
|
16
|
+
*
|
|
17
|
+
* 1. `err.response.status` — axios (all three apps' clients);
|
|
18
|
+
* 2. `err.status` — ofetch's `FetchError`, the generated OpenAPI clients' `ApiError`, and
|
|
19
|
+
* a `Response` thrown as it came;
|
|
20
|
+
* 3. `err.statusCode` — ofetch's alias, and Node-style errors.
|
|
21
|
+
*
|
|
22
|
+
* `undefined` when none is a whole number.
|
|
23
|
+
*/
|
|
24
|
+
export declare function httpStatusOf(err: unknown): number | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const isBag = (value) => typeof value === "object" && value !== null;
|
|
3
|
+
function statusOf(value) {
|
|
4
|
+
return typeof value === "number" && Number.isInteger(value) ? value : void 0;
|
|
5
|
+
}
|
|
6
|
+
function httpStatusOf(err) {
|
|
7
|
+
if (!isBag(err)) return void 0;
|
|
8
|
+
const response = err.response;
|
|
9
|
+
return (isBag(response) ? statusOf(response.status) : void 0) ?? statusOf(err.status) ?? statusOf(err.statusCode);
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
httpStatusOf
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=http-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/http-status.ts"],"sourcesContent":["/**\n * The HTTP status of a thrown error, as one probe (DUP-11, refactor audit 2026-10: the\n * session refresh carried its own copy).\n *\n * It lived in `auth/auth-errors.ts` until 0.52, which made `lib/session-refresh` and\n * `/sync`'s engine import the auth folder — and put auth's English error words in the\n * `/sync` entry's closure — for a three-line reader of `status` (refactor audit\n * 2026-10-30, MOD-6). `auth-errors.ts` re-exports it, so nothing that imported it there\n * moved.\n *\n * @internal Package-private — not re-exported from the barrel.\n */\n\ntype Bag = Record<string, unknown>;\n\nconst isBag = (value: unknown): value is Bag => typeof value === \"object\" && value !== null;\n\n/** An HTTP status as a number, or undefined. */\nfunction statusOf(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) ? value : undefined;\n}\n\n/**\n * The HTTP status a thrown error carries, wherever the apps' HTTP clients put it —\n * without depending on any of them:\n *\n * 1. `err.response.status` — axios (all three apps' clients);\n * 2. `err.status` — ofetch's `FetchError`, the generated OpenAPI clients' `ApiError`, and\n * a `Response` thrown as it came;\n * 3. `err.statusCode` — ofetch's alias, and Node-style errors.\n *\n * `undefined` when none is a whole number.\n */\nexport function httpStatusOf(err: unknown): number | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n return (isBag(response) ? statusOf(response.status) : undefined) ?? statusOf(err.status) ?? statusOf(err.statusCode);\n}\n"],"mappings":";AAeA,MAAM,QAAQ,CAAC,UAAiC,OAAO,UAAU,YAAY,UAAU;AAGvF,SAAS,SAAS,OAAoC;AACpD,SAAO,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,IAAI,QAAQ;AACxE;AAaO,SAAS,aAAa,KAAkC;AAC7D,MAAI,CAAC,MAAM,GAAG,EAAG,QAAO;AACxB,QAAM,WAAW,IAAI;AACrB,UAAQ,MAAM,QAAQ,IAAI,SAAS,SAAS,MAAM,IAAI,WAAc,SAAS,IAAI,MAAM,KAAK,SAAS,IAAI,UAAU;AACrH;","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One `Intl.DateTimeFormat` per locale and option set, for every date the kit prints.
|
|
3
|
+
*
|
|
4
|
+
* Constructing the formatter is the expensive part: keksdose measured 392 ms per click on
|
|
5
|
+
* a 270-row register when each cell built its own (dev#632), and kept its own renderer of
|
|
6
|
+
* the date-format preference because the kit's built one per call (refactor audit
|
|
7
|
+
* 2026-10-30, DUP-2). `lib/format` and `lib/date-format` both format through this one.
|
|
8
|
+
*
|
|
9
|
+
* @internal Not part of the barrel — `lib/format` is re-exported whole, so the cache
|
|
10
|
+
* lives apart from it.
|
|
11
|
+
*/
|
|
12
|
+
/** The cached formatter for `locale` and `options`. Throws as `Intl.DateTimeFormat` does
|
|
13
|
+
* for a tag it refuses — and caches nothing then. */
|
|
14
|
+
export declare function dateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const dateFormatters = /* @__PURE__ */ new Map();
|
|
3
|
+
function dateFormatter(locale, options) {
|
|
4
|
+
const key = `${locale ?? ""}|${JSON.stringify(options)}`;
|
|
5
|
+
let f = dateFormatters.get(key);
|
|
6
|
+
if (!f) {
|
|
7
|
+
f = new Intl.DateTimeFormat(locale, options);
|
|
8
|
+
dateFormatters.set(key, f);
|
|
9
|
+
}
|
|
10
|
+
return f;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
dateFormatter
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=intl-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/intl-cache.ts"],"sourcesContent":["/**\n * One `Intl.DateTimeFormat` per locale and option set, for every date the kit prints.\n *\n * Constructing the formatter is the expensive part: keksdose measured 392 ms per click on\n * a 270-row register when each cell built its own (dev#632), and kept its own renderer of\n * the date-format preference because the kit's built one per call (refactor audit\n * 2026-10-30, DUP-2). `lib/format` and `lib/date-format` both format through this one.\n *\n * @internal Not part of the barrel — `lib/format` is re-exported whole, so the cache\n * lives apart from it.\n */\n\nconst dateFormatters = new Map<string, Intl.DateTimeFormat>();\n\n/** The cached formatter for `locale` and `options`. Throws as `Intl.DateTimeFormat` does\n * for a tag it refuses — and caches nothing then. */\nexport function dateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = dateFormatters.get(key);\n if (!f) {\n f = new Intl.DateTimeFormat(locale, options);\n dateFormatters.set(key, f);\n }\n return f;\n}\n"],"mappings":";AAYA,MAAM,iBAAiB,oBAAI,IAAiC;AAIrD,SAAS,cAAc,QAA4B,SAA0D;AAClH,QAAM,MAAM,GAAG,UAAU,EAAE,IAAI,KAAK,UAAU,OAAO,CAAC;AACtD,MAAI,IAAI,eAAe,IAAI,GAAG;AAC9B,MAAI,CAAC,GAAG;AACN,QAAI,IAAI,KAAK,eAAe,QAAQ,OAAO;AAC3C,mBAAe,IAAI,KAAK,CAAC;AAAA,EAC3B;AACA,SAAO;AACT;","names":[]}
|
package/dist/lib/isin.d.ts
CHANGED
|
@@ -9,20 +9,30 @@
|
|
|
9
9
|
* with every letter expanded to two digits (A = 10 … Z = 35) — which is all that is
|
|
10
10
|
* checked here. Whether the prefix is an assigned code, and whether the security
|
|
11
11
|
* exists, is a lookup, not a checksum.
|
|
12
|
+
*
|
|
13
|
+
* Deprecated in 0.52, removed in 0.53 (decision 4 of the refactor audit 2026-10-30,
|
|
14
|
+
* MOD-5): keksdose, which they were written for, never imported them, and no other app
|
|
15
|
+
* does.
|
|
12
16
|
*/
|
|
13
17
|
/**
|
|
14
18
|
* The ISIN as it is printed: upper case, unbroken (ISO 6166 has no grouping), with
|
|
15
19
|
* the spaces and hyphens a paste from a broker's statement carries dropped.
|
|
20
|
+
*
|
|
21
|
+
* @deprecated 0.52 — none: no app checks an ISIN with it; copy lib/isin.ts (MOD-5); removed in 0.53.
|
|
16
22
|
*/
|
|
17
23
|
export declare function formatIsin(value: string): string;
|
|
18
24
|
/**
|
|
19
25
|
* The check digit for the first eleven characters of an ISIN (prefix and national
|
|
20
26
|
* number). For BUILDING one — a test fixture, a demo — rather than checking it.
|
|
27
|
+
*
|
|
28
|
+
* @deprecated 0.52 — none: no app checks an ISIN with it; copy lib/isin.ts (MOD-5); removed in 0.53.
|
|
21
29
|
*/
|
|
22
30
|
export declare function isinCheckDigit(body: string): string;
|
|
23
31
|
/**
|
|
24
32
|
* Whether `value` is a valid ISIN: two letters, nine letters or digits, one digit,
|
|
25
33
|
* and a matching Luhn check digit over the letter-expanded form. Spaces, hyphens and
|
|
26
34
|
* lower case are fine (see {@link formatIsin}).
|
|
35
|
+
*
|
|
36
|
+
* @deprecated 0.52 — none: no app checks an ISIN with it; copy lib/isin.ts (MOD-5); removed in 0.53.
|
|
27
37
|
*/
|
|
28
38
|
export declare function isValidIsin(value: string): boolean;
|
package/dist/lib/isin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/isin.ts"],"sourcesContent":["/**\n * ISIN helpers — ISO 6166 — for keksdose, whose securities holdings are keyed by ISIN\n * and whose import and edit forms checked only the shape (two letters, ten\n * characters): a mistyped ISIN saved happily and then matched no price feed.\n *\n * An ISIN is twelve characters: a two-letter prefix (the issuing country, or a\n * supranational code such as `XS`), nine letters or digits (the national number) and\n * one check digit. The check digit is the Luhn digit of the first eleven characters\n * with every letter expanded to two digits (A = 10 … Z = 35) — which is all that is\n * checked here. Whether the prefix is an assigned code, and whether the security\n * exists, is a lookup, not a checksum.\n */\n\n/** Letters to their two-digit numbers (A = 10 … Z = 35), digits as they are. */\nfunction expandLetters(value: string): string {\n return value.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55));\n}\n\n/** The Luhn sum of `digits` with the RIGHTMOST digit doubled first — the shape of the\n * sum when a check digit is about to be appended. */\nfunction luhnSum(digits: string, doubleRightmost: boolean): number {\n let sum = 0;\n let double = doubleRightmost;\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48;\n if (double) {\n d *= 2;\n if (d > 9) d -= 9;\n }\n sum += d;\n double = !double;\n }\n return sum;\n}\n\n/**\n * The ISIN as it is printed: upper case, unbroken (ISO 6166 has no grouping), with\n * the spaces and hyphens a paste from a broker's statement carries dropped.\n */\nexport function formatIsin(value: string): string {\n return value.replace(/[\\s-]/g, \"\").toUpperCase();\n}\n\n/**\n * The check digit for the first eleven characters of an ISIN (prefix and national\n * number). For BUILDING one — a test fixture, a demo — rather than checking it.\n */\nexport function isinCheckDigit(body: string): string {\n const sum = luhnSum(expandLetters(formatIsin(body)), true);\n return String((10 - (sum % 10)) % 10);\n}\n\n/**\n * Whether `value` is a valid ISIN: two letters, nine letters or digits, one digit,\n * and a matching Luhn check digit over the letter-expanded form. Spaces, hyphens and\n * lower case are fine (see {@link formatIsin}).\n */\nexport function isValidIsin(value: string): boolean {\n const isin = formatIsin(value);\n if (!/^[A-Z]{2}[A-Z0-9]{9}\\d$/.test(isin)) return false;\n return luhnSum(expandLetters(isin), false) % 10 === 0;\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/lib/isin.ts"],"sourcesContent":["/**\n * ISIN helpers — ISO 6166 — for keksdose, whose securities holdings are keyed by ISIN\n * and whose import and edit forms checked only the shape (two letters, ten\n * characters): a mistyped ISIN saved happily and then matched no price feed.\n *\n * An ISIN is twelve characters: a two-letter prefix (the issuing country, or a\n * supranational code such as `XS`), nine letters or digits (the national number) and\n * one check digit. The check digit is the Luhn digit of the first eleven characters\n * with every letter expanded to two digits (A = 10 … Z = 35) — which is all that is\n * checked here. Whether the prefix is an assigned code, and whether the security\n * exists, is a lookup, not a checksum.\n *\n * Deprecated in 0.52, removed in 0.53 (decision 4 of the refactor audit 2026-10-30,\n * MOD-5): keksdose, which they were written for, never imported them, and no other app\n * does.\n */\n\n/** Letters to their two-digit numbers (A = 10 … Z = 35), digits as they are. */\nfunction expandLetters(value: string): string {\n return value.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55));\n}\n\n/** The Luhn sum of `digits` with the RIGHTMOST digit doubled first — the shape of the\n * sum when a check digit is about to be appended. */\nfunction luhnSum(digits: string, doubleRightmost: boolean): number {\n let sum = 0;\n let double = doubleRightmost;\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48;\n if (double) {\n d *= 2;\n if (d > 9) d -= 9;\n }\n sum += d;\n double = !double;\n }\n return sum;\n}\n\n/**\n * The ISIN as it is printed: upper case, unbroken (ISO 6166 has no grouping), with\n * the spaces and hyphens a paste from a broker's statement carries dropped.\n *\n * @deprecated 0.52 — none: no app checks an ISIN with it; copy lib/isin.ts (MOD-5); removed in 0.53.\n */\nexport function formatIsin(value: string): string {\n return value.replace(/[\\s-]/g, \"\").toUpperCase();\n}\n\n/**\n * The check digit for the first eleven characters of an ISIN (prefix and national\n * number). For BUILDING one — a test fixture, a demo — rather than checking it.\n *\n * @deprecated 0.52 — none: no app checks an ISIN with it; copy lib/isin.ts (MOD-5); removed in 0.53.\n */\nexport function isinCheckDigit(body: string): string {\n const sum = luhnSum(expandLetters(formatIsin(body)), true);\n return String((10 - (sum % 10)) % 10);\n}\n\n/**\n * Whether `value` is a valid ISIN: two letters, nine letters or digits, one digit,\n * and a matching Luhn check digit over the letter-expanded form. Spaces, hyphens and\n * lower case are fine (see {@link formatIsin}).\n *\n * @deprecated 0.52 — none: no app checks an ISIN with it; copy lib/isin.ts (MOD-5); removed in 0.53.\n */\nexport function isValidIsin(value: string): boolean {\n const isin = formatIsin(value);\n if (!/^[A-Z]{2}[A-Z0-9]{9}\\d$/.test(isin)) return false;\n return luhnSum(expandLetters(isin), false) % 10 === 0;\n}\n"],"mappings":";AAkBA,SAAS,cAAc,OAAuB;AAC5C,SAAO,MAAM,QAAQ,UAAU,CAAC,MAAM,OAAO,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC;AACpE;AAIA,SAAS,QAAQ,QAAgB,iBAAkC;AACjE,MAAI,MAAM;AACV,MAAI,SAAS;AACb,WAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,QAAI,IAAI,OAAO,WAAW,CAAC,IAAI;AAC/B,QAAI,QAAQ;AACV,WAAK;AACL,UAAI,IAAI,EAAG,MAAK;AAAA,IAClB;AACA,WAAO;AACP,aAAS,CAAC;AAAA,EACZ;AACA,SAAO;AACT;AAQO,SAAS,WAAW,OAAuB;AAChD,SAAO,MAAM,QAAQ,UAAU,EAAE,EAAE,YAAY;AACjD;AAQO,SAAS,eAAe,MAAsB;AACnD,QAAM,MAAM,QAAQ,cAAc,WAAW,IAAI,CAAC,GAAG,IAAI;AACzD,SAAO,QAAQ,KAAM,MAAM,MAAO,EAAE;AACtC;AASO,SAAS,YAAY,OAAwB;AAClD,QAAM,OAAO,WAAW,KAAK;AAC7B,MAAI,CAAC,0BAA0B,KAAK,IAAI,EAAG,QAAO;AAClD,SAAO,QAAQ,cAAc,IAAI,GAAG,KAAK,IAAI,OAAO;AACtD;","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bare ISO calendar day, `"YYYY-MM-DD"` — no time, no zone: the day it names in every
|
|
3
|
+
* zone. `toDate` and `formatDate` (lib/format), the kit's day ladder (`useDayFormatter`,
|
|
4
|
+
* lib/day-format) and the preference's renderers (lib/date-format) each kept a copy of this
|
|
5
|
+
* pattern (0.52 code review, lib/day-format.ts). Were one to learn a new shape (say
|
|
6
|
+
* `YYYY-MM-DDT00:00`) alone, the ladder would ask the provider for a different day than the
|
|
7
|
+
* kit prints. One rule.
|
|
8
|
+
*
|
|
9
|
+
* @internal Not part of the barrel: `lib/format` is re-exported whole, so it lives apart.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isBareIsoDay(value: unknown): value is string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/iso-day.ts"],"sourcesContent":["/**\n * The bare ISO calendar day, `\"YYYY-MM-DD\"` — no time, no zone: the day it names in every\n * zone. `toDate` and `formatDate` (lib/format), the kit's day ladder (`useDayFormatter`,\n * lib/day-format) and the preference's renderers (lib/date-format) each kept a copy of this\n * pattern (0.52 code review, lib/day-format.ts). Were one to learn a new shape (say\n * `YYYY-MM-DDT00:00`) alone, the ladder would ask the provider for a different day than the\n * kit prints. One rule.\n *\n * @internal Not part of the barrel: `lib/format` is re-exported whole, so it lives apart.\n */\nexport function isBareIsoDay(value: unknown): value is string {\n return typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value);\n}\n"],"mappings":";AAUO,SAAS,aAAa,OAAiC;AAC5D,SAAO,OAAO,UAAU,YAAY,sBAAsB,KAAK,KAAK;AACtE;","names":[]}
|