Skip to content

Fill iOS availability menu dots with design-system colors - #9331

Draft
brow wants to merge 2 commits into
mainfrom
opus/filled-presence-dots
Draft

brow wants to merge 2 commits into
mainfrom
opus/filled-presence-dots

Conversation

@brow

@brow brow commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

The iOS native availability menu showed outlined dots because NativeTabler maps circle.fill to Tabler's outline circle glyph (the font has no filled variant). It also used UIKit system colors instead of our theme tokens.

  • Adds context.presenceColor() to the design-system theme extensions (appColors.success / appColors.warning / colors.onSurfaceVariant). It is now used by the channel tile, the settings availability popover, the DM header dot, the native title presence color and the native bridge.
  • NativePresenceMenu draws a solid 10pt dot in the bridged dotColors. Shutter and swatch callers of circle.fill are unchanged.
  • Offline resolves to onSurfaceVariant, not colors.outline: outline is a hairline-border token (#E8E8E8 in the light scheme) and rendered the Offline dot at 1.17:1 on the light menu surface — invisible. onSurfaceVariant (#646464 light / #C7C7C7 dark) matches the profile chip's existing offline treatment.
  • Tests: settings_overlay_test asserts the bridged dotColors match the tokens, including the resolved offline color; new presence_color_test asserts ≥3:1 contrast for all three dots on both the light and dark surface; channels_page_test follows the token change.

Validation at 4ca8ae0a48: flutter analyze clean; presence_color_test, settings_overlay_test, settings_profile_header_test, channels_page_test and channel_detail_page_test all pass; Android settings capture shows filled green/orange/gray dots. iOS verified by the Swift CI target and the native dot drawing, not on a device/simulator yet — needs a human run on iOS before ready.

Not addressed here (optional, unchanged): JumpToLatestGlassButton.swift still routes colorSwatch through NativeTabler.image("circle.fill"), which yields the same outline glyph.

brow commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Reviewed at 2687a56f49 (independent verification: flutter analyze clean, settings_overlay_test and settings_profile_header_test and channels_page_test all pass in a worktree at that SHA).

The core fix is right and I confirm the diagnosis: the bundled Tabler font is outline-only, NativeTabler.glyphs maps "circle.fill" to the outline circle codepoint 0xea6b, so the iOS availability menu drew hollow rings. Drawing the dot natively in NativePresenceMenu instead of reusing NativeTabler is the correct seam.

One blocking finding — the offline dot is invisible in light mode.

context.presenceColor() falls through to colors.outline, which in lightColorScheme is #E8E8E8 (mobile/lib/shared/theme/color_scheme.dart:26). That dot is now painted on a light menu surface (#FAFAFA-ish). Contrast is 1.17:1, i.e. effectively invisible — this is exactly what the shipped build shows for the Offline row, and after this change it stays that way while Online/Away become correct.

outline is a border token (it is what OutlinedButtonTheme and InputDecorationTheme use for hairline borders, app_theme.dart:173/213). It was never meant as a foreground. Two other places already got this right and diverge from the helper: user_profile_sheet.dart:562 uses context.colors.onSurfaceVariant for offline (5.67:1 on the same surface), and channel_detail_page.dart:826 also emits colors.outline for nativeTitlePresenceColor, so the DM header dot has the same defect on the same token.

Suggested fix: map offline to onSurfaceVariant in presenceColor, so the helper is the single source of truth and the two stragglers can adopt it. That also makes the pill and menu agree with the profile chip. Worth adding an assertion on the resolved offline color rather than only on success/warning, since the test as written passes with an invisible dot:

expect(menuContext.presenceColor('offline'),
    Theme.of(menuContext).colorScheme.onSurfaceVariant);

Optional, non-blocking:

  1. NativePresenceMenu is now the only circle.fill consumer in PresenceMenu, but JumpToLatestGlassButton.swift:552/637 still routes colorSwatch through NativeTabler.image("circle.fill") while also drawing swatchView from swatchColor. Given the glyph is outline-only, that call yields a hollow ring behind/around the real swatch. Worth a look in the same pass, or at least a note that it is unchanged and why.
  2. The dot(...) helper hardcodes diameter: 10 with size: 20. Flutter draws 10pt in the popover (settings_profile_header.dart:297), so that matches, but a comment tying 10 to the token rather than a literal would keep it honest if the popover size changes.
  3. Closed PR fix(mobile): render filled status dots on iOS #8550 tried the same fix in NativeTabler itself. That approach was rejected implicitly by the rebase conflict (the file has since gained agentImage), and doing it in the presence view is better scoped — just don't leave NativeTabler still mapping circle.fill to an outline glyph for the next caller to trip on.

Co-authored-by: sol <abf13a3cd01f48aa6017b7531dee849b0b154423d77135f3705ee042319b1721@localhost>
Signed-off-by: Tom Brow <tomb@squareup.com>
@brow
brow force-pushed the opus/filled-presence-dots branch from 2687a56 to e0f4425 Compare November 10, 2026 22:26
`context.presenceColor()` mapped offline to `colors.outline` (#E8E8E8 in
the light scheme), a hairline-border token that reads at 1.17:1 against the
light menu surface, so the Offline dot was effectively invisible. Point it
at `onSurfaceVariant`, the text-tertiary foreground the profile chip
already uses for offline, and adopt the helper in the DM header dot and the
native title presence color so there is one source of truth.

Adds a contrast test for all three dots in both brightnesses, and asserts
the resolved offline token in the settings overlay test so an invisible
dot cannot pass again.

Co-authored-by: deepseek <7ac6f32e110347b0d36a57d9b69ccdc4d933b591d26f076b1e8a92b56ec5b2a5>
Signed-off-by: deepseek <7ac6f32e110347b0d36a57d9b69ccdc4d933b591d26f076b1e8a92b56ec5b2a5@buzz.local>
@brow
brow force-pushed the opus/filled-presence-dots branch from b2fb051 to 4ca8ae0 Compare November 10, 2026 22:53

brow commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the blocking finding at 4ca8ae0a48:

  • context.presenceColor() now maps offline to colors.onSurfaceVariant and the doc comment says why outline must not be used for a dot.
  • Adopted the helper at the two stragglers you named: the Flutter DM header dot (channel_detail_page/app_bar.dart) and nativeTitlePresenceColor (channel_detail_page.dart).
  • Tests: settings_overlay_test now asserts onSurfaceVariant for offline (the assertion you suggested), and presence_color_test asserts ≥3:1 contrast for online/away/offline/unknown against both AppTheme.light() and AppTheme.dark() surfaces, so an invisible dot fails regardless of token name. channels_page_test follows the token.

Verified at that SHA: flutter analyze clean; presence_color_test, settings_overlay_test, settings_profile_header_test, channels_page_test, channel_detail_page_test pass (554 tests). Android settings capture shows filled green/orange/gray dots.

Your optional items: (1) JumpToLatestGlassButton.swift still routes colorSwatch through NativeTabler.image("circle.fill") — left unchanged deliberately, noted in the PR body, since it is a different surface and out of scope for this fix; (2) added no literal-size comment — the 10pt in dot(...) matches the popover's width: 10; (3) NativeTabler's circle.fill → 0xea6b mapping is untouched, which is exactly why this is drawn natively here.

Still needs a human iOS run before ready: the Swift target is green but the native dot has not been seen on a device/simulator in this session.

This branch has not been deployed

No deployments
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