From 49aacb0078b8c9c41a27c1d49bf4d54419ef1891 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Mon, 24 Aug 2026 12:59:24 -0700 Subject: [PATCH 1/3] fix(desktop): polish inline chip states Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho --- .../src/shared/styles/globals/composer.css | 8 ++++- .../src/shared/styles/globals/markdown.css | 13 +++----- .../e2e/entity-link-recipient-cards.spec.ts | 31 +++++++++++++++++++ desktop/tests/e2e/mentions.spec.ts | 29 +++++++++++++++++ 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/desktop/src/shared/styles/globals/composer.css b/desktop/src/shared/styles/globals/composer.css index 781ac8a1968..63d31f5fac9 100644 --- a/desktop/src/shared/styles/globals/composer.css +++ b/desktop/src/shared/styles/globals/composer.css @@ -260,8 +260,14 @@ caret into their decorated range and swallows the trailing space after an @mention. Composer Buzz links are atom nodes, but their labels may still fragment between characters; only the icon and leading label fragment stay - together. */ + together. The line-height keeps each inline decoration box tall enough to + paint both block-padding edges without clipping. */ .rich-text-composer .tiptap .mention-chip { display: inline; min-height: 0; + line-height: calc( + 1em + + var(--inline-chip-padding-block-start) + + var(--inline-chip-padding-block-end) + ); } diff --git a/desktop/src/shared/styles/globals/markdown.css b/desktop/src/shared/styles/globals/markdown.css index 9d0c382ac3a..135c036e74c 100644 --- a/desktop/src/shared/styles/globals/markdown.css +++ b/desktop/src/shared/styles/globals/markdown.css @@ -1,5 +1,5 @@ .message-markdown { - --inline-chip-padding-block-start: 0.1875rem; + --inline-chip-padding-block-start: 0.125rem; --inline-chip-padding-block-end: 0.125rem; --inline-chip-padding-inline: 0.25rem; --inline-chip-min-height: calc( @@ -101,15 +101,11 @@ } .message-markdown .mention-chip.buzz-link-unavailable, -.message-markdown .mention-chip.buzz-link-unavailable:hover { - background: hsl(var(--secondary)); - color: hsl(var(--secondary-foreground) / 0.7); -} - +.message-markdown .mention-chip.buzz-link-unavailable:hover, .message-markdown .mention-chip.buzz-link-deleted, .message-markdown .mention-chip.buzz-link-deleted:hover { - background: hsl(var(--destructive) / 0.12); - color: hsl(var(--destructive)); + background: hsl(var(--secondary)); + color: hsl(var(--secondary-foreground) / 0.7); } .message-markdown .mention-chip.inbox-channel-chip { @@ -336,6 +332,7 @@ } .message-markdown .inline-chip-icon-human::before { + transform: translateY(calc(-50% + 0.0625rem)); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8'/%3E%3C/svg%3E") center / contain no-repeat; diff --git a/desktop/tests/e2e/entity-link-recipient-cards.spec.ts b/desktop/tests/e2e/entity-link-recipient-cards.spec.ts index 056b059b0e7..6ca502c3462 100644 --- a/desktop/tests/e2e/entity-link-recipient-cards.spec.ts +++ b/desktop/tests/e2e/entity-link-recipient-cards.spec.ts @@ -662,6 +662,10 @@ test("deleted reply links identify deletion and fall back to their thread root", test("deleted top-level message links identify deletion and fall back to channel navigation", async ({ page, }) => { + await page.addInitScript(() => { + window.localStorage.setItem("buzz-theme", "buzz-dark"); + window.localStorage.setItem("buzz-accent-color", "#c0a2f1"); + }); const missingMessageId = "d".repeat(64); const channelId = "9dae0116-799b-5071-a0a8-fdd30a91a35d"; const link = `buzz://message?channel=${channelId}&id=${missingMessageId}`; @@ -685,6 +689,33 @@ test("deleted top-level message links identify deletion and fall back to channel ); await expect(deletedLink).toHaveClass(/buzz-link-deleted/); await expect(deletedLink).not.toHaveClass(/buzz-link-unavailable/); + const unavailableColors = await linkMessage.evaluate((message) => { + const probe = document.createElement("span"); + probe.className = "mention-chip buzz-link-unavailable"; + message.querySelector(".message-markdown")?.append(probe); + const styles = getComputedStyle(probe); + const colors = { + backgroundColor: styles.backgroundColor, + color: styles.color, + }; + probe.remove(); + return colors; + }); + await expect + .poll(() => + deletedLink.evaluate((element) => { + const styles = getComputedStyle(element); + return { + backgroundColor: styles.backgroundColor, + color: styles.color, + }; + }), + ) + .toEqual(unavailableColors); + await waitForAnimations(page); + await linkMessage.screenshot({ + path: `${SHOTS}/deleted-message-link-after.png`, + }); await deletedLink.hover(); await expect(page.getByRole("tooltip")).toHaveText("Message deleted"); diff --git a/desktop/tests/e2e/mentions.spec.ts b/desktop/tests/e2e/mentions.spec.ts index ad1acf87c34..dc05486cec4 100644 --- a/desktop/tests/e2e/mentions.spec.ts +++ b/desktop/tests/e2e/mentions.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from "@playwright/test"; +import { waitForAnimations } from "../helpers/animations"; + import { installMockBridge, openChannelBrowser, @@ -837,6 +839,9 @@ test("mention autocomplete caps global people search at 50 results", async ({ test("selecting a person mention inserts @Name into input", async ({ page, }) => { + await page.addInitScript(() => { + window.localStorage.setItem("buzz-theme", "buzz-dark"); + }); await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); @@ -865,6 +870,30 @@ test("selecting a person mention inserts @Name into input", async ({ ), ); expect(iconMask).toContain("data:image/svg+xml"); + await expect(mentionChip).toHaveCSS("line-height", "18px"); + expect(await mentionChip.evaluate((element) => element.clientHeight)).toBe( + await mentionChip.evaluate((element) => element.scrollHeight), + ); + const humanIconTranslateY = await mentionChip.evaluate( + (element) => + new DOMMatrix(getComputedStyle(element, "::before").transform).m42, + ); + const channelIconTranslateY = await input.evaluate((composer) => { + const probe = document.createElement("span"); + probe.className = + "mention-chip inline-chip-with-icon inline-chip-icon-channel"; + composer.append(probe); + const translateY = new DOMMatrix( + getComputedStyle(probe, "::before").transform, + ).m42; + probe.remove(); + return translateY; + }); + expect(humanIconTranslateY - channelIconTranslateY).toBeCloseTo(1); + await waitForAnimations(page); + await page.getByTestId("message-composer").screenshot({ + path: "test-results/inline-chip-polish/composer-after.png", + }); }); test("immediate ArrowLeft after a person mention is not bounced past the trailing space", async ({ From 0475bf9bf2cf831660fb85e996a18f1f6ee531f0 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Mon, 24 Aug 2026 14:24:55 -0700 Subject: [PATCH 2/3] fix(desktop): keep composer chips within paint bounds Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho --- desktop/src/shared/styles/globals/composer.css | 1 + desktop/tests/e2e/mentions.spec.ts | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/desktop/src/shared/styles/globals/composer.css b/desktop/src/shared/styles/globals/composer.css index 63d31f5fac9..5ebe4b57757 100644 --- a/desktop/src/shared/styles/globals/composer.css +++ b/desktop/src/shared/styles/globals/composer.css @@ -96,6 +96,7 @@ @apply text-message font-normal tracking-normal; outline: none; min-height: 1lh; + padding-block: 0.0625rem; } .rich-text-composer .tiptap p { diff --git a/desktop/tests/e2e/mentions.spec.ts b/desktop/tests/e2e/mentions.spec.ts index dc05486cec4..2ca2f10c5f6 100644 --- a/desktop/tests/e2e/mentions.spec.ts +++ b/desktop/tests/e2e/mentions.spec.ts @@ -871,8 +871,20 @@ test("selecting a person mention inserts @Name into input", async ({ ); expect(iconMask).toContain("data:image/svg+xml"); await expect(mentionChip).toHaveCSS("line-height", "18px"); - expect(await mentionChip.evaluate((element) => element.clientHeight)).toBe( - await mentionChip.evaluate((element) => element.scrollHeight), + const paintedBounds = await mentionChip.evaluate((element) => { + const chip = element.getBoundingClientRect(); + const editor = element.closest(".tiptap")?.getBoundingClientRect(); + if (!editor) throw new Error("Mention chip is missing its editor"); + return { + chipTop: chip.top, + chipBottom: chip.bottom, + editorTop: editor.top, + editorBottom: editor.bottom, + }; + }); + expect(paintedBounds.chipTop).toBeGreaterThanOrEqual(paintedBounds.editorTop); + expect(paintedBounds.chipBottom).toBeLessThanOrEqual( + paintedBounds.editorBottom, ); const humanIconTranslateY = await mentionChip.evaluate( (element) => From b4b97a0b0bd0debb247e0ccccf20769ecfcb2c31 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Mon, 24 Aug 2026 15:08:42 -0700 Subject: [PATCH 3/3] test(desktop): cover composer chip paint bounds Co-authored-by: Carl Signed-off-by: Taylor Ho --- desktop/tests/e2e/mentions.spec.ts | 84 +++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/desktop/tests/e2e/mentions.spec.ts b/desktop/tests/e2e/mentions.spec.ts index 2ca2f10c5f6..e80bc21aea0 100644 --- a/desktop/tests/e2e/mentions.spec.ts +++ b/desktop/tests/e2e/mentions.spec.ts @@ -871,20 +871,26 @@ test("selecting a person mention inserts @Name into input", async ({ ); expect(iconMask).toContain("data:image/svg+xml"); await expect(mentionChip).toHaveCSS("line-height", "18px"); + const scrollViewport = page.getByTestId("message-input-scroll"); const paintedBounds = await mentionChip.evaluate((element) => { const chip = element.getBoundingClientRect(); - const editor = element.closest(".tiptap")?.getBoundingClientRect(); - if (!editor) throw new Error("Mention chip is missing its editor"); + const viewport = element + .closest("[data-testid='message-input-scroll']") + ?.getBoundingClientRect(); + if (!viewport) + throw new Error("Mention chip is missing its scroll viewport"); return { chipTop: chip.top, chipBottom: chip.bottom, - editorTop: editor.top, - editorBottom: editor.bottom, + viewportTop: viewport.top, + viewportBottom: viewport.bottom, }; }); - expect(paintedBounds.chipTop).toBeGreaterThanOrEqual(paintedBounds.editorTop); + expect(paintedBounds.chipTop).toBeGreaterThanOrEqual( + paintedBounds.viewportTop, + ); expect(paintedBounds.chipBottom).toBeLessThanOrEqual( - paintedBounds.editorBottom, + paintedBounds.viewportBottom, ); const humanIconTranslateY = await mentionChip.evaluate( (element) => @@ -902,6 +908,72 @@ test("selecting a person mention inserts @Name into input", async ({ return translateY; }); expect(humanIconTranslateY - channelIconTranslateY).toBeCloseTo(1); + + await input.fill("@bo"); + await dropdown.getByText("bob").click(); + await input.press("Shift+Enter"); + await page.keyboard.type("@bo"); + await dropdown.getByText("bob").click(); + await input.press("Shift+Enter"); + await page.keyboard.type("@bo"); + await dropdown.getByText("bob").click(); + const multilineChips = input.locator(".human-mention-highlight"); + await expect(multilineChips).toHaveCount(3); + const multilinePaintBounds = await multilineChips.evaluateAll((elements) => { + const viewport = elements[0] + ?.closest("[data-testid='message-input-scroll']") + ?.getBoundingClientRect(); + if (!viewport) { + throw new Error("Mention chips are missing their scroll viewport"); + } + return elements.map((element) => { + const chip = element.getBoundingClientRect(); + return { + chipTop: chip.top, + chipBottom: chip.bottom, + viewportTop: viewport.top, + viewportBottom: viewport.bottom, + }; + }); + }); + for (const bounds of multilinePaintBounds) { + expect(bounds.chipTop).toBeGreaterThanOrEqual(bounds.viewportTop); + expect(bounds.chipBottom).toBeLessThanOrEqual(bounds.viewportBottom); + } + + await scrollViewport.evaluate((element) => { + element.style.width = "8rem"; + }); + await input.fill("A deliberately long prefix that forces @bo"); + await dropdown.getByText("bob").click(); + const wrappedChip = input.locator(".human-mention-highlight", { + hasText: "bob", + }); + const wrappedPaintBounds = await wrappedChip.evaluate((element) => { + const chip = element.getBoundingClientRect(); + const viewport = element + .closest("[data-testid='message-input-scroll']") + ?.getBoundingClientRect(); + if (!viewport) + throw new Error("Mention chip is missing its scroll viewport"); + return { + chipTop: chip.top, + chipBottom: chip.bottom, + viewportTop: viewport.top, + viewportBottom: viewport.bottom, + }; + }); + expect(wrappedPaintBounds.chipTop).toBeGreaterThanOrEqual( + wrappedPaintBounds.viewportTop, + ); + expect(wrappedPaintBounds.chipBottom).toBeLessThanOrEqual( + wrappedPaintBounds.viewportBottom, + ); + await expect(input).toHaveCSS("height", /^(?!20px$)/); + await scrollViewport.evaluate((element) => { + element.style.removeProperty("width"); + }); + await waitForAnimations(page); await page.getByTestId("message-composer").screenshot({ path: "test-results/inline-chip-polish/composer-after.png",