@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,317 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BindingPath,
|
|
3
|
+
CommandContext,
|
|
4
|
+
CommandDefinition,
|
|
5
|
+
ReactiveStore,
|
|
6
|
+
ResourceRendererDefinition,
|
|
7
|
+
TextUIApp,
|
|
8
|
+
} from '@textui/core';
|
|
9
|
+
import { useContext, useEffect, useRuntime, useState, useStoreValue } from '@textui/core';
|
|
10
|
+
import type { PanelHandle, PanelView } from './resource-panel.js';
|
|
11
|
+
import {
|
|
12
|
+
PANEL_PATH,
|
|
13
|
+
defaultRenderer,
|
|
14
|
+
kindRendererPath,
|
|
15
|
+
panelClaimPath,
|
|
16
|
+
panelPath,
|
|
17
|
+
panelRendererPath,
|
|
18
|
+
panelUriPath,
|
|
19
|
+
panelViewPath,
|
|
20
|
+
} from './resource-panel.js';
|
|
21
|
+
import { LOOSE, PanelContext } from './shared.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Panels.
|
|
25
|
+
*
|
|
26
|
+
* A resource is a file, a record, a log; a panel is a place one is being
|
|
27
|
+
* shown, and the renderer is a late-bound choice between the components
|
|
28
|
+
* registered for that kind. Those are three separate things, and every gain
|
|
29
|
+
* here comes from keeping them separate:
|
|
30
|
+
*
|
|
31
|
+
* - the same resource can be open in two panels at once, as source in one and
|
|
32
|
+
* rendered in the other, over one buffer - so an edit in either shows in
|
|
33
|
+
* both;
|
|
34
|
+
* - "open with" is one list from one registry, so a new renderer arriving
|
|
35
|
+
* from an extension is offered everywhere without anything being told;
|
|
36
|
+
* - and where a panel is looking - scroll, caret, which node was expanded -
|
|
37
|
+
* belongs to the panel rather than to whichever component happens to be
|
|
38
|
+
* drawing it, so every renderer remembers its place without implementing
|
|
39
|
+
* anything.
|
|
40
|
+
*
|
|
41
|
+
* That last one is what `usePanelState` is. A renderer asks for the state it
|
|
42
|
+
* wants, in whatever shape suits it, and gets back what this panel last had
|
|
43
|
+
* for this resource. The panel never knows what is in the record - a markdown
|
|
44
|
+
* view keeps a row, an editor keeps a caret and two offsets, a tree keeps a
|
|
45
|
+
* set of open nodes - which is precisely why it works for renderers that do
|
|
46
|
+
* not exist yet.
|
|
47
|
+
*/
|
|
48
|
+
export * from './resource-panel.js';
|
|
49
|
+
|
|
50
|
+
export function panelStatusPath(id: string): BindingPath {
|
|
51
|
+
return `${panelPath(id)}/status` as BindingPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Ask a panel to take the keyboard as soon as it has something that can. */
|
|
55
|
+
export function claimPanel(store: ReactiveStore, id: string): void {
|
|
56
|
+
store.set(panelClaimPath(id), (store.get<number>(panelClaimPath(id)) ?? 0) + 1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PanelRenderer {
|
|
60
|
+
id: string;
|
|
61
|
+
title: string;
|
|
62
|
+
/** Whether what is on screen can write the resource back. */
|
|
63
|
+
saves: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function panelRenderer(store: ReactiveStore, id: string): PanelRenderer | null {
|
|
67
|
+
return store.get<PanelRenderer>(panelRendererPath(id)) ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function activePanel(store: ReactiveStore): string | null {
|
|
71
|
+
return store.get<string>(PANEL_PATH) ?? null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function panelUri(store: ReactiveStore, id: string): string | null {
|
|
75
|
+
return store.get<string>(panelUriPath(id)) ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function panelView(store: ReactiveStore, id: string, uri: string): PanelView {
|
|
79
|
+
return store.get<PanelView>(panelViewPath(id, uri)) ?? {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function kindRenderer(store: ReactiveStore, kind: string): string | null {
|
|
83
|
+
return store.get<string>(kindRendererPath(kind)) ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function setKindRenderer(store: ReactiveStore, kind: string, renderer: string): void {
|
|
87
|
+
store.set(kindRendererPath(kind), renderer);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Remember which renderer this panel uses for this resource. */
|
|
91
|
+
export function setPanelRenderer(
|
|
92
|
+
store: ReactiveStore,
|
|
93
|
+
id: string,
|
|
94
|
+
uri: string,
|
|
95
|
+
renderer: string | null,
|
|
96
|
+
): void {
|
|
97
|
+
const view = panelView(store, id, uri);
|
|
98
|
+
const next: PanelView = { ...view };
|
|
99
|
+
if (renderer === null) delete next.renderer;
|
|
100
|
+
else next.renderer = renderer;
|
|
101
|
+
store.set(panelViewPath(id, uri), next);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The panel a renderer is drawing in, or null when it is mounted bare. */
|
|
105
|
+
export function usePanel(): PanelHandle | null {
|
|
106
|
+
return useContext(PanelContext);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function same(a: Record<string, unknown>, b: Record<string, unknown>): boolean {
|
|
110
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
111
|
+
for (const key of keys) if (!Object.is(a[key], b[key])) return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* State that belongs to this panel showing this resource.
|
|
117
|
+
*
|
|
118
|
+
* `useState`, except that it survives the renderer being swapped, the tab
|
|
119
|
+
* being switched away and back, and the component unmounting - and does not
|
|
120
|
+
* survive being carried to another panel, because where the other panel is
|
|
121
|
+
* looking is its own business.
|
|
122
|
+
*
|
|
123
|
+
* A renderer mounted outside a panel gets ordinary component state, so nothing
|
|
124
|
+
* has to check whether it is in one.
|
|
125
|
+
*/
|
|
126
|
+
export function usePanelState<T extends Record<string, unknown>>(
|
|
127
|
+
defaults: T,
|
|
128
|
+
): [T, (patch: Partial<T>) => void] {
|
|
129
|
+
const runtime = useRuntime();
|
|
130
|
+
const panel = usePanel();
|
|
131
|
+
const path = panel ? panelViewPath(panel.id, panel.uri) : LOOSE;
|
|
132
|
+
const stored = useStoreValue<PanelView>(path);
|
|
133
|
+
const [local, setLocal] = useState<Record<string, unknown>>({});
|
|
134
|
+
|
|
135
|
+
const remembered = panel ? (stored?.state ?? {}) : local;
|
|
136
|
+
const value = { ...defaults, ...remembered } as T;
|
|
137
|
+
|
|
138
|
+
const set = (patch: Partial<T>): void => {
|
|
139
|
+
if (panel) {
|
|
140
|
+
// Read the store rather than the render's snapshot: two sets in one
|
|
141
|
+
// frame - a caret move that also scrolls - would otherwise have the
|
|
142
|
+
// second overwrite the first with stale neighbours.
|
|
143
|
+
const view = runtime.store.get<PanelView>(path) ?? {};
|
|
144
|
+
const base = view.state ?? {};
|
|
145
|
+
const next = { ...base, ...patch };
|
|
146
|
+
if (same(base, next)) return;
|
|
147
|
+
runtime.store.set(path, { ...view, state: next });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
setLocal((prev) => (same(prev, { ...prev, ...patch }) ? prev : { ...prev, ...patch }));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return [value, set];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* One line about what this panel is doing, for whoever draws a status bar.
|
|
158
|
+
*
|
|
159
|
+
* A renderer knows things nothing outside it can compute - how much is
|
|
160
|
+
* selected, which hunk is under the caret, how many matches are left - and a
|
|
161
|
+
* status bar wants them without knowing which renderer is mounted. Publishing
|
|
162
|
+
* it here means the bar reads one path and every renderer, present and future,
|
|
163
|
+
* can fill it.
|
|
164
|
+
*/
|
|
165
|
+
export function usePanelStatus(text: string | null): void {
|
|
166
|
+
const runtime = useRuntime();
|
|
167
|
+
const panel = usePanel();
|
|
168
|
+
const id = panel?.id ?? null;
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (id === null) return;
|
|
172
|
+
runtime.store.set(panelStatusPath(id), text);
|
|
173
|
+
return () => { runtime.store.set(panelStatusPath(id), null); };
|
|
174
|
+
}, [id, text]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ------------------------------------------------------------------ commands
|
|
178
|
+
|
|
179
|
+
interface Choice {
|
|
180
|
+
def: ResourceRendererDefinition;
|
|
181
|
+
label: string;
|
|
182
|
+
kind: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The renderers on offer for whatever the focused panel is showing.
|
|
187
|
+
*
|
|
188
|
+
* Titles are what a person picks from, so a title that two renderers share is
|
|
189
|
+
* disambiguated by the component behind it rather than being offered twice.
|
|
190
|
+
*/
|
|
191
|
+
async function choicesFor(app: TextUIApp, uri: string | null): Promise<Choice[]> {
|
|
192
|
+
if (uri === null) return [];
|
|
193
|
+
const resource = await app.resources.stat(uri);
|
|
194
|
+
if (!resource) return [];
|
|
195
|
+
|
|
196
|
+
const renderers = app.resources.renderersFor(resource.kind);
|
|
197
|
+
const counts = new Map<string, number>();
|
|
198
|
+
for (const r of renderers) counts.set(r.title, (counts.get(r.title) ?? 0) + 1);
|
|
199
|
+
|
|
200
|
+
return renderers.map((def) => ({
|
|
201
|
+
def,
|
|
202
|
+
kind: resource.kind,
|
|
203
|
+
label: (counts.get(def.title) ?? 0) > 1 ? `${def.title} (${def.component})` : def.title,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function apply(
|
|
208
|
+
app: TextUIApp,
|
|
209
|
+
id: string | null,
|
|
210
|
+
uri: string | null,
|
|
211
|
+
choice: Choice | undefined,
|
|
212
|
+
): void {
|
|
213
|
+
if (id === null || uri === null || !choice) return;
|
|
214
|
+
setPanelRenderer(app.store, id, uri, choice.def.id);
|
|
215
|
+
// And against the kind, because a person who opens one markdown as source is
|
|
216
|
+
// usually saying something about markdown. The file's own answer still wins
|
|
217
|
+
// wherever there is one.
|
|
218
|
+
setKindRenderer(app.store, choice.kind, choice.def.id);
|
|
219
|
+
// Asking for a different view of something is asking to look at it.
|
|
220
|
+
claimPanel(app.store, id);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The panel commands every application gets.
|
|
225
|
+
*
|
|
226
|
+
* They are here rather than in each host because the list they act on comes
|
|
227
|
+
* from the registry, not from the host: an application that mounts a panel has
|
|
228
|
+
* already said everything it needs to say for "open with" to work. What a host
|
|
229
|
+
* still chooses is the keys.
|
|
230
|
+
*/
|
|
231
|
+
export function panelCommands(app: TextUIApp): CommandDefinition[] {
|
|
232
|
+
const current = (args: Record<string, unknown>, ctx: CommandContext): string | null =>
|
|
233
|
+
(args.panel as string | undefined) ?? activePanel(ctx.store);
|
|
234
|
+
|
|
235
|
+
/*
|
|
236
|
+
* Which resource to act on.
|
|
237
|
+
*
|
|
238
|
+
* The panel publishes what it is showing from an effect, so a caller that
|
|
239
|
+
* opens a file and asks to edit it in the same tick would be acting on
|
|
240
|
+
* whatever the panel had a frame ago. A caller that knows the URI says so
|
|
241
|
+
* and the choice is remembered against it, ready for the panel that is about
|
|
242
|
+
* to draw it.
|
|
243
|
+
*/
|
|
244
|
+
const targetUri = (args: Record<string, unknown>, id: string | null): string | null =>
|
|
245
|
+
(args.uri as string | undefined) ?? (id === null ? null : panelUri(app.store, id));
|
|
246
|
+
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
id: 'panel.openWith',
|
|
250
|
+
title: 'Open With…',
|
|
251
|
+
category: 'View',
|
|
252
|
+
slots: ['palette'],
|
|
253
|
+
when: PANEL_PATH,
|
|
254
|
+
args: [{
|
|
255
|
+
name: 'renderer',
|
|
256
|
+
type: 'string' as const,
|
|
257
|
+
required: true,
|
|
258
|
+
description: 'How to show this resource',
|
|
259
|
+
choices: async () => {
|
|
260
|
+
const id = activePanel(app.store);
|
|
261
|
+
return (await choicesFor(app, id === null ? null : panelUri(app.store, id)))
|
|
262
|
+
.map((c) => c.label);
|
|
263
|
+
},
|
|
264
|
+
}],
|
|
265
|
+
run: async (args: Record<string, unknown>, ctx: CommandContext) => {
|
|
266
|
+
const id = current(args, ctx);
|
|
267
|
+
const uri = targetUri(args, id);
|
|
268
|
+
const label = String(args.renderer ?? '');
|
|
269
|
+
const choices = await choicesFor(app, uri);
|
|
270
|
+
apply(app, id, uri, choices.find((c) => c.label === label || c.def.id === label));
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 'panel.nextRenderer',
|
|
275
|
+
title: 'Next View',
|
|
276
|
+
category: 'View',
|
|
277
|
+
slots: ['palette'],
|
|
278
|
+
when: PANEL_PATH,
|
|
279
|
+
run: async (args: Record<string, unknown>, ctx: CommandContext) => {
|
|
280
|
+
const id = current(args, ctx);
|
|
281
|
+
const uri = targetUri(args, id);
|
|
282
|
+
if (id === null || uri === null) return;
|
|
283
|
+
const choices = await choicesFor(app, uri);
|
|
284
|
+
if (choices.length < 2) return;
|
|
285
|
+
|
|
286
|
+
const at = panelView(app.store, id, uri).renderer
|
|
287
|
+
?? defaultRenderer(choices.map((c) => c.def))?.id;
|
|
288
|
+
const index = choices.findIndex((c) => c.def.id === at);
|
|
289
|
+
apply(app, id, uri, choices[(index + 1) % choices.length]);
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: 'panel.toggleEdit',
|
|
294
|
+
title: 'Edit / View',
|
|
295
|
+
category: 'View',
|
|
296
|
+
slots: ['palette'],
|
|
297
|
+
when: PANEL_PATH,
|
|
298
|
+
run: async (args: Record<string, unknown>, ctx: CommandContext) => {
|
|
299
|
+
const id = current(args, ctx);
|
|
300
|
+
const uri = targetUri(args, id);
|
|
301
|
+
if (id === null || uri === null) return;
|
|
302
|
+
const choices = await choicesFor(app, uri);
|
|
303
|
+
const defs = choices.map((c) => c.def);
|
|
304
|
+
const at = panelView(app.store, id, uri).renderer ?? defaultRenderer(defs)?.id;
|
|
305
|
+
const showing = defs.find((d) => d.id === at);
|
|
306
|
+
|
|
307
|
+
// Back to whatever this kind opens as, or forward to whatever writes
|
|
308
|
+
// it. Not a cycle: the footer says "edit / view", and a key that says
|
|
309
|
+
// one thing and does another is worse than a key that does less.
|
|
310
|
+
const next = showing?.saves === true
|
|
311
|
+
? defaultRenderer(defs.filter((d) => d.saves !== true))
|
|
312
|
+
: defs.find((d) => d.saves === true);
|
|
313
|
+
apply(app, id, uri, choices.find((c) => c.def.id === next?.id));
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BindingPath,
|
|
3
|
+
BoxProps,
|
|
4
|
+
ComponentDefinition,
|
|
5
|
+
ComponentNode,
|
|
6
|
+
Resource,
|
|
7
|
+
ResourceRendererDefinition,
|
|
8
|
+
} from '@textui/core';
|
|
9
|
+
import {
|
|
10
|
+
defineComponent,
|
|
11
|
+
escapeSegment,
|
|
12
|
+
h,
|
|
13
|
+
useEffect,
|
|
14
|
+
useFocusScope,
|
|
15
|
+
useRef,
|
|
16
|
+
useRuntime,
|
|
17
|
+
useStoreValue,
|
|
18
|
+
useTask,
|
|
19
|
+
} from '@textui/core';
|
|
20
|
+
import { EmptyState, ErrorState, Spinner } from '../display/index.js';
|
|
21
|
+
import { LOOSE, PanelContext } from './shared.js';
|
|
22
|
+
|
|
23
|
+
/** Every panel's remembered state. */
|
|
24
|
+
export const PANELS_ROOT = '$/ui/panels';
|
|
25
|
+
|
|
26
|
+
/** Which panel the keyboard is in. */
|
|
27
|
+
export const PANEL_PATH = '$/ui/panel' as BindingPath;
|
|
28
|
+
|
|
29
|
+
export function panelPath(id: string): BindingPath {
|
|
30
|
+
return `${PANELS_ROOT}/${escapeSegment(id)}` as BindingPath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What one panel remembers about one resource. */
|
|
34
|
+
export function panelViewPath(id: string, uri: string): BindingPath {
|
|
35
|
+
return `${panelPath(id)}/views/${escapeSegment(uri)}` as BindingPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function panelUriPath(id: string): BindingPath {
|
|
39
|
+
return `${panelPath(id)}/uri` as BindingPath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Which renderer a panel settled on, published for whoever draws chrome. */
|
|
43
|
+
export function panelRendererPath(id: string): BindingPath {
|
|
44
|
+
return `${panelPath(id)}/renderer` as BindingPath;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A pending "put the keyboard in here", counted rather than flagged.
|
|
49
|
+
*
|
|
50
|
+
* A command can ask before the panel showing that resource has even rendered -
|
|
51
|
+
* "open this file and edit it" is two calls in one tick - so the ask has to
|
|
52
|
+
* survive until there is something to focus. A counter, because asking twice
|
|
53
|
+
* in a row for the same panel is two asks.
|
|
54
|
+
*/
|
|
55
|
+
export function panelClaimPath(id: string): BindingPath {
|
|
56
|
+
return `${panelPath(id)}/claim` as BindingPath;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PanelView {
|
|
60
|
+
/** Which renderer this panel uses for this resource. */
|
|
61
|
+
renderer?: string;
|
|
62
|
+
/** Opaque, and owned by whichever renderer is showing. */
|
|
63
|
+
state?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Which renderer a *kind* opens with, once somebody has said.
|
|
68
|
+
*
|
|
69
|
+
* The per-resource memory answers "this file", and a person who opens one
|
|
70
|
+
* markdown as source is usually telling you something about markdown rather
|
|
71
|
+
* than about that file. So a choice is remembered twice: against the resource,
|
|
72
|
+
* where it wins, and against its kind, where it becomes the answer for the
|
|
73
|
+
* next file of that kind nobody has an opinion about yet.
|
|
74
|
+
*/
|
|
75
|
+
export const RENDERERS_ROOT = '$/ui/renderers';
|
|
76
|
+
|
|
77
|
+
export function kindRendererPath(kind: string): BindingPath {
|
|
78
|
+
return `${RENDERERS_ROOT}/${escapeSegment(kind)}` as BindingPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Which renderer a panel opens a resource with when it has no opinion.
|
|
83
|
+
*
|
|
84
|
+
* The highest-priority renderer that does not save. Opening a file is looking
|
|
85
|
+
* at it: an editor is something you ask for, and a kind whose only renderer
|
|
86
|
+
* writes back is the one case where asking is not required.
|
|
87
|
+
*/
|
|
88
|
+
export function defaultRenderer(
|
|
89
|
+
renderers: ResourceRendererDefinition[],
|
|
90
|
+
): ResourceRendererDefinition | null {
|
|
91
|
+
return renderers.find((r) => r.saves !== true) ?? renderers[0] ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ------------------------------------------------------------------ context
|
|
95
|
+
|
|
96
|
+
export interface PanelHandle {
|
|
97
|
+
id: string;
|
|
98
|
+
uri: string;
|
|
99
|
+
/** The renderer currently drawing, which may not be the remembered one. */
|
|
100
|
+
renderer: string | null;
|
|
101
|
+
resource: Resource;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ------------------------------------------------------------------ component
|
|
105
|
+
|
|
106
|
+
export interface ResourcePanelProps extends BoxProps {
|
|
107
|
+
/**
|
|
108
|
+
* This panel's identity, and the focus scope it makes.
|
|
109
|
+
*
|
|
110
|
+
* A panel with no id still works and still remembers - it takes the scope id
|
|
111
|
+
* the runtime generates - but that id dies with the mount, so it forgets
|
|
112
|
+
* when it is unmounted. Naming a panel is what makes it a place.
|
|
113
|
+
*/
|
|
114
|
+
id?: string;
|
|
115
|
+
uri: string | null;
|
|
116
|
+
/** Force a renderer, ignoring what the panel remembers. */
|
|
117
|
+
renderer?: string;
|
|
118
|
+
/**
|
|
119
|
+
* What the caller wants, when it has no renderer in mind: `edit` asks for
|
|
120
|
+
* one that writes back. An intent, resolved against whatever is registered -
|
|
121
|
+
* not a second way of naming a component.
|
|
122
|
+
*/
|
|
123
|
+
mode?: 'view' | 'edit';
|
|
124
|
+
/** Props for whichever renderer is chosen. The caller does not know which. */
|
|
125
|
+
rendererProps?: Record<string, unknown>;
|
|
126
|
+
/** Component to mount when nothing is registered for the kind. */
|
|
127
|
+
fallbackComponent?: string;
|
|
128
|
+
/** Claim the keyboard when nothing else holds it. */
|
|
129
|
+
autoFocus?: boolean;
|
|
130
|
+
emptyTitle?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A place a resource is shown.
|
|
135
|
+
*
|
|
136
|
+
* `ResourcePanel` rather than `Panel` only because `Panel` is already a titled
|
|
137
|
+
* box in this catalog. The vocabulary everywhere else - `usePanelState`,
|
|
138
|
+
* `panelViewPath`, `$/ui/panels` - is the short word.
|
|
139
|
+
*
|
|
140
|
+
* It resolves the resource, picks a renderer, mounts it, and owns the focus
|
|
141
|
+
* scope and the remembered state around it. What it deliberately does not own
|
|
142
|
+
* is a tab strip, a title or a border: a panel is one slot, and how a host
|
|
143
|
+
* arranges its slots is the host's business.
|
|
144
|
+
*/
|
|
145
|
+
export const ResourcePanel = defineComponent<ResourcePanelProps>('ResourcePanel', (props) => {
|
|
146
|
+
const runtime = useRuntime();
|
|
147
|
+
const {
|
|
148
|
+
id: named, uri, renderer, mode, rendererProps, fallbackComponent, autoFocus,
|
|
149
|
+
emptyTitle, ...rest
|
|
150
|
+
} = props;
|
|
151
|
+
const app = runtime.app();
|
|
152
|
+
const scope = useFocusScope({
|
|
153
|
+
...(named !== undefined ? { id: named } : {}),
|
|
154
|
+
...(autoFocus === true ? { autoFocus: true } : {}),
|
|
155
|
+
});
|
|
156
|
+
const id = scope;
|
|
157
|
+
const focused = useStoreValue<string | null>('$/focus/scope', null);
|
|
158
|
+
const active = useStoreValue<string | null>(PANEL_PATH, null);
|
|
159
|
+
|
|
160
|
+
// What this panel is showing, published for commands that act on it and for
|
|
161
|
+
// a host that would rather ask the store than thread props back up.
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
runtime.store.set(panelUriPath(id), uri);
|
|
164
|
+
return () => {
|
|
165
|
+
// An unnamed panel's id was invented for this mount, so what it
|
|
166
|
+
// remembered can never be found again - it goes with it rather than
|
|
167
|
+
// accumulating in the store one screen at a time.
|
|
168
|
+
if (named === undefined) runtime.store.set(panelPath(id), null);
|
|
169
|
+
else runtime.store.set(panelUriPath(id), null);
|
|
170
|
+
};
|
|
171
|
+
}, [id, uri]);
|
|
172
|
+
|
|
173
|
+
// The panel the keyboard is in is wherever the keyboard actually is - and
|
|
174
|
+
// the first panel to exist, before anything has been focused at all, so a
|
|
175
|
+
// command run from the palette on a fresh screen still has a target.
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (focused === scope || active == null) runtime.store.set(PANEL_PATH, id);
|
|
178
|
+
}, [focused, scope, id, active]);
|
|
179
|
+
|
|
180
|
+
// And no panel at all once the last one goes: the panel commands exist only
|
|
181
|
+
// while there is somewhere for them to act, so an application with no panels
|
|
182
|
+
// does not carry "Open With" in its palette. Whichever panel is still
|
|
183
|
+
// mounted takes the mark back on the next frame.
|
|
184
|
+
useEffect(() => () => {
|
|
185
|
+
if (runtime.store.get<string>(PANEL_PATH) === id) runtime.store.set(PANEL_PATH, null);
|
|
186
|
+
}, [id]);
|
|
187
|
+
|
|
188
|
+
const task = useTask(async () => {
|
|
189
|
+
if (!uri || !app) return null;
|
|
190
|
+
return app.resources.stat(uri);
|
|
191
|
+
}, [uri]);
|
|
192
|
+
|
|
193
|
+
useEffect(() => { void task.run(); }, [uri]);
|
|
194
|
+
|
|
195
|
+
// Subscribing here, not only in the renderer: picking "open with" writes the
|
|
196
|
+
// choice into this record, and the panel is what has to redraw.
|
|
197
|
+
const view = useStoreValue<PanelView>(uri ? panelViewPath(id, uri) : LOOSE);
|
|
198
|
+
const resourceKind = task.status === 'success' ? (task.data?.kind ?? null) : null;
|
|
199
|
+
// Subscribed, not just read: choosing how markdown opens in one panel is an
|
|
200
|
+
// answer every other panel showing markdown should pick up.
|
|
201
|
+
const forKind = useStoreValue<string>(kindRendererPath(resourceKind ?? '\u0000'));
|
|
202
|
+
|
|
203
|
+
/*
|
|
204
|
+
* What the caller named, then what the caller asked for, then what this
|
|
205
|
+
* panel last used for this resource, then what the kind opens as. A caller
|
|
206
|
+
* that says nothing gets the remembered choice, which is what makes "open
|
|
207
|
+
* this json as a tree" stick.
|
|
208
|
+
*/
|
|
209
|
+
const resource = task.status === 'success' ? task.data : null;
|
|
210
|
+
const renderers = resource && app ? app.resources.renderersFor(resource.kind) : [];
|
|
211
|
+
const asked = mode === 'edit'
|
|
212
|
+
? renderers.find((r) => r.saves === true)
|
|
213
|
+
: mode === 'view'
|
|
214
|
+
? defaultRenderer(renderers.filter((r) => r.saves !== true))
|
|
215
|
+
: undefined;
|
|
216
|
+
const chosen =
|
|
217
|
+
(renderer !== undefined ? renderers.find((r) => r.id === renderer) : undefined) ??
|
|
218
|
+
asked ??
|
|
219
|
+
renderers.find((r) => r.id === view?.renderer) ??
|
|
220
|
+
renderers.find((r) => r.id === forKind) ??
|
|
221
|
+
defaultRenderer(renderers);
|
|
222
|
+
|
|
223
|
+
// Published rather than returned, because what is on screen is a fact a
|
|
224
|
+
// status bar, a menu and a key hint all want and none of them is this
|
|
225
|
+
// panel's parent. Resolved here, since the choice is made here.
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
runtime.store.set(
|
|
228
|
+
panelRendererPath(id),
|
|
229
|
+
chosen ? { id: chosen.id, title: chosen.title, saves: chosen.saves === true } : null,
|
|
230
|
+
);
|
|
231
|
+
return () => { runtime.store.set(panelRendererPath(id), null); };
|
|
232
|
+
}, [id, chosen?.id ?? null]);
|
|
233
|
+
|
|
234
|
+
/*
|
|
235
|
+
* Swapping the renderer puts the keyboard in the panel.
|
|
236
|
+
*
|
|
237
|
+
* Asking for a different view of a thing is asking to look at it, and the
|
|
238
|
+
* swap unmounts whatever had focus - so without this the new renderer draws
|
|
239
|
+
* perfectly and reads no keys, which looks like a broken viewer rather than
|
|
240
|
+
* an unfocused one. It is handed to the renderer as `autoFocus` rather than
|
|
241
|
+
* pushed from here afterwards, because the mounting render is the first
|
|
242
|
+
* moment the thing that should have focus exists.
|
|
243
|
+
*
|
|
244
|
+
* Only on a *change*: a panel appearing on screen is not a reason to take
|
|
245
|
+
* the keyboard off whatever the person was already using.
|
|
246
|
+
*
|
|
247
|
+
* And only for the *same resource*. This used to compare renderer ids and
|
|
248
|
+
* nothing else, so walking a tree from a markdown file to a JSON one was a
|
|
249
|
+
* swap - the renderer changed because the file did - and the keyboard left
|
|
250
|
+
* the tree in the middle of browsing it. A different view of one thing is a
|
|
251
|
+
* request; a different thing is just the next thing.
|
|
252
|
+
*/
|
|
253
|
+
const previous = useRef<{ uri: string | null; renderer: string | null } | null>(null);
|
|
254
|
+
const before = previous.current;
|
|
255
|
+
const swapped = before !== null
|
|
256
|
+
&& before.uri === uri
|
|
257
|
+
&& before.renderer !== (chosen?.id ?? null);
|
|
258
|
+
if (chosen) previous.current = { uri, renderer: chosen.id };
|
|
259
|
+
|
|
260
|
+
// Or because a command asked outright, which is the same thing arriving a
|
|
261
|
+
// frame earlier: "open this and edit it" writes the choice before this panel
|
|
262
|
+
// has drawn once, so there is no swap to notice.
|
|
263
|
+
const claim = useStoreValue<number>(panelClaimPath(id), 0) ?? 0;
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (claim > 0 && chosen) runtime.store.set(panelClaimPath(id), 0);
|
|
266
|
+
}, [id, claim, chosen?.id ?? null]);
|
|
267
|
+
|
|
268
|
+
if (!uri) return h(EmptyState, { title: emptyTitle ?? 'Nothing selected', ...rest });
|
|
269
|
+
if (!app) return null;
|
|
270
|
+
if (task.status === 'running' || task.status === 'idle') {
|
|
271
|
+
return h(Spinner, { label: 'Loading…', ...rest });
|
|
272
|
+
}
|
|
273
|
+
if (task.status === 'error') return h(ErrorState, { error: task.error, ...rest });
|
|
274
|
+
if (!resource) return h(EmptyState, { title: 'Not found', message: uri, ...rest });
|
|
275
|
+
|
|
276
|
+
if (!chosen) {
|
|
277
|
+
return h('box', { direction: 'column', flex: 1, id: scope, ...rest },
|
|
278
|
+
fallbackComponent
|
|
279
|
+
? { component: fallbackComponent, resource, uri }
|
|
280
|
+
: h(EmptyState, { title: 'No renderer', message: resource.kind }));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const node: ComponentNode = {
|
|
284
|
+
component: chosen.component,
|
|
285
|
+
resource,
|
|
286
|
+
uri,
|
|
287
|
+
...(swapped || claim > 0 ? { autoFocus: true } : {}),
|
|
288
|
+
...(rendererProps ?? {}),
|
|
289
|
+
} as ComponentNode;
|
|
290
|
+
|
|
291
|
+
const handle: PanelHandle = { id, uri, renderer: chosen.id, resource };
|
|
292
|
+
|
|
293
|
+
return h('box', { direction: 'column', flex: 1, id: scope, ...rest },
|
|
294
|
+
h(PanelContext.Provider, { value: handle }, node));
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
export const PANEL_COMPONENTS: ComponentDefinition[] = [
|
|
298
|
+
{
|
|
299
|
+
component: 'ResourcePanel',
|
|
300
|
+
category: 'resource',
|
|
301
|
+
renderer: { kind: 'function', render: ResourcePanel },
|
|
302
|
+
description: 'A place a resource is shown, by whichever renderer is registered for it.',
|
|
303
|
+
},
|
|
304
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BindingPath } from '@textui/core';
|
|
2
|
+
import { createContext } from '@textui/core';
|
|
3
|
+
import type { PanelHandle } from './resource-panel.js';
|
|
4
|
+
|
|
5
|
+
export const PanelContext = createContext<PanelHandle | null>('Panel', null);
|
|
6
|
+
|
|
7
|
+
/** Where a renderer with no panel keeps state, so the hook is always safe. */
|
|
8
|
+
export const LOOSE = '$/local/panel/loose' as BindingPath;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineComponent, h, useSize, useStoreValue, useTheme } from '@textui/core';
|
|
2
|
+
import { ToastHost } from '../overlay/index.js';
|
|
3
|
+
import { SurfaceArea } from '../surface/index.js';
|
|
4
|
+
import type { ShellProps } from './shared.js';
|
|
5
|
+
import { useSurfaceMounted } from './shared.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Direction A: the dense operator console.
|
|
9
|
+
*
|
|
10
|
+
* Every region is a labelled box, chrome carries counts, nothing is wasted.
|
|
11
|
+
* Sidebar left, panel bottom, one-line status.
|
|
12
|
+
*/
|
|
13
|
+
export const ConsoleShell = defineComponent<ShellProps>('ConsoleShell', (props) => {
|
|
14
|
+
const theme = useTheme();
|
|
15
|
+
const size = useSize();
|
|
16
|
+
const sidebarCollapsed = useStoreValue<boolean>('$/ui/sidebar/collapsed', false);
|
|
17
|
+
const sidebar = useSurfaceMounted('sidebar');
|
|
18
|
+
const narrow = size.width < 80;
|
|
19
|
+
|
|
20
|
+
return h('box', {
|
|
21
|
+
direction: 'column', width: '100%', height: '100%',
|
|
22
|
+
// Ink as well as page: a themed background with the terminal's own
|
|
23
|
+
// foreground on top is how a light theme comes out white on white.
|
|
24
|
+
bg: 'canvas', fg: 'text',
|
|
25
|
+
...props,
|
|
26
|
+
},
|
|
27
|
+
h(SurfaceArea, { surface: 'header' }),
|
|
28
|
+
|
|
29
|
+
h('box', { direction: 'row', flex: 1 },
|
|
30
|
+
sidebar && !narrow && !sidebarCollapsed
|
|
31
|
+
? h('box', {
|
|
32
|
+
width: 18,
|
|
33
|
+
border: { style: theme.border, sides: { right: true } },
|
|
34
|
+
direction: 'column',
|
|
35
|
+
}, h(SurfaceArea, { surface: 'sidebar', flex: 1 }))
|
|
36
|
+
: null,
|
|
37
|
+
|
|
38
|
+
h('box', { flex: 1, direction: 'column' },
|
|
39
|
+
h(SurfaceArea, { surface: 'main', flex: 1 }),
|
|
40
|
+
h(SurfaceArea, { surface: 'panel' })),
|
|
41
|
+
|
|
42
|
+
!narrow ? h(SurfaceArea, { surface: 'aside' }) : null),
|
|
43
|
+
|
|
44
|
+
h(SurfaceArea, { surface: 'status' }),
|
|
45
|
+
h(ToastHost, { anchor: 'bottom-right' }),
|
|
46
|
+
);
|
|
47
|
+
});
|