@cueloop/client 0.1.0-alpha.78 → 0.1.0-alpha.79
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 +14 -13
- package/src/App.diff.test.tsx +9 -4
- package/src/App.focus.test.ts +1 -59
- package/src/App.identity.test.tsx +3 -3
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +75 -26
- package/src/App.readonly.test.tsx +6 -5
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +6 -4
- package/src/App.test.tsx +31 -28
- package/src/App.toast.test.tsx +11 -4
- package/src/App.tsx +259 -274
- package/src/App.walk.test.tsx +9 -8
- package/src/App.workbench.test.tsx +86 -0
- package/src/annotation-palette.ts +1 -0
- package/src/app-screens.tsx +12 -6
- package/src/app-view-model.ts +0 -1
- package/src/components/ConfirmDialog.stories.tsx +3 -3
- package/src/components/DiffContentView.tsx +14 -7
- package/src/components/DiscussionMarkerRail.tsx +9 -4
- package/src/components/GridTabContent.tsx +8 -2
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/SettingsDialog.tsx +1 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ThreadFooter.tsx +4 -1
- package/src/components/ThreadTree.tsx +1 -1
- package/src/components/ThreadView.slash.test.tsx +0 -35
- package/src/components/ThreadView.tsx +142 -68
- package/src/components/Toast.stories.tsx +7 -0
- package/src/components/Toast.tsx +15 -3
- package/src/components/__snapshots__/stories.test.tsx.snap +258 -134
- package/src/components/primitives/Button.tsx +6 -2
- package/src/components/primitives/Card.tsx +2 -2
- package/src/components/primitives/Dialog.tsx +2 -2
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tree.tsx +5 -1
- package/src/components/primitives/frame.ts +5 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories.test.tsx +4 -3
- package/src/config.test.ts +18 -0
- package/src/config.ts +8 -1
- package/src/diff-fold.ts +1 -1
- package/src/intent-dispatch.test.ts +12 -3
- package/src/intent-dispatch.ts +10 -25
- package/src/key-bindings.ts +0 -6
- package/src/keymap.ts +0 -1
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/prototype-browser.test.ts +9 -1
- package/src/prototype-browser.ts +22 -0
- package/src/run.ts +8 -6
- package/src/share.ts +15 -1
- package/src/test-support.ts +14 -0
- package/src/thread-chords.test.ts +55 -178
- package/src/thread-chords.ts +78 -169
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.live-diff.test.ts +3 -0
- package/src/thread-controller.share.test.ts +60 -5
- package/src/thread-controller.switch-cache.test.ts +30 -0
- package/src/thread-controller.tree.test.ts +6 -1
- package/src/thread-controller.ts +229 -54
- package/src/use-annotation-surface.tsx +45 -36
- package/src/use-settings-dialog.tsx +14 -3
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/view-plan.test.ts +56 -6
- package/src/view-plan.ts +154 -5
- package/src/App.share-choice.test.tsx +0 -105
- package/src/components/ManageAccessDialog.stories.tsx +0 -36
- package/src/components/ManageAccessDialog.test.tsx +0 -94
- package/src/components/ManageAccessDialog.tsx +0 -122
- package/src/components/ShareChoiceDialog.stories.tsx +0 -34
- package/src/components/ShareChoiceDialog.test.tsx +0 -103
- package/src/components/ShareChoiceDialog.tsx +0 -80
- package/src/components/ThreadsSidebar.stories.tsx +0 -36
- package/src/components/ThreadsSidebar.tsx +0 -71
- package/src/components/agent-launcher.stories.tsx +0 -26
- package/src/components/agent-launcher.test.tsx +0 -50
- package/src/components/agent-launcher.tsx +0 -149
|
@@ -16,16 +16,28 @@ import React, { useEffect, useRef } from "react";
|
|
|
16
16
|
import { useTerminalDimensions } from "@opentui/react";
|
|
17
17
|
import type { KeyEvent, ScrollBoxRenderable } from "@opentui/core";
|
|
18
18
|
import type { Annotation, Thread } from "@cueloop/schema";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
displayText,
|
|
21
|
+
renderedText,
|
|
22
|
+
renderedStyleRuns,
|
|
23
|
+
styledRunsFor,
|
|
24
|
+
type DisplayBlock,
|
|
25
|
+
type Mark,
|
|
26
|
+
type RenderedRun,
|
|
27
|
+
type RunRole,
|
|
28
|
+
type StyledRun,
|
|
29
|
+
} from "../view-plan";
|
|
20
30
|
import type { TextSpan } from "../thread-selection";
|
|
21
31
|
import type { QuickAction } from "../config";
|
|
22
32
|
import type { CheatsheetSection } from "../key-bindings";
|
|
23
33
|
import type { Theme } from "../theme";
|
|
24
|
-
import { BOLD, CUT, UNDERLINE } from "../annotation-palette";
|
|
25
|
-
import {
|
|
34
|
+
import { BOLD, CUT, ITALIC, UNDERLINE } from "../annotation-palette";
|
|
35
|
+
import { wrapLines, type MarkRange, type VisualLine } from "../mark-runs";
|
|
36
|
+
import { MarkdownGridBlock } from "./MarkdownGridBlock";
|
|
26
37
|
import { useFrameMeasure } from "../use-frame-measure";
|
|
27
38
|
import { useTerminalVirtualizer } from "../use-terminal-virtualizer";
|
|
28
39
|
import { useAnnotationSurface, type LineSource } from "../use-annotation-surface";
|
|
40
|
+
import { NavModeHint } from "./NavModeHint";
|
|
29
41
|
import { DiscussionMarkerRail } from "./DiscussionMarkerRail";
|
|
30
42
|
import { useComponentTheme } from "./theme-context";
|
|
31
43
|
|
|
@@ -35,15 +47,14 @@ const OVERSCAN_BLOCKS = 8;
|
|
|
35
47
|
export { lighten } from "../annotation-palette";
|
|
36
48
|
|
|
37
49
|
/**
|
|
38
|
-
* How a block's rows are painted: heading weight, muted kinds, the list or
|
|
39
|
-
* quote marker
|
|
40
|
-
*
|
|
50
|
+
* How a block's rows are painted: heading weight, muted kinds, and the list or
|
|
51
|
+
* quote marker. A cut block reads dim and struck through; other edits are
|
|
52
|
+
* tracked into the working copy but not tagged in the read-only view.
|
|
41
53
|
*/
|
|
42
54
|
interface BlockStyle {
|
|
43
55
|
baseFg: string;
|
|
44
56
|
baseAttributes: number;
|
|
45
57
|
marker: string;
|
|
46
|
-
changeTag: { text: string; fg: string } | null;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
function blockStyle(block: DisplayBlock, tokens: Theme): BlockStyle {
|
|
@@ -58,27 +69,42 @@ function blockStyle(block: DisplayBlock, tokens: Theme): BlockStyle {
|
|
|
58
69
|
: block.kind === "quote"
|
|
59
70
|
? "▏ "
|
|
60
71
|
: "";
|
|
61
|
-
const changeTag =
|
|
62
|
-
block.type === "add"
|
|
63
|
-
? { text: " [new]", fg: tokens.green }
|
|
64
|
-
: block.type === "mod"
|
|
65
|
-
? { text: " [edited]", fg: tokens.accent }
|
|
66
|
-
: null;
|
|
67
72
|
|
|
68
73
|
return {
|
|
69
74
|
baseFg: isCut ? tokens.textDim : muted ? tokens.textMuted : tokens.text,
|
|
70
75
|
baseAttributes: (isHeading ? BOLD : 0) | (isCut ? CUT : 0),
|
|
71
76
|
marker,
|
|
72
|
-
changeTag,
|
|
73
77
|
};
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/** The read-only color of an inline markdown role: links accent, code and removals dim, insertions green. */
|
|
81
|
+
function roleForeground(role: RunRole, baseFg: string, tokens: Theme): string {
|
|
82
|
+
if (role === "link") return tokens.blue;
|
|
83
|
+
if (role === "ins") return tokens.green;
|
|
84
|
+
if (role === "code" || role === "del") return tokens.textDim;
|
|
85
|
+
|
|
86
|
+
return baseFg;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The text attributes an inline role adds over a block's base attributes. */
|
|
90
|
+
function roleAttributes(role: RunRole, baseAttributes: number): number {
|
|
91
|
+
if (role === "strong") return baseAttributes | BOLD;
|
|
92
|
+
if (role === "em") return baseAttributes | ITALIC;
|
|
93
|
+
if (role === "strike" || role === "del") return baseAttributes | CUT;
|
|
94
|
+
|
|
95
|
+
return baseAttributes;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isGridKind(kind: DisplayBlock["kind"]): boolean {
|
|
99
|
+
return kind === "table" || kind === "frontmatter";
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
/* ------------------------------------------------------------------ view */
|
|
77
103
|
|
|
78
104
|
/** The grammar as the keybinds dialog lists it; the view owns these keys, so they are not rebindable. */
|
|
79
105
|
export const THREAD_VIEW_CHEATSHEET: CheatsheetSection[] = [
|
|
80
106
|
{
|
|
81
|
-
title: "Thread",
|
|
107
|
+
title: "Thread (type)",
|
|
82
108
|
entries: [
|
|
83
109
|
{ keys: "click", label: "place the caret" },
|
|
84
110
|
{ keys: "drag", label: "mark text, across blocks" },
|
|
@@ -87,11 +113,9 @@ export const THREAD_VIEW_CHEATSHEET: CheatsheetSection[] = [
|
|
|
87
113
|
{ keys: "⇧← / ⇧→", label: "hold a mark" },
|
|
88
114
|
{ keys: "↑ / ↓", label: "move by block" },
|
|
89
115
|
{ keys: "type", label: "comment on the mark" },
|
|
90
|
-
{ keys: "⌘⌥m", label: "comment" },
|
|
91
116
|
{ keys: "enter", label: "reply to the comment" },
|
|
92
|
-
{ keys: "
|
|
93
|
-
{ keys: "
|
|
94
|
-
{ keys: "esc", label: "drop the mark" },
|
|
117
|
+
{ keys: "esc", label: "nav mode" },
|
|
118
|
+
{ keys: "tab", label: "cycle panes" },
|
|
95
119
|
{ keys: "⌃q", label: "quit" },
|
|
96
120
|
],
|
|
97
121
|
},
|
|
@@ -134,8 +158,7 @@ export interface ThreadViewProps {
|
|
|
134
158
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
135
159
|
/** The author's display name for a comment's hover tooltip. */
|
|
136
160
|
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
137
|
-
|
|
138
|
-
onLeaderCommand?: (key: KeyEvent) => void;
|
|
161
|
+
onNavCommand?: (key: KeyEvent) => boolean;
|
|
139
162
|
onExit: () => void;
|
|
140
163
|
theme?: Theme;
|
|
141
164
|
}
|
|
@@ -158,15 +181,15 @@ export function ThreadView({
|
|
|
158
181
|
onReply,
|
|
159
182
|
onUpdateAnnotation,
|
|
160
183
|
resolveAuthorLabel,
|
|
161
|
-
|
|
162
|
-
onLeaderCommand,
|
|
184
|
+
onNavCommand,
|
|
163
185
|
onExit,
|
|
164
186
|
theme,
|
|
165
187
|
}: ThreadViewProps): React.ReactNode {
|
|
166
188
|
const tokens = useComponentTheme(theme);
|
|
167
189
|
const source: LineSource = {
|
|
168
190
|
count: display.length,
|
|
169
|
-
|
|
191
|
+
// the surface hit-tests and paints in rendered text (inline markers concealed); a grid anchors on its raw source
|
|
192
|
+
textAt: (blockIndex) => renderedText(display[blockIndex]!),
|
|
170
193
|
annotatable: () => true,
|
|
171
194
|
};
|
|
172
195
|
const surface = useAnnotationSurface({
|
|
@@ -187,8 +210,7 @@ export function ThreadView({
|
|
|
187
210
|
onReply,
|
|
188
211
|
onUpdateAnnotation,
|
|
189
212
|
resolveAuthorLabel,
|
|
190
|
-
|
|
191
|
-
onLeaderCommand,
|
|
213
|
+
onNavCommand,
|
|
192
214
|
onExit,
|
|
193
215
|
});
|
|
194
216
|
const { palette, discussions } = surface;
|
|
@@ -227,21 +249,34 @@ export function ThreadView({
|
|
|
227
249
|
|
|
228
250
|
interface LineContext {
|
|
229
251
|
blockIndex: number;
|
|
230
|
-
|
|
252
|
+
roleRuns: RenderedRun[];
|
|
231
253
|
line: VisualLine;
|
|
232
254
|
lineIndex: number;
|
|
233
255
|
ranges: MarkRange[];
|
|
234
256
|
marker: string;
|
|
235
257
|
baseFg: string;
|
|
236
258
|
baseAttributes: number;
|
|
237
|
-
/** A tag after the row's text, such as the tracked-change label. */
|
|
238
|
-
trailing: { text: string; fg: string } | null;
|
|
239
259
|
}
|
|
240
260
|
|
|
261
|
+
const paintedSpan = (
|
|
262
|
+
run: StyledRun,
|
|
263
|
+
runIndex: number,
|
|
264
|
+
baseFg: string,
|
|
265
|
+
baseAttributes: number,
|
|
266
|
+
) => (
|
|
267
|
+
<span
|
|
268
|
+
key={runIndex}
|
|
269
|
+
fg={roleForeground(run.role, baseFg, tokens)}
|
|
270
|
+
bg={run.caretOnly ? palette.caretCell : run.marked ? palette.markBackdrop : undefined}
|
|
271
|
+
attributes={roleAttributes(run.role, baseAttributes) | (run.marked ? UNDERLINE : 0)}
|
|
272
|
+
>
|
|
273
|
+
{run.text}
|
|
274
|
+
</span>
|
|
275
|
+
);
|
|
276
|
+
|
|
241
277
|
const lineRowFor = (context: LineContext): React.ReactNode => {
|
|
242
|
-
const { blockIndex,
|
|
278
|
+
const { blockIndex, roleRuns, line, lineIndex, ranges, marker, baseFg, baseAttributes } =
|
|
243
279
|
context;
|
|
244
|
-
const lineRanges = lineMarkRanges(ranges, line);
|
|
245
280
|
|
|
246
281
|
return (
|
|
247
282
|
<box key={`line-${lineIndex}`} style={{ flexDirection: "row" }}>
|
|
@@ -255,17 +290,25 @@ export function ThreadView({
|
|
|
255
290
|
ref={surface.registerLine(blockIndex, lineIndex, line)}
|
|
256
291
|
onMouseDown={surface.onLineMouseDown}
|
|
257
292
|
>
|
|
258
|
-
{
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
293
|
+
{styledRunsFor(roleRuns, line, ranges).map((run, runIndex) =>
|
|
294
|
+
paintedSpan(run, runIndex, baseFg, baseAttributes),
|
|
295
|
+
)}
|
|
296
|
+
</text>
|
|
297
|
+
</box>
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// an h1 or h2 sits over a dim rule; h3 gets weight only. The row is
|
|
302
|
+
// always present for those kinds (only its width tracks the measured viewport) so a late width
|
|
303
|
+
// measurement never adds a row and shifts the blocks below it out from under a pending click
|
|
304
|
+
const headingRule = (block: DisplayBlock): React.ReactNode => {
|
|
305
|
+
if (block.kind !== "h1" && block.kind !== "h2") return null;
|
|
306
|
+
const width = Math.max(1, (viewWidth > 6 ? viewWidth : 46) - 6);
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<box key="heading-rule" style={{ flexDirection: "row" }}>
|
|
310
|
+
<text selectable={false} fg={tokens.textDim}>
|
|
311
|
+
{" " + "─".repeat(width)}
|
|
269
312
|
</text>
|
|
270
313
|
</box>
|
|
271
314
|
);
|
|
@@ -273,10 +316,54 @@ export function ThreadView({
|
|
|
273
316
|
|
|
274
317
|
const blockNodeFor = (blockIndex: number): React.ReactNode => {
|
|
275
318
|
const block = display[blockIndex]!;
|
|
276
|
-
const
|
|
319
|
+
const { baseFg, baseAttributes, marker } = blockStyle(block, tokens);
|
|
320
|
+
// list items of one list stay tight; every other block sits a blank row below its neighbour
|
|
321
|
+
const previous = display[blockIndex - 1];
|
|
322
|
+
const tight =
|
|
323
|
+
previous !== undefined &&
|
|
324
|
+
block.kind === previous.kind &&
|
|
325
|
+
(block.kind === "li" || block.kind === "oli");
|
|
326
|
+
// the leading gap rides inside the measured box (not marginTop, which sits outside it) so the
|
|
327
|
+
// virtualizer's spacer math counts it
|
|
328
|
+
const leadingGap =
|
|
329
|
+
blockIndex === 0 || tight ? null : <box key="lead-gap" style={{ height: 1 }} />;
|
|
330
|
+
const measuredBox = (children: React.ReactNode): React.ReactNode => (
|
|
331
|
+
<box
|
|
332
|
+
key={`discussion-block-${blockIndex}`}
|
|
333
|
+
id={`discussion-block-${blockIndex}`}
|
|
334
|
+
ref={virtual.measureRef(blockIndex)}
|
|
335
|
+
style={{ flexDirection: "column" }}
|
|
336
|
+
>
|
|
337
|
+
{leadingGap}
|
|
338
|
+
{children}
|
|
339
|
+
</box>
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (isGridKind(block.kind)) {
|
|
343
|
+
// a grid annotates as one unit: every row hit-tests to the same block span, a mark tints the grid,
|
|
344
|
+
// and the block's discussion cards slot under it, so annotations resolving into a grid stay visible
|
|
345
|
+
const gridLine: VisualLine = { start: 0, end: renderedText(block).length };
|
|
346
|
+
const marked = surface.rangesFor(blockIndex).some((range) => !range.caretOnly);
|
|
347
|
+
|
|
348
|
+
return measuredBox(
|
|
349
|
+
<>
|
|
350
|
+
<MarkdownGridBlock
|
|
351
|
+
block={block}
|
|
352
|
+
theme={tokens}
|
|
353
|
+
contentWidth={viewWidth > 0 ? viewWidth - 6 : 40}
|
|
354
|
+
marked={marked}
|
|
355
|
+
markBackdrop={palette.markBackdrop}
|
|
356
|
+
registerRow={(rowIndex) => surface.registerLine(blockIndex, rowIndex, gridLine)}
|
|
357
|
+
onRowMouseDown={surface.onLineMouseDown}
|
|
358
|
+
/>
|
|
359
|
+
{surface.cardsAfterLine(blockIndex, gridLine, true)}
|
|
360
|
+
</>,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const roleRuns = renderedStyleRuns(block);
|
|
277
365
|
const ranges = surface.rangesFor(blockIndex);
|
|
278
|
-
const
|
|
279
|
-
const lines = wrapLines(text, viewWidth > 0 ? viewWidth - marker.length - 6 : 0);
|
|
366
|
+
const lines = wrapLines(renderedText(block), viewWidth > 0 ? viewWidth - marker.length - 6 : 0);
|
|
280
367
|
const lineRows: React.ReactNode[] = [];
|
|
281
368
|
|
|
282
369
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
@@ -286,16 +373,16 @@ export function ThreadView({
|
|
|
286
373
|
lineRows.push(
|
|
287
374
|
lineRowFor({
|
|
288
375
|
blockIndex,
|
|
289
|
-
|
|
376
|
+
roleRuns,
|
|
290
377
|
line,
|
|
291
378
|
lineIndex,
|
|
292
379
|
ranges,
|
|
293
380
|
marker,
|
|
294
381
|
baseFg,
|
|
295
382
|
baseAttributes,
|
|
296
|
-
trailing: lineIndex === 0 ? changeTag : null,
|
|
297
383
|
}),
|
|
298
384
|
);
|
|
385
|
+
if (isLastLine) lineRows.push(headingRule(block));
|
|
299
386
|
const cards = surface.cardsAfterLine(blockIndex, line, isLastLine);
|
|
300
387
|
|
|
301
388
|
lineRows.push(...cards);
|
|
@@ -304,13 +391,7 @@ export function ThreadView({
|
|
|
304
391
|
}
|
|
305
392
|
}
|
|
306
393
|
|
|
307
|
-
//
|
|
308
|
-
// below its neighbour, and a code block announces its language first
|
|
309
|
-
const previous = display[blockIndex - 1];
|
|
310
|
-
const tight =
|
|
311
|
-
previous !== undefined &&
|
|
312
|
-
block.kind === previous.kind &&
|
|
313
|
-
(block.kind === "li" || block.kind === "oli");
|
|
394
|
+
// a code block announces its language first
|
|
314
395
|
const languageRow =
|
|
315
396
|
block.kind === "code" ? (
|
|
316
397
|
<text key="language" fg={tokens.textDim} selectable={false}>
|
|
@@ -318,22 +399,11 @@ export function ThreadView({
|
|
|
318
399
|
</text>
|
|
319
400
|
) : null;
|
|
320
401
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const leadingGap =
|
|
324
|
-
blockIndex === 0 || tight ? null : <box key="lead-gap" style={{ height: 1 }} />;
|
|
325
|
-
|
|
326
|
-
return (
|
|
327
|
-
<box
|
|
328
|
-
key={`discussion-block-${blockIndex}`}
|
|
329
|
-
id={`discussion-block-${blockIndex}`}
|
|
330
|
-
ref={virtual.measureRef(blockIndex)}
|
|
331
|
-
style={{ flexDirection: "column" }}
|
|
332
|
-
>
|
|
333
|
-
{leadingGap}
|
|
402
|
+
return measuredBox(
|
|
403
|
+
<>
|
|
334
404
|
{languageRow}
|
|
335
405
|
{lineRows}
|
|
336
|
-
|
|
406
|
+
</>,
|
|
337
407
|
);
|
|
338
408
|
};
|
|
339
409
|
|
|
@@ -385,10 +455,14 @@ export function ThreadView({
|
|
|
385
455
|
>
|
|
386
456
|
{virtualBlocks()}
|
|
387
457
|
</scrollbox>
|
|
458
|
+
{suspended ? null : (
|
|
459
|
+
<NavModeHint navMode={surface.navMode} surface="thread" theme={tokens} />
|
|
460
|
+
)}
|
|
388
461
|
</box>
|
|
389
462
|
<DiscussionMarkerRail
|
|
390
463
|
discussions={discussions}
|
|
391
464
|
spanQuote={surface.spanQuote}
|
|
465
|
+
focusedKey={surface.focusedDiscussion}
|
|
392
466
|
onJump={surface.jumpToDiscussion}
|
|
393
467
|
scrollbox={scrollRef}
|
|
394
468
|
theme={theme}
|
|
@@ -10,3 +10,10 @@ export const ShareLink: Story = {
|
|
|
10
10
|
expectedColors: [DARK.accent],
|
|
11
11
|
size: { width: 60, height: 12 },
|
|
12
12
|
};
|
|
13
|
+
|
|
14
|
+
export const LongMessage: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<Toast body="could not stop sharing: gateway revoke failed - the share may already be gone, try again in a moment" />
|
|
17
|
+
),
|
|
18
|
+
size: { width: 90, height: 14 },
|
|
19
|
+
};
|
package/src/components/Toast.tsx
CHANGED
|
@@ -10,17 +10,29 @@ export interface ToastProps {
|
|
|
10
10
|
theme?: Theme;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** A toast never spans the screen: a long message wraps inside this, like the settings dialog. */
|
|
14
|
+
const TOAST_MAX_WIDTH = 64;
|
|
15
|
+
|
|
13
16
|
export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
|
|
14
17
|
const tokens = useComponentTheme(theme);
|
|
15
|
-
const { width: terminalWidth } = useTerminalDimensions();
|
|
16
|
-
const width = Math.min(
|
|
18
|
+
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
19
|
+
const width = Math.min(
|
|
20
|
+
terminalWidth - 6,
|
|
21
|
+
Math.max(28, Math.min(TOAST_MAX_WIDTH, body.length + 4)),
|
|
22
|
+
);
|
|
23
|
+
// the message wraps at the content width, so grow the box to fit the wrapped lines
|
|
24
|
+
const contentWidth = Math.max(1, width - 4);
|
|
25
|
+
const lines = body
|
|
26
|
+
.split("\n")
|
|
27
|
+
.reduce((total, line) => total + Math.max(1, Math.ceil(line.length / contentWidth)), 0);
|
|
28
|
+
const height = Math.min(terminalHeight - 4, Math.max(6, lines + 4));
|
|
17
29
|
|
|
18
30
|
return (
|
|
19
31
|
<Dialog
|
|
20
32
|
isOpen
|
|
21
33
|
title={title ? ` ${title} ` : undefined}
|
|
22
34
|
width={width}
|
|
23
|
-
height={
|
|
35
|
+
height={height}
|
|
24
36
|
background={tokens.elevated}
|
|
25
37
|
theme={theme}
|
|
26
38
|
>
|