@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.100
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/annotations/annotation-palette.ts +62 -0
- package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
- package/src/annotations/components/AnnotationCards.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +364 -0
- package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
- package/src/annotations/discussions.ts +77 -0
- package/src/annotations/thread-selection.test.ts +210 -0
- package/src/annotations/thread-selection.ts +242 -0
- package/src/annotations/use-annotation-surface.tsx +1238 -0
- package/src/app/App.appearance.test.tsx +53 -0
- package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
- package/src/app/App.deleted-scroll.test.tsx +187 -0
- package/src/app/App.diff.test.tsx +313 -0
- package/src/app/App.export.test.tsx +105 -0
- package/src/app/App.focus.test.ts +89 -0
- package/src/app/App.identity.test.tsx +84 -0
- package/src/app/App.plan-render.test.tsx +209 -0
- package/src/app/App.plan-review.test.tsx +428 -0
- package/src/app/App.prototype-markdown.test.tsx +128 -0
- package/src/app/App.prototype.test.tsx +176 -0
- package/src/app/App.readonly.test.tsx +210 -0
- package/src/app/App.regression.test.tsx +99 -0
- package/src/app/App.rendering.test.tsx +134 -0
- package/src/app/App.share-dialog.test.tsx +177 -0
- package/src/app/App.submit-confirm.test.tsx +267 -0
- package/src/app/App.test.tsx +496 -0
- package/src/app/App.thread-agent.test.ts +12 -0
- package/src/app/App.toast.test.tsx +124 -0
- package/src/app/App.tsx +1650 -0
- package/src/app/App.visual.test.tsx +59 -0
- package/src/app/App.walk.test.tsx +299 -0
- package/src/app/App.workbench.test.tsx +1149 -0
- package/src/app/app-key-state.ts +61 -0
- package/src/app/app-screens.tsx +606 -0
- package/src/app/app-view-model.ts +168 -0
- package/src/app/components/AppHeader.stories.tsx +50 -0
- package/src/app/components/AppHeader.tsx +81 -0
- package/src/app/components/AppShell.brand.test.tsx +58 -0
- package/src/app/components/AppShell.extensions.test.tsx +191 -0
- package/src/app/components/AppShell.stories.tsx +89 -0
- package/src/app/components/AppShell.tsx +358 -0
- package/src/app/components/AppShell.zoom.test.tsx +149 -0
- package/src/app/components/Breadcrumb.stories.tsx +19 -0
- package/src/app/components/Breadcrumb.tsx +45 -0
- package/src/app/components/MenuBar.stories.tsx +28 -0
- package/src/app/components/MenuBar.tsx +44 -0
- package/src/app/components/PaneGrid.stories.tsx +84 -0
- package/src/app/components/PaneGrid.tsx +128 -0
- package/src/app/components/PanelColumn.stories.tsx +25 -0
- package/src/app/components/PanelColumn.tsx +93 -0
- package/src/app/components/ShellHeader.stories.tsx +50 -0
- package/src/app/components/ShellHeader.tsx +137 -0
- package/src/app/components/WorkspacePanels.stories.tsx +36 -0
- package/src/app/components/WorkspacePanels.tsx +120 -0
- package/src/app/components/pages/Workspace.stories.tsx +181 -0
- package/src/app/intent-dispatch.test.ts +728 -0
- package/src/app/intent-dispatch.ts +687 -0
- package/src/app/launch-layout.test.ts +78 -0
- package/src/app/launch-layout.ts +57 -0
- package/src/app/ready-signal.test.ts +85 -0
- package/src/app/ready-signal.ts +52 -0
- package/src/app/run.ts +109 -0
- package/src/{serve.test.ts → app/serve.test.ts} +24 -12
- package/src/app/serve.ts +111 -0
- package/src/app/version.ts +5 -0
- package/src/appearance/components/theme-context.stories.tsx +46 -0
- package/src/appearance/components/theme-context.tsx +37 -0
- package/src/appearance/theme-presets.test.ts +51 -0
- package/src/appearance/theme-presets.ts +219 -0
- package/src/appearance/theme.ts +112 -0
- package/src/appearance/visual/contrast.test.ts +35 -0
- package/src/appearance/visual/contrast.ts +33 -0
- package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
- package/src/diff/components/AnnotatableFileView.tsx +141 -0
- package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
- package/src/diff/components/DiffContentView.split.test.tsx +140 -0
- package/src/diff/components/DiffContentView.stories.tsx +80 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/diff/components/DiffContentView.tsx +1055 -0
- package/src/diff/components/WalkWizard.stories.tsx +100 -0
- package/src/diff/components/WalkWizard.tsx +146 -0
- package/src/diff/components/diff-content-view-layout.test.ts +198 -0
- package/src/diff/components/diff-content-view-layout.ts +84 -0
- package/src/diff/diff-fold.test.ts +78 -0
- package/src/diff/diff-fold.ts +126 -0
- package/src/diff/diff-hunk-curate.test.ts +315 -0
- package/src/diff/diff-hunk-curate.ts +152 -0
- package/src/diff/diff-intraline.test.ts +239 -0
- package/src/diff/diff-intraline.ts +238 -0
- package/src/diff/diff-syntax.test.ts +125 -0
- package/src/diff/diff-syntax.ts +186 -0
- package/src/diff/split-diff.test.ts +110 -0
- package/src/diff/split-diff.ts +144 -0
- package/src/diff/view-diff.test.ts +325 -0
- package/src/diff/view-diff.ts +522 -0
- package/src/diff/walk.test.ts +121 -0
- package/src/diff/walk.ts +73 -0
- package/src/index.ts +31 -2
- package/src/integrations/attribution.test.ts +31 -0
- package/src/integrations/attribution.ts +42 -0
- package/src/integrations/client-extension-registry.test.tsx +61 -0
- package/src/integrations/client-extension-registry.ts +159 -0
- package/src/integrations/clipboard.test.ts +23 -0
- package/src/integrations/clipboard.ts +33 -0
- package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
- package/src/integrations/components/ExtensionZone.tsx +145 -0
- package/src/integrations/components/ShareDialog.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +550 -0
- package/src/integrations/components/share-dialog-store.ts +55 -0
- package/src/integrations/editor.test.ts +149 -0
- package/src/integrations/editor.ts +160 -0
- package/src/integrations/github-identity.test.ts +58 -0
- package/src/integrations/github-identity.ts +41 -0
- package/src/integrations/herdr.test.ts +43 -0
- package/src/integrations/herdr.ts +38 -0
- package/src/integrations/integrations.test.ts +41 -0
- package/src/integrations/integrations.ts +32 -0
- package/src/integrations/pasted-image.test.ts +67 -0
- package/src/integrations/pasted-image.ts +60 -0
- package/src/integrations/share.ts +238 -0
- package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
- package/src/keyboard/components/CompletionOverlay.tsx +75 -0
- package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
- package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
- package/src/keyboard/components/HandleChipsInput.tsx +143 -0
- package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
- package/src/keyboard/components/KeybindsDialog.tsx +103 -0
- package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
- package/src/keyboard/components/NavModeHint.tsx +31 -0
- package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
- package/src/keyboard/components/SlashComposer.tsx +87 -0
- package/src/keyboard/key-bindings.test.ts +156 -0
- package/src/keyboard/key-bindings.ts +459 -0
- package/src/keyboard/keymap.test.ts +611 -0
- package/src/keyboard/keymap.ts +395 -0
- package/src/keyboard/skills.test.ts +74 -0
- package/src/keyboard/skills.ts +75 -0
- package/src/keyboard/slash-palette.test.ts +103 -0
- package/src/keyboard/slash-palette.ts +129 -0
- package/src/keyboard/thread-chords.test.ts +79 -0
- package/src/keyboard/thread-chords.ts +163 -0
- package/src/keyboard/use-shared-keyboard.ts +55 -0
- package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
- package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
- package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
- package/src/markdown/components/ThreadView.stories.tsx +26 -0
- package/src/markdown/components/ThreadView.tsx +560 -0
- package/src/markdown/mark-runs.ts +121 -0
- package/src/markdown/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown/markdown-grid.test.ts +101 -0
- package/src/markdown/markdown-grid.ts +203 -0
- package/src/markdown/markdown-highlight.test.ts +61 -0
- package/src/markdown/markdown-highlight.ts +124 -0
- package/src/markdown/thread-code-syntax.ts +45 -0
- package/src/markdown/use-thread-body-editing.tsx +54 -0
- package/src/markdown/view-plan.regression.test.ts +60 -0
- package/src/markdown/view-plan.test.ts +545 -0
- package/src/markdown/view-plan.ts +1009 -0
- package/src/performance/otlp-trace.test.ts +119 -0
- package/src/performance/otlp-trace.ts +134 -0
- package/src/performance/perf-report.ts +18 -0
- package/src/performance/perf-timings.test.ts +18 -0
- package/src/performance/perf-timings.ts +39 -0
- package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
- package/src/prototype/components/PrototypeContentView.tsx +442 -0
- package/src/prototype/prototype-browser.test.ts +81 -0
- package/src/prototype/prototype-browser.ts +321 -0
- package/src/prototype/prototype-pixels.tsx +35 -0
- package/src/settings/components/SettingsDialog.stories.tsx +117 -0
- package/src/settings/components/SettingsDialog.tsx +183 -0
- package/src/settings/components/SettingsRows.stories.tsx +18 -0
- package/src/settings/components/SettingsRows.tsx +112 -0
- package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
- package/src/settings/components/quick-actions-editor.test.tsx +120 -0
- package/src/settings/components/quick-actions-editor.tsx +222 -0
- package/src/settings/config-identity.test.ts +78 -0
- package/src/settings/config.test.ts +676 -0
- package/src/settings/config.ts +628 -0
- package/src/settings/use-settings-dialog.tsx +351 -0
- package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
- package/src/stories/stories-app.tsx +196 -0
- package/src/stories/stories.test.tsx +74 -0
- package/src/stories/story-fixtures.ts +102 -0
- package/src/stories/story-tree.test.ts +108 -0
- package/src/stories/story.ts +154 -0
- package/src/terminal/ansi-screen.test.ts +28 -0
- package/src/terminal/ansi-screen.ts +72 -0
- package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
- package/src/terminal/components/AnsiScreen.tsx +28 -0
- package/src/terminal/components/terminal-pane.test.tsx +40 -0
- package/src/terminal/components/terminal-pane.ts +196 -0
- package/src/terminal/ghostty-terminal.test.ts +85 -0
- package/src/terminal/ghostty-terminal.ts +197 -0
- package/src/terminal/kitty-image.test.ts +66 -0
- package/src/terminal/kitty-image.ts +137 -0
- package/src/terminal/pty.test.ts +81 -0
- package/src/terminal/pty.ts +246 -0
- package/src/testing/test-support.test.ts +264 -0
- package/src/testing/test-support.ts +367 -0
- package/src/thread/agent-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +829 -0
- package/src/thread/components/AgentThreadPane.tsx +367 -0
- package/src/thread/components/ThreadFooter.stories.tsx +30 -0
- package/src/thread/components/ThreadFooter.test.tsx +49 -0
- package/src/thread/components/ThreadFooter.tsx +77 -0
- package/src/thread/components/ThreadTree.stories.tsx +46 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +267 -0
- package/src/thread/components/thread-tree.test.ts +137 -0
- package/src/thread/components/thread-tree.ts +112 -0
- package/src/thread/thread-controller.curate.test.ts +502 -0
- package/src/thread/thread-controller.derivation.test.ts +62 -0
- package/src/thread/thread-controller.live-diff.test.ts +357 -0
- package/src/thread/thread-controller.optimistic.test.ts +69 -0
- package/src/thread/thread-controller.share.test.ts +573 -0
- package/src/thread/thread-controller.switch-cache.test.ts +174 -0
- package/src/thread/thread-controller.tree.test.ts +263 -0
- package/src/thread/thread-controller.ts +2018 -0
- package/src/thread/tree-view.test.ts +128 -0
- package/src/thread/tree-view.ts +146 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +111 -0
- package/src/ui/components/CodeBlock.stories.tsx +42 -0
- package/src/ui/components/CodeBlock.tsx +80 -0
- package/src/ui/components/ConfirmCard.stories.tsx +37 -0
- package/src/ui/components/ConfirmCard.tsx +151 -0
- package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
- package/src/ui/components/ConfirmDialog.tsx +72 -0
- package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
- package/src/ui/components/OverlayScrollbar.tsx +129 -0
- package/src/ui/components/PromptDialog.stories.tsx +36 -0
- package/src/ui/components/PromptDialog.test.tsx +65 -0
- package/src/ui/components/PromptDialog.tsx +99 -0
- package/src/ui/components/RootOverlay.stories.tsx +45 -0
- package/src/ui/components/RootOverlay.tsx +49 -0
- package/src/ui/components/ScrollArea.stories.tsx +20 -0
- package/src/ui/components/ScrollArea.tsx +61 -0
- package/src/ui/components/Toast.stories.tsx +19 -0
- package/src/ui/components/Toast.tsx +54 -0
- package/src/ui/components/Tooltip.stories.tsx +26 -0
- package/src/ui/components/Tooltip.tsx +72 -0
- package/src/ui/components/menu-control.stories.tsx +35 -0
- package/src/ui/components/menu-control.tsx +43 -0
- package/src/ui/components/primitives/Button.stories.tsx +32 -0
- package/src/ui/components/primitives/Button.tsx +57 -0
- package/src/ui/components/primitives/Card.stories.tsx +25 -0
- package/src/ui/components/primitives/Card.tsx +62 -0
- package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
- package/src/ui/components/primitives/Dialog.test.tsx +38 -0
- package/src/ui/components/primitives/Dialog.tsx +69 -0
- package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/ui/components/primitives/DialogActions.tsx +39 -0
- package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
- package/src/ui/components/primitives/IconButton.tsx +58 -0
- package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/ui/components/primitives/StatusBar.tsx +23 -0
- package/src/ui/components/primitives/Switch.stories.tsx +17 -0
- package/src/ui/components/primitives/Switch.tsx +32 -0
- package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
- package/src/ui/components/primitives/Tabs.tsx +101 -0
- package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/ui/components/primitives/Toolbar.tsx +14 -0
- package/src/ui/components/primitives/Tree.stories.tsx +124 -0
- package/src/ui/components/primitives/Tree.tsx +133 -0
- package/src/ui/components/primitives/frame.test.ts +12 -0
- package/src/ui/components/primitives/frame.ts +17 -0
- package/src/ui/components/primitives/icons.ts +61 -0
- package/src/ui/components/primitives/tree-model.test.ts +99 -0
- package/src/ui/components/primitives/tree-model.ts +137 -0
- package/src/ui/components/syntax-highlight.ts +133 -0
- package/src/ui/components/truncate-text.ts +7 -0
- package/src/ui/components/truncate-title.test.ts +31 -0
- package/src/ui/components/truncate-title.ts +15 -0
- package/src/ui/use-frame-measure.ts +74 -0
- package/src/ui/use-terminal-virtualizer.ts +259 -0
- package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
- package/src/workbench/components/ChangesColumn.tsx +203 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
- package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.stories.tsx +86 -0
- package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.tsx +544 -0
- package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
- package/src/workbench/components/FileContentsView.stories.tsx +16 -0
- package/src/workbench/components/FileContentsView.test.tsx +29 -0
- package/src/workbench/components/FileContentsView.tsx +100 -0
- package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
- package/src/workbench/components/GridTabContent.stories.tsx +59 -0
- package/src/workbench/components/GridTabContent.tsx +193 -0
- package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
- package/src/workbench/components/ProjectTreeView.tsx +151 -0
- package/src/workbench/components/TreePane.stories.tsx +106 -0
- package/src/workbench/components/TreePane.tsx +155 -0
- package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
- package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
- package/src/workbench/components/WelcomePlayground.tsx +138 -0
- package/src/workbench/components/editor-grid.test.ts +184 -0
- package/src/workbench/components/editor-grid.ts +281 -0
- package/src/workbench/components/file-tree.test.ts +60 -0
- package/src/workbench/components/file-tree.ts +112 -0
- package/src/workbench/use-changes-workbench.ts +205 -0
- package/src/workbench/use-remember-layout.ts +31 -0
- package/src/workbench/use-repo-changes.ts +91 -0
- package/src/workbench/workbench-snapshot.test.ts +99 -0
- package/src/workbench/workbench-snapshot.ts +59 -0
- package/src/App.diff.test.tsx +0 -99
- package/src/App.export.test.tsx +0 -88
- package/src/App.plan-review.test.tsx +0 -259
- package/src/App.readonly.test.tsx +0 -148
- package/src/App.rendering.test.tsx +0 -108
- package/src/App.submit-confirm.test.tsx +0 -183
- package/src/App.test.tsx +0 -218
- package/src/App.tsx +0 -1249
- package/src/config.test.ts +0 -116
- package/src/config.ts +0 -150
- package/src/editor.ts +0 -34
- package/src/herdr.test.ts +0 -68
- package/src/herdr.ts +0 -34
- package/src/keymap.test.ts +0 -270
- package/src/keymap.ts +0 -173
- package/src/run.ts +0 -30
- package/src/serve.ts +0 -75
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/theme.ts +0 -46
- package/src/view-diff.test.ts +0 -55
- package/src/view-diff.ts +0 -86
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Headless-Chromium backing for prototype review: render an HTML file to a PNG
|
|
4
|
+
* and resolve a click coordinate to a DOM element. puppeteer-core is imported
|
|
5
|
+
* lazily so plan/diff review and the test suite never load it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface PrototypeElement {
|
|
9
|
+
selector: string;
|
|
10
|
+
quote: string;
|
|
11
|
+
box: ElementBox;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ElementBox {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PrototypeViewport {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PrototypeRenderer {
|
|
27
|
+
readonly viewport: PrototypeViewport;
|
|
28
|
+
screenshot(): Promise<Uint8Array>;
|
|
29
|
+
elementAt(cssX: number, cssY: number): Promise<PrototypeElement | null>;
|
|
30
|
+
scrollBy(deltaY: number): Promise<boolean>;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LaunchOptions {
|
|
35
|
+
filePath: string;
|
|
36
|
+
viewport: PrototypeViewport;
|
|
37
|
+
deviceScaleFactor?: number;
|
|
38
|
+
executablePath?: string;
|
|
39
|
+
colorScheme?: "dark" | "light";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Cell click -> CSS pixel inside the letterboxed image, or null when outside it. */
|
|
43
|
+
export function imageCellToCss(
|
|
44
|
+
event: { x: number; y: number },
|
|
45
|
+
image: { x: number; y: number; width: number; height: number },
|
|
46
|
+
viewport: PrototypeViewport,
|
|
47
|
+
): { x: number; y: number } | null {
|
|
48
|
+
const columnInImage = event.x - image.x;
|
|
49
|
+
const rowInImage = event.y - image.y;
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
columnInImage < 0 ||
|
|
53
|
+
rowInImage < 0 ||
|
|
54
|
+
columnInImage >= image.width ||
|
|
55
|
+
rowInImage >= image.height
|
|
56
|
+
) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
x: ((columnInImage + 0.5) / image.width) * viewport.width,
|
|
62
|
+
y: ((rowInImage + 0.5) / image.height) * viewport.height,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** CSS pixel rect -> the image cell it maps to, for anchoring overlays. */
|
|
67
|
+
export function cssBoxToCell(
|
|
68
|
+
box: ElementBox,
|
|
69
|
+
image: { x: number; y: number; width: number; height: number },
|
|
70
|
+
viewport: PrototypeViewport,
|
|
71
|
+
) {
|
|
72
|
+
const scaleColumn = image.width / viewport.width;
|
|
73
|
+
const scaleRow = image.height / viewport.height;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
column: image.x + Math.floor(box.x * scaleColumn),
|
|
77
|
+
row: image.y + Math.floor(box.y * scaleRow),
|
|
78
|
+
columns: Math.max(1, Math.round(box.width * scaleColumn)),
|
|
79
|
+
rows: Math.max(1, Math.round(box.height * scaleRow)),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type PrototypeRendererFactory = (options: LaunchOptions) => Promise<PrototypeRenderer>;
|
|
84
|
+
|
|
85
|
+
let rendererFactory: PrototypeRendererFactory | null = null;
|
|
86
|
+
|
|
87
|
+
/** Override the renderer factory so tests can inject a browserless fake. */
|
|
88
|
+
export function setPrototypeRendererFactory(factory: PrototypeRendererFactory | null): void {
|
|
89
|
+
rendererFactory = factory;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The active factory - the injected fake in tests, else real headless Chromium. */
|
|
93
|
+
export function prototypeRendererFactory(): PrototypeRendererFactory {
|
|
94
|
+
return rendererFactory ?? launchPrototypeRenderer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type PuppeteerBrowser = Awaited<ReturnType<typeof import("puppeteer-core").default.launch>>;
|
|
98
|
+
|
|
99
|
+
// One Chromium per process, kept warm and reused across prototype opens: the
|
|
100
|
+
// launch is the dominant load cost, so each open only spawns a fresh page.
|
|
101
|
+
let sharedBrowser: Promise<PuppeteerBrowser> | null = null;
|
|
102
|
+
|
|
103
|
+
/** Install only the browser lifecycle used by unit tests, without launching Chrome. */
|
|
104
|
+
export function setPrototypeBrowserForTest(browser: Pick<PuppeteerBrowser, "close"> | null): void {
|
|
105
|
+
// SAFETY: tests call only closePrototypeBrowser before resetting this seam, so close is the sole
|
|
106
|
+
// browser member the shared slot can read while the double is installed.
|
|
107
|
+
sharedBrowser = browser ? Promise.resolve(browser as PuppeteerBrowser) : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function warmBrowser(executablePath: string | undefined): Promise<PuppeteerBrowser> {
|
|
111
|
+
// reuse the warm browser only while it is still connected; a crashed or
|
|
112
|
+
// disconnected Chromium is dropped so the next open relaunches instead of
|
|
113
|
+
// calling newPage on a dead process forever
|
|
114
|
+
const cached = sharedBrowser ? await sharedBrowser.catch(() => null) : null;
|
|
115
|
+
|
|
116
|
+
if (cached && cached.connected) return cached;
|
|
117
|
+
const puppeteer = (await import("puppeteer-core")).default;
|
|
118
|
+
const launched = puppeteer.launch({
|
|
119
|
+
executablePath: executablePath ?? chromeExecutable(),
|
|
120
|
+
headless: true,
|
|
121
|
+
args: ["--no-sandbox", "--hide-scrollbars", "--force-color-profile=srgb"],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
sharedBrowser = launched;
|
|
125
|
+
const browser = await launched.catch((error) => {
|
|
126
|
+
if (sharedBrowser === launched) sharedBrowser = null;
|
|
127
|
+
throw error;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
browser.once("disconnected", () => {
|
|
131
|
+
if (sharedBrowser === launched) sharedBrowser = null;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return browser;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Close the warm Chromium so it never orphans when cueloop exits. Called from
|
|
139
|
+
* the shutdown path (the normal quit is not a signal, so puppeteer's own signal
|
|
140
|
+
* handlers do not fire). Idempotent.
|
|
141
|
+
*/
|
|
142
|
+
export async function closePrototypeBrowser(): Promise<void> {
|
|
143
|
+
const pending = sharedBrowser;
|
|
144
|
+
|
|
145
|
+
sharedBrowser = null;
|
|
146
|
+
if (!pending) return;
|
|
147
|
+
const browser = await pending.catch(() => null);
|
|
148
|
+
|
|
149
|
+
await browser?.close().catch(() => undefined);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function launchPrototypeRenderer(options: LaunchOptions): Promise<PrototypeRenderer> {
|
|
153
|
+
const { pathToFileURL } = await import("node:url");
|
|
154
|
+
const browser = await warmBrowser(options.executablePath);
|
|
155
|
+
// close only the page on teardown, never the shared browser, so the next open
|
|
156
|
+
// reuses the warm Chromium
|
|
157
|
+
const page = await browser.newPage();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await page.setViewport({
|
|
161
|
+
width: options.viewport.width,
|
|
162
|
+
height: options.viewport.height,
|
|
163
|
+
deviceScaleFactor: options.deviceScaleFactor ?? 2,
|
|
164
|
+
});
|
|
165
|
+
// match the terminal's scheme so a theme-aware mockup renders its dark or
|
|
166
|
+
// light variant to sit correctly on the surface it emerges into
|
|
167
|
+
await page.emulateMediaFeatures([
|
|
168
|
+
{ name: "prefers-color-scheme", value: options.colorScheme ?? "light" },
|
|
169
|
+
]);
|
|
170
|
+
// `load` waits for images and styles but skips networkidle0's fixed 500ms
|
|
171
|
+
// idle window, which a static local file would otherwise always pay
|
|
172
|
+
await page.goto(pathToFileURL(options.filePath).href, { waitUntil: "load" });
|
|
173
|
+
// Render the mockup on the terminal's own surface rather than on an opaque
|
|
174
|
+
// page: strip the root background so it never paints a square box, and the
|
|
175
|
+
// capture (taken with alpha, below) lets the active terminal theme show
|
|
176
|
+
// through around the mockup's own components - the prototype emerges into
|
|
177
|
+
// whatever theme is running instead of sitting in a grey card.
|
|
178
|
+
await page.evaluate(() => {
|
|
179
|
+
for (const element of [document.documentElement, document.body]) {
|
|
180
|
+
element?.style.setProperty("background", "transparent", "important");
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
await page.close().catch(() => undefined);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
viewport: options.viewport,
|
|
190
|
+
async screenshot() {
|
|
191
|
+
// omitBackground keeps the alpha from the stripped page background, so the
|
|
192
|
+
// terminal composites the mockup over its own theme surface.
|
|
193
|
+
const buffer = await page.screenshot({
|
|
194
|
+
type: "png",
|
|
195
|
+
encoding: "binary",
|
|
196
|
+
omitBackground: true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return new Uint8Array(buffer);
|
|
200
|
+
},
|
|
201
|
+
async elementAt(cssX, cssY) {
|
|
202
|
+
return page.evaluate(elementAtScript, cssX, cssY);
|
|
203
|
+
},
|
|
204
|
+
async scrollBy(deltaY) {
|
|
205
|
+
return page.evaluate(scrollByScript, deltaY);
|
|
206
|
+
},
|
|
207
|
+
async close() {
|
|
208
|
+
await page.close().catch(() => undefined);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Serialized into the page (self-contained for puppeteer): resolve the click to a component element. */
|
|
214
|
+
function elementAtScript(x: number, y: number) {
|
|
215
|
+
const SEMANTIC = new Set([
|
|
216
|
+
"SECTION",
|
|
217
|
+
"ARTICLE",
|
|
218
|
+
"LI",
|
|
219
|
+
"NAV",
|
|
220
|
+
"HEADER",
|
|
221
|
+
"FOOTER",
|
|
222
|
+
"ASIDE",
|
|
223
|
+
"FORM",
|
|
224
|
+
]);
|
|
225
|
+
const isNamed = (element: Element): boolean =>
|
|
226
|
+
element.classList.length > 0 || SEMANTIC.has(element.tagName);
|
|
227
|
+
// a card wraps several children: climb to the nearest named container holding
|
|
228
|
+
// more than one child, else the nearest named ancestor, else the clicked leaf
|
|
229
|
+
const componentRoot = (start: Element): Element => {
|
|
230
|
+
let namedFallback: Element | null = null;
|
|
231
|
+
let current: Element | null = start;
|
|
232
|
+
|
|
233
|
+
while (current && current !== document.body) {
|
|
234
|
+
if (isNamed(current)) {
|
|
235
|
+
if (current.childElementCount > 1) return current;
|
|
236
|
+
namedFallback = namedFallback ?? current;
|
|
237
|
+
}
|
|
238
|
+
current = current.parentElement;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return namedFallback ?? start;
|
|
242
|
+
};
|
|
243
|
+
const selectorFor = (start: Element): string => {
|
|
244
|
+
const parts: string[] = [];
|
|
245
|
+
let current: Element | null = start;
|
|
246
|
+
|
|
247
|
+
while (current && current.nodeType === 1 && current !== document.documentElement) {
|
|
248
|
+
let part = current.tagName.toLowerCase();
|
|
249
|
+
|
|
250
|
+
if (current.id) {
|
|
251
|
+
parts.unshift(part + "#" + CSS.escape(current.id));
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
const parent: Element | null = current.parentElement;
|
|
255
|
+
|
|
256
|
+
if (parent) {
|
|
257
|
+
const currentTagName = current.tagName;
|
|
258
|
+
const twins = [...parent.children].filter((child) => child.tagName === currentTagName);
|
|
259
|
+
|
|
260
|
+
if (twins.length > 1) part += ":nth-of-type(" + (twins.indexOf(current) + 1) + ")";
|
|
261
|
+
}
|
|
262
|
+
parts.unshift(part);
|
|
263
|
+
current = current.parentElement;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return parts.join(" > ");
|
|
267
|
+
};
|
|
268
|
+
// Prefer the nearest focusable target before falling back to its component container.
|
|
269
|
+
const focusableTarget = (start: Element | null): Element | null => {
|
|
270
|
+
for (
|
|
271
|
+
let element: Element | null = start;
|
|
272
|
+
element && element !== document.body;
|
|
273
|
+
element = element.parentElement
|
|
274
|
+
) {
|
|
275
|
+
if (element instanceof HTMLElement && element.tabIndex >= 0) {
|
|
276
|
+
return element;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return null;
|
|
281
|
+
};
|
|
282
|
+
const hit = document.elementFromPoint(x, y);
|
|
283
|
+
|
|
284
|
+
if (!(hit instanceof Element)) return null;
|
|
285
|
+
const node = focusableTarget(hit) ?? componentRoot(hit);
|
|
286
|
+
const selector = selectorFor(node);
|
|
287
|
+
|
|
288
|
+
if (!selector) return null;
|
|
289
|
+
const text = (node.textContent || "").replace(/\s+/g, " ").trim();
|
|
290
|
+
const tag = node.tagName.toLowerCase();
|
|
291
|
+
const quote = text
|
|
292
|
+
? text.slice(0, 80)
|
|
293
|
+
: node.id
|
|
294
|
+
? tag + "#" + node.id
|
|
295
|
+
: node.classList.length
|
|
296
|
+
? tag + "." + node.classList[0]
|
|
297
|
+
: tag;
|
|
298
|
+
const rect = node.getBoundingClientRect();
|
|
299
|
+
|
|
300
|
+
return { selector, quote, box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function scrollByScript(deltaY: number): boolean {
|
|
304
|
+
const before = window.scrollY;
|
|
305
|
+
|
|
306
|
+
window.scrollBy(0, deltaY);
|
|
307
|
+
|
|
308
|
+
return window.scrollY !== before;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Standard Chrome install locations by platform; puppeteer-core ships no browser. */
|
|
312
|
+
function chromeExecutable(): string {
|
|
313
|
+
if (process.platform === "darwin") {
|
|
314
|
+
return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
315
|
+
}
|
|
316
|
+
if (process.platform === "win32") {
|
|
317
|
+
return "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return "/usr/bin/google-chrome";
|
|
321
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy boundary for the opt-in kitty pixel prototype. The pixel path pulls in the
|
|
3
|
+
* kitty image writer, the headless-browser renderer, and (on first paint) puppeteer;
|
|
4
|
+
* none of that belongs in the runtime unless the `[experimental] prototype_pixels`
|
|
5
|
+
* flag is on. Importing only the TYPE here is erased at build time, so the real
|
|
6
|
+
* module - and its dependencies - loads only when this component actually mounts,
|
|
7
|
+
* which App does exclusively for a pixel prototype. This lives outside components/
|
|
8
|
+
* on purpose: it is an app boundary, not a visual component the story catalog covers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useEffect, useState } from "react";
|
|
12
|
+
import type { PrototypeContentViewProps } from "./components/PrototypeContentView";
|
|
13
|
+
|
|
14
|
+
type PixelPrototypeView =
|
|
15
|
+
(typeof import("./components/PrototypeContentView"))["PrototypeContentView"];
|
|
16
|
+
|
|
17
|
+
export function PrototypePixels(props: PrototypeContentViewProps): React.ReactNode {
|
|
18
|
+
const [View, setView] = useState<PixelPrototypeView | null>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let alive = true;
|
|
22
|
+
|
|
23
|
+
void import("./components/PrototypeContentView").then((module) => {
|
|
24
|
+
if (alive) setView(() => module.PrototypeContentView);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
alive = false;
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
if (!View) return null;
|
|
33
|
+
|
|
34
|
+
return <View {...props} />;
|
|
35
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../appearance/theme";
|
|
3
|
+
import { THEME_PRESETS } from "../../appearance/theme-presets";
|
|
4
|
+
import type { Story, StoryMeta } from "../../stories/story";
|
|
5
|
+
import { SettingsDialog, type SettingsCategory } from "./SettingsDialog";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Overlays/SettingsDialog" };
|
|
8
|
+
|
|
9
|
+
const CATEGORIES: SettingsCategory[] = [
|
|
10
|
+
{
|
|
11
|
+
id: "general",
|
|
12
|
+
name: "General",
|
|
13
|
+
rows: [
|
|
14
|
+
{ key: "displayName", label: "Display name", kind: "text" },
|
|
15
|
+
{
|
|
16
|
+
key: "autoClose",
|
|
17
|
+
label: "Auto-close on submit",
|
|
18
|
+
kind: "cycle",
|
|
19
|
+
options: ["off", "3s", "on"],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "display",
|
|
25
|
+
name: "Display",
|
|
26
|
+
rows: [
|
|
27
|
+
{
|
|
28
|
+
key: "planWidth",
|
|
29
|
+
label: "Plan width",
|
|
30
|
+
kind: "cycle",
|
|
31
|
+
options: ["default", "wide", "full"],
|
|
32
|
+
},
|
|
33
|
+
{ key: "showLineNumbers", label: "Line numbers", kind: "toggle" },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "appearance",
|
|
38
|
+
name: "Appearance",
|
|
39
|
+
rows: [
|
|
40
|
+
{
|
|
41
|
+
key: "theme",
|
|
42
|
+
label: "Theme",
|
|
43
|
+
kind: "cycle",
|
|
44
|
+
options: ["cueloop", "Rosé Pine Moon", "Nord"],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const VALUES = {
|
|
51
|
+
displayName: "amber-heron",
|
|
52
|
+
autoClose: "3s",
|
|
53
|
+
planWidth: "default",
|
|
54
|
+
showLineNumbers: true,
|
|
55
|
+
theme: "Rosé Pine Moon",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const NavZone: Story = {
|
|
59
|
+
render: () => (
|
|
60
|
+
<SettingsDialog
|
|
61
|
+
isOpen
|
|
62
|
+
version="0.1.0-alpha.32"
|
|
63
|
+
keybindsSections={[]}
|
|
64
|
+
categories={CATEGORIES}
|
|
65
|
+
values={VALUES}
|
|
66
|
+
activeCategoryId="general"
|
|
67
|
+
activeRowIndex={0}
|
|
68
|
+
activeZone="nav"
|
|
69
|
+
onCategorySelect={() => {}}
|
|
70
|
+
onRowActivate={() => {}}
|
|
71
|
+
onClose={() => {}}
|
|
72
|
+
/>
|
|
73
|
+
),
|
|
74
|
+
expectedColors: [DARK.accent],
|
|
75
|
+
size: { width: 100, height: 30 },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const BodyZone: Story = {
|
|
79
|
+
render: () => (
|
|
80
|
+
<SettingsDialog
|
|
81
|
+
isOpen
|
|
82
|
+
version="0.1.0-alpha.32"
|
|
83
|
+
keybindsSections={[]}
|
|
84
|
+
categories={CATEGORIES}
|
|
85
|
+
values={VALUES}
|
|
86
|
+
activeCategoryId="display"
|
|
87
|
+
activeRowIndex={1}
|
|
88
|
+
activeZone="body"
|
|
89
|
+
onCategorySelect={() => {}}
|
|
90
|
+
onRowActivate={() => {}}
|
|
91
|
+
onClose={() => {}}
|
|
92
|
+
/>
|
|
93
|
+
),
|
|
94
|
+
expectedColors: [DARK.green],
|
|
95
|
+
size: { width: 100, height: 30 },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const AppearanceThemed: Story = {
|
|
99
|
+
render: () => (
|
|
100
|
+
<SettingsDialog
|
|
101
|
+
isOpen
|
|
102
|
+
version="0.1.0-alpha.32"
|
|
103
|
+
keybindsSections={[]}
|
|
104
|
+
categories={CATEGORIES}
|
|
105
|
+
values={VALUES}
|
|
106
|
+
activeCategoryId="appearance"
|
|
107
|
+
activeRowIndex={0}
|
|
108
|
+
activeZone="body"
|
|
109
|
+
onCategorySelect={() => {}}
|
|
110
|
+
onRowActivate={() => {}}
|
|
111
|
+
onClose={() => {}}
|
|
112
|
+
theme={THEME_PRESETS["rose-pine-moon"]}
|
|
113
|
+
/>
|
|
114
|
+
),
|
|
115
|
+
expectedColors: [THEME_PRESETS["rose-pine-moon"].accent],
|
|
116
|
+
size: { width: 100, height: 30 },
|
|
117
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings overlay: a centered Dialog whose left nav lists the categories as
|
|
3
|
+
* first-level leaves (General, Appearance, Actions, Keybinds), and whose right
|
|
4
|
+
* body shows the active category's typed rows or the keybinds cheatsheet. The
|
|
5
|
+
* component is controlled: the caller owns the open flag, the active
|
|
6
|
+
* category/row/zone, and value changes, so the app's keyboard grammar (and the
|
|
7
|
+
* stories catalog) drive it without a second key handler inside.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ScrollArea } from "../../ui/components/ScrollArea";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
13
|
+
import type { Theme } from "../../appearance/theme";
|
|
14
|
+
import type { CheatsheetSection } from "../../keyboard/key-bindings";
|
|
15
|
+
import { useComponentTheme } from "../../appearance/components/theme-context";
|
|
16
|
+
import { Dialog } from "../../ui/components/primitives/Dialog";
|
|
17
|
+
import { Tree } from "../../ui/components/primitives/Tree";
|
|
18
|
+
import type { TreeNode } from "../../ui/components/primitives/tree-model";
|
|
19
|
+
import { CycleRow, TextRow, ToggleRow, type SettingsRowDescriptor } from "./SettingsRows";
|
|
20
|
+
|
|
21
|
+
export interface SettingsCategory {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
rows: SettingsRowDescriptor[];
|
|
25
|
+
customBody?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type SettingsValues = Record<string, string | boolean>;
|
|
29
|
+
|
|
30
|
+
/** The category id whose right pane shows the keybinds cheatsheet rather than rows. */
|
|
31
|
+
const KEYBINDS_CATEGORY_ID = "keybinds";
|
|
32
|
+
|
|
33
|
+
export interface SettingsDialogProps {
|
|
34
|
+
isOpen: boolean;
|
|
35
|
+
version: string;
|
|
36
|
+
categories: SettingsCategory[];
|
|
37
|
+
values: SettingsValues;
|
|
38
|
+
keybindsSections: CheatsheetSection[];
|
|
39
|
+
activeCategoryId: string;
|
|
40
|
+
activeRowIndex: number;
|
|
41
|
+
activeZone: "nav" | "body";
|
|
42
|
+
onCategorySelect: (categoryId: string) => void;
|
|
43
|
+
onRowActivate: (row: SettingsRowDescriptor) => void;
|
|
44
|
+
onClose: () => void;
|
|
45
|
+
theme?: Theme;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function navTree(categories: SettingsCategory[]): TreeNode[] {
|
|
49
|
+
return categories.map((category) => ({ id: category.id, label: category.name }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function KeybindsBody({
|
|
53
|
+
sections,
|
|
54
|
+
tokens,
|
|
55
|
+
}: {
|
|
56
|
+
sections: CheatsheetSection[];
|
|
57
|
+
tokens: Theme;
|
|
58
|
+
}): React.ReactNode {
|
|
59
|
+
return (
|
|
60
|
+
<ScrollArea>
|
|
61
|
+
{sections.map((section) => (
|
|
62
|
+
<box key={section.title} style={{ flexDirection: "column" }}>
|
|
63
|
+
<text fg={tokens.accent}>{section.title}</text>
|
|
64
|
+
{section.entries.map((entry, index) => (
|
|
65
|
+
<text key={`${section.title}-${index}`}>
|
|
66
|
+
<span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
|
|
67
|
+
<span fg={tokens.textMuted}>{entry.label}</span>
|
|
68
|
+
</text>
|
|
69
|
+
))}
|
|
70
|
+
<text> </text>
|
|
71
|
+
</box>
|
|
72
|
+
))}
|
|
73
|
+
</ScrollArea>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function SettingsDialog({
|
|
78
|
+
isOpen,
|
|
79
|
+
version,
|
|
80
|
+
categories,
|
|
81
|
+
values,
|
|
82
|
+
keybindsSections,
|
|
83
|
+
activeCategoryId,
|
|
84
|
+
activeRowIndex,
|
|
85
|
+
activeZone,
|
|
86
|
+
onCategorySelect,
|
|
87
|
+
onRowActivate,
|
|
88
|
+
onClose,
|
|
89
|
+
theme,
|
|
90
|
+
}: SettingsDialogProps): React.ReactNode {
|
|
91
|
+
const tokens = useComponentTheme(theme);
|
|
92
|
+
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
93
|
+
|
|
94
|
+
if (!isOpen) return null;
|
|
95
|
+
const category =
|
|
96
|
+
categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
|
|
97
|
+
|
|
98
|
+
if (!category) return null;
|
|
99
|
+
const onKeybinds = category.id === KEYBINDS_CATEGORY_ID;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Dialog
|
|
103
|
+
isOpen
|
|
104
|
+
title=" Settings "
|
|
105
|
+
width={Math.min(76, terminalWidth - 6)}
|
|
106
|
+
height={Math.min(22, terminalHeight - 4)}
|
|
107
|
+
background={tokens.elevated}
|
|
108
|
+
onDismiss={onClose}
|
|
109
|
+
theme={theme}
|
|
110
|
+
>
|
|
111
|
+
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
112
|
+
<box style={{ flexDirection: "column", width: 20, paddingLeft: 1, paddingRight: 1 }}>
|
|
113
|
+
<Tree
|
|
114
|
+
nodes={navTree(categories)}
|
|
115
|
+
expandedIds={new Set<string>()}
|
|
116
|
+
selectedId={category.id}
|
|
117
|
+
hideIcons
|
|
118
|
+
selectedBackground={tokens.border}
|
|
119
|
+
onSelect={onCategorySelect}
|
|
120
|
+
onToggle={onCategorySelect}
|
|
121
|
+
theme={theme}
|
|
122
|
+
/>
|
|
123
|
+
</box>
|
|
124
|
+
<box style={{ borderStyle: "single", border: ["left"], borderColor: tokens.border }} />
|
|
125
|
+
<box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 1 }}>
|
|
126
|
+
{onKeybinds ? <KeybindsBody sections={keybindsSections} tokens={tokens} /> : null}
|
|
127
|
+
{onKeybinds ? null : category.customBody}
|
|
128
|
+
{onKeybinds
|
|
129
|
+
? null
|
|
130
|
+
: category.rows.map((row, rowIndex) => {
|
|
131
|
+
const isActive = activeZone === "body" && rowIndex === activeRowIndex;
|
|
132
|
+
const value = values[row.key];
|
|
133
|
+
|
|
134
|
+
if (row.kind === "toggle") {
|
|
135
|
+
return (
|
|
136
|
+
<ToggleRow
|
|
137
|
+
key={row.key}
|
|
138
|
+
label={row.label}
|
|
139
|
+
value={value === true}
|
|
140
|
+
isActive={isActive}
|
|
141
|
+
onPress={() => onRowActivate(row)}
|
|
142
|
+
theme={theme}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (row.kind === "cycle") {
|
|
147
|
+
return (
|
|
148
|
+
<CycleRow
|
|
149
|
+
key={row.key}
|
|
150
|
+
label={row.label}
|
|
151
|
+
value={String(value ?? "")}
|
|
152
|
+
isActive={isActive}
|
|
153
|
+
onPress={() => onRowActivate(row)}
|
|
154
|
+
theme={theme}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<TextRow
|
|
161
|
+
key={row.key}
|
|
162
|
+
label={row.label}
|
|
163
|
+
value={String(value ?? "")}
|
|
164
|
+
isActive={isActive}
|
|
165
|
+
onPress={() => onRowActivate(row)}
|
|
166
|
+
theme={theme}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
})}
|
|
170
|
+
</box>
|
|
171
|
+
</box>
|
|
172
|
+
<box style={{ flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1 }}>
|
|
173
|
+
<text fg={tokens.textDim}>
|
|
174
|
+
{activeZone === "nav"
|
|
175
|
+
? "j/k nav · l/tab open · esc close"
|
|
176
|
+
: "j/k row · l/space change · h back · esc close"}
|
|
177
|
+
</text>
|
|
178
|
+
<box style={{ flexGrow: 1 }} />
|
|
179
|
+
<text fg={tokens.textDim}>{`cueloop v${version}`}</text>
|
|
180
|
+
</box>
|
|
181
|
+
</Dialog>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../appearance/theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../../stories/story";
|
|
4
|
+
import { CycleRow, TextRow, ToggleRow } from "./SettingsRows";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/SettingsRows" };
|
|
7
|
+
|
|
8
|
+
export const AllKinds: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<box style={{ flexDirection: "column", width: 50 }}>
|
|
11
|
+
<ToggleRow label="Line numbers" value={true} isActive={false} onPress={() => {}} />
|
|
12
|
+
<ToggleRow label="Hide whitespace" value={false} isActive={true} onPress={() => {}} />
|
|
13
|
+
<CycleRow label="Palette" value="dark" isActive={false} onPress={() => {}} />
|
|
14
|
+
<TextRow label="Vault path" value="~/vaults/notes" isActive={false} onPress={() => {}} />
|
|
15
|
+
</box>
|
|
16
|
+
),
|
|
17
|
+
expectedColors: [DARK.green, DARK.accent, DARK.border],
|
|
18
|
+
};
|