magus-cli 0.0.0-placeholder → 7.0.0
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/README.md +309 -0
- package/bin/magus.js +170 -0
- package/package.json +67 -3
- package/scripts/build-binaries.ts +113 -0
- package/scripts/capture-builtin.ts +412 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +49 -0
- package/scripts/verify-binary.ts +95 -0
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/alias-adopt.test.ts +365 -0
- package/src/__tests__/alias-parser.test.ts +417 -0
- package/src/__tests__/alias-shell-writer.test.ts +681 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/binary-signing.test.ts +120 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cache-provenance.test.ts +84 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/catalog-cache-store.test.ts +309 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +288 -0
- package/src/__tests__/cli-live.test.ts +391 -0
- package/src/__tests__/cli-router.test.ts +240 -0
- package/src/__tests__/cli-tool-commands.test.ts +239 -0
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +445 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +720 -0
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/content-drift.test.ts +353 -0
- package/src/__tests__/conventions-integration.test.ts +830 -0
- package/src/__tests__/conventions-manager.test.ts +1236 -0
- package/src/__tests__/doctor-bins.test.ts +43 -0
- package/src/__tests__/doctor.test.ts +463 -0
- package/src/__tests__/dotenv.test.ts +195 -0
- package/src/__tests__/enabled-not-installed.test.ts +94 -0
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/footer-hints.test.ts +40 -0
- package/src/__tests__/format-command.test.ts +78 -0
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/github-budget.test.ts +206 -0
- package/src/__tests__/gitignore-check.test.ts +71 -0
- package/src/__tests__/gitignore-detector.test.ts +176 -0
- package/src/__tests__/gitignore-fixer.test.ts +317 -0
- package/src/__tests__/gitignore-resolver.test.ts +193 -0
- package/src/__tests__/gitignore-service.test.ts +137 -0
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +104 -0
- package/src/__tests__/launcher-signal.test.ts +308 -0
- package/src/__tests__/local-marketplace-scan.test.ts +174 -0
- package/src/__tests__/manifest.test.ts +281 -0
- package/src/__tests__/markdown-renderer.test.ts +142 -0
- package/src/__tests__/marketplace-badge.test.ts +140 -0
- package/src/__tests__/marketplace-refresh.test.ts +335 -0
- package/src/__tests__/marketplace-version.test.ts +84 -0
- package/src/__tests__/marketplaces.test.ts +128 -0
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/mcp-registry.test.ts +158 -0
- package/src/__tests__/model-visuals.test.tsx +2466 -0
- package/src/__tests__/models-adapter.test.ts +332 -0
- package/src/__tests__/models-cli.test.ts +273 -0
- package/src/__tests__/models-core.test.ts +802 -0
- package/src/__tests__/models-manager.test.ts +500 -0
- package/src/__tests__/models-presets-marketplace.test.ts +168 -0
- package/src/__tests__/models-screen-state.test.ts +315 -0
- package/src/__tests__/moved-marketplace.test.ts +86 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/owned-marketplaces.test.ts +93 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-cli-argv.test.ts +109 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
- package/src/__tests__/plugin-releases.test.ts +98 -0
- package/src/__tests__/plugin-requires.test.ts +187 -0
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/plugin-setup.test.ts +1016 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/profile-command.test.ts +110 -0
- package/src/__tests__/profile-materializer.test.ts +238 -0
- package/src/__tests__/profile-sync.test.ts +154 -0
- package/src/__tests__/registry-version-resolution.test.ts +88 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +351 -0
- package/src/__tests__/scope-action.test.ts +134 -0
- package/src/__tests__/scope-squares.test.tsx +164 -0
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/shell-script-callers.test.ts +92 -0
- package/src/__tests__/skill-install-files.test.ts +136 -0
- package/src/__tests__/skills-catalog-client.test.ts +48 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1098 -0
- package/src/__tests__/styles-origins.test.ts +414 -0
- package/src/__tests__/styles-screen-state.test.ts +457 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/symlink-manager.test.ts +180 -0
- package/src/__tests__/tabbar-layout.test.ts +100 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/toolchain.test.ts +232 -0
- package/src/__tests__/ui-version-writers.test.ts +88 -0
- package/src/__tests__/update-apply.test.ts +637 -0
- package/src/__tests__/update-plan.test.ts +1373 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/useGitignoreModal.test.ts +79 -0
- package/src/__tests__/version-check-refresh.test.ts +45 -0
- package/src/__tests__/version-snapshot.test.ts +212 -0
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +520 -0
- package/src/cli/bootstrap.ts +134 -0
- package/src/cli/doctor.ts +218 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/install.ts +382 -0
- package/src/cli/live.ts +374 -0
- package/src/cli/models.ts +297 -0
- package/src/cli/profile.ts +197 -0
- package/src/cli/prompt.ts +32 -0
- package/src/cli/router.ts +207 -0
- package/src/cli/update-view.ts +526 -0
- package/src/cli/update.ts +613 -0
- package/src/cli/upgrade.ts +97 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/cli-tools.ts +146 -0
- package/src/data/community-styles.ts +531 -0
- package/src/data/gitignore-defaults.ts +33 -0
- package/src/data/gitignore-reasons.ts +93 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.ts +278 -0
- package/src/data/mcp-servers.ts +51 -0
- package/src/data/models-presets.ts +329 -0
- package/src/data/predefined-profiles.ts +252 -0
- package/src/data/settings-catalog.ts +714 -0
- package/src/data/skill-repos.ts +91 -0
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/main.tsx +110 -0
- package/src/markdown.d.ts +13 -0
- package/src/opentui.d.ts +203 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/alias-settings.ts +60 -0
- package/src/services/alias-shell-writer.ts +1244 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/binary-signing.ts +78 -0
- package/src/services/catalog-cache-store.ts +316 -0
- package/src/services/claude-cli.ts +304 -0
- package/src/services/claude-runner.ts +31 -0
- package/src/services/claude-settings.ts +1589 -0
- package/src/services/cli-tool-commands.ts +133 -0
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/content-drift.ts +208 -0
- package/src/services/conventions-manager.ts +945 -0
- package/src/services/doctor-bins.ts +63 -0
- package/src/services/doctor.ts +570 -0
- package/src/services/dotenv.ts +181 -0
- package/src/services/git-worktree.ts +129 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/gitignore-check.ts +63 -0
- package/src/services/gitignore-detector.ts +167 -0
- package/src/services/gitignore-fixer.ts +280 -0
- package/src/services/gitignore-resolver.ts +208 -0
- package/src/services/gitignore-service.ts +167 -0
- package/src/services/hook-registration.ts +218 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/local-marketplace.ts +587 -0
- package/src/services/manifest.ts +250 -0
- package/src/services/marketplace-catalog-git.ts +173 -0
- package/src/services/marketplace-catalog.ts +129 -0
- package/src/services/marketplace-fetcher.ts +400 -0
- package/src/services/marketplace-refresh.ts +253 -0
- package/src/services/marketplace-sync.ts +145 -0
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/mcp-registry.ts +171 -0
- package/src/services/models-core.ts +969 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +878 -0
- package/src/services/plugin-mcp-config.ts +242 -0
- package/src/services/plugin-releases.ts +47 -0
- package/src/services/plugin-requires.ts +227 -0
- package/src/services/plugin-setup.ts +965 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/profile-materializer.ts +133 -0
- package/src/services/profile-sync.ts +158 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/resolver.ts +344 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/settings-manager.ts +307 -0
- package/src/services/skills-manager.ts +1111 -0
- package/src/services/skillsmp-client.ts +193 -0
- package/src/services/styles-manager.ts +1294 -0
- package/src/services/symlink-manager.ts +189 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +431 -0
- package/src/services/update-engine.ts +691 -0
- package/src/services/update-plan.ts +863 -0
- package/src/services/version-check.ts +127 -0
- package/src/services/version-snapshot.ts +189 -0
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +27 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +447 -0
- package/src/ui/App.tsx +538 -0
- package/src/ui/adapters/catalogNotice.ts +122 -0
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/modelsAdapter.ts +213 -0
- package/src/ui/adapters/pluginsAdapter.ts +413 -0
- package/src/ui/adapters/settingsAdapter.ts +168 -0
- package/src/ui/adapters/skillsAdapter.ts +229 -0
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/CategoryHeader.tsx +64 -0
- package/src/ui/components/EmptyFilterState.tsx +32 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +63 -0
- package/src/ui/components/ScrollableList.tsx +95 -0
- package/src/ui/components/SearchInput.tsx +57 -0
- package/src/ui/components/StyledText.tsx +75 -0
- package/src/ui/components/TabBar.tsx +101 -0
- package/src/ui/components/layout/FooterHints.tsx +78 -0
- package/src/ui/components/layout/Panel.tsx +63 -0
- package/src/ui/components/layout/ProgressBar.tsx +44 -0
- package/src/ui/components/layout/ScopeTabs.tsx +54 -0
- package/src/ui/components/layout/ScreenLayout.tsx +321 -0
- package/src/ui/components/layout/index.ts +9 -0
- package/src/ui/components/modals/ConfirmModal.tsx +62 -0
- package/src/ui/components/modals/InputModal.tsx +75 -0
- package/src/ui/components/modals/LoadingModal.tsx +51 -0
- package/src/ui/components/modals/MessageModal.tsx +66 -0
- package/src/ui/components/modals/ModalContainer.tsx +159 -0
- package/src/ui/components/modals/SelectModal.tsx +90 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/components/primitives/ActionHints.tsx +44 -0
- package/src/ui/components/primitives/DetailSection.tsx +23 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
- package/src/ui/components/primitives/MetaText.tsx +40 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
- package/src/ui/components/primitives/SelectableRow.tsx +30 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/hooks/index.ts +2 -0
- package/src/ui/hooks/useGitignoreModal.ts +95 -0
- package/src/ui/hooks/useKeyboard.ts +24 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/src/ui/registry.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +326 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/mcpRenderers.tsx +149 -0
- package/src/ui/renderers/modelRenderers.tsx +1309 -0
- package/src/ui/renderers/modelVisuals.tsx +1402 -0
- package/src/ui/renderers/pluginRenderers.tsx +1089 -0
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/renderers/profileRenderers.tsx +406 -0
- package/src/ui/renderers/settingsRenderers.tsx +218 -0
- package/src/ui/renderers/skillRenderers.tsx +479 -0
- package/src/ui/renderers/styleRenderers.tsx +813 -0
- package/src/ui/screens/AliasScreen.tsx +1558 -0
- package/src/ui/screens/CliToolsScreen.tsx +367 -0
- package/src/ui/screens/EnvVarsScreen.tsx +221 -0
- package/src/ui/screens/GitignoreScreen.tsx +562 -0
- package/src/ui/screens/McpRegistryScreen.tsx +366 -0
- package/src/ui/screens/McpScreen.tsx +256 -0
- package/src/ui/screens/ModelsScreen.tsx +542 -0
- package/src/ui/screens/PluginsScreen.tsx +1464 -0
- package/src/ui/screens/ProfilesScreen.tsx +438 -0
- package/src/ui/screens/SkillsScreen.tsx +853 -0
- package/src/ui/screens/StylesScreen.tsx +1064 -0
- package/src/ui/screens/index.ts +11 -0
- package/src/ui/state/AppContext.tsx +233 -0
- package/src/ui/state/DimensionsContext.tsx +154 -0
- package/src/ui/state/reducer.ts +810 -0
- package/src/ui/state/types.ts +503 -0
- package/src/ui/theme-mode.ts +352 -0
- package/src/ui/theme-resolve.ts +147 -0
- package/src/ui/theme.ts +175 -0
- package/src/utils/clipboard.ts +58 -0
- package/src/utils/command-utils.ts +144 -0
- package/src/utils/config-dir.ts +45 -0
- package/src/utils/file-locking.ts +146 -0
- package/src/utils/fuzzy-search.ts +138 -0
- package/src/utils/go-module.ts +36 -0
- package/src/utils/open-file.ts +84 -0
- package/src/utils/run.ts +67 -0
- package/src/utils/string-utils.ts +88 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { useKeyboard } from "../../hooks/useKeyboard.js";
|
|
5
|
+
import { useApp } from "../../state/AppContext.js";
|
|
6
|
+
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
7
|
+
import type { Screen } from "../../state/types.js";
|
|
8
|
+
import { theme } from "../../theme.js";
|
|
9
|
+
import { TabBar } from "../TabBar.js";
|
|
10
|
+
import { type FooterHint, FooterHints } from "./FooterHints.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Panel content: a node, or a function handed the panel's usable columns.
|
|
14
|
+
*
|
|
15
|
+
* The function form exists because the geometry below — the app frame's side
|
|
16
|
+
* padding, the 49/50 split, the panel padding and the scrollbar column — is
|
|
17
|
+
* ScreenLayout's, and a screen that re-derives it from `terminalWidth` is
|
|
18
|
+
* guessing at constants it cannot see. Both screens that wrap body text did
|
|
19
|
+
* exactly that, with the same hand-copied formula, and both were wrong.
|
|
20
|
+
*
|
|
21
|
+
* `React.ReactNode` does not include functions, so the union is unambiguous.
|
|
22
|
+
*/
|
|
23
|
+
export type PanelContent =
|
|
24
|
+
| React.ReactNode
|
|
25
|
+
| ((width: number) => React.ReactNode);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The app frame (App.tsx) pads the screen one column each side before
|
|
29
|
+
* ScreenLayout ever renders, so every percentage below resolves against
|
|
30
|
+
* `terminalWidth - 2`.
|
|
31
|
+
*/
|
|
32
|
+
const APP_SIDE_PADDING = 2;
|
|
33
|
+
const LIST_FRACTION = 0.49;
|
|
34
|
+
const DETAIL_FRACTION = 0.5;
|
|
35
|
+
/** The list panel's own `paddingRight`. */
|
|
36
|
+
const LIST_CHROME = 1;
|
|
37
|
+
/** The detail panel's `paddingLeft`, plus the scrollbox's scrollbar column. */
|
|
38
|
+
const DETAIL_CHROME = 2;
|
|
39
|
+
|
|
40
|
+
/** Columns a panel's content can actually use, at this terminal width. */
|
|
41
|
+
function panelWidth(
|
|
42
|
+
terminalWidth: number,
|
|
43
|
+
fraction: number,
|
|
44
|
+
chrome: number,
|
|
45
|
+
): number {
|
|
46
|
+
const inner = Math.max(1, terminalWidth - APP_SIDE_PADDING);
|
|
47
|
+
return Math.max(8, Math.floor(inner * fraction) - chrome);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Usable columns in the list panel.
|
|
52
|
+
*
|
|
53
|
+
* MEASURED against real renders at 70…200 columns, not derived from the JSX:
|
|
54
|
+
* Yoga rounds a fractional percentage to whole cells, so the arithmetic only
|
|
55
|
+
* matches the pixels if you check. This is exact at 8 of the 12 widths sampled
|
|
56
|
+
* and one column short at the other four, which is the safe direction — a
|
|
57
|
+
* budget one column under folds nothing, a budget one column over paints into
|
|
58
|
+
* the separator.
|
|
59
|
+
*/
|
|
60
|
+
export function listPanelWidth(terminalWidth: number): number {
|
|
61
|
+
return panelWidth(terminalWidth, LIST_FRACTION, LIST_CHROME);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Usable columns in the detail panel, scrollbar excluded.
|
|
66
|
+
*
|
|
67
|
+
* Exact at 11 of the 12 widths sampled, one column short at the twelfth. The
|
|
68
|
+
* formula each screen used to carry instead was `floor(width * 0.5) - 4`: it
|
|
69
|
+
* forgot the app frame's padding, counted the scrollbar twice, and then floored
|
|
70
|
+
* at 24 — so below about 56 columns it asked for MORE room than the panel has.
|
|
71
|
+
*/
|
|
72
|
+
export function detailPanelWidth(terminalWidth: number): number {
|
|
73
|
+
return panelWidth(terminalWidth, DETAIL_FRACTION, DETAIL_CHROME);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ScreenLayoutProps {
|
|
77
|
+
/** Screen title (e.g., "magus Plugins") */
|
|
78
|
+
title: string;
|
|
79
|
+
/** Optional subtitle shown to the right of title */
|
|
80
|
+
subtitle?: string;
|
|
81
|
+
/** Current screen for tab highlighting */
|
|
82
|
+
currentScreen: Screen;
|
|
83
|
+
/** Search bar configuration (for screens with search) */
|
|
84
|
+
search?: {
|
|
85
|
+
/** Is search currently active */
|
|
86
|
+
isActive: boolean;
|
|
87
|
+
/** Current search query */
|
|
88
|
+
query: string;
|
|
89
|
+
/** Placeholder when not searching (default: "/") */
|
|
90
|
+
placeholder?: string;
|
|
91
|
+
};
|
|
92
|
+
/** Status line content (for screens without search) - shown in second row */
|
|
93
|
+
statusLine?: React.ReactNode;
|
|
94
|
+
/**
|
|
95
|
+
* One-line warning shown under the header, above the content.
|
|
96
|
+
*
|
|
97
|
+
* For conditions that make the data on screen untrustworthy — not for
|
|
98
|
+
* transient progress. It sits in the fixed chrome rather than in a panel so it
|
|
99
|
+
* cannot be scrolled out of view, and it takes a row from the panels only
|
|
100
|
+
* while it is present.
|
|
101
|
+
*/
|
|
102
|
+
notice?: React.ReactNode;
|
|
103
|
+
/** Footer hints (left side). Pass a FooterHint[] to render key badges
|
|
104
|
+
* (the standard look). A string or ReactNode is still accepted for
|
|
105
|
+
* free-form footers. */
|
|
106
|
+
footerHints: FooterHint[] | string | React.ReactNode;
|
|
107
|
+
/** Left panel content, or a function given the list panel's usable columns. */
|
|
108
|
+
listPanel: PanelContent;
|
|
109
|
+
/** Right panel content, or a function given the detail panel's usable columns. */
|
|
110
|
+
detailPanel: PanelContent;
|
|
111
|
+
/**
|
|
112
|
+
* Identity of the detail content, e.g. the selected row's id. When it
|
|
113
|
+
* changes, the detail pane scrolls back to the top — a reading position
|
|
114
|
+
* belongs to one document, not to the panel.
|
|
115
|
+
*/
|
|
116
|
+
detailKey?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const HEADER_COLOR = theme.colors.accent;
|
|
120
|
+
|
|
121
|
+
export function ScreenLayout({
|
|
122
|
+
title,
|
|
123
|
+
subtitle,
|
|
124
|
+
currentScreen,
|
|
125
|
+
search,
|
|
126
|
+
statusLine,
|
|
127
|
+
notice,
|
|
128
|
+
footerHints,
|
|
129
|
+
listPanel,
|
|
130
|
+
detailPanel,
|
|
131
|
+
detailKey,
|
|
132
|
+
}: ScreenLayoutProps) {
|
|
133
|
+
const dimensions = useDimensions();
|
|
134
|
+
const { state } = useApp();
|
|
135
|
+
|
|
136
|
+
const hasSearchBar = search && (search.isActive || search.query);
|
|
137
|
+
|
|
138
|
+
// Fixed chrome: top line + tabs + line + header + separator + footer = 6
|
|
139
|
+
// Search bar adds 1 when active; the notice adds 1 while present.
|
|
140
|
+
const fixedHeight = 6 + (hasSearchBar ? 1 : 0) + (notice ? 1 : 0);
|
|
141
|
+
const panelHeight = Math.max(5, dimensions.contentHeight - fixedHeight);
|
|
142
|
+
const lineWidth = Math.max(10, dimensions.terminalWidth - 4);
|
|
143
|
+
|
|
144
|
+
const listContent =
|
|
145
|
+
typeof listPanel === "function"
|
|
146
|
+
? listPanel(listPanelWidth(dimensions.terminalWidth))
|
|
147
|
+
: listPanel;
|
|
148
|
+
const detailContent =
|
|
149
|
+
typeof detailPanel === "function"
|
|
150
|
+
? detailPanel(detailPanelWidth(dimensions.terminalWidth))
|
|
151
|
+
: detailPanel;
|
|
152
|
+
|
|
153
|
+
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
154
|
+
|
|
155
|
+
// The detail pane has no cursor, so it needs its own scroll keys: arrows
|
|
156
|
+
// move the LIST selection, and the wheel only helps when the terminal
|
|
157
|
+
// delivers mouse reports (see src/main.tsx). PgUp/PgDn page with two rows
|
|
158
|
+
// of overlap so a page turn doesn't lose the reader's place; Ctrl+U/Ctrl+D
|
|
159
|
+
// half-page in the pager tradition — and exist because a MacBook keyboard
|
|
160
|
+
// hides PgDn behind Fn+↓, where nobody finds it.
|
|
161
|
+
const detailPageStep = Math.max(1, panelHeight - 2);
|
|
162
|
+
const detailHalfStep = Math.max(1, Math.floor(panelHeight / 2));
|
|
163
|
+
useKeyboard((event) => {
|
|
164
|
+
if (state.modal) return;
|
|
165
|
+
const ctrl = event.ctrl === true;
|
|
166
|
+
if (event.name === "pageup" || (ctrl && event.name === "u")) {
|
|
167
|
+
detailScrollRef.current?.scrollBy(
|
|
168
|
+
event.name === "pageup" ? -detailPageStep : -detailHalfStep,
|
|
169
|
+
);
|
|
170
|
+
} else if (event.name === "pagedown" || (ctrl && event.name === "d")) {
|
|
171
|
+
detailScrollRef.current?.scrollBy(
|
|
172
|
+
event.name === "pagedown" ? detailPageStep : detailHalfStep,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (detailKey !== undefined) detailScrollRef.current?.scrollTo(0);
|
|
179
|
+
}, [detailKey]);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<box flexDirection="column" height={dimensions.contentHeight}>
|
|
183
|
+
{/* Line above tabs */}
|
|
184
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
185
|
+
<text fg={theme.colors.dim}>{"─".repeat(lineWidth)}</text>
|
|
186
|
+
</box>
|
|
187
|
+
|
|
188
|
+
{/* Tab bar */}
|
|
189
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
190
|
+
<TabBar currentScreen={currentScreen} />
|
|
191
|
+
</box>
|
|
192
|
+
|
|
193
|
+
{/* Line below tabs */}
|
|
194
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
195
|
+
<text fg={theme.colors.dim}>{"─".repeat(lineWidth)}</text>
|
|
196
|
+
</box>
|
|
197
|
+
|
|
198
|
+
{/* Header — title on left, subtitle/status on right */}
|
|
199
|
+
<box
|
|
200
|
+
height={1}
|
|
201
|
+
paddingLeft={1}
|
|
202
|
+
paddingRight={1}
|
|
203
|
+
flexDirection="row"
|
|
204
|
+
justifyContent="space-between"
|
|
205
|
+
>
|
|
206
|
+
{/* The title never yields: an overlong right-hand status once
|
|
207
|
+
shrank it away and the two texts read as one sentence. Long
|
|
208
|
+
content belongs in the `notice` row, not beside the title. */}
|
|
209
|
+
<box flexShrink={0} marginRight={2}>
|
|
210
|
+
<text fg={HEADER_COLOR}>
|
|
211
|
+
<strong>{title}</strong>
|
|
212
|
+
</text>
|
|
213
|
+
</box>
|
|
214
|
+
{subtitle && <text fg={theme.colors.muted}>{subtitle}</text>}
|
|
215
|
+
{!subtitle && statusLine ? statusLine : null}
|
|
216
|
+
</box>
|
|
217
|
+
|
|
218
|
+
{/* Search bar — own line, only when active */}
|
|
219
|
+
{hasSearchBar && (
|
|
220
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
221
|
+
{search.isActive ? (
|
|
222
|
+
<text fg={theme.colors.text}>
|
|
223
|
+
<span fg={theme.colors.success}>{"Filter: "}</span>
|
|
224
|
+
<span fg={theme.colors.text}>{search.query}</span>
|
|
225
|
+
<span bg={theme.colors.text} fg={theme.hints.fg}>
|
|
226
|
+
{" "}
|
|
227
|
+
</span>
|
|
228
|
+
</text>
|
|
229
|
+
) : (
|
|
230
|
+
<text fg={theme.colors.text}>
|
|
231
|
+
<span fg={theme.colors.success}>{"Filter: "}</span>
|
|
232
|
+
<span fg={theme.colors.warning}>{search.query}</span>
|
|
233
|
+
<span fg={theme.colors.muted}> (Esc to clear)</span>
|
|
234
|
+
</text>
|
|
235
|
+
)}
|
|
236
|
+
</box>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Warning line — only when the data on screen cannot be trusted */}
|
|
240
|
+
{notice && (
|
|
241
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
242
|
+
{notice}
|
|
243
|
+
</box>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{/* Separator below header */}
|
|
247
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
248
|
+
<text fg={theme.colors.border}>{"─".repeat(lineWidth)}</text>
|
|
249
|
+
</box>
|
|
250
|
+
|
|
251
|
+
{/* Main content area */}
|
|
252
|
+
<box flexDirection="row" height={panelHeight}>
|
|
253
|
+
{/* List panel. `overflow="hidden"` is the safety net, not the plan:
|
|
254
|
+
the box has a fixed height, so content taller than it does NOT
|
|
255
|
+
scroll and does NOT clip on its own — Yoga shrinks the children
|
|
256
|
+
and they composite on top of each other. MEASURED at 80×24:
|
|
257
|
+
"fable-advisor" and "fable-lead" rendered as "fable-leadical".
|
|
258
|
+
A screen that can overflow should budget its own rows; this
|
|
259
|
+
stops the failure being unreadable when one does not. */}
|
|
260
|
+
<box
|
|
261
|
+
flexDirection="column"
|
|
262
|
+
width="49%"
|
|
263
|
+
height={panelHeight}
|
|
264
|
+
paddingRight={1}
|
|
265
|
+
overflow="hidden"
|
|
266
|
+
>
|
|
267
|
+
{listContent}
|
|
268
|
+
</box>
|
|
269
|
+
|
|
270
|
+
{/* Vertical separator */}
|
|
271
|
+
<box flexDirection="column" width={1} height={panelHeight}>
|
|
272
|
+
<text fg={theme.colors.border}>{"│".repeat(panelHeight)}</text>
|
|
273
|
+
</box>
|
|
274
|
+
|
|
275
|
+
{/* Detail panel — scrollable. The thumb is themed so the reader can
|
|
276
|
+
see there is more below the fold; the track keeps the library
|
|
277
|
+
default so it stays chrome, not signal. */}
|
|
278
|
+
{/* `overflow: hidden`, like the list panel opposite. Without it a line wider
|
|
279
|
+
than the panel is drawn OUTSIDE it, over the terminal's right edge, and the
|
|
280
|
+
reader loses characters off the end of a sentence with nothing to say they
|
|
281
|
+
were lost. Clipping at the panel is the guarantee; wrapping to
|
|
282
|
+
`detailPanelWidth` is what keeps there being nothing to clip. */}
|
|
283
|
+
<box width="50%" height={panelHeight} paddingLeft={1} overflow="hidden">
|
|
284
|
+
<scrollbox
|
|
285
|
+
ref={detailScrollRef}
|
|
286
|
+
height={panelHeight}
|
|
287
|
+
scrollY={true}
|
|
288
|
+
scrollX={false}
|
|
289
|
+
verticalScrollbarOptions={{
|
|
290
|
+
showArrows: false,
|
|
291
|
+
trackOptions: {
|
|
292
|
+
foregroundColor: theme.colors.muted,
|
|
293
|
+
},
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
<box flexDirection="column">{detailContent}</box>
|
|
297
|
+
</scrollbox>
|
|
298
|
+
</box>
|
|
299
|
+
</box>
|
|
300
|
+
|
|
301
|
+
{/* Footer — the layout appends its own scroll hint so every screen
|
|
302
|
+
advertises the detail-pane keys without each screen repeating it */}
|
|
303
|
+
<box height={1} paddingLeft={1}>
|
|
304
|
+
{Array.isArray(footerHints) ? (
|
|
305
|
+
<FooterHints
|
|
306
|
+
hints={[
|
|
307
|
+
...(footerHints as FooterHint[]),
|
|
308
|
+
{ keys: ["^U/^D", "PgUp/Dn"], label: "scroll" },
|
|
309
|
+
]}
|
|
310
|
+
/>
|
|
311
|
+
) : typeof footerHints === "string" ? (
|
|
312
|
+
<text fg={theme.colors.muted}>{footerHints}</text>
|
|
313
|
+
) : (
|
|
314
|
+
footerHints
|
|
315
|
+
)}
|
|
316
|
+
</box>
|
|
317
|
+
</box>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export default ScreenLayout;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { Panel } from "./Panel.js";
|
|
2
|
+
export { ScopeTabs } from "./ScopeTabs.js";
|
|
3
|
+
export { ProgressBar } from "./ProgressBar.js";
|
|
4
|
+
export { ScreenLayout } from "./ScreenLayout.js";
|
|
5
|
+
export {
|
|
6
|
+
FooterHints,
|
|
7
|
+
renderFooterHints,
|
|
8
|
+
type FooterHint,
|
|
9
|
+
} from "./FooterHints.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useKeyboard } from "../../hooks/useKeyboard.js";
|
|
3
|
+
import { theme } from "../../theme.js";
|
|
4
|
+
|
|
5
|
+
interface ConfirmModalProps {
|
|
6
|
+
/** Modal title */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Modal message */
|
|
9
|
+
message: string;
|
|
10
|
+
/** Callback when confirmed */
|
|
11
|
+
onConfirm: () => void;
|
|
12
|
+
/** Callback when cancelled */
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ConfirmModal({
|
|
17
|
+
title,
|
|
18
|
+
message,
|
|
19
|
+
onConfirm,
|
|
20
|
+
onCancel,
|
|
21
|
+
}: ConfirmModalProps) {
|
|
22
|
+
useKeyboard((key) => {
|
|
23
|
+
if (key.name === "y") {
|
|
24
|
+
onConfirm();
|
|
25
|
+
} else if (key.name === "n" || key.name === "escape") {
|
|
26
|
+
onCancel();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<box
|
|
32
|
+
flexDirection="column"
|
|
33
|
+
border
|
|
34
|
+
borderStyle="rounded"
|
|
35
|
+
borderColor={theme.colors.dim}
|
|
36
|
+
backgroundColor={theme.surface.bg}
|
|
37
|
+
paddingLeft={3}
|
|
38
|
+
paddingRight={3}
|
|
39
|
+
paddingTop={1}
|
|
40
|
+
paddingBottom={1}
|
|
41
|
+
width={60}
|
|
42
|
+
>
|
|
43
|
+
<box marginBottom={1}>
|
|
44
|
+
<text fg={theme.colors.text}>
|
|
45
|
+
<strong>{title}</strong>
|
|
46
|
+
</text>
|
|
47
|
+
</box>
|
|
48
|
+
<box marginBottom={1}>
|
|
49
|
+
<text fg={theme.colors.muted}>{message}</text>
|
|
50
|
+
</box>
|
|
51
|
+
<box marginTop={1} flexDirection="row">
|
|
52
|
+
<text fg={theme.colors.muted}>Press </text>
|
|
53
|
+
<text fg={theme.colors.text}>Y</text>
|
|
54
|
+
<text fg={theme.colors.muted}> to confirm • </text>
|
|
55
|
+
<text fg={theme.colors.text}>N</text>
|
|
56
|
+
<text fg={theme.colors.muted}> to cancel</text>
|
|
57
|
+
</box>
|
|
58
|
+
</box>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default ConfirmModal;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SubmitEvent } from "@opentui/core";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { theme } from "../../theme.js";
|
|
4
|
+
|
|
5
|
+
interface InputModalProps {
|
|
6
|
+
/** Modal title */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Input label */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Default input value */
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
/** Callback when submitted */
|
|
13
|
+
onSubmit: (value: string) => void;
|
|
14
|
+
/** Callback when cancelled */
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function InputModal({
|
|
19
|
+
title,
|
|
20
|
+
label,
|
|
21
|
+
defaultValue = "",
|
|
22
|
+
onSubmit,
|
|
23
|
+
}: InputModalProps) {
|
|
24
|
+
return (
|
|
25
|
+
<box
|
|
26
|
+
flexDirection="column"
|
|
27
|
+
border
|
|
28
|
+
borderStyle="rounded"
|
|
29
|
+
borderColor={theme.colors.dim}
|
|
30
|
+
backgroundColor={theme.surface.bg}
|
|
31
|
+
paddingLeft={3}
|
|
32
|
+
paddingRight={3}
|
|
33
|
+
paddingTop={1}
|
|
34
|
+
paddingBottom={1}
|
|
35
|
+
width={60}
|
|
36
|
+
>
|
|
37
|
+
<box marginBottom={1}>
|
|
38
|
+
<text fg={theme.colors.text}>
|
|
39
|
+
<strong>{title}</strong>
|
|
40
|
+
</text>
|
|
41
|
+
</box>
|
|
42
|
+
|
|
43
|
+
<box marginBottom={1}>
|
|
44
|
+
<text fg={theme.colors.muted}>{label}</text>
|
|
45
|
+
</box>
|
|
46
|
+
|
|
47
|
+
<box
|
|
48
|
+
border
|
|
49
|
+
borderStyle="rounded"
|
|
50
|
+
borderColor={theme.colors.dim}
|
|
51
|
+
paddingLeft={1}
|
|
52
|
+
paddingRight={1}
|
|
53
|
+
width={54}
|
|
54
|
+
>
|
|
55
|
+
<input
|
|
56
|
+
value={defaultValue}
|
|
57
|
+
onSubmit={(value: string | SubmitEvent) => {
|
|
58
|
+
// @opentui/react types the prop as an intersection of the core
|
|
59
|
+
// `(event: SubmitEvent) => void` and its own `(value: string) => void`;
|
|
60
|
+
// at runtime the input hands over the string.
|
|
61
|
+
if (typeof value === "string") onSubmit(value);
|
|
62
|
+
}}
|
|
63
|
+
focused
|
|
64
|
+
width={50}
|
|
65
|
+
/>
|
|
66
|
+
</box>
|
|
67
|
+
|
|
68
|
+
<box marginTop={1}>
|
|
69
|
+
<text fg={theme.colors.muted}>↵ to confirm • Esc to cancel</text>
|
|
70
|
+
</box>
|
|
71
|
+
</box>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default InputModal;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
interface LoadingModalProps {
|
|
5
|
+
/** Loading message — supports multiline (first line = status, subsequent = detail) */
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
10
|
+
|
|
11
|
+
export function LoadingModal({ message }: LoadingModalProps) {
|
|
12
|
+
const [frame, setFrame] = useState(0);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const interval = setInterval(() => {
|
|
16
|
+
setFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
|
|
17
|
+
}, 80);
|
|
18
|
+
|
|
19
|
+
return () => clearInterval(interval);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
const lines = message.split("\n");
|
|
23
|
+
const mainMessage = lines[0];
|
|
24
|
+
const detail = lines.slice(1).join("\n");
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<box
|
|
28
|
+
flexDirection="column"
|
|
29
|
+
border
|
|
30
|
+
borderStyle="rounded"
|
|
31
|
+
borderColor={theme.colors.dim}
|
|
32
|
+
backgroundColor={theme.surface.bg}
|
|
33
|
+
paddingLeft={3}
|
|
34
|
+
paddingRight={3}
|
|
35
|
+
paddingTop={1}
|
|
36
|
+
paddingBottom={1}
|
|
37
|
+
>
|
|
38
|
+
<box flexDirection="row">
|
|
39
|
+
<text fg={theme.colors.muted}>{SPINNER_FRAMES[frame]}</text>
|
|
40
|
+
<text fg={theme.colors.text}> {mainMessage}</text>
|
|
41
|
+
</box>
|
|
42
|
+
{detail ? (
|
|
43
|
+
<box marginTop={0} paddingLeft={2}>
|
|
44
|
+
<text fg={theme.colors.dim}>{detail}</text>
|
|
45
|
+
</box>
|
|
46
|
+
) : null}
|
|
47
|
+
</box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default LoadingModal;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useKeyboard } from "../../hooks/useKeyboard.js";
|
|
3
|
+
import { theme } from "../../theme.js";
|
|
4
|
+
|
|
5
|
+
interface MessageModalProps {
|
|
6
|
+
/** Modal title */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Modal message */
|
|
9
|
+
message: string;
|
|
10
|
+
/** Message variant */
|
|
11
|
+
variant: "info" | "success" | "error";
|
|
12
|
+
/** Callback when dismissed */
|
|
13
|
+
onDismiss: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const variantConfig = {
|
|
17
|
+
info: { icon: "ℹ", color: theme.colors.link },
|
|
18
|
+
success: { icon: "✓", color: theme.colors.success },
|
|
19
|
+
error: { icon: "✗", color: theme.colors.danger },
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export function MessageModal({
|
|
23
|
+
title,
|
|
24
|
+
message,
|
|
25
|
+
variant,
|
|
26
|
+
onDismiss,
|
|
27
|
+
}: MessageModalProps) {
|
|
28
|
+
const config = variantConfig[variant];
|
|
29
|
+
|
|
30
|
+
useKeyboard(() => {
|
|
31
|
+
// Any key dismisses
|
|
32
|
+
onDismiss();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<box
|
|
37
|
+
flexDirection="column"
|
|
38
|
+
border
|
|
39
|
+
borderStyle="rounded"
|
|
40
|
+
borderColor={theme.colors.dim}
|
|
41
|
+
backgroundColor={theme.surface.bg}
|
|
42
|
+
paddingLeft={3}
|
|
43
|
+
paddingRight={3}
|
|
44
|
+
paddingTop={1}
|
|
45
|
+
paddingBottom={1}
|
|
46
|
+
width={60}
|
|
47
|
+
>
|
|
48
|
+
<box marginBottom={1}>
|
|
49
|
+
<text fg={config.color}>{config.icon} </text>
|
|
50
|
+
<text fg={theme.colors.text}>
|
|
51
|
+
<strong>{title}</strong>
|
|
52
|
+
</text>
|
|
53
|
+
</box>
|
|
54
|
+
|
|
55
|
+
<box marginBottom={1}>
|
|
56
|
+
<text fg={theme.colors.muted}>{message}</text>
|
|
57
|
+
</box>
|
|
58
|
+
|
|
59
|
+
<box marginTop={1}>
|
|
60
|
+
<text fg={theme.colors.muted}>Press any key to continue</text>
|
|
61
|
+
</box>
|
|
62
|
+
</box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default MessageModal;
|