更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)

* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)

纯格式化:移除分号、React Compiler import、import 多行展开。
修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-04 23:24:27 +08:00
committed by GitHub
parent 02694918b5
commit 5b1a52b8e0
559 changed files with 103807 additions and 101817 deletions

View File

@@ -1,10 +1,10 @@
import { c as _c } from "react/compiler-runtime";
import React, { Children, isValidElement } from 'react';
import { Text } from '../../ink.js';
import React, { Children, isValidElement } from 'react'
import { Text } from '../../ink.js'
type Props = {
/** The items to join with a middot separator */
children: React.ReactNode;
};
children: React.ReactNode
}
/**
* Joins children with a middot separator (" · ") for inline metadata display.
@@ -34,43 +34,24 @@ type Props = {
* </Text>
*
*/
export function Byline(t0) {
const $ = _c(5);
const {
children
} = t0;
let t1;
let t2;
if ($[0] !== children) {
t2 = Symbol.for("react.early_return_sentinel");
bb0: {
const validChildren = Children.toArray(children);
if (validChildren.length === 0) {
t2 = null;
break bb0;
}
t1 = validChildren.map(_temp);
}
$[0] = children;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
export function Byline({ children }: Props): React.ReactNode {
// Children.toArray already filters out null, undefined, and booleans
const validChildren = Children.toArray(children)
if (validChildren.length === 0) {
return null
}
if (t2 !== Symbol.for("react.early_return_sentinel")) {
return t2;
}
let t3;
if ($[3] !== t1) {
t3 = <>{t1}</>;
$[3] = t1;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
function _temp(child, index) {
return <React.Fragment key={isValidElement(child) ? child.key ?? index : index}>{index > 0 && <Text dimColor={true}> · </Text>}{child}</React.Fragment>;
return (
<>
{validChildren.map((child, index) => (
<React.Fragment
key={isValidElement(child) ? (child.key ?? index) : index}
>
{index > 0 && <Text dimColor> · </Text>}
{child}
</React.Fragment>
))}
</>
)
}

View File

@@ -1,23 +1,26 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { type ExitState, useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
import { Box, Text } from '../../ink.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js';
import type { Theme } from '../../utils/theme.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { Byline } from './Byline.js';
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
import { Pane } from './Pane.js';
import React from 'react'
import {
type ExitState,
useExitOnCtrlCDWithKeybindings,
} from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import type { Theme } from '../../utils/theme.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
import { Byline } from './Byline.js'
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
import { Pane } from './Pane.js'
type DialogProps = {
title: React.ReactNode;
subtitle?: React.ReactNode;
children: React.ReactNode;
onCancel: () => void;
color?: keyof Theme;
hideInputGuide?: boolean;
hideBorder?: boolean;
title: React.ReactNode
subtitle?: React.ReactNode
children: React.ReactNode
onCancel: () => void
color?: keyof Theme
hideInputGuide?: boolean
hideBorder?: boolean
/** Custom input guide content. Receives exitState for Ctrl+C/D pending display. */
inputGuide?: (exitState: ExitState) => React.ReactNode;
inputGuide?: (exitState: ExitState) => React.ReactNode
/**
* Controls whether Dialog's built-in confirm:no (Esc/n) and app:exit/interrupt
* (Ctrl-C/D) keybindings are active. Set to `false` while an embedded text
@@ -25,113 +28,73 @@ type DialogProps = {
* consumed by Dialog. TextInput has its own ctrl+c/d handlers (cancel on
* press, delete-forward on ctrl+d with text). Defaults to `true`.
*/
isCancelActive?: boolean;
};
export function Dialog(t0) {
const $ = _c(27);
const {
title,
subtitle,
children,
onCancel,
color: t1,
hideInputGuide,
hideBorder,
inputGuide,
isCancelActive: t2
} = t0;
const color = t1 === undefined ? "permission" : t1;
const isCancelActive = t2 === undefined ? true : t2;
const exitState = useExitOnCtrlCDWithKeybindings(undefined, undefined, isCancelActive);
let t3;
if ($[0] !== isCancelActive) {
t3 = {
context: "Confirmation",
isActive: isCancelActive
};
$[0] = isCancelActive;
$[1] = t3;
} else {
t3 = $[1];
}
useKeybinding("confirm:no", onCancel, t3);
let t4;
if ($[2] !== exitState.keyName || $[3] !== exitState.pending) {
t4 = exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
$[2] = exitState.keyName;
$[3] = exitState.pending;
$[4] = t4;
} else {
t4 = $[4];
}
const defaultInputGuide = t4;
let t5;
if ($[5] !== color || $[6] !== title) {
t5 = <Text bold={true} color={color}>{title}</Text>;
$[5] = color;
$[6] = title;
$[7] = t5;
} else {
t5 = $[7];
}
let t6;
if ($[8] !== subtitle) {
t6 = subtitle && <Text dimColor={true}>{subtitle}</Text>;
$[8] = subtitle;
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] !== t5 || $[11] !== t6) {
t7 = <Box flexDirection="column">{t5}{t6}</Box>;
$[10] = t5;
$[11] = t6;
$[12] = t7;
} else {
t7 = $[12];
}
let t8;
if ($[13] !== children || $[14] !== t7) {
t8 = <Box flexDirection="column" gap={1}>{t7}{children}</Box>;
$[13] = children;
$[14] = t7;
$[15] = t8;
} else {
t8 = $[15];
}
let t9;
if ($[16] !== defaultInputGuide || $[17] !== exitState || $[18] !== hideInputGuide || $[19] !== inputGuide) {
t9 = !hideInputGuide && <Box marginTop={1}><Text dimColor={true} italic={true}>{inputGuide ? inputGuide(exitState) : defaultInputGuide}</Text></Box>;
$[16] = defaultInputGuide;
$[17] = exitState;
$[18] = hideInputGuide;
$[19] = inputGuide;
$[20] = t9;
} else {
t9 = $[20];
}
let t10;
if ($[21] !== t8 || $[22] !== t9) {
t10 = <>{t8}{t9}</>;
$[21] = t8;
$[22] = t9;
$[23] = t10;
} else {
t10 = $[23];
}
const content = t10;
if (hideBorder) {
return content;
}
let t11;
if ($[24] !== color || $[25] !== content) {
t11 = <Pane color={color}>{content}</Pane>;
$[24] = color;
$[25] = content;
$[26] = t11;
} else {
t11 = $[26];
}
return t11;
isCancelActive?: boolean
}
export function Dialog({
title,
subtitle,
children,
onCancel,
color = 'permission',
hideInputGuide,
hideBorder,
inputGuide,
isCancelActive = true,
}: DialogProps): React.ReactNode {
const exitState = useExitOnCtrlCDWithKeybindings(
undefined,
undefined,
isCancelActive,
)
// Use configurable keybinding for ESC to cancel.
// isCancelActive lets consumers (e.g. ElicitationDialog) disable this while
// an embedded TextInput is focused, so that keys like 'n' reach the field
// instead of being consumed here.
useKeybinding('confirm:no', onCancel, {
context: 'Confirmation',
isActive: isCancelActive,
})
const defaultInputGuide = exitState.pending ? (
<Text>Press {exitState.keyName} again to exit</Text>
) : (
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
</Byline>
)
const content = (
<>
<Box flexDirection="column" gap={1}>
<Box flexDirection="column">
<Text bold color={color}>
{title}
</Text>
{subtitle && <Text dimColor>{subtitle}</Text>}
</Box>
{children}
</Box>
{!hideInputGuide && (
<Box marginTop={1}>
<Text dimColor italic>
{inputGuide ? inputGuide(exitState) : defaultInputGuide}
</Text>
</Box>
)}
</>
)
if (hideBorder) {
return content
}
return <Pane color={color}>{content}</Pane>
}

View File

@@ -1,33 +1,33 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { stringWidth } from '../../ink/stringWidth.js';
import { Ansi, Text } from '../../ink.js';
import type { Theme } from '../../utils/theme.js';
import React from 'react'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { stringWidth } from '../../ink/stringWidth.js'
import { Ansi, Text } from '../../ink.js'
import type { Theme } from '../../utils/theme.js'
type DividerProps = {
/**
* Width of the divider in characters.
* Defaults to terminal width.
*/
width?: number;
width?: number
/**
* Theme color for the divider.
* If not provided, dimColor is used.
*/
color?: keyof Theme;
color?: keyof Theme
/**
* Character to use for the divider line.
* @default '─'
*/
char?: string;
char?: string
/**
* Padding to subtract from the width (e.g., for indentation).
* @default 0
*/
padding?: number;
padding?: number
/**
* Title shown in the middle of the divider.
@@ -37,8 +37,8 @@ type DividerProps = {
* // ─────────── Title ───────────
* <Divider title="Title" />
*/
title?: string;
};
title?: string
}
/**
* A horizontal divider line.
@@ -63,86 +63,35 @@ type DividerProps = {
* // With centered title
* <Divider title="3 new messages" />
*/
export function Divider(t0) {
const $ = _c(21);
const {
width,
color,
char: t1,
padding: t2,
title
} = t0;
const char = t1 === undefined ? "\u2500" : t1;
const padding = t2 === undefined ? 0 : t2;
const {
columns: terminalWidth
} = useTerminalSize();
const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding);
export function Divider({
width,
color,
char = '─',
padding = 0,
title,
}: DividerProps): React.ReactNode {
const { columns: terminalWidth } = useTerminalSize()
const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding)
if (title) {
const titleWidth = stringWidth(title) + 2;
const sideWidth = Math.max(0, effectiveWidth - titleWidth);
const leftWidth = Math.floor(sideWidth / 2);
const rightWidth = sideWidth - leftWidth;
const t3 = !color;
let t4;
if ($[0] !== char || $[1] !== leftWidth) {
t4 = char.repeat(leftWidth);
$[0] = char;
$[1] = leftWidth;
$[2] = t4;
} else {
t4 = $[2];
}
let t5;
if ($[3] !== title) {
t5 = <Text dimColor={true}><Ansi>{title}</Ansi></Text>;
$[3] = title;
$[4] = t5;
} else {
t5 = $[4];
}
let t6;
if ($[5] !== char || $[6] !== rightWidth) {
t6 = char.repeat(rightWidth);
$[5] = char;
$[6] = rightWidth;
$[7] = t6;
} else {
t6 = $[7];
}
let t7;
if ($[8] !== color || $[9] !== t3 || $[10] !== t4 || $[11] !== t5 || $[12] !== t6) {
t7 = <Text color={color} dimColor={t3}>{t4}{" "}{t5}{" "}{t6}</Text>;
$[8] = color;
$[9] = t3;
$[10] = t4;
$[11] = t5;
$[12] = t6;
$[13] = t7;
} else {
t7 = $[13];
}
return t7;
const titleWidth = stringWidth(title) + 2 // +2 for spaces around title
const sideWidth = Math.max(0, effectiveWidth - titleWidth)
const leftWidth = Math.floor(sideWidth / 2)
const rightWidth = sideWidth - leftWidth
return (
<Text color={color} dimColor={!color}>
{char.repeat(leftWidth)}{' '}
<Text dimColor>
<Ansi>{title}</Ansi>
</Text>{' '}
{char.repeat(rightWidth)}
</Text>
)
}
const t3 = !color;
let t4;
if ($[14] !== char || $[15] !== effectiveWidth) {
t4 = char.repeat(effectiveWidth);
$[14] = char;
$[15] = effectiveWidth;
$[16] = t4;
} else {
t4 = $[16];
}
let t5;
if ($[17] !== color || $[18] !== t3 || $[19] !== t4) {
t5 = <Text color={color} dimColor={t3}>{t4}</Text>;
$[17] = color;
$[18] = t3;
$[19] = t4;
$[20] = t5;
} else {
t5 = $[20];
}
return t5;
return (
<Text color={color} dimColor={!color}>
{char.repeat(effectiveWidth)}
</Text>
)
}

View File

@@ -1,70 +1,73 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useSearchInput } from '../../hooks/useSearchInput.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { clamp } from '../../ink/layout/geometry.js';
import { Box, Text, useTerminalFocus } from '../../ink.js';
import { SearchBox } from '../SearchBox.js';
import { Byline } from './Byline.js';
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
import { ListItem } from './ListItem.js';
import { Pane } from './Pane.js';
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useSearchInput } from '../../hooks/useSearchInput.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { clamp } from '../../ink/layout/geometry.js'
import { Box, Text, useTerminalFocus } from '../../ink.js'
import { SearchBox } from '../SearchBox.js'
import { Byline } from './Byline.js'
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
import { ListItem } from './ListItem.js'
import { Pane } from './Pane.js'
type PickerAction<T> = {
/** Hint label shown in the byline, e.g. "mention" → "Tab to mention". */
action: string;
handler: (item: T) => void;
};
action: string
handler: (item: T) => void
}
type Props<T> = {
title: string;
placeholder?: string;
initialQuery?: string;
items: readonly T[];
getKey: (item: T) => string;
title: string
placeholder?: string
initialQuery?: string
items: readonly T[]
getKey: (item: T) => string
/** Keep to one line — preview handles overflow. */
renderItem: (item: T, isFocused: boolean) => React.ReactNode;
renderPreview?: (item: T) => React.ReactNode;
renderItem: (item: T, isFocused: boolean) => React.ReactNode
renderPreview?: (item: T) => React.ReactNode
/** 'right' keeps hints stable (no bounce), but needs width. */
previewPosition?: 'bottom' | 'right';
visibleCount?: number;
previewPosition?: 'bottom' | 'right'
visibleCount?: number
/**
* 'up' puts items[0] at the bottom next to the input (atuin-style). Arrows
* always match screen direction — ↑ walks visually up regardless.
*/
direction?: 'down' | 'up';
direction?: 'down' | 'up'
/** Caller owns filtering: re-filter on each call and pass new items. */
onQueryChange: (query: string) => void;
onQueryChange: (query: string) => void
/** Enter key. Primary action. */
onSelect: (item: T) => void;
onSelect: (item: T) => void
/**
* Tab key. If provided, Tab no longer aliases Enter — it gets its own
* handler and hint. Shift+Tab falls through to this if onShiftTab is unset.
*/
onTab?: PickerAction<T>;
onTab?: PickerAction<T>
/** Shift+Tab key. Gets its own hint. */
onShiftTab?: PickerAction<T>;
onShiftTab?: PickerAction<T>
/**
* Fires when the focused item changes (via arrows or when items reset).
* Useful for async preview loading — keeps I/O out of renderPreview.
*/
onFocus?: (item: T | undefined) => void;
onCancel: () => void;
onFocus?: (item: T | undefined) => void
onCancel: () => void
/** Shown when items is empty. Caller bakes loading/searching state into this. */
emptyMessage?: string | ((query: string) => string);
emptyMessage?: string | ((query: string) => string)
/**
* Status line below the list, e.g. "500+ matches" or "42 matches…".
* Caller decides when to show it — pass undefined to hide.
*/
matchLabel?: string;
selectAction?: string;
extraHints?: React.ReactNode;
};
const DEFAULT_VISIBLE = 8;
matchLabel?: string
selectAction?: string
extraHints?: React.ReactNode
}
const DEFAULT_VISIBLE = 8
// Pane (paddingTop + Divider) + title + 3 gaps + SearchBox (rounded border = 3
// rows) + hints. matchLabel adds +1 when present, accounted for separately.
const CHROME_ROWS = 10;
const MIN_VISIBLE = 2;
const CHROME_ROWS = 10
const MIN_VISIBLE = 2
export function FuzzyPicker<T>({
title,
placeholder = 'Type to search…',
@@ -85,117 +88,168 @@ export function FuzzyPicker<T>({
emptyMessage = 'No results',
matchLabel,
selectAction = 'select',
extraHints
extraHints,
}: Props<T>): React.ReactNode {
const isTerminalFocused = useTerminalFocus();
const {
rows,
columns
} = useTerminalSize();
const [focusedIndex, setFocusedIndex] = useState(0);
const isTerminalFocused = useTerminalFocus()
const { rows, columns } = useTerminalSize()
const [focusedIndex, setFocusedIndex] = useState(0)
// Cap visibleCount so the picker never exceeds the terminal height. When it
// overflows, each re-render (arrow key, ctrl+p) mis-positions the cursor-up
// by the overflow amount and a previously-drawn line flashes blank.
const visibleCount = Math.max(MIN_VISIBLE, Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)));
const visibleCount = Math.max(
MIN_VISIBLE,
Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)),
)
// Full hint row with onTab+onShiftTab is ~100 chars and wraps inconsistently
// below that. Compact mode drops shift+tab and shortens labels.
const compact = columns < 120;
const compact = columns < 120
const step = (delta: 1 | -1) => {
setFocusedIndex(i => clamp(i + delta, 0, items.length - 1));
};
setFocusedIndex(i => clamp(i + delta, 0, items.length - 1))
}
// onKeyDown fires after useSearchInput's useInput, so onExit must be a
// no-op — return/downArrow are handled by handleKeyDown below. onCancel
// still covers escape/ctrl+c/ctrl+d. Backspace-on-empty is disabled so
// a held backspace doesn't eject the user from the dialog.
const {
query,
cursorOffset
} = useSearchInput({
const { query, cursorOffset } = useSearchInput({
isActive: true,
onExit: () => {},
onCancel,
initialQuery,
backspaceExitsOnEmpty: false
});
backspaceExitsOnEmpty: false,
})
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'up' || e.ctrl && e.key === 'p') {
e.preventDefault();
e.stopImmediatePropagation();
step(direction === 'up' ? 1 : -1);
return;
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
e.preventDefault()
e.stopImmediatePropagation()
step(direction === 'up' ? 1 : -1)
return
}
if (e.key === 'down' || e.ctrl && e.key === 'n') {
e.preventDefault();
e.stopImmediatePropagation();
step(direction === 'up' ? -1 : 1);
return;
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
e.preventDefault()
e.stopImmediatePropagation()
step(direction === 'up' ? -1 : 1)
return
}
if (e.key === 'return') {
e.preventDefault();
e.stopImmediatePropagation();
const selected = items[focusedIndex];
if (selected) onSelect(selected);
return;
e.preventDefault()
e.stopImmediatePropagation()
const selected = items[focusedIndex]
if (selected) onSelect(selected)
return
}
if (e.key === 'tab') {
e.preventDefault();
e.stopImmediatePropagation();
const selected = items[focusedIndex];
if (!selected) return;
const tabAction = e.shift ? onShiftTab ?? onTab : onTab;
e.preventDefault()
e.stopImmediatePropagation()
const selected = items[focusedIndex]
if (!selected) return
const tabAction = e.shift ? (onShiftTab ?? onTab) : onTab
if (tabAction) {
tabAction.handler(selected);
tabAction.handler(selected)
} else {
onSelect(selected);
onSelect(selected)
}
}
};
}
useEffect(() => {
onQueryChange(query);
setFocusedIndex(0);
onQueryChange(query)
setFocusedIndex(0)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
}, [query])
useEffect(() => {
setFocusedIndex(i => clamp(i, 0, items.length - 1));
}, [items.length]);
const focused = items[focusedIndex];
setFocusedIndex(i => clamp(i, 0, items.length - 1))
}, [items.length])
const focused = items[focusedIndex]
useEffect(() => {
onFocus?.(focused);
onFocus?.(focused)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [focused]);
const windowStart = clamp(focusedIndex - visibleCount + 1, 0, items.length - visibleCount);
const visible = items.slice(windowStart, windowStart + visibleCount);
const emptyText = typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage;
const searchBox = <SearchBox query={query} cursorOffset={cursorOffset} placeholder={placeholder} isFocused isTerminalFocused={isTerminalFocused} />;
const listBlock = <List visible={visible} windowStart={windowStart} visibleCount={visibleCount} total={items.length} focusedIndex={focusedIndex} direction={direction} getKey={getKey} renderItem={renderItem} emptyText={emptyText} />;
const preview = renderPreview && focused ? <Box flexDirection="column" flexGrow={1}>
}, [focused])
const windowStart = clamp(
focusedIndex - visibleCount + 1,
0,
items.length - visibleCount,
)
const visible = items.slice(windowStart, windowStart + visibleCount)
const emptyText =
typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage
const searchBox = (
<SearchBox
query={query}
cursorOffset={cursorOffset}
placeholder={placeholder}
isFocused
isTerminalFocused={isTerminalFocused}
/>
)
const listBlock = (
<List
visible={visible}
windowStart={windowStart}
visibleCount={visibleCount}
total={items.length}
focusedIndex={focusedIndex}
direction={direction}
getKey={getKey}
renderItem={renderItem}
emptyText={emptyText}
/>
)
const preview =
renderPreview && focused ? (
<Box flexDirection="column" flexGrow={1}>
{renderPreview(focused)}
</Box> : null;
</Box>
) : null
// Structure must not depend on preview truthiness — when focused goes
// undefined (e.g. delete clears matches), switching row→fragment would
// change both layout AND gap count, bouncing the searchBox below.
const listGroup = renderPreview && previewPosition === 'right' ? <Box flexDirection="row" gap={2} height={visibleCount + (matchLabel ? 1 : 0)}>
const listGroup =
renderPreview && previewPosition === 'right' ? (
<Box
flexDirection="row"
gap={2}
height={visibleCount + (matchLabel ? 1 : 0)}
>
<Box flexDirection="column" flexShrink={0}>
{listBlock}
{matchLabel && <Text dimColor>{matchLabel}</Text>}
</Box>
{preview ?? <Box flexGrow={1} />}
</Box> :
// Box (not fragment) so the outer gap={1} doesn't insert a blank line
// between list/matchLabel/preview — that read as extra space above the
// prompt in direction='up'.
<Box flexDirection="column">
</Box>
) : (
// Box (not fragment) so the outer gap={1} doesn't insert a blank line
// between list/matchLabel/preview — that read as extra space above the
// prompt in direction='up'.
<Box flexDirection="column">
{listBlock}
{matchLabel && <Text dimColor>{matchLabel}</Text>}
{preview}
</Box>;
const inputAbove = direction !== 'up';
return <Pane color="permission">
<Box flexDirection="column" gap={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
</Box>
)
const inputAbove = direction !== 'up'
return (
<Pane color="permission">
<Box
flexDirection="column"
gap={1}
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Text bold color="permission">
{title}
</Text>
@@ -204,108 +258,93 @@ export function FuzzyPicker<T>({
{!inputAbove && searchBox}
<Text dimColor>
<Byline>
<KeyboardShortcutHint shortcut="↑/↓" action={compact ? 'nav' : 'navigate'} />
<KeyboardShortcutHint shortcut="Enter" action={compact ? firstWord(selectAction) : selectAction} />
{onTab && <KeyboardShortcutHint shortcut="Tab" action={onTab.action} />}
{onShiftTab && !compact && <KeyboardShortcutHint shortcut="shift+tab" action={onShiftTab.action} />}
<KeyboardShortcutHint
shortcut="↑/↓"
action={compact ? 'nav' : 'navigate'}
/>
<KeyboardShortcutHint
shortcut="Enter"
action={compact ? firstWord(selectAction) : selectAction}
/>
{onTab && (
<KeyboardShortcutHint shortcut="Tab" action={onTab.action} />
)}
{onShiftTab && !compact && (
<KeyboardShortcutHint
shortcut="shift+tab"
action={onShiftTab.action}
/>
)}
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
{extraHints}
</Byline>
</Text>
</Box>
</Pane>;
</Pane>
)
}
type ListProps<T> = Pick<Props<T>, 'visibleCount' | 'direction' | 'getKey' | 'renderItem'> & {
visible: readonly T[];
windowStart: number;
total: number;
focusedIndex: number;
emptyText: string;
};
function List(t0) {
const $ = _c(27);
const {
visible,
windowStart,
visibleCount,
total,
focusedIndex,
direction,
getKey,
renderItem,
emptyText
} = t0;
type ListProps<T> = Pick<
Props<T>,
'visibleCount' | 'direction' | 'getKey' | 'renderItem'
> & {
visible: readonly T[]
windowStart: number
total: number
focusedIndex: number
emptyText: string
}
function List<T>({
visible,
windowStart,
visibleCount,
total,
focusedIndex,
direction,
getKey,
renderItem,
emptyText,
}: ListProps<T>): React.ReactNode {
if (visible.length === 0) {
let t1;
if ($[0] !== emptyText) {
t1 = <Text dimColor={true}>{emptyText}</Text>;
$[0] = emptyText;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1 || $[3] !== visibleCount) {
t2 = <Box height={visibleCount} flexShrink={0}>{t1}</Box>;
$[2] = t1;
$[3] = visibleCount;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
return (
<Box height={visibleCount} flexShrink={0}>
<Text dimColor>{emptyText}</Text>
</Box>
)
}
let t1;
if ($[5] !== direction || $[6] !== focusedIndex || $[7] !== getKey || $[8] !== renderItem || $[9] !== total || $[10] !== visible || $[11] !== visibleCount || $[12] !== windowStart) {
let t2;
if ($[14] !== direction || $[15] !== focusedIndex || $[16] !== getKey || $[17] !== renderItem || $[18] !== total || $[19] !== visible.length || $[20] !== visibleCount || $[21] !== windowStart) {
t2 = (item, i) => {
const actualIndex = windowStart + i;
const isFocused = actualIndex === focusedIndex;
const atLowEdge = i === 0 && windowStart > 0;
const atHighEdge = i === visible.length - 1 && windowStart + visibleCount < total;
return <ListItem key={getKey(item)} isFocused={isFocused} showScrollUp={direction === "up" ? atHighEdge : atLowEdge} showScrollDown={direction === "up" ? atLowEdge : atHighEdge} styled={false}>{renderItem(item, isFocused)}</ListItem>;
};
$[14] = direction;
$[15] = focusedIndex;
$[16] = getKey;
$[17] = renderItem;
$[18] = total;
$[19] = visible.length;
$[20] = visibleCount;
$[21] = windowStart;
$[22] = t2;
} else {
t2 = $[22];
}
t1 = visible.map(t2);
$[5] = direction;
$[6] = focusedIndex;
$[7] = getKey;
$[8] = renderItem;
$[9] = total;
$[10] = visible;
$[11] = visibleCount;
$[12] = windowStart;
$[13] = t1;
} else {
t1 = $[13];
}
const rows = t1;
const t2 = direction === "up" ? "column-reverse" : "column";
let t3;
if ($[23] !== rows || $[24] !== t2 || $[25] !== visibleCount) {
t3 = <Box height={visibleCount} flexShrink={0} flexDirection={t2}>{rows}</Box>;
$[23] = rows;
$[24] = t2;
$[25] = visibleCount;
$[26] = t3;
} else {
t3 = $[26];
}
return t3;
const rows = visible.map((item, i) => {
const actualIndex = windowStart + i
const isFocused = actualIndex === focusedIndex
const atLowEdge = i === 0 && windowStart > 0
const atHighEdge =
i === visible.length - 1 && windowStart + visibleCount! < total
return (
<ListItem
key={getKey(item)}
isFocused={isFocused}
showScrollUp={direction === 'up' ? atHighEdge : atLowEdge}
showScrollDown={direction === 'up' ? atLowEdge : atHighEdge}
styled={false}
>
{renderItem(item, isFocused)}
</ListItem>
)
})
return (
<Box
height={visibleCount}
flexShrink={0}
flexDirection={direction === 'up' ? 'column-reverse' : 'column'}
>
{rows}
</Box>
)
}
function firstWord(s: string): string {
const i = s.indexOf(' ');
return i === -1 ? s : s.slice(0, i);
const i = s.indexOf(' ')
return i === -1 ? s : s.slice(0, i)
}

View File

@@ -1,16 +1,16 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import Text from '../../ink/components/Text.js';
import React from 'react'
import Text from '../../ink/components/Text.js'
type Props = {
/** The key or chord to display (e.g., "ctrl+o", "Enter", "↑/↓") */
shortcut: string;
shortcut: string
/** The action the key performs (e.g., "expand", "select", "navigate") */
action: string;
action: string
/** Whether to wrap the hint in parentheses. Default: false */
parens?: boolean;
parens?: boolean
/** Whether to render the shortcut in bold. Default: false */
bold?: boolean;
};
bold?: boolean
}
/**
* Renders a keyboard shortcut hint like "ctrl+o to expand" or "(tab to toggle)"
@@ -35,46 +35,24 @@ type Props = {
* </Byline>
* </Text>
*/
export function KeyboardShortcutHint(t0) {
const $ = _c(9);
const {
shortcut,
action,
parens: t1,
bold: t2
} = t0;
const parens = t1 === undefined ? false : t1;
const bold = t2 === undefined ? false : t2;
let t3;
if ($[0] !== bold || $[1] !== shortcut) {
t3 = bold ? <Text bold={true}>{shortcut}</Text> : shortcut;
$[0] = bold;
$[1] = shortcut;
$[2] = t3;
} else {
t3 = $[2];
}
const shortcutText = t3;
export function KeyboardShortcutHint({
shortcut,
action,
parens = false,
bold = false,
}: Props): React.ReactNode {
const shortcutText = bold ? <Text bold>{shortcut}</Text> : shortcut
if (parens) {
let t4;
if ($[3] !== action || $[4] !== shortcutText) {
t4 = <Text>({shortcutText} to {action})</Text>;
$[3] = action;
$[4] = shortcutText;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
return (
<Text>
({shortcutText} to {action})
</Text>
)
}
let t4;
if ($[6] !== action || $[7] !== shortcutText) {
t4 = <Text>{shortcutText} to {action}</Text>;
$[6] = action;
$[7] = shortcutText;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
return (
<Text>
{shortcutText} to {action}
</Text>
)
}

View File

@@ -1,44 +1,44 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import type { ReactNode } from 'react';
import React from 'react';
import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js';
import { Box, Text } from '../../ink.js';
import figures from 'figures'
import type { ReactNode } from 'react'
import React from 'react'
import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js'
import { Box, Text } from '../../ink.js'
type ListItemProps = {
/**
* Whether this item is currently focused (keyboard selection).
* Shows the pointer indicator () when true.
*/
isFocused: boolean;
isFocused: boolean
/**
* Whether this item is selected (chosen/checked).
* Shows the checkmark indicator (✓) when true.
* @default false
*/
isSelected?: boolean;
isSelected?: boolean
/**
* The content to display for this item.
*/
children: ReactNode;
children: ReactNode
/**
* Optional description text displayed below the main content.
*/
description?: string;
description?: string
/**
* Show a down arrow indicator instead of pointer (for scroll hints).
* Only applies when not focused.
*/
showScrollDown?: boolean;
showScrollDown?: boolean
/**
* Show an up arrow indicator instead of pointer (for scroll hints).
* Only applies when not focused.
*/
showScrollUp?: boolean;
showScrollUp?: boolean
/**
* Whether to apply automatic styling to the children based on focus/selection state.
@@ -46,21 +46,21 @@ type ListItemProps = {
* - When false: children are rendered as-is, allowing custom styling
* @default true
*/
styled?: boolean;
styled?: boolean
/**
* Whether this item is disabled. Disabled items show dimmed text and no indicators.
* @default false
*/
disabled?: boolean;
disabled?: boolean
/**
* Whether this ListItem should declare the terminal cursor position.
* Set false when a child (e.g. BaseTextInput) declares its own cursor.
* @default true
*/
declareCursor?: boolean;
};
declareCursor?: boolean
}
/**
* A list item component for selection UIs (dropdowns, multi-selects, menus).
@@ -101,143 +101,88 @@ type ListItemProps = {
* <Text color="claude">Custom styled content</Text>
* </ListItem>
*/
export function ListItem(t0) {
const $ = _c(32);
const {
isFocused,
isSelected: t1,
children,
description,
showScrollDown,
showScrollUp,
styled: t2,
disabled: t3,
declareCursor
} = t0;
const isSelected = t1 === undefined ? false : t1;
const styled = t2 === undefined ? true : t2;
const disabled = t3 === undefined ? false : t3;
let t4;
if ($[0] !== disabled || $[1] !== isFocused || $[2] !== showScrollDown || $[3] !== showScrollUp) {
t4 = function renderIndicator() {
if (disabled) {
return <Text> </Text>;
}
if (isFocused) {
return <Text color="suggestion">{figures.pointer}</Text>;
}
if (showScrollDown) {
return <Text dimColor={true}>{figures.arrowDown}</Text>;
}
if (showScrollUp) {
return <Text dimColor={true}>{figures.arrowUp}</Text>;
}
return <Text> </Text>;
};
$[0] = disabled;
$[1] = isFocused;
$[2] = showScrollDown;
$[3] = showScrollUp;
$[4] = t4;
} else {
t4 = $[4];
export function ListItem({
isFocused,
isSelected = false,
children,
description,
showScrollDown,
showScrollUp,
styled = true,
disabled = false,
declareCursor,
}: ListItemProps): React.ReactNode {
// Determine which indicator to show
function renderIndicator(): ReactNode {
if (disabled) {
return <Text> </Text>
}
if (isFocused) {
return <Text color="suggestion">{figures.pointer}</Text>
}
if (showScrollDown) {
return <Text dimColor>{figures.arrowDown}</Text>
}
if (showScrollUp) {
return <Text dimColor>{figures.arrowUp}</Text>
}
return <Text> </Text>
}
const renderIndicator = t4;
let t5;
if ($[5] !== disabled || $[6] !== isFocused || $[7] !== isSelected || $[8] !== styled) {
const getTextColor = function getTextColor() {
if (disabled) {
return "inactive";
}
if (!styled) {
return;
}
if (isSelected) {
return "success";
}
if (isFocused) {
return "suggestion";
}
};
t5 = getTextColor();
$[5] = disabled;
$[6] = isFocused;
$[7] = isSelected;
$[8] = styled;
$[9] = t5;
} else {
t5 = $[9];
// Determine text color based on state
function getTextColor(): 'success' | 'suggestion' | 'inactive' | undefined {
if (disabled) {
return 'inactive'
}
if (!styled) {
return undefined
}
if (isSelected) {
return 'success'
}
if (isFocused) {
return 'suggestion'
}
return undefined
}
const textColor = t5;
const t6 = isFocused && !disabled && declareCursor !== false;
let t7;
if ($[10] !== t6) {
t7 = {
line: 0,
column: 0,
active: t6
};
$[10] = t6;
$[11] = t7;
} else {
t7 = $[11];
}
const cursorRef = useDeclaredCursor(t7);
let t8;
if ($[12] !== renderIndicator) {
t8 = renderIndicator();
$[12] = renderIndicator;
$[13] = t8;
} else {
t8 = $[13];
}
let t9;
if ($[14] !== children || $[15] !== disabled || $[16] !== styled || $[17] !== textColor) {
t9 = styled ? <Text color={textColor} dimColor={disabled}>{children}</Text> : children;
$[14] = children;
$[15] = disabled;
$[16] = styled;
$[17] = textColor;
$[18] = t9;
} else {
t9 = $[18];
}
let t10;
if ($[19] !== disabled || $[20] !== isSelected) {
t10 = isSelected && !disabled && <Text color="success">{figures.tick}</Text>;
$[19] = disabled;
$[20] = isSelected;
$[21] = t10;
} else {
t10 = $[21];
}
let t11;
if ($[22] !== t10 || $[23] !== t8 || $[24] !== t9) {
t11 = <Box flexDirection="row" gap={1}>{t8}{t9}{t10}</Box>;
$[22] = t10;
$[23] = t8;
$[24] = t9;
$[25] = t11;
} else {
t11 = $[25];
}
let t12;
if ($[26] !== description) {
t12 = description && <Box paddingLeft={2}><Text color="inactive">{description}</Text></Box>;
$[26] = description;
$[27] = t12;
} else {
t12 = $[27];
}
let t13;
if ($[28] !== cursorRef || $[29] !== t11 || $[30] !== t12) {
t13 = <Box ref={cursorRef} flexDirection="column">{t11}{t12}</Box>;
$[28] = cursorRef;
$[29] = t11;
$[30] = t12;
$[31] = t13;
} else {
t13 = $[31];
}
return t13;
const textColor = getTextColor()
// Park the native terminal cursor on the pointer indicator so screen
// readers / magnifiers track the focused item. (0,0) is the top-left of
// this Box, where the pointer renders.
const cursorRef = useDeclaredCursor({
line: 0,
column: 0,
active: isFocused && !disabled && declareCursor !== false,
})
return (
<Box ref={cursorRef} flexDirection="column">
<Box flexDirection="row" gap={1}>
{renderIndicator()}
{styled ? (
<Text color={textColor} dimColor={disabled}>
{children}
</Text>
) : (
children
)}
{isSelected && !disabled && <Text color="success">{figures.tick}</Text>}
</Box>
{description && (
<Box paddingLeft={2}>
<Text color="inactive">{description}</Text>
</Box>
)}
</Box>
)
}

View File

@@ -1,30 +1,30 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { Box, Text } from '../../ink.js';
import { Spinner } from '../Spinner.js';
import React from 'react'
import { Box, Text } from '../../ink.js'
import { Spinner } from '../Spinner.js'
type LoadingStateProps = {
/**
* The loading message to display next to the spinner.
*/
message: string;
message: string
/**
* Display the message in bold.
* @default false
*/
bold?: boolean;
bold?: boolean
/**
* Display the message in dimmed color.
* @default false
*/
dimColor?: boolean;
dimColor?: boolean
/**
* Optional subtitle displayed below the main message.
*/
subtitle?: string;
};
subtitle?: string
}
/**
* A spinner with loading message for async operations.
@@ -45,49 +45,22 @@ type LoadingStateProps = {
* subtitle="Fetching your Claude Code sessions..."
* />
*/
export function LoadingState(t0) {
const $ = _c(10);
const {
message,
bold: t1,
dimColor: t2,
subtitle
} = t0;
const bold = t1 === undefined ? false : t1;
const dimColor = t2 === undefined ? false : t2;
let t3;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Spinner />;
$[0] = t3;
} else {
t3 = $[0];
}
let t4;
if ($[1] !== bold || $[2] !== dimColor || $[3] !== message) {
t4 = <Box flexDirection="row">{t3}<Text bold={bold} dimColor={dimColor}>{" "}{message}</Text></Box>;
$[1] = bold;
$[2] = dimColor;
$[3] = message;
$[4] = t4;
} else {
t4 = $[4];
}
let t5;
if ($[5] !== subtitle) {
t5 = subtitle && <Text dimColor={true}>{subtitle}</Text>;
$[5] = subtitle;
$[6] = t5;
} else {
t5 = $[6];
}
let t6;
if ($[7] !== t4 || $[8] !== t5) {
t6 = <Box flexDirection="column">{t4}{t5}</Box>;
$[7] = t4;
$[8] = t5;
$[9] = t6;
} else {
t6 = $[9];
}
return t6;
export function LoadingState({
message,
bold = false,
dimColor = false,
subtitle,
}: LoadingStateProps): React.ReactNode {
return (
<Box flexDirection="column">
<Box flexDirection="row">
<Spinner />
<Text bold={bold} dimColor={dimColor}>
{' '}
{message}
</Text>
</Box>
{subtitle && <Text dimColor>{subtitle}</Text>}
</Box>
)
}

View File

@@ -1,16 +1,16 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { useIsInsideModal } from '../../context/modalContext.js';
import { Box } from '../../ink.js';
import type { Theme } from '../../utils/theme.js';
import { Divider } from './Divider.js';
import React from 'react'
import { useIsInsideModal } from '../../context/modalContext.js'
import { Box } from '../../ink.js'
import type { Theme } from '../../utils/theme.js'
import { Divider } from './Divider.js'
type PaneProps = {
children: React.ReactNode;
children: React.ReactNode
/**
* Theme color for the top border line.
*/
color?: keyof Theme;
};
color?: keyof Theme
}
/**
* A pane — a region of the terminal that appears below the REPL prompt,
@@ -30,47 +30,28 @@ type PaneProps = {
* <Tabs title="Sandbox:">...</Tabs>
* </Pane>
*/
export function Pane(t0) {
const $ = _c(9);
const {
children,
color
} = t0;
export function Pane({ children, color }: PaneProps): React.ReactNode {
// When rendered inside FullscreenLayout's modal slot, its ▔ divider IS
// the frame. Skip our own Divider (would double-frame) and the extra top
// padding. This lets slash-command screens that wrap in Pane (e.g.
// /model → ModelPicker) route through the modal slot unchanged.
if (useIsInsideModal()) {
let t1;
if ($[0] !== children) {
t1 = <Box flexDirection="column" paddingX={1} flexShrink={0}>{children}</Box>;
$[0] = children;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
// flexShrink=0: the modal slot's absolute Box has no explicit height
// (grows to fit, maxHeight cap). With flexGrow=1, re-renders cause
// yoga to resolve this Box's height to 0 against the undetermined
// parent — /permissions body blanks on Down arrow. See #23592.
return (
<Box flexDirection="column" paddingX={1} flexShrink={0}>
{children}
</Box>
)
}
let t1;
if ($[2] !== color) {
t1 = <Divider color={color} />;
$[2] = color;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== children) {
t2 = <Box flexDirection="column" paddingX={2}>{children}</Box>;
$[4] = children;
$[5] = t2;
} else {
t2 = $[5];
}
let t3;
if ($[6] !== t1 || $[7] !== t2) {
t3 = <Box flexDirection="column" paddingTop={1}>{t1}{t2}</Box>;
$[6] = t1;
$[7] = t2;
$[8] = t3;
} else {
t3 = $[8];
}
return t3;
return (
<Box flexDirection="column" paddingTop={1}>
<Divider color={color} />
<Box flexDirection="column" paddingX={2}>
{children}
</Box>
</Box>
)
}

View File

@@ -1,85 +1,54 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { Text } from '../../ink.js';
import type { Theme } from '../../utils/theme.js';
import React from 'react'
import { Text } from '../../ink.js'
import type { Theme } from '../../utils/theme.js'
type Props = {
/**
* How much progress to display, between 0 and 1 inclusive
*/
ratio: number; // [0, 1]
ratio: number // [0, 1]
/**
* How many characters wide to draw the progress bar
*/
width: number; // how many characters wide
width: number // how many characters wide
/**
* Optional color for the filled portion of the bar
*/
fillColor?: keyof Theme;
fillColor?: keyof Theme
/**
* Optional color for the empty portion of the bar
*/
emptyColor?: keyof Theme;
};
const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
export function ProgressBar(t0) {
const $ = _c(13);
const {
ratio: inputRatio,
width,
fillColor,
emptyColor
} = t0;
const ratio = Math.min(1, Math.max(0, inputRatio));
const whole = Math.floor(ratio * width);
let t1;
if ($[0] !== whole) {
t1 = BLOCKS[BLOCKS.length - 1].repeat(whole);
$[0] = whole;
$[1] = t1;
} else {
t1 = $[1];
}
let segments;
if ($[2] !== ratio || $[3] !== t1 || $[4] !== whole || $[5] !== width) {
segments = [t1];
if (whole < width) {
const remainder = ratio * width - whole;
const middle = Math.floor(remainder * BLOCKS.length);
segments.push(BLOCKS[middle]);
const empty = width - whole - 1;
if (empty > 0) {
let t2;
if ($[7] !== empty) {
t2 = BLOCKS[0].repeat(empty);
$[7] = empty;
$[8] = t2;
} else {
t2 = $[8];
}
segments.push(t2);
}
}
$[2] = ratio;
$[3] = t1;
$[4] = whole;
$[5] = width;
$[6] = segments;
} else {
segments = $[6];
}
const t2 = segments.join("");
let t3;
if ($[9] !== emptyColor || $[10] !== fillColor || $[11] !== t2) {
t3 = <Text color={fillColor} backgroundColor={emptyColor}>{t2}</Text>;
$[9] = emptyColor;
$[10] = fillColor;
$[11] = t2;
$[12] = t3;
} else {
t3 = $[12];
}
return t3;
emptyColor?: keyof Theme
}
const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']
export function ProgressBar({
ratio: inputRatio,
width,
fillColor,
emptyColor,
}: Props): React.ReactNode {
const ratio = Math.min(1, Math.max(0, inputRatio))
const whole = Math.floor(ratio * width)
const segments = [BLOCKS[BLOCKS.length - 1]!.repeat(whole)]
if (whole < width) {
const remainder = ratio * width - whole
const middle = Math.floor(remainder * BLOCKS.length)
segments.push(BLOCKS[middle]!)
const empty = width - whole - 1
if (empty > 0) {
segments.push(BLOCKS[0]!.repeat(empty))
}
}
return (
<Text color={fillColor} backgroundColor={emptyColor}>
{segments.join('')}
</Text>
)
}

View File

@@ -1,79 +1,45 @@
import { c as _c } from "react/compiler-runtime";
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js';
import { Box, type DOMElement, measureElement } from '../../ink.js';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js'
import { Box, type DOMElement, measureElement } from '../../ink.js'
type Props = {
children: React.ReactNode;
lock?: 'always' | 'offscreen';
};
export function Ratchet(t0) {
const $ = _c(10);
const {
children,
lock: t1
} = t0;
const lock = t1 === undefined ? "always" : t1;
const [viewportRef, t2] = useTerminalViewport();
const {
isVisible
} = t2;
const {
rows
} = useTerminalSize();
const innerRef = useRef(null);
const maxHeight = useRef(0);
const [minHeight, setMinHeight] = useState(0);
let t3;
if ($[0] !== viewportRef) {
t3 = el => {
viewportRef(el);
};
$[0] = viewportRef;
$[1] = t3;
} else {
t3 = $[1];
}
const outerRef = t3;
const engaged = lock === "always" || !isVisible;
let t4;
if ($[2] !== rows) {
t4 = () => {
if (!innerRef.current) {
return;
}
const {
height
} = measureElement(innerRef.current);
if (height > maxHeight.current) {
maxHeight.current = Math.min(height, rows);
setMinHeight(maxHeight.current);
}
};
$[2] = rows;
$[3] = t4;
} else {
t4 = $[3];
}
useLayoutEffect(t4);
const t5 = engaged ? minHeight : undefined;
let t6;
if ($[4] !== children) {
t6 = <Box ref={innerRef} flexDirection="column">{children}</Box>;
$[4] = children;
$[5] = t6;
} else {
t6 = $[5];
}
let t7;
if ($[6] !== outerRef || $[7] !== t5 || $[8] !== t6) {
t7 = <Box minHeight={t5} ref={outerRef}>{t6}</Box>;
$[6] = outerRef;
$[7] = t5;
$[8] = t6;
$[9] = t7;
} else {
t7 = $[9];
}
return t7;
children: React.ReactNode
lock?: 'always' | 'offscreen'
}
export function Ratchet({ children, lock = 'always' }: Props): React.ReactNode {
const [viewportRef, { isVisible }] = useTerminalViewport()
const { rows } = useTerminalSize()
const innerRef = useRef<DOMElement | null>(null)
const maxHeight = useRef(0)
const [minHeight, setMinHeight] = useState(0)
const outerRef = useCallback(
(el: DOMElement | null) => {
viewportRef(el)
},
[viewportRef],
)
const engaged = lock === 'always' || !isVisible
useLayoutEffect(() => {
if (!innerRef.current) {
return
}
const { height } = measureElement(innerRef.current)
if (height > maxHeight.current) {
maxHeight.current = Math.min(height, rows)
setMinHeight(maxHeight.current)
}
})
return (
<Box minHeight={engaged ? minHeight : undefined} ref={outerRef}>
<Box ref={innerRef} flexDirection="column">
{children}
</Box>
</Box>
)
}

View File

@@ -1,8 +1,9 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import React from 'react';
import { Text } from '../../ink.js';
type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading';
import figures from 'figures'
import React from 'react'
import { Text } from '../../ink.js'
type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading'
type Props = {
/**
* The status to display. Determines both the icon and color.
@@ -14,42 +15,28 @@ type Props = {
* - `pending`: Dimmed circle (○)
* - `loading`: Dimmed ellipsis (…)
*/
status: Status;
status: Status
/**
* Include a trailing space after the icon. Useful when followed by text.
* @default false
*/
withSpace?: boolean;
};
const STATUS_CONFIG: Record<Status, {
icon: string;
color: 'success' | 'error' | 'warning' | 'suggestion' | undefined;
}> = {
success: {
icon: figures.tick,
color: 'success'
},
error: {
icon: figures.cross,
color: 'error'
},
warning: {
icon: figures.warning,
color: 'warning'
},
info: {
icon: figures.info,
color: 'suggestion'
},
pending: {
icon: figures.circle,
color: undefined
},
loading: {
icon: '…',
color: undefined
withSpace?: boolean
}
const STATUS_CONFIG: Record<
Status,
{
icon: string
color: 'success' | 'error' | 'warning' | 'suggestion' | undefined
}
};
> = {
success: { icon: figures.tick, color: 'success' },
error: { icon: figures.cross, color: 'error' },
warning: { icon: figures.warning, color: 'warning' },
info: { icon: figures.info, color: 'suggestion' },
pending: { icon: figures.circle, color: undefined },
loading: { icon: '…', color: undefined },
}
/**
* Renders a status indicator icon with appropriate color.
@@ -69,26 +56,16 @@ const STATUS_CONFIG: Record<Status, {
* Waiting for response
* </Text>
*/
export function StatusIcon(t0) {
const $ = _c(5);
const {
status,
withSpace: t1
} = t0;
const withSpace = t1 === undefined ? false : t1;
const config = STATUS_CONFIG[status];
const t2 = !config.color;
const t3 = withSpace && " ";
let t4;
if ($[0] !== config.color || $[1] !== config.icon || $[2] !== t2 || $[3] !== t3) {
t4 = <Text color={config.color} dimColor={t2}>{config.icon}{t3}</Text>;
$[0] = config.color;
$[1] = config.icon;
$[2] = t2;
$[3] = t3;
$[4] = t4;
} else {
t4 = $[4];
}
return t4;
export function StatusIcon({
status,
withSpace = false,
}: Props): React.ReactNode {
const config = STATUS_CONFIG[status]
return (
<Text color={config.color} dimColor={!config.color}>
{config.icon}
{withSpace && ' '}
</Text>
)
}

View File

@@ -1,28 +1,37 @@
import { c as _c } from "react/compiler-runtime";
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { useIsInsideModal, useModalScrollRef } from '../../context/modalContext.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import ScrollBox from '../../ink/components/ScrollBox.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { stringWidth } from '../../ink/stringWidth.js';
import { Box, Text } from '../../ink.js';
import { useKeybindings } from '../../keybindings/useKeybinding.js';
import type { Theme } from '../../utils/theme.js';
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import {
useIsInsideModal,
useModalScrollRef,
} from '../../context/modalContext.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import ScrollBox from '../../ink/components/ScrollBox.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { stringWidth } from '../../ink/stringWidth.js'
import { Box, Text } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import type { Theme } from '../../utils/theme.js'
type TabsProps = {
children: Array<React.ReactElement<TabProps>>;
title?: string;
color?: keyof Theme;
defaultTab?: string;
hidden?: boolean;
useFullWidth?: boolean;
children: Array<React.ReactElement<TabProps>>
title?: string
color?: keyof Theme
defaultTab?: string
hidden?: boolean
useFullWidth?: boolean
/** Controlled mode: current selected tab id/title */
selectedTab?: string;
selectedTab?: string
/** Controlled mode: callback when tab changes */
onTabChange?: (tabId: string) => void;
onTabChange?: (tabId: string) => void
/** Optional banner to display below tabs header */
banner?: React.ReactNode;
banner?: React.ReactNode
/** Disable keyboard navigation (e.g. when a child component handles arrow keys) */
disableNavigation?: boolean;
disableNavigation?: boolean
/**
* Initial focus state for the tab header row. Defaults to true (header
* focused, nav always works). Keep the default for Select/list content —
@@ -31,28 +40,30 @@ type TabsProps = {
* content actually binds left/right/tab (e.g. enum cycling), and show a
* "↑ tabs" footer hint — without it tabs look broken.
*/
initialHeaderFocused?: boolean;
initialHeaderFocused?: boolean
/**
* Fixed height for the content area. When set, all tabs render within the
* same height (overflow hidden) so switching tabs doesn't cause layout
* shifts. Shorter tabs get whitespace; taller tabs are clipped.
*/
contentHeight?: number;
contentHeight?: number
/**
* Let Tab/←/→ switch tabs from focused content. Opt-in since some
* content uses those keys; pass a reactive boolean to cede them when
* needed. Switching from content focuses the header.
*/
navFromContent?: boolean;
};
navFromContent?: boolean
}
type TabsContextValue = {
selectedTab: string | undefined;
width: number | undefined;
headerFocused: boolean;
focusHeader: () => void;
blurHeader: () => void;
registerOptIn: () => () => void;
};
selectedTab: string | undefined
width: number | undefined
headerFocused: boolean
focusHeader: () => void
blurHeader: () => void
registerOptIn: () => () => void
}
const TabsContext = createContext<TabsContextValue>({
selectedTab: undefined,
width: undefined,
@@ -61,236 +72,248 @@ const TabsContext = createContext<TabsContextValue>({
headerFocused: false,
focusHeader: () => {},
blurHeader: () => {},
registerOptIn: () => () => {}
});
export function Tabs(t0) {
const $ = _c(25);
const {
title,
color,
defaultTab,
children,
hidden,
useFullWidth,
selectedTab: controlledSelectedTab,
onTabChange,
banner,
disableNavigation,
initialHeaderFocused: t1,
contentHeight,
navFromContent: t2
} = t0;
const initialHeaderFocused = t1 === undefined ? true : t1;
const navFromContent = t2 === undefined ? false : t2;
const {
columns: terminalWidth
} = useTerminalSize();
const tabs = children.map(_temp);
const defaultTabIndex = defaultTab ? tabs.findIndex(tab => defaultTab === tab[0]) : 0;
const isControlled = controlledSelectedTab !== undefined;
const [internalSelectedTab, setInternalSelectedTab] = useState(defaultTabIndex !== -1 ? defaultTabIndex : 0);
const controlledTabIndex = isControlled ? tabs.findIndex(tab_0 => tab_0[0] === controlledSelectedTab) : -1;
const selectedTabIndex = isControlled ? controlledTabIndex !== -1 ? controlledTabIndex : 0 : internalSelectedTab;
const modalScrollRef = useModalScrollRef();
const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused);
let t3;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t3 = () => setHeaderFocused(true);
$[0] = t3;
} else {
t3 = $[0];
}
const focusHeader = t3;
let t4;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t4 = () => setHeaderFocused(false);
$[1] = t4;
} else {
t4 = $[1];
}
const blurHeader = t4;
const [optInCount, setOptInCount] = useState(0);
let t5;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t5 = () => {
setOptInCount(_temp2);
return () => setOptInCount(_temp3);
};
$[2] = t5;
} else {
t5 = $[2];
}
const registerOptIn = t5;
const optedIn = optInCount > 0;
const handleTabChange = offset => {
const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length;
const newTabId = tabs[newIndex]?.[0];
registerOptIn: () => () => {},
})
export function Tabs({
title,
color,
defaultTab,
children,
hidden,
useFullWidth,
selectedTab: controlledSelectedTab,
onTabChange,
banner,
disableNavigation,
initialHeaderFocused = true,
contentHeight,
navFromContent = false,
}: TabsProps): React.ReactNode {
const { columns: terminalWidth } = useTerminalSize()
const tabs = children.map(child => [
child.props.id ?? child.props.title,
child.props.title,
])
const defaultTabIndex = defaultTab
? tabs.findIndex(tab => defaultTab === tab[0])
: 0
// Support both controlled and uncontrolled modes
const isControlled = controlledSelectedTab !== undefined
const [internalSelectedTab, setInternalSelectedTab] = useState(
defaultTabIndex !== -1 ? defaultTabIndex : 0,
)
// In controlled mode, find the index of the controlled tab
const controlledTabIndex = isControlled
? tabs.findIndex(tab => tab[0] === controlledSelectedTab)
: -1
const selectedTabIndex = isControlled
? controlledTabIndex !== -1
? controlledTabIndex
: 0
: internalSelectedTab
const modalScrollRef = useModalScrollRef()
// Header focus: left/right/tab only switch tabs when the header row is
// focused. Children with interactive content call focusHeader() (via
// useTabHeaderFocus) on up-arrow to hand focus back here; down-arrow
// returns it. Tabs that never call the hook see no behavior change —
// initialHeaderFocused defaults to true so nav always works.
const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused)
const focusHeader = useCallback(() => setHeaderFocused(true), [])
const blurHeader = useCallback(() => setHeaderFocused(false), [])
// Count of mounted children using useTabHeaderFocus(). Down-arrow blur and
// the ↓ hint only engage when at least one child has opted in — otherwise
// pressing down on a legacy tab would strand the user with nav disabled.
const [optInCount, setOptInCount] = useState(0)
const registerOptIn = useCallback(() => {
setOptInCount(n => n + 1)
return () => setOptInCount(n => n - 1)
}, [])
const optedIn = optInCount > 0
const handleTabChange = (offset: number) => {
const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length
const newTabId = tabs[newIndex]?.[0]
if (isControlled && onTabChange && newTabId) {
onTabChange(newTabId);
onTabChange(newTabId)
} else {
setInternalSelectedTab(newIndex);
setInternalSelectedTab(newIndex)
}
setHeaderFocused(true);
};
const t6 = !hidden && !disableNavigation && headerFocused;
let t7;
if ($[3] !== t6) {
t7 = {
context: "Tabs",
isActive: t6
};
$[3] = t6;
$[4] = t7;
} else {
t7 = $[4];
// Tab switching is a header action — stay focused so the user can keep
// cycling. The newly mounted tab can blur via its own interaction.
setHeaderFocused(true)
}
useKeybindings({
"tabs:next": () => handleTabChange(1),
"tabs:previous": () => handleTabChange(-1)
}, t7);
let t8;
if ($[5] !== headerFocused || $[6] !== hidden || $[7] !== optedIn) {
t8 = e => {
if (!headerFocused || !optedIn || hidden) {
return;
}
if (e.key === "down") {
e.preventDefault();
setHeaderFocused(false);
}
};
$[5] = headerFocused;
$[6] = hidden;
$[7] = optedIn;
$[8] = t8;
} else {
t8 = $[8];
}
const handleKeyDown = t8;
const t9 = navFromContent && !headerFocused && optedIn && !hidden && !disableNavigation;
let t10;
if ($[9] !== t9) {
t10 = {
context: "Tabs",
isActive: t9
};
$[9] = t9;
$[10] = t10;
} else {
t10 = $[10];
}
useKeybindings({
"tabs:next": () => {
handleTabChange(1);
setHeaderFocused(true);
useKeybindings(
{
'tabs:next': () => handleTabChange(1),
'tabs:previous': () => handleTabChange(-1),
},
"tabs:previous": () => {
handleTabChange(-1);
setHeaderFocused(true);
{
context: 'Tabs',
isActive: !hidden && !disableNavigation && headerFocused,
},
)
// When the header is focused, down-arrow returns focus to content. Only
// active when the selected tab has opted in via useTabHeaderFocus() —
// legacy tabs have nowhere to return focus to.
const handleKeyDown = (e: KeyboardEvent) => {
if (!headerFocused || !optedIn || hidden) return
if (e.key === 'down') {
e.preventDefault()
setHeaderFocused(false)
}
}, t10);
const titleWidth = title ? stringWidth(title) + 1 : 0;
const tabsWidth = tabs.reduce(_temp4, 0);
const usedWidth = titleWidth + tabsWidth;
const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0;
const contentWidth = useFullWidth ? terminalWidth : undefined;
const T0 = Box;
const t11 = "column";
const t12 = 0;
const t13 = true;
const t14 = modalScrollRef ? 0 : undefined;
const t15 = !hidden && <Box flexDirection="row" gap={1} flexShrink={modalScrollRef ? 0 : undefined}>{title !== undefined && <Text bold={true} color={color}>{title}</Text>}{tabs.map((t16, i) => {
const [id, title_0] = t16;
const isCurrent = selectedTabIndex === i;
const hasColorCursor = color && isCurrent && headerFocused;
return <Text key={id} backgroundColor={hasColorCursor ? color : undefined} color={hasColorCursor ? "inverseText" : undefined} inverse={isCurrent && !hasColorCursor} bold={isCurrent}>{" "}{title_0}{" "}</Text>;
})}{spacerWidth > 0 && <Text>{" ".repeat(spacerWidth)}</Text>}</Box>;
let t17;
if ($[11] !== children || $[12] !== contentHeight || $[13] !== contentWidth || $[14] !== hidden || $[15] !== modalScrollRef || $[16] !== selectedTabIndex) {
t17 = modalScrollRef ? <Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}><ScrollBox key={selectedTabIndex} ref={modalScrollRef} flexDirection="column" flexShrink={0}>{children}</ScrollBox></Box> : <Box width={contentWidth} marginTop={hidden ? 0 : 1} height={contentHeight} overflowY={contentHeight !== undefined ? "hidden" : undefined}>{children}</Box>;
$[11] = children;
$[12] = contentHeight;
$[13] = contentWidth;
$[14] = hidden;
$[15] = modalScrollRef;
$[16] = selectedTabIndex;
$[17] = t17;
} else {
t17 = $[17];
}
let t18;
if ($[18] !== T0 || $[19] !== banner || $[20] !== handleKeyDown || $[21] !== t14 || $[22] !== t15 || $[23] !== t17) {
t18 = <T0 flexDirection={t11} tabIndex={t12} autoFocus={t13} onKeyDown={handleKeyDown} flexShrink={t14}>{t15}{banner}{t17}</T0>;
$[18] = T0;
$[19] = banner;
$[20] = handleKeyDown;
$[21] = t14;
$[22] = t15;
$[23] = t17;
$[24] = t18;
} else {
t18 = $[24];
}
return <TabsContext.Provider value={{
selectedTab: tabs[selectedTabIndex][0],
width: contentWidth,
headerFocused,
focusHeader,
blurHeader,
registerOptIn
}}>{t18}</TabsContext.Provider>;
}
function _temp4(sum, t0) {
const [, tabTitle] = t0;
return sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1;
}
function _temp3(n_0) {
return n_0 - 1;
}
function _temp2(n) {
return n + 1;
}
function _temp(child) {
return [child.props.id ?? child.props.title, child.props.title];
// Opt-in: same tabs:next/previous actions, active from content. Focuses
// the header so subsequent presses cycle via the handler above.
useKeybindings(
{
'tabs:next': () => {
handleTabChange(1)
setHeaderFocused(true)
},
'tabs:previous': () => {
handleTabChange(-1)
setHeaderFocused(true)
},
},
{
context: 'Tabs',
isActive:
navFromContent &&
!headerFocused &&
optedIn &&
!hidden &&
!disableNavigation,
},
)
// Calculate spacing to fill the available width. No keyboard hint in the
// header row — content footers own hints (see useTabHeaderFocus docs).
const titleWidth = title ? stringWidth(title) + 1 : 0 // +1 for gap
const tabsWidth = tabs.reduce(
(sum, [, tabTitle]) => sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1, // +2 for padding, +1 for gap
0,
)
const usedWidth = titleWidth + tabsWidth
const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0
const contentWidth = useFullWidth ? terminalWidth : undefined
return (
<TabsContext.Provider
value={{
selectedTab: tabs[selectedTabIndex]![0],
width: contentWidth,
headerFocused,
focusHeader,
blurHeader,
registerOptIn,
}}
>
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
// flexShrink=0 inside modal slot — the modal's absolute Box has no
// explicit height (grows to fit, maxHeight cap), so flexGrow=1 here
// resolves to 0 on re-render and the body blanks on Down arrow.
// See #23592. Outside modal, leave layout alone.
flexShrink={modalScrollRef ? 0 : undefined}
>
{!hidden && (
<Box
flexDirection="row"
gap={1}
flexShrink={modalScrollRef ? 0 : undefined}
>
{title !== undefined && (
<Text bold color={color}>
{title}
</Text>
)}
{tabs.map(([id, title], i) => {
const isCurrent = selectedTabIndex === i
const hasColorCursor = color && isCurrent && headerFocused
return (
<Text
key={id}
backgroundColor={hasColorCursor ? color : undefined}
color={hasColorCursor ? 'inverseText' : undefined}
inverse={isCurrent && !hasColorCursor}
bold={isCurrent}
>
{' '}
{title}{' '}
</Text>
)
})}
{spacerWidth > 0 && <Text>{' '.repeat(spacerWidth)}</Text>}
</Box>
)}
{banner}
{modalScrollRef ? (
// Inside the modal slot: own the ScrollBox here so the tabs
// header row above sits OUTSIDE the scroll area — it can never
// scroll off. The ref reaches REPL's ScrollKeybindingHandler via
// ModalContext. Keyed by selectedTabIndex → remounts on tab
// switch, resetting scrollTop to 0 without scrollTo() timing games.
<Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}>
<ScrollBox
key={selectedTabIndex}
ref={modalScrollRef}
flexDirection="column"
flexShrink={0}
>
{children}
</ScrollBox>
</Box>
) : (
<Box
width={contentWidth}
marginTop={hidden ? 0 : 1}
height={contentHeight}
overflowY={contentHeight !== undefined ? 'hidden' : undefined}
>
{children}
</Box>
)}
</Box>
</TabsContext.Provider>
)
}
type TabProps = {
title: string;
id?: string;
children: React.ReactNode;
};
export function Tab(t0) {
const $ = _c(4);
const {
title,
id,
children
} = t0;
const {
selectedTab,
width
} = useContext(TabsContext);
const insideModal = useIsInsideModal();
if (selectedTab !== (id ?? title)) {
return null;
}
const t1 = insideModal ? 0 : undefined;
let t2;
if ($[0] !== children || $[1] !== t1 || $[2] !== width) {
t2 = <Box width={width} flexShrink={t1}>{children}</Box>;
$[0] = children;
$[1] = t1;
$[2] = width;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
title: string
id?: string
children: React.ReactNode
}
export function useTabsWidth() {
const {
width
} = useContext(TabsContext);
return width;
export function Tab({ title, id, children }: TabProps): React.ReactNode {
const { selectedTab, width } = useContext(TabsContext)
const insideModal = useIsInsideModal()
if (selectedTab !== (id ?? title)) {
return null
}
return (
<Box width={width} flexShrink={insideModal ? 0 : undefined}>
{children}
</Box>
)
}
export function useTabsWidth(): number | undefined {
const { width } = useContext(TabsContext)
return width
}
/**
@@ -304,36 +327,13 @@ export function useTabsWidth() {
* no onUpFromFirstItem to recover. Split the component so the hook only runs
* when the Select renders.
*/
export function useTabHeaderFocus() {
const $ = _c(6);
const {
headerFocused,
focusHeader,
blurHeader,
registerOptIn
} = useContext(TabsContext);
let t0;
if ($[0] !== registerOptIn) {
t0 = [registerOptIn];
$[0] = registerOptIn;
$[1] = t0;
} else {
t0 = $[1];
}
useEffect(registerOptIn, t0);
let t1;
if ($[2] !== blurHeader || $[3] !== focusHeader || $[4] !== headerFocused) {
t1 = {
headerFocused,
focusHeader,
blurHeader
};
$[2] = blurHeader;
$[3] = focusHeader;
$[4] = headerFocused;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
export function useTabHeaderFocus(): {
headerFocused: boolean
focusHeader: () => void
blurHeader: () => void
} {
const { headerFocused, focusHeader, blurHeader, registerOptIn } =
useContext(TabsContext)
useEffect(registerOptIn, [registerOptIn])
return { headerFocused, focusHeader, blurHeader }
}

View File

@@ -1,169 +1,160 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import useStdin from '../../ink/hooks/use-stdin.js';
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
import { getSystemThemeName, type SystemTheme } from '../../utils/systemTheme.js';
import type { ThemeName, ThemeSetting } from '../../utils/theme.js';
import { feature } from 'bun:bundle'
import React, {
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import useStdin from '../../ink/hooks/use-stdin.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import {
getSystemThemeName,
type SystemTheme,
} from '../../utils/systemTheme.js'
import type { ThemeName, ThemeSetting } from '../../utils/theme.js'
type ThemeContextValue = {
/** The saved user preference. May be 'auto'. */
themeSetting: ThemeSetting;
setThemeSetting: (setting: ThemeSetting) => void;
setPreviewTheme: (setting: ThemeSetting) => void;
savePreview: () => void;
cancelPreview: () => void;
themeSetting: ThemeSetting
setThemeSetting: (setting: ThemeSetting) => void
setPreviewTheme: (setting: ThemeSetting) => void
savePreview: () => void
cancelPreview: () => void
/** The resolved theme to render with. Never 'auto'. */
currentTheme: ThemeName;
};
currentTheme: ThemeName
}
// Non-'auto' default so useTheme() works without a provider (tests, tooling).
const DEFAULT_THEME: ThemeName = 'dark';
const DEFAULT_THEME: ThemeName = 'dark'
const ThemeContext = createContext<ThemeContextValue>({
themeSetting: DEFAULT_THEME,
setThemeSetting: () => {},
setPreviewTheme: () => {},
savePreview: () => {},
cancelPreview: () => {},
currentTheme: DEFAULT_THEME
});
currentTheme: DEFAULT_THEME,
})
type Props = {
children: React.ReactNode;
initialState?: ThemeSetting;
onThemeSave?: (setting: ThemeSetting) => void;
};
children: React.ReactNode
initialState?: ThemeSetting
onThemeSave?: (setting: ThemeSetting) => void
}
function defaultInitialTheme(): ThemeSetting {
return getGlobalConfig().theme;
return getGlobalConfig().theme
}
function defaultSaveTheme(setting: ThemeSetting): void {
saveGlobalConfig(current => ({
...current,
theme: setting
}));
saveGlobalConfig(current => ({ ...current, theme: setting }))
}
export function ThemeProvider({
children,
initialState,
onThemeSave = defaultSaveTheme
onThemeSave = defaultSaveTheme,
}: Props) {
const [themeSetting, setThemeSetting] = useState(initialState ?? defaultInitialTheme);
const [previewTheme, setPreviewTheme] = useState<ThemeSetting | null>(null);
const [themeSetting, setThemeSetting] = useState(
initialState ?? defaultInitialTheme,
)
const [previewTheme, setPreviewTheme] = useState<ThemeSetting | null>(null)
// Track terminal theme for 'auto' resolution. Seeds from $COLORFGBG (or
// 'dark' if unset); the OSC 11 watcher corrects it on first poll.
const [systemTheme, setSystemTheme] = useState<SystemTheme>(() => (initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark');
const [systemTheme, setSystemTheme] = useState<SystemTheme>(() =>
(initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark',
)
// The setting currently in effect (preview wins while picker is open)
const activeSetting = previewTheme ?? themeSetting;
const {
internal_querier
} = useStdin();
const activeSetting = previewTheme ?? themeSetting
const { internal_querier } = useStdin()
// Watch for live terminal theme changes while 'auto' is active.
// Positive feature() pattern so the watcher import is dead-code-eliminated
// in external builds.
useEffect(() => {
if (feature('AUTO_THEME')) {
if (activeSetting !== 'auto' || !internal_querier) return;
let cleanup: (() => void) | undefined;
let cancelled = false;
void import('../../utils/systemThemeWatcher.js').then(({
watchSystemTheme
}) => {
if (cancelled) return;
cleanup = watchSystemTheme(internal_querier, setSystemTheme);
});
if (activeSetting !== 'auto' || !internal_querier) return
let cleanup: (() => void) | undefined
let cancelled = false
void import('../../utils/systemThemeWatcher.js').then(
({ watchSystemTheme }) => {
if (cancelled) return
cleanup = watchSystemTheme(internal_querier, setSystemTheme)
},
)
return () => {
cancelled = true;
cleanup?.();
};
cancelled = true
cleanup?.()
}
}
}, [activeSetting, internal_querier]);
const currentTheme: ThemeName = activeSetting === 'auto' ? systemTheme : activeSetting;
const value = useMemo<ThemeContextValue>(() => ({
themeSetting,
setThemeSetting: (newSetting: ThemeSetting) => {
setThemeSetting(newSetting);
setPreviewTheme(null);
// Switching to 'auto' restarts the watcher (activeSetting dep), whose
// first poll fires immediately. Seed from the cache so the OSC
// round-trip doesn't flash the wrong palette.
if (newSetting === 'auto') {
setSystemTheme(getSystemThemeName());
}
onThemeSave?.(newSetting);
},
setPreviewTheme: (newSetting_0: ThemeSetting) => {
setPreviewTheme(newSetting_0);
if (newSetting_0 === 'auto') {
setSystemTheme(getSystemThemeName());
}
},
savePreview: () => {
if (previewTheme !== null) {
setThemeSetting(previewTheme);
setPreviewTheme(null);
onThemeSave?.(previewTheme);
}
},
cancelPreview: () => {
if (previewTheme !== null) {
setPreviewTheme(null);
}
},
currentTheme
}), [themeSetting, previewTheme, currentTheme, onThemeSave]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}, [activeSetting, internal_querier])
const currentTheme: ThemeName =
activeSetting === 'auto' ? systemTheme : activeSetting
const value = useMemo<ThemeContextValue>(
() => ({
themeSetting,
setThemeSetting: (newSetting: ThemeSetting) => {
setThemeSetting(newSetting)
setPreviewTheme(null)
// Switching to 'auto' restarts the watcher (activeSetting dep), whose
// first poll fires immediately. Seed from the cache so the OSC
// round-trip doesn't flash the wrong palette.
if (newSetting === 'auto') {
setSystemTheme(getSystemThemeName())
}
onThemeSave?.(newSetting)
},
setPreviewTheme: (newSetting: ThemeSetting) => {
setPreviewTheme(newSetting)
if (newSetting === 'auto') {
setSystemTheme(getSystemThemeName())
}
},
savePreview: () => {
if (previewTheme !== null) {
setThemeSetting(previewTheme)
setPreviewTheme(null)
onThemeSave?.(previewTheme)
}
},
cancelPreview: () => {
if (previewTheme !== null) {
setPreviewTheme(null)
}
},
currentTheme,
}),
[themeSetting, previewTheme, currentTheme, onThemeSave],
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
/**
* Returns the resolved theme for rendering (never 'auto') and a setter that
* accepts any ThemeSetting (including 'auto').
*/
export function useTheme() {
const $ = _c(3);
const {
currentTheme,
setThemeSetting
} = useContext(ThemeContext);
let t0;
if ($[0] !== currentTheme || $[1] !== setThemeSetting) {
t0 = [currentTheme, setThemeSetting];
$[0] = currentTheme;
$[1] = setThemeSetting;
$[2] = t0;
} else {
t0 = $[2];
}
return t0;
export function useTheme(): [ThemeName, (setting: ThemeSetting) => void] {
const { currentTheme, setThemeSetting } = useContext(ThemeContext)
return [currentTheme, setThemeSetting]
}
/**
* Returns the raw theme setting as stored in config. Use this in UI that
* needs to show 'auto' as a distinct choice (e.g., ThemePicker).
*/
export function useThemeSetting() {
return useContext(ThemeContext).themeSetting;
export function useThemeSetting(): ThemeSetting {
return useContext(ThemeContext).themeSetting
}
export function usePreviewTheme() {
const $ = _c(4);
const {
setPreviewTheme,
savePreview,
cancelPreview
} = useContext(ThemeContext);
let t0;
if ($[0] !== cancelPreview || $[1] !== savePreview || $[2] !== setPreviewTheme) {
t0 = {
setPreviewTheme,
savePreview,
cancelPreview
};
$[0] = cancelPreview;
$[1] = savePreview;
$[2] = setPreviewTheme;
$[3] = t0;
} else {
t0 = $[3];
}
return t0;
const { setPreviewTheme, savePreview, cancelPreview } =
useContext(ThemeContext)
return { setPreviewTheme, savePreview, cancelPreview }
}

View File

@@ -1,155 +1,112 @@
import { c as _c } from "react/compiler-runtime";
import React, { type PropsWithChildren, type Ref } from 'react';
import Box from '../../ink/components/Box.js';
import type { DOMElement } from '../../ink/dom.js';
import type { ClickEvent } from '../../ink/events/click-event.js';
import type { FocusEvent } from '../../ink/events/focus-event.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import type { Color, Styles } from '../../ink/styles.js';
import { getTheme, type Theme } from '../../utils/theme.js';
import { useTheme } from './ThemeProvider.js';
import React, { type PropsWithChildren, type Ref } from 'react'
import Box from '../../ink/components/Box.js'
import type { DOMElement } from '../../ink/dom.js'
import type { ClickEvent } from '../../ink/events/click-event.js'
import type { FocusEvent } from '../../ink/events/focus-event.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import type { Color, Styles } from '../../ink/styles.js'
import { getTheme, type Theme } from '../../utils/theme.js'
import { useTheme } from './ThemeProvider.js'
// Color props that accept theme keys
type ThemedColorProps = {
readonly borderColor?: keyof Theme | Color;
readonly borderTopColor?: keyof Theme | Color;
readonly borderBottomColor?: keyof Theme | Color;
readonly borderLeftColor?: keyof Theme | Color;
readonly borderRightColor?: keyof Theme | Color;
readonly backgroundColor?: keyof Theme | Color;
};
readonly borderColor?: keyof Theme | Color
readonly borderTopColor?: keyof Theme | Color
readonly borderBottomColor?: keyof Theme | Color
readonly borderLeftColor?: keyof Theme | Color
readonly borderRightColor?: keyof Theme | Color
readonly backgroundColor?: keyof Theme | Color
}
// Base Styles without color props (they'll be overridden)
type BaseStylesWithoutColors = Omit<Styles, 'textWrap' | 'borderColor' | 'borderTopColor' | 'borderBottomColor' | 'borderLeftColor' | 'borderRightColor' | 'backgroundColor'>;
export type Props = BaseStylesWithoutColors & ThemedColorProps & {
ref?: Ref<DOMElement>;
tabIndex?: number;
autoFocus?: boolean;
onClick?: (event: ClickEvent) => void;
onFocus?: (event: FocusEvent) => void;
onFocusCapture?: (event: FocusEvent) => void;
onBlur?: (event: FocusEvent) => void;
onBlurCapture?: (event: FocusEvent) => void;
onKeyDown?: (event: KeyboardEvent) => void;
onKeyDownCapture?: (event: KeyboardEvent) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
};
type BaseStylesWithoutColors = Omit<
Styles,
| 'textWrap'
| 'borderColor'
| 'borderTopColor'
| 'borderBottomColor'
| 'borderLeftColor'
| 'borderRightColor'
| 'backgroundColor'
>
export type Props = BaseStylesWithoutColors &
ThemedColorProps & {
ref?: Ref<DOMElement>
tabIndex?: number
autoFocus?: boolean
onClick?: (event: ClickEvent) => void
onFocus?: (event: FocusEvent) => void
onFocusCapture?: (event: FocusEvent) => void
onBlur?: (event: FocusEvent) => void
onBlurCapture?: (event: FocusEvent) => void
onKeyDown?: (event: KeyboardEvent) => void
onKeyDownCapture?: (event: KeyboardEvent) => void
onMouseEnter?: () => void
onMouseLeave?: () => void
}
/**
* Resolves a color value that may be a theme key to a raw Color.
*/
function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined {
if (!color) return undefined;
function resolveColor(
color: keyof Theme | Color | undefined,
theme: Theme,
): Color | undefined {
if (!color) return undefined
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) {
return color as Color;
if (
color.startsWith('rgb(') ||
color.startsWith('#') ||
color.startsWith('ansi256(') ||
color.startsWith('ansi:')
) {
return color as Color
}
// It's a theme key - resolve it
return theme[color as keyof Theme] as Color;
return theme[color as keyof Theme] as Color
}
/**
* Theme-aware Box component that resolves theme color keys to raw colors.
* This wraps the base Box component with theme resolution for border colors.
*/
function ThemedBox(t0) {
const $ = _c(33);
let backgroundColor;
let borderBottomColor;
let borderColor;
let borderLeftColor;
let borderRightColor;
let borderTopColor;
let children;
let ref;
let rest;
if ($[0] !== t0) {
({
borderColor,
borderTopColor,
borderBottomColor,
borderLeftColor,
borderRightColor,
backgroundColor,
children,
ref,
...rest
} = t0);
$[0] = t0;
$[1] = backgroundColor;
$[2] = borderBottomColor;
$[3] = borderColor;
$[4] = borderLeftColor;
$[5] = borderRightColor;
$[6] = borderTopColor;
$[7] = children;
$[8] = ref;
$[9] = rest;
} else {
backgroundColor = $[1];
borderBottomColor = $[2];
borderColor = $[3];
borderLeftColor = $[4];
borderRightColor = $[5];
borderTopColor = $[6];
children = $[7];
ref = $[8];
rest = $[9];
}
const [themeName] = useTheme();
let resolvedBorderBottomColor;
let resolvedBorderColor;
let resolvedBorderLeftColor;
let resolvedBorderRightColor;
let resolvedBorderTopColor;
let t1;
if ($[10] !== backgroundColor || $[11] !== borderBottomColor || $[12] !== borderColor || $[13] !== borderLeftColor || $[14] !== borderRightColor || $[15] !== borderTopColor || $[16] !== themeName) {
const theme = getTheme(themeName);
resolvedBorderColor = resolveColor(borderColor, theme);
resolvedBorderTopColor = resolveColor(borderTopColor, theme);
resolvedBorderBottomColor = resolveColor(borderBottomColor, theme);
resolvedBorderLeftColor = resolveColor(borderLeftColor, theme);
resolvedBorderRightColor = resolveColor(borderRightColor, theme);
t1 = resolveColor(backgroundColor, theme);
$[10] = backgroundColor;
$[11] = borderBottomColor;
$[12] = borderColor;
$[13] = borderLeftColor;
$[14] = borderRightColor;
$[15] = borderTopColor;
$[16] = themeName;
$[17] = resolvedBorderBottomColor;
$[18] = resolvedBorderColor;
$[19] = resolvedBorderLeftColor;
$[20] = resolvedBorderRightColor;
$[21] = resolvedBorderTopColor;
$[22] = t1;
} else {
resolvedBorderBottomColor = $[17];
resolvedBorderColor = $[18];
resolvedBorderLeftColor = $[19];
resolvedBorderRightColor = $[20];
resolvedBorderTopColor = $[21];
t1 = $[22];
}
const resolvedBackgroundColor = t1;
let t2;
if ($[23] !== children || $[24] !== ref || $[25] !== resolvedBackgroundColor || $[26] !== resolvedBorderBottomColor || $[27] !== resolvedBorderColor || $[28] !== resolvedBorderLeftColor || $[29] !== resolvedBorderRightColor || $[30] !== resolvedBorderTopColor || $[31] !== rest) {
t2 = <Box ref={ref} borderColor={resolvedBorderColor} borderTopColor={resolvedBorderTopColor} borderBottomColor={resolvedBorderBottomColor} borderLeftColor={resolvedBorderLeftColor} borderRightColor={resolvedBorderRightColor} backgroundColor={resolvedBackgroundColor} {...rest}>{children}</Box>;
$[23] = children;
$[24] = ref;
$[25] = resolvedBackgroundColor;
$[26] = resolvedBorderBottomColor;
$[27] = resolvedBorderColor;
$[28] = resolvedBorderLeftColor;
$[29] = resolvedBorderRightColor;
$[30] = resolvedBorderTopColor;
$[31] = rest;
$[32] = t2;
} else {
t2 = $[32];
}
return t2;
function ThemedBox({
borderColor,
borderTopColor,
borderBottomColor,
borderLeftColor,
borderRightColor,
backgroundColor,
children,
ref,
...rest
}: PropsWithChildren<Props>): React.ReactNode {
const [themeName] = useTheme()
const theme = getTheme(themeName)
// Resolve theme keys to raw colors
const resolvedBorderColor = resolveColor(borderColor, theme)
const resolvedBorderTopColor = resolveColor(borderTopColor, theme)
const resolvedBorderBottomColor = resolveColor(borderBottomColor, theme)
const resolvedBorderLeftColor = resolveColor(borderLeftColor, theme)
const resolvedBorderRightColor = resolveColor(borderRightColor, theme)
const resolvedBackgroundColor = resolveColor(backgroundColor, theme)
return (
<Box
ref={ref}
borderColor={resolvedBorderColor}
borderTopColor={resolvedBorderTopColor}
borderBottomColor={resolvedBorderBottomColor}
borderLeftColor={resolvedBorderLeftColor}
borderRightColor={resolvedBorderRightColor}
backgroundColor={resolvedBackgroundColor}
{...rest}
>
{children}
</Box>
)
}
export default ThemedBox;
export default ThemedBox

View File

@@ -1,123 +1,132 @@
import { c as _c } from "react/compiler-runtime";
import type { ReactNode } from 'react';
import React, { useContext } from 'react';
import Text from '../../ink/components/Text.js';
import type { Color, Styles } from '../../ink/styles.js';
import { getTheme, type Theme } from '../../utils/theme.js';
import { useTheme } from './ThemeProvider.js';
import type { ReactNode } from 'react'
import React, { useContext } from 'react'
import Text from '../../ink/components/Text.js'
import type { Color, Styles } from '../../ink/styles.js'
import { getTheme, type Theme } from '../../utils/theme.js'
import { useTheme } from './ThemeProvider.js'
/** Colors uncolored ThemedText in the subtree. Precedence: explicit `color` >
* this > dimColor. Crosses Box boundaries (Ink's style cascade doesn't). */
export const TextHoverColorContext = React.createContext<keyof Theme | undefined>(undefined);
export const TextHoverColorContext = React.createContext<
keyof Theme | undefined
>(undefined)
export type Props = {
/**
* Change text color. Accepts a theme key or raw color value.
*/
readonly color?: keyof Theme | Color;
readonly color?: keyof Theme | Color
/**
* Same as `color`, but for background. Must be a theme key.
*/
readonly backgroundColor?: keyof Theme;
readonly backgroundColor?: keyof Theme
/**
* Dim the color using the theme's inactive color.
* This is compatible with bold (unlike ANSI dim).
*/
readonly dimColor?: boolean;
readonly dimColor?: boolean
/**
* Make the text bold.
*/
readonly bold?: boolean;
readonly bold?: boolean
/**
* Make the text italic.
*/
readonly italic?: boolean;
readonly italic?: boolean
/**
* Make the text underlined.
*/
readonly underline?: boolean;
readonly underline?: boolean
/**
* Make the text crossed with a line.
*/
readonly strikethrough?: boolean;
readonly strikethrough?: boolean
/**
* Inverse background and foreground colors.
*/
readonly inverse?: boolean;
readonly inverse?: boolean
/**
* This property tells Ink to wrap or truncate text if its width is larger than container.
* If `wrap` is passed (by default), Ink will wrap text and split it into multiple lines.
* If `truncate-*` is passed, Ink will truncate text instead, which will result in one line of text with the rest cut off.
*/
readonly wrap?: Styles['textWrap'];
readonly children?: ReactNode;
};
readonly wrap?: Styles['textWrap']
readonly children?: ReactNode
}
/**
* Resolves a color value that may be a theme key to a raw Color.
*/
function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined {
if (!color) return undefined;
function resolveColor(
color: keyof Theme | Color | undefined,
theme: Theme,
): Color | undefined {
if (!color) return undefined
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) {
return color as Color;
if (
color.startsWith('rgb(') ||
color.startsWith('#') ||
color.startsWith('ansi256(') ||
color.startsWith('ansi:')
) {
return color as Color
}
// It's a theme key - resolve it
return theme[color as keyof Theme] as Color;
return theme[color as keyof Theme] as Color
}
/**
* Theme-aware Text component that resolves theme color keys to raw colors.
* This wraps the base Text component with theme resolution.
*/
export default function ThemedText(t0) {
const $ = _c(10);
const {
color,
backgroundColor,
dimColor: t1,
bold: t2,
italic: t3,
underline: t4,
strikethrough: t5,
inverse: t6,
wrap: t7,
children
} = t0;
const dimColor = t1 === undefined ? false : t1;
const bold = t2 === undefined ? false : t2;
const italic = t3 === undefined ? false : t3;
const underline = t4 === undefined ? false : t4;
const strikethrough = t5 === undefined ? false : t5;
const inverse = t6 === undefined ? false : t6;
const wrap = t7 === undefined ? "wrap" : t7;
const [themeName] = useTheme();
const theme = getTheme(themeName);
const hoverColor = useContext(TextHoverColorContext);
const resolvedColor = !color && hoverColor ? resolveColor(hoverColor, theme) : dimColor ? theme.inactive as Color : resolveColor(color, theme);
const resolvedBackgroundColor = backgroundColor ? theme[backgroundColor] as Color : undefined;
let t8;
if ($[0] !== bold || $[1] !== children || $[2] !== inverse || $[3] !== italic || $[4] !== resolvedBackgroundColor || $[5] !== resolvedColor || $[6] !== strikethrough || $[7] !== underline || $[8] !== wrap) {
t8 = <Text color={resolvedColor} backgroundColor={resolvedBackgroundColor} bold={bold} italic={italic} underline={underline} strikethrough={strikethrough} inverse={inverse} wrap={wrap}>{children}</Text>;
$[0] = bold;
$[1] = children;
$[2] = inverse;
$[3] = italic;
$[4] = resolvedBackgroundColor;
$[5] = resolvedColor;
$[6] = strikethrough;
$[7] = underline;
$[8] = wrap;
$[9] = t8;
} else {
t8 = $[9];
}
return t8;
export default function ThemedText({
color,
backgroundColor,
dimColor = false,
bold = false,
italic = false,
underline = false,
strikethrough = false,
inverse = false,
wrap = 'wrap',
children,
}: Props): React.ReactNode {
const [themeName] = useTheme()
const theme = getTheme(themeName)
const hoverColor = useContext(TextHoverColorContext)
// Resolve theme keys to raw colors
const resolvedColor =
!color && hoverColor
? resolveColor(hoverColor, theme)
: dimColor
? (theme.inactive as Color)
: resolveColor(color, theme)
const resolvedBackgroundColor = backgroundColor
? (theme[backgroundColor] as Color)
: undefined
return (
<Text
color={resolvedColor}
backgroundColor={resolvedBackgroundColor}
bold={bold}
italic={italic}
underline={underline}
strikethrough={strikethrough}
inverse={inverse}
wrap={wrap}
>
{children}
</Text>
)
}