@eifi1/ui-kit 0.51.3 → 0.51.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/account/account-settings-labels.js.map +1 -1
- package/dist/account/account-settings.js +11 -9
- package/dist/account/account-settings.js.map +1 -1
- package/dist/account/delete-account-setting.js +56 -65
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.js +4 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.js +19 -11
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/admin/admin-tool-links.d.ts +1 -1
- package/dist/admin/admin-tool-links.js +7 -1
- package/dist/admin/admin-tool-links.js.map +1 -1
- package/dist/admin/invitations-panel.js +2 -13
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/mail-test-card.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +4 -11
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/user-roster.js +1 -9
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.js +23 -31
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.js +13 -5
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.js +1 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/checkout-processing.js.map +1 -1
- package/dist/billing/plan-card.js +91 -85
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-limit-toast.js.map +1 -1
- package/dist/billing/plan-limit.js.map +1 -1
- package/dist/billing/plan-price.js.map +1 -1
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/bin/eifi1-pay-page.mjs +6 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.js +1 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.js +29 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +1 -4
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.js +1 -7
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.js +5 -5
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +4 -10
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/card.js +1 -8
- package/dist/components/card.js.map +1 -1
- package/dist/components/chart-zoom-core.js +3 -17
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +4 -22
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/chip-input.js +4 -1
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/chip.js +10 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.js +58 -74
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper-labels.js.map +1 -1
- package/dist/components/column-mapper.js +167 -227
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +5 -9
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +1 -4
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.d.ts +5 -4
- package/dist/components/country-select.js +3 -19
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.js +13 -11
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.js +1 -6
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.js.map +1 -1
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.js +2 -18
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js +7 -5
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.js +3 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.js +49 -57
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.js +31 -44
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-row-actions.js +19 -16
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.js +4 -1
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +8 -0
- package/dist/components/data-table.js +30 -39
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +1 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/description-list.js +10 -10
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +9 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +17 -27
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +25 -35
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-base.js +5 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +1 -1
- package/dist/components/field-parts.js +10 -14
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.js +8 -10
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +4 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.js.map +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/grouped-picker.js +11 -12
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.js +1 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input-labels.js.map +1 -1
- package/dist/components/icon-button.js +4 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/icon-picker.js +6 -10
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/image-grid.js +41 -50
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +6 -12
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +14 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +1 -7
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-labels.js +8 -2
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.d.ts +18 -1
- package/dist/components/legal-page.js +27 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +40 -5
- package/dist/components/legal.js +41 -5
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +26 -36
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list-labels.js +3 -1
- package/dist/components/list-labels.js.map +1 -1
- package/dist/components/list.d.ts +18 -1
- package/dist/components/list.js +54 -13
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.js +29 -53
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +13 -31
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +10 -21
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +3 -16
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/nav-pills.js +10 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.js +1 -7
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +1 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +1 -7
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +13 -22
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.js +1 -11
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/password-strength-labels.js.map +1 -1
- package/dist/components/password-strength.js +2 -8
- package/dist/components/password-strength.js.map +1 -1
- package/dist/components/phone-input.js +42 -57
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +1 -8
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +16 -9
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.js +1 -8
- package/dist/components/qr-code.js.map +1 -1
- package/dist/components/row-actions.js +5 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/select.js +28 -38
- package/dist/components/select.js.map +1 -1
- package/dist/components/separator.js +1 -5
- package/dist/components/separator.js.map +1 -1
- package/dist/components/series-chart-axes.js +4 -1
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.js +29 -45
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.js +21 -17
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.js +4 -9
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.js +4 -13
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/signed-amount.js +12 -5
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/skeleton.js +2 -17
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +13 -36
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -10
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.js +14 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +1 -1
- package/dist/components/swipeable-row.js +1 -8
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table-skeleton.js +4 -1
- package/dist/components/table-skeleton.js.map +1 -1
- package/dist/components/table.js +12 -9
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.js +5 -24
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +1 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +6 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +2 -11
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.js +2 -22
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -4
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -1
- package/dist/components/translation-review-parts.js +7 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/translation-review.js +18 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +14 -10
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.js +1 -13
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/use-table-state.js +14 -11
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.js +3 -26
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/demo/demo-start.js +8 -4
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-labels.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +1 -8
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-capture.d.ts +1 -1
- package/dist/feedback/feedback-capture.js +3 -1
- package/dist/feedback/feedback-capture.js.map +1 -1
- package/dist/feedback/feedback-comment-thread.js +27 -13
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-context.js +1 -4
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-public-list.js +7 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-record.js +1 -5
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.js +25 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +3 -1
- package/dist/feedback/feedback-table.js +22 -5
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +2 -13
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback/feedback-vote-button.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-document-title.js.map +1 -1
- package/dist/hooks/use-file-drop.js.map +1 -1
- package/dist/hooks/use-file-picker-labels.js.map +1 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +10 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.d.ts +1 -1
- package/dist/hooks/use-row-swipe.js +1 -6
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.js +1 -4
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +15 -4
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.js +2 -8
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.js.map +1 -1
- package/dist/i18n/locales/en.js +19 -4
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +15 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +15 -4
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +15 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +15 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +11 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.js +15 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +17 -17
- package/dist/index.js +40 -23
- package/dist/index.js.map +1 -1
- package/dist/landing/access.js +1 -4
- package/dist/landing/access.js.map +1 -1
- package/dist/landing/landing-sections.js +35 -38
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/device-settings.js +2 -1
- package/dist/lib/device-settings.js.map +1 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/iban.js.map +1 -1
- package/dist/lib/logger.js +1 -5
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/person-name.js.map +1 -1
- package/dist/lib/phone.js.map +1 -1
- package/dist/lib/qr-encode.js +344 -8
- package/dist/lib/qr-encode.js.map +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/table-text.js +15 -3
- package/dist/lib/table-text.js.map +1 -1
- package/dist/lib/text-match.d.ts +20 -0
- package/dist/lib/text-match.js +11 -1
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/notification-inbox.js +63 -44
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +6 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/use-push-device.js +4 -1
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/pwa-update-prompt.d.ts +1 -1
- package/dist/offline/pwa-update-prompt.js +7 -1
- package/dist/offline/pwa-update-prompt.js.map +1 -1
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +6 -1
- package/dist/offline.js.map +1 -1
- package/dist/pay/pay.css +8 -1
- package/dist/rhf/fields.d.ts +1 -2
- package/dist/rhf/fields.js +1 -10
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.js +1 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/search/command-palette.js +71 -62
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +34 -5
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/settings/device-reset-setting.js +1 -6
- package/dist/settings/device-reset-setting.js.map +1 -1
- package/dist/settings/settings-heading.js.map +1 -1
- package/dist/settings/settings-layout.js +9 -20
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/use-settings-focus.d.ts +7 -0
- package/dist/settings/use-settings-focus.js +2 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.js +62 -31
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -10
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +5 -0
- package/dist/shell/dev-menu.js +14 -7
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/sync-status-indicator.js +7 -6
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +14 -1
- package/dist/shell/topbar-action-menu.js +85 -34
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +1 -10
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.js +9 -1
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +2 -1
- package/dist/shell.js.map +1 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.js +24 -6
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/react.js.map +1 -1
- package/dist/testing/viewport.js +7 -4
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/contrast-tokens.js +13 -2
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/palette-derive.d.ts +1 -1
- package/dist/theme/palette-derive.js +18 -29
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +1 -3
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.js +5 -1
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-engine.js +1 -4
- package/dist/tour/tour-engine.js.map +1 -1
- package/dist/tour/tour.js +21 -12
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.js +3 -24
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +24 -28
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +1 -1
- package/dist/wizard/wizard-step.js +1 -8
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +5 -2
- package/src/account/account-settings-labels.ts +1 -2
- package/src/account/account-settings.tsx +11 -17
- package/src/account/delete-account-setting.tsx +1 -6
- package/src/account/email-change-setting-labels.ts +1 -2
- package/src/account/email-change-setting.tsx +3 -1
- package/src/account/passkeys-setting.tsx +1 -3
- package/src/account/sessions-setting.tsx +4 -2
- package/src/admin/admin-action-confirm.tsx +4 -7
- package/src/admin/admin-action-log.tsx +29 -29
- package/src/admin/admin-area-labels.ts +4 -1
- package/src/admin/admin-overview.tsx +8 -2
- package/src/admin/admin-sections.tsx +3 -1
- package/src/admin/admin-tool-links.tsx +7 -1
- package/src/admin/invitations-panel.tsx +5 -11
- package/src/admin/mail-test-card.tsx +1 -2
- package/src/admin/reviewer-scope-editor.tsx +4 -9
- package/src/admin/role-select.tsx +4 -2
- package/src/admin/user-roster.tsx +2 -11
- package/src/auth/accept-invitation.tsx +1 -7
- package/src/auth/auth-errors.ts +1 -6
- package/src/auth/forgot-password-form.tsx +2 -7
- package/src/auth/register-form.tsx +7 -8
- package/src/auth/reset-password-form.tsx +2 -5
- package/src/auth/sign-in-form.tsx +15 -14
- package/src/auth/verify-email.tsx +2 -7
- package/src/billing/billing-banner.tsx +3 -7
- package/src/billing/billing-labels.ts +6 -4
- package/src/billing/checkout-processing.ts +0 -1
- package/src/billing/plan-card.tsx +9 -8
- package/src/billing/plan-limit-toast.tsx +1 -2
- package/src/billing/plan-limit.ts +1 -2
- package/src/billing/plan-price.ts +1 -4
- package/src/billing/plan-switch.tsx +1 -2
- package/src/billing/subscription-actions.tsx +5 -2
- package/src/components/account-chips.tsx +1 -7
- package/src/components/alert-banner.tsx +2 -8
- package/src/components/amount-input.tsx +206 -178
- package/src/components/authed-image.tsx +7 -4
- package/src/components/autocomplete.tsx +7 -9
- package/src/components/bulk-action-bar.tsx +2 -7
- package/src/components/button-group.tsx +1 -2
- package/src/components/button-look.ts +6 -17
- package/src/components/button.tsx +1 -4
- package/src/components/calculator.tsx +6 -9
- package/src/components/calendar-heatmap.tsx +5 -10
- package/src/components/card.tsx +9 -13
- package/src/components/chart-zoom-core.tsx +6 -33
- package/src/components/chart.tsx +8 -31
- package/src/components/checkbox-group.tsx +1 -4
- package/src/components/chip-input.tsx +6 -6
- package/src/components/chip.tsx +14 -26
- package/src/components/choice-card.tsx +4 -18
- package/src/components/column-mapper-labels.ts +3 -10
- package/src/components/column-mapper.tsx +33 -122
- package/src/components/combobox-core.tsx +17 -47
- package/src/components/combobox.tsx +2 -6
- package/src/components/confirm-dialog.tsx +1 -3
- package/src/components/copy-button.tsx +2 -4
- package/src/components/country-select.tsx +9 -21
- package/src/components/currency-option-list.tsx +16 -18
- package/src/components/currency-select.tsx +2 -7
- package/src/components/danger-confirm.tsx +8 -9
- package/src/components/data-table-cells.tsx +1 -2
- package/src/components/data-table-column-settings.tsx +1 -3
- package/src/components/data-table-filter-popover.tsx +5 -19
- package/src/components/data-table-filters.ts +1 -6
- package/src/components/data-table-fit.ts +7 -7
- package/src/components/data-table-groups.tsx +3 -1
- package/src/components/data-table-head.tsx +32 -46
- package/src/components/data-table-mobile.tsx +99 -108
- package/src/components/data-table-pagination.tsx +2 -6
- package/src/components/data-table-row-actions.tsx +25 -24
- package/src/components/data-table-row.tsx +16 -17
- package/src/components/data-table-shared.tsx +10 -16
- package/src/components/data-table-sort.ts +1 -2
- package/src/components/data-table-virtual.ts +9 -4
- package/src/components/data-table.tsx +497 -501
- package/src/components/date-picker.tsx +1 -7
- package/src/components/date-range-picker.tsx +3 -13
- package/src/components/description-list.tsx +7 -13
- package/src/components/dialog-frame.tsx +12 -3
- package/src/components/disclosure.tsx +8 -12
- package/src/components/dropdown.tsx +3 -11
- package/src/components/empty-state.tsx +1 -3
- package/src/components/entity-combobox.tsx +2 -6
- package/src/components/error-boundary.tsx +7 -7
- package/src/components/field-base.tsx +8 -14
- package/src/components/field-parts.tsx +12 -23
- package/src/components/field-sync.tsx +7 -11
- package/src/components/file-button.tsx +7 -3
- package/src/components/file-dropzone.tsx +7 -12
- package/src/components/floating-panel.tsx +44 -51
- package/src/components/focus-ring.ts +1 -2
- package/src/components/form-actions.tsx +1 -4
- package/src/components/grouped-picker.tsx +21 -16
- package/src/components/hover-menu.tsx +1 -4
- package/src/components/iban-input-labels.ts +1 -2
- package/src/components/icon-button.tsx +46 -48
- package/src/components/icon-picker.tsx +9 -20
- package/src/components/image-grid.tsx +1 -6
- package/src/components/inline-edit-field.tsx +1 -5
- package/src/components/inline-entity-combobox.tsx +11 -25
- package/src/components/input.tsx +14 -2
- package/src/components/jump-calendar.tsx +2 -6
- package/src/components/legal-labels.ts +8 -3
- package/src/components/legal-page.tsx +56 -6
- package/src/components/legal.tsx +83 -10
- package/src/components/lightbox.tsx +1 -3
- package/src/components/line-items.tsx +7 -15
- package/src/components/list-labels.ts +2 -0
- package/src/components/list.tsx +81 -27
- package/src/components/measured-grid.tsx +8 -30
- package/src/components/menu-item.tsx +1 -3
- package/src/components/mini-calendar.tsx +129 -159
- package/src/components/money-field.tsx +4 -2
- package/src/components/month-picker.tsx +14 -21
- package/src/components/multi-entity-combobox.tsx +6 -26
- package/src/components/multi-select.tsx +5 -9
- package/src/components/nav-pills.tsx +10 -2
- package/src/components/number-field.tsx +18 -15
- package/src/components/number-input.tsx +3 -19
- package/src/components/numpad-sheet.tsx +9 -13
- package/src/components/one-time-code-input.tsx +6 -11
- package/src/components/page-contents.tsx +2 -9
- package/src/components/page-header.tsx +5 -10
- package/src/components/password-strength-labels.ts +1 -2
- package/src/components/password-strength.tsx +6 -9
- package/src/components/phone-input.tsx +2 -13
- package/src/components/picker-sheet.tsx +2 -5
- package/src/components/pie-chart.tsx +6 -12
- package/src/components/popover.tsx +2 -1
- package/src/components/progress-bar.tsx +16 -25
- package/src/components/qr-code.tsx +2 -9
- package/src/components/row-actions.tsx +9 -1
- package/src/components/search-field.tsx +4 -5
- package/src/components/select.tsx +2 -10
- package/src/components/separator.tsx +1 -5
- package/src/components/series-chart-axes.ts +5 -5
- package/src/components/series-chart-budget.ts +1 -4
- package/src/components/series-chart-ticks.ts +1 -4
- package/src/components/series-chart.tsx +43 -83
- package/src/components/settings-fields.tsx +20 -15
- package/src/components/share-card.tsx +6 -14
- package/src/components/sign-chip.tsx +16 -16
- package/src/components/signature-pad.tsx +21 -13
- package/src/components/signed-amount.tsx +6 -14
- package/src/components/skeleton.tsx +2 -13
- package/src/components/slider.tsx +9 -20
- package/src/components/sparkline.tsx +2 -13
- package/src/components/stat-tile.tsx +13 -24
- package/src/components/swatch-picker.tsx +8 -3
- package/src/components/swipeable-row.tsx +2 -11
- package/src/components/table-skeleton.tsx +4 -1
- package/src/components/table.tsx +12 -8
- package/src/components/tabs.tsx +24 -39
- package/src/components/text-link.tsx +6 -6
- package/src/components/text.tsx +1 -2
- package/src/components/tile-radio.tsx +6 -19
- package/src/components/time-input.tsx +4 -5
- package/src/components/toast.tsx +3 -12
- package/src/components/toggle-group.tsx +9 -9
- package/src/components/toggle-legend.tsx +4 -16
- package/src/components/tooltip.tsx +9 -48
- package/src/components/translation-review-editor.tsx +4 -4
- package/src/components/translation-review-parts.tsx +7 -1
- package/src/components/translation-review.tsx +15 -16
- package/src/components/tree-view.tsx +17 -20
- package/src/components/treemap.tsx +3 -15
- package/src/components/trigger-aria.ts +1 -4
- package/src/components/use-table-state.ts +16 -17
- package/src/components/user-avatar.tsx +4 -21
- package/src/demo/demo-start.tsx +8 -4
- package/src/feedback/feedback-attachment-labels.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +16 -29
- package/src/feedback/feedback-capture.ts +3 -1
- package/src/feedback/feedback-comment-thread.tsx +9 -6
- package/src/feedback/feedback-context.tsx +1 -4
- package/src/feedback/feedback-dialog.tsx +3 -4
- package/src/feedback/feedback-inbox.tsx +3 -9
- package/src/feedback/feedback-public-list.tsx +7 -1
- package/src/feedback/feedback-record.ts +6 -14
- package/src/feedback/feedback-replies.tsx +10 -5
- package/src/feedback/feedback-row-detail.tsx +2 -5
- package/src/feedback/feedback-status-undo.ts +1 -4
- package/src/feedback/feedback-submit-dialog.tsx +4 -4
- package/src/feedback/feedback-table.tsx +29 -9
- package/src/feedback/feedback-thread.tsx +4 -16
- package/src/feedback/feedback-vocabulary.ts +72 -82
- package/src/feedback/feedback-vote-button.tsx +9 -8
- package/src/hooks/use-anchored-rect.ts +1 -4
- package/src/hooks/use-document-title.ts +3 -1
- package/src/hooks/use-file-drop.ts +1 -3
- package/src/hooks/use-file-picker-labels.ts +1 -2
- package/src/hooks/use-file-picker.tsx +1 -5
- package/src/hooks/use-focus-trap.ts +1 -2
- package/src/hooks/use-overlay-history.ts +10 -5
- package/src/hooks/use-row-swipe.ts +1 -6
- package/src/i18n/defaults.ts +1 -4
- package/src/i18n/german.ts +42 -50
- package/src/i18n/kit-labels.tsx +5 -20
- package/src/i18n/languages.ts +2 -8
- package/src/i18n/locales/en.ts +15 -31
- package/src/i18n/locales/es.ts +44 -50
- package/src/i18n/locales/fr.ts +43 -52
- package/src/i18n/locales/hu.ts +35 -38
- package/src/i18n/locales/it.ts +44 -53
- package/src/i18n/locales/zh.ts +23 -33
- package/src/i18n/plurals.ts +4 -1
- package/src/i18n/review.ts +20 -7
- package/src/i18n/shared-labels.ts +1 -2
- package/src/index.ts +107 -32
- package/src/landing/access.ts +2 -7
- package/src/landing/landing-sections.tsx +14 -15
- package/src/landing/page-seo.ts +1 -3
- package/src/lib/calc.ts +4 -1
- package/src/lib/clipboard-minus.ts +4 -1
- package/src/lib/dates.ts +2 -10
- package/src/lib/decimal-marks.ts +1 -3
- package/src/lib/device-settings.ts +2 -1
- package/src/lib/direction.ts +1 -3
- package/src/lib/iban.ts +89 -9
- package/src/lib/logger.ts +1 -5
- package/src/lib/person-name.ts +1 -3
- package/src/lib/phone.ts +5 -8
- package/src/lib/qr-encode.ts +32 -8
- package/src/lib/session-refresh.ts +5 -6
- package/src/lib/table-text.ts +24 -6
- package/src/lib/text-match.ts +43 -1
- package/src/lib/translation-review.ts +1 -3
- package/src/notifications/notification-inbox.tsx +17 -4
- package/src/notifications/notification-parts.tsx +1 -2
- package/src/notifications/notification-types-setting.tsx +19 -18
- package/src/notifications/notifications-labels.ts +1 -2
- package/src/notifications/push-device-setting.tsx +7 -1
- package/src/notifications/push-devices-setting.tsx +7 -3
- package/src/notifications/push-worker.ts +10 -2
- package/src/notifications/quiet-hours-setting.tsx +4 -1
- package/src/notifications/use-push-device.ts +4 -1
- package/src/offline/pwa-update-prompt.tsx +7 -1
- package/src/offline/query-persistence.ts +9 -6
- package/src/offline.ts +6 -1
- package/src/rhf/fields.tsx +50 -19
- package/src/rhf/form-core.tsx +2 -6
- package/src/rhf/line-items.tsx +5 -7
- package/src/rhf/use-rhf-wizard-step.ts +3 -12
- package/src/search/command-palette.tsx +15 -23
- package/src/search/global-search.tsx +38 -8
- package/src/search/search-index.ts +5 -1
- package/src/settings/device-reset-setting.tsx +5 -8
- package/src/settings/settings-heading.tsx +3 -1
- package/src/settings/settings-layout.tsx +6 -13
- package/src/settings/use-settings-focus.ts +8 -0
- package/src/settings/use-settings-route.ts +4 -3
- package/src/shell/app-shell.tsx +48 -24
- package/src/shell/auth-layout.tsx +1 -6
- package/src/shell/dev-menu.tsx +18 -6
- package/src/shell/option-switcher-menu.tsx +1 -3
- package/src/shell/sync-status-indicator.tsx +14 -12
- package/src/shell/top-bar-parts-labels.ts +1 -3
- package/src/shell/topbar-action-menu.tsx +130 -45
- package/src/shell/topbar-controls.tsx +5 -19
- package/src/shell/tour-menu.tsx +9 -1
- package/src/shell.ts +1 -1
- package/src/sync/db.ts +5 -1
- package/src/sync/engine.ts +44 -12
- package/src/sync.ts +16 -2
- package/src/testing/css-variables.ts +1 -2
- package/src/testing/index.ts +1 -6
- package/src/testing/react.ts +1 -2
- package/src/testing/viewport.ts +9 -9
- package/src/theme/contrast-tokens.ts +13 -2
- package/src/theme/palette-derive.ts +24 -50
- package/src/theme/palette-presets.ts +0 -1
- package/src/theme/palette-store.ts +1 -4
- package/src/theme/text-size.ts +1 -1
- package/src/theme/theme-store.ts +2 -6
- package/src/tour/tour-catalogue.tsx +4 -2
- package/src/tour/tour-dock.ts +5 -1
- package/src/tour/tour-engine.ts +2 -6
- package/src/tour/tour.tsx +28 -12
- package/src/tour/use-tour-progress.ts +8 -2
- package/src/tour/use-tour-url-sync.ts +3 -1
- package/src/vite-env.d.ts +1 -4
- package/src/wizard/stepper-nav.tsx +10 -35
- package/src/wizard/types.ts +1 -4
- package/src/wizard/use-wizard.ts +28 -43
- package/src/wizard/validation.ts +1 -5
- package/src/wizard/wizard-context.tsx +2 -6
- package/src/wizard/wizard-step.tsx +1 -8
- package/src/wizard/wizard-summary.tsx +2 -6
- package/tokens.css +393 -152
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/topbar-action-menu.tsx"],"sourcesContent":["import { useId, useLayoutEffect, useRef } from \"react\";\nimport type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu, type HoverMenuProps } from \"../components/hover-menu\";\nimport { MenuItem } from \"../components/menu-item\";\nimport type { MenuItemTone } from \"../components/menu-item\";\nimport { StatusDot } from \"../components/status-dot\";\nimport { Tooltip } from \"../components/tooltip\";\nimport type { UserAvatarBadge } from \"../components/user-avatar\";\nimport { TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\n\n/**\n * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a\n * `link` navigates via react-router, a `divider` draws a rule, and a `heading` names\n * the rows after it. Action and link rows close the menu automatically when chosen.\n * `kind` may be omitted for actions, keeping the common case terse.\n *\n * The rows are {@link MenuItem}s, so they take its options: `tone: \"danger\"` for the\n * log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the\n * page already open, `disabled`.\n */\nexport type TopBarMenuEntry =\n | {\n kind?: \"action\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n onSelect: () => void;\n tone?: MenuItemTone;\n /** Makes the row a choice (`menuitemradio`, or `menuitemcheckbox` with\n * `checkable: \"checkbox\"`) and says whether it is chosen. */\n checked?: boolean;\n checkable?: \"radio\" | \"checkbox\";\n disabled?: boolean;\n }\n | {\n kind: \"link\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n to: string;\n tone?: MenuItemTone;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n disabled?: boolean;\n }\n | { kind: \"divider\"; key: string }\n | {\n /**\n * A group heading (0.23, keksdose's guided-tours menu): the rows after it, up to\n * the next heading, the next `divider` or the end of `entries`, are ONE group\n * named by `label`. keksdose files its tours by topic and separated the blocks\n * with bare dividers — \"the kit's menu has no heading row to name them with\"\n * (tour-menu.tsx) — so a sighted reader had to guess what each block was and a\n * screen reader was told nothing at all.\n *\n * Not a row: it takes no focus, the arrow keys (and Home/End) pass over it, and it\n * never closes the menu. It is announced the way the kit's other grouped lists\n * announce theirs (`CommandPalette`): the group's rows sit in a `role=\"group\"`\n * whose `aria-labelledby` is this heading, so a reader says \"On this page, group\"\n * when the focus enters the first row, and nothing between rows.\n *\n * A divider ENDS the group, so a closing row such as \"All tours…\" behind a rule\n * belongs to no topic. A heading with no rows under it is not drawn — an empty\n * group names nothing; build the list from data and it may well be empty.\n *\n * The menu-wide `heading` prop is the panel's title, drawn once above everything;\n * with group headings it is usually redundant (two stacked caps lines read as one),\n * and the trigger's `aria-label` already names the menu.\n */\n kind: \"heading\";\n key: string;\n /** The group's name, shown above its rows and announced as the group's name. Keep\n * it short: it is set in small caps and wraps on a narrow panel. */\n label: ReactNode;\n };\n\ntype HeadingEntry = Extract<TopBarMenuEntry, { kind: \"heading\" }>;\ntype DividerEntry = Extract<TopBarMenuEntry, { kind: \"divider\" }>;\ntype RowEntry = Exclude<TopBarMenuEntry, HeadingEntry | DividerEntry>;\n\n/** What the panel draws from `entries`: loose rows and rules, and headed groups. */\ntype Block = { kind: \"loose\"; entry: RowEntry | DividerEntry } | { kind: \"group\"; heading: HeadingEntry; rows: RowEntry[] };\n\n/**\n * Cut `entries` into blocks: a heading opens a group that takes the rows after it until\n * the next heading or divider; everything else stands alone, as it always has.\n */\nfunction toBlocks(entries: readonly TopBarMenuEntry[]): Block[] {\n const blocks: Block[] = [];\n let group: Extract<Block, { kind: \"group\" }> | null = null;\n for (const entry of entries) {\n if (entry.kind === \"heading\") {\n group = { kind: \"group\", heading: entry, rows: [] };\n blocks.push(group);\n } else if (entry.kind === \"divider\") {\n group = null;\n blocks.push({ kind: \"loose\", entry });\n } else if (group) {\n group.rows.push(entry);\n } else {\n blocks.push({ kind: \"loose\", entry });\n }\n }\n // A heading with nothing under it names nothing: not drawn (see the `heading` kind).\n return blocks.filter((block) => block.kind === \"loose\" || block.rows.length > 0);\n}\n\n/**\n * The non-interactive identity block at the top of an account menu — kastlan's name +\n * role, keksdose's display name + email + role chip. It is text, not a `menuitem`: the\n * arrow keys pass over it.\n */\nexport interface TopBarMenuHeader {\n /** The first line — the user's name. */\n title: ReactNode;\n /** The muted second line — an email, a role. */\n subtitle?: ReactNode;\n /**\n * Anything below — a role chip. A link or button here stays out of the arrow keys\n * and in the Tab order, like the footer's.\n *\n * Pass a function to get `close`, as `footer` does — keksdose F7: its admin role\n * chip links to the admin area, and a router link does not unmount the menu, so\n * without `close` the panel stayed open over the page it had just navigated to.\n */\n extra?: ReactNode | ((close: () => void) => ReactNode);\n}\n\n/** What a control is, as far as `HoverMenu` is concerned, when it lives in the header\n * or the footer: not a menu row. */\nconst SECTION_CONTROLS = \"a[href]:not([role]),button:not([role])\";\nconst TABBABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]';\n\n/**\n * `scrollable`'s cap (0.51, DevMenu): the glass under the bar. Under `TopBar` the panel\n * starts about 50px down — the `size-9` trigger centred in the `h-12` bar ends at 42px,\n * and `HoverMenu` hangs the panel `pt-2` below it — so 4rem (64px) leaves some 14px clear\n * under the last row. `dvh`, not `vh`: on a phone `100vh` is the viewport with the\n * browser's toolbars put away, taller than the glass while they show.\n * `overscroll-contain`, so a fling past the last row does not scroll the page behind.\n */\nconst SCROLLABLE_PANEL = \"max-h-[calc(100dvh-4rem)] overflow-y-auto overscroll-contain\";\n\n/**\n * A header or footer inside the menu panel.\n *\n * `HoverMenu` promotes EVERY role-less button and link in its panel to a `menuitem` out\n * of the Tab order, and turns every list into `role=\"none\"` — right for the rows, wrong\n * for a footer of legal links, which would join the arrow-key cycle and drop out of Tab.\n * So before it looks (a child's layout effect runs before its parent's), the controls\n * in here get their own role spelled out, which `HoverMenu` leaves alone, and any list\n * keeps its list semantics.\n */\nfunction MenuSection({ className, children }: { className: string; children: ReactNode }) {\n const ref = useRef<HTMLLIElement>(null);\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n for (const c of el.querySelectorAll<HTMLElement>(SECTION_CONTROLS)) {\n c.setAttribute(\"role\", c.tagName === \"A\" ? \"link\" : \"button\");\n }\n for (const l of el.querySelectorAll<HTMLElement>(\"ul:not([role]),ol:not([role])\")) l.setAttribute(\"role\", \"list\");\n for (const l of el.querySelectorAll<HTMLElement>(\"li:not([role])\")) l.setAttribute(\"role\", \"listitem\");\n });\n return (\n <li ref={ref} role=\"none\" className={className}>\n {children}\n </li>\n );\n}\n\n/**\n * `HoverMenu` closes on any Tab inside the panel — right when Tab leaves it, wrong when\n * Tab is on its way from a row to a footer link, or from one footer link to the next:\n * the panel would unmount under the focus. So a Tab whose next stop is still inside the\n * panel stops here and the browser moves focus; one that leaves reaches `HoverMenu`,\n * which closes.\n */\nfunction keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {\n if (e.key !== \"Tab\") return;\n const from = e.target as HTMLElement;\n const stops = Array.from(e.currentTarget.querySelectorAll<HTMLElement>(TABBABLE)).filter(\n (el) => el.getAttribute(\"tabindex\") !== \"-1\",\n );\n const way = e.shiftKey ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING;\n if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();\n}\n\n/**\n * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,\n * with an optional uppercase heading and divider rules. Generic glue over the\n * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —\n * so those call sites describe their rows as data instead of rebuilding the\n * trigger + list markup. For a checkmarked value-switcher use\n * {@link OptionSwitcherMenu} instead.\n *\n * It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a\n * custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a\n * free-content `footer` (the legal links).\n *\n * A long menu is grouped by topic with `{ kind: \"heading\" }` entries (0.23) — see\n * {@link TopBarMenuEntry}. There is no separate phone layout: the panel is the same\n * hover panel at 390px, kept inside the viewport by `HoverMenu`, and a heading wraps\n * rather than widening it. A menu that can grow taller than a phone's glass passes\n * `scrollable` (0.51), since that clamp is sideways only.\n */\nexport function TopBarActionMenu({\n icon,\n iconBadge,\n trigger,\n triggerClassName,\n href,\n \"aria-label\": ariaLabel,\n heading,\n header,\n entries,\n leading,\n children,\n footer,\n footerLabel,\n align,\n panelClassName,\n scrollable = false,\n triggerAttributes,\n}: {\n /** The trigger's face for the usual icon button. Ignored when `trigger` is given. */\n icon?: ReactNode;\n /**\n * A status dot in the top-end corner of `icon` — keksdose's unread-support dot on the\n * feedback trigger. Like `UserAvatar`'s `badge`, its `label` joins the trigger's name\n * (\"Send feedback 2 unread\"), so the dot is not something only the eye is told about.\n * Ignored with `trigger`: give the face its own badge there.\n */\n iconBadge?: UserAvatarBadge | null;\n /**\n * A face of your own for the trigger — the account menu's `UserAvatar`. It is drawn\n * INSIDE the kit's button, which keeps the wiring: `aria-haspopup`, `aria-expanded`,\n * the toggle, the keyboard. `open` is there for a chevron that turns.\n *\n * The button is named by `aria-label` followed by any readable text in the face — a\n * `UserAvatar` `badge` label reaches a screen reader that way (\"Account menu 3\n * unread\"), where an `aria-label` on the button would have hidden it. A\n * `UserAvatar`'s initials are `aria-hidden`, so the person's name is only in the\n * name if `aria-label` carries it (\"Marcel Eifert, account menu\").\n */\n trigger?: (state: { open: boolean }) => ReactNode;\n /** Extra classes for the trigger button — `rounded-full` round an avatar. */\n triggerClassName?: string;\n /**\n * Make the trigger a LINK to this route (keksdose dev #585, Marcel: \"clicking on the\n * guided tours icon shall lead to the tours page like the all tours element does\").\n * A click or Enter follows it and shuts the panel; hover still opens the panel after\n * the usual delay, and ↓/↑ on the focused trigger still open it onto the first/last\n * row, so the rows stay reachable by keyboard. A tap on a touch screen follows the\n * link — so use it where the destination offers everything the menu does. Rendered as\n * an `<a>` (the router's `Link`) with `aria-haspopup=\"menu\"` and `aria-expanded`,\n * named by `aria-label`: a link inside the kit's button would be interactive content\n * inside interactive content.\n */\n href?: string;\n /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit\n * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */\n \"aria-label\": string;\n heading?: string;\n /** A non-interactive identity block above the rows. */\n header?: TopBarMenuHeader;\n entries: TopBarMenuEntry[];\n /**\n * Rows the entry data cannot describe (a `Disclosure` sub-list, an app's own\n * section), after `entries`. Render `<li>`s holding `MenuItem`s or plain controls;\n * `close` shuts the menu.\n */\n children?: (close: () => void) => ReactNode;\n /**\n * Rows like {@link children}, but BEFORE `entries`, under the header — the account\n * menu's `AccountMenuLanguage`, which goes first after the app's context rows (§6.5)\n * in a menu whose other rows are all entries (kastlan, build review B1). Render `<li>`s;\n * `close` shuts the menu.\n */\n leading?: (close: () => void) => ReactNode;\n /**\n * Free content below the rows, behind a rule — the legal links. Its links and\n * buttons are NOT menu rows: the arrow keys skip them and Tab reaches them. Pass a\n * function to get `close` for the links' `onClick`.\n */\n footer?: ReactNode | ((close: () => void) => ReactNode);\n /** Wraps the footer in a `<nav>` with this name (\"Legal\"). Without it, a plain block. */\n footerLabel?: string;\n /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading\n * direction (see `HoverMenu`, whose type this is). Default `end`. */\n align?: HoverMenuProps[\"align\"];\n panelClassName?: string;\n /**\n * Cap the panel at the glass under the top bar and scroll the rows inside it (0.51,\n * `DevMenu`). `HoverMenu` keeps a panel inside the viewport sideways only, so a tall\n * one ran off the bottom of the screen: DevMenu's ~25 rows on a 390×844 phone, with\n * the last ones out of reach — the page cannot scroll them up, the panel hangs from\n * the sticky bar.\n *\n * Off by default: a menu that fits keeps its natural height and has no scroll\n * container (`TourMenu` keeps its own `max-h-[70vh]`). The keyboard needs nothing\n * extra: `HoverMenu`'s roving focus is a plain `focus()`, which the browser follows by\n * scrolling the panel to the row, so ↓, ↑, Home and End reach a row below the cap. The\n * header and the footer are inside the list and scroll with the rows.\n */\n scrollable?: boolean;\n /** `data-*` attributes for the trigger itself — a tour's anchor (`data-tour`), a test\n * hook. The parts built on this menu (`TourMenu`) pass theirs through here. */\n triggerAttributes?: { [key: `data-${string}`]: string | number | boolean | undefined };\n}) {\n // Names the groups' headings for `aria-labelledby`. Positional rather than from the\n // entry's `key`, which is the caller's text and may hold a space — and a space in\n // `aria-labelledby` is a list separator.\n const uid = useId();\n return (\n <HoverMenu\n aria-label={ariaLabel}\n align={align}\n trigger={({ open, toggle, close }) => {\n // One element for both kinds: the router's link when `href` is set, else the\n // kit's button. Same classes and face either way.\n //\n // Its name in the kit's Tooltip on hover and focus (0.48, keksdose dev#610): an\n // icon alone told a sighted user nothing the screen reader was told. In a\n // fragment, as IconButton does it, so the bubble is not the trigger's\n // `aria-describedby` — the name it repeats is the trigger's own, and a reader\n // would hear it twice. Portalled, like the top bar's other triggers.\n const control = (className: string, children: ReactNode, label?: string) => (\n <Tooltip label={ariaLabel} portal>\n <>\n {href ? (\n <Link {...triggerAttributes} to={href} onClick={close} aria-label={label} className={className}>\n {children}\n </Link>\n ) : (\n <button {...triggerAttributes} type=\"button\" onClick={toggle} aria-label={label} className={className}>\n {children}\n </button>\n )}\n </>\n </Tooltip>\n );\n if (trigger) {\n return control(\n cn(TOPBAR_TRIGGER_CLASS, \"relative\", triggerClassName),\n <>\n <span className=\"sr-only\">{ariaLabel}</span>\n {/* The space keeps the name from running into the face's text (\"Account\n menu3 unread\"); as a flex item it takes no room. */}\n {\" \"}\n {trigger({ open })}\n </>,\n );\n }\n if (iconBadge) {\n // Named by its content, like the custom-trigger branch, so the badge label is\n // part of the name; an `aria-label` would have hidden it.\n return control(\n cn(TOPBAR_TRIGGER_CLASS, \"relative\", triggerClassName),\n <>\n <span className=\"sr-only\">{ariaLabel}</span>{\" \"}\n <span aria-hidden className=\"relative inline-flex\">\n {icon}\n <StatusDot ring tone={iconBadge.tone ?? \"danger\"} className=\"absolute -end-0.5 -top-0.5\" />\n </span>{\" \"}\n <span className=\"sr-only\">{iconBadge.label}</span>\n </>,\n );\n }\n return control(cn(TOPBAR_TRIGGER_CLASS, triggerClassName), icon, ariaLabel);\n }}\n >\n {(close) => {\n const footerBody = typeof footer === \"function\" ? footer(close) : footer;\n const headerExtra = typeof header?.extra === \"function\" ? header.extra(close) : header?.extra;\n const footerClass =\n \"flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted [&_a:hover]:text-primary [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-focus [&_a:focus-visible]:ring-brand\";\n /** One action or link row — loose, or inside a heading's group. */\n const row = (entry: RowEntry) => (\n <li key={entry.key}>\n {entry.kind === \"link\" ? (\n <MenuItem\n href={entry.to}\n renderLink={({ href, ...p }) => <Link to={href} {...p} />}\n onClick={close}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n current={entry.current}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n ) : (\n <MenuItem\n onClick={() => {\n entry.onSelect();\n close();\n }}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n checked={entry.checked}\n checkable={entry.checkable}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n )}\n </li>\n );\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel\n <ul className={cn(\"py-1\", scrollable && SCROLLABLE_PANEL, panelClassName)} onKeyDown={keepTabInsidePanel}>\n {header && (\n <MenuSection className=\"mb-1 border-b border-subtle px-3 py-2\">\n <div className=\"truncate text-sm font-medium text-primary\">{header.title}</div>\n {header.subtitle != null && (\n <div className=\"truncate text-xs text-muted\">{header.subtitle}</div>\n )}\n {headerExtra != null && headerExtra !== false && <div className=\"mt-1\">{headerExtra}</div>}\n </MenuSection>\n )}\n {heading && (\n <li className=\"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\">\n {heading}\n </li>\n )}\n {leading?.(close)}\n {toBlocks(entries).map((block, i, blocks) => {\n if (block.kind === \"loose\") {\n const entry = block.entry;\n if (entry.kind === \"divider\") {\n return <li key={entry.key} className=\"my-1 border-t border-subtle\" />;\n }\n return row(entry);\n }\n const headingId = `${uid}-group-${i}`;\n const before = blocks[i - 1];\n // Under a rule (or at the top of the rows) the rule's margin is the gap;\n // straight under another group's last row the heading makes its own.\n const tight = !before || (before.kind === \"loose\" && before.entry.kind === \"divider\");\n return (\n <li key={block.heading.key} role=\"none\">\n <div\n id={headingId}\n role=\"presentation\"\n className={cn(\n \"px-3 pb-0.5 text-caption font-semibold uppercase tracking-wide text-muted [overflow-wrap:anywhere]\",\n tight ? \"pt-1\" : \"pt-2.5\",\n )}\n >\n {block.heading.label}\n </div>\n <ul role=\"group\" aria-labelledby={headingId}>\n {block.rows.map(row)}\n </ul>\n </li>\n );\n })}\n {children?.(close)}\n {footerBody != null && footerBody !== false && (\n <MenuSection className=\"mt-1 border-t border-subtle px-3 py-2\">\n {footerLabel ? (\n <nav aria-label={footerLabel} className={footerClass}>\n {footerBody}\n </nav>\n ) : (\n <div className={footerClass}>{footerBody}</div>\n )}\n </MenuSection>\n )}\n </ul>\n );\n }}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":["children","href"]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/topbar-action-menu.tsx"],"sourcesContent":["import { useId, useLayoutEffect, useRef } from \"react\";\nimport type { CSSProperties, KeyboardEvent as ReactKeyboardEvent, ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu, type HoverMenuProps } from \"../components/hover-menu\";\nimport { MenuItem } from \"../components/menu-item\";\nimport type { MenuItemTone } from \"../components/menu-item\";\nimport { StatusDot } from \"../components/status-dot\";\nimport { Tooltip } from \"../components/tooltip\";\nimport type { UserAvatarBadge } from \"../components/user-avatar\";\nimport { TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\n\n/**\n * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a\n * `link` navigates via react-router, a `divider` draws a rule, and a `heading` names\n * the rows after it. Action and link rows close the menu automatically when chosen.\n * `kind` may be omitted for actions, keeping the common case terse.\n *\n * The rows are {@link MenuItem}s, so they take its options: `tone: \"danger\"` for the\n * log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the\n * page already open, `disabled`.\n */\nexport type TopBarMenuEntry =\n | {\n kind?: \"action\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n onSelect: () => void;\n tone?: MenuItemTone;\n /** Makes the row a choice (`menuitemradio`, or `menuitemcheckbox` with\n * `checkable: \"checkbox\"`) and says whether it is chosen. */\n checked?: boolean;\n checkable?: \"radio\" | \"checkbox\";\n disabled?: boolean;\n }\n | {\n kind: \"link\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n to: string;\n tone?: MenuItemTone;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n disabled?: boolean;\n }\n | { kind: \"divider\"; key: string }\n | {\n /**\n * A group heading (0.23, keksdose's guided-tours menu): the rows after it, up to\n * the next heading, the next `divider` or the end of `entries`, are ONE group\n * named by `label`. keksdose files its tours by topic and separated the blocks\n * with bare dividers — \"the kit's menu has no heading row to name them with\"\n * (tour-menu.tsx) — so a sighted reader had to guess what each block was and a\n * screen reader was told nothing at all.\n *\n * Not a row: it takes no focus, the arrow keys (and Home/End) pass over it, and it\n * never closes the menu. It is announced the way the kit's other grouped lists\n * announce theirs (`CommandPalette`): the group's rows sit in a `role=\"group\"`\n * whose `aria-labelledby` is this heading, so a reader says \"On this page, group\"\n * when the focus enters the first row, and nothing between rows.\n *\n * A divider ENDS the group, so a closing row such as \"All tours…\" behind a rule\n * belongs to no topic. A heading with no rows under it is not drawn — an empty\n * group names nothing; build the list from data and it may well be empty.\n *\n * The menu-wide `heading` prop is the panel's title, drawn once above everything;\n * with group headings it is usually redundant (two stacked caps lines read as one),\n * and the trigger's `aria-label` already names the menu.\n */\n kind: \"heading\";\n key: string;\n /** The group's name, shown above its rows and announced as the group's name. Keep\n * it short: it is set in small caps and wraps on a narrow panel. */\n label: ReactNode;\n };\n\ntype HeadingEntry = Extract<TopBarMenuEntry, { kind: \"heading\" }>;\ntype DividerEntry = Extract<TopBarMenuEntry, { kind: \"divider\" }>;\ntype RowEntry = Exclude<TopBarMenuEntry, HeadingEntry | DividerEntry>;\n\n/** What the panel draws from `entries`: loose rows and rules, and headed groups. */\ntype Block =\n { kind: \"loose\"; entry: RowEntry | DividerEntry } | { kind: \"group\"; heading: HeadingEntry; rows: RowEntry[] };\n\n/**\n * Cut `entries` into blocks: a heading opens a group that takes the rows after it until\n * the next heading or divider; everything else stands alone, as it always has.\n */\nfunction toBlocks(entries: readonly TopBarMenuEntry[]): Block[] {\n const blocks: Block[] = [];\n let group: Extract<Block, { kind: \"group\" }> | null = null;\n for (const entry of entries) {\n if (entry.kind === \"heading\") {\n group = { kind: \"group\", heading: entry, rows: [] };\n blocks.push(group);\n } else if (entry.kind === \"divider\") {\n group = null;\n blocks.push({ kind: \"loose\", entry });\n } else if (group) {\n group.rows.push(entry);\n } else {\n blocks.push({ kind: \"loose\", entry });\n }\n }\n // A heading with nothing under it names nothing: not drawn (see the `heading` kind).\n return blocks.filter((block) => block.kind === \"loose\" || block.rows.length > 0);\n}\n\n/** One column of `columns` (0.51.4): a group and the loose entries after it, up to the\n * next group — so a group's own way out behind a rule (DevMenu's \"Back to {role}\") stays\n * under it instead of running across the panel. */\ntype LooseBlock = Extract<Block, { kind: \"loose\" }>;\ntype Column = { group: Extract<Block, { kind: \"group\" }>; after: Array<RowEntry | DividerEntry>; index: number };\n\n/** `blocks` as the columns layout draws them: what comes before the first group, then a\n * column per group. A rule that would end a column is dropped: the column's edge is the\n * separation there. */\nfunction toColumns(blocks: readonly Block[]): { lead: LooseBlock[]; columns: Column[] } {\n const lead: LooseBlock[] = [];\n const columns: Column[] = [];\n blocks.forEach((block, index) => {\n if (block.kind === \"group\") columns.push({ group: block, after: [], index });\n else if (columns.length === 0) lead.push(block);\n else columns[columns.length - 1]!.after.push(block.entry);\n });\n for (const column of columns) {\n while (column.after.at(-1)?.kind === \"divider\") column.after.pop();\n }\n return { lead, columns };\n}\n\n/**\n * `columns`' panel: each column 14rem, the panel as wide as its columns (a CSS variable\n * the list sets) but never wider than the glass, and from `md` up only. The grid places\n * as many 14rem columns as that width holds, so a narrow desktop wraps the groups onto a\n * second row instead of scrolling sideways. The header, the title and the footer span\n * the row.\n */\nconst COLUMNS_PANEL =\n \"md:grid md:grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] md:items-start md:gap-y-2 md:w-[min(calc(100vw-2rem),var(--menu-columns-width))]\";\nconst SPAN_ALL = \"md:col-span-full\";\n\n/**\n * The non-interactive identity block at the top of an account menu — kastlan's name +\n * role, keksdose's display name + email + role chip. It is text, not a `menuitem`: the\n * arrow keys pass over it.\n */\nexport interface TopBarMenuHeader {\n /** The first line — the user's name. */\n title: ReactNode;\n /** The muted second line — an email, a role. */\n subtitle?: ReactNode;\n /**\n * Anything below — a role chip. A link or button here stays out of the arrow keys\n * and in the Tab order, like the footer's.\n *\n * Pass a function to get `close`, as `footer` does — keksdose F7: its admin role\n * chip links to the admin area, and a router link does not unmount the menu, so\n * without `close` the panel stayed open over the page it had just navigated to.\n */\n extra?: ReactNode | ((close: () => void) => ReactNode);\n}\n\n/** What a control is, as far as `HoverMenu` is concerned, when it lives in the header\n * or the footer: not a menu row. */\nconst SECTION_CONTROLS = \"a[href]:not([role]),button:not([role])\";\nconst TABBABLE =\n \"a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]\";\n\n/**\n * `scrollable`'s cap (0.51, DevMenu): the glass under the bar. Under `TopBar` the panel\n * starts about 50px down — the `size-9` trigger centred in the `h-12` bar ends at 42px,\n * and `HoverMenu` hangs the panel `pt-2` below it — so 4rem (64px) leaves some 14px clear\n * under the last row. `dvh`, not `vh`: on a phone `100vh` is the viewport with the\n * browser's toolbars put away, taller than the glass while they show.\n * `overscroll-contain`, so a fling past the last row does not scroll the page behind.\n */\nconst SCROLLABLE_PANEL = \"max-h-[calc(100dvh-4rem)] overflow-y-auto overscroll-contain\";\n\n/**\n * A header or footer inside the menu panel.\n *\n * `HoverMenu` promotes EVERY role-less button and link in its panel to a `menuitem` out\n * of the Tab order, and turns every list into `role=\"none\"` — right for the rows, wrong\n * for a footer of legal links, which would join the arrow-key cycle and drop out of Tab.\n * So before it looks (a child's layout effect runs before its parent's), the controls\n * in here get their own role spelled out, which `HoverMenu` leaves alone, and any list\n * keeps its list semantics.\n */\nfunction MenuSection({ className, children }: { className: string; children: ReactNode }) {\n const ref = useRef<HTMLLIElement>(null);\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n for (const c of el.querySelectorAll<HTMLElement>(SECTION_CONTROLS)) {\n c.setAttribute(\"role\", c.tagName === \"A\" ? \"link\" : \"button\");\n }\n for (const l of el.querySelectorAll<HTMLElement>(\"ul:not([role]),ol:not([role])\")) l.setAttribute(\"role\", \"list\");\n for (const l of el.querySelectorAll<HTMLElement>(\"li:not([role])\")) l.setAttribute(\"role\", \"listitem\");\n });\n return (\n <li ref={ref} role=\"none\" className={className}>\n {children}\n </li>\n );\n}\n\n/**\n * `HoverMenu` closes on any Tab inside the panel — right when Tab leaves it, wrong when\n * Tab is on its way from a row to a footer link, or from one footer link to the next:\n * the panel would unmount under the focus. So a Tab whose next stop is still inside the\n * panel stops here and the browser moves focus; one that leaves reaches `HoverMenu`,\n * which closes.\n */\nfunction keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {\n if (e.key !== \"Tab\") return;\n const from = e.target as HTMLElement;\n const stops = Array.from(e.currentTarget.querySelectorAll<HTMLElement>(TABBABLE)).filter(\n (el) => el.getAttribute(\"tabindex\") !== \"-1\",\n );\n const way = e.shiftKey ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING;\n if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();\n}\n\n/**\n * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,\n * with an optional uppercase heading and divider rules. Generic glue over the\n * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —\n * so those call sites describe their rows as data instead of rebuilding the\n * trigger + list markup. For a checkmarked value-switcher use\n * {@link OptionSwitcherMenu} instead.\n *\n * It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a\n * custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a\n * free-content `footer` (the legal links).\n *\n * A long menu is grouped by topic with `{ kind: \"heading\" }` entries (0.23) — see\n * {@link TopBarMenuEntry}. There is no separate phone layout: the panel is the same\n * hover panel at 390px, kept inside the viewport by `HoverMenu`, and a heading wraps\n * rather than widening it. A menu that can grow taller than a phone's glass passes\n * `scrollable` (0.51), since that clamp is sideways only.\n */\nexport function TopBarActionMenu({\n icon,\n iconBadge,\n trigger,\n triggerClassName,\n href,\n \"aria-label\": ariaLabel,\n heading,\n header,\n entries,\n leading,\n children,\n footer,\n footerLabel,\n align,\n panelClassName,\n scrollable = false,\n columns = false,\n triggerAttributes,\n}: {\n /** The trigger's face for the usual icon button. Ignored when `trigger` is given. */\n icon?: ReactNode;\n /**\n * A status dot in the top-end corner of `icon` — keksdose's unread-support dot on the\n * feedback trigger. Like `UserAvatar`'s `badge`, its `label` joins the trigger's name\n * (\"Send feedback 2 unread\"), so the dot is not something only the eye is told about.\n * Ignored with `trigger`: give the face its own badge there.\n */\n iconBadge?: UserAvatarBadge | null;\n /**\n * A face of your own for the trigger — the account menu's `UserAvatar`. It is drawn\n * INSIDE the kit's button, which keeps the wiring: `aria-haspopup`, `aria-expanded`,\n * the toggle, the keyboard. `open` is there for a chevron that turns.\n *\n * The button is named by `aria-label` followed by any readable text in the face — a\n * `UserAvatar` `badge` label reaches a screen reader that way (\"Account menu 3\n * unread\"), where an `aria-label` on the button would have hidden it. A\n * `UserAvatar`'s initials are `aria-hidden`, so the person's name is only in the\n * name if `aria-label` carries it (\"Marcel Eifert, account menu\").\n */\n trigger?: (state: { open: boolean }) => ReactNode;\n /** Extra classes for the trigger button — `rounded-full` round an avatar. */\n triggerClassName?: string;\n /**\n * Make the trigger a LINK to this route (keksdose dev #585, Marcel: \"clicking on the\n * guided tours icon shall lead to the tours page like the all tours element does\").\n * A click or Enter follows it and shuts the panel; hover still opens the panel after\n * the usual delay, and ↓/↑ on the focused trigger still open it onto the first/last\n * row, so the rows stay reachable by keyboard. A tap on a touch screen follows the\n * link — so use it where the destination offers everything the menu does. Rendered as\n * an `<a>` (the router's `Link`) with `aria-haspopup=\"menu\"` and `aria-expanded`,\n * named by `aria-label`: a link inside the kit's button would be interactive content\n * inside interactive content.\n */\n href?: string;\n /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit\n * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */\n \"aria-label\": string;\n heading?: string;\n /** A non-interactive identity block above the rows. */\n header?: TopBarMenuHeader;\n entries: TopBarMenuEntry[];\n /**\n * Rows the entry data cannot describe (a `Disclosure` sub-list, an app's own\n * section), after `entries`. Render `<li>`s holding `MenuItem`s or plain controls;\n * `close` shuts the menu.\n */\n children?: (close: () => void) => ReactNode;\n /**\n * Rows like {@link children}, but BEFORE `entries`, under the header — the account\n * menu's `AccountMenuLanguage`, which goes first after the app's context rows (§6.5)\n * in a menu whose other rows are all entries (kastlan, build review B1). Render `<li>`s;\n * `close` shuts the menu.\n */\n leading?: (close: () => void) => ReactNode;\n /**\n * Free content below the rows, behind a rule — the legal links. Its links and\n * buttons are NOT menu rows: the arrow keys skip them and Tab reaches them. Pass a\n * function to get `close` for the links' `onClick`.\n */\n footer?: ReactNode | ((close: () => void) => ReactNode);\n /** Wraps the footer in a `<nav>` with this name (\"Legal\"). Without it, a plain block. */\n footerLabel?: string;\n /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading\n * direction (see `HoverMenu`, whose type this is). Default `end`. */\n align?: HoverMenuProps[\"align\"];\n panelClassName?: string;\n /**\n * Cap the panel at the glass under the top bar and scroll the rows inside it (0.51,\n * `DevMenu`). `HoverMenu` keeps a panel inside the viewport sideways only, so a tall\n * one ran off the bottom of the screen: DevMenu's ~25 rows on a 390×844 phone, with\n * the last ones out of reach — the page cannot scroll them up, the panel hangs from\n * the sticky bar.\n *\n * Off by default: a menu that fits keeps its natural height and has no scroll\n * container (`TourMenu` keeps its own `max-h-[70vh]`). The keyboard needs nothing\n * extra: `HoverMenu`'s roving focus is a plain `focus()`, which the browser follows by\n * scrolling the panel to the row, so ↓, ↑, Home and End reach a row below the cap. The\n * header and the footer are inside the list and scroll with the rows.\n */\n scrollable?: boolean;\n /**\n * From `md` up, lay the heading groups side by side, one column per group (0.51.4,\n * `DevMenu`; keksdose dev #640, Marcel: \"Each group gets a column. That way it would be\n * much better visible for desktop users\"). Below `md`, the one column as before.\n *\n * A column is a group and the loose rows after it up to the next heading, so a row\n * behind a rule at the end of a group (DevMenu's \"Back to {role}\") stays in that\n * group's column. Loose rows before the first heading, the header, the menu-wide\n * `heading` and the footer span the panel. The DOM order is unchanged, so ↓/↑ walk the\n * rows down one column and on into the next, as a reader reads them. `leading` and\n * `children` rows take a cell each; add `md:col-span-full` to one that should span.\n */\n columns?: boolean;\n /** `data-*` attributes for the trigger itself — a tour's anchor (`data-tour`), a test\n * hook. The parts built on this menu (`TourMenu`) pass theirs through here. */\n triggerAttributes?: { [key: `data-${string}`]: string | number | boolean | undefined };\n}) {\n // Names the groups' headings for `aria-labelledby`. Positional rather than from the\n // entry's `key`, which is the caller's text and may hold a space — and a space in\n // `aria-labelledby` is a list separator.\n const uid = useId();\n const blocks = toBlocks(entries);\n const grouped = columns ? toColumns(blocks) : null;\n // A menu with no group has nothing to put side by side: the list as always.\n const laidOut = grouped && grouped.columns.length > 0 ? grouped : null;\n return (\n <HoverMenu\n aria-label={ariaLabel}\n align={align}\n trigger={({ open, toggle, close }) => {\n // One element for both kinds: the router's link when `href` is set, else the\n // kit's button. Same classes and face either way.\n //\n // Its name in the kit's Tooltip on hover and focus (0.48, keksdose dev#610): an\n // icon alone told a sighted user nothing the screen reader was told. In a\n // fragment, as IconButton does it, so the bubble is not the trigger's\n // `aria-describedby` — the name it repeats is the trigger's own, and a reader\n // would hear it twice. Portalled, like the top bar's other triggers.\n const control = (className: string, children: ReactNode, label?: string) => (\n <Tooltip label={ariaLabel} portal>\n <>\n {href ? (\n <Link {...triggerAttributes} to={href} onClick={close} aria-label={label} className={className}>\n {children}\n </Link>\n ) : (\n <button {...triggerAttributes} type=\"button\" onClick={toggle} aria-label={label} className={className}>\n {children}\n </button>\n )}\n </>\n </Tooltip>\n );\n if (trigger) {\n return control(\n cn(TOPBAR_TRIGGER_CLASS, \"relative\", triggerClassName),\n <>\n <span className=\"sr-only\">{ariaLabel}</span>\n {/* The space keeps the name from running into the face's text (\"Account\n menu3 unread\"); as a flex item it takes no room. */}{\" \"}\n {trigger({ open })}\n </>,\n );\n }\n if (iconBadge) {\n // Named by its content, like the custom-trigger branch, so the badge label is\n // part of the name; an `aria-label` would have hidden it.\n return control(\n cn(TOPBAR_TRIGGER_CLASS, \"relative\", triggerClassName),\n <>\n <span className=\"sr-only\">{ariaLabel}</span>{\" \"}\n <span aria-hidden className=\"relative inline-flex\">\n {icon}\n <StatusDot ring tone={iconBadge.tone ?? \"danger\"} className=\"absolute -end-0.5 -top-0.5\" />\n </span>{\" \"}\n <span className=\"sr-only\">{iconBadge.label}</span>\n </>,\n );\n }\n return control(cn(TOPBAR_TRIGGER_CLASS, triggerClassName), icon, ariaLabel);\n }}\n >\n {(close) => {\n const footerBody = typeof footer === \"function\" ? footer(close) : footer;\n const headerExtra = typeof header?.extra === \"function\" ? header.extra(close) : header?.extra;\n const footerClass =\n \"flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted [&_a:hover]:text-primary [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-focus [&_a:focus-visible]:ring-brand\";\n /** One action or link row — loose, or inside a heading's group. */\n const row = (entry: RowEntry, className?: string) => (\n <li key={entry.key} className={className}>\n {entry.kind === \"link\" ? (\n <MenuItem\n href={entry.to}\n renderLink={({ href, ...p }) => <Link to={href} {...p} />}\n onClick={close}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n current={entry.current}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n ) : (\n <MenuItem\n onClick={() => {\n entry.onSelect();\n close();\n }}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n checked={entry.checked}\n checkable={entry.checkable}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n )}\n </li>\n );\n /** A rule or a row outside any group; `span` stretches it across the columns. */\n const loose = (block: LooseBlock, span: boolean) => {\n const entry = block.entry;\n if (entry.kind === \"divider\") {\n return <li key={entry.key} className={cn(\"my-1 border-t border-subtle\", span && SPAN_ALL)} />;\n }\n return row(entry, span ? SPAN_ALL : undefined);\n };\n /** A heading's group: its name, then its rows in a named `role=\"group\"`. */\n const group = (block: Extract<Block, { kind: \"group\" }>, i: number, tight: boolean, inColumn = false) => {\n const headingId = `${uid}-group-${i}`;\n return (\n <li key={block.heading.key} role=\"none\">\n <div\n id={headingId}\n role=\"presentation\"\n className={cn(\n \"px-3 pb-0.5 text-caption font-semibold uppercase tracking-wide text-muted [overflow-wrap:anywhere]\",\n // A column's heading lines up with its neighbours' at the top of the\n // grid row; under `md` the columns stack, and it spaces like the list's.\n tight ? \"pt-1\" : cn(\"pt-2.5\", inColumn && \"md:pt-1\"),\n )}\n >\n {block.heading.label}\n </div>\n <ul role=\"group\" aria-labelledby={headingId}>\n {block.rows.map((entry) => row(entry))}\n </ul>\n </li>\n );\n };\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel\n <ul\n className={cn(\"py-1\", scrollable && SCROLLABLE_PANEL, laidOut && COLUMNS_PANEL, panelClassName)}\n style={\n laidOut ? ({ \"--menu-columns-width\": `${laidOut.columns.length * 14}rem` } as CSSProperties) : undefined\n }\n onKeyDown={keepTabInsidePanel}\n >\n {header && (\n <MenuSection className={cn(\"mb-1 border-b border-subtle px-3 py-2\", laidOut && SPAN_ALL)}>\n <div className=\"truncate text-sm font-medium text-primary\">{header.title}</div>\n {header.subtitle != null && <div className=\"truncate text-xs text-muted\">{header.subtitle}</div>}\n {headerExtra != null && headerExtra !== false && <div className=\"mt-1\">{headerExtra}</div>}\n </MenuSection>\n )}\n {heading && (\n <li\n className={cn(\n \"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\",\n laidOut && SPAN_ALL,\n )}\n >\n {heading}\n </li>\n )}\n {leading?.(close)}\n {laidOut ? (\n <>\n {laidOut.lead.map((block) => loose(block, true))}\n {laidOut.columns.map((column, c) => (\n // A column of the grid; under `md` a group as in the list, its rows\n // following.\n <li key={column.group.heading.key} role=\"none\" data-menu-column=\"\" className=\"min-w-0\">\n <ul role=\"none\">\n {group(column.group, column.index, c === 0 && laidOut.lead.length === 0, true)}\n {column.after.map((entry) => loose({ kind: \"loose\", entry }, false))}\n </ul>\n </li>\n ))}\n </>\n ) : (\n blocks.map((block, i) => {\n if (block.kind === \"loose\") return loose(block, false);\n const before = blocks[i - 1];\n // Under a rule (or at the top of the rows) the rule's margin is the gap;\n // straight under another group's last row the heading makes its own.\n return group(block, i, !before || (before.kind === \"loose\" && before.entry.kind === \"divider\"));\n })\n )}\n {children?.(close)}\n {footerBody != null && footerBody !== false && (\n <MenuSection className={cn(\"mt-1 border-t border-subtle px-3 py-2\", laidOut && SPAN_ALL)}>\n {footerLabel ? (\n <nav aria-label={footerLabel} className={footerClass}>\n {footerBody}\n </nav>\n ) : (\n <div className={footerClass}>{footerBody}</div>\n )}\n </MenuSection>\n )}\n </ul>\n );\n }}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":["children","href"]}
|
|
@@ -51,16 +51,7 @@ function PaletteMenu({
|
|
|
51
51
|
HoverMenu,
|
|
52
52
|
{
|
|
53
53
|
"aria-label": labels.palette,
|
|
54
|
-
trigger: ({ toggle }) => /* @__PURE__ */ jsx(Tooltip, { label: activeName, portal: true, children: /* @__PURE__ */ jsx(
|
|
55
|
-
"button",
|
|
56
|
-
{
|
|
57
|
-
type: "button",
|
|
58
|
-
onClick: toggle,
|
|
59
|
-
"aria-label": labels.palette,
|
|
60
|
-
className: TOPBAR_TRIGGER_CLASS,
|
|
61
|
-
children: /* @__PURE__ */ jsx(Palette, { className: "size-5 text-[var(--chart-1)]" })
|
|
62
|
-
}
|
|
63
|
-
) }),
|
|
54
|
+
trigger: ({ toggle }) => /* @__PURE__ */ jsx(Tooltip, { label: activeName, portal: true, children: /* @__PURE__ */ jsx("button", { type: "button", onClick: toggle, "aria-label": labels.palette, className: TOPBAR_TRIGGER_CLASS, children: /* @__PURE__ */ jsx(Palette, { className: "size-5 text-[var(--chart-1)]" }) }) }),
|
|
64
55
|
children: (close) => /* @__PURE__ */ jsxs("ul", { className: "w-60 py-1", children: [
|
|
65
56
|
heading && /* @__PURE__ */ jsx(MenuHeading, { children: heading }),
|
|
66
57
|
palettes.map((p) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/topbar-controls.tsx"],"sourcesContent":["import { Check, Globe, Moon, Palette, Sun } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport type { PalettePreset } from \"../theme/palette-presets\";\nimport type { ThemeMode } from \"../theme/theme-store\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { languageCodeLabel } from \"./language-code\";\n\n/** Shared square icon-button styling for top-bar triggers, so app-owned controls\n * (account/feedback menus) line up with the shared ones. */\nexport const TOPBAR_TRIGGER_CLASS =\n \"size-9 rounded-md flex items-center justify-center text-secondary hover:text-primary hover:bg-hover\";\n\n/** Shared row styling for the top-bar hover-menu items. `text-start`, so a label that\n * wraps aligns to the reading edge in RTL too. */\nexport const TOPBAR_MENU_ITEM_CLASS =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-secondary hover:bg-hover\";\n\n/** The small uppercase heading row the top-bar menus share (`heading`). */\nfunction MenuHeading({ children }: { children: string }) {\n return (\n <li className=\"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\">\n {children}\n </li>\n );\n}\n\n/**\n * Sun/Moon toggle for the light/dark theme. Controlled: the app owns the mode, and the\n * toggle keeps no storage key of its own (docs/top-bar-harmonization.md §6.4).\n *\n * `mode` is the theme ON SCREEN — the resolved one, so with a \"system\" preference it is\n * whatever the OS gives. A tap asks for the opposite of that: `onChange(\"light\")` while\n * dark is shown, `onChange(\"dark\")` while light is. The app writes it as its theme\n * preference (an explicit light or dark), where Settings → Appearance keeps the three-way\n * choice. `onToggle` is the older way to hear the tap, without the next mode; both fire.\n */\nexport function ThemeToggle({\n mode,\n onToggle,\n onChange,\n \"aria-label\": ariaLabel,\n title,\n className,\n}: {\n /** The resolved light/dark on screen (`useThemeStore`'s `mode`, not its preference). */\n mode: ThemeMode;\n /** Hears a tap. Kept for 0.41's callers; `onChange` says what to switch to. */\n onToggle?: () => void;\n /** 0.42: the mode to switch to — always the opposite of `mode`. */\n onChange?: (next: ThemeMode) => void;\n /** Default: `topBar.theme` from the {@link UiKitProvider}, else English. */\n \"aria-label\"?: string;\n title?: string;\n className?: string;\n}) {\n // This had no default at all, so a toggle rendered without `aria-label` was an\n // icon button with no name — a reader heard \"button\" and nothing else.\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });\n const Icon = mode === \"dark\" ? Moon : Sun;\n const button = (\n <button\n type=\"button\"\n onClick={() => {\n onChange?.(mode === \"dark\" ? \"light\" : \"dark\");\n onToggle?.();\n }}\n aria-label={labels.theme}\n className={cn(TOPBAR_TRIGGER_CLASS, className)}\n >\n <Icon className=\"size-5\" />\n </button>\n );\n // The name on hover and focus even without a `title` (0.48, keksdose dev#610); a bubble\n // that only repeats it is visual, in a fragment as on IconButton — see OptionSwitcherMenu.\n const tip = title ?? labels.theme;\n return (\n <Tooltip label={tip} portal>\n {tip === labels.theme ? <>{button}</> : button}\n </Tooltip>\n );\n}\n\n/** Appearance-preset switcher: a hover menu of palette presets with per-theme\n * colour swatches. Controlled — the app owns the active id and applies it. */\nexport function PaletteMenu({\n palettes,\n activeId,\n mode,\n onSelect,\n \"aria-label\": ariaLabel,\n heading,\n}: {\n palettes: PalettePreset[];\n activeId: string;\n mode: ThemeMode;\n onSelect: (id: string) => void;\n /** Default: `topBar.palette` from the {@link UiKitProvider}, else English. */\n \"aria-label\"?: string;\n /** Optional small heading row above the list. */\n heading?: string;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabel });\n const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;\n return (\n <HoverMenu\n aria-label={labels.palette}\n trigger={({ toggle }) => (\n <Tooltip label={activeName} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={labels.palette}\n className={TOPBAR_TRIGGER_CLASS}\n >\n {/* A DATA colour, not a semantic one: the palette mark is the\n \"colour\" control, so it carries a colour, and which hue is\n arbitrary. `--chart-1` is the violet end of the CVD-safe\n categorical ramp and flips per theme — a fixed `violet-500` did\n not, and stayed a cool violet island on a warm page. */}\n <Palette className=\"size-5 text-[var(--chart-1)]\" />\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"w-60 py-1\">\n {heading && <MenuHeading>{heading}</MenuHeading>}\n {palettes.map((p) => {\n const ts = p[mode];\n const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];\n return (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(p.id);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex shrink-0 gap-0.5\">\n {/* Deliberately a neutral at 10% and not a token: this hairline\n sits on an ARBITRARY preset colour, so it has to read against\n whatever that preset paints, which no themed border does. */}\n {swatches.map((c, i) => (\n <span\n key={i}\n className=\"size-3.5 rounded-sm ring-1 ring-inset ring-black/10 dark:ring-white/10\"\n style={{ background: c }}\n />\n ))}\n </span>\n <span className=\"flex min-w-0 flex-col text-start\">\n <span className=\"truncate\">{p.name}</span>\n <span className=\"truncate text-caption text-placeholder\">\n {p.blurb}\n </span>\n </span>\n </span>\n {activeId === p.id && (\n <Check className=\"size-4 shrink-0 text-secondary\" />\n )}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n\nexport interface LanguageOption {\n code: string;\n label: string;\n /** ISO 3166-1 alpha-2 country code (lowercase) for the flag-icons flag. */\n country: string;\n}\n\n/** What {@link LanguageMenu}'s trigger shows of the active language (0.49 KIT-10). */\nexport type LanguageMenuTrigger = \"flag+code\" | \"code\" | \"flag\";\n\n/** The trigger with a code: as tall as the others, as wide as flag and code need. */\nconst LANGUAGE_TRIGGER_WIDE_CLASS = TOPBAR_TRIGGER_CLASS.replace(\"size-9\", \"h-9 min-w-9 gap-1.5 px-2\");\n\nconst FLAG_CLASS =\n \"inline-block h-[0.9375rem] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]\";\n\n/** Language switcher: a hover menu of options with flags and a check on the\n * active one. Controlled — the app owns the current code and change handler.\n *\n * The trigger is the *active* option's flag and its language code in capitals\n * (\"DE\", \"FR\", \"IT\"; `trigger`, 0.49 KIT-10), not an icon: lucide's `Languages`\n * glyph (文A) is the same mark Chrome puts in the omnibox for \"translate this\n * page\", so it read as machine-translation rather than \"pick the UI language\"\n * (Keksdose feedback #121). A flag alone could not tell de-CH, fr-CH and it-CH\n * apart — one Swiss flag each — so the code is beside it. It is named \"Choose\n * language, current: Deutsch\" (`topBar.languageCurrent`). A globe covers the\n * case where `current` matches nothing in `options`. */\nexport function LanguageMenu({\n options,\n current,\n onChange,\n \"aria-label\": ariaLabel,\n heading,\n trigger = \"flag+code\",\n}: {\n options: LanguageOption[];\n current: string | undefined;\n onChange: (code: string) => void;\n /** The menu's name, and the trigger's as given. Default: the menu `topBar.language`,\n * the trigger `topBar.languageCurrent(<the active label>)`, from the\n * {@link UiKitProvider}, else English. */\n \"aria-label\"?: string;\n /** Optional small heading row above the list — as on `TopBarActionMenu`,\n * `OptionSwitcherMenu` and `PaletteMenu`. */\n heading?: string;\n /** What the trigger shows: `\"flag+code\"` (default), `\"code\"` or `\"flag\"`. */\n trigger?: LanguageMenuTrigger;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });\n const active = options.find((o) => o.code === current);\n const triggerName = ariaLabel ?? (active ? labels.languageCurrent(active.label) : labels.language);\n const showsCode = active !== undefined && trigger !== \"flag\";\n return (\n <HoverMenu\n aria-label={labels.language}\n trigger={({ toggle }) => (\n <Tooltip label={active?.label ?? labels.language} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={triggerName}\n className={showsCode ? LANGUAGE_TRIGGER_WIDE_CLASS : TOPBAR_TRIGGER_CLASS}\n >\n {active ? (\n <>\n {trigger !== \"code\" && <span aria-hidden className={`fi fi-${active.country} ${FLAG_CLASS}`} />}\n {showsCode && (\n <span aria-hidden className=\"text-xs font-semibold tabular-nums tracking-wide\">\n {languageCodeLabel(active.code)}\n </span>\n )}\n </>\n ) : (\n <Globe className=\"size-5\" />\n )}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {heading && <MenuHeading>{heading}</MenuHeading>}\n {options.map((lang) => {\n const isActive = lang.code === current;\n return (\n <li key={lang.code}>\n <button\n type=\"button\"\n onClick={() => {\n onChange(lang.code);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex items-center gap-2\">\n <span aria-hidden className={`fi fi-${lang.country} ${FLAG_CLASS}`} />\n {lang.label}\n </span>\n {isActive && <Check className=\"size-4 text-secondary\" />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/topbar-controls.tsx"],"sourcesContent":["import { Check, Globe, Moon, Palette, Sun } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport type { PalettePreset } from \"../theme/palette-presets\";\nimport type { ThemeMode } from \"../theme/theme-store\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { languageCodeLabel } from \"./language-code\";\n\n/** Shared square icon-button styling for top-bar triggers, so app-owned controls\n * (account/feedback menus) line up with the shared ones. */\nexport const TOPBAR_TRIGGER_CLASS =\n \"size-9 rounded-md flex items-center justify-center text-secondary hover:text-primary hover:bg-hover\";\n\n/** Shared row styling for the top-bar hover-menu items. `text-start`, so a label that\n * wraps aligns to the reading edge in RTL too. */\nexport const TOPBAR_MENU_ITEM_CLASS =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-secondary hover:bg-hover\";\n\n/** The small uppercase heading row the top-bar menus share (`heading`). */\nfunction MenuHeading({ children }: { children: string }) {\n return <li className=\"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\">{children}</li>;\n}\n\n/**\n * Sun/Moon toggle for the light/dark theme. Controlled: the app owns the mode, and the\n * toggle keeps no storage key of its own (docs/top-bar-harmonization.md §6.4).\n *\n * `mode` is the theme ON SCREEN — the resolved one, so with a \"system\" preference it is\n * whatever the OS gives. A tap asks for the opposite of that: `onChange(\"light\")` while\n * dark is shown, `onChange(\"dark\")` while light is. The app writes it as its theme\n * preference (an explicit light or dark), where Settings → Appearance keeps the three-way\n * choice. `onToggle` is the older way to hear the tap, without the next mode; both fire.\n */\nexport function ThemeToggle({\n mode,\n onToggle,\n onChange,\n \"aria-label\": ariaLabel,\n title,\n className,\n}: {\n /** The resolved light/dark on screen (`useThemeStore`'s `mode`, not its preference). */\n mode: ThemeMode;\n /** Hears a tap. Kept for 0.41's callers; `onChange` says what to switch to. */\n onToggle?: () => void;\n /** 0.42: the mode to switch to — always the opposite of `mode`. */\n onChange?: (next: ThemeMode) => void;\n /** Default: `topBar.theme` from the {@link UiKitProvider}, else English. */\n \"aria-label\"?: string;\n title?: string;\n className?: string;\n}) {\n // This had no default at all, so a toggle rendered without `aria-label` was an\n // icon button with no name — a reader heard \"button\" and nothing else.\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });\n const Icon = mode === \"dark\" ? Moon : Sun;\n const button = (\n <button\n type=\"button\"\n onClick={() => {\n onChange?.(mode === \"dark\" ? \"light\" : \"dark\");\n onToggle?.();\n }}\n aria-label={labels.theme}\n className={cn(TOPBAR_TRIGGER_CLASS, className)}\n >\n <Icon className=\"size-5\" />\n </button>\n );\n // The name on hover and focus even without a `title` (0.48, keksdose dev#610); a bubble\n // that only repeats it is visual, in a fragment as on IconButton — see OptionSwitcherMenu.\n const tip = title ?? labels.theme;\n return (\n <Tooltip label={tip} portal>\n {tip === labels.theme ? <>{button}</> : button}\n </Tooltip>\n );\n}\n\n/** Appearance-preset switcher: a hover menu of palette presets with per-theme\n * colour swatches. Controlled — the app owns the active id and applies it. */\nexport function PaletteMenu({\n palettes,\n activeId,\n mode,\n onSelect,\n \"aria-label\": ariaLabel,\n heading,\n}: {\n palettes: PalettePreset[];\n activeId: string;\n mode: ThemeMode;\n onSelect: (id: string) => void;\n /** Default: `topBar.palette` from the {@link UiKitProvider}, else English. */\n \"aria-label\"?: string;\n /** Optional small heading row above the list. */\n heading?: string;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabel });\n const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;\n return (\n <HoverMenu\n aria-label={labels.palette}\n trigger={({ toggle }) => (\n <Tooltip label={activeName} portal>\n <button type=\"button\" onClick={toggle} aria-label={labels.palette} className={TOPBAR_TRIGGER_CLASS}>\n {/* A DATA colour, not a semantic one: the palette mark is the\n \"colour\" control, so it carries a colour, and which hue is\n arbitrary. `--chart-1` is the violet end of the CVD-safe\n categorical ramp and flips per theme — a fixed `violet-500` did\n not, and stayed a cool violet island on a warm page. */}\n <Palette className=\"size-5 text-[var(--chart-1)]\" />\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"w-60 py-1\">\n {heading && <MenuHeading>{heading}</MenuHeading>}\n {palettes.map((p) => {\n const ts = p[mode];\n const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];\n return (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(p.id);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex shrink-0 gap-0.5\">\n {/* Deliberately a neutral at 10% and not a token: this hairline\n sits on an ARBITRARY preset colour, so it has to read against\n whatever that preset paints, which no themed border does. */}\n {swatches.map((c, i) => (\n <span\n key={i}\n className=\"size-3.5 rounded-sm ring-1 ring-inset ring-black/10 dark:ring-white/10\"\n style={{ background: c }}\n />\n ))}\n </span>\n <span className=\"flex min-w-0 flex-col text-start\">\n <span className=\"truncate\">{p.name}</span>\n <span className=\"truncate text-caption text-placeholder\">{p.blurb}</span>\n </span>\n </span>\n {activeId === p.id && <Check className=\"size-4 shrink-0 text-secondary\" />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n\nexport interface LanguageOption {\n code: string;\n label: string;\n /** ISO 3166-1 alpha-2 country code (lowercase) for the flag-icons flag. */\n country: string;\n}\n\n/** What {@link LanguageMenu}'s trigger shows of the active language (0.49 KIT-10). */\nexport type LanguageMenuTrigger = \"flag+code\" | \"code\" | \"flag\";\n\n/** The trigger with a code: as tall as the others, as wide as flag and code need. */\nconst LANGUAGE_TRIGGER_WIDE_CLASS = TOPBAR_TRIGGER_CLASS.replace(\"size-9\", \"h-9 min-w-9 gap-1.5 px-2\");\n\nconst FLAG_CLASS = \"inline-block h-[0.9375rem] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]\";\n\n/** Language switcher: a hover menu of options with flags and a check on the\n * active one. Controlled — the app owns the current code and change handler.\n *\n * The trigger is the *active* option's flag and its language code in capitals\n * (\"DE\", \"FR\", \"IT\"; `trigger`, 0.49 KIT-10), not an icon: lucide's `Languages`\n * glyph (文A) is the same mark Chrome puts in the omnibox for \"translate this\n * page\", so it read as machine-translation rather than \"pick the UI language\"\n * (Keksdose feedback #121). A flag alone could not tell de-CH, fr-CH and it-CH\n * apart — one Swiss flag each — so the code is beside it. It is named \"Choose\n * language, current: Deutsch\" (`topBar.languageCurrent`). A globe covers the\n * case where `current` matches nothing in `options`. */\nexport function LanguageMenu({\n options,\n current,\n onChange,\n \"aria-label\": ariaLabel,\n heading,\n trigger = \"flag+code\",\n}: {\n options: LanguageOption[];\n current: string | undefined;\n onChange: (code: string) => void;\n /** The menu's name, and the trigger's as given. Default: the menu `topBar.language`,\n * the trigger `topBar.languageCurrent(<the active label>)`, from the\n * {@link UiKitProvider}, else English. */\n \"aria-label\"?: string;\n /** Optional small heading row above the list — as on `TopBarActionMenu`,\n * `OptionSwitcherMenu` and `PaletteMenu`. */\n heading?: string;\n /** What the trigger shows: `\"flag+code\"` (default), `\"code\"` or `\"flag\"`. */\n trigger?: LanguageMenuTrigger;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });\n const active = options.find((o) => o.code === current);\n const triggerName = ariaLabel ?? (active ? labels.languageCurrent(active.label) : labels.language);\n const showsCode = active !== undefined && trigger !== \"flag\";\n return (\n <HoverMenu\n aria-label={labels.language}\n trigger={({ toggle }) => (\n <Tooltip label={active?.label ?? labels.language} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={triggerName}\n className={showsCode ? LANGUAGE_TRIGGER_WIDE_CLASS : TOPBAR_TRIGGER_CLASS}\n >\n {active ? (\n <>\n {trigger !== \"code\" && <span aria-hidden className={`fi fi-${active.country} ${FLAG_CLASS}`} />}\n {showsCode && (\n <span aria-hidden className=\"text-xs font-semibold tabular-nums tracking-wide\">\n {languageCodeLabel(active.code)}\n </span>\n )}\n </>\n ) : (\n <Globe className=\"size-5\" />\n )}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {heading && <MenuHeading>{heading}</MenuHeading>}\n {options.map((lang) => {\n const isActive = lang.code === current;\n return (\n <li key={lang.code}>\n <button\n type=\"button\"\n onClick={() => {\n onChange(lang.code);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex items-center gap-2\">\n <span aria-hidden className={`fi fi-${lang.country} ${FLAG_CLASS}`} />\n {lang.label}\n </span>\n {isActive && <Check className=\"size-4 text-secondary\" />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/shell/tour-menu.js
CHANGED
|
@@ -58,7 +58,15 @@ function TourMenu({
|
|
|
58
58
|
].filter((b) => b.tours.length > 0);
|
|
59
59
|
const tail = [
|
|
60
60
|
...rest.map(entryFor),
|
|
61
|
-
...href ? [
|
|
61
|
+
...href ? [
|
|
62
|
+
{
|
|
63
|
+
kind: "link",
|
|
64
|
+
key: "all-tours",
|
|
65
|
+
icon: /* @__PURE__ */ jsx(Compass, { className: "size-4" }),
|
|
66
|
+
label: words.allTours,
|
|
67
|
+
to: href
|
|
68
|
+
}
|
|
69
|
+
] : []
|
|
62
70
|
];
|
|
63
71
|
const entries = [
|
|
64
72
|
...blocks.flatMap((b) => [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/tour-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useLocation } from \"react-router\";\nimport { Check, Compass } from \"lucide-react\";\nimport { Chip } from \"../components/chip\";\nimport { isTourDone, isTourNew, isTourReset, type TourCompletions, type TourInfo } from \"../tour/tour-info\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type TourMenuLabels } from \"./top-bar-parts-labels\";\n\n/**\n * One tour as the menu needs it — data, so the menu depends on no app's tour registry.\n * The kit's tour engine (`TourProvider`) runs steps and keeps none; which tours exist,\n * which are new and which are taken is the app's (keksdose: `useTours`, `useIsNewTour`,\n * its progress store).\n *\n * Since 0.43 it is a subset of the catalogue's `TourInfo` (docs/tours-harmonization.md\n * §3), so an app keeps ONE list: hand the menu the catalogue's tours with `completed`\n * and `joined`, and it works out ✓, NEW and \"Updated\" itself. Given the record, the kit's\n * ✓ and \"Updated\" win over a row's `completed` and `reset`; given `joined`, its NEW wins\n * over `isNew` — so a row built from an app's own booleans cannot hide an update (§8,\n * build review kastlan A3, keksdose K-S6). The booleans are for a menu with neither.\n */\nexport type TourMenuTour = Partial<Pick<TourInfo, \"route\" | \"since\" | \"updated\">> & {\n id: string;\n title: ReactNode;\n /** The row's icon, drawn at 16px. */\n icon?: ReactNode;\n /** Shipped after this account was opened and not yet taken: a NEW chip on the row,\n * and the dot on the compass. */\n isNew?: boolean;\n /** Taken: a ✓ on the row. */\n completed?: boolean;\n /** Taken before its last update (§8): an \"Updated\" chip, and the dot, as for NEW. */\n reset?: boolean;\n};\n\nexport interface TourMenuProps {\n tours: readonly TourMenuTour[];\n /** Start a tour — `useTour().start(steps, { id })` with the app's steps. */\n onLaunch: (id: string) => void;\n /** The device's completions (`useTourProgress().completed`): ✓ and \"Updated\" from them. */\n completed?: TourCompletions;\n /** The account's `created_at`: NEW from it, by `isTourNew`. */\n joined?: string | null;\n /**\n * The app's tours page (keksdose `/tours`). With it the compass is a LINK there plus a\n * hover menu, and the menu ends in \"All tours…\". Without it (§6.7, Kurvenschmiede) the\n * compass opens the menu, and the tours on neither list follow below a rule.\n */\n href?: string;\n /** The open page's path. Default: the router's location. */\n pathname?: string;\n /** How many new tours the menu offers beyond the page's own. Default 3. */\n maxNew?: number;\n /**\n * `\"all\"`: list every tour — this page's and the new ones first, then the rest below a\n * rule — with `href` too, before \"All tours…\". For an app whose tours carry no `route`,\n * `isNew` or `since` (kastlan, build review A1), where the short list would be empty\n * off the tours' page. Default `\"short\"`: with `href`, only this page's and the new.\n */\n list?: \"short\" | \"all\";\n /** Prop > `<UiKitProvider labels={{ topBarParts: { tours } }}>` > English. */\n labels?: LabelOverride<TourMenuLabels>;\n /** `data-*` attributes go on the compass — a tour's anchor (`data-tour=\"tours\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** A path's first segment — \"/reports/spending?g=month\" → \"/reports\". */\nconst pageOf = (path: string) => `/${path.split(/[?#]/)[0].split(\"/\")[1] ?? \"\"}`;\n\nconst isFresh = (t: TourMenuTour) => (!!t.isNew && !t.completed) || !!t.reset;\n/** When it last became news: its update, else its ship date. */\nconst newsOf = (t: TourMenuTour) => (t.reset ? t.updated?.date : undefined) ?? t.since ?? \"\";\n\n/**\n * The top bar's guided-tours control (docs/top-bar-harmonization.md §2.4, §6.7), lifted\n * from keksdose's: the lucide `Compass`, with a NEW dot while a tour is new, and a short\n * list — the tours that start on this page, up to three new ones (newest first), a rule,\n * and \"All tours…\". A taken tour's row carries a ✓, a new one a NEW chip.\n */\nexport function TourMenu({\n tours: toursProp,\n onLaunch,\n completed: completions,\n joined,\n href,\n pathname: pathnameProp,\n maxNew = 3,\n list = \"short\",\n labels: labelsProp,\n ...attributes\n}: TourMenuProps) {\n const words = useTopBarPartsLabels(labelsProp ? { tours: labelsProp } : undefined).tours;\n const location = useLocation();\n const here = pageOf(pathnameProp ?? location.pathname);\n const tours =\n completions === undefined && joined === undefined\n ? toursProp\n : toursProp.map((t) => {\n const at = completions?.[t.id];\n // Ever completed: the record when there is one, else the row's own word.\n const ever = completions ? at !== undefined : !!t.completed;\n return {\n ...t,\n completed: completions ? isTourDone(t, at) : t.completed,\n reset: completions ? isTourReset(t, at) : t.reset,\n isNew: joined !== undefined ? isTourNew({ since: t.since, joined, completed: ever }) : t.isNew,\n };\n });\n\n const forThisPage = tours.filter((t) => t.route !== undefined && pageOf(t.route) === here);\n const fresh = tours.filter(isFresh);\n // ISO dates sort as text; a stable sort keeps one day's tours in the app's order.\n const newRows = fresh\n .filter((t) => !forThisPage.includes(t))\n .sort((a, b) => newsOf(b).localeCompare(newsOf(a)))\n .slice(0, maxNew);\n const rest = href && list !== \"all\" ? [] : tours.filter((t) => !forThisPage.includes(t) && !newRows.includes(t));\n\n if (tours.length === 0 && !href) return null;\n\n const entryFor = (tour: TourMenuTour): TopBarMenuEntry => ({\n key: tour.id,\n icon: tour.icon,\n label: tour.title,\n trailing: tour.reset ? (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeUpdated}\n </Chip>\n ) : tour.completed ? (\n <>\n <Check aria-hidden className=\"size-4 text-success\" />\n <span className=\"sr-only\">{words.completed}</span>\n </>\n ) : tour.isNew ? (\n // The kit's inert Chip: a plain <span>, no role of its own inside the menu.\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeNew}\n </Chip>\n ) : undefined,\n onSelect: () => onLaunch(tour.id),\n });\n\n const blocks = [\n { key: \"this-page\", label: words.thisPage, tours: forThisPage },\n { key: \"new\", label: newRows.some((t) => t.reset) ? words.newAndUpdated : words.newTours, tours: newRows },\n ].filter((b) => b.tours.length > 0);\n const tail: TopBarMenuEntry[] = [\n ...rest.map(entryFor),\n ...(href\n ? [{ kind: \"link\" as const, key: \"all-tours\", icon: <Compass className=\"size-4\" />, label: words.allTours, to: href }]\n : []),\n ];\n\n const entries: TopBarMenuEntry[] = [\n ...blocks.flatMap((b): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `head-${b.key}`, label: b.label },\n ...b.tours.map(entryFor),\n ]),\n ...(blocks.length > 0 && tail.length > 0 ? [{ kind: \"divider\" as const, key: \"sep-all\" }] : []),\n ...tail,\n ];\n\n return (\n <TopBarActionMenu\n icon={<Compass className=\"size-5\" />}\n iconBadge={\n fresh.length > 0\n ? { label: fresh.some((t) => !t.reset) ? words.newDot : words.updatedDot, tone: \"brand\" }\n : null\n }\n aria-label={words.menu}\n href={href}\n panelClassName=\"w-72 max-h-[70vh] overflow-y-auto\"\n entries={entries}\n triggerAttributes={Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(\"data-\")))}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/tour-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useLocation } from \"react-router\";\nimport { Check, Compass } from \"lucide-react\";\nimport { Chip } from \"../components/chip\";\nimport { isTourDone, isTourNew, isTourReset, type TourCompletions, type TourInfo } from \"../tour/tour-info\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type TourMenuLabels } from \"./top-bar-parts-labels\";\n\n/**\n * One tour as the menu needs it — data, so the menu depends on no app's tour registry.\n * The kit's tour engine (`TourProvider`) runs steps and keeps none; which tours exist,\n * which are new and which are taken is the app's (keksdose: `useTours`, `useIsNewTour`,\n * its progress store).\n *\n * Since 0.43 it is a subset of the catalogue's `TourInfo` (docs/tours-harmonization.md\n * §3), so an app keeps ONE list: hand the menu the catalogue's tours with `completed`\n * and `joined`, and it works out ✓, NEW and \"Updated\" itself. Given the record, the kit's\n * ✓ and \"Updated\" win over a row's `completed` and `reset`; given `joined`, its NEW wins\n * over `isNew` — so a row built from an app's own booleans cannot hide an update (§8,\n * build review kastlan A3, keksdose K-S6). The booleans are for a menu with neither.\n */\nexport type TourMenuTour = Partial<Pick<TourInfo, \"route\" | \"since\" | \"updated\">> & {\n id: string;\n title: ReactNode;\n /** The row's icon, drawn at 16px. */\n icon?: ReactNode;\n /** Shipped after this account was opened and not yet taken: a NEW chip on the row,\n * and the dot on the compass. */\n isNew?: boolean;\n /** Taken: a ✓ on the row. */\n completed?: boolean;\n /** Taken before its last update (§8): an \"Updated\" chip, and the dot, as for NEW. */\n reset?: boolean;\n};\n\nexport interface TourMenuProps {\n tours: readonly TourMenuTour[];\n /** Start a tour — `useTour().start(steps, { id })` with the app's steps. */\n onLaunch: (id: string) => void;\n /** The device's completions (`useTourProgress().completed`): ✓ and \"Updated\" from them. */\n completed?: TourCompletions;\n /** The account's `created_at`: NEW from it, by `isTourNew`. */\n joined?: string | null;\n /**\n * The app's tours page (keksdose `/tours`). With it the compass is a LINK there plus a\n * hover menu, and the menu ends in \"All tours…\". Without it (§6.7, Kurvenschmiede) the\n * compass opens the menu, and the tours on neither list follow below a rule.\n */\n href?: string;\n /** The open page's path. Default: the router's location. */\n pathname?: string;\n /** How many new tours the menu offers beyond the page's own. Default 3. */\n maxNew?: number;\n /**\n * `\"all\"`: list every tour — this page's and the new ones first, then the rest below a\n * rule — with `href` too, before \"All tours…\". For an app whose tours carry no `route`,\n * `isNew` or `since` (kastlan, build review A1), where the short list would be empty\n * off the tours' page. Default `\"short\"`: with `href`, only this page's and the new.\n */\n list?: \"short\" | \"all\";\n /** Prop > `<UiKitProvider labels={{ topBarParts: { tours } }}>` > English. */\n labels?: LabelOverride<TourMenuLabels>;\n /** `data-*` attributes go on the compass — a tour's anchor (`data-tour=\"tours\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** A path's first segment — \"/reports/spending?g=month\" → \"/reports\". */\nconst pageOf = (path: string) => `/${path.split(/[?#]/)[0].split(\"/\")[1] ?? \"\"}`;\n\nconst isFresh = (t: TourMenuTour) => (!!t.isNew && !t.completed) || !!t.reset;\n/** When it last became news: its update, else its ship date. */\nconst newsOf = (t: TourMenuTour) => (t.reset ? t.updated?.date : undefined) ?? t.since ?? \"\";\n\n/**\n * The top bar's guided-tours control (docs/top-bar-harmonization.md §2.4, §6.7), lifted\n * from keksdose's: the lucide `Compass`, with a NEW dot while a tour is new, and a short\n * list — the tours that start on this page, up to three new ones (newest first), a rule,\n * and \"All tours…\". A taken tour's row carries a ✓, a new one a NEW chip.\n */\nexport function TourMenu({\n tours: toursProp,\n onLaunch,\n completed: completions,\n joined,\n href,\n pathname: pathnameProp,\n maxNew = 3,\n list = \"short\",\n labels: labelsProp,\n ...attributes\n}: TourMenuProps) {\n const words = useTopBarPartsLabels(labelsProp ? { tours: labelsProp } : undefined).tours;\n const location = useLocation();\n const here = pageOf(pathnameProp ?? location.pathname);\n const tours =\n completions === undefined && joined === undefined\n ? toursProp\n : toursProp.map((t) => {\n const at = completions?.[t.id];\n // Ever completed: the record when there is one, else the row's own word.\n const ever = completions ? at !== undefined : !!t.completed;\n return {\n ...t,\n completed: completions ? isTourDone(t, at) : t.completed,\n reset: completions ? isTourReset(t, at) : t.reset,\n isNew: joined !== undefined ? isTourNew({ since: t.since, joined, completed: ever }) : t.isNew,\n };\n });\n\n const forThisPage = tours.filter((t) => t.route !== undefined && pageOf(t.route) === here);\n const fresh = tours.filter(isFresh);\n // ISO dates sort as text; a stable sort keeps one day's tours in the app's order.\n const newRows = fresh\n .filter((t) => !forThisPage.includes(t))\n .sort((a, b) => newsOf(b).localeCompare(newsOf(a)))\n .slice(0, maxNew);\n const rest = href && list !== \"all\" ? [] : tours.filter((t) => !forThisPage.includes(t) && !newRows.includes(t));\n\n if (tours.length === 0 && !href) return null;\n\n const entryFor = (tour: TourMenuTour): TopBarMenuEntry => ({\n key: tour.id,\n icon: tour.icon,\n label: tour.title,\n trailing: tour.reset ? (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeUpdated}\n </Chip>\n ) : tour.completed ? (\n <>\n <Check aria-hidden className=\"size-4 text-success\" />\n <span className=\"sr-only\">{words.completed}</span>\n </>\n ) : tour.isNew ? (\n // The kit's inert Chip: a plain <span>, no role of its own inside the menu.\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeNew}\n </Chip>\n ) : undefined,\n onSelect: () => onLaunch(tour.id),\n });\n\n const blocks = [\n { key: \"this-page\", label: words.thisPage, tours: forThisPage },\n { key: \"new\", label: newRows.some((t) => t.reset) ? words.newAndUpdated : words.newTours, tours: newRows },\n ].filter((b) => b.tours.length > 0);\n const tail: TopBarMenuEntry[] = [\n ...rest.map(entryFor),\n ...(href\n ? [\n {\n kind: \"link\" as const,\n key: \"all-tours\",\n icon: <Compass className=\"size-4\" />,\n label: words.allTours,\n to: href,\n },\n ]\n : []),\n ];\n\n const entries: TopBarMenuEntry[] = [\n ...blocks.flatMap((b): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `head-${b.key}`, label: b.label },\n ...b.tours.map(entryFor),\n ]),\n ...(blocks.length > 0 && tail.length > 0 ? [{ kind: \"divider\" as const, key: \"sep-all\" }] : []),\n ...tail,\n ];\n\n return (\n <TopBarActionMenu\n icon={<Compass className=\"size-5\" />}\n iconBadge={\n fresh.length > 0\n ? { label: fresh.some((t) => !t.reset) ? words.newDot : words.updatedDot, tone: \"brand\" }\n : null\n }\n aria-label={words.menu}\n href={href}\n panelClassName=\"w-72 max-h-[70vh] overflow-y-auto\"\n entries={entries}\n triggerAttributes={Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(\"data-\")))}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/shell.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from "./settings/settings-l
|
|
|
18
18
|
export type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from "./settings/settings-layout.js";
|
|
19
19
|
export { useSettingsRoute, resolveSettingsLocation, settingsFromListState, SETTINGS_FROM_LIST_STATE, } from "./settings/use-settings-route.js";
|
|
20
20
|
export type { UseSettingsRouteOptions, SettingsRoute, SettingsLayoutMode, SettingsLocation, SettingsLocationRules, SettingsLocationResult, } from "./settings/use-settings-route.js";
|
|
21
|
-
export { useSettingsFocus, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from "./settings/use-settings-focus.js";
|
|
21
|
+
export { useSettingsFocus, useFocusParam, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from "./settings/use-settings-focus.js";
|
|
22
22
|
export { SettingsSection } from "./settings/settings-section.js";
|
|
23
23
|
export type { SettingsSectionProps } from "./settings/settings-section.js";
|
|
24
24
|
export { useSettingsLayout } from "./settings/settings-context.js";
|
package/dist/shell.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
settingsFromListState,
|
|
19
19
|
SETTINGS_FROM_LIST_STATE
|
|
20
20
|
} from "./settings/use-settings-route.js";
|
|
21
|
-
import { useSettingsFocus, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from "./settings/use-settings-focus.js";
|
|
21
|
+
import { useSettingsFocus, useFocusParam, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from "./settings/use-settings-focus.js";
|
|
22
22
|
import { SettingsSection } from "./settings/settings-section.js";
|
|
23
23
|
import { useSettingsLayout } from "./settings/settings-context.js";
|
|
24
24
|
import { SettingsHeadingLevel, useSettingsHeadingLevel } from "./settings/settings-heading.js";
|
|
@@ -84,6 +84,7 @@ export {
|
|
|
84
84
|
settingsSearchEntries,
|
|
85
85
|
syncState,
|
|
86
86
|
useAdminMenuEntry,
|
|
87
|
+
useFocusParam,
|
|
87
88
|
useLastVisitedPage,
|
|
88
89
|
useSettingsFocus,
|
|
89
90
|
useSettingsHeadingLevel,
|
package/dist/shell.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.43.0 (kastlan): the entry with the provider's words.\nexport { useAdminMenuEntry } from \"./shell/admin-menu\";\nexport type { UseAdminMenuEntryOptions } from \"./shell/admin-menu\";\n// 0.45.0 (docs/admin-harmonization.md §9.3.4): the row's waiting pill on its own, for\n// IconButton's `badge` (kastlan's platform button); its name is `adminArea.waitingLabel`.\nexport { AdminWaitingBadge } from \"./shell/admin-menu\";\nexport type { AdminWaitingBadgeProps } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport {\n useSettingsRoute,\n resolveSettingsLocation,\n settingsFromListState,\n SETTINGS_FROM_LIST_STATE,\n} from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev switchers, and their words.\nexport { TopBarActions, TOP_BAR_ACTION_ORDER } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\n// 0.46.0 (keksdose live #426): A− / A+ for the text size, beside the language.\nexport { AccountMenuTextSize } from \"./shell/account-menu-text-size\";\nexport type { AccountMenuTextSizeProps } from \"./shell/account-menu-text-size\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator, syncState } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\n// 0.51 (§2.7.1, keksdose): one flask, the kit's Account section and the app's own dev\n// sections; `DevSwitchers` is a deprecated wrapper over it until 0.52.\nexport { DevMenu, DEV_ORIGINAL_ROLE_KEY } from \"./shell/dev-menu\";\nexport type { DevMenuProps, DevMenuSection } from \"./shell/dev-menu\";\nexport { DevSwitchers } from \"./shell/dev-switchers\";\nexport type { DevSwitchersProps } from \"./shell/dev-switchers\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,yBAAyB;AAIlC,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB,iCAAiC;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,kBAAkB,qBAAqB,yBAAyB;
|
|
1
|
+
{"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.43.0 (kastlan): the entry with the provider's words.\nexport { useAdminMenuEntry } from \"./shell/admin-menu\";\nexport type { UseAdminMenuEntryOptions } from \"./shell/admin-menu\";\n// 0.45.0 (docs/admin-harmonization.md §9.3.4): the row's waiting pill on its own, for\n// IconButton's `badge` (kastlan's platform button); its name is `adminArea.waitingLabel`.\nexport { AdminWaitingBadge } from \"./shell/admin-menu\";\nexport type { AdminWaitingBadgeProps } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport {\n useSettingsRoute,\n resolveSettingsLocation,\n settingsFromListState,\n SETTINGS_FROM_LIST_STATE,\n} from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, useFocusParam, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev switchers, and their words.\nexport { TopBarActions, TOP_BAR_ACTION_ORDER } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\n// 0.46.0 (keksdose live #426): A− / A+ for the text size, beside the language.\nexport { AccountMenuTextSize } from \"./shell/account-menu-text-size\";\nexport type { AccountMenuTextSizeProps } from \"./shell/account-menu-text-size\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator, syncState } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\n// 0.51 (§2.7.1, keksdose): one flask, the kit's Account section and the app's own dev\n// sections; `DevSwitchers` is a deprecated wrapper over it until 0.52.\nexport { DevMenu, DEV_ORIGINAL_ROLE_KEY } from \"./shell/dev-menu\";\nexport type { DevMenuProps, DevMenuSection } from \"./shell/dev-menu\";\nexport { DevSwitchers } from \"./shell/dev-switchers\";\nexport type { DevSwitchersProps } from \"./shell/dev-switchers\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,yBAAyB;AAIlC,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB,iCAAiC;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,kBAAkB,eAAe,qBAAqB,yBAAyB;AACxF,SAAS,uBAAuB;AAEhC,SAAS,yBAAyB;AAElC,SAAS,sBAAsB,+BAA+B;AAE9D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,oBAAoB;AAK7B,SAAS,eAAe,4BAA4B;AAEpD,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AAEpC,SAAS,gBAAgB;AAEzB,SAAS,qBAAqB,iBAAiB;AAI/C,SAAS,SAAS,6BAA6B;AAE/C,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B,4BAA4B;","names":[]}
|
package/dist/sync/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sync/db.ts"],"sourcesContent":["import Dexie, { type Table, type Transaction } from \"dexie\";\n\n/*\n * The device's copy for the sync engine (docs/local-first-harmonization.md §2.3, D10): ONE\n * Dexie database with the engine's three tables — the rows as `/sync` carries them, the\n * outbox of changes made here, and a key/value `meta` — plus the app's own (kastlan's\n * `photos`). A generic store, not keksdose's per-entity tables.\n */\n\n/** A row as `/sync` carries it: the entity (a table name), its sync id, the server's id\n * once it has one, and its fields. */\nexport interface SyncedRow {\n entity: string;\n id: string;\n pk: number | null;\n data: Record<string, unknown>;\n /** The server's stamp of the row, as the pull carried it BESIDE `data` (kastlan's wire);\n * the next edit here is stamped after it (`stampAfter`, D2). */\n updated_at?: string | null;\n}\n\n/**\n * Where a change made here stands. `pending`: queued. `sending`: in the request now out (a\n * new edit starts a change of its own). `waiting`: refused with a hold code (a lapsed plan,\n * `holdCodes`), sent again every round. `blocked`: refused with any other code, kept, sent\n * again only on `retryBlocked()`. `rejected`: the server's rules refused it (`reason` says\n * why); never sent again.\n */\nexport type OutboxStatus = \"pending\" | \"sending\" | \"waiting\" | \"blocked\" | \"rejected\";\n\n/** A change made on this device, for `/sync`, in the scope its caller named (D11). */\nexport interface OutboxChange {\n seq?: number;\n scope: string;\n entity: string;\n id: string;\n op: \"upsert\" | \"delete\";\n data: Record<string, unknown>;\n /** The edit's time on this device (D2), stamped with `stampAfter`. */\n updated_at: string;\n status: OutboxStatus;\n /** The server's words: a rejection's `detail`, a refusal's `detail`. */\n reason?: string;\n /** The server's code: a rejection's or a refusal's `code`. */\n code?: string;\n}\n\nexport interface SyncMetaEntry {\n key: string;\n value: unknown;\n}\n\n/** The engine's database: its three tables, and the app's own under the names it gave. */\nexport type SyncDb = Dexie & {\n rows: Table<SyncedRow, [string, string]>;\n outbox: Table<OutboxChange, number>;\n meta: Table<SyncMetaEntry, string>;\n};\n\n/** A schema step of an existing database (kastlan's v2: `scope` on the outbox). */\nexport interface SyncDbUpgrade {\n version: number;\n /** Stores this version adds or changes, beside the engine's and `extraStores`. */\n stores?: Record<string, string | null>;\n upgrade?(tx: Transaction): Promise<void> | void;\n}\n\nexport interface CreateSyncDbOptions {\n /** The app's own tables, Dexie schema strings: kastlan `{ photos: \"id, roomId, status\" }`. */\n extraStores?: Record<string, string>;\n /** Later versions of a database that exists on devices already, oldest first. */\n upgrades?: readonly SyncDbUpgrade[];\n}\n\n/** The engine's own tables. `scope` is indexed: a round takes one scope's queue (D11). */\nexport const SYNC_DB_STORES = {\n rows: \"[entity+id], entity\",\n outbox: \"++seq, [entity+id], status, scope\",\n meta: \"key\",\n} as const;\n\n/**\n * The `rows` schema with an `[entity+pk]` index (0.46.1), opt-in: the D16 pages start from a\n * server id (`/meters/7`) and {@link rowByPk} finds its copy row by this index. Not in\n * {@link SYNC_DB_STORES}, so no database on a device changes until its app names it in a\n * version of its own — kastlan, whose devices are on v2: `upgrades: [..., { version: 3,\n * stores: SYNC_DB_PK_INDEX }]`. A database new to the devices may take it at once:\n * `extraStores: { ...SYNC_DB_PK_INDEX }`.\n */\nexport const SYNC_DB_PK_INDEX = {\n rows: \"[entity+id], entity, [entity+pk]\",\n} as const;\n\n/**\n * The copy row of the server's id `pk` in `entity` (0.46.1): by the `[entity+pk]` index when\n * the database has it ({@link SYNC_DB_PK_INDEX}), else by a scan of the entity's rows.\n * `undefined` when the copy holds no such row.\n */\nexport async function rowByPk(db: SyncDb, entity: string, pk: number): Promise<SyncedRow | undefined> {\n if (db.rows.schema.idxByName[\"[entity+pk]\"]) return db.rows.where(\"[entity+pk]\").equals([entity, pk]).first();\n return db.rows.where(\"entity\").equals(entity).filter((row) => row.pk === pk).first();\n}\n\n/**\n * Open (lazily, on first use) the sync database `name`: the engine's tables plus the app's\n * `extraStores`, at version 1 or the last of `upgrades`. One database per account (kastlan:\n * per account and company, `kastlan-handover-<user>-<company>`), and one engine on it (D15).\n *\n * An app whose database predates the engine names its next version in `upgrades` and moves\n * its data there — kastlan: `{ version: 2, upgrade: (tx) => tx.table(\"outbox\").toCollection()\n * .modify({ scope: \"handover\" }) }`. Dexie diffs the schema against the one on the device.\n * Name the app's own type for its tables: `createSyncDb<HandoverDb>(name, { extraStores })`.\n */\nexport function createSyncDb<D extends SyncDb = SyncDb>(name: string, options: CreateSyncDbOptions = {}): D {\n const db = new Dexie(name) as D;\n let stores: Record<string, string | null> = { ...SYNC_DB_STORES, ...options.extraStores };\n db.version(1).stores(stores);\n const upgrades = [...(options.upgrades ?? [])].sort((a, b) => a.version - b.version);\n for (const step of upgrades) {\n stores = { ...stores, ...step.stores };\n const version = db.version(step.version).stores(stores);\n if (step.upgrade) version.upgrade(step.upgrade);\n }\n return db;\n}\n"],"mappings":";AAAA,OAAO,WAA6C;AA2E7C,MAAM,iBAAiB;AAAA,EAC5B,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AACR;AAUO,MAAM,mBAAmB;AAAA,EAC9B,MAAM;AACR;AAOA,eAAsB,QAAQ,IAAY,QAAgB,IAA4C;AACpG,MAAI,GAAG,KAAK,OAAO,UAAU,aAAa,EAAG,QAAO,GAAG,KAAK,MAAM,aAAa,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM;AAC5G,SAAO,GAAG,
|
|
1
|
+
{"version":3,"sources":["../../src/sync/db.ts"],"sourcesContent":["import Dexie, { type Table, type Transaction } from \"dexie\";\n\n/*\n * The device's copy for the sync engine (docs/local-first-harmonization.md §2.3, D10): ONE\n * Dexie database with the engine's three tables — the rows as `/sync` carries them, the\n * outbox of changes made here, and a key/value `meta` — plus the app's own (kastlan's\n * `photos`). A generic store, not keksdose's per-entity tables.\n */\n\n/** A row as `/sync` carries it: the entity (a table name), its sync id, the server's id\n * once it has one, and its fields. */\nexport interface SyncedRow {\n entity: string;\n id: string;\n pk: number | null;\n data: Record<string, unknown>;\n /** The server's stamp of the row, as the pull carried it BESIDE `data` (kastlan's wire);\n * the next edit here is stamped after it (`stampAfter`, D2). */\n updated_at?: string | null;\n}\n\n/**\n * Where a change made here stands. `pending`: queued. `sending`: in the request now out (a\n * new edit starts a change of its own). `waiting`: refused with a hold code (a lapsed plan,\n * `holdCodes`), sent again every round. `blocked`: refused with any other code, kept, sent\n * again only on `retryBlocked()`. `rejected`: the server's rules refused it (`reason` says\n * why); never sent again.\n */\nexport type OutboxStatus = \"pending\" | \"sending\" | \"waiting\" | \"blocked\" | \"rejected\";\n\n/** A change made on this device, for `/sync`, in the scope its caller named (D11). */\nexport interface OutboxChange {\n seq?: number;\n scope: string;\n entity: string;\n id: string;\n op: \"upsert\" | \"delete\";\n data: Record<string, unknown>;\n /** The edit's time on this device (D2), stamped with `stampAfter`. */\n updated_at: string;\n status: OutboxStatus;\n /** The server's words: a rejection's `detail`, a refusal's `detail`. */\n reason?: string;\n /** The server's code: a rejection's or a refusal's `code`. */\n code?: string;\n}\n\nexport interface SyncMetaEntry {\n key: string;\n value: unknown;\n}\n\n/** The engine's database: its three tables, and the app's own under the names it gave. */\nexport type SyncDb = Dexie & {\n rows: Table<SyncedRow, [string, string]>;\n outbox: Table<OutboxChange, number>;\n meta: Table<SyncMetaEntry, string>;\n};\n\n/** A schema step of an existing database (kastlan's v2: `scope` on the outbox). */\nexport interface SyncDbUpgrade {\n version: number;\n /** Stores this version adds or changes, beside the engine's and `extraStores`. */\n stores?: Record<string, string | null>;\n upgrade?(tx: Transaction): Promise<void> | void;\n}\n\nexport interface CreateSyncDbOptions {\n /** The app's own tables, Dexie schema strings: kastlan `{ photos: \"id, roomId, status\" }`. */\n extraStores?: Record<string, string>;\n /** Later versions of a database that exists on devices already, oldest first. */\n upgrades?: readonly SyncDbUpgrade[];\n}\n\n/** The engine's own tables. `scope` is indexed: a round takes one scope's queue (D11). */\nexport const SYNC_DB_STORES = {\n rows: \"[entity+id], entity\",\n outbox: \"++seq, [entity+id], status, scope\",\n meta: \"key\",\n} as const;\n\n/**\n * The `rows` schema with an `[entity+pk]` index (0.46.1), opt-in: the D16 pages start from a\n * server id (`/meters/7`) and {@link rowByPk} finds its copy row by this index. Not in\n * {@link SYNC_DB_STORES}, so no database on a device changes until its app names it in a\n * version of its own — kastlan, whose devices are on v2: `upgrades: [..., { version: 3,\n * stores: SYNC_DB_PK_INDEX }]`. A database new to the devices may take it at once:\n * `extraStores: { ...SYNC_DB_PK_INDEX }`.\n */\nexport const SYNC_DB_PK_INDEX = {\n rows: \"[entity+id], entity, [entity+pk]\",\n} as const;\n\n/**\n * The copy row of the server's id `pk` in `entity` (0.46.1): by the `[entity+pk]` index when\n * the database has it ({@link SYNC_DB_PK_INDEX}), else by a scan of the entity's rows.\n * `undefined` when the copy holds no such row.\n */\nexport async function rowByPk(db: SyncDb, entity: string, pk: number): Promise<SyncedRow | undefined> {\n if (db.rows.schema.idxByName[\"[entity+pk]\"]) return db.rows.where(\"[entity+pk]\").equals([entity, pk]).first();\n return db.rows\n .where(\"entity\")\n .equals(entity)\n .filter((row) => row.pk === pk)\n .first();\n}\n\n/**\n * Open (lazily, on first use) the sync database `name`: the engine's tables plus the app's\n * `extraStores`, at version 1 or the last of `upgrades`. One database per account (kastlan:\n * per account and company, `kastlan-handover-<user>-<company>`), and one engine on it (D15).\n *\n * An app whose database predates the engine names its next version in `upgrades` and moves\n * its data there — kastlan: `{ version: 2, upgrade: (tx) => tx.table(\"outbox\").toCollection()\n * .modify({ scope: \"handover\" }) }`. Dexie diffs the schema against the one on the device.\n * Name the app's own type for its tables: `createSyncDb<HandoverDb>(name, { extraStores })`.\n */\nexport function createSyncDb<D extends SyncDb = SyncDb>(name: string, options: CreateSyncDbOptions = {}): D {\n const db = new Dexie(name) as D;\n let stores: Record<string, string | null> = { ...SYNC_DB_STORES, ...options.extraStores };\n db.version(1).stores(stores);\n const upgrades = [...(options.upgrades ?? [])].sort((a, b) => a.version - b.version);\n for (const step of upgrades) {\n stores = { ...stores, ...step.stores };\n const version = db.version(step.version).stores(stores);\n if (step.upgrade) version.upgrade(step.upgrade);\n }\n return db;\n}\n"],"mappings":";AAAA,OAAO,WAA6C;AA2E7C,MAAM,iBAAiB;AAAA,EAC5B,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AACR;AAUO,MAAM,mBAAmB;AAAA,EAC9B,MAAM;AACR;AAOA,eAAsB,QAAQ,IAAY,QAAgB,IAA4C;AACpG,MAAI,GAAG,KAAK,OAAO,UAAU,aAAa,EAAG,QAAO,GAAG,KAAK,MAAM,aAAa,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM;AAC5G,SAAO,GAAG,KACP,MAAM,QAAQ,EACd,OAAO,MAAM,EACb,OAAO,CAAC,QAAQ,IAAI,OAAO,EAAE,EAC7B,MAAM;AACX;AAYO,SAAS,aAAwC,MAAc,UAA+B,CAAC,GAAM;AAC1G,QAAM,KAAK,IAAI,MAAM,IAAI;AACzB,MAAI,SAAwC,EAAE,GAAG,gBAAgB,GAAG,QAAQ,YAAY;AACxF,KAAG,QAAQ,CAAC,EAAE,OAAO,MAAM;AAC3B,QAAM,WAAW,CAAC,GAAI,QAAQ,YAAY,CAAC,CAAE,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO;AACnF,aAAW,QAAQ,UAAU;AAC3B,aAAS,EAAE,GAAG,QAAQ,GAAG,KAAK,OAAO;AACrC,UAAM,UAAU,GAAG,QAAQ,KAAK,OAAO,EAAE,OAAO,MAAM;AACtD,QAAI,KAAK,QAAS,SAAQ,QAAQ,KAAK,OAAO;AAAA,EAChD;AACA,SAAO;AACT;","names":[]}
|
package/dist/sync/engine.js
CHANGED
|
@@ -48,7 +48,13 @@ function createSyncEngine(options) {
|
|
|
48
48
|
const backoffMs = options.backoffMs?.length ? options.backoffMs : DEFAULT_SYNC_BACKOFF_MS;
|
|
49
49
|
const sharedComputer = options.sharedComputer ?? isSharedComputer;
|
|
50
50
|
let generation = 0;
|
|
51
|
-
let state = {
|
|
51
|
+
let state = {
|
|
52
|
+
phase: "idle",
|
|
53
|
+
online: browserOnline(),
|
|
54
|
+
counts: ZERO,
|
|
55
|
+
lastSyncedAt: null,
|
|
56
|
+
nextRetryAt: null
|
|
57
|
+
};
|
|
52
58
|
const listeners = /* @__PURE__ */ new Set();
|
|
53
59
|
let started = false;
|
|
54
60
|
let poll = null;
|
|
@@ -108,7 +114,8 @@ function createSyncEngine(options) {
|
|
|
108
114
|
const rejected = sent.filter((c) => !refusedIds.has(c.id) && rejections.has(c.id));
|
|
109
115
|
await db.outbox.bulkDelete(sent.filter((c) => !refusedIds.has(c.id) && !rejections.has(c.id)).map((c) => c.seq));
|
|
110
116
|
const hold = refused != null && holdCodes.has(refused.code);
|
|
111
|
-
if (refused)
|
|
117
|
+
if (refused)
|
|
118
|
+
await mark(kept2, { status: hold ? "waiting" : "blocked", code: refused.code, reason: refused.detail });
|
|
112
119
|
for (const change of rejected) {
|
|
113
120
|
const why = rejections.get(change.id);
|
|
114
121
|
await db.outbox.update(change.seq, { status: "rejected", code: why.code, reason: why.detail });
|
|
@@ -137,7 +144,13 @@ function createSyncEngine(options) {
|
|
|
137
144
|
continue;
|
|
138
145
|
}
|
|
139
146
|
const row = await db.rows.get([change.entity, change.id]);
|
|
140
|
-
await db.rows.put({
|
|
147
|
+
await db.rows.put({
|
|
148
|
+
...row,
|
|
149
|
+
entity: change.entity,
|
|
150
|
+
id: change.id,
|
|
151
|
+
pk: row?.pk ?? null,
|
|
152
|
+
data: { ...row?.data, ...change.data }
|
|
153
|
+
});
|
|
141
154
|
}
|
|
142
155
|
if (lastPage) {
|
|
143
156
|
await db.meta.put({ key: lastKnowledgeKey(scope), value: answer.server_knowledge });
|
|
@@ -185,7 +198,8 @@ function createSyncEngine(options) {
|
|
|
185
198
|
await mark(sent, { status: "waiting", code: plan, reason: plan });
|
|
186
199
|
return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };
|
|
187
200
|
}
|
|
188
|
-
if (httpStatusOf(err) === 403)
|
|
201
|
+
if (httpStatusOf(err) === 403)
|
|
202
|
+
return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };
|
|
189
203
|
throw err;
|
|
190
204
|
}
|
|
191
205
|
let cursor = options.paged ? answer.page_cursor ?? null : null;
|
|
@@ -342,7 +356,9 @@ function createSyncEngine(options) {
|
|
|
342
356
|
const row = await db.rows.get([entity, id]);
|
|
343
357
|
const ofRow = await db.outbox.where("[entity+id]").equals([entity, id]).toArray();
|
|
344
358
|
ofRow.sort((a, b) => a.seq - b.seq);
|
|
345
|
-
const updated_at = stampAfter(
|
|
359
|
+
const updated_at = stampAfter(
|
|
360
|
+
latest([row?.updated_at, row?.data.updated_at, ...ofRow.map((c) => c.updated_at)])
|
|
361
|
+
);
|
|
346
362
|
const rejected = ofRow.filter((c) => c.scope === scope && c.status === "rejected");
|
|
347
363
|
if (op === "delete") {
|
|
348
364
|
await db.rows.delete([entity, id]);
|
|
@@ -413,7 +429,9 @@ function createSyncEngine(options) {
|
|
|
413
429
|
await runFresh(true);
|
|
414
430
|
},
|
|
415
431
|
async discardRejected(scope) {
|
|
416
|
-
const gone = (await db.outbox.where("status").equals("rejected").toArray()).filter(
|
|
432
|
+
const gone = (await db.outbox.where("status").equals("rejected").toArray()).filter(
|
|
433
|
+
(c) => !scope || c.scope === scope
|
|
434
|
+
);
|
|
417
435
|
if (gone.length === 0) return;
|
|
418
436
|
await db.transaction("rw", db.rows, db.outbox, db.meta, async () => {
|
|
419
437
|
await db.outbox.bulkDelete(gone.map((c) => c.seq));
|