@eifi1/ui-kit 0.51.3 → 0.51.5
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 +6 -6
- package/dist/account/account-settings-labels.js.map +1 -1
- package/dist/account/account-settings.js +11 -9
- package/dist/account/account-settings.js.map +1 -1
- package/dist/account/delete-account-setting.js +56 -65
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.js +4 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.js +19 -11
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/admin/admin-tool-links.d.ts +1 -1
- package/dist/admin/admin-tool-links.js +7 -1
- package/dist/admin/admin-tool-links.js.map +1 -1
- package/dist/admin/invitations-panel.js +2 -13
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/mail-test-card.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +4 -11
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/user-roster.js +1 -9
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.js +23 -31
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.js +13 -5
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.js +1 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/checkout-processing.js.map +1 -1
- package/dist/billing/plan-card.js +91 -85
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-limit-toast.js.map +1 -1
- package/dist/billing/plan-limit.js.map +1 -1
- package/dist/billing/plan-price.js.map +1 -1
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/bin/eifi1-pay-page.mjs +6 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.js +1 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.js +29 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +1 -4
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.js +1 -7
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.js +5 -5
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +4 -10
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/card.js +1 -8
- package/dist/components/card.js.map +1 -1
- package/dist/components/chart-zoom-core.js +3 -17
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +4 -22
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/chip-input.js +4 -1
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/chip.js +10 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.js +58 -74
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper-labels.js.map +1 -1
- package/dist/components/column-mapper.js +167 -227
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +5 -9
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +1 -4
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.d.ts +5 -4
- package/dist/components/country-select.js +3 -19
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.js +13 -11
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.js +1 -6
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.js.map +1 -1
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.js +2 -18
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js +7 -5
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.js +3 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.js +49 -57
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.js +31 -44
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-row-actions.js +19 -16
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.js +4 -1
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +8 -0
- package/dist/components/data-table.js +30 -39
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +1 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/description-list.js +10 -10
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +9 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +17 -27
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +25 -35
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-base.js +5 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +1 -1
- package/dist/components/field-parts.js +10 -14
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.js +8 -10
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +4 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.js.map +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/grouped-picker.js +11 -12
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.js +1 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input-labels.js.map +1 -1
- package/dist/components/icon-button.js +4 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/icon-picker.js +6 -10
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/image-grid.js +41 -50
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +6 -12
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +14 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +1 -7
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-labels.js +8 -2
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.d.ts +18 -1
- package/dist/components/legal-page.js +27 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +40 -5
- package/dist/components/legal.js +41 -5
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +26 -36
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list-labels.js +3 -1
- package/dist/components/list-labels.js.map +1 -1
- package/dist/components/list.d.ts +18 -1
- package/dist/components/list.js +54 -13
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.js +29 -53
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +13 -31
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +10 -21
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +3 -16
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/nav-pills.js +10 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.js +1 -7
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +1 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +1 -7
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +13 -22
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.js +1 -11
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/password-strength-labels.js.map +1 -1
- package/dist/components/password-strength.js +2 -8
- package/dist/components/password-strength.js.map +1 -1
- package/dist/components/phone-input.js +42 -57
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +1 -8
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +16 -9
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.js +1 -8
- package/dist/components/qr-code.js.map +1 -1
- package/dist/components/row-actions.js +5 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/select.js +28 -38
- package/dist/components/select.js.map +1 -1
- package/dist/components/separator.js +1 -5
- package/dist/components/separator.js.map +1 -1
- package/dist/components/series-chart-axes.js +4 -1
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.js +29 -45
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.js +21 -17
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.js +4 -9
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.js +4 -13
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/signed-amount.js +12 -5
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/skeleton.js +2 -17
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +13 -36
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -10
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.js +14 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +1 -1
- package/dist/components/swipeable-row.js +1 -8
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table-skeleton.js +4 -1
- package/dist/components/table-skeleton.js.map +1 -1
- package/dist/components/table.js +12 -9
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.js +5 -24
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +1 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +6 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +2 -11
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.js +2 -22
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -4
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -1
- package/dist/components/translation-review-parts.js +7 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/translation-review.js +18 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +14 -10
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.js +1 -13
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/use-table-state.js +14 -11
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.js +3 -26
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/demo/demo-start.js +8 -4
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-labels.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +1 -8
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-capture.d.ts +1 -1
- package/dist/feedback/feedback-capture.js +3 -1
- package/dist/feedback/feedback-capture.js.map +1 -1
- package/dist/feedback/feedback-comment-thread.js +27 -13
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-context.js +1 -4
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-public-list.js +7 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-record.js +1 -5
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.js +25 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +3 -1
- package/dist/feedback/feedback-table.js +22 -5
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +2 -13
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback/feedback-vote-button.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-document-title.js.map +1 -1
- package/dist/hooks/use-file-drop.js.map +1 -1
- package/dist/hooks/use-file-picker-labels.js.map +1 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +10 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.d.ts +1 -1
- package/dist/hooks/use-row-swipe.js +1 -6
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.js +1 -4
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +15 -4
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.js +2 -8
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.js.map +1 -1
- package/dist/i18n/locales/en.js +19 -4
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +15 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +15 -4
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +15 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +15 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +11 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.js +15 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +17 -17
- package/dist/index.js +40 -23
- package/dist/index.js.map +1 -1
- package/dist/landing/access.js +1 -4
- package/dist/landing/access.js.map +1 -1
- package/dist/landing/landing-sections.js +35 -38
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/device-settings.js +2 -1
- package/dist/lib/device-settings.js.map +1 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/iban.js.map +1 -1
- package/dist/lib/logger.js +1 -5
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/person-name.js.map +1 -1
- package/dist/lib/phone.js.map +1 -1
- package/dist/lib/qr-encode.js +344 -8
- package/dist/lib/qr-encode.js.map +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/table-text.js +15 -3
- package/dist/lib/table-text.js.map +1 -1
- package/dist/lib/text-match.d.ts +20 -0
- package/dist/lib/text-match.js +11 -1
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/notification-inbox.js +63 -44
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +6 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/use-push-device.js +4 -1
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/pwa-update-prompt.d.ts +1 -1
- package/dist/offline/pwa-update-prompt.js +7 -1
- package/dist/offline/pwa-update-prompt.js.map +1 -1
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +6 -1
- package/dist/offline.js.map +1 -1
- package/dist/pay/pay.css +8 -1
- package/dist/rhf/fields.d.ts +1 -2
- package/dist/rhf/fields.js +1 -10
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.js +1 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/search/command-palette.js +71 -62
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +34 -5
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/settings/device-reset-setting.js +1 -6
- package/dist/settings/device-reset-setting.js.map +1 -1
- package/dist/settings/settings-heading.js.map +1 -1
- package/dist/settings/settings-layout.js +9 -20
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/use-settings-focus.d.ts +7 -0
- package/dist/settings/use-settings-focus.js +2 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.js +62 -31
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -10
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +5 -0
- package/dist/shell/dev-menu.js +14 -7
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/sync-status-indicator.js +7 -6
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +14 -1
- package/dist/shell/topbar-action-menu.js +85 -34
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +1 -10
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.js +9 -1
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +2 -1
- package/dist/shell.js.map +1 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.js +24 -6
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/react.js.map +1 -1
- package/dist/testing/viewport.js +7 -4
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/contrast-tokens.js +13 -2
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/palette-derive.d.ts +1 -1
- package/dist/theme/palette-derive.js +18 -29
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +1 -3
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.js +5 -1
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-engine.js +1 -4
- package/dist/tour/tour-engine.js.map +1 -1
- package/dist/tour/tour.js +21 -12
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.js +3 -24
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +24 -28
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +1 -1
- package/dist/wizard/wizard-step.js +1 -8
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +5 -2
- package/src/account/account-settings-labels.ts +1 -2
- package/src/account/account-settings.tsx +11 -17
- package/src/account/delete-account-setting.tsx +1 -6
- package/src/account/email-change-setting-labels.ts +1 -2
- package/src/account/email-change-setting.tsx +3 -1
- package/src/account/passkeys-setting.tsx +1 -3
- package/src/account/sessions-setting.tsx +4 -2
- package/src/admin/admin-action-confirm.tsx +4 -7
- package/src/admin/admin-action-log.tsx +29 -29
- package/src/admin/admin-area-labels.ts +4 -1
- package/src/admin/admin-overview.tsx +8 -2
- package/src/admin/admin-sections.tsx +3 -1
- package/src/admin/admin-tool-links.tsx +7 -1
- package/src/admin/invitations-panel.tsx +5 -11
- package/src/admin/mail-test-card.tsx +1 -2
- package/src/admin/reviewer-scope-editor.tsx +4 -9
- package/src/admin/role-select.tsx +4 -2
- package/src/admin/user-roster.tsx +2 -11
- package/src/auth/accept-invitation.tsx +1 -7
- package/src/auth/auth-errors.ts +1 -6
- package/src/auth/forgot-password-form.tsx +2 -7
- package/src/auth/register-form.tsx +7 -8
- package/src/auth/reset-password-form.tsx +2 -5
- package/src/auth/sign-in-form.tsx +15 -14
- package/src/auth/verify-email.tsx +2 -7
- package/src/billing/billing-banner.tsx +3 -7
- package/src/billing/billing-labels.ts +6 -4
- package/src/billing/checkout-processing.ts +0 -1
- package/src/billing/plan-card.tsx +9 -8
- package/src/billing/plan-limit-toast.tsx +1 -2
- package/src/billing/plan-limit.ts +1 -2
- package/src/billing/plan-price.ts +1 -4
- package/src/billing/plan-switch.tsx +1 -2
- package/src/billing/subscription-actions.tsx +5 -2
- package/src/components/account-chips.tsx +1 -7
- package/src/components/alert-banner.tsx +2 -8
- package/src/components/amount-input.tsx +206 -178
- package/src/components/authed-image.tsx +7 -4
- package/src/components/autocomplete.tsx +7 -9
- package/src/components/bulk-action-bar.tsx +2 -7
- package/src/components/button-group.tsx +1 -2
- package/src/components/button-look.ts +6 -17
- package/src/components/button.tsx +1 -4
- package/src/components/calculator.tsx +6 -9
- package/src/components/calendar-heatmap.tsx +5 -10
- package/src/components/card.tsx +9 -13
- package/src/components/chart-zoom-core.tsx +6 -33
- package/src/components/chart.tsx +8 -31
- package/src/components/checkbox-group.tsx +1 -4
- package/src/components/chip-input.tsx +6 -6
- package/src/components/chip.tsx +14 -26
- package/src/components/choice-card.tsx +4 -18
- package/src/components/column-mapper-labels.ts +3 -10
- package/src/components/column-mapper.tsx +33 -122
- package/src/components/combobox-core.tsx +17 -47
- package/src/components/combobox.tsx +2 -6
- package/src/components/confirm-dialog.tsx +1 -3
- package/src/components/copy-button.tsx +2 -4
- package/src/components/country-select.tsx +9 -21
- package/src/components/currency-option-list.tsx +16 -18
- package/src/components/currency-select.tsx +2 -7
- package/src/components/danger-confirm.tsx +8 -9
- package/src/components/data-table-cells.tsx +1 -2
- package/src/components/data-table-column-settings.tsx +1 -3
- package/src/components/data-table-filter-popover.tsx +5 -19
- package/src/components/data-table-filters.ts +1 -6
- package/src/components/data-table-fit.ts +7 -7
- package/src/components/data-table-groups.tsx +3 -1
- package/src/components/data-table-head.tsx +32 -46
- package/src/components/data-table-mobile.tsx +99 -108
- package/src/components/data-table-pagination.tsx +2 -6
- package/src/components/data-table-row-actions.tsx +25 -24
- package/src/components/data-table-row.tsx +16 -17
- package/src/components/data-table-shared.tsx +10 -16
- package/src/components/data-table-sort.ts +1 -2
- package/src/components/data-table-virtual.ts +9 -4
- package/src/components/data-table.tsx +497 -501
- package/src/components/date-picker.tsx +1 -7
- package/src/components/date-range-picker.tsx +3 -13
- package/src/components/description-list.tsx +7 -13
- package/src/components/dialog-frame.tsx +12 -3
- package/src/components/disclosure.tsx +8 -12
- package/src/components/dropdown.tsx +3 -11
- package/src/components/empty-state.tsx +1 -3
- package/src/components/entity-combobox.tsx +2 -6
- package/src/components/error-boundary.tsx +7 -7
- package/src/components/field-base.tsx +8 -14
- package/src/components/field-parts.tsx +12 -23
- package/src/components/field-sync.tsx +7 -11
- package/src/components/file-button.tsx +7 -3
- package/src/components/file-dropzone.tsx +7 -12
- package/src/components/floating-panel.tsx +44 -51
- package/src/components/focus-ring.ts +1 -2
- package/src/components/form-actions.tsx +1 -4
- package/src/components/grouped-picker.tsx +21 -16
- package/src/components/hover-menu.tsx +1 -4
- package/src/components/iban-input-labels.ts +1 -2
- package/src/components/icon-button.tsx +46 -48
- package/src/components/icon-picker.tsx +9 -20
- package/src/components/image-grid.tsx +1 -6
- package/src/components/inline-edit-field.tsx +1 -5
- package/src/components/inline-entity-combobox.tsx +11 -25
- package/src/components/input.tsx +14 -2
- package/src/components/jump-calendar.tsx +2 -6
- package/src/components/legal-labels.ts +8 -3
- package/src/components/legal-page.tsx +56 -6
- package/src/components/legal.tsx +83 -10
- package/src/components/lightbox.tsx +1 -3
- package/src/components/line-items.tsx +7 -15
- package/src/components/list-labels.ts +2 -0
- package/src/components/list.tsx +81 -27
- package/src/components/measured-grid.tsx +8 -30
- package/src/components/menu-item.tsx +1 -3
- package/src/components/mini-calendar.tsx +129 -159
- package/src/components/money-field.tsx +4 -2
- package/src/components/month-picker.tsx +14 -21
- package/src/components/multi-entity-combobox.tsx +6 -26
- package/src/components/multi-select.tsx +5 -9
- package/src/components/nav-pills.tsx +10 -2
- package/src/components/number-field.tsx +18 -15
- package/src/components/number-input.tsx +3 -19
- package/src/components/numpad-sheet.tsx +9 -13
- package/src/components/one-time-code-input.tsx +6 -11
- package/src/components/page-contents.tsx +2 -9
- package/src/components/page-header.tsx +5 -10
- package/src/components/password-strength-labels.ts +1 -2
- package/src/components/password-strength.tsx +6 -9
- package/src/components/phone-input.tsx +2 -13
- package/src/components/picker-sheet.tsx +2 -5
- package/src/components/pie-chart.tsx +6 -12
- package/src/components/popover.tsx +2 -1
- package/src/components/progress-bar.tsx +16 -25
- package/src/components/qr-code.tsx +2 -9
- package/src/components/row-actions.tsx +9 -1
- package/src/components/search-field.tsx +4 -5
- package/src/components/select.tsx +2 -10
- package/src/components/separator.tsx +1 -5
- package/src/components/series-chart-axes.ts +5 -5
- package/src/components/series-chart-budget.ts +1 -4
- package/src/components/series-chart-ticks.ts +1 -4
- package/src/components/series-chart.tsx +43 -83
- package/src/components/settings-fields.tsx +20 -15
- package/src/components/share-card.tsx +6 -14
- package/src/components/sign-chip.tsx +16 -16
- package/src/components/signature-pad.tsx +21 -13
- package/src/components/signed-amount.tsx +6 -14
- package/src/components/skeleton.tsx +2 -13
- package/src/components/slider.tsx +9 -20
- package/src/components/sparkline.tsx +2 -13
- package/src/components/stat-tile.tsx +13 -24
- package/src/components/swatch-picker.tsx +8 -3
- package/src/components/swipeable-row.tsx +2 -11
- package/src/components/table-skeleton.tsx +4 -1
- package/src/components/table.tsx +12 -8
- package/src/components/tabs.tsx +24 -39
- package/src/components/text-link.tsx +6 -6
- package/src/components/text.tsx +1 -2
- package/src/components/tile-radio.tsx +6 -19
- package/src/components/time-input.tsx +4 -5
- package/src/components/toast.tsx +3 -12
- package/src/components/toggle-group.tsx +9 -9
- package/src/components/toggle-legend.tsx +4 -16
- package/src/components/tooltip.tsx +9 -48
- package/src/components/translation-review-editor.tsx +4 -4
- package/src/components/translation-review-parts.tsx +7 -1
- package/src/components/translation-review.tsx +15 -16
- package/src/components/tree-view.tsx +17 -20
- package/src/components/treemap.tsx +3 -15
- package/src/components/trigger-aria.ts +1 -4
- package/src/components/use-table-state.ts +16 -17
- package/src/components/user-avatar.tsx +4 -21
- package/src/demo/demo-start.tsx +8 -4
- package/src/feedback/feedback-attachment-labels.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +16 -29
- package/src/feedback/feedback-capture.ts +3 -1
- package/src/feedback/feedback-comment-thread.tsx +9 -6
- package/src/feedback/feedback-context.tsx +1 -4
- package/src/feedback/feedback-dialog.tsx +3 -4
- package/src/feedback/feedback-inbox.tsx +3 -9
- package/src/feedback/feedback-public-list.tsx +7 -1
- package/src/feedback/feedback-record.ts +6 -14
- package/src/feedback/feedback-replies.tsx +10 -5
- package/src/feedback/feedback-row-detail.tsx +2 -5
- package/src/feedback/feedback-status-undo.ts +1 -4
- package/src/feedback/feedback-submit-dialog.tsx +4 -4
- package/src/feedback/feedback-table.tsx +29 -9
- package/src/feedback/feedback-thread.tsx +4 -16
- package/src/feedback/feedback-vocabulary.ts +72 -82
- package/src/feedback/feedback-vote-button.tsx +9 -8
- package/src/hooks/use-anchored-rect.ts +1 -4
- package/src/hooks/use-document-title.ts +3 -1
- package/src/hooks/use-file-drop.ts +1 -3
- package/src/hooks/use-file-picker-labels.ts +1 -2
- package/src/hooks/use-file-picker.tsx +1 -5
- package/src/hooks/use-focus-trap.ts +1 -2
- package/src/hooks/use-overlay-history.ts +10 -5
- package/src/hooks/use-row-swipe.ts +1 -6
- package/src/i18n/defaults.ts +1 -4
- package/src/i18n/german.ts +42 -50
- package/src/i18n/kit-labels.tsx +5 -20
- package/src/i18n/languages.ts +2 -8
- package/src/i18n/locales/en.ts +15 -31
- package/src/i18n/locales/es.ts +44 -50
- package/src/i18n/locales/fr.ts +43 -52
- package/src/i18n/locales/hu.ts +35 -38
- package/src/i18n/locales/it.ts +44 -53
- package/src/i18n/locales/zh.ts +23 -33
- package/src/i18n/plurals.ts +4 -1
- package/src/i18n/review.ts +20 -7
- package/src/i18n/shared-labels.ts +1 -2
- package/src/index.ts +107 -32
- package/src/landing/access.ts +2 -7
- package/src/landing/landing-sections.tsx +14 -15
- package/src/landing/page-seo.ts +1 -3
- package/src/lib/calc.ts +4 -1
- package/src/lib/clipboard-minus.ts +4 -1
- package/src/lib/dates.ts +2 -10
- package/src/lib/decimal-marks.ts +1 -3
- package/src/lib/device-settings.ts +2 -1
- package/src/lib/direction.ts +1 -3
- package/src/lib/iban.ts +89 -9
- package/src/lib/logger.ts +1 -5
- package/src/lib/person-name.ts +1 -3
- package/src/lib/phone.ts +5 -8
- package/src/lib/qr-encode.ts +32 -8
- package/src/lib/session-refresh.ts +5 -6
- package/src/lib/table-text.ts +24 -6
- package/src/lib/text-match.ts +43 -1
- package/src/lib/translation-review.ts +1 -3
- package/src/notifications/notification-inbox.tsx +17 -4
- package/src/notifications/notification-parts.tsx +1 -2
- package/src/notifications/notification-types-setting.tsx +19 -18
- package/src/notifications/notifications-labels.ts +1 -2
- package/src/notifications/push-device-setting.tsx +7 -1
- package/src/notifications/push-devices-setting.tsx +7 -3
- package/src/notifications/push-worker.ts +10 -2
- package/src/notifications/quiet-hours-setting.tsx +4 -1
- package/src/notifications/use-push-device.ts +4 -1
- package/src/offline/pwa-update-prompt.tsx +7 -1
- package/src/offline/query-persistence.ts +9 -6
- package/src/offline.ts +6 -1
- package/src/rhf/fields.tsx +50 -19
- package/src/rhf/form-core.tsx +2 -6
- package/src/rhf/line-items.tsx +5 -7
- package/src/rhf/use-rhf-wizard-step.ts +3 -12
- package/src/search/command-palette.tsx +15 -23
- package/src/search/global-search.tsx +38 -8
- package/src/search/search-index.ts +5 -1
- package/src/settings/device-reset-setting.tsx +5 -8
- package/src/settings/settings-heading.tsx +3 -1
- package/src/settings/settings-layout.tsx +6 -13
- package/src/settings/use-settings-focus.ts +8 -0
- package/src/settings/use-settings-route.ts +4 -3
- package/src/shell/app-shell.tsx +48 -24
- package/src/shell/auth-layout.tsx +1 -6
- package/src/shell/dev-menu.tsx +18 -6
- package/src/shell/option-switcher-menu.tsx +1 -3
- package/src/shell/sync-status-indicator.tsx +14 -12
- package/src/shell/top-bar-parts-labels.ts +1 -3
- package/src/shell/topbar-action-menu.tsx +130 -45
- package/src/shell/topbar-controls.tsx +5 -19
- package/src/shell/tour-menu.tsx +9 -1
- package/src/shell.ts +1 -1
- package/src/sync/db.ts +5 -1
- package/src/sync/engine.ts +44 -12
- package/src/sync.ts +16 -2
- package/src/testing/css-variables.ts +1 -2
- package/src/testing/index.ts +1 -6
- package/src/testing/react.ts +1 -2
- package/src/testing/viewport.ts +9 -9
- package/src/theme/contrast-tokens.ts +13 -2
- package/src/theme/palette-derive.ts +24 -50
- package/src/theme/palette-presets.ts +0 -1
- package/src/theme/palette-store.ts +1 -4
- package/src/theme/text-size.ts +1 -1
- package/src/theme/theme-store.ts +2 -6
- package/src/tour/tour-catalogue.tsx +4 -2
- package/src/tour/tour-dock.ts +5 -1
- package/src/tour/tour-engine.ts +2 -6
- package/src/tour/tour.tsx +28 -12
- package/src/tour/use-tour-progress.ts +8 -2
- package/src/tour/use-tour-url-sync.ts +3 -1
- package/src/vite-env.d.ts +1 -4
- package/src/wizard/stepper-nav.tsx +10 -35
- package/src/wizard/types.ts +1 -4
- package/src/wizard/use-wizard.ts +28 -43
- package/src/wizard/validation.ts +1 -5
- package/src/wizard/wizard-context.tsx +2 -6
- package/src/wizard/wizard-step.tsx +1 -8
- package/src/wizard/wizard-summary.tsx +2 -6
- package/tokens.css +393 -152
package/dist/sync/engine.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sync/engine.ts"],"sourcesContent":["import { httpStatusOf } from \"../auth/auth-errors\";\nimport { isOfflineError, stampAfter } from \"../offline/local-first\";\nimport { isSharedComputer } from \"../offline/shared-computer\";\nimport type { OutboxChange, OutboxStatus, SyncDb, SyncedRow } from \"./db\";\nimport type { SyncRequestBody, SyncResponseBody, SyncWireChange } from \"./wire\";\n\n/*\n * The sync engine (docs/local-first-harmonization.md §2.3): kastlan's handover client,\n * generalised (D10). Keksdose's SERVER rules and wire shapes; the client policies are ka's —\n * a new edit merges into the row's last `pending` upsert, a refused change is held per change\n * (`waiting` / `blocked` / `rejected`), a capped backoff on errors, the queue kept at sign-out.\n *\n * A round, per scope in `scopes()` order (read every round, D11): take the scope's queue and\n * mark it `sending`; post `{scope, last_knowledge, changes, refusals, client_now, paged?}`\n * with `client_now` read fresh (D2 amendment); apply the answer in ONE transaction, only if\n * the generation is unchanged (D15); follow `page_cursor` with pull-only requests, the cursor\n * written with the last page only (D12). Then the app's `steps`, then one more pass if a step\n * changed something. One round at a time; a request during a round runs once more after it.\n */\n\nexport interface SyncCounts {\n pending: number;\n waiting: number;\n blocked: number;\n rejected: number;\n}\n\nexport interface SyncStepContext {\n signal: AbortSignal;\n /** False once `forget()` bumped the generation: the step writes nothing more (D15). */\n isCurrent(): boolean;\n}\n\nexport interface SyncStepResult {\n /** The step met no connection: the round ends offline. */\n offline?: boolean;\n /** One more push/pull in the same round (kastlan's second pass after its photos). */\n changed?: boolean;\n /** What the step keeps on the device, added to the engine's counts. */\n counts?: Partial<SyncCounts>;\n}\n\n/** Work after the push/pull, such as kastlan's photo upload. `D` is the app's database type\n * (`createSyncDb<HandoverDb>`), so a step reaches the app's own tables without a cast. */\nexport interface SyncStep<D extends SyncDb = SyncDb> {\n name: string;\n run(db: D, ctx: SyncStepContext): Promise<SyncStepResult>;\n /** Its counts read from the device between rounds (a photo taken offline shows at once). */\n counts?(db: D): Promise<Partial<SyncCounts>>;\n}\n\nexport interface SyncEngineState {\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n online: boolean;\n counts: SyncCounts;\n /** When a scope's walk last completed (ISO), kept in `meta` (D9). */\n lastSyncedAt: string | null;\n /** When the next attempt after an error runs (epoch ms), or `null`. */\n nextRetryAt: number | null;\n}\n\nexport interface SyncRoundResult {\n /** `paused`: `paused()` held the round. `dropped`: `forget()` ran during it, its answer unused. */\n outcome: \"synced\" | \"offline\" | \"error\" | \"paused\" | \"dropped\";\n /** Changes this round set `waiting`, `blocked` or `rejected`. */\n waiting: number;\n blocked: number;\n rejected: number;\n error?: unknown;\n}\n\nexport interface SyncEngineOptions<D extends SyncDb = SyncDb> {\n /** The database; its type `D` is the one the `steps` get. */\n db: D;\n /** The scopes this session's role holds, in round order (kastlan: handover, meetings, meters,\n * maintenance, contacts). Read at the start of every round: roles are per company. */\n scopes: () => readonly string[];\n /** Posts one request (`POST /sync`) and answers its body; throws as the app's client does. */\n send: (body: SyncRequestBody, signal: AbortSignal) => Promise<SyncResponseBody>;\n /** Default {@link isOfflineError}: no answer, or the session refresh's passing 401. */\n isOffline?: (err: unknown) => boolean;\n /** A whole request refused for the plan → every sent change `waiting` with this code.\n * Default: a 402 whose body names a `code` (or `detail.code`). */\n planCode?: (err: unknown) => string | null;\n /** `refused.code`s kept as `waiting` and sent again every round; any other code is\n * `blocked`, sent again only on `retryBlocked()`. Default `[\"billing_read_only\"]`. */\n holdCodes?: readonly string[];\n /** Ask for a paged pull and follow its cursor (D12). */\n paged?: boolean;\n steps?: readonly SyncStep<D>[];\n /** No round while this holds (kastlan: a practice tour fakes the server's answers). */\n paused?: () => boolean;\n /** The poll (30 s) and the debounce after a write (800 ms). */\n pollMs?: number;\n soonMs?: number;\n /** The waits after consecutive errors; the last repeats (D13). */\n backoffMs?: readonly number[];\n /** Default `isSharedComputer`: on a computer marked shared the engine does not start (D15). */\n sharedComputer?: () => boolean;\n}\n\nexport interface SyncEngine {\n /** Into the copy at once and into the queue, in the scope the caller names (D11). An edit\n * merges into the row's last `pending` upsert; deleting a row that never reached the\n * server forgets its queue; stamped with `stampAfter`. */\n write(scope: string, entity: string, id: string, op: \"upsert\" | \"delete\", data?: Record<string, unknown>): Promise<void>;\n start(): void;\n /** Stops the poll and the timers, aborts the round in flight and drops its answer (the\n * queue is laid back). A new engine's round on the same database waits for it. */\n stop(): void;\n /** Sync soon (debounced): after a write. */\n requestSync(): void;\n /** \"Sync now\": resets the backoff, runs a round (after the one under way). */\n syncNow(): Promise<SyncRoundResult>;\n /** A round that sends the `blocked` changes too. */\n retryBlocked(): Promise<void>;\n /** Drop the rejected changes (of one scope) and pull that scope afresh. */\n discardRejected(scope?: string): Promise<void>;\n /** The rejected changes with their reasons, for the app's banner. */\n rejected(): Promise<OutboxChange[]>;\n /** Sign-out: bump the generation (a round in flight writes nothing more), then clear the\n * rows and `meta` — and the outbox and the app's tables unless `keepQueue` (D15). */\n forget(opts: { keepQueue: boolean }): Promise<void>;\n getState(): SyncEngineState;\n subscribe(listener: () => void): () => void;\n}\n\nexport const DEFAULT_SYNC_BACKOFF_MS: readonly number[] = [5_000, 15_000, 60_000, 300_000];\nexport const DEFAULT_HOLD_CODES: readonly string[] = [\"billing_read_only\"];\n\nconst LAST_SYNCED_AT = \"lastSyncedAt\";\nconst lastKnowledgeKey = (scope: string) => `lastKnowledge:${scope}`;\nconst UNSENT: readonly OutboxStatus[] = [\"pending\", \"sending\", \"waiting\", \"blocked\"];\nconst ZERO: SyncCounts = { pending: 0, waiting: 0, blocked: 0, rejected: 0 };\n\n/** The started engine of each database: one engine per database (D15). */\nconst running = new Map<string, SyncEngine>();\n\n/** The round in flight on each database, of whichever engine: a new engine's round waits\n * for it, so two never take the same queue (a company switch A→B→A within one round). */\nconst flights = new Map<string, Promise<unknown>>();\n\n/** The engines {@link syncEngineFor} keeps, by database name. */\nconst kept = new Map<string, { db: SyncDb; engine: SyncEngine }>();\n\n/** The generation changed under a round: its answer is dropped. */\nclass Dropped extends Error {}\n\n/** A row created on this device and never echoed by the server carries `local: true`. */\ntype CopyRow = SyncedRow & { local?: boolean };\n\nfunction browserOnline(): boolean {\n return typeof navigator === \"undefined\" || navigator.onLine !== false;\n}\n\nfunction defaultPlanCode(err: unknown): string | null {\n if (httpStatusOf(err) !== 402) return null;\n const body = (err as { response?: { data?: { code?: unknown; detail?: { code?: unknown } } } }).response?.data;\n const code = typeof body?.code === \"string\" ? body.code : body?.detail?.code;\n return typeof code === \"string\" ? code : null;\n}\n\n/** A refusal's `code` and `detail` from an error's body: `{code, detail}` or FastAPI's\n * `{detail: {code, detail}}`. */\nfunction refusalOf(err: unknown): { code?: string; detail?: string } {\n const body = (err as { response?: { data?: unknown } }).response?.data as\n | { code?: unknown; detail?: unknown }\n | undefined;\n const inner = body?.detail && typeof body.detail === \"object\" ? (body.detail as { code?: unknown; detail?: unknown }) : body;\n const text = (v: unknown) => (typeof v === \"string\" && v ? v : undefined);\n return { code: text(inner?.code), detail: text(inner?.detail) };\n}\n\nfunction latest(stamps: unknown[]): string | undefined {\n let best: string | undefined;\n for (const s of stamps) {\n if (typeof s !== \"string\" || !Number.isFinite(Date.parse(s))) continue;\n if (best === undefined || Date.parse(s) > Date.parse(best)) best = s;\n }\n return best;\n}\n\nconst toWire = ({ entity, id, op, data, updated_at }: OutboxChange): SyncWireChange =>\n op === \"delete\" ? { entity, id, op, updated_at } : { entity, id, op, data, updated_at };\n\n/**\n * The engine on one database (D15: one engine per database; a company switch builds a new\n * engine on the new database and stops the old one). Nothing runs until `start()`\n * (`useSyncWorker`), and `start()` does nothing on a computer marked shared.\n */\nexport function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const { db } = options;\n const isOffline = options.isOffline ?? isOfflineError;\n const planCode = options.planCode ?? defaultPlanCode;\n const holdCodes = new Set(options.holdCodes ?? DEFAULT_HOLD_CODES);\n const steps = options.steps ?? [];\n const pollMs = options.pollMs ?? 30_000;\n const soonMs = options.soonMs ?? 800;\n const backoffMs = options.backoffMs?.length ? options.backoffMs : DEFAULT_SYNC_BACKOFF_MS;\n const sharedComputer = options.sharedComputer ?? isSharedComputer;\n\n let generation = 0;\n let state: SyncEngineState = { phase: \"idle\", online: browserOnline(), counts: ZERO, lastSyncedAt: null, nextRetryAt: null };\n const listeners = new Set<() => void>();\n let started = false;\n let poll: ReturnType<typeof setInterval> | null = null;\n let soon: ReturnType<typeof setTimeout> | null = null;\n let retry: ReturnType<typeof setTimeout> | null = null;\n let failures = 0;\n let current: Promise<SyncRoundResult> | null = null;\n let again = false;\n let controller: AbortController | null = null;\n let stepCounts: Record<string, Partial<SyncCounts>> = {};\n\n function set(next: Partial<SyncEngineState>) {\n state = { ...state, ...next };\n listeners.forEach((listener) => listener());\n }\n\n /** The counts and `lastSyncedAt` read again from the device. Never throws: a closed or\n * deleted database leaves the last counts standing. */\n function refreshCounts(): Promise<void> {\n return readCounts().catch(() => undefined);\n }\n\n async function readCounts(): Promise<void> {\n const gen = generation;\n const count = (status: OutboxStatus) => db.outbox.where(\"status\").equals(status).count();\n const [pending, sending, waiting, blocked, rejected] = await Promise.all(\n ([\"pending\", \"sending\", \"waiting\", \"blocked\", \"rejected\"] as const).map(count),\n );\n const fromSteps = { ...stepCounts };\n for (const step of steps) if (step.counts) fromSteps[step.name] = await step.counts(db);\n const last = (await db.meta.get(LAST_SYNCED_AT))?.value;\n if (gen !== generation) return;\n if (Object.keys(fromSteps).length) stepCounts = fromSteps;\n const counts = { pending: pending + sending, waiting, blocked, rejected };\n for (const extra of Object.values(stepCounts)) {\n for (const key of Object.keys(counts) as (keyof SyncCounts)[]) counts[key] += extra[key] ?? 0;\n }\n set({ counts, lastSyncedAt: typeof last === \"string\" ? last : null });\n }\n\n // ── The queue ──────────────────────────────────────────────────────────────\n\n /** Mark the scope's queue as in the request (a `sending` left by a closed tab goes back\n * first); answers it with the statuses it had. */\n function takeQueue(scope: string, withBlocked: boolean): Promise<OutboxChange[]> {\n return db.transaction(\"rw\", db.outbox, async () => {\n await db.outbox\n .where(\"scope\")\n .equals(scope)\n .and((c) => c.status === \"sending\")\n .modify({ status: \"pending\" });\n const take: OutboxStatus[] = withBlocked ? [\"pending\", \"waiting\", \"blocked\"] : [\"pending\", \"waiting\"];\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray())\n .filter((c) => take.includes(c.status))\n .sort((a, b) => a.seq! - b.seq!);\n await db.outbox.bulkUpdate(queue.map((c) => ({ key: c.seq!, changes: { status: \"sending\" as const } })));\n return queue;\n });\n }\n\n /** The request failed: each change back to the status it had. */\n function restore(sent: OutboxChange[]) {\n return db.outbox.bulkUpdate(sent.map((c) => ({ key: c.seq!, changes: { status: c.status } })));\n }\n\n function mark(changes: OutboxChange[], fields: Partial<OutboxChange>) {\n return db.outbox.bulkUpdate(changes.map((c) => ({ key: c.seq!, changes: fields })));\n }\n\n /** Put one page's rows in the copy, settle the sent changes, lay the unsent ones back. */\n async function apply(scope: string, answer: SyncResponseBody, sent: OutboxChange[], gen: number, lastPage: boolean) {\n return db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n if (gen !== generation) throw new Dropped();\n const refused = answer.refused ?? null;\n const refusedIds = new Set(refused?.change_ids ?? []);\n const rejections = new Map((answer.rejections ?? []).map((r) => [r.change_id, r]));\n const kept = sent.filter((c) => refusedIds.has(c.id));\n const rejected = sent.filter((c) => !refusedIds.has(c.id) && rejections.has(c.id));\n await db.outbox.bulkDelete(sent.filter((c) => !refusedIds.has(c.id) && !rejections.has(c.id)).map((c) => c.seq!));\n const hold = refused != null && holdCodes.has(refused.code);\n if (refused) await mark(kept, { status: hold ? \"waiting\" : \"blocked\", code: refused.code, reason: refused.detail });\n for (const change of rejected) {\n const why = rejections.get(change.id)!;\n await db.outbox.update(change.seq!, { status: \"rejected\", code: why.code, reason: why.detail });\n }\n const pulled = new Set<string>();\n for (const change of answer.changes) {\n pulled.add(`${change.entity}\\u0000${change.id}`);\n if (change.op === \"delete\") await db.rows.delete([change.entity, change.id]);\n else {\n const row: CopyRow = {\n entity: change.entity,\n id: change.id,\n pk: change.pk ?? null,\n local: false,\n data: change.data ?? {},\n // The server's stamp rides beside `data`: kept, so the next edit is stamped after it.\n updated_at: change.updated_at ?? null,\n };\n await db.rows.put(row);\n }\n }\n // A pull never undoes what was typed while the request was out.\n const unsent = (await db.outbox.where(\"status\").anyOf(UNSENT).toArray())\n .filter((c) => pulled.has(`${c.entity}\\u0000${c.id}`))\n .sort((a, b) => a.seq! - b.seq!);\n for (const change of unsent) {\n if (change.op === \"delete\") {\n await db.rows.delete([change.entity, change.id]);\n continue;\n }\n const row = (await db.rows.get([change.entity, change.id])) as CopyRow | undefined;\n await db.rows.put({ ...row, entity: change.entity, id: change.id, pk: row?.pk ?? null, data: { ...row?.data, ...change.data } });\n }\n if (lastPage) {\n await db.meta.put({ key: lastKnowledgeKey(scope), value: answer.server_knowledge });\n await db.meta.put({ key: LAST_SYNCED_AT, value: new Date().toISOString() });\n }\n return {\n waiting: hold ? kept.length : 0,\n blocked: refused && !hold ? kept.length : 0,\n rejected: rejected.length,\n };\n });\n }\n\n /** A scope the role no longer holds (403): its queue is rejected and it leaves the round.\n * The server's `code` and `detail` are kept (a demo's `demo_read_only`); without them,\n * `forbidden` and \"Not allowed to sync <scope>\". */\n async function loseScope(scope: string, err: unknown): Promise<number> {\n const why = refusalOf(err);\n return db.transaction(\"rw\", db.outbox, async () => {\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray()).filter((c) => UNSENT.includes(c.status));\n await mark(queue, {\n status: \"rejected\",\n code: why.code ?? \"forbidden\",\n reason: why.code && why.detail ? why.detail : `Not allowed to sync ${scope}`,\n });\n return queue.length;\n });\n }\n\n type ScopeResult = { offline: true } | { offline: false; waiting: number; blocked: number; rejected: number };\n\n async function syncScope(scope: string, gen: number, signal: AbortSignal, withBlocked: boolean): Promise<ScopeResult> {\n const sent = await takeQueue(scope, withBlocked);\n const last = Number((await db.meta.get(lastKnowledgeKey(scope)))?.value ?? 0) || 0;\n const body = (changes: SyncWireChange[], page_cursor: SyncRequestBody[\"page_cursor\"]): SyncRequestBody => ({\n scope,\n last_knowledge: last,\n changes,\n refusals: true,\n client_now: new Date().toISOString(),\n ...(options.paged ? { paged: true, page_cursor } : {}),\n });\n let answer: SyncResponseBody;\n try {\n answer = await options.send(body(sent.map(toWire), null), signal);\n } catch (err) {\n await restore(sent);\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n const plan = planCode(err);\n if (plan) {\n // The whole request refused for the plan: every change waits for it.\n await mark(sent, { status: \"waiting\", code: plan, reason: plan });\n return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };\n }\n if (httpStatusOf(err) === 403) return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };\n throw err;\n }\n let cursor = options.paged ? (answer.page_cursor ?? null) : null;\n let tally: ScopeResult;\n try {\n tally = { offline: false, ...(await apply(scope, answer, sent, gen, cursor == null)) };\n } catch (err) {\n if (err instanceof Dropped) await restore(sent);\n throw err;\n }\n // The rest of a paged pull: pull-only; the cursor is written with the last page alone,\n // so an interrupted walk starts again from the old one (D12).\n while (cursor) {\n let page: SyncResponseBody;\n try {\n page = await options.send(body([], cursor), signal);\n } catch (err) {\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n throw err;\n }\n cursor = page.page_cursor ?? null;\n await apply(scope, page, [], gen, cursor == null);\n }\n return tally;\n }\n\n // ── The round ──────────────────────────────────────────────────────────────\n\n async function round(withBlocked: boolean, gen: number): Promise<SyncRoundResult> {\n const result: SyncRoundResult = { outcome: \"synced\", waiting: 0, blocked: 0, rejected: 0 };\n if (gen !== generation) return { ...result, outcome: \"dropped\" };\n if (options.paused?.()) return { ...result, outcome: \"paused\" };\n if (!browserOnline()) return { ...result, outcome: \"offline\" };\n const own = new AbortController();\n controller = own;\n set({ phase: \"syncing\" });\n try {\n for (let pass = 0; ; pass++) {\n for (const scope of options.scopes()) {\n if (gen !== generation) throw new Dropped();\n const r = await syncScope(scope, gen, own.signal, withBlocked);\n if (r.offline) return { ...result, outcome: \"offline\" };\n result.waiting += r.waiting;\n result.blocked += r.blocked;\n result.rejected += r.rejected;\n }\n if (pass > 0) break;\n let changed = false;\n for (const step of steps) {\n if (gen !== generation) throw new Dropped();\n const ran = await step.run(db, { signal: own.signal, isCurrent: () => gen === generation });\n if (gen !== generation) throw new Dropped();\n if (ran.counts) stepCounts = { ...stepCounts, [step.name]: ran.counts };\n if (ran.offline) return { ...result, outcome: \"offline\" };\n if (ran.changed) changed = true;\n }\n if (!changed) break;\n }\n return result;\n } catch (err) {\n if (err instanceof Dropped || gen !== generation) return { ...result, outcome: \"dropped\" };\n return { ...result, outcome: \"error\", error: err };\n } finally {\n if (controller === own) controller = null;\n }\n }\n\n function clearRetry() {\n if (retry) clearTimeout(retry);\n retry = null;\n }\n\n function resetBackoff() {\n failures = 0;\n clearRetry();\n if (state.nextRetryAt !== null) set({ nextRetryAt: null });\n }\n\n const inBackoff = () => state.nextRetryAt !== null && Date.now() < state.nextRetryAt;\n\n function settle(result: SyncRoundResult) {\n switch (result.outcome) {\n case \"synced\":\n failures = 0;\n clearRetry();\n set({ phase: \"idle\", online: true, nextRetryAt: null });\n break;\n case \"offline\":\n // No retry timer offline: the `online` event or the poll brings the next round (D13).\n clearRetry();\n set({ phase: \"offline\", online: false, nextRetryAt: null });\n break;\n case \"error\": {\n const wait = backoffMs[Math.min(failures, backoffMs.length - 1)];\n failures += 1;\n clearRetry();\n if (started) {\n retry = setTimeout(() => {\n retry = null;\n void run(false);\n }, wait);\n }\n set({ phase: \"error\", nextRetryAt: started ? Date.now() + wait : null });\n break;\n }\n case \"dropped\":\n set({ phase: \"idle\" });\n break;\n case \"paused\":\n break;\n }\n }\n\n function run(withBlocked: boolean): Promise<SyncRoundResult> {\n if (current) {\n again = true;\n return current;\n }\n const before = flights.get(db.name);\n // The generation is read before any wait: a stop() while this waits drops the round, so\n // it never sends this copy's queue with another account's client nor pulls into it.\n const gen = generation;\n const flight = (async () => {\n // Another engine's round on this database (stopped, still settling) ends first.\n if (before) await before.catch(() => undefined);\n try {\n const result = await round(withBlocked, gen);\n settle(result);\n await refreshCounts();\n return result;\n } finally {\n current = null;\n if (again) {\n again = false;\n tick();\n }\n }\n })();\n current = flight;\n flights.set(db.name, flight);\n const done = () => {\n if (flights.get(db.name) === flight) flights.delete(db.name);\n };\n void flight.then(done, done);\n return flight;\n }\n\n /** A round from the poll, a write or a request during a round: not while backing off. */\n function tick() {\n if (started && !inBackoff()) void run(false);\n }\n\n async function runFresh(withBlocked: boolean): Promise<SyncRoundResult> {\n resetBackoff();\n if (current) {\n again = false;\n await current.catch(() => undefined);\n }\n return run(withBlocked);\n }\n\n const onOnline = () => {\n set({ online: true });\n resetBackoff();\n void run(false);\n };\n const onOffline = () => set({ online: false });\n\n const engine: SyncEngine = {\n async write(scope, entity, id, op, data = {}) {\n await db.transaction(\"rw\", db.rows, db.outbox, async () => {\n const row = (await db.rows.get([entity, id])) as CopyRow | undefined;\n const ofRow = await db.outbox.where(\"[entity+id]\").equals([entity, id]).toArray();\n ofRow.sort((a, b) => a.seq! - b.seq!);\n const updated_at = stampAfter(latest([row?.updated_at, row?.data.updated_at, ...ofRow.map((c) => c.updated_at)]));\n // A new edit to the row moves a rejected change out of the way (D13).\n const rejected = ofRow.filter((c) => c.scope === scope && c.status === \"rejected\");\n if (op === \"delete\") {\n await db.rows.delete([entity, id]);\n const neverSynced = row != null && (row.local ?? row.pk == null);\n if (neverSynced && !ofRow.some((c) => c.status === \"sending\")) {\n // Never reached the server: forgetting its queue is enough — a waiting change\n // included, or it would create the row once the plan is back.\n await db.outbox.bulkDelete(ofRow.map((c) => c.seq!));\n return;\n }\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n await db.outbox.add({ scope, entity, id, op, data: {}, updated_at, status: \"pending\" });\n return;\n }\n const next: CopyRow = { ...row, entity, id, pk: row?.pk ?? null, data: { ...row?.data, ...data } };\n if (!row) next.local = true;\n await db.rows.put(next);\n const carried = Object.assign({}, ...rejected.filter((c) => c.op === \"upsert\").map((c) => c.data)) as Record<string, unknown>;\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n const last = ofRow.filter((c) => c.scope === scope && c.status === \"pending\").at(-1);\n if (last && last.op === \"upsert\") {\n // Several edits before a round go up as one change, with the last one's time.\n await db.outbox.update(last.seq!, { data: { ...carried, ...last.data, ...data }, updated_at });\n } else {\n await db.outbox.add({ scope, entity, id, op, data: { ...carried, ...data }, updated_at, status: \"pending\" });\n }\n });\n engine.requestSync();\n },\n\n start() {\n if (started || sharedComputer()) return;\n const other = running.get(db.name);\n if (other && other !== engine) other.stop();\n running.set(db.name, engine);\n started = true;\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", onOnline);\n window.addEventListener(\"offline\", onOffline);\n }\n poll = setInterval(tick, pollMs);\n void refreshCounts();\n void run(false);\n },\n\n stop() {\n if (!started) return;\n started = false;\n // The round in flight writes nothing more: its request is aborted and its answer\n // dropped, the queue laid back; the next engine's round waits for it to settle.\n generation += 1;\n controller?.abort();\n if (running.get(db.name) === engine) running.delete(db.name);\n if (poll) clearInterval(poll);\n poll = null;\n if (soon) clearTimeout(soon);\n soon = null;\n clearRetry();\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", onOnline);\n window.removeEventListener(\"offline\", onOffline);\n }\n },\n\n requestSync() {\n void refreshCounts();\n if (!started) return;\n if (soon) clearTimeout(soon);\n soon = setTimeout(() => {\n soon = null;\n tick();\n }, soonMs);\n },\n\n syncNow: () => runFresh(false),\n\n async retryBlocked() {\n await runFresh(true);\n },\n\n async discardRejected(scope) {\n const gone = (await db.outbox.where(\"status\").equals(\"rejected\").toArray()).filter((c) => !scope || c.scope === scope);\n if (gone.length === 0) return;\n await db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n await db.outbox.bulkDelete(gone.map((c) => c.seq!));\n for (const change of gone) {\n const others = await db.outbox.where(\"[entity+id]\").equals([change.entity, change.id]).count();\n if (others === 0) await db.rows.delete([change.entity, change.id]);\n }\n // The scope pulls afresh, so the copy shows what the server holds.\n for (const s of new Set(gone.map((c) => c.scope))) await db.meta.put({ key: lastKnowledgeKey(s), value: 0 });\n });\n await refreshCounts();\n engine.requestSync();\n },\n\n async rejected() {\n return db.outbox.where(\"status\").equals(\"rejected\").sortBy(\"seq\");\n },\n\n async forget({ keepQueue }) {\n generation += 1;\n controller?.abort();\n const tables = keepQueue ? [db.rows, db.meta] : db.tables;\n await db.transaction(\"rw\", tables, async () => {\n for (const table of tables) await table.clear();\n });\n stepCounts = {};\n set({ lastSyncedAt: null });\n await refreshCounts();\n },\n\n getState: () => state,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n return engine;\n}\n\n/**\n * The engine kept for `options.db` (by its name), built on first use: one engine per\n * database for the session (D15), so a company switch A→B→A finds A's engine again instead\n * of building a second one beside a round in flight. The first call's options stand: read\n * changing values (scopes, the client) through functions. A database object opened afresh\n * under the same name replaces the kept engine (the old one is stopped).\n */\nexport function syncEngineFor<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const name = options.db.name;\n const found = kept.get(name);\n if (found && found.db === options.db) return found.engine;\n found?.engine.stop();\n const engine = createSyncEngine(options);\n kept.set(name, { db: options.db, engine });\n return engine;\n}\n\n/**\n * Let go of the engine {@link syncEngineFor} keeps for the database `name` (0.46): it is\n * stopped, its database closed and forgotten, so a later `syncEngineFor` builds afresh. Call it\n * on sign-out (and for a company left for good): every database visited in a tab otherwise\n * keeps its engine and an open IndexedDB connection until a reload. Nothing kept: a no-op.\n */\nexport function releaseSyncEngine(name: string): void {\n const found = kept.get(name);\n if (!found) return;\n kept.delete(name);\n found.engine.stop();\n found.db.close();\n}\n"],"mappings":"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","names":["kept"]}
|
|
1
|
+
{"version":3,"sources":["../../src/sync/engine.ts"],"sourcesContent":["import { httpStatusOf } from \"../auth/auth-errors\";\nimport { isOfflineError, stampAfter } from \"../offline/local-first\";\nimport { isSharedComputer } from \"../offline/shared-computer\";\nimport type { OutboxChange, OutboxStatus, SyncDb, SyncedRow } from \"./db\";\nimport type { SyncRequestBody, SyncResponseBody, SyncWireChange } from \"./wire\";\n\n/*\n * The sync engine (docs/local-first-harmonization.md §2.3): kastlan's handover client,\n * generalised (D10). Keksdose's SERVER rules and wire shapes; the client policies are ka's —\n * a new edit merges into the row's last `pending` upsert, a refused change is held per change\n * (`waiting` / `blocked` / `rejected`), a capped backoff on errors, the queue kept at sign-out.\n *\n * A round, per scope in `scopes()` order (read every round, D11): take the scope's queue and\n * mark it `sending`; post `{scope, last_knowledge, changes, refusals, client_now, paged?}`\n * with `client_now` read fresh (D2 amendment); apply the answer in ONE transaction, only if\n * the generation is unchanged (D15); follow `page_cursor` with pull-only requests, the cursor\n * written with the last page only (D12). Then the app's `steps`, then one more pass if a step\n * changed something. One round at a time; a request during a round runs once more after it.\n */\n\nexport interface SyncCounts {\n pending: number;\n waiting: number;\n blocked: number;\n rejected: number;\n}\n\nexport interface SyncStepContext {\n signal: AbortSignal;\n /** False once `forget()` bumped the generation: the step writes nothing more (D15). */\n isCurrent(): boolean;\n}\n\nexport interface SyncStepResult {\n /** The step met no connection: the round ends offline. */\n offline?: boolean;\n /** One more push/pull in the same round (kastlan's second pass after its photos). */\n changed?: boolean;\n /** What the step keeps on the device, added to the engine's counts. */\n counts?: Partial<SyncCounts>;\n}\n\n/** Work after the push/pull, such as kastlan's photo upload. `D` is the app's database type\n * (`createSyncDb<HandoverDb>`), so a step reaches the app's own tables without a cast. */\nexport interface SyncStep<D extends SyncDb = SyncDb> {\n name: string;\n run(db: D, ctx: SyncStepContext): Promise<SyncStepResult>;\n /** Its counts read from the device between rounds (a photo taken offline shows at once). */\n counts?(db: D): Promise<Partial<SyncCounts>>;\n}\n\nexport interface SyncEngineState {\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n online: boolean;\n counts: SyncCounts;\n /** When a scope's walk last completed (ISO), kept in `meta` (D9). */\n lastSyncedAt: string | null;\n /** When the next attempt after an error runs (epoch ms), or `null`. */\n nextRetryAt: number | null;\n}\n\nexport interface SyncRoundResult {\n /** `paused`: `paused()` held the round. `dropped`: `forget()` ran during it, its answer unused. */\n outcome: \"synced\" | \"offline\" | \"error\" | \"paused\" | \"dropped\";\n /** Changes this round set `waiting`, `blocked` or `rejected`. */\n waiting: number;\n blocked: number;\n rejected: number;\n error?: unknown;\n}\n\nexport interface SyncEngineOptions<D extends SyncDb = SyncDb> {\n /** The database; its type `D` is the one the `steps` get. */\n db: D;\n /** The scopes this session's role holds, in round order (kastlan: handover, meetings, meters,\n * maintenance, contacts). Read at the start of every round: roles are per company. */\n scopes: () => readonly string[];\n /** Posts one request (`POST /sync`) and answers its body; throws as the app's client does. */\n send: (body: SyncRequestBody, signal: AbortSignal) => Promise<SyncResponseBody>;\n /** Default {@link isOfflineError}: no answer, or the session refresh's passing 401. */\n isOffline?: (err: unknown) => boolean;\n /** A whole request refused for the plan → every sent change `waiting` with this code.\n * Default: a 402 whose body names a `code` (or `detail.code`). */\n planCode?: (err: unknown) => string | null;\n /** `refused.code`s kept as `waiting` and sent again every round; any other code is\n * `blocked`, sent again only on `retryBlocked()`. Default `[\"billing_read_only\"]`. */\n holdCodes?: readonly string[];\n /** Ask for a paged pull and follow its cursor (D12). */\n paged?: boolean;\n steps?: readonly SyncStep<D>[];\n /** No round while this holds (kastlan: a practice tour fakes the server's answers). */\n paused?: () => boolean;\n /** The poll (30 s) and the debounce after a write (800 ms). */\n pollMs?: number;\n soonMs?: number;\n /** The waits after consecutive errors; the last repeats (D13). */\n backoffMs?: readonly number[];\n /** Default `isSharedComputer`: on a computer marked shared the engine does not start (D15). */\n sharedComputer?: () => boolean;\n}\n\nexport interface SyncEngine {\n /** Into the copy at once and into the queue, in the scope the caller names (D11). An edit\n * merges into the row's last `pending` upsert; deleting a row that never reached the\n * server forgets its queue; stamped with `stampAfter`. */\n write(\n scope: string,\n entity: string,\n id: string,\n op: \"upsert\" | \"delete\",\n data?: Record<string, unknown>,\n ): Promise<void>;\n start(): void;\n /** Stops the poll and the timers, aborts the round in flight and drops its answer (the\n * queue is laid back). A new engine's round on the same database waits for it. */\n stop(): void;\n /** Sync soon (debounced): after a write. */\n requestSync(): void;\n /** \"Sync now\": resets the backoff, runs a round (after the one under way). */\n syncNow(): Promise<SyncRoundResult>;\n /** A round that sends the `blocked` changes too. */\n retryBlocked(): Promise<void>;\n /** Drop the rejected changes (of one scope) and pull that scope afresh. */\n discardRejected(scope?: string): Promise<void>;\n /** The rejected changes with their reasons, for the app's banner. */\n rejected(): Promise<OutboxChange[]>;\n /** Sign-out: bump the generation (a round in flight writes nothing more), then clear the\n * rows and `meta` — and the outbox and the app's tables unless `keepQueue` (D15). */\n forget(opts: { keepQueue: boolean }): Promise<void>;\n getState(): SyncEngineState;\n subscribe(listener: () => void): () => void;\n}\n\nexport const DEFAULT_SYNC_BACKOFF_MS: readonly number[] = [5_000, 15_000, 60_000, 300_000];\nexport const DEFAULT_HOLD_CODES: readonly string[] = [\"billing_read_only\"];\n\nconst LAST_SYNCED_AT = \"lastSyncedAt\";\nconst lastKnowledgeKey = (scope: string) => `lastKnowledge:${scope}`;\nconst UNSENT: readonly OutboxStatus[] = [\"pending\", \"sending\", \"waiting\", \"blocked\"];\nconst ZERO: SyncCounts = { pending: 0, waiting: 0, blocked: 0, rejected: 0 };\n\n/** The started engine of each database: one engine per database (D15). */\nconst running = new Map<string, SyncEngine>();\n\n/** The round in flight on each database, of whichever engine: a new engine's round waits\n * for it, so two never take the same queue (a company switch A→B→A within one round). */\nconst flights = new Map<string, Promise<unknown>>();\n\n/** The engines {@link syncEngineFor} keeps, by database name. */\nconst kept = new Map<string, { db: SyncDb; engine: SyncEngine }>();\n\n/** The generation changed under a round: its answer is dropped. */\nclass Dropped extends Error {}\n\n/** A row created on this device and never echoed by the server carries `local: true`. */\ntype CopyRow = SyncedRow & { local?: boolean };\n\nfunction browserOnline(): boolean {\n return typeof navigator === \"undefined\" || navigator.onLine !== false;\n}\n\nfunction defaultPlanCode(err: unknown): string | null {\n if (httpStatusOf(err) !== 402) return null;\n const body = (err as { response?: { data?: { code?: unknown; detail?: { code?: unknown } } } }).response?.data;\n const code = typeof body?.code === \"string\" ? body.code : body?.detail?.code;\n return typeof code === \"string\" ? code : null;\n}\n\n/** A refusal's `code` and `detail` from an error's body: `{code, detail}` or FastAPI's\n * `{detail: {code, detail}}`. */\nfunction refusalOf(err: unknown): { code?: string; detail?: string } {\n const body = (err as { response?: { data?: unknown } }).response?.data as\n { code?: unknown; detail?: unknown } | undefined;\n const inner =\n body?.detail && typeof body.detail === \"object\" ? (body.detail as { code?: unknown; detail?: unknown }) : body;\n const text = (v: unknown) => (typeof v === \"string\" && v ? v : undefined);\n return { code: text(inner?.code), detail: text(inner?.detail) };\n}\n\nfunction latest(stamps: unknown[]): string | undefined {\n let best: string | undefined;\n for (const s of stamps) {\n if (typeof s !== \"string\" || !Number.isFinite(Date.parse(s))) continue;\n if (best === undefined || Date.parse(s) > Date.parse(best)) best = s;\n }\n return best;\n}\n\nconst toWire = ({ entity, id, op, data, updated_at }: OutboxChange): SyncWireChange =>\n op === \"delete\" ? { entity, id, op, updated_at } : { entity, id, op, data, updated_at };\n\n/**\n * The engine on one database (D15: one engine per database; a company switch builds a new\n * engine on the new database and stops the old one). Nothing runs until `start()`\n * (`useSyncWorker`), and `start()` does nothing on a computer marked shared.\n */\nexport function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const { db } = options;\n const isOffline = options.isOffline ?? isOfflineError;\n const planCode = options.planCode ?? defaultPlanCode;\n const holdCodes = new Set(options.holdCodes ?? DEFAULT_HOLD_CODES);\n const steps = options.steps ?? [];\n const pollMs = options.pollMs ?? 30_000;\n const soonMs = options.soonMs ?? 800;\n const backoffMs = options.backoffMs?.length ? options.backoffMs : DEFAULT_SYNC_BACKOFF_MS;\n const sharedComputer = options.sharedComputer ?? isSharedComputer;\n\n let generation = 0;\n let state: SyncEngineState = {\n phase: \"idle\",\n online: browserOnline(),\n counts: ZERO,\n lastSyncedAt: null,\n nextRetryAt: null,\n };\n const listeners = new Set<() => void>();\n let started = false;\n let poll: ReturnType<typeof setInterval> | null = null;\n let soon: ReturnType<typeof setTimeout> | null = null;\n let retry: ReturnType<typeof setTimeout> | null = null;\n let failures = 0;\n let current: Promise<SyncRoundResult> | null = null;\n let again = false;\n let controller: AbortController | null = null;\n let stepCounts: Record<string, Partial<SyncCounts>> = {};\n\n function set(next: Partial<SyncEngineState>) {\n state = { ...state, ...next };\n listeners.forEach((listener) => listener());\n }\n\n /** The counts and `lastSyncedAt` read again from the device. Never throws: a closed or\n * deleted database leaves the last counts standing. */\n function refreshCounts(): Promise<void> {\n return readCounts().catch(() => undefined);\n }\n\n async function readCounts(): Promise<void> {\n const gen = generation;\n const count = (status: OutboxStatus) => db.outbox.where(\"status\").equals(status).count();\n const [pending, sending, waiting, blocked, rejected] = await Promise.all(\n ([\"pending\", \"sending\", \"waiting\", \"blocked\", \"rejected\"] as const).map(count),\n );\n const fromSteps = { ...stepCounts };\n for (const step of steps) if (step.counts) fromSteps[step.name] = await step.counts(db);\n const last = (await db.meta.get(LAST_SYNCED_AT))?.value;\n if (gen !== generation) return;\n if (Object.keys(fromSteps).length) stepCounts = fromSteps;\n const counts = { pending: pending + sending, waiting, blocked, rejected };\n for (const extra of Object.values(stepCounts)) {\n for (const key of Object.keys(counts) as (keyof SyncCounts)[]) counts[key] += extra[key] ?? 0;\n }\n set({ counts, lastSyncedAt: typeof last === \"string\" ? last : null });\n }\n\n // ── The queue ──────────────────────────────────────────────────────────────\n\n /** Mark the scope's queue as in the request (a `sending` left by a closed tab goes back\n * first); answers it with the statuses it had. */\n function takeQueue(scope: string, withBlocked: boolean): Promise<OutboxChange[]> {\n return db.transaction(\"rw\", db.outbox, async () => {\n await db.outbox\n .where(\"scope\")\n .equals(scope)\n .and((c) => c.status === \"sending\")\n .modify({ status: \"pending\" });\n const take: OutboxStatus[] = withBlocked ? [\"pending\", \"waiting\", \"blocked\"] : [\"pending\", \"waiting\"];\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray())\n .filter((c) => take.includes(c.status))\n .sort((a, b) => a.seq! - b.seq!);\n await db.outbox.bulkUpdate(queue.map((c) => ({ key: c.seq!, changes: { status: \"sending\" as const } })));\n return queue;\n });\n }\n\n /** The request failed: each change back to the status it had. */\n function restore(sent: OutboxChange[]) {\n return db.outbox.bulkUpdate(sent.map((c) => ({ key: c.seq!, changes: { status: c.status } })));\n }\n\n function mark(changes: OutboxChange[], fields: Partial<OutboxChange>) {\n return db.outbox.bulkUpdate(changes.map((c) => ({ key: c.seq!, changes: fields })));\n }\n\n /** Put one page's rows in the copy, settle the sent changes, lay the unsent ones back. */\n async function apply(scope: string, answer: SyncResponseBody, sent: OutboxChange[], gen: number, lastPage: boolean) {\n return db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n if (gen !== generation) throw new Dropped();\n const refused = answer.refused ?? null;\n const refusedIds = new Set(refused?.change_ids ?? []);\n const rejections = new Map((answer.rejections ?? []).map((r) => [r.change_id, r]));\n const kept = sent.filter((c) => refusedIds.has(c.id));\n const rejected = sent.filter((c) => !refusedIds.has(c.id) && rejections.has(c.id));\n await db.outbox.bulkDelete(sent.filter((c) => !refusedIds.has(c.id) && !rejections.has(c.id)).map((c) => c.seq!));\n const hold = refused != null && holdCodes.has(refused.code);\n if (refused)\n await mark(kept, { status: hold ? \"waiting\" : \"blocked\", code: refused.code, reason: refused.detail });\n for (const change of rejected) {\n const why = rejections.get(change.id)!;\n await db.outbox.update(change.seq!, { status: \"rejected\", code: why.code, reason: why.detail });\n }\n const pulled = new Set<string>();\n for (const change of answer.changes) {\n pulled.add(`${change.entity}\\u0000${change.id}`);\n if (change.op === \"delete\") await db.rows.delete([change.entity, change.id]);\n else {\n const row: CopyRow = {\n entity: change.entity,\n id: change.id,\n pk: change.pk ?? null,\n local: false,\n data: change.data ?? {},\n // The server's stamp rides beside `data`: kept, so the next edit is stamped after it.\n updated_at: change.updated_at ?? null,\n };\n await db.rows.put(row);\n }\n }\n // A pull never undoes what was typed while the request was out.\n const unsent = (await db.outbox.where(\"status\").anyOf(UNSENT).toArray())\n .filter((c) => pulled.has(`${c.entity}\\u0000${c.id}`))\n .sort((a, b) => a.seq! - b.seq!);\n for (const change of unsent) {\n if (change.op === \"delete\") {\n await db.rows.delete([change.entity, change.id]);\n continue;\n }\n const row = (await db.rows.get([change.entity, change.id])) as CopyRow | undefined;\n await db.rows.put({\n ...row,\n entity: change.entity,\n id: change.id,\n pk: row?.pk ?? null,\n data: { ...row?.data, ...change.data },\n });\n }\n if (lastPage) {\n await db.meta.put({ key: lastKnowledgeKey(scope), value: answer.server_knowledge });\n await db.meta.put({ key: LAST_SYNCED_AT, value: new Date().toISOString() });\n }\n return {\n waiting: hold ? kept.length : 0,\n blocked: refused && !hold ? kept.length : 0,\n rejected: rejected.length,\n };\n });\n }\n\n /** A scope the role no longer holds (403): its queue is rejected and it leaves the round.\n * The server's `code` and `detail` are kept (a demo's `demo_read_only`); without them,\n * `forbidden` and \"Not allowed to sync <scope>\". */\n async function loseScope(scope: string, err: unknown): Promise<number> {\n const why = refusalOf(err);\n return db.transaction(\"rw\", db.outbox, async () => {\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray()).filter((c) => UNSENT.includes(c.status));\n await mark(queue, {\n status: \"rejected\",\n code: why.code ?? \"forbidden\",\n reason: why.code && why.detail ? why.detail : `Not allowed to sync ${scope}`,\n });\n return queue.length;\n });\n }\n\n type ScopeResult = { offline: true } | { offline: false; waiting: number; blocked: number; rejected: number };\n\n async function syncScope(\n scope: string,\n gen: number,\n signal: AbortSignal,\n withBlocked: boolean,\n ): Promise<ScopeResult> {\n const sent = await takeQueue(scope, withBlocked);\n const last = Number((await db.meta.get(lastKnowledgeKey(scope)))?.value ?? 0) || 0;\n const body = (changes: SyncWireChange[], page_cursor: SyncRequestBody[\"page_cursor\"]): SyncRequestBody => ({\n scope,\n last_knowledge: last,\n changes,\n refusals: true,\n client_now: new Date().toISOString(),\n ...(options.paged ? { paged: true, page_cursor } : {}),\n });\n let answer: SyncResponseBody;\n try {\n answer = await options.send(body(sent.map(toWire), null), signal);\n } catch (err) {\n await restore(sent);\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n const plan = planCode(err);\n if (plan) {\n // The whole request refused for the plan: every change waits for it.\n await mark(sent, { status: \"waiting\", code: plan, reason: plan });\n return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };\n }\n if (httpStatusOf(err) === 403)\n return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };\n throw err;\n }\n let cursor = options.paged ? (answer.page_cursor ?? null) : null;\n let tally: ScopeResult;\n try {\n tally = { offline: false, ...(await apply(scope, answer, sent, gen, cursor == null)) };\n } catch (err) {\n if (err instanceof Dropped) await restore(sent);\n throw err;\n }\n // The rest of a paged pull: pull-only; the cursor is written with the last page alone,\n // so an interrupted walk starts again from the old one (D12).\n while (cursor) {\n let page: SyncResponseBody;\n try {\n page = await options.send(body([], cursor), signal);\n } catch (err) {\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n throw err;\n }\n cursor = page.page_cursor ?? null;\n await apply(scope, page, [], gen, cursor == null);\n }\n return tally;\n }\n\n // ── The round ──────────────────────────────────────────────────────────────\n\n async function round(withBlocked: boolean, gen: number): Promise<SyncRoundResult> {\n const result: SyncRoundResult = { outcome: \"synced\", waiting: 0, blocked: 0, rejected: 0 };\n if (gen !== generation) return { ...result, outcome: \"dropped\" };\n if (options.paused?.()) return { ...result, outcome: \"paused\" };\n if (!browserOnline()) return { ...result, outcome: \"offline\" };\n const own = new AbortController();\n controller = own;\n set({ phase: \"syncing\" });\n try {\n for (let pass = 0; ; pass++) {\n for (const scope of options.scopes()) {\n if (gen !== generation) throw new Dropped();\n const r = await syncScope(scope, gen, own.signal, withBlocked);\n if (r.offline) return { ...result, outcome: \"offline\" };\n result.waiting += r.waiting;\n result.blocked += r.blocked;\n result.rejected += r.rejected;\n }\n if (pass > 0) break;\n let changed = false;\n for (const step of steps) {\n if (gen !== generation) throw new Dropped();\n const ran = await step.run(db, { signal: own.signal, isCurrent: () => gen === generation });\n if (gen !== generation) throw new Dropped();\n if (ran.counts) stepCounts = { ...stepCounts, [step.name]: ran.counts };\n if (ran.offline) return { ...result, outcome: \"offline\" };\n if (ran.changed) changed = true;\n }\n if (!changed) break;\n }\n return result;\n } catch (err) {\n if (err instanceof Dropped || gen !== generation) return { ...result, outcome: \"dropped\" };\n return { ...result, outcome: \"error\", error: err };\n } finally {\n if (controller === own) controller = null;\n }\n }\n\n function clearRetry() {\n if (retry) clearTimeout(retry);\n retry = null;\n }\n\n function resetBackoff() {\n failures = 0;\n clearRetry();\n if (state.nextRetryAt !== null) set({ nextRetryAt: null });\n }\n\n const inBackoff = () => state.nextRetryAt !== null && Date.now() < state.nextRetryAt;\n\n function settle(result: SyncRoundResult) {\n switch (result.outcome) {\n case \"synced\":\n failures = 0;\n clearRetry();\n set({ phase: \"idle\", online: true, nextRetryAt: null });\n break;\n case \"offline\":\n // No retry timer offline: the `online` event or the poll brings the next round (D13).\n clearRetry();\n set({ phase: \"offline\", online: false, nextRetryAt: null });\n break;\n case \"error\": {\n const wait = backoffMs[Math.min(failures, backoffMs.length - 1)];\n failures += 1;\n clearRetry();\n if (started) {\n retry = setTimeout(() => {\n retry = null;\n void run(false);\n }, wait);\n }\n set({ phase: \"error\", nextRetryAt: started ? Date.now() + wait : null });\n break;\n }\n case \"dropped\":\n set({ phase: \"idle\" });\n break;\n case \"paused\":\n break;\n }\n }\n\n function run(withBlocked: boolean): Promise<SyncRoundResult> {\n if (current) {\n again = true;\n return current;\n }\n const before = flights.get(db.name);\n // The generation is read before any wait: a stop() while this waits drops the round, so\n // it never sends this copy's queue with another account's client nor pulls into it.\n const gen = generation;\n const flight = (async () => {\n // Another engine's round on this database (stopped, still settling) ends first.\n if (before) await before.catch(() => undefined);\n try {\n const result = await round(withBlocked, gen);\n settle(result);\n await refreshCounts();\n return result;\n } finally {\n current = null;\n if (again) {\n again = false;\n tick();\n }\n }\n })();\n current = flight;\n flights.set(db.name, flight);\n const done = () => {\n if (flights.get(db.name) === flight) flights.delete(db.name);\n };\n void flight.then(done, done);\n return flight;\n }\n\n /** A round from the poll, a write or a request during a round: not while backing off. */\n function tick() {\n if (started && !inBackoff()) void run(false);\n }\n\n async function runFresh(withBlocked: boolean): Promise<SyncRoundResult> {\n resetBackoff();\n if (current) {\n again = false;\n await current.catch(() => undefined);\n }\n return run(withBlocked);\n }\n\n const onOnline = () => {\n set({ online: true });\n resetBackoff();\n void run(false);\n };\n const onOffline = () => set({ online: false });\n\n const engine: SyncEngine = {\n async write(scope, entity, id, op, data = {}) {\n await db.transaction(\"rw\", db.rows, db.outbox, async () => {\n const row = (await db.rows.get([entity, id])) as CopyRow | undefined;\n const ofRow = await db.outbox.where(\"[entity+id]\").equals([entity, id]).toArray();\n ofRow.sort((a, b) => a.seq! - b.seq!);\n const updated_at = stampAfter(\n latest([row?.updated_at, row?.data.updated_at, ...ofRow.map((c) => c.updated_at)]),\n );\n // A new edit to the row moves a rejected change out of the way (D13).\n const rejected = ofRow.filter((c) => c.scope === scope && c.status === \"rejected\");\n if (op === \"delete\") {\n await db.rows.delete([entity, id]);\n const neverSynced = row != null && (row.local ?? row.pk == null);\n if (neverSynced && !ofRow.some((c) => c.status === \"sending\")) {\n // Never reached the server: forgetting its queue is enough — a waiting change\n // included, or it would create the row once the plan is back.\n await db.outbox.bulkDelete(ofRow.map((c) => c.seq!));\n return;\n }\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n await db.outbox.add({ scope, entity, id, op, data: {}, updated_at, status: \"pending\" });\n return;\n }\n const next: CopyRow = { ...row, entity, id, pk: row?.pk ?? null, data: { ...row?.data, ...data } };\n if (!row) next.local = true;\n await db.rows.put(next);\n const carried = Object.assign({}, ...rejected.filter((c) => c.op === \"upsert\").map((c) => c.data)) as Record<\n string,\n unknown\n >;\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n const last = ofRow.filter((c) => c.scope === scope && c.status === \"pending\").at(-1);\n if (last && last.op === \"upsert\") {\n // Several edits before a round go up as one change, with the last one's time.\n await db.outbox.update(last.seq!, { data: { ...carried, ...last.data, ...data }, updated_at });\n } else {\n await db.outbox.add({ scope, entity, id, op, data: { ...carried, ...data }, updated_at, status: \"pending\" });\n }\n });\n engine.requestSync();\n },\n\n start() {\n if (started || sharedComputer()) return;\n const other = running.get(db.name);\n if (other && other !== engine) other.stop();\n running.set(db.name, engine);\n started = true;\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", onOnline);\n window.addEventListener(\"offline\", onOffline);\n }\n poll = setInterval(tick, pollMs);\n void refreshCounts();\n void run(false);\n },\n\n stop() {\n if (!started) return;\n started = false;\n // The round in flight writes nothing more: its request is aborted and its answer\n // dropped, the queue laid back; the next engine's round waits for it to settle.\n generation += 1;\n controller?.abort();\n if (running.get(db.name) === engine) running.delete(db.name);\n if (poll) clearInterval(poll);\n poll = null;\n if (soon) clearTimeout(soon);\n soon = null;\n clearRetry();\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", onOnline);\n window.removeEventListener(\"offline\", onOffline);\n }\n },\n\n requestSync() {\n void refreshCounts();\n if (!started) return;\n if (soon) clearTimeout(soon);\n soon = setTimeout(() => {\n soon = null;\n tick();\n }, soonMs);\n },\n\n syncNow: () => runFresh(false),\n\n async retryBlocked() {\n await runFresh(true);\n },\n\n async discardRejected(scope) {\n const gone = (await db.outbox.where(\"status\").equals(\"rejected\").toArray()).filter(\n (c) => !scope || c.scope === scope,\n );\n if (gone.length === 0) return;\n await db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n await db.outbox.bulkDelete(gone.map((c) => c.seq!));\n for (const change of gone) {\n const others = await db.outbox.where(\"[entity+id]\").equals([change.entity, change.id]).count();\n if (others === 0) await db.rows.delete([change.entity, change.id]);\n }\n // The scope pulls afresh, so the copy shows what the server holds.\n for (const s of new Set(gone.map((c) => c.scope))) await db.meta.put({ key: lastKnowledgeKey(s), value: 0 });\n });\n await refreshCounts();\n engine.requestSync();\n },\n\n async rejected() {\n return db.outbox.where(\"status\").equals(\"rejected\").sortBy(\"seq\");\n },\n\n async forget({ keepQueue }) {\n generation += 1;\n controller?.abort();\n const tables = keepQueue ? [db.rows, db.meta] : db.tables;\n await db.transaction(\"rw\", tables, async () => {\n for (const table of tables) await table.clear();\n });\n stepCounts = {};\n set({ lastSyncedAt: null });\n await refreshCounts();\n },\n\n getState: () => state,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n return engine;\n}\n\n/**\n * The engine kept for `options.db` (by its name), built on first use: one engine per\n * database for the session (D15), so a company switch A→B→A finds A's engine again instead\n * of building a second one beside a round in flight. The first call's options stand: read\n * changing values (scopes, the client) through functions. A database object opened afresh\n * under the same name replaces the kept engine (the old one is stopped).\n */\nexport function syncEngineFor<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const name = options.db.name;\n const found = kept.get(name);\n if (found && found.db === options.db) return found.engine;\n found?.engine.stop();\n const engine = createSyncEngine(options);\n kept.set(name, { db: options.db, engine });\n return engine;\n}\n\n/**\n * Let go of the engine {@link syncEngineFor} keeps for the database `name` (0.46): it is\n * stopped, its database closed and forgotten, so a later `syncEngineFor` builds afresh. Call it\n * on sign-out (and for a company left for good): every database visited in a tab otherwise\n * keeps its engine and an open IndexedDB connection until a reload. Nothing kept: a no-op.\n */\nexport function releaseSyncEngine(name: string): void {\n const found = kept.get(name);\n if (!found) return;\n kept.delete(name);\n found.engine.stop();\n found.db.close();\n}\n"],"mappings":"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","names":["kept"]}
|
package/dist/sync.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createSyncDb, rowByPk, SYNC_DB_PK_INDEX, SYNC_DB_STORES } from "./sync/db.js";
|
|
2
|
-
export type { CreateSyncDbOptions, OutboxChange, OutboxStatus, SyncDb, SyncDbUpgrade, SyncedRow, SyncMetaEntry } from "./sync/db.js";
|
|
3
|
-
export { createSyncEngine, DEFAULT_HOLD_CODES, DEFAULT_SYNC_BACKOFF_MS, releaseSyncEngine, syncEngineFor } from "./sync/engine.js";
|
|
2
|
+
export type { CreateSyncDbOptions, OutboxChange, OutboxStatus, SyncDb, SyncDbUpgrade, SyncedRow, SyncMetaEntry, } from "./sync/db.js";
|
|
3
|
+
export { createSyncEngine, DEFAULT_HOLD_CODES, DEFAULT_SYNC_BACKOFF_MS, releaseSyncEngine, syncEngineFor, } from "./sync/engine.js";
|
|
4
4
|
export type { SyncCounts, SyncEngine, SyncEngineOptions, SyncEngineState, SyncRoundResult, SyncStep, SyncStepContext, SyncStepResult, } from "./sync/engine.js";
|
|
5
5
|
export { useSyncQuery, useSyncStatus, useSyncWorker } from "./sync/hooks.js";
|
|
6
6
|
export type { SyncPageCursor, SyncRefusalBody, SyncRejectionBody, SyncRequestBody, SyncResponseBody, SyncWireChange, } from "./sync/wire.js";
|
package/dist/sync.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { createSyncDb, rowByPk, SYNC_DB_PK_INDEX, SYNC_DB_STORES } from "./sync/db.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createSyncEngine,
|
|
5
|
+
DEFAULT_HOLD_CODES,
|
|
6
|
+
DEFAULT_SYNC_BACKOFF_MS,
|
|
7
|
+
releaseSyncEngine,
|
|
8
|
+
syncEngineFor
|
|
9
|
+
} from "./sync/engine.js";
|
|
4
10
|
import { useSyncQuery, useSyncStatus, useSyncWorker } from "./sync/hooks.js";
|
|
5
11
|
import { isOfflineError, readLocalFirst, stampAfter, uuid5, writeLocalFirst } from "./offline/local-first.js";
|
|
6
12
|
export {
|
package/dist/sync.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sync.ts"],"sourcesContent":["// `@eifi1/ui-kit/sync` — local-first phase 3 (docs/local-first-harmonization.md §2.3, 0.46):\n// the sync engine on a Dexie copy, its worker and status hooks, and the live read of the copy.\n//\n// The only entry point that imports `dexie` — an optional peer for that reason. It never loads\n// `/offline`'s TanStack and idb-keyval peers: the leaf utilities both entries export live in\n// one module of their own (src/offline/local-first.ts). NOT re-exported from the main barrel.\nexport { createSyncDb, rowByPk, SYNC_DB_PK_INDEX, SYNC_DB_STORES } from \"./sync/db\";\nexport type {
|
|
1
|
+
{"version":3,"sources":["../src/sync.ts"],"sourcesContent":["// `@eifi1/ui-kit/sync` — local-first phase 3 (docs/local-first-harmonization.md §2.3, 0.46):\n// the sync engine on a Dexie copy, its worker and status hooks, and the live read of the copy.\n//\n// The only entry point that imports `dexie` — an optional peer for that reason. It never loads\n// `/offline`'s TanStack and idb-keyval peers: the leaf utilities both entries export live in\n// one module of their own (src/offline/local-first.ts). NOT re-exported from the main barrel.\nexport { createSyncDb, rowByPk, SYNC_DB_PK_INDEX, SYNC_DB_STORES } from \"./sync/db\";\nexport type {\n CreateSyncDbOptions,\n OutboxChange,\n OutboxStatus,\n SyncDb,\n SyncDbUpgrade,\n SyncedRow,\n SyncMetaEntry,\n} from \"./sync/db\";\nexport {\n createSyncEngine,\n DEFAULT_HOLD_CODES,\n DEFAULT_SYNC_BACKOFF_MS,\n releaseSyncEngine,\n syncEngineFor,\n} from \"./sync/engine\";\nexport type {\n SyncCounts,\n SyncEngine,\n SyncEngineOptions,\n SyncEngineState,\n SyncRoundResult,\n SyncStep,\n SyncStepContext,\n SyncStepResult,\n} from \"./sync/engine\";\nexport { useSyncQuery, useSyncStatus, useSyncWorker } from \"./sync/hooks\";\nexport type {\n SyncPageCursor,\n SyncRefusalBody,\n SyncRejectionBody,\n SyncRequestBody,\n SyncResponseBody,\n SyncWireChange,\n} from \"./sync/wire\";\nexport { isOfflineError, readLocalFirst, stampAfter, uuid5, writeLocalFirst } from \"./offline/local-first\";\n"],"mappings":";AAMA,SAAS,cAAc,SAAS,kBAAkB,sBAAsB;AAUxE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWP,SAAS,cAAc,eAAe,qBAAqB;AAS3D,SAAS,gBAAgB,gBAAgB,YAAY,OAAO,uBAAuB;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/css-variables.ts"],"sourcesContent":["import { KIT_NAMES, TAILWIND_THEME_NAMES } from \"./css-variables.generated\";\n\n/**\n * The guard against an undeclared CSS custom property (0.33,\n * docs/colour-roles-harmonization.md §8, §12.5).\n *\n * A `var(--name)` with no fallback, where nothing declares `--name`, makes the whole\n * declaration invalid at computed-value time: `fill` inherits, `color` inherits, a\n * background goes transparent. Kurvenschmiede's control diagram painted\n * `fill-[var(--surface)]` — a token that never existed (the kit's is `--bg-surface`) —\n * and drew black boxes on a public page. The class compiled, the build passed, the type\n * checker had no opinion, and nothing in any suite noticed. This is the check that\n * notices, run from an app's own test over the source texts the app collects.\n *\n * Browser-safe on purpose, like the rest of the kit: it takes TEXTS, not paths, so an app\n * reads its files however its setup allows (`import.meta.glob` with `?raw`, or `node:fs`)\n * and the kit carries no `node:fs`.\n */\n\n/** Every custom property the installed kit declares or publishes to apps.\n *\n * Generated from `tokens.css` when the kit is built (scripts/gen-kit-css-variables.mjs),\n * so it is always the installed version's, and an app never searches `node_modules`.\n * It leaves out two kinds of name on purpose:\n * - a name declared only in an `@theme inline` block (`--text-color-muted`,\n * `--background-color-surface`): Tailwind emits no variable for it, so\n * `var(--text-color-muted)` would pass a guard that counted it and paint nothing;\n * - AppShell's measured `--app-nav-h`, which exists only while an AppShell is mounted —\n * on a landing, legal, sign-in or pay page it is undefined. Read it with a fallback,\n * `var(--app-nav-h,0px)`, as every kit read does.\n * Everything `applyTokenSet` and More contrast write on `<html>` is in, through the\n * stylesheet's `:root` / `.dark` mirror. Component internals (`--slider-fill`, `--fp-*`,\n * `--icon-button-tone`, `--label-strip-edge`) are not: an app that reads one depends on\n * the kit's internals, and the guard says so. */\nexport const KIT_CSS_VARIABLES: ReadonlySet<string> = new Set(KIT_NAMES.split(\" \"));\n\n/** Tailwind's own theme variables (`--color-rose-500`, `--spacing`, `--text-sm` …), from\n * the `tailwindcss/theme.css` the kit builds with. Tailwind emits one when a scanned file\n * names it, so a `var()` of it resolves. */\nconst TAILWIND_THEME: ReadonlySet<string> = new Set(TAILWIND_THEME_NAMES.split(\" \"));\n\n/** A Tailwind PALETTE colour: a hue with a shade, black or white. `refusePalette` reports\n * these although they resolve — an app that has no palette lint asks this test to be\n * its refusal. */\nconst PALETTE_VARIABLE = /^--color-(?:[a-z]+-\\d{2,3}|black|white)$/;\n\n/** Read only where it is set: the guard accepts it in a fallback use, never bare. */\nconst APP_NAV_H = \"--app-nav-h\";\n\nexport interface UndeclaredCssVariablesOptions {\n /** Variables a library writes at runtime, as names or patterns:\n * `[\"--radix-collapsible-content-height\"]`. An app's own writes are found in its\n * source and need no listing. */\n runtime?: readonly (string | RegExp)[];\n /** Also check `var(--x, …)`. Off by default: a fallback says what happens when the\n * name is missing. `--app-nav-h` counts as declared only here. */\n checkFallbacks?: boolean;\n /** Report Tailwind palette variables (`--color-red-500`, `--color-white`) although\n * they resolve. Default false. */\n refusePalette?: boolean;\n}\n\n/** Comments are not CSS. `/* *\\/` in both; `//` in TS only (not after a `:`, so a URL in a\n * string survives). Replaced by spaces of the same shape, so line numbers hold. */\nfunction stripComments(text: string, isCss: boolean): string {\n const blank = (m: string) => m.replace(/[^\\n]/g, \" \");\n const noBlocks = text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, blank);\n return isCss ? noBlocks : noBlocks.replace(/(^|[^:])(\\/\\/[^\\n]*)/g, (_, pre: string, c: string) => pre + blank(c));\n}\n\n/** The text with every `@theme … inline … { … }` block blanked out, braces and all. */\nfunction withoutInlineTheme(css: string): string {\n let out = css;\n for (const m of css.matchAll(/@theme\\b[^{;]*\\binline\\b[^{;]*\\{/g)) {\n let depth = 0;\n let end = m.index + m[0].length - 1;\n for (; end < css.length; end++) {\n if (css[end] === \"{\") depth++;\n else if (css[end] === \"}\" && --depth === 0) break;\n }\n out = out.slice(0, m.index) + css.slice(m.index, end + 1).replace(/[^\\n]/g, \" \") + out.slice(end + 1);\n }\n return out;\n}\n\n/**\n * Every custom property a stylesheet declares (`--name:`), in any block — except a name\n * declared only inside `@theme inline`, which Tailwind never emits as a variable. Comments\n * are skipped. The same rule builds {@link KIT_CSS_VARIABLES} from `tokens.css`.\n */\nexport function declaredCustomProperties(css: string): Set<string> {\n const code = withoutInlineTheme(stripComments(css, true));\n return new Set([...code.matchAll(/(--[a-zA-Z0-9_-]+)\\s*:/g)].map((m) => m[1]));\n}\n\n/** What an app's source writes at runtime: `setProperty(\"--name\"`, a React style key\n * `\"--name\":`, a Tailwind arbitrary property `[--name:…]`. */\nfunction runtimeWrites(code: string): string[] {\n return [\n ...code.matchAll(/setProperty\\(\\s*[\"'`](--[a-zA-Z0-9_-]+)[\"'`]/g),\n ...code.matchAll(/[\"'](--[a-zA-Z0-9_-]+)[\"']\\s*:/g),\n ...code.matchAll(/\\[(--[a-zA-Z0-9_-]+):/g),\n ].map((m) => m[1]);\n}\n\n/** A chart series' colour (`var(--color-total)`) is declared in a file with a `total: {`\n * config entry: the kit's ChartContainer writes `--color-<key>` for each key. */\nfunction chartKeys(code: string): Set<string> {\n return new Set([...code.matchAll(/(?:^|[\\s{,])[\"']?([A-Za-z_][\\w-]*)[\"']?\\s*:\\s*\\{/g)].map((m) => m[1]));\n}\n\nconst isCssPath = (path: string) => /\\.css(?:\\?|$)/.test(path);\n\n/**\n * Every `var(--name)` without a fallback that nothing declares, as `\"path:line --name\"`.\n *\n * `sources` maps a path to its text: the app's `.ts`, `.tsx` and `.css` source, tests\n * left out. A name counts as declared when it is:\n * 1. the kit's ({@link KIT_CSS_VARIABLES});\n * 2. declared in any `.css` entry of `sources` (outside `@theme inline`);\n * 3. written by the source at runtime (`setProperty`, a style key, `[--name:…]`);\n * 4. one of Tailwind's theme variables — unless `refusePalette` and it is a palette colour;\n * 5. a chart series' `--color-<key>`, in a file with a `<key>: {` entry;\n * 6. listed in `options.runtime`.\n *\n * Not counted: comments; a use with a fallback, unless `checkFallbacks`; a name built at\n * runtime (`` `var(--chart-${n})` `` — a name must be followed by `)` or `,`).\n *\n * It REFUSES A VACUOUS RUN: it throws when `sources` holds fewer than ten files, or when\n * a `.css` entry is empty. Vitest stubs every stylesheet with an empty string by default,\n * `?raw` included, so an app that reads its CSS that way without opting it out of the stub\n * (`test.css.include`) would otherwise lose every declaration and every use in it, and\n * this would pass or fail for the wrong reason.\n */\nexport function undeclaredCssVariables(\n sources: Record<string, string>,\n options: UndeclaredCssVariablesOptions = {},\n): string[] {\n const { runtime = [], checkFallbacks = false, refusePalette = false } = options;\n const entries = Object.entries(sources);\n if (entries.length < 10) {\n throw new Error(\n `undeclaredCssVariables: only ${entries.length} source file(s) — the glob or the file list found ` +\n `(almost) nothing, and a check over nothing passes. Pass the app's .ts/.tsx/.css source.`,\n );\n }\n const emptyCss = entries.filter(([path, text]) => isCssPath(path) && text.trim() === \"\").map(([path]) => path);\n if (emptyCss.length) {\n throw new Error(\n `undeclaredCssVariables: ${emptyCss.join(\", \")} read as empty. Vitest stubs stylesheets with \"\" ` +\n `(\\`?raw\\` too) unless the file is listed in \\`test.css.include\\`.`,\n );\n }\n\n const code = entries.map(([path, text]) => [path, stripComments(text, isCssPath(path))] as const);\n const declared = new Set<string>();\n for (const [path, text] of code) {\n if (isCssPath(path)) for (const name of declaredCustomProperties(text)) declared.add(name);\n for (const name of runtimeWrites(text)) declared.add(name);\n }\n const listed = (name: string) =>\n runtime.some((r) => (typeof r === \"string\" ? r === name : r.test(name)));\n\n const out: string[] = [];\n for (const [path, text] of code) {\n let keys: Set<string> | null = null;\n for (const m of text.matchAll(/var\\(\\s*(--[a-zA-Z0-9_-]+)\\s*([,)])/g)) {\n const [, name, next] = m;\n const fallback = next === \",\";\n if (fallback && !checkFallbacks) continue;\n if (KIT_CSS_VARIABLES.has(name) || declared.has(name) || listed(name)) continue;\n if (fallback && name === APP_NAV_H) continue;\n if (TAILWIND_THEME.has(name)) {\n if (refusePalette && PALETTE_VARIABLE.test(name)) {\n out.push(`${path}:${lineOf(text, m.index)} ${name} (a Tailwind palette colour; refusePalette is on)`);\n }\n continue;\n }\n const chart = /^--color-([A-Za-z_][\\w-]*)$/.exec(name);\n if (chart && (keys ??= chartKeys(text)).has(chart[1])) continue;\n out.push(`${path}:${lineOf(text, m.index)} ${name}`);\n }\n }\n return out;\n}\n\nfunction lineOf(text: string, index: number): number {\n let line = 1;\n for (let i = 0; i < index; i++) if (text.charCodeAt(i) === 10) line++;\n return line;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/testing/css-variables.ts"],"sourcesContent":["import { KIT_NAMES, TAILWIND_THEME_NAMES } from \"./css-variables.generated\";\n\n/**\n * The guard against an undeclared CSS custom property (0.33,\n * docs/colour-roles-harmonization.md §8, §12.5).\n *\n * A `var(--name)` with no fallback, where nothing declares `--name`, makes the whole\n * declaration invalid at computed-value time: `fill` inherits, `color` inherits, a\n * background goes transparent. Kurvenschmiede's control diagram painted\n * `fill-[var(--surface)]` — a token that never existed (the kit's is `--bg-surface`) —\n * and drew black boxes on a public page. The class compiled, the build passed, the type\n * checker had no opinion, and nothing in any suite noticed. This is the check that\n * notices, run from an app's own test over the source texts the app collects.\n *\n * Browser-safe on purpose, like the rest of the kit: it takes TEXTS, not paths, so an app\n * reads its files however its setup allows (`import.meta.glob` with `?raw`, or `node:fs`)\n * and the kit carries no `node:fs`.\n */\n\n/** Every custom property the installed kit declares or publishes to apps.\n *\n * Generated from `tokens.css` when the kit is built (scripts/gen-kit-css-variables.mjs),\n * so it is always the installed version's, and an app never searches `node_modules`.\n * It leaves out two kinds of name on purpose:\n * - a name declared only in an `@theme inline` block (`--text-color-muted`,\n * `--background-color-surface`): Tailwind emits no variable for it, so\n * `var(--text-color-muted)` would pass a guard that counted it and paint nothing;\n * - AppShell's measured `--app-nav-h`, which exists only while an AppShell is mounted —\n * on a landing, legal, sign-in or pay page it is undefined. Read it with a fallback,\n * `var(--app-nav-h,0px)`, as every kit read does.\n * Everything `applyTokenSet` and More contrast write on `<html>` is in, through the\n * stylesheet's `:root` / `.dark` mirror. Component internals (`--slider-fill`, `--fp-*`,\n * `--icon-button-tone`, `--label-strip-edge`) are not: an app that reads one depends on\n * the kit's internals, and the guard says so. */\nexport const KIT_CSS_VARIABLES: ReadonlySet<string> = new Set(KIT_NAMES.split(\" \"));\n\n/** Tailwind's own theme variables (`--color-rose-500`, `--spacing`, `--text-sm` …), from\n * the `tailwindcss/theme.css` the kit builds with. Tailwind emits one when a scanned file\n * names it, so a `var()` of it resolves. */\nconst TAILWIND_THEME: ReadonlySet<string> = new Set(TAILWIND_THEME_NAMES.split(\" \"));\n\n/** A Tailwind PALETTE colour: a hue with a shade, black or white. `refusePalette` reports\n * these although they resolve — an app that has no palette lint asks this test to be\n * its refusal. */\nconst PALETTE_VARIABLE = /^--color-(?:[a-z]+-\\d{2,3}|black|white)$/;\n\n/** Read only where it is set: the guard accepts it in a fallback use, never bare. */\nconst APP_NAV_H = \"--app-nav-h\";\n\nexport interface UndeclaredCssVariablesOptions {\n /** Variables a library writes at runtime, as names or patterns:\n * `[\"--radix-collapsible-content-height\"]`. An app's own writes are found in its\n * source and need no listing. */\n runtime?: readonly (string | RegExp)[];\n /** Also check `var(--x, …)`. Off by default: a fallback says what happens when the\n * name is missing. `--app-nav-h` counts as declared only here. */\n checkFallbacks?: boolean;\n /** Report Tailwind palette variables (`--color-red-500`, `--color-white`) although\n * they resolve. Default false. */\n refusePalette?: boolean;\n}\n\n/** Comments are not CSS. `/* *\\/` in both; `//` in TS only (not after a `:`, so a URL in a\n * string survives). Replaced by spaces of the same shape, so line numbers hold. */\nfunction stripComments(text: string, isCss: boolean): string {\n const blank = (m: string) => m.replace(/[^\\n]/g, \" \");\n const noBlocks = text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, blank);\n return isCss ? noBlocks : noBlocks.replace(/(^|[^:])(\\/\\/[^\\n]*)/g, (_, pre: string, c: string) => pre + blank(c));\n}\n\n/** The text with every `@theme … inline … { … }` block blanked out, braces and all. */\nfunction withoutInlineTheme(css: string): string {\n let out = css;\n for (const m of css.matchAll(/@theme\\b[^{;]*\\binline\\b[^{;]*\\{/g)) {\n let depth = 0;\n let end = m.index + m[0].length - 1;\n for (; end < css.length; end++) {\n if (css[end] === \"{\") depth++;\n else if (css[end] === \"}\" && --depth === 0) break;\n }\n out = out.slice(0, m.index) + css.slice(m.index, end + 1).replace(/[^\\n]/g, \" \") + out.slice(end + 1);\n }\n return out;\n}\n\n/**\n * Every custom property a stylesheet declares (`--name:`), in any block — except a name\n * declared only inside `@theme inline`, which Tailwind never emits as a variable. Comments\n * are skipped. The same rule builds {@link KIT_CSS_VARIABLES} from `tokens.css`.\n */\nexport function declaredCustomProperties(css: string): Set<string> {\n const code = withoutInlineTheme(stripComments(css, true));\n return new Set([...code.matchAll(/(--[a-zA-Z0-9_-]+)\\s*:/g)].map((m) => m[1]));\n}\n\n/** What an app's source writes at runtime: `setProperty(\"--name\"`, a React style key\n * `\"--name\":`, a Tailwind arbitrary property `[--name:…]`. */\nfunction runtimeWrites(code: string): string[] {\n return [\n ...code.matchAll(/setProperty\\(\\s*[\"'`](--[a-zA-Z0-9_-]+)[\"'`]/g),\n ...code.matchAll(/[\"'](--[a-zA-Z0-9_-]+)[\"']\\s*:/g),\n ...code.matchAll(/\\[(--[a-zA-Z0-9_-]+):/g),\n ].map((m) => m[1]);\n}\n\n/** A chart series' colour (`var(--color-total)`) is declared in a file with a `total: {`\n * config entry: the kit's ChartContainer writes `--color-<key>` for each key. */\nfunction chartKeys(code: string): Set<string> {\n return new Set([...code.matchAll(/(?:^|[\\s{,])[\"']?([A-Za-z_][\\w-]*)[\"']?\\s*:\\s*\\{/g)].map((m) => m[1]));\n}\n\nconst isCssPath = (path: string) => /\\.css(?:\\?|$)/.test(path);\n\n/**\n * Every `var(--name)` without a fallback that nothing declares, as `\"path:line --name\"`.\n *\n * `sources` maps a path to its text: the app's `.ts`, `.tsx` and `.css` source, tests\n * left out. A name counts as declared when it is:\n * 1. the kit's ({@link KIT_CSS_VARIABLES});\n * 2. declared in any `.css` entry of `sources` (outside `@theme inline`);\n * 3. written by the source at runtime (`setProperty`, a style key, `[--name:…]`);\n * 4. one of Tailwind's theme variables — unless `refusePalette` and it is a palette colour;\n * 5. a chart series' `--color-<key>`, in a file with a `<key>: {` entry;\n * 6. listed in `options.runtime`.\n *\n * Not counted: comments; a use with a fallback, unless `checkFallbacks`; a name built at\n * runtime (`` `var(--chart-${n})` `` — a name must be followed by `)` or `,`).\n *\n * It REFUSES A VACUOUS RUN: it throws when `sources` holds fewer than ten files, or when\n * a `.css` entry is empty. Vitest stubs every stylesheet with an empty string by default,\n * `?raw` included, so an app that reads its CSS that way without opting it out of the stub\n * (`test.css.include`) would otherwise lose every declaration and every use in it, and\n * this would pass or fail for the wrong reason.\n */\nexport function undeclaredCssVariables(\n sources: Record<string, string>,\n options: UndeclaredCssVariablesOptions = {},\n): string[] {\n const { runtime = [], checkFallbacks = false, refusePalette = false } = options;\n const entries = Object.entries(sources);\n if (entries.length < 10) {\n throw new Error(\n `undeclaredCssVariables: only ${entries.length} source file(s) — the glob or the file list found ` +\n `(almost) nothing, and a check over nothing passes. Pass the app's .ts/.tsx/.css source.`,\n );\n }\n const emptyCss = entries.filter(([path, text]) => isCssPath(path) && text.trim() === \"\").map(([path]) => path);\n if (emptyCss.length) {\n throw new Error(\n `undeclaredCssVariables: ${emptyCss.join(\", \")} read as empty. Vitest stubs stylesheets with \"\" ` +\n `(\\`?raw\\` too) unless the file is listed in \\`test.css.include\\`.`,\n );\n }\n\n const code = entries.map(([path, text]) => [path, stripComments(text, isCssPath(path))] as const);\n const declared = new Set<string>();\n for (const [path, text] of code) {\n if (isCssPath(path)) for (const name of declaredCustomProperties(text)) declared.add(name);\n for (const name of runtimeWrites(text)) declared.add(name);\n }\n const listed = (name: string) => runtime.some((r) => (typeof r === \"string\" ? r === name : r.test(name)));\n\n const out: string[] = [];\n for (const [path, text] of code) {\n let keys: Set<string> | null = null;\n for (const m of text.matchAll(/var\\(\\s*(--[a-zA-Z0-9_-]+)\\s*([,)])/g)) {\n const [, name, next] = m;\n const fallback = next === \",\";\n if (fallback && !checkFallbacks) continue;\n if (KIT_CSS_VARIABLES.has(name) || declared.has(name) || listed(name)) continue;\n if (fallback && name === APP_NAV_H) continue;\n if (TAILWIND_THEME.has(name)) {\n if (refusePalette && PALETTE_VARIABLE.test(name)) {\n out.push(`${path}:${lineOf(text, m.index)} ${name} (a Tailwind palette colour; refusePalette is on)`);\n }\n continue;\n }\n const chart = /^--color-([A-Za-z_][\\w-]*)$/.exec(name);\n if (chart && (keys ??= chartKeys(text)).has(chart[1])) continue;\n out.push(`${path}:${lineOf(text, m.index)} ${name}`);\n }\n }\n return out;\n}\n\nfunction lineOf(text: string, index: number): number {\n let line = 1;\n for (let i = 0; i < index; i++) if (text.charCodeAt(i) === 10) line++;\n return line;\n}\n"],"mappings":"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","names":[]}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { KIT_CSS_VARIABLES, declaredCustomProperties, undeclaredCssVariables, type UndeclaredCssVariablesOptions, } from "./css-variables.js";
|
|
2
2
|
export { setViewportWidth, stubResizeObserver, stubViewportWidth, viewportMatchMedia, type ResizeObserverStub, type StubbedBoxSize, type ViewportOptions, } from "./viewport.js";
|
|
3
|
-
export { tourStepContext, type TestTourStepContext, type TourStepContextOptions, type TourStepVia
|
|
3
|
+
export { tourStepContext, type TestTourStepContext, type TourStepContextOptions, type TourStepVia } from "./tour.js";
|
package/dist/testing/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/index.ts"],"sourcesContent":["// `@eifi1/ui-kit/testing` — helpers an app runs from its OWN test suite (0.33).\n//\n// Not part of the main barrel: nothing here renders, and an app's production bundle has\n// no business carrying it. Browser-safe like the rest of the kit (no `node:fs`): each\n// helper takes texts, so an app collects them with `import.meta.glob` or `node:fs`, as\n// its setup allows. The first content is the guard against undeclared CSS custom\n// properties (docs/colour-roles-harmonization.md §8).\nexport {\n KIT_CSS_VARIABLES,\n declaredCustomProperties,\n undeclaredCssVariables,\n type UndeclaredCssVariablesOptions,\n} from \"./css-variables\";\n// 0.34 (refactor audit 2026-10, TST-10): the environment jsdom lacks — a `matchMedia`\n// that answers from a viewport width (right at every text size) and a `ResizeObserver`\n// the test drives. Framework-free: each returns its own undo. 0.35: `setViewportWidth`,\n// a live resize of the stub in force — its lists flip and fire `change` (keksdose).\nexport {\n setViewportWidth,\n stubResizeObserver,\n stubViewportWidth,\n viewportMatchMedia,\n type ResizeObserverStub,\n type StubbedBoxSize,\n type ViewportOptions,\n} from \"./viewport\";\n// 0.50.1 (keksdose N4): a valid `TourStepContext` for a test that calls a step's\n// `beforeStep` or `when` itself — `beforeStep!(tourStepContext())`.\nexport {
|
|
1
|
+
{"version":3,"sources":["../../src/testing/index.ts"],"sourcesContent":["// `@eifi1/ui-kit/testing` — helpers an app runs from its OWN test suite (0.33).\n//\n// Not part of the main barrel: nothing here renders, and an app's production bundle has\n// no business carrying it. Browser-safe like the rest of the kit (no `node:fs`): each\n// helper takes texts, so an app collects them with `import.meta.glob` or `node:fs`, as\n// its setup allows. The first content is the guard against undeclared CSS custom\n// properties (docs/colour-roles-harmonization.md §8).\nexport {\n KIT_CSS_VARIABLES,\n declaredCustomProperties,\n undeclaredCssVariables,\n type UndeclaredCssVariablesOptions,\n} from \"./css-variables\";\n// 0.34 (refactor audit 2026-10, TST-10): the environment jsdom lacks — a `matchMedia`\n// that answers from a viewport width (right at every text size) and a `ResizeObserver`\n// the test drives. Framework-free: each returns its own undo. 0.35: `setViewportWidth`,\n// a live resize of the stub in force — its lists flip and fire `change` (keksdose).\nexport {\n setViewportWidth,\n stubResizeObserver,\n stubViewportWidth,\n viewportMatchMedia,\n type ResizeObserverStub,\n type StubbedBoxSize,\n type ViewportOptions,\n} from \"./viewport\";\n// 0.50.1 (keksdose N4): a valid `TourStepContext` for a test that calls a step's\n// `beforeStep` or `when` itself — `beforeStep!(tourStepContext())`.\nexport { tourStepContext, type TestTourStepContext, type TourStepContextOptions, type TourStepVia } from \"./tour\";\n"],"mappings":";AAOA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAKP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAGP,SAAS,uBAAgG;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/react.ts"],"sourcesContent":["import { fireEvent, screen, within } from \"@testing-library/react\";\n\n// `@eifi1/ui-kit/testing/react` (0.50) — helpers that drive the kit's components through\n// Testing Library, from an app's own test suite.\n//\n// A sibling of `@eifi1/ui-kit/testing`, not part of it: that entry is pure (it reaches no\n// package, so it runs under node or jsdom alike), and this one needs\n// `@testing-library/react`, an optional peer only a test suite installs. Neither is part of\n// the barrel, so an app's production bundle never carries either.\n\n/** The words of the row's \"⋯\" button, when the app's tests render translated kit labels. */\nexport interface RowMenuLabels {\n /** The button's name on a row with no `name`. Default \"Actions\". */\n actions?: string;\n /** The button's name given the row's. Default `Actions for ${name}`. */\n actionsFor?: (name: string) => string;\n}\n\n// The kit's English defaults (DEFAULT_ROW_ACTIONS_LABELS, and the user roster's), written\n// out rather than imported: the label module's type import would walk this entry into the\n// components. src/testing/__tests__/react.test.tsx holds them equal.\nconst ACTIONS = \"Actions\";\nconst ACTIONS_FOR = (name: string) => `Actions for ${name}`;\n\n/**\n * Open a row's \"⋯\" menu and find one of its actions — the entry, unclicked, so a test can\n * check it (locked, described) before it clicks it.\n *\n * `row` is the row's name as the app gave it to `RowActions` / `rowActionsColumn` /\n * `UserRoster` (\"Actions for <row>\"); `undefined` takes the FIRST \"⋯\" on the page, named\n * or not. Since 0.49 (KIT-12) a `tone: \"danger\"` action lives in that menu rather than as\n * an icon in the row, and at Large text every action does.\n *\n * The entries are BUTTONS in a dialog popover named like the \"⋯\" button — not `menuitem`s:\n * `getByRole(\"menuitem\")` finds nothing there. A danger action asks for confirmation once\n * clicked (an `alertdialog`), unless the app turned that off.\n *\n * @example\n * fireEvent.click(await rowMenuAction(\"Ada Example\", \"Delete\"));\n * fireEvent.click(await screen.findByRole(\"button\", { name: \"Confirm\" }));\n */\nexport async function rowMenuAction(\n row: string | undefined,\n action: string | RegExp,\n labels: RowMenuLabels = {},\n): Promise<HTMLElement> {\n const actions = labels.actions ?? ACTIONS;\n const actionsFor = labels.actionsFor ?? ACTIONS_FOR;\n // Any row's name: the words around the name, whatever the language puts first.\n const [before, after] = actionsFor(\"\\u0000\").split(\"\\u0000\");\n const anyRow = (name: string) =>\n name === actions
|
|
1
|
+
{"version":3,"sources":["../../src/testing/react.ts"],"sourcesContent":["import { fireEvent, screen, within } from \"@testing-library/react\";\n\n// `@eifi1/ui-kit/testing/react` (0.50) — helpers that drive the kit's components through\n// Testing Library, from an app's own test suite.\n//\n// A sibling of `@eifi1/ui-kit/testing`, not part of it: that entry is pure (it reaches no\n// package, so it runs under node or jsdom alike), and this one needs\n// `@testing-library/react`, an optional peer only a test suite installs. Neither is part of\n// the barrel, so an app's production bundle never carries either.\n\n/** The words of the row's \"⋯\" button, when the app's tests render translated kit labels. */\nexport interface RowMenuLabels {\n /** The button's name on a row with no `name`. Default \"Actions\". */\n actions?: string;\n /** The button's name given the row's. Default `Actions for ${name}`. */\n actionsFor?: (name: string) => string;\n}\n\n// The kit's English defaults (DEFAULT_ROW_ACTIONS_LABELS, and the user roster's), written\n// out rather than imported: the label module's type import would walk this entry into the\n// components. src/testing/__tests__/react.test.tsx holds them equal.\nconst ACTIONS = \"Actions\";\nconst ACTIONS_FOR = (name: string) => `Actions for ${name}`;\n\n/**\n * Open a row's \"⋯\" menu and find one of its actions — the entry, unclicked, so a test can\n * check it (locked, described) before it clicks it.\n *\n * `row` is the row's name as the app gave it to `RowActions` / `rowActionsColumn` /\n * `UserRoster` (\"Actions for <row>\"); `undefined` takes the FIRST \"⋯\" on the page, named\n * or not. Since 0.49 (KIT-12) a `tone: \"danger\"` action lives in that menu rather than as\n * an icon in the row, and at Large text every action does.\n *\n * The entries are BUTTONS in a dialog popover named like the \"⋯\" button — not `menuitem`s:\n * `getByRole(\"menuitem\")` finds nothing there. A danger action asks for confirmation once\n * clicked (an `alertdialog`), unless the app turned that off.\n *\n * @example\n * fireEvent.click(await rowMenuAction(\"Ada Example\", \"Delete\"));\n * fireEvent.click(await screen.findByRole(\"button\", { name: \"Confirm\" }));\n */\nexport async function rowMenuAction(\n row: string | undefined,\n action: string | RegExp,\n labels: RowMenuLabels = {},\n): Promise<HTMLElement> {\n const actions = labels.actions ?? ACTIONS;\n const actionsFor = labels.actionsFor ?? ACTIONS_FOR;\n // Any row's name: the words around the name, whatever the language puts first.\n const [before, after] = actionsFor(\"\\u0000\").split(\"\\u0000\");\n const anyRow = (name: string) =>\n name === actions || (name.length > before.length + after.length && name.startsWith(before) && name.endsWith(after));\n const trigger = (await screen.findAllByRole(\"button\", { name: row === undefined ? anyRow : actionsFor(row) }))[0];\n fireEvent.click(trigger);\n const title = trigger.getAttribute(\"aria-label\") ?? trigger.textContent ?? \"\";\n const menu = await screen.findByRole(\"dialog\", { name: title });\n return within(menu).getByRole(\"button\", { name: action });\n}\n"],"mappings":";AAAA,SAAS,WAAW,QAAQ,cAAc;AAqB1C,MAAM,UAAU;AAChB,MAAM,cAAc,CAAC,SAAiB,eAAe,IAAI;AAmBzD,eAAsB,cACpB,KACA,QACA,SAAwB,CAAC,GACH;AACtB,QAAM,UAAU,OAAO,WAAW;AAClC,QAAM,aAAa,OAAO,cAAc;AAExC,QAAM,CAAC,QAAQ,KAAK,IAAI,WAAW,IAAQ,EAAE,MAAM,IAAQ;AAC3D,QAAM,SAAS,CAAC,SACd,SAAS,WAAY,KAAK,SAAS,OAAO,SAAS,MAAM,UAAU,KAAK,WAAW,MAAM,KAAK,KAAK,SAAS,KAAK;AACnH,QAAM,WAAW,MAAM,OAAO,cAAc,UAAU,EAAE,MAAM,QAAQ,SAAY,SAAS,WAAW,GAAG,EAAE,CAAC,GAAG,CAAC;AAChH,YAAU,MAAM,OAAO;AACvB,QAAM,QAAQ,QAAQ,aAAa,YAAY,KAAK,QAAQ,eAAe;AAC3E,QAAM,OAAO,MAAM,OAAO,WAAW,UAAU,EAAE,MAAM,MAAM,CAAC;AAC9D,SAAO,OAAO,IAAI,EAAE,UAAU,UAAU,EAAE,MAAM,OAAO,CAAC;AAC1D;","names":[]}
|
package/dist/testing/viewport.js
CHANGED
|
@@ -49,9 +49,7 @@ function condition(part, width, options) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
function answer(query, width, options) {
|
|
52
|
-
return query.split(",").some(
|
|
53
|
-
(alternative) => alternative.split(/\s+and\s+/).every((part) => condition(part, width, options))
|
|
54
|
-
);
|
|
52
|
+
return query.split(",").some((alternative) => alternative.split(/\s+and\s+/).every((part) => condition(part, width, options)));
|
|
55
53
|
}
|
|
56
54
|
const VIEWPORTS = /* @__PURE__ */ new WeakMap();
|
|
57
55
|
function mediaQueryList(query, viewport) {
|
|
@@ -87,7 +85,12 @@ function mediaQueryList(query, viewport) {
|
|
|
87
85
|
}
|
|
88
86
|
function changeEvent(list) {
|
|
89
87
|
const event = new Event("change");
|
|
90
|
-
for (const [key, value] of Object.entries({
|
|
88
|
+
for (const [key, value] of Object.entries({
|
|
89
|
+
matches: list.matches,
|
|
90
|
+
media: list.media,
|
|
91
|
+
target: list,
|
|
92
|
+
currentTarget: list
|
|
93
|
+
})) {
|
|
91
94
|
Object.defineProperty(event, key, { value, enumerable: true });
|
|
92
95
|
}
|
|
93
96
|
return event;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/viewport.ts"],"sourcesContent":["/**\n * Environment stubs for an app's own tests (0.34, refactor audit 2026-10, TST-10): the\n * two browser APIs jsdom does not have and the kit's layout reads — `matchMedia` and\n * `ResizeObserver`.\n *\n * Every suite rebuilt them by hand: the kit stubbed `matchMedia` in 25 files and defined\n * the same `stubPhone()` eight times, keksdose stubs it in 41, and most copies answer\n * one literal string. That breaks quietly at Large text, where `md` is 960 px and the\n * phone query the kit writes is `(width < 960px)`, not `(width < 768px)`. These answer\n * every width query from a WIDTH, as a browser does, so they stay right at every text\n * size (docs/text-size-harmonization.md §3.3). The reference is keksdose's\n * `frontend/src/test/viewport.ts`, which this generalises.\n *\n * Framework-free, like the rest of `@eifi1/ui-kit/testing`: nothing here imports vitest\n * or jest. Each stub returns its own undo, so it works under either runner, and a suite\n * that prefers `vi.stubGlobal` passes it {@link viewportMatchMedia} instead.\n */\n\n/** The media features a test may want besides the width. Each is off by default. */\nexport interface ViewportOptions {\n /** The primary pointer is a finger: `(pointer: coarse)`, `(any-pointer: coarse)` and\n * `(hover: none)` match; `(pointer: fine)` and `(hover: hover)` do not. */\n coarse?: boolean;\n /** The person asked for less motion: `(prefers-reduced-motion: reduce)` matches, and\n * the kit's overlays skip their exit animations. */\n reducedMotion?: boolean;\n /** `(prefers-color-scheme: dark)` matches (`light` does not). */\n dark?: boolean;\n /** `(prefers-contrast: more)` matches. */\n moreContrast?: boolean;\n}\n\n/** One length in CSS px. A media query's `rem`/`em` is the browser's default font size,\n * 16 px, never `<html>`'s — the kit's own `breakpointQuery` writes px. */\nfunction lengthPx(value: string, unit: string): number {\n return unit === \"px\" ? Number(value) : Number(value) * 16;\n}\n\nconst RANGE = /^\\(\\s*width\\s*(<=|>=|<|>)\\s*([\\d.]+)(px|r?em)\\s*\\)$/;\nconst MIN_MAX = /^\\(\\s*(min|max)-width\\s*:\\s*([\\d.]+)(px|r?em)\\s*\\)$/;\nconst FEATURE = /^\\(\\s*([a-z-]+)\\s*:\\s*([a-z-]+)\\s*\\)$/;\n\n/** One `(…)` condition, or a media type. */\nfunction condition(part: string, width: number, options: ViewportOptions): boolean {\n const q = part.trim();\n if (q === \"all\" || q === \"screen\") return true;\n const range = RANGE.exec(q);\n if (range) {\n const px = lengthPx(range[2], range[3]);\n switch (range[1]) {\n case \"<\":\n return width < px;\n case \"<=\":\n return width <= px;\n case \">\":\n return width > px;\n default:\n return width >= px;\n }\n }\n const minMax = MIN_MAX.exec(q);\n if (minMax) {\n const px = lengthPx(minMax[2], minMax[3]);\n return minMax[1] === \"min\" ? width >= px : width <= px;\n }\n const feature = FEATURE.exec(q);\n if (!feature) return false;\n const [, name, value] = feature;\n const coarse = Boolean(options.coarse);\n switch (name) {\n case \"pointer\":\n case \"any-pointer\":\n return value === (coarse ? \"coarse\" : \"fine\");\n case \"hover\":\n case \"any-hover\":\n return value === (coarse ? \"none\" : \"hover\");\n case \"prefers-reduced-motion\":\n return value === (options.reducedMotion ? \"reduce\" : \"no-preference\");\n case \"prefers-color-scheme\":\n return value === (options.dark ? \"dark\" : \"light\");\n case \"prefers-contrast\":\n return value === (options.moreContrast ? \"more\" : \"no-preference\");\n default:\n return false;\n }\n}\n\n/** A whole query: comma lists are alternatives, `and` joins conditions, as in CSS. */\nfunction answer(query: string, width: number, options: ViewportOptions): boolean {\n return query\n .split(\",\")\n .some((alternative) =>\n alternative.split(/\\s+and\\s+/).every((part) => condition(part, width, options)),\n );\n}\n\n/** A list {@link viewportMatchMedia} handed out: what {@link setViewportWidth} flips. */\ninterface StubMediaQueryList {\n matches: boolean;\n readonly media: string;\n onchange: ((this: MediaQueryList, event: MediaQueryListEvent) => unknown) | null;\n addEventListener(type: string, listener: EventListenerOrEventListenerObject | null): void;\n removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null): void;\n addListener(listener: EventListenerOrEventListenerObject | null): void;\n removeListener(listener: EventListenerOrEventListenerObject | null): void;\n dispatchEvent(event: Event): boolean;\n}\n\n/** One stub's viewport: its width now, and every list it has handed out. */\ninterface StubViewport {\n width: number;\n readonly options: ViewportOptions;\n readonly lists: Set<StubMediaQueryList>;\n}\n\n/** Each `matchMedia` {@link viewportMatchMedia} made → its viewport. Keyed by the function,\n * so {@link setViewportWidth} finds whichever stub is in force however it was put in. */\nconst VIEWPORTS = new WeakMap<object, StubViewport>();\n\nfunction mediaQueryList(query: string, viewport: StubViewport): StubMediaQueryList {\n // One set for both APIs: in a browser `addListener(fn)` IS `addEventListener(\"change\",\n // fn)`, so adding the same function through both registers it once.\n const listeners = new Set<EventListenerOrEventListenerObject>();\n const list: StubMediaQueryList = {\n matches: answer(query, viewport.width, viewport.options),\n media: query,\n onchange: null,\n addEventListener(type, listener) {\n if (type === \"change\" && listener) listeners.add(listener);\n },\n removeEventListener(type, listener) {\n if (type === \"change\" && listener) listeners.delete(listener);\n },\n addListener(listener) {\n if (listener) listeners.add(listener);\n },\n removeListener(listener) {\n if (listener) listeners.delete(listener);\n },\n dispatchEvent(event) {\n if (event.type !== \"change\") return true;\n const self = list as unknown as MediaQueryList;\n list.onchange?.call(self, event as MediaQueryListEvent);\n for (const listener of [...listeners]) {\n if (typeof listener === \"function\") listener.call(self, event);\n else listener.handleEvent(event);\n }\n return !event.defaultPrevented;\n },\n };\n return list;\n}\n\n/** The `change` a browser fires at a list whose answer flipped: a `MediaQueryListEvent`'s\n * `matches` and `media`, on an `Event` (jsdom has no `MediaQueryListEvent`). */\nfunction changeEvent(list: StubMediaQueryList): Event {\n const event = new Event(\"change\");\n for (const [key, value] of Object.entries({ matches: list.matches, media: list.media, target: list, currentTarget: list })) {\n Object.defineProperty(event, key, { value, enumerable: true });\n }\n return event;\n}\n\n/**\n * A `window.matchMedia` for a viewport `width` CSS px wide.\n *\n * Width queries answer from `width`: the range syntax the kit writes for `max-*`\n * (`(width < 768px)`) and every other comparison, `(min-width: …)` and `(max-width: …)`,\n * in px or rem; `and` and comma lists combine as in CSS. The features in `options` answer\n * as set there; every other query is false. The lists keep their listeners, and\n * {@link setViewportWidth} resizes the viewport under them the way a browser window\n * does: each list whose answer flips fires `change`.\n *\n * Hand it to the runner's own global stub where the suite undoes stubs that way —\n * `vi.stubGlobal(\"matchMedia\", viewportMatchMedia(375))` — or use\n * {@link stubViewportWidth}.\n */\nexport function viewportMatchMedia(\n width: number,\n options: ViewportOptions = {},\n): (query: string) => MediaQueryList {\n const viewport: StubViewport = { width, options, lists: new Set() };\n const matchMedia = (query: string) => {\n const list = mediaQueryList(query, viewport);\n viewport.lists.add(list);\n return list as unknown as MediaQueryList;\n };\n VIEWPORTS.set(matchMedia, viewport);\n return matchMedia;\n}\n\n/**\n * Resize the stubbed viewport to `width` CSS px, live — the window being dragged narrower,\n * a phone turned (0.35, keksdose's 0.34 report). Every list the stub in force has handed\n * out answers from the new width, and each one whose answer flipped fires `change` to its\n * `addEventListener` listeners, its legacy `addListener` ones and its `onchange`, as in a\n * browser; a list whose answer stayed the same tells nobody. So a mounted component that\n * listens to a breakpoint (`usePhoneLayout()`, `useBreakpoint()`) switches without being\n * rendered again.\n *\n * The stub in force is whatever `window.matchMedia` is now — one {@link stubViewportWidth}\n * or `vi.stubGlobal(\"matchMedia\", viewportMatchMedia(…))` put in; its features\n * (`options`) stay as they were. Throws when `matchMedia` is not one of these stubs, rather\n * than resize nothing. Synchronous: wrap it in `act()` when a listener sets state.\n *\n * ```ts\n * undo = stubViewportWidth(1280);\n * render(<Page />);\n * act(() => setViewportWidth(375));\n * expect(screen.getByRole(\"list\")).toBeInTheDocument(); // the cards\n * ```\n */\nexport function setViewportWidth(width: number): void {\n const viewport = typeof window === \"undefined\" ? undefined : VIEWPORTS.get(window.matchMedia);\n if (!viewport) {\n throw new Error(\n \"setViewportWidth: no viewport stub is in force — put one in first with \" +\n \"stubViewportWidth(width), or vi.stubGlobal(\\\"matchMedia\\\", viewportMatchMedia(width)).\",\n );\n }\n viewport.width = width;\n for (const list of [...viewport.lists]) {\n const matches = answer(list.media, width, viewport.options);\n if (matches === list.matches) continue;\n list.matches = matches;\n list.dispatchEvent(changeEvent(list));\n }\n}\n\n/**\n * Make `window.matchMedia` answer as a viewport `width` CSS px wide (see\n * {@link viewportMatchMedia}), and return the undo: it puts back what was there, which\n * in jsdom is nothing — no `matchMedia` at all, the desktop fallback of every kit hook.\n *\n * ```ts\n * let undo = () => {};\n * afterEach(() => undo());\n * it(\"shows cards on a phone\", () => {\n * undo = stubViewportWidth(375);\n * …\n * });\n * ```\n *\n * Right at every text size: at Large the kit's phone query is `(width < 960px)`, which a\n * 375 px viewport matches and a 1280 px one does not, as in a browser.\n */\nexport function stubViewportWidth(width: number, options: ViewportOptions = {}): () => void {\n const before = Object.getOwnPropertyDescriptor(window, \"matchMedia\");\n Object.defineProperty(window, \"matchMedia\", {\n configurable: true,\n writable: true,\n value: viewportMatchMedia(width, options),\n });\n return () => {\n if (before) Object.defineProperty(window, \"matchMedia\", before);\n else Reflect.deleteProperty(window, \"matchMedia\");\n };\n}\n\n/** A box size for {@link ResizeObserverStub.resize}, in CSS px. */\nexport interface StubbedBoxSize {\n width: number;\n height: number;\n}\n\n/** What {@link stubResizeObserver} returns: the test drives the observers. */\nexport interface ResizeObserverStub {\n /**\n * Report `target` at `size` to every live observer watching it, as a browser does once\n * layout settles: one entry, with `contentRect` and the three box sizes all `size`.\n * Synchronous; wrap it in `act()` when the callback sets state.\n */\n resize(target: Element, size: StubbedBoxSize): void;\n /** The elements some live observer watches now, each once. */\n observed(): Element[];\n /** Put back the `ResizeObserver` that was there before — in jsdom, none. */\n restore(): void;\n}\n\n/**\n * Install a `ResizeObserver` the test drives. jsdom has none, and lays nothing out, so a\n * component that measures itself (a windowed list, a fitted chart, a measured grid)\n * either crashes on the missing global or never learns a size. Observers here record\n * what they watch and are called only by {@link ResizeObserverStub.resize}.\n *\n * ```ts\n * const ro = stubResizeObserver();\n * afterEach(() => ro.restore());\n * act(() => ro.resize(screen.getByRole(\"list\"), { width: 400, height: 320 }));\n * ```\n */\nexport function stubResizeObserver(): ResizeObserverStub {\n const live = new Set<StubResizeObserver>();\n\n class StubResizeObserver {\n readonly targets = new Set<Element>();\n constructor(readonly callback: ResizeObserverCallback) {\n live.add(this);\n }\n observe(target: Element) {\n this.targets.add(target);\n }\n unobserve(target: Element) {\n this.targets.delete(target);\n }\n disconnect() {\n this.targets.clear();\n live.delete(this);\n }\n }\n\n const before = Object.getOwnPropertyDescriptor(globalThis, \"ResizeObserver\");\n Object.defineProperty(globalThis, \"ResizeObserver\", {\n configurable: true,\n writable: true,\n value: StubResizeObserver,\n });\n\n return {\n resize(target, { width, height }) {\n const box = [{ inlineSize: width, blockSize: height }];\n const contentRect = {\n x: 0,\n y: 0,\n top: 0,\n left: 0,\n width,\n height,\n right: width,\n bottom: height,\n toJSON: () => ({ x: 0, y: 0, width, height }),\n };\n const entry = {\n target,\n contentRect,\n borderBoxSize: box,\n contentBoxSize: box,\n devicePixelContentBoxSize: box,\n } as unknown as ResizeObserverEntry;\n for (const observer of [...live]) {\n if (observer.targets.has(target)) {\n observer.callback([entry], observer as unknown as ResizeObserver);\n }\n }\n },\n observed() {\n const all: Element[] = [];\n for (const observer of live) for (const target of observer.targets) if (!all.includes(target)) all.push(target);\n return all;\n },\n restore() {\n live.clear();\n if (before) Object.defineProperty(globalThis, \"ResizeObserver\", before);\n else Reflect.deleteProperty(globalThis, \"ResizeObserver\");\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/testing/viewport.ts"],"sourcesContent":["/**\n * Environment stubs for an app's own tests (0.34, refactor audit 2026-10, TST-10): the\n * two browser APIs jsdom does not have and the kit's layout reads — `matchMedia` and\n * `ResizeObserver`.\n *\n * Every suite rebuilt them by hand: the kit stubbed `matchMedia` in 25 files and defined\n * the same `stubPhone()` eight times, keksdose stubs it in 41, and most copies answer\n * one literal string. That breaks quietly at Large text, where `md` is 960 px and the\n * phone query the kit writes is `(width < 960px)`, not `(width < 768px)`. These answer\n * every width query from a WIDTH, as a browser does, so they stay right at every text\n * size (docs/text-size-harmonization.md §3.3). The reference is keksdose's\n * `frontend/src/test/viewport.ts`, which this generalises.\n *\n * Framework-free, like the rest of `@eifi1/ui-kit/testing`: nothing here imports vitest\n * or jest. Each stub returns its own undo, so it works under either runner, and a suite\n * that prefers `vi.stubGlobal` passes it {@link viewportMatchMedia} instead.\n */\n\n/** The media features a test may want besides the width. Each is off by default. */\nexport interface ViewportOptions {\n /** The primary pointer is a finger: `(pointer: coarse)`, `(any-pointer: coarse)` and\n * `(hover: none)` match; `(pointer: fine)` and `(hover: hover)` do not. */\n coarse?: boolean;\n /** The person asked for less motion: `(prefers-reduced-motion: reduce)` matches, and\n * the kit's overlays skip their exit animations. */\n reducedMotion?: boolean;\n /** `(prefers-color-scheme: dark)` matches (`light` does not). */\n dark?: boolean;\n /** `(prefers-contrast: more)` matches. */\n moreContrast?: boolean;\n}\n\n/** One length in CSS px. A media query's `rem`/`em` is the browser's default font size,\n * 16 px, never `<html>`'s — the kit's own `breakpointQuery` writes px. */\nfunction lengthPx(value: string, unit: string): number {\n return unit === \"px\" ? Number(value) : Number(value) * 16;\n}\n\nconst RANGE = /^\\(\\s*width\\s*(<=|>=|<|>)\\s*([\\d.]+)(px|r?em)\\s*\\)$/;\nconst MIN_MAX = /^\\(\\s*(min|max)-width\\s*:\\s*([\\d.]+)(px|r?em)\\s*\\)$/;\nconst FEATURE = /^\\(\\s*([a-z-]+)\\s*:\\s*([a-z-]+)\\s*\\)$/;\n\n/** One `(…)` condition, or a media type. */\nfunction condition(part: string, width: number, options: ViewportOptions): boolean {\n const q = part.trim();\n if (q === \"all\" || q === \"screen\") return true;\n const range = RANGE.exec(q);\n if (range) {\n const px = lengthPx(range[2], range[3]);\n switch (range[1]) {\n case \"<\":\n return width < px;\n case \"<=\":\n return width <= px;\n case \">\":\n return width > px;\n default:\n return width >= px;\n }\n }\n const minMax = MIN_MAX.exec(q);\n if (minMax) {\n const px = lengthPx(minMax[2], minMax[3]);\n return minMax[1] === \"min\" ? width >= px : width <= px;\n }\n const feature = FEATURE.exec(q);\n if (!feature) return false;\n const [, name, value] = feature;\n const coarse = Boolean(options.coarse);\n switch (name) {\n case \"pointer\":\n case \"any-pointer\":\n return value === (coarse ? \"coarse\" : \"fine\");\n case \"hover\":\n case \"any-hover\":\n return value === (coarse ? \"none\" : \"hover\");\n case \"prefers-reduced-motion\":\n return value === (options.reducedMotion ? \"reduce\" : \"no-preference\");\n case \"prefers-color-scheme\":\n return value === (options.dark ? \"dark\" : \"light\");\n case \"prefers-contrast\":\n return value === (options.moreContrast ? \"more\" : \"no-preference\");\n default:\n return false;\n }\n}\n\n/** A whole query: comma lists are alternatives, `and` joins conditions, as in CSS. */\nfunction answer(query: string, width: number, options: ViewportOptions): boolean {\n return query\n .split(\",\")\n .some((alternative) => alternative.split(/\\s+and\\s+/).every((part) => condition(part, width, options)));\n}\n\n/** A list {@link viewportMatchMedia} handed out: what {@link setViewportWidth} flips. */\ninterface StubMediaQueryList {\n matches: boolean;\n readonly media: string;\n onchange: ((this: MediaQueryList, event: MediaQueryListEvent) => unknown) | null;\n addEventListener(type: string, listener: EventListenerOrEventListenerObject | null): void;\n removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null): void;\n addListener(listener: EventListenerOrEventListenerObject | null): void;\n removeListener(listener: EventListenerOrEventListenerObject | null): void;\n dispatchEvent(event: Event): boolean;\n}\n\n/** One stub's viewport: its width now, and every list it has handed out. */\ninterface StubViewport {\n width: number;\n readonly options: ViewportOptions;\n readonly lists: Set<StubMediaQueryList>;\n}\n\n/** Each `matchMedia` {@link viewportMatchMedia} made → its viewport. Keyed by the function,\n * so {@link setViewportWidth} finds whichever stub is in force however it was put in. */\nconst VIEWPORTS = new WeakMap<object, StubViewport>();\n\nfunction mediaQueryList(query: string, viewport: StubViewport): StubMediaQueryList {\n // One set for both APIs: in a browser `addListener(fn)` IS `addEventListener(\"change\",\n // fn)`, so adding the same function through both registers it once.\n const listeners = new Set<EventListenerOrEventListenerObject>();\n const list: StubMediaQueryList = {\n matches: answer(query, viewport.width, viewport.options),\n media: query,\n onchange: null,\n addEventListener(type, listener) {\n if (type === \"change\" && listener) listeners.add(listener);\n },\n removeEventListener(type, listener) {\n if (type === \"change\" && listener) listeners.delete(listener);\n },\n addListener(listener) {\n if (listener) listeners.add(listener);\n },\n removeListener(listener) {\n if (listener) listeners.delete(listener);\n },\n dispatchEvent(event) {\n if (event.type !== \"change\") return true;\n const self = list as unknown as MediaQueryList;\n list.onchange?.call(self, event as MediaQueryListEvent);\n for (const listener of [...listeners]) {\n if (typeof listener === \"function\") listener.call(self, event);\n else listener.handleEvent(event);\n }\n return !event.defaultPrevented;\n },\n };\n return list;\n}\n\n/** The `change` a browser fires at a list whose answer flipped: a `MediaQueryListEvent`'s\n * `matches` and `media`, on an `Event` (jsdom has no `MediaQueryListEvent`). */\nfunction changeEvent(list: StubMediaQueryList): Event {\n const event = new Event(\"change\");\n for (const [key, value] of Object.entries({\n matches: list.matches,\n media: list.media,\n target: list,\n currentTarget: list,\n })) {\n Object.defineProperty(event, key, { value, enumerable: true });\n }\n return event;\n}\n\n/**\n * A `window.matchMedia` for a viewport `width` CSS px wide.\n *\n * Width queries answer from `width`: the range syntax the kit writes for `max-*`\n * (`(width < 768px)`) and every other comparison, `(min-width: …)` and `(max-width: …)`,\n * in px or rem; `and` and comma lists combine as in CSS. The features in `options` answer\n * as set there; every other query is false. The lists keep their listeners, and\n * {@link setViewportWidth} resizes the viewport under them the way a browser window\n * does: each list whose answer flips fires `change`.\n *\n * Hand it to the runner's own global stub where the suite undoes stubs that way —\n * `vi.stubGlobal(\"matchMedia\", viewportMatchMedia(375))` — or use\n * {@link stubViewportWidth}.\n */\nexport function viewportMatchMedia(width: number, options: ViewportOptions = {}): (query: string) => MediaQueryList {\n const viewport: StubViewport = { width, options, lists: new Set() };\n const matchMedia = (query: string) => {\n const list = mediaQueryList(query, viewport);\n viewport.lists.add(list);\n return list as unknown as MediaQueryList;\n };\n VIEWPORTS.set(matchMedia, viewport);\n return matchMedia;\n}\n\n/**\n * Resize the stubbed viewport to `width` CSS px, live — the window being dragged narrower,\n * a phone turned (0.35, keksdose's 0.34 report). Every list the stub in force has handed\n * out answers from the new width, and each one whose answer flipped fires `change` to its\n * `addEventListener` listeners, its legacy `addListener` ones and its `onchange`, as in a\n * browser; a list whose answer stayed the same tells nobody. So a mounted component that\n * listens to a breakpoint (`usePhoneLayout()`, `useBreakpoint()`) switches without being\n * rendered again.\n *\n * The stub in force is whatever `window.matchMedia` is now — one {@link stubViewportWidth}\n * or `vi.stubGlobal(\"matchMedia\", viewportMatchMedia(…))` put in; its features\n * (`options`) stay as they were. Throws when `matchMedia` is not one of these stubs, rather\n * than resize nothing. Synchronous: wrap it in `act()` when a listener sets state.\n *\n * ```ts\n * undo = stubViewportWidth(1280);\n * render(<Page />);\n * act(() => setViewportWidth(375));\n * expect(screen.getByRole(\"list\")).toBeInTheDocument(); // the cards\n * ```\n */\nexport function setViewportWidth(width: number): void {\n const viewport = typeof window === \"undefined\" ? undefined : VIEWPORTS.get(window.matchMedia);\n if (!viewport) {\n throw new Error(\n \"setViewportWidth: no viewport stub is in force — put one in first with \" +\n 'stubViewportWidth(width), or vi.stubGlobal(\"matchMedia\", viewportMatchMedia(width)).',\n );\n }\n viewport.width = width;\n for (const list of [...viewport.lists]) {\n const matches = answer(list.media, width, viewport.options);\n if (matches === list.matches) continue;\n list.matches = matches;\n list.dispatchEvent(changeEvent(list));\n }\n}\n\n/**\n * Make `window.matchMedia` answer as a viewport `width` CSS px wide (see\n * {@link viewportMatchMedia}), and return the undo: it puts back what was there, which\n * in jsdom is nothing — no `matchMedia` at all, the desktop fallback of every kit hook.\n *\n * ```ts\n * let undo = () => {};\n * afterEach(() => undo());\n * it(\"shows cards on a phone\", () => {\n * undo = stubViewportWidth(375);\n * …\n * });\n * ```\n *\n * Right at every text size: at Large the kit's phone query is `(width < 960px)`, which a\n * 375 px viewport matches and a 1280 px one does not, as in a browser.\n */\nexport function stubViewportWidth(width: number, options: ViewportOptions = {}): () => void {\n const before = Object.getOwnPropertyDescriptor(window, \"matchMedia\");\n Object.defineProperty(window, \"matchMedia\", {\n configurable: true,\n writable: true,\n value: viewportMatchMedia(width, options),\n });\n return () => {\n if (before) Object.defineProperty(window, \"matchMedia\", before);\n else Reflect.deleteProperty(window, \"matchMedia\");\n };\n}\n\n/** A box size for {@link ResizeObserverStub.resize}, in CSS px. */\nexport interface StubbedBoxSize {\n width: number;\n height: number;\n}\n\n/** What {@link stubResizeObserver} returns: the test drives the observers. */\nexport interface ResizeObserverStub {\n /**\n * Report `target` at `size` to every live observer watching it, as a browser does once\n * layout settles: one entry, with `contentRect` and the three box sizes all `size`.\n * Synchronous; wrap it in `act()` when the callback sets state.\n */\n resize(target: Element, size: StubbedBoxSize): void;\n /** The elements some live observer watches now, each once. */\n observed(): Element[];\n /** Put back the `ResizeObserver` that was there before — in jsdom, none. */\n restore(): void;\n}\n\n/**\n * Install a `ResizeObserver` the test drives. jsdom has none, and lays nothing out, so a\n * component that measures itself (a windowed list, a fitted chart, a measured grid)\n * either crashes on the missing global or never learns a size. Observers here record\n * what they watch and are called only by {@link ResizeObserverStub.resize}.\n *\n * ```ts\n * const ro = stubResizeObserver();\n * afterEach(() => ro.restore());\n * act(() => ro.resize(screen.getByRole(\"list\"), { width: 400, height: 320 }));\n * ```\n */\nexport function stubResizeObserver(): ResizeObserverStub {\n const live = new Set<StubResizeObserver>();\n\n class StubResizeObserver {\n readonly targets = new Set<Element>();\n constructor(readonly callback: ResizeObserverCallback) {\n live.add(this);\n }\n observe(target: Element) {\n this.targets.add(target);\n }\n unobserve(target: Element) {\n this.targets.delete(target);\n }\n disconnect() {\n this.targets.clear();\n live.delete(this);\n }\n }\n\n const before = Object.getOwnPropertyDescriptor(globalThis, \"ResizeObserver\");\n Object.defineProperty(globalThis, \"ResizeObserver\", {\n configurable: true,\n writable: true,\n value: StubResizeObserver,\n });\n\n return {\n resize(target, { width, height }) {\n const box = [{ inlineSize: width, blockSize: height }];\n const contentRect = {\n x: 0,\n y: 0,\n top: 0,\n left: 0,\n width,\n height,\n right: width,\n bottom: height,\n toJSON: () => ({ x: 0, y: 0, width, height }),\n };\n const entry = {\n target,\n contentRect,\n borderBoxSize: box,\n contentBoxSize: box,\n devicePixelContentBoxSize: box,\n } as unknown as ResizeObserverEntry;\n for (const observer of [...live]) {\n if (observer.targets.has(target)) {\n observer.callback([entry], observer as unknown as ResizeObserver);\n }\n }\n },\n observed() {\n const all: Element[] = [];\n for (const observer of live) for (const target of observer.targets) if (!all.includes(target)) all.push(target);\n return all;\n },\n restore() {\n live.clear();\n if (before) Object.defineProperty(globalThis, \"ResizeObserver\", before);\n else Reflect.deleteProperty(globalThis, \"ResizeObserver\");\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
contrast,
|
|
4
|
+
hexToOklch,
|
|
5
|
+
luminance,
|
|
6
|
+
oklchToHex,
|
|
7
|
+
oklchToRgb,
|
|
8
|
+
parseHex,
|
|
9
|
+
rgbToOklch,
|
|
10
|
+
solveLightness,
|
|
11
|
+
toHex
|
|
12
|
+
} from "./color.js";
|
|
3
13
|
const CONTRAST_STEPPED_VARS = [
|
|
4
14
|
"--text-secondary",
|
|
5
15
|
"--text-muted",
|
|
@@ -82,7 +92,8 @@ function worstContrast(color, t, well) {
|
|
|
82
92
|
function steppedBorder(t, well) {
|
|
83
93
|
const surface2 = well?.bgSurface2 ?? t.bgSurface2;
|
|
84
94
|
let lo = BORDER_STRONG_SHARE;
|
|
85
|
-
if (worstContrast(mix(t.border, t.textPrimary, 1 - lo), t, surface2) >= BORDER_TARGET)
|
|
95
|
+
if (worstContrast(mix(t.border, t.textPrimary, 1 - lo), t, surface2) >= BORDER_TARGET)
|
|
96
|
+
return mix(t.border, t.textPrimary, 1 - lo);
|
|
86
97
|
let hi = 1;
|
|
87
98
|
for (let i = 0; i < 16; i++) {
|
|
88
99
|
const mid = (lo + hi) / 2;
|