@cueloop/client 0.1.0-alpha.64 → 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.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +8 -73
- package/src/App.export.test.tsx +2 -2
- package/src/App.plan-review.test.tsx +60 -412
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +85 -62
- package/src/App.rendering.test.tsx +5 -6
- package/src/App.submit-confirm.test.tsx +32 -70
- package/src/App.test.tsx +136 -66
- package/src/App.toast.test.tsx +17 -8
- package/src/App.tsx +243 -284
- package/src/App.walk.test.tsx +1 -1
- package/src/app-screens.tsx +67 -52
- package/src/app-view-model.ts +6 -100
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AnnotationCard.tsx +2 -2
- 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/KeybindsDialog.tsx +61 -17
- 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 +15 -1
- package/src/components/ReviewRail.stories.tsx +15 -1
- package/src/components/ReviewRail.tsx +13 -6
- 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.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +1708 -0
- 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 +106 -0
- package/src/components/TreePane.tsx +156 -0
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +463 -305
- 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/diff-hunk-curate.ts +4 -104
- package/src/index.ts +1 -0
- package/src/intent-dispatch.test.ts +136 -3
- package/src/intent-dispatch.ts +92 -24
- package/src/key-bindings.test.ts +1 -1
- package/src/keymap.test.ts +4 -4
- package/src/keymap.ts +14 -7
- package/src/session-controller.curate.test.ts +89 -2
- package/src/session-controller.share.test.ts +195 -55
- package/src/session-controller.tree.test.ts +228 -0
- package/src/session-controller.ts +331 -72
- package/src/share.ts +114 -16
- package/src/test-support.test.ts +193 -1
- package/src/test-support.ts +64 -0
- package/src/theme.ts +4 -4
- package/src/thread-chords.test.ts +154 -0
- package/src/thread-chords.ts +144 -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-settings-dialog.tsx +9 -0
- package/src/view-plan.ts +29 -12
- package/src/App.review-panel.test.tsx +0 -143
- package/src/components/PlanSheet.stories.tsx +0 -113
- package/src/components/PlanSheet.tsx +0 -419
- package/src/components/plan-sheet-run-style.ts +0 -88
|
@@ -5,10 +5,24 @@ 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
|
+
|
|
10
|
+
/** An empty Tree tab: the stories here show the review tab. */
|
|
11
|
+
const tree = {
|
|
12
|
+
rows: [],
|
|
13
|
+
canMove: true,
|
|
14
|
+
canFork: true,
|
|
15
|
+
onSelect: () => {},
|
|
16
|
+
onGo: () => {},
|
|
17
|
+
onBranch: () => {},
|
|
18
|
+
onLabel: () => {},
|
|
19
|
+
onFork: () => {},
|
|
20
|
+
onForkAndShare: () => {},
|
|
21
|
+
};
|
|
9
22
|
|
|
10
23
|
const rail = {
|
|
11
24
|
session: fixturePlanSession(),
|
|
25
|
+
tree,
|
|
12
26
|
authorNames: {},
|
|
13
27
|
selectedId: FIXTURE_ANNOTATIONS[0]!.id,
|
|
14
28
|
resolvedIds: new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id)),
|
|
@@ -4,10 +4,24 @@ 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
|
+
|
|
9
|
+
/** An empty Tree tab: the stories here show the review tab. */
|
|
10
|
+
const tree = {
|
|
11
|
+
rows: [],
|
|
12
|
+
canMove: true,
|
|
13
|
+
canFork: true,
|
|
14
|
+
onSelect: () => {},
|
|
15
|
+
onGo: () => {},
|
|
16
|
+
onBranch: () => {},
|
|
17
|
+
onLabel: () => {},
|
|
18
|
+
onFork: () => {},
|
|
19
|
+
onForkAndShare: () => {},
|
|
20
|
+
};
|
|
8
21
|
|
|
9
22
|
const callbacks = {
|
|
10
23
|
curationItems: [],
|
|
24
|
+
tree,
|
|
11
25
|
onTabChange: () => {},
|
|
12
26
|
onSelectCard: () => {},
|
|
13
27
|
onActivateCard: () => {},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The right review rail: tab strip (Review/Agent), the scrollable annotation
|
|
2
|
+
* The right review rail: tab strip (Review/Tree/Agent), the scrollable annotation
|
|
3
3
|
* stack, and the pinned submit affordance. The confirm card sits OUTSIDE the
|
|
4
4
|
* scrollbox so the stack scrolls while the card stays at the rail bottom.
|
|
5
5
|
* Card reveal goes through the scrollbox's own scrollChildIntoView - no
|
|
@@ -18,11 +18,12 @@ import { Toolbar } from "./primitives/Toolbar";
|
|
|
18
18
|
import { AnnotationCard, type AnnotationDraft } from "./AnnotationCard";
|
|
19
19
|
import { ConfirmCard, type ConfirmCardProps } from "./ConfirmCard";
|
|
20
20
|
import { AgentLauncher, type AgentTerminalHandle } from "./agent-launcher";
|
|
21
|
+
import { TreePane, type TreePaneProps } from "./TreePane";
|
|
21
22
|
import { Card } from "./primitives/Card";
|
|
22
23
|
import { truncateToSingleLine } from "./truncate-text";
|
|
23
24
|
import { resolveDisplayName } from "../attribution";
|
|
24
25
|
|
|
25
|
-
export type RailTab = "review" | "agent";
|
|
26
|
+
export type RailTab = "review" | "agent" | "tree";
|
|
26
27
|
|
|
27
28
|
export interface RailCardEdit extends AnnotationDraft {
|
|
28
29
|
id: string;
|
|
@@ -65,6 +66,8 @@ export interface ReviewRailProps {
|
|
|
65
66
|
onLaunchHarness: (command: string, seedText?: string) => void;
|
|
66
67
|
/** In-tab agent terminal handle (embedded path); null when detached. Lets the app route keys to it. */
|
|
67
68
|
onAgentTerminal?: (handle: AgentTerminalHandle | null) => void;
|
|
69
|
+
/** The Tree tab: the session's history rows and the primitives that move it. */
|
|
70
|
+
tree: Omit<TreePaneProps, "theme">;
|
|
68
71
|
/** Rail column width; the app derives it from the persisted review layout. */
|
|
69
72
|
width?: number;
|
|
70
73
|
/**
|
|
@@ -95,6 +98,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
95
98
|
onSubmitRequest,
|
|
96
99
|
onLaunchHarness,
|
|
97
100
|
onAgentTerminal,
|
|
101
|
+
tree,
|
|
98
102
|
width = 34,
|
|
99
103
|
onCollapse,
|
|
100
104
|
theme,
|
|
@@ -188,16 +192,19 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
188
192
|
<Tabs
|
|
189
193
|
selectedKey={railTab}
|
|
190
194
|
onSelectionChange={(key) => {
|
|
191
|
-
if (key === "review" || key === "agent") onTabChange(key);
|
|
195
|
+
if (key === "review" || key === "agent" || key === "tree") onTabChange(key);
|
|
192
196
|
}}
|
|
193
197
|
theme={theme}
|
|
194
198
|
>
|
|
195
199
|
<TabList>
|
|
196
200
|
<Tab id="review">Review</Tab>
|
|
201
|
+
<Tab id="tree">Tree</Tab>
|
|
197
202
|
<Tab id="agent">Agent</Tab>
|
|
198
203
|
</TabList>
|
|
199
204
|
</Tabs>
|
|
200
|
-
{railTab === "
|
|
205
|
+
{railTab === "tree" ? (
|
|
206
|
+
<TreePane {...tree} theme={theme} />
|
|
207
|
+
) : railTab === "agent" ? (
|
|
201
208
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
202
209
|
<AgentLauncher
|
|
203
210
|
session={session}
|
|
@@ -223,7 +230,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
223
230
|
</box>
|
|
224
231
|
)}
|
|
225
232
|
{/* the confirm card keeps full width, above the footer row */}
|
|
226
|
-
{railTab
|
|
233
|
+
{railTab === "review" && submitConfirm ? (
|
|
227
234
|
<ConfirmCard {...submitConfirm} theme={theme} />
|
|
228
235
|
) : null}
|
|
229
236
|
{/* footer row: collapse chevron left-bound, the submit affordance inline
|
|
@@ -234,7 +241,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
234
241
|
<text fg={tokens.textDim}>{">"}</text>
|
|
235
242
|
</box>
|
|
236
243
|
) : null}
|
|
237
|
-
{railTab
|
|
244
|
+
{railTab === "review" && !submitConfirm ? (
|
|
238
245
|
session.status === "resolved" ? (
|
|
239
246
|
<text fg={tokens.green}>resolved: {session.verdict!.kind.replace("_", " ")}</text>
|
|
240
247
|
) : (
|
|
@@ -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: () => (
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ShellHeader } from "./ShellHeader";
|
|
5
|
+
import { NERD } from "./primitives/icons";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Layout/ShellHeader" };
|
|
8
|
+
|
|
9
|
+
function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
10
|
+
return <box style={{ width: "100%", backgroundColor: DARK.panel }}>{children}</box>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ReviewSurface: Story = {
|
|
14
|
+
render: () => (
|
|
15
|
+
<Frame>
|
|
16
|
+
<ShellHeader
|
|
17
|
+
leftIcons={[{ glyph: NERD.settings }]}
|
|
18
|
+
leftLabel="Read Cueloop Repository"
|
|
19
|
+
tabs={[{ label: "Changes", active: true }]}
|
|
20
|
+
rightIcons={[
|
|
21
|
+
{ glyph: NERD.expand },
|
|
22
|
+
{ glyph: NERD.diff, active: true },
|
|
23
|
+
{ glyph: NERD.listTree },
|
|
24
|
+
]}
|
|
25
|
+
onToggleSidebar={() => {}}
|
|
26
|
+
onToggleInspector={() => {}}
|
|
27
|
+
/>
|
|
28
|
+
</Frame>
|
|
29
|
+
),
|
|
30
|
+
expectedColors: [DARK.accent, DARK.textMuted],
|
|
31
|
+
size: { width: 120, height: 3 },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const InspectorCollapsed: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<Frame>
|
|
37
|
+
<ShellHeader
|
|
38
|
+
leftIcons={[{ glyph: NERD.settings }]}
|
|
39
|
+
leftLabel="Read Cueloop Repository"
|
|
40
|
+
rightIcons={[{ glyph: NERD.search }, { glyph: NERD.expand }]}
|
|
41
|
+
onToggleSidebar={() => {}}
|
|
42
|
+
sidebarOpen={false}
|
|
43
|
+
onToggleInspector={() => {}}
|
|
44
|
+
inspectorOpen={false}
|
|
45
|
+
/>
|
|
46
|
+
</Frame>
|
|
47
|
+
),
|
|
48
|
+
expectedColors: [DARK.textMuted],
|
|
49
|
+
size: { width: 120, height: 3 },
|
|
50
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// The app shell's top bar: left panel toggle + icons + label, an optional title, right icons + panel toggle.
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { DARK, type Theme } from "../theme";
|
|
5
|
+
import { NERD } from "./primitives/icons";
|
|
6
|
+
|
|
7
|
+
export interface ShellTab {
|
|
8
|
+
label: string;
|
|
9
|
+
active?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// A header icon; active paints it in the brand accent to mark the current view or mode.
|
|
13
|
+
export interface ShellIcon {
|
|
14
|
+
glyph: string;
|
|
15
|
+
active?: boolean;
|
|
16
|
+
onPress?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ShellHeaderProps {
|
|
20
|
+
leftIcons?: readonly ShellIcon[];
|
|
21
|
+
leftLabel?: string;
|
|
22
|
+
titleIcon?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
tabs?: readonly ShellTab[];
|
|
25
|
+
rightIcons?: readonly ShellIcon[];
|
|
26
|
+
sidebarOpen?: boolean;
|
|
27
|
+
onToggleSidebar?: () => void;
|
|
28
|
+
inspectorOpen?: boolean;
|
|
29
|
+
onToggleInspector?: () => void;
|
|
30
|
+
theme?: Theme;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function IconView({
|
|
34
|
+
icon,
|
|
35
|
+
tokens,
|
|
36
|
+
marginRight,
|
|
37
|
+
}: {
|
|
38
|
+
icon: ShellIcon;
|
|
39
|
+
tokens: Theme;
|
|
40
|
+
marginRight: number;
|
|
41
|
+
}): React.ReactNode {
|
|
42
|
+
const color = icon.active ? tokens.accent : tokens.textMuted;
|
|
43
|
+
|
|
44
|
+
// alignSelf center keeps the clickable box on the same row as sibling icons
|
|
45
|
+
return (
|
|
46
|
+
<box onMouseUp={icon.onPress} style={{ alignSelf: "center", marginRight }}>
|
|
47
|
+
<text fg={color}>{icon.glyph}</text>
|
|
48
|
+
</box>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ShellHeader({
|
|
53
|
+
leftIcons = [],
|
|
54
|
+
leftLabel,
|
|
55
|
+
titleIcon,
|
|
56
|
+
title,
|
|
57
|
+
tabs = [],
|
|
58
|
+
rightIcons = [],
|
|
59
|
+
sidebarOpen = true,
|
|
60
|
+
onToggleSidebar,
|
|
61
|
+
inspectorOpen = true,
|
|
62
|
+
onToggleInspector,
|
|
63
|
+
theme,
|
|
64
|
+
}: ShellHeaderProps): React.ReactNode {
|
|
65
|
+
const tokens = theme ?? DARK;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<box
|
|
69
|
+
style={{
|
|
70
|
+
flexDirection: "row",
|
|
71
|
+
height: "100%",
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
paddingLeft: 2,
|
|
74
|
+
paddingRight: 2,
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<box style={{ flexDirection: "row", alignItems: "center" }}>
|
|
78
|
+
{onToggleSidebar !== undefined ? (
|
|
79
|
+
<IconView
|
|
80
|
+
icon={{
|
|
81
|
+
glyph: sidebarOpen ? NERD.sidebarLeft : NERD.sidebarLeftOff,
|
|
82
|
+
onPress: onToggleSidebar,
|
|
83
|
+
}}
|
|
84
|
+
tokens={tokens}
|
|
85
|
+
marginRight={3}
|
|
86
|
+
/>
|
|
87
|
+
) : null}
|
|
88
|
+
{leftIcons.map((icon, index) => (
|
|
89
|
+
<IconView
|
|
90
|
+
key={`left-${index}-${icon.glyph}`}
|
|
91
|
+
icon={icon}
|
|
92
|
+
tokens={tokens}
|
|
93
|
+
marginRight={3}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
{leftLabel !== undefined ? (
|
|
97
|
+
<text fg={tokens.textMuted}>
|
|
98
|
+
{leftLabel}
|
|
99
|
+
{" "}
|
|
100
|
+
</text>
|
|
101
|
+
) : null}
|
|
102
|
+
{titleIcon !== undefined ? <text fg={tokens.textDim}>{titleIcon} </text> : null}
|
|
103
|
+
{title !== undefined ? <text fg={tokens.text}>{title}</text> : null}
|
|
104
|
+
</box>
|
|
105
|
+
<box style={{ flexGrow: 1 }} />
|
|
106
|
+
<box style={{ flexDirection: "row", alignItems: "center" }}>
|
|
107
|
+
{tabs.map((tab, index) => (
|
|
108
|
+
<text
|
|
109
|
+
key={`tab-${index}-${tab.label}`}
|
|
110
|
+
fg={tab.active ? tokens.accent : tokens.textMuted}
|
|
111
|
+
>
|
|
112
|
+
{tab.label}
|
|
113
|
+
{" "}
|
|
114
|
+
</text>
|
|
115
|
+
))}
|
|
116
|
+
{rightIcons.map((icon, index) => (
|
|
117
|
+
<IconView
|
|
118
|
+
key={`right-${index}-${icon.glyph}`}
|
|
119
|
+
icon={icon}
|
|
120
|
+
tokens={tokens}
|
|
121
|
+
marginRight={3}
|
|
122
|
+
/>
|
|
123
|
+
))}
|
|
124
|
+
{onToggleInspector !== undefined ? (
|
|
125
|
+
<IconView
|
|
126
|
+
icon={{
|
|
127
|
+
glyph: inspectorOpen ? NERD.sidebarRight : NERD.sidebarRightOff,
|
|
128
|
+
onPress: onToggleInspector,
|
|
129
|
+
}}
|
|
130
|
+
tokens={tokens}
|
|
131
|
+
marginRight={0}
|
|
132
|
+
/>
|
|
133
|
+
) : null}
|
|
134
|
+
</box>
|
|
135
|
+
</box>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ThreadFooter } from "./ThreadFooter";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/ThreadFooter" };
|
|
7
|
+
|
|
8
|
+
function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
9
|
+
return <box style={{ width: "100%", backgroundColor: DARK.panel }}>{children}</box>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Default: Story = {
|
|
13
|
+
render: () => (
|
|
14
|
+
<Frame>
|
|
15
|
+
<ThreadFooter repo="cueloop" branch="main" onSubmit={() => {}} />
|
|
16
|
+
</Frame>
|
|
17
|
+
),
|
|
18
|
+
expectedColors: [DARK.accent, DARK.textMuted],
|
|
19
|
+
size: { width: 80, height: 1 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const ReadOnly: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<Frame>
|
|
25
|
+
<ThreadFooter repo="cueloop" branch="design/tui" canSubmit={false} onSubmit={() => {}} />
|
|
26
|
+
</Frame>
|
|
27
|
+
),
|
|
28
|
+
expectedColors: [DARK.textDim],
|
|
29
|
+
size: { width: 80, height: 1 },
|
|
30
|
+
};
|