@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71
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 +39 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +76 -50
- package/src/App.diff.test.tsx +156 -42
- package/src/App.export.test.tsx +58 -40
- package/src/App.plan-review.test.tsx +363 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +128 -67
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.submit-confirm.test.tsx +191 -0
- package/src/App.test.tsx +310 -114
- package/src/App.toast.test.tsx +89 -0
- package/src/App.tsx +1043 -798
- package/src/App.walk.test.tsx +298 -0
- package/src/App.workbench.test.tsx +495 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +61 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +432 -0
- package/src/app-view-model.ts +159 -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 +126 -0
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +290 -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 +39 -0
- package/src/components/ChangesColumn.tsx +171 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +128 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -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 +859 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +368 -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/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -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 +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +83 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +87 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -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 +195 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +219 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +158 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +330 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +71 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -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 +152 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +140 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -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/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -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 +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -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/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 +96 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -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 +54 -0
- package/src/components/quick-actions-editor.test.tsx +49 -0
- package/src/components/quick-actions-editor.tsx +150 -0
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +184 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +142 -0
- package/src/components/syntax-highlight.ts +110 -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/truncate-text.ts +7 -0
- package/src/config.test.ts +461 -31
- package/src/config.ts +389 -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 +165 -0
- package/src/diff-intraline.ts +146 -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/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +11 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +698 -0
- package/src/intent-dispatch.ts +700 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +465 -0
- package/src/keymap.test.ts +611 -0
- package/src/keymap.ts +410 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/mark-runs.ts +120 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +307 -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 +30 -9
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +20 -9
- package/src/share.ts +215 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +68 -0
- package/src/slash-palette.test.ts +54 -0
- package/src/slash-palette.ts +104 -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 +355 -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 +170 -0
- package/src/thread-chords.ts +169 -0
- package/src/thread-controller.curate.test.ts +438 -0
- package/src/thread-controller.live-diff.test.ts +199 -0
- package/src/thread-controller.share.test.ts +472 -0
- package/src/thread-controller.tree.test.ts +255 -0
- package/src/thread-controller.ts +1509 -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 +851 -0
- package/src/use-changes-workbench.ts +153 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +274 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +66 -9
- package/src/view-diff.ts +231 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +570 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- 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,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inline comment pieces every annotated surface renders: the composer (a
|
|
3
|
+
* textarea in a card row), the "/" palette and inline completion hint below it,
|
|
4
|
+
* the edge-segmented discussion card, and one comment row. The plan thread view
|
|
5
|
+
* and the diff sheet both draw their comments with these, so a discussion looks
|
|
6
|
+
* and behaves the same on prose and on code.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
10
|
+
import type { BoxRenderable, KeyBinding, TextareaRenderable } from "@opentui/core";
|
|
11
|
+
import type { Annotation } from "@cueloop/schema";
|
|
12
|
+
import type { Theme } from "../theme";
|
|
13
|
+
import { lighten } from "../annotation-palette";
|
|
14
|
+
import { useFrameMeasure } from "../use-frame-measure";
|
|
15
|
+
import type { InlineSlash, SlashItem } from "../slash-palette";
|
|
16
|
+
|
|
17
|
+
/* -------------------------------------------------------------- composer */
|
|
18
|
+
|
|
19
|
+
// cmd+enter sends (super under the kitty protocol, meta where cmd arrives
|
|
20
|
+
// ESC-prefixed, ctrl as the fallback where the terminal itself consumes
|
|
21
|
+
// cmd+enter); plain enter breaks the line
|
|
22
|
+
const COMPOSE_KEY_BINDINGS: KeyBinding[] = [
|
|
23
|
+
{ name: "return", super: true, action: "submit" },
|
|
24
|
+
{ name: "return", meta: true, action: "submit" },
|
|
25
|
+
{ name: "return", ctrl: true, action: "submit" },
|
|
26
|
+
{ name: "return", action: "newline" },
|
|
27
|
+
{ name: "return", shift: true, action: "newline" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function composeRowCount(text: string, contentWidth: number): number {
|
|
31
|
+
const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
|
|
32
|
+
let visualRowCount = 0;
|
|
33
|
+
|
|
34
|
+
for (const line of text.split("\n")) {
|
|
35
|
+
visualRowCount += Math.max(1, Math.ceil(line.length / usableWidth));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// no cap: the card is the discussion, and folding is its containment
|
|
39
|
+
return Math.max(1, visualRowCount);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Composer({
|
|
43
|
+
seed,
|
|
44
|
+
glyph,
|
|
45
|
+
tokens,
|
|
46
|
+
onSave,
|
|
47
|
+
onReady,
|
|
48
|
+
onInput,
|
|
49
|
+
}: {
|
|
50
|
+
seed: string;
|
|
51
|
+
glyph: string;
|
|
52
|
+
tokens: Theme;
|
|
53
|
+
onSave: (body: string) => void;
|
|
54
|
+
onReady: () => void;
|
|
55
|
+
onInput: (text: string) => void;
|
|
56
|
+
}): React.ReactNode {
|
|
57
|
+
const editorRef = useRef<TextareaRenderable | null>(null);
|
|
58
|
+
const [rows, setRows] = useState(1);
|
|
59
|
+
|
|
60
|
+
// once per mount (the composer is keyed by its seed): later re-renders
|
|
61
|
+
// must NOT reset the caret, or input lands before a just-typed newline
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const editor = editorRef.current;
|
|
64
|
+
|
|
65
|
+
if (!editor) return;
|
|
66
|
+
editor.cursorOffset = seed.length;
|
|
67
|
+
setRows(composeRowCount(editor.plainText, editor.width));
|
|
68
|
+
onReady();
|
|
69
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<box style={{ flexDirection: "row" }}>
|
|
74
|
+
<text selectable={false} fg={tokens.text} style={{ flexShrink: 0 }}>{`${glyph} `}</text>
|
|
75
|
+
<textarea
|
|
76
|
+
ref={editorRef}
|
|
77
|
+
focused
|
|
78
|
+
initialValue={seed}
|
|
79
|
+
cursorStyle={{ style: "block", blinking: true }}
|
|
80
|
+
keyBindings={COMPOSE_KEY_BINDINGS}
|
|
81
|
+
onSubmit={() => onSave(editorRef.current?.plainText ?? "")}
|
|
82
|
+
onContentChange={() => {
|
|
83
|
+
const editor = editorRef.current;
|
|
84
|
+
|
|
85
|
+
if (!editor) return;
|
|
86
|
+
setRows(composeRowCount(editor.plainText, editor.width));
|
|
87
|
+
onInput(editor.plainText);
|
|
88
|
+
}}
|
|
89
|
+
style={{
|
|
90
|
+
height: rows,
|
|
91
|
+
flexGrow: 1,
|
|
92
|
+
backgroundColor: "transparent",
|
|
93
|
+
focusedBackgroundColor: "transparent",
|
|
94
|
+
textColor: tokens.text,
|
|
95
|
+
focusedTextColor: tokens.text,
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
</box>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ---------------------------------------------------------- slash palette */
|
|
103
|
+
|
|
104
|
+
function SlashList({
|
|
105
|
+
items,
|
|
106
|
+
selected,
|
|
107
|
+
tokens,
|
|
108
|
+
}: {
|
|
109
|
+
items: SlashItem[];
|
|
110
|
+
selected: number;
|
|
111
|
+
tokens: Theme;
|
|
112
|
+
}): React.ReactNode {
|
|
113
|
+
const WINDOW = 5;
|
|
114
|
+
const start = Math.max(0, Math.min(selected - 2, items.length - WINDOW));
|
|
115
|
+
const visible = items.slice(start, start + WINDOW);
|
|
116
|
+
|
|
117
|
+
if (items.length === 0) return <text fg={tokens.textDim}>no matching actions</text>;
|
|
118
|
+
|
|
119
|
+
const nameColumn = Math.min(36, Math.max(16, ...visible.map((item) => item.name.length + 5)));
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<box style={{ flexDirection: "column" }}>
|
|
123
|
+
{visible.map((item, offset) => {
|
|
124
|
+
const index = start + offset;
|
|
125
|
+
const isSelected = index === selected;
|
|
126
|
+
const name = `${isSelected ? "→ " : " "}/${item.name}`.padEnd(nameColumn);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<text key={item.name} style={{ flexShrink: 0, wrapMode: "none" }}>
|
|
130
|
+
<span fg={isSelected ? tokens.accent : tokens.textMuted}>{name}</span>
|
|
131
|
+
<span fg={tokens.textDim}>{item.description}</span>
|
|
132
|
+
</text>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
{items.length > WINDOW ? (
|
|
136
|
+
<text fg={tokens.textDim}>{`(${selected + 1}/${items.length})`}</text>
|
|
137
|
+
) : null}
|
|
138
|
+
</box>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Below the composer: the palette list for a leading "/", else the inline tab-hint, else nothing. */
|
|
143
|
+
export function ComposerPalette({
|
|
144
|
+
slashActive,
|
|
145
|
+
slashItems,
|
|
146
|
+
slashIndex,
|
|
147
|
+
inline,
|
|
148
|
+
tokens,
|
|
149
|
+
}: {
|
|
150
|
+
slashActive: boolean;
|
|
151
|
+
slashItems: SlashItem[];
|
|
152
|
+
slashIndex: number;
|
|
153
|
+
inline: InlineSlash | null;
|
|
154
|
+
tokens: Theme;
|
|
155
|
+
}): React.ReactNode {
|
|
156
|
+
if (slashActive) {
|
|
157
|
+
return (
|
|
158
|
+
<box style={{ flexDirection: "column", marginLeft: 3 }}>
|
|
159
|
+
<SlashList
|
|
160
|
+
items={slashItems}
|
|
161
|
+
selected={Math.min(slashIndex, Math.max(0, slashItems.length - 1))}
|
|
162
|
+
tokens={tokens}
|
|
163
|
+
/>
|
|
164
|
+
</box>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (inline === null) return null;
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<box style={{ flexDirection: "row", marginLeft: 3 }}>
|
|
171
|
+
<text>
|
|
172
|
+
<span fg={tokens.textDim}>{"⇥ "}</span>
|
|
173
|
+
<span fg={tokens.accent}>{`/${inline.suggestion.name}`}</span>
|
|
174
|
+
<span fg={tokens.textDim}>{` ${inline.suggestion.description}`}</span>
|
|
175
|
+
</text>
|
|
176
|
+
</box>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ------------------------------------------------------------ discussion card */
|
|
181
|
+
|
|
182
|
+
/** One card row group with its own accent-edge color, one per voice. */
|
|
183
|
+
export interface EdgeSegment {
|
|
184
|
+
color: string;
|
|
185
|
+
node: React.ReactNode;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The comment-card frame. The accent edge is drawn by hand so that (a) its
|
|
190
|
+
* corner cells are half-height glyphs ("╷"/"╵") terminating flush with the
|
|
191
|
+
* hairline rows, and (b) each voice's rows carry their own edge color with
|
|
192
|
+
* zero gap. Focus lightens every segment; the hairlines never change.
|
|
193
|
+
*/
|
|
194
|
+
export function DiscussionCard({
|
|
195
|
+
segments,
|
|
196
|
+
tokens,
|
|
197
|
+
focused = false,
|
|
198
|
+
onFocus,
|
|
199
|
+
}: {
|
|
200
|
+
segments: EdgeSegment[];
|
|
201
|
+
tokens: Theme;
|
|
202
|
+
focused?: boolean;
|
|
203
|
+
onFocus?: () => void;
|
|
204
|
+
}): React.ReactNode {
|
|
205
|
+
const segmentRefs = useRef<Array<BoxRenderable | null>>([]);
|
|
206
|
+
const noHeights: number[] = [];
|
|
207
|
+
const heights = useFrameMeasure(
|
|
208
|
+
() => segmentRefs.current.map((renderable) => renderable?.height ?? 0),
|
|
209
|
+
(left, right) => left.join() === right.join(),
|
|
210
|
+
noHeights,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const edgeColor = (color: string): string => (focused ? lighten(color) : color);
|
|
214
|
+
const glyphs = focused
|
|
215
|
+
? { top: "╻", middle: "┃", bottom: "╹" }
|
|
216
|
+
: { top: "╷", middle: "│", bottom: "╵" };
|
|
217
|
+
const edgeRows: Array<{ glyph: string; color: string }> = [
|
|
218
|
+
{
|
|
219
|
+
glyph: glyphs.top,
|
|
220
|
+
color: edgeColor(segments[0]?.color ?? tokens.border),
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
for (let index = 0; index < segments.length; index++) {
|
|
225
|
+
const segmentRows = Math.max(1, heights[index] ?? 1);
|
|
226
|
+
|
|
227
|
+
for (let row = 0; row < segmentRows; row++) {
|
|
228
|
+
edgeRows.push({
|
|
229
|
+
glyph: glyphs.middle,
|
|
230
|
+
color: edgeColor(segments[index]!.color),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
edgeRows.push({
|
|
235
|
+
glyph: glyphs.bottom,
|
|
236
|
+
color: edgeColor(segments[segments.length - 1]?.color ?? tokens.border),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<box style={{ flexDirection: "row", marginTop: 1, marginLeft: 2 }}>
|
|
241
|
+
<text selectable={false} style={{ flexShrink: 0, width: 1 }}>
|
|
242
|
+
{edgeRows.map((row, index) => (
|
|
243
|
+
<span key={index} fg={row.color}>
|
|
244
|
+
{index < edgeRows.length - 1 ? `${row.glyph}\n` : row.glyph}
|
|
245
|
+
</span>
|
|
246
|
+
))}
|
|
247
|
+
</text>
|
|
248
|
+
<box
|
|
249
|
+
onMouseDown={onFocus}
|
|
250
|
+
style={{
|
|
251
|
+
flexGrow: 1,
|
|
252
|
+
flexDirection: "column",
|
|
253
|
+
border: ["top", "right", "bottom"],
|
|
254
|
+
borderStyle: "single",
|
|
255
|
+
borderColor: tokens.border,
|
|
256
|
+
backgroundColor: "transparent",
|
|
257
|
+
paddingLeft: 1,
|
|
258
|
+
paddingRight: 1,
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
{segments.map((segment, index) => (
|
|
262
|
+
<box
|
|
263
|
+
key={index}
|
|
264
|
+
ref={(renderable: BoxRenderable | null) => {
|
|
265
|
+
segmentRefs.current[index] = renderable;
|
|
266
|
+
}}
|
|
267
|
+
style={{ flexDirection: "column" }}
|
|
268
|
+
>
|
|
269
|
+
{segment.node}
|
|
270
|
+
</box>
|
|
271
|
+
))}
|
|
272
|
+
</box>
|
|
273
|
+
</box>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function CommentRow({
|
|
278
|
+
annotation,
|
|
279
|
+
tokens,
|
|
280
|
+
}: {
|
|
281
|
+
annotation: Annotation;
|
|
282
|
+
tokens: Theme;
|
|
283
|
+
}): React.ReactNode {
|
|
284
|
+
// own comments (no author) wear the filled dot, collaborators the outline
|
|
285
|
+
const own = annotation.author === undefined;
|
|
286
|
+
const glyph = own ? "●" : "○";
|
|
287
|
+
const glyphColor = own ? tokens.text : tokens.textMuted;
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<box style={{ flexDirection: "column" }}>
|
|
291
|
+
{annotation.body.split("\n").map((line, lineIndex) => (
|
|
292
|
+
<box key={lineIndex} style={{ flexDirection: "row" }}>
|
|
293
|
+
<text selectable={false} fg={glyphColor} style={{ flexShrink: 0 }}>
|
|
294
|
+
{lineIndex === 0 ? `${glyph} ` : " "}
|
|
295
|
+
</text>
|
|
296
|
+
<text fg={tokens.text} style={{ wrapMode: "word", flexGrow: 1, flexShrink: 1 }}>
|
|
297
|
+
{line}
|
|
298
|
+
</text>
|
|
299
|
+
</box>
|
|
300
|
+
))}
|
|
301
|
+
</box>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { AppHeader } from "./AppHeader";
|
|
5
|
+
import { Button } from "./primitives/Button";
|
|
6
|
+
import { Toolbar } from "./primitives/Toolbar";
|
|
7
|
+
|
|
8
|
+
export const meta: StoryMeta = { title: "Chrome/AppHeader" };
|
|
9
|
+
|
|
10
|
+
export const NoThread: Story = {
|
|
11
|
+
render: () => (
|
|
12
|
+
<AppHeader
|
|
13
|
+
onOpenMenu={() => {}}
|
|
14
|
+
sidebarOpen
|
|
15
|
+
onToggleSidebar={() => {}}
|
|
16
|
+
title=""
|
|
17
|
+
changesOpen={false}
|
|
18
|
+
onToggleChanges={() => {}}
|
|
19
|
+
theme={DARK}
|
|
20
|
+
/>
|
|
21
|
+
),
|
|
22
|
+
expectedColors: [DARK.accent],
|
|
23
|
+
size: { width: 80, height: 3 },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const ThreadWithActions: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<AppHeader
|
|
29
|
+
onOpenMenu={() => {}}
|
|
30
|
+
sidebarOpen
|
|
31
|
+
onToggleSidebar={() => {}}
|
|
32
|
+
title="Review the accent change"
|
|
33
|
+
editShare={
|
|
34
|
+
<Toolbar>
|
|
35
|
+
<Button onPress={() => {}} theme={DARK}>
|
|
36
|
+
{" Edit "}
|
|
37
|
+
</Button>
|
|
38
|
+
<Button onPress={() => {}} theme={DARK}>
|
|
39
|
+
{" Share "}
|
|
40
|
+
</Button>
|
|
41
|
+
</Toolbar>
|
|
42
|
+
}
|
|
43
|
+
changesOpen
|
|
44
|
+
onToggleChanges={() => {}}
|
|
45
|
+
theme={DARK}
|
|
46
|
+
/>
|
|
47
|
+
),
|
|
48
|
+
expectedColors: [DARK.accent],
|
|
49
|
+
size: { width: 80, height: 3 },
|
|
50
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The one workbench header, split into column-aligned segments so it reads the
|
|
2
|
+
// same for every primitive: gear + left toggle over the Threads sidebar, the
|
|
3
|
+
// current thread's title (mirroring the sidebar selection) plus the owner's
|
|
4
|
+
// Edit/Share over the thread column, and a Changes label + right toggle over the
|
|
5
|
+
// right column. Collapsing a column keeps its toggle reachable at the edge.
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import type { Theme } from "../theme";
|
|
9
|
+
import { IconButton } from "./primitives/IconButton";
|
|
10
|
+
import { NERD } from "./primitives/icons";
|
|
11
|
+
|
|
12
|
+
export interface AppHeaderProps {
|
|
13
|
+
onOpenMenu: () => void;
|
|
14
|
+
sidebarOpen: boolean;
|
|
15
|
+
sidebarWidth?: number;
|
|
16
|
+
onToggleSidebar: () => void;
|
|
17
|
+
/** The center segment title; mirrors the selected sidebar thread, blank when none. */
|
|
18
|
+
title: string;
|
|
19
|
+
/** Owner actions rendered at the right edge of the thread segment (Edit/Share). */
|
|
20
|
+
editShare?: React.ReactNode;
|
|
21
|
+
changesOpen: boolean;
|
|
22
|
+
changesWidth?: number;
|
|
23
|
+
onToggleChanges: () => void;
|
|
24
|
+
theme?: Theme;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function AppHeader({
|
|
28
|
+
onOpenMenu,
|
|
29
|
+
sidebarOpen,
|
|
30
|
+
sidebarWidth = 30,
|
|
31
|
+
onToggleSidebar,
|
|
32
|
+
title,
|
|
33
|
+
editShare,
|
|
34
|
+
changesOpen,
|
|
35
|
+
changesWidth = 32,
|
|
36
|
+
onToggleChanges,
|
|
37
|
+
theme,
|
|
38
|
+
}: AppHeaderProps): React.ReactNode {
|
|
39
|
+
return (
|
|
40
|
+
<box style={{ flexDirection: "row", height: 2, paddingTop: 1, backgroundColor: theme?.panel }}>
|
|
41
|
+
{/* over the Threads sidebar: settings gear + the left toggle, product mark */}
|
|
42
|
+
<box
|
|
43
|
+
style={{
|
|
44
|
+
flexDirection: "row",
|
|
45
|
+
width: sidebarOpen ? sidebarWidth : undefined,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<box onMouseUp={onOpenMenu} style={{ paddingRight: 2 }}>
|
|
49
|
+
<text fg={theme?.textMuted}>{NERD.settings}</text>
|
|
50
|
+
</box>
|
|
51
|
+
<IconButton
|
|
52
|
+
glyph={sidebarOpen ? NERD.sidebarLeft : NERD.sidebarLeftOff}
|
|
53
|
+
onPress={onToggleSidebar}
|
|
54
|
+
marginRight={2}
|
|
55
|
+
theme={theme}
|
|
56
|
+
/>
|
|
57
|
+
<text fg={theme?.accent}>cueloop</text>
|
|
58
|
+
</box>
|
|
59
|
+
{/* over the thread column: the title, then the owner's Edit/Share at its right edge */}
|
|
60
|
+
<box style={{ flexDirection: "row", flexGrow: 1, paddingLeft: 1, paddingRight: 1 }}>
|
|
61
|
+
<text fg={theme?.textDim}>{title}</text>
|
|
62
|
+
<box style={{ flexGrow: 1 }} />
|
|
63
|
+
{editShare}
|
|
64
|
+
</box>
|
|
65
|
+
{/* over the right column: the Changes label when open, the right toggle always at the edge */}
|
|
66
|
+
<box
|
|
67
|
+
style={{
|
|
68
|
+
flexDirection: "row",
|
|
69
|
+
paddingRight: 1,
|
|
70
|
+
width: changesOpen ? changesWidth : undefined,
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
{changesOpen ? <text fg={theme?.textDim}>Changes</text> : null}
|
|
74
|
+
<box style={{ flexGrow: 1 }} />
|
|
75
|
+
<IconButton
|
|
76
|
+
glyph={changesOpen ? NERD.sidebarRight : NERD.sidebarRightOff}
|
|
77
|
+
onPress={onToggleChanges}
|
|
78
|
+
theme={theme}
|
|
79
|
+
/>
|
|
80
|
+
</box>
|
|
81
|
+
</box>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { AppShell } from "./AppShell";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Layout/AppShell" };
|
|
7
|
+
|
|
8
|
+
function Threads(): React.ReactNode {
|
|
9
|
+
return (
|
|
10
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
11
|
+
<text fg={DARK.textDim}>Pinned</text>
|
|
12
|
+
<text fg={DARK.accent}> Read Cueloop Repository</text>
|
|
13
|
+
<text> </text>
|
|
14
|
+
<text fg={DARK.textDim}>Threads</text>
|
|
15
|
+
<text fg={DARK.textMuted}> A standalone thought</text>
|
|
16
|
+
</box>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Thread(): React.ReactNode {
|
|
21
|
+
return (
|
|
22
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
23
|
+
<text fg={DARK.text}>the conversation renders here</text>
|
|
24
|
+
</box>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Changes(): React.ReactNode {
|
|
29
|
+
return (
|
|
30
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
31
|
+
<text fg={DARK.textMuted}>the editor grid renders here</text>
|
|
32
|
+
</box>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Project(): React.ReactNode {
|
|
37
|
+
return (
|
|
38
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
39
|
+
<text fg={DARK.textDim}>No changes</text>
|
|
40
|
+
</box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const noop = (): void => {};
|
|
45
|
+
|
|
46
|
+
export const FourPane: Story = {
|
|
47
|
+
render: () => (
|
|
48
|
+
<AppShell
|
|
49
|
+
sidebarOpen
|
|
50
|
+
onToggleSidebar={noop}
|
|
51
|
+
onOpenMenu={noop}
|
|
52
|
+
threadsPanel={<Threads />}
|
|
53
|
+
threadTitle="Read Cueloop Repository"
|
|
54
|
+
threadPanel={<Thread />}
|
|
55
|
+
changesOpen
|
|
56
|
+
projectOpen
|
|
57
|
+
onToggleChanges={noop}
|
|
58
|
+
onToggleProject={noop}
|
|
59
|
+
onToggleRight={noop}
|
|
60
|
+
projectMode="changes"
|
|
61
|
+
changesPanel={<Changes />}
|
|
62
|
+
projectPanel={<Project />}
|
|
63
|
+
/>
|
|
64
|
+
),
|
|
65
|
+
expectedColors: [DARK.border, DARK.accent],
|
|
66
|
+
size: { width: 130, height: 28 },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const CollapsedRightRail: Story = {
|
|
70
|
+
render: () => (
|
|
71
|
+
<AppShell
|
|
72
|
+
sidebarOpen
|
|
73
|
+
onToggleSidebar={noop}
|
|
74
|
+
onOpenMenu={noop}
|
|
75
|
+
threadsPanel={<Threads />}
|
|
76
|
+
threadTitle="Read Cueloop Repository"
|
|
77
|
+
threadPanel={<Thread />}
|
|
78
|
+
changesOpen={false}
|
|
79
|
+
projectOpen={false}
|
|
80
|
+
onToggleChanges={noop}
|
|
81
|
+
onToggleProject={noop}
|
|
82
|
+
onToggleRight={noop}
|
|
83
|
+
projectMode="tree"
|
|
84
|
+
projectPanel={<Project />}
|
|
85
|
+
/>
|
|
86
|
+
),
|
|
87
|
+
expectedColors: [DARK.border],
|
|
88
|
+
size: { width: 100, height: 22 },
|
|
89
|
+
};
|