refactor: adopt Material3 primitives for surface containers - #28
Conversation
Replace manual clip+background+border chains with Material3 Surface so cards and bubbles pick up shape, color, and border semantics from the component instead of raw modifier composition.
Replace the hand-rolled Dialog with AlertDialog so the attachment picker gets Material3 semantics for title, message, confirm, and dismiss slots plus the themed container shape and color.
Replace the Box with clip+background+border using OutlinedCard so card chrome inherits Material3 outline card semantics and a ColumnScope content receiver.
Replace the wrapped TextField inside a background+border Row with OutlinedTextField so the field carries its own container, border, and leading icon semantics.
Replace the clip+background+border Row with Surface so the voice recording composer inherits Material3 shape, color, and border semantics.
bengidev
left a comment
There was a problem hiding this comment.
Review: verified — no blocking issues
I built and tested the branch locally:
./gradlew :app:testDebugUnitTest→ 438 tests, 0 failures, 0 errors ✅:app:compileDebugAndroidTestKotlinfails, but the errors are inSidePanelPresenterTest.kt(untouched by this PR) and reproduce identically onmain— pre-existing, not a regression.- AGP 9.3.1 ↔ Gradle 9.5.0 is the correct pairing (AGP 9.3 requires Gradle ≥ 9.5), and
9.3.1is published.
I cross-checked each conversion against the M3 1.4.0 source (Surface, Card/OutlinedCard, AlertDialog/AlertDialogFlowRow, contentColorFor). Shape/color/border/padding are preserved; CardChrome's content receiver change (() -> Unit → ColumnScope.() -> Unit) is a compatible widening and the sole caller compiles.
Behavioral differences to be aware of (non-blocking)
-
SidePanelSettingContentAPI key field is ~24dp shorter. Old: 12dp-paddedRowaround a 56dp TextField ≈ 80dp tall. New: bareOutlinedTextFieldat M3's 56dp container height. If the taller field was intentional, addModifier.height(...)/padding back. Also note M3's focused border is 2dp (was 1dp always) — and since focused/unfocused border colors are identical here, focus now only shows as a thicker stroke rather than a color change. -
ComposerAttachmentMenuDialogbuttons now flow-wrap.AlertDialogusesAlertDialogFlowRow(not aRow): if Cancel + both option buttons don't fit on one line, confirm actions wrap above dismiss, and button spacing becomes M3 tokens instead of 12dp. Usually identical on typical phone widths, but real behavior on narrow screens / large font scales. -
CardChromecontent color is unchanged —contentColorFor(surfacePaper)resolves to the ambientLocalContentColorsincesurfacePaperisn't a scheme color, so no text-color shift. ✅
Pre-existing issue (not from this PR)
SidePanelPresenterTest.kt doesn't compile (longClick unresolved; credentialStore/preferenceStore params missing). Reproduces on main; CI masks it because it only runs unit tests. Worth a separate fix.
Nit
ChatErrorBannerView's inner content isn't re-indented after the Surface wrap — cosmetic.
Summary
Replace hand-rolled
clip + background + bordermodifier chains with Material3 primitives so surface containers inherit component-level shape, color, and border semantics:Surfacefor user message bubble, error banner, reasoning card, and output stream cardAlertDialogfor the composer attachment menuOutlinedCardforCardChromeOutlinedTextFieldfor the API key fieldSurfacefor the recording composer capsuleTests
./gradlew :app:testDebugUnitTestpasses on the committed treeCommits
build: bump AGP to 9.3.1 and Gradle wrapper to 9.5.0refactor(chat): use Material3 Surface for message bubbles and cardsrefactor(home): use Material3 AlertDialog for composer attachment menurefactor(onboarding): use Material3 OutlinedCard for CardChromerefactor(sidepanel): use Material3 OutlinedTextField for API key fieldrefactor(speech): use Material3 Surface for recording composer