@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,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prototype review surface: renders the HTML screenshot into a reserved cell
|
|
3
|
+
* region via the kitty graphics protocol, and turns a click into a DOM-element
|
|
4
|
+
* selection that opens the shared compose card - the same edit primitive plan
|
|
5
|
+
* and diff use - so a note anchors to an element rather than a text span. The
|
|
6
|
+
* image is painted directly (not through
|
|
7
|
+
* OpenTUI's ImageRenderable, which does not display in every terminal): the
|
|
8
|
+
* region's cells are never repainted, and the picture is re-placed after each
|
|
9
|
+
* frame so it survives OpenTUI's own draws.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
13
|
+
import type { BoxRenderable } from "@opentui/core";
|
|
14
|
+
import * as v from "valibot";
|
|
15
|
+
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
16
|
+
import type { Theme } from "../theme";
|
|
17
|
+
import { useComponentTheme } from "./theme-context";
|
|
18
|
+
import { Composer, DiscussionCard } from "./AnnotationCards";
|
|
19
|
+
import { annotationPaletteFor } from "../annotation-palette";
|
|
20
|
+
import {
|
|
21
|
+
cssBoxToCell,
|
|
22
|
+
imageCellToCss,
|
|
23
|
+
prototypeRendererFactory,
|
|
24
|
+
type PrototypeElement,
|
|
25
|
+
type PrototypeRenderer,
|
|
26
|
+
type PrototypeViewport,
|
|
27
|
+
} from "../prototype-browser";
|
|
28
|
+
import {
|
|
29
|
+
deleteKittyImage,
|
|
30
|
+
placeKittyImage,
|
|
31
|
+
resolveTransmitMedium,
|
|
32
|
+
transmitKittyImage,
|
|
33
|
+
type CellRegion,
|
|
34
|
+
} from "../kitty-image";
|
|
35
|
+
|
|
36
|
+
export interface PrototypeContentViewProps {
|
|
37
|
+
prototypePath: string;
|
|
38
|
+
canComment: boolean;
|
|
39
|
+
onCommentElement: (element: PrototypeElement, body: string) => void;
|
|
40
|
+
/** Signals when the inline compose owns the keyboard, so the app suppresses
|
|
41
|
+
* its global keymap and the compose textarea receives the typed note. */
|
|
42
|
+
onComposingChange?: (active: boolean) => void;
|
|
43
|
+
/** True while an app overlay (menu, settings) covers the sheet; the image is
|
|
44
|
+
* removed so those overlays are not shown through the graphics layer. */
|
|
45
|
+
hidden?: boolean;
|
|
46
|
+
theme?: Theme;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type SheetStatus = "loading" | "ready" | "unsupported" | "error";
|
|
50
|
+
|
|
51
|
+
const OVERLAY_ROWS = 3;
|
|
52
|
+
/** Compose-card width, so the floating composer reads like the plan/diff one
|
|
53
|
+
* instead of shrinking to its content. */
|
|
54
|
+
const COMPOSE_COLS = 46;
|
|
55
|
+
const PROTOTYPE_IMAGE_ID = 811;
|
|
56
|
+
/** Page pixels scrolled per wheel notch. */
|
|
57
|
+
const SCROLL_STEP = 240;
|
|
58
|
+
// The capture viewport width in CSS pixels; its height matches the region's
|
|
59
|
+
// cell aspect so the rendered image fills the box instead of letterboxing. The
|
|
60
|
+
// aspect comes from the terminal's reported cell pixels, falling back to a
|
|
61
|
+
// typical monospace ratio when the terminal never reports one.
|
|
62
|
+
const CAPTURE_WIDTH = 1280;
|
|
63
|
+
const FALLBACK_CELL_HEIGHT_OVER_WIDTH = 2.18;
|
|
64
|
+
|
|
65
|
+
function cellHeightOverWidth(renderer: {
|
|
66
|
+
resolution: { width: number; height: number } | null;
|
|
67
|
+
terminalWidth: number;
|
|
68
|
+
terminalHeight: number;
|
|
69
|
+
}): number {
|
|
70
|
+
const resolution = renderer.resolution;
|
|
71
|
+
|
|
72
|
+
if (!resolution || renderer.terminalWidth < 1 || renderer.terminalHeight < 1) {
|
|
73
|
+
return FALLBACK_CELL_HEIGHT_OVER_WIDTH;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return resolution.height / renderer.terminalHeight / (resolution.width / renderer.terminalWidth);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function regionOf(box: BoxRenderable | null): CellRegion | null {
|
|
80
|
+
if (!box || box.width < 1 || box.height < 1) return null;
|
|
81
|
+
|
|
82
|
+
return { column: box.x, row: box.y, columns: box.width, rows: box.height };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type CaptureRenderer = {
|
|
86
|
+
resolution: { width: number; height: number } | null;
|
|
87
|
+
terminalWidth: number;
|
|
88
|
+
terminalHeight: number;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Capture at the region's own pixel size when the terminal reports its cell
|
|
93
|
+
* pixels, so the terminal never downscales an oversized image (less Chromium
|
|
94
|
+
* raster and fewer bytes). Without a resolution report, fall back to a fixed
|
|
95
|
+
* width at 2x and derive the height from the cell aspect.
|
|
96
|
+
*/
|
|
97
|
+
function captureConfig(region: CellRegion, renderer: CaptureRenderer) {
|
|
98
|
+
const resolution = renderer.resolution;
|
|
99
|
+
|
|
100
|
+
if (resolution && renderer.terminalWidth >= 1 && renderer.terminalHeight >= 1) {
|
|
101
|
+
const cellWidth = resolution.width / renderer.terminalWidth;
|
|
102
|
+
const cellHeight = resolution.height / renderer.terminalHeight;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
viewport: {
|
|
106
|
+
width: Math.max(1, Math.round(region.columns * cellWidth)),
|
|
107
|
+
height: Math.max(1, Math.round(region.rows * cellHeight)),
|
|
108
|
+
},
|
|
109
|
+
deviceScaleFactor: 1,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
viewport: {
|
|
115
|
+
width: CAPTURE_WIDTH,
|
|
116
|
+
height: Math.round(
|
|
117
|
+
(CAPTURE_WIDTH * region.rows * cellHeightOverWidth(renderer)) / region.columns,
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
deviceScaleFactor: 2,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The terminal's scheme, read from its reading-text color: a dark terminal
|
|
126
|
+
* carries light text, a light terminal dark text. Passed to the renderer as
|
|
127
|
+
* prefers-color-scheme so a theme-aware mockup emerges into the right surface.
|
|
128
|
+
*/
|
|
129
|
+
function terminalColorScheme(textColor: string): "dark" | "light" {
|
|
130
|
+
const hex = textColor.replace("#", "");
|
|
131
|
+
|
|
132
|
+
if (hex.length < 6) return "dark";
|
|
133
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
134
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
135
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
136
|
+
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
137
|
+
|
|
138
|
+
return luminance > 0.5 ? "dark" : "light";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function PrototypeContentViewImpl({
|
|
142
|
+
prototypePath,
|
|
143
|
+
canComment,
|
|
144
|
+
onCommentElement,
|
|
145
|
+
onComposingChange,
|
|
146
|
+
hidden = false,
|
|
147
|
+
theme,
|
|
148
|
+
}: PrototypeContentViewProps): React.ReactNode {
|
|
149
|
+
const tokens = useComponentTheme(theme);
|
|
150
|
+
const palette = annotationPaletteFor(tokens);
|
|
151
|
+
const renderer = useRenderer();
|
|
152
|
+
const regionRef = useRef<BoxRenderable | null>(null);
|
|
153
|
+
const browserRef = useRef<PrototypeRenderer | null>(null);
|
|
154
|
+
const launchedRef = useRef(false);
|
|
155
|
+
const viewportRef = useRef<PrototypeViewport>({ width: CAPTURE_WIDTH, height: 800 });
|
|
156
|
+
const pngRef = useRef<Uint8Array | null>(null);
|
|
157
|
+
const transmittedRef = useRef<Uint8Array | null>(null);
|
|
158
|
+
const paintRef = useRef<() => void>(() => undefined);
|
|
159
|
+
const launchRef = useRef<() => void>(() => undefined);
|
|
160
|
+
const hiddenRef = useRef(hidden);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
hiddenRef.current = hidden;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const [status, setStatus] = useState<SheetStatus>("loading");
|
|
167
|
+
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
168
|
+
const [selected, setSelected] = useState<PrototypeElement | null>(null);
|
|
169
|
+
const [overlayCell, setOverlayCell] = useState<{
|
|
170
|
+
left: number;
|
|
171
|
+
top: number;
|
|
172
|
+
regionColumns: number;
|
|
173
|
+
} | null>(null);
|
|
174
|
+
const [composing, setComposing] = useState(false);
|
|
175
|
+
|
|
176
|
+
const setPng = (png: Uint8Array): void => {
|
|
177
|
+
pngRef.current = png;
|
|
178
|
+
transmittedRef.current = null;
|
|
179
|
+
paintRef.current();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Every frame: start the browser once layout is known, then keep the picture
|
|
183
|
+
// asserted over its cells. The kitty emission (transmit once, cheap re-place
|
|
184
|
+
// after) is skipped when the renderer has no raw writeOut channel (the test
|
|
185
|
+
// harness), but the launch trigger still runs.
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (!renderer) return;
|
|
188
|
+
const rendererOutput = v.safeParse(v.object({ writeOut: v.function() }), renderer);
|
|
189
|
+
const rawWrite = rendererOutput.success ? rendererOutput.output.writeOut : null;
|
|
190
|
+
const write = rawWrite ? (chunk: string) => rawWrite.call(renderer, chunk) : null;
|
|
191
|
+
const medium = resolveTransmitMedium();
|
|
192
|
+
const paint = (): void => {
|
|
193
|
+
if (!write) return;
|
|
194
|
+
// an app overlay is covering the sheet: pull the image so it does not show
|
|
195
|
+
// through the overlay's cells, and re-transmit once the overlay closes
|
|
196
|
+
if (hiddenRef.current) {
|
|
197
|
+
if (transmittedRef.current !== null) {
|
|
198
|
+
deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
|
|
199
|
+
transmittedRef.current = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const region = regionOf(regionRef.current);
|
|
205
|
+
const png = pngRef.current;
|
|
206
|
+
|
|
207
|
+
if (!region || !png) return;
|
|
208
|
+
if (transmittedRef.current !== png) {
|
|
209
|
+
transmitKittyImage(write, png, region, PROTOTYPE_IMAGE_ID, medium);
|
|
210
|
+
transmittedRef.current = png;
|
|
211
|
+
} else {
|
|
212
|
+
placeKittyImage(write, region, PROTOTYPE_IMAGE_ID);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
paintRef.current = paint;
|
|
217
|
+
const onFrame = (): void => {
|
|
218
|
+
launchRef.current();
|
|
219
|
+
paint();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
renderer.on("frame", onFrame);
|
|
223
|
+
|
|
224
|
+
return () => {
|
|
225
|
+
renderer.off("frame", onFrame);
|
|
226
|
+
if (write) deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
|
|
227
|
+
};
|
|
228
|
+
}, [renderer]);
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
onComposingChange?.(composing);
|
|
232
|
+
}, [composing, onComposingChange]);
|
|
233
|
+
|
|
234
|
+
const unmountedRef = useRef(false);
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
return () => {
|
|
238
|
+
unmountedRef.current = true;
|
|
239
|
+
void browserRef.current?.close();
|
|
240
|
+
};
|
|
241
|
+
}, []);
|
|
242
|
+
|
|
243
|
+
const launch = (): void => {
|
|
244
|
+
const region = regionOf(regionRef.current);
|
|
245
|
+
const caps = renderer?.capabilities;
|
|
246
|
+
|
|
247
|
+
if (launchedRef.current || !region) return;
|
|
248
|
+
if (caps && caps.kitty_graphics === false) {
|
|
249
|
+
setStatus("unsupported");
|
|
250
|
+
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
launchedRef.current = true;
|
|
254
|
+
const capture = captureConfig(region, renderer!);
|
|
255
|
+
|
|
256
|
+
viewportRef.current = capture.viewport;
|
|
257
|
+
void (async () => {
|
|
258
|
+
try {
|
|
259
|
+
const browser = await prototypeRendererFactory()({
|
|
260
|
+
filePath: prototypePath,
|
|
261
|
+
viewport: capture.viewport,
|
|
262
|
+
deviceScaleFactor: capture.deviceScaleFactor,
|
|
263
|
+
colorScheme: terminalColorScheme(tokens.text),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// the sheet may have unmounted while Chromium was launching; close the
|
|
267
|
+
// late arrival instead of leaking the process
|
|
268
|
+
if (unmountedRef.current) {
|
|
269
|
+
void browser.close();
|
|
270
|
+
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
browserRef.current = browser;
|
|
274
|
+
setPng(await browser.screenshot());
|
|
275
|
+
setStatus("ready");
|
|
276
|
+
} catch (error) {
|
|
277
|
+
setErrorMessage(error instanceof Error ? error.message : String(error));
|
|
278
|
+
setStatus("error");
|
|
279
|
+
}
|
|
280
|
+
})();
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
launchRef.current = launch;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const refresh = async (): Promise<void> => {
|
|
288
|
+
const browser = browserRef.current;
|
|
289
|
+
|
|
290
|
+
if (browser) setPng(await browser.screenshot());
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const clearSelection = (): void => {
|
|
294
|
+
setSelected(null);
|
|
295
|
+
setOverlayCell(null);
|
|
296
|
+
setComposing(false);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// While the compose textarea owns the keyboard, the global keymap is
|
|
300
|
+
// suppressed, so escape-to-cancel is handled here - matching the plan
|
|
301
|
+
// composer, where escape closes the overlay.
|
|
302
|
+
useKeyboard((key) => {
|
|
303
|
+
if (composing && key.name === "escape") clearSelection();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const onRegionMouseDown = (event: { x: number; y: number }): void => {
|
|
307
|
+
const browser = browserRef.current;
|
|
308
|
+
const region = regionOf(regionRef.current);
|
|
309
|
+
|
|
310
|
+
if (status !== "ready" || !browser || !region) return;
|
|
311
|
+
const geometry = {
|
|
312
|
+
x: region.column,
|
|
313
|
+
y: region.row,
|
|
314
|
+
width: region.columns,
|
|
315
|
+
height: region.rows,
|
|
316
|
+
};
|
|
317
|
+
const cssPoint = imageCellToCss(event, geometry, viewportRef.current);
|
|
318
|
+
|
|
319
|
+
if (!cssPoint) return;
|
|
320
|
+
void (async () => {
|
|
321
|
+
const element = await browser.elementAt(cssPoint.x, cssPoint.y);
|
|
322
|
+
|
|
323
|
+
if (!element) return;
|
|
324
|
+
const cell = cssBoxToCell(element.box, geometry, viewportRef.current);
|
|
325
|
+
const topRaw = cell.row - geometry.y;
|
|
326
|
+
|
|
327
|
+
setSelected(element);
|
|
328
|
+
setOverlayCell({
|
|
329
|
+
left: Math.max(0, cell.column - geometry.x),
|
|
330
|
+
top: Math.max(0, topRaw < OVERLAY_ROWS ? topRaw + 1 : topRaw - OVERLAY_ROWS),
|
|
331
|
+
regionColumns: geometry.width,
|
|
332
|
+
});
|
|
333
|
+
// selecting an element opens the compose card directly, the same edit
|
|
334
|
+
// primitive plan and diff use - there is no intermediate marker toolbar
|
|
335
|
+
if (canComment) {
|
|
336
|
+
onComposingChange?.(true);
|
|
337
|
+
setComposing(true);
|
|
338
|
+
} else {
|
|
339
|
+
setComposing(false);
|
|
340
|
+
}
|
|
341
|
+
})().catch((error) => {
|
|
342
|
+
setErrorMessage(error instanceof Error ? error.message : String(error));
|
|
343
|
+
setStatus("error");
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const onRegionScroll = (event: { scroll?: { direction: string } }): void => {
|
|
348
|
+
const browser = browserRef.current;
|
|
349
|
+
|
|
350
|
+
if (status !== "ready" || !browser) return;
|
|
351
|
+
const delta = event.scroll?.direction === "up" ? -SCROLL_STEP : SCROLL_STEP;
|
|
352
|
+
|
|
353
|
+
// a selection would drift once the page scrolls under it, so drop it
|
|
354
|
+
clearSelection();
|
|
355
|
+
void browser
|
|
356
|
+
.scrollBy(delta)
|
|
357
|
+
.then(() => refresh())
|
|
358
|
+
.catch(() => undefined);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const commit = (body: string): void => {
|
|
362
|
+
if (!canComment) return;
|
|
363
|
+
if (selected && body.trim()) onCommentElement(selected, body.trim());
|
|
364
|
+
clearSelection();
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// the overlay carries an opaque fill so the compose card reads as a solid
|
|
368
|
+
// card over the image, matching the plan surface
|
|
369
|
+
const overlayStyle = {
|
|
370
|
+
position: "absolute" as const,
|
|
371
|
+
left: overlayCell?.left ?? 0,
|
|
372
|
+
top: Math.max(0, overlayCell?.top ?? 0),
|
|
373
|
+
flexDirection: "column" as const,
|
|
374
|
+
backgroundColor: tokens.elevated,
|
|
375
|
+
};
|
|
376
|
+
// keep the composer inside the region: never wider than the region, and
|
|
377
|
+
// clamped so it cannot run off either edge on a narrow preview
|
|
378
|
+
const regionColumns = overlayCell?.regionColumns ?? 0;
|
|
379
|
+
const composeWidth = regionColumns > 0 ? Math.min(COMPOSE_COLS, regionColumns) : COMPOSE_COLS;
|
|
380
|
+
const composeStyle = {
|
|
381
|
+
...overlayStyle,
|
|
382
|
+
left: Math.max(0, Math.min(overlayStyle.left, regionColumns - composeWidth)),
|
|
383
|
+
width: composeWidth,
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<box
|
|
388
|
+
style={{
|
|
389
|
+
flexGrow: 1,
|
|
390
|
+
flexDirection: "column",
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
<box
|
|
394
|
+
ref={(instance: BoxRenderable | null) => {
|
|
395
|
+
regionRef.current = instance;
|
|
396
|
+
}}
|
|
397
|
+
style={{ flexGrow: 1 }}
|
|
398
|
+
onSizeChange={launch}
|
|
399
|
+
onMouseDown={onRegionMouseDown}
|
|
400
|
+
onMouseScroll={onRegionScroll}
|
|
401
|
+
/>
|
|
402
|
+
{status !== "ready" ? (
|
|
403
|
+
<box style={{ position: "absolute", left: 2, top: 1 }}>
|
|
404
|
+
<text fg={tokens.textDim}>{statusLine(status, errorMessage)}</text>
|
|
405
|
+
</box>
|
|
406
|
+
) : null}
|
|
407
|
+
{selected && composing ? (
|
|
408
|
+
<box style={composeStyle}>
|
|
409
|
+
<DiscussionCard
|
|
410
|
+
tokens={tokens}
|
|
411
|
+
segments={[
|
|
412
|
+
{
|
|
413
|
+
color: palette.cardEdge,
|
|
414
|
+
node: (
|
|
415
|
+
<Composer
|
|
416
|
+
seed=""
|
|
417
|
+
glyph="●"
|
|
418
|
+
tokens={tokens}
|
|
419
|
+
onSave={commit}
|
|
420
|
+
onReady={() => undefined}
|
|
421
|
+
onInput={() => undefined}
|
|
422
|
+
/>
|
|
423
|
+
),
|
|
424
|
+
},
|
|
425
|
+
]}
|
|
426
|
+
/>
|
|
427
|
+
</box>
|
|
428
|
+
) : null}
|
|
429
|
+
</box>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Memoized so an unrelated App re-render (status ticks, a rail-width drag) does
|
|
435
|
+
* not re-render the view; the parent passes a stable onCommentElement so the
|
|
436
|
+
* shallow prop compare holds.
|
|
437
|
+
*/
|
|
438
|
+
export const PrototypeContentView = React.memo(PrototypeContentViewImpl);
|
|
439
|
+
|
|
440
|
+
function statusLine(status: SheetStatus, errorMessage: string): string {
|
|
441
|
+
if (status === "loading") return "rendering prototype…";
|
|
442
|
+
if (status === "unsupported")
|
|
443
|
+
return "prototype preview needs a graphics terminal (kitty or ghostty)";
|
|
444
|
+
|
|
445
|
+
return `prototype preview failed: ${errorMessage}`;
|
|
446
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { RootOverlayProvider, useRootOverlay } from "./RootOverlay";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Layout/RootOverlay" };
|
|
7
|
+
|
|
8
|
+
// The provider renders whatever a descendant pushes after the rest of the tree, so it draws on top.
|
|
9
|
+
function OverlayDemo(): React.ReactNode {
|
|
10
|
+
const { setOverlay } = useRootOverlay();
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setOverlay(
|
|
14
|
+
"demo",
|
|
15
|
+
<box
|
|
16
|
+
style={{
|
|
17
|
+
position: "absolute",
|
|
18
|
+
top: 2,
|
|
19
|
+
left: 6,
|
|
20
|
+
borderStyle: "single",
|
|
21
|
+
borderColor: DARK.border,
|
|
22
|
+
backgroundColor: DARK.elevated,
|
|
23
|
+
paddingLeft: 1,
|
|
24
|
+
paddingRight: 1,
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<text fg={DARK.text}>floating overlay</text>
|
|
28
|
+
</box>,
|
|
29
|
+
);
|
|
30
|
+
}, [setOverlay]);
|
|
31
|
+
|
|
32
|
+
return <text fg={DARK.textDim}>base content under the overlay</text>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const FloatingNode: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<RootOverlayProvider>
|
|
38
|
+
<box style={{ padding: 1, backgroundColor: DARK.panel }}>
|
|
39
|
+
<OverlayDemo />
|
|
40
|
+
</box>
|
|
41
|
+
</RootOverlayProvider>
|
|
42
|
+
),
|
|
43
|
+
expectedColors: [DARK.text],
|
|
44
|
+
size: { width: 40, height: 6 },
|
|
45
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// A root overlay slot: OpenTUI has no z-index, so an overlay drawn inside a pane is sliced by the
|
|
2
|
+
// next pane's border. Anything pushed here renders after every pane (last in draw order), so it sits
|
|
3
|
+
// on top of the whole shell - the same trick the tooltip uses. Callers position their node with
|
|
4
|
+
// absolute screen coordinates and hold a stable key, so several overlays (a menu, a hover preview)
|
|
5
|
+
// coexist instead of clobbering one shared slot.
|
|
6
|
+
|
|
7
|
+
import React, { createContext, useContext, useMemo, useState } from "react";
|
|
8
|
+
|
|
9
|
+
interface RootOverlayApi {
|
|
10
|
+
setOverlay: (key: string, node: React.ReactNode) => void;
|
|
11
|
+
clearOverlay: (key: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const RootOverlayContext = createContext<RootOverlayApi>({
|
|
15
|
+
setOverlay: () => {},
|
|
16
|
+
clearOverlay: () => {},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function useRootOverlay(): RootOverlayApi {
|
|
20
|
+
return useContext(RootOverlayContext);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function RootOverlayProvider({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
24
|
+
const [overlays, setOverlays] = useState<Map<string, React.ReactNode>>(new Map());
|
|
25
|
+
const api = useMemo<RootOverlayApi>(
|
|
26
|
+
() => ({
|
|
27
|
+
setOverlay: (key, node) => setOverlays((current) => new Map(current).set(key, node)),
|
|
28
|
+
clearOverlay: (key) =>
|
|
29
|
+
setOverlays((current) => {
|
|
30
|
+
if (!current.has(key)) return current;
|
|
31
|
+
const next = new Map(current);
|
|
32
|
+
next.delete(key);
|
|
33
|
+
|
|
34
|
+
return next;
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
[],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<RootOverlayContext.Provider value={api}>
|
|
42
|
+
{children}
|
|
43
|
+
{[...overlays].map(([key, node]) => (
|
|
44
|
+
<React.Fragment key={key}>{node}</React.Fragment>
|
|
45
|
+
))}
|
|
46
|
+
</RootOverlayContext.Provider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import { ScrollArea } from "./ScrollArea";
|
|
4
|
+
import type { Story, StoryMeta } from "./story";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/ScrollArea" };
|
|
7
|
+
|
|
8
|
+
// Taller-than-viewport content; the overlay bar stays hidden until the surface scrolls.
|
|
9
|
+
export const AtRest: Story = {
|
|
10
|
+
render: () => (
|
|
11
|
+
<box style={{ width: 24, height: 8, backgroundColor: DARK.background }}>
|
|
12
|
+
<ScrollArea>
|
|
13
|
+
{Array.from({ length: 24 }, (_unused, index) => (
|
|
14
|
+
<text key={index} fg={DARK.textMuted}>{`row ${index + 1}`}</text>
|
|
15
|
+
))}
|
|
16
|
+
</ScrollArea>
|
|
17
|
+
</box>
|
|
18
|
+
),
|
|
19
|
+
size: { width: 24, height: 8 },
|
|
20
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scrollbox whose bar follows the global rule: no always-on scrollbar. The built-in bar is
|
|
3
|
+
* hidden and an OverlayScrollbar rides the rightmost column, appearing only while scrolling.
|
|
4
|
+
* Pass scrollRef to keep scrollTo / scrollChildIntoView on the underlying ScrollBox.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useRef, type ReactNode, type RefObject } from "react";
|
|
8
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
9
|
+
import type { Theme } from "../theme";
|
|
10
|
+
import { OverlayScrollbar } from "./OverlayScrollbar";
|
|
11
|
+
|
|
12
|
+
export interface ScrollAreaProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
focused?: boolean;
|
|
15
|
+
theme?: Theme;
|
|
16
|
+
scrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ScrollArea({
|
|
20
|
+
children,
|
|
21
|
+
focused = false,
|
|
22
|
+
theme,
|
|
23
|
+
scrollRef,
|
|
24
|
+
}: ScrollAreaProps): React.ReactNode {
|
|
25
|
+
const innerRef = useRef<ScrollBoxRenderable | null>(null);
|
|
26
|
+
const attach = (node: ScrollBoxRenderable | null): void => {
|
|
27
|
+
innerRef.current = node;
|
|
28
|
+
if (scrollRef) scrollRef.current = node;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
33
|
+
<scrollbox
|
|
34
|
+
ref={attach}
|
|
35
|
+
style={{ flexGrow: 1 }}
|
|
36
|
+
focused={focused}
|
|
37
|
+
verticalScrollbarOptions={{ visible: false }}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</scrollbox>
|
|
41
|
+
<OverlayScrollbar scrollbox={innerRef} theme={theme} />
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|