@eifi1/ui-kit 0.51.4 → 0.51.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/account/account-settings-labels.js.map +1 -1
- package/dist/account/account-settings.js +11 -9
- package/dist/account/account-settings.js.map +1 -1
- package/dist/account/delete-account-setting.js +56 -65
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.js +4 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.js +19 -11
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/admin/admin-tool-links.d.ts +1 -1
- package/dist/admin/admin-tool-links.js +7 -1
- package/dist/admin/admin-tool-links.js.map +1 -1
- package/dist/admin/invitations-panel.js +2 -13
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/mail-test-card.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +4 -11
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/user-roster.js +1 -9
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.js +23 -31
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.js +13 -5
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.js +1 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/checkout-processing.js.map +1 -1
- package/dist/billing/plan-card.js +91 -85
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-limit-toast.js.map +1 -1
- package/dist/billing/plan-limit.js.map +1 -1
- package/dist/billing/plan-price.js.map +1 -1
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/bin/eifi1-pay-page.mjs +6 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.js +1 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.js +29 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +1 -4
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.js +1 -7
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.js +5 -5
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +4 -10
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/card.js +1 -8
- package/dist/components/card.js.map +1 -1
- package/dist/components/chart-zoom-core.js +3 -17
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +4 -22
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/chip-input.js +4 -1
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/chip.js +10 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.js +58 -74
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper-labels.js.map +1 -1
- package/dist/components/column-mapper.js +167 -227
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +1 -4
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +1 -4
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.d.ts +5 -4
- package/dist/components/country-select.js +3 -19
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.js +10 -8
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.js +1 -6
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.js.map +1 -1
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.js +2 -18
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.js +3 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.js +44 -56
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.js +31 -44
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-row-actions.js +19 -16
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.js +4 -1
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.js +30 -39
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +1 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/description-list.js +10 -10
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +9 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +17 -27
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +25 -35
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-base.js +5 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +1 -1
- package/dist/components/field-parts.js +10 -14
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.js +8 -10
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +4 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.js.map +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/grouped-picker.js +6 -8
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.js +1 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input-labels.js.map +1 -1
- package/dist/components/icon-button.js +4 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/icon-picker.js +2 -7
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/image-grid.js +41 -50
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +2 -8
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +14 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +1 -7
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.js +14 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.js +12 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +26 -36
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.js +22 -10
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.js +29 -53
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +13 -31
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +10 -21
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +3 -16
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/nav-pills.js +10 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.js +1 -7
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +1 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +1 -7
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +13 -22
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.js +1 -11
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/password-strength-labels.js.map +1 -1
- package/dist/components/password-strength.js +2 -8
- package/dist/components/password-strength.js.map +1 -1
- package/dist/components/phone-input.js +42 -57
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +1 -8
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +16 -9
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.js +1 -8
- package/dist/components/qr-code.js.map +1 -1
- package/dist/components/row-actions.js +5 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/select.js +28 -38
- package/dist/components/select.js.map +1 -1
- package/dist/components/separator.js +1 -5
- package/dist/components/separator.js.map +1 -1
- package/dist/components/series-chart-axes.js +4 -1
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.js +29 -45
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.js +21 -17
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.js +4 -9
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.js +4 -13
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/signed-amount.js +12 -5
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/skeleton.js +2 -17
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +13 -36
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -10
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.js +14 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +1 -1
- package/dist/components/swipeable-row.js +1 -8
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table-skeleton.js +4 -1
- package/dist/components/table-skeleton.js.map +1 -1
- package/dist/components/table.js +12 -9
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.js +5 -24
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +1 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +6 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +2 -11
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.js +2 -22
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -4
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -1
- package/dist/components/translation-review-parts.js +7 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/translation-review.js +18 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +14 -10
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.js +1 -13
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/use-table-state.js +14 -11
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.js +3 -26
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/demo/demo-start.js +8 -4
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-labels.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +1 -8
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-capture.d.ts +1 -1
- package/dist/feedback/feedback-capture.js +3 -1
- package/dist/feedback/feedback-capture.js.map +1 -1
- package/dist/feedback/feedback-comment-thread.js +27 -13
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-context.js +1 -4
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-public-list.js +7 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-record.js +1 -5
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.js +25 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-table.js +4 -1
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +2 -13
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback/feedback-vote-button.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-document-title.js.map +1 -1
- package/dist/hooks/use-file-drop.js.map +1 -1
- package/dist/hooks/use-file-picker-labels.js.map +1 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +10 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.d.ts +1 -1
- package/dist/hooks/use-row-swipe.js +1 -6
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.js +1 -4
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +4 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.js +2 -8
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.js.map +1 -1
- package/dist/i18n/locales/en.js +4 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +4 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +4 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +4 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +4 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.js +13 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +40 -23
- package/dist/index.js.map +1 -1
- package/dist/landing/access.js +1 -4
- package/dist/landing/access.js.map +1 -1
- package/dist/landing/landing-sections.js +35 -38
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/device-settings.js +2 -1
- package/dist/lib/device-settings.js.map +1 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/iban.js.map +1 -1
- package/dist/lib/logger.js +1 -5
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/person-name.js.map +1 -1
- package/dist/lib/phone.js.map +1 -1
- package/dist/lib/qr-encode.js +344 -8
- package/dist/lib/qr-encode.js.map +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/table-text.js +15 -3
- package/dist/lib/table-text.js.map +1 -1
- package/dist/lib/text-match.d.ts +4 -0
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/notification-inbox.js +63 -44
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +6 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/use-push-device.js +4 -1
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/pwa-update-prompt.d.ts +1 -1
- package/dist/offline/pwa-update-prompt.js +7 -1
- package/dist/offline/pwa-update-prompt.js.map +1 -1
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +6 -1
- package/dist/offline.js.map +1 -1
- package/dist/pay/pay.css +8 -1
- package/dist/rhf/fields.d.ts +1 -2
- package/dist/rhf/fields.js +1 -10
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.js +1 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/search/command-palette.js +71 -62
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +34 -5
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/settings/device-reset-setting.js +1 -6
- package/dist/settings/device-reset-setting.js.map +1 -1
- package/dist/settings/settings-heading.js.map +1 -1
- package/dist/settings/settings-layout.js +9 -20
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.js +62 -31
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -10
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/dev-menu.js +12 -6
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/sync-status-indicator.js +7 -6
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +1 -10
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.js +9 -1
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.js +24 -6
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/react.js.map +1 -1
- package/dist/testing/viewport.js +7 -4
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/contrast-tokens.js +13 -2
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/palette-derive.d.ts +1 -1
- package/dist/theme/palette-derive.js +18 -29
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +1 -3
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.js +5 -1
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-engine.js +1 -4
- package/dist/tour/tour-engine.js.map +1 -1
- package/dist/tour/tour.js +21 -12
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.js +3 -24
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +24 -28
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +1 -1
- package/dist/wizard/wizard-step.js +1 -8
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +5 -2
- package/src/account/account-settings-labels.ts +1 -2
- package/src/account/account-settings.tsx +11 -17
- package/src/account/delete-account-setting.tsx +1 -6
- package/src/account/email-change-setting-labels.ts +1 -2
- package/src/account/email-change-setting.tsx +3 -1
- package/src/account/passkeys-setting.tsx +1 -3
- package/src/account/sessions-setting.tsx +4 -2
- package/src/admin/admin-action-confirm.tsx +4 -7
- package/src/admin/admin-action-log.tsx +29 -29
- package/src/admin/admin-area-labels.ts +4 -1
- package/src/admin/admin-overview.tsx +8 -2
- package/src/admin/admin-sections.tsx +3 -1
- package/src/admin/admin-tool-links.tsx +7 -1
- package/src/admin/invitations-panel.tsx +5 -11
- package/src/admin/mail-test-card.tsx +1 -2
- package/src/admin/reviewer-scope-editor.tsx +4 -9
- package/src/admin/role-select.tsx +4 -2
- package/src/admin/user-roster.tsx +2 -11
- package/src/auth/accept-invitation.tsx +1 -7
- package/src/auth/auth-errors.ts +1 -6
- package/src/auth/forgot-password-form.tsx +2 -7
- package/src/auth/register-form.tsx +7 -8
- package/src/auth/reset-password-form.tsx +2 -5
- package/src/auth/sign-in-form.tsx +15 -14
- package/src/auth/verify-email.tsx +2 -7
- package/src/billing/billing-banner.tsx +3 -7
- package/src/billing/billing-labels.ts +6 -4
- package/src/billing/checkout-processing.ts +0 -1
- package/src/billing/plan-card.tsx +9 -8
- package/src/billing/plan-limit-toast.tsx +1 -2
- package/src/billing/plan-limit.ts +1 -2
- package/src/billing/plan-price.ts +1 -4
- package/src/billing/plan-switch.tsx +1 -2
- package/src/billing/subscription-actions.tsx +5 -2
- package/src/components/account-chips.tsx +1 -7
- package/src/components/alert-banner.tsx +2 -8
- package/src/components/amount-input.tsx +206 -178
- package/src/components/authed-image.tsx +7 -4
- package/src/components/autocomplete.tsx +7 -9
- package/src/components/bulk-action-bar.tsx +2 -7
- package/src/components/button-group.tsx +1 -2
- package/src/components/button-look.ts +6 -17
- package/src/components/button.tsx +1 -4
- package/src/components/calculator.tsx +6 -9
- package/src/components/calendar-heatmap.tsx +5 -10
- package/src/components/card.tsx +9 -13
- package/src/components/chart-zoom-core.tsx +6 -33
- package/src/components/chart.tsx +8 -31
- package/src/components/checkbox-group.tsx +1 -4
- package/src/components/chip-input.tsx +6 -6
- package/src/components/chip.tsx +14 -26
- package/src/components/choice-card.tsx +4 -18
- package/src/components/column-mapper-labels.ts +3 -10
- package/src/components/column-mapper.tsx +33 -122
- package/src/components/combobox-core.tsx +12 -42
- package/src/components/combobox.tsx +2 -6
- package/src/components/confirm-dialog.tsx +1 -3
- package/src/components/copy-button.tsx +2 -4
- package/src/components/country-select.tsx +9 -21
- package/src/components/currency-option-list.tsx +13 -14
- package/src/components/currency-select.tsx +2 -7
- package/src/components/danger-confirm.tsx +8 -9
- package/src/components/data-table-cells.tsx +1 -2
- package/src/components/data-table-column-settings.tsx +1 -3
- package/src/components/data-table-filter-popover.tsx +5 -19
- package/src/components/data-table-filters.ts +1 -6
- package/src/components/data-table-fit.ts +1 -3
- package/src/components/data-table-groups.tsx +3 -1
- package/src/components/data-table-head.tsx +27 -45
- package/src/components/data-table-mobile.tsx +99 -108
- package/src/components/data-table-pagination.tsx +2 -6
- package/src/components/data-table-row-actions.tsx +25 -24
- package/src/components/data-table-row.tsx +16 -17
- package/src/components/data-table-shared.tsx +10 -16
- package/src/components/data-table-sort.ts +1 -2
- package/src/components/data-table-virtual.ts +9 -4
- package/src/components/data-table.tsx +489 -501
- package/src/components/date-picker.tsx +1 -7
- package/src/components/date-range-picker.tsx +3 -13
- package/src/components/description-list.tsx +7 -13
- package/src/components/dialog-frame.tsx +12 -3
- package/src/components/disclosure.tsx +8 -12
- package/src/components/dropdown.tsx +3 -11
- package/src/components/empty-state.tsx +1 -3
- package/src/components/entity-combobox.tsx +2 -6
- package/src/components/error-boundary.tsx +7 -7
- package/src/components/field-base.tsx +8 -14
- package/src/components/field-parts.tsx +12 -23
- package/src/components/field-sync.tsx +7 -11
- package/src/components/file-button.tsx +7 -3
- package/src/components/file-dropzone.tsx +7 -12
- package/src/components/floating-panel.tsx +44 -51
- package/src/components/focus-ring.ts +1 -2
- package/src/components/form-actions.tsx +1 -4
- package/src/components/grouped-picker.tsx +6 -8
- package/src/components/hover-menu.tsx +1 -4
- package/src/components/iban-input-labels.ts +1 -2
- package/src/components/icon-button.tsx +46 -48
- package/src/components/icon-picker.tsx +4 -17
- package/src/components/image-grid.tsx +1 -6
- package/src/components/inline-edit-field.tsx +1 -5
- package/src/components/inline-entity-combobox.tsx +6 -21
- package/src/components/input.tsx +14 -2
- package/src/components/jump-calendar.tsx +2 -6
- package/src/components/legal-labels.ts +1 -2
- package/src/components/legal-page.tsx +18 -7
- package/src/components/legal.tsx +14 -5
- package/src/components/lightbox.tsx +1 -3
- package/src/components/line-items.tsx +7 -15
- package/src/components/list.tsx +29 -25
- package/src/components/measured-grid.tsx +8 -30
- package/src/components/menu-item.tsx +1 -3
- package/src/components/mini-calendar.tsx +129 -159
- package/src/components/money-field.tsx +4 -2
- package/src/components/month-picker.tsx +14 -21
- package/src/components/multi-entity-combobox.tsx +6 -26
- package/src/components/multi-select.tsx +5 -9
- package/src/components/nav-pills.tsx +10 -2
- package/src/components/number-field.tsx +18 -15
- package/src/components/number-input.tsx +3 -19
- package/src/components/numpad-sheet.tsx +9 -13
- package/src/components/one-time-code-input.tsx +6 -11
- package/src/components/page-contents.tsx +2 -9
- package/src/components/page-header.tsx +5 -10
- package/src/components/password-strength-labels.ts +1 -2
- package/src/components/password-strength.tsx +6 -9
- package/src/components/phone-input.tsx +2 -13
- package/src/components/picker-sheet.tsx +2 -5
- package/src/components/pie-chart.tsx +6 -12
- package/src/components/popover.tsx +2 -1
- package/src/components/progress-bar.tsx +16 -25
- package/src/components/qr-code.tsx +2 -9
- package/src/components/row-actions.tsx +9 -1
- package/src/components/search-field.tsx +4 -5
- package/src/components/select.tsx +2 -10
- package/src/components/separator.tsx +1 -5
- package/src/components/series-chart-axes.ts +5 -5
- package/src/components/series-chart-budget.ts +1 -4
- package/src/components/series-chart-ticks.ts +1 -4
- package/src/components/series-chart.tsx +43 -83
- package/src/components/settings-fields.tsx +20 -15
- package/src/components/share-card.tsx +6 -14
- package/src/components/sign-chip.tsx +16 -16
- package/src/components/signature-pad.tsx +21 -13
- package/src/components/signed-amount.tsx +6 -14
- package/src/components/skeleton.tsx +2 -13
- package/src/components/slider.tsx +9 -20
- package/src/components/sparkline.tsx +2 -13
- package/src/components/stat-tile.tsx +13 -24
- package/src/components/swatch-picker.tsx +8 -3
- package/src/components/swipeable-row.tsx +2 -11
- package/src/components/table-skeleton.tsx +4 -1
- package/src/components/table.tsx +12 -8
- package/src/components/tabs.tsx +24 -39
- package/src/components/text-link.tsx +6 -6
- package/src/components/text.tsx +1 -2
- package/src/components/tile-radio.tsx +6 -19
- package/src/components/time-input.tsx +4 -5
- package/src/components/toast.tsx +3 -12
- package/src/components/toggle-group.tsx +9 -9
- package/src/components/toggle-legend.tsx +4 -16
- package/src/components/tooltip.tsx +9 -48
- package/src/components/translation-review-editor.tsx +4 -4
- package/src/components/translation-review-parts.tsx +7 -1
- package/src/components/translation-review.tsx +15 -16
- package/src/components/tree-view.tsx +17 -20
- package/src/components/treemap.tsx +3 -15
- package/src/components/trigger-aria.ts +1 -4
- package/src/components/use-table-state.ts +16 -17
- package/src/components/user-avatar.tsx +4 -21
- package/src/demo/demo-start.tsx +8 -4
- package/src/feedback/feedback-attachment-labels.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +16 -29
- package/src/feedback/feedback-capture.ts +3 -1
- package/src/feedback/feedback-comment-thread.tsx +9 -6
- package/src/feedback/feedback-context.tsx +1 -4
- package/src/feedback/feedback-dialog.tsx +3 -4
- package/src/feedback/feedback-inbox.tsx +3 -9
- package/src/feedback/feedback-public-list.tsx +7 -1
- package/src/feedback/feedback-record.ts +6 -14
- package/src/feedback/feedback-replies.tsx +10 -5
- package/src/feedback/feedback-row-detail.tsx +2 -5
- package/src/feedback/feedback-status-undo.ts +1 -4
- package/src/feedback/feedback-submit-dialog.tsx +4 -4
- package/src/feedback/feedback-table.tsx +12 -8
- package/src/feedback/feedback-thread.tsx +4 -16
- package/src/feedback/feedback-vocabulary.ts +72 -82
- package/src/feedback/feedback-vote-button.tsx +9 -8
- package/src/hooks/use-anchored-rect.ts +1 -4
- package/src/hooks/use-document-title.ts +3 -1
- package/src/hooks/use-file-drop.ts +1 -3
- package/src/hooks/use-file-picker-labels.ts +1 -2
- package/src/hooks/use-file-picker.tsx +1 -5
- package/src/hooks/use-focus-trap.ts +1 -2
- package/src/hooks/use-overlay-history.ts +10 -5
- package/src/hooks/use-row-swipe.ts +1 -6
- package/src/i18n/defaults.ts +1 -4
- package/src/i18n/german.ts +33 -49
- package/src/i18n/kit-labels.tsx +5 -20
- package/src/i18n/languages.ts +2 -8
- package/src/i18n/locales/en.ts +11 -31
- package/src/i18n/locales/es.ts +35 -49
- package/src/i18n/locales/fr.ts +34 -51
- package/src/i18n/locales/hu.ts +26 -37
- package/src/i18n/locales/it.ts +35 -52
- package/src/i18n/locales/zh.ts +14 -32
- package/src/i18n/plurals.ts +4 -1
- package/src/i18n/review.ts +18 -7
- package/src/i18n/shared-labels.ts +1 -2
- package/src/index.ts +106 -32
- package/src/landing/access.ts +2 -7
- package/src/landing/landing-sections.tsx +14 -15
- package/src/landing/page-seo.ts +1 -3
- package/src/lib/calc.ts +4 -1
- package/src/lib/clipboard-minus.ts +4 -1
- package/src/lib/dates.ts +2 -10
- package/src/lib/decimal-marks.ts +1 -3
- package/src/lib/device-settings.ts +2 -1
- package/src/lib/direction.ts +1 -3
- package/src/lib/iban.ts +89 -9
- package/src/lib/logger.ts +1 -5
- package/src/lib/person-name.ts +1 -3
- package/src/lib/phone.ts +5 -8
- package/src/lib/qr-encode.ts +32 -8
- package/src/lib/session-refresh.ts +5 -6
- package/src/lib/table-text.ts +24 -6
- package/src/lib/text-match.ts +15 -3
- package/src/lib/translation-review.ts +1 -3
- package/src/notifications/notification-inbox.tsx +17 -4
- package/src/notifications/notification-parts.tsx +1 -2
- package/src/notifications/notification-types-setting.tsx +19 -18
- package/src/notifications/notifications-labels.ts +1 -2
- package/src/notifications/push-device-setting.tsx +7 -1
- package/src/notifications/push-devices-setting.tsx +7 -3
- package/src/notifications/push-worker.ts +10 -2
- package/src/notifications/quiet-hours-setting.tsx +4 -1
- package/src/notifications/use-push-device.ts +4 -1
- package/src/offline/pwa-update-prompt.tsx +7 -1
- package/src/offline/query-persistence.ts +9 -6
- package/src/offline.ts +6 -1
- package/src/rhf/fields.tsx +50 -19
- package/src/rhf/form-core.tsx +2 -6
- package/src/rhf/line-items.tsx +5 -7
- package/src/rhf/use-rhf-wizard-step.ts +3 -12
- package/src/search/command-palette.tsx +15 -23
- package/src/search/global-search.tsx +38 -8
- package/src/search/search-index.ts +5 -1
- package/src/settings/device-reset-setting.tsx +5 -8
- package/src/settings/settings-heading.tsx +3 -1
- package/src/settings/settings-layout.tsx +6 -13
- package/src/settings/use-settings-route.ts +4 -3
- package/src/shell/app-shell.tsx +48 -24
- package/src/shell/auth-layout.tsx +1 -6
- package/src/shell/dev-menu.tsx +12 -6
- package/src/shell/option-switcher-menu.tsx +1 -3
- package/src/shell/sync-status-indicator.tsx +14 -12
- package/src/shell/top-bar-parts-labels.ts +1 -3
- package/src/shell/topbar-action-menu.tsx +9 -8
- package/src/shell/topbar-controls.tsx +5 -19
- package/src/shell/tour-menu.tsx +9 -1
- package/src/sync/db.ts +5 -1
- package/src/sync/engine.ts +44 -12
- package/src/sync.ts +16 -2
- package/src/testing/css-variables.ts +1 -2
- package/src/testing/index.ts +1 -6
- package/src/testing/react.ts +1 -2
- package/src/testing/viewport.ts +9 -9
- package/src/theme/contrast-tokens.ts +13 -2
- package/src/theme/palette-derive.ts +24 -50
- package/src/theme/palette-presets.ts +0 -1
- package/src/theme/palette-store.ts +1 -4
- package/src/theme/text-size.ts +1 -1
- package/src/theme/theme-store.ts +2 -6
- package/src/tour/tour-catalogue.tsx +4 -2
- package/src/tour/tour-dock.ts +5 -1
- package/src/tour/tour-engine.ts +2 -6
- package/src/tour/tour.tsx +28 -12
- package/src/tour/use-tour-progress.ts +8 -2
- package/src/tour/use-tour-url-sync.ts +3 -1
- package/src/vite-env.d.ts +1 -4
- package/src/wizard/stepper-nav.tsx +10 -35
- package/src/wizard/types.ts +1 -4
- package/src/wizard/use-wizard.ts +28 -43
- package/src/wizard/validation.ts +1 -5
- package/src/wizard/wizard-context.tsx +2 -6
- package/src/wizard/wizard-step.tsx +1 -8
- package/src/wizard/wizard-summary.tsx +2 -6
- package/tokens.css +393 -152
|
@@ -39,59 +39,50 @@ function ImageGrid({
|
|
|
39
39
|
...style
|
|
40
40
|
};
|
|
41
41
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
42
|
-
/* @__PURE__ */ jsx(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
type: "button",
|
|
58
|
-
"aria-label": name,
|
|
59
|
-
onClick: () => onOpen ? onOpen(i) : setOpenIndex(i),
|
|
60
|
-
className: cn(
|
|
61
|
-
"block w-full overflow-hidden rounded-md border border-subtle bg-hover",
|
|
62
|
-
"focus-visible:outline-focus focus-visible:outline-offset-2 focus-visible:outline-brand",
|
|
63
|
-
ASPECT_CLASS[aspect]
|
|
64
|
-
),
|
|
65
|
-
children: kind === "file" ? /* @__PURE__ */ jsxs("span", { className: "flex h-full w-full flex-col items-center justify-center gap-1 p-2 text-center text-xs text-secondary", children: [
|
|
66
|
-
/* @__PURE__ */ jsx(FileText, { "aria-hidden": true, className: "size-6 shrink-0 text-muted" }),
|
|
67
|
-
/* @__PURE__ */ jsx(FileNameLabel, { name: item.fileName ?? item.alt ?? "" })
|
|
68
|
-
] }) : /* @__PURE__ */ jsx(
|
|
69
|
-
AuthedImage,
|
|
70
|
-
{
|
|
71
|
-
src: item.thumbnailSrc ?? item.src,
|
|
72
|
-
alt: "",
|
|
73
|
-
fetcher,
|
|
74
|
-
loading: "lazy",
|
|
75
|
-
wrapperClassName: "h-full w-full",
|
|
76
|
-
className: cn("h-full w-full", aspect === "auto" ? "object-contain" : "object-cover")
|
|
77
|
-
}
|
|
78
|
-
)
|
|
79
|
-
}
|
|
42
|
+
/* @__PURE__ */ jsx("ul", { "aria-label": labels.list, ...rest, className: cn("grid gap-2", className), style: gridStyle, children: items.map((item, i) => {
|
|
43
|
+
const name = item.alt || labels.item(i + 1, count);
|
|
44
|
+
const kind = imageItemKind(item);
|
|
45
|
+
const actions = renderActions?.(item, i);
|
|
46
|
+
return /* @__PURE__ */ jsxs("li", { className: "relative min-w-0", children: [
|
|
47
|
+
/* @__PURE__ */ jsx(
|
|
48
|
+
"button",
|
|
49
|
+
{
|
|
50
|
+
type: "button",
|
|
51
|
+
"aria-label": name,
|
|
52
|
+
onClick: () => onOpen ? onOpen(i) : setOpenIndex(i),
|
|
53
|
+
className: cn(
|
|
54
|
+
"block w-full overflow-hidden rounded-md border border-subtle bg-hover",
|
|
55
|
+
"focus-visible:outline-focus focus-visible:outline-offset-2 focus-visible:outline-brand",
|
|
56
|
+
ASPECT_CLASS[aspect]
|
|
80
57
|
),
|
|
81
|
-
|
|
82
|
-
"
|
|
58
|
+
children: kind === "file" ? /* @__PURE__ */ jsxs("span", { className: "flex h-full w-full flex-col items-center justify-center gap-1 p-2 text-center text-xs text-secondary", children: [
|
|
59
|
+
/* @__PURE__ */ jsx(FileText, { "aria-hidden": true, className: "size-6 shrink-0 text-muted" }),
|
|
60
|
+
/* @__PURE__ */ jsx(FileNameLabel, { name: item.fileName ?? item.alt ?? "" })
|
|
61
|
+
] }) : /* @__PURE__ */ jsx(
|
|
62
|
+
AuthedImage,
|
|
83
63
|
{
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
64
|
+
src: item.thumbnailSrc ?? item.src,
|
|
65
|
+
alt: "",
|
|
66
|
+
fetcher,
|
|
67
|
+
loading: "lazy",
|
|
68
|
+
wrapperClassName: "h-full w-full",
|
|
69
|
+
className: cn("h-full w-full", aspect === "auto" ? "object-contain" : "object-cover")
|
|
88
70
|
}
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
),
|
|
74
|
+
actions !== void 0 && actions !== null && actions !== false && /* @__PURE__ */ jsx(
|
|
75
|
+
"div",
|
|
76
|
+
{
|
|
77
|
+
role: "group",
|
|
78
|
+
"aria-label": labels.actions(name),
|
|
79
|
+
className: "absolute end-1 top-1 flex gap-1 rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_40%,transparent)] p-0.5 backdrop-blur-sm",
|
|
80
|
+
children: actions
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
item.caption !== void 0 && item.caption !== null && item.caption !== false && /* @__PURE__ */ jsx("div", { className: "mt-1 line-clamp-2 text-xs text-secondary", children: item.caption })
|
|
84
|
+
] }, item.key ?? item.src);
|
|
85
|
+
}) }),
|
|
95
86
|
!onOpen && /* @__PURE__ */ jsx(
|
|
96
87
|
Lightbox,
|
|
97
88
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/image-grid.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { FileText } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { AuthedImage } from \"./authed-image\";\nimport { Lightbox, imageItemKind } from \"./lightbox\";\nimport type { ImageItem, LightboxLabels } from \"./lightbox\";\nimport { DEFAULT_IMAGE_GRID_LABELS } from \"./image-grid-labels\";\n\nexport interface ImageGridLabels {\n /** The list's accessible name. */\n list: string;\n /** An open button's name when an item's `alt` is empty: \"Image 3 of 7\". */\n item: (index: number, count: number) => string;\n /** The name of the group of per-image actions, given the image's name. */\n actions: (name: string) => string;\n}\n\n// Defined in ./image-grid-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_IMAGE_GRID_LABELS };\n\nexport interface ImageGridProps extends Omit<ComponentPropsWithRef<\"ul\">, \"children\"> {\n items: readonly ImageItem[];\n /** The app's authenticated GET — thumbnails and the lightbox fetch through it. Only\n * images are fetched for the grid; a file (PDF) tile shows its icon and name. */\n fetcher?: AuthedFetcher | null;\n /**\n * Per-image actions — a delete, an edit. Rendered BESIDE the open button (on top of\n * the thumbnail's corner), never inside it: a button inside a button is invalid HTML\n * and a screen reader flattens it into the outer one's name, where it cannot be\n * reached. Use `IconButton variant=\"overlay\"` for controls that sit on a photo.\n */\n renderActions?: (item: ImageItem, index: number) => ReactNode;\n /**\n * What a tile's click does. Left out, the grid opens its own {@link Lightbox} on that\n * image. Given, the grid opens nothing — for a caller that keeps the lightbox's\n * index in the URL, or that opens something else.\n */\n onOpen?: (index: number) => void;\n /**\n * A fixed column count. Left out, the grid is responsive: as many columns of at\n * least {@link minTileSize} px as fit, so the same grid is three across in a card on\n * a phone and eight across on a desktop page — no breakpoints to choose.\n */\n columns?: number;\n /** The smallest a tile may be in the responsive layout. Default 96 (px). */\n minTileSize?: number;\n /** `square` (default) crops to a square; `video` to 16:9; `auto` keeps each image's\n * own proportions (a floor plan), within a square-ish box. */\n aspect?: \"square\" | \"video\" | \"auto\";\n /** Props for the built-in lightbox (ignored with {@link onOpen}). */\n lightbox?: {\n loop?: boolean;\n zoom?: boolean;\n download?: boolean;\n onDownload?: (item: ImageItem, index: number) => void;\n labels?: LabelOverride<LightboxLabels>;\n };\n labels?: LabelOverride<ImageGridLabels>;\n}\n\nconst ASPECT_CLASS = {\n square: \"aspect-square\",\n video: \"aspect-video\",\n auto: \"aspect-[4/3]\",\n} as const;\n\n/**\n * A file name as stem and extension, so a tile can cut the stem and keep the\n * extension whole: \"floor-plan-level-2.p / df\" (a break inside the extension) told the\n * reader less than \"floor-plan-le….pdf\" does. A leading dot (\".env\") or an\n * \"extension\" longer than five characters is not treated as one.\n */\nexport function splitFileName(name: string): [stem: string, ext: string] {\n const dot = name.lastIndexOf(\".\");\n if (dot <= 0 || name.length - dot > 6) return [name, \"\"];\n return [name.slice(0, dot), name.slice(dot)];\n}\n\n/**\n * A grid of thumbnails that open in a {@link Lightbox} — kastlan's room and defect\n * photos (room-inspector, unit-rooms-tab) and its floor plans (unit-floor-plan), each\n * of which drew its own `grid grid-cols-N` of `<img>` that nothing could open, with no\n * loading state and nowhere to put a delete.\n *\n * Each tile is a real `<button>` named by the image's alt, with the actions beside it\n * and the caption below it — outside the button, so the name stays the alt and the\n * caption is read once. Placeholders come from {@link AuthedImage}: a skeleton while\n * loading, a \"couldn't load\" tile on failure.\n */\nexport function ImageGrid({\n items,\n fetcher,\n renderActions,\n onOpen,\n columns,\n minTileSize = 96,\n aspect = \"square\",\n lightbox,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: ImageGridProps) {\n const labels = useKitLabels(\"imageGrid\", DEFAULT_IMAGE_GRID_LABELS, labelsProp);\n const [openIndex, setOpenIndex] = useState<number | null>(null);\n const count = items.length;\n\n const gridStyle: CSSProperties = {\n gridTemplateColumns:\n columns !== undefined\n ? `repeat(${columns}, minmax(0, 1fr))`\n : `repeat(auto-fill, minmax(min(${minTileSize}px, 100%), 1fr))`,\n ...style,\n };\n\n return (\n <>\n <ul\n aria-label={labels.list}\n {...rest}\n className={cn(\"grid gap-2\", className)}\n style={gridStyle}\n >\n {items.map((item, i) => {\n const name = item.alt || labels.item(i + 1, count);\n const kind = imageItemKind(item);\n const actions = renderActions?.(item, i);\n return (\n <li key={item.key ?? item.src} className=\"relative min-w-0\">\n <button\n type=\"button\"\n aria-label={name}\n onClick={() => (onOpen ? onOpen(i) : setOpenIndex(i))}\n className={cn(\n \"block w-full overflow-hidden rounded-md border border-subtle bg-hover\",\n \"focus-visible:outline-focus focus-visible:outline-offset-2 focus-visible:outline-brand\",\n ASPECT_CLASS[aspect],\n )}\n >\n {kind === \"file\" ? (\n <span className=\"flex h-full w-full flex-col items-center justify-center gap-1 p-2 text-center text-xs text-secondary\">\n <FileText aria-hidden className=\"size-6 shrink-0 text-muted\" />\n <FileNameLabel name={item.fileName ?? item.alt ?? \"\"} />\n </span>\n ) : (\n <AuthedImage\n src={item.thumbnailSrc ?? item.src}\n // The button is named already; the picture inside it is decoration\n // to a reader, or the name would be read twice.\n alt=\"\"\n fetcher={fetcher}\n loading=\"lazy\"\n wrapperClassName=\"h-full w-full\"\n className={cn(\"h-full w-full\", aspect === \"auto\" ? \"object-contain\" : \"object-cover\")}\n />\n )}\n </button>\n {actions !== undefined && actions !== null && actions !== false && (\n <div\n role=\"group\"\n aria-label={labels.actions(name)}\n // A backdrop behind the whole group, in the inverse token the overlay\n // buttons use: a translucent disc alone took most of its colour from\n // the picture, and on a pale photo or the light file tile the glyphs\n // washed out. Two layers of the inverse keep them readable over any\n // picture, in both themes.\n className=\"absolute end-1 top-1 flex gap-1 rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_40%,transparent)] p-0.5 backdrop-blur-sm\"\n >\n {actions}\n </div>\n )}\n {item.caption !== undefined && item.caption !== null && item.caption !== false && (\n <div className=\"mt-1 line-clamp-2 text-xs text-secondary\">{item.caption}</div>\n )}\n </li>\n );\n })}\n </ul>\n {!onOpen && (\n <Lightbox\n open={openIndex !== null}\n onClose={() => setOpenIndex(null)}\n items={items}\n index={openIndex ?? 0}\n onIndexChange={setOpenIndex}\n fetcher={fetcher}\n {...lightbox}\n />\n )}\n </>\n );\n}\n\n/** One line: the stem truncates, the extension never does. */\nfunction FileNameLabel({ name }: { name: string }) {\n const [stem, ext] = splitFileName(name);\n return (\n <span aria-hidden className=\"flex max-w-full min-w-0\">\n <span className=\"min-w-0 truncate\">{stem}</span>\n {ext && <span className=\"shrink-0\">{ext}</span>}\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/image-grid.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { FileText } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { AuthedImage } from \"./authed-image\";\nimport { Lightbox, imageItemKind } from \"./lightbox\";\nimport type { ImageItem, LightboxLabels } from \"./lightbox\";\nimport { DEFAULT_IMAGE_GRID_LABELS } from \"./image-grid-labels\";\n\nexport interface ImageGridLabels {\n /** The list's accessible name. */\n list: string;\n /** An open button's name when an item's `alt` is empty: \"Image 3 of 7\". */\n item: (index: number, count: number) => string;\n /** The name of the group of per-image actions, given the image's name. */\n actions: (name: string) => string;\n}\n\n// Defined in ./image-grid-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_IMAGE_GRID_LABELS };\n\nexport interface ImageGridProps extends Omit<ComponentPropsWithRef<\"ul\">, \"children\"> {\n items: readonly ImageItem[];\n /** The app's authenticated GET — thumbnails and the lightbox fetch through it. Only\n * images are fetched for the grid; a file (PDF) tile shows its icon and name. */\n fetcher?: AuthedFetcher | null;\n /**\n * Per-image actions — a delete, an edit. Rendered BESIDE the open button (on top of\n * the thumbnail's corner), never inside it: a button inside a button is invalid HTML\n * and a screen reader flattens it into the outer one's name, where it cannot be\n * reached. Use `IconButton variant=\"overlay\"` for controls that sit on a photo.\n */\n renderActions?: (item: ImageItem, index: number) => ReactNode;\n /**\n * What a tile's click does. Left out, the grid opens its own {@link Lightbox} on that\n * image. Given, the grid opens nothing — for a caller that keeps the lightbox's\n * index in the URL, or that opens something else.\n */\n onOpen?: (index: number) => void;\n /**\n * A fixed column count. Left out, the grid is responsive: as many columns of at\n * least {@link minTileSize} px as fit, so the same grid is three across in a card on\n * a phone and eight across on a desktop page — no breakpoints to choose.\n */\n columns?: number;\n /** The smallest a tile may be in the responsive layout. Default 96 (px). */\n minTileSize?: number;\n /** `square` (default) crops to a square; `video` to 16:9; `auto` keeps each image's\n * own proportions (a floor plan), within a square-ish box. */\n aspect?: \"square\" | \"video\" | \"auto\";\n /** Props for the built-in lightbox (ignored with {@link onOpen}). */\n lightbox?: {\n loop?: boolean;\n zoom?: boolean;\n download?: boolean;\n onDownload?: (item: ImageItem, index: number) => void;\n labels?: LabelOverride<LightboxLabels>;\n };\n labels?: LabelOverride<ImageGridLabels>;\n}\n\nconst ASPECT_CLASS = {\n square: \"aspect-square\",\n video: \"aspect-video\",\n auto: \"aspect-[4/3]\",\n} as const;\n\n/**\n * A file name as stem and extension, so a tile can cut the stem and keep the\n * extension whole: \"floor-plan-level-2.p / df\" (a break inside the extension) told the\n * reader less than \"floor-plan-le….pdf\" does. A leading dot (\".env\") or an\n * \"extension\" longer than five characters is not treated as one.\n */\nexport function splitFileName(name: string): [stem: string, ext: string] {\n const dot = name.lastIndexOf(\".\");\n if (dot <= 0 || name.length - dot > 6) return [name, \"\"];\n return [name.slice(0, dot), name.slice(dot)];\n}\n\n/**\n * A grid of thumbnails that open in a {@link Lightbox} — kastlan's room and defect\n * photos (room-inspector, unit-rooms-tab) and its floor plans (unit-floor-plan), each\n * of which drew its own `grid grid-cols-N` of `<img>` that nothing could open, with no\n * loading state and nowhere to put a delete.\n *\n * Each tile is a real `<button>` named by the image's alt, with the actions beside it\n * and the caption below it — outside the button, so the name stays the alt and the\n * caption is read once. Placeholders come from {@link AuthedImage}: a skeleton while\n * loading, a \"couldn't load\" tile on failure.\n */\nexport function ImageGrid({\n items,\n fetcher,\n renderActions,\n onOpen,\n columns,\n minTileSize = 96,\n aspect = \"square\",\n lightbox,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: ImageGridProps) {\n const labels = useKitLabels(\"imageGrid\", DEFAULT_IMAGE_GRID_LABELS, labelsProp);\n const [openIndex, setOpenIndex] = useState<number | null>(null);\n const count = items.length;\n\n const gridStyle: CSSProperties = {\n gridTemplateColumns:\n columns !== undefined\n ? `repeat(${columns}, minmax(0, 1fr))`\n : `repeat(auto-fill, minmax(min(${minTileSize}px, 100%), 1fr))`,\n ...style,\n };\n\n return (\n <>\n <ul aria-label={labels.list} {...rest} className={cn(\"grid gap-2\", className)} style={gridStyle}>\n {items.map((item, i) => {\n const name = item.alt || labels.item(i + 1, count);\n const kind = imageItemKind(item);\n const actions = renderActions?.(item, i);\n return (\n <li key={item.key ?? item.src} className=\"relative min-w-0\">\n <button\n type=\"button\"\n aria-label={name}\n onClick={() => (onOpen ? onOpen(i) : setOpenIndex(i))}\n className={cn(\n \"block w-full overflow-hidden rounded-md border border-subtle bg-hover\",\n \"focus-visible:outline-focus focus-visible:outline-offset-2 focus-visible:outline-brand\",\n ASPECT_CLASS[aspect],\n )}\n >\n {kind === \"file\" ? (\n <span className=\"flex h-full w-full flex-col items-center justify-center gap-1 p-2 text-center text-xs text-secondary\">\n <FileText aria-hidden className=\"size-6 shrink-0 text-muted\" />\n <FileNameLabel name={item.fileName ?? item.alt ?? \"\"} />\n </span>\n ) : (\n <AuthedImage\n src={item.thumbnailSrc ?? item.src}\n // The button is named already; the picture inside it is decoration\n // to a reader, or the name would be read twice.\n alt=\"\"\n fetcher={fetcher}\n loading=\"lazy\"\n wrapperClassName=\"h-full w-full\"\n className={cn(\"h-full w-full\", aspect === \"auto\" ? \"object-contain\" : \"object-cover\")}\n />\n )}\n </button>\n {actions !== undefined && actions !== null && actions !== false && (\n <div\n role=\"group\"\n aria-label={labels.actions(name)}\n // A backdrop behind the whole group, in the inverse token the overlay\n // buttons use: a translucent disc alone took most of its colour from\n // the picture, and on a pale photo or the light file tile the glyphs\n // washed out. Two layers of the inverse keep them readable over any\n // picture, in both themes.\n className=\"absolute end-1 top-1 flex gap-1 rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_40%,transparent)] p-0.5 backdrop-blur-sm\"\n >\n {actions}\n </div>\n )}\n {item.caption !== undefined && item.caption !== null && item.caption !== false && (\n <div className=\"mt-1 line-clamp-2 text-xs text-secondary\">{item.caption}</div>\n )}\n </li>\n );\n })}\n </ul>\n {!onOpen && (\n <Lightbox\n open={openIndex !== null}\n onClose={() => setOpenIndex(null)}\n items={items}\n index={openIndex ?? 0}\n onIndexChange={setOpenIndex}\n fetcher={fetcher}\n {...lightbox}\n />\n )}\n </>\n );\n}\n\n/** One line: the stem truncates, the extension never does. */\nfunction FileNameLabel({ name }: { name: string }) {\n const [stem, ext] = splitFileName(name);\n return (\n <span aria-hidden className=\"flex max-w-full min-w-0\">\n <span className=\"min-w-0 truncate\">{stem}</span>\n {ext && <span className=\"shrink-0\">{ext}</span>}\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/inline-edit-field.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { FocusEvent, InputHTMLAttributes, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { hasContent } from \"../lib/presence\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { claimEscape } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { Input } from \"./input\";\nimport { Spinner } from \"./spinner\";\nimport { FIELD_ERROR_CLASS } from \"./field-parts\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { Tooltip } from \"./tooltip\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { DEFAULT_INLINE_EDIT_LABELS } from \"./inline-edit-field-labels\";\n\n/**\n * Every string {@link InlineEditField} says on its own behalf — the `inlineEdit`\n * namespace of `<UiKitProvider labels>`, overridable per instance through `labels`.\n */\nexport interface InlineEditLabels {\n /** The display button's tooltip and description — what pressing it does. Receives\n * the field's `label` (\"Budget name\" → \"Edit Budget name\"). */\n edit: (label: string) => string;\n /** Shown (and spoken) when `onCommit` rejects and no `formatError` says better. */\n failed: string;\n /** What the display shows for an empty value, when no `placeholder` is given. */\n empty: string;\n}\n\n// Defined in ./inline-edit-field-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_INLINE_EDIT_LABELS };\n\n/** What an `editor` render prop is handed: the draft and the means to end the edit. */\nexport interface InlineEditorProps<T> {\n /** The draft — seeded from `value` each time the editor opens. */\n value: T;\n /** Replace the draft (every keystroke). */\n onChange: (next: T) => void;\n /**\n * End the edit and save. Without an argument it saves the draft; WITH one it saves\n * that — for an editor whose own commit hands over a resolved value (`NumberInput`'s\n * `onCommit` gives \"1050\" for a typed \"1100-50\"). Called twice (the editor's own\n * Enter, then the field's), the second call does nothing.\n */\n commit: (next?: T) => void;\n /** End the edit and drop the draft — what Escape does. */\n cancel: () => void;\n /** The save is in flight: make the control read-only (not disabled — it has focus). */\n pending: boolean;\n /** The field's `label`, for the control's accessible name. */\n label: string;\n /** The last save failed and the draft has not been touched since. */\n invalid: boolean;\n /** The id of the failure message while there is one, for `aria-describedby`. */\n describedBy: string | undefined;\n /** The four above as attributes — spread them on a kit field that takes the DOM\n * spelling: `<AmountInput {...p.inputProps} … />`. */\n inputProps: {\n \"aria-label\": string;\n \"aria-invalid\": true | undefined;\n \"aria-describedby\": string | undefined;\n readOnly: boolean;\n };\n}\n\ninterface InlineEditFieldBaseProps<T> {\n /** The saved value — what the display shows, and what the editor opens on. */\n value: T;\n /**\n * Save `next`. Return a promise and the field waits: the editor stays open and\n * read-only with a spinner until it settles, closes when it resolves, and STAYS OPEN\n * with the failure under it when it rejects (or throws) — the typed value is kept for\n * a retry. Not called when the draft equals `value` (see `isEqual`): opening the\n * editor and leaving it unchanged is not a write.\n */\n onCommit: (next: T) => void | Promise<unknown>;\n /** What the value is (\"Budget name\", \"Assigned\") — the editor's accessible name and\n * the subject of the display's \"Edit …\" description. Not drawn: inline, the row or\n * the column header is the visible label. */\n label: string;\n /** The value as the display shows it — a formatted amount, a date. Default: the\n * value as text. */\n display?: (value: T) => ReactNode;\n /** Whether the draft is unchanged, so leaving the editor is not a write. Default\n * `Object.is`. keksdose compares assigned amounts as NUMBERS — \"1100\" is the\n * server's \"1100.0000\". */\n isEqual?: (a: T, b: T) => boolean;\n /** What the display shows for an empty value (`\"\"`, `null`, an empty `display`).\n * Default `labels.empty`. */\n placeholder?: ReactNode;\n /** `\"end\"` for a figure in a right-aligned money column. Default `\"start\"`. */\n align?: \"start\" | \"end\";\n /** Look, do not touch — a guest's view: the value as plain text, no button. */\n readOnly?: boolean;\n /**\n * Why it cannot be edited now — the write lock's sentence, a closed period. The\n * value stays a BUTTON (focusable, `aria-disabled`) with the reason in the kit\n * Tooltip and its description; it opens no editor. An editor that is open when the\n * reason arrives closes, its draft dropped: a field you can type into must be one\n * whose value can be saved (keksdose dev#496).\n */\n disabledReason?: ReactNode;\n /** This field COMMITS — under a locked {@link WriteLockProvider} it takes the\n * `disabledReason` path with the lock's reason (which wins over its own). No\n * provider, or an unlocked one: no effect. */\n commit?: CommitScope;\n /** The value is the user's own data: `data-private` on the display and the editor,\n * so the app's demo-mode blur covers both. */\n redact?: boolean;\n /** Turns a rejection into words. Default `labels.failed`. */\n formatError?: (error: unknown) => ReactNode;\n /** Controlled open state — a row's swipe action that opens the editor from outside\n * (keksdose's assigned cell). Left out, the field keeps its own. */\n editing?: boolean;\n /** Called with the next open state: on open, on a save that resolved, on cancel. */\n onEditingChange?: (editing: boolean) => void;\n /** Attributes for the DEFAULT text editor's `<input>` — `maxLength`, `inputMode`,\n * `autoComplete`. Ignored with `editor`. */\n inputProps?: Omit<InputHTMLAttributes<HTMLInputElement>, \"value\" | \"defaultValue\" | \"onChange\" | \"type\">;\n /** Classes for the root. */\n className?: string;\n labels?: LabelOverride<InlineEditLabels>;\n}\n\ntype EditorRender<T> = (props: InlineEditorProps<T>) => ReactNode;\n\n/**\n * The editor. Optional for a string value — the kit's `Input`, compact — and\n * REQUIRED for anything else, since only the caller knows how to edit a number or an\n * amount. Render a kit field from the props it is handed:\n *\n * ```tsx\n * editor={(p) => (\n * <NumberInput calculator={false} aria-label={p.label} value={p.value} onChange={p.onChange}\n * onCommit={p.commit} aria-invalid={p.invalid || undefined} aria-describedby={p.describedBy} />\n * )}\n * ```\n *\n * The field itself handles Enter (save), Escape (cancel) and focus leaving the editor\n * (save) around whatever is rendered — a key the editor has already handled\n * (`preventDefault`, as a listbox does for its Enter) is left to it. Focus counts as\n * \"inside\" across a portal, so a picker or a phone keypad the editor opens does not\n * end the edit.\n */\nexport type InlineEditFieldProps<T = string> = InlineEditFieldBaseProps<T> &\n ([T] extends [string] ? { editor?: EditorRender<T> } : { editor: EditorRender<T> });\n\n/** Calls the `editor` render prop as a component of its own: the props it is handed\n * close over the field's refs, and a render prop called in the field's own render\n * would read like a ref read during render. Module-level, so the type is stable and\n * the editor never remounts. */\nfunction RenderEditor<T>({ render, ...props }: InlineEditorProps<T> & { render: EditorRender<T> }) {\n return render(props);\n}\n\n/**\n * The value at Large and Extra large (0.32.1, keksdose's 0.32 report;\n * docs/text-size-harmonization.md §4 \"nothing truncates\"): the display button, the\n * locked one and the read-only text wrap instead of ending in \"…\", and break a word too\n * long for the cell, so an IBAN or an e-mail address shows to its last character. The\n * value IS the button's accessible name and the thing being edited: a reader who needs\n * big type cannot recover a cut-off end by squinting. A `large:` class, so a table of\n * these keeps its one-line cells at Normal.\n *\n * 0.33 (§10.17): the kit's one `truncate-until-large` (tokens.css) in place of this\n * file's own `truncate` + `large:whitespace-normal large:break-words`. The utility\n * breaks with `overflow-wrap: anywhere`, which lowers the value's min-content width: in\n * an auto-sized table column (keksdose's budget and invoice lines) give the column a\n * width, or an amount can be squeezed to a digit per line.\n */\nconst VALUE_TRUNCATE = \"truncate-until-large\";\n\n/** `onCommit`'s result, if it is a promise. */\nfunction asPromise(result: unknown): Promise<unknown> | null {\n return result && typeof (result as Promise<unknown>).then === \"function\" ? (result as Promise<unknown>) : null;\n}\n\n/**\n * A value that edits in place: a quiet button showing the value; click it (or Enter\n * on it) and it becomes a field; Enter or leaving the field saves, Escape cancels\n * (keksdose K9).\n *\n * keksdose has three of them by hand — the budget's assigned cell (budget-cells.tsx),\n * the invoice line's value cells (invoice-lines-table.tsx `EditableCell`) and the\n * budget rename (budgets-page.tsx, an `Input` + Save + Cancel with no Escape) — and\n * each had to learn the same rules:\n *\n * - **The display's accessible name is the VALUE.** A cell named \"Edit\" reads a table\n * of \"edit, edit, edit\"; the action is the description instead (\"Edit Assigned\"),\n * in the kit Tooltip on hover and focus.\n * - **An unchanged draft is not a write.** Opening the editor and leaving it was a PUT\n * in keksdose until `isEqual` compared numbers, and on a sealed budget an outbox row\n * for a change nobody made.\n * - **A failed save keeps the editor.** The typed value is the user's work; the\n * failure is shown under it (attached with `aria-describedby`, and spoken), and the\n * next keystroke clears it. Escape still drops the draft.\n * - **Locked means a disabled BUTTON, not a disabled field** (keksdose dev#496): at\n * rest a real row shows this button, so a locked page shows the same, with the\n * reason — never a field that takes keys it cannot save.\n *\n * Inside a {@link DataTable} cell it needs nothing extra: the table already leaves a\n * click or a key on a control of the cell to that control. Focus returns to the value\n * after Enter or Escape; after a save that came from focus leaving, it stays where the\n * user put it.\n *\n * ```tsx\n * <InlineEditField label={t(\"budgets.name\")} value={budget.name}\n * onCommit={(name) => rename.mutateAsync({ id: budget.id, name })} inputProps={{ maxLength: 120 }} />\n * ```\n */\nexport function InlineEditField<T = string>(props: InlineEditFieldProps<T>) {\n const {\n value,\n onCommit,\n label,\n display,\n isEqual = Object.is,\n placeholder,\n align = \"start\",\n readOnly = false,\n disabledReason: ownDisabledReason,\n commit,\n redact = false,\n formatError,\n editing: editingProp,\n onEditingChange,\n inputProps,\n className,\n labels: labelsProp,\n } = props;\n const editor = props.editor as EditorRender<T> | undefined;\n const labels = useKitLabels(\"inlineEdit\", DEFAULT_INLINE_EDIT_LABELS, labelsProp);\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n\n const [openState, setOpenState] = useState(false);\n const open = editingProp ?? openState;\n // Locked (or read-only) wins over an open flag: a stale `editing` must never bring a\n // live commit-on-blur field back on a page that cannot save it.\n const editing = open && !locked && !readOnly;\n const [draft, setDraft] = useState<T>(value);\n const [pending, setPending] = useState(false);\n const [failure, setFailure] = useState<ReactNode>(null);\n const failed = hasContent(failure);\n\n const errorId = useId();\n const reasonId = useId();\n const displayRef = useRef<HTMLButtonElement>(null);\n const editorRef = useRef<HTMLDivElement>(null);\n const alert = useAnnounce({ politeness: \"assertive\" });\n\n // One edit ends once: Enter in a NumberInput commits through its own `onCommit` and\n // then reaches the field's Enter; a save that closes the editor unmounts the focused\n // control, which some browsers answer with a blur. The second ending is a no-op.\n const ended = useRef(false);\n const blurTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const mounted = useRef(true);\n // The latest draft for the deferred blur check, which runs after a render.\n const draftRef = useRef(draft);\n useEffect(() => {\n draftRef.current = draft;\n });\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n if (blurTimer.current) clearTimeout(blurTimer.current);\n };\n }, []);\n\n // Seed the draft when the editor opens — from wherever the open came (the button, a\n // controlled `editing`). During render, like DangerConfirm's wipe, so no frame shows\n // the editor holding the previous edit's text.\n const [wasEditing, setWasEditing] = useState(editing);\n if (wasEditing !== editing) {\n setWasEditing(editing);\n if (editing) {\n setDraft(value);\n setFailure(null);\n setPending(false);\n }\n }\n // An open editor that a lock (or `readOnly`) took away is closed for good, not\n // merely hidden until the lock lifts.\n if (openState && !editing && editingProp === undefined) setOpenState(false);\n\n // Focus follows the edit: into the editor when it opens, back to the value when it\n // closes — but only if focus went down with the editor (Enter, Escape, a save that\n // resolved under it). A save that came from focus LEAVING keeps focus where it went.\n const prevEditing = useRef(editing);\n useEffect(() => {\n const changed = prevEditing.current !== editing;\n prevEditing.current = editing;\n if (!changed) return;\n if (editing) {\n // A fresh edit, which may end once. Here rather than in the seed above: a ref is\n // not written during render, and no event can end the edit before this runs.\n ended.current = false;\n const control = editorRef.current?.querySelector<HTMLElement>(\n \"input:not([type=hidden]), textarea, select, [contenteditable='true'], [tabindex]:not([tabindex='-1']), button\",\n );\n control?.focus();\n if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) control.select();\n return;\n }\n // However it closed — a save, Escape, a lock taking it away — a blur check still\n // queued from the editor has nothing left to save.\n ended.current = true;\n if (blurTimer.current) clearTimeout(blurTimer.current);\n blurTimer.current = null;\n const lost = document.activeElement === null || document.activeElement === document.body;\n if (lost) displayRef.current?.focus();\n }, [editing]);\n\n const setOpen = (next: boolean) => {\n if (editingProp === undefined) setOpenState(next);\n onEditingChange?.(next);\n };\n\n const fail = (caught: unknown) => {\n ended.current = false;\n const message = formatError ? formatError(caught) : labels.failed;\n setFailure(hasContent(message) ? message : labels.failed);\n alert.announce(typeof message === \"string\" && message !== \"\" ? message : labels.failed);\n // Focus back into the editor if the failure left it nowhere (a pending field that\n // the browser let go of); never pulled back from somewhere the user went.\n const active = document.activeElement;\n if (active === null || active === document.body) {\n editorRef.current?.querySelector<HTMLElement>(\"input, textarea, select, [tabindex]\")?.focus();\n }\n };\n\n const save = (next: T = draftRef.current) => {\n if (!editing || ended.current || pending) return;\n ended.current = true;\n if (next !== draftRef.current) setDraft(next);\n if (isEqual(next, value)) {\n setOpen(false);\n return;\n }\n let result: unknown;\n try {\n result = onCommit(next);\n } catch (caught) {\n fail(caught);\n return;\n }\n const promise = asPromise(result);\n if (!promise) {\n setOpen(false);\n return;\n }\n setPending(true);\n setFailure(null);\n promise.then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n setOpen(false);\n },\n (caught: unknown) => {\n if (!mounted.current) return;\n setPending(false);\n fail(caught);\n },\n );\n };\n\n const cancel = () => {\n // A request that has left cannot be called back; Escape waits for it.\n if (!editing || pending) return;\n ended.current = true;\n setOpen(false);\n };\n\n const change = (next: T) => {\n if (pending) return;\n setDraft(next);\n // Now, not after the render: a blur check scheduled before that render reads it.\n draftRef.current = next;\n // The failure described the draft that was sent; once edited it describes nothing\n // on screen, and the next save (or blur) is a fresh attempt.\n setFailure(null);\n };\n\n const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.defaultPrevented) return;\n if (e.key === \"Escape\") {\n // Ours, not the dialog's or the popover's around the field: claimed on the kit's\n // Escape stack (Wave 2, DUP-1), and stopped for anything else listening.\n claimEscape(e);\n e.stopPropagation();\n cancel();\n return;\n }\n // Not mid-composition: Enter there picks the IME's candidate (Japanese, Chinese) —\n // and Safari reports the Enter that ended one as keyCode 229 (`isImeEnter`).\n if (e.key === \"Enter\" && !isImeEnter(e) && !e.shiftKey) {\n e.preventDefault();\n save();\n }\n };\n\n // Leaving the editor saves. Deferred one task, and cancelled by any focus that lands\n // back inside: React reports focus moves through PORTALS to the component tree, so a\n // calculator popover or a phone keypad the editor opened counts as inside, which a\n // DOM `contains` check would get wrong. A failure on screen holds the editor: the\n // user is looking at why, and moving away is not a retry.\n const onBlur = (e: FocusEvent<HTMLDivElement>) => {\n if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;\n if (blurTimer.current) clearTimeout(blurTimer.current);\n blurTimer.current = setTimeout(() => {\n blurTimer.current = null;\n if (!mounted.current || failed) return;\n save();\n }, 0);\n };\n const onFocus = () => {\n if (blurTimer.current) clearTimeout(blurTimer.current);\n blurTimer.current = null;\n };\n\n const private_ = redact ? { \"data-private\": true } : undefined;\n const shown = display ? display(value) : value === null || value === undefined ? \"\" : String(value);\n const content = hasContent(shown) ? (\n shown\n ) : (\n <span className=\"text-muted\">{placeholder ?? labels.empty}</span>\n );\n const alignClass = align === \"end\" ? \"text-end\" : \"text-start\";\n\n let body: ReactNode;\n if (readOnly) {\n body = (\n <span {...private_} className={cn(\"block min-w-0\", VALUE_TRUNCATE, alignClass)}>\n {content}\n </span>\n );\n } else if (editing) {\n const describedBy = failed ? errorId : undefined;\n const editorProps: InlineEditorProps<T> = {\n value: draft,\n onChange: change,\n commit: save,\n cancel,\n pending,\n label,\n invalid: failed,\n describedBy,\n inputProps: {\n \"aria-label\": label,\n \"aria-invalid\": failed || undefined,\n \"aria-describedby\": describedBy,\n readOnly: pending,\n },\n };\n body = (\n // A wrapper for the keys and the focus, not a control: the keys belong to the\n // field inside it, which is what has focus.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- see above\n <div\n ref={editorRef}\n {...private_}\n onKeyDown={onKeyDown}\n onBlur={onBlur}\n onFocus={onFocus}\n aria-busy={pending || undefined}\n className=\"min-w-0\"\n >\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <div className=\"min-w-0 flex-1\">\n {editor ? (\n <RenderEditor render={editor} {...editorProps} />\n ) : (\n <Input\n {...inputProps}\n {...editorProps.inputProps}\n value={String(draft ?? \"\")}\n onChange={(e) => change(e.target.value as unknown as T)}\n className={cn(\"px-2 py-1\", alignClass, inputProps?.className)}\n />\n )}\n </div>\n {/* Decorative: `aria-busy` on the wrapper says it. */}\n {pending && <Spinner label={null} className=\"size-4 shrink-0\" />}\n </div>\n {failed && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {failure}\n </p>\n )}\n </div>\n );\n } else if (locked) {\n // Button's `disabledReason` path, spelled out for a button that must look like\n // the value rather than like a Button: focusable, `aria-disabled`, every\n // activation swallowed, the reason in the kit Tooltip and in a `hidden` copy that\n // describes it. The button in a FRAGMENT so the Tooltip does not add the bubble as\n // a second description of the same sentence.\n body = (\n <Tooltip label={disabledReason} className=\"flex w-full min-w-0\">\n <>\n <button\n ref={displayRef}\n type=\"button\"\n {...private_}\n aria-disabled\n aria-describedby={reasonId}\n onClick={(e) => e.preventDefault()}\n className={cn(\n \"block w-full min-w-0 cursor-not-allowed rounded-sm focus:outline-none\",\n VALUE_TRUNCATE,\n FOCUS_RING,\n alignClass,\n )}\n >\n {content}\n </button>\n <span id={reasonId} hidden>\n {disabledReason}\n </span>\n </>\n </Tooltip>\n );\n } else {\n body = (\n // Lazy: a table of these mounts no bubble until one is hovered or focused, and\n // the closed bubble's text is in no ancestor's accessible name.\n <Tooltip label={labels.edit(label)} lazy className=\"flex w-full min-w-0\">\n <button\n ref={displayRef}\n type=\"button\"\n {...private_}\n onClick={() => setOpen(true)}\n className={cn(\n \"block w-full min-w-0 rounded-sm underline-offset-2 hover:underline focus:outline-none\",\n VALUE_TRUNCATE,\n FOCUS_RING,\n alignClass,\n )}\n >\n {content}\n </button>\n </Tooltip>\n );\n }\n\n return (\n <div className={cn(\"min-w-0\", className)} data-editing={editing || undefined}>\n {body}\n {/* Always mounted: a live region that mounts with its message is usually missed. */}\n <span {...alert.regionProps} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/inline-edit-field.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { FocusEvent, InputHTMLAttributes, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { hasContent } from \"../lib/presence\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { claimEscape } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { Input } from \"./input\";\nimport { Spinner } from \"./spinner\";\nimport { FIELD_ERROR_CLASS } from \"./field-parts\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { Tooltip } from \"./tooltip\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { DEFAULT_INLINE_EDIT_LABELS } from \"./inline-edit-field-labels\";\n\n/**\n * Every string {@link InlineEditField} says on its own behalf — the `inlineEdit`\n * namespace of `<UiKitProvider labels>`, overridable per instance through `labels`.\n */\nexport interface InlineEditLabels {\n /** The display button's tooltip and description — what pressing it does. Receives\n * the field's `label` (\"Budget name\" → \"Edit Budget name\"). */\n edit: (label: string) => string;\n /** Shown (and spoken) when `onCommit` rejects and no `formatError` says better. */\n failed: string;\n /** What the display shows for an empty value, when no `placeholder` is given. */\n empty: string;\n}\n\n// Defined in ./inline-edit-field-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_INLINE_EDIT_LABELS };\n\n/** What an `editor` render prop is handed: the draft and the means to end the edit. */\nexport interface InlineEditorProps<T> {\n /** The draft — seeded from `value` each time the editor opens. */\n value: T;\n /** Replace the draft (every keystroke). */\n onChange: (next: T) => void;\n /**\n * End the edit and save. Without an argument it saves the draft; WITH one it saves\n * that — for an editor whose own commit hands over a resolved value (`NumberInput`'s\n * `onCommit` gives \"1050\" for a typed \"1100-50\"). Called twice (the editor's own\n * Enter, then the field's), the second call does nothing.\n */\n commit: (next?: T) => void;\n /** End the edit and drop the draft — what Escape does. */\n cancel: () => void;\n /** The save is in flight: make the control read-only (not disabled — it has focus). */\n pending: boolean;\n /** The field's `label`, for the control's accessible name. */\n label: string;\n /** The last save failed and the draft has not been touched since. */\n invalid: boolean;\n /** The id of the failure message while there is one, for `aria-describedby`. */\n describedBy: string | undefined;\n /** The four above as attributes — spread them on a kit field that takes the DOM\n * spelling: `<AmountInput {...p.inputProps} … />`. */\n inputProps: {\n \"aria-label\": string;\n \"aria-invalid\": true | undefined;\n \"aria-describedby\": string | undefined;\n readOnly: boolean;\n };\n}\n\ninterface InlineEditFieldBaseProps<T> {\n /** The saved value — what the display shows, and what the editor opens on. */\n value: T;\n /**\n * Save `next`. Return a promise and the field waits: the editor stays open and\n * read-only with a spinner until it settles, closes when it resolves, and STAYS OPEN\n * with the failure under it when it rejects (or throws) — the typed value is kept for\n * a retry. Not called when the draft equals `value` (see `isEqual`): opening the\n * editor and leaving it unchanged is not a write.\n */\n onCommit: (next: T) => void | Promise<unknown>;\n /** What the value is (\"Budget name\", \"Assigned\") — the editor's accessible name and\n * the subject of the display's \"Edit …\" description. Not drawn: inline, the row or\n * the column header is the visible label. */\n label: string;\n /** The value as the display shows it — a formatted amount, a date. Default: the\n * value as text. */\n display?: (value: T) => ReactNode;\n /** Whether the draft is unchanged, so leaving the editor is not a write. Default\n * `Object.is`. keksdose compares assigned amounts as NUMBERS — \"1100\" is the\n * server's \"1100.0000\". */\n isEqual?: (a: T, b: T) => boolean;\n /** What the display shows for an empty value (`\"\"`, `null`, an empty `display`).\n * Default `labels.empty`. */\n placeholder?: ReactNode;\n /** `\"end\"` for a figure in a right-aligned money column. Default `\"start\"`. */\n align?: \"start\" | \"end\";\n /** Look, do not touch — a guest's view: the value as plain text, no button. */\n readOnly?: boolean;\n /**\n * Why it cannot be edited now — the write lock's sentence, a closed period. The\n * value stays a BUTTON (focusable, `aria-disabled`) with the reason in the kit\n * Tooltip and its description; it opens no editor. An editor that is open when the\n * reason arrives closes, its draft dropped: a field you can type into must be one\n * whose value can be saved (keksdose dev#496).\n */\n disabledReason?: ReactNode;\n /** This field COMMITS — under a locked {@link WriteLockProvider} it takes the\n * `disabledReason` path with the lock's reason (which wins over its own). No\n * provider, or an unlocked one: no effect. */\n commit?: CommitScope;\n /** The value is the user's own data: `data-private` on the display and the editor,\n * so the app's demo-mode blur covers both. */\n redact?: boolean;\n /** Turns a rejection into words. Default `labels.failed`. */\n formatError?: (error: unknown) => ReactNode;\n /** Controlled open state — a row's swipe action that opens the editor from outside\n * (keksdose's assigned cell). Left out, the field keeps its own. */\n editing?: boolean;\n /** Called with the next open state: on open, on a save that resolved, on cancel. */\n onEditingChange?: (editing: boolean) => void;\n /** Attributes for the DEFAULT text editor's `<input>` — `maxLength`, `inputMode`,\n * `autoComplete`. Ignored with `editor`. */\n inputProps?: Omit<InputHTMLAttributes<HTMLInputElement>, \"value\" | \"defaultValue\" | \"onChange\" | \"type\">;\n /** Classes for the root. */\n className?: string;\n labels?: LabelOverride<InlineEditLabels>;\n}\n\ntype EditorRender<T> = (props: InlineEditorProps<T>) => ReactNode;\n\n/**\n * The editor. Optional for a string value — the kit's `Input`, compact — and\n * REQUIRED for anything else, since only the caller knows how to edit a number or an\n * amount. Render a kit field from the props it is handed:\n *\n * ```tsx\n * editor={(p) => (\n * <NumberInput calculator={false} aria-label={p.label} value={p.value} onChange={p.onChange}\n * onCommit={p.commit} aria-invalid={p.invalid || undefined} aria-describedby={p.describedBy} />\n * )}\n * ```\n *\n * The field itself handles Enter (save), Escape (cancel) and focus leaving the editor\n * (save) around whatever is rendered — a key the editor has already handled\n * (`preventDefault`, as a listbox does for its Enter) is left to it. Focus counts as\n * \"inside\" across a portal, so a picker or a phone keypad the editor opens does not\n * end the edit.\n */\nexport type InlineEditFieldProps<T = string> = InlineEditFieldBaseProps<T> &\n ([T] extends [string] ? { editor?: EditorRender<T> } : { editor: EditorRender<T> });\n\n/** Calls the `editor` render prop as a component of its own: the props it is handed\n * close over the field's refs, and a render prop called in the field's own render\n * would read like a ref read during render. Module-level, so the type is stable and\n * the editor never remounts. */\nfunction RenderEditor<T>({ render, ...props }: InlineEditorProps<T> & { render: EditorRender<T> }) {\n return render(props);\n}\n\n/**\n * The value at Large and Extra large (0.32.1, keksdose's 0.32 report;\n * docs/text-size-harmonization.md §4 \"nothing truncates\"): the display button, the\n * locked one and the read-only text wrap instead of ending in \"…\", and break a word too\n * long for the cell, so an IBAN or an e-mail address shows to its last character. The\n * value IS the button's accessible name and the thing being edited: a reader who needs\n * big type cannot recover a cut-off end by squinting. A `large:` class, so a table of\n * these keeps its one-line cells at Normal.\n *\n * 0.33 (§10.17): the kit's one `truncate-until-large` (tokens.css) in place of this\n * file's own `truncate` + `large:whitespace-normal large:break-words`. The utility\n * breaks with `overflow-wrap: anywhere`, which lowers the value's min-content width: in\n * an auto-sized table column (keksdose's budget and invoice lines) give the column a\n * width, or an amount can be squeezed to a digit per line.\n */\nconst VALUE_TRUNCATE = \"truncate-until-large\";\n\n/** `onCommit`'s result, if it is a promise. */\nfunction asPromise(result: unknown): Promise<unknown> | null {\n return result && typeof (result as Promise<unknown>).then === \"function\" ? (result as Promise<unknown>) : null;\n}\n\n/**\n * A value that edits in place: a quiet button showing the value; click it (or Enter\n * on it) and it becomes a field; Enter or leaving the field saves, Escape cancels\n * (keksdose K9).\n *\n * keksdose has three of them by hand — the budget's assigned cell (budget-cells.tsx),\n * the invoice line's value cells (invoice-lines-table.tsx `EditableCell`) and the\n * budget rename (budgets-page.tsx, an `Input` + Save + Cancel with no Escape) — and\n * each had to learn the same rules:\n *\n * - **The display's accessible name is the VALUE.** A cell named \"Edit\" reads a table\n * of \"edit, edit, edit\"; the action is the description instead (\"Edit Assigned\"),\n * in the kit Tooltip on hover and focus.\n * - **An unchanged draft is not a write.** Opening the editor and leaving it was a PUT\n * in keksdose until `isEqual` compared numbers, and on a sealed budget an outbox row\n * for a change nobody made.\n * - **A failed save keeps the editor.** The typed value is the user's work; the\n * failure is shown under it (attached with `aria-describedby`, and spoken), and the\n * next keystroke clears it. Escape still drops the draft.\n * - **Locked means a disabled BUTTON, not a disabled field** (keksdose dev#496): at\n * rest a real row shows this button, so a locked page shows the same, with the\n * reason — never a field that takes keys it cannot save.\n *\n * Inside a {@link DataTable} cell it needs nothing extra: the table already leaves a\n * click or a key on a control of the cell to that control. Focus returns to the value\n * after Enter or Escape; after a save that came from focus leaving, it stays where the\n * user put it.\n *\n * ```tsx\n * <InlineEditField label={t(\"budgets.name\")} value={budget.name}\n * onCommit={(name) => rename.mutateAsync({ id: budget.id, name })} inputProps={{ maxLength: 120 }} />\n * ```\n */\nexport function InlineEditField<T = string>(props: InlineEditFieldProps<T>) {\n const {\n value,\n onCommit,\n label,\n display,\n isEqual = Object.is,\n placeholder,\n align = \"start\",\n readOnly = false,\n disabledReason: ownDisabledReason,\n commit,\n redact = false,\n formatError,\n editing: editingProp,\n onEditingChange,\n inputProps,\n className,\n labels: labelsProp,\n } = props;\n const editor = props.editor as EditorRender<T> | undefined;\n const labels = useKitLabels(\"inlineEdit\", DEFAULT_INLINE_EDIT_LABELS, labelsProp);\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n\n const [openState, setOpenState] = useState(false);\n const open = editingProp ?? openState;\n // Locked (or read-only) wins over an open flag: a stale `editing` must never bring a\n // live commit-on-blur field back on a page that cannot save it.\n const editing = open && !locked && !readOnly;\n const [draft, setDraft] = useState<T>(value);\n const [pending, setPending] = useState(false);\n const [failure, setFailure] = useState<ReactNode>(null);\n const failed = hasContent(failure);\n\n const errorId = useId();\n const reasonId = useId();\n const displayRef = useRef<HTMLButtonElement>(null);\n const editorRef = useRef<HTMLDivElement>(null);\n const alert = useAnnounce({ politeness: \"assertive\" });\n\n // One edit ends once: Enter in a NumberInput commits through its own `onCommit` and\n // then reaches the field's Enter; a save that closes the editor unmounts the focused\n // control, which some browsers answer with a blur. The second ending is a no-op.\n const ended = useRef(false);\n const blurTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const mounted = useRef(true);\n // The latest draft for the deferred blur check, which runs after a render.\n const draftRef = useRef(draft);\n useEffect(() => {\n draftRef.current = draft;\n });\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n if (blurTimer.current) clearTimeout(blurTimer.current);\n };\n }, []);\n\n // Seed the draft when the editor opens — from wherever the open came (the button, a\n // controlled `editing`). During render, like DangerConfirm's wipe, so no frame shows\n // the editor holding the previous edit's text.\n const [wasEditing, setWasEditing] = useState(editing);\n if (wasEditing !== editing) {\n setWasEditing(editing);\n if (editing) {\n setDraft(value);\n setFailure(null);\n setPending(false);\n }\n }\n // An open editor that a lock (or `readOnly`) took away is closed for good, not\n // merely hidden until the lock lifts.\n if (openState && !editing && editingProp === undefined) setOpenState(false);\n\n // Focus follows the edit: into the editor when it opens, back to the value when it\n // closes — but only if focus went down with the editor (Enter, Escape, a save that\n // resolved under it). A save that came from focus LEAVING keeps focus where it went.\n const prevEditing = useRef(editing);\n useEffect(() => {\n const changed = prevEditing.current !== editing;\n prevEditing.current = editing;\n if (!changed) return;\n if (editing) {\n // A fresh edit, which may end once. Here rather than in the seed above: a ref is\n // not written during render, and no event can end the edit before this runs.\n ended.current = false;\n const control = editorRef.current?.querySelector<HTMLElement>(\n \"input:not([type=hidden]), textarea, select, [contenteditable='true'], [tabindex]:not([tabindex='-1']), button\",\n );\n control?.focus();\n if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) control.select();\n return;\n }\n // However it closed — a save, Escape, a lock taking it away — a blur check still\n // queued from the editor has nothing left to save.\n ended.current = true;\n if (blurTimer.current) clearTimeout(blurTimer.current);\n blurTimer.current = null;\n const lost = document.activeElement === null || document.activeElement === document.body;\n if (lost) displayRef.current?.focus();\n }, [editing]);\n\n const setOpen = (next: boolean) => {\n if (editingProp === undefined) setOpenState(next);\n onEditingChange?.(next);\n };\n\n const fail = (caught: unknown) => {\n ended.current = false;\n const message = formatError ? formatError(caught) : labels.failed;\n setFailure(hasContent(message) ? message : labels.failed);\n alert.announce(typeof message === \"string\" && message !== \"\" ? message : labels.failed);\n // Focus back into the editor if the failure left it nowhere (a pending field that\n // the browser let go of); never pulled back from somewhere the user went.\n const active = document.activeElement;\n if (active === null || active === document.body) {\n editorRef.current?.querySelector<HTMLElement>(\"input, textarea, select, [tabindex]\")?.focus();\n }\n };\n\n const save = (next: T = draftRef.current) => {\n if (!editing || ended.current || pending) return;\n ended.current = true;\n if (next !== draftRef.current) setDraft(next);\n if (isEqual(next, value)) {\n setOpen(false);\n return;\n }\n let result: unknown;\n try {\n result = onCommit(next);\n } catch (caught) {\n fail(caught);\n return;\n }\n const promise = asPromise(result);\n if (!promise) {\n setOpen(false);\n return;\n }\n setPending(true);\n setFailure(null);\n promise.then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n setOpen(false);\n },\n (caught: unknown) => {\n if (!mounted.current) return;\n setPending(false);\n fail(caught);\n },\n );\n };\n\n const cancel = () => {\n // A request that has left cannot be called back; Escape waits for it.\n if (!editing || pending) return;\n ended.current = true;\n setOpen(false);\n };\n\n const change = (next: T) => {\n if (pending) return;\n setDraft(next);\n // Now, not after the render: a blur check scheduled before that render reads it.\n draftRef.current = next;\n // The failure described the draft that was sent; once edited it describes nothing\n // on screen, and the next save (or blur) is a fresh attempt.\n setFailure(null);\n };\n\n const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.defaultPrevented) return;\n if (e.key === \"Escape\") {\n // Ours, not the dialog's or the popover's around the field: claimed on the kit's\n // Escape stack (Wave 2, DUP-1), and stopped for anything else listening.\n claimEscape(e);\n e.stopPropagation();\n cancel();\n return;\n }\n // Not mid-composition: Enter there picks the IME's candidate (Japanese, Chinese) —\n // and Safari reports the Enter that ended one as keyCode 229 (`isImeEnter`).\n if (e.key === \"Enter\" && !isImeEnter(e) && !e.shiftKey) {\n e.preventDefault();\n save();\n }\n };\n\n // Leaving the editor saves. Deferred one task, and cancelled by any focus that lands\n // back inside: React reports focus moves through PORTALS to the component tree, so a\n // calculator popover or a phone keypad the editor opened counts as inside, which a\n // DOM `contains` check would get wrong. A failure on screen holds the editor: the\n // user is looking at why, and moving away is not a retry.\n const onBlur = (e: FocusEvent<HTMLDivElement>) => {\n if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;\n if (blurTimer.current) clearTimeout(blurTimer.current);\n blurTimer.current = setTimeout(() => {\n blurTimer.current = null;\n if (!mounted.current || failed) return;\n save();\n }, 0);\n };\n const onFocus = () => {\n if (blurTimer.current) clearTimeout(blurTimer.current);\n blurTimer.current = null;\n };\n\n const private_ = redact ? { \"data-private\": true } : undefined;\n const shown = display ? display(value) : value === null || value === undefined ? \"\" : String(value);\n const content = hasContent(shown) ? shown : <span className=\"text-muted\">{placeholder ?? labels.empty}</span>;\n const alignClass = align === \"end\" ? \"text-end\" : \"text-start\";\n\n let body: ReactNode;\n if (readOnly) {\n body = (\n <span {...private_} className={cn(\"block min-w-0\", VALUE_TRUNCATE, alignClass)}>\n {content}\n </span>\n );\n } else if (editing) {\n const describedBy = failed ? errorId : undefined;\n const editorProps: InlineEditorProps<T> = {\n value: draft,\n onChange: change,\n commit: save,\n cancel,\n pending,\n label,\n invalid: failed,\n describedBy,\n inputProps: {\n \"aria-label\": label,\n \"aria-invalid\": failed || undefined,\n \"aria-describedby\": describedBy,\n readOnly: pending,\n },\n };\n body = (\n // A wrapper for the keys and the focus, not a control: the keys belong to the\n // field inside it, which is what has focus.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- see above\n <div\n ref={editorRef}\n {...private_}\n onKeyDown={onKeyDown}\n onBlur={onBlur}\n onFocus={onFocus}\n aria-busy={pending || undefined}\n className=\"min-w-0\"\n >\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <div className=\"min-w-0 flex-1\">\n {editor ? (\n <RenderEditor render={editor} {...editorProps} />\n ) : (\n <Input\n {...inputProps}\n {...editorProps.inputProps}\n value={String(draft ?? \"\")}\n onChange={(e) => change(e.target.value as unknown as T)}\n className={cn(\"px-2 py-1\", alignClass, inputProps?.className)}\n />\n )}\n </div>\n {/* Decorative: `aria-busy` on the wrapper says it. */}\n {pending && <Spinner label={null} className=\"size-4 shrink-0\" />}\n </div>\n {failed && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {failure}\n </p>\n )}\n </div>\n );\n } else if (locked) {\n // Button's `disabledReason` path, spelled out for a button that must look like\n // the value rather than like a Button: focusable, `aria-disabled`, every\n // activation swallowed, the reason in the kit Tooltip and in a `hidden` copy that\n // describes it. The button in a FRAGMENT so the Tooltip does not add the bubble as\n // a second description of the same sentence.\n body = (\n <Tooltip label={disabledReason} className=\"flex w-full min-w-0\">\n <>\n <button\n ref={displayRef}\n type=\"button\"\n {...private_}\n aria-disabled\n aria-describedby={reasonId}\n onClick={(e) => e.preventDefault()}\n className={cn(\n \"block w-full min-w-0 cursor-not-allowed rounded-sm focus:outline-none\",\n VALUE_TRUNCATE,\n FOCUS_RING,\n alignClass,\n )}\n >\n {content}\n </button>\n <span id={reasonId} hidden>\n {disabledReason}\n </span>\n </>\n </Tooltip>\n );\n } else {\n body = (\n // Lazy: a table of these mounts no bubble until one is hovered or focused, and\n // the closed bubble's text is in no ancestor's accessible name.\n <Tooltip label={labels.edit(label)} lazy className=\"flex w-full min-w-0\">\n <button\n ref={displayRef}\n type=\"button\"\n {...private_}\n onClick={() => setOpen(true)}\n className={cn(\n \"block w-full min-w-0 rounded-sm underline-offset-2 hover:underline focus:outline-none\",\n VALUE_TRUNCATE,\n FOCUS_RING,\n alignClass,\n )}\n >\n {content}\n </button>\n </Tooltip>\n );\n }\n\n return (\n <div className={cn(\"min-w-0\", className)} data-editing={editing || undefined}>\n {body}\n {/* Always mounted: a live region that mounts with its message is usually missed. */}\n <span {...alert.regionProps} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -178,9 +178,7 @@ function InlineEntityCombobox(props) {
|
|
|
178
178
|
if (!q) {
|
|
179
179
|
if (value != null) onChange(clearValue);
|
|
180
180
|
} else {
|
|
181
|
-
const hits = options.filter(
|
|
182
|
-
(o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase()
|
|
183
|
-
);
|
|
181
|
+
const hits = options.filter((o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase());
|
|
184
182
|
const ids = new Set(hits.map((h) => h.value));
|
|
185
183
|
if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);
|
|
186
184
|
}
|
|
@@ -389,11 +387,7 @@ function InlineEntityCombobox(props) {
|
|
|
389
387
|
"aria-selected": false,
|
|
390
388
|
"aria-disabled": createLocked || void 0,
|
|
391
389
|
onClick: create,
|
|
392
|
-
className: cn(
|
|
393
|
-
SHEET_ROW_CLASS,
|
|
394
|
-
CREATE_ROW_CLASS,
|
|
395
|
-
createLocked && DISABLED_ROW_CLASS
|
|
396
|
-
),
|
|
390
|
+
className: cn(SHEET_ROW_CLASS, CREATE_ROW_CLASS, createLocked && DISABLED_ROW_CLASS),
|
|
397
391
|
children: createRowContent
|
|
398
392
|
}
|
|
399
393
|
) }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/inline-entity-combobox.tsx"],"sourcesContent":["// The id-keyed sibling of Combobox. Moved out of combobox.tsx as it was (MOD-9,\n// refactor audit 2026-10); combobox still exports `InlineEntityCombobox` and its props.\nimport { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown, Plus, X } from \"lucide-react\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { isQuietFocus } from \"../lib/quiet-focus\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GROUP_HEADING_STICKY,\n isOptionEnabled,\n offersCreate,\n stepEnabled,\n useActiveOptionScroll,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { matchesSearch, rankMatch, searchWords } from \"../lib/text-match\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { lockedKeyDown, rowClass, SuggestionList, usePrimaryPressOnly } from \"./combobox-suggestions\";\n\n/** {@link InlineEntityCombobox}'s create row over the shared row look: in the brand\n * colour {@link Combobox}'s own create row wears, with a leading \"+\" as the panel\n * pickers' has — an action at the foot of the list, not one more record. */\nconst CREATE_ROW_CLASS = \"flex items-center gap-2 font-medium text-brand\";\n\n/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}\n * for why the div's spellings of them are omitted. */\nexport interface InlineEntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\">,\n FieldHelpProps {\n /** Selected option id; `null` or `clearValue` when nothing is selected. */\n value: V | C | null;\n /** Required and unanswered — see {@link Combobox}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value — see {@link Combobox}'s `error`. */\n error?: ReactNode;\n /** Standing advice — see {@link Combobox}'s `hint` (keksdose K4). */\n hint?: ReactNode;\n /** A picked/typed option emits its id; emptying the field, or the \"×\", emits\n * `clearValue`. */\n onChange: (v: V | C) => void;\n /** What a clear emits — `null` by default, `\"\"` for a schema that wants an empty\n * string. See {@link EntityCombobox}'s `clearValue`. */\n clearValue?: C;\n options: ComboOption<V>[];\n /** The visible label — see {@link Combobox}'s `label` (a ReactNode since 0.34, API-9). */\n label?: ReactNode;\n id?: string;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`\n * below, open the list with it. A click-the-value editor needs it: the cell\n * the user clicked names the field the editor should land the caret in, and\n * without a passthrough this component's fixed prop list puts the `<input>`\n * out of a caller's reach entirely (Keksdose feedback live #218). */\n autoFocus?: boolean;\n /** Phone sheet only — the anchored list has the field itself to type in. Falls\n * back to `placeholder`, then to `combobox.search` from the {@link UiKitProvider}. */\n searchPlaceholder?: string;\n /** Phone sheet only: a full screen showing nothing has to say why. The anchored\n * list simply does not open. Default: `combobox.noResults` from the provider. */\n emptyLabel?: string;\n closeLabel?: string;\n /** The phone sheet's heading (and, as a string, its accessible name) for a field\n * whose label is drawn by someone else — a bound field's label;\n * `RhfInlineEntityCombobox` hands its label over here (0.24). Default: `label`.\n * See {@link Combobox}'s `sheetTitle`. */\n sheetTitle?: ReactNode;\n /** Offer a clear \"×\" in place of the chevron whenever something is selected.\n *\n * Emptying the text already clears (see {@link reconcile}), and on a desktop that\n * is the fast way. It is not a way at all on a PHONE: touching the field opens the\n * full-screen sheet, which covers the very input the text would have been deleted\n * from — so a field that had been answered could not be UNanswered by any gesture\n * the screen offered (Keksdose live #236, filed from a phone). The \"×\" is the one\n * affordance both shells share, and it clears without opening anything. */\n clearable?: boolean;\n clearLabel?: string;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a picker\n * that SAVES on change. keksdose G2: the statement review's account picker IS the\n * write (picking re-runs the duplicate check on the server), and on the read-only demo\n * the app wrapped it in a hand-made Tooltip over a native `disabled` — out of the tab\n * order, so the reason never reached a keyboard.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey), opens no list or sheet, offers no clear \"×\", and no pick, typed\n * label or clear reaches `onChange`; the reason is in the kit {@link Tooltip} and on\n * the input's `aria-describedby`. Wins over `disabled`. Use it CONTROLLED, as the\n * component always is: the shown label is `value`'s.\n */\n disabledReason?: ReactNode;\n /**\n * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * A picker inside a form with its own Save stays editable under the lock — leave this\n * off there and put `commit` on the Save. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * A last row that makes a new record from what was typed — keksdose G9, whose \"Create\n * cash account\" had to become a separate button because this picker had no such row.\n *\n * Shown while the typed query is non-empty and names no option exactly\n * (case-insensitive — an exact hit is a record the list already shows), worded\n * `createLabel(query)`, default `combobox.create`: \"Create “{query}”\". The arrow keys\n * reach it like an option; Enter or a click calls `onCreate(query)` and closes the\n * list. Making the record, and then passing its id as `value`, is the caller's: the\n * field shows the old selection until it does.\n *\n * The same rule as {@link EntityCombobox}'s `onCreate`, so the family agrees. It is a\n * row of the LIST, not an option: it is never `aria-selected`, never matched by a\n * typed label on blur, and never sorted among the records the way a sentinel option\n * would be (the reason keksdose gave for the separate button).\n */\n onCreate?: (query: string) => void;\n /** The create row's words for a query. Default: `combobox.create` from the\n * {@link UiKitProvider}, else English `Create “{query}”`. */\n createLabel?: (query: string) => string;\n /**\n * Offer the create row with NOTHING typed too, worded as this — keksdose's \"Create cash\n * account\", which mints an account under a default name and so needs no query.\n * `onCreate` then receives `\"\"`, and the caller supplies the name. Left out, the row\n * needs a query.\n *\n * One string rather than a switch: a row with no query has nothing to quote, so the\n * kit cannot word it — \"Create “”\" says nothing — and naming it is what turns it on.\n * With both, one handler serves both rows:\n *\n * ```tsx\n * onCreate={(name) => createCash(name || t(\"cash_account_default_name\"))}\n * createEmptyLabel={t(\"create_cash_account\")}\n * ```\n */\n createEmptyLabel?: string;\n /**\n * The create row WRITES while picking does not — keksdose dev#496: on the read-only\n * demo choosing the cash account is draft state and stays live, minting one is a\n * write. Under a locked {@link WriteLockProvider} the row stays in the list, dimmed and\n * passed over by the arrows, with the lock's reason as its second line (a hover is the\n * one explanation a phone cannot show), and `onCreate` is not called. Not needed with\n * `commit`: a locked field opens no list at all.\n */\n createCommit?: CommitScope;\n /**\n * The `<input>` — the focusable control, the one a reader types in — as a React 19\n * ref prop (0.24, kastlan). Its account picker is this component inside an\n * `RhfField` render, and react-hook-form's focus-on-error calls `focus()` on whatever\n * `field.ref` is handed: with no ref there was nothing to hand it, so a line-item form\n * whose account was missing failed its submit and left the caret where it was. Pass\n * `field.ref` here, or use `RhfInlineEntityCombobox`, which does. The wrapper is not\n * reachable this way — focus and selection are the input's. See {@link Combobox}'s `ref`.\n */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input\n * with the shared field styling, chevron and inline as-you-type filtering) but\n * the value is an option id, not free text. Built so an entity picker can sit\n * next to a free-text combobox without any visible difference — a button-trigger\n * panel picker ({@link EntityCombobox}) never matches an input field exactly.\n *\n * Text-vs-value reconciliation: the input's text is transient. Picking a row or\n * typing an exact (unique) label commits that option; emptying the text commits\n * a clear; anything else reverts to the selected option's label on blur/Escape.\n *\n * **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}\n * beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto\n * this component and must not lose its sheet). The sheet carries its OWN query state\n * rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a\n * sheet that emptied it to show the full list would read as \"the user cleared the\n * field\" the moment it closed.\n */\nexport function InlineEntityCombobox<V extends string | number, C extends ComboClearValue = null>(\n props: InlineEntityComboboxProps<V, C>,\n) {\n const {\n value: rawValue,\n onChange,\n clearValue = null as C,\n options,\n label,\n id,\n placeholder,\n className,\n disabled,\n autoFocus,\n searchPlaceholder,\n emptyLabel,\n closeLabel,\n sheetTitle,\n clearable,\n clearLabel,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onCreate,\n createLabel,\n createEmptyLabel,\n createCommit,\n invalid,\n error,\n hint,\n help,\n ref,\n \"aria-label\": ariaLabel,\n // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s\n // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and\n // on the wrapper div the hint and required state described nothing.\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n // The clear value reads as \"nothing selected\", whichever one the caller picked —\n // so from here down `value` is the id or `null`, as it always was.\n const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // See the twin above on why these hang off the generated id.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const isPhone = usePhoneLayout();\n // See the twin above: the phone sheet owns its own Back entry (live #309), and the\n // Escape layer is this field's own.\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });\n const primaryOnly = usePrimaryPressOnly();\n // The field took the tour's quiet focus (§11.8) and has not opened since.\n const quietRef = useRef(false);\n const [active, setActive] = useState(-1);\n // null = not editing → the input shows the selected option's label.\n const [text, setText] = useState<string | null>(null);\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — see {@link SuggestionList}.\n const fieldRef = useRef<HTMLDivElement>(null);\n // The sheet's search box. Starts empty on every open, so a field that already\n // holds a value still offers the whole list — the shape a native <select> has\n // on a phone, and what the anchored panel gets from `query` below.\n const [sheetQuery, setSheetQuery] = useState(\"\");\n // Props first, provider second — see `EntityCombobox`. `emptyLabel` used to render\n // nothing when omitted, which left a phone user staring at a blank full-screen\n // sheet; now an omitted one says \"No results\" in the provider's language.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n // The lock (see `lockedKeyDown` above). A lock arriving mid-edit closes the list and\n // drops the loose text, adjusted while rendering: nothing typed before it may be\n // judged by `reconcile` after it.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n if (locked && (open || text !== null)) {\n setOpen(false);\n setText(null);\n }\n // The create row's own lock (`createCommit`): the list stays live, the row does not.\n const createReason = useCommitReason(createCommit, undefined);\n const createLocked = hasContent(createReason);\n\n const selected = useMemo(\n () => (value == null ? null : (options.find((o) => o.value === value) ?? null)),\n [options, value],\n );\n const shown = text ?? selected?.label ?? \"\";\n // Nothing selected has nothing to clear, and the chevron comes back — the field\n // keeps exactly one trailing control, so the \"×\" never crowds the value it sits on.\n const showClear = Boolean(clearable && value != null && !inert);\n\n // Focusing select-alls the current label; filtering only kicks in once the\n // text actually differs from it, so an already-filled field still opens on\n // the FULL list instead of a single-row \"filter\" of its own value.\n const typedQuery = text !== null && text !== (selected?.label ?? \"\") ? text.trim() : \"\";\n const query = isPhone ? sheetQuery.trim() : typedQuery;\n // Folded as every kit list folds (DUP-5): accents off and the locale's case, so\n // \"zurich\" finds Zürich and, in Turkish, \"ı\" finds \"IRMAK\".\n const locale = useKitLocale();\n const matches = useMemo(() => {\n let ranked = options;\n if (query) {\n // Every word of the query, in the label or the sublabel (0.51.4).\n const words = searchWords(query, locale);\n const hit = (o: ComboOption<V>) => matchesSearch(words, [o.label, o.sublabel], locale);\n // The label's whole name or its start first (`rankMatch` 0 and 1), then a match\n // anywhere in the label or the sublabel. Not the group (0.40, kk run 79): with\n // first-letter groups one typed letter matched its whole group.\n const leads = (o: ComboOption<V>) => {\n const rank = rankMatch(o.label, query, { locale });\n return rank === 0 || rank === 1;\n };\n const starts = options.filter(leads);\n const rest = options.filter((o) => !leads(o) && hit(o));\n ranked = [...starts, ...rest];\n }\n if (!ranked.some((o) => o.group)) return ranked;\n // Keep each group contiguous so the headings below appear once instead of\n // re-appearing every time the ranking interleaves two groups — while preserving\n // \"best match first\": a Map keeps insertion order, so groups come out ordered by\n // their best-ranked member and members keep their rank order inside the group.\n const blocks = new Map<string, ComboOption<V>[]>();\n for (const o of ranked) {\n const key = o.group ?? \"\";\n if (!blocks.has(key)) blocks.set(key, []);\n blocks.get(key)!.push(o);\n }\n return [...blocks.values()].flat();\n }, [options, query, locale]);\n\n // The create row (`onCreate`), on the query as typed — not lowercased: it becomes a\n // name. Compared against EVERY option, not `matches`: an exact hit anywhere is a\n // record that exists, disabled or not. Restated from `text` rather than derived from\n // `typedQuery`: this value is handed to the caller's `onCreate`, and the React\n // Compiler cannot then keep `matches` memoised on anything it was derived from.\n const createQuery = isPhone\n ? sheetQuery.trim()\n : text !== null && text !== (selected?.label ?? \"\")\n ? text.trim()\n : \"\";\n const showCreate = offersCreate(\n onCreate,\n createQuery,\n options.map((o) => o.label),\n createEmptyLabel,\n );\n const createText = createQuery ? labels.create(createQuery) : createEmptyLabel;\n // Every row the keyboard can land on: the options, then the create row — passed over\n // like a disabled option while a write lock holds it.\n const rows: readonly { disabled?: boolean }[] = showCreate\n ? [...matches, { disabled: createLocked }]\n : matches;\n const createIndex = matches.length;\n\n // Pointer-device only: the phone's rows live in a {@link PickerSheet} whose own\n // search box holds focus, so the field behind it must not claim to be pointing at\n // one of them.\n // A disabled row is never the keyboard's, even if the list changed under it.\n const activeId = !isPhone && isOptionEnabled(rows[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n setSheetQuery(\"\");\n };\n // See the twin above: a layer while the list SHOWS, and Escape puts the label back.\n const showing = !isPhone && open && !inert && (matches.length > 0 || showCreate);\n useDismissLayer(showing, () => {\n setText(null);\n close();\n });\n const take = (o: ComboOption<V>) => {\n // A `disabled` option is listed, never taken — by any path; nor is anything while\n // the field is locked.\n if (o.disabled || locked) return;\n if (o.value !== value) onChange(o.value);\n setText(null);\n close();\n };\n /** Turn loose text into a decision: empty clears, an exact label match that names\n * ONE entity commits, anything else reverts to the selected label.\n *\n * Unique by VALUE, not by row. An option may deliberately appear twice — Keksdose\n * repeats recently-used categories in a \"Recent\" group at the top (live #203) — and\n * counting rows made every such option uncommittable: two hits, so nothing fired and\n * the field reverted to whatever was selected before. Typing a category you had just\n * used, then tabbing away, silently discarded it, and the more categories you used\n * the more of them stopped working. Two rows naming the same id are not an ambiguity;\n * two ids sharing a label are. */\n const create = () => {\n if (!onCreate || locked || createLocked) return;\n onCreate(createQuery);\n setText(null);\n close();\n };\n const reconcile = () => {\n // Locked, loose text is dropped rather than judged: nothing reaches `onChange`.\n if (text !== null && !locked) {\n const q = text.trim();\n if (!q) {\n if (value != null) onChange(clearValue);\n } else {\n // A disabled option's label typed out in full is not a way round `disabled`:\n // it is not a hit, so the text reverts like any other non-answer.\n const hits = options.filter(\n (o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase(),\n );\n const ids = new Set(hits.map((h) => h.value));\n if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);\n }\n }\n setText(null);\n close();\n };\n\n // The create row's inside, one for both shells: a \"+\" (it is an action, not a record),\n // the words, and — while a write lock holds it — the lock's reason as a second line.\n // Both lines wrap from Large up (text size §10.17, round 0.34 T1): the words hold what\n // the reader typed, which an ellipsis would cut.\n const createRowContent = (\n <>\n <Plus aria-hidden className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large\">{createText}</span>\n {createLocked && (\n <span className=\"block truncate-until-large text-xs font-normal text-placeholder\">\n {createReason}\n </span>\n )}\n </span>\n </>\n );\n // While locked the field sits in the reason's Tooltip — see Combobox.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,\n // description, invalid or required state: those belong on the <input> below,\n // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {/* A real <label for> — see {@link Combobox}; with a \"?\", in a row after the field. */}\n {label !== undefined && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel htmlFor={fieldId} className={inert ? \"opacity-50\" : undefined}>\n {label}\n </ComboboxFieldLabel>\n )}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n /* Inner wrapper for chevron centering — same reasoning as Combobox above.\n It is also what the portalled list anchors to.\n Dimmed as a whole when disabled (or locked), the way EntityCombobox dims its\n trigger. FIELD_BASE's grey alone left a disabled picker looking like a\n filled-in one beside the pickers that do dim (the label is not the input's\n `peer`, so it is dimmed by hand above). */\n <div ref={fieldRef} className={cn(\"relative\", inert && \"cursor-not-allowed opacity-50\")}>\n <input\n ref={ref}\n id={fieldId}\n value={shown}\n placeholder={placeholder}\n // The caller's name, over the <label for> above. Just the label, never\n // \"label: value\" the way a trigger button has to compose it: an input\n // already exposes its value separately.\n aria-label={ariaLabel}\n role=\"combobox\"\n aria-expanded={open}\n // Required by the role, and the half that was missing: the field said it\n // was expanded and never said what it had expanded (ESLint's\n // `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n aria-required={ariaRequired}\n autoComplete=\"off\"\n // Locked: focusable, `readOnly`, `aria-disabled` — see `lockedKeyDown`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n // `inputMode=\"none\"` rather than readOnly, for the same reason Combobox\n // above gives: the sheet carries the keyboard, and a readOnly field would\n // take FIELD_BASE's settled look on a field that is perfectly editable.\n inputMode={isPhone ? \"none\" : undefined}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={(e) => {\n // See {@link usePrimaryPressOnly}: a back/forward button lands here on\n // its way to navigating, and neither the list nor the select-all is\n // anything it asked for (live #309 rework). A locked field takes focus to\n // say why, and opens nothing.\n if (primaryOnly.fromAuxButton() || locked) return;\n setText(shown);\n // The tour's focus on arrival (§11.8) opens nothing: the list would cover\n // its card (0.50.1). A click, ArrowDown or typing opens it.\n if (isQuietFocus(e.currentTarget)) {\n quietRef.current = true;\n return;\n }\n e.currentTarget.select();\n setOpen(true);\n // The sheet has its own input; the field behind it must not also pull up\n // the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n // After a quiet focus, the person's click on the already-focused field\n // fires no `focus`: the click is their \"open\" instead.\n onClick={() => {\n if (!quietRef.current || locked) return;\n quietRef.current = false;\n setOpen(true);\n }}\n onBlur={() => {\n quietRef.current = false;\n // On a phone the blur is the SHEET taking focus, not the user leaving the\n // field — reconciling there would close the sheet the instant it opened.\n if (!isPhone) reconcile();\n }}\n onChange={(e) => {\n if (locked) return;\n setText(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n if (locked) return lockedKeyDown(e);\n // Disabled rows are passed over; Up from \"nothing highlighted\" lands on\n // the first takeable row, as it always landed on row 0. The create row is\n // the last row the arrows reach.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => stepEnabled(rows, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => (i < 0 ? stepEnabled(rows, -1, 1) : stepEnabled(rows, i, -1)));\n } else if (e.key === \"Enter\") {\n // An IME's Enter takes nothing, and judges no loose text (BUG-9).\n if (isImeEnter(e)) return;\n if (open && isOptionEnabled(rows[active])) {\n e.preventDefault();\n if (active === createIndex && showCreate) create();\n else take(matches[active]);\n } else {\n reconcile();\n }\n } else if (e.key === \"Escape\") {\n // A showing list takes the key and stops it — see the twin in\n // {@link Combobox} (round 0.34 BUG-1).\n if (showing && takeEscape(e)) return;\n setText(null);\n close();\n } else if (e.key === \"Tab\") {\n // Focus is in THIS input and stays there, so the browser's own Tab is\n // left alone; all that is needed is that the list stop covering what\n // the user is tabbing to. `reconcile` rather than `close`, because\n // leaving the field is exactly when loose text has to be judged.\n reconcile();\n }\n // Home/End stay with the caret — see the note in {@link Combobox}: this\n // field is editable, and its text is what `reconcile` judges.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pe-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n {showClear ? (\n <button\n type=\"button\"\n // Out of the tab order, like the clear on `EntityCombobox`: the keyboard\n // already clears this field by selecting its text and deleting, and a\n // second stop between every picker and the next field is a worse trade\n // than the one gesture it saves.\n tabIndex={-1}\n aria-label={labels.clear}\n // preventDefault, exactly as the chevron does: without it the press\n // focuses the input, which on a phone opens the sheet over the field the\n // press was clearing.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n setText(null);\n close();\n if (!locked) onChange(clearValue);\n }}\n className={cn(\n \"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder\",\n \"hover:text-secondary\",\n )}\n >\n <X aria-hidden className=\"size-4\" />\n </button>\n ) : (\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input — and\n // not at all on a disabled or locked field, which the input's own\n // `disabled`/`readOnly` does not stop from here.\n e.preventDefault();\n if (!inert) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\",\n // The box above does the dimming; dimming here too would halve it again.\n !inert && \"cursor-pointer\",\n )}\n />\n )}\n </div>,\n )}\n </EndHintRow>\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, inert && \"opacity-50\")}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !inert}\n // Closing without choosing keeps the value: `text` was never emptied, so\n // reconcile has nothing to undo — it just puts the label back.\n onClose={reconcile}\n title={sheetTitle ?? label}\n query={sheetQuery}\n onQueryChange={(v) => {\n setSheetQuery(v);\n setActive(-1);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder ?? labels.search}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {matches.map((o, i) => (\n // Keyed by group AND value, like combobox-core.tsx: an option may\n // deliberately appear twice (see `reconcile`), and a bare value key\n // would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className={cn(\n GROUP_HEADING_STICKY,\n \"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n onClick={() => take(o)}\n className={cn(\n SHEET_ROW_CLASS,\n o.value === value && \"font-medium\",\n o.disabled && DISABLED_ROW_CLASS,\n )}\n >\n {o.label}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n // An action, never chosen — see `onCreate`.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n onClick={create}\n className={cn(\n SHEET_ROW_CLASS,\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n {/* \"Nothing matched\" only when there is nothing to do either: with a create\n row the list is not empty, it is offering to fill itself. */}\n {matches.length === 0 && !showCreate && (\n <li className=\"px-4 py-3 text-sm text-muted\">{labels.noResults}</li>\n )}\n </ul>\n </PickerSheet>\n )}\n {showing && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {matches.map((o, i) => (\n // A group heading is emitted at each group boundary rather than repeating\n // the group on every row (feedback #136). `matches` is group-contiguous,\n // so comparing with the previous row is enough. Fragment key sits here;\n // the heading and the option carry their own list semantics — keyed by\n // group AND value, because an option may deliberately appear twice (see\n // `reconcile`).\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className={cn(\n GROUP_HEADING_STICKY,\n \"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // See {@link Combobox}: the role rides the button, `aria-selected`\n // is the CHOSEN row, and the keyboard's row is the field's\n // `aria-activedescendant`.\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the input's blur can't close the\n // list before the selection registers. Prevented on a disabled\n // row too, so pressing one leaves the field focused and open.\n e.preventDefault();\n take(o);\n }}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n rowClass(i === active && !o.disabled),\n o.disabled && DISABLED_ROW_CLASS,\n // Indented under its heading, so the hierarchy is readable at a\n // glance instead of inferred from grey trailing text.\n o.group && \"ps-6\",\n o.value === value && \"font-medium\",\n )}\n >\n {o.label}\n {o.sublabel && (\n <span className=\"ms-2 text-xs text-placeholder\">\n {o.sublabel}\n </span>\n )}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(createIndex)}\n role=\"option\"\n // An action, never chosen; whether the keyboard is ON it is the field's\n // `aria-activedescendant`'s to say.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows above: the input's blur would otherwise\n // reconcile and close the list before the click landed.\n e.preventDefault();\n create();\n }}\n onMouseEnter={() => {\n if (!createLocked) setActive(createIndex);\n }}\n className={cn(\n rowClass(active === createIndex && !createLocked),\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Fragment","rest"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/inline-entity-combobox.tsx"],"sourcesContent":["// The id-keyed sibling of Combobox. Moved out of combobox.tsx as it was (MOD-9,\n// refactor audit 2026-10); combobox still exports `InlineEntityCombobox` and its props.\nimport { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown, Plus, X } from \"lucide-react\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { isQuietFocus } from \"../lib/quiet-focus\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GROUP_HEADING_STICKY,\n isOptionEnabled,\n offersCreate,\n stepEnabled,\n useActiveOptionScroll,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { matchesSearch, rankMatch, searchWords } from \"../lib/text-match\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { lockedKeyDown, rowClass, SuggestionList, usePrimaryPressOnly } from \"./combobox-suggestions\";\n\n/** {@link InlineEntityCombobox}'s create row over the shared row look: in the brand\n * colour {@link Combobox}'s own create row wears, with a leading \"+\" as the panel\n * pickers' has — an action at the foot of the list, not one more record. */\nconst CREATE_ROW_CLASS = \"flex items-center gap-2 font-medium text-brand\";\n\n/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}\n * for why the div's spellings of them are omitted. */\nexport interface InlineEntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\">, FieldHelpProps {\n /** Selected option id; `null` or `clearValue` when nothing is selected. */\n value: V | C | null;\n /** Required and unanswered — see {@link Combobox}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value — see {@link Combobox}'s `error`. */\n error?: ReactNode;\n /** Standing advice — see {@link Combobox}'s `hint` (keksdose K4). */\n hint?: ReactNode;\n /** A picked/typed option emits its id; emptying the field, or the \"×\", emits\n * `clearValue`. */\n onChange: (v: V | C) => void;\n /** What a clear emits — `null` by default, `\"\"` for a schema that wants an empty\n * string. See {@link EntityCombobox}'s `clearValue`. */\n clearValue?: C;\n options: ComboOption<V>[];\n /** The visible label — see {@link Combobox}'s `label` (a ReactNode since 0.34, API-9). */\n label?: ReactNode;\n id?: string;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`\n * below, open the list with it. A click-the-value editor needs it: the cell\n * the user clicked names the field the editor should land the caret in, and\n * without a passthrough this component's fixed prop list puts the `<input>`\n * out of a caller's reach entirely (Keksdose feedback live #218). */\n autoFocus?: boolean;\n /** Phone sheet only — the anchored list has the field itself to type in. Falls\n * back to `placeholder`, then to `combobox.search` from the {@link UiKitProvider}. */\n searchPlaceholder?: string;\n /** Phone sheet only: a full screen showing nothing has to say why. The anchored\n * list simply does not open. Default: `combobox.noResults` from the provider. */\n emptyLabel?: string;\n closeLabel?: string;\n /** The phone sheet's heading (and, as a string, its accessible name) for a field\n * whose label is drawn by someone else — a bound field's label;\n * `RhfInlineEntityCombobox` hands its label over here (0.24). Default: `label`.\n * See {@link Combobox}'s `sheetTitle`. */\n sheetTitle?: ReactNode;\n /** Offer a clear \"×\" in place of the chevron whenever something is selected.\n *\n * Emptying the text already clears (see {@link reconcile}), and on a desktop that\n * is the fast way. It is not a way at all on a PHONE: touching the field opens the\n * full-screen sheet, which covers the very input the text would have been deleted\n * from — so a field that had been answered could not be UNanswered by any gesture\n * the screen offered (Keksdose live #236, filed from a phone). The \"×\" is the one\n * affordance both shells share, and it clears without opening anything. */\n clearable?: boolean;\n clearLabel?: string;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a picker\n * that SAVES on change. keksdose G2: the statement review's account picker IS the\n * write (picking re-runs the duplicate check on the server), and on the read-only demo\n * the app wrapped it in a hand-made Tooltip over a native `disabled` — out of the tab\n * order, so the reason never reached a keyboard.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey), opens no list or sheet, offers no clear \"×\", and no pick, typed\n * label or clear reaches `onChange`; the reason is in the kit {@link Tooltip} and on\n * the input's `aria-describedby`. Wins over `disabled`. Use it CONTROLLED, as the\n * component always is: the shown label is `value`'s.\n */\n disabledReason?: ReactNode;\n /**\n * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * A picker inside a form with its own Save stays editable under the lock — leave this\n * off there and put `commit` on the Save. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * A last row that makes a new record from what was typed — keksdose G9, whose \"Create\n * cash account\" had to become a separate button because this picker had no such row.\n *\n * Shown while the typed query is non-empty and names no option exactly\n * (case-insensitive — an exact hit is a record the list already shows), worded\n * `createLabel(query)`, default `combobox.create`: \"Create “{query}”\". The arrow keys\n * reach it like an option; Enter or a click calls `onCreate(query)` and closes the\n * list. Making the record, and then passing its id as `value`, is the caller's: the\n * field shows the old selection until it does.\n *\n * The same rule as {@link EntityCombobox}'s `onCreate`, so the family agrees. It is a\n * row of the LIST, not an option: it is never `aria-selected`, never matched by a\n * typed label on blur, and never sorted among the records the way a sentinel option\n * would be (the reason keksdose gave for the separate button).\n */\n onCreate?: (query: string) => void;\n /** The create row's words for a query. Default: `combobox.create` from the\n * {@link UiKitProvider}, else English `Create “{query}”`. */\n createLabel?: (query: string) => string;\n /**\n * Offer the create row with NOTHING typed too, worded as this — keksdose's \"Create cash\n * account\", which mints an account under a default name and so needs no query.\n * `onCreate` then receives `\"\"`, and the caller supplies the name. Left out, the row\n * needs a query.\n *\n * One string rather than a switch: a row with no query has nothing to quote, so the\n * kit cannot word it — \"Create “”\" says nothing — and naming it is what turns it on.\n * With both, one handler serves both rows:\n *\n * ```tsx\n * onCreate={(name) => createCash(name || t(\"cash_account_default_name\"))}\n * createEmptyLabel={t(\"create_cash_account\")}\n * ```\n */\n createEmptyLabel?: string;\n /**\n * The create row WRITES while picking does not — keksdose dev#496: on the read-only\n * demo choosing the cash account is draft state and stays live, minting one is a\n * write. Under a locked {@link WriteLockProvider} the row stays in the list, dimmed and\n * passed over by the arrows, with the lock's reason as its second line (a hover is the\n * one explanation a phone cannot show), and `onCreate` is not called. Not needed with\n * `commit`: a locked field opens no list at all.\n */\n createCommit?: CommitScope;\n /**\n * The `<input>` — the focusable control, the one a reader types in — as a React 19\n * ref prop (0.24, kastlan). Its account picker is this component inside an\n * `RhfField` render, and react-hook-form's focus-on-error calls `focus()` on whatever\n * `field.ref` is handed: with no ref there was nothing to hand it, so a line-item form\n * whose account was missing failed its submit and left the caret where it was. Pass\n * `field.ref` here, or use `RhfInlineEntityCombobox`, which does. The wrapper is not\n * reachable this way — focus and selection are the input's. See {@link Combobox}'s `ref`.\n */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input\n * with the shared field styling, chevron and inline as-you-type filtering) but\n * the value is an option id, not free text. Built so an entity picker can sit\n * next to a free-text combobox without any visible difference — a button-trigger\n * panel picker ({@link EntityCombobox}) never matches an input field exactly.\n *\n * Text-vs-value reconciliation: the input's text is transient. Picking a row or\n * typing an exact (unique) label commits that option; emptying the text commits\n * a clear; anything else reverts to the selected option's label on blur/Escape.\n *\n * **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}\n * beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto\n * this component and must not lose its sheet). The sheet carries its OWN query state\n * rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a\n * sheet that emptied it to show the full list would read as \"the user cleared the\n * field\" the moment it closed.\n */\nexport function InlineEntityCombobox<V extends string | number, C extends ComboClearValue = null>(\n props: InlineEntityComboboxProps<V, C>,\n) {\n const {\n value: rawValue,\n onChange,\n clearValue = null as C,\n options,\n label,\n id,\n placeholder,\n className,\n disabled,\n autoFocus,\n searchPlaceholder,\n emptyLabel,\n closeLabel,\n sheetTitle,\n clearable,\n clearLabel,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onCreate,\n createLabel,\n createEmptyLabel,\n createCommit,\n invalid,\n error,\n hint,\n help,\n ref,\n \"aria-label\": ariaLabel,\n // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s\n // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and\n // on the wrapper div the hint and required state described nothing.\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n // The clear value reads as \"nothing selected\", whichever one the caller picked —\n // so from here down `value` is the id or `null`, as it always was.\n const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // See the twin above on why these hang off the generated id.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const isPhone = usePhoneLayout();\n // See the twin above: the phone sheet owns its own Back entry (live #309), and the\n // Escape layer is this field's own.\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });\n const primaryOnly = usePrimaryPressOnly();\n // The field took the tour's quiet focus (§11.8) and has not opened since.\n const quietRef = useRef(false);\n const [active, setActive] = useState(-1);\n // null = not editing → the input shows the selected option's label.\n const [text, setText] = useState<string | null>(null);\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — see {@link SuggestionList}.\n const fieldRef = useRef<HTMLDivElement>(null);\n // The sheet's search box. Starts empty on every open, so a field that already\n // holds a value still offers the whole list — the shape a native <select> has\n // on a phone, and what the anchored panel gets from `query` below.\n const [sheetQuery, setSheetQuery] = useState(\"\");\n // Props first, provider second — see `EntityCombobox`. `emptyLabel` used to render\n // nothing when omitted, which left a phone user staring at a blank full-screen\n // sheet; now an omitted one says \"No results\" in the provider's language.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n // The lock (see `lockedKeyDown` above). A lock arriving mid-edit closes the list and\n // drops the loose text, adjusted while rendering: nothing typed before it may be\n // judged by `reconcile` after it.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n if (locked && (open || text !== null)) {\n setOpen(false);\n setText(null);\n }\n // The create row's own lock (`createCommit`): the list stays live, the row does not.\n const createReason = useCommitReason(createCommit, undefined);\n const createLocked = hasContent(createReason);\n\n const selected = useMemo(\n () => (value == null ? null : (options.find((o) => o.value === value) ?? null)),\n [options, value],\n );\n const shown = text ?? selected?.label ?? \"\";\n // Nothing selected has nothing to clear, and the chevron comes back — the field\n // keeps exactly one trailing control, so the \"×\" never crowds the value it sits on.\n const showClear = Boolean(clearable && value != null && !inert);\n\n // Focusing select-alls the current label; filtering only kicks in once the\n // text actually differs from it, so an already-filled field still opens on\n // the FULL list instead of a single-row \"filter\" of its own value.\n const typedQuery = text !== null && text !== (selected?.label ?? \"\") ? text.trim() : \"\";\n const query = isPhone ? sheetQuery.trim() : typedQuery;\n // Folded as every kit list folds (DUP-5): accents off and the locale's case, so\n // \"zurich\" finds Zürich and, in Turkish, \"ı\" finds \"IRMAK\".\n const locale = useKitLocale();\n const matches = useMemo(() => {\n let ranked = options;\n if (query) {\n // Every word of the query, in the label or the sublabel (0.51.4).\n const words = searchWords(query, locale);\n const hit = (o: ComboOption<V>) => matchesSearch(words, [o.label, o.sublabel], locale);\n // The label's whole name or its start first (`rankMatch` 0 and 1), then a match\n // anywhere in the label or the sublabel. Not the group (0.40, kk run 79): with\n // first-letter groups one typed letter matched its whole group.\n const leads = (o: ComboOption<V>) => {\n const rank = rankMatch(o.label, query, { locale });\n return rank === 0 || rank === 1;\n };\n const starts = options.filter(leads);\n const rest = options.filter((o) => !leads(o) && hit(o));\n ranked = [...starts, ...rest];\n }\n if (!ranked.some((o) => o.group)) return ranked;\n // Keep each group contiguous so the headings below appear once instead of\n // re-appearing every time the ranking interleaves two groups — while preserving\n // \"best match first\": a Map keeps insertion order, so groups come out ordered by\n // their best-ranked member and members keep their rank order inside the group.\n const blocks = new Map<string, ComboOption<V>[]>();\n for (const o of ranked) {\n const key = o.group ?? \"\";\n if (!blocks.has(key)) blocks.set(key, []);\n blocks.get(key)!.push(o);\n }\n return [...blocks.values()].flat();\n }, [options, query, locale]);\n\n // The create row (`onCreate`), on the query as typed — not lowercased: it becomes a\n // name. Compared against EVERY option, not `matches`: an exact hit anywhere is a\n // record that exists, disabled or not. Restated from `text` rather than derived from\n // `typedQuery`: this value is handed to the caller's `onCreate`, and the React\n // Compiler cannot then keep `matches` memoised on anything it was derived from.\n const createQuery = isPhone\n ? sheetQuery.trim()\n : text !== null && text !== (selected?.label ?? \"\")\n ? text.trim()\n : \"\";\n const showCreate = offersCreate(\n onCreate,\n createQuery,\n options.map((o) => o.label),\n createEmptyLabel,\n );\n const createText = createQuery ? labels.create(createQuery) : createEmptyLabel;\n // Every row the keyboard can land on: the options, then the create row — passed over\n // like a disabled option while a write lock holds it.\n const rows: readonly { disabled?: boolean }[] = showCreate ? [...matches, { disabled: createLocked }] : matches;\n const createIndex = matches.length;\n\n // Pointer-device only: the phone's rows live in a {@link PickerSheet} whose own\n // search box holds focus, so the field behind it must not claim to be pointing at\n // one of them.\n // A disabled row is never the keyboard's, even if the list changed under it.\n const activeId = !isPhone && isOptionEnabled(rows[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n setSheetQuery(\"\");\n };\n // See the twin above: a layer while the list SHOWS, and Escape puts the label back.\n const showing = !isPhone && open && !inert && (matches.length > 0 || showCreate);\n useDismissLayer(showing, () => {\n setText(null);\n close();\n });\n const take = (o: ComboOption<V>) => {\n // A `disabled` option is listed, never taken — by any path; nor is anything while\n // the field is locked.\n if (o.disabled || locked) return;\n if (o.value !== value) onChange(o.value);\n setText(null);\n close();\n };\n /** Turn loose text into a decision: empty clears, an exact label match that names\n * ONE entity commits, anything else reverts to the selected label.\n *\n * Unique by VALUE, not by row. An option may deliberately appear twice — Keksdose\n * repeats recently-used categories in a \"Recent\" group at the top (live #203) — and\n * counting rows made every such option uncommittable: two hits, so nothing fired and\n * the field reverted to whatever was selected before. Typing a category you had just\n * used, then tabbing away, silently discarded it, and the more categories you used\n * the more of them stopped working. Two rows naming the same id are not an ambiguity;\n * two ids sharing a label are. */\n const create = () => {\n if (!onCreate || locked || createLocked) return;\n onCreate(createQuery);\n setText(null);\n close();\n };\n const reconcile = () => {\n // Locked, loose text is dropped rather than judged: nothing reaches `onChange`.\n if (text !== null && !locked) {\n const q = text.trim();\n if (!q) {\n if (value != null) onChange(clearValue);\n } else {\n // A disabled option's label typed out in full is not a way round `disabled`:\n // it is not a hit, so the text reverts like any other non-answer.\n const hits = options.filter((o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase());\n const ids = new Set(hits.map((h) => h.value));\n if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);\n }\n }\n setText(null);\n close();\n };\n\n // The create row's inside, one for both shells: a \"+\" (it is an action, not a record),\n // the words, and — while a write lock holds it — the lock's reason as a second line.\n // Both lines wrap from Large up (text size §10.17, round 0.34 T1): the words hold what\n // the reader typed, which an ellipsis would cut.\n const createRowContent = (\n <>\n <Plus aria-hidden className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large\">{createText}</span>\n {createLocked && (\n <span className=\"block truncate-until-large text-xs font-normal text-placeholder\">{createReason}</span>\n )}\n </span>\n </>\n );\n // While locked the field sits in the reason's Tooltip — see Combobox.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,\n // description, invalid or required state: those belong on the <input> below,\n // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {/* A real <label for> — see {@link Combobox}; with a \"?\", in a row after the field. */}\n {label !== undefined && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel htmlFor={fieldId} className={inert ? \"opacity-50\" : undefined}>\n {label}\n </ComboboxFieldLabel>\n )}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n /* Inner wrapper for chevron centering — same reasoning as Combobox above.\n It is also what the portalled list anchors to.\n Dimmed as a whole when disabled (or locked), the way EntityCombobox dims its\n trigger. FIELD_BASE's grey alone left a disabled picker looking like a\n filled-in one beside the pickers that do dim (the label is not the input's\n `peer`, so it is dimmed by hand above). */\n <div ref={fieldRef} className={cn(\"relative\", inert && \"cursor-not-allowed opacity-50\")}>\n <input\n ref={ref}\n id={fieldId}\n value={shown}\n placeholder={placeholder}\n // The caller's name, over the <label for> above. Just the label, never\n // \"label: value\" the way a trigger button has to compose it: an input\n // already exposes its value separately.\n aria-label={ariaLabel}\n role=\"combobox\"\n aria-expanded={open}\n // Required by the role, and the half that was missing: the field said it\n // was expanded and never said what it had expanded (ESLint's\n // `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n aria-required={ariaRequired}\n autoComplete=\"off\"\n // Locked: focusable, `readOnly`, `aria-disabled` — see `lockedKeyDown`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n // `inputMode=\"none\"` rather than readOnly, for the same reason Combobox\n // above gives: the sheet carries the keyboard, and a readOnly field would\n // take FIELD_BASE's settled look on a field that is perfectly editable.\n inputMode={isPhone ? \"none\" : undefined}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={(e) => {\n // See {@link usePrimaryPressOnly}: a back/forward button lands here on\n // its way to navigating, and neither the list nor the select-all is\n // anything it asked for (live #309 rework). A locked field takes focus to\n // say why, and opens nothing.\n if (primaryOnly.fromAuxButton() || locked) return;\n setText(shown);\n // The tour's focus on arrival (§11.8) opens nothing: the list would cover\n // its card (0.50.1). A click, ArrowDown or typing opens it.\n if (isQuietFocus(e.currentTarget)) {\n quietRef.current = true;\n return;\n }\n e.currentTarget.select();\n setOpen(true);\n // The sheet has its own input; the field behind it must not also pull up\n // the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n // After a quiet focus, the person's click on the already-focused field\n // fires no `focus`: the click is their \"open\" instead.\n onClick={() => {\n if (!quietRef.current || locked) return;\n quietRef.current = false;\n setOpen(true);\n }}\n onBlur={() => {\n quietRef.current = false;\n // On a phone the blur is the SHEET taking focus, not the user leaving the\n // field — reconciling there would close the sheet the instant it opened.\n if (!isPhone) reconcile();\n }}\n onChange={(e) => {\n if (locked) return;\n setText(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n if (locked) return lockedKeyDown(e);\n // Disabled rows are passed over; Up from \"nothing highlighted\" lands on\n // the first takeable row, as it always landed on row 0. The create row is\n // the last row the arrows reach.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => stepEnabled(rows, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => (i < 0 ? stepEnabled(rows, -1, 1) : stepEnabled(rows, i, -1)));\n } else if (e.key === \"Enter\") {\n // An IME's Enter takes nothing, and judges no loose text (BUG-9).\n if (isImeEnter(e)) return;\n if (open && isOptionEnabled(rows[active])) {\n e.preventDefault();\n if (active === createIndex && showCreate) create();\n else take(matches[active]);\n } else {\n reconcile();\n }\n } else if (e.key === \"Escape\") {\n // A showing list takes the key and stops it — see the twin in\n // {@link Combobox} (round 0.34 BUG-1).\n if (showing && takeEscape(e)) return;\n setText(null);\n close();\n } else if (e.key === \"Tab\") {\n // Focus is in THIS input and stays there, so the browser's own Tab is\n // left alone; all that is needed is that the list stop covering what\n // the user is tabbing to. `reconcile` rather than `close`, because\n // leaving the field is exactly when loose text has to be judged.\n reconcile();\n }\n // Home/End stay with the caret — see the note in {@link Combobox}: this\n // field is editable, and its text is what `reconcile` judges.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pe-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n {showClear ? (\n <button\n type=\"button\"\n // Out of the tab order, like the clear on `EntityCombobox`: the keyboard\n // already clears this field by selecting its text and deleting, and a\n // second stop between every picker and the next field is a worse trade\n // than the one gesture it saves.\n tabIndex={-1}\n aria-label={labels.clear}\n // preventDefault, exactly as the chevron does: without it the press\n // focuses the input, which on a phone opens the sheet over the field the\n // press was clearing.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n setText(null);\n close();\n if (!locked) onChange(clearValue);\n }}\n className={cn(\n \"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder\",\n \"hover:text-secondary\",\n )}\n >\n <X aria-hidden className=\"size-4\" />\n </button>\n ) : (\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input — and\n // not at all on a disabled or locked field, which the input's own\n // `disabled`/`readOnly` does not stop from here.\n e.preventDefault();\n if (!inert) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\",\n // The box above does the dimming; dimming here too would halve it again.\n !inert && \"cursor-pointer\",\n )}\n />\n )}\n </div>,\n )}\n </EndHintRow>\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, inert && \"opacity-50\")}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !inert}\n // Closing without choosing keeps the value: `text` was never emptied, so\n // reconcile has nothing to undo — it just puts the label back.\n onClose={reconcile}\n title={sheetTitle ?? label}\n query={sheetQuery}\n onQueryChange={(v) => {\n setSheetQuery(v);\n setActive(-1);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder ?? labels.search}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {matches.map((o, i) => (\n // Keyed by group AND value, like combobox-core.tsx: an option may\n // deliberately appear twice (see `reconcile`), and a bare value key\n // would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className={cn(\n GROUP_HEADING_STICKY,\n \"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n onClick={() => take(o)}\n className={cn(\n SHEET_ROW_CLASS,\n o.value === value && \"font-medium\",\n o.disabled && DISABLED_ROW_CLASS,\n )}\n >\n {o.label}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n // An action, never chosen — see `onCreate`.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n onClick={create}\n className={cn(SHEET_ROW_CLASS, CREATE_ROW_CLASS, createLocked && DISABLED_ROW_CLASS)}\n >\n {createRowContent}\n </button>\n </li>\n )}\n {/* \"Nothing matched\" only when there is nothing to do either: with a create\n row the list is not empty, it is offering to fill itself. */}\n {matches.length === 0 && !showCreate && (\n <li className=\"px-4 py-3 text-sm text-muted\">{labels.noResults}</li>\n )}\n </ul>\n </PickerSheet>\n )}\n {showing && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {matches.map((o, i) => (\n // A group heading is emitted at each group boundary rather than repeating\n // the group on every row (feedback #136). `matches` is group-contiguous,\n // so comparing with the previous row is enough. Fragment key sits here;\n // the heading and the option carry their own list semantics — keyed by\n // group AND value, because an option may deliberately appear twice (see\n // `reconcile`).\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className={cn(\n GROUP_HEADING_STICKY,\n \"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // See {@link Combobox}: the role rides the button, `aria-selected`\n // is the CHOSEN row, and the keyboard's row is the field's\n // `aria-activedescendant`.\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the input's blur can't close the\n // list before the selection registers. Prevented on a disabled\n // row too, so pressing one leaves the field focused and open.\n e.preventDefault();\n take(o);\n }}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n rowClass(i === active && !o.disabled),\n o.disabled && DISABLED_ROW_CLASS,\n // Indented under its heading, so the hierarchy is readable at a\n // glance instead of inferred from grey trailing text.\n o.group && \"ps-6\",\n o.value === value && \"font-medium\",\n )}\n >\n {o.label}\n {o.sublabel && <span className=\"ms-2 text-xs text-placeholder\">{o.sublabel}</span>}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(createIndex)}\n role=\"option\"\n // An action, never chosen; whether the keyboard is ON it is the field's\n // `aria-activedescendant`'s to say.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows above: the input's blur would otherwise\n // reconcile and close the list before the click landed.\n e.preventDefault();\n create();\n }}\n onMouseEnter={() => {\n if (!createLocked) setActive(createIndex);\n }}\n className={cn(\n rowClass(active === createIndex && !createLocked),\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Fragment","rest"]}
|