mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
Decouple conversation font size and density
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="px-4 py-3" data-testid="conversation-density-preview">
|
||||
<div className="px-4 py-3" data-testid="conversation-preview">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative overflow-hidden rounded-xl border border-border/65 bg-muted/45"
|
||||
data-testid="conversation-density-preview-surface"
|
||||
data-testid="conversation-preview-surface"
|
||||
>
|
||||
<span className="absolute right-3 top-3 inline-flex rounded-full border border-border/65 bg-background/70 px-2.5 py-1 text-xs font-medium text-muted-foreground">
|
||||
Preview
|
||||
</span>
|
||||
<div className="p-4" data-testid="conversation-density-preview-content">
|
||||
<div className="p-4" data-testid="conversation-preview-content">
|
||||
<ConversationDensityPreviewMessage
|
||||
avatar="M"
|
||||
author="Maya"
|
||||
@@ -176,12 +200,34 @@ function ConversationDensityPreview() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Message sizing and spacing shared by every conversation surface. */
|
||||
export function ConversationDensitySetting() {
|
||||
/** Independent message type and spacing controls shared by every conversation. */
|
||||
export function ConversationDisplaySettings() {
|
||||
const density = useConversationDensity();
|
||||
const fontSize = useConversationFontSize();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsOptionRow data-testid="conversation-font-size-row">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">Font size</p>
|
||||
<p
|
||||
className="text-sm font-normal text-muted-foreground/70"
|
||||
data-settings-subcopy
|
||||
>
|
||||
Adjust message text and line height.
|
||||
</p>
|
||||
</div>
|
||||
<SettingsSegmentedControl
|
||||
className="w-72"
|
||||
legend="Font size"
|
||||
onPreviewChange={previewConversationFontSize}
|
||||
onValueChange={setConversationFontSize}
|
||||
optionTestIdPrefix="conversation-font-size"
|
||||
options={CONVERSATION_FONT_SIZE_OPTIONS}
|
||||
testId="conversation-font-size-control"
|
||||
value={fontSize}
|
||||
/>
|
||||
</SettingsOptionRow>
|
||||
<SettingsOptionRow data-testid="conversation-density-row">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">Conversation density</p>
|
||||
@@ -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.
|
||||
</p>
|
||||
</div>
|
||||
<SettingsSegmentedControl
|
||||
@@ -203,7 +249,7 @@ export function ConversationDensitySetting() {
|
||||
value={density}
|
||||
/>
|
||||
</SettingsOptionRow>
|
||||
<ConversationDensityPreview />
|
||||
<ConversationPreview />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<ConversationDensitySetting />
|
||||
<ConversationDisplaySettings />
|
||||
<LinkPreviewStyleSetting />
|
||||
<ThreadLayoutSetting />
|
||||
</SettingsOptionGroup>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
84
desktop/src/shared/lib/conversationFontSizePreference.ts
Normal file
84
desktop/src/shared/lib/conversationFontSizePreference.ts
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)" },
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user