@cueloop/client 0.1.0-alpha.32 → 0.1.0-alpha.34
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 +27 -26
- package/src/App.autoclose.test.tsx +7 -4
- package/src/App.diff.test.tsx +131 -5
- package/src/App.export.test.tsx +23 -5
- package/src/App.plan-review.test.tsx +133 -32
- package/src/App.readonly.test.tsx +35 -13
- package/src/App.rendering.test.tsx +14 -4
- package/src/App.review-panel.test.tsx +27 -15
- package/src/App.submit-confirm.test.tsx +14 -13
- package/src/App.test.tsx +50 -13
- package/src/App.toast.test.tsx +75 -0
- package/src/App.tsx +505 -101
- package/src/App.walk.test.tsx +31 -11
- package/src/attribution.ts +8 -2
- package/src/clipboard.ts +5 -1
- package/src/components/AnnotationCard.stories.tsx +29 -10
- package/src/components/AnnotationCard.tsx +57 -37
- package/src/components/Breadcrumb.tsx +7 -1
- package/src/components/CodeBlock.stories.tsx +19 -2
- package/src/components/CodeBlock.tsx +24 -9
- package/src/components/CompletionOverlay.tsx +13 -3
- package/src/components/ConfirmCard.stories.tsx +22 -3
- package/src/components/ConfirmCard.tsx +23 -5
- package/src/components/ConfirmDialog.tsx +10 -2
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +35 -1
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +213 -101
- package/src/components/InboxList.stories.tsx +16 -3
- package/src/components/InboxList.tsx +17 -10
- 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 +102 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +74 -0
- package/src/components/PlanSheet.stories.tsx +35 -3
- package/src/components/PlanSheet.tsx +160 -100
- package/src/components/PromptDialog.tsx +19 -3
- package/src/components/ReviewPanel.stories.tsx +17 -19
- package/src/components/ReviewPanel.tsx +17 -32
- package/src/components/ReviewRail.stories.tsx +159 -47
- package/src/components/ReviewRail.tsx +206 -62
- package/src/components/SettingsDialog.stories.tsx +51 -3
- package/src/components/SettingsDialog.tsx +11 -5
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/SettingsRows.tsx +19 -3
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +44 -0
- package/src/components/WalkWizard.stories.tsx +13 -2
- package/src/components/WalkWizard.tsx +9 -3
- package/src/components/__snapshots__/stories.test.tsx.snap +853 -623
- 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 +2 -2
- package/src/components/primitives/Button.tsx +5 -1
- package/src/components/primitives/Dialog.tsx +12 -1
- package/src/components/primitives/Tabs.tsx +40 -39
- package/src/components/stories-app.tsx +34 -6
- package/src/components/story-fixtures.ts +11 -3
- package/src/components/story.ts +3 -1
- package/src/components/syntax-highlight.ts +47 -12
- package/src/components/theme-context.stories.tsx +6 -1
- package/src/components/theme-context.tsx +7 -1
- package/src/config.test.ts +178 -2
- package/src/config.ts +121 -23
- 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 +13 -4
- package/src/editor.ts +31 -5
- package/src/herdr.ts +8 -2
- package/src/index.ts +9 -1
- package/src/integrations.ts +3 -1
- package/src/intent-dispatch.test.ts +193 -9
- package/src/intent-dispatch.ts +151 -18
- package/src/key-bindings.test.ts +19 -9
- package/src/key-bindings.ts +77 -16
- package/src/keymap.test.ts +171 -26
- package/src/keymap.ts +83 -16
- package/src/review-panel.test.ts +3 -1
- package/src/review-panel.ts +9 -2
- package/src/serve.test.ts +13 -5
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +77 -16
- package/src/session-controller.ts +319 -26
- package/src/share.ts +41 -8
- package/src/test-support.ts +97 -23
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +1 -3
- package/src/version.ts +5 -0
- package/src/view-diff.ts +15 -4
- package/src/view-plan.test.ts +40 -6
- package/src/view-plan.ts +71 -28
- package/src/walk.test.ts +7 -2
- package/src/walk.ts +7 -2
- package/src/components/ComposeBar.stories.tsx +0 -18
- package/src/components/ComposeBar.tsx +0 -54
|
@@ -8,16 +8,15 @@
|
|
|
8
8
|
* which keeps quote anchors char-precise.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
|
11
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
12
12
|
import { useRenderer } from "@opentui/react";
|
|
13
|
-
import type
|
|
13
|
+
import { createTextAttributes, type ScrollBoxRenderable, type TextRenderable } from "@opentui/core";
|
|
14
14
|
import type { ReviewSession } from "@cueloop/schema";
|
|
15
15
|
import {
|
|
16
16
|
blockRuns,
|
|
17
17
|
displayText,
|
|
18
18
|
overlayMarks,
|
|
19
19
|
renderedOffsetFor,
|
|
20
|
-
revisionDelta,
|
|
21
20
|
workRangeForRendered,
|
|
22
21
|
type DisplayBlock,
|
|
23
22
|
type Mark,
|
|
@@ -28,8 +27,11 @@ import type { Theme } from "../theme";
|
|
|
28
27
|
import { useComponentTheme } from "./theme-context";
|
|
29
28
|
import { CodeBlock } from "./CodeBlock";
|
|
30
29
|
import { AnnotationCard, type AnnotationDraft } from "./AnnotationCard";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
30
|
+
import { MarkerPopover, type MarkerPopoverProps } from "./MarkerPopover";
|
|
31
|
+
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
32
|
+
|
|
33
|
+
/** A cut block reads as removed: struck through and grayed, never red. */
|
|
34
|
+
const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
|
|
33
35
|
|
|
34
36
|
export interface PlanSelection {
|
|
35
37
|
displayIndex: number;
|
|
@@ -39,7 +41,8 @@ export interface PlanSelection {
|
|
|
39
41
|
|
|
40
42
|
export interface PlanSheetHandle {
|
|
41
43
|
/** Work-text range of the current native (mouse) selection, if any. */
|
|
42
|
-
|
|
44
|
+
/** The current native selection; `preferredIndex` (the mouse-up block) re-anchors a fresh drag. */
|
|
45
|
+
readSelection(preferredIndex?: number): PlanSelection | null;
|
|
43
46
|
/** Anchor/extend the renderer's native selection from keyboard span offsets. */
|
|
44
47
|
driveSpanSelection(span: SpanState): void;
|
|
45
48
|
clearSelection(): void;
|
|
@@ -47,12 +50,17 @@ export interface PlanSheetHandle {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export interface PlanComposeState {
|
|
50
|
-
kind: "comment"
|
|
53
|
+
kind: "comment";
|
|
51
54
|
displayIndex: number;
|
|
52
55
|
quote: string;
|
|
53
56
|
draft: AnnotationDraft;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
/** The marker-actions popover, rendered inline at its block while span mode is live. */
|
|
60
|
+
export interface PlanPopoverState extends MarkerPopoverProps {
|
|
61
|
+
displayIndex: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
export interface PlanSheetProps {
|
|
57
65
|
session: ReviewSession;
|
|
58
66
|
display: DisplayBlock[];
|
|
@@ -61,12 +69,10 @@ export interface PlanSheetProps {
|
|
|
61
69
|
/** Extra selection-style paint on one block (keyboard span or compose anchor). */
|
|
62
70
|
activeSpan: { displayIndex: number; start: number; end: number } | null;
|
|
63
71
|
compose: PlanComposeState | null;
|
|
72
|
+
/** The marker-actions popover for the span's block; null when not in span mode. */
|
|
73
|
+
popover: PlanPopoverState | null;
|
|
64
74
|
editOrphanCount: number;
|
|
65
75
|
onLineActivate: (displayIndex: number) => void;
|
|
66
|
-
onEditRequest: () => void;
|
|
67
|
-
onShareRequest: () => void;
|
|
68
|
-
/** Owner-only: show the Edit and Share affordances. A share viewer never sees either. */
|
|
69
|
-
canEdit: boolean;
|
|
70
76
|
theme?: Theme;
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -76,7 +82,10 @@ interface BlockRef {
|
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
/** Screen position of a rendered-text offset inside a wrapped text. */
|
|
79
|
-
function positionOfRenderedOffset(
|
|
85
|
+
function positionOfRenderedOffset(
|
|
86
|
+
renderable: TextRenderable,
|
|
87
|
+
renderedOffset: number,
|
|
88
|
+
): { x: number; y: number } {
|
|
80
89
|
const info = renderable.lineInfo;
|
|
81
90
|
let lineIndex = 0;
|
|
82
91
|
for (let index = 0; index < info.lineStartCols.length; index++) {
|
|
@@ -91,17 +100,14 @@ function positionOfRenderedOffset(renderable: TextRenderable, renderedOffset: nu
|
|
|
91
100
|
|
|
92
101
|
export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function PlanSheet(
|
|
93
102
|
{
|
|
94
|
-
session,
|
|
95
103
|
display,
|
|
96
104
|
marks,
|
|
97
105
|
cursor,
|
|
98
106
|
activeSpan,
|
|
99
107
|
compose,
|
|
108
|
+
popover,
|
|
100
109
|
editOrphanCount,
|
|
101
110
|
onLineActivate,
|
|
102
|
-
onEditRequest,
|
|
103
|
-
onShareRequest,
|
|
104
|
-
canEdit,
|
|
105
111
|
theme,
|
|
106
112
|
}: PlanSheetProps,
|
|
107
113
|
handleRef,
|
|
@@ -112,14 +118,27 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
112
118
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
113
119
|
|
|
114
120
|
useImperativeHandle(handleRef, () => ({
|
|
115
|
-
readSelection: (): PlanSelection | null => {
|
|
121
|
+
readSelection: (preferredIndex?: number): PlanSelection | null => {
|
|
116
122
|
if (!renderer?.hasSelection) return null;
|
|
117
|
-
const
|
|
118
|
-
|
|
123
|
+
const readBlock = (displayIndex: number): PlanSelection | null => {
|
|
124
|
+
const blockRef = blockRefs.current.get(displayIndex);
|
|
125
|
+
if (!blockRef) return null;
|
|
119
126
|
const selection = blockRef.renderable.getSelection();
|
|
120
|
-
if (!selection || selection.end <= selection.start)
|
|
127
|
+
if (!selection || selection.end <= selection.start) return null;
|
|
121
128
|
const range = workRangeForRendered(blockRef.runs, selection.start, selection.end);
|
|
122
|
-
|
|
129
|
+
return range ? { displayIndex, ...range } : null;
|
|
130
|
+
};
|
|
131
|
+
// the block the mouse released on wins, so a fresh drag re-anchors the span
|
|
132
|
+
// (one marker at a time) instead of sticking to the topmost prior selection
|
|
133
|
+
if (preferredIndex !== undefined) {
|
|
134
|
+
const preferred = readBlock(preferredIndex);
|
|
135
|
+
if (preferred) return preferred;
|
|
136
|
+
}
|
|
137
|
+
for (const displayIndex of [...blockRefs.current.keys()].sort(
|
|
138
|
+
(left, right) => left - right,
|
|
139
|
+
)) {
|
|
140
|
+
const found = readBlock(displayIndex);
|
|
141
|
+
if (found) return found;
|
|
123
142
|
}
|
|
124
143
|
return null;
|
|
125
144
|
},
|
|
@@ -147,7 +166,30 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
147
166
|
},
|
|
148
167
|
}));
|
|
149
168
|
|
|
150
|
-
|
|
169
|
+
// the popover floats above its block, anchored to the selection start; flip
|
|
170
|
+
// below when the block sits too near the viewport top to fit above it
|
|
171
|
+
const [popoverFlipBelow, setPopoverFlipBelow] = useState(false);
|
|
172
|
+
const [popoverLeft, setPopoverLeft] = useState(2);
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (!popover) return;
|
|
175
|
+
const blockRef = blockRefs.current.get(popover.displayIndex);
|
|
176
|
+
const scrollbox = scrollRef.current;
|
|
177
|
+
if (!blockRef || !scrollbox) return;
|
|
178
|
+
// only the toolbar (3 rows) + gap must fit above; the dropdown flows down
|
|
179
|
+
setPopoverFlipBelow(blockRef.renderable.y - scrollbox.y < 4);
|
|
180
|
+
// left-anchor to the start of the selection: gutter width + its column
|
|
181
|
+
const startColumn =
|
|
182
|
+
activeSpan && activeSpan.displayIndex === popover.displayIndex
|
|
183
|
+
? (renderedOffsetFor(blockRef.runs, activeSpan.start) ?? 0)
|
|
184
|
+
: 0;
|
|
185
|
+
setPopoverLeft(2 + startColumn);
|
|
186
|
+
}, [popover, activeSpan, display, cursor]);
|
|
187
|
+
|
|
188
|
+
const registerBlock = (
|
|
189
|
+
displayIndex: number,
|
|
190
|
+
renderable: TextRenderable | null,
|
|
191
|
+
runs: StyleRun[],
|
|
192
|
+
): void => {
|
|
151
193
|
if (renderable) blockRefs.current.set(displayIndex, { renderable, runs });
|
|
152
194
|
else blockRefs.current.delete(displayIndex);
|
|
153
195
|
};
|
|
@@ -173,7 +215,8 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
173
215
|
isCursor={isCursor}
|
|
174
216
|
marginTop={gap}
|
|
175
217
|
isAnnotated={(marks.get(displayIndex) ?? []).length > 0}
|
|
176
|
-
changeTag={block
|
|
218
|
+
changeTag={showsChangeTag(block) ? tagLabel(block) : undefined}
|
|
219
|
+
cut={block.type === "del"}
|
|
177
220
|
theme={theme}
|
|
178
221
|
/>,
|
|
179
222
|
);
|
|
@@ -183,11 +226,23 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
183
226
|
blockMarks.push({ start: activeSpan.start, end: activeSpan.end, role: "kspan" });
|
|
184
227
|
}
|
|
185
228
|
const runs = overlayMarks(blockRuns(block, true), blockMarks);
|
|
186
|
-
// the change tag participates in the rendered text but carries no offsets
|
|
187
|
-
|
|
188
|
-
|
|
229
|
+
// the change tag participates in the rendered text but carries no offsets;
|
|
230
|
+
// a cut block shows no tag (its strikethrough already reads as removed)
|
|
231
|
+
const mappedRuns: StyleRun[] = showsChangeTag(block)
|
|
232
|
+
? [...runs, { text: ` [${tagLabel(block)}]`, role: "plain", start: null }]
|
|
233
|
+
: runs;
|
|
189
234
|
children.push(
|
|
190
|
-
<box
|
|
235
|
+
<box
|
|
236
|
+
key={displayIndex}
|
|
237
|
+
id={`plan-block-${displayIndex}`}
|
|
238
|
+
// raise the block holding the popover so its overlay paints over the
|
|
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
|
+
}}
|
|
245
|
+
>
|
|
191
246
|
<text selectable={false}>
|
|
192
247
|
<span fg={isCursor ? tokens.accent : tokens.textDim}>{isCursor ? "▎ " : " "}</span>
|
|
193
248
|
<span fg={tokens.textDim}>{marker(block)}</span>
|
|
@@ -198,7 +253,9 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
198
253
|
selectionBg={tokens.accent}
|
|
199
254
|
selectionFg={tokens.accentInk}
|
|
200
255
|
style={{ wrapMode: "word", flexGrow: 1, flexShrink: 1 }}
|
|
201
|
-
ref={(renderable: TextRenderable | null) =>
|
|
256
|
+
ref={(renderable: TextRenderable | null) =>
|
|
257
|
+
registerBlock(displayIndex, renderable, mappedRuns)
|
|
258
|
+
}
|
|
202
259
|
onMouseUp={() => onLineActivate(displayIndex)}
|
|
203
260
|
>
|
|
204
261
|
{runs.map((run, runIndex) => (
|
|
@@ -206,89 +263,79 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
206
263
|
{run.text}
|
|
207
264
|
</span>
|
|
208
265
|
))}
|
|
209
|
-
{block
|
|
266
|
+
{showsChangeTag(block) ? (
|
|
267
|
+
<span fg={tagColor(block, tokens)}> [{tagLabel(block)}]</span>
|
|
268
|
+
) : null}
|
|
210
269
|
</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}
|
|
211
286
|
</box>,
|
|
212
287
|
);
|
|
213
288
|
}
|
|
214
289
|
if (compose && compose.displayIndex === displayIndex) {
|
|
215
290
|
children.push(
|
|
216
|
-
<AnnotationCard
|
|
291
|
+
<AnnotationCard
|
|
292
|
+
key={`compose-${displayIndex}`}
|
|
293
|
+
kind={compose.kind}
|
|
294
|
+
quote={compose.quote}
|
|
295
|
+
draft={compose.draft}
|
|
296
|
+
theme={theme}
|
|
297
|
+
/>,
|
|
217
298
|
);
|
|
218
299
|
}
|
|
219
300
|
}
|
|
220
301
|
|
|
221
302
|
return (
|
|
222
303
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
</scrollbox>
|
|
240
|
-
</box>
|
|
241
|
-
);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
/** Sheet chrome: submitted-by + revision delta left, the Edit and Share word-buttons right. */
|
|
245
|
-
function SheetHeader({
|
|
246
|
-
session,
|
|
247
|
-
onEditRequest,
|
|
248
|
-
onShareRequest,
|
|
249
|
-
canEdit,
|
|
250
|
-
theme,
|
|
251
|
-
}: {
|
|
252
|
-
session: ReviewSession;
|
|
253
|
-
onEditRequest: () => void;
|
|
254
|
-
onShareRequest: () => void;
|
|
255
|
-
canEdit: boolean;
|
|
256
|
-
theme?: Theme;
|
|
257
|
-
}): React.ReactNode {
|
|
258
|
-
const tokens = useComponentTheme(theme);
|
|
259
|
-
const revisionCount = session.revisions.length;
|
|
260
|
-
const previous = revisionCount > 1 ? session.revisions[revisionCount - 2] : undefined;
|
|
261
|
-
const delta = previous ? revisionDelta(previous.content, session.artifact.content) : null;
|
|
262
|
-
return (
|
|
263
|
-
<box style={{ height: 1, flexDirection: "row", paddingLeft: 2, paddingRight: 1 }}>
|
|
264
|
-
<text fg={tokens.textDim}>
|
|
265
|
-
submitted by <span fg={tokens.textMuted}>{session.artifact.meta.agent ?? "unknown"}</span> · revision {revisionCount}
|
|
266
|
-
{delta ? (
|
|
267
|
-
<span fg={tokens.green}>
|
|
268
|
-
{" "}· v{revisionCount - 1}→v{revisionCount} +{delta.added} -{delta.removed}
|
|
269
|
-
</span>
|
|
304
|
+
<box
|
|
305
|
+
style={{
|
|
306
|
+
flexGrow: 1,
|
|
307
|
+
flexDirection: "column",
|
|
308
|
+
border: true,
|
|
309
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
310
|
+
borderColor: tokens.text,
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
{editOrphanCount > 0 ? (
|
|
314
|
+
<box style={{ height: 1, backgroundColor: tokens.markCommentBackground, paddingLeft: 1 }}>
|
|
315
|
+
<text fg={tokens.red}>
|
|
316
|
+
{editOrphanCount} annotation{editOrphanCount === 1 ? "" : "s"} no longer match - the
|
|
317
|
+
passage was removed.
|
|
318
|
+
</text>
|
|
319
|
+
</box>
|
|
270
320
|
) : null}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
{" Share "}
|
|
280
|
-
</Button>
|
|
281
|
-
</Toolbar>
|
|
282
|
-
) : null}
|
|
321
|
+
<scrollbox
|
|
322
|
+
ref={scrollRef}
|
|
323
|
+
style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 0 }}
|
|
324
|
+
focused={false}
|
|
325
|
+
>
|
|
326
|
+
{children}
|
|
327
|
+
</scrollbox>
|
|
328
|
+
</box>
|
|
283
329
|
</box>
|
|
284
330
|
);
|
|
285
|
-
}
|
|
331
|
+
});
|
|
286
332
|
|
|
287
333
|
/** Vertical rhythm: gaps live ABOVE blocks so boundaries never collapse. */
|
|
288
334
|
function topGap(previous: DisplayBlock | undefined, current: DisplayBlock): number {
|
|
289
335
|
if (!previous) return 0;
|
|
290
336
|
const tightPair =
|
|
291
|
-
(current.kind === "li" && previous.kind === "li") ||
|
|
337
|
+
(current.kind === "li" && previous.kind === "li") ||
|
|
338
|
+
(current.kind === "oli" && previous.kind === "oli");
|
|
292
339
|
return tightPair ? 0 : 1;
|
|
293
340
|
}
|
|
294
341
|
|
|
@@ -299,17 +346,32 @@ function marker(block: DisplayBlock): string {
|
|
|
299
346
|
return "";
|
|
300
347
|
}
|
|
301
348
|
|
|
302
|
-
|
|
303
|
-
|
|
349
|
+
/** An added or edited block earns a tag; a cut block relies on its strikethrough. */
|
|
350
|
+
function showsChangeTag(block: DisplayBlock): boolean {
|
|
351
|
+
return block.type === "add" || block.type === "mod";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function tagLabel(block: DisplayBlock): "new" | "edited" {
|
|
355
|
+
return block.type === "add" ? "new" : "edited";
|
|
304
356
|
}
|
|
305
357
|
|
|
306
358
|
function tagColor(block: DisplayBlock, tokens: Theme): string {
|
|
307
|
-
return block.type === "
|
|
359
|
+
return block.type === "add" ? tokens.green : tokens.accent;
|
|
308
360
|
}
|
|
309
361
|
|
|
310
|
-
function runStyle(
|
|
311
|
-
|
|
312
|
-
|
|
362
|
+
function runStyle(
|
|
363
|
+
run: StyleRun,
|
|
364
|
+
block: DisplayBlock,
|
|
365
|
+
tokens: Theme,
|
|
366
|
+
): { fg?: string; bg?: string; attributes?: number } {
|
|
367
|
+
// a cut block reads as removed: every run struck through and grayed, never red
|
|
368
|
+
if (block.type === "del") return { fg: tokens.textDim, attributes: CUT_ATTRIBUTES };
|
|
369
|
+
const headingFg =
|
|
370
|
+
block.kind === "h1"
|
|
371
|
+
? tokens.text
|
|
372
|
+
: block.kind === "h2" || block.kind === "h3"
|
|
373
|
+
? tokens.accent
|
|
374
|
+
: undefined;
|
|
313
375
|
switch (run.role) {
|
|
314
376
|
case "ins":
|
|
315
377
|
return { fg: tokens.insertedForeground };
|
|
@@ -317,13 +379,11 @@ function runStyle(run: StyleRun, block: DisplayBlock, tokens: Theme): { fg?: str
|
|
|
317
379
|
return { fg: tokens.deletedForeground };
|
|
318
380
|
case "mark-comment":
|
|
319
381
|
return { fg: tokens.text, bg: tokens.markCommentBackground };
|
|
320
|
-
case "mark-suggestion":
|
|
321
|
-
return { fg: tokens.text, bg: tokens.markSuggestionBackground };
|
|
322
382
|
case "mark-focus":
|
|
323
383
|
return { fg: tokens.accentInk, bg: tokens.accent };
|
|
324
384
|
case "kspan":
|
|
325
385
|
return { fg: tokens.accentInk, bg: tokens.accent };
|
|
326
386
|
default:
|
|
327
|
-
return { fg:
|
|
387
|
+
return { fg: headingFg ?? tokens.text };
|
|
328
388
|
}
|
|
329
389
|
}
|
|
@@ -25,7 +25,15 @@ export interface PromptDialogProps {
|
|
|
25
25
|
theme?: Theme;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function PromptDialog({
|
|
28
|
+
export function PromptDialog({
|
|
29
|
+
isOpen,
|
|
30
|
+
title,
|
|
31
|
+
label,
|
|
32
|
+
value,
|
|
33
|
+
placeholder,
|
|
34
|
+
onInput,
|
|
35
|
+
theme,
|
|
36
|
+
}: PromptDialogProps): React.ReactNode {
|
|
29
37
|
const tokens = useComponentTheme(theme);
|
|
30
38
|
const { width: terminalWidth } = useTerminalDimensions();
|
|
31
39
|
const inputRef = useRef<TextareaRenderable | null>(null);
|
|
@@ -37,7 +45,15 @@ export function PromptDialog({ isOpen, title, label, value, placeholder, onInput
|
|
|
37
45
|
if (!isOpen) return null;
|
|
38
46
|
return (
|
|
39
47
|
<Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
|
|
40
|
-
<box
|
|
48
|
+
<box
|
|
49
|
+
style={{
|
|
50
|
+
flexDirection: "column",
|
|
51
|
+
flexGrow: 1,
|
|
52
|
+
paddingLeft: 1,
|
|
53
|
+
paddingRight: 1,
|
|
54
|
+
paddingTop: 1,
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
41
57
|
<text fg={tokens.textDim}>{label}</text>
|
|
42
58
|
<textarea
|
|
43
59
|
ref={inputRef}
|
|
@@ -55,7 +71,7 @@ export function PromptDialog({ isOpen, title, label, value, placeholder, onInput
|
|
|
55
71
|
}}
|
|
56
72
|
/>
|
|
57
73
|
<box style={{ flexGrow: 1 }} />
|
|
58
|
-
<text fg={tokens.textDim}
|
|
74
|
+
<text fg={tokens.textDim}>enter save · esc cancel</text>
|
|
59
75
|
</box>
|
|
60
76
|
</Dialog>
|
|
61
77
|
);
|
|
@@ -12,6 +12,7 @@ const rail = {
|
|
|
12
12
|
authorNames: {},
|
|
13
13
|
selectedId: FIXTURE_ANNOTATIONS[0]!.id,
|
|
14
14
|
resolvedIds: new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id)),
|
|
15
|
+
curationItems: [],
|
|
15
16
|
railTab: "review" as const,
|
|
16
17
|
pendingCount: 2,
|
|
17
18
|
cardEdit: null,
|
|
@@ -19,6 +20,8 @@ const rail = {
|
|
|
19
20
|
onTabChange: () => {},
|
|
20
21
|
onSelectCard: () => {},
|
|
21
22
|
onActivateCard: () => {},
|
|
23
|
+
onSelectCuration: () => {},
|
|
24
|
+
onUndoCuration: () => {},
|
|
22
25
|
onSubmitRequest: () => {},
|
|
23
26
|
};
|
|
24
27
|
|
|
@@ -27,7 +30,14 @@ const panelCallbacks = { onDividerGrab: () => {}, onToggle: () => {} };
|
|
|
27
30
|
/** The plan area to the left of the divider, so the layout reads in context. */
|
|
28
31
|
function PanelFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
29
32
|
return (
|
|
30
|
-
<box
|
|
33
|
+
<box
|
|
34
|
+
style={{
|
|
35
|
+
flexDirection: "row",
|
|
36
|
+
width: "100%",
|
|
37
|
+
height: "100%",
|
|
38
|
+
backgroundColor: DARK.background,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
31
41
|
<box style={{ flexGrow: 1, paddingLeft: 1 }}>
|
|
32
42
|
<text fg={DARK.textMuted}>The plan takes the width the panel hands back.</text>
|
|
33
43
|
</box>
|
|
@@ -39,48 +49,36 @@ function PanelFrame({ children }: { children: React.ReactNode }): React.ReactNod
|
|
|
39
49
|
export const ExpandedNarrow: Story = {
|
|
40
50
|
render: () => (
|
|
41
51
|
<PanelFrame>
|
|
42
|
-
<ReviewPanel mode="expanded" width={REVIEW_MIN_WIDTH}
|
|
52
|
+
<ReviewPanel mode="expanded" width={REVIEW_MIN_WIDTH} rail={rail} {...panelCallbacks} />
|
|
43
53
|
</PanelFrame>
|
|
44
54
|
),
|
|
45
|
-
expectedColors: [DARK.border],
|
|
46
55
|
size: { width: 80, height: 24 },
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
export const ExpandedWide: Story = {
|
|
50
59
|
render: () => (
|
|
51
60
|
<PanelFrame>
|
|
52
|
-
<ReviewPanel mode="expanded" width={REVIEW_MAX_WIDTH}
|
|
53
|
-
</PanelFrame>
|
|
54
|
-
),
|
|
55
|
-
expectedColors: [DARK.green],
|
|
56
|
-
size: { width: 90, height: 24 },
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/** Dragging accents the divider glyph column. */
|
|
60
|
-
export const DraggingDivider: Story = {
|
|
61
|
-
render: () => (
|
|
62
|
-
<PanelFrame>
|
|
63
|
-
<ReviewPanel mode="expanded" width={34} height={24} dragging={true} rail={rail} {...panelCallbacks} />
|
|
61
|
+
<ReviewPanel mode="expanded" width={REVIEW_MAX_WIDTH} rail={rail} {...panelCallbacks} />
|
|
64
62
|
</PanelFrame>
|
|
65
63
|
),
|
|
66
64
|
expectedColors: [DARK.accent],
|
|
67
|
-
size: { width:
|
|
65
|
+
size: { width: 90, height: 24 },
|
|
68
66
|
};
|
|
69
67
|
|
|
70
68
|
export const Compact: Story = {
|
|
71
69
|
render: () => (
|
|
72
70
|
<PanelFrame>
|
|
73
|
-
<ReviewPanel mode="compact" width={34}
|
|
71
|
+
<ReviewPanel mode="compact" width={34} rail={rail} {...panelCallbacks} />
|
|
74
72
|
</PanelFrame>
|
|
75
73
|
),
|
|
76
|
-
expectedColors: [DARK.accent
|
|
74
|
+
expectedColors: [DARK.accent],
|
|
77
75
|
size: { width: 60, height: 24 },
|
|
78
76
|
};
|
|
79
77
|
|
|
80
78
|
export const Hidden: Story = {
|
|
81
79
|
render: () => (
|
|
82
80
|
<PanelFrame>
|
|
83
|
-
<ReviewPanel mode="hidden" width={34}
|
|
81
|
+
<ReviewPanel mode="hidden" width={34} rail={rail} {...panelCallbacks} />
|
|
84
82
|
</PanelFrame>
|
|
85
83
|
),
|
|
86
84
|
size: { width: 60, height: 24 },
|
|
@@ -14,16 +14,12 @@ import type { Annotation } from "@cueloop/schema";
|
|
|
14
14
|
import type { Theme } from "../theme";
|
|
15
15
|
import { useComponentTheme } from "./theme-context";
|
|
16
16
|
import { ReviewRail, type ReviewRailHandle, type ReviewRailProps } from "./ReviewRail";
|
|
17
|
-
import { REVIEW_COMPACT_WIDTH,
|
|
17
|
+
import { REVIEW_COMPACT_WIDTH, type ReviewPanelMode } from "../review-panel";
|
|
18
18
|
|
|
19
19
|
export interface ReviewPanelProps {
|
|
20
20
|
mode: ReviewPanelMode;
|
|
21
21
|
/** Expanded-rail width in columns (already clamped by the app). */
|
|
22
22
|
width: number;
|
|
23
|
-
/** Rows for the divider glyph column - the height of the plan/rail row. */
|
|
24
|
-
height: number;
|
|
25
|
-
/** Accent the divider while a drag is live. */
|
|
26
|
-
dragging: boolean;
|
|
27
23
|
/** Arm a divider drag; the app tracks the drag itself on the container. */
|
|
28
24
|
onDividerGrab: () => void;
|
|
29
25
|
/** Chevron click: expanded <-> compact (never hidden). */
|
|
@@ -34,28 +30,15 @@ export interface ReviewPanelProps {
|
|
|
34
30
|
theme?: Theme;
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
/**
|
|
38
|
-
function ReviewDivider({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
onGrab
|
|
42
|
-
theme,
|
|
43
|
-
}: {
|
|
44
|
-
dragging: boolean;
|
|
45
|
-
rows: number;
|
|
46
|
-
onGrab: () => void;
|
|
47
|
-
theme?: Theme;
|
|
48
|
-
}): React.ReactNode {
|
|
49
|
-
const tokens = useComponentTheme(theme);
|
|
50
|
-
return (
|
|
51
|
-
<box style={{ width: 1 }} onMouseDown={onGrab}>
|
|
52
|
-
<text fg={dragging ? tokens.accent : tokens.border}>{reviewRowsToDivider(rows)}</text>
|
|
53
|
-
</box>
|
|
54
|
-
);
|
|
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} />;
|
|
55
38
|
}
|
|
56
39
|
|
|
57
40
|
/** The compact strip: the card count, one kind-colored dot per card, and the
|
|
58
|
-
*
|
|
41
|
+
* `<` expand chevron, bottom-padded to sit at the same row as the expanded `>`. */
|
|
59
42
|
function CompactRail({
|
|
60
43
|
annotations,
|
|
61
44
|
onExpand,
|
|
@@ -66,11 +49,15 @@ function CompactRail({
|
|
|
66
49
|
theme?: Theme;
|
|
67
50
|
}): React.ReactNode {
|
|
68
51
|
const tokens = useComponentTheme(theme);
|
|
69
|
-
const dotColor = (annotation: Annotation): string =>
|
|
70
|
-
annotation.kind === "suggestion" ? tokens.green : tokens.accent;
|
|
71
52
|
return (
|
|
72
53
|
<box
|
|
73
|
-
style={{
|
|
54
|
+
style={{
|
|
55
|
+
width: REVIEW_COMPACT_WIDTH,
|
|
56
|
+
backgroundColor: tokens.panel,
|
|
57
|
+
flexDirection: "column",
|
|
58
|
+
paddingTop: 1,
|
|
59
|
+
paddingBottom: 1,
|
|
60
|
+
}}
|
|
74
61
|
>
|
|
75
62
|
<box style={{ alignItems: "center" }}>
|
|
76
63
|
<text fg={tokens.accent}>{String(annotations.length)}</text>
|
|
@@ -78,14 +65,14 @@ function CompactRail({
|
|
|
78
65
|
<box style={{ height: 1 }} />
|
|
79
66
|
<box style={{ flexDirection: "column", alignItems: "center" }}>
|
|
80
67
|
{annotations.map((annotation) => (
|
|
81
|
-
<text key={annotation.id} fg={
|
|
68
|
+
<text key={annotation.id} fg={tokens.accent}>
|
|
82
69
|
●
|
|
83
70
|
</text>
|
|
84
71
|
))}
|
|
85
72
|
</box>
|
|
86
73
|
<box style={{ flexGrow: 1 }} />
|
|
87
74
|
<box style={{ paddingLeft: 1 }} onMouseUp={onExpand}>
|
|
88
|
-
<text fg={tokens.textDim}
|
|
75
|
+
<text fg={tokens.textDim}>{"<"}</text>
|
|
89
76
|
</box>
|
|
90
77
|
</box>
|
|
91
78
|
);
|
|
@@ -94,8 +81,6 @@ function CompactRail({
|
|
|
94
81
|
export function ReviewPanel({
|
|
95
82
|
mode,
|
|
96
83
|
width,
|
|
97
|
-
height,
|
|
98
|
-
dragging,
|
|
99
84
|
onDividerGrab,
|
|
100
85
|
onToggle,
|
|
101
86
|
rail,
|
|
@@ -105,7 +90,7 @@ export function ReviewPanel({
|
|
|
105
90
|
if (mode === "hidden") return null;
|
|
106
91
|
return (
|
|
107
92
|
<>
|
|
108
|
-
<ReviewDivider
|
|
93
|
+
<ReviewDivider onGrab={onDividerGrab} />
|
|
109
94
|
{mode === "expanded" ? (
|
|
110
95
|
<ReviewRail ref={railRef} {...rail} width={width} onCollapse={onToggle} theme={theme} />
|
|
111
96
|
) : (
|