@textui/widgets 0.1.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 -0
- package/README.md +69 -0
- package/dist/chart/area-chart.d.ts +4 -0
- package/dist/chart/area-chart.d.ts.map +1 -0
- package/dist/chart/area-chart.js +3 -0
- package/dist/chart/bar-chart.d.ts +32 -0
- package/dist/chart/bar-chart.d.ts.map +1 -0
- package/dist/chart/bar-chart.js +32 -0
- package/dist/chart/gauge.d.ts +26 -0
- package/dist/chart/gauge.d.ts.map +1 -0
- package/dist/chart/gauge.js +26 -0
- package/dist/chart/heatmap.d.ts +13 -0
- package/dist/chart/heatmap.d.ts.map +1 -0
- package/dist/chart/heatmap.js +27 -0
- package/dist/chart/histogram.d.ts +12 -0
- package/dist/chart/histogram.d.ts.map +1 -0
- package/dist/chart/histogram.js +22 -0
- package/dist/chart/index.d.ts +20 -0
- package/dist/chart/index.d.ts.map +1 -0
- package/dist/chart/index.js +32 -0
- package/dist/chart/line-chart.d.ts +7 -0
- package/dist/chart/line-chart.d.ts.map +1 -0
- package/dist/chart/line-chart.js +75 -0
- package/dist/chart/shared.d.ts +34 -0
- package/dist/chart/shared.d.ts.map +1 -0
- package/dist/chart/shared.js +78 -0
- package/dist/chart/sparkline.d.ts +15 -0
- package/dist/chart/sparkline.d.ts.map +1 -0
- package/dist/chart/sparkline.js +22 -0
- package/dist/control/button.d.ts +27 -0
- package/dist/control/button.d.ts.map +1 -0
- package/dist/control/button.js +130 -0
- package/dist/control/checkbox.d.ts +10 -0
- package/dist/control/checkbox.d.ts.map +1 -0
- package/dist/control/checkbox.js +31 -0
- package/dist/control/index.d.ts +20 -0
- package/dist/control/index.d.ts.map +1 -0
- package/dist/control/index.js +36 -0
- package/dist/control/radio-group.d.ts +17 -0
- package/dist/control/radio-group.d.ts.map +1 -0
- package/dist/control/radio-group.js +32 -0
- package/dist/control/search-box.d.ts +7 -0
- package/dist/control/search-box.d.ts.map +1 -0
- package/dist/control/search-box.js +3 -0
- package/dist/control/select.d.ts +42 -0
- package/dist/control/select.d.ts.map +1 -0
- package/dist/control/select.js +166 -0
- package/dist/control/shared.d.ts +41 -0
- package/dist/control/shared.d.ts.map +1 -0
- package/dist/control/shared.js +1 -0
- package/dist/control/slider.d.ts +14 -0
- package/dist/control/slider.d.ts.map +1 -0
- package/dist/control/slider.js +45 -0
- package/dist/control/switch.d.ts +10 -0
- package/dist/control/switch.d.ts.map +1 -0
- package/dist/control/switch.js +32 -0
- package/dist/control/text-area.d.ts +87 -0
- package/dist/control/text-area.d.ts.map +1 -0
- package/dist/control/text-area.js +299 -0
- package/dist/control/text-input.d.ts +10 -0
- package/dist/control/text-input.d.ts.map +1 -0
- package/dist/control/text-input.js +129 -0
- package/dist/data/code-viewer.d.ts +71 -0
- package/dist/data/code-viewer.d.ts.map +1 -0
- package/dist/data/code-viewer.js +232 -0
- package/dist/data/feed.d.ts +43 -0
- package/dist/data/feed.d.ts.map +1 -0
- package/dist/data/feed.js +178 -0
- package/dist/data/index.d.ts +54 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +187 -0
- package/dist/data/list.d.ts +31 -0
- package/dist/data/list.d.ts.map +1 -0
- package/dist/data/list.js +104 -0
- package/dist/data/log-viewer.d.ts +26 -0
- package/dist/data/log-viewer.d.ts.map +1 -0
- package/dist/data/log-viewer.js +69 -0
- package/dist/data/markdown-view.d.ts +31 -0
- package/dist/data/markdown-view.d.ts.map +1 -0
- package/dist/data/markdown-view.js +139 -0
- package/dist/data/pagination.d.ts +9 -0
- package/dist/data/pagination.d.ts.map +1 -0
- package/dist/data/pagination.js +18 -0
- package/dist/data/scroll-thumb.d.ts +15 -0
- package/dist/data/scroll-thumb.d.ts.map +1 -0
- package/dist/data/scroll-thumb.js +32 -0
- package/dist/data/shared.d.ts +12 -0
- package/dist/data/shared.d.ts.map +1 -0
- package/dist/data/shared.js +17 -0
- package/dist/data/tree.d.ts +35 -0
- package/dist/data/tree.d.ts.map +1 -0
- package/dist/data/tree.js +158 -0
- package/dist/decorations.d.ts +88 -0
- package/dist/decorations.d.ts.map +1 -0
- package/dist/decorations.js +135 -0
- package/dist/display/alert.d.ts +8 -0
- package/dist/display/alert.d.ts.map +1 -0
- package/dist/display/alert.js +12 -0
- package/dist/display/badge.d.ts +10 -0
- package/dist/display/badge.d.ts.map +1 -0
- package/dist/display/badge.js +21 -0
- package/dist/display/card.d.ts +8 -0
- package/dist/display/card.d.ts.map +1 -0
- package/dist/display/card.js +12 -0
- package/dist/display/empty-state.d.ts +10 -0
- package/dist/display/empty-state.d.ts.map +1 -0
- package/dist/display/empty-state.js +5 -0
- package/dist/display/error-state.d.ts +9 -0
- package/dist/display/error-state.d.ts.map +1 -0
- package/dist/display/error-state.js +7 -0
- package/dist/display/heading.d.ts +6 -0
- package/dist/display/heading.d.ts.map +1 -0
- package/dist/display/heading.js +8 -0
- package/dist/display/index.d.ts +25 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +52 -0
- package/dist/display/key-value.d.ts +14 -0
- package/dist/display/key-value.d.ts.map +1 -0
- package/dist/display/key-value.js +11 -0
- package/dist/display/label.d.ts +6 -0
- package/dist/display/label.d.ts.map +1 -0
- package/dist/display/label.js +3 -0
- package/dist/display/marquee.d.ts +36 -0
- package/dist/display/marquee.d.ts.map +1 -0
- package/dist/display/marquee.js +55 -0
- package/dist/display/progress.d.ts +39 -0
- package/dist/display/progress.d.ts.map +1 -0
- package/dist/display/progress.js +46 -0
- package/dist/display/skeleton.d.ts +20 -0
- package/dist/display/skeleton.d.ts.map +1 -0
- package/dist/display/skeleton.js +20 -0
- package/dist/display/spinner.d.ts +7 -0
- package/dist/display/spinner.d.ts.map +1 -0
- package/dist/display/spinner.js +9 -0
- package/dist/display/status-dot.d.ts +8 -0
- package/dist/display/status-dot.d.ts.map +1 -0
- package/dist/display/status-dot.js +13 -0
- package/dist/display/timeline.d.ts +12 -0
- package/dist/display/timeline.d.ts.map +1 -0
- package/dist/display/timeline.js +22 -0
- package/dist/find.d.ts +74 -0
- package/dist/find.d.ts.map +1 -0
- package/dist/find.js +102 -0
- package/dist/form/danger-zone.d.ts +11 -0
- package/dist/form/danger-zone.d.ts.map +1 -0
- package/dist/form/danger-zone.js +15 -0
- package/dist/form/field.d.ts +19 -0
- package/dist/form/field.d.ts.map +1 -0
- package/dist/form/field.js +35 -0
- package/dist/form/form-actions.d.ts +11 -0
- package/dist/form/form-actions.d.ts.map +1 -0
- package/dist/form/form-actions.js +16 -0
- package/dist/form/form-section.d.ts +7 -0
- package/dist/form/form-section.d.ts.map +1 -0
- package/dist/form/form-section.js +5 -0
- package/dist/form/form.d.ts +7 -0
- package/dist/form/form.d.ts.map +1 -0
- package/dist/form/form.js +3 -0
- package/dist/form/index.d.ts +42 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +140 -0
- package/dist/form/shared.d.ts +25 -0
- package/dist/form/shared.d.ts.map +1 -0
- package/dist/form/shared.js +9 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/layout/center.d.ts +7 -0
- package/dist/layout/center.d.ts.map +1 -0
- package/dist/layout/center.js +8 -0
- package/dist/layout/column.d.ts +3 -0
- package/dist/layout/column.d.ts.map +1 -0
- package/dist/layout/column.js +2 -0
- package/dist/layout/divider.d.ts +11 -0
- package/dist/layout/divider.d.ts.map +1 -0
- package/dist/layout/divider.js +13 -0
- package/dist/layout/grid.d.ts +10 -0
- package/dist/layout/grid.d.ts.map +1 -0
- package/dist/layout/grid.js +22 -0
- package/dist/layout/index.d.ts +19 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +36 -0
- package/dist/layout/panel.d.ts +37 -0
- package/dist/layout/panel.d.ts.map +1 -0
- package/dist/layout/panel.js +71 -0
- package/dist/layout/row.d.ts +7 -0
- package/dist/layout/row.d.ts.map +1 -0
- package/dist/layout/row.js +2 -0
- package/dist/layout/scroll-view.d.ts +27 -0
- package/dist/layout/scroll-view.d.ts.map +1 -0
- package/dist/layout/scroll-view.js +93 -0
- package/dist/layout/splitter.d.ts +10 -0
- package/dist/layout/splitter.d.ts.map +1 -0
- package/dist/layout/splitter.js +9 -0
- package/dist/layout/stack.d.ts +7 -0
- package/dist/layout/stack.d.ts.map +1 -0
- package/dist/layout/stack.js +5 -0
- package/dist/navigation/breadcrumb.d.ts +14 -0
- package/dist/navigation/breadcrumb.d.ts.map +1 -0
- package/dist/navigation/breadcrumb.js +24 -0
- package/dist/navigation/index.d.ts +17 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +30 -0
- package/dist/navigation/key-hints.d.ts +11 -0
- package/dist/navigation/key-hints.d.ts.map +1 -0
- package/dist/navigation/key-hints.js +13 -0
- package/dist/navigation/menu.d.ts +35 -0
- package/dist/navigation/menu.d.ts.map +1 -0
- package/dist/navigation/menu.js +88 -0
- package/dist/navigation/status-bar.d.ts +18 -0
- package/dist/navigation/status-bar.d.ts.map +1 -0
- package/dist/navigation/status-bar.js +12 -0
- package/dist/navigation/tabs.d.ts +20 -0
- package/dist/navigation/tabs.d.ts.map +1 -0
- package/dist/navigation/tabs.js +41 -0
- package/dist/navigation/toolbar.d.ts +14 -0
- package/dist/navigation/toolbar.d.ts.map +1 -0
- package/dist/navigation/toolbar.js +10 -0
- package/dist/navigation/wizard.d.ts +15 -0
- package/dist/navigation/wizard.d.ts.map +1 -0
- package/dist/navigation/wizard.js +17 -0
- package/dist/overlay/command-palette.d.ts +63 -0
- package/dist/overlay/command-palette.d.ts.map +1 -0
- package/dist/overlay/command-palette.js +415 -0
- package/dist/overlay/dialog.d.ts +16 -0
- package/dist/overlay/dialog.d.ts.map +1 -0
- package/dist/overlay/dialog.js +44 -0
- package/dist/overlay/index.d.ts +84 -0
- package/dist/overlay/index.d.ts.map +1 -0
- package/dist/overlay/index.js +157 -0
- package/dist/overlay/layer-scope.d.ts +21 -0
- package/dist/overlay/layer-scope.d.ts.map +1 -0
- package/dist/overlay/layer-scope.js +5 -0
- package/dist/overlay/path-picker.d.ts +14 -0
- package/dist/overlay/path-picker.d.ts.map +1 -0
- package/dist/overlay/path-picker.js +200 -0
- package/dist/overlay/prompt-dialog.d.ts +12 -0
- package/dist/overlay/prompt-dialog.d.ts.map +1 -0
- package/dist/overlay/prompt-dialog.js +29 -0
- package/dist/overlay/shared.d.ts +17 -0
- package/dist/overlay/shared.d.ts.map +1 -0
- package/dist/overlay/shared.js +17 -0
- package/dist/overlay/toast-host.d.ts +8 -0
- package/dist/overlay/toast-host.d.ts.map +1 -0
- package/dist/overlay/toast-host.js +19 -0
- package/dist/overlay/toast.d.ts +9 -0
- package/dist/overlay/toast.d.ts.map +1 -0
- package/dist/overlay/toast.js +20 -0
- package/dist/overlay/tooltip.d.ts +6 -0
- package/dist/overlay/tooltip.d.ts.map +1 -0
- package/dist/overlay/tooltip.js +11 -0
- package/dist/panel/index.d.ts +79 -0
- package/dist/panel/index.d.ts.map +1 -0
- package/dist/panel/index.js +256 -0
- package/dist/panel/resource-panel.d.ts +93 -0
- package/dist/panel/resource-panel.d.ts.map +1 -0
- package/dist/panel/resource-panel.js +213 -0
- package/dist/panel/shared.d.ts +6 -0
- package/dist/panel/shared.d.ts.map +1 -0
- package/dist/panel/shared.js +4 -0
- package/dist/shells/console-shell.d.ts +8 -0
- package/dist/shells/console-shell.d.ts.map +1 -0
- package/dist/shells/console-shell.js +30 -0
- package/dist/shells/index.d.ts +20 -0
- package/dist/shells/index.d.ts.map +1 -0
- package/dist/shells/index.js +61 -0
- package/dist/shells/paper-shell.d.ts +9 -0
- package/dist/shells/paper-shell.d.ts.map +1 -0
- package/dist/shells/paper-shell.js +23 -0
- package/dist/shells/plain-shell.d.ts +3 -0
- package/dist/shells/plain-shell.d.ts.map +1 -0
- package/dist/shells/plain-shell.js +11 -0
- package/dist/shells/shared.d.ts +12 -0
- package/dist/shells/shared.d.ts.map +1 -0
- package/dist/shells/shared.js +21 -0
- package/dist/shells/workbench-shell.d.ts +9 -0
- package/dist/shells/workbench-shell.d.ts.map +1 -0
- package/dist/shells/workbench-shell.js +43 -0
- package/dist/surface/bar-layout.d.ts +4 -0
- package/dist/surface/bar-layout.d.ts.map +1 -0
- package/dist/surface/bar-layout.js +7 -0
- package/dist/surface/index.d.ts +58 -0
- package/dist/surface/index.d.ts.map +1 -0
- package/dist/surface/index.js +48 -0
- package/dist/surface/inline-layout.d.ts +4 -0
- package/dist/surface/inline-layout.d.ts.map +1 -0
- package/dist/surface/inline-layout.js +7 -0
- package/dist/surface/mount-view.d.ts +7 -0
- package/dist/surface/mount-view.d.ts.map +1 -0
- package/dist/surface/mount-view.js +19 -0
- package/dist/surface/rail-layout.d.ts +4 -0
- package/dist/surface/rail-layout.d.ts.map +1 -0
- package/dist/surface/rail-layout.js +19 -0
- package/dist/surface/shared.d.ts +7 -0
- package/dist/surface/shared.d.ts.map +1 -0
- package/dist/surface/shared.js +1 -0
- package/dist/surface/single-layout.d.ts +4 -0
- package/dist/surface/single-layout.d.ts.map +1 -0
- package/dist/surface/single-layout.js +18 -0
- package/dist/surface/split-layout.d.ts +4 -0
- package/dist/surface/split-layout.d.ts.map +1 -0
- package/dist/surface/split-layout.js +7 -0
- package/dist/surface/stack-layout.d.ts +4 -0
- package/dist/surface/stack-layout.d.ts.map +1 -0
- package/dist/surface/stack-layout.js +22 -0
- package/dist/surface/surface-area.d.ts +16 -0
- package/dist/surface/surface-area.d.ts.map +1 -0
- package/dist/surface/surface-area.js +32 -0
- package/dist/surface/tabs-layout.d.ts +4 -0
- package/dist/surface/tabs-layout.d.ts.map +1 -0
- package/dist/surface/tabs-layout.js +24 -0
- package/dist/tone.d.ts +16 -0
- package/dist/tone.d.ts.map +1 -0
- package/dist/tone.js +45 -0
- package/dist/viewport.d.ts +30 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +17 -0
- package/package.json +54 -0
- package/src/chart/area-chart.ts +9 -0
- package/src/chart/bar-chart.ts +74 -0
- package/src/chart/gauge.ts +60 -0
- package/src/chart/heatmap.ts +49 -0
- package/src/chart/histogram.ts +36 -0
- package/src/chart/index.ts +36 -0
- package/src/chart/line-chart.ts +100 -0
- package/src/chart/shared.ts +112 -0
- package/src/chart/sparkline.ts +46 -0
- package/src/control/button.ts +184 -0
- package/src/control/checkbox.ts +48 -0
- package/src/control/index.ts +40 -0
- package/src/control/radio-group.ts +60 -0
- package/src/control/search-box.ts +15 -0
- package/src/control/select.ts +230 -0
- package/src/control/shared.ts +41 -0
- package/src/control/slider.ts +63 -0
- package/src/control/switch.ts +39 -0
- package/src/control/text-area.ts +390 -0
- package/src/control/text-input.ts +150 -0
- package/src/data/code-viewer.ts +337 -0
- package/src/data/feed.ts +204 -0
- package/src/data/index.ts +243 -0
- package/src/data/list.ts +149 -0
- package/src/data/log-viewer.ts +105 -0
- package/src/data/markdown-view.ts +182 -0
- package/src/data/pagination.ts +31 -0
- package/src/data/scroll-thumb.ts +40 -0
- package/src/data/shared.ts +23 -0
- package/src/data/tree.ts +215 -0
- package/src/decorations.ts +176 -0
- package/src/display/alert.ts +28 -0
- package/src/display/badge.ts +40 -0
- package/src/display/card.ts +23 -0
- package/src/display/empty-state.ts +21 -0
- package/src/display/error-state.ts +23 -0
- package/src/display/heading.ts +17 -0
- package/src/display/index.ts +55 -0
- package/src/display/key-value.ts +32 -0
- package/src/display/label.ts +11 -0
- package/src/display/marquee.ts +88 -0
- package/src/display/progress.ts +92 -0
- package/src/display/skeleton.ts +34 -0
- package/src/display/spinner.ts +20 -0
- package/src/display/status-dot.ts +24 -0
- package/src/display/timeline.ts +47 -0
- package/src/find.ts +138 -0
- package/src/form/danger-zone.ts +34 -0
- package/src/form/field.ts +61 -0
- package/src/form/form-actions.ts +35 -0
- package/src/form/form-section.ts +16 -0
- package/src/form/form.ts +13 -0
- package/src/form/index.ts +191 -0
- package/src/form/shared.ts +36 -0
- package/src/index.ts +63 -0
- package/src/layout/center.ts +17 -0
- package/src/layout/column.ts +6 -0
- package/src/layout/divider.ts +31 -0
- package/src/layout/grid.ts +34 -0
- package/src/layout/index.ts +39 -0
- package/src/layout/panel.ts +114 -0
- package/src/layout/row.ts +11 -0
- package/src/layout/scroll-view.ts +115 -0
- package/src/layout/splitter.ts +24 -0
- package/src/layout/stack.ts +12 -0
- package/src/navigation/breadcrumb.ts +43 -0
- package/src/navigation/index.ts +33 -0
- package/src/navigation/key-hints.ts +25 -0
- package/src/navigation/menu.ts +134 -0
- package/src/navigation/status-bar.ts +43 -0
- package/src/navigation/tabs.ts +73 -0
- package/src/navigation/toolbar.ts +25 -0
- package/src/navigation/wizard.ts +43 -0
- package/src/overlay/command-palette.ts +485 -0
- package/src/overlay/dialog.ts +65 -0
- package/src/overlay/index.ts +232 -0
- package/src/overlay/layer-scope.ts +26 -0
- package/src/overlay/path-picker.ts +215 -0
- package/src/overlay/prompt-dialog.ts +45 -0
- package/src/overlay/shared.ts +19 -0
- package/src/overlay/toast-host.ts +30 -0
- package/src/overlay/toast.ts +35 -0
- package/src/overlay/tooltip.ts +17 -0
- package/src/panel/index.ts +317 -0
- package/src/panel/resource-panel.ts +304 -0
- package/src/panel/shared.ts +8 -0
- package/src/shells/console-shell.ts +47 -0
- package/src/shells/index.ts +66 -0
- package/src/shells/paper-shell.ts +31 -0
- package/src/shells/plain-shell.ts +19 -0
- package/src/shells/shared.ts +24 -0
- package/src/shells/workbench-shell.ts +62 -0
- package/src/surface/bar-layout.ts +11 -0
- package/src/surface/index.ts +53 -0
- package/src/surface/inline-layout.ts +11 -0
- package/src/surface/mount-view.ts +30 -0
- package/src/surface/rail-layout.ts +27 -0
- package/src/surface/shared.ts +7 -0
- package/src/surface/single-layout.ts +25 -0
- package/src/surface/split-layout.ts +11 -0
- package/src/surface/stack-layout.ts +32 -0
- package/src/surface/surface-area.ts +46 -0
- package/src/surface/tabs-layout.ts +30 -0
- package/src/tone.ts +48 -0
- package/src/viewport.ts +45 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import {
|
|
3
|
+
chorded,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
useFocus,
|
|
7
|
+
useInput,
|
|
8
|
+
useMeasure,
|
|
9
|
+
useScrollExtent,
|
|
10
|
+
useState,
|
|
11
|
+
} from '@textui/core';
|
|
12
|
+
import { ScrollThumb } from '../data/scroll-thumb.js';
|
|
13
|
+
|
|
14
|
+
export interface ScrollViewProps extends BoxProps {
|
|
15
|
+
/** Controlled offset. Omit to let the view manage its own. */
|
|
16
|
+
offset?: number;
|
|
17
|
+
onScroll?(offset: number): void;
|
|
18
|
+
/** Draw a scrollbar on the right when the content overflows. */
|
|
19
|
+
scrollbar?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* A tab stop, so the keys that scroll it can reach it.
|
|
22
|
+
*
|
|
23
|
+
* On by default: a viewport had the arrow handlers all along and registered
|
|
24
|
+
* nothing, so unless the caller happened to make it focusable itself the
|
|
25
|
+
* only way to scroll was the wheel - which is to say, on a keyboard, not at
|
|
26
|
+
* all. Turn it off for a view that scrolls inside something already focused.
|
|
27
|
+
*/
|
|
28
|
+
focusable?: boolean;
|
|
29
|
+
autoFocus?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A scrolling viewport.
|
|
34
|
+
*
|
|
35
|
+
* Scroll position is a number of cells, not a fraction: a terminal cannot
|
|
36
|
+
* scroll by half a line, and pretending otherwise makes a list jitter as it
|
|
37
|
+
* rounds.
|
|
38
|
+
*/
|
|
39
|
+
export const ScrollView = defineComponent<ScrollViewProps>('ScrollView', (props) => {
|
|
40
|
+
const {
|
|
41
|
+
offset, onScroll, scrollbar = true, focusable = true, autoFocus, id,
|
|
42
|
+
children, ...rest
|
|
43
|
+
} = props;
|
|
44
|
+
const focus = useFocus({ disabled: !focusable, autoFocus });
|
|
45
|
+
const [internal, setInternal] = useState(0);
|
|
46
|
+
const top = offset ?? internal;
|
|
47
|
+
// How far down this can go before the last line leaves the bottom. Written
|
|
48
|
+
// by the viewport during its render, read here when a key arrives - so a
|
|
49
|
+
// limit that changed with the terminal's height needs no round trip.
|
|
50
|
+
const measured = useMeasure();
|
|
51
|
+
const extent = useScrollExtent();
|
|
52
|
+
// How far the top can go before the last line leaves the bottom of the view.
|
|
53
|
+
// Zero when everything fits, which is also what makes the arrows do nothing
|
|
54
|
+
// in a viewport that has nothing to scroll.
|
|
55
|
+
const limit = Math.max(0, (extent?.height ?? 0) - measured.height);
|
|
56
|
+
|
|
57
|
+
const scrollTo = (next: number): void => {
|
|
58
|
+
const clamped = Math.max(0, Math.min(next, limit));
|
|
59
|
+
if (offset === undefined) setInternal(clamped);
|
|
60
|
+
onScroll?.(clamped);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
useInput(
|
|
64
|
+
(event) => {
|
|
65
|
+
if (chorded(event)) return false;
|
|
66
|
+
if (event.name === 'up') { scrollTo(top - 1); return true; }
|
|
67
|
+
if (event.name === 'down') { scrollTo(top + 1); return true; }
|
|
68
|
+
if (event.name === 'pageup') { scrollTo(top - 10); return true; }
|
|
69
|
+
if (event.name === 'pagedown') { scrollTo(top + 10); return true; }
|
|
70
|
+
if (event.name === 'home') { scrollTo(0); return true; }
|
|
71
|
+
return false;
|
|
72
|
+
},
|
|
73
|
+
{ focusId: focus.id, enabled: focusable },
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// The outer box holds the viewport beside the scrollbar and does not scroll
|
|
77
|
+
// itself. It used to say `overflow: 'scroll'` with a `scrollTop`, on a row -
|
|
78
|
+
// where the scrolling axis is horizontal, so the offset did nothing and the
|
|
79
|
+
// only real effect was to stop the viewport being clamped to the width it
|
|
80
|
+
// had. That made the row report *its own* sideways overflow as this view's
|
|
81
|
+
// scroll extent: a number about a different box on a different axis.
|
|
82
|
+
return h('box', {
|
|
83
|
+
id: id ?? focus.id,
|
|
84
|
+
role: 'region',
|
|
85
|
+
...rest,
|
|
86
|
+
overflow: 'hidden',
|
|
87
|
+
direction: 'row',
|
|
88
|
+
onMouse: (event: { action: string; wheel?: number }) => {
|
|
89
|
+
if (event.action !== 'wheel') return false;
|
|
90
|
+
scrollTo(top + (event.wheel ?? 0) * 3);
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
h('box', { flex: 1, direction: 'column', scrollTop: Math.min(top, limit), overflow: 'scroll' }, children),
|
|
95
|
+
// `ScrollThumb`, which is what every other viewport in the library draws -
|
|
96
|
+
// its own comment says so: "two components drawing their own would drift".
|
|
97
|
+
// This one drew a different thing, a one-cell column filled with a border
|
|
98
|
+
// glyph, that took the offset as a prop and ignored it. So it said nothing
|
|
99
|
+
// about where you were or how much there was, and against a bordered child
|
|
100
|
+
// it was indistinguishable from another border. A scrollbar that cannot be
|
|
101
|
+
// told from a frame is not a scrollbar.
|
|
102
|
+
//
|
|
103
|
+
// And only when there is something to scroll. `limit` is zero when it all
|
|
104
|
+
// fits, which is exactly when a bar is a lie - the prop has always been
|
|
105
|
+
// documented that way and was drawn unconditionally.
|
|
106
|
+
scrollbar && limit > 0
|
|
107
|
+
? h(ScrollThumb, {
|
|
108
|
+
total: extent?.height ?? measured.height,
|
|
109
|
+
rows: measured.height,
|
|
110
|
+
offset: Math.min(top, limit),
|
|
111
|
+
focused: focus.focused,
|
|
112
|
+
})
|
|
113
|
+
: null,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BoxProps, Dimension } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
import { Divider } from './divider.js';
|
|
4
|
+
|
|
5
|
+
export interface SplitterProps extends BoxProps {
|
|
6
|
+
direction?: 'row' | 'column';
|
|
7
|
+
/** Size of the first pane, in cells or percent. */
|
|
8
|
+
size?: Dimension;
|
|
9
|
+
/** Cells the divider occupies. 0 hides it. */
|
|
10
|
+
dividerSize?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Splitter = defineComponent<SplitterProps>('Splitter', (props) => {
|
|
14
|
+
const { direction = 'row', size = '50%', dividerSize = 1, children, ...rest } = props;
|
|
15
|
+
const panes = Array.isArray(children) ? children : [children];
|
|
16
|
+
|
|
17
|
+
return h('box', { direction, ...rest },
|
|
18
|
+
h('box', { [direction === 'row' ? 'width' : 'height']: size }, panes[0]),
|
|
19
|
+
dividerSize > 0
|
|
20
|
+
? h(Divider, { direction: direction === 'row' ? 'vertical' : 'horizontal' })
|
|
21
|
+
: null,
|
|
22
|
+
h('box', { flex: 1 }, panes[1]),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface StackProps extends BoxProps {
|
|
5
|
+
/** Space between children, from the theme's scale. */
|
|
6
|
+
spacing?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Stack = defineComponent<StackProps>('Stack', ({ spacing = 'sm', ...props }) => {
|
|
10
|
+
const theme = useTheme();
|
|
11
|
+
return h('box', { direction: 'column', gap: theme.spacing[spacing], ...props });
|
|
12
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface BreadcrumbProps extends BoxProps {
|
|
5
|
+
items: { id: string; label: string; icon?: string }[];
|
|
6
|
+
onSelect?(id: string): void;
|
|
7
|
+
separator?: string;
|
|
8
|
+
/** Collapse the middle when it does not fit. */
|
|
9
|
+
maxItems?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Breadcrumb = defineComponent<BreadcrumbProps>('Breadcrumb', (props) => {
|
|
13
|
+
const theme = useTheme();
|
|
14
|
+
const { items, onSelect, separator, maxItems, ...rest } = props;
|
|
15
|
+
const sep = separator ?? theme.glyphs.breadcrumb;
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
// Keep the root and the tail; the middle is what a reader needs least.
|
|
19
|
+
type Crumb = { id: string; label: string; icon?: string };
|
|
20
|
+
const shown: Crumb[] = maxItems && items.length > maxItems
|
|
21
|
+
? [
|
|
22
|
+
items[0] as Crumb,
|
|
23
|
+
{ id: '__ellipsis__', label: theme.glyphs.ellipsis },
|
|
24
|
+
...items.slice(items.length - (maxItems - 2)),
|
|
25
|
+
]
|
|
26
|
+
: items;
|
|
27
|
+
|
|
28
|
+
return h('box', { role: 'navigation', direction: 'row', gap: 1, ...rest },
|
|
29
|
+
...shown.flatMap((item, i) => {
|
|
30
|
+
const last = i === shown.length - 1;
|
|
31
|
+
const crumb = h('box', {
|
|
32
|
+
key: item.id,
|
|
33
|
+
direction: 'row',
|
|
34
|
+
gap: item.icon ? 1 : 0,
|
|
35
|
+
onClick: () => { if (item.id !== '__ellipsis__') onSelect?.(item.id); },
|
|
36
|
+
},
|
|
37
|
+
item.icon ? h('text', { content: item.icon }) : null,
|
|
38
|
+
h('text', { content: item.label, fg: last ? 'text' : 'muted', bold: last }),
|
|
39
|
+
);
|
|
40
|
+
return last ? [crumb] : [crumb, h('text', { key: `${item.id}-s`, content: sep, fg: 'subtle' })];
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ComponentDefinition } from '@textui/core';
|
|
2
|
+
import { Breadcrumb } from './breadcrumb.js';
|
|
3
|
+
import { KeyHints } from './key-hints.js';
|
|
4
|
+
import { Menu } from './menu.js';
|
|
5
|
+
import { StatusBar } from './status-bar.js';
|
|
6
|
+
import { Tabs } from './tabs.js';
|
|
7
|
+
import { Toolbar } from './toolbar.js';
|
|
8
|
+
import { Wizard } from './wizard.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Navigation chrome.
|
|
12
|
+
*
|
|
13
|
+
* These are the components a shell is made of, which is why they all take
|
|
14
|
+
* their own items rather than reading a surface: the same `Tabs` serves a tab
|
|
15
|
+
* strip in a workbench and a segmented control inside a panel.
|
|
16
|
+
*/
|
|
17
|
+
export * from './breadcrumb.js';
|
|
18
|
+
export * from './key-hints.js';
|
|
19
|
+
export * from './menu.js';
|
|
20
|
+
export * from './status-bar.js';
|
|
21
|
+
export * from './tabs.js';
|
|
22
|
+
export * from './toolbar.js';
|
|
23
|
+
export * from './wizard.js';
|
|
24
|
+
|
|
25
|
+
export const NAVIGATION_COMPONENTS: ComponentDefinition[] = [
|
|
26
|
+
{ component: 'Tabs', category: 'navigation', renderer: { kind: 'function', render: Tabs }, role: 'tablist', variants: ['underline', 'solid', 'plain'], description: 'Tab strip or segmented control.' },
|
|
27
|
+
{ component: 'Breadcrumb', category: 'navigation', renderer: { kind: 'function', render: Breadcrumb }, role: 'navigation', description: 'Where you are, collapsing in the middle when narrow.' },
|
|
28
|
+
{ component: 'Menu', category: 'navigation', renderer: { kind: 'function', render: Menu }, role: 'menu', description: 'Keyboard-driven list of actions.' },
|
|
29
|
+
{ component: 'StatusBar', category: 'chrome', renderer: { kind: 'function', render: StatusBar }, role: 'contentinfo', description: 'One line, segments left and right.' },
|
|
30
|
+
{ component: 'Toolbar', category: 'chrome', renderer: { kind: 'function', render: Toolbar }, role: 'toolbar', description: 'A row of actions.' },
|
|
31
|
+
{ component: 'KeyHints', category: 'chrome', renderer: { kind: 'function', render: KeyHints }, description: 'What the keys do, right now.' },
|
|
32
|
+
{ component: 'Wizard', category: 'navigation', renderer: { kind: 'function', render: Wizard }, description: 'Ordered steps with progress.' },
|
|
33
|
+
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface KeyHintsProps extends BoxProps {
|
|
5
|
+
hints: { keys: string; label: string }[];
|
|
6
|
+
separator?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The footer line every TUI needs: what the keys do, right now. */
|
|
10
|
+
export const KeyHints = defineComponent<KeyHintsProps>('KeyHints', (props) => {
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
const { hints, separator, ...rest } = props;
|
|
13
|
+
const sep = separator ?? ` ${theme.glyphs.separator} `;
|
|
14
|
+
|
|
15
|
+
return h('box', { direction: 'row', height: 1, ...rest },
|
|
16
|
+
...hints.flatMap((hint, i) => {
|
|
17
|
+
const item = h('box', { key: hint.keys, direction: 'row', gap: 1 },
|
|
18
|
+
h('text', { content: hint.keys, fg: 'accent', bold: true }),
|
|
19
|
+
h('text', { content: hint.label, fg: 'muted' }));
|
|
20
|
+
return i === hints.length - 1
|
|
21
|
+
? [item]
|
|
22
|
+
: [item, h('text', { key: `${hint.keys}-s`, content: sep, fg: 'subtle' })];
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { BoxProps, SemanticVariant } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useFocus, useInput, useState, useTheme } from '@textui/core';
|
|
3
|
+
import { Marquee } from '../display/index.js';
|
|
4
|
+
import { TONE } from '../tone.js';
|
|
5
|
+
|
|
6
|
+
export interface MenuItem {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon?: string;
|
|
10
|
+
/** Keyboard hint shown right-aligned. */
|
|
11
|
+
shortcut?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
tone?: SemanticVariant;
|
|
15
|
+
separatorBefore?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* A switch, and whether it is on. Absent means the row is not a switch, so
|
|
18
|
+
* a menu of ordinary commands keeps its left edge rather than indenting
|
|
19
|
+
* every label to make room for a mark nothing uses.
|
|
20
|
+
*/
|
|
21
|
+
checked?: boolean;
|
|
22
|
+
children?: MenuItem[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MenuProps extends BoxProps {
|
|
26
|
+
items: MenuItem[];
|
|
27
|
+
onSelect?(id: string, item: MenuItem): void;
|
|
28
|
+
/** Rows shown at once. */
|
|
29
|
+
visibleRows?: number;
|
|
30
|
+
activeId?: string;
|
|
31
|
+
autoFocus?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Take focus and handle keys. Off when something else drives the selection -
|
|
34
|
+
* a command palette, where typing belongs to the search field and the list
|
|
35
|
+
* only follows.
|
|
36
|
+
*/
|
|
37
|
+
interactive?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Menu = defineComponent<MenuProps>('Menu', (props) => {
|
|
41
|
+
const theme = useTheme();
|
|
42
|
+
const {
|
|
43
|
+
items, onSelect, visibleRows, activeId, autoFocus, interactive = true, ...rest
|
|
44
|
+
} = props;
|
|
45
|
+
const focus = useFocus({ autoFocus, disabled: !interactive });
|
|
46
|
+
const selectable = items.filter((i) => !i.disabled);
|
|
47
|
+
const [internalHighlight, setHighlight] = useState(
|
|
48
|
+
Math.max(0, items.findIndex((i) => i.id === activeId)),
|
|
49
|
+
);
|
|
50
|
+
// When something else drives the list, `activeId` is the whole truth.
|
|
51
|
+
const highlight = interactive
|
|
52
|
+
? internalHighlight
|
|
53
|
+
: Math.max(0, items.findIndex((i) => i.id === activeId));
|
|
54
|
+
|
|
55
|
+
useInput(
|
|
56
|
+
(event) => {
|
|
57
|
+
switch (event.name) {
|
|
58
|
+
case 'up': setHighlight((highlight - 1 + items.length) % items.length); return true;
|
|
59
|
+
case 'down': setHighlight((highlight + 1) % items.length); return true;
|
|
60
|
+
case 'home': setHighlight(0); return true;
|
|
61
|
+
case 'end': setHighlight(items.length - 1); return true;
|
|
62
|
+
case 'enter': {
|
|
63
|
+
const item = items[highlight];
|
|
64
|
+
if (item && !item.disabled) onSelect?.(item.id, item);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
default: return false;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{ focusId: focus.id, enabled: interactive },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const rows = visibleRows ?? items.length;
|
|
74
|
+
const start = Math.max(0, Math.min(highlight - Math.floor(rows / 2), items.length - rows));
|
|
75
|
+
const window = items.slice(start, start + rows);
|
|
76
|
+
|
|
77
|
+
// One switch in the menu gives every row the column, blank on the rows that
|
|
78
|
+
// are not switches. Per-row would left-align the labels differently
|
|
79
|
+
// depending on what happened to be a toggle, which reads as a broken menu.
|
|
80
|
+
const switches = items.some((item) => item.checked !== undefined);
|
|
81
|
+
|
|
82
|
+
return h('box', { id: focus.id, role: 'menu', direction: 'column', ...rest },
|
|
83
|
+
...window.flatMap((item, i) => {
|
|
84
|
+
const active = start + i === highlight;
|
|
85
|
+
const row = h('box', {
|
|
86
|
+
key: item.id,
|
|
87
|
+
role: 'menuitem',
|
|
88
|
+
label: item.label,
|
|
89
|
+
selected: active,
|
|
90
|
+
direction: 'row',
|
|
91
|
+
gap: 1,
|
|
92
|
+
bg: active ? 'selected' : undefined,
|
|
93
|
+
fg: item.disabled ? 'disabled' : active ? 'inverted' : item.tone ? TONE[item.tone] : undefined,
|
|
94
|
+
onClick: () => { if (!item.disabled) onSelect?.(item.id, item); },
|
|
95
|
+
},
|
|
96
|
+
h('text', { content: active ? theme.glyphs.chevronRight : ' ', shrink: 0 }),
|
|
97
|
+
switches
|
|
98
|
+
? h('text', { content: item.checked === true ? theme.glyphs.check : ' ', shrink: 0 })
|
|
99
|
+
: null,
|
|
100
|
+
// The mark never gives up room. It is the shortest thing on the row
|
|
101
|
+
// and the one that stays legible when everything else is an ellipsis.
|
|
102
|
+
item.icon ? h('text', { content: item.icon, shrink: 0 }) : null,
|
|
103
|
+
// The row under the cursor reads itself out. Everything else is
|
|
104
|
+
// truncated and still, which is what you want of nineteen rows you
|
|
105
|
+
// are scanning past - and useless for the one you have stopped on.
|
|
106
|
+
h(Marquee, { content: item.label, active }),
|
|
107
|
+
h('spacer', { flex: 1 }),
|
|
108
|
+
// The description yields first, and by a lot. It is the elaboration;
|
|
109
|
+
// the label is the thing being chosen, and a row reading "Accept ed…"
|
|
110
|
+
// beside a full sentence has given up the wrong half.
|
|
111
|
+
item.description
|
|
112
|
+
? h(Marquee, {
|
|
113
|
+
content: item.description,
|
|
114
|
+
active,
|
|
115
|
+
fg: active ? 'inverted' : 'muted',
|
|
116
|
+
shrink: 8,
|
|
117
|
+
})
|
|
118
|
+
: null,
|
|
119
|
+
item.shortcut ? h('text', { content: item.shortcut, fg: active ? 'inverted' : 'subtle' }) : null,
|
|
120
|
+
// Present but empty means "this opens something, contents unknown" -
|
|
121
|
+
// which is what the command palette knows about an argument whose
|
|
122
|
+
// choices it has not resolved yet.
|
|
123
|
+
item.children ? h('text', { content: theme.glyphs.chevronRight }) : null,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return item.separatorBefore
|
|
127
|
+
? [h('box', { key: `${item.id}-sep`, height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }), row]
|
|
128
|
+
: [row];
|
|
129
|
+
}),
|
|
130
|
+
selectable.length === 0
|
|
131
|
+
? h('text', { content: ' no matches', fg: 'subtle' })
|
|
132
|
+
: null,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BoxProps, SemanticVariant } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
import { TONE } from '../tone.js';
|
|
4
|
+
|
|
5
|
+
export interface StatusSegment {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
tone?: SemanticVariant;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StatusBarProps extends BoxProps {
|
|
13
|
+
/**
|
|
14
|
+
* Segments before the gap and after it. Named `leading`/`trailing` rather
|
|
15
|
+
* than `left`/`right` because those are style props on every node.
|
|
16
|
+
*/
|
|
17
|
+
leading?: StatusSegment[];
|
|
18
|
+
trailing?: StatusSegment[];
|
|
19
|
+
separator?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const StatusBar = defineComponent<StatusBarProps>('StatusBar', (props) => {
|
|
23
|
+
const theme = useTheme();
|
|
24
|
+
const { leading = [], trailing = [], separator, ...rest } = props;
|
|
25
|
+
const sep = separator ?? ` ${theme.glyphs.separator} `;
|
|
26
|
+
|
|
27
|
+
const segment = (item: StatusSegment): unknown =>
|
|
28
|
+
h('box', { key: item.id, direction: 'row', gap: item.icon ? 1 : 0 },
|
|
29
|
+
item.icon ? h('text', { content: item.icon, fg: item.tone ? TONE[item.tone] : undefined }) : null,
|
|
30
|
+
h('text', { content: item.label, fg: item.tone ? TONE[item.tone] : 'muted' }));
|
|
31
|
+
|
|
32
|
+
const join = (items: StatusSegment[]): unknown[] =>
|
|
33
|
+
items.flatMap((item, i) =>
|
|
34
|
+
i === items.length - 1
|
|
35
|
+
? [segment(item)]
|
|
36
|
+
: [segment(item), h('text', { key: `${item.id}-s`, content: sep, fg: 'subtle' })]);
|
|
37
|
+
|
|
38
|
+
return h('box', { role: 'contentinfo', direction: 'row', height: 1, ...rest },
|
|
39
|
+
...join(leading),
|
|
40
|
+
h('spacer', { flex: 1 }),
|
|
41
|
+
...join(trailing),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useFocus, useInput } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export interface TabItem {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
badge?: string | number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TabsProps extends BoxProps {
|
|
14
|
+
items: TabItem[];
|
|
15
|
+
activeId?: string;
|
|
16
|
+
onChange?(id: string): void;
|
|
17
|
+
/** Underline the active tab instead of inverting it. */
|
|
18
|
+
variant?: 'underline' | 'solid' | 'plain';
|
|
19
|
+
separator?: string;
|
|
20
|
+
/** Take focus on mount, so the keyboard has somewhere to be. */
|
|
21
|
+
autoFocus?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Tabs = defineComponent<TabsProps>('Tabs', (props) => {
|
|
25
|
+
const { items, activeId, onChange, variant = 'underline', separator, autoFocus, ...rest } = props;
|
|
26
|
+
const focus = useFocus({ autoFocus });
|
|
27
|
+
const index = Math.max(0, items.findIndex((t) => t.id === activeId));
|
|
28
|
+
|
|
29
|
+
useInput(
|
|
30
|
+
(event) => {
|
|
31
|
+
const step = event.name === 'right' ? 1 : event.name === 'left' ? -1 : 0;
|
|
32
|
+
if (step === 0) return false;
|
|
33
|
+
const next = items[(index + step + items.length) % items.length];
|
|
34
|
+
if (next && !next.disabled) onChange?.(next.id);
|
|
35
|
+
return true;
|
|
36
|
+
},
|
|
37
|
+
{ focusId: focus.id },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const gap = separator ? 0 : 1;
|
|
41
|
+
|
|
42
|
+
return h('box', { id: focus.id, role: 'tablist', direction: 'row', gap, ...rest },
|
|
43
|
+
...items.flatMap((item, i) => {
|
|
44
|
+
const active = item.id === activeId;
|
|
45
|
+
const tab = h('box', {
|
|
46
|
+
key: item.id,
|
|
47
|
+
role: 'tab',
|
|
48
|
+
label: item.label,
|
|
49
|
+
selected: active,
|
|
50
|
+
direction: 'row',
|
|
51
|
+
gap: 1,
|
|
52
|
+
padding: variant === 'solid' ? [0, 1] : 0,
|
|
53
|
+
bg: variant === 'solid' && active ? 'selected' : undefined,
|
|
54
|
+
fg: item.disabled ? 'disabled' : active ? (variant === 'solid' ? 'inverted' : 'accent') : 'muted',
|
|
55
|
+
bold: active,
|
|
56
|
+
underline: variant === 'underline' && active,
|
|
57
|
+
onClick: () => { if (!item.disabled) onChange?.(item.id); },
|
|
58
|
+
},
|
|
59
|
+
item.icon ? h('text', { content: item.icon }) : null,
|
|
60
|
+
h('text', { content: item.label }),
|
|
61
|
+
item.badge !== undefined
|
|
62
|
+
// No colour of its own on the active tab: `muted` against the
|
|
63
|
+
// selected background is the one pairing that never reads.
|
|
64
|
+
? h('text', { content: String(item.badge), fg: active ? undefined : 'muted' })
|
|
65
|
+
: null,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return separator && i < items.length - 1
|
|
69
|
+
? [tab, h('text', { key: `${item.id}-sep`, content: separator, fg: 'borderSubtle' })]
|
|
70
|
+
: [tab];
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BoxProps, SemanticVariant } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
import { TONE } from '../tone.js';
|
|
4
|
+
|
|
5
|
+
export interface ToolbarProps extends BoxProps {
|
|
6
|
+
items: { id: string; label: string; icon?: string; shortcut?: string; disabled?: boolean; tone?: SemanticVariant }[];
|
|
7
|
+
onSelect?(id: string): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Toolbar = defineComponent<ToolbarProps>('Toolbar', ({ items, onSelect, ...rest }) =>
|
|
11
|
+
h('box', { role: 'toolbar', direction: 'row', gap: 2, ...rest },
|
|
12
|
+
...items.map((item) =>
|
|
13
|
+
h('box', {
|
|
14
|
+
key: item.id,
|
|
15
|
+
direction: 'row',
|
|
16
|
+
gap: 1,
|
|
17
|
+
fg: item.disabled ? 'disabled' : item.tone ? TONE[item.tone] : undefined,
|
|
18
|
+
onClick: () => { if (!item.disabled) onSelect?.(item.id); },
|
|
19
|
+
},
|
|
20
|
+
item.shortcut ? h('text', { content: item.shortcut, fg: 'accent', bold: true }) : null,
|
|
21
|
+
item.icon ? h('text', { content: item.icon }) : null,
|
|
22
|
+
h('text', { content: item.label, fg: 'muted' }),
|
|
23
|
+
)),
|
|
24
|
+
),
|
|
25
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BoxProps, StyleColor } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface WizardStep {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WizardProps extends BoxProps {
|
|
11
|
+
steps: WizardStep[];
|
|
12
|
+
activeId: string;
|
|
13
|
+
/** Steps already completed. */
|
|
14
|
+
completedIds?: string[];
|
|
15
|
+
orientation?: 'horizontal' | 'vertical';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Wizard = defineComponent<WizardProps>('Wizard', (props) => {
|
|
19
|
+
const theme = useTheme();
|
|
20
|
+
const { steps, activeId, completedIds = [], orientation = 'horizontal', ...rest } = props;
|
|
21
|
+
const activeIndex = steps.findIndex((s) => s.id === activeId);
|
|
22
|
+
|
|
23
|
+
return h('box', { direction: orientation === 'horizontal' ? 'row' : 'column', gap: orientation === 'horizontal' ? 2 : 0, ...rest },
|
|
24
|
+
...steps.map((step, i) => {
|
|
25
|
+
const done = completedIds.includes(step.id);
|
|
26
|
+
const active = step.id === activeId;
|
|
27
|
+
const glyph = done ? theme.glyphs.check : active ? theme.glyphs.bulletFilled : theme.glyphs.bulletHollow;
|
|
28
|
+
const tone: StyleColor = done ? 'success' : active ? 'accent' : 'subtle';
|
|
29
|
+
|
|
30
|
+
return h('box', { key: step.id, direction: 'row', gap: 1 },
|
|
31
|
+
h('text', { content: glyph, fg: tone }),
|
|
32
|
+
h('box', { direction: 'column' },
|
|
33
|
+
h('text', { content: `${i + 1}. ${step.label}`, bold: active, fg: active ? 'text' : 'muted' }),
|
|
34
|
+
step.description && active
|
|
35
|
+
? h('text', { content: step.description, fg: 'subtle' })
|
|
36
|
+
: null),
|
|
37
|
+
orientation === 'horizontal' && i < steps.length - 1
|
|
38
|
+
? h('text', { content: theme.glyphs.chevronRight, fg: i < activeIndex ? 'success' : 'subtle' })
|
|
39
|
+
: null,
|
|
40
|
+
);
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
});
|