@eifi1/ui-kit 0.4.0 → 0.5.0
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/LICENSE +32 -0
- package/README.md +498 -57
- package/dist/chart.d.ts +10 -0
- package/dist/chart.js +10 -0
- package/dist/chart.js.map +1 -0
- package/dist/components/account-settings.d.ts +11 -0
- package/dist/components/account-settings.js +19 -12
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +10 -6
- package/dist/components/alert-banner.js +20 -16
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +30 -4
- package/dist/components/amount-input.js +36 -14
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/calculator.d.ts +24 -7
- package/dist/components/calculator.js +37 -20
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.d.ts +156 -0
- package/dist/components/chart-zoom.js +262 -0
- package/dist/components/chart-zoom.js.map +1 -0
- package/dist/components/chart.d.ts +28 -3
- package/dist/components/chart.js +71 -32
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.d.ts +51 -0
- package/dist/components/checkbox.js +123 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip.d.ts +128 -0
- package/dist/components/chip.js +357 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/choice-parts.d.ts +24 -0
- package/dist/components/choice-parts.js +18 -0
- package/dist/components/choice-parts.js.map +1 -0
- package/dist/components/combobox-core.d.ts +52 -9
- package/dist/components/combobox-core.js +91 -20
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +43 -33
- package/dist/components/combobox.js +409 -340
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.d.ts +48 -8
- package/dist/components/currency-select.js +107 -65
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +15 -3
- package/dist/components/data-table-filter-popover.js +23 -17
- 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-filters.js +3 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +53 -2
- package/dist/components/data-table-labels.js +31 -2
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table-pagination.js +16 -10
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.js +1 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +201 -85
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -9
- package/dist/components/date-picker.js +232 -94
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dropdown.d.ts +96 -20
- package/dist/components/dropdown.js +77 -33
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +5 -4
- package/dist/components/entity-combobox.js +107 -76
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +36 -0
- package/dist/components/facing-pair.js +40 -0
- package/dist/components/facing-pair.js.map +1 -0
- package/dist/components/field-sync.d.ts +164 -0
- package/dist/components/field-sync.js +264 -0
- package/dist/components/field-sync.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +38 -6
- package/dist/components/file-dropzone.js +34 -9
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +37 -26
- package/dist/components/full-bleed-dialog.js +20 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/grouped-picker.d.ts +21 -10
- package/dist/components/grouped-picker.js +66 -52
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +20 -4
- package/dist/components/hover-menu.js +15 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +102 -14
- package/dist/components/mini-calendar.js +256 -70
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +9 -3
- package/dist/components/modal.js +34 -45
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +79 -0
- package/dist/components/month-picker.js +342 -0
- package/dist/components/month-picker.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +7 -6
- package/dist/components/multi-entity-combobox.js +112 -72
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +13 -11
- package/dist/components/multi-select.js +161 -84
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +92 -0
- package/dist/components/number-field.js +111 -0
- package/dist/components/number-field.js.map +1 -0
- package/dist/components/number-input.d.ts +28 -2
- package/dist/components/number-input.js +12 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +32 -4
- package/dist/components/numpad-sheet.js +30 -13
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-contents.d.ts +90 -0
- package/dist/components/page-contents.js +170 -0
- package/dist/components/page-contents.js.map +1 -0
- package/dist/components/password-strength.d.ts +106 -0
- package/dist/components/password-strength.js +132 -0
- package/dist/components/password-strength.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +23 -15
- package/dist/components/picker-sheet.js +34 -14
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.d.ts +62 -4
- package/dist/components/popover.js +52 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/search-field.d.ts +25 -7
- package/dist/components/search-field.js +7 -5
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +14 -0
- package/dist/components/series-chart-labels.js +11 -0
- package/dist/components/series-chart-labels.js.map +1 -0
- package/dist/components/series-chart.d.ts +207 -0
- package/dist/components/series-chart.js +223 -0
- package/dist/components/series-chart.js.map +1 -0
- package/dist/components/settings-fields.d.ts +14 -4
- package/dist/components/settings-fields.js +19 -8
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/signature-pad.d.ts +119 -0
- package/dist/components/signature-pad.js +405 -0
- package/dist/components/signature-pad.js.map +1 -0
- package/dist/components/slider.d.ts +80 -0
- package/dist/components/slider.js +155 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sparkline.d.ts +93 -0
- package/dist/components/sparkline.js +205 -0
- package/dist/components/sparkline.js.map +1 -0
- package/dist/components/stat-tile.d.ts +151 -0
- package/dist/components/stat-tile.js +236 -0
- package/dist/components/stat-tile.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +25 -2
- package/dist/components/swipeable-row.js +31 -0
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +60 -0
- package/dist/components/switch.js +81 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/time-input.d.ts +58 -0
- package/dist/components/time-input.js +50 -0
- package/dist/components/time-input.js.map +1 -0
- package/dist/components/toggle-group.d.ts +18 -3
- package/dist/components/toggle-group.js +10 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +115 -0
- package/dist/components/toggle-legend.js +114 -0
- package/dist/components/toggle-legend.js.map +1 -0
- package/dist/components/tooltip.d.ts +40 -11
- package/dist/components/tooltip.js +77 -20
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +132 -0
- package/dist/components/treemap.js +179 -0
- package/dist/components/treemap.js.map +1 -0
- package/dist/components/ui.d.ts +159 -46
- package/dist/components/ui.js +220 -115
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-mobile-reveal.js +1 -0
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/use-table-state.js +38 -17
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +15 -4
- package/dist/components/user-avatar.js +5 -4
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.js +3 -2
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
- package/dist/data-table.d.ts +7 -0
- package/dist/data-table.js +12 -0
- package/dist/data-table.js.map +1 -0
- package/dist/feedback/feedback-attachment.js +12 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +2 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +33 -7
- package/dist/feedback/feedback-inbox.js +49 -37
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.js +5 -0
- package/dist/feedback.js.map +1 -0
- package/dist/hooks/use-anchored-panel.js +1 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js +1 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-announce.d.ts +46 -0
- package/dist/hooks/use-announce.js +37 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.js +1 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -1
- package/dist/hooks/use-close-transition.js +1 -0
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-dismiss.js +1 -0
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +16 -0
- package/dist/hooks/use-focus-trap.js +67 -0
- package/dist/hooks/use-focus-trap.js.map +1 -0
- package/dist/hooks/use-media-query.js +1 -0
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +27 -1
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.js +1 -0
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.d.ts +31 -0
- package/dist/i18n/defaults.js +63 -0
- package/dist/i18n/defaults.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +250 -0
- package/dist/i18n/kit-labels.js +184 -0
- package/dist/i18n/kit-labels.js.map +1 -0
- package/dist/index.d.ts +47 -22
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/calc.js +1 -0
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/cn.js +1 -0
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/dates.js +1 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/safe-storage.js +1 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/search/command-palette.d.ts +11 -1
- package/dist/search/command-palette.js +21 -15
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search.d.ts +2 -0
- package/dist/search.js +3 -0
- package/dist/search.js.map +1 -0
- package/dist/shell/app-shell.d.ts +33 -4
- package/dist/shell/app-shell.js +275 -78
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +3 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +6 -0
- package/dist/shell/role-switcher.js +7 -4
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar.d.ts +15 -7
- package/dist/shell/top-bar.js +3 -5
- package/dist/shell/top-bar.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +3 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +5 -2
- package/dist/shell/topbar-controls.js +20 -15
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +13 -0
- package/dist/shell.js +8 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +19 -12
- package/dist/theme/chart-palette.js +24 -30
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/color.d.ts +78 -0
- package/dist/theme/color.js +148 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/theme/palette-derive.d.ts +140 -0
- package/dist/theme/palette-derive.js +376 -0
- package/dist/theme/palette-derive.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +27 -5
- package/dist/theme/palette-presets.js +231 -33
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +1 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour.d.ts +2 -1
- package/dist/tour/tour.js +15 -9
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.js +10 -8
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +18 -2
- package/dist/wizard/types.js +4 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +12 -4
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js +1 -0
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js +1 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.js +1 -0
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js +5 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +8 -0
- package/dist/wizard.js +9 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +67 -14
- package/src/chart.ts +22 -0
- package/src/components/account-settings.tsx +275 -0
- package/src/components/alert-banner.tsx +75 -0
- package/src/components/amount-input.tsx +471 -0
- package/src/components/calculator.tsx +252 -0
- package/src/components/chart-zoom.tsx +499 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +222 -0
- package/src/components/chip.tsx +570 -0
- package/src/components/choice-parts.ts +33 -0
- package/src/components/combobox-core.tsx +540 -0
- package/src/components/combobox.tsx +1026 -0
- package/src/components/currency-select.tsx +269 -0
- package/src/components/data-table-filter-popover.tsx +252 -0
- package/src/components/data-table-filters.ts +176 -0
- package/src/components/data-table-labels.ts +190 -0
- package/src/components/data-table-pagination.tsx +126 -0
- package/src/components/data-table-sort.ts +70 -0
- package/src/components/data-table.tsx +1740 -0
- package/src/components/date-picker.tsx +631 -0
- package/src/components/dropdown.tsx +341 -0
- package/src/components/entity-combobox.tsx +215 -0
- package/src/components/facing-pair.ts +86 -0
- package/src/components/field-sync.tsx +472 -0
- package/src/components/file-dropzone.tsx +181 -0
- package/src/components/full-bleed-dialog.tsx +179 -0
- package/src/components/grouped-picker.tsx +157 -0
- package/src/components/hover-menu.tsx +183 -0
- package/src/components/mini-calendar.tsx +554 -0
- package/src/components/modal.tsx +287 -0
- package/src/components/month-picker.tsx +549 -0
- package/src/components/multi-entity-combobox.tsx +226 -0
- package/src/components/multi-select.tsx +303 -0
- package/src/components/number-field.tsx +225 -0
- package/src/components/number-input.tsx +211 -0
- package/src/components/numpad-sheet.tsx +264 -0
- package/src/components/page-contents.tsx +289 -0
- package/src/components/password-strength.tsx +258 -0
- package/src/components/picker-sheet.tsx +251 -0
- package/src/components/popover.tsx +195 -0
- package/src/components/search-field.tsx +146 -0
- package/src/components/series-chart-labels.ts +26 -0
- package/src/components/series-chart.tsx +481 -0
- package/src/components/settings-fields.tsx +68 -0
- package/src/components/signature-pad.tsx +630 -0
- package/src/components/slider.tsx +313 -0
- package/src/components/sparkline.tsx +329 -0
- package/src/components/stat-tile.tsx +448 -0
- package/src/components/swipeable-row.tsx +271 -0
- package/src/components/switch.tsx +170 -0
- package/src/components/time-input.tsx +107 -0
- package/src/components/toggle-group.tsx +153 -0
- package/src/components/toggle-legend.tsx +229 -0
- package/src/components/tooltip.tsx +477 -0
- package/src/components/treemap.tsx +390 -0
- package/src/components/ui.tsx +1284 -0
- package/src/components/use-mobile-reveal.ts +54 -0
- package/src/components/use-table-state.ts +295 -0
- package/src/components/user-avatar.tsx +61 -0
- package/src/components/wizard-stepper.tsx +37 -0
- package/src/data-table.ts +19 -0
- package/src/feedback/feedback-attachment.tsx +246 -0
- package/src/feedback/feedback-dialog.tsx +209 -0
- package/src/feedback/feedback-inbox.tsx +615 -0
- package/src/feedback.ts +8 -0
- package/src/hooks/use-anchored-panel.ts +169 -0
- package/src/hooks/use-anchored-rect.ts +78 -0
- package/src/hooks/use-announce.ts +85 -0
- package/src/hooks/use-body-scroll-lock.ts +94 -0
- package/src/hooks/use-close-transition.ts +99 -0
- package/src/hooks/use-dismiss.ts +77 -0
- package/src/hooks/use-focus-trap.ts +160 -0
- package/src/hooks/use-media-query.ts +23 -0
- package/src/hooks/use-overlay-history.ts +345 -0
- package/src/hooks/use-row-swipe.ts +212 -0
- package/src/i18n/defaults.ts +68 -0
- package/src/i18n/kit-labels.tsx +447 -0
- package/src/index.ts +169 -0
- package/src/lib/calc.ts +309 -0
- package/src/lib/cn.ts +5 -0
- package/src/lib/dates.ts +187 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/safe-storage.ts +37 -0
- package/src/search/command-palette.tsx +342 -0
- package/src/search.ts +3 -0
- package/src/shell/app-shell.tsx +772 -0
- package/src/shell/option-switcher-menu.tsx +72 -0
- package/src/shell/role-switcher.tsx +46 -0
- package/src/shell/top-bar.tsx +39 -0
- package/src/shell/topbar-action-menu.tsx +91 -0
- package/src/shell/topbar-controls.tsx +229 -0
- package/src/shell.ts +10 -0
- package/src/theme/chart-palette.ts +145 -0
- package/src/theme/color.ts +266 -0
- package/src/theme/palette-derive.ts +681 -0
- package/src/theme/palette-presets.ts +760 -0
- package/src/theme/palette-store.ts +94 -0
- package/src/theme/theme-store.ts +145 -0
- package/src/tour/tour.tsx +586 -0
- package/src/tour.ts +3 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wizard/stepper-nav.tsx +221 -0
- package/src/wizard/types.ts +178 -0
- package/src/wizard/use-wizard.ts +345 -0
- package/src/wizard/validation.ts +37 -0
- package/src/wizard/wizard-context.tsx +29 -0
- package/src/wizard/wizard-step.tsx +14 -0
- package/src/wizard/wizard-summary.tsx +69 -0
- package/src/wizard.ts +14 -0
- package/tokens.css +222 -20
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import type {
|
|
4
|
+
ComponentPropsWithoutRef,
|
|
5
|
+
KeyboardEvent,
|
|
6
|
+
MouseEvent as ReactMouseEvent,
|
|
7
|
+
PointerEvent as ReactPointerEvent,
|
|
8
|
+
ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { cn } from "../lib/cn";
|
|
11
|
+
import { useFocusTrap } from "../hooks/use-focus-trap";
|
|
12
|
+
import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
|
|
13
|
+
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
14
|
+
import { useCloseTransition } from "../hooks/use-close-transition";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Drag the panel by its top strip, in pointer space (dev#460).
|
|
18
|
+
*
|
|
19
|
+
* Deliberately not a library and not persisted: the offset is component state that
|
|
20
|
+
* dies with the dialog. It starts only on a press that lands on the panel's own
|
|
21
|
+
* chrome — never on a field, a button or a link, which is what keeps "drag to select
|
|
22
|
+
* text in the textarea" working. Above `md` only: below it the panel is a
|
|
23
|
+
* full-width bottom sheet with nothing beside it to uncover.
|
|
24
|
+
*
|
|
25
|
+
* The whole gesture stays on the PANEL — captured pointer, React handlers — rather than
|
|
26
|
+
* on `window`. The first shape of this added `pointermove`/`pointerup` to `window` from
|
|
27
|
+
* inside the `pointerdown` handler and took them off again from the `pointerup` handler,
|
|
28
|
+
* so the teardown ran only if the gesture ended: React never sees a listener added from
|
|
29
|
+
* an event handler, and an unmount mid-drag left a live `pointermove` on `window` for
|
|
30
|
+
* the life of the page, pinning this hook's state with it. The one caller that enables
|
|
31
|
+
* dragging is the feedback composer, which closes ITSELF on submit — a Ctrl+Enter sent
|
|
32
|
+
* with the pointer still down is exactly that order of events, once per drag.
|
|
33
|
+
*/
|
|
34
|
+
function useDragOffset(enabled: boolean) {
|
|
35
|
+
const [offset, setOffset] = useState<{ x: number; y: number } | null>(null);
|
|
36
|
+
const from = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null);
|
|
37
|
+
|
|
38
|
+
const endDrag = () => {
|
|
39
|
+
from.current = null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
style: offset ? { transform: `translate(${offset.x}px, ${offset.y}px)` } : undefined,
|
|
44
|
+
handlers: !enabled
|
|
45
|
+
? undefined
|
|
46
|
+
: {
|
|
47
|
+
onPointerDown: (e: ReactPointerEvent<HTMLDivElement>) => {
|
|
48
|
+
if (e.button !== 0 || window.innerWidth < 768) return;
|
|
49
|
+
const target = e.target as HTMLElement;
|
|
50
|
+
// Only the panel itself and its non-interactive chrome start a drag; a
|
|
51
|
+
// press inside any control belongs to that control.
|
|
52
|
+
if (target.closest("input,textarea,select,button,a,[role='button']")) return;
|
|
53
|
+
from.current = { x: e.clientX, y: e.clientY, ox: offset?.x ?? 0, oy: offset?.y ?? 0 };
|
|
54
|
+
try {
|
|
55
|
+
// Capture is what lets the handlers live on the panel: every later
|
|
56
|
+
// pointer event for this pointer retargets here, so the drag keeps
|
|
57
|
+
// working once the pointer outruns the panel's own box — which it always
|
|
58
|
+
// does, since the panel moves out from under it.
|
|
59
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
60
|
+
} catch {
|
|
61
|
+
// jsdom implements no pointer capture, and a browser throws
|
|
62
|
+
// NotFoundError for a pointerId that is already gone. Neither is worth
|
|
63
|
+
// refusing the drag: uncaptured, the panel still gets the moves that
|
|
64
|
+
// land on it.
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
onPointerMove: (e: ReactPointerEvent<HTMLDivElement>) => {
|
|
68
|
+
const start = from.current;
|
|
69
|
+
if (!start) return;
|
|
70
|
+
setOffset({ x: start.ox + e.clientX - start.x, y: start.oy + e.clientY - start.y });
|
|
71
|
+
},
|
|
72
|
+
onPointerUp: endDrag,
|
|
73
|
+
// A cancel (the browser taking the pointer for a scroll or a gesture) and a
|
|
74
|
+
// lost capture both mean no `pointerup` is coming. Without them the drag would
|
|
75
|
+
// stay "live" and the next move over the panel would jump it.
|
|
76
|
+
onPointerCancel: endDrag,
|
|
77
|
+
onLostPointerCapture: endDrag,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Backdrop handlers that close only when a press starts AND ends on the
|
|
84
|
+
* backdrop itself. A plain onClick fires on mouseup, so dragging a text
|
|
85
|
+
* selection that starts inside the panel and releases over the backdrop would
|
|
86
|
+
* dismiss the dialog (feedback #254) — and the reverse drag would too, since
|
|
87
|
+
* click targets the common ancestor of the down/up elements.
|
|
88
|
+
*/
|
|
89
|
+
export function useBackdropClose(onClose: () => void) {
|
|
90
|
+
const pressStartedOnBackdrop = useRef(false);
|
|
91
|
+
return {
|
|
92
|
+
onMouseDown: (e: ReactMouseEvent<HTMLDivElement>) => {
|
|
93
|
+
pressStartedOnBackdrop.current = e.target === e.currentTarget;
|
|
94
|
+
},
|
|
95
|
+
onMouseUp: (e: ReactMouseEvent<HTMLDivElement>) => {
|
|
96
|
+
const started = pressStartedOnBackdrop.current;
|
|
97
|
+
pressStartedOnBackdrop.current = false;
|
|
98
|
+
if (started && e.target === e.currentTarget) onClose();
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Tab-cycle target set: the interactive descendants the focus trap rotates
|
|
104
|
+
// through. Querying live (per keystroke) means dynamically rendered fields are
|
|
105
|
+
// included automatically.
|
|
106
|
+
/**
|
|
107
|
+
* `extends ComponentPropsWithoutRef<"div">` so anything a `<div>` takes reaches the
|
|
108
|
+
* PANEL — the element with `role="dialog"`, which is the one a consumer means. That is
|
|
109
|
+
* where a `data-tour` anchor, a test id, an `aria-describedby` or an `aria-label` has
|
|
110
|
+
* to land; before this, a closed prop list dropped all four silently (audit §api-design).
|
|
111
|
+
*/
|
|
112
|
+
export interface ModalProps extends ComponentPropsWithoutRef<"div"> {
|
|
113
|
+
/** Invoked on backdrop click and on Escape. */
|
|
114
|
+
onClose: () => void;
|
|
115
|
+
children: ReactNode;
|
|
116
|
+
/**
|
|
117
|
+
* Panel max-width: `md` (28rem), `lg` (32rem), `xl` (48rem).
|
|
118
|
+
*
|
|
119
|
+
* `xl` exists because two callers had already written `className="max-w-3xl"` to
|
|
120
|
+
* reach it — the payee backfill's preview table and the price-merge dialog, both
|
|
121
|
+
* of which hold a list and several fields rather than one question. A caller
|
|
122
|
+
* patching a missing size IS the signal that the size is missing, and a
|
|
123
|
+
* hand-written width in a `className` is a width nothing else can line up with.
|
|
124
|
+
*
|
|
125
|
+
* The panel is `w-full` under every one of them, so the cap only bites once the
|
|
126
|
+
* viewport is wider than it: on a phone all three are the same full-width bottom
|
|
127
|
+
* sheet. Nothing here may introduce a `min-w-*`, which is what would break that.
|
|
128
|
+
*/
|
|
129
|
+
size?: "md" | "lg" | "xl";
|
|
130
|
+
/** Extra classes for the panel (e.g. `space-y-3` for form content spacing). */
|
|
131
|
+
className?: string;
|
|
132
|
+
/** id of the heading element, wired to `aria-labelledby`. */
|
|
133
|
+
labelledBy?: string;
|
|
134
|
+
/**
|
|
135
|
+
* Extra key handling on the panel (e.g. Ctrl/Cmd+Enter to submit). Runs first;
|
|
136
|
+
* if it calls `preventDefault()` the built-in Escape/Tab handling is skipped
|
|
137
|
+
* for that event.
|
|
138
|
+
*/
|
|
139
|
+
onKeyDown?: (e: KeyboardEvent<HTMLDivElement>) => void;
|
|
140
|
+
/**
|
|
141
|
+
* Drop the backdrop's mobile margin so the panel can reach the screen edges
|
|
142
|
+
* (a full-screen sheet on phones). The panel still needs its own
|
|
143
|
+
* `h-[100dvh] max-w-full rounded-none` classes via `className` to fill it; the
|
|
144
|
+
* desktop `md:p-4` margin is kept. Defaults to false (padded on all sizes).
|
|
145
|
+
*/
|
|
146
|
+
fullBleed?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Let the user drag the panel out of the way by its top edge (Keksdose feedback
|
|
149
|
+
* dev#460: *"Make the feedback dialog draggable so I can see behind it if it
|
|
150
|
+
* blocks something."*).
|
|
151
|
+
*
|
|
152
|
+
* Opt-in, because it only makes sense for a dialog whose content is ABOUT the page
|
|
153
|
+
* behind it — the feedback composer is the case, an ordinary form is not, and a
|
|
154
|
+
* draggable panel that has nothing to reveal is a control that can only get lost.
|
|
155
|
+
*
|
|
156
|
+
* Pointer-only by design: it is a "move this out of the way" gesture, not a layout
|
|
157
|
+
* choice worth persisting, and the position resets when the dialog closes. On a
|
|
158
|
+
* phone the panel is a bottom sheet the width of the screen, so dragging is
|
|
159
|
+
* suppressed there — there is nothing beside it to see.
|
|
160
|
+
*/
|
|
161
|
+
draggable?: boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Centered (bottom-sheet on mobile) modal dialog: dimmed backdrop, single
|
|
166
|
+
* rounded panel. Owns the overlay mechanics that the hand-rolled dialogs each
|
|
167
|
+
* re-implemented or lacked — backdrop-click + Escape to close, the platform Back
|
|
168
|
+
* gesture (Keksdose feedback #172), background scroll lock (feedback #204), focus
|
|
169
|
+
* into the panel on open and back to the trigger on close, a Tab focus trap, and
|
|
170
|
+
* `role="dialog"`/`aria-modal`.
|
|
171
|
+
*/
|
|
172
|
+
export function Modal({
|
|
173
|
+
onClose,
|
|
174
|
+
children,
|
|
175
|
+
size = "md",
|
|
176
|
+
className,
|
|
177
|
+
labelledBy,
|
|
178
|
+
onKeyDown,
|
|
179
|
+
fullBleed,
|
|
180
|
+
draggable,
|
|
181
|
+
style,
|
|
182
|
+
...rest
|
|
183
|
+
}: ModalProps) {
|
|
184
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
185
|
+
const drag = useDragOffset(Boolean(draggable));
|
|
186
|
+
// The panel lowers itself before the caller unmounts it (live #320 rework). Every
|
|
187
|
+
// dismissal below goes through `requestClose`; `onClose` still does the closing, one
|
|
188
|
+
// animation later. A caller that closes the dialog ITSELF — after a save, say —
|
|
189
|
+
// unmounts with no exit, which is the documented limit of this.
|
|
190
|
+
const { closing, requestClose } = useCloseTransition(onClose);
|
|
191
|
+
const backdropClose = useBackdropClose(requestClose);
|
|
192
|
+
// Back means the same as Escape here. On a phone Escape doesn't exist, so without
|
|
193
|
+
// this the only way out of a dialog is finding its close button — and Back, the
|
|
194
|
+
// gesture everyone reaches for, navigated the page underneath instead (#172).
|
|
195
|
+
// Mounted only while open, hence the constant `true`.
|
|
196
|
+
useOverlayHistory(true, requestClose);
|
|
197
|
+
|
|
198
|
+
// Mounted only while open, hence the constant `true`. Through the shared hook and
|
|
199
|
+
// not by hand: this component's own save/restore copy was one half of the pair that
|
|
200
|
+
// left the page permanently unscrollable when a dialog containing an open sheet was
|
|
201
|
+
// closed — see the note in use-body-scroll-lock.ts.
|
|
202
|
+
useBodyScrollLock(true);
|
|
203
|
+
|
|
204
|
+
// Focus the panel itself (not the first field) so opening doesn't pop the mobile
|
|
205
|
+
// keyboard, while still moving focus into the dialog for keyboard and screen-reader
|
|
206
|
+
// users. Through the shared hook since this component is where it came from — the
|
|
207
|
+
// hook adds per-keystroke recomputation, a guard on restoring to a detached node,
|
|
208
|
+
// and nesting, none of which this copy had.
|
|
209
|
+
useFocusTrap(panelRef, { active: true, initialFocus: "container" });
|
|
210
|
+
|
|
211
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
212
|
+
onKeyDown?.(e);
|
|
213
|
+
if (e.defaultPrevented) return;
|
|
214
|
+
if (e.key === "Escape") {
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
requestClose();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Tab containment lives in useFocusTrap, which listens on the panel itself.
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Portal to <body> so the `fixed inset-0 z-50` overlay escapes whatever stacking
|
|
223
|
+
// context its caller sits in. Rendered inline, a modal opened from e.g. the top
|
|
224
|
+
// bar (a z-30 context) could paint BELOW a sibling z-30 sticky element like the
|
|
225
|
+
// expanded transaction editor (feedback #320). The tour/command-palette overlays
|
|
226
|
+
// already portal for the same reason.
|
|
227
|
+
return createPortal(
|
|
228
|
+
<div
|
|
229
|
+
className={cn(
|
|
230
|
+
// Live #320. The panel below rises only where it is bottom-anchored: from md up
|
|
231
|
+
// it is centred, and a centred box sliding up from off-screen reads as a
|
|
232
|
+
// different component arriving rather than as the same one settling.
|
|
233
|
+
closing ? "animate-overlay-out" : "animate-overlay",
|
|
234
|
+
"fixed inset-0 z-50 flex items-end justify-center bg-black/40 md:items-center",
|
|
235
|
+
fullBleed ? "p-0 md:p-4" : "p-4",
|
|
236
|
+
)}
|
|
237
|
+
{...backdropClose}
|
|
238
|
+
>
|
|
239
|
+
<div
|
|
240
|
+
// `...rest` FIRST, everything the dialog needs to be a dialog after it. A
|
|
241
|
+
// wrapper that spreads its own props through to this one cannot then take away
|
|
242
|
+
// the role, the modality or the `tabIndex={-1}` that `useFocusTrap` needs —
|
|
243
|
+
// losing any of the three is silent, and the symptom (a dialog a screen reader
|
|
244
|
+
// walks straight out of) shows up nowhere near the call site that caused it.
|
|
245
|
+
{...rest}
|
|
246
|
+
ref={panelRef}
|
|
247
|
+
role="dialog"
|
|
248
|
+
aria-modal="true"
|
|
249
|
+
aria-labelledby={labelledBy}
|
|
250
|
+
tabIndex={-1}
|
|
251
|
+
onKeyDown={handleKeyDown}
|
|
252
|
+
{...drag.handlers}
|
|
253
|
+
// MERGED, not replaced: `drag.style` is undefined until a drag starts, so
|
|
254
|
+
// assigning it outright would drop a caller's own `style` on every render but
|
|
255
|
+
// the ones where the panel is being dragged.
|
|
256
|
+
style={{ ...style, ...drag.style }}
|
|
257
|
+
className={cn(
|
|
258
|
+
closing ? "animate-sheet-out md:animate-none" : "animate-sheet md:animate-none",
|
|
259
|
+
"w-full rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-sm outline-none",
|
|
260
|
+
{ md: "max-w-md", lg: "max-w-lg", xl: "max-w-3xl" }[size],
|
|
261
|
+
// A panel taller than the screen has to scroll ITSELF. The backdrop is
|
|
262
|
+
// `fixed inset-0` and the body is scroll-locked while a dialog is open, so
|
|
263
|
+
// without this the overflow simply had nowhere to go: on a phone the panel
|
|
264
|
+
// is bottom-anchored, so it grew UPWARD past the top edge and the part that
|
|
265
|
+
// went past it could not be reached by any gesture (Keksdose live #254, on
|
|
266
|
+
// the price-merge dialog — but it was every dialog with more than a screen
|
|
267
|
+
// of content). `max-h-full` resolves against the backdrop's content box,
|
|
268
|
+
// which is the viewport minus its own padding, so the panel stops exactly
|
|
269
|
+
// where the screen does; `overscroll-contain` keeps the fling inside it.
|
|
270
|
+
//
|
|
271
|
+
// Three callers had already patched themselves with `max-h-[90vh]` +
|
|
272
|
+
// an inner scroller. Those still win — `cn` is tailwind-merge — and this is
|
|
273
|
+
// the default they should not have needed.
|
|
274
|
+
"max-h-full overflow-y-auto overscroll-contain",
|
|
275
|
+
"p-4",
|
|
276
|
+
// The grab affordance sits on the panel's own top strip: a drag handle of
|
|
277
|
+
// its own would be one more control in a dialog that is mostly one field.
|
|
278
|
+
draggable && "md:[&>*:first-child]:cursor-grab",
|
|
279
|
+
className,
|
|
280
|
+
)}
|
|
281
|
+
>
|
|
282
|
+
{children}
|
|
283
|
+
</div>
|
|
284
|
+
</div>,
|
|
285
|
+
document.body,
|
|
286
|
+
);
|
|
287
|
+
}
|