Ink has no reset branch for these two props (every other flex prop does),
so explicit `={undefined}` leaves a stale value. vue-tui resets to the
row/nowrap default. Why: the render is a function of the current props —
absent a special contract, dropping/changing a prop changes the output;
Ink keeping a previous render's value is the anomaly. KEEP.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.3 KiB
vue-tui — Intentional Divergences from Ink
vue-tui started as a Vue 3 port of Ink, and it still tracks Ink closely — the aim is behavioral parity except where a difference is deliberate. But it is no longer just a port: it has grown its own design decisions, additive features, and Vue-native choices. This document records the places vue-tui intentionally differs from Ink — by design, not as a gap to fix. A difference that is not listed here is treated as a bug (or simply unverified), not a design choice.
Reference baseline: Ink v7.0.4 (commit
40b3a7578811fd616341ca4e31cc7748aeeff12f). When bumping the target Ink version, re-validate every entry below against the new source.
How to read this
Each entry states what Ink does, what vue-tui does, and why the divergence is deliberate. Divergences fall into a few kinds:
- API surface — public API renamed/reshaped to fit Vue idioms.
- Additive — vue-tui supports something Ink doesn't (a strict superset).
- Framework semantics — a consequence of Vue ≠ React that cannot be papered over.
- N/A — a React-only concept with no Vue equivalent.
Public API surface
Entry point — createApp() instead of render()
- Ink:
render(<App/>). - vue-tui:
createApp(App).mount(options). - Why: mirrors Vue's own
createAppmental model — a Vue developer expects an app object they mount, not a one-shot render call.
Named type / prop re-exports
- Ink: re-exports its component prop types plus a few data/handle types:
BoxProps,TextProps,StaticProps,TransformProps,NewlineProps,WindowSize,CursorPosition,DOMElement,RenderOptions,Instance,AppProps,StdinProps,StdoutProps,StderrProps. - vue-tui: re-exports the framework-neutral ones under the same names —
BoxProps,TextProps,StaticProps,TransformProps,NewlineProps,WindowSize({ columns, rows }) andCursorPosition({ x, y }). These are not divergences: a<Box>has props in Vue exactly as in React, so the names carry over. They are derived from the runtimepropsobjects via Vue'sExtractPublicPropTypes, so they never drift from the components' real props. Only the remaining few genuinely differ, each for a concrete reason — never merely to "avoid React-shaped names":DOMElement→TuiNode. The one genuinely DOM-shaped type: Ink'sDOMElementmodels a DOM-emulation node (nodeName/attributes/childNodes). vue-tui's host tree is a different representation (TuiContainer | TuiTextLeaf | TuiComment), exported asTuiNodefrom@vue-tui/runtime/internal.RenderOptions/Instance→MountOptions/TuiApp. Downstream of thecreateApp()entry above — vue-tui mounts a Vue app, so the options bag and the returned handle are Vue-shaped, notrender()-shaped.AppProps/StdinProps/StdoutProps/StderrProps→ N/A. These are the props of Ink's internal React context-provider components (<AppContext>,<StdinContext>, …). vue-tui has no such components — that state is reached viacreateAppplus theuseStdin/useStdout/useStderrcomposables — so there is nothing to name.
- Why: the earlier blanket "expose a Vue-native type surface, don't leak
React-shaped names" over-reached — it withheld names like
BoxPropsthat have no React vs Vue content at all. The rule is narrower: mirror Ink's names wherever the underlying type is framework-neutral; reshape only where Vue genuinely has a different thing (a host node, a mounted app) or no thing at all.
Additive features (vue-tui is a strict superset)
Multiple <Static> regions
- Ink: keeps a single
staticNode; only one<Static>is honored. - vue-tui:
findStatics(root)renders every<Static>in the tree. - Why: strictly more capable — a tree with two
<Static>regions both render. Maintainer decision (2026-05-30): KEEP.
Ctrl+C exits under the kitty protocol too
- Ink: wires
exitOnCtrlConly to the legacy\x03path, so a kitty-protocol Ctrl+C (\x1b[99;5u) does not exit. - vue-tui:
useInputexits oninput === 'c' && key.ctrl, gated onexitOnCtrlC(defaulttrue), so Ctrl+C exits under both legacy and kitty protocols. - Why:
exitOnCtrlCreliably exiting under all protocols is the intended behavior; opt out withexitOnCtrlC: false. Maintainer decision (2026-05-30): KEEP.
Framework-semantic divergences (Vue ≠ React)
Removing flexDirection / flexWrap resets to the default
- Ink: these two props have no reset branch in
applyFlexStyles(every other flex prop does), so an explicitflexDirection={undefined}leaves the previous value in place. - vue-tui: resets to the Box default (
row/nowrap) — the same state as if the prop had never been set. - Why: the render is a function of the current props — with no value set you get the default, and (absent a special contract) dropping or changing a prop changes the output. Keeping a previous render's value, as Ink does for these two props, is the anomaly — and an inconsistent one, since every other flex prop resets. Maintainer decision (2026-05-30): KEEP.
Other Vue-vs-React semantics (placeholder)
- (maintainer: candidates to document —
v-if/nullrendering as comment host nodes; reactivity-driven re-render timing vs React's render cycle; keyed reconciliation order. Add the ones that are genuinely by-design.)
Not applicable in Vue
React concurrent mode
- Ink: built on React; Suspense /
useTransitionare React features. - vue-tui: no equivalent — N/A, not a gap.
Framework idioms (noted, not behavioral divergences)
Surface conventions, listed so they aren't mistaken for gaps:
- Vue composables (
useFocus,useInput, …) instead of React hooks. <script setup>SFCs /defineComponentinstead of function components.- kebab-case filenames;
.tsover.tsxwhere there's no JSX. shallowRefby default for reactive state.
Maintainer additions
Space for divergences to add or refine. For each, capture: what Ink does, what vue-tui does, and why it's deliberate (the trade-off, not just the what).