@eifi1/ui-kit 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +32 -0
- package/README.md +498 -57
- package/dist/chart.d.ts +10 -0
- package/dist/chart.js +10 -0
- package/dist/chart.js.map +1 -0
- package/dist/components/account-settings.d.ts +11 -0
- package/dist/components/account-settings.js +19 -12
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +10 -6
- package/dist/components/alert-banner.js +20 -16
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +30 -4
- package/dist/components/amount-input.js +36 -14
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/calculator.d.ts +24 -7
- package/dist/components/calculator.js +37 -20
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.d.ts +156 -0
- package/dist/components/chart-zoom.js +262 -0
- package/dist/components/chart-zoom.js.map +1 -0
- package/dist/components/chart.d.ts +28 -3
- package/dist/components/chart.js +71 -32
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.d.ts +51 -0
- package/dist/components/checkbox.js +123 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip.d.ts +128 -0
- package/dist/components/chip.js +357 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/choice-parts.d.ts +24 -0
- package/dist/components/choice-parts.js +18 -0
- package/dist/components/choice-parts.js.map +1 -0
- package/dist/components/combobox-core.d.ts +52 -9
- package/dist/components/combobox-core.js +91 -20
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +43 -33
- package/dist/components/combobox.js +409 -340
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.d.ts +48 -8
- package/dist/components/currency-select.js +107 -65
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +15 -3
- package/dist/components/data-table-filter-popover.js +23 -17
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +3 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +53 -2
- package/dist/components/data-table-labels.js +31 -2
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table-pagination.js +16 -10
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.js +1 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +201 -85
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -9
- package/dist/components/date-picker.js +232 -94
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dropdown.d.ts +96 -20
- package/dist/components/dropdown.js +77 -33
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +5 -4
- package/dist/components/entity-combobox.js +107 -76
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +36 -0
- package/dist/components/facing-pair.js +40 -0
- package/dist/components/facing-pair.js.map +1 -0
- package/dist/components/field-sync.d.ts +164 -0
- package/dist/components/field-sync.js +264 -0
- package/dist/components/field-sync.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +38 -6
- package/dist/components/file-dropzone.js +34 -9
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +37 -26
- package/dist/components/full-bleed-dialog.js +20 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/grouped-picker.d.ts +21 -10
- package/dist/components/grouped-picker.js +66 -52
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +20 -4
- package/dist/components/hover-menu.js +15 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +102 -14
- package/dist/components/mini-calendar.js +256 -70
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +9 -3
- package/dist/components/modal.js +34 -45
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +79 -0
- package/dist/components/month-picker.js +342 -0
- package/dist/components/month-picker.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +7 -6
- package/dist/components/multi-entity-combobox.js +112 -72
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +13 -11
- package/dist/components/multi-select.js +161 -84
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +92 -0
- package/dist/components/number-field.js +111 -0
- package/dist/components/number-field.js.map +1 -0
- package/dist/components/number-input.d.ts +28 -2
- package/dist/components/number-input.js +12 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +32 -4
- package/dist/components/numpad-sheet.js +30 -13
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-contents.d.ts +90 -0
- package/dist/components/page-contents.js +170 -0
- package/dist/components/page-contents.js.map +1 -0
- package/dist/components/password-strength.d.ts +106 -0
- package/dist/components/password-strength.js +132 -0
- package/dist/components/password-strength.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +23 -15
- package/dist/components/picker-sheet.js +34 -14
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.d.ts +62 -4
- package/dist/components/popover.js +52 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/search-field.d.ts +25 -7
- package/dist/components/search-field.js +7 -5
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +14 -0
- package/dist/components/series-chart-labels.js +11 -0
- package/dist/components/series-chart-labels.js.map +1 -0
- package/dist/components/series-chart.d.ts +207 -0
- package/dist/components/series-chart.js +223 -0
- package/dist/components/series-chart.js.map +1 -0
- package/dist/components/settings-fields.d.ts +14 -4
- package/dist/components/settings-fields.js +19 -8
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/signature-pad.d.ts +119 -0
- package/dist/components/signature-pad.js +405 -0
- package/dist/components/signature-pad.js.map +1 -0
- package/dist/components/slider.d.ts +80 -0
- package/dist/components/slider.js +155 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sparkline.d.ts +93 -0
- package/dist/components/sparkline.js +205 -0
- package/dist/components/sparkline.js.map +1 -0
- package/dist/components/stat-tile.d.ts +151 -0
- package/dist/components/stat-tile.js +236 -0
- package/dist/components/stat-tile.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +25 -2
- package/dist/components/swipeable-row.js +31 -0
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +60 -0
- package/dist/components/switch.js +81 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/time-input.d.ts +58 -0
- package/dist/components/time-input.js +50 -0
- package/dist/components/time-input.js.map +1 -0
- package/dist/components/toggle-group.d.ts +18 -3
- package/dist/components/toggle-group.js +10 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +115 -0
- package/dist/components/toggle-legend.js +114 -0
- package/dist/components/toggle-legend.js.map +1 -0
- package/dist/components/tooltip.d.ts +40 -11
- package/dist/components/tooltip.js +77 -20
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +132 -0
- package/dist/components/treemap.js +179 -0
- package/dist/components/treemap.js.map +1 -0
- package/dist/components/ui.d.ts +159 -46
- package/dist/components/ui.js +220 -115
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-mobile-reveal.js +1 -0
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/use-table-state.js +38 -17
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +15 -4
- package/dist/components/user-avatar.js +5 -4
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.js +3 -2
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
- package/dist/data-table.d.ts +7 -0
- package/dist/data-table.js +12 -0
- package/dist/data-table.js.map +1 -0
- package/dist/feedback/feedback-attachment.js +12 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +2 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +33 -7
- package/dist/feedback/feedback-inbox.js +49 -37
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.js +5 -0
- package/dist/feedback.js.map +1 -0
- package/dist/hooks/use-anchored-panel.js +1 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js +1 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-announce.d.ts +46 -0
- package/dist/hooks/use-announce.js +37 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.js +1 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -1
- package/dist/hooks/use-close-transition.js +1 -0
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-dismiss.js +1 -0
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +16 -0
- package/dist/hooks/use-focus-trap.js +67 -0
- package/dist/hooks/use-focus-trap.js.map +1 -0
- package/dist/hooks/use-media-query.js +1 -0
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +27 -1
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.js +1 -0
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.d.ts +31 -0
- package/dist/i18n/defaults.js +63 -0
- package/dist/i18n/defaults.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +250 -0
- package/dist/i18n/kit-labels.js +184 -0
- package/dist/i18n/kit-labels.js.map +1 -0
- package/dist/index.d.ts +47 -22
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/calc.js +1 -0
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/cn.js +1 -0
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/dates.js +1 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/safe-storage.js +1 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/search/command-palette.d.ts +11 -1
- package/dist/search/command-palette.js +21 -15
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search.d.ts +2 -0
- package/dist/search.js +3 -0
- package/dist/search.js.map +1 -0
- package/dist/shell/app-shell.d.ts +33 -4
- package/dist/shell/app-shell.js +275 -78
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +3 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +6 -0
- package/dist/shell/role-switcher.js +7 -4
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar.d.ts +15 -7
- package/dist/shell/top-bar.js +3 -5
- package/dist/shell/top-bar.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +3 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +5 -2
- package/dist/shell/topbar-controls.js +20 -15
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +13 -0
- package/dist/shell.js +8 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +19 -12
- package/dist/theme/chart-palette.js +24 -30
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/color.d.ts +78 -0
- package/dist/theme/color.js +148 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/theme/palette-derive.d.ts +140 -0
- package/dist/theme/palette-derive.js +376 -0
- package/dist/theme/palette-derive.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +27 -5
- package/dist/theme/palette-presets.js +231 -33
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +1 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour.d.ts +2 -1
- package/dist/tour/tour.js +15 -9
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.js +10 -8
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +18 -2
- package/dist/wizard/types.js +4 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +12 -4
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js +1 -0
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js +1 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.js +1 -0
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js +5 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +8 -0
- package/dist/wizard.js +9 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +67 -14
- package/src/chart.ts +22 -0
- package/src/components/account-settings.tsx +275 -0
- package/src/components/alert-banner.tsx +75 -0
- package/src/components/amount-input.tsx +471 -0
- package/src/components/calculator.tsx +252 -0
- package/src/components/chart-zoom.tsx +499 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +222 -0
- package/src/components/chip.tsx +570 -0
- package/src/components/choice-parts.ts +33 -0
- package/src/components/combobox-core.tsx +540 -0
- package/src/components/combobox.tsx +1026 -0
- package/src/components/currency-select.tsx +269 -0
- package/src/components/data-table-filter-popover.tsx +252 -0
- package/src/components/data-table-filters.ts +176 -0
- package/src/components/data-table-labels.ts +190 -0
- package/src/components/data-table-pagination.tsx +126 -0
- package/src/components/data-table-sort.ts +70 -0
- package/src/components/data-table.tsx +1740 -0
- package/src/components/date-picker.tsx +631 -0
- package/src/components/dropdown.tsx +341 -0
- package/src/components/entity-combobox.tsx +215 -0
- package/src/components/facing-pair.ts +86 -0
- package/src/components/field-sync.tsx +472 -0
- package/src/components/file-dropzone.tsx +181 -0
- package/src/components/full-bleed-dialog.tsx +179 -0
- package/src/components/grouped-picker.tsx +157 -0
- package/src/components/hover-menu.tsx +183 -0
- package/src/components/mini-calendar.tsx +554 -0
- package/src/components/modal.tsx +287 -0
- package/src/components/month-picker.tsx +549 -0
- package/src/components/multi-entity-combobox.tsx +226 -0
- package/src/components/multi-select.tsx +303 -0
- package/src/components/number-field.tsx +225 -0
- package/src/components/number-input.tsx +211 -0
- package/src/components/numpad-sheet.tsx +264 -0
- package/src/components/page-contents.tsx +289 -0
- package/src/components/password-strength.tsx +258 -0
- package/src/components/picker-sheet.tsx +251 -0
- package/src/components/popover.tsx +195 -0
- package/src/components/search-field.tsx +146 -0
- package/src/components/series-chart-labels.ts +26 -0
- package/src/components/series-chart.tsx +481 -0
- package/src/components/settings-fields.tsx +68 -0
- package/src/components/signature-pad.tsx +630 -0
- package/src/components/slider.tsx +313 -0
- package/src/components/sparkline.tsx +329 -0
- package/src/components/stat-tile.tsx +448 -0
- package/src/components/swipeable-row.tsx +271 -0
- package/src/components/switch.tsx +170 -0
- package/src/components/time-input.tsx +107 -0
- package/src/components/toggle-group.tsx +153 -0
- package/src/components/toggle-legend.tsx +229 -0
- package/src/components/tooltip.tsx +477 -0
- package/src/components/treemap.tsx +390 -0
- package/src/components/ui.tsx +1284 -0
- package/src/components/use-mobile-reveal.ts +54 -0
- package/src/components/use-table-state.ts +295 -0
- package/src/components/user-avatar.tsx +61 -0
- package/src/components/wizard-stepper.tsx +37 -0
- package/src/data-table.ts +19 -0
- package/src/feedback/feedback-attachment.tsx +246 -0
- package/src/feedback/feedback-dialog.tsx +209 -0
- package/src/feedback/feedback-inbox.tsx +615 -0
- package/src/feedback.ts +8 -0
- package/src/hooks/use-anchored-panel.ts +169 -0
- package/src/hooks/use-anchored-rect.ts +78 -0
- package/src/hooks/use-announce.ts +85 -0
- package/src/hooks/use-body-scroll-lock.ts +94 -0
- package/src/hooks/use-close-transition.ts +99 -0
- package/src/hooks/use-dismiss.ts +77 -0
- package/src/hooks/use-focus-trap.ts +160 -0
- package/src/hooks/use-media-query.ts +23 -0
- package/src/hooks/use-overlay-history.ts +345 -0
- package/src/hooks/use-row-swipe.ts +212 -0
- package/src/i18n/defaults.ts +68 -0
- package/src/i18n/kit-labels.tsx +447 -0
- package/src/index.ts +169 -0
- package/src/lib/calc.ts +309 -0
- package/src/lib/cn.ts +5 -0
- package/src/lib/dates.ts +187 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/safe-storage.ts +37 -0
- package/src/search/command-palette.tsx +342 -0
- package/src/search.ts +3 -0
- package/src/shell/app-shell.tsx +772 -0
- package/src/shell/option-switcher-menu.tsx +72 -0
- package/src/shell/role-switcher.tsx +46 -0
- package/src/shell/top-bar.tsx +39 -0
- package/src/shell/topbar-action-menu.tsx +91 -0
- package/src/shell/topbar-controls.tsx +229 -0
- package/src/shell.ts +10 -0
- package/src/theme/chart-palette.ts +145 -0
- package/src/theme/color.ts +266 -0
- package/src/theme/palette-derive.ts +681 -0
- package/src/theme/palette-presets.ts +760 -0
- package/src/theme/palette-store.ts +94 -0
- package/src/theme/theme-store.ts +145 -0
- package/src/tour/tour.tsx +586 -0
- package/src/tour.ts +3 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wizard/stepper-nav.tsx +221 -0
- package/src/wizard/types.ts +178 -0
- package/src/wizard/use-wizard.ts +345 -0
- package/src/wizard/validation.ts +37 -0
- package/src/wizard/wizard-context.tsx +29 -0
- package/src/wizard/wizard-step.tsx +14 -0
- package/src/wizard/wizard-summary.tsx +69 -0
- package/src/wizard.ts +14 -0
- package/tokens.css +222 -20
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { Search } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
7
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
8
|
+
|
|
9
|
+
export interface CommandItem {
|
|
10
|
+
id: string;
|
|
11
|
+
/** Primary text. */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Secondary text on the right (e.g. an amount, a date, a group hint). */
|
|
14
|
+
hint?: string;
|
|
15
|
+
/** Heading this item is grouped under. */
|
|
16
|
+
group: string;
|
|
17
|
+
/** Leading icon / element. */
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
/** Invoked when the item is chosen (navigate, run an action, …). */
|
|
20
|
+
onSelect: () => void;
|
|
21
|
+
/** Where this item leads, when it is a navigation rather than an action. Renders
|
|
22
|
+
* the row as a real anchor so it can be middle-/⌘-clicked into a new tab
|
|
23
|
+
* (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so
|
|
24
|
+
* in-app navigation stays client-side. `href` only adds what the browser can do
|
|
25
|
+
* with a link and JavaScript cannot fake — the rejected alternative, an
|
|
26
|
+
* `onAuxClick` calling `window.open`, gets the middle click and nothing else. */
|
|
27
|
+
href?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CommandPaletteLabels {
|
|
31
|
+
placeholder: string;
|
|
32
|
+
empty: string;
|
|
33
|
+
loading: string;
|
|
34
|
+
/** Hint shown in the footer, e.g. "↑↓ to navigate · ↵ to select · esc to close". */
|
|
35
|
+
hint?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Accessible name of the dialog itself. It used to borrow `placeholder`, which is
|
|
38
|
+
* written to sit greyed-out in an empty box ("Search…", "Type a command"), not to
|
|
39
|
+
* name a window — and a reader heard the ellipsis read out as part of the name.
|
|
40
|
+
*
|
|
41
|
+
* Optional, unlike `placeholder`, so a translation annotated as
|
|
42
|
+
* `CommandPaletteLabels` keeps compiling; the default always carries it.
|
|
43
|
+
*/
|
|
44
|
+
dialog?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {
|
|
48
|
+
placeholder: "Search…",
|
|
49
|
+
empty: "No results",
|
|
50
|
+
loading: "Searching…",
|
|
51
|
+
dialog: "Search",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —
|
|
55
|
+
* see the dialog's `aria-label` for why that distinction matters. */
|
|
56
|
+
const { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;
|
|
57
|
+
|
|
58
|
+
interface CommandPaletteProps {
|
|
59
|
+
open: boolean;
|
|
60
|
+
onClose: () => void;
|
|
61
|
+
/**
|
|
62
|
+
* Returns results for a query (called debounced as the user types; an empty
|
|
63
|
+
* query is allowed — return defaults like pages/recent). Sync or async; stale
|
|
64
|
+
* async responses are ignored.
|
|
65
|
+
*
|
|
66
|
+
* Its IDENTITY is not a re-run signal — it is read through a ref, so the palette
|
|
67
|
+
* never re-searches merely because the caller rebuilt the callback. That is
|
|
68
|
+
* deliberate: a provider closing over a list rendered from a query allocates a new
|
|
69
|
+
* function on most renders, and re-running the search on each would make typing
|
|
70
|
+
* stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the
|
|
71
|
+
* provider changes.
|
|
72
|
+
*/
|
|
73
|
+
search: (query: string) => CommandItem[] | Promise<CommandItem[]>;
|
|
74
|
+
/**
|
|
75
|
+
* Anything whose change means the current results are out of date — typically the
|
|
76
|
+
* arrays the provider searches over. Compared by identity (as an effect dependency
|
|
77
|
+
* would be), so pass stable references: a `data = []` default allocates a fresh
|
|
78
|
+
* array on every render while a query is pending and would re-search forever.
|
|
79
|
+
*
|
|
80
|
+
* Without it, a provider whose data arrives AFTER the palette opened never gets
|
|
81
|
+
* asked again: the user's last keystroke settles the debounce, the queries resolve a
|
|
82
|
+
* moment later, and the groups they would have contributed are simply absent until
|
|
83
|
+
* another keystroke. That is the ordinary first open on a cold cache.
|
|
84
|
+
*/
|
|
85
|
+
revision?: unknown;
|
|
86
|
+
labels?: Partial<CommandPaletteLabels>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */
|
|
90
|
+
export function useCommandKey(onOpen: () => void): void {
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const handler = (e: KeyboardEvent) => {
|
|
93
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
onOpen();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
document.addEventListener("keydown", handler);
|
|
99
|
+
return () => document.removeEventListener("keydown", handler);
|
|
100
|
+
}, [onOpen]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A ⌘K-style command palette: a top-centred modal with a search field and a
|
|
105
|
+
* grouped, keyboard-navigable result list. Domain-free — the app supplies
|
|
106
|
+
* results via the `search` provider (pages, actions, records, …); each result
|
|
107
|
+
* carries its own `onSelect`.
|
|
108
|
+
*/
|
|
109
|
+
export function CommandPalette({
|
|
110
|
+
open,
|
|
111
|
+
onClose,
|
|
112
|
+
search,
|
|
113
|
+
revision,
|
|
114
|
+
labels,
|
|
115
|
+
}: CommandPaletteProps) {
|
|
116
|
+
const l = useKitLabels("commandPalette", DEFAULTS_WITHOUT_DIALOG, labels);
|
|
117
|
+
// `dialog` is new, and until it existed the dialog was named by `placeholder`. An
|
|
118
|
+
// app that translated `placeholder` and has not heard of `dialog` would otherwise
|
|
119
|
+
// go from a German name to an English "Search" on update — so a translated
|
|
120
|
+
// placeholder keeps doing the job until a `dialog` is supplied.
|
|
121
|
+
const dialogName =
|
|
122
|
+
l.dialog ??
|
|
123
|
+
(l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);
|
|
124
|
+
const [query, setQuery] = useState("");
|
|
125
|
+
const [results, setResults] = useState<CommandItem[]>([]);
|
|
126
|
+
const [loading, setLoading] = useState(false);
|
|
127
|
+
const [active, setActive] = useState(0);
|
|
128
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
129
|
+
const listRef = useRef<HTMLUListElement>(null);
|
|
130
|
+
const searchRef = useRef(search);
|
|
131
|
+
searchRef.current = search;
|
|
132
|
+
const reqId = useRef(0);
|
|
133
|
+
|
|
134
|
+
// Back dismisses the palette (Keksdose feedback #172). Declared with the other
|
|
135
|
+
// hooks, above the `if (!open) return null` below — a hook past a conditional
|
|
136
|
+
// return changes the hook order between renders. Passing `open` rather than
|
|
137
|
+
// mounting-while-open is what lets it sit here.
|
|
138
|
+
useOverlayHistory(open, onClose);
|
|
139
|
+
|
|
140
|
+
// Reset + focus when opened.
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!open) return;
|
|
143
|
+
setQuery("");
|
|
144
|
+
setActive(0);
|
|
145
|
+
const id = requestAnimationFrame(() => inputRef.current?.focus());
|
|
146
|
+
return () => cancelAnimationFrame(id);
|
|
147
|
+
}, [open]);
|
|
148
|
+
|
|
149
|
+
// Debounced, race-safe search.
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!open) return;
|
|
152
|
+
const id = ++reqId.current;
|
|
153
|
+
setLoading(true);
|
|
154
|
+
const run = async () => {
|
|
155
|
+
try {
|
|
156
|
+
const r = await Promise.resolve(searchRef.current(query));
|
|
157
|
+
if (reqId.current === id) {
|
|
158
|
+
setResults(r);
|
|
159
|
+
setActive(0);
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
if (reqId.current === id) setLoading(false);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const t = setTimeout(run, query ? 150 : 0);
|
|
166
|
+
return () => clearTimeout(t);
|
|
167
|
+
// `revision`, not `search`: see the prop's docstring. The debounce is re-armed on a
|
|
168
|
+
// revision change, which is right — data arriving is not a keystroke and there is
|
|
169
|
+
// nothing to feel laggy about.
|
|
170
|
+
}, [query, open, revision]);
|
|
171
|
+
|
|
172
|
+
// Group results, preserving first-seen group order; keep a flat list for nav.
|
|
173
|
+
const { groups, flat } = useMemo(() => {
|
|
174
|
+
const order: string[] = [];
|
|
175
|
+
const byGroup = new Map<string, CommandItem[]>();
|
|
176
|
+
for (const item of results) {
|
|
177
|
+
if (!byGroup.has(item.group)) {
|
|
178
|
+
byGroup.set(item.group, []);
|
|
179
|
+
order.push(item.group);
|
|
180
|
+
}
|
|
181
|
+
byGroup.get(item.group)!.push(item);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),
|
|
185
|
+
flat: order.flatMap((g) => byGroup.get(g)!),
|
|
186
|
+
};
|
|
187
|
+
}, [results]);
|
|
188
|
+
|
|
189
|
+
const choose = (item: CommandItem | undefined) => {
|
|
190
|
+
if (!item) return;
|
|
191
|
+
onClose();
|
|
192
|
+
item.onSelect();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const onKeyDown = (e: React.KeyboardEvent) => {
|
|
196
|
+
if (e.key === "ArrowDown") {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
setActive((i) => Math.min(i + 1, flat.length - 1));
|
|
199
|
+
} else if (e.key === "ArrowUp") {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
setActive((i) => Math.max(i - 1, 0));
|
|
202
|
+
} else if (e.key === "Enter") {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
choose(flat[active]);
|
|
205
|
+
} else if (e.key === "Escape") {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
onClose();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Keep the active row scrolled into view.
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
const el = listRef.current?.querySelector<HTMLElement>(`[data-index="${active}"]`);
|
|
214
|
+
el?.scrollIntoView({ block: "nearest" });
|
|
215
|
+
}, [active]);
|
|
216
|
+
|
|
217
|
+
if (!open) return null;
|
|
218
|
+
|
|
219
|
+
let flatIndex = -1;
|
|
220
|
+
return createPortal(
|
|
221
|
+
<div
|
|
222
|
+
className="fixed inset-0 z-[70] flex items-start justify-center bg-black/40 p-4 pt-[10vh]"
|
|
223
|
+
onMouseDown={(e) => {
|
|
224
|
+
if (e.target === e.currentTarget) onClose();
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<div
|
|
228
|
+
role="dialog"
|
|
229
|
+
aria-modal="true"
|
|
230
|
+
aria-label={dialogName}
|
|
231
|
+
className="flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl"
|
|
232
|
+
>
|
|
233
|
+
<div className="flex items-center gap-2 border-b border-[var(--border)] px-3">
|
|
234
|
+
<Search className="size-4 shrink-0 text-[var(--text-placeholder)]" />
|
|
235
|
+
<input
|
|
236
|
+
ref={inputRef}
|
|
237
|
+
role="combobox"
|
|
238
|
+
aria-expanded
|
|
239
|
+
aria-controls="command-palette-list"
|
|
240
|
+
aria-activedescendant={flat[active] ? `command-item-${flat[active].id}` : undefined}
|
|
241
|
+
value={query}
|
|
242
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
243
|
+
onKeyDown={onKeyDown}
|
|
244
|
+
placeholder={l.placeholder}
|
|
245
|
+
className="w-full bg-transparent py-3 text-sm outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]"
|
|
246
|
+
/>
|
|
247
|
+
{loading && <span className="shrink-0 text-[11px] text-[var(--text-placeholder)]">{l.loading}</span>}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<ul id="command-palette-list" ref={listRef} role="listbox" className="min-h-0 flex-1 overflow-y-auto py-1">
|
|
251
|
+
{flat.length === 0 && !loading && (
|
|
252
|
+
<li className="px-3 py-6 text-center text-sm text-[var(--text-muted)]">{l.empty}</li>
|
|
253
|
+
)}
|
|
254
|
+
{groups.map(({ group, items }) => (
|
|
255
|
+
<li key={group}>
|
|
256
|
+
<div className="px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]">
|
|
257
|
+
{group}
|
|
258
|
+
</div>
|
|
259
|
+
<ul>
|
|
260
|
+
{items.map((item) => {
|
|
261
|
+
flatIndex += 1;
|
|
262
|
+
const idx = flatIndex;
|
|
263
|
+
const isActive = idx === active;
|
|
264
|
+
// Written once and worn by either tag below, so the anchor and the
|
|
265
|
+
// button can never drift apart in looks or in listbox semantics.
|
|
266
|
+
// `role="option"` on an <a href> is fine: the browser's middle-click
|
|
267
|
+
// behaviour keys off the element, not the ARIA role.
|
|
268
|
+
const shared = {
|
|
269
|
+
id: `command-item-${item.id}`,
|
|
270
|
+
role: "option" as const,
|
|
271
|
+
"aria-selected": isActive,
|
|
272
|
+
"data-index": idx,
|
|
273
|
+
onMouseMove: () => setActive(idx),
|
|
274
|
+
className: cn(
|
|
275
|
+
"flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm",
|
|
276
|
+
isActive
|
|
277
|
+
? "bg-[var(--bg-active)] text-[var(--text-primary)]"
|
|
278
|
+
: "text-[var(--text-secondary)]",
|
|
279
|
+
),
|
|
280
|
+
};
|
|
281
|
+
const inner = (
|
|
282
|
+
<>
|
|
283
|
+
{item.icon && (
|
|
284
|
+
<span className="flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]">
|
|
285
|
+
{item.icon}
|
|
286
|
+
</span>
|
|
287
|
+
)}
|
|
288
|
+
<span className="min-w-0 flex-1 truncate">{item.label}</span>
|
|
289
|
+
{item.hint && (
|
|
290
|
+
<span className="shrink-0 text-xs text-[var(--text-placeholder)]">{item.hint}</span>
|
|
291
|
+
)}
|
|
292
|
+
</>
|
|
293
|
+
);
|
|
294
|
+
return (
|
|
295
|
+
<li key={item.id}>
|
|
296
|
+
{item.href ? (
|
|
297
|
+
<a
|
|
298
|
+
{...shared}
|
|
299
|
+
href={item.href}
|
|
300
|
+
draggable={false}
|
|
301
|
+
onClick={(e) => {
|
|
302
|
+
// A modified click belongs to the browser (feedback
|
|
303
|
+
// #451) — and the palette deliberately stays OPEN for
|
|
304
|
+
// it: ⌘-clicking three results in a row is the reason to
|
|
305
|
+
// want this, and closing after the first would undo it.
|
|
306
|
+
if (
|
|
307
|
+
e.button !== 0 ||
|
|
308
|
+
e.metaKey ||
|
|
309
|
+
e.ctrlKey ||
|
|
310
|
+
e.shiftKey ||
|
|
311
|
+
e.altKey
|
|
312
|
+
)
|
|
313
|
+
return;
|
|
314
|
+
e.preventDefault();
|
|
315
|
+
choose(item);
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
{inner}
|
|
319
|
+
</a>
|
|
320
|
+
) : (
|
|
321
|
+
<button type="button" {...shared} onClick={() => choose(item)}>
|
|
322
|
+
{inner}
|
|
323
|
+
</button>
|
|
324
|
+
)}
|
|
325
|
+
</li>
|
|
326
|
+
);
|
|
327
|
+
})}
|
|
328
|
+
</ul>
|
|
329
|
+
</li>
|
|
330
|
+
))}
|
|
331
|
+
</ul>
|
|
332
|
+
|
|
333
|
+
{l.hint && (
|
|
334
|
+
<div className="border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]">
|
|
335
|
+
{l.hint}
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>,
|
|
340
|
+
document.body,
|
|
341
|
+
);
|
|
342
|
+
}
|
package/src/search.ts
ADDED