@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.66
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 +5 -5
- package/src/App.diff.test.tsx +8 -73
- package/src/App.plan-review.test.tsx +4 -185
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +6 -28
- package/src/App.submit-confirm.test.tsx +19 -72
- package/src/App.test.tsx +70 -24
- package/src/App.tsx +177 -254
- package/src/App.walk.test.tsx +1 -1
- package/src/app-screens.tsx +40 -51
- package/src/app-view-model.ts +2 -4
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AppShell.stories.tsx +84 -0
- package/src/components/AppShell.tsx +127 -0
- package/src/components/Breadcrumb.stories.tsx +1 -1
- package/src/components/ChangesColumn.stories.tsx +39 -0
- package/src/components/ChangesColumn.tsx +147 -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.stories.tsx +1 -1
- package/src/components/InboxList.stories.tsx +23 -8
- package/src/components/InboxList.tsx +45 -19
- 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/PromptDialog.stories.tsx +1 -1
- package/src/components/PrototypeSheet.stories.tsx +1 -1
- package/src/components/ReviewPanel.stories.tsx +1 -1
- package/src/components/ReviewRail.stories.tsx +1 -1
- package/src/components/SettingsDialog.stories.tsx +7 -1
- package/src/components/SettingsDialog.tsx +112 -66
- 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/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.tsx +49 -0
- package/src/components/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +1 -1
- package/src/components/ThreadView.tsx +99 -11
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +57 -0
- package/src/components/Toast.stories.tsx +1 -1
- package/src/components/TreePane.stories.tsx +1 -1
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +348 -138
- package/src/components/agent-launcher.stories.tsx +1 -1
- package/src/components/file-tree.test.ts +45 -0
- package/src/components/file-tree.ts +57 -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 +43 -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 +87 -0
- package/src/components/primitives/icons.ts +38 -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 +114 -0
- package/src/components/session-tree.ts +94 -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/session-controller.ts +9 -4
- package/src/theme.ts +4 -4
- package/src/use-settings-dialog.tsx +9 -0
- package/src/App.review-panel.test.tsx +0 -140
- package/src/App.tree.test.tsx +0 -224
|
@@ -2,9 +2,10 @@ import React from "react";
|
|
|
2
2
|
import { DARK } from "../theme";
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { InboxList } from "./InboxList";
|
|
5
|
+
import { groupInbox } from "./session-tree";
|
|
5
6
|
import { fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
|
|
6
7
|
|
|
7
|
-
export const meta: StoryMeta = { title: "InboxList" };
|
|
8
|
+
export const meta: StoryMeta = { title: "Surfaces/InboxList" };
|
|
8
9
|
|
|
9
10
|
/** InboxList is the body under App's header/menu chrome; a frame gives it height. */
|
|
10
11
|
function BodyFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
@@ -12,19 +13,33 @@ function BodyFrame({ children }: { children: React.ReactNode }): React.ReactNode
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export const TwoPending: Story = {
|
|
15
|
-
render: () =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
render: () => {
|
|
17
|
+
const plan = fixturePlanSession();
|
|
18
|
+
// one repo-bound thread (a project) and one standalone thread
|
|
19
|
+
const projectPlan = {
|
|
20
|
+
...plan,
|
|
21
|
+
workspace: {
|
|
22
|
+
...plan.workspace,
|
|
23
|
+
rootCommit: "root-1",
|
|
24
|
+
remote: "git@github.com:acme/widget.git",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const { rows } = groupInbox([projectPlan, fixtureDiffSession()]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<BodyFrame>
|
|
31
|
+
<InboxList rows={rows} cursor={0} />
|
|
32
|
+
</BodyFrame>
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
expectedColors: [DARK.accent],
|
|
21
36
|
size: { width: 80, height: 12 },
|
|
22
37
|
};
|
|
23
38
|
|
|
24
39
|
export const Empty: Story = {
|
|
25
40
|
render: () => (
|
|
26
41
|
<BodyFrame>
|
|
27
|
-
<InboxList
|
|
42
|
+
<InboxList rows={[]} cursor={0} />
|
|
28
43
|
</BodyFrame>
|
|
29
44
|
),
|
|
30
45
|
size: { width: 80, height: 8 },
|
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pending-review inbox: a
|
|
3
|
-
* the keyboard grammar
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* The pending-review inbox: a Projects and Threads tree. Selection stays with
|
|
3
|
+
* the keyboard grammar - the cursor indexes the flat thread order and this
|
|
4
|
+
* component only renders the snapshot. The selected thread carries a [delete]
|
|
5
|
+
* word-button (click, or `d` in the grammar). App supplies the surrounding
|
|
6
|
+
* header and MenuBar chrome; this is the flex-growing body.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React from "react";
|
|
10
|
-
import type { ReviewSession } from "@cueloop/schema";
|
|
11
10
|
import type { Theme } from "../theme";
|
|
12
11
|
import { useComponentTheme } from "./theme-context";
|
|
13
12
|
import { Button } from "./primitives/Button";
|
|
13
|
+
import { NERD } from "./primitives/icons";
|
|
14
|
+
import type { InboxRow } from "./session-tree";
|
|
14
15
|
|
|
15
16
|
export interface InboxListProps {
|
|
16
|
-
|
|
17
|
+
rows: InboxRow[];
|
|
17
18
|
cursor: number;
|
|
18
|
-
/**
|
|
19
|
+
/** The open thread's id; highlights it instead of the cursor (the left sidebar case). */
|
|
20
|
+
activeId?: string;
|
|
21
|
+
/** Open a thread by clicking its row (the left sidebar case). */
|
|
22
|
+
onSelect?: (sessionId: string) => void;
|
|
23
|
+
/** Ask to delete a thread (the selected row's [delete] button). */
|
|
19
24
|
onRequestDelete?: (id: string, title: string) => void;
|
|
20
25
|
theme?: Theme;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export function InboxList({
|
|
24
|
-
|
|
29
|
+
rows,
|
|
25
30
|
cursor,
|
|
31
|
+
activeId,
|
|
32
|
+
onSelect,
|
|
26
33
|
onRequestDelete,
|
|
27
34
|
theme,
|
|
28
35
|
}: InboxListProps): React.ReactNode {
|
|
@@ -30,28 +37,47 @@ export function InboxList({
|
|
|
30
37
|
|
|
31
38
|
return (
|
|
32
39
|
<box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
33
|
-
{
|
|
40
|
+
{rows.length === 0 ? (
|
|
34
41
|
<text fg={tokens.textDim}>nothing waiting for review</text>
|
|
35
42
|
) : (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
rows.map((row) => {
|
|
44
|
+
if (row.kind === "section") {
|
|
45
|
+
return (
|
|
46
|
+
<text key={row.id} fg={tokens.textDim}>
|
|
47
|
+
{row.label}
|
|
48
|
+
</text>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (row.kind === "project") {
|
|
53
|
+
return (
|
|
54
|
+
<text key={row.id}>
|
|
55
|
+
<span fg={tokens.blue}>{` ${NERD.folderOpen} `}</span>
|
|
56
|
+
<span fg={tokens.textMuted}>{row.label}</span>
|
|
57
|
+
</text>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const selected =
|
|
62
|
+
activeId !== undefined ? activeId === row.session.id : row.selectionIndex === cursor;
|
|
63
|
+
const title = row.session.artifact.meta.title ?? row.session.id;
|
|
39
64
|
|
|
40
65
|
return (
|
|
41
66
|
<box
|
|
42
|
-
key={
|
|
67
|
+
key={row.id}
|
|
68
|
+
onMouseUp={onSelect ? () => onSelect(row.session.id) : undefined}
|
|
43
69
|
style={{
|
|
44
70
|
flexDirection: "row",
|
|
45
|
-
backgroundColor: selected ? tokens.
|
|
71
|
+
backgroundColor: selected ? tokens.elevated : undefined,
|
|
46
72
|
}}
|
|
47
73
|
>
|
|
48
|
-
<text fg={selected ? tokens.
|
|
49
|
-
{
|
|
50
|
-
{title}
|
|
74
|
+
<text fg={selected ? tokens.accent : tokens.textMuted}>
|
|
75
|
+
{" "}
|
|
76
|
+
{title}
|
|
51
77
|
</text>
|
|
52
78
|
<box style={{ flexGrow: 1 }} />
|
|
53
79
|
{selected && onRequestDelete ? (
|
|
54
|
-
<Button onPress={() => onRequestDelete(session.id, title)} theme={theme}>
|
|
80
|
+
<Button onPress={() => onRequestDelete(row.session.id, title)} theme={theme}>
|
|
55
81
|
{" [delete] "}
|
|
56
82
|
</Button>
|
|
57
83
|
) : null}
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { KeybindsDialog } from "./KeybindsDialog";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "KeybindsDialog" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Overlays/KeybindsDialog" };
|
|
7
7
|
|
|
8
8
|
const SECTIONS = [
|
|
9
9
|
{
|
|
@@ -4,7 +4,7 @@ import { DEFAULT_QUICK_ACTIONS } from "../config";
|
|
|
4
4
|
import type { Story, StoryMeta } from "./story";
|
|
5
5
|
import { MarkerPopover } from "./MarkerPopover";
|
|
6
6
|
|
|
7
|
-
export const meta: StoryMeta = { title: "MarkerPopover" };
|
|
7
|
+
export const meta: StoryMeta = { title: "Overlays/MarkerPopover" };
|
|
8
8
|
|
|
9
9
|
const callbacks = {
|
|
10
10
|
onComment: () => {},
|
|
@@ -3,9 +3,9 @@ import { DARK } from "../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { MenuBar } from "./MenuBar";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "MenuBar" };
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/MenuBar" };
|
|
7
7
|
|
|
8
|
-
const callbacks = {
|
|
8
|
+
const callbacks = { onSettings: () => {}, onKeybinds: () => {} };
|
|
9
9
|
|
|
10
10
|
function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
11
11
|
return (
|
|
@@ -17,19 +17,10 @@ function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNo
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export const Closed: Story = {
|
|
21
|
-
render: () => (
|
|
22
|
-
<BottomFrame>
|
|
23
|
-
<MenuBar open={false} version="0.1.0-alpha.32" {...callbacks} />
|
|
24
|
-
</BottomFrame>
|
|
25
|
-
),
|
|
26
|
-
size: { width: 60, height: 8 },
|
|
27
|
-
};
|
|
28
|
-
|
|
29
20
|
export const Open: Story = {
|
|
30
21
|
render: () => (
|
|
31
22
|
<BottomFrame>
|
|
32
|
-
<MenuBar open
|
|
23
|
+
<MenuBar open {...callbacks} />
|
|
33
24
|
</BottomFrame>
|
|
34
25
|
),
|
|
35
26
|
expectedColors: [DARK.text],
|
|
@@ -1,75 +1,44 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { useTerminalDimensions } from "@opentui/react";
|
|
3
2
|
import type { Theme } from "../theme";
|
|
4
3
|
import { useComponentTheme } from "./theme-context";
|
|
5
4
|
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
6
5
|
|
|
7
6
|
export interface MenuBarProps {
|
|
8
7
|
open: boolean;
|
|
9
|
-
version: string;
|
|
10
|
-
/** The transient status line, shown between the menu and the version. */
|
|
11
|
-
status?: string;
|
|
12
|
-
onToggle: () => void;
|
|
13
8
|
onSettings: () => void;
|
|
14
9
|
onKeybinds: () => void;
|
|
15
10
|
theme?: Theme;
|
|
16
11
|
}
|
|
17
12
|
|
|
18
|
-
const
|
|
13
|
+
const DROPDOWN_OPTION_ROWS = 2;
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
version,
|
|
23
|
-
status,
|
|
24
|
-
onToggle,
|
|
25
|
-
onSettings,
|
|
26
|
-
onKeybinds,
|
|
27
|
-
theme,
|
|
28
|
-
}: MenuBarProps): React.ReactNode {
|
|
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 {
|
|
29
17
|
const tokens = useComponentTheme(theme);
|
|
30
|
-
const
|
|
31
|
-
|
|
18
|
+
const dropdownHeight = DROPDOWN_OPTION_ROWS + 2;
|
|
19
|
+
|
|
20
|
+
if (!open) return null;
|
|
32
21
|
|
|
33
22
|
return (
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<box onMouseUp={onKeybinds}>
|
|
53
|
-
<text fg={tokens.text}> Keybinds</text>
|
|
54
|
-
</box>
|
|
55
|
-
</box>
|
|
56
|
-
) : null}
|
|
57
|
-
<box
|
|
58
|
-
style={{
|
|
59
|
-
flexDirection: "row",
|
|
60
|
-
height: 1,
|
|
61
|
-
backgroundColor: tokens.panel,
|
|
62
|
-
paddingLeft: 1,
|
|
63
|
-
paddingRight: 1,
|
|
64
|
-
}}
|
|
65
|
-
>
|
|
66
|
-
<box onMouseUp={onToggle}>
|
|
67
|
-
<text fg={tokens.textDim}>menu</text>
|
|
68
|
-
</box>
|
|
69
|
-
{status ? <text fg={tokens.accent}>{` ${status}`}</text> : null}
|
|
70
|
-
<box style={{ flexGrow: 1 }} />
|
|
71
|
-
<text fg={tokens.textDim}>v{version}</text>
|
|
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>
|
|
72
41
|
</box>
|
|
73
|
-
|
|
42
|
+
</box>
|
|
74
43
|
);
|
|
75
44
|
}
|
|
@@ -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: () => (
|
|
@@ -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 = {
|
|
@@ -5,7 +5,7 @@ import type { Story, StoryMeta } from "./story";
|
|
|
5
5
|
import { ReviewPanel } from "./ReviewPanel";
|
|
6
6
|
import { FIXTURE_ANNOTATIONS, fixturePlanSession } from "./story-fixtures";
|
|
7
7
|
|
|
8
|
-
export const meta: StoryMeta = { title: "ReviewPanel" };
|
|
8
|
+
export const meta: StoryMeta = { title: "Surfaces/ReviewPanel" };
|
|
9
9
|
|
|
10
10
|
/** An empty Tree tab: the stories here show the review tab. */
|
|
11
11
|
const tree = {
|
|
@@ -4,7 +4,7 @@ import type { Story, StoryMeta } from "./story";
|
|
|
4
4
|
import { ReviewRail } from "./ReviewRail";
|
|
5
5
|
import { FIXTURE_ANNOTATIONS, fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
|
|
6
6
|
|
|
7
|
-
export const meta: StoryMeta = { title: "ReviewRail" };
|
|
7
|
+
export const meta: StoryMeta = { title: "Surfaces/ReviewRail" };
|
|
8
8
|
|
|
9
9
|
/** An empty Tree tab: the stories here show the review tab. */
|
|
10
10
|
const tree = {
|
|
@@ -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,75 @@ 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
|
-
backgroundColor: isActive && activeZone === "nav" ? tokens.border : undefined,
|
|
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
|
+
onSelect={onCategorySelect}
|
|
127
|
+
onToggle={onCategorySelect}
|
|
128
|
+
theme={theme}
|
|
129
|
+
/>
|
|
89
130
|
</box>
|
|
90
131
|
<box style={{ width: 1, backgroundColor: tokens.border }} />
|
|
91
132
|
<box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 1 }}>
|
|
92
133
|
<text fg={tokens.text}>{category.name}</text>
|
|
93
134
|
<text fg={tokens.textDim}>{category.description}</text>
|
|
94
135
|
<box style={{ height: 1 }} />
|
|
95
|
-
{
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
136
|
+
{onKeybinds ? <KeybindsBody sections={keybindsSections} tokens={tokens} /> : null}
|
|
137
|
+
{onKeybinds ? null : category.customBody}
|
|
138
|
+
{onKeybinds
|
|
139
|
+
? null
|
|
140
|
+
: category.rows.map((row, rowIndex) => {
|
|
141
|
+
const isActive = activeZone === "body" && rowIndex === activeRowIndex;
|
|
142
|
+
const value = values[row.key];
|
|
99
143
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
if (row.kind === "toggle") {
|
|
145
|
+
return (
|
|
146
|
+
<ToggleRow
|
|
147
|
+
key={row.key}
|
|
148
|
+
label={row.label}
|
|
149
|
+
value={value === true}
|
|
150
|
+
isActive={isActive}
|
|
151
|
+
onPress={() => onRowActivate(row)}
|
|
152
|
+
theme={theme}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (row.kind === "cycle") {
|
|
157
|
+
return (
|
|
158
|
+
<CycleRow
|
|
159
|
+
key={row.key}
|
|
160
|
+
label={row.label}
|
|
161
|
+
value={String(value ?? "")}
|
|
162
|
+
isActive={isActive}
|
|
163
|
+
onPress={() => onRowActivate(row)}
|
|
164
|
+
theme={theme}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
124
168
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
169
|
+
return (
|
|
170
|
+
<TextRow
|
|
171
|
+
key={row.key}
|
|
172
|
+
label={row.label}
|
|
173
|
+
value={String(value ?? "")}
|
|
174
|
+
isActive={isActive}
|
|
175
|
+
onPress={() => onRowActivate(row)}
|
|
176
|
+
theme={theme}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
136
180
|
</box>
|
|
137
181
|
</box>
|
|
138
|
-
<box style={{ flexDirection: "row", height: 1, paddingLeft: 1 }}>
|
|
182
|
+
<box style={{ flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1 }}>
|
|
139
183
|
<text fg={tokens.textDim}>
|
|
140
184
|
{activeZone === "nav"
|
|
141
|
-
? "j/k
|
|
185
|
+
? "j/k nav · l/tab open · esc close"
|
|
142
186
|
: "j/k row · l/space change · h back · esc close"}
|
|
143
187
|
</text>
|
|
188
|
+
<box style={{ flexGrow: 1 }} />
|
|
189
|
+
<text fg={tokens.textDim}>{`cueloop v${version}`}</text>
|
|
144
190
|
</box>
|
|
145
191
|
</Dialog>
|
|
146
192
|
);
|
|
@@ -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: () => (
|