@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.80
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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A one-cell vertical scrollbar that mirrors a scrollbox's scroll state, so the bar can sit
|
|
3
|
+
* where the layout wants it (the panel's rightmost column, past the discussion dots) instead
|
|
4
|
+
* of inside the scrollbox's own viewport row. It is an overlay: the thumb appears while the
|
|
5
|
+
* surface is scrolling and hides once scrolling has been idle, so a resting surface shows none.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useRef, useState, type RefObject } from "react";
|
|
9
|
+
import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
|
|
10
|
+
import type { Theme } from "../theme";
|
|
11
|
+
import { useFrameMeasure } from "../use-frame-measure";
|
|
12
|
+
import { useComponentTheme } from "./theme-context";
|
|
13
|
+
|
|
14
|
+
const HIDE_DELAY_MS = 1500;
|
|
15
|
+
|
|
16
|
+
interface ScrollState {
|
|
17
|
+
top: number;
|
|
18
|
+
height: number;
|
|
19
|
+
viewport: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ThumbGeometry {
|
|
23
|
+
row: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function thumbGeometry(state: ScrollState, track: number): ThumbGeometry | null {
|
|
28
|
+
if (track <= 0 || state.height <= state.viewport || state.viewport <= 0) return null;
|
|
29
|
+
const height = Math.max(1, Math.round((state.viewport / state.height) * track));
|
|
30
|
+
const maxTop = state.height - state.viewport;
|
|
31
|
+
const row = Math.round((state.top / maxTop) * (track - height));
|
|
32
|
+
|
|
33
|
+
return { row: Math.min(track - height, Math.max(0, row)), height };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OverlayScrollbarProps {
|
|
37
|
+
scrollbox: RefObject<ScrollBoxRenderable | null>;
|
|
38
|
+
theme?: Theme;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function OverlayScrollbar({ scrollbox, theme }: OverlayScrollbarProps): React.ReactNode {
|
|
42
|
+
const tokens = useComponentTheme(theme);
|
|
43
|
+
const trackRef = useRef<BoxRenderable | null>(null);
|
|
44
|
+
const [visible, setVisible] = useState(false);
|
|
45
|
+
const draggingRef = useRef(false);
|
|
46
|
+
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
47
|
+
const prevTopRef = useRef<number | null>(null);
|
|
48
|
+
|
|
49
|
+
const state = useFrameMeasure<ScrollState>(
|
|
50
|
+
() => ({
|
|
51
|
+
top: scrollbox.current?.scrollTop ?? 0,
|
|
52
|
+
height: scrollbox.current?.scrollHeight ?? 0,
|
|
53
|
+
viewport: scrollbox.current?.viewport.height ?? 0,
|
|
54
|
+
}),
|
|
55
|
+
(left, right) =>
|
|
56
|
+
left.top === right.top && left.height === right.height && left.viewport === right.viewport,
|
|
57
|
+
{ top: 0, height: 0, viewport: 0 },
|
|
58
|
+
);
|
|
59
|
+
const track = useFrameMeasure(
|
|
60
|
+
() => trackRef.current?.height ?? 0,
|
|
61
|
+
(left, right) => left === right,
|
|
62
|
+
0,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const reveal = (): void => {
|
|
66
|
+
setVisible(true);
|
|
67
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
68
|
+
hideTimerRef.current = setTimeout(() => {
|
|
69
|
+
if (!draggingRef.current) setVisible(false);
|
|
70
|
+
}, HIDE_DELAY_MS);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// reveal on real scroll movement, never on first mount, then hide once idle
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (prevTopRef.current !== null && state.top !== prevTopRef.current) reveal();
|
|
76
|
+
prevTopRef.current = state.top;
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, [state.top]);
|
|
79
|
+
|
|
80
|
+
useEffect(
|
|
81
|
+
() => () => {
|
|
82
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
83
|
+
},
|
|
84
|
+
[],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const geometry = thumbGeometry(state, track);
|
|
88
|
+
const thumb = visible ? geometry : null;
|
|
89
|
+
|
|
90
|
+
const scrollToTrackRow = (screenY: number): void => {
|
|
91
|
+
const box = scrollbox.current;
|
|
92
|
+
|
|
93
|
+
if (!box || !geometry) return;
|
|
94
|
+
const row = screenY - (trackRef.current?.y ?? 0) - Math.floor(geometry.height / 2);
|
|
95
|
+
const ratio = Math.min(1, Math.max(0, row / Math.max(1, track - geometry.height)));
|
|
96
|
+
|
|
97
|
+
box.scrollTo(Math.round(ratio * (state.height - state.viewport)));
|
|
98
|
+
reveal();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<box
|
|
103
|
+
ref={trackRef}
|
|
104
|
+
style={{ width: 1, flexShrink: 0, flexDirection: "column" }}
|
|
105
|
+
onMouseDown={(event) => {
|
|
106
|
+
draggingRef.current = true;
|
|
107
|
+
scrollToTrackRow(event.y);
|
|
108
|
+
}}
|
|
109
|
+
onMouseDrag={(event) => scrollToTrackRow(event.y)}
|
|
110
|
+
onMouseUp={() => {
|
|
111
|
+
draggingRef.current = false;
|
|
112
|
+
reveal();
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{thumb ? (
|
|
116
|
+
<box
|
|
117
|
+
style={{
|
|
118
|
+
position: "absolute",
|
|
119
|
+
top: thumb.row,
|
|
120
|
+
left: 0,
|
|
121
|
+
width: 1,
|
|
122
|
+
height: thumb.height,
|
|
123
|
+
backgroundColor: tokens.textDim,
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
) : null}
|
|
127
|
+
</box>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { PaneGrid } from "./PaneGrid";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Layout/PaneGrid" };
|
|
7
|
+
|
|
8
|
+
function Sidebar(): React.ReactNode {
|
|
9
|
+
return (
|
|
10
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
11
|
+
<text fg={DARK.textDim}>Projects</text>
|
|
12
|
+
<text fg={DARK.text}> cueloop</text>
|
|
13
|
+
<box style={{ backgroundColor: DARK.elevated }}>
|
|
14
|
+
<text fg={DARK.accent}> Read Cueloop Repository</text>
|
|
15
|
+
</box>
|
|
16
|
+
<text> </text>
|
|
17
|
+
<text fg={DARK.textDim}>Threads</text>
|
|
18
|
+
<text fg={DARK.textMuted}> Welcome to cueloop</text>
|
|
19
|
+
</box>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Main(): React.ReactNode {
|
|
24
|
+
return (
|
|
25
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
26
|
+
<text fg={DARK.text}>read the repo</text>
|
|
27
|
+
<text> </text>
|
|
28
|
+
<text fg={DARK.textMuted}>Repository context loaded. main is clean.</text>
|
|
29
|
+
<text> </text>
|
|
30
|
+
<text fg={DARK.text}>Core architecture</text>
|
|
31
|
+
<text fg={DARK.textMuted}> - packages/schema is the dependency root</text>
|
|
32
|
+
<text fg={DARK.textMuted}> - packages/daemon owns session state</text>
|
|
33
|
+
</box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Inspector(): React.ReactNode {
|
|
38
|
+
return (
|
|
39
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
40
|
+
<text fg={DARK.text}>cueloop/main</text>
|
|
41
|
+
<text fg={DARK.textDim}> No changes</text>
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Header(): React.ReactNode {
|
|
47
|
+
return (
|
|
48
|
+
<box style={{ flexDirection: "row", paddingLeft: 1 }}>
|
|
49
|
+
<text fg={DARK.textMuted}>Read Cueloop Repository</text>
|
|
50
|
+
<box style={{ flexGrow: 1 }} />
|
|
51
|
+
<text fg={DARK.textDim}>markdown.test.ts</text>
|
|
52
|
+
</box>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Footer(): React.ReactNode {
|
|
57
|
+
return (
|
|
58
|
+
<box style={{ flexDirection: "row", paddingLeft: 1 }}>
|
|
59
|
+
<text fg={DARK.textDim}>cueloop / main</text>
|
|
60
|
+
<box style={{ flexGrow: 1 }} />
|
|
61
|
+
<text fg={DARK.textDim}>Fable 5</text>
|
|
62
|
+
</box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ThreePane: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<PaneGrid
|
|
69
|
+
sidebar={<Sidebar />}
|
|
70
|
+
main={<Main />}
|
|
71
|
+
mainHeader={<Header />}
|
|
72
|
+
inspector={<Inspector />}
|
|
73
|
+
footer={<Footer />}
|
|
74
|
+
/>
|
|
75
|
+
),
|
|
76
|
+
expectedColors: [DARK.border, DARK.accent],
|
|
77
|
+
size: { width: 120, height: 28 },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const SidebarAndMain: Story = {
|
|
81
|
+
render: () => <PaneGrid sidebar={<Sidebar />} main={<Main />} />,
|
|
82
|
+
expectedColors: [DARK.border],
|
|
83
|
+
size: { width: 90, height: 20 },
|
|
84
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// A generic pane grid: a row of columns, each stacking a header cell over its body, with the
|
|
2
|
+
// vertical rules run full height and every header cell's bottom rule joined into one horizontal
|
|
3
|
+
// line. Rules only - no enclosing frames. This is the layout primitive for dev surfaces (the
|
|
4
|
+
// storybook runner); the product's four-pane workbench is AppShell.
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { DARK, type Theme } from "../theme";
|
|
8
|
+
import type { BorderSides } from "@opentui/core";
|
|
9
|
+
|
|
10
|
+
export interface PaneGridProps {
|
|
11
|
+
headerHeight?: number;
|
|
12
|
+
sidebar?: React.ReactNode;
|
|
13
|
+
sidebarHeader?: React.ReactNode;
|
|
14
|
+
sidebarWidth?: number;
|
|
15
|
+
main: React.ReactNode;
|
|
16
|
+
mainHeader?: React.ReactNode;
|
|
17
|
+
inspector?: React.ReactNode;
|
|
18
|
+
inspectorHeader?: React.ReactNode;
|
|
19
|
+
inspectorWidth?: number;
|
|
20
|
+
footer?: React.ReactNode;
|
|
21
|
+
theme?: Theme;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PaneColumnProps {
|
|
25
|
+
width?: number;
|
|
26
|
+
grow?: boolean;
|
|
27
|
+
side?: BorderSides;
|
|
28
|
+
header: React.ReactNode;
|
|
29
|
+
body: React.ReactNode;
|
|
30
|
+
headerHeight: number;
|
|
31
|
+
theme: Theme;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function PaneColumn({
|
|
35
|
+
width,
|
|
36
|
+
grow,
|
|
37
|
+
side,
|
|
38
|
+
header,
|
|
39
|
+
body,
|
|
40
|
+
headerHeight,
|
|
41
|
+
theme,
|
|
42
|
+
}: PaneColumnProps): React.ReactNode {
|
|
43
|
+
return (
|
|
44
|
+
<box
|
|
45
|
+
style={{
|
|
46
|
+
flexDirection: "column",
|
|
47
|
+
...(grow ? { flexGrow: 1 } : { width }),
|
|
48
|
+
borderStyle: "single",
|
|
49
|
+
border: side ? [side] : [],
|
|
50
|
+
borderColor: theme.border,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<box
|
|
54
|
+
style={{
|
|
55
|
+
height: headerHeight,
|
|
56
|
+
borderStyle: "single",
|
|
57
|
+
border: ["bottom"],
|
|
58
|
+
borderColor: theme.border,
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{header}
|
|
62
|
+
</box>
|
|
63
|
+
<box style={{ flexGrow: 1 }}>{body}</box>
|
|
64
|
+
</box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function PaneGrid({
|
|
69
|
+
headerHeight = 3,
|
|
70
|
+
sidebar,
|
|
71
|
+
sidebarHeader,
|
|
72
|
+
sidebarWidth = 28,
|
|
73
|
+
main,
|
|
74
|
+
mainHeader,
|
|
75
|
+
inspector,
|
|
76
|
+
inspectorHeader,
|
|
77
|
+
inspectorWidth = 40,
|
|
78
|
+
footer,
|
|
79
|
+
theme,
|
|
80
|
+
}: PaneGridProps): React.ReactNode {
|
|
81
|
+
const tokens = theme ?? DARK;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<box
|
|
85
|
+
style={{
|
|
86
|
+
flexDirection: "column",
|
|
87
|
+
width: "100%",
|
|
88
|
+
height: "100%",
|
|
89
|
+
backgroundColor: tokens.background,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
93
|
+
{sidebar !== undefined ? (
|
|
94
|
+
<PaneColumn
|
|
95
|
+
width={sidebarWidth}
|
|
96
|
+
side="right"
|
|
97
|
+
header={sidebarHeader}
|
|
98
|
+
body={sidebar}
|
|
99
|
+
headerHeight={headerHeight}
|
|
100
|
+
theme={tokens}
|
|
101
|
+
/>
|
|
102
|
+
) : null}
|
|
103
|
+
<PaneColumn
|
|
104
|
+
grow
|
|
105
|
+
header={mainHeader}
|
|
106
|
+
body={main}
|
|
107
|
+
headerHeight={headerHeight}
|
|
108
|
+
theme={tokens}
|
|
109
|
+
/>
|
|
110
|
+
{inspector !== undefined ? (
|
|
111
|
+
<PaneColumn
|
|
112
|
+
width={inspectorWidth}
|
|
113
|
+
side="left"
|
|
114
|
+
header={inspectorHeader}
|
|
115
|
+
body={inspector}
|
|
116
|
+
headerHeight={headerHeight}
|
|
117
|
+
theme={tokens}
|
|
118
|
+
/>
|
|
119
|
+
) : null}
|
|
120
|
+
</box>
|
|
121
|
+
{footer !== undefined ? (
|
|
122
|
+
<box style={{ borderStyle: "single", border: ["top"], borderColor: tokens.border }}>
|
|
123
|
+
{footer}
|
|
124
|
+
</box>
|
|
125
|
+
) : null}
|
|
126
|
+
</box>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { PanelColumn, FileTab } from "./PanelColumn";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Surfaces/PanelColumn" };
|
|
7
|
+
|
|
8
|
+
export const WithFileTab: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<box style={{ width: "100%", height: "100%", flexDirection: "row" }}>
|
|
11
|
+
<PanelColumn
|
|
12
|
+
width={40}
|
|
13
|
+
border="left"
|
|
14
|
+
header={<FileTab label="Welcome" active onClose={() => {}} theme={DARK} />}
|
|
15
|
+
theme={DARK}
|
|
16
|
+
>
|
|
17
|
+
<box style={{ paddingLeft: 1, paddingTop: 1 }}>
|
|
18
|
+
<text fg={DARK.textDim}>panel body</text>
|
|
19
|
+
</box>
|
|
20
|
+
</PanelColumn>
|
|
21
|
+
</box>
|
|
22
|
+
),
|
|
23
|
+
expectedColors: [DARK.accent, DARK.border],
|
|
24
|
+
size: { width: 50, height: 8 },
|
|
25
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// A workbench panel: one full-height grid column with a file-tab header cell on
|
|
2
|
+
// top and a body below. The header row carries the tabs on the left and optional
|
|
3
|
+
// controls on the right, underlined by a thin rule in the same divider gray so the
|
|
4
|
+
// header bottoms join the side rules into one grid. The side border runs from the top.
|
|
5
|
+
|
|
6
|
+
import React, { useState } from "react";
|
|
7
|
+
import type { Theme } from "../theme";
|
|
8
|
+
import { NERD, HEADER_UNDERLINE_CHARS } from "./primitives/icons";
|
|
9
|
+
|
|
10
|
+
export interface FileTabProps {
|
|
11
|
+
label: string;
|
|
12
|
+
active?: boolean;
|
|
13
|
+
/** When set, a close control reveals on hover (a disposable tab). */
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
theme?: Theme;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A file tab: the label in the accent when active, with a hover-revealed close. */
|
|
19
|
+
export function FileTab({ label, active, onClose, theme }: FileTabProps): React.ReactNode {
|
|
20
|
+
const [hovered, setHovered] = useState(false);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<box
|
|
24
|
+
style={{ flexDirection: "row", paddingRight: 1 }}
|
|
25
|
+
onMouseOver={() => setHovered(true)}
|
|
26
|
+
onMouseOut={() => setHovered(false)}
|
|
27
|
+
>
|
|
28
|
+
<text fg={active ? theme?.accent : theme?.textDim}>{label}</text>
|
|
29
|
+
{onClose ? (
|
|
30
|
+
<box onMouseUp={onClose} style={{ paddingLeft: 1 }}>
|
|
31
|
+
<text fg={theme?.textDim}>{hovered ? NERD.close : " "}</text>
|
|
32
|
+
</box>
|
|
33
|
+
) : null}
|
|
34
|
+
</box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PanelColumnProps {
|
|
39
|
+
/** Fixed column width; omit for the flex-growing center panel. */
|
|
40
|
+
width?: number;
|
|
41
|
+
/** Draw the divider on this side (the sidebar borders right, the rest border left). */
|
|
42
|
+
border?: "left" | "right";
|
|
43
|
+
/** Tabs and controls for the header cell. */
|
|
44
|
+
header: React.ReactNode;
|
|
45
|
+
/** Right-aligned header controls (toggles, search). */
|
|
46
|
+
headerRight?: React.ReactNode;
|
|
47
|
+
onFocus?: () => void;
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
theme?: Theme;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function PanelColumn({
|
|
53
|
+
width,
|
|
54
|
+
border,
|
|
55
|
+
header,
|
|
56
|
+
headerRight,
|
|
57
|
+
onFocus,
|
|
58
|
+
children,
|
|
59
|
+
theme,
|
|
60
|
+
}: PanelColumnProps): React.ReactNode {
|
|
61
|
+
return (
|
|
62
|
+
<box
|
|
63
|
+
onMouseDown={onFocus}
|
|
64
|
+
style={{
|
|
65
|
+
width,
|
|
66
|
+
// grow panes split evenly and clip wide content (a long diff line) rather than stretching
|
|
67
|
+
flexGrow: width === undefined ? 1 : undefined,
|
|
68
|
+
flexBasis: width === undefined ? 0 : undefined,
|
|
69
|
+
minWidth: 0,
|
|
70
|
+
flexDirection: "column",
|
|
71
|
+
borderStyle: "single",
|
|
72
|
+
border: border ? [border] : [],
|
|
73
|
+
borderColor: theme?.border,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<box
|
|
77
|
+
customBorderChars={HEADER_UNDERLINE_CHARS}
|
|
78
|
+
style={{
|
|
79
|
+
flexDirection: "row",
|
|
80
|
+
height: 2,
|
|
81
|
+
paddingLeft: 1,
|
|
82
|
+
// right controls need the same breathing room as the left, or the rightmost icon renders
|
|
83
|
+
// squeezed against the terminal's last column
|
|
84
|
+
paddingRight: 1,
|
|
85
|
+
backgroundColor: theme?.panel,
|
|
86
|
+
borderStyle: "single",
|
|
87
|
+
border: ["bottom"],
|
|
88
|
+
borderColor: theme?.border,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{header}
|
|
92
|
+
<box style={{ flexGrow: 1 }} />
|
|
93
|
+
{headerRight}
|
|
94
|
+
</box>
|
|
95
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>{children}</box>
|
|
96
|
+
</box>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ProjectTreeView } from "./ProjectTreeView";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Surfaces/ProjectTreeView" };
|
|
7
|
+
|
|
8
|
+
const SAMPLE = [
|
|
9
|
+
"packages/client/src/App.tsx",
|
|
10
|
+
"packages/client/src/components/AppShell.tsx",
|
|
11
|
+
"packages/daemon/src/api.ts",
|
|
12
|
+
"README.md",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const Loaded: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<ProjectTreeView
|
|
18
|
+
loadFiles={() => Promise.resolve(SAMPLE)}
|
|
19
|
+
onSelectFile={() => {}}
|
|
20
|
+
theme={DARK}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
expectedColors: [DARK.textDim],
|
|
24
|
+
size: { width: 40, height: 16 },
|
|
25
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** The Project tree navigates by keyboard when focused: j/k move the cursor, tab folds a folder or
|
|
2
|
+
* opens a file. */
|
|
3
|
+
|
|
4
|
+
import { test, expect } from "bun:test";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
7
|
+
import { ProjectTreeView } from "./ProjectTreeView";
|
|
8
|
+
import { settle, waitForText } from "../test-support";
|
|
9
|
+
import { DARK } from "../theme";
|
|
10
|
+
|
|
11
|
+
test("tab expands a folder, then j moves down and tab opens the file", async () => {
|
|
12
|
+
const opened: string[] = [];
|
|
13
|
+
const setup = await testRender(
|
|
14
|
+
<ProjectTreeView
|
|
15
|
+
loadFiles={async () => ["src/a.ts", "README.md"]}
|
|
16
|
+
onSelectFile={(path) => opened.push(path)}
|
|
17
|
+
focused
|
|
18
|
+
theme={DARK}
|
|
19
|
+
/>,
|
|
20
|
+
{ width: 40, height: 12 },
|
|
21
|
+
);
|
|
22
|
+
await settle(setup);
|
|
23
|
+
await waitForText(setup, "src");
|
|
24
|
+
|
|
25
|
+
// cursor starts on the "src" folder; tab expands it
|
|
26
|
+
setup.mockInput.pressKey("TAB");
|
|
27
|
+
await waitForText(setup, "a.ts");
|
|
28
|
+
|
|
29
|
+
// j moves onto the file, tab opens it
|
|
30
|
+
setup.mockInput.pressKey("j");
|
|
31
|
+
await settle(setup);
|
|
32
|
+
setup.mockInput.pressKey("TAB");
|
|
33
|
+
await settle(setup);
|
|
34
|
+
|
|
35
|
+
expect(opened).toEqual(["src/a.ts"]);
|
|
36
|
+
|
|
37
|
+
setup.renderer.destroy();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("without focus the keyboard does nothing", async () => {
|
|
41
|
+
const opened: string[] = [];
|
|
42
|
+
const setup = await testRender(
|
|
43
|
+
<ProjectTreeView
|
|
44
|
+
loadFiles={async () => ["src/a.ts"]}
|
|
45
|
+
onSelectFile={(path) => opened.push(path)}
|
|
46
|
+
theme={DARK}
|
|
47
|
+
/>,
|
|
48
|
+
{ width: 40, height: 12 },
|
|
49
|
+
);
|
|
50
|
+
await settle(setup);
|
|
51
|
+
await waitForText(setup, "src");
|
|
52
|
+
|
|
53
|
+
setup.mockInput.pressKey("TAB");
|
|
54
|
+
await settle(setup);
|
|
55
|
+
expect(setup.captureCharFrame()).not.toContain("a.ts");
|
|
56
|
+
expect(opened).toEqual([]);
|
|
57
|
+
|
|
58
|
+
setup.renderer.destroy();
|
|
59
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The Project pane in tree mode: the workspace's tracked files as a collapsible directory tree.
|
|
2
|
+
// Folders start collapsed and expand on click; a file click opens it as a tab. The caller keys this
|
|
3
|
+
// on the session id so switching sessions remounts it and never shows the previous repository's paths.
|
|
4
|
+
|
|
5
|
+
import { ScrollArea } from "./ScrollArea";
|
|
6
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { useKeyboard } from "@opentui/react";
|
|
8
|
+
import type { Theme } from "../theme";
|
|
9
|
+
import { useComponentTheme } from "./theme-context";
|
|
10
|
+
import { Tree } from "./primitives/Tree";
|
|
11
|
+
import { flattenTree } from "./primitives/tree-model";
|
|
12
|
+
import { buildPathTree } from "./file-tree";
|
|
13
|
+
|
|
14
|
+
export interface ProjectTreeViewProps {
|
|
15
|
+
loadFiles: () => Promise<string[]>;
|
|
16
|
+
onSelectFile: (path: string) => void;
|
|
17
|
+
/** The pane owns the keyboard: j/k and arrows move the cursor, tab/enter open or fold. */
|
|
18
|
+
focused?: boolean;
|
|
19
|
+
theme?: Theme;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ProjectTreeView({
|
|
23
|
+
loadFiles,
|
|
24
|
+
onSelectFile,
|
|
25
|
+
focused = false,
|
|
26
|
+
theme,
|
|
27
|
+
}: ProjectTreeViewProps): React.ReactNode {
|
|
28
|
+
const tokens = useComponentTheme(theme);
|
|
29
|
+
const [paths, setPaths] = useState<readonly string[] | null>(null);
|
|
30
|
+
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set());
|
|
31
|
+
const [cursor, setCursor] = useState(0);
|
|
32
|
+
const nodes = useMemo(() => (paths ? buildPathTree(paths) : []), [paths]);
|
|
33
|
+
const rows = useMemo(() => flattenTree(nodes, { expandedIds }), [nodes, expandedIds]);
|
|
34
|
+
const cursorIndex = Math.min(cursor, Math.max(0, rows.length - 1));
|
|
35
|
+
const loadRef = useRef(loadFiles);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
loadRef.current = loadFiles;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const toggle = (id: string): void =>
|
|
41
|
+
setExpandedIds((current) => {
|
|
42
|
+
const next = new Set(current);
|
|
43
|
+
|
|
44
|
+
if (next.has(id)) next.delete(id);
|
|
45
|
+
else next.add(id);
|
|
46
|
+
|
|
47
|
+
return next;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
useKeyboard((key) => {
|
|
51
|
+
if (!focused || rows.length === 0) return;
|
|
52
|
+
if (key.name === "j" || key.name === "down")
|
|
53
|
+
return setCursor(Math.min(cursorIndex + 1, rows.length - 1));
|
|
54
|
+
if (key.name === "k" || key.name === "up") return setCursor(Math.max(cursorIndex - 1, 0));
|
|
55
|
+
const row = rows[cursorIndex];
|
|
56
|
+
if (!row) return;
|
|
57
|
+
if (key.name === "tab" || key.name === "return" || key.name === "enter")
|
|
58
|
+
return row.isFolder ? toggle(row.id) : onSelectFile(row.id);
|
|
59
|
+
if (key.name === "l" && row.isFolder && !expandedIds.has(row.id)) return toggle(row.id);
|
|
60
|
+
if (key.name === "h" && row.isFolder && expandedIds.has(row.id)) return toggle(row.id);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
let alive = true;
|
|
65
|
+
void loadRef.current().then(
|
|
66
|
+
(files) => {
|
|
67
|
+
if (alive) setPaths(files);
|
|
68
|
+
},
|
|
69
|
+
() => {
|
|
70
|
+
if (alive) setPaths([]);
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
alive = false;
|
|
76
|
+
};
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
if (paths === null) {
|
|
80
|
+
return (
|
|
81
|
+
<box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
|
|
82
|
+
<text fg={tokens.textDim}>loading...</text>
|
|
83
|
+
</box>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (paths.length === 0) {
|
|
87
|
+
return (
|
|
88
|
+
<box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
|
|
89
|
+
<text fg={tokens.textDim}>empty</text>
|
|
90
|
+
</box>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<ScrollArea>
|
|
96
|
+
<Tree
|
|
97
|
+
nodes={nodes}
|
|
98
|
+
expandedIds={expandedIds}
|
|
99
|
+
selectedId={focused ? rows[cursorIndex]?.id : undefined}
|
|
100
|
+
onSelect={(id) => {
|
|
101
|
+
const index = rows.findIndex((row) => row.id === id);
|
|
102
|
+
|
|
103
|
+
if (index >= 0) setCursor(index);
|
|
104
|
+
onSelectFile(id);
|
|
105
|
+
}}
|
|
106
|
+
onToggle={toggle}
|
|
107
|
+
theme={theme}
|
|
108
|
+
/>
|
|
109
|
+
</ScrollArea>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { PromptDialog } from "./PromptDialog";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Overlays/PromptDialog" };
|
|
7
|
+
|
|
8
|
+
export const RenameAuthor: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<PromptDialog
|
|
11
|
+
isOpen
|
|
12
|
+
title=" Rename author "
|
|
13
|
+
label="Display name for this collaborator:"
|
|
14
|
+
value="Robin"
|
|
15
|
+
placeholder="their name"
|
|
16
|
+
onInput={() => {}}
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
expectedColors: [DARK.elevated, DARK.accent],
|
|
20
|
+
size: { width: 64, height: 16 },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const NameYourself: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<PromptDialog
|
|
26
|
+
isOpen
|
|
27
|
+
title=" Welcome "
|
|
28
|
+
label="Your name (optional) - it attributes the notes you leave:"
|
|
29
|
+
value=""
|
|
30
|
+
placeholder="your name"
|
|
31
|
+
onInput={() => {}}
|
|
32
|
+
/>
|
|
33
|
+
),
|
|
34
|
+
expectedColors: [DARK.elevated],
|
|
35
|
+
size: { width: 64, height: 16 },
|
|
36
|
+
};
|