@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,97 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
/** Which axis the region is expected to scroll on. */
|
|
3
|
+
export type ScrollAreaOrientation = 'vertical' | 'horizontal' | 'both';
|
|
4
|
+
/** The props the ScrollArea accepts. */
|
|
5
|
+
export interface ScrollAreaProps extends Omit<ComponentProps<'div'>, 'children'> {
|
|
6
|
+
/**
|
|
7
|
+
* The content the region scrolls through.
|
|
8
|
+
*
|
|
9
|
+
* Ordinary children, not a render prop and not a measured height. A consumer
|
|
10
|
+
* sizes the region from the outside with `className` and puts whatever they like
|
|
11
|
+
* inside, which is the arrangement that lets a region hold a table, a list of
|
|
12
|
+
* anything, or a form.
|
|
13
|
+
*/
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Which scrollbars to draw.
|
|
17
|
+
*
|
|
18
|
+
* `both` is the default rather than `vertical` because a region that overflows
|
|
19
|
+
* on an axis nobody asked about is still a region a reader has to be able to
|
|
20
|
+
* scroll on that axis, and a scrollbar that is missing is a scrollbar that has
|
|
21
|
+
* to be found in the browser's own chrome, which is the thing this Component
|
|
22
|
+
* exists to replace. Pass `vertical` when the content is known never to scroll
|
|
23
|
+
* sideways, and the horizontal bar is not drawn at all.
|
|
24
|
+
*/
|
|
25
|
+
orientation?: ScrollAreaOrientation;
|
|
26
|
+
/**
|
|
27
|
+
* Keeps the scrollbar in the DOM when the region has nothing to scroll.
|
|
28
|
+
*
|
|
29
|
+
* Off by default, and that default is the decision: a region with nothing to
|
|
30
|
+
* scroll has no scroll position, so a bar drawn beside it is a claim that there
|
|
31
|
+
* is something past the edge. Turn it on only where a region is expected to
|
|
32
|
+
* grow and the reader should see the affordance before the content arrives.
|
|
33
|
+
*/
|
|
34
|
+
keepMounted?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The accessible name of the region.
|
|
37
|
+
*
|
|
38
|
+
* Required rather than defaulted. A scroll region with no name is announced as
|
|
39
|
+
* a group of focusable nothing, and a page with three of them is a page where a
|
|
40
|
+
* keyboard reader tabs into a region and cannot tell what is in it or what is
|
|
41
|
+
* past the edge. The name is the caller's word.
|
|
42
|
+
*/
|
|
43
|
+
label: string;
|
|
44
|
+
/** Layout only. */
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A scrollable region whose scrollbar belongs to this design system.
|
|
49
|
+
*
|
|
50
|
+
* **The browser's scrollbar is the one piece of a page that belongs to no design
|
|
51
|
+
* system at all.** Every other surface a consumer draws is a token from here, and
|
|
52
|
+
* the scrollbar beside it is a rectangle the operating system drew, in a colour
|
|
53
|
+
* chosen for the OS's own background rather than for the card the content is on.
|
|
54
|
+
* It is also the only control on a page that is resized by dragging, and the only
|
|
55
|
+
* one that appears and disappears on a timer, so the browser's version is the one
|
|
56
|
+
* control a design system cannot restyle at all. This is where the package takes
|
|
57
|
+
* it over, and it is worth saying plainly that the native scrollbar is not
|
|
58
|
+
* available to restyle: `::-webkit-scrollbar` is one engine, `scrollbar-width` is
|
|
59
|
+
* two values, and neither can express a track and a thumb in tokens.
|
|
60
|
+
*
|
|
61
|
+
* **The scrolling is still the browser's.** This is the decision that matters most
|
|
62
|
+
* and the one worth arguing with, so here is the argument. The tempting version of
|
|
63
|
+
* this Component is a `div` with `overflow: hidden` whose content is moved by a
|
|
64
|
+
* transform on a wheel handler, with a `div` above it pretending to be a
|
|
65
|
+
* scrollbar. That version has no scroll position at all, so there is nothing for
|
|
66
|
+
* a keyboard to move, nothing for a screen reader to announce, no momentum, no
|
|
67
|
+
* scroll-into-view, and no way for a reader to know how far through they are. A
|
|
68
|
+
* custom scrollbar that is a `div` is worse than the browser's, because the browser
|
|
69
|
+
* gives a reader all of that for free and the `div` version takes it away.
|
|
70
|
+
*
|
|
71
|
+
* So the region here is a real scrollable element in the document: the browser
|
|
72
|
+
* scrolls it, the browser gives it keyboard focus when it overflows, the arrow
|
|
73
|
+
* keys, Page Up, Page Down, Home and End all work because it is a scroll
|
|
74
|
+
* container and not a picture of one. Only the appearance of the bar is taken
|
|
75
|
+
* over, and the bar is a real control layered over the native one rather than a
|
|
76
|
+
* replacement for the scrolling.
|
|
77
|
+
*
|
|
78
|
+
* **A native scrollbar cannot be styled, so this one is a sibling of the scroll
|
|
79
|
+
* container rather than a part of it.** That is the whole reason a consumer cannot
|
|
80
|
+
* assemble this from `overflow-auto` and a coloured box, and it is why the region
|
|
81
|
+
* is a named landmark: a scrollable region that a keyboard reader can tab into and
|
|
82
|
+
* that has no name is a tab stop that says nothing. The name is a prop.
|
|
83
|
+
*
|
|
84
|
+
* **A region with nothing to scroll draws no scrollbar.** Not a disabled one and
|
|
85
|
+
* not a faint one: a bar beside a region with no overflow is a claim that there is
|
|
86
|
+
* something past the edge, and a reader who is told that twice loses trust in the
|
|
87
|
+
* third bar they meet. The bar is absent, and a caller who wants it present before
|
|
88
|
+
* the content arrives says so with `keepMounted`.
|
|
89
|
+
*
|
|
90
|
+
* It is a client Component, because measuring overflow and positioning a thumb is
|
|
91
|
+
* a measurement and a measurement is not something a server can do. Everything it
|
|
92
|
+
* draws is a semantic utility, so a scoped pack boundary restyles the thumb and the
|
|
93
|
+
* track through the cascade exactly as it restyles the text beside them.
|
|
94
|
+
*/
|
|
95
|
+
declare function ScrollArea({ className, children, orientation, keepMounted, label, ...props }: ScrollAreaProps): import("react").JSX.Element;
|
|
96
|
+
export { ScrollArea };
|
|
97
|
+
//# sourceMappingURL=scroll-area.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll-area.d.ts","sourceRoot":"","sources":["../../../src/components/ui/scroll-area.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItD,sDAAsD;AACtD,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,YAAY,GAAG,MAAM,CAAA;AAEtE,wCAAwC;AACxC,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC;IAC9E;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAA;IACnC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,mBAAmB;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,iBAAS,UAAU,CAAC,EAClB,SAAS,EACT,QAAQ,EACR,WAAoB,EACpB,WAAmB,EACnB,KAAK,EACL,GAAG,KAAK,EACT,EAAE,eAAe,+BAiEjB;AAED,OAAO,EAAE,UAAU,EAAE,CAAA"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area';
|
|
4
|
+
import { cn } from '../../lib/utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* A scrollable region whose scrollbar belongs to this design system.
|
|
7
|
+
*
|
|
8
|
+
* **The browser's scrollbar is the one piece of a page that belongs to no design
|
|
9
|
+
* system at all.** Every other surface a consumer draws is a token from here, and
|
|
10
|
+
* the scrollbar beside it is a rectangle the operating system drew, in a colour
|
|
11
|
+
* chosen for the OS's own background rather than for the card the content is on.
|
|
12
|
+
* It is also the only control on a page that is resized by dragging, and the only
|
|
13
|
+
* one that appears and disappears on a timer, so the browser's version is the one
|
|
14
|
+
* control a design system cannot restyle at all. This is where the package takes
|
|
15
|
+
* it over, and it is worth saying plainly that the native scrollbar is not
|
|
16
|
+
* available to restyle: `::-webkit-scrollbar` is one engine, `scrollbar-width` is
|
|
17
|
+
* two values, and neither can express a track and a thumb in tokens.
|
|
18
|
+
*
|
|
19
|
+
* **The scrolling is still the browser's.** This is the decision that matters most
|
|
20
|
+
* and the one worth arguing with, so here is the argument. The tempting version of
|
|
21
|
+
* this Component is a `div` with `overflow: hidden` whose content is moved by a
|
|
22
|
+
* transform on a wheel handler, with a `div` above it pretending to be a
|
|
23
|
+
* scrollbar. That version has no scroll position at all, so there is nothing for
|
|
24
|
+
* a keyboard to move, nothing for a screen reader to announce, no momentum, no
|
|
25
|
+
* scroll-into-view, and no way for a reader to know how far through they are. A
|
|
26
|
+
* custom scrollbar that is a `div` is worse than the browser's, because the browser
|
|
27
|
+
* gives a reader all of that for free and the `div` version takes it away.
|
|
28
|
+
*
|
|
29
|
+
* So the region here is a real scrollable element in the document: the browser
|
|
30
|
+
* scrolls it, the browser gives it keyboard focus when it overflows, the arrow
|
|
31
|
+
* keys, Page Up, Page Down, Home and End all work because it is a scroll
|
|
32
|
+
* container and not a picture of one. Only the appearance of the bar is taken
|
|
33
|
+
* over, and the bar is a real control layered over the native one rather than a
|
|
34
|
+
* replacement for the scrolling.
|
|
35
|
+
*
|
|
36
|
+
* **A native scrollbar cannot be styled, so this one is a sibling of the scroll
|
|
37
|
+
* container rather than a part of it.** That is the whole reason a consumer cannot
|
|
38
|
+
* assemble this from `overflow-auto` and a coloured box, and it is why the region
|
|
39
|
+
* is a named landmark: a scrollable region that a keyboard reader can tab into and
|
|
40
|
+
* that has no name is a tab stop that says nothing. The name is a prop.
|
|
41
|
+
*
|
|
42
|
+
* **A region with nothing to scroll draws no scrollbar.** Not a disabled one and
|
|
43
|
+
* not a faint one: a bar beside a region with no overflow is a claim that there is
|
|
44
|
+
* something past the edge, and a reader who is told that twice loses trust in the
|
|
45
|
+
* third bar they meet. The bar is absent, and a caller who wants it present before
|
|
46
|
+
* the content arrives says so with `keepMounted`.
|
|
47
|
+
*
|
|
48
|
+
* It is a client Component, because measuring overflow and positioning a thumb is
|
|
49
|
+
* a measurement and a measurement is not something a server can do. Everything it
|
|
50
|
+
* draws is a semantic utility, so a scoped pack boundary restyles the thumb and the
|
|
51
|
+
* track through the cascade exactly as it restyles the text beside them.
|
|
52
|
+
*/
|
|
53
|
+
function ScrollArea({ className, children, orientation = 'both', keepMounted = false, label, ...props }) {
|
|
54
|
+
return (_jsxs(ScrollAreaPrimitive.Root, { "data-slot": "scroll-area", className: cn('relative', className), ...props, children: [_jsx(ScrollAreaPrimitive.Viewport, { "data-slot": "scroll-area-viewport", role: "group", "aria-label": label, className: cn('size-full overscroll-contain rounded-[inherit] focus-visible:ring-ring outline-none focus-visible:ring-[3px]'), children: children }), orientation === 'horizontal' ? null : (_jsx(ScrollAreaPrimitive.Scrollbar, { "data-slot": "scroll-area-scrollbar", orientation: "vertical", keepMounted: keepMounted,
|
|
55
|
+
/*
|
|
56
|
+
* Transparent until there is something to scroll, then visible on its
|
|
57
|
+
* own, while the pointer is over the region, and while it is being
|
|
58
|
+
* scrolled. The `bg-transparent` is a declared absence rather than a
|
|
59
|
+
* missing class: the track is the surface the content is already on, so
|
|
60
|
+
* drawing one would put a second surface between the reader and the
|
|
61
|
+
* content for no information. The thumb is the only part that carries
|
|
62
|
+
* ink, and it is `bg-border` at rest and the full muted ink on hover and
|
|
63
|
+
* during a scroll, so it is visible against a light card and a dark one
|
|
64
|
+
* without a value that belongs to neither.
|
|
65
|
+
*/
|
|
66
|
+
className: "flex w-2 touch-none select-none p-px opacity-0 transition-opacity duration-fast ease-out data-[has-overflow-y]:opacity-100 data-[hovering]:opacity-100 data-[scrolling]:opacity-100", children: _jsx(ScrollAreaPrimitive.Thumb, { "data-slot": "scroll-area-thumb", className: "bg-border relative flex-1 rounded-full transition-colors duration-fast ease-out data-[hovering]:bg-muted-foreground data-[scrolling]:bg-muted-foreground" }) })), orientation === 'vertical' ? null : (_jsx(ScrollAreaPrimitive.Scrollbar, { "data-slot": "scroll-area-scrollbar", orientation: "horizontal", keepMounted: keepMounted, className: "flex h-2 touch-none select-none flex-col p-px opacity-0 transition-opacity duration-fast ease-out data-[has-overflow-x]:opacity-100 data-[hovering]:opacity-100 data-[scrolling]:opacity-100", children: _jsx(ScrollAreaPrimitive.Thumb, { "data-slot": "scroll-area-thumb", className: "bg-border relative flex-1 rounded-full transition-colors duration-fast ease-out data-[hovering]:bg-muted-foreground data-[scrolling]:bg-muted-foreground" }) }))] }));
|
|
67
|
+
}
|
|
68
|
+
export { ScrollArea };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One page in a search index.
|
|
3
|
+
*
|
|
4
|
+
* The four fields a result is drawn from, and nothing else. A consumer's index is
|
|
5
|
+
* a JSON array of these served from a static route, so the shape is a contract
|
|
6
|
+
* between a build step Prism does not own and a dialog it does, and it is
|
|
7
|
+
* declared here rather than inferred: an index that grew a field this Component
|
|
8
|
+
* ignored would be a field no reader could be shown and no build would report.
|
|
9
|
+
*
|
|
10
|
+
* `description` and `content` are both optional because the two halves of a site
|
|
11
|
+
* differ. A landing page has a standfirst and a body; a section index has a title
|
|
12
|
+
* and a route and nothing else, and an index entry that had to invent an empty
|
|
13
|
+
* string to satisfy a required field would be an index carrying a fact nobody
|
|
14
|
+
* wrote.
|
|
15
|
+
*/
|
|
16
|
+
export type SearchIndexEntry = {
|
|
17
|
+
/** A stable key for the entry. The route is the usual choice and needs no coordination. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** The result's own name, which is what the dialog puts on the first line. */
|
|
20
|
+
title: string;
|
|
21
|
+
/** Where the result goes. */
|
|
22
|
+
url: string;
|
|
23
|
+
/** One line of context, shown under the title when the index carries it. */
|
|
24
|
+
description?: string;
|
|
25
|
+
/** The body text the query is matched against. */
|
|
26
|
+
content?: string;
|
|
27
|
+
/** Where the result sits in the site's own hierarchy, shown as a trail. */
|
|
28
|
+
breadcrumbs?: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Every sentence the dialog can say, as props.
|
|
32
|
+
*
|
|
33
|
+
* A Component ships no copy and no accessible name it did not receive, because a
|
|
34
|
+
* hardcoded string here is a claim every consumer of the package inherits and the
|
|
35
|
+
* corpus publishes as the design system's own voice. It is a prop per string
|
|
36
|
+
* rather than one `messages` object with defaults, so that overriding any of them
|
|
37
|
+
* is a decision a consumer makes visibly rather than a partial merge of a shipped
|
|
38
|
+
* object.
|
|
39
|
+
*
|
|
40
|
+
* The two result labels are a pair rather than one label with a number appended,
|
|
41
|
+
* and the reason is that this is the only place a count is spoken. English puts
|
|
42
|
+
* the number first and inflects the noun; most other written languages inflect the
|
|
43
|
+
* noun and put the number where their own grammar puts it. A Component that chose
|
|
44
|
+
* between "1 result" and "3 results" for a consumer would be choosing a language's
|
|
45
|
+
* word order on their behalf, and the two strings are the seam where that choice
|
|
46
|
+
* belongs to the site.
|
|
47
|
+
*/
|
|
48
|
+
export type SearchDialogMessages = {
|
|
49
|
+
/** The accessible name of the control that dismisses the dialog. */
|
|
50
|
+
close: string;
|
|
51
|
+
/** What is shown while the index is being fetched. */
|
|
52
|
+
loading: string;
|
|
53
|
+
/** What is shown when the index could not be fetched. */
|
|
54
|
+
failed: string;
|
|
55
|
+
/** What is shown when the query matched nothing. */
|
|
56
|
+
empty: string;
|
|
57
|
+
/** The count announced when exactly one result matched. */
|
|
58
|
+
one: string;
|
|
59
|
+
/** The count announced when any other number of results matched. */
|
|
60
|
+
other: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The props SearchDialog takes.
|
|
64
|
+
*
|
|
65
|
+
* `indexUrl` is where the index is fetched from and it is required, because a
|
|
66
|
+
* dialog with nothing to search is a text field. It is a URL rather than an
|
|
67
|
+
* index object for the reason the whole search design turns on: the index is
|
|
68
|
+
* large, it is the same bytes on every page of a site, and a prop would serialize
|
|
69
|
+
* it into the document of every route. Fetched on the first keystroke instead, it
|
|
70
|
+
* is on the wire once and only for the reader who searched.
|
|
71
|
+
*/
|
|
72
|
+
export type SearchDialogProps = {
|
|
73
|
+
/**
|
|
74
|
+
* Where the JSON index is served from. Fetched once, when the dialog opens.
|
|
75
|
+
*
|
|
76
|
+
* Opening rather than the first keystroke, because a reader who has opened a
|
|
77
|
+
* search box is already searching and the round trip is the difference between
|
|
78
|
+
* results on the first character and results a moment after it. The dialog is
|
|
79
|
+
* itself only mounted after a deliberate act, so "when the dialog opens" is
|
|
80
|
+
* still well after the page has painted and never on a page nobody searched.
|
|
81
|
+
*/
|
|
82
|
+
indexUrl: string;
|
|
83
|
+
/** The accessible name of the dialog, and the placeholder in its field. */
|
|
84
|
+
label: string;
|
|
85
|
+
/** Every sentence the dialog can say. */
|
|
86
|
+
messages: SearchDialogMessages;
|
|
87
|
+
/** The text shown before the reader has typed anything. */
|
|
88
|
+
hint?: string;
|
|
89
|
+
/** Called when the reader dismisses the dialog, by Escape or by the backdrop. */
|
|
90
|
+
onClose: () => void;
|
|
91
|
+
/** The most results to draw. Further matches are dropped rather than paged. */
|
|
92
|
+
limit?: number;
|
|
93
|
+
/** Layout only. Changing a Prism-owned visual property from here is prohibited. */
|
|
94
|
+
className?: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* A search dialog over a static index, computed in the browser.
|
|
98
|
+
*
|
|
99
|
+
* It is a Component and not a Block because it is a control that appears in a
|
|
100
|
+
* region rather than a region of a page, and because it is the one place in the
|
|
101
|
+
* search design that touches the network: the index is fetched here rather than
|
|
102
|
+
* passed in, so a site ships one index file and every page of that site reads it
|
|
103
|
+
* from the same bytes. `SiteNavbar` composes it behind a trigger, and a consumer
|
|
104
|
+
* that wants a different ranking can render this directly or pass its own control
|
|
105
|
+
* through the navbar's `actions` slot.
|
|
106
|
+
*
|
|
107
|
+
* **The ranking is a scorer, not a search engine, and the difference is the
|
|
108
|
+
* trade.** There is no stemming, no fuzzy matching and no typo tolerance: a query
|
|
109
|
+
* token matches a document when the token is a substring of one of its fields.
|
|
110
|
+
* That is a real downgrade against the inverted index a documentation site would
|
|
111
|
+
* otherwise use, and it is deliberate, because the alternative is a Component in
|
|
112
|
+
* a package whose entire value is that it depends on a design system and a token
|
|
113
|
+
* pipeline and nothing else. A fuzzy engine is a large dependency whose ranking a
|
|
114
|
+
* consumer cannot restyle, cannot audit against this package's gates and cannot
|
|
115
|
+
* replace without forking the dialog. The scorer is thirty lines, its behaviour
|
|
116
|
+
* is stated above, and a consumer whose content needs typo tolerance passes its
|
|
117
|
+
* own control instead.
|
|
118
|
+
*
|
|
119
|
+
* **Every query token has to match somewhere.** That is AND rather than OR, and
|
|
120
|
+
* it is why a two-word query narrows instead of widening: with OR, a reader
|
|
121
|
+
* searching for "docs search" is shown every page that mentions either word,
|
|
122
|
+
* which on a documentation site is most of them, and the list stops being a
|
|
123
|
+
* ranking and becomes a filter they have to work through themselves.
|
|
124
|
+
*
|
|
125
|
+
* The weights are per field and stated rather than tuned: a match in a title is
|
|
126
|
+
* worth more than one in a body, and a match at the start of a title is worth more
|
|
127
|
+
* than one at its end, because a reader who types the first word of a page's name
|
|
128
|
+
* is usually right about which page they meant. `FIELD_WEIGHTS` is the whole of
|
|
129
|
+
* the ranking and is a named constant so a reader can see the numbers rather than
|
|
130
|
+
* infer them.
|
|
131
|
+
*
|
|
132
|
+
* The field is focused on mount and Escape closes, and the backdrop closes, so
|
|
133
|
+
* every exit is a control rather than a gesture. The list is marked as a list of
|
|
134
|
+
* results and the count is announced through a live region, so a screen reader
|
|
135
|
+
* user learns that a search ran rather than hearing the list change under them.
|
|
136
|
+
*/
|
|
137
|
+
export declare function SearchDialog({ indexUrl, label, messages, hint, onClose, limit, className, }: SearchDialogProps): import("react").JSX.Element;
|
|
138
|
+
export default SearchDialog;
|
|
139
|
+
//# sourceMappingURL=search-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/ui/search-dialog.tsx"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2FAA2F;IAC3F,EAAE,EAAE,MAAM,CAAA;IACV,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAA;IACb,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAA;IACf,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;OAQG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAA;IACb,yCAAyC;IACzC,QAAQ,EAAE,oBAAoB,CAAA;IAC9B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iFAAiF;IACjF,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,KAAU,EACV,SAAS,GACV,EAAE,iBAAiB,+BA6JnB;AA+ED,eAAe,YAAY,CAAA"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Search, X } from 'lucide-react';
|
|
5
|
+
/**
|
|
6
|
+
* A search dialog over a static index, computed in the browser.
|
|
7
|
+
*
|
|
8
|
+
* It is a Component and not a Block because it is a control that appears in a
|
|
9
|
+
* region rather than a region of a page, and because it is the one place in the
|
|
10
|
+
* search design that touches the network: the index is fetched here rather than
|
|
11
|
+
* passed in, so a site ships one index file and every page of that site reads it
|
|
12
|
+
* from the same bytes. `SiteNavbar` composes it behind a trigger, and a consumer
|
|
13
|
+
* that wants a different ranking can render this directly or pass its own control
|
|
14
|
+
* through the navbar's `actions` slot.
|
|
15
|
+
*
|
|
16
|
+
* **The ranking is a scorer, not a search engine, and the difference is the
|
|
17
|
+
* trade.** There is no stemming, no fuzzy matching and no typo tolerance: a query
|
|
18
|
+
* token matches a document when the token is a substring of one of its fields.
|
|
19
|
+
* That is a real downgrade against the inverted index a documentation site would
|
|
20
|
+
* otherwise use, and it is deliberate, because the alternative is a Component in
|
|
21
|
+
* a package whose entire value is that it depends on a design system and a token
|
|
22
|
+
* pipeline and nothing else. A fuzzy engine is a large dependency whose ranking a
|
|
23
|
+
* consumer cannot restyle, cannot audit against this package's gates and cannot
|
|
24
|
+
* replace without forking the dialog. The scorer is thirty lines, its behaviour
|
|
25
|
+
* is stated above, and a consumer whose content needs typo tolerance passes its
|
|
26
|
+
* own control instead.
|
|
27
|
+
*
|
|
28
|
+
* **Every query token has to match somewhere.** That is AND rather than OR, and
|
|
29
|
+
* it is why a two-word query narrows instead of widening: with OR, a reader
|
|
30
|
+
* searching for "docs search" is shown every page that mentions either word,
|
|
31
|
+
* which on a documentation site is most of them, and the list stops being a
|
|
32
|
+
* ranking and becomes a filter they have to work through themselves.
|
|
33
|
+
*
|
|
34
|
+
* The weights are per field and stated rather than tuned: a match in a title is
|
|
35
|
+
* worth more than one in a body, and a match at the start of a title is worth more
|
|
36
|
+
* than one at its end, because a reader who types the first word of a page's name
|
|
37
|
+
* is usually right about which page they meant. `FIELD_WEIGHTS` is the whole of
|
|
38
|
+
* the ranking and is a named constant so a reader can see the numbers rather than
|
|
39
|
+
* infer them.
|
|
40
|
+
*
|
|
41
|
+
* The field is focused on mount and Escape closes, and the backdrop closes, so
|
|
42
|
+
* every exit is a control rather than a gesture. The list is marked as a list of
|
|
43
|
+
* results and the count is announced through a live region, so a screen reader
|
|
44
|
+
* user learns that a search ran rather than hearing the list change under them.
|
|
45
|
+
*/
|
|
46
|
+
export function SearchDialog({ indexUrl, label, messages, hint, onClose, limit = 12, className, }) {
|
|
47
|
+
const inputRef = useRef(null);
|
|
48
|
+
const [query, setQuery] = useState('');
|
|
49
|
+
const [entries, setEntries] = useState(null);
|
|
50
|
+
const [failed, setFailed] = useState(false);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
inputRef.current?.focus();
|
|
53
|
+
const onKeyDown = (event) => {
|
|
54
|
+
if (event.key === 'Escape')
|
|
55
|
+
onClose();
|
|
56
|
+
};
|
|
57
|
+
document.addEventListener('keydown', onKeyDown);
|
|
58
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
59
|
+
}, [onClose]);
|
|
60
|
+
/**
|
|
61
|
+
* The index, fetched once per dialog.
|
|
62
|
+
*
|
|
63
|
+
* The dialog is mounted only after a reader asks for search, so this runs after
|
|
64
|
+
* a deliberate act rather than on every page load, and the result is held for
|
|
65
|
+
* the life of the mount. A second fetch would be a second download of the same
|
|
66
|
+
* bytes for a reader who reopened the dialog, so `entries` is the cache and the
|
|
67
|
+
* `null` is what "not fetched yet" means.
|
|
68
|
+
*
|
|
69
|
+
* A failed fetch is a state rather than a throw. A reader whose network dropped
|
|
70
|
+
* is told the search is unavailable and still has the close control, which is
|
|
71
|
+
* the whole of what can be done here; an unhandled rejection would take the
|
|
72
|
+
* dialog's event handlers with it and leave a panel with no way out.
|
|
73
|
+
*/
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (entries !== null || failed)
|
|
76
|
+
return;
|
|
77
|
+
let live = true;
|
|
78
|
+
fetch(indexUrl)
|
|
79
|
+
.then((response) => {
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw new Error(`search index responded ${response.status}`);
|
|
82
|
+
return response.json();
|
|
83
|
+
})
|
|
84
|
+
.then((value) => {
|
|
85
|
+
if (live)
|
|
86
|
+
setEntries(Array.isArray(value) ? value : []);
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {
|
|
89
|
+
if (live)
|
|
90
|
+
setFailed(true);
|
|
91
|
+
});
|
|
92
|
+
return () => {
|
|
93
|
+
live = false;
|
|
94
|
+
};
|
|
95
|
+
}, [indexUrl, entries, failed]);
|
|
96
|
+
const results = useMemo(() => rank(entries, query, limit), [entries, query, limit]);
|
|
97
|
+
const typed = query.trim().length > 0;
|
|
98
|
+
const status = (() => {
|
|
99
|
+
if (!typed)
|
|
100
|
+
return hint ?? null;
|
|
101
|
+
if (failed)
|
|
102
|
+
return messages.failed;
|
|
103
|
+
if (entries === null)
|
|
104
|
+
return messages.loading;
|
|
105
|
+
if (results.length === 0)
|
|
106
|
+
return messages.empty;
|
|
107
|
+
return null;
|
|
108
|
+
})();
|
|
109
|
+
return (_jsx("div", { role: "presentation", onClick: onClose, className: "bg-foreground/40 fixed inset-0 z-50 flex items-start justify-center p-4 pt-[15vh]", children: _jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": label, onClick: (event) => event.stopPropagation(), className: 'border-border bg-popover text-popover-foreground flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border shadow-lg ' +
|
|
110
|
+
(className ?? ''), children: [_jsxs("div", { className: "border-border flex items-center gap-2 border-b px-3", children: [_jsx(Search, { "aria-hidden": true, className: "text-muted-foreground size-4 shrink-0" }), _jsx("input", { ref: inputRef, value: query, onChange: (event) => setQuery(event.target.value), placeholder: label, "aria-label": label, className: "text-foreground h-12 flex-1 bg-transparent text-sm outline-none" }), _jsx("button", { type: "button", onClick: onClose, "aria-label": messages.close, className: "text-muted-foreground hover:text-foreground focus-visible:border-ring focus-visible:ring-ring rounded-md p-1 transition-colors focus-visible:ring-[3px] focus-visible:outline-none", children: _jsx(X, { "aria-hidden": true, className: "size-4" }) })] }), _jsxs("div", { className: "overflow-y-auto p-2", children: [_jsx("p", { "aria-live": "polite", className: "sr-only", children: typed ? (_jsxs(_Fragment, { children: [_jsx("span", { children: results.length }), ' ', _jsx("span", { children: results.length === 1 ? messages.one : messages.other })] })) : null }), status ? _jsx("p", { className: "text-muted-foreground px-3 py-6 text-sm", children: status }) : null, _jsx("ul", { className: "flex flex-col", children: results.map((result) => (_jsx("li", { children: _jsxs("a", { href: result.url, onClick: onClose, className: "hover:bg-accent focus-visible:border-ring focus-visible:ring-ring flex flex-col gap-0.5 rounded-lg px-3 py-2 transition-colors focus-visible:ring-[3px] focus-visible:outline-none", children: [_jsx("span", { className: "text-foreground text-sm font-medium", children: result.title }), result.description ? (_jsx("span", { className: "text-muted-foreground line-clamp-2 text-xs", children: result.description })) : null, result.breadcrumbs?.length ? (_jsx("span", { className: "text-muted-foreground text-xs", children: result.breadcrumbs.join(' / ') })) : null] }) }, result.id))) })] })] }) }));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* What a match in each field is worth, and the whole of the ranking.
|
|
114
|
+
*
|
|
115
|
+
* A title match outranks a description match, which outranks a body match, because
|
|
116
|
+
* a page's name is the claim it makes about itself and its body is the evidence.
|
|
117
|
+
* The route carries a small weight of its own because a site whose routes are
|
|
118
|
+
* words, which is most of them, is searchable by them.
|
|
119
|
+
*/
|
|
120
|
+
const FIELD_WEIGHTS = { title: 40, url: 15, description: 10, content: 4 };
|
|
121
|
+
/** What a match at the very start of a field adds on top of its weight. */
|
|
122
|
+
const PREFIX_BONUS = 20;
|
|
123
|
+
function fold(value) {
|
|
124
|
+
return (value ?? '').toLowerCase();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* One entry's score for one query, or `null` when a token matched nothing.
|
|
128
|
+
*
|
|
129
|
+
* `null` rather than zero is what makes the conjunction work: a caller summing
|
|
130
|
+
* per-token scores cannot tell "matched with a low score" from "did not match",
|
|
131
|
+
* so the miss is returned as its own value and the entry is dropped there.
|
|
132
|
+
*/
|
|
133
|
+
function scoreEntry(entry, tokens) {
|
|
134
|
+
const fields = {
|
|
135
|
+
title: fold(entry.title),
|
|
136
|
+
url: fold(entry.url),
|
|
137
|
+
description: fold(entry.description),
|
|
138
|
+
content: fold(entry.content),
|
|
139
|
+
};
|
|
140
|
+
let total = 0;
|
|
141
|
+
for (const token of tokens) {
|
|
142
|
+
let best = 0;
|
|
143
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
144
|
+
const at = value.indexOf(token);
|
|
145
|
+
if (at === -1)
|
|
146
|
+
continue;
|
|
147
|
+
const weight = FIELD_WEIGHTS[field];
|
|
148
|
+
best = Math.max(best, at === 0 ? weight + PREFIX_BONUS : weight);
|
|
149
|
+
}
|
|
150
|
+
if (best === 0)
|
|
151
|
+
return null;
|
|
152
|
+
total += best;
|
|
153
|
+
}
|
|
154
|
+
return total;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The index, narrowed by the query and cut to `limit`.
|
|
158
|
+
*
|
|
159
|
+
* The sort is by score and then by title, so two entries that match equally are
|
|
160
|
+
* in a stable order rather than in the order the index happened to be written.
|
|
161
|
+
* Without the second key a reordering of the build's page list would silently
|
|
162
|
+
* reorder equal-scoring results, which is the kind of change no test catches and
|
|
163
|
+
* every reader notices.
|
|
164
|
+
*/
|
|
165
|
+
function rank(entries, query, limit) {
|
|
166
|
+
const tokens = fold(query).split(/\s+/).filter(Boolean);
|
|
167
|
+
if (entries === null || tokens.length === 0)
|
|
168
|
+
return [];
|
|
169
|
+
const scored = [];
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const score = scoreEntry(entry, tokens);
|
|
172
|
+
if (score !== null)
|
|
173
|
+
scored.push({ entry, score });
|
|
174
|
+
}
|
|
175
|
+
scored.sort((a, b) => b.score - a.score || a.entry.title.localeCompare(b.entry.title));
|
|
176
|
+
return scored.slice(0, limit).map((row) => row.entry);
|
|
177
|
+
}
|
|
178
|
+
export default SearchDialog;
|
|
@@ -20,6 +20,32 @@ export declare function Section({ className, children, ...props }: React.Compone
|
|
|
20
20
|
* placed, not about the block, which is why it is a prop and not a default.
|
|
21
21
|
*/
|
|
22
22
|
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
23
|
+
/**
|
|
24
|
+
* The level one step below a section's own heading, which is the level a title
|
|
25
|
+
* *inside* that section is composed at.
|
|
26
|
+
*
|
|
27
|
+
* **Why a function and not a second prop.** A block that draws a set of titled
|
|
28
|
+
* cards has two headings in it: the section's own, and one per card. Asking the
|
|
29
|
+
* caller for both is asking them to keep two levels in step by hand, and the two
|
|
30
|
+
* drift the moment a block is embedded one level deeper than it was written for.
|
|
31
|
+
* Deriving the child from the parent means a block moved from an `h2` section to an
|
|
32
|
+
* `h3` one carries its card titles with it, which is the whole reason
|
|
33
|
+
* `headingLevel` is a prop.
|
|
34
|
+
*
|
|
35
|
+
* This is what answers the question for every block that draws a titled card, so
|
|
36
|
+
* the answer is one function rather than six independent guesses. A block that
|
|
37
|
+
* hardcodes `h3` is right exactly once, at the nesting depth it was written for.
|
|
38
|
+
*
|
|
39
|
+
* **The clamp at `h6`, and why it is a clamp rather than a wrap.** A section
|
|
40
|
+
* already at `h6` has no child level, so a card title inside it is asked to be
|
|
41
|
+
* something it cannot be. Wrapping to `h1` would put a card's title above the
|
|
42
|
+
* section that introduces it, which is worse than a sibling of it: a reader
|
|
43
|
+
* navigating by heading would meet the card before the section it belongs to.
|
|
44
|
+
* Holding at `h6` costs a repeated level, which a screen reader announces as the
|
|
45
|
+
* same depth rather than as a break in the outline, and a `h6` section holding
|
|
46
|
+
* cards is already a document that has run out of levels.
|
|
47
|
+
*/
|
|
48
|
+
export declare function childLevel(level: HeadingLevel): HeadingLevel;
|
|
23
49
|
/**
|
|
24
50
|
* The heading a section opens with: an optional eyebrow, the title, an optional
|
|
25
51
|
* supporting line, and the level of the one heading it renders.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../../src/components/ui/section.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC;;;GAGG;AACH,wBAAgB,OAAO,CAAC,EACtB,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,+BAWjC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../../src/components/ui/section.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC;;;GAGG;AACH,wBAAgB,OAAO,CAAC,EACtB,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,+BAWjC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAKlE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAG5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,EACP,KAAK,EACL,EAAE,EAAE,OAAc,EAClB,WAAW,EACX,KAAgB,EAChB,SAAS,GACV,EAAE;IACD;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,KAAK,EAAE,SAAS,CAAA;IAChB,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,KAAK,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;IACzB;;;;;OAKG;IACH,EAAE,CAAC,EAAE,YAAY,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,+BA6BA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { cn } from '../../lib/utils';
|
|
2
|
+
import { cn } from '../../lib/utils.js';
|
|
3
3
|
/**
|
|
4
4
|
* Container primitive. Every block composes its own section from this so vertical
|
|
5
5
|
* rhythm stays consistent across the catalog instead of being re-invented per block.
|
|
@@ -7,6 +7,37 @@ import { cn } from '../../lib/utils';
|
|
|
7
7
|
export function Section({ className, children, ...props }) {
|
|
8
8
|
return (_jsx("section", { className: cn('py-16 sm:py-24', className), ...props, children: _jsx("div", { className: "mx-auto w-full max-w-6xl px-6 lg:px-8", children: children }) }));
|
|
9
9
|
}
|
|
10
|
+
/** The six levels in outline order, so a level can be stepped rather than compared. */
|
|
11
|
+
const LEVELS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
12
|
+
/**
|
|
13
|
+
* The level one step below a section's own heading, which is the level a title
|
|
14
|
+
* *inside* that section is composed at.
|
|
15
|
+
*
|
|
16
|
+
* **Why a function and not a second prop.** A block that draws a set of titled
|
|
17
|
+
* cards has two headings in it: the section's own, and one per card. Asking the
|
|
18
|
+
* caller for both is asking them to keep two levels in step by hand, and the two
|
|
19
|
+
* drift the moment a block is embedded one level deeper than it was written for.
|
|
20
|
+
* Deriving the child from the parent means a block moved from an `h2` section to an
|
|
21
|
+
* `h3` one carries its card titles with it, which is the whole reason
|
|
22
|
+
* `headingLevel` is a prop.
|
|
23
|
+
*
|
|
24
|
+
* This is what answers the question for every block that draws a titled card, so
|
|
25
|
+
* the answer is one function rather than six independent guesses. A block that
|
|
26
|
+
* hardcodes `h3` is right exactly once, at the nesting depth it was written for.
|
|
27
|
+
*
|
|
28
|
+
* **The clamp at `h6`, and why it is a clamp rather than a wrap.** A section
|
|
29
|
+
* already at `h6` has no child level, so a card title inside it is asked to be
|
|
30
|
+
* something it cannot be. Wrapping to `h1` would put a card's title above the
|
|
31
|
+
* section that introduces it, which is worse than a sibling of it: a reader
|
|
32
|
+
* navigating by heading would meet the card before the section it belongs to.
|
|
33
|
+
* Holding at `h6` costs a repeated level, which a screen reader announces as the
|
|
34
|
+
* same depth rather than as a break in the outline, and a `h6` section holding
|
|
35
|
+
* cards is already a document that has run out of levels.
|
|
36
|
+
*/
|
|
37
|
+
export function childLevel(level) {
|
|
38
|
+
const at = LEVELS.indexOf(level);
|
|
39
|
+
return LEVELS[Math.min(at + 1, LEVELS.length - 1)];
|
|
40
|
+
}
|
|
10
41
|
/**
|
|
11
42
|
* The heading a section opens with: an optional eyebrow, the title, an optional
|
|
12
43
|
* supporting line, and the level of the one heading it renders.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Select as SelectPrimitive } from '@base-ui/react/select';
|
|
4
4
|
import { CheckIcon, ChevronDownIcon } from 'lucide-react';
|
|
5
|
-
import { cn } from '../../lib/utils';
|
|
5
|
+
import { cn } from '../../lib/utils.js';
|
|
6
6
|
/**
|
|
7
7
|
* A control for choosing one option from a list that opens on demand.
|
|
8
8
|
*
|