diff --git a/desktop/src/shared/styles/globals/composer.css b/desktop/src/shared/styles/globals/composer.css index 781ac8a1968..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 { @@ -260,8 +261,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..e80bc21aea0 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,114 @@ 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 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(paintedBounds.chipTop).toBeGreaterThanOrEqual( + paintedBounds.viewportTop, + ); + expect(paintedBounds.chipBottom).toBeLessThanOrEqual( + paintedBounds.viewportBottom, + ); + 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 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", + }); }); test("immediate ArrowLeft after a person mention is not bounced past the trailing space", async ({