Skip to content

feat(typography): render the app in GT America - #44

Open
altaywtf wants to merge 21 commits into
mainfrom
gt-america-typography
Open

feat(typography): render the app in GT America#44
altaywtf wants to merge 21 commits into
mainfrom
gt-america-typography

Conversation

@altaywtf

@altaywtf altaywtf commented Jul 25, 2026

Copy link
Copy Markdown
Member

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, and pnpm verify fails outright if git ever
tracks an .otf/.ttf/.ttc. A clone without them builds, verifies and runs — falling
back to the Roku system font — which is exactly what CI does on every PR.

Changed

  • Supply chainconfig/brand-fonts.json pins three GT America faces to a
    putio-static commit plus a digest each. scripts/sync-brand-fonts.ts backs
    pnpm 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.
  • Fallback made explicit — packaging bundles fonts/ only when it actually holds faces
    and compiles the same answer into source/BuildConfig.brs as
    buildConfigBrandFontsAvailable(), so the runtime reads a build-time truth instead of
    rendering a missing pkg:/ URI that Roku would silently resolve to the system font. Any
    build without the faces logs that it happened.
  • Type scalecomponents/shared/Typography/Typography.brs owns six roles;
    components call applyTypography(node, role) next to their existing
    setDialogNodeColor calls. All 72 font:*SystemFont sites across 14 components migrated.
  • Screen headerOverhang exposes titleColor but no font field, so ScreenHeader
    keeps it for the logo, options affordance and background and draws the title itself.
  • TrackMenu checkmark — GT America has no U+2713, so the selected-track indicator (the
    only signal of the active audio/subtitle/speed track) becomes a tinted Phosphor glyph.
  • Release — the release job mints a scoped putio-static read token and syncs before
    building, 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/*.jpg are re-recorded on real hardware, so GitHub's image
diff shows every screen side by side. Start with files.jpg (list rows plus the new
header), lab-app-dialog-message.jpg (dialog wrapping) and player-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 and
at one and two 3px grid steps up. Measured ink extents from that screenshot:

Role Built-in GT @ same size GT +3px GT +6px
h1 (45) 33px 32px (95%) 35px 36px
body (36) 27px 26px (94%) 27px 30px
small (33) 29px 30px (105%) 33px 35px
caption (27) 24px 26px (106%) 27px 30px

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

  • GT America's line box is ~26% taller than the system font's (hhea ascent+descent
    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 and
    the harness's title/controls overlap assertion confirms no collision. Any Label without an
    explicit height is now slightly taller.
  • Character-count wrap constants are deliberately left alone. GT America is narrower,
    so AppDialog/DeleteFileDialog/ContinueWatchingPrompt under-fill their lines rather
    than 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.
  • Residual seam: optionsText ("Delete", Files only) and the MiniKeyboard key glyphs
    stay Roku-styled — the former has no font field, the latter is bitmap art.
  • Single commit by design: the components need the scale, the scale needs the compiled
    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 verify green with the faces (138 files) and without them (135 files, zero
    font entries in the ZIP, fallback logged). git ls-files reports no font binaries.
  • pnpm roku fonts-check exercised in all four states: verified, absent (exit 0, optional),
    wrong digest (exit 1), unlisted face (exit 1). A real fonts-setup download from private
    putio-static verified against its pin, plus prune and idempotency.
  • On device: live-test-flow-smoke (auth, files, history, dialogs, settings, get-new-code)
    and live-test-player-ui-screenshots both pass; Lab sweep across every migrated
    component; all 19 references re-recorded, visual-validate and visual-gallery clean.
  • New 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.json points at static.put.io; pnpm roku fonts-setup / fonts-check download and validate faces (sfnt structure, family name) before they land in gitignored fonts/. Packaging bundles only manifest-listed files when every face validates, sets buildConfigBrandFontsAvailable() in generated BuildConfig.brs, and logs when the ZIP ships without brand fonts. Release runs fonts-setup and fonts-check before semantic-release so published artifacts include GT America; CI stays fonts-less.

Typography: Typography.brs defines six roles; product components use applyTypography instead of font:*SystemFont. ScreenHeader keeps Roku Overhang for logo/options but draws the title in the brand face. TrackMenu swaps the text checkmark for a Phosphor check icon. Visual references are re-captured and tagged gt-america; Lab adds typography and header stories; docs/FONTS.md documents 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

    • Brand fonts manifest in config/brand-fonts.json; pnpm roku fonts-setup streams and validates faces from static.put.io, and fonts-check re-verifies; release runs both before packaging.
    • Packaging ships only the verified files and compiles buildConfigBrandFontsAvailable(); fonts-less builds log the fallback.
    • Centralized type scale in components/shared/Typography/Typography.brs; components call applyTypography(node, role) instead of font:*SystemFont.
    • ScreenHeader keeps Roku Overhang for 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.md added.
    • TrackMenu replaces the text checkmark with a tinted Phosphor check.
  • Bug Fixes

    • All‑or‑nothing availability: every pinned face must validate or none ship; case‑insensitive .otf/.ttf/.ttc; importer owns .ttc; nested/unlisted faces cannot ship; packaging lists only verified filenames.
    • Importer robustness: streamed downloads with a size cap; sfnt table bounds/family checks; malformed UTF‑16 name records skipped.
    • Build/tests: compiled font flag matches ZIP payload; brand‑font URI audit composes paths like runtime; unknown applyTypography roles rejected; symbols typed as text blocked; XML comments linted for “--”.
    • ScreenHeader: hides the logo divider with no title, forwards showOptions at init, and reserves the options gutter only when the affordance actually draws; duplicate overhang id removed.
    • Player layout tests accept measured h1 heights in both font states.

Written for commit aaea402. Summary will update on new commits.

Copilot AI review requested due to automatic review settings July 25, 2026 06:00
Comment thread scripts/package-roku.ts
Comment thread components/shared/ScreenHeader/ScreenHeader.brs Outdated
Comment thread scripts/sync-brand-fonts.ts

Copilot AI 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.

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 from font:*SystemFont literals, including a new ScreenHeader to 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 .vref references 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.

Comment thread scripts/package-roku.ts Outdated
@altaywtf

Copy link
Copy Markdown
Member Author

Fixed in 206a194: the header divider now only draws when there is a title.

Replacing Overhang's built-in divider with our own Rectangle missed that Overhang only drew it alongside a title — so Home, the one screen with no title, ended up with a divider pointing at nothing. It is now tied to the title being non-empty and defaults hidden so it cannot flash before init.

Verified per screen on device: Home shows the logo alone; Files, Settings, Search and History keep their divider. home.jpg is the only visual reference that changed.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread scripts/sync-brand-fonts.ts Outdated
Comment thread scripts/package-roku.ts Outdated
Comment thread components/lab/Lab.brs
Comment thread components/screens/Home/Home.xml Outdated
Comment thread components/shared/ScreenHeader/ScreenHeader.brs Outdated
Comment thread .gitignore
Comment thread scripts/live-test/scenegraph.ts Outdated
altaywtf added a commit that referenced this pull request Jul 25, 2026
…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>
@altaywtf

Copy link
Copy Markdown
Member Author

Worked through all four review findings — three were real, one I'm pushing back on. Fixed in 9edd0b4 and 234d2ce.

1. Partial fonts/ enabled the brand flag — real (Cursor, Copilot, cubic)

Correct, and worse than a latent risk: docs/FONTS.md already claimed the runtime never renders a missing pkg:/fonts URI, and the any-font check did not deliver that. Availability now requires every face pinned in config/brand-fonts.json, and packaging skips the fonts root entirely otherwise so a partial set cannot ship. Doc corrected to state the all-or-nothing rule.

Verified end to end:

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).

Comment thread components/shared/ScreenHeader/ScreenHeader.brs
Comment thread scripts/package-roku.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

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

Comment thread components/shared/ScreenHeader/ScreenHeader.xml
Comment thread scripts/package-roku.ts Outdated
altaywtf added a commit that referenced this pull request Jul 25, 2026
…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>
@altaywtf

Copy link
Copy Markdown
Member Author

Second round addressed in 30b895a. Six findings; five real, one I'm rejecting with evidence.

P1 — showOptions not forwarded at init (Cursor + cubic)

Real, and a regression from my own previous fix. Converting showOptions from an aliased field to a forwarded one meant screens setting it as an XML attribute never reached the handler with m.overhang resolved — my own if m.overhang <> invalid guard would have swallowed it silently. Net effect: the options affordance would not appear on any screen. init() now forwards it once; the handler still covers later writes.

That's twice this component has been wrong in a way that looked fine in the diff. Worth a careful eye on it in review.

Packaging ignored digests (Cursor + cubic)

Real. Filename presence isn't availability — a stale or truncated face advertised the brand face while its URI fell back per label, and pnpm verify skips fonts-check. Availability now comes from inspectBrandFonts(): all pinned faces present, digests matching, no unlisted face. Verified by appending one byte to a face → flag false, zero bundled. Tests mint their own manifest so the digests are real and no repo fonts/ is needed.

playerTitle height assumed fonts (Cursor)

Real, and my fault for hardcoding 58. The same h1 role measures 46 on a fonts-less package, so the suite couldn't pass against both. It accepts either, with the overlap assertion still guarding the layout.

Lab story could validate the wrong face (cubic)

Real. A fonts-less Lab build rendered the GT columns in the system font, so a calibration capture would compare a face against itself and look like a pass. The story now shows an explicit warning when buildConfigBrandFontsAvailable() is false.

.gitignore missing *.ttc (cubic)

Correct — checkRokuFontBinaries fails on .ttc but the ignore rules didn't cover it. Added, with the test asserting all three so the rules and the gate agree.

Home title — rejecting this one

cubic says the old <Overhang> defaulted to the channel name "put.io" and Home is now missing a title. It didn't. Measuring the pre-change committed reference (d843d59~1:.vref/screenshots/roku-720p/home.jpg), the region right of the logo is empty — no title, no divider. Current behaviour matches the original exactly, which is also why home.jpg is the only reference that changed when I fixed the dangling divider.


pnpm verify green with faces (138 files) and without (135, zero font entries).

One caveat: my Roku went offline mid-session, so the showOptions fix is reasoned from SceneGraph init semantics and my own guard rather than confirmed on hardware. I'll re-run live-test-flow-smoke and re-check the Files header once the device is back — worth holding merge for that, since it's the second issue in this component.

Comment thread scripts/sync-brand-fonts.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

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

Comment thread scripts/sync-brand-fonts.ts
Comment thread scripts/sync-brand-fonts.ts Outdated
Comment thread components/shared/ScreenHeader/ScreenHeader.brs

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread components/shared/AppDialog/AppDialog.xml

@cubic-dev-ai cubic-dev-ai 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.

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

Comment thread components/screens/Settings/Settings.xml
Comment thread components/screens/Files/Files.brs Outdated

@cubic-dev-ai cubic-dev-ai 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.

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

Comment thread scripts/sync-brand-fonts.ts Outdated
Comment thread scripts/sync-brand-fonts.ts Outdated
altaywtf added a commit that referenced this pull request Jul 30, 2026
…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>
altaywtf added a commit that referenced this pull request Jul 30, 2026
…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>
@altaywtf
altaywtf force-pushed the gt-america-typography branch from d67cf7e to cef7684 Compare July 30, 2026 18:26
altaywtf and others added 8 commits August 3, 2026 14:57
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>
altaywtf and others added 12 commits August 3, 2026 14:57
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>
@altaywtf
altaywtf force-pushed the gt-america-typography branch from cef7684 to 5268567 Compare August 3, 2026 11:58
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