@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/wizard-stepper.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\n\n/** Compact step indicator for the import wizards. Past steps are plain, the\n * current one is highlighted, future ones muted. */\nexport function WizardStepper({\n steps,\n current,\n ariaLabel,\n}: {\n steps: { key: string; label: string }[];\n current: string;\n ariaLabel?: string;\n}) {\n const currentIndex = steps.findIndex((s) => s.key === current);\n return (\n <ol className=\"flex items-center gap-1 text-xs\" aria-label={ariaLabel}>\n {steps.map((step, i) => (\n <li key={step.key} className=\"flex items-center gap-1\">\n {i > 0 && <span className=\"w-4 border-t border-
|
|
1
|
+
{"version":3,"sources":["../../src/components/wizard-stepper.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\n\n/** Compact step indicator for the import wizards. Past steps are plain, the\n * current one is highlighted, future ones muted. */\nexport function WizardStepper({\n steps,\n current,\n ariaLabel,\n}: {\n steps: { key: string; label: string }[];\n current: string;\n ariaLabel?: string;\n}) {\n const currentIndex = steps.findIndex((s) => s.key === current);\n return (\n <ol className=\"flex items-center gap-1 text-xs\" aria-label={ariaLabel}>\n {steps.map((step, i) => (\n <li key={step.key} className=\"flex items-center gap-1\">\n {i > 0 && <span className=\"w-4 border-t border-[var(--border)]\" />}\n <span\n aria-current={step.key === current ? \"step\" : undefined}\n className={cn(\n \"rounded-full px-2 py-0.5\",\n i === currentIndex\n ? \"bg-[var(--bg-inverse)] text-[var(--text-inverse)] font-medium\"\n : i < currentIndex\n ? \"text-[var(--text-secondary)]\"\n : \"text-[var(--text-placeholder)]\",\n )}\n >\n {step.label}\n </span>\n </li>\n ))}\n </ol>\n );\n}\n"],"mappings":";AAiBQ,SACY,KADZ;AAjBR,SAAS,UAAU;AAIZ,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,eAAe,MAAM,UAAU,CAAC,MAAM,EAAE,QAAQ,OAAO;AAC7D,SACE,oBAAC,QAAG,WAAU,mCAAkC,cAAY,WACzD,gBAAM,IAAI,CAAC,MAAM,MAChB,qBAAC,QAAkB,WAAU,2BAC1B;AAAA,QAAI,KAAK,oBAAC,UAAK,WAAU,uCAAsC;AAAA,IAChE;AAAA,MAAC;AAAA;AAAA,QACC,gBAAc,KAAK,QAAQ,UAAU,SAAS;AAAA,QAC9C,WAAW;AAAA,UACT;AAAA,UACA,MAAM,eACF,kEACA,IAAI,eACF,iCACA;AAAA,QACR;AAAA,QAEC,eAAK;AAAA;AAAA,IACR;AAAA,OAdO,KAAK,GAed,CACD,GACH;AAEJ;","names":[]}
|
|
@@ -145,13 +145,29 @@ interface DataTableProps<T> {
|
|
|
145
145
|
isSelectable?: (row: T) => boolean;
|
|
146
146
|
isSelected: (row: T) => boolean;
|
|
147
147
|
onToggle: (row: T, checked: boolean) => void;
|
|
148
|
+
/**
|
|
149
|
+
* Commit a whole Shift+click range in ONE call (audit 2026-09-22).
|
|
150
|
+
*
|
|
151
|
+
* Optional and purely additive: without it the range is still walked row by
|
|
152
|
+
* row, exactly as before. With it, an owner holding a `Set` copies that Set
|
|
153
|
+
* once instead of once per row — a 400-row range was 400 `onToggle` calls,
|
|
154
|
+
* 400 Set copies and 400 renders of this table, all to reach a state the
|
|
155
|
+
* owner could have reached in one.
|
|
156
|
+
*
|
|
157
|
+
* `rows` is already filtered by `isSelectable` and is in the order the table
|
|
158
|
+
* is showing, so it can be applied as-is.
|
|
159
|
+
*/
|
|
160
|
+
onToggleMany?: (rows: T[], checked: boolean) => void;
|
|
148
161
|
allSelected: boolean;
|
|
149
162
|
someSelected: boolean;
|
|
150
163
|
onToggleAll: (checked: boolean) => void;
|
|
151
164
|
};
|
|
152
|
-
/** User-facing strings (English defaults); pass translated overrides.
|
|
165
|
+
/** User-facing strings (English defaults); pass translated overrides. Merged over
|
|
166
|
+
* `dataTable` from `<UiKitProvider labels>`, so a call site only states what
|
|
167
|
+
* differs for THIS table — typically `table`, its accessible name. */
|
|
153
168
|
labels?: Partial<DataTableLabels>;
|
|
154
|
-
/** BCP-47 locale for the date
|
|
169
|
+
/** BCP-47 locale for the date filter's calendar and the pager's numbers. Falls
|
|
170
|
+
* back to `<UiKitProvider locale>`, then to the runtime's default. */
|
|
155
171
|
locale?: string;
|
|
156
172
|
/** localStorage namespace prefix for `storageKey` persistence. Defaults to
|
|
157
173
|
* "hbui-table:"; pass your app's own prefix to keep a stable namespace. */
|
|
@@ -204,7 +220,7 @@ interface DataTableProps<T> {
|
|
|
204
220
|
}
|
|
205
221
|
type FilterState = Record<string, FilterValue>;
|
|
206
222
|
|
|
207
|
-
declare function DataTable<T>({ rows, columns, rowKey, defaultPageSize, onRowClick, rowHref, expandedRow, isExpanded, rowClassName, rowAttributes, paginated, empty, storageKey, urlSync, fillHeight, maxBodyHeight, serverPagination, filters: filtersProp, onFiltersChange, sorts: sortsProp, onSortsChange, selection, labels: labelsProp, locale, storageKeyPrefix, mobileExpandAsDialog, mobileGroupBy, mobileGroupLabel, mobileCard, mobileSwipeActions, leadingRow, }: DataTableProps<T>): react.JSX.Element;
|
|
223
|
+
declare function DataTable<T>({ rows, columns, rowKey, defaultPageSize, onRowClick, rowHref, expandedRow, isExpanded, rowClassName, rowAttributes, paginated, empty, storageKey, urlSync, fillHeight, maxBodyHeight, serverPagination, filters: filtersProp, onFiltersChange, sorts: sortsProp, onSortsChange, selection, labels: labelsProp, locale: localeProp, storageKeyPrefix, mobileExpandAsDialog, mobileGroupBy, mobileGroupLabel, mobileCard, mobileSwipeActions, leadingRow, }: DataTableProps<T>): react.JSX.Element;
|
|
208
224
|
|
|
209
225
|
/** How a column is filtered. `text`/`select`/`date`/`number` each read a value
|
|
210
226
|
* off the row and compare it against the user's {@link FilterValue}. */
|
|
@@ -246,8 +262,14 @@ type FilterValue = {
|
|
|
246
262
|
declare function resolveFilter<T>(col: DataTableColumn<T>): ColumnFilter<T> | null;
|
|
247
263
|
/** Whether a filter value is actually narrowing anything (vs its empty default). */
|
|
248
264
|
declare function isFilterActive(v: FilterValue | undefined): boolean;
|
|
249
|
-
/**
|
|
250
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Whether `row` passes column `col`'s filter given the user's `state`.
|
|
267
|
+
*
|
|
268
|
+
* `locale` is for the text filter's case folding. Plain `toLowerCase` folds by the
|
|
269
|
+
* root rules, under which a Turkish user typing "istanbul" does not find "İSTANBUL";
|
|
270
|
+
* `toLocaleLowerCase(locale)` does. Optional, and `undefined` is the runtime's default.
|
|
271
|
+
*/
|
|
272
|
+
declare function rowMatches<T>(col: DataTableColumn<T>, row: T, state: FilterValue, locale?: string): boolean;
|
|
251
273
|
/** The empty filter state for a column's filter type. */
|
|
252
274
|
declare function defaultFilterState<T>(filter: ColumnFilter<T>): FilterValue;
|
|
253
275
|
/** URL-safe encoding of a filter value (`f.<column>` param), null when empty. */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DEFAULT_DATA_TABLE_LABELS, DataTableLabels, missingDataTableLabels, resolveDataTableLabels } from './components/data-table-labels.js';
|
|
2
|
+
export { SortDir, SortState, decodeSorts, encodeSorts, nextSorts, normalizeSorts } from './components/data-table-sort.js';
|
|
3
|
+
export { C as ColumnFilter, D as DataTable, a as DataTableColumn, b as DataTableProps, F as FilterState, c as FilterValue, S as ServerPagination, d as decodeFilterValue, e as decodeFilterValueOfType, f as defaultFilterState, g as encodeFilterValue, i as isFilterActive, r as resolveFilter, h as rowMatches } from './data-table-filters-DM1auUfH.js';
|
|
4
|
+
export { PAGE_SIZE_OPTIONS, Pagination } from './components/data-table-pagination.js';
|
|
5
|
+
export { FilterPopover } from './components/data-table-filter-popover.js';
|
|
6
|
+
import 'react';
|
|
7
|
+
import './components/swipeable-row.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
export * from "./components/data-table-labels.js";
|
|
3
|
+
export * from "./components/data-table-sort.js";
|
|
4
|
+
export * from "./components/data-table-filters.js";
|
|
5
|
+
export * from "./components/data-table-pagination.js";
|
|
6
|
+
import { FilterPopover } from "./components/data-table-filter-popover.js";
|
|
7
|
+
import { DataTable } from "./components/data-table.js";
|
|
8
|
+
export {
|
|
9
|
+
DataTable,
|
|
10
|
+
FilterPopover
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=data-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data-table.ts"],"sourcesContent":["// `@eifi1/ui-kit/data-table` — the table suite.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: DataTable calls useSearchParams unconditionally, above any `urlSync`\n// branch, so a Router is needed even with URL sync switched off.\nexport * from \"./components/data-table-labels\";\nexport * from \"./components/data-table-sort\";\nexport * from \"./components/data-table-filters\";\nexport * from \"./components/data-table-pagination\";\nexport { FilterPopover } from \"./components/data-table-filter-popover\";\n// `SortState` comes from data-table-sort above; re-exporting the module wholesale\n// would collide with it, which is why the main barrel names these explicitly too.\nexport { DataTable } from \"./components/data-table\";\nexport type {\n DataTableColumn,\n DataTableProps,\n ServerPagination,\n FilterState,\n} from \"./components/data-table\";\n"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,qBAAqB;AAG9B,SAAS,iBAAiB;","names":[]}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
2
4
|
import { useEffect, useRef, useState } from "react";
|
|
3
5
|
import { Camera, FileText, Paperclip, X } from "lucide-react";
|
|
4
6
|
import { Button } from "../components/ui.js";
|
|
7
|
+
import { useKitFileLabels } from "../i18n/kit-labels.js";
|
|
5
8
|
const DEFAULT_ATTACHMENT_ACCEPT = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
6
9
|
const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
7
10
|
function FeedbackAttachmentField({
|
|
@@ -19,6 +22,7 @@ function FeedbackAttachmentField({
|
|
|
19
22
|
const [preview, setPreview] = useState(null);
|
|
20
23
|
const [capturing, setCapturing] = useState(false);
|
|
21
24
|
const fileInputRef = useRef(null);
|
|
25
|
+
const fileText = useKitFileLabels();
|
|
22
26
|
useEffect(() => {
|
|
23
27
|
if (!value) {
|
|
24
28
|
setPreview(null);
|
|
@@ -66,29 +70,26 @@ function FeedbackAttachmentField({
|
|
|
66
70
|
return /* @__PURE__ */ jsxs(
|
|
67
71
|
"div",
|
|
68
72
|
{
|
|
69
|
-
className,
|
|
73
|
+
className: cn("relative", className),
|
|
70
74
|
onPaste: listensElsewhere ? void 0 : (event) => takeImage(event.clipboardData, () => event.preventDefault()),
|
|
71
75
|
children: [
|
|
72
|
-
labels.attachment && /* @__PURE__ */ jsx("div", { className: "mb-1 text-xs font-medium uppercase tracking-wide text-
|
|
76
|
+
labels.attachment && /* @__PURE__ */ jsx("div", { className: "mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]", children: labels.attachment }),
|
|
73
77
|
value ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
74
78
|
value.type.startsWith("image/") && preview ? /* @__PURE__ */ jsx(
|
|
75
79
|
"img",
|
|
76
80
|
{
|
|
77
81
|
src: preview,
|
|
78
82
|
alt: value.name,
|
|
79
|
-
className: "h-20 w-20 rounded border border-
|
|
83
|
+
className: "h-20 w-20 rounded border border-[var(--border)] object-cover"
|
|
80
84
|
}
|
|
81
85
|
) : (
|
|
82
86
|
// Non-image attachments (PDF, text) can't preview as an <img>, so
|
|
83
87
|
// show a neutral file tile with the name/size beside it instead.
|
|
84
|
-
/* @__PURE__ */ jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded border border-
|
|
88
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded border border-[var(--border)] text-[var(--text-placeholder)]", children: /* @__PURE__ */ jsx(FileText, { className: "size-8" }) })
|
|
85
89
|
),
|
|
86
90
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
87
|
-
/* @__PURE__ */ jsx("div", { className: "truncate text-sm text-
|
|
88
|
-
/* @__PURE__ */
|
|
89
|
-
Math.round(value.size / 1024),
|
|
90
|
-
" KB"
|
|
91
|
-
] })
|
|
91
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-sm text-[var(--text-secondary)]", children: value.name }),
|
|
92
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-[var(--text-muted)]", children: fileText.size(value.size) })
|
|
92
93
|
] }),
|
|
93
94
|
/* @__PURE__ */ jsx(
|
|
94
95
|
"button",
|
|
@@ -96,7 +97,7 @@ function FeedbackAttachmentField({
|
|
|
96
97
|
type: "button",
|
|
97
98
|
onClick: () => onChange(null),
|
|
98
99
|
"aria-label": labels.attachmentRemove,
|
|
99
|
-
className: "rounded p-1.5 text-
|
|
100
|
+
className: "rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]",
|
|
100
101
|
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
101
102
|
}
|
|
102
103
|
)
|
|
@@ -128,7 +129,7 @@ function FeedbackAttachmentField({
|
|
|
128
129
|
}
|
|
129
130
|
)
|
|
130
131
|
] }),
|
|
131
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-
|
|
132
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: labels.attachmentPaste ?? "\u2026or paste a screenshot from the clipboard." })
|
|
132
133
|
] }),
|
|
133
134
|
/* @__PURE__ */ jsx(
|
|
134
135
|
"input",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-attachment.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type RefObject } from \"react\";\nimport { Camera, FileText, Paperclip, X } from \"lucide-react\";\nimport { Button } from \"../components/ui\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\n\nexport const DEFAULT_ATTACHMENT_ACCEPT = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"];\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;\n\n/**\n * Picking one picture: the file dialog, a capture of the app view, or a paste.\n *\n * Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can\n * have it too (Steering Design feedback #128). A report is a conversation — it\n * is filed, it is answered, and the reporter sends it back saying that is not\n * what they meant — and the screenshot showing what they mean is taken at\n * whichever of those points they looked. Only the first of them had a way to\n * attach one, so everything after it had to be described in words.\n *\n * **Three ways in, and they are three because no one of them covers the others.**\n * The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the\n * *whole* app view; and the clipboard is where a region snip already is on every\n * platform (Steering Design feedback #39). Somebody who wanted to show one panel\n * had to save a crop and then find it again.\n *\n * `documentPaste` and `pasteFrom` decide where the paste is listened for, and\n * it is a real choice rather than a flag with a default. A modal traps focus,\n * so while it is up every paste in the page is meant for it — including one\n * made with nothing in particular focused, which never reaches a React\n * `onPaste` on a child: that is `documentPaste`. An editor **inline on a\n * page** is not that: the page around it has its own fields, so it listens\n * within its own subtree and a paste elsewhere stays where it was aimed. But\n * \"its own subtree\" has to include the text box the paste is actually made in,\n * and that box is this field's *sibling*, not its child — a paste in it bubbles\n * to their common parent and never through here. `pasteFrom` is that parent\n * (Steering Design feedback #140): the element whose subtree is listened to,\n * handed in by whoever renders both the box and this field side by side.\n */\nexport function FeedbackAttachmentField({\n value,\n onChange,\n labels,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n className,\n}: {\n value: File | null;\n onChange: (file: File | null) => void;\n labels: FeedbackAttachmentLabels;\n accept?: string[];\n maxBytes?: number;\n onError?: (kind: \"type\" | \"size\") => void;\n /** Snapshot the app view behind this and return it as a File. A \"Capture\n * screenshot\" button appears only when it is given. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** Listen for the paste on `document` rather than on this field's own\n * subtree. For a modal, which owns the whole page while it is up. */\n documentPaste?: boolean;\n /** Listen for the paste within this element's subtree rather than this\n * field's own — for a field standing *beside* the text box a paste is made\n * in, whose common parent is where the event bubbles to. Ignored when\n * `documentPaste` is set. */\n pasteFrom?: RefObject<HTMLElement | null>;\n className?: string;\n}) {\n const [preview, setPreview] = useState<string | null>(null);\n const [capturing, setCapturing] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n // Object-URL preview lifecycle (create on change, revoke on cleanup).\n useEffect(() => {\n if (!value) {\n setPreview(null);\n return;\n }\n const url = URL.createObjectURL(value);\n setPreview(url);\n return () => URL.revokeObjectURL(url);\n }, [value]);\n\n const pick = (file: File | undefined | null) => {\n if (!file) return;\n if (!accept.includes(file.type)) {\n onError?.(\"type\");\n return;\n }\n if (file.size > maxBytes) {\n onError?.(\"size\");\n return;\n }\n onChange(file);\n };\n\n // The paste handler is registered once rather than per render, so it reads\n // `pick` — which closes over props that change identity on every render — out\n // of a ref rather than out of its own dependency list.\n const latest = useRef(pick);\n useEffect(() => {\n latest.current = pick;\n });\n\n const takeImage = (clipboard: DataTransfer | null, stop: () => void) => {\n const items = Array.from(clipboard?.items ?? []);\n const image = items.find((item) => item.kind === \"file\" && item.type.startsWith(\"image/\"));\n const file = image?.getAsFile();\n if (!file) return;\n // Only once there IS an image: a paste of text into a field must stay a\n // paste of text, and a clipboard holding both is a copy whose text half is\n // what the field was focused for.\n stop();\n // Clipboard images arrive named \"image.png\" at best and unnamed at worst,\n // and the name is what the inbox shows beside the thumbnail. A name that\n // says where it came from is more use than the browser's.\n latest.current(new File([file], pastedName(file.type), { type: file.type }));\n };\n\n // The element listened on, where it is not this field's own subtree: the\n // document for a modal, the given parent for an inline editor. Read inside\n // the effect rather than in render, because a ref's `current` is only set\n // once the parent has mounted — which it has by the time effects run.\n useEffect(() => {\n const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);\n if (!target) return;\n const onPaste = (event: Event) => {\n const clipboard = (event as ClipboardEvent).clipboardData;\n takeImage(clipboard, () => event.preventDefault());\n };\n target.addEventListener(\"paste\", onPaste);\n return () => target.removeEventListener(\"paste\", onPaste);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- takeImage reads `latest`, which is a ref\n }, [documentPaste, pasteFrom]);\n const listensElsewhere = documentPaste || pasteFrom !== undefined;\n\n return (\n <div\n className={className}\n onPaste={\n listensElsewhere\n ? undefined\n : (event) => takeImage(event.clipboardData, () => event.preventDefault())\n }\n >\n {labels.attachment && (\n <div className=\"mb-1 text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400\">\n {labels.attachment}\n </div>\n )}\n {value ? (\n <div className=\"flex items-start gap-2\">\n {value.type.startsWith(\"image/\") && preview ? (\n <img\n src={preview}\n alt={value.name}\n className=\"h-20 w-20 rounded border border-slate-200 object-cover dark:border-slate-700\"\n />\n ) : (\n // Non-image attachments (PDF, text) can't preview as an <img>, so\n // show a neutral file tile with the name/size beside it instead.\n <div className=\"flex h-20 w-20 items-center justify-center rounded border border-slate-200 text-slate-400 dark:border-slate-700 dark:text-slate-500\">\n <FileText className=\"size-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-slate-700 dark:text-slate-200\">{value.name}</div>\n <div className=\"text-xs text-slate-500 dark:text-slate-400\">\n {Math.round(value.size / 1024)} KB\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => onChange(null)}\n aria-label={labels.attachmentRemove}\n className=\"rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n <div className=\"flex flex-wrap gap-2\">\n <Button type=\"button\" variant=\"secondary\" onClick={() => fileInputRef.current?.click()}>\n <Paperclip className=\"size-4\" /> {labels.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={capturing}\n onClick={() => {\n if (capturing) return;\n setCapturing(true);\n void onCaptureScreenshot()\n .then((file) => {\n if (file) pick(file);\n })\n .finally(() => setCapturing(false));\n }}\n >\n <Camera className=\"size-4\" />{\" \"}\n {capturing ? \"…\" : (labels.attachmentCapture ?? \"Capture screenshot\")}\n </Button>\n )}\n </div>\n {/* Said out loud, because a gesture with no affordance is a gesture\n nobody finds. */}\n <p className=\"text-xs text-slate-500 dark:text-slate-400\">\n {labels.attachmentPaste ?? \"…or paste a screenshot from the clipboard.\"}\n </p>\n </div>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept.join(\",\")}\n className=\"sr-only\"\n onChange={(e) => {\n pick(e.target.files?.[0]);\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** What a pasted image is called once it is an attachment.\n *\n * The extension is read off the mime type rather than assumed to be `.png`:\n * Safari puts TIFF on the clipboard and a file called `pasted.png` that is not\n * a PNG is one the receiving end opens wrong. */\nexport function pastedName(type: string): string {\n const subtype = type.split(\"/\")[1] ?? \"png\";\n // `image/svg+xml` and friends carry a suffix that is not part of the\n // extension, and none of them are in the accepted list anyway.\n return `pasted.${subtype.split(\"+\")[0]}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-attachment.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\nimport { Camera, FileText, Paperclip, X } from \"lucide-react\";\nimport { Button } from \"../components/ui\";\nimport { useKitFileLabels } from \"../i18n/kit-labels\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\n\nexport const DEFAULT_ATTACHMENT_ACCEPT = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"];\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;\n\n/**\n * Picking one picture: the file dialog, a capture of the app view, or a paste.\n *\n * Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can\n * have it too (Steering Design feedback #128). A report is a conversation — it\n * is filed, it is answered, and the reporter sends it back saying that is not\n * what they meant — and the screenshot showing what they mean is taken at\n * whichever of those points they looked. Only the first of them had a way to\n * attach one, so everything after it had to be described in words.\n *\n * **Three ways in, and they are three because no one of them covers the others.**\n * The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the\n * *whole* app view; and the clipboard is where a region snip already is on every\n * platform (Steering Design feedback #39). Somebody who wanted to show one panel\n * had to save a crop and then find it again.\n *\n * `documentPaste` and `pasteFrom` decide where the paste is listened for, and\n * it is a real choice rather than a flag with a default. A modal traps focus,\n * so while it is up every paste in the page is meant for it — including one\n * made with nothing in particular focused, which never reaches a React\n * `onPaste` on a child: that is `documentPaste`. An editor **inline on a\n * page** is not that: the page around it has its own fields, so it listens\n * within its own subtree and a paste elsewhere stays where it was aimed. But\n * \"its own subtree\" has to include the text box the paste is actually made in,\n * and that box is this field's *sibling*, not its child — a paste in it bubbles\n * to their common parent and never through here. `pasteFrom` is that parent\n * (Steering Design feedback #140): the element whose subtree is listened to,\n * handed in by whoever renders both the box and this field side by side.\n */\nexport function FeedbackAttachmentField({\n value,\n onChange,\n labels,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n className,\n}: {\n value: File | null;\n onChange: (file: File | null) => void;\n labels: FeedbackAttachmentLabels;\n accept?: string[];\n maxBytes?: number;\n onError?: (kind: \"type\" | \"size\") => void;\n /** Snapshot the app view behind this and return it as a File. A \"Capture\n * screenshot\" button appears only when it is given. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** Listen for the paste on `document` rather than on this field's own\n * subtree. For a modal, which owns the whole page while it is up. */\n documentPaste?: boolean;\n /** Listen for the paste within this element's subtree rather than this\n * field's own — for a field standing *beside* the text box a paste is made\n * in, whose common parent is where the event bubbles to. Ignored when\n * `documentPaste` is set. */\n pasteFrom?: RefObject<HTMLElement | null>;\n className?: string;\n}) {\n const [preview, setPreview] = useState<string | null>(null);\n const [capturing, setCapturing] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n // `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.\n const fileText = useKitFileLabels();\n\n // Object-URL preview lifecycle (create on change, revoke on cleanup).\n useEffect(() => {\n if (!value) {\n setPreview(null);\n return;\n }\n const url = URL.createObjectURL(value);\n setPreview(url);\n return () => URL.revokeObjectURL(url);\n }, [value]);\n\n const pick = (file: File | undefined | null) => {\n if (!file) return;\n if (!accept.includes(file.type)) {\n onError?.(\"type\");\n return;\n }\n if (file.size > maxBytes) {\n onError?.(\"size\");\n return;\n }\n onChange(file);\n };\n\n // The paste handler is registered once rather than per render, so it reads\n // `pick` — which closes over props that change identity on every render — out\n // of a ref rather than out of its own dependency list.\n const latest = useRef(pick);\n useEffect(() => {\n latest.current = pick;\n });\n\n const takeImage = (clipboard: DataTransfer | null, stop: () => void) => {\n const items = Array.from(clipboard?.items ?? []);\n const image = items.find((item) => item.kind === \"file\" && item.type.startsWith(\"image/\"));\n const file = image?.getAsFile();\n if (!file) return;\n // Only once there IS an image: a paste of text into a field must stay a\n // paste of text, and a clipboard holding both is a copy whose text half is\n // what the field was focused for.\n stop();\n // Clipboard images arrive named \"image.png\" at best and unnamed at worst,\n // and the name is what the inbox shows beside the thumbnail. A name that\n // says where it came from is more use than the browser's.\n latest.current(new File([file], pastedName(file.type), { type: file.type }));\n };\n\n // The element listened on, where it is not this field's own subtree: the\n // document for a modal, the given parent for an inline editor. Read inside\n // the effect rather than in render, because a ref's `current` is only set\n // once the parent has mounted — which it has by the time effects run.\n useEffect(() => {\n const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);\n if (!target) return;\n const onPaste = (event: Event) => {\n const clipboard = (event as ClipboardEvent).clipboardData;\n takeImage(clipboard, () => event.preventDefault());\n };\n target.addEventListener(\"paste\", onPaste);\n return () => target.removeEventListener(\"paste\", onPaste);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- takeImage reads `latest`, which is a ref\n }, [documentPaste, pasteFrom]);\n const listensElsewhere = documentPaste || pasteFrom !== undefined;\n\n return (\n <div\n // `relative` for the same reason as FileDropzone: the `sr-only` file input below\n // is `position: absolute`, and without a positioned ancestor it is laid out\n // against the initial containing block — extending the DOCUMENT height to its own\n // offset and producing a phantom second scrollbar on any long page.\n className={cn(\"relative\", className)}\n onPaste={\n listensElsewhere\n ? undefined\n : (event) => takeImage(event.clipboardData, () => event.preventDefault())\n }\n >\n {labels.attachment && (\n <div className=\"mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n {labels.attachment}\n </div>\n )}\n {value ? (\n <div className=\"flex items-start gap-2\">\n {value.type.startsWith(\"image/\") && preview ? (\n <img\n src={preview}\n alt={value.name}\n className=\"h-20 w-20 rounded border border-[var(--border)] object-cover\"\n />\n ) : (\n // Non-image attachments (PDF, text) can't preview as an <img>, so\n // show a neutral file tile with the name/size beside it instead.\n <div className=\"flex h-20 w-20 items-center justify-center rounded border border-[var(--border)] text-[var(--text-placeholder)]\">\n <FileText className=\"size-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-[var(--text-secondary)]\">{value.name}</div>\n <div className=\"text-xs text-[var(--text-muted)]\">\n {fileText.size(value.size)}\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => onChange(null)}\n aria-label={labels.attachmentRemove}\n className=\"rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n <div className=\"flex flex-wrap gap-2\">\n <Button type=\"button\" variant=\"secondary\" onClick={() => fileInputRef.current?.click()}>\n <Paperclip className=\"size-4\" /> {labels.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={capturing}\n onClick={() => {\n if (capturing) return;\n setCapturing(true);\n void onCaptureScreenshot()\n .then((file) => {\n if (file) pick(file);\n })\n .finally(() => setCapturing(false));\n }}\n >\n <Camera className=\"size-4\" />{\" \"}\n {capturing ? \"…\" : (labels.attachmentCapture ?? \"Capture screenshot\")}\n </Button>\n )}\n </div>\n {/* Said out loud, because a gesture with no affordance is a gesture\n nobody finds. */}\n <p className=\"text-xs text-[var(--text-muted)]\">\n {labels.attachmentPaste ?? \"…or paste a screenshot from the clipboard.\"}\n </p>\n </div>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept.join(\",\")}\n className=\"sr-only\"\n onChange={(e) => {\n pick(e.target.files?.[0]);\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** What a pasted image is called once it is an attachment.\n *\n * The extension is read off the mime type rather than assumed to be `.png`:\n * Safari puts TIFF on the clipboard and a file called `pasted.png` that is not\n * a PNG is one the receiving end opens wrong. */\nexport function pastedName(type: string): string {\n const subtype = type.split(\"/\")[1] ?? \"png\";\n // `image/svg+xml` and friends carry a suffix that is not part of the\n // extension, and none of them are in the accepted list anyway.\n return `pasted.${subtype.split(\"+\")[0]}`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { useEffect, useState } from "react";
|
|
3
4
|
import { Button, Input, PHONE_QUERY, Select, Textarea } from "../components/ui.js";
|
|
@@ -92,7 +93,7 @@ function FeedbackDialog({
|
|
|
92
93
|
contextSlot
|
|
93
94
|
] }),
|
|
94
95
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
95
|
-
/* @__PURE__ */ jsx("div", { className: "text-xs text-
|
|
96
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-[var(--text-placeholder)]", children: labels.submitHint }),
|
|
96
97
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
97
98
|
/* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onClose, children: labels.cancel }),
|
|
98
99
|
/* @__PURE__ */ jsx(Button, { onClick: trySubmit, disabled: !canSubmit, children: labels.save })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button, Input, PHONE_QUERY, Select, Textarea } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n} from \"./feedback-attachment\";\n\nexport interface FeedbackCategoryOption {\n value: string;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to an English default. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to an English default. */\n attachmentPaste?: string;\n attachmentRemove: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n submitHint: string;\n cancel: string;\n save: string;\n}\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n */\nexport function FeedbackDialog({\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels,\n onSubmit,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onAttachmentError,\n onCaptureScreenshot,\n}: {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n labels: FeedbackDialogLabels;\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n onAttachmentError?: (kind: \"type\" | \"size\") => void;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n}) {\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n const isMobile = useMediaQuery(PHONE_QUERY, false);\n\n // Reset the form whenever the dialog is (re)opened.\n useEffect(() => {\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n }\n }, [open]);\n\n const canSubmit = !!title && !!body && !submitting;\n const trySubmit = () => {\n if (canSubmit) void onSubmit({ title, body, category, attachment });\n };\n\n if (!open) return null;\n\n const categoryField = (\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyField = (\n <Textarea rows={5} label={labels.body} value={body} onChange={(e) => setBody(e.target.value)} />\n );\n const attachmentField = (\n <FeedbackAttachmentField\n value={attachment}\n onChange={setAttachment}\n labels={labels}\n accept={attachmentAccept}\n maxBytes={maxAttachmentBytes}\n onError={onAttachmentError}\n onCaptureScreenshot={onCaptureScreenshot}\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste={open}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button, Input, PHONE_QUERY, Select, Textarea } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n} from \"./feedback-attachment\";\n\nexport interface FeedbackCategoryOption {\n value: string;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to an English default. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to an English default. */\n attachmentPaste?: string;\n attachmentRemove: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n submitHint: string;\n cancel: string;\n save: string;\n}\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n */\nexport function FeedbackDialog({\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels,\n onSubmit,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onAttachmentError,\n onCaptureScreenshot,\n}: {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n labels: FeedbackDialogLabels;\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n onAttachmentError?: (kind: \"type\" | \"size\") => void;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n}) {\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n const isMobile = useMediaQuery(PHONE_QUERY, false);\n\n // Reset the form whenever the dialog is (re)opened.\n useEffect(() => {\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n }\n }, [open]);\n\n const canSubmit = !!title && !!body && !submitting;\n const trySubmit = () => {\n if (canSubmit) void onSubmit({ title, body, category, attachment });\n };\n\n if (!open) return null;\n\n const categoryField = (\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyField = (\n <Textarea rows={5} label={labels.body} value={body} onChange={(e) => setBody(e.target.value)} />\n );\n const attachmentField = (\n <FeedbackAttachmentField\n value={attachment}\n onChange={setAttachment}\n labels={labels}\n accept={attachmentAccept}\n maxBytes={maxAttachmentBytes}\n onError={onAttachmentError}\n onCaptureScreenshot={onCaptureScreenshot}\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste={open}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-[var(--text-placeholder)]\">{labels.submitHint}</div>\n <div className=\"flex gap-2\">\n <Button variant=\"ghost\" onClick={onClose}>\n {labels.cancel}\n </Button>\n <Button onClick={trySubmit} disabled={!canSubmit}>\n {labels.save}\n </Button>\n </div>\n </div>\n </div>\n </Modal>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -38,11 +38,12 @@ type FeedbackCategory = "CRASH" | "BUG" | "IDEA" | "QUESTION" | "OTHER";
|
|
|
38
38
|
/**
|
|
39
39
|
* A glyph and a tone per status.
|
|
40
40
|
*
|
|
41
|
-
* Colour alone is not a label — two of these
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* Colour alone is not a label — two of these sit a hue apart, and a consumer who
|
|
42
|
+
* moves `--brand` can park it beside any of the others — so every status carries
|
|
43
|
+
* its own shape as well. Declaration order is chain order, and
|
|
44
|
+
* {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because
|
|
45
|
+
* a restated list cannot be checked for exhaustiveness: an eighth status would
|
|
46
|
+
* break the build here and leave a literal seven long.
|
|
46
47
|
*/
|
|
47
48
|
declare const FEEDBACK_STATUS_META: Record<FeedbackStatus, {
|
|
48
49
|
icon: LucideIcon;
|
|
@@ -90,7 +91,7 @@ declare function selectableFeedbackStatuses(_current: FeedbackStatus): FeedbackS
|
|
|
90
91
|
*
|
|
91
92
|
* A category cell that is one grey pill holding the raw enum makes a CRASH —
|
|
92
93
|
* filed automatically, by somebody staring at a broken page right now — read
|
|
93
|
-
* exactly like a QUESTION, and sit unnoticed in the queue.
|
|
94
|
+
* exactly like a QUESTION, and sit unnoticed in the queue. `--danger` plus its own
|
|
94
95
|
* glyph is what makes that impossible; the hand-filed categories stay
|
|
95
96
|
* deliberately quiet so that the loud one means something.
|
|
96
97
|
*
|
|
@@ -163,8 +164,28 @@ declare function FeedbackStatusTransitions({ status, statuses, canEdit, onPick,
|
|
|
163
164
|
* One component because all three are the same gesture: a draft that is not
|
|
164
165
|
* committed until it is saved. Ctrl/⌘+Enter submits, which is the same shortcut
|
|
165
166
|
* the compose dialog next door uses, so the habit carries across the feature.
|
|
167
|
+
*
|
|
168
|
+
* ## `initial` seeds the draft; it does not own it (changed 2026-09-22)
|
|
169
|
+
*
|
|
170
|
+
* This used to re-seed from `initial` in an effect, and that effect threw away
|
|
171
|
+
* whatever was in the box — **and the picture attached to it** — every time the
|
|
172
|
+
* prop arrived with a different value. `initial` is normally the saved note, so
|
|
173
|
+
* the list refreshing under the editor, or the owner re-deriving the same string,
|
|
174
|
+
* was enough: a triager halfway through a reply, with a screenshot picked out,
|
|
175
|
+
* lost both and got the saved text back with no way to undo it.
|
|
176
|
+
*
|
|
177
|
+
* A draft belongs to the thing being edited, so switching to a different note is a
|
|
178
|
+
* different editor — say so with a `key`:
|
|
179
|
+
*
|
|
180
|
+
* ```tsx
|
|
181
|
+
* <FeedbackNoteEditor key={note.id} initial={note.body} … />
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
184
|
+
* Where the id is not in hand at the call site, pass {@link resetKey} instead and
|
|
185
|
+
* the editor re-seeds when THAT changes. Either way the decision is the caller's,
|
|
186
|
+
* which is the point: the editor cannot tell a new subject from a new render.
|
|
166
187
|
*/
|
|
167
|
-
declare function FeedbackNoteEditor({ initial, pending, onSave, onCancel, saveLabel, cancelLabel, placeholder, rows, attachment, }: {
|
|
188
|
+
declare function FeedbackNoteEditor({ initial, pending, onSave, onCancel, saveLabel, cancelLabel, placeholder, rows, attachment, resetKey, }: {
|
|
168
189
|
initial: string;
|
|
169
190
|
pending: boolean;
|
|
170
191
|
onSave: (value: string, attachment?: File | null) => void;
|
|
@@ -181,6 +202,11 @@ declare function FeedbackNoteEditor({ initial, pending, onSave, onCancel, saveLa
|
|
|
181
202
|
* *outcome* editor beside it wants, since an outcome is the answer rather
|
|
182
203
|
* than the evidence. */
|
|
183
204
|
attachment?: FeedbackNoteAttachment;
|
|
205
|
+
/** Change this to say "the editor is now editing something ELSE", and the draft
|
|
206
|
+
* and its attachment are dropped and re-seeded from `initial`. For call sites
|
|
207
|
+
* that cannot put a `key` on the editor (see the note above). Leave it out and
|
|
208
|
+
* the draft is never thrown away behind the user's back. */
|
|
209
|
+
resetKey?: string | number;
|
|
184
210
|
}): react.JSX.Element;
|
|
185
211
|
/** What {@link FeedbackNoteEditor} needs in order to offer a picture with the
|
|
186
212
|
* note: the same four things {@link FeedbackAttachmentField} takes, so the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
+
import { useRef, useState } from "react";
|
|
3
4
|
import {
|
|
4
5
|
Ban,
|
|
5
6
|
Bug,
|
|
@@ -21,44 +22,52 @@ import { FeedbackAttachmentField } from "./feedback-attachment.js";
|
|
|
21
22
|
const FEEDBACK_STATUS_META = {
|
|
22
23
|
OPEN: {
|
|
23
24
|
icon: Inbox,
|
|
24
|
-
activeBg: "bg-
|
|
25
|
-
activeText: "text-
|
|
25
|
+
activeBg: "bg-[var(--bg-active)]",
|
|
26
|
+
activeText: "text-[var(--text-primary)]"
|
|
26
27
|
},
|
|
27
28
|
IN_PROGRESS: {
|
|
28
29
|
icon: Wrench,
|
|
29
|
-
activeBg: "bg-
|
|
30
|
-
activeText: "text-
|
|
30
|
+
activeBg: "bg-[var(--warning-bg)]",
|
|
31
|
+
activeText: "text-[var(--warning)]"
|
|
31
32
|
},
|
|
32
33
|
// Where solved work is parked, waiting on the person who reported it. Its own
|
|
33
34
|
// colour, because "somebody has to check this" is a state you want to find by
|
|
34
35
|
// scanning rather than by reading.
|
|
35
36
|
IN_EVALUATION: {
|
|
36
37
|
icon: Eye,
|
|
37
|
-
activeBg: "bg-
|
|
38
|
-
activeText: "text-
|
|
38
|
+
activeBg: "bg-[var(--info-bg)]",
|
|
39
|
+
activeText: "text-[var(--info)]"
|
|
39
40
|
},
|
|
40
|
-
// Resolved but only checkable on a deployed build —
|
|
41
|
-
// "waiting on something" rather than as a done or a refusal
|
|
41
|
+
// Resolved but only checkable on a deployed build — the SOFT brand tint, so it
|
|
42
|
+
// reads as "waiting on something" rather than as a done or a refusal, and it
|
|
43
|
+
// follows whatever the consumer's brand is instead of pinning an indigo island
|
|
44
|
+
// beside it.
|
|
42
45
|
NEEDS_LIVE_TEST: {
|
|
43
46
|
icon: CloudUpload,
|
|
44
|
-
activeBg: "bg-
|
|
45
|
-
activeText: "text-
|
|
47
|
+
activeBg: "bg-[var(--brand-bg)]",
|
|
48
|
+
activeText: "text-[var(--brand-muted)]"
|
|
46
49
|
},
|
|
47
|
-
// Parked on purpose. Muted, not
|
|
50
|
+
// Parked on purpose. Muted, not `--danger`: it is not a refusal.
|
|
48
51
|
POSTPONED: {
|
|
49
52
|
icon: PauseCircle,
|
|
50
|
-
activeBg: "bg-
|
|
51
|
-
activeText: "text-
|
|
53
|
+
activeBg: "bg-[var(--bg-active)]",
|
|
54
|
+
activeText: "text-[var(--text-secondary)]"
|
|
52
55
|
},
|
|
56
|
+
// The one status with nowhere to move to: the vocabulary has a brand, an info, a
|
|
57
|
+
// warning and a danger, and nothing that means "succeeded". Deliberately NOT
|
|
58
|
+
// borrowed from a family that means something else — `--status-synced` is a
|
|
59
|
+
// FIELD's trip to the database rather than an outcome, and tokens.css says so in
|
|
60
|
+
// as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which
|
|
61
|
+
// is why both carry a glyph and neither leans on colour alone.
|
|
53
62
|
DONE: {
|
|
54
63
|
icon: CheckCircle2,
|
|
55
|
-
activeBg: "bg-
|
|
56
|
-
activeText: "text-
|
|
64
|
+
activeBg: "bg-[var(--success-bg)]",
|
|
65
|
+
activeText: "text-[var(--success)]"
|
|
57
66
|
},
|
|
58
67
|
WONT_DO: {
|
|
59
68
|
icon: Ban,
|
|
60
|
-
activeBg: "bg-
|
|
61
|
-
activeText: "text-
|
|
69
|
+
activeBg: "bg-[var(--danger-bg)]",
|
|
70
|
+
activeText: "text-[var(--danger)]"
|
|
62
71
|
}
|
|
63
72
|
};
|
|
64
73
|
const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META);
|
|
@@ -88,28 +97,28 @@ function selectableFeedbackStatuses(_current) {
|
|
|
88
97
|
const FEEDBACK_CATEGORY_META = {
|
|
89
98
|
CRASH: {
|
|
90
99
|
icon: OctagonAlert,
|
|
91
|
-
badgeBg: "bg-
|
|
92
|
-
badgeText: "text-
|
|
100
|
+
badgeBg: "bg-[var(--danger-bg)]",
|
|
101
|
+
badgeText: "text-[var(--danger)]"
|
|
93
102
|
},
|
|
94
103
|
BUG: {
|
|
95
104
|
icon: Bug,
|
|
96
|
-
badgeBg: "bg-
|
|
97
|
-
badgeText: "text-
|
|
105
|
+
badgeBg: "bg-[var(--warning-bg)]",
|
|
106
|
+
badgeText: "text-[var(--warning)]"
|
|
98
107
|
},
|
|
99
108
|
IDEA: {
|
|
100
109
|
icon: Lightbulb,
|
|
101
|
-
badgeBg: "bg-
|
|
102
|
-
badgeText: "text-
|
|
110
|
+
badgeBg: "bg-[var(--bg-surface-2)]",
|
|
111
|
+
badgeText: "text-[var(--text-secondary)]"
|
|
103
112
|
},
|
|
104
113
|
QUESTION: {
|
|
105
114
|
icon: HelpCircle,
|
|
106
|
-
badgeBg: "bg-
|
|
107
|
-
badgeText: "text-
|
|
115
|
+
badgeBg: "bg-[var(--bg-surface-2)]",
|
|
116
|
+
badgeText: "text-[var(--text-secondary)]"
|
|
108
117
|
},
|
|
109
118
|
OTHER: {
|
|
110
119
|
icon: MoreHorizontal,
|
|
111
|
-
badgeBg: "bg-
|
|
112
|
-
badgeText: "text-
|
|
120
|
+
badgeBg: "bg-[var(--bg-surface-2)]",
|
|
121
|
+
badgeText: "text-[var(--text-secondary)]"
|
|
113
122
|
}
|
|
114
123
|
};
|
|
115
124
|
const FEEDBACK_CATEGORY_ORDER = Object.keys(FEEDBACK_CATEGORY_META);
|
|
@@ -197,7 +206,7 @@ function FeedbackStatusTransitions({
|
|
|
197
206
|
"transition-colors",
|
|
198
207
|
variant === "icon" ? cn(
|
|
199
208
|
"flex size-7 items-center justify-center rounded",
|
|
200
|
-
active ? cn(meta.activeBg, meta.activeText) : "text-
|
|
209
|
+
active ? cn(meta.activeBg, meta.activeText) : "text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
|
|
201
210
|
) : cn(
|
|
202
211
|
"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium",
|
|
203
212
|
active ? cn(meta.activeBg, meta.activeText, "border-transparent") : "border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)]"
|
|
@@ -230,15 +239,18 @@ function FeedbackNoteEditor({
|
|
|
230
239
|
cancelLabel,
|
|
231
240
|
placeholder,
|
|
232
241
|
rows = 3,
|
|
233
|
-
attachment
|
|
242
|
+
attachment,
|
|
243
|
+
resetKey
|
|
234
244
|
}) {
|
|
235
245
|
const [draft, setDraft] = useState(initial);
|
|
236
246
|
const [file, setFile] = useState(null);
|
|
237
|
-
const
|
|
238
|
-
|
|
247
|
+
const [seededFor, setSeededFor] = useState(resetKey);
|
|
248
|
+
if (resetKey !== seededFor) {
|
|
249
|
+
setSeededFor(resetKey);
|
|
239
250
|
setDraft(initial);
|
|
240
251
|
setFile(null);
|
|
241
|
-
}
|
|
252
|
+
}
|
|
253
|
+
const root = useRef(null);
|
|
242
254
|
const submit = () => onSave(draft, file);
|
|
243
255
|
return /* @__PURE__ */ jsxs(
|
|
244
256
|
"div",
|
|
@@ -252,7 +264,7 @@ function FeedbackNoteEditor({
|
|
|
252
264
|
}
|
|
253
265
|
},
|
|
254
266
|
children: [
|
|
255
|
-
placeholder && /* @__PURE__ */ jsx("p", { className: "text-xs text-
|
|
267
|
+
placeholder && /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: placeholder }),
|
|
256
268
|
/* @__PURE__ */ jsx(Textarea, { rows, value: draft, onChange: (event) => setDraft(event.target.value) }),
|
|
257
269
|
attachment && /* @__PURE__ */ jsx(
|
|
258
270
|
FeedbackAttachmentField,
|
|
@@ -282,7 +294,7 @@ function FeedbackDetailSection({
|
|
|
282
294
|
}) {
|
|
283
295
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
284
296
|
/* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [
|
|
285
|
-
/* @__PURE__ */ jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-
|
|
297
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]", children: title }),
|
|
286
298
|
action
|
|
287
299
|
] }),
|
|
288
300
|
children
|
|
@@ -292,8 +304,8 @@ function FeedbackDetail({ children }) {
|
|
|
292
304
|
return /* @__PURE__ */ jsx("div", { className: "space-y-3 text-sm", children });
|
|
293
305
|
}
|
|
294
306
|
function FeedbackProse({ children, empty }) {
|
|
295
|
-
if (!children) return /* @__PURE__ */ jsx("p", { className: "text-
|
|
296
|
-
return /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-
|
|
307
|
+
if (!children) return /* @__PURE__ */ jsx("p", { className: "text-[var(--text-placeholder)]", children: empty ?? "\u2014" });
|
|
308
|
+
return /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-[var(--text-secondary)]", children });
|
|
297
309
|
}
|
|
298
310
|
export {
|
|
299
311
|
FEEDBACK_CATEGORY_META,
|