Ink coerces ANY string dimension/position value to a percent before handing it
to yoga (applyDimensionStyles uses parseInt; applyPositionStyles uses parseFloat).
vue forwarded the raw string to native yoga, which only treats %-suffixed strings
as percent — so width="50" rendered as 50 absolute cells instead of 50%, top="2"
as 2 cells instead of 2%, and width="" crashed the render ("Invalid value").
width/height now also fall back to setWidthAuto()/setHeightAuto() on a non-number,
non-string junk value (matching Ink's else branch), so width={false} no longer
throws where Ink renders fine. min/max/position keep forwarding junk to the raw
cell setter, matching Ink (which has no auto fallback there and throws identically).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vue-tui left `display` out of RESETTABLE_PROPS, so a removed/undefined `display` persisted
its prior value (a removed display="none" stayed hidden). Adds `display` to RESETTABLE_PROPS
-- the setter already maps undefined -> DISPLAY_FLEX -- so a withdrawn `display` returns to
the Box default (visible), per render = f(current props), like flexDirection/flexWrap (G19).
Deliberate, documented divergence from Ink (which hides on a present-undefined `display` via
DISPLAY_NONE, and persists on omitted) -- recorded in ink-divergences.md. The reset is
consistent across the visual and screen-reader paths (both read yoga's display state).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A string flexBasis was forwarded to yoga's setFlexBasis, so a bare numeric string like
"3" became 3 absolute cells; Ink coerces ANY string to a percent via Number.parseInt ->
setFlexBasisPercent (styles.ts:547-555). "3" now means 3% (at width 6 the box collapses
to 0 and the sibling takes the row), matching Ink. The setter branch is now structurally
identical to Ink -- number -> absolute, string -> percent, anything else (incl. a
non-number/non-string value Vue's prop validation only warns about) -> setFlexBasisAuto()
instead of throwing.
A separate, pre-existing downstream divergence (zero/negative flexBasis% wraps the
sibling in Ink) is documented with a skipped test -- not caused or fixed here.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ink keeps its `measure-text` module internal and never re-exports it. vue-tui
exported `measureText` / `measureTextNatural` from the public index under the
mistaken belief — stated verbatim in commit 0e7d775's own message — that doing
so "matched Ink's public API". It does not; Ink keeps that module internal. A
later design doc then rationalized the leak post-hoc as an intentional
divergence. It was neither intentional nor a divergence — it was a mistake.
Align with Ink:
- Drop both from the public index. `measureTextNatural` stays as an internal
helper (yoga.ts uses it). `measureText` had zero production callers (yoga uses
`wrapText` + `measureTextNatural`, never `measureText`) and is removed.
- Integration tests that used `measureText(stripAnsi(x), 9999).width` as a
line-width helper now use `stringWidth(stripAnsi(x))` directly.
- public-api.test.ts gains a regression test asserting neither is exported.
- Remove the now-obsolete entry from .agents/docs/ink-divergences.md.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ink's output.ts maps sliceAnsi(line, from, to) (the horizontal clip) over the
lines BEFORE the lines.entries() loop that runs transformer(line, index), and it
never re-clips the transformer's output. vue-tui's paint write() did the reverse
(commit 2c99431 deliberately moved clip AFTER transform), so a width-sensitive
transform inside an overflowX:"hidden" box received the FULL line and had its
result sliced — corrupting gradients (wrong char count) and OSC-8 hyperlinks
(closing sequence sliced off).
Reorder so the per-line horizontal clip runs first, then the transformers apply
to the already-clipped span. The post-vertical-clip line index passed to each
transformer is unchanged, so transform index/nesting behavior (G21/G32/G52/G58)
is untouched. The pre-existing overflow test that asserted the old re-clip order
(transform-returns-wide-char dropped at the boundary) is rewritten to Ink's
clip-then-transform output (the widened glyph overflows), verified against the
pinned Ink reference (7.0.4 / 40b3a75) via renderToString.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(runtime): reset yoga props to default on dynamic removal (Ink parity, G19)
Removed style props now reset to the yoga default (margin/padding/min/gap/flexGrow→0, flexShrink→1, flexBasis→auto, flexDirection→ROW, flexWrap→NO_WRAP, alignItems→STRETCH, alignSelf→AUTO, justifyContent→FLEX_START, position→RELATIVE) instead of keeping a stale value — matches Ink's reconciler diff + styles.ts.
The fix threads the previous prop value (prev) from patchProp into applyYogaProp so that resets only fire on genuine removals (prev is a real value, not null/undefined from Vue's initial-mount or never-set patches). RESETTABLE_PROPS is extended with all newly resettable keys.
Follow-up blocker fixes:
- marginX/marginY/paddingX/paddingY now map to Yoga.EDGE_HORIZONTAL/EDGE_VERTICAL (matching Ink styles.ts) instead of concrete EDGE_START/END/TOP/BOTTOM. They compose with the specific edges per yoga precedence, so removing an axis shorthand no longer clobbers a surviving marginLeft/etc.
- applyYogaProp and all setters now treat null the same as undefined (value == null) for the removal/reset path. Vue's host renderer passes next=null (not undefined) when a key disappears from a spread props object (e.g. Static spreads style into host props), which previously bypassed the reset and forwarded raw null into yoga (NaN/0 corruption).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(parity): ledger — G19 pr-open, reconcile G18 merged
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Replace three `stdout.columns ?? 80` / `stdout.rows ?? 24` spots in render.ts with
`resolveSize(stdout).columns/rows`. The `??` guard only falls back on null/undefined,
not on 0 — so non-TTY environments where stdout reports 0 columns would collapse Yoga
layout to width 0. Ink's `getWindowSize` (utils.ts:8-23) uses a truthy guard
(`if (columns && rows)`) and a fallback chain through terminal-size → 80/24 defaults.
`resolveSize()` in useTerminalSize.ts already implements this chain; now exported and
used by the renderer. The non-TTY viewportRows → 24 branch is preserved (Ink-aligned).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bump slice-ansi@9, string-width@8, wrap-ansi@10 and add cli-truncate@6
(both the runtime dep and the pnpm catalog entry for string-width). Rewrite
wrapText truncate variants to delegate to cli-truncate, matching Ink's
wrap-text.ts: grapheme clusters (ZWJ emoji, combining marks) stay whole and
newlines are preserved. Adjust the horizontal-clip left-edge compensation in
paint.ts because slice-ansi@9 drops a straddling wide grapheme whole rather
than splitting it, so lineX must advance by the actually-dropped width.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an early-return guard in paintNode so nodes with DISPLAY_NONE
(already set on their Yoga node) are entirely skipped during paint,
matching Ink's renderNodeToOutput behavior. Without the guard, hidden
text/borders leaked onto visible siblings at x=0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Horizontal clipping now runs per-line AFTER transforms instead of before,
preventing Transform-widened text from escaping clip boundaries. Left-edge
clipping also uses the actual removed width to position subsequent text
correctly when a wide char straddles the boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Absolute-positioned wide characters (CJK, emoji) could paint past the
right edge of a clipped box or the terminal grid, producing output wider
than the column limit. Three fixes:
- Safe-slice after sliceAnsi in clip logic to handle wide char overshoot
- Bounds check in grid write loop to skip chars exceeding grid width
- Width-aware border fill to account for measured corner char widths
Set flexDirection=row, flexShrink=1, flexGrow=0 on text yoga nodes at
creation time, matching Ink's <ink-text> style defaults.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yoga's measure function already receives the content-area width (minus
parent border/padding), so text wrapping is correct without additional
adjustment in the paint pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match Ink's Box defaults by setting yoga properties at node creation time.
This ensures proper row-based layout regardless of Vue's prop patching order.
Updated affected test snapshots and added explicit flexDirection="column"
where tests relied on the old column default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add independent horizontal/vertical clip support in the paint pass,
matching Ink's per-axis overflow behavior. Add overflowX/overflowY to
STYLE_PROPS so they are stored in el.props for the paint pass.
Implement 26 new overflow tests translated from Ink.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port Ink's clip/unclip approach to vue-tui's paint output. When a box
has overflow="hidden", content painted by its children that falls
outside the box's content area (inside borders) is clipped.
Implementation:
- Add ClipRect, clip(), and unclip() to the Output class
- Maintain a clip stack; active clip is applied to each write op
- Horizontal clipping uses slice-ansi for correct ANSI handling
- Vertical clipping slices the lines array
- Store "overflow" in STYLE_PROPS so it's available in node.props for
the paint pass
Also adds explicit flexDirection="row" to two overflow tests where
multiple child boxes should lay out side-by-side (Ink defaults to row,
vue-tui defaults to yoga's column).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ink's Box defaults to flexDirection="row", so multiple <Text> children
lay out side-by-side. Vue-tui uses yoga's default (column). Rather than
changing the global default and breaking 20+ existing tests, add
explicit flexDirection="row" to the inner boxes in these two tests so
they match the intended layout behavior being verified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The space-around alignment distributes space as 1-row gap / AB / 2-row
gap / CD / 1-row gap for a 6-row container with 2 content rows.
Expected value was "\nAB\n\nCD\n\n" but Ink produces "\nAB\n\n\nCD\n".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
height=6 containers produce 6 lines. Ink's renderToString returns
"A\n\n\nB\n\n" (preserving trailing blank lines). The tests incorrectly
expected "A\n\n\nB" with a comment claiming lastFrame({ trimLines: true })
trims trailing newlines — it does not; it only trims trailing spaces per
line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ink's Output.get() calls .trimEnd() on each line of the rendered grid.
Without this, vue-tui's paint grid always fills the full root width with
spaces, causing raw frame output to contain 100-char-wide padding. This
broke tests using { raw: true } and made meaningful trailing-space
assertions impossible with { trimLines: true }.
Also corrects test assertions for 'set width in percent' and 'relative
position' that expected trailing spaces which Ink never produces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Distributes across text, static, transform, newline-spacer,
error-handling, and prop-reset test files.
Tests requiring features not yet in vue-tui are marked test.todo:
- <Transform> inside <Text> (paint pass limitation)
- multi-line transform (transform nodes not yoga carriers)
- text/box nesting validation (no runtime validation yet)
- complex Static rerender patterns (Ink-specific API)
Also fixes wrap="hard" to use wordWrap:false so text breaks at exact
character boundaries rather than word boundaries, matching Ink behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translates flex, direction, wrap, align, justify, gap, padding,
margin, width-height, position, display, and overflow tests from
Ink. Adds trimLines mode and trailing-newline strip to test helper.
Fixes Box type for alignContent (space-between/around/evenly) and
alignSelf (stretch/baseline).
21 tests fail due to behavioral differences (overflow clipping,
percentage dimensions, relative position offsets, align-content
space-* variants). These represent real gaps to investigate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>