@eifi1/ui-kit 0.53.0 → 0.55.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 +96 -82
- package/dist/account/delete-account-setting.d.ts +3 -2
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/admin/plan-change.js +2 -2
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/user-roster.d.ts +6 -0
- package/dist/admin/user-roster.js +4 -0
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +36 -0
- package/dist/auth/auth-errors.js +20 -0
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +26 -0
- package/dist/billing/billing-labels.js +9 -0
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/subscription-standing.d.ts +61 -0
- package/dist/billing/subscription-standing.js +51 -0
- package/dist/billing/subscription-standing.js.map +1 -0
- package/dist/billing/usage-meter.d.ts +52 -0
- package/dist/billing/usage-meter.js +52 -0
- package/dist/billing/usage-meter.js.map +1 -0
- package/dist/chart.d.ts +1 -0
- package/dist/chart.js +4 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/alert-banner.js +2 -3
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -1
- package/dist/components/amount-input.js +3 -15
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +2 -2
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +2 -1
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button.d.ts +35 -1
- package/dist/components/button.js +5 -9
- package/dist/components/button.js.map +1 -1
- package/dist/components/calendar-heatmap.js +6 -4
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/camera-button.d.ts +89 -0
- package/dist/components/camera-button.js +122 -0
- package/dist/components/camera-button.js.map +1 -0
- package/dist/components/camera-capture.d.ts +66 -0
- package/dist/components/camera-capture.js +190 -0
- package/dist/components/camera-capture.js.map +1 -0
- package/dist/components/camera-labels.d.ts +20 -0
- package/dist/components/camera-labels.js +15 -0
- package/dist/components/camera-labels.js.map +1 -0
- package/dist/components/chart-edge.d.ts +9 -0
- package/dist/components/chart-edge.js +14 -3
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-zoom-core.js +34 -29
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -1
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/combobox-core.js +3 -2
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +2 -2
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/consent-again-notice-labels.d.ts +25 -0
- package/dist/components/consent-again-notice-labels.js +15 -0
- package/dist/components/consent-again-notice-labels.js.map +1 -0
- package/dist/components/consent-again-notice.d.ts +85 -0
- package/dist/components/consent-again-notice.js +94 -0
- package/dist/components/consent-again-notice.js.map +1 -0
- package/dist/components/country-select.js +2 -7
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.d.ts +2 -1
- package/dist/components/currency-option-list.js +10 -3
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.d.ts +3 -1
- package/dist/components/currency-select.js +2 -2
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +22 -1
- package/dist/components/danger-confirm.js +52 -30
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-column-settings.js +2 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +2 -1
- package/dist/components/data-table-filter-popover.js +3 -2
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +33 -3
- package/dist/components/data-table-filters.js +6 -4
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-virtual.js +13 -3
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/date-field.js +2 -2
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.js +2 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js +2 -6
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +5 -1
- package/dist/components/dialog-frame.js +6 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/dropdown.js +7 -3
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.js +2 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/field-parts.d.ts +108 -6
- package/dist/components/field-parts.js +59 -6
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-suggestion-labels.d.ts +2 -0
- package/dist/components/field-suggestion-labels.js +10 -0
- package/dist/components/field-suggestion-labels.js.map +1 -0
- package/dist/components/field.js +1 -1
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/floating-panel.d.ts +7 -6
- package/dist/components/floating-panel.js +3 -17
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +3 -2
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.js +3 -2
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.d.ts +23 -5
- package/dist/components/icon-button.js +6 -8
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.js +2 -1
- package/dist/components/inline-confirm.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +2 -2
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +3 -3
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal.d.ts +13 -1
- package/dist/components/legal.js +12 -0
- package/dist/components/legal.js.map +1 -1
- package/dist/components/legended-series-chart.d.ts +80 -0
- package/dist/components/legended-series-chart.js +70 -0
- package/dist/components/legended-series-chart.js.map +1 -0
- package/dist/components/lightbox.js +2 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/mini-calendar.js +16 -7
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +48 -0
- package/dist/components/modal.js +26 -6
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +1 -1
- package/dist/components/month-picker.js +6 -6
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js +2 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-input.js +2 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/page-header-layout.d.ts +18 -0
- package/dist/components/page-header-layout.js +23 -0
- package/dist/components/page-header-layout.js.map +1 -0
- package/dist/components/page-header-skeleton.d.ts +41 -0
- package/dist/components/page-header-skeleton.js +52 -0
- package/dist/components/page-header-skeleton.js.map +1 -0
- package/dist/components/page-header.js +4 -17
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/popover.js +7 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/release-notes-labels.d.ts +16 -0
- package/dist/components/release-notes-labels.js +18 -0
- package/dist/components/release-notes-labels.js.map +1 -0
- package/dist/components/release-notes.d.ts +65 -0
- package/dist/components/release-notes.js +65 -0
- package/dist/components/release-notes.js.map +1 -0
- package/dist/components/select.js +2 -2
- package/dist/components/select.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +9 -0
- package/dist/components/series-chart-labels.js +3 -1
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +5 -0
- package/dist/components/series-chart.js +89 -36
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/stat-tile.d.ts +21 -2
- package/dist/components/stat-tile.js +4 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/toggle-group.js +3 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/treemap-layout.js.map +1 -1
- package/dist/components/treemap.d.ts +17 -15
- package/dist/components/treemap.js +167 -85
- package/dist/components/treemap.js.map +1 -1
- package/dist/currencies.d.ts +16 -2
- package/dist/currencies.js +2 -1
- package/dist/currencies.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +2 -2
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-context-labels.js +2 -1
- package/dist/feedback/feedback-context-labels.js.map +1 -1
- package/dist/feedback/feedback-context.d.ts +19 -3
- package/dist/feedback/feedback-context.js +12 -1
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-diagnostics.d.ts +17 -0
- package/dist/feedback/feedback-diagnostics.js +27 -0
- package/dist/feedback/feedback-diagnostics.js.map +1 -0
- package/dist/feedback/feedback-dialog.d.ts +10 -0
- package/dist/feedback/feedback-dialog.js +6 -4
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox-page.d.ts +260 -0
- package/dist/feedback/feedback-inbox-page.js +207 -0
- package/dist/feedback/feedback-inbox-page.js.map +1 -0
- package/dist/feedback/feedback-record.d.ts +5 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +1 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +3 -0
- package/dist/feedback/feedback-row-detail.js +3 -0
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
- package/dist/feedback/feedback-submit-dialog.js +15 -3
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.d.ts +30 -2
- package/dist/feedback/feedback-submit.js +5 -2
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js +2 -2
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback.d.ts +1 -0
- package/dist/feedback.js +6 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-debounce.d.ts +32 -5
- package/dist/hooks/use-debounce.js +6 -5
- package/dist/hooks/use-debounce.js.map +1 -1
- package/dist/hooks/use-file-picker.d.ts +3 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +19 -3
- package/dist/hooks/use-focus-trap.js +15 -5
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-has-camera.d.ts +36 -0
- package/dist/hooks/use-has-camera.js +53 -0
- package/dist/hooks/use-has-camera.js.map +1 -0
- package/dist/hooks/use-hotkey.js +2 -1
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-overlay-history.d.ts +6 -1
- package/dist/hooks/use-overlay-history.js +10 -2
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +11 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +78 -4
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +20 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +95 -5
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +74 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +75 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +77 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +77 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +74 -4
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +22 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +27 -8
- package/dist/index.js +41 -2
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-labels.d.ts +8 -0
- package/dist/landing/landing-labels.js +2 -1
- package/dist/landing/landing-labels.js.map +1 -1
- package/dist/lib/changelog.d.ts +130 -0
- package/dist/lib/changelog.js +137 -0
- package/dist/lib/changelog.js.map +1 -0
- package/dist/lib/clipboard-minus.js +2 -2
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.d.ts +36 -4
- package/dist/lib/dates.js +49 -13
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/editable-content.d.ts +33 -0
- package/dist/lib/editable-content.js +15 -0
- package/dist/lib/editable-content.js.map +1 -0
- package/dist/lib/focus-left.d.ts +47 -1
- package/dist/lib/focus-left.js +11 -2
- package/dist/lib/focus-left.js.map +1 -1
- package/dist/lib/format.d.ts +37 -2
- package/dist/lib/format.js +25 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/intl-cache.d.ts +4 -2
- package/dist/lib/intl-cache.js.map +1 -1
- package/dist/lib/server-wake.d.ts +49 -9
- package/dist/lib/server-wake.js +25 -14
- package/dist/lib/server-wake.js.map +1 -1
- package/dist/lib/session-refresh.d.ts +53 -0
- package/dist/lib/session-refresh.js +27 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/translation-review.d.ts +26 -0
- package/dist/lib/translation-review.js +14 -0
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/rhf/fields.js +3 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/shell/app-shell.d.ts +25 -1
- package/dist/shell/app-shell.js +3 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +3 -0
- package/dist/shell/dev-menu.js +4 -2
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/held-changes-banner-labels.d.ts +23 -0
- package/dist/shell/held-changes-banner-labels.js +14 -0
- package/dist/shell/held-changes-banner-labels.js.map +1 -0
- package/dist/shell/held-changes-banner.d.ts +75 -0
- package/dist/shell/held-changes-banner.js +106 -0
- package/dist/shell/held-changes-banner.js.map +1 -0
- package/dist/shell/sync-status-indicator.d.ts +27 -12
- package/dist/shell/sync-status-indicator.js +13 -4
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +5 -0
- package/dist/shell/top-bar-parts-labels.js +3 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +3 -0
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +11 -2
- package/dist/sync/engine.js +13 -5
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync/hooks.d.ts +3 -2
- package/dist/sync/hooks.js +4 -2
- package/dist/sync/hooks.js.map +1 -1
- package/dist/testing/css-variables.generated.d.ts +2 -2
- package/dist/testing/css-variables.generated.js +1 -1
- package/dist/testing/css-variables.generated.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +18 -0
- package/dist/theme/chart-palette.js +17 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/contrast-tokens.d.ts +3 -3
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +4 -4
- package/dist/theme/contrast.js.map +1 -1
- package/dist/tour/tour-overlay.js +2 -1
- package/dist/tour/tour-overlay.js.map +1 -1
- package/package.json +5 -9
- package/tokens.css +19 -54
- package/src/account/account-parts.ts +0 -47
- package/src/account/account-settings-labels.ts +0 -240
- package/src/account/account-settings.tsx +0 -698
- package/src/account/data-export-setting-labels.ts +0 -20
- package/src/account/data-export-setting.tsx +0 -200
- package/src/account/delete-account-setting-labels.ts +0 -33
- package/src/account/delete-account-setting.tsx +0 -341
- package/src/account/email-change-setting-labels.ts +0 -31
- package/src/account/email-change-setting.tsx +0 -443
- package/src/account/passkeys-setting.tsx +0 -445
- package/src/account/sessions-setting-labels.ts +0 -24
- package/src/account/sessions-setting.tsx +0 -283
- package/src/admin/admin-action-confirm-labels.ts +0 -19
- package/src/admin/admin-action-confirm.tsx +0 -509
- package/src/admin/admin-action-log-labels.ts +0 -83
- package/src/admin/admin-action-log.tsx +0 -464
- package/src/admin/admin-area-labels.ts +0 -114
- package/src/admin/admin-forbidden.tsx +0 -75
- package/src/admin/admin-overview.tsx +0 -258
- package/src/admin/admin-parts.ts +0 -68
- package/src/admin/admin-sections.tsx +0 -208
- package/src/admin/admin-tool-links.tsx +0 -90
- package/src/admin/invitations-panel-labels.ts +0 -31
- package/src/admin/invitations-panel.tsx +0 -516
- package/src/admin/mail-test-card-labels.ts +0 -19
- package/src/admin/mail-test-card.tsx +0 -189
- package/src/admin/plan-change-labels.ts +0 -5
- package/src/admin/plan-change.tsx +0 -474
- package/src/admin/reviewer-scope-editor-labels.ts +0 -16
- package/src/admin/reviewer-scope-editor.tsx +0 -224
- package/src/admin/role-select-labels.ts +0 -16
- package/src/admin/role-select.tsx +0 -282
- package/src/admin/transfer-ownership-dialog-labels.ts +0 -20
- package/src/admin/transfer-ownership-dialog.tsx +0 -162
- package/src/admin/user-roster-labels.ts +0 -17
- package/src/admin/user-roster.tsx +0 -559
- package/src/auth/accept-invitation-labels.ts +0 -21
- package/src/auth/accept-invitation.tsx +0 -210
- package/src/auth/auth-errors-labels.ts +0 -23
- package/src/auth/auth-errors.ts +0 -370
- package/src/auth/complete-name-dialog-labels.ts +0 -16
- package/src/auth/complete-name-dialog.tsx +0 -253
- package/src/auth/email-tag.ts +0 -75
- package/src/auth/forgot-password-form-labels.ts +0 -20
- package/src/auth/forgot-password-form.tsx +0 -221
- package/src/auth/form-rules.ts +0 -40
- package/src/auth/new-password-fields.tsx +0 -85
- package/src/auth/not-found-page-labels.ts +0 -12
- package/src/auth/not-found-page.tsx +0 -95
- package/src/auth/register-form-labels.ts +0 -31
- package/src/auth/register-form.tsx +0 -456
- package/src/auth/reset-password-form-labels.ts +0 -27
- package/src/auth/reset-password-form.tsx +0 -341
- package/src/auth/sign-in-form-labels.ts +0 -44
- package/src/auth/sign-in-form.tsx +0 -858
- package/src/auth/status-parts.tsx +0 -132
- package/src/auth/verify-email-labels.ts +0 -26
- package/src/auth/verify-email.tsx +0 -445
- package/src/billing/billing-banner.tsx +0 -258
- package/src/billing/billing-labels.ts +0 -381
- package/src/billing/billing-lock.ts +0 -108
- package/src/billing/billing-overview.ts +0 -157
- package/src/billing/billing-standing.ts +0 -80
- package/src/billing/cancel-request.ts +0 -97
- package/src/billing/cancel-subscription.tsx +0 -98
- package/src/billing/checkout-processing.ts +0 -394
- package/src/billing/checkout-return.ts +0 -90
- package/src/billing/pay-page.ts +0 -330
- package/src/billing/plan-card.tsx +0 -562
- package/src/billing/plan-change-labels.ts +0 -66
- package/src/billing/plan-limit-notice.tsx +0 -148
- package/src/billing/plan-limit-toast.tsx +0 -90
- package/src/billing/plan-limit.ts +0 -52
- package/src/billing/plan-price.ts +0 -98
- package/src/billing/plan-switch.tsx +0 -216
- package/src/billing/provider-page.ts +0 -84
- package/src/billing/subscription-actions.tsx +0 -247
- package/src/billing/subscription-status-chip.tsx +0 -95
- package/src/chart.ts +0 -103
- package/src/components/account-chips-labels.ts +0 -16
- package/src/components/account-chips.tsx +0 -474
- package/src/components/alert-banner.tsx +0 -670
- package/src/components/amount-input.tsx +0 -815
- package/src/components/appearance-labels.ts +0 -63
- package/src/components/authed-image-labels.ts +0 -11
- package/src/components/authed-image.tsx +0 -209
- package/src/components/autocomplete.tsx +0 -645
- package/src/components/breadcrumbs-labels.ts +0 -10
- package/src/components/breadcrumbs.tsx +0 -205
- package/src/components/bulk-action-bar-labels.ts +0 -11
- package/src/components/bulk-action-bar.tsx +0 -443
- package/src/components/button-group.tsx +0 -223
- package/src/components/button-look.ts +0 -256
- package/src/components/button.tsx +0 -339
- package/src/components/calculator.tsx +0 -341
- package/src/components/calendar-heatmap-labels.ts +0 -15
- package/src/components/calendar-heatmap.tsx +0 -594
- package/src/components/card.tsx +0 -377
- package/src/components/chart-edge.ts +0 -106
- package/src/components/chart-figure.tsx +0 -35
- package/src/components/chart-height.ts +0 -30
- package/src/components/chart-zoom-core.tsx +0 -627
- package/src/components/chart-zoom.tsx +0 -22
- package/src/components/chart.tsx +0 -554
- package/src/components/checkbox-group.tsx +0 -247
- package/src/components/checkbox.tsx +0 -346
- package/src/components/chip-input.tsx +0 -370
- package/src/components/chip-labels.ts +0 -13
- package/src/components/chip.tsx +0 -843
- package/src/components/choice-card.tsx +0 -544
- package/src/components/column-mapper-labels.ts +0 -38
- package/src/components/column-mapper.tsx +0 -655
- package/src/components/combobox-core.tsx +0 -1321
- package/src/components/combobox-suggestions.tsx +0 -131
- package/src/components/combobox.tsx +0 -716
- package/src/components/confirm-context.ts +0 -7
- package/src/components/confirm-dialog-labels.ts +0 -13
- package/src/components/confirm-dialog.tsx +0 -341
- package/src/components/copy-button-labels.ts +0 -13
- package/src/components/copy-button.tsx +0 -192
- package/src/components/country-select-labels.ts +0 -11
- package/src/components/country-select.tsx +0 -539
- package/src/components/currency-option-list.tsx +0 -180
- package/src/components/currency-select.tsx +0 -341
- package/src/components/danger-confirm-labels.ts +0 -20
- package/src/components/danger-confirm.tsx +0 -934
- package/src/components/data-table-cells.tsx +0 -118
- package/src/components/data-table-column-settings.tsx +0 -198
- package/src/components/data-table-filter-popover.tsx +0 -281
- package/src/components/data-table-filters.ts +0 -287
- package/src/components/data-table-fit.ts +0 -301
- package/src/components/data-table-groups.tsx +0 -288
- package/src/components/data-table-head.tsx +0 -280
- package/src/components/data-table-labels.ts +0 -198
- package/src/components/data-table-leading-row.tsx +0 -58
- package/src/components/data-table-mobile.tsx +0 -1117
- package/src/components/data-table-pagination.tsx +0 -202
- package/src/components/data-table-resize.ts +0 -188
- package/src/components/data-table-row-actions.tsx +0 -302
- package/src/components/data-table-row.tsx +0 -496
- package/src/components/data-table-selection.ts +0 -71
- package/src/components/data-table-shared.tsx +0 -287
- package/src/components/data-table-sort.ts +0 -163
- package/src/components/data-table-virtual.ts +0 -576
- package/src/components/data-table.tsx +0 -2598
- package/src/components/date-field.tsx +0 -684
- package/src/components/date-format-setting.tsx +0 -64
- package/src/components/date-picker.tsx +0 -404
- package/src/components/date-range-picker.tsx +0 -518
- package/src/components/description-list-labels.ts +0 -9
- package/src/components/description-list.tsx +0 -386
- package/src/components/dialog-frame-labels.ts +0 -7
- package/src/components/dialog-frame.tsx +0 -276
- package/src/components/disclosure.tsx +0 -476
- package/src/components/dropdown.tsx +0 -461
- package/src/components/empty-state.tsx +0 -136
- package/src/components/entity-combobox.tsx +0 -422
- package/src/components/error-boundary-labels.ts +0 -20
- package/src/components/error-boundary.tsx +0 -675
- package/src/components/facing-pair.ts +0 -93
- package/src/components/field-base-labels.ts +0 -20
- package/src/components/field-base.tsx +0 -656
- package/src/components/field-parts.tsx +0 -825
- package/src/components/field-strip.tsx +0 -129
- package/src/components/field.tsx +0 -206
- package/src/components/file-button.tsx +0 -295
- package/src/components/file-dropzone.tsx +0 -559
- package/src/components/floating-panel-labels.ts +0 -10
- package/src/components/floating-panel.tsx +0 -828
- package/src/components/focus-ring.ts +0 -16
- package/src/components/form-actions-labels.ts +0 -11
- package/src/components/form-actions.tsx +0 -578
- package/src/components/full-bleed-dialog.tsx +0 -294
- package/src/components/grouped-picker.tsx +0 -184
- package/src/components/help-popover.tsx +0 -117
- package/src/components/hover-menu.tsx +0 -343
- package/src/components/iban-input-labels.ts +0 -14
- package/src/components/iban-input.tsx +0 -236
- package/src/components/icon-button.tsx +0 -959
- package/src/components/icon-picker-labels.ts +0 -13
- package/src/components/icon-picker.tsx +0 -232
- package/src/components/image-grid-labels.ts +0 -11
- package/src/components/image-grid.tsx +0 -202
- package/src/components/inline-confirm.tsx +0 -116
- package/src/components/inline-edit-field-labels.ts +0 -11
- package/src/components/inline-edit-field.tsx +0 -552
- package/src/components/inline-entity-combobox.tsx +0 -849
- package/src/components/input.tsx +0 -394
- package/src/components/jump-calendar.tsx +0 -265
- package/src/components/language-select.tsx +0 -93
- package/src/components/legal-labels.ts +0 -105
- package/src/components/legal-page.tsx +0 -711
- package/src/components/legal.tsx +0 -668
- package/src/components/lightbox-labels.ts +0 -18
- package/src/components/lightbox.tsx +0 -399
- package/src/components/line-items-labels.ts +0 -14
- package/src/components/line-items.tsx +0 -602
- package/src/components/list-labels.ts +0 -12
- package/src/components/list.tsx +0 -870
- package/src/components/loading-state.tsx +0 -78
- package/src/components/measured-grid-labels.ts +0 -25
- package/src/components/measured-grid.tsx +0 -837
- package/src/components/menu-item.tsx +0 -241
- package/src/components/mini-calendar-labels.ts +0 -17
- package/src/components/mini-calendar.tsx +0 -789
- package/src/components/modal.tsx +0 -505
- package/src/components/money-draft.ts +0 -83
- package/src/components/money-field.tsx +0 -76
- package/src/components/month-grid.tsx +0 -229
- package/src/components/month-picker-labels.ts +0 -22
- package/src/components/month-picker.tsx +0 -1087
- package/src/components/multi-entity-combobox.tsx +0 -369
- package/src/components/multi-select.tsx +0 -376
- package/src/components/nav-pills.tsx +0 -198
- package/src/components/number-field.tsx +0 -327
- package/src/components/number-input.tsx +0 -558
- package/src/components/numeric-field.ts +0 -169
- package/src/components/numpad-sheet.tsx +0 -285
- package/src/components/one-time-code-input.tsx +0 -270
- package/src/components/page-contents-labels.ts +0 -9
- package/src/components/page-contents.tsx +0 -282
- package/src/components/page-header-context.ts +0 -8
- package/src/components/page-header.tsx +0 -298
- package/src/components/password-strength-labels.ts +0 -22
- package/src/components/password-strength.tsx +0 -241
- package/src/components/phone-input-labels.ts +0 -10
- package/src/components/phone-input.tsx +0 -282
- package/src/components/picker-sheet.tsx +0 -330
- package/src/components/pie-chart-labels.ts +0 -40
- package/src/components/pie-chart.tsx +0 -480
- package/src/components/popover-labels.ts +0 -14
- package/src/components/popover.tsx +0 -268
- package/src/components/progress-bar-labels.ts +0 -10
- package/src/components/progress-bar.tsx +0 -479
- package/src/components/qr-code.tsx +0 -76
- package/src/components/reauth-dialog-labels.ts +0 -13
- package/src/components/reauth-dialog.tsx +0 -265
- package/src/components/row-actions-labels.ts +0 -12
- package/src/components/row-actions.tsx +0 -795
- package/src/components/scroll-area.tsx +0 -123
- package/src/components/search-field.tsx +0 -192
- package/src/components/select.tsx +0 -294
- package/src/components/separator.tsx +0 -33
- package/src/components/series-chart-axes.ts +0 -342
- package/src/components/series-chart-budget.ts +0 -150
- package/src/components/series-chart-labels.ts +0 -33
- package/src/components/series-chart-thin.ts +0 -180
- package/src/components/series-chart-ticks.ts +0 -194
- package/src/components/series-chart.tsx +0 -1906
- package/src/components/server-wake-labels.ts +0 -11
- package/src/components/server-wake.tsx +0 -129
- package/src/components/settings-fields.tsx +0 -237
- package/src/components/share-card-labels.ts +0 -30
- package/src/components/share-card.tsx +0 -675
- package/src/components/signature-pad-labels.ts +0 -24
- package/src/components/signature-pad.tsx +0 -747
- package/src/components/signed-amount-labels.ts +0 -10
- package/src/components/signed-amount.tsx +0 -336
- package/src/components/skeleton.tsx +0 -80
- package/src/components/slider.tsx +0 -338
- package/src/components/sparkline-labels.ts +0 -14
- package/src/components/sparkline.tsx +0 -322
- package/src/components/spinner.tsx +0 -81
- package/src/components/stat-tile-labels.ts +0 -15
- package/src/components/stat-tile.tsx +0 -725
- package/src/components/status-dot.tsx +0 -170
- package/src/components/swatch-picker-labels.ts +0 -10
- package/src/components/swatch-picker.tsx +0 -283
- package/src/components/swipeable-row.tsx +0 -386
- package/src/components/switch.tsx +0 -273
- package/src/components/table-skeleton.tsx +0 -136
- package/src/components/table.tsx +0 -647
- package/src/components/tabs-labels.ts +0 -13
- package/src/components/tabs.tsx +0 -693
- package/src/components/text-link.tsx +0 -359
- package/src/components/text.tsx +0 -150
- package/src/components/textarea.tsx +0 -330
- package/src/components/tile-radio.tsx +0 -332
- package/src/components/time-input.tsx +0 -121
- package/src/components/toast-labels.ts +0 -12
- package/src/components/toast.tsx +0 -519
- package/src/components/toggle-group.tsx +0 -752
- package/src/components/toggle-legend.tsx +0 -394
- package/src/components/tooltip.tsx +0 -1149
- package/src/components/translation-review-editor.tsx +0 -290
- package/src/components/translation-review-labels.ts +0 -181
- package/src/components/translation-review-parts.tsx +0 -272
- package/src/components/translation-review.tsx +0 -1240
- package/src/components/tree-view.tsx +0 -589
- package/src/components/treemap-labels.ts +0 -47
- package/src/components/treemap-layout.ts +0 -210
- package/src/components/treemap.tsx +0 -1184
- package/src/components/trigger-aria.ts +0 -44
- package/src/components/use-anchor-dir.ts +0 -20
- package/src/components/use-mobile-reveal.ts +0 -62
- package/src/components/use-table-state.ts +0 -753
- package/src/components/user-avatar.tsx +0 -116
- package/src/components/wizard-stepper.tsx +0 -42
- package/src/components/write-lock-labels.ts +0 -10
- package/src/components/write-lock.tsx +0 -253
- package/src/currencies.ts +0 -70
- package/src/data-table.ts +0 -42
- package/src/demo/demo-banner.tsx +0 -153
- package/src/demo/demo-countdown.ts +0 -136
- package/src/demo/demo-ended.tsx +0 -90
- package/src/demo/demo-labels.ts +0 -78
- package/src/demo/demo-session.ts +0 -32
- package/src/demo/demo-start.tsx +0 -156
- package/src/eslint/index.ts +0 -161
- package/src/eslint/role-utilities.generated.ts +0 -146
- package/src/eslint/role-utilities.ts +0 -233
- package/src/feedback/feedback-attachment-download.tsx +0 -77
- package/src/feedback/feedback-attachment-labels.ts +0 -37
- package/src/feedback/feedback-attachment.tsx +0 -1239
- package/src/feedback/feedback-capture.ts +0 -287
- package/src/feedback/feedback-comment-thread.tsx +0 -353
- package/src/feedback/feedback-context-labels.ts +0 -10
- package/src/feedback/feedback-context.tsx +0 -213
- package/src/feedback/feedback-crash.ts +0 -547
- package/src/feedback/feedback-dialog-labels.ts +0 -29
- package/src/feedback/feedback-dialog.tsx +0 -440
- package/src/feedback/feedback-entry-file.tsx +0 -150
- package/src/feedback/feedback-inbox.tsx +0 -552
- package/src/feedback/feedback-labels.ts +0 -144
- package/src/feedback/feedback-menu-labels.ts +0 -12
- package/src/feedback/feedback-menu.tsx +0 -177
- package/src/feedback/feedback-mutation.ts +0 -20
- package/src/feedback/feedback-page-link.ts +0 -90
- package/src/feedback/feedback-public-labels.ts +0 -81
- package/src/feedback/feedback-public-list.tsx +0 -524
- package/src/feedback/feedback-public-switch.tsx +0 -102
- package/src/feedback/feedback-public-types.ts +0 -101
- package/src/feedback/feedback-record.ts +0 -423
- package/src/feedback/feedback-replies-context.ts +0 -9
- package/src/feedback/feedback-replies.tsx +0 -343
- package/src/feedback/feedback-rework-entry-chip.tsx +0 -18
- package/src/feedback/feedback-row-detail-labels.ts +0 -30
- package/src/feedback/feedback-row-detail.tsx +0 -858
- package/src/feedback/feedback-status-undo.ts +0 -109
- package/src/feedback/feedback-submit-dialog.tsx +0 -118
- package/src/feedback/feedback-submit.tsx +0 -237
- package/src/feedback/feedback-swipe.tsx +0 -192
- package/src/feedback/feedback-table-labels.ts +0 -27
- package/src/feedback/feedback-table.tsx +0 -912
- package/src/feedback/feedback-thread-labels.ts +0 -22
- package/src/feedback/feedback-thread-parts-labels.ts +0 -41
- package/src/feedback/feedback-thread.tsx +0 -905
- package/src/feedback/feedback-vocabulary.ts +0 -263
- package/src/feedback/feedback-vote-button.tsx +0 -124
- package/src/feedback.ts +0 -116
- package/src/hooks/use-anchored-panel.ts +0 -396
- package/src/hooks/use-anchored-rect.ts +0 -77
- package/src/hooks/use-announce.ts +0 -85
- package/src/hooks/use-authed-src.ts +0 -167
- package/src/hooks/use-body-scroll-lock.ts +0 -119
- package/src/hooks/use-breakpoint.ts +0 -133
- package/src/hooks/use-close-transition.ts +0 -107
- package/src/hooks/use-copy-to-clipboard.ts +0 -122
- package/src/hooks/use-debounce.ts +0 -114
- package/src/hooks/use-dismiss.ts +0 -371
- package/src/hooks/use-document-title.ts +0 -33
- package/src/hooks/use-file-drop.ts +0 -121
- package/src/hooks/use-file-picker-labels.ts +0 -28
- package/src/hooks/use-file-picker.tsx +0 -359
- package/src/hooks/use-focus-trap.ts +0 -199
- package/src/hooks/use-hotkey.ts +0 -189
- package/src/hooks/use-large-text.ts +0 -41
- package/src/hooks/use-long-press.ts +0 -140
- package/src/hooks/use-media-query.ts +0 -39
- package/src/hooks/use-mounted.ts +0 -39
- package/src/hooks/use-noindex.ts +0 -37
- package/src/hooks/use-overlay-history.ts +0 -714
- package/src/hooks/use-row-swipe.ts +0 -207
- package/src/hooks/use-search-param-state.ts +0 -434
- package/src/hooks/use-windowed-rows.ts +0 -225
- package/src/i18n/defaults.ts +0 -267
- package/src/i18n/german.ts +0 -2057
- package/src/i18n/kit-labels.tsx +0 -1003
- package/src/i18n/languages.ts +0 -349
- package/src/i18n/locales/de-CH.ts +0 -24
- package/src/i18n/locales/en.ts +0 -267
- package/src/i18n/locales/es.ts +0 -1997
- package/src/i18n/locales/fr.ts +0 -2035
- package/src/i18n/locales/hu.ts +0 -2002
- package/src/i18n/locales/it.ts +0 -2012
- package/src/i18n/locales/zh.ts +0 -1910
- package/src/i18n/plurals.ts +0 -86
- package/src/i18n/review.ts +0 -475
- package/src/i18n/shared-labels.ts +0 -138
- package/src/i18n/swiss.ts +0 -29
- package/src/index.ts +0 -1179
- package/src/landing/access.ts +0 -93
- package/src/landing/landing-actions.tsx +0 -97
- package/src/landing/landing-labels.ts +0 -59
- package/src/landing/landing-sections.tsx +0 -341
- package/src/landing/page-seo.ts +0 -261
- package/src/landing/public-header.tsx +0 -121
- package/src/landing/public-page.tsx +0 -106
- package/src/landing/routing.tsx +0 -225
- package/src/lib/broadcast.ts +0 -52
- package/src/lib/calc.ts +0 -323
- package/src/lib/chip-edge-snap.ts +0 -212
- package/src/lib/clipboard-minus.ts +0 -107
- package/src/lib/clipping.ts +0 -34
- package/src/lib/cn.ts +0 -28
- package/src/lib/column-mapping.ts +0 -234
- package/src/lib/countries.ts +0 -91
- package/src/lib/data-attributes.ts +0 -14
- package/src/lib/date-format.ts +0 -227
- package/src/lib/dates.ts +0 -266
- package/src/lib/day-format.ts +0 -96
- package/src/lib/decimal-marks.ts +0 -108
- package/src/lib/describe-failure.ts +0 -18
- package/src/lib/device-settings.ts +0 -147
- package/src/lib/direction.ts +0 -74
- package/src/lib/document-navigation.ts +0 -17
- package/src/lib/document-title.ts +0 -41
- package/src/lib/download.ts +0 -172
- package/src/lib/float-clearance.ts +0 -70
- package/src/lib/floating-stack.ts +0 -70
- package/src/lib/focus-left.ts +0 -156
- package/src/lib/format.ts +0 -522
- package/src/lib/http-status.ts +0 -38
- package/src/lib/iban.ts +0 -251
- package/src/lib/ime.ts +0 -24
- package/src/lib/intl-cache.ts +0 -25
- package/src/lib/iso-day.ts +0 -13
- package/src/lib/lazy-with-fallback.ts +0 -82
- package/src/lib/logger.ts +0 -69
- package/src/lib/minus.ts +0 -50
- package/src/lib/path-pattern.ts +0 -55
- package/src/lib/person-name.ts +0 -155
- package/src/lib/phone.ts +0 -208
- package/src/lib/plain-text.ts +0 -22
- package/src/lib/presence.ts +0 -18
- package/src/lib/qr-encode.ts +0 -444
- package/src/lib/quiet-focus.ts +0 -32
- package/src/lib/reduced-motion.ts +0 -22
- package/src/lib/refs.ts +0 -22
- package/src/lib/round.ts +0 -43
- package/src/lib/safe-storage.ts +0 -119
- package/src/lib/server-wake.ts +0 -315
- package/src/lib/session-refresh.ts +0 -472
- package/src/lib/share-format.ts +0 -18
- package/src/lib/status-tone.ts +0 -43
- package/src/lib/strip-fade.ts +0 -287
- package/src/lib/table-text.ts +0 -554
- package/src/lib/text-case.ts +0 -19
- package/src/lib/text-match.ts +0 -169
- package/src/lib/thenable.ts +0 -19
- package/src/lib/translation-review.ts +0 -717
- package/src/notifications/device-zone-report.ts +0 -67
- package/src/notifications/feedback-notification-types-labels.ts +0 -33
- package/src/notifications/feedback-notification-types.ts +0 -71
- package/src/notifications/notification-bell.tsx +0 -119
- package/src/notifications/notification-inbox.tsx +0 -309
- package/src/notifications/notification-parts.tsx +0 -81
- package/src/notifications/notification-types-setting.tsx +0 -406
- package/src/notifications/notification-types.ts +0 -89
- package/src/notifications/notifications-labels.ts +0 -287
- package/src/notifications/push-device-setting.tsx +0 -230
- package/src/notifications/push-devices-setting.tsx +0 -374
- package/src/notifications/push-worker.ts +0 -168
- package/src/notifications/quiet-hours-setting.tsx +0 -238
- package/src/notifications/unsubscribe-panel.tsx +0 -200
- package/src/notifications/use-push-device.ts +0 -448
- package/src/offline/local-first.ts +0 -100
- package/src/offline/pwa-update-prompt-labels.ts +0 -9
- package/src/offline/pwa-update-prompt.tsx +0 -78
- package/src/offline/query-persistence.ts +0 -391
- package/src/offline/service-worker-updates.ts +0 -81
- package/src/offline/shared-computer-labels.ts +0 -8
- package/src/offline/shared-computer.tsx +0 -87
- package/src/offline/sign-out-pending-dialog-labels.ts +0 -21
- package/src/offline/sign-out-pending-dialog.tsx +0 -135
- package/src/offline.ts +0 -37
- package/src/push-worker.ts +0 -25
- package/src/rhf/fields.tsx +0 -2187
- package/src/rhf/form-core.tsx +0 -324
- package/src/rhf/form.tsx +0 -12
- package/src/rhf/line-items.tsx +0 -135
- package/src/rhf/use-rhf-wizard-step.ts +0 -104
- package/src/rhf.ts +0 -21
- package/src/search/command-palette-labels.ts +0 -16
- package/src/search/command-palette.tsx +0 -624
- package/src/search/global-search-labels.ts +0 -13
- package/src/search/global-search.tsx +0 -660
- package/src/search/search-index.ts +0 -283
- package/src/search/use-command-key.ts +0 -23
- package/src/search.ts +0 -14
- package/src/settings/device-reset-setting-labels.ts +0 -20
- package/src/settings/device-reset-setting.tsx +0 -116
- package/src/settings/settings-card-classes.ts +0 -11
- package/src/settings/settings-catalogue.ts +0 -150
- package/src/settings/settings-context.ts +0 -33
- package/src/settings/settings-heading.tsx +0 -67
- package/src/settings/settings-labels.ts +0 -87
- package/src/settings/settings-layout.tsx +0 -471
- package/src/settings/settings-section.tsx +0 -108
- package/src/settings/use-account-appearance.ts +0 -148
- package/src/settings/use-account-language.ts +0 -146
- package/src/settings/use-settings-focus.ts +0 -128
- package/src/settings/use-settings-route.ts +0 -258
- package/src/shell/account-menu-language.tsx +0 -130
- package/src/shell/account-menu-text-size.tsx +0 -80
- package/src/shell/admin-menu.tsx +0 -183
- package/src/shell/app-shell-labels.ts +0 -11
- package/src/shell/app-shell.tsx +0 -1560
- package/src/shell/auth-layout.tsx +0 -186
- package/src/shell/company-switcher-labels.ts +0 -13
- package/src/shell/company-switcher.tsx +0 -136
- package/src/shell/dev-menu.tsx +0 -257
- package/src/shell/fill-height.tsx +0 -45
- package/src/shell/language-code.ts +0 -5
- package/src/shell/option-switcher-menu.tsx +0 -84
- package/src/shell/sync-status-indicator.tsx +0 -303
- package/src/shell/top-bar-actions.tsx +0 -87
- package/src/shell/top-bar-brand.tsx +0 -78
- package/src/shell/top-bar-parts-labels.ts +0 -148
- package/src/shell/top-bar.tsx +0 -40
- package/src/shell/topbar-action-menu.tsx +0 -567
- package/src/shell/topbar-controls.tsx +0 -269
- package/src/shell/tour-menu.tsx +0 -187
- package/src/shell.ts +0 -108
- package/src/sync/db.ts +0 -129
- package/src/sync/engine.ts +0 -1011
- package/src/sync/hooks.ts +0 -98
- package/src/sync/testing.ts +0 -73
- package/src/sync/wire.ts +0 -61
- package/src/sync.ts +0 -37
- package/src/table-text.ts +0 -8
- package/src/testing/css-variables.generated.ts +0 -112
- package/src/testing/css-variables.ts +0 -189
- package/src/testing/index.ts +0 -29
- package/src/testing/react.ts +0 -58
- package/src/testing/tour.ts +0 -41
- package/src/testing/viewport.ts +0 -357
- package/src/theme/chart-palette.ts +0 -232
- package/src/theme/color.ts +0 -272
- package/src/theme/contrast-tokens.ts +0 -268
- package/src/theme/contrast.ts +0 -250
- package/src/theme/palette-derive.ts +0 -825
- package/src/theme/palette-presets.ts +0 -544
- package/src/theme/palette-store.ts +0 -108
- package/src/theme/text-size.ts +0 -289
- package/src/theme/theme-store.ts +0 -157
- package/src/tour/tour-catalogue-labels.ts +0 -75
- package/src/tour/tour-catalogue.tsx +0 -335
- package/src/tour/tour-dock.ts +0 -134
- package/src/tour/tour-engine.ts +0 -37
- package/src/tour/tour-gates.ts +0 -218
- package/src/tour/tour-guide-button.tsx +0 -64
- package/src/tour/tour-guide-labels.ts +0 -15
- package/src/tour/tour-info.ts +0 -144
- package/src/tour/tour-labels.ts +0 -23
- package/src/tour/tour-overlay.tsx +0 -1170
- package/src/tour/tour.tsx +0 -750
- package/src/tour/use-tour-progress.ts +0 -671
- package/src/tour/use-tour-url-sync.ts +0 -266
- package/src/tour.ts +0 -21
- package/src/vite-env.d.ts +0 -23
- package/src/wizard/stepper-nav.tsx +0 -360
- package/src/wizard/types.ts +0 -275
- package/src/wizard/use-wizard.ts +0 -560
- package/src/wizard/validation.ts +0 -39
- package/src/wizard/wizard-context.tsx +0 -74
- package/src/wizard/wizard-labels.ts +0 -29
- package/src/wizard/wizard-step.tsx +0 -76
- package/src/wizard/wizard-summary.tsx +0 -84
- package/src/wizard.ts +0 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/dev-menu.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { FlaskConical, Undo2 } from \"lucide-react\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type DevSwitchersLabels } from \"./top-bar-parts-labels\";\n// The kit's one storage guard, which 0.51 wrote here again by hand (DUP-8, refactor audit\n// 2026-10-30). Storage blocked, \"Back to\" lasts as long as this page.\nimport { readSessionStored, writeSessionStored } from \"../lib/safe-storage\";\n\n/** Where the role the session started with is kept, unless the app names its own key. */\nexport const DEV_ORIGINAL_ROLE_KEY = \"ui-kit:dev-original-role\";\n\n/**\n * One of the app's own groups in the {@link DevMenu} (docs/top-bar-harmonization.md\n * §2.7.1): keksdose's budget role, its \"today\" override, its resets.\n *\n * The rows are plain {@link TopBarMenuEntry}s, and there is no dev item type beside them\n * (the contract with keksdose, 0.51): a choice of one is radio rows (`checked`), a toggle\n * is a `checkable: \"checkbox\"` row, an action is a plain row. No form fields — a menu is\n * a `role=\"menu\"`, where a text or date input has no place in the arrow keys; a free\n * value is a \"Custom…\" row that opens the app's own dialog.\n */\nexport interface DevMenuSection {\n /** Unique among the sections. The rows' keys need be unique only within theirs: the\n * menu prefixes them with this. */\n key: string;\n /** The group's heading, drawn above its rows and announced as the group's name. */\n title: ReactNode;\n /** The rows, as `TopBarActionMenu` takes them. A `divider` ends the group, as there. */\n entries: readonly TopBarMenuEntry[];\n}\n\nexport interface DevMenuProps<R extends string, P extends string> {\n /**\n * Draw anything at all. The app passes `import.meta.env.DEV` AND its probe of the dev\n * route (§2.7.1): the constant keeps the menu off a production build's screen, the\n * probe off a dev build whose server has the route shut (§6.1) — flasks with nothing\n * behind them. Neither takes code out of the bundle; see `sections`.\n */\n enabled: boolean;\n roles?: readonly R[];\n /** The role the server enforces now. Named `currentRole`, not `role`, because `role` on\n * a JSX element is the ARIA role (as `RoleChip`'s `value`). */\n currentRole?: R;\n /** Apply a role — the app calls server-kit's `POST /dev/switch {role}` and re-reads\n * `/auth/me` (§2.7, §6.2). */\n onRole?: (role: R) => void;\n plans?: readonly P[];\n /** The plan the server enforces now. Left out (not known yet — kastlan's overview is a\n * 404 while billing is off), the plan group still renders, with no row checked; a\n * `GET /dev/switch` answers it. */\n currentPlan?: P;\n /** Apply a plan — `POST /dev/switch {plan}`, then the overview (§6.3). */\n onPlan?: (plan: P) => void;\n /**\n * The role to offer \"Back to {role}\" for. Left out, the kit keeps it itself: the role\n * before the first switch of this browser session, in `sessionStorage` under\n * `storageKey` — the server answers with the NEW role after a switch, so only the\n * browser still knows the old one.\n */\n originalRole?: R;\n /** The `sessionStorage` key for the kept role. Default {@link DEV_ORIGINAL_ROLE_KEY};\n * an app with several accounts on one browser may scope it per user. */\n storageKey?: string;\n /** Billing is off locally: the plan group says plans change nothing, and stays usable\n * (§6.3). */\n billingOff?: boolean;\n /** A role's or a plan's words in the menu. Default: the value itself. */\n roleLabel?: (role: R) => ReactNode;\n planLabel?: (plan: P) => ReactNode;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { dev } }}>` > English. */\n labels?: LabelOverride<DevSwitchersLabels>;\n /** 0.51: the app's own dev groups, after the kit's role and plan, in this order. Their\n * DEFINITIONS belong in a module only a dev build imports (§2.7.1): `enabled={false}`\n * hides the menu, but the code of a section built inline stays in the bundle. */\n sections?: readonly DevMenuSection[];\n /**\n * 0.51: an override is on that the developer could forget — a \"today\" that is not\n * today, a simulated slow server. The flask wears a dot, and its name says so (\"Dev\n * tools · Role: ADMIN · Plan: free Overrides on\"), since a dot alone tells a screen\n * reader nothing. The app decides what counts; the kit cannot see into its sections.\n */\n active?: boolean;\n}\n\nconst flask = <FlaskConical className=\"size-5 text-warning\" />;\n\n/** A label's text for a sentence, or the raw value when the label is not text. */\nconst textOf = (label: ReactNode, value: string) =>\n typeof label === \"string\" || typeof label === \"number\" ? String(label) : value;\n\n/** A row someone can pick — what makes a menu worth drawing. */\nconst isRow = (entry: TopBarMenuEntry) => entry.kind !== \"heading\" && entry.kind !== \"divider\";\n\n/**\n * The top bar's local-dev menu (docs/top-bar-harmonization.md §2.7, §2.7.1, §6.2, §6.3):\n * ONE amber flask over the kit's Account section and the app's own.\n *\n * 0.42's `DevSwitchers` drew a flask per switcher and had no place for anything else,\n * and an app has more dev controls than a role and a plan — keksdose's budget role, a\n * \"today\" override, resets (agreed with keksdose for 0.51). So the kit's part is now the\n * menu, and the app's rows go in it.\n *\n * - **The Account section** comes first: the role group, ending in \"Back to {role}\" once\n * the role differs from the one the session started with (the dev route needs only a\n * signed-in user, so a switch to a lesser role can always switch back); then the plan\n * group, which with `billingOff` notes that plans change nothing. Each renders only\n * with its list and handler.\n * - **The app's `sections`** follow, each a group under its own heading.\n * - **`active`** puts a dot on the flask for an override that is easy to forget.\n *\n * Nothing renders unless `enabled`, and nothing with no row to show. The panel scrolls\n * (`TopBarActionMenu`'s `scrollable`): some 25 rows ran off a 390×844 phone.\n *\n * From `md` up the groups stand side by side, one column each, in that order (0.51.4,\n * `TopBarActionMenu`'s `columns`; keksdose dev #640, Marcel: \"Each group gets a column\").\n * \"Back to {role}\" stays under the roles. A desktop too narrow for all of them wraps\n * the columns onto a second row; a phone keeps the one scrolling column.\n */\nexport function DevMenu<R extends string, P extends string>({\n enabled,\n roles,\n currentRole: role,\n onRole,\n plans,\n currentPlan: plan,\n onPlan,\n originalRole,\n storageKey = DEV_ORIGINAL_ROLE_KEY,\n billingOff = false,\n roleLabel = (r) => r,\n planLabel = (p) => p,\n labels: labelsProp,\n sections = [],\n active = false,\n}: DevMenuProps<R, P>) {\n const bar = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n const words = useTopBarPartsLabels(labelsProp ? { dev: labelsProp } : undefined).dev;\n const [kept, setKept] = useState<string | null>(() => (enabled ? readSessionStored(storageKey) : null));\n // 0.43 (keksdose): `enabled` may start false and turn true — an app's GET probe of the\n // dev route — so the kept role is read again then, or \"Back to {role}\" is lost after a\n // reload. Adjusted during render, React's pattern for state that follows a prop.\n const [wasEnabled, setWasEnabled] = useState(enabled);\n if (wasEnabled !== enabled) {\n setWasEnabled(enabled);\n if (enabled) setKept(readSessionStored(storageKey));\n }\n if (!enabled) return null;\n\n const original = (originalRole ?? kept ?? undefined) as R | undefined;\n\n const pickRole = (next: R) => {\n if (originalRole === undefined) {\n if (next === original) {\n // Back where the session began: nothing left to go back to.\n writeSessionStored(storageKey, null);\n setKept(null);\n } else if (kept === null && role !== undefined && next !== role) {\n writeSessionStored(storageKey, role);\n setKept(role);\n }\n }\n onRole?.(next);\n };\n\n const roleGroup: TopBarMenuEntry[] =\n roles && roles.length > 0 && role !== undefined && onRole\n ? [\n { kind: \"heading\", key: \"head-role\", label: words.roleHeading },\n ...roles.map((r): TopBarMenuEntry => ({\n key: `role-${r}`,\n label: roleLabel(r),\n checked: r === role,\n onSelect: () => pickRole(r),\n })),\n // Behind a rule, which ends the role group: \"Back to\" is a way out, not a role.\n ...(original !== undefined && original !== role\n ? ([\n { kind: \"divider\", key: \"sep-back\" },\n {\n key: \"back\",\n icon: <Undo2 className=\"size-4\" />,\n label: words.backTo(textOf(roleLabel(original), original)),\n onSelect: () => pickRole(original),\n },\n ] satisfies TopBarMenuEntry[])\n : []),\n ]\n : [];\n\n const planGroup: TopBarMenuEntry[] =\n plans && plans.length > 0 && onPlan\n ? [\n {\n kind: \"heading\",\n key: \"head-plan\",\n // The billing-off note is part of the group's NAME, under the heading. In 0.42's\n // own plan menu it was a trailing `<li>`: not a row, so the arrow keys passed\n // over it and a screen reader moving through the menu never reached it. Named,\n // it is read on the way into the plans. And in one menu a trailing note would\n // sit under the app's sections, not under the plans.\n label: billingOff ? (\n <>\n {words.planHeading}{\" \"}\n <span\n data-dev-billing-off=\"\"\n className=\"mt-0.5 block font-normal normal-case tracking-normal text-xs text-muted\"\n >\n {words.billingOff}\n </span>\n </>\n ) : (\n words.planHeading\n ),\n },\n ...plans.map((p): TopBarMenuEntry => ({\n key: `plan-${p}`,\n label: planLabel(p),\n checked: p === plan,\n onSelect: () => onPlan(p),\n })),\n ]\n : [];\n\n // The app's rows keyed under their section: two sections may well both have a \"reset\".\n const appGroups = sections.flatMap((section): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `section:${section.key}`, label: section.title },\n ...section.entries.map((entry): TopBarMenuEntry => ({ ...entry, key: `${section.key}:${entry.key}` })),\n ]);\n\n const entries = [...roleGroup, ...planGroup, ...appGroups];\n if (!entries.some(isRow)) return null;\n\n // The menu, then the role and the plan, as 0.42's two triggers named them: the bar\n // shows the flask only, so its name (and tooltip) is where they are read.\n const name = [\n words.menu,\n roleGroup.length > 0 && role !== undefined ? bar.role(role) : null,\n planGroup.length > 0 && plan !== undefined ? words.plan(plan) : null,\n ]\n .filter((part): part is string => part !== null)\n .join(\" · \");\n\n return (\n <TopBarActionMenu\n icon={flask}\n // The badge's words join the trigger's name, as the tours compass's NEW dot does.\n // `danger`, the badge's default: an amber dot would vanish into the amber flask.\n iconBadge={active ? { label: words.overridesOn } : null}\n aria-label={name}\n entries={entries}\n scrollable\n columns\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/dev-menu.tsx"],"sourcesContent":["import { useState, useSyncExternalStore } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { FlaskConical, Undo2 } from \"lucide-react\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type DevSwitchersLabels } from \"./top-bar-parts-labels\";\n// The kit's one storage guard, which 0.51 wrote here again by hand (DUP-8, refactor audit\n// 2026-10-30). Storage blocked, \"Back to\" lasts as long as this page.\nimport { readSessionStored, writeSessionStored } from \"../lib/safe-storage\";\n// The kit's one \"today\" (0.55): a day moved there is an override on, whatever `active` says.\nimport { subscribeToday, todayOverride } from \"../lib/dates\";\n\n/** Where the role the session started with is kept, unless the app names its own key. */\nexport const DEV_ORIGINAL_ROLE_KEY = \"ui-kit:dev-original-role\";\n\n/**\n * One of the app's own groups in the {@link DevMenu} (docs/top-bar-harmonization.md\n * §2.7.1): keksdose's budget role, its \"today\" override, its resets.\n *\n * The rows are plain {@link TopBarMenuEntry}s, and there is no dev item type beside them\n * (the contract with keksdose, 0.51): a choice of one is radio rows (`checked`), a toggle\n * is a `checkable: \"checkbox\"` row, an action is a plain row. No form fields — a menu is\n * a `role=\"menu\"`, where a text or date input has no place in the arrow keys; a free\n * value is a \"Custom…\" row that opens the app's own dialog.\n */\nexport interface DevMenuSection {\n /** Unique among the sections. The rows' keys need be unique only within theirs: the\n * menu prefixes them with this. */\n key: string;\n /** The group's heading, drawn above its rows and announced as the group's name. */\n title: ReactNode;\n /** The rows, as `TopBarActionMenu` takes them. A `divider` ends the group, as there. */\n entries: readonly TopBarMenuEntry[];\n}\n\nexport interface DevMenuProps<R extends string, P extends string> {\n /**\n * Draw anything at all. The app passes `import.meta.env.DEV` AND its probe of the dev\n * route (§2.7.1): the constant keeps the menu off a production build's screen, the\n * probe off a dev build whose server has the route shut (§6.1) — flasks with nothing\n * behind them. Neither takes code out of the bundle; see `sections`.\n */\n enabled: boolean;\n roles?: readonly R[];\n /** The role the server enforces now. Named `currentRole`, not `role`, because `role` on\n * a JSX element is the ARIA role (as `RoleChip`'s `value`). */\n currentRole?: R;\n /** Apply a role — the app calls server-kit's `POST /dev/switch {role}` and re-reads\n * `/auth/me` (§2.7, §6.2). */\n onRole?: (role: R) => void;\n plans?: readonly P[];\n /** The plan the server enforces now. Left out (not known yet — kastlan's overview is a\n * 404 while billing is off), the plan group still renders, with no row checked; a\n * `GET /dev/switch` answers it. */\n currentPlan?: P;\n /** Apply a plan — `POST /dev/switch {plan}`, then the overview (§6.3). */\n onPlan?: (plan: P) => void;\n /**\n * The role to offer \"Back to {role}\" for. Left out, the kit keeps it itself: the role\n * before the first switch of this browser session, in `sessionStorage` under\n * `storageKey` — the server answers with the NEW role after a switch, so only the\n * browser still knows the old one.\n */\n originalRole?: R;\n /** The `sessionStorage` key for the kept role. Default {@link DEV_ORIGINAL_ROLE_KEY};\n * an app with several accounts on one browser may scope it per user. */\n storageKey?: string;\n /** Billing is off locally: the plan group says plans change nothing, and stays usable\n * (§6.3). */\n billingOff?: boolean;\n /** A role's or a plan's words in the menu. Default: the value itself. */\n roleLabel?: (role: R) => ReactNode;\n planLabel?: (plan: P) => ReactNode;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { dev } }}>` > English. */\n labels?: LabelOverride<DevSwitchersLabels>;\n /** 0.51: the app's own dev groups, after the kit's role and plan, in this order. Their\n * DEFINITIONS belong in a module only a dev build imports (§2.7.1): `enabled={false}`\n * hides the menu, but the code of a section built inline stays in the bundle. */\n sections?: readonly DevMenuSection[];\n /**\n * 0.51: an override is on that the developer could forget — a \"today\" that is not\n * today, a simulated slow server. The flask wears a dot, and its name says so (\"Dev\n * tools · Role: ADMIN · Plan: free Overrides on\"), since a dot alone tells a screen\n * reader nothing. The app decides what counts; the kit cannot see into its sections.\n *\n * 0.55: a \"today\" the app moved with `setToday` (`@eifi1/ui-kit/dates`, the dev clock's\n * day) counts by itself — the dot is on while one is set, `active` or not.\n */\n active?: boolean;\n}\n\nconst flask = <FlaskConical className=\"size-5 text-warning\" />;\n\n/** A label's text for a sentence, or the raw value when the label is not text. */\nconst textOf = (label: ReactNode, value: string) =>\n typeof label === \"string\" || typeof label === \"number\" ? String(label) : value;\n\n/** A row someone can pick — what makes a menu worth drawing. */\nconst isRow = (entry: TopBarMenuEntry) => entry.kind !== \"heading\" && entry.kind !== \"divider\";\n\n/**\n * The top bar's local-dev menu (docs/top-bar-harmonization.md §2.7, §2.7.1, §6.2, §6.3):\n * ONE amber flask over the kit's Account section and the app's own.\n *\n * 0.42's `DevSwitchers` drew a flask per switcher and had no place for anything else,\n * and an app has more dev controls than a role and a plan — keksdose's budget role, a\n * \"today\" override, resets (agreed with keksdose for 0.51). So the kit's part is now the\n * menu, and the app's rows go in it.\n *\n * - **The Account section** comes first: the role group, ending in \"Back to {role}\" once\n * the role differs from the one the session started with (the dev route needs only a\n * signed-in user, so a switch to a lesser role can always switch back); then the plan\n * group, which with `billingOff` notes that plans change nothing. Each renders only\n * with its list and handler.\n * - **The app's `sections`** follow, each a group under its own heading.\n * - **`active`** puts a dot on the flask for an override that is easy to forget.\n *\n * Nothing renders unless `enabled`, and nothing with no row to show. The panel scrolls\n * (`TopBarActionMenu`'s `scrollable`): some 25 rows ran off a 390×844 phone.\n *\n * From `md` up the groups stand side by side, one column each, in that order (0.51.4,\n * `TopBarActionMenu`'s `columns`; keksdose dev #640, Marcel: \"Each group gets a column\").\n * \"Back to {role}\" stays under the roles. A desktop too narrow for all of them wraps\n * the columns onto a second row; a phone keeps the one scrolling column.\n */\nexport function DevMenu<R extends string, P extends string>({\n enabled,\n roles,\n currentRole: role,\n onRole,\n plans,\n currentPlan: plan,\n onPlan,\n originalRole,\n storageKey = DEV_ORIGINAL_ROLE_KEY,\n billingOff = false,\n roleLabel = (r) => r,\n planLabel = (p) => p,\n labels: labelsProp,\n sections = [],\n active = false,\n}: DevMenuProps<R, P>) {\n const bar = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n // Server render: no day is moved there.\n const movedToday = useSyncExternalStore(subscribeToday, todayOverride, () => null);\n const words = useTopBarPartsLabels(labelsProp ? { dev: labelsProp } : undefined).dev;\n const [kept, setKept] = useState<string | null>(() => (enabled ? readSessionStored(storageKey) : null));\n // 0.43 (keksdose): `enabled` may start false and turn true — an app's GET probe of the\n // dev route — so the kept role is read again then, or \"Back to {role}\" is lost after a\n // reload. Adjusted during render, React's pattern for state that follows a prop.\n const [wasEnabled, setWasEnabled] = useState(enabled);\n if (wasEnabled !== enabled) {\n setWasEnabled(enabled);\n if (enabled) setKept(readSessionStored(storageKey));\n }\n if (!enabled) return null;\n\n const original = (originalRole ?? kept ?? undefined) as R | undefined;\n\n const pickRole = (next: R) => {\n if (originalRole === undefined) {\n if (next === original) {\n // Back where the session began: nothing left to go back to.\n writeSessionStored(storageKey, null);\n setKept(null);\n } else if (kept === null && role !== undefined && next !== role) {\n writeSessionStored(storageKey, role);\n setKept(role);\n }\n }\n onRole?.(next);\n };\n\n const roleGroup: TopBarMenuEntry[] =\n roles && roles.length > 0 && role !== undefined && onRole\n ? [\n { kind: \"heading\", key: \"head-role\", label: words.roleHeading },\n ...roles.map((r): TopBarMenuEntry => ({\n key: `role-${r}`,\n label: roleLabel(r),\n checked: r === role,\n onSelect: () => pickRole(r),\n })),\n // Behind a rule, which ends the role group: \"Back to\" is a way out, not a role.\n ...(original !== undefined && original !== role\n ? ([\n { kind: \"divider\", key: \"sep-back\" },\n {\n key: \"back\",\n icon: <Undo2 className=\"size-4\" />,\n label: words.backTo(textOf(roleLabel(original), original)),\n onSelect: () => pickRole(original),\n },\n ] satisfies TopBarMenuEntry[])\n : []),\n ]\n : [];\n\n const planGroup: TopBarMenuEntry[] =\n plans && plans.length > 0 && onPlan\n ? [\n {\n kind: \"heading\",\n key: \"head-plan\",\n // The billing-off note is part of the group's NAME, under the heading. In 0.42's\n // own plan menu it was a trailing `<li>`: not a row, so the arrow keys passed\n // over it and a screen reader moving through the menu never reached it. Named,\n // it is read on the way into the plans. And in one menu a trailing note would\n // sit under the app's sections, not under the plans.\n label: billingOff ? (\n <>\n {words.planHeading}{\" \"}\n <span\n data-dev-billing-off=\"\"\n className=\"mt-0.5 block font-normal normal-case tracking-normal text-xs text-muted\"\n >\n {words.billingOff}\n </span>\n </>\n ) : (\n words.planHeading\n ),\n },\n ...plans.map((p): TopBarMenuEntry => ({\n key: `plan-${p}`,\n label: planLabel(p),\n checked: p === plan,\n onSelect: () => onPlan(p),\n })),\n ]\n : [];\n\n // The app's rows keyed under their section: two sections may well both have a \"reset\".\n const appGroups = sections.flatMap((section): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `section:${section.key}`, label: section.title },\n ...section.entries.map((entry): TopBarMenuEntry => ({ ...entry, key: `${section.key}:${entry.key}` })),\n ]);\n\n const entries = [...roleGroup, ...planGroup, ...appGroups];\n if (!entries.some(isRow)) return null;\n\n // The menu, then the role and the plan, as 0.42's two triggers named them: the bar\n // shows the flask only, so its name (and tooltip) is where they are read.\n const name = [\n words.menu,\n roleGroup.length > 0 && role !== undefined ? bar.role(role) : null,\n planGroup.length > 0 && plan !== undefined ? words.plan(plan) : null,\n ]\n .filter((part): part is string => part !== null)\n .join(\" · \");\n\n return (\n <TopBarActionMenu\n icon={flask}\n // The badge's words join the trigger's name, as the tours compass's NEW dot does.\n // `danger`, the badge's default: an amber dot would vanish into the amber flask.\n iconBadge={active || movedToday !== null ? { label: words.overridesOn } : null}\n aria-label={name}\n entries={entries}\n scrollable\n columns\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `HeldChangesBanner`'s words — the `heldChanges` namespace of `<UiKitProvider labels>`
|
|
3
|
+
* (0.55, docs/top-bar-harmonization.md §2.6.1). The confirm's Cancel is
|
|
4
|
+
* `confirmDialog.cancel`.
|
|
5
|
+
*/
|
|
6
|
+
export interface HeldChangesBannerLabels {
|
|
7
|
+
/** The banner's sentence when the app passes none: how many wait, that they stay on the
|
|
8
|
+
* device, and the two ways out. Says nothing of a way back, which the app may not have. */
|
|
9
|
+
message: (count: number) => string;
|
|
10
|
+
/** The way back — keksdose's switch to the budget the changes were made in. */
|
|
11
|
+
switchBack: string;
|
|
12
|
+
/** The button that asks first; the ellipsis says a question follows. */
|
|
13
|
+
discard: string;
|
|
14
|
+
/** The question, with the count that would be lost. */
|
|
15
|
+
confirmTitle: (count: number) => string;
|
|
16
|
+
/** The consequence under it: they are gone, and the device takes the server's state. */
|
|
17
|
+
confirmBody: (count: number) => string;
|
|
18
|
+
/** The button that answers it. */
|
|
19
|
+
confirmAction: string;
|
|
20
|
+
/** A switch back or a discard rejected and the app's `formatError` says nothing better. */
|
|
21
|
+
failed: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_HELD_CHANGES_BANNER_LABELS: HeldChangesBannerLabels;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const DEFAULT_HELD_CHANGES_BANNER_LABELS = {
|
|
3
|
+
message: (count) => count === 1 ? "1 change cannot be sent from here. It stays on this device until it can be sent, or until you discard it." : `${count} changes cannot be sent from here. They stay on this device until they can be sent, or until you discard them.`,
|
|
4
|
+
switchBack: "Switch back",
|
|
5
|
+
discard: "Discard\u2026",
|
|
6
|
+
confirmTitle: (count) => count === 1 ? "Discard 1 change that cannot be sent from here?" : `Discard ${count} changes that cannot be sent from here?`,
|
|
7
|
+
confirmBody: (count) => count === 1 ? "It cannot be recovered. This device then syncs with what the server has." : "They cannot be recovered. This device then syncs with what the server has.",
|
|
8
|
+
confirmAction: "Discard",
|
|
9
|
+
failed: "That didn\u2019t work. Please try again."
|
|
10
|
+
};
|
|
11
|
+
export {
|
|
12
|
+
DEFAULT_HELD_CHANGES_BANNER_LABELS
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=held-changes-banner-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/held-changes-banner-labels.ts"],"sourcesContent":["// The English words of ./held-changes-banner, in a module with no component code: the\n// English catalogue (src/i18n/defaults.ts) imports them without the component (0.34, KS U8).\n// keksdose's reviewed words (sync.held_*, sync.discard_held_*, sync.switch_back_held) are\n// the canon, without its budgets: the kit cannot know why a change waits, only that it does.\n\n/**\n * `HeldChangesBanner`'s words — the `heldChanges` namespace of `<UiKitProvider labels>`\n * (0.55, docs/top-bar-harmonization.md §2.6.1). The confirm's Cancel is\n * `confirmDialog.cancel`.\n */\nexport interface HeldChangesBannerLabels {\n /** The banner's sentence when the app passes none: how many wait, that they stay on the\n * device, and the two ways out. Says nothing of a way back, which the app may not have. */\n message: (count: number) => string;\n /** The way back — keksdose's switch to the budget the changes were made in. */\n switchBack: string;\n /** The button that asks first; the ellipsis says a question follows. */\n discard: string;\n /** The question, with the count that would be lost. */\n confirmTitle: (count: number) => string;\n /** The consequence under it: they are gone, and the device takes the server's state. */\n confirmBody: (count: number) => string;\n /** The button that answers it. */\n confirmAction: string;\n /** A switch back or a discard rejected and the app's `formatError` says nothing better. */\n failed: string;\n}\n\nexport const DEFAULT_HELD_CHANGES_BANNER_LABELS: HeldChangesBannerLabels = {\n message: (count) =>\n count === 1\n ? \"1 change cannot be sent from here. It stays on this device until it can be sent, or until you discard it.\"\n : `${count} changes cannot be sent from here. They stay on this device until they can be sent, or until you discard them.`,\n switchBack: \"Switch back\",\n discard: \"Discard…\",\n confirmTitle: (count) =>\n count === 1\n ? \"Discard 1 change that cannot be sent from here?\"\n : `Discard ${count} changes that cannot be sent from here?`,\n confirmBody: (count) =>\n count === 1\n ? \"It cannot be recovered. This device then syncs with what the server has.\"\n : \"They cannot be recovered. This device then syncs with what the server has.\",\n confirmAction: \"Discard\",\n failed: \"That didn’t work. Please try again.\",\n};\n"],"mappings":";AA4BO,MAAM,qCAA8D;AAAA,EACzE,SAAS,CAAC,UACR,UAAU,IACN,8GACA,GAAG,KAAK;AAAA,EACd,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,cAAc,CAAC,UACb,UAAU,IACN,oDACA,WAAW,KAAK;AAAA,EACtB,aAAa,CAAC,UACZ,UAAU,IACN,6EACA;AAAA,EACN,eAAe;AAAA,EACf,QAAQ;AACV;","names":[]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
3
|
+
import { DEFAULT_HELD_CHANGES_BANNER_LABELS, type HeldChangesBannerLabels } from "./held-changes-banner-labels.js";
|
|
4
|
+
export { DEFAULT_HELD_CHANGES_BANNER_LABELS };
|
|
5
|
+
export type { HeldChangesBannerLabels };
|
|
6
|
+
export interface HeldChangesBannerProps {
|
|
7
|
+
/**
|
|
8
|
+
* Changes kept on this device that cannot be sent from where the user is now — the
|
|
9
|
+
* indicator's `blocked`, or the part of it this banner is about (kastlan: the ones the
|
|
10
|
+
* server refused). 0 renders nothing.
|
|
11
|
+
*/
|
|
12
|
+
count: number;
|
|
13
|
+
/**
|
|
14
|
+
* The app's sentence: why they wait and what would send them — keksdose's "2 changes
|
|
15
|
+
* were made in “Holiday”, which is no longer the active budget…". Default
|
|
16
|
+
* `heldChanges.message(count)`, which names no reason and no way back.
|
|
17
|
+
*/
|
|
18
|
+
message?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Room under the sentence — kastlan's refusal reasons, as a list. For the box: a strip
|
|
21
|
+
* holds its message to one line on a phone, and a list behind "More" is a list nobody
|
|
22
|
+
* opens, so a strip with `extra` is not clamped.
|
|
23
|
+
*/
|
|
24
|
+
extra?: ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* The way back, where there is one — keksdose's switch to the budget the changes were
|
|
27
|
+
* made in, which sends them. Without it Discard is the one way out. A returned promise
|
|
28
|
+
* keeps the button busy until it settles; a rejection is shown as Discard's is.
|
|
29
|
+
*/
|
|
30
|
+
onSwitchBack?: () => Promise<unknown> | void;
|
|
31
|
+
/**
|
|
32
|
+
* Lets them go, once the user has said yes to the kit's confirm: the app drops what is
|
|
33
|
+
* held and syncs (keksdose `discardWaitingChanges`, kastlan `discardRejected` then
|
|
34
|
+
* `onSync`). Busy until the promise settles. A rejection leaves the banner where it is,
|
|
35
|
+
* with the error under the sentence; the count going to 0 is what removes it.
|
|
36
|
+
*/
|
|
37
|
+
onDiscard: () => Promise<unknown> | void;
|
|
38
|
+
/** Turns a rejection into words. Default `heldChanges.failed`. */
|
|
39
|
+
formatError?: (error: unknown) => ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* `box` (default): a framed callout above a page (kastlan's handover pages); on a phone
|
|
42
|
+
* its actions take a line of their own under the sentence. `strip`: a band in the
|
|
43
|
+
* shell beside the app's other strips (keksdose), its message held to one line on a
|
|
44
|
+
* phone with "More" (`clampMobile`, keksdose dev#611), so the actions keep theirs.
|
|
45
|
+
*/
|
|
46
|
+
variant?: "box" | "strip";
|
|
47
|
+
/** Prop > `<UiKitProvider labels={{ heldChanges }}>` > English. */
|
|
48
|
+
labels?: LabelOverride<HeldChangesBannerLabels>;
|
|
49
|
+
className?: string;
|
|
50
|
+
/** `data-*` attributes go on the banner — a tour's anchor (`data-tour="…"`). */
|
|
51
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Changes this device keeps and cannot send from where the user is now, with the two ways
|
|
55
|
+
* out: back to where they can be sent, or let them go (docs/top-bar-harmonization.md
|
|
56
|
+
* §2.6.1). `SyncStatusIndicator` shows the state (`blocked`, ranked first); this is the
|
|
57
|
+
* room for the action that one control in the top bar does not have.
|
|
58
|
+
*
|
|
59
|
+
* Two apps built it by hand:
|
|
60
|
+
* - **keksdose budget switch** (app/held-changes-banner.tsx): the active budget moved
|
|
61
|
+
* and the queue waits for the budget it was made in. A strip in the shell with "Switch
|
|
62
|
+
* back" and "Discard…", which asks first with the count, then discards and syncs.
|
|
63
|
+
* - **kastlan handover-sync-banner** (:57-68): the changes the server refused, a danger
|
|
64
|
+
* box with their reasons and a Discard that asked nothing.
|
|
65
|
+
*
|
|
66
|
+
* The kit's part is the banner and the question; what is held, how to discard it and the
|
|
67
|
+
* sync after are the app's (`onDiscard`). Warning, not danger: nothing is lost until the
|
|
68
|
+
* user says so, and the indicator in the bar already draws the danger. The safe action
|
|
69
|
+
* comes first, so the destructive one is never the only button somebody in a hurry finds
|
|
70
|
+
* (keksdose); Discard is then the quiet one. The confirm is `useConfirm`'s, in the danger
|
|
71
|
+
* tone (focus starts on Cancel), so the app needs a `ConfirmProvider` — all three mount
|
|
72
|
+
* one. Not behind the write lock: the changes are not the page's, and a viewer of what is
|
|
73
|
+
* on screen may still let go of what is held elsewhere (keksdose).
|
|
74
|
+
*/
|
|
75
|
+
export declare function HeldChangesBanner(props: HeldChangesBannerProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { AlertBanner } from "../components/alert-banner.js";
|
|
5
|
+
import { Button } from "../components/button.js";
|
|
6
|
+
import { useConfirm } from "../components/confirm-dialog.js";
|
|
7
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
8
|
+
import { hasContent } from "../lib/presence.js";
|
|
9
|
+
import { DEFAULT_HELD_CHANGES_BANNER_LABELS } from "./held-changes-banner-labels.js";
|
|
10
|
+
function HeldChangesBanner(props) {
|
|
11
|
+
if (!(props.count > 0)) return null;
|
|
12
|
+
return /* @__PURE__ */ jsx(HeldChangesBody, { ...props });
|
|
13
|
+
}
|
|
14
|
+
function HeldChangesBody({
|
|
15
|
+
count,
|
|
16
|
+
message,
|
|
17
|
+
extra,
|
|
18
|
+
onSwitchBack,
|
|
19
|
+
onDiscard,
|
|
20
|
+
formatError,
|
|
21
|
+
variant = "box",
|
|
22
|
+
labels: labelsProp,
|
|
23
|
+
className,
|
|
24
|
+
...rest
|
|
25
|
+
}) {
|
|
26
|
+
const labels = useKitLabels("heldChanges", DEFAULT_HELD_CHANGES_BANNER_LABELS, labelsProp);
|
|
27
|
+
const confirm = useConfirm();
|
|
28
|
+
const [busy, setBusy] = useState(null);
|
|
29
|
+
const [failure, setFailure] = useState(null);
|
|
30
|
+
const mounted = useRef(true);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
mounted.current = true;
|
|
33
|
+
return () => {
|
|
34
|
+
mounted.current = false;
|
|
35
|
+
};
|
|
36
|
+
}, []);
|
|
37
|
+
const run = async (key, action) => {
|
|
38
|
+
setBusy(key);
|
|
39
|
+
setFailure(null);
|
|
40
|
+
try {
|
|
41
|
+
await action();
|
|
42
|
+
} catch (caught) {
|
|
43
|
+
if (!mounted.current) return;
|
|
44
|
+
const said = formatError?.(caught);
|
|
45
|
+
setFailure(hasContent(said) ? said : labels.failed);
|
|
46
|
+
} finally {
|
|
47
|
+
if (mounted.current) setBusy(null);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const discard = async () => {
|
|
51
|
+
const yes = await confirm({
|
|
52
|
+
title: labels.confirmTitle(count),
|
|
53
|
+
body: labels.confirmBody(count),
|
|
54
|
+
confirmLabel: labels.confirmAction,
|
|
55
|
+
tone: "danger"
|
|
56
|
+
});
|
|
57
|
+
if (yes && mounted.current) await run("discard", onDiscard);
|
|
58
|
+
};
|
|
59
|
+
const strip = variant === "strip";
|
|
60
|
+
return /* @__PURE__ */ jsxs(
|
|
61
|
+
AlertBanner,
|
|
62
|
+
{
|
|
63
|
+
...rest,
|
|
64
|
+
tone: "warning",
|
|
65
|
+
variant: strip ? "strip" : "box",
|
|
66
|
+
actionPlacement: "own-line",
|
|
67
|
+
clampMobile: strip && !failure && !hasContent(extra),
|
|
68
|
+
className,
|
|
69
|
+
action: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
70
|
+
onSwitchBack && /* @__PURE__ */ jsx(
|
|
71
|
+
Button,
|
|
72
|
+
{
|
|
73
|
+
variant: "secondary",
|
|
74
|
+
size: "sm",
|
|
75
|
+
pending: busy === "switchBack",
|
|
76
|
+
disabled: busy === "discard",
|
|
77
|
+
onClick: () => void run("switchBack", onSwitchBack),
|
|
78
|
+
children: labels.switchBack
|
|
79
|
+
}
|
|
80
|
+
),
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
Button,
|
|
83
|
+
{
|
|
84
|
+
variant: onSwitchBack ? "ghost" : "secondary",
|
|
85
|
+
tone: "danger",
|
|
86
|
+
size: "sm",
|
|
87
|
+
pending: busy === "discard",
|
|
88
|
+
disabled: busy === "switchBack",
|
|
89
|
+
onClick: () => void discard(),
|
|
90
|
+
children: labels.discard
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
] }),
|
|
94
|
+
children: [
|
|
95
|
+
message ?? labels.message(count),
|
|
96
|
+
hasContent(extra) && /* @__PURE__ */ jsx("div", { className: "mt-1", children: extra }),
|
|
97
|
+
hasContent(failure) && /* @__PURE__ */ jsx(AlertBanner, { variant: "inline", tone: "danger", size: "sm", block: true, className: "mt-1", children: failure })
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
DEFAULT_HELD_CHANGES_BANNER_LABELS,
|
|
104
|
+
HeldChangesBanner
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=held-changes-banner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/held-changes-banner.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { useConfirm } from \"../components/confirm-dialog\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { hasContent } from \"../lib/presence\";\nimport { DEFAULT_HELD_CHANGES_BANNER_LABELS, type HeldChangesBannerLabels } from \"./held-changes-banner-labels\";\n\n// Defined in ./held-changes-banner-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_HELD_CHANGES_BANNER_LABELS };\nexport type { HeldChangesBannerLabels };\n\nexport interface HeldChangesBannerProps {\n /**\n * Changes kept on this device that cannot be sent from where the user is now — the\n * indicator's `blocked`, or the part of it this banner is about (kastlan: the ones the\n * server refused). 0 renders nothing.\n */\n count: number;\n /**\n * The app's sentence: why they wait and what would send them — keksdose's \"2 changes\n * were made in “Holiday”, which is no longer the active budget…\". Default\n * `heldChanges.message(count)`, which names no reason and no way back.\n */\n message?: ReactNode;\n /**\n * Room under the sentence — kastlan's refusal reasons, as a list. For the box: a strip\n * holds its message to one line on a phone, and a list behind \"More\" is a list nobody\n * opens, so a strip with `extra` is not clamped.\n */\n extra?: ReactNode;\n /**\n * The way back, where there is one — keksdose's switch to the budget the changes were\n * made in, which sends them. Without it Discard is the one way out. A returned promise\n * keeps the button busy until it settles; a rejection is shown as Discard's is.\n */\n onSwitchBack?: () => Promise<unknown> | void;\n /**\n * Lets them go, once the user has said yes to the kit's confirm: the app drops what is\n * held and syncs (keksdose `discardWaitingChanges`, kastlan `discardRejected` then\n * `onSync`). Busy until the promise settles. A rejection leaves the banner where it is,\n * with the error under the sentence; the count going to 0 is what removes it.\n */\n onDiscard: () => Promise<unknown> | void;\n /** Turns a rejection into words. Default `heldChanges.failed`. */\n formatError?: (error: unknown) => ReactNode;\n /**\n * `box` (default): a framed callout above a page (kastlan's handover pages); on a phone\n * its actions take a line of their own under the sentence. `strip`: a band in the\n * shell beside the app's other strips (keksdose), its message held to one line on a\n * phone with \"More\" (`clampMobile`, keksdose dev#611), so the actions keep theirs.\n */\n variant?: \"box\" | \"strip\";\n /** Prop > `<UiKitProvider labels={{ heldChanges }}>` > English. */\n labels?: LabelOverride<HeldChangesBannerLabels>;\n className?: string;\n /** `data-*` attributes go on the banner — a tour's anchor (`data-tour=\"…\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/**\n * Changes this device keeps and cannot send from where the user is now, with the two ways\n * out: back to where they can be sent, or let them go (docs/top-bar-harmonization.md\n * §2.6.1). `SyncStatusIndicator` shows the state (`blocked`, ranked first); this is the\n * room for the action that one control in the top bar does not have.\n *\n * Two apps built it by hand:\n * - **keksdose budget switch** (app/held-changes-banner.tsx): the active budget moved\n * and the queue waits for the budget it was made in. A strip in the shell with \"Switch\n * back\" and \"Discard…\", which asks first with the count, then discards and syncs.\n * - **kastlan handover-sync-banner** (:57-68): the changes the server refused, a danger\n * box with their reasons and a Discard that asked nothing.\n *\n * The kit's part is the banner and the question; what is held, how to discard it and the\n * sync after are the app's (`onDiscard`). Warning, not danger: nothing is lost until the\n * user says so, and the indicator in the bar already draws the danger. The safe action\n * comes first, so the destructive one is never the only button somebody in a hurry finds\n * (keksdose); Discard is then the quiet one. The confirm is `useConfirm`'s, in the danger\n * tone (focus starts on Cancel), so the app needs a `ConfirmProvider` — all three mount\n * one. Not behind the write lock: the changes are not the page's, and a viewer of what is\n * on screen may still let go of what is held elsewhere (keksdose).\n */\nexport function HeldChangesBanner(props: HeldChangesBannerProps) {\n // Nothing held: nothing to say. The body's state (a running action, an error) goes with\n // it, so a later hold starts clean.\n if (!(props.count > 0)) return null;\n return <HeldChangesBody {...props} />;\n}\n\nfunction HeldChangesBody({\n count,\n message,\n extra,\n onSwitchBack,\n onDiscard,\n formatError,\n variant = \"box\",\n labels: labelsProp,\n className,\n ...rest\n}: HeldChangesBannerProps) {\n const labels = useKitLabels(\"heldChanges\", DEFAULT_HELD_CHANGES_BANNER_LABELS, labelsProp);\n const confirm = useConfirm();\n // One action at a time: a switch back racing a discard would send what is being dropped.\n const [busy, setBusy] = useState<\"switchBack\" | \"discard\" | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n // A discard that worked usually takes the banner with it (the count reaches 0) before\n // its promise settles here.\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const run = async (key: \"switchBack\" | \"discard\", action: () => Promise<unknown> | void) => {\n setBusy(key);\n setFailure(null);\n try {\n await action();\n } catch (caught) {\n if (!mounted.current) return;\n const said = formatError?.(caught);\n setFailure(hasContent(said) ? said : labels.failed);\n } finally {\n if (mounted.current) setBusy(null);\n }\n };\n\n const discard = async () => {\n const yes = await confirm({\n title: labels.confirmTitle(count),\n body: labels.confirmBody(count),\n confirmLabel: labels.confirmAction,\n tone: \"danger\",\n });\n if (yes && mounted.current) await run(\"discard\", onDiscard);\n };\n\n const strip = variant === \"strip\";\n return (\n <AlertBanner\n {...rest}\n tone=\"warning\"\n variant={strip ? \"strip\" : \"box\"}\n // A long sentence beside two buttons squeezed into a column on a phone (kastlan 53):\n // the actions get a line of their own under it. AlertBanner's default, said here.\n actionPlacement=\"own-line\"\n // Not while an error shows or `extra` is there: the clamp would hide them behind \"More\".\n clampMobile={strip && !failure && !hasContent(extra)}\n className={className}\n action={\n <div className=\"flex flex-wrap items-center gap-2\">\n {onSwitchBack && (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n pending={busy === \"switchBack\"}\n disabled={busy === \"discard\"}\n onClick={() => void run(\"switchBack\", onSwitchBack)}\n >\n {labels.switchBack}\n </Button>\n )}\n <Button\n variant={onSwitchBack ? \"ghost\" : \"secondary\"}\n tone=\"danger\"\n size=\"sm\"\n pending={busy === \"discard\"}\n disabled={busy === \"switchBack\"}\n onClick={() => void discard()}\n >\n {labels.discard}\n </Button>\n </div>\n }\n >\n {message ?? labels.message(count)}\n {hasContent(extra) && <div className=\"mt-1\">{extra}</div>}\n {/* Inline, so it is a live region: the answer to the button just pressed is heard. */}\n {hasContent(failure) && (\n <AlertBanner variant=\"inline\" tone=\"danger\" size=\"sm\" block className=\"mt-1\">\n {failure}\n </AlertBanner>\n )}\n </AlertBanner>\n );\n}\n"],"mappings":";AAuFS,cAmED,YAnEC;AAvFT,SAAS,WAAW,QAAQ,gBAAgB;AAE5C,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,oBAAwC;AACjD,SAAS,kBAAkB;AAC3B,SAAS,0CAAwE;AA4E1E,SAAS,kBAAkB,OAA+B;AAG/D,MAAI,EAAE,MAAM,QAAQ,GAAI,QAAO;AAC/B,SAAO,oBAAC,mBAAiB,GAAG,OAAO;AACrC;AAEA,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAA2B;AACzB,QAAM,SAAS,aAAa,eAAe,oCAAoC,UAAU;AACzF,QAAM,UAAU,WAAW;AAE3B,QAAM,CAAC,MAAM,OAAO,IAAI,SAA0C,IAAI;AACtE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAoB,IAAI;AAGtD,QAAM,UAAU,OAAO,IAAI;AAC3B,YAAU,MAAM;AACd,YAAQ,UAAU;AAClB,WAAO,MAAM;AACX,cAAQ,UAAU;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,MAAM,OAAO,KAA+B,WAA0C;AAC1F,YAAQ,GAAG;AACX,eAAW,IAAI;AACf,QAAI;AACF,YAAM,OAAO;AAAA,IACf,SAAS,QAAQ;AACf,UAAI,CAAC,QAAQ,QAAS;AACtB,YAAM,OAAO,cAAc,MAAM;AACjC,iBAAW,WAAW,IAAI,IAAI,OAAO,OAAO,MAAM;AAAA,IACpD,UAAE;AACA,UAAI,QAAQ,QAAS,SAAQ,IAAI;AAAA,IACnC;AAAA,EACF;AAEA,QAAM,UAAU,YAAY;AAC1B,UAAM,MAAM,MAAM,QAAQ;AAAA,MACxB,OAAO,OAAO,aAAa,KAAK;AAAA,MAChC,MAAM,OAAO,YAAY,KAAK;AAAA,MAC9B,cAAc,OAAO;AAAA,MACrB,MAAM;AAAA,IACR,CAAC;AACD,QAAI,OAAO,QAAQ,QAAS,OAAM,IAAI,WAAW,SAAS;AAAA,EAC5D;AAEA,QAAM,QAAQ,YAAY;AAC1B,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,SAAS,QAAQ,UAAU;AAAA,MAG3B,iBAAgB;AAAA,MAEhB,aAAa,SAAS,CAAC,WAAW,CAAC,WAAW,KAAK;AAAA,MACnD;AAAA,MACA,QACE,qBAAC,SAAI,WAAU,qCACZ;AAAA,wBACC;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,SAAS,SAAS;AAAA,YAClB,UAAU,SAAS;AAAA,YACnB,SAAS,MAAM,KAAK,IAAI,cAAc,YAAY;AAAA,YAEjD,iBAAO;AAAA;AAAA,QACV;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,SAAS,eAAe,UAAU;AAAA,YAClC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,SAAS,SAAS;AAAA,YAClB,UAAU,SAAS;AAAA,YACnB,SAAS,MAAM,KAAK,QAAQ;AAAA,YAE3B,iBAAO;AAAA;AAAA,QACV;AAAA,SACF;AAAA,MAGD;AAAA,mBAAW,OAAO,QAAQ,KAAK;AAAA,QAC/B,WAAW,KAAK,KAAK,oBAAC,SAAI,WAAU,QAAQ,iBAAM;AAAA,QAElD,WAAW,OAAO,KACjB,oBAAC,eAAY,SAAQ,UAAS,MAAK,UAAS,MAAK,MAAK,OAAK,MAAC,WAAU,QACnE,mBACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
|
|
@@ -19,15 +19,27 @@ export interface SyncStatus {
|
|
|
19
19
|
waitingForPlan?: number;
|
|
20
20
|
/** When the last good round ended — epoch ms, an ISO string or a `Date`. */
|
|
21
21
|
lastSyncedAt?: number | string | Date | null;
|
|
22
|
+
/**
|
|
23
|
+
* 0.55 (held back from 0.53): a round of this tab waits for another tab's on the same
|
|
24
|
+
* device copy — the sync engine's Web Lock (0.52) lets one tab's round run at a time — so
|
|
25
|
+
* nothing goes out from here until that one ends. `useSyncStatus` fills it from the
|
|
26
|
+
* engine's `waitingForTab`.
|
|
27
|
+
*/
|
|
28
|
+
waitingForTab?: boolean;
|
|
22
29
|
}
|
|
23
|
-
/** The
|
|
24
|
-
export type SyncState = "blocked" | "waitingForPlan" | "offline" | "error" | "syncing" | "pending" | "synced";
|
|
30
|
+
/** The eight states, in their ranking: the first that holds wins. */
|
|
31
|
+
export type SyncState = "blocked" | "waitingForPlan" | "offline" | "waitingForTab" | "error" | "syncing" | "pending" | "synced";
|
|
25
32
|
/**
|
|
26
|
-
* Which of the
|
|
33
|
+
* Which of the eight states a {@link SyncStatus} is in — keksdose's ranking
|
|
27
34
|
* (sync-status-indicator.tsx): a write that cannot be sent at all ranks above everything,
|
|
28
35
|
* because waiting cannot help it; then one waiting for a plan; then offline; then a refused
|
|
29
36
|
* round; then a round under way; then writes waiting; else in sync. `syncing` also holds
|
|
30
37
|
* while a tap's own round runs (`refreshing`), before the engine publishes its phase.
|
|
38
|
+
*
|
|
39
|
+
* 0.55 (held back from 0.53): a round waiting for another tab's (`waitingForTab`) ranks
|
|
40
|
+
* after offline, which it cannot help either, and before the rest: it is the start of a
|
|
41
|
+
* round, which replaces an error as `syncing` does once it runs, and the engine never says
|
|
42
|
+
* it beside `syncing` — so it is what a tap's own round shows while it waits its turn.
|
|
31
43
|
*/
|
|
32
44
|
export declare function syncState(status: SyncStatus, refreshing?: boolean): SyncState;
|
|
33
45
|
/** The scope words of an indicator that covers only part of the app (§6.10). */
|
|
@@ -46,7 +58,8 @@ export interface SyncStatusIndicatorProps {
|
|
|
46
58
|
* Run a round now (push, then pull, then re-read what is on screen). With it the
|
|
47
59
|
* indicator is a "sync now" button, disabled offline and mid-round but keeping its
|
|
48
60
|
* colour; a returned promise keeps the spin until it settles. Without it, a status only
|
|
49
|
-
* (§6.9).
|
|
61
|
+
* (§6.9). Disabled too while a round waits for another tab's (0.55): a tap would only
|
|
62
|
+
* queue one more behind it.
|
|
50
63
|
*/
|
|
51
64
|
onSync?: () => void | Promise<unknown>;
|
|
52
65
|
/** Render nothing while in sync — kastlan and Kurvenschmiede show it only while offline,
|
|
@@ -59,11 +72,12 @@ export interface SyncStatusIndicatorProps {
|
|
|
59
72
|
* offline-first app (keksdose, kastlan's handovers) — offline means "kept here, sent
|
|
60
73
|
* later". `"none"`: an online-only app that keeps nothing on the device (Kurvenschmiede,
|
|
61
74
|
* build review B1) — offline reads "changes can't be saved until you reconnect", a
|
|
62
|
-
* round reads "Saving…", and `pending`, `blocked` and `
|
|
63
|
-
* since there is nowhere for a change to wait. "Up to date" when settled.
|
|
75
|
+
* round reads "Saving…", and `pending`, `blocked`, `waitingForPlan` and `waitingForTab`
|
|
76
|
+
* are ignored, since there is nowhere for a change to wait. "Up to date" when settled.
|
|
64
77
|
*/
|
|
65
78
|
storage?: "device" | "none";
|
|
66
|
-
/** Short words beside the cloud ("Offline", "Syncing…", "3 waiting"
|
|
79
|
+
/** Short words beside the cloud ("Offline", "Syncing…", "3 waiting", "Waiting for tab")
|
|
80
|
+
* while not in sync. */
|
|
67
81
|
showLabel?: boolean;
|
|
68
82
|
/** Prop > `<UiKitProvider labels={{ topBarParts: { sync } }}>` > English. */
|
|
69
83
|
labels?: LabelOverride<SyncStatusLabels>;
|
|
@@ -75,12 +89,13 @@ export interface SyncStatusIndicatorProps {
|
|
|
75
89
|
}
|
|
76
90
|
/**
|
|
77
91
|
* The top bar's sync status (docs/top-bar-harmonization.md §2.6), lifted from keksdose's
|
|
78
|
-
* `SyncStatusIndicator`:
|
|
79
|
-
*
|
|
80
|
-
*
|
|
92
|
+
* `SyncStatusIndicator`: its seven states and their ranking ({@link syncState}; 0.55 adds an
|
|
93
|
+
* eighth, "Waiting for another tab", an hourglass), one cloud per state, a ✓ badge when in
|
|
94
|
+
* sync and a spin while syncing. With `onSync` it is also the "sync now" button, and its
|
|
95
|
+
* name says both what it is and what a tap does.
|
|
81
96
|
*
|
|
82
97
|
* Colours are the kit's roles — danger for a write that cannot be sent, warning for
|
|
83
|
-
* anything waiting, info while syncing
|
|
84
|
-
* rose/amber/sky/emerald pairs.
|
|
98
|
+
* anything waiting, info while syncing (or waiting for another tab's round), success when
|
|
99
|
+
* in sync — where keksdose had raw rose/amber/sky/emerald pairs.
|
|
85
100
|
*/
|
|
86
101
|
export declare function SyncStatusIndicator({ status, onSync, hideWhenIdle, scope, showLabel, labels: labelsProp, locale: localeProp, className, storage, ...rest }: SyncStatusIndicatorProps): import("react").JSX.Element | null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { Check, Cloud, CloudAlert, CloudOff, RefreshCw, UploadCloud } from "lucide-react";
|
|
4
|
+
import { Check, Cloud, CloudAlert, CloudOff, Hourglass, RefreshCw, UploadCloud } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { IconButton } from "../components/icon-button.js";
|
|
7
7
|
import { Tooltip } from "../components/tooltip.js";
|
|
@@ -12,6 +12,7 @@ function syncState(status, refreshing = false) {
|
|
|
12
12
|
if ((status.blocked ?? 0) > 0) return "blocked";
|
|
13
13
|
if ((status.waitingForPlan ?? 0) > 0) return "waitingForPlan";
|
|
14
14
|
if (!status.online || status.phase === "offline") return "offline";
|
|
15
|
+
if (status.waitingForTab) return "waitingForTab";
|
|
15
16
|
if (status.phase === "error") return "error";
|
|
16
17
|
if (status.phase === "syncing" || refreshing) return "syncing";
|
|
17
18
|
if (status.pending > 0) return "pending";
|
|
@@ -23,6 +24,8 @@ const LOOK = {
|
|
|
23
24
|
// Safe, and a payment sends them: amber, the kit's warning.
|
|
24
25
|
waitingForPlan: { Icon: CloudAlert, tone: "warning" },
|
|
25
26
|
offline: { Icon: CloudOff, tone: "warning" },
|
|
27
|
+
// A round under way, in another tab: info, as syncing, without the spin.
|
|
28
|
+
waitingForTab: { Icon: Hourglass, tone: "info" },
|
|
26
29
|
// Usually resolves by itself (a deploy, a read-only view): amber, not danger.
|
|
27
30
|
error: { Icon: CloudAlert, tone: "warning" },
|
|
28
31
|
syncing: { Icon: RefreshCw, tone: "info" },
|
|
@@ -62,7 +65,10 @@ function SyncStatusIndicator({
|
|
|
62
65
|
};
|
|
63
66
|
}, []);
|
|
64
67
|
const onlineOnly = storage === "none";
|
|
65
|
-
const state = syncState(
|
|
68
|
+
const state = syncState(
|
|
69
|
+
onlineOnly ? { ...status, pending: 0, blocked: 0, waitingForPlan: 0, waitingForTab: false } : status,
|
|
70
|
+
refreshing
|
|
71
|
+
);
|
|
66
72
|
const data = dataAttributes(rest);
|
|
67
73
|
if (hideWhenIdle && state === "synced") return null;
|
|
68
74
|
const { Icon, tone } = LOOK[state];
|
|
@@ -78,6 +84,9 @@ function SyncStatusIndicator({
|
|
|
78
84
|
case "offline":
|
|
79
85
|
text = onlineOnly ? words.offlineUnsaved : words.offline;
|
|
80
86
|
break;
|
|
87
|
+
case "waitingForTab":
|
|
88
|
+
text = words.waitingForTab;
|
|
89
|
+
break;
|
|
81
90
|
case "error":
|
|
82
91
|
text = words.refused;
|
|
83
92
|
break;
|
|
@@ -96,11 +105,11 @@ function SyncStatusIndicator({
|
|
|
96
105
|
if (scope) {
|
|
97
106
|
text = state === "offline" && scope.offline ? scope.offline : words.scoped(scope.name, text);
|
|
98
107
|
}
|
|
99
|
-
const short = state === "offline" ? words.offlineShort : state === "syncing" ? onlineOnly ? words.saving : words.syncingShort : state === "error" ? words.refusedShort : state === "pending" || state === "blocked" || state === "waitingForPlan" ? words.waitingShort(
|
|
108
|
+
const short = state === "offline" ? words.offlineShort : state === "syncing" ? onlineOnly ? words.saving : words.syncingShort : state === "error" ? words.refusedShort : state === "waitingForTab" ? words.waitingForTabShort : state === "pending" || state === "blocked" || state === "waitingForPlan" ? words.waitingShort(
|
|
100
109
|
state === "pending" ? status.pending : state === "blocked" ? status.blocked ?? 0 : status.waitingForPlan ?? 0
|
|
101
110
|
) : null;
|
|
102
111
|
const offline = state === "offline";
|
|
103
|
-
const canSync = !!onSync && status.online && status.phase !== "offline" && !spin;
|
|
112
|
+
const canSync = !!onSync && status.online && status.phase !== "offline" && !spin && state !== "waitingForTab";
|
|
104
113
|
const name = onSync ? canSync ? words.withHint(text, words.syncHint) : offline ? words.withHint(text, words.offlineHint) : text : text;
|
|
105
114
|
const badge = state === "synced" ? /* @__PURE__ */ jsx(Check, { className: "stroke-[3]" }) : void 0;
|
|
106
115
|
const glyph = /* @__PURE__ */ jsx(Icon, { "aria-hidden": true, className: spin ? "animate-spin" : void 0 });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/sync-status-indicator.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ComponentType } from \"react\";\nimport { Check, Cloud, CloudAlert, CloudOff, RefreshCw, UploadCloud } from \"lucide-react\";\nimport type { LucideProps } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { IconButton } from \"../components/icon-button\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { useBillingLabels } from \"../billing/billing-labels\";\nimport { useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTopBarPartsLabels, type SyncStatusLabels } from \"./top-bar-parts-labels\";\n\n/**\n * What the indicator shows, fed by the app's sync engine (docs/top-bar-harmonization.md\n * §2.6). Small on purpose: keksdose's sync-status store, kastlan's handover controller and\n * Kurvenschmiede's online state + `useIsMutating` all fill it.\n */\nexport interface SyncStatus {\n /** What the engine is doing. `offline` is the engine's own verdict; `online: false` is\n * the browser's — either shows the offline state. */\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n /** The browser's `navigator.onLine` (or the app's better guess). */\n online: boolean;\n /** Writes kept on this device and not yet sent. */\n pending: number;\n /** Writes this device cannot send at all (keksdose: no key for the budget). Ranked first. */\n blocked?: number;\n /** Writes the server refused as read-only and keeps until a plan (billing §12.5). */\n waitingForPlan?: number;\n /** When the last good round ended — epoch ms, an ISO string or a `Date`. */\n lastSyncedAt?: number | string | Date | null;\n}\n\n/** The seven states, in their ranking: the first that holds wins. */\nexport type SyncState = \"blocked\" | \"waitingForPlan\" | \"offline\" | \"error\" | \"syncing\" | \"pending\" | \"synced\";\n\n/**\n * Which of the seven states a {@link SyncStatus} is in — keksdose's ranking\n * (sync-status-indicator.tsx): a write that cannot be sent at all ranks above everything,\n * because waiting cannot help it; then one waiting for a plan; then offline; then a refused\n * round; then a round under way; then writes waiting; else in sync. `syncing` also holds\n * while a tap's own round runs (`refreshing`), before the engine publishes its phase.\n */\nexport function syncState(status: SyncStatus, refreshing = false): SyncState {\n if ((status.blocked ?? 0) > 0) return \"blocked\";\n if ((status.waitingForPlan ?? 0) > 0) return \"waitingForPlan\";\n if (!status.online || status.phase === \"offline\") return \"offline\";\n if (status.phase === \"error\") return \"error\";\n if (status.phase === \"syncing\" || refreshing) return \"syncing\";\n if (status.pending > 0) return \"pending\";\n return \"synced\";\n}\n\ntype Tone = \"danger\" | \"warning\" | \"info\" | \"success\";\n\nconst LOOK: Record<SyncState, { Icon: ComponentType<LucideProps>; tone: Tone }> = {\n // The one the user must act on: the kit's danger, where keksdose had rose.\n blocked: { Icon: CloudAlert, tone: \"danger\" },\n // Safe, and a payment sends them: amber, the kit's warning.\n waitingForPlan: { Icon: CloudAlert, tone: \"warning\" },\n offline: { Icon: CloudOff, tone: \"warning\" },\n // Usually resolves by itself (a deploy, a read-only view): amber, not danger.\n error: { Icon: CloudAlert, tone: \"warning\" },\n syncing: { Icon: RefreshCw, tone: \"info\" },\n pending: { Icon: UploadCloud, tone: \"warning\" },\n synced: { Icon: Cloud, tone: \"success\" },\n};\n\nconst TEXT_TONE: Record<Tone, string> = {\n danger: \"text-danger\",\n warning: \"text-warning\",\n info: \"text-info\",\n success: \"text-success\",\n};\n\n/** The scope words of an indicator that covers only part of the app (§6.10). */\nexport interface SyncStatusScope {\n /** Put before every state in the tooltip: \"Handovers on this device\" →\n * \"Handovers on this device: 3 changes waiting to sync\". */\n name: string;\n /** The whole offline sentence instead of the kit's, when offline means something\n * narrower: \"Offline: handovers keep working on this device; everything else needs a\n * connection\". */\n offline?: string;\n}\n\nexport interface SyncStatusIndicatorProps {\n status: SyncStatus;\n /**\n * Run a round now (push, then pull, then re-read what is on screen). With it the\n * indicator is a \"sync now\" button, disabled offline and mid-round but keeping its\n * colour; a returned promise keeps the spin until it settles. Without it, a status only\n * (§6.9).\n */\n onSync?: () => void | Promise<unknown>;\n /** Render nothing while in sync — kastlan and Kurvenschmiede show it only while offline,\n * syncing or with changes waiting, held or refused (§2.6). */\n hideWhenIdle?: boolean;\n /** The app's words for its part of the app (§6.10). */\n scope?: SyncStatusScope;\n /**\n * Where the app keeps a change before the server has it. `\"device\"` (the default): an\n * offline-first app (keksdose, kastlan's handovers) — offline means \"kept here, sent\n * later\". `\"none\"`: an online-only app that keeps nothing on the device (Kurvenschmiede,\n * build review B1) — offline reads \"changes can't be saved until you reconnect\", a\n * round reads \"Saving…\", and `pending`, `blocked` and `waitingForPlan` are ignored,\n * since there is nowhere for a change to wait. \"Up to date\" when settled.\n */\n storage?: \"device\" | \"none\";\n /** Short words beside the cloud (\"Offline\", \"Syncing…\", \"3 waiting\") while not in sync. */\n showLabel?: boolean;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { sync } }}>` > English. */\n labels?: LabelOverride<SyncStatusLabels>;\n /** The clock's language for \"Synced at 14:05\". Default: the provider's locale. */\n locale?: string;\n className?: string;\n /** `data-*` attributes go on the trigger — a tour's anchor (`data-tour=\"sync-status\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** The `data-*` attributes of a rest object, for a trigger. */\nfunction dataAttributes(rest: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith(\"data-\")));\n}\n\n/**\n * The top bar's sync status (docs/top-bar-harmonization.md §2.6), lifted from keksdose's\n * `SyncStatusIndicator`: the seven states and their ranking ({@link syncState}), one cloud\n * per state, a ✓ badge when in sync and a spin while syncing. With `onSync` it is also the\n * \"sync now\" button, and its name says both what it is and what a tap does.\n *\n * Colours are the kit's roles — danger for a write that cannot be sent, warning for\n * anything waiting, info while syncing, success when in sync — where keksdose had raw\n * rose/amber/sky/emerald pairs.\n */\nexport function SyncStatusIndicator({\n status,\n onSync,\n hideWhenIdle = false,\n scope,\n showLabel = false,\n labels: labelsProp,\n locale: localeProp,\n className,\n storage = \"device\",\n ...rest\n}: SyncStatusIndicatorProps) {\n const words = useTopBarPartsLabels(labelsProp ? { sync: labelsProp } : undefined).sync;\n const billing = useBillingLabels();\n const locale = useKitLocale(localeProp);\n // The tap's own round, local rather than in the app's store: the store's phase belongs\n // to the sync engine, and a second writer would race the round itself.\n const [refreshing, setRefreshing] = useState(false);\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const onlineOnly = storage === \"none\";\n // Online-only: nothing waits on the device, so only offline, error, saving and settled.\n const state = syncState(onlineOnly ? { ...status, pending: 0, blocked: 0, waitingForPlan: 0 } : status, refreshing);\n const data = dataAttributes(rest);\n if (hideWhenIdle && state === \"synced\") return null;\n\n const { Icon, tone } = LOOK[state];\n const spin = state === \"syncing\";\n let text: string;\n switch (state) {\n case \"blocked\":\n text = words.blocked(status.blocked ?? 0);\n break;\n case \"waitingForPlan\":\n text = billing.waitingChanges(status.waitingForPlan ?? 0);\n break;\n case \"offline\":\n text = onlineOnly ? words.offlineUnsaved : words.offline;\n break;\n case \"error\":\n text = words.refused;\n break;\n case \"syncing\":\n text = onlineOnly ? words.saving : words.syncing;\n break;\n case \"pending\":\n text = words.pending(status.pending);\n break;\n default:\n text =\n !onlineOnly && status.lastSyncedAt\n ? words.syncedAt(\n // A clock time in the UI's language, never the browser's (keksdose live #246).\n new Date(status.lastSyncedAt).toLocaleTimeString(locale, { hour: \"2-digit\", minute: \"2-digit\" }),\n )\n : words.idle;\n }\n if (scope) {\n text = state === \"offline\" && scope.offline ? scope.offline : words.scoped(scope.name, text);\n }\n\n const short =\n state === \"offline\"\n ? words.offlineShort\n : state === \"syncing\"\n ? onlineOnly\n ? words.saving\n : words.syncingShort\n : state === \"error\"\n ? words.refusedShort\n : state === \"pending\" || state === \"blocked\" || state === \"waitingForPlan\"\n ? words.waitingShort(\n state === \"pending\"\n ? status.pending\n : state === \"blocked\"\n ? (status.blocked ?? 0)\n : (status.waitingForPlan ?? 0),\n )\n : null;\n\n const offline = state === \"offline\";\n const canSync = !!onSync && status.online && status.phase !== \"offline\" && !spin;\n const name = onSync\n ? canSync\n ? words.withHint(text, words.syncHint)\n : offline\n ? words.withHint(text, words.offlineHint)\n : text\n : text;\n const badge = state === \"synced\" ? <Check className=\"stroke-[3]\" /> : undefined;\n const glyph = <Icon aria-hidden className={spin ? \"animate-spin\" : undefined} />;\n\n const sync = async () => {\n if (!canSync || !onSync) return;\n setRefreshing(true);\n try {\n await onSync();\n } finally {\n if (mounted.current) setRefreshing(false);\n }\n };\n\n const control = onSync ? (\n <IconButton\n {...data}\n data-sync-state={state}\n size=\"2xs\"\n glyphSize={20}\n shape=\"round\"\n // A status that is also an action: disabled (offline, mid-round) is exactly when\n // the state matters most, so it keeps its colour.\n disabledStyle=\"keep\"\n tone={tone}\n quiet={false}\n label={name}\n labelVisible={false}\n // Below: the bar is at the top edge, where a bubble above has no room.\n tooltipSide=\"bottom\"\n onClick={() => void sync()}\n disabled={!canSync}\n // It announces itself as a status: a reader hears the state change.\n aria-live=\"polite\"\n badge={badge}\n >\n {glyph}\n </IconButton>\n ) : (\n <Tooltip label={name} side=\"bottom\">\n <span\n {...data}\n role=\"status\"\n data-sync-state={state}\n className={cn(\n \"relative inline-flex size-9 shrink-0 items-center justify-center [&>svg]:size-5\",\n TEXT_TONE[tone],\n )}\n >\n {glyph}\n <span className=\"sr-only\">{name}</span>\n {badge && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute bottom-1.5 end-1.5 inline-flex rounded-full bg-surface [&_svg]:size-3\"\n >\n {badge}\n </span>\n )}\n </span>\n </Tooltip>\n );\n\n if (!showLabel || !short)\n return className ? <span className={cn(\"inline-flex\", className)}>{control}</span> : control;\n return (\n <span className={cn(\"inline-flex items-center gap-1\", className)}>\n {control}\n {/* The eye's short words; the name above already says it in full. */}\n <span aria-hidden className={cn(\"whitespace-nowrap text-xs font-medium\", TEXT_TONE[tone])}>\n {short}\n </span>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/sync-status-indicator.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ComponentType } from \"react\";\nimport { Check, Cloud, CloudAlert, CloudOff, Hourglass, RefreshCw, UploadCloud } from \"lucide-react\";\nimport type { LucideProps } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { IconButton } from \"../components/icon-button\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { useBillingLabels } from \"../billing/billing-labels\";\nimport { useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTopBarPartsLabels, type SyncStatusLabels } from \"./top-bar-parts-labels\";\n\n/**\n * What the indicator shows, fed by the app's sync engine (docs/top-bar-harmonization.md\n * §2.6). Small on purpose: keksdose's sync-status store, kastlan's handover controller and\n * Kurvenschmiede's online state + `useIsMutating` all fill it.\n */\nexport interface SyncStatus {\n /** What the engine is doing. `offline` is the engine's own verdict; `online: false` is\n * the browser's — either shows the offline state. */\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n /** The browser's `navigator.onLine` (or the app's better guess). */\n online: boolean;\n /** Writes kept on this device and not yet sent. */\n pending: number;\n /** Writes this device cannot send at all (keksdose: no key for the budget). Ranked first. */\n blocked?: number;\n /** Writes the server refused as read-only and keeps until a plan (billing §12.5). */\n waitingForPlan?: number;\n /** When the last good round ended — epoch ms, an ISO string or a `Date`. */\n lastSyncedAt?: number | string | Date | null;\n /**\n * 0.55 (held back from 0.53): a round of this tab waits for another tab's on the same\n * device copy — the sync engine's Web Lock (0.52) lets one tab's round run at a time — so\n * nothing goes out from here until that one ends. `useSyncStatus` fills it from the\n * engine's `waitingForTab`.\n */\n waitingForTab?: boolean;\n}\n\n/** The eight states, in their ranking: the first that holds wins. */\nexport type SyncState =\n \"blocked\" | \"waitingForPlan\" | \"offline\" | \"waitingForTab\" | \"error\" | \"syncing\" | \"pending\" | \"synced\";\n\n/**\n * Which of the eight states a {@link SyncStatus} is in — keksdose's ranking\n * (sync-status-indicator.tsx): a write that cannot be sent at all ranks above everything,\n * because waiting cannot help it; then one waiting for a plan; then offline; then a refused\n * round; then a round under way; then writes waiting; else in sync. `syncing` also holds\n * while a tap's own round runs (`refreshing`), before the engine publishes its phase.\n *\n * 0.55 (held back from 0.53): a round waiting for another tab's (`waitingForTab`) ranks\n * after offline, which it cannot help either, and before the rest: it is the start of a\n * round, which replaces an error as `syncing` does once it runs, and the engine never says\n * it beside `syncing` — so it is what a tap's own round shows while it waits its turn.\n */\nexport function syncState(status: SyncStatus, refreshing = false): SyncState {\n if ((status.blocked ?? 0) > 0) return \"blocked\";\n if ((status.waitingForPlan ?? 0) > 0) return \"waitingForPlan\";\n if (!status.online || status.phase === \"offline\") return \"offline\";\n if (status.waitingForTab) return \"waitingForTab\";\n if (status.phase === \"error\") return \"error\";\n if (status.phase === \"syncing\" || refreshing) return \"syncing\";\n if (status.pending > 0) return \"pending\";\n return \"synced\";\n}\n\ntype Tone = \"danger\" | \"warning\" | \"info\" | \"success\";\n\nconst LOOK: Record<SyncState, { Icon: ComponentType<LucideProps>; tone: Tone }> = {\n // The one the user must act on: the kit's danger, where keksdose had rose.\n blocked: { Icon: CloudAlert, tone: \"danger\" },\n // Safe, and a payment sends them: amber, the kit's warning.\n waitingForPlan: { Icon: CloudAlert, tone: \"warning\" },\n offline: { Icon: CloudOff, tone: \"warning\" },\n // A round under way, in another tab: info, as syncing, without the spin.\n waitingForTab: { Icon: Hourglass, tone: \"info\" },\n // Usually resolves by itself (a deploy, a read-only view): amber, not danger.\n error: { Icon: CloudAlert, tone: \"warning\" },\n syncing: { Icon: RefreshCw, tone: \"info\" },\n pending: { Icon: UploadCloud, tone: \"warning\" },\n synced: { Icon: Cloud, tone: \"success\" },\n};\n\nconst TEXT_TONE: Record<Tone, string> = {\n danger: \"text-danger\",\n warning: \"text-warning\",\n info: \"text-info\",\n success: \"text-success\",\n};\n\n/** The scope words of an indicator that covers only part of the app (§6.10). */\nexport interface SyncStatusScope {\n /** Put before every state in the tooltip: \"Handovers on this device\" →\n * \"Handovers on this device: 3 changes waiting to sync\". */\n name: string;\n /** The whole offline sentence instead of the kit's, when offline means something\n * narrower: \"Offline: handovers keep working on this device; everything else needs a\n * connection\". */\n offline?: string;\n}\n\nexport interface SyncStatusIndicatorProps {\n status: SyncStatus;\n /**\n * Run a round now (push, then pull, then re-read what is on screen). With it the\n * indicator is a \"sync now\" button, disabled offline and mid-round but keeping its\n * colour; a returned promise keeps the spin until it settles. Without it, a status only\n * (§6.9). Disabled too while a round waits for another tab's (0.55): a tap would only\n * queue one more behind it.\n */\n onSync?: () => void | Promise<unknown>;\n /** Render nothing while in sync — kastlan and Kurvenschmiede show it only while offline,\n * syncing or with changes waiting, held or refused (§2.6). */\n hideWhenIdle?: boolean;\n /** The app's words for its part of the app (§6.10). */\n scope?: SyncStatusScope;\n /**\n * Where the app keeps a change before the server has it. `\"device\"` (the default): an\n * offline-first app (keksdose, kastlan's handovers) — offline means \"kept here, sent\n * later\". `\"none\"`: an online-only app that keeps nothing on the device (Kurvenschmiede,\n * build review B1) — offline reads \"changes can't be saved until you reconnect\", a\n * round reads \"Saving…\", and `pending`, `blocked`, `waitingForPlan` and `waitingForTab`\n * are ignored, since there is nowhere for a change to wait. \"Up to date\" when settled.\n */\n storage?: \"device\" | \"none\";\n /** Short words beside the cloud (\"Offline\", \"Syncing…\", \"3 waiting\", \"Waiting for tab\")\n * while not in sync. */\n showLabel?: boolean;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { sync } }}>` > English. */\n labels?: LabelOverride<SyncStatusLabels>;\n /** The clock's language for \"Synced at 14:05\". Default: the provider's locale. */\n locale?: string;\n className?: string;\n /** `data-*` attributes go on the trigger — a tour's anchor (`data-tour=\"sync-status\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** The `data-*` attributes of a rest object, for a trigger. */\nfunction dataAttributes(rest: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith(\"data-\")));\n}\n\n/**\n * The top bar's sync status (docs/top-bar-harmonization.md §2.6), lifted from keksdose's\n * `SyncStatusIndicator`: its seven states and their ranking ({@link syncState}; 0.55 adds an\n * eighth, \"Waiting for another tab\", an hourglass), one cloud per state, a ✓ badge when in\n * sync and a spin while syncing. With `onSync` it is also the \"sync now\" button, and its\n * name says both what it is and what a tap does.\n *\n * Colours are the kit's roles — danger for a write that cannot be sent, warning for\n * anything waiting, info while syncing (or waiting for another tab's round), success when\n * in sync — where keksdose had raw rose/amber/sky/emerald pairs.\n */\nexport function SyncStatusIndicator({\n status,\n onSync,\n hideWhenIdle = false,\n scope,\n showLabel = false,\n labels: labelsProp,\n locale: localeProp,\n className,\n storage = \"device\",\n ...rest\n}: SyncStatusIndicatorProps) {\n const words = useTopBarPartsLabels(labelsProp ? { sync: labelsProp } : undefined).sync;\n const billing = useBillingLabels();\n const locale = useKitLocale(localeProp);\n // The tap's own round, local rather than in the app's store: the store's phase belongs\n // to the sync engine, and a second writer would race the round itself.\n const [refreshing, setRefreshing] = useState(false);\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const onlineOnly = storage === \"none\";\n // Online-only: nothing waits on the device, so only offline, error, saving and settled.\n const state = syncState(\n onlineOnly ? { ...status, pending: 0, blocked: 0, waitingForPlan: 0, waitingForTab: false } : status,\n refreshing,\n );\n const data = dataAttributes(rest);\n if (hideWhenIdle && state === \"synced\") return null;\n\n const { Icon, tone } = LOOK[state];\n const spin = state === \"syncing\";\n let text: string;\n switch (state) {\n case \"blocked\":\n text = words.blocked(status.blocked ?? 0);\n break;\n case \"waitingForPlan\":\n text = billing.waitingChanges(status.waitingForPlan ?? 0);\n break;\n case \"offline\":\n text = onlineOnly ? words.offlineUnsaved : words.offline;\n break;\n case \"waitingForTab\":\n text = words.waitingForTab;\n break;\n case \"error\":\n text = words.refused;\n break;\n case \"syncing\":\n text = onlineOnly ? words.saving : words.syncing;\n break;\n case \"pending\":\n text = words.pending(status.pending);\n break;\n default:\n text =\n !onlineOnly && status.lastSyncedAt\n ? words.syncedAt(\n // A clock time in the UI's language, never the browser's (keksdose live #246).\n new Date(status.lastSyncedAt).toLocaleTimeString(locale, { hour: \"2-digit\", minute: \"2-digit\" }),\n )\n : words.idle;\n }\n if (scope) {\n text = state === \"offline\" && scope.offline ? scope.offline : words.scoped(scope.name, text);\n }\n\n const short =\n state === \"offline\"\n ? words.offlineShort\n : state === \"syncing\"\n ? onlineOnly\n ? words.saving\n : words.syncingShort\n : state === \"error\"\n ? words.refusedShort\n : state === \"waitingForTab\"\n ? words.waitingForTabShort\n : state === \"pending\" || state === \"blocked\" || state === \"waitingForPlan\"\n ? words.waitingShort(\n state === \"pending\"\n ? status.pending\n : state === \"blocked\"\n ? (status.blocked ?? 0)\n : (status.waitingForPlan ?? 0),\n )\n : null;\n\n const offline = state === \"offline\";\n // Not while a round waits for another tab's either: a tap would queue one more behind it.\n const canSync = !!onSync && status.online && status.phase !== \"offline\" && !spin && state !== \"waitingForTab\";\n const name = onSync\n ? canSync\n ? words.withHint(text, words.syncHint)\n : offline\n ? words.withHint(text, words.offlineHint)\n : text\n : text;\n const badge = state === \"synced\" ? <Check className=\"stroke-[3]\" /> : undefined;\n const glyph = <Icon aria-hidden className={spin ? \"animate-spin\" : undefined} />;\n\n const sync = async () => {\n if (!canSync || !onSync) return;\n setRefreshing(true);\n try {\n await onSync();\n } finally {\n if (mounted.current) setRefreshing(false);\n }\n };\n\n const control = onSync ? (\n <IconButton\n {...data}\n data-sync-state={state}\n size=\"2xs\"\n glyphSize={20}\n shape=\"round\"\n // A status that is also an action: disabled (offline, mid-round) is exactly when\n // the state matters most, so it keeps its colour.\n disabledStyle=\"keep\"\n tone={tone}\n quiet={false}\n label={name}\n labelVisible={false}\n // Below: the bar is at the top edge, where a bubble above has no room.\n tooltipSide=\"bottom\"\n onClick={() => void sync()}\n disabled={!canSync}\n // It announces itself as a status: a reader hears the state change.\n aria-live=\"polite\"\n badge={badge}\n >\n {glyph}\n </IconButton>\n ) : (\n <Tooltip label={name} side=\"bottom\">\n <span\n {...data}\n role=\"status\"\n data-sync-state={state}\n className={cn(\n \"relative inline-flex size-9 shrink-0 items-center justify-center [&>svg]:size-5\",\n TEXT_TONE[tone],\n )}\n >\n {glyph}\n <span className=\"sr-only\">{name}</span>\n {badge && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute bottom-1.5 end-1.5 inline-flex rounded-full bg-surface [&_svg]:size-3\"\n >\n {badge}\n </span>\n )}\n </span>\n </Tooltip>\n );\n\n if (!showLabel || !short)\n return className ? <span className={cn(\"inline-flex\", className)}>{control}</span> : control;\n return (\n <span className={cn(\"inline-flex items-center gap-1\", className)}>\n {control}\n {/* The eye's short words; the name above already says it in full. */}\n <span aria-hidden className={cn(\"whitespace-nowrap text-xs font-medium\", TEXT_TONE[tone])}>\n {short}\n </span>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -65,6 +65,11 @@ export interface SyncStatusLabels {
|
|
|
65
65
|
/** A round under way in an online-only app — it saves, it doesn't sync. Also its short
|
|
66
66
|
* words beside the cloud. */
|
|
67
67
|
saving: string;
|
|
68
|
+
/** 0.55 (held back from 0.53): a round waits for another tab's on the same device copy
|
|
69
|
+
* (the sync engine's Web Lock lets one tab sync at a time). */
|
|
70
|
+
waitingForTab: string;
|
|
71
|
+
/** Its short words beside the cloud. */
|
|
72
|
+
waitingForTabShort: string;
|
|
68
73
|
}
|
|
69
74
|
/**
|
|
70
75
|
* `DevMenu`'s words (§2.7, §2.7.1, §6.2, §6.3). Still named for 0.42's `DevSwitchers`: the
|
|
@@ -30,7 +30,9 @@ const DEFAULT_TOP_BAR_PARTS_LABELS = {
|
|
|
30
30
|
waitingShort: (count) => `${count} waiting`,
|
|
31
31
|
refusedShort: "Not synced",
|
|
32
32
|
offlineUnsaved: "Offline \u2014 changes can't be saved until you reconnect",
|
|
33
|
-
saving: "Saving\u2026"
|
|
33
|
+
saving: "Saving\u2026",
|
|
34
|
+
waitingForTab: "Waiting for another tab to finish syncing",
|
|
35
|
+
waitingForTabShort: "Waiting for tab"
|
|
34
36
|
},
|
|
35
37
|
dev: {
|
|
36
38
|
menu: "Dev tools",
|