Repository navigation
ui: adobe's own font files, visible button edges, and a refresh that keeps contrast - #48
Merged
Merged
Conversation
added 13 commits
September 25, 2026 23:04
Source Sans 3, Source Serif 4 and Source Code Pro were self-hosted as latin subsets cut from Adobe's variable fonts. Under the SIL Open Font License a subset is a Modified Version, and a Modified Version may not carry the Reserved Font Name 'Source' the way these did, both in their name tables and as the CSS font-family. Each font file is now Adobe's own unmodified static WOFF2 build from a tagged GitHub release, byte for byte. NOTICE.md names the release tag each file came from, and the OFL texts are updated to the exact text shipped in each release. A test pins every shipped file to its release's SHA-256, with the release URL in a comment, so a re-subset cannot slip back in. The built stylesheet still references every font by URL rather than inlining one as data, which the font-src CSP depends on.
The outline variant (Refresh, Check now, the copy buttons) used --cm-border, the default border role, which measures about 1.3:1 against the page in the light theme — a plain line, not a visible edge. border-input maps to --cm-border-interactive, the input border role, at about 4:1, and is already the role every text input and select draws its edge in. Only the outline variant changes.
Content reloading in place was drawn at half opacity, alongside the aria-busy that already marked it — dropping muted text to about 2.2:1 and body text to about 3.5:1 in the light theme for as long as the reload took. Every such site (the overview's summary and chart, the domains, links, clicks, sessions and API key lists, and the breakdown panels) now keeps aria-busy and drops the opacity. The visible cue moves to the header's Refresh button instead: it reads Freshness's own load state, the one thing in the header that reloads on every round, and shows 'Refreshing…' and disables itself for the round's duration. A source-rule test refuses opacity-50 anywhere under screens/, so the invariant is stated once rather than per screen.
The Refresh button read Freshness's own status load as a proxy for the whole screen: a slower report load left the button re-enabled while the numbers under it were still on the old window, and a failed status left it disabled forever if that one request never resolved. useLoad now registers every attempt with a shared counter in RefreshContext, begun when it starts and ended exactly once when it settles — success, failure or superseded — whether that attempt came from a Refresh press, a window change, a filter or a search. The button reads the counter, not one particular load, so a 429 on one request no longer clears the flag while another is still going. The disabled state also inherited the button base's disabled:opacity-50, dimming the very cue that replaced the dimmed content #46 removed. It now overrides back to full contrast while Refreshing… is the reason it is disabled.
Report's summary and chart, Settings and API keys all mark their region aria-busy while a reload is in flight, but none of the four had a test for it — aria-busy is the only signal left now that #46 removed the opacity dimming, so an untested site could silently stop marking itself busy. Settings never depended on the refresh round, so Refresh did nothing on that screen and there was no reload to test busy against. It now reloads on a round like every other screen, remounting the form on each one so a background refresh cannot leave stale fields under a fresh answer.
The source rule only refused the literal opacity-50: opacity-60 or any other weight on a busy region passed it, and so did reaching the same dimming through a colour swap instead of opacity — a class that reads text-muted-foreground only while a loading/stale/busy flag is set composites text under the same risk. Nothing under screens/ has a legitimate reason for either, so both are refused outright rather than the one value and the one class that happened to be in use.
The font test asserted only that some woff2 reference existed, so losing one font's @font-face (an accidental edit, a bad merge) would pass it as long as any other font was still linked. It now checks each of the eight file stems by name. The comment above it also had font-src 'self' backwards: that directive does not match data:, so an inlined font is not a policy bypass, it is simply blocked and falls back to nothing. Inlining is still refused — the goal is every font rendering, not merely the policy holding.
Test names and comments still said a busy region dims or is dimmed — true before #46 removed the opacity, stale after. Nothing here changes behaviour.
Settings does not depend on the refresh round again: SettingsBody reads
its answer into local state once and edits it there, and remounting it
on every round (the previous commit's key={round}) threw an in-progress
edit away on a Refresh press, on the 60s focus-return refresh, and
under an open retention-deletion confirmation — none of it saved, none
of it asked about. The counter this branch added still counts
Settings' own initial load regardless; nothing about that needs a
round.
The busy test that depended on the round reload is replaced with one
that reads busy correctly once the settings have loaded, and a new
test that a Refresh round leaves an unsaved edit alone.
Three edges the counter's own tests did not cover: unmounting a load mid-flight when it ignores its abort signal, a superseded attempt's abort rejection releasing its slot a second time on top of the cleanup that already released it, and load() throwing synchronously before ever returning a promise. The third was a real leak, not just an unpinned guard: beginLoad() ran before the call, so a synchronous throw skipped both finish() calls and left the count up forever. load() is now called inside a try that releases it before re-throwing, the same defect it would otherwise still reach React as.
A ternary Prettier wraps across several lines passed the rule, since it
only ever looked at one line at a time. It now matches each
className={…} attribute's whole value instead, so a wrapped ternary is
still caught. The matcher is a one-level brace balancer, not a parser,
so it stays simple: a second level of nested braces inside the
attribute would still close the match early, which the test's own
comment says.
Three revisions of the regex matching a colour swapped in while a load is stale/loading/busy each missed a shape the last one caught or a new one it never covered — a class hoisted into a variable, a && instead of a ?. A colour swap tied to a loading state is a review concern, not a pattern a regex over JSX can state, so it stays one rather than a fourth revision. The opacity rule stays as it is: an exact match, not a heuristic.
Settings has no reload in place, and the region it was on only renders once the data has already arrived — the condition it read was always false by the time anything using it existed. The test that pinned it asserted a constant: setting the attribute to a literal false passed it exactly as the real condition did. Removed both, rather than keep an attribute and a test that state nothing.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This replaces the interface's subset fonts with Adobe's own files, gives outline buttons an edge you can see, and stops reloading content from dimming below contrast. The cue for a reload moves to the Refresh button.
Fonts
The fonts are now Adobe's static WOFF2 files, byte for byte, from Source Sans 3
3.052R, Source Serif 44.005Rand Source Code Pro2.042R. Under the SIL Open Font License a subset is a Modified Version, and a Modified Version may not use a Reserved Font Name. Adobe reserves "Source". An unmodified file is an Original Version, so the name stands. That was forced by the license rather than preferred: the files are larger than the subsets, which matters little for assets served once and cached as immutable. The subsets also carried wrong internal names left by instancing: the 500 weight of Source Sans 3 called itself "ExtraLight Medium". A test pins every shipped font to its release file by SHA-256, and the build-output test asserts every font is referenced, so a later re-subset cannot slip back in. The OFL texts are the ones each release ships, andNOTICE.mdnames the tags.Contrast
Outline buttons are edged in the input border role, which the brand's contrast report measures at about 4:1. Before, they were edged in the plain border role at about 1.3:1, and Refresh, Check now and the copy buttons read as plain text in the light theme.
Reloading content stays at full contrast. It used to drop to half opacity, which took muted text to about 2.2:1 for as long as a reload took. Every panel still sets
aria-busy. The visible cue is the Refresh button, which reads "Refreshing…" while any load on the screen is in flight: a Refresh round, a window change, a filter or a search. It does not track the header's own request alone, because that request finishes long before the report queries do. The label is drawn at full contrast. A source rule refuses anyopacity-class in the screens.The in-flight count is kept by the one data hook every screen uses. It releases exactly once per load, on success, on failure, on abort, on unmount and on a synchronous throw, and each of those paths has its own test.
Closes #45
Closes #46
Verification
The gate ran in CI's order:
pnpm lint,pnpm lint:sh,pnpm typecheckandpnpm build. Thenpnpm testran 65 files and 1,550 tests for the services and 46 files and 590 tests for the interface, and the interface suite passed five runs in a row. The stack suites passed 86 tests across seven files, including the browser suite with no content-security-policy violation. The durability suite passed. The screenshots were read in both themes after each change.Every guard was mutated individually and caught by exactly its own test.