diff --git a/.agents/docs/parity-ledger.md b/.agents/docs/parity-ledger.md index 2ba41af..9b46b74 100644 --- a/.agents/docs/parity-ledger.md +++ b/.agents/docs/parity-ledger.md @@ -47,9 +47,9 @@ Non-obvious calls made while fixing gaps, recorded for review in the final repor | G11 | render-lifecycle-reconciler | Resize handler does not clear+reset on terminal-width decrease | P2 | merged | `fix/parity-resize-clear` | #41 | | G12 | render-lifecycle-reconciler | Renderer frame width/rows lack terminal-size fallback (only ?? defaults) | P2 | merged | `fix/parity-renderer-size` | #33 | | G13 | box-layout-border | Custom border style objects (BoxStyle) not supported | P3 | merged | `fix/parity-custom-border` | #42 | -| G14 | app-exit-instances-animation-sr | No per-stdout instance reuse/guard — two concurrent renderers can compete for the same stdout | P3 | pr-open | `fix/parity-instance-reuse` | #43 | +| G14 | app-exit-instances-animation-sr | No per-stdout instance reuse/guard — two concurrent renderers can compete for the same stdout | P3 | merged | `fix/parity-instance-reuse` | #43 | | G15 | box-layout-border | Vertical border sides not shifted up when borderTop=false (Ink offsetY) — left/right rails mispositioned | P2 | merged | `fix/parity-border-1cell` | #37 | -| G16 | box-layout-border | Per-edge borderDimColor=false cannot override general borderDimColor (`\|\| dimAll` vs Ink's `??`) | P3 | todo | — | — | +| G16 | box-layout-border | Per-edge borderDimColor=false cannot override general borderDimColor (`\|\| dimAll` vs Ink's `??`) | P3 | pr-open | `fix/parity-border-dim` | #44 | | G17 | render-lifecycle-reconciler | Screen-reader live-path edges: still grid-painted (Ink linearizes, skipStaticElements:false) + empty SR frame gets a trailing newline (Ink writes wrapped output directly) | P3 | todo | — | — | ## Gap details diff --git a/packages/runtime-tests/integration/components/borders.test.tsx b/packages/runtime-tests/integration/components/borders.test.tsx index 4732b23..93957aa 100644 --- a/packages/runtime-tests/integration/components/borders.test.tsx +++ b/packages/runtime-tests/integration/components/borders.test.tsx @@ -1303,6 +1303,29 @@ test("G13: custom border object renders correct glyphs", async ({ expect }) => { expect(lines[2]).toMatch(/^GF+E$/); }); +// G16 — per-edge borderDimColor=false can override general borderDimColor (Ink parity) +// With borderDimColor (general dim ON) and borderTopDimColor={false} (top dim explicitly OFF), +// the top border must NOT be dim while other edges (e.g. bottom) remain dim. +// Before the fix, `false || true` = true caused the top edge to be wrongly dimmed. +test("G16: per-edge borderDimColor=false overrides general borderDimColor", async ({ expect }) => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hi + + )), + { columns: 100 }, + ); + const frame = lastFrame()!; + const lines = frame.split("\n"); + const topLine = lines[0]!; + const bottomLine = lines[lines.length - 1]!; + // Top border must NOT contain dim ANSI code (\x1b[2m / "[2m") + expect(topLine).not.toContain("[2m"); + // Bottom border MUST contain dim ANSI code (general dim still applies) + expect(bottomLine).toContain("[2m"); +}); + // borderDimColor should not dim styled child Text touching left edge test("borderDimColor does not dim styled child Text touching left edge", async ({ expect }) => { const { lastFrame } = await render( diff --git a/packages/runtime/src/components/Box.ts b/packages/runtime/src/components/Box.ts index a0a26f1..ce1034d 100644 --- a/packages/runtime/src/components/Box.ts +++ b/packages/runtime/src/components/Box.ts @@ -117,11 +117,21 @@ const BoxImpl = defineComponent({ // Ink types borderStyle as `keyof Boxes | BoxStyle`; we mirror that here. borderStyle: [String, Object] as PropType, borderColor: [String, Array], - borderDimColor: Boolean, - borderTopDimColor: Boolean, - borderBottomDimColor: Boolean, - borderLeftDimColor: Boolean, - borderRightDimColor: Boolean, + // `default: undefined` is intentional and load-bearing: Vue's boolean-casting + // rule coerces absent Boolean props to `false` only when there is no explicit + // default. Adding `default: undefined` suppresses that coercion so absent + // per-edge dim props arrive in the paint pass as `undefined`, not `false`. + // This lets `edgeDim = (perEdge ?? generalDim)` correctly fall back to the + // general value only when the per-edge prop was truly omitted — mirroring + // Ink render-border.ts:54 which uses real-undefined via React's prop model + // (G16). The `Boolean` type is kept so Vue still accepts bare-attribute + // `` in templates (coerces `""` → `true`) and passes + // TypeScript type-checking for consumers. + borderDimColor: { type: Boolean as PropType, default: undefined }, + borderTopDimColor: { type: Boolean as PropType, default: undefined }, + borderBottomDimColor: { type: Boolean as PropType, default: undefined }, + borderLeftDimColor: { type: Boolean as PropType, default: undefined }, + borderRightDimColor: { type: Boolean as PropType, default: undefined }, borderTop: { type: Boolean, default: true }, borderBottom: { type: Boolean, default: true }, borderLeft: { type: Boolean, default: true }, diff --git a/packages/runtime/src/paint/paint.ts b/packages/runtime/src/paint/paint.ts index 422569b..7e9674a 100644 --- a/packages/runtime/src/paint/paint.ts +++ b/packages/runtime/src/paint/paint.ts @@ -354,12 +354,16 @@ function drawBorder( const right = props["borderRight"] !== false; const borderColor = props["borderColor"] as string | undefined; - const dimAll = !!props["borderDimColor"]; + // Keep the raw (non-coerced) general dim value so per-edge overrides work correctly. + const generalDim = props["borderDimColor"] as boolean | undefined; function colorizeEdge(s: string, edge: "top" | "bottom" | "left" | "right"): string { const capEdge = edge.charAt(0).toUpperCase() + edge.slice(1); const edgeColor = (props[`border${capEdge}Color`] as string | undefined) ?? borderColor; - const edgeDim = (props[`border${capEdge}DimColor`] as boolean | undefined) || dimAll; + // Use nullish coalescing (not ||) so an explicit per-edge `false` wins over + // generalDim — only `undefined` falls back to the general value. + // Mirrors Ink render-border.ts:54: `borderTopDimColor ?? borderDimColor`. + const edgeDim = (props[`border${capEdge}DimColor`] as boolean | undefined) ?? generalDim; // Ink parity (render-border.ts:44-52): an edge's background comes only from the // per-edge or general border background — never from the Box's own backgroundColor. const edgeBg =