@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +40 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +176 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +428 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +222 -0
- package/src/App.test.tsx +333 -131
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1338 -608
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +701 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +574 -0
- package/src/app-view-model.ts +162 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +329 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +153 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +876 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- 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/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -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/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +59 -0
- package/src/components/ProjectTreeView.tsx +111 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +899 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +472 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +142 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2238 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +56 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +517 -33
- package/src/config.ts +467 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +13 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +723 -0
- package/src/intent-dispatch.ts +692 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +461 -113
- package/src/keymap.ts +321 -79
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +52 -0
- package/src/launch-layout.ts +42 -0
- package/src/mark-runs.ts +121 -0
- 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/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +64 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +78 -13
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-controller.curate.test.ts +450 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +1893 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/use-annotation-surface.tsx +915 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-settings-dialog.tsx +320 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +137 -9
- package/src/view-diff.ts +269 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +456 -0
- package/src/view-plan.ts +719 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +81 -0
- package/src/workbench-snapshot.ts +29 -0
- package/src/session-controller.ts +0 -345
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -312
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The body an editor tab paints: the whole working-tree/artifact diff for the
|
|
3
|
+
* Changes tab, or a single file's diff (or read-only contents) for a file tab.
|
|
4
|
+
* Shared by the thread workbench and the bare-launch shell so both render a
|
|
5
|
+
* changed file the same way, with comments routed back through one surface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from "react";
|
|
9
|
+
import { type Anchor } from "@cueloop/schema";
|
|
10
|
+
import { dimmedTheme, type Theme } from "../theme";
|
|
11
|
+
import { changesMarks, type DiffRow } from "../view-diff";
|
|
12
|
+
import {
|
|
13
|
+
DiffContentView,
|
|
14
|
+
type DiffFoldControls,
|
|
15
|
+
type DiffContentViewProps,
|
|
16
|
+
} from "./DiffContentView";
|
|
17
|
+
import { AnnotatableFileView } from "./AnnotatableFileView";
|
|
18
|
+
import type { EditorTab } from "./editor-grid";
|
|
19
|
+
|
|
20
|
+
/** The inline-commenting props the diff sheet shares with the thread view, wired once by the app. */
|
|
21
|
+
export type DiffSurfaceProps = Pick<
|
|
22
|
+
DiffContentViewProps,
|
|
23
|
+
| "session"
|
|
24
|
+
| "quickActions"
|
|
25
|
+
| "observer"
|
|
26
|
+
| "commentsEnabled"
|
|
27
|
+
| "resolved"
|
|
28
|
+
| "suspended"
|
|
29
|
+
| "onComposingChange"
|
|
30
|
+
| "onObserverBlocked"
|
|
31
|
+
| "onCursorChange"
|
|
32
|
+
| "focusedAnnotationId"
|
|
33
|
+
| "onFocusAnnotation"
|
|
34
|
+
| "onAnnotate"
|
|
35
|
+
| "onReply"
|
|
36
|
+
| "onUpdateAnnotation"
|
|
37
|
+
| "resolveAuthorLabel"
|
|
38
|
+
| "onNavCommand"
|
|
39
|
+
| "onExit"
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
/** The Changes tab body: the whole diff in one scroll container, or a bare hint when nothing changed. */
|
|
43
|
+
export function ChangesTabBody(props: {
|
|
44
|
+
rows: DiffRow[];
|
|
45
|
+
surface: DiffSurfaceProps;
|
|
46
|
+
rejectedRows: Set<number>;
|
|
47
|
+
fold?: DiffFoldControls;
|
|
48
|
+
fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
|
|
49
|
+
split?: boolean;
|
|
50
|
+
dimmed: boolean;
|
|
51
|
+
/** Rows the sibling Thread pane spends on its footer, so this empty hint centers level with it. */
|
|
52
|
+
emptyBottomPadding?: number;
|
|
53
|
+
theme: Theme;
|
|
54
|
+
}): React.ReactNode {
|
|
55
|
+
const session = props.surface.session;
|
|
56
|
+
const marks = useMemo(
|
|
57
|
+
() => changesMarks(session, props.rows, props.surface.focusedAnnotationId),
|
|
58
|
+
[session, props.rows, props.surface.focusedAnnotationId],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (props.rows.length === 0) {
|
|
62
|
+
return (
|
|
63
|
+
<box
|
|
64
|
+
style={{
|
|
65
|
+
flexGrow: 1,
|
|
66
|
+
flexDirection: "column",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
justifyContent: "center",
|
|
69
|
+
paddingBottom: props.emptyBottomPadding,
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<text fg={props.theme.textDim}>No changes</text>
|
|
73
|
+
</box>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<DiffContentView
|
|
79
|
+
rows={props.rows}
|
|
80
|
+
marks={marks}
|
|
81
|
+
{...props.surface}
|
|
82
|
+
rejectedRows={props.rejectedRows}
|
|
83
|
+
fold={props.fold}
|
|
84
|
+
fileStats={props.fileStats}
|
|
85
|
+
split={props.split}
|
|
86
|
+
theme={props.dimmed ? dimmedTheme(props.theme) : undefined}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** An editor tab's body: the whole diff for the Changes tab, a file's diff or contents for a file tab. */
|
|
92
|
+
export function GridTabContent(props: {
|
|
93
|
+
tab: EditorTab;
|
|
94
|
+
rows: DiffRow[];
|
|
95
|
+
surface: DiffSurfaceProps;
|
|
96
|
+
rejectedRows: Set<number>;
|
|
97
|
+
fold?: DiffFoldControls;
|
|
98
|
+
fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
|
|
99
|
+
split?: boolean;
|
|
100
|
+
dimmed: boolean;
|
|
101
|
+
readFile: (path: string) => Promise<string | null>;
|
|
102
|
+
/** Persist a comment on a project file; the file view built the anchor against its own lines. */
|
|
103
|
+
onAddFileComment: (path: string, anchor: Anchor, body: string) => void;
|
|
104
|
+
/** Rows the sibling Thread pane spends on its footer, so the empty hint centers level with it. */
|
|
105
|
+
emptyBottomPadding?: number;
|
|
106
|
+
theme: Theme;
|
|
107
|
+
}): React.ReactNode {
|
|
108
|
+
const { tab } = props;
|
|
109
|
+
if (tab.kind === "file" && tab.fileView === "contents" && tab.path !== undefined) {
|
|
110
|
+
const filePath = tab.path;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<AnnotatableFileView
|
|
114
|
+
path={filePath}
|
|
115
|
+
loadContents={props.readFile}
|
|
116
|
+
session={props.surface.session}
|
|
117
|
+
quickActions={props.surface.quickActions}
|
|
118
|
+
observer={props.surface.observer}
|
|
119
|
+
resolved={props.surface.resolved}
|
|
120
|
+
suspended={props.surface.suspended}
|
|
121
|
+
focusedAnnotationId={props.surface.focusedAnnotationId}
|
|
122
|
+
onFocusAnnotation={props.surface.onFocusAnnotation}
|
|
123
|
+
onComposingChange={props.surface.onComposingChange}
|
|
124
|
+
onCursorChange={props.surface.onCursorChange}
|
|
125
|
+
onObserverBlocked={props.surface.onObserverBlocked}
|
|
126
|
+
onAddComment={(anchor, body) => props.onAddFileComment(filePath, anchor, body)}
|
|
127
|
+
onReply={props.surface.onReply}
|
|
128
|
+
onUpdateAnnotation={props.surface.onUpdateAnnotation}
|
|
129
|
+
resolveAuthorLabel={props.surface.resolveAuthorLabel}
|
|
130
|
+
onExit={props.surface.onExit}
|
|
131
|
+
theme={props.theme}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (tab.kind !== "file") {
|
|
136
|
+
return (
|
|
137
|
+
<ChangesTabBody
|
|
138
|
+
rows={props.rows}
|
|
139
|
+
surface={props.surface}
|
|
140
|
+
rejectedRows={props.rejectedRows}
|
|
141
|
+
fold={props.fold}
|
|
142
|
+
fileStats={props.fileStats}
|
|
143
|
+
split={props.split}
|
|
144
|
+
dimmed={props.dimmed}
|
|
145
|
+
emptyBottomPadding={props.emptyBottomPadding}
|
|
146
|
+
theme={props.theme}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
// a single-file tab shows that file's rows alone, so its row indices are its own: comments and
|
|
151
|
+
// the caret report back in whole-diff indices, and the fold controls (a band to fold) do not apply
|
|
152
|
+
const fileRowIndices = props.rows.flatMap((row, index) => (row.file === tab.path ? [index] : []));
|
|
153
|
+
const rows = fileRowIndices.map((index) => props.rows[index]!);
|
|
154
|
+
const wholeIndex = (rowIndex: number): number => fileRowIndices[rowIndex] ?? rowIndex;
|
|
155
|
+
const rejectedRows = new Set(
|
|
156
|
+
fileRowIndices.flatMap((index, rowIndex) => (props.rejectedRows.has(index) ? [rowIndex] : [])),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<ChangesTabBody
|
|
161
|
+
rows={rows}
|
|
162
|
+
surface={{
|
|
163
|
+
...props.surface,
|
|
164
|
+
onCursorChange: (rowIndex) => props.surface.onCursorChange?.(wholeIndex(rowIndex)),
|
|
165
|
+
onAnnotate: (span, body) =>
|
|
166
|
+
props.surface.onAnnotate(
|
|
167
|
+
{
|
|
168
|
+
start: { blockIndex: wholeIndex(span.start.blockIndex), char: span.start.char },
|
|
169
|
+
end: { blockIndex: wholeIndex(span.end.blockIndex), char: span.end.char },
|
|
170
|
+
},
|
|
171
|
+
body,
|
|
172
|
+
),
|
|
173
|
+
}}
|
|
174
|
+
rejectedRows={rejectedRows}
|
|
175
|
+
fileStats={props.fileStats}
|
|
176
|
+
split={props.split}
|
|
177
|
+
dimmed={props.dimmed}
|
|
178
|
+
emptyBottomPadding={props.emptyBottomPadding}
|
|
179
|
+
theme={props.theme}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { HandleChipsInput } from "./HandleChipsInput";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Inputs/HandleChips" };
|
|
7
|
+
|
|
8
|
+
export const WithHandles: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<box style={{ padding: 1, backgroundColor: DARK.elevated }}>
|
|
11
|
+
<HandleChipsInput logins={["octocat", "hubot"]} onChange={() => {}} theme={DARK} />
|
|
12
|
+
</box>
|
|
13
|
+
),
|
|
14
|
+
size: { width: 48, height: 6 },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const Empty: Story = {
|
|
18
|
+
render: () => (
|
|
19
|
+
<box style={{ padding: 1, backgroundColor: DARK.elevated }}>
|
|
20
|
+
<HandleChipsInput logins={[]} onChange={() => {}} theme={DARK} />
|
|
21
|
+
</box>
|
|
22
|
+
),
|
|
23
|
+
size: { width: 48, height: 6 },
|
|
24
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Backspace on the empty field pulls the last chip back in to edit, rather than deleting it. */
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
6
|
+
import { DARK } from "../theme";
|
|
7
|
+
import { press, settle } from "../test-support";
|
|
8
|
+
import { HandleChipsInput } from "./HandleChipsInput";
|
|
9
|
+
|
|
10
|
+
/** A controlled host so onChange edits flow back into the rendered list, like the wizard. */
|
|
11
|
+
function Harness({ initial }: { initial: string[] }): React.ReactNode {
|
|
12
|
+
const [logins, setLogins] = useState(initial);
|
|
13
|
+
|
|
14
|
+
return <HandleChipsInput logins={logins} onChange={setLogins} theme={DARK} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("HandleChipsInput", () => {
|
|
18
|
+
test("backspace on the empty field restores the last chip as editable text", async () => {
|
|
19
|
+
// Arrange - one committed chip and an empty input showing its placeholder
|
|
20
|
+
const setup = await testRender(<Harness initial={["octocat"]} />, { width: 48, height: 6 });
|
|
21
|
+
|
|
22
|
+
await settle(setup);
|
|
23
|
+
expect(setup.captureCharFrame()).toContain("handle");
|
|
24
|
+
|
|
25
|
+
// Act - backspace with the input empty
|
|
26
|
+
await press(setup, "backspace");
|
|
27
|
+
|
|
28
|
+
// Assert - the handle is back in the field (placeholder gone), not deleted
|
|
29
|
+
const frame = setup.captureCharFrame();
|
|
30
|
+
|
|
31
|
+
expect(frame).toContain("octocat");
|
|
32
|
+
expect(frame).not.toContain("handle");
|
|
33
|
+
setup.renderer.destroy();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tag-style input for GitHub logins: each added handle is an inline chip with
|
|
3
|
+
* a hover × to remove, and a trailing one-line field where enter adds the next
|
|
4
|
+
* and backspace on an empty field drops the last. Controlled - the caller owns
|
|
5
|
+
* the list and receives every change.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { useKeyboard } from "@opentui/react";
|
|
10
|
+
import type { KeyBinding, TextareaRenderable } from "@opentui/core";
|
|
11
|
+
import type { Theme } from "../theme";
|
|
12
|
+
import { useComponentTheme } from "./theme-context";
|
|
13
|
+
import { NERD } from "./primitives/icons";
|
|
14
|
+
|
|
15
|
+
// enter adds the handle rather than inserting a newline into the one-line field
|
|
16
|
+
const ADD_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
|
|
17
|
+
|
|
18
|
+
function HandleChip({
|
|
19
|
+
login,
|
|
20
|
+
onRemove,
|
|
21
|
+
tokens,
|
|
22
|
+
}: {
|
|
23
|
+
login: string;
|
|
24
|
+
onRemove: () => void;
|
|
25
|
+
tokens: Theme;
|
|
26
|
+
}): React.ReactNode {
|
|
27
|
+
const [hovered, setHovered] = useState(false);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<box
|
|
31
|
+
onMouseOver={() => setHovered(true)}
|
|
32
|
+
onMouseOut={() => setHovered(false)}
|
|
33
|
+
style={{
|
|
34
|
+
flexDirection: "row",
|
|
35
|
+
flexShrink: 0,
|
|
36
|
+
marginRight: 2,
|
|
37
|
+
paddingLeft: 1,
|
|
38
|
+
backgroundColor: tokens.border,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<text fg={tokens.text} style={{ wrapMode: "none" }}>{`@${login}`}</text>
|
|
42
|
+
<box
|
|
43
|
+
onMouseUp={(event) => {
|
|
44
|
+
event.stopPropagation();
|
|
45
|
+
onRemove();
|
|
46
|
+
}}
|
|
47
|
+
style={{ paddingLeft: 1, paddingRight: 1 }}
|
|
48
|
+
>
|
|
49
|
+
<text fg={tokens.textDim}>{hovered ? NERD.close : " "}</text>
|
|
50
|
+
</box>
|
|
51
|
+
</box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface HandleChipsInputProps {
|
|
56
|
+
logins: string[];
|
|
57
|
+
onChange: (logins: string[]) => void;
|
|
58
|
+
/** Whether this field owns the cursor and backspace; false shows the chips without a blinking caret. */
|
|
59
|
+
focused?: boolean;
|
|
60
|
+
/** Fired on backspace when the field is empty and no chips remain, so the caller can collapse the section. */
|
|
61
|
+
onEmptyBackspace?: () => void;
|
|
62
|
+
theme?: Theme;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function HandleChipsInput({
|
|
66
|
+
logins,
|
|
67
|
+
onChange,
|
|
68
|
+
focused = true,
|
|
69
|
+
onEmptyBackspace,
|
|
70
|
+
theme,
|
|
71
|
+
}: HandleChipsInputProps): React.ReactNode {
|
|
72
|
+
const tokens = useComponentTheme(theme);
|
|
73
|
+
const inputRef = useRef<TextareaRenderable | null>(null);
|
|
74
|
+
// the text the current field generation mounts with; a remount reseeds it
|
|
75
|
+
const [seed, setSeed] = useState("");
|
|
76
|
+
const [inputGeneration, setInputGeneration] = useState(0);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const input = inputRef.current;
|
|
80
|
+
|
|
81
|
+
if (!input || !focused) return;
|
|
82
|
+
input.focus();
|
|
83
|
+
input.cursorOffset = seed.length; // land the caret at the end of the seeded text
|
|
84
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
|
+
}, [inputGeneration, focused]);
|
|
86
|
+
|
|
87
|
+
const reseed = (text: string): void => {
|
|
88
|
+
setSeed(text);
|
|
89
|
+
setInputGeneration((generation) => generation + 1);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const addDraft = (): void => {
|
|
93
|
+
const handle = (inputRef.current?.plainText ?? "").trim().replace(/^@/, "").toLowerCase();
|
|
94
|
+
|
|
95
|
+
if (handle && !logins.includes(handle)) onChange([...logins, handle]);
|
|
96
|
+
reseed("");
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
useKeyboard((key) => {
|
|
100
|
+
if (!focused) return;
|
|
101
|
+
if (key.name !== "backspace" || (inputRef.current?.plainText ?? "").length > 0) return;
|
|
102
|
+
// backspace on an empty field pulls the last chip back in to edit; with none left, collapse
|
|
103
|
+
if (logins.length > 0) {
|
|
104
|
+
const last = logins[logins.length - 1]!;
|
|
105
|
+
|
|
106
|
+
onChange(logins.slice(0, -1));
|
|
107
|
+
reseed(last);
|
|
108
|
+
} else {
|
|
109
|
+
onEmptyBackspace?.();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<box style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center" }}>
|
|
115
|
+
{logins.map((login) => (
|
|
116
|
+
<HandleChip
|
|
117
|
+
key={login}
|
|
118
|
+
login={login}
|
|
119
|
+
onRemove={() => onChange(logins.filter((entry) => entry !== login))}
|
|
120
|
+
tokens={tokens}
|
|
121
|
+
/>
|
|
122
|
+
))}
|
|
123
|
+
<box style={{ flexDirection: "row", flexShrink: 0 }}>
|
|
124
|
+
<text fg={tokens.textDim}>{"@"}</text>
|
|
125
|
+
<textarea
|
|
126
|
+
key={inputGeneration}
|
|
127
|
+
ref={inputRef}
|
|
128
|
+
focused
|
|
129
|
+
initialValue={seed}
|
|
130
|
+
placeholder="handle"
|
|
131
|
+
keyBindings={ADD_KEY_BINDINGS}
|
|
132
|
+
onSubmit={addDraft}
|
|
133
|
+
style={{
|
|
134
|
+
height: 1,
|
|
135
|
+
width: 16,
|
|
136
|
+
backgroundColor: tokens.elevated,
|
|
137
|
+
focusedBackgroundColor: tokens.elevated,
|
|
138
|
+
textColor: tokens.text,
|
|
139
|
+
focusedTextColor: tokens.text,
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
</box>
|
|
143
|
+
</box>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { KeybindsDialog } from "./KeybindsDialog";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Overlays/KeybindsDialog" };
|
|
7
|
+
|
|
8
|
+
const SECTIONS = [
|
|
9
|
+
{
|
|
10
|
+
title: "Review",
|
|
11
|
+
entries: [
|
|
12
|
+
{ keys: "j / k", label: "move" },
|
|
13
|
+
{ keys: "v", label: "span" },
|
|
14
|
+
{ keys: "c", label: "comment" },
|
|
15
|
+
{ keys: "enter", label: "submit" },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
title: "Submit",
|
|
20
|
+
entries: [
|
|
21
|
+
{ keys: "← / →", label: "message" },
|
|
22
|
+
{ keys: "esc", label: "cancel" },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const Cheatsheet: Story = {
|
|
28
|
+
render: () => <KeybindsDialog sections={SECTIONS} />,
|
|
29
|
+
expectedColors: [DARK.accent],
|
|
30
|
+
size: { width: 70, height: 24 },
|
|
31
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import type { Theme } from "../theme";
|
|
4
|
+
import { useComponentTheme } from "./theme-context";
|
|
5
|
+
import { Dialog } from "./primitives/Dialog";
|
|
6
|
+
import type { CheatsheetSection } from "../key-bindings";
|
|
7
|
+
|
|
8
|
+
export interface KeybindsDialogProps {
|
|
9
|
+
sections: CheatsheetSection[];
|
|
10
|
+
theme?: Theme;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Rows a section takes: its title, its entries, and the blank line after it. */
|
|
14
|
+
function sectionRows(section: CheatsheetSection): number {
|
|
15
|
+
return 1 + section.entries.length + 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split the sections over `count` columns, filling each column to the
|
|
20
|
+
* balanced share of rows before starting the next, so a long cheatsheet
|
|
21
|
+
* reads top to bottom, then left to right.
|
|
22
|
+
*/
|
|
23
|
+
function columnsOf(sections: CheatsheetSection[], count: number): CheatsheetSection[][] {
|
|
24
|
+
const total = sections.reduce((sum, section) => sum + sectionRows(section), 0);
|
|
25
|
+
const share = Math.ceil(total / count);
|
|
26
|
+
const columns: CheatsheetSection[][] = [[]];
|
|
27
|
+
let filled = 0;
|
|
28
|
+
|
|
29
|
+
for (const section of sections) {
|
|
30
|
+
const column = columns[columns.length - 1]!;
|
|
31
|
+
|
|
32
|
+
if (filled > 0 && filled + sectionRows(section) > share && columns.length < count) {
|
|
33
|
+
columns.push([section]);
|
|
34
|
+
filled = sectionRows(section);
|
|
35
|
+
} else {
|
|
36
|
+
column.push(section);
|
|
37
|
+
filled += sectionRows(section);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return columns;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const COLUMN_WIDTH = 48;
|
|
45
|
+
|
|
46
|
+
export function KeybindsDialog({ sections, theme }: KeybindsDialogProps): React.ReactNode {
|
|
47
|
+
const tokens = useComponentTheme(theme);
|
|
48
|
+
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
49
|
+
const totalRows = sections.reduce((sum, section) => sum + sectionRows(section), 0) + 1;
|
|
50
|
+
// a cheatsheet taller than the terminal folds into a second column
|
|
51
|
+
const columnCount =
|
|
52
|
+
totalRows + 3 > terminalHeight - 4 && terminalWidth >= COLUMN_WIDTH * 2 + 6 ? 2 : 1;
|
|
53
|
+
const columns = columnsOf(sections, columnCount);
|
|
54
|
+
const tallest = Math.max(
|
|
55
|
+
...columns.map((column) => column.reduce((sum, section) => sum + sectionRows(section), 0)),
|
|
56
|
+
);
|
|
57
|
+
const width = Math.min(COLUMN_WIDTH * columnCount + 4, terminalWidth - 6);
|
|
58
|
+
const height = Math.min(terminalHeight - 4, tallest + 1 + 3);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Dialog
|
|
62
|
+
isOpen
|
|
63
|
+
title=" keybinds "
|
|
64
|
+
width={width}
|
|
65
|
+
height={height}
|
|
66
|
+
background={tokens.elevated}
|
|
67
|
+
theme={theme}
|
|
68
|
+
>
|
|
69
|
+
<box
|
|
70
|
+
style={{
|
|
71
|
+
flexDirection: "column",
|
|
72
|
+
flexGrow: 1,
|
|
73
|
+
paddingLeft: 1,
|
|
74
|
+
paddingRight: 1,
|
|
75
|
+
paddingTop: 1,
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
79
|
+
{columns.map((column, columnIndex) => (
|
|
80
|
+
<box key={columnIndex} style={{ flexDirection: "column", width: COLUMN_WIDTH }}>
|
|
81
|
+
{column.map((section) => (
|
|
82
|
+
<box key={section.title} style={{ flexDirection: "column" }}>
|
|
83
|
+
<text fg={tokens.accent}>{section.title}</text>
|
|
84
|
+
{section.entries.map((entry, index) => (
|
|
85
|
+
<text key={index}>
|
|
86
|
+
<span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
|
|
87
|
+
<span fg={tokens.textMuted}>{entry.label}</span>
|
|
88
|
+
</text>
|
|
89
|
+
))}
|
|
90
|
+
<text> </text>
|
|
91
|
+
</box>
|
|
92
|
+
))}
|
|
93
|
+
</box>
|
|
94
|
+
))}
|
|
95
|
+
</box>
|
|
96
|
+
<box style={{ flexDirection: "row" }}>
|
|
97
|
+
<box style={{ flexGrow: 1 }} />
|
|
98
|
+
<text fg={tokens.textDim}>[esc]</text>
|
|
99
|
+
</box>
|
|
100
|
+
</box>
|
|
101
|
+
</Dialog>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import { buildDisplay, type DisplayBlock } from "../view-plan";
|
|
4
|
+
import type { Story, StoryMeta } from "./story";
|
|
5
|
+
import { MarkdownGridBlock } from "./MarkdownGridBlock";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Surfaces/MarkdownGridBlock" };
|
|
8
|
+
|
|
9
|
+
/** The first display block of a kind, for a story that renders one grid in isolation. */
|
|
10
|
+
function firstBlock(markdown: string, kind: DisplayBlock["kind"]): DisplayBlock {
|
|
11
|
+
return buildDisplay(markdown).find((block) => block.kind === kind)!;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TABLE = firstBlock(
|
|
15
|
+
"| Name | Size |\n| :--- | ---: |\n| daemon | 12 |\n| client | 7 |",
|
|
16
|
+
"table",
|
|
17
|
+
);
|
|
18
|
+
const FRONTMATTER = firstBlock(
|
|
19
|
+
"---\ntitle: Migration Plan\nowner: platform team\nstatus: in review\n---\n\n# Plan\n",
|
|
20
|
+
"frontmatter",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const Table: Story = {
|
|
24
|
+
render: () => <MarkdownGridBlock block={TABLE} theme={DARK} contentWidth={60} />,
|
|
25
|
+
expectedColors: [DARK.text, DARK.textDim],
|
|
26
|
+
size: { width: 62, height: 6 },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Frontmatter: Story = {
|
|
30
|
+
render: () => <MarkdownGridBlock block={FRONTMATTER} theme={DARK} contentWidth={60} />,
|
|
31
|
+
expectedColors: [DARK.text, DARK.textDim],
|
|
32
|
+
size: { width: 62, height: 9 },
|
|
33
|
+
};
|
|
@@ -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
|
+
};
|