@eifi1/ui-kit 0.51.4 → 0.51.6
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 +5 -5
- package/dist/account/account-settings-labels.js.map +1 -1
- package/dist/account/account-settings.js +11 -9
- package/dist/account/account-settings.js.map +1 -1
- package/dist/account/delete-account-setting.js +56 -65
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.js +4 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.js +19 -11
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/admin/admin-tool-links.d.ts +1 -1
- package/dist/admin/admin-tool-links.js +7 -1
- package/dist/admin/admin-tool-links.js.map +1 -1
- package/dist/admin/invitations-panel.js +2 -13
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/mail-test-card.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +4 -11
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/user-roster.js +1 -9
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.js +23 -31
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.js +13 -5
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.js +1 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/checkout-processing.js.map +1 -1
- package/dist/billing/plan-card.js +91 -85
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-limit-toast.js.map +1 -1
- package/dist/billing/plan-limit.js.map +1 -1
- package/dist/billing/plan-price.js.map +1 -1
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/bin/eifi1-pay-page.mjs +6 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.js +1 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.js +29 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +1 -4
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.js +1 -7
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.js +5 -5
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +4 -10
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/card.js +1 -8
- package/dist/components/card.js.map +1 -1
- package/dist/components/chart-zoom-core.js +3 -17
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +4 -22
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/chip-input.js +4 -1
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/chip.js +10 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.js +58 -74
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper-labels.js.map +1 -1
- package/dist/components/column-mapper.js +167 -227
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +1 -4
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +1 -4
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.d.ts +5 -4
- package/dist/components/country-select.js +3 -19
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.js +10 -8
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.js +1 -6
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.js.map +1 -1
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.js +2 -18
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.js +3 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.js +44 -56
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.js +31 -44
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-row-actions.js +19 -16
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.js +4 -1
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.js +30 -39
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +1 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/description-list.js +10 -10
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +9 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +17 -27
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +25 -35
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-base.js +5 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +1 -1
- package/dist/components/field-parts.js +10 -14
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.js +8 -10
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +4 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.js.map +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/grouped-picker.js +6 -8
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.js +1 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input-labels.js.map +1 -1
- package/dist/components/icon-button.js +4 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/icon-picker.js +2 -7
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/image-grid.js +41 -50
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +2 -8
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +14 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +1 -7
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.js +14 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.js +12 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +26 -36
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.js +22 -10
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.js +29 -53
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +13 -31
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +10 -21
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +3 -16
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/nav-pills.js +10 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.js +1 -7
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +1 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +1 -7
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +13 -22
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.js +1 -11
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/password-strength-labels.js.map +1 -1
- package/dist/components/password-strength.js +2 -8
- package/dist/components/password-strength.js.map +1 -1
- package/dist/components/phone-input.js +42 -57
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +1 -8
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +16 -9
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.js +1 -8
- package/dist/components/qr-code.js.map +1 -1
- package/dist/components/row-actions.js +5 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/select.js +28 -38
- package/dist/components/select.js.map +1 -1
- package/dist/components/separator.js +1 -5
- package/dist/components/separator.js.map +1 -1
- package/dist/components/series-chart-axes.js +4 -1
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.js +29 -45
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.js +21 -17
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.js +4 -9
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.js +4 -13
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/signed-amount.js +12 -5
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/skeleton.js +2 -17
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +13 -36
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -10
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.js +14 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +1 -1
- package/dist/components/swipeable-row.js +1 -8
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table-skeleton.js +4 -1
- package/dist/components/table-skeleton.js.map +1 -1
- package/dist/components/table.js +12 -9
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.js +5 -24
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +1 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +6 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +2 -11
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.js +2 -22
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -4
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -1
- package/dist/components/translation-review-parts.js +7 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/translation-review.js +18 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +14 -10
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.d.ts +24 -2
- package/dist/components/treemap.js +28 -4
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/use-table-state.js +14 -11
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.js +3 -26
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/demo/demo-start.js +8 -4
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-labels.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +1 -8
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-capture.d.ts +1 -1
- package/dist/feedback/feedback-capture.js +3 -1
- package/dist/feedback/feedback-capture.js.map +1 -1
- package/dist/feedback/feedback-comment-thread.js +27 -13
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-context.js +1 -4
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-public-list.js +7 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-record.js +1 -5
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.js +25 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-table.js +4 -1
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +2 -13
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback/feedback-vote-button.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-document-title.js.map +1 -1
- package/dist/hooks/use-file-drop.js.map +1 -1
- package/dist/hooks/use-file-picker-labels.js.map +1 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +10 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.d.ts +1 -1
- package/dist/hooks/use-row-swipe.js +1 -6
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.js +1 -4
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +4 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.js +2 -8
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.js.map +1 -1
- package/dist/i18n/locales/en.js +4 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +4 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +4 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +4 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +4 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.js +13 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +40 -23
- package/dist/index.js.map +1 -1
- package/dist/landing/access.js +1 -4
- package/dist/landing/access.js.map +1 -1
- package/dist/landing/landing-sections.js +35 -38
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/device-settings.js +2 -1
- package/dist/lib/device-settings.js.map +1 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/iban.js.map +1 -1
- package/dist/lib/logger.js +1 -5
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/person-name.js.map +1 -1
- package/dist/lib/phone.js.map +1 -1
- package/dist/lib/qr-encode.js +344 -8
- package/dist/lib/qr-encode.js.map +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/table-text.js +15 -3
- package/dist/lib/table-text.js.map +1 -1
- package/dist/lib/text-match.d.ts +4 -0
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/notification-inbox.js +63 -44
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +6 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/use-push-device.js +4 -1
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/pwa-update-prompt.d.ts +1 -1
- package/dist/offline/pwa-update-prompt.js +7 -1
- package/dist/offline/pwa-update-prompt.js.map +1 -1
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +6 -1
- package/dist/offline.js.map +1 -1
- package/dist/pay/pay.css +8 -1
- package/dist/rhf/fields.d.ts +1 -2
- package/dist/rhf/fields.js +1 -10
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.js +1 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/search/command-palette.js +71 -62
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +34 -5
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/settings/device-reset-setting.js +1 -6
- package/dist/settings/device-reset-setting.js.map +1 -1
- package/dist/settings/settings-heading.js.map +1 -1
- package/dist/settings/settings-layout.js +9 -20
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.js +62 -31
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -10
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/dev-menu.js +12 -6
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/sync-status-indicator.js +7 -6
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +1 -10
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.js +9 -1
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.js +24 -6
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/react.js.map +1 -1
- package/dist/testing/viewport.js +7 -4
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/contrast-tokens.js +13 -2
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -2
- package/dist/theme/palette-derive.d.ts +1 -1
- package/dist/theme/palette-derive.js +18 -29
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +1 -3
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.js +5 -1
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-engine.js +1 -4
- package/dist/tour/tour-engine.js.map +1 -1
- package/dist/tour/tour.js +21 -12
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.js +3 -24
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +24 -28
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +1 -1
- package/dist/wizard/wizard-step.js +1 -8
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +5 -2
- package/src/account/account-settings-labels.ts +1 -2
- package/src/account/account-settings.tsx +11 -17
- package/src/account/delete-account-setting.tsx +1 -6
- package/src/account/email-change-setting-labels.ts +1 -2
- package/src/account/email-change-setting.tsx +3 -1
- package/src/account/passkeys-setting.tsx +1 -3
- package/src/account/sessions-setting.tsx +4 -2
- package/src/admin/admin-action-confirm.tsx +4 -7
- package/src/admin/admin-action-log.tsx +29 -29
- package/src/admin/admin-area-labels.ts +4 -1
- package/src/admin/admin-overview.tsx +8 -2
- package/src/admin/admin-sections.tsx +3 -1
- package/src/admin/admin-tool-links.tsx +7 -1
- package/src/admin/invitations-panel.tsx +5 -11
- package/src/admin/mail-test-card.tsx +1 -2
- package/src/admin/reviewer-scope-editor.tsx +4 -9
- package/src/admin/role-select.tsx +4 -2
- package/src/admin/user-roster.tsx +2 -11
- package/src/auth/accept-invitation.tsx +1 -7
- package/src/auth/auth-errors.ts +1 -6
- package/src/auth/forgot-password-form.tsx +2 -7
- package/src/auth/register-form.tsx +7 -8
- package/src/auth/reset-password-form.tsx +2 -5
- package/src/auth/sign-in-form.tsx +15 -14
- package/src/auth/verify-email.tsx +2 -7
- package/src/billing/billing-banner.tsx +3 -7
- package/src/billing/billing-labels.ts +6 -4
- package/src/billing/checkout-processing.ts +0 -1
- package/src/billing/plan-card.tsx +9 -8
- package/src/billing/plan-limit-toast.tsx +1 -2
- package/src/billing/plan-limit.ts +1 -2
- package/src/billing/plan-price.ts +1 -4
- package/src/billing/plan-switch.tsx +1 -2
- package/src/billing/subscription-actions.tsx +5 -2
- package/src/components/account-chips.tsx +1 -7
- package/src/components/alert-banner.tsx +2 -8
- package/src/components/amount-input.tsx +206 -178
- package/src/components/authed-image.tsx +7 -4
- package/src/components/autocomplete.tsx +7 -9
- package/src/components/bulk-action-bar.tsx +2 -7
- package/src/components/button-group.tsx +1 -2
- package/src/components/button-look.ts +6 -17
- package/src/components/button.tsx +1 -4
- package/src/components/calculator.tsx +6 -9
- package/src/components/calendar-heatmap.tsx +5 -10
- package/src/components/card.tsx +9 -13
- package/src/components/chart-zoom-core.tsx +6 -33
- package/src/components/chart.tsx +8 -31
- package/src/components/checkbox-group.tsx +1 -4
- package/src/components/chip-input.tsx +6 -6
- package/src/components/chip.tsx +14 -26
- package/src/components/choice-card.tsx +4 -18
- package/src/components/column-mapper-labels.ts +3 -10
- package/src/components/column-mapper.tsx +33 -122
- package/src/components/combobox-core.tsx +12 -42
- package/src/components/combobox.tsx +2 -6
- package/src/components/confirm-dialog.tsx +1 -3
- package/src/components/copy-button.tsx +2 -4
- package/src/components/country-select.tsx +9 -21
- package/src/components/currency-option-list.tsx +13 -14
- package/src/components/currency-select.tsx +2 -7
- package/src/components/danger-confirm.tsx +8 -9
- package/src/components/data-table-cells.tsx +1 -2
- package/src/components/data-table-column-settings.tsx +1 -3
- package/src/components/data-table-filter-popover.tsx +5 -19
- package/src/components/data-table-filters.ts +1 -6
- package/src/components/data-table-fit.ts +1 -3
- package/src/components/data-table-groups.tsx +3 -1
- package/src/components/data-table-head.tsx +27 -45
- package/src/components/data-table-mobile.tsx +99 -108
- package/src/components/data-table-pagination.tsx +2 -6
- package/src/components/data-table-row-actions.tsx +25 -24
- package/src/components/data-table-row.tsx +16 -17
- package/src/components/data-table-shared.tsx +10 -16
- package/src/components/data-table-sort.ts +1 -2
- package/src/components/data-table-virtual.ts +9 -4
- package/src/components/data-table.tsx +489 -501
- package/src/components/date-picker.tsx +1 -7
- package/src/components/date-range-picker.tsx +3 -13
- package/src/components/description-list.tsx +7 -13
- package/src/components/dialog-frame.tsx +12 -3
- package/src/components/disclosure.tsx +8 -12
- package/src/components/dropdown.tsx +3 -11
- package/src/components/empty-state.tsx +1 -3
- package/src/components/entity-combobox.tsx +2 -6
- package/src/components/error-boundary.tsx +7 -7
- package/src/components/field-base.tsx +8 -14
- package/src/components/field-parts.tsx +12 -23
- package/src/components/field-sync.tsx +7 -11
- package/src/components/file-button.tsx +7 -3
- package/src/components/file-dropzone.tsx +7 -12
- package/src/components/floating-panel.tsx +44 -51
- package/src/components/focus-ring.ts +1 -2
- package/src/components/form-actions.tsx +1 -4
- package/src/components/grouped-picker.tsx +6 -8
- package/src/components/hover-menu.tsx +1 -4
- package/src/components/iban-input-labels.ts +1 -2
- package/src/components/icon-button.tsx +46 -48
- package/src/components/icon-picker.tsx +4 -17
- package/src/components/image-grid.tsx +1 -6
- package/src/components/inline-edit-field.tsx +1 -5
- package/src/components/inline-entity-combobox.tsx +6 -21
- package/src/components/input.tsx +14 -2
- package/src/components/jump-calendar.tsx +2 -6
- package/src/components/legal-labels.ts +1 -2
- package/src/components/legal-page.tsx +18 -7
- package/src/components/legal.tsx +14 -5
- package/src/components/lightbox.tsx +1 -3
- package/src/components/line-items.tsx +7 -15
- package/src/components/list.tsx +29 -25
- package/src/components/measured-grid.tsx +8 -30
- package/src/components/menu-item.tsx +1 -3
- package/src/components/mini-calendar.tsx +129 -159
- package/src/components/money-field.tsx +4 -2
- package/src/components/month-picker.tsx +14 -21
- package/src/components/multi-entity-combobox.tsx +6 -26
- package/src/components/multi-select.tsx +5 -9
- package/src/components/nav-pills.tsx +10 -2
- package/src/components/number-field.tsx +18 -15
- package/src/components/number-input.tsx +3 -19
- package/src/components/numpad-sheet.tsx +9 -13
- package/src/components/one-time-code-input.tsx +6 -11
- package/src/components/page-contents.tsx +2 -9
- package/src/components/page-header.tsx +5 -10
- package/src/components/password-strength-labels.ts +1 -2
- package/src/components/password-strength.tsx +6 -9
- package/src/components/phone-input.tsx +2 -13
- package/src/components/picker-sheet.tsx +2 -5
- package/src/components/pie-chart.tsx +6 -12
- package/src/components/popover.tsx +2 -1
- package/src/components/progress-bar.tsx +16 -25
- package/src/components/qr-code.tsx +2 -9
- package/src/components/row-actions.tsx +9 -1
- package/src/components/search-field.tsx +4 -5
- package/src/components/select.tsx +2 -10
- package/src/components/separator.tsx +1 -5
- package/src/components/series-chart-axes.ts +5 -5
- package/src/components/series-chart-budget.ts +1 -4
- package/src/components/series-chart-ticks.ts +1 -4
- package/src/components/series-chart.tsx +43 -83
- package/src/components/settings-fields.tsx +20 -15
- package/src/components/share-card.tsx +6 -14
- package/src/components/sign-chip.tsx +16 -16
- package/src/components/signature-pad.tsx +21 -13
- package/src/components/signed-amount.tsx +6 -14
- package/src/components/skeleton.tsx +2 -13
- package/src/components/slider.tsx +9 -20
- package/src/components/sparkline.tsx +2 -13
- package/src/components/stat-tile.tsx +13 -24
- package/src/components/swatch-picker.tsx +8 -3
- package/src/components/swipeable-row.tsx +2 -11
- package/src/components/table-skeleton.tsx +4 -1
- package/src/components/table.tsx +12 -8
- package/src/components/tabs.tsx +24 -39
- package/src/components/text-link.tsx +6 -6
- package/src/components/text.tsx +1 -2
- package/src/components/tile-radio.tsx +6 -19
- package/src/components/time-input.tsx +4 -5
- package/src/components/toast.tsx +3 -12
- package/src/components/toggle-group.tsx +9 -9
- package/src/components/toggle-legend.tsx +4 -16
- package/src/components/tooltip.tsx +9 -48
- package/src/components/translation-review-editor.tsx +4 -4
- package/src/components/translation-review-parts.tsx +7 -1
- package/src/components/translation-review.tsx +15 -16
- package/src/components/tree-view.tsx +17 -20
- package/src/components/treemap.tsx +59 -9
- package/src/components/trigger-aria.ts +1 -4
- package/src/components/use-table-state.ts +16 -17
- package/src/components/user-avatar.tsx +4 -21
- package/src/demo/demo-start.tsx +8 -4
- package/src/feedback/feedback-attachment-labels.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +16 -29
- package/src/feedback/feedback-capture.ts +3 -1
- package/src/feedback/feedback-comment-thread.tsx +9 -6
- package/src/feedback/feedback-context.tsx +1 -4
- package/src/feedback/feedback-dialog.tsx +3 -4
- package/src/feedback/feedback-inbox.tsx +3 -9
- package/src/feedback/feedback-public-list.tsx +7 -1
- package/src/feedback/feedback-record.ts +6 -14
- package/src/feedback/feedback-replies.tsx +10 -5
- package/src/feedback/feedback-row-detail.tsx +2 -5
- package/src/feedback/feedback-status-undo.ts +1 -4
- package/src/feedback/feedback-submit-dialog.tsx +4 -4
- package/src/feedback/feedback-table.tsx +12 -8
- package/src/feedback/feedback-thread.tsx +4 -16
- package/src/feedback/feedback-vocabulary.ts +72 -82
- package/src/feedback/feedback-vote-button.tsx +9 -8
- package/src/hooks/use-anchored-rect.ts +1 -4
- package/src/hooks/use-document-title.ts +3 -1
- package/src/hooks/use-file-drop.ts +1 -3
- package/src/hooks/use-file-picker-labels.ts +1 -2
- package/src/hooks/use-file-picker.tsx +1 -5
- package/src/hooks/use-focus-trap.ts +1 -2
- package/src/hooks/use-overlay-history.ts +10 -5
- package/src/hooks/use-row-swipe.ts +1 -6
- package/src/i18n/defaults.ts +1 -4
- package/src/i18n/german.ts +33 -49
- package/src/i18n/kit-labels.tsx +5 -20
- package/src/i18n/languages.ts +2 -8
- package/src/i18n/locales/en.ts +11 -31
- package/src/i18n/locales/es.ts +35 -49
- package/src/i18n/locales/fr.ts +34 -51
- package/src/i18n/locales/hu.ts +26 -37
- package/src/i18n/locales/it.ts +35 -52
- package/src/i18n/locales/zh.ts +14 -32
- package/src/i18n/plurals.ts +4 -1
- package/src/i18n/review.ts +18 -7
- package/src/i18n/shared-labels.ts +1 -2
- package/src/index.ts +106 -32
- package/src/landing/access.ts +2 -7
- package/src/landing/landing-sections.tsx +14 -15
- package/src/landing/page-seo.ts +1 -3
- package/src/lib/calc.ts +4 -1
- package/src/lib/clipboard-minus.ts +4 -1
- package/src/lib/dates.ts +2 -10
- package/src/lib/decimal-marks.ts +1 -3
- package/src/lib/device-settings.ts +2 -1
- package/src/lib/direction.ts +1 -3
- package/src/lib/iban.ts +89 -9
- package/src/lib/logger.ts +1 -5
- package/src/lib/person-name.ts +1 -3
- package/src/lib/phone.ts +5 -8
- package/src/lib/qr-encode.ts +32 -8
- package/src/lib/session-refresh.ts +5 -6
- package/src/lib/table-text.ts +24 -6
- package/src/lib/text-match.ts +15 -3
- package/src/lib/translation-review.ts +1 -3
- package/src/notifications/notification-inbox.tsx +17 -4
- package/src/notifications/notification-parts.tsx +1 -2
- package/src/notifications/notification-types-setting.tsx +19 -18
- package/src/notifications/notifications-labels.ts +1 -2
- package/src/notifications/push-device-setting.tsx +7 -1
- package/src/notifications/push-devices-setting.tsx +7 -3
- package/src/notifications/push-worker.ts +10 -2
- package/src/notifications/quiet-hours-setting.tsx +4 -1
- package/src/notifications/use-push-device.ts +4 -1
- package/src/offline/pwa-update-prompt.tsx +7 -1
- package/src/offline/query-persistence.ts +9 -6
- package/src/offline.ts +6 -1
- package/src/rhf/fields.tsx +50 -19
- package/src/rhf/form-core.tsx +2 -6
- package/src/rhf/line-items.tsx +5 -7
- package/src/rhf/use-rhf-wizard-step.ts +3 -12
- package/src/search/command-palette.tsx +15 -23
- package/src/search/global-search.tsx +38 -8
- package/src/search/search-index.ts +5 -1
- package/src/settings/device-reset-setting.tsx +5 -8
- package/src/settings/settings-heading.tsx +3 -1
- package/src/settings/settings-layout.tsx +6 -13
- package/src/settings/use-settings-route.ts +4 -3
- package/src/shell/app-shell.tsx +48 -24
- package/src/shell/auth-layout.tsx +1 -6
- package/src/shell/dev-menu.tsx +12 -6
- package/src/shell/option-switcher-menu.tsx +1 -3
- package/src/shell/sync-status-indicator.tsx +14 -12
- package/src/shell/top-bar-parts-labels.ts +1 -3
- package/src/shell/topbar-action-menu.tsx +9 -8
- package/src/shell/topbar-controls.tsx +5 -19
- package/src/shell/tour-menu.tsx +9 -1
- package/src/sync/db.ts +5 -1
- package/src/sync/engine.ts +44 -12
- package/src/sync.ts +16 -2
- package/src/testing/css-variables.ts +1 -2
- package/src/testing/index.ts +1 -6
- package/src/testing/react.ts +1 -2
- package/src/testing/viewport.ts +9 -9
- package/src/theme/contrast-tokens.ts +13 -2
- package/src/theme/palette-derive.ts +24 -50
- package/src/theme/palette-presets.ts +0 -1
- package/src/theme/palette-store.ts +1 -4
- package/src/theme/text-size.ts +1 -1
- package/src/theme/theme-store.ts +2 -6
- package/src/tour/tour-catalogue.tsx +4 -2
- package/src/tour/tour-dock.ts +5 -1
- package/src/tour/tour-engine.ts +2 -6
- package/src/tour/tour.tsx +28 -12
- package/src/tour/use-tour-progress.ts +8 -2
- package/src/tour/use-tour-url-sync.ts +3 -1
- package/src/vite-env.d.ts +1 -4
- package/src/wizard/stepper-nav.tsx +10 -35
- package/src/wizard/types.ts +1 -4
- package/src/wizard/use-wizard.ts +28 -43
- package/src/wizard/validation.ts +1 -5
- package/src/wizard/wizard-context.tsx +2 -6
- package/src/wizard/wizard-step.tsx +1 -8
- package/src/wizard/wizard-summary.tsx +2 -6
- package/tokens.css +393 -152
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/theme/palette-presets.ts"],"sourcesContent":["// Appearance presets — the single source of truth for the \"palette switcher\"\n// (feedback #307). Each preset is a full set of design-token values for BOTH\n// light and dark, covering the page/surface backgrounds, the brand accent, the\n// semantic money trio and the categorical chart palette (anyplot's eight \"Imprint\"\n// hues by default since 0.49, Paul Tol's nine in `PAUL_TOL_PRESET`), plus the\n// heatmap stops. A preset is applied at runtime by writing these as CSS custom\n// properties onto <html> (see palette-store.ts), which instantly re-skins every\n// token-driven surface and chart. The two heatmaps and the treemap interpolate\n// concrete hex in JS, so they read the same values from here via the hooks in\n// palette-store.\n//\n// The default preset here mirrors the :root/.dark blocks in tokens.css exactly. The\n// five hand-picked alternatives of #307 (`ALTERNATIVE_PRESETS`, a reference bank that\n// `presetById` never resolved) were removed in 0.35 (refactor audit 2026-10, API-6c;\n// deprecated in 0.34): `PALETTES` is what an app offers, and `derivePalette` makes a\n// palette of its own.\n\nimport type { HeatStops } from \"./chart-palette\";\nimport { contrastStep, rememberTokenSet } from \"./contrast-tokens\";\n\nexport interface TokenSet {\n // Backgrounds & chrome\n bgPage: string;\n bgSurface: string;\n bgSurface2: string;\n border: string;\n textPrimary: string;\n // The two quieter text roles. Derived per preset by blending `textPrimary` toward\n // that preset's own surfaces until the contrast against the WORST of bgPage /\n // bgSurface / bgSurface2 hits 7:1 and 4.8:1 — so both clear WCAG AA for normal text\n // (4.5:1) on every surface they can land on, and both inherit the preset's warmth\n // instead of importing a cool grey from Tailwind's palette.\n textSecondary: string;\n textMuted: string;\n // Interactive brand accent\n brand: string;\n brandHover: string;\n brandContrast: string;\n // Semantic money (never red/green — CVD-safe, paired with +/- cues elsewhere)\n moneyIncome: string;\n moneyExpense: string;\n moneyNet: string;\n moneyNeutral: string;\n // Categorical chart series, written as `--chart-1…9`. anyplot's eight \"Imprint\" hues by\n // default (0.49), Paul Tol \"Muted\"'s nine in `PAUL_TOL_PRESET`. `paletteFor` cycles\n // eight; a shorter ramp is repeated to fill `--chart-9` (see `applyTokenSet`).\n chart: string[];\n // Heatmap interpolation stops (variance diverging + spending sequential)\n heat: HeatStops;\n}\n\nexport interface PalettePreset {\n id: string;\n name: string;\n blurb: string;\n light: TokenSet;\n dark: TokenSet;\n}\n\n// ── \"Imprint\" chart hues (feedback #119; the default since 0.49) ─────────────\n// anyplot's categorical palette (https://anyplot.ai/palette), by a colleague of\n// Marcel's. 8 hues + an amber semantic anchor, derived from Wong 2011 by a\n// Petroff-style max-min ΔE search in CAM02-UCS with Machado 2009 CVD simulation —\n// the same lineage as Paul Tol \"Muted\", one generation on, and tuned for exactly the\n// warm cream paper our light theme uses.\n//\n// 0.49 (Marcel, 2026-10-22): the DEFAULT, in anyplot's slot order, eight slots — amber\n// #DDCC77 left the pool, it is anyplot's semantic warning anchor, not a series colour.\n// The SAME hues in both themes: 0.48 lifted matte red and rose for the dark ground\n// (#BF413E, #A25083), which made the dark chart a different palette from the light one.\n// anyplot answers a hue under 3:1 with a 1px ink edge instead, and so does the kit now\n// (`chartEdge`, drawn by SeriesChart's bars and areas and by Treemap's tiles).\n//\n// Measured on our surfaces (CIEDE2000, Machado severity-100, 8 like-for-like slots on\n// the cream light surface #f6ecd9) — neither palette dominates:\n// Paul Tol Muted min-contrast 1.38 · worst-CVD ΔE00 11.8 · normal ΔE00 16.1\n// imprint min-contrast 1.95 · worst-CVD ΔE00 7.2 · normal ΔE00 22.2\n// i.e. imprint is punchier and has no near-invisible pale slot (Tol's #88ccee sits at\n// 1.5:1 on cream), while Tol separates better under colour-vision deficiency — which is\n// why `paletteFor` offers anyplot's CVD-optimal order (`{ order: \"cvd\" }`).\nconst IMPRINT_HUES = [\n \"#009e73\", // brand green — first series, always\n \"#c475fd\", // lavender — creative\n \"#4467a3\", // blue — cool / info\n \"#bd8233\", // ochre — earth / commodity\n \"#ae3030\", // matte red — bad / loss / error\n \"#2abccd\", // cyan — sky / tech-cool\n \"#954477\", // rose — wellness / health\n \"#99b314\", // lime — growth / nature\n];\n\n/** anyplot's scales (0.49): sequential green → blue, diverging blue (under) ↔ paper or\n * ink (0) ↔ matte red (over). `chart-palette.ts`'s `HEATMAP_HEX` holds the same. */\nconst ANYPLOT_HEAT: Record<\"light\" | \"dark\", HeatStops> = {\n light: {\n neutral: \"#faf8f1\",\n under: \"#4467a3\",\n over: \"#ae3030\",\n seqLow: \"#009e73\",\n seqHigh: \"#4467a3\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n dark: {\n neutral: \"#1a1a17\",\n under: \"#4467a3\",\n over: \"#ae3030\",\n seqLow: \"#009e73\",\n seqHigh: \"#4467a3\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n};\n\n// The shipped default (feedback #328): SOLAR-DUSK warm light + INDIGO-NOIR deep\n// dark surfaces — the two curated presets the user picked per mode — but with a\n// SINGLE unified brand accent (indigo) across both modes rather than each\n// preset's own accent (the user asked for the brand accent to stay the same).\n// So the light block is Solar Dusk with its teal brand overridden to indigo, and\n// the dark block is Indigo Noir (already indigo). The money trio\n// (income/expense/net) drives the chart bars and stays palette-driven so those\n// semantic colours adapt to each mode's contrast (best practice — feedback #328).\n//\n// 0.49: its chart ramp and heat stops are anyplot's (\"Imprint\", above) — the money\n// trio stays the kit's own (Marcel, 2026-10-22: chart income/expense/net/neutral keep\n// the money tokens, never anyplot's green/red). The 0.48 default, the same surfaces\n// with Paul Tol's ramp, is `PAUL_TOL_PRESET`.\nexport const DEFAULT_PRESET: PalettePreset = {\n id: \"default\",\n name: \"Imprint\",\n blurb: \"anyplot.ai chart hues · solar-dusk light · indigo-noir dark\",\n light: {\n // Solar Dusk (light) surfaces, with the brand unified to indigo (see above).\n bgPage: \"#ede3cf\",\n bgSurface: \"#f6ecd9\",\n // The well is the page in light (0.33, k26): #f3ead6 stood 1.02:1 from the card it\n // is inset in. Every light preset does the same; see SURFACE_L in palette-derive.ts.\n bgSurface2: \"#ede3cf\",\n border: \"#e2d6bd\",\n textPrimary: \"#463f33\",\n textSecondary: \"#50493c\",\n textMuted: \"#696153\",\n brand: \"#4f46e5\",\n brandHover: \"#4338ca\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#0b6650\",\n moneyExpense: \"#905902\",\n moneyNet: \"#7a4fa3\",\n moneyNeutral: \"#6b6453\",\n chart: IMPRINT_HUES,\n heat: ANYPLOT_HEAT.light,\n },\n dark: {\n // Indigo Noir (dark)\n bgPage: \"#0e0f1c\",\n bgSurface: \"#181a2c\",\n bgSurface2: \"#21243a\",\n border: \"#2e3252\",\n textPrimary: \"#e6e8f4\",\n textSecondary: \"#adafbe\",\n textMuted: \"#8d90a0\",\n brand: \"#8b93f8\",\n brandHover: \"#aab1fb\",\n brandContrast: \"#16182b\",\n moneyIncome: \"#22c3b6\",\n // Amber-400 (#fbbf24) sat at OKLab L 0.84 while every other dark token lives\n // at 0.68–0.74, so outflows outshouted the inflows they are paired with. Same\n // hue and chroma, pulled to L 0.785 (dev#434). The heat stops below keep the\n // brighter amber on purpose: a gradient endpoint is meant to be the loudest\n // thing in its scale, and it is a fill, not a figure.\n moneyExpense: \"#e4b035\",\n moneyNet: \"#b39aef\",\n moneyNeutral: \"#9aa1bd\",\n chart: IMPRINT_HUES,\n heat: ANYPLOT_HEAT.dark,\n },\n};\n\n\n/**\n * The 0.48 default: the same surfaces, brand and money, with Paul Tol's \"Muted\" nine\n * as the chart ramp and the teal/amber heat stops that went with it. Selectable, no\n * longer the default (0.49). `paletteFor` cycles eight, so its ninth hue (#aa4499) is\n * reachable only as `var(--chart-9)`.\n */\nexport const PAUL_TOL_PRESET: PalettePreset = {\n id: \"paul-tol\",\n name: \"Paul Tol muted\",\n blurb: \"Paul Tol's CVD-safe chart hues · solar-dusk light · indigo-noir dark\",\n light: {\n ...DEFAULT_PRESET.light,\n chart: [\"#332288\", \"#88ccee\", \"#44aa99\", \"#117733\", \"#999933\", \"#ddcc77\", \"#cc6677\", \"#882255\", \"#aa4499\"],\n heat: {\n neutral: \"#f1e7d2\",\n under: \"#0b6650\",\n over: \"#9c6418\",\n seqLow: \"#f6ecd0\",\n seqHigh: \"#9c6418\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n },\n dark: {\n ...DEFAULT_PRESET.dark,\n chart: [\"#7e72d6\", \"#9fd8f2\", \"#5fc4b0\", \"#3fa45f\", \"#bfbf5e\", \"#e8dda0\", \"#e08c9a\", \"#c25a78\", \"#cc78be\"],\n heat: {\n neutral: \"#363a5a\",\n under: \"#22c3b6\",\n over: \"#fbbf24\",\n seqLow: \"#3a2c14\",\n seqHigh: \"#fbbf24\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n },\n};\n\n// The full-theme switcher from #307 stayed retired (feedback #328) — the default\n// is still the single source of truth for surfaces, brand and money (its five\n// alternatives are gone, see the header). What IS selectable is\n// the categorical chart ramp (feedback #119), so Imprint (the default) and Paul Tol are\n// the live set; presetById falls back to the default for any stale persisted id.\n/**\n * The categorical chart ramp, shared by every derived preset below.\n *\n * The default's ramp (anyplot's eight since 0.49, Paul Tol's \"Muted\" before), and\n * deliberately NOT re-derived per preset. A\n * categorical ramp solves a different problem from a UI palette — separability among\n * nine unknown siblings, rather than contrast against one known surface — and a set\n * optimised against real dichromat confusion lines beats one spaced evenly by formula.\n * `auditChartRamp` in palette-derive.ts measures it: 0.059 minimum separation in the\n * light variant, which is the number MIN_SEPARATION was calibrated under.\n *\n * It pays for that in contrast: four of the nine light hues sit below the 3:1 WCAG\n * 1.4.11 asks of a filled shape. That is not fixable by darkening them — see\n * SINGLE_LIGHTNESS_MAX_SERIES — so a chart with more than four series must carry a\n * second channel (a direct label, a pattern, a legend adjacent to the mark).\n */\nconst KIT_RAMP = {\n light: DEFAULT_PRESET.light.chart,\n dark: DEFAULT_PRESET.dark.chart,\n} as const;\n\n/**\n * Presets DERIVED rather than picked, by `derivePalette` from one brand colour each.\n *\n * Every text role here was solved to a contrast target against the worst of the three\n * surfaces it can land on, every semantic colour against both the page and its own badge\n * tint, and the whole set is re-checked by `__tests__/palette-audit.test.ts` (\"derived\n * presets\") — which re-runs the deriver and fails if these committed values drift from\n * what it now produces.\n *\n * They are committed rather than derived at import because deriving eight token sets\n * costs ~34ms, and charging every consumer that at startup for palettes most of them\n * will never select is the wrong trade. The test is what keeps the two in agreement.\n *\n * Regenerated in 0.34 (round 0.34 BUG-8): the solve now measures the hex it ships rather\n * than the float before rounding, and the audit no longer lets a hex pair pass 0.05 under\n * its threshold. 52 values moved one step (66 counting the heat stops that repeat them);\n * Contrast's neutrals lost half their slate tint, its brand having half the chroma at\n * which the neutrals carry a hue in full.\n */\nexport const DERIVED_PRESETS: PalettePreset[] = [\n {\n id: \"ink\",\n name: \"Ink\",\n blurb: \"Cool blue-grey ground · solved for AAA body text\",\n light: {\n bgPage: \"#dce2ef\",\n bgSurface: \"#e6edfa\",\n bgSurface2: \"#dce2ef\",\n border: \"#b0bace\",\n textPrimary: \"#2e3139\",\n textSecondary: \"#454850\",\n textMuted: \"#5f636c\",\n brand: \"#2f5fd0\",\n brandHover: \"#1f4cbc\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00725f\",\n moneyExpense: \"#8e5900\",\n moneyNet: \"#7356a2\",\n moneyNeutral: \"#61646c\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#dce2ef\",\n under: \"#00725f\",\n over: \"#8e5900\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8e5900\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#090d15\",\n bgSurface: \"#131720\",\n bgSurface2: \"#1d212b\",\n border: \"#363e4f\",\n textPrimary: \"#c8ccd6\",\n textSecondary: \"#a7abb5\",\n textMuted: \"#858a92\",\n brand: \"#3466d7\",\n brandHover: \"#2353c3\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a1a5ac\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#1d212b\",\n under: \"#009a80\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"moss\",\n name: \"Moss\",\n blurb: \"Green ground · solved for AAA body text\",\n light: {\n bgPage: \"#d8e6dd\",\n bgSurface: \"#e3f1e8\",\n bgSurface2: \"#d8e6dd\",\n border: \"#abbfb2\",\n textPrimary: \"#2c332e\",\n textSecondary: \"#424b45\",\n textMuted: \"#5d6660\",\n brand: \"#2f7a52\",\n brandHover: \"#196842\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00725f\",\n moneyExpense: \"#8e5900\",\n moneyNet: \"#7356a2\",\n moneyNeutral: \"#5f6662\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#d8e6dd\",\n under: \"#00725f\",\n over: \"#8e5900\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8e5900\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#060f0a\",\n bgSurface: \"#101a14\",\n bgSurface2: \"#1a241e\",\n border: \"#314337\",\n textPrimary: \"#c6cfc9\",\n textSecondary: \"#a5aea8\",\n textMuted: \"#838c86\",\n brand: \"#317b53\",\n brandHover: \"#1c6943\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b87c2a\",\n moneyNet: \"#977ac9\",\n moneyNeutral: \"#9fa7a2\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#1a241e\",\n under: \"#009a80\",\n over: \"#b87c2a\",\n seqLow: \"#331f05\",\n seqHigh: \"#b87c2a\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"plum\",\n name: \"Plum\",\n blurb: \"Violet ground · solved for AAA body text\",\n light: {\n bgPage: \"#e7dfea\",\n bgSurface: \"#f2e9f5\",\n bgSurface2: \"#e7dfea\",\n border: \"#c2b5c6\",\n textPrimary: \"#352f36\",\n textSecondary: \"#4c464e\",\n textMuted: \"#676169\",\n brand: \"#7b3f8f\",\n brandHover: \"#692d7d\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00715e\",\n moneyExpense: \"#8d5800\",\n moneyNet: \"#7355a2\",\n moneyNeutral: \"#676269\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#e7dfea\",\n under: \"#00715e\",\n over: \"#8d5800\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8d5800\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#100b12\",\n bgSurface: \"#1b151d\",\n bgSurface2: \"#251f27\",\n border: \"#453b48\",\n textPrimary: \"#d0cad2\",\n textSecondary: \"#afa9b1\",\n textMuted: \"#8d8790\",\n brand: \"#9154a6\",\n brandHover: \"#7f4293\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009980\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a8a2aa\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#251f27\",\n under: \"#009980\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"contrast\",\n name: \"High contrast\",\n blurb: \"Near-neutral ground · text solved to 13:1 / 8.5:1 / 5.5:1\",\n light: {\n bgPage: \"#e1e2e3\",\n bgSurface: \"#ecedee\",\n bgSurface2: \"#e1e2e3\",\n border: \"#b8babc\",\n textPrimary: \"#1b1d21\",\n textSecondary: \"#3a3c40\",\n textMuted: \"#55585c\",\n brand: \"#1f2937\",\n brandHover: \"#111b28\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00715e\",\n moneyExpense: \"#8d5800\",\n moneyNet: \"#7255a1\",\n moneyNeutral: \"#616467\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#e1e2e3\",\n under: \"#00715e\",\n over: \"#8d5800\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8d5800\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#0c0d0e\",\n bgSurface: \"#171718\",\n bgSurface2: \"#212122\",\n border: \"#3d3e3f\",\n textPrimary: \"#e4e8ec\",\n textSecondary: \"#b9bdc1\",\n textMuted: \"#95979c\",\n brand: \"#556c8e\",\n brandHover: \"#455b7c\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a2a5a8\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#212122\",\n under: \"#009a80\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n];\n\nexport const PALETTES: PalettePreset[] = [DEFAULT_PRESET, PAUL_TOL_PRESET, ...DERIVED_PRESETS];\n\nexport function presetById(id: string): PalettePreset {\n return PALETTES.find((p) => p.id === id) ?? DEFAULT_PRESET;\n}\n\n/**\n * Write a token set onto an element's inline style as CSS custom properties.\n *\n * This is the only writer, and there is no un-writer: `presetById` falls back to\n * `DEFAULT_PRESET` and this then writes the full set, so returning to the default\n * APPLIES it rather than clearing anything. A `clearTokenSet` and the hand-kept\n * 24-name `TOKEN_VARS` list it looped over used to live here, both exported, both\n * called by nobody in either consumer — a second copy of these property names with\n * nothing comparing the two, so adding a token here and forgetting it there would\n * have been silent and only a dead function would have noticed. The list is gone;\n * the test beside this asserts instead that every field of a `TokenSet` reaches the\n * element, which is what that list was standing in for.\n *\n * **More contrast (0.32, docs/text-size-harmonization.md §5, §10.5).** With `contrast`\n * the text and border tokens are written one step stronger — in a light set the well and\n * the four money colours too (0.36, docs/colour-roles-harmonization.md §13) — and `--text-placeholder` —\n * otherwise derived in tokens.css — is written inline too; without it that inline\n * placeholder is removed again, so the stylesheet's formula applies. The step is made\n * HERE because these tokens are inline: no `[data-contrast=more]` rule could beat them.\n *\n * `contrast` defaults to the element's own `data-contrast=\"more\"`, which the contrast\n * layer (`applyContrast`, `useApplyContrast`) sets first. That is how the palette and the\n * contrast cooperate without knowing each other: a palette change re-applies through\n * here and keeps the contrast, and a contrast change re-applies the set remembered here.\n */\nexport function applyTokenSet(\n el: HTMLElement,\n t: TokenSet,\n contrast: boolean = el.getAttribute(\"data-contrast\") === \"more\",\n): void {\n rememberTokenSet(el, t);\n const step = contrast ? contrastStep(t) : null;\n el.style.setProperty(\"--bg-page\", t.bgPage);\n el.style.setProperty(\"--bg-surface\", t.bgSurface);\n // The light well and money step under More contrast (0.36, colour-roles §13); in dark\n // and at Standard `well` is null and the set's own are written.\n el.style.setProperty(\"--bg-surface-2\", step?.well?.bgSurface2 ?? t.bgSurface2);\n el.style.setProperty(\"--border\", step?.border ?? t.border);\n el.style.setProperty(\"--text-primary\", t.textPrimary);\n el.style.setProperty(\"--text-secondary\", step?.textSecondary ?? t.textSecondary);\n el.style.setProperty(\"--text-muted\", step?.textMuted ?? t.textMuted);\n if (step) el.style.setProperty(\"--text-placeholder\", step.textPlaceholder);\n else el.style.removeProperty(\"--text-placeholder\");\n el.style.setProperty(\"--brand\", t.brand);\n el.style.setProperty(\"--brand-hover\", t.brandHover);\n el.style.setProperty(\"--brand-contrast\", t.brandContrast);\n el.style.setProperty(\"--money-income\", step?.well?.moneyIncome ?? t.moneyIncome);\n el.style.setProperty(\"--money-expense\", step?.well?.moneyExpense ?? t.moneyExpense);\n el.style.setProperty(\"--money-net\", step?.well?.moneyNet ?? t.moneyNet);\n el.style.setProperty(\"--money-neutral\", step?.well?.moneyNeutral ?? t.moneyNeutral);\n // All nine `--chart-N`, a shorter ramp repeated: Imprint's eight write `--chart-9` as\n // slot 0 — what `paletteFor(8)` names — so a host addressing `var(--chart-9)` directly\n // follows the cycle, and switching away from Paul Tol's nine leaves no stale ninth hue.\n // An empty ramp (a `derivePalette` set) leaves whatever was there, deliberately.\n if (t.chart.length > 0) {\n for (let i = 0; i < 9; i++) el.style.setProperty(`--chart-${i + 1}`, t.chart[i % t.chart.length]);\n }\n}\n"],"mappings":";AAkBA,SAAS,cAAc,wBAAwB;AA8D/C,MAAM,eAAe;AAAA,EACnB;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF;AAIA,MAAM,eAAoD;AAAA,EACxD,OAAO;AAAA,IACL,SAAS;AAAA,IACT,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,SAAS;AAAA,IACT,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AACF;AAeO,MAAM,iBAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA;AAAA,IAEL,QAAQ;AAAA,IACR,WAAW;AAAA;AAAA;AAAA,IAGX,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,eAAe;AAAA,IACf,WAAW;AAAA,IACX,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,aAAa;AAAA,IACb,cAAc;AAAA,IACd,UAAU;AAAA,IACV,cAAc;AAAA,IACd,OAAO;AAAA,IACP,MAAM,aAAa;AAAA,EACrB;AAAA,EACA,MAAM;AAAA;AAAA,IAEJ,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,eAAe;AAAA,IACf,WAAW;AAAA,IACX,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMb,cAAc;AAAA,IACd,UAAU;AAAA,IACV,cAAc;AAAA,IACd,OAAO;AAAA,IACP,MAAM,aAAa;AAAA,EACrB;AACF;AASO,MAAM,kBAAiC;AAAA,EAC5C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA,IACL,GAAG,eAAe;AAAA,IAClB,OAAO,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAAA,IACzG,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,OAAO;AAAA,MACP,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AAAA,EACA,MAAM;AAAA,IACJ,GAAG,eAAe;AAAA,IAClB,OAAO,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAAA,IACzG,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,OAAO;AAAA,MACP,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AACF;AAuBA,MAAM,WAAW;AAAA,EACf,OAAO,eAAe,MAAM;AAAA,EAC5B,MAAM,eAAe,KAAK;AAC5B;AAqBO,MAAM,kBAAmC;AAAA,EAC9C;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,eAAe;AAAA,MACf,WAAW;AAAA,MACX,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,aAAa;AAAA,MACb,cAAc;AAAA,MACd,UAAU;AAAA,MACV,cAAc;AAAA,MACd,OAAO,SAAS;AAAA,MAChB,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,eAAe;AAAA,MACf,WAAW;AAAA,MACX,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,aAAa;AAAA,MACb,cAAc;AAAA,MACd,UAAU;AAAA,MACV,cAAc;AAAA,MACd,OAAO,SAAS;AAAA,MAChB,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,eAAe;AAAA,MACf,WAAW;AAAA,MACX,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,aAAa;AAAA,MACb,cAAc;AAAA,MACd,UAAU;AAAA,MACV,cAAc;AAAA,MACd,OAAO,SAAS;AAAA,MAChB,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,eAAe;AAAA,MACf,WAAW;AAAA,MACX,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,aAAa;AAAA,MACb,cAAc;AAAA,MACd,UAAU;AAAA,MACV,cAAc;AAAA,MACd,OAAO,SAAS;AAAA,MAChB,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,eAAe;AAAA,MACf,WAAW;AAAA,MACX,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,aAAa;AAAA,MACb,cAAc;AAAA,MACd,UAAU;AAAA,MACV,cAAc;AAAA,MACd,OAAO,SAAS;AAAA,MAChB,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,eAAe;AAAA,MACf,WAAW;AAAA,MACX,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,aAAa;AAAA,MACb,cAAc;AAAA,MACd,UAAU;AAAA,MACV,cAAc;AAAA,MACd,OAAO,SAAS;AAAA,MAChB,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,eAAe;AAAA,MACf,WAAW;AAAA,MACX,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,aAAa;AAAA,MACb,cAAc;AAAA,MACd,UAAU;AAAA,MACV,cAAc;AAAA,MACd,OAAO,SAAS;AAAA,MAChB,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,eAAe;AAAA,MACf,WAAW;AAAA,MACX,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,aAAa;AAAA,MACb,cAAc;AAAA,MACd,UAAU;AAAA,MACV,cAAc;AAAA,MACd,OAAO,SAAS;AAAA,MAChB,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAA4B,CAAC,gBAAgB,iBAAiB,GAAG,eAAe;AAEtF,SAAS,WAAW,IAA2B;AACpD,SAAO,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK;AAC9C;AA2BO,SAAS,cACd,IACA,GACA,WAAoB,GAAG,aAAa,eAAe,MAAM,QACnD;AACN,mBAAiB,IAAI,CAAC;AACtB,QAAM,OAAO,WAAW,aAAa,CAAC,IAAI;AAC1C,KAAG,MAAM,YAAY,aAAa,EAAE,MAAM;AAC1C,KAAG,MAAM,YAAY,gBAAgB,EAAE,SAAS;AAGhD,KAAG,MAAM,YAAY,kBAAkB,MAAM,MAAM,cAAc,EAAE,UAAU;AAC7E,KAAG,MAAM,YAAY,YAAY,MAAM,UAAU,EAAE,MAAM;AACzD,KAAG,MAAM,YAAY,kBAAkB,EAAE,WAAW;AACpD,KAAG,MAAM,YAAY,oBAAoB,MAAM,iBAAiB,EAAE,aAAa;AAC/E,KAAG,MAAM,YAAY,gBAAgB,MAAM,aAAa,EAAE,SAAS;AACnE,MAAI,KAAM,IAAG,MAAM,YAAY,sBAAsB,KAAK,eAAe;AAAA,MACpE,IAAG,MAAM,eAAe,oBAAoB;AACjD,KAAG,MAAM,YAAY,WAAW,EAAE,KAAK;AACvC,KAAG,MAAM,YAAY,iBAAiB,EAAE,UAAU;AAClD,KAAG,MAAM,YAAY,oBAAoB,EAAE,aAAa;AACxD,KAAG,MAAM,YAAY,kBAAkB,MAAM,MAAM,eAAe,EAAE,WAAW;AAC/E,KAAG,MAAM,YAAY,mBAAmB,MAAM,MAAM,gBAAgB,EAAE,YAAY;AAClF,KAAG,MAAM,YAAY,eAAe,MAAM,MAAM,YAAY,EAAE,QAAQ;AACtE,KAAG,MAAM,YAAY,mBAAmB,MAAM,MAAM,gBAAgB,EAAE,YAAY;AAKlF,MAAI,EAAE,MAAM,SAAS,GAAG;AACtB,aAAS,IAAI,GAAG,IAAI,GAAG,IAAK,IAAG,MAAM,YAAY,WAAW,IAAI,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,MAAM,MAAM,CAAC;AAAA,EAClG;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/theme/palette-presets.ts"],"sourcesContent":["// Appearance presets — the single source of truth for the \"palette switcher\"\n// (feedback #307). Each preset is a full set of design-token values for BOTH\n// light and dark, covering the page/surface backgrounds, the brand accent, the\n// semantic money trio and the categorical chart palette (anyplot's eight \"Imprint\"\n// hues by default since 0.49, Paul Tol's nine in `PAUL_TOL_PRESET`), plus the\n// heatmap stops. A preset is applied at runtime by writing these as CSS custom\n// properties onto <html> (see palette-store.ts), which instantly re-skins every\n// token-driven surface and chart. The two heatmaps and the treemap interpolate\n// concrete hex in JS, so they read the same values from here via the hooks in\n// palette-store.\n//\n// The default preset here mirrors the :root/.dark blocks in tokens.css exactly. The\n// five hand-picked alternatives of #307 (`ALTERNATIVE_PRESETS`, a reference bank that\n// `presetById` never resolved) were removed in 0.35 (refactor audit 2026-10, API-6c;\n// deprecated in 0.34): `PALETTES` is what an app offers, and `derivePalette` makes a\n// palette of its own.\n\nimport type { HeatStops } from \"./chart-palette\";\nimport { contrastStep, rememberTokenSet } from \"./contrast-tokens\";\n\nexport interface TokenSet {\n // Backgrounds & chrome\n bgPage: string;\n bgSurface: string;\n bgSurface2: string;\n border: string;\n textPrimary: string;\n // The two quieter text roles. Derived per preset by blending `textPrimary` toward\n // that preset's own surfaces until the contrast against the WORST of bgPage /\n // bgSurface / bgSurface2 hits 7:1 and 4.8:1 — so both clear WCAG AA for normal text\n // (4.5:1) on every surface they can land on, and both inherit the preset's warmth\n // instead of importing a cool grey from Tailwind's palette.\n textSecondary: string;\n textMuted: string;\n // Interactive brand accent\n brand: string;\n brandHover: string;\n brandContrast: string;\n // Semantic money (never red/green — CVD-safe, paired with +/- cues elsewhere)\n moneyIncome: string;\n moneyExpense: string;\n moneyNet: string;\n moneyNeutral: string;\n // Categorical chart series, written as `--chart-1…9`. anyplot's eight \"Imprint\" hues by\n // default (0.49), Paul Tol \"Muted\"'s nine in `PAUL_TOL_PRESET`. `paletteFor` cycles\n // eight; a shorter ramp is repeated to fill `--chart-9` (see `applyTokenSet`).\n chart: string[];\n // Heatmap interpolation stops (variance diverging + spending sequential)\n heat: HeatStops;\n}\n\nexport interface PalettePreset {\n id: string;\n name: string;\n blurb: string;\n light: TokenSet;\n dark: TokenSet;\n}\n\n// ── \"Imprint\" chart hues (feedback #119; the default since 0.49) ─────────────\n// anyplot's categorical palette (https://anyplot.ai/palette), by a colleague of\n// Marcel's. 8 hues + an amber semantic anchor, derived from Wong 2011 by a\n// Petroff-style max-min ΔE search in CAM02-UCS with Machado 2009 CVD simulation —\n// the same lineage as Paul Tol \"Muted\", one generation on, and tuned for exactly the\n// warm cream paper our light theme uses.\n//\n// 0.49 (Marcel, 2026-10-22): the DEFAULT, in anyplot's slot order, eight slots — amber\n// #DDCC77 left the pool, it is anyplot's semantic warning anchor, not a series colour.\n// The SAME hues in both themes: 0.48 lifted matte red and rose for the dark ground\n// (#BF413E, #A25083), which made the dark chart a different palette from the light one.\n// anyplot answers a hue under 3:1 with a 1px ink edge instead, and so does the kit now\n// (`chartEdge`, drawn by SeriesChart's bars and areas and by Treemap's tiles).\n//\n// Measured on our surfaces (CIEDE2000, Machado severity-100, 8 like-for-like slots on\n// the cream light surface #f6ecd9) — neither palette dominates:\n// Paul Tol Muted min-contrast 1.38 · worst-CVD ΔE00 11.8 · normal ΔE00 16.1\n// imprint min-contrast 1.95 · worst-CVD ΔE00 7.2 · normal ΔE00 22.2\n// i.e. imprint is punchier and has no near-invisible pale slot (Tol's #88ccee sits at\n// 1.5:1 on cream), while Tol separates better under colour-vision deficiency — which is\n// why `paletteFor` offers anyplot's CVD-optimal order (`{ order: \"cvd\" }`).\nconst IMPRINT_HUES = [\n \"#009e73\", // brand green — first series, always\n \"#c475fd\", // lavender — creative\n \"#4467a3\", // blue — cool / info\n \"#bd8233\", // ochre — earth / commodity\n \"#ae3030\", // matte red — bad / loss / error\n \"#2abccd\", // cyan — sky / tech-cool\n \"#954477\", // rose — wellness / health\n \"#99b314\", // lime — growth / nature\n];\n\n/** anyplot's scales (0.49): sequential green → blue, diverging blue (under) ↔ paper or\n * ink (0) ↔ matte red (over). `chart-palette.ts`'s `HEATMAP_HEX` holds the same. */\nconst ANYPLOT_HEAT: Record<\"light\" | \"dark\", HeatStops> = {\n light: {\n neutral: \"#faf8f1\",\n under: \"#4467a3\",\n over: \"#ae3030\",\n seqLow: \"#009e73\",\n seqHigh: \"#4467a3\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n dark: {\n neutral: \"#1a1a17\",\n under: \"#4467a3\",\n over: \"#ae3030\",\n seqLow: \"#009e73\",\n seqHigh: \"#4467a3\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n};\n\n// The shipped default (feedback #328): SOLAR-DUSK warm light + INDIGO-NOIR deep\n// dark surfaces — the two curated presets the user picked per mode — but with a\n// SINGLE unified brand accent (indigo) across both modes rather than each\n// preset's own accent (the user asked for the brand accent to stay the same).\n// So the light block is Solar Dusk with its teal brand overridden to indigo, and\n// the dark block is Indigo Noir (already indigo). The money trio\n// (income/expense/net) drives the chart bars and stays palette-driven so those\n// semantic colours adapt to each mode's contrast (best practice — feedback #328).\n//\n// 0.49: its chart ramp and heat stops are anyplot's (\"Imprint\", above) — the money\n// trio stays the kit's own (Marcel, 2026-10-22: chart income/expense/net/neutral keep\n// the money tokens, never anyplot's green/red). The 0.48 default, the same surfaces\n// with Paul Tol's ramp, is `PAUL_TOL_PRESET`.\nexport const DEFAULT_PRESET: PalettePreset = {\n id: \"default\",\n name: \"Imprint\",\n blurb: \"anyplot.ai chart hues · solar-dusk light · indigo-noir dark\",\n light: {\n // Solar Dusk (light) surfaces, with the brand unified to indigo (see above).\n bgPage: \"#ede3cf\",\n bgSurface: \"#f6ecd9\",\n // The well is the page in light (0.33, k26): #f3ead6 stood 1.02:1 from the card it\n // is inset in. Every light preset does the same; see SURFACE_L in palette-derive.ts.\n bgSurface2: \"#ede3cf\",\n border: \"#e2d6bd\",\n textPrimary: \"#463f33\",\n textSecondary: \"#50493c\",\n textMuted: \"#696153\",\n brand: \"#4f46e5\",\n brandHover: \"#4338ca\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#0b6650\",\n moneyExpense: \"#905902\",\n moneyNet: \"#7a4fa3\",\n moneyNeutral: \"#6b6453\",\n chart: IMPRINT_HUES,\n heat: ANYPLOT_HEAT.light,\n },\n dark: {\n // Indigo Noir (dark)\n bgPage: \"#0e0f1c\",\n bgSurface: \"#181a2c\",\n bgSurface2: \"#21243a\",\n border: \"#2e3252\",\n textPrimary: \"#e6e8f4\",\n textSecondary: \"#adafbe\",\n textMuted: \"#8d90a0\",\n brand: \"#8b93f8\",\n brandHover: \"#aab1fb\",\n brandContrast: \"#16182b\",\n moneyIncome: \"#22c3b6\",\n // Amber-400 (#fbbf24) sat at OKLab L 0.84 while every other dark token lives\n // at 0.68–0.74, so outflows outshouted the inflows they are paired with. Same\n // hue and chroma, pulled to L 0.785 (dev#434). The heat stops below keep the\n // brighter amber on purpose: a gradient endpoint is meant to be the loudest\n // thing in its scale, and it is a fill, not a figure.\n moneyExpense: \"#e4b035\",\n moneyNet: \"#b39aef\",\n moneyNeutral: \"#9aa1bd\",\n chart: IMPRINT_HUES,\n heat: ANYPLOT_HEAT.dark,\n },\n};\n\n/**\n * The 0.48 default: the same surfaces, brand and money, with Paul Tol's \"Muted\" nine\n * as the chart ramp and the teal/amber heat stops that went with it. Selectable, no\n * longer the default (0.49). `paletteFor` cycles eight, so its ninth hue (#aa4499) is\n * reachable only as `var(--chart-9)`.\n */\nexport const PAUL_TOL_PRESET: PalettePreset = {\n id: \"paul-tol\",\n name: \"Paul Tol muted\",\n blurb: \"Paul Tol's CVD-safe chart hues · solar-dusk light · indigo-noir dark\",\n light: {\n ...DEFAULT_PRESET.light,\n chart: [\"#332288\", \"#88ccee\", \"#44aa99\", \"#117733\", \"#999933\", \"#ddcc77\", \"#cc6677\", \"#882255\", \"#aa4499\"],\n heat: {\n neutral: \"#f1e7d2\",\n under: \"#0b6650\",\n over: \"#9c6418\",\n seqLow: \"#f6ecd0\",\n seqHigh: \"#9c6418\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n },\n dark: {\n ...DEFAULT_PRESET.dark,\n chart: [\"#7e72d6\", \"#9fd8f2\", \"#5fc4b0\", \"#3fa45f\", \"#bfbf5e\", \"#e8dda0\", \"#e08c9a\", \"#c25a78\", \"#cc78be\"],\n heat: {\n neutral: \"#363a5a\",\n under: \"#22c3b6\",\n over: \"#fbbf24\",\n seqLow: \"#3a2c14\",\n seqHigh: \"#fbbf24\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n },\n};\n\n// The full-theme switcher from #307 stayed retired (feedback #328) — the default\n// is still the single source of truth for surfaces, brand and money (its five\n// alternatives are gone, see the header). What IS selectable is\n// the categorical chart ramp (feedback #119), so Imprint (the default) and Paul Tol are\n// the live set; presetById falls back to the default for any stale persisted id.\n/**\n * The categorical chart ramp, shared by every derived preset below.\n *\n * The default's ramp (anyplot's eight since 0.49, Paul Tol's \"Muted\" before), and\n * deliberately NOT re-derived per preset. A\n * categorical ramp solves a different problem from a UI palette — separability among\n * nine unknown siblings, rather than contrast against one known surface — and a set\n * optimised against real dichromat confusion lines beats one spaced evenly by formula.\n * `auditChartRamp` in palette-derive.ts measures it: 0.059 minimum separation in the\n * light variant, which is the number MIN_SEPARATION was calibrated under.\n *\n * It pays for that in contrast: four of the nine light hues sit below the 3:1 WCAG\n * 1.4.11 asks of a filled shape. That is not fixable by darkening them — see\n * SINGLE_LIGHTNESS_MAX_SERIES — so a chart with more than four series must carry a\n * second channel (a direct label, a pattern, a legend adjacent to the mark).\n */\nconst KIT_RAMP = {\n light: DEFAULT_PRESET.light.chart,\n dark: DEFAULT_PRESET.dark.chart,\n} as const;\n\n/**\n * Presets DERIVED rather than picked, by `derivePalette` from one brand colour each.\n *\n * Every text role here was solved to a contrast target against the worst of the three\n * surfaces it can land on, every semantic colour against both the page and its own badge\n * tint, and the whole set is re-checked by `__tests__/palette-audit.test.ts` (\"derived\n * presets\") — which re-runs the deriver and fails if these committed values drift from\n * what it now produces.\n *\n * They are committed rather than derived at import because deriving eight token sets\n * costs ~34ms, and charging every consumer that at startup for palettes most of them\n * will never select is the wrong trade. The test is what keeps the two in agreement.\n *\n * Regenerated in 0.34 (round 0.34 BUG-8): the solve now measures the hex it ships rather\n * than the float before rounding, and the audit no longer lets a hex pair pass 0.05 under\n * its threshold. 52 values moved one step (66 counting the heat stops that repeat them);\n * Contrast's neutrals lost half their slate tint, its brand having half the chroma at\n * which the neutrals carry a hue in full.\n */\nexport const DERIVED_PRESETS: PalettePreset[] = [\n {\n id: \"ink\",\n name: \"Ink\",\n blurb: \"Cool blue-grey ground · solved for AAA body text\",\n light: {\n bgPage: \"#dce2ef\",\n bgSurface: \"#e6edfa\",\n bgSurface2: \"#dce2ef\",\n border: \"#b0bace\",\n textPrimary: \"#2e3139\",\n textSecondary: \"#454850\",\n textMuted: \"#5f636c\",\n brand: \"#2f5fd0\",\n brandHover: \"#1f4cbc\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00725f\",\n moneyExpense: \"#8e5900\",\n moneyNet: \"#7356a2\",\n moneyNeutral: \"#61646c\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#dce2ef\",\n under: \"#00725f\",\n over: \"#8e5900\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8e5900\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#090d15\",\n bgSurface: \"#131720\",\n bgSurface2: \"#1d212b\",\n border: \"#363e4f\",\n textPrimary: \"#c8ccd6\",\n textSecondary: \"#a7abb5\",\n textMuted: \"#858a92\",\n brand: \"#3466d7\",\n brandHover: \"#2353c3\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a1a5ac\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#1d212b\",\n under: \"#009a80\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"moss\",\n name: \"Moss\",\n blurb: \"Green ground · solved for AAA body text\",\n light: {\n bgPage: \"#d8e6dd\",\n bgSurface: \"#e3f1e8\",\n bgSurface2: \"#d8e6dd\",\n border: \"#abbfb2\",\n textPrimary: \"#2c332e\",\n textSecondary: \"#424b45\",\n textMuted: \"#5d6660\",\n brand: \"#2f7a52\",\n brandHover: \"#196842\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00725f\",\n moneyExpense: \"#8e5900\",\n moneyNet: \"#7356a2\",\n moneyNeutral: \"#5f6662\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#d8e6dd\",\n under: \"#00725f\",\n over: \"#8e5900\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8e5900\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#060f0a\",\n bgSurface: \"#101a14\",\n bgSurface2: \"#1a241e\",\n border: \"#314337\",\n textPrimary: \"#c6cfc9\",\n textSecondary: \"#a5aea8\",\n textMuted: \"#838c86\",\n brand: \"#317b53\",\n brandHover: \"#1c6943\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b87c2a\",\n moneyNet: \"#977ac9\",\n moneyNeutral: \"#9fa7a2\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#1a241e\",\n under: \"#009a80\",\n over: \"#b87c2a\",\n seqLow: \"#331f05\",\n seqHigh: \"#b87c2a\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"plum\",\n name: \"Plum\",\n blurb: \"Violet ground · solved for AAA body text\",\n light: {\n bgPage: \"#e7dfea\",\n bgSurface: \"#f2e9f5\",\n bgSurface2: \"#e7dfea\",\n border: \"#c2b5c6\",\n textPrimary: \"#352f36\",\n textSecondary: \"#4c464e\",\n textMuted: \"#676169\",\n brand: \"#7b3f8f\",\n brandHover: \"#692d7d\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00715e\",\n moneyExpense: \"#8d5800\",\n moneyNet: \"#7355a2\",\n moneyNeutral: \"#676269\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#e7dfea\",\n under: \"#00715e\",\n over: \"#8d5800\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8d5800\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#100b12\",\n bgSurface: \"#1b151d\",\n bgSurface2: \"#251f27\",\n border: \"#453b48\",\n textPrimary: \"#d0cad2\",\n textSecondary: \"#afa9b1\",\n textMuted: \"#8d8790\",\n brand: \"#9154a6\",\n brandHover: \"#7f4293\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009980\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a8a2aa\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#251f27\",\n under: \"#009980\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"contrast\",\n name: \"High contrast\",\n blurb: \"Near-neutral ground · text solved to 13:1 / 8.5:1 / 5.5:1\",\n light: {\n bgPage: \"#e1e2e3\",\n bgSurface: \"#ecedee\",\n bgSurface2: \"#e1e2e3\",\n border: \"#b8babc\",\n textPrimary: \"#1b1d21\",\n textSecondary: \"#3a3c40\",\n textMuted: \"#55585c\",\n brand: \"#1f2937\",\n brandHover: \"#111b28\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00715e\",\n moneyExpense: \"#8d5800\",\n moneyNet: \"#7255a1\",\n moneyNeutral: \"#616467\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#e1e2e3\",\n under: \"#00715e\",\n over: \"#8d5800\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8d5800\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#0c0d0e\",\n bgSurface: \"#171718\",\n bgSurface2: \"#212122\",\n border: \"#3d3e3f\",\n textPrimary: \"#e4e8ec\",\n textSecondary: \"#b9bdc1\",\n textMuted: \"#95979c\",\n brand: \"#556c8e\",\n brandHover: \"#455b7c\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a2a5a8\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#212122\",\n under: \"#009a80\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n];\n\nexport const PALETTES: PalettePreset[] = [DEFAULT_PRESET, PAUL_TOL_PRESET, ...DERIVED_PRESETS];\n\nexport function presetById(id: string): PalettePreset {\n return PALETTES.find((p) => p.id === id) ?? DEFAULT_PRESET;\n}\n\n/**\n * Write a token set onto an element's inline style as CSS custom properties.\n *\n * This is the only writer, and there is no un-writer: `presetById` falls back to\n * `DEFAULT_PRESET` and this then writes the full set, so returning to the default\n * APPLIES it rather than clearing anything. A `clearTokenSet` and the hand-kept\n * 24-name `TOKEN_VARS` list it looped over used to live here, both exported, both\n * called by nobody in either consumer — a second copy of these property names with\n * nothing comparing the two, so adding a token here and forgetting it there would\n * have been silent and only a dead function would have noticed. The list is gone;\n * the test beside this asserts instead that every field of a `TokenSet` reaches the\n * element, which is what that list was standing in for.\n *\n * **More contrast (0.32, docs/text-size-harmonization.md §5, §10.5).** With `contrast`\n * the text and border tokens are written one step stronger — in a light set the well and\n * the four money colours too (0.36, docs/colour-roles-harmonization.md §13) — and `--text-placeholder` —\n * otherwise derived in tokens.css — is written inline too; without it that inline\n * placeholder is removed again, so the stylesheet's formula applies. The step is made\n * HERE because these tokens are inline: no `[data-contrast=more]` rule could beat them.\n *\n * `contrast` defaults to the element's own `data-contrast=\"more\"`, which the contrast\n * layer (`applyContrast`, `useApplyContrast`) sets first. That is how the palette and the\n * contrast cooperate without knowing each other: a palette change re-applies through\n * here and keeps the contrast, and a contrast change re-applies the set remembered here.\n */\nexport function applyTokenSet(\n el: HTMLElement,\n t: TokenSet,\n contrast: boolean = el.getAttribute(\"data-contrast\") === \"more\",\n): void {\n rememberTokenSet(el, t);\n const step = contrast ? contrastStep(t) : null;\n el.style.setProperty(\"--bg-page\", t.bgPage);\n el.style.setProperty(\"--bg-surface\", t.bgSurface);\n // The light well and money step under More contrast (0.36, colour-roles §13); in dark\n // and at Standard `well` is null and the set's own are written.\n el.style.setProperty(\"--bg-surface-2\", step?.well?.bgSurface2 ?? t.bgSurface2);\n el.style.setProperty(\"--border\", step?.border ?? t.border);\n el.style.setProperty(\"--text-primary\", t.textPrimary);\n el.style.setProperty(\"--text-secondary\", step?.textSecondary ?? t.textSecondary);\n el.style.setProperty(\"--text-muted\", step?.textMuted ?? t.textMuted);\n if (step) el.style.setProperty(\"--text-placeholder\", step.textPlaceholder);\n else el.style.removeProperty(\"--text-placeholder\");\n el.style.setProperty(\"--brand\", t.brand);\n el.style.setProperty(\"--brand-hover\", t.brandHover);\n el.style.setProperty(\"--brand-contrast\", t.brandContrast);\n el.style.setProperty(\"--money-income\", step?.well?.moneyIncome ?? t.moneyIncome);\n el.style.setProperty(\"--money-expense\", step?.well?.moneyExpense ?? t.moneyExpense);\n el.style.setProperty(\"--money-net\", step?.well?.moneyNet ?? t.moneyNet);\n el.style.setProperty(\"--money-neutral\", step?.well?.moneyNeutral ?? t.moneyNeutral);\n // All nine `--chart-N`, a shorter ramp repeated: Imprint's eight write `--chart-9` as\n // slot 0 — what `paletteFor(8)` names — so a host addressing `var(--chart-9)` directly\n // follows the cycle, and switching away from Paul Tol's nine leaves no stale ninth hue.\n // An empty ramp (a `derivePalette` set) leaves whatever was there, deliberately.\n if (t.chart.length > 0) {\n for (let i = 0; i < 9; i++) el.style.setProperty(`--chart-${i + 1}`, t.chart[i % t.chart.length]);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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 let id = DEFAULT_PRESET.id;\n try {\n const blob = JSON.parse(localStorage.getItem(storageKey) ?? \"{}\") as {\n state?: { id?: string };\n };\n if (blob.state?.id) id = blob.state.id;\n } catch {\n /* malformed storage → default preset */\n }\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(
|
|
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 let id = DEFAULT_PRESET.id;\n try {\n const blob = JSON.parse(localStorage.getItem(storageKey) ?? \"{}\") as {\n state?: { id?: string };\n };\n if (blob.state?.id) id = blob.state.id;\n } catch {\n /* malformed storage → default preset */\n }\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,6BAA6B;AAEtC,SAAS,eAAe,gBAAgB,kBAAiC;AAUlE,SAAS,sBAAsB,YAAoB,MAAuB;AAC/E,MAAI,KAAK,eAAe;AACxB,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,aAAa,QAAQ,UAAU,KAAK,IAAI;AAGhE,QAAI,KAAK,OAAO,GAAI,MAAK,KAAK,MAAM;AAAA,EACtC,QAAQ;AAAA,EAER;AACA,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":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/theme/text-size.ts"],"sourcesContent":["import { useEffect, useMemo, useSyncExternalStore } 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\";\n\n/**\n * The text size (docs/text-size-harmonization.md §2.1, §3.1, §10.3, §10.6).\n *\n * ONE scale on `<html>`: `data-text-size=\"normal|large|xlarge\"`, and `tokens.css` turns\n * it into `font-size: 100% / 125% / 150%` — percent, never px, so \"Normal\" keeps\n * whatever size the person set in the browser. Tailwind's spacing and type are rem, so\n * about 90 % of every screen grows with that one declaration; the breakpoints follow too\n * (tokens.css redefines `sm:` … `3xl:` per size, {@link useBreakpoint} answers the same\n * in JS).\n *\n * The names below are the mechanism's public vocabulary and are FROZEN: an app's\n * storage, its `/auth/me` column (server-kit's `TEXT_SIZES`) and the inline boot snippet\n * all spell them this way.\n */\n\n/** The three steps, in order (§2.1, §10.6): server-kit's `TEXT_SIZES`, value for value. */\nexport const TEXT_SIZES = [\"normal\", \"large\", \"xlarge\"] as const;\n\nexport type TextSize = (typeof TEXT_SIZES)[number];\n\n/** The root font size each step sets, as a factor of the browser's own (tokens.css). For\n * what CSS cannot reach: a canvas or SVG chart's px constants, a windowed list's row\n * estimate, a popover width in px (§3.2). */\nexport const TEXT_SCALE: Readonly<Record<TextSize, number>> = { normal: 1, large: 1.25, xlarge: 1.5 };\n\n/** The attribute on `<html>` that carries the size. */\nconst TEXT_SIZE_ATTRIBUTE = \"data-text-size\";\n\nexport function isTextSize(value: unknown): value is TextSize {\n return value === \"normal\" || value === \"large\" || value === \"xlarge\";\n}\n\n/**\n * The size in force: **this device's own choice → the account's → Normal** (§2.2, §2.6).\n * Each is taken only when it is one of {@link TEXT_SIZES}; anything else (a stale value,\n * `null` = never chosen) falls through to the next. The one order the pre-paint call, the\n * apply hook and `useAccountAppearance` all use, so the three can never disagree.\n */\nexport function resolveTextSize(device: unknown, account?: unknown): TextSize {\n if (isTextSize(device)) return device;\n if (isTextSize(account)) return account;\n return \"normal\";\n}\n\n/* ── <html data-text-size> and the subscribers that read it ─────────────── */\n\nconst listeners = new Set<() => void>();\nlet observer: MutationObserver | null = null;\n\nfunction notify(): void {\n for (const listener of listeners) listener();\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener);\n // Also catches writers that are not this module — the inline boot snippet, an app\n // setting the attribute by hand, a test.\n if (!observer && typeof MutationObserver !== \"undefined\" && typeof document !== \"undefined\") {\n observer = new MutationObserver(notify);\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [TEXT_SIZE_ATTRIBUTE] });\n }\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n observer?.disconnect();\n observer = null;\n }\n };\n}\n\n/** The size `<html>` carries now; Normal without one (or outside a browser). */\nexport function readTextSize(): TextSize {\n if (typeof document === \"undefined\") return \"normal\";\n const value = document.documentElement.getAttribute(TEXT_SIZE_ATTRIBUTE);\n return isTextSize(value) ? value : \"normal\";\n}\n\n/** Put `size` on `<html>`. Every {@link useTextSize} re-renders with it. */\nexport function applyTextSize(size: TextSize): void {\n if (typeof document === \"undefined\") return;\n const root = document.documentElement;\n if (root.getAttribute(TEXT_SIZE_ATTRIBUTE) === size) return;\n root.setAttribute(TEXT_SIZE_ATTRIBUTE, size);\n notify();\n}\n\nexport interface TextSizeInfo {\n size: TextSize;\n /** {@link TEXT_SCALE} for `size`: 1, 1.25 or 1.5. */\n scale: number;\n}\n\n/**\n * The size in force on this page, and its scale (§3.1) — read from `<html>`, so it needs\n * no store and works in every kit component: whoever applied the size (the app's store,\n * the pre-paint call, the inline snippet), this follows it.\n *\n * Only for what CSS cannot do. A class that should differ at Large is a `large:`\n * variant; a layout switch is a breakpoint, which already scales.\n */\nexport function useTextSize(): TextSizeInfo {\n const size = useSyncExternalStore(subscribe, readTextSize, () => \"normal\" as const);\n return useMemo(() => ({ size, scale: TEXT_SCALE[size] }), [size]);\n}\n\n/* ── Storage: the frozen format, the pre-paint call ─────────────────────── */\n\n/**\n * What a text-size store persists under its key — zustand's persist envelope, and FROZEN\n * (§10.3) because an app's inline `<head>` snippet reads it before any module loads:\n *\n * {\"state\":{\"size\":\"large\"},\"version\":1}\n *\n * `size` is one of {@link TEXT_SIZES}, or `null` / absent when this device never chose.\n * A future change adds a key; it never renames `state.size` or changes its values.\n *\n * A hand-written seed (a test, a screenshot script) must carry `\"version\":1`: the store\n * drops a stored state of another version as a migration it doesn't know, silently\n * (keksdose's 0.32 report). The inline snippet reads `state.size` whatever the version,\n * so the two would disagree. In the kit's showcase, `?text-size=` needs no seed.\n */\nexport interface PersistedTextSize {\n state?: { size?: TextSize | null };\n version?: number;\n}\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 size). */\nexport function readPersistedTextSize(storageKey: string): TextSize | null {\n try {\n const blob = JSON.parse(readStored(storageKey) ?? \"null\") as PersistedTextSize | null;\n const size = blob?.state?.size;\n return isTextSize(size) ? size : null;\n } catch {\n return null;\n }\n}\n\nexport interface PersistedAppearanceOptions {\n /**\n * The account's last known value, for a device without a choice of its own (§10.3):\n * what the app persisted with its user (keksdose keeps `/auth/me` in localStorage), so\n * a cold boot paints the account's size instead of Normal and then jumping — offline\n * too. Called once, guarded: a throw counts as \"no account value\".\n */\n account?: () => string | null | undefined;\n}\n\nfunction readAccount(account: PersistedAppearanceOptions[\"account\"]): unknown {\n if (!account) return undefined;\n try {\n return account();\n } catch {\n return undefined;\n }\n}\n\n/**\n * Pre-paint (§3.1, §10.3): read this device's choice under `storageKey` — else the\n * account's last known value — and put it on `<html>` BEFORE `createRoot`, next to\n * `applyPersistedTheme`, so the first paint already has the right size. Returns the size\n * applied.\n *\n * ```ts\n * applyPersistedTextSize(\"keksdose-text-size\", {\n * account: () => readPersistedUser()?.text_size,\n * });\n * ```\n */\nexport function applyPersistedTextSize(storageKey: string, options: PersistedAppearanceOptions = {}): TextSize {\n const size = resolveTextSize(readPersistedTextSize(storageKey), readAccount(options.account));\n applyTextSize(size);\n return size;\n}\n\n/**\n * The same pre-paint as {@link applyPersistedTextSize}, as an inline `<head>` script for\n * a boot splash painted from `index.html` before any module runs (§10.3). A function\n * EXPRESSION: call it with the storage key and, optionally, a function returning the\n * account's last known value.\n *\n * ```html\n * <script>\n * (function(k,a){…})(\"keksdose-text-size\", function () {\n * return JSON.parse(localStorage.getItem(\"keksdose-auth\")).state.user.text_size;\n * });\n * </script>\n * ```\n *\n * Paste the constant's value in place of `(function(k,a){…})`. It reads the frozen\n * {@link PersistedTextSize} format, never throws, and sets only `data-text-size`.\n *\n * The splash sizes in rem, and needs the root rules inline beside it, since the app's\n * stylesheet is a separate request and arrives after the splash paints (0.32.1,\n * keksdose's 0.32 report: that is every app, not an edge case):\n *\n * ```html\n * <style>html[data-text-size=large]{font-size:125%}html[data-text-size=xlarge]{font-size:150%}</style>\n * ```\n *\n * Under a hash-based Content Security Policy (`script-src 'sha256-…'`) this is a second\n * inline script with its own hash: add it to the policy, and recompute it whenever the\n * storage key or the account function in the call changes (keksdose added it to both\n * its Caddyfiles, and its guard test hashes every inline script).\n */\nexport const TEXT_SIZE_INLINE_SCRIPT =\n \"(function(k,a){function ok(v){return v==='normal'||v==='large'||v==='xlarge'}\" +\n \"var v=null;try{var s=JSON.parse(localStorage.getItem(k)||'null');v=s&&s.state&&s.state.size}catch(e){}\" +\n \"if(!ok(v)){v=null;try{v=a?a():null}catch(e){}}\" +\n \"try{document.documentElement.setAttribute('data-text-size',ok(v)?v:'normal')}catch(e){}})\";\n\n/* ── The store ──────────────────────────────────────────────────────────── */\n\nexport interface TextSizeState {\n /** This device's own choice (persisted), or `null` when it never chose — then the\n * account's value applies (§2.2). */\n size: TextSize | null;\n /** Store this device's choice; `null` forgets it, so the account's applies again. */\n setSize: (size: TextSize | null) => void;\n}\n\nexport interface ApplyTextSizeOptions {\n /** The account's value (`/auth/me`'s `text_size`): it applies while this device has no\n * choice of its own. Leave it out where there is no account. */\n account?: string | null;\n}\n\n/**\n * Build a text-size store bound to a consumer-supplied localStorage key, shaped like\n * `createThemeStore` (§6). It holds the DEVICE's choice only; the account's value comes\n * in through `useApplyTextSize({ account })` and `useAccountAppearance`, and the size in\n * force is always {@link resolveTextSize} of the two.\n *\n * - `useTextSizeStore` — the zustand hook: `size` (device choice or null) and `setSize`.\n * - `useApplyTextSize({ account })` — the effect that puts the size in force on `<html>`.\n * Mount it once near the root, beside `useApplyTheme`.\n *\n * The persisted format is {@link PersistedTextSize}, and frozen.\n */\nexport function createTextSizeStore(storageKey: string) {\n const useTextSizeStore = create<TextSizeState>()(\n logger(\n persist(\n (set) => ({\n size: null,\n setSize: (size) => set({ size: isTextSize(size) ? size : null }),\n }),\n {\n name: storageKey,\n version: 1,\n partialize: (s) => ({ size: s.size }),\n // A hand-edited or future value must not reach <html> as an unknown size.\n merge: (persisted, current) => ({\n ...current,\n size: isTextSize((persisted as { size?: unknown } | null)?.size)\n ? ((persisted as { size: TextSize }).size)\n : null,\n }),\n },\n ),\n \"text-size\",\n ),\n );\n // Reset this device's settings (docs/settings-harmonization.md §11.4): no choice on\n // this device, so `useApplyTextSize` re-resolves to the account's size, else Normal.\n registerDeviceSetting({\n key: storageKey,\n kind: \"appearance\",\n reset: () => useTextSizeStore.getState().setSize(null),\n });\n\n function useApplyTextSize({ account }: ApplyTextSizeOptions = {}): TextSize {\n const device = useTextSizeStore((s) => s.size);\n const size = resolveTextSize(device, account);\n useEffect(() => {\n applyTextSize(size);\n }, [size]);\n return size;\n }\n\n return { useTextSizeStore, useApplyTextSize };\n}\n\nexport type TextSizeStore = ReturnType<typeof createTextSizeStore>;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/theme/text-size.ts"],"sourcesContent":["import { useEffect, useMemo, useSyncExternalStore } 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\";\n\n/**\n * The text size (docs/text-size-harmonization.md §2.1, §3.1, §10.3, §10.6).\n *\n * ONE scale on `<html>`: `data-text-size=\"normal|large|xlarge\"`, and `tokens.css` turns\n * it into `font-size: 100% / 125% / 150%` — percent, never px, so \"Normal\" keeps\n * whatever size the person set in the browser. Tailwind's spacing and type are rem, so\n * about 90 % of every screen grows with that one declaration; the breakpoints follow too\n * (tokens.css redefines `sm:` … `3xl:` per size, {@link useBreakpoint} answers the same\n * in JS).\n *\n * The names below are the mechanism's public vocabulary and are FROZEN: an app's\n * storage, its `/auth/me` column (server-kit's `TEXT_SIZES`) and the inline boot snippet\n * all spell them this way.\n */\n\n/** The three steps, in order (§2.1, §10.6): server-kit's `TEXT_SIZES`, value for value. */\nexport const TEXT_SIZES = [\"normal\", \"large\", \"xlarge\"] as const;\n\nexport type TextSize = (typeof TEXT_SIZES)[number];\n\n/** The root font size each step sets, as a factor of the browser's own (tokens.css). For\n * what CSS cannot reach: a canvas or SVG chart's px constants, a windowed list's row\n * estimate, a popover width in px (§3.2). */\nexport const TEXT_SCALE: Readonly<Record<TextSize, number>> = { normal: 1, large: 1.25, xlarge: 1.5 };\n\n/** The attribute on `<html>` that carries the size. */\nconst TEXT_SIZE_ATTRIBUTE = \"data-text-size\";\n\nexport function isTextSize(value: unknown): value is TextSize {\n return value === \"normal\" || value === \"large\" || value === \"xlarge\";\n}\n\n/**\n * The size in force: **this device's own choice → the account's → Normal** (§2.2, §2.6).\n * Each is taken only when it is one of {@link TEXT_SIZES}; anything else (a stale value,\n * `null` = never chosen) falls through to the next. The one order the pre-paint call, the\n * apply hook and `useAccountAppearance` all use, so the three can never disagree.\n */\nexport function resolveTextSize(device: unknown, account?: unknown): TextSize {\n if (isTextSize(device)) return device;\n if (isTextSize(account)) return account;\n return \"normal\";\n}\n\n/* ── <html data-text-size> and the subscribers that read it ─────────────── */\n\nconst listeners = new Set<() => void>();\nlet observer: MutationObserver | null = null;\n\nfunction notify(): void {\n for (const listener of listeners) listener();\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener);\n // Also catches writers that are not this module — the inline boot snippet, an app\n // setting the attribute by hand, a test.\n if (!observer && typeof MutationObserver !== \"undefined\" && typeof document !== \"undefined\") {\n observer = new MutationObserver(notify);\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [TEXT_SIZE_ATTRIBUTE] });\n }\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n observer?.disconnect();\n observer = null;\n }\n };\n}\n\n/** The size `<html>` carries now; Normal without one (or outside a browser). */\nexport function readTextSize(): TextSize {\n if (typeof document === \"undefined\") return \"normal\";\n const value = document.documentElement.getAttribute(TEXT_SIZE_ATTRIBUTE);\n return isTextSize(value) ? value : \"normal\";\n}\n\n/** Put `size` on `<html>`. Every {@link useTextSize} re-renders with it. */\nexport function applyTextSize(size: TextSize): void {\n if (typeof document === \"undefined\") return;\n const root = document.documentElement;\n if (root.getAttribute(TEXT_SIZE_ATTRIBUTE) === size) return;\n root.setAttribute(TEXT_SIZE_ATTRIBUTE, size);\n notify();\n}\n\nexport interface TextSizeInfo {\n size: TextSize;\n /** {@link TEXT_SCALE} for `size`: 1, 1.25 or 1.5. */\n scale: number;\n}\n\n/**\n * The size in force on this page, and its scale (§3.1) — read from `<html>`, so it needs\n * no store and works in every kit component: whoever applied the size (the app's store,\n * the pre-paint call, the inline snippet), this follows it.\n *\n * Only for what CSS cannot do. A class that should differ at Large is a `large:`\n * variant; a layout switch is a breakpoint, which already scales.\n */\nexport function useTextSize(): TextSizeInfo {\n const size = useSyncExternalStore(subscribe, readTextSize, () => \"normal\" as const);\n return useMemo(() => ({ size, scale: TEXT_SCALE[size] }), [size]);\n}\n\n/* ── Storage: the frozen format, the pre-paint call ─────────────────────── */\n\n/**\n * What a text-size store persists under its key — zustand's persist envelope, and FROZEN\n * (§10.3) because an app's inline `<head>` snippet reads it before any module loads:\n *\n * {\"state\":{\"size\":\"large\"},\"version\":1}\n *\n * `size` is one of {@link TEXT_SIZES}, or `null` / absent when this device never chose.\n * A future change adds a key; it never renames `state.size` or changes its values.\n *\n * A hand-written seed (a test, a screenshot script) must carry `\"version\":1`: the store\n * drops a stored state of another version as a migration it doesn't know, silently\n * (keksdose's 0.32 report). The inline snippet reads `state.size` whatever the version,\n * so the two would disagree. In the kit's showcase, `?text-size=` needs no seed.\n */\nexport interface PersistedTextSize {\n state?: { size?: TextSize | null };\n version?: number;\n}\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 size). */\nexport function readPersistedTextSize(storageKey: string): TextSize | null {\n try {\n const blob = JSON.parse(readStored(storageKey) ?? \"null\") as PersistedTextSize | null;\n const size = blob?.state?.size;\n return isTextSize(size) ? size : null;\n } catch {\n return null;\n }\n}\n\nexport interface PersistedAppearanceOptions {\n /**\n * The account's last known value, for a device without a choice of its own (§10.3):\n * what the app persisted with its user (keksdose keeps `/auth/me` in localStorage), so\n * a cold boot paints the account's size instead of Normal and then jumping — offline\n * too. Called once, guarded: a throw counts as \"no account value\".\n */\n account?: () => string | null | undefined;\n}\n\nfunction readAccount(account: PersistedAppearanceOptions[\"account\"]): unknown {\n if (!account) return undefined;\n try {\n return account();\n } catch {\n return undefined;\n }\n}\n\n/**\n * Pre-paint (§3.1, §10.3): read this device's choice under `storageKey` — else the\n * account's last known value — and put it on `<html>` BEFORE `createRoot`, next to\n * `applyPersistedTheme`, so the first paint already has the right size. Returns the size\n * applied.\n *\n * ```ts\n * applyPersistedTextSize(\"keksdose-text-size\", {\n * account: () => readPersistedUser()?.text_size,\n * });\n * ```\n */\nexport function applyPersistedTextSize(storageKey: string, options: PersistedAppearanceOptions = {}): TextSize {\n const size = resolveTextSize(readPersistedTextSize(storageKey), readAccount(options.account));\n applyTextSize(size);\n return size;\n}\n\n/**\n * The same pre-paint as {@link applyPersistedTextSize}, as an inline `<head>` script for\n * a boot splash painted from `index.html` before any module runs (§10.3). A function\n * EXPRESSION: call it with the storage key and, optionally, a function returning the\n * account's last known value.\n *\n * ```html\n * <script>\n * (function(k,a){…})(\"keksdose-text-size\", function () {\n * return JSON.parse(localStorage.getItem(\"keksdose-auth\")).state.user.text_size;\n * });\n * </script>\n * ```\n *\n * Paste the constant's value in place of `(function(k,a){…})`. It reads the frozen\n * {@link PersistedTextSize} format, never throws, and sets only `data-text-size`.\n *\n * The splash sizes in rem, and needs the root rules inline beside it, since the app's\n * stylesheet is a separate request and arrives after the splash paints (0.32.1,\n * keksdose's 0.32 report: that is every app, not an edge case):\n *\n * ```html\n * <style>html[data-text-size=large]{font-size:125%}html[data-text-size=xlarge]{font-size:150%}</style>\n * ```\n *\n * Under a hash-based Content Security Policy (`script-src 'sha256-…'`) this is a second\n * inline script with its own hash: add it to the policy, and recompute it whenever the\n * storage key or the account function in the call changes (keksdose added it to both\n * its Caddyfiles, and its guard test hashes every inline script).\n */\nexport const TEXT_SIZE_INLINE_SCRIPT =\n \"(function(k,a){function ok(v){return v==='normal'||v==='large'||v==='xlarge'}\" +\n \"var v=null;try{var s=JSON.parse(localStorage.getItem(k)||'null');v=s&&s.state&&s.state.size}catch(e){}\" +\n \"if(!ok(v)){v=null;try{v=a?a():null}catch(e){}}\" +\n \"try{document.documentElement.setAttribute('data-text-size',ok(v)?v:'normal')}catch(e){}})\";\n\n/* ── The store ──────────────────────────────────────────────────────────── */\n\nexport interface TextSizeState {\n /** This device's own choice (persisted), or `null` when it never chose — then the\n * account's value applies (§2.2). */\n size: TextSize | null;\n /** Store this device's choice; `null` forgets it, so the account's applies again. */\n setSize: (size: TextSize | null) => void;\n}\n\nexport interface ApplyTextSizeOptions {\n /** The account's value (`/auth/me`'s `text_size`): it applies while this device has no\n * choice of its own. Leave it out where there is no account. */\n account?: string | null;\n}\n\n/**\n * Build a text-size store bound to a consumer-supplied localStorage key, shaped like\n * `createThemeStore` (§6). It holds the DEVICE's choice only; the account's value comes\n * in through `useApplyTextSize({ account })` and `useAccountAppearance`, and the size in\n * force is always {@link resolveTextSize} of the two.\n *\n * - `useTextSizeStore` — the zustand hook: `size` (device choice or null) and `setSize`.\n * - `useApplyTextSize({ account })` — the effect that puts the size in force on `<html>`.\n * Mount it once near the root, beside `useApplyTheme`.\n *\n * The persisted format is {@link PersistedTextSize}, and frozen.\n */\nexport function createTextSizeStore(storageKey: string) {\n const useTextSizeStore = create<TextSizeState>()(\n logger(\n persist(\n (set) => ({\n size: null,\n setSize: (size) => set({ size: isTextSize(size) ? size : null }),\n }),\n {\n name: storageKey,\n version: 1,\n partialize: (s) => ({ size: s.size }),\n // A hand-edited or future value must not reach <html> as an unknown size.\n merge: (persisted, current) => ({\n ...current,\n size: isTextSize((persisted as { size?: unknown } | null)?.size)\n ? (persisted as { size: TextSize }).size\n : null,\n }),\n },\n ),\n \"text-size\",\n ),\n );\n // Reset this device's settings (docs/settings-harmonization.md §11.4): no choice on\n // this device, so `useApplyTextSize` re-resolves to the account's size, else Normal.\n registerDeviceSetting({\n key: storageKey,\n kind: \"appearance\",\n reset: () => useTextSizeStore.getState().setSize(null),\n });\n\n function useApplyTextSize({ account }: ApplyTextSizeOptions = {}): TextSize {\n const device = useTextSizeStore((s) => s.size);\n const size = resolveTextSize(device, account);\n useEffect(() => {\n applyTextSize(size);\n }, [size]);\n return size;\n }\n\n return { useTextSizeStore, useApplyTextSize };\n}\n\nexport type TextSizeStore = ReturnType<typeof createTextSizeStore>;\n"],"mappings":"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","names":[]}
|
|
@@ -58,9 +58,7 @@ function createThemeStore(storageKey) {
|
|
|
58
58
|
// Only the preference is persisted; `mode` is derived on load.
|
|
59
59
|
partialize: (s) => ({ preference: s.preference }),
|
|
60
60
|
migrate: (persistedState) => ({
|
|
61
|
-
preference: preferenceFromState(
|
|
62
|
-
persistedState
|
|
63
|
-
)
|
|
61
|
+
preference: preferenceFromState(persistedState)
|
|
64
62
|
}),
|
|
65
63
|
onRehydrateStorage: () => (state) => {
|
|
66
64
|
if (state) state.mode = resolve(state.preference);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/theme/theme-store.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { logger } from \"../lib/logger\";\nimport { registerDeviceSetting } from \"../lib/device-settings\";\n\nexport type ThemeMode = \"light\" | \"dark\";\n/** What the user chose; \"system\" tracks the OS `prefers-color-scheme`. */\nexport type ThemePreference = \"light\" | \"dark\" | \"system\";\n\nexport interface ThemeState {\n /** The user's choice (persisted). */\n preference: ThemePreference;\n /** The RESOLVED light/dark actually in effect (system → OS). Read this for\n * anything that needs the concrete theme (e.g. token selection, toasts). */\n mode: ThemeMode;\n setPreference: (preference: ThemePreference) => void;\n /** Set an explicit light/dark choice (preference follows). */\n setMode: (mode: ThemeMode) => void;\n /** Flip the effective theme to the opposite explicit choice. */\n toggle: () => void;\n}\n\nfunction systemPreference(): ThemeMode {\n try {\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n } catch {\n return \"light\";\n }\n}\n\nfunction resolve(preference: ThemePreference): ThemeMode {\n return preference === \"system\" ? systemPreference() : preference;\n}\n\n/** Normalize a persisted theme-state blob to a valid preference. Older blobs\n * persisted the concrete `mode` (light/dark); carry that over as an explicit\n * preference so the choice survives. Shared by the store's `migrate` and the\n * pre-hydration {@link applyPersistedTheme}. */\nfunction preferenceFromState(
|
|
1
|
+
{"version":3,"sources":["../../src/theme/theme-store.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { logger } from \"../lib/logger\";\nimport { registerDeviceSetting } from \"../lib/device-settings\";\n\nexport type ThemeMode = \"light\" | \"dark\";\n/** What the user chose; \"system\" tracks the OS `prefers-color-scheme`. */\nexport type ThemePreference = \"light\" | \"dark\" | \"system\";\n\nexport interface ThemeState {\n /** The user's choice (persisted). */\n preference: ThemePreference;\n /** The RESOLVED light/dark actually in effect (system → OS). Read this for\n * anything that needs the concrete theme (e.g. token selection, toasts). */\n mode: ThemeMode;\n setPreference: (preference: ThemePreference) => void;\n /** Set an explicit light/dark choice (preference follows). */\n setMode: (mode: ThemeMode) => void;\n /** Flip the effective theme to the opposite explicit choice. */\n toggle: () => void;\n}\n\nfunction systemPreference(): ThemeMode {\n try {\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n } catch {\n return \"light\";\n }\n}\n\nfunction resolve(preference: ThemePreference): ThemeMode {\n return preference === \"system\" ? systemPreference() : preference;\n}\n\n/** Normalize a persisted theme-state blob to a valid preference. Older blobs\n * persisted the concrete `mode` (light/dark); carry that over as an explicit\n * preference so the choice survives. Shared by the store's `migrate` and the\n * pre-hydration {@link applyPersistedTheme}. */\nfunction preferenceFromState(s: { mode?: string; preference?: string } | null | undefined): ThemePreference {\n if (s?.preference === \"light\" || s?.preference === \"dark\" || s?.preference === \"system\") {\n return s.preference;\n }\n if (s?.mode === \"light\" || s?.mode === \"dark\") return s.mode;\n return \"system\";\n}\n\n/**\n * Toggle the `.dark` class on <html> for a resolved light/dark mode, and say the same\n * to the BROWSER through `color-scheme` (0.47, keksdose dev #600): the class switches\n * the kit's tokens, but everything the browser paints itself — a native `<select>`'s\n * open list, a date input's popup, scrollbars, autofill — follows `color-scheme` only,\n * and stayed light on the dark theme. Inline on <html>, so it holds whatever an app's\n * stylesheet says, and it is set by the pre-paint {@link applyPersistedTheme} too.\n */\nfunction applyThemeClass(mode: ThemeMode): void {\n const root = document.documentElement;\n if (mode === \"dark\") root.classList.add(\"dark\");\n else root.classList.remove(\"dark\");\n root.style.colorScheme = mode;\n}\n\n/**\n * Pre-hydration: read the theme PREFERENCE persisted under `storageKey` straight\n * from localStorage, resolve it to a concrete light/dark mode, and toggle the\n * `.dark` class (and `color-scheme`) on <html> — BEFORE React and the store mount,\n * so the first paint already has the right theme (no flash). Returns the resolved\n * mode so the caller can chain {@link applyPersistedPalette}. Encapsulates the\n * zustand-persist envelope + the legacy `mode`→`preference` fallback so apps never re-type the\n * storage shape in their entrypoint.\n */\nexport function applyPersistedTheme(storageKey: string): ThemeMode {\n let preference: ThemePreference = \"system\";\n try {\n const blob = JSON.parse(localStorage.getItem(storageKey) ?? \"{}\") as {\n state?: { mode?: string; preference?: string };\n };\n preference = preferenceFromState(blob.state);\n } catch {\n /* malformed storage → system */\n }\n const mode = resolve(preference);\n applyThemeClass(mode);\n return mode;\n}\n\n/**\n * Build a theme store bound to a consumer-supplied localStorage key. Tracks a\n * light/dark/system PREFERENCE (feedback #330) and exposes the resolved `mode`.\n * Returns the `useTheme` hook plus a `useApplyTheme` effect that toggles the\n * `.dark` class on <html> and, while on \"system\", follows OS theme changes live.\n */\nexport function createThemeStore(storageKey: string) {\n const useTheme = create<ThemeState>()(\n logger(\n persist(\n (set, get) => ({\n preference: \"system\" as ThemePreference,\n mode: systemPreference(),\n setPreference: (preference) => set({ preference, mode: resolve(preference) }),\n setMode: (mode) => set({ preference: mode, mode }),\n toggle: () => {\n const next: ThemeMode = get().mode === \"dark\" ? \"light\" : \"dark\";\n set({ preference: next, mode: next });\n },\n }),\n {\n name: storageKey,\n version: 2,\n // Only the preference is persisted; `mode` is derived on load.\n partialize: (s) => ({ preference: s.preference }),\n migrate: (persistedState): { preference: ThemePreference } => ({\n preference: preferenceFromState(persistedState as { mode?: string; preference?: string } | null),\n }),\n onRehydrateStorage: () => (state) => {\n // The persisted preference decides the concrete mode at load.\n if (state) state.mode = resolve(state.preference);\n },\n },\n ),\n \"theme\",\n ),\n );\n // Reset this device's settings (docs/settings-harmonization.md §11.4): back to\n // \"System\" through the setter, which re-resolves `mode`; `useApplyTheme` puts the\n // class back and follows the OS again, and the persisted blob is what the pre-paint\n // `applyPersistedTheme` reads next load.\n registerDeviceSetting({\n key: storageKey,\n kind: \"appearance\",\n reset: () => useTheme.getState().setPreference(\"system\"),\n });\n\n function useApplyTheme(): void {\n const mode = useTheme((s) => s.mode);\n const preference = useTheme((s) => s.preference);\n useEffect(() => {\n applyThemeClass(mode);\n }, [mode]);\n // While tracking the system, follow OS theme changes live.\n useEffect(() => {\n if (preference !== \"system\") return;\n let mq: MediaQueryList;\n try {\n mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n } catch {\n return;\n }\n const onChange = () => useTheme.setState({ mode: mq.matches ? \"dark\" : \"light\" });\n onChange();\n mq.addEventListener?.(\"change\", onChange);\n return () => mq.removeEventListener?.(\"change\", onChange);\n }, [preference]);\n }\n\n return { useTheme, useApplyTheme };\n}\n\nexport type ThemeStore = ReturnType<typeof createThemeStore>;\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tour/tour-catalogue.tsx"],"sourcesContent":["import { useId, type ReactNode } from \"react\";\nimport { Circle, CircleCheck, Clock, Play } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitFormat } from \"../lib/format\";\nimport { foldForSearch } from \"../lib/text-match\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { Chip } from \"../components/chip\";\nimport { Caption } from \"../components/text\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { SearchField } from \"../components/search-field\";\nimport { Select } from \"../components/select\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport { DEFAULT_TOUR_CATALOGUE_LABELS, type TourCatalogueLabels } from \"./tour-catalogue-labels\";\nimport { isTourDone, isTourNew, isTourReset, type TourCompletions, type TourGroup, type TourInfo } from \"./tour-info\";\nimport type { TourCatalogueFilter, TourStateFilter } from \"./use-tour-url-sync\";\n\nexport { DEFAULT_TOUR_CATALOGUE_LABELS };\nexport type { TourCatalogueLabels };\n\n/* ── The card ───────────────────────────────────────────────────────────────── */\n\nexport interface TourCatalogueCardProps {\n tour: TourInfo;\n /** The completion's ISO timestamp, `null` for one without a date; absent while untaken. */\n completedAt?: string | null;\n /** NEW — `isTourNew`. The catalogue works it out; a card on its own is told. */\n isNew?: boolean;\n onLaunch: (id: string) => void;\n /** \"Mark as not done\". Without it, the link is not drawn. */\n onReset?: (id: string) => void;\n /**\n * Formats the completion and update days. Default: the kit's medium date in the provider's locale.\n * It gets the completion's ISO instant and the update's bare `\"YYYY-MM-DD\"` (`updated.date`): read\n * the bare day on its calendar (the kit's `formatDate`, `parseIsoDate`), never `new Date(day)`,\n * which is UTC midnight and the day before west of UTC (round 0.45, N9).\n */\n formatDate?: (iso: string) => string;\n labels?: LabelOverride<TourCatalogueLabels>;\n className?: string;\n}\n\n/**\n * One tour's card — keksdose's (tours-page.tsx `TourCard`, dev#557), laid out top to\n * bottom so the cards sit side by side: the state icon beside a column holding the tour's\n * icon, title, NEW (or \"Updated\", §8) and the app's `badge`, the one-liner and, from `sm`,\n * the longer account. The foot is pinned to the bottom edge, so every Start in a row of\n * cards lands on one line: the points chip, the step count and the duration, the\n * completion (or reset) line, \"Mark as not done\" and Start / Replay. Points, steps and\n * duration are each left out when the tour has none — no gap, no \"0 points\" (KS).\n */\nexport function TourCatalogueCard({\n tour,\n completedAt,\n isNew = false,\n onLaunch,\n onReset,\n formatDate: formatDateProp,\n labels: labelsProp,\n className,\n}: TourCatalogueCardProps) {\n const words = useKitLabels(\"tourCatalogue\", DEFAULT_TOUR_CATALOGUE_LABELS, labelsProp);\n const { formatDate } = useKitFormat();\n const day = formatDateProp ?? ((iso: string) => formatDate(iso, \"medium\"));\n const titleId = useId();\n const done = isTourDone(tour, completedAt);\n const reset = isTourReset(tour, completedAt);\n const hasFacts = tour.points != null || tour.steps != null || tour.duration != null;\n const note = done\n ? completedAt\n ? words.completedOn(day(completedAt))\n : words.completed\n : reset && tour.updated\n ? words.resetOn(day(tour.updated.date), tour.updated.reason)\n : null;\n\n return (\n <Card className={cn(\"flex h-full flex-col p-4\", className)}>\n {/* Title, one-liner and the long body share ONE left edge, in the column beside the\n state icon. */}\n <div className=\"flex flex-1 items-start gap-3\">\n <div className={cn(\"mt-0.5 shrink-0\", done ? \"text-success\" : \"text-muted\")}>\n {done ? <CircleCheck aria-hidden className=\"size-5\" /> : <Circle aria-hidden className=\"size-5\" />}\n <span className=\"sr-only\">{done ? words.completed : words.notCompleted}</span>\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n {tour.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 text-brand [&_svg]:size-4\">\n {tour.icon}\n </span>\n )}\n {/* The id on the words, not the heading: the Start button is described by them. */}\n <h3 className=\"font-medium\">\n <span id={titleId}>{tour.title}</span>\n </h3>\n {/* It retires itself the moment the tour is taken — nothing to dismiss. */}\n {isNew && !reset && (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeNew}\n </Chip>\n )}\n {reset && (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeUpdated}\n </Chip>\n )}\n {tour.badge}\n </div>\n {tour.description && <p className=\"mt-1 text-sm text-muted\">{tour.description}</p>}\n {/* `hidden sm:block`: one card per row is the phone, where these paragraphs\n would turn the page into a scroll; the search still reads them. */}\n {tour.about && <p className=\"mt-2 hidden text-sm leading-relaxed text-secondary sm:block\">{tour.about}</p>}\n </div>\n </div>\n <div className=\"mt-3 flex flex-wrap items-end justify-between gap-2\">\n <div className=\"min-w-0\">\n {hasFacts && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {tour.points != null && (\n <Chip size=\"sm\" tone=\"brand\">\n {words.points(tour.points)}\n </Chip>\n )}\n {tour.steps != null && <Caption as=\"span\">{words.steps(tour.steps)}</Caption>}\n {tour.duration != null && (\n <Caption as=\"span\" className=\"inline-flex items-center gap-1\">\n <Clock aria-hidden className=\"size-3\" />\n {words.duration(tour.duration)}\n </Caption>\n )}\n </div>\n )}\n {note && <p className={cn(\"text-xs text-muted\", hasFacts && \"mt-1\")}>{note}</p>}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {done && onReset && (\n <Button variant=\"link\" tone=\"muted\" size=\"sm\" aria-describedby={titleId} onClick={() => onReset(tour.id)}>\n {words.markUndone}\n </Button>\n )}\n <Button aria-describedby={titleId} onClick={() => onLaunch(tour.id)}>\n <Play aria-hidden className=\"size-4\" /> {done ? words.replay : words.start}\n </Button>\n </div>\n </div>\n </Card>\n );\n}\n\n/* ── The page body ─────────────────────────────────────────────────────────── */\n\nexport interface TourCatalogueProps extends TourCatalogueFilter {\n /** The tours this person may take, already filtered by role and data (§6.3). */\n tours: readonly TourInfo[];\n /** The sections, in the app's order. A tour whose group is not here is not listed. */\n groups: readonly TourGroup[];\n completed: TourCompletions;\n /** The account's `created_at`: a tour shipped after it, untaken, is NEW. Unknown → none is. */\n joined?: string | null;\n /** A filter changed — `{query}`, `{group}`, `{state}`, or all three for \"Clear filters\". */\n onChange: (patch: Partial<TourCatalogueFilter>) => void;\n onLaunch: (id: string) => void;\n /** \"Mark as not done\" — `useTourProgress().reset`. */\n onReset?: (id: string) => void;\n /** Above the filter bar: the app's page header with its points tile (§6.1). */\n header?: ReactNode;\n /** Each card's `formatDate`: see {@link TourCatalogueCardProps.formatDate} (a bare day for the update). */\n formatDate?: (iso: string) => string;\n /** The search's locale (case folding). Default: the provider's. */\n locale?: string;\n labels?: LabelOverride<TourCatalogueLabels>;\n className?: string;\n}\n\nconst STATE_FILTERS: readonly TourStateFilter[] = [\"all\", \"todo\", \"done\"];\n\n/**\n * The tours page's body (docs/tours-harmonization.md §2.1, §3) — keksdose's /tours:\n *\n * - **The filter bar**: a search, a group select and an all / to do / done toggle,\n * stacked below `lg` and side by side from it. Each count is what the OTHER filters\n * leave: the toggle counts the search and the group, each group the search and the\n * state. Under it, while a filter is on, \"Showing X of Y tours\".\n * - **The sections**: one per group, in the app's order — an h2 with its icon and help\n * line over a `sm:2 / xl:3` grid of `TourCatalogueCard`s. A group the filters empty is\n * hidden; when nothing is left, an empty state with \"Clear filters\".\n * - **Controlled**: `query`, `group`, `state` and `onChange`, so the app keeps them in its\n * URL (`useTourCatalogueUrlState`).\n *\n * A tour reset by an update (§8) is \"to do\" for the toggle and the counts.\n */\nexport function TourCatalogue({\n tours,\n groups,\n completed,\n joined,\n query,\n group: groupProp,\n state: stateProp,\n onChange,\n onLaunch,\n onReset,\n header,\n formatDate,\n locale: localeProp,\n labels: labelsProp,\n className,\n}: TourCatalogueProps) {\n const words = useKitLabels(\"tourCatalogue\", DEFAULT_TOUR_CATALOGUE_LABELS, labelsProp);\n const uid = useId();\n const { locale: kitLocale } = useKitFormat();\n const locale = localeProp ?? kitLocale;\n // A group or state the list does not have is no filter at all (an old link).\n const group = groups.some((g) => g.id === groupProp) ? groupProp : null;\n const state = STATE_FILTERS.includes(stateProp) ? stateProp : \"all\";\n\n const listed = tours.filter((t) => groups.some((g) => g.id === t.group));\n const done = (tour: TourInfo) => isTourDone(tour, completed[tour.id]);\n const needle = foldForSearch(query.trim(), locale);\n const matchesQuery = (tour: TourInfo) =>\n !needle || [tour.title, tour.description, tour.about].some((text) => !!text && foldForSearch(text, locale).includes(needle));\n const inState = (tour: TourInfo) => (state === \"all\" ? true : state === \"done\" ? done(tour) : !done(tour));\n const inGroup = (tour: TourInfo, id: string | null = group) => id === null || tour.group === id;\n\n const found = listed.filter((t) => matchesQuery(t) && inGroup(t));\n const foundDone = found.filter(done).length;\n const visible = found.filter(inState);\n const groupCount = (id: string | null) => listed.filter((t) => matchesQuery(t) && inState(t) && inGroup(t, id)).length;\n const filtering = needle.length > 0 || state !== \"all\" || group !== null;\n const sections = groups\n .map((g) => ({ ...g, tours: visible.filter((t) => t.group === g.id) }))\n .filter((g) => g.tours.length > 0);\n\n return (\n <div className={cn(\"space-y-4\", className)}>\n {header}\n <div className=\"flex flex-col gap-2 lg:flex-row lg:items-center\">\n <SearchField\n className=\"lg:max-w-xs lg:flex-1\"\n value={query}\n onChange={(value) => onChange({ query: value })}\n aria-label={words.searchLabel}\n placeholder={words.searchPlaceholder}\n />\n {/* A select, not a chip per group: on a phone those wrap into rows above the list\n they filter, and an app may have ten groups (ka F8). Named, not labelled: a\n floating label makes it taller than the search box beside it (dev #589). */}\n <Select\n className=\"lg:w-60\"\n aria-label={words.groupLabel}\n value={group ?? \"\"}\n onChange={(event) => onChange({ group: event.target.value || null })}\n >\n <option value=\"\">{words.groupAll(groupCount(null))}</option>\n {groups.map((g) => (\n <option key={g.id} value={g.id}>\n {words.groupOption(g.title, groupCount(g.id))}\n </option>\n ))}\n </Select>\n <ToggleGroup\n value={state}\n onChange={(value: TourStateFilter) => onChange({ state: value })}\n aria-label={words.stateLabel}\n className=\"lg:w-auto\"\n options={[\n { value: \"all\", label: words.stateAll(found.length) },\n { value: \"todo\", label: words.stateTodo(found.length - foundDone) },\n { value: \"done\", label: words.stateDone(foundDone) },\n ]}\n />\n </div>\n {filtering && (\n <p role=\"status\" className=\"-mt-2 text-xs text-muted\">\n {words.showing(visible.length, listed.length)}\n </p>\n )}\n\n {visible.length === 0 ? (\n <div className=\"space-y-3\">\n <EmptyState title={words.noMatches} hint={words.noMatchesHint} />\n <div className=\"flex justify-center\">\n <Button variant=\"secondary\" onClick={() => onChange({ query: \"\", group: null, state: \"all\" })}>\n {words.clearFilters}\n </Button>\n </div>\n </div>\n ) : (\n // A GRID, never columns: a section holding one card keeps a cell's width.\n sections.map((section) => (\n <section key={section.id} aria-labelledby={`${uid}-${section.id}`} className=\"space-y-3\">\n <h2 id={`${uid}-${section.id}`} className=\"flex items-center gap-1.5 text-sm font-semibold text-muted\">\n {section.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 [&_svg]:size-4\">\n {section.icon}\n </span>\n )}\n {section.title}\n </h2>\n {section.help && <p className=\"-mt-2 text-xs text-muted\">{section.help}</p>}\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {section.tours.map((tour) => (\n <TourCatalogueCard\n key={tour.id}\n tour={tour}\n completedAt={completed[tour.id]}\n isNew={isTourNew({ since: tour.since, joined, completed: completed[tour.id] !== undefined })}\n onLaunch={onLaunch}\n onReset={onReset}\n formatDate={formatDate}\n labels={labelsProp}\n />\n ))}\n </div>\n </section>\n ))\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/tour/tour-catalogue.tsx"],"sourcesContent":["import { useId, type ReactNode } from \"react\";\nimport { Circle, CircleCheck, Clock, Play } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitFormat } from \"../lib/format\";\nimport { foldForSearch } from \"../lib/text-match\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { Chip } from \"../components/chip\";\nimport { Caption } from \"../components/text\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { SearchField } from \"../components/search-field\";\nimport { Select } from \"../components/select\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport { DEFAULT_TOUR_CATALOGUE_LABELS, type TourCatalogueLabels } from \"./tour-catalogue-labels\";\nimport { isTourDone, isTourNew, isTourReset, type TourCompletions, type TourGroup, type TourInfo } from \"./tour-info\";\nimport type { TourCatalogueFilter, TourStateFilter } from \"./use-tour-url-sync\";\n\nexport { DEFAULT_TOUR_CATALOGUE_LABELS };\nexport type { TourCatalogueLabels };\n\n/* ── The card ───────────────────────────────────────────────────────────────── */\n\nexport interface TourCatalogueCardProps {\n tour: TourInfo;\n /** The completion's ISO timestamp, `null` for one without a date; absent while untaken. */\n completedAt?: string | null;\n /** NEW — `isTourNew`. The catalogue works it out; a card on its own is told. */\n isNew?: boolean;\n onLaunch: (id: string) => void;\n /** \"Mark as not done\". Without it, the link is not drawn. */\n onReset?: (id: string) => void;\n /**\n * Formats the completion and update days. Default: the kit's medium date in the provider's locale.\n * It gets the completion's ISO instant and the update's bare `\"YYYY-MM-DD\"` (`updated.date`): read\n * the bare day on its calendar (the kit's `formatDate`, `parseIsoDate`), never `new Date(day)`,\n * which is UTC midnight and the day before west of UTC (round 0.45, N9).\n */\n formatDate?: (iso: string) => string;\n labels?: LabelOverride<TourCatalogueLabels>;\n className?: string;\n}\n\n/**\n * One tour's card — keksdose's (tours-page.tsx `TourCard`, dev#557), laid out top to\n * bottom so the cards sit side by side: the state icon beside a column holding the tour's\n * icon, title, NEW (or \"Updated\", §8) and the app's `badge`, the one-liner and, from `sm`,\n * the longer account. The foot is pinned to the bottom edge, so every Start in a row of\n * cards lands on one line: the points chip, the step count and the duration, the\n * completion (or reset) line, \"Mark as not done\" and Start / Replay. Points, steps and\n * duration are each left out when the tour has none — no gap, no \"0 points\" (KS).\n */\nexport function TourCatalogueCard({\n tour,\n completedAt,\n isNew = false,\n onLaunch,\n onReset,\n formatDate: formatDateProp,\n labels: labelsProp,\n className,\n}: TourCatalogueCardProps) {\n const words = useKitLabels(\"tourCatalogue\", DEFAULT_TOUR_CATALOGUE_LABELS, labelsProp);\n const { formatDate } = useKitFormat();\n const day = formatDateProp ?? ((iso: string) => formatDate(iso, \"medium\"));\n const titleId = useId();\n const done = isTourDone(tour, completedAt);\n const reset = isTourReset(tour, completedAt);\n const hasFacts = tour.points != null || tour.steps != null || tour.duration != null;\n const note = done\n ? completedAt\n ? words.completedOn(day(completedAt))\n : words.completed\n : reset && tour.updated\n ? words.resetOn(day(tour.updated.date), tour.updated.reason)\n : null;\n\n return (\n <Card className={cn(\"flex h-full flex-col p-4\", className)}>\n {/* Title, one-liner and the long body share ONE left edge, in the column beside the\n state icon. */}\n <div className=\"flex flex-1 items-start gap-3\">\n <div className={cn(\"mt-0.5 shrink-0\", done ? \"text-success\" : \"text-muted\")}>\n {done ? <CircleCheck aria-hidden className=\"size-5\" /> : <Circle aria-hidden className=\"size-5\" />}\n <span className=\"sr-only\">{done ? words.completed : words.notCompleted}</span>\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n {tour.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 text-brand [&_svg]:size-4\">\n {tour.icon}\n </span>\n )}\n {/* The id on the words, not the heading: the Start button is described by them. */}\n <h3 className=\"font-medium\">\n <span id={titleId}>{tour.title}</span>\n </h3>\n {/* It retires itself the moment the tour is taken — nothing to dismiss. */}\n {isNew && !reset && (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeNew}\n </Chip>\n )}\n {reset && (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeUpdated}\n </Chip>\n )}\n {tour.badge}\n </div>\n {tour.description && <p className=\"mt-1 text-sm text-muted\">{tour.description}</p>}\n {/* `hidden sm:block`: one card per row is the phone, where these paragraphs\n would turn the page into a scroll; the search still reads them. */}\n {tour.about && <p className=\"mt-2 hidden text-sm leading-relaxed text-secondary sm:block\">{tour.about}</p>}\n </div>\n </div>\n <div className=\"mt-3 flex flex-wrap items-end justify-between gap-2\">\n <div className=\"min-w-0\">\n {hasFacts && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {tour.points != null && (\n <Chip size=\"sm\" tone=\"brand\">\n {words.points(tour.points)}\n </Chip>\n )}\n {tour.steps != null && <Caption as=\"span\">{words.steps(tour.steps)}</Caption>}\n {tour.duration != null && (\n <Caption as=\"span\" className=\"inline-flex items-center gap-1\">\n <Clock aria-hidden className=\"size-3\" />\n {words.duration(tour.duration)}\n </Caption>\n )}\n </div>\n )}\n {note && <p className={cn(\"text-xs text-muted\", hasFacts && \"mt-1\")}>{note}</p>}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {done && onReset && (\n <Button variant=\"link\" tone=\"muted\" size=\"sm\" aria-describedby={titleId} onClick={() => onReset(tour.id)}>\n {words.markUndone}\n </Button>\n )}\n <Button aria-describedby={titleId} onClick={() => onLaunch(tour.id)}>\n <Play aria-hidden className=\"size-4\" /> {done ? words.replay : words.start}\n </Button>\n </div>\n </div>\n </Card>\n );\n}\n\n/* ── The page body ─────────────────────────────────────────────────────────── */\n\nexport interface TourCatalogueProps extends TourCatalogueFilter {\n /** The tours this person may take, already filtered by role and data (§6.3). */\n tours: readonly TourInfo[];\n /** The sections, in the app's order. A tour whose group is not here is not listed. */\n groups: readonly TourGroup[];\n completed: TourCompletions;\n /** The account's `created_at`: a tour shipped after it, untaken, is NEW. Unknown → none is. */\n joined?: string | null;\n /** A filter changed — `{query}`, `{group}`, `{state}`, or all three for \"Clear filters\". */\n onChange: (patch: Partial<TourCatalogueFilter>) => void;\n onLaunch: (id: string) => void;\n /** \"Mark as not done\" — `useTourProgress().reset`. */\n onReset?: (id: string) => void;\n /** Above the filter bar: the app's page header with its points tile (§6.1). */\n header?: ReactNode;\n /** Each card's `formatDate`: see {@link TourCatalogueCardProps.formatDate} (a bare day for the update). */\n formatDate?: (iso: string) => string;\n /** The search's locale (case folding). Default: the provider's. */\n locale?: string;\n labels?: LabelOverride<TourCatalogueLabels>;\n className?: string;\n}\n\nconst STATE_FILTERS: readonly TourStateFilter[] = [\"all\", \"todo\", \"done\"];\n\n/**\n * The tours page's body (docs/tours-harmonization.md §2.1, §3) — keksdose's /tours:\n *\n * - **The filter bar**: a search, a group select and an all / to do / done toggle,\n * stacked below `lg` and side by side from it. Each count is what the OTHER filters\n * leave: the toggle counts the search and the group, each group the search and the\n * state. Under it, while a filter is on, \"Showing X of Y tours\".\n * - **The sections**: one per group, in the app's order — an h2 with its icon and help\n * line over a `sm:2 / xl:3` grid of `TourCatalogueCard`s. A group the filters empty is\n * hidden; when nothing is left, an empty state with \"Clear filters\".\n * - **Controlled**: `query`, `group`, `state` and `onChange`, so the app keeps them in its\n * URL (`useTourCatalogueUrlState`).\n *\n * A tour reset by an update (§8) is \"to do\" for the toggle and the counts.\n */\nexport function TourCatalogue({\n tours,\n groups,\n completed,\n joined,\n query,\n group: groupProp,\n state: stateProp,\n onChange,\n onLaunch,\n onReset,\n header,\n formatDate,\n locale: localeProp,\n labels: labelsProp,\n className,\n}: TourCatalogueProps) {\n const words = useKitLabels(\"tourCatalogue\", DEFAULT_TOUR_CATALOGUE_LABELS, labelsProp);\n const uid = useId();\n const { locale: kitLocale } = useKitFormat();\n const locale = localeProp ?? kitLocale;\n // A group or state the list does not have is no filter at all (an old link).\n const group = groups.some((g) => g.id === groupProp) ? groupProp : null;\n const state = STATE_FILTERS.includes(stateProp) ? stateProp : \"all\";\n\n const listed = tours.filter((t) => groups.some((g) => g.id === t.group));\n const done = (tour: TourInfo) => isTourDone(tour, completed[tour.id]);\n const needle = foldForSearch(query.trim(), locale);\n const matchesQuery = (tour: TourInfo) =>\n !needle ||\n [tour.title, tour.description, tour.about].some((text) => !!text && foldForSearch(text, locale).includes(needle));\n const inState = (tour: TourInfo) => (state === \"all\" ? true : state === \"done\" ? done(tour) : !done(tour));\n const inGroup = (tour: TourInfo, id: string | null = group) => id === null || tour.group === id;\n\n const found = listed.filter((t) => matchesQuery(t) && inGroup(t));\n const foundDone = found.filter(done).length;\n const visible = found.filter(inState);\n const groupCount = (id: string | null) =>\n listed.filter((t) => matchesQuery(t) && inState(t) && inGroup(t, id)).length;\n const filtering = needle.length > 0 || state !== \"all\" || group !== null;\n const sections = groups\n .map((g) => ({ ...g, tours: visible.filter((t) => t.group === g.id) }))\n .filter((g) => g.tours.length > 0);\n\n return (\n <div className={cn(\"space-y-4\", className)}>\n {header}\n <div className=\"flex flex-col gap-2 lg:flex-row lg:items-center\">\n <SearchField\n className=\"lg:max-w-xs lg:flex-1\"\n value={query}\n onChange={(value) => onChange({ query: value })}\n aria-label={words.searchLabel}\n placeholder={words.searchPlaceholder}\n />\n {/* A select, not a chip per group: on a phone those wrap into rows above the list\n they filter, and an app may have ten groups (ka F8). Named, not labelled: a\n floating label makes it taller than the search box beside it (dev #589). */}\n <Select\n className=\"lg:w-60\"\n aria-label={words.groupLabel}\n value={group ?? \"\"}\n onChange={(event) => onChange({ group: event.target.value || null })}\n >\n <option value=\"\">{words.groupAll(groupCount(null))}</option>\n {groups.map((g) => (\n <option key={g.id} value={g.id}>\n {words.groupOption(g.title, groupCount(g.id))}\n </option>\n ))}\n </Select>\n <ToggleGroup\n value={state}\n onChange={(value: TourStateFilter) => onChange({ state: value })}\n aria-label={words.stateLabel}\n className=\"lg:w-auto\"\n options={[\n { value: \"all\", label: words.stateAll(found.length) },\n { value: \"todo\", label: words.stateTodo(found.length - foundDone) },\n { value: \"done\", label: words.stateDone(foundDone) },\n ]}\n />\n </div>\n {filtering && (\n <p role=\"status\" className=\"-mt-2 text-xs text-muted\">\n {words.showing(visible.length, listed.length)}\n </p>\n )}\n\n {visible.length === 0 ? (\n <div className=\"space-y-3\">\n <EmptyState title={words.noMatches} hint={words.noMatchesHint} />\n <div className=\"flex justify-center\">\n <Button variant=\"secondary\" onClick={() => onChange({ query: \"\", group: null, state: \"all\" })}>\n {words.clearFilters}\n </Button>\n </div>\n </div>\n ) : (\n // A GRID, never columns: a section holding one card keeps a cell's width.\n sections.map((section) => (\n <section key={section.id} aria-labelledby={`${uid}-${section.id}`} className=\"space-y-3\">\n <h2 id={`${uid}-${section.id}`} className=\"flex items-center gap-1.5 text-sm font-semibold text-muted\">\n {section.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 [&_svg]:size-4\">\n {section.icon}\n </span>\n )}\n {section.title}\n </h2>\n {section.help && <p className=\"-mt-2 text-xs text-muted\">{section.help}</p>}\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {section.tours.map((tour) => (\n <TourCatalogueCard\n key={tour.id}\n tour={tour}\n completedAt={completed[tour.id]}\n isNew={isTourNew({ since: tour.since, joined, completed: completed[tour.id] !== undefined })}\n onLaunch={onLaunch}\n onReset={onReset}\n formatDate={formatDate}\n labels={labelsProp}\n />\n ))}\n </div>\n </section>\n ))\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/tour/tour-dock.js
CHANGED
|
@@ -40,7 +40,11 @@ function dockStyle(edge, spot, band) {
|
|
|
40
40
|
maxHeight: cap
|
|
41
41
|
};
|
|
42
42
|
if (edge === "top") {
|
|
43
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
top: band.top <= 0 ? `max(${GUTTER}px, env(safe-area-inset-top))` : band.top + GUTTER,
|
|
46
|
+
bottom: "auto"
|
|
47
|
+
};
|
|
44
48
|
}
|
|
45
49
|
const fromBottom = vh - band.bottom;
|
|
46
50
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tour/tour-dock.ts"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { readKeyboardInset } from \"../hooks/use-anchored-panel\";\n\n/*\n * # The docked card (docs/tours-harmonization.md §11.7, contract round 054 §6)\n *\n * On a phone a field step's card is pinned full-width to the top or bottom edge of the\n * USABLE BAND — the visual viewport less the keyboard, raised above the highest\n * `[data-tour-obstruction]` (the number pad, a picker sheet) — on the edge away from the\n * spotlighted field, so it covers neither the field nor the keyboard nor the pad.\n */\n\nexport type DockEdge = \"top\" | \"bottom\";\n\nexport interface DockRect {\n top: number;\n left: number;\n width: number;\n height: number;\n}\n\n/** The band, in layout-viewport px (what `position: fixed` measures from). */\nexport interface Band {\n top: number;\n bottom: number;\n}\n\n/** Below this the docked layout applies (Tailwind's `sm`). */\nexport const DOCK_MAX_WIDTH = 640;\n/** The gutter between the card and the band's edge, and between the card and the field. */\nconst GUTTER = 8;\n/** An obstruction whose top is within this of the band's top covers the screen. */\nconst FULL_SCREEN_SLACK = 64;\n/** Under this much free space on its edge the card flips (the hysteresis floor). */\nconst MIN_DOCK = 140;\n\n/**\n * The usable band now: the visual viewport while a keyboard is up (`readKeyboardInset`'s\n * rules — pinch zoom is not a keyboard, 100 px minimum), else the layout viewport; its\n * bottom raised to the top of the highest visible obstruction that holds neither the field\n * nor the card. An obstruction that starts at the band's top (a full-screen picker, opened\n * over everything) is not a bottom layer and is ignored; a tall one (the number pad at\n * Large text is over half a phone) still counts.\n */\nexport function readBand(target: Element | null, card: Element | null): Band {\n const vh = window.innerHeight;\n let top = 0;\n let bottom = vh;\n const vv = window.visualViewport;\n if (vv && readKeyboardInset() > 0) {\n top = vv.offsetTop;\n bottom = vv.offsetTop + vv.height;\n }\n for (const el of Array.from(document.querySelectorAll(\"[data-tour-obstruction]\"))) {\n if ((target && el.contains(target)) || (card && el.contains(card))) continue;\n const r = el.getBoundingClientRect();\n if (r.width <= 0 || r.height <= 0 || r.top <= top + FULL_SCREEN_SLACK) continue;\n if (r.top < bottom) bottom = r.top;\n }\n return { top, bottom };\n}\n\n/** The card's free space on `edge`, between the band's edge and the spotlight. */\nfunction room(edge: DockEdge, spot: DockRect, band: Band): number {\n return edge === \"bottom\" ? band.bottom - (spot.top + spot.height) - GUTTER * 2 : spot.top - band.top - GUTTER * 2;\n}\n\n/**\n * The edge away from the field: the bottom when the spotlight's centre is in the band's\n * upper half, else the top. With a `previous` edge, it stays unless the card would have to\n * cover the spotlight there (no flip on every keyboard animation frame).\n */\nexport function dockEdge(spot: DockRect, band: Band, previous: DockEdge | null, cardHeight: number): DockEdge {\n const preferred: DockEdge = spot.top + spot.height / 2 < band.top + (band.bottom - band.top) / 2 ? \"bottom\" : \"top\";\n if (!previous || previous === preferred) return preferred;\n return room(previous, spot, band) >= Math.min(MIN_DOCK, cardHeight) ? previous : preferred;\n}\n\n/** The docked card's position: full-width with 8 px (safe-area aware) gutters, its height\n * capped to the free space so its body scrolls and its title and buttons show. */\nexport function dockStyle(edge: DockEdge, spot: DockRect | null, band: Band): CSSProperties {\n const vh = window.innerHeight;\n const cap = spot ? Math.max(MIN_DOCK / 2, room(edge, spot, band)) : band.bottom - band.top - GUTTER * 2;\n const base: CSSProperties = {\n left: `max(${GUTTER}px, env(safe-area-inset-left))`,\n right: `max(${GUTTER}px, env(safe-area-inset-right))`,\n width: \"auto\",\n maxWidth: \"none\",\n maxHeight: cap,\n };\n if (edge === \"top\") {\n return {
|
|
1
|
+
{"version":3,"sources":["../../src/tour/tour-dock.ts"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { readKeyboardInset } from \"../hooks/use-anchored-panel\";\n\n/*\n * # The docked card (docs/tours-harmonization.md §11.7, contract round 054 §6)\n *\n * On a phone a field step's card is pinned full-width to the top or bottom edge of the\n * USABLE BAND — the visual viewport less the keyboard, raised above the highest\n * `[data-tour-obstruction]` (the number pad, a picker sheet) — on the edge away from the\n * spotlighted field, so it covers neither the field nor the keyboard nor the pad.\n */\n\nexport type DockEdge = \"top\" | \"bottom\";\n\nexport interface DockRect {\n top: number;\n left: number;\n width: number;\n height: number;\n}\n\n/** The band, in layout-viewport px (what `position: fixed` measures from). */\nexport interface Band {\n top: number;\n bottom: number;\n}\n\n/** Below this the docked layout applies (Tailwind's `sm`). */\nexport const DOCK_MAX_WIDTH = 640;\n/** The gutter between the card and the band's edge, and between the card and the field. */\nconst GUTTER = 8;\n/** An obstruction whose top is within this of the band's top covers the screen. */\nconst FULL_SCREEN_SLACK = 64;\n/** Under this much free space on its edge the card flips (the hysteresis floor). */\nconst MIN_DOCK = 140;\n\n/**\n * The usable band now: the visual viewport while a keyboard is up (`readKeyboardInset`'s\n * rules — pinch zoom is not a keyboard, 100 px minimum), else the layout viewport; its\n * bottom raised to the top of the highest visible obstruction that holds neither the field\n * nor the card. An obstruction that starts at the band's top (a full-screen picker, opened\n * over everything) is not a bottom layer and is ignored; a tall one (the number pad at\n * Large text is over half a phone) still counts.\n */\nexport function readBand(target: Element | null, card: Element | null): Band {\n const vh = window.innerHeight;\n let top = 0;\n let bottom = vh;\n const vv = window.visualViewport;\n if (vv && readKeyboardInset() > 0) {\n top = vv.offsetTop;\n bottom = vv.offsetTop + vv.height;\n }\n for (const el of Array.from(document.querySelectorAll(\"[data-tour-obstruction]\"))) {\n if ((target && el.contains(target)) || (card && el.contains(card))) continue;\n const r = el.getBoundingClientRect();\n if (r.width <= 0 || r.height <= 0 || r.top <= top + FULL_SCREEN_SLACK) continue;\n if (r.top < bottom) bottom = r.top;\n }\n return { top, bottom };\n}\n\n/** The card's free space on `edge`, between the band's edge and the spotlight. */\nfunction room(edge: DockEdge, spot: DockRect, band: Band): number {\n return edge === \"bottom\" ? band.bottom - (spot.top + spot.height) - GUTTER * 2 : spot.top - band.top - GUTTER * 2;\n}\n\n/**\n * The edge away from the field: the bottom when the spotlight's centre is in the band's\n * upper half, else the top. With a `previous` edge, it stays unless the card would have to\n * cover the spotlight there (no flip on every keyboard animation frame).\n */\nexport function dockEdge(spot: DockRect, band: Band, previous: DockEdge | null, cardHeight: number): DockEdge {\n const preferred: DockEdge = spot.top + spot.height / 2 < band.top + (band.bottom - band.top) / 2 ? \"bottom\" : \"top\";\n if (!previous || previous === preferred) return preferred;\n return room(previous, spot, band) >= Math.min(MIN_DOCK, cardHeight) ? previous : preferred;\n}\n\n/** The docked card's position: full-width with 8 px (safe-area aware) gutters, its height\n * capped to the free space so its body scrolls and its title and buttons show. */\nexport function dockStyle(edge: DockEdge, spot: DockRect | null, band: Band): CSSProperties {\n const vh = window.innerHeight;\n const cap = spot ? Math.max(MIN_DOCK / 2, room(edge, spot, band)) : band.bottom - band.top - GUTTER * 2;\n const base: CSSProperties = {\n left: `max(${GUTTER}px, env(safe-area-inset-left))`,\n right: `max(${GUTTER}px, env(safe-area-inset-right))`,\n width: \"auto\",\n maxWidth: \"none\",\n maxHeight: cap,\n };\n if (edge === \"top\") {\n return {\n ...base,\n top: band.top <= 0 ? `max(${GUTTER}px, env(safe-area-inset-top))` : band.top + GUTTER,\n bottom: \"auto\",\n };\n }\n const fromBottom = vh - band.bottom;\n return {\n ...base,\n top: \"auto\",\n bottom: fromBottom <= 0 ? `max(${GUTTER}px, env(safe-area-inset-bottom))` : fromBottom + GUTTER,\n };\n}\n\n/** The target's nearest ancestor that scrolls vertically now (a sheet's body), or null. */\nexport function scrollParent(el: Element): HTMLElement | null {\n for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {\n const { overflowY } = getComputedStyle(node);\n if ((overflowY === \"auto\" || overflowY === \"scroll\") && node.scrollHeight > node.clientHeight) return node;\n }\n return null;\n}\n\n/**\n * Scroll the field into the half of the band away from the card, through its nearest\n * scrollable ancestor (the window only when there is none — the page behind a sheet never\n * moves). A field already inside that half is left where it is. Returns the edge the card\n * takes.\n */\nexport function scrollIntoFreeHalf(el: Element, card: Element | null, previous: DockEdge | null): DockEdge {\n const r = el.getBoundingClientRect();\n const band = readBand(el, card);\n const edge = dockEdge(r, band, previous, card instanceof HTMLElement ? card.offsetHeight : MIN_DOCK);\n const mid = band.top + (band.bottom - band.top) / 2;\n const half = edge === \"bottom\" ? { top: band.top + GUTTER, bottom: mid } : { top: mid, bottom: band.bottom - GUTTER };\n if (r.top >= half.top && r.top + r.height <= half.bottom) return edge;\n // Centred in the half, or its top at the half's top when it is taller than the half.\n const delta =\n r.height > half.bottom - half.top ? r.top - half.top : r.top + r.height / 2 - (half.top + half.bottom) / 2;\n const scroller = scrollParent(el);\n if (scroller) scroller.scrollTop += delta;\n // A field in a kit layer with nothing to scroll: the page behind it stays put.\n else if (!el.closest(\"[data-kit-layer]\")) window.scrollBy?.(0, delta);\n return edge;\n}\n"],"mappings":"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","names":[]}
|
package/dist/tour/tour-engine.js
CHANGED
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
function liveEngine(keys, initial = null) {
|
|
3
3
|
let current = initial;
|
|
4
4
|
const read = () => {
|
|
5
|
-
if (!current)
|
|
6
|
-
throw new Error(
|
|
7
|
-
"[ui-kit] The tour engine was read before its provider rendered."
|
|
8
|
-
);
|
|
5
|
+
if (!current) throw new Error("[ui-kit] The tour engine was read before its provider rendered.");
|
|
9
6
|
return current;
|
|
10
7
|
};
|
|
11
8
|
const engine = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tour/tour-engine.ts"],"sourcesContent":["/**\n * 0.50.1 (keksdose N1): the tour engine a launcher is handed as its fourth argument — one\n * object for the life of the provider (or of `useTourUrlSync`) whose fields always read the\n * engine's LATEST render: `engine.active`, `engine.index` after an `await` are current, and\n * `engine.stop()` mid-run stops the run that is up now. Internal: the type is `TourEngine`.\n *\n * `set` is called from a layout effect with each render's engine; `engine` reads it.\n */\nexport function liveEngine<T extends object>(\n keys: readonly (keyof T & string)[],\n initial: T | null = null,\n): { engine: T; set: (current: T) => void } {\n let current = initial;\n const read = () => {\n if (!current)
|
|
1
|
+
{"version":3,"sources":["../../src/tour/tour-engine.ts"],"sourcesContent":["/**\n * 0.50.1 (keksdose N1): the tour engine a launcher is handed as its fourth argument — one\n * object for the life of the provider (or of `useTourUrlSync`) whose fields always read the\n * engine's LATEST render: `engine.active`, `engine.index` after an `await` are current, and\n * `engine.stop()` mid-run stops the run that is up now. Internal: the type is `TourEngine`.\n *\n * `set` is called from a layout effect with each render's engine; `engine` reads it.\n */\nexport function liveEngine<T extends object>(\n keys: readonly (keyof T & string)[],\n initial: T | null = null,\n): { engine: T; set: (current: T) => void } {\n let current = initial;\n const read = () => {\n if (!current) throw new Error(\"[ui-kit] The tour engine was read before its provider rendered.\");\n return current;\n };\n const engine = {} as T;\n for (const key of keys) {\n Object.defineProperty(engine, key, {\n enumerable: true,\n get: () => {\n const value = read()[key];\n // A method is looked up when it is CALLED, so a kept `engine.stop` is the latest too.\n return typeof value === \"function\"\n ? (...args: unknown[]) => (read()[key] as (...a: unknown[]) => unknown)(...args)\n : value;\n },\n });\n }\n return {\n engine,\n set: (next) => {\n current = next;\n },\n };\n}\n"],"mappings":";AAQO,SAAS,WACd,MACA,UAAoB,MACsB;AAC1C,MAAI,UAAU;AACd,QAAM,OAAO,MAAM;AACjB,QAAI,CAAC,QAAS,OAAM,IAAI,MAAM,iEAAiE;AAC/F,WAAO;AAAA,EACT;AACA,QAAM,SAAS,CAAC;AAChB,aAAW,OAAO,MAAM;AACtB,WAAO,eAAe,QAAQ,KAAK;AAAA,MACjC,YAAY;AAAA,MACZ,KAAK,MAAM;AACT,cAAM,QAAQ,KAAK,EAAE,GAAG;AAExB,eAAO,OAAO,UAAU,aACpB,IAAI,SAAqB,KAAK,EAAE,GAAG,EAAmC,GAAG,IAAI,IAC7E;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AAAA,IACL;AAAA,IACA,KAAK,CAAC,SAAS;AACb,gBAAU;AAAA,IACZ;AAAA,EACF;AACF;","names":[]}
|
package/dist/tour/tour.js
CHANGED
|
@@ -295,7 +295,8 @@ function resolveAt(steps, at) {
|
|
|
295
295
|
const anchor = `data-tour="${at}"`;
|
|
296
296
|
const found = steps.findIndex((step) => step.target?.includes(anchor));
|
|
297
297
|
if (found >= 0) return found;
|
|
298
|
-
if (devWarnings())
|
|
298
|
+
if (devWarnings())
|
|
299
|
+
console.warn(`[ui-kit] A tour was started at "${at}", but no step targets [${anchor}]: it starts at 0.`);
|
|
299
300
|
return 0;
|
|
300
301
|
}
|
|
301
302
|
function firstEmptied(steps, floor, index, gates) {
|
|
@@ -827,7 +828,14 @@ function SpotlightShields({ spot }) {
|
|
|
827
828
|
const shield = "pointer-events-auto fixed";
|
|
828
829
|
const bottom = spot.top + spot.height;
|
|
829
830
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
830
|
-
/* @__PURE__ */ jsx(
|
|
831
|
+
/* @__PURE__ */ jsx(
|
|
832
|
+
"div",
|
|
833
|
+
{
|
|
834
|
+
"data-tour-shield": "",
|
|
835
|
+
className: shield,
|
|
836
|
+
style: { top: 0, left: 0, right: 0, height: Math.max(0, spot.top) }
|
|
837
|
+
}
|
|
838
|
+
),
|
|
831
839
|
/* @__PURE__ */ jsx("div", { "data-tour-shield": "", className: shield, style: { top: bottom, left: 0, right: 0, bottom: 0 } }),
|
|
832
840
|
/* @__PURE__ */ jsx(
|
|
833
841
|
"div",
|
|
@@ -953,7 +961,16 @@ function TourCard({
|
|
|
953
961
|
}
|
|
954
962
|
setEdge(null);
|
|
955
963
|
setStyle(
|
|
956
|
-
placeCard(
|
|
964
|
+
placeCard(
|
|
965
|
+
rect,
|
|
966
|
+
card.offsetWidth,
|
|
967
|
+
card.offsetHeight,
|
|
968
|
+
window.innerWidth,
|
|
969
|
+
window.innerHeight,
|
|
970
|
+
12,
|
|
971
|
+
step.placement,
|
|
972
|
+
dir
|
|
973
|
+
)
|
|
957
974
|
);
|
|
958
975
|
}, [ref, rect, index, step.placement, step.target, dir, docked, bandTick]);
|
|
959
976
|
const awaitsTarget = (step.awaitClick || step.awaitInput) && step.target && rect || awaitsRelease;
|
|
@@ -986,15 +1003,7 @@ function TourCard({
|
|
|
986
1003
|
step.action && /* @__PURE__ */ jsx("div", { className: "mt-3", children: step.action }),
|
|
987
1004
|
/* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
|
|
988
1005
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
989
|
-
/* @__PURE__ */ jsx(
|
|
990
|
-
"button",
|
|
991
|
-
{
|
|
992
|
-
type: "button",
|
|
993
|
-
onClick: onSkip,
|
|
994
|
-
className: "text-xs text-placeholder hover:text-secondary",
|
|
995
|
-
children: labels.skip
|
|
996
|
-
}
|
|
997
|
-
),
|
|
1006
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onSkip, className: "text-xs text-placeholder hover:text-secondary", children: labels.skip }),
|
|
998
1007
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
999
1008
|
/* @__PURE__ */ jsx("span", { className: "whitespace-nowrap text-xs tabular-nums text-placeholder", children: labels.step(position, total) }),
|
|
1000
1009
|
!isFirst && /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onBack, className: "px-2.5 py-1 text-xs", children: labels.back }),
|