Skip to content

feat(ai): theme the composer and the suggestion chips - #13

Merged
renefloor merged 4 commits into
mainfrom
claude/component-theming
Sep 23, 2026
Merged

renefloor merged 4 commits into
mainfrom
claude/component-theming

Conversation

@renefloor

@renefloor renefloor commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Closes the last open item on the parity roadmap (2.5). PR #12 deletes that file as part of the 0.1.0 release; this is the work it was still tracking.

The gap

The chart theming PR (#10) built the AITheme machinery but wired exactly one component into it. Everything else resolved its colors straight from the ambient ColorScheme, with no seam for a host to intervene — the input pill and its border, the leading "+" button, the send/stop/mic fills, the selected-option chip, the attachment sheet's tiles and photo-selection ring, and the suggestion chips.

What this adds

  • ComposerThemeData — 14 fields covering ChatComposer and ComposerAttachmentSheet. The photo grid's ring and badge are photoSelectionColor, not selectionColor, which would read as TextSelectionThemeData's text highlight inside a composer that holds a TextField.
  • SuggestionsThemeData — 3 fields for AISuggestionsView.
  • ComposerTheme / SuggestionsTheme — subtree overrides, mirroring ChartTheme.
  • All three theme datas (chart included) are now Diagnosticable, so a set field shows up in DevTools.

Both are InheritedThemes, so a scope above the composer carries across the Navigator to the attachment sheet showModalBottomSheet pushes. That is the same reason ChartTheme is one, and there's a test pinning it — return child from wrap and nothing else in the suite would notice while every sheet silently lost the host's theme.

Every field is nullable, and unset means "derive from the ambient ThemeData". Registering nothing renders exactly as before: the 471 pre-existing tests, goldens included, pass untouched, which is the roadmap's acceptance criterion. 81 new tests bring the suite to 552. They check every field in the widget that draws it, using finders aimed at that widget: a scan for "any box with this colour" would pass on the "+" button alone, because the pill shares its fill.

Two resolution choices worth a look

  • disabledIconColor derives from the resolved iconColor, not from the scheme's. A host that sets only iconColor gets a matching disabled shade for free, rather than a dimmed onSurface that no longer relates to the color they picked. The derived shade scales iconColor's alpha to 30% rather than replacing it, so a translucent iconColor never ends up with a disabled shade more opaque than itself.
  • A thumbnail's broken-image glyph follows a host iconColor, but when unset it keeps the quieter onSurfaceVariant it has always had, not iconColor's onSurface default. That keeps the "renders exactly as before" claim true on a path the goldens don't render.
  • hintStyle is merged over the derived color, and the color is restored when a host's inherit: false style would otherwise leave the hint with none.
  • Each glyph is paired with the surface it sits on. iconColor covers only the icons drawn on fillColor (the "+" and the camera tile). A thumbnail's "×" and broken-image glyph take attachmentPlaceholderForegroundColor, paired with attachmentPlaceholderColor. The sheet's chat-option icons follow ColorScheme.onSurface, because the sheet background under them isn't themed. Overriding one pair never leaves an icon on a surface its colour wasn't chosen for.
  • The suggestion chips keep onSurface under an inherit: false textStyle, the same way the composer's hint keeps its colour.
  • ComposerActionButton takes an optional foregroundColor next to color. When unset, it falls back to the theme.
  • The idle microphone takes sendButtonColor, not recordingButtonColor. It occupies the send button's slot and the two are meant to read as one control morphing in place; recordingButtonColor applies only while it is listening. Both are documented on the fields.

Deliberately out of scope

CodeBlockView keeps its two constructor parameters and stays independent of the ambient Theme. The roadmap flagged this as a real question rather than a mechanical port, and the answer is no: a codeTheme that a ThemeData change could reach would undo, on purpose, the property that code reads as code. AITypingIndicatorView's dot color, count and size are already constructor parameters.

Refactor

The null-aware lerp helpers (null means "derive", so a field only one side sets swaps at the midpoint rather than fading through transparency) were private to ChartThemeData. Extracted to src/theme/theme_lerp.dart rather than triplicated; ChartThemeData and ResolvedChartTheme now point at the shared copies. Pure move, no behavior change. mergeTextStyle's comment claimed that TextStyle.merge asserts on inherit: false. It doesn't, so the helper is now just base.merge.

Note on merge order

Branched from main, so this carries the pre-release README and CHANGELOG. The changelog entry sits under a new ## Unreleased heading, and #12 rewrites that file wholesale — expect a small conflict there if #12 lands first, resolved by keeping ## Unreleased above ## 0.1.0.

🤖 Generated with Claude Code

renefloor and others added 3 commits September 22, 2026 15:45
Closes the last open roadmap item. 2.4 built the AITheme machinery but
wired one component into it; every other widget resolved its colors
straight from the ambient ColorScheme with no seam for a host to
intervene.

Adds ComposerThemeData (13 fields, covering ChatComposer and
ComposerAttachmentSheet) and SuggestionsThemeData (3), alongside the
existing ChartThemeData on the AITheme extension, plus ComposerTheme and
SuggestionsTheme for subtree overrides. Both are InheritedThemes, so a
scope above the composer carries across the Navigator to the attachment
sheet that showModalBottomSheet pushes — the same reason ChartTheme is
one.

Every field is nullable and unset means "derive from the ambient
ThemeData", so registering nothing renders exactly as before; the 471
existing tests, goldens included, pass untouched.

Two resolution choices worth naming. disabledIconColor derives from the
*resolved* iconColor rather than the scheme's, so a host that sets only
iconColor gets a matching disabled shade instead of a dimmed onSurface
that no longer relates to it. And the idle microphone takes
sendButtonColor, not recordingButtonColor: it occupies the send button's
slot and the two read as one control morphing in place.

CodeBlockView is deliberately left out. Its two colors are already
constructor parameters and deliberately independent of the ambient Theme
— a codeTheme a ThemeData change could reach would undo that on purpose.

Extracts the null-aware lerp helpers into src/theme/theme_lerp.dart
rather than triplicating them, and points ChartThemeData and
ResolvedChartTheme at the shared copies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ResolvedComposerTheme, ResolvedChartTheme, LruCache and the theme_lerp
helpers were documented as internal but only by convention: nothing stopped
the barrel from exporting them. With @internal, an accidental export is an
invalid_export_of_internal_element warning, which --fatal-infos fails.

These are exactly the lib/src members the barrel doesn't export, so the
annotation now matches reality rather than changing it. Tests inside the
package can still import them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keep the broken-thumbnail glyph on onSurfaceVariant by default, so an
  unthemed composer really renders as before; a host iconColor still
  reaches it.
- Rename ComposerThemeData.selectionColor to photoSelectionColor, which
  no longer reads as TextSelectionThemeData's text highlight.
- Resolve a hint color even under an inherit: false hintStyle, and scale
  iconColor's alpha for the derived disabled shade instead of replacing it.
- Make the three theme datas Diagnosticable.
- Correct field docs that missed surfaces they color, and the
  mergeTextStyle rationale, which misdescribed TextStyle.merge.
- Test AITheme's composer/suggestions wiring, lerp field by field, and
  each field in the widget that draws it (pill, mic, attachment sheet,
  thumbnails, hint), with targeted finders rather than colour scans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- iconColor now covers only the icons drawn on fillColor (the "+" and the
  camera tile). A thumbnail's remove "×" and broken-image glyph take a new
  attachmentPlaceholderForegroundColor, paired with the placeholder they
  sit on; the sheet's chat-option icons follow ColorScheme.onSurface, as
  the sheet background under them is unthemed. Defaults match main.
- ComposerActionButton takes an optional foregroundColor alongside color,
  falling back to the theme's actionButtonForegroundColor.
- AISuggestionsView restores onSurface when an inherit: false textStyle
  would leave the chip text with no color, as the composer does for its
  hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@renefloor
renefloor merged commit 3ab706a into main Sep 23, 2026
9 of 10 checks passed
@renefloor
renefloor deleted the claude/component-theming branch September 23, 2026 12:39
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