mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): match compact link preview thumbnail corners to card shell (#5711)
## 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 | |  |  | ## 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>
This commit is contained in:
@@ -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<string | null>(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<HTMLDivElement | null>(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 ? (
|
||||
<AttachmentMedia
|
||||
ref={thumbnailRef}
|
||||
aria-hidden={showImage ? undefined : "true"}
|
||||
className="aspect-auto h-16 w-26 rounded-xl bg-muted"
|
||||
className="aspect-auto h-16 w-26 rounded-2xl bg-muted"
|
||||
data-link-preview-thumbnail=""
|
||||
variant="image"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user