diff --git a/packages/runtime-tests/integration/components/background-color.test.tsx b/packages/runtime-tests/integration/components/background-color.test.tsx
new file mode 100644
index 0000000..b3ba8a6
--- /dev/null
+++ b/packages/runtime-tests/integration/components/background-color.test.tsx
@@ -0,0 +1,47 @@
+import chalk from "chalk";
+import { afterEach, beforeEach, expect, test } from "vite-plus/test";
+import { render } from "@vue-tui/testing";
+import { Box, Text } from "@vue-tui/runtime";
+
+const BG_BLUE = "\x1b[44m";
+
+let prevLevel: typeof chalk.level;
+beforeEach(() => {
+ prevLevel = chalk.level;
+ chalk.level = 1;
+});
+afterEach(() => {
+ chalk.level = prevLevel;
+});
+
+test("Box backgroundColor produces ANSI background codes", async () => {
+ const { frames } = await render(
+ () => ,
+ { columns: 10 },
+ );
+ expect(frames.at(-1)).toContain(BG_BLUE);
+});
+
+test("Box backgroundColor survives border rendering", async () => {
+ const { frames } = await render(
+ () => ,
+ { columns: 10 },
+ );
+ const raw = frames.at(-1)!;
+ expect(raw).toContain(BG_BLUE);
+ expect(raw).toContain("┌");
+});
+
+test("child Text inherits backgroundColor from parent Box", async () => {
+ const { frames } = await render(
+ () => (
+
+ hello
+
+ ),
+ { columns: 20 },
+ );
+ const raw = frames.at(-1)!;
+ expect(raw).toContain("hello");
+ expect(raw).toContain(BG_BLUE);
+});
diff --git a/packages/runtime-tests/package.json b/packages/runtime-tests/package.json
index a712add..e8bad91 100644
--- a/packages/runtime-tests/package.json
+++ b/packages/runtime-tests/package.json
@@ -12,6 +12,7 @@
"@vitejs/plugin-vue-jsx": "catalog:",
"@vue-tui/runtime": "workspace:*",
"@vue-tui/testing": "workspace:*",
+ "chalk": "catalog:",
"typescript": "^6.0.3",
"vite-plus": "^0.1.20",
"vue": "^3.4.0"
diff --git a/packages/runtime/src/paint/paint.ts b/packages/runtime/src/paint/paint.ts
index 9e0d820..8978f6f 100644
--- a/packages/runtime/src/paint/paint.ts
+++ b/packages/runtime/src/paint/paint.ts
@@ -91,7 +91,10 @@ function placeLine(grid: string[][], x: number, y: number, line: string): void {
}
function renderTextWithInlineStyles(node: TuiText | TuiVirtualText, acc: TextProps = {}): string {
- const merged: TextProps = { ...acc, ...node.props };
+ const defined = Object.fromEntries(
+ Object.entries(node.props).filter(([, v]) => v !== undefined),
+ );
+ const merged: TextProps = { ...acc, ...defined };
let out = "";
for (const child of node.children) {
if (child.type === "text-leaf") {
@@ -120,28 +123,31 @@ function drawBorder(
if (!chars) return;
if (w < 2 || h < 2) return;
- // Per-edge toggles default to true when borderStyle is set.
const top = props["borderTop"] !== false;
const bottom = props["borderBottom"] !== false;
const left = props["borderLeft"] !== false;
const right = props["borderRight"] !== false;
- // Corners require both adjacent edges to be enabled; otherwise the
- // adjacent edge character is used as a "stub" so the visible edge still
- // terminates cleanly.
+ const borderColor = props["borderColor"] as string | undefined;
+ const bgColor = props["backgroundColor"] as string | undefined;
+ const colorProps: TextProps = {};
+ if (borderColor) colorProps.color = borderColor;
+ if (bgColor) colorProps.backgroundColor = bgColor;
+ const colorize = (s: string) => (borderColor || bgColor ? applyChalk(s, colorProps) : s);
+
if (top) {
const tl = left ? chars.topLeft : chars.top;
const tr = right ? chars.topRight : chars.top;
- output.write(x, y, [tl + chars.top.repeat(w - 2) + tr], transformers);
+ output.write(x, y, [colorize(tl + chars.top.repeat(w - 2) + tr)], transformers);
}
if (bottom) {
const bl = left ? chars.bottomLeft : chars.bottom;
const br = right ? chars.bottomRight : chars.bottom;
- output.write(x, y + h - 1, [bl + chars.bottom.repeat(w - 2) + br], transformers);
+ output.write(x, y + h - 1, [colorize(bl + chars.bottom.repeat(w - 2) + br)], transformers);
}
for (let i = 1; i < h - 1; i++) {
- if (left) output.write(x, y + i, [chars.left], transformers);
- if (right) output.write(x + w - 1, y + i, [chars.right], transformers);
+ if (left) output.write(x, y + i, [colorize(chars.left)], transformers);
+ if (right) output.write(x + w - 1, y + i, [colorize(chars.right)], transformers);
}
}
@@ -175,6 +181,7 @@ function paintNode(
x0: number,
y0: number,
transformers: Transformer[],
+ inheritedBg?: string,
): void {
switch (node.type) {
case "root": {
@@ -187,18 +194,20 @@ function paintNode(
const y = y0 + layout.top;
const w = Math.max(0, Math.floor(layout.width));
const h = Math.max(0, Math.floor(layout.height));
+ const bg = (node.props["backgroundColor"] as string | undefined) ?? inheritedBg;
if (node.props["backgroundColor"]) {
fillBackground(output, x, y, w, h, node.props["backgroundColor"], transformers);
}
if (node.props["borderStyle"]) {
drawBorder(output, x, y, w, h, node.props, transformers);
}
- for (const child of node.children) paintNode(child, output, x, y, transformers);
+ for (const child of node.children) paintNode(child, output, x, y, transformers, bg);
return;
}
case "text": {
const layout = node.yoga.getComputedLayout();
- const text = renderTextWithInlineStyles(node);
+ const bgProps: TextProps = inheritedBg ? { backgroundColor: inheritedBg } : {};
+ const text = renderTextWithInlineStyles(node, bgProps);
const wrapped = wrapText(
text,
Math.max(1, Math.floor(layout.width)),
@@ -214,7 +223,7 @@ function paintNode(
}
case "transform": {
const next = [...transformers, node.transform];
- for (const child of node.children) paintNode(child, output, x0, y0, next);
+ for (const child of node.children) paintNode(child, output, x0, y0, next, inheritedBg);
return;
}
case "virtual-text":
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3322df2..036235b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -12,6 +12,9 @@ catalogs:
'@vitejs/plugin-vue-jsx':
specifier: ^5.1.5
version: 5.1.5
+ chalk:
+ specifier: ^5.6.2
+ version: 5.6.2
vite-plus:
specifier: latest
version: 0.1.22
@@ -186,6 +189,9 @@ importers:
'@vue-tui/testing':
specifier: workspace:*
version: link:../testing
+ chalk:
+ specifier: 'catalog:'
+ version: 5.6.2
typescript:
specifier: ^6.0.3
version: 6.0.3