From eb7d9ddb6fc2cf364b68ec82748f27e273d0801b Mon Sep 17 00:00:00 2001 From: Sheng Kun Chang Date: Wed, 24 Jun 2026 05:37:18 +0800 Subject: [PATCH] fix(Notch): bleed the headOnly tab connecting edge 1px to kill the fractional-DPI seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A headOnly Notch tab sits flush on the surface below it (e.g. the agent sidebar's active tab over its message body). Both are filled with the same --color-on-background but are SEPARATE fills meeting edge-to-edge at the tab's flat connecting edge. When that shared boundary lands on a fractional device-pixel row — sub-pixel transforms, or non-integer DPR such as Windows display scaling — each fill paints <100% coverage on that physical row and the darker layer beneath shows through as a crisp 1px line. Invisible on integer-DPR / retina, where the edges snap and abut cleanly. Extend the tab fill 1px past the flat connecting edge (build x=0 -> -1) so the two surfaces OVERLAP instead of abutting, and render the headOnly svg with overflow:visible so the bleed is not clipped. Only the build-x=0 edge moves — the radiused tab corners (the tab top) are byte-for-byte unchanged, and bleed=0 reproduces the original path. Fixes the seam for every consumer of the agent sidebar / notch tab. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- .../ui/surfaces/notch/Notch.test.tsx | 13 ++++++++ src/components/ui/surfaces/notch/Notch.tsx | 30 +++++++++++++++---- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 47574ec74..3af983163 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mirrorstack-ai/web-ui-kit", "packageManager": "pnpm@10.29.3", - "version": "0.5.13", + "version": "0.5.14", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { diff --git a/src/components/ui/surfaces/notch/Notch.test.tsx b/src/components/ui/surfaces/notch/Notch.test.tsx index 44c6bd566..08a015e8b 100644 --- a/src/components/ui/surfaces/notch/Notch.test.tsx +++ b/src/components/ui/surfaces/notch/Notch.test.tsx @@ -50,6 +50,19 @@ describe("Notch", () => { expect(svg).toHaveAttribute("viewBox", "-0.5 -0.5 67 47"); }); + it("bleeds the headOnly connecting edge to avoid a fractional-DPI seam", () => { + const { container } = render( + , + ); + const svg = container.querySelector("svg"); + // The flat connecting edge (build x=0) is extended to x=-1 so the tab fill + // OVERLAPS the surface below rather than abutting it edge-to-edge — otherwise + // the shared boundary aliases into a 1px seam on non-integer DPR. overflow + // must be visible so that 1px bleed isn't clipped to the box. + expect(svg).toHaveAttribute("overflow", "visible"); + expect(svg?.querySelector("path")?.getAttribute("d")).toMatch(/^M -1,/); + }); + it("applies fill and stroke", () => { const { container } = render( , diff --git a/src/components/ui/surfaces/notch/Notch.tsx b/src/components/ui/surfaces/notch/Notch.tsx index 518407222..637ec853a 100644 --- a/src/components/ui/surfaces/notch/Notch.tsx +++ b/src/components/ui/surfaces/notch/Notch.tsx @@ -80,17 +80,34 @@ function buildPath( return d.join(" "); } -function buildHeadPath(nw: number, nh: number, r: number, ir: number, atStart: boolean, atEnd: boolean) { +// A headOnly tab sits flush on the surface below it. Extending its fill 1px past +// that flat CONNECTING edge so the two same-colour fills OVERLAP (rather than abut +// edge-to-edge) removes a sub-pixel seam: when the shared boundary lands on a +// fractional device-pixel row — sub-pixel transforms or non-integer DPR such as +// Windows display scaling — each fill paints <100% coverage on that physical row +// and the layer beneath shows through as a 1px line. A whole-pixel overlap closes +// the gap. (Invisible on integer-DPR/retina, where the edges snap and abut cleanly.) +const HEAD_SEAM_BLEED = 1; + +function buildHeadPath( + nw: number, nh: number, r: number, ir: number, + atStart: boolean, atEnd: boolean, + // px to extend the flat connecting edge (build x = 0) past the box, toward the + // surface the tab attaches to. Only the build-x=0 edge moves; the radiused tab + // corners (build x = w, the tab TOP) are untouched. `bleed = 0` ⇒ original path. + bleed = 0, +) { const w = nw + ir; const topIr = atStart ? 0 : ir; const botIr = atEnd ? 0 : ir; + const x0 = -bleed; const d: string[] = []; if (!atStart) { - d.push(`M 0,0`); + d.push(`M ${x0},0`); d.push(`A ${ir},${ir} 0 0,0 ${ir},${topIr}`); } else { - d.push(`M 0,${topIr}`); + d.push(`M ${x0},${topIr}`); } d.push(`H ${w - r}`); @@ -100,7 +117,7 @@ function buildHeadPath(nw: number, nh: number, r: number, ir: number, atStart: b d.push(`H ${ir}`); if (!atEnd) { - d.push(`A ${ir},${ir} 0 0,0 0,${topIr + nh + botIr}`); + d.push(`A ${ir},${ir} 0 0,0 ${x0},${topIr + nh + botIr}`); } d.push(`V 0`); @@ -188,7 +205,7 @@ export function Notch({ const botIr = atEnd ? 0 : ir; const pathW = notchWidth + ir; const pathH = notchHeight + topIr + botIr; - const headPath = buildHeadPath(notchWidth, notchHeight, radius, ir, atStart, atEnd); + const headPath = buildHeadPath(notchWidth, notchHeight, radius, ir, atStart, atEnd, HEAD_SEAM_BLEED); let svgW: number, svgH: number; if (horiz) { svgW = pathW; svgH = pathH; } @@ -201,10 +218,13 @@ export function Notch({ // every side so the centered stroke renders fully INSIDE the box. (Sizing // the element to svgW+strokeWidth instead pins it `pad` larger and pushes // the right & bottom strokes outside the box.) + // `overflow: visible` lets the HEAD_SEAM_BLEED extend the fill 1px past the + // box on the connecting edge without being clipped to the viewport.