@eifi1/ui-kit 0.51.4 → 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 +3 -3
- 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 +1 -4
- 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 +10 -8
- 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.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 +44 -56
- 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.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 +6 -8
- 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 +2 -7
- 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 +2 -8
- 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.map +1 -1
- package/dist/components/legal-page.js +14 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.js +12 -2
- 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.js +22 -10
- 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.js +4 -1
- 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 +4 -1
- 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 +4 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +4 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +4 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +4 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +4 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.js +13 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +16 -16
- 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 +4 -0
- 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-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.js +12 -6
- 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.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/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 +12 -42
- 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 +13 -14
- 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 +1 -3
- package/src/components/data-table-groups.tsx +3 -1
- package/src/components/data-table-head.tsx +27 -45
- 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 +489 -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 +6 -8
- 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 +4 -17
- package/src/components/image-grid.tsx +1 -6
- package/src/components/inline-edit-field.tsx +1 -5
- package/src/components/inline-entity-combobox.tsx +6 -21
- package/src/components/input.tsx +14 -2
- package/src/components/jump-calendar.tsx +2 -6
- package/src/components/legal-labels.ts +1 -2
- package/src/components/legal-page.tsx +18 -7
- package/src/components/legal.tsx +14 -5
- package/src/components/lightbox.tsx +1 -3
- package/src/components/line-items.tsx +7 -15
- package/src/components/list.tsx +29 -25
- 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 +12 -8
- 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 +33 -49
- package/src/i18n/kit-labels.tsx +5 -20
- package/src/i18n/languages.ts +2 -8
- package/src/i18n/locales/en.ts +11 -31
- package/src/i18n/locales/es.ts +35 -49
- package/src/i18n/locales/fr.ts +34 -51
- package/src/i18n/locales/hu.ts +26 -37
- package/src/i18n/locales/it.ts +35 -52
- package/src/i18n/locales/zh.ts +14 -32
- package/src/i18n/plurals.ts +4 -1
- package/src/i18n/review.ts +18 -7
- package/src/i18n/shared-labels.ts +1 -2
- package/src/index.ts +106 -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 +15 -3
- 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-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 +12 -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 +9 -8
- package/src/shell/topbar-controls.tsx +5 -19
- package/src/shell/tour-menu.tsx +9 -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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/tour-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useLocation } from \"react-router\";\nimport { Check, Compass } from \"lucide-react\";\nimport { Chip } from \"../components/chip\";\nimport { isTourDone, isTourNew, isTourReset, type TourCompletions, type TourInfo } from \"../tour/tour-info\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type TourMenuLabels } from \"./top-bar-parts-labels\";\n\n/**\n * One tour as the menu needs it — data, so the menu depends on no app's tour registry.\n * The kit's tour engine (`TourProvider`) runs steps and keeps none; which tours exist,\n * which are new and which are taken is the app's (keksdose: `useTours`, `useIsNewTour`,\n * its progress store).\n *\n * Since 0.43 it is a subset of the catalogue's `TourInfo` (docs/tours-harmonization.md\n * §3), so an app keeps ONE list: hand the menu the catalogue's tours with `completed`\n * and `joined`, and it works out ✓, NEW and \"Updated\" itself. Given the record, the kit's\n * ✓ and \"Updated\" win over a row's `completed` and `reset`; given `joined`, its NEW wins\n * over `isNew` — so a row built from an app's own booleans cannot hide an update (§8,\n * build review kastlan A3, keksdose K-S6). The booleans are for a menu with neither.\n */\nexport type TourMenuTour = Partial<Pick<TourInfo, \"route\" | \"since\" | \"updated\">> & {\n id: string;\n title: ReactNode;\n /** The row's icon, drawn at 16px. */\n icon?: ReactNode;\n /** Shipped after this account was opened and not yet taken: a NEW chip on the row,\n * and the dot on the compass. */\n isNew?: boolean;\n /** Taken: a ✓ on the row. */\n completed?: boolean;\n /** Taken before its last update (§8): an \"Updated\" chip, and the dot, as for NEW. */\n reset?: boolean;\n};\n\nexport interface TourMenuProps {\n tours: readonly TourMenuTour[];\n /** Start a tour — `useTour().start(steps, { id })` with the app's steps. */\n onLaunch: (id: string) => void;\n /** The device's completions (`useTourProgress().completed`): ✓ and \"Updated\" from them. */\n completed?: TourCompletions;\n /** The account's `created_at`: NEW from it, by `isTourNew`. */\n joined?: string | null;\n /**\n * The app's tours page (keksdose `/tours`). With it the compass is a LINK there plus a\n * hover menu, and the menu ends in \"All tours…\". Without it (§6.7, Kurvenschmiede) the\n * compass opens the menu, and the tours on neither list follow below a rule.\n */\n href?: string;\n /** The open page's path. Default: the router's location. */\n pathname?: string;\n /** How many new tours the menu offers beyond the page's own. Default 3. */\n maxNew?: number;\n /**\n * `\"all\"`: list every tour — this page's and the new ones first, then the rest below a\n * rule — with `href` too, before \"All tours…\". For an app whose tours carry no `route`,\n * `isNew` or `since` (kastlan, build review A1), where the short list would be empty\n * off the tours' page. Default `\"short\"`: with `href`, only this page's and the new.\n */\n list?: \"short\" | \"all\";\n /** Prop > `<UiKitProvider labels={{ topBarParts: { tours } }}>` > English. */\n labels?: LabelOverride<TourMenuLabels>;\n /** `data-*` attributes go on the compass — a tour's anchor (`data-tour=\"tours\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** A path's first segment — \"/reports/spending?g=month\" → \"/reports\". */\nconst pageOf = (path: string) => `/${path.split(/[?#]/)[0].split(\"/\")[1] ?? \"\"}`;\n\nconst isFresh = (t: TourMenuTour) => (!!t.isNew && !t.completed) || !!t.reset;\n/** When it last became news: its update, else its ship date. */\nconst newsOf = (t: TourMenuTour) => (t.reset ? t.updated?.date : undefined) ?? t.since ?? \"\";\n\n/**\n * The top bar's guided-tours control (docs/top-bar-harmonization.md §2.4, §6.7), lifted\n * from keksdose's: the lucide `Compass`, with a NEW dot while a tour is new, and a short\n * list — the tours that start on this page, up to three new ones (newest first), a rule,\n * and \"All tours…\". A taken tour's row carries a ✓, a new one a NEW chip.\n */\nexport function TourMenu({\n tours: toursProp,\n onLaunch,\n completed: completions,\n joined,\n href,\n pathname: pathnameProp,\n maxNew = 3,\n list = \"short\",\n labels: labelsProp,\n ...attributes\n}: TourMenuProps) {\n const words = useTopBarPartsLabels(labelsProp ? { tours: labelsProp } : undefined).tours;\n const location = useLocation();\n const here = pageOf(pathnameProp ?? location.pathname);\n const tours =\n completions === undefined && joined === undefined\n ? toursProp\n : toursProp.map((t) => {\n const at = completions?.[t.id];\n // Ever completed: the record when there is one, else the row's own word.\n const ever = completions ? at !== undefined : !!t.completed;\n return {\n ...t,\n completed: completions ? isTourDone(t, at) : t.completed,\n reset: completions ? isTourReset(t, at) : t.reset,\n isNew: joined !== undefined ? isTourNew({ since: t.since, joined, completed: ever }) : t.isNew,\n };\n });\n\n const forThisPage = tours.filter((t) => t.route !== undefined && pageOf(t.route) === here);\n const fresh = tours.filter(isFresh);\n // ISO dates sort as text; a stable sort keeps one day's tours in the app's order.\n const newRows = fresh\n .filter((t) => !forThisPage.includes(t))\n .sort((a, b) => newsOf(b).localeCompare(newsOf(a)))\n .slice(0, maxNew);\n const rest = href && list !== \"all\" ? [] : tours.filter((t) => !forThisPage.includes(t) && !newRows.includes(t));\n\n if (tours.length === 0 && !href) return null;\n\n const entryFor = (tour: TourMenuTour): TopBarMenuEntry => ({\n key: tour.id,\n icon: tour.icon,\n label: tour.title,\n trailing: tour.reset ? (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeUpdated}\n </Chip>\n ) : tour.completed ? (\n <>\n <Check aria-hidden className=\"size-4 text-success\" />\n <span className=\"sr-only\">{words.completed}</span>\n </>\n ) : tour.isNew ? (\n // The kit's inert Chip: a plain <span>, no role of its own inside the menu.\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeNew}\n </Chip>\n ) : undefined,\n onSelect: () => onLaunch(tour.id),\n });\n\n const blocks = [\n { key: \"this-page\", label: words.thisPage, tours: forThisPage },\n { key: \"new\", label: newRows.some((t) => t.reset) ? words.newAndUpdated : words.newTours, tours: newRows },\n ].filter((b) => b.tours.length > 0);\n const tail: TopBarMenuEntry[] = [\n ...rest.map(entryFor),\n ...(href\n ? [{ kind: \"link\" as const, key: \"all-tours\", icon: <Compass className=\"size-4\" />, label: words.allTours, to: href }]\n : []),\n ];\n\n const entries: TopBarMenuEntry[] = [\n ...blocks.flatMap((b): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `head-${b.key}`, label: b.label },\n ...b.tours.map(entryFor),\n ]),\n ...(blocks.length > 0 && tail.length > 0 ? [{ kind: \"divider\" as const, key: \"sep-all\" }] : []),\n ...tail,\n ];\n\n return (\n <TopBarActionMenu\n icon={<Compass className=\"size-5\" />}\n iconBadge={\n fresh.length > 0\n ? { label: fresh.some((t) => !t.reset) ? words.newDot : words.updatedDot, tone: \"brand\" }\n : null\n }\n aria-label={words.menu}\n href={href}\n panelClassName=\"w-72 max-h-[70vh] overflow-y-auto\"\n entries={entries}\n triggerAttributes={Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(\"data-\")))}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/tour-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useLocation } from \"react-router\";\nimport { Check, Compass } from \"lucide-react\";\nimport { Chip } from \"../components/chip\";\nimport { isTourDone, isTourNew, isTourReset, type TourCompletions, type TourInfo } from \"../tour/tour-info\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type TourMenuLabels } from \"./top-bar-parts-labels\";\n\n/**\n * One tour as the menu needs it — data, so the menu depends on no app's tour registry.\n * The kit's tour engine (`TourProvider`) runs steps and keeps none; which tours exist,\n * which are new and which are taken is the app's (keksdose: `useTours`, `useIsNewTour`,\n * its progress store).\n *\n * Since 0.43 it is a subset of the catalogue's `TourInfo` (docs/tours-harmonization.md\n * §3), so an app keeps ONE list: hand the menu the catalogue's tours with `completed`\n * and `joined`, and it works out ✓, NEW and \"Updated\" itself. Given the record, the kit's\n * ✓ and \"Updated\" win over a row's `completed` and `reset`; given `joined`, its NEW wins\n * over `isNew` — so a row built from an app's own booleans cannot hide an update (§8,\n * build review kastlan A3, keksdose K-S6). The booleans are for a menu with neither.\n */\nexport type TourMenuTour = Partial<Pick<TourInfo, \"route\" | \"since\" | \"updated\">> & {\n id: string;\n title: ReactNode;\n /** The row's icon, drawn at 16px. */\n icon?: ReactNode;\n /** Shipped after this account was opened and not yet taken: a NEW chip on the row,\n * and the dot on the compass. */\n isNew?: boolean;\n /** Taken: a ✓ on the row. */\n completed?: boolean;\n /** Taken before its last update (§8): an \"Updated\" chip, and the dot, as for NEW. */\n reset?: boolean;\n};\n\nexport interface TourMenuProps {\n tours: readonly TourMenuTour[];\n /** Start a tour — `useTour().start(steps, { id })` with the app's steps. */\n onLaunch: (id: string) => void;\n /** The device's completions (`useTourProgress().completed`): ✓ and \"Updated\" from them. */\n completed?: TourCompletions;\n /** The account's `created_at`: NEW from it, by `isTourNew`. */\n joined?: string | null;\n /**\n * The app's tours page (keksdose `/tours`). With it the compass is a LINK there plus a\n * hover menu, and the menu ends in \"All tours…\". Without it (§6.7, Kurvenschmiede) the\n * compass opens the menu, and the tours on neither list follow below a rule.\n */\n href?: string;\n /** The open page's path. Default: the router's location. */\n pathname?: string;\n /** How many new tours the menu offers beyond the page's own. Default 3. */\n maxNew?: number;\n /**\n * `\"all\"`: list every tour — this page's and the new ones first, then the rest below a\n * rule — with `href` too, before \"All tours…\". For an app whose tours carry no `route`,\n * `isNew` or `since` (kastlan, build review A1), where the short list would be empty\n * off the tours' page. Default `\"short\"`: with `href`, only this page's and the new.\n */\n list?: \"short\" | \"all\";\n /** Prop > `<UiKitProvider labels={{ topBarParts: { tours } }}>` > English. */\n labels?: LabelOverride<TourMenuLabels>;\n /** `data-*` attributes go on the compass — a tour's anchor (`data-tour=\"tours\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** A path's first segment — \"/reports/spending?g=month\" → \"/reports\". */\nconst pageOf = (path: string) => `/${path.split(/[?#]/)[0].split(\"/\")[1] ?? \"\"}`;\n\nconst isFresh = (t: TourMenuTour) => (!!t.isNew && !t.completed) || !!t.reset;\n/** When it last became news: its update, else its ship date. */\nconst newsOf = (t: TourMenuTour) => (t.reset ? t.updated?.date : undefined) ?? t.since ?? \"\";\n\n/**\n * The top bar's guided-tours control (docs/top-bar-harmonization.md §2.4, §6.7), lifted\n * from keksdose's: the lucide `Compass`, with a NEW dot while a tour is new, and a short\n * list — the tours that start on this page, up to three new ones (newest first), a rule,\n * and \"All tours…\". A taken tour's row carries a ✓, a new one a NEW chip.\n */\nexport function TourMenu({\n tours: toursProp,\n onLaunch,\n completed: completions,\n joined,\n href,\n pathname: pathnameProp,\n maxNew = 3,\n list = \"short\",\n labels: labelsProp,\n ...attributes\n}: TourMenuProps) {\n const words = useTopBarPartsLabels(labelsProp ? { tours: labelsProp } : undefined).tours;\n const location = useLocation();\n const here = pageOf(pathnameProp ?? location.pathname);\n const tours =\n completions === undefined && joined === undefined\n ? toursProp\n : toursProp.map((t) => {\n const at = completions?.[t.id];\n // Ever completed: the record when there is one, else the row's own word.\n const ever = completions ? at !== undefined : !!t.completed;\n return {\n ...t,\n completed: completions ? isTourDone(t, at) : t.completed,\n reset: completions ? isTourReset(t, at) : t.reset,\n isNew: joined !== undefined ? isTourNew({ since: t.since, joined, completed: ever }) : t.isNew,\n };\n });\n\n const forThisPage = tours.filter((t) => t.route !== undefined && pageOf(t.route) === here);\n const fresh = tours.filter(isFresh);\n // ISO dates sort as text; a stable sort keeps one day's tours in the app's order.\n const newRows = fresh\n .filter((t) => !forThisPage.includes(t))\n .sort((a, b) => newsOf(b).localeCompare(newsOf(a)))\n .slice(0, maxNew);\n const rest = href && list !== \"all\" ? [] : tours.filter((t) => !forThisPage.includes(t) && !newRows.includes(t));\n\n if (tours.length === 0 && !href) return null;\n\n const entryFor = (tour: TourMenuTour): TopBarMenuEntry => ({\n key: tour.id,\n icon: tour.icon,\n label: tour.title,\n trailing: tour.reset ? (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeUpdated}\n </Chip>\n ) : tour.completed ? (\n <>\n <Check aria-hidden className=\"size-4 text-success\" />\n <span className=\"sr-only\">{words.completed}</span>\n </>\n ) : tour.isNew ? (\n // The kit's inert Chip: a plain <span>, no role of its own inside the menu.\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeNew}\n </Chip>\n ) : undefined,\n onSelect: () => onLaunch(tour.id),\n });\n\n const blocks = [\n { key: \"this-page\", label: words.thisPage, tours: forThisPage },\n { key: \"new\", label: newRows.some((t) => t.reset) ? words.newAndUpdated : words.newTours, tours: newRows },\n ].filter((b) => b.tours.length > 0);\n const tail: TopBarMenuEntry[] = [\n ...rest.map(entryFor),\n ...(href\n ? [\n {\n kind: \"link\" as const,\n key: \"all-tours\",\n icon: <Compass className=\"size-4\" />,\n label: words.allTours,\n to: href,\n },\n ]\n : []),\n ];\n\n const entries: TopBarMenuEntry[] = [\n ...blocks.flatMap((b): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `head-${b.key}`, label: b.label },\n ...b.tours.map(entryFor),\n ]),\n ...(blocks.length > 0 && tail.length > 0 ? [{ kind: \"divider\" as const, key: \"sep-all\" }] : []),\n ...tail,\n ];\n\n return (\n <TopBarActionMenu\n icon={<Compass className=\"size-5\" />}\n iconBadge={\n fresh.length > 0\n ? { label: fresh.some((t) => !t.reset) ? words.newDot : words.updatedDot, tone: \"brand\" }\n : null\n }\n aria-label={words.menu}\n href={href}\n panelClassName=\"w-72 max-h-[70vh] overflow-y-auto\"\n entries={entries}\n triggerAttributes={Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(\"data-\")))}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/sync/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sync/db.ts"],"sourcesContent":["import Dexie, { type Table, type Transaction } from \"dexie\";\n\n/*\n * The device's copy for the sync engine (docs/local-first-harmonization.md §2.3, D10): ONE\n * Dexie database with the engine's three tables — the rows as `/sync` carries them, the\n * outbox of changes made here, and a key/value `meta` — plus the app's own (kastlan's\n * `photos`). A generic store, not keksdose's per-entity tables.\n */\n\n/** A row as `/sync` carries it: the entity (a table name), its sync id, the server's id\n * once it has one, and its fields. */\nexport interface SyncedRow {\n entity: string;\n id: string;\n pk: number | null;\n data: Record<string, unknown>;\n /** The server's stamp of the row, as the pull carried it BESIDE `data` (kastlan's wire);\n * the next edit here is stamped after it (`stampAfter`, D2). */\n updated_at?: string | null;\n}\n\n/**\n * Where a change made here stands. `pending`: queued. `sending`: in the request now out (a\n * new edit starts a change of its own). `waiting`: refused with a hold code (a lapsed plan,\n * `holdCodes`), sent again every round. `blocked`: refused with any other code, kept, sent\n * again only on `retryBlocked()`. `rejected`: the server's rules refused it (`reason` says\n * why); never sent again.\n */\nexport type OutboxStatus = \"pending\" | \"sending\" | \"waiting\" | \"blocked\" | \"rejected\";\n\n/** A change made on this device, for `/sync`, in the scope its caller named (D11). */\nexport interface OutboxChange {\n seq?: number;\n scope: string;\n entity: string;\n id: string;\n op: \"upsert\" | \"delete\";\n data: Record<string, unknown>;\n /** The edit's time on this device (D2), stamped with `stampAfter`. */\n updated_at: string;\n status: OutboxStatus;\n /** The server's words: a rejection's `detail`, a refusal's `detail`. */\n reason?: string;\n /** The server's code: a rejection's or a refusal's `code`. */\n code?: string;\n}\n\nexport interface SyncMetaEntry {\n key: string;\n value: unknown;\n}\n\n/** The engine's database: its three tables, and the app's own under the names it gave. */\nexport type SyncDb = Dexie & {\n rows: Table<SyncedRow, [string, string]>;\n outbox: Table<OutboxChange, number>;\n meta: Table<SyncMetaEntry, string>;\n};\n\n/** A schema step of an existing database (kastlan's v2: `scope` on the outbox). */\nexport interface SyncDbUpgrade {\n version: number;\n /** Stores this version adds or changes, beside the engine's and `extraStores`. */\n stores?: Record<string, string | null>;\n upgrade?(tx: Transaction): Promise<void> | void;\n}\n\nexport interface CreateSyncDbOptions {\n /** The app's own tables, Dexie schema strings: kastlan `{ photos: \"id, roomId, status\" }`. */\n extraStores?: Record<string, string>;\n /** Later versions of a database that exists on devices already, oldest first. */\n upgrades?: readonly SyncDbUpgrade[];\n}\n\n/** The engine's own tables. `scope` is indexed: a round takes one scope's queue (D11). */\nexport const SYNC_DB_STORES = {\n rows: \"[entity+id], entity\",\n outbox: \"++seq, [entity+id], status, scope\",\n meta: \"key\",\n} as const;\n\n/**\n * The `rows` schema with an `[entity+pk]` index (0.46.1), opt-in: the D16 pages start from a\n * server id (`/meters/7`) and {@link rowByPk} finds its copy row by this index. Not in\n * {@link SYNC_DB_STORES}, so no database on a device changes until its app names it in a\n * version of its own — kastlan, whose devices are on v2: `upgrades: [..., { version: 3,\n * stores: SYNC_DB_PK_INDEX }]`. A database new to the devices may take it at once:\n * `extraStores: { ...SYNC_DB_PK_INDEX }`.\n */\nexport const SYNC_DB_PK_INDEX = {\n rows: \"[entity+id], entity, [entity+pk]\",\n} as const;\n\n/**\n * The copy row of the server's id `pk` in `entity` (0.46.1): by the `[entity+pk]` index when\n * the database has it ({@link SYNC_DB_PK_INDEX}), else by a scan of the entity's rows.\n * `undefined` when the copy holds no such row.\n */\nexport async function rowByPk(db: SyncDb, entity: string, pk: number): Promise<SyncedRow | undefined> {\n if (db.rows.schema.idxByName[\"[entity+pk]\"]) return db.rows.where(\"[entity+pk]\").equals([entity, pk]).first();\n return db.rows.where(\"entity\").equals(entity).filter((row) => row.pk === pk).first();\n}\n\n/**\n * Open (lazily, on first use) the sync database `name`: the engine's tables plus the app's\n * `extraStores`, at version 1 or the last of `upgrades`. One database per account (kastlan:\n * per account and company, `kastlan-handover-<user>-<company>`), and one engine on it (D15).\n *\n * An app whose database predates the engine names its next version in `upgrades` and moves\n * its data there — kastlan: `{ version: 2, upgrade: (tx) => tx.table(\"outbox\").toCollection()\n * .modify({ scope: \"handover\" }) }`. Dexie diffs the schema against the one on the device.\n * Name the app's own type for its tables: `createSyncDb<HandoverDb>(name, { extraStores })`.\n */\nexport function createSyncDb<D extends SyncDb = SyncDb>(name: string, options: CreateSyncDbOptions = {}): D {\n const db = new Dexie(name) as D;\n let stores: Record<string, string | null> = { ...SYNC_DB_STORES, ...options.extraStores };\n db.version(1).stores(stores);\n const upgrades = [...(options.upgrades ?? [])].sort((a, b) => a.version - b.version);\n for (const step of upgrades) {\n stores = { ...stores, ...step.stores };\n const version = db.version(step.version).stores(stores);\n if (step.upgrade) version.upgrade(step.upgrade);\n }\n return db;\n}\n"],"mappings":";AAAA,OAAO,WAA6C;AA2E7C,MAAM,iBAAiB;AAAA,EAC5B,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AACR;AAUO,MAAM,mBAAmB;AAAA,EAC9B,MAAM;AACR;AAOA,eAAsB,QAAQ,IAAY,QAAgB,IAA4C;AACpG,MAAI,GAAG,KAAK,OAAO,UAAU,aAAa,EAAG,QAAO,GAAG,KAAK,MAAM,aAAa,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM;AAC5G,SAAO,GAAG,
|
|
1
|
+
{"version":3,"sources":["../../src/sync/db.ts"],"sourcesContent":["import Dexie, { type Table, type Transaction } from \"dexie\";\n\n/*\n * The device's copy for the sync engine (docs/local-first-harmonization.md §2.3, D10): ONE\n * Dexie database with the engine's three tables — the rows as `/sync` carries them, the\n * outbox of changes made here, and a key/value `meta` — plus the app's own (kastlan's\n * `photos`). A generic store, not keksdose's per-entity tables.\n */\n\n/** A row as `/sync` carries it: the entity (a table name), its sync id, the server's id\n * once it has one, and its fields. */\nexport interface SyncedRow {\n entity: string;\n id: string;\n pk: number | null;\n data: Record<string, unknown>;\n /** The server's stamp of the row, as the pull carried it BESIDE `data` (kastlan's wire);\n * the next edit here is stamped after it (`stampAfter`, D2). */\n updated_at?: string | null;\n}\n\n/**\n * Where a change made here stands. `pending`: queued. `sending`: in the request now out (a\n * new edit starts a change of its own). `waiting`: refused with a hold code (a lapsed plan,\n * `holdCodes`), sent again every round. `blocked`: refused with any other code, kept, sent\n * again only on `retryBlocked()`. `rejected`: the server's rules refused it (`reason` says\n * why); never sent again.\n */\nexport type OutboxStatus = \"pending\" | \"sending\" | \"waiting\" | \"blocked\" | \"rejected\";\n\n/** A change made on this device, for `/sync`, in the scope its caller named (D11). */\nexport interface OutboxChange {\n seq?: number;\n scope: string;\n entity: string;\n id: string;\n op: \"upsert\" | \"delete\";\n data: Record<string, unknown>;\n /** The edit's time on this device (D2), stamped with `stampAfter`. */\n updated_at: string;\n status: OutboxStatus;\n /** The server's words: a rejection's `detail`, a refusal's `detail`. */\n reason?: string;\n /** The server's code: a rejection's or a refusal's `code`. */\n code?: string;\n}\n\nexport interface SyncMetaEntry {\n key: string;\n value: unknown;\n}\n\n/** The engine's database: its three tables, and the app's own under the names it gave. */\nexport type SyncDb = Dexie & {\n rows: Table<SyncedRow, [string, string]>;\n outbox: Table<OutboxChange, number>;\n meta: Table<SyncMetaEntry, string>;\n};\n\n/** A schema step of an existing database (kastlan's v2: `scope` on the outbox). */\nexport interface SyncDbUpgrade {\n version: number;\n /** Stores this version adds or changes, beside the engine's and `extraStores`. */\n stores?: Record<string, string | null>;\n upgrade?(tx: Transaction): Promise<void> | void;\n}\n\nexport interface CreateSyncDbOptions {\n /** The app's own tables, Dexie schema strings: kastlan `{ photos: \"id, roomId, status\" }`. */\n extraStores?: Record<string, string>;\n /** Later versions of a database that exists on devices already, oldest first. */\n upgrades?: readonly SyncDbUpgrade[];\n}\n\n/** The engine's own tables. `scope` is indexed: a round takes one scope's queue (D11). */\nexport const SYNC_DB_STORES = {\n rows: \"[entity+id], entity\",\n outbox: \"++seq, [entity+id], status, scope\",\n meta: \"key\",\n} as const;\n\n/**\n * The `rows` schema with an `[entity+pk]` index (0.46.1), opt-in: the D16 pages start from a\n * server id (`/meters/7`) and {@link rowByPk} finds its copy row by this index. Not in\n * {@link SYNC_DB_STORES}, so no database on a device changes until its app names it in a\n * version of its own — kastlan, whose devices are on v2: `upgrades: [..., { version: 3,\n * stores: SYNC_DB_PK_INDEX }]`. A database new to the devices may take it at once:\n * `extraStores: { ...SYNC_DB_PK_INDEX }`.\n */\nexport const SYNC_DB_PK_INDEX = {\n rows: \"[entity+id], entity, [entity+pk]\",\n} as const;\n\n/**\n * The copy row of the server's id `pk` in `entity` (0.46.1): by the `[entity+pk]` index when\n * the database has it ({@link SYNC_DB_PK_INDEX}), else by a scan of the entity's rows.\n * `undefined` when the copy holds no such row.\n */\nexport async function rowByPk(db: SyncDb, entity: string, pk: number): Promise<SyncedRow | undefined> {\n if (db.rows.schema.idxByName[\"[entity+pk]\"]) return db.rows.where(\"[entity+pk]\").equals([entity, pk]).first();\n return db.rows\n .where(\"entity\")\n .equals(entity)\n .filter((row) => row.pk === pk)\n .first();\n}\n\n/**\n * Open (lazily, on first use) the sync database `name`: the engine's tables plus the app's\n * `extraStores`, at version 1 or the last of `upgrades`. One database per account (kastlan:\n * per account and company, `kastlan-handover-<user>-<company>`), and one engine on it (D15).\n *\n * An app whose database predates the engine names its next version in `upgrades` and moves\n * its data there — kastlan: `{ version: 2, upgrade: (tx) => tx.table(\"outbox\").toCollection()\n * .modify({ scope: \"handover\" }) }`. Dexie diffs the schema against the one on the device.\n * Name the app's own type for its tables: `createSyncDb<HandoverDb>(name, { extraStores })`.\n */\nexport function createSyncDb<D extends SyncDb = SyncDb>(name: string, options: CreateSyncDbOptions = {}): D {\n const db = new Dexie(name) as D;\n let stores: Record<string, string | null> = { ...SYNC_DB_STORES, ...options.extraStores };\n db.version(1).stores(stores);\n const upgrades = [...(options.upgrades ?? [])].sort((a, b) => a.version - b.version);\n for (const step of upgrades) {\n stores = { ...stores, ...step.stores };\n const version = db.version(step.version).stores(stores);\n if (step.upgrade) version.upgrade(step.upgrade);\n }\n return db;\n}\n"],"mappings":";AAAA,OAAO,WAA6C;AA2E7C,MAAM,iBAAiB;AAAA,EAC5B,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AACR;AAUO,MAAM,mBAAmB;AAAA,EAC9B,MAAM;AACR;AAOA,eAAsB,QAAQ,IAAY,QAAgB,IAA4C;AACpG,MAAI,GAAG,KAAK,OAAO,UAAU,aAAa,EAAG,QAAO,GAAG,KAAK,MAAM,aAAa,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM;AAC5G,SAAO,GAAG,KACP,MAAM,QAAQ,EACd,OAAO,MAAM,EACb,OAAO,CAAC,QAAQ,IAAI,OAAO,EAAE,EAC7B,MAAM;AACX;AAYO,SAAS,aAAwC,MAAc,UAA+B,CAAC,GAAM;AAC1G,QAAM,KAAK,IAAI,MAAM,IAAI;AACzB,MAAI,SAAwC,EAAE,GAAG,gBAAgB,GAAG,QAAQ,YAAY;AACxF,KAAG,QAAQ,CAAC,EAAE,OAAO,MAAM;AAC3B,QAAM,WAAW,CAAC,GAAI,QAAQ,YAAY,CAAC,CAAE,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO;AACnF,aAAW,QAAQ,UAAU;AAC3B,aAAS,EAAE,GAAG,QAAQ,GAAG,KAAK,OAAO;AACrC,UAAM,UAAU,GAAG,QAAQ,KAAK,OAAO,EAAE,OAAO,MAAM;AACtD,QAAI,KAAK,QAAS,SAAQ,QAAQ,KAAK,OAAO;AAAA,EAChD;AACA,SAAO;AACT;","names":[]}
|
package/dist/sync/engine.js
CHANGED
|
@@ -48,7 +48,13 @@ function createSyncEngine(options) {
|
|
|
48
48
|
const backoffMs = options.backoffMs?.length ? options.backoffMs : DEFAULT_SYNC_BACKOFF_MS;
|
|
49
49
|
const sharedComputer = options.sharedComputer ?? isSharedComputer;
|
|
50
50
|
let generation = 0;
|
|
51
|
-
let state = {
|
|
51
|
+
let state = {
|
|
52
|
+
phase: "idle",
|
|
53
|
+
online: browserOnline(),
|
|
54
|
+
counts: ZERO,
|
|
55
|
+
lastSyncedAt: null,
|
|
56
|
+
nextRetryAt: null
|
|
57
|
+
};
|
|
52
58
|
const listeners = /* @__PURE__ */ new Set();
|
|
53
59
|
let started = false;
|
|
54
60
|
let poll = null;
|
|
@@ -108,7 +114,8 @@ function createSyncEngine(options) {
|
|
|
108
114
|
const rejected = sent.filter((c) => !refusedIds.has(c.id) && rejections.has(c.id));
|
|
109
115
|
await db.outbox.bulkDelete(sent.filter((c) => !refusedIds.has(c.id) && !rejections.has(c.id)).map((c) => c.seq));
|
|
110
116
|
const hold = refused != null && holdCodes.has(refused.code);
|
|
111
|
-
if (refused)
|
|
117
|
+
if (refused)
|
|
118
|
+
await mark(kept2, { status: hold ? "waiting" : "blocked", code: refused.code, reason: refused.detail });
|
|
112
119
|
for (const change of rejected) {
|
|
113
120
|
const why = rejections.get(change.id);
|
|
114
121
|
await db.outbox.update(change.seq, { status: "rejected", code: why.code, reason: why.detail });
|
|
@@ -137,7 +144,13 @@ function createSyncEngine(options) {
|
|
|
137
144
|
continue;
|
|
138
145
|
}
|
|
139
146
|
const row = await db.rows.get([change.entity, change.id]);
|
|
140
|
-
await db.rows.put({
|
|
147
|
+
await db.rows.put({
|
|
148
|
+
...row,
|
|
149
|
+
entity: change.entity,
|
|
150
|
+
id: change.id,
|
|
151
|
+
pk: row?.pk ?? null,
|
|
152
|
+
data: { ...row?.data, ...change.data }
|
|
153
|
+
});
|
|
141
154
|
}
|
|
142
155
|
if (lastPage) {
|
|
143
156
|
await db.meta.put({ key: lastKnowledgeKey(scope), value: answer.server_knowledge });
|
|
@@ -185,7 +198,8 @@ function createSyncEngine(options) {
|
|
|
185
198
|
await mark(sent, { status: "waiting", code: plan, reason: plan });
|
|
186
199
|
return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };
|
|
187
200
|
}
|
|
188
|
-
if (httpStatusOf(err) === 403)
|
|
201
|
+
if (httpStatusOf(err) === 403)
|
|
202
|
+
return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };
|
|
189
203
|
throw err;
|
|
190
204
|
}
|
|
191
205
|
let cursor = options.paged ? answer.page_cursor ?? null : null;
|
|
@@ -342,7 +356,9 @@ function createSyncEngine(options) {
|
|
|
342
356
|
const row = await db.rows.get([entity, id]);
|
|
343
357
|
const ofRow = await db.outbox.where("[entity+id]").equals([entity, id]).toArray();
|
|
344
358
|
ofRow.sort((a, b) => a.seq - b.seq);
|
|
345
|
-
const updated_at = stampAfter(
|
|
359
|
+
const updated_at = stampAfter(
|
|
360
|
+
latest([row?.updated_at, row?.data.updated_at, ...ofRow.map((c) => c.updated_at)])
|
|
361
|
+
);
|
|
346
362
|
const rejected = ofRow.filter((c) => c.scope === scope && c.status === "rejected");
|
|
347
363
|
if (op === "delete") {
|
|
348
364
|
await db.rows.delete([entity, id]);
|
|
@@ -413,7 +429,9 @@ function createSyncEngine(options) {
|
|
|
413
429
|
await runFresh(true);
|
|
414
430
|
},
|
|
415
431
|
async discardRejected(scope) {
|
|
416
|
-
const gone = (await db.outbox.where("status").equals("rejected").toArray()).filter(
|
|
432
|
+
const gone = (await db.outbox.where("status").equals("rejected").toArray()).filter(
|
|
433
|
+
(c) => !scope || c.scope === scope
|
|
434
|
+
);
|
|
417
435
|
if (gone.length === 0) return;
|
|
418
436
|
await db.transaction("rw", db.rows, db.outbox, db.meta, async () => {
|
|
419
437
|
await db.outbox.bulkDelete(gone.map((c) => c.seq));
|
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;
|