@eifi1/ui-kit 0.4.1 → 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 +89 -19
- 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 +74 -19
- package/dist/components/dropdown.js +72 -22
- 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 +21 -7
- package/dist/components/file-dropzone.js +28 -7
- 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 +21 -2
- package/dist/components/number-input.js +2 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +25 -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 +5 -3
- package/dist/components/user-avatar.js +4 -2
- 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 +14 -1
- package/dist/theme/palette-presets.js +231 -3
- 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 +66 -13
- 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,246 @@
|
|
|
1
|
+
import { cn } from "../lib/cn";
|
|
2
|
+
import { useEffect, useRef, useState, type RefObject } from "react";
|
|
3
|
+
import { Camera, FileText, Paperclip, X } from "lucide-react";
|
|
4
|
+
import { Button } from "../components/ui";
|
|
5
|
+
import { useKitFileLabels } from "../i18n/kit-labels";
|
|
6
|
+
import type { FeedbackAttachmentLabels } from "./feedback-dialog";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_ATTACHMENT_ACCEPT = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
9
|
+
export const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Picking one picture: the file dialog, a capture of the app view, or a paste.
|
|
13
|
+
*
|
|
14
|
+
* Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can
|
|
15
|
+
* have it too (Steering Design feedback #128). A report is a conversation — it
|
|
16
|
+
* is filed, it is answered, and the reporter sends it back saying that is not
|
|
17
|
+
* what they meant — and the screenshot showing what they mean is taken at
|
|
18
|
+
* whichever of those points they looked. Only the first of them had a way to
|
|
19
|
+
* attach one, so everything after it had to be described in words.
|
|
20
|
+
*
|
|
21
|
+
* **Three ways in, and they are three because no one of them covers the others.**
|
|
22
|
+
* The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the
|
|
23
|
+
* *whole* app view; and the clipboard is where a region snip already is on every
|
|
24
|
+
* platform (Steering Design feedback #39). Somebody who wanted to show one panel
|
|
25
|
+
* had to save a crop and then find it again.
|
|
26
|
+
*
|
|
27
|
+
* `documentPaste` and `pasteFrom` decide where the paste is listened for, and
|
|
28
|
+
* it is a real choice rather than a flag with a default. A modal traps focus,
|
|
29
|
+
* so while it is up every paste in the page is meant for it — including one
|
|
30
|
+
* made with nothing in particular focused, which never reaches a React
|
|
31
|
+
* `onPaste` on a child: that is `documentPaste`. An editor **inline on a
|
|
32
|
+
* page** is not that: the page around it has its own fields, so it listens
|
|
33
|
+
* within its own subtree and a paste elsewhere stays where it was aimed. But
|
|
34
|
+
* "its own subtree" has to include the text box the paste is actually made in,
|
|
35
|
+
* and that box is this field's *sibling*, not its child — a paste in it bubbles
|
|
36
|
+
* to their common parent and never through here. `pasteFrom` is that parent
|
|
37
|
+
* (Steering Design feedback #140): the element whose subtree is listened to,
|
|
38
|
+
* handed in by whoever renders both the box and this field side by side.
|
|
39
|
+
*/
|
|
40
|
+
export function FeedbackAttachmentField({
|
|
41
|
+
value,
|
|
42
|
+
onChange,
|
|
43
|
+
labels,
|
|
44
|
+
accept = DEFAULT_ATTACHMENT_ACCEPT,
|
|
45
|
+
maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
46
|
+
onError,
|
|
47
|
+
onCaptureScreenshot,
|
|
48
|
+
documentPaste = false,
|
|
49
|
+
pasteFrom,
|
|
50
|
+
className,
|
|
51
|
+
}: {
|
|
52
|
+
value: File | null;
|
|
53
|
+
onChange: (file: File | null) => void;
|
|
54
|
+
labels: FeedbackAttachmentLabels;
|
|
55
|
+
accept?: string[];
|
|
56
|
+
maxBytes?: number;
|
|
57
|
+
onError?: (kind: "type" | "size") => void;
|
|
58
|
+
/** Snapshot the app view behind this and return it as a File. A "Capture
|
|
59
|
+
* screenshot" button appears only when it is given. */
|
|
60
|
+
onCaptureScreenshot?: () => Promise<File | null>;
|
|
61
|
+
/** Listen for the paste on `document` rather than on this field's own
|
|
62
|
+
* subtree. For a modal, which owns the whole page while it is up. */
|
|
63
|
+
documentPaste?: boolean;
|
|
64
|
+
/** Listen for the paste within this element's subtree rather than this
|
|
65
|
+
* field's own — for a field standing *beside* the text box a paste is made
|
|
66
|
+
* in, whose common parent is where the event bubbles to. Ignored when
|
|
67
|
+
* `documentPaste` is set. */
|
|
68
|
+
pasteFrom?: RefObject<HTMLElement | null>;
|
|
69
|
+
className?: string;
|
|
70
|
+
}) {
|
|
71
|
+
const [preview, setPreview] = useState<string | null>(null);
|
|
72
|
+
const [capturing, setCapturing] = useState(false);
|
|
73
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
74
|
+
// `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.
|
|
75
|
+
const fileText = useKitFileLabels();
|
|
76
|
+
|
|
77
|
+
// Object-URL preview lifecycle (create on change, revoke on cleanup).
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!value) {
|
|
80
|
+
setPreview(null);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const url = URL.createObjectURL(value);
|
|
84
|
+
setPreview(url);
|
|
85
|
+
return () => URL.revokeObjectURL(url);
|
|
86
|
+
}, [value]);
|
|
87
|
+
|
|
88
|
+
const pick = (file: File | undefined | null) => {
|
|
89
|
+
if (!file) return;
|
|
90
|
+
if (!accept.includes(file.type)) {
|
|
91
|
+
onError?.("type");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (file.size > maxBytes) {
|
|
95
|
+
onError?.("size");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
onChange(file);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// The paste handler is registered once rather than per render, so it reads
|
|
102
|
+
// `pick` — which closes over props that change identity on every render — out
|
|
103
|
+
// of a ref rather than out of its own dependency list.
|
|
104
|
+
const latest = useRef(pick);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
latest.current = pick;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const takeImage = (clipboard: DataTransfer | null, stop: () => void) => {
|
|
110
|
+
const items = Array.from(clipboard?.items ?? []);
|
|
111
|
+
const image = items.find((item) => item.kind === "file" && item.type.startsWith("image/"));
|
|
112
|
+
const file = image?.getAsFile();
|
|
113
|
+
if (!file) return;
|
|
114
|
+
// Only once there IS an image: a paste of text into a field must stay a
|
|
115
|
+
// paste of text, and a clipboard holding both is a copy whose text half is
|
|
116
|
+
// what the field was focused for.
|
|
117
|
+
stop();
|
|
118
|
+
// Clipboard images arrive named "image.png" at best and unnamed at worst,
|
|
119
|
+
// and the name is what the inbox shows beside the thumbnail. A name that
|
|
120
|
+
// says where it came from is more use than the browser's.
|
|
121
|
+
latest.current(new File([file], pastedName(file.type), { type: file.type }));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// The element listened on, where it is not this field's own subtree: the
|
|
125
|
+
// document for a modal, the given parent for an inline editor. Read inside
|
|
126
|
+
// the effect rather than in render, because a ref's `current` is only set
|
|
127
|
+
// once the parent has mounted — which it has by the time effects run.
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);
|
|
130
|
+
if (!target) return;
|
|
131
|
+
const onPaste = (event: Event) => {
|
|
132
|
+
const clipboard = (event as ClipboardEvent).clipboardData;
|
|
133
|
+
takeImage(clipboard, () => event.preventDefault());
|
|
134
|
+
};
|
|
135
|
+
target.addEventListener("paste", onPaste);
|
|
136
|
+
return () => target.removeEventListener("paste", onPaste);
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- takeImage reads `latest`, which is a ref
|
|
138
|
+
}, [documentPaste, pasteFrom]);
|
|
139
|
+
const listensElsewhere = documentPaste || pasteFrom !== undefined;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div
|
|
143
|
+
// `relative` for the same reason as FileDropzone: the `sr-only` file input below
|
|
144
|
+
// is `position: absolute`, and without a positioned ancestor it is laid out
|
|
145
|
+
// against the initial containing block — extending the DOCUMENT height to its own
|
|
146
|
+
// offset and producing a phantom second scrollbar on any long page.
|
|
147
|
+
className={cn("relative", className)}
|
|
148
|
+
onPaste={
|
|
149
|
+
listensElsewhere
|
|
150
|
+
? undefined
|
|
151
|
+
: (event) => takeImage(event.clipboardData, () => event.preventDefault())
|
|
152
|
+
}
|
|
153
|
+
>
|
|
154
|
+
{labels.attachment && (
|
|
155
|
+
<div className="mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]">
|
|
156
|
+
{labels.attachment}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
{value ? (
|
|
160
|
+
<div className="flex items-start gap-2">
|
|
161
|
+
{value.type.startsWith("image/") && preview ? (
|
|
162
|
+
<img
|
|
163
|
+
src={preview}
|
|
164
|
+
alt={value.name}
|
|
165
|
+
className="h-20 w-20 rounded border border-[var(--border)] object-cover"
|
|
166
|
+
/>
|
|
167
|
+
) : (
|
|
168
|
+
// Non-image attachments (PDF, text) can't preview as an <img>, so
|
|
169
|
+
// show a neutral file tile with the name/size beside it instead.
|
|
170
|
+
<div className="flex h-20 w-20 items-center justify-center rounded border border-[var(--border)] text-[var(--text-placeholder)]">
|
|
171
|
+
<FileText className="size-8" />
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
<div className="min-w-0 flex-1">
|
|
175
|
+
<div className="truncate text-sm text-[var(--text-secondary)]">{value.name}</div>
|
|
176
|
+
<div className="text-xs text-[var(--text-muted)]">
|
|
177
|
+
{fileText.size(value.size)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={() => onChange(null)}
|
|
183
|
+
aria-label={labels.attachmentRemove}
|
|
184
|
+
className="rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
185
|
+
>
|
|
186
|
+
<X className="size-4" />
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
) : (
|
|
190
|
+
<div className="space-y-1.5">
|
|
191
|
+
<div className="flex flex-wrap gap-2">
|
|
192
|
+
<Button type="button" variant="secondary" onClick={() => fileInputRef.current?.click()}>
|
|
193
|
+
<Paperclip className="size-4" /> {labels.attachmentAdd}
|
|
194
|
+
</Button>
|
|
195
|
+
{onCaptureScreenshot && (
|
|
196
|
+
<Button
|
|
197
|
+
type="button"
|
|
198
|
+
variant="secondary"
|
|
199
|
+
disabled={capturing}
|
|
200
|
+
onClick={() => {
|
|
201
|
+
if (capturing) return;
|
|
202
|
+
setCapturing(true);
|
|
203
|
+
void onCaptureScreenshot()
|
|
204
|
+
.then((file) => {
|
|
205
|
+
if (file) pick(file);
|
|
206
|
+
})
|
|
207
|
+
.finally(() => setCapturing(false));
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
<Camera className="size-4" />{" "}
|
|
211
|
+
{capturing ? "…" : (labels.attachmentCapture ?? "Capture screenshot")}
|
|
212
|
+
</Button>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
{/* Said out loud, because a gesture with no affordance is a gesture
|
|
216
|
+
nobody finds. */}
|
|
217
|
+
<p className="text-xs text-[var(--text-muted)]">
|
|
218
|
+
{labels.attachmentPaste ?? "…or paste a screenshot from the clipboard."}
|
|
219
|
+
</p>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
<input
|
|
223
|
+
ref={fileInputRef}
|
|
224
|
+
type="file"
|
|
225
|
+
accept={accept.join(",")}
|
|
226
|
+
className="sr-only"
|
|
227
|
+
onChange={(e) => {
|
|
228
|
+
pick(e.target.files?.[0]);
|
|
229
|
+
e.target.value = "";
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** What a pasted image is called once it is an attachment.
|
|
237
|
+
*
|
|
238
|
+
* The extension is read off the mime type rather than assumed to be `.png`:
|
|
239
|
+
* Safari puts TIFF on the clipboard and a file called `pasted.png` that is not
|
|
240
|
+
* a PNG is one the receiving end opens wrong. */
|
|
241
|
+
export function pastedName(type: string): string {
|
|
242
|
+
const subtype = type.split("/")[1] ?? "png";
|
|
243
|
+
// `image/svg+xml` and friends carry a suffix that is not part of the
|
|
244
|
+
// extension, and none of them are in the accepted list anyway.
|
|
245
|
+
return `pasted.${subtype.split("+")[0]}`;
|
|
246
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Button, Input, PHONE_QUERY, Select, Textarea } from "../components/ui";
|
|
4
|
+
import { Modal } from "../components/modal";
|
|
5
|
+
import { useMediaQuery } from "../hooks/use-media-query";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_ATTACHMENT_ACCEPT,
|
|
8
|
+
DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
9
|
+
FeedbackAttachmentField,
|
|
10
|
+
} from "./feedback-attachment";
|
|
11
|
+
|
|
12
|
+
export interface FeedbackCategoryOption {
|
|
13
|
+
value: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FeedbackAttachmentLabels {
|
|
18
|
+
/** The heading over the field. Optional: a note editor puts the buttons
|
|
19
|
+
* straight under its textarea, where a second heading is noise. */
|
|
20
|
+
attachment?: string;
|
|
21
|
+
attachmentAdd: string;
|
|
22
|
+
/** Label for the "capture screenshot" button. Optional — falls back to an English default. */
|
|
23
|
+
attachmentCapture?: string;
|
|
24
|
+
/** The line under the attachment buttons saying a screenshot can be pasted
|
|
25
|
+
* straight in. Optional — falls back to an English default. */
|
|
26
|
+
attachmentPaste?: string;
|
|
27
|
+
attachmentRemove: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FeedbackDialogLabels extends FeedbackAttachmentLabels {
|
|
31
|
+
title: string;
|
|
32
|
+
category: string;
|
|
33
|
+
subject: string;
|
|
34
|
+
body: string;
|
|
35
|
+
attachment: string;
|
|
36
|
+
submitHint: string;
|
|
37
|
+
cancel: string;
|
|
38
|
+
save: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FeedbackSubmission {
|
|
42
|
+
title: string;
|
|
43
|
+
body: string;
|
|
44
|
+
category: string;
|
|
45
|
+
attachment: File | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The generic feedback form dialog: category + subject + body + an optional image
|
|
50
|
+
* attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the
|
|
51
|
+
* category options, labels and an `onSubmit` that talks to its own backend, plus
|
|
52
|
+
* an optional `contextSlot` for app-specific context (user, current URL, …).
|
|
53
|
+
*
|
|
54
|
+
* **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog
|
|
55
|
+
* takes an image off the clipboard and makes it the attachment, through the same
|
|
56
|
+
* validation and the same preview as the file picker. It is the gesture the two
|
|
57
|
+
* ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,
|
|
58
|
+
* and the file picker needs a file — so somebody who wanted to show one panel,
|
|
59
|
+
* or one region of one, had to save a crop to disk first and then find it again
|
|
60
|
+
* (Steering Design feedback #39). The clipboard is where a region snip already
|
|
61
|
+
* is on every platform.
|
|
62
|
+
*/
|
|
63
|
+
export function FeedbackDialog({
|
|
64
|
+
open,
|
|
65
|
+
onClose,
|
|
66
|
+
categories,
|
|
67
|
+
category,
|
|
68
|
+
onCategoryChange,
|
|
69
|
+
labels,
|
|
70
|
+
onSubmit,
|
|
71
|
+
submitting = false,
|
|
72
|
+
contextSlot,
|
|
73
|
+
attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,
|
|
74
|
+
maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
75
|
+
onAttachmentError,
|
|
76
|
+
onCaptureScreenshot,
|
|
77
|
+
}: {
|
|
78
|
+
open: boolean;
|
|
79
|
+
onClose: () => void;
|
|
80
|
+
categories: FeedbackCategoryOption[];
|
|
81
|
+
category: string;
|
|
82
|
+
onCategoryChange: (value: string) => void;
|
|
83
|
+
labels: FeedbackDialogLabels;
|
|
84
|
+
onSubmit: (data: FeedbackSubmission) => void | Promise<void>;
|
|
85
|
+
submitting?: boolean;
|
|
86
|
+
contextSlot?: ReactNode;
|
|
87
|
+
attachmentAccept?: string[];
|
|
88
|
+
maxAttachmentBytes?: number;
|
|
89
|
+
onAttachmentError?: (kind: "type" | "size") => void;
|
|
90
|
+
/**
|
|
91
|
+
* Optional: capture a screenshot of the underlying app view and return it as a
|
|
92
|
+
* File. When provided, a "Capture screenshot" button is shown next to "Add
|
|
93
|
+
* attachment"; the returned file is fed through the same validation + preview.
|
|
94
|
+
*/
|
|
95
|
+
onCaptureScreenshot?: () => Promise<File | null>;
|
|
96
|
+
}) {
|
|
97
|
+
const [title, setTitle] = useState("");
|
|
98
|
+
const [body, setBody] = useState("");
|
|
99
|
+
const [attachment, setAttachment] = useState<File | null>(null);
|
|
100
|
+
const isMobile = useMediaQuery(PHONE_QUERY, false);
|
|
101
|
+
|
|
102
|
+
// Reset the form whenever the dialog is (re)opened.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (open) {
|
|
105
|
+
setTitle("");
|
|
106
|
+
setBody("");
|
|
107
|
+
setAttachment(null);
|
|
108
|
+
}
|
|
109
|
+
}, [open]);
|
|
110
|
+
|
|
111
|
+
const canSubmit = !!title && !!body && !submitting;
|
|
112
|
+
const trySubmit = () => {
|
|
113
|
+
if (canSubmit) void onSubmit({ title, body, category, attachment });
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (!open) return null;
|
|
117
|
+
|
|
118
|
+
const categoryField = (
|
|
119
|
+
<Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>
|
|
120
|
+
{categories.map((c) => (
|
|
121
|
+
<option key={c.value} value={c.value}>
|
|
122
|
+
{c.label}
|
|
123
|
+
</option>
|
|
124
|
+
))}
|
|
125
|
+
</Select>
|
|
126
|
+
);
|
|
127
|
+
// On a phone the subject leads as a heading rather than as the second of two
|
|
128
|
+
// identical boxes under a select (Keksdose feedback #179). `variant="display"`
|
|
129
|
+
// is a no-op above the phone breakpoint, so the desktop dialog is unchanged.
|
|
130
|
+
const subjectField = (
|
|
131
|
+
<Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant="display" />
|
|
132
|
+
);
|
|
133
|
+
const bodyField = (
|
|
134
|
+
<Textarea rows={5} label={labels.body} value={body} onChange={(e) => setBody(e.target.value)} />
|
|
135
|
+
);
|
|
136
|
+
const attachmentField = (
|
|
137
|
+
<FeedbackAttachmentField
|
|
138
|
+
value={attachment}
|
|
139
|
+
onChange={setAttachment}
|
|
140
|
+
labels={labels}
|
|
141
|
+
accept={attachmentAccept}
|
|
142
|
+
maxBytes={maxAttachmentBytes}
|
|
143
|
+
onError={onAttachmentError}
|
|
144
|
+
onCaptureScreenshot={onCaptureScreenshot}
|
|
145
|
+
// On `document`, not on the panel: the Modal focuses its own panel on open
|
|
146
|
+
// and traps Tab inside it, so while this dialog is up every paste in the
|
|
147
|
+
// page is meant for it — including the one made with nothing in
|
|
148
|
+
// particular focused, which never reaches a React `onPaste` on a child.
|
|
149
|
+
documentPaste={open}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Modal
|
|
155
|
+
onClose={onClose}
|
|
156
|
+
// The one dialog whose subject is the page behind it (Keksdose dev#460: "Make
|
|
157
|
+
// the feedback dialog draggable so I can see behind it if it blocks
|
|
158
|
+
// something") — you are describing what is under it while you type.
|
|
159
|
+
draggable
|
|
160
|
+
onKeyDown={(e) => {
|
|
161
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
trySubmit();
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,
|
|
168
|
+
and the heading is the strip a user reaches for. */}
|
|
169
|
+
<h2 className="mb-3 text-lg font-semibold">{labels.title}</h2>
|
|
170
|
+
<div className="space-y-3">
|
|
171
|
+
{isMobile ? (
|
|
172
|
+
// Phone shape, the same reasoning as the transaction editor's (#177):
|
|
173
|
+
// the desktop order is the DATA MODEL's order — category, then subject,
|
|
174
|
+
// then body — but nobody opens this dialog to pick a category. What they
|
|
175
|
+
// came to do is say the thing, so that leads; the classification and the
|
|
176
|
+
// evidence follow, a tier down.
|
|
177
|
+
<>
|
|
178
|
+
{subjectField}
|
|
179
|
+
{bodyField}
|
|
180
|
+
<div className="space-y-3 border-t border-[var(--border)] pt-3">
|
|
181
|
+
{categoryField}
|
|
182
|
+
{attachmentField}
|
|
183
|
+
{contextSlot}
|
|
184
|
+
</div>
|
|
185
|
+
</>
|
|
186
|
+
) : (
|
|
187
|
+
<>
|
|
188
|
+
{categoryField}
|
|
189
|
+
{subjectField}
|
|
190
|
+
{bodyField}
|
|
191
|
+
{attachmentField}
|
|
192
|
+
{contextSlot}
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
<div className="flex items-center justify-between gap-2">
|
|
196
|
+
<div className="text-xs text-[var(--text-placeholder)]">{labels.submitHint}</div>
|
|
197
|
+
<div className="flex gap-2">
|
|
198
|
+
<Button variant="ghost" onClick={onClose}>
|
|
199
|
+
{labels.cancel}
|
|
200
|
+
</Button>
|
|
201
|
+
<Button onClick={trySubmit} disabled={!canSubmit}>
|
|
202
|
+
{labels.save}
|
|
203
|
+
</Button>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</Modal>
|
|
208
|
+
);
|
|
209
|
+
}
|