@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90
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 +42 -34
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -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 +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -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 +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1402 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +774 -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 +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -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.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -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 +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -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 +906 -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/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -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 +75 -0
- package/src/components/ProjectTreeView.tsx +125 -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 +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -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 +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -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 +59 -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 +579 -34
- package/src/config.ts +533 -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 +28 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +67 -0
- package/src/launch-layout.ts +47 -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 +81 -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 +85 -12
- 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 +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -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 +2050 -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 +153 -0
- package/src/use-annotation-surface.tsx +975 -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-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -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 +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- 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 -364
|
@@ -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
|
+
};
|
|
@@ -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,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { MenuBar } from "./MenuBar";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/MenuBar" };
|
|
7
|
+
|
|
8
|
+
const callbacks = { onSettings: () => {}, onKeybinds: () => {} };
|
|
9
|
+
|
|
10
|
+
function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
11
|
+
return (
|
|
12
|
+
<box
|
|
13
|
+
style={{ flexDirection: "column", width: "100%", height: "100%", justifyContent: "flex-end" }}
|
|
14
|
+
>
|
|
15
|
+
{children}
|
|
16
|
+
</box>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Open: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<BottomFrame>
|
|
23
|
+
<MenuBar open {...callbacks} />
|
|
24
|
+
</BottomFrame>
|
|
25
|
+
),
|
|
26
|
+
expectedColors: [DARK.text],
|
|
27
|
+
size: { width: 60, height: 8 },
|
|
28
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Theme } from "../theme";
|
|
3
|
+
import { useComponentTheme } from "./theme-context";
|
|
4
|
+
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
5
|
+
|
|
6
|
+
export interface MenuBarProps {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onSettings: () => void;
|
|
9
|
+
onKeybinds: () => void;
|
|
10
|
+
theme?: Theme;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DROPDOWN_OPTION_ROWS = 2;
|
|
14
|
+
|
|
15
|
+
/** The settings gear's drop-down: Settings and Keybinds, below the top-left gear. */
|
|
16
|
+
export function MenuBar({ open, onSettings, onKeybinds, theme }: MenuBarProps): React.ReactNode {
|
|
17
|
+
const tokens = useComponentTheme(theme);
|
|
18
|
+
const dropdownHeight = DROPDOWN_OPTION_ROWS + 2;
|
|
19
|
+
|
|
20
|
+
if (!open) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<box
|
|
24
|
+
style={{
|
|
25
|
+
position: "absolute",
|
|
26
|
+
left: 1,
|
|
27
|
+
top: 2,
|
|
28
|
+
width: 16,
|
|
29
|
+
height: dropdownHeight,
|
|
30
|
+
border: true,
|
|
31
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
32
|
+
borderColor: tokens.text,
|
|
33
|
+
flexDirection: "column",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<box onMouseUp={onSettings}>
|
|
37
|
+
<text fg={tokens.text}> Settings</text>
|
|
38
|
+
</box>
|
|
39
|
+
<box onMouseUp={onKeybinds}>
|
|
40
|
+
<text fg={tokens.text}> Keybinds</text>
|
|
41
|
+
</box>
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import { DARK } from "../theme";
|
|
4
|
+
import { OverlayScrollbar } from "./OverlayScrollbar";
|
|
5
|
+
import type { Story, StoryMeta } from "./story";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Chrome/OverlayScrollbar" };
|
|
8
|
+
|
|
9
|
+
function TallSurface(): React.ReactNode {
|
|
10
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<box style={{ width: 24, height: 8, flexDirection: "row", backgroundColor: DARK.background }}>
|
|
14
|
+
<scrollbox
|
|
15
|
+
ref={scrollRef}
|
|
16
|
+
style={{ flexGrow: 1 }}
|
|
17
|
+
focused={false}
|
|
18
|
+
verticalScrollbarOptions={{ visible: false }}
|
|
19
|
+
>
|
|
20
|
+
{Array.from({ length: 24 }, (_unused, index) => (
|
|
21
|
+
<text key={index} fg={DARK.textMuted}>{`row ${index + 1}`}</text>
|
|
22
|
+
))}
|
|
23
|
+
</scrollbox>
|
|
24
|
+
<OverlayScrollbar scrollbox={scrollRef} />
|
|
25
|
+
</box>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// At rest the overlay bar is hidden; the thumb only appears while the surface is scrolling.
|
|
30
|
+
export const AtRest: Story = {
|
|
31
|
+
render: () => <TallSurface />,
|
|
32
|
+
size: { width: 24, height: 8 },
|
|
33
|
+
};
|