@cueloop/client 0.1.0-alpha.77 → 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.
Files changed (130) hide show
  1. package/package.json +14 -13
  2. package/src/App.autoclose.test.tsx +3 -3
  3. package/src/App.diff.test.tsx +22 -5
  4. package/src/App.export.test.tsx +1 -2
  5. package/src/App.focus.test.ts +1 -59
  6. package/src/App.identity.test.tsx +3 -3
  7. package/src/App.plan-render.test.tsx +209 -0
  8. package/src/App.plan-review.test.tsx +91 -26
  9. package/src/App.readonly.test.tsx +6 -5
  10. package/src/App.share-dialog.test.tsx +172 -0
  11. package/src/App.submit-confirm.test.tsx +39 -8
  12. package/src/App.test.tsx +33 -30
  13. package/src/App.toast.test.tsx +11 -4
  14. package/src/App.tsx +263 -274
  15. package/src/App.walk.test.tsx +11 -10
  16. package/src/App.workbench.test.tsx +86 -0
  17. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  18. package/src/annotation-palette.ts +1 -0
  19. package/src/ansi-screen.test.ts +28 -0
  20. package/src/ansi-screen.ts +72 -0
  21. package/src/app-screens.tsx +19 -9
  22. package/src/app-view-model.ts +3 -1
  23. package/src/components/AnnotatableFileView.tsx +1 -1
  24. package/src/components/AnnotationCards.tsx +18 -2
  25. package/src/components/AnsiScreen.stories.tsx +12 -0
  26. package/src/components/AnsiScreen.tsx +28 -0
  27. package/src/components/CompletionOverlay.stories.tsx +15 -1
  28. package/src/components/CompletionOverlay.tsx +22 -5
  29. package/src/components/ConfirmCard.stories.tsx +2 -0
  30. package/src/components/ConfirmCard.tsx +45 -18
  31. package/src/components/ConfirmDialog.stories.tsx +3 -3
  32. package/src/components/ConfirmDialog.tsx +11 -12
  33. package/src/components/DiffContentView.tsx +18 -11
  34. package/src/components/DiscussionMarkerRail.tsx +9 -4
  35. package/src/components/FileContentsView.tsx +1 -1
  36. package/src/components/GridTabContent.tsx +8 -2
  37. package/src/components/HandleChipsInput.stories.tsx +24 -0
  38. package/src/components/HandleChipsInput.test.tsx +35 -0
  39. package/src/components/HandleChipsInput.tsx +145 -0
  40. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  41. package/src/components/MarkdownGridBlock.tsx +72 -0
  42. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  43. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  44. package/src/components/MarkdownThreadEditor.tsx +162 -0
  45. package/src/components/NavModeHint.stories.tsx +40 -0
  46. package/src/components/NavModeHint.tsx +31 -0
  47. package/src/components/PromptDialog.test.tsx +2 -2
  48. package/src/components/PromptDialog.tsx +9 -10
  49. package/src/components/SettingsDialog.tsx +1 -0
  50. package/src/components/ShareDialog.stories.tsx +121 -0
  51. package/src/components/ShareDialog.tsx +552 -0
  52. package/src/components/SlashComposer.stories.tsx +37 -0
  53. package/src/components/SlashComposer.tsx +87 -0
  54. package/src/components/ThreadFooter.tsx +4 -1
  55. package/src/components/ThreadTree.tsx +12 -7
  56. package/src/components/ThreadView.selection.test.tsx +88 -1
  57. package/src/components/ThreadView.slash.test.tsx +0 -36
  58. package/src/components/ThreadView.tsx +200 -67
  59. package/src/components/Toast.stories.tsx +7 -0
  60. package/src/components/Toast.tsx +15 -8
  61. package/src/components/WalkWizard.stories.tsx +2 -2
  62. package/src/components/WalkWizard.tsx +10 -12
  63. package/src/components/__snapshots__/stories.test.tsx.snap +436 -182
  64. package/src/components/primitives/Button.tsx +6 -2
  65. package/src/components/primitives/Card.tsx +5 -2
  66. package/src/components/primitives/Dialog.tsx +2 -2
  67. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  68. package/src/components/primitives/DialogActions.tsx +39 -0
  69. package/src/components/primitives/Switch.stories.tsx +17 -0
  70. package/src/components/primitives/Switch.tsx +32 -0
  71. package/src/components/primitives/Tree.tsx +5 -1
  72. package/src/components/primitives/frame.ts +5 -0
  73. package/src/components/session-tree.ts +7 -6
  74. package/src/components/share-dialog-store.ts +58 -0
  75. package/src/components/stories-app.tsx +13 -4
  76. package/src/components/stories.test.tsx +12 -4
  77. package/src/components/story-tree.test.ts +32 -24
  78. package/src/components/story.ts +32 -15
  79. package/src/config.test.ts +18 -0
  80. package/src/config.ts +8 -1
  81. package/src/diff-fold.ts +1 -1
  82. package/src/diff-intraline.test.ts +75 -2
  83. package/src/diff-intraline.ts +95 -26
  84. package/src/intent-dispatch.test.ts +12 -3
  85. package/src/intent-dispatch.ts +10 -25
  86. package/src/key-bindings.ts +0 -6
  87. package/src/keymap.test.ts +2 -2
  88. package/src/keymap.ts +0 -2
  89. package/src/markdown-editor-syntax-style.ts +50 -0
  90. package/src/markdown-grid.test.ts +101 -0
  91. package/src/markdown-grid.ts +203 -0
  92. package/src/markdown-highlight.test.ts +61 -0
  93. package/src/markdown-highlight.ts +122 -0
  94. package/src/pasted-image.test.ts +67 -0
  95. package/src/pasted-image.ts +60 -0
  96. package/src/prototype-browser.test.ts +9 -1
  97. package/src/prototype-browser.ts +22 -0
  98. package/src/run.ts +39 -25
  99. package/src/serve.ts +1 -1
  100. package/src/share.ts +16 -2
  101. package/src/slash-palette.test.ts +17 -0
  102. package/src/slash-palette.ts +11 -0
  103. package/src/test-support.ts +14 -0
  104. package/src/thread-chords.test.ts +55 -178
  105. package/src/thread-chords.ts +78 -169
  106. package/src/thread-controller.curate.test.ts +1 -0
  107. package/src/thread-controller.derivation.test.ts +62 -0
  108. package/src/thread-controller.live-diff.test.ts +3 -0
  109. package/src/thread-controller.optimistic.test.ts +69 -0
  110. package/src/thread-controller.share.test.ts +60 -5
  111. package/src/thread-controller.switch-cache.test.ts +176 -0
  112. package/src/thread-controller.tree.test.ts +6 -1
  113. package/src/thread-controller.ts +386 -78
  114. package/src/use-annotation-surface.tsx +88 -63
  115. package/src/use-settings-dialog.tsx +14 -3
  116. package/src/use-thread-body-editing.tsx +58 -0
  117. package/src/view-plan.test.ts +56 -6
  118. package/src/view-plan.ts +154 -5
  119. package/src/App.share-choice.test.tsx +0 -105
  120. package/src/components/ManageAccessDialog.stories.tsx +0 -36
  121. package/src/components/ManageAccessDialog.test.tsx +0 -94
  122. package/src/components/ManageAccessDialog.tsx +0 -120
  123. package/src/components/ShareChoiceDialog.stories.tsx +0 -34
  124. package/src/components/ShareChoiceDialog.test.tsx +0 -103
  125. package/src/components/ShareChoiceDialog.tsx +0 -82
  126. package/src/components/ThreadsSidebar.stories.tsx +0 -36
  127. package/src/components/ThreadsSidebar.tsx +0 -71
  128. package/src/components/agent-launcher.stories.tsx +0 -26
  129. package/src/components/agent-launcher.test.tsx +0 -50
  130. package/src/components/agent-launcher.tsx +0 -149
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Render a table or frontmatter display block as its grid: a GFM table (bold
3
+ * header over a rule, aligned columns) or a leading-frontmatter key/value box.
4
+ * Layout is computed in markdown-grid; this only paints the positioned segments,
5
+ * dim for borders and bold for the header or key column. The grid annotates as
6
+ * one unit: every row registers the same block span for hit-testing, so a click
7
+ * or drag anchors a comment on it and a mark tints the whole grid.
8
+ */
9
+
10
+ import React from "react";
11
+ import type { MouseEvent as TerminalMouseEvent, TextRenderable } from "@opentui/core";
12
+ import { displayText, type DisplayBlock } from "../view-plan";
13
+ import type { Theme } from "../theme";
14
+ import { BOLD } from "../annotation-palette";
15
+ import {
16
+ layoutFrontmatterGrid,
17
+ layoutMarkdownTable,
18
+ parseFrontmatterRows,
19
+ parseMarkdownTable,
20
+ type GridLine,
21
+ } from "../markdown-grid";
22
+
23
+ function gridLines(block: DisplayBlock, contentWidth: number): GridLine[] {
24
+ if (block.kind === "table") return layoutMarkdownTable(parseMarkdownTable(displayText(block)));
25
+
26
+ return layoutFrontmatterGrid(parseFrontmatterRows(displayText(block)), contentWidth);
27
+ }
28
+
29
+ export function MarkdownGridBlock({
30
+ block,
31
+ theme,
32
+ contentWidth,
33
+ marked = false,
34
+ markBackdrop,
35
+ registerRow,
36
+ onRowMouseDown,
37
+ }: {
38
+ block: DisplayBlock;
39
+ theme: Theme;
40
+ contentWidth: number;
41
+ /** True while a discussion or held selection marks this grid; tints it so the annotation reads. */
42
+ marked?: boolean;
43
+ markBackdrop?: string;
44
+ /** Registers a grid row's renderable with the annotation surface, all mapping to the one block span. */
45
+ registerRow?: (rowIndex: number) => (renderable: TextRenderable | null) => void;
46
+ onRowMouseDown?: (event: TerminalMouseEvent) => void;
47
+ }): React.ReactNode {
48
+ const lines = gridLines(block, Math.max(12, contentWidth));
49
+
50
+ return (
51
+ <box style={{ flexDirection: "column", backgroundColor: marked ? markBackdrop : undefined }}>
52
+ {lines.map((line, lineIndex) => (
53
+ <box key={`grid-${lineIndex}`} style={{ flexDirection: "row" }}>
54
+ <text selectable={false}>
55
+ <span fg={theme.textDim}>{" "}</span>
56
+ </text>
57
+ <text selectable={false} ref={registerRow?.(lineIndex)} onMouseDown={onRowMouseDown}>
58
+ {line.map((segment, segmentIndex) => (
59
+ <span
60
+ key={segmentIndex}
61
+ fg={segment.dim ? theme.textDim : theme.text}
62
+ attributes={segment.bold ? BOLD : 0}
63
+ >
64
+ {segment.text}
65
+ </span>
66
+ ))}
67
+ </text>
68
+ </box>
69
+ ))}
70
+ </box>
71
+ );
72
+ }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { MarkdownThreadEditor } from "./MarkdownThreadEditor";
5
+
6
+ export const meta: StoryMeta = { title: "Surfaces/MarkdownThreadEditor" };
7
+
8
+ const SAMPLE =
9
+ "# Rollout Plan\n\nShip the store move behind a `flag`. See [docs](https://x.dev).\n\n- first step\n- second step\n";
10
+
11
+ export const Editing: Story = {
12
+ render: () => <MarkdownThreadEditor initialText={SAMPLE} theme={DARK} onExitEditor={() => {}} />,
13
+ expectedColors: [DARK.blue, DARK.textMuted, DARK.textDim],
14
+ size: { width: 62, height: 14 },
15
+ };
@@ -0,0 +1,98 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import React from "react";
3
+ import { testRender } from "@opentui/react/test-utils";
4
+ import { DARK } from "../theme";
5
+ import { press, settle } from "../test-support";
6
+ import { MarkdownThreadEditor, type MarkdownEditorHandle } from "./MarkdownThreadEditor";
7
+
8
+ describe("MarkdownThreadEditor", () => {
9
+ test("opens on the working copy and shows the caret position and save hint", async () => {
10
+ const setup = await testRender(
11
+ <MarkdownThreadEditor
12
+ initialText={"# Title\n\nbody text here\n"}
13
+ theme={DARK}
14
+ onExitEditor={() => {}}
15
+ />,
16
+ { width: 50, height: 12 },
17
+ );
18
+
19
+ await settle(setup);
20
+ const frame = setup.captureCharFrame();
21
+
22
+ expect(frame).toContain("Title");
23
+ expect(frame).toContain("body text here");
24
+ expect(frame).toContain("Ln 1/4");
25
+ expect(frame).toContain("save & close");
26
+ setup.renderer.destroy();
27
+ });
28
+
29
+ test("the exit handle saves the current text and leaves (the header toggle's path)", async () => {
30
+ const handleRef = React.createRef<MarkdownEditorHandle>();
31
+ let exitedWith = "";
32
+ const setup = await testRender(
33
+ <MarkdownThreadEditor
34
+ ref={handleRef}
35
+ initialText={"keep this"}
36
+ theme={DARK}
37
+ onExitEditor={(text) => {
38
+ exitedWith = text;
39
+ }}
40
+ />,
41
+ { width: 40, height: 8 },
42
+ );
43
+
44
+ await settle(setup);
45
+ handleRef.current?.requestExit();
46
+
47
+ expect(exitedWith).toBe("keep this");
48
+ setup.renderer.destroy();
49
+ });
50
+
51
+ test("highlights align to the exact characters across lines (no newline drift)", async () => {
52
+ const setup = await testRender(
53
+ <MarkdownThreadEditor
54
+ initialText={"# Head\n\nrun `code` now\n"}
55
+ theme={DARK}
56
+ onExitEditor={() => {}}
57
+ />,
58
+ { width: 40, height: 10 },
59
+ );
60
+
61
+ await settle(setup);
62
+ const spans = setup.captureSpans().lines.flatMap((line) => line.spans);
63
+ const hexOf = (color: { toInts: () => number[] }): string =>
64
+ "#" +
65
+ color
66
+ .toInts()
67
+ .slice(0, 3)
68
+ .map((part) => part.toString(16).padStart(2, "0"))
69
+ .join("");
70
+ // the code highlight must cover exactly `code`, not a drifted slice like "ode` "
71
+ const codeSpan = spans.find((span) => span.text === "`code`");
72
+
73
+ expect(codeSpan).toBeDefined();
74
+ expect(codeSpan?.fg && hexOf(codeSpan.fg)).toBe(DARK.textDim);
75
+ setup.renderer.destroy();
76
+ });
77
+
78
+ test("escape is a no-op - it never leaves the editor (IDE convention)", async () => {
79
+ let exits = 0;
80
+ const setup = await testRender(
81
+ <MarkdownThreadEditor
82
+ initialText={"stay put"}
83
+ theme={DARK}
84
+ onExitEditor={() => {
85
+ exits += 1;
86
+ }}
87
+ />,
88
+ { width: 40, height: 8 },
89
+ );
90
+
91
+ await settle(setup);
92
+ await press(setup, "escape");
93
+
94
+ expect(exits).toBe(0);
95
+ expect(setup.captureCharFrame()).toContain("save & close");
96
+ setup.renderer.destroy();
97
+ });
98
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The native inline markdown editor for a thread's body: a full-height text area
3
+ * over the working copy that lightly marks markdown as you type - a heading's
4
+ * marker dims and its title bolds, links color, inline and fenced code gray -
5
+ * and reports the caret line and column, replacing the external-editor hand-off so
6
+ * editing needs no per-user editor configuration. cmd, meta, or ctrl + enter
7
+ * saves the working copy and closes (loss-free, like the hand-off it replaces),
8
+ * as does the header edit/normal toggle; plain enter breaks the line. Escape is
9
+ * a no-op inside the editor, matching a modeless IDE - you leave by an explicit
10
+ * action, not by escaping the text. The raw markdown stays visible - a concealed
11
+ * rendered preview is a later layer.
12
+ */
13
+
14
+ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
15
+ import type { KeyBinding, TextareaRenderable } from "@opentui/core";
16
+ import { type Theme } from "../theme";
17
+ import { markdownHighlightRanges } from "../markdown-highlight";
18
+ import { markdownEditorStyle } from "../markdown-editor-syntax-style";
19
+
20
+ export interface MarkdownThreadEditorProps {
21
+ /** The thread body's working copy; the editor opens on this markdown text. */
22
+ initialText: string;
23
+ theme: Theme;
24
+ /** Called with the edited markdown when the reviewer leaves (cmd/ctrl+enter or the header toggle); both save. */
25
+ onExitEditor: (text: string) => void;
26
+ }
27
+
28
+ /** An imperative exit for callers outside the editor (the header edit/normal toggle): save the current text and leave. */
29
+ export interface MarkdownEditorHandle {
30
+ requestExit: () => void;
31
+ }
32
+
33
+ // three modifiers cover terminal variance for save-and-close; plain enter is a newline, escape stays unbound
34
+ const MARKDOWN_EDITOR_KEY_BINDINGS: KeyBinding[] = [
35
+ { name: "return", super: true, action: "submit" },
36
+ { name: "return", meta: true, action: "submit" },
37
+ { name: "return", ctrl: true, action: "submit" },
38
+ { name: "a", super: true, action: "select-all" },
39
+ { name: "a", meta: true, action: "select-all" },
40
+ { name: "return", action: "newline" },
41
+ { name: "return", shift: true, action: "newline" },
42
+ ];
43
+
44
+ /** The caret's 1-based line and column, plus the buffer's total line count, for the status footer. */
45
+ interface MarkdownEditorPosition {
46
+ line: number;
47
+ column: number;
48
+ lineCount: number;
49
+ }
50
+
51
+ /** For each offset 0..length, the count of newlines before it, mapping a source offset to the buffer's newline-excluded offset. */
52
+ function newlinePrefixCounts(text: string): number[] {
53
+ const counts = Array.from({ length: text.length + 1 }, () => 0);
54
+ let seen = 0;
55
+
56
+ for (let index = 0; index < text.length; index++) {
57
+ counts[index] = seen;
58
+ if (text[index] === "\n") seen += 1;
59
+ }
60
+ counts[text.length] = seen;
61
+
62
+ return counts;
63
+ }
64
+
65
+ export const MarkdownThreadEditor = forwardRef<MarkdownEditorHandle, MarkdownThreadEditorProps>(
66
+ function MarkdownThreadEditor({ initialText, theme, onExitEditor }, handleRef): React.ReactNode {
67
+ const editorRef = useRef<TextareaRenderable | null>(null);
68
+ const [position, setPosition] = useState<MarkdownEditorPosition>({
69
+ line: 1,
70
+ column: 1,
71
+ lineCount: initialText.split("\n").length,
72
+ });
73
+
74
+ const exitWithCurrentText = (): void =>
75
+ onExitEditor(editorRef.current?.plainText ?? initialText);
76
+
77
+ useImperativeHandle(handleRef, () => ({ requestExit: exitWithCurrentText }));
78
+
79
+ const paintMarkdown = (editor: TextareaRenderable): void => {
80
+ const { styleIdFor } = markdownEditorStyle(theme);
81
+ const text = editor.plainText;
82
+ // addHighlightByCharRange counts offsets with newlines excluded, so map source offsets down by the
83
+ // newlines before them - otherwise a highlight drifts one cell per preceding line
84
+ const newlinesBefore = newlinePrefixCounts(text);
85
+
86
+ editor.editBuffer.clearAllHighlights();
87
+ for (const range of markdownHighlightRanges(text)) {
88
+ editor.editBuffer.addHighlightByCharRange({
89
+ start: range.start - newlinesBefore[range.start]!,
90
+ end: range.end - newlinesBefore[range.end]!,
91
+ styleId: styleIdFor(range.group),
92
+ });
93
+ }
94
+ };
95
+
96
+ // paint synchronously at commit so the first frame already shows styled markdown
97
+ useLayoutEffect(() => {
98
+ const editor = editorRef.current;
99
+
100
+ if (!editor) return;
101
+ editor.editBuffer.setSyntaxStyle(markdownEditorStyle(theme).style);
102
+ paintMarkdown(editor);
103
+ setPosition((prior) => ({ ...prior, lineCount: editor.editBuffer.getLineCount() }));
104
+ // eslint-disable-next-line react-hooks/exhaustive-deps
105
+ }, []);
106
+
107
+ return (
108
+ <box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2 }}>
109
+ <textarea
110
+ ref={editorRef}
111
+ focused
112
+ initialValue={initialText}
113
+ keyBindings={MARKDOWN_EDITOR_KEY_BINDINGS}
114
+ wrapMode="word"
115
+ cursorStyle={{ style: "block", blinking: true }}
116
+ onSubmit={exitWithCurrentText}
117
+ onCursorChange={(event) =>
118
+ setPosition((prior) => ({
119
+ ...prior,
120
+ line: event.line + 1,
121
+ column: event.visualColumn + 1,
122
+ }))
123
+ }
124
+ onContentChange={() => {
125
+ const editor = editorRef.current;
126
+
127
+ if (!editor) return;
128
+ paintMarkdown(editor);
129
+ setPosition((prior) => ({ ...prior, lineCount: editor.editBuffer.getLineCount() }));
130
+ }}
131
+ style={{
132
+ flexGrow: 1,
133
+ backgroundColor: "transparent",
134
+ focusedBackgroundColor: "transparent",
135
+ textColor: theme.text,
136
+ focusedTextColor: theme.text,
137
+ }}
138
+ />
139
+ <MarkdownEditorStatus position={position} theme={theme} />
140
+ </box>
141
+ );
142
+ },
143
+ );
144
+
145
+ /** The one-row footer under the editor: caret line and column, total lines, and the save-and-close hint. */
146
+ function MarkdownEditorStatus({
147
+ position,
148
+ theme,
149
+ }: {
150
+ position: MarkdownEditorPosition;
151
+ theme: Theme;
152
+ }): React.ReactNode {
153
+ return (
154
+ <box style={{ flexDirection: "row", flexShrink: 0 }}>
155
+ <text
156
+ fg={theme.textMuted}
157
+ >{`Ln ${position.line}/${position.lineCount} Col ${position.column}`}</text>
158
+ <box style={{ flexGrow: 1 }} />
159
+ <text fg={theme.textDim}>{"⌃⏎ save & close"}</text>
160
+ </box>
161
+ );
162
+ }
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { NavModeHint } from "./NavModeHint";
5
+
6
+ export const meta: StoryMeta = { title: "Chrome/NavModeHint" };
7
+
8
+ function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
9
+ return <box style={{ width: "100%", backgroundColor: DARK.panel }}>{children}</box>;
10
+ }
11
+
12
+ export const ThreadNav: Story = {
13
+ render: () => (
14
+ <Frame>
15
+ <NavModeHint navMode surface="thread" theme={DARK} />
16
+ </Frame>
17
+ ),
18
+ expectedColors: [DARK.textDim],
19
+ size: { width: 80, height: 1 },
20
+ };
21
+
22
+ export const DiffNav: Story = {
23
+ render: () => (
24
+ <Frame>
25
+ <NavModeHint navMode surface="diff" theme={DARK} />
26
+ </Frame>
27
+ ),
28
+ expectedColors: [DARK.textDim],
29
+ size: { width: 80, height: 1 },
30
+ };
31
+
32
+ export const Composing: Story = {
33
+ render: () => (
34
+ <Frame>
35
+ <NavModeHint navMode={false} surface="thread" theme={DARK} />
36
+ </Frame>
37
+ ),
38
+ expectedColors: [DARK.textDim],
39
+ size: { width: 80, height: 1 },
40
+ };
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import type { Theme } from "../theme";
3
+
4
+ const DIFF_NAV_HINT = "x reject · c fold · d layout · k walk · ⏎ submit · type to leave";
5
+ const THREAD_NAV_HINT = "c comment · e edit · s share · n/p cards · ⏎ submit · type to leave";
6
+ const BARE_NAV_HINT = "c comment · z fold · type to leave";
7
+ const COMPOSE_HINT = "type to comment · esc for nav mode";
8
+
9
+ const NAV_HINTS = { thread: THREAD_NAV_HINT, diff: DIFF_NAV_HINT, bare: BARE_NAV_HINT };
10
+
11
+ export type NavHintSurface = keyof typeof NAV_HINTS;
12
+
13
+ export function NavModeHint({
14
+ navMode,
15
+ surface,
16
+ theme,
17
+ }: {
18
+ navMode: boolean;
19
+ surface: NavHintSurface;
20
+ theme: Theme;
21
+ }): React.ReactNode {
22
+ const navHint = NAV_HINTS[surface];
23
+
24
+ return (
25
+ <box style={{ height: 1, flexDirection: "row", paddingLeft: 2 }}>
26
+ <text selectable={false} fg={theme.textDim}>
27
+ {navMode ? navHint : COMPOSE_HINT}
28
+ </text>
29
+ </box>
30
+ );
31
+ }
@@ -47,12 +47,12 @@ test("clicking save fires onSave and clicking cancel fires onCancel", async () =
47
47
  );
48
48
  await settle(setup);
49
49
 
50
- const save = locateText(setup, "enter save");
50
+ const save = locateText(setup, "save");
51
51
  await setup.mockMouse.click(save.column, save.row);
52
52
  expect(saved).toBe(1);
53
53
  expect(cancelled).toBe(0);
54
54
 
55
- const cancel = locateText(setup, "esc cancel");
55
+ const cancel = locateText(setup, "cancel");
56
56
  await setup.mockMouse.click(cancel.column, cancel.row);
57
57
  expect(cancelled).toBe(1);
58
58
  expect(saved).toBe(1);
@@ -11,6 +11,7 @@ import type { KeyBinding, TextareaRenderable } from "@opentui/core";
11
11
  import type { Theme } from "../theme";
12
12
  import { useComponentTheme } from "./theme-context";
13
13
  import { Dialog } from "./primitives/Dialog";
14
+ import { DialogActions } from "./primitives/DialogActions";
14
15
 
15
16
  // ⏎ submits (suppresses the textarea's default newline); the grammar owns save.
16
17
  const PROMPT_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
@@ -18,7 +19,7 @@ const PROMPT_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }]
18
19
  export interface PromptDialogProps {
19
20
  isOpen: boolean;
20
21
  title?: string;
21
- label: string;
22
+ label?: string;
22
23
  value: string;
23
24
  placeholder?: string;
24
25
  onInput: (text: string) => void;
@@ -69,7 +70,7 @@ export function PromptDialog({
69
70
  paddingTop: 1,
70
71
  }}
71
72
  >
72
- <text fg={tokens.textDim}>{label}</text>
73
+ {label ? <text fg={tokens.textDim}>{label}</text> : null}
73
74
  <textarea
74
75
  ref={inputRef}
75
76
  focused
@@ -86,14 +87,12 @@ export function PromptDialog({
86
87
  }}
87
88
  />
88
89
  <box style={{ flexGrow: 1 }} />
89
- <box style={{ flexDirection: "row" }}>
90
- <box onMouseUp={onSave} style={{ marginRight: 3 }}>
91
- <text fg={tokens.textDim}>enter save</text>
92
- </box>
93
- <box onMouseUp={onCancel}>
94
- <text fg={tokens.textDim}>esc cancel</text>
95
- </box>
96
- </box>
90
+ <DialogActions
91
+ confirmLabel="save"
92
+ onConfirm={() => onSave?.()}
93
+ onCancel={() => onCancel?.()}
94
+ theme={theme}
95
+ />
97
96
  </box>
98
97
  </Dialog>
99
98
  );
@@ -118,6 +118,7 @@ export function SettingsDialog({
118
118
  expandedIds={new Set<string>()}
119
119
  selectedId={category.id}
120
120
  hideIcons
121
+ selectedBackground={tokens.border}
121
122
  onSelect={onCategorySelect}
122
123
  onToggle={onCategorySelect}
123
124
  theme={theme}
@@ -0,0 +1,121 @@
1
+ import React from "react";
2
+ import type { ShareLink } from "@cueloop/schema";
3
+ import { DARK } from "../theme";
4
+ import type { Story, StoryMeta } from "./story";
5
+ import { ShareDialog } from "./ShareDialog";
6
+ import { newWizardDraft, shareDialogStore } from "./share-dialog-store";
7
+
8
+ export const meta: StoryMeta = { title: "Dialogs/Share" };
9
+
10
+ const LINKS: ShareLink[] = [
11
+ { id: "p_ab12cd", name: "reviewers", requireAuth: true, allowlist: ["octocat"] },
12
+ { id: "p_ef34gh", requireAuth: false, allowlist: [] },
13
+ ];
14
+
15
+ const NOOP = {
16
+ onCreateLink: () => {},
17
+ onUpdateLink: () => {},
18
+ onDeleteLink: () => {},
19
+ onCopyLink: () => {},
20
+ onClose: () => {},
21
+ };
22
+
23
+ const SIZE = { width: 80, height: 24 };
24
+
25
+ export const EmptyList: Story = {
26
+ render: () => {
27
+ shareDialogStore.getState().reset();
28
+
29
+ return (
30
+ <ShareDialog
31
+ isOpen
32
+ threadName="Improve retry logic"
33
+ links={[]}
34
+ isOwner
35
+ {...NOOP}
36
+ theme={DARK}
37
+ />
38
+ );
39
+ },
40
+ size: SIZE,
41
+ };
42
+
43
+ export const WithLinks: Story = {
44
+ render: () => {
45
+ shareDialogStore.getState().reset();
46
+
47
+ return (
48
+ <ShareDialog
49
+ isOpen
50
+ threadName="Improve retry logic"
51
+ links={LINKS}
52
+ isOwner
53
+ {...NOOP}
54
+ theme={DARK}
55
+ />
56
+ );
57
+ },
58
+ size: SIZE,
59
+ };
60
+
61
+ export const ExportComingSoon: Story = {
62
+ render: () => {
63
+ shareDialogStore.getState().reset();
64
+ shareDialogStore.getState().setCategory("export");
65
+
66
+ return (
67
+ <ShareDialog
68
+ isOpen
69
+ threadName="Improve retry logic"
70
+ links={LINKS}
71
+ isOwner
72
+ {...NOOP}
73
+ theme={DARK}
74
+ />
75
+ );
76
+ },
77
+ size: SIZE,
78
+ };
79
+
80
+ export const NewLinkWizard: Story = {
81
+ render: () => {
82
+ shareDialogStore.getState().reset();
83
+ shareDialogStore.getState().openWizard(newWizardDraft("Improve retry logic"));
84
+
85
+ return (
86
+ <ShareDialog
87
+ isOpen
88
+ threadName="Improve retry logic"
89
+ links={[]}
90
+ isOwner
91
+ {...NOOP}
92
+ theme={DARK}
93
+ />
94
+ );
95
+ },
96
+ size: SIZE,
97
+ };
98
+
99
+ export const AuthWizard: Story = {
100
+ render: () => {
101
+ shareDialogStore.getState().reset();
102
+ shareDialogStore.getState().openWizard({
103
+ editingId: null,
104
+ name: "reviewers",
105
+ requireAuth: true,
106
+ allowlist: ["octocat", "hubot"],
107
+ });
108
+
109
+ return (
110
+ <ShareDialog
111
+ isOpen
112
+ threadName="Improve retry logic"
113
+ links={[]}
114
+ isOwner
115
+ {...NOOP}
116
+ theme={DARK}
117
+ />
118
+ );
119
+ },
120
+ size: SIZE,
121
+ };