@nanisoft/prism-ui 0.10.2 → 0.13.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 +21 -21
- package/dist/.tsbuildinfo +1 -1
- package/dist/blocks/app-shell-01/app-shell.js +4 -4
- package/dist/blocks/app-shell-01/index.d.ts +2 -2
- package/dist/blocks/app-shell-01/index.js +1 -1
- package/dist/blocks/auth-form-01/auth-form.d.ts +11 -1
- package/dist/blocks/auth-form-01/auth-form.d.ts.map +1 -1
- package/dist/blocks/auth-form-01/auth-form.js +11 -8
- package/dist/blocks/auth-form-01/index.d.ts +2 -2
- package/dist/blocks/auth-form-01/index.js +1 -1
- package/dist/blocks/cta-01/cta.d.ts +26 -21
- package/dist/blocks/cta-01/cta.d.ts.map +1 -1
- package/dist/blocks/cta-01/cta.js +2 -2
- package/dist/blocks/cta-01/index.d.ts +2 -2
- package/dist/blocks/cta-01/index.js +1 -1
- package/dist/blocks/data-table-01/data-table.js +7 -7
- package/dist/blocks/data-table-01/index.d.ts +2 -2
- package/dist/blocks/data-table-01/index.js +1 -1
- package/dist/blocks/empty-state-01/empty-state.d.ts +143 -0
- package/dist/blocks/empty-state-01/empty-state.d.ts.map +1 -0
- package/dist/blocks/empty-state-01/empty-state.js +104 -0
- package/dist/blocks/empty-state-01/index.d.ts +3 -0
- package/dist/blocks/empty-state-01/index.d.ts.map +1 -0
- package/dist/blocks/empty-state-01/index.js +1 -0
- package/dist/blocks/feature-grid-01/feature-grid.d.ts +28 -1
- package/dist/blocks/feature-grid-01/feature-grid.d.ts.map +1 -1
- package/dist/blocks/feature-grid-01/feature-grid.js +34 -3
- package/dist/blocks/feature-grid-01/index.d.ts +2 -2
- package/dist/blocks/feature-grid-01/index.js +1 -1
- package/dist/blocks/hero-01/hero.d.ts +35 -15
- package/dist/blocks/hero-01/hero.d.ts.map +1 -1
- package/dist/blocks/hero-01/hero.js +14 -9
- package/dist/blocks/hero-01/index.d.ts +2 -1
- package/dist/blocks/hero-01/index.d.ts.map +1 -1
- package/dist/blocks/hero-01/index.js +1 -1
- package/dist/blocks/index.d.ts +20 -20
- package/dist/blocks/index.js +19 -19
- package/dist/blocks/instrument-panel-01/index.d.ts +2 -2
- package/dist/blocks/instrument-panel-01/index.js +1 -1
- package/dist/blocks/instrument-panel-01/instrument-panel.js +1 -1
- package/dist/blocks/logo-strip-01/index.d.ts +2 -2
- package/dist/blocks/logo-strip-01/index.js +1 -1
- package/dist/blocks/logo-strip-01/logo-strip.d.ts +1 -1
- package/dist/blocks/logo-strip-01/logo-strip.js +2 -2
- package/dist/blocks/note-grid-01/index.d.ts +2 -2
- package/dist/blocks/note-grid-01/index.js +1 -1
- package/dist/blocks/note-grid-01/note-grid.d.ts +1 -1
- package/dist/blocks/note-grid-01/note-grid.js +1 -1
- package/dist/blocks/page-header-01/index.d.ts +2 -2
- package/dist/blocks/page-header-01/index.js +1 -1
- package/dist/blocks/page-header-01/page-header.d.ts +1 -1
- package/dist/blocks/page-header-01/page-header.js +4 -4
- package/dist/blocks/pricing-01/index.d.ts +1 -1
- package/dist/blocks/pricing-01/index.js +1 -1
- package/dist/blocks/pricing-01/pricing.d.ts +8 -1
- package/dist/blocks/pricing-01/pricing.d.ts.map +1 -1
- package/dist/blocks/pricing-01/pricing.js +15 -5
- package/dist/blocks/process-flow-01/index.d.ts +3 -0
- package/dist/blocks/process-flow-01/index.d.ts.map +1 -0
- package/dist/blocks/process-flow-01/index.js +1 -0
- package/dist/blocks/process-flow-01/process-flow.d.ts +118 -0
- package/dist/blocks/process-flow-01/process-flow.d.ts.map +1 -0
- package/dist/blocks/process-flow-01/process-flow.js +71 -0
- package/dist/blocks/process-rail-01/index.d.ts +2 -2
- package/dist/blocks/process-rail-01/index.js +1 -1
- package/dist/blocks/process-rail-01/process-rail.d.ts +1 -1
- package/dist/blocks/process-rail-01/process-rail.js +2 -2
- package/dist/blocks/product-grid-01/index.d.ts +2 -2
- package/dist/blocks/product-grid-01/index.js +1 -1
- package/dist/blocks/product-grid-01/product-grid.d.ts +37 -5
- package/dist/blocks/product-grid-01/product-grid.d.ts.map +1 -1
- package/dist/blocks/product-grid-01/product-grid.js +20 -6
- package/dist/blocks/run-console-01/index.d.ts +3 -0
- package/dist/blocks/run-console-01/index.d.ts.map +1 -0
- package/dist/blocks/run-console-01/index.js +1 -0
- package/dist/blocks/run-console-01/run-console.d.ts +111 -0
- package/dist/blocks/run-console-01/run-console.d.ts.map +1 -0
- package/dist/blocks/run-console-01/run-console.js +48 -0
- package/dist/blocks/settings-panel-01/index.d.ts +2 -2
- package/dist/blocks/settings-panel-01/index.js +1 -1
- package/dist/blocks/settings-panel-01/settings-panel.d.ts +11 -1
- package/dist/blocks/settings-panel-01/settings-panel.d.ts.map +1 -1
- package/dist/blocks/settings-panel-01/settings-panel.js +15 -10
- package/dist/blocks/site-footer/index.d.ts +2 -2
- package/dist/blocks/site-footer/index.js +1 -1
- package/dist/blocks/site-footer/site-footer.d.ts +1 -1
- package/dist/blocks/site-footer/site-footer.js +2 -2
- package/dist/blocks/site-header/index.d.ts +2 -2
- package/dist/blocks/site-header/index.js +1 -1
- package/dist/blocks/site-header/site-header.d.ts +2 -2
- package/dist/blocks/site-header/site-header.js +3 -3
- package/dist/blocks/site-navbar/index.d.ts +3 -0
- package/dist/blocks/site-navbar/index.d.ts.map +1 -0
- package/dist/blocks/site-navbar/index.js +1 -0
- package/dist/blocks/site-navbar/mobile-nav.d.ts +48 -0
- package/dist/blocks/site-navbar/mobile-nav.d.ts.map +1 -0
- package/dist/blocks/site-navbar/mobile-nav.js +35 -0
- package/dist/blocks/site-navbar/mode-toggle.d.ts +39 -0
- package/dist/blocks/site-navbar/mode-toggle.d.ts.map +1 -0
- package/dist/blocks/site-navbar/mode-toggle.js +39 -0
- package/dist/blocks/site-navbar/navbar-controls.d.ts +79 -0
- package/dist/blocks/site-navbar/navbar-controls.d.ts.map +1 -0
- package/dist/blocks/site-navbar/navbar-controls.js +40 -0
- package/dist/blocks/site-navbar/search-entry.d.ts +29 -0
- package/dist/blocks/site-navbar/search-entry.d.ts.map +1 -0
- package/dist/blocks/site-navbar/search-entry.js +52 -0
- package/dist/blocks/site-navbar/site-navbar.d.ts +212 -0
- package/dist/blocks/site-navbar/site-navbar.d.ts.map +1 -0
- package/dist/blocks/site-navbar/site-navbar.js +61 -0
- package/dist/blocks/site-navbar/sites-menu.d.ts +40 -0
- package/dist/blocks/site-navbar/sites-menu.d.ts.map +1 -0
- package/dist/blocks/site-navbar/sites-menu.js +48 -0
- package/dist/blocks/site-navbar/theme-menu.d.ts +47 -0
- package/dist/blocks/site-navbar/theme-menu.d.ts.map +1 -0
- package/dist/blocks/site-navbar/theme-menu.js +55 -0
- package/dist/blocks/stack-grid-01/index.d.ts +2 -2
- package/dist/blocks/stack-grid-01/index.js +1 -1
- package/dist/blocks/stack-grid-01/stack-grid.d.ts +42 -11
- package/dist/blocks/stack-grid-01/stack-grid.d.ts.map +1 -1
- package/dist/blocks/stack-grid-01/stack-grid.js +15 -7
- package/dist/blocks/stats-01/index.d.ts +9 -1
- package/dist/blocks/stats-01/index.d.ts.map +1 -1
- package/dist/blocks/stats-01/index.js +8 -1
- package/dist/blocks/stats-01/stats.d.ts +1 -1
- package/dist/blocks/stats-01/stats.js +2 -2
- package/dist/blocks/status-ledger-01/index.d.ts +2 -2
- package/dist/blocks/status-ledger-01/index.js +1 -1
- package/dist/blocks/status-ledger-01/status-ledger.d.ts +1 -1
- package/dist/blocks/status-ledger-01/status-ledger.js +2 -2
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +473 -1
- package/dist/components/index.d.ts +104 -66
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +74 -40
- package/dist/components/ui/accordion.js +1 -1
- package/dist/components/ui/alert-dialog.d.ts +119 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.js +82 -0
- package/dist/components/ui/alert.js +1 -1
- package/dist/components/ui/aspect-ratio.d.ts +60 -0
- package/dist/components/ui/aspect-ratio.d.ts.map +1 -0
- package/dist/components/ui/aspect-ratio.js +61 -0
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/breadcrumb.js +1 -1
- package/dist/components/ui/button-group.d.ts +68 -0
- package/dist/components/ui/button-group.d.ts.map +1 -0
- package/dist/components/ui/button-group.js +54 -0
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/calendar.d.ts +158 -0
- package/dist/components/ui/calendar.d.ts.map +1 -0
- package/dist/components/ui/calendar.js +216 -0
- package/dist/components/ui/card.d.ts +18 -6
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/card.js +23 -8
- package/dist/components/ui/carousel.d.ts +118 -0
- package/dist/components/ui/carousel.d.ts.map +1 -0
- package/dist/components/ui/carousel.js +152 -0
- package/dist/components/ui/chart-frame.d.ts +216 -0
- package/dist/components/ui/chart-frame.d.ts.map +1 -0
- package/dist/components/ui/chart-frame.js +286 -0
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/collapsible.d.ts +97 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +54 -0
- package/dist/components/ui/combobox.d.ts +141 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +243 -0
- package/dist/components/ui/command-palette.d.ts +106 -0
- package/dist/components/ui/command-palette.d.ts.map +1 -0
- package/dist/components/ui/command-palette.js +176 -0
- package/dist/components/ui/command.d.ts +82 -0
- package/dist/components/ui/command.d.ts.map +1 -0
- package/dist/components/ui/command.js +37 -0
- package/dist/components/ui/context-menu.d.ts +151 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +97 -0
- package/dist/components/ui/cta-link.js +1 -1
- package/dist/components/ui/date-picker.d.ts +111 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +74 -0
- package/dist/components/ui/diagram.d.ts +84 -15
- package/dist/components/ui/diagram.d.ts.map +1 -1
- package/dist/components/ui/diagram.js +77 -7
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/diff.d.ts +105 -0
- package/dist/components/ui/diff.d.ts.map +1 -0
- package/dist/components/ui/diff.js +113 -0
- package/dist/components/ui/dropdown-menu.d.ts +18 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.js +1 -1
- package/dist/components/ui/fact-list.js +1 -1
- package/dist/components/ui/field.js +1 -1
- package/dist/components/ui/form.d.ts +151 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +73 -0
- package/dist/components/ui/hover-card.d.ts +105 -0
- package/dist/components/ui/hover-card.d.ts.map +1 -0
- package/dist/components/ui/hover-card.js +49 -0
- package/dist/components/ui/input-group.d.ts +99 -0
- package/dist/components/ui/input-group.d.ts.map +1 -0
- package/dist/components/ui/input-group.js +83 -0
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/item.d.ts +113 -0
- package/dist/components/ui/item.d.ts.map +1 -0
- package/dist/components/ui/item.js +61 -0
- package/dist/components/ui/kbd.js +1 -1
- package/dist/components/ui/label.d.ts +58 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +29 -0
- package/dist/components/ui/live-region.d.ts +9 -0
- package/dist/components/ui/live-region.d.ts.map +1 -1
- package/dist/components/ui/live-region.js +10 -2
- package/dist/components/ui/mark.js +1 -1
- package/dist/components/ui/menubar.d.ts +197 -0
- package/dist/components/ui/menubar.d.ts.map +1 -0
- package/dist/components/ui/menubar.js +109 -0
- package/dist/components/ui/meter.d.ts +121 -0
- package/dist/components/ui/meter.d.ts.map +1 -0
- package/dist/components/ui/meter.js +82 -0
- package/dist/components/ui/native-select.d.ts +72 -0
- package/dist/components/ui/native-select.d.ts.map +1 -0
- package/dist/components/ui/native-select.js +55 -0
- package/dist/components/ui/navigation-menu.d.ts +186 -0
- package/dist/components/ui/navigation-menu.d.ts.map +1 -0
- package/dist/components/ui/navigation-menu.js +94 -0
- package/dist/components/ui/number-field.d.ts +111 -0
- package/dist/components/ui/number-field.d.ts.map +1 -0
- package/dist/components/ui/number-field.js +84 -0
- package/dist/components/ui/one-time-code.d.ts +127 -0
- package/dist/components/ui/one-time-code.d.ts.map +1 -0
- package/dist/components/ui/one-time-code.js +57 -0
- package/dist/components/ui/pagination.js +1 -1
- package/dist/components/ui/popover.js +1 -1
- package/dist/components/ui/product-mark.d.ts +1 -1
- package/dist/components/ui/product-mark.js +1 -1
- package/dist/components/ui/product-switcher.d.ts +1 -1
- package/dist/components/ui/product-switcher.js +2 -2
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/prose.js +1 -1
- package/dist/components/ui/pulse-graph.js +1 -1
- package/dist/components/ui/pulse-series.js +1 -1
- package/dist/components/ui/radio-group.js +1 -1
- package/dist/components/ui/resizable.d.ts +129 -0
- package/dist/components/ui/resizable.d.ts.map +1 -0
- package/dist/components/ui/resizable.js +205 -0
- package/dist/components/ui/scroll-area.d.ts +97 -0
- package/dist/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/components/ui/scroll-area.js +68 -0
- package/dist/components/ui/search-dialog.d.ts +139 -0
- package/dist/components/ui/search-dialog.d.ts.map +1 -0
- package/dist/components/ui/search-dialog.js +178 -0
- package/dist/components/ui/section.d.ts +26 -0
- package/dist/components/ui/section.d.ts.map +1 -1
- package/dist/components/ui/section.js +32 -1
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/separator.js +1 -1
- package/dist/components/ui/sheet.d.ts +103 -0
- package/dist/components/ui/sheet.d.ts.map +1 -0
- package/dist/components/ui/sheet.js +58 -0
- package/dist/components/ui/sidebar.d.ts +228 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +172 -0
- package/dist/components/ui/signal-field.js +1 -1
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/slider.js +1 -1
- package/dist/components/ui/spinner.d.ts +79 -0
- package/dist/components/ui/spinner.d.ts.map +1 -0
- package/dist/components/ui/spinner.js +79 -0
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/table-sort.d.ts +97 -0
- package/dist/components/ui/table-sort.d.ts.map +1 -0
- package/dist/components/ui/table-sort.js +102 -0
- package/dist/components/ui/table.js +1 -1
- package/dist/components/ui/tabs.js +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/timeline.d.ts +106 -0
- package/dist/components/ui/timeline.d.ts.map +1 -0
- package/dist/components/ui/timeline.js +111 -0
- package/dist/components/ui/toast.d.ts +219 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +270 -0
- package/dist/components/ui/toggle-group.d.ts +118 -0
- package/dist/components/ui/toggle-group.d.ts.map +1 -0
- package/dist/components/ui/toggle-group.js +194 -0
- package/dist/components/ui/toggle.d.ts +73 -0
- package/dist/components/ui/toggle.d.ts.map +1 -0
- package/dist/components/ui/toggle.js +44 -0
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/components/ui/tree.js +1 -1
- package/dist/components/ui/typography.js +1 -1
- package/dist/fonts/Inter-OFL.txt +93 -93
- package/dist/index.d.ts +72 -70
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -60
- package/dist/lib/rank.d.ts +69 -0
- package/dist/lib/rank.d.ts.map +1 -0
- package/dist/lib/rank.js +77 -0
- package/dist/live/index.d.ts +22 -0
- package/dist/live/index.d.ts.map +1 -0
- package/dist/live/index.js +20 -0
- package/dist/live/run-stream-01/index.d.ts +3 -0
- package/dist/live/run-stream-01/index.d.ts.map +1 -0
- package/dist/live/run-stream-01/index.js +1 -0
- package/dist/live/run-stream-01/run-stream.d.ts +125 -0
- package/dist/live/run-stream-01/run-stream.d.ts.map +1 -0
- package/dist/live/run-stream-01/run-stream.js +111 -0
- package/dist/pages/auth-page/index.d.ts +15 -2
- package/dist/pages/auth-page/index.d.ts.map +1 -1
- package/dist/pages/auth-page/index.js +8 -4
- package/dist/pages/blog-post-page/index.js +5 -5
- package/dist/pages/dashboard-page/index.d.ts +4 -4
- package/dist/pages/dashboard-page/index.js +4 -4
- package/dist/pages/docs-shell/docs-shell.d.ts +37 -4
- package/dist/pages/docs-shell/docs-shell.d.ts.map +1 -1
- package/dist/pages/docs-shell/docs-shell.js +107 -16
- package/dist/pages/docs-shell/index.d.ts +2 -2
- package/dist/pages/docs-shell/index.js +1 -1
- package/dist/pages/index.d.ts +14 -14
- package/dist/pages/index.js +7 -7
- package/dist/pages/marketing-page/index.d.ts +6 -6
- package/dist/pages/marketing-page/index.js +5 -5
- package/dist/pages/not-found-page/index.js +4 -4
- package/dist/pages/settings-page/index.d.ts +3 -3
- package/dist/pages/settings-page/index.js +4 -4
- package/dist/provider/index.d.ts +4 -4
- package/dist/provider/index.js +2 -2
- package/dist/provider/provider.d.ts +1 -1
- package/dist/provider/provider.js +1 -1
- package/dist/provider/theme-script.d.ts +1 -1
- package/dist/provider/theme-script.js +1 -1
- package/dist/styles.css +689 -0
- package/package.json +9 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
|
|
4
|
+
import { ChevronRightIcon } from 'lucide-react';
|
|
5
|
+
import { cn } from '../../lib/utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* A section that shows and hides, in place.
|
|
8
|
+
*
|
|
9
|
+
* **A Collapsible is one section; an Accordion is a stack of them.** The
|
|
10
|
+
* difference is what a reader can do with the result. An accordion lets one
|
|
11
|
+
* section open by closing another, which is right for a set of peers and wrong
|
|
12
|
+
* for a page that needs two things visible at once. A collapsible keeps its own
|
|
13
|
+
* state and touches nothing else, so a reader can have three open, which is
|
|
14
|
+
* usually what a page of settings wants and an accordion forbids.
|
|
15
|
+
*
|
|
16
|
+
* **The trigger and the region are wired by the library, not by the caller.**
|
|
17
|
+
* A consumer who writes the two as siblings with their own `aria-expanded` gets a
|
|
18
|
+
* button that says "collapsed" while the panel is open, or a panel no button
|
|
19
|
+
* controls, and the failure is invisible until a reader meets it. Here the
|
|
20
|
+
* trigger is a real button carrying the expanded state and pointing at the
|
|
21
|
+
* panel, so Enter and Space open it and the announcement is right without the
|
|
22
|
+
* caller writing a line of it. The chevron turns on that same state rather than
|
|
23
|
+
* on a timer: something happened and the reader can see that it did.
|
|
24
|
+
*
|
|
25
|
+
* **A reader who collapses something must be able to find it again.** The
|
|
26
|
+
* trigger is always in the document and always carries its own words, so the way
|
|
27
|
+
* back never depends on the panel that was just closed, and a collapsed section
|
|
28
|
+
* never leaves a heading with nothing under it. Pass `hiddenUntilFound` on the
|
|
29
|
+
* content when the closed section holds text the reader may be searching for.
|
|
30
|
+
*/
|
|
31
|
+
function Collapsible({ className, ...props }) {
|
|
32
|
+
return (_jsx(CollapsiblePrimitive.Root, { "data-slot": "collapsible", className: cn('flex flex-col', className), ...props }));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The button that shows and hides the panel.
|
|
36
|
+
*
|
|
37
|
+
* It is the way back, so it keeps its own words when the panel is closed, and it
|
|
38
|
+
* is a real button so Enter and Space work and so the reader can find it again
|
|
39
|
+
* with the keyboard after closing the section without their hands.
|
|
40
|
+
*/
|
|
41
|
+
function CollapsibleTrigger({ className, children, ...props }) {
|
|
42
|
+
return (_jsxs(CollapsiblePrimitive.Trigger, { "data-slot": "collapsible-trigger", className: cn('text-foreground group flex w-full items-center justify-between gap-3 rounded-md py-3 text-left text-sm font-medium outline-none', 'transition-[color,background-color,box-shadow] duration-fast ease-out', 'hover:bg-accent/60 hover:text-accent-foreground focus-visible:ring-ring focus-visible:ring-[3px]', 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50', className), ...props, children: [children, _jsx(ChevronRightIcon, { className: "text-muted-foreground pointer-events-none size-4 shrink-0 transition-transform duration-base ease-out group-data-[panel-open]:rotate-90" })] }));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The region the trigger shows and hides.
|
|
46
|
+
*
|
|
47
|
+
* The height transition is state feedback rather than decoration: the panel is
|
|
48
|
+
* opening or closing and the reader can watch it happen, which is the one motion
|
|
49
|
+
* this system allows on a surface like this.
|
|
50
|
+
*/
|
|
51
|
+
function CollapsibleContent({ className, ...props }) {
|
|
52
|
+
return (_jsx(CollapsiblePrimitive.Panel, { "data-slot": "collapsible-content", className: cn('h-(--collapsible-panel-height) overflow-hidden text-sm', 'transition-[height] duration-base ease-out', 'data-[starting-style]:h-0 data-[ending-style]:h-0', '[&[hidden]:not([hidden="until-found"])]:hidden', className), ...props }));
|
|
53
|
+
}
|
|
54
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One option the reader can choose.
|
|
3
|
+
*
|
|
4
|
+
* A single type rather than a union of "an option with a value" and "an option
|
|
5
|
+
* that is disabled", because a disabled option is still an option and still has
|
|
6
|
+
* to be rendered: a reader who cannot see what they cannot choose cannot tell a
|
|
7
|
+
* sold-out flight from a flight that was never offered.
|
|
8
|
+
*/
|
|
9
|
+
export interface ComboboxItem {
|
|
10
|
+
/** What the form submits. It is a machine value, never a sentence. */
|
|
11
|
+
value: string;
|
|
12
|
+
/** The words on the row, and the words the query is matched against. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** A short run at the far end of the row, usually a region or a stock count. */
|
|
15
|
+
hint?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Words that should also find this option.
|
|
18
|
+
*
|
|
19
|
+
* The same argument the command row makes. An option is findable by what it is
|
|
20
|
+
* for as well as by what it is called, and only the caller knows the other words
|
|
21
|
+
* for it.
|
|
22
|
+
*/
|
|
23
|
+
keywords?: readonly string[];
|
|
24
|
+
/** Whether the option is shown but cannot be chosen. */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** The props the Combobox accepts. */
|
|
28
|
+
export interface ComboboxProps {
|
|
29
|
+
/**
|
|
30
|
+
* The accessible name of the field.
|
|
31
|
+
*
|
|
32
|
+
* Required, and required as a name rather than as a placeholder: a placeholder
|
|
33
|
+
* disappears the moment a reader types, which leaves the field with no name at
|
|
34
|
+
* exactly the moment the reader most needs to know what they are filling in.
|
|
35
|
+
*/
|
|
36
|
+
label: string;
|
|
37
|
+
/** The options. */
|
|
38
|
+
items: readonly ComboboxItem[];
|
|
39
|
+
/**
|
|
40
|
+
* The chosen value, when the field is controlled.
|
|
41
|
+
*
|
|
42
|
+
* Pass `null` for nothing chosen. Omit it for an uncontrolled field, which
|
|
43
|
+
* starts at `defaultValue`.
|
|
44
|
+
*/
|
|
45
|
+
value?: string | null;
|
|
46
|
+
/** The value chosen initially, for an uncontrolled field. */
|
|
47
|
+
defaultValue?: string | null;
|
|
48
|
+
/** Called with the chosen value, or `null` when the reader clears the field. */
|
|
49
|
+
onValueChange?: (value: string | null) => void;
|
|
50
|
+
/**
|
|
51
|
+
* What a reader is told when nothing matches.
|
|
52
|
+
*
|
|
53
|
+
* `message` receives the query, so a caller can put it in their own sentence. A
|
|
54
|
+
* field that filtered to nothing and said nothing is the state that reads as a
|
|
55
|
+
* broken control rather than as a search that found nothing.
|
|
56
|
+
*/
|
|
57
|
+
empty: {
|
|
58
|
+
message: (query: string) => string;
|
|
59
|
+
hint?: string;
|
|
60
|
+
};
|
|
61
|
+
/** The placeholder in the field. The caller's word, and it disappears on typing. */
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
/** Whether the list is open, when the field is controlled. */
|
|
64
|
+
open?: boolean;
|
|
65
|
+
/** Whether the list starts open, for an uncontrolled field. */
|
|
66
|
+
defaultOpen?: boolean;
|
|
67
|
+
/** Called when the list opens or closes. */
|
|
68
|
+
onOpenChange?: (open: boolean) => void;
|
|
69
|
+
/** Whether the field ignores interaction. */
|
|
70
|
+
disabled?: boolean;
|
|
71
|
+
/** Whether a value must be chosen before the form submits. */
|
|
72
|
+
required?: boolean;
|
|
73
|
+
/** The form field name. The chosen `value` is submitted under it. */
|
|
74
|
+
name?: string;
|
|
75
|
+
/** Identifies the form that owns the hidden input. */
|
|
76
|
+
form?: string;
|
|
77
|
+
/** The field's own id, so a label elsewhere can point at it. */
|
|
78
|
+
id?: string;
|
|
79
|
+
/** Identifies the element that describes the field. */
|
|
80
|
+
'aria-describedby'?: string;
|
|
81
|
+
/** Layout only. */
|
|
82
|
+
className?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A text field that filters a list of options and commits one of them.
|
|
86
|
+
*
|
|
87
|
+
* **It ranks, and the ranking is not written here.** `locate` and `RANKS` are
|
|
88
|
+
* imported from the command palette rather than reimplemented, because a combobox
|
|
89
|
+
* that filters without ordering shows every option containing the query in the
|
|
90
|
+
* order the options happened to be fetched, so the one the reader meant sits below
|
|
91
|
+
* one that merely mentions what they typed. That is the defect this Component
|
|
92
|
+
* exists to avoid, and a second scorer would be a second answer to it. The rank
|
|
93
|
+
* table is imported, not copied, so the two surfaces cannot drift apart.
|
|
94
|
+
*
|
|
95
|
+
* **Typing never discards a choice.** The submitted value changes only when the
|
|
96
|
+
* reader chooses an option or empties the field, both of which are acts. Filtering
|
|
97
|
+
* the list under a chosen value is a question about the list, not about the
|
|
98
|
+
* answer, and a combobox that cleared the selection because the reader typed one
|
|
99
|
+
* more character would silently change what the form submits. The field's own
|
|
100
|
+
* text is restored to the chosen label when the list closes, so a reader who typed
|
|
101
|
+
* a query and changed their mind gets their answer back rather than a stray
|
|
102
|
+
* string.
|
|
103
|
+
*
|
|
104
|
+
* **A query is the difference between the text and the choice.** A field showing
|
|
105
|
+
* exactly the chosen label is not narrowing anything, so opening a filled field
|
|
106
|
+
* shows every option with the chosen one selected rather than the one option whose
|
|
107
|
+
* name matches what is already in the box. That is the same fact the previous
|
|
108
|
+
* paragraph describes, read the other way round, and it means "chosen and
|
|
109
|
+
* filtered out" cannot happen: there is no state in which the answer is a value
|
|
110
|
+
* the reader cannot see.
|
|
111
|
+
*
|
|
112
|
+
* **Nothing matching says so in words, and keeps the list open.** The field stays
|
|
113
|
+
* open and explains that the query found nothing, because a combobox that closed
|
|
114
|
+
* itself on a non-match tells the reader their keystroke broke the control. The
|
|
115
|
+
* explanation is a prop, since the sentence is the caller's and the query is put
|
|
116
|
+
* into it.
|
|
117
|
+
*
|
|
118
|
+
* **The field keeps the caret for the whole interaction.** The arrows move a
|
|
119
|
+
* highlight through the list and leave focus where it is, so a reader can keep
|
|
120
|
+
* typing while they choose. Enter commits the highlighted option and nothing else:
|
|
121
|
+
* an empty result with Enter does nothing rather than committing the first row,
|
|
122
|
+
* because committing a value a reader did not point at is the one outcome this
|
|
123
|
+
* Component must never produce.
|
|
124
|
+
*
|
|
125
|
+
* **The arrows wrap, exactly as the palette's do.** One interaction, one
|
|
126
|
+
* behaviour: a reader who learned one of these surfaces has learned both, and a
|
|
127
|
+
* list that stops at its end makes an overshoot cost a second keypress.
|
|
128
|
+
*
|
|
129
|
+
* **A disabled option is shown and skipped.** It is in the list, so a reader can
|
|
130
|
+
* see that it exists, and the arrows step over it, so a reader cannot land on
|
|
131
|
+
* something they cannot choose. Explain why it is unavailable in text beside the
|
|
132
|
+
* field; a greyed row on its own says only that it is not available.
|
|
133
|
+
*
|
|
134
|
+
* The list is anchored to the field rather than portalled, so it scrolls with the
|
|
135
|
+
* page and is clipped by an ancestor with `overflow: hidden`. That is the trade
|
|
136
|
+
* for a list that is always the right width and never flips over the field: put
|
|
137
|
+
* the field near the top of any scroll container it lives in.
|
|
138
|
+
*/
|
|
139
|
+
declare function Combobox({ label, items, value, defaultValue, onValueChange, empty, placeholder, open, defaultOpen, onOpenChange, disabled, required, name, form, id, 'aria-describedby': describedBy, className, }: ComboboxProps): import("react").JSX.Element;
|
|
140
|
+
export { Combobox };
|
|
141
|
+
//# sourceMappingURL=combobox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["../../../src/components/ui/combobox.tsx"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,mBAAmB;IACnB,KAAK,EAAE,SAAS,YAAY,EAAE,CAAA;IAC9B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,gFAAgF;IAChF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC9C;;;;;;OAMG;IACH,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5D,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,+DAA+D;IAC/D,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gEAAgE;IAChE,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,uDAAuD;IACvD,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,mBAAmB;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,iBAAS,QAAQ,CAAC,EAChB,KAAK,EACL,KAAK,EACL,KAAK,EACL,YAAmB,EACnB,aAAa,EACb,KAAK,EACL,WAAW,EACX,IAAI,EACJ,WAAmB,EACnB,YAAY,EACZ,QAAgB,EAChB,QAAgB,EAChB,IAAI,EACJ,IAAI,EACJ,EAAE,EACF,kBAAkB,EAAE,WAAW,EAC/B,SAAS,GACV,EAAE,aAAa,+BAyTf;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { RANKS, locate } from '../../lib/rank.js';
|
|
5
|
+
import { cn } from '../../lib/utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* A text field that filters a list of options and commits one of them.
|
|
8
|
+
*
|
|
9
|
+
* **It ranks, and the ranking is not written here.** `locate` and `RANKS` are
|
|
10
|
+
* imported from the command palette rather than reimplemented, because a combobox
|
|
11
|
+
* that filters without ordering shows every option containing the query in the
|
|
12
|
+
* order the options happened to be fetched, so the one the reader meant sits below
|
|
13
|
+
* one that merely mentions what they typed. That is the defect this Component
|
|
14
|
+
* exists to avoid, and a second scorer would be a second answer to it. The rank
|
|
15
|
+
* table is imported, not copied, so the two surfaces cannot drift apart.
|
|
16
|
+
*
|
|
17
|
+
* **Typing never discards a choice.** The submitted value changes only when the
|
|
18
|
+
* reader chooses an option or empties the field, both of which are acts. Filtering
|
|
19
|
+
* the list under a chosen value is a question about the list, not about the
|
|
20
|
+
* answer, and a combobox that cleared the selection because the reader typed one
|
|
21
|
+
* more character would silently change what the form submits. The field's own
|
|
22
|
+
* text is restored to the chosen label when the list closes, so a reader who typed
|
|
23
|
+
* a query and changed their mind gets their answer back rather than a stray
|
|
24
|
+
* string.
|
|
25
|
+
*
|
|
26
|
+
* **A query is the difference between the text and the choice.** A field showing
|
|
27
|
+
* exactly the chosen label is not narrowing anything, so opening a filled field
|
|
28
|
+
* shows every option with the chosen one selected rather than the one option whose
|
|
29
|
+
* name matches what is already in the box. That is the same fact the previous
|
|
30
|
+
* paragraph describes, read the other way round, and it means "chosen and
|
|
31
|
+
* filtered out" cannot happen: there is no state in which the answer is a value
|
|
32
|
+
* the reader cannot see.
|
|
33
|
+
*
|
|
34
|
+
* **Nothing matching says so in words, and keeps the list open.** The field stays
|
|
35
|
+
* open and explains that the query found nothing, because a combobox that closed
|
|
36
|
+
* itself on a non-match tells the reader their keystroke broke the control. The
|
|
37
|
+
* explanation is a prop, since the sentence is the caller's and the query is put
|
|
38
|
+
* into it.
|
|
39
|
+
*
|
|
40
|
+
* **The field keeps the caret for the whole interaction.** The arrows move a
|
|
41
|
+
* highlight through the list and leave focus where it is, so a reader can keep
|
|
42
|
+
* typing while they choose. Enter commits the highlighted option and nothing else:
|
|
43
|
+
* an empty result with Enter does nothing rather than committing the first row,
|
|
44
|
+
* because committing a value a reader did not point at is the one outcome this
|
|
45
|
+
* Component must never produce.
|
|
46
|
+
*
|
|
47
|
+
* **The arrows wrap, exactly as the palette's do.** One interaction, one
|
|
48
|
+
* behaviour: a reader who learned one of these surfaces has learned both, and a
|
|
49
|
+
* list that stops at its end makes an overshoot cost a second keypress.
|
|
50
|
+
*
|
|
51
|
+
* **A disabled option is shown and skipped.** It is in the list, so a reader can
|
|
52
|
+
* see that it exists, and the arrows step over it, so a reader cannot land on
|
|
53
|
+
* something they cannot choose. Explain why it is unavailable in text beside the
|
|
54
|
+
* field; a greyed row on its own says only that it is not available.
|
|
55
|
+
*
|
|
56
|
+
* The list is anchored to the field rather than portalled, so it scrolls with the
|
|
57
|
+
* page and is clipped by an ancestor with `overflow: hidden`. That is the trade
|
|
58
|
+
* for a list that is always the right width and never flips over the field: put
|
|
59
|
+
* the field near the top of any scroll container it lives in.
|
|
60
|
+
*/
|
|
61
|
+
function Combobox({ label, items, value, defaultValue = null, onValueChange, empty, placeholder, open, defaultOpen = false, onOpenChange, disabled = false, required = false, name, form, id, 'aria-describedby': describedBy, className, }) {
|
|
62
|
+
const [ownValue, setOwnValue] = useState(defaultValue);
|
|
63
|
+
const [ownOpen, setOwnOpen] = useState(defaultOpen);
|
|
64
|
+
const [query, setQuery] = useState('');
|
|
65
|
+
const [active, setActive] = useState(0);
|
|
66
|
+
const listRef = useRef(null);
|
|
67
|
+
const generated = useId();
|
|
68
|
+
const inputId = id ?? generated;
|
|
69
|
+
const listId = `${generated}-list`;
|
|
70
|
+
const chosen = value === undefined ? ownValue : value;
|
|
71
|
+
/** The chosen option, resolved against the option set the caller passed. */
|
|
72
|
+
const chosenItem = useMemo(() => items.find((item) => item.value === chosen) ?? null, [items, chosen]);
|
|
73
|
+
const chosenLabel = chosenItem?.label ?? '';
|
|
74
|
+
const isOpen = (open ?? ownOpen) && !disabled;
|
|
75
|
+
const setOpen = useCallback((next) => {
|
|
76
|
+
if (open === undefined)
|
|
77
|
+
setOwnOpen(next);
|
|
78
|
+
onOpenChange?.(next);
|
|
79
|
+
}, [onOpenChange, open]);
|
|
80
|
+
const choose = useCallback((next) => {
|
|
81
|
+
if (value === undefined)
|
|
82
|
+
setOwnValue(next);
|
|
83
|
+
onValueChange?.(next);
|
|
84
|
+
}, [onValueChange, value]);
|
|
85
|
+
/**
|
|
86
|
+
* The field text follows the chosen value, and only the chosen value. Typing
|
|
87
|
+
* does not change the choice, so this effect does not run while a reader is
|
|
88
|
+
* typing, which is what lets the field hold a query that is not yet an answer.
|
|
89
|
+
*/
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
setQuery(chosenLabel);
|
|
92
|
+
}, [chosenLabel]);
|
|
93
|
+
// A field showing exactly what is chosen is not narrowing the list. Reading the
|
|
94
|
+
// narrowing off the difference between the two rather than off a flag is what
|
|
95
|
+
// makes "chosen and filtered out" impossible: there is no sequence of keystrokes
|
|
96
|
+
// that produces a query equal to the chosen label and still filters.
|
|
97
|
+
const narrowing = query !== chosenLabel;
|
|
98
|
+
const trimmed = narrowing ? query.trim() : '';
|
|
99
|
+
const ranked = useMemo(() => {
|
|
100
|
+
if (trimmed === '')
|
|
101
|
+
return items.map((item) => ({ item, rank: RANKS.prefix }));
|
|
102
|
+
return items
|
|
103
|
+
.map((item) => {
|
|
104
|
+
const inLabel = locate(item.label, trimmed);
|
|
105
|
+
if (inLabel.rank < RANKS.keyword)
|
|
106
|
+
return { item, rank: inLabel.rank, range: inLabel.range };
|
|
107
|
+
// Keywords are read only after the label misses, so a keyword can never
|
|
108
|
+
// outrank a name that matched. Same rule, same scorer, same reason.
|
|
109
|
+
const keywordHit = (item.keywords ?? []).some((word) => word.toLowerCase().includes(trimmed.toLowerCase()));
|
|
110
|
+
return keywordHit ? { item, rank: RANKS.keyword } : null;
|
|
111
|
+
})
|
|
112
|
+
.filter((hit) => hit !== null)
|
|
113
|
+
.sort((a, b) => a.rank - b.rank);
|
|
114
|
+
}, [items, trimmed]);
|
|
115
|
+
// Clamped rather than reset on every keystroke. A reader who has moved the
|
|
116
|
+
// highlight and then types one more character is choosing from a list that just
|
|
117
|
+
// changed under them, and putting the highlight back at the top discards where
|
|
118
|
+
// they were.
|
|
119
|
+
const activeIndex = Math.min(active, Math.max(ranked.length - 1, 0));
|
|
120
|
+
/**
|
|
121
|
+
* Moves the highlight by `delta` rows, stepping over the options that cannot be
|
|
122
|
+
* chosen. It stops on the row it started from if every option in that direction
|
|
123
|
+
* is disabled, so a list of nothing but unavailable options cannot spin.
|
|
124
|
+
*/
|
|
125
|
+
const step = (from, delta) => {
|
|
126
|
+
if (ranked.length === 0)
|
|
127
|
+
return 0;
|
|
128
|
+
let next = from;
|
|
129
|
+
for (let moved = 0; moved < ranked.length; moved += 1) {
|
|
130
|
+
next += delta;
|
|
131
|
+
if (next >= ranked.length)
|
|
132
|
+
next = 0;
|
|
133
|
+
if (next < 0)
|
|
134
|
+
next = ranked.length - 1;
|
|
135
|
+
if (!ranked[next]?.item.disabled)
|
|
136
|
+
return next;
|
|
137
|
+
}
|
|
138
|
+
return from;
|
|
139
|
+
};
|
|
140
|
+
/** Opens the list with the chosen option under the highlight, or the first one. */
|
|
141
|
+
const openAt = useCallback((from) => {
|
|
142
|
+
setOpen(true);
|
|
143
|
+
setActive(from);
|
|
144
|
+
}, [setOpen]);
|
|
145
|
+
const openWithHighlight = () => {
|
|
146
|
+
const at = ranked.findIndex((hit) => hit.item.value === chosen && !hit.item.disabled);
|
|
147
|
+
openAt(at === -1 ? 0 : at);
|
|
148
|
+
};
|
|
149
|
+
/** Puts the chosen label back, for the paths that abandon a query. */
|
|
150
|
+
const restore = () => {
|
|
151
|
+
setQuery(chosenLabel);
|
|
152
|
+
setActive(0);
|
|
153
|
+
};
|
|
154
|
+
const run = (item) => {
|
|
155
|
+
if (item.disabled)
|
|
156
|
+
return;
|
|
157
|
+
choose(item.value);
|
|
158
|
+
setOpen(false);
|
|
159
|
+
restore();
|
|
160
|
+
};
|
|
161
|
+
const onKeyDown = (event) => {
|
|
162
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
if (!isOpen) {
|
|
165
|
+
openWithHighlight();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setActive((index) => step(Math.min(index, Math.max(ranked.length - 1, 0)), event.key === 'ArrowDown' ? 1 : -1));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (event.key === 'Enter') {
|
|
172
|
+
const hit = ranked[activeIndex];
|
|
173
|
+
// Enter with nothing selectable highlighted does nothing. Committing the
|
|
174
|
+
// first row because the reader pressed Enter is the one outcome a combobox
|
|
175
|
+
// must never produce.
|
|
176
|
+
if (hit === undefined || hit.item.disabled)
|
|
177
|
+
return;
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
run(hit.item);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (event.key === 'Escape') {
|
|
183
|
+
// Escape abandons the query and keeps the answer. A reader who pressed it to
|
|
184
|
+
// get the list out of the way did not mean to discard their choice, and a
|
|
185
|
+
// field that emptied itself here would submit nothing they asked for.
|
|
186
|
+
if (!isOpen)
|
|
187
|
+
return;
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
setOpen(false);
|
|
190
|
+
restore();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (event.key === 'Tab') {
|
|
194
|
+
// Tab commits the highlighted option and leaves, rather than dropping the
|
|
195
|
+
// highlight on the floor. A reader tabbing past a field they just narrowed
|
|
196
|
+
// was on their way to submitting it.
|
|
197
|
+
const hit = ranked[activeIndex];
|
|
198
|
+
if (isOpen && hit !== undefined && !hit.item.disabled)
|
|
199
|
+
choose(hit.item.value);
|
|
200
|
+
setOpen(false);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
/** Keeps the highlighted row in view when the arrows move it. */
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const list = listRef.current;
|
|
206
|
+
if (list === null)
|
|
207
|
+
return;
|
|
208
|
+
list.querySelector('[data-active="true"]')?.scrollIntoView({ block: 'nearest' });
|
|
209
|
+
}, [activeIndex, isOpen]);
|
|
210
|
+
const activeRow = isOpen ? ranked[activeIndex] : undefined;
|
|
211
|
+
return (_jsxs("div", { "data-slot": "combobox", className: cn('relative w-full', className), children: [_jsx("input", { "data-slot": "combobox-input", id: inputId, type: "text", role: "combobox", autoComplete: "off", "aria-expanded": isOpen && ranked.length > 0, "aria-controls": listId, "aria-haspopup": "listbox", "aria-autocomplete": "list", "aria-label": label, "aria-required": required || undefined, "aria-activedescendant": activeRow === undefined ? undefined : `${listId}-option-${activeIndex}`, ...(describedBy === undefined ? null : { 'aria-describedby': describedBy }), ...(placeholder === undefined ? null : { placeholder }), value: query, disabled: disabled, onChange: (event) => {
|
|
212
|
+
const next = event.target.value;
|
|
213
|
+
setQuery(next);
|
|
214
|
+
setActive(0);
|
|
215
|
+
if (!isOpen)
|
|
216
|
+
setOpen(true);
|
|
217
|
+
// Emptying the field is the one edit that changes the answer, because it
|
|
218
|
+
// is the only way a reader can say "not that one". Any other keystroke
|
|
219
|
+
// is a query, and a query is not a submission.
|
|
220
|
+
if (next === '')
|
|
221
|
+
choose(null);
|
|
222
|
+
}, onFocus: () => {
|
|
223
|
+
openWithHighlight();
|
|
224
|
+
}, onClick: () => {
|
|
225
|
+
if (!isOpen)
|
|
226
|
+
openWithHighlight();
|
|
227
|
+
}, onBlur: () => {
|
|
228
|
+
setOpen(false);
|
|
229
|
+
restore();
|
|
230
|
+
}, onKeyDown: onKeyDown, className: cn('border-input bg-background text-foreground placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] duration-fast ease-out outline-none md:text-sm', 'focus-visible:border-ring focus-visible:ring-ring focus-visible:ring-[3px]', 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50') }), name === undefined ? null : (_jsx("input", { type: "hidden", name: name, ...(form === undefined ? null : { form }), value: chosen ?? '', readOnly: true })), isOpen ? (ranked.length === 0 ? (_jsx("div", { "data-slot": "combobox-popup", className: "bg-popover text-popover-foreground absolute z-50 mt-1 w-full min-w-(--anchor-width) rounded-md border p-1 shadow-md", children: _jsxs("p", { "data-slot": "combobox-empty", role: "status", className: "text-muted-foreground px-3 py-6 text-center text-sm", children: [empty.message(trimmed), empty.hint === undefined ? null : (_jsx("span", { className: "mt-1 block text-xs", children: empty.hint }))] }) })) : (_jsx("div", { "data-slot": "combobox-popup", className: "bg-popover text-popover-foreground absolute z-50 mt-1 w-full min-w-(--anchor-width) rounded-md border p-1 shadow-md", children: _jsx("div", { "data-slot": "combobox-list", id: listId, ref: listRef, role: "listbox", "aria-label": label, className: "max-h-72 overflow-y-auto", children: ranked.map((hit, index) => {
|
|
231
|
+
const isActive = index === activeIndex;
|
|
232
|
+
return (_jsxs("div", { id: `${listId}-option-${index}`, "data-slot": "combobox-option", "data-active": isActive, role: "option", "aria-selected": hit.item.value === chosen, "aria-disabled": hit.item.disabled || undefined, onMouseEnter: () => {
|
|
233
|
+
if (!hit.item.disabled)
|
|
234
|
+
setActive(index);
|
|
235
|
+
},
|
|
236
|
+
// The mousedown is swallowed so the field does not blur and
|
|
237
|
+
// restore the query before the click lands. Without it a
|
|
238
|
+
// pointer choice is a blur, and the blur is the path that puts
|
|
239
|
+
// the old answer back into the field.
|
|
240
|
+
onMouseDown: (event) => event.preventDefault(), onClick: () => run(hit.item), className: cn('flex cursor-pointer items-baseline justify-between gap-3 rounded-sm px-3 py-2 text-sm outline-none', 'focus-visible:ring-ring focus-visible:ring-[3px]', hit.item.disabled && 'text-muted-foreground cursor-not-allowed opacity-50', !hit.item.disabled && isActive && 'bg-accent text-accent-foreground', !hit.item.disabled && !isActive && 'text-muted-foreground hover:text-foreground'), children: [_jsx("span", { className: "min-w-0 truncate", children: hit.range === undefined ? (hit.item.label) : (_jsxs(_Fragment, { children: [hit.item.label.slice(0, hit.range[0]), _jsx("strong", { className: "font-semibold", children: hit.item.label.slice(hit.range[0], hit.range[1]) }), hit.item.label.slice(hit.range[1])] })) }), hit.item.hint === undefined ? null : (_jsx("span", { className: "shrink-0 text-xs", children: hit.item.hint }))] }, hit.item.value));
|
|
241
|
+
}) }) }))) : null] }));
|
|
242
|
+
}
|
|
243
|
+
export { Combobox };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One thing a reader can do from the palette.
|
|
3
|
+
*
|
|
4
|
+
* `keywords` is what makes the palette good rather than merely present. A command
|
|
5
|
+
* called "Toggle theme" is findable by "theme" and not by "dark" or "colour", and
|
|
6
|
+
* a caller who has to name a command exactly has to already know it exists, which
|
|
7
|
+
* defeats the surface. Keywords are how a command is found by what it does rather
|
|
8
|
+
* than by what it is called, and they are the caller's because only the caller
|
|
9
|
+
* knows the other words for it.
|
|
10
|
+
*/
|
|
11
|
+
export type CommandItem = {
|
|
12
|
+
/** A stable key. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** The words on the row. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** A second line, usually the shortcut. The caller's word. */
|
|
17
|
+
hint?: string;
|
|
18
|
+
/** Words that should also find this command. */
|
|
19
|
+
keywords?: readonly string[];
|
|
20
|
+
/** What happens when it is chosen. */
|
|
21
|
+
onSelect: () => void;
|
|
22
|
+
};
|
|
23
|
+
/** A named set of commands, kept together in the list. */
|
|
24
|
+
export type CommandGroup = {
|
|
25
|
+
id: string;
|
|
26
|
+
/** The words on the group heading. */
|
|
27
|
+
label: string;
|
|
28
|
+
items: readonly CommandItem[];
|
|
29
|
+
};
|
|
30
|
+
/** The props the Command palette accepts. */
|
|
31
|
+
export interface CommandPaletteProps {
|
|
32
|
+
/** Whether the palette is open. */
|
|
33
|
+
open: boolean;
|
|
34
|
+
/** Called when it should open or close. */
|
|
35
|
+
onOpenChange: (open: boolean) => void;
|
|
36
|
+
/**
|
|
37
|
+
* The accessible name of the palette.
|
|
38
|
+
*
|
|
39
|
+
* Required, because a palette is announced as a dialog and a reader who opened
|
|
40
|
+
* one needs to know what they opened. The name is the caller's word.
|
|
41
|
+
*/
|
|
42
|
+
label: string;
|
|
43
|
+
/** The accessible name of the search field. */
|
|
44
|
+
inputLabel: string;
|
|
45
|
+
/** The placeholder in the search field. The caller's word. */
|
|
46
|
+
placeholder?: string;
|
|
47
|
+
/** The commands, grouped. */
|
|
48
|
+
groups: readonly CommandGroup[];
|
|
49
|
+
/**
|
|
50
|
+
* The commands shown before anything is typed.
|
|
51
|
+
*
|
|
52
|
+
* A palette that opens onto an empty list makes a reader type before they know
|
|
53
|
+
* what is available, which inverts the point of a surface whose whole purpose is
|
|
54
|
+
* to be faster than the menu it replaces. This is where recent and suggested
|
|
55
|
+
* commands go, and it is the caller's data.
|
|
56
|
+
*/
|
|
57
|
+
suggest?: {
|
|
58
|
+
label: string;
|
|
59
|
+
items: readonly CommandItem[];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* What a reader is told when nothing matches.
|
|
63
|
+
*
|
|
64
|
+
* `message` receives the query, so a caller can put it in their own sentence. An
|
|
65
|
+
* empty result with no words is the one state a palette must never show, because
|
|
66
|
+
* it reads as a broken surface rather than as a search that found nothing.
|
|
67
|
+
*/
|
|
68
|
+
empty: {
|
|
69
|
+
message: (query: string) => string;
|
|
70
|
+
hint?: string;
|
|
71
|
+
};
|
|
72
|
+
/** Layout only. */
|
|
73
|
+
className?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A searchable list of commands, over the Dialog this system already has.
|
|
77
|
+
*
|
|
78
|
+
* **It ranks, and that is the difference between this and a filtered menu.** A
|
|
79
|
+
* palette that filters without ordering shows every command containing the query
|
|
80
|
+
* in whatever order the groups happened to be declared, so the command you meant
|
|
81
|
+
* sits below one that merely mentions your query. Ranking by where the match falls
|
|
82
|
+
* puts it first, and the whole premise of a palette is that typing three letters
|
|
83
|
+
* is faster than navigating to it.
|
|
84
|
+
*
|
|
85
|
+
* **It is composed on the Dialog rather than beside it.** Focus trapping, Escape,
|
|
86
|
+
* the portal, the scroll lock and the return of focus to the trigger are five
|
|
87
|
+
* behaviours that are correct in the Dialog and would be five chances to get one
|
|
88
|
+
* wrong here. A palette that rolled its own overlay would be a second answer to
|
|
89
|
+
* all five questions, and the second answer is the one that ships the bug.
|
|
90
|
+
*
|
|
91
|
+
* **The matched run is emphasised by weight, not by a background.** A Mark is the
|
|
92
|
+
* right treatment in a list of results, where the match is the reason the row is
|
|
93
|
+
* there and a full background is the point. In a palette the match is a hint while
|
|
94
|
+
* the row's label is what the reader is reading, and a saturated background on
|
|
95
|
+
* every matched character fights the text it is inside. So the palette uses the
|
|
96
|
+
* quieter of the two, and the two surfaces are not the same surface. See the Mark
|
|
97
|
+
* documentation for the other half of that decision.
|
|
98
|
+
*
|
|
99
|
+
* **The search field keeps focus for the whole interaction.** Arrows move a
|
|
100
|
+
* highlight through the list rather than moving focus into it, so a reader can keep
|
|
101
|
+
* typing while they choose, and the highlighted row carries the state. That is the
|
|
102
|
+
* combobox pattern and it is the reason the arrows do not steal the caret.
|
|
103
|
+
*/
|
|
104
|
+
declare function CommandPalette({ open, onOpenChange, label, inputLabel, placeholder, groups, suggest, empty, className, }: CommandPaletteProps): import("react").JSX.Element;
|
|
105
|
+
export { CommandPalette };
|
|
106
|
+
//# sourceMappingURL=command-palette.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../../src/components/ui/command-palette.tsx"],"names":[],"mappings":"AAQA;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,oBAAoB;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,sCAAsC;IACtC,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,CAAA;AAED,0DAA0D;AAC1D,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;CAC9B,CAAA;AAED,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,IAAI,EAAE,OAAO,CAAA;IACb,2CAA2C;IAC3C,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6BAA6B;IAC7B,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;IAC/B;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;KAAE,CAAA;IAC1D;;;;;;OAMG;IACH,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5D,mBAAmB;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAeD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,iBAAS,cAAc,CAAC,EACtB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,UAAU,EACV,WAAW,EACX,MAAM,EACN,OAAO,EACP,KAAK,EACL,SAAS,GACV,EAAE,mBAAmB,+BAqPrB;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
|