diff --git a/packages/runtime-tests/integration/components/newline-standalone.test.tsx b/packages/runtime-tests/integration/components/newline-standalone.test.tsx new file mode 100644 index 0000000..1b805d0 --- /dev/null +++ b/packages/runtime-tests/integration/components/newline-standalone.test.tsx @@ -0,0 +1,42 @@ +import { defineComponent } from "vue"; +import { expect, test } from "vite-plus/test"; +import { render } from "@vue-tui/testing"; +import { Box, Text, Newline } from "@vue-tui/runtime"; + +test("Newline works standalone (outside Text) as a yoga carrier", async () => { + // In Ink, Newline renders as ink-text (yoga carrier) so it works + // outside a Text context as a standalone line break. + const { lastFrame } = await render( + defineComponent(() => () => ( + + + hi + + )), + { columns: 100 }, + ); + // Newline should occupy a line before "hi" + const frame = lastFrame({ trimLines: true })!; + const lines = frame.split("\n"); + expect(lines.length).toBeGreaterThanOrEqual(2); + expect(lines.at(-1)).toBe("hi"); +}); + +test("Newline count=2 adds two blank lines standalone", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + above + + below + + )), + { columns: 100 }, + ); + const frame = lastFrame({ trimLines: true })!; + expect(frame).toContain("above"); + expect(frame).toContain("below"); + const lines = frame.split("\n"); + // "above", 2 blank lines, "below" = at least 4 lines + expect(lines.length).toBeGreaterThanOrEqual(4); +}); diff --git a/packages/runtime/src/components/Newline.ts b/packages/runtime/src/components/Newline.ts index 8fa9637..b96ca94 100644 --- a/packages/runtime/src/components/Newline.ts +++ b/packages/runtime/src/components/Newline.ts @@ -1,9 +1,28 @@ -import { defineComponent, h } from "vue"; +import { defineComponent, getCurrentInstance, h } from "vue"; export const Newline = defineComponent({ name: "Newline", props: { count: { type: Number, default: 1 } }, setup(props) { - return () => h("virtual-text", {}, "\n".repeat(props.count)); + return () => { + const content = "\n".repeat(props.count); + // Inside a Text parent, render as inline virtual-text. + // Outside Text, render as "text" (yoga carrier) so Newline participates + // in layout standalone, matching Ink's ink-text behavior. + if (isInsideText()) { + return h("virtual-text", {}, content); + } + return h("text", {}, content); + }; }, }); + +function isInsideText(): boolean { + let parent = getCurrentInstance()?.parent; + while (parent) { + const name = parent.type && (parent.type as { name?: string }).name; + if (name === "Text") return true; + parent = parent.parent; + } + return false; +}