mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(desktop): polish inline chip states (#6718)
**Category:** fix **User Impact:** Inline chips now read more consistently, fit cleanly in the composer, and show deleted message links with the same calm muted treatment as unresolved links. **Problem:** Deleted message links looked like destructive actions even though they are informational, while mention chips had slightly uneven vertical spacing, a high-set human icon, and could clip inside the composer. **Solution:** Unify unavailable-state styling, tighten chip spacing, optically align the human icon, and give composer chips enough line height to paint without changing caret behavior. <details> <summary>File changes</summary> **desktop/src/shared/styles/globals/markdown.css** Makes chip padding vertically symmetric, moves only the human `@` icon down by 1px, and shares muted colors between deleted and unresolved message links while preserving their separate semantic classes. **desktop/src/shared/styles/globals/composer.css** Adds a composer-only line height derived from the text size and chip padding so inline chips no longer clip while retaining inline caret behavior. **desktop/tests/e2e/entity-link-recipient-cards.spec.ts** Verifies deleted and unresolved chips have matching computed colors while deleted links retain their tooltip, semantics, and navigation behavior. **desktop/tests/e2e/mentions.spec.ts** Covers composer chip height, clipping, the human icon's 1px optical offset, and visual capture. </details> ## Reproduction steps 1. Open a channel containing a link to a definitively deleted message and compare it with a transient or unresolved message link; both should use the muted unavailable treatment, while the deleted link still says “Message deleted” and navigates to its fallback destination. 2. Insert a person mention in the composer; the chip should have balanced vertical spacing and paint fully without clipping. 3. Compare a person mention with a channel chip; only the human `@` icon should sit 1px lower. ## Screenshots ### Deleted message link | Before | After | | --- | --- | |  |  | ### Composer chip polish | Before | After | | --- | --- | |  |  | ## Validation - `desktop/tests/e2e/entity-link-recipient-cards.spec.ts` + `desktop/tests/e2e/mentions.spec.ts`: 82/82 passed - Desktop unit suite: 5,397 passed - Formatting and lint checks passed (existing informational warnings only) - Pre-push desktop check, typecheck, and unit tests passed at `0255c3fd49f9244cd727d0dd20c514b3a1812152` --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
|
||||
import {
|
||||
installMockBridge,
|
||||
openChannelBrowser,
|
||||
@@ -844,6 +846,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");
|
||||
@@ -872,6 +877,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 ({
|
||||
|
||||
Reference in New Issue
Block a user