@eifi1/ui-kit 0.51.7 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -23
- package/dist/account/account-parts.d.ts +10 -8
- package/dist/account/account-parts.js +1 -18
- package/dist/account/account-parts.js.map +1 -1
- package/dist/account/data-export-setting-labels.js +1 -1
- package/dist/account/data-export-setting-labels.js.map +1 -1
- package/dist/account/delete-account-setting-labels.js +1 -1
- package/dist/account/delete-account-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting-labels.js +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/passkeys-setting.d.ts +9 -2
- package/dist/account/passkeys-setting.js +41 -29
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +3 -2
- package/dist/account/sessions-setting.js +10 -8
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-parts.d.ts +6 -3
- package/dist/admin/admin-parts.js +6 -21
- package/dist/admin/admin-parts.js.map +1 -1
- package/dist/admin/mail-test-card-labels.js +1 -1
- package/dist/admin/mail-test-card-labels.js.map +1 -1
- package/dist/admin/plan-change-labels.d.ts +2 -2
- package/dist/admin/plan-change-labels.js +1 -17
- package/dist/admin/plan-change-labels.js.map +1 -1
- package/dist/admin/plan-change.d.ts +3 -42
- package/dist/admin/plan-change.js +1 -1
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +3 -3
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/auth/auth-errors-labels.d.ts +11 -0
- package/dist/auth/auth-errors-labels.js +13 -0
- package/dist/auth/auth-errors-labels.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +7 -23
- package/dist/auth/auth-errors.js +2 -15
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.js +2 -1
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/forgot-password-form-labels.js +1 -1
- package/dist/auth/forgot-password-form-labels.js.map +1 -1
- package/dist/auth/forgot-password-form.js +7 -2
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form-labels.js +1 -1
- package/dist/auth/register-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form-labels.js +1 -1
- package/dist/auth/reset-password-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form.js +5 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form-labels.js +1 -1
- package/dist/auth/sign-in-form-labels.js.map +1 -1
- package/dist/auth/status-parts.d.ts +0 -3
- package/dist/auth/status-parts.js +0 -4
- package/dist/auth/status-parts.js.map +1 -1
- package/dist/auth/verify-email-labels.js +1 -1
- package/dist/auth/verify-email-labels.js.map +1 -1
- package/dist/auth/verify-email.js +7 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/cancel-subscription.d.ts +20 -8
- package/dist/billing/cancel-subscription.js +7 -5
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/dialog-close.d.ts +29 -0
- package/dist/billing/dialog-close.js +8 -0
- package/dist/billing/dialog-close.js.map +1 -0
- package/dist/billing/plan-card.d.ts +14 -4
- package/dist/billing/plan-card.js +15 -8
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-change-labels.d.ts +42 -0
- package/dist/billing/plan-change-labels.js +22 -0
- package/dist/billing/plan-change-labels.js.map +1 -0
- package/dist/billing/plan-switch.d.ts +19 -7
- package/dist/billing/plan-switch.js +8 -4
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +12 -0
- package/dist/chart.js +16 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +5 -5
- package/dist/components/account-chips.js +11 -13
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +16 -5
- package/dist/components/alert-banner.js +46 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +13 -0
- package/dist/components/amount-input.js +1 -0
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +57 -65
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +12 -2
- package/dist/components/bulk-action-bar.js +65 -21
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-look.d.ts +5 -5
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/chart-edge.d.ts +16 -0
- package/dist/components/chart-edge.js +14 -1
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-figure.d.ts +13 -0
- package/dist/components/chart-figure.js +15 -0
- package/dist/components/chart-figure.js.map +1 -0
- package/dist/components/chart-zoom-core.js +1 -4
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +8 -1
- package/dist/components/chart.js +19 -10
- package/dist/components/chart.js.map +1 -1
- package/dist/components/combobox-core.d.ts +111 -2
- package/dist/components/combobox-core.js +148 -42
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +141 -130
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +6 -5
- package/dist/components/copy-button.js +5 -3
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.js +2 -2
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +1 -1
- package/dist/components/data-table-column-settings.js +33 -7
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +4 -2
- package/dist/components/data-table-filters.js +2 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +6 -4
- package/dist/components/data-table-fit.js +4 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +1 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +1 -1
- package/dist/components/data-table-head.js +15 -12
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +12 -7
- package/dist/components/data-table-mobile.js +43 -30
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js +11 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-resize.d.ts +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +6 -2
- package/dist/components/data-table-row.js +17 -10
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-selection.d.ts +1 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +10 -2
- package/dist/components/data-table-shared.js +18 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +21 -2
- package/dist/components/data-table-sort.js +24 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +1 -1
- package/dist/components/data-table-virtual.js +18 -6
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +13 -4
- package/dist/components/data-table.js +139 -36
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +24 -2
- package/dist/components/date-field.js +33 -8
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +15 -2
- package/dist/components/date-picker.js +55 -26
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.d.ts +47 -20
- package/dist/components/date-range-picker.js +23 -7
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/error-boundary.js +37 -30
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
- package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
- package/dist/components/field-base-labels.js.map +1 -0
- package/dist/components/field-base.d.ts +4 -3
- package/dist/components/field-base.js +3 -3
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-sync-labels.d.ts +1 -0
- package/dist/components/field-sync-labels.js.map +1 -1
- package/dist/components/field-sync.d.ts +23 -1
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/form-actions.js +11 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +5 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +14 -9
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.d.ts +43 -0
- package/dist/components/inline-confirm.js +67 -0
- package/dist/components/inline-confirm.js.map +1 -0
- package/dist/components/inline-entity-combobox.d.ts +4 -3
- package/dist/components/inline-entity-combobox.js +148 -140
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +28 -112
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-page.d.ts +3 -1
- package/dist/components/legal-page.js +3 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +4 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.d.ts +2 -2
- package/dist/components/lightbox.js +17 -6
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/list.d.ts +6 -2
- package/dist/components/list.js +3 -2
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +1 -1
- package/dist/components/measured-grid.js +2 -1
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/mini-calendar.js +3 -5
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.js +3 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-grid.d.ts +55 -0
- package/dist/components/month-grid.js +148 -0
- package/dist/components/month-grid.js.map +1 -0
- package/dist/components/month-picker.d.ts +36 -4
- package/dist/components/month-picker.js +123 -146
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/multi-select.js +11 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +7 -0
- package/dist/components/pie-chart-labels.js +1 -0
- package/dist/components/pie-chart-labels.js.map +1 -1
- package/dist/components/pie-chart.d.ts +5 -3
- package/dist/components/pie-chart.js +7 -22
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +3 -0
- package/dist/components/popover.js +7 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.js +2 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +7 -0
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +3 -3
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +7 -4
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +31 -7
- package/dist/components/series-chart.js +72 -29
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +3 -3
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip-labels.d.ts +1 -0
- package/dist/components/sign-chip-labels.js.map +1 -1
- package/dist/components/sign-chip.d.ts +10 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/tabs-labels.d.ts +2 -0
- package/dist/components/tabs-labels.js +11 -0
- package/dist/components/tabs-labels.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/tabs.js +1 -1
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +2 -2
- package/dist/components/toggle-legend.js +6 -6
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -2
- package/dist/components/translation-review-editor.js +2 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.d.ts +2 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap-labels.d.ts +26 -0
- package/dist/components/treemap-labels.js +10 -0
- package/dist/components/treemap-labels.js.map +1 -0
- package/dist/components/treemap-layout.d.ts +62 -0
- package/dist/components/treemap-layout.js +126 -0
- package/dist/components/treemap-layout.js.map +1 -0
- package/dist/components/treemap.d.ts +140 -17
- package/dist/components/treemap.js +324 -97
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.d.ts +5 -5
- package/dist/components/use-table-state.js +4 -1
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +6 -2
- package/dist/data-table.js +7 -2
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +36 -7
- package/dist/eslint/role-utilities.js +42 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +25 -16
- package/dist/feedback/feedback-dialog.js +6 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-mutation.d.ts +30 -0
- package/dist/feedback/feedback-mutation.js +16 -0
- package/dist/feedback/feedback-mutation.js.map +1 -0
- package/dist/feedback/feedback-page-link.d.ts +53 -0
- package/dist/feedback/feedback-page-link.js +37 -0
- package/dist/feedback/feedback-page-link.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +12 -3
- package/dist/feedback/feedback-public-list.js +2 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +3 -1
- package/dist/feedback/feedback-replies.js +5 -4
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +5 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +64 -28
- package/dist/feedback/feedback-row-detail.js +30 -28
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +30 -11
- package/dist/feedback/feedback-status-undo.js +10 -15
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
- package/dist/feedback/feedback-submit-dialog.js +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.js +9 -3
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +1 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +21 -9
- package/dist/feedback/feedback-table.js +45 -25
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-labels.d.ts +2 -2
- package/dist/feedback/feedback-thread-labels.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +24 -23
- package/dist/feedback/feedback-thread.js +2 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +34 -0
- package/dist/feedback.js +42 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +35 -0
- package/dist/hooks/use-focus-trap.js +6 -4
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-hotkey.d.ts +5 -2
- package/dist/hooks/use-hotkey.js +5 -4
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +7 -4
- package/dist/hooks/use-long-press.js +3 -0
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-media-query.js +12 -4
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-mounted.d.ts +22 -0
- package/dist/hooks/use-mounted.js +24 -0
- package/dist/hooks/use-mounted.js.map +1 -0
- package/dist/hooks/use-overlay-history.js +19 -3
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +4 -5
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +7 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +20 -41
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -10
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +8 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +5 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +64 -17
- package/dist/index.js +77 -12
- package/dist/index.js.map +1 -1
- package/dist/landing/routing.d.ts +3 -1
- package/dist/landing/routing.js +4 -7
- package/dist/landing/routing.js.map +1 -1
- package/dist/lib/broadcast.d.ts +17 -0
- package/dist/lib/broadcast.js +30 -0
- package/dist/lib/broadcast.js.map +1 -0
- package/dist/lib/date-format.d.ts +37 -2
- package/dist/lib/date-format.js +34 -8
- package/dist/lib/date-format.js.map +1 -1
- package/dist/lib/day-format.d.ts +46 -0
- package/dist/lib/day-format.js +46 -0
- package/dist/lib/day-format.js.map +1 -0
- package/dist/lib/describe-failure.d.ts +14 -0
- package/dist/lib/describe-failure.js +9 -0
- package/dist/lib/describe-failure.js.map +1 -0
- package/dist/lib/direction.d.ts +16 -0
- package/dist/lib/direction.js +9 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/focus-left.d.ts +63 -0
- package/dist/lib/focus-left.js +67 -0
- package/dist/lib/focus-left.js.map +1 -0
- package/dist/lib/format.js +5 -14
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/http-status.d.ts +24 -0
- package/dist/lib/http-status.js +14 -0
- package/dist/lib/http-status.js.map +1 -0
- package/dist/lib/intl-cache.d.ts +14 -0
- package/dist/lib/intl-cache.js +15 -0
- package/dist/lib/intl-cache.js.map +1 -0
- package/dist/lib/isin.d.ts +10 -0
- package/dist/lib/isin.js.map +1 -1
- package/dist/lib/iso-day.d.ts +11 -0
- package/dist/lib/iso-day.js +8 -0
- package/dist/lib/iso-day.js.map +1 -0
- package/dist/lib/lazy-with-fallback.d.ts +31 -0
- package/dist/lib/lazy-with-fallback.js +40 -0
- package/dist/lib/lazy-with-fallback.js.map +1 -0
- package/dist/lib/path-pattern.d.ts +34 -0
- package/dist/lib/path-pattern.js +24 -0
- package/dist/lib/path-pattern.js.map +1 -0
- package/dist/lib/reduced-motion.d.ts +2 -1
- package/dist/lib/reduced-motion.js.map +1 -1
- package/dist/lib/round.d.ts +2 -1
- package/dist/lib/round.js +2 -1
- package/dist/lib/round.js.map +1 -1
- package/dist/lib/safe-storage.d.ts +24 -5
- package/dist/lib/safe-storage.js +27 -2
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/lib/session-refresh.js +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/share-format.d.ts +13 -0
- package/dist/lib/share-format.js +10 -0
- package/dist/lib/share-format.js.map +1 -0
- package/dist/lib/text-case.d.ts +17 -0
- package/dist/lib/text-case.js +8 -0
- package/dist/lib/text-case.js.map +1 -0
- package/dist/lib/text-match.d.ts +29 -0
- package/dist/lib/text-match.js +20 -2
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/thenable.d.ts +13 -0
- package/dist/lib/thenable.js +8 -0
- package/dist/lib/thenable.js.map +1 -0
- package/dist/lib/translation-review.d.ts +7 -2
- package/dist/lib/translation-review.js +4 -4
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/device-zone-report.js +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +2 -1
- package/dist/notifications/notification-bell.js +3 -3
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +10 -6
- package/dist/notifications/notification-inbox.js +26 -48
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +3 -2
- package/dist/notifications/notification-parts.js +1 -3
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.d.ts +6 -0
- package/dist/notifications/notifications-labels.js +1 -0
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +1 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +2 -1
- package/dist/notifications/push-devices-setting.js +27 -49
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +10 -3
- package/dist/notifications/quiet-hours-setting.js +65 -26
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/unsubscribe-panel.js +4 -7
- package/dist/notifications/unsubscribe-panel.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +39 -1
- package/dist/notifications/use-push-device.js +28 -7
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/query-persistence.d.ts +3 -1
- package/dist/offline/query-persistence.js +19 -2
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/rhf/fields.d.ts +11 -4
- package/dist/rhf/fields.js.map +1 -1
- package/dist/search/command-palette.js +1 -1
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +19 -9
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/use-command-key.d.ts +12 -1
- package/dist/search/use-command-key.js +3 -12
- package/dist/search/use-command-key.js.map +1 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.js +10 -0
- package/dist/search.js.map +1 -1
- package/dist/settings/use-settings-focus.js +1 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +21 -5
- package/dist/shell/account-menu-language.js +3 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +26 -9
- package/dist/shell/account-menu-text-size.js +3 -2
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/dev-menu.js +5 -18
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +7 -0
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +1 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +0 -2
- package/dist/shell/top-bar-parts-labels.js +0 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +27 -8
- package/dist/shell.js +28 -7
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +30 -4
- package/dist/sync/engine.js +172 -52
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +6 -2
- package/dist/sync.js +6 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js +3 -8
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -1
- package/dist/theme/contrast.js +3 -8
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-store.js +6 -6
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +3 -8
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +3 -6
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.d.ts +7 -4
- package/dist/tour/tour-catalogue.js +12 -10
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +0 -2
- package/dist/tour/tour-dock.js +0 -2
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-gates.d.ts +4 -0
- package/dist/tour/tour-gates.js +14 -0
- package/dist/tour/tour-gates.js.map +1 -1
- package/dist/tour/tour-overlay.d.ts +27 -0
- package/dist/tour/tour-overlay.js +741 -0
- package/dist/tour/tour-overlay.js.map +1 -0
- package/dist/tour/tour.d.ts +13 -3
- package/dist/tour/tour.js +59 -781
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +6 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.d.ts +3 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.d.ts +3 -1
- package/dist/wizard/validation.js.map +1 -1
- package/package.json +10 -6
- package/src/account/account-parts.ts +5 -25
- package/src/account/data-export-setting-labels.ts +1 -1
- package/src/account/delete-account-setting-labels.ts +1 -1
- package/src/account/email-change-setting-labels.ts +1 -1
- package/src/account/passkeys-setting.tsx +60 -38
- package/src/account/sessions-setting.tsx +18 -12
- package/src/admin/admin-action-confirm.tsx +1 -5
- package/src/admin/admin-parts.ts +11 -25
- package/src/admin/mail-test-card-labels.ts +1 -1
- package/src/admin/plan-change-labels.ts +5 -23
- package/src/admin/plan-change.tsx +7 -44
- package/src/admin/reviewer-scope-editor.tsx +3 -3
- package/src/auth/auth-errors-labels.ts +23 -0
- package/src/auth/auth-errors.ts +11 -38
- package/src/auth/complete-name-dialog.tsx +4 -1
- package/src/auth/forgot-password-form-labels.ts +1 -1
- package/src/auth/forgot-password-form.tsx +11 -6
- package/src/auth/register-form-labels.ts +1 -1
- package/src/auth/reset-password-form-labels.ts +1 -1
- package/src/auth/reset-password-form.tsx +8 -4
- package/src/auth/sign-in-form-labels.ts +1 -1
- package/src/auth/status-parts.tsx +0 -6
- package/src/auth/verify-email-labels.ts +1 -1
- package/src/auth/verify-email.tsx +11 -6
- package/src/billing/cancel-subscription.tsx +26 -12
- package/src/billing/dialog-close.ts +34 -0
- package/src/billing/plan-card.tsx +36 -11
- package/src/billing/plan-change-labels.ts +66 -0
- package/src/billing/plan-switch.tsx +26 -12
- package/src/chart.ts +21 -3
- package/src/components/account-chips.tsx +20 -23
- package/src/components/alert-banner.tsx +82 -5
- package/src/components/amount-input.tsx +18 -7
- package/src/components/autocomplete.tsx +64 -70
- package/src/components/bulk-action-bar.tsx +70 -2
- package/src/components/button-look.ts +5 -5
- package/src/components/button.tsx +2 -2
- package/src/components/chart-edge.ts +34 -1
- package/src/components/chart-figure.tsx +35 -0
- package/src/components/chart-zoom-core.tsx +6 -5
- package/src/components/chart.tsx +26 -5
- package/src/components/combobox-core.tsx +293 -62
- package/src/components/combobox.tsx +183 -153
- package/src/components/copy-button.tsx +34 -23
- package/src/components/country-select.tsx +16 -11
- package/src/components/data-table-column-settings.tsx +52 -10
- package/src/components/data-table-filters.ts +6 -4
- package/src/components/data-table-fit.ts +19 -5
- package/src/components/data-table-groups.tsx +1 -1
- package/src/components/data-table-head.tsx +32 -22
- package/src/components/data-table-mobile.tsx +61 -26
- package/src/components/data-table-pagination.tsx +21 -6
- package/src/components/data-table-resize.ts +1 -1
- package/src/components/data-table-row-actions.tsx +3 -3
- package/src/components/data-table-row.tsx +25 -14
- package/src/components/data-table-selection.ts +1 -1
- package/src/components/data-table-shared.tsx +33 -1
- package/src/components/data-table-sort.ts +57 -2
- package/src/components/data-table-virtual.ts +32 -8
- package/src/components/data-table.tsx +239 -68
- package/src/components/date-field.tsx +65 -8
- package/src/components/date-picker.tsx +125 -48
- package/src/components/date-range-picker.tsx +82 -31
- package/src/components/error-boundary.tsx +53 -39
- package/src/components/field-base-labels.ts +20 -0
- package/src/components/field-base.tsx +13 -7
- package/src/components/field-sync-labels.ts +1 -0
- package/src/components/field-sync.tsx +23 -1
- package/src/components/form-actions.tsx +20 -5
- package/src/components/hover-menu.tsx +18 -8
- package/src/components/icon-button.tsx +14 -9
- package/src/components/inline-confirm.tsx +116 -0
- package/src/components/inline-entity-combobox.tsx +197 -153
- package/src/components/input.tsx +1 -1
- package/src/components/jump-calendar.tsx +30 -146
- package/src/components/legal-page.tsx +10 -7
- package/src/components/legal.tsx +4 -2
- package/src/components/lightbox.tsx +21 -8
- package/src/components/list.tsx +10 -4
- package/src/components/measured-grid.tsx +8 -4
- package/src/components/mini-calendar.tsx +6 -5
- package/src/components/modal.tsx +6 -1
- package/src/components/month-grid.tsx +229 -0
- package/src/components/month-picker.tsx +192 -200
- package/src/components/multi-select.tsx +21 -9
- package/src/components/picker-sheet.tsx +12 -9
- package/src/components/pie-chart-labels.ts +8 -0
- package/src/components/pie-chart.tsx +22 -28
- package/src/components/popover.tsx +18 -1
- package/src/components/row-actions.tsx +2 -1
- package/src/components/section-nav.tsx +7 -0
- package/src/components/series-chart-axes.ts +3 -3
- package/src/components/series-chart-labels.ts +7 -4
- package/src/components/series-chart.tsx +178 -69
- package/src/components/share-card.tsx +6 -6
- package/src/components/sign-chip-labels.ts +1 -0
- package/src/components/sign-chip.tsx +10 -1
- package/src/components/tabs-labels.ts +13 -0
- package/src/components/tabs.tsx +2 -2
- package/src/components/toggle-legend.tsx +16 -11
- package/src/components/translation-review-editor.tsx +6 -5
- package/src/components/translation-review.tsx +4 -3
- package/src/components/treemap-labels.ts +39 -0
- package/src/components/treemap-layout.ts +210 -0
- package/src/components/treemap.tsx +722 -133
- package/src/components/use-table-state.ts +12 -9
- package/src/data-table.ts +9 -2
- package/src/eslint/role-utilities.ts +97 -19
- package/src/feedback/feedback-dialog.tsx +30 -19
- package/src/feedback/feedback-mutation.ts +41 -0
- package/src/feedback/feedback-page-link.ts +90 -0
- package/src/feedback/feedback-public-list.tsx +17 -5
- package/src/feedback/feedback-replies.tsx +13 -6
- package/src/feedback/feedback-row-detail-labels.ts +5 -0
- package/src/feedback/feedback-row-detail.tsx +103 -67
- package/src/feedback/feedback-status-undo.ts +41 -27
- package/src/feedback/feedback-submit-dialog.tsx +2 -2
- package/src/feedback/feedback-submit.tsx +21 -7
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/feedback/feedback-table.tsx +86 -38
- package/src/feedback/feedback-thread-labels.ts +2 -2
- package/src/feedback/feedback-thread.tsx +27 -25
- package/src/feedback.ts +42 -0
- package/src/hooks/use-focus-trap.ts +8 -24
- package/src/hooks/use-hotkey.ts +24 -7
- package/src/hooks/use-long-press.ts +16 -2
- package/src/hooks/use-media-query.ts +20 -4
- package/src/hooks/use-mounted.ts +39 -0
- package/src/hooks/use-overlay-history.ts +32 -3
- package/src/i18n/defaults.ts +4 -5
- package/src/i18n/german.ts +8 -2
- package/src/i18n/kit-labels.tsx +9 -2
- package/src/i18n/locales/es.ts +8 -2
- package/src/i18n/locales/fr.ts +21 -13
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +8 -2
- package/src/i18n/locales/zh.ts +9 -2
- package/src/i18n/review.ts +5 -0
- package/src/index.ts +103 -13
- package/src/landing/routing.tsx +14 -10
- package/src/lib/broadcast.ts +52 -0
- package/src/lib/date-format.ts +89 -13
- package/src/lib/day-format.ts +96 -0
- package/src/lib/describe-failure.ts +18 -0
- package/src/lib/direction.ts +39 -0
- package/src/lib/focus-left.ts +156 -0
- package/src/lib/format.ts +5 -17
- package/src/lib/http-status.ts +38 -0
- package/src/lib/intl-cache.ts +25 -0
- package/src/lib/isin.ts +10 -0
- package/src/lib/iso-day.ts +13 -0
- package/src/lib/lazy-with-fallback.ts +82 -0
- package/src/lib/path-pattern.ts +55 -0
- package/src/lib/reduced-motion.ts +2 -1
- package/src/lib/round.ts +7 -2
- package/src/lib/safe-storage.ts +48 -6
- package/src/lib/session-refresh.ts +1 -1
- package/src/lib/share-format.ts +18 -0
- package/src/lib/text-case.ts +19 -0
- package/src/lib/text-match.ts +57 -2
- package/src/lib/thenable.ts +19 -0
- package/src/lib/translation-review.ts +13 -5
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-bell.tsx +7 -4
- package/src/notifications/notification-inbox.tsx +40 -58
- package/src/notifications/notification-parts.tsx +3 -4
- package/src/notifications/notification-types-setting.tsx +1 -1
- package/src/notifications/notifications-labels.ts +7 -0
- package/src/notifications/push-device-setting.tsx +1 -1
- package/src/notifications/push-devices-setting.tsx +30 -55
- package/src/notifications/quiet-hours-setting.tsx +93 -31
- package/src/notifications/unsubscribe-panel.tsx +4 -7
- package/src/notifications/use-push-device.ts +76 -10
- package/src/offline/query-persistence.ts +35 -3
- package/src/rhf/fields.tsx +11 -4
- package/src/search/command-palette.tsx +2 -4
- package/src/search/global-search.tsx +36 -15
- package/src/search/use-command-key.ts +13 -12
- package/src/search.ts +9 -0
- package/src/settings/use-settings-focus.ts +3 -0
- package/src/shell/account-menu-language.tsx +27 -5
- package/src/shell/account-menu-text-size.tsx +32 -10
- package/src/shell/dev-menu.tsx +7 -21
- package/src/shell/role-switcher.tsx +7 -0
- package/src/shell/top-bar-actions.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +0 -3
- package/src/shell.ts +40 -9
- package/src/sync/engine.ts +339 -61
- package/src/sync.ts +8 -1
- package/src/testing/css-variables.ts +8 -9
- package/src/theme/contrast.ts +8 -9
- package/src/theme/palette-store.ts +9 -9
- package/src/theme/text-size.ts +6 -8
- package/src/theme/theme-store.ts +7 -9
- package/src/tour/tour-catalogue.tsx +26 -15
- package/src/tour/tour-dock.ts +0 -2
- package/src/tour/tour-gates.ts +23 -0
- package/src/tour/tour-overlay.tsx +1170 -0
- package/src/tour/tour.tsx +108 -1202
- package/src/tour/use-tour-progress.ts +1 -1
- package/src/wizard/stepper-nav.tsx +3 -1
- package/src/wizard/types.ts +11 -2
- package/src/wizard/use-wizard.ts +2 -1
- package/src/wizard/validation.ts +8 -2
- package/tokens.css +2 -1
- package/dist/components/ui-labels.js.map +0 -1
- package/dist/shell/dev-switchers.d.ts +0 -6
- package/dist/shell/dev-switchers.js +0 -12
- package/dist/shell/dev-switchers.js.map +0 -1
- package/src/components/ui-labels.ts +0 -24
- package/src/shell/dev-switchers.tsx +0 -18
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(\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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/sync/engine.ts"],"sourcesContent":["import { listenOn, postOnce } from \"../lib/broadcast\";\nimport { httpStatusOf } from \"../lib/http-status\";\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 * Across tabs (0.52; refactor audit 2026-10-30, FLW-1): every tab runs its own engine on the one\n * IndexedDB copy, and before 0.52 nothing stood between them — a second tab took the first's\n * `sending` changes as a closed tab's and sent them again, its failure laid them back so an edit\n * merged into a change already out and was deleted with it, and a sign-out in one tab was pulled\n * straight back from 0 by the other's poll. Now a round runs under a Web Lock named after the\n * database, so the rounds of all tabs take turns, first come first served; and `forget()`\n * writes a new epoch into `meta` with the clear, which every round reads, so another tab's\n * round drops its answer and that tab's engine stops.\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 — in this tab, or (0.52) in another tab on\n * the same database, heard over a BroadcastChannel: 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. (A round asked for while another tab's round holds\n * the database waits its turn, 0.52.) `dropped`: `forget()` ran during it, here or in\n * another tab, 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 * 0.52: the backstop on one round, counted from the moment it holds the database's lock\n * (a tab waiting behind a stuck round is not cut with it). Past it the round is cut: its\n * requests and steps are aborted, anything they answer or write later is dropped, and the\n * round ends `error` with a `TimeoutError` (`DOMException`), not `offline`, so the backoff\n * starts and the lock goes to the next tab even when a request or a step ignores its\n * signal. The app's own request timeouts come first; this only bounds what they miss.\n * Default 10 minutes, long on purpose: kastlan's photo backlog can take minutes, and a cut\n * round has still made progress (each photo uploaded before the cut is off the queue).\n * `0` or `Infinity`: none.\n */\n roundTimeoutMs?: number;\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 /** Starts the poll and the round on `online` (nothing on a computer marked shared).\n * An engine another tab's `forget()` stopped stays stopped through `start()`: only its\n * own `forget()` (the app's session end) or a new engine (`releaseSyncEngine`) re-arms\n * it. A start cannot tell a new sign-in from a remount in a tab whose session never\n * ended, and the latter would pull the signed-out account back into the wiped copy\n * (D15; the 0.52 code review asked, and the contract stands). */\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 of its own after the one under way — another\n * tab's too (0.52) — and answers its result. */\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). In every\n * tab (0.52): the clear writes a new epoch into `meta`, so a round of another tab's engine on\n * this database drops its answer, and that engine stops — the session it ran for has ended.\n * Another tab's `forget()` heard while this clear runs is decided by the epoch the copy is\n * left with: one that committed first was undone by this clear and stops nothing. */\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/** `roundTimeoutMs`'s default (see there). */\nconst ROUND_TIMEOUT_MS = 10 * 60_000;\n/** The longest delay `setTimeout` keeps (2^31 - 1 ms, ~24.8 days); a longer one fires at once. */\nconst MAX_TIMEOUT_MS = 2_147_483_647;\n\nconst LAST_SYNCED_AT = \"lastSyncedAt\";\n/** The copy's epoch in `meta` (0.52): a new one with every `forget()`, in its clear. */\nconst EPOCH = \"epoch\";\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/** The copy's epoch moved since this engine started: `forget()` ran in another tab. The\n * round's answer is dropped as well. */\nclass ForgottenElsewhere extends Dropped {}\n\n/** The browser's Web Locks: a secure context (https, localhost), Safari 15.4+. */\nfunction webLocks(): LockManager | undefined {\n return typeof navigator === \"undefined\" ? undefined : (navigator as { locks?: LockManager }).locks;\n}\n\n/** A `forget()`'s epoch: unique enough to tell two apart, and on plain http too, where\n * `crypto.randomUUID` is missing (refactor audit 2026-10-30, DUP-7). */\nfunction freshEpoch(): string {\n const uuid = (globalThis.crypto as Partial<Crypto> | undefined)?.randomUUID;\n return uuid ? uuid.call(globalThis.crypto) : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\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 const roundTimeoutMs = options.roundTimeoutMs ?? ROUND_TIMEOUT_MS;\n /** The rounds the deadline cut, by their signal: their late work lays nothing back. */\n const cut = new WeakSet<AbortSignal>();\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 // ── Across tabs (0.52, FLW-1) ───────────────────────────────────────────────\n /** The name of this database's Web Lock and BroadcastChannel (one origin's tabs share both,\n * as they share the IndexedDB). */\n const across = `eifi1-sync:${db.name}`;\n /** The copy's epochs this engine runs under: the one read when it started (or at its first\n * round), and each its own `forget()` wrote. Any other one means `forget()` ran elsewhere. */\n const epochs = new Set<string | null>();\n let epochRead: Promise<void> | null = null;\n /** `forget()` ran in another tab: this engine runs no round until its own `forget()`. */\n let forgotten = false;\n /** This engine's own `forget()`s whose clear is under way. */\n let forgetting = 0;\n let unlisten: (() => void) | null = null;\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 function readEpoch(): Promise<string | null> {\n return db.meta.get(EPOCH).then((entry) => (typeof entry?.value === \"string\" ? entry.value : null));\n }\n\n /** Read once per engine — at `start()`, before its first round can wait on another tab's —\n * so a `forget()` that lands while it waits is seen as another tab's. A failed read is\n * tried again by the next round. */\n function captureEpoch(): Promise<void> {\n if (epochRead) return epochRead;\n const read = readEpoch().then((epoch) => void epochs.add(epoch));\n epochRead = read;\n read.catch(() => {\n if (epochRead === read) epochRead = null;\n });\n return read;\n }\n\n /** While started, hear `forget()` in another tab at once: a step in flight sees\n * `isCurrent()` turn false, the request in flight is aborted, and the poll stops. Without\n * BroadcastChannel the next round's read of the epoch stops it instead. */\n function listen() {\n unlisten ??= listenOn(across, (data) => {\n const epoch = (data as { epoch?: unknown } | null)?.epoch;\n if (typeof epoch === \"string\" && !epochs.has(epoch)) void forgetHere();\n });\n }\n\n /** `forget()` ran in another tab: the session this engine ran for has ended there. Its\n * round in flight writes nothing more, and it runs none of its own any more.\n *\n * Not while its own `forget()` clears the copy: that clear decides, by the epoch it leaves\n * in `meta`. 0.52 code review (src/sync/engine.ts): in kastlan's sign-out cascade tab A's\n * epoch reached tab B while B's own clear waited behind A's, so B's `forget()` resolved\n * with B stopped and forgotten — the forget that re-arms an engine left it dead. */\n function forgetHere(): Promise<void> {\n if (forgotten || forgetting > 0) return Promise.resolve();\n forgotten = true;\n if (started) halt();\n else drop();\n return endSession();\n }\n\n /** The round in flight, or waiting for the lock, writes nothing more: its wait and its\n * request are aborted, its answer dropped, the queue laid back. */\n function drop() {\n generation += 1;\n controller?.abort();\n }\n\n /** What the engine shows of a session that ended — by its own `forget()` or another tab's —\n * goes with it: the steps' counts, the last sync, an error and its retry. 0.52 code review\n * (src/sync/engine.ts): the two paths each reset part of it, and an engine another tab\n * stopped showed \"error\" for a retry that never came. */\n function endSession(): Promise<void> {\n stepCounts = {};\n failures = 0;\n clearRetry();\n set({ phase: \"idle\", lastSyncedAt: null, nextRetryAt: null });\n return refreshCounts();\n }\n\n /** The round's epoch still stands in `meta`, read in the caller's transaction, so another\n * tab's `forget()` commits wholly before it or after it. Else `forget()` ran: in this tab\n * (the generation moved too) or in another — then the engine stops. */\n async function standing(gen: number, epoch: string | null): Promise<void> {\n const now = await readEpoch();\n if (gen !== generation) throw new Dropped();\n if (now !== epoch) throw epochs.has(now) ? new Dropped() : new ForgottenElsewhere();\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. Under the Web Lock (0.52) a `sending` here\n * is a closed tab's, as this assumes: no other tab's round is out. Without Web Locks\n * (plain http) it may be another open tab's, as before 0.52.\n *\n * Only under the round's epoch. 0.52 code review (src/sync/engine.ts): after another tab's\n * `forget()` had landed mid-round, every scope left still posted the kept queue under the\n * session that had ended and pulled itself from 0, only for `apply` to drop the answer. */\n function takeQueue(scope: string, withBlocked: boolean, gen: number, epoch: string | null): Promise<OutboxChange[]> {\n return db.transaction(\"rw\", db.outbox, db.meta, async () => {\n await standing(gen, epoch);\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 or its answer was dropped: each change back to the status it had.\n * Not by a round the deadline cut: its late work ends after the next round — this tab's\n * or another's — may have taken these changes `sending` again, and laid back `pending`\n * under that round an edit would merge into a change already out and be deleted with it\n * (FLW-1's lost edit). The next round's `takeQueue` takes them back instead, as a closed\n * tab's. */\n function layBack(sent: OutboxChange[], signal: AbortSignal) {\n if (cut.has(signal)) return;\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. The\n * epoch is read in the same transaction: a `forget()` of another tab either commits before\n * it (this answer is dropped) or after it (and clears what it wrote). */\n async function apply(\n scope: string,\n answer: SyncResponseBody,\n sent: OutboxChange[],\n gen: number,\n epoch: string | null,\n lastPage: boolean,\n ) {\n return db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n await standing(gen, epoch);\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 epoch: string | null,\n signal: AbortSignal,\n withBlocked: boolean,\n ): Promise<ScopeResult> {\n const sent = await takeQueue(scope, withBlocked, gen, epoch);\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 layBack(sent, signal);\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n const plan = planCode(err);\n if (!plan && httpStatusOf(err) !== 403) throw err;\n // A refusal is the answer, kept only under the round's epoch as `apply` keeps one. 0.52\n // code review (src/sync/engine.ts): a 403 heard after another tab's `forget()` marked\n // the kept queue `rejected`, never sent again, for a session that had ended.\n return db.transaction(\"rw\", db.outbox, db.meta, async (): Promise<ScopeResult> => {\n await standing(gen, epoch);\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 return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };\n });\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, epoch, cursor == null)) };\n } catch (err) {\n if (err instanceof Dropped) await layBack(sent, signal);\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, epoch, 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 || forgotten) return { ...result, outcome: \"dropped\" };\n // Aborted by stop() and forget(): the wait for the lock, then the round's requests.\n const own = new AbortController();\n controller = own;\n try {\n const locks = webLocks();\n if (!locks) return await begin(result, withBlocked, gen, own);\n let granted = false;\n try {\n // Queued, first come first served: the rounds of every tab on this database take\n // turns, and one asked for meanwhile waits for its turn (`current` and `again` keep\n // one per engine). 0.52 code review (src/sync/engine.ts): an `ifAvailable` round\n // that found the lock taken answered `paused` at once, so the sign-out dialog's\n // \"Sync now\" closed as if it had synced; its one deferred round lost a\n // `retryBlocked()`'s `blocked`; and two tabs' waits refused each other's next try —\n // the spec refuses `ifAvailable` while any request is queued. A closed tab's lock\n // is released with it.\n return await locks.request(across, { signal: own.signal }, () => {\n granted = true;\n return begin(result, withBlocked, gen, own);\n });\n } catch (err) {\n // `begin` throws only what the app's `paused()` throws. A wait aborted by stop() or\n // forget() is dropped there; a lock manager that refuses (an opaque origin, a\n // document no longer active) runs the round as before 0.52.\n if (granted) return { ...result, outcome: \"error\", error: err };\n return await begin(result, withBlocked, gen, own);\n }\n } finally {\n if (controller === own) controller = null;\n }\n }\n\n /** The round begins — under the lock, where there is one, after however long it waited:\n * the decision point, and the start of its deadline. */\n async function begin(\n result: SyncRoundResult,\n withBlocked: boolean,\n gen: number,\n own: AbortController,\n ): Promise<SyncRoundResult> {\n if (gen !== generation || forgotten) return { ...result, outcome: \"dropped\" };\n if (options.paused?.()) return { ...result, outcome: \"paused\" };\n if (!browserOnline()) return { ...result, outcome: \"offline\" };\n return withDeadline(locked(result, withBlocked, gen, own.signal), result, gen, own);\n }\n\n /** The round, or its cut at `roundTimeoutMs`, whichever comes first. 0.52 code review,\n * finding 3 (Marcel: \"kit backstop + app timeouts\"): a round holds the lock of every tab\n * on its database, and one whose request never settles (axios has no default timeout) or\n * whose step never does stopped sync in all of them until that tab was closed. The cut\n * settles the round by the race, so the lock is released even when the app ignores the\n * abort; the generation moves, so what the cut work answers or writes later is dropped. */\n function withDeadline(\n round: Promise<SyncRoundResult>,\n result: SyncRoundResult,\n gen: number,\n own: AbortController,\n ): Promise<SyncRoundResult> {\n if (!(roundTimeoutMs > 0 && Number.isFinite(roundTimeoutMs))) return round;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<SyncRoundResult>((resolve) => {\n timer = setTimeout(\n () => {\n const error = new DOMException(`The sync round ran past ${roundTimeoutMs} ms`, \"TimeoutError\");\n // Settled first: the race ends on the cut, whatever the abort below sets off.\n resolve({ ...result, outcome: \"error\", error });\n cut.add(own.signal);\n if (gen === generation) generation += 1;\n own.abort(error);\n },\n Math.min(roundTimeoutMs, MAX_TIMEOUT_MS),\n );\n });\n return Promise.race([round, deadline]).finally(() => clearTimeout(timer));\n }\n\n /** The round itself. Never throws. */\n async function locked(\n result: SyncRoundResult,\n withBlocked: boolean,\n gen: number,\n signal: AbortSignal,\n ): Promise<SyncRoundResult> {\n try {\n await captureEpoch();\n const epoch = await readEpoch();\n if (gen !== generation) throw new Dropped();\n if (!epochs.has(epoch)) throw new ForgottenElsewhere();\n set({ phase: \"syncing\" });\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, epoch, 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, 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 ForgottenElsewhere) void forgetHere();\n if (err instanceof Dropped || gen !== generation) return { ...result, outcome: \"dropped\" };\n return { ...result, outcome: \"error\", error: err };\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 /** A round of its own, begun after the one under way — and after one that a request made\n * meanwhile started as that one ended — so `retryBlocked()` never joins a round that\n * leaves `blocked` out (0.52 code review, src/sync/engine.ts). */\n async function runFresh(withBlocked: boolean): Promise<SyncRoundResult> {\n resetBackoff();\n while (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 /** `stop()`, and the end `forgetHere` puts to a started engine. */\n function halt() {\n if (!started) return;\n started = false;\n // The next engine's round waits for the dropped one to settle.\n drop();\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 unlisten?.();\n unlisten = null;\n }\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 || forgotten || 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 captureEpoch().catch(() => undefined);\n listen();\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: halt,\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 drop();\n // Its own forget() starts this engine's next session: it runs again where another\n // tab's forget() had ended it.\n forgotten = false;\n const epoch = freshEpoch();\n epochs.add(epoch);\n const tables = keepQueue ? [db.rows, db.meta] : db.tables;\n forgetting += 1;\n try {\n await db.transaction(\"rw\", tables, async () => {\n for (const table of tables) await table.clear();\n await db.meta.put({ key: EPOCH, value: epoch });\n });\n } finally {\n forgetting -= 1;\n }\n // Best-effort: the epoch in `meta` still stops another tab's next round. This engine's\n // own channel hears it too, and keeps running: the epoch is already one of its own.\n postOnce(across, { epoch });\n // Another tab's forget() met while this clear ran was left to the copy: one committed\n // before this clear was undone by it; one committed after it ended this session too.\n // (Its message is posted after its commit, so that commit is in the epoch read here.)\n const now = await readEpoch().catch(() => epoch);\n await (epochs.has(now) ? endSession() : forgetHere());\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","round"]}
|
package/dist/sync.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
export { createSyncDb, rowByPk, SYNC_DB_PK_INDEX,
|
|
1
|
+
export { createSyncDb, rowByPk, SYNC_DB_PK_INDEX,
|
|
2
|
+
/** @deprecated 0.52 — `createSyncDb`, which declares them (`extraStores` for an app's own) (API-4); removed in 0.53 */
|
|
3
|
+
SYNC_DB_STORES, } from "./sync/db.js";
|
|
2
4
|
export type { CreateSyncDbOptions, OutboxChange, OutboxStatus, SyncDb, SyncDbUpgrade, SyncedRow, SyncMetaEntry, } from "./sync/db.js";
|
|
3
|
-
export { createSyncEngine, DEFAULT_HOLD_CODES,
|
|
5
|
+
export { createSyncEngine, DEFAULT_HOLD_CODES,
|
|
6
|
+
/** @deprecated 0.52 — `createSyncEngine`, whose `backoffMs` defaults to it (API-4); removed in 0.53 */
|
|
7
|
+
DEFAULT_SYNC_BACKOFF_MS, releaseSyncEngine, syncEngineFor, } from "./sync/engine.js";
|
|
4
8
|
export type { SyncCounts, SyncEngine, SyncEngineOptions, SyncEngineState, SyncRoundResult, SyncStep, SyncStepContext, SyncStepResult, } from "./sync/engine.js";
|
|
5
9
|
export { useSyncQuery, useSyncStatus, useSyncWorker } from "./sync/hooks.js";
|
|
6
10
|
export type { SyncPageCursor, SyncRefusalBody, SyncRejectionBody, SyncRequestBody, SyncResponseBody, SyncWireChange, } from "./sync/wire.js";
|
package/dist/sync.js
CHANGED
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
|
|
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 {\n createSyncDb,\n rowByPk,\n SYNC_DB_PK_INDEX,\n /** @deprecated 0.52 — `createSyncDb`, which declares them (`extraStores` for an app's own) (API-4); removed in 0.53 */\n SYNC_DB_STORES,\n} 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 /** @deprecated 0.52 — `createSyncEngine`, whose `backoffMs` defaults to it (API-4); removed in 0.53 */\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;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AAUP;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWP,SAAS,cAAc,eAAe,qBAAqB;AAS3D,SAAS,gBAAgB,gBAAgB,YAAY,OAAO,uBAAuB;","names":[]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { themeBlocks } from "../eslint/role-utilities.js";
|
|
2
3
|
import { KIT_NAMES, TAILWIND_THEME_NAMES } from "./css-variables.generated.js";
|
|
3
4
|
const KIT_CSS_VARIABLES = new Set(KIT_NAMES.split(" "));
|
|
4
5
|
const TAILWIND_THEME = new Set(TAILWIND_THEME_NAMES.split(" "));
|
|
@@ -11,14 +12,8 @@ function stripComments(text, isCss) {
|
|
|
11
12
|
}
|
|
12
13
|
function withoutInlineTheme(css) {
|
|
13
14
|
let out = css;
|
|
14
|
-
for (const
|
|
15
|
-
|
|
16
|
-
let end = m.index + m[0].length - 1;
|
|
17
|
-
for (; end < css.length; end++) {
|
|
18
|
-
if (css[end] === "{") depth++;
|
|
19
|
-
else if (css[end] === "}" && --depth === 0) break;
|
|
20
|
-
}
|
|
21
|
-
out = out.slice(0, m.index) + css.slice(m.index, end + 1).replace(/[^\n]/g, " ") + out.slice(end + 1);
|
|
15
|
+
for (const { start, end } of themeBlocks(css, { inline: true })) {
|
|
16
|
+
out = out.slice(0, start) + out.slice(start, end).replace(/[^\n]/g, " ") + out.slice(end);
|
|
22
17
|
}
|
|
23
18
|
return out;
|
|
24
19
|
}
|
|
@@ -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) => 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 { themeBlocks } from \"../eslint/role-utilities\";\nimport { 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. The\n * blocks are the kit's one block finder's, `themeBlocks` (src/eslint/role-utilities.ts,\n * which imports nothing): this was a second brace-counting copy of it, kept equal to the\n * generator's (scripts/gen-kit-css-variables.mjs, on `themeBlocks` since DUP-13) only by\n * token-vars-declared.test.ts (0.52 code review). */\nfunction withoutInlineTheme(css: string): string {\n let out = css;\n for (const { start, end } of themeBlocks(css, { inline: true })) {\n out = out.slice(0, start) + out.slice(start, end).replace(/[^\\n]/g, \" \") + out.slice(end);\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/theme/contrast.d.ts
CHANGED
|
@@ -48,7 +48,8 @@ export interface PersistedContrast {
|
|
|
48
48
|
};
|
|
49
49
|
version?: number;
|
|
50
50
|
}
|
|
51
|
-
/** This device's own choice under `storageKey`, or `null` when it never chose
|
|
51
|
+
/** This device's own choice under `storageKey`, or `null` when it never chose (or the
|
|
52
|
+
* storage is blocked, malformed, or holds a value that is not a mode). */
|
|
52
53
|
export declare function readPersistedContrast(storageKey: string): ContrastMode | null;
|
|
53
54
|
/**
|
|
54
55
|
* Pre-paint (§10.3, §10.5): this device's choice under `storageKey` — else the account's
|
package/dist/theme/contrast.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useEffect } from "react";
|
|
|
3
3
|
import { create } from "zustand";
|
|
4
4
|
import { persist } from "zustand/middleware";
|
|
5
5
|
import { logger } from "../lib/logger.js";
|
|
6
|
-
import {
|
|
6
|
+
import { readStoredJson } from "../lib/safe-storage.js";
|
|
7
7
|
import { registerDeviceSetting } from "../lib/device-settings.js";
|
|
8
8
|
import { useMediaQuery } from "../hooks/use-media-query.js";
|
|
9
9
|
import { CONTRAST_STEPPED_VARS, contrastStep, rememberedTokenSet } from "./contrast-tokens.js";
|
|
@@ -60,14 +60,9 @@ function applyContrast(level) {
|
|
|
60
60
|
root.setAttribute(CONTRAST_ATTRIBUTE, level);
|
|
61
61
|
reapplyContrastTokens(root);
|
|
62
62
|
}
|
|
63
|
+
const holdsContrast = (blob) => isContrastMode(blob?.state?.contrast);
|
|
63
64
|
function readPersistedContrast(storageKey) {
|
|
64
|
-
|
|
65
|
-
const blob = JSON.parse(readStored(storageKey) ?? "null");
|
|
66
|
-
const mode = blob?.state?.contrast;
|
|
67
|
-
return isContrastMode(mode) ? mode : null;
|
|
68
|
-
} catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
65
|
+
return readStoredJson(storageKey, holdsContrast)?.state.contrast ?? null;
|
|
71
66
|
}
|
|
72
67
|
function applyPersistedContrast(storageKey, options = {}) {
|
|
73
68
|
let account;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/theme/contrast.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { logger } from \"../lib/logger\";\nimport { readStored } from \"../lib/safe-storage\";\nimport { registerDeviceSetting } from \"../lib/device-settings\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { CONTRAST_STEPPED_VARS, contrastStep, rememberedTokenSet } from \"./contrast-tokens\";\nimport { applyTokenSet, DEFAULT_PRESET } from \"./palette-presets\";\nimport type { PersistedAppearanceOptions } from \"./text-size\";\n\n/**\n * \"More contrast\" (docs/text-size-harmonization.md §2.4, §5, §10.5, §10.6): a switch\n * beside the text size, on by choice or — on \"System\" — when the device asks for more\n * contrast (`prefers-contrast: more`). Any text size.\n *\n * In force it is `<html data-contrast=\"more\">` (else `\"standard\"`), which does two things:\n * - the inline colour tokens are re-written one step stronger (`applyTokenSet`'s\n * contrast flag — see contrast-tokens.ts for what moves and why it is JS);\n * - tokens.css raises `--focus-ring-width` from 2 to 3 px, in light deepens the status,\n * hue and field-sync literals and `--bg-active` to read on the stepped well (0.36,\n * colour-roles §13), and turns on the `contrast:` variant for anything else a\n * component wants to change.\n *\n * The names below are frozen like the text size's: server-kit's `CONTRAST_MODES`, the\n * stored format, the attribute.\n */\n\n/** The stored choices (§10.6): server-kit's `CONTRAST_MODES`. \"system\" IS a stored value\n * — the PATCH rule refuses an explicit null, so null means only \"never chosen\". */\nexport const CONTRAST_MODES = [\"system\", \"standard\", \"more\"] as const;\n\nexport type ContrastMode = (typeof CONTRAST_MODES)[number];\n\n/** What is in force on `<html data-contrast>`: a mode with \"system\" resolved. */\nexport type ContrastLevel = \"standard\" | \"more\";\n\nconst CONTRAST_ATTRIBUTE = \"data-contrast\";\nconst PREFERS_MORE_QUERY = \"(prefers-contrast: more)\";\n\nexport function isContrastMode(value: unknown): value is ContrastMode {\n return value === \"system\" || value === \"standard\" || value === \"more\";\n}\n\n/** The mode in force: **this device's own choice → the account's → \"system\"** (§2.6).\n * The one order the pre-paint call, the apply hook and `useAccountAppearance` share. */\nexport function resolveContrastMode(device: unknown, account?: unknown): ContrastMode {\n if (isContrastMode(device)) return device;\n if (isContrastMode(account)) return account;\n return \"system\";\n}\n\n/** Whether the device asks for more contrast now. False without `matchMedia`. */\nexport function prefersMoreContrast(): boolean {\n try {\n return typeof window !== \"undefined\" && typeof window.matchMedia === \"function\"\n ? window.matchMedia(PREFERS_MORE_QUERY).matches\n : false;\n } catch {\n return false;\n }\n}\n\n/** A mode to the level it puts on `<html>`; \"system\" asks the device (or takes\n * `prefersMore`, when the caller already knows). */\nexport function contrastLevel(mode: ContrastMode, prefersMore: boolean = prefersMoreContrast()): ContrastLevel {\n return mode === \"more\" || (mode === \"system\" && prefersMore) ? \"more\" : \"standard\";\n}\n\n/** The level `<html>` carries now; \"standard\" without one. */\nexport function readContrastLevel(): ContrastLevel {\n if (typeof document === \"undefined\") return \"standard\";\n return document.documentElement.getAttribute(CONTRAST_ATTRIBUTE) === \"more\" ? \"more\" : \"standard\";\n}\n\n/**\n * Re-write the contrast-dependent tokens on `el` for its current `data-contrast`.\n *\n * With a palette layer (`applyTokenSet` has written a set here), that set is applied\n * again and steps itself. Without one — an app whose base tokens come from tokens.css,\n * like Kurvenschmiede's — the stepped tokens are written over the stylesheet for the\n * default preset in the current light/dark, or removed again: the four text and line\n * tokens in both modes, and in light the well and the four money colours too (0.36,\n * docs/colour-roles-harmonization.md §13.3 decision 10). In dark those five stay removed,\n * so the stylesheet's dark well and money paint.\n *\n * It never calls `applyTokenSet` here: that would remember a token set on `<html>`, and\n * this app would then take the palette branch above, which a theme switch does not re-run\n * (the class observer in `useApplyContrast` re-writes only while nothing is remembered).\n */\nfunction reapplyContrastTokens(el: HTMLElement): void {\n const written = rememberedTokenSet(el);\n if (written) {\n applyTokenSet(el, written);\n return;\n }\n for (const name of CONTRAST_STEPPED_VARS) el.style.removeProperty(name);\n if (el.getAttribute(CONTRAST_ATTRIBUTE) !== \"more\") return;\n const step = contrastStep(DEFAULT_PRESET[el.classList.contains(\"dark\") ? \"dark\" : \"light\"]);\n el.style.setProperty(\"--text-secondary\", step.textSecondary);\n el.style.setProperty(\"--text-muted\", step.textMuted);\n el.style.setProperty(\"--text-placeholder\", step.textPlaceholder);\n el.style.setProperty(\"--border\", step.border);\n // One line per variable, not a loop: the step's fields are not the variables' names.\n if (step.well) {\n el.style.setProperty(\"--bg-surface-2\", step.well.bgSurface2);\n el.style.setProperty(\"--money-income\", step.well.moneyIncome);\n el.style.setProperty(\"--money-expense\", step.well.moneyExpense);\n el.style.setProperty(\"--money-net\", step.well.moneyNet);\n el.style.setProperty(\"--money-neutral\", step.well.moneyNeutral);\n }\n}\n\n/** Put `level` on `<html>` and re-write the colour tokens for it. */\nexport function applyContrast(level: ContrastLevel): void {\n if (typeof document === \"undefined\") return;\n const root = document.documentElement;\n root.setAttribute(CONTRAST_ATTRIBUTE, level);\n reapplyContrastTokens(root);\n}\n\n/* ── Storage: the frozen format, the pre-paint call ─────────────────────── */\n\n/**\n * What a contrast store persists under its key — FROZEN, like the text size's:\n *\n * {\"state\":{\"contrast\":\"more\"},\"version\":1}\n *\n * `contrast` is one of {@link CONTRAST_MODES}, or `null` / absent when this device never\n * chose.\n */\nexport interface PersistedContrast {\n state?: { contrast?: ContrastMode | null };\n version?: number;\n}\n\n/** This device's own choice under `storageKey`, or `null` when it never chose. */\nexport function readPersistedContrast(storageKey: string): ContrastMode | null {\n try {\n const blob = JSON.parse(readStored(storageKey) ?? \"null\") as PersistedContrast | null;\n const mode = blob?.state?.contrast;\n return isContrastMode(mode) ? mode : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Pre-paint (§10.3, §10.5): this device's choice under `storageKey` — else the account's\n * last known value, else \"system\" — resolved against `prefers-contrast` and applied\n * BEFORE `createRoot`. Call it next to `applyPersistedPalette`, in EITHER order — the\n * palette steps itself when `data-contrast` is already set, and this re-applies the\n * palette's set when it is not; no app has to put one first (0.32.1, keksdose's 0.32\n * report asked which). Returns the level applied.\n */\nexport function applyPersistedContrast(storageKey: string, options: PersistedAppearanceOptions = {}): ContrastLevel {\n let account: unknown;\n try {\n account = options.account?.();\n } catch {\n account = undefined;\n }\n const level = contrastLevel(resolveContrastMode(readPersistedContrast(storageKey), account));\n applyContrast(level);\n return level;\n}\n\n/* ── The store ──────────────────────────────────────────────────────────── */\n\nexport interface ContrastState {\n /** This device's own choice (persisted), or `null` when it never chose — then the\n * account's value applies, else \"system\". */\n contrast: ContrastMode | null;\n /** Store this device's choice; `null` forgets it. */\n setContrast: (contrast: ContrastMode | null) => void;\n}\n\nexport interface ApplyContrastOptions {\n /** The account's value (`/auth/me`'s `contrast`): it applies while this device has no\n * choice of its own. */\n account?: string | null;\n}\n\n/**\n * Build a contrast store bound to a consumer-supplied localStorage key, shaped like\n * `createThemeStore` and {@link createTextSizeStore} (§6). It holds the DEVICE's choice;\n * the account's value comes in through `useApplyContrast({ account })`.\n *\n * `useApplyContrast` — mount it once near the root, beside `useApplyPalette`:\n * - puts the level on `<html>` and steps the tokens;\n * - on \"System\", follows `prefers-contrast: more` live (§10.5);\n * - without a palette layer, re-writes the stepped tokens when the light/dark class\n * changes, since they differ per theme.\n *\n * The persisted format is {@link PersistedContrast}, and frozen.\n */\nexport function createContrastStore(storageKey: string) {\n const useContrastStore = create<ContrastState>()(\n logger(\n persist(\n (set) => ({\n contrast: null,\n setContrast: (contrast) => set({ contrast: isContrastMode(contrast) ? contrast : null }),\n }),\n {\n name: storageKey,\n version: 1,\n partialize: (s) => ({ contrast: s.contrast }),\n merge: (persisted, current) => {\n const value = (persisted as { contrast?: unknown } | null)?.contrast;\n return { ...current, contrast: isContrastMode(value) ? value : null };\n },\n },\n ),\n \"contrast\",\n ),\n );\n // Reset this device's settings (docs/settings-harmonization.md §11.4): no choice on\n // this device, so `useApplyContrast` re-resolves to the account's mode, else \"system\",\n // and re-steps the tokens.\n registerDeviceSetting({\n key: storageKey,\n kind: \"appearance\",\n reset: () => useContrastStore.getState().setContrast(null),\n });\n\n function useApplyContrast({ account }: ApplyContrastOptions = {}): ContrastLevel {\n const device = useContrastStore((s) => s.contrast);\n // Subscribed whatever the mode (hooks cannot be skipped); only \"system\" reads it.\n const prefersMore = useMediaQuery(PREFERS_MORE_QUERY, false);\n const level = contrastLevel(resolveContrastMode(device, account), prefersMore);\n useEffect(() => {\n applyContrast(level);\n }, [level]);\n // Without a palette layer the stepped tokens are per theme: follow `.dark`.\n useEffect(() => {\n if (typeof MutationObserver === \"undefined\") return;\n const root = document.documentElement;\n const observer = new MutationObserver(() => {\n if (!rememberedTokenSet(root)) reapplyContrastTokens(root);\n });\n observer.observe(root, { attributes: true, attributeFilter: [\"class\"] });\n return () => observer.disconnect();\n }, []);\n return level;\n }\n\n return { useContrastStore, useApplyContrast };\n}\n\nexport type ContrastStore = ReturnType<typeof createContrastStore>;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/theme/contrast.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { logger } from \"../lib/logger\";\nimport { readStoredJson } from \"../lib/safe-storage\";\nimport { registerDeviceSetting } from \"../lib/device-settings\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { CONTRAST_STEPPED_VARS, contrastStep, rememberedTokenSet } from \"./contrast-tokens\";\nimport { applyTokenSet, DEFAULT_PRESET } from \"./palette-presets\";\nimport type { PersistedAppearanceOptions } from \"./text-size\";\n\n/**\n * \"More contrast\" (docs/text-size-harmonization.md §2.4, §5, §10.5, §10.6): a switch\n * beside the text size, on by choice or — on \"System\" — when the device asks for more\n * contrast (`prefers-contrast: more`). Any text size.\n *\n * In force it is `<html data-contrast=\"more\">` (else `\"standard\"`), which does two things:\n * - the inline colour tokens are re-written one step stronger (`applyTokenSet`'s\n * contrast flag — see contrast-tokens.ts for what moves and why it is JS);\n * - tokens.css raises `--focus-ring-width` from 2 to 3 px, in light deepens the status,\n * hue and field-sync literals and `--bg-active` to read on the stepped well (0.36,\n * colour-roles §13), and turns on the `contrast:` variant for anything else a\n * component wants to change.\n *\n * The names below are frozen like the text size's: server-kit's `CONTRAST_MODES`, the\n * stored format, the attribute.\n */\n\n/** The stored choices (§10.6): server-kit's `CONTRAST_MODES`. \"system\" IS a stored value\n * — the PATCH rule refuses an explicit null, so null means only \"never chosen\". */\nexport const CONTRAST_MODES = [\"system\", \"standard\", \"more\"] as const;\n\nexport type ContrastMode = (typeof CONTRAST_MODES)[number];\n\n/** What is in force on `<html data-contrast>`: a mode with \"system\" resolved. */\nexport type ContrastLevel = \"standard\" | \"more\";\n\nconst CONTRAST_ATTRIBUTE = \"data-contrast\";\nconst PREFERS_MORE_QUERY = \"(prefers-contrast: more)\";\n\nexport function isContrastMode(value: unknown): value is ContrastMode {\n return value === \"system\" || value === \"standard\" || value === \"more\";\n}\n\n/** The mode in force: **this device's own choice → the account's → \"system\"** (§2.6).\n * The one order the pre-paint call, the apply hook and `useAccountAppearance` share. */\nexport function resolveContrastMode(device: unknown, account?: unknown): ContrastMode {\n if (isContrastMode(device)) return device;\n if (isContrastMode(account)) return account;\n return \"system\";\n}\n\n/** Whether the device asks for more contrast now. False without `matchMedia`. */\nexport function prefersMoreContrast(): boolean {\n try {\n return typeof window !== \"undefined\" && typeof window.matchMedia === \"function\"\n ? window.matchMedia(PREFERS_MORE_QUERY).matches\n : false;\n } catch {\n return false;\n }\n}\n\n/** A mode to the level it puts on `<html>`; \"system\" asks the device (or takes\n * `prefersMore`, when the caller already knows). */\nexport function contrastLevel(mode: ContrastMode, prefersMore: boolean = prefersMoreContrast()): ContrastLevel {\n return mode === \"more\" || (mode === \"system\" && prefersMore) ? \"more\" : \"standard\";\n}\n\n/** The level `<html>` carries now; \"standard\" without one. */\nexport function readContrastLevel(): ContrastLevel {\n if (typeof document === \"undefined\") return \"standard\";\n return document.documentElement.getAttribute(CONTRAST_ATTRIBUTE) === \"more\" ? \"more\" : \"standard\";\n}\n\n/**\n * Re-write the contrast-dependent tokens on `el` for its current `data-contrast`.\n *\n * With a palette layer (`applyTokenSet` has written a set here), that set is applied\n * again and steps itself. Without one — an app whose base tokens come from tokens.css,\n * like Kurvenschmiede's — the stepped tokens are written over the stylesheet for the\n * default preset in the current light/dark, or removed again: the four text and line\n * tokens in both modes, and in light the well and the four money colours too (0.36,\n * docs/colour-roles-harmonization.md §13.3 decision 10). In dark those five stay removed,\n * so the stylesheet's dark well and money paint.\n *\n * It never calls `applyTokenSet` here: that would remember a token set on `<html>`, and\n * this app would then take the palette branch above, which a theme switch does not re-run\n * (the class observer in `useApplyContrast` re-writes only while nothing is remembered).\n */\nfunction reapplyContrastTokens(el: HTMLElement): void {\n const written = rememberedTokenSet(el);\n if (written) {\n applyTokenSet(el, written);\n return;\n }\n for (const name of CONTRAST_STEPPED_VARS) el.style.removeProperty(name);\n if (el.getAttribute(CONTRAST_ATTRIBUTE) !== \"more\") return;\n const step = contrastStep(DEFAULT_PRESET[el.classList.contains(\"dark\") ? \"dark\" : \"light\"]);\n el.style.setProperty(\"--text-secondary\", step.textSecondary);\n el.style.setProperty(\"--text-muted\", step.textMuted);\n el.style.setProperty(\"--text-placeholder\", step.textPlaceholder);\n el.style.setProperty(\"--border\", step.border);\n // One line per variable, not a loop: the step's fields are not the variables' names.\n if (step.well) {\n el.style.setProperty(\"--bg-surface-2\", step.well.bgSurface2);\n el.style.setProperty(\"--money-income\", step.well.moneyIncome);\n el.style.setProperty(\"--money-expense\", step.well.moneyExpense);\n el.style.setProperty(\"--money-net\", step.well.moneyNet);\n el.style.setProperty(\"--money-neutral\", step.well.moneyNeutral);\n }\n}\n\n/** Put `level` on `<html>` and re-write the colour tokens for it. */\nexport function applyContrast(level: ContrastLevel): void {\n if (typeof document === \"undefined\") return;\n const root = document.documentElement;\n root.setAttribute(CONTRAST_ATTRIBUTE, level);\n reapplyContrastTokens(root);\n}\n\n/* ── Storage: the frozen format, the pre-paint call ─────────────────────── */\n\n/**\n * What a contrast store persists under its key — FROZEN, like the text size's:\n *\n * {\"state\":{\"contrast\":\"more\"},\"version\":1}\n *\n * `contrast` is one of {@link CONTRAST_MODES}, or `null` / absent when this device never\n * chose.\n */\nexport interface PersistedContrast {\n state?: { contrast?: ContrastMode | null };\n version?: number;\n}\n\n/** The frozen format holding a mode this kit knows — the shape `readStoredJson` admits. */\nconst holdsContrast = (blob: unknown): blob is { state: { contrast: ContrastMode } } =>\n isContrastMode((blob as PersistedContrast | null)?.state?.contrast);\n\n/** This device's own choice under `storageKey`, or `null` when it never chose (or the\n * storage is blocked, malformed, or holds a value that is not a mode). */\nexport function readPersistedContrast(storageKey: string): ContrastMode | null {\n return readStoredJson(storageKey, holdsContrast)?.state.contrast ?? null;\n}\n\n/**\n * Pre-paint (§10.3, §10.5): this device's choice under `storageKey` — else the account's\n * last known value, else \"system\" — resolved against `prefers-contrast` and applied\n * BEFORE `createRoot`. Call it next to `applyPersistedPalette`, in EITHER order — the\n * palette steps itself when `data-contrast` is already set, and this re-applies the\n * palette's set when it is not; no app has to put one first (0.32.1, keksdose's 0.32\n * report asked which). Returns the level applied.\n */\nexport function applyPersistedContrast(storageKey: string, options: PersistedAppearanceOptions = {}): ContrastLevel {\n let account: unknown;\n try {\n account = options.account?.();\n } catch {\n account = undefined;\n }\n const level = contrastLevel(resolveContrastMode(readPersistedContrast(storageKey), account));\n applyContrast(level);\n return level;\n}\n\n/* ── The store ──────────────────────────────────────────────────────────── */\n\nexport interface ContrastState {\n /** This device's own choice (persisted), or `null` when it never chose — then the\n * account's value applies, else \"system\". */\n contrast: ContrastMode | null;\n /** Store this device's choice; `null` forgets it. */\n setContrast: (contrast: ContrastMode | null) => void;\n}\n\nexport interface ApplyContrastOptions {\n /** The account's value (`/auth/me`'s `contrast`): it applies while this device has no\n * choice of its own. */\n account?: string | null;\n}\n\n/**\n * Build a contrast store bound to a consumer-supplied localStorage key, shaped like\n * `createThemeStore` and {@link createTextSizeStore} (§6). It holds the DEVICE's choice;\n * the account's value comes in through `useApplyContrast({ account })`.\n *\n * `useApplyContrast` — mount it once near the root, beside `useApplyPalette`:\n * - puts the level on `<html>` and steps the tokens;\n * - on \"System\", follows `prefers-contrast: more` live (§10.5);\n * - without a palette layer, re-writes the stepped tokens when the light/dark class\n * changes, since they differ per theme.\n *\n * The persisted format is {@link PersistedContrast}, and frozen.\n */\nexport function createContrastStore(storageKey: string) {\n const useContrastStore = create<ContrastState>()(\n logger(\n persist(\n (set) => ({\n contrast: null,\n setContrast: (contrast) => set({ contrast: isContrastMode(contrast) ? contrast : null }),\n }),\n {\n name: storageKey,\n version: 1,\n partialize: (s) => ({ contrast: s.contrast }),\n merge: (persisted, current) => {\n const value = (persisted as { contrast?: unknown } | null)?.contrast;\n return { ...current, contrast: isContrastMode(value) ? value : null };\n },\n },\n ),\n \"contrast\",\n ),\n );\n // Reset this device's settings (docs/settings-harmonization.md §11.4): no choice on\n // this device, so `useApplyContrast` re-resolves to the account's mode, else \"system\",\n // and re-steps the tokens.\n registerDeviceSetting({\n key: storageKey,\n kind: \"appearance\",\n reset: () => useContrastStore.getState().setContrast(null),\n });\n\n function useApplyContrast({ account }: ApplyContrastOptions = {}): ContrastLevel {\n const device = useContrastStore((s) => s.contrast);\n // Subscribed whatever the mode (hooks cannot be skipped); only \"system\" reads it.\n const prefersMore = useMediaQuery(PREFERS_MORE_QUERY, false);\n const level = contrastLevel(resolveContrastMode(device, account), prefersMore);\n useEffect(() => {\n applyContrast(level);\n }, [level]);\n // Without a palette layer the stepped tokens are per theme: follow `.dark`.\n useEffect(() => {\n if (typeof MutationObserver === \"undefined\") return;\n const root = document.documentElement;\n const observer = new MutationObserver(() => {\n if (!rememberedTokenSet(root)) reapplyContrastTokens(root);\n });\n observer.observe(root, { attributes: true, attributeFilter: [\"class\"] });\n return () => observer.disconnect();\n }, []);\n return level;\n }\n\n return { useContrastStore, useApplyContrast };\n}\n\nexport type ContrastStore = ReturnType<typeof createContrastStore>;\n"],"mappings":"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","names":[]}
|
|
@@ -3,15 +3,15 @@ import { useEffect } from "react";
|
|
|
3
3
|
import { create } from "zustand";
|
|
4
4
|
import { persist } from "zustand/middleware";
|
|
5
5
|
import { logger } from "../lib/logger.js";
|
|
6
|
+
import { readStoredJson } from "../lib/safe-storage.js";
|
|
6
7
|
import { registerDeviceSetting } from "../lib/device-settings.js";
|
|
7
8
|
import { applyTokenSet, DEFAULT_PRESET, presetById } from "./palette-presets.js";
|
|
9
|
+
const holdsPaletteId = (blob) => {
|
|
10
|
+
const id = blob?.state?.id;
|
|
11
|
+
return typeof id === "string" && id !== "";
|
|
12
|
+
};
|
|
8
13
|
function applyPersistedPalette(storageKey, mode) {
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
const blob = JSON.parse(localStorage.getItem(storageKey) ?? "{}");
|
|
12
|
-
if (blob.state?.id) id = blob.state.id;
|
|
13
|
-
} catch {
|
|
14
|
-
}
|
|
14
|
+
const id = readStoredJson(storageKey, holdsPaletteId)?.state.id ?? DEFAULT_PRESET.id;
|
|
15
15
|
applyTokenSet(document.documentElement, presetById(id)[mode]);
|
|
16
16
|
}
|
|
17
17
|
function createPaletteStore(storageKey, useTheme) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/theme/palette-store.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport type { StoreApi, UseBoundStore } from \"zustand\";\nimport { logger } from \"../lib/logger\";\nimport { registerDeviceSetting } from \"../lib/device-settings\";\nimport type { HeatStops } from \"./chart-palette\";\nimport { applyTokenSet, DEFAULT_PRESET, presetById, type TokenSet } from \"./palette-presets\";\nimport type { ThemeState, ThemeMode } from \"./theme-store\";\n\n/**\n * Pre-hydration counterpart to `applyPersistedTheme`: read the palette id\n * persisted under `storageKey` straight from localStorage and apply that preset's\n * tokens for the given resolved `mode` to <html>, BEFORE React mounts (no flash of\n * the default palette). Falls back to the default preset for missing/malformed\n * state, so an app entrypoint never re-types the persist envelope.\n */\nexport function applyPersistedPalette(storageKey: string, mode: ThemeMode): void {\n
|
|
1
|
+
{"version":3,"sources":["../../src/theme/palette-store.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport type { StoreApi, UseBoundStore } from \"zustand\";\nimport { logger } from \"../lib/logger\";\nimport { readStoredJson } from \"../lib/safe-storage\";\nimport { registerDeviceSetting } from \"../lib/device-settings\";\nimport type { HeatStops } from \"./chart-palette\";\nimport { applyTokenSet, DEFAULT_PRESET, presetById, type TokenSet } from \"./palette-presets\";\nimport type { ThemeState, ThemeMode } from \"./theme-store\";\n\n/** zustand's envelope with a preset id in it; anything else (absent, blocked, malformed)\n * is the default preset. An unknown id is too — `presetById`'s fallback. */\nconst holdsPaletteId = (blob: unknown): blob is { state: { id: string } } => {\n const id = (blob as { state?: { id?: unknown } } | null)?.state?.id;\n return typeof id === \"string\" && id !== \"\";\n};\n\n/**\n * Pre-hydration counterpart to `applyPersistedTheme`: read the palette id\n * persisted under `storageKey` straight from localStorage and apply that preset's\n * tokens for the given resolved `mode` to <html>, BEFORE React mounts (no flash of\n * the default palette). Falls back to the default preset for missing/malformed\n * state, so an app entrypoint never re-types the persist envelope.\n */\nexport function applyPersistedPalette(storageKey: string, mode: ThemeMode): void {\n const id = readStoredJson(storageKey, holdsPaletteId)?.state.id ?? DEFAULT_PRESET.id;\n applyTokenSet(document.documentElement, presetById(id)[mode]);\n}\n\n// The palette layer flips the whole app between candidate appearance presets at\n// runtime (feedback #307), re-skinning every token-driven surface and chart.\n// Persisted like the theme so a reload keeps the selection.\ninterface PaletteState {\n id: string;\n setId: (id: string) => void;\n}\n\n/**\n * Build a palette store bound to a consumer-supplied localStorage key, wired to\n * that app's theme store (for the active light/dark mode). Returns the palette\n * hook plus the effect + selector hooks that read the active preset's tokens.\n */\nexport function createPaletteStore(storageKey: string, useTheme: UseBoundStore<StoreApi<ThemeState>>) {\n const usePalette = create<PaletteState>()(\n logger(\n persist(\n (set) => ({\n id: DEFAULT_PRESET.id,\n setId: (id) => set({ id }),\n }),\n { name: storageKey, version: 1 },\n ),\n \"palette\",\n ),\n );\n // Reset this device's settings (docs/settings-harmonization.md §11.4): the Default\n // palette, STATE ONLY. `useApplyPalette`, where the app mounts it, re-applies through\n // its effect. Never an imperative `applyTokenSet` here: it would remember a token set\n // on <html>, and an app without a palette layer (Kurvenschmiede) would then take the\n // contrast store's palette branch, where a theme switch no longer re-writes the stepped\n // tokens (KS 3.3).\n registerDeviceSetting({\n key: storageKey,\n kind: \"appearance\",\n reset: () => usePalette.getState().setId(DEFAULT_PRESET.id),\n });\n\n /** Apply the active preset's tokens to <html> as inline CSS custom properties,\n * re-applying when the preset or the light/dark theme changes. Tailwind v4\n * strips a bare root token block from the bundle, so this inline write from\n * the TS source of truth is what actually drives the colours.\n *\n * 0.32: keeps More contrast. `applyTokenSet` reads `<html data-contrast>`, which the\n * contrast store sets, and steps the tokens itself; the contrast store re-applies\n * the set written here when the contrast changes (docs/text-size-harmonization.md\n * §10.5). Neither store needs the other. */\n function useApplyPalette(): void {\n const id = usePalette((s) => s.id);\n const mode = useTheme((s) => s.mode);\n useEffect(() => {\n applyTokenSet(document.documentElement, presetById(id)[mode]);\n }, [id, mode]);\n }\n\n /** The active preset's token set for the current theme — the source for the\n * JS-interpolated charts (treemap fills, heatmap stops) that can't read CSS\n * vars. */\n function useActiveTokenSet(): TokenSet {\n const id = usePalette((s) => s.id);\n const mode = useTheme((s) => s.mode);\n return presetById(id)[mode];\n }\n\n /** The 9 categorical chart hues for the active preset/theme (treemap cells). */\n function useChartHex(): string[] {\n return useActiveTokenSet().chart;\n }\n\n /** The active preset/theme heatmap interpolation stops. */\n function useHeatStops(): HeatStops {\n return useActiveTokenSet().heat;\n }\n\n return { usePalette, useApplyPalette, useActiveTokenSet, useChartHex, useHeatStops };\n}\n\nexport type PaletteStore = ReturnType<typeof createPaletteStore>;\n"],"mappings":";AAAA,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AACvB,SAAS,eAAe;AAExB,SAAS,cAAc;AACvB,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AAEtC,SAAS,eAAe,gBAAgB,kBAAiC;AAKzE,MAAM,iBAAiB,CAAC,SAAqD;AAC3E,QAAM,KAAM,MAA8C,OAAO;AACjE,SAAO,OAAO,OAAO,YAAY,OAAO;AAC1C;AASO,SAAS,sBAAsB,YAAoB,MAAuB;AAC/E,QAAM,KAAK,eAAe,YAAY,cAAc,GAAG,MAAM,MAAM,eAAe;AAClF,gBAAc,SAAS,iBAAiB,WAAW,EAAE,EAAE,IAAI,CAAC;AAC9D;AAeO,SAAS,mBAAmB,YAAoB,UAA+C;AACpG,QAAM,aAAa,OAAqB;AAAA,IACtC;AAAA,MACE;AAAA,QACE,CAAC,SAAS;AAAA,UACR,IAAI,eAAe;AAAA,UACnB,OAAO,CAAC,OAAO,IAAI,EAAE,GAAG,CAAC;AAAA,QAC3B;AAAA,QACA,EAAE,MAAM,YAAY,SAAS,EAAE;AAAA,MACjC;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAOA,wBAAsB;AAAA,IACpB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,OAAO,MAAM,WAAW,SAAS,EAAE,MAAM,eAAe,EAAE;AAAA,EAC5D,CAAC;AAWD,WAAS,kBAAwB;AAC/B,UAAM,KAAK,WAAW,CAAC,MAAM,EAAE,EAAE;AACjC,UAAM,OAAO,SAAS,CAAC,MAAM,EAAE,IAAI;AACnC,cAAU,MAAM;AACd,oBAAc,SAAS,iBAAiB,WAAW,EAAE,EAAE,IAAI,CAAC;AAAA,IAC9D,GAAG,CAAC,IAAI,IAAI,CAAC;AAAA,EACf;AAKA,WAAS,oBAA8B;AACrC,UAAM,KAAK,WAAW,CAAC,MAAM,EAAE,EAAE;AACjC,UAAM,OAAO,SAAS,CAAC,MAAM,EAAE,IAAI;AACnC,WAAO,WAAW,EAAE,EAAE,IAAI;AAAA,EAC5B;AAGA,WAAS,cAAwB;AAC/B,WAAO,kBAAkB,EAAE;AAAA,EAC7B;AAGA,WAAS,eAA0B;AACjC,WAAO,kBAAkB,EAAE;AAAA,EAC7B;AAEA,SAAO,EAAE,YAAY,iBAAiB,mBAAmB,aAAa,aAAa;AACrF;","names":[]}
|
package/dist/theme/text-size.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useSyncExternalStore } from "react";
|
|
|
3
3
|
import { create } from "zustand";
|
|
4
4
|
import { persist } from "zustand/middleware";
|
|
5
5
|
import { logger } from "../lib/logger.js";
|
|
6
|
-
import {
|
|
6
|
+
import { readStoredJson } from "../lib/safe-storage.js";
|
|
7
7
|
import { registerDeviceSetting } from "../lib/device-settings.js";
|
|
8
8
|
const TEXT_SIZES = ["normal", "large", "xlarge"];
|
|
9
9
|
const TEXT_SCALE = { normal: 1, large: 1.25, xlarge: 1.5 };
|
|
@@ -51,14 +51,9 @@ function useTextSize() {
|
|
|
51
51
|
const size = useSyncExternalStore(subscribe, readTextSize, () => "normal");
|
|
52
52
|
return useMemo(() => ({ size, scale: TEXT_SCALE[size] }), [size]);
|
|
53
53
|
}
|
|
54
|
+
const holdsTextSize = (blob) => isTextSize(blob?.state?.size);
|
|
54
55
|
function readPersistedTextSize(storageKey) {
|
|
55
|
-
|
|
56
|
-
const blob = JSON.parse(readStored(storageKey) ?? "null");
|
|
57
|
-
const size = blob?.state?.size;
|
|
58
|
-
return isTextSize(size) ? size : null;
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
56
|
+
return readStoredJson(storageKey, holdsTextSize)?.state.size ?? null;
|
|
62
57
|
}
|
|
63
58
|
function readAccount(account) {
|
|
64
59
|
if (!account) return void 0;
|