From 1d525e3e48401a2dfb49c0c8bc82417446941949 Mon Sep 17 00:00:00 2001 From: Byntham <135178921+Byntham@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:58:28 -0800 Subject: [PATCH] Make prompt text larger by default and adjustable The composer text now starts at 20px instead of 14px. Two buttons in the composer footer step it between 14px and 32px, and the choice persists in localStorage. Co-Authored-By: Claude Opus 5.5 --- src/components/chat/ChatComposer.tsx | 50 ++++++++++++++++++++++++++-- src/lib/composerFont.ts | 47 ++++++++++++++++++++++++++ tests/lib.test.ts | 10 ++++++ tests/play.mjs | 21 ++++++++++++ 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 src/lib/composerFont.ts diff --git a/src/components/chat/ChatComposer.tsx b/src/components/chat/ChatComposer.tsx index 40b9a62..2356e1d 100644 --- a/src/components/chat/ChatComposer.tsx +++ b/src/components/chat/ChatComposer.tsx @@ -1,7 +1,13 @@ -import { FolderIcon, GitBranchIcon, PaperclipIcon } from "lucide-react"; +import { AArrowDownIcon, AArrowUpIcon, FolderIcon, GitBranchIcon, PaperclipIcon } from "lucide-react"; import type { ComponentProps } from "react"; import { personOf, sourceAuthor, threadBranch, type PromptDef } from "~/content/prompts"; import { isWorking, nextPrompt, type GameState, type ThreadState } from "~/game/store"; +import { + COMPOSER_FONT_MAX, + COMPOSER_FONT_MIN, + stepComposerFont, + useComposerFont, +} from "~/lib/composerFont"; import { cn } from "~/lib/utils"; import { Button } from "../ui/button"; import { ComposerControl, ComposerPermissionsChip, ProviderModelPicker } from "./ComposerControls"; @@ -53,8 +59,13 @@ function Main({ className, ...props }: ComponentProps<"div">) { function GhostEditor({ draft, target, locked }: { draft: string; target: string; locked: boolean }) { const typed = Array.from(draft); const goal = Array.from(target); + const fontPx = useComposerFont(); return ( -
+
{typed.map((ch, index) => { const ok = goal[index] === ch; @@ -113,6 +124,38 @@ function PromptKindLabel({ thread, prompt }: { thread: ThreadState; prompt: Prom ); } +function FontSizeControl() { + const fontPx = useComposerFont(); + return ( +
+ + +
+ ); +} + function SendButton({ enabled }: { enabled: boolean }) { return ( @@ -237,6 +280,7 @@ export function ChatComposer({ game, thread }: { game: GameState; thread: Thread data-chat-composer-controls="footer" className="relative flex min-w-0 flex-1 items-center justify-end gap-0.5" > +
{working ? : 0} />} diff --git a/src/lib/composerFont.ts b/src/lib/composerFont.ts new file mode 100644 index 0000000..bb3d23f --- /dev/null +++ b/src/lib/composerFont.ts @@ -0,0 +1,47 @@ +import { useSyncExternalStore } from "react"; + +export const COMPOSER_FONT_DEFAULT = 20; +export const COMPOSER_FONT_MIN = 14; +export const COMPOSER_FONT_MAX = 32; +export const COMPOSER_FONT_STEP = 2; +const STORAGE_KEY = "t3type.composerFontPx"; + +export function clampComposerFont(px: number): number { + if (!Number.isFinite(px)) return COMPOSER_FONT_DEFAULT; + return Math.min(COMPOSER_FONT_MAX, Math.max(COMPOSER_FONT_MIN, Math.round(px))); +} + +function readStored(): number { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw === null ? COMPOSER_FONT_DEFAULT : clampComposerFont(Number(raw)); + } catch { + return COMPOSER_FONT_DEFAULT; + } +} + +let current = typeof window === "undefined" ? COMPOSER_FONT_DEFAULT : readStored(); +const listeners = new Set<() => void>(); + +export function stepComposerFont(direction: 1 | -1) { + const next = clampComposerFont(current + direction * COMPOSER_FONT_STEP); + if (next === current) return; + current = next; + try { + localStorage.setItem(STORAGE_KEY, String(next)); + } catch {} + listeners.forEach((listener) => listener()); +} + +function subscribe(listener: () => void) { + listeners.add(listener); + return () => listeners.delete(listener); +} + +export function useComposerFont(): number { + return useSyncExternalStore( + subscribe, + () => current, + () => COMPOSER_FONT_DEFAULT, + ); +} diff --git a/tests/lib.test.ts b/tests/lib.test.ts index 685e184..ff06bf7 100644 --- a/tests/lib.test.ts +++ b/tests/lib.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { compactSidebarTimeLabel, formatRelativeElapsed } from "~/lib/time"; +import { clampComposerFont } from "~/lib/composerFont"; import { cn } from "~/lib/utils"; describe("compactSidebarTimeLabel", () => { @@ -19,3 +20,12 @@ describe("cn", () => { expect(cn("text-2xs", "text-sm")).toBe("text-sm"); }); }); + +describe("clampComposerFont", () => { + it("keeps sizes inside 14 to 32 and falls back to 20 for junk", () => { + expect(clampComposerFont(8)).toBe(14); + expect(clampComposerFont(40)).toBe(32); + expect(clampComposerFont(22.4)).toBe(22); + expect(clampComposerFont(Number("abc"))).toBe(20); + }); +}); diff --git a/tests/play.mjs b/tests/play.mjs index cf160ff..7601510 100644 --- a/tests/play.mjs +++ b/tests/play.mjs @@ -205,11 +205,32 @@ async function checkPerPromptWpmInLight(browser) { await page.close(); } +async function checkComposerFontSize(browser) { + const { page, errors } = await openPage(browser, "dark", "speed=4"); + const editorPx = () => + page.locator('[data-testid="composer-editor"]').evaluate((el) => parseFloat(getComputedStyle(el).fontSize)); + check((await editorPx()) === 20, `prompt text defaults to 20px (got ${await editorPx()})`); + await page.getByRole("button", { name: "Larger prompt text" }).click(); + await page.getByRole("button", { name: "Larger prompt text" }).click(); + check((await editorPx()) === 24, `two clicks on larger give 24px (got ${await editorPx()})`); + await page.keyboard.type("ab"); + check(activeOf(await state(page)).draft === "ab", "typing still reaches the composer after clicking the size buttons"); + await page.reload(); + await page.waitForFunction(() => window.__game !== undefined); + check((await editorPx()) === 24, `size survives a reload (got ${await editorPx()})`); + await page.screenshot({ path: join(shots, "dark-font-24.png") }); + for (let i = 0; i < 20; i += 1) await page.getByRole("button", { name: "Smaller prompt text" }).click({ force: true }); + check((await editorPx()) === 14, `size stops at 14px (got ${await editorPx()})`); + check(errors.length === 0, `no console errors in font size run${errors.length ? `: ${errors.join(" | ")}` : ""}`); + await page.close(); +} + const browser = await chromium.launch(); try { await playFullSessionInDark(browser); await checkAutoSettle(browser); await checkPerPromptWpmInLight(browser); + await checkComposerFontSize(browser); } finally { await browser.close(); server?.kill();