@eifi1/ui-kit 0.31.1 → 0.32.1
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 +51 -28
- package/dist/account/delete-account-setting.d.ts +8 -3
- package/dist/admin/admin-action-confirm.d.ts +8 -3
- package/dist/admin/admin-action-log.d.ts +1 -1
- package/dist/admin/admin-action-log.js +4 -2
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/invitations-panel.d.ts +8 -3
- package/dist/admin/invitations-panel.js +29 -26
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.d.ts +8 -3
- package/dist/admin/role-select.d.ts +10 -5
- package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
- package/dist/admin/user-roster.d.ts +10 -5
- package/dist/admin/user-roster.js +8 -47
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +43 -8
- package/dist/auth/auth-errors.js +24 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/register-form.d.ts +8 -3
- package/dist/billing/billing-banner.d.ts +184 -0
- package/dist/billing/billing-banner.js +98 -0
- package/dist/billing/billing-banner.js.map +1 -0
- package/dist/billing/billing-labels.d.ts +100 -0
- package/dist/billing/billing-labels.js +76 -0
- package/dist/billing/billing-labels.js.map +1 -0
- package/dist/billing/billing-lock.d.ts +158 -0
- package/dist/billing/billing-lock.js +17 -0
- package/dist/billing/billing-lock.js.map +1 -0
- package/dist/billing/billing-standing.d.ts +149 -0
- package/dist/billing/billing-standing.js +28 -0
- package/dist/billing/billing-standing.js.map +1 -0
- package/dist/billing/plan-card.d.ts +248 -0
- package/dist/billing/plan-card.js +248 -0
- package/dist/billing/plan-card.js.map +1 -0
- package/dist/billing/plan-limit-notice.d.ts +156 -0
- package/dist/billing/plan-limit-notice.js +76 -0
- package/dist/billing/plan-limit-notice.js.map +1 -0
- package/dist/billing/plan-limit.d.ts +34 -0
- package/dist/billing/plan-limit.js +20 -0
- package/dist/billing/plan-limit.js.map +1 -0
- package/dist/billing/plan-price.d.ts +61 -0
- package/dist/billing/plan-price.js +45 -0
- package/dist/billing/plan-price.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +133 -0
- package/dist/billing/subscription-actions.js +28 -0
- package/dist/billing/subscription-actions.js.map +1 -0
- package/dist/billing/subscription-status-chip.d.ts +149 -0
- package/dist/billing/subscription-status-chip.js +48 -0
- package/dist/billing/subscription-status-chip.js.map +1 -0
- package/dist/chart.d.ts +9 -3
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +16 -4
- package/dist/components/account-chips.js +21 -3
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +6 -1
- package/dist/components/alert-banner.js +5 -4
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +8 -3
- package/dist/components/amount-input.js +5 -5
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/appearance-labels.d.ts +38 -0
- package/dist/components/appearance-labels.js +19 -0
- package/dist/components/appearance-labels.js.map +1 -0
- package/dist/components/autocomplete.js +2 -2
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/breadcrumbs.js +3 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +2 -2
- package/dist/components/bulk-action-bar.js +7 -12
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +8 -3
- package/dist/components/button-group.js +7 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +8 -3
- package/dist/components/calculator.js +4 -3
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +5 -5
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart-height.d.ts +23 -0
- package/dist/components/chart-height.js +15 -0
- package/dist/components/chart-height.js.map +1 -0
- package/dist/components/chart-zoom.js +6 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/checkbox-group.js +2 -2
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/checkbox.d.ts +10 -0
- package/dist/components/checkbox.js +25 -10
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +19 -3
- package/dist/components/chip.js +54 -23
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +1 -1
- package/dist/components/choice-card.js +9 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper.js +1 -1
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +8 -7
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +8 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +8 -3
- package/dist/components/copy-button.d.ts +10 -5
- package/dist/components/country-select.js +1 -1
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +8 -3
- package/dist/components/data-table-cells.d.ts +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +4 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-labels.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -2
- package/dist/components/data-table-pagination.js +15 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +123 -52
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +1 -1
- package/dist/components/date-picker.js +18 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.js +2 -2
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +2 -1
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +3 -2
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +1 -1
- package/dist/components/dropdown.js +7 -1
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -6
- package/dist/components/facing-pair.js +3 -2
- package/dist/components/facing-pair.js.map +1 -1
- package/dist/components/field-parts.d.ts +102 -6
- package/dist/components/field-parts.js +81 -6
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.d.ts +4 -1
- package/dist/components/field-sync.js +12 -2
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/field.js +2 -2
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +8 -3
- package/dist/components/file-dropzone.d.ts +8 -3
- package/dist/components/floating-panel.js +7 -4
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.d.ts +12 -0
- package/dist/components/focus-ring.js +8 -0
- package/dist/components/focus-ring.js.map +1 -0
- package/dist/components/form-actions.d.ts +8 -3
- package/dist/components/form-actions.js +33 -19
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +11 -1
- package/dist/components/full-bleed-dialog.js +3 -1
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/iban-input.d.ts +8 -3
- package/dist/components/image-grid.js +1 -1
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js +10 -4
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/language-select.d.ts +8 -3
- package/dist/components/legal-page.d.ts +26 -5
- package/dist/components/legal-page.js +3 -1
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +8 -3
- package/dist/components/legal.js +2 -1
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js +24 -2
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +2 -1
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +5 -0
- package/dist/components/list.js +13 -9
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +3 -1
- package/dist/components/measured-grid.js +38 -20
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js +2 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +8 -4
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.d.ts +8 -3
- package/dist/components/month-picker.js +50 -39
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/nav-pills.js +1 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +8 -3
- package/dist/components/number-input.d.ts +11 -4
- package/dist/components/number-input.js +8 -6
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +8 -3
- package/dist/components/numpad-sheet.js +2 -1
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +2 -2
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +2 -2
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/phone-input.d.ts +8 -3
- package/dist/components/phone-input.js +4 -4
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/pie-chart.d.ts +8 -2
- package/dist/components/pie-chart.js +11 -7
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +9 -1
- package/dist/components/popover.js +8 -4
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.d.ts +100 -0
- package/dist/components/row-actions.js +231 -0
- package/dist/components/row-actions.js.map +1 -0
- package/dist/components/scroll-area.js +1 -1
- package/dist/components/scroll-area.js.map +1 -1
- package/dist/components/search-field.js +3 -2
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-budget.d.ts +30 -7
- package/dist/components/series-chart-budget.js +12 -4
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +7 -1
- package/dist/components/series-chart-ticks.js +6 -0
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.d.ts +58 -28
- package/dist/components/series-chart.js +42 -32
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +57 -8
- package/dist/components/settings-fields.js +39 -1
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +11 -6
- package/dist/components/share-card.js +8 -2
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.d.ts +4 -0
- package/dist/components/sign-chip.js +17 -4
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.d.ts +3 -1
- package/dist/components/signature-pad.js +8 -5
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/slider.js +7 -6
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.d.ts +5 -1
- package/dist/components/sparkline.js +4 -2
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.d.ts +17 -1
- package/dist/components/stat-tile.js +48 -20
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +4 -0
- package/dist/components/status-dot.js +9 -2
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swipeable-row.js +10 -2
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +21 -7
- package/dist/components/switch.js +24 -11
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.js +2 -2
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +8 -3
- package/dist/components/text-link.js +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.d.ts +7 -2
- package/dist/components/text.js +5 -5
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +2 -1
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +8 -3
- package/dist/components/toast.d.ts +18 -2
- package/dist/components/toast.js +8 -5
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.d.ts +25 -3
- package/dist/components/toggle-group.js +40 -11
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +10 -1
- package/dist/components/toggle-legend.js +21 -7
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -1
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +4 -0
- package/dist/components/translation-review.d.ts +4 -0
- package/dist/components/translation-review.js +3 -3
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +3 -2
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.d.ts +5 -2
- package/dist/components/treemap.js +33 -16
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +8 -3
- package/dist/components/ui.js +207 -119
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/user-avatar.d.ts +5 -1
- package/dist/components/user-avatar.js +1 -1
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.js +20 -14
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
- package/dist/data-table.d.ts +1 -1
- package/dist/{delete-account-setting-CpIhzxxZ.d.ts → delete-account-setting-FLyJf-kZ.d.ts} +642 -60
- package/dist/demo/demo-banner.js +1 -0
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +8 -3
- package/dist/feedback/feedback-context.d.ts +8 -3
- package/dist/feedback/feedback-crash.d.ts +8 -3
- package/dist/feedback/feedback-dialog.d.ts +8 -3
- package/dist/feedback/feedback-dialog.js +3 -3
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +8 -3
- package/dist/feedback/feedback-inbox.js +2 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +8 -3
- package/dist/feedback/feedback-menu.d.ts +9 -4
- package/dist/feedback/feedback-record.d.ts +8 -3
- package/dist/feedback/feedback-row-detail.d.ts +8 -3
- package/dist/feedback/feedback-status-undo.d.ts +8 -3
- package/dist/feedback/feedback-submit.d.ts +8 -3
- package/dist/feedback/feedback-swipe.d.ts +8 -3
- package/dist/feedback/feedback-table.d.ts +8 -3
- package/dist/feedback/feedback-thread.d.ts +8 -3
- package/dist/feedback/feedback-thread.js +3 -3
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +8 -3
- package/dist/hooks/use-anchored-panel.d.ts +32 -3
- package/dist/hooks/use-anchored-panel.js +21 -3
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-breakpoint.d.ts +88 -0
- package/dist/hooks/use-breakpoint.js +50 -0
- package/dist/hooks/use-breakpoint.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +8 -3
- package/dist/hooks/use-large-text.d.ts +29 -0
- package/dist/hooks/use-large-text.js +20 -0
- package/dist/hooks/use-large-text.js.map +1 -0
- package/dist/hooks/use-windowed-rows.d.ts +52 -8
- package/dist/hooks/use-windowed-rows.js +94 -5
- package/dist/hooks/use-windowed-rows.js.map +1 -1
- package/dist/i18n/defaults.d.ts +8 -3
- package/dist/i18n/defaults.js +8 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +8 -3
- package/dist/i18n/german.js +94 -3
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +8 -3
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -5
- package/dist/i18n/locales/de-CH.d.ts +8 -3
- package/dist/i18n/locales/en.d.ts +8 -3
- package/dist/i18n/locales/es.d.ts +8 -3
- package/dist/i18n/locales/es.js +89 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +8 -3
- package/dist/i18n/locales/fr.js +91 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +8 -3
- package/dist/i18n/locales/hu.js +91 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +8 -3
- package/dist/i18n/locales/it.js +92 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +8 -3
- package/dist/i18n/locales/zh.js +89 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +8 -3
- package/dist/i18n/review.js +15 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +27 -11
- package/dist/index.js +77 -4
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.d.ts +8 -3
- package/dist/landing/landing-sections.d.ts +8 -3
- package/dist/landing/landing-sections.js +7 -3
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/public-header.d.ts +9 -4
- package/dist/landing/public-header.js +3 -3
- package/dist/landing/public-header.js.map +1 -1
- package/dist/landing/routing.d.ts +8 -3
- package/dist/lib/cn.js +3 -2
- package/dist/lib/cn.js.map +1 -1
- package/dist/rhf/fields.d.ts +8 -3
- package/dist/rhf/fields.js +1 -1
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +8 -3
- package/dist/rhf/form.js +2 -2
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf.d.ts +8 -3
- package/dist/search/command-palette.d.ts +1 -1
- package/dist/search/command-palette.js +9 -8
- package/dist/search/command-palette.js.map +1 -1
- package/dist/settings/settings-layout.js +2 -1
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/settings-section.d.ts +8 -3
- package/dist/settings/use-account-appearance.d.ts +92 -0
- package/dist/settings/use-account-appearance.js +63 -0
- package/dist/settings/use-account-appearance.js.map +1 -0
- package/dist/settings/use-settings-route.d.ts +1 -1
- package/dist/settings/use-settings-route.js +2 -3
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.d.ts +12 -171
- package/dist/shell/app-shell.js +144 -28
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -1
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +8 -3
- package/dist/shell/top-bar-brand.js +3 -1
- package/dist/shell/top-bar-brand.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +4 -0
- package/dist/shell/topbar-action-menu.js +3 -3
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +4 -4
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +9 -5
- package/dist/theme/contrast-tokens.d.ts +41 -0
- package/dist/theme/contrast-tokens.js +59 -0
- package/dist/theme/contrast-tokens.js.map +1 -0
- package/dist/theme/contrast.d.ts +110 -0
- package/dist/theme/contrast.js +128 -0
- package/dist/theme/contrast.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +12 -1
- package/dist/theme/palette-presets.js +9 -4
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.d.ts +173 -0
- package/dist/theme/text-size.js +121 -0
- package/dist/theme/text-size.js.map +1 -0
- package/dist/wizard/stepper-nav.d.ts +8 -3
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +8 -3
- package/package.json +1 -1
- package/src/admin/admin-action-log.tsx +6 -1
- package/src/admin/invitations-panel.tsx +36 -29
- package/src/admin/user-roster.tsx +17 -72
- package/src/auth/auth-errors.ts +68 -7
- package/src/billing/billing-banner.tsx +205 -0
- package/src/billing/billing-labels.ts +240 -0
- package/src/billing/billing-lock.ts +75 -0
- package/src/billing/billing-standing.ts +72 -0
- package/src/billing/plan-card.tsx +458 -0
- package/src/billing/plan-limit-notice.tsx +145 -0
- package/src/billing/plan-limit.ts +53 -0
- package/src/billing/plan-price.ts +101 -0
- package/src/billing/subscription-actions.tsx +76 -0
- package/src/billing/subscription-status-chip.tsx +95 -0
- package/src/chart.ts +2 -0
- package/src/components/account-chips.tsx +36 -10
- package/src/components/alert-banner.tsx +16 -7
- package/src/components/amount-input.tsx +5 -5
- package/src/components/appearance-labels.ts +47 -0
- package/src/components/autocomplete.tsx +2 -2
- package/src/components/breadcrumbs.tsx +3 -3
- package/src/components/bulk-action-bar.tsx +11 -14
- package/src/components/button-group.tsx +21 -0
- package/src/components/calculator.tsx +4 -3
- package/src/components/calendar-heatmap.tsx +5 -5
- package/src/components/chart-height.ts +30 -0
- package/src/components/chart-zoom.tsx +6 -1
- package/src/components/checkbox-group.tsx +2 -2
- package/src/components/checkbox.tsx +62 -20
- package/src/components/chip.tsx +139 -30
- package/src/components/choice-card.tsx +16 -11
- package/src/components/column-mapper.tsx +1 -1
- package/src/components/combobox-core.tsx +8 -7
- package/src/components/combobox.tsx +8 -8
- package/src/components/country-select.tsx +1 -1
- package/src/components/currency-select.tsx +3 -3
- package/src/components/data-table-filter-popover.tsx +7 -1
- package/src/components/data-table-pagination.tsx +60 -31
- package/src/components/data-table.tsx +164 -43
- package/src/components/date-picker.tsx +44 -10
- package/src/components/description-list.tsx +2 -2
- package/src/components/dialog-frame.tsx +2 -1
- package/src/components/disclosure.tsx +3 -2
- package/src/components/dropdown.tsx +10 -2
- package/src/components/facing-pair.ts +12 -5
- package/src/components/field-parts.tsx +190 -7
- package/src/components/field-sync.tsx +17 -3
- package/src/components/field.tsx +2 -2
- package/src/components/floating-panel.tsx +7 -4
- package/src/components/focus-ring.ts +12 -0
- package/src/components/form-actions.tsx +55 -18
- package/src/components/full-bleed-dialog.tsx +18 -4
- package/src/components/image-grid.tsx +1 -1
- package/src/components/inline-edit-field.tsx +26 -4
- package/src/components/legal-page.tsx +28 -2
- package/src/components/legal.tsx +19 -1
- package/src/components/lightbox.tsx +24 -2
- package/src/components/line-items.tsx +7 -1
- package/src/components/list.tsx +36 -10
- package/src/components/measured-grid.tsx +49 -24
- package/src/components/menu-item.tsx +2 -2
- package/src/components/mini-calendar.tsx +8 -4
- package/src/components/month-picker.tsx +15 -6
- package/src/components/nav-pills.tsx +1 -1
- package/src/components/number-input.tsx +14 -7
- package/src/components/numpad-sheet.tsx +2 -2
- package/src/components/one-time-code-input.tsx +2 -2
- package/src/components/page-contents.tsx +2 -2
- package/src/components/phone-input.tsx +9 -4
- package/src/components/pie-chart.tsx +19 -9
- package/src/components/popover.tsx +22 -5
- package/src/components/row-actions.tsx +450 -0
- package/src/components/scroll-area.tsx +1 -1
- package/src/components/search-field.tsx +4 -2
- package/src/components/series-chart-budget.ts +40 -8
- package/src/components/series-chart-ticks.ts +10 -0
- package/src/components/series-chart.tsx +123 -72
- package/src/components/settings-fields.tsx +89 -5
- package/src/components/share-card.tsx +31 -2
- package/src/components/sign-chip.tsx +28 -4
- package/src/components/signature-pad.tsx +13 -7
- package/src/components/slider.tsx +22 -9
- package/src/components/sparkline.tsx +11 -3
- package/src/components/stat-tile.tsx +70 -14
- package/src/components/status-dot.tsx +25 -2
- package/src/components/swipeable-row.tsx +10 -2
- package/src/components/switch.tsx +57 -23
- package/src/components/table.tsx +2 -2
- package/src/components/text-link.tsx +1 -1
- package/src/components/text.tsx +11 -6
- package/src/components/tile-radio.tsx +4 -1
- package/src/components/toast.tsx +27 -7
- package/src/components/toggle-group.tsx +115 -14
- package/src/components/toggle-legend.tsx +53 -8
- package/src/components/translation-review-editor.tsx +1 -1
- package/src/components/translation-review.tsx +11 -5
- package/src/components/tree-view.tsx +3 -2
- package/src/components/treemap.tsx +27 -10
- package/src/components/ui.tsx +353 -153
- package/src/components/user-avatar.tsx +1 -1
- package/src/components/wizard-stepper.tsx +5 -1
- package/src/demo/demo-banner.tsx +5 -0
- package/src/feedback/feedback-dialog.tsx +3 -3
- package/src/feedback/feedback-inbox.tsx +2 -2
- package/src/feedback/feedback-thread.tsx +3 -3
- package/src/hooks/use-anchored-panel.ts +67 -5
- package/src/hooks/use-breakpoint.ts +132 -0
- package/src/hooks/use-large-text.ts +41 -0
- package/src/hooks/use-windowed-rows.ts +168 -12
- package/src/i18n/defaults.ts +8 -0
- package/src/i18n/german.ts +107 -0
- package/src/i18n/kit-labels.tsx +12 -0
- package/src/i18n/locales/es.ts +98 -0
- package/src/i18n/locales/fr.ts +102 -0
- package/src/i18n/locales/hu.ts +100 -0
- package/src/i18n/locales/it.ts +103 -0
- package/src/i18n/locales/zh.ts +94 -0
- package/src/i18n/review.ts +14 -0
- package/src/index.ts +76 -6
- package/src/landing/landing-sections.tsx +7 -3
- package/src/landing/public-header.tsx +4 -4
- package/src/lib/cn.ts +13 -2
- package/src/rhf/fields.tsx +1 -1
- package/src/rhf/form.tsx +2 -2
- package/src/search/command-palette.tsx +10 -9
- package/src/settings/settings-layout.tsx +2 -1
- package/src/settings/use-account-appearance.ts +148 -0
- package/src/settings/use-settings-route.ts +3 -4
- package/src/shell/app-shell.tsx +278 -54
- package/src/shell/auth-layout.tsx +1 -1
- package/src/shell/option-switcher-menu.tsx +1 -1
- package/src/shell/top-bar-brand.tsx +3 -1
- package/src/shell/topbar-action-menu.tsx +3 -3
- package/src/shell/topbar-controls.tsx +4 -4
- package/src/theme/contrast-tokens.ts +112 -0
- package/src/theme/contrast.ts +225 -0
- package/src/theme/palette-presets.ts +24 -4
- package/src/theme/palette-store.ts +6 -1
- package/src/theme/text-size.ts +283 -0
- package/src/wizard/wizard-summary.tsx +4 -2
- package/tokens.css +158 -8
|
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
import { Calculator as CalculatorIcon, Delete } from "lucide-react";
|
|
5
5
|
import { Popover } from "./popover.js";
|
|
6
6
|
import { cn } from "../lib/cn.js";
|
|
7
|
+
import { FIELD_TOUCH_TEXT } from "./ui.js";
|
|
7
8
|
import { evaluateExpression, formatResult, isBareAmount, splitLeadingSign } from "../lib/calc.js";
|
|
8
9
|
import {
|
|
9
10
|
DEFAULT_CALCULATOR_LABELS,
|
|
@@ -27,7 +28,7 @@ const KEYS = [
|
|
|
27
28
|
{ kind: "back" },
|
|
28
29
|
{ kind: "ins", label: "+", ins: "+", accent: true, name: "plus" }
|
|
29
30
|
];
|
|
30
|
-
const KEY_BASE = "flex h-9 items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-
|
|
31
|
+
const KEY_BASE = "flex h-9 items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-[length:var(--focus-ring-width)] focus:ring-[var(--border-strong)]";
|
|
31
32
|
const KEY_DIGIT = "bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)]";
|
|
32
33
|
const KEY_ACCENT = "bg-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--bg-surface-2)]";
|
|
33
34
|
function seed(initial) {
|
|
@@ -78,7 +79,7 @@ function CalculatorPanel({
|
|
|
78
79
|
}
|
|
79
80
|
},
|
|
80
81
|
dir: "ltr",
|
|
81
|
-
className: "block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 text-right font-mono text-sm text-[var(--text-primary)] focus:border-[var(--border-strong)] focus:ring-[var(--border-strong)]"
|
|
82
|
+
className: cn("block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 text-right font-mono text-sm text-[var(--text-primary)] focus:border-[var(--border-strong)] focus:ring-[var(--border-strong)]", FIELD_TOUCH_TEXT)
|
|
82
83
|
}
|
|
83
84
|
),
|
|
84
85
|
/* @__PURE__ */ jsx("div", { dir: "ltr", className: "h-4 pr-1 text-right font-mono text-xs text-[var(--text-placeholder)]", children: result !== null && formatResult(result) !== text.trim() ? `= ${formatResult(result)}` : "" }),
|
|
@@ -151,7 +152,7 @@ function CalculatorButton({
|
|
|
151
152
|
return /* @__PURE__ */ jsx(
|
|
152
153
|
Popover,
|
|
153
154
|
{
|
|
154
|
-
width:
|
|
155
|
+
width: "14rem",
|
|
155
156
|
labels: { panel: labels.panel },
|
|
156
157
|
trigger: ({ open, toggle, ref }) => /* @__PURE__ */ jsx(
|
|
157
158
|
"button",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/calculator.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Calculator as CalculatorIcon, Delete } from \"lucide-react\";\nimport { Popover } from \"./popover\";\nimport { cn } from \"../lib/cn\";\nimport { evaluateExpression, formatResult, isBareAmount, splitLeadingSign } from \"../lib/calc\";\nimport {\n DEFAULT_CALCULATOR_LABELS,\n useKitLabels,\n type CalculatorLabels,\n} from \"../i18n/kit-labels\";\n\n/** Screen-reader names for the calculator panel's own controls — any part of the\n * kit's `calculator` namespace. Once three optional keys of its own; widened, not\n * replaced, so every object that type-checked before still does. */\nexport type CalculatorButtonLabels = Partial<CalculatorLabels>;\n\ninterface CalculatorButtonProps {\n /** Current field text — seeds the keypad so the user can keep calculating\n * from what's already entered. */\n value: string;\n /**\n * Called with the evaluated result whenever the running expression resolves\n * to a number, so the host field tracks the keypad live.\n *\n * `expression` is the TEXT that result came out of — `\"-42+50\"` for an 8 — and\n * is omitted when there was no calculation (a cleared pad). A host that keeps\n * the sign of its figure OUTSIDE the text (see `AmountInput`'s `negative` /\n * `onNegativeChange`, where a direction toggle owns it) cannot otherwise tell\n * that 8 from a freshly typed one: the first is a sum that came out positive\n * and has to take the direction with it, the second is a magnitude that must\n * leave the direction alone. Hosts whose text carries its own sign can take\n * the one argument and ignore this.\n */\n onChange: (value: string, expression?: string) => void;\n className?: string;\n /** The trigger's accessible name; wins over `calculator.open` from the provider. */\n ariaLabel?: string;\n /** Screen-reader names for the panel's own controls. `ariaLabel` above already\n * covers the trigger; these three were hardcoded English inside the popover,\n * which a translating host had no way to reach. */\n labels?: CalculatorButtonLabels;\n}\n\n/** Keypad layout (4 columns). The final C/= row is rendered separately. `name` is\n * the key of the {@link CalculatorLabels} entry that names a non-digit key; a digit\n * is named by its own glyph. */\ntype Key =\n | { kind: \"ins\"; label: string; ins: string; accent?: boolean; name?: KeyName }\n | { kind: \"back\" };\n\ntype KeyName = \"plus\" | \"minus\" | \"times\" | \"divide\" | \"decimal\";\n\nconst KEYS: Key[] = [\n { kind: \"ins\", label: \"7\", ins: \"7\" },\n { kind: \"ins\", label: \"8\", ins: \"8\" },\n { kind: \"ins\", label: \"9\", ins: \"9\" },\n { kind: \"ins\", label: \"÷\", ins: \"÷\", accent: true, name: \"divide\" },\n { kind: \"ins\", label: \"4\", ins: \"4\" },\n { kind: \"ins\", label: \"5\", ins: \"5\" },\n { kind: \"ins\", label: \"6\", ins: \"6\" },\n { kind: \"ins\", label: \"×\", ins: \"×\", accent: true, name: \"times\" },\n { kind: \"ins\", label: \"1\", ins: \"1\" },\n { kind: \"ins\", label: \"2\", ins: \"2\" },\n { kind: \"ins\", label: \"3\", ins: \"3\" },\n { kind: \"ins\", label: \"−\", ins: \"−\", accent: true, name: \"minus\" },\n { kind: \"ins\", label: \"0\", ins: \"0\" },\n { kind: \"ins\", label: \".\", ins: \".\", name: \"decimal\" },\n { kind: \"back\" },\n { kind: \"ins\", label: \"+\", ins: \"+\", accent: true, name: \"plus\" },\n];\n\nconst KEY_BASE =\n \"flex h-9 items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]\";\n// Two key families, two fills: digits sit on the inset surface and darken on\n// hover, operators sit a step down on `--border` and lift toward that surface —\n// the same split `NumberPadSheet` uses for this keypad on a phone. The tokens\n// flip with the theme, so neither needs a `dark:` twin.\nconst KEY_DIGIT =\n \"bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\";\nconst KEY_ACCENT =\n \"bg-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--bg-surface-2)]\";\n\nfunction seed(initial: string): string {\n const t = initial.trim();\n return t === \"-\" ? \"\" : t;\n}\n\nfunction CalculatorPanel({\n initial,\n onChange,\n labels,\n}: {\n initial: string;\n onChange: (value: string, expression?: string) => void;\n labels: CalculatorLabels;\n}) {\n const [expr, setExpr] = useState(() => seed(initial));\n const inputRef = useRef<HTMLInputElement>(null);\n // What the pad SHOWS. Normally the expression it holds — but when the host owns\n // the sign of the figure, the magnitude it hands back is not the whole number:\n // type 50 into a pad opened on an outflow and the host answers \"-50\". Derived\n // rather than stored, so there is no echo to guard against and a host that\n // ignores `onChange` entirely (a static `value`) still sees its own keystrokes:\n // the pad only defers when the host's text is character-for-character its own\n // last number with a minus in front.\n const host = splitLeadingSign(initial.trim());\n const text = host.sign === \"-\" && isBareAmount(expr) && host.rest === expr.trim() ? initial.trim() : expr;\n const result = evaluateExpression(text);\n\n // Focus the display on open so the user can type or hit Enter immediately.\n useEffect(() => {\n inputRef.current?.focus();\n }, []);\n\n // Update the host field whenever the expression resolves to a number; an\n // incomplete expression (e.g. \"12+\") simply leaves the field untouched. The\n // expression rides along with the result — see `onChange` above.\n const apply = (next: string) => {\n setExpr(next);\n const n = evaluateExpression(next);\n if (n !== null) onChange(formatResult(n), next);\n };\n\n const clearAll = () => {\n setExpr(\"\");\n onChange(\"\");\n };\n\n const equals = () => {\n if (result === null) return;\n const out = formatResult(result);\n setExpr(out);\n onChange(out, text);\n };\n\n return (\n <div className=\"space-y-2\">\n <input\n ref={inputRef}\n aria-label={labels.calculation}\n value={text}\n inputMode=\"decimal\"\n onChange={(e) => apply(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n equals();\n }\n }}\n // Arithmetic reads left to right in every script, and a calculator display\n // is right-aligned in every script too — so both stay physical, pinned\n // with `dir=\"ltr\"` rather than mirrored in a right-to-left form.\n dir=\"ltr\"\n className=\"block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 text-right font-mono text-sm text-[var(--text-primary)] focus:border-[var(--border-strong)] focus:ring-[var(--border-strong)]\"\n />\n <div dir=\"ltr\" className=\"h-4 pr-1 text-right font-mono text-xs text-[var(--text-placeholder)]\">\n {result !== null && formatResult(result) !== text.trim() ? `= ${formatResult(result)}` : \"\"}\n </div>\n <div className=\"grid grid-cols-4 gap-1.5\">\n {KEYS.map((key, i) =>\n key.kind === \"back\" ? (\n <button\n key=\"back\"\n type=\"button\"\n aria-label={labels.backspace}\n onClick={() => apply(text.slice(0, -1))}\n className={cn(KEY_BASE, KEY_ACCENT)}\n >\n <Delete className=\"size-4\" />\n </button>\n ) : (\n <button\n key={`${key.ins}-${i}`}\n type=\"button\"\n aria-label={key.name ? labels[key.name] : undefined}\n onClick={() => apply(text + key.ins)}\n className={cn(KEY_BASE, key.accent ? KEY_ACCENT : KEY_DIGIT)}\n >\n {key.label}\n </button>\n ),\n )}\n <button\n type=\"button\"\n // \"C\" is a convention, not a name: read aloud it is the letter.\n aria-label={labels.clear}\n onClick={clearAll}\n className={cn(KEY_BASE, \"col-span-2 bg-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--bg-surface-2)]\")}\n >\n C\n </button>\n <button\n type=\"button\"\n aria-label={labels.equals}\n onClick={equals}\n className={cn(\n KEY_BASE,\n // The one chip that inverts against the page. There is no hover token for\n // an inverted fill, so the hover is the chip mixed back toward the\n // surface — a step quieter in either theme, and still a colour, so\n // KEY_BASE's `transition-colors` carries it.\n \"col-span-2 bg-[var(--bg-inverse)] text-[var(--text-inverse)] hover:bg-[color-mix(in_oklab,var(--bg-inverse)_88%,var(--bg-surface))]\",\n )}\n >\n =\n </button>\n </div>\n </div>\n );\n}\n\n/**\n * A calculator icon that opens a small keypad popover anchored to its host\n * field. Designed as a trailing-edge adornment: render it inside the field's\n * `relative` wrapper, positioned absolutely. The keypad seeds from the current\n * field value and writes evaluated results straight back through `onChange`.\n */\nexport function CalculatorButton({\n value,\n onChange,\n className,\n ariaLabel,\n labels: labelsProp,\n}: CalculatorButtonProps) {\n // prop > provider > English, with the older `ariaLabel` prop folded in as the\n // `open` key it has always been.\n const fromProps = useMemo(\n () => (ariaLabel === undefined ? labelsProp : { ...labelsProp, open: ariaLabel }),\n [ariaLabel, labelsProp],\n );\n const labels = useKitLabels(\"calculator\", DEFAULT_CALCULATOR_LABELS, fromProps);\n return (\n <Popover\n width={224}\n labels={{ panel: labels.panel }}\n trigger={({ open, toggle, ref }) => (\n <button\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n onClick={toggle}\n aria-label={labels.open}\n aria-expanded={open}\n className={cn(\n \"flex items-center justify-center text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)]\",\n className,\n )}\n >\n <CalculatorIcon className=\"size-4\" />\n </button>\n )}\n >\n {() => <CalculatorPanel initial={value} onChange={onChange} labels={labels} />}\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/calculator.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Calculator as CalculatorIcon, Delete } from \"lucide-react\";\nimport { Popover } from \"./popover\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_TOUCH_TEXT } from \"./ui\";\nimport { evaluateExpression, formatResult, isBareAmount, splitLeadingSign } from \"../lib/calc\";\nimport {\n DEFAULT_CALCULATOR_LABELS,\n useKitLabels,\n type CalculatorLabels,\n} from \"../i18n/kit-labels\";\n\n/** Screen-reader names for the calculator panel's own controls — any part of the\n * kit's `calculator` namespace. Once three optional keys of its own; widened, not\n * replaced, so every object that type-checked before still does. */\nexport type CalculatorButtonLabels = Partial<CalculatorLabels>;\n\ninterface CalculatorButtonProps {\n /** Current field text — seeds the keypad so the user can keep calculating\n * from what's already entered. */\n value: string;\n /**\n * Called with the evaluated result whenever the running expression resolves\n * to a number, so the host field tracks the keypad live.\n *\n * `expression` is the TEXT that result came out of — `\"-42+50\"` for an 8 — and\n * is omitted when there was no calculation (a cleared pad). A host that keeps\n * the sign of its figure OUTSIDE the text (see `AmountInput`'s `negative` /\n * `onNegativeChange`, where a direction toggle owns it) cannot otherwise tell\n * that 8 from a freshly typed one: the first is a sum that came out positive\n * and has to take the direction with it, the second is a magnitude that must\n * leave the direction alone. Hosts whose text carries its own sign can take\n * the one argument and ignore this.\n */\n onChange: (value: string, expression?: string) => void;\n className?: string;\n /** The trigger's accessible name; wins over `calculator.open` from the provider. */\n ariaLabel?: string;\n /** Screen-reader names for the panel's own controls. `ariaLabel` above already\n * covers the trigger; these three were hardcoded English inside the popover,\n * which a translating host had no way to reach. */\n labels?: CalculatorButtonLabels;\n}\n\n/** Keypad layout (4 columns). The final C/= row is rendered separately. `name` is\n * the key of the {@link CalculatorLabels} entry that names a non-digit key; a digit\n * is named by its own glyph. */\ntype Key =\n | { kind: \"ins\"; label: string; ins: string; accent?: boolean; name?: KeyName }\n | { kind: \"back\" };\n\ntype KeyName = \"plus\" | \"minus\" | \"times\" | \"divide\" | \"decimal\";\n\nconst KEYS: Key[] = [\n { kind: \"ins\", label: \"7\", ins: \"7\" },\n { kind: \"ins\", label: \"8\", ins: \"8\" },\n { kind: \"ins\", label: \"9\", ins: \"9\" },\n { kind: \"ins\", label: \"÷\", ins: \"÷\", accent: true, name: \"divide\" },\n { kind: \"ins\", label: \"4\", ins: \"4\" },\n { kind: \"ins\", label: \"5\", ins: \"5\" },\n { kind: \"ins\", label: \"6\", ins: \"6\" },\n { kind: \"ins\", label: \"×\", ins: \"×\", accent: true, name: \"times\" },\n { kind: \"ins\", label: \"1\", ins: \"1\" },\n { kind: \"ins\", label: \"2\", ins: \"2\" },\n { kind: \"ins\", label: \"3\", ins: \"3\" },\n { kind: \"ins\", label: \"−\", ins: \"−\", accent: true, name: \"minus\" },\n { kind: \"ins\", label: \"0\", ins: \"0\" },\n { kind: \"ins\", label: \".\", ins: \".\", name: \"decimal\" },\n { kind: \"back\" },\n { kind: \"ins\", label: \"+\", ins: \"+\", accent: true, name: \"plus\" },\n];\n\nconst KEY_BASE =\n \"flex h-9 items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-[length:var(--focus-ring-width)] focus:ring-[var(--border-strong)]\";\n// Two key families, two fills: digits sit on the inset surface and darken on\n// hover, operators sit a step down on `--border` and lift toward that surface —\n// the same split `NumberPadSheet` uses for this keypad on a phone. The tokens\n// flip with the theme, so neither needs a `dark:` twin.\nconst KEY_DIGIT =\n \"bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\";\nconst KEY_ACCENT =\n \"bg-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--bg-surface-2)]\";\n\nfunction seed(initial: string): string {\n const t = initial.trim();\n return t === \"-\" ? \"\" : t;\n}\n\nfunction CalculatorPanel({\n initial,\n onChange,\n labels,\n}: {\n initial: string;\n onChange: (value: string, expression?: string) => void;\n labels: CalculatorLabels;\n}) {\n const [expr, setExpr] = useState(() => seed(initial));\n const inputRef = useRef<HTMLInputElement>(null);\n // What the pad SHOWS. Normally the expression it holds — but when the host owns\n // the sign of the figure, the magnitude it hands back is not the whole number:\n // type 50 into a pad opened on an outflow and the host answers \"-50\". Derived\n // rather than stored, so there is no echo to guard against and a host that\n // ignores `onChange` entirely (a static `value`) still sees its own keystrokes:\n // the pad only defers when the host's text is character-for-character its own\n // last number with a minus in front.\n const host = splitLeadingSign(initial.trim());\n const text = host.sign === \"-\" && isBareAmount(expr) && host.rest === expr.trim() ? initial.trim() : expr;\n const result = evaluateExpression(text);\n\n // Focus the display on open so the user can type or hit Enter immediately.\n useEffect(() => {\n inputRef.current?.focus();\n }, []);\n\n // Update the host field whenever the expression resolves to a number; an\n // incomplete expression (e.g. \"12+\") simply leaves the field untouched. The\n // expression rides along with the result — see `onChange` above.\n const apply = (next: string) => {\n setExpr(next);\n const n = evaluateExpression(next);\n if (n !== null) onChange(formatResult(n), next);\n };\n\n const clearAll = () => {\n setExpr(\"\");\n onChange(\"\");\n };\n\n const equals = () => {\n if (result === null) return;\n const out = formatResult(result);\n setExpr(out);\n onChange(out, text);\n };\n\n return (\n <div className=\"space-y-2\">\n <input\n ref={inputRef}\n aria-label={labels.calculation}\n value={text}\n inputMode=\"decimal\"\n onChange={(e) => apply(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n equals();\n }\n }}\n // Arithmetic reads left to right in every script, and a calculator display\n // is right-aligned in every script too — so both stay physical, pinned\n // with `dir=\"ltr\"` rather than mirrored in a right-to-left form.\n dir=\"ltr\"\n className={cn(\"block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 text-right font-mono text-sm text-[var(--text-primary)] focus:border-[var(--border-strong)] focus:ring-[var(--border-strong)]\", FIELD_TOUCH_TEXT)}\n />\n <div dir=\"ltr\" className=\"h-4 pr-1 text-right font-mono text-xs text-[var(--text-placeholder)]\">\n {result !== null && formatResult(result) !== text.trim() ? `= ${formatResult(result)}` : \"\"}\n </div>\n <div className=\"grid grid-cols-4 gap-1.5\">\n {KEYS.map((key, i) =>\n key.kind === \"back\" ? (\n <button\n key=\"back\"\n type=\"button\"\n aria-label={labels.backspace}\n onClick={() => apply(text.slice(0, -1))}\n className={cn(KEY_BASE, KEY_ACCENT)}\n >\n <Delete className=\"size-4\" />\n </button>\n ) : (\n <button\n key={`${key.ins}-${i}`}\n type=\"button\"\n aria-label={key.name ? labels[key.name] : undefined}\n onClick={() => apply(text + key.ins)}\n className={cn(KEY_BASE, key.accent ? KEY_ACCENT : KEY_DIGIT)}\n >\n {key.label}\n </button>\n ),\n )}\n <button\n type=\"button\"\n // \"C\" is a convention, not a name: read aloud it is the letter.\n aria-label={labels.clear}\n onClick={clearAll}\n className={cn(KEY_BASE, \"col-span-2 bg-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--bg-surface-2)]\")}\n >\n C\n </button>\n <button\n type=\"button\"\n aria-label={labels.equals}\n onClick={equals}\n className={cn(\n KEY_BASE,\n // The one chip that inverts against the page. There is no hover token for\n // an inverted fill, so the hover is the chip mixed back toward the\n // surface — a step quieter in either theme, and still a colour, so\n // KEY_BASE's `transition-colors` carries it.\n \"col-span-2 bg-[var(--bg-inverse)] text-[var(--text-inverse)] hover:bg-[color-mix(in_oklab,var(--bg-inverse)_88%,var(--bg-surface))]\",\n )}\n >\n =\n </button>\n </div>\n </div>\n );\n}\n\n/**\n * A calculator icon that opens a small keypad popover anchored to its host\n * field. Designed as a trailing-edge adornment: render it inside the field's\n * `relative` wrapper, positioned absolutely. The keypad seeds from the current\n * field value and writes evaluated results straight back through `onChange`.\n */\nexport function CalculatorButton({\n value,\n onChange,\n className,\n ariaLabel,\n labels: labelsProp,\n}: CalculatorButtonProps) {\n // prop > provider > English, with the older `ariaLabel` prop folded in as the\n // `open` key it has always been.\n const fromProps = useMemo(\n () => (ariaLabel === undefined ? labelsProp : { ...labelsProp, open: ariaLabel }),\n [ariaLabel, labelsProp],\n );\n const labels = useKitLabels(\"calculator\", DEFAULT_CALCULATOR_LABELS, fromProps);\n return (\n <Popover\n width=\"14rem\"\n labels={{ panel: labels.panel }}\n trigger={({ open, toggle, ref }) => (\n <button\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n onClick={toggle}\n aria-label={labels.open}\n aria-expanded={open}\n className={cn(\n \"flex items-center justify-center text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)]\",\n className,\n )}\n >\n <CalculatorIcon className=\"size-4\" />\n </button>\n )}\n >\n {() => <CalculatorPanel initial={value} onChange={onChange} labels={labels} />}\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -252,7 +252,7 @@ function CalendarHeatmap({
|
|
|
252
252
|
onSelect && "cursor-pointer hover:ring-1 hover:ring-[var(--brand)]"
|
|
253
253
|
)
|
|
254
254
|
};
|
|
255
|
-
const inner = withNumber ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-
|
|
255
|
+
const inner = withNumber ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-caption leading-none tabular-nums", children: numberFormat.format(d.getDate()) }) : null;
|
|
256
256
|
return /* @__PURE__ */ jsx(Tooltip, { label, portal: true, redact: sensitive, role: "none", className: wrapClass, children: onSelect ? /* @__PURE__ */ jsx("button", { type: "button", ...shared, onClick: () => onSelect(iso), children: inner }) : /* @__PURE__ */ jsx("div", { ...shared, children: inner }) }, iso);
|
|
257
257
|
};
|
|
258
258
|
const gridName = ariaLabel ?? labels.grid;
|
|
@@ -270,7 +270,7 @@ function CalendarHeatmap({
|
|
|
270
270
|
return "";
|
|
271
271
|
});
|
|
272
272
|
body = /* @__PURE__ */ jsx("div", { ref: scrollerRef, className: "overflow-x-auto pb-1", children: /* @__PURE__ */ jsxs("div", { ref: gridRef, role: "grid", "aria-label": gridName, className: "inline-grid gap-0.5", children: [
|
|
273
|
-
/* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "grid gap-0.5 text-
|
|
273
|
+
/* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "grid gap-0.5 text-micro leading-3 text-[var(--text-muted)]", style: columns, children: [
|
|
274
274
|
/* @__PURE__ */ jsx("span", {}),
|
|
275
275
|
monthLabels.map((m, w) => /* @__PURE__ */ jsx("span", { className: "overflow-visible whitespace-nowrap", children: m }, w))
|
|
276
276
|
] }),
|
|
@@ -280,7 +280,7 @@ function CalendarHeatmap({
|
|
|
280
280
|
{
|
|
281
281
|
role: "rowheader",
|
|
282
282
|
"aria-label": weekdayLabels[r].long,
|
|
283
|
-
className: "overflow-hidden pe-1 text-
|
|
283
|
+
className: "overflow-hidden pe-1 text-micro leading-3 text-[var(--text-muted)]",
|
|
284
284
|
children: r % 2 === 1 ? weekdayLabels[r].short : ""
|
|
285
285
|
}
|
|
286
286
|
),
|
|
@@ -298,7 +298,7 @@ function CalendarHeatmap({
|
|
|
298
298
|
{
|
|
299
299
|
role: "columnheader",
|
|
300
300
|
"aria-label": w.long,
|
|
301
|
-
className: "pb-0.5 text-center text-
|
|
301
|
+
className: "pb-0.5 text-center text-micro uppercase tracking-wide text-[var(--text-muted)]",
|
|
302
302
|
children: w.short
|
|
303
303
|
},
|
|
304
304
|
i
|
|
@@ -312,7 +312,7 @@ function CalendarHeatmap({
|
|
|
312
312
|
return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("min-w-0", className), children: [
|
|
313
313
|
dropped > 0 && /* @__PURE__ */ jsx("div", { className: "mb-2 text-xs text-[var(--text-muted)]", children: labels.truncated(dropped) }),
|
|
314
314
|
body,
|
|
315
|
-
legend && /* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "mt-2 flex items-center justify-end gap-1 text-
|
|
315
|
+
legend && /* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "mt-2 flex items-center justify-end gap-1 text-micro text-[var(--text-muted)]", children: [
|
|
316
316
|
/* @__PURE__ */ jsx("span", { className: "me-0.5", children: labels.less }),
|
|
317
317
|
legendLevels.map((l) => /* @__PURE__ */ jsx(
|
|
318
318
|
"span",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/calendar-heatmap.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, horizontalStep } from \"../lib/direction\";\nimport { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from \"../lib/dates\";\nimport { useKitLabels, useKitLocale, useKitWeekStart } from \"../i18n/kit-labels\";\nimport { lerpHex, textOn } from \"../theme/chart-palette\";\nimport { parseHex } from \"../theme/color\";\nimport { Tooltip } from \"./tooltip\";\nimport type { WeekDay } from \"./mini-calendar\";\n\n/**\n * Every string the heatmap speaks — the `calendarHeatmap` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. Dates and\n * values arrive already formatted (in `locale`, and through `formatValue`), so the\n * functions only arrange them.\n */\nexport interface CalendarHeatmapLabels {\n /** Accessible name of the grid. */\n grid: string;\n /**\n * One day: its accessible name, and the tooltip unless `tooltip` is given. `date`\n * is the whole date in `locale` (\"Monday, 14 September 2026\"), `value` the day's\n * figure through `formatValue`.\n */\n day: (date: string, value: string) => string;\n /** The two ends of the legend's scale. */\n less: string;\n more: string;\n /** `maxDays` cut the window: how many of its earliest days are not drawn. */\n truncated: (count: number) => string;\n}\n\n/** English defaults, exported as the `calendarHeatmap` namespace of `UiKitLabels`. */\nexport const DEFAULT_CALENDAR_HEATMAP_LABELS: CalendarHeatmapLabels = {\n grid: \"Daily values\",\n day: (date, value) => `${date}: ${value}`,\n less: \"Less\",\n more: \"More\",\n truncated: (count) =>\n `Showing the most recent days; ${count} earlier ${count === 1 ? \"day is\" : \"days are\"} not shown.`,\n};\n\n/** One day's figure. Several entries for one date are added up. */\nexport interface CalendarHeatmapDatum {\n /** \"YYYY-MM-DD\". */\n date: string;\n value: number;\n}\n\n/** What `tooltip` is told about the day under the pointer. */\nexport interface CalendarHeatmapDay {\n iso: string;\n date: Date;\n /** The day's figure; `0` when `data` has no entry for it (see `hasData`). */\n value: number;\n hasData: boolean;\n /** `0` = empty … `levels` = the busiest. */\n level: number;\n /** The whole date in `locale`. */\n formattedDate: string;\n /** `value` through `formatValue`. */\n formattedValue: string;\n}\n\n/**\n * `onSelect` is omitted from the `<div>` attributes for the reason `MiniCalendar` gives:\n * the DOM event of that name is a text-selection event, and this one answers with a date.\n */\nexport interface CalendarHeatmapProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onSelect\"> {\n data: readonly CalendarHeatmapDatum[];\n /** The window, inclusive, as \"YYYY-MM-DD\". */\n from: string;\n to: string;\n /**\n * `\"weeks\"` (default): the contribution-graph shape — one COLUMN per week, the\n * weekdays down the side, month names along the top; a year fits in ~53 small\n * squares and scrolls sideways where it does not. `\"month\"`: a 7-column calendar\n * with day numbers in large square cells, for a single month on a phone (switch on\n * `useMediaQuery` and narrow `from`/`to` to the month).\n */\n layout?: \"weeks\" | \"month\";\n /** The day shown as selected (\"YYYY-MM-DD\"). */\n selected?: string;\n /** Makes each day a button: a click (or Enter/Space) selects it — a drill-down into\n * that day's rows, a day panel. Without it the days are focusable but inert. */\n onSelect?: (iso: string) => void;\n /** How a value is written in the tooltip and the day's name. Default: `Intl.NumberFormat`\n * in `locale`. Pass a currency formatter for money. */\n formatValue?: (value: number) => string;\n /** Tooltip content for a day. Default: `labels.day(date, value)`. */\n tooltip?: (day: CalendarHeatmapDay) => ReactNode;\n /**\n * Tag each tooltip `data-private`, so the host's demo-mode rule blurs it (the\n * attribute `Tooltip redact` and `StatTile sensitive` carry). ON by default: a heatmap\n * is almost always the user's own figures, and a missing tag is silent — the grid\n * looks anonymous and the bubble spells the amount out on hover.\n */\n sensitive?: boolean;\n /** The value that reaches the top of the scale. Default: the largest value ON SCREEN,\n * so a peak outside the window does not wash the drawn days out to empty. */\n max?: number;\n /** How many non-empty intensity steps. Default 4. */\n levels?: number;\n /** The scale's colour, as any CSS colour. Default `var(--brand)`; each step is this\n * mixed into `--bg-surface`, so the ramp follows the theme. */\n color?: string;\n /**\n * A CONTINUOUS scale from `colorFrom` (the smallest value) to `colorTo` (the busiest\n * day), instead of `levels` steps of `color` — keksdose's palette-store ramp,\n * `seqLow` → `seqHigh`. Both are needed; `color` is then unused. Two `#rrggbb`\n * colours are interpolated in JS, so the fill is a concrete hex and the day number's\n * ink (in `\"month\"`) is MEASURED against it; anything else (a `var()`) is mixed with\n * `color-mix()`. `levels` still sets `data-level` and the legend's swatch count.\n */\n colorFrom?: string;\n colorTo?: string;\n /**\n * A fill of the caller's own for a day with a value: handed the value and the\n * scale's `max`, returns any CSS colour. Wins over `color` and `colorFrom`/`colorTo`.\n * Also paints the legend, at `max × level / levels`.\n */\n fill?: (value: number, max: number) => string;\n /** The fill of a day with no value (or a value of 0). Default: a wash of the text\n * colour over the surface, which reads as a cell on both themes. */\n emptyColor?: string;\n /**\n * Where the window ENDS — the day `maxDays` counts back from, and the last one drawn:\n *\n * - `\"to\"` (default): on `to`.\n * - `\"today\"`: on today.\n * - `\"latest\"`: on the last day that can carry a figure — today, or a later day that\n * actually has a value. A window running into the future (a report's \"all\" preset\n * ends a year ahead) then spends its cells on the days a person is looking for,\n * not on a year of empty ones.\n * - a `\"YYYY-MM-DD\"`: on that day.\n *\n * Always kept inside `from`…`to`.\n */\n anchor?: \"to\" | \"today\" | \"latest\" | (string & {});\n /**\n * The most days to draw. A longer window keeps its LATEST `maxDays` and says how many\n * it left out (`labels.truncated`) rather than silently ending. Default: no cap.\n */\n maxDays?: number;\n /** Draw the Less … More legend. Default `true`. */\n legend?: boolean;\n /** BCP 47 tag for the dates, month and weekday names and numbers. */\n locale?: string;\n /** `0` = Sunday … `6` = Saturday. Default: the provider's, else the locale's. */\n weekStartsOn?: WeekDay;\n labels?: Partial<CalendarHeatmapLabels>;\n className?: string;\n}\n\nconst DAY_NAME_FORMAT: Intl.DateTimeFormatOptions = {\n weekday: \"long\",\n day: \"numeric\",\n month: \"long\",\n year: \"numeric\",\n};\n\n/** The empty day: a wash of the text colour, like `--bg-hover`, so it reads as a cell\n * on both themes without claiming any value. */\nconst EMPTY_FILL = \"color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface))\";\n\nconst weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;\n\n/** Where the window ends for an {@link CalendarHeatmapProps.anchor}, kept in `[from, to]`.\n * `latestData` is the latest ISO date carrying a value, or `\"\"`. */\nexport function heatmapWindowEnd(\n from: string,\n to: string,\n anchor: string,\n latestData: string,\n today: string = toLocalIso(new Date()),\n): string {\n let end: string;\n if (anchor === \"today\") end = today;\n else if (anchor === \"latest\") end = latestData > today ? latestData : today;\n else if (anchor === \"to\" || !parseIsoDate(anchor)) end = to;\n else end = anchor;\n // ISO dates sort as strings, so the clamp is plain comparison.\n if (end > to) end = to;\n if (end < from) end = from;\n return end;\n}\n\n/** The step a value falls on: 0 for nothing, else 1…levels by its share of `max`. */\nexport function heatmapLevel(value: number, max: number, levels: number): number {\n if (!(value > 0) || !(max > 0) || levels < 1) return 0;\n return Math.min(levels, Math.max(1, Math.ceil((value / max) * levels)));\n}\n\n/** How much of `color` a step mixes into the surface: 20% for the first step, 100% for\n * the last — the floor keeps the smallest value visibly apart from an empty day. */\nfunction levelPercent(level: number, levels: number): number {\n if (levels <= 1) return 100;\n return Math.round(20 + (80 * (level - 1)) / (levels - 1));\n}\n\nfunction levelFill(level: number, levels: number, color: string): string {\n if (level === 0) return EMPTY_FILL;\n return `color-mix(in oklab, ${color} ${levelPercent(level, levels)}%, var(--bg-surface))`;\n}\n\n/**\n * Days as a grid of shaded squares — how much happened on each, at a glance\n * (a contribution graph, a spending calendar).\n *\n * **Every day is a cell you can reach.** The grid is a `role=\"grid\"` with ONE tab\n * stop (the roving tabindex, as in `MiniCalendar`); the arrows move by the grid's own\n * geometry — in `\"weeks\"` ↑/↓ are the previous/next day and ←/→ a week, in `\"month\"`\n * the other way round, mirrored in RTL — PageUp/PageDown move a month and Home/End go\n * to the window's first/last day. Each cell is named with its whole date AND its value,\n * because the colour is not something a screen reader can say, and carries\n * `data-day=\"YYYY-MM-DD\"` as its identity for tests and for the host. Note that in\n * `\"weeks\"` the cells are in DOM order by WEEKDAY ROW (all Mondays, then all\n * Tuesdays…), not by date — query by `data-day`, not by position.\n *\n * The tooltip is portalled (a year of squares always sits in a sideways scroller) and\n * `data-private` unless `sensitive={false}`. Cell size in `\"weeks\"` is the CSS variable\n * `--heatmap-cell` (default `0.75rem`): `style={{ \"--heatmap-cell\": \"1rem\" }}`.\n */\nexport function CalendarHeatmap({\n data,\n from,\n to,\n layout = \"weeks\",\n selected,\n onSelect,\n formatValue: formatValueProp,\n tooltip,\n sensitive = true,\n max: maxProp,\n levels = 4,\n color = \"var(--brand)\",\n colorFrom,\n colorTo,\n fill: fillProp,\n emptyColor,\n anchor = \"to\",\n maxDays,\n legend = true,\n locale: localeProp,\n weekStartsOn,\n labels: labelsProp,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: CalendarHeatmapProps) {\n const labels = useKitLabels(\"calendarHeatmap\", DEFAULT_CALENDAR_HEATMAP_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const providerWeekStart = useKitWeekStart();\n const weekStart: WeekDay = weekStartsOn ?? providerWeekStart ?? localeWeekStart(locale);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const formatValue = formatValueProp ?? ((v: number) => numberFormat.format(v));\n\n const values = useMemo(() => {\n const m = new Map<string, number>();\n for (const p of data) m.set(p.date, (m.get(p.date) ?? 0) + p.value);\n return m;\n }, [data]);\n\n // The window, trimmed from the FRONT: the days a person looks for are the recent\n // ones, and a cap that kept the oldest would end the calendar before today.\n const latestData = useMemo(() => {\n let latest = \"\";\n for (const [iso, v] of values) if (v !== 0 && iso > latest) latest = iso;\n return latest;\n }, [values]);\n const windowEnd = heatmapWindowEnd(from, to, anchor, latestData);\n\n const { days, dropped } = useMemo(() => {\n const start = parseIsoDate(from);\n const end = parseIsoDate(windowEnd);\n const list: Date[] = [];\n if (!start || !end || end < start) return { days: list, dropped: 0 };\n const cursor = new Date(end);\n const cap = maxDays && maxDays > 0 ? maxDays : Infinity;\n while (cursor >= start && list.length < cap) {\n list.unshift(new Date(cursor));\n cursor.setDate(cursor.getDate() - 1);\n }\n const first = list[0];\n const trimmed = first ? Math.round((first.getTime() - start.getTime()) / 86_400_000) : 0;\n return { days: list, dropped: Math.max(0, trimmed) };\n }, [from, windowEnd, maxDays]);\n\n const firstIso = days.length ? toLocalIso(days[0]) : \"\";\n const lastIso = days.length ? toLocalIso(days[days.length - 1]) : \"\";\n\n const max = useMemo(() => {\n if (maxProp !== undefined) return maxProp;\n let m = 0;\n for (const d of days) m = Math.max(m, values.get(toLocalIso(d)) ?? 0);\n return m;\n }, [maxProp, days, values]);\n\n const clampIso = (iso: string) => (iso < firstIso ? firstIso : iso > lastIso ? lastIso : iso);\n\n // The roving tab stop: the selected day, else the latest one.\n const [activeState, setActiveIso] = useState(() => selected || lastIso);\n const [lastSelected, setLastSelected] = useState(selected);\n if (lastSelected !== selected) {\n setLastSelected(selected);\n if (selected) setActiveIso(selected);\n }\n const activeIso = days.length ? clampIso(activeState || lastIso) : \"\";\n\n const gridRef = useRef<HTMLDivElement>(null);\n const scrollerRef = useRef<HTMLDivElement>(null);\n const pendingFocus = useRef<string | null>(null);\n useEffect(() => {\n const iso = pendingFocus.current;\n if (!iso) return;\n pendingFocus.current = null;\n gridRef.current?.querySelector<HTMLElement>(`[data-day=\"${iso}\"]`)?.focus();\n });\n\n // Open the sideways scroller on the latest weeks, not the oldest. In RTL the end is\n // on the left, where browsers count scrollLeft down from 0.\n //\n // And KEEP it there while the box changes size — a sidebar or a contents rail that\n // appears after mount narrows the scroller, which leaves scrollLeft where it was and\n // so shows older weeks than it opened on. The pin holds only while the view sits at\n // the end: a user who scrolls back through the year unpins it, and a resize then\n // leaves their position alone. (Scrolling back to the end pins it again.)\n useLayoutEffect(() => {\n const el = scrollerRef.current;\n if (!el || layout !== \"weeks\") return;\n const toEnd = () => {\n el.scrollLeft = dirOf(el) === \"rtl\" ? -el.scrollWidth : el.scrollWidth;\n };\n toEnd();\n if (typeof ResizeObserver === \"undefined\") return;\n // |scrollLeft| covers both directions: 0 → max in LTR, 0 → -max in RTL.\n let pinned = true;\n const onScroll = () => {\n pinned = Math.abs(el.scrollLeft) >= el.scrollWidth - el.clientWidth - 1;\n };\n const ro = new ResizeObserver(() => {\n if (pinned) toEnd();\n });\n el.addEventListener(\"scroll\", onScroll, { passive: true });\n ro.observe(el);\n return () => {\n ro.disconnect();\n el.removeEventListener(\"scroll\", onScroll);\n };\n }, [layout, firstIso, lastIso]);\n\n const formatDay = (d: Date) => d.toLocaleDateString(locale, DAY_NAME_FORMAT);\n\n const weekdayLabels = useMemo(() => {\n const ref = new Date(2024, 0, 7 + weekStart); // 7 January 2024 was a Sunday\n const week = Array.from({ length: 7 }, (_, i) => {\n const d = new Date(ref);\n d.setDate(ref.getDate() + i);\n return d;\n });\n const name = (d: Date, weekday: \"short\" | \"narrow\" | \"long\") => d.toLocaleDateString(locale, { weekday });\n // The label column is 2rem at 10px type, room for about four characters. Most\n // locales' \"short\" is three or four (\"Thu\", \"jeu.\", \"czw.\"), but some have no\n // abbreviation at all — Arabic's short Thursday IS \"الخميس\", which overran the box\n // and was cut. Such a locale takes the \"narrow\" form (\"خ\") for ALL seven, so the\n // column never mixes two forms; the full name is on the aria-label either way.\n const tooLong = week.some((d) => [...name(d, \"short\")].length > 4);\n return week.map((d) => ({ short: name(d, tooLong ? \"narrow\" : \"short\"), long: name(d, \"long\") }));\n }, [locale, weekStart]);\n\n const lead = days.length ? weekdayIndex(days[0], weekStart) : 0;\n const weekCount = Math.ceil((lead + days.length) / 7);\n /** The day at position `i` of the week-aligned sequence (blanks before the first). */\n const dayAt = (i: number): Date | null => (i >= lead && i - lead < days.length ? days[i - lead] : null);\n\n const move = (e: KeyboardEvent<HTMLElement>) => {\n const current = parseIsoDate(activeIso);\n if (!current) return;\n const next = new Date(current);\n const step = horizontalStep(e.key, e.currentTarget);\n // In \"weeks\" a row is a weekday, so sideways is a week and up/down is a day.\n const across = layout === \"weeks\" ? 7 : 1;\n const down = layout === \"weeks\" ? 1 : 7;\n switch (step ? \"horizontal\" : e.key) {\n case \"horizontal\":\n next.setDate(next.getDate() + step * across);\n break;\n case \"ArrowUp\":\n next.setDate(next.getDate() - down);\n break;\n case \"ArrowDown\":\n next.setDate(next.getDate() + down);\n break;\n case \"PageUp\":\n next.setTime(addMonthsClamped(next, -1).getTime());\n break;\n case \"PageDown\":\n next.setTime(addMonthsClamped(next, 1).getTime());\n break;\n case \"Home\":\n next.setTime(days[0].getTime());\n break;\n case \"End\":\n next.setTime(days[days.length - 1].getTime());\n break;\n default:\n return;\n }\n e.preventDefault();\n const iso = clampIso(toLocalIso(next));\n if (iso === activeIso) return;\n pendingFocus.current = iso;\n setActiveIso(iso);\n };\n\n const legendLevels = Array.from({ length: levels + 1 }, (_, l) => l);\n const empty = emptyColor ?? EMPTY_FILL;\n const continuous = colorFrom !== undefined && colorTo !== undefined;\n const hexRamp = continuous && parseHex(colorFrom) != null && parseHex(colorTo) != null;\n\n /** A day's fill, and the ink its number is set in (`\"month\"`). */\n const paint = (value: number, level: number): { fill: string; ink: string } => {\n if (level === 0) return { fill: empty, ink: \"var(--text-primary)\" };\n if (fillProp || continuous) {\n const t = max > 0 ? Math.min(1, Math.max(0, value / max)) : 1;\n const fill = fillProp\n ? fillProp(value, max)\n : hexRamp\n ? lerpHex(colorFrom!, colorTo!, t)\n : `color-mix(in oklab, ${colorTo} ${Math.round(t * 100)}%, ${colorFrom})`;\n // Measured where the fill is a colour; a mix of references cannot be measured,\n // and the page's own text colour is the safe answer there.\n const measured = textOn(fill);\n return { fill, ink: measured === \"currentColor\" ? \"var(--text-primary)\" : measured };\n }\n // Light text once the fill is the stronger half of the mix — `--bg-surface` is the\n // colour the scale is mixed INTO, so it is the one that contrasts with its far end\n // on either theme.\n const strong = levelPercent(level, levels) >= 45;\n return {\n fill: levelFill(level, levels, color),\n ink: strong ? \"var(--bg-surface)\" : \"var(--text-primary)\",\n };\n };\n // The legend's swatches: a value that lands on each step (the top one on `max`).\n const legendFill = (l: number) => paint(((max > 0 ? max : 1) * l) / levels, l).fill;\n\n const cell = (d: Date, wrapClass: string, cellClass: string, withNumber: boolean) => {\n const iso = toLocalIso(d);\n const raw = values.get(iso);\n const value = raw ?? 0;\n const level = heatmapLevel(value, max, levels);\n const { fill, ink } = paint(value, level);\n const formattedDate = formatDay(d);\n const formattedValue = formatValue(value);\n const name = labels.day(formattedDate, formattedValue);\n const label = tooltip\n ? tooltip({ iso, date: d, value, hasData: raw !== undefined, level, formattedDate, formattedValue })\n : name;\n const isSelected = selected === iso;\n const shared = {\n role: \"gridcell\",\n \"data-day\": iso,\n \"data-level\": level,\n tabIndex: iso === activeIso ? 0 : -1,\n \"aria-label\": name,\n \"aria-selected\": onSelect ? isSelected : undefined,\n onKeyDown: move,\n onFocus: () => {\n if (iso !== activeIso) setActiveIso(iso);\n },\n style: { background: fill, color: ink },\n className: cn(\n cellClass,\n isSelected && \"ring-2 ring-[var(--text-primary)]\",\n onSelect && \"cursor-pointer hover:ring-1 hover:ring-[var(--brand)]\",\n ),\n } as const;\n const inner = withNumber ? (\n <span aria-hidden className=\"text-[11px] leading-none tabular-nums\">\n {numberFormat.format(d.getDate())}\n </span>\n ) : null;\n return (\n <Tooltip key={iso} label={label} portal redact={sensitive} role=\"none\" className={wrapClass}>\n {onSelect ? (\n <button type=\"button\" {...shared} onClick={() => onSelect(iso)}>\n {inner}\n </button>\n ) : (\n <div {...shared}>{inner}</div>\n )}\n </Tooltip>\n );\n };\n\n const gridName = ariaLabel ?? labels.grid;\n let body: ReactNode;\n if (layout === \"weeks\") {\n const columns = { gridTemplateColumns: `var(--heatmap-label, 2rem) repeat(${weekCount}, var(--heatmap-cell, 0.75rem))` } as CSSProperties;\n const cellSize = \"size-[var(--heatmap-cell,0.75rem)]\";\n // A month's name over the first column that holds its 1st — or over the first\n // column, when the window opens far enough before the next month for it to fit.\n const monthLabels = Array.from({ length: weekCount }, (_, w) => {\n for (let r = 0; r < 7; r++) {\n const d = dayAt(w * 7 + r);\n if (d && (d.getDate() === 1 || (w === 0 && r === lead && d.getDate() <= 14))) {\n return d.toLocaleDateString(locale, { month: \"short\" });\n }\n }\n return \"\";\n });\n body = (\n <div ref={scrollerRef} className=\"overflow-x-auto pb-1\">\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"inline-grid gap-0.5\">\n {/* Decoration: every cell's name already carries its month. */}\n <div aria-hidden className=\"grid gap-0.5 text-[10px] leading-3 text-[var(--text-muted)]\" style={columns}>\n <span />\n {monthLabels.map((m, w) => (\n <span key={w} className=\"overflow-visible whitespace-nowrap\">\n {m}\n </span>\n ))}\n </div>\n {Array.from({ length: 7 }, (_, r) => (\n <div key={r} role=\"row\" className=\"grid gap-0.5\" style={columns}>\n <div\n role=\"rowheader\"\n aria-label={weekdayLabels[r].long}\n className=\"overflow-hidden pe-1 text-[10px] leading-3 text-[var(--text-muted)]\"\n >\n {/* Every other weekday, as a contribution graph does: seven labels\n at this size are a smear. The name is there for all seven. */}\n {r % 2 === 1 ? weekdayLabels[r].short : \"\"}\n </div>\n {Array.from({ length: weekCount }, (_, w) => {\n const d = dayAt(w * 7 + r);\n return d ? (\n cell(d, cellSize, cn(cellSize, \"block rounded-sm\"), false)\n ) : (\n <div key={`pad-${w}`} role=\"gridcell\" className={cellSize} />\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n } else {\n const rows = Array.from({ length: weekCount }, (_, w) => w);\n body = (\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"grid gap-1\">\n <div role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {weekdayLabels.map((w, i) => (\n <div\n key={i}\n role=\"columnheader\"\n aria-label={w.long}\n className=\"pb-0.5 text-center text-[10px] uppercase tracking-wide text-[var(--text-muted)]\"\n >\n {w.short}\n </div>\n ))}\n </div>\n {rows.map((w) => (\n <div key={w} role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {Array.from({ length: 7 }, (_, c) => {\n const d = dayAt(w * 7 + c);\n return d ? (\n cell(d, \"aspect-square w-full\", \"flex h-full w-full items-center justify-center rounded\", true)\n ) : (\n <div key={`pad-${c}`} role=\"gridcell\" className=\"aspect-square\" />\n );\n })}\n </div>\n ))}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"min-w-0\", className)}>\n {dropped > 0 && (\n <div className=\"mb-2 text-xs text-[var(--text-muted)]\">{labels.truncated(dropped)}</div>\n )}\n {body}\n {legend && (\n <div aria-hidden className=\"mt-2 flex items-center justify-end gap-1 text-[10px] text-[var(--text-muted)]\">\n <span className=\"me-0.5\">{labels.less}</span>\n {legendLevels.map((l) => (\n <span\n key={l}\n data-legend-level={l}\n className=\"size-2.5 rounded-sm\"\n style={{ background: legendFill(l) }}\n />\n ))}\n <span className=\"ms-0.5\">{labels.more}</span>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";AAgeM,cAqCI,YArCJ;AAheN,SAAS,WAAW,iBAAiB,SAAS,QAAQ,gBAAgB;AAEtE,SAAS,UAAU;AACnB,SAAS,OAAO,sBAAsB;AACtC,SAAS,kBAAkB,iBAAiB,cAAc,kBAAkB;AAC5E,SAAS,cAAc,cAAc,uBAAuB;AAC5D,SAAS,SAAS,cAAc;AAChC,SAAS,gBAAgB;AACzB,SAAS,eAAe;AA0BjB,MAAM,kCAAyD;AAAA,EACpE,MAAM;AAAA,EACN,KAAK,CAAC,MAAM,UAAU,GAAG,IAAI,KAAK,KAAK;AAAA,EACvC,MAAM;AAAA,EACN,MAAM;AAAA,EACN,WAAW,CAAC,UACV,iCAAiC,KAAK,YAAY,UAAU,IAAI,WAAW,UAAU;AACzF;AAkHA,MAAM,kBAA8C;AAAA,EAClD,SAAS;AAAA,EACT,KAAK;AAAA,EACL,OAAO;AAAA,EACP,MAAM;AACR;AAIA,MAAM,aAAa;AAEnB,MAAM,eAAe,CAAC,GAAS,eAAwB,EAAE,OAAO,IAAI,YAAY,KAAK;AAI9E,SAAS,iBACd,MACA,IACA,QACA,YACA,QAAgB,WAAW,oBAAI,KAAK,CAAC,GAC7B;AACR,MAAI;AACJ,MAAI,WAAW,QAAS,OAAM;AAAA,WACrB,WAAW,SAAU,OAAM,aAAa,QAAQ,aAAa;AAAA,WAC7D,WAAW,QAAQ,CAAC,aAAa,MAAM,EAAG,OAAM;AAAA,MACpD,OAAM;AAEX,MAAI,MAAM,GAAI,OAAM;AACpB,MAAI,MAAM,KAAM,OAAM;AACtB,SAAO;AACT;AAGO,SAAS,aAAa,OAAe,KAAa,QAAwB;AAC/E,MAAI,EAAE,QAAQ,MAAM,EAAE,MAAM,MAAM,SAAS,EAAG,QAAO;AACrD,SAAO,KAAK,IAAI,QAAQ,KAAK,IAAI,GAAG,KAAK,KAAM,QAAQ,MAAO,MAAM,CAAC,CAAC;AACxE;AAIA,SAAS,aAAa,OAAe,QAAwB;AAC3D,MAAI,UAAU,EAAG,QAAO;AACxB,SAAO,KAAK,MAAM,KAAM,MAAM,QAAQ,MAAO,SAAS,EAAE;AAC1D;AAEA,SAAS,UAAU,OAAe,QAAgB,OAAuB;AACvE,MAAI,UAAU,EAAG,QAAO;AACxB,SAAO,uBAAuB,KAAK,IAAI,aAAa,OAAO,MAAM,CAAC;AACpE;AAoBO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA,YAAY;AAAA,EACZ,KAAK;AAAA,EACL,SAAS;AAAA,EACT,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,SAAS;AAAA,EACT,QAAQ;AAAA,EACR;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAyB;AACvB,QAAM,SAAS,aAAa,mBAAmB,iCAAiC,UAAU;AAC1F,QAAM,SAAS,aAAa,UAAU;AACtC,QAAM,oBAAoB,gBAAgB;AAC1C,QAAM,YAAqB,gBAAgB,qBAAqB,gBAAgB,MAAM;AACtF,QAAM,eAAe,QAAQ,MAAM,IAAI,KAAK,aAAa,MAAM,GAAG,CAAC,MAAM,CAAC;AAC1E,QAAM,cAAc,oBAAoB,CAAC,MAAc,aAAa,OAAO,CAAC;AAE5E,QAAM,SAAS,QAAQ,MAAM;AAC3B,UAAM,IAAI,oBAAI,IAAoB;AAClC,eAAW,KAAK,KAAM,GAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,KAAK,KAAK,EAAE,KAAK;AAClE,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,CAAC;AAIT,QAAM,aAAa,QAAQ,MAAM;AAC/B,QAAI,SAAS;AACb,eAAW,CAAC,KAAK,CAAC,KAAK,OAAQ,KAAI,MAAM,KAAK,MAAM,OAAQ,UAAS;AACrE,WAAO;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AACX,QAAM,YAAY,iBAAiB,MAAM,IAAI,QAAQ,UAAU;AAE/D,QAAM,EAAE,MAAM,QAAQ,IAAI,QAAQ,MAAM;AACtC,UAAM,QAAQ,aAAa,IAAI;AAC/B,UAAM,MAAM,aAAa,SAAS;AAClC,UAAM,OAAe,CAAC;AACtB,QAAI,CAAC,SAAS,CAAC,OAAO,MAAM,MAAO,QAAO,EAAE,MAAM,MAAM,SAAS,EAAE;AACnE,UAAM,SAAS,IAAI,KAAK,GAAG;AAC3B,UAAM,MAAM,WAAW,UAAU,IAAI,UAAU;AAC/C,WAAO,UAAU,SAAS,KAAK,SAAS,KAAK;AAC3C,WAAK,QAAQ,IAAI,KAAK,MAAM,CAAC;AAC7B,aAAO,QAAQ,OAAO,QAAQ,IAAI,CAAC;AAAA,IACrC;AACA,UAAM,QAAQ,KAAK,CAAC;AACpB,UAAM,UAAU,QAAQ,KAAK,OAAO,MAAM,QAAQ,IAAI,MAAM,QAAQ,KAAK,KAAU,IAAI;AACvF,WAAO,EAAE,MAAM,MAAM,SAAS,KAAK,IAAI,GAAG,OAAO,EAAE;AAAA,EACrD,GAAG,CAAC,MAAM,WAAW,OAAO,CAAC;AAE7B,QAAM,WAAW,KAAK,SAAS,WAAW,KAAK,CAAC,CAAC,IAAI;AACrD,QAAM,UAAU,KAAK,SAAS,WAAW,KAAK,KAAK,SAAS,CAAC,CAAC,IAAI;AAElE,QAAM,MAAM,QAAQ,MAAM;AACxB,QAAI,YAAY,OAAW,QAAO;AAClC,QAAI,IAAI;AACR,eAAW,KAAK,KAAM,KAAI,KAAK,IAAI,GAAG,OAAO,IAAI,WAAW,CAAC,CAAC,KAAK,CAAC;AACpE,WAAO;AAAA,EACT,GAAG,CAAC,SAAS,MAAM,MAAM,CAAC;AAE1B,QAAM,WAAW,CAAC,QAAiB,MAAM,WAAW,WAAW,MAAM,UAAU,UAAU;AAGzF,QAAM,CAAC,aAAa,YAAY,IAAI,SAAS,MAAM,YAAY,OAAO;AACtE,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,QAAQ;AACzD,MAAI,iBAAiB,UAAU;AAC7B,oBAAgB,QAAQ;AACxB,QAAI,SAAU,cAAa,QAAQ;AAAA,EACrC;AACA,QAAM,YAAY,KAAK,SAAS,SAAS,eAAe,OAAO,IAAI;AAEnE,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,cAAc,OAAuB,IAAI;AAC/C,QAAM,eAAe,OAAsB,IAAI;AAC/C,YAAU,MAAM;AACd,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,IAAK;AACV,iBAAa,UAAU;AACvB,YAAQ,SAAS,cAA2B,cAAc,GAAG,IAAI,GAAG,MAAM;AAAA,EAC5E,CAAC;AAUD,kBAAgB,MAAM;AACpB,UAAM,KAAK,YAAY;AACvB,QAAI,CAAC,MAAM,WAAW,QAAS;AAC/B,UAAM,QAAQ,MAAM;AAClB,SAAG,aAAa,MAAM,EAAE,MAAM,QAAQ,CAAC,GAAG,cAAc,GAAG;AAAA,IAC7D;AACA,UAAM;AACN,QAAI,OAAO,mBAAmB,YAAa;AAE3C,QAAI,SAAS;AACb,UAAM,WAAW,MAAM;AACrB,eAAS,KAAK,IAAI,GAAG,UAAU,KAAK,GAAG,cAAc,GAAG,cAAc;AAAA,IACxE;AACA,UAAM,KAAK,IAAI,eAAe,MAAM;AAClC,UAAI,OAAQ,OAAM;AAAA,IACpB,CAAC;AACD,OAAG,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;AACzD,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM;AACX,SAAG,WAAW;AACd,SAAG,oBAAoB,UAAU,QAAQ;AAAA,IAC3C;AAAA,EACF,GAAG,CAAC,QAAQ,UAAU,OAAO,CAAC;AAE9B,QAAM,YAAY,CAAC,MAAY,EAAE,mBAAmB,QAAQ,eAAe;AAE3E,QAAM,gBAAgB,QAAQ,MAAM;AAClC,UAAM,MAAM,IAAI,KAAK,MAAM,GAAG,IAAI,SAAS;AAC3C,UAAM,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,MAAM;AAC/C,YAAM,IAAI,IAAI,KAAK,GAAG;AACtB,QAAE,QAAQ,IAAI,QAAQ,IAAI,CAAC;AAC3B,aAAO;AAAA,IACT,CAAC;AACD,UAAM,OAAO,CAAC,GAAS,YAAyC,EAAE,mBAAmB,QAAQ,EAAE,QAAQ,CAAC;AAMxG,UAAM,UAAU,KAAK,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,GAAG,OAAO,CAAC,EAAE,SAAS,CAAC;AACjE,WAAO,KAAK,IAAI,CAAC,OAAO,EAAE,OAAO,KAAK,GAAG,UAAU,WAAW,OAAO,GAAG,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE;AAAA,EAClG,GAAG,CAAC,QAAQ,SAAS,CAAC;AAEtB,QAAM,OAAO,KAAK,SAAS,aAAa,KAAK,CAAC,GAAG,SAAS,IAAI;AAC9D,QAAM,YAAY,KAAK,MAAM,OAAO,KAAK,UAAU,CAAC;AAEpD,QAAM,QAAQ,CAAC,MAA4B,KAAK,QAAQ,IAAI,OAAO,KAAK,SAAS,KAAK,IAAI,IAAI,IAAI;AAElG,QAAM,OAAO,CAAC,MAAkC;AAC9C,UAAM,UAAU,aAAa,SAAS;AACtC,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,IAAI,KAAK,OAAO;AAC7B,UAAM,OAAO,eAAe,EAAE,KAAK,EAAE,aAAa;AAElD,UAAM,SAAS,WAAW,UAAU,IAAI;AACxC,UAAM,OAAO,WAAW,UAAU,IAAI;AACtC,YAAQ,OAAO,eAAe,EAAE,KAAK;AAAA,MACnC,KAAK;AACH,aAAK,QAAQ,KAAK,QAAQ,IAAI,OAAO,MAAM;AAC3C;AAAA,MACF,KAAK;AACH,aAAK,QAAQ,KAAK,QAAQ,IAAI,IAAI;AAClC;AAAA,MACF,KAAK;AACH,aAAK,QAAQ,KAAK,QAAQ,IAAI,IAAI;AAClC;AAAA,MACF,KAAK;AACH,aAAK,QAAQ,iBAAiB,MAAM,EAAE,EAAE,QAAQ,CAAC;AACjD;AAAA,MACF,KAAK;AACH,aAAK,QAAQ,iBAAiB,MAAM,CAAC,EAAE,QAAQ,CAAC;AAChD;AAAA,MACF,KAAK;AACH,aAAK,QAAQ,KAAK,CAAC,EAAE,QAAQ,CAAC;AAC9B;AAAA,MACF,KAAK;AACH,aAAK,QAAQ,KAAK,KAAK,SAAS,CAAC,EAAE,QAAQ,CAAC;AAC5C;AAAA,MACF;AACE;AAAA,IACJ;AACA,MAAE,eAAe;AACjB,UAAM,MAAM,SAAS,WAAW,IAAI,CAAC;AACrC,QAAI,QAAQ,UAAW;AACvB,iBAAa,UAAU;AACvB,iBAAa,GAAG;AAAA,EAClB;AAEA,QAAM,eAAe,MAAM,KAAK,EAAE,QAAQ,SAAS,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC;AACnE,QAAM,QAAQ,cAAc;AAC5B,QAAM,aAAa,cAAc,UAAa,YAAY;AAC1D,QAAM,UAAU,cAAc,SAAS,SAAS,KAAK,QAAQ,SAAS,OAAO,KAAK;AAGlF,QAAM,QAAQ,CAAC,OAAe,UAAiD;AAC7E,QAAI,UAAU,EAAG,QAAO,EAAE,MAAM,OAAO,KAAK,sBAAsB;AAClE,QAAI,YAAY,YAAY;AAC1B,YAAM,IAAI,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,GAAG,CAAC,IAAI;AAC5D,YAAM,OAAO,WACT,SAAS,OAAO,GAAG,IACnB,UACE,QAAQ,WAAY,SAAU,CAAC,IAC/B,uBAAuB,OAAO,IAAI,KAAK,MAAM,IAAI,GAAG,CAAC,MAAM,SAAS;AAG1E,YAAM,WAAW,OAAO,IAAI;AAC5B,aAAO,EAAE,MAAM,KAAK,aAAa,iBAAiB,wBAAwB,SAAS;AAAA,IACrF;AAIA,UAAM,SAAS,aAAa,OAAO,MAAM,KAAK;AAC9C,WAAO;AAAA,MACL,MAAM,UAAU,OAAO,QAAQ,KAAK;AAAA,MACpC,KAAK,SAAS,sBAAsB;AAAA,IACtC;AAAA,EACF;AAEA,QAAM,aAAa,CAAC,MAAc,OAAQ,MAAM,IAAI,MAAM,KAAK,IAAK,QAAQ,CAAC,EAAE;AAE/E,QAAM,OAAO,CAAC,GAAS,WAAmB,WAAmB,eAAwB;AACnF,UAAM,MAAM,WAAW,CAAC;AACxB,UAAM,MAAM,OAAO,IAAI,GAAG;AAC1B,UAAM,QAAQ,OAAO;AACrB,UAAM,QAAQ,aAAa,OAAO,KAAK,MAAM;AAC7C,UAAM,EAAE,MAAM,IAAI,IAAI,MAAM,OAAO,KAAK;AACxC,UAAM,gBAAgB,UAAU,CAAC;AACjC,UAAM,iBAAiB,YAAY,KAAK;AACxC,UAAM,OAAO,OAAO,IAAI,eAAe,cAAc;AACrD,UAAM,QAAQ,UACV,QAAQ,EAAE,KAAK,MAAM,GAAG,OAAO,SAAS,QAAQ,QAAW,OAAO,eAAe,eAAe,CAAC,IACjG;AACJ,UAAM,aAAa,aAAa;AAChC,UAAM,SAAS;AAAA,MACb,MAAM;AAAA,MACN,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,UAAU,QAAQ,YAAY,IAAI;AAAA,MAClC,cAAc;AAAA,MACd,iBAAiB,WAAW,aAAa;AAAA,MACzC,WAAW;AAAA,MACX,SAAS,MAAM;AACb,YAAI,QAAQ,UAAW,cAAa,GAAG;AAAA,MACzC;AAAA,MACA,OAAO,EAAE,YAAY,MAAM,OAAO,IAAI;AAAA,MACtC,WAAW;AAAA,QACT;AAAA,QACA,cAAc;AAAA,QACd,YAAY;AAAA,MACd;AAAA,IACF;AACA,UAAM,QAAQ,aACZ,oBAAC,UAAK,eAAW,MAAC,WAAU,yCACzB,uBAAa,OAAO,EAAE,QAAQ,CAAC,GAClC,IACE;AACJ,WACE,oBAAC,WAAkB,OAAc,QAAM,MAAC,QAAQ,WAAW,MAAK,QAAO,WAAW,WAC/E,qBACC,oBAAC,YAAO,MAAK,UAAU,GAAG,QAAQ,SAAS,MAAM,SAAS,GAAG,GAC1D,iBACH,IAEA,oBAAC,SAAK,GAAG,QAAS,iBAAM,KANd,GAQd;AAAA,EAEJ;AAEA,QAAM,WAAW,aAAa,OAAO;AACrC,MAAI;AACJ,MAAI,WAAW,SAAS;AACtB,UAAM,UAAU,EAAE,qBAAqB,qCAAqC,SAAS,kCAAkC;AACvH,UAAM,WAAW;AAGjB,UAAM,cAAc,MAAM,KAAK,EAAE,QAAQ,UAAU,GAAG,CAAC,GAAG,MAAM;AAC9D,eAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,cAAM,IAAI,MAAM,IAAI,IAAI,CAAC;AACzB,YAAI,MAAM,EAAE,QAAQ,MAAM,KAAM,MAAM,KAAK,MAAM,QAAQ,EAAE,QAAQ,KAAK,KAAM;AAC5E,iBAAO,EAAE,mBAAmB,QAAQ,EAAE,OAAO,QAAQ,CAAC;AAAA,QACxD;AAAA,MACF;AACA,aAAO;AAAA,IACT,CAAC;AACD,WACE,oBAAC,SAAI,KAAK,aAAa,WAAU,wBAC/B,+BAAC,SAAI,KAAK,SAAS,MAAK,QAAO,cAAY,UAAU,WAAU,uBAE7D;AAAA,2BAAC,SAAI,eAAW,MAAC,WAAU,+DAA8D,OAAO,SAC9F;AAAA,4BAAC,UAAK;AAAA,QACL,YAAY,IAAI,CAAC,GAAG,MACnB,oBAAC,UAAa,WAAU,sCACrB,eADQ,CAEX,CACD;AAAA,SACH;AAAA,MACC,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,MAC7B,qBAAC,SAAY,MAAK,OAAM,WAAU,gBAAe,OAAO,SACtD;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY,cAAc,CAAC,EAAE;AAAA,YAC7B,WAAU;AAAA,YAIT,cAAI,MAAM,IAAI,cAAc,CAAC,EAAE,QAAQ;AAAA;AAAA,QAC1C;AAAA,QACC,MAAM,KAAK,EAAE,QAAQ,UAAU,GAAG,CAACA,IAAG,MAAM;AAC3C,gBAAM,IAAI,MAAM,IAAI,IAAI,CAAC;AACzB,iBAAO,IACL,KAAK,GAAG,UAAU,GAAG,UAAU,kBAAkB,GAAG,KAAK,IAEzD,oBAAC,SAAqB,MAAK,YAAW,WAAW,YAAvC,OAAO,CAAC,EAAyC;AAAA,QAE/D,CAAC;AAAA,WAjBO,CAkBV,CACD;AAAA,OACH,GACF;AAAA,EAEJ,OAAO;AACL,UAAM,OAAO,MAAM,KAAK,EAAE,QAAQ,UAAU,GAAG,CAAC,GAAG,MAAM,CAAC;AAC1D,WACE,qBAAC,SAAI,KAAK,SAAS,MAAK,QAAO,cAAY,UAAU,WAAU,cAC7D;AAAA,0BAAC,SAAI,MAAK,OAAM,WAAU,0BACvB,wBAAc,IAAI,CAAC,GAAG,MACrB;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,cAAY,EAAE;AAAA,UACd,WAAU;AAAA,UAET,YAAE;AAAA;AAAA,QALE;AAAA,MAMP,CACD,GACH;AAAA,MACC,KAAK,IAAI,CAAC,MACT,oBAAC,SAAY,MAAK,OAAM,WAAU,0BAC/B,gBAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,MAAM;AACnC,cAAM,IAAI,MAAM,IAAI,IAAI,CAAC;AACzB,eAAO,IACL,KAAK,GAAG,wBAAwB,0DAA0D,IAAI,IAE9F,oBAAC,SAAqB,MAAK,YAAW,WAAU,mBAAtC,OAAO,CAAC,EAA8C;AAAA,MAEpE,CAAC,KARO,CASV,CACD;AAAA,OACH;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,WAAW,SAAS,GAC9C;AAAA,cAAU,KACT,oBAAC,SAAI,WAAU,yCAAyC,iBAAO,UAAU,OAAO,GAAE;AAAA,IAEnF;AAAA,IACA,UACC,qBAAC,SAAI,eAAW,MAAC,WAAU,iFACzB;AAAA,0BAAC,UAAK,WAAU,UAAU,iBAAO,MAAK;AAAA,MACrC,aAAa,IAAI,CAAC,MACjB;AAAA,QAAC;AAAA;AAAA,UAEC,qBAAmB;AAAA,UACnB,WAAU;AAAA,UACV,OAAO,EAAE,YAAY,WAAW,CAAC,EAAE;AAAA;AAAA,QAH9B;AAAA,MAIP,CACD;AAAA,MACD,oBAAC,UAAK,WAAU,UAAU,iBAAO,MAAK;AAAA,OACxC;AAAA,KAEJ;AAEJ;","names":["_"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/calendar-heatmap.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, horizontalStep } from \"../lib/direction\";\nimport { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from \"../lib/dates\";\nimport { useKitLabels, useKitLocale, useKitWeekStart } from \"../i18n/kit-labels\";\nimport { lerpHex, textOn } from \"../theme/chart-palette\";\nimport { parseHex } from \"../theme/color\";\nimport { Tooltip } from \"./tooltip\";\nimport type { WeekDay } from \"./mini-calendar\";\n\n/**\n * Every string the heatmap speaks — the `calendarHeatmap` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. Dates and\n * values arrive already formatted (in `locale`, and through `formatValue`), so the\n * functions only arrange them.\n */\nexport interface CalendarHeatmapLabels {\n /** Accessible name of the grid. */\n grid: string;\n /**\n * One day: its accessible name, and the tooltip unless `tooltip` is given. `date`\n * is the whole date in `locale` (\"Monday, 14 September 2026\"), `value` the day's\n * figure through `formatValue`.\n */\n day: (date: string, value: string) => string;\n /** The two ends of the legend's scale. */\n less: string;\n more: string;\n /** `maxDays` cut the window: how many of its earliest days are not drawn. */\n truncated: (count: number) => string;\n}\n\n/** English defaults, exported as the `calendarHeatmap` namespace of `UiKitLabels`. */\nexport const DEFAULT_CALENDAR_HEATMAP_LABELS: CalendarHeatmapLabels = {\n grid: \"Daily values\",\n day: (date, value) => `${date}: ${value}`,\n less: \"Less\",\n more: \"More\",\n truncated: (count) =>\n `Showing the most recent days; ${count} earlier ${count === 1 ? \"day is\" : \"days are\"} not shown.`,\n};\n\n/** One day's figure. Several entries for one date are added up. */\nexport interface CalendarHeatmapDatum {\n /** \"YYYY-MM-DD\". */\n date: string;\n value: number;\n}\n\n/** What `tooltip` is told about the day under the pointer. */\nexport interface CalendarHeatmapDay {\n iso: string;\n date: Date;\n /** The day's figure; `0` when `data` has no entry for it (see `hasData`). */\n value: number;\n hasData: boolean;\n /** `0` = empty … `levels` = the busiest. */\n level: number;\n /** The whole date in `locale`. */\n formattedDate: string;\n /** `value` through `formatValue`. */\n formattedValue: string;\n}\n\n/**\n * `onSelect` is omitted from the `<div>` attributes for the reason `MiniCalendar` gives:\n * the DOM event of that name is a text-selection event, and this one answers with a date.\n */\nexport interface CalendarHeatmapProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onSelect\"> {\n data: readonly CalendarHeatmapDatum[];\n /** The window, inclusive, as \"YYYY-MM-DD\". */\n from: string;\n to: string;\n /**\n * `\"weeks\"` (default): the contribution-graph shape — one COLUMN per week, the\n * weekdays down the side, month names along the top; a year fits in ~53 small\n * squares and scrolls sideways where it does not. `\"month\"`: a 7-column calendar\n * with day numbers in large square cells, for a single month on a phone (switch on\n * `useMediaQuery` and narrow `from`/`to` to the month).\n */\n layout?: \"weeks\" | \"month\";\n /** The day shown as selected (\"YYYY-MM-DD\"). */\n selected?: string;\n /** Makes each day a button: a click (or Enter/Space) selects it — a drill-down into\n * that day's rows, a day panel. Without it the days are focusable but inert. */\n onSelect?: (iso: string) => void;\n /** How a value is written in the tooltip and the day's name. Default: `Intl.NumberFormat`\n * in `locale`. Pass a currency formatter for money. */\n formatValue?: (value: number) => string;\n /** Tooltip content for a day. Default: `labels.day(date, value)`. */\n tooltip?: (day: CalendarHeatmapDay) => ReactNode;\n /**\n * Tag each tooltip `data-private`, so the host's demo-mode rule blurs it (the\n * attribute `Tooltip redact` and `StatTile sensitive` carry). ON by default: a heatmap\n * is almost always the user's own figures, and a missing tag is silent — the grid\n * looks anonymous and the bubble spells the amount out on hover.\n */\n sensitive?: boolean;\n /** The value that reaches the top of the scale. Default: the largest value ON SCREEN,\n * so a peak outside the window does not wash the drawn days out to empty. */\n max?: number;\n /** How many non-empty intensity steps. Default 4. */\n levels?: number;\n /** The scale's colour, as any CSS colour. Default `var(--brand)`; each step is this\n * mixed into `--bg-surface`, so the ramp follows the theme. */\n color?: string;\n /**\n * A CONTINUOUS scale from `colorFrom` (the smallest value) to `colorTo` (the busiest\n * day), instead of `levels` steps of `color` — keksdose's palette-store ramp,\n * `seqLow` → `seqHigh`. Both are needed; `color` is then unused. Two `#rrggbb`\n * colours are interpolated in JS, so the fill is a concrete hex and the day number's\n * ink (in `\"month\"`) is MEASURED against it; anything else (a `var()`) is mixed with\n * `color-mix()`. `levels` still sets `data-level` and the legend's swatch count.\n */\n colorFrom?: string;\n colorTo?: string;\n /**\n * A fill of the caller's own for a day with a value: handed the value and the\n * scale's `max`, returns any CSS colour. Wins over `color` and `colorFrom`/`colorTo`.\n * Also paints the legend, at `max × level / levels`.\n */\n fill?: (value: number, max: number) => string;\n /** The fill of a day with no value (or a value of 0). Default: a wash of the text\n * colour over the surface, which reads as a cell on both themes. */\n emptyColor?: string;\n /**\n * Where the window ENDS — the day `maxDays` counts back from, and the last one drawn:\n *\n * - `\"to\"` (default): on `to`.\n * - `\"today\"`: on today.\n * - `\"latest\"`: on the last day that can carry a figure — today, or a later day that\n * actually has a value. A window running into the future (a report's \"all\" preset\n * ends a year ahead) then spends its cells on the days a person is looking for,\n * not on a year of empty ones.\n * - a `\"YYYY-MM-DD\"`: on that day.\n *\n * Always kept inside `from`…`to`.\n */\n anchor?: \"to\" | \"today\" | \"latest\" | (string & {});\n /**\n * The most days to draw. A longer window keeps its LATEST `maxDays` and says how many\n * it left out (`labels.truncated`) rather than silently ending. Default: no cap.\n */\n maxDays?: number;\n /** Draw the Less … More legend. Default `true`. */\n legend?: boolean;\n /** BCP 47 tag for the dates, month and weekday names and numbers. */\n locale?: string;\n /** `0` = Sunday … `6` = Saturday. Default: the provider's, else the locale's. */\n weekStartsOn?: WeekDay;\n labels?: Partial<CalendarHeatmapLabels>;\n className?: string;\n}\n\nconst DAY_NAME_FORMAT: Intl.DateTimeFormatOptions = {\n weekday: \"long\",\n day: \"numeric\",\n month: \"long\",\n year: \"numeric\",\n};\n\n/** The empty day: a wash of the text colour, like `--bg-hover`, so it reads as a cell\n * on both themes without claiming any value. */\nconst EMPTY_FILL = \"color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface))\";\n\nconst weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;\n\n/** Where the window ends for an {@link CalendarHeatmapProps.anchor}, kept in `[from, to]`.\n * `latestData` is the latest ISO date carrying a value, or `\"\"`. */\nexport function heatmapWindowEnd(\n from: string,\n to: string,\n anchor: string,\n latestData: string,\n today: string = toLocalIso(new Date()),\n): string {\n let end: string;\n if (anchor === \"today\") end = today;\n else if (anchor === \"latest\") end = latestData > today ? latestData : today;\n else if (anchor === \"to\" || !parseIsoDate(anchor)) end = to;\n else end = anchor;\n // ISO dates sort as strings, so the clamp is plain comparison.\n if (end > to) end = to;\n if (end < from) end = from;\n return end;\n}\n\n/** The step a value falls on: 0 for nothing, else 1…levels by its share of `max`. */\nexport function heatmapLevel(value: number, max: number, levels: number): number {\n if (!(value > 0) || !(max > 0) || levels < 1) return 0;\n return Math.min(levels, Math.max(1, Math.ceil((value / max) * levels)));\n}\n\n/** How much of `color` a step mixes into the surface: 20% for the first step, 100% for\n * the last — the floor keeps the smallest value visibly apart from an empty day. */\nfunction levelPercent(level: number, levels: number): number {\n if (levels <= 1) return 100;\n return Math.round(20 + (80 * (level - 1)) / (levels - 1));\n}\n\nfunction levelFill(level: number, levels: number, color: string): string {\n if (level === 0) return EMPTY_FILL;\n return `color-mix(in oklab, ${color} ${levelPercent(level, levels)}%, var(--bg-surface))`;\n}\n\n/**\n * Days as a grid of shaded squares — how much happened on each, at a glance\n * (a contribution graph, a spending calendar).\n *\n * **Every day is a cell you can reach.** The grid is a `role=\"grid\"` with ONE tab\n * stop (the roving tabindex, as in `MiniCalendar`); the arrows move by the grid's own\n * geometry — in `\"weeks\"` ↑/↓ are the previous/next day and ←/→ a week, in `\"month\"`\n * the other way round, mirrored in RTL — PageUp/PageDown move a month and Home/End go\n * to the window's first/last day. Each cell is named with its whole date AND its value,\n * because the colour is not something a screen reader can say, and carries\n * `data-day=\"YYYY-MM-DD\"` as its identity for tests and for the host. Note that in\n * `\"weeks\"` the cells are in DOM order by WEEKDAY ROW (all Mondays, then all\n * Tuesdays…), not by date — query by `data-day`, not by position.\n *\n * The tooltip is portalled (a year of squares always sits in a sideways scroller) and\n * `data-private` unless `sensitive={false}`. Cell size in `\"weeks\"` is the CSS variable\n * `--heatmap-cell` (default `0.75rem`): `style={{ \"--heatmap-cell\": \"1rem\" }}`.\n */\nexport function CalendarHeatmap({\n data,\n from,\n to,\n layout = \"weeks\",\n selected,\n onSelect,\n formatValue: formatValueProp,\n tooltip,\n sensitive = true,\n max: maxProp,\n levels = 4,\n color = \"var(--brand)\",\n colorFrom,\n colorTo,\n fill: fillProp,\n emptyColor,\n anchor = \"to\",\n maxDays,\n legend = true,\n locale: localeProp,\n weekStartsOn,\n labels: labelsProp,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: CalendarHeatmapProps) {\n const labels = useKitLabels(\"calendarHeatmap\", DEFAULT_CALENDAR_HEATMAP_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const providerWeekStart = useKitWeekStart();\n const weekStart: WeekDay = weekStartsOn ?? providerWeekStart ?? localeWeekStart(locale);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const formatValue = formatValueProp ?? ((v: number) => numberFormat.format(v));\n\n const values = useMemo(() => {\n const m = new Map<string, number>();\n for (const p of data) m.set(p.date, (m.get(p.date) ?? 0) + p.value);\n return m;\n }, [data]);\n\n // The window, trimmed from the FRONT: the days a person looks for are the recent\n // ones, and a cap that kept the oldest would end the calendar before today.\n const latestData = useMemo(() => {\n let latest = \"\";\n for (const [iso, v] of values) if (v !== 0 && iso > latest) latest = iso;\n return latest;\n }, [values]);\n const windowEnd = heatmapWindowEnd(from, to, anchor, latestData);\n\n const { days, dropped } = useMemo(() => {\n const start = parseIsoDate(from);\n const end = parseIsoDate(windowEnd);\n const list: Date[] = [];\n if (!start || !end || end < start) return { days: list, dropped: 0 };\n const cursor = new Date(end);\n const cap = maxDays && maxDays > 0 ? maxDays : Infinity;\n while (cursor >= start && list.length < cap) {\n list.unshift(new Date(cursor));\n cursor.setDate(cursor.getDate() - 1);\n }\n const first = list[0];\n const trimmed = first ? Math.round((first.getTime() - start.getTime()) / 86_400_000) : 0;\n return { days: list, dropped: Math.max(0, trimmed) };\n }, [from, windowEnd, maxDays]);\n\n const firstIso = days.length ? toLocalIso(days[0]) : \"\";\n const lastIso = days.length ? toLocalIso(days[days.length - 1]) : \"\";\n\n const max = useMemo(() => {\n if (maxProp !== undefined) return maxProp;\n let m = 0;\n for (const d of days) m = Math.max(m, values.get(toLocalIso(d)) ?? 0);\n return m;\n }, [maxProp, days, values]);\n\n const clampIso = (iso: string) => (iso < firstIso ? firstIso : iso > lastIso ? lastIso : iso);\n\n // The roving tab stop: the selected day, else the latest one.\n const [activeState, setActiveIso] = useState(() => selected || lastIso);\n const [lastSelected, setLastSelected] = useState(selected);\n if (lastSelected !== selected) {\n setLastSelected(selected);\n if (selected) setActiveIso(selected);\n }\n const activeIso = days.length ? clampIso(activeState || lastIso) : \"\";\n\n const gridRef = useRef<HTMLDivElement>(null);\n const scrollerRef = useRef<HTMLDivElement>(null);\n const pendingFocus = useRef<string | null>(null);\n useEffect(() => {\n const iso = pendingFocus.current;\n if (!iso) return;\n pendingFocus.current = null;\n gridRef.current?.querySelector<HTMLElement>(`[data-day=\"${iso}\"]`)?.focus();\n });\n\n // Open the sideways scroller on the latest weeks, not the oldest. In RTL the end is\n // on the left, where browsers count scrollLeft down from 0.\n //\n // And KEEP it there while the box changes size — a sidebar or a contents rail that\n // appears after mount narrows the scroller, which leaves scrollLeft where it was and\n // so shows older weeks than it opened on. The pin holds only while the view sits at\n // the end: a user who scrolls back through the year unpins it, and a resize then\n // leaves their position alone. (Scrolling back to the end pins it again.)\n useLayoutEffect(() => {\n const el = scrollerRef.current;\n if (!el || layout !== \"weeks\") return;\n const toEnd = () => {\n el.scrollLeft = dirOf(el) === \"rtl\" ? -el.scrollWidth : el.scrollWidth;\n };\n toEnd();\n if (typeof ResizeObserver === \"undefined\") return;\n // |scrollLeft| covers both directions: 0 → max in LTR, 0 → -max in RTL.\n let pinned = true;\n const onScroll = () => {\n pinned = Math.abs(el.scrollLeft) >= el.scrollWidth - el.clientWidth - 1;\n };\n const ro = new ResizeObserver(() => {\n if (pinned) toEnd();\n });\n el.addEventListener(\"scroll\", onScroll, { passive: true });\n ro.observe(el);\n return () => {\n ro.disconnect();\n el.removeEventListener(\"scroll\", onScroll);\n };\n }, [layout, firstIso, lastIso]);\n\n const formatDay = (d: Date) => d.toLocaleDateString(locale, DAY_NAME_FORMAT);\n\n const weekdayLabels = useMemo(() => {\n const ref = new Date(2024, 0, 7 + weekStart); // 7 January 2024 was a Sunday\n const week = Array.from({ length: 7 }, (_, i) => {\n const d = new Date(ref);\n d.setDate(ref.getDate() + i);\n return d;\n });\n const name = (d: Date, weekday: \"short\" | \"narrow\" | \"long\") => d.toLocaleDateString(locale, { weekday });\n // The label column is 2rem at 10px type, room for about four characters. Most\n // locales' \"short\" is three or four (\"Thu\", \"jeu.\", \"czw.\"), but some have no\n // abbreviation at all — Arabic's short Thursday IS \"الخميس\", which overran the box\n // and was cut. Such a locale takes the \"narrow\" form (\"خ\") for ALL seven, so the\n // column never mixes two forms; the full name is on the aria-label either way.\n const tooLong = week.some((d) => [...name(d, \"short\")].length > 4);\n return week.map((d) => ({ short: name(d, tooLong ? \"narrow\" : \"short\"), long: name(d, \"long\") }));\n }, [locale, weekStart]);\n\n const lead = days.length ? weekdayIndex(days[0], weekStart) : 0;\n const weekCount = Math.ceil((lead + days.length) / 7);\n /** The day at position `i` of the week-aligned sequence (blanks before the first). */\n const dayAt = (i: number): Date | null => (i >= lead && i - lead < days.length ? days[i - lead] : null);\n\n const move = (e: KeyboardEvent<HTMLElement>) => {\n const current = parseIsoDate(activeIso);\n if (!current) return;\n const next = new Date(current);\n const step = horizontalStep(e.key, e.currentTarget);\n // In \"weeks\" a row is a weekday, so sideways is a week and up/down is a day.\n const across = layout === \"weeks\" ? 7 : 1;\n const down = layout === \"weeks\" ? 1 : 7;\n switch (step ? \"horizontal\" : e.key) {\n case \"horizontal\":\n next.setDate(next.getDate() + step * across);\n break;\n case \"ArrowUp\":\n next.setDate(next.getDate() - down);\n break;\n case \"ArrowDown\":\n next.setDate(next.getDate() + down);\n break;\n case \"PageUp\":\n next.setTime(addMonthsClamped(next, -1).getTime());\n break;\n case \"PageDown\":\n next.setTime(addMonthsClamped(next, 1).getTime());\n break;\n case \"Home\":\n next.setTime(days[0].getTime());\n break;\n case \"End\":\n next.setTime(days[days.length - 1].getTime());\n break;\n default:\n return;\n }\n e.preventDefault();\n const iso = clampIso(toLocalIso(next));\n if (iso === activeIso) return;\n pendingFocus.current = iso;\n setActiveIso(iso);\n };\n\n const legendLevels = Array.from({ length: levels + 1 }, (_, l) => l);\n const empty = emptyColor ?? EMPTY_FILL;\n const continuous = colorFrom !== undefined && colorTo !== undefined;\n const hexRamp = continuous && parseHex(colorFrom) != null && parseHex(colorTo) != null;\n\n /** A day's fill, and the ink its number is set in (`\"month\"`). */\n const paint = (value: number, level: number): { fill: string; ink: string } => {\n if (level === 0) return { fill: empty, ink: \"var(--text-primary)\" };\n if (fillProp || continuous) {\n const t = max > 0 ? Math.min(1, Math.max(0, value / max)) : 1;\n const fill = fillProp\n ? fillProp(value, max)\n : hexRamp\n ? lerpHex(colorFrom!, colorTo!, t)\n : `color-mix(in oklab, ${colorTo} ${Math.round(t * 100)}%, ${colorFrom})`;\n // Measured where the fill is a colour; a mix of references cannot be measured,\n // and the page's own text colour is the safe answer there.\n const measured = textOn(fill);\n return { fill, ink: measured === \"currentColor\" ? \"var(--text-primary)\" : measured };\n }\n // Light text once the fill is the stronger half of the mix — `--bg-surface` is the\n // colour the scale is mixed INTO, so it is the one that contrasts with its far end\n // on either theme.\n const strong = levelPercent(level, levels) >= 45;\n return {\n fill: levelFill(level, levels, color),\n ink: strong ? \"var(--bg-surface)\" : \"var(--text-primary)\",\n };\n };\n // The legend's swatches: a value that lands on each step (the top one on `max`).\n const legendFill = (l: number) => paint(((max > 0 ? max : 1) * l) / levels, l).fill;\n\n const cell = (d: Date, wrapClass: string, cellClass: string, withNumber: boolean) => {\n const iso = toLocalIso(d);\n const raw = values.get(iso);\n const value = raw ?? 0;\n const level = heatmapLevel(value, max, levels);\n const { fill, ink } = paint(value, level);\n const formattedDate = formatDay(d);\n const formattedValue = formatValue(value);\n const name = labels.day(formattedDate, formattedValue);\n const label = tooltip\n ? tooltip({ iso, date: d, value, hasData: raw !== undefined, level, formattedDate, formattedValue })\n : name;\n const isSelected = selected === iso;\n const shared = {\n role: \"gridcell\",\n \"data-day\": iso,\n \"data-level\": level,\n tabIndex: iso === activeIso ? 0 : -1,\n \"aria-label\": name,\n \"aria-selected\": onSelect ? isSelected : undefined,\n onKeyDown: move,\n onFocus: () => {\n if (iso !== activeIso) setActiveIso(iso);\n },\n style: { background: fill, color: ink },\n className: cn(\n cellClass,\n isSelected && \"ring-2 ring-[var(--text-primary)]\",\n onSelect && \"cursor-pointer hover:ring-1 hover:ring-[var(--brand)]\",\n ),\n } as const;\n const inner = withNumber ? (\n <span aria-hidden className=\"text-caption leading-none tabular-nums\">\n {numberFormat.format(d.getDate())}\n </span>\n ) : null;\n return (\n <Tooltip key={iso} label={label} portal redact={sensitive} role=\"none\" className={wrapClass}>\n {onSelect ? (\n <button type=\"button\" {...shared} onClick={() => onSelect(iso)}>\n {inner}\n </button>\n ) : (\n <div {...shared}>{inner}</div>\n )}\n </Tooltip>\n );\n };\n\n const gridName = ariaLabel ?? labels.grid;\n let body: ReactNode;\n if (layout === \"weeks\") {\n const columns = { gridTemplateColumns: `var(--heatmap-label, 2rem) repeat(${weekCount}, var(--heatmap-cell, 0.75rem))` } as CSSProperties;\n const cellSize = \"size-[var(--heatmap-cell,0.75rem)]\";\n // A month's name over the first column that holds its 1st — or over the first\n // column, when the window opens far enough before the next month for it to fit.\n const monthLabels = Array.from({ length: weekCount }, (_, w) => {\n for (let r = 0; r < 7; r++) {\n const d = dayAt(w * 7 + r);\n if (d && (d.getDate() === 1 || (w === 0 && r === lead && d.getDate() <= 14))) {\n return d.toLocaleDateString(locale, { month: \"short\" });\n }\n }\n return \"\";\n });\n body = (\n <div ref={scrollerRef} className=\"overflow-x-auto pb-1\">\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"inline-grid gap-0.5\">\n {/* Decoration: every cell's name already carries its month. */}\n <div aria-hidden className=\"grid gap-0.5 text-micro leading-3 text-[var(--text-muted)]\" style={columns}>\n <span />\n {monthLabels.map((m, w) => (\n <span key={w} className=\"overflow-visible whitespace-nowrap\">\n {m}\n </span>\n ))}\n </div>\n {Array.from({ length: 7 }, (_, r) => (\n <div key={r} role=\"row\" className=\"grid gap-0.5\" style={columns}>\n <div\n role=\"rowheader\"\n aria-label={weekdayLabels[r].long}\n className=\"overflow-hidden pe-1 text-micro leading-3 text-[var(--text-muted)]\"\n >\n {/* Every other weekday, as a contribution graph does: seven labels\n at this size are a smear. The name is there for all seven. */}\n {r % 2 === 1 ? weekdayLabels[r].short : \"\"}\n </div>\n {Array.from({ length: weekCount }, (_, w) => {\n const d = dayAt(w * 7 + r);\n return d ? (\n cell(d, cellSize, cn(cellSize, \"block rounded-sm\"), false)\n ) : (\n <div key={`pad-${w}`} role=\"gridcell\" className={cellSize} />\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n } else {\n const rows = Array.from({ length: weekCount }, (_, w) => w);\n body = (\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"grid gap-1\">\n <div role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {weekdayLabels.map((w, i) => (\n <div\n key={i}\n role=\"columnheader\"\n aria-label={w.long}\n className=\"pb-0.5 text-center text-micro uppercase tracking-wide text-[var(--text-muted)]\"\n >\n {w.short}\n </div>\n ))}\n </div>\n {rows.map((w) => (\n <div key={w} role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {Array.from({ length: 7 }, (_, c) => {\n const d = dayAt(w * 7 + c);\n return d ? (\n cell(d, \"aspect-square w-full\", \"flex h-full w-full items-center justify-center rounded\", true)\n ) : (\n <div key={`pad-${c}`} role=\"gridcell\" className=\"aspect-square\" />\n );\n })}\n </div>\n ))}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"min-w-0\", className)}>\n {dropped > 0 && (\n <div className=\"mb-2 text-xs text-[var(--text-muted)]\">{labels.truncated(dropped)}</div>\n )}\n {body}\n {legend && (\n <div aria-hidden className=\"mt-2 flex items-center justify-end gap-1 text-micro text-[var(--text-muted)]\">\n <span className=\"me-0.5\">{labels.less}</span>\n {legendLevels.map((l) => (\n <span\n key={l}\n data-legend-level={l}\n className=\"size-2.5 rounded-sm\"\n style={{ background: legendFill(l) }}\n />\n ))}\n <span className=\"ms-0.5\">{labels.more}</span>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["_"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A chart's height as the kit's charts take it (docs/text-size-harmonization.md §10.10):
|
|
5
|
+
*
|
|
6
|
+
* - a CSS length — `"20rem"`, `"min(20rem, 60dvh)"`, `"clamp(14rem, 40vh, 28rem)"` —
|
|
7
|
+
* set inline, so a height can grow with the text size AND stop at the screen: scaled
|
|
8
|
+
* px would make a 480 px chart 720 px at Extra large, more than a phone shows;
|
|
9
|
+
* - a Tailwind class (`"h-72"`, `"h-64 md:h-80"`), as before;
|
|
10
|
+
* - a number of px, set inline, as before — kept for the callers that hold their height
|
|
11
|
+
* as a number, but it does not follow the text size.
|
|
12
|
+
*/
|
|
13
|
+
type ChartHeight = string | number;
|
|
14
|
+
/** Whether a string `height` is a CSS length rather than a class. */
|
|
15
|
+
declare function isCssLength(value: string): boolean;
|
|
16
|
+
/** The class or the inline style a {@link ChartHeight} becomes — one of the two, or
|
|
17
|
+
* neither for `undefined`. */
|
|
18
|
+
declare function chartHeightProps(height: ChartHeight | undefined): {
|
|
19
|
+
className?: string;
|
|
20
|
+
style?: CSSProperties;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { type ChartHeight, chartHeightProps, isCssLength };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const CSS_LENGTH = /^(?:[-+]?(?:\d+\.?\d*|\.\d+)(?:%|[a-z]+)?|(?:min|max|clamp|calc|var)\(.*\))$/i;
|
|
3
|
+
function isCssLength(value) {
|
|
4
|
+
return CSS_LENGTH.test(value.trim());
|
|
5
|
+
}
|
|
6
|
+
function chartHeightProps(height) {
|
|
7
|
+
if (height === void 0) return {};
|
|
8
|
+
if (typeof height === "number") return { style: { height } };
|
|
9
|
+
return isCssLength(height) ? { style: { height: height.trim() } } : { className: height };
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
chartHeightProps,
|
|
13
|
+
isCssLength
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=chart-height.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/chart-height.ts"],"sourcesContent":["import type { CSSProperties } from \"react\";\n\n/**\n * A chart's height as the kit's charts take it (docs/text-size-harmonization.md §10.10):\n *\n * - a CSS length — `\"20rem\"`, `\"min(20rem, 60dvh)\"`, `\"clamp(14rem, 40vh, 28rem)\"` —\n * set inline, so a height can grow with the text size AND stop at the screen: scaled\n * px would make a 480 px chart 720 px at Extra large, more than a phone shows;\n * - a Tailwind class (`\"h-72\"`, `\"h-64 md:h-80\"`), as before;\n * - a number of px, set inline, as before — kept for the callers that hold their height\n * as a number, but it does not follow the text size.\n */\nexport type ChartHeight = string | number;\n\n/** A number with an optional unit, or one of the CSS functions a length is written with.\n * A Tailwind class never starts with a digit or `min(`, so the two cannot be confused. */\nconst CSS_LENGTH = /^(?:[-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:%|[a-z]+)?|(?:min|max|clamp|calc|var)\\(.*\\))$/i;\n\n/** Whether a string `height` is a CSS length rather than a class. */\nexport function isCssLength(value: string): boolean {\n return CSS_LENGTH.test(value.trim());\n}\n\n/** The class or the inline style a {@link ChartHeight} becomes — one of the two, or\n * neither for `undefined`. */\nexport function chartHeightProps(height: ChartHeight | undefined): { className?: string; style?: CSSProperties } {\n if (height === undefined) return {};\n if (typeof height === \"number\") return { style: { height } };\n return isCssLength(height) ? { style: { height: height.trim() } } : { className: height };\n}\n"],"mappings":";AAgBA,MAAM,aAAa;AAGZ,SAAS,YAAY,OAAwB;AAClD,SAAO,WAAW,KAAK,MAAM,KAAK,CAAC;AACrC;AAIO,SAAS,iBAAiB,QAAgF;AAC/G,MAAI,WAAW,OAAW,QAAO,CAAC;AAClC,MAAI,OAAO,WAAW,SAAU,QAAO,EAAE,OAAO,EAAE,OAAO,EAAE;AAC3D,SAAO,YAAY,MAAM,IAAI,EAAE,OAAO,EAAE,QAAQ,OAAO,KAAK,EAAE,EAAE,IAAI,EAAE,WAAW,OAAO;AAC1F;","names":[]}
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
useYAxisInverseScale
|
|
8
8
|
} from "recharts";
|
|
9
9
|
import { X } from "lucide-react";
|
|
10
|
+
import { cn } from "../lib/cn.js";
|
|
11
|
+
import { FOCUS_RING } from "./focus-ring.js";
|
|
10
12
|
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
11
13
|
import { DEFAULT_SERIES_CHART_LABELS } from "./series-chart-labels.js";
|
|
12
14
|
const DEFAULT_Y_AXIS = "y";
|
|
@@ -200,7 +202,10 @@ function withChartZoom(Chart) {
|
|
|
200
202
|
{
|
|
201
203
|
type: "button",
|
|
202
204
|
onClick: clear,
|
|
203
|
-
className:
|
|
205
|
+
className: cn(
|
|
206
|
+
"absolute end-1 top-1 z-10 inline-flex items-center gap-1 rounded border border-[var(--border)] bg-[var(--bg-surface)]/90 px-1.5 py-0.5 text-micro text-[var(--text-muted)] backdrop-blur transition-colors hover:text-[var(--text-primary)]",
|
|
207
|
+
FOCUS_RING
|
|
208
|
+
),
|
|
204
209
|
children: [
|
|
205
210
|
/* @__PURE__ */ jsx(X, { "aria-hidden": true, className: "size-3" }),
|
|
206
211
|
" ",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/chart-zoom.tsx"],"sourcesContent":["// Drag-to-zoom for cartesian charts — the HOC, the shared x window, and the arithmetic.\n// Lifted out of lenkbank, where every plot in the app is one `SeriesChart` and zoom is\n// therefore wired in exactly once.\n//\n// **Which axes a drag zooms is read off the drag itself.** A selection that is roughly\n// square is a box: the reader framed a REGION and means both axes. A long thin one is a\n// band, and its own direction says which: a wide flat drag means \"this stretch of the\n// abscissa\", a tall narrow one \"this range of values\". Asking with a modifier key\n// instead would be asking people to remember a convention for something they have\n// already drawn.\n//\n// Squareness is measured **in fractions of the plot**, not pixels. These charts are\n// three times wider than they are tall, so a pixel-square selection is a very wide\n// slice of the data and would read as a band to anybody looking at it.\n//\n// The zoom is VIEW state and lives here, never in a query or a store: it is where\n// somebody is looking, and a chart whose data reloads should not lose it. It is cleared\n// by the reset button, by a double click, and by nothing else.\n//\n// Everything with a decision in it is a pure exported function below (`zoomAxesFor`,\n// `selectionFromDrag`, `zoomAfter`, `zoomDomains`, `fitYToX`, `fitXToY`); the\n// components are plumbing between a pointer and those.\nimport { createContext, useContext, useEffect, useRef, useState } from \"react\";\nimport type { ComponentType, Dispatch, ReactNode, SetStateAction } from \"react\";\nimport {\n usePlotArea,\n useXAxisInverseScale,\n useYAxisInverseScale,\n type InverseScaleFunction,\n} from \"recharts\";\nimport { X } from \"lucide-react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { DEFAULT_SERIES_CHART_LABELS, type SeriesChartLabels } from \"./series-chart-labels\";\n\n/** The y axis a series lands on when it names none — the single shared scale most\n * charts have. `SeriesChart` and the fit below agree on it by importing this. */\nexport const DEFAULT_Y_AXIS = \"y\";\n\n/* ── The arithmetic ─────────────────────────────────────────────────────── */\n\n/**\n * How square a selection has to be before it counts as a box: the shorter side\n * against the longer, both as fractions of the plot. At 0.5 a selection twice as wide\n * as it is tall is still a box and one three times as wide is a band.\n */\nexport const ZOOM_SQUARE_ENOUGH = 0.5;\n\n/** Below this a drag is a click that moved, not a selection. In fractions of the plot,\n * so it is the same gesture on a phone and on a wall display. */\nexport const ZOOM_MIN_DRAG = 0.02;\n\nexport type ZoomAxes = \"both\" | \"x\" | \"y\";\n\n/** Which axes a drag zooms — or `null` for a drag too small to mean one, or a plot\n * that has not been laid out yet. Direction does not matter; proportion does. */\nexport function zoomAxesFor(\n dx: number,\n dy: number,\n width: number,\n height: number,\n threshold: number = ZOOM_SQUARE_ENOUGH,\n /** A chart that zooms ONE axis only (see {@link ZoomTarget.zoomAxes}): every drag is\n * a band along it, however it was drawn, and a drag with no extent along it is\n * nothing — a tall narrow drag over a bar chart names no stretch of the abscissa. */\n only?: \"x\" | \"y\",\n): ZoomAxes | null {\n if (!(width > 0) || !(height > 0)) return null;\n const across = Math.abs(dx) / width;\n const down = Math.abs(dy) / height;\n if (only === \"x\") return across < ZOOM_MIN_DRAG ? null : \"x\";\n if (only === \"y\") return down < ZOOM_MIN_DRAG ? null : \"y\";\n if (across < ZOOM_MIN_DRAG && down < ZOOM_MIN_DRAG) return null;\n const longer = Math.max(across, down);\n const shorter = Math.min(across, down);\n if (shorter / longer >= threshold) return \"both\";\n return across > down ? \"x\" : \"y\";\n}\n\n/** What a drag chose, in DATA coordinates, and which axes it actually named — the\n * caller needs the second to know which of the two it may derive from the other. */\nexport interface ZoomSelection {\n which: ZoomAxes;\n x?: [number, number];\n /** One window per y axis, keyed by axis id: they are in different units. */\n y: Record<string, [number, number]>;\n}\n\n/** A drag, in the chart's own pixel coordinates. */\nexport interface ZoomDrag {\n fromX: number;\n fromY: number;\n toX: number;\n toY: number;\n}\n\n/** A pair as a domain, smallest first. Recharts wants an ascending domain whichever\n * way the drag went, and a y axis counts downward in pixels while its values count\n * upward — so both ends are read and sorted rather than one assumed to be low. */\nfunction ordered(a: number, b: number): [number, number] {\n return a <= b ? [a, b] : [b, a];\n}\n\n/**\n * A finished drag as a selection, or `null` when it selects nothing.\n *\n * `xInverse` and `yInverse(axisId)` are recharts' pixel→value functions; an axis whose\n * function is missing, or whose ends come back non-finite, is left out rather than\n * handed a NaN window.\n */\nexport function selectionFromDrag(\n drag: ZoomDrag,\n plot: { width: number; height: number },\n xInverse: InverseScaleFunction | undefined,\n yInverse: (axisId: string) => InverseScaleFunction | undefined,\n axisIds: readonly string[],\n only?: \"x\" | \"y\",\n): ZoomSelection | null {\n const which = zoomAxesFor(\n drag.toX - drag.fromX,\n drag.toY - drag.fromY,\n plot.width,\n plot.height,\n ZOOM_SQUARE_ENOUGH,\n only,\n );\n if (!which) return null;\n const next: ZoomSelection = { which, y: {} };\n if (which !== \"y\" && xInverse) {\n const from = Number(xInverse(drag.fromX));\n const to = Number(xInverse(drag.toX));\n if (Number.isFinite(from) && Number.isFinite(to)) next.x = ordered(from, to);\n }\n if (which !== \"x\") {\n for (const id of axisIds) {\n const inverse = yInverse(id);\n if (!inverse) continue;\n const from = Number(inverse(drag.fromY));\n const to = Number(inverse(drag.toY));\n if (Number.isFinite(from) && Number.isFinite(to)) next.y[id] = ordered(from, to);\n }\n }\n return next.x || Object.keys(next.y).length ? next : null;\n}\n\n/** One row of a zoomable chart. Loose on purpose: a row also carries what the chart\n * does not plot — keksdose's `rawPeriod` for a drilldown, a label for a category axis —\n * and only the finite NUMBERS in it are ever read as samples. */\nexport type ZoomRow = Readonly<Record<string, unknown>>;\n\n/** A series as the fit sees it. `stack` groups bars/areas drawn on top of each other\n * (recharts' `stackId`), whose extent is their SUM, not any one of them. */\nexport interface ZoomFitSeries {\n key: string;\n axis?: string;\n stack?: string;\n /** Only `\"line\"` matters here: recharts cannot stack a line, so a line's `stack` is\n * ignored rather than summed into a height nothing on the chart reaches. */\n type?: string;\n}\n\n/** What a band refit needs to know about the chart — a subset of `SeriesChart`'s own\n * props, so a chart hands its data straight over. */\nexport interface ZoomFitSource {\n rows: readonly ZoomRow[];\n series: readonly ZoomFitSeries[];\n axes: readonly { id: string }[];\n xKey: string;\n}\n\n/** How much of a refitted span to leave as air, top and bottom. Without it the\n * highest sample sits exactly on the frame, where a line is half drawn and reads as\n * clipped rather than as the maximum. */\nconst FIT_AIR = 0.05;\n\n/** A span with air around it — and a real span for a flat channel, which would\n * otherwise be a zero-height window recharts cannot scale. */\nfunction airy(low: number, high: number): [number, number] {\n const span = high - low;\n const air = span > 0 ? span * FIT_AIR : Math.max(Math.abs(high), 1) * FIT_AIR;\n return [low - air, high + air];\n}\n\n/** A running low/high pair. A pair rather than an array to take `Math.min(...)` of:\n * the spread passes one argument per sample, and V8 faults past ~125,000 of them. */\ntype Span = [number, number] | undefined;\n\n/** A pair widened by one value. A non-finite one is not a value — one NaN would\n * otherwise take the whole window with it. */\nfunction widen(span: Span, value: unknown): Span {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return span;\n if (!span) return [value, value];\n if (value < span[0]) span[0] = value;\n else if (value > span[1]) span[1] = value;\n return span;\n}\n\n/** Which series are measured on an axis. */\nfunction keysOn(source: ZoomFitSource, axisId: string): string[] {\n return source.series\n .filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axisId)\n .map((entry) => entry.key);\n}\n\n/** A row's value as a sample, or 0 for a stack's missing layer — a stacked bar with no\n * value for one series draws the rest of the stack from where that one would end. */\nconst layer = (value: unknown) => (typeof value === \"number\" && Number.isFinite(value) ? value : 0);\n\n/**\n * The lowest and highest value an axis DRAWS over the rows whose abscissa is inside\n * `window` (every row when there is none) — `undefined` when it draws nothing finite.\n *\n * Stack-aware, and that is why it exists: a stack of three bars reaches the SUM of\n * them, and fitting the axis to the tallest single layer (what fitting on values did)\n * cuts the top two thirds of every stack off. Positive and negative layers are summed\n * apart, as recharts draws them under `stackOffset=\"sign\"` — a month with 900 income\n * and −300 expense stacks up to 900 and down to −300, not to 600. A stacked extent\n * therefore always contains the zero its layers stand on.\n *\n * `SeriesChart` fits its axes with this and the zoom refits them with it, so the two\n * cannot disagree about how tall a stack is.\n */\nexport function axisExtent(\n source: ZoomFitSource,\n axisId: string,\n window?: readonly [number, number],\n): [number, number] | undefined {\n const mine = source.series.filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axisId);\n const loose: string[] = [];\n const stacks = new Map<string, string[]>();\n for (const entry of mine) {\n if (entry.stack === undefined || entry.type === \"line\" || entry.type === undefined) {\n loose.push(entry.key);\n } else {\n stacks.set(entry.stack, [...(stacks.get(entry.stack) ?? []), entry.key]);\n }\n }\n let span: Span;\n for (const row of source.rows) {\n if (window) {\n const at = row[source.xKey];\n if (typeof at !== \"number\" || !(at >= window[0] && at <= window[1])) continue;\n }\n for (const key of loose) span = widen(span, row[key]);\n for (const keys of stacks.values()) {\n let up = 0;\n let down = 0;\n let any = false;\n for (const key of keys) {\n const value = layer(row[key]);\n any ||= typeof row[key] === \"number\" && Number.isFinite(row[key]);\n if (value >= 0) up += value;\n else down += value;\n }\n if (!any) continue;\n span = widen(widen(span, down), up);\n }\n }\n return span;\n}\n\n/**\n * Each y axis fitted to the samples inside an x window — what a band across x leaves\n * the axes nobody dragged showing. Zoom into a tenth of a sweep and a y axis still\n * scaled to the whole run draws that tenth as a flat line, which is the one thing the\n * zoom was for; so the unnamed axes are refitted, which is what \"zoom\" means everywhere\n * else.\n *\n * An axis with nothing inside the window is left OUT rather than collapsed: a window\n * between two samples is one where the chart should keep drawing the line across.\n */\nexport function fitYToX(\n source: ZoomFitSource,\n window: readonly [number, number],\n): Record<string, [number, number]> {\n const fitted: Record<string, [number, number]> = {};\n for (const axis of source.axes) {\n const span = axisExtent(source, axis.id, window);\n if (span) fitted[axis.id] = airy(span[0], span[1]);\n }\n return fitted;\n}\n\n/**\n * The x window that holds every sample inside a set of y windows — the other direction\n * of the same idea. A channel crossing its band once gives a tight window; one that\n * oscillates through it gives back most of the sweep, which is the truthful answer.\n * `undefined` when no sample qualifies, rather than a guess.\n */\nexport function fitXToY(\n source: ZoomFitSource,\n windows: Readonly<Record<string, readonly [number, number]>>,\n): [number, number] | undefined {\n let hits: Span;\n for (const [axisId, [low, high]] of Object.entries(windows)) {\n for (const key of keysOn(source, axisId)) {\n for (const row of source.rows) {\n const value = row[key];\n if (typeof value === \"number\" && Number.isFinite(value) && value >= low && value <= high) {\n hits = widen(hits, row[source.xKey]);\n }\n }\n }\n }\n return hits ? airy(hits[0], hits[1]) : undefined;\n}\n\n/**\n * What a chart remembers about its zoom: the x window, and the y windows the reader\n * DRAGGED. `y: null` means \"derive y from x\" — the split is the whole design: what was\n * dragged is kept, and what follows from it is recomputed on every render, so a band\n * across x refits y here and in every chart sharing the window.\n */\nexport interface ZoomState {\n x?: [number, number];\n y: Record<string, [number, number]> | null;\n}\n\nexport const NO_ZOOM: ZoomState = { x: undefined, y: null };\n\n/**\n * The state after a selection. Two band drags COMPOSE rather than the second undoing\n * the first: a band down y keeps the x window when the band holds no samples, and\n * otherwise pulls x in to where they are.\n */\nexport function zoomAfter(\n state: ZoomState,\n selection: ZoomSelection,\n source: ZoomFitSource,\n): ZoomState {\n if (selection.which === \"both\") return { x: selection.x, y: selection.y };\n // Leave y derived, so it fits itself to what is inside the new window.\n if (selection.which === \"x\") return { x: selection.x, y: null };\n return { x: fitXToY(source, selection.y) ?? state.x, y: selection.y };\n}\n\n/** The domains a chart draws for a state: the x window as is, and each y axis either\n * as dragged or fitted to the x window. `{}` means \"fit your own data\". */\nexport function zoomDomains(\n state: ZoomState,\n source: ZoomFitSource,\n): { xDomain?: [number, number]; yDomains: Record<string, [number, number]> } {\n return {\n xDomain: state.x,\n yDomains: state.y ?? (state.x ? fitYToX(source, state.x) : {}),\n };\n}\n\n/* ── The components ─────────────────────────────────────────────────────── */\n\n/** What the wrapped chart is handed. It applies the domains to its own axes and renders\n * `layer` INSIDE the recharts chart, where recharts' hooks answer. */\nexport interface ZoomBinding {\n xDomain?: [number, number];\n yDomains: Record<string, [number, number]>;\n layer: ReactNode;\n}\n\n/**\n * Which axes a chart lets a drag zoom. `\"none\"` draws no drag layer and no reset.\n * A drag on a one-axis chart is always a band along that axis (see {@link zoomAxesFor}).\n */\nexport type ZoomAxesSetting = ZoomAxes | \"none\";\n\n/**\n * The zoom a chart gets when it does not say, read off what its series ARE.\n *\n * - Only lines: `\"both\"` — lenkbank's measurement plots, unchanged.\n * - Any area: `\"x\"`. An area's height is a length from zero, and a y window that cuts\n * the baseline off draws a band of 40 as three times one of 20 — so its y axis refits\n * to the x window (still from zero), and a drag never names a y range.\n * - Any bar: `\"none\"`. A bar chart is a handful of periods read one bar at a time, and\n * in keksdose every one of them is CLICKED — to drill into a payee, a month, a budget\n * line — which a drag layer over the plot would swallow. A chart of many bars opts\n * back in with `zoomAxes: \"x\"`; a click then reports the period, not the bar.\n */\nexport function defaultZoomAxes(series: readonly { type?: string }[] | undefined): ZoomAxesSetting {\n if (series?.some((entry) => entry.type === \"bar\")) return \"none\";\n if (series?.some((entry) => entry.type === \"area\")) return \"x\";\n return \"both\";\n}\n\n/** The props `withChartZoom` reads off the chart it wraps. */\nexport interface ZoomTarget {\n /** Default: the single axis {@link DEFAULT_Y_AXIS}. */\n axes?: readonly { id: string; hide?: boolean }[];\n /** The chart's own data, so a band can refit the axis it did not name. Optional: a\n * chart with nothing plotted has nothing to fit to. */\n rows?: readonly ZoomRow[];\n series?: readonly ZoomFitSeries[];\n x?: { key?: string };\n zoom?: ZoomBinding;\n labels?: Partial<SeriesChartLabels>;\n /** Which axes a drag may zoom. Default: {@link defaultZoomAxes} of `series`. */\n zoomAxes?: ZoomAxesSetting;\n}\n\nconst SharedX = createContext<{\n window?: [number, number];\n set: Dispatch<SetStateAction<[number, number] | undefined>>;\n} | null>(null);\n\n/**\n * One x window for several charts.\n *\n * A column of charts over one abscissa is one picture, and zooming one of them and not\n * the rest turns it into several pictures of different things. Every zoomable chart\n * inside this shares the x window: any drag in any of them moves all of them.\n *\n * **Only x.** The y axes stay each chart's own — they are in different units — and\n * each refits its own y to what is inside the shared window.\n */\nexport function SharedXZoom({ children }: { children: ReactNode }) {\n const [window, set] = useState<[number, number] | undefined>(undefined);\n return <SharedX.Provider value={{ window, set }}>{children}</SharedX.Provider>;\n}\n\n/**\n * Wrap a chart in drag-to-zoom.\n *\n * The chart receives a `zoom` binding: it applies `xDomain`/`yDomains` to its axes\n * (with `allowDataOverflow`, so recharts clips to the window instead of widening it\n * back out) and renders `layer` as a child of its recharts chart. A reset button\n * appears over the chart's top end corner while it is zoomed — a real button, so the\n * keyboard can undo what the pointer did.\n */\nexport function withChartZoom<P extends ZoomTarget>(Chart: ComponentType<P>) {\n function Zoomable(props: P) {\n const labels = useKitLabels(\"seriesChart\", DEFAULT_SERIES_CHART_LABELS, props.labels);\n const group = useContext(SharedX);\n const [ownX, setOwnX] = useState<[number, number] | undefined>(undefined);\n const [ownY, setOwnY] = useState<Record<string, [number, number]> | null>(null);\n const xWindow = group ? group.window : ownX;\n const setXWindow = group ? group.set : setOwnX;\n const axes = props.axes ?? [{ id: DEFAULT_Y_AXIS }];\n const setting = props.zoomAxes ?? defaultZoomAxes(props.series);\n const only = setting === \"x\" || setting === \"y\" ? setting : undefined;\n\n // Every axis, hidden ones included: a hidden axis still scales its lines, and a\n // line whose axis did not refit is drawn as a flat streak inside the window.\n const source: ZoomFitSource = {\n rows: props.rows ?? [],\n series: [...(props.series ?? [])],\n axes: [...axes],\n xKey: props.x?.key ?? \"x\",\n };\n const state: ZoomState = { x: xWindow, y: ownY };\n const zoomed = xWindow !== undefined || ownY !== null;\n // A chart handed no rows or no series draws its empty state, not a plot — and a\n // \"Reset zoom\" floating over \"No data\" (every line toggled off while zoomed) offers\n // to undo something the reader cannot see. The zoom itself is KEPT: it is where they\n // were looking, and it applies again the moment a line comes back. Only an array\n // that is there and empty counts; a chart that does not pass its data is not known\n // to be empty.\n const empty = props.rows?.length === 0 || props.series?.length === 0;\n\n const clear = () => {\n setXWindow(undefined);\n setOwnY(null);\n };\n\n // Switched off, the chart still gets a binding — with no window and no layer — so a\n // chart whose series change type mid-life is not remounted by a different tree.\n if (setting === \"none\") {\n return (\n <div className=\"relative\">\n <Chart {...props} zoom={{ yDomains: {}, layer: null }} />\n </div>\n );\n }\n\n const binding: ZoomBinding = {\n ...zoomDomains(state, source),\n layer: (\n <ZoomLayer\n axisIds={axes.map((axis) => axis.id)}\n only={only}\n label={labels.zoomHint}\n onZoom={(selection) => {\n const next = zoomAfter(state, selection, source);\n if (next.x !== xWindow) setXWindow(next.x);\n setOwnY(next.y);\n }}\n onReset={clear}\n />\n ),\n };\n\n return (\n <div className=\"relative\">\n <Chart {...props} zoom={binding} />\n {zoomed && !empty && (\n <button\n type=\"button\"\n onClick={clear}\n className=\"absolute end-1 top-1 z-10 inline-flex items-center gap-1 rounded border border-[var(--border)] bg-[var(--bg-surface)]/90 px-1.5 py-0.5 text-[10px] text-[var(--text-muted)] backdrop-blur transition-colors hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\"\n >\n <X aria-hidden className=\"size-3\" /> {labels.resetZoom}\n </button>\n )}\n </div>\n );\n }\n Zoomable.displayName = `withChartZoom(${Chart.displayName ?? Chart.name ?? \"Chart\"})`;\n return Zoomable;\n}\n\n/**\n * The part that lives INSIDE the recharts chart, where recharts will say where the plot\n * is and what value a pixel is.\n *\n * A transparent rectangle over the plot area that reads the drag. It does NOT stop the\n * event: recharts drives its tooltip from the same pointer move bubbling up to its own\n * wrapper, so swallowing it here would trade the tooltip for the zoom.\n */\nfunction ZoomLayer({\n axisIds,\n only,\n label,\n onZoom,\n onReset,\n}: {\n axisIds: string[];\n only?: \"x\" | \"y\";\n label: string;\n onZoom: (selection: ZoomSelection) => void;\n onReset: () => void;\n}) {\n const plot = usePlotArea();\n const xInverse = useXAxisInverseScale();\n // One probe COMPONENT per axis rather than a hook in a loop: the number of axes\n // changes as series are toggled, and a hook count that changes with the data is the\n // one thing React will not forgive.\n const [scales] = useState(() => new Map<string, InverseScaleFunction | undefined>());\n const [drag, setDrag] = useState<ZoomDrag | null>(null);\n // A finished drag is followed by a click on the same element, and a chart with an\n // `onClick` (keksdose opens a drilldown on one) would read the zoom as a pick.\n const draggedRef = useRef(false);\n\n if (!plot || plot.width <= 0 || plot.height <= 0) return null;\n\n const at = (event: { clientX: number; clientY: number; currentTarget: Element }) => {\n // Against the overlay's own screen box, so padding around the container cannot\n // shift every reading — plus the plot's offset, because the scales are in the\n // chart's coordinates rather than the plot's.\n const box = event.currentTarget.getBoundingClientRect();\n return { x: event.clientX - box.left + plot.x, y: event.clientY - box.top + plot.y };\n };\n\n const finish = () => {\n if (!drag) return;\n setDrag(null);\n const selection = selectionFromDrag(drag, plot, xInverse, (id) => scales.get(id), axisIds, only);\n draggedRef.current = selection !== null;\n if (selection) onZoom(selection);\n };\n\n // What the drag would do, shown while it is being made: a band paints the full width\n // or height, so the reader sees it is about to let the other axis refit.\n const which = drag\n ? zoomAxesFor(\n drag.toX - drag.fromX,\n drag.toY - drag.fromY,\n plot.width,\n plot.height,\n ZOOM_SQUARE_ENOUGH,\n only,\n )\n : null;\n\n return (\n <g>\n {axisIds.map((id) => (\n <AxisProbe key={id} axisId={id} scales={scales} />\n ))}\n {drag && which && (\n <rect\n x={which === \"y\" ? plot.x : Math.min(drag.fromX, drag.toX)}\n y={which === \"x\" ? plot.y : Math.min(drag.fromY, drag.toY)}\n width={which === \"y\" ? plot.width : Math.abs(drag.toX - drag.fromX)}\n height={which === \"x\" ? plot.height : Math.abs(drag.toY - drag.fromY)}\n className=\"fill-[var(--brand)]/15 stroke-[var(--brand)]\"\n strokeDasharray=\"4 3\"\n pointerEvents=\"none\"\n />\n )}\n <rect\n x={plot.x}\n y={plot.y}\n width={plot.width}\n height={plot.height}\n // `transparent`, not `none`: an unpainted shape takes no pointer events.\n fill=\"transparent\"\n role=\"application\"\n aria-label={label}\n style={{ cursor: \"crosshair\" }}\n onPointerDown={(event) => {\n if (event.button !== 0) return;\n draggedRef.current = false;\n const point = at(event);\n // Best effort: capture keeps a drag alive past the plot's edge, but a\n // pointer id the environment does not know throws — jsdom refuses them all —\n // and a zoom that threw on pointer-down would be a chart nobody could click.\n try {\n event.currentTarget.setPointerCapture(event.pointerId);\n } catch {\n /* the drag still works; it just stops at the edge of the plot */\n }\n setDrag({ fromX: point.x, fromY: point.y, toX: point.x, toY: point.y });\n }}\n onPointerMove={(event) => {\n if (!drag) return;\n const point = at(event);\n setDrag({ ...drag, toX: point.x, toY: point.y });\n }}\n onPointerUp={finish}\n onPointerCancel={() => setDrag(null)}\n onClick={(event) => {\n if (!draggedRef.current) return;\n draggedRef.current = false;\n event.stopPropagation();\n }}\n onDoubleClick={onReset}\n />\n </g>\n );\n}\n\n/** One axis's pixel→value function, parked where the pointer handler can reach it. */\nfunction AxisProbe({\n axisId,\n scales,\n}: {\n axisId: string;\n scales: Map<string, InverseScaleFunction | undefined>;\n}) {\n const inverse = useYAxisInverseScale(axisId);\n useEffect(() => {\n scales.set(axisId, inverse);\n return () => {\n scales.delete(axisId);\n };\n }, [axisId, inverse, scales]);\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/chart-zoom.tsx"],"sourcesContent":["// Drag-to-zoom for cartesian charts — the HOC, the shared x window, and the arithmetic.\n// Lifted out of lenkbank, where every plot in the app is one `SeriesChart` and zoom is\n// therefore wired in exactly once.\n//\n// **Which axes a drag zooms is read off the drag itself.** A selection that is roughly\n// square is a box: the reader framed a REGION and means both axes. A long thin one is a\n// band, and its own direction says which: a wide flat drag means \"this stretch of the\n// abscissa\", a tall narrow one \"this range of values\". Asking with a modifier key\n// instead would be asking people to remember a convention for something they have\n// already drawn.\n//\n// Squareness is measured **in fractions of the plot**, not pixels. These charts are\n// three times wider than they are tall, so a pixel-square selection is a very wide\n// slice of the data and would read as a band to anybody looking at it.\n//\n// The zoom is VIEW state and lives here, never in a query or a store: it is where\n// somebody is looking, and a chart whose data reloads should not lose it. It is cleared\n// by the reset button, by a double click, and by nothing else.\n//\n// Everything with a decision in it is a pure exported function below (`zoomAxesFor`,\n// `selectionFromDrag`, `zoomAfter`, `zoomDomains`, `fitYToX`, `fitXToY`); the\n// components are plumbing between a pointer and those.\nimport { createContext, useContext, useEffect, useRef, useState } from \"react\";\nimport type { ComponentType, Dispatch, ReactNode, SetStateAction } from \"react\";\nimport {\n usePlotArea,\n useXAxisInverseScale,\n useYAxisInverseScale,\n type InverseScaleFunction,\n} from \"recharts\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { DEFAULT_SERIES_CHART_LABELS, type SeriesChartLabels } from \"./series-chart-labels\";\n\n/** The y axis a series lands on when it names none — the single shared scale most\n * charts have. `SeriesChart` and the fit below agree on it by importing this. */\nexport const DEFAULT_Y_AXIS = \"y\";\n\n/* ── The arithmetic ─────────────────────────────────────────────────────── */\n\n/**\n * How square a selection has to be before it counts as a box: the shorter side\n * against the longer, both as fractions of the plot. At 0.5 a selection twice as wide\n * as it is tall is still a box and one three times as wide is a band.\n */\nexport const ZOOM_SQUARE_ENOUGH = 0.5;\n\n/** Below this a drag is a click that moved, not a selection. In fractions of the plot,\n * so it is the same gesture on a phone and on a wall display. */\nexport const ZOOM_MIN_DRAG = 0.02;\n\nexport type ZoomAxes = \"both\" | \"x\" | \"y\";\n\n/** Which axes a drag zooms — or `null` for a drag too small to mean one, or a plot\n * that has not been laid out yet. Direction does not matter; proportion does. */\nexport function zoomAxesFor(\n dx: number,\n dy: number,\n width: number,\n height: number,\n threshold: number = ZOOM_SQUARE_ENOUGH,\n /** A chart that zooms ONE axis only (see {@link ZoomTarget.zoomAxes}): every drag is\n * a band along it, however it was drawn, and a drag with no extent along it is\n * nothing — a tall narrow drag over a bar chart names no stretch of the abscissa. */\n only?: \"x\" | \"y\",\n): ZoomAxes | null {\n if (!(width > 0) || !(height > 0)) return null;\n const across = Math.abs(dx) / width;\n const down = Math.abs(dy) / height;\n if (only === \"x\") return across < ZOOM_MIN_DRAG ? null : \"x\";\n if (only === \"y\") return down < ZOOM_MIN_DRAG ? null : \"y\";\n if (across < ZOOM_MIN_DRAG && down < ZOOM_MIN_DRAG) return null;\n const longer = Math.max(across, down);\n const shorter = Math.min(across, down);\n if (shorter / longer >= threshold) return \"both\";\n return across > down ? \"x\" : \"y\";\n}\n\n/** What a drag chose, in DATA coordinates, and which axes it actually named — the\n * caller needs the second to know which of the two it may derive from the other. */\nexport interface ZoomSelection {\n which: ZoomAxes;\n x?: [number, number];\n /** One window per y axis, keyed by axis id: they are in different units. */\n y: Record<string, [number, number]>;\n}\n\n/** A drag, in the chart's own pixel coordinates. */\nexport interface ZoomDrag {\n fromX: number;\n fromY: number;\n toX: number;\n toY: number;\n}\n\n/** A pair as a domain, smallest first. Recharts wants an ascending domain whichever\n * way the drag went, and a y axis counts downward in pixels while its values count\n * upward — so both ends are read and sorted rather than one assumed to be low. */\nfunction ordered(a: number, b: number): [number, number] {\n return a <= b ? [a, b] : [b, a];\n}\n\n/**\n * A finished drag as a selection, or `null` when it selects nothing.\n *\n * `xInverse` and `yInverse(axisId)` are recharts' pixel→value functions; an axis whose\n * function is missing, or whose ends come back non-finite, is left out rather than\n * handed a NaN window.\n */\nexport function selectionFromDrag(\n drag: ZoomDrag,\n plot: { width: number; height: number },\n xInverse: InverseScaleFunction | undefined,\n yInverse: (axisId: string) => InverseScaleFunction | undefined,\n axisIds: readonly string[],\n only?: \"x\" | \"y\",\n): ZoomSelection | null {\n const which = zoomAxesFor(\n drag.toX - drag.fromX,\n drag.toY - drag.fromY,\n plot.width,\n plot.height,\n ZOOM_SQUARE_ENOUGH,\n only,\n );\n if (!which) return null;\n const next: ZoomSelection = { which, y: {} };\n if (which !== \"y\" && xInverse) {\n const from = Number(xInverse(drag.fromX));\n const to = Number(xInverse(drag.toX));\n if (Number.isFinite(from) && Number.isFinite(to)) next.x = ordered(from, to);\n }\n if (which !== \"x\") {\n for (const id of axisIds) {\n const inverse = yInverse(id);\n if (!inverse) continue;\n const from = Number(inverse(drag.fromY));\n const to = Number(inverse(drag.toY));\n if (Number.isFinite(from) && Number.isFinite(to)) next.y[id] = ordered(from, to);\n }\n }\n return next.x || Object.keys(next.y).length ? next : null;\n}\n\n/** One row of a zoomable chart. Loose on purpose: a row also carries what the chart\n * does not plot — keksdose's `rawPeriod` for a drilldown, a label for a category axis —\n * and only the finite NUMBERS in it are ever read as samples. */\nexport type ZoomRow = Readonly<Record<string, unknown>>;\n\n/** A series as the fit sees it. `stack` groups bars/areas drawn on top of each other\n * (recharts' `stackId`), whose extent is their SUM, not any one of them. */\nexport interface ZoomFitSeries {\n key: string;\n axis?: string;\n stack?: string;\n /** Only `\"line\"` matters here: recharts cannot stack a line, so a line's `stack` is\n * ignored rather than summed into a height nothing on the chart reaches. */\n type?: string;\n}\n\n/** What a band refit needs to know about the chart — a subset of `SeriesChart`'s own\n * props, so a chart hands its data straight over. */\nexport interface ZoomFitSource {\n rows: readonly ZoomRow[];\n series: readonly ZoomFitSeries[];\n axes: readonly { id: string }[];\n xKey: string;\n}\n\n/** How much of a refitted span to leave as air, top and bottom. Without it the\n * highest sample sits exactly on the frame, where a line is half drawn and reads as\n * clipped rather than as the maximum. */\nconst FIT_AIR = 0.05;\n\n/** A span with air around it — and a real span for a flat channel, which would\n * otherwise be a zero-height window recharts cannot scale. */\nfunction airy(low: number, high: number): [number, number] {\n const span = high - low;\n const air = span > 0 ? span * FIT_AIR : Math.max(Math.abs(high), 1) * FIT_AIR;\n return [low - air, high + air];\n}\n\n/** A running low/high pair. A pair rather than an array to take `Math.min(...)` of:\n * the spread passes one argument per sample, and V8 faults past ~125,000 of them. */\ntype Span = [number, number] | undefined;\n\n/** A pair widened by one value. A non-finite one is not a value — one NaN would\n * otherwise take the whole window with it. */\nfunction widen(span: Span, value: unknown): Span {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return span;\n if (!span) return [value, value];\n if (value < span[0]) span[0] = value;\n else if (value > span[1]) span[1] = value;\n return span;\n}\n\n/** Which series are measured on an axis. */\nfunction keysOn(source: ZoomFitSource, axisId: string): string[] {\n return source.series\n .filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axisId)\n .map((entry) => entry.key);\n}\n\n/** A row's value as a sample, or 0 for a stack's missing layer — a stacked bar with no\n * value for one series draws the rest of the stack from where that one would end. */\nconst layer = (value: unknown) => (typeof value === \"number\" && Number.isFinite(value) ? value : 0);\n\n/**\n * The lowest and highest value an axis DRAWS over the rows whose abscissa is inside\n * `window` (every row when there is none) — `undefined` when it draws nothing finite.\n *\n * Stack-aware, and that is why it exists: a stack of three bars reaches the SUM of\n * them, and fitting the axis to the tallest single layer (what fitting on values did)\n * cuts the top two thirds of every stack off. Positive and negative layers are summed\n * apart, as recharts draws them under `stackOffset=\"sign\"` — a month with 900 income\n * and −300 expense stacks up to 900 and down to −300, not to 600. A stacked extent\n * therefore always contains the zero its layers stand on.\n *\n * `SeriesChart` fits its axes with this and the zoom refits them with it, so the two\n * cannot disagree about how tall a stack is.\n */\nexport function axisExtent(\n source: ZoomFitSource,\n axisId: string,\n window?: readonly [number, number],\n): [number, number] | undefined {\n const mine = source.series.filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axisId);\n const loose: string[] = [];\n const stacks = new Map<string, string[]>();\n for (const entry of mine) {\n if (entry.stack === undefined || entry.type === \"line\" || entry.type === undefined) {\n loose.push(entry.key);\n } else {\n stacks.set(entry.stack, [...(stacks.get(entry.stack) ?? []), entry.key]);\n }\n }\n let span: Span;\n for (const row of source.rows) {\n if (window) {\n const at = row[source.xKey];\n if (typeof at !== \"number\" || !(at >= window[0] && at <= window[1])) continue;\n }\n for (const key of loose) span = widen(span, row[key]);\n for (const keys of stacks.values()) {\n let up = 0;\n let down = 0;\n let any = false;\n for (const key of keys) {\n const value = layer(row[key]);\n any ||= typeof row[key] === \"number\" && Number.isFinite(row[key]);\n if (value >= 0) up += value;\n else down += value;\n }\n if (!any) continue;\n span = widen(widen(span, down), up);\n }\n }\n return span;\n}\n\n/**\n * Each y axis fitted to the samples inside an x window — what a band across x leaves\n * the axes nobody dragged showing. Zoom into a tenth of a sweep and a y axis still\n * scaled to the whole run draws that tenth as a flat line, which is the one thing the\n * zoom was for; so the unnamed axes are refitted, which is what \"zoom\" means everywhere\n * else.\n *\n * An axis with nothing inside the window is left OUT rather than collapsed: a window\n * between two samples is one where the chart should keep drawing the line across.\n */\nexport function fitYToX(\n source: ZoomFitSource,\n window: readonly [number, number],\n): Record<string, [number, number]> {\n const fitted: Record<string, [number, number]> = {};\n for (const axis of source.axes) {\n const span = axisExtent(source, axis.id, window);\n if (span) fitted[axis.id] = airy(span[0], span[1]);\n }\n return fitted;\n}\n\n/**\n * The x window that holds every sample inside a set of y windows — the other direction\n * of the same idea. A channel crossing its band once gives a tight window; one that\n * oscillates through it gives back most of the sweep, which is the truthful answer.\n * `undefined` when no sample qualifies, rather than a guess.\n */\nexport function fitXToY(\n source: ZoomFitSource,\n windows: Readonly<Record<string, readonly [number, number]>>,\n): [number, number] | undefined {\n let hits: Span;\n for (const [axisId, [low, high]] of Object.entries(windows)) {\n for (const key of keysOn(source, axisId)) {\n for (const row of source.rows) {\n const value = row[key];\n if (typeof value === \"number\" && Number.isFinite(value) && value >= low && value <= high) {\n hits = widen(hits, row[source.xKey]);\n }\n }\n }\n }\n return hits ? airy(hits[0], hits[1]) : undefined;\n}\n\n/**\n * What a chart remembers about its zoom: the x window, and the y windows the reader\n * DRAGGED. `y: null` means \"derive y from x\" — the split is the whole design: what was\n * dragged is kept, and what follows from it is recomputed on every render, so a band\n * across x refits y here and in every chart sharing the window.\n */\nexport interface ZoomState {\n x?: [number, number];\n y: Record<string, [number, number]> | null;\n}\n\nexport const NO_ZOOM: ZoomState = { x: undefined, y: null };\n\n/**\n * The state after a selection. Two band drags COMPOSE rather than the second undoing\n * the first: a band down y keeps the x window when the band holds no samples, and\n * otherwise pulls x in to where they are.\n */\nexport function zoomAfter(\n state: ZoomState,\n selection: ZoomSelection,\n source: ZoomFitSource,\n): ZoomState {\n if (selection.which === \"both\") return { x: selection.x, y: selection.y };\n // Leave y derived, so it fits itself to what is inside the new window.\n if (selection.which === \"x\") return { x: selection.x, y: null };\n return { x: fitXToY(source, selection.y) ?? state.x, y: selection.y };\n}\n\n/** The domains a chart draws for a state: the x window as is, and each y axis either\n * as dragged or fitted to the x window. `{}` means \"fit your own data\". */\nexport function zoomDomains(\n state: ZoomState,\n source: ZoomFitSource,\n): { xDomain?: [number, number]; yDomains: Record<string, [number, number]> } {\n return {\n xDomain: state.x,\n yDomains: state.y ?? (state.x ? fitYToX(source, state.x) : {}),\n };\n}\n\n/* ── The components ─────────────────────────────────────────────────────── */\n\n/** What the wrapped chart is handed. It applies the domains to its own axes and renders\n * `layer` INSIDE the recharts chart, where recharts' hooks answer. */\nexport interface ZoomBinding {\n xDomain?: [number, number];\n yDomains: Record<string, [number, number]>;\n layer: ReactNode;\n}\n\n/**\n * Which axes a chart lets a drag zoom. `\"none\"` draws no drag layer and no reset.\n * A drag on a one-axis chart is always a band along that axis (see {@link zoomAxesFor}).\n */\nexport type ZoomAxesSetting = ZoomAxes | \"none\";\n\n/**\n * The zoom a chart gets when it does not say, read off what its series ARE.\n *\n * - Only lines: `\"both\"` — lenkbank's measurement plots, unchanged.\n * - Any area: `\"x\"`. An area's height is a length from zero, and a y window that cuts\n * the baseline off draws a band of 40 as three times one of 20 — so its y axis refits\n * to the x window (still from zero), and a drag never names a y range.\n * - Any bar: `\"none\"`. A bar chart is a handful of periods read one bar at a time, and\n * in keksdose every one of them is CLICKED — to drill into a payee, a month, a budget\n * line — which a drag layer over the plot would swallow. A chart of many bars opts\n * back in with `zoomAxes: \"x\"`; a click then reports the period, not the bar.\n */\nexport function defaultZoomAxes(series: readonly { type?: string }[] | undefined): ZoomAxesSetting {\n if (series?.some((entry) => entry.type === \"bar\")) return \"none\";\n if (series?.some((entry) => entry.type === \"area\")) return \"x\";\n return \"both\";\n}\n\n/** The props `withChartZoom` reads off the chart it wraps. */\nexport interface ZoomTarget {\n /** Default: the single axis {@link DEFAULT_Y_AXIS}. */\n axes?: readonly { id: string; hide?: boolean }[];\n /** The chart's own data, so a band can refit the axis it did not name. Optional: a\n * chart with nothing plotted has nothing to fit to. */\n rows?: readonly ZoomRow[];\n series?: readonly ZoomFitSeries[];\n x?: { key?: string };\n zoom?: ZoomBinding;\n labels?: Partial<SeriesChartLabels>;\n /** Which axes a drag may zoom. Default: {@link defaultZoomAxes} of `series`. */\n zoomAxes?: ZoomAxesSetting;\n}\n\nconst SharedX = createContext<{\n window?: [number, number];\n set: Dispatch<SetStateAction<[number, number] | undefined>>;\n} | null>(null);\n\n/**\n * One x window for several charts.\n *\n * A column of charts over one abscissa is one picture, and zooming one of them and not\n * the rest turns it into several pictures of different things. Every zoomable chart\n * inside this shares the x window: any drag in any of them moves all of them.\n *\n * **Only x.** The y axes stay each chart's own — they are in different units — and\n * each refits its own y to what is inside the shared window.\n */\nexport function SharedXZoom({ children }: { children: ReactNode }) {\n const [window, set] = useState<[number, number] | undefined>(undefined);\n return <SharedX.Provider value={{ window, set }}>{children}</SharedX.Provider>;\n}\n\n/**\n * Wrap a chart in drag-to-zoom.\n *\n * The chart receives a `zoom` binding: it applies `xDomain`/`yDomains` to its axes\n * (with `allowDataOverflow`, so recharts clips to the window instead of widening it\n * back out) and renders `layer` as a child of its recharts chart. A reset button\n * appears over the chart's top end corner while it is zoomed — a real button, so the\n * keyboard can undo what the pointer did.\n */\nexport function withChartZoom<P extends ZoomTarget>(Chart: ComponentType<P>) {\n function Zoomable(props: P) {\n const labels = useKitLabels(\"seriesChart\", DEFAULT_SERIES_CHART_LABELS, props.labels);\n const group = useContext(SharedX);\n const [ownX, setOwnX] = useState<[number, number] | undefined>(undefined);\n const [ownY, setOwnY] = useState<Record<string, [number, number]> | null>(null);\n const xWindow = group ? group.window : ownX;\n const setXWindow = group ? group.set : setOwnX;\n const axes = props.axes ?? [{ id: DEFAULT_Y_AXIS }];\n const setting = props.zoomAxes ?? defaultZoomAxes(props.series);\n const only = setting === \"x\" || setting === \"y\" ? setting : undefined;\n\n // Every axis, hidden ones included: a hidden axis still scales its lines, and a\n // line whose axis did not refit is drawn as a flat streak inside the window.\n const source: ZoomFitSource = {\n rows: props.rows ?? [],\n series: [...(props.series ?? [])],\n axes: [...axes],\n xKey: props.x?.key ?? \"x\",\n };\n const state: ZoomState = { x: xWindow, y: ownY };\n const zoomed = xWindow !== undefined || ownY !== null;\n // A chart handed no rows or no series draws its empty state, not a plot — and a\n // \"Reset zoom\" floating over \"No data\" (every line toggled off while zoomed) offers\n // to undo something the reader cannot see. The zoom itself is KEPT: it is where they\n // were looking, and it applies again the moment a line comes back. Only an array\n // that is there and empty counts; a chart that does not pass its data is not known\n // to be empty.\n const empty = props.rows?.length === 0 || props.series?.length === 0;\n\n const clear = () => {\n setXWindow(undefined);\n setOwnY(null);\n };\n\n // Switched off, the chart still gets a binding — with no window and no layer — so a\n // chart whose series change type mid-life is not remounted by a different tree.\n if (setting === \"none\") {\n return (\n <div className=\"relative\">\n <Chart {...props} zoom={{ yDomains: {}, layer: null }} />\n </div>\n );\n }\n\n const binding: ZoomBinding = {\n ...zoomDomains(state, source),\n layer: (\n <ZoomLayer\n axisIds={axes.map((axis) => axis.id)}\n only={only}\n label={labels.zoomHint}\n onZoom={(selection) => {\n const next = zoomAfter(state, selection, source);\n if (next.x !== xWindow) setXWindow(next.x);\n setOwnY(next.y);\n }}\n onReset={clear}\n />\n ),\n };\n\n return (\n <div className=\"relative\">\n <Chart {...props} zoom={binding} />\n {zoomed && !empty && (\n <button\n type=\"button\"\n onClick={clear}\n className={cn(\n \"absolute end-1 top-1 z-10 inline-flex items-center gap-1 rounded border border-[var(--border)] bg-[var(--bg-surface)]/90 px-1.5 py-0.5 text-micro text-[var(--text-muted)] backdrop-blur transition-colors hover:text-[var(--text-primary)]\",\n FOCUS_RING,\n )}\n >\n <X aria-hidden className=\"size-3\" /> {labels.resetZoom}\n </button>\n )}\n </div>\n );\n }\n Zoomable.displayName = `withChartZoom(${Chart.displayName ?? Chart.name ?? \"Chart\"})`;\n return Zoomable;\n}\n\n/**\n * The part that lives INSIDE the recharts chart, where recharts will say where the plot\n * is and what value a pixel is.\n *\n * A transparent rectangle over the plot area that reads the drag. It does NOT stop the\n * event: recharts drives its tooltip from the same pointer move bubbling up to its own\n * wrapper, so swallowing it here would trade the tooltip for the zoom.\n */\nfunction ZoomLayer({\n axisIds,\n only,\n label,\n onZoom,\n onReset,\n}: {\n axisIds: string[];\n only?: \"x\" | \"y\";\n label: string;\n onZoom: (selection: ZoomSelection) => void;\n onReset: () => void;\n}) {\n const plot = usePlotArea();\n const xInverse = useXAxisInverseScale();\n // One probe COMPONENT per axis rather than a hook in a loop: the number of axes\n // changes as series are toggled, and a hook count that changes with the data is the\n // one thing React will not forgive.\n const [scales] = useState(() => new Map<string, InverseScaleFunction | undefined>());\n const [drag, setDrag] = useState<ZoomDrag | null>(null);\n // A finished drag is followed by a click on the same element, and a chart with an\n // `onClick` (keksdose opens a drilldown on one) would read the zoom as a pick.\n const draggedRef = useRef(false);\n\n if (!plot || plot.width <= 0 || plot.height <= 0) return null;\n\n const at = (event: { clientX: number; clientY: number; currentTarget: Element }) => {\n // Against the overlay's own screen box, so padding around the container cannot\n // shift every reading — plus the plot's offset, because the scales are in the\n // chart's coordinates rather than the plot's.\n const box = event.currentTarget.getBoundingClientRect();\n return { x: event.clientX - box.left + plot.x, y: event.clientY - box.top + plot.y };\n };\n\n const finish = () => {\n if (!drag) return;\n setDrag(null);\n const selection = selectionFromDrag(drag, plot, xInverse, (id) => scales.get(id), axisIds, only);\n draggedRef.current = selection !== null;\n if (selection) onZoom(selection);\n };\n\n // What the drag would do, shown while it is being made: a band paints the full width\n // or height, so the reader sees it is about to let the other axis refit.\n const which = drag\n ? zoomAxesFor(\n drag.toX - drag.fromX,\n drag.toY - drag.fromY,\n plot.width,\n plot.height,\n ZOOM_SQUARE_ENOUGH,\n only,\n )\n : null;\n\n return (\n <g>\n {axisIds.map((id) => (\n <AxisProbe key={id} axisId={id} scales={scales} />\n ))}\n {drag && which && (\n <rect\n x={which === \"y\" ? plot.x : Math.min(drag.fromX, drag.toX)}\n y={which === \"x\" ? plot.y : Math.min(drag.fromY, drag.toY)}\n width={which === \"y\" ? plot.width : Math.abs(drag.toX - drag.fromX)}\n height={which === \"x\" ? plot.height : Math.abs(drag.toY - drag.fromY)}\n className=\"fill-[var(--brand)]/15 stroke-[var(--brand)]\"\n strokeDasharray=\"4 3\"\n pointerEvents=\"none\"\n />\n )}\n <rect\n x={plot.x}\n y={plot.y}\n width={plot.width}\n height={plot.height}\n // `transparent`, not `none`: an unpainted shape takes no pointer events.\n fill=\"transparent\"\n role=\"application\"\n aria-label={label}\n style={{ cursor: \"crosshair\" }}\n onPointerDown={(event) => {\n if (event.button !== 0) return;\n draggedRef.current = false;\n const point = at(event);\n // Best effort: capture keeps a drag alive past the plot's edge, but a\n // pointer id the environment does not know throws — jsdom refuses them all —\n // and a zoom that threw on pointer-down would be a chart nobody could click.\n try {\n event.currentTarget.setPointerCapture(event.pointerId);\n } catch {\n /* the drag still works; it just stops at the edge of the plot */\n }\n setDrag({ fromX: point.x, fromY: point.y, toX: point.x, toY: point.y });\n }}\n onPointerMove={(event) => {\n if (!drag) return;\n const point = at(event);\n setDrag({ ...drag, toX: point.x, toY: point.y });\n }}\n onPointerUp={finish}\n onPointerCancel={() => setDrag(null)}\n onClick={(event) => {\n if (!draggedRef.current) return;\n draggedRef.current = false;\n event.stopPropagation();\n }}\n onDoubleClick={onReset}\n />\n </g>\n );\n}\n\n/** One axis's pixel→value function, parked where the pointer handler can reach it. */\nfunction AxisProbe({\n axisId,\n scales,\n}: {\n axisId: string;\n scales: Map<string, InverseScaleFunction | undefined>;\n}) {\n const inverse = useYAxisInverseScale(axisId);\n useEffect(() => {\n scales.set(axisId, inverse);\n return () => {\n scales.delete(axisId);\n };\n }, [axisId, inverse, scales]);\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -99,8 +99,8 @@ function CheckboxGroup({
|
|
|
99
99
|
}
|
|
100
100
|
) }),
|
|
101
101
|
showHint && // A `div`, not a `p`: a hint may be a node with a link in it.
|
|
102
|
-
/* @__PURE__ */ jsx("div", { id: hintId, className: "mt-1.5 text-
|
|
103
|
-
showError && /* @__PURE__ */ jsx("p", { id: errorId, className: "mt-1 text-
|
|
102
|
+
/* @__PURE__ */ jsx("div", { id: hintId, className: "mt-1.5 text-caption leading-tight text-[var(--text-muted)]", children: hint }),
|
|
103
|
+
showError && /* @__PURE__ */ jsx("p", { id: errorId, className: "mt-1 text-caption leading-tight text-[var(--danger)]", children: error })
|
|
104
104
|
]
|
|
105
105
|
}
|
|
106
106
|
);
|