@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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fits a thread title onto one line, fading the last few characters toward the
|
|
3
|
+
* background instead of cutting hard or wrapping - a gradient mask on the right.
|
|
4
|
+
* Returns colored segments the caller renders as spans.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface TitleSegment {
|
|
8
|
+
text: string;
|
|
9
|
+
fg: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function channel(hex: string, start: number): number {
|
|
13
|
+
return Number.parseInt(hex.slice(start, start + 2), 16);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Blend two #rrggbb colors; t=0 is `from`, t=1 is `to`. */
|
|
17
|
+
export function mixHex(from: string, to: string, t: number): string {
|
|
18
|
+
const clamp = Math.max(0, Math.min(1, t));
|
|
19
|
+
const parts = [1, 3, 5].map((start) => {
|
|
20
|
+
const value = Math.round(
|
|
21
|
+
channel(from, start) + (channel(to, start) - channel(from, start)) * clamp,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return value.toString(16).padStart(2, "0");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return `#${parts.join("")}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The tail length that fades out; kept short so most of the title stays legible. */
|
|
31
|
+
const FADE_WIDTH = 5;
|
|
32
|
+
|
|
33
|
+
export function fadeTitle(
|
|
34
|
+
title: string,
|
|
35
|
+
maxWidth: number,
|
|
36
|
+
baseColor: string,
|
|
37
|
+
fadeColor: string,
|
|
38
|
+
): TitleSegment[] {
|
|
39
|
+
const oneLine = title.replace(/\n/g, " ");
|
|
40
|
+
|
|
41
|
+
if (maxWidth <= 0) return [];
|
|
42
|
+
if (oneLine.length <= maxWidth) return [{ text: oneLine, fg: baseColor }];
|
|
43
|
+
|
|
44
|
+
const clipped = oneLine.slice(0, maxWidth);
|
|
45
|
+
const fadeLength = Math.min(FADE_WIDTH, maxWidth);
|
|
46
|
+
const head = clipped.slice(0, maxWidth - fadeLength);
|
|
47
|
+
const segments: TitleSegment[] = head.length > 0 ? [{ text: head, fg: baseColor }] : [];
|
|
48
|
+
|
|
49
|
+
for (let index = 0; index < fadeLength; index++) {
|
|
50
|
+
// the last character reaches the background fully, so the tail masks out
|
|
51
|
+
// rather than leaving a legible word fragment
|
|
52
|
+
const progress = (index + 1) / fadeLength;
|
|
53
|
+
|
|
54
|
+
segments.push({
|
|
55
|
+
text: clipped[maxWidth - fadeLength + index]!,
|
|
56
|
+
fg: mixHex(baseColor, fadeColor, progress),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return segments;
|
|
61
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { DiffFileContents } from "@cueloop/schema";
|
|
3
|
+
import { buildFileTree } from "./file-tree";
|
|
4
|
+
|
|
5
|
+
function file(path: string, status: DiffFileContents["status"] = "modified"): DiffFileContents {
|
|
6
|
+
return { path, oldContents: "a", newContents: "b", status };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("buildFileTree", () => {
|
|
10
|
+
test("nests files under their directories and keeps the full path as the leaf id", () => {
|
|
11
|
+
// Arrange
|
|
12
|
+
const files = [file("src/app/main.ts"), file("src/app/util.ts"), file("README.md")];
|
|
13
|
+
|
|
14
|
+
// Act
|
|
15
|
+
const tree = buildFileTree(files);
|
|
16
|
+
|
|
17
|
+
// Assert - one src folder over one app folder over two leaves, plus a root file
|
|
18
|
+
expect(tree.map((node) => node.label)).toEqual(["src", "README.md"]);
|
|
19
|
+
const app = tree[0]!.children![0]!;
|
|
20
|
+
|
|
21
|
+
expect(app.label).toBe("app");
|
|
22
|
+
expect(app.children!.map((leaf) => leaf.id)).toEqual(["src/app/main.ts", "src/app/util.ts"]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("carries the git status onto each leaf so the tree can tint it", () => {
|
|
26
|
+
// Act
|
|
27
|
+
const tree = buildFileTree([file("a.ts", "added"), file("b.ts", "deleted")]);
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
expect(tree.map((leaf) => leaf.status)).toEqual(["added", "deleted"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("merges files that share a directory into one folder node", () => {
|
|
34
|
+
// Act
|
|
35
|
+
const tree = buildFileTree([file("pkg/one.ts"), file("pkg/two.ts")]);
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
expect(tree).toHaveLength(1);
|
|
39
|
+
expect(tree[0]!.children).toHaveLength(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("returns an empty tree for no changes", () => {
|
|
43
|
+
expect(buildFileTree([])).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("badges a leaf with its comment count, and leaves an uncommented file bare", () => {
|
|
47
|
+
// Act
|
|
48
|
+
const tree = buildFileTree(
|
|
49
|
+
[file("a.ts"), file("b.ts")],
|
|
50
|
+
new Map([
|
|
51
|
+
["a.ts", 3],
|
|
52
|
+
["b.ts", 0],
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Assert - a.ts shows the dot-and-count, b.ts (0) shows nothing
|
|
57
|
+
expect(tree[0]!.badge).toBe("● 3");
|
|
58
|
+
expect(tree[1]!.badge).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a nested file tree from a diff's changed files, so the Changes column
|
|
3
|
+
* can browse them by directory. Each leaf keeps its repo-relative path as the id
|
|
4
|
+
* (what the diff rows are keyed by) and carries the git status for its tint.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DiffFileContents, DiffFileStatus } from "@cueloop/schema";
|
|
8
|
+
import type { GitStatus, TreeNode } from "./primitives/tree-model";
|
|
9
|
+
|
|
10
|
+
/** The diff file statuses are a subset of the tree's git statuses. */
|
|
11
|
+
function gitStatusOf(status: DiffFileStatus): GitStatus {
|
|
12
|
+
return status;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MutableTreeNode {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
status?: GitStatus;
|
|
19
|
+
badge?: string;
|
|
20
|
+
children?: Map<string, MutableTreeNode>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function freeze(node: MutableTreeNode): TreeNode {
|
|
24
|
+
if (node.children === undefined) {
|
|
25
|
+
const leaf: TreeNode = { id: node.id, label: node.label, status: node.status };
|
|
26
|
+
|
|
27
|
+
if (node.badge !== undefined) leaf.badge = node.badge;
|
|
28
|
+
|
|
29
|
+
return leaf;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id: node.id,
|
|
34
|
+
label: node.label,
|
|
35
|
+
children: [...node.children.values()].map(freeze),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A file's comment count as a leaf badge, or undefined when it carries none. */
|
|
40
|
+
function commentBadge(
|
|
41
|
+
counts: ReadonlyMap<string, number> | undefined,
|
|
42
|
+
path: string,
|
|
43
|
+
): string | undefined {
|
|
44
|
+
const count = counts?.get(path);
|
|
45
|
+
|
|
46
|
+
return count ? `● ${count}` : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Group changed files into a directory tree; leaves are keyed by their full path. A leaf whose
|
|
51
|
+
* file carries comments shows a dot-and-count badge, so feedback is visible even with its tab closed.
|
|
52
|
+
*/
|
|
53
|
+
export function buildFileTree(
|
|
54
|
+
files: readonly DiffFileContents[],
|
|
55
|
+
commentCounts?: ReadonlyMap<string, number>,
|
|
56
|
+
): TreeNode[] {
|
|
57
|
+
const roots = new Map<string, MutableTreeNode>();
|
|
58
|
+
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const segments = file.path.split("/").filter((segment) => segment.length > 0);
|
|
61
|
+
let level = roots;
|
|
62
|
+
let prefix = "";
|
|
63
|
+
|
|
64
|
+
segments.forEach((segment, index) => {
|
|
65
|
+
prefix = prefix.length > 0 ? `${prefix}/${segment}` : segment;
|
|
66
|
+
const isLeaf = index === segments.length - 1;
|
|
67
|
+
let node = level.get(segment);
|
|
68
|
+
|
|
69
|
+
if (node === undefined) {
|
|
70
|
+
node = isLeaf
|
|
71
|
+
? {
|
|
72
|
+
id: file.path,
|
|
73
|
+
label: segment,
|
|
74
|
+
status: gitStatusOf(file.status),
|
|
75
|
+
badge: commentBadge(commentCounts, file.path),
|
|
76
|
+
}
|
|
77
|
+
: { id: prefix, label: segment, children: new Map() };
|
|
78
|
+
level.set(segment, node);
|
|
79
|
+
}
|
|
80
|
+
if (!isLeaf) level = node.children!;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [...roots.values()].map(freeze);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Group plain repo-relative paths (git ls-files) into a directory tree; leaves keep their full path. */
|
|
88
|
+
export function buildPathTree(paths: readonly string[]): TreeNode[] {
|
|
89
|
+
const roots = new Map<string, MutableTreeNode>();
|
|
90
|
+
|
|
91
|
+
for (const path of paths) {
|
|
92
|
+
const segments = path.split("/").filter((segment) => segment.length > 0);
|
|
93
|
+
let level = roots;
|
|
94
|
+
let prefix = "";
|
|
95
|
+
|
|
96
|
+
segments.forEach((segment, index) => {
|
|
97
|
+
prefix = prefix.length > 0 ? `${prefix}/${segment}` : segment;
|
|
98
|
+
const isLeaf = index === segments.length - 1;
|
|
99
|
+
let node = level.get(segment);
|
|
100
|
+
|
|
101
|
+
if (node === undefined) {
|
|
102
|
+
node = isLeaf
|
|
103
|
+
? { id: path, label: segment }
|
|
104
|
+
: { id: prefix, label: segment, children: new Map() };
|
|
105
|
+
level.set(segment, node);
|
|
106
|
+
}
|
|
107
|
+
if (!isLeaf) level = node.children!;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [...roots.values()].map(freeze);
|
|
112
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../story";
|
|
4
|
+
import { PaneGrid } from "../PaneGrid";
|
|
5
|
+
import { Tree } from "../primitives/Tree";
|
|
6
|
+
import type { TreeNode } from "../primitives/tree-model";
|
|
7
|
+
|
|
8
|
+
export const meta: StoryMeta = { title: "Pages/Workspace" };
|
|
9
|
+
|
|
10
|
+
const NAVIGATION: TreeNode[] = [
|
|
11
|
+
{
|
|
12
|
+
id: "projects",
|
|
13
|
+
label: "Projects",
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
id: "cueloop",
|
|
17
|
+
label: "cueloop",
|
|
18
|
+
children: [
|
|
19
|
+
{ id: "cueloop/review", label: "Review the app shell" },
|
|
20
|
+
{ id: "cueloop/read", label: "Read Cueloop Repository" },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "threads",
|
|
27
|
+
label: "Threads",
|
|
28
|
+
children: [{ id: "threads/welcome", label: "Welcome to cueloop" }],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const CHANGED_FILES: TreeNode[] = [
|
|
33
|
+
{
|
|
34
|
+
id: "packages",
|
|
35
|
+
label: "packages",
|
|
36
|
+
children: [
|
|
37
|
+
{
|
|
38
|
+
id: "client",
|
|
39
|
+
label: "client",
|
|
40
|
+
children: [
|
|
41
|
+
{ id: "client/AppShell.tsx", label: "AppShell.tsx", status: "modified" },
|
|
42
|
+
{ id: "client/Workspace.stories.tsx", label: "Workspace.stories.tsx", status: "added" },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{ id: "README.md", label: "README.md", status: "modified" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
interface CodeSampleLine {
|
|
51
|
+
number: number;
|
|
52
|
+
code: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CODE_SAMPLE: readonly CodeSampleLine[] = [
|
|
56
|
+
{ number: 17, code: "export function AppShell({" },
|
|
57
|
+
{ number: 18, code: " header," },
|
|
58
|
+
{ number: 19, code: " sidebar," },
|
|
59
|
+
{ number: 20, code: " main," },
|
|
60
|
+
{ number: 21, code: " inspector," },
|
|
61
|
+
{ number: 22, code: "}: AppShellProps) {" },
|
|
62
|
+
{ number: 23, code: " const tokens = theme ?? DARK;" },
|
|
63
|
+
{ number: 24, code: " return <box>{main}</box>;" },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function Sidebar(): React.ReactNode {
|
|
67
|
+
return (
|
|
68
|
+
<box style={{ flexDirection: "column", paddingTop: 1 }}>
|
|
69
|
+
<Tree
|
|
70
|
+
nodes={NAVIGATION}
|
|
71
|
+
expandedIds={new Set(["projects", "cueloop", "threads"])}
|
|
72
|
+
selectedId="cueloop/review"
|
|
73
|
+
/>
|
|
74
|
+
</box>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function TranscriptLine({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
79
|
+
return <text fg={DARK.textMuted}>{children}</text>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function CommentRow(): React.ReactNode {
|
|
83
|
+
return (
|
|
84
|
+
<box
|
|
85
|
+
style={{
|
|
86
|
+
border: ["left"],
|
|
87
|
+
borderStyle: "single",
|
|
88
|
+
borderColor: DARK.accent,
|
|
89
|
+
flexDirection: "column",
|
|
90
|
+
paddingLeft: 1,
|
|
91
|
+
marginTop: 1,
|
|
92
|
+
marginBottom: 1,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<text fg={DARK.textDim}>markus commented on AppShell.tsx</text>
|
|
96
|
+
<text fg={DARK.text}>The single rules read cleaner than an enclosing frame here.</text>
|
|
97
|
+
</box>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function Main(): React.ReactNode {
|
|
102
|
+
return (
|
|
103
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
104
|
+
<text fg={DARK.text}>Review the app shell</text>
|
|
105
|
+
<text> </text>
|
|
106
|
+
<TranscriptLine>
|
|
107
|
+
The three-pane layout divides regions with straight rules only.
|
|
108
|
+
</TranscriptLine>
|
|
109
|
+
<TranscriptLine>
|
|
110
|
+
Sidebar navigates, the transcript reads, the inspector shows code.
|
|
111
|
+
</TranscriptLine>
|
|
112
|
+
<CommentRow />
|
|
113
|
+
<TranscriptLine>Header and footer are thin label rows the shell rules itself.</TranscriptLine>
|
|
114
|
+
</box>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function CodePane(): React.ReactNode {
|
|
119
|
+
return (
|
|
120
|
+
<box style={{ flexDirection: "column" }}>
|
|
121
|
+
<text fg={DARK.textMuted}>AppShell.tsx</text>
|
|
122
|
+
<text> </text>
|
|
123
|
+
{CODE_SAMPLE.map((line) => (
|
|
124
|
+
<box key={line.number} style={{ flexDirection: "row" }}>
|
|
125
|
+
<text fg={DARK.textDim}>{String(line.number).padStart(3, " ")} </text>
|
|
126
|
+
<text fg={DARK.text}>{line.code}</text>
|
|
127
|
+
</box>
|
|
128
|
+
))}
|
|
129
|
+
</box>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Inspector(): React.ReactNode {
|
|
134
|
+
return (
|
|
135
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
136
|
+
<CodePane />
|
|
137
|
+
<text> </text>
|
|
138
|
+
<text fg={DARK.textDim}>Changed files</text>
|
|
139
|
+
<Tree
|
|
140
|
+
nodes={CHANGED_FILES}
|
|
141
|
+
expandedIds={new Set(["packages", "client"])}
|
|
142
|
+
selectedId="client/AppShell.tsx"
|
|
143
|
+
showStatus
|
|
144
|
+
/>
|
|
145
|
+
</box>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function Header(): React.ReactNode {
|
|
150
|
+
return (
|
|
151
|
+
<box style={{ flexDirection: "row", paddingLeft: 1 }}>
|
|
152
|
+
<text fg={DARK.textMuted}>Review the app shell</text>
|
|
153
|
+
<box style={{ flexGrow: 1 }} />
|
|
154
|
+
<text fg={DARK.textDim}>cueloop / main</text>
|
|
155
|
+
</box>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function Footer(): React.ReactNode {
|
|
160
|
+
return (
|
|
161
|
+
<box style={{ flexDirection: "row", paddingLeft: 1 }}>
|
|
162
|
+
<text fg={DARK.textDim}>2 files changed</text>
|
|
163
|
+
<box style={{ flexGrow: 1 }} />
|
|
164
|
+
<text fg={DARK.textDim}>Fable 5</text>
|
|
165
|
+
</box>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const ThreePane: Story = {
|
|
170
|
+
render: () => (
|
|
171
|
+
<PaneGrid
|
|
172
|
+
sidebar={<Sidebar />}
|
|
173
|
+
main={<Main />}
|
|
174
|
+
mainHeader={<Header />}
|
|
175
|
+
inspector={<Inspector />}
|
|
176
|
+
footer={<Footer />}
|
|
177
|
+
/>
|
|
178
|
+
),
|
|
179
|
+
expectedColors: [DARK.border, DARK.accent],
|
|
180
|
+
size: { width: 160, height: 42 },
|
|
181
|
+
};
|
|
@@ -4,7 +4,7 @@ import type { Story, StoryMeta } from "../story";
|
|
|
4
4
|
import { Button } from "./Button";
|
|
5
5
|
import { Toolbar } from "./Toolbar";
|
|
6
6
|
|
|
7
|
-
export const meta: StoryMeta = { title: "
|
|
7
|
+
export const meta: StoryMeta = { title: "Primitives/Button" };
|
|
8
8
|
|
|
9
9
|
export const Variants: Story = {
|
|
10
10
|
render: () => (
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "../story";
|
|
4
4
|
import { Card } from "./Card";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "
|
|
6
|
+
export const meta: StoryMeta = { title: "Primitives/Card" };
|
|
7
7
|
|
|
8
8
|
export const Titled: Story = {
|
|
9
9
|
render: () => (
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "../story";
|
|
4
4
|
import { Dialog } from "./Dialog";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "
|
|
6
|
+
export const meta: StoryMeta = { title: "Primitives/Dialog" };
|
|
7
7
|
|
|
8
8
|
export const Open: Story = {
|
|
9
9
|
render: () => (
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../story";
|
|
4
|
+
import { IconButton } from "./IconButton";
|
|
5
|
+
import { NERD } from "./icons";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Primitives/IconButton" };
|
|
8
|
+
|
|
9
|
+
function Row({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
10
|
+
return (
|
|
11
|
+
<box style={{ flexDirection: "row", padding: 1, backgroundColor: DARK.panel }}>{children}</box>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const States: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<Row>
|
|
18
|
+
<IconButton glyph={NERD.settings} onPress={() => {}} marginRight={3} />
|
|
19
|
+
<IconButton glyph={NERD.listTree} active onPress={() => {}} marginRight={3} />
|
|
20
|
+
<IconButton glyph={NERD.diff} disabled marginRight={3} />
|
|
21
|
+
<IconButton glyph={NERD.submit} onPress={() => {}} />
|
|
22
|
+
</Row>
|
|
23
|
+
),
|
|
24
|
+
expectedColors: [DARK.accent, DARK.textMuted, DARK.textDim],
|
|
25
|
+
size: { width: 40, height: 3 },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const Submit: Story = {
|
|
29
|
+
render: () => (
|
|
30
|
+
<Row>
|
|
31
|
+
<IconButton glyph={NERD.submit} active onPress={() => {}} />
|
|
32
|
+
</Row>
|
|
33
|
+
),
|
|
34
|
+
expectedColors: [DARK.accent],
|
|
35
|
+
size: { width: 20, height: 3 },
|
|
36
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A clickable single-glyph button for header and footer controls: an icon in a
|
|
2
|
+
// box that calls onPress, painted in the brand accent when active. alignSelf
|
|
3
|
+
// center keeps it on the same row as sibling text or icons.
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import type { MouseEvent as TerminalMouseEvent } from "@opentui/core";
|
|
7
|
+
import { DARK, type Theme } from "../../theme";
|
|
8
|
+
import { useTooltip } from "../Tooltip";
|
|
9
|
+
|
|
10
|
+
export interface IconButtonProps {
|
|
11
|
+
glyph: string;
|
|
12
|
+
onPress?: () => void;
|
|
13
|
+
/** Paint the glyph in the brand accent to mark the current mode or view. */
|
|
14
|
+
active?: boolean;
|
|
15
|
+
/** Dim the glyph and ignore presses. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Override the resolved foreground (wins over active/disabled colouring). */
|
|
18
|
+
color?: string;
|
|
19
|
+
/** Label shown on hover, surfaced at the screen root so a tiny header cell cannot clip it. */
|
|
20
|
+
tip?: string;
|
|
21
|
+
marginLeft?: number;
|
|
22
|
+
marginRight?: number;
|
|
23
|
+
theme?: Theme;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function IconButton({
|
|
27
|
+
glyph,
|
|
28
|
+
onPress,
|
|
29
|
+
active,
|
|
30
|
+
disabled,
|
|
31
|
+
color,
|
|
32
|
+
tip,
|
|
33
|
+
marginLeft,
|
|
34
|
+
marginRight,
|
|
35
|
+
theme,
|
|
36
|
+
}: IconButtonProps): React.ReactNode {
|
|
37
|
+
const tokens = theme ?? DARK;
|
|
38
|
+
const resolved = color ?? (disabled ? tokens.textDim : active ? tokens.accent : tokens.textMuted);
|
|
39
|
+
const { showTooltip, hideTooltip } = useTooltip();
|
|
40
|
+
// a button press is its own action - never let it bubble to a clickable row behind it
|
|
41
|
+
const handleMouseUp = (event: TerminalMouseEvent): void => {
|
|
42
|
+
event.stopPropagation();
|
|
43
|
+
hideTooltip();
|
|
44
|
+
onPress?.();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<box
|
|
49
|
+
onMouseUp={disabled ? undefined : handleMouseUp}
|
|
50
|
+
onMouseOver={tip ? (event) => showTooltip(tip, event.x, event.y) : undefined}
|
|
51
|
+
onMouseOut={tip ? hideTooltip : undefined}
|
|
52
|
+
style={{
|
|
53
|
+
flexShrink: 0,
|
|
54
|
+
alignSelf: "center",
|
|
55
|
+
marginLeft,
|
|
56
|
+
marginRight,
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<text fg={resolved}>{glyph}</text>
|
|
60
|
+
</box>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "../story";
|
|
4
4
|
import { StatusBar } from "./StatusBar";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "
|
|
6
|
+
export const meta: StoryMeta = { title: "Primitives/StatusBar" };
|
|
7
7
|
|
|
8
8
|
export const HintLine: Story = {
|
|
9
9
|
render: () => <StatusBar>j/k move · v span · c comment · q quit</StatusBar>,
|
|
@@ -3,7 +3,7 @@ import { DARK } from "../../theme";
|
|
|
3
3
|
import type { Story, StoryMeta } from "../story";
|
|
4
4
|
import { Tab, TabList, Tabs } from "./Tabs";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "
|
|
6
|
+
export const meta: StoryMeta = { title: "Primitives/Tabs" };
|
|
7
7
|
|
|
8
8
|
export const TwoTabs: Story = {
|
|
9
9
|
render: () => (
|
|
@@ -3,7 +3,7 @@ import type { Story, StoryMeta } from "../story";
|
|
|
3
3
|
import { Button } from "./Button";
|
|
4
4
|
import { Toolbar } from "./Toolbar";
|
|
5
5
|
|
|
6
|
-
export const meta: StoryMeta = { title: "
|
|
6
|
+
export const meta: StoryMeta = { title: "Primitives/Toolbar" };
|
|
7
7
|
|
|
8
8
|
export const ActionRow: Story = {
|
|
9
9
|
render: () => (
|