@eifi1/ui-kit 0.51.4 → 0.51.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/account/account-settings-labels.js.map +1 -1
- package/dist/account/account-settings.js +11 -9
- package/dist/account/account-settings.js.map +1 -1
- package/dist/account/delete-account-setting.js +56 -65
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.js +4 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.js +19 -11
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/admin/admin-tool-links.d.ts +1 -1
- package/dist/admin/admin-tool-links.js +7 -1
- package/dist/admin/admin-tool-links.js.map +1 -1
- package/dist/admin/invitations-panel.js +2 -13
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/mail-test-card.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +4 -11
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/user-roster.js +1 -9
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.js +23 -31
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.js +13 -5
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.js +1 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/checkout-processing.js.map +1 -1
- package/dist/billing/plan-card.js +91 -85
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-limit-toast.js.map +1 -1
- package/dist/billing/plan-limit.js.map +1 -1
- package/dist/billing/plan-price.js.map +1 -1
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/bin/eifi1-pay-page.mjs +6 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.js +1 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.js +29 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +1 -4
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.js +1 -7
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.js +5 -5
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +4 -10
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/card.js +1 -8
- package/dist/components/card.js.map +1 -1
- package/dist/components/chart-zoom-core.js +3 -17
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +4 -22
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/chip-input.js +4 -1
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/chip.js +10 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.js +58 -74
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper-labels.js.map +1 -1
- package/dist/components/column-mapper.js +167 -227
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +1 -4
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +1 -4
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.d.ts +5 -4
- package/dist/components/country-select.js +3 -19
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.js +10 -8
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.js +1 -6
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.js.map +1 -1
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.js +2 -18
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.js +3 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.js +44 -56
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.js +31 -44
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-row-actions.js +19 -16
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.js +4 -1
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.js +30 -39
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +1 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/description-list.js +10 -10
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +9 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +17 -27
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +25 -35
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-base.js +5 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +1 -1
- package/dist/components/field-parts.js +10 -14
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.js +8 -10
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +4 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.js.map +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/grouped-picker.js +6 -8
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.js +1 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input-labels.js.map +1 -1
- package/dist/components/icon-button.js +4 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/icon-picker.js +2 -7
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/image-grid.js +41 -50
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +2 -8
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +14 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +1 -7
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.js +14 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.js +12 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +26 -36
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.js +22 -10
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.js +29 -53
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +13 -31
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +10 -21
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +3 -16
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/nav-pills.js +10 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.js +1 -7
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +1 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +1 -7
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +13 -22
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.js +1 -11
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/password-strength-labels.js.map +1 -1
- package/dist/components/password-strength.js +2 -8
- package/dist/components/password-strength.js.map +1 -1
- package/dist/components/phone-input.js +42 -57
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +1 -8
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +16 -9
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.js +1 -8
- package/dist/components/qr-code.js.map +1 -1
- package/dist/components/row-actions.js +5 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/select.js +28 -38
- package/dist/components/select.js.map +1 -1
- package/dist/components/separator.js +1 -5
- package/dist/components/separator.js.map +1 -1
- package/dist/components/series-chart-axes.js +4 -1
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.js +29 -45
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.js +21 -17
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.js +4 -9
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.js +4 -13
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/signed-amount.js +12 -5
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/skeleton.js +2 -17
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +13 -36
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -10
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.js +14 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +1 -1
- package/dist/components/swipeable-row.js +1 -8
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table-skeleton.js +4 -1
- package/dist/components/table-skeleton.js.map +1 -1
- package/dist/components/table.js +12 -9
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.js +5 -24
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +1 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +6 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +2 -11
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.js +2 -22
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -4
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -1
- package/dist/components/translation-review-parts.js +7 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/translation-review.js +18 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +14 -10
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.js +1 -13
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/use-table-state.js +14 -11
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.js +3 -26
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/demo/demo-start.js +8 -4
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-labels.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +1 -8
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-capture.d.ts +1 -1
- package/dist/feedback/feedback-capture.js +3 -1
- package/dist/feedback/feedback-capture.js.map +1 -1
- package/dist/feedback/feedback-comment-thread.js +27 -13
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-context.js +1 -4
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-public-list.js +7 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-record.js +1 -5
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.js +25 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-table.js +4 -1
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +2 -13
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback/feedback-vote-button.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-document-title.js.map +1 -1
- package/dist/hooks/use-file-drop.js.map +1 -1
- package/dist/hooks/use-file-picker-labels.js.map +1 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +10 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.d.ts +1 -1
- package/dist/hooks/use-row-swipe.js +1 -6
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.js +1 -4
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +4 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.js +2 -8
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.js.map +1 -1
- package/dist/i18n/locales/en.js +4 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +4 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +4 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +4 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +4 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.js +13 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +40 -23
- package/dist/index.js.map +1 -1
- package/dist/landing/access.js +1 -4
- package/dist/landing/access.js.map +1 -1
- package/dist/landing/landing-sections.js +35 -38
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/device-settings.js +2 -1
- package/dist/lib/device-settings.js.map +1 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/iban.js.map +1 -1
- package/dist/lib/logger.js +1 -5
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/person-name.js.map +1 -1
- package/dist/lib/phone.js.map +1 -1
- package/dist/lib/qr-encode.js +344 -8
- package/dist/lib/qr-encode.js.map +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/table-text.js +15 -3
- package/dist/lib/table-text.js.map +1 -1
- package/dist/lib/text-match.d.ts +4 -0
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/notification-inbox.js +63 -44
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +6 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/use-push-device.js +4 -1
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/pwa-update-prompt.d.ts +1 -1
- package/dist/offline/pwa-update-prompt.js +7 -1
- package/dist/offline/pwa-update-prompt.js.map +1 -1
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +6 -1
- package/dist/offline.js.map +1 -1
- package/dist/pay/pay.css +8 -1
- package/dist/rhf/fields.d.ts +1 -2
- package/dist/rhf/fields.js +1 -10
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.js +1 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/search/command-palette.js +71 -62
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +34 -5
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/settings/device-reset-setting.js +1 -6
- package/dist/settings/device-reset-setting.js.map +1 -1
- package/dist/settings/settings-heading.js.map +1 -1
- package/dist/settings/settings-layout.js +9 -20
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.js +62 -31
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -10
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/dev-menu.js +12 -6
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/sync-status-indicator.js +7 -6
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +1 -10
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.js +9 -1
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.js +24 -6
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/react.js.map +1 -1
- package/dist/testing/viewport.js +7 -4
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/contrast-tokens.js +13 -2
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/palette-derive.d.ts +1 -1
- package/dist/theme/palette-derive.js +18 -29
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +1 -3
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.js +5 -1
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-engine.js +1 -4
- package/dist/tour/tour-engine.js.map +1 -1
- package/dist/tour/tour.js +21 -12
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.js +3 -24
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +24 -28
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +1 -1
- package/dist/wizard/wizard-step.js +1 -8
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +5 -2
- package/src/account/account-settings-labels.ts +1 -2
- package/src/account/account-settings.tsx +11 -17
- package/src/account/delete-account-setting.tsx +1 -6
- package/src/account/email-change-setting-labels.ts +1 -2
- package/src/account/email-change-setting.tsx +3 -1
- package/src/account/passkeys-setting.tsx +1 -3
- package/src/account/sessions-setting.tsx +4 -2
- package/src/admin/admin-action-confirm.tsx +4 -7
- package/src/admin/admin-action-log.tsx +29 -29
- package/src/admin/admin-area-labels.ts +4 -1
- package/src/admin/admin-overview.tsx +8 -2
- package/src/admin/admin-sections.tsx +3 -1
- package/src/admin/admin-tool-links.tsx +7 -1
- package/src/admin/invitations-panel.tsx +5 -11
- package/src/admin/mail-test-card.tsx +1 -2
- package/src/admin/reviewer-scope-editor.tsx +4 -9
- package/src/admin/role-select.tsx +4 -2
- package/src/admin/user-roster.tsx +2 -11
- package/src/auth/accept-invitation.tsx +1 -7
- package/src/auth/auth-errors.ts +1 -6
- package/src/auth/forgot-password-form.tsx +2 -7
- package/src/auth/register-form.tsx +7 -8
- package/src/auth/reset-password-form.tsx +2 -5
- package/src/auth/sign-in-form.tsx +15 -14
- package/src/auth/verify-email.tsx +2 -7
- package/src/billing/billing-banner.tsx +3 -7
- package/src/billing/billing-labels.ts +6 -4
- package/src/billing/checkout-processing.ts +0 -1
- package/src/billing/plan-card.tsx +9 -8
- package/src/billing/plan-limit-toast.tsx +1 -2
- package/src/billing/plan-limit.ts +1 -2
- package/src/billing/plan-price.ts +1 -4
- package/src/billing/plan-switch.tsx +1 -2
- package/src/billing/subscription-actions.tsx +5 -2
- package/src/components/account-chips.tsx +1 -7
- package/src/components/alert-banner.tsx +2 -8
- package/src/components/amount-input.tsx +206 -178
- package/src/components/authed-image.tsx +7 -4
- package/src/components/autocomplete.tsx +7 -9
- package/src/components/bulk-action-bar.tsx +2 -7
- package/src/components/button-group.tsx +1 -2
- package/src/components/button-look.ts +6 -17
- package/src/components/button.tsx +1 -4
- package/src/components/calculator.tsx +6 -9
- package/src/components/calendar-heatmap.tsx +5 -10
- package/src/components/card.tsx +9 -13
- package/src/components/chart-zoom-core.tsx +6 -33
- package/src/components/chart.tsx +8 -31
- package/src/components/checkbox-group.tsx +1 -4
- package/src/components/chip-input.tsx +6 -6
- package/src/components/chip.tsx +14 -26
- package/src/components/choice-card.tsx +4 -18
- package/src/components/column-mapper-labels.ts +3 -10
- package/src/components/column-mapper.tsx +33 -122
- package/src/components/combobox-core.tsx +12 -42
- package/src/components/combobox.tsx +2 -6
- package/src/components/confirm-dialog.tsx +1 -3
- package/src/components/copy-button.tsx +2 -4
- package/src/components/country-select.tsx +9 -21
- package/src/components/currency-option-list.tsx +13 -14
- package/src/components/currency-select.tsx +2 -7
- package/src/components/danger-confirm.tsx +8 -9
- package/src/components/data-table-cells.tsx +1 -2
- package/src/components/data-table-column-settings.tsx +1 -3
- package/src/components/data-table-filter-popover.tsx +5 -19
- package/src/components/data-table-filters.ts +1 -6
- package/src/components/data-table-fit.ts +1 -3
- package/src/components/data-table-groups.tsx +3 -1
- package/src/components/data-table-head.tsx +27 -45
- package/src/components/data-table-mobile.tsx +99 -108
- package/src/components/data-table-pagination.tsx +2 -6
- package/src/components/data-table-row-actions.tsx +25 -24
- package/src/components/data-table-row.tsx +16 -17
- package/src/components/data-table-shared.tsx +10 -16
- package/src/components/data-table-sort.ts +1 -2
- package/src/components/data-table-virtual.ts +9 -4
- package/src/components/data-table.tsx +489 -501
- package/src/components/date-picker.tsx +1 -7
- package/src/components/date-range-picker.tsx +3 -13
- package/src/components/description-list.tsx +7 -13
- package/src/components/dialog-frame.tsx +12 -3
- package/src/components/disclosure.tsx +8 -12
- package/src/components/dropdown.tsx +3 -11
- package/src/components/empty-state.tsx +1 -3
- package/src/components/entity-combobox.tsx +2 -6
- package/src/components/error-boundary.tsx +7 -7
- package/src/components/field-base.tsx +8 -14
- package/src/components/field-parts.tsx +12 -23
- package/src/components/field-sync.tsx +7 -11
- package/src/components/file-button.tsx +7 -3
- package/src/components/file-dropzone.tsx +7 -12
- package/src/components/floating-panel.tsx +44 -51
- package/src/components/focus-ring.ts +1 -2
- package/src/components/form-actions.tsx +1 -4
- package/src/components/grouped-picker.tsx +6 -8
- package/src/components/hover-menu.tsx +1 -4
- package/src/components/iban-input-labels.ts +1 -2
- package/src/components/icon-button.tsx +46 -48
- package/src/components/icon-picker.tsx +4 -17
- package/src/components/image-grid.tsx +1 -6
- package/src/components/inline-edit-field.tsx +1 -5
- package/src/components/inline-entity-combobox.tsx +6 -21
- package/src/components/input.tsx +14 -2
- package/src/components/jump-calendar.tsx +2 -6
- package/src/components/legal-labels.ts +1 -2
- package/src/components/legal-page.tsx +18 -7
- package/src/components/legal.tsx +14 -5
- package/src/components/lightbox.tsx +1 -3
- package/src/components/line-items.tsx +7 -15
- package/src/components/list.tsx +29 -25
- package/src/components/measured-grid.tsx +8 -30
- package/src/components/menu-item.tsx +1 -3
- package/src/components/mini-calendar.tsx +129 -159
- package/src/components/money-field.tsx +4 -2
- package/src/components/month-picker.tsx +14 -21
- package/src/components/multi-entity-combobox.tsx +6 -26
- package/src/components/multi-select.tsx +5 -9
- package/src/components/nav-pills.tsx +10 -2
- package/src/components/number-field.tsx +18 -15
- package/src/components/number-input.tsx +3 -19
- package/src/components/numpad-sheet.tsx +9 -13
- package/src/components/one-time-code-input.tsx +6 -11
- package/src/components/page-contents.tsx +2 -9
- package/src/components/page-header.tsx +5 -10
- package/src/components/password-strength-labels.ts +1 -2
- package/src/components/password-strength.tsx +6 -9
- package/src/components/phone-input.tsx +2 -13
- package/src/components/picker-sheet.tsx +2 -5
- package/src/components/pie-chart.tsx +6 -12
- package/src/components/popover.tsx +2 -1
- package/src/components/progress-bar.tsx +16 -25
- package/src/components/qr-code.tsx +2 -9
- package/src/components/row-actions.tsx +9 -1
- package/src/components/search-field.tsx +4 -5
- package/src/components/select.tsx +2 -10
- package/src/components/separator.tsx +1 -5
- package/src/components/series-chart-axes.ts +5 -5
- package/src/components/series-chart-budget.ts +1 -4
- package/src/components/series-chart-ticks.ts +1 -4
- package/src/components/series-chart.tsx +43 -83
- package/src/components/settings-fields.tsx +20 -15
- package/src/components/share-card.tsx +6 -14
- package/src/components/sign-chip.tsx +16 -16
- package/src/components/signature-pad.tsx +21 -13
- package/src/components/signed-amount.tsx +6 -14
- package/src/components/skeleton.tsx +2 -13
- package/src/components/slider.tsx +9 -20
- package/src/components/sparkline.tsx +2 -13
- package/src/components/stat-tile.tsx +13 -24
- package/src/components/swatch-picker.tsx +8 -3
- package/src/components/swipeable-row.tsx +2 -11
- package/src/components/table-skeleton.tsx +4 -1
- package/src/components/table.tsx +12 -8
- package/src/components/tabs.tsx +24 -39
- package/src/components/text-link.tsx +6 -6
- package/src/components/text.tsx +1 -2
- package/src/components/tile-radio.tsx +6 -19
- package/src/components/time-input.tsx +4 -5
- package/src/components/toast.tsx +3 -12
- package/src/components/toggle-group.tsx +9 -9
- package/src/components/toggle-legend.tsx +4 -16
- package/src/components/tooltip.tsx +9 -48
- package/src/components/translation-review-editor.tsx +4 -4
- package/src/components/translation-review-parts.tsx +7 -1
- package/src/components/translation-review.tsx +15 -16
- package/src/components/tree-view.tsx +17 -20
- package/src/components/treemap.tsx +3 -15
- package/src/components/trigger-aria.ts +1 -4
- package/src/components/use-table-state.ts +16 -17
- package/src/components/user-avatar.tsx +4 -21
- package/src/demo/demo-start.tsx +8 -4
- package/src/feedback/feedback-attachment-labels.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +16 -29
- package/src/feedback/feedback-capture.ts +3 -1
- package/src/feedback/feedback-comment-thread.tsx +9 -6
- package/src/feedback/feedback-context.tsx +1 -4
- package/src/feedback/feedback-dialog.tsx +3 -4
- package/src/feedback/feedback-inbox.tsx +3 -9
- package/src/feedback/feedback-public-list.tsx +7 -1
- package/src/feedback/feedback-record.ts +6 -14
- package/src/feedback/feedback-replies.tsx +10 -5
- package/src/feedback/feedback-row-detail.tsx +2 -5
- package/src/feedback/feedback-status-undo.ts +1 -4
- package/src/feedback/feedback-submit-dialog.tsx +4 -4
- package/src/feedback/feedback-table.tsx +12 -8
- package/src/feedback/feedback-thread.tsx +4 -16
- package/src/feedback/feedback-vocabulary.ts +72 -82
- package/src/feedback/feedback-vote-button.tsx +9 -8
- package/src/hooks/use-anchored-rect.ts +1 -4
- package/src/hooks/use-document-title.ts +3 -1
- package/src/hooks/use-file-drop.ts +1 -3
- package/src/hooks/use-file-picker-labels.ts +1 -2
- package/src/hooks/use-file-picker.tsx +1 -5
- package/src/hooks/use-focus-trap.ts +1 -2
- package/src/hooks/use-overlay-history.ts +10 -5
- package/src/hooks/use-row-swipe.ts +1 -6
- package/src/i18n/defaults.ts +1 -4
- package/src/i18n/german.ts +33 -49
- package/src/i18n/kit-labels.tsx +5 -20
- package/src/i18n/languages.ts +2 -8
- package/src/i18n/locales/en.ts +11 -31
- package/src/i18n/locales/es.ts +35 -49
- package/src/i18n/locales/fr.ts +34 -51
- package/src/i18n/locales/hu.ts +26 -37
- package/src/i18n/locales/it.ts +35 -52
- package/src/i18n/locales/zh.ts +14 -32
- package/src/i18n/plurals.ts +4 -1
- package/src/i18n/review.ts +18 -7
- package/src/i18n/shared-labels.ts +1 -2
- package/src/index.ts +106 -32
- package/src/landing/access.ts +2 -7
- package/src/landing/landing-sections.tsx +14 -15
- package/src/landing/page-seo.ts +1 -3
- package/src/lib/calc.ts +4 -1
- package/src/lib/clipboard-minus.ts +4 -1
- package/src/lib/dates.ts +2 -10
- package/src/lib/decimal-marks.ts +1 -3
- package/src/lib/device-settings.ts +2 -1
- package/src/lib/direction.ts +1 -3
- package/src/lib/iban.ts +89 -9
- package/src/lib/logger.ts +1 -5
- package/src/lib/person-name.ts +1 -3
- package/src/lib/phone.ts +5 -8
- package/src/lib/qr-encode.ts +32 -8
- package/src/lib/session-refresh.ts +5 -6
- package/src/lib/table-text.ts +24 -6
- package/src/lib/text-match.ts +15 -3
- package/src/lib/translation-review.ts +1 -3
- package/src/notifications/notification-inbox.tsx +17 -4
- package/src/notifications/notification-parts.tsx +1 -2
- package/src/notifications/notification-types-setting.tsx +19 -18
- package/src/notifications/notifications-labels.ts +1 -2
- package/src/notifications/push-device-setting.tsx +7 -1
- package/src/notifications/push-devices-setting.tsx +7 -3
- package/src/notifications/push-worker.ts +10 -2
- package/src/notifications/quiet-hours-setting.tsx +4 -1
- package/src/notifications/use-push-device.ts +4 -1
- package/src/offline/pwa-update-prompt.tsx +7 -1
- package/src/offline/query-persistence.ts +9 -6
- package/src/offline.ts +6 -1
- package/src/rhf/fields.tsx +50 -19
- package/src/rhf/form-core.tsx +2 -6
- package/src/rhf/line-items.tsx +5 -7
- package/src/rhf/use-rhf-wizard-step.ts +3 -12
- package/src/search/command-palette.tsx +15 -23
- package/src/search/global-search.tsx +38 -8
- package/src/search/search-index.ts +5 -1
- package/src/settings/device-reset-setting.tsx +5 -8
- package/src/settings/settings-heading.tsx +3 -1
- package/src/settings/settings-layout.tsx +6 -13
- package/src/settings/use-settings-route.ts +4 -3
- package/src/shell/app-shell.tsx +48 -24
- package/src/shell/auth-layout.tsx +1 -6
- package/src/shell/dev-menu.tsx +12 -6
- package/src/shell/option-switcher-menu.tsx +1 -3
- package/src/shell/sync-status-indicator.tsx +14 -12
- package/src/shell/top-bar-parts-labels.ts +1 -3
- package/src/shell/topbar-action-menu.tsx +9 -8
- package/src/shell/topbar-controls.tsx +5 -19
- package/src/shell/tour-menu.tsx +9 -1
- package/src/sync/db.ts +5 -1
- package/src/sync/engine.ts +44 -12
- package/src/sync.ts +16 -2
- package/src/testing/css-variables.ts +1 -2
- package/src/testing/index.ts +1 -6
- package/src/testing/react.ts +1 -2
- package/src/testing/viewport.ts +9 -9
- package/src/theme/contrast-tokens.ts +13 -2
- package/src/theme/palette-derive.ts +24 -50
- package/src/theme/palette-presets.ts +0 -1
- package/src/theme/palette-store.ts +1 -4
- package/src/theme/text-size.ts +1 -1
- package/src/theme/theme-store.ts +2 -6
- package/src/tour/tour-catalogue.tsx +4 -2
- package/src/tour/tour-dock.ts +5 -1
- package/src/tour/tour-engine.ts +2 -6
- package/src/tour/tour.tsx +28 -12
- package/src/tour/use-tour-progress.ts +8 -2
- package/src/tour/use-tour-url-sync.ts +3 -1
- package/src/vite-env.d.ts +1 -4
- package/src/wizard/stepper-nav.tsx +10 -35
- package/src/wizard/types.ts +1 -4
- package/src/wizard/use-wizard.ts +28 -43
- package/src/wizard/validation.ts +1 -5
- package/src/wizard/wizard-context.tsx +2 -6
- package/src/wizard/wizard-step.tsx +1 -8
- package/src/wizard/wizard-summary.tsx +2 -6
- package/tokens.css +393 -152
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/dev-menu.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { FlaskConical, Undo2 } from \"lucide-react\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type DevSwitchersLabels } from \"./top-bar-parts-labels\";\n\n/** Where the role the session started with is kept, unless the app names its own key. */\nexport const DEV_ORIGINAL_ROLE_KEY = \"ui-kit:dev-original-role\";\n\nfunction readKey(key: string): string | null {\n try {\n return sessionStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeKey(key: string, value: string | null): void {\n try {\n if (value === null) sessionStorage.removeItem(key);\n else sessionStorage.setItem(key, value);\n } catch {\n /* storage blocked: \"Back to\" lasts as long as this page */\n }\n}\n\n/**\n * One of the app's own groups in the {@link DevMenu} (docs/top-bar-harmonization.md\n * §2.7.1): keksdose's budget role, its \"today\" override, its resets.\n *\n * The rows are plain {@link TopBarMenuEntry}s, and there is no dev item type beside them\n * (the contract with keksdose, 0.51): a choice of one is radio rows (`checked`), a toggle\n * is a `checkable: \"checkbox\"` row, an action is a plain row. No form fields — a menu is\n * a `role=\"menu\"`, where a text or date input has no place in the arrow keys; a free\n * value is a \"Custom…\" row that opens the app's own dialog.\n */\nexport interface DevMenuSection {\n /** Unique among the sections. The rows' keys need be unique only within theirs: the\n * menu prefixes them with this. */\n key: string;\n /** The group's heading, drawn above its rows and announced as the group's name. */\n title: ReactNode;\n /** The rows, as `TopBarActionMenu` takes them. A `divider` ends the group, as there. */\n entries: readonly TopBarMenuEntry[];\n}\n\nexport interface DevMenuProps<R extends string, P extends string> {\n /**\n * Draw anything at all. The app passes `import.meta.env.DEV` AND its probe of the dev\n * route (§2.7.1): the constant keeps the menu off a production build's screen, the\n * probe off a dev build whose server has the route shut (§6.1) — flasks with nothing\n * behind them. Neither takes code out of the bundle; see `sections`.\n */\n enabled: boolean;\n roles?: readonly R[];\n /** The role the server enforces now. Named `currentRole`, not `role`, because `role` on\n * a JSX element is the ARIA role (as `RoleChip`'s `value`). */\n currentRole?: R;\n /** Apply a role — the app calls server-kit's `POST /dev/switch {role}` and re-reads\n * `/auth/me` (§2.7, §6.2). */\n onRole?: (role: R) => void;\n plans?: readonly P[];\n /** The plan the server enforces now. Left out (not known yet — kastlan's overview is a\n * 404 while billing is off), the plan group still renders, with no row checked; a\n * `GET /dev/switch` answers it. */\n currentPlan?: P;\n /** Apply a plan — `POST /dev/switch {plan}`, then the overview (§6.3). */\n onPlan?: (plan: P) => void;\n /**\n * The role to offer \"Back to {role}\" for. Left out, the kit keeps it itself: the role\n * before the first switch of this browser session, in `sessionStorage` under\n * `storageKey` — the server answers with the NEW role after a switch, so only the\n * browser still knows the old one.\n */\n originalRole?: R;\n /** The `sessionStorage` key for the kept role. Default {@link DEV_ORIGINAL_ROLE_KEY};\n * an app with several accounts on one browser may scope it per user. */\n storageKey?: string;\n /** Billing is off locally: the plan group says plans change nothing, and stays usable\n * (§6.3). */\n billingOff?: boolean;\n /** A role's or a plan's words in the menu. Default: the value itself. */\n roleLabel?: (role: R) => ReactNode;\n planLabel?: (plan: P) => ReactNode;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { dev } }}>` > English. */\n labels?: LabelOverride<DevSwitchersLabels>;\n /** 0.51: the app's own dev groups, after the kit's role and plan, in this order. Their\n * DEFINITIONS belong in a module only a dev build imports (§2.7.1): `enabled={false}`\n * hides the menu, but the code of a section built inline stays in the bundle. */\n sections?: readonly DevMenuSection[];\n /**\n * 0.51: an override is on that the developer could forget — a \"today\" that is not\n * today, a simulated slow server. The flask wears a dot, and its name says so (\"Dev\n * tools · Role: ADMIN · Plan: free Overrides on\"), since a dot alone tells a screen\n * reader nothing. The app decides what counts; the kit cannot see into its sections.\n */\n active?: boolean;\n}\n\nconst flask = <FlaskConical className=\"size-5 text-warning\" />;\n\n/** A label's text for a sentence, or the raw value when the label is not text. */\nconst textOf = (label: ReactNode, value: string) =>\n typeof label === \"string\" || typeof label === \"number\" ? String(label) : value;\n\n/** A row someone can pick — what makes a menu worth drawing. */\nconst isRow = (entry: TopBarMenuEntry) => entry.kind !== \"heading\" && entry.kind !== \"divider\";\n\n/**\n * The top bar's local-dev menu (docs/top-bar-harmonization.md §2.7, §2.7.1, §6.2, §6.3):\n * ONE amber flask, `RoleSwitcher`'s, over the kit's Account section and the app's own.\n *\n * 0.42's `DevSwitchers` drew a flask per switcher and had no place for anything else,\n * and an app has more dev controls than a role and a plan — keksdose's budget role, a\n * \"today\" override, resets (agreed with keksdose for 0.51). So the kit's part is now the\n * menu, and the app's rows go in it.\n *\n * - **The Account section** comes first: the role group, ending in \"Back to {role}\" once\n * the role differs from the one the session started with (the dev route needs only a\n * signed-in user, so a switch to a lesser role can always switch back); then the plan\n * group, which with `billingOff` notes that plans change nothing. Each renders only\n * with its list and handler.\n * - **The app's `sections`** follow, each a group under its own heading.\n * - **`active`** puts a dot on the flask for an override that is easy to forget.\n *\n * Nothing renders unless `enabled`, and nothing with no row to show. The panel scrolls\n * (`TopBarActionMenu`'s `scrollable`): some 25 rows ran off a 390×844 phone.\n *\n * From `md` up the groups stand side by side, one column each, in that order (0.51.4,\n * `TopBarActionMenu`'s `columns`; keksdose dev #640, Marcel: \"Each group gets a column\").\n * \"Back to {role}\" stays under the roles. A desktop too narrow for all of them wraps\n * the columns onto a second row; a phone keeps the one scrolling column.\n */\nexport function DevMenu<R extends string, P extends string>({\n enabled,\n roles,\n currentRole: role,\n onRole,\n plans,\n currentPlan: plan,\n onPlan,\n originalRole,\n storageKey = DEV_ORIGINAL_ROLE_KEY,\n billingOff = false,\n roleLabel = (r) => r,\n planLabel = (p) => p,\n labels: labelsProp,\n sections = [],\n active = false,\n}: DevMenuProps<R, P>) {\n const bar = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n const words = useTopBarPartsLabels(labelsProp ? { dev: labelsProp } : undefined).dev;\n const [kept, setKept] = useState<string | null>(() => (enabled ? readKey(storageKey) : null));\n // 0.43 (keksdose): `enabled` may start false and turn true — an app's GET probe of the\n // dev route — so the kept role is read again then, or \"Back to {role}\" is lost after a\n // reload. Adjusted during render, React's pattern for state that follows a prop.\n const [wasEnabled, setWasEnabled] = useState(enabled);\n if (wasEnabled !== enabled) {\n setWasEnabled(enabled);\n if (enabled) setKept(readKey(storageKey));\n }\n if (!enabled) return null;\n\n const original = (originalRole ?? kept ?? undefined) as R | undefined;\n\n const pickRole = (next: R) => {\n if (originalRole === undefined) {\n if (next === original) {\n // Back where the session began: nothing left to go back to.\n writeKey(storageKey, null);\n setKept(null);\n } else if (kept === null && role !== undefined && next !== role) {\n writeKey(storageKey, role);\n setKept(role);\n }\n }\n onRole?.(next);\n };\n\n const roleGroup: TopBarMenuEntry[] =\n roles && roles.length > 0 && role !== undefined && onRole\n ? [\n { kind: \"heading\", key: \"head-role\", label: words.roleHeading },\n ...roles.map(\n (r): TopBarMenuEntry => ({ key: `role-${r}`, label: roleLabel(r), checked: r === role, onSelect: () => pickRole(r) }),\n ),\n // Behind a rule, which ends the role group: \"Back to\" is a way out, not a role.\n ...(original !== undefined && original !== role\n ? ([\n { kind: \"divider\", key: \"sep-back\" },\n {\n key: \"back\",\n icon: <Undo2 className=\"size-4\" />,\n label: words.backTo(textOf(roleLabel(original), original)),\n onSelect: () => pickRole(original),\n },\n ] satisfies TopBarMenuEntry[])\n : []),\n ]\n : [];\n\n const planGroup: TopBarMenuEntry[] =\n plans && plans.length > 0 && onPlan\n ? [\n {\n kind: \"heading\",\n key: \"head-plan\",\n // The billing-off note is part of the group's NAME, under the heading. In 0.42's\n // own plan menu it was a trailing `<li>`: not a row, so the arrow keys passed\n // over it and a screen reader moving through the menu never reached it. Named,\n // it is read on the way into the plans. And in one menu a trailing note would\n // sit under the app's sections, not under the plans.\n label: billingOff ? (\n <>\n {words.planHeading}{\" \"}\n <span\n data-dev-billing-off=\"\"\n className=\"mt-0.5 block font-normal normal-case tracking-normal text-xs text-muted\"\n >\n {words.billingOff}\n </span>\n </>\n ) : (\n words.planHeading\n ),\n },\n ...plans.map(\n (p): TopBarMenuEntry => ({ key: `plan-${p}`, label: planLabel(p), checked: p === plan, onSelect: () => onPlan(p) }),\n ),\n ]\n : [];\n\n // The app's rows keyed under their section: two sections may well both have a \"reset\".\n const appGroups = sections.flatMap((section): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `section:${section.key}`, label: section.title },\n ...section.entries.map((entry): TopBarMenuEntry => ({ ...entry, key: `${section.key}:${entry.key}` })),\n ]);\n\n const entries = [...roleGroup, ...planGroup, ...appGroups];\n if (!entries.some(isRow)) return null;\n\n // The menu, then the role and the plan, as 0.42's two triggers named them: the bar\n // shows the flask only, so its name (and tooltip) is where they are read.\n const name = [\n words.menu,\n roleGroup.length > 0 && role !== undefined ? bar.role(role) : null,\n planGroup.length > 0 && plan !== undefined ? words.plan(plan) : null,\n ]\n .filter((part): part is string => part !== null)\n .join(\" · \");\n\n return (\n <TopBarActionMenu\n icon={flask}\n // The badge's words join the trigger's name, as the tours compass's NEW dot does.\n // `danger`, the badge's default: an amber dot would vanish into the amber flask.\n iconBadge={active ? { label: words.overridesOn } : null}\n aria-label={name}\n entries={entries}\n scrollable\n columns\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/dev-menu.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { FlaskConical, Undo2 } from \"lucide-react\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type DevSwitchersLabels } from \"./top-bar-parts-labels\";\n\n/** Where the role the session started with is kept, unless the app names its own key. */\nexport const DEV_ORIGINAL_ROLE_KEY = \"ui-kit:dev-original-role\";\n\nfunction readKey(key: string): string | null {\n try {\n return sessionStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeKey(key: string, value: string | null): void {\n try {\n if (value === null) sessionStorage.removeItem(key);\n else sessionStorage.setItem(key, value);\n } catch {\n /* storage blocked: \"Back to\" lasts as long as this page */\n }\n}\n\n/**\n * One of the app's own groups in the {@link DevMenu} (docs/top-bar-harmonization.md\n * §2.7.1): keksdose's budget role, its \"today\" override, its resets.\n *\n * The rows are plain {@link TopBarMenuEntry}s, and there is no dev item type beside them\n * (the contract with keksdose, 0.51): a choice of one is radio rows (`checked`), a toggle\n * is a `checkable: \"checkbox\"` row, an action is a plain row. No form fields — a menu is\n * a `role=\"menu\"`, where a text or date input has no place in the arrow keys; a free\n * value is a \"Custom…\" row that opens the app's own dialog.\n */\nexport interface DevMenuSection {\n /** Unique among the sections. The rows' keys need be unique only within theirs: the\n * menu prefixes them with this. */\n key: string;\n /** The group's heading, drawn above its rows and announced as the group's name. */\n title: ReactNode;\n /** The rows, as `TopBarActionMenu` takes them. A `divider` ends the group, as there. */\n entries: readonly TopBarMenuEntry[];\n}\n\nexport interface DevMenuProps<R extends string, P extends string> {\n /**\n * Draw anything at all. The app passes `import.meta.env.DEV` AND its probe of the dev\n * route (§2.7.1): the constant keeps the menu off a production build's screen, the\n * probe off a dev build whose server has the route shut (§6.1) — flasks with nothing\n * behind them. Neither takes code out of the bundle; see `sections`.\n */\n enabled: boolean;\n roles?: readonly R[];\n /** The role the server enforces now. Named `currentRole`, not `role`, because `role` on\n * a JSX element is the ARIA role (as `RoleChip`'s `value`). */\n currentRole?: R;\n /** Apply a role — the app calls server-kit's `POST /dev/switch {role}` and re-reads\n * `/auth/me` (§2.7, §6.2). */\n onRole?: (role: R) => void;\n plans?: readonly P[];\n /** The plan the server enforces now. Left out (not known yet — kastlan's overview is a\n * 404 while billing is off), the plan group still renders, with no row checked; a\n * `GET /dev/switch` answers it. */\n currentPlan?: P;\n /** Apply a plan — `POST /dev/switch {plan}`, then the overview (§6.3). */\n onPlan?: (plan: P) => void;\n /**\n * The role to offer \"Back to {role}\" for. Left out, the kit keeps it itself: the role\n * before the first switch of this browser session, in `sessionStorage` under\n * `storageKey` — the server answers with the NEW role after a switch, so only the\n * browser still knows the old one.\n */\n originalRole?: R;\n /** The `sessionStorage` key for the kept role. Default {@link DEV_ORIGINAL_ROLE_KEY};\n * an app with several accounts on one browser may scope it per user. */\n storageKey?: string;\n /** Billing is off locally: the plan group says plans change nothing, and stays usable\n * (§6.3). */\n billingOff?: boolean;\n /** A role's or a plan's words in the menu. Default: the value itself. */\n roleLabel?: (role: R) => ReactNode;\n planLabel?: (plan: P) => ReactNode;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { dev } }}>` > English. */\n labels?: LabelOverride<DevSwitchersLabels>;\n /** 0.51: the app's own dev groups, after the kit's role and plan, in this order. Their\n * DEFINITIONS belong in a module only a dev build imports (§2.7.1): `enabled={false}`\n * hides the menu, but the code of a section built inline stays in the bundle. */\n sections?: readonly DevMenuSection[];\n /**\n * 0.51: an override is on that the developer could forget — a \"today\" that is not\n * today, a simulated slow server. The flask wears a dot, and its name says so (\"Dev\n * tools · Role: ADMIN · Plan: free Overrides on\"), since a dot alone tells a screen\n * reader nothing. The app decides what counts; the kit cannot see into its sections.\n */\n active?: boolean;\n}\n\nconst flask = <FlaskConical className=\"size-5 text-warning\" />;\n\n/** A label's text for a sentence, or the raw value when the label is not text. */\nconst textOf = (label: ReactNode, value: string) =>\n typeof label === \"string\" || typeof label === \"number\" ? String(label) : value;\n\n/** A row someone can pick — what makes a menu worth drawing. */\nconst isRow = (entry: TopBarMenuEntry) => entry.kind !== \"heading\" && entry.kind !== \"divider\";\n\n/**\n * The top bar's local-dev menu (docs/top-bar-harmonization.md §2.7, §2.7.1, §6.2, §6.3):\n * ONE amber flask, `RoleSwitcher`'s, over the kit's Account section and the app's own.\n *\n * 0.42's `DevSwitchers` drew a flask per switcher and had no place for anything else,\n * and an app has more dev controls than a role and a plan — keksdose's budget role, a\n * \"today\" override, resets (agreed with keksdose for 0.51). So the kit's part is now the\n * menu, and the app's rows go in it.\n *\n * - **The Account section** comes first: the role group, ending in \"Back to {role}\" once\n * the role differs from the one the session started with (the dev route needs only a\n * signed-in user, so a switch to a lesser role can always switch back); then the plan\n * group, which with `billingOff` notes that plans change nothing. Each renders only\n * with its list and handler.\n * - **The app's `sections`** follow, each a group under its own heading.\n * - **`active`** puts a dot on the flask for an override that is easy to forget.\n *\n * Nothing renders unless `enabled`, and nothing with no row to show. The panel scrolls\n * (`TopBarActionMenu`'s `scrollable`): some 25 rows ran off a 390×844 phone.\n *\n * From `md` up the groups stand side by side, one column each, in that order (0.51.4,\n * `TopBarActionMenu`'s `columns`; keksdose dev #640, Marcel: \"Each group gets a column\").\n * \"Back to {role}\" stays under the roles. A desktop too narrow for all of them wraps\n * the columns onto a second row; a phone keeps the one scrolling column.\n */\nexport function DevMenu<R extends string, P extends string>({\n enabled,\n roles,\n currentRole: role,\n onRole,\n plans,\n currentPlan: plan,\n onPlan,\n originalRole,\n storageKey = DEV_ORIGINAL_ROLE_KEY,\n billingOff = false,\n roleLabel = (r) => r,\n planLabel = (p) => p,\n labels: labelsProp,\n sections = [],\n active = false,\n}: DevMenuProps<R, P>) {\n const bar = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n const words = useTopBarPartsLabels(labelsProp ? { dev: labelsProp } : undefined).dev;\n const [kept, setKept] = useState<string | null>(() => (enabled ? readKey(storageKey) : null));\n // 0.43 (keksdose): `enabled` may start false and turn true — an app's GET probe of the\n // dev route — so the kept role is read again then, or \"Back to {role}\" is lost after a\n // reload. Adjusted during render, React's pattern for state that follows a prop.\n const [wasEnabled, setWasEnabled] = useState(enabled);\n if (wasEnabled !== enabled) {\n setWasEnabled(enabled);\n if (enabled) setKept(readKey(storageKey));\n }\n if (!enabled) return null;\n\n const original = (originalRole ?? kept ?? undefined) as R | undefined;\n\n const pickRole = (next: R) => {\n if (originalRole === undefined) {\n if (next === original) {\n // Back where the session began: nothing left to go back to.\n writeKey(storageKey, null);\n setKept(null);\n } else if (kept === null && role !== undefined && next !== role) {\n writeKey(storageKey, role);\n setKept(role);\n }\n }\n onRole?.(next);\n };\n\n const roleGroup: TopBarMenuEntry[] =\n roles && roles.length > 0 && role !== undefined && onRole\n ? [\n { kind: \"heading\", key: \"head-role\", label: words.roleHeading },\n ...roles.map((r): TopBarMenuEntry => ({\n key: `role-${r}`,\n label: roleLabel(r),\n checked: r === role,\n onSelect: () => pickRole(r),\n })),\n // Behind a rule, which ends the role group: \"Back to\" is a way out, not a role.\n ...(original !== undefined && original !== role\n ? ([\n { kind: \"divider\", key: \"sep-back\" },\n {\n key: \"back\",\n icon: <Undo2 className=\"size-4\" />,\n label: words.backTo(textOf(roleLabel(original), original)),\n onSelect: () => pickRole(original),\n },\n ] satisfies TopBarMenuEntry[])\n : []),\n ]\n : [];\n\n const planGroup: TopBarMenuEntry[] =\n plans && plans.length > 0 && onPlan\n ? [\n {\n kind: \"heading\",\n key: \"head-plan\",\n // The billing-off note is part of the group's NAME, under the heading. In 0.42's\n // own plan menu it was a trailing `<li>`: not a row, so the arrow keys passed\n // over it and a screen reader moving through the menu never reached it. Named,\n // it is read on the way into the plans. And in one menu a trailing note would\n // sit under the app's sections, not under the plans.\n label: billingOff ? (\n <>\n {words.planHeading}{\" \"}\n <span\n data-dev-billing-off=\"\"\n className=\"mt-0.5 block font-normal normal-case tracking-normal text-xs text-muted\"\n >\n {words.billingOff}\n </span>\n </>\n ) : (\n words.planHeading\n ),\n },\n ...plans.map((p): TopBarMenuEntry => ({\n key: `plan-${p}`,\n label: planLabel(p),\n checked: p === plan,\n onSelect: () => onPlan(p),\n })),\n ]\n : [];\n\n // The app's rows keyed under their section: two sections may well both have a \"reset\".\n const appGroups = sections.flatMap((section): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `section:${section.key}`, label: section.title },\n ...section.entries.map((entry): TopBarMenuEntry => ({ ...entry, key: `${section.key}:${entry.key}` })),\n ]);\n\n const entries = [...roleGroup, ...planGroup, ...appGroups];\n if (!entries.some(isRow)) return null;\n\n // The menu, then the role and the plan, as 0.42's two triggers named them: the bar\n // shows the flask only, so its name (and tooltip) is where they are read.\n const name = [\n words.menu,\n roleGroup.length > 0 && role !== undefined ? bar.role(role) : null,\n planGroup.length > 0 && plan !== undefined ? words.plan(plan) : null,\n ]\n .filter((part): part is string => part !== null)\n .join(\" · \");\n\n return (\n <TopBarActionMenu\n icon={flask}\n // The badge's words join the trigger's name, as the tours compass's NEW dot does.\n // `danger`, the badge's default: an amber dot would vanish into the amber flask.\n iconBadge={active ? { label: words.overridesOn } : null}\n aria-label={name}\n entries={entries}\n scrollable\n columns\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/option-switcher-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\nimport { Tooltip } from \"../components/tooltip\";\n\nexport interface OptionSwitcherOption<T extends string> {\n value: T;\n label: ReactNode;\n}\n\n/**\n * A top-bar hover menu that switches between a small set of options, with a\n * check on the active one and an optional heading. Generic glue over\n * {@link HoverMenu} — e.g. a dev role/impersonation switcher, an environment\n * picker. Controlled: the app owns `value` and `onSelect`.\n */\nexport function OptionSwitcherMenu<T extends string>({\n icon,\n \"aria-label\": ariaLabel,\n title,\n heading,\n options,\n value,\n onSelect,\n}: {\n icon: ReactNode;\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 /** The trigger's tooltip. Default: `aria-label` (0.48) — the name, shown on hover and\n * focus. A different text also describes the trigger (`aria-describedby`). */\n title?: string;\n heading?: string;\n options: OptionSwitcherOption<T>[];\n value: T;\n onSelect: (value: T) => void;\n}) {\n return (\n <HoverMenu\n aria-label={ariaLabel}\n trigger={({ toggle }) => {\n const button = (\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n );\n // The name on hover and focus even without a `title` (0.48, keksdose dev#610). A\n // bubble that repeats the name sits round the button in a fragment, as IconButton's\n // does, so it does not describe the button by its own name — a reader heard \"Text\n // size, button, Text size\". A `title` that says more still describes it.\n const tip = title ?? ariaLabel;\n return (\n <Tooltip label={tip} portal>\n {tip === ariaLabel ? <>{button}</> : button}\n </Tooltip>\n );\n }}\n >\n {(close) => (\n <ul className=\"py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\"
|
|
1
|
+
{"version":3,"sources":["../../src/shell/option-switcher-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\nimport { Tooltip } from \"../components/tooltip\";\n\nexport interface OptionSwitcherOption<T extends string> {\n value: T;\n label: ReactNode;\n}\n\n/**\n * A top-bar hover menu that switches between a small set of options, with a\n * check on the active one and an optional heading. Generic glue over\n * {@link HoverMenu} — e.g. a dev role/impersonation switcher, an environment\n * picker. Controlled: the app owns `value` and `onSelect`.\n */\nexport function OptionSwitcherMenu<T extends string>({\n icon,\n \"aria-label\": ariaLabel,\n title,\n heading,\n options,\n value,\n onSelect,\n}: {\n icon: ReactNode;\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 /** The trigger's tooltip. Default: `aria-label` (0.48) — the name, shown on hover and\n * focus. A different text also describes the trigger (`aria-describedby`). */\n title?: string;\n heading?: string;\n options: OptionSwitcherOption<T>[];\n value: T;\n onSelect: (value: T) => void;\n}) {\n return (\n <HoverMenu\n aria-label={ariaLabel}\n trigger={({ toggle }) => {\n const button = (\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n );\n // The name on hover and focus even without a `title` (0.48, keksdose dev#610). A\n // bubble that repeats the name sits round the button in a fragment, as IconButton's\n // does, so it does not describe the button by its own name — a reader heard \"Text\n // size, button, Text size\". A `title` that says more still describes it.\n const tip = title ?? ariaLabel;\n return (\n <Tooltip label={tip} portal>\n {tip === ariaLabel ? <>{button}</> : button}\n </Tooltip>\n );\n }}\n >\n {(close) => (\n <ul className=\"py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\">{heading}</li>\n )}\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(opt.value);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span>{opt.label}</span>\n {value === opt.value && <Check className=\"size-4 text-secondary\" />}\n </button>\n </li>\n ))}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":";AA2CU,SAWuB,UAXvB,KAuBI,YAvBJ;AA1CV,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAC7D,SAAS,eAAe;AAajB,SAAS,mBAAqC;AAAA,EACnD;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAYG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY;AAAA,MACZ,SAAS,CAAC,EAAE,OAAO,MAAM;AACvB,cAAM,SACJ,oBAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH;AAMF,cAAM,MAAM,SAAS;AACrB,eACE,oBAAC,WAAQ,OAAO,KAAK,QAAM,MACxB,kBAAQ,YAAY,gCAAG,kBAAO,IAAM,QACvC;AAAA,MAEJ;AAAA,MAEC,WAAC,UACA,qBAAC,QAAG,WAAU,QACX;AAAA,mBACC,oBAAC,QAAG,WAAU,+EAA+E,mBAAQ;AAAA,QAEtG,QAAQ,IAAI,CAAC,QACZ,oBAAC,QACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM;AACb,uBAAS,IAAI,KAAK;AAClB,oBAAM;AAAA,YACR;AAAA,YACA,WAAW;AAAA,YAEX;AAAA,kCAAC,UAAM,cAAI,OAAM;AAAA,cAChB,UAAU,IAAI,SAAS,oBAAC,SAAM,WAAU,yBAAwB;AAAA;AAAA;AAAA,QACnE,KAXO,IAAI,KAYb,CACD;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
|
|
@@ -62,10 +62,7 @@ function SyncStatusIndicator({
|
|
|
62
62
|
};
|
|
63
63
|
}, []);
|
|
64
64
|
const onlineOnly = storage === "none";
|
|
65
|
-
const state = syncState(
|
|
66
|
-
onlineOnly ? { ...status, pending: 0, blocked: 0, waitingForPlan: 0 } : status,
|
|
67
|
-
refreshing
|
|
68
|
-
);
|
|
65
|
+
const state = syncState(onlineOnly ? { ...status, pending: 0, blocked: 0, waitingForPlan: 0 } : status, refreshing);
|
|
69
66
|
const data = dataAttributes(rest);
|
|
70
67
|
if (hideWhenIdle && state === "synced") return null;
|
|
71
68
|
const { Icon, tone } = LOOK[state];
|
|
@@ -142,7 +139,10 @@ function SyncStatusIndicator({
|
|
|
142
139
|
...data,
|
|
143
140
|
role: "status",
|
|
144
141
|
"data-sync-state": state,
|
|
145
|
-
className: cn(
|
|
142
|
+
className: cn(
|
|
143
|
+
"relative inline-flex size-9 shrink-0 items-center justify-center [&>svg]:size-5",
|
|
144
|
+
TEXT_TONE[tone]
|
|
145
|
+
),
|
|
146
146
|
children: [
|
|
147
147
|
glyph,
|
|
148
148
|
/* @__PURE__ */ jsx("span", { className: "sr-only", children: name }),
|
|
@@ -157,7 +157,8 @@ function SyncStatusIndicator({
|
|
|
157
157
|
]
|
|
158
158
|
}
|
|
159
159
|
) });
|
|
160
|
-
if (!showLabel || !short)
|
|
160
|
+
if (!showLabel || !short)
|
|
161
|
+
return className ? /* @__PURE__ */ jsx("span", { className: cn("inline-flex", className), children: control }) : control;
|
|
161
162
|
return /* @__PURE__ */ jsxs("span", { className: cn("inline-flex items-center gap-1", className), children: [
|
|
162
163
|
control,
|
|
163
164
|
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("whitespace-nowrap text-xs font-medium", TEXT_TONE[tone]), children: short })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/sync-status-indicator.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ComponentType } from \"react\";\nimport { Check, Cloud, CloudAlert, CloudOff, RefreshCw, UploadCloud } from \"lucide-react\";\nimport type { LucideProps } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { IconButton } from \"../components/icon-button\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { useBillingLabels } from \"../billing/billing-labels\";\nimport { useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTopBarPartsLabels, type SyncStatusLabels } from \"./top-bar-parts-labels\";\n\n/**\n * What the indicator shows, fed by the app's sync engine (docs/top-bar-harmonization.md\n * §2.6). Small on purpose: keksdose's sync-status store, kastlan's handover controller and\n * Kurvenschmiede's online state + `useIsMutating` all fill it.\n */\nexport interface SyncStatus {\n /** What the engine is doing. `offline` is the engine's own verdict; `online: false` is\n * the browser's — either shows the offline state. */\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n /** The browser's `navigator.onLine` (or the app's better guess). */\n online: boolean;\n /** Writes kept on this device and not yet sent. */\n pending: number;\n /** Writes this device cannot send at all (keksdose: no key for the budget). Ranked first. */\n blocked?: number;\n /** Writes the server refused as read-only and keeps until a plan (billing §12.5). */\n waitingForPlan?: number;\n /** When the last good round ended — epoch ms, an ISO string or a `Date`. */\n lastSyncedAt?: number | string | Date | null;\n}\n\n/** The seven states, in their ranking: the first that holds wins. */\nexport type SyncState = \"blocked\" | \"waitingForPlan\" | \"offline\" | \"error\" | \"syncing\" | \"pending\" | \"synced\";\n\n/**\n * Which of the seven states a {@link SyncStatus} is in — keksdose's ranking\n * (sync-status-indicator.tsx): a write that cannot be sent at all ranks above everything,\n * because waiting cannot help it; then one waiting for a plan; then offline; then a refused\n * round; then a round under way; then writes waiting; else in sync. `syncing` also holds\n * while a tap's own round runs (`refreshing`), before the engine publishes its phase.\n */\nexport function syncState(status: SyncStatus, refreshing = false): SyncState {\n if ((status.blocked ?? 0) > 0) return \"blocked\";\n if ((status.waitingForPlan ?? 0) > 0) return \"waitingForPlan\";\n if (!status.online || status.phase === \"offline\") return \"offline\";\n if (status.phase === \"error\") return \"error\";\n if (status.phase === \"syncing\" || refreshing) return \"syncing\";\n if (status.pending > 0) return \"pending\";\n return \"synced\";\n}\n\ntype Tone = \"danger\" | \"warning\" | \"info\" | \"success\";\n\nconst LOOK: Record<SyncState, { Icon: ComponentType<LucideProps>; tone: Tone }> = {\n // The one the user must act on: the kit's danger, where keksdose had rose.\n blocked: { Icon: CloudAlert, tone: \"danger\" },\n // Safe, and a payment sends them: amber, the kit's warning.\n waitingForPlan: { Icon: CloudAlert, tone: \"warning\" },\n offline: { Icon: CloudOff, tone: \"warning\" },\n // Usually resolves by itself (a deploy, a read-only view): amber, not danger.\n error: { Icon: CloudAlert, tone: \"warning\" },\n syncing: { Icon: RefreshCw, tone: \"info\" },\n pending: { Icon: UploadCloud, tone: \"warning\" },\n synced: { Icon: Cloud, tone: \"success\" },\n};\n\nconst TEXT_TONE: Record<Tone, string> = {\n danger: \"text-danger\",\n warning: \"text-warning\",\n info: \"text-info\",\n success: \"text-success\",\n};\n\n/** The scope words of an indicator that covers only part of the app (§6.10). */\nexport interface SyncStatusScope {\n /** Put before every state in the tooltip: \"Handovers on this device\" →\n * \"Handovers on this device: 3 changes waiting to sync\". */\n name: string;\n /** The whole offline sentence instead of the kit's, when offline means something\n * narrower: \"Offline: handovers keep working on this device; everything else needs a\n * connection\". */\n offline?: string;\n}\n\nexport interface SyncStatusIndicatorProps {\n status: SyncStatus;\n /**\n * Run a round now (push, then pull, then re-read what is on screen). With it the\n * indicator is a \"sync now\" button, disabled offline and mid-round but keeping its\n * colour; a returned promise keeps the spin until it settles. Without it, a status only\n * (§6.9).\n */\n onSync?: () => void | Promise<unknown>;\n /** Render nothing while in sync — kastlan and Kurvenschmiede show it only while offline,\n * syncing or with changes waiting, held or refused (§2.6). */\n hideWhenIdle?: boolean;\n /** The app's words for its part of the app (§6.10). */\n scope?: SyncStatusScope;\n /**\n * Where the app keeps a change before the server has it. `\"device\"` (the default): an\n * offline-first app (keksdose, kastlan's handovers) — offline means \"kept here, sent\n * later\". `\"none\"`: an online-only app that keeps nothing on the device (Kurvenschmiede,\n * build review B1) — offline reads \"changes can't be saved until you reconnect\", a\n * round reads \"Saving…\", and `pending`, `blocked` and `waitingForPlan` are ignored,\n * since there is nowhere for a change to wait. \"Up to date\" when settled.\n */\n storage?: \"device\" | \"none\";\n /** Short words beside the cloud (\"Offline\", \"Syncing…\", \"3 waiting\") while not in sync. */\n showLabel?: boolean;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { sync } }}>` > English. */\n labels?: LabelOverride<SyncStatusLabels>;\n /** The clock's language for \"Synced at 14:05\". Default: the provider's locale. */\n locale?: string;\n className?: string;\n /** `data-*` attributes go on the trigger — a tour's anchor (`data-tour=\"sync-status\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** The `data-*` attributes of a rest object, for a trigger. */\nfunction dataAttributes(rest: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith(\"data-\")));\n}\n\n/**\n * The top bar's sync status (docs/top-bar-harmonization.md §2.6), lifted from keksdose's\n * `SyncStatusIndicator`: the seven states and their ranking ({@link syncState}), one cloud\n * per state, a ✓ badge when in sync and a spin while syncing. With `onSync` it is also the\n * \"sync now\" button, and its name says both what it is and what a tap does.\n *\n * Colours are the kit's roles — danger for a write that cannot be sent, warning for\n * anything waiting, info while syncing, success when in sync — where keksdose had raw\n * rose/amber/sky/emerald pairs.\n */\nexport function SyncStatusIndicator({\n status,\n onSync,\n hideWhenIdle = false,\n scope,\n showLabel = false,\n labels: labelsProp,\n locale: localeProp,\n className,\n storage = \"device\",\n ...rest\n}: SyncStatusIndicatorProps) {\n const words = useTopBarPartsLabels(labelsProp ? { sync: labelsProp } : undefined).sync;\n const billing = useBillingLabels();\n const locale = useKitLocale(localeProp);\n // The tap's own round, local rather than in the app's store: the store's phase belongs\n // to the sync engine, and a second writer would race the round itself.\n const [refreshing, setRefreshing] = useState(false);\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const onlineOnly = storage === \"none\";\n // Online-only: nothing waits on the device, so only offline, error, saving and settled.\n const state = syncState(\n onlineOnly ? { ...status, pending: 0, blocked: 0, waitingForPlan: 0 } : status,\n refreshing,\n );\n const data = dataAttributes(rest);\n if (hideWhenIdle && state === \"synced\") return null;\n\n const { Icon, tone } = LOOK[state];\n const spin = state === \"syncing\";\n let text: string;\n switch (state) {\n case \"blocked\":\n text = words.blocked(status.blocked ?? 0);\n break;\n case \"waitingForPlan\":\n text = billing.waitingChanges(status.waitingForPlan ?? 0);\n break;\n case \"offline\":\n text = onlineOnly ? words.offlineUnsaved : words.offline;\n break;\n case \"error\":\n text = words.refused;\n break;\n case \"syncing\":\n text = onlineOnly ? words.saving : words.syncing;\n break;\n case \"pending\":\n text = words.pending(status.pending);\n break;\n default:\n text = !onlineOnly && status.lastSyncedAt\n ? words.syncedAt(\n // A clock time in the UI's language, never the browser's (keksdose live #246).\n new Date(status.lastSyncedAt).toLocaleTimeString(locale, { hour: \"2-digit\", minute: \"2-digit\" }),\n )\n : words.idle;\n }\n if (scope) {\n text = state === \"offline\" && scope.offline ? scope.offline : words.scoped(scope.name, text);\n }\n\n const short =\n state === \"offline\"\n ? words.offlineShort\n : state === \"syncing\"\n ? onlineOnly\n ? words.saving\n : words.syncingShort\n : state === \"error\"\n ? words.refusedShort\n : state === \"pending\" || state === \"blocked\" || state === \"waitingForPlan\"\n ? words.waitingShort(\n state === \"pending\"\n ? status.pending\n : state === \"blocked\"\n ? (status.blocked ?? 0)\n : (status.waitingForPlan ?? 0),\n )\n : null;\n\n const offline = state === \"offline\";\n const canSync = !!onSync && status.online && status.phase !== \"offline\" && !spin;\n const name = onSync\n ? canSync\n ? words.withHint(text, words.syncHint)\n : offline\n ? words.withHint(text, words.offlineHint)\n : text\n : text;\n const badge = state === \"synced\" ? <Check className=\"stroke-[3]\" /> : undefined;\n const glyph = <Icon aria-hidden className={spin ? \"animate-spin\" : undefined} />;\n\n const sync = async () => {\n if (!canSync || !onSync) return;\n setRefreshing(true);\n try {\n await onSync();\n } finally {\n if (mounted.current) setRefreshing(false);\n }\n };\n\n const control = onSync ? (\n <IconButton\n {...data}\n data-sync-state={state}\n size=\"2xs\"\n glyphSize={20}\n shape=\"round\"\n // A status that is also an action: disabled (offline, mid-round) is exactly when\n // the state matters most, so it keeps its colour.\n disabledStyle=\"keep\"\n tone={tone}\n quiet={false}\n label={name}\n labelVisible={false}\n // Below: the bar is at the top edge, where a bubble above has no room.\n tooltipSide=\"bottom\"\n onClick={() => void sync()}\n disabled={!canSync}\n // It announces itself as a status: a reader hears the state change.\n aria-live=\"polite\"\n badge={badge}\n >\n {glyph}\n </IconButton>\n ) : (\n <Tooltip label={name} side=\"bottom\">\n <span\n {...data}\n role=\"status\"\n data-sync-state={state}\n className={cn(\"relative inline-flex size-9 shrink-0 items-center justify-center [&>svg]:size-5\", TEXT_TONE[tone])}\n >\n {glyph}\n <span className=\"sr-only\">{name}</span>\n {badge && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute bottom-1.5 end-1.5 inline-flex rounded-full bg-surface [&_svg]:size-3\"\n >\n {badge}\n </span>\n )}\n </span>\n </Tooltip>\n );\n\n if (!showLabel || !short) return className ? <span className={cn(\"inline-flex\", className)}>{control}</span> : control;\n return (\n <span className={cn(\"inline-flex items-center gap-1\", className)}>\n {control}\n {/* The eye's short words; the name above already says it in full. */}\n <span aria-hidden className={cn(\"whitespace-nowrap text-xs font-medium\", TEXT_TONE[tone])}>\n {short}\n </span>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/sync-status-indicator.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ComponentType } from \"react\";\nimport { Check, Cloud, CloudAlert, CloudOff, RefreshCw, UploadCloud } from \"lucide-react\";\nimport type { LucideProps } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { IconButton } from \"../components/icon-button\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { useBillingLabels } from \"../billing/billing-labels\";\nimport { useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTopBarPartsLabels, type SyncStatusLabels } from \"./top-bar-parts-labels\";\n\n/**\n * What the indicator shows, fed by the app's sync engine (docs/top-bar-harmonization.md\n * §2.6). Small on purpose: keksdose's sync-status store, kastlan's handover controller and\n * Kurvenschmiede's online state + `useIsMutating` all fill it.\n */\nexport interface SyncStatus {\n /** What the engine is doing. `offline` is the engine's own verdict; `online: false` is\n * the browser's — either shows the offline state. */\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n /** The browser's `navigator.onLine` (or the app's better guess). */\n online: boolean;\n /** Writes kept on this device and not yet sent. */\n pending: number;\n /** Writes this device cannot send at all (keksdose: no key for the budget). Ranked first. */\n blocked?: number;\n /** Writes the server refused as read-only and keeps until a plan (billing §12.5). */\n waitingForPlan?: number;\n /** When the last good round ended — epoch ms, an ISO string or a `Date`. */\n lastSyncedAt?: number | string | Date | null;\n}\n\n/** The seven states, in their ranking: the first that holds wins. */\nexport type SyncState = \"blocked\" | \"waitingForPlan\" | \"offline\" | \"error\" | \"syncing\" | \"pending\" | \"synced\";\n\n/**\n * Which of the seven states a {@link SyncStatus} is in — keksdose's ranking\n * (sync-status-indicator.tsx): a write that cannot be sent at all ranks above everything,\n * because waiting cannot help it; then one waiting for a plan; then offline; then a refused\n * round; then a round under way; then writes waiting; else in sync. `syncing` also holds\n * while a tap's own round runs (`refreshing`), before the engine publishes its phase.\n */\nexport function syncState(status: SyncStatus, refreshing = false): SyncState {\n if ((status.blocked ?? 0) > 0) return \"blocked\";\n if ((status.waitingForPlan ?? 0) > 0) return \"waitingForPlan\";\n if (!status.online || status.phase === \"offline\") return \"offline\";\n if (status.phase === \"error\") return \"error\";\n if (status.phase === \"syncing\" || refreshing) return \"syncing\";\n if (status.pending > 0) return \"pending\";\n return \"synced\";\n}\n\ntype Tone = \"danger\" | \"warning\" | \"info\" | \"success\";\n\nconst LOOK: Record<SyncState, { Icon: ComponentType<LucideProps>; tone: Tone }> = {\n // The one the user must act on: the kit's danger, where keksdose had rose.\n blocked: { Icon: CloudAlert, tone: \"danger\" },\n // Safe, and a payment sends them: amber, the kit's warning.\n waitingForPlan: { Icon: CloudAlert, tone: \"warning\" },\n offline: { Icon: CloudOff, tone: \"warning\" },\n // Usually resolves by itself (a deploy, a read-only view): amber, not danger.\n error: { Icon: CloudAlert, tone: \"warning\" },\n syncing: { Icon: RefreshCw, tone: \"info\" },\n pending: { Icon: UploadCloud, tone: \"warning\" },\n synced: { Icon: Cloud, tone: \"success\" },\n};\n\nconst TEXT_TONE: Record<Tone, string> = {\n danger: \"text-danger\",\n warning: \"text-warning\",\n info: \"text-info\",\n success: \"text-success\",\n};\n\n/** The scope words of an indicator that covers only part of the app (§6.10). */\nexport interface SyncStatusScope {\n /** Put before every state in the tooltip: \"Handovers on this device\" →\n * \"Handovers on this device: 3 changes waiting to sync\". */\n name: string;\n /** The whole offline sentence instead of the kit's, when offline means something\n * narrower: \"Offline: handovers keep working on this device; everything else needs a\n * connection\". */\n offline?: string;\n}\n\nexport interface SyncStatusIndicatorProps {\n status: SyncStatus;\n /**\n * Run a round now (push, then pull, then re-read what is on screen). With it the\n * indicator is a \"sync now\" button, disabled offline and mid-round but keeping its\n * colour; a returned promise keeps the spin until it settles. Without it, a status only\n * (§6.9).\n */\n onSync?: () => void | Promise<unknown>;\n /** Render nothing while in sync — kastlan and Kurvenschmiede show it only while offline,\n * syncing or with changes waiting, held or refused (§2.6). */\n hideWhenIdle?: boolean;\n /** The app's words for its part of the app (§6.10). */\n scope?: SyncStatusScope;\n /**\n * Where the app keeps a change before the server has it. `\"device\"` (the default): an\n * offline-first app (keksdose, kastlan's handovers) — offline means \"kept here, sent\n * later\". `\"none\"`: an online-only app that keeps nothing on the device (Kurvenschmiede,\n * build review B1) — offline reads \"changes can't be saved until you reconnect\", a\n * round reads \"Saving…\", and `pending`, `blocked` and `waitingForPlan` are ignored,\n * since there is nowhere for a change to wait. \"Up to date\" when settled.\n */\n storage?: \"device\" | \"none\";\n /** Short words beside the cloud (\"Offline\", \"Syncing…\", \"3 waiting\") while not in sync. */\n showLabel?: boolean;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { sync } }}>` > English. */\n labels?: LabelOverride<SyncStatusLabels>;\n /** The clock's language for \"Synced at 14:05\". Default: the provider's locale. */\n locale?: string;\n className?: string;\n /** `data-*` attributes go on the trigger — a tour's anchor (`data-tour=\"sync-status\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** The `data-*` attributes of a rest object, for a trigger. */\nfunction dataAttributes(rest: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith(\"data-\")));\n}\n\n/**\n * The top bar's sync status (docs/top-bar-harmonization.md §2.6), lifted from keksdose's\n * `SyncStatusIndicator`: the seven states and their ranking ({@link syncState}), one cloud\n * per state, a ✓ badge when in sync and a spin while syncing. With `onSync` it is also the\n * \"sync now\" button, and its name says both what it is and what a tap does.\n *\n * Colours are the kit's roles — danger for a write that cannot be sent, warning for\n * anything waiting, info while syncing, success when in sync — where keksdose had raw\n * rose/amber/sky/emerald pairs.\n */\nexport function SyncStatusIndicator({\n status,\n onSync,\n hideWhenIdle = false,\n scope,\n showLabel = false,\n labels: labelsProp,\n locale: localeProp,\n className,\n storage = \"device\",\n ...rest\n}: SyncStatusIndicatorProps) {\n const words = useTopBarPartsLabels(labelsProp ? { sync: labelsProp } : undefined).sync;\n const billing = useBillingLabels();\n const locale = useKitLocale(localeProp);\n // The tap's own round, local rather than in the app's store: the store's phase belongs\n // to the sync engine, and a second writer would race the round itself.\n const [refreshing, setRefreshing] = useState(false);\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const onlineOnly = storage === \"none\";\n // Online-only: nothing waits on the device, so only offline, error, saving and settled.\n const state = syncState(onlineOnly ? { ...status, pending: 0, blocked: 0, waitingForPlan: 0 } : status, refreshing);\n const data = dataAttributes(rest);\n if (hideWhenIdle && state === \"synced\") return null;\n\n const { Icon, tone } = LOOK[state];\n const spin = state === \"syncing\";\n let text: string;\n switch (state) {\n case \"blocked\":\n text = words.blocked(status.blocked ?? 0);\n break;\n case \"waitingForPlan\":\n text = billing.waitingChanges(status.waitingForPlan ?? 0);\n break;\n case \"offline\":\n text = onlineOnly ? words.offlineUnsaved : words.offline;\n break;\n case \"error\":\n text = words.refused;\n break;\n case \"syncing\":\n text = onlineOnly ? words.saving : words.syncing;\n break;\n case \"pending\":\n text = words.pending(status.pending);\n break;\n default:\n text =\n !onlineOnly && status.lastSyncedAt\n ? words.syncedAt(\n // A clock time in the UI's language, never the browser's (keksdose live #246).\n new Date(status.lastSyncedAt).toLocaleTimeString(locale, { hour: \"2-digit\", minute: \"2-digit\" }),\n )\n : words.idle;\n }\n if (scope) {\n text = state === \"offline\" && scope.offline ? scope.offline : words.scoped(scope.name, text);\n }\n\n const short =\n state === \"offline\"\n ? words.offlineShort\n : state === \"syncing\"\n ? onlineOnly\n ? words.saving\n : words.syncingShort\n : state === \"error\"\n ? words.refusedShort\n : state === \"pending\" || state === \"blocked\" || state === \"waitingForPlan\"\n ? words.waitingShort(\n state === \"pending\"\n ? status.pending\n : state === \"blocked\"\n ? (status.blocked ?? 0)\n : (status.waitingForPlan ?? 0),\n )\n : null;\n\n const offline = state === \"offline\";\n const canSync = !!onSync && status.online && status.phase !== \"offline\" && !spin;\n const name = onSync\n ? canSync\n ? words.withHint(text, words.syncHint)\n : offline\n ? words.withHint(text, words.offlineHint)\n : text\n : text;\n const badge = state === \"synced\" ? <Check className=\"stroke-[3]\" /> : undefined;\n const glyph = <Icon aria-hidden className={spin ? \"animate-spin\" : undefined} />;\n\n const sync = async () => {\n if (!canSync || !onSync) return;\n setRefreshing(true);\n try {\n await onSync();\n } finally {\n if (mounted.current) setRefreshing(false);\n }\n };\n\n const control = onSync ? (\n <IconButton\n {...data}\n data-sync-state={state}\n size=\"2xs\"\n glyphSize={20}\n shape=\"round\"\n // A status that is also an action: disabled (offline, mid-round) is exactly when\n // the state matters most, so it keeps its colour.\n disabledStyle=\"keep\"\n tone={tone}\n quiet={false}\n label={name}\n labelVisible={false}\n // Below: the bar is at the top edge, where a bubble above has no room.\n tooltipSide=\"bottom\"\n onClick={() => void sync()}\n disabled={!canSync}\n // It announces itself as a status: a reader hears the state change.\n aria-live=\"polite\"\n badge={badge}\n >\n {glyph}\n </IconButton>\n ) : (\n <Tooltip label={name} side=\"bottom\">\n <span\n {...data}\n role=\"status\"\n data-sync-state={state}\n className={cn(\n \"relative inline-flex size-9 shrink-0 items-center justify-center [&>svg]:size-5\",\n TEXT_TONE[tone],\n )}\n >\n {glyph}\n <span className=\"sr-only\">{name}</span>\n {badge && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute bottom-1.5 end-1.5 inline-flex rounded-full bg-surface [&_svg]:size-3\"\n >\n {badge}\n </span>\n )}\n </span>\n </Tooltip>\n );\n\n if (!showLabel || !short)\n return className ? <span className={cn(\"inline-flex\", className)}>{control}</span> : control;\n return (\n <span className={cn(\"inline-flex items-center gap-1\", className)}>\n {control}\n {/* The eye's short words; the name above already says it in full. */}\n <span aria-hidden className={cn(\"whitespace-nowrap text-xs font-medium\", TEXT_TONE[tone])}>\n {short}\n </span>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/top-bar-parts-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The `topBarParts` namespace (0.42, docs/top-bar-harmonization.md §3.1): the words of the\n * top bar's shared parts — `TourMenu`, `SyncStatusIndicator` and `DevMenu` (0.42's\n * `DevSwitchers`, whose words these still are, §2.7.1). A module\n * of words with no component code, like every `*-labels.ts` (0.34, KS U8). The language\n * sub-list (`AccountMenuLanguage`) names itself with `topBar.language`, and the role in the\n * dev flask's name is `topBar.role`, as `RoleSwitcher`'s tooltip.\n */\n\n/** `TourMenu`'s words (§2.4). */\nexport interface TourMenuLabels {\n /** The compass's name, and the menu's. */\n menu: string;\n /** The NEW dot on the compass, for a reader: it joins the trigger's name. */\n newDot: string;\n /** The heading over the tours that start on the open page. */\n thisPage: string;\n /** The heading over the untaken new tours. */\n newTours: string;\n /** The last row, the way to the app's tours page (only with `href`). */\n allTours: string;\n /** The chip on a new tour's row. */\n badgeNew: string;\n /** What the ✓ on a taken tour's row says to a reader. */\n completed: string;\n /** The chip on a tour reset by an update (0.43, docs/tours-harmonization.md §8). */\n badgeUpdated: string;\n /** The dot on the compass while only updated tours light it, for a reader. */\n updatedDot: string;\n /** The heading over the untaken new tours when updated ones are among them. */\n newAndUpdated: string;\n}\n\n/** `SyncStatusIndicator`'s words (§2.6). The \"waiting for a plan\" state is\n * `billing.waitingChanges`, the kit's words for it since billing §12.5. */\nexport interface SyncStatusLabels {\n /** Writes this device cannot send (keksdose: no key for the budget). Ranked first. */\n blocked: (count: number) => string;\n offline: string;\n /** The server refused the last round. */\n refused: string;\n syncing: string;\n pending: (count: number) => string;\n /** Settled, with the time of the last good round (already formatted). */\n syncedAt: (time: string) => string;\n /** Settled, before the first round. */\n idle: string;\n /** Joined to the state when a tap syncs: what the tap does. */\n syncHint: string;\n /** Joined to the state while offline, when a tap can do nothing. */\n offlineHint: string;\n /** The state and what a tap does, as one tooltip: \"Offline — nothing to sync…\". */\n withHint: (state: string, hint: string) => string;\n /** The state with the app's `scope` words before it: \"Handovers on this device: …\". */\n scoped: (scope: string, state: string) => string;\n /** The short words beside the cloud (`showLabel`): offline. */\n offlineShort: string;\n /** The short words while syncing. */\n syncingShort: string;\n /** The short words while changes wait (pending, held for a plan, or blocked). */\n waitingShort: (count: number) => string;\n /** The short words after a refused round. */\n refusedShort: string;\n /** Offline in an online-only app (`storage=\"none\"`): nothing is kept on the device. */\n offlineUnsaved: string;\n /** A round under way in an online-only app — it saves, it doesn't sync. Also its short\n * words beside the cloud. */\n saving: string;\n}\n\n/**\n * `DevMenu`'s words (§2.7, §2.7.1, §6.2, §6.3). Still named for 0.42's `DevSwitchers`: the\n * namespace is the same `topBarParts.dev`, and an app's label tree keeps compiling.\n */\nexport interface DevSwitchersLabels {\n /** 0.51: the flask's name, and the menu's — the role and the plan follow it. */\n menu: string;\n /** 0.51: what the dot on the flask means while `active`, joined to its name — an\n * override the developer could forget is on (a \"today\", a slow server). */\n overridesOn: string;\n /** The heading of the role group. */\n roleHeading: string;\n /** @deprecated 0.51 — DevMenu's one flask is named by `menu`; removed in 0.52. */\n switchPlan: string;\n /** The plan in the flask's name, given the active plan. */\n plan: (value: string) => string;\n /** The heading of the plan group. */\n planHeading: string;\n /** The row back to the role the session started with. */\n backTo: (role: string) => string;\n /** The plan group's note while billing is off locally. */\n billingOff: string;\n}\n\nexport interface TopBarPartsLabels {\n tours: TourMenuLabels;\n sync: SyncStatusLabels;\n dev: DevSwitchersLabels;\n}\n\nexport const DEFAULT_TOP_BAR_PARTS_LABELS: TopBarPartsLabels = {\n tours: {\n menu: \"Guided tours\",\n newDot: \"New tours\",\n thisPage: \"On this page\",\n newTours: \"New tours\",\n allTours: \"All tours…\",\n badgeNew: \"New\",\n completed: \"Completed\",\n badgeUpdated: \"Updated\",\n updatedDot: \"Updated tours\",\n newAndUpdated: \"New and updated\",\n },\n sync: {\n blocked: (count) =>\n count === 1
|
|
1
|
+
{"version":3,"sources":["../../src/shell/top-bar-parts-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The `topBarParts` namespace (0.42, docs/top-bar-harmonization.md §3.1): the words of the\n * top bar's shared parts — `TourMenu`, `SyncStatusIndicator` and `DevMenu` (0.42's\n * `DevSwitchers`, whose words these still are, §2.7.1). A module\n * of words with no component code, like every `*-labels.ts` (0.34, KS U8). The language\n * sub-list (`AccountMenuLanguage`) names itself with `topBar.language`, and the role in the\n * dev flask's name is `topBar.role`, as `RoleSwitcher`'s tooltip.\n */\n\n/** `TourMenu`'s words (§2.4). */\nexport interface TourMenuLabels {\n /** The compass's name, and the menu's. */\n menu: string;\n /** The NEW dot on the compass, for a reader: it joins the trigger's name. */\n newDot: string;\n /** The heading over the tours that start on the open page. */\n thisPage: string;\n /** The heading over the untaken new tours. */\n newTours: string;\n /** The last row, the way to the app's tours page (only with `href`). */\n allTours: string;\n /** The chip on a new tour's row. */\n badgeNew: string;\n /** What the ✓ on a taken tour's row says to a reader. */\n completed: string;\n /** The chip on a tour reset by an update (0.43, docs/tours-harmonization.md §8). */\n badgeUpdated: string;\n /** The dot on the compass while only updated tours light it, for a reader. */\n updatedDot: string;\n /** The heading over the untaken new tours when updated ones are among them. */\n newAndUpdated: string;\n}\n\n/** `SyncStatusIndicator`'s words (§2.6). The \"waiting for a plan\" state is\n * `billing.waitingChanges`, the kit's words for it since billing §12.5. */\nexport interface SyncStatusLabels {\n /** Writes this device cannot send (keksdose: no key for the budget). Ranked first. */\n blocked: (count: number) => string;\n offline: string;\n /** The server refused the last round. */\n refused: string;\n syncing: string;\n pending: (count: number) => string;\n /** Settled, with the time of the last good round (already formatted). */\n syncedAt: (time: string) => string;\n /** Settled, before the first round. */\n idle: string;\n /** Joined to the state when a tap syncs: what the tap does. */\n syncHint: string;\n /** Joined to the state while offline, when a tap can do nothing. */\n offlineHint: string;\n /** The state and what a tap does, as one tooltip: \"Offline — nothing to sync…\". */\n withHint: (state: string, hint: string) => string;\n /** The state with the app's `scope` words before it: \"Handovers on this device: …\". */\n scoped: (scope: string, state: string) => string;\n /** The short words beside the cloud (`showLabel`): offline. */\n offlineShort: string;\n /** The short words while syncing. */\n syncingShort: string;\n /** The short words while changes wait (pending, held for a plan, or blocked). */\n waitingShort: (count: number) => string;\n /** The short words after a refused round. */\n refusedShort: string;\n /** Offline in an online-only app (`storage=\"none\"`): nothing is kept on the device. */\n offlineUnsaved: string;\n /** A round under way in an online-only app — it saves, it doesn't sync. Also its short\n * words beside the cloud. */\n saving: string;\n}\n\n/**\n * `DevMenu`'s words (§2.7, §2.7.1, §6.2, §6.3). Still named for 0.42's `DevSwitchers`: the\n * namespace is the same `topBarParts.dev`, and an app's label tree keeps compiling.\n */\nexport interface DevSwitchersLabels {\n /** 0.51: the flask's name, and the menu's — the role and the plan follow it. */\n menu: string;\n /** 0.51: what the dot on the flask means while `active`, joined to its name — an\n * override the developer could forget is on (a \"today\", a slow server). */\n overridesOn: string;\n /** The heading of the role group. */\n roleHeading: string;\n /** @deprecated 0.51 — DevMenu's one flask is named by `menu`; removed in 0.52. */\n switchPlan: string;\n /** The plan in the flask's name, given the active plan. */\n plan: (value: string) => string;\n /** The heading of the plan group. */\n planHeading: string;\n /** The row back to the role the session started with. */\n backTo: (role: string) => string;\n /** The plan group's note while billing is off locally. */\n billingOff: string;\n}\n\nexport interface TopBarPartsLabels {\n tours: TourMenuLabels;\n sync: SyncStatusLabels;\n dev: DevSwitchersLabels;\n}\n\nexport const DEFAULT_TOP_BAR_PARTS_LABELS: TopBarPartsLabels = {\n tours: {\n menu: \"Guided tours\",\n newDot: \"New tours\",\n thisPage: \"On this page\",\n newTours: \"New tours\",\n allTours: \"All tours…\",\n badgeNew: \"New\",\n completed: \"Completed\",\n badgeUpdated: \"Updated\",\n updatedDot: \"Updated tours\",\n newAndUpdated: \"New and updated\",\n },\n sync: {\n blocked: (count) =>\n count === 1 ? \"1 change cannot be sent from this device\" : `${count} changes cannot be sent from this device`,\n offline: \"Offline — changes are saved on this device and will sync when you reconnect\",\n refused: \"The server refused the last sync\",\n syncing: \"Syncing…\",\n pending: (count) => (count === 1 ? \"1 change waiting to sync\" : `${count} changes waiting to sync`),\n syncedAt: (time) => `Synced at ${time}`,\n idle: \"Up to date\",\n syncHint: \"Tap to sync now\",\n offlineHint: \"nothing to sync while offline\",\n withHint: (state, hint) => `${state} — ${hint}`,\n scoped: (scope, state) => `${scope}: ${state}`,\n offlineShort: \"Offline\",\n syncingShort: \"Syncing…\",\n waitingShort: (count) => `${count} waiting`,\n refusedShort: \"Not synced\",\n offlineUnsaved: \"Offline — changes can't be saved until you reconnect\",\n saving: \"Saving…\",\n },\n dev: {\n menu: \"Dev tools\",\n overridesOn: \"Overrides on\",\n roleHeading: \"Dev · role\",\n switchPlan: \"Switch plan\",\n plan: (value) => `Plan: ${value}`,\n planHeading: \"Dev · plan\",\n backTo: (role) => `Back to ${role}`,\n billingOff: \"Billing is off locally: plans change nothing\",\n },\n};\n\n/** The `topBarParts` namespace, resolved — English, then the provider, then `labels`. */\nexport function useTopBarPartsLabels(labels?: LabelOverride<TopBarPartsLabels>): TopBarPartsLabels {\n return useKitLabels(\"topBarParts\", DEFAULT_TOP_BAR_PARTS_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AAsG1C,MAAM,+BAAkD;AAAA,EAC7D,OAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,WAAW;AAAA,IACX,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB;AAAA,EACA,MAAM;AAAA,IACJ,SAAS,CAAC,UACR,UAAU,IAAI,6CAA6C,GAAG,KAAK;AAAA,IACrE,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS,CAAC,UAAW,UAAU,IAAI,6BAA6B,GAAG,KAAK;AAAA,IACxE,UAAU,CAAC,SAAS,aAAa,IAAI;AAAA,IACrC,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,UAAU,CAAC,OAAO,SAAS,GAAG,KAAK,WAAM,IAAI;AAAA,IAC7C,QAAQ,CAAC,OAAO,UAAU,GAAG,KAAK,KAAK,KAAK;AAAA,IAC5C,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc,CAAC,UAAU,GAAG,KAAK;AAAA,IACjC,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA,EACA,KAAK;AAAA,IACH,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,MAAM,CAAC,UAAU,SAAS,KAAK;AAAA,IAC/B,aAAa;AAAA,IACb,QAAQ,CAAC,SAAS,WAAW,IAAI;AAAA,IACjC,YAAY;AAAA,EACd;AACF;AAGO,SAAS,qBAAqB,QAA8D;AACjG,SAAO,aAAa,eAAe,8BAA8B,MAAM;AACzE;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
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 = { 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 = '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 {\" \"}\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={laidOut ? ({ \"--menu-columns-width\": `${laidOut.columns.length * 14}rem` } as CSSProperties) : undefined}\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 && (\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\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"]}
|
|
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":";AAsBI,SAyD0B,UAzD1B,KAqIgB,YArIhB;AAtBJ,SAAS,OAAO,OAAO,MAAM,SAAS,WAAW;AACjD,SAAS,UAAU;AACnB,SAAS,iBAAiB;AAG1B,SAAS,eAAe;AACxB,SAAS,wBAAwB,oBAAoB;AACrD,SAAS,yBAAyB;AAI3B,MAAM,uBACX;AAIK,MAAM,yBACX;AAGF,SAAS,YAAY,EAAE,SAAS,GAAyB;AACvD,SACE,oBAAC,QAAG,WAAU,+EACX,UACH;AAEJ;AAYO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAWG;AAGD,QAAM,SAAS,aAAa,UAAU,wBAAwB,EAAE,OAAO,UAAU,CAAC;AAClF,QAAM,OAAO,SAAS,SAAS,OAAO;AACtC,QAAM,SACJ;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,MAAM;AACb,mBAAW,SAAS,SAAS,UAAU,MAAM;AAC7C,mBAAW;AAAA,MACb;AAAA,MACA,cAAY,OAAO;AAAA,MACnB,WAAW,GAAG,sBAAsB,SAAS;AAAA,MAE7C,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,EAC3B;AAIF,QAAM,MAAM,SAAS,OAAO;AAC5B,SACE,oBAAC,WAAQ,OAAO,KAAK,QAAM,MACxB,kBAAQ,OAAO,QAAQ,gCAAG,kBAAO,IAAM,QAC1C;AAEJ;AAIO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AACF,GASG;AACD,QAAM,SAAS,aAAa,UAAU,wBAAwB,EAAE,SAAS,UAAU,CAAC;AACpF,QAAM,aAAa,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,GAAG,QAAQ;AACpE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,OAAO;AAAA,MACnB,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,WAAQ,OAAO,YAAY,QAAM,MAChC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,cAAY,OAAO;AAAA,UACnB,WAAW;AAAA,UAOX,8BAAC,WAAQ,WAAU,gCAA+B;AAAA;AAAA,MACpD,GACF;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAU,aACX;AAAA,mBAAW,oBAAC,eAAa,mBAAQ;AAAA,QACjC,SAAS,IAAI,CAAC,MAAM;AACnB,gBAAM,KAAK,EAAE,IAAI;AACjB,gBAAM,WAAW,CAAC,GAAG,QAAQ,GAAG,OAAO,GAAG,aAAa,GAAG,cAAc,GAAG,QAAQ;AACnF,iBACE,oBAAC,QACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM;AACb,yBAAS,EAAE,EAAE;AACb,sBAAM;AAAA,cACR;AAAA,cACA,WAAW;AAAA,cAEX;AAAA,qCAAC,UAAK,WAAU,mCACd;AAAA,sCAAC,UAAK,WAAU,yBAIb,mBAAS,IAAI,CAAC,GAAG,MAChB;AAAA,oBAAC;AAAA;AAAA,sBAEC,WAAU;AAAA,sBACV,OAAO,EAAE,YAAY,EAAE;AAAA;AAAA,oBAFlB;AAAA,kBAGP,CACD,GACH;AAAA,kBACA,qBAAC,UAAK,WAAU,oCACd;AAAA,wCAAC,UAAK,WAAU,YAAY,YAAE,MAAK;AAAA,oBACnC,oBAAC,UAAK,WAAU,0CACb,YAAE,OACL;AAAA,qBACF;AAAA,mBACF;AAAA,gBACC,aAAa,EAAE,MACd,oBAAC,SAAM,WAAU,kCAAiC;AAAA;AAAA;AAAA,UAEtD,KAhCO,EAAE,EAiCX;AAAA,QAEJ,CAAC;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;AAaA,MAAM,8BAA8B,qBAAqB,QAAQ,UAAU,0BAA0B;AAErG,MAAM,aACJ;AAaK,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,UAAU;AACZ,GAaG;AACD,QAAM,SAAS,aAAa,UAAU,wBAAwB,EAAE,UAAU,UAAU,CAAC;AACrF,QAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,OAAO;AACrD,QAAM,cAAc,cAAc,SAAS,OAAO,gBAAgB,OAAO,KAAK,IAAI,OAAO;AACzF,QAAM,YAAY,WAAW,UAAa,YAAY;AACtD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,OAAO;AAAA,MACnB,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,WAAQ,OAAO,QAAQ,SAAS,OAAO,UAAU,QAAM,MACtD;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,cAAY;AAAA,UACZ,WAAW,YAAY,8BAA8B;AAAA,UAEpD,mBACC,iCACG;AAAA,wBAAY,UAAU,oBAAC,UAAK,eAAW,MAAC,WAAW,SAAS,OAAO,OAAO,IAAI,UAAU,IAAI;AAAA,YAC5F,aACC,oBAAC,UAAK,eAAW,MAAC,WAAU,oDACzB,4BAAkB,OAAO,IAAI,GAChC;AAAA,aAEJ,IAEA,oBAAC,SAAM,WAAU,UAAS;AAAA;AAAA,MAE9B,GACF;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAU,QACX;AAAA,mBAAW,oBAAC,eAAa,mBAAQ;AAAA,QACjC,QAAQ,IAAI,CAAC,SAAS;AACrB,gBAAM,WAAW,KAAK,SAAS;AAC/B,iBACE,oBAAC,QACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM;AACb,yBAAS,KAAK,IAAI;AAClB,sBAAM;AAAA,cACR;AAAA,cACA,WAAW;AAAA,cAEX;AAAA,qCAAC,UAAK,WAAU,2BACd;AAAA,sCAAC,UAAK,eAAW,MAAC,WAAW,SAAS,KAAK,OAAO,IAAI,UAAU,IAAI;AAAA,kBACnE,KAAK;AAAA,mBACR;AAAA,gBACC,YAAY,oBAAC,SAAM,WAAU,yBAAwB;AAAA;AAAA;AAAA,UACxD,KAdO,KAAK,IAed;AAAA,QAEJ,CAAC;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","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) => [
|