diff --git a/desktop/src/features/settings/ui/AppearanceSettingsControls.tsx b/desktop/src/features/settings/ui/AppearanceSettingsControls.tsx
index c37ba8f259..bd7113a461 100644
--- a/desktop/src/features/settings/ui/AppearanceSettingsControls.tsx
+++ b/desktop/src/features/settings/ui/AppearanceSettingsControls.tsx
@@ -20,6 +20,12 @@ import {
useConversationDensity,
type ConversationDensity,
} from "@/shared/lib/conversationDensityPreference";
+import {
+ previewConversationFontSize,
+ setConversationFontSize,
+ useConversationFontSize,
+ type ConversationFontSize,
+} from "@/shared/lib/conversationFontSizePreference";
import {
ACCENT_COLORS,
DEFAULT_GLASS_OPACITY,
@@ -105,6 +111,24 @@ const CONVERSATION_DENSITY_OPTIONS: readonly {
},
];
+const CONVERSATION_FONT_SIZE_OPTIONS: readonly {
+ value: ConversationFontSize;
+ label: string;
+}[] = [
+ {
+ value: "smaller",
+ label: "Smaller",
+ },
+ {
+ value: "default",
+ label: "Default",
+ },
+ {
+ value: "larger",
+ label: "Larger",
+ },
+];
+
function ConversationDensityPreviewMessage({
avatar,
author,
@@ -138,18 +162,18 @@ function ConversationDensityPreviewMessage({
);
}
-function ConversationDensityPreview() {
+function ConversationPreview() {
return (
-
+
Preview
-
+
+
+
+
Font size
+
+ Adjust message text and line height.
+
+
+
+
Conversation density
@@ -189,7 +235,7 @@ export function ConversationDensitySetting() {
className="text-sm font-normal text-muted-foreground/70"
data-settings-subcopy
>
- Adjust message size and spacing.
+ Adjust message and paragraph spacing.
-
+
>
);
}
diff --git a/desktop/src/features/settings/ui/SettingsPanels.tsx b/desktop/src/features/settings/ui/SettingsPanels.tsx
index 4896c207b1..094e021a9b 100644
--- a/desktop/src/features/settings/ui/SettingsPanels.tsx
+++ b/desktop/src/features/settings/ui/SettingsPanels.tsx
@@ -55,7 +55,7 @@ import {
import { appearanceCommunityLabel } from "../lib/appearanceScopeCopy";
import {
AccentPickerContent,
- ConversationDensitySetting,
+ ConversationDisplaySettings,
GlassBackgroundSetting,
LinkPreviewStyleSetting,
ProminentActiveTabSetting,
@@ -782,7 +782,7 @@ function ThemeSettingsCard() {
data-testid="appearance-preferences-card"
title="Preferences"
>
-
+
diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx
index 91b002eefc..6fc0028380 100644
--- a/desktop/src/main.tsx
+++ b/desktop/src/main.tsx
@@ -21,6 +21,7 @@ import { TooltipProvider } from "@/shared/ui/tooltip";
import { recoverLocalStorageQuotaOnStartup } from "@/shared/lib/localStorageQuota";
import { startLocalStorageSweep } from "@/shared/lib/localStorageSweep";
import { initializeConversationDensityPreference } from "@/shared/lib/conversationDensityPreference";
+import { initializeConversationFontSizePreference } from "@/shared/lib/conversationFontSizePreference";
type E2eWindow = Window & {
__BUZZ_E2E__?: unknown;
@@ -126,6 +127,7 @@ async function bootstrap() {
configureDevE2eBridgeFromUrl();
recoverLocalStorageQuotaOnStartup();
initializeConversationDensityPreference();
+ initializeConversationFontSizePreference();
startLocalStorageSweep();
await installE2eBridgeIfConfigured();
await migrateLegacyCommunityStorageBeforeRender();
diff --git a/desktop/src/shared/lib/conversationDensityPreference.ts b/desktop/src/shared/lib/conversationDensityPreference.ts
index 5477b80626..302bce565d 100644
--- a/desktop/src/shared/lib/conversationDensityPreference.ts
+++ b/desktop/src/shared/lib/conversationDensityPreference.ts
@@ -1,6 +1,6 @@
import * as React from "react";
-/** Device-level message sizing and spacing used across conversation surfaces. */
+/** Device-level spacing used across conversation surfaces. */
export type ConversationDensity = "compact" | "comfortable" | "spacious";
export const CONVERSATION_DENSITY_STORAGE_KEY =
diff --git a/desktop/src/shared/lib/conversationFontSizePreference.test.mjs b/desktop/src/shared/lib/conversationFontSizePreference.test.mjs
new file mode 100644
index 0000000000..d1d8ec602d
--- /dev/null
+++ b/desktop/src/shared/lib/conversationFontSizePreference.test.mjs
@@ -0,0 +1,56 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+const values = new Map();
+const attributes = new Map();
+
+globalThis.localStorage = {
+ getItem: (key) => values.get(key) ?? null,
+ setItem: (key, value) => values.set(key, String(value)),
+};
+globalThis.document = {
+ documentElement: {
+ setAttribute: (name, value) => attributes.set(name, value),
+ },
+};
+
+const preference = await import("./conversationFontSizePreference.ts");
+
+test("defaults invalid and missing conversation font sizes to default", () => {
+ assert.equal(preference.parseConversationFontSize(null), "default");
+ assert.equal(preference.parseConversationFontSize("medium"), "default");
+ assert.equal(preference.parseConversationFontSize("smaller"), "smaller");
+ assert.equal(preference.parseConversationFontSize("default"), "default");
+ assert.equal(preference.parseConversationFontSize("larger"), "larger");
+});
+
+test("persists and applies the selected conversation font size", () => {
+ preference.setConversationFontSize("smaller");
+ assert.equal(preference.getConversationFontSize(), "smaller");
+ assert.equal(
+ values.get(preference.CONVERSATION_FONT_SIZE_STORAGE_KEY),
+ "smaller",
+ );
+ assert.equal(attributes.get("data-conversation-font-size"), "smaller");
+});
+
+test("previews a font size without changing the saved preference", () => {
+ preference.setConversationFontSize("smaller");
+ preference.previewConversationFontSize("larger");
+ assert.equal(preference.getConversationFontSize(), "smaller");
+ assert.equal(
+ values.get(preference.CONVERSATION_FONT_SIZE_STORAGE_KEY),
+ "smaller",
+ );
+ assert.equal(attributes.get("data-conversation-font-size"), "larger");
+
+ preference.previewConversationFontSize(null);
+ assert.equal(attributes.get("data-conversation-font-size"), "smaller");
+});
+
+test("initializes from the stored conversation font size", () => {
+ values.set(preference.CONVERSATION_FONT_SIZE_STORAGE_KEY, "larger");
+ preference.initializeConversationFontSizePreference();
+ assert.equal(preference.getConversationFontSize(), "larger");
+ assert.equal(attributes.get("data-conversation-font-size"), "larger");
+});
diff --git a/desktop/src/shared/lib/conversationFontSizePreference.ts b/desktop/src/shared/lib/conversationFontSizePreference.ts
new file mode 100644
index 0000000000..6fb5fb2eab
--- /dev/null
+++ b/desktop/src/shared/lib/conversationFontSizePreference.ts
@@ -0,0 +1,84 @@
+import * as React from "react";
+
+/** Device-level text sizing used across conversation surfaces. */
+export type ConversationFontSize = "smaller" | "default" | "larger";
+
+export const CONVERSATION_FONT_SIZE_STORAGE_KEY =
+ "buzz.appearance.conversationFontSize";
+export const DEFAULT_CONVERSATION_FONT_SIZE: ConversationFontSize = "default";
+
+const listeners = new Set<() => void>();
+let conversationFontSize: ConversationFontSize = DEFAULT_CONVERSATION_FONT_SIZE;
+
+export function parseConversationFontSize(
+ value: string | null | undefined,
+): ConversationFontSize {
+ return value === "smaller" || value === "default" || value === "larger"
+ ? value
+ : DEFAULT_CONVERSATION_FONT_SIZE;
+}
+
+function readStoredConversationFontSize(): ConversationFontSize {
+ try {
+ return parseConversationFontSize(
+ globalThis.localStorage?.getItem(CONVERSATION_FONT_SIZE_STORAGE_KEY),
+ );
+ } catch {
+ return DEFAULT_CONVERSATION_FONT_SIZE;
+ }
+}
+
+function applyConversationFontSize(size: ConversationFontSize): void {
+ globalThis.document?.documentElement?.setAttribute(
+ "data-conversation-font-size",
+ size,
+ );
+}
+
+function notifyListeners(): void {
+ for (const listener of listeners) listener();
+}
+
+/** Apply the persisted preference before React renders to avoid a layout jump. */
+export function initializeConversationFontSizePreference(): void {
+ const nextSize = readStoredConversationFontSize();
+ const changed = nextSize !== conversationFontSize;
+ conversationFontSize = nextSize;
+ applyConversationFontSize(nextSize);
+ if (changed) notifyListeners();
+}
+
+function subscribe(listener: () => void): () => void {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+}
+
+export function getConversationFontSize(): ConversationFontSize {
+ return conversationFontSize;
+}
+
+export function setConversationFontSize(size: ConversationFontSize): void {
+ conversationFontSize = size;
+ applyConversationFontSize(size);
+ try {
+ globalThis.localStorage?.setItem(CONVERSATION_FONT_SIZE_STORAGE_KEY, size);
+ } catch {
+ // Persistence is best-effort; the live preference still applies.
+ }
+ notifyListeners();
+}
+
+/** Temporarily apply a size without changing the saved preference. */
+export function previewConversationFontSize(
+ size: ConversationFontSize | null,
+): void {
+ applyConversationFontSize(size ?? conversationFontSize);
+}
+
+export function useConversationFontSize(): ConversationFontSize {
+ return React.useSyncExternalStore(
+ subscribe,
+ getConversationFontSize,
+ () => DEFAULT_CONVERSATION_FONT_SIZE,
+ );
+}
diff --git a/desktop/src/shared/styles/globals/theme.css b/desktop/src/shared/styles/globals/theme.css
index 91de1ed1e1..acd87d71f6 100644
--- a/desktop/src/shared/styles/globals/theme.css
+++ b/desktop/src/shared/styles/globals/theme.css
@@ -3,9 +3,9 @@
/* Catppuccin Latte (mauve accent) */
--radius: 0.625rem;
/*
- * Comfortable conversation rhythm for channels, DMs, threads, Inbox, and
- * the composer. The Appearance preference overrides this complete scale
- * below, while rem units keep Cmd +/- text scaling intact.
+ * Default conversation type and comfy spacing for channels, DMs, threads,
+ * Inbox, and the composer. Independent Appearance preferences override the
+ * two scales below, while rem units keep Cmd +/- text scaling intact.
*/
--conversation-message-font-size: 0.9375rem;
--conversation-message-line-height: 1.25rem;
@@ -80,25 +80,31 @@
}
:root[data-conversation-density="compact"] {
- --conversation-message-font-size: 0.875rem;
- --conversation-message-line-height: 1.1875rem;
- --conversation-author-line-height: 1rem;
--conversation-body-gap: 0rem;
--conversation-row-padding-block: 0.25rem;
--conversation-paragraph-gap: 0.375rem;
--conversation-list-item-gap: 0.25rem;
- --conversation-timestamp-font-size: 0.625rem;
- --conversation-timestamp-line-height: 0.875rem;
}
:root[data-conversation-density="spacious"] {
- --conversation-message-font-size: 1rem;
- --conversation-message-line-height: 1.375rem;
- --conversation-author-line-height: 1.125rem;
--conversation-body-gap: 0.25rem;
--conversation-row-padding-block: 0.5rem;
--conversation-paragraph-gap: 0.625rem;
--conversation-list-item-gap: 0.5rem;
+ }
+
+ :root[data-conversation-font-size="smaller"] {
+ --conversation-message-font-size: 0.875rem;
+ --conversation-message-line-height: 1.1875rem;
+ --conversation-author-line-height: 1rem;
+ --conversation-timestamp-font-size: 0.625rem;
+ --conversation-timestamp-line-height: 0.875rem;
+ }
+
+ :root[data-conversation-font-size="larger"] {
+ --conversation-message-font-size: 1rem;
+ --conversation-message-line-height: 1.375rem;
+ --conversation-author-line-height: 1.125rem;
--conversation-timestamp-font-size: 0.75rem;
--conversation-timestamp-line-height: 1.125rem;
}
diff --git a/desktop/tailwind.config.js b/desktop/tailwind.config.js
index c663a2ac3b..18e9eae494 100644
--- a/desktop/tailwind.config.js
+++ b/desktop/tailwind.config.js
@@ -12,8 +12,8 @@ export default {
"3xs": "0.5rem", // 8px — tiny glyphs / micro labels
badge: "0.625rem", // 10px — compact status badges
// Shared channel, DM, thread, and composer type. Backed by CSS
- // variables so the Appearance preference can change the complete
- // conversation rhythm without branching individual components.
+ // variables so font size and density can change independently without
+ // branching individual components.
message: [
"var(--conversation-message-font-size)",
{ lineHeight: "var(--conversation-message-line-height)" },
diff --git a/desktop/tests/e2e/buzz-theme-screenshots.spec.ts b/desktop/tests/e2e/buzz-theme-screenshots.spec.ts
index cd3a262648..75581205bd 100644
--- a/desktop/tests/e2e/buzz-theme-screenshots.spec.ts
+++ b/desktop/tests/e2e/buzz-theme-screenshots.spec.ts
@@ -9,6 +9,8 @@ const GLASS_BACKGROUND_STORAGE_KEY = "buzz-glass-background";
const GLASS_OPACITY_STORAGE_KEY = "buzz-glass-opacity";
const PROMINENT_ACTIVE_TAB_STORAGE_KEY = "buzz-prominent-active-tab";
const CONVERSATION_DENSITY_STORAGE_KEY = "buzz.appearance.conversationDensity";
+const CONVERSATION_FONT_SIZE_STORAGE_KEY =
+ "buzz.appearance.conversationFontSize";
const MOCK_PUBKEY = "deadbeef".repeat(8);
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
@@ -617,7 +619,7 @@ test("appearance groups theme and preferences into labeled rows", async ({
).toBeLessThanOrEqual(0.5);
});
-test("conversation density applies and persists all three scales", async ({
+test("conversation font size and density apply independently", async ({
page,
}) => {
await seedTheme(page, "buzz");
@@ -625,27 +627,35 @@ test("conversation density applies and persists all three scales", async ({
await openAppearance(page, "light");
const root = page.locator("html");
- const control = page.getByTestId("conversation-density-control");
+ const densityControl = page.getByTestId("conversation-density-control");
const compact = page.getByTestId("conversation-density-compact");
const comfortable = page.getByTestId("conversation-density-comfortable");
const spacious = page.getByTestId("conversation-density-spacious");
- const indicator = page.getByTestId("conversation-density-control-indicator");
- const preview = page.getByTestId("conversation-density-preview");
- const previewSurface = page.getByTestId(
- "conversation-density-preview-surface",
+ const densityIndicator = page.getByTestId(
+ "conversation-density-control-indicator",
);
- const previewContent = page.getByTestId(
- "conversation-density-preview-content",
+ const fontSizeControl = page.getByTestId("conversation-font-size-control");
+ const smaller = page.getByTestId("conversation-font-size-smaller");
+ const defaultSize = page.getByTestId("conversation-font-size-default");
+ const larger = page.getByTestId("conversation-font-size-larger");
+ const fontSizeIndicator = page.getByTestId(
+ "conversation-font-size-control-indicator",
);
+ const preview = page.getByTestId("conversation-preview");
+ const previewSurface = page.getByTestId("conversation-preview-surface");
+ const previewContent = page.getByTestId("conversation-preview-content");
const previewChip = preview.getByText("Preview");
const firstPreviewMessage = previewSurface.locator("article").first();
const previewMessage = preview.getByText(
"The revised conversation layout is ready to review.",
);
const previewTimestamp = preview.getByText("9:41");
- const description = page
+ const densityDescription = page
.getByTestId("conversation-density-row")
.locator("[data-settings-subcopy]");
+ const fontSizeDescription = page
+ .getByTestId("conversation-font-size-row")
+ .locator("[data-settings-subcopy]");
const readScale = () =>
root.evaluate((element) => {
const style = window.getComputedStyle(element);
@@ -710,12 +720,21 @@ test("conversation density applies and persists all three scales", async ({
await expect(
page.getByRole("group", { name: "Conversation density" }),
).toBeVisible();
- await expect(control).toHaveAccessibleName("Conversation density");
+ await expect(page.getByRole("group", { name: "Font size" })).toBeVisible();
+ await expect(densityControl).toHaveAccessibleName("Conversation density");
+ await expect(fontSizeControl).toHaveAccessibleName("Font size");
await expect(comfortable).toHaveText("Comfy");
+ await expect(defaultSize).toHaveText("Default");
await expect(preview).toContainText("Preview");
await expect(preview).not.toContainText("Message #design");
await expect(comfortable).toHaveAttribute("aria-pressed", "true");
- await expect(description).toHaveText("Adjust message size and spacing.");
+ await expect(defaultSize).toHaveAttribute("aria-pressed", "true");
+ await expect(densityDescription).toHaveText(
+ "Adjust message and paragraph spacing.",
+ );
+ await expect(fontSizeDescription).toHaveText(
+ "Adjust message text and line height.",
+ );
await expect.poll(readScale).toEqual({
authorLineHeight: 1.0625,
bodyGap: 0.125,
@@ -729,13 +748,18 @@ test("conversation density applies and persists all three scales", async ({
await expect
.poll(() =>
Promise.all([
- control.evaluate((element) => element.getBoundingClientRect().width),
+ densityControl.evaluate(
+ (element) => element.getBoundingClientRect().width,
+ ),
+ fontSizeControl.evaluate(
+ (element) => element.getBoundingClientRect().width,
+ ),
page
.getByTestId("appearance-color-mode-control")
.evaluate((element) => element.getBoundingClientRect().width),
]),
)
- .toEqual([288, 240]);
+ .toEqual([288, 288, 240]);
await expect
.poll(() =>
previewMessage.evaluate((element) => {
@@ -752,15 +776,17 @@ test("conversation density applies and persists all three scales", async ({
});
await expect.poll(readPreviewTimestampScale).toEqual(["11px", "16px"]);
await expect.poll(readSettingsChromeScale).toEqual(["24px", "14px", "18px"]);
- await expect(indicator).toHaveCSS("transition-duration", "0.2s");
- await expect(indicator).toHaveCSS("transition-property", /transform/);
+ await expect(densityIndicator).toHaveCSS("transition-duration", "0.2s");
+ await expect(densityIndicator).toHaveCSS("transition-property", /transform/);
+ await expect(fontSizeIndicator).toHaveCSS("transition-duration", "0.2s");
+ await expect(fontSizeIndicator).toHaveCSS("transition-property", /transform/);
await expect
.poll(async () => {
const [previewBackground, controlBackground] = await Promise.all([
previewSurface.evaluate(
(element) => window.getComputedStyle(element).backgroundColor,
),
- control.evaluate(
+ densityControl.evaluate(
(element) => window.getComputedStyle(element).backgroundColor,
),
]);
@@ -774,7 +800,7 @@ test("conversation density applies and persists all three scales", async ({
expect(previewChipBox).not.toBeNull();
expect(firstPreviewMessageBox).not.toBeNull();
if (!previewSurfaceBox || !previewChipBox || !firstPreviewMessageBox) {
- throw new Error("Conversation density preview geometry is missing");
+ throw new Error("Conversation preview geometry is missing");
}
const previewChipRightInset =
previewSurfaceBox.x +
@@ -804,7 +830,7 @@ test("conversation density applies and persists all three scales", async ({
await expect(previewChip).toHaveCSS("padding-top", "4px");
await expect(previewChip).toHaveCSS("padding-bottom", "4px");
- await indicator.evaluate((element) => {
+ await densityIndicator.evaluate((element) => {
element.addEventListener(
"transitionrun",
() => element.setAttribute("data-transition-ran", "true"),
@@ -813,10 +839,10 @@ test("conversation density applies and persists all three scales", async ({
});
await compact.click();
- await expect(indicator).toHaveAttribute("data-transition-ran", "true");
+ await expect(densityIndicator).toHaveAttribute("data-transition-ran", "true");
await expect(root).toHaveAttribute("data-conversation-density", "compact");
+ await expect(root).toHaveAttribute("data-conversation-font-size", "default");
await expect(compact).toHaveAttribute("aria-pressed", "true");
- await expect(description).toHaveText("Adjust message size and spacing.");
await expect
.poll(() =>
page.evaluate(
@@ -826,14 +852,14 @@ test("conversation density applies and persists all three scales", async ({
)
.toBe("compact");
await expect.poll(readScale).toEqual({
- authorLineHeight: 1,
+ authorLineHeight: 1.0625,
bodyGap: 0,
- fontSize: 0.875,
- lineHeight: 1.1875,
+ fontSize: 0.9375,
+ lineHeight: 1.25,
paragraphGap: 0.375,
rowPadding: 0.25,
- timestampFontSize: 0.625,
- timestampLineHeight: 0.875,
+ timestampFontSize: 0.6875,
+ timestampLineHeight: 1,
});
await expect.poll(readSettingsScale).toEqual({
fontSize: "14px",
@@ -841,17 +867,41 @@ test("conversation density applies and persists all three scales", async ({
minHeight: "64px",
paddingBlock: "12px",
});
- await expect.poll(readPreviewTimestampScale).toEqual(["10px", "14px"]);
+ await expect.poll(readPreviewTimestampScale).toEqual(["11px", "16px"]);
await expect.poll(readSettingsChromeScale).toEqual(["24px", "14px", "18px"]);
+
+ await larger.click();
+ await expect(root).toHaveAttribute("data-conversation-density", "compact");
+ await expect(root).toHaveAttribute("data-conversation-font-size", "larger");
+ await expect(larger).toHaveAttribute("aria-pressed", "true");
+ await expect
+ .poll(() =>
+ page.evaluate(
+ (key) => window.localStorage.getItem(key),
+ CONVERSATION_FONT_SIZE_STORAGE_KEY,
+ ),
+ )
+ .toBe("larger");
+ await expect.poll(readScale).toEqual({
+ authorLineHeight: 1.125,
+ bodyGap: 0,
+ fontSize: 1,
+ lineHeight: 1.375,
+ paragraphGap: 0.375,
+ rowPadding: 0.25,
+ timestampFontSize: 0.75,
+ timestampLineHeight: 1.125,
+ });
+ await expect.poll(readPreviewTimestampScale).toEqual(["12px", "18px"]);
await waitForAnimations(page);
await page.getByTestId("appearance-preferences-card").screenshot({
- path: `${SHOTS}/15-conversation-density-compact.png`,
+ path: `${SHOTS}/15-conversation-compact-larger.png`,
});
await spacious.click();
await expect(root).toHaveAttribute("data-conversation-density", "spacious");
+ await expect(root).toHaveAttribute("data-conversation-font-size", "larger");
await expect(spacious).toHaveAttribute("aria-pressed", "true");
- await expect(description).toHaveText("Adjust message size and spacing.");
await expect.poll(readScale).toEqual({
authorLineHeight: 1.125,
bodyGap: 0.25,
@@ -870,19 +920,36 @@ test("conversation density applies and persists all three scales", async ({
});
await expect.poll(readPreviewTimestampScale).toEqual(["12px", "18px"]);
await expect.poll(readSettingsChromeScale).toEqual(["24px", "14px", "18px"]);
+
+ await smaller.click();
+ await expect(root).toHaveAttribute("data-conversation-density", "spacious");
+ await expect(root).toHaveAttribute("data-conversation-font-size", "smaller");
+ await expect(smaller).toHaveAttribute("aria-pressed", "true");
+ await expect.poll(readScale).toEqual({
+ authorLineHeight: 1,
+ bodyGap: 0.25,
+ fontSize: 0.875,
+ lineHeight: 1.1875,
+ paragraphGap: 0.625,
+ rowPadding: 0.5,
+ timestampFontSize: 0.625,
+ timestampLineHeight: 0.875,
+ });
+ await expect.poll(readPreviewTimestampScale).toEqual(["10px", "14px"]);
await waitForAnimations(page);
await page.getByTestId("appearance-preferences-card").screenshot({
- path: `${SHOTS}/16-conversation-density-spacious.png`,
+ path: `${SHOTS}/16-conversation-spacious-smaller.png`,
});
await comfortable.click();
+ await defaultSize.click();
await expect(root).toHaveAttribute(
"data-conversation-density",
"comfortable",
);
await expect(comfortable).toHaveAttribute("aria-pressed", "true");
- const controlBox = await control.boundingBox();
+ const controlBox = await densityControl.boundingBox();
const compactBox = await compact.boundingBox();
const spaciousBox = await spacious.boundingBox();
expect(controlBox).not.toBeNull();
@@ -901,6 +968,7 @@ test("conversation density applies and persists all three scales", async ({
controlBox.y + controlBox.height / 2,
);
await expect(root).toHaveAttribute("data-conversation-density", "spacious");
+ await expect(root).toHaveAttribute("data-conversation-font-size", "default");
await expect
.poll(() =>
page.evaluate(
@@ -909,14 +977,16 @@ test("conversation density applies and persists all three scales", async ({
),
)
.toBe("comfortable");
- await expect
- .poll(() =>
- previewMessage.evaluate((element) => {
- const style = window.getComputedStyle(element);
- return [style.fontSize, style.lineHeight];
- }),
- )
- .toEqual(["16px", "22px"]);
+ await expect.poll(readScale).toEqual({
+ authorLineHeight: 1.0625,
+ bodyGap: 0.25,
+ fontSize: 0.9375,
+ lineHeight: 1.25,
+ paragraphGap: 0.625,
+ rowPadding: 0.5,
+ timestampFontSize: 0.6875,
+ timestampLineHeight: 1,
+ });
await expect.poll(readSettingsScale).toEqual({
fontSize: "14px",
lineHeight: "20px",
@@ -934,9 +1004,60 @@ test("conversation density applies and persists all three scales", async ({
)
.toBe("spacious");
await comfortable.click();
+
+ const fontSizeControlBox = await fontSizeControl.boundingBox();
+ const smallerBox = await smaller.boundingBox();
+ const largerBox = await larger.boundingBox();
+ expect(fontSizeControlBox).not.toBeNull();
+ expect(smallerBox).not.toBeNull();
+ expect(largerBox).not.toBeNull();
+ if (!fontSizeControlBox || !smallerBox || !largerBox) {
+ throw new Error("Conversation font size control geometry is missing");
+ }
+ await page.mouse.move(
+ smallerBox.x + smallerBox.width / 2,
+ fontSizeControlBox.y + fontSizeControlBox.height / 2,
+ );
+ await page.mouse.down();
+ await page.mouse.move(
+ largerBox.x + largerBox.width / 2,
+ fontSizeControlBox.y + fontSizeControlBox.height / 2,
+ );
+ await expect(root).toHaveAttribute("data-conversation-font-size", "larger");
+ await expect(root).toHaveAttribute(
+ "data-conversation-density",
+ "comfortable",
+ );
+ await expect
+ .poll(() =>
+ page.evaluate(
+ (key) => window.localStorage.getItem(key),
+ CONVERSATION_FONT_SIZE_STORAGE_KEY,
+ ),
+ )
+ .toBe("default");
+ await expect
+ .poll(() =>
+ previewMessage.evaluate((element) => {
+ const style = window.getComputedStyle(element);
+ return [style.fontSize, style.lineHeight];
+ }),
+ )
+ .toEqual(["16px", "22px"]);
+ await page.mouse.up();
+ await expect(larger).toHaveAttribute("aria-pressed", "true");
+ await expect
+ .poll(() =>
+ page.evaluate(
+ (key) => window.localStorage.getItem(key),
+ CONVERSATION_FONT_SIZE_STORAGE_KEY,
+ ),
+ )
+ .toBe("larger");
+ await defaultSize.click();
await waitForAnimations(page);
await page.getByTestId("appearance-preferences-card").screenshot({
- path: `${SHOTS}/14-conversation-density.png`,
+ path: `${SHOTS}/14-conversation-preferences.png`,
});
});
@@ -1352,6 +1473,8 @@ test("glass background keeps the content panel solid", async ({ page }) => {
const matchingRadiusControls = [
page.getByTestId("appearance-color-mode-control"),
page.getByTestId("appearance-color-mode-indicator"),
+ page.getByTestId("conversation-font-size-control"),
+ page.getByTestId("conversation-font-size-control-indicator"),
page.getByTestId("conversation-density-control"),
page.getByTestId("conversation-density-control-indicator"),
page.getByTestId("theme-style-trigger"),
diff --git a/desktop/tests/e2e/inbox-refactor-screenshots.spec.ts b/desktop/tests/e2e/inbox-refactor-screenshots.spec.ts
index 9a73d34e91..4e9ca8ca3b 100644
--- a/desktop/tests/e2e/inbox-refactor-screenshots.spec.ts
+++ b/desktop/tests/e2e/inbox-refactor-screenshots.spec.ts
@@ -427,6 +427,18 @@ test.describe("inbox refactor screenshots", () => {
await page.locator("html").evaluate((root) => {
root.setAttribute("data-conversation-density", "compact");
});
+ await expect.poll(readConversationMetrics).toEqual([
+ { fontSize: "15px", lineHeight: "20px" },
+ { paddingBottom: "4px", paddingTop: "4px" },
+ { fontSize: "15px", lineHeight: "17px" },
+ { fontSize: "15px", lineHeight: "20px" },
+ { fontSize: "11px", lineHeight: "16px" },
+ { fontSize: "15px", lineHeight: "20px" },
+ ]);
+
+ await page.locator("html").evaluate((root) => {
+ root.setAttribute("data-conversation-font-size", "smaller");
+ });
await expect.poll(readConversationMetrics).toEqual([
{ fontSize: "14px", lineHeight: "19px" },
{ paddingBottom: "4px", paddingTop: "4px" },
@@ -440,6 +452,7 @@ test.describe("inbox refactor screenshots", () => {
await page.locator("html").evaluate((root) => {
root.setAttribute("data-conversation-density", "spacious");
+ root.setAttribute("data-conversation-font-size", "larger");
});
await expect.poll(readConversationMetrics).toEqual([
{ fontSize: "16px", lineHeight: "22px" },
@@ -454,6 +467,7 @@ test.describe("inbox refactor screenshots", () => {
await page.locator("html").evaluate((root) => {
root.setAttribute("data-conversation-density", "comfortable");
+ root.setAttribute("data-conversation-font-size", "default");
});
await page.evaluate(() => {