') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat: switch to Google Sans and Google Sans Code via google_fonts by rahulbisht25 · Pull Request #29 · StacDev/flow_ui · GitHub
Skip to content

feat: switch to Google Sans and Google Sans Code via google_fonts - #29

Merged
divyanshub024 merged 6 commits into
mainfrom
theme-and-typography-fixes
Aug 26, 2026
Merged

feat: switch to Google Sans and Google Sans Code via google_fonts#29
divyanshub024 merged 6 commits into
mainfrom
theme-and-typography-fixes

Conversation

@rahulbisht25

@rahulbisht25rahulbisht25 commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

Google Sans becomes the prose face and Google Sans Code the code face, replacing Figtree and Geist Mono across the package, the playground, and the docs site. Neither is bundled: the package now depends on google_fonts, which fetches each cut from Google Fonts on first use and caches it on the device.

Package

  • pubspec.yaml drops the flutter: fonts: block and the fonts/ directory; adds google_fonts ^8.2.1 — the one dependency outside flutter.dev's own packages (CLAUDE.md's constraint updated to record the exception).
  • FlowTypography builds every role through GoogleFonts.googleSans / googleSansCode. Those calls aren't const, so FlowTypography.standard is now a static final; FlowTheme keeps its const constructor by resolving typography through a getter. The public surface (typography: parameter, context.flowTypography) is unchanged.
  • New FlowTypography.recut(style, fontWeight:, fontStyle:). google_fonts registers each weight as its own font family, so copyWith(fontWeight:) on a token would have the engine fake the weight; recut resolves the real cut. The five in-library sites that bolded or italicised tokens (markdown emphasis, error-state titles, the attachment type pill, the bold-text accessibility merge) go through it. Styles in any other face pass through copyWith untouched.

Playground — the chrome's shellText uses GoogleFonts.googleSans directly.

Docs site — the font sync script and its predev/prebuild hooks are removed; astro.config.mjs loads Google Sans via a Google Fonts <link>, and the @font-face blocks in theme.css are gone.

README, CHANGELOG (a Typefaces entry under 0.3.0) and the mdx pages updated. The generated_plugins.cmake files and lockfiles in example/ and playground/ changed because path_provider arrives transitively.

Notes for review

  • Breaking for hosts that referenced Figtree or GeistMono by family name, and hosts that copyWith(fontWeight:) on standard tokens get faux bold until they switch to recut — both called out in the changelog.
  • Offline on first launch, google_fonts logs a debug message and text falls back to the platform font; it doesn't throw. Hosts that need offline-first rendering can ship the files under a google_fonts/ asset folder, which the package checks before the network (documented in the README).
  • The lockfile diffs also carry a few pre-existing minor version refreshes from a local pub get.

Testing

  • flutter analyze clean at the root, in playground/ and in example/; dart format clean tree-wide.
  • Release web build of the playground verified in headless Chrome: five font files fetched from fonts.gstatic.com (HTTP 200), no console errors, prose in Google Sans and the code panel in Google Sans Code — including the w500/w700 chrome cuts.
  • dart pub publish --dry-run validates the archive (383 KB compressed, down from ~8 MB with the font files).

Note

Medium Risk
Adds a third-party dependency and changes default rendering to network-fetched fonts, with breaking typography API behavior for hosts that bold tokens via copyWith; offline-first apps must bundle assets or accept platform fallbacks.

Overview
Replaces bundled Figtree and Geist Mono with Google Sans and Google Sans Code loaded through a new google_fonts dependency. The flutter: fonts: block and shipped font assets are removed, which shrinks the published package and makes runtime network access (or a host-provided google_fonts/ asset folder) required for the design faces on first launch.

FlowTypography.standard is now a static final built with GoogleFonts.googleSans / googleSansCode; FlowTheme keeps a const constructor by resolving typography through a getter. FlowTypography.recut is added so bold/italic use real font cuts instead of copyWith(fontWeight:) (which would faux-bold with google_fonts’ per-weight families); in-library markdown, error state, attachment pills, and accessibility bolding call it.

Docs and playground follow the same branding: Astro loads Google Sans via a stylesheet link (font sync script removed), playground chrome uses GoogleFonts.googleSans, and Android INTERNET / macOS network.client are documented and wired in the playground for release fetches.

Breaking for hosts: references to Figtree / GeistMono by family name, and copyWith(fontWeight:) on standard typography tokens, should move to recut or custom faces.

Reviewed by Cursor Bugbot for commit fd7d2ae. Bugbot is set up for automated code reviews on this repo. Configure here.

@rahulbisht25

Copy link
Copy Markdown
MemberAuthor

@cursoragent review

1 similar comment
@rahulbisht25

Copy link
Copy Markdown
MemberAuthor

@cursoragent review

@cursor

cursorBot commented Aug 25, 2026

Copy link
Copy Markdown

Based on my review of the changes, here's my assessment:

Code Review Summary

Overview

This PR cleanly replaces Figtree and Geist Mono with Google Sans and Google Sans Code as the design system's typefaces. The implementation is well-structured and follows the existing patterns.

Strengths

Centralized font declarations — The font family constants (_fontFamily, _monoFontFamily) are defined once at the top of flow_typography.dart, making the swap minimally invasive. No widgets needed changes.

Proper licensing — Both OFL-GoogleSans.txt and OFL-GoogleSansCode.txt include the correct copyright lines extracted from font metadata, with the full SIL OFL 1.1 text.

Complete documentation — CHANGELOG documents the breaking change for hosts referencing Figtree/GeistMono directly, README updated with the new font names, and CLAUDE.md reflects the bundled assets.

Clean pubspec comment — The rationale for static instances vs variable fonts is documented inline.

Observations

File size (noted in PR) — The four Google Sans files total ~7.6MB vs Figtree's ~280KB. The five Google Sans Code files add ~280KB. The subsetting recommendation in the PR notes is worth addressing before release — pyftsubset to Latin would reduce Google Sans to a few hundred KB.

Playground couplingplayground/lib/src/shell_text.dart hardcodes 'GoogleSans' and 'flow_ui' directly:

 fontFamily:'GoogleSans',
package:'flow_ui',

This works but creates implicit coupling. Consider importing and using the constants from the library if they're ever exposed, or adding a comment noting this matches the library's bundled face.

Verdict

The implementation is solid. The only material decision before shipping is whether to subset the Google Sans files to Latin to avoid the ~7MB payload for every host app. That's a product/release decision rather than a code issue.

Approved — ready to merge once the subsetting decision is made.

Open in WebOpen in Cursor

@rahulbisht25rahulbisht25 changed the title feat: replace Figtree and Geist Mono with Google Sans and Google Sans Codefeat: Google Sans and Google Sans Code via google_fonts, replacing Figtree and Geist MonoAug 25, 2026

@cursorcursorBot 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 default 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 8ce3aae. Configure here.

Comment threadlib/src/theme/flow_typography.dart Outdated
@rahulbisht25rahulbisht25 changed the title feat: Google Sans and Google Sans Code via google_fonts, replacing Figtree and Geist Monofeat: switch to Google Sans and Google Sans Code via google_fontsAug 25, 2026
A style whose fontFamily a host replaced via merge or copyWith still
carries google_fonts' fallback, so matching on fontFamilyFallback re-cut
it in Google Sans; matching the `<family>_<variant>` name lets it pass
through. Also tightens the typeface comments and drops the removed
fonts/ from CLAUDE.md.
@rahulbisht25

Copy link
Copy Markdown
MemberAuthor

@cursoragent review

@cursor

cursorBot commented Aug 25, 2026

Copy link
Copy Markdown

I've reviewed the updated PR. The implementation has been significantly refactored to use google_fonts instead of bundling font files.

Code Review Summary

Overview

The PR now uses google_fonts to fetch Google Sans and Google Sans Code at runtime, reducing the package archive from ~8MB to ~383KB. The recut function has been fixed in the latest commit to properly match google_fonts' family naming convention.

Strengths

Significant size reduction — Package goes from ~8MB (bundled fonts) to ~383KB compressed. Fonts are fetched and cached on first use.

Clean architecture for non-const typographyFlowTypography.standard is now static final since GoogleFonts.* calls aren't const. FlowTheme maintains its const constructor via a private _typography field and getter pattern:

finalFlowTypography? _typography;
/// The type scale — [FlowTypography.standard] unless the host set one. /// Resolved on read, since the standard scale isn't `const`.FlowTypographyget typography => _typography ??FlowTypography.standard;

recut function works correctly — The latest commit (04559b2) fixes the Bugbot-reported issue. The function now checks family.startsWith('${_fontFamily}_') which correctly matches google_fonts' naming pattern (GoogleSans_400, GoogleSans_600, etc.):

final family = style.fontFamily ??'';
if (family.startsWith('${_fontFamily}_')) {
returnGoogleFonts.googleSans(
textStyle: style,
fontWeight: fontWeight,
fontStyle: fontStyle,
);
}

Comprehensive usage of recut — All five in-library sites that need weight/style changes use it: markdown emphasis (bold/italic), error state titles, attachment type pills, retry pills, and bold-text accessibility merges.

Documentation updated — CHANGELOG, README, and CLAUDE.md all document the change to google_fonts, the offline-first asset folder option, and the breaking change for copyWith(fontWeight:) on standard tokens.

Observations

Runtime dependency trade-off — First-launch offline shows platform fonts until google_fonts caches the cuts. The README documents the google_fonts/ asset folder workaround for offline-first apps.

One added dependencygoogle_fonts ^8.2.1 is the first non-flutter.dev dependency. CLAUDE.md correctly records this exception to the "no third-party dependencies" constraint.

Verdict

The implementation is solid and the Bugbot-reported issue has been addressed. The trade-off between bundled fonts (~8MB payload for every host) versus runtime-fetched fonts (offline-first requires asset workaround) is documented and reasonable.

Approved — ready to merge.

Open in WebOpen in Cursor

Comment threadlib/src/theme/flow_theme.dart
Comment threadREADME.md Outdated
Comment threadplayground/pubspec.yaml
Its macOS entitlements lacked network.client and its main Android
manifest INTERNET, so sandboxed macOS and release Android builds fell
back to the platform face.
@divyanshub024
divyanshub024 merged commit 9913cfd into mainAug 26, 2026
5 checks passed
@divyanshub024
divyanshub024 deleted the theme-and-typography-fixes branch August 26, 2026 17:58
divyanshub024 added a commit that referenced this pull request Aug 29, 2026
The site loads Google Sans from Google Fonts since #29, which deleted
the script that copied these out of the package but not the copies it
had already made. Nothing references them; the OFL goes with them.
divyanshub024 added a commit that referenced this pull request Aug 29, 2026
…#31)
* feat: built-in attachment input with picker, web drag-and-drop, and paste
The composer picks its own files now: onAttachmentsPicked opens the
platform dialog through file_selector (flutter.dev-published, no host
configuration except the macOS file-read entitlement), and hands back
FlowAttachments already read, decoded, and carrying their bytes and MIME
type for upload. FlowChatView.onAttachmentsDropped and
FlowComposer.onAttachmentsDropped take web drag-and-drop at two scopes,
with a frosted gradient invitation on the surface and a lit card on the
composer; onAttachmentsPasted takes Ctrl+V images. All three decode
through FlowAttachmentOptions and report refusals through
onAttachmentRejected. onAttach stays as the host-picks escape hatch and
dropActive as the desktop override, since drop and paste have no SDK
path off the web.
Closes#25
* feat: image parts with a generating shimmer state
FlowImagePart renders an AI-generated picture as large-format content,
unlike FlowAttachmentPart's tiles: a null image draws a shimmering block
at the part's aspect ratio until the host re-renders with the provider
set, and tapping the landed picture opens the full-screen preview. The
sweep animation moves out of FlowShimmerText into FlowShimmerSweep so
both surfaces share it. Adding a subtype to the sealed FlowMessagePart
is source-breaking for exhaustive switches downstream; called out in the
changelog.
* fix: judge UTI accept groups and give a single slot to the first passing file
A group naming only uniform type identifiers used to defer to blanket
acceptance, which let any file through an accept list that also had
checkable groups. UTIs are now judged through a root-type table
(public.image, public.movie, com.adobe.pdf and the like), and deferral
is left only for identifiers the table does not know.
With allowMultiple false the intake took the first file offered, so a
refused one ahead of an acceptable one consumed the slot and, on paste,
suppressed the browser's own paste with nothing attached. The slot now
goes to the first file that passes, with the refused ones still
reported.
* feat: lift a user turn's images out of the bubble into tiles above it
Attachments in a user turn now sit above the bubble in a row from the
trailing edge, each image a 116 square tile, cover-cropped, under an
outlineVariant hairline that strengthens to outline on hover; files keep
their pill tiles. A picture with no caption draws no bubble at all, and
assistant turns are unchanged. FlowMessageStyle gains attachmentCardColor,
attachmentCardBorderColor and attachmentCardHoverBorderColor. The
playground's Message stage gains a With image variant.
* feat: close the attachment preview on a tap outside the picture
Two tap targets, one inside the other: a tap on the picture is absorbed
and a tap anywhere else on the frosted page closes, as the close button
and Escape already did. The image is centred inside the viewer so its box
is the painted picture rather than the whole viewport, which is what
makes the two places distinct.
* feat: generate pictures with Gemini's image model in the example
streamReply yields typed deltas, text fragments or a whole picture, and
asks for image output only on the image models, since a text model
refuses the request. The screen offers Gemini 3.1 Flash Image in the
selector and renders the reply through FlowImagePart: a generating block
from the first delta, the picture at its measured aspect ratio once the
bytes land, the slot dropped if the model answers in words alone.
* fix: address review findings on the attachment and image work
- public.audiovisual-content is Apple's parent for audio as well as
video, so the UTI table maps each identifier to a list of MIME
prefixes and that one accepts both; an audio file dropped against a
group naming it was refused as unsupportedType.
- A lifted sent image drew previewImage, which for the package's own
attachments is bounded for the full-screen viewer, so a 116 tile
decoded the picture at 2048. It now draws the plain thumbnail wrapped
at tile resolution, with the same guard the attachment tiles use.
- A disabled composer now refuses drops as it already refused picks,
pastes and removals; before, a drag still lit the card and delivered.
- FlowImagePart gains bytes and mimeType, the pair FlowAttachment
carries. Without them the example dropped generated pictures from the
history: a follow-up turn sent only the text, and an image-only turn
was skipped entirely.
- A measurement landing after the stream failed replaced the turn's
parts and dropped the error card's message; it now stands down once
the turn has failed.
- The install docs claimed nothing outside flutter.dev and no Android
permission. google_fonts is a dependency and its fetch needs INTERNET
and macOS network.client on every app.
* refactor: rest a sent image tile on outline, softening to outlineVariant on hover
The rest ink now matches the pending strip's tiles, so a picture keeps
the same hairline before and after it is sent.
* feat: open the file picker from any control, and fix the attachment rough edges
showFlowAttachmentPicker is public: the function behind the composer's
attach button, callable from a host's own menu. It never throws — a
dialog that cannot open is reported as unreadable under an empty name —
and must be called synchronously from the gesture on the web. The
playground and the example route 'Add Files or Photos' through it and
drop the separate paperclip; the paperclip API stays.
Picked, dropped and pasted images now carry their type as kind, so the
tile draws the JPG pill the design shows. The composer's pending strip
scrolls its tiles under the card's inset, so an overflowing strip cuts
the last tile at the edge as the cue that there is more — no counter.
The preview's close button sits on an opaque surfaceBright disc with an
outlineVariant hairline and the theme's shadow, and grows to 44 on touch
platforms; the translucent wash it had vanished over a dark photo in the
light theme and a light one in the dark.
Refused files are announced by a host-side notice that fades after four
seconds, in the playground and the example; the composer stage gets one
too. The pinned drop treatment moves from Full Chat to a third
Attachments variant, with the docs embed following.
* chore: drop the Figtree files the docs site no longer serves
The site loads Google Sans from Google Fonts since #29, which deleted
the script that copied these out of the package but not the copies it
had already made. Nothing references them; the OFL goes with them.
* feat: composer error banner, and the drop and preview frost from the design
FlowComposer.errorMessage raises the design's tab above the card: the
error wash with a warning glyph and the host's line, left-aligned,
wrapping when long, growing in from the card's edge, and staying until
its cross (onErrorDismiss) or the host clears it. errorIcon swaps the
drawn glyph; FlowComposerStyle.errorBackgroundColor and
errorForegroundColor recolour it. The playground and the example use it
for refused files in place of a host-side notice that faded on a timer.
The drop treatment takes the design's values: a vertical wash from
surfaceBright at 40% to surface at 80% over a 12 blur, replacing the
72-94% surface frost; dropBlurSigma goes with it, unreleased and with
nothing left to control. The attachment preview's backdrop adopts the
same frost so the two read as one.
Picked, dropped and pasted images no longer carry a kind pill; files
without a picture keep theirs. The example seeds an image model's turn
with the generating block so the shimmer shows before the bytes land,
rather than the thinking indicator.
* feat: attachmentsEnabled, to turn every way in off without unwiring it
Presence of a callback says a way in is wired; this flag on FlowComposer
and FlowChatView says it is available — off for a plan tier, a model
that takes no images, a thread that has closed. False stops the attach
button, both drops, paste and keyboard media at once, leaves the
handlers in place, and keeps pending attachments in the strip and
removable, since they are the host's state.
* fix: a drop target that is off stays registered and swallows the drop
attachmentsEnabled false, and the composer's enabled false, passed a
null onDropped into FlowDropTarget, which unregistered the web listener
— the only thing calling preventDefault inside the surface. A file
dropped on a chat that had merely switched attachments off then let the
browser navigate the tab to it. The target now takes an enabled flag of
its own: off keeps the zone registered, raises no treatment, delivers
nothing, and shows the browser's refusing cursor instead of promising a
copy.
* fix: a switched-off drop target yields to an enabled one, and light error ink clears AA
_targetAt picked the smallest rectangle under the pointer regardless of
whether it was on, so a composer with attachments off shadowed a chat
view that still took drops: refusing cursor, no treatment, nothing
delivered. It now prefers the innermost enabled target and falls back to
a disabled one only where nothing enabled contains the point — there,
claiming the drop to swallow it still beats the browser navigating.
The light preset's onErrorContainer is #B54141, a darker cut of the
error accent: #C14A4A on its own 6% wash over surface came to 4.2:1,
under WCAG AA at label sizes, so the composer's error banner and a
failed user bubble were hard to read. The new ink clears 4.8:1 and still
reads as the same red; the dark preset was already clear.
* revert: keep the light error ink at the accent
Puts onErrorContainer back to #C14A4A in the light preset, as the retune
set it, and drops the changelog and CLAUDE.md notes that described the
darker cut. The banner's 4.2:1 on its wash stands as a palette decision
rather than a code fix.
Sign up for freeto 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

@rahulbisht25@divyanshub024