mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import * as React from "react";
|
|
|
|
/** Device-level type scale applied throughout the desktop interface. */
|
|
export type FontSize = "smaller" | "default" | "larger";
|
|
|
|
export const FONT_SIZE_STORAGE_KEY = "buzz.appearance.fontSize";
|
|
export const DEFAULT_FONT_SIZE: FontSize = "default";
|
|
|
|
/**
|
|
* Virtual rem sizes used by typography tokens. Keeping the real root at 16px
|
|
* prevents a text preference from also resizing rem-based layout geometry.
|
|
*/
|
|
const TYPE_REM_SIZE_PX: Record<FontSize, number> = {
|
|
smaller: 14 / 0.875,
|
|
default: 15 / 0.875,
|
|
larger: 16 / 0.875,
|
|
};
|
|
|
|
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"
|
|
? value
|
|
: DEFAULT_FONT_SIZE;
|
|
}
|
|
|
|
function readStoredFontSize(): FontSize {
|
|
try {
|
|
return parseFontSize(
|
|
globalThis.localStorage?.getItem(FONT_SIZE_STORAGE_KEY),
|
|
);
|
|
} catch {
|
|
return DEFAULT_FONT_SIZE;
|
|
}
|
|
}
|
|
|
|
function typeRemSizePx(size: FontSize): number {
|
|
return (
|
|
Math.round(TYPE_REM_SIZE_PX[size] * textZoomFactor * 1_000_000) / 1_000_000
|
|
);
|
|
}
|
|
|
|
function applyFontSize(size: FontSize): void {
|
|
const root = globalThis.document?.documentElement;
|
|
root?.setAttribute("data-font-size", size);
|
|
root?.style.setProperty(TYPE_REM_PROPERTY, `${typeRemSizePx(size)}px`);
|
|
}
|
|
|
|
function notifyListeners(): void {
|
|
for (const listener of listeners) listener();
|
|
}
|
|
|
|
function applyStoredFontSize(): void {
|
|
const nextSize = readStoredFontSize();
|
|
const changed = nextSize !== fontSize;
|
|
fontSize = nextSize;
|
|
applyFontSize(nextSize);
|
|
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;
|
|
textZoomFactor = zoomFactor;
|
|
applyFontSize(fontSize);
|
|
}
|
|
|
|
function subscribe(listener: () => void): () => void {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
}
|
|
|
|
export function getFontSize(): FontSize {
|
|
return fontSize;
|
|
}
|
|
|
|
export function setFontSize(size: FontSize): void {
|
|
fontSize = size;
|
|
applyFontSize(size);
|
|
try {
|
|
globalThis.localStorage?.setItem(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 previewFontSize(size: FontSize | null): void {
|
|
applyFontSize(size ?? fontSize);
|
|
}
|
|
|
|
export function useFontSize(): FontSize {
|
|
return React.useSyncExternalStore(
|
|
subscribe,
|
|
getFontSize,
|
|
() => DEFAULT_FONT_SIZE,
|
|
);
|
|
}
|