mcpField
I
inkjs-design

inkjs-design

github.com/akiojin开发759 次安装34 周前更新
代码仓库

关于 inkjs-design

面向使用 Ink.js 构建终端界面的开发者的设计指南,围绕屏幕、可复用部件和输入组件整理基于 React 的 CLI 模式、示例与问题排查参考。

主要功能

  1. 01Screen、Part 和 Common 组件模式
  2. 02键盘输入与焦点处理指南
  3. 03多屏幕导航模式
  4. 04响应式终端布局指南
  5. 05性能与动画参考
  6. 06测试与问题排查参考

适用场景

  1. 01规划新 Ink.js CLI 的组件
  2. 02解决终端布局或输入冲突
  3. 03为 CLI 屏幕添加导航

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/akiojin/skills --skill inkjs-design

安装后在 Claude Code 中输入/inkjs-design确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

examples/
component-examples.md
references/
animation-patterns.md
component-patterns.md
hooks-guide.md
ink-gotchas.md
input-handling.md
multi-screen-navigation.md
performance-optimization.md
responsive-layout.md
state-management.md
testing-patterns.md

详细介绍

Ink.js 设计

使用 Ink.js(用于 CLI 的 React)构建终端 UI 的综合指南。

快速开始

创建新组件
  1. 确定组件类型:Screen / Part / Common
  2. 参考 component-patterns.md 中的类似模式
  3. 添加类型定义
  4. 实现组件
  5. 编写测试
常见问题与解决方案
问题参考
Emoji 宽度错位ink-gotchas.md
Ctrl+C 被调用两次ink-gotchas.md
useInput 冲突ink-gotchas.md
布局错乱responsive-layout.md
屏幕导航multi-screen-navigation.md

目录约定

src/cli/ui/
├── components/
│   ├── App.tsx              # Root component with screen management
│   ├── common/              # Common input components (Select, Input)
│   ├── parts/               # Reusable UI parts (Header, Footer)
│   └── screens/             # Full-screen components
├── hooks/                   # Custom hooks
├── utils/                   # Utility functions
└── types.ts                 # Type definitions

组件分类

Screen(全屏视图)
  • 表示完整的屏幕/页面
  • 通过 useInput 处理键盘输入
  • 实现 Header/Content/Footer 布局
  • 管理屏幕级状态
Part(可复用元素)
  • 可复用的 UI 构建块
  • 使用 React.memo 进行优化
  • 优先使用无状态/纯组件
  • 通过 props 接收配置
Common(输入组件)
  • 基本输入组件
  • 支持受控和非受控模式
  • 处理焦点管理
  • 提供一致的 UX

基本模式

1. 图标宽度覆盖

修复 string-width v8 的 Emoji 宽度计算问题:

const WIDTH_OVERRIDES: Record<string, number> = {
  "⚡": 1, "✨": 1, "🐛": 1, "🔥": 1, "🚀": 1,
  "🟢": 1, "🟠": 1, "✅": 1, "⚠️": 1,
};

const getIconWidth = (icon: string): number => {
  const baseWidth = stringWidth(icon);
  const override = WIDTH_OVERRIDES[icon];
  return override !== undefined ? Math.max(baseWidth, override) : baseWidth;
};
2. 避免 useInput 冲突

多个 useInput 钩子都会触发——使用提前返回或 isActive:

useInput((input, key) => {
  if (disabled) return;  // Early return when inactive
  // Handle input...
}, { isActive: isFocused });
3. Ctrl+C 处理
render(<App />, { exitOnCtrlC: false });

// In component
const { exit } = useApp();
useInput((input, key) => {
  if (key.ctrl && input === "c") {
    cleanup();
    exit();
  }
});
4. 动态高度计算
const { rows } = useTerminalSize();
const HEADER_LINES = 3;
const FOOTER_LINES = 2;
const contentHeight = rows - HEADER_LINES - FOOTER_LINES;
const visibleItems = Math.max(5, contentHeight);
5. 使用自定义比较器的 React.memo
function arePropsEqual<T>(prev: Props<T>, next: Props<T>): boolean {
  if (prev.items.length !== next.items.length) return false;
  for (let i = 0; i < prev.items.length; i++) {
    if (prev.items[i].value !== next.items[i].value) return false;
  }
  return prev.selectedIndex === next.selectedIndex;
}

export const Select = React.memo(SelectComponent, arePropsEqual);
6. 多屏幕导航
type ScreenType = "main" | "detail" | "settings";

const [screenStack, setScreenStack] = useState<ScreenType[]>(["main"]);
const currentScreen = screenStack[screenStack.length - 1];

const navigateTo = (screen: ScreenType) => {
  setScreenStack(prev => [...prev, screen]);
};

const goBack = () => {
  if (screenStack.length > 1) {
    setScreenStack(prev => prev.slice(0, -1));
  }
};

详细参考

核心模式
高级主题
故障排查

示例

请参阅 examples/ 获取实际实现示例。

评论

支持 Markdown