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;
+}