@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,243 @@
|
|
|
1
|
+
import type { BoxProps, ComponentDefinition, RenderOutput, SemanticVariant } from '@textui/core';
|
|
2
|
+
import {
|
|
3
|
+
chorded,
|
|
4
|
+
defineComponent,
|
|
5
|
+
fitTo,
|
|
6
|
+
h,
|
|
7
|
+
stringWidth,
|
|
8
|
+
useFocus,
|
|
9
|
+
useInput,
|
|
10
|
+
useMeasure,
|
|
11
|
+
useState,
|
|
12
|
+
useTheme,
|
|
13
|
+
} from '@textui/core';
|
|
14
|
+
import { EmptyState } from '../display/index.js';
|
|
15
|
+
import { TONE } from '../tone.js';
|
|
16
|
+
import { viewportRows } from '../viewport.js';
|
|
17
|
+
import { CodeViewer } from './code-viewer.js';
|
|
18
|
+
import { Feed } from './feed.js';
|
|
19
|
+
import { List } from './list.js';
|
|
20
|
+
import { LogViewer } from './log-viewer.js';
|
|
21
|
+
import { MarkdownView } from './markdown-view.js';
|
|
22
|
+
import { Pagination } from './pagination.js';
|
|
23
|
+
import { Tree } from './tree.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lists, tables and trees.
|
|
27
|
+
*
|
|
28
|
+
* These are the components that own real interaction state - selection,
|
|
29
|
+
* keyboard navigation, scrolling - so they are the ones where a controlled and
|
|
30
|
+
* an uncontrolled mode both have to work. The rule throughout: if the caller
|
|
31
|
+
* passes the value, the caller owns it; otherwise the component keeps it.
|
|
32
|
+
*/
|
|
33
|
+
export * from './code-viewer.js';
|
|
34
|
+
export * from './feed.js';
|
|
35
|
+
export * from './list.js';
|
|
36
|
+
export * from './log-viewer.js';
|
|
37
|
+
export * from './markdown-view.js';
|
|
38
|
+
export * from './pagination.js';
|
|
39
|
+
export * from './scroll-thumb.js';
|
|
40
|
+
export * from './tree.js';
|
|
41
|
+
|
|
42
|
+
export interface TableColumn<T extends object = Record<string, unknown>> {
|
|
43
|
+
key: string;
|
|
44
|
+
header: string;
|
|
45
|
+
/** Fixed cell width. Omit to size from the content. */
|
|
46
|
+
width?: number;
|
|
47
|
+
align?: 'left' | 'center' | 'right';
|
|
48
|
+
/** Lower priority columns are dropped first as the table narrows. */
|
|
49
|
+
priority?: number;
|
|
50
|
+
/** Render a cell to a string. Defaults to `String(row[key])`. */
|
|
51
|
+
format?(value: unknown, row: T): string;
|
|
52
|
+
tone?(value: unknown, row: T): SemanticVariant | undefined;
|
|
53
|
+
/** Take the whole remaining width. At most one column should. */
|
|
54
|
+
flex?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TableProps<T extends object = Record<string, unknown>> extends BoxProps {
|
|
58
|
+
columns: TableColumn<T>[];
|
|
59
|
+
rows: T[];
|
|
60
|
+
/** Field used as the row key. Defaults to `id`. */
|
|
61
|
+
rowKey?: string;
|
|
62
|
+
selectedKey?: string;
|
|
63
|
+
onSelect?(key: string, row: T): void;
|
|
64
|
+
onActivate?(key: string, row: T): void;
|
|
65
|
+
visibleRows?: number;
|
|
66
|
+
showHeader?: boolean;
|
|
67
|
+
emptyMessage?: string;
|
|
68
|
+
focusable?: boolean;
|
|
69
|
+
/** Width below which low-priority columns start being dropped. */
|
|
70
|
+
responsive?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A data table.
|
|
75
|
+
*
|
|
76
|
+
* Responsiveness is a column-priority problem, not a font-size problem: as the
|
|
77
|
+
* table narrows it drops the lowest-priority columns rather than squeezing
|
|
78
|
+
* every column until none of them is readable.
|
|
79
|
+
*/
|
|
80
|
+
const TableImpl = defineComponent<TableProps<Record<string, unknown>>>('Table', (props) => {
|
|
81
|
+
const theme = useTheme();
|
|
82
|
+
const {
|
|
83
|
+
columns, rows, rowKey = 'id', selectedKey, onSelect, onActivate,
|
|
84
|
+
visibleRows, showHeader = true, emptyMessage = 'No rows',
|
|
85
|
+
focusable = true, responsive = true, width, ...rest
|
|
86
|
+
} = props;
|
|
87
|
+
|
|
88
|
+
const focus = useFocus({ disabled: !focusable });
|
|
89
|
+
const measured = useMeasure();
|
|
90
|
+
const [internalKey, setInternalKey] = useState<string | null>(null);
|
|
91
|
+
const keyOf = (row: Record<string, unknown>): string => String(row[rowKey] ?? '');
|
|
92
|
+
|
|
93
|
+
// The header is a row of the pane too, so it comes out of the body's share.
|
|
94
|
+
const bodyRows = viewportRows(props, measured, rows.length, {
|
|
95
|
+
requested: visibleRows,
|
|
96
|
+
reserved: showHeader ? 1 : 0,
|
|
97
|
+
});
|
|
98
|
+
const pageSize = bodyRows;
|
|
99
|
+
|
|
100
|
+
const currentKey = selectedKey ?? internalKey ?? (rows[0] ? keyOf(rows[0]) : null);
|
|
101
|
+
const index = Math.max(0, rows.findIndex((r) => keyOf(r) === currentKey));
|
|
102
|
+
|
|
103
|
+
const select = (next: number): void => {
|
|
104
|
+
const row = rows[Math.max(0, Math.min(rows.length - 1, next))];
|
|
105
|
+
if (!row) return;
|
|
106
|
+
if (selectedKey === undefined) setInternalKey(keyOf(row));
|
|
107
|
+
onSelect?.(keyOf(row), row);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
useInput(
|
|
111
|
+
(event) => {
|
|
112
|
+
if (rows.length === 0 || chorded(event)) return false;
|
|
113
|
+
switch (event.name) {
|
|
114
|
+
case 'up': select(index - 1); return true;
|
|
115
|
+
case 'down': select(index + 1); return true;
|
|
116
|
+
case 'home': select(0); return true;
|
|
117
|
+
case 'end': select(rows.length - 1); return true;
|
|
118
|
+
case 'pageup': select(index - pageSize); return true;
|
|
119
|
+
case 'pagedown': select(index + pageSize); return true;
|
|
120
|
+
case 'enter': {
|
|
121
|
+
const row = rows[index];
|
|
122
|
+
if (row) onActivate?.(keyOf(row), row);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
default: return false;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{ focusId: focus.id, enabled: focusable },
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (rows.length === 0) return h(EmptyState, { title: emptyMessage, ...rest });
|
|
132
|
+
|
|
133
|
+
// Column widths: fixed where stated, else the widest cell, capped.
|
|
134
|
+
const columnWidths = columns.map((col) => {
|
|
135
|
+
if (col.width !== undefined) return col.width;
|
|
136
|
+
const header = showHeader ? stringWidth(col.header) : 0;
|
|
137
|
+
const cells = rows.map((row) => {
|
|
138
|
+
const value = (row as Record<string, unknown>)[col.key];
|
|
139
|
+
return stringWidth(col.format ? col.format(value, row) : String(value ?? ''));
|
|
140
|
+
});
|
|
141
|
+
return Math.max(header, ...cells, 1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const gap = theme.density === 'compact' ? 1 : 2;
|
|
145
|
+
const available = typeof width === 'number' ? width : Infinity;
|
|
146
|
+
|
|
147
|
+
// Drop the lowest-priority columns until what remains fits.
|
|
148
|
+
//
|
|
149
|
+
// Two rules keep this from making a table worse. A column that states no
|
|
150
|
+
// priority inherits its position - earlier columns matter more - so an
|
|
151
|
+
// unstated column never ties with one explicitly marked unimportant. And the
|
|
152
|
+
// first column is never dropped: it is what identifies the row, and a table
|
|
153
|
+
// of measurements with nothing to attach them to is not a smaller table, it
|
|
154
|
+
// is a useless one.
|
|
155
|
+
let shown = columns.map((col, i) => ({
|
|
156
|
+
col,
|
|
157
|
+
width: columnWidths[i] as number,
|
|
158
|
+
priority: col.priority ?? columns.length - i,
|
|
159
|
+
index: i,
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
if (responsive && Number.isFinite(available)) {
|
|
163
|
+
const total = (): number =>
|
|
164
|
+
shown.reduce((sum, c) => sum + c.width, 0) + gap * Math.max(0, shown.length - 1);
|
|
165
|
+
|
|
166
|
+
while (total() > available && shown.length > 1) {
|
|
167
|
+
let worstAt = -1;
|
|
168
|
+
let worst = Infinity;
|
|
169
|
+
// Skip index 0, and prefer the rightmost on a tie.
|
|
170
|
+
for (let i = 1; i < shown.length; i++) {
|
|
171
|
+
const p = (shown[i] as { priority: number }).priority;
|
|
172
|
+
if (p <= worst) { worst = p; worstAt = i; }
|
|
173
|
+
}
|
|
174
|
+
if (worstAt === -1) break;
|
|
175
|
+
shown = shown.filter((_, i) => i !== worstAt);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const rowsWindow = (): { row: Record<string, unknown>; key: string }[] => {
|
|
180
|
+
const all = rows.map((row) => ({ row: row as Record<string, unknown>, key: keyOf(row) }));
|
|
181
|
+
if (all.length <= bodyRows) return all;
|
|
182
|
+
const start = Math.max(0, Math.min(index - Math.floor(bodyRows / 2), all.length - bodyRows));
|
|
183
|
+
return all.slice(start, start + bodyRows);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const cell = (text: string, entry: { col: TableColumn; width: number }, tone?: SemanticVariant): unknown =>
|
|
187
|
+
h('box', { key: entry.col.key, width: entry.col.flex ? undefined : entry.width, flex: entry.col.flex ? 1 : undefined },
|
|
188
|
+
h('text', {
|
|
189
|
+
content: fitTo(text, entry.width, entry.col.align ?? 'left'),
|
|
190
|
+
fg: tone ? TONE[tone] : undefined,
|
|
191
|
+
truncate: 'end',
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
return h('box', { id: focus.id, role: 'table', direction: 'column', width, ...rest },
|
|
195
|
+
showHeader
|
|
196
|
+
? h('box', { role: 'row', direction: 'row', gap, dim: true },
|
|
197
|
+
...shown.map((entry) =>
|
|
198
|
+
h('box', { key: entry.col.key, width: entry.col.flex ? undefined : entry.width, flex: entry.col.flex ? 1 : undefined, role: 'columnheader' },
|
|
199
|
+
h('text', { content: fitTo(entry.col.header, entry.width, entry.col.align ?? 'left') }))))
|
|
200
|
+
: null,
|
|
201
|
+
|
|
202
|
+
...rowsWindow().map(({ row, key }) => {
|
|
203
|
+
const active = key === currentKey;
|
|
204
|
+
return h('box', {
|
|
205
|
+
key,
|
|
206
|
+
role: 'row',
|
|
207
|
+
selected: active,
|
|
208
|
+
direction: 'row',
|
|
209
|
+
gap,
|
|
210
|
+
bg: active && focus.focused ? 'selected' : active ? 'active' : undefined,
|
|
211
|
+
fg: active && focus.focused ? 'inverted' : undefined,
|
|
212
|
+
onClick: () => {
|
|
213
|
+
if (selectedKey === undefined) setInternalKey(key);
|
|
214
|
+
onSelect?.(key, row);
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
...shown.map((entry) => {
|
|
218
|
+
const value = row[entry.col.key];
|
|
219
|
+
const text = entry.col.format ? entry.col.format(value, row) : String(value ?? '');
|
|
220
|
+
return cell(text, entry, entry.col.tone?.(value, row));
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Exported with its own generic signature so a caller's row type flows into
|
|
229
|
+
* `format` and `tone` - the alternative is every table taking
|
|
230
|
+
* `Record<string, unknown>` and every cell function starting with a cast.
|
|
231
|
+
*/
|
|
232
|
+
export const Table = TableImpl as <T extends object>(props: TableProps<T>) => RenderOutput;
|
|
233
|
+
|
|
234
|
+
export const DATA_COMPONENTS: ComponentDefinition[] = [
|
|
235
|
+
{ component: 'List', category: 'data', renderer: { kind: 'function', render: List }, role: 'list', description: 'Selectable rows with keyboard navigation.' },
|
|
236
|
+
{ component: 'Table', category: 'data', renderer: { kind: 'function', render: TableImpl }, role: 'table', description: 'Columns that drop by priority as space runs out.' },
|
|
237
|
+
{ component: 'Tree', category: 'data', renderer: { kind: 'function', render: Tree }, role: 'tree', description: 'Expandable hierarchy.' },
|
|
238
|
+
{ component: 'Pagination', category: 'data', renderer: { kind: 'function', render: Pagination }, description: 'Page of pages.' },
|
|
239
|
+
{ component: 'LogViewer', category: 'data', renderer: { kind: 'function', render: LogViewer }, role: 'log', description: 'Streaming lines that follow the tail until you scroll.' },
|
|
240
|
+
{ component: 'CodeViewer', category: 'data', renderer: { kind: 'function', render: CodeViewer }, role: 'document', description: 'A scrolling, syntax-coloured file viewer.' },
|
|
241
|
+
{ component: 'MarkdownView', category: 'data', renderer: { kind: 'function', render: MarkdownView }, role: 'document', description: 'Markdown drawn into the width it was given. Does not scroll.' },
|
|
242
|
+
{ component: 'Feed', category: 'data', renderer: { kind: 'function', render: Feed }, role: 'list', description: 'A viewport over entries of any height, with a cursor and a tail it follows.' },
|
|
243
|
+
];
|
package/src/data/list.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { BoxProps, SemanticVariant } from '@textui/core';
|
|
2
|
+
import {
|
|
3
|
+
chorded,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
useFocus,
|
|
7
|
+
useInput,
|
|
8
|
+
useMeasure,
|
|
9
|
+
useState,
|
|
10
|
+
useTheme,
|
|
11
|
+
} from '@textui/core';
|
|
12
|
+
import { EmptyState } from '../display/index.js';
|
|
13
|
+
import { TONE } from '../tone.js';
|
|
14
|
+
import { viewportRows } from '../viewport.js';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export interface ListItem {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
icon?: string;
|
|
22
|
+
tone?: SemanticVariant;
|
|
23
|
+
meta?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ListProps extends BoxProps {
|
|
28
|
+
items: ListItem[];
|
|
29
|
+
selectedId?: string;
|
|
30
|
+
onSelect?(id: string, item: ListItem): void;
|
|
31
|
+
onActivate?(id: string, item: ListItem): void;
|
|
32
|
+
/** Rows visible at once. Scrolls when there are more. */
|
|
33
|
+
visibleRows?: number;
|
|
34
|
+
emptyMessage?: string;
|
|
35
|
+
/** Draw a marker column for the selected row. */
|
|
36
|
+
marker?: boolean;
|
|
37
|
+
focusable?: boolean;
|
|
38
|
+
autoFocus?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* A stable focus id, so a command - or the screen that owns this - can put
|
|
41
|
+
* the reader here by name. Without one the id comes from the instance, which
|
|
42
|
+
* nothing outside the render can know.
|
|
43
|
+
*/
|
|
44
|
+
focusId?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const List = defineComponent<ListProps>('List', (props) => {
|
|
48
|
+
const theme = useTheme();
|
|
49
|
+
const {
|
|
50
|
+
items, selectedId, onSelect, onActivate, visibleRows,
|
|
51
|
+
emptyMessage = 'Nothing here', marker = true, focusable = true,
|
|
52
|
+
autoFocus, focusId, ...rest
|
|
53
|
+
} = props;
|
|
54
|
+
|
|
55
|
+
const focus = useFocus({
|
|
56
|
+
...(focusId ? { id: focusId } : {}),
|
|
57
|
+
disabled: !focusable,
|
|
58
|
+
...(autoFocus ? { autoFocus } : {}),
|
|
59
|
+
});
|
|
60
|
+
const measured = useMeasure();
|
|
61
|
+
const [internalId, setInternalId] = useState<string | null>(
|
|
62
|
+
items.find((i) => !i.disabled)?.id ?? null,
|
|
63
|
+
);
|
|
64
|
+
const currentId = selectedId ?? internalId;
|
|
65
|
+
const pageSize = viewportRows(props, measured, 10, { requested: visibleRows });
|
|
66
|
+
const index = Math.max(0, items.findIndex((i) => i.id === currentId));
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Move to `target`, skipping past anything unselectable on the way.
|
|
70
|
+
*
|
|
71
|
+
* The direction has to be given rather than inferred, because `home` and
|
|
72
|
+
* `end` land on an index without coming from one. Stopping at a disabled row
|
|
73
|
+
* instead of stepping over it makes a heading into a wall - which is what a
|
|
74
|
+
* grouped list is made of, so it could not be built at all.
|
|
75
|
+
*/
|
|
76
|
+
const selectAt = (target: number, direction: 1 | -1): void => {
|
|
77
|
+
let i = Math.max(0, Math.min(items.length - 1, target));
|
|
78
|
+
while (i >= 0 && i < items.length && items[i]?.disabled === true) i += direction;
|
|
79
|
+
const item = items[i];
|
|
80
|
+
if (!item) return;
|
|
81
|
+
if (selectedId === undefined) setInternalId(item.id);
|
|
82
|
+
onSelect?.(item.id, item);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
useInput(
|
|
86
|
+
(event) => {
|
|
87
|
+
if (items.length === 0 || chorded(event)) return false;
|
|
88
|
+
switch (event.name) {
|
|
89
|
+
case 'up': selectAt(index - 1, -1); return true;
|
|
90
|
+
case 'down': selectAt(index + 1, 1); return true;
|
|
91
|
+
case 'home': selectAt(0, 1); return true;
|
|
92
|
+
case 'end': selectAt(items.length - 1, -1); return true;
|
|
93
|
+
case 'pageup': selectAt(index - pageSize, -1); return true;
|
|
94
|
+
case 'pagedown': selectAt(index + pageSize, 1); return true;
|
|
95
|
+
case 'enter': {
|
|
96
|
+
const item = items[index];
|
|
97
|
+
if (item && item.disabled !== true) onActivate?.(item.id, item);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
default: return false;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{ focusId: focus.id, enabled: focusable },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (items.length === 0) {
|
|
107
|
+
return h(EmptyState, { title: emptyMessage, ...rest });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Keep the selection in view without moving it more than necessary.
|
|
111
|
+
//
|
|
112
|
+
// Unstated, the row count comes from the space the list was given rather
|
|
113
|
+
// than from how many items it holds - a thousand-row list must not decide
|
|
114
|
+
// how tall its own pane is.
|
|
115
|
+
const rows = viewportRows(props, measured, items.length, { requested: visibleRows });
|
|
116
|
+
const start = Math.max(0, Math.min(index - Math.floor(rows / 2), items.length - rows));
|
|
117
|
+
const window = items.slice(start, start + rows);
|
|
118
|
+
|
|
119
|
+
return h('box', { id: focus.id, role: 'list', direction: 'column', ...rest },
|
|
120
|
+
...window.map((item) => {
|
|
121
|
+
const active = item.id === currentId;
|
|
122
|
+
return h('box', {
|
|
123
|
+
key: item.id,
|
|
124
|
+
role: 'listitem',
|
|
125
|
+
label: item.label,
|
|
126
|
+
selected: active,
|
|
127
|
+
direction: 'row',
|
|
128
|
+
gap: 1,
|
|
129
|
+
bg: active && focus.focused ? 'selected' : active ? 'active' : undefined,
|
|
130
|
+
fg: item.disabled ? 'disabled' : active && focus.focused ? 'inverted' : undefined,
|
|
131
|
+
onClick: () => {
|
|
132
|
+
if (item.disabled) return;
|
|
133
|
+
if (selectedId === undefined) setInternalId(item.id);
|
|
134
|
+
onSelect?.(item.id, item);
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
marker
|
|
138
|
+
? h('text', { content: active ? theme.glyphs.chevronRight : ' ' })
|
|
139
|
+
: null,
|
|
140
|
+
item.icon ? h('text', { content: item.icon, fg: item.tone ? TONE[item.tone] : undefined }) : null,
|
|
141
|
+
h('text', { content: item.label, flex: 1, truncate: 'end' }),
|
|
142
|
+
// The secondary columns keep the row's colour once it is selected;
|
|
143
|
+
// `muted` on a selected background is unreadable.
|
|
144
|
+
item.description ? h('text', { content: item.description, fg: active ? undefined : 'muted', truncate: 'end' }) : null,
|
|
145
|
+
item.meta ? h('text', { content: item.meta, fg: active ? undefined : 'muted' }) : null,
|
|
146
|
+
);
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { BoxProps, SemanticVariant } from '@textui/core';
|
|
2
|
+
import {
|
|
3
|
+
chorded,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
useFocus,
|
|
7
|
+
useInput,
|
|
8
|
+
useMeasure,
|
|
9
|
+
useState,
|
|
10
|
+
} from '@textui/core';
|
|
11
|
+
import { TONE } from '../tone.js';
|
|
12
|
+
import { viewportRows } from '../viewport.js';
|
|
13
|
+
|
|
14
|
+
export interface LogLine {
|
|
15
|
+
time?: string;
|
|
16
|
+
level?: 'debug' | 'info' | 'warn' | 'error';
|
|
17
|
+
source?: string;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LogViewerProps extends BoxProps {
|
|
22
|
+
lines: LogLine[];
|
|
23
|
+
/** Rows shown. Older lines scroll off the top. */
|
|
24
|
+
visibleRows?: number;
|
|
25
|
+
/** Stick to the newest line. Turned off when the reader scrolls up. */
|
|
26
|
+
follow?: boolean;
|
|
27
|
+
showTime?: boolean;
|
|
28
|
+
showLevel?: boolean;
|
|
29
|
+
onFollowChange?(follow: boolean): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const LEVEL_TONE: Record<string, SemanticVariant> = {
|
|
33
|
+
debug: 'muted', info: 'info', warn: 'warning', error: 'danger',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Streaming text.
|
|
38
|
+
*
|
|
39
|
+
* A log viewer follows the tail until the reader scrolls, then stops - the one
|
|
40
|
+
* behaviour that makes the difference between a log you can read and a log
|
|
41
|
+
* that yanks itself out from under you.
|
|
42
|
+
*/
|
|
43
|
+
export const LogViewer = defineComponent<LogViewerProps>('LogViewer', (props) => {
|
|
44
|
+
const {
|
|
45
|
+
lines, visibleRows: visibleRowsProp, follow: followProp, showTime = true,
|
|
46
|
+
showLevel = true, onFollowChange, ...rest
|
|
47
|
+
} = props;
|
|
48
|
+
|
|
49
|
+
const focus = useFocus({});
|
|
50
|
+
const measured = useMeasure();
|
|
51
|
+
const [offset, setOffset] = useState(0);
|
|
52
|
+
const [following, setFollowing] = useState(followProp ?? true);
|
|
53
|
+
const follow = followProp ?? following;
|
|
54
|
+
|
|
55
|
+
// Ten rows was a guess. The pane knows the answer.
|
|
56
|
+
const visibleRows = viewportRows(props, measured, visibleRowsProp ?? 10, {
|
|
57
|
+
requested: visibleRowsProp,
|
|
58
|
+
});
|
|
59
|
+
const maxOffset = Math.max(0, lines.length - visibleRows);
|
|
60
|
+
const top = follow ? maxOffset : Math.min(offset, maxOffset);
|
|
61
|
+
|
|
62
|
+
const scroll = (delta: number): void => {
|
|
63
|
+
const next = Math.max(0, Math.min(maxOffset, top + delta));
|
|
64
|
+
setOffset(next);
|
|
65
|
+
const nowFollowing = next >= maxOffset;
|
|
66
|
+
if (followProp === undefined) setFollowing(nowFollowing);
|
|
67
|
+
onFollowChange?.(nowFollowing);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
useInput(
|
|
71
|
+
(event) => {
|
|
72
|
+
if (chorded(event)) return false;
|
|
73
|
+
switch (event.name) {
|
|
74
|
+
case 'up': scroll(-1); return true;
|
|
75
|
+
case 'down': scroll(1); return true;
|
|
76
|
+
case 'pageup': scroll(-visibleRows); return true;
|
|
77
|
+
case 'pagedown': scroll(visibleRows); return true;
|
|
78
|
+
case 'home': scroll(-lines.length); return true;
|
|
79
|
+
case 'end': scroll(lines.length); return true;
|
|
80
|
+
default: return false;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{ focusId: focus.id },
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const window = lines.slice(top, top + visibleRows);
|
|
87
|
+
|
|
88
|
+
return h('box', { id: focus.id, role: 'log', direction: 'column', ...rest },
|
|
89
|
+
...window.map((line, i) =>
|
|
90
|
+
h('box', { key: top + i, direction: 'row', gap: 1 },
|
|
91
|
+
showTime && line.time ? h('text', { content: line.time, fg: 'subtle' }) : null,
|
|
92
|
+
showLevel && line.level
|
|
93
|
+
? h('text', {
|
|
94
|
+
content: line.level.toUpperCase().padEnd(5),
|
|
95
|
+
fg: TONE[LEVEL_TONE[line.level] ?? 'muted'],
|
|
96
|
+
})
|
|
97
|
+
: null,
|
|
98
|
+
line.source ? h('text', { content: line.source, fg: 'muted' }) : null,
|
|
99
|
+
h('text', { content: line.message, flex: 1, truncate: 'end' }),
|
|
100
|
+
)),
|
|
101
|
+
!follow
|
|
102
|
+
? h('text', { content: ' paused - end to follow', fg: 'warning', dim: true })
|
|
103
|
+
: null,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { BoxProps, MarkdownRow, MarkdownRun, ResolvedTheme, StyleColor } from '@textui/core';
|
|
2
|
+
import {
|
|
3
|
+
defineComponent,
|
|
4
|
+
h,
|
|
5
|
+
layoutMarkdown,
|
|
6
|
+
repeatToWidth,
|
|
7
|
+
useMeasure,
|
|
8
|
+
useMemo,
|
|
9
|
+
useTheme,
|
|
10
|
+
} from '@textui/core';
|
|
11
|
+
import { CodeViewer } from './code-viewer.js';
|
|
12
|
+
|
|
13
|
+
// ------------------------------------------------------------------ markdown
|
|
14
|
+
|
|
15
|
+
export interface MarkdownViewProps extends BoxProps {
|
|
16
|
+
/** The document. Ignored when `rows` is passed. */
|
|
17
|
+
content?: string;
|
|
18
|
+
/** Already laid out - for a viewer that windows the rows itself. */
|
|
19
|
+
rows?: MarkdownRow[];
|
|
20
|
+
/** Paint only this slice. The caller owns the scrolling when it passes one. */
|
|
21
|
+
window?: { first: number; count: number };
|
|
22
|
+
/** Collapse past this many rows, with a count of what is hidden. */
|
|
23
|
+
maxLines?: number;
|
|
24
|
+
/** Dim everything, for reasoning and other second-voice text. */
|
|
25
|
+
quiet?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Markdown, drawn into the width it was given.
|
|
30
|
+
*
|
|
31
|
+
* It fills the space it is in and does not scroll: a document viewer owns its
|
|
32
|
+
* viewport and a message in a transcript does not, so scrolling belongs to
|
|
33
|
+
* whoever holds the rows, not to the thing that paints them. Pass `content`
|
|
34
|
+
* and it lays out what it measured; pass `rows` and a `window` and it paints
|
|
35
|
+
* that slice of somebody else's layout.
|
|
36
|
+
*
|
|
37
|
+
* Inline emphasis, code and links survive, because they are meaning rather
|
|
38
|
+
* than markup wherever the text was written by an agent or a service for a
|
|
39
|
+
* person to read.
|
|
40
|
+
*/
|
|
41
|
+
export const MarkdownView = defineComponent<MarkdownViewProps>('MarkdownView', (props) => {
|
|
42
|
+
const { content = '', rows: given, window: slice, maxLines, quiet, ...rest } = props;
|
|
43
|
+
const theme = useTheme();
|
|
44
|
+
const measured = useMeasure();
|
|
45
|
+
const width = measured.width > 0 ? measured.width : 0;
|
|
46
|
+
const ruled = theme.border !== 'none';
|
|
47
|
+
|
|
48
|
+
const laid = useMemo(
|
|
49
|
+
() => given ?? layoutMarkdown(content, {
|
|
50
|
+
width,
|
|
51
|
+
bullet: theme.glyphs.bulletFilled,
|
|
52
|
+
quoteBar: theme.borderChars().left,
|
|
53
|
+
ruled,
|
|
54
|
+
}),
|
|
55
|
+
[given, content, width, theme, ruled],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const first = slice?.first ?? 0;
|
|
59
|
+
const shown = slice ? laid.slice(first, first + slice.count) : laid;
|
|
60
|
+
const capped = maxLines !== undefined ? shown.slice(0, maxLines) : shown;
|
|
61
|
+
const hidden = shown.length - capped.length;
|
|
62
|
+
|
|
63
|
+
const out: unknown[] = [];
|
|
64
|
+
for (let i = 0; i < capped.length;) {
|
|
65
|
+
const row = capped[i] as MarkdownRow;
|
|
66
|
+
const key = first + i;
|
|
67
|
+
|
|
68
|
+
if (row.kind === 'fence') {
|
|
69
|
+
// A fence the window cuts through is still one box, with only the rules
|
|
70
|
+
// that are actually on screen.
|
|
71
|
+
let end = i;
|
|
72
|
+
while (end < capped.length) {
|
|
73
|
+
const next = capped[end] as MarkdownRow;
|
|
74
|
+
if (next.kind !== 'fence' || next.fence !== row.fence) break;
|
|
75
|
+
end++;
|
|
76
|
+
}
|
|
77
|
+
const group = capped.slice(i, end) as Extract<MarkdownRow, { kind: 'fence' }>[];
|
|
78
|
+
const code = group.filter((r) => r.part === 'code').map((r) => r.text ?? '');
|
|
79
|
+
const language = group.find((r) => r.language)?.language;
|
|
80
|
+
|
|
81
|
+
if (!ruled) {
|
|
82
|
+
out.push(h('box', { key, padding: [0, 1], bg: 'surfaceAlt' }, h(CodeViewer, {
|
|
83
|
+
content: code.join('\n'),
|
|
84
|
+
lineNumbers: false, scrollbar: false, showCaret: false, disabled: true,
|
|
85
|
+
...(language ? { language } : {}),
|
|
86
|
+
})));
|
|
87
|
+
} else if (code.length === 0) {
|
|
88
|
+
// Only one edge of the box is on screen, and a box one row tall cannot
|
|
89
|
+
// say which. Drawn directly, so the seam is invisible as it scrolls.
|
|
90
|
+
out.push(h('text', {
|
|
91
|
+
key,
|
|
92
|
+
content: fenceEdge(theme, width, group.some((r) => r.part === 'open')),
|
|
93
|
+
fg: 'borderSubtle',
|
|
94
|
+
wrap: 'none',
|
|
95
|
+
}));
|
|
96
|
+
} else {
|
|
97
|
+
out.push(h('box', {
|
|
98
|
+
key,
|
|
99
|
+
border: {
|
|
100
|
+
style: theme.border,
|
|
101
|
+
color: 'borderSubtle',
|
|
102
|
+
sides: {
|
|
103
|
+
top: group.some((r) => r.part === 'open'),
|
|
104
|
+
bottom: group.some((r) => r.part === 'close'),
|
|
105
|
+
left: true,
|
|
106
|
+
right: true,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
padding: [0, 1],
|
|
110
|
+
}, h(CodeViewer, {
|
|
111
|
+
content: code.join('\n'),
|
|
112
|
+
lineNumbers: false, scrollbar: false, showCaret: false,
|
|
113
|
+
// A fence inside a rendered document is typography, not a control.
|
|
114
|
+
// Focusable, every code block in a README becomes a tab stop.
|
|
115
|
+
disabled: true,
|
|
116
|
+
...(language ? { language } : {}),
|
|
117
|
+
})));
|
|
118
|
+
}
|
|
119
|
+
i = end;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (row.kind === 'rule') {
|
|
124
|
+
out.push(h('box', { key, height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }));
|
|
125
|
+
i++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (row.kind === 'heading') {
|
|
130
|
+
out.push(h('box', { key, direction: 'row', overflow: 'hidden' },
|
|
131
|
+
...runNodes(row.runs, {
|
|
132
|
+
bold: row.level <= 2,
|
|
133
|
+
underline: row.level === 1,
|
|
134
|
+
fg: quiet ? 'muted' : row.level <= 2 ? 'text' : 'muted',
|
|
135
|
+
})));
|
|
136
|
+
i++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const style = { ...(quiet ? { fg: 'muted' as StyleColor } : row.fg ? { fg: row.fg } : {}) };
|
|
141
|
+
if (row.prefix !== undefined) {
|
|
142
|
+
out.push(h('box', { key, direction: 'row', gap: 1, overflow: 'hidden' },
|
|
143
|
+
h('text', { content: row.prefix, fg: quiet ? 'subtle' : row.prefixFg ?? 'accent' }),
|
|
144
|
+
h('box', { direction: 'row', flex: 1, overflow: 'hidden' }, ...runNodes(row.runs, style))));
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
out.push(h('box', { key, direction: 'row', overflow: 'hidden' }, ...runNodes(row.runs, style)));
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return h('box', { role: 'document', direction: 'column', ...rest },
|
|
154
|
+
...out,
|
|
155
|
+
hidden > 0
|
|
156
|
+
? h('text', { content: `${theme.glyphs.ellipsis} ${hidden} more lines`, fg: 'subtle' })
|
|
157
|
+
: null,
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/** One `text` per run, so a bold half of a sentence stays bold after wrapping. */
|
|
162
|
+
function runNodes(runs: MarkdownRun[], style: Record<string, unknown>): unknown[] {
|
|
163
|
+
return runs.map((run, i) => h('text', {
|
|
164
|
+
key: i,
|
|
165
|
+
content: run.text,
|
|
166
|
+
wrap: 'none',
|
|
167
|
+
...style,
|
|
168
|
+
...(run.bold ? { bold: true } : {}),
|
|
169
|
+
...(run.italic ? { italic: true } : {}),
|
|
170
|
+
...(run.code ? { fg: 'accent', bg: 'surfaceAlt' } : {}),
|
|
171
|
+
...(run.link ? { underline: true, fg: 'info', link: run.link } : {}),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The top or bottom edge of a fence, for when the window shows only that row. */
|
|
176
|
+
function fenceEdge(theme: ResolvedTheme, width: number, top: boolean): string {
|
|
177
|
+
const chars = theme.borderChars();
|
|
178
|
+
const [left, mid, right] = top
|
|
179
|
+
? [chars.topLeft, chars.top, chars.topRight]
|
|
180
|
+
: [chars.bottomLeft, chars.bottom, chars.bottomRight];
|
|
181
|
+
return `${left}${repeatToWidth(mid, Math.max(0, width - 2))}${right}`;
|
|
182
|
+
}
|