@cueloop/client 0.1.0-alpha.44 → 0.1.0-alpha.46
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.plan-review.test.tsx +16 -2
- package/src/App.readonly.test.tsx +3 -3
- package/src/App.submit-confirm.test.tsx +6 -8
- package/src/App.tsx +39 -23
- package/src/App.walk.test.tsx +2 -1
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/components/AnnotationCard.stories.tsx +7 -11
- package/src/components/AnnotationCard.tsx +13 -11
- package/src/components/ConfirmCard.stories.tsx +3 -24
- package/src/components/ConfirmCard.tsx +7 -15
- package/src/components/MarkerPopover.tsx +7 -1
- package/src/components/PlanSheet.tsx +115 -48
- package/src/components/PrototypeSheet.stories.tsx +20 -0
- package/src/components/PrototypeSheet.tsx +228 -0
- package/src/components/ReviewRail.stories.tsx +0 -2
- package/src/components/ReviewRail.tsx +2 -9
- package/src/components/__snapshots__/stories.test.tsx.snap +37 -9
- package/src/intent-dispatch.test.ts +1 -0
- package/src/prototype-browser.test.ts +55 -0
- package/src/prototype-browser.ts +218 -0
- package/src/session-controller.ts +14 -0
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +105 -0
- package/src/view-plan.ts +96 -9
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
blockRuns,
|
|
17
17
|
displayText,
|
|
18
18
|
overlayMarks,
|
|
19
|
-
|
|
19
|
+
renderedOffsetAtOrAfter,
|
|
20
|
+
renderedOffsetAtOrBefore,
|
|
21
|
+
safeLinkHref,
|
|
20
22
|
workRangeForRendered,
|
|
21
23
|
type DisplayBlock,
|
|
22
24
|
type Mark,
|
|
@@ -33,6 +35,20 @@ import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
|
33
35
|
/** A cut block reads as removed: struck through and grayed, never red. */
|
|
34
36
|
const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
|
|
35
37
|
|
|
38
|
+
/** Inline-emphasis attributes, reused per run so the render loop allocates none. */
|
|
39
|
+
const STRONG_ATTRIBUTES = createTextAttributes({ bold: true });
|
|
40
|
+
const EM_ATTRIBUTES = createTextAttributes({ italic: true });
|
|
41
|
+
const STRIKE_ATTRIBUTES = createTextAttributes({ strikethrough: true });
|
|
42
|
+
const LINK_ATTRIBUTES = createTextAttributes({ underline: true });
|
|
43
|
+
/** Block-level base attributes: headings read bold, blockquotes read italic. */
|
|
44
|
+
const HEADING_ATTRIBUTES = createTextAttributes({ bold: true });
|
|
45
|
+
const QUOTE_ATTRIBUTES = createTextAttributes({ italic: true });
|
|
46
|
+
|
|
47
|
+
/** The popover toolbar card is 3 rows tall and sits flush above the marked words. */
|
|
48
|
+
const POPOVER_ROWS_ABOVE = 3;
|
|
49
|
+
/** Widest toolbar row ("comment · cut · actions · [x]") plus border and padding. */
|
|
50
|
+
const POPOVER_TOOLBAR_COLUMNS = 33;
|
|
51
|
+
|
|
36
52
|
export interface PlanSelection {
|
|
37
53
|
displayIndex: number;
|
|
38
54
|
start: number;
|
|
@@ -73,6 +89,13 @@ export interface PlanSheetProps {
|
|
|
73
89
|
popover: PlanPopoverState | null;
|
|
74
90
|
editOrphanCount: number;
|
|
75
91
|
onLineActivate: (displayIndex: number) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Fires on any mouse release inside the sheet. A drag released outside a
|
|
94
|
+
* block's text (the gutter, past a line end, a gap between blocks) never
|
|
95
|
+
* reaches a block's own handler, so this fallback lets the app still turn
|
|
96
|
+
* the finished drag's native selection into a span.
|
|
97
|
+
*/
|
|
98
|
+
onSelectionRelease?: () => void;
|
|
76
99
|
theme?: Theme;
|
|
77
100
|
}
|
|
78
101
|
|
|
@@ -108,6 +131,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
108
131
|
popover,
|
|
109
132
|
editOrphanCount,
|
|
110
133
|
onLineActivate,
|
|
134
|
+
onSelectionRelease,
|
|
111
135
|
theme,
|
|
112
136
|
}: PlanSheetProps,
|
|
113
137
|
handleRef,
|
|
@@ -146,9 +170,11 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
146
170
|
if (!renderer) return;
|
|
147
171
|
const blockRef = blockRefs.current.get(span.displayIndex);
|
|
148
172
|
if (!blockRef) return;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
// a span over an emphasized word starts/ends on concealed markers with
|
|
174
|
+
// no rendered cell; snap to the visible characters inside them
|
|
175
|
+
const renderedStart = renderedOffsetAtOrAfter(blockRef.runs, span.start);
|
|
176
|
+
const renderedEnd = renderedOffsetAtOrBefore(blockRef.runs, span.end - 1);
|
|
177
|
+
if (renderedStart === null || renderedEnd === null || renderedEnd < renderedStart) return;
|
|
152
178
|
const startPosition = positionOfRenderedOffset(blockRef.renderable, renderedStart);
|
|
153
179
|
const endPosition = positionOfRenderedOffset(blockRef.renderable, renderedEnd);
|
|
154
180
|
renderer.startSelection(blockRef.renderable, startPosition.x, startPosition.y);
|
|
@@ -166,24 +192,43 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
166
192
|
},
|
|
167
193
|
}));
|
|
168
194
|
|
|
169
|
-
// the popover
|
|
170
|
-
//
|
|
171
|
-
|
|
195
|
+
// the popover sits flush above the marked words, anchored to the
|
|
196
|
+
// selection start mapped through the wrap geometry (line + column, not the
|
|
197
|
+
// block's linear offset). It renders as the LAST child of the scrollbox in
|
|
198
|
+
// content coordinates: content coordinates are scroll-invariant so the card
|
|
199
|
+
// tracks its block, and the last sibling paints over every block it floats
|
|
200
|
+
// across. Flips below when the line sits too near the viewport top.
|
|
201
|
+
const [popoverTop, setPopoverTop] = useState(0);
|
|
172
202
|
const [popoverLeft, setPopoverLeft] = useState(2);
|
|
203
|
+
// a terminal resize rewraps every block; re-run the anchor math against the
|
|
204
|
+
// new geometry instead of leaving the card at its pre-resize cell
|
|
205
|
+
const [resizeEpoch, setResizeEpoch] = useState(0);
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!renderer) return;
|
|
208
|
+
const onResize = (): void => setResizeEpoch((epoch) => epoch + 1);
|
|
209
|
+
renderer.on("resize", onResize);
|
|
210
|
+
return () => void renderer.off("resize", onResize);
|
|
211
|
+
}, [renderer]);
|
|
173
212
|
useEffect(() => {
|
|
174
213
|
if (!popover) return;
|
|
175
214
|
const blockRef = blockRefs.current.get(popover.displayIndex);
|
|
176
215
|
const scrollbox = scrollRef.current;
|
|
177
216
|
if (!blockRef || !scrollbox) return;
|
|
178
|
-
|
|
179
|
-
setPopoverFlipBelow(blockRef.renderable.y - scrollbox.y < 4);
|
|
180
|
-
// left-anchor to the start of the selection: gutter width + its column
|
|
181
|
-
const startColumn =
|
|
217
|
+
const renderedStart =
|
|
182
218
|
activeSpan && activeSpan.displayIndex === popover.displayIndex
|
|
183
|
-
? (
|
|
219
|
+
? (renderedOffsetAtOrAfter(blockRef.runs, activeSpan.start) ?? 0)
|
|
184
220
|
: 0;
|
|
185
|
-
|
|
186
|
-
|
|
221
|
+
const startPosition = positionOfRenderedOffset(blockRef.renderable, renderedStart);
|
|
222
|
+
const content = scrollbox.content;
|
|
223
|
+
const anchorLeft = startPosition.x - content.x;
|
|
224
|
+
const anchorTop = startPosition.y - content.y;
|
|
225
|
+
// clamp so the toolbar card never runs past the content's right edge
|
|
226
|
+
const maxLeft = Math.max(0, content.width - POPOVER_TOOLBAR_COLUMNS);
|
|
227
|
+
setPopoverLeft(Math.min(anchorLeft, maxLeft));
|
|
228
|
+
// only the toolbar (3 rows) + gap must fit above; the dropdown flows down
|
|
229
|
+
const lineViewportRow = startPosition.y - scrollbox.y;
|
|
230
|
+
setPopoverTop(anchorTop + (lineViewportRow < POPOVER_ROWS_ABOVE ? 1 : -POPOVER_ROWS_ABOVE));
|
|
231
|
+
}, [popover, activeSpan, display, cursor, resizeEpoch]);
|
|
187
232
|
|
|
188
233
|
const registerBlock = (
|
|
189
234
|
displayIndex: number,
|
|
@@ -235,13 +280,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
235
280
|
<box
|
|
236
281
|
key={displayIndex}
|
|
237
282
|
id={`plan-block-${displayIndex}`}
|
|
238
|
-
|
|
239
|
-
// later blocks it floats across (siblings paint in z-index order)
|
|
240
|
-
style={{
|
|
241
|
-
flexDirection: "row",
|
|
242
|
-
marginTop: gap,
|
|
243
|
-
zIndex: popover?.displayIndex === displayIndex ? 10 : undefined,
|
|
244
|
-
}}
|
|
283
|
+
style={{ flexDirection: "row", marginTop: gap }}
|
|
245
284
|
>
|
|
246
285
|
<text selectable={false}>
|
|
247
286
|
<span fg={isCursor ? tokens.accent : tokens.textDim}>{isCursor ? "▎ " : " "}</span>
|
|
@@ -258,31 +297,22 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
258
297
|
}
|
|
259
298
|
onMouseUp={() => onLineActivate(displayIndex)}
|
|
260
299
|
>
|
|
261
|
-
{runs.map((run, runIndex) =>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
300
|
+
{runs.map((run, runIndex) => {
|
|
301
|
+
const url = run.role === "link" ? safeLinkHref(run.href) : undefined;
|
|
302
|
+
return (
|
|
303
|
+
<span
|
|
304
|
+
key={runIndex}
|
|
305
|
+
{...runStyle(run, block, tokens)}
|
|
306
|
+
{...(url ? { link: { url } } : {})}
|
|
307
|
+
>
|
|
308
|
+
{run.text}
|
|
309
|
+
</span>
|
|
310
|
+
);
|
|
311
|
+
})}
|
|
266
312
|
{showsChangeTag(block) ? (
|
|
267
313
|
<span fg={tagColor(block, tokens)}> [{tagLabel(block)}]</span>
|
|
268
314
|
) : null}
|
|
269
315
|
</text>
|
|
270
|
-
{popover && popover.displayIndex === displayIndex ? (
|
|
271
|
-
// float the card over the block, centered, with a one-row gap; flip
|
|
272
|
-
// below only when there is no room above (near the viewport top)
|
|
273
|
-
<box
|
|
274
|
-
style={{
|
|
275
|
-
position: "absolute",
|
|
276
|
-
left: popoverLeft,
|
|
277
|
-
// fixed offset: the toolbar sits one row above the selection and
|
|
278
|
-
// stays put; the dropdown flows down from it, over the selection
|
|
279
|
-
top: popoverFlipBelow ? 1 : -4,
|
|
280
|
-
flexDirection: "column",
|
|
281
|
-
}}
|
|
282
|
-
>
|
|
283
|
-
<MarkerPopover {...popover} theme={theme} />
|
|
284
|
-
</box>
|
|
285
|
-
) : null}
|
|
286
316
|
</box>,
|
|
287
317
|
);
|
|
288
318
|
}
|
|
@@ -298,9 +328,26 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
298
328
|
);
|
|
299
329
|
}
|
|
300
330
|
}
|
|
331
|
+
if (popover) {
|
|
332
|
+
// last child on purpose: later siblings paint on top, so the floating card
|
|
333
|
+
// covers every block it overlaps regardless of scroll position
|
|
334
|
+
children.push(
|
|
335
|
+
<box
|
|
336
|
+
key="marker-popover"
|
|
337
|
+
style={{
|
|
338
|
+
position: "absolute",
|
|
339
|
+
left: popoverLeft,
|
|
340
|
+
top: popoverTop,
|
|
341
|
+
flexDirection: "column",
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
<MarkerPopover {...popover} theme={theme} />
|
|
345
|
+
</box>,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
301
348
|
|
|
302
349
|
return (
|
|
303
|
-
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
350
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }} onMouseUp={onSelectionRelease}>
|
|
304
351
|
<box
|
|
305
352
|
style={{
|
|
306
353
|
flexGrow: 1,
|
|
@@ -366,15 +413,25 @@ function runStyle(
|
|
|
366
413
|
): { fg?: string; bg?: string; attributes?: number } {
|
|
367
414
|
// a cut block reads as removed: every run struck through and grayed, never red
|
|
368
415
|
if (block.type === "del") return { fg: tokens.textDim, attributes: CUT_ATTRIBUTES };
|
|
416
|
+
const isHeading = block.kind === "h1" || block.kind === "h2" || block.kind === "h3";
|
|
417
|
+
const isQuote = block.kind === "quote";
|
|
418
|
+
// headings are all bold; level reads from descending brightness alone (a
|
|
419
|
+
// terminal cannot scale font size), leaving the salmon accent to annotations
|
|
369
420
|
const headingFg =
|
|
370
421
|
block.kind === "h1"
|
|
371
422
|
? tokens.text
|
|
372
|
-
: block.kind === "h2"
|
|
373
|
-
? tokens.
|
|
374
|
-
:
|
|
423
|
+
: block.kind === "h2"
|
|
424
|
+
? tokens.textMuted
|
|
425
|
+
: block.kind === "h3"
|
|
426
|
+
? tokens.textDim
|
|
427
|
+
: undefined;
|
|
428
|
+
// block-level base: headings bold, quotes muted italic; inline roles compose on top
|
|
429
|
+
const baseFg = headingFg ?? (isQuote ? tokens.textMuted : tokens.text);
|
|
430
|
+
const baseAttributes = (isHeading ? HEADING_ATTRIBUTES : 0) | (isQuote ? QUOTE_ATTRIBUTES : 0);
|
|
431
|
+
const withBase = (roleAttributes: number): number => baseAttributes | roleAttributes;
|
|
375
432
|
switch (run.role) {
|
|
376
433
|
case "ins":
|
|
377
|
-
return { fg: tokens.insertedForeground };
|
|
434
|
+
return { fg: tokens.insertedForeground, attributes: baseAttributes || undefined };
|
|
378
435
|
case "del":
|
|
379
436
|
return { fg: tokens.deletedForeground };
|
|
380
437
|
case "mark-comment":
|
|
@@ -383,7 +440,17 @@ function runStyle(
|
|
|
383
440
|
return { fg: tokens.accentInk, bg: tokens.accent };
|
|
384
441
|
case "kspan":
|
|
385
442
|
return { fg: tokens.accentInk, bg: tokens.accent };
|
|
443
|
+
case "strong":
|
|
444
|
+
return { fg: baseFg, attributes: withBase(STRONG_ATTRIBUTES) };
|
|
445
|
+
case "em":
|
|
446
|
+
return { fg: baseFg, attributes: withBase(EM_ATTRIBUTES) };
|
|
447
|
+
case "code":
|
|
448
|
+
return { fg: tokens.text, bg: tokens.elevated };
|
|
449
|
+
case "strike":
|
|
450
|
+
return { fg: tokens.textMuted, attributes: withBase(STRIKE_ATTRIBUTES) };
|
|
451
|
+
case "link":
|
|
452
|
+
return { fg: tokens.blue, attributes: withBase(LINK_ATTRIBUTES) };
|
|
386
453
|
default:
|
|
387
|
-
return { fg:
|
|
454
|
+
return { fg: baseFg, attributes: baseAttributes || undefined };
|
|
388
455
|
}
|
|
389
456
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { PrototypeSheet } from "./PrototypeSheet";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "PrototypeSheet" };
|
|
7
|
+
|
|
8
|
+
// The story renderer reports no kitty graphics, so the sheet shows its fallback.
|
|
9
|
+
export const Unsupported: Story = {
|
|
10
|
+
render: () => (
|
|
11
|
+
<PrototypeSheet
|
|
12
|
+
prototypePath="/tmp/example.html"
|
|
13
|
+
quickActions={[]}
|
|
14
|
+
canComment={true}
|
|
15
|
+
onCommentElement={() => {}}
|
|
16
|
+
/>
|
|
17
|
+
),
|
|
18
|
+
expectedColors: [DARK.textDim],
|
|
19
|
+
size: { width: 80, height: 24 },
|
|
20
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prototype review surface: renders the HTML as an image and turns a click into
|
|
3
|
+
* a DOM-element selection, so the shared marker actions bar and compose card
|
|
4
|
+
* annotate an element rather than a text span.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
8
|
+
import type { ImageRenderable } from "@opentui/core";
|
|
9
|
+
import { useRenderer } from "@opentui/react";
|
|
10
|
+
import type { Theme } from "../theme";
|
|
11
|
+
import { quickActionBody, type QuickAction } from "../config";
|
|
12
|
+
import { useComponentTheme } from "./theme-context";
|
|
13
|
+
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
14
|
+
import { MarkerPopover } from "./MarkerPopover";
|
|
15
|
+
import { AnnotationCard } from "./AnnotationCard";
|
|
16
|
+
import {
|
|
17
|
+
cssBoxToCell,
|
|
18
|
+
imageCellToCss,
|
|
19
|
+
launchPrototypeRenderer,
|
|
20
|
+
type PrototypeElement,
|
|
21
|
+
type PrototypeRenderer,
|
|
22
|
+
type PrototypeViewport,
|
|
23
|
+
} from "../prototype-browser";
|
|
24
|
+
|
|
25
|
+
export interface PrototypeSheetProps {
|
|
26
|
+
prototypePath: string;
|
|
27
|
+
quickActions: QuickAction[];
|
|
28
|
+
canComment: boolean;
|
|
29
|
+
onCommentElement: (element: PrototypeElement, body: string) => void;
|
|
30
|
+
theme?: Theme;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type SheetStatus = "loading" | "ready" | "unsupported" | "error";
|
|
34
|
+
|
|
35
|
+
const DEFAULT_CELL_ASPECT = 2;
|
|
36
|
+
const POPOVER_ROWS = 3;
|
|
37
|
+
|
|
38
|
+
export function PrototypeSheet({
|
|
39
|
+
prototypePath,
|
|
40
|
+
quickActions,
|
|
41
|
+
canComment,
|
|
42
|
+
onCommentElement,
|
|
43
|
+
theme,
|
|
44
|
+
}: PrototypeSheetProps): React.ReactNode {
|
|
45
|
+
const tokens = useComponentTheme(theme);
|
|
46
|
+
const renderer = useRenderer();
|
|
47
|
+
const imageRef = useRef<ImageRenderable | null>(null);
|
|
48
|
+
const browserRef = useRef<PrototypeRenderer | null>(null);
|
|
49
|
+
const launchedRef = useRef(false);
|
|
50
|
+
const viewportRef = useRef<PrototypeViewport>({ width: 1280, height: 800 });
|
|
51
|
+
|
|
52
|
+
const [status, setStatus] = useState<SheetStatus>("loading");
|
|
53
|
+
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
54
|
+
const [png, setPng] = useState<Uint8Array | null>(null);
|
|
55
|
+
const [selected, setSelected] = useState<PrototypeElement | null>(null);
|
|
56
|
+
const [overlayCell, setOverlayCell] = useState<{ left: number; top: number } | null>(null);
|
|
57
|
+
const [actionsOpen, setActionsOpen] = useState(false);
|
|
58
|
+
const [composing, setComposing] = useState(false);
|
|
59
|
+
const [draftText, setDraftText] = useState("");
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
return () => {
|
|
63
|
+
void browserRef.current?.close();
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const viewportFor = (columns: number, rows: number): PrototypeViewport => {
|
|
68
|
+
const resolution = renderer?.resolution ?? null;
|
|
69
|
+
const cellWidth = resolution ? resolution.width / renderer!.terminalWidth : 1;
|
|
70
|
+
const cellHeight = resolution
|
|
71
|
+
? resolution.height / renderer!.terminalHeight
|
|
72
|
+
: DEFAULT_CELL_ASPECT;
|
|
73
|
+
return {
|
|
74
|
+
width: Math.max(320, Math.round(columns * cellWidth)),
|
|
75
|
+
height: Math.max(240, Math.round(rows * cellHeight)),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const launch = (): void => {
|
|
80
|
+
const image = imageRef.current;
|
|
81
|
+
if (launchedRef.current || !image || image.width < 1 || image.height < 1) return;
|
|
82
|
+
if (renderer?.capabilities && renderer.capabilities.kitty_graphics === false) {
|
|
83
|
+
setStatus("unsupported");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
launchedRef.current = true;
|
|
87
|
+
const viewport = viewportFor(image.width, image.height);
|
|
88
|
+
viewportRef.current = viewport;
|
|
89
|
+
void (async () => {
|
|
90
|
+
try {
|
|
91
|
+
const browser = await launchPrototypeRenderer({ filePath: prototypePath, viewport });
|
|
92
|
+
browserRef.current = browser;
|
|
93
|
+
setPng(await browser.screenshot());
|
|
94
|
+
setStatus("ready");
|
|
95
|
+
} catch (error) {
|
|
96
|
+
setErrorMessage(error instanceof Error ? error.message : String(error));
|
|
97
|
+
setStatus("error");
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const refresh = async (): Promise<void> => {
|
|
103
|
+
const browser = browserRef.current;
|
|
104
|
+
if (browser) setPng(await browser.screenshot());
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const clearSelection = (): void => {
|
|
108
|
+
setSelected(null);
|
|
109
|
+
setOverlayCell(null);
|
|
110
|
+
setActionsOpen(false);
|
|
111
|
+
setComposing(false);
|
|
112
|
+
setDraftText("");
|
|
113
|
+
void browserRef.current
|
|
114
|
+
?.highlight(null)
|
|
115
|
+
.then(refresh)
|
|
116
|
+
.catch(() => undefined);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const onImageMouseDown = (event: { x: number; y: number }): void => {
|
|
120
|
+
const browser = browserRef.current;
|
|
121
|
+
const image = imageRef.current;
|
|
122
|
+
if (status !== "ready" || !browser || !image) return;
|
|
123
|
+
const geometry = { x: image.x, y: image.y, width: image.width, height: image.height };
|
|
124
|
+
const cssPoint = imageCellToCss(event, geometry, viewportRef.current);
|
|
125
|
+
if (!cssPoint) return;
|
|
126
|
+
void (async () => {
|
|
127
|
+
const element = await browser.elementAt(cssPoint.x, cssPoint.y);
|
|
128
|
+
if (!element) return;
|
|
129
|
+
const cell = cssBoxToCell(element.box, geometry, viewportRef.current);
|
|
130
|
+
const topRaw = cell.row - geometry.y;
|
|
131
|
+
setSelected(element);
|
|
132
|
+
setOverlayCell({
|
|
133
|
+
left: Math.max(0, cell.column - geometry.x),
|
|
134
|
+
top: Math.max(0, topRaw < POPOVER_ROWS ? topRaw + 1 : topRaw - POPOVER_ROWS),
|
|
135
|
+
});
|
|
136
|
+
setActionsOpen(false);
|
|
137
|
+
setComposing(false);
|
|
138
|
+
await browser.highlight(element.selector);
|
|
139
|
+
await refresh();
|
|
140
|
+
})().catch((error) => {
|
|
141
|
+
setErrorMessage(error instanceof Error ? error.message : String(error));
|
|
142
|
+
setStatus("error");
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const commit = (body: string): void => {
|
|
147
|
+
if (!canComment) return;
|
|
148
|
+
if (selected && body.trim()) onCommentElement(selected, body.trim());
|
|
149
|
+
clearSelection();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const overlayLeft = overlayCell?.left ?? 0;
|
|
153
|
+
const overlayTop = Math.max(0, overlayCell?.top ?? 0);
|
|
154
|
+
const overlayStyle = {
|
|
155
|
+
position: "absolute" as const,
|
|
156
|
+
left: overlayLeft,
|
|
157
|
+
top: overlayTop,
|
|
158
|
+
flexDirection: "column" as const,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<box
|
|
163
|
+
style={{
|
|
164
|
+
flexGrow: 1,
|
|
165
|
+
flexDirection: "column",
|
|
166
|
+
border: true,
|
|
167
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
168
|
+
borderColor: tokens.text,
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<image
|
|
172
|
+
ref={(instance: ImageRenderable | null) => {
|
|
173
|
+
imageRef.current = instance;
|
|
174
|
+
}}
|
|
175
|
+
{...(png ? { source: png } : {})}
|
|
176
|
+
fit="fill"
|
|
177
|
+
protocol="auto"
|
|
178
|
+
style={{ flexGrow: 1 }}
|
|
179
|
+
onSizeChange={launch}
|
|
180
|
+
onMouseDown={onImageMouseDown}
|
|
181
|
+
/>
|
|
182
|
+
{status !== "ready" ? (
|
|
183
|
+
<box style={{ position: "absolute", left: 2, top: 1 }}>
|
|
184
|
+
<text fg={tokens.textDim}>{statusLine(status, errorMessage)}</text>
|
|
185
|
+
</box>
|
|
186
|
+
) : null}
|
|
187
|
+
{selected && !composing ? (
|
|
188
|
+
<box style={overlayStyle}>
|
|
189
|
+
<MarkerPopover
|
|
190
|
+
view={actionsOpen ? "actions" : "toolbar"}
|
|
191
|
+
actions={quickActions}
|
|
192
|
+
actionIndex={0}
|
|
193
|
+
canCut={false}
|
|
194
|
+
onComment={() => (canComment ? setComposing(true) : undefined)}
|
|
195
|
+
onCut={() => undefined}
|
|
196
|
+
onOpenActions={() => setActionsOpen(true)}
|
|
197
|
+
onClose={clearSelection}
|
|
198
|
+
onPickAction={(index) => commit(quickActionBody(quickActions[index]!))}
|
|
199
|
+
onBack={() => setActionsOpen(false)}
|
|
200
|
+
theme={theme}
|
|
201
|
+
/>
|
|
202
|
+
</box>
|
|
203
|
+
) : null}
|
|
204
|
+
{selected && composing ? (
|
|
205
|
+
<box style={overlayStyle}>
|
|
206
|
+
<AnnotationCard
|
|
207
|
+
kind="comment"
|
|
208
|
+
quote={selected.quote}
|
|
209
|
+
draft={{
|
|
210
|
+
text: draftText,
|
|
211
|
+
onInput: setDraftText,
|
|
212
|
+
onSave: () => commit(draftText),
|
|
213
|
+
onCancel: clearSelection,
|
|
214
|
+
}}
|
|
215
|
+
theme={theme}
|
|
216
|
+
/>
|
|
217
|
+
</box>
|
|
218
|
+
) : null}
|
|
219
|
+
</box>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function statusLine(status: SheetStatus, errorMessage: string): string {
|
|
224
|
+
if (status === "loading") return "rendering prototype…";
|
|
225
|
+
if (status === "unsupported")
|
|
226
|
+
return "prototype preview needs a graphics terminal (kitty or ghostty)";
|
|
227
|
+
return `prototype preview failed: ${errorMessage}`;
|
|
228
|
+
}
|
|
@@ -75,11 +75,6 @@ export interface ReviewRailProps {
|
|
|
75
75
|
theme?: Theme;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** Forward-compatible: open annotation kinds may carry a blocking flag. */
|
|
79
|
-
export function annotationBlocking(annotation: Annotation): boolean {
|
|
80
|
-
return (annotation as Annotation & { blocking?: boolean }).blocking === true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
78
|
export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function ReviewRail(
|
|
84
79
|
{
|
|
85
80
|
session,
|
|
@@ -133,15 +128,13 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
133
128
|
body: annotation.body,
|
|
134
129
|
isSelected: annotation.id === selectedId,
|
|
135
130
|
isOrphan: resolvedIds !== null && !resolvedIds.has(annotation.id),
|
|
136
|
-
isBlocking: annotationBlocking(annotation),
|
|
137
131
|
// border-title author: a collaborator's name, "agent" for agent notes, else "me"
|
|
138
132
|
author: annotation.author
|
|
139
133
|
? resolveDisplayName(annotation.author, session.participants, authorNames)
|
|
140
134
|
: isAgentNote(annotation)
|
|
141
135
|
? "agent"
|
|
142
136
|
: "me",
|
|
143
|
-
|
|
144
|
-
tone: annotation.author && !isAgentNote(annotation) ? tokens.blue : tokens.accent,
|
|
137
|
+
tone: tokens.blue,
|
|
145
138
|
editing:
|
|
146
139
|
cardEdit && cardEdit.id === annotation.id
|
|
147
140
|
? {
|
|
@@ -296,7 +289,7 @@ function RemovalCard({
|
|
|
296
289
|
title={title}
|
|
297
290
|
contentRows={contentRows}
|
|
298
291
|
borderColor={tokens.red}
|
|
299
|
-
backgroundColor=
|
|
292
|
+
backgroundColor="transparent"
|
|
300
293
|
theme={theme}
|
|
301
294
|
>
|
|
302
295
|
{previewLines.length === 0 ? (
|
|
@@ -84,8 +84,8 @@ exports[`stories catalog AnnotationCard/SavedEditing renders, snapshots, and car
|
|
|
84
84
|
"
|
|
85
85
|
`;
|
|
86
86
|
|
|
87
|
-
exports[`stories catalog AnnotationCard/
|
|
88
|
-
"╭─ COMMENT · me ·
|
|
87
|
+
exports[`stories catalog AnnotationCard/SavedOrphan renders, snapshots, and carries its colors: AnnotationCard/SavedOrphan 1`] = `
|
|
88
|
+
"╭─ COMMENT · me · ORPHANED ────────────────────────────────────────────────────╮
|
|
89
89
|
│ "a removed passage" │
|
|
90
90
|
│ The passage is gone. │
|
|
91
91
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
@@ -338,8 +338,6 @@ exports[`stories catalog CompletionOverlay/FeedbackCounting renders, snapshots,
|
|
|
338
338
|
|
|
339
339
|
exports[`stories catalog ConfirmCard/ApproveDefault renders, snapshots, and carries its colors: ConfirmCard/ApproveDefault 1`] = `
|
|
340
340
|
"╭─ submit review ─────────────────────╮
|
|
341
|
-
│ 0 annotations · 0 blocking │
|
|
342
|
-
│ │
|
|
343
341
|
│ Comment [Approve] Changes │
|
|
344
342
|
│ │
|
|
345
343
|
│ summary for the agent (optional) │
|
|
@@ -351,13 +349,13 @@ exports[`stories catalog ConfirmCard/ApproveDefault renders, snapshots, and carr
|
|
|
351
349
|
|
|
352
350
|
|
|
353
351
|
|
|
352
|
+
|
|
353
|
+
|
|
354
354
|
"
|
|
355
355
|
`;
|
|
356
356
|
|
|
357
357
|
exports[`stories catalog ConfirmCard/RequestChanges renders, snapshots, and carries its colors: ConfirmCard/RequestChanges 1`] = `
|
|
358
358
|
"╭─ submit review ─────────────────────╮
|
|
359
|
-
│ 3 annotations · 1 blocking │
|
|
360
|
-
│ │
|
|
361
359
|
│ Comment Approve [Changes] │
|
|
362
360
|
│ │
|
|
363
361
|
│ Tighten the steps. │
|
|
@@ -369,12 +367,13 @@ exports[`stories catalog ConfirmCard/RequestChanges renders, snapshots, and carr
|
|
|
369
367
|
|
|
370
368
|
|
|
371
369
|
|
|
370
|
+
|
|
371
|
+
|
|
372
372
|
"
|
|
373
373
|
`;
|
|
374
374
|
|
|
375
375
|
exports[`stories catalog ConfirmCard/WithViewedSummary renders, snapshots, and carries its colors: ConfirmCard/WithViewedSummary 1`] = `
|
|
376
376
|
"╭─ submit review ─────────────────────╮
|
|
377
|
-
│ 1 annotations · 0 blocking │
|
|
378
377
|
│ 2/3 files viewed │
|
|
379
378
|
│ │
|
|
380
379
|
│ Comment [Approve] Changes │
|
|
@@ -387,6 +386,7 @@ exports[`stories catalog ConfirmCard/WithViewedSummary renders, snapshots, and c
|
|
|
387
386
|
|
|
388
387
|
|
|
389
388
|
|
|
389
|
+
|
|
390
390
|
"
|
|
391
391
|
`;
|
|
392
392
|
|
|
@@ -1121,9 +1121,9 @@ exports[`stories catalog ReviewRail/SubmitConfirmOpen renders, snapshots, and ca
|
|
|
1121
1121
|
|
|
1122
1122
|
|
|
1123
1123
|
|
|
1124
|
+
|
|
1125
|
+
|
|
1124
1126
|
╭─ submit review ───────────────╮
|
|
1125
|
-
│ 2 annotations · 0 blocking │
|
|
1126
|
-
│ │
|
|
1127
1127
|
│ Comment Approve [Changes] │
|
|
1128
1128
|
│ │
|
|
1129
1129
|
│ summary for the agent (optiona│
|
|
@@ -1825,5 +1825,33 @@ exports[`stories catalog theme-context/ProviderSwap renders, snapshots, and carr
|
|
|
1825
1825
|
|
|
1826
1826
|
|
|
1827
1827
|
|
|
1828
|
+
"
|
|
1829
|
+
`;
|
|
1830
|
+
|
|
1831
|
+
exports[`stories catalog PrototypeSheet/Unsupported renders, snapshots, and carries its colors: PrototypeSheet/Unsupported 1`] = `
|
|
1832
|
+
"╭──────────────────────────────────────────────────────────────────────────────╮
|
|
1833
|
+
│ │
|
|
1834
|
+
│ rendering prototype… │
|
|
1835
|
+
│ │
|
|
1836
|
+
│ │
|
|
1837
|
+
│ │
|
|
1838
|
+
│ │
|
|
1839
|
+
│ │
|
|
1840
|
+
│ │
|
|
1841
|
+
│ │
|
|
1842
|
+
│ │
|
|
1843
|
+
│ │
|
|
1844
|
+
│ │
|
|
1845
|
+
│ │
|
|
1846
|
+
│ │
|
|
1847
|
+
│ │
|
|
1848
|
+
│ │
|
|
1849
|
+
│ │
|
|
1850
|
+
│ │
|
|
1851
|
+
│ │
|
|
1852
|
+
│ │
|
|
1853
|
+
│ │
|
|
1854
|
+
│ │
|
|
1855
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
1828
1856
|
"
|
|
1829
1857
|
`;
|