feat(typography): render the app in GT America - #44
Conversation
There was a problem hiding this comment.
Pull request overview
This PR migrates the Roku app’s typography from Roku system fonts to the put.io brand face (GT America) end-to-end, while keeping the licensed font binaries out of the public repo via a pinned/verified sync flow and a deliberate runtime fallback.
Changes:
- Introduces a pinned brand-font manifest plus a sync/check script (sha256 verified) and CI/release wiring to fetch fonts only for release artifacts.
- Adds a centralized Typography role scale (
applyTypography) and migrates UI components away fromfont:*SystemFontliterals, including a newScreenHeaderto replace Overhang’s unbrandable title. - Updates packaging/build config to compile a
buildConfigBrandFontsAvailable()flag, adds tests/guards to prevent font binaries from being committed, and refreshes.vrefreferences for GT America.
Reviewed changes
Copilot reviewed 67 out of 88 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/live-test/visual-capture.test.ts | Updates Lab visual capture navigation indices for new stories. |
| test/live-test/roku-package.test.ts | Extends build config tests to include brand-font availability flag. |
| test/live-test/brand-fonts.test.ts | Adds Vitest contract tests for manifest, ignores, and font usage audits. |
| source/BuildConfig.brs | Adds buildConfigBrandFontsAvailable() default implementation. |
| scripts/sync-brand-fonts.ts | Adds font sync/check implementation with sha256 verification. |
| scripts/roku-task/runtime.ts | Adds runCapture() helper for capturing command stdout. |
| scripts/roku-task/live.ts | Adds Lab story titles for typography and screen header. |
| scripts/roku-task/build.ts | Adds verify-time guard preventing committing font binaries; adds fonts tasks. |
| scripts/roku-task.ts | Registers fonts-setup/fonts-check and updates help output. |
| scripts/package-roku.ts | Conditionally bundles fonts/ and compiles brand-font availability into BuildConfig. |
| scripts/live-test/visual-capture.ts | Adds new Lab stories to the visual capture story list. |
| scripts/live-test/scenegraph.ts | Updates player OSD layout assertion for taller font line box. |
| docs/FONTS.md | Documents the font supply chain, licensing boundary, fallback, and type scale. |
| CONTRIBUTING.md | Adds contributor guidance about GT America and never committing font binaries. |
| config/phosphor-icons.json | Adds check icon for TrackMenu selection indicator. |
| config/brand-fonts.json | Adds pinned GT America font manifest (repo+commit+sha256 per face). |
| components/shared/VideoConversionStatus/VideoConversionStatus.xml | Removes system font literals; includes BuildConfig + Typography scripts. |
| components/shared/VideoConversionStatus/VideoConversionStatus.brs | Applies typography roles for dialog labels. |
| components/shared/Typography/Typography.brs | Adds shared role-based typography + build-config-gated brand font resolution. |
| components/shared/TrackMenu/TrackMenu.xml | Replaces checkmark text with Phosphor check poster; removes font literals. |
| components/shared/TrackMenu/TrackMenu.brs | Applies typography and simplifies focus rendering for track rows. |
| components/shared/ScreenHeader/ScreenHeader.xml | Adds new ScreenHeader component using Overhang + branded title Label. |
| components/shared/ScreenHeader/ScreenHeader.brs | Implements title rendering + measured layout and role application. |
| components/shared/ListItem/ListItem.xml | Removes font literals; includes BuildConfig + Typography scripts. |
| components/shared/ListItem/ListItem.brs | Applies typography roles to title/description/value labels. |
| components/shared/EmptyState/EmptyState.xml | Removes font literals; includes BuildConfig + Typography scripts. |
| components/shared/EmptyState/EmptyState.brs | Applies typography roles for heading/body. |
| components/shared/DeleteFileDialog/DeleteFileDialog.xml | Removes font literals; includes BuildConfig + Typography scripts. |
| components/shared/DeleteFileDialog/DeleteFileDialog.brs | Applies typography roles for title/message/buttons. |
| components/shared/ContinueWatchingPrompt/ContinueWatchingPrompt.xml | Removes font literals; includes BuildConfig + Typography scripts. |
| components/shared/ContinueWatchingPrompt/ContinueWatchingPrompt.brs | Applies typography roles for title/file/buttons. |
| components/shared/AppDialog/AppDialog.xml | Removes font literals; includes BuildConfig + Typography scripts. |
| components/shared/AppDialog/AppDialog.brs | Applies typography roles for title/message/buttons. |
| components/screens/VideoPlayer/VideoPlayer.xml | Removes font literals; includes BuildConfig + Typography scripts. |
| components/screens/VideoPlayer/VideoPlayer.brs | Applies typography roles across player labels and control labels. |
| components/screens/Video/Video.xml | Replaces Overhang with ScreenHeader. |
| components/screens/Video/Video.brs | Removes direct Overhang color application (handled by ScreenHeader). |
| components/screens/Settings/Settings.xml | Replaces Overhang with ScreenHeader. |
| components/screens/Settings/Settings.brs | Removes direct Overhang color application (handled by ScreenHeader). |
| components/screens/Search/SearchHistoryItem.xml | Removes font literal; includes BuildConfig + Typography scripts. |
| components/screens/Search/SearchHistoryItem.brs | Applies typography role to history item title label. |
| components/screens/Search/Search.xml | Replaces Overhang with ScreenHeader. |
| components/screens/Search/Search.brs | Removes direct Overhang color application (handled by ScreenHeader). |
| components/screens/Image/Image.xml | Replaces Overhang with ScreenHeader. |
| components/screens/Image/Image.brs | Removes direct Overhang color application (handled by ScreenHeader). |
| components/screens/Home/Home.xml | Replaces Overhang with ScreenHeader. |
| components/screens/Home/Home.brs | Removes direct Overhang color application (handled by ScreenHeader). |
| components/screens/History/HistoryListItem.xml | Removes font literals; includes BuildConfig + Typography scripts. |
| components/screens/History/HistoryListItem.brs | Applies typography roles to title/description labels. |
| components/screens/History/History.xml | Replaces Overhang with ScreenHeader. |
| components/screens/History/History.brs | Removes direct Overhang color application (handled by ScreenHeader). |
| components/screens/Files/Files.xml | Replaces Overhang with ScreenHeader (options text/availability preserved). |
| components/screens/Files/Files.brs | Removes direct Overhang color application (handled by ScreenHeader). |
| components/screens/Files/FileListItem.xml | Removes font literals; includes BuildConfig + Typography scripts. |
| components/screens/Files/FileListItem.brs | Applies typography roles to title/description. |
| components/screens/Auth/Auth.xml | Removes font literals; includes BuildConfig + Typography scripts. |
| components/screens/Auth/Auth.brs | Applies typography roles to auth labels and code tiles. |
| components/screens/Audio/Audio.xml | Replaces Overhang with ScreenHeader; removes font literals. |
| components/screens/Audio/Audio.brs | Removes direct Overhang color application; applies typography to time labels. |
| components/lab/Lab.brs | Adds Lab stories for typography calibration and ScreenHeader validation. |
| bsconfig.json | Includes fonts/**/*.* in project file globs. |
| AGENTS.md | Documents new font system tasks and worktree font carry behavior. |
| .worktreeinclude | Includes /fonts for agent worktrees. |
| .vref/README.md | Adds rule to record references with GT America bundled + tag guidance. |
| .vref/manifest.json | Updates reference metadata/tags/notes for GT America recaptures. |
| .vref/index.html | Updates generated gallery for new gt-america tag filtering and metadata. |
| .gitignore | Ignores synced font binaries (/fonts/*.otf, /fonts/*.ttf). |
| .github/workflows/release.yml | Mints read token and runs fonts sync/check before packaging release ZIP. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Fixed in 206a194: the header divider now only draws when there is a title. Replacing Overhang's built-in divider with our own Verified per screen on device: Home shows the logo alone; Files, Settings, Search and History keep their divider. |
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…flag Addresses review feedback on #44. Availability was "any .otf/.ttf under fonts/", so a partial directory compiled buildConfigBrandFontsAvailable() to true and left individual roles resolving to missing pkg:/fonts URIs, which Roku renders in the system font per label and shows as mixed typography. It now requires every face pinned in config/brand-fonts.json, and packaging skips the fonts root entirely otherwise so a partial set cannot ship. This is what docs/FONTS.md already claimed; the doc is corrected to state the all-or-nothing rule explicitly. The branded header title reserved no space for the options affordance, so a long title (Files sets it from the folder name) drew straight over "Delete *". The title now reserves that region. showOptions moves from an aliased field to a forwarded one: aliases fire no onChange, so a screen setting showOptions after title left the width computed from a stale value -- which is exactly what the first attempt at this fix hit on device. Font downloads stage under the gitignored dist/tmp instead of os.tmpdir() so the moves into fonts/ are same-filesystem renames; tmpdir can be a different mount, where rename fails with EXDEV. Dropped the gh auth status pre-flight gate. It exits 0 even when GH_TOKEN is invalid, so it never caught a real problem, while risking a false negative in environments where only GH_TOKEN is set. The gh api call reports auth failures directly and now names both gh auth login and GH_TOKEN. Co-Authored-By: Claude <noreply@anthropic.com>
|
Worked through all four review findings — three were real, one I'm pushing back on. Fixed in 9edd0b4 and 234d2ce. 1. Partial
|
fonts/ contents |
flag | bundled |
|---|---|---|
| all 3 pinned faces | true |
3 |
| 1 of 3 | false |
0 (+ fallback logged) |
| unlisted face only | false |
0 |
Locked by fixture-based tests in brand-fonts.test.ts (complete / partial / empty / missing dir).
2. Header title ignored the options width — real (Cursor)
Confirmed on device before fixing: the title ran straight over Delete *. The title now reserves that region.
Worth flagging that my first attempt at this silently didn't work: showOptions was an aliased field, aliases fire no onChange, and screens set showOptions after title — so the width was computed from a stale value. It's now a forwarded field with a handler. Measured after: title ink ends x=989, options ink starts x=1052, 63px clear at 720p.
3. gh auth status blocks the release — pushing back, but changed anyway
I could not reproduce the described failure: gh auth status does treat GH_TOKEN as an auth source. What I did find is worse for the check itself — it exits 0 with a deliberately invalid token, so it never gated anything real.
I removed the pre-flight gate rather than keep it: it earned nothing and added an environment-dependent failure mode ahead of the download. The gh api call reports auth failures directly and now names both gh auth login and GH_TOKEN. Same outcome as suggested, different reasoning — flagging that the stated mechanism didn't hold up.
4. Non-atomic rename across faces — real, and there was a sharper edge (cubic)
All downloads were already verified into staging before any rename, so the window was narrower than described. But the staging dir was os.tmpdir(), which can be a different mount — on CI rename into the repo would fail with EXDEV outright. Staging moved under the gitignored dist/tmp so the moves are same-filesystem renames. Exercised by a real re-sync of two faces.
pnpm verify green with the faces (138 files) and without (135, zero font entries, fallback logged).
There was a problem hiding this comment.
All reported issues were addressed across 7 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…packaging Second round of review feedback on #44. showOptions regressed in the previous commit: converting it from an aliased field to a forwarded one meant screens setting it as an XML attribute never reached the handler with m.overhang resolved, so the options affordance would not appear at all. init() now forwards it once and the handler still covers later writes. Packaging treated filename presence as availability, so a stale or truncated face could still advertise the brand face while its pkg:/fonts URI fell back per label. Availability now comes from inspectBrandFonts(): every pinned face present, digest matching, and no unlisted face that would ship unverified beside them. assertPlayerOsdLayout pinned playerTitle's measured height to 58, which only holds with the faces bundled; the same h1 role measures 46 on a fonts-less package, so the player suite could not pass against both. It accepts either and leaves the overlap assertion to guard the layout. The Lab calibration story now says so on screen when a build has no brand fonts, rather than letting a capture compare the system font against itself and look like a passing A/B. .gitignore gains /fonts/*.ttc so the ignore rules cover every extension checkRokuFontBinaries fails on, with the test and docs updated to match. Co-Authored-By: Claude <noreply@anthropic.com>
|
Second round addressed in 30b895a. Six findings; five real, one I'm rejecting with evidence. P1 —
|
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b996cf7. Configure here.
There was a problem hiding this comment.
All reported issues were addressed across 23 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 9 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…flag Addresses review feedback on #44. Availability was "any .otf/.ttf under fonts/", so a partial directory compiled buildConfigBrandFontsAvailable() to true and left individual roles resolving to missing pkg:/fonts URIs, which Roku renders in the system font per label and shows as mixed typography. It now requires every face pinned in config/brand-fonts.json, and packaging skips the fonts root entirely otherwise so a partial set cannot ship. This is what docs/FONTS.md already claimed; the doc is corrected to state the all-or-nothing rule explicitly. The branded header title reserved no space for the options affordance, so a long title (Files sets it from the folder name) drew straight over "Delete *". The title now reserves that region. showOptions moves from an aliased field to a forwarded one: aliases fire no onChange, so a screen setting showOptions after title left the width computed from a stale value -- which is exactly what the first attempt at this fix hit on device. Font downloads stage under the gitignored dist/tmp instead of os.tmpdir() so the moves into fonts/ are same-filesystem renames; tmpdir can be a different mount, where rename fails with EXDEV. Dropped the gh auth status pre-flight gate. It exits 0 even when GH_TOKEN is invalid, so it never caught a real problem, while risking a false negative in environments where only GH_TOKEN is set. The gh api call reports auth failures directly and now names both gh auth login and GH_TOKEN. Co-Authored-By: Claude <noreply@anthropic.com>
…packaging Second round of review feedback on #44. showOptions regressed in the previous commit: converting it from an aliased field to a forwarded one meant screens setting it as an XML attribute never reached the handler with m.overhang resolved, so the options affordance would not appear at all. init() now forwards it once and the handler still covers later writes. Packaging treated filename presence as availability, so a stale or truncated face could still advertise the brand face while its pkg:/fonts URI fell back per label. Availability now comes from inspectBrandFonts(): every pinned face present, digest matching, and no unlisted face that would ship unverified beside them. assertPlayerOsdLayout pinned playerTitle's measured height to 58, which only holds with the faces bundled; the same h1 role measures 46 on a fonts-less package, so the player suite could not pass against both. It accepts either and leaves the overlap assertion to guard the layout. The Lab calibration story now says so on screen when a build has no brand fonts, rather than letting a capture compare the system font against itself and look like a passing A/B. .gitignore gains /fonts/*.ttc so the ignore rules cover every extension checkRokuFontBinaries fails on, with the test and docs updated to match. Co-Authored-By: Claude <noreply@anthropic.com>
d67cf7e to
cef7684
Compare
Roku was the last put.io surface still on the built-in system fonts. The licensed GT America faces are now pinned by sha256 in config/brand-fonts.json, synced at dev time from private putio-static, and never committed to this public repo. A clone without them builds, verifies and runs, falling back to the Roku system font, and pnpm verify fails outright if git ever tracks an otf/ttf/ttc. The type scale is measured rather than guessed. A Lab story renders each Roku built-in beside GT America at the same pixel size and one and two 3px grid steps up; the measured ink extents put GT America within a few percent of the built-in it replaces at matching size (94-95% on cap-height strings, 105-107% on digits), while one step up is 6-9% oversized. Role sizes therefore stay identical to the built-ins, which keeps every Label height, character-count wrap budget and list-row baseline valid. Components now call applyTypography(node, role) beside their existing setDialogNodeColor calls, and an audit fails the build if a font:*SystemFont literal reappears in a product component. Roku's Overhang exposes titleColor but no font field, so ScreenHeader keeps it for the logo, options affordance and background and draws the title itself, landing pixel-identical to the built-in one. Overhang only draws its logo divider alongside its own title, so that is drawn from the border token too. GT America carries no U+2713, so TrackMenu's selected-track checkmark becomes a tinted Phosphor check glyph. The release workflow mints a scoped putio-static read token and syncs the faces before building, so the published ZIP ships GT America; CI stays deliberately fonts-less as the standing proof that the fallback works. Co-Authored-By: Claude <noreply@anthropic.com>
Overhang only drew its logo divider when it had a title to separate from the logo, so replacing it with our own Rectangle left the Home screen showing a divider pointing at nothing. Tie the divider to the title being non-empty, and default it hidden so it cannot flash before init. Home is the only screen without a title; Files and Audio set theirs after init, which renderScreenHeaderTitle already tracks. Co-Authored-By: Claude <noreply@anthropic.com>
…flag Addresses review feedback on #44. Availability was "any .otf/.ttf under fonts/", so a partial directory compiled buildConfigBrandFontsAvailable() to true and left individual roles resolving to missing pkg:/fonts URIs, which Roku renders in the system font per label and shows as mixed typography. It now requires every face pinned in config/brand-fonts.json, and packaging skips the fonts root entirely otherwise so a partial set cannot ship. This is what docs/FONTS.md already claimed; the doc is corrected to state the all-or-nothing rule explicitly. The branded header title reserved no space for the options affordance, so a long title (Files sets it from the folder name) drew straight over "Delete *". The title now reserves that region. showOptions moves from an aliased field to a forwarded one: aliases fire no onChange, so a screen setting showOptions after title left the width computed from a stale value -- which is exactly what the first attempt at this fix hit on device. Font downloads stage under the gitignored dist/tmp instead of os.tmpdir() so the moves into fonts/ are same-filesystem renames; tmpdir can be a different mount, where rename fails with EXDEV. Dropped the gh auth status pre-flight gate. It exits 0 even when GH_TOKEN is invalid, so it never caught a real problem, while risking a false negative in environments where only GH_TOKEN is set. The gh api call reports auth failures directly and now names both gh auth login and GH_TOKEN. Co-Authored-By: Claude <noreply@anthropic.com>
…rding Co-Authored-By: Claude <noreply@anthropic.com>
…packaging Second round of review feedback on #44. showOptions regressed in the previous commit: converting it from an aliased field to a forwarded one meant screens setting it as an XML attribute never reached the handler with m.overhang resolved, so the options affordance would not appear at all. init() now forwards it once and the handler still covers later writes. Packaging treated filename presence as availability, so a stale or truncated face could still advertise the brand face while its pkg:/fonts URI fell back per label. Availability now comes from inspectBrandFonts(): every pinned face present, digest matching, and no unlisted face that would ship unverified beside them. assertPlayerOsdLayout pinned playerTitle's measured height to 58, which only holds with the faces bundled; the same h1 role measures 46 on a fonts-less package, so the player suite could not pass against both. It accepts either and leaves the overlap assertion to guard the layout. The Lab calibration story now says so on screen when a build has no brand fonts, rather than letting a capture compare the system font against itself and look like a passing A/B. .gitignore gains /fonts/*.ttc so the ignore rules cover every extension checkRokuFontBinaries fails on, with the test and docs updated to match. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The story assigned showOptions in BrightScript, which routes through the onChange handler -- the path that already worked. Real screens set it as an XML attribute, and that is the path where the affordance went missing. Declaring the header from markup in a small Lab-only wrapper makes the story cover the case that actually regressed. Co-Authored-By: Claude <noreply@anthropic.com>
"--" is invalid inside an XML comment, and Roku only surfaces it at sideload time as "Install Failure: Error parsing XML component", well after pnpm verify passes. Fix the comment and add a check alongside the existing duplicate-attribute lint so the whole class fails in verify instead. Co-Authored-By: Claude <noreply@anthropic.com>
The ignore rules and checkRokuFontBinaries both treat .ttc as a font binary, but the importer's extension list did not, so a stray .ttc escaped pruning and the unlisted check while still shipping -- packaging bundles the whole fonts/ root. A test now pins the three lists in step. Co-Authored-By: Claude <noreply@anthropic.com>
Review feedback read the .ttc asymmetry as a bug. It is intentional: fontExtensions is what the importer prunes and counts as unlisted inside fonts/, while fontFileNamePattern is what a manifest entry may pin. A .ttc is a collection and Roku's Font.uri takes a single face, so it belongs in the first list and not the second. Spell that out so the next reader does not 'fix' it. Co-Authored-By: Claude <noreply@anthropic.com>
Overhang draws the options affordance only when showOptions and optionsAvailable are both set. The title reserved that gutter whenever showOptions was true, and seven screens keep showOptions with optionsAvailable false and draw nothing there, so their titles gave up 300px for no reason. Audio and Image set the title from a file name, so both truncated early. Reserve on the same condition Overhang draws on. optionsAvailable joins showOptions as a forwarded rather than aliased field for the same reason as before: an initial XML value fires no onChange, so the width would be computed from a stale value. The Lab preview now shows both option states with the same long title, so the difference in truncation point is visible rather than argued. Co-Authored-By: Claude <noreply@anthropic.com>
…se-insensitively Two bypasses in the licensed-font boundary, both found by autoreview. Packaging added the fonts/ directory as a package root. Roots are copied recursively while inspectBrandFonts only scans one level and matched extensions case-sensitively, so with the three pinned faces valid a nested fonts/backup/unlicensed.otf or an EXTRA.OTF was neither reported as unlisted nor kept out of the ZIP. Reproduced: fonts-check reported 3 verified while the package shipped 5 font files. Packaging now lists the manifest files individually, so what ships is exactly what was digest-checked, and isFontFile is case-insensitive so a stray uppercase face is reported as unlisted. checkRokuFontBinaries used lowercase-only git pathspecs, so a tracked GT-America.OTF passed the guard that is supposed to keep font binaries out of this public repo. Reproduced: the lowercase spec matched none, :(icase) matched it. Co-Authored-By: Claude <noreply@anthropic.com>
…comments
Audit gap: the pkg:/fonts reference audit only matched literal URIs, and the
shipping path has none -- brandFontUri composes them. It therefore validated the
Lab probe's three hardcoded strings and nothing that ships, so repinning to renamed
faces would have left all 14 components requesting absent names with the
availability flag still true. The audit now composes the URI the way the runtime
does; proven to fail on a renamed prefix. A second audit rejects applyTypography
roles the scale does not define, since unknown roles resolve to body silently.
Removed the duplicate node id: screens declared ScreenHeader as id=overhang while
its internal Overhang child used the same id, so findNode("overhang") in Files,
Audio, Image and Video was ambiguous and a resolution to the inner node would write
the title onto the deliberately emptied Overhang title.
verifiedBrandFontRoots now returns the faces inspectBrandFonts verified rather than
re-reading the manifest, so what ships and what was checked cannot drift apart.
TrackMenu no longer restates its check Poster geometry in BrightScript.
Comment and doc corrections: the 'packaging bundles the whole fonts/ root'
rationale was stale in three places including AGENTS.md; the 'guaranteed-safe
margin' claim about wrap budgets contradicted the 105-107% digit measurement; the
type-scale percentages are attributed to pixel measurement off the Lab screenshot
rather than to the story computing them; the fonts-less fallback claim now notes
ScreenHeader is the exception; the playerTitle heights are labelled measurements
rather than a derivation that did not reproduce; the staging comment no longer
claims interrupt-safety across renames; the buffer cap sits below the Contents API
limit, not above it; release.yml no longer implies fonts-check can catch a silent
no-op sync; and the .vref auth entry no longer claims a redaction it does not have.
Co-Authored-By: Claude <noreply@anthropic.com>
Nothing tied buildConfigBrandFontsAvailable to what createPackageZip actually emitted, so reverting the font roots would have shipped a build advertising the brand face with no faces in it and every test still green. The assertion holds in both states, so it is meaningful on a fonts-less CI checkout and on a machine with the faces synced; proven to fail when the roots are dropped. Co-Authored-By: Claude <noreply@anthropic.com>
review-gang caught that image.jpg still showed the logo divider with no title -- the exact state 206a194 fixed -- because only home.jpg was re-copied after that commit and the rest of the batch predated it. Image is the one page where it was visible, since every other page carries a title. All eleven page references are now captured from ef3ab99 in one session, so their provenance is consistent, and each satisfies the rule the code enforces: divider drawn only alongside a title (verified per reference). Left as they were, with reasoning rather than churn: search-no-results keeps a non-empty "Search" title so the divider is correct either way, and the title is far shorter than both the reserved and unreserved widths, so the options-gutter change cannot alter its rendering. The five player references have no ScreenHeader at all, and the two Lab component references are unaffected by the header work. Co-Authored-By: Claude <noreply@anthropic.com>
findNode("screenHeader") returns a ScreenHeader component, not a raw Overhang, so
m.overhang in Audio and Image and the local in Files were misleading after the id
rename. No behaviour change.
Co-Authored-By: Claude <noreply@anthropic.com>
autoreview raised the brand face's coverage as a P1 international-text regression: GT America Standard is 523 codepoints with no Cyrillic, Greek, CJK, Arabic, Hebrew, Thai or emoji, file names are user content, and Roku's Font node has no per-glyph fallback. The coverage and the missing fallback are both real. It is not a regression, though. The typography Lab story now renders Cyrillic and Japanese file names in both faces side by side, and on device the Roku system font shows hollow placeholder boxes for exactly the characters where GT America shows crosshatched ones. Non-Latin names were unreadable before this migration and are equally unreadable after it, so the brand face neither causes nor fixes the problem. Documented rather than silently accepted, with the Lab row as standing evidence, and named the real fix -- a coverage-adequate face for user content -- as separate work. Co-Authored-By: Claude <noreply@anthropic.com>
The faces were pinned by sha256 against a commit in private putdotio/putio-static and fetched with `gh api`, which meant every developer needed the GitHub CLI authenticated against a private repo and the release workflow had to mint a scoped App token just to read three files. static.put.io already serves the same desktop OTF cuts over plain HTTPS, so fetch them there instead. The bytes are identical: all three faces still hash to the digests this commit removes, verified before the pins were dropped. What goes away: the gh dependency, the sha256 and commit-ref pinning, the per-file upstream paths, and the putio-static token step in release.yml. The manifest collapses to a baseUrl, the expected family, and a list of filenames. What replaces the digest is a validity check rather than an integrity one, since the bytes now come from our own CDN over TLS. A face is accepted only when it is a single sfnt (not a .ttc collection), every table in its directory lies inside the file, the tables Roku needs to render are present, and its name table declares GT America. That covers what a CDN actually gets wrong: a 200 carrying an error page, a half-finished download, or the wrong typeface under the right filename. The table-bounds check is load-bearing and was not obvious. A truncated face keeps its name table intact often enough that magic bytes and family both pass while the outlines are gone -- the first draft of this accepted the first 5KB of a real face. Only bounds catch it, now down to a single lost byte. Unchanged: nothing licensed enters git, availability stays all-or-nothing so a partial set can never advertise the brand face, packaging still bundles the listed faces individually rather than the fonts/ directory, and a fonts-less clone still verifies green and renders in the system font. docs/FONTS.md also drops a claim that this makes false. It said contributors and CI clones never receive the faces; with an unauthenticated CDN anyone who can read the repo can fetch them. That was already true of the CDN before this change, so the boundary is now stated as what the repo actually controls: the binaries are not in its tree, history, or packages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two findings from cubic on the CDN switch, both valid. A name record claiming an odd number of UTF-16 bytes made Buffer.swap16 throw a RangeError straight out of brandFontRejection, which propagates through inspectBrandFonts into packaging and verify. A corrupt face is supposed to be classified invalid so the build falls back to the Roku system font; instead it aborted the build. Reproduced with a hand-built sfnt before fixing. Malformed UTF-16 records are now skipped, which leaves the face with no readable family -- the rejection it deserves -- and a regression test asserts it does not throw. The download ceiling was also decorative: response.arrayBuffer() materialises the whole body before any size check runs, so a misrouted large object could exhaust the release process before the limit was consulted. The body is now read incrementally and abandoned as soon as it crosses the ceiling. Re-fetched cold afterwards; the streamed bytes still match the digests this branch removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GT America carries no dingbats, and the one place this bit was found by accident: TrackMenu used text="✓" as the only indication of the active audio, subtitle and speed track, so the brand face would have blanked all eight labels. Nothing stopped the next one. Audits product components for characters in the arrow, media-control, box-drawing, geometric, dingbat, supplemental-arrow and emoji blocks, pointing offenders at config/phosphor-icons.json. Reintroducing text="✓" in TrackMenu fails it. Matches those blocks rather than the face's own cmap so it still holds on a fonts-less clone, which is what CI runs. A second case guards the pattern itself: the glyphs this exists to catch must match, and covered characters -- Turkish and Central European letters, en dash, bullet, ellipsis -- must not, or the rule would reject legitimate filenames. Scans .brs/.bs/.xml only. Reading the checked-in PNGs as text turns arbitrary bytes into arbitrary codepoints, and loader.png decodes a U+26A0 that way. Lab is exempt: its typography story deliberately renders uncovered Cyrillic and Japanese to document the limitation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cef7684 to
5268567
Compare

Summary
Roku was the last put.io surface still on Roku's built-in system fonts. This brings the
brand typeface to the app end to end: a pinned, verified font supply chain; a measured type
scale; every component migrated; and the curated visual references re-recorded on the brand
face.
The licensed faces are never committed to this public repo. They are pinned by sha256,
synced at dev time from private
putio-static, andpnpm verifyfails outright if git evertracks an
.otf/.ttf/.ttc. A clone without them builds, verifies and runs — fallingback to the Roku system font — which is exactly what CI does on every PR.
Changed
config/brand-fonts.jsonpins three GT America faces to aputio-staticcommit plus a digest each.scripts/sync-brand-fonts.tsbackspnpm roku fonts-setup/fonts-check: it verifies each download before writing,stages the whole set in a temp dir so an interrupted sync cannot leave a mixed set, and
prunes faces the manifest does not list.
fonts/only when it actually holds facesand compiles the same answer into
source/BuildConfig.brsasbuildConfigBrandFontsAvailable(), so the runtime reads a build-time truth instead ofrendering a missing
pkg:/URI that Roku would silently resolve to the system font. Anybuild without the faces logs that it happened.
components/shared/Typography/Typography.brsowns six roles;components call
applyTypography(node, role)next to their existingsetDialogNodeColorcalls. All 72font:*SystemFontsites across 14 components migrated.OverhangexposestitleColorbut no font field, soScreenHeaderkeeps it for the logo, options affordance and background and draws the title itself.
only signal of the active audio/subtitle/speed track) becomes a tinted Phosphor glyph.
putio-staticread token and syncs beforebuilding, so the published ZIP actually ships GT America. Without this the pipeline would
have quietly published a system-font build.
Review aids
The visual references in this diff are the before/after. All 19 committed
.vref/screenshots/roku-720p/*.jpgare re-recorded on real hardware, so GitHub's imagediff shows every screen side by side. Start with
files.jpg(list rows plus the newheader),
lab-app-dialog-message.jpg(dialog wrapping) andplayer-subtitle-menu.jpg(the new check glyph). The gallery publishes to
roku.put.io/vref/on release.Why the sizes are unchanged. Roku does not document its built-in font sizes, so a Lab
story (
typography-gt-america) renders each built-in beside GT America at the same size andat one and two 3px grid steps up. Measured ink extents from that screenshot:
h1(45)body(36)small(33)caption(27)GT America at matching size is within a few percent; one step up is 6–9% oversized. Role
sizes therefore stay identical to the built-ins, so every Label height, wrap budget and
list-row baseline stays valid and the brand face is a drop-in.
Header placement, measured against the previous reference: title
y 57..77,centerY 67.0— pixel-identical to the built-in title it replaces.Media cannot be uploaded to this description from the authoring environment, hence the
pointer to the committed references rather than inline images.
Risks
1.258em against roughly 1.02em). This surfaced as a real assertion failure —
playerTitle's auto-measured height went 46 → 58 — so that layout contract is updated andthe harness's title/controls overlap assertion confirms no collision. Any Label without an
explicit height is now slightly taller.
so
AppDialog/DeleteFileDialog/ContinueWatchingPromptunder-fill their lines ratherthan overflowing, and because the budgets are counted in characters the wrap and
truncation points are unchanged. Re-tuning them would trade a guaranteed-safe margin for a
clipping risk; better as its own change with proper measurement.
optionsText("Delete", Files only) and theMiniKeyboardkey glyphsstay Roku-styled — the former has no font field, the latter is bitmap art.
BuildConfig flag, the audits need the migration and the references need all of it, so any
split leaves an intermediate that fails
pnpm verify.Verification
pnpm verifygreen with the faces (138 files) and without them (135 files, zerofont entries in the ZIP, fallback logged).
git ls-filesreports no font binaries.pnpm roku fonts-checkexercised in all four states: verified, absent (exit 0, optional),wrong digest (exit 1), unlisted face (exit 1). A real
fonts-setupdownload from privateputio-staticverified against its pin, plus prune and idempotency.live-test-flow-smoke(auth, files, history, dialogs, settings, get-new-code)and
live-test-player-ui-screenshotsboth pass; Lab sweep across every migratedcomponent; all 19 references re-recorded,
visual-validateandvisual-galleryclean.test/live-test/brand-fonts.test.ts: manifest schema and digests, ignore rules,worktree carry, reference validity, the six-role scale, and the no-literals completeness
audit (67 component files scanned).
Complexity
Mechanical across 14 components, with three judgement calls worth a look: keeping the role
sizes identical to the built-ins, taking over only the Overhang title rather than replacing
the node, and leaving the wrap constants untouched.
🤖 Generated with Claude Code
Note
Medium Risk
Wide UI and header refactor across all main screens plus release-time CDN dependency for official builds; mitigated by all-or-nothing bundling, system-font fallback, and contract tests.
Overview
This PR moves the Roku app onto GT America with a full supply chain, shared type scale, and UI migration—while keeping fonts out of git and letting font-less clones still verify and ship with system fonts.
Font pipeline:
config/brand-fonts.jsonpoints atstatic.put.io;pnpm roku fonts-setup/fonts-checkdownload and validate faces (sfnt structure, family name) before they land in gitignoredfonts/. Packaging bundles only manifest-listed files when every face validates, setsbuildConfigBrandFontsAvailable()in generatedBuildConfig.brs, and logs when the ZIP ships without brand fonts. Release runsfonts-setupandfonts-checkbefore semantic-release so published artifacts include GT America; CI stays fonts-less.Typography:
Typography.brsdefines six roles; product components useapplyTypographyinstead offont:*SystemFont.ScreenHeaderkeeps RokuOverhangfor logo/options but draws the title in the brand face. TrackMenu swaps the text checkmark for a Phosphorcheckicon. Visual references are re-captured and taggedgt-america; Lab adds typography and header stories;docs/FONTS.mddocuments the system.Reviewed by Cursor Bugbot for commit d67cf7e. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Render the Roku app in GT America with a validated, all-or-nothing font pipeline and a shared type scale; builds without fonts fall back to Roku system fonts. Branded builds fetch faces from put.io’s CDN (
static.put.io) at dev and release time; the fonts are never committed.New Features
config/brand-fonts.json;pnpm roku fonts-setupstreams and validates faces fromstatic.put.io, andfonts-checkre-verifies; release runs both before packaging.buildConfigBrandFontsAvailable(); fonts-less builds log the fallback.components/shared/Typography/Typography.brs; components callapplyTypography(node, role)instead offont:*SystemFont.ScreenHeaderkeeps RokuOverhangfor logo/options and draws the branded title; Lab adds “Typography / GT America” and an XML‑declared “ScreenHeader / branded title” story; visual refs re‑recorded;docs/FONTS.mdadded.TrackMenureplaces the text checkmark with a tinted Phosphorcheck.Bug Fixes
.otf/.ttf/.ttc; importer owns.ttc; nested/unlisted faces cannot ship; packaging lists only verified filenames.applyTypographyroles rejected; symbols typed as text blocked; XML comments linted for “--”.ScreenHeader: hides the logo divider with no title, forwardsshowOptionsat init, and reserves the options gutter only when the affordance actually draws; duplicateoverhangid removed.h1heights in both font states.Written for commit aaea402. Summary will update on new commits.