From eedcd886a04833a78c18f49931abe68792518f97 Mon Sep 17 00:00:00 2001 From: morgmart <98432065+morgmart@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:42:24 -0500 Subject: [PATCH] fix(desktop): match compact link preview thumbnail corners to card shell (#5711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem In compact link preview cards with an image, the thumbnail's corners looked inconsistent — the flush left side and the interior right side read as different shapes. ## Cause The `Attachment` shell rounds its corners with a **smooth-corner (squircle) clip path** via `useSmoothCorners`, not a plain `border-radius`. In compact image mode the shell has `p-0`, so the thumbnail sits flush against its left, top, and bottom edges. That means: - **Left corners** are carved by the shell's smoothed clip path. - **Right corners** are drawn by the thumbnail's own plain `border-radius`. A circular arc and a smoothed corner of the *same* radius are different shapes (at 16px the smoothed curve starts 25.6px along the edge instead of 16px). So the two sides could never match by picking a radius value — the thumbnail's class has no effect on its left corners at all. ## Fix Give the thumbnail the same `useSmoothCorners` treatment as the shell, so both sides share one curve. - Radius token unchanged: `rounded-2xl` (16px). - The shell and the shared `Attachment` component are untouched, so no other `Attachment` consumer changes. Verified on a rendered card — thumbnail vs shell now agree on all three: arc radius (16), smoothing (0.6), and curve start (25.6px). ## Hardening The underlying issue is an invariant that lived nowhere: **a child flush against a smooth-cornered parent must share its corner treatment.** This is why the bug was easy to introduce and hard to diagnose. - Documented the invariant in `smoothCorners.ts`, where anyone reaching for the hook will see it. - Added an `expectSmoothCorners()` guard to the existing compact-preview e2e test. Confirmed it **fails** when the fix is reverted, so it genuinely bites. Note: this cannot be a lint rule — "flush" is a runtime layout fact, not visible in the source. ## Known follow-up (not in this PR) The composer link preview (`useComposerLinkPreviews.tsx`) has the same latent issue: a flush thumbnail with a hand-copied `rounded-l-2xl` that happens to match the shell's current 16px. It is correct today only by coincidence of two literals agreeing. Left for a separate PR rather than expanding scope here. ## Screenshots The same compact card and content before and after the change. | Before | After | | --- | --- | | Original `rounded-xl` (12px) thumbnail: left corners are clipped by the card’s 16px smooth silhouette while the right corners keep the thumbnail’s smaller plain radius | `rounded-2xl` (16px) thumbnail with the same smooth-corner treatment as the card | | ![Before: compact link preview with the original 12px thumbnail corners](https://raw.githubusercontent.com/block/buzz/9911631a0698cae25df4d41471dde69faa4169dc/pr-5711--before.png) | ![After: compact link preview with matching 16px smooth thumbnail corners](https://raw.githubusercontent.com/block/buzz/9911631a0698cae25df4d41471dde69faa4169dc/pr-5711--after.png) | ## Verification - `pnpm exec biome check` on all three touched files - `pnpm exec tsc --noEmit` - `node --test src/shared/ui/smoothCorners.test.mjs` — 3 passed - All 18 link-preview e2e tests pass - Guard verified to fail without the fix, then pass with it Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> --- .../ui/compact-link-preview-attachment.tsx | 13 ++++++-- desktop/src/shared/ui/smoothCorners.ts | 33 +++++++++++++++++++ desktop/tests/e2e/messaging.spec.ts | 12 +++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/desktop/src/shared/ui/compact-link-preview-attachment.tsx b/desktop/src/shared/ui/compact-link-preview-attachment.tsx index ad71e8b6e..0a9e52bca 100644 --- a/desktop/src/shared/ui/compact-link-preview-attachment.tsx +++ b/desktop/src/shared/ui/compact-link-preview-attachment.tsx @@ -1,5 +1,5 @@ import { ImageOff } from "lucide-react"; -import { useState } from "react"; +import { useRef, useState } from "react"; import type { ResolvedLinkPreview } from "@/shared/lib/useResolvedLinkPreviews"; import { cn } from "@/shared/lib/cn"; @@ -13,6 +13,7 @@ import { } from "@/shared/ui/attachment"; import { LinkPreviewControls } from "@/shared/ui/link-preview-controls"; import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; +import { useSmoothCorners } from "@/shared/ui/smoothCorners"; function getHostname(preview: ResolvedLinkPreview): string { if (preview.href.startsWith("buzz://")) return preview.provider; @@ -64,6 +65,13 @@ export function CompactLinkPreviewAttachment({ const imageSrc = preview.imageState === "image" ? (preview.imageDataUrl ?? null) : null; const [failedImageSrc, setFailedImageSrc] = useState(null); + // The shell clips its corners with a smooth-corner (squircle) path, and the + // thumbnail sits flush against its left edge — so the thumbnail's left + // corners are carved by that path while its right corners are its own. + // A plain border-radius can never match a squircle of the same radius, so + // give the thumbnail the same treatment: both sides then share one curve. + const thumbnailRef = useRef(null); + useSmoothCorners(thumbnailRef); const showImage = Boolean(imageSrc && failedImageSrc !== imageSrc); const showFallback = preview.imageState === "fallback" || Boolean(imageSrc && !showImage); @@ -88,8 +96,9 @@ export function CompactLinkPreviewAttachment({ > {reserveImage ? ( diff --git a/desktop/src/shared/ui/smoothCorners.ts b/desktop/src/shared/ui/smoothCorners.ts index 4927ea49e..07ed26883 100644 --- a/desktop/src/shared/ui/smoothCorners.ts +++ b/desktop/src/shared/ui/smoothCorners.ts @@ -1,3 +1,36 @@ +/** + * Corner smoothing ("squircle") for media surfaces. + * + * CSS `border-radius` can only draw a circular arc. This helper reads an + * element's computed radius and replaces the shape with an equivalent + * *smoothed* corner, applied at runtime as an inline `clip-path`. The radius + * token stays the source of truth — `rounded-2xl` still means a 16px corner, + * it just renders as a smoothed 16px corner (the curve starts 1.6x further + * along the edge, so it reads wider and softer than a plain arc). + * + * INVARIANT — flush children must share the parent's corner treatment. + * + * A smooth-cornered parent clips its subtree. Any child that sits *flush* + * against a clipped edge therefore has those corners carved by the parent's + * smoothed path, while its remaining corners are drawn by its own + * `border-radius`. A plain arc and a smoothed corner of the *same* radius are + * different shapes, so such a child renders visibly mismatched corners — the + * number matching is not enough. + * + * So when a child is flush to a smooth-cornered parent (typically because the + * parent zeroes its padding, e.g. `p-0`), either: + * - give the child `useSmoothCorners` too, so both sides share one curve + * (see `compact-link-preview-attachment.tsx`), or + * - give the child no radius on the flush side and let the parent's clip own + * that silhouette entirely. + * + * Inset children are unaffected: they never share a corner with the clip. + * + * This cannot be caught by a lint rule — "flush" is a runtime layout fact, not + * something visible in the source. Guard it with `expectSmoothCorners()` from + * `desktop/tests/helpers/css.ts` instead. + */ + import * as React from "react"; export const SMOOTH_CORNER_SMOOTHING = 0.6; diff --git a/desktop/tests/e2e/messaging.spec.ts b/desktop/tests/e2e/messaging.spec.ts index 68a5fad7f..cdfe8e84b 100644 --- a/desktop/tests/e2e/messaging.spec.ts +++ b/desktop/tests/e2e/messaging.spec.ts @@ -531,6 +531,18 @@ test("sent link preview media uses the authenticated proxy in compact and rich c .poll(() => compactThumbnail.evaluate((image) => image.naturalWidth)) .toBe(40); + // The compact thumbnail sits flush against the card shell's left edge, so the + // shell's smooth-corner clip carves those corners. It must therefore carry + // the same treatment itself, or its right corners render as a plain arc + // against the shell's smoothed left corners. See the invariant in + // `shared/ui/smoothCorners.ts`. + const compactThumbnailFrame = compactPreview + .locator("[data-link-preview-thumbnail]") + .first(); + await expectCornerRadiusPx(compactPreview, 16); + await expectCornerRadiusPx(compactThumbnailFrame, 16); + await expectSmoothCorners(compactThumbnailFrame); + await openSettings(page, "appearance"); await page.getByTestId("link-preview-style-trigger").click(); await page.getByTestId("link-preview-style-rich").click();