Skip to content

Names and caption fonts: eleven caption styles in both builders, every Latin letter they draw, provider names kept whole - #844

Merged
TheAnswerManIsHere merged 8 commits into
mainfrom
claude/names-and-fonts
Nov 3, 2026
Merged

TheAnswerManIsHere merged 8 commits into
mainfrom
claude/names-and-fonts

Conversation

@TheAnswerManIsHere

@TheAnswerManIsHere TheAnswerManIsHere commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Workstream: #719

What & why

The names-and-fonts plan David approved on 2026-11-03, built as one increment.

  • Names: the name rule now admits every Latin letter all eleven caption fonts draw, so "Nguyễn", "Ștefan" and "Türkoğlu" can be typed.
  • Fonts: both builders offer eleven visibly different caption styles, and a saved or downloaded meme is drawn in exactly the face its preview showed.
  • Google and Apple names: a provider name the rule can't hold is no longer stored garbled; onboarding asks for the name in English letters.

Before this, four of the wizard's five fonts saved in a fallback face that didn't match the preview, the meme page's builder had no font choice, and a Google name like "Nguyễn" was stored as "Nguy n".

Approved-plan oracle

Oracle source: docs/plans/PLAN_NAMES_AND_FONTS.md, sha256 5856af1e38c6cb42a73c4f339aa24c9b379fccd47f91de4252876ffbdf531b6c (committed on this branch).

Direction: n/a — the plan cites David's 2026-11-02 English-script decision in decisions.md, quoted in its oracle block.

Product intent:

  1. The name rule admits every Latin letter all eleven caption fonts draw. That includes Vietnamese, Romanian, Turkish, Polish, Czech and the Western European accents. Nothing outside the Latin alphabet is admitted, and nothing changes about punctuation, normalization or the 50-character field limit.
  2. A meme's caption can be set in one of eleven visibly different styles, and the downloaded or saved meme matches its preview in that style. Anton stays the default.
  3. A Google or Apple first or last name the rule can't hold is not stored. The account just lacks it, and sign-in proceeds as before. Onboarding's required display-name field, already the place every new account confirms its name, tells the visitor to write the name in English letters. Nothing is ever transliterated automatically.
  4. Memes already saved, and memes still rendered from a saved recipe, keep the face they had.

Must not change:

  • Latin script only; the 50-character limit, which the field stops at; one rule for browser and server; the disallowed-character and length hints.
  • Phase 2's onboarding name order (draft, the account's explicit display name, the browser's name, blank). A provider name never displaces the browser's name.
  • An account's existing names are never overwritten by a later sign-in.
  • Stored meme files are untouched. A legacy font string in a stored recipe resolves to exactly the face it resolves to today.
  • Anton is the default caption face.

Settled decisions:

  • The eleven faces (David, 2026-11-03, from 75 Google Fonts measured with each file's own character map, fc-query -i 0, and a page showing each face with sample names and memes). All are free, open-licensed Google Fonts:

    Id Style Font Weight Letters it lacks
    block Block (default) Anton (already bundled) 400 none
    sans Clean sans Montserrat 800 none
    serif Elegant serif Playfair Display 700 none
    slab Slab poster Alfa Slab One 400 ſ Ǎ ǎ Ǔ ǔ
    comic Comic shout Bangers 400 ʼn ſ Ǎ ǎ Ǔ ǔ
    stencil Stencil Black Ops One 400 ʼn ſ Ǔ ǔ
    rounded Rounded, playful Baloo 2 700 ſ
    hand Handwritten Patrick Hand 400 none
    sketch Sketched capitals Amatic SC 700 ſ
    script Script Lobster 400 none
    terminal Retro terminal VT323 400 ſ Ǎ ǎ Ǔ ǔ

    Dropped after measurement: Bebas Neue and Oswald-like condensed faces (too close to Anton), Pacifico (lacks IJ ij ſ), and every horror, pixel, western, marker and graffiti face measured, each of which lacks 100 or more letters including all of Vietnamese.

  • Two of today's letters leave the rule: ʼn and ſ (David, 2026-11-03). Both are obsolete (ʼn was deprecated by Unicode; ſ left English print around 1800), Bangers and Black Ops One lack ʼn, and five of the faces lack ſ. David accepted this in choosing the eleven. No stored name holds either (Phase 2's check found no name outside the rule; an account with one would meet the field's refusal on its next edit, as any refused character does).

  • The admitted letter set is derived from the font files, not written by hand (Astra, rounds 1 and 2). A candidate character must have Unicode General_Category Letter and Script Latin. It is admitted when every chosen face contains it, and contains every code point of its JavaScript uppercase mapping, the form the caption renderer's all-caps draws (toUpperCase, memeGenerator.ts:329, LivePreview.tsx:161). Measured 2026-11-03 on the eleven files, the result is 349 characters:

    U+0041–U+005A U+0061–U+007A U+00AA U+00BA U+00C0–U+00D6 U+00D8–U+00F6 U+00F8–U+0148 U+014A–U+017E U+01A0–U+01A1 U+01AF–U+01B0 U+01FE–U+01FF U+0218–U+021B U+0237 U+1E80–U+1E85 U+1EA0–U+1EF9

    That is:

    • every letter admitted today except ʼn and ſ (above);
    • all Vietnamese, Romanian and Turkish letters;
    • Ǿ ǿ Ẁ ẁ Ẃ ẃ Ẅ ẅ and the ordinal indicators ª º, among others.

    ƒ is out, because its capital Ƒ is missing from several faces; so are the pinyin letters Ǎ ǎ Ǔ ǔ.

    How the set is kept honest:

    • A derivation script in the repository computes the set from the built static font files. The pinned character class in the shared rule is generated from it.
    • A test fails if the pinned class and the derivation disagree in either direction, so a supported letter can't be silently omitted and an unsupported one can't be admitted.
    • The rendering test then draws every admitted letter, and its uppercase, in every face, and compares each with that face's own missing-glyph box. It extends today's test, which stops at U+024F and misses the Vietnamese range (scope NF1). It also checks a decomposed input, such as e plus a combining mark, after NFC.
  • One shared caption-font catalogue in lib/api-zod, which both the browser and the server import (Astra, scope NF2). Each entry has:

    • a stable id (the table above);
    • a label;
    • an internal family name distinct from every legacy CSS string, for example OH Block;
    • its font file;
    • its weight.

    How the catalogue is used:

    • The picker, the preview, request validation, download and save all speak in catalogue ids.
    • textOptions.fontFamily carries an id for new memes. The schema accepts the eleven ids, or a legacy string on a stored recipe only.
    • The server registers each face under its internal family and draws "<internal>", sans-serif.
    • The browser loads the same file via @font-face under the same internal name.
  • The preview and the saved meme never differ in face or weight (Astra, round 1).

    • Preview loading: the preview measures and draws only after the selected face has loaded (document.fonts.load). While it loads, save and download are unavailable. If loading fails, the selection and the draft are kept, a Retry appears, and any other face can be chosen. Save and download return once the face shown matches the selected id.
    • Server registration: if the server cannot register the selected face, the render fails with an error rather than storing an image in a fallback font. Today a registration failure logs a warning and carries on (memeGenerator.ts:48); the missing-packaged-font bug of PR Fix #830: ship the meme font with the built server #831 was exactly that.
    • Weight: each catalogue entry fixes its weight, and the browser and the server both draw new ids at that weight. Today the preview treats an omitted bold as normal and the server treats it as bold (LivePreview.tsx:123, memeGenerator.ts:307).
  • Restored wizard drafts are moved to ids (Astra, round 1). A wizard draft restored from storage (wizardStorage.ts, kept up to an hour) that holds an old picker's CSS stack has its font mapped to the nearest catalogue id before preview or save; the rest of the draft is untouched, and the picker shows the mapped choice. New API requests accept ids only.

  • Legacy font strings keep today's interpretation (Astra, scope NF4). A stored recipe with an omitted fontFamily also keeps today's default face and weight; the new catalogue default (Anton, block) applies to new memes only. A stored recipe whose fontFamily is not a catalogue id resolves exactly as the renderer does today. That path is the legacy recipe render (GET /memes/:slug/image for an unbaked row or a missing file) and the boot backfill. Because the internal family names are new, a legacy string such as 'Bebas Neue', Impact, sans-serif can never start selecting a newly bundled face. A test renders a legacy-string recipe before and after and compares the bytes.

  • Provider names: whole value or nothing (Astra, scope NF3). cleanProvidedPersonalName is replaced by acceptProvidedPersonalName:

    • It normalizes the provider's first or last name.
    • It keeps the name only if the whole normalized value passes the shared rule, refusing both a disallowed character and an over-length name; otherwise it returns null.
    • It never strips, spaces out or truncates.
    • Sign-in proceeds either way, and an existing account name is never replaced.

    The correction surface is onboarding's existing required display-name field. Its guidance line reads "Write your name in English letters — the way you'd like it on your memes." No correction-state subsystem and no stored rejected text.

  • The picker in both builders (Question N1, answered): the wizard's sheet, and a new control in the remix builder on the meme page, both rendering the one catalogue.

Decisions taken on my recommendation

  • Fixed weights: Montserrat at 800, Playfair Display, Baloo 2 and Amatic SC at 700, the rest at their single weight. Captions read as display type at those weights. Undo: change weight in lib/api-zod/src/captionFonts.ts and re-cut the file.
  • The three fonts cut from variable files are renamed inside ("OH Sans", "OH Serif", "OH Rounded"), because Playfair Display's open font licence reserves its name for unmodified copies. Each font's licence ships beside it. Undo: none needed; it is how the licence is met.
  • The picker shows each style's name in the site's own font, not in its face. Showing all eleven faces would download about 2.4 MB the moment the picker opens; the preview shows the chosen face instead. Undo: render each chip's label in its face, in CaptionFontPicker.tsx.
  • An old wizard draft's font maps to the nearest style: DM Sans to Clean sans, JetBrains Mono to Retro terminal, and Bebas Neue, Anton and Impact to Block. The mapping happens wherever the builders read or save a font, which covers a restored draft. Undo: edit the map in captionFonts.ts.
  • The default face is preloaded with the page, so the builder's first caption doesn't wait a round trip. Undo: remove the preload line in index.html.
  • The rule's letters are scanned across all of Unicode, not only the Latin blocks, so a Latin letter in a block nobody thought of is still caught by the two-way test. It finds the same 349 letters the font tools measured. Undo: n/a.
  • The wizard's font notice sits beside its save area, in the scrolling controls rather than under the fixed button bar, because the bar's height sets the page's bottom padding (review round 1). Undo: move CaptionFontStatus in Step2Image.tsx.
  • The old-recipe check compares font strings and renders, not stored image hashes (review round 1): hashes recorded on one machine failed on CI's, since the canvas draws pixels slightly differently per machine. It checks the canvas font string is exactly the one the earlier code built, for every old font with bold and italic, and that old recipes still draw as Impact. Undo: n/a.
  • A small test-only hook in the renderer simulates a face that failed to register, since the test runner shares one process and can't make a real registration fail. Undo: remove withCaptionFontUnregisteredForTest and its test.

Latitude granted to me: none.

Pre-PR quality pass: /simplify ran (reuse, simplification, efficiency, altitude); its fixes are commit d8a7cd4.

Review round 1 (0417c42): the wizard's font failure is now explained, with its retry, outside Advanced options.

Specialist review: the sign-in change (provider names) is in the auth class, so Codex's security review on this PR is its specialist review.

Verification

  • Server, full suite (pnpm test, four shards), on the branch with main merged in: 3,632 tests, 0 failed.
  • New server tests:
    • every face ships identically to the server and the web app and renders differently from every other;
    • a catalogue face draws at its fixed weight whatever "bold" says;
    • a request accepts only catalogue ids, while a stored recipe keeps its old string;
    • a stored recipe's canvas font string is exactly the one the earlier code built; no font still draws as Impact, Impact is still Anton, and an old Bebas Neue stack still draws as Impact (the canvas finds "Impact" inside it);
    • a face that did not register refuses to render.
  • Name rule: the pinned 349 letters equal those derived from the font files in both directions; every admitted character and its capital is a real glyph in all eleven faces; the shared table accepts Vietnamese (composed and decomposed), Romanian, Turkish, Czech and IJ, and refuses Greek, ƒ, ſ, ʼn and ǎ.
  • Provider names: a refused Google first name is stored as nothing, an accepted surname is kept whole, and a later sign-in never overwrites either (a real callback test against the test database).
  • Frontend (npx vitest run): 100 files, 1,046 tests passed. Each face loads from the server's file under its family and weight; the preview waits for the face; a failed load offers Try again; in the real wizard, with Advanced options closed, a failed face shows its notice and Try again; saves always send a face id; an old draft previews and saves in the mapped face; onboarding shows the English-letters line.
  • pnpm run typecheck: clean. pnpm run check:codegen-drift: clean.

Post-merge verification (live environment)

  • Live, read-only: through the Replit connector after the sync, list the built server's font directory and confirm the eleven caption files are present, so the Repl's server can draw every style. That is the PR Fix #830: ship the meme font with the built server #831 lesson: a font missing from the build silently falls back.

UAT

Captions now come in eleven styles in both builders, and names can hold Vietnamese, Romanian and Turkish letters. The machine checks run in CI; the steps that are yours are how the styles look and whether the meme page's picker feels right.

Setup

  • [claude] Confirm the Repl is synced to the merge commit and its built server lists the eleven caption fonts.

Steps

1. Each style looks right on a meme with an accented name

Do: On a fact page on the Repl, open the builder, set your name to "Zoë Nguyễn", pick a photo, open Advanced options and try each caption style; save or download one meme in two or three styles.

Expect: Each style looks distinct and suits a meme, the accents sit correctly, and the saved meme looks the same as its preview.

Lane: human

Judgement: Whether each style looks good on a real meme, and whether the saved image matches what you saw, is a visual judgement only you can make.

2. The meme page's builder offers the same styles

Do: Open a shared meme's "make it your own" builder and look below the preview.

Expect: The same eleven caption styles appear, and choosing one changes the preview.

Lane: human

Judgement: Whether the picker feels at home in that builder is your call.

3. Onboarding asks for the name in English letters

Do: Run the onboarding screen tests.

Expect: The name field's guidance reads "Write your name in English letters — the way you'd like it on your memes."

Lane: ci

Check: artifacts/overhype-me/src/pages/Onboard.test.tsx

4. Every face ships, renders distinctly and refuses to fall back

Do: Run the caption font tests.

Expect: Every face is identical on the server and the web app, each renders differently, and an unregistered face refuses to render.

Lane: ci

Check: artifacts/api-server/src/__tests__/captionFonts.test.ts

5. The name rule matches the fonts

Do: Run the coverage and rule tests.

Expect: The pinned letters equal those the fonts draw, every admitted letter and its capital renders in every face, and the shared table passes.

Lane: ci

Check: artifacts/api-server/src/__tests__/nameRuleFontCoverage.test.ts

6. Google names are kept whole or not at all

Do: Run the Google callback tests.

Expect: A refused first name is not stored, an accepted surname is kept whole, and a later sign-in never overwrites a stored name.

Lane: ci

Check: artifacts/api-server/src/__tests__/routes.google.callback.test.ts

Regression

R1. Old memes keep their font

Do: Run the caption font tests.

Expect: Stored recipes with no font, "Impact", or an old Bebas Neue stack get the same canvas font as before and still draw in Anton.

Lane: ci

Check: artifacts/api-server/src/__tests__/captionFonts.test.ts

R2. The browser's builders load and wait for the chosen face

Do: Run the browser caption font tests.

Expect: The preview waits for the face, a failed load offers Try again in both builders (in the wizard without opening Advanced options), and every save sends a face id.

Lane: ci

Check: artifacts/overhype-me/src/components/meme-builder/__tests__/captionFonts.test.tsx

Not bugs

  • Two obsolete letters, ʼn and ſ, are no longer allowed in names; David accepted this when choosing the eleven fonts.
  • A Google or Apple name with a letter outside the rule leaves that name blank on the account; onboarding asks for it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMMrrrX1dR5EdX15XJxkAZ

David approved the plan on 2026-11-03 with the picker in both builders and
eleven caption faces chosen from a measured shortlist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMMrrrX1dR5EdX15XJxkAZ
…ared list

Bundles the eleven caption faces David chose (static weights; the three cut
from variable fonts renamed, as the open font licence requires for a modified
copy) with their licences, adds the shared catalogue in api-zod, registers
every face on the server, draws a catalogue id at its fixed weight, refuses a
render when the chosen face did not register, and keeps every stored font
string drawing exactly as before. Requests may name only a catalogue face;
stored recipes keep their old strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMMrrrX1dR5EdX15XJxkAZ
…ovider names kept whole or not at all

The admitted letters are now derived from the shipped caption fonts (349:
adds Vietnamese, Romanian and Turkish; drops the obsolete ʼn and ſ, which
several faces lack) and pinned in a generated file; a test fails when the pin
and the fonts disagree in either direction, and every admitted character and
its uppercase is drawn in every face. A Google or Apple name is stored only
when the whole name passes the rule, so nothing garbled is ever kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMMrrrX1dR5EdX15XJxkAZ
…n face

Replaces the wizard's five-entry font list (four of which saved in a fallback
face) with the eleven-face picker, adds the same picker to the meme page's
remix builder, loads only the chosen face from the file the server draws
with, draws the preview only once it has loaded and holds save and download
until then, offers a retry when a face fails to load, stamps the face id on
every save and download, and moves a restored draft's old font stack to the
nearest face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMMrrrX1dR5EdX15XJxkAZ
…er names whole

Onboarding's name field now says to write the name in English letters, the
way it should appear on memes, since a Google or Apple name the rule can't
hold is no longer stored. A Google sign-in test drives a refused first name,
an accepted surname, and a later sign-in that never overwrites either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMMrrrX1dR5EdX15XJxkAZ
The picker loads and reports its own face's status, so both builders just
place it; a restored draft's old font is normalized where every read and
save already normalizes it, not again at restore; the Anton-only path
helper gives way to the general one; the default caption face is preloaded
so the preview's first caption is not a round trip late; two stale comments
corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMMrrrX1dR5EdX15XJxkAZ
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-03T00:58:36.407162Z 0417c42 Manual request
🔒 Security Review ✅ Completed 2026-11-03T00:45:53.018060Z d8a7cd4 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8a7cd4968

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Owner Author

Astra — round 1

Assessed at d8a7cd4968ec998068951a7d3f67d5233c199938 · findings 4449520994 · requested gpt-6-astra at xhigh

Oracle met at this head: no

A temporary font-download failure can leave someone unable to make their meme, with no visible explanation or retry. The approved plan explicitly promises that recovery. I recommend a bounded interface correction using the existing loader; the overall font approach does not need replacing. No new product decision is needed from David.

Finding 4449520994 — recommend correcting it.

Direct inspection confirms the reported path. Step2Image.tsx reduces the font status to readiness and disables saving until ready. AdvancedOptionsSheet.tsx starts closed and mounts the picker only when opened. That picker contains the failure explanation and retry. The preview also withholds captions until the font is ready, without displaying an explanation itself.

The bounded failure class is a required resource blocking the main action while its recovery controls exist only inside optional, collapsed settings. It covers the initial default font, a restored selection, and a newly selected font whose panel is closed before loading fails. A failed network request is sufficient; no unusual user behavior is needed. Opening Advanced options can recover, but the interface gives users no reason to discover that workaround. Preventing this unexplained interruption is worth the small correction, particularly because visible recovery is an explicit settled requirement.

The underlying captionFontLoader.ts already exposes status and retry. I recommend presenting those outside the collapsible content, near the preview or blocked action, while keeping an understandable route to choose another style. Preserve the selected font and draft, and keep saving unavailable until the selected font loads: allowing a fallback would defeat the agreed preview/save match. This needs no separate recovery state or automatic retry system. The remix builder already mounts its picker outside a collapsible panel; inspection does not establish the same defect there.

For verification, demonstrate that:

  • Failure with Advanced options closed visibly explains the problem and offers retry, both on initial entry and after choosing another font and closing the panel.
  • Retry or choosing another font restores captions and saving after successful loading, without losing the draft or silently changing the selection.
  • Recovery remains available in the remix builder if shared presentation changes.

The inspected existing failure test mounts the picker directly, so it does not establish recovery in the collapsed wizard. My conclusion is based on code inspection, not a browser reproduction; focused verification of the composed wizard would address that gap.

I recommend Claude make this correction and verify those outcomes, then continue through the existing review and required checks.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Fable — round 1

Assessed at d8a7cd4968ec998068951a7d3f67d5233c199938 · findings 4449520994 · expected claude-fable-5-1 · instructed alias fable · definition effort xhigh

Oracle met at this head: no
Running as: Claude Fable 5.1 (claude-fable-5-1) at unable to name.

Readout

In the meme wizard — the main "Make my meme" path — if the chosen lettering style fails to download, the button goes grey and the preview shows the photo with no words, and nothing on screen says why or offers a way out. The "try again" and the other styles exist, but only behind "Advanced options", which starts closed, and a visitor has no cue to look there. The approved plan says a failed load shows a Retry; in the wizard it does not until the visitor guesses. That is why the gate reads "no" — one named requirement, in one of the two builders, in a failure path; the remix builder on the meme page is fine because its style picker is always on screen.

I recommend fixing it in this round: one screen, a small change (show the same "didn't load — try again" line where the visitor is already looking), plus a check that mounts the actual wizard step, since the existing check looked only at the picker on its own and so passed while the wizard had this gap.

One consequence to know about, not to decide: a visitor whose browser refuses font downloads altogether can no longer make a meme in either builder, because the agreed rule is that a saved meme must match its preview, so nothing is saved in a stand-in face. The plan's preview-loading decision covers that trade; the fix at least tells such a visitor what happened.

Assessment

4449520994 — correct it

Verified by my own inspection (checkout at the reviewed commit, clean):

  • Wizard: Step2Image.tsx:155 keeps only useCaptionFont(...).status === "ready"; the primary action is disabled={!sourceSelected || !captionFontReady} (:286) and handleMakeMyMeme returns early on !captionFontReady (:215). Nothing in the file renders the status.
  • The only renderer of the failed and loading text is CaptionFontPicker.tsx:41–54, mounted by AdvancedOptionsSheet.tsx:56–58 inside {isOpen && …}, with isOpen from useState(false) (:31) and no prop to open it from outside.
  • The preview draws the background and returns before the caption when fontStatus !== "ready" (LivePreview.tsx:132–135): a photo without words.
  • Remix builder: MemeBuilder.tsx:183–186 renders CaptionFontPicker unconditionally, so its failed line and Retry are visible there. Not a member of the class.
  • Loader: status is a module-level Map with listeners (captionFontLoader.ts:17–23); retry is loadCaptionFont(id) (:56), which reloads a failed face (:26–28) and notifies every subscriber. So the finding's "preserve the load status" is already true — the status is held and shared; Step2Image only discards it locally. The correction is presentational: read { status, retry } from the hook the component already calls.

Class. A status that disables a step's primary action, whose explanation and recovery control are rendered only in a subtree not mounted by default. useCaptionFont has four consumers (grep over artifacts/overhype-me/src): Step2Image, MemeBuilder, LivePreview, CaptionFontPicker. Step2Image is the only one that both gates an action and shows nothing; the one member. The plan put the picker itself in the sheet (Preflight, affected surfaces: "the wizard's font picker (AdvancedOptionsSheet.tsx)"), so the picker stays where it is — only the status line needs to escape the disclosure.

Credible paths and consequence. (1) The font file missing from a deployment — the plan cites PR #831 as exactly that bug on the server side; the browser's copies are eleven static .ttf files under public/fonts/caption/ (93–422 KB, listed), a separate thing to go missing. (2) A dropped request on a mobile connection — the photo may have loaded moments earlier, so the visitor has no reason to suspect the network. (3) A browser or extension set to refuse font downloads — permanent; Retry and "pick another" cannot help. In each, a visitor on the product's primary flow sees a grey button and a caption-less preview and most likely leaves; David would not hear of it, because nobody reports a grey button. Before this PR the same visitor could save (in a mismatched face). Low detectability, silent per incident, lasting until someone tries the wizard under the same condition. That outweighs the cost: a few lines in one component and one check.

Who produces the value. The browser's font loader, outside the agent's control. A status display at that boundary is in scope.

Correction, scope and constraints.

  • Scope: Step2Image only. Render, outside any disclosure, the failed state with the picker's wording and a Retry wired to the hook's retry. The disabling condition is status !== "ready", so the same slot can carry the loading cue for a string's cost; note the load is asynchronous even when cached, so a loading line flashes briefly on every entry (the remix picker already does this). Required: failed. Loading: Claude's call.
  • Placement: visible without opening anything and near the disabled button. WizardPrimaryAction has an unused subText slot under the button (:8–9, :44–46); the bar is fixed … bottom-0 (:31), and Step2Image.tsx:237–247 sizes the scroll area's bottom padding to the bar's tallest state — the unregistered tier's pb-24 is the bare bar's height (24 + 56 + 16 px), so a line in that slot needs the padding to still clear it. The alternative is beside the existing saveError alert in the scroll area (:271–278), which avoids the padding question but can scroll out of view. A technical fork; decide and name which.
  • Prefer one statement of the wording and test id — a small shared status-line component used by the picker and the wizard is the cheap way; inlining is acceptable.
  • Do not lift the sheet's open state to auto-open it (coupling for no visitor benefit), and do not draw a stand-in face on failure (the settled decision forbids it). "Pick another" from outside the sheet is not a constraint: Retry is the recovery for the transient case, and the text can point at Advanced options for another style.

Acceptance observable. A check that mounts Step2Image (or the wizard step around it) with Advanced options closed, rejects the face load, and finds the failed line and its Retry without clicking advanced-options-trigger; after Retry and a resolved load, "Make my meme" is enabled. The existing test (captionFonts.test.tsx:72–83) renders CaptionFontPicker alone and is precisely the check that passed while the wizard had the gap — a check that does not mount the wizard step proves nothing about it. No test renders Step2Image today (glob over **/*.{test,spec}.{ts,tsx} in artifacts/overhype-me: none), so this is a new harness; it is the proof the plan's own Testing line already promises — "a failed load shows Retry, keeps the draft, and lets another face be chosen."

Material uncertainty. Whether the padding constraint bites depends on the placement chosen; I did not render it. The share of visitors who block font downloads is small and unmeasured; stated as a consequence, not a probability.

Accepted consequence, named rather than asked. At this head a visitor whose browser refuses font downloads cannot make a meme in either builder. The settled decision "the preview and the saved meme never differ in face or weight" (plan, Settled decisions; Astra, plan round 1; approved 2026-11-03) makes "no face, no save" the agreed trade, and the fix makes the state visible. No action recommended.

Questions and next action

  • For Claude: nothing disputed. Placement is the one fork, bounded by the padding constraint above.
  • For Astra: nothing to settle. If Astra reads the gate as met because the Retry exists behind the disclosure, my reading is that the oracle's "a Retry appears" describes what the visitor sees when the load fails, and in the wizard's default state nothing appears.
  • For David: nothing to decide this round.

Next action: write. Round 1, one finding, a bounded presentational fix in one component plus one check that mounts the wizard step. All of it rests on the inspection above; nothing waits on an unresolved question. A recommendation on content, not merge authorisation.


Generated by Claude Code

…zard's collapsed panel

The wizard keeps its style picker under Advanced options, which starts
closed, so a caption face that failed to load left Make my meme disabled
with no reason and no retry on screen. A shared status line now says the
style is loading or didn't load, with Try again, beside the wizard's save
area; the picker uses the same line. The old-recipe check no longer pins
image hashes from one machine, which failed on CI's: it checks the canvas
font string is the one the earlier code built, that Impact is still Anton,
and that an old picker stack still draws as Impact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMMrrrX1dR5EdX15XJxkAZ

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 0417c4261c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants