@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,35 @@
|
|
|
1
|
+
import type { BoxProps, SemanticVariant } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
import { Button } from '../control/index.js';
|
|
4
|
+
import { useFormContext } from './shared.js';
|
|
5
|
+
|
|
6
|
+
export interface FormActionsProps extends BoxProps {
|
|
7
|
+
submitLabel?: string;
|
|
8
|
+
cancelLabel?: string;
|
|
9
|
+
onCancel?(): void;
|
|
10
|
+
tone?: SemanticVariant;
|
|
11
|
+
/** Disable submit until something changed. */
|
|
12
|
+
requireDirty?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const FormActions = defineComponent<FormActionsProps>('FormActions', (props) => {
|
|
16
|
+
const theme = useTheme();
|
|
17
|
+
const form = useFormContext();
|
|
18
|
+
const {
|
|
19
|
+
submitLabel = 'Submit', cancelLabel = 'Cancel', onCancel,
|
|
20
|
+
tone = 'primary', requireDirty = false, ...rest
|
|
21
|
+
} = props;
|
|
22
|
+
|
|
23
|
+
const blocked = form.submitting || (requireDirty && !form.dirty);
|
|
24
|
+
|
|
25
|
+
return h('box', { direction: 'row', gap: 1, justify: 'end', ...rest },
|
|
26
|
+
onCancel ? h(Button, { label: cancelLabel, onPress: onCancel }) : null,
|
|
27
|
+
h(Button, {
|
|
28
|
+
label: form.submitting ? `${submitLabel}${theme.glyphs.ellipsis}` : submitLabel,
|
|
29
|
+
tone,
|
|
30
|
+
variant: 'solid',
|
|
31
|
+
disabled: blocked,
|
|
32
|
+
onPress: () => void form.submit(),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface FormSectionProps extends BoxProps {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const FormSection = defineComponent<FormSectionProps>('FormSection', (props) => {
|
|
10
|
+
const { title, description, children, ...rest } = props;
|
|
11
|
+
return h('box', { direction: 'column', ...rest },
|
|
12
|
+
h('text', { content: title, bold: true }),
|
|
13
|
+
description ? h('text', { content: description, fg: 'muted', wrap: 'word' }) : null,
|
|
14
|
+
h('box', { direction: 'column' }, children),
|
|
15
|
+
);
|
|
16
|
+
});
|
package/src/form/form.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
import type { FormApi } from './shared.js';
|
|
4
|
+
import { FormContext } from './shared.js';
|
|
5
|
+
|
|
6
|
+
export interface FormProps extends BoxProps {
|
|
7
|
+
form: FormApi<Record<string, unknown>>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Form = defineComponent<FormProps>('Form', ({ form, children, ...rest }) =>
|
|
11
|
+
h(FormContext.Provider, { value: form },
|
|
12
|
+
h('box', { role: 'form', direction: 'column', ...rest }, children)),
|
|
13
|
+
);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ComponentDefinition } from '@textui/core';
|
|
2
|
+
import { useCallback, useMemo, useState } from '@textui/core';
|
|
3
|
+
import { DangerZone } from './danger-zone.js';
|
|
4
|
+
import { Field } from './field.js';
|
|
5
|
+
import { FormActions } from './form-actions.js';
|
|
6
|
+
import { FormSection } from './form-section.js';
|
|
7
|
+
import { Form } from './form.js';
|
|
8
|
+
import type { FormApi, FormErrors } from './shared.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Forms and validation.
|
|
12
|
+
*
|
|
13
|
+
* Validation runs on a whole values object rather than per field, because the
|
|
14
|
+
* rules people actually need are cross-field - "confirm must match password",
|
|
15
|
+
* "end must be after start" - and a per-field validator cannot see its
|
|
16
|
+
* siblings. Fields subscribe to the errors that name them.
|
|
17
|
+
*/
|
|
18
|
+
export * from './danger-zone.js';
|
|
19
|
+
export * from './field.js';
|
|
20
|
+
export * from './form.js';
|
|
21
|
+
export * from './form-actions.js';
|
|
22
|
+
export * from './form-section.js';
|
|
23
|
+
export type { FormApi, FormErrors, FormState } from './shared.js';
|
|
24
|
+
export { useFormContext } from './shared.js';
|
|
25
|
+
|
|
26
|
+
export type Validator<T> = (values: T) => FormErrors<T> | null;
|
|
27
|
+
|
|
28
|
+
export interface UseFormOptions<T extends Record<string, unknown>> {
|
|
29
|
+
initialValues: T;
|
|
30
|
+
validate?: Validator<T>;
|
|
31
|
+
onSubmit?(values: T): void | Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useForm<T extends Record<string, unknown>>(
|
|
35
|
+
options: UseFormOptions<T>,
|
|
36
|
+
): FormApi<T> {
|
|
37
|
+
const [values, setValuesState] = useState<T>(options.initialValues);
|
|
38
|
+
// Validate the initial values immediately: a form whose required fields are
|
|
39
|
+
// empty is not valid, and reporting it as valid until the first keystroke
|
|
40
|
+
// makes `form.valid` useless for enabling a submit button.
|
|
41
|
+
const [errors, setErrors] = useState<FormErrors<T>>(
|
|
42
|
+
() => options.validate?.(options.initialValues) ?? {},
|
|
43
|
+
);
|
|
44
|
+
const [touched, setTouched] = useState<Partial<Record<keyof T & string, boolean>>>({});
|
|
45
|
+
const [submitting, setSubmitting] = useState(false);
|
|
46
|
+
const [submitted, setSubmitted] = useState(false);
|
|
47
|
+
const [initial] = useState(options.initialValues);
|
|
48
|
+
|
|
49
|
+
const runValidation = useCallback(
|
|
50
|
+
(next: T): FormErrors<T> => options.validate?.(next) ?? {},
|
|
51
|
+
[options.validate],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const setValue = <K extends keyof T & string>(field: K, value: T[K]): void => {
|
|
55
|
+
const next = { ...values, [field]: value } as T;
|
|
56
|
+
setValuesState(next);
|
|
57
|
+
// Re-validate as they type, but only surface what they have touched.
|
|
58
|
+
setErrors(runValidation(next));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const dirty = useMemo(
|
|
62
|
+
() => JSON.stringify(values) !== JSON.stringify(initial),
|
|
63
|
+
[values, initial],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const api: FormApi<T> = {
|
|
67
|
+
values,
|
|
68
|
+
errors,
|
|
69
|
+
touched,
|
|
70
|
+
submitting,
|
|
71
|
+
submitted,
|
|
72
|
+
valid: Object.keys(errors).length === 0,
|
|
73
|
+
dirty,
|
|
74
|
+
|
|
75
|
+
setValue,
|
|
76
|
+
setValues(patch) {
|
|
77
|
+
const next = { ...values, ...patch } as T;
|
|
78
|
+
setValuesState(next);
|
|
79
|
+
setErrors(runValidation(next));
|
|
80
|
+
},
|
|
81
|
+
setError(field, message) {
|
|
82
|
+
setErrors({ ...errors, [field]: message } as FormErrors<T>);
|
|
83
|
+
},
|
|
84
|
+
touch(field) {
|
|
85
|
+
if (touched[field]) return;
|
|
86
|
+
setTouched({ ...touched, [field]: true });
|
|
87
|
+
},
|
|
88
|
+
reset(next) {
|
|
89
|
+
setValuesState(next ?? initial);
|
|
90
|
+
setErrors({});
|
|
91
|
+
setTouched({});
|
|
92
|
+
setSubmitted(false);
|
|
93
|
+
},
|
|
94
|
+
validate() {
|
|
95
|
+
const found = runValidation(values);
|
|
96
|
+
setErrors(found);
|
|
97
|
+
return Object.keys(found).length === 0;
|
|
98
|
+
},
|
|
99
|
+
async submit() {
|
|
100
|
+
const found = runValidation(values);
|
|
101
|
+
setErrors(found);
|
|
102
|
+
// Submitting reveals every error, touched or not.
|
|
103
|
+
setTouched(
|
|
104
|
+
Object.keys(values).reduce(
|
|
105
|
+
(acc, key) => ({ ...acc, [key]: true }),
|
|
106
|
+
{} as Partial<Record<keyof T & string, boolean>>,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
if (Object.keys(found).length > 0) return;
|
|
110
|
+
|
|
111
|
+
setSubmitting(true);
|
|
112
|
+
try {
|
|
113
|
+
await options.onSubmit?.(values);
|
|
114
|
+
setSubmitted(true);
|
|
115
|
+
} finally {
|
|
116
|
+
setSubmitting(false);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
errorFor(field) {
|
|
120
|
+
return touched[field] || submitted ? errors[field] : undefined;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return api;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ------------------------------------------------------------- validators
|
|
128
|
+
|
|
129
|
+
/** Small composable validators, because every form needs the same four. */
|
|
130
|
+
export const validators = {
|
|
131
|
+
required: (message = 'Required') =>
|
|
132
|
+
(value: unknown): string | undefined =>
|
|
133
|
+
value === undefined || value === null || value === '' ? message : undefined,
|
|
134
|
+
|
|
135
|
+
minLength: (n: number, message?: string) =>
|
|
136
|
+
(value: unknown): string | undefined =>
|
|
137
|
+
typeof value === 'string' && value.length < n
|
|
138
|
+
? message ?? `Must be at least ${n} characters`
|
|
139
|
+
: undefined,
|
|
140
|
+
|
|
141
|
+
maxLength: (n: number, message?: string) =>
|
|
142
|
+
(value: unknown): string | undefined =>
|
|
143
|
+
typeof value === 'string' && value.length > n
|
|
144
|
+
? message ?? `Must be at most ${n} characters`
|
|
145
|
+
: undefined,
|
|
146
|
+
|
|
147
|
+
pattern: (re: RegExp, message = 'Invalid format') =>
|
|
148
|
+
(value: unknown): string | undefined =>
|
|
149
|
+
typeof value === 'string' && value !== '' && !re.test(value) ? message : undefined,
|
|
150
|
+
|
|
151
|
+
range: (min: number, max: number, message?: string) =>
|
|
152
|
+
(value: unknown): string | undefined => {
|
|
153
|
+
const n = Number(value);
|
|
154
|
+
return Number.isNaN(n) || n < min || n > max
|
|
155
|
+
? message ?? `Must be between ${min} and ${max}`
|
|
156
|
+
: undefined;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build a whole-form validator from per-field rules.
|
|
162
|
+
*
|
|
163
|
+
* Returns the error map rather than `FormErrors | null`, so it composes: a
|
|
164
|
+
* caller can take the result, add a cross-field rule, and hand the whole thing
|
|
165
|
+
* to `useForm` without a null check in between.
|
|
166
|
+
*/
|
|
167
|
+
export function fieldValidators<T extends Record<string, unknown>>(
|
|
168
|
+
rules: Partial<Record<keyof T & string, ((value: unknown) => string | undefined)[]>>,
|
|
169
|
+
): (values: T) => FormErrors<T> {
|
|
170
|
+
return (values) => {
|
|
171
|
+
const errors: FormErrors<T> = {};
|
|
172
|
+
for (const [field, checks] of Object.entries(rules) as [keyof T & string, ((value: unknown) => string | undefined)[]][]) {
|
|
173
|
+
for (const check of checks ?? []) {
|
|
174
|
+
const message = check(values[field]);
|
|
175
|
+
if (message) {
|
|
176
|
+
errors[field] = message;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return errors;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const FORM_COMPONENTS: ComponentDefinition[] = [
|
|
186
|
+
{ component: 'Form', category: 'form', renderer: { kind: 'function', render: Form }, role: 'form', description: 'Provides form state to its fields.' },
|
|
187
|
+
{ component: 'Field', category: 'form', renderer: { kind: 'function', render: Field }, description: 'Label, control and error, aligned.' },
|
|
188
|
+
{ component: 'FormSection', category: 'form', renderer: { kind: 'function', render: FormSection }, description: 'A titled group of fields.' },
|
|
189
|
+
{ component: 'FormActions', category: 'form', renderer: { kind: 'function', render: FormActions }, description: 'Submit and cancel.' },
|
|
190
|
+
{ component: 'DangerZone', category: 'form', renderer: { kind: 'function', render: DangerZone }, description: 'Destructive action, fenced off.' },
|
|
191
|
+
];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createContext, useContext } from '@textui/core';
|
|
2
|
+
|
|
3
|
+
export type FormErrors<T> = Partial<Record<keyof T & string, string>>;
|
|
4
|
+
|
|
5
|
+
export interface FormState<T extends Record<string, unknown>> {
|
|
6
|
+
values: T;
|
|
7
|
+
errors: FormErrors<T>;
|
|
8
|
+
/** Fields the user has interacted with. Errors show only for these. */
|
|
9
|
+
touched: Partial<Record<keyof T & string, boolean>>;
|
|
10
|
+
submitting: boolean;
|
|
11
|
+
submitted: boolean;
|
|
12
|
+
valid: boolean;
|
|
13
|
+
dirty: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FormApi<T extends Record<string, unknown>> extends FormState<T> {
|
|
17
|
+
setValue<K extends keyof T & string>(field: K, value: T[K]): void;
|
|
18
|
+
setValues(values: Partial<T>): void;
|
|
19
|
+
setError(field: keyof T & string, message: string | undefined): void;
|
|
20
|
+
touch(field: keyof T & string): void;
|
|
21
|
+
reset(values?: T): void;
|
|
22
|
+
validate(): boolean;
|
|
23
|
+
submit(): Promise<void>;
|
|
24
|
+
/** The error to show for a field: only after it has been touched. */
|
|
25
|
+
errorFor(field: keyof T & string): string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const FormContext = createContext<FormApi<Record<string, unknown>> | null>('Form', null);
|
|
29
|
+
|
|
30
|
+
export function useFormContext<T extends Record<string, unknown>>(): FormApi<T> {
|
|
31
|
+
const form = useContext(FormContext);
|
|
32
|
+
if (!form) {
|
|
33
|
+
throw new Error('[textui] a Field must be rendered inside a Form');
|
|
34
|
+
}
|
|
35
|
+
return form as unknown as FormApi<T>;
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ComponentDefinition, TextUIApp, Disposable } from '@textui/core';
|
|
2
|
+
import { createBag, PRIMITIVES } from '@textui/core';
|
|
3
|
+
import { LAYOUT_COMPONENTS } from './layout/index.js';
|
|
4
|
+
import { DISPLAY_COMPONENTS } from './display/index.js';
|
|
5
|
+
import { CONTROL_COMPONENTS } from './control/index.js';
|
|
6
|
+
import { FORM_COMPONENTS } from './form/index.js';
|
|
7
|
+
import { DATA_COMPONENTS } from './data/index.js';
|
|
8
|
+
import { NAVIGATION_COMPONENTS } from './navigation/index.js';
|
|
9
|
+
import { OVERLAY_COMPONENTS } from './overlay/index.js';
|
|
10
|
+
import { CHART_COMPONENTS } from './chart/index.js';
|
|
11
|
+
import { SURFACE_COMPONENTS, BUILTIN_LAYOUTS } from './surface/index.js';
|
|
12
|
+
import { SHELL_COMPONENTS, BUILTIN_SHELLS } from './shells/index.js';
|
|
13
|
+
import { PANEL_COMPONENTS, panelCommands } from './panel/index.js';
|
|
14
|
+
|
|
15
|
+
export * from './layout/index.js';
|
|
16
|
+
export * from './display/index.js';
|
|
17
|
+
export * from './control/index.js';
|
|
18
|
+
export * from './form/index.js';
|
|
19
|
+
export * from './data/index.js';
|
|
20
|
+
export * from './navigation/index.js';
|
|
21
|
+
export * from './overlay/index.js';
|
|
22
|
+
export * from './chart/index.js';
|
|
23
|
+
export * from './surface/index.js';
|
|
24
|
+
export * from './shells/index.js';
|
|
25
|
+
export * from './panel/index.js';
|
|
26
|
+
export * from './decorations.js';
|
|
27
|
+
export * from './find.js';
|
|
28
|
+
export * from './viewport.js';
|
|
29
|
+
export * from './tone.js';
|
|
30
|
+
|
|
31
|
+
/** Every component the library ships, in one list. */
|
|
32
|
+
export const CATALOG: ComponentDefinition[] = [
|
|
33
|
+
...PRIMITIVES,
|
|
34
|
+
...LAYOUT_COMPONENTS,
|
|
35
|
+
...DISPLAY_COMPONENTS,
|
|
36
|
+
...CONTROL_COMPONENTS,
|
|
37
|
+
...FORM_COMPONENTS,
|
|
38
|
+
...DATA_COMPONENTS,
|
|
39
|
+
...NAVIGATION_COMPONENTS,
|
|
40
|
+
...OVERLAY_COMPONENTS,
|
|
41
|
+
...CHART_COMPONENTS,
|
|
42
|
+
...SURFACE_COMPONENTS,
|
|
43
|
+
...SHELL_COMPONENTS,
|
|
44
|
+
...PANEL_COMPONENTS,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register the catalog, the surface layouts and the built-in shells.
|
|
49
|
+
*
|
|
50
|
+
* One call, because a half-registered catalog fails at mount time rather than
|
|
51
|
+
* at boot - and a runtime miss is much harder to read than a boot error.
|
|
52
|
+
*/
|
|
53
|
+
export function registerBuiltins(app: TextUIApp): Disposable {
|
|
54
|
+
const bag = createBag();
|
|
55
|
+
bag.add(app.components.registerMany(CATALOG));
|
|
56
|
+
for (const layout of BUILTIN_LAYOUTS) bag.add(app.layouts.register(layout));
|
|
57
|
+
for (const shell of BUILTIN_SHELLS) bag.add(app.shells.register(shell));
|
|
58
|
+
// The panel commands come with the panel: what they offer is read off the
|
|
59
|
+
// resource registry, so a host that mounts a panel has already said
|
|
60
|
+
// everything "open with" needs. Keys stay the host's choice.
|
|
61
|
+
for (const command of panelCommands(app)) bag.add(app.commands.register(command));
|
|
62
|
+
return bag;
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface CenterProps extends BoxProps {
|
|
5
|
+
/** Centre horizontally, vertically, or both. */
|
|
6
|
+
axis?: 'both' | 'horizontal' | 'vertical';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Center = defineComponent<CenterProps>('Center', ({ axis = 'both', ...props }) =>
|
|
10
|
+
h('box', {
|
|
11
|
+
flex: props.flex ?? 1,
|
|
12
|
+
direction: 'column',
|
|
13
|
+
justify: axis === 'horizontal' ? 'start' : 'center',
|
|
14
|
+
align: axis === 'vertical' ? 'stretch' : 'center',
|
|
15
|
+
...props,
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface DividerProps extends Omit<BoxProps, 'direction'> {
|
|
5
|
+
/** A divider runs across the flow, so it names its own axis. */
|
|
6
|
+
direction?: 'horizontal' | 'vertical';
|
|
7
|
+
/** Text set into the rule. */
|
|
8
|
+
label?: string;
|
|
9
|
+
labelAlign?: 'left' | 'center' | 'right';
|
|
10
|
+
char?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Divider = defineComponent<DividerProps>('Divider', (props) => {
|
|
14
|
+
const theme = useTheme();
|
|
15
|
+
const { direction = 'horizontal', label, labelAlign = 'left', char, ...rest } = props;
|
|
16
|
+
const chars = theme.borderChars();
|
|
17
|
+
|
|
18
|
+
if (direction === 'vertical') {
|
|
19
|
+
return h('box', { role: 'separator', width: 1, fill: char ?? chars.left, fg: 'border', ...rest });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!label) {
|
|
23
|
+
return h('box', { role: 'separator', height: 1, fill: char ?? chars.top, fg: 'border', ...rest });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return h('box', { direction: 'row', gap: 1, height: 1, ...rest },
|
|
27
|
+
labelAlign !== 'left' ? h('box', { flex: 1, fill: char ?? chars.top, fg: 'border' }) : null,
|
|
28
|
+
h('text', { content: label, fg: 'muted' }),
|
|
29
|
+
labelAlign !== 'right' ? h('box', { flex: 1, fill: char ?? chars.top, fg: 'border' }) : null,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface GridProps extends BoxProps {
|
|
5
|
+
columns: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A grid is rows of equal-flex cells. Terminals have no sub-cell measurement,
|
|
10
|
+
* so an even split is the honest primitive; anything else is a Row with widths.
|
|
11
|
+
*/
|
|
12
|
+
export const Grid = defineComponent<GridProps>('Grid', ({ columns, columnGap, rowGap, children, ...props }) => {
|
|
13
|
+
const items = Array.isArray(children) ? children : children === undefined ? [] : [children];
|
|
14
|
+
const rows: unknown[][] = [];
|
|
15
|
+
for (let i = 0; i < items.length; i += columns) rows.push(items.slice(i, i + columns));
|
|
16
|
+
|
|
17
|
+
return h('box', { direction: 'column', gap: rowGap ?? props.gap ?? 0, ...props },
|
|
18
|
+
...rows.map((row, i) =>
|
|
19
|
+
h('box', { key: i, direction: 'row', gap: columnGap ?? props.gap ?? 1 },
|
|
20
|
+
// `basis: 0` is what makes the columns *equal* rather than merely
|
|
21
|
+
// flexible. Without it a cell starts at the width its content asked
|
|
22
|
+
// for and only the space left over is shared - so a pane holding one
|
|
23
|
+
// long unwrapped line takes two thirds of the row and the one beside
|
|
24
|
+
// it is squeezed to a column of single words. Equal columns is what
|
|
25
|
+
// this component says it is for; anything that wants to be sized by
|
|
26
|
+
// its content wants a `Row`.
|
|
27
|
+
...row.map((cell, j) => h('box', { key: j, flex: 1, basis: 0, minWidth: 0 }, cell)),
|
|
28
|
+
// Pad the last row so its cells keep the same width as the others.
|
|
29
|
+
...Array.from({ length: columns - row.length }, (_, k) =>
|
|
30
|
+
h('box', { key: `pad${k}`, flex: 1, basis: 0 })),
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ComponentDefinition } from '@textui/core';
|
|
2
|
+
import { Center } from './center.js';
|
|
3
|
+
import { Column } from './column.js';
|
|
4
|
+
import { Divider } from './divider.js';
|
|
5
|
+
import { Grid } from './grid.js';
|
|
6
|
+
import { Panel } from './panel.js';
|
|
7
|
+
import { Row } from './row.js';
|
|
8
|
+
import { ScrollView } from './scroll-view.js';
|
|
9
|
+
import { Splitter } from './splitter.js';
|
|
10
|
+
import { Stack } from './stack.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Layout and containers.
|
|
14
|
+
*
|
|
15
|
+
* Every one of these is `box` with an opinion. That is the point: the layout
|
|
16
|
+
* engine only ever sees the primitive, so a new container costs a function
|
|
17
|
+
* rather than a new case in the engine.
|
|
18
|
+
*/
|
|
19
|
+
export * from './center.js';
|
|
20
|
+
export * from './column.js';
|
|
21
|
+
export * from './divider.js';
|
|
22
|
+
export * from './grid.js';
|
|
23
|
+
export * from './panel.js';
|
|
24
|
+
export * from './row.js';
|
|
25
|
+
export * from './scroll-view.js';
|
|
26
|
+
export * from './splitter.js';
|
|
27
|
+
export * from './stack.js';
|
|
28
|
+
|
|
29
|
+
export const LAYOUT_COMPONENTS: ComponentDefinition[] = [
|
|
30
|
+
{ component: 'Row', category: 'layout', renderer: { kind: 'function', render: Row }, description: 'Horizontal flex container.' },
|
|
31
|
+
{ component: 'Column', category: 'layout', renderer: { kind: 'function', render: Column }, description: 'Vertical flex container.' },
|
|
32
|
+
{ component: 'Center', category: 'layout', renderer: { kind: 'function', render: Center }, description: 'Centres its children.' },
|
|
33
|
+
{ component: 'Grid', category: 'layout', renderer: { kind: 'function', render: Grid }, description: 'Equal-width columns, wrapping into rows.' },
|
|
34
|
+
{ component: 'Panel', category: 'layout', renderer: { kind: 'function', render: Panel }, description: 'Titled region. Bordered or airy, following the theme.', variants: ['bordered', 'plain'], role: 'region' },
|
|
35
|
+
{ component: 'Divider', category: 'layout', renderer: { kind: 'function', render: Divider }, description: 'A rule, optionally labelled.', role: 'separator' },
|
|
36
|
+
{ component: 'Stack', category: 'layout', renderer: { kind: 'function', render: Stack }, description: 'Column with themed spacing.' },
|
|
37
|
+
{ component: 'ScrollView', category: 'layout', renderer: { kind: 'function', render: ScrollView }, description: 'Scrolling viewport with keyboard and wheel support.' },
|
|
38
|
+
{ component: 'Splitter', category: 'layout', renderer: { kind: 'function', render: Splitter }, description: 'Two panes with a divider.' },
|
|
39
|
+
];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { BorderSpec, BoxProps, StyleColor } from '@textui/core';
|
|
2
|
+
import { defineComponent, h, resolveEdges, useTheme } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface PanelProps extends BoxProps {
|
|
5
|
+
/** Section heading drawn into the border, or above a borderless panel. */
|
|
6
|
+
title?: string;
|
|
7
|
+
subtitle?: string;
|
|
8
|
+
/** Accent colour for the title. */
|
|
9
|
+
tone?: StyleColor;
|
|
10
|
+
/** Overrides the theme's default border. `'none'` gives an airy panel. */
|
|
11
|
+
border?: BorderSpec;
|
|
12
|
+
/**
|
|
13
|
+
* Right-aligned text beside the title, on the same row.
|
|
14
|
+
*
|
|
15
|
+
* The short thing that belongs next to a heading rather than under it: a
|
|
16
|
+
* count, a state, the shortcut that opens it. Optional, and most panels do
|
|
17
|
+
* not have one.
|
|
18
|
+
*/
|
|
19
|
+
rightTitle?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Right-aligned text on the *bottom* rule, where there is one.
|
|
22
|
+
*
|
|
23
|
+
* Not the title row - which is what this said for a long time while doing
|
|
24
|
+
* something else. A bordered panel put it in the footer and a borderless one
|
|
25
|
+
* put it beside the title, so the same prop meant two places depending on a
|
|
26
|
+
* different prop. `rightTitle` is the title row, in both; this is the
|
|
27
|
+
* bottom, and on a borderless panel there is no bottom to put it on.
|
|
28
|
+
*/
|
|
29
|
+
meta?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The workhorse container.
|
|
34
|
+
*
|
|
35
|
+
* A panel is the one component that has to look right in all three house
|
|
36
|
+
* styles, so it renders its title into the border when it has one and as a
|
|
37
|
+
* heading row when it does not - rather than forcing a border to have a title.
|
|
38
|
+
*/
|
|
39
|
+
export const Panel = defineComponent<PanelProps>('Panel', (props) => {
|
|
40
|
+
const theme = useTheme();
|
|
41
|
+
const { title, subtitle, tone, meta, rightTitle, children, padding, ...rest } = props;
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* Padding is about the *body*, and a subtitle is not the body.
|
|
45
|
+
*
|
|
46
|
+
* It used to be one box: the border or the heading row, then the subtitle
|
|
47
|
+
* and the children together inside whatever padding the caller asked for. So
|
|
48
|
+
* `padding={1}` put a blank row between the title and its own caption and
|
|
49
|
+
* left the caption sitting directly on top of the content it was not part
|
|
50
|
+
* of - the subtitle read as the first line of the body. On a borderless
|
|
51
|
+
* panel it pushed the title down as well.
|
|
52
|
+
*
|
|
53
|
+
* Split, then: the horizontal padding stays outside and applies to
|
|
54
|
+
* everything, because a subtitle indented differently from the body under it
|
|
55
|
+
* is worse than either; the vertical padding wraps the children alone.
|
|
56
|
+
*
|
|
57
|
+
* Only when there is a subtitle to protect. Without one there is nothing to
|
|
58
|
+
* separate the padding from, and a panel that gained a box would be a panel
|
|
59
|
+
* whose layout changed for no reason.
|
|
60
|
+
*/
|
|
61
|
+
const pad = resolveEdges(padding);
|
|
62
|
+
const split = subtitle !== undefined && (pad.top > 0 || pad.bottom > 0);
|
|
63
|
+
const outerPad = split ? { left: pad.left, right: pad.right } : padding;
|
|
64
|
+
const body = split
|
|
65
|
+
? h('box', { direction: 'column', padding: { top: pad.top, bottom: pad.bottom } }, children)
|
|
66
|
+
: children;
|
|
67
|
+
const border = props.border ?? theme.border;
|
|
68
|
+
const borderless = border === 'none' || (typeof border === 'object' && border.style === 'none');
|
|
69
|
+
|
|
70
|
+
// A panel is a pane, so it fills its row rather than floating in the middle
|
|
71
|
+
// of it. `Row` centres its children by default, which is right for a row of
|
|
72
|
+
// labels and wrong for a row of panels sitting next to a taller one.
|
|
73
|
+
const fill = { alignSelf: 'stretch' as const };
|
|
74
|
+
|
|
75
|
+
if (!borderless) {
|
|
76
|
+
// Two rules, two labels: `rightTitle` on the top one beside the title, and
|
|
77
|
+
// `meta` on the bottom one. The border painter gives the right label its
|
|
78
|
+
// width first, so a long title truncates rather than running into it.
|
|
79
|
+
return h('box', {
|
|
80
|
+
role: 'region',
|
|
81
|
+
border,
|
|
82
|
+
title,
|
|
83
|
+
...(rightTitle !== undefined ? { rightTitle } : {}),
|
|
84
|
+
footer: meta,
|
|
85
|
+
footerAlign: 'right',
|
|
86
|
+
...fill,
|
|
87
|
+
...(outerPad === undefined ? {} : { padding: outerPad }),
|
|
88
|
+
...rest,
|
|
89
|
+
},
|
|
90
|
+
subtitle !== undefined ? h('text', { content: subtitle, fg: 'muted' }) : null,
|
|
91
|
+
body,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return h('box', {
|
|
96
|
+
role: 'region',
|
|
97
|
+
direction: 'column',
|
|
98
|
+
...fill,
|
|
99
|
+
...(outerPad === undefined ? {} : { padding: outerPad }),
|
|
100
|
+
...rest,
|
|
101
|
+
border: 'none',
|
|
102
|
+
},
|
|
103
|
+
title
|
|
104
|
+
? h('box', { direction: 'row', gap: 1 },
|
|
105
|
+
h('text', { content: title, bold: true, fg: tone ?? 'text', flex: 1, truncate: 'end' }),
|
|
106
|
+
// No border to write on, so the same two labels share the heading
|
|
107
|
+
// row - `rightTitle` first, because it is the one that must survive.
|
|
108
|
+
rightTitle ? h('text', { content: rightTitle, fg: 'muted' }) : null,
|
|
109
|
+
meta ? h('text', { content: meta, fg: 'muted' }) : null)
|
|
110
|
+
: null,
|
|
111
|
+
subtitle !== undefined ? h('text', { content: subtitle, fg: 'muted' }) : null,
|
|
112
|
+
body,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BoxProps } from '@textui/core';
|
|
2
|
+
import { defineComponent, h } from '@textui/core';
|
|
3
|
+
|
|
4
|
+
export interface RowProps extends BoxProps {
|
|
5
|
+
/** Shorthand for `align`, which reads better on a row. */
|
|
6
|
+
vAlign?: BoxProps['align'];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Row = defineComponent<RowProps>('Row', ({ vAlign, ...props }) =>
|
|
10
|
+
h('box', { direction: 'row', align: vAlign ?? props.align ?? 'center', ...props }),
|
|
11
|
+
);
|