@cueloop/client 0.1.0-alpha.5 → 0.1.0-alpha.50
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +36 -31
- package/src/App.autoclose.test.tsx +137 -0
- package/src/App.diff.test.tsx +158 -22
- package/src/App.export.test.tsx +44 -37
- package/src/App.plan-review.test.tsx +672 -0
- package/src/App.prototype.test.tsx +155 -0
- package/src/App.readonly.test.tsx +60 -36
- package/src/App.rendering.test.tsx +132 -0
- package/src/App.review-panel.test.tsx +136 -0
- package/src/App.submit-confirm.test.tsx +218 -0
- package/src/App.test.tsx +128 -69
- package/src/App.toast.test.tsx +75 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +289 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +38 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +30 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +241 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +44 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +83 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +59 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +127 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +355 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +62 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +58 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +108 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +74 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +456 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +78 -0
- package/src/components/PrototypeSheet.stories.tsx +20 -0
- package/src/components/PrototypeSheet.tsx +358 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +101 -0
- package/src/components/ReviewRail.stories.tsx +218 -0
- package/src/components/ReviewRail.tsx +317 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +141 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +108 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +44 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +49 -0
- package/src/components/agent-launcher.tsx +147 -0
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +51 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +59 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +64 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +22 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +98 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +136 -0
- package/src/components/stories-app.tsx +123 -0
- package/src/components/stories.test.tsx +62 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +69 -0
- package/src/components/syntax-highlight.ts +92 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +181 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +36 -0
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +430 -31
- package/src/config.ts +304 -26
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +143 -0
- package/src/editor.ts +123 -9
- package/src/ghostty-terminal.test.ts +82 -0
- package/src/ghostty-terminal.ts +168 -0
- package/src/herdr.test.ts +41 -0
- package/src/herdr.ts +27 -0
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +30 -0
- package/src/intent-dispatch.test.ts +493 -0
- package/src/intent-dispatch.ts +428 -0
- package/src/key-bindings.test.ts +152 -0
- package/src/key-bindings.ts +430 -0
- package/src/keymap.test.ts +622 -0
- package/src/keymap.ts +339 -0
- package/src/kitty-image.test.ts +20 -0
- package/src/kitty-image.ts +75 -0
- package/src/prototype-browser.test.ts +55 -0
- package/src/prototype-browser.ts +243 -0
- package/src/pty.test.ts +78 -0
- package/src/pty.ts +222 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +9 -5
- package/src/serve.test.ts +24 -9
- package/src/serve.ts +7 -7
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +309 -0
- package/src/session-controller.ts +914 -0
- package/src/share.ts +113 -0
- package/src/test-support.test.ts +55 -0
- package/src/test-support.ts +187 -0
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +38 -17
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +19 -8
- package/src/view-diff.ts +34 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +399 -0
- package/src/view-plan.ts +495 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +70 -0
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prototype review surface: renders the HTML screenshot into a reserved cell
|
|
3
|
+
* region via the kitty graphics protocol, and turns a click into a DOM-element
|
|
4
|
+
* selection, so the shared marker actions bar and compose card annotate an
|
|
5
|
+
* element rather than a text span. The image is painted directly (not through
|
|
6
|
+
* OpenTUI's ImageRenderable, which does not display in every terminal): the
|
|
7
|
+
* region's cells are never repainted, and the picture is re-placed after each
|
|
8
|
+
* frame so it survives OpenTUI's own draws.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
12
|
+
import type { BoxRenderable } from "@opentui/core";
|
|
13
|
+
import { useRenderer } from "@opentui/react";
|
|
14
|
+
import type { Theme } from "../theme";
|
|
15
|
+
import { quickActionBody, type QuickAction } from "../config";
|
|
16
|
+
import { useComponentTheme } from "./theme-context";
|
|
17
|
+
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
18
|
+
import { MarkerPopover } from "./MarkerPopover";
|
|
19
|
+
import { AnnotationCard } from "./AnnotationCard";
|
|
20
|
+
import {
|
|
21
|
+
cssBoxToCell,
|
|
22
|
+
imageCellToCss,
|
|
23
|
+
prototypeRendererFactory,
|
|
24
|
+
type PrototypeElement,
|
|
25
|
+
type PrototypeRenderer,
|
|
26
|
+
type PrototypeViewport,
|
|
27
|
+
} from "../prototype-browser";
|
|
28
|
+
import {
|
|
29
|
+
deleteKittyImage,
|
|
30
|
+
placeKittyImage,
|
|
31
|
+
transmitKittyImage,
|
|
32
|
+
type CellRegion,
|
|
33
|
+
} from "../kitty-image";
|
|
34
|
+
|
|
35
|
+
export interface PrototypeSheetProps {
|
|
36
|
+
prototypePath: string;
|
|
37
|
+
quickActions: QuickAction[];
|
|
38
|
+
canComment: boolean;
|
|
39
|
+
onCommentElement: (element: PrototypeElement, body: string) => void;
|
|
40
|
+
/** Signals when the inline compose owns the keyboard, so the app suppresses
|
|
41
|
+
* its global keymap and the compose textarea receives the typed note. */
|
|
42
|
+
onComposingChange?: (active: boolean) => void;
|
|
43
|
+
/** True while an app overlay (menu, settings) covers the sheet; the image is
|
|
44
|
+
* removed so those overlays are not shown through the graphics layer. */
|
|
45
|
+
hidden?: boolean;
|
|
46
|
+
theme?: Theme;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type SheetStatus = "loading" | "ready" | "unsupported" | "error";
|
|
50
|
+
|
|
51
|
+
const POPOVER_ROWS = 3;
|
|
52
|
+
const PROTOTYPE_IMAGE_ID = 811;
|
|
53
|
+
/** Page pixels scrolled per wheel notch. */
|
|
54
|
+
const SCROLL_STEP = 240;
|
|
55
|
+
// The capture viewport width in CSS pixels; its height matches the region's
|
|
56
|
+
// cell aspect so the rendered image fills the box instead of letterboxing. The
|
|
57
|
+
// aspect comes from the terminal's reported cell pixels, falling back to a
|
|
58
|
+
// typical monospace ratio when the terminal never reports one.
|
|
59
|
+
const CAPTURE_WIDTH = 1280;
|
|
60
|
+
const FALLBACK_CELL_HEIGHT_OVER_WIDTH = 2.18;
|
|
61
|
+
|
|
62
|
+
function cellHeightOverWidth(renderer: {
|
|
63
|
+
resolution: { width: number; height: number } | null;
|
|
64
|
+
terminalWidth: number;
|
|
65
|
+
terminalHeight: number;
|
|
66
|
+
}): number {
|
|
67
|
+
const resolution = renderer.resolution;
|
|
68
|
+
if (!resolution || renderer.terminalWidth < 1 || renderer.terminalHeight < 1) {
|
|
69
|
+
return FALLBACK_CELL_HEIGHT_OVER_WIDTH;
|
|
70
|
+
}
|
|
71
|
+
return resolution.height / renderer.terminalHeight / (resolution.width / renderer.terminalWidth);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function regionOf(box: BoxRenderable | null): CellRegion | null {
|
|
75
|
+
if (!box || box.width < 1 || box.height < 1) return null;
|
|
76
|
+
return { column: box.x, row: box.y, columns: box.width, rows: box.height };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function PrototypeSheet({
|
|
80
|
+
prototypePath,
|
|
81
|
+
quickActions,
|
|
82
|
+
canComment,
|
|
83
|
+
onCommentElement,
|
|
84
|
+
onComposingChange,
|
|
85
|
+
hidden = false,
|
|
86
|
+
theme,
|
|
87
|
+
}: PrototypeSheetProps): React.ReactNode {
|
|
88
|
+
const tokens = useComponentTheme(theme);
|
|
89
|
+
const renderer = useRenderer();
|
|
90
|
+
const regionRef = useRef<BoxRenderable | null>(null);
|
|
91
|
+
const browserRef = useRef<PrototypeRenderer | null>(null);
|
|
92
|
+
const launchedRef = useRef(false);
|
|
93
|
+
const viewportRef = useRef<PrototypeViewport>({ width: CAPTURE_WIDTH, height: 800 });
|
|
94
|
+
const pngRef = useRef<Uint8Array | null>(null);
|
|
95
|
+
const transmittedRef = useRef<Uint8Array | null>(null);
|
|
96
|
+
const paintRef = useRef<() => void>(() => undefined);
|
|
97
|
+
const launchRef = useRef<() => void>(() => undefined);
|
|
98
|
+
const hiddenRef = useRef(hidden);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
hiddenRef.current = hidden;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const [status, setStatus] = useState<SheetStatus>("loading");
|
|
104
|
+
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
105
|
+
const [selected, setSelected] = useState<PrototypeElement | null>(null);
|
|
106
|
+
const [overlayCell, setOverlayCell] = useState<{ left: number; top: number } | null>(null);
|
|
107
|
+
const [actionsOpen, setActionsOpen] = useState(false);
|
|
108
|
+
const [composing, setComposing] = useState(false);
|
|
109
|
+
const [draftText, setDraftText] = useState("");
|
|
110
|
+
|
|
111
|
+
const setPng = (png: Uint8Array): void => {
|
|
112
|
+
pngRef.current = png;
|
|
113
|
+
transmittedRef.current = null;
|
|
114
|
+
paintRef.current();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Every frame: start the browser once layout is known, then keep the picture
|
|
118
|
+
// asserted over its cells. The kitty emission (transmit once, cheap re-place
|
|
119
|
+
// after) is skipped when the renderer has no raw writeOut channel (the test
|
|
120
|
+
// harness), but the launch trigger still runs.
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!renderer) return;
|
|
123
|
+
// writeOut is the renderer's raw output channel, ordered against its frame
|
|
124
|
+
// bytes; it is private, so reach it through one narrow cast
|
|
125
|
+
const rawWrite = (renderer as unknown as { writeOut?: (data: string) => void }).writeOut;
|
|
126
|
+
const write =
|
|
127
|
+
typeof rawWrite === "function" ? (chunk: string) => rawWrite.call(renderer, chunk) : null;
|
|
128
|
+
const paint = (): void => {
|
|
129
|
+
if (!write) return;
|
|
130
|
+
// an app overlay is covering the sheet: pull the image so it does not show
|
|
131
|
+
// through the overlay's cells, and re-transmit once the overlay closes
|
|
132
|
+
if (hiddenRef.current) {
|
|
133
|
+
if (transmittedRef.current !== null) {
|
|
134
|
+
deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
|
|
135
|
+
transmittedRef.current = null;
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const region = regionOf(regionRef.current);
|
|
140
|
+
const png = pngRef.current;
|
|
141
|
+
if (!region || !png) return;
|
|
142
|
+
if (transmittedRef.current !== png) {
|
|
143
|
+
transmitKittyImage(write, png, region, PROTOTYPE_IMAGE_ID);
|
|
144
|
+
transmittedRef.current = png;
|
|
145
|
+
} else {
|
|
146
|
+
placeKittyImage(write, region, PROTOTYPE_IMAGE_ID);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
paintRef.current = paint;
|
|
150
|
+
const onFrame = (): void => {
|
|
151
|
+
launchRef.current();
|
|
152
|
+
paint();
|
|
153
|
+
};
|
|
154
|
+
renderer.on("frame", onFrame);
|
|
155
|
+
return () => {
|
|
156
|
+
renderer.off("frame", onFrame);
|
|
157
|
+
if (write) deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
|
|
158
|
+
};
|
|
159
|
+
}, [renderer]);
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
onComposingChange?.(composing);
|
|
163
|
+
}, [composing, onComposingChange]);
|
|
164
|
+
|
|
165
|
+
const unmountedRef = useRef(false);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
return () => {
|
|
168
|
+
unmountedRef.current = true;
|
|
169
|
+
void browserRef.current?.close();
|
|
170
|
+
};
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
const launch = (): void => {
|
|
174
|
+
const region = regionOf(regionRef.current);
|
|
175
|
+
const caps = renderer?.capabilities;
|
|
176
|
+
if (launchedRef.current || !region) return;
|
|
177
|
+
if (caps && caps.kitty_graphics === false) {
|
|
178
|
+
setStatus("unsupported");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
launchedRef.current = true;
|
|
182
|
+
const viewport = {
|
|
183
|
+
width: CAPTURE_WIDTH,
|
|
184
|
+
height: Math.round(
|
|
185
|
+
(CAPTURE_WIDTH * region.rows * cellHeightOverWidth(renderer!)) / region.columns,
|
|
186
|
+
),
|
|
187
|
+
};
|
|
188
|
+
viewportRef.current = viewport;
|
|
189
|
+
void (async () => {
|
|
190
|
+
try {
|
|
191
|
+
const browser = await prototypeRendererFactory()({ filePath: prototypePath, viewport });
|
|
192
|
+
// the sheet may have unmounted while Chromium was launching; close the
|
|
193
|
+
// late arrival instead of leaking the process
|
|
194
|
+
if (unmountedRef.current) {
|
|
195
|
+
void browser.close();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
browserRef.current = browser;
|
|
199
|
+
setPng(await browser.screenshot());
|
|
200
|
+
setStatus("ready");
|
|
201
|
+
} catch (error) {
|
|
202
|
+
setErrorMessage(error instanceof Error ? error.message : String(error));
|
|
203
|
+
setStatus("error");
|
|
204
|
+
}
|
|
205
|
+
})();
|
|
206
|
+
};
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
launchRef.current = launch;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const refresh = async (): Promise<void> => {
|
|
212
|
+
const browser = browserRef.current;
|
|
213
|
+
if (browser) setPng(await browser.screenshot());
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const clearSelection = (): void => {
|
|
217
|
+
setSelected(null);
|
|
218
|
+
setOverlayCell(null);
|
|
219
|
+
setActionsOpen(false);
|
|
220
|
+
setComposing(false);
|
|
221
|
+
setDraftText("");
|
|
222
|
+
void browserRef.current
|
|
223
|
+
?.highlight(null)
|
|
224
|
+
.then(refresh)
|
|
225
|
+
.catch(() => undefined);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const onRegionMouseDown = (event: { x: number; y: number }): void => {
|
|
229
|
+
const browser = browserRef.current;
|
|
230
|
+
const region = regionOf(regionRef.current);
|
|
231
|
+
if (status !== "ready" || !browser || !region) return;
|
|
232
|
+
const geometry = {
|
|
233
|
+
x: region.column,
|
|
234
|
+
y: region.row,
|
|
235
|
+
width: region.columns,
|
|
236
|
+
height: region.rows,
|
|
237
|
+
};
|
|
238
|
+
const cssPoint = imageCellToCss(event, geometry, viewportRef.current);
|
|
239
|
+
if (!cssPoint) return;
|
|
240
|
+
void (async () => {
|
|
241
|
+
const element = await browser.elementAt(cssPoint.x, cssPoint.y);
|
|
242
|
+
if (!element) return;
|
|
243
|
+
const cell = cssBoxToCell(element.box, geometry, viewportRef.current);
|
|
244
|
+
const topRaw = cell.row - geometry.y;
|
|
245
|
+
setSelected(element);
|
|
246
|
+
setOverlayCell({
|
|
247
|
+
left: Math.max(0, cell.column - geometry.x),
|
|
248
|
+
top: Math.max(0, topRaw < POPOVER_ROWS ? topRaw + 1 : topRaw - POPOVER_ROWS),
|
|
249
|
+
});
|
|
250
|
+
setActionsOpen(false);
|
|
251
|
+
setComposing(false);
|
|
252
|
+
await browser.highlight(element.selector);
|
|
253
|
+
await refresh();
|
|
254
|
+
})().catch((error) => {
|
|
255
|
+
setErrorMessage(error instanceof Error ? error.message : String(error));
|
|
256
|
+
setStatus("error");
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const onRegionScroll = (event: { scroll?: { direction: string } }): void => {
|
|
261
|
+
const browser = browserRef.current;
|
|
262
|
+
if (status !== "ready" || !browser) return;
|
|
263
|
+
const delta = event.scroll?.direction === "up" ? -SCROLL_STEP : SCROLL_STEP;
|
|
264
|
+
// a selection would drift once the page scrolls under it, so drop it
|
|
265
|
+
clearSelection();
|
|
266
|
+
void browser
|
|
267
|
+
.scrollBy(delta)
|
|
268
|
+
.then(() => refresh())
|
|
269
|
+
.catch(() => undefined);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const commit = (body: string): void => {
|
|
273
|
+
if (!canComment) return;
|
|
274
|
+
if (selected && body.trim()) onCommentElement(selected, body.trim());
|
|
275
|
+
clearSelection();
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// the overlay carries an opaque fill so the popover and compose card read as
|
|
279
|
+
// solid cards over the image, matching the plan surface
|
|
280
|
+
const overlayStyle = {
|
|
281
|
+
position: "absolute" as const,
|
|
282
|
+
left: overlayCell?.left ?? 0,
|
|
283
|
+
top: Math.max(0, overlayCell?.top ?? 0),
|
|
284
|
+
flexDirection: "column" as const,
|
|
285
|
+
backgroundColor: tokens.elevated,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<box
|
|
290
|
+
style={{
|
|
291
|
+
flexGrow: 1,
|
|
292
|
+
flexDirection: "column",
|
|
293
|
+
border: true,
|
|
294
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
295
|
+
borderColor: tokens.text,
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
<box
|
|
299
|
+
ref={(instance: BoxRenderable | null) => {
|
|
300
|
+
regionRef.current = instance;
|
|
301
|
+
}}
|
|
302
|
+
style={{ flexGrow: 1 }}
|
|
303
|
+
onSizeChange={launch}
|
|
304
|
+
onMouseDown={onRegionMouseDown}
|
|
305
|
+
onMouseScroll={onRegionScroll}
|
|
306
|
+
/>
|
|
307
|
+
{status !== "ready" ? (
|
|
308
|
+
<box style={{ position: "absolute", left: 2, top: 1 }}>
|
|
309
|
+
<text fg={tokens.textDim}>{statusLine(status, errorMessage)}</text>
|
|
310
|
+
</box>
|
|
311
|
+
) : null}
|
|
312
|
+
{selected && !composing ? (
|
|
313
|
+
<box style={overlayStyle}>
|
|
314
|
+
<MarkerPopover
|
|
315
|
+
view={actionsOpen ? "actions" : "toolbar"}
|
|
316
|
+
actions={quickActions}
|
|
317
|
+
actionIndex={0}
|
|
318
|
+
canCut={false}
|
|
319
|
+
onComment={() => {
|
|
320
|
+
if (!canComment) return;
|
|
321
|
+
onComposingChange?.(true);
|
|
322
|
+
setComposing(true);
|
|
323
|
+
}}
|
|
324
|
+
onCut={() => undefined}
|
|
325
|
+
onOpenActions={() => setActionsOpen(true)}
|
|
326
|
+
onClose={clearSelection}
|
|
327
|
+
onPickAction={(index) => commit(quickActionBody(quickActions[index]!))}
|
|
328
|
+
onBack={() => setActionsOpen(false)}
|
|
329
|
+
theme={theme}
|
|
330
|
+
/>
|
|
331
|
+
</box>
|
|
332
|
+
) : null}
|
|
333
|
+
{selected && composing ? (
|
|
334
|
+
<box style={overlayStyle}>
|
|
335
|
+
<AnnotationCard
|
|
336
|
+
kind="comment"
|
|
337
|
+
quote={selected.quote}
|
|
338
|
+
draft={{
|
|
339
|
+
text: draftText,
|
|
340
|
+
onInput: setDraftText,
|
|
341
|
+
onSave: () => commit(draftText),
|
|
342
|
+
onSubmit: () => commit(draftText),
|
|
343
|
+
onCancel: clearSelection,
|
|
344
|
+
}}
|
|
345
|
+
theme={theme}
|
|
346
|
+
/>
|
|
347
|
+
</box>
|
|
348
|
+
) : null}
|
|
349
|
+
</box>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function statusLine(status: SheetStatus, errorMessage: string): string {
|
|
354
|
+
if (status === "loading") return "rendering prototype…";
|
|
355
|
+
if (status === "unsupported")
|
|
356
|
+
return "prototype preview needs a graphics terminal (kitty or ghostty)";
|
|
357
|
+
return `prototype preview failed: ${errorMessage}`;
|
|
358
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
const rail = {
|
|
11
|
+
session: fixturePlanSession(),
|
|
12
|
+
authorNames: {},
|
|
13
|
+
selectedId: FIXTURE_ANNOTATIONS[0]!.id,
|
|
14
|
+
resolvedIds: new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id)),
|
|
15
|
+
curationItems: [],
|
|
16
|
+
railTab: "review" as const,
|
|
17
|
+
pendingCount: 2,
|
|
18
|
+
cardEdit: null,
|
|
19
|
+
submitConfirm: null,
|
|
20
|
+
onTabChange: () => {},
|
|
21
|
+
onSelectCard: () => {},
|
|
22
|
+
onActivateCard: () => {},
|
|
23
|
+
onSelectCuration: () => {},
|
|
24
|
+
onUndoCuration: () => {},
|
|
25
|
+
onSubmitRequest: () => {},
|
|
26
|
+
onLaunchHarness: () => {},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const panelCallbacks = { onDividerGrab: () => {}, onToggle: () => {} };
|
|
30
|
+
|
|
31
|
+
/** The plan area to the left of the divider, so the layout reads in context. */
|
|
32
|
+
function PanelFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
33
|
+
return (
|
|
34
|
+
<box
|
|
35
|
+
style={{
|
|
36
|
+
flexDirection: "row",
|
|
37
|
+
width: "100%",
|
|
38
|
+
height: "100%",
|
|
39
|
+
backgroundColor: DARK.background,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<box style={{ flexGrow: 1, paddingLeft: 1 }}>
|
|
43
|
+
<text fg={DARK.textMuted}>The plan takes the width the panel hands back.</text>
|
|
44
|
+
</box>
|
|
45
|
+
{children}
|
|
46
|
+
</box>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ExpandedNarrow: Story = {
|
|
51
|
+
render: () => (
|
|
52
|
+
<PanelFrame>
|
|
53
|
+
<ReviewPanel mode="expanded" width={REVIEW_MIN_WIDTH} rail={rail} {...panelCallbacks} />
|
|
54
|
+
</PanelFrame>
|
|
55
|
+
),
|
|
56
|
+
size: { width: 80, height: 24 },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const ExpandedWide: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<PanelFrame>
|
|
62
|
+
<ReviewPanel mode="expanded" width={REVIEW_MAX_WIDTH} rail={rail} {...panelCallbacks} />
|
|
63
|
+
</PanelFrame>
|
|
64
|
+
),
|
|
65
|
+
expectedColors: [DARK.accent],
|
|
66
|
+
size: { width: 90, height: 24 },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const Compact: Story = {
|
|
70
|
+
render: () => (
|
|
71
|
+
<PanelFrame>
|
|
72
|
+
<ReviewPanel mode="compact" width={34} rail={rail} {...panelCallbacks} />
|
|
73
|
+
</PanelFrame>
|
|
74
|
+
),
|
|
75
|
+
expectedColors: [DARK.accent],
|
|
76
|
+
size: { width: 60, height: 24 },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const Hidden: Story = {
|
|
80
|
+
render: () => (
|
|
81
|
+
<PanelFrame>
|
|
82
|
+
<ReviewPanel mode="hidden" width={34} rail={rail} {...panelCallbacks} />
|
|
83
|
+
</PanelFrame>
|
|
84
|
+
),
|
|
85
|
+
size: { width: 60, height: 24 },
|
|
86
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
return (
|
|
53
|
+
<box
|
|
54
|
+
style={{
|
|
55
|
+
width: REVIEW_COMPACT_WIDTH,
|
|
56
|
+
backgroundColor: tokens.panel,
|
|
57
|
+
flexDirection: "column",
|
|
58
|
+
paddingTop: 1,
|
|
59
|
+
paddingBottom: 1,
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<box style={{ alignItems: "center" }}>
|
|
63
|
+
<text fg={tokens.accent}>{String(annotations.length)}</text>
|
|
64
|
+
</box>
|
|
65
|
+
<box style={{ height: 1 }} />
|
|
66
|
+
<box style={{ flexDirection: "column", alignItems: "center" }}>
|
|
67
|
+
{annotations.map((annotation) => (
|
|
68
|
+
<text key={annotation.id} fg={tokens.accent}>
|
|
69
|
+
●
|
|
70
|
+
</text>
|
|
71
|
+
))}
|
|
72
|
+
</box>
|
|
73
|
+
<box style={{ flexGrow: 1 }} />
|
|
74
|
+
<box style={{ paddingLeft: 1 }} onMouseUp={onExpand}>
|
|
75
|
+
<text fg={tokens.textDim}>{"<"}</text>
|
|
76
|
+
</box>
|
|
77
|
+
</box>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function ReviewPanel({
|
|
82
|
+
mode,
|
|
83
|
+
width,
|
|
84
|
+
onDividerGrab,
|
|
85
|
+
onToggle,
|
|
86
|
+
rail,
|
|
87
|
+
railRef,
|
|
88
|
+
theme,
|
|
89
|
+
}: ReviewPanelProps): React.ReactNode {
|
|
90
|
+
if (mode === "hidden") return null;
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<ReviewDivider onGrab={onDividerGrab} />
|
|
94
|
+
{mode === "expanded" ? (
|
|
95
|
+
<ReviewRail ref={railRef} {...rail} width={width} onCollapse={onToggle} theme={theme} />
|
|
96
|
+
) : (
|
|
97
|
+
<CompactRail annotations={rail.session.annotations} onExpand={onToggle} theme={theme} />
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}
|