@eifi1/ui-kit 0.51.4 → 0.51.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/account/account-settings-labels.js.map +1 -1
- package/dist/account/account-settings.js +11 -9
- package/dist/account/account-settings.js.map +1 -1
- package/dist/account/delete-account-setting.js +56 -65
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.js +4 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.js +19 -11
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/admin/admin-tool-links.d.ts +1 -1
- package/dist/admin/admin-tool-links.js +7 -1
- package/dist/admin/admin-tool-links.js.map +1 -1
- package/dist/admin/invitations-panel.js +2 -13
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/mail-test-card.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +4 -11
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/user-roster.js +1 -9
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.js +23 -31
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.js +13 -5
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.js +1 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/checkout-processing.js.map +1 -1
- package/dist/billing/plan-card.js +91 -85
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-limit-toast.js.map +1 -1
- package/dist/billing/plan-limit.js.map +1 -1
- package/dist/billing/plan-price.js.map +1 -1
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/bin/eifi1-pay-page.mjs +6 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.js +1 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.js +29 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +1 -4
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.js +1 -7
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.js +5 -5
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +4 -10
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/card.js +1 -8
- package/dist/components/card.js.map +1 -1
- package/dist/components/chart-zoom-core.js +3 -17
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +4 -22
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/chip-input.js +4 -1
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/chip.js +10 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.js +58 -74
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper-labels.js.map +1 -1
- package/dist/components/column-mapper.js +167 -227
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +1 -4
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +1 -4
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.d.ts +5 -4
- package/dist/components/country-select.js +3 -19
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.js +10 -8
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.js +1 -6
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.js.map +1 -1
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.js +2 -18
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.js +3 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.js +44 -56
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.js +31 -44
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-row-actions.js +19 -16
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.js +4 -1
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.js +30 -39
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +1 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/description-list.js +10 -10
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +9 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +17 -27
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +25 -35
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-base.js +5 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +1 -1
- package/dist/components/field-parts.js +10 -14
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.js +8 -10
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +4 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.js.map +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/grouped-picker.js +6 -8
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.js +1 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input-labels.js.map +1 -1
- package/dist/components/icon-button.js +4 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/icon-picker.js +2 -7
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/image-grid.js +41 -50
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +2 -8
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +14 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +1 -7
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.js +14 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.js +12 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +26 -36
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.js +22 -10
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.js +29 -53
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +13 -31
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +10 -21
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +3 -16
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/nav-pills.js +10 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.js +1 -7
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +1 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +1 -7
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +13 -22
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.js +1 -11
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/password-strength-labels.js.map +1 -1
- package/dist/components/password-strength.js +2 -8
- package/dist/components/password-strength.js.map +1 -1
- package/dist/components/phone-input.js +42 -57
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +1 -8
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +16 -9
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.js +1 -8
- package/dist/components/qr-code.js.map +1 -1
- package/dist/components/row-actions.js +5 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/select.js +28 -38
- package/dist/components/select.js.map +1 -1
- package/dist/components/separator.js +1 -5
- package/dist/components/separator.js.map +1 -1
- package/dist/components/series-chart-axes.js +4 -1
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.js +29 -45
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.js +21 -17
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.js +4 -9
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.js +4 -13
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/signed-amount.js +12 -5
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/skeleton.js +2 -17
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +13 -36
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -10
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.js +14 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +1 -1
- package/dist/components/swipeable-row.js +1 -8
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table-skeleton.js +4 -1
- package/dist/components/table-skeleton.js.map +1 -1
- package/dist/components/table.js +12 -9
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.js +5 -24
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +1 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +6 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +2 -11
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.js +2 -22
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -4
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -1
- package/dist/components/translation-review-parts.js +7 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/translation-review.js +18 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +14 -10
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.d.ts +24 -2
- package/dist/components/treemap.js +28 -4
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/use-table-state.js +14 -11
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.js +3 -26
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/demo/demo-start.js +8 -4
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-labels.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +1 -8
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-capture.d.ts +1 -1
- package/dist/feedback/feedback-capture.js +3 -1
- package/dist/feedback/feedback-capture.js.map +1 -1
- package/dist/feedback/feedback-comment-thread.js +27 -13
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-context.js +1 -4
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-public-list.js +7 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-record.js +1 -5
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.js +25 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-table.js +4 -1
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +2 -13
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback/feedback-vote-button.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-document-title.js.map +1 -1
- package/dist/hooks/use-file-drop.js.map +1 -1
- package/dist/hooks/use-file-picker-labels.js.map +1 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +10 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.d.ts +1 -1
- package/dist/hooks/use-row-swipe.js +1 -6
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.js +1 -4
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +4 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.js +2 -8
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.js.map +1 -1
- package/dist/i18n/locales/en.js +4 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +4 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +4 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +4 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +4 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.js +13 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +40 -23
- package/dist/index.js.map +1 -1
- package/dist/landing/access.js +1 -4
- package/dist/landing/access.js.map +1 -1
- package/dist/landing/landing-sections.js +35 -38
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/device-settings.js +2 -1
- package/dist/lib/device-settings.js.map +1 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/iban.js.map +1 -1
- package/dist/lib/logger.js +1 -5
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/person-name.js.map +1 -1
- package/dist/lib/phone.js.map +1 -1
- package/dist/lib/qr-encode.js +344 -8
- package/dist/lib/qr-encode.js.map +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/table-text.js +15 -3
- package/dist/lib/table-text.js.map +1 -1
- package/dist/lib/text-match.d.ts +4 -0
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/notification-inbox.js +63 -44
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +6 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/use-push-device.js +4 -1
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/pwa-update-prompt.d.ts +1 -1
- package/dist/offline/pwa-update-prompt.js +7 -1
- package/dist/offline/pwa-update-prompt.js.map +1 -1
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +6 -1
- package/dist/offline.js.map +1 -1
- package/dist/pay/pay.css +8 -1
- package/dist/rhf/fields.d.ts +1 -2
- package/dist/rhf/fields.js +1 -10
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.js +1 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/search/command-palette.js +71 -62
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +34 -5
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/settings/device-reset-setting.js +1 -6
- package/dist/settings/device-reset-setting.js.map +1 -1
- package/dist/settings/settings-heading.js.map +1 -1
- package/dist/settings/settings-layout.js +9 -20
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.js +62 -31
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -10
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/dev-menu.js +12 -6
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/sync-status-indicator.js +7 -6
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +1 -10
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.js +9 -1
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.js +24 -6
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/react.js.map +1 -1
- package/dist/testing/viewport.js +7 -4
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/contrast-tokens.js +13 -2
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -2
- package/dist/theme/palette-derive.d.ts +1 -1
- package/dist/theme/palette-derive.js +18 -29
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +1 -3
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.js +5 -1
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-engine.js +1 -4
- package/dist/tour/tour-engine.js.map +1 -1
- package/dist/tour/tour.js +21 -12
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.js +3 -24
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +24 -28
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +1 -1
- package/dist/wizard/wizard-step.js +1 -8
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +5 -2
- package/src/account/account-settings-labels.ts +1 -2
- package/src/account/account-settings.tsx +11 -17
- package/src/account/delete-account-setting.tsx +1 -6
- package/src/account/email-change-setting-labels.ts +1 -2
- package/src/account/email-change-setting.tsx +3 -1
- package/src/account/passkeys-setting.tsx +1 -3
- package/src/account/sessions-setting.tsx +4 -2
- package/src/admin/admin-action-confirm.tsx +4 -7
- package/src/admin/admin-action-log.tsx +29 -29
- package/src/admin/admin-area-labels.ts +4 -1
- package/src/admin/admin-overview.tsx +8 -2
- package/src/admin/admin-sections.tsx +3 -1
- package/src/admin/admin-tool-links.tsx +7 -1
- package/src/admin/invitations-panel.tsx +5 -11
- package/src/admin/mail-test-card.tsx +1 -2
- package/src/admin/reviewer-scope-editor.tsx +4 -9
- package/src/admin/role-select.tsx +4 -2
- package/src/admin/user-roster.tsx +2 -11
- package/src/auth/accept-invitation.tsx +1 -7
- package/src/auth/auth-errors.ts +1 -6
- package/src/auth/forgot-password-form.tsx +2 -7
- package/src/auth/register-form.tsx +7 -8
- package/src/auth/reset-password-form.tsx +2 -5
- package/src/auth/sign-in-form.tsx +15 -14
- package/src/auth/verify-email.tsx +2 -7
- package/src/billing/billing-banner.tsx +3 -7
- package/src/billing/billing-labels.ts +6 -4
- package/src/billing/checkout-processing.ts +0 -1
- package/src/billing/plan-card.tsx +9 -8
- package/src/billing/plan-limit-toast.tsx +1 -2
- package/src/billing/plan-limit.ts +1 -2
- package/src/billing/plan-price.ts +1 -4
- package/src/billing/plan-switch.tsx +1 -2
- package/src/billing/subscription-actions.tsx +5 -2
- package/src/components/account-chips.tsx +1 -7
- package/src/components/alert-banner.tsx +2 -8
- package/src/components/amount-input.tsx +206 -178
- package/src/components/authed-image.tsx +7 -4
- package/src/components/autocomplete.tsx +7 -9
- package/src/components/bulk-action-bar.tsx +2 -7
- package/src/components/button-group.tsx +1 -2
- package/src/components/button-look.ts +6 -17
- package/src/components/button.tsx +1 -4
- package/src/components/calculator.tsx +6 -9
- package/src/components/calendar-heatmap.tsx +5 -10
- package/src/components/card.tsx +9 -13
- package/src/components/chart-zoom-core.tsx +6 -33
- package/src/components/chart.tsx +8 -31
- package/src/components/checkbox-group.tsx +1 -4
- package/src/components/chip-input.tsx +6 -6
- package/src/components/chip.tsx +14 -26
- package/src/components/choice-card.tsx +4 -18
- package/src/components/column-mapper-labels.ts +3 -10
- package/src/components/column-mapper.tsx +33 -122
- package/src/components/combobox-core.tsx +12 -42
- package/src/components/combobox.tsx +2 -6
- package/src/components/confirm-dialog.tsx +1 -3
- package/src/components/copy-button.tsx +2 -4
- package/src/components/country-select.tsx +9 -21
- package/src/components/currency-option-list.tsx +13 -14
- package/src/components/currency-select.tsx +2 -7
- package/src/components/danger-confirm.tsx +8 -9
- package/src/components/data-table-cells.tsx +1 -2
- package/src/components/data-table-column-settings.tsx +1 -3
- package/src/components/data-table-filter-popover.tsx +5 -19
- package/src/components/data-table-filters.ts +1 -6
- package/src/components/data-table-fit.ts +1 -3
- package/src/components/data-table-groups.tsx +3 -1
- package/src/components/data-table-head.tsx +27 -45
- package/src/components/data-table-mobile.tsx +99 -108
- package/src/components/data-table-pagination.tsx +2 -6
- package/src/components/data-table-row-actions.tsx +25 -24
- package/src/components/data-table-row.tsx +16 -17
- package/src/components/data-table-shared.tsx +10 -16
- package/src/components/data-table-sort.ts +1 -2
- package/src/components/data-table-virtual.ts +9 -4
- package/src/components/data-table.tsx +489 -501
- package/src/components/date-picker.tsx +1 -7
- package/src/components/date-range-picker.tsx +3 -13
- package/src/components/description-list.tsx +7 -13
- package/src/components/dialog-frame.tsx +12 -3
- package/src/components/disclosure.tsx +8 -12
- package/src/components/dropdown.tsx +3 -11
- package/src/components/empty-state.tsx +1 -3
- package/src/components/entity-combobox.tsx +2 -6
- package/src/components/error-boundary.tsx +7 -7
- package/src/components/field-base.tsx +8 -14
- package/src/components/field-parts.tsx +12 -23
- package/src/components/field-sync.tsx +7 -11
- package/src/components/file-button.tsx +7 -3
- package/src/components/file-dropzone.tsx +7 -12
- package/src/components/floating-panel.tsx +44 -51
- package/src/components/focus-ring.ts +1 -2
- package/src/components/form-actions.tsx +1 -4
- package/src/components/grouped-picker.tsx +6 -8
- package/src/components/hover-menu.tsx +1 -4
- package/src/components/iban-input-labels.ts +1 -2
- package/src/components/icon-button.tsx +46 -48
- package/src/components/icon-picker.tsx +4 -17
- package/src/components/image-grid.tsx +1 -6
- package/src/components/inline-edit-field.tsx +1 -5
- package/src/components/inline-entity-combobox.tsx +6 -21
- package/src/components/input.tsx +14 -2
- package/src/components/jump-calendar.tsx +2 -6
- package/src/components/legal-labels.ts +1 -2
- package/src/components/legal-page.tsx +18 -7
- package/src/components/legal.tsx +14 -5
- package/src/components/lightbox.tsx +1 -3
- package/src/components/line-items.tsx +7 -15
- package/src/components/list.tsx +29 -25
- package/src/components/measured-grid.tsx +8 -30
- package/src/components/menu-item.tsx +1 -3
- package/src/components/mini-calendar.tsx +129 -159
- package/src/components/money-field.tsx +4 -2
- package/src/components/month-picker.tsx +14 -21
- package/src/components/multi-entity-combobox.tsx +6 -26
- package/src/components/multi-select.tsx +5 -9
- package/src/components/nav-pills.tsx +10 -2
- package/src/components/number-field.tsx +18 -15
- package/src/components/number-input.tsx +3 -19
- package/src/components/numpad-sheet.tsx +9 -13
- package/src/components/one-time-code-input.tsx +6 -11
- package/src/components/page-contents.tsx +2 -9
- package/src/components/page-header.tsx +5 -10
- package/src/components/password-strength-labels.ts +1 -2
- package/src/components/password-strength.tsx +6 -9
- package/src/components/phone-input.tsx +2 -13
- package/src/components/picker-sheet.tsx +2 -5
- package/src/components/pie-chart.tsx +6 -12
- package/src/components/popover.tsx +2 -1
- package/src/components/progress-bar.tsx +16 -25
- package/src/components/qr-code.tsx +2 -9
- package/src/components/row-actions.tsx +9 -1
- package/src/components/search-field.tsx +4 -5
- package/src/components/select.tsx +2 -10
- package/src/components/separator.tsx +1 -5
- package/src/components/series-chart-axes.ts +5 -5
- package/src/components/series-chart-budget.ts +1 -4
- package/src/components/series-chart-ticks.ts +1 -4
- package/src/components/series-chart.tsx +43 -83
- package/src/components/settings-fields.tsx +20 -15
- package/src/components/share-card.tsx +6 -14
- package/src/components/sign-chip.tsx +16 -16
- package/src/components/signature-pad.tsx +21 -13
- package/src/components/signed-amount.tsx +6 -14
- package/src/components/skeleton.tsx +2 -13
- package/src/components/slider.tsx +9 -20
- package/src/components/sparkline.tsx +2 -13
- package/src/components/stat-tile.tsx +13 -24
- package/src/components/swatch-picker.tsx +8 -3
- package/src/components/swipeable-row.tsx +2 -11
- package/src/components/table-skeleton.tsx +4 -1
- package/src/components/table.tsx +12 -8
- package/src/components/tabs.tsx +24 -39
- package/src/components/text-link.tsx +6 -6
- package/src/components/text.tsx +1 -2
- package/src/components/tile-radio.tsx +6 -19
- package/src/components/time-input.tsx +4 -5
- package/src/components/toast.tsx +3 -12
- package/src/components/toggle-group.tsx +9 -9
- package/src/components/toggle-legend.tsx +4 -16
- package/src/components/tooltip.tsx +9 -48
- package/src/components/translation-review-editor.tsx +4 -4
- package/src/components/translation-review-parts.tsx +7 -1
- package/src/components/translation-review.tsx +15 -16
- package/src/components/tree-view.tsx +17 -20
- package/src/components/treemap.tsx +59 -9
- package/src/components/trigger-aria.ts +1 -4
- package/src/components/use-table-state.ts +16 -17
- package/src/components/user-avatar.tsx +4 -21
- package/src/demo/demo-start.tsx +8 -4
- package/src/feedback/feedback-attachment-labels.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +16 -29
- package/src/feedback/feedback-capture.ts +3 -1
- package/src/feedback/feedback-comment-thread.tsx +9 -6
- package/src/feedback/feedback-context.tsx +1 -4
- package/src/feedback/feedback-dialog.tsx +3 -4
- package/src/feedback/feedback-inbox.tsx +3 -9
- package/src/feedback/feedback-public-list.tsx +7 -1
- package/src/feedback/feedback-record.ts +6 -14
- package/src/feedback/feedback-replies.tsx +10 -5
- package/src/feedback/feedback-row-detail.tsx +2 -5
- package/src/feedback/feedback-status-undo.ts +1 -4
- package/src/feedback/feedback-submit-dialog.tsx +4 -4
- package/src/feedback/feedback-table.tsx +12 -8
- package/src/feedback/feedback-thread.tsx +4 -16
- package/src/feedback/feedback-vocabulary.ts +72 -82
- package/src/feedback/feedback-vote-button.tsx +9 -8
- package/src/hooks/use-anchored-rect.ts +1 -4
- package/src/hooks/use-document-title.ts +3 -1
- package/src/hooks/use-file-drop.ts +1 -3
- package/src/hooks/use-file-picker-labels.ts +1 -2
- package/src/hooks/use-file-picker.tsx +1 -5
- package/src/hooks/use-focus-trap.ts +1 -2
- package/src/hooks/use-overlay-history.ts +10 -5
- package/src/hooks/use-row-swipe.ts +1 -6
- package/src/i18n/defaults.ts +1 -4
- package/src/i18n/german.ts +33 -49
- package/src/i18n/kit-labels.tsx +5 -20
- package/src/i18n/languages.ts +2 -8
- package/src/i18n/locales/en.ts +11 -31
- package/src/i18n/locales/es.ts +35 -49
- package/src/i18n/locales/fr.ts +34 -51
- package/src/i18n/locales/hu.ts +26 -37
- package/src/i18n/locales/it.ts +35 -52
- package/src/i18n/locales/zh.ts +14 -32
- package/src/i18n/plurals.ts +4 -1
- package/src/i18n/review.ts +18 -7
- package/src/i18n/shared-labels.ts +1 -2
- package/src/index.ts +106 -32
- package/src/landing/access.ts +2 -7
- package/src/landing/landing-sections.tsx +14 -15
- package/src/landing/page-seo.ts +1 -3
- package/src/lib/calc.ts +4 -1
- package/src/lib/clipboard-minus.ts +4 -1
- package/src/lib/dates.ts +2 -10
- package/src/lib/decimal-marks.ts +1 -3
- package/src/lib/device-settings.ts +2 -1
- package/src/lib/direction.ts +1 -3
- package/src/lib/iban.ts +89 -9
- package/src/lib/logger.ts +1 -5
- package/src/lib/person-name.ts +1 -3
- package/src/lib/phone.ts +5 -8
- package/src/lib/qr-encode.ts +32 -8
- package/src/lib/session-refresh.ts +5 -6
- package/src/lib/table-text.ts +24 -6
- package/src/lib/text-match.ts +15 -3
- package/src/lib/translation-review.ts +1 -3
- package/src/notifications/notification-inbox.tsx +17 -4
- package/src/notifications/notification-parts.tsx +1 -2
- package/src/notifications/notification-types-setting.tsx +19 -18
- package/src/notifications/notifications-labels.ts +1 -2
- package/src/notifications/push-device-setting.tsx +7 -1
- package/src/notifications/push-devices-setting.tsx +7 -3
- package/src/notifications/push-worker.ts +10 -2
- package/src/notifications/quiet-hours-setting.tsx +4 -1
- package/src/notifications/use-push-device.ts +4 -1
- package/src/offline/pwa-update-prompt.tsx +7 -1
- package/src/offline/query-persistence.ts +9 -6
- package/src/offline.ts +6 -1
- package/src/rhf/fields.tsx +50 -19
- package/src/rhf/form-core.tsx +2 -6
- package/src/rhf/line-items.tsx +5 -7
- package/src/rhf/use-rhf-wizard-step.ts +3 -12
- package/src/search/command-palette.tsx +15 -23
- package/src/search/global-search.tsx +38 -8
- package/src/search/search-index.ts +5 -1
- package/src/settings/device-reset-setting.tsx +5 -8
- package/src/settings/settings-heading.tsx +3 -1
- package/src/settings/settings-layout.tsx +6 -13
- package/src/settings/use-settings-route.ts +4 -3
- package/src/shell/app-shell.tsx +48 -24
- package/src/shell/auth-layout.tsx +1 -6
- package/src/shell/dev-menu.tsx +12 -6
- package/src/shell/option-switcher-menu.tsx +1 -3
- package/src/shell/sync-status-indicator.tsx +14 -12
- package/src/shell/top-bar-parts-labels.ts +1 -3
- package/src/shell/topbar-action-menu.tsx +9 -8
- package/src/shell/topbar-controls.tsx +5 -19
- package/src/shell/tour-menu.tsx +9 -1
- package/src/sync/db.ts +5 -1
- package/src/sync/engine.ts +44 -12
- package/src/sync.ts +16 -2
- package/src/testing/css-variables.ts +1 -2
- package/src/testing/index.ts +1 -6
- package/src/testing/react.ts +1 -2
- package/src/testing/viewport.ts +9 -9
- package/src/theme/contrast-tokens.ts +13 -2
- package/src/theme/palette-derive.ts +24 -50
- package/src/theme/palette-presets.ts +0 -1
- package/src/theme/palette-store.ts +1 -4
- package/src/theme/text-size.ts +1 -1
- package/src/theme/theme-store.ts +2 -6
- package/src/tour/tour-catalogue.tsx +4 -2
- package/src/tour/tour-dock.ts +5 -1
- package/src/tour/tour-engine.ts +2 -6
- package/src/tour/tour.tsx +28 -12
- package/src/tour/use-tour-progress.ts +8 -2
- package/src/tour/use-tour-url-sync.ts +3 -1
- package/src/vite-env.d.ts +1 -4
- package/src/wizard/stepper-nav.tsx +10 -35
- package/src/wizard/types.ts +1 -4
- package/src/wizard/use-wizard.ts +28 -43
- package/src/wizard/validation.ts +1 -5
- package/src/wizard/wizard-context.tsx +2 -6
- package/src/wizard/wizard-step.tsx +1 -8
- package/src/wizard/wizard-summary.tsx +2 -6
- package/tokens.css +393 -152
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/toggle-legend.tsx"],"sourcesContent":["// A legend whose entries are SWITCHES — and the stroke table it shares with\n// `SeriesChart`. Lifted out of lenkbank's measurement plots, where ten legends over\n// thirteen charts took channels on and off the picture.\n//\n// No recharts in this module: a legend of switches is plain buttons, and it is as\n// useful under a chart the consumer drew by hand as under `SeriesChart`.\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_SERIES_CHART_LABELS, type SeriesChartLabels } from \"./series-chart-labels\";\n\n/**\n * The strokes a reader can tell apart, in the order they are handed out.\n *\n * For the charts where the colour is already spoken for: a comparison draws each\n * measurement in its own colour, so a chart carrying several CHANNELS of each has\n * nothing left but the stroke — and \"solid versus dashed\" only gets you two.\n *\n * **Five, and the fifth is the last one.** They are the draughtsman's line types, in\n * the draughtsman's order — continuous, dashed, dotted, dash-dot, dash-dot-dot — the\n * one family of five a century of drawings has already proved legible at a hair's\n * width. A sixth is not distinguishable from one of these at two pixels wide, so a\n * chart needing one is a chart that should be split.\n *\n * Kept here, beside the legend's own mark, and read by `SeriesChart` from here — so\n * the key under a chart and the lines on it cannot disagree.\n */\nexport const STROKE_PATTERNS: readonly (string | undefined)[] = [\n undefined,\n \"5 4\",\n \"1 3\",\n \"9 3 2 3\",\n \"9 3 2 3 2 3\",\n];\n\n/** The dash array for the nth pattern, wrapping past the fifth. */\nexport function strokeDash(order: number): string | undefined {\n const n = STROKE_PATTERNS.length;\n return STROKE_PATTERNS[((Math.trunc(order) % n) + n) % n];\n}\n\n/** Which of the patterns a `step` series is drawn in.\n *\n * A whole-number channel that jumps rather than travels is drawn dashed, and it has\n * to be one of {@link STROKE_PATTERNS} rather than a dash array written out at the\n * call site: a legend entry says its pattern by index, so a step line drawn in\n * anything else would be a legend promising a solid line for a dashed one. */\nexport const STEP_DASH = 1;\n\n/**\n * One key switched, as a new hidden set.\n *\n * Every caller writes `onToggle={(key) => setHidden(toggleHidden(hidden, key))}`. A\n * new set rather than a mutated one, because the state is read during render and a\n * mutation of something a component already holds is invisible to React.\n */\nexport function toggleHidden(hidden: ReadonlySet<string>, key: string): ReadonlySet<string> {\n const next = new Set(hidden);\n if (!next.delete(key)) next.add(key);\n return next;\n}\n\n/**\n * Where a legend that stands BESIDE a chart goes from Large up: UNDER it\n * (docs/text-size-harmonization.md §4). Beside a chart, a legend column takes a third of\n * a row whose plot needs all of it once the text is 25–50 % larger; under it, the row\n * is the plot's and the legend wraps across the width.\n *\n * Pure CSS (`large:`), so a legend moves with the text size alone, whatever drew the\n * row around it:\n * - in a GRID it spans a row of its own (`col-span-full`) after the charts\n * (`order-last`) — Kurvenschmiede's facing pairs;\n * - in a FLEX row it takes a line of its own (`basis-full`), and its row is allowed to\n * wrap for it (`[*:has(>&)]:flex-wrap` sets `flex-wrap` on the PARENT — the one rule\n * here that reaches outside the legend, because a row that may not wrap would squeeze\n * the chart to nothing beside a full-width legend) — Kurvenschmiede's chart rows.\n *\n * A caller that places its legend itself and wants it kept beside the chart at every\n * size overrides with `large:col-auto large:basis-auto`.\n */\nconst UNDER_THE_CHART_AT_LARGE =\n \"large:order-last large:col-span-full large:basis-full large:[*:has(>&)]:flex-wrap\";\n\n/** A vertical legend's entries from Large up: across, wrapping — it is under the chart\n * now (see {@link UNDER_THE_CHART_AT_LARGE}), where a column of entries would be a\n * screenful of one-word lines. */\nconst VERTICAL_AT_LARGE = \"large:flex-row large:flex-wrap large:items-center large:justify-start\";\n\nexport interface LegendEntry {\n key: string;\n label: ReactNode;\n /** Any CSS colour — `paletteFor(i)` for the kit's own ramp. */\n color: string;\n /**\n * What the entry's mark looks like. `swatch` — a filled square, the default — says\n * WHICH MEASUREMENT. `stroke` says WHICH QUANTITY, for the charts that carry both at\n * once and tell them apart by colour and by dash. `dot` is a round mark, for a\n * MARKER on the chart (a point, an event) rather than a series.\n */\n marker?: \"swatch\" | \"stroke\" | \"dot\";\n /** Which of {@link STROKE_PATTERNS}, when the marker is a stroke — the same index\n * `SeriesChartSeries.dash` takes, so a legend cannot promise a dot-dash the plot\n * draws dashed. Or the same custom `stroke-dasharray` string the series draws with\n * (keksdose's `\"4 3\"`). */\n dash?: number | string;\n /**\n * A mark of the caller's own, drawn in place of {@link marker} — for a key whose\n * mark is not a colour at all (a fading bar for \"open-ended\", a hairline for\n * \"today\"). Decorative: it is wrapped `aria-hidden`, and the label is the name.\n */\n icon?: ReactNode;\n /**\n * `\"end\"`: push this entry to the legend's inline END — the far right in LTR, the far\n * left in RTL — with everything before it kept at the start. kastlan's lease gantt\n * (feedback #49) keys its bars at the start and its \"today\" hairline at the end, over\n * the time axis whose end \"today\" is nearest, and wrote it as\n * `className=\"[&>li:last-child]:ms-auto\"` — a selector that breaks the moment the\n * entries are reordered and that reaches into the legend's markup.\n *\n * An auto inline-start margin, so it is logical (RTL mirrors it) and it takes only the\n * row's LEFTOVER space: on a narrow screen where the row wraps, the entry wraps with it\n * and sits at the end of its own line rather than overflowing. Give it to the LAST\n * entry — auto margins push everything after them too, so an end entry in the middle\n * drags the rest over with it (which is also how to end-align a GROUP: mark its first).\n * In a vertical legend it sits at the column's inline end. Honoured by both\n * {@link StaticLegend} and {@link ToggleLegend}, which share this entry type.\n */\n align?: \"start\" | \"end\";\n}\n\nexport interface ToggleLegendProps {\n entries: LegendEntry[];\n /** The keys currently off the chart. */\n hidden: ReadonlySet<string>;\n onToggle: (key: string) => void;\n /** `vertical` is for a legend standing BESIDE the charts rather than under them —\n * what a row of charts sharing one legend wants, since under two charts there is\n * no \"under\", and putting it under one says it belongs to that one.\n *\n * From Large up a vertical legend moves under the charts and lays its entries\n * across (0.32, docs/text-size-harmonization.md §4) — see {@link LegendColumn}. */\n orientation?: \"horizontal\" | \"vertical\";\n /**\n * Draw a legend of ONE entry rather than nothing.\n *\n * A single line's legend is usually noise. It is not where the legend is the only\n * SWITCH that line has — a series that starts hidden and has no entry to bring it\n * back is gone for good.\n */\n showSingle?: boolean;\n className?: string;\n /** Per-instance strings over `<UiKitProvider labels={{ seriesChart }}>`. */\n labels?: LabelOverride<SeriesChartLabels>;\n}\n\n/**\n * A legend whose entries are switches.\n *\n * `ChartLegendContent` with `onItemClick`/`activeKey` is the other kind: clicking an\n * entry HIGHLIGHTS it and dims the rest, which is right when every series belongs on\n * the chart and one of them is momentarily interesting. This is for the opposite —\n * three coordinates on one axis where y runs to 800 and z barely moves, and the only\n * way to see z is to take y off the chart entirely. Any number may be off at once.\n *\n * A hidden entry keeps its mark and its place. Removing it would reflow the legend on\n * every click and lose the one thing it is for: saying what COULD be shown.\n *\n * Each entry is a real `<button>` with `aria-pressed` — pressed means \"on the chart\" —\n * so Tab reaches every switch and Space/Enter flips it.\n */\nexport function ToggleLegend({\n entries,\n hidden,\n onToggle,\n orientation = \"horizontal\",\n showSingle = false,\n className,\n labels: labelsProp,\n}: ToggleLegendProps) {\n const labels = useKitLabels(\"seriesChart\", DEFAULT_SERIES_CHART_LABELS, labelsProp);\n if (entries.length < (showSingle ? 1 : 2)) return null;\n return (\n <div\n role=\"group\"\n aria-label={labels.legend}\n className={cn(\n \"flex gap-x-3 gap-y-1\",\n orientation === \"vertical\"\n ? cn(\"flex-col items-start justify-center\", VERTICAL_AT_LARGE, UNDER_THE_CHART_AT_LARGE)\n : \"mt-2 flex-wrap items-center\",\n className,\n )}\n >\n {entries.map((entry) => {\n const off = hidden.has(entry.key);\n return (\n <button\n key={entry.key}\n type=\"button\"\n // Pressed rather than a checkbox: it is a control over what the chart\n // draws, and \"not pressed\" is the same sentence the dimmed mark says.\n aria-pressed={!off}\n onClick={() => onToggle(entry.key)}\n className={cn(\n \"flex items-center gap-1.5 rounded text-start text-caption text-secondary transition-opacity hover:opacity-80\",\n FOCUS_RING,\n off && \"opacity-35\",\n entry.align === \"end\" && \"ms-auto\",\n )}\n >\n <LegendMark entry={entry} off={off} />\n {entry.label}\n </button>\n );\n })}\n </div>\n );\n}\n\nexport interface StaticLegendProps {\n entries: LegendEntry[];\n /** See {@link ToggleLegendProps.orientation}. */\n orientation?: \"horizontal\" | \"vertical\";\n /** The list's accessible name. Default: the `seriesChart.legend` label. */\n \"aria-label\"?: string;\n /**\n * `ul` (default): a list — \"list, 4 items\", walked with the list keys. Its entries are\n * `listitem`s, so a host test that counts `getAllByRole(\"listitem\")` on a page with\n * the legend on it counts the legend too: scope it (`within(...)`) or use `div`.\n *\n * `div`: a named `role=\"group\"` of plain entries, for a legend set inside something\n * that is already a list or a table row, where a list in a list reads as nesting that\n * is not there, or where the entries are too few for \"list, 2 items\" to help. The\n * entries are read in order either way; only the list framing goes.\n */\n as?: \"ul\" | \"div\";\n className?: string;\n /** Per-instance strings over `<UiKitProvider labels={{ seriesChart }}>`. */\n labels?: LabelOverride<SeriesChartLabels>;\n}\n\n/**\n * A legend that is a KEY, not a control: the same entries and marks as\n * {@link ToggleLegend} — swatch, stroke (with the chart's own dash), dot, or a mark of\n * the caller's own — but nothing to press.\n *\n * A separate component rather than `ToggleLegend` without `onToggle`, because the two\n * are different things to a screen reader: a group of switches announces a pressed\n * state on every entry, a key is a LIST, read as \"list, 4 items\" and walked with the\n * list keys, and costs no tab stops. The apps drew this by hand under the charts that\n * say what a colour or a dash means without letting you switch it (kastlan's lease\n * timeline, keksdose's cash buffer).\n *\n * Unlike `ToggleLegend` it draws a single entry: a key with one line still says what\n * the one mark on the chart means.\n */\nexport function StaticLegend({\n entries,\n orientation = \"horizontal\",\n \"aria-label\": ariaLabel,\n as = \"ul\",\n className,\n labels: labelsProp,\n}: StaticLegendProps) {\n const labels = useKitLabels(\"seriesChart\", DEFAULT_SERIES_CHART_LABELS, labelsProp);\n if (entries.length === 0) return null;\n const List = as;\n const Item = as === \"ul\" ? \"li\" : \"div\";\n return (\n <List\n // A `div` takes `group`: a name on a role-less div is one ARIA forbids and\n // screen readers drop, and the name is what says these marks are a key.\n role={as === \"div\" ? \"group\" : undefined}\n aria-label={ariaLabel ?? labels.legend}\n className={cn(\n \"m-0 flex list-none gap-x-3 gap-y-1 p-0\",\n orientation === \"vertical\"\n ? cn(\"flex-col items-start justify-center\", VERTICAL_AT_LARGE, UNDER_THE_CHART_AT_LARGE)\n : \"mt-2 flex-wrap items-center\",\n className,\n )}\n >\n {entries.map((entry) => (\n <Item\n key={entry.key}\n className={cn(\n \"flex items-center gap-1.5 text-caption text-secondary\",\n entry.align === \"end\" && \"ms-auto\",\n )}\n >\n <LegendMark entry={entry} off={false} />\n {entry.label}\n </Item>\n ))}\n </List>\n );\n}\n\n/**\n * The column a vertical legend stands in: beside the charts, on their horizontal\n * centre line. `h-full` + `justify-center`, so it centres in a grid cell or a flex row\n * that stretches it — and stacks whatever it is given, which is what a panel showing\n * two legends at once needs. The width is the caller's: how wide a legend column is\n * is a fact about that screen's layout.\n *\n * **From Large up it goes UNDER the charts** (0.32, docs/text-size-harmonization.md\n * §4): a row of its own in a grid, a line of its own in a flex row (see\n * `UNDER_THE_CHART_AT_LARGE`), with what it holds laid across and wrapping. A\n * caller's own `max-sm:flex-row …` for the phone keeps working; the caller's width\n * (`sm:w-40`) gives way to the full row, because a size wins over a breakpoint.\n */\nexport function LegendColumn({ children, className }: { children: ReactNode; className?: string }) {\n return (\n <div\n className={cn(\n \"flex h-full flex-col justify-center gap-3\",\n UNDER_THE_CHART_AT_LARGE,\n \"large:h-auto large:flex-row large:flex-wrap large:items-start large:justify-start large:gap-x-6\",\n className,\n )}\n >\n {children}\n </div>\n );\n}\n\n/**\n * A legend and what it is a legend OF, as a headed column.\n *\n * For legends that are several vocabularies at once — which measurement and which\n * channel — so the entries are read down a group and the groups across. Wrapped rows\n * would put the second half of one group on a line under the first half of the next.\n */\nexport function LegendGroup({ title, children }: { title: ReactNode; children: ReactNode }) {\n return (\n <div className=\"flex min-w-0 flex-col items-start gap-1\">\n <span className=\"text-micro font-semibold uppercase tracking-wide text-muted\">\n {title}\n </span>\n <div className=\"flex flex-col items-start gap-y-0.5\">{children}</div>\n </div>\n );\n}\n\n/** The entry's mark: a square for a measurement, a stroke for a quantity. A hidden one\n * keeps its outline — a mark that vanished would leave a line of text with nothing in\n * front of it.\n *\n * A stroke is an SVG line with the chart's own `strokeDasharray`, not a CSS gradient\n * approximating it: a gradient can say \"solid or dashed\" and nothing more.\n *\n * A hidden stroke is NOT faded here: the button around it already is (`opacity-35`),\n * and the line used to take its own 0.35 on top — 0.35 × 0.35, about 12 %, a mark\n * gone rather than dimmed, while the square beside it read at the full 35 %. */\nfunction LegendMark({ entry, off }: { entry: LegendEntry; off: boolean }) {\n if (entry.icon != null) {\n return (\n <span aria-hidden className=\"inline-flex shrink-0 items-center\">\n {entry.icon}\n </span>\n );\n }\n if (entry.marker === \"dot\") {\n return (\n <span\n aria-hidden\n className=\"size-2 shrink-0 rounded-full\"\n style={{\n backgroundColor: off ? \"transparent\" : entry.color,\n boxShadow: `inset 0 0 0 1.5px ${entry.color}`,\n }}\n />\n );\n }\n if (entry.marker === \"stroke\") {\n return (\n <svg aria-hidden viewBox=\"0 0 20 2\" className=\"h-0.5 w-5 shrink-0 overflow-visible\">\n <line\n x1={0}\n y1={1}\n x2={20}\n y2={1}\n stroke={entry.color}\n strokeWidth={2}\n strokeDasharray={typeof entry.dash === \"string\" ? entry.dash : strokeDash(entry.dash ?? 0)}\n />\n </svg>\n );\n }\n return (\n <span\n aria-hidden\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{\n backgroundColor: off ? \"transparent\" : entry.color,\n boxShadow: `inset 0 0 0 1.5px ${entry.color}`,\n }}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/toggle-legend.tsx"],"sourcesContent":["// A legend whose entries are SWITCHES — and the stroke table it shares with\n// `SeriesChart`. Lifted out of lenkbank's measurement plots, where ten legends over\n// thirteen charts took channels on and off the picture.\n//\n// No recharts in this module: a legend of switches is plain buttons, and it is as\n// useful under a chart the consumer drew by hand as under `SeriesChart`.\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_SERIES_CHART_LABELS, type SeriesChartLabels } from \"./series-chart-labels\";\n\n/**\n * The strokes a reader can tell apart, in the order they are handed out.\n *\n * For the charts where the colour is already spoken for: a comparison draws each\n * measurement in its own colour, so a chart carrying several CHANNELS of each has\n * nothing left but the stroke — and \"solid versus dashed\" only gets you two.\n *\n * **Five, and the fifth is the last one.** They are the draughtsman's line types, in\n * the draughtsman's order — continuous, dashed, dotted, dash-dot, dash-dot-dot — the\n * one family of five a century of drawings has already proved legible at a hair's\n * width. A sixth is not distinguishable from one of these at two pixels wide, so a\n * chart needing one is a chart that should be split.\n *\n * Kept here, beside the legend's own mark, and read by `SeriesChart` from here — so\n * the key under a chart and the lines on it cannot disagree.\n */\nexport const STROKE_PATTERNS: readonly (string | undefined)[] = [undefined, \"5 4\", \"1 3\", \"9 3 2 3\", \"9 3 2 3 2 3\"];\n\n/** The dash array for the nth pattern, wrapping past the fifth. */\nexport function strokeDash(order: number): string | undefined {\n const n = STROKE_PATTERNS.length;\n return STROKE_PATTERNS[((Math.trunc(order) % n) + n) % n];\n}\n\n/** Which of the patterns a `step` series is drawn in.\n *\n * A whole-number channel that jumps rather than travels is drawn dashed, and it has\n * to be one of {@link STROKE_PATTERNS} rather than a dash array written out at the\n * call site: a legend entry says its pattern by index, so a step line drawn in\n * anything else would be a legend promising a solid line for a dashed one. */\nexport const STEP_DASH = 1;\n\n/**\n * One key switched, as a new hidden set.\n *\n * Every caller writes `onToggle={(key) => setHidden(toggleHidden(hidden, key))}`. A\n * new set rather than a mutated one, because the state is read during render and a\n * mutation of something a component already holds is invisible to React.\n */\nexport function toggleHidden(hidden: ReadonlySet<string>, key: string): ReadonlySet<string> {\n const next = new Set(hidden);\n if (!next.delete(key)) next.add(key);\n return next;\n}\n\n/**\n * Where a legend that stands BESIDE a chart goes from Large up: UNDER it\n * (docs/text-size-harmonization.md §4). Beside a chart, a legend column takes a third of\n * a row whose plot needs all of it once the text is 25–50 % larger; under it, the row\n * is the plot's and the legend wraps across the width.\n *\n * Pure CSS (`large:`), so a legend moves with the text size alone, whatever drew the\n * row around it:\n * - in a GRID it spans a row of its own (`col-span-full`) after the charts\n * (`order-last`) — Kurvenschmiede's facing pairs;\n * - in a FLEX row it takes a line of its own (`basis-full`), and its row is allowed to\n * wrap for it (`[*:has(>&)]:flex-wrap` sets `flex-wrap` on the PARENT — the one rule\n * here that reaches outside the legend, because a row that may not wrap would squeeze\n * the chart to nothing beside a full-width legend) — Kurvenschmiede's chart rows.\n *\n * A caller that places its legend itself and wants it kept beside the chart at every\n * size overrides with `large:col-auto large:basis-auto`.\n */\nconst UNDER_THE_CHART_AT_LARGE = \"large:order-last large:col-span-full large:basis-full large:[*:has(>&)]:flex-wrap\";\n\n/** A vertical legend's entries from Large up: across, wrapping — it is under the chart\n * now (see {@link UNDER_THE_CHART_AT_LARGE}), where a column of entries would be a\n * screenful of one-word lines. */\nconst VERTICAL_AT_LARGE = \"large:flex-row large:flex-wrap large:items-center large:justify-start\";\n\nexport interface LegendEntry {\n key: string;\n label: ReactNode;\n /** Any CSS colour — `paletteFor(i)` for the kit's own ramp. */\n color: string;\n /**\n * What the entry's mark looks like. `swatch` — a filled square, the default — says\n * WHICH MEASUREMENT. `stroke` says WHICH QUANTITY, for the charts that carry both at\n * once and tell them apart by colour and by dash. `dot` is a round mark, for a\n * MARKER on the chart (a point, an event) rather than a series.\n */\n marker?: \"swatch\" | \"stroke\" | \"dot\";\n /** Which of {@link STROKE_PATTERNS}, when the marker is a stroke — the same index\n * `SeriesChartSeries.dash` takes, so a legend cannot promise a dot-dash the plot\n * draws dashed. Or the same custom `stroke-dasharray` string the series draws with\n * (keksdose's `\"4 3\"`). */\n dash?: number | string;\n /**\n * A mark of the caller's own, drawn in place of {@link marker} — for a key whose\n * mark is not a colour at all (a fading bar for \"open-ended\", a hairline for\n * \"today\"). Decorative: it is wrapped `aria-hidden`, and the label is the name.\n */\n icon?: ReactNode;\n /**\n * `\"end\"`: push this entry to the legend's inline END — the far right in LTR, the far\n * left in RTL — with everything before it kept at the start. kastlan's lease gantt\n * (feedback #49) keys its bars at the start and its \"today\" hairline at the end, over\n * the time axis whose end \"today\" is nearest, and wrote it as\n * `className=\"[&>li:last-child]:ms-auto\"` — a selector that breaks the moment the\n * entries are reordered and that reaches into the legend's markup.\n *\n * An auto inline-start margin, so it is logical (RTL mirrors it) and it takes only the\n * row's LEFTOVER space: on a narrow screen where the row wraps, the entry wraps with it\n * and sits at the end of its own line rather than overflowing. Give it to the LAST\n * entry — auto margins push everything after them too, so an end entry in the middle\n * drags the rest over with it (which is also how to end-align a GROUP: mark its first).\n * In a vertical legend it sits at the column's inline end. Honoured by both\n * {@link StaticLegend} and {@link ToggleLegend}, which share this entry type.\n */\n align?: \"start\" | \"end\";\n}\n\nexport interface ToggleLegendProps {\n entries: LegendEntry[];\n /** The keys currently off the chart. */\n hidden: ReadonlySet<string>;\n onToggle: (key: string) => void;\n /** `vertical` is for a legend standing BESIDE the charts rather than under them —\n * what a row of charts sharing one legend wants, since under two charts there is\n * no \"under\", and putting it under one says it belongs to that one.\n *\n * From Large up a vertical legend moves under the charts and lays its entries\n * across (0.32, docs/text-size-harmonization.md §4) — see {@link LegendColumn}. */\n orientation?: \"horizontal\" | \"vertical\";\n /**\n * Draw a legend of ONE entry rather than nothing.\n *\n * A single line's legend is usually noise. It is not where the legend is the only\n * SWITCH that line has — a series that starts hidden and has no entry to bring it\n * back is gone for good.\n */\n showSingle?: boolean;\n className?: string;\n /** Per-instance strings over `<UiKitProvider labels={{ seriesChart }}>`. */\n labels?: LabelOverride<SeriesChartLabels>;\n}\n\n/**\n * A legend whose entries are switches.\n *\n * `ChartLegendContent` with `onItemClick`/`activeKey` is the other kind: clicking an\n * entry HIGHLIGHTS it and dims the rest, which is right when every series belongs on\n * the chart and one of them is momentarily interesting. This is for the opposite —\n * three coordinates on one axis where y runs to 800 and z barely moves, and the only\n * way to see z is to take y off the chart entirely. Any number may be off at once.\n *\n * A hidden entry keeps its mark and its place. Removing it would reflow the legend on\n * every click and lose the one thing it is for: saying what COULD be shown.\n *\n * Each entry is a real `<button>` with `aria-pressed` — pressed means \"on the chart\" —\n * so Tab reaches every switch and Space/Enter flips it.\n */\nexport function ToggleLegend({\n entries,\n hidden,\n onToggle,\n orientation = \"horizontal\",\n showSingle = false,\n className,\n labels: labelsProp,\n}: ToggleLegendProps) {\n const labels = useKitLabels(\"seriesChart\", DEFAULT_SERIES_CHART_LABELS, labelsProp);\n if (entries.length < (showSingle ? 1 : 2)) return null;\n return (\n <div\n role=\"group\"\n aria-label={labels.legend}\n className={cn(\n \"flex gap-x-3 gap-y-1\",\n orientation === \"vertical\"\n ? cn(\"flex-col items-start justify-center\", VERTICAL_AT_LARGE, UNDER_THE_CHART_AT_LARGE)\n : \"mt-2 flex-wrap items-center\",\n className,\n )}\n >\n {entries.map((entry) => {\n const off = hidden.has(entry.key);\n return (\n <button\n key={entry.key}\n type=\"button\"\n // Pressed rather than a checkbox: it is a control over what the chart\n // draws, and \"not pressed\" is the same sentence the dimmed mark says.\n aria-pressed={!off}\n onClick={() => onToggle(entry.key)}\n className={cn(\n \"flex items-center gap-1.5 rounded text-start text-caption text-secondary transition-opacity hover:opacity-80\",\n FOCUS_RING,\n off && \"opacity-35\",\n entry.align === \"end\" && \"ms-auto\",\n )}\n >\n <LegendMark entry={entry} off={off} />\n {entry.label}\n </button>\n );\n })}\n </div>\n );\n}\n\nexport interface StaticLegendProps {\n entries: LegendEntry[];\n /** See {@link ToggleLegendProps.orientation}. */\n orientation?: \"horizontal\" | \"vertical\";\n /** The list's accessible name. Default: the `seriesChart.legend` label. */\n \"aria-label\"?: string;\n /**\n * `ul` (default): a list — \"list, 4 items\", walked with the list keys. Its entries are\n * `listitem`s, so a host test that counts `getAllByRole(\"listitem\")` on a page with\n * the legend on it counts the legend too: scope it (`within(...)`) or use `div`.\n *\n * `div`: a named `role=\"group\"` of plain entries, for a legend set inside something\n * that is already a list or a table row, where a list in a list reads as nesting that\n * is not there, or where the entries are too few for \"list, 2 items\" to help. The\n * entries are read in order either way; only the list framing goes.\n */\n as?: \"ul\" | \"div\";\n className?: string;\n /** Per-instance strings over `<UiKitProvider labels={{ seriesChart }}>`. */\n labels?: LabelOverride<SeriesChartLabels>;\n}\n\n/**\n * A legend that is a KEY, not a control: the same entries and marks as\n * {@link ToggleLegend} — swatch, stroke (with the chart's own dash), dot, or a mark of\n * the caller's own — but nothing to press.\n *\n * A separate component rather than `ToggleLegend` without `onToggle`, because the two\n * are different things to a screen reader: a group of switches announces a pressed\n * state on every entry, a key is a LIST, read as \"list, 4 items\" and walked with the\n * list keys, and costs no tab stops. The apps drew this by hand under the charts that\n * say what a colour or a dash means without letting you switch it (kastlan's lease\n * timeline, keksdose's cash buffer).\n *\n * Unlike `ToggleLegend` it draws a single entry: a key with one line still says what\n * the one mark on the chart means.\n */\nexport function StaticLegend({\n entries,\n orientation = \"horizontal\",\n \"aria-label\": ariaLabel,\n as = \"ul\",\n className,\n labels: labelsProp,\n}: StaticLegendProps) {\n const labels = useKitLabels(\"seriesChart\", DEFAULT_SERIES_CHART_LABELS, labelsProp);\n if (entries.length === 0) return null;\n const List = as;\n const Item = as === \"ul\" ? \"li\" : \"div\";\n return (\n <List\n // A `div` takes `group`: a name on a role-less div is one ARIA forbids and\n // screen readers drop, and the name is what says these marks are a key.\n role={as === \"div\" ? \"group\" : undefined}\n aria-label={ariaLabel ?? labels.legend}\n className={cn(\n \"m-0 flex list-none gap-x-3 gap-y-1 p-0\",\n orientation === \"vertical\"\n ? cn(\"flex-col items-start justify-center\", VERTICAL_AT_LARGE, UNDER_THE_CHART_AT_LARGE)\n : \"mt-2 flex-wrap items-center\",\n className,\n )}\n >\n {entries.map((entry) => (\n <Item\n key={entry.key}\n className={cn(\"flex items-center gap-1.5 text-caption text-secondary\", entry.align === \"end\" && \"ms-auto\")}\n >\n <LegendMark entry={entry} off={false} />\n {entry.label}\n </Item>\n ))}\n </List>\n );\n}\n\n/**\n * The column a vertical legend stands in: beside the charts, on their horizontal\n * centre line. `h-full` + `justify-center`, so it centres in a grid cell or a flex row\n * that stretches it — and stacks whatever it is given, which is what a panel showing\n * two legends at once needs. The width is the caller's: how wide a legend column is\n * is a fact about that screen's layout.\n *\n * **From Large up it goes UNDER the charts** (0.32, docs/text-size-harmonization.md\n * §4): a row of its own in a grid, a line of its own in a flex row (see\n * `UNDER_THE_CHART_AT_LARGE`), with what it holds laid across and wrapping. A\n * caller's own `max-sm:flex-row …` for the phone keeps working; the caller's width\n * (`sm:w-40`) gives way to the full row, because a size wins over a breakpoint.\n */\nexport function LegendColumn({ children, className }: { children: ReactNode; className?: string }) {\n return (\n <div\n className={cn(\n \"flex h-full flex-col justify-center gap-3\",\n UNDER_THE_CHART_AT_LARGE,\n \"large:h-auto large:flex-row large:flex-wrap large:items-start large:justify-start large:gap-x-6\",\n className,\n )}\n >\n {children}\n </div>\n );\n}\n\n/**\n * A legend and what it is a legend OF, as a headed column.\n *\n * For legends that are several vocabularies at once — which measurement and which\n * channel — so the entries are read down a group and the groups across. Wrapped rows\n * would put the second half of one group on a line under the first half of the next.\n */\nexport function LegendGroup({ title, children }: { title: ReactNode; children: ReactNode }) {\n return (\n <div className=\"flex min-w-0 flex-col items-start gap-1\">\n <span className=\"text-micro font-semibold uppercase tracking-wide text-muted\">{title}</span>\n <div className=\"flex flex-col items-start gap-y-0.5\">{children}</div>\n </div>\n );\n}\n\n/** The entry's mark: a square for a measurement, a stroke for a quantity. A hidden one\n * keeps its outline — a mark that vanished would leave a line of text with nothing in\n * front of it.\n *\n * A stroke is an SVG line with the chart's own `strokeDasharray`, not a CSS gradient\n * approximating it: a gradient can say \"solid or dashed\" and nothing more.\n *\n * A hidden stroke is NOT faded here: the button around it already is (`opacity-35`),\n * and the line used to take its own 0.35 on top — 0.35 × 0.35, about 12 %, a mark\n * gone rather than dimmed, while the square beside it read at the full 35 %. */\nfunction LegendMark({ entry, off }: { entry: LegendEntry; off: boolean }) {\n if (entry.icon != null) {\n return (\n <span aria-hidden className=\"inline-flex shrink-0 items-center\">\n {entry.icon}\n </span>\n );\n }\n if (entry.marker === \"dot\") {\n return (\n <span\n aria-hidden\n className=\"size-2 shrink-0 rounded-full\"\n style={{\n backgroundColor: off ? \"transparent\" : entry.color,\n boxShadow: `inset 0 0 0 1.5px ${entry.color}`,\n }}\n />\n );\n }\n if (entry.marker === \"stroke\") {\n return (\n <svg aria-hidden viewBox=\"0 0 20 2\" className=\"h-0.5 w-5 shrink-0 overflow-visible\">\n <line\n x1={0}\n y1={1}\n x2={20}\n y2={1}\n stroke={entry.color}\n strokeWidth={2}\n strokeDasharray={typeof entry.dash === \"string\" ? entry.dash : strokeDash(entry.dash ?? 0)}\n />\n </svg>\n );\n }\n return (\n <span\n aria-hidden\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{\n backgroundColor: off ? \"transparent\" : entry.color,\n boxShadow: `inset 0 0 0 1.5px ${entry.color}`,\n }}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -48,18 +48,7 @@ function Tooltip({
|
|
|
48
48
|
}) {
|
|
49
49
|
if (isEmptyLabel(label)) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
50
50
|
if (portal === true) {
|
|
51
|
-
return /* @__PURE__ */ jsx(
|
|
52
|
-
PortalTooltip,
|
|
53
|
-
{
|
|
54
|
-
label,
|
|
55
|
-
side,
|
|
56
|
-
className,
|
|
57
|
-
redact,
|
|
58
|
-
tap,
|
|
59
|
-
...rest,
|
|
60
|
-
children
|
|
61
|
-
}
|
|
62
|
-
);
|
|
51
|
+
return /* @__PURE__ */ jsx(PortalTooltip, { label, side, className, redact, tap, ...rest, children });
|
|
63
52
|
}
|
|
64
53
|
return /* @__PURE__ */ jsx(
|
|
65
54
|
InPlaceTooltip,
|
|
@@ -404,16 +393,7 @@ function placeTooltip(r, side, size, viewport) {
|
|
|
404
393
|
side: chosen
|
|
405
394
|
};
|
|
406
395
|
}
|
|
407
|
-
function PortalTooltip({
|
|
408
|
-
label,
|
|
409
|
-
side,
|
|
410
|
-
className,
|
|
411
|
-
redact,
|
|
412
|
-
tap,
|
|
413
|
-
children,
|
|
414
|
-
ref,
|
|
415
|
-
...rest
|
|
416
|
-
}) {
|
|
396
|
+
function PortalTooltip({ label, side, className, redact, tap, children, ref, ...rest }) {
|
|
417
397
|
const { triggerRef, setTrigger } = useTriggerRef(ref);
|
|
418
398
|
const [dir, setDir] = useState("ltr");
|
|
419
399
|
const id = useId();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/tooltip.tsx"],"sourcesContent":["import {\n cloneElement,\n isValidElement,\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type FocusEvent,\n type MouseEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type Ref,\n type RefObject,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { useDismissLayer, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useAnchoredRect, type AnchorRect } from \"../hooks/use-anchored-rect\";\nimport { viewportSize } from \"../hooks/use-anchored-panel\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { hasClippingAncestor } from \"../lib/clipping\";\nimport { assignRef } from \"../lib/refs\";\n\n/** The attribute that marks an element as a clipping container for {@link Tooltip}'s\n * auto-portal, whatever its computed `overflow` — `<div data-clips>` or\n * `<div {...{ [CLIPS_ATTRIBUTE]: \"\" }}>`. Put it on an app's own scroller so a test\n * environment without stylesheets (jsdom) portals the same tooltips the browser\n * does. DataTable's body and Table's wrapper already carry it. */\nexport { CLIPS_ATTRIBUTE } from \"../lib/clipping\";\n\n/** Where the bubble sits. `start` / `end` follow the reading direction — `end` is the\n * right in LTR and the left in RTL — and are what a layout that mirrors should ask\n * for. `left` / `right` stay physical, for a bubble tied to something that does not\n * mirror (a chart axis, a map). */\nexport type TooltipSide = \"top\" | \"bottom\" | \"left\" | \"right\" | \"start\" | \"end\";\n\n/** What a TAP — a touch or a pen press, never the mouse — on the trigger does to the\n * bubble. See \"A tap is not a hover\" on {@link Tooltip}.\n *\n * - `\"auto\"` (the default): a tap that activates something shows nothing; a tap that\n * activates nothing — on a disabled or `aria-disabled` control (a write lock's\n * reason), or on a trigger with no control at all (a truncated name, a badge) —\n * toggles the bubble, since showing it is then the tap's only answer.\n * - `\"toggle\"`: every tap toggles it — for a control that exists only to explain, such\n * as a \"?\" whose click does nothing.\n * - `\"ignore\"`: a tap never shows it. */\nexport type TooltipTap = \"auto\" | \"toggle\" | \"ignore\";\n\n/** The placement the maths works in: a logical side resolved against the trigger. */\ntype PhysicalSide = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nfunction physicalSide(side: TooltipSide, dir: Direction): PhysicalSide {\n if (side === \"start\") return dir === \"rtl\" ? \"right\" : \"left\";\n if (side === \"end\") return dir === \"rtl\" ? \"left\" : \"right\";\n return side;\n}\n\n/** The floating bubble itself. Uses the shared surface/border/text tokens so it\n * reads as part of the app's chrome (like the top bar and cards) rather than the\n * cold slate pill it used to be.\n *\n * `w-max` keeps a short label on one line — the old `whitespace-nowrap` did that\n * too, but it also let a sentence-length label grow without bound, and a bubble\n * wider than the space beside its trigger gets clipped by whatever overflow\n * container it sits in. So cap it and let long text wrap instead. The cap tracks\n * the viewport as well, for narrow screens where 20rem is already most of it.\n * `side` is a preference rather than an instruction for the PORTALLED variant,\n * which measures the bubble and turns it round when it would not fit\n * (Steering Design feedback #126). The CSS-placed one never turns round, so there\n * `side` is still the whole of which side it is on — but since 0.14 it is slid back\n * along the cross axis when it would cross the viewport edge (keksdose G7), which the\n * viewport term in this cap is what makes possible: a bubble never wider than the glass\n * minus the margins always fits once slid. */\nconst TOOLTIP_SURFACE =\n \"w-max max-w-[min(20rem,calc(100vw-1rem))] rounded-md border border-subtle bg-surface px-2 py-1 text-xs font-medium text-primary shadow-lg\";\n\nconst sidePositionClass: Record<TooltipSide, string> = {\n top: \"bottom-full left-1/2 -translate-x-1/2 mb-1\",\n bottom: \"top-full left-1/2 -translate-x-1/2 mt-1\",\n left: \"right-full top-1/2 -translate-y-1/2 mr-1\",\n right: \"left-full top-1/2 -translate-y-1/2 ml-1\",\n // Logical insets, so CSS resolves the side from the inherited direction with no\n // JavaScript: `end-full` pins the bubble's END edge to the trigger's start.\n start: \"end-full top-1/2 -translate-y-1/2 me-1\",\n end: \"start-full top-1/2 -translate-y-1/2 ms-1\",\n};\n\n/**\n * `extends ComponentPropsWithoutRef<\"span\">` because the wrapper this renders IS a span,\n * and a tooltip is the component a caller most often needs to reach past: it sits\n * between the layout and the control, so a `data-tour` anchor, a test id or an\n * `aria-label` aimed at the trigger used to be swallowed by it.\n *\n * ⚠️ On the empty-label branch there is no wrapper at all, and therefore nothing for\n * those attributes to land on — see the note in the body.\n */\nexport interface TooltipProps extends ComponentPropsWithoutRef<\"span\"> {\n label: ReactNode;\n side?: TooltipSide;\n className?: string;\n /**\n * Where the bubble lives. Left out (the default since 0.10.0), the tooltip decides for\n * itself: the bubble stays next to the trigger unless an ancestor clips or scrolls\n * (`overflow` other than `visible`, or the {@link CLIPS_ATTRIBUTE} marker), in which\n * case it is portalled — see \"Inside a scroll container\" below. `true` always portals, `false` never does; both are exactly\n * what they were before the default existed.\n */\n portal?: boolean;\n /**\n * Mount the IN-PLACE bubble only while it is up (hovered or focused, not dismissed),\n * instead of always. It still sits next to its trigger and is still placed by CSS\n * alone; what changes is that, while closed, it is not in the DOM — so it is not in\n * `getAllByRole(\"tooltip\")`, not in an ancestor's `textContent` and not in an\n * ancestor's accessible name. The trigger is described (`aria-describedby`) while\n * the bubble is up, exactly as a portalled one is. See \"Lazy in place\" on\n * {@link Tooltip} for why this exists (keksdose F6) and why it is not the default.\n *\n * Has no effect on a bubble that is portalled (`portal`, or auto-portalled inside a\n * clipping container) — that one is already mounted only while up.\n */\n lazy?: boolean;\n /** Tag the bubble `data-private`, for a label that repeats the user's own data. */\n redact?: boolean;\n /**\n * What a tap (touch or pen) on the trigger does — see {@link TooltipTap} and \"A tap is\n * not a hover\" below. Default `\"auto\"`: a tap that activates the control shows nothing;\n * one that activates nothing (a disabled control, plain text) toggles the bubble.\n */\n tap?: TooltipTap;\n children: ReactNode;\n /**\n * The trigger WRAPPER — the `<span>` the rest of the props go to, around the caller's\n * child (round 0.35 R4, as Card and EmptyState took one in 0.34) — merged with its own\n * ref, which the bubble is anchored to and dismissed by. Not the bubble: a portalled\n * one exists only while it is up. On the empty-label branch there is no wrapper, so\n * the ref stays `null` (see the ⚠️ above).\n */\n ref?: Ref<HTMLSpanElement>;\n}\n\n/** What each variant below takes: the resolved `side` and `tap`, and every span attribute\n * the caller handed {@link Tooltip}, forwarded to that variant's own wrapper. */\ntype TooltipVariantProps = Omit<TooltipProps, \"side\" | \"portal\" | \"lazy\" | \"tap\"> & {\n side: TooltipSide;\n tap: TooltipTap;\n};\n\n/**\n * Hover/focus label for a control.\n *\n * Two placements, and the choice matters more than it looks. In place, the bubble is\n * always mounted next to the trigger — so a plain render test finds it without a hover —\n * and shown while it is up (`display: none` otherwise, see \"never widens the page\"\n * below). Portalled, the bubble is mounted in `document.body` only while it is up,\n * positioned by measurement.\n *\n * ⚠️ **A bubble that repeats a value has to be redactable.** The consuming app blurs\n * `[data-private]` under a `demo-mode` class on `<html>` — and the portalled bubble is\n * mounted on `document.body`, which is INSIDE that class, so the rule reaches it as\n * long as the bubble is tagged. It is not tagged by default, because most labels are\n * UI strings; pass `redact` on the ones that repeat the user's own data (a truncated\n * payee, an account name, a memo). Getting this wrong is silent: the trigger blurs,\n * the bubble spells the value out on hover.\n *\n * ⚠️ **An empty label renders nothing at all.** `title={payee ?? \"\"}` is an ordinary\n * shape at a call site that reveals truncated text, and the native attribute answers\n * it by showing no tooltip. A component that faithfully rendered an empty bubble\n * would be a worse `title`, so the emptiness check is here rather than at every call\n * site that could forget it.\n *\n * ⚠️ **The bubble describes its trigger, which means cloning it.** `role=\"tooltip\"` is\n * a name for a box, not a relationship — so for as long as nothing referenced the\n * bubble, the label reached the pointer and nobody else. That is worst on the call\n * sites that need it most: the kit's icon-only buttons, where the tooltip IS the\n * label. `aria-describedby` has to sit on the focusable element, which is the\n * caller's child and not this component's wrapper, so the child is CLONED to carry\n * it. A description the caller already set is appended to, never replaced — a field's\n * error text and its hint bubble both describe it. Children that cannot take props (a\n * fragment, a bare string, several elements) are left exactly as they were.\n *\n * ⚠️ **Escape dismisses it (WCAG 1.4.13).** Anything that appears on hover or focus has\n * to be dismissible without moving the pointer, and a bubble is opaque: it lands over\n * the row, field or figure you were reading, and the only way out of it used to be to\n * point somewhere else — which is precisely what you cannot do when what you need to\n * read is underneath it. The listener is the document's rather than the wrapper's\n * because the pointer opens this with the keyboard focus somewhere else entirely, and\n * it is subscribed only while a bubble is actually up: the in-place bubble is always\n * mounted, and a table of forty tooltips must not mean forty keydown listeners.\n *\n * ⚠️ **Inside a scroll container, the bubble has to be portalled — and by default it\n * now is.** An always-mounted bubble is absolutely positioned, but an absolutely\n * positioned descendant still counts towards its scroll-container ancestor's\n * scrollable overflow — so an invisible bubble on a control near the right edge makes\n * the container scroll sideways with nothing to reveal. That is what Keksdose feedback\n * dev#488 reported on the admin roster: 66px of horizontal scroll on a table that fit,\n * 44px of it owed to tooltips nobody could see. And a visible one is clipped by the\n * container's edge. The portalled bubble is `position: fixed` and absent until hovered,\n * so it adds no width and cannot be clipped.\n *\n * Until 0.10.0 the cure was `portal` at the call site, and kastlan asked for it to stop\n * being one: every tooltip in a table, a drawer or a scrolling card had to remember it,\n * and the ones that forgot were only found by someone scrolling sideways. So with\n * `portal` left out, the tooltip looks for a clipping ancestor itself — any element\n * between it and `<body>` whose computed `overflow-x` / `overflow-y` is not `visible`,\n * or that carries {@link CLIPS_ATTRIBUTE} (`data-clips`). The marker is what makes the\n * answer the same under test: jsdom computes no Tailwind, so there every scroller\n * reads `visible` and a table-cell tooltip used to stay in place — its always-mounted\n * bubble then repeated the label in the cell's accessible name and `textContent`\n * (\"CheckingChecking\"), and apps pinned `portal` to stop it. The kit's own scrollers\n * are marked, so a tooltip in a DataTable or Table cell portals in jsdom exactly as it\n * does in the browser.\n * It looks at MOUNT, not only on open: dev#488's phantom scroll is caused by a bubble\n * nobody opened, so a check that waited for the hover would find the damage already\n * done. It looks again on every open, for a container that started scrolling after\n * the tooltip mounted (a table that grew). Only the bubble changes place; the trigger\n * and the caller's child stay mounted, so a switch never costs a focused button its\n * focus. Outside any such container the in-place bubble is kept, which is still the\n * cheaper one and the one a plain render test can find without a hover.\n *\n * Why not simply portal everything? Because the in-place bubble is the one existing\n * app tests rely on (it is in the DOM without a hover), and because it follows its\n * trigger through a scroll or an animation for free — the portalled one re-measures.\n *\n * ⚠️ **Lazy in place: `lazy` keeps the bubble next to its trigger but out of the DOM\n * until it is up (keksdose F6).** `data-clips` cured the doubled text for tooltips\n * inside a table; it did nothing for the ones outside any clipping container, where the\n * always-mounted bubble is still a real `role=\"tooltip\"` node full of text. About seven\n * keksdose sites — column-header tooltips, a direction toggle, an fx-estimate row, a\n * FlagBadge, a toast inside a `<button>` — still pinned `portal` for that alone: their\n * tests found two tooltips with `getAllByRole(\"tooltip\")`, read the label twice in a\n * `textContent`, or got a button whose accessible name had the bubble's sentence glued\n * on. `portal` fixed the test and cost the browser the measured, re-positioning bubble\n * for no reason. `lazy` is the in-place bubble with the portalled one's lifetime: it is\n * rendered only while hovered or focused (and not dismissed), in the same slot, with the\n * same classes, so it looks and sits exactly as the default one does when it is up.\n *\n * Why unmount rather than hide? Hiding was the other option — keep the bubble mounted\n * with `hidden` (or `aria-hidden` plus `display: none` until `:hover` / `:focus-within`)\n * and let `aria-describedby` go on pointing at it, which the accname algorithm allows:\n * a node referenced directly by `aria-describedby` contributes its text even while\n * hidden. That would take the bubble out of the role queries and out of an ancestor's\n * accessible name — but NOT out of `textContent`, which is plain DOM and counts hidden\n * text too, and `textContent` is one of the three things F6 lists. Only a bubble that\n * is not there solves all three. The cost is the portalled variant's: the trigger is\n * described while the bubble is up rather than always. Focusing the trigger IS what\n * puts it up, and React commits the `aria-describedby` in the same task as that focus\n * event, so a screen reader landing on the control still hears it; what goes is the description\n * of a trigger that is read in browse mode without ever being focused.\n *\n * Escape still dismisses it — here by unmounting it, which takes it off the screen and\n * out of the accessibility tree at once — and the next hover or focus brings it back.\n * There is no fade to lose: neither in-place bubble has ever had a transition, only the\n * `opacity` switch, so appearing on a state change looks the same as appearing on\n * `:hover`.\n *\n * Why opt-in rather than the new default? Because the always-mounted bubble is the one\n * existing app tests find without a hover (see above): flipping it would turn every\n * `getByRole(\"tooltip\")` written against 0.12 into a failure in all three apps at once.\n * Reach for `lazy` wherever a `portal` was pinned only to keep a test's DOM clean.\n *\n * ⚠️ **The in-place bubble is clamped to the viewport when it opens (keksdose G7).** The\n * portalled bubble has always measured itself and been pushed back onto the glass\n * ({@link placeTooltip}); the in-place one never learnt its own size, so `side` was the\n * whole of its placement and a `top` / `bottom` bubble sat centred on its trigger\n * whatever that cost. The cost shows on a phone: long labels live at a row's START edge\n * — a gcloud command in keksdose's jobs panel, a canned reply in a support thread — and\n * a 20rem bubble centred on a trigger 16px from the edge hangs half its text off the\n * screen. keksdose pinned `portal` on those sites for that alone, which gave up `lazy`'s\n * point (the cheap, in-place bubble) to buy a placement.\n *\n * So now, when an in-place bubble goes up (default or `lazy`), a layout effect measures\n * it and, if it crosses the viewport edge minus the same margin the portalled bubble\n * keeps, slides it back along its CROSS axis only — sideways for `top` / `bottom`, up or\n * down for the four side placements — with an inline margin (a `transform` until 0.25; see\n * {@link shiftStyle} for why that widened the page). The main axis is left alone on purpose: sliding a\n * `start` bubble along the main axis would slide it over its own trigger, and turning it\n * round is a measured-placement decision this CSS-placed bubble does not make (pass\n * `portal` for that). The width is already capped to the viewport (see\n * `TOOLTIP_SURFACE`), so a bubble can always fit once slid, and a long label wraps\n * instead of growing past the glass.\n *\n * Why on by default, with no prop? Because it is a no-op for every bubble that already\n * fits — the shift is zero unless the bubble would overflow — so the only placements it\n * changes are ones that were broken. The maths is in physical viewport pixels: an RTL\n * row puts its long label at the RIGHT edge and gets slid left by the same code, and the\n * reading direction only decides which edge is kept when a bubble is wider than the room\n * (its start, as {@link placeTooltip} keeps). Under jsdom there is no layout — every rect\n * is zero-sized — and a zero-sized bubble is taken to be unmeasured, so tests see no\n * transform at all. It is measured on open (and when the label or side changes while\n * open), not on every scroll: an in-place bubble follows its trigger for free, and a page\n * that scrolls sideways under an open tooltip is not a case worth a listener per tooltip.\n *\n * ⚠️ **An in-place bubble never widens the page (keksdose run 72, live #381).** Two\n * holes let it, and a page wider than the screen is worse than a bubble cut off: on a\n * phone Chrome then lays every `position: fixed` layer out against the WIDER page while\n * the glass still shows the old width. Measured on a 406px phone: the sync icon at x=254,\n * its `bottom` bubble at 102–422, the page 426px wide, and a FloatingPanel 17px off the\n * glass. First, the clamp compared against `window.innerWidth` — and on a phone that is\n * the layout viewport, which GROWS to the page's width once something sticks out (430 on\n * a 406px screen, measured in Chromium's mobile emulation), so a bubble already past the\n * edge found itself \"inside\" and stayed there. It now measures against the root's client\n * width, which on a phone is the screen (the initial containing block, the width `100vw`\n * and media queries see) whatever overflows, and on a desktop also leaves out a classic\n * scrollbar; the window is the fallback where there is no layout (jsdom). The portalled\n * bubble takes the same width, since a `fixed` bubble placed against a grown layout\n * viewport is off the glass for the same reason. Second, the always-mounted bubble was\n * `opacity: 0` while closed — invisible, but still laid out where the CSS puts it, so a\n * bubble nobody opened, centred on a trigger near the end edge, widened the page all by\n * itself (dev#488's phantom scroll, at the scale of the page instead of a scroller, where\n * the auto-portal cannot help: the page is not an ancestor it can portal out of). It is\n * now `display: none` until it is up, so a closed bubble takes no room at all, and an open\n * one is slid onto the glass in the same layout pass that shows it, before paint. jsdom\n * computes no Tailwind, so tests still find the closed bubble without a hover.\n *\n * ⚠️ **A tap is not a hover (keksdose run 72, live #379).** The bubble used to show on\n * `mouseenter` and on `focus`, and hide only on `mouseleave` and `blur`. A tap on a phone\n * fires an emulated `mouseenter` and, on a button, focuses it — and the virtual mouse and\n * the focus both stay on the button until the next tap somewhere else, so every\n * IconButton's label (the top-bar icons, the sync indicator, a row's actions) stayed up\n * over whatever the tap had just opened. Now a touch or pen press is told apart from the\n * mouse: the emulated `mouseenter` that follows a tap is ignored, the press itself closes\n * a bubble that was up, and a focus that arrives while the page's last input was a finger\n * or a pen — the tap's own, or one a closing dialog RETURNS to the icon it was opened\n * from — shows nothing. The next key press (a Bluetooth keyboard on a tablet) or mouse\n * press makes focus count again, and a mouse entering the trigger hovers as ever, so a\n * hybrid laptop gets both.\n *\n * Focus otherwise shows the bubble, as it always did — the keyboard, a script, a test's\n * `.focus()` — with one more exception: the focus a mouse press on the trigger gives it.\n * There it follows `:focus-visible`, so a text field still shows its bubble while you type\n * and a clicked button does not hold it: a desktop click behaves as it always did while\n * the pointer is over the button (the hover shows it) and lets it go when the pointer\n * leaves, where the in-place bubble used to linger until the button lost focus — the same\n * left-behind bubble on a smaller scale, and what the portalled bubble always did. A\n * keyboard user's bubble, in turn, now stays up when a mouse passes over and leaves (the\n * portalled one used to close). Any `pointerdown` outside the trigger closes an open\n * bubble too — the Safari case, where a tap elsewhere moves no focus.\n *\n * What a tap DOES show is {@link TooltipTap} (`tap`). By default a tap that activates\n * something shows nothing — the action is the answer — while a tap that activates nothing\n * toggles the bubble, because there the bubble is the only answer: a write-locked or\n * disabled control, whose bubble is the reason it did not respond, and a trigger with no\n * control at all, a truncated payee or a badge, whose bubble is the rest of the text. A\n * second tap, a tap anywhere else, Escape or the focus leaving closes it. \"Activates\" is\n * read off the DOM — a link, a button, a field, a label, an element with a widget role\n * (`button`, `link`, `checkbox`, `option`, …), or a DataTable row (`tr[tabindex]`) — from\n * the tapped element outwards, past the tooltip, so a glyph inside a clickable row or a\n * button counts as that row or button. What it cannot read is a click handler on an\n * element with no role (give it `role=\"button\"`, which it needs anyway, or pass\n * `tap=\"ignore\"`), and a button that exists only to explain, whose click does nothing —\n * FieldHint's \"?\" — which says so with `tap=\"toggle\"`.\n *\n * Why not show the label on a long press, as Android does for its own icons? Because the\n * web gives no reliable long press on a control: browsers disagree on whether the release\n * that ends one still clicks the button, and the button's own long press — the context\n * menu, text selection, a row's drag — claims the gesture first on others. A gesture meant\n * to ask \"what does this do\" must not risk doing it. Screen readers do not need it (the\n * bubble is in `aria-describedby`, and an IconButton's label is its name), and a tap on\n * anything that does nothing already shows it. No scroll listener either: every touch scroll begins with a `pointerdown`, which\n * already closes the bubble, and closing on scroll would close a keyboard user's bubble\n * the moment Tab scrolled its control into view.\n */\nexport function Tooltip({\n label,\n side = \"top\",\n className,\n portal,\n lazy = false,\n redact = false,\n tap = \"auto\",\n children,\n ...rest\n}: TooltipProps) {\n // No label, no bubble — and no wrapper either, so a conditional tooltip costs the\n // layout nothing on the branch where it does not apply. `...rest` goes with the\n // wrapper on this branch, which is the documented limit of the pass-through: there is\n // no element left to put an attribute on.\n if (isEmptyLabel(label)) return <>{children}</>;\n if (portal === true) {\n return (\n <PortalTooltip\n label={label}\n side={side}\n className={className}\n redact={redact}\n tap={tap}\n {...rest}\n >\n {children}\n </PortalTooltip>\n );\n }\n // Both variants are separate components so that `Tooltip` itself can keep calling NO\n // hooks: the empty-label branch above returns before either of them, and a hook after\n // a conditional return is a hooks-order bug rather than a style violation.\n return (\n <InPlaceTooltip\n label={label}\n side={side}\n className={className}\n redact={redact}\n detect={portal === undefined}\n lazy={lazy}\n tap={tap}\n {...rest}\n >\n {children}\n </InPlaceTooltip>\n );\n}\n\n\n/** The pointer handlers a caller may hand {@link Tooltip} that the trigger also needs:\n * both run, the trigger's first. (The mouse and focus handlers are the trigger's alone,\n * as they have always been.) */\ntype PointerHandlers = Pick<\n ComponentPropsWithoutRef<\"span\">,\n \"onPointerEnter\" | \"onPointerDown\" | \"onPointerUp\" | \"onPointerCancel\"\n>;\n\n/** A press that is not the mouse's: a finger, or a pen on the glass. */\nfunction isTap(pointerType: string): boolean {\n return pointerType === \"touch\" || pointerType === \"pen\";\n}\n\n/**\n * What a tap ACTIVATES, for `tap=\"auto\"`: the tapped element or the nearest ancestor —\n * inside the tooltip or round it — that a tap would do something with. The DataTable's\n * own list of row-owned controls (`OWN_CONTROL`), plus `tr[tabindex]` — a clickable\n * DataTable row is a roving tab stop, so all but one of its rows say `-1` — and the other\n * widget roles a tap selects or toggles.\n *\n * Two things on the DataTable's list are left off on purpose. A bare `tabindex`: a roving\n * tab stop is `0` on one element of a widget and `-1` on the rest, so it would make one\n * cell of a display-only grid \"activate\" and its neighbours not — and dialogs, panels and\n * `<main>` carry `-1` only to be focusable by script. And `gridcell` / `row`: the\n * CalendarHeatmap's days are display-only grid cells whose tooltip is the day's value,\n * and a tap is the only way to read it on a phone; a heatmap day that does select is a\n * `<button>`, caught above.\n */\nconst ACTIVATES = [\n \"a[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"label\",\n \"summary\",\n '[contenteditable=\"\"]',\n '[contenteditable=\"true\"]',\n \"tr[tabindex]\",\n ...[\n \"button\",\n \"link\",\n \"checkbox\",\n \"radio\",\n \"switch\",\n \"tab\",\n \"menuitem\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"option\",\n \"treeitem\",\n \"slider\",\n \"spinbutton\",\n \"combobox\",\n ].map((role) => `[role=\"${role}\"]`),\n].join(\",\");\n\n/** Whether a tap on `target` should toggle the bubble — see {@link TooltipTap}. Under\n * `auto`: when the tap activates nothing, either because there is no control under it\n * or because the control is disabled (natively, or `aria-disabled` — a write lock). */\nfunction tapShows(tap: TooltipTap, target: EventTarget | null): boolean {\n if (tap !== \"auto\") return tap === \"toggle\";\n if (!(target instanceof Element)) return true;\n const control = target.closest(ACTIVATES);\n return control === null || control.matches(\":disabled\") || control.closest('[aria-disabled=\"true\"]') !== null;\n}\n\n/**\n * The last kind of input the document saw: a `pointerdown`'s `pointerType` (\"mouse\",\n * \"touch\", \"pen\"), \"keyboard\" after a key press, `null` before either. ONE pair of\n * capturing listeners for the whole page, installed by the first trigger that mounts —\n * not a pair per tooltip, which a table of forty would multiply.\n *\n * Why a page-wide note and not only the trigger's own: the focus that brings a label back\n * after a tap is not always the tap's. A tap on a top-bar icon opens a dialog; closing it\n * (another tap) RETURNS the focus to the icon by script, and that focus arrives with no\n * press on the icon at all. On a phone it would put the label up over the page the dialog\n * just left, and keep it there. Keys an on-screen keyboard sends while typing\n * (`Unidentified`, a composition) and shortcut chords do not count as the keyboard.\n */\nlet lastInput: string | null = null;\nlet trackingInput = false;\n\nfunction trackInput(): void {\n if (trackingInput || typeof document === \"undefined\") return;\n trackingInput = true;\n document.addEventListener(\n \"pointerdown\",\n (e) => {\n lastInput = e.pointerType;\n },\n { capture: true, passive: true },\n );\n document.addEventListener(\n \"keydown\",\n (e) => {\n if (e.key === \"Unidentified\" || e.isComposing || e.ctrlKey || e.metaKey || e.altKey) return;\n lastInput = \"keyboard\";\n },\n { capture: true, passive: true },\n );\n}\n\n/**\n * Whether a focus should show the bubble. Not after a tap — the last input was a finger\n * or a pen, whoever moved the focus. Not after a mouse press on this very trigger either,\n * unless the browser would draw a focus ring there (`:focus-visible`: a text field, not a\n * button) — the hover already shows it while the pointer is there. Every other focus\n * shows it as every focus did before: the keyboard, a script, and a test's `.focus()` or\n * `fireEvent.focus` (jsdom's own `:focus-visible` guesses from whatever events the test\n * file fired before, so it is consulted only where a press makes the answer certain).\n */\nfunction focusShows(target: EventTarget, pressedByMouse: boolean): boolean {\n if (lastInput !== null && isTap(lastInput)) return false;\n if (!pressedByMouse || !(target instanceof Element)) return true;\n try {\n return target.matches(\":focus-visible\");\n } catch {\n return true;\n }\n}\n\n/**\n * Whether the bubble is up, from what the trigger hears — shared by both variants so the\n * in-place and the portalled bubble cannot disagree about it. See \"A tap is not a hover\"\n * on {@link Tooltip} for the rules; this is their bookkeeping.\n *\n * Three ways up — hovered by a mouse (or a hovering pen), focused (see {@link focusShows}),\n * and tapped (`tap`) — and Escape over all three (`dismissed`), re-armed by the next\n * request rather than by an effect watching the flags: coming back to a trigger is a\n * fresh request for its label, and an effect would also re-show the bubble under a\n * pointer that never left. `onArm` runs with each request, for the variant's own per-open\n * work (the reading direction, the clipping check).\n *\n * The refs change nothing on screen, only how the next event is read. `pressedByTouch`\n * is set by a touch or pen arriving or pressing — which precedes the emulated\n * `mouseenter` a tap produces (pointer events first, then the compatibility mouse\n * events) — and cleared by a mouse, or a pen that is not pressing, entering again, so a\n * hybrid laptop's mouse still hovers. `pressedByMouse` spans one mouse press, the window\n * in which that press's own focus arrives.\n */\nfunction useTooltipTrigger(\n triggerRef: RefObject<HTMLSpanElement | null>,\n tap: TooltipTap,\n passed: PointerHandlers,\n onArm: (el: HTMLElement) => void,\n) {\n const [hovered, setHovered] = useState(false);\n const [focused, setFocused] = useState(false);\n const [tapped, setTapped] = useState(false);\n const [dismissed, setDismissed] = useState(false);\n const pressedByTouch = useRef(false);\n const pressedByMouse = useRef(false);\n // The tap in progress, and whether a bubble was up when it began: a tap on an open\n // bubble closes it (at pointerdown) and must not open it again at pointerup.\n const press = useRef<{ wasOpen: boolean } | null>(null);\n useEffect(trackInput, []);\n const open = (hovered || focused || tapped) && !dismissed;\n const close = () => {\n setHovered(false);\n setFocused(false);\n setTapped(false);\n };\n // A layer on the Escape stack while up (Wave 2, DUP-1): over a dialog, the first\n // Escape dismisses the bubble — WCAG 1.4.13, without moving focus — and the next one\n // the dialog. Inside a Modal the bubble used to be beyond reach of the key.\n // Not an overlay: a tooltip annotates what it is on, so a floating launcher with one\n // does not step aside for its own bubble (`hideWhenOverlay`, 0.49 KIT-11).\n useDismissLayer(open, () => setDismissed(true), { overlay: false });\n // A press anywhere else closes it — the case a blur does not cover: Safari moves no\n // focus on a tap, and a tap on plain text moves it nowhere.\n useOutsideClick(triggerRef, close, open);\n const arm = (el: HTMLElement) => {\n setDismissed(false);\n onArm(el);\n };\n const handlers = {\n onPointerEnter: (e: PointerEvent<HTMLSpanElement>) => {\n if (e.pointerType === \"touch\") pressedByTouch.current = true;\n else if (e.buttons === 0) pressedByTouch.current = false;\n passed.onPointerEnter?.(e);\n },\n onPointerDown: (e: PointerEvent<HTMLSpanElement>) => {\n if (isTap(e.pointerType)) {\n pressedByTouch.current = true;\n press.current = { wasOpen: open };\n close();\n } else {\n pressedByTouch.current = false;\n pressedByMouse.current = true;\n }\n passed.onPointerDown?.(e);\n },\n onPointerUp: (e: PointerEvent<HTMLSpanElement>) => {\n pressedByMouse.current = false;\n const began = press.current;\n press.current = null;\n if (began && !began.wasOpen && isTap(e.pointerType) && tapShows(tap, e.target)) {\n setTapped(true);\n arm(e.currentTarget);\n }\n passed.onPointerUp?.(e);\n },\n // The browser took the gesture over — a scroll, a pinch: not a tap.\n onPointerCancel: (e: PointerEvent<HTMLSpanElement>) => {\n pressedByMouse.current = false;\n press.current = null;\n passed.onPointerCancel?.(e);\n },\n onMouseEnter: (e: MouseEvent<HTMLSpanElement>) => {\n if (pressedByTouch.current) return;\n setHovered(true);\n arm(e.currentTarget);\n },\n onMouseLeave: () => setHovered(false),\n onFocus: (e: FocusEvent<HTMLSpanElement>) => {\n if (!focusShows(e.target, pressedByMouse.current)) return;\n setFocused(true);\n arm(e.currentTarget);\n },\n onBlur: (e: FocusEvent<HTMLSpanElement>) => {\n setFocused(false);\n if (e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)) return;\n setTapped(false);\n },\n };\n return { open, dismissed, handlers };\n}\n\n/**\n * The trigger wrapper's own ref, and one callback ref that hands the wrapper to the\n * caller's `ref` as well (round 0.35 R4).\n */\nfunction useTriggerRef(ref: Ref<HTMLSpanElement> | undefined) {\n const triggerRef = useRef<HTMLSpanElement | null>(null);\n const setTrigger = useCallback(\n (el: HTMLSpanElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref],\n );\n return { triggerRef, setTrigger };\n}\n\n/** The variant that lives next to its trigger, and — when `detect` is on, which is the\n * default — moves its bubble to `<body>` when that turns out to be inside a clipping\n * container (see \"Inside a scroll container\" on {@link Tooltip}).\n *\n * Whether the bubble is up is {@link useTooltipTrigger}'s, shared with the portalled\n * variant. Until 0.25 the always-mounted bubble showed itself with CSS alone —\n * `group-hover` / `group-focus-within` — which is exactly what a tap on a phone could not\n * get rid of: the tapped button keeps the focus, and `:focus-within` cannot be told that\n * the focus came from a finger. It is shown from the same state as every other bubble\n * now, and is `display: none` while closed (see \"never widens the page\" on\n * {@link Tooltip}).\n *\n * ONE component for both placements rather than a switch between the two variants: a\n * switch would be a different component at the same place in the tree, and React\n * would remount the caller's child with it — a button that loses focus the moment\n * its own tooltip decided where to go. Here the wrapper and the child stay put and\n * only the bubble's slot changes. */\nfunction InPlaceTooltip({\n label,\n side,\n className,\n redact,\n detect,\n lazy,\n tap,\n children,\n ref,\n ...rest\n}: TooltipVariantProps & { detect: boolean; lazy: boolean }) {\n const id = useId();\n const { triggerRef, setTrigger } = useTriggerRef(ref);\n const [clipped, setClipped] = useState(false);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n // The reading direction on every request (the clamp keeps the start edge of a bubble\n // too wide for the room); the clipping check too, for a container that began to scroll\n // after mount.\n const { open, dismissed, handlers } = useTooltipTrigger(triggerRef, tap, rest, (el) => {\n setDir(dirOf(el));\n if (detect) setClipped(hasClippingAncestor(el));\n });\n // keksdose G7: slide an open in-place bubble back onto the screen. See \"The in-place\n // bubble is clamped\" on {@link Tooltip}.\n const bubbleRef = useRef<HTMLSpanElement | null>(null);\n const { shift, measuring } = useViewportClamp(bubbleRef, open && !clipped, side, label, dir);\n // At mount, before the first paint: the in-place bubble inside a scroller is the\n // phantom-scroll bug whether or not anyone opens it.\n useLayoutEffect(() => {\n if (detect) setClipped(hasClippingAncestor(triggerRef.current));\n // `triggerRef` is the same object for the component's life (useTriggerRef's useRef).\n }, [detect, triggerRef]);\n return (\n // No role for this span: its handlers track whether the bubble is up and activate\n // nothing; the caller's child is the interactive element, keeps its own handlers,\n // and its focus shows the bubble too.\n <span\n // `...rest` first: the handlers below are what decides whether a bubble is up, and\n // a caller passing an `onFocus` of its own must not replace them (its pointer\n // handlers are called from them).\n {...rest}\n ref={setTrigger}\n // Clipped for the one render in which the bubble is measured (see useViewportClamp):\n // that layout must not reach the page's width.\n style={measuring ? { ...rest.style, overflow: \"clip\" } : rest.style}\n className={cn(\"relative inline-flex\", !clipped && \"group/tooltip\", className)}\n // These track WHETHER A BUBBLE IS UP. They activate nothing — the only thing here\n // that can be activated is the caller's child, which keeps every handler it\n // arrived with — so this wrapper needs no role and no key handling of its own.\n {...handlers}\n >\n {/* In place the bubble is always there to point at; portalled or lazy, only while up. */}\n {describedBy(children, clipped || lazy ? (open ? id : undefined) : dismissed ? undefined : id)}\n {clipped ? (\n open && (\n <PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />\n )\n ) : lazy ? (\n // keksdose F6: the in-place slot and classes, the portalled lifetime. Mounted only\n // while up, so it is visible whenever it exists.\n open && (\n <span\n ref={bubbleRef}\n id={id}\n role=\"tooltip\"\n data-private={redact ? \"\" : undefined}\n style={shiftStyle(shift)}\n className={cn(TOOLTIP_SURFACE, \"pointer-events-none absolute z-50 opacity-100\", sidePositionClass[side])}\n >\n {label}\n </span>\n )\n ) : (\n <span\n ref={bubbleRef}\n id={id}\n role=\"tooltip\"\n style={shiftStyle(shift)}\n // The `hidden` ATTRIBUTE, not a class: dismissing has to take the bubble out of\n // the accessibility tree as well as off the screen, or a screen reader still\n // reads out the description of a bubble the user just closed. Merely CLOSED it\n // is the `hidden` CLASS — `display: none` in the browser, so it takes no room\n // and widens nothing (live #381), and nothing at all under jsdom, where tests\n // have always found this bubble without a hover. `aria-describedby` reads a\n // `display: none` node all the same.\n hidden={dismissed || undefined}\n data-private={redact ? \"\" : undefined}\n className={cn(\n TOOLTIP_SURFACE,\n \"pointer-events-none absolute z-50\",\n open ? \"opacity-100\" : \"hidden\",\n sidePositionClass[side],\n )}\n >\n {label}\n </span>\n )}\n </span>\n );\n}\n\n/** Hand `children` the bubble's id as an `aria-describedby`, if it is an element that\n * can hold one. `id` is undefined while there is no bubble to point at — a dangling\n * reference describes the trigger as nothing at all, which is worse than silence. */\nfunction describedBy(children: ReactNode, id: string | undefined): ReactNode {\n if (id === undefined || !isValidElement(children)) return children;\n const child = children as ReactElement<{ \"aria-describedby\"?: string }>;\n // Fragments, Suspense and friends are symbol-typed and take no DOM props; cloning one\n // with an aria attribute warns in development and drops it in production.\n if (typeof child.type === \"symbol\") return children;\n const own = child.props[\"aria-describedby\"];\n return cloneElement(child, { \"aria-describedby\": own ? `${own} ${id}` : id });\n}\n\n/** \"Would this bubble be blank.\" Only the values a call site actually produces when\n * it has nothing to say — `\"\"`, `null`, `undefined`, `false` from a `&&` guard. A\n * numeric `0` is a real label and stays one. */\nfunction isEmptyLabel(label: ReactNode): boolean {\n return (\n label == null ||\n label === false ||\n (typeof label === \"string\" && label.trim() === \"\")\n );\n}\n\nconst TOOLTIP_GAP = 4;\n\n/** How close to the viewport edge a bubble may sit. Not zero: a label flush\n * against the glass reads as clipped even when every character is on screen. */\nconst TOOLTIP_MARGIN = 4;\n\n/** A cross-axis slide, in viewport pixels, for an in-place bubble. */\ninterface Shift {\n x: number;\n y: number;\n}\n\nconst NO_SHIFT: Shift = { x: 0, y: 0 };\n\n/** An in-place bubble's slide and the label, side and direction it was measured for. */\ninterface Placement {\n shift: Shift;\n for: { side: TooltipSide; label: ReactNode; dir: Direction } | null;\n}\n\nconst UNPLACED: Placement = { shift: NO_SHIFT, for: null };\n\n/** How far to slide the span `[low, high]` so it sits inside `[TOOLTIP_MARGIN,\n * extent - TOOLTIP_MARGIN]`. Zero when it already does. When the span is wider than the\n * room, the edge it keeps is its START — as in {@link clamp}, the start of a label is the\n * half worth keeping — which is the low edge unless `keepHigh` says the start is the\n * high one (an RTL label, along x). The width cap means that only happens on a viewport\n * barely wider than the margins, or one whose scrollbar the cap's `100vw` counts. */\nfunction slideInto(low: number, high: number, extent: number, keepHigh = false): number {\n const min = TOOLTIP_MARGIN;\n const max = extent - TOOLTIP_MARGIN;\n if (high - low > max - min) return keepHigh ? max - high : min - low;\n if (low < min) return min - low;\n if (high > max) return max - high;\n return 0;\n}\n\n// The glass a bubble has to stay on — the root's client width, not `innerWidth` (keksdose\n// run 72, live #381) — is `viewportSize` in use-anchored-panel.ts since 0.34 (Wave 2,\n// DUP-7), where every anchored panel now reads it.\n\n/**\n * The in-place bubble's viewport clamp (keksdose G7): while `active`, measure the bubble\n * in a LAYOUT effect — before paint, so there is no frame with the label off the screen —\n * and return the cross-axis slide that brings it inside the viewport ({@link viewportSize})\n * minus `TOOLTIP_MARGIN`. `top` / `bottom` slide along x; `left` / `right` / `start` / `end`\n * along y. {@link NO_SHIFT} while closed, so the next open measures the bubble where the\n * CSS alone puts it. `dir` only decides which edge survives a bubble wider than the room.\n *\n * `measuring` is true for the one render in which the bubble is measured — on open, and\n * again when the label, side or direction changes while open — and the caller clips its\n * wrapper (`overflow: clip`) for that render. Measuring means laying the bubble out where\n * the CSS alone puts it, which is past the edge whenever there is something to slide, and\n * that layout counts towards the page's scrollable overflow even though it is never\n * painted. Chrome on a phone grows the layout viewport to it and does not always give the\n * width back: in an RTL page, where overflow on the left is scrollable, a bubble measured\n * at −24 left the page 426 wide and scrolled by −20 after the slide had already brought it\n * to 4 (Chromium mobile emulation, 406px). A clipped wrapper keeps the measurement out of\n * every ancestor's overflow — `clip` and not `hidden`, so the wrapper never becomes a\n * scroll container — and it is unclipped, with the slide, in the same task, before paint.\n *\n * The rect it reads already includes the slide it applied last time (a label that\n * changed while open), so that slide is taken back out before deciding the new one.\n * A zero-sized rect means no layout (jsdom, `display: none`) and leaves the bubble put.\n */\nfunction useViewportClamp(\n bubbleRef: RefObject<HTMLSpanElement | null>,\n active: boolean,\n side: TooltipSide,\n label: ReactNode,\n dir: Direction,\n): { shift: Shift; measuring: boolean } {\n // The slide, and what it was measured for: anything else on screen — a fresh open, a\n // new label, side or direction — is not measured yet, and renders clipped until it is.\n const [placement, setPlacement] = useState<Placement>(UNPLACED);\n const measured =\n placement.for !== null &&\n placement.for.side === side &&\n placement.for.label === label &&\n placement.for.dir === dir;\n useLayoutEffect(() => {\n const el = bubbleRef.current;\n if (!active || !el) {\n setPlacement(UNPLACED);\n return;\n }\n if (measured) return;\n const r = el.getBoundingClientRect();\n const unlaid = r.width === 0 && r.height === 0;\n const horizontal = side === \"top\" || side === \"bottom\";\n const viewport = viewportSize();\n setPlacement(({ shift: previous }) => {\n const next = unlaid\n ? previous\n : horizontal\n ? { x: slideInto(r.left - previous.x, r.right - previous.x, viewport.width, dir === \"rtl\"), y: 0 }\n : { x: 0, y: slideInto(r.top - previous.y, r.bottom - previous.y, viewport.height) };\n const shift = next.x === previous.x && next.y === previous.y ? previous : next;\n return { shift, for: { side, label, dir } };\n });\n }, [bubbleRef, active, measured, side, label, dir]);\n return { shift: placement.shift, measuring: active && !measured };\n}\n\n/**\n * The inline style for a slide: none at all when there is nothing to slide, so a bubble\n * that fits renders exactly as it did before G7.\n *\n * A MARGIN, not a `transform` (0.14–0.24 used `transform: translate(…)`), because of what\n * Blink does with the page's width (keksdose run 72, live #381). The slide is decided after\n * a layout that placed the bubble where the CSS alone puts it — past the edge — and that\n * layout already counted it into the page's scrollable overflow. A later change to\n * `transform` alone re-paints but does not re-lay-out, so that overflow was never taken\n * back: measured in Chromium's mobile emulation, a bubble shown at 108–428 on a 406px\n * screen and then slid to 400 by a transform left the page 428 wide, the layout viewport\n * with it (`position: fixed` layers laid out against 428), until the bubble closed. The\n * same slide as a margin is a layout change; the page went back to 406 in the same frame.\n *\n * Physical margins, matching the physical maths: `margin-left` moves a `top` / `bottom`\n * bubble, whose placement is the physical `left: 50%`; `margin-top` moves the four side\n * placements, placed by `top: 50%`. Neither is a margin the placement classes set — they\n * keep their gap on the main axis (`mb-1`, `ms-1`, …).\n */\nfunction shiftStyle(shift: Shift) {\n if (shift.x === 0 && shift.y === 0) return undefined;\n return shift.x !== 0 ? { marginLeft: shift.x } : { marginTop: shift.y };\n}\n\nconst portalTransformBySide: Record<PhysicalSide, string> = {\n right: \"translate(0, -50%)\",\n left: \"translate(-100%, -50%)\",\n top: \"translate(-50%, -100%)\",\n bottom: \"translate(-50%, 0)\",\n};\n\n/** Anchor point (viewport px) for the tooltip on the given side of `r`. Paired\n * with {@link portalTransformBySide}, which shifts the box onto that point. */\nfunction tooltipAnchor(\n r: AnchorRect,\n side: PhysicalSide,\n): { left: number; top: number } {\n switch (side) {\n case \"right\":\n return { left: r.right + TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"left\":\n return { left: r.left - TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"top\":\n return { left: r.left + r.width / 2, top: r.top - TOOLTIP_GAP };\n case \"bottom\":\n return { left: r.left + r.width / 2, top: r.bottom + TOOLTIP_GAP };\n }\n}\n\nexport interface TooltipSize {\n width: number;\n height: number;\n}\n\nexport interface TooltipViewport {\n width: number;\n height: number;\n}\n\nexport interface TooltipPlacement {\n left: number;\n top: number;\n /** Which side it ended up on, which need not be the one that was asked for. */\n side: PhysicalSide;\n}\n\nconst opposite: Record<PhysicalSide, PhysicalSide> = {\n left: \"right\",\n right: \"left\",\n top: \"bottom\",\n bottom: \"top\",\n};\n\n/** Whether the bubble clears the viewport edge on `side` of the trigger. */\nfunction roomOn(\n r: AnchorRect,\n side: PhysicalSide,\n size: TooltipSize,\n viewport: TooltipViewport,\n): boolean {\n switch (side) {\n case \"left\":\n return r.left - TOOLTIP_GAP - size.width >= TOOLTIP_MARGIN;\n case \"right\":\n return r.right + TOOLTIP_GAP + size.width <= viewport.width - TOOLTIP_MARGIN;\n case \"top\":\n return r.top - TOOLTIP_GAP - size.height >= TOOLTIP_MARGIN;\n case \"bottom\":\n return r.bottom + TOOLTIP_GAP + size.height <= viewport.height - TOOLTIP_MARGIN;\n }\n}\n\nfunction sameRoom(\n a: { size: TooltipSize; viewport: TooltipViewport },\n b: { size: TooltipSize; viewport: TooltipViewport },\n): boolean {\n return (\n a.size.width === b.size.width &&\n a.size.height === b.size.height &&\n a.viewport.width === b.viewport.width &&\n a.viewport.height === b.viewport.height\n );\n}\n\nfunction clamp(value: number, low: number, high: number): number {\n // `high` first, so a bubble taller or wider than the viewport is pinned to the\n // top-left corner rather than to the bottom-right one — the start of a label\n // is the half worth keeping.\n return Math.max(low, Math.min(value, high));\n}\n\n/**\n * Where the bubble actually goes, given how big it turned out to be.\n *\n * Two rules, and they are separate because they fix separate failures.\n *\n * **Turn round when the preferred side has no room.** `side` says which side of\n * the trigger the label reads best on, and on a form near the left edge of the\n * window that side is off the screen — the capped bubble can only wrap, not\n * move, so what the reader gets is a sentence with its first half outside the\n * glass. Flipped only when the *other* side is genuinely better: a trigger in a\n * viewport too narrow for the bubble either way keeps the side it asked for, and\n * the clamp below does what it can.\n *\n * **Then clamp both axes.** The cross axis is the one that needs it — a `top`\n * bubble is centred on the trigger, so a trigger near the left edge pushes half\n * the label off even though the side it is on is right — and clamping the main\n * axis too costs nothing and covers the flip having nowhere to land.\n *\n * Pure, and measured in viewport pixels throughout, so it can be tested without\n * a layout: the caller supplies the trigger's rect, the bubble's own size and\n * the window.\n */\nexport function placeTooltip(\n r: AnchorRect,\n side: PhysicalSide,\n size: TooltipSize,\n viewport: TooltipViewport,\n): TooltipPlacement {\n const chosen =\n roomOn(r, side, size, viewport) || !roomOn(r, opposite[side], size, viewport)\n ? side\n : opposite[side];\n const point = tooltipAnchor(r, chosen);\n const box =\n chosen === \"left\"\n ? { left: point.left - size.width, top: point.top - size.height / 2 }\n : chosen === \"right\"\n ? { left: point.left, top: point.top - size.height / 2 }\n : chosen === \"top\"\n ? { left: point.left - size.width / 2, top: point.top - size.height }\n : { left: point.left - size.width / 2, top: point.top };\n return {\n left: clamp(box.left, TOOLTIP_MARGIN, viewport.width - size.width - TOOLTIP_MARGIN),\n top: clamp(box.top, TOOLTIP_MARGIN, viewport.height - size.height - TOOLTIP_MARGIN),\n side: chosen,\n };\n}\n\nfunction PortalTooltip({\n label,\n side,\n className,\n redact,\n tap,\n children,\n ref,\n ...rest\n}: TooltipVariantProps) {\n const { triggerRef, setTrigger } = useTriggerRef(ref);\n // The trigger's reading direction, read when the bubble is asked for (an event, not a\n // render): it resolves `start` / `end`, and the portalled bubble — which has left the\n // subtree it would have inherited `dir` from — carries it too.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const id = useId();\n // Escape closes it outright, since this variant's bubble only exists while it is\n // shown. The next mouseenter/focus brings it back, which is the behaviour WCAG\n // 1.4.13 asks for: dismissible now, still available when you ask again.\n const { open, handlers } = useTooltipTrigger(triggerRef, tap, rest, (el) => setDir(dirOf(el)));\n\n return (\n <>\n {/* As in `InPlaceTooltip`, no role: the handlers only show and hide the bubble; the\n caller's child is the interactive element, and focusing it shows the bubble. */}\n <span\n // As in `InPlaceTooltip`: the caller's attributes first, the handlers that run\n // this component after them. The BUBBLE is deliberately not given them — it\n // is portalled to `<body>`, and an id or a tour anchor duplicated onto a node\n // that only exists while hovered would match twice or match nothing. So, too,\n // the caller's `ref` (round 0.35 R4).\n {...rest}\n ref={setTrigger}\n className={cn(\"relative inline-flex\", className)}\n {...handlers}\n >\n {describedBy(children, open ? id : undefined)}\n </span>\n {open && (\n <PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />\n )}\n </>\n );\n}\n\n/** The measured, `position: fixed` bubble on `<body>`, mounted only while it is up.\n * Shared by {@link PortalTooltip} and the in-place variant's clipped mode, so the two\n * cannot place a bubble differently. */\nfunction PortalBubble({\n triggerRef,\n id,\n label,\n side,\n dir,\n redact,\n}: {\n triggerRef: RefObject<HTMLSpanElement | null>;\n id: string;\n label: ReactNode;\n side: TooltipSide;\n dir: Direction;\n redact: boolean | undefined;\n}) {\n const bubbleRef = useRef<HTMLSpanElement | null>(null);\n // The measure + scroll/resize-tracking lifecycle is owned by useAnchoredRect;\n // here we only map the rect to a side-specific anchor point.\n const rect = useAnchoredRect(triggerRef, true);\n // The bubble's own size and the window it has to fit in — neither of which is\n // knowable in render: the width is whatever the label wrapped to inside the\n // cap, and reading `window` while rendering is not a pure thing to do. Both\n // are taken in a LAYOUT effect, so the correction lands before the browser\n // paints and there is no frame in which the label sits off the screen.\n const [room, setRoom] = useState<{ size: TooltipSize; viewport: TooltipViewport } | null>(null);\n useLayoutEffect(() => {\n const measured = bubbleRef.current?.getBoundingClientRect();\n setRoom((previous) => {\n if (!measured) return null;\n const next = {\n size: { width: measured.width, height: measured.height },\n // The glass, not the window: see viewportSize (live #381).\n viewport: viewportSize(),\n };\n // Only publish what actually CHANGED: every re-measure allocates a fresh\n // object, and a new object on every scroll event would re-render the\n // bubble forever.\n return previous && sameRoom(previous, next) ? previous : next;\n });\n }, [rect, label]);\n\n const physical = physicalSide(side, dir);\n const point = rect ? tooltipAnchor(rect, physical) : null;\n // Unmeasured on the very first pass, where the anchor point plus the side's\n // own transform is exactly what this always did. One layout effect later the\n // size is known and the placement is decided properly.\n const placed = rect && room ? placeTooltip(rect, physical, room.size, room.viewport) : null;\n\n if (!point || typeof document === \"undefined\") return null;\n return createPortal(\n <span\n ref={bubbleRef}\n id={id}\n role=\"tooltip\"\n dir={dir}\n data-private={redact ? \"\" : undefined}\n style={\n placed\n ? { position: \"fixed\", left: placed.left, top: placed.top }\n : {\n position: \"fixed\",\n left: point.left,\n top: point.top,\n transform: portalTransformBySide[physical],\n }\n }\n className={cn(TOOLTIP_SURFACE, \"pointer-events-none z-50\")}\n >\n {label}\n </span>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/tooltip.tsx"],"sourcesContent":["import {\n cloneElement,\n isValidElement,\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type FocusEvent,\n type MouseEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type Ref,\n type RefObject,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { useDismissLayer, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useAnchoredRect, type AnchorRect } from \"../hooks/use-anchored-rect\";\nimport { viewportSize } from \"../hooks/use-anchored-panel\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { hasClippingAncestor } from \"../lib/clipping\";\nimport { assignRef } from \"../lib/refs\";\n\n/** The attribute that marks an element as a clipping container for {@link Tooltip}'s\n * auto-portal, whatever its computed `overflow` — `<div data-clips>` or\n * `<div {...{ [CLIPS_ATTRIBUTE]: \"\" }}>`. Put it on an app's own scroller so a test\n * environment without stylesheets (jsdom) portals the same tooltips the browser\n * does. DataTable's body and Table's wrapper already carry it. */\nexport { CLIPS_ATTRIBUTE } from \"../lib/clipping\";\n\n/** Where the bubble sits. `start` / `end` follow the reading direction — `end` is the\n * right in LTR and the left in RTL — and are what a layout that mirrors should ask\n * for. `left` / `right` stay physical, for a bubble tied to something that does not\n * mirror (a chart axis, a map). */\nexport type TooltipSide = \"top\" | \"bottom\" | \"left\" | \"right\" | \"start\" | \"end\";\n\n/** What a TAP — a touch or a pen press, never the mouse — on the trigger does to the\n * bubble. See \"A tap is not a hover\" on {@link Tooltip}.\n *\n * - `\"auto\"` (the default): a tap that activates something shows nothing; a tap that\n * activates nothing — on a disabled or `aria-disabled` control (a write lock's\n * reason), or on a trigger with no control at all (a truncated name, a badge) —\n * toggles the bubble, since showing it is then the tap's only answer.\n * - `\"toggle\"`: every tap toggles it — for a control that exists only to explain, such\n * as a \"?\" whose click does nothing.\n * - `\"ignore\"`: a tap never shows it. */\nexport type TooltipTap = \"auto\" | \"toggle\" | \"ignore\";\n\n/** The placement the maths works in: a logical side resolved against the trigger. */\ntype PhysicalSide = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nfunction physicalSide(side: TooltipSide, dir: Direction): PhysicalSide {\n if (side === \"start\") return dir === \"rtl\" ? \"right\" : \"left\";\n if (side === \"end\") return dir === \"rtl\" ? \"left\" : \"right\";\n return side;\n}\n\n/** The floating bubble itself. Uses the shared surface/border/text tokens so it\n * reads as part of the app's chrome (like the top bar and cards) rather than the\n * cold slate pill it used to be.\n *\n * `w-max` keeps a short label on one line — the old `whitespace-nowrap` did that\n * too, but it also let a sentence-length label grow without bound, and a bubble\n * wider than the space beside its trigger gets clipped by whatever overflow\n * container it sits in. So cap it and let long text wrap instead. The cap tracks\n * the viewport as well, for narrow screens where 20rem is already most of it.\n * `side` is a preference rather than an instruction for the PORTALLED variant,\n * which measures the bubble and turns it round when it would not fit\n * (Steering Design feedback #126). The CSS-placed one never turns round, so there\n * `side` is still the whole of which side it is on — but since 0.14 it is slid back\n * along the cross axis when it would cross the viewport edge (keksdose G7), which the\n * viewport term in this cap is what makes possible: a bubble never wider than the glass\n * minus the margins always fits once slid. */\nconst TOOLTIP_SURFACE =\n \"w-max max-w-[min(20rem,calc(100vw-1rem))] rounded-md border border-subtle bg-surface px-2 py-1 text-xs font-medium text-primary shadow-lg\";\n\nconst sidePositionClass: Record<TooltipSide, string> = {\n top: \"bottom-full left-1/2 -translate-x-1/2 mb-1\",\n bottom: \"top-full left-1/2 -translate-x-1/2 mt-1\",\n left: \"right-full top-1/2 -translate-y-1/2 mr-1\",\n right: \"left-full top-1/2 -translate-y-1/2 ml-1\",\n // Logical insets, so CSS resolves the side from the inherited direction with no\n // JavaScript: `end-full` pins the bubble's END edge to the trigger's start.\n start: \"end-full top-1/2 -translate-y-1/2 me-1\",\n end: \"start-full top-1/2 -translate-y-1/2 ms-1\",\n};\n\n/**\n * `extends ComponentPropsWithoutRef<\"span\">` because the wrapper this renders IS a span,\n * and a tooltip is the component a caller most often needs to reach past: it sits\n * between the layout and the control, so a `data-tour` anchor, a test id or an\n * `aria-label` aimed at the trigger used to be swallowed by it.\n *\n * ⚠️ On the empty-label branch there is no wrapper at all, and therefore nothing for\n * those attributes to land on — see the note in the body.\n */\nexport interface TooltipProps extends ComponentPropsWithoutRef<\"span\"> {\n label: ReactNode;\n side?: TooltipSide;\n className?: string;\n /**\n * Where the bubble lives. Left out (the default since 0.10.0), the tooltip decides for\n * itself: the bubble stays next to the trigger unless an ancestor clips or scrolls\n * (`overflow` other than `visible`, or the {@link CLIPS_ATTRIBUTE} marker), in which\n * case it is portalled — see \"Inside a scroll container\" below. `true` always portals, `false` never does; both are exactly\n * what they were before the default existed.\n */\n portal?: boolean;\n /**\n * Mount the IN-PLACE bubble only while it is up (hovered or focused, not dismissed),\n * instead of always. It still sits next to its trigger and is still placed by CSS\n * alone; what changes is that, while closed, it is not in the DOM — so it is not in\n * `getAllByRole(\"tooltip\")`, not in an ancestor's `textContent` and not in an\n * ancestor's accessible name. The trigger is described (`aria-describedby`) while\n * the bubble is up, exactly as a portalled one is. See \"Lazy in place\" on\n * {@link Tooltip} for why this exists (keksdose F6) and why it is not the default.\n *\n * Has no effect on a bubble that is portalled (`portal`, or auto-portalled inside a\n * clipping container) — that one is already mounted only while up.\n */\n lazy?: boolean;\n /** Tag the bubble `data-private`, for a label that repeats the user's own data. */\n redact?: boolean;\n /**\n * What a tap (touch or pen) on the trigger does — see {@link TooltipTap} and \"A tap is\n * not a hover\" below. Default `\"auto\"`: a tap that activates the control shows nothing;\n * one that activates nothing (a disabled control, plain text) toggles the bubble.\n */\n tap?: TooltipTap;\n children: ReactNode;\n /**\n * The trigger WRAPPER — the `<span>` the rest of the props go to, around the caller's\n * child (round 0.35 R4, as Card and EmptyState took one in 0.34) — merged with its own\n * ref, which the bubble is anchored to and dismissed by. Not the bubble: a portalled\n * one exists only while it is up. On the empty-label branch there is no wrapper, so\n * the ref stays `null` (see the ⚠️ above).\n */\n ref?: Ref<HTMLSpanElement>;\n}\n\n/** What each variant below takes: the resolved `side` and `tap`, and every span attribute\n * the caller handed {@link Tooltip}, forwarded to that variant's own wrapper. */\ntype TooltipVariantProps = Omit<TooltipProps, \"side\" | \"portal\" | \"lazy\" | \"tap\"> & {\n side: TooltipSide;\n tap: TooltipTap;\n};\n\n/**\n * Hover/focus label for a control.\n *\n * Two placements, and the choice matters more than it looks. In place, the bubble is\n * always mounted next to the trigger — so a plain render test finds it without a hover —\n * and shown while it is up (`display: none` otherwise, see \"never widens the page\"\n * below). Portalled, the bubble is mounted in `document.body` only while it is up,\n * positioned by measurement.\n *\n * ⚠️ **A bubble that repeats a value has to be redactable.** The consuming app blurs\n * `[data-private]` under a `demo-mode` class on `<html>` — and the portalled bubble is\n * mounted on `document.body`, which is INSIDE that class, so the rule reaches it as\n * long as the bubble is tagged. It is not tagged by default, because most labels are\n * UI strings; pass `redact` on the ones that repeat the user's own data (a truncated\n * payee, an account name, a memo). Getting this wrong is silent: the trigger blurs,\n * the bubble spells the value out on hover.\n *\n * ⚠️ **An empty label renders nothing at all.** `title={payee ?? \"\"}` is an ordinary\n * shape at a call site that reveals truncated text, and the native attribute answers\n * it by showing no tooltip. A component that faithfully rendered an empty bubble\n * would be a worse `title`, so the emptiness check is here rather than at every call\n * site that could forget it.\n *\n * ⚠️ **The bubble describes its trigger, which means cloning it.** `role=\"tooltip\"` is\n * a name for a box, not a relationship — so for as long as nothing referenced the\n * bubble, the label reached the pointer and nobody else. That is worst on the call\n * sites that need it most: the kit's icon-only buttons, where the tooltip IS the\n * label. `aria-describedby` has to sit on the focusable element, which is the\n * caller's child and not this component's wrapper, so the child is CLONED to carry\n * it. A description the caller already set is appended to, never replaced — a field's\n * error text and its hint bubble both describe it. Children that cannot take props (a\n * fragment, a bare string, several elements) are left exactly as they were.\n *\n * ⚠️ **Escape dismisses it (WCAG 1.4.13).** Anything that appears on hover or focus has\n * to be dismissible without moving the pointer, and a bubble is opaque: it lands over\n * the row, field or figure you were reading, and the only way out of it used to be to\n * point somewhere else — which is precisely what you cannot do when what you need to\n * read is underneath it. The listener is the document's rather than the wrapper's\n * because the pointer opens this with the keyboard focus somewhere else entirely, and\n * it is subscribed only while a bubble is actually up: the in-place bubble is always\n * mounted, and a table of forty tooltips must not mean forty keydown listeners.\n *\n * ⚠️ **Inside a scroll container, the bubble has to be portalled — and by default it\n * now is.** An always-mounted bubble is absolutely positioned, but an absolutely\n * positioned descendant still counts towards its scroll-container ancestor's\n * scrollable overflow — so an invisible bubble on a control near the right edge makes\n * the container scroll sideways with nothing to reveal. That is what Keksdose feedback\n * dev#488 reported on the admin roster: 66px of horizontal scroll on a table that fit,\n * 44px of it owed to tooltips nobody could see. And a visible one is clipped by the\n * container's edge. The portalled bubble is `position: fixed` and absent until hovered,\n * so it adds no width and cannot be clipped.\n *\n * Until 0.10.0 the cure was `portal` at the call site, and kastlan asked for it to stop\n * being one: every tooltip in a table, a drawer or a scrolling card had to remember it,\n * and the ones that forgot were only found by someone scrolling sideways. So with\n * `portal` left out, the tooltip looks for a clipping ancestor itself — any element\n * between it and `<body>` whose computed `overflow-x` / `overflow-y` is not `visible`,\n * or that carries {@link CLIPS_ATTRIBUTE} (`data-clips`). The marker is what makes the\n * answer the same under test: jsdom computes no Tailwind, so there every scroller\n * reads `visible` and a table-cell tooltip used to stay in place — its always-mounted\n * bubble then repeated the label in the cell's accessible name and `textContent`\n * (\"CheckingChecking\"), and apps pinned `portal` to stop it. The kit's own scrollers\n * are marked, so a tooltip in a DataTable or Table cell portals in jsdom exactly as it\n * does in the browser.\n * It looks at MOUNT, not only on open: dev#488's phantom scroll is caused by a bubble\n * nobody opened, so a check that waited for the hover would find the damage already\n * done. It looks again on every open, for a container that started scrolling after\n * the tooltip mounted (a table that grew). Only the bubble changes place; the trigger\n * and the caller's child stay mounted, so a switch never costs a focused button its\n * focus. Outside any such container the in-place bubble is kept, which is still the\n * cheaper one and the one a plain render test can find without a hover.\n *\n * Why not simply portal everything? Because the in-place bubble is the one existing\n * app tests rely on (it is in the DOM without a hover), and because it follows its\n * trigger through a scroll or an animation for free — the portalled one re-measures.\n *\n * ⚠️ **Lazy in place: `lazy` keeps the bubble next to its trigger but out of the DOM\n * until it is up (keksdose F6).** `data-clips` cured the doubled text for tooltips\n * inside a table; it did nothing for the ones outside any clipping container, where the\n * always-mounted bubble is still a real `role=\"tooltip\"` node full of text. About seven\n * keksdose sites — column-header tooltips, a direction toggle, an fx-estimate row, a\n * FlagBadge, a toast inside a `<button>` — still pinned `portal` for that alone: their\n * tests found two tooltips with `getAllByRole(\"tooltip\")`, read the label twice in a\n * `textContent`, or got a button whose accessible name had the bubble's sentence glued\n * on. `portal` fixed the test and cost the browser the measured, re-positioning bubble\n * for no reason. `lazy` is the in-place bubble with the portalled one's lifetime: it is\n * rendered only while hovered or focused (and not dismissed), in the same slot, with the\n * same classes, so it looks and sits exactly as the default one does when it is up.\n *\n * Why unmount rather than hide? Hiding was the other option — keep the bubble mounted\n * with `hidden` (or `aria-hidden` plus `display: none` until `:hover` / `:focus-within`)\n * and let `aria-describedby` go on pointing at it, which the accname algorithm allows:\n * a node referenced directly by `aria-describedby` contributes its text even while\n * hidden. That would take the bubble out of the role queries and out of an ancestor's\n * accessible name — but NOT out of `textContent`, which is plain DOM and counts hidden\n * text too, and `textContent` is one of the three things F6 lists. Only a bubble that\n * is not there solves all three. The cost is the portalled variant's: the trigger is\n * described while the bubble is up rather than always. Focusing the trigger IS what\n * puts it up, and React commits the `aria-describedby` in the same task as that focus\n * event, so a screen reader landing on the control still hears it; what goes is the description\n * of a trigger that is read in browse mode without ever being focused.\n *\n * Escape still dismisses it — here by unmounting it, which takes it off the screen and\n * out of the accessibility tree at once — and the next hover or focus brings it back.\n * There is no fade to lose: neither in-place bubble has ever had a transition, only the\n * `opacity` switch, so appearing on a state change looks the same as appearing on\n * `:hover`.\n *\n * Why opt-in rather than the new default? Because the always-mounted bubble is the one\n * existing app tests find without a hover (see above): flipping it would turn every\n * `getByRole(\"tooltip\")` written against 0.12 into a failure in all three apps at once.\n * Reach for `lazy` wherever a `portal` was pinned only to keep a test's DOM clean.\n *\n * ⚠️ **The in-place bubble is clamped to the viewport when it opens (keksdose G7).** The\n * portalled bubble has always measured itself and been pushed back onto the glass\n * ({@link placeTooltip}); the in-place one never learnt its own size, so `side` was the\n * whole of its placement and a `top` / `bottom` bubble sat centred on its trigger\n * whatever that cost. The cost shows on a phone: long labels live at a row's START edge\n * — a gcloud command in keksdose's jobs panel, a canned reply in a support thread — and\n * a 20rem bubble centred on a trigger 16px from the edge hangs half its text off the\n * screen. keksdose pinned `portal` on those sites for that alone, which gave up `lazy`'s\n * point (the cheap, in-place bubble) to buy a placement.\n *\n * So now, when an in-place bubble goes up (default or `lazy`), a layout effect measures\n * it and, if it crosses the viewport edge minus the same margin the portalled bubble\n * keeps, slides it back along its CROSS axis only — sideways for `top` / `bottom`, up or\n * down for the four side placements — with an inline margin (a `transform` until 0.25; see\n * {@link shiftStyle} for why that widened the page). The main axis is left alone on purpose: sliding a\n * `start` bubble along the main axis would slide it over its own trigger, and turning it\n * round is a measured-placement decision this CSS-placed bubble does not make (pass\n * `portal` for that). The width is already capped to the viewport (see\n * `TOOLTIP_SURFACE`), so a bubble can always fit once slid, and a long label wraps\n * instead of growing past the glass.\n *\n * Why on by default, with no prop? Because it is a no-op for every bubble that already\n * fits — the shift is zero unless the bubble would overflow — so the only placements it\n * changes are ones that were broken. The maths is in physical viewport pixels: an RTL\n * row puts its long label at the RIGHT edge and gets slid left by the same code, and the\n * reading direction only decides which edge is kept when a bubble is wider than the room\n * (its start, as {@link placeTooltip} keeps). Under jsdom there is no layout — every rect\n * is zero-sized — and a zero-sized bubble is taken to be unmeasured, so tests see no\n * transform at all. It is measured on open (and when the label or side changes while\n * open), not on every scroll: an in-place bubble follows its trigger for free, and a page\n * that scrolls sideways under an open tooltip is not a case worth a listener per tooltip.\n *\n * ⚠️ **An in-place bubble never widens the page (keksdose run 72, live #381).** Two\n * holes let it, and a page wider than the screen is worse than a bubble cut off: on a\n * phone Chrome then lays every `position: fixed` layer out against the WIDER page while\n * the glass still shows the old width. Measured on a 406px phone: the sync icon at x=254,\n * its `bottom` bubble at 102–422, the page 426px wide, and a FloatingPanel 17px off the\n * glass. First, the clamp compared against `window.innerWidth` — and on a phone that is\n * the layout viewport, which GROWS to the page's width once something sticks out (430 on\n * a 406px screen, measured in Chromium's mobile emulation), so a bubble already past the\n * edge found itself \"inside\" and stayed there. It now measures against the root's client\n * width, which on a phone is the screen (the initial containing block, the width `100vw`\n * and media queries see) whatever overflows, and on a desktop also leaves out a classic\n * scrollbar; the window is the fallback where there is no layout (jsdom). The portalled\n * bubble takes the same width, since a `fixed` bubble placed against a grown layout\n * viewport is off the glass for the same reason. Second, the always-mounted bubble was\n * `opacity: 0` while closed — invisible, but still laid out where the CSS puts it, so a\n * bubble nobody opened, centred on a trigger near the end edge, widened the page all by\n * itself (dev#488's phantom scroll, at the scale of the page instead of a scroller, where\n * the auto-portal cannot help: the page is not an ancestor it can portal out of). It is\n * now `display: none` until it is up, so a closed bubble takes no room at all, and an open\n * one is slid onto the glass in the same layout pass that shows it, before paint. jsdom\n * computes no Tailwind, so tests still find the closed bubble without a hover.\n *\n * ⚠️ **A tap is not a hover (keksdose run 72, live #379).** The bubble used to show on\n * `mouseenter` and on `focus`, and hide only on `mouseleave` and `blur`. A tap on a phone\n * fires an emulated `mouseenter` and, on a button, focuses it — and the virtual mouse and\n * the focus both stay on the button until the next tap somewhere else, so every\n * IconButton's label (the top-bar icons, the sync indicator, a row's actions) stayed up\n * over whatever the tap had just opened. Now a touch or pen press is told apart from the\n * mouse: the emulated `mouseenter` that follows a tap is ignored, the press itself closes\n * a bubble that was up, and a focus that arrives while the page's last input was a finger\n * or a pen — the tap's own, or one a closing dialog RETURNS to the icon it was opened\n * from — shows nothing. The next key press (a Bluetooth keyboard on a tablet) or mouse\n * press makes focus count again, and a mouse entering the trigger hovers as ever, so a\n * hybrid laptop gets both.\n *\n * Focus otherwise shows the bubble, as it always did — the keyboard, a script, a test's\n * `.focus()` — with one more exception: the focus a mouse press on the trigger gives it.\n * There it follows `:focus-visible`, so a text field still shows its bubble while you type\n * and a clicked button does not hold it: a desktop click behaves as it always did while\n * the pointer is over the button (the hover shows it) and lets it go when the pointer\n * leaves, where the in-place bubble used to linger until the button lost focus — the same\n * left-behind bubble on a smaller scale, and what the portalled bubble always did. A\n * keyboard user's bubble, in turn, now stays up when a mouse passes over and leaves (the\n * portalled one used to close). Any `pointerdown` outside the trigger closes an open\n * bubble too — the Safari case, where a tap elsewhere moves no focus.\n *\n * What a tap DOES show is {@link TooltipTap} (`tap`). By default a tap that activates\n * something shows nothing — the action is the answer — while a tap that activates nothing\n * toggles the bubble, because there the bubble is the only answer: a write-locked or\n * disabled control, whose bubble is the reason it did not respond, and a trigger with no\n * control at all, a truncated payee or a badge, whose bubble is the rest of the text. A\n * second tap, a tap anywhere else, Escape or the focus leaving closes it. \"Activates\" is\n * read off the DOM — a link, a button, a field, a label, an element with a widget role\n * (`button`, `link`, `checkbox`, `option`, …), or a DataTable row (`tr[tabindex]`) — from\n * the tapped element outwards, past the tooltip, so a glyph inside a clickable row or a\n * button counts as that row or button. What it cannot read is a click handler on an\n * element with no role (give it `role=\"button\"`, which it needs anyway, or pass\n * `tap=\"ignore\"`), and a button that exists only to explain, whose click does nothing —\n * FieldHint's \"?\" — which says so with `tap=\"toggle\"`.\n *\n * Why not show the label on a long press, as Android does for its own icons? Because the\n * web gives no reliable long press on a control: browsers disagree on whether the release\n * that ends one still clicks the button, and the button's own long press — the context\n * menu, text selection, a row's drag — claims the gesture first on others. A gesture meant\n * to ask \"what does this do\" must not risk doing it. Screen readers do not need it (the\n * bubble is in `aria-describedby`, and an IconButton's label is its name), and a tap on\n * anything that does nothing already shows it. No scroll listener either: every touch scroll begins with a `pointerdown`, which\n * already closes the bubble, and closing on scroll would close a keyboard user's bubble\n * the moment Tab scrolled its control into view.\n */\nexport function Tooltip({\n label,\n side = \"top\",\n className,\n portal,\n lazy = false,\n redact = false,\n tap = \"auto\",\n children,\n ...rest\n}: TooltipProps) {\n // No label, no bubble — and no wrapper either, so a conditional tooltip costs the\n // layout nothing on the branch where it does not apply. `...rest` goes with the\n // wrapper on this branch, which is the documented limit of the pass-through: there is\n // no element left to put an attribute on.\n if (isEmptyLabel(label)) return <>{children}</>;\n if (portal === true) {\n return (\n <PortalTooltip label={label} side={side} className={className} redact={redact} tap={tap} {...rest}>\n {children}\n </PortalTooltip>\n );\n }\n // Both variants are separate components so that `Tooltip` itself can keep calling NO\n // hooks: the empty-label branch above returns before either of them, and a hook after\n // a conditional return is a hooks-order bug rather than a style violation.\n return (\n <InPlaceTooltip\n label={label}\n side={side}\n className={className}\n redact={redact}\n detect={portal === undefined}\n lazy={lazy}\n tap={tap}\n {...rest}\n >\n {children}\n </InPlaceTooltip>\n );\n}\n\n/** The pointer handlers a caller may hand {@link Tooltip} that the trigger also needs:\n * both run, the trigger's first. (The mouse and focus handlers are the trigger's alone,\n * as they have always been.) */\ntype PointerHandlers = Pick<\n ComponentPropsWithoutRef<\"span\">,\n \"onPointerEnter\" | \"onPointerDown\" | \"onPointerUp\" | \"onPointerCancel\"\n>;\n\n/** A press that is not the mouse's: a finger, or a pen on the glass. */\nfunction isTap(pointerType: string): boolean {\n return pointerType === \"touch\" || pointerType === \"pen\";\n}\n\n/**\n * What a tap ACTIVATES, for `tap=\"auto\"`: the tapped element or the nearest ancestor —\n * inside the tooltip or round it — that a tap would do something with. The DataTable's\n * own list of row-owned controls (`OWN_CONTROL`), plus `tr[tabindex]` — a clickable\n * DataTable row is a roving tab stop, so all but one of its rows say `-1` — and the other\n * widget roles a tap selects or toggles.\n *\n * Two things on the DataTable's list are left off on purpose. A bare `tabindex`: a roving\n * tab stop is `0` on one element of a widget and `-1` on the rest, so it would make one\n * cell of a display-only grid \"activate\" and its neighbours not — and dialogs, panels and\n * `<main>` carry `-1` only to be focusable by script. And `gridcell` / `row`: the\n * CalendarHeatmap's days are display-only grid cells whose tooltip is the day's value,\n * and a tap is the only way to read it on a phone; a heatmap day that does select is a\n * `<button>`, caught above.\n */\nconst ACTIVATES = [\n \"a[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"label\",\n \"summary\",\n '[contenteditable=\"\"]',\n '[contenteditable=\"true\"]',\n \"tr[tabindex]\",\n ...[\n \"button\",\n \"link\",\n \"checkbox\",\n \"radio\",\n \"switch\",\n \"tab\",\n \"menuitem\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"option\",\n \"treeitem\",\n \"slider\",\n \"spinbutton\",\n \"combobox\",\n ].map((role) => `[role=\"${role}\"]`),\n].join(\",\");\n\n/** Whether a tap on `target` should toggle the bubble — see {@link TooltipTap}. Under\n * `auto`: when the tap activates nothing, either because there is no control under it\n * or because the control is disabled (natively, or `aria-disabled` — a write lock). */\nfunction tapShows(tap: TooltipTap, target: EventTarget | null): boolean {\n if (tap !== \"auto\") return tap === \"toggle\";\n if (!(target instanceof Element)) return true;\n const control = target.closest(ACTIVATES);\n return control === null || control.matches(\":disabled\") || control.closest('[aria-disabled=\"true\"]') !== null;\n}\n\n/**\n * The last kind of input the document saw: a `pointerdown`'s `pointerType` (\"mouse\",\n * \"touch\", \"pen\"), \"keyboard\" after a key press, `null` before either. ONE pair of\n * capturing listeners for the whole page, installed by the first trigger that mounts —\n * not a pair per tooltip, which a table of forty would multiply.\n *\n * Why a page-wide note and not only the trigger's own: the focus that brings a label back\n * after a tap is not always the tap's. A tap on a top-bar icon opens a dialog; closing it\n * (another tap) RETURNS the focus to the icon by script, and that focus arrives with no\n * press on the icon at all. On a phone it would put the label up over the page the dialog\n * just left, and keep it there. Keys an on-screen keyboard sends while typing\n * (`Unidentified`, a composition) and shortcut chords do not count as the keyboard.\n */\nlet lastInput: string | null = null;\nlet trackingInput = false;\n\nfunction trackInput(): void {\n if (trackingInput || typeof document === \"undefined\") return;\n trackingInput = true;\n document.addEventListener(\n \"pointerdown\",\n (e) => {\n lastInput = e.pointerType;\n },\n { capture: true, passive: true },\n );\n document.addEventListener(\n \"keydown\",\n (e) => {\n if (e.key === \"Unidentified\" || e.isComposing || e.ctrlKey || e.metaKey || e.altKey) return;\n lastInput = \"keyboard\";\n },\n { capture: true, passive: true },\n );\n}\n\n/**\n * Whether a focus should show the bubble. Not after a tap — the last input was a finger\n * or a pen, whoever moved the focus. Not after a mouse press on this very trigger either,\n * unless the browser would draw a focus ring there (`:focus-visible`: a text field, not a\n * button) — the hover already shows it while the pointer is there. Every other focus\n * shows it as every focus did before: the keyboard, a script, and a test's `.focus()` or\n * `fireEvent.focus` (jsdom's own `:focus-visible` guesses from whatever events the test\n * file fired before, so it is consulted only where a press makes the answer certain).\n */\nfunction focusShows(target: EventTarget, pressedByMouse: boolean): boolean {\n if (lastInput !== null && isTap(lastInput)) return false;\n if (!pressedByMouse || !(target instanceof Element)) return true;\n try {\n return target.matches(\":focus-visible\");\n } catch {\n return true;\n }\n}\n\n/**\n * Whether the bubble is up, from what the trigger hears — shared by both variants so the\n * in-place and the portalled bubble cannot disagree about it. See \"A tap is not a hover\"\n * on {@link Tooltip} for the rules; this is their bookkeeping.\n *\n * Three ways up — hovered by a mouse (or a hovering pen), focused (see {@link focusShows}),\n * and tapped (`tap`) — and Escape over all three (`dismissed`), re-armed by the next\n * request rather than by an effect watching the flags: coming back to a trigger is a\n * fresh request for its label, and an effect would also re-show the bubble under a\n * pointer that never left. `onArm` runs with each request, for the variant's own per-open\n * work (the reading direction, the clipping check).\n *\n * The refs change nothing on screen, only how the next event is read. `pressedByTouch`\n * is set by a touch or pen arriving or pressing — which precedes the emulated\n * `mouseenter` a tap produces (pointer events first, then the compatibility mouse\n * events) — and cleared by a mouse, or a pen that is not pressing, entering again, so a\n * hybrid laptop's mouse still hovers. `pressedByMouse` spans one mouse press, the window\n * in which that press's own focus arrives.\n */\nfunction useTooltipTrigger(\n triggerRef: RefObject<HTMLSpanElement | null>,\n tap: TooltipTap,\n passed: PointerHandlers,\n onArm: (el: HTMLElement) => void,\n) {\n const [hovered, setHovered] = useState(false);\n const [focused, setFocused] = useState(false);\n const [tapped, setTapped] = useState(false);\n const [dismissed, setDismissed] = useState(false);\n const pressedByTouch = useRef(false);\n const pressedByMouse = useRef(false);\n // The tap in progress, and whether a bubble was up when it began: a tap on an open\n // bubble closes it (at pointerdown) and must not open it again at pointerup.\n const press = useRef<{ wasOpen: boolean } | null>(null);\n useEffect(trackInput, []);\n const open = (hovered || focused || tapped) && !dismissed;\n const close = () => {\n setHovered(false);\n setFocused(false);\n setTapped(false);\n };\n // A layer on the Escape stack while up (Wave 2, DUP-1): over a dialog, the first\n // Escape dismisses the bubble — WCAG 1.4.13, without moving focus — and the next one\n // the dialog. Inside a Modal the bubble used to be beyond reach of the key.\n // Not an overlay: a tooltip annotates what it is on, so a floating launcher with one\n // does not step aside for its own bubble (`hideWhenOverlay`, 0.49 KIT-11).\n useDismissLayer(open, () => setDismissed(true), { overlay: false });\n // A press anywhere else closes it — the case a blur does not cover: Safari moves no\n // focus on a tap, and a tap on plain text moves it nowhere.\n useOutsideClick(triggerRef, close, open);\n const arm = (el: HTMLElement) => {\n setDismissed(false);\n onArm(el);\n };\n const handlers = {\n onPointerEnter: (e: PointerEvent<HTMLSpanElement>) => {\n if (e.pointerType === \"touch\") pressedByTouch.current = true;\n else if (e.buttons === 0) pressedByTouch.current = false;\n passed.onPointerEnter?.(e);\n },\n onPointerDown: (e: PointerEvent<HTMLSpanElement>) => {\n if (isTap(e.pointerType)) {\n pressedByTouch.current = true;\n press.current = { wasOpen: open };\n close();\n } else {\n pressedByTouch.current = false;\n pressedByMouse.current = true;\n }\n passed.onPointerDown?.(e);\n },\n onPointerUp: (e: PointerEvent<HTMLSpanElement>) => {\n pressedByMouse.current = false;\n const began = press.current;\n press.current = null;\n if (began && !began.wasOpen && isTap(e.pointerType) && tapShows(tap, e.target)) {\n setTapped(true);\n arm(e.currentTarget);\n }\n passed.onPointerUp?.(e);\n },\n // The browser took the gesture over — a scroll, a pinch: not a tap.\n onPointerCancel: (e: PointerEvent<HTMLSpanElement>) => {\n pressedByMouse.current = false;\n press.current = null;\n passed.onPointerCancel?.(e);\n },\n onMouseEnter: (e: MouseEvent<HTMLSpanElement>) => {\n if (pressedByTouch.current) return;\n setHovered(true);\n arm(e.currentTarget);\n },\n onMouseLeave: () => setHovered(false),\n onFocus: (e: FocusEvent<HTMLSpanElement>) => {\n if (!focusShows(e.target, pressedByMouse.current)) return;\n setFocused(true);\n arm(e.currentTarget);\n },\n onBlur: (e: FocusEvent<HTMLSpanElement>) => {\n setFocused(false);\n if (e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)) return;\n setTapped(false);\n },\n };\n return { open, dismissed, handlers };\n}\n\n/**\n * The trigger wrapper's own ref, and one callback ref that hands the wrapper to the\n * caller's `ref` as well (round 0.35 R4).\n */\nfunction useTriggerRef(ref: Ref<HTMLSpanElement> | undefined) {\n const triggerRef = useRef<HTMLSpanElement | null>(null);\n const setTrigger = useCallback(\n (el: HTMLSpanElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref],\n );\n return { triggerRef, setTrigger };\n}\n\n/** The variant that lives next to its trigger, and — when `detect` is on, which is the\n * default — moves its bubble to `<body>` when that turns out to be inside a clipping\n * container (see \"Inside a scroll container\" on {@link Tooltip}).\n *\n * Whether the bubble is up is {@link useTooltipTrigger}'s, shared with the portalled\n * variant. Until 0.25 the always-mounted bubble showed itself with CSS alone —\n * `group-hover` / `group-focus-within` — which is exactly what a tap on a phone could not\n * get rid of: the tapped button keeps the focus, and `:focus-within` cannot be told that\n * the focus came from a finger. It is shown from the same state as every other bubble\n * now, and is `display: none` while closed (see \"never widens the page\" on\n * {@link Tooltip}).\n *\n * ONE component for both placements rather than a switch between the two variants: a\n * switch would be a different component at the same place in the tree, and React\n * would remount the caller's child with it — a button that loses focus the moment\n * its own tooltip decided where to go. Here the wrapper and the child stay put and\n * only the bubble's slot changes. */\nfunction InPlaceTooltip({\n label,\n side,\n className,\n redact,\n detect,\n lazy,\n tap,\n children,\n ref,\n ...rest\n}: TooltipVariantProps & { detect: boolean; lazy: boolean }) {\n const id = useId();\n const { triggerRef, setTrigger } = useTriggerRef(ref);\n const [clipped, setClipped] = useState(false);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n // The reading direction on every request (the clamp keeps the start edge of a bubble\n // too wide for the room); the clipping check too, for a container that began to scroll\n // after mount.\n const { open, dismissed, handlers } = useTooltipTrigger(triggerRef, tap, rest, (el) => {\n setDir(dirOf(el));\n if (detect) setClipped(hasClippingAncestor(el));\n });\n // keksdose G7: slide an open in-place bubble back onto the screen. See \"The in-place\n // bubble is clamped\" on {@link Tooltip}.\n const bubbleRef = useRef<HTMLSpanElement | null>(null);\n const { shift, measuring } = useViewportClamp(bubbleRef, open && !clipped, side, label, dir);\n // At mount, before the first paint: the in-place bubble inside a scroller is the\n // phantom-scroll bug whether or not anyone opens it.\n useLayoutEffect(() => {\n if (detect) setClipped(hasClippingAncestor(triggerRef.current));\n // `triggerRef` is the same object for the component's life (useTriggerRef's useRef).\n }, [detect, triggerRef]);\n return (\n // No role for this span: its handlers track whether the bubble is up and activate\n // nothing; the caller's child is the interactive element, keeps its own handlers,\n // and its focus shows the bubble too.\n <span\n // `...rest` first: the handlers below are what decides whether a bubble is up, and\n // a caller passing an `onFocus` of its own must not replace them (its pointer\n // handlers are called from them).\n {...rest}\n ref={setTrigger}\n // Clipped for the one render in which the bubble is measured (see useViewportClamp):\n // that layout must not reach the page's width.\n style={measuring ? { ...rest.style, overflow: \"clip\" } : rest.style}\n className={cn(\"relative inline-flex\", !clipped && \"group/tooltip\", className)}\n // These track WHETHER A BUBBLE IS UP. They activate nothing — the only thing here\n // that can be activated is the caller's child, which keeps every handler it\n // arrived with — so this wrapper needs no role and no key handling of its own.\n {...handlers}\n >\n {/* In place the bubble is always there to point at; portalled or lazy, only while up. */}\n {describedBy(children, clipped || lazy ? (open ? id : undefined) : dismissed ? undefined : id)}\n {clipped ? (\n open && <PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />\n ) : lazy ? (\n // keksdose F6: the in-place slot and classes, the portalled lifetime. Mounted only\n // while up, so it is visible whenever it exists.\n open && (\n <span\n ref={bubbleRef}\n id={id}\n role=\"tooltip\"\n data-private={redact ? \"\" : undefined}\n style={shiftStyle(shift)}\n className={cn(TOOLTIP_SURFACE, \"pointer-events-none absolute z-50 opacity-100\", sidePositionClass[side])}\n >\n {label}\n </span>\n )\n ) : (\n <span\n ref={bubbleRef}\n id={id}\n role=\"tooltip\"\n style={shiftStyle(shift)}\n // The `hidden` ATTRIBUTE, not a class: dismissing has to take the bubble out of\n // the accessibility tree as well as off the screen, or a screen reader still\n // reads out the description of a bubble the user just closed. Merely CLOSED it\n // is the `hidden` CLASS — `display: none` in the browser, so it takes no room\n // and widens nothing (live #381), and nothing at all under jsdom, where tests\n // have always found this bubble without a hover. `aria-describedby` reads a\n // `display: none` node all the same.\n hidden={dismissed || undefined}\n data-private={redact ? \"\" : undefined}\n className={cn(\n TOOLTIP_SURFACE,\n \"pointer-events-none absolute z-50\",\n open ? \"opacity-100\" : \"hidden\",\n sidePositionClass[side],\n )}\n >\n {label}\n </span>\n )}\n </span>\n );\n}\n\n/** Hand `children` the bubble's id as an `aria-describedby`, if it is an element that\n * can hold one. `id` is undefined while there is no bubble to point at — a dangling\n * reference describes the trigger as nothing at all, which is worse than silence. */\nfunction describedBy(children: ReactNode, id: string | undefined): ReactNode {\n if (id === undefined || !isValidElement(children)) return children;\n const child = children as ReactElement<{ \"aria-describedby\"?: string }>;\n // Fragments, Suspense and friends are symbol-typed and take no DOM props; cloning one\n // with an aria attribute warns in development and drops it in production.\n if (typeof child.type === \"symbol\") return children;\n const own = child.props[\"aria-describedby\"];\n return cloneElement(child, { \"aria-describedby\": own ? `${own} ${id}` : id });\n}\n\n/** \"Would this bubble be blank.\" Only the values a call site actually produces when\n * it has nothing to say — `\"\"`, `null`, `undefined`, `false` from a `&&` guard. A\n * numeric `0` is a real label and stays one. */\nfunction isEmptyLabel(label: ReactNode): boolean {\n return label == null || label === false || (typeof label === \"string\" && label.trim() === \"\");\n}\n\nconst TOOLTIP_GAP = 4;\n\n/** How close to the viewport edge a bubble may sit. Not zero: a label flush\n * against the glass reads as clipped even when every character is on screen. */\nconst TOOLTIP_MARGIN = 4;\n\n/** A cross-axis slide, in viewport pixels, for an in-place bubble. */\ninterface Shift {\n x: number;\n y: number;\n}\n\nconst NO_SHIFT: Shift = { x: 0, y: 0 };\n\n/** An in-place bubble's slide and the label, side and direction it was measured for. */\ninterface Placement {\n shift: Shift;\n for: { side: TooltipSide; label: ReactNode; dir: Direction } | null;\n}\n\nconst UNPLACED: Placement = { shift: NO_SHIFT, for: null };\n\n/** How far to slide the span `[low, high]` so it sits inside `[TOOLTIP_MARGIN,\n * extent - TOOLTIP_MARGIN]`. Zero when it already does. When the span is wider than the\n * room, the edge it keeps is its START — as in {@link clamp}, the start of a label is the\n * half worth keeping — which is the low edge unless `keepHigh` says the start is the\n * high one (an RTL label, along x). The width cap means that only happens on a viewport\n * barely wider than the margins, or one whose scrollbar the cap's `100vw` counts. */\nfunction slideInto(low: number, high: number, extent: number, keepHigh = false): number {\n const min = TOOLTIP_MARGIN;\n const max = extent - TOOLTIP_MARGIN;\n if (high - low > max - min) return keepHigh ? max - high : min - low;\n if (low < min) return min - low;\n if (high > max) return max - high;\n return 0;\n}\n\n// The glass a bubble has to stay on — the root's client width, not `innerWidth` (keksdose\n// run 72, live #381) — is `viewportSize` in use-anchored-panel.ts since 0.34 (Wave 2,\n// DUP-7), where every anchored panel now reads it.\n\n/**\n * The in-place bubble's viewport clamp (keksdose G7): while `active`, measure the bubble\n * in a LAYOUT effect — before paint, so there is no frame with the label off the screen —\n * and return the cross-axis slide that brings it inside the viewport ({@link viewportSize})\n * minus `TOOLTIP_MARGIN`. `top` / `bottom` slide along x; `left` / `right` / `start` / `end`\n * along y. {@link NO_SHIFT} while closed, so the next open measures the bubble where the\n * CSS alone puts it. `dir` only decides which edge survives a bubble wider than the room.\n *\n * `measuring` is true for the one render in which the bubble is measured — on open, and\n * again when the label, side or direction changes while open — and the caller clips its\n * wrapper (`overflow: clip`) for that render. Measuring means laying the bubble out where\n * the CSS alone puts it, which is past the edge whenever there is something to slide, and\n * that layout counts towards the page's scrollable overflow even though it is never\n * painted. Chrome on a phone grows the layout viewport to it and does not always give the\n * width back: in an RTL page, where overflow on the left is scrollable, a bubble measured\n * at −24 left the page 426 wide and scrolled by −20 after the slide had already brought it\n * to 4 (Chromium mobile emulation, 406px). A clipped wrapper keeps the measurement out of\n * every ancestor's overflow — `clip` and not `hidden`, so the wrapper never becomes a\n * scroll container — and it is unclipped, with the slide, in the same task, before paint.\n *\n * The rect it reads already includes the slide it applied last time (a label that\n * changed while open), so that slide is taken back out before deciding the new one.\n * A zero-sized rect means no layout (jsdom, `display: none`) and leaves the bubble put.\n */\nfunction useViewportClamp(\n bubbleRef: RefObject<HTMLSpanElement | null>,\n active: boolean,\n side: TooltipSide,\n label: ReactNode,\n dir: Direction,\n): { shift: Shift; measuring: boolean } {\n // The slide, and what it was measured for: anything else on screen — a fresh open, a\n // new label, side or direction — is not measured yet, and renders clipped until it is.\n const [placement, setPlacement] = useState<Placement>(UNPLACED);\n const measured =\n placement.for !== null && placement.for.side === side && placement.for.label === label && placement.for.dir === dir;\n useLayoutEffect(() => {\n const el = bubbleRef.current;\n if (!active || !el) {\n setPlacement(UNPLACED);\n return;\n }\n if (measured) return;\n const r = el.getBoundingClientRect();\n const unlaid = r.width === 0 && r.height === 0;\n const horizontal = side === \"top\" || side === \"bottom\";\n const viewport = viewportSize();\n setPlacement(({ shift: previous }) => {\n const next = unlaid\n ? previous\n : horizontal\n ? { x: slideInto(r.left - previous.x, r.right - previous.x, viewport.width, dir === \"rtl\"), y: 0 }\n : { x: 0, y: slideInto(r.top - previous.y, r.bottom - previous.y, viewport.height) };\n const shift = next.x === previous.x && next.y === previous.y ? previous : next;\n return { shift, for: { side, label, dir } };\n });\n }, [bubbleRef, active, measured, side, label, dir]);\n return { shift: placement.shift, measuring: active && !measured };\n}\n\n/**\n * The inline style for a slide: none at all when there is nothing to slide, so a bubble\n * that fits renders exactly as it did before G7.\n *\n * A MARGIN, not a `transform` (0.14–0.24 used `transform: translate(…)`), because of what\n * Blink does with the page's width (keksdose run 72, live #381). The slide is decided after\n * a layout that placed the bubble where the CSS alone puts it — past the edge — and that\n * layout already counted it into the page's scrollable overflow. A later change to\n * `transform` alone re-paints but does not re-lay-out, so that overflow was never taken\n * back: measured in Chromium's mobile emulation, a bubble shown at 108–428 on a 406px\n * screen and then slid to 400 by a transform left the page 428 wide, the layout viewport\n * with it (`position: fixed` layers laid out against 428), until the bubble closed. The\n * same slide as a margin is a layout change; the page went back to 406 in the same frame.\n *\n * Physical margins, matching the physical maths: `margin-left` moves a `top` / `bottom`\n * bubble, whose placement is the physical `left: 50%`; `margin-top` moves the four side\n * placements, placed by `top: 50%`. Neither is a margin the placement classes set — they\n * keep their gap on the main axis (`mb-1`, `ms-1`, …).\n */\nfunction shiftStyle(shift: Shift) {\n if (shift.x === 0 && shift.y === 0) return undefined;\n return shift.x !== 0 ? { marginLeft: shift.x } : { marginTop: shift.y };\n}\n\nconst portalTransformBySide: Record<PhysicalSide, string> = {\n right: \"translate(0, -50%)\",\n left: \"translate(-100%, -50%)\",\n top: \"translate(-50%, -100%)\",\n bottom: \"translate(-50%, 0)\",\n};\n\n/** Anchor point (viewport px) for the tooltip on the given side of `r`. Paired\n * with {@link portalTransformBySide}, which shifts the box onto that point. */\nfunction tooltipAnchor(r: AnchorRect, side: PhysicalSide): { left: number; top: number } {\n switch (side) {\n case \"right\":\n return { left: r.right + TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"left\":\n return { left: r.left - TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"top\":\n return { left: r.left + r.width / 2, top: r.top - TOOLTIP_GAP };\n case \"bottom\":\n return { left: r.left + r.width / 2, top: r.bottom + TOOLTIP_GAP };\n }\n}\n\nexport interface TooltipSize {\n width: number;\n height: number;\n}\n\nexport interface TooltipViewport {\n width: number;\n height: number;\n}\n\nexport interface TooltipPlacement {\n left: number;\n top: number;\n /** Which side it ended up on, which need not be the one that was asked for. */\n side: PhysicalSide;\n}\n\nconst opposite: Record<PhysicalSide, PhysicalSide> = {\n left: \"right\",\n right: \"left\",\n top: \"bottom\",\n bottom: \"top\",\n};\n\n/** Whether the bubble clears the viewport edge on `side` of the trigger. */\nfunction roomOn(r: AnchorRect, side: PhysicalSide, size: TooltipSize, viewport: TooltipViewport): boolean {\n switch (side) {\n case \"left\":\n return r.left - TOOLTIP_GAP - size.width >= TOOLTIP_MARGIN;\n case \"right\":\n return r.right + TOOLTIP_GAP + size.width <= viewport.width - TOOLTIP_MARGIN;\n case \"top\":\n return r.top - TOOLTIP_GAP - size.height >= TOOLTIP_MARGIN;\n case \"bottom\":\n return r.bottom + TOOLTIP_GAP + size.height <= viewport.height - TOOLTIP_MARGIN;\n }\n}\n\nfunction sameRoom(\n a: { size: TooltipSize; viewport: TooltipViewport },\n b: { size: TooltipSize; viewport: TooltipViewport },\n): boolean {\n return (\n a.size.width === b.size.width &&\n a.size.height === b.size.height &&\n a.viewport.width === b.viewport.width &&\n a.viewport.height === b.viewport.height\n );\n}\n\nfunction clamp(value: number, low: number, high: number): number {\n // `high` first, so a bubble taller or wider than the viewport is pinned to the\n // top-left corner rather than to the bottom-right one — the start of a label\n // is the half worth keeping.\n return Math.max(low, Math.min(value, high));\n}\n\n/**\n * Where the bubble actually goes, given how big it turned out to be.\n *\n * Two rules, and they are separate because they fix separate failures.\n *\n * **Turn round when the preferred side has no room.** `side` says which side of\n * the trigger the label reads best on, and on a form near the left edge of the\n * window that side is off the screen — the capped bubble can only wrap, not\n * move, so what the reader gets is a sentence with its first half outside the\n * glass. Flipped only when the *other* side is genuinely better: a trigger in a\n * viewport too narrow for the bubble either way keeps the side it asked for, and\n * the clamp below does what it can.\n *\n * **Then clamp both axes.** The cross axis is the one that needs it — a `top`\n * bubble is centred on the trigger, so a trigger near the left edge pushes half\n * the label off even though the side it is on is right — and clamping the main\n * axis too costs nothing and covers the flip having nowhere to land.\n *\n * Pure, and measured in viewport pixels throughout, so it can be tested without\n * a layout: the caller supplies the trigger's rect, the bubble's own size and\n * the window.\n */\nexport function placeTooltip(\n r: AnchorRect,\n side: PhysicalSide,\n size: TooltipSize,\n viewport: TooltipViewport,\n): TooltipPlacement {\n const chosen = roomOn(r, side, size, viewport) || !roomOn(r, opposite[side], size, viewport) ? side : opposite[side];\n const point = tooltipAnchor(r, chosen);\n const box =\n chosen === \"left\"\n ? { left: point.left - size.width, top: point.top - size.height / 2 }\n : chosen === \"right\"\n ? { left: point.left, top: point.top - size.height / 2 }\n : chosen === \"top\"\n ? { left: point.left - size.width / 2, top: point.top - size.height }\n : { left: point.left - size.width / 2, top: point.top };\n return {\n left: clamp(box.left, TOOLTIP_MARGIN, viewport.width - size.width - TOOLTIP_MARGIN),\n top: clamp(box.top, TOOLTIP_MARGIN, viewport.height - size.height - TOOLTIP_MARGIN),\n side: chosen,\n };\n}\n\nfunction PortalTooltip({ label, side, className, redact, tap, children, ref, ...rest }: TooltipVariantProps) {\n const { triggerRef, setTrigger } = useTriggerRef(ref);\n // The trigger's reading direction, read when the bubble is asked for (an event, not a\n // render): it resolves `start` / `end`, and the portalled bubble — which has left the\n // subtree it would have inherited `dir` from — carries it too.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const id = useId();\n // Escape closes it outright, since this variant's bubble only exists while it is\n // shown. The next mouseenter/focus brings it back, which is the behaviour WCAG\n // 1.4.13 asks for: dismissible now, still available when you ask again.\n const { open, handlers } = useTooltipTrigger(triggerRef, tap, rest, (el) => setDir(dirOf(el)));\n\n return (\n <>\n {/* As in `InPlaceTooltip`, no role: the handlers only show and hide the bubble; the\n caller's child is the interactive element, and focusing it shows the bubble. */}\n <span\n // As in `InPlaceTooltip`: the caller's attributes first, the handlers that run\n // this component after them. The BUBBLE is deliberately not given them — it\n // is portalled to `<body>`, and an id or a tour anchor duplicated onto a node\n // that only exists while hovered would match twice or match nothing. So, too,\n // the caller's `ref` (round 0.35 R4).\n {...rest}\n ref={setTrigger}\n className={cn(\"relative inline-flex\", className)}\n {...handlers}\n >\n {describedBy(children, open ? id : undefined)}\n </span>\n {open && <PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />}\n </>\n );\n}\n\n/** The measured, `position: fixed` bubble on `<body>`, mounted only while it is up.\n * Shared by {@link PortalTooltip} and the in-place variant's clipped mode, so the two\n * cannot place a bubble differently. */\nfunction PortalBubble({\n triggerRef,\n id,\n label,\n side,\n dir,\n redact,\n}: {\n triggerRef: RefObject<HTMLSpanElement | null>;\n id: string;\n label: ReactNode;\n side: TooltipSide;\n dir: Direction;\n redact: boolean | undefined;\n}) {\n const bubbleRef = useRef<HTMLSpanElement | null>(null);\n // The measure + scroll/resize-tracking lifecycle is owned by useAnchoredRect;\n // here we only map the rect to a side-specific anchor point.\n const rect = useAnchoredRect(triggerRef, true);\n // The bubble's own size and the window it has to fit in — neither of which is\n // knowable in render: the width is whatever the label wrapped to inside the\n // cap, and reading `window` while rendering is not a pure thing to do. Both\n // are taken in a LAYOUT effect, so the correction lands before the browser\n // paints and there is no frame in which the label sits off the screen.\n const [room, setRoom] = useState<{ size: TooltipSize; viewport: TooltipViewport } | null>(null);\n useLayoutEffect(() => {\n const measured = bubbleRef.current?.getBoundingClientRect();\n setRoom((previous) => {\n if (!measured) return null;\n const next = {\n size: { width: measured.width, height: measured.height },\n // The glass, not the window: see viewportSize (live #381).\n viewport: viewportSize(),\n };\n // Only publish what actually CHANGED: every re-measure allocates a fresh\n // object, and a new object on every scroll event would re-render the\n // bubble forever.\n return previous && sameRoom(previous, next) ? previous : next;\n });\n }, [rect, label]);\n\n const physical = physicalSide(side, dir);\n const point = rect ? tooltipAnchor(rect, physical) : null;\n // Unmeasured on the very first pass, where the anchor point plus the side's\n // own transform is exactly what this always did. One layout effect later the\n // size is known and the placement is decided properly.\n const placed = rect && room ? placeTooltip(rect, physical, room.size, room.viewport) : null;\n\n if (!point || typeof document === \"undefined\") return null;\n return createPortal(\n <span\n ref={bubbleRef}\n id={id}\n role=\"tooltip\"\n dir={dir}\n data-private={redact ? \"\" : undefined}\n style={\n placed\n ? { position: \"fixed\", left: placed.left, top: placed.top }\n : {\n position: \"fixed\",\n left: point.left,\n top: point.top,\n transform: portalTransformBySide[physical],\n }\n }\n className={cn(TOOLTIP_SURFACE, \"pointer-events-none z-50\")}\n >\n {label}\n </span>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -67,10 +67,7 @@ function TranslationReviewEditor({
|
|
|
67
67
|
if (!onSave) return;
|
|
68
68
|
const trimmedNote = note.trim() || null;
|
|
69
69
|
const sent = verdict === "APPROVED" ? null : suggestion.trim() || null;
|
|
70
|
-
void run(
|
|
71
|
-
verdict === "APPROVED" ? "approve" : "flag",
|
|
72
|
-
() => onSave(reviewWrite(row, verdict, trimmedNote, sent))
|
|
73
|
-
);
|
|
70
|
+
void run(verdict === "APPROVED" ? "approve" : "flag", () => onSave(reviewWrite(row, verdict, trimmedNote, sent)));
|
|
74
71
|
};
|
|
75
72
|
return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0 space-y-3 text-sm", className), children: [
|
|
76
73
|
/* @__PURE__ */ jsx("code", { className: "block break-all text-xs text-muted", children: row.key }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/translation-review-editor.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { useKitFormat } from \"../lib/format\";\nimport { placeholderTokens, reviewWrite } from \"../lib/translation-review\";\nimport type {\n TranslationReviewKey,\n TranslationReviewWrite,\n TranslationRow,\n TranslationVerdict,\n} from \"../lib/translation-review\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Caption, SectionLabel } from \"./text\";\nimport { DEFAULT_TRANSLATION_REVIEW_LABELS } from \"./translation-review-labels\";\nimport type { TranslationReviewLabels } from \"./translation-review-labels\";\nimport { Button } from \"./button\";\nimport { Textarea } from \"./textarea\";\nimport { COMMIT_EXCEPT_BILLING } from \"./write-lock\";\n\n/** A callback may return a promise: resolve and the editor closes, reject and it stays\n * open with the error. */\ntype MaybePromise = void | Promise<unknown>;\n\nexport interface TranslationReviewEditorProps {\n row: TranslationRow;\n /** The reference language's own name (\"English\", \"Deutsch\") — the app's choice of\n * reference, so the app's word for it. */\n referenceLabel: ReactNode;\n /** The reviewed language's own name (\"Français\"). */\n localeLabel: ReactNode;\n /** Store a verdict. Left out (or `readOnly`): the editor shows the row and its last\n * verdict, with no fields and no actions. */\n onSave?: (write: TranslationReviewWrite) => MaybePromise;\n /** Clear the verdict — back to unreviewed. Left out: no reset button. */\n onClear?: (key: TranslationReviewKey) => MaybePromise;\n /** The Cancel button — and called after a save or a reset resolves, so the panel\n * that opened the editor closes it. */\n onClose?: () => void;\n /** Look, do not touch: a locale the viewer may read but not review. */\n readOnly?: boolean;\n /**\n * 0.25: put the cursor in the wording field when the editor opens, at the end of the\n * text. The panel sets it when a phone card is swiped toward \"Needs a change\" (keksdose\n * live #377): that swipe opens the editor to be written in, and a reviewer who has just\n * said \"this is wrong\" should not have to find the field first. Not for an editor\n * opened by a tap — that one may only be opened to read.\n */\n focusWording?: boolean;\n /** How the verdict's date reads. Default: the kit's `formatDate`, medium, in the\n * provider's locale. */\n formatDate?: (iso: string) => string;\n /** Turns a rejection into words. Default {@link TranslationReviewLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n className?: string;\n labels?: LabelOverride<TranslationReviewLabels>;\n}\n\n/** The wording a verdict was given on, under the line that says it changed. */\nfunction Then({ label, text }: { label: string; text: string }) {\n return (\n <div className=\"mt-1.5 min-w-0\">\n <span className=\"text-xs font-medium\">{label}</span>\n <p className=\"whitespace-pre-line break-words text-secondary\">{text}</p>\n </div>\n );\n}\n\n/**\n * One string, opened: the reference beside the translation, what changed since the last\n * verdict, what a machine can see is wrong (the placeholders), and the two things a\n * reviewer can say about it — keksdose's and kastlan's `translation-review-editor.tsx`.\n *\n * \"Better wording\" opens on the CURRENT text rather than empty, because the usual\n * correction is a word or an ending, and retyping a sentence to change one word is how\n * new mistakes get in. It is sent only with \"Needs a change\" — approving says the text is\n * fine as it stands. A \"Needs a change\" with neither a note nor a different wording tells\n * whoever fixes it nothing, so it takes one of the two.\n *\n * A MISSING string (kastlan: the locale lacks the key) cannot be approved — there is\n * nothing to approve. Its field starts empty and is the translation to add; the verdict\n * is NEEDS_CHANGE on the empty text, which the export hands a developer as the string to\n * add.\n *\n * Every action waits for its callback: the buttons hold while the promise is out, a\n * resolution closes the editor through `onClose`, and a rejection keeps it open with what\n * was typed and the error under the buttons.\n *\n * Each verdict is a `commit` exempt from billing (`COMMIT_EXCEPT_BILLING`, 0.33): a review\n * is an admin route, which a lapsed plan never locks (docs/billing-harmonization.md\n * §12.13, §12.36); a demo's lock still holds it.\n */\nexport function TranslationReviewEditor({\n row,\n referenceLabel,\n localeLabel,\n onSave,\n onClear,\n onClose,\n readOnly = false,\n focusWording = false,\n formatDate: formatDateProp,\n formatError,\n className,\n labels: labelsProp,\n}: TranslationReviewEditorProps) {\n const labels = useKitLabels(\"translationReview\", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);\n const { formatDate } = useKitFormat();\n const review = row.review;\n const missing = row.text === \"\";\n const editable = !readOnly && onSave !== undefined;\n const [suggestion, setSuggestion] = useState(review?.suggestion ?? row.text);\n const [note, setNote] = useState(review?.note ?? \"\");\n const [busy, setBusy] = useState<\"approve\" | \"flag\" | \"clear\" | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n const wordingRef = useRef<HTMLTextAreaElement>(null);\n // On mount only: the prop says how the editor was OPENED, not where focus belongs\n // for the rest of its life.\n const focusOnOpen = useRef(focusWording && editable);\n useEffect(() => {\n const field = wordingRef.current;\n if (!focusOnOpen.current || !field) return;\n field.focus();\n field.setSelectionRange(field.value.length, field.value.length);\n }, []);\n\n const canFlag = note.trim() !== \"\" || suggestion.trim() !== row.text.trim();\n const changed = row.status === \"changed\" ? review : null;\n const date = (iso: string) => (formatDateProp ? formatDateProp(iso) : formatDate(iso, \"medium\"));\n\n const run = async (kind: \"approve\" | \"flag\" | \"clear\", action: () => MaybePromise) => {\n setBusy(kind);\n setFailure(null);\n try {\n await action();\n onClose?.();\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n } finally {\n setBusy(null);\n }\n };\n\n const save = (verdict: TranslationVerdict) => {\n if (!onSave) return;\n const trimmedNote = note.trim() || null;\n // Approving says the text is right as it stands: there is no wording to suggest.\n const sent = verdict === \"APPROVED\" ? null : suggestion.trim() || null;\n void run(verdict === \"APPROVED\" ? \"approve\" : \"flag\", () =>\n onSave(reviewWrite(row, verdict, trimmedNote, sent)),\n );\n };\n\n return (\n <div className={cn(\"min-w-0 space-y-3 text-sm\", className)}>\n <code className=\"block break-all text-xs text-muted\">{row.key}</code>\n\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <div className=\"min-w-0\">\n <SectionLabel as=\"span\" size=\"xs\">\n {referenceLabel}\n </SectionLabel>\n <p className=\"mt-1 whitespace-pre-line break-words text-secondary\">{row.reference}</p>\n </div>\n <div className=\"min-w-0\">\n <SectionLabel as=\"span\" size=\"xs\">\n {localeLabel}\n </SectionLabel>\n {missing ? (\n <p className=\"mt-1 italic text-muted\">{labels.missingText}</p>\n ) : (\n <p lang={row.locale} className=\"mt-1 whitespace-pre-line break-words\">\n {row.text}\n </p>\n )}\n </div>\n </div>\n\n {changed && (\n <AlertBanner tone=\"warning\" variant=\"inline\" size=\"sm\" block live={false}>\n <div className=\"min-w-0\">\n <p>{labels.changedSince}</p>\n {changed.text !== row.text && <Then label={labels.reviewedWording} text={changed.text || \"—\"} />}\n {changed.referenceText != null && changed.referenceText !== row.reference && (\n <Then label={labels.reviewedReference} text={changed.referenceText} />\n )}\n </div>\n </AlertBanner>\n )}\n\n {row.placeholderMismatch && (\n <AlertBanner tone=\"danger\" variant=\"inline\" size=\"sm\" block live={false}>\n <span className=\"min-w-0 break-words\">\n {labels.placeholderMismatch(\n placeholderTokens(row.reference).join(\" \") || \"—\",\n placeholderTokens(row.text).join(\" \") || \"—\",\n )}\n </span>\n </AlertBanner>\n )}\n\n {review && (\n <Caption>\n {(review.verdict === \"APPROVED\" ? labels.lastApproved : labels.lastFlagged)(\n review.reviewerName ?? labels.erasedReviewer,\n date(review.reviewedAt),\n )}\n </Caption>\n )}\n\n {/* Read-only: what the reviewer said, as text — the fields are for saying it. */}\n {!editable && review?.verdict === \"NEEDS_CHANGE\" && (review.suggestion || review.note) && (\n <div className=\"space-y-2\">\n {review.suggestion && <Then label={missing ? labels.translation : labels.suggestion} text={review.suggestion} />}\n {review.note && <Then label={labels.note} text={review.note} />}\n </div>\n )}\n\n {editable && (\n <>\n <Textarea\n ref={wordingRef}\n // The panel's \"Needs a change\" swipe on an already open row finds it here (#416).\n data-review-wording=\"\"\n label={missing ? labels.translation : labels.suggestion}\n lang={row.locale}\n rows={Math.min(8, Math.max(2, Math.ceil(Math.max(row.text.length, row.reference.length) / 80)))}\n value={suggestion}\n disabled={busy !== null}\n onChange={(e) => setSuggestion(e.target.value)}\n />\n <Textarea\n label={labels.note}\n rows={2}\n value={note}\n placeholder={labels.notePlaceholder}\n disabled={busy !== null}\n onChange={(e) => setNote(e.target.value)}\n />\n <div className=\"flex flex-wrap gap-2\">\n {!missing && (\n <Button\n variant=\"default\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy === \"approve\"}\n disabled={busy !== null}\n onClick={() => save(\"APPROVED\")}\n >\n {labels.approve}\n </Button>\n )}\n <Button\n variant={missing ? \"default\" : \"danger\"}\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy === \"flag\"}\n disabled={busy !== null || !canFlag}\n onClick={() => save(\"NEEDS_CHANGE\")}\n >\n {missing ? labels.suggest : labels.flag}\n </Button>\n {review && onClear && (\n <Button\n variant=\"ghost\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy === \"clear\"}\n disabled={busy !== null}\n onClick={() => void run(\"clear\", () => onClear({ locale: row.locale, key: row.key }))}\n >\n {labels.reset}\n </Button>\n )}\n {onClose && (\n <Button variant=\"ghost\" disabled={busy !== null} onClick={onClose}>\n {labels.cancel}\n </Button>\n )}\n </div>\n </>\n )}\n\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n </div>\n );\n}\n"],"mappings":";AA8DI,SA8JI,UA7JF,KADF;AA9DJ,SAAS,WAAW,QAAQ,gBAAgB;AAG5C,SAAS,oBAAwC;AACjD,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB,mBAAmB;AAO/C,SAAS,mBAAmB;AAC5B,SAAS,SAAS,oBAAoB;AACtC,SAAS,yCAAyC;AAElD,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,6BAA6B;AAyCtC,SAAS,KAAK,EAAE,OAAO,KAAK,GAAoC;AAC9D,SACE,qBAAC,SAAI,WAAU,kBACb;AAAA,wBAAC,UAAK,WAAU,uBAAuB,iBAAM;AAAA,IAC7C,oBAAC,OAAE,WAAU,kDAAkD,gBAAK;AAAA,KACtE;AAEJ;AA0BO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,eAAe;AAAA,EACf,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAAiC;AAC/B,QAAM,SAAS,aAAa,qBAAqB,mCAAmC,UAAU;AAC9F,QAAM,EAAE,WAAW,IAAI,aAAa;AACpC,QAAM,SAAS,IAAI;AACnB,QAAM,UAAU,IAAI,SAAS;AAC7B,QAAM,WAAW,CAAC,YAAY,WAAW;AACzC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,QAAQ,cAAc,IAAI,IAAI;AAC3E,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,QAAQ,QAAQ,EAAE;AACnD,QAAM,CAAC,MAAM,OAAO,IAAI,SAA8C,IAAI;AAC1E,QAAM,CAAC,SAAS,UAAU,IAAI,SAAoB,IAAI;AACtD,QAAM,aAAa,OAA4B,IAAI;AAGnD,QAAM,cAAc,OAAO,gBAAgB,QAAQ;AACnD,YAAU,MAAM;AACd,UAAM,QAAQ,WAAW;AACzB,QAAI,CAAC,YAAY,WAAW,CAAC,MAAO;AACpC,UAAM,MAAM;AACZ,UAAM,kBAAkB,MAAM,MAAM,QAAQ,MAAM,MAAM,MAAM;AAAA,EAChE,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,KAAK,KAAK,MAAM,MAAM,WAAW,KAAK,MAAM,IAAI,KAAK,KAAK;AAC1E,QAAM,UAAU,IAAI,WAAW,YAAY,SAAS;AACpD,QAAM,OAAO,CAAC,QAAiB,iBAAiB,eAAe,GAAG,IAAI,WAAW,KAAK,QAAQ;AAE9F,QAAM,MAAM,OAAO,MAAoC,WAA+B;AACpF,YAAQ,IAAI;AACZ,eAAW,IAAI;AACf,QAAI;AACF,YAAM,OAAO;AACb,gBAAU;AAAA,IACZ,SAAS,QAAQ;AACf,iBAAW,cAAc,YAAY,MAAM,IAAI,OAAO,MAAM;AAAA,IAC9D,UAAE;AACA,cAAQ,IAAI;AAAA,IACd;AAAA,EACF;AAEA,QAAM,OAAO,CAAC,YAAgC;AAC5C,QAAI,CAAC,OAAQ;AACb,UAAM,cAAc,KAAK,KAAK,KAAK;AAEnC,UAAM,OAAO,YAAY,aAAa,OAAO,WAAW,KAAK,KAAK;AAClE,SAAK;AAAA,MAAI,YAAY,aAAa,YAAY;AAAA,MAAQ,MACpD,OAAO,YAAY,KAAK,SAAS,aAAa,IAAI,CAAC;AAAA,IACrD;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAW,GAAG,6BAA6B,SAAS,GACvD;AAAA,wBAAC,UAAK,WAAU,sCAAsC,cAAI,KAAI;AAAA,IAE9D,qBAAC,SAAI,WAAU,yCACb;AAAA,2BAAC,SAAI,WAAU,WACb;AAAA,4BAAC,gBAAa,IAAG,QAAO,MAAK,MAC1B,0BACH;AAAA,QACA,oBAAC,OAAE,WAAU,uDAAuD,cAAI,WAAU;AAAA,SACpF;AAAA,MACA,qBAAC,SAAI,WAAU,WACb;AAAA,4BAAC,gBAAa,IAAG,QAAO,MAAK,MAC1B,uBACH;AAAA,QACC,UACC,oBAAC,OAAE,WAAU,0BAA0B,iBAAO,aAAY,IAE1D,oBAAC,OAAE,MAAM,IAAI,QAAQ,WAAU,wCAC5B,cAAI,MACP;AAAA,SAEJ;AAAA,OACF;AAAA,IAEC,WACC,oBAAC,eAAY,MAAK,WAAU,SAAQ,UAAS,MAAK,MAAK,OAAK,MAAC,MAAM,OACjE,+BAAC,SAAI,WAAU,WACb;AAAA,0BAAC,OAAG,iBAAO,cAAa;AAAA,MACvB,QAAQ,SAAS,IAAI,QAAQ,oBAAC,QAAK,OAAO,OAAO,iBAAiB,MAAM,QAAQ,QAAQ,UAAK;AAAA,MAC7F,QAAQ,iBAAiB,QAAQ,QAAQ,kBAAkB,IAAI,aAC9D,oBAAC,QAAK,OAAO,OAAO,mBAAmB,MAAM,QAAQ,eAAe;AAAA,OAExE,GACF;AAAA,IAGD,IAAI,uBACH,oBAAC,eAAY,MAAK,UAAS,SAAQ,UAAS,MAAK,MAAK,OAAK,MAAC,MAAM,OAChE,8BAAC,UAAK,WAAU,uBACb,iBAAO;AAAA,MACN,kBAAkB,IAAI,SAAS,EAAE,KAAK,GAAG,KAAK;AAAA,MAC9C,kBAAkB,IAAI,IAAI,EAAE,KAAK,GAAG,KAAK;AAAA,IAC3C,GACF,GACF;AAAA,IAGD,UACC,oBAAC,WACG,kBAAO,YAAY,aAAa,OAAO,eAAe,OAAO;AAAA,MAC7D,OAAO,gBAAgB,OAAO;AAAA,MAC9B,KAAK,OAAO,UAAU;AAAA,IACxB,GACF;AAAA,IAID,CAAC,YAAY,QAAQ,YAAY,mBAAmB,OAAO,cAAc,OAAO,SAC/E,qBAAC,SAAI,WAAU,aACZ;AAAA,aAAO,cAAc,oBAAC,QAAK,OAAO,UAAU,OAAO,cAAc,OAAO,YAAY,MAAM,OAAO,YAAY;AAAA,MAC7G,OAAO,QAAQ,oBAAC,QAAK,OAAO,OAAO,MAAM,MAAM,OAAO,MAAM;AAAA,OAC/D;AAAA,IAGD,YACC,iCACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UAEL,uBAAoB;AAAA,UACpB,OAAO,UAAU,OAAO,cAAc,OAAO;AAAA,UAC7C,MAAM,IAAI;AAAA,UACV,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,IAAI,IAAI,KAAK,QAAQ,IAAI,UAAU,MAAM,IAAI,EAAE,CAAC,CAAC;AAAA,UAC9F,OAAO;AAAA,UACP,UAAU,SAAS;AAAA,UACnB,UAAU,CAAC,MAAM,cAAc,EAAE,OAAO,KAAK;AAAA;AAAA,MAC/C;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,OAAO;AAAA,UACd,MAAM;AAAA,UACN,OAAO;AAAA,UACP,aAAa,OAAO;AAAA,UACpB,UAAU,SAAS;AAAA,UACnB,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA;AAAA,MACzC;AAAA,MACA,qBAAC,SAAI,WAAU,wBACZ;AAAA,SAAC,WACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,QAAQ;AAAA,YACR,SAAS,SAAS;AAAA,YAClB,UAAU,SAAS;AAAA,YACnB,SAAS,MAAM,KAAK,UAAU;AAAA,YAE7B,iBAAO;AAAA;AAAA,QACV;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,SAAS,UAAU,YAAY;AAAA,YAC/B,QAAQ;AAAA,YACR,SAAS,SAAS;AAAA,YAClB,UAAU,SAAS,QAAQ,CAAC;AAAA,YAC5B,SAAS,MAAM,KAAK,cAAc;AAAA,YAEjC,oBAAU,OAAO,UAAU,OAAO;AAAA;AAAA,QACrC;AAAA,QACC,UAAU,WACT;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,QAAQ;AAAA,YACR,SAAS,SAAS;AAAA,YAClB,UAAU,SAAS;AAAA,YACnB,SAAS,MAAM,KAAK,IAAI,SAAS,MAAM,QAAQ,EAAE,QAAQ,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,CAAC;AAAA,YAEnF,iBAAO;AAAA;AAAA,QACV;AAAA,QAED,WACC,oBAAC,UAAO,SAAQ,SAAQ,UAAU,SAAS,MAAM,SAAS,SACvD,iBAAO,QACV;AAAA,SAEJ;AAAA,OACF;AAAA,IAGD,WACC,oBAAC,eAAY,MAAK,UAAS,MAAK,MAAK,MAAK,SACvC,mBACH;AAAA,KAEJ;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/translation-review-editor.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { useKitFormat } from \"../lib/format\";\nimport { placeholderTokens, reviewWrite } from \"../lib/translation-review\";\nimport type {\n TranslationReviewKey,\n TranslationReviewWrite,\n TranslationRow,\n TranslationVerdict,\n} from \"../lib/translation-review\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Caption, SectionLabel } from \"./text\";\nimport { DEFAULT_TRANSLATION_REVIEW_LABELS } from \"./translation-review-labels\";\nimport type { TranslationReviewLabels } from \"./translation-review-labels\";\nimport { Button } from \"./button\";\nimport { Textarea } from \"./textarea\";\nimport { COMMIT_EXCEPT_BILLING } from \"./write-lock\";\n\n/** A callback may return a promise: resolve and the editor closes, reject and it stays\n * open with the error. */\ntype MaybePromise = void | Promise<unknown>;\n\nexport interface TranslationReviewEditorProps {\n row: TranslationRow;\n /** The reference language's own name (\"English\", \"Deutsch\") — the app's choice of\n * reference, so the app's word for it. */\n referenceLabel: ReactNode;\n /** The reviewed language's own name (\"Français\"). */\n localeLabel: ReactNode;\n /** Store a verdict. Left out (or `readOnly`): the editor shows the row and its last\n * verdict, with no fields and no actions. */\n onSave?: (write: TranslationReviewWrite) => MaybePromise;\n /** Clear the verdict — back to unreviewed. Left out: no reset button. */\n onClear?: (key: TranslationReviewKey) => MaybePromise;\n /** The Cancel button — and called after a save or a reset resolves, so the panel\n * that opened the editor closes it. */\n onClose?: () => void;\n /** Look, do not touch: a locale the viewer may read but not review. */\n readOnly?: boolean;\n /**\n * 0.25: put the cursor in the wording field when the editor opens, at the end of the\n * text. The panel sets it when a phone card is swiped toward \"Needs a change\" (keksdose\n * live #377): that swipe opens the editor to be written in, and a reviewer who has just\n * said \"this is wrong\" should not have to find the field first. Not for an editor\n * opened by a tap — that one may only be opened to read.\n */\n focusWording?: boolean;\n /** How the verdict's date reads. Default: the kit's `formatDate`, medium, in the\n * provider's locale. */\n formatDate?: (iso: string) => string;\n /** Turns a rejection into words. Default {@link TranslationReviewLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n className?: string;\n labels?: LabelOverride<TranslationReviewLabels>;\n}\n\n/** The wording a verdict was given on, under the line that says it changed. */\nfunction Then({ label, text }: { label: string; text: string }) {\n return (\n <div className=\"mt-1.5 min-w-0\">\n <span className=\"text-xs font-medium\">{label}</span>\n <p className=\"whitespace-pre-line break-words text-secondary\">{text}</p>\n </div>\n );\n}\n\n/**\n * One string, opened: the reference beside the translation, what changed since the last\n * verdict, what a machine can see is wrong (the placeholders), and the two things a\n * reviewer can say about it — keksdose's and kastlan's `translation-review-editor.tsx`.\n *\n * \"Better wording\" opens on the CURRENT text rather than empty, because the usual\n * correction is a word or an ending, and retyping a sentence to change one word is how\n * new mistakes get in. It is sent only with \"Needs a change\" — approving says the text is\n * fine as it stands. A \"Needs a change\" with neither a note nor a different wording tells\n * whoever fixes it nothing, so it takes one of the two.\n *\n * A MISSING string (kastlan: the locale lacks the key) cannot be approved — there is\n * nothing to approve. Its field starts empty and is the translation to add; the verdict\n * is NEEDS_CHANGE on the empty text, which the export hands a developer as the string to\n * add.\n *\n * Every action waits for its callback: the buttons hold while the promise is out, a\n * resolution closes the editor through `onClose`, and a rejection keeps it open with what\n * was typed and the error under the buttons.\n *\n * Each verdict is a `commit` exempt from billing (`COMMIT_EXCEPT_BILLING`, 0.33): a review\n * is an admin route, which a lapsed plan never locks (docs/billing-harmonization.md\n * §12.13, §12.36); a demo's lock still holds it.\n */\nexport function TranslationReviewEditor({\n row,\n referenceLabel,\n localeLabel,\n onSave,\n onClear,\n onClose,\n readOnly = false,\n focusWording = false,\n formatDate: formatDateProp,\n formatError,\n className,\n labels: labelsProp,\n}: TranslationReviewEditorProps) {\n const labels = useKitLabels(\"translationReview\", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);\n const { formatDate } = useKitFormat();\n const review = row.review;\n const missing = row.text === \"\";\n const editable = !readOnly && onSave !== undefined;\n const [suggestion, setSuggestion] = useState(review?.suggestion ?? row.text);\n const [note, setNote] = useState(review?.note ?? \"\");\n const [busy, setBusy] = useState<\"approve\" | \"flag\" | \"clear\" | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n const wordingRef = useRef<HTMLTextAreaElement>(null);\n // On mount only: the prop says how the editor was OPENED, not where focus belongs\n // for the rest of its life.\n const focusOnOpen = useRef(focusWording && editable);\n useEffect(() => {\n const field = wordingRef.current;\n if (!focusOnOpen.current || !field) return;\n field.focus();\n field.setSelectionRange(field.value.length, field.value.length);\n }, []);\n\n const canFlag = note.trim() !== \"\" || suggestion.trim() !== row.text.trim();\n const changed = row.status === \"changed\" ? review : null;\n const date = (iso: string) => (formatDateProp ? formatDateProp(iso) : formatDate(iso, \"medium\"));\n\n const run = async (kind: \"approve\" | \"flag\" | \"clear\", action: () => MaybePromise) => {\n setBusy(kind);\n setFailure(null);\n try {\n await action();\n onClose?.();\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n } finally {\n setBusy(null);\n }\n };\n\n const save = (verdict: TranslationVerdict) => {\n if (!onSave) return;\n const trimmedNote = note.trim() || null;\n // Approving says the text is right as it stands: there is no wording to suggest.\n const sent = verdict === \"APPROVED\" ? null : suggestion.trim() || null;\n void run(verdict === \"APPROVED\" ? \"approve\" : \"flag\", () => onSave(reviewWrite(row, verdict, trimmedNote, sent)));\n };\n\n return (\n <div className={cn(\"min-w-0 space-y-3 text-sm\", className)}>\n <code className=\"block break-all text-xs text-muted\">{row.key}</code>\n\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <div className=\"min-w-0\">\n <SectionLabel as=\"span\" size=\"xs\">\n {referenceLabel}\n </SectionLabel>\n <p className=\"mt-1 whitespace-pre-line break-words text-secondary\">{row.reference}</p>\n </div>\n <div className=\"min-w-0\">\n <SectionLabel as=\"span\" size=\"xs\">\n {localeLabel}\n </SectionLabel>\n {missing ? (\n <p className=\"mt-1 italic text-muted\">{labels.missingText}</p>\n ) : (\n <p lang={row.locale} className=\"mt-1 whitespace-pre-line break-words\">\n {row.text}\n </p>\n )}\n </div>\n </div>\n\n {changed && (\n <AlertBanner tone=\"warning\" variant=\"inline\" size=\"sm\" block live={false}>\n <div className=\"min-w-0\">\n <p>{labels.changedSince}</p>\n {changed.text !== row.text && <Then label={labels.reviewedWording} text={changed.text || \"—\"} />}\n {changed.referenceText != null && changed.referenceText !== row.reference && (\n <Then label={labels.reviewedReference} text={changed.referenceText} />\n )}\n </div>\n </AlertBanner>\n )}\n\n {row.placeholderMismatch && (\n <AlertBanner tone=\"danger\" variant=\"inline\" size=\"sm\" block live={false}>\n <span className=\"min-w-0 break-words\">\n {labels.placeholderMismatch(\n placeholderTokens(row.reference).join(\" \") || \"—\",\n placeholderTokens(row.text).join(\" \") || \"—\",\n )}\n </span>\n </AlertBanner>\n )}\n\n {review && (\n <Caption>\n {(review.verdict === \"APPROVED\" ? labels.lastApproved : labels.lastFlagged)(\n review.reviewerName ?? labels.erasedReviewer,\n date(review.reviewedAt),\n )}\n </Caption>\n )}\n\n {/* Read-only: what the reviewer said, as text — the fields are for saying it. */}\n {!editable && review?.verdict === \"NEEDS_CHANGE\" && (review.suggestion || review.note) && (\n <div className=\"space-y-2\">\n {review.suggestion && (\n <Then label={missing ? labels.translation : labels.suggestion} text={review.suggestion} />\n )}\n {review.note && <Then label={labels.note} text={review.note} />}\n </div>\n )}\n\n {editable && (\n <>\n <Textarea\n ref={wordingRef}\n // The panel's \"Needs a change\" swipe on an already open row finds it here (#416).\n data-review-wording=\"\"\n label={missing ? labels.translation : labels.suggestion}\n lang={row.locale}\n rows={Math.min(8, Math.max(2, Math.ceil(Math.max(row.text.length, row.reference.length) / 80)))}\n value={suggestion}\n disabled={busy !== null}\n onChange={(e) => setSuggestion(e.target.value)}\n />\n <Textarea\n label={labels.note}\n rows={2}\n value={note}\n placeholder={labels.notePlaceholder}\n disabled={busy !== null}\n onChange={(e) => setNote(e.target.value)}\n />\n <div className=\"flex flex-wrap gap-2\">\n {!missing && (\n <Button\n variant=\"default\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy === \"approve\"}\n disabled={busy !== null}\n onClick={() => save(\"APPROVED\")}\n >\n {labels.approve}\n </Button>\n )}\n <Button\n variant={missing ? \"default\" : \"danger\"}\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy === \"flag\"}\n disabled={busy !== null || !canFlag}\n onClick={() => save(\"NEEDS_CHANGE\")}\n >\n {missing ? labels.suggest : labels.flag}\n </Button>\n {review && onClear && (\n <Button\n variant=\"ghost\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy === \"clear\"}\n disabled={busy !== null}\n onClick={() => void run(\"clear\", () => onClear({ locale: row.locale, key: row.key }))}\n >\n {labels.reset}\n </Button>\n )}\n {onClose && (\n <Button variant=\"ghost\" disabled={busy !== null} onClick={onClose}>\n {labels.cancel}\n </Button>\n )}\n </div>\n </>\n )}\n\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -70,7 +70,7 @@ export interface TranslationLocaleTabsProps {
|
|
|
70
70
|
* strip's caret saying it goes on (`overflowCue`). Nothing for a single locale:
|
|
71
71
|
* a strip with one tab is a heading that looks clickable.
|
|
72
72
|
*/
|
|
73
|
-
export declare function TranslationLocaleTabs({ locales, active, onChange, className, labels: labelsProp }: TranslationLocaleTabsProps): import("react").JSX.Element | null;
|
|
73
|
+
export declare function TranslationLocaleTabs({ locales, active, onChange, className, labels: labelsProp, }: TranslationLocaleTabsProps): import("react").JSX.Element | null;
|
|
74
74
|
export interface TranslationExportButtonProps {
|
|
75
75
|
/** Every row the export covers — usually every locale's, not only the open one. */
|
|
76
76
|
rows: readonly TranslationRow[];
|