Sync appearance preferences across windows

Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
This commit is contained in:
Princess Donut
2026-08-12 14:12:15 +01:00
parent 1a9fa4f299
commit 6166fa1cd7
4 changed files with 61 additions and 4 deletions
@@ -3,7 +3,11 @@ import test from "node:test";
const values = new Map();
const attributes = new Map();
const windowListeners = new Map();
globalThis.window = {
addEventListener: (type, listener) => windowListeners.set(type, listener),
};
globalThis.localStorage = {
getItem: (key) => values.get(key) ?? null,
setItem: (key, value) => values.set(key, String(value)),
@@ -57,3 +61,12 @@ test("initializes from the persisted conversation density", () => {
assert.equal(preference.getConversationDensity(), "spacious");
assert.equal(attributes.get("data-conversation-density"), "spacious");
});
test("applies conversation density changes from another window", () => {
values.set(preference.CONVERSATION_DENSITY_STORAGE_KEY, "compact");
windowListeners.get("storage")({
key: preference.CONVERSATION_DENSITY_STORAGE_KEY,
});
assert.equal(preference.getConversationDensity(), "compact");
assert.equal(attributes.get("data-conversation-density"), "compact");
});
@@ -9,6 +9,7 @@ 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,
@@ -39,8 +40,7 @@ function notifyListeners(): void {
for (const listener of listeners) listener();
}
/** Apply the persisted preference before React renders to avoid a layout jump. */
export function initializeConversationDensityPreference(): void {
function applyStoredConversationDensity(): void {
const nextDensity = readStoredConversationDensity();
const changed = nextDensity !== conversationDensity;
conversationDensity = nextDensity;
@@ -48,6 +48,23 @@ export function initializeConversationDensityPreference(): void {
if (changed) notifyListeners();
}
function listenForStorageChanges(): void {
if (listeningForStorageChanges || !globalThis.window?.addEventListener)
return;
globalThis.window.addEventListener("storage", (event) => {
if (event.key === CONVERSATION_DENSITY_STORAGE_KEY) {
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);
@@ -4,10 +4,14 @@ import test from "node:test";
const values = new Map();
const attributes = new Map();
const styleValues = new Map();
const windowListeners = new Map();
const style = {
setProperty: (name, value) => styleValues.set(name, value),
};
globalThis.window = {
addEventListener: (type, listener) => windowListeners.set(type, listener),
};
globalThis.localStorage = {
getItem: (key) => values.get(key) ?? null,
setItem: (key, value) => values.set(key, String(value)),
@@ -60,3 +64,11 @@ test("initializes from the stored font size", () => {
assert.equal(attributes.get("data-font-size"), "larger");
assert.equal(styleValues.get("--buzz-type-rem"), "18.285714px");
});
test("applies font size changes from another window", () => {
values.set(preference.FONT_SIZE_STORAGE_KEY, "smaller");
windowListeners.get("storage")({ key: preference.FONT_SIZE_STORAGE_KEY });
assert.equal(preference.getFontSize(), "smaller");
assert.equal(attributes.get("data-font-size"), "smaller");
assert.equal(styleValues.get("--buzz-type-rem"), "16px");
});
+17 -2
View File
@@ -21,6 +21,7 @@ const TYPE_REM_PROPERTY = "--buzz-type-rem";
const listeners = new Set<() => void>();
let fontSize: FontSize = DEFAULT_FONT_SIZE;
let textZoomFactor = 1;
let listeningForStorageChanges = false;
export function parseFontSize(value: string | null | undefined): FontSize {
return value === "smaller" || value === "default" || value === "larger"
@@ -54,8 +55,7 @@ function notifyListeners(): void {
for (const listener of listeners) listener();
}
/** Apply the persisted preference before React renders to avoid a layout jump. */
export function initializeFontSizePreference(): void {
function applyStoredFontSize(): void {
const nextSize = readStoredFontSize();
const changed = nextSize !== fontSize;
fontSize = nextSize;
@@ -63,6 +63,21 @@ export function initializeFontSizePreference(): void {
if (changed) notifyListeners();
}
function listenForStorageChanges(): void {
if (listeningForStorageChanges || !globalThis.window?.addEventListener)
return;
globalThis.window.addEventListener("storage", (event) => {
if (event.key === FONT_SIZE_STORAGE_KEY) applyStoredFontSize();
});
listeningForStorageChanges = true;
}
/** Apply the persisted preference before React renders to avoid a layout jump. */
export function initializeFontSizePreference(): void {
applyStoredFontSize();
listenForStorageChanges();
}
/** Combine Cmd +/- zoom with the selected app-wide type scale. */
export function applyTextZoomFactor(zoomFactor: number): void {
if (!Number.isFinite(zoomFactor) || zoomFactor <= 0) return;