@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.67
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/package.json +6 -5
- package/src/App.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +79 -112
- package/src/App.plan-review.test.tsx +23 -205
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +9 -27
- package/src/App.submit-confirm.test.tsx +19 -72
- package/src/App.test.tsx +136 -31
- package/src/App.tsx +491 -322
- package/src/App.walk.test.tsx +15 -16
- package/src/App.workbench.test.tsx +390 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +61 -0
- package/src/app-screens.tsx +197 -77
- package/src/app-view-model.ts +9 -69
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AnnotationCard.tsx +1 -1
- 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 +239 -0
- package/src/components/AppShell.zoom.test.tsx +94 -0
- package/src/components/Breadcrumb.stories.tsx +1 -1
- package/src/components/ChangesColumn.stories.tsx +39 -0
- package/src/components/ChangesColumn.tsx +170 -0
- package/src/components/CodeBlock.stories.tsx +1 -1
- package/src/components/CompletionOverlay.stories.tsx +1 -1
- package/src/components/ConfirmCard.stories.tsx +1 -1
- package/src/components/ConfirmCard.tsx +3 -4
- package/src/components/ConfirmDialog.stories.tsx +1 -1
- package/src/components/DiffSheet.scroll.test.tsx +38 -55
- package/src/components/DiffSheet.split.test.tsx +140 -0
- package/src/components/DiffSheet.stories.tsx +56 -54
- package/src/components/DiffSheet.syntax.test.tsx +24 -7
- package/src/components/DiffSheet.tsx +607 -276
- 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 +105 -0
- package/src/components/EditorGrid.tsx +338 -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 +78 -0
- package/src/components/InboxList.stories.tsx +23 -8
- package/src/components/InboxList.tsx +188 -33
- package/src/components/KeybindsDialog.stories.tsx +1 -1
- package/src/components/MarkerPopover.stories.tsx +1 -1
- package/src/components/MenuBar.stories.tsx +3 -12
- package/src/components/MenuBar.tsx +25 -56
- 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 +82 -0
- package/src/components/PromptDialog.stories.tsx +1 -1
- package/src/components/PromptDialog.tsx +8 -1
- package/src/components/PrototypeSheet.stories.tsx +1 -1
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -0
- package/src/components/SettingsDialog.stories.tsx +7 -1
- package/src/components/SettingsDialog.tsx +114 -67
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SurfaceScrollbar.stories.tsx +34 -0
- package/src/components/SurfaceScrollbar.test.tsx +111 -0
- package/src/components/SurfaceScrollbar.tsx +93 -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/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +1 -1
- package/src/components/ThreadView.tsx +62 -1352
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +70 -0
- package/src/components/Toast.stories.tsx +1 -1
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +1 -1
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/WelcomeSurface.stories.tsx +12 -0
- package/src/components/WelcomeSurface.tsx +52 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +732 -572
- package/src/components/agent-launcher.stories.tsx +1 -1
- package/src/components/diff-sheet-layout.test.ts +125 -55
- package/src/components/diff-sheet-layout.ts +3 -103
- 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 +1 -1
- package/src/components/primitives/Card.stories.tsx +1 -1
- package/src/components/primitives/Dialog.stories.tsx +1 -1
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +1 -1
- package/src/components/primitives/Tabs.stories.tsx +1 -1
- package/src/components/primitives/Toolbar.stories.tsx +1 -1
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +96 -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 +1 -1
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +142 -82
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +55 -0
- package/src/components/theme-context.stories.tsx +1 -1
- package/src/config.test.ts +20 -49
- package/src/config.ts +26 -28
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-syntax.test.ts +16 -0
- package/src/diff-syntax.ts +6 -3
- package/src/discussions.ts +79 -0
- package/src/intent-dispatch.test.ts +31 -11
- package/src/intent-dispatch.ts +93 -48
- package/src/key-bindings.test.ts +1 -1
- package/src/keymap.test.ts +3 -25
- package/src/keymap.ts +17 -18
- package/src/mark-runs.ts +120 -0
- package/src/pty.ts +10 -2
- package/src/run.ts +17 -7
- package/src/serve.test.ts +4 -8
- package/src/session-controller.curate.test.ts +75 -0
- package/src/session-controller.share.test.ts +1 -0
- package/src/session-controller.tree.test.ts +26 -0
- package/src/session-controller.ts +160 -31
- package/src/slash-palette.ts +83 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +3 -3
- package/src/test-support.ts +39 -8
- package/src/theme.ts +4 -4
- package/src/thread-chords.test.ts +25 -9
- package/src/thread-chords.ts +33 -8
- package/src/use-annotation-surface.tsx +837 -0
- package/src/use-changes-workbench.ts +151 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +23 -24
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/view-diff.ts +114 -0
- package/src/App.review-panel.test.tsx +0 -140
- package/src/App.tree.test.tsx +0 -224
- package/src/components/ReviewPanel.stories.tsx +0 -100
- package/src/components/ReviewPanel.tsx +0 -103
- package/src/components/ReviewRail.stories.tsx +0 -232
- package/src/components/ReviewRail.tsx +0 -327
- package/src/review-panel.test.ts +0 -113
- package/src/review-panel.ts +0 -83
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// A workbench panel: one full-height grid column with a file-tab header cell on
|
|
2
|
+
// top and a body below. The header row carries the tabs on the left and optional
|
|
3
|
+
// controls on the right, underlined by a thin rule in the same divider gray so the
|
|
4
|
+
// header bottoms join the side rules into one grid. The side border runs from the top.
|
|
5
|
+
|
|
6
|
+
import React, { useState } from "react";
|
|
7
|
+
import type { Theme } from "../theme";
|
|
8
|
+
import { NERD, HEADER_UNDERLINE_CHARS } from "./primitives/icons";
|
|
9
|
+
|
|
10
|
+
export interface FileTabProps {
|
|
11
|
+
label: string;
|
|
12
|
+
active?: boolean;
|
|
13
|
+
/** When set, a close control reveals on hover (a disposable tab). */
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
theme?: Theme;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A file tab: the label in the accent when active, with a hover-revealed close. */
|
|
19
|
+
export function FileTab({ label, active, onClose, theme }: FileTabProps): React.ReactNode {
|
|
20
|
+
const [hovered, setHovered] = useState(false);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<box
|
|
24
|
+
style={{ flexDirection: "row", paddingRight: 1 }}
|
|
25
|
+
onMouseOver={() => setHovered(true)}
|
|
26
|
+
onMouseOut={() => setHovered(false)}
|
|
27
|
+
>
|
|
28
|
+
<text fg={active ? theme?.accent : theme?.textDim}>{label}</text>
|
|
29
|
+
{onClose ? (
|
|
30
|
+
<box onMouseUp={onClose} style={{ paddingLeft: 1 }}>
|
|
31
|
+
<text fg={theme?.textDim}>{hovered ? NERD.close : " "}</text>
|
|
32
|
+
</box>
|
|
33
|
+
) : null}
|
|
34
|
+
</box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PanelColumnProps {
|
|
39
|
+
/** Fixed column width; omit for the flex-growing center panel. */
|
|
40
|
+
width?: number;
|
|
41
|
+
/** Draw the divider on this side (the sidebar borders right, the rest border left). */
|
|
42
|
+
border?: "left" | "right";
|
|
43
|
+
/** Tabs and controls for the header cell. */
|
|
44
|
+
header: React.ReactNode;
|
|
45
|
+
/** Right-aligned header controls (toggles, search). */
|
|
46
|
+
headerRight?: React.ReactNode;
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
theme?: Theme;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function PanelColumn({
|
|
52
|
+
width,
|
|
53
|
+
border,
|
|
54
|
+
header,
|
|
55
|
+
headerRight,
|
|
56
|
+
children,
|
|
57
|
+
theme,
|
|
58
|
+
}: PanelColumnProps): React.ReactNode {
|
|
59
|
+
return (
|
|
60
|
+
<box
|
|
61
|
+
style={{
|
|
62
|
+
width,
|
|
63
|
+
// grow panes split evenly and clip wide content (a long diff line) rather than stretching
|
|
64
|
+
flexGrow: width === undefined ? 1 : undefined,
|
|
65
|
+
flexBasis: width === undefined ? 0 : undefined,
|
|
66
|
+
minWidth: 0,
|
|
67
|
+
flexDirection: "column",
|
|
68
|
+
borderStyle: "single",
|
|
69
|
+
border: border ? [border] : [],
|
|
70
|
+
borderColor: theme?.border,
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<box
|
|
74
|
+
customBorderChars={HEADER_UNDERLINE_CHARS}
|
|
75
|
+
style={{
|
|
76
|
+
flexDirection: "row",
|
|
77
|
+
height: 2,
|
|
78
|
+
paddingLeft: 1,
|
|
79
|
+
// right controls need the same breathing room as the left, or the rightmost icon renders
|
|
80
|
+
// squeezed against the terminal's last column
|
|
81
|
+
paddingRight: 1,
|
|
82
|
+
backgroundColor: theme?.panel,
|
|
83
|
+
borderStyle: "single",
|
|
84
|
+
border: ["bottom"],
|
|
85
|
+
borderColor: theme?.border,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{header}
|
|
89
|
+
<box style={{ flexGrow: 1 }} />
|
|
90
|
+
{headerRight}
|
|
91
|
+
</box>
|
|
92
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>{children}</box>
|
|
93
|
+
</box>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ProjectTreeView } from "./ProjectTreeView";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Surfaces/ProjectTreeView" };
|
|
7
|
+
|
|
8
|
+
const SAMPLE = [
|
|
9
|
+
"packages/client/src/App.tsx",
|
|
10
|
+
"packages/client/src/components/AppShell.tsx",
|
|
11
|
+
"packages/daemon/src/api.ts",
|
|
12
|
+
"README.md",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const Loaded: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<ProjectTreeView
|
|
18
|
+
loadFiles={() => Promise.resolve(SAMPLE)}
|
|
19
|
+
onSelectFile={() => {}}
|
|
20
|
+
theme={DARK}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
expectedColors: [DARK.textDim],
|
|
24
|
+
size: { width: 40, height: 16 },
|
|
25
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { ProjectTreeView } from "./ProjectTreeView";
|
|
5
|
+
import { clickText, waitForText } from "../test-support";
|
|
6
|
+
import { DARK } from "../theme";
|
|
7
|
+
|
|
8
|
+
test("renders the loaded files as a tree and opens a file on click", async () => {
|
|
9
|
+
const opened: string[] = [];
|
|
10
|
+
const setup = await testRender(
|
|
11
|
+
<ProjectTreeView
|
|
12
|
+
loadFiles={() => Promise.resolve(["src/App.tsx", "README.md"])}
|
|
13
|
+
onSelectFile={(path) => opened.push(path)}
|
|
14
|
+
theme={DARK}
|
|
15
|
+
/>,
|
|
16
|
+
{ width: 40, height: 16 },
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
await waitForText(setup, "README.md");
|
|
20
|
+
await clickText(setup, "README.md");
|
|
21
|
+
|
|
22
|
+
expect(opened).toContain("README.md");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("shows an empty hint when there are no files", async () => {
|
|
26
|
+
const setup = await testRender(
|
|
27
|
+
<ProjectTreeView loadFiles={() => Promise.resolve([])} onSelectFile={() => {}} theme={DARK} />,
|
|
28
|
+
{ width: 40, height: 8 },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
await waitForText(setup, "empty");
|
|
32
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The Project pane in tree mode: the workspace's tracked files as a collapsible directory tree.
|
|
2
|
+
// Folders start collapsed and expand on click; a file click opens it as a tab. The caller keys this
|
|
3
|
+
// on the session id so switching sessions remounts it and never shows the previous repository's paths.
|
|
4
|
+
|
|
5
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import type { Theme } from "../theme";
|
|
7
|
+
import { useComponentTheme } from "./theme-context";
|
|
8
|
+
import { Tree } from "./primitives/Tree";
|
|
9
|
+
import { buildPathTree } from "./file-tree";
|
|
10
|
+
|
|
11
|
+
export interface ProjectTreeViewProps {
|
|
12
|
+
loadFiles: () => Promise<string[]>;
|
|
13
|
+
onSelectFile: (path: string) => void;
|
|
14
|
+
theme?: Theme;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ProjectTreeView({
|
|
18
|
+
loadFiles,
|
|
19
|
+
onSelectFile,
|
|
20
|
+
theme,
|
|
21
|
+
}: ProjectTreeViewProps): React.ReactNode {
|
|
22
|
+
const tokens = useComponentTheme(theme);
|
|
23
|
+
const [paths, setPaths] = useState<readonly string[] | null>(null);
|
|
24
|
+
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set());
|
|
25
|
+
const nodes = useMemo(() => (paths ? buildPathTree(paths) : []), [paths]);
|
|
26
|
+
const loadRef = useRef(loadFiles);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
loadRef.current = loadFiles;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let alive = true;
|
|
33
|
+
void loadRef.current().then(
|
|
34
|
+
(files) => {
|
|
35
|
+
if (alive) setPaths(files);
|
|
36
|
+
},
|
|
37
|
+
() => {
|
|
38
|
+
if (alive) setPaths([]);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
alive = false;
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
if (paths === null) {
|
|
48
|
+
return (
|
|
49
|
+
<box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
|
|
50
|
+
<text fg={tokens.textDim}>loading...</text>
|
|
51
|
+
</box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (paths.length === 0) {
|
|
55
|
+
return (
|
|
56
|
+
<box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
|
|
57
|
+
<text fg={tokens.textDim}>empty</text>
|
|
58
|
+
</box>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<scrollbox style={{ flexGrow: 1 }} focused={false}>
|
|
64
|
+
<Tree
|
|
65
|
+
nodes={nodes}
|
|
66
|
+
expandedIds={expandedIds}
|
|
67
|
+
onSelect={onSelectFile}
|
|
68
|
+
onToggle={(id) =>
|
|
69
|
+
setExpandedIds((current) => {
|
|
70
|
+
const next = new Set(current);
|
|
71
|
+
|
|
72
|
+
if (next.has(id)) next.delete(id);
|
|
73
|
+
else next.add(id);
|
|
74
|
+
|
|
75
|
+
return next;
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
theme={theme}
|
|
79
|
+
/>
|
|
80
|
+
</scrollbox>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { PromptDialog } from "./PromptDialog";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "PromptDialog" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Overlays/PromptDialog" };
|
|
7
7
|
|
|
8
8
|
export const RenameAuthor: Story = {
|
|
9
9
|
render: () => (
|
|
@@ -46,7 +46,14 @@ export function PromptDialog({
|
|
|
46
46
|
if (!isOpen) return null;
|
|
47
47
|
|
|
48
48
|
return (
|
|
49
|
-
<Dialog
|
|
49
|
+
<Dialog
|
|
50
|
+
isOpen
|
|
51
|
+
title={title}
|
|
52
|
+
width={Math.min(54, terminalWidth - 6)}
|
|
53
|
+
height={7}
|
|
54
|
+
background={tokens.elevated}
|
|
55
|
+
theme={theme}
|
|
56
|
+
>
|
|
50
57
|
<box
|
|
51
58
|
style={{
|
|
52
59
|
flexDirection: "column",
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { PrototypeSheet } from "./PrototypeSheet";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "PrototypeSheet" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Surfaces/PrototypeSheet" };
|
|
7
7
|
|
|
8
8
|
// The story renderer reports no kitty graphics, so the sheet shows its fallback.
|
|
9
9
|
export const Unsupported: Story = {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { RootOverlayProvider, useRootOverlay } from "./RootOverlay";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Layout/RootOverlay" };
|
|
7
|
+
|
|
8
|
+
// The provider renders whatever a descendant pushes after the rest of the tree, so it draws on top.
|
|
9
|
+
function OverlayDemo(): React.ReactNode {
|
|
10
|
+
const { setOverlay } = useRootOverlay();
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setOverlay(
|
|
14
|
+
<box
|
|
15
|
+
style={{
|
|
16
|
+
position: "absolute",
|
|
17
|
+
top: 2,
|
|
18
|
+
left: 6,
|
|
19
|
+
borderStyle: "single",
|
|
20
|
+
borderColor: DARK.border,
|
|
21
|
+
backgroundColor: DARK.elevated,
|
|
22
|
+
paddingLeft: 1,
|
|
23
|
+
paddingRight: 1,
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<text fg={DARK.text}>floating overlay</text>
|
|
27
|
+
</box>,
|
|
28
|
+
);
|
|
29
|
+
}, [setOverlay]);
|
|
30
|
+
|
|
31
|
+
return <text fg={DARK.textDim}>base content under the overlay</text>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const FloatingNode: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<RootOverlayProvider>
|
|
37
|
+
<box style={{ padding: 1, backgroundColor: DARK.panel }}>
|
|
38
|
+
<OverlayDemo />
|
|
39
|
+
</box>
|
|
40
|
+
</RootOverlayProvider>
|
|
41
|
+
),
|
|
42
|
+
expectedColors: [DARK.text],
|
|
43
|
+
size: { width: 40, height: 6 },
|
|
44
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// A root overlay slot: OpenTUI has no z-index, so an overlay drawn inside a pane is sliced by the
|
|
2
|
+
// next pane's border. Anything pushed here renders after every pane (last in draw order), so it sits
|
|
3
|
+
// on top of the whole shell - the same trick the tooltip uses. Callers position their node with
|
|
4
|
+
// absolute screen coordinates.
|
|
5
|
+
|
|
6
|
+
import React, { createContext, useContext, useMemo, useState } from "react";
|
|
7
|
+
|
|
8
|
+
interface RootOverlayApi {
|
|
9
|
+
setOverlay: (node: React.ReactNode) => void;
|
|
10
|
+
clearOverlay: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RootOverlayContext = createContext<RootOverlayApi>({
|
|
14
|
+
setOverlay: () => {},
|
|
15
|
+
clearOverlay: () => {},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function useRootOverlay(): RootOverlayApi {
|
|
19
|
+
return useContext(RootOverlayContext);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function RootOverlayProvider({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
23
|
+
const [overlay, setOverlay] = useState<React.ReactNode>(null);
|
|
24
|
+
const api = useMemo<RootOverlayApi>(
|
|
25
|
+
() => ({ setOverlay, clearOverlay: () => setOverlay(null) }),
|
|
26
|
+
[],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<RootOverlayContext.Provider value={api}>
|
|
31
|
+
{children}
|
|
32
|
+
{overlay}
|
|
33
|
+
</RootOverlayContext.Provider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -4,7 +4,7 @@ import { THEME_PRESETS } from "../theme-presets";
|
|
|
4
4
|
import type { Story, StoryMeta } from "./story";
|
|
5
5
|
import { SettingsDialog, type SettingsCategory } from "./SettingsDialog";
|
|
6
6
|
|
|
7
|
-
export const meta: StoryMeta = { title: "SettingsDialog" };
|
|
7
|
+
export const meta: StoryMeta = { title: "Overlays/SettingsDialog" };
|
|
8
8
|
|
|
9
9
|
const CATEGORIES: SettingsCategory[] = [
|
|
10
10
|
{
|
|
@@ -62,6 +62,8 @@ export const NavZone: Story = {
|
|
|
62
62
|
render: () => (
|
|
63
63
|
<SettingsDialog
|
|
64
64
|
isOpen
|
|
65
|
+
version="0.1.0-alpha.32"
|
|
66
|
+
keybindsSections={[]}
|
|
65
67
|
categories={CATEGORIES}
|
|
66
68
|
values={VALUES}
|
|
67
69
|
activeCategoryId="general"
|
|
@@ -79,6 +81,8 @@ export const BodyZone: Story = {
|
|
|
79
81
|
render: () => (
|
|
80
82
|
<SettingsDialog
|
|
81
83
|
isOpen
|
|
84
|
+
version="0.1.0-alpha.32"
|
|
85
|
+
keybindsSections={[]}
|
|
82
86
|
categories={CATEGORIES}
|
|
83
87
|
values={VALUES}
|
|
84
88
|
activeCategoryId="display"
|
|
@@ -96,6 +100,8 @@ export const AppearanceThemed: Story = {
|
|
|
96
100
|
render: () => (
|
|
97
101
|
<SettingsDialog
|
|
98
102
|
isOpen
|
|
103
|
+
version="0.1.0-alpha.32"
|
|
104
|
+
keybindsSections={[]}
|
|
99
105
|
categories={CATEGORIES}
|
|
100
106
|
values={VALUES}
|
|
101
107
|
activeCategoryId="appearance"
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The settings overlay
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The settings overlay: a centered Dialog whose left nav is a tree - a Settings
|
|
3
|
+
* group over its categories, plus Keybinds as a sibling leaf - and whose right
|
|
4
|
+
* body shows the active category's typed rows or the keybinds cheatsheet. The
|
|
5
|
+
* component is controlled: the caller owns the open flag, the active
|
|
6
|
+
* category/row/zone, and value changes, so the app's keyboard grammar (and the
|
|
7
|
+
* stories catalog) drive it without a second key handler inside.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from "react";
|
|
11
11
|
import { useTerminalDimensions } from "@opentui/react";
|
|
12
12
|
import type { Theme } from "../theme";
|
|
13
|
+
import type { CheatsheetSection } from "../key-bindings";
|
|
13
14
|
import { useComponentTheme } from "./theme-context";
|
|
14
15
|
import { Dialog } from "./primitives/Dialog";
|
|
16
|
+
import { Tree } from "./primitives/Tree";
|
|
17
|
+
import type { TreeNode } from "./primitives/tree-model";
|
|
15
18
|
import { CycleRow, TextRow, ToggleRow, type SettingsRowDescriptor } from "./SettingsRows";
|
|
16
19
|
|
|
17
20
|
export interface SettingsCategory {
|
|
@@ -25,10 +28,17 @@ export interface SettingsCategory {
|
|
|
25
28
|
|
|
26
29
|
export type SettingsValues = Record<string, string | boolean>;
|
|
27
30
|
|
|
31
|
+
/** The category id whose right pane shows the keybinds cheatsheet rather than rows. */
|
|
32
|
+
const KEYBINDS_CATEGORY_ID = "keybinds";
|
|
33
|
+
|
|
28
34
|
export interface SettingsDialogProps {
|
|
29
35
|
isOpen: boolean;
|
|
36
|
+
/** The client version, shown in the dialog footer (its home now the menu bar is gone). */
|
|
37
|
+
version: string;
|
|
30
38
|
categories: SettingsCategory[];
|
|
31
39
|
values: SettingsValues;
|
|
40
|
+
/** The keybinds cheatsheet, shown when the Keybinds leaf is active. */
|
|
41
|
+
keybindsSections: CheatsheetSection[];
|
|
32
42
|
activeCategoryId: string;
|
|
33
43
|
activeRowIndex: number;
|
|
34
44
|
activeZone: "nav" | "body";
|
|
@@ -37,10 +47,50 @@ export interface SettingsDialogProps {
|
|
|
37
47
|
theme?: Theme;
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
/** The left nav tree: a Settings group over its categories, then Keybinds as a leaf. */
|
|
51
|
+
function navTree(categories: SettingsCategory[]): TreeNode[] {
|
|
52
|
+
const children = categories
|
|
53
|
+
.filter((category) => category.id !== KEYBINDS_CATEGORY_ID)
|
|
54
|
+
.map((category) => ({ id: category.id, label: category.name }));
|
|
55
|
+
const nodes: TreeNode[] = [{ id: "settings", label: "Settings", children }];
|
|
56
|
+
const keybinds = categories.find((category) => category.id === KEYBINDS_CATEGORY_ID);
|
|
57
|
+
|
|
58
|
+
if (keybinds !== undefined) nodes.push({ id: keybinds.id, label: keybinds.name });
|
|
59
|
+
|
|
60
|
+
return nodes;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function KeybindsBody({
|
|
64
|
+
sections,
|
|
65
|
+
tokens,
|
|
66
|
+
}: {
|
|
67
|
+
sections: CheatsheetSection[];
|
|
68
|
+
tokens: Theme;
|
|
69
|
+
}): React.ReactNode {
|
|
70
|
+
return (
|
|
71
|
+
<scrollbox style={{ flexGrow: 1 }} focused={false}>
|
|
72
|
+
{sections.map((section) => (
|
|
73
|
+
<box key={section.title} style={{ flexDirection: "column" }}>
|
|
74
|
+
<text fg={tokens.accent}>{section.title}</text>
|
|
75
|
+
{section.entries.map((entry, index) => (
|
|
76
|
+
<text key={`${section.title}-${index}`}>
|
|
77
|
+
<span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
|
|
78
|
+
<span fg={tokens.textMuted}>{entry.label}</span>
|
|
79
|
+
</text>
|
|
80
|
+
))}
|
|
81
|
+
<text> </text>
|
|
82
|
+
</box>
|
|
83
|
+
))}
|
|
84
|
+
</scrollbox>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
40
88
|
export function SettingsDialog({
|
|
41
89
|
isOpen,
|
|
90
|
+
version,
|
|
42
91
|
categories,
|
|
43
92
|
values,
|
|
93
|
+
keybindsSections,
|
|
44
94
|
activeCategoryId,
|
|
45
95
|
activeRowIndex,
|
|
46
96
|
activeZone,
|
|
@@ -56,6 +106,7 @@ export function SettingsDialog({
|
|
|
56
106
|
categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
|
|
57
107
|
|
|
58
108
|
if (!category) return null;
|
|
109
|
+
const onKeybinds = category.id === KEYBINDS_CATEGORY_ID;
|
|
59
110
|
|
|
60
111
|
return (
|
|
61
112
|
<Dialog
|
|
@@ -67,80 +118,76 @@ export function SettingsDialog({
|
|
|
67
118
|
theme={theme}
|
|
68
119
|
>
|
|
69
120
|
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
70
|
-
<box style={{ flexDirection: "column", width:
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}}
|
|
81
|
-
onMouseUp={() => onCategorySelect(candidate.id)}
|
|
82
|
-
>
|
|
83
|
-
<text
|
|
84
|
-
fg={isActive ? tokens.accent : tokens.textMuted}
|
|
85
|
-
>{`${isActive ? "› " : " "}${candidate.name}`}</text>
|
|
86
|
-
</box>
|
|
87
|
-
);
|
|
88
|
-
})}
|
|
121
|
+
<box style={{ flexDirection: "column", width: 20, paddingLeft: 1, paddingRight: 1 }}>
|
|
122
|
+
<Tree
|
|
123
|
+
nodes={navTree(categories)}
|
|
124
|
+
expandedIds={new Set(["settings"])}
|
|
125
|
+
selectedId={category.id}
|
|
126
|
+
hideIcons
|
|
127
|
+
onSelect={onCategorySelect}
|
|
128
|
+
onToggle={onCategorySelect}
|
|
129
|
+
theme={theme}
|
|
130
|
+
/>
|
|
89
131
|
</box>
|
|
90
|
-
<box style={{
|
|
132
|
+
<box style={{ borderStyle: "single", border: ["left"], borderColor: tokens.border }} />
|
|
91
133
|
<box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 1 }}>
|
|
92
134
|
<text fg={tokens.text}>{category.name}</text>
|
|
93
135
|
<text fg={tokens.textDim}>{category.description}</text>
|
|
94
136
|
<box style={{ height: 1 }} />
|
|
95
|
-
{
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
137
|
+
{onKeybinds ? <KeybindsBody sections={keybindsSections} tokens={tokens} /> : null}
|
|
138
|
+
{onKeybinds ? null : category.customBody}
|
|
139
|
+
{onKeybinds
|
|
140
|
+
? null
|
|
141
|
+
: category.rows.map((row, rowIndex) => {
|
|
142
|
+
const isActive = activeZone === "body" && rowIndex === activeRowIndex;
|
|
143
|
+
const value = values[row.key];
|
|
99
144
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
if (row.kind === "toggle") {
|
|
146
|
+
return (
|
|
147
|
+
<ToggleRow
|
|
148
|
+
key={row.key}
|
|
149
|
+
label={row.label}
|
|
150
|
+
value={value === true}
|
|
151
|
+
isActive={isActive}
|
|
152
|
+
onPress={() => onRowActivate(row)}
|
|
153
|
+
theme={theme}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (row.kind === "cycle") {
|
|
158
|
+
return (
|
|
159
|
+
<CycleRow
|
|
160
|
+
key={row.key}
|
|
161
|
+
label={row.label}
|
|
162
|
+
value={String(value ?? "")}
|
|
163
|
+
isActive={isActive}
|
|
164
|
+
onPress={() => onRowActivate(row)}
|
|
165
|
+
theme={theme}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
124
169
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
170
|
+
return (
|
|
171
|
+
<TextRow
|
|
172
|
+
key={row.key}
|
|
173
|
+
label={row.label}
|
|
174
|
+
value={String(value ?? "")}
|
|
175
|
+
isActive={isActive}
|
|
176
|
+
onPress={() => onRowActivate(row)}
|
|
177
|
+
theme={theme}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
136
181
|
</box>
|
|
137
182
|
</box>
|
|
138
|
-
<box style={{ flexDirection: "row", height: 1, paddingLeft: 1 }}>
|
|
183
|
+
<box style={{ flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1 }}>
|
|
139
184
|
<text fg={tokens.textDim}>
|
|
140
185
|
{activeZone === "nav"
|
|
141
|
-
? "j/k
|
|
186
|
+
? "j/k nav · l/tab open · esc close"
|
|
142
187
|
: "j/k row · l/space change · h back · esc close"}
|
|
143
188
|
</text>
|
|
189
|
+
<box style={{ flexGrow: 1 }} />
|
|
190
|
+
<text fg={tokens.textDim}>{`cueloop v${version}`}</text>
|
|
144
191
|
</box>
|
|
145
192
|
</Dialog>
|
|
146
193
|
);
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { CycleRow, TextRow, ToggleRow } from "./SettingsRows";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "SettingsRows" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/SettingsRows" };
|
|
7
7
|
|
|
8
8
|
export const AllKinds: Story = {
|
|
9
9
|
render: () => (
|