Files
claude-code/packages/@ant/ink/docs/05-design-system.md
claude-code-best 2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

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

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

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

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

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

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

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

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00

391 lines
11 KiB
Markdown

# Chapter 5: Design System Components
Pre-built theme-aware UI components for common terminal interface patterns.
## Dialog
Modal dialog with border, title, and keyboard navigation.
```tsx
import { Dialog } from '@anthropic/ink'
<Dialog
title="Confirm Action"
subtitle="This cannot be undone"
onCancel={() => setShowDialog(false)}
color="warning"
>
<Text>Are you sure you want to proceed?</Text>
</Dialog>
```
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `ReactNode` | - | Dialog title (required) |
| `subtitle` | `ReactNode` | - | Optional subtitle |
| `children` | `ReactNode` | - | Dialog body content |
| `onCancel` | `() => void` | - | Called on Esc/n (required) |
| `color` | `keyof Theme` | `'permission'` | Title and border color |
| `hideInputGuide` | `boolean` | `false` | Hide the keyboard hint footer |
| `hideBorder` | `boolean` | `false` | Render without Pane border |
| `inputGuide` | `(exitState) => ReactNode` | - | Custom input guide footer |
| `isCancelActive` | `boolean` | `true` | Enable/disable cancel keybindings |
### Keyboard Shortcuts
- **Enter** -- Confirm (consumer handles this)
- **Esc / n** -- Cancel (calls `onCancel`)
- **Ctrl+C / Ctrl+D** -- Double-press to exit
### Custom Input Guide
```tsx
<Dialog
title="Save file?"
onCancel={handleCancel}
inputGuide={(exitState) => (
exitState.pending
? <Text>Press {exitState.keyName} again to exit</Text>
: <Text>Press Enter to save, Esc to cancel</Text>
)}
>
...
</Dialog>
```
## Pane
Bordered container with themed top border.
```tsx
import { Pane } from '@anthropic/ink'
<Pane color="permission">
<Text>Content inside a bordered pane</Text>
</Pane>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Content |
| `color` | `keyof Theme` | `'permission'` | Top border color |
## ProgressBar
Visual progress indicator.
```tsx
import { ProgressBar } from '@anthropic/ink'
<ProgressBar
ratio={0.65}
width={40}
fillColor="rate_limit_fill"
emptyColor="rate_limit_empty"
/>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `ratio` | `number` | - | Progress 0..1 (required) |
| `width` | `number` | - | Character width (required) |
| `fillColor` | `keyof Theme` | - | Filled portion color |
| `emptyColor` | `keyof Theme` | - | Empty portion color |
## Spinner
Animated loading spinner. No props.
```tsx
import { Spinner } from '@anthropic/ink'
<Box gap={1}>
<Spinner />
<Text>Loading...</Text>
</Box>
```
## LoadingState
Loading message with spinner and optional subtitle.
```tsx
import { LoadingState } from '@anthropic/ink'
<LoadingState
message="Installing dependencies"
subtitle="This may take a moment"
bold={true}
/>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `message` | `string` | - | Loading message (required) |
| `bold` | `boolean` | `false` | Bold message |
| `dimColor` | `boolean` | `false` | Dimmed message |
| `subtitle` | `string` | - | Secondary text below |
## StatusIcon
Semantic status indicator with icon and color.
```tsx
import { StatusIcon } from '@anthropic/ink'
<StatusIcon status="success" withSpace />
<Text>Build complete</Text>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `status` | `'success' \| 'error' \| 'warning' \| 'info' \| 'pending' \| 'loading'` | - | Status type (required) |
| `withSpace` | `boolean` | `false` | Add trailing space |
Status icons:
- `success` -- Green checkmark
- `error` -- Red cross
- `warning` -- Yellow warning
- `info` -- Blue info
- `pending` -- Dimmed circle
- `loading` -- Dimmed ellipsis
## FuzzyPicker
Full-featured fuzzy search selector with preview support.
```tsx
import { FuzzyPicker } from '@anthropic/ink'
<FuzzyPicker
title="Select a file"
items={files}
getKey={(f) => f.path}
renderItem={(f, focused) => <Text>{f.name}</Text>}
onQueryChange={(q) => setFilteredFiles(filterFiles(q))}
onSelect={(f) => openFile(f)}
onCancel={() => setShowPicker(false)}
/>
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Picker title (required) |
| `items` | `readonly T[]` | Items to display (required) |
| `getKey` | `(item: T) => string` | Unique key extractor (required) |
| `renderItem` | `(item: T, isFocused: boolean) => ReactNode` | Item renderer (required) |
| `onQueryChange` | `(query: string) => void` | Filter callback (required) |
| `onSelect` | `(item: T) => void` | Enter key handler (required) |
| `onCancel` | `() => void` | Esc handler (required) |
| `renderPreview` | `(item: T) => ReactNode` | Preview panel renderer |
| `previewPosition` | `'bottom' \| 'right'` | Preview placement |
| `visibleCount` | `number` | Max visible items |
| `direction` | `'down' \| 'up'` | Item ordering |
| `onTab` | `PickerAction<T>` | Tab key handler |
| `onShiftTab` | `PickerAction<T>` | Shift+Tab handler |
| `onFocus` | `(item: T \| undefined) => void` | Focus change callback |
| `emptyMessage` | `string \| ((query: string) => string)` | Empty state message |
| `matchLabel` | `string` | Status line below list |
| `placeholder` | `string` | Input placeholder |
| `initialQuery` | `string` | Initial search query |
| `selectAction` | `string` | Action label for byline |
| `extraHints` | `ReactNode` | Additional keyboard hints |
## Tabs / Tab
Tabbed interface with keyboard navigation.
```tsx
import { Tabs, Tab } from '@anthropic/ink'
<Tabs title="Settings" color="claude">
<Tab title="General" id="general">
<GeneralSettings />
</Tab>
<Tab title="Advanced" id="advanced">
<AdvancedSettings />
</Tab>
</Tabs>
```
### Tabs Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactElement<TabProps>[]` | - | Tab elements |
| `title` | `string` | - | Header title |
| `color` | `keyof Theme` | - | Active tab indicator color |
| `defaultTab` | `string` | - | Initial tab id |
| `selectedTab` | `string` | - | Controlled selected tab |
| `onTabChange` | `(tabId: string) => void` | - | Tab change callback |
| `hidden` | `boolean` | `false` | Hide tab headers |
| `useFullWidth` | `boolean` | `false` | Use full terminal width |
| `banner` | `ReactNode` | - | Banner below tab headers |
| `disableNavigation` | `boolean` | `false` | Disable keyboard nav |
| `initialHeaderFocused` | `boolean` | `true` | Start with header focused |
| `contentHeight` | `number` | - | Fixed content height |
| `navFromContent` | `boolean` | `false` | Allow Tab/Arrow from content |
### Tab Props
| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Tab label (required) |
| `id` | `string` | Tab identifier |
| `children` | `ReactNode` | Tab content |
### Tab Hooks
```tsx
import { useTabsWidth, useTabHeaderFocus } from '@anthropic/ink'
const width = useTabsWidth() // Available content width
const focused = useTabHeaderFocus() // Whether tab header is focused
```
## ListItem
Selectable list item with focus/selection indicators.
```tsx
import { ListItem } from '@anthropic/ink'
<ListItem isFocused={index === focusedIndex} isSelected={item.checked}>
<Text>{item.label}</Text>
</ListItem>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `isFocused` | `boolean` | - | Keyboard focus (required) |
| `isSelected` | `boolean` | `false` | Checked/active state |
| `children` | `ReactNode` | - | Content |
| `description` | `string` | - | Secondary text below |
| `styled` | `boolean` | `true` | Auto-style based on state |
| `disabled` | `boolean` | `false` | Dimmed, non-interactive |
| `showScrollDown` | `boolean` | `false` | Scroll-down hint arrow |
| `showScrollUp` | `boolean` | `false` | Scroll-up hint arrow |
| `declareCursor` | `boolean` | `true` | Declare terminal cursor |
## SearchBox
Search input with theme-aware styling.
```tsx
import { SearchBox } from '@anthropic/ink'
<SearchBox
query={searchQuery}
placeholder="Search..."
isFocused={true}
isTerminalFocused={true}
width="100%"
/>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `query` | `string` | - | Current search text |
| `placeholder` | `string` | - | Placeholder text |
| `isFocused` | `boolean` | - | Focus state |
| `isTerminalFocused` | `boolean` | - | Terminal focus state |
| `prefix` | `string` | - | Input prefix label |
| `width` | `number \| string` | - | Input width |
| `cursorOffset` | `number` | - | Cursor position offset |
| `borderless` | `boolean` | `false` | Remove border |
## Divider
Horizontal/vertical divider line.
```tsx
import { Divider } from '@anthropic/ink'
<Divider width={60} color="subtle" />
<Divider title="Section Title" />
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `width` | `number` | Terminal width | Divider width |
| `color` | `keyof Theme` | Dimmed | Line color |
| `char` | `string` | `'─'` | Line character |
| `padding` | `number` | `0` | Width reduction |
| `title` | `string` | - | Centered title text |
## Byline
Footer with middot-separated items.
```tsx
import { Byline } from '@anthropic/ink'
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
</Byline>
```
## KeyboardShortcutHint
Display a keyboard shortcut with its action.
```tsx
import { KeyboardShortcutHint } from '@anthropic/ink'
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<KeyboardShortcutHint shortcut="↑/↓" action="navigate" parens />
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `shortcut` | `string` | - | Key or chord to display |
| `action` | `string` | - | Action description |
| `parens` | `boolean` | `false` | Wrap in parentheses |
| `bold` | `boolean` | `false` | Bold shortcut text |
## ConfigurableShortcutHint
Displays a shortcut hint that reads the actual keybinding from config.
```tsx
import { ConfigurableShortcutHint } from '@anthropic/ink'
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
```
| Prop | Type | Description |
|------|------|-------------|
| `action` | `string` | Keybinding action name |
| `context` | `string` | Keybinding context |
| `fallback` | `string` | Default shortcut if unbound |
| `description` | `string` | Action description |
| `parens` | `boolean` | Wrap in parentheses |
| `bold` | `boolean` | Bold shortcut text |
## Ratchet
Animated counter component that prevents layout jumps.
```tsx
import { Ratchet } from '@anthropic/ink'
<Ratchet lock="always">
<Text>{count}</Text>
</Ratchet>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Content |
| `lock` | `'always' \| 'offscreen'` | `'always'` | Width locking strategy. `'always'` locks always; `'offscreen'` only locks when the element is scrolled off-screen |