@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +39 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +76 -50
- package/src/App.diff.test.tsx +156 -42
- package/src/App.export.test.tsx +58 -40
- package/src/App.plan-review.test.tsx +363 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +128 -67
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.submit-confirm.test.tsx +191 -0
- package/src/App.test.tsx +310 -114
- package/src/App.toast.test.tsx +89 -0
- package/src/App.tsx +1043 -798
- package/src/App.walk.test.tsx +298 -0
- package/src/App.workbench.test.tsx +495 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +61 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +432 -0
- package/src/app-view-model.ts +159 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +126 -0
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +290 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +39 -0
- package/src/components/ChangesColumn.tsx +171 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +128 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +859 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +368 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +83 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +87 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +195 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +219 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +158 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +330 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +71 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +140 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +96 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.test.tsx +49 -0
- package/src/components/quick-actions-editor.tsx +150 -0
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +184 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +142 -0
- package/src/components/syntax-highlight.ts +110 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/config.test.ts +461 -31
- package/src/config.ts +389 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +165 -0
- package/src/diff-intraline.ts +146 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +11 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +698 -0
- package/src/intent-dispatch.ts +700 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +465 -0
- package/src/keymap.test.ts +611 -0
- package/src/keymap.ts +410 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/mark-runs.ts +120 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +307 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +30 -9
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +20 -9
- package/src/share.ts +215 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +68 -0
- package/src/slash-palette.test.ts +54 -0
- package/src/slash-palette.ts +104 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +355 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +170 -0
- package/src/thread-chords.ts +169 -0
- package/src/thread-controller.curate.test.ts +438 -0
- package/src/thread-controller.live-diff.test.ts +199 -0
- package/src/thread-controller.share.test.ts +472 -0
- package/src/thread-controller.tree.test.ts +255 -0
- package/src/thread-controller.ts +1509 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/use-annotation-surface.tsx +851 -0
- package/src/use-changes-workbench.ts +153 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +274 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +66 -9
- package/src/view-diff.ts +231 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +570 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -312
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import type { Theme } from "../theme";
|
|
4
|
+
import { useComponentTheme } from "./theme-context";
|
|
5
|
+
import { Dialog } from "./primitives/Dialog";
|
|
6
|
+
import type { CheatsheetSection } from "../key-bindings";
|
|
7
|
+
|
|
8
|
+
export interface KeybindsDialogProps {
|
|
9
|
+
sections: CheatsheetSection[];
|
|
10
|
+
theme?: Theme;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Rows a section takes: its title, its entries, and the blank line after it. */
|
|
14
|
+
function sectionRows(section: CheatsheetSection): number {
|
|
15
|
+
return 1 + section.entries.length + 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split the sections over `count` columns, filling each column to the
|
|
20
|
+
* balanced share of rows before starting the next, so a long cheatsheet
|
|
21
|
+
* reads top to bottom, then left to right.
|
|
22
|
+
*/
|
|
23
|
+
function columnsOf(sections: CheatsheetSection[], count: number): CheatsheetSection[][] {
|
|
24
|
+
const total = sections.reduce((sum, section) => sum + sectionRows(section), 0);
|
|
25
|
+
const share = Math.ceil(total / count);
|
|
26
|
+
const columns: CheatsheetSection[][] = [[]];
|
|
27
|
+
let filled = 0;
|
|
28
|
+
|
|
29
|
+
for (const section of sections) {
|
|
30
|
+
const column = columns[columns.length - 1]!;
|
|
31
|
+
|
|
32
|
+
if (filled > 0 && filled + sectionRows(section) > share && columns.length < count) {
|
|
33
|
+
columns.push([section]);
|
|
34
|
+
filled = sectionRows(section);
|
|
35
|
+
} else {
|
|
36
|
+
column.push(section);
|
|
37
|
+
filled += sectionRows(section);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return columns;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const COLUMN_WIDTH = 48;
|
|
45
|
+
|
|
46
|
+
export function KeybindsDialog({ sections, theme }: KeybindsDialogProps): React.ReactNode {
|
|
47
|
+
const tokens = useComponentTheme(theme);
|
|
48
|
+
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
49
|
+
const totalRows = sections.reduce((sum, section) => sum + sectionRows(section), 0) + 1;
|
|
50
|
+
// a cheatsheet taller than the terminal folds into a second column
|
|
51
|
+
const columnCount =
|
|
52
|
+
totalRows + 3 > terminalHeight - 4 && terminalWidth >= COLUMN_WIDTH * 2 + 6 ? 2 : 1;
|
|
53
|
+
const columns = columnsOf(sections, columnCount);
|
|
54
|
+
const tallest = Math.max(
|
|
55
|
+
...columns.map((column) => column.reduce((sum, section) => sum + sectionRows(section), 0)),
|
|
56
|
+
);
|
|
57
|
+
const width = Math.min(COLUMN_WIDTH * columnCount + 4, terminalWidth - 6);
|
|
58
|
+
const height = Math.min(terminalHeight - 4, tallest + 1 + 3);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Dialog
|
|
62
|
+
isOpen
|
|
63
|
+
title=" keybinds "
|
|
64
|
+
width={width}
|
|
65
|
+
height={height}
|
|
66
|
+
background={tokens.elevated}
|
|
67
|
+
theme={theme}
|
|
68
|
+
>
|
|
69
|
+
<box
|
|
70
|
+
style={{
|
|
71
|
+
flexDirection: "column",
|
|
72
|
+
flexGrow: 1,
|
|
73
|
+
paddingLeft: 1,
|
|
74
|
+
paddingRight: 1,
|
|
75
|
+
paddingTop: 1,
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
79
|
+
{columns.map((column, columnIndex) => (
|
|
80
|
+
<box key={columnIndex} style={{ flexDirection: "column", width: COLUMN_WIDTH }}>
|
|
81
|
+
{column.map((section) => (
|
|
82
|
+
<box key={section.title} style={{ flexDirection: "column" }}>
|
|
83
|
+
<text fg={tokens.accent}>{section.title}</text>
|
|
84
|
+
{section.entries.map((entry, index) => (
|
|
85
|
+
<text key={index}>
|
|
86
|
+
<span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
|
|
87
|
+
<span fg={tokens.textMuted}>{entry.label}</span>
|
|
88
|
+
</text>
|
|
89
|
+
))}
|
|
90
|
+
<text> </text>
|
|
91
|
+
</box>
|
|
92
|
+
))}
|
|
93
|
+
</box>
|
|
94
|
+
))}
|
|
95
|
+
</box>
|
|
96
|
+
<box style={{ flexDirection: "row" }}>
|
|
97
|
+
<box style={{ flexGrow: 1 }} />
|
|
98
|
+
<text fg={tokens.textDim}>[esc]</text>
|
|
99
|
+
</box>
|
|
100
|
+
</box>
|
|
101
|
+
</Dialog>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { MenuBar } from "./MenuBar";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/MenuBar" };
|
|
7
|
+
|
|
8
|
+
const callbacks = { onSettings: () => {}, onKeybinds: () => {} };
|
|
9
|
+
|
|
10
|
+
function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
11
|
+
return (
|
|
12
|
+
<box
|
|
13
|
+
style={{ flexDirection: "column", width: "100%", height: "100%", justifyContent: "flex-end" }}
|
|
14
|
+
>
|
|
15
|
+
{children}
|
|
16
|
+
</box>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Open: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<BottomFrame>
|
|
23
|
+
<MenuBar open {...callbacks} />
|
|
24
|
+
</BottomFrame>
|
|
25
|
+
),
|
|
26
|
+
expectedColors: [DARK.text],
|
|
27
|
+
size: { width: 60, height: 8 },
|
|
28
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Theme } from "../theme";
|
|
3
|
+
import { useComponentTheme } from "./theme-context";
|
|
4
|
+
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
5
|
+
|
|
6
|
+
export interface MenuBarProps {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onSettings: () => void;
|
|
9
|
+
onKeybinds: () => void;
|
|
10
|
+
theme?: Theme;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DROPDOWN_OPTION_ROWS = 2;
|
|
14
|
+
|
|
15
|
+
/** The settings gear's drop-down: Settings and Keybinds, below the top-left gear. */
|
|
16
|
+
export function MenuBar({ open, onSettings, onKeybinds, theme }: MenuBarProps): React.ReactNode {
|
|
17
|
+
const tokens = useComponentTheme(theme);
|
|
18
|
+
const dropdownHeight = DROPDOWN_OPTION_ROWS + 2;
|
|
19
|
+
|
|
20
|
+
if (!open) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<box
|
|
24
|
+
style={{
|
|
25
|
+
position: "absolute",
|
|
26
|
+
left: 1,
|
|
27
|
+
top: 2,
|
|
28
|
+
width: 16,
|
|
29
|
+
height: dropdownHeight,
|
|
30
|
+
border: true,
|
|
31
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
32
|
+
borderColor: tokens.text,
|
|
33
|
+
flexDirection: "column",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<box onMouseUp={onSettings}>
|
|
37
|
+
<text fg={tokens.text}> Settings</text>
|
|
38
|
+
</box>
|
|
39
|
+
<box onMouseUp={onKeybinds}>
|
|
40
|
+
<text fg={tokens.text}> Keybinds</text>
|
|
41
|
+
</box>
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import { DARK } from "../theme";
|
|
4
|
+
import { OverlayScrollbar } from "./OverlayScrollbar";
|
|
5
|
+
import type { Story, StoryMeta } from "./story";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Chrome/OverlayScrollbar" };
|
|
8
|
+
|
|
9
|
+
function TallSurface(): React.ReactNode {
|
|
10
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<box style={{ width: 24, height: 8, flexDirection: "row", backgroundColor: DARK.background }}>
|
|
14
|
+
<scrollbox
|
|
15
|
+
ref={scrollRef}
|
|
16
|
+
style={{ flexGrow: 1 }}
|
|
17
|
+
focused={false}
|
|
18
|
+
verticalScrollbarOptions={{ visible: false }}
|
|
19
|
+
>
|
|
20
|
+
{Array.from({ length: 24 }, (_unused, index) => (
|
|
21
|
+
<text key={index} fg={DARK.textMuted}>{`row ${index + 1}`}</text>
|
|
22
|
+
))}
|
|
23
|
+
</scrollbox>
|
|
24
|
+
<OverlayScrollbar scrollbox={scrollRef} />
|
|
25
|
+
</box>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// At rest the overlay bar is hidden; the thumb only appears while the surface is scrolling.
|
|
30
|
+
export const AtRest: Story = {
|
|
31
|
+
render: () => <TallSurface />,
|
|
32
|
+
size: { width: 24, height: 8 },
|
|
33
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mirrored scrollbar: thumb geometry follows the scroll state, and on a surface it
|
|
3
|
+
* draws as the panel's rightmost column, past the discussion dots - not inside the
|
|
4
|
+
* scrollbox's own viewport row.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
9
|
+
import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
|
|
10
|
+
import React from "react";
|
|
11
|
+
import { DiffContentView } from "./DiffContentView";
|
|
12
|
+
import { diffRows, marksByRows } from "../view-diff";
|
|
13
|
+
import { allowEventLoopUpdates } from "../test-support";
|
|
14
|
+
import { fixtureDiffSession } from "./story-fixtures";
|
|
15
|
+
import { DARK } from "../theme";
|
|
16
|
+
import { thumbGeometry } from "./OverlayScrollbar";
|
|
17
|
+
|
|
18
|
+
const noop = (): void => {};
|
|
19
|
+
|
|
20
|
+
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
21
|
+
if (node.id === id) return node;
|
|
22
|
+
for (const child of node.getChildren()) {
|
|
23
|
+
const found = findById(child, id);
|
|
24
|
+
|
|
25
|
+
if (found) return found;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hex(color: { toInts(): [number, number, number, number] }): string {
|
|
32
|
+
const [red, green, blue] = color.toInts();
|
|
33
|
+
|
|
34
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("thumbGeometry", () => {
|
|
38
|
+
test("nothing to scroll draws no thumb", () => {
|
|
39
|
+
// Arrange / Act / Assert - content fits the viewport
|
|
40
|
+
expect(thumbGeometry({ top: 0, height: 10, viewport: 14 }, 14)).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("the thumb's height is the viewport's share of the content, at least one row", () => {
|
|
44
|
+
// Act
|
|
45
|
+
const tall = thumbGeometry({ top: 0, height: 44, viewport: 14 }, 14);
|
|
46
|
+
const tiny = thumbGeometry({ top: 0, height: 10_000, viewport: 14 }, 14);
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(tall).toEqual({ row: 0, height: 4 });
|
|
50
|
+
expect(tiny?.height).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("the thumb reaches the track's end at the maximum scroll offset", () => {
|
|
54
|
+
// Act - scrolled to the bottom
|
|
55
|
+
const thumb = thumbGeometry({ top: 30, height: 44, viewport: 14 }, 14);
|
|
56
|
+
|
|
57
|
+
// Assert - its last row is the track's last row
|
|
58
|
+
expect(thumb).toEqual({ row: 10, height: 4 });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("the diff sheet's scrollbar", () => {
|
|
63
|
+
const body = Array.from({ length: 40 }, (_unused, index) => `+line ${index} of the change;`).join(
|
|
64
|
+
"\n",
|
|
65
|
+
);
|
|
66
|
+
const patch = `diff --git a/x.ts b/x.ts\n--- a/x.ts\n+++ b/x.ts\n@@ -0,0 +1,40 @@\n${body}\n`;
|
|
67
|
+
|
|
68
|
+
test("draws as the rightmost column, past the discussion dot", async () => {
|
|
69
|
+
// Arrange - a diff taller than the viewport, with one discussion so the dot rail has a dot
|
|
70
|
+
const rows = diffRows(patch);
|
|
71
|
+
const session = fixtureDiffSession({
|
|
72
|
+
annotations: [
|
|
73
|
+
{
|
|
74
|
+
id: "a1",
|
|
75
|
+
kind: "comment",
|
|
76
|
+
body: "hm",
|
|
77
|
+
anchor: { quote: "line 7 of the change;", prefix: "", suffix: "" },
|
|
78
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
const width = 60;
|
|
83
|
+
const setup = await testRender(
|
|
84
|
+
<DiffContentView
|
|
85
|
+
rows={rows}
|
|
86
|
+
session={session}
|
|
87
|
+
marks={marksByRows(session.annotations, rows)}
|
|
88
|
+
quickActions={[]}
|
|
89
|
+
observer={false}
|
|
90
|
+
onAnnotate={noop}
|
|
91
|
+
onReply={noop}
|
|
92
|
+
onUpdateAnnotation={noop}
|
|
93
|
+
onExit={noop}
|
|
94
|
+
/>,
|
|
95
|
+
{ width, height: 14 },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
allowEventLoopUpdates();
|
|
99
|
+
await setup.waitForVisualIdle();
|
|
100
|
+
// the overlay bar hides at rest; a scroll reveals the thumb
|
|
101
|
+
const scroller = findById(setup.renderer.root, "diff-scroll");
|
|
102
|
+
|
|
103
|
+
if (!(scroller instanceof ScrollBoxRenderable))
|
|
104
|
+
throw new Error("diff-scroll is not a scrollbox");
|
|
105
|
+
const countThumbRows = (): number =>
|
|
106
|
+
setup.captureSpans().lines.filter((line) => {
|
|
107
|
+
let column = 0;
|
|
108
|
+
|
|
109
|
+
for (const span of line.spans) {
|
|
110
|
+
column += span.text.length;
|
|
111
|
+
if (column >= width) return span.bg !== undefined && hex(span.bg) === DARK.textDim;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}).length;
|
|
116
|
+
|
|
117
|
+
// the overlay bar reveals on a scroll and hides after an idle delay; alternate the offset to
|
|
118
|
+
// re-trigger the reveal until the thumb is captured, so a slow CI settle never races the hide
|
|
119
|
+
let thumbRows = 0;
|
|
120
|
+
|
|
121
|
+
for (let attempt = 0; attempt < 20 && thumbRows === 0; attempt++) {
|
|
122
|
+
scroller.scrollTo(attempt % 2 === 0 ? 4 : 5);
|
|
123
|
+
await setup.waitForVisualIdle();
|
|
124
|
+
thumbRows = countThumbRows();
|
|
125
|
+
}
|
|
126
|
+
const frame = setup.captureCharFrame().split("\n");
|
|
127
|
+
|
|
128
|
+
// Assert - the thumb occupies the last column at the top (scroll offset 0), and the dot sits
|
|
129
|
+
// to its left in the rail, so the order reads content, dots, scrollbar
|
|
130
|
+
expect(thumbRows).toBe(4);
|
|
131
|
+
const dotLine = frame.find((line) => line.includes("○"));
|
|
132
|
+
|
|
133
|
+
expect(dotLine).toBeDefined();
|
|
134
|
+
expect(dotLine!.indexOf("○")).toBeLessThan(width - 1);
|
|
135
|
+
// let the async highlight settle before the renderer goes away
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
137
|
+
setup.renderer.destroy();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A one-cell vertical scrollbar that mirrors a scrollbox's scroll state, so the bar can sit
|
|
3
|
+
* where the layout wants it (the panel's rightmost column, past the discussion dots) instead
|
|
4
|
+
* of inside the scrollbox's own viewport row. It is an overlay: the thumb appears while the
|
|
5
|
+
* surface is scrolling and hides once scrolling has been idle, so a resting surface shows none.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useRef, useState, type RefObject } from "react";
|
|
9
|
+
import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
|
|
10
|
+
import type { Theme } from "../theme";
|
|
11
|
+
import { useFrameMeasure } from "../use-frame-measure";
|
|
12
|
+
import { useComponentTheme } from "./theme-context";
|
|
13
|
+
|
|
14
|
+
const HIDE_DELAY_MS = 1500;
|
|
15
|
+
|
|
16
|
+
interface ScrollState {
|
|
17
|
+
top: number;
|
|
18
|
+
height: number;
|
|
19
|
+
viewport: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ThumbGeometry {
|
|
23
|
+
row: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function thumbGeometry(state: ScrollState, track: number): ThumbGeometry | null {
|
|
28
|
+
if (track <= 0 || state.height <= state.viewport || state.viewport <= 0) return null;
|
|
29
|
+
const height = Math.max(1, Math.round((state.viewport / state.height) * track));
|
|
30
|
+
const maxTop = state.height - state.viewport;
|
|
31
|
+
const row = Math.round((state.top / maxTop) * (track - height));
|
|
32
|
+
|
|
33
|
+
return { row: Math.min(track - height, Math.max(0, row)), height };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OverlayScrollbarProps {
|
|
37
|
+
scrollbox: RefObject<ScrollBoxRenderable | null>;
|
|
38
|
+
theme?: Theme;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function OverlayScrollbar({ scrollbox, theme }: OverlayScrollbarProps): React.ReactNode {
|
|
42
|
+
const tokens = useComponentTheme(theme);
|
|
43
|
+
const trackRef = useRef<BoxRenderable | null>(null);
|
|
44
|
+
const [visible, setVisible] = useState(false);
|
|
45
|
+
const draggingRef = useRef(false);
|
|
46
|
+
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
47
|
+
const prevTopRef = useRef<number | null>(null);
|
|
48
|
+
|
|
49
|
+
const state = useFrameMeasure<ScrollState>(
|
|
50
|
+
() => ({
|
|
51
|
+
top: scrollbox.current?.scrollTop ?? 0,
|
|
52
|
+
height: scrollbox.current?.scrollHeight ?? 0,
|
|
53
|
+
viewport: scrollbox.current?.viewport.height ?? 0,
|
|
54
|
+
}),
|
|
55
|
+
(left, right) =>
|
|
56
|
+
left.top === right.top && left.height === right.height && left.viewport === right.viewport,
|
|
57
|
+
{ top: 0, height: 0, viewport: 0 },
|
|
58
|
+
);
|
|
59
|
+
const track = useFrameMeasure(
|
|
60
|
+
() => trackRef.current?.height ?? 0,
|
|
61
|
+
(left, right) => left === right,
|
|
62
|
+
0,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const reveal = (): void => {
|
|
66
|
+
setVisible(true);
|
|
67
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
68
|
+
hideTimerRef.current = setTimeout(() => {
|
|
69
|
+
if (!draggingRef.current) setVisible(false);
|
|
70
|
+
}, HIDE_DELAY_MS);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// reveal on real scroll movement, never on first mount, then hide once idle
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (prevTopRef.current !== null && state.top !== prevTopRef.current) reveal();
|
|
76
|
+
prevTopRef.current = state.top;
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, [state.top]);
|
|
79
|
+
|
|
80
|
+
useEffect(
|
|
81
|
+
() => () => {
|
|
82
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
83
|
+
},
|
|
84
|
+
[],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const geometry = thumbGeometry(state, track);
|
|
88
|
+
const thumb = visible ? geometry : null;
|
|
89
|
+
|
|
90
|
+
const scrollToTrackRow = (screenY: number): void => {
|
|
91
|
+
const box = scrollbox.current;
|
|
92
|
+
|
|
93
|
+
if (!box || !geometry) return;
|
|
94
|
+
const row = screenY - (trackRef.current?.y ?? 0) - Math.floor(geometry.height / 2);
|
|
95
|
+
const ratio = Math.min(1, Math.max(0, row / Math.max(1, track - geometry.height)));
|
|
96
|
+
|
|
97
|
+
box.scrollTo(Math.round(ratio * (state.height - state.viewport)));
|
|
98
|
+
reveal();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<box
|
|
103
|
+
ref={trackRef}
|
|
104
|
+
style={{ width: 1, flexShrink: 0, flexDirection: "column" }}
|
|
105
|
+
onMouseDown={(event) => {
|
|
106
|
+
draggingRef.current = true;
|
|
107
|
+
scrollToTrackRow(event.y);
|
|
108
|
+
}}
|
|
109
|
+
onMouseDrag={(event) => scrollToTrackRow(event.y)}
|
|
110
|
+
onMouseUp={() => {
|
|
111
|
+
draggingRef.current = false;
|
|
112
|
+
reveal();
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{thumb ? (
|
|
116
|
+
<box
|
|
117
|
+
style={{
|
|
118
|
+
position: "absolute",
|
|
119
|
+
top: thumb.row,
|
|
120
|
+
left: 0,
|
|
121
|
+
width: 1,
|
|
122
|
+
height: thumb.height,
|
|
123
|
+
backgroundColor: tokens.textDim,
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
) : null}
|
|
127
|
+
</box>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { PaneGrid } from "./PaneGrid";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Layout/PaneGrid" };
|
|
7
|
+
|
|
8
|
+
function Sidebar(): React.ReactNode {
|
|
9
|
+
return (
|
|
10
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
11
|
+
<text fg={DARK.textDim}>Projects</text>
|
|
12
|
+
<text fg={DARK.text}> cueloop</text>
|
|
13
|
+
<box style={{ backgroundColor: DARK.elevated }}>
|
|
14
|
+
<text fg={DARK.accent}> Read Cueloop Repository</text>
|
|
15
|
+
</box>
|
|
16
|
+
<text> </text>
|
|
17
|
+
<text fg={DARK.textDim}>Threads</text>
|
|
18
|
+
<text fg={DARK.textMuted}> Welcome to cueloop</text>
|
|
19
|
+
</box>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Main(): React.ReactNode {
|
|
24
|
+
return (
|
|
25
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
26
|
+
<text fg={DARK.text}>read the repo</text>
|
|
27
|
+
<text> </text>
|
|
28
|
+
<text fg={DARK.textMuted}>Repository context loaded. main is clean.</text>
|
|
29
|
+
<text> </text>
|
|
30
|
+
<text fg={DARK.text}>Core architecture</text>
|
|
31
|
+
<text fg={DARK.textMuted}> - packages/schema is the dependency root</text>
|
|
32
|
+
<text fg={DARK.textMuted}> - packages/daemon owns session state</text>
|
|
33
|
+
</box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Inspector(): React.ReactNode {
|
|
38
|
+
return (
|
|
39
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
40
|
+
<text fg={DARK.text}>cueloop/main</text>
|
|
41
|
+
<text fg={DARK.textDim}> No changes</text>
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Header(): React.ReactNode {
|
|
47
|
+
return (
|
|
48
|
+
<box style={{ flexDirection: "row", paddingLeft: 1 }}>
|
|
49
|
+
<text fg={DARK.textMuted}>Read Cueloop Repository</text>
|
|
50
|
+
<box style={{ flexGrow: 1 }} />
|
|
51
|
+
<text fg={DARK.textDim}>markdown.test.ts</text>
|
|
52
|
+
</box>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Footer(): React.ReactNode {
|
|
57
|
+
return (
|
|
58
|
+
<box style={{ flexDirection: "row", paddingLeft: 1 }}>
|
|
59
|
+
<text fg={DARK.textDim}>cueloop / main</text>
|
|
60
|
+
<box style={{ flexGrow: 1 }} />
|
|
61
|
+
<text fg={DARK.textDim}>Fable 5</text>
|
|
62
|
+
</box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ThreePane: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<PaneGrid
|
|
69
|
+
sidebar={<Sidebar />}
|
|
70
|
+
main={<Main />}
|
|
71
|
+
mainHeader={<Header />}
|
|
72
|
+
inspector={<Inspector />}
|
|
73
|
+
footer={<Footer />}
|
|
74
|
+
/>
|
|
75
|
+
),
|
|
76
|
+
expectedColors: [DARK.border, DARK.accent],
|
|
77
|
+
size: { width: 120, height: 28 },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const SidebarAndMain: Story = {
|
|
81
|
+
render: () => <PaneGrid sidebar={<Sidebar />} main={<Main />} />,
|
|
82
|
+
expectedColors: [DARK.border],
|
|
83
|
+
size: { width: 90, height: 20 },
|
|
84
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// A generic pane grid: a row of columns, each stacking a header cell over its body, with the
|
|
2
|
+
// vertical rules run full height and every header cell's bottom rule joined into one horizontal
|
|
3
|
+
// line. Rules only - no enclosing frames. This is the layout primitive for dev surfaces (the
|
|
4
|
+
// storybook runner); the product's four-pane workbench is AppShell.
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { DARK, type Theme } from "../theme";
|
|
8
|
+
import type { BorderSides } from "@opentui/core";
|
|
9
|
+
|
|
10
|
+
export interface PaneGridProps {
|
|
11
|
+
headerHeight?: number;
|
|
12
|
+
sidebar?: React.ReactNode;
|
|
13
|
+
sidebarHeader?: React.ReactNode;
|
|
14
|
+
sidebarWidth?: number;
|
|
15
|
+
main: React.ReactNode;
|
|
16
|
+
mainHeader?: React.ReactNode;
|
|
17
|
+
inspector?: React.ReactNode;
|
|
18
|
+
inspectorHeader?: React.ReactNode;
|
|
19
|
+
inspectorWidth?: number;
|
|
20
|
+
footer?: React.ReactNode;
|
|
21
|
+
theme?: Theme;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PaneColumnProps {
|
|
25
|
+
width?: number;
|
|
26
|
+
grow?: boolean;
|
|
27
|
+
side?: BorderSides;
|
|
28
|
+
header: React.ReactNode;
|
|
29
|
+
body: React.ReactNode;
|
|
30
|
+
headerHeight: number;
|
|
31
|
+
theme: Theme;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function PaneColumn({
|
|
35
|
+
width,
|
|
36
|
+
grow,
|
|
37
|
+
side,
|
|
38
|
+
header,
|
|
39
|
+
body,
|
|
40
|
+
headerHeight,
|
|
41
|
+
theme,
|
|
42
|
+
}: PaneColumnProps): React.ReactNode {
|
|
43
|
+
return (
|
|
44
|
+
<box
|
|
45
|
+
style={{
|
|
46
|
+
flexDirection: "column",
|
|
47
|
+
...(grow ? { flexGrow: 1 } : { width }),
|
|
48
|
+
borderStyle: "single",
|
|
49
|
+
border: side ? [side] : [],
|
|
50
|
+
borderColor: theme.border,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<box
|
|
54
|
+
style={{
|
|
55
|
+
height: headerHeight,
|
|
56
|
+
borderStyle: "single",
|
|
57
|
+
border: ["bottom"],
|
|
58
|
+
borderColor: theme.border,
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{header}
|
|
62
|
+
</box>
|
|
63
|
+
<box style={{ flexGrow: 1 }}>{body}</box>
|
|
64
|
+
</box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function PaneGrid({
|
|
69
|
+
headerHeight = 3,
|
|
70
|
+
sidebar,
|
|
71
|
+
sidebarHeader,
|
|
72
|
+
sidebarWidth = 28,
|
|
73
|
+
main,
|
|
74
|
+
mainHeader,
|
|
75
|
+
inspector,
|
|
76
|
+
inspectorHeader,
|
|
77
|
+
inspectorWidth = 40,
|
|
78
|
+
footer,
|
|
79
|
+
theme,
|
|
80
|
+
}: PaneGridProps): React.ReactNode {
|
|
81
|
+
const tokens = theme ?? DARK;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<box
|
|
85
|
+
style={{
|
|
86
|
+
flexDirection: "column",
|
|
87
|
+
width: "100%",
|
|
88
|
+
height: "100%",
|
|
89
|
+
backgroundColor: tokens.background,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
93
|
+
{sidebar !== undefined ? (
|
|
94
|
+
<PaneColumn
|
|
95
|
+
width={sidebarWidth}
|
|
96
|
+
side="right"
|
|
97
|
+
header={sidebarHeader}
|
|
98
|
+
body={sidebar}
|
|
99
|
+
headerHeight={headerHeight}
|
|
100
|
+
theme={tokens}
|
|
101
|
+
/>
|
|
102
|
+
) : null}
|
|
103
|
+
<PaneColumn
|
|
104
|
+
grow
|
|
105
|
+
header={mainHeader}
|
|
106
|
+
body={main}
|
|
107
|
+
headerHeight={headerHeight}
|
|
108
|
+
theme={tokens}
|
|
109
|
+
/>
|
|
110
|
+
{inspector !== undefined ? (
|
|
111
|
+
<PaneColumn
|
|
112
|
+
width={inspectorWidth}
|
|
113
|
+
side="left"
|
|
114
|
+
header={inspectorHeader}
|
|
115
|
+
body={inspector}
|
|
116
|
+
headerHeight={headerHeight}
|
|
117
|
+
theme={tokens}
|
|
118
|
+
/>
|
|
119
|
+
) : null}
|
|
120
|
+
</box>
|
|
121
|
+
{footer !== undefined ? (
|
|
122
|
+
<box style={{ borderStyle: "single", border: ["top"], borderColor: tokens.border }}>
|
|
123
|
+
{footer}
|
|
124
|
+
</box>
|
|
125
|
+
) : null}
|
|
126
|
+
</box>
|
|
127
|
+
);
|
|
128
|
+
}
|