@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.
Files changed (160) hide show
  1. package/package.json +6 -5
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.diff.test.tsx +79 -112
  4. package/src/App.plan-review.test.tsx +23 -205
  5. package/src/App.prototype.test.tsx +1 -8
  6. package/src/App.readonly.test.tsx +9 -27
  7. package/src/App.submit-confirm.test.tsx +19 -72
  8. package/src/App.test.tsx +136 -31
  9. package/src/App.tsx +491 -322
  10. package/src/App.walk.test.tsx +15 -16
  11. package/src/App.workbench.test.tsx +390 -0
  12. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  13. package/src/annotation-palette.ts +61 -0
  14. package/src/app-screens.tsx +197 -77
  15. package/src/app-view-model.ts +9 -69
  16. package/src/components/AnnotationCard.stories.tsx +1 -1
  17. package/src/components/AnnotationCard.tsx +1 -1
  18. package/src/components/AnnotationCards.stories.tsx +87 -0
  19. package/src/components/AnnotationCards.tsx +303 -0
  20. package/src/components/AppHeader.stories.tsx +50 -0
  21. package/src/components/AppHeader.tsx +83 -0
  22. package/src/components/AppShell.stories.tsx +89 -0
  23. package/src/components/AppShell.tsx +239 -0
  24. package/src/components/AppShell.zoom.test.tsx +94 -0
  25. package/src/components/Breadcrumb.stories.tsx +1 -1
  26. package/src/components/ChangesColumn.stories.tsx +39 -0
  27. package/src/components/ChangesColumn.tsx +170 -0
  28. package/src/components/CodeBlock.stories.tsx +1 -1
  29. package/src/components/CompletionOverlay.stories.tsx +1 -1
  30. package/src/components/ConfirmCard.stories.tsx +1 -1
  31. package/src/components/ConfirmCard.tsx +3 -4
  32. package/src/components/ConfirmDialog.stories.tsx +1 -1
  33. package/src/components/DiffSheet.scroll.test.tsx +38 -55
  34. package/src/components/DiffSheet.split.test.tsx +140 -0
  35. package/src/components/DiffSheet.stories.tsx +56 -54
  36. package/src/components/DiffSheet.syntax.test.tsx +24 -7
  37. package/src/components/DiffSheet.tsx +607 -276
  38. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  39. package/src/components/DiscussionMarkerRail.tsx +181 -0
  40. package/src/components/EditorGrid.stories.tsx +59 -0
  41. package/src/components/EditorGrid.tabs.test.tsx +105 -0
  42. package/src/components/EditorGrid.tsx +338 -0
  43. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  44. package/src/components/FileContentsView.stories.tsx +16 -0
  45. package/src/components/FileContentsView.test.tsx +29 -0
  46. package/src/components/FileContentsView.tsx +78 -0
  47. package/src/components/InboxList.stories.tsx +23 -8
  48. package/src/components/InboxList.tsx +188 -33
  49. package/src/components/KeybindsDialog.stories.tsx +1 -1
  50. package/src/components/MarkerPopover.stories.tsx +1 -1
  51. package/src/components/MenuBar.stories.tsx +3 -12
  52. package/src/components/MenuBar.tsx +25 -56
  53. package/src/components/PaneGrid.stories.tsx +84 -0
  54. package/src/components/PaneGrid.tsx +128 -0
  55. package/src/components/PanelColumn.stories.tsx +25 -0
  56. package/src/components/PanelColumn.tsx +95 -0
  57. package/src/components/ProjectTreeView.stories.tsx +25 -0
  58. package/src/components/ProjectTreeView.test.tsx +32 -0
  59. package/src/components/ProjectTreeView.tsx +82 -0
  60. package/src/components/PromptDialog.stories.tsx +1 -1
  61. package/src/components/PromptDialog.tsx +8 -1
  62. package/src/components/PrototypeSheet.stories.tsx +1 -1
  63. package/src/components/RootOverlay.stories.tsx +44 -0
  64. package/src/components/RootOverlay.tsx +35 -0
  65. package/src/components/SettingsDialog.stories.tsx +7 -1
  66. package/src/components/SettingsDialog.tsx +114 -67
  67. package/src/components/SettingsRows.stories.tsx +1 -1
  68. package/src/components/ShellHeader.stories.tsx +50 -0
  69. package/src/components/ShellHeader.tsx +137 -0
  70. package/src/components/SurfaceScrollbar.stories.tsx +34 -0
  71. package/src/components/SurfaceScrollbar.test.tsx +111 -0
  72. package/src/components/SurfaceScrollbar.tsx +93 -0
  73. package/src/components/ThreadFooter.stories.tsx +30 -0
  74. package/src/components/ThreadFooter.test.tsx +31 -0
  75. package/src/components/ThreadFooter.tsx +54 -0
  76. package/src/components/ThreadView.slash.test.tsx +113 -0
  77. package/src/components/ThreadView.stories.tsx +1 -1
  78. package/src/components/ThreadView.tsx +62 -1352
  79. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  80. package/src/components/ThreadsSidebar.tsx +70 -0
  81. package/src/components/Toast.stories.tsx +1 -1
  82. package/src/components/Tooltip.stories.tsx +26 -0
  83. package/src/components/Tooltip.tsx +72 -0
  84. package/src/components/TreePane.stories.tsx +1 -1
  85. package/src/components/WalkWizard.stories.tsx +1 -1
  86. package/src/components/WelcomeSurface.stories.tsx +12 -0
  87. package/src/components/WelcomeSurface.tsx +52 -0
  88. package/src/components/__snapshots__/stories.test.tsx.snap +732 -572
  89. package/src/components/agent-launcher.stories.tsx +1 -1
  90. package/src/components/diff-sheet-layout.test.ts +125 -55
  91. package/src/components/diff-sheet-layout.ts +3 -103
  92. package/src/components/editor-grid.test.ts +86 -0
  93. package/src/components/editor-grid.ts +138 -0
  94. package/src/components/fade-title.test.ts +49 -0
  95. package/src/components/fade-title.ts +61 -0
  96. package/src/components/file-tree.test.ts +60 -0
  97. package/src/components/file-tree.ts +112 -0
  98. package/src/components/pages/Workspace.stories.tsx +181 -0
  99. package/src/components/primitives/Button.stories.tsx +1 -1
  100. package/src/components/primitives/Card.stories.tsx +1 -1
  101. package/src/components/primitives/Dialog.stories.tsx +1 -1
  102. package/src/components/primitives/IconButton.stories.tsx +36 -0
  103. package/src/components/primitives/IconButton.tsx +62 -0
  104. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  105. package/src/components/primitives/Tabs.stories.tsx +1 -1
  106. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  107. package/src/components/primitives/Tree.stories.tsx +124 -0
  108. package/src/components/primitives/Tree.tsx +96 -0
  109. package/src/components/primitives/icons.ts +61 -0
  110. package/src/components/primitives/tree-model.test.ts +99 -0
  111. package/src/components/primitives/tree-model.ts +137 -0
  112. package/src/components/quick-actions-editor.stories.tsx +1 -1
  113. package/src/components/session-tree.test.ts +134 -0
  114. package/src/components/session-tree.ts +108 -0
  115. package/src/components/stories-app.tsx +142 -82
  116. package/src/components/story-tree.test.ts +100 -0
  117. package/src/components/story.ts +55 -0
  118. package/src/components/theme-context.stories.tsx +1 -1
  119. package/src/config.test.ts +20 -49
  120. package/src/config.ts +26 -28
  121. package/src/diff-fold.test.ts +78 -0
  122. package/src/diff-fold.ts +126 -0
  123. package/src/diff-syntax.test.ts +16 -0
  124. package/src/diff-syntax.ts +6 -3
  125. package/src/discussions.ts +79 -0
  126. package/src/intent-dispatch.test.ts +31 -11
  127. package/src/intent-dispatch.ts +93 -48
  128. package/src/key-bindings.test.ts +1 -1
  129. package/src/keymap.test.ts +3 -25
  130. package/src/keymap.ts +17 -18
  131. package/src/mark-runs.ts +120 -0
  132. package/src/pty.ts +10 -2
  133. package/src/run.ts +17 -7
  134. package/src/serve.test.ts +4 -8
  135. package/src/session-controller.curate.test.ts +75 -0
  136. package/src/session-controller.share.test.ts +1 -0
  137. package/src/session-controller.tree.test.ts +26 -0
  138. package/src/session-controller.ts +160 -31
  139. package/src/slash-palette.ts +83 -0
  140. package/src/split-diff.test.ts +110 -0
  141. package/src/split-diff.ts +149 -0
  142. package/src/test-support.test.ts +3 -3
  143. package/src/test-support.ts +39 -8
  144. package/src/theme.ts +4 -4
  145. package/src/thread-chords.test.ts +25 -9
  146. package/src/thread-chords.ts +33 -8
  147. package/src/use-annotation-surface.tsx +837 -0
  148. package/src/use-changes-workbench.ts +151 -0
  149. package/src/use-frame-measure.ts +38 -0
  150. package/src/use-settings-dialog.tsx +23 -24
  151. package/src/use-terminal-virtualizer.ts +233 -0
  152. package/src/view-diff.ts +114 -0
  153. package/src/App.review-panel.test.tsx +0 -140
  154. package/src/App.tree.test.tsx +0 -224
  155. package/src/components/ReviewPanel.stories.tsx +0 -100
  156. package/src/components/ReviewPanel.tsx +0 -103
  157. package/src/components/ReviewRail.stories.tsx +0 -232
  158. package/src/components/ReviewRail.tsx +0 -327
  159. package/src/review-panel.test.ts +0 -113
  160. package/src/review-panel.ts +0 -83
@@ -1,140 +0,0 @@
1
- /** The review panel end to end: the b key cycles expanded -> compact -> hidden -> expanded on the live App, and the collapse mode persists to the user config so it survives a restart. Rendering details are locked by the ReviewPanel stories; this suite proves the App wiring and the persistence. */
2
-
3
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
- import { mkdtempSync, rmSync } from "node:fs";
5
- import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- import React from "react";
8
- import { testRender } from "@opentui/react/test-utils";
9
- import { DaemonServer } from "@cueloop/daemon";
10
- import type { ReviewSession } from "@cueloop/schema";
11
- import { App } from "./App";
12
- import { loadConfig } from "./config";
13
- import { isolateUserConfig, settle, waitForText, waitForTextGone, pressKey } from "./test-support";
14
-
15
- const PLAN = "# Plan\n\nShip the thing.\n";
16
-
17
- let home: string;
18
- let server: DaemonServer;
19
- let session: ReviewSession;
20
- let configPath: string;
21
- let restoreUserConfig: () => void;
22
-
23
- beforeEach(() => {
24
- home = mkdtempSync(join(tmpdir(), "cueloop-panel-"));
25
- configPath = join(home, "config.toml");
26
- restoreUserConfig = isolateUserConfig(home, "config.toml");
27
- server = new DaemonServer({ home, idleExitMs: 0 });
28
- server.start();
29
- session = server.core.sessionCreate({
30
- workspace: { repoRoot: "/repo", branch: "main" },
31
- artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
32
- });
33
- });
34
- afterEach(() => {
35
- restoreUserConfig();
36
- server.stop();
37
- rmSync(home, { recursive: true, force: true });
38
- });
39
-
40
- async function renderApp() {
41
- const setup = await testRender(<App home={home} sessionId={session.id} />, {
42
- width: 120,
43
- height: 30,
44
- });
45
-
46
- await waitForText(setup, "Review");
47
-
48
- return setup;
49
- }
50
-
51
- describe("review panel", () => {
52
- test("ctrl+r cycles expanded -> compact -> hidden -> expanded", async () => {
53
- // Arrange
54
- const setup = await renderApp();
55
- // each mode has its own signature: expanded draws the tab box and the
56
- // submit button, compact keeps only the count strip, hidden draws nothing
57
- const railSignature = () => {
58
- const frame = setup.captureCharFrame();
59
-
60
- return {
61
- tabs: frame.includes("Review Tree Agent"),
62
- submit: frame.includes("Submit review"),
63
- strip: /\s0\s*\n/.test(frame) && frame.includes("<"),
64
- };
65
- };
66
-
67
- // Assert: expanded
68
- expect(railSignature()).toEqual({ tabs: true, submit: true, strip: false });
69
-
70
- // Act
71
- await pressKey(setup, "r", { ctrl: true });
72
-
73
- // Assert: compact drops the tabs and the button, keeps the strip
74
- await waitForTextGone(setup, "Submit review");
75
- expect(railSignature()).toEqual({ tabs: false, submit: false, strip: true });
76
-
77
- // Act
78
- await pressKey(setup, "r", { ctrl: true });
79
- await settle(setup);
80
-
81
- // Assert: hidden draws no rail at all
82
- expect(railSignature()).toEqual({ tabs: false, submit: false, strip: false });
83
-
84
- // Act
85
- await pressKey(setup, "r", { ctrl: true });
86
-
87
- // Assert
88
- await waitForText(setup, "Submit review"); // back to expanded
89
- });
90
-
91
- test("the collapse mode persists to the user config", async () => {
92
- // Arrange
93
- const setup = await renderApp();
94
-
95
- // Act
96
- await pressKey(setup, "r", { ctrl: true }); // -> compact
97
-
98
- // Assert
99
- expect(loadConfig({ userConfigPath: configPath }).ui.reviewState).toBe("compact");
100
- });
101
-
102
- test("option+w widens the rail and persists the new width", async () => {
103
- // Arrange
104
- const setup = await renderApp();
105
- const startWidth = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
106
-
107
- // Act
108
- await pressKey(setup, "w", { meta: true });
109
-
110
- // Assert
111
- const widened = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
112
-
113
- expect(widened).toBeGreaterThan(startWidth);
114
- });
115
-
116
- test("submitting from a hidden panel force-opens the rail so the confirm card is reachable", async () => {
117
- // Arrange
118
- const setup = await renderApp();
119
-
120
- await pressKey(setup, "r", { ctrl: true }); // -> compact
121
- await pressKey(setup, "r", { ctrl: true }); // -> hidden
122
-
123
- // Assert
124
- await waitForTextGone(setup, "Submit review"); // rail is gone
125
- expect(setup.captureCharFrame()).not.toContain("Submit review");
126
-
127
- // Act
128
- await pressKey(setup, "RETURN", { meta: true }); // submit while hidden must not trap in an invisible modal
129
-
130
- // Assert
131
- await waitForText(setup, "submit review");
132
- const frame = setup.captureCharFrame();
133
-
134
- expect(frame).toContain("submit review"); // the confirm card is visible again
135
- expect(frame).toContain("[Approve]");
136
-
137
- // the reveal is live-only: the saved panel preference stays as the user left it
138
- expect(loadConfig({ userConfigPath: configPath }).ui.reviewState).toBe("hidden");
139
- });
140
- });
@@ -1,224 +0,0 @@
1
- /**
2
- * The rail's Tree tab over the real App and an in-process daemon: the option
3
- * chords and the mouse reach the same primitives, the pane paints the path,
4
- * the tips, and the checkpoints, and the thread view shows a moved path in the
5
- * frame after the key.
6
- */
7
-
8
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
- import { mkdtempSync, rmSync } from "node:fs";
10
- import { tmpdir } from "node:os";
11
- import { join } from "node:path";
12
- import React from "react";
13
- import { testRender } from "@opentui/react/test-utils";
14
- import { DaemonServer } from "@cueloop/daemon";
15
- import type { ReviewSession } from "@cueloop/schema";
16
- import { App } from "./App";
17
- import {
18
- clickText,
19
- isolateUserConfig,
20
- press,
21
- pressKey,
22
- typeText as type,
23
- waitForState,
24
- waitForText,
25
- waitForTextGone,
26
- } from "./test-support";
27
-
28
- const PLAN = `# Migration Plan
29
-
30
- ## Context
31
-
32
- The daemon persists sessions to disk atomically.
33
-
34
- ## Steps
35
-
36
- - move the store
37
- - add recovery
38
- `;
39
-
40
- let home: string;
41
- let server: DaemonServer;
42
- let session: ReviewSession;
43
- let restoreUserConfig: () => void;
44
-
45
- beforeEach(() => {
46
- home = mkdtempSync(join(tmpdir(), "cueloop-tree-"));
47
- restoreUserConfig = isolateUserConfig(home);
48
- server = new DaemonServer({ home, idleExitMs: 0 });
49
- server.start();
50
- session = server.core.sessionCreate({
51
- workspace: { repoRoot: "/repo", branch: "main" },
52
- artifact: {
53
- type: "plan",
54
- content: PLAN,
55
- meta: { title: "Migration Plan", planPath: "plan.md", agent: "agent/worker-3" },
56
- },
57
- });
58
- });
59
- afterEach(() => {
60
- restoreUserConfig();
61
- server.stop();
62
- rmSync(home, { recursive: true, force: true });
63
- });
64
-
65
- async function renderApp(role: "owner" | "collaborator" = "owner") {
66
- const setup = await testRender(<App home={home} sessionId={session.id} role={role} />, {
67
- width: 120,
68
- height: 32,
69
- });
70
-
71
- await waitForText(setup, "cueloop");
72
-
73
- return setup;
74
- }
75
-
76
- type Setup = Awaited<ReturnType<typeof renderApp>>;
77
-
78
- /** Leave a comment on the word, sent with the terminal-safe chord. */
79
- async function comment(setup: Setup, word: string, body: string): Promise<void> {
80
- await clickText(setup, word);
81
- await type(setup, body);
82
- await pressKey(setup, "RETURN", { meta: true });
83
- await waitForText(setup, body);
84
- }
85
-
86
- /** Frames painted until the needle leaves the screen, starting right after a key with no settle. */
87
- async function framesUntilGone(setup: Setup, needle: string, limit = 4): Promise<number> {
88
- for (let frame = 1; frame <= limit; frame++) {
89
- await new Promise((resolve) => setTimeout(resolve, 0));
90
- await setup.renderOnce();
91
- if (!setup.captureCharFrame().includes(needle)) return frame;
92
- }
93
- throw new Error(`"${needle}" never left the frame`);
94
- }
95
-
96
- describe("the Tree tab", () => {
97
- test("option+t shows the tree: the revision, the comments, the current tip, a checkpoint", async () => {
98
- // Arrange
99
- const setup = await renderApp();
100
-
101
- await comment(setup, "daemon", "Which daemon?");
102
-
103
- // Act
104
- await pressKey(setup, "t", { meta: true });
105
- await waitForText(setup, "● comment");
106
- await pressKey(setup, "l", { meta: true });
107
- await waitForText(setup, "Checkpoint");
108
- await type(setup, "first pass");
109
- await press(setup, "enter");
110
-
111
- // Assert: the tip row carries the branch and the new label
112
- await waitForText(setup, "⚑ first pass");
113
- expect(setup.captureCharFrame()).toContain("◉ revision 1");
114
- expect(setup.captureCharFrame()).toContain("← main");
115
- await waitForState(
116
- setup,
117
- () => Object.values(server.core.sessionGet(session.id).history!.labels)[0] === "first pass",
118
- );
119
- });
120
-
121
- test("option+b branches; option+p / option+g move back and the path repaints in one frame", async () => {
122
- // Arrange: a comment on main, a branch, a second comment on it
123
- const setup = await renderApp();
124
-
125
- await comment(setup, "daemon", "Which daemon?");
126
- await pressKey(setup, "t", { meta: true });
127
- await waitForText(setup, "● comment");
128
- await pressKey(setup, "b", { meta: true });
129
- await waitForText(setup, "Branch");
130
- await type(setup, "alt");
131
- await press(setup, "enter");
132
- await waitForText(setup, "← main, alt");
133
- await comment(setup, "recovery", "Only on alt.");
134
- await waitForText(setup, "● comment ← alt");
135
-
136
- // Act: stand on the revision (two rows up from alt's tip) and move back to it
137
- await pressKey(setup, "p", { meta: true });
138
- await pressKey(setup, "p", { meta: true });
139
- await pressKey(setup, "g", { meta: true });
140
- await waitForText(setup, "Move back");
141
- setup.mockInput.pressKey("RETURN");
142
-
143
- // Assert: both comments leave the document in the frame after the key; the record follows
144
- expect(await framesUntilGone(setup, "Only on alt.")).toBeLessThanOrEqual(1);
145
- expect(setup.captureCharFrame()).not.toContain("Which daemon?");
146
- await waitForState(setup, () => {
147
- const record = server.core.sessionGet(session.id);
148
-
149
- return (
150
- record.history!.branch === "alt" &&
151
- record.annotations.length === 0 &&
152
- record.shelvedAnnotations?.length === 2
153
- );
154
- });
155
-
156
- // Act: main's tip is the first comment - going there is a switch, no prompt
157
- await pressKey(setup, "n", { meta: true });
158
- await pressKey(setup, "g", { meta: true });
159
-
160
- // Assert
161
- await waitForText(setup, "on branch main");
162
- await waitForText(setup, "Which daemon?");
163
- expect(setup.captureCharFrame()).not.toContain("Only on alt.");
164
- await waitForState(setup, () => server.core.sessionGet(session.id).history!.branch === "main");
165
- });
166
-
167
- test("the mouse reaches the same primitives: the tab, a row, and the buttons", async () => {
168
- // Arrange
169
- const setup = await renderApp();
170
-
171
- await comment(setup, "daemon", "Which daemon?");
172
-
173
- // Act: open the tab, select the revision row, open the label prompt from its button
174
- await clickText(setup, "Tree");
175
- await waitForText(setup, "◉ revision 1");
176
- await clickText(setup, "◉ revision 1");
177
- await clickText(setup, "Label");
178
-
179
- // Assert
180
- await waitForText(setup, "Checkpoint");
181
- await press(setup, "escape");
182
- await waitForTextGone(setup, "Checkpoint");
183
-
184
- // Act: a second click on the selected row goes there - a move back asks for a summary
185
- await clickText(setup, "◉ revision 1");
186
- await waitForText(setup, "Move back");
187
- });
188
-
189
- test("a collaborator reads the tree and cannot move it", async () => {
190
- // Arrange
191
- const setup = await renderApp("collaborator");
192
-
193
- // Act
194
- await pressKey(setup, "t", { meta: true });
195
- await waitForText(setup, "● revision 1");
196
- await pressKey(setup, "b", { meta: true });
197
-
198
- // Assert: no prompt, the read-only answer, no buttons
199
- await waitForText(setup, "observer - read-only");
200
- expect(setup.captureCharFrame()).not.toContain("Fork+share");
201
-
202
- // Act: two clicks on the revision row select it and nothing more
203
- await clickText(setup, "● revision 1");
204
- await clickText(setup, "● revision 1");
205
-
206
- // Assert: no prompt opened, the record stands
207
- expect(setup.captureCharFrame()).not.toContain("Move back");
208
- expect(server.core.sessionGet(session.id).history!.entries).toHaveLength(1);
209
- });
210
-
211
- test("a resolved review keeps the fork buttons and drops the moving ones", async () => {
212
- // Arrange
213
- server.core.sessionResolve(session.id, "approve", "ship it");
214
- const setup = await renderApp();
215
-
216
- // Act
217
- await pressKey(setup, "t", { meta: true });
218
- await waitForText(setup, "Fork+share");
219
-
220
- // Assert
221
- expect(setup.captureCharFrame()).not.toContain("Branch");
222
- expect(setup.captureCharFrame()).not.toContain("Label");
223
- });
224
- });
@@ -1,100 +0,0 @@
1
- import React from "react";
2
- import { DARK } from "../theme";
3
- import { REVIEW_MAX_WIDTH, REVIEW_MIN_WIDTH } from "../review-panel";
4
- import type { Story, StoryMeta } from "./story";
5
- import { ReviewPanel } from "./ReviewPanel";
6
- import { FIXTURE_ANNOTATIONS, fixturePlanSession } from "./story-fixtures";
7
-
8
- export const meta: StoryMeta = { title: "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
- };
22
-
23
- const rail = {
24
- session: fixturePlanSession(),
25
- tree,
26
- authorNames: {},
27
- selectedId: FIXTURE_ANNOTATIONS[0]!.id,
28
- resolvedIds: new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id)),
29
- curationItems: [],
30
- railTab: "review" as const,
31
- pendingCount: 2,
32
- cardEdit: null,
33
- submitConfirm: null,
34
- onTabChange: () => {},
35
- onSelectCard: () => {},
36
- onActivateCard: () => {},
37
- onSelectCuration: () => {},
38
- onUndoCuration: () => {},
39
- onSubmitRequest: () => {},
40
- onLaunchHarness: () => {},
41
- };
42
-
43
- const panelCallbacks = { onDividerGrab: () => {}, onToggle: () => {} };
44
-
45
- /** The plan area to the left of the divider, so the layout reads in context. */
46
- function PanelFrame({ children }: { children: React.ReactNode }): React.ReactNode {
47
- return (
48
- <box
49
- style={{
50
- flexDirection: "row",
51
- width: "100%",
52
- height: "100%",
53
- backgroundColor: DARK.background,
54
- }}
55
- >
56
- <box style={{ flexGrow: 1, paddingLeft: 1 }}>
57
- <text fg={DARK.textMuted}>The plan takes the width the panel hands back.</text>
58
- </box>
59
- {children}
60
- </box>
61
- );
62
- }
63
-
64
- export const ExpandedNarrow: Story = {
65
- render: () => (
66
- <PanelFrame>
67
- <ReviewPanel mode="expanded" width={REVIEW_MIN_WIDTH} rail={rail} {...panelCallbacks} />
68
- </PanelFrame>
69
- ),
70
- size: { width: 80, height: 24 },
71
- };
72
-
73
- export const ExpandedWide: Story = {
74
- render: () => (
75
- <PanelFrame>
76
- <ReviewPanel mode="expanded" width={REVIEW_MAX_WIDTH} rail={rail} {...panelCallbacks} />
77
- </PanelFrame>
78
- ),
79
- expectedColors: [DARK.accent],
80
- size: { width: 90, height: 24 },
81
- };
82
-
83
- export const Compact: Story = {
84
- render: () => (
85
- <PanelFrame>
86
- <ReviewPanel mode="compact" width={34} rail={rail} {...panelCallbacks} />
87
- </PanelFrame>
88
- ),
89
- expectedColors: [DARK.accent],
90
- size: { width: 60, height: 24 },
91
- };
92
-
93
- export const Hidden: Story = {
94
- render: () => (
95
- <PanelFrame>
96
- <ReviewPanel mode="hidden" width={34} rail={rail} {...panelCallbacks} />
97
- </PanelFrame>
98
- ),
99
- size: { width: 60, height: 24 },
100
- };
@@ -1,103 +0,0 @@
1
- /**
2
- * The review surface's outer shell: the grabbable divider plus the rail in one
3
- * of three modes - expanded (the full ReviewRail, resizable), compact (a narrow
4
- * count + dots strip that hands width back to the plan), or hidden (nothing at
5
- * all, no leftover tab). The keybinding cycles all three; the clickable chevron
6
- * toggles expanded and compact only. App owns the width/mode state and the drag
7
- * on the container; this file only reads them. The geometry (width clamp, the
8
- * divider glyph column, the cycle) lives in review-panel.ts so this stays
9
- * declarative.
10
- */
11
-
12
- import React from "react";
13
- import type { Annotation } from "@cueloop/schema";
14
- import type { Theme } from "../theme";
15
- import { useComponentTheme } from "./theme-context";
16
- import { ReviewRail, type ReviewRailHandle, type ReviewRailProps } from "./ReviewRail";
17
- import { REVIEW_COMPACT_WIDTH, type ReviewPanelMode } from "../review-panel";
18
-
19
- export interface ReviewPanelProps {
20
- mode: ReviewPanelMode;
21
- /** Expanded-rail width in columns (already clamped by the app). */
22
- width: number;
23
- /** Arm a divider drag; the app tracks the drag itself on the container. */
24
- onDividerGrab: () => void;
25
- /** Chevron click: expanded <-> compact (never hidden). */
26
- onToggle: () => void;
27
- /** The expanded ReviewRail's props; width and onCollapse are supplied here. */
28
- rail: Omit<ReviewRailProps, "width" | "onCollapse" | "theme">;
29
- railRef?: React.Ref<ReviewRailHandle>;
30
- theme?: Theme;
31
- }
32
-
33
- /** An invisible, full-height grab column: the plan's right border is the seam. */
34
- function ReviewDivider({ onGrab }: { onGrab: () => void }): React.ReactNode {
35
- // childless: it stretches to the row height like the plan and rail beside it,
36
- // so the three columns share one bottom edge. Invisible, but full-height grab.
37
- return <box style={{ width: 1 }} onMouseDown={onGrab} />;
38
- }
39
-
40
- /** The compact strip: the card count, one kind-colored dot per card, and the
41
- * `<` expand chevron, bottom-padded to sit at the same row as the expanded `>`. */
42
- function CompactRail({
43
- annotations,
44
- onExpand,
45
- theme,
46
- }: {
47
- annotations: Annotation[];
48
- onExpand: () => void;
49
- theme?: Theme;
50
- }): React.ReactNode {
51
- const tokens = useComponentTheme(theme);
52
-
53
- return (
54
- <box
55
- style={{
56
- width: REVIEW_COMPACT_WIDTH,
57
- backgroundColor: tokens.panel,
58
- flexDirection: "column",
59
- paddingTop: 1,
60
- paddingBottom: 1,
61
- }}
62
- >
63
- <box style={{ alignItems: "center" }}>
64
- <text fg={tokens.accent}>{String(annotations.length)}</text>
65
- </box>
66
- <box style={{ height: 1 }} />
67
- <box style={{ flexDirection: "column", alignItems: "center" }}>
68
- {annotations.map((annotation) => (
69
- <text key={annotation.id} fg={tokens.accent}>
70
- ●
71
- </text>
72
- ))}
73
- </box>
74
- <box style={{ flexGrow: 1 }} />
75
- <box style={{ paddingLeft: 1 }} onMouseUp={onExpand}>
76
- <text fg={tokens.textDim}>{"<"}</text>
77
- </box>
78
- </box>
79
- );
80
- }
81
-
82
- export function ReviewPanel({
83
- mode,
84
- width,
85
- onDividerGrab,
86
- onToggle,
87
- rail,
88
- railRef,
89
- theme,
90
- }: ReviewPanelProps): React.ReactNode {
91
- if (mode === "hidden") return null;
92
-
93
- return (
94
- <>
95
- <ReviewDivider onGrab={onDividerGrab} />
96
- {mode === "expanded" ? (
97
- <ReviewRail ref={railRef} {...rail} width={width} onCollapse={onToggle} theme={theme} />
98
- ) : (
99
- <CompactRail annotations={rail.session.annotations} onExpand={onToggle} theme={theme} />
100
- )}
101
- </>
102
- );
103
- }