mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
## Summary - Add separate **Font size** and **Conversation density** controls in Appearance. - Use a 13 / 14 / 15px conversation text ramp for Smaller / Default / Larger while scaling interface typography through a shared virtual-rem system. - Keep layout geometry fixed while typography changes, and compose Cmd +/- text zoom on top of the selected preference. - Let Compact, Comfy, and Spacious control conversation row spacing plus paragraph and list rhythm in Markdown content across Buzz. - Preview font size and density together with click-and-drag comparison; interrupted scrubbing restores the saved preference. - Synchronize preferences across open Buzz windows, including full local-storage clears during sign-out or dev reset. - Promote the segmented control to shared UI and make Settings rows adapt to their card width. ## Validation - `just ci` - 4,965+ desktop unit tests across the final merged branch - 1,465 mobile tests through the full repository gate - Focused Playwright coverage for font-size/density independence, drag preview cancellation, Inbox geometry, thread rendering, keyboard zoom, and cross-surface typography - Fresh E2E build against the final merged branch --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com>
102 lines
3.0 KiB
TypeScript
102 lines
3.0 KiB
TypeScript
import * as React from "react";
|
|
|
|
/** Device-level spacing used across conversation surfaces. */
|
|
export type ConversationDensity = "compact" | "comfortable" | "spacious";
|
|
|
|
export const CONVERSATION_DENSITY_STORAGE_KEY =
|
|
"buzz.appearance.conversationDensity";
|
|
export const DEFAULT_CONVERSATION_DENSITY: ConversationDensity = "comfortable";
|
|
|
|
const listeners = new Set<() => void>();
|
|
let conversationDensity: ConversationDensity = DEFAULT_CONVERSATION_DENSITY;
|
|
let listeningForStorageChanges = false;
|
|
|
|
export function parseConversationDensity(
|
|
value: string | null | undefined,
|
|
): ConversationDensity {
|
|
return value === "compact" || value === "comfortable" || value === "spacious"
|
|
? value
|
|
: DEFAULT_CONVERSATION_DENSITY;
|
|
}
|
|
|
|
function readStoredConversationDensity(): ConversationDensity {
|
|
try {
|
|
return parseConversationDensity(
|
|
globalThis.localStorage?.getItem(CONVERSATION_DENSITY_STORAGE_KEY),
|
|
);
|
|
} catch {
|
|
return DEFAULT_CONVERSATION_DENSITY;
|
|
}
|
|
}
|
|
|
|
function applyConversationDensity(density: ConversationDensity): void {
|
|
globalThis.document?.documentElement?.setAttribute(
|
|
"data-conversation-density",
|
|
density,
|
|
);
|
|
}
|
|
|
|
function notifyListeners(): void {
|
|
for (const listener of listeners) listener();
|
|
}
|
|
|
|
function applyStoredConversationDensity(): void {
|
|
const nextDensity = readStoredConversationDensity();
|
|
const changed = nextDensity !== conversationDensity;
|
|
conversationDensity = nextDensity;
|
|
applyConversationDensity(nextDensity);
|
|
if (changed) notifyListeners();
|
|
}
|
|
|
|
function listenForStorageChanges(): void {
|
|
if (listeningForStorageChanges || !globalThis.window?.addEventListener)
|
|
return;
|
|
globalThis.window.addEventListener("storage", (event) => {
|
|
if (event.key === CONVERSATION_DENSITY_STORAGE_KEY || event.key === null) {
|
|
applyStoredConversationDensity();
|
|
}
|
|
});
|
|
listeningForStorageChanges = true;
|
|
}
|
|
|
|
/** Apply the persisted preference before React renders to avoid a layout jump. */
|
|
export function initializeConversationDensityPreference(): void {
|
|
applyStoredConversationDensity();
|
|
listenForStorageChanges();
|
|
}
|
|
|
|
function subscribe(listener: () => void): () => void {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
}
|
|
|
|
export function getConversationDensity(): ConversationDensity {
|
|
return conversationDensity;
|
|
}
|
|
|
|
export function setConversationDensity(density: ConversationDensity): void {
|
|
conversationDensity = density;
|
|
applyConversationDensity(density);
|
|
try {
|
|
globalThis.localStorage?.setItem(CONVERSATION_DENSITY_STORAGE_KEY, density);
|
|
} catch {
|
|
// Persistence is best-effort; the live preference still applies.
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
/** Temporarily apply a density without changing the saved preference. */
|
|
export function previewConversationDensity(
|
|
density: ConversationDensity | null,
|
|
): void {
|
|
applyConversationDensity(density ?? conversationDensity);
|
|
}
|
|
|
|
export function useConversationDensity(): ConversationDensity {
|
|
return React.useSyncExternalStore(
|
|
subscribe,
|
|
getConversationDensity,
|
|
() => DEFAULT_CONVERSATION_DENSITY,
|
|
);
|
|
}
|