@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,31 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useFocus, useInput, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface PaginationProps extends BoxProps {
|
|
5
|
+
page: number;
|
|
6
|
+
pageCount: number;
|
|
7
|
+
total?: number;
|
|
8
|
+
onChange?(page: number): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Pagination = defineComponent<PaginationProps>('Pagination', (props) => {
|
|
12
|
+
const theme = useTheme();
|
|
13
|
+
const { page, pageCount, total, onChange, ...rest } = props;
|
|
14
|
+
const focus = useFocus({});
|
|
15
|
+
|
|
16
|
+
useInput(
|
|
17
|
+
(event) => {
|
|
18
|
+
if (event.name === 'left' && page > 1) { onChange?.(page - 1); return true; }
|
|
19
|
+
if (event.name === 'right' && page < pageCount) { onChange?.(page + 1); return true; }
|
|
20
|
+
return false;
|
|
21
|
+
},
|
|
22
|
+
{ focusId: focus.id },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return h('box', { id: focus.id, direction: 'row', gap: 1, ...rest },
|
|
26
|
+
h('text', { content: theme.glyphs.chevronLeft, fg: page > 1 ? 'accent' : 'disabled' }),
|
|
27
|
+
h('text', { content: `${page} / ${pageCount}`, bold: focus.focused }),
|
|
28
|
+
h('text', { content: theme.glyphs.chevronRight, fg: page < pageCount ? 'accent' : 'disabled' }),
|
|
29
|
+
total !== undefined ? h('text', { content: `${total} items`, fg: 'muted' }) : null,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RenderOutput } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A one-cell scrollbar.
|
|
6
|
+
*
|
|
7
|
+
* Exported because anything with a viewport wants the same one: two components
|
|
8
|
+
* drawing their own would drift, and the thumb is the only thing on screen
|
|
9
|
+
* that says how much of a file you are looking at.
|
|
10
|
+
*/
|
|
11
|
+
export const ScrollThumb = defineComponent<{
|
|
12
|
+
total: number;
|
|
13
|
+
rows: number;
|
|
14
|
+
offset: number;
|
|
15
|
+
focused: boolean;
|
|
16
|
+
}>('ScrollThumb', ({ total, rows, offset, focused }) => {
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
// Not border glyphs: `left` and `right` are the same character in every
|
|
19
|
+
// border style, so a thumb drawn with one would be invisible against a
|
|
20
|
+
// track drawn with the other.
|
|
21
|
+
const thumbChar = theme.glyphs.progressFull;
|
|
22
|
+
const trackChar = theme.glyphs.progressEmpty;
|
|
23
|
+
const track = Math.max(1, rows);
|
|
24
|
+
const size = Math.max(1, Math.round((rows / Math.max(1, total)) * track));
|
|
25
|
+
const span = Math.max(0, track - size);
|
|
26
|
+
const position = total <= rows
|
|
27
|
+
? 0
|
|
28
|
+
: Math.round((offset / Math.max(1, total - rows)) * span);
|
|
29
|
+
|
|
30
|
+
const cells: RenderOutput[] = [];
|
|
31
|
+
for (let i = 0; i < track; i++) {
|
|
32
|
+
const onThumb = i >= position && i < position + size;
|
|
33
|
+
cells.push(h('text', {
|
|
34
|
+
key: i,
|
|
35
|
+
content: onThumb ? thumbChar : trackChar,
|
|
36
|
+
fg: onThumb ? (focused ? 'focus' : 'border') : 'borderSubtle',
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
return h('box', { width: 1, direction: 'column' }, ...cells);
|
|
40
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useEffect, useMeasure, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An entry that reports how tall it turned out to be.
|
|
6
|
+
*
|
|
7
|
+
* Through a ref rather than state, deliberately: a height arriving must not
|
|
8
|
+
* schedule a render, or every measurement would cause the next measurement.
|
|
9
|
+
*/
|
|
10
|
+
export const FeedEntry = defineComponent<BoxProps & { onHeight(height: number): void }>(
|
|
11
|
+
'FeedEntry',
|
|
12
|
+
(props) => {
|
|
13
|
+
const { onHeight, children, ...rest } = props;
|
|
14
|
+
const measured = useMeasure();
|
|
15
|
+
useEffect(() => { onHeight(measured.height); }, [measured.height]);
|
|
16
|
+
return h('box', { direction: 'column', ...rest }, children);
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const FeedScrollbar = defineComponent<Record<string, never>>('FeedScrollbar', () => {
|
|
21
|
+
const theme = useTheme();
|
|
22
|
+
return h('box', { width: 1, fill: theme.borderChars().left, fg: 'borderSubtle' });
|
|
23
|
+
});
|
package/src/data/tree.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { BoxProps, SemanticVariant } from '@textui/core';
|
|
2
|
+
import {
|
|
3
|
+
chorded,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
useFocus,
|
|
7
|
+
useInput,
|
|
8
|
+
useMeasure,
|
|
9
|
+
useTheme,
|
|
10
|
+
} from '@textui/core';
|
|
11
|
+
import { usePanelState } from '../panel/index.js';
|
|
12
|
+
import { TONE } from '../tone.js';
|
|
13
|
+
import { viewportRows } from '../viewport.js';
|
|
14
|
+
|
|
15
|
+
export interface TreeNode {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
icon?: string;
|
|
19
|
+
children?: TreeNode[];
|
|
20
|
+
/** Children exist but are not loaded yet. */
|
|
21
|
+
hasChildren?: boolean;
|
|
22
|
+
tone?: SemanticVariant;
|
|
23
|
+
meta?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TreeProps extends BoxProps {
|
|
27
|
+
nodes: TreeNode[];
|
|
28
|
+
selectedId?: string;
|
|
29
|
+
expandedIds?: string[];
|
|
30
|
+
onSelect?(id: string, node: TreeNode): void;
|
|
31
|
+
onActivate?(id: string, node: TreeNode): void;
|
|
32
|
+
onToggle?(id: string, expanded: boolean): void;
|
|
33
|
+
visibleRows?: number;
|
|
34
|
+
indent?: number;
|
|
35
|
+
/**
|
|
36
|
+
* What the expand mark looks like, when a chevron is not what it means.
|
|
37
|
+
*
|
|
38
|
+
* A file tree's twisty is not only "there is more here" - it is also the one
|
|
39
|
+
* thing on the row that says this is a folder, because a folder has no size
|
|
40
|
+
* beside it and nothing else distinguishes it. A caller that knows its rows
|
|
41
|
+
* are folders can say so; everything else gets the theme's chevrons.
|
|
42
|
+
*/
|
|
43
|
+
twistyOpen?: string;
|
|
44
|
+
twistyClosed?: string;
|
|
45
|
+
/** Claim focus on mount, if nothing in this scope already has it. */
|
|
46
|
+
autoFocus?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface FlatNode {
|
|
50
|
+
node: TreeNode;
|
|
51
|
+
depth: number;
|
|
52
|
+
expanded: boolean;
|
|
53
|
+
expandable: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function flatten(nodes: TreeNode[], expanded: Set<string>, depth = 0, out: FlatNode[] = []): FlatNode[] {
|
|
57
|
+
for (const node of nodes) {
|
|
58
|
+
const expandable = Boolean(node.hasChildren || (node.children && node.children.length > 0));
|
|
59
|
+
const isExpanded = expanded.has(node.id);
|
|
60
|
+
out.push({ node, depth, expanded: isExpanded, expandable });
|
|
61
|
+
if (isExpanded && node.children) flatten(node.children, expanded, depth + 1, out);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const Tree = defineComponent<TreeProps>('Tree', (props) => {
|
|
67
|
+
const theme = useTheme();
|
|
68
|
+
const {
|
|
69
|
+
nodes, selectedId, expandedIds, onSelect, onActivate, onToggle,
|
|
70
|
+
visibleRows, indent = 2, twistyOpen, twistyClosed, autoFocus, ...rest
|
|
71
|
+
} = props;
|
|
72
|
+
|
|
73
|
+
const focus = useFocus({ autoFocus });
|
|
74
|
+
const measured = useMeasure();
|
|
75
|
+
const treePage = viewportRows(props, measured, 10, { requested: visibleRows });
|
|
76
|
+
/*
|
|
77
|
+
* Uncontrolled expansion belongs to the panel when there is one: a JSON file
|
|
78
|
+
* opened as a structure, switched away from and switched back to, comes back
|
|
79
|
+
* open where it was rather than collapsed to the root. A tree in a sidebar
|
|
80
|
+
* is in no panel and keeps ordinary component state, which is what it had.
|
|
81
|
+
*/
|
|
82
|
+
const [treeView, setTreeView] = usePanelState<{ expanded: string[]; selected: string | null }>(
|
|
83
|
+
{ expanded: [], selected: null },
|
|
84
|
+
);
|
|
85
|
+
const internalExpanded = treeView.expanded;
|
|
86
|
+
const setInternalExpanded = (next: string[]): void => setTreeView({ expanded: next });
|
|
87
|
+
const internalSelected = treeView.selected;
|
|
88
|
+
const setInternalSelected = (next: string | null): void => setTreeView({ selected: next });
|
|
89
|
+
|
|
90
|
+
const expanded = new Set(expandedIds ?? internalExpanded);
|
|
91
|
+
const rows = flatten(nodes, expanded);
|
|
92
|
+
const currentId = selectedId ?? internalSelected ?? rows[0]?.node.id ?? null;
|
|
93
|
+
const index = Math.max(0, rows.findIndex((r) => r.node.id === currentId));
|
|
94
|
+
|
|
95
|
+
const select = (next: number): void => {
|
|
96
|
+
const row = rows[Math.max(0, Math.min(rows.length - 1, next))];
|
|
97
|
+
if (!row) return;
|
|
98
|
+
if (selectedId === undefined) setInternalSelected(row.node.id);
|
|
99
|
+
onSelect?.(row.node.id, row.node);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const toggle = (id: string, next: boolean): void => {
|
|
103
|
+
if (expandedIds === undefined) {
|
|
104
|
+
setInternalExpanded(next
|
|
105
|
+
? [...internalExpanded, id]
|
|
106
|
+
: internalExpanded.filter((e) => e !== id));
|
|
107
|
+
}
|
|
108
|
+
onToggle?.(id, next);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
useInput(
|
|
112
|
+
(event) => {
|
|
113
|
+
const row = rows[index];
|
|
114
|
+
if (!row || chorded(event)) return false;
|
|
115
|
+
switch (event.name) {
|
|
116
|
+
case 'up': select(index - 1); return true;
|
|
117
|
+
case 'down': select(index + 1); return true;
|
|
118
|
+
case 'pageup': select(index - treePage); return true;
|
|
119
|
+
case 'pagedown': select(index + treePage); return true;
|
|
120
|
+
case 'home': select(0); return true;
|
|
121
|
+
case 'end': select(rows.length - 1); return true;
|
|
122
|
+
case 'right':
|
|
123
|
+
// Right expands, and only moves inward once already expanded.
|
|
124
|
+
if (row.expandable && !row.expanded) toggle(row.node.id, true);
|
|
125
|
+
else select(index + 1);
|
|
126
|
+
return true;
|
|
127
|
+
case 'left':
|
|
128
|
+
if (row.expandable && row.expanded) toggle(row.node.id, false);
|
|
129
|
+
else {
|
|
130
|
+
const parent = rows.slice(0, index).reverse().find((r) => r.depth < row.depth);
|
|
131
|
+
if (parent) select(rows.indexOf(parent));
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
case 'enter':
|
|
135
|
+
if (row.expandable) toggle(row.node.id, !row.expanded);
|
|
136
|
+
onActivate?.(row.node.id, row.node);
|
|
137
|
+
return true;
|
|
138
|
+
default: return false;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{ focusId: focus.id },
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// As with the list: how many rows fit is a question for the layout, not for
|
|
145
|
+
// the data. A deep tree scrolls inside its pane instead of stretching it.
|
|
146
|
+
const shown = viewportRows(props, measured, rows.length, { requested: visibleRows });
|
|
147
|
+
const window = rows.length <= shown
|
|
148
|
+
? rows
|
|
149
|
+
: rows.slice(
|
|
150
|
+
Math.max(0, Math.min(index - Math.floor(shown / 2), rows.length - shown)),
|
|
151
|
+
Math.max(0, Math.min(index - Math.floor(shown / 2), rows.length - shown)) + shown,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
* One glyph column, and a second only when something needs both.
|
|
156
|
+
*
|
|
157
|
+
* A row that cannot expand has no twisty, so its icon goes in the twisty's
|
|
158
|
+
* place - otherwise a file sits one column right of a folder at the same
|
|
159
|
+
* depth and reads as being inside it. That was exactly the bug: an explorer
|
|
160
|
+
* that puts the folder glyph *in* the twisty (which is what `twistyClosed`
|
|
161
|
+
* is for) had its files pushed two columns clear of their siblings the
|
|
162
|
+
* moment they grew icons of their own.
|
|
163
|
+
*
|
|
164
|
+
* A second column appears only for a tree where something expandable also
|
|
165
|
+
* has an icon - a chevron *and* a folder mark - and then it is reserved on
|
|
166
|
+
* every row, including the ones that leave it blank, because a column that
|
|
167
|
+
* comes and go by row is the same misalignment wearing a different hat.
|
|
168
|
+
* Measured over every row rather than the visible ones, so scrolling cannot
|
|
169
|
+
* shift the layout under you.
|
|
170
|
+
*/
|
|
171
|
+
const iconColumn = rows.some((r) => r.expandable && r.node.icon !== undefined);
|
|
172
|
+
|
|
173
|
+
return h('box', { id: focus.id, role: 'tree', direction: 'column', ...rest },
|
|
174
|
+
...window.map((row) => {
|
|
175
|
+
const active = row.node.id === currentId;
|
|
176
|
+
const twisty = row.expandable
|
|
177
|
+
? (row.expanded ? (twistyOpen ?? theme.glyphs.chevronDown)
|
|
178
|
+
: (twistyClosed ?? theme.glyphs.chevronRight))
|
|
179
|
+
: (iconColumn ? ' ' : row.node.icon ?? ' ');
|
|
180
|
+
// The marker carries the node's colour only when it *is* the node's
|
|
181
|
+
// icon; a twisty is structure and takes the row's own colour.
|
|
182
|
+
const markerTone = !row.expandable && !iconColumn && row.node.tone !== undefined
|
|
183
|
+
? TONE[row.node.tone]
|
|
184
|
+
: undefined;
|
|
185
|
+
|
|
186
|
+
return h('box', {
|
|
187
|
+
key: row.node.id,
|
|
188
|
+
role: 'treeitem',
|
|
189
|
+
label: row.node.label,
|
|
190
|
+
selected: active,
|
|
191
|
+
direction: 'row',
|
|
192
|
+
gap: 1,
|
|
193
|
+
bg: active && focus.focused ? 'selected' : active ? 'active' : undefined,
|
|
194
|
+
fg: active && focus.focused ? 'inverted' : undefined,
|
|
195
|
+
onClick: () => {
|
|
196
|
+
if (selectedId === undefined) setInternalSelected(row.node.id);
|
|
197
|
+
onSelect?.(row.node.id, row.node);
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
h('text', {
|
|
201
|
+
content: ' '.repeat(row.depth * indent) + twisty,
|
|
202
|
+
...(markerTone !== undefined && !(active && focus.focused) ? { fg: markerTone } : {}),
|
|
203
|
+
}),
|
|
204
|
+
iconColumn
|
|
205
|
+
? h('text', {
|
|
206
|
+
content: row.node.icon ?? ' ',
|
|
207
|
+
...(row.node.tone !== undefined ? { fg: TONE[row.node.tone] } : {}),
|
|
208
|
+
})
|
|
209
|
+
: null,
|
|
210
|
+
h('text', { content: row.node.label, flex: 1, truncate: 'end' }),
|
|
211
|
+
row.node.meta ? h('text', { content: row.node.meta, fg: active ? undefined : 'muted' }) : null,
|
|
212
|
+
);
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { BindingPath, ReactiveStore, SemanticVariant, StyleColor } from '@textui/core';
|
|
2
|
+
import { useStoreSubtree, useRuntime, escapeSegment } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Marks on resources.
|
|
6
|
+
*
|
|
7
|
+
* A tree of files is the same tree whether or not anything has an opinion
|
|
8
|
+
* about the files in it - so "this one is modified", "this one has an error",
|
|
9
|
+
* "this one matched your search" cannot be properties of the explorer. They
|
|
10
|
+
* are contributions, and the explorer's job is to draw whatever arrived.
|
|
11
|
+
*
|
|
12
|
+
* They live in the store rather than behind a registry because a decoration is
|
|
13
|
+
* a *value that changes*, not a capability that is registered once: git
|
|
14
|
+
* refreshes, a build finishes, a search runs again. The explorer subscribes to
|
|
15
|
+
* one subtree and redraws; nothing has to be told about anything.
|
|
16
|
+
*
|
|
17
|
+
* Each contributor owns a source name and writes under it, which is what makes
|
|
18
|
+
* two contributors possible and what makes unloading one of them a single
|
|
19
|
+
* delete rather than a diff.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const DECORATIONS_ROOT = '$/ui/decorations';
|
|
23
|
+
|
|
24
|
+
export interface ResourceDecoration {
|
|
25
|
+
/** A short mark at the end of the row. Wins over whatever the row had. */
|
|
26
|
+
badge?: string;
|
|
27
|
+
/** Colours the row, so a glance is enough. */
|
|
28
|
+
tone?: SemanticVariant;
|
|
29
|
+
/** Replaces the row's icon. */
|
|
30
|
+
icon?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Where one contributor's marks live. */
|
|
34
|
+
export function decorationsPath(source: string): BindingPath {
|
|
35
|
+
return `${DECORATIONS_ROOT}/${escapeSegment(source)}` as BindingPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Publish one contributor's marks, as a whole.
|
|
40
|
+
*
|
|
41
|
+
* The whole set at once, deliberately: a decoration that is *gone* - the file
|
|
42
|
+
* was committed, the error was fixed - has to disappear, and a merge of
|
|
43
|
+
* updates leaves the old ones behind forever.
|
|
44
|
+
*/
|
|
45
|
+
export function setDecorations(
|
|
46
|
+
store: ReactiveStore,
|
|
47
|
+
source: string,
|
|
48
|
+
byUri: Record<string, ResourceDecoration>,
|
|
49
|
+
): void {
|
|
50
|
+
const escaped: Record<string, ResourceDecoration> = {};
|
|
51
|
+
for (const [uri, decoration] of Object.entries(byUri)) {
|
|
52
|
+
escaped[escapeSegment(uri)] = decoration;
|
|
53
|
+
}
|
|
54
|
+
store.set(decorationsPath(source), escaped);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function clearDecorations(store: ReactiveStore, source: string): void {
|
|
58
|
+
store.set(decorationsPath(source), null);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Every contributor's mark for one resource, merged. */
|
|
62
|
+
export function decorationFor(store: ReactiveStore, uri: string): ResourceDecoration | null {
|
|
63
|
+
const all = store.get<Record<string, Record<string, ResourceDecoration>>>(
|
|
64
|
+
DECORATIONS_ROOT as BindingPath,
|
|
65
|
+
);
|
|
66
|
+
if (!all) return null;
|
|
67
|
+
|
|
68
|
+
const key = escapeSegment(uri);
|
|
69
|
+
let merged: ResourceDecoration | null = null;
|
|
70
|
+
for (const bySource of Object.values(all)) {
|
|
71
|
+
const found = bySource?.[key];
|
|
72
|
+
if (found) merged = { ...(merged ?? {}), ...found };
|
|
73
|
+
}
|
|
74
|
+
return merged;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A reader that redraws when anything changes.
|
|
79
|
+
*
|
|
80
|
+
* One subscription for the whole tree rather than one per row: a component
|
|
81
|
+
* cannot call a hook per item without its hook count moving as the list does,
|
|
82
|
+
* and the marks arrive as one refresh anyway.
|
|
83
|
+
*/
|
|
84
|
+
export function useDecorations(): (uri: string) => ResourceDecoration | null {
|
|
85
|
+
const runtime = useRuntime();
|
|
86
|
+
useStoreSubtree(DECORATIONS_ROOT as BindingPath);
|
|
87
|
+
return (uri: string) => decorationFor(runtime.store, uri);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ------------------------------------------------------------------ lines
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Marks on lines, which is the same idea one level down.
|
|
94
|
+
*
|
|
95
|
+
* A gutter belongs to whatever is drawing the file, and what a line *means* -
|
|
96
|
+
* added since the last commit, covered by a test, carrying an error - belongs
|
|
97
|
+
* to whatever knows that. Same bargain as the tree: the editor draws a column
|
|
98
|
+
* of marks and has never heard of git.
|
|
99
|
+
*
|
|
100
|
+
* Kept per resource rather than per panel, because "this line is new" is true
|
|
101
|
+
* of the file and not of where you happen to be looking at it.
|
|
102
|
+
*/
|
|
103
|
+
export const LINE_MARKS_ROOT = '$/ui/line-marks';
|
|
104
|
+
|
|
105
|
+
export type LineMark = 'added' | 'changed' | 'removed';
|
|
106
|
+
|
|
107
|
+
/** Line numbers count from zero, like an index, because a caret does. */
|
|
108
|
+
export type LineMarks = Record<number, LineMark>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* What a marked line looks like, in one cell.
|
|
112
|
+
*
|
|
113
|
+
* ASCII, so every terminal draws one - and here rather than in whichever
|
|
114
|
+
* component happened to need it first, because two renderers drawing the same
|
|
115
|
+
* mark differently is the same file looking like two files. It lived in the
|
|
116
|
+
* editor, which is why the *viewer* drew no marks at all: you turned the
|
|
117
|
+
* setting on, stayed in view mode, and nothing happened.
|
|
118
|
+
*/
|
|
119
|
+
export const MARK_GLYPH: Record<LineMark, string> = {
|
|
120
|
+
added: '+',
|
|
121
|
+
changed: '~',
|
|
122
|
+
removed: '_',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const MARK_TONE: Record<LineMark, StyleColor> = {
|
|
126
|
+
added: 'success',
|
|
127
|
+
changed: 'warning',
|
|
128
|
+
removed: 'danger',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export function lineMarksPath(source: string, uri: string): BindingPath {
|
|
132
|
+
return `${LINE_MARKS_ROOT}/${escapeSegment(source)}/${escapeSegment(uri)}` as BindingPath;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function setLineMarks(
|
|
136
|
+
store: ReactiveStore,
|
|
137
|
+
source: string,
|
|
138
|
+
uri: string,
|
|
139
|
+
marks: LineMarks | null,
|
|
140
|
+
): void {
|
|
141
|
+
store.set(lineMarksPath(source, uri), marks);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Drop everything one contributor said, wherever it said it. */
|
|
145
|
+
export function clearLineMarks(store: ReactiveStore, source: string): void {
|
|
146
|
+
store.set(`${LINE_MARKS_ROOT}/${escapeSegment(source)}` as BindingPath, null);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function lineMarksFor(store: ReactiveStore, uri: string | null): LineMarks {
|
|
150
|
+
if (uri === null) return {};
|
|
151
|
+
const all = store.get<Record<string, Record<string, LineMarks>>>(
|
|
152
|
+
LINE_MARKS_ROOT as BindingPath,
|
|
153
|
+
);
|
|
154
|
+
if (!all) return {};
|
|
155
|
+
|
|
156
|
+
const key = escapeSegment(uri);
|
|
157
|
+
let merged: LineMarks = {};
|
|
158
|
+
for (const bySource of Object.values(all)) {
|
|
159
|
+
const found = bySource?.[key];
|
|
160
|
+
if (found) merged = { ...merged, ...found };
|
|
161
|
+
}
|
|
162
|
+
return merged;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The marks on one file, redrawn when they change.
|
|
167
|
+
*
|
|
168
|
+
* One subscription, and an empty object when nobody has said anything - so a
|
|
169
|
+
* renderer can ask unconditionally and draw no column at all when the answer
|
|
170
|
+
* is empty.
|
|
171
|
+
*/
|
|
172
|
+
export function useLineMarks(uri: string | null): LineMarks {
|
|
173
|
+
const runtime = useRuntime();
|
|
174
|
+
useStoreSubtree(LINE_MARKS_ROOT as BindingPath);
|
|
175
|
+
return lineMarksFor(runtime.store, uri);
|
|
176
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface AlertProps extends BoxProps {
|
|
5
|
+
tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
6
|
+
title?: string;
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Alert = defineComponent<AlertProps>('Alert', (props) => {
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
const { tone = 'info', title, message, children, ...rest } = props;
|
|
13
|
+
const icon = {
|
|
14
|
+
info: theme.glyphs.info,
|
|
15
|
+
success: theme.glyphs.check,
|
|
16
|
+
warning: theme.glyphs.warning,
|
|
17
|
+
danger: theme.glyphs.cross,
|
|
18
|
+
}[tone];
|
|
19
|
+
|
|
20
|
+
return h('box', { role: 'alert', direction: 'row', gap: 1, ...rest },
|
|
21
|
+
h('text', { content: icon, fg: tone }),
|
|
22
|
+
h('box', { direction: 'column', flex: 1 },
|
|
23
|
+
title ? h('text', { content: title, bold: true, fg: tone }) : null,
|
|
24
|
+
message ? h('text', { content: message, wrap: 'word' }) : null,
|
|
25
|
+
children,
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BoxProps, SemanticVariant, SurfaceVariant } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
import { ON_TONE, TONE as TONE_COLOR } from '../tone.js';
|
|
4
|
+
|
|
5
|
+
export interface BadgeProps extends BoxProps {
|
|
6
|
+
label: string;
|
|
7
|
+
tone?: SemanticVariant;
|
|
8
|
+
variant?: SurfaceVariant;
|
|
9
|
+
/** Glyph before the label, so the badge reads without colour. */
|
|
10
|
+
icon?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Badge = defineComponent<BadgeProps>('Badge', (props) => {
|
|
14
|
+
const { label, tone = 'default', variant = 'ghost', icon, ...rest } = props;
|
|
15
|
+
const color = TONE_COLOR[tone];
|
|
16
|
+
|
|
17
|
+
// `ghost` is chrome-less: the tone, and nothing around it. That is the
|
|
18
|
+
// default because it is the badge that fits in a sentence, and it is what
|
|
19
|
+
// `ghost` already means on Button - one word, one meaning, both components.
|
|
20
|
+
const style =
|
|
21
|
+
variant === 'solid' ? { bg: color, fg: ON_TONE[tone] } : { fg: color };
|
|
22
|
+
|
|
23
|
+
// A badge is inline: it sits in a row of text and must stay one line tall.
|
|
24
|
+
// The outline variant is therefore brackets rather than a box border - a
|
|
25
|
+
// three-row chip in a sentence is not an outline, it is a panel.
|
|
26
|
+
const brackets = variant === 'outline';
|
|
27
|
+
|
|
28
|
+
return h('box', {
|
|
29
|
+
direction: 'row',
|
|
30
|
+
gap: icon ? 1 : 0,
|
|
31
|
+
padding: variant === 'solid' ? [0, 1] : 0,
|
|
32
|
+
...style,
|
|
33
|
+
...rest,
|
|
34
|
+
},
|
|
35
|
+
brackets ? h('text', { content: '[' }) : null,
|
|
36
|
+
icon ? h('text', { content: icon }) : null,
|
|
37
|
+
h('text', { content: label }),
|
|
38
|
+
brackets ? h('text', { content: ']' }) : null,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface CardProps extends BoxProps {
|
|
5
|
+
title?: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
footer?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Card = defineComponent<CardProps>('Card', (props) => {
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
const { title, subtitle, children, ...rest } = props;
|
|
13
|
+
return h('box', {
|
|
14
|
+
border: theme.border,
|
|
15
|
+
padding: theme.density === 'compact' ? 0 : [0, 1],
|
|
16
|
+
direction: 'column',
|
|
17
|
+
title,
|
|
18
|
+
...rest,
|
|
19
|
+
},
|
|
20
|
+
subtitle ? h('text', { content: subtitle, fg: 'muted' }) : null,
|
|
21
|
+
children,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface EmptyStateProps extends BoxProps {
|
|
5
|
+
title: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
/** Hint text: what the reader can do about it. */
|
|
9
|
+
hint?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const EmptyState = defineComponent<EmptyStateProps>('EmptyState', (props) => {
|
|
13
|
+
const { title, message, icon, hint, children, ...rest } = props;
|
|
14
|
+
return h('box', { direction: 'column', align: 'center', justify: 'center', flex: 1, gap: 0, ...rest },
|
|
15
|
+
icon ? h('text', { content: icon, fg: 'subtle' }) : null,
|
|
16
|
+
h('text', { content: title, bold: true, fg: 'muted' }),
|
|
17
|
+
message ? h('text', { content: message, fg: 'subtle', wrap: 'word', textAlign: 'center' }) : null,
|
|
18
|
+
hint ? h('text', { content: hint, fg: 'subtle', dim: true }) : null,
|
|
19
|
+
children,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface ErrorStateProps extends BoxProps {
|
|
5
|
+
title?: string;
|
|
6
|
+
error: unknown;
|
|
7
|
+
/** Command id offered as a retry. */
|
|
8
|
+
onRetry?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ErrorState = defineComponent<ErrorStateProps>('ErrorState', (props) => {
|
|
12
|
+
const theme = useTheme();
|
|
13
|
+
const { title = 'Something went wrong', error, onRetry, ...rest } = props;
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
|
|
16
|
+
return h('box', { role: 'alert', direction: 'column', gap: 1, padding: 1, ...rest },
|
|
17
|
+
h('box', { direction: 'row', gap: 1 },
|
|
18
|
+
h('text', { content: theme.glyphs.cross, fg: 'danger' }),
|
|
19
|
+
h('text', { content: title, bold: true, fg: 'danger' })),
|
|
20
|
+
h('text', { content: message, fg: 'muted', wrap: 'word' }),
|
|
21
|
+
onRetry ? h('text', { content: 'r retry', fg: 'subtle' }) : null,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TextProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export interface HeadingProps extends TextProps {
|
|
6
|
+
level?: 1 | 2 | 3;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Heading = defineComponent<HeadingProps>('Heading', ({ level = 1, ...props }) =>
|
|
10
|
+
h('text', {
|
|
11
|
+
role: 'heading',
|
|
12
|
+
bold: level <= 2,
|
|
13
|
+
dim: level === 3,
|
|
14
|
+
fg: level === 1 ? 'text' : level === 2 ? 'text' : 'muted',
|
|
15
|
+
...props,
|
|
16
|
+
}),
|
|
17
|
+
);
|