fix(ui): center text inside buttons with min_size across the app#851
fix(ui): center text inside buttons with min_size across the app#851
Conversation
Button::new(text).min_size(size) inherits horizontal_align from the outer layout (Align::LEFT in top_down). Text left-shifted inside the button rect across every callsite whose label is narrower than the min_size width — visible on disabled primary buttons and every toolbar/tab-switcher element. Swap to ui.add_sized(size, Button::new(text).fill(...)...), which wraps the widget in allocate_ui_with_layout with Layout::centered_and_justified. horizontal_align=Center; text centers inside the fill rect. Affects ~60 callsites that go through ComponentStyles helpers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR systematically refactors button UI construction across multiple screens by removing fixed minimum size constraints from button definitions and consolidating button styling through centralized Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Review complete (commit f9f24d3) |
Supersedes the mechanical ui.add_sized fix from the prior version of this PR. Each primary CTA, tab switcher, and toolbar button that was constructing Button::new(...).fill(...).min_size(...) manually now delegates to the appropriate ComponentStyles helper. Helpers already go through ui.add_sized after 8428fb3, so the centering bug is fixed at the source — callsites stop carrying the boilerplate. Accepts the DIALOG_BUTTON_MIN_SIZE (96×36) default for every callsite; the custom widths (120×28 tabs, 160×36 send, 150×36 toolbar) are dropped in favor of one source of truth. Migrations by bucket: - Primary CTA (add_primary_button / add_primary_button_enabled): dashpay/mod.rs, my_tokens.rs (Import/Refresh), token_creator.rs (Load Identity / Create Token / Register Token Contract / View JSON), top_up by_platform_address.rs, send_screen.rs (2 sends), single_key_send_screen.rs, withdraw_screen.rs, network_chooser (Connect). - Danger (add_danger_button): network_chooser Disconnect. - Tab switch (primary/secondary swap): contacts_list.rs, contact_requests.rs, transfer_screen.rs. - Bucket 7 (custom glass-panel styling — keep direct Button, drop .min_size and add_sized, plain ui.add): the 4 *_subscreen_chooser panels and grovestark_screen mode tabs. Also adds a kittest suite (tests/kittest/button_sizing.rs) that verifies the helpers do NOT cap button max width — long labels ("Register Token Contract") grow beyond the 96px floor; short labels ("OK") stay at the floor. This locks in the correct semantics of ui.add_sized: it treats its arg as a MAX, but egui's horizontal layout expands frame_size to max(desired, available) in next_frame_ignore_wrap, so content overflows upward rather than clipping. Future reviewers can rerun the tests instead of re-reading egui internals. Net diff: 17 files, -214 lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4f66fad to
f9f24d3
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
tests/kittest/button_sizing.rs (2)
92-110: Add a disabled-path sizing check foradd_primary_button_enabled.This file currently verifies only
enabled = true; addingenabled = falsecoverage would better protect the exact branch this PR also updates.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/kittest/button_sizing.rs` around lines 92 - 110, Add a companion test that covers the disabled path of ComponentStyles::add_primary_button_enabled: create a new test (e.g., primary_button_disabled_does_not_shrink_for_long_label or primary_button_disabled_grows_for_long_label) that mirrors primary_button_enabled_grows_for_long_label but calls add_primary_button_enabled with enabled = false, builds the same Harness with the long label "Register Token Contract", obtains the rect via button_rect(&mut harness, "Register Token Contract"), and asserts the expected sizing behaviour (same width threshold as the enabled test or an appropriate threshold for disabled state). This ensures the disabled branch of add_primary_button_enabled is exercised alongside the existing enabled test.
41-50: Tighten floor assertions to the shared min-size constant.The current
>= 90/>= 30checks are a bit too permissive and can pass if the96x36floor regresses.Proposed test hardening
fn primary_button_floors_short_label() { @@ let rect = button_rect(&mut harness, "OK"); + let min = ComponentStyles::DIALOG_BUTTON_MIN_SIZE; assert!( - rect.width() >= 90.0, - "short label must still honor ~96px min width (actual: {})", + rect.width() >= min.x - 0.5, + "short label must honor min width {} (actual: {})", + min.x, rect.width() ); assert!( - rect.height() >= 30.0, - "short label must honor ~36px min height (actual: {})", + rect.height() >= min.y - 0.5, + "short label must honor min height {} (actual: {})", + min.y, rect.height() ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/kittest/button_sizing.rs` around lines 41 - 50, The tests use loose numeric floors (>= 90.0 and >= 30.0) that can hide regressions; update the two assertions in tests/kittest/button_sizing.rs to compare against the shared min-size constant(s) instead of hardcoded numbers — e.g., replace 90.0 with the app's BUTTON_MIN_WIDTH (or MIN_BUTTON_SIZE.width) and 30.0 with BUTTON_MIN_HEIGHT (or MIN_BUTTON_SIZE.height), import or reference the module constant used by the UI layout code, and keep the existing rect.width()/rect.height() checks and message formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@tests/kittest/button_sizing.rs`:
- Around line 92-110: Add a companion test that covers the disabled path of
ComponentStyles::add_primary_button_enabled: create a new test (e.g.,
primary_button_disabled_does_not_shrink_for_long_label or
primary_button_disabled_grows_for_long_label) that mirrors
primary_button_enabled_grows_for_long_label but calls add_primary_button_enabled
with enabled = false, builds the same Harness with the long label "Register
Token Contract", obtains the rect via button_rect(&mut harness, "Register Token
Contract"), and asserts the expected sizing behaviour (same width threshold as
the enabled test or an appropriate threshold for disabled state). This ensures
the disabled branch of add_primary_button_enabled is exercised alongside the
existing enabled test.
- Around line 41-50: The tests use loose numeric floors (>= 90.0 and >= 30.0)
that can hide regressions; update the two assertions in
tests/kittest/button_sizing.rs to compare against the shared min-size
constant(s) instead of hardcoded numbers — e.g., replace 90.0 with the app's
BUTTON_MIN_WIDTH (or MIN_BUTTON_SIZE.width) and 30.0 with BUTTON_MIN_HEIGHT (or
MIN_BUTTON_SIZE.height), import or reference the module constant used by the UI
layout code, and keep the existing rect.width()/rect.height() checks and message
formatting.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2df0d3a8-e0cb-4c2f-97f0-6098f5dfba31
📒 Files selected for processing (20)
src/ui/components/dashpay_subscreen_chooser_panel.rssrc/ui/components/dpns_subscreen_chooser_panel.rssrc/ui/components/tokens_subscreen_chooser_panel.rssrc/ui/components/tools_subscreen_chooser_panel.rssrc/ui/dashpay/contact_requests.rssrc/ui/dashpay/contacts_list.rssrc/ui/dashpay/mod.rssrc/ui/identities/top_up_identity_screen/by_platform_address.rssrc/ui/identities/transfer_screen.rssrc/ui/identities/withdraw_screen.rssrc/ui/network_chooser_screen.rssrc/ui/theme.rssrc/ui/tokens/tokens_screen/my_tokens.rssrc/ui/tokens/tokens_screen/token_creator.rssrc/ui/tools/grovestark_screen.rssrc/ui/wallets/send_screen.rssrc/ui/wallets/single_key_send_screen.rssrc/ui/wallets/wallets_screen/dialogs.rstests/kittest/button_sizing.rstests/kittest/main.rs
💤 Files with no reviewable changes (5)
- src/ui/components/dashpay_subscreen_chooser_panel.rs
- src/ui/components/dpns_subscreen_chooser_panel.rs
- src/ui/components/tokens_subscreen_chooser_panel.rs
- src/ui/components/tools_subscreen_chooser_panel.rs
- src/ui/tools/grovestark_screen.rs
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
The two UI commits at this SHA mechanically swap from Button::min_size(...) to ui.add_sized(...) in ComponentStyles helpers and 17 direct Button::new(...) callsites to fix text centering. The change is semantically intentional and documented in the helper doc comments, but ships without a kittest harness locking in the new sizing contract. No correctness or security defects.
Reviewed commit: 4f66fad
🟡 1 suggestion(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/theme.rs`:
- [SUGGESTION] lines 848-915: Add a kittest harness for the new add_sized button-sizing contract
`add_primary_button`, `add_primary_button_enabled`, `add_secondary_button`, and `add_danger_button` now route every callsite through `ui.add_sized(DIALOG_BUTTON_MIN_SIZE, ...)` instead of `Button::min_size(...)`. `add_sized` wraps the widget in a `centered_and_justified` inner layout — that is exactly what fixes the text-centering bug, but it is not a drop-in replacement for `min_size`: it imposes a fixed size from the parent rather than a floor on the widget's own desired size. The doc comments on these helpers correctly explain the new semantics, but with 17 direct `Button::new(...)` callsites also migrated and `tests/kittest/button_sizing.rs` not present at this SHA, there is nothing in the repo that fails when a future cleanup reverts to `.min_size(...)` or drops `add_sized`. A small kittest covering at least one of each helper plus a long-label case (label wider than `DIALOG_BUTTON_MIN_SIZE`) and a short-label case would lock in both the centering fix and the expected min footprint.
| @@ -877,25 +879,38 @@ impl ComponentStyles { | |||
| .fill(DashColors::BUTTON_DISABLED) | |||
| .stroke(egui::Stroke::NONE) | |||
| .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) | |||
| .min_size(Self::DIALOG_BUTTON_MIN_SIZE) | |||
| }; | |||
| ui.add_enabled(enabled, button) | |||
| .on_hover_cursor(CursorIcon::PointingHand) | |||
| // `add_sized` wraps the widget in a `centered_and_justified` inner layout so the | |||
| // button's `AtomLayout` inherits `horizontal_align = Center`. This keeps the text | |||
| // centered within the fill rect for both the enabled and disabled branches, matching | |||
| // footprints so swapping between states doesn't jitter the cursor. | |||
| ui.add_enabled_ui(enabled, |ui| { | |||
| ui.add_sized(Self::DIALOG_BUTTON_MIN_SIZE, button) | |||
| }) | |||
| .inner | |||
| .on_hover_cursor(CursorIcon::PointingHand) | |||
| } | |||
|
|
|||
| /// Add a secondary button to the UI with pointer cursor on hover. | |||
| /// | |||
| /// See `add_primary_button` for why `add_sized` is used. | |||
| pub fn add_secondary_button( | |||
| ui: &mut egui::Ui, | |||
| label: impl Into<WidgetText>, | |||
| dark_mode: bool, | |||
| ) -> egui::Response { | |||
| ui.add(Self::secondary_button(label, dark_mode)) | |||
| .on_hover_cursor(CursorIcon::PointingHand) | |||
| ui.add_sized( | |||
| Self::DIALOG_BUTTON_MIN_SIZE, | |||
| Self::secondary_button(label, dark_mode), | |||
| ) | |||
| .on_hover_cursor(CursorIcon::PointingHand) | |||
| } | |||
|
|
|||
| /// Add a danger button to the UI with pointer cursor on hover. | |||
| /// | |||
| /// See `add_primary_button` for why `add_sized` is used. | |||
| pub fn add_danger_button(ui: &mut egui::Ui, label: impl Into<WidgetText>) -> egui::Response { | |||
| ui.add(Self::danger_button(label)) | |||
| ui.add_sized(Self::DIALOG_BUTTON_MIN_SIZE, Self::danger_button(label)) | |||
| .on_hover_cursor(CursorIcon::PointingHand) | |||
| } | |||
There was a problem hiding this comment.
🟡 Suggestion: Add a kittest harness for the new add_sized button-sizing contract
add_primary_button, add_primary_button_enabled, add_secondary_button, and add_danger_button now route every callsite through ui.add_sized(DIALOG_BUTTON_MIN_SIZE, ...) instead of Button::min_size(...). add_sized wraps the widget in a centered_and_justified inner layout — that is exactly what fixes the text-centering bug, but it is not a drop-in replacement for min_size: it imposes a fixed size from the parent rather than a floor on the widget's own desired size. The doc comments on these helpers correctly explain the new semantics, but with 17 direct Button::new(...) callsites also migrated and tests/kittest/button_sizing.rs not present at this SHA, there is nothing in the repo that fails when a future cleanup reverts to .min_size(...) or drops add_sized. A small kittest covering at least one of each helper plus a long-label case (label wider than DIALOG_BUTTON_MIN_SIZE) and a short-label case would lock in both the centering fix and the expected min footprint.
source: ['codex']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/theme.rs`:
- [SUGGESTION] lines 848-915: Add a kittest harness for the new add_sized button-sizing contract
`add_primary_button`, `add_primary_button_enabled`, `add_secondary_button`, and `add_danger_button` now route every callsite through `ui.add_sized(DIALOG_BUTTON_MIN_SIZE, ...)` instead of `Button::min_size(...)`. `add_sized` wraps the widget in a `centered_and_justified` inner layout — that is exactly what fixes the text-centering bug, but it is not a drop-in replacement for `min_size`: it imposes a fixed size from the parent rather than a floor on the widget's own desired size. The doc comments on these helpers correctly explain the new semantics, but with 17 direct `Button::new(...)` callsites also migrated and `tests/kittest/button_sizing.rs` not present at this SHA, there is nothing in the repo that fails when a future cleanup reverts to `.min_size(...)` or drops `add_sized`. A small kittest covering at least one of each helper plus a long-label case (label wider than `DIALOG_BUTTON_MIN_SIZE`) and a short-label case would lock in both the centering fix and the expected min footprint.
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
The helper migration looks coherent against the checked-out PR head, and I did not find a blocking behavioral regression in the migrated callsites from source review. The only issue worth surfacing is in the new kittest coverage: it verifies button sizing semantics, but it still does not protect the text-centering regression this PR is meant to address. That makes this a comment-level test gap, not a change blocker.
Reviewed commit: f9f24d3
🟡 1 suggestion(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `tests/kittest/button_sizing.rs`:
- [SUGGESTION] lines 13-110: The new kittests do not actually guard the button-centering regression
All five tests assert only the outer button `Rect` width/height, and they do so inside `ui.horizontal(...)` layouts. That covers the new `add_sized(...)` sizing semantics, but it does not exercise the top-down/left layout that originally caused short labels to render left-shifted inside the button, and it never inspects text placement at all. A regression back to `ui.add(button.min_size(...))` could therefore reintroduce the user-visible centering bug while these tests still pass, so the core behavior this PR fixes remains effectively unguarded.
| #[test] | ||
| fn primary_button_grows_for_long_label() { | ||
| let mut harness = Harness::builder() | ||
| .with_size(egui::vec2(800.0, 200.0)) | ||
| .build_ui(|ui| { | ||
| ui.horizontal(|ui| { | ||
| let _ = ComponentStyles::add_primary_button(ui, "Register Token Contract"); | ||
| }); | ||
| }); | ||
| let rect = button_rect(&mut harness, "Register Token Contract"); | ||
| eprintln!("Register Token Contract rect: {rect:?}"); | ||
| assert!( | ||
| rect.width() > 120.0, | ||
| "long label must grow past the 96px floor (actual width: {})", | ||
| rect.width() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn primary_button_floors_short_label() { | ||
| let mut harness = Harness::builder() | ||
| .with_size(egui::vec2(800.0, 200.0)) | ||
| .build_ui(|ui| { | ||
| ui.horizontal(|ui| { | ||
| let _ = ComponentStyles::add_primary_button(ui, "OK"); | ||
| }); | ||
| }); | ||
| let rect = button_rect(&mut harness, "OK"); | ||
| assert!( | ||
| rect.width() >= 90.0, | ||
| "short label must still honor ~96px min width (actual: {})", | ||
| rect.width() | ||
| ); | ||
| assert!( | ||
| rect.height() >= 30.0, | ||
| "short label must honor ~36px min height (actual: {})", | ||
| rect.height() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn secondary_button_grows_for_long_label() { | ||
| let mut harness = Harness::builder() | ||
| .with_size(egui::vec2(800.0, 200.0)) | ||
| .build_ui(|ui| { | ||
| ui.horizontal(|ui| { | ||
| let _ = ComponentStyles::add_secondary_button( | ||
| ui, | ||
| "Create Asset Lock Transaction", | ||
| false, | ||
| ); | ||
| }); | ||
| }); | ||
| let rect = button_rect(&mut harness, "Create Asset Lock Transaction"); | ||
| assert!( | ||
| rect.width() > 150.0, | ||
| "long label on secondary must grow (actual: {})", | ||
| rect.width() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn danger_button_grows_for_long_label() { | ||
| let mut harness = Harness::builder() | ||
| .with_size(egui::vec2(800.0, 200.0)) | ||
| .build_ui(|ui| { | ||
| ui.horizontal(|ui| { | ||
| let _ = ComponentStyles::add_danger_button(ui, "Remove From Local Database"); | ||
| }); | ||
| }); | ||
| let rect = button_rect(&mut harness, "Remove From Local Database"); | ||
| assert!( | ||
| rect.width() > 150.0, | ||
| "long label on danger must grow (actual: {})", | ||
| rect.width() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn primary_button_enabled_grows_for_long_label() { | ||
| let mut harness = Harness::builder() | ||
| .with_size(egui::vec2(800.0, 200.0)) | ||
| .build_ui(|ui| { | ||
| ui.horizontal(|ui| { | ||
| let _ = ComponentStyles::add_primary_button_enabled( | ||
| ui, | ||
| true, | ||
| "Register Token Contract", | ||
| ); | ||
| }); | ||
| }); | ||
| let rect = button_rect(&mut harness, "Register Token Contract"); | ||
| assert!( | ||
| rect.width() > 120.0, | ||
| "long label on enabled primary must grow (actual: {})", | ||
| rect.width() | ||
| ); | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: The new kittests do not actually guard the button-centering regression
All five tests assert only the outer button Rect width/height, and they do so inside ui.horizontal(...) layouts. That covers the new add_sized(...) sizing semantics, but it does not exercise the top-down/left layout that originally caused short labels to render left-shifted inside the button, and it never inspects text placement at all. A regression back to ui.add(button.min_size(...)) could therefore reintroduce the user-visible centering bug while these tests still pass, so the core behavior this PR fixes remains effectively unguarded.
source: ['claude-general', 'codex-general']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `tests/kittest/button_sizing.rs`:
- [SUGGESTION] lines 13-110: The new kittests do not actually guard the button-centering regression
All five tests assert only the outer button `Rect` width/height, and they do so inside `ui.horizontal(...)` layouts. That covers the new `add_sized(...)` sizing semantics, but it does not exercise the top-down/left layout that originally caused short labels to render left-shifted inside the button, and it never inspects text placement at all. A regression back to `ui.add(button.min_size(...))` could therefore reintroduce the user-visible centering bug while these tests still pass, so the core behavior this PR fixes remains effectively unguarded.
Summary
Fixes a widespread egui 0.33 behavior where
Button::new(text).min_size(size)inherits
horizontal_alignfrom the outer layout (Align::LEFTin defaulttop_down). Text left-shifts inside the button rect whenever the label isnarrower than the min_size width.
Follow-up to PR #850, which fixed this for one specific callsite
(the disabled Transfer button) at commit
7be4f39b. This PR propagatesthe fix across every helper in
ComponentStylesAND eliminates thedirect-callsite boilerplate that was duplicating the fix.
Design
Two commits:
refactor(ui): use ui.add_sized in ComponentStyles button helpers—Swap
ui.add(Button::new(text).min_size(size).fill(...))forui.add_sized(size, Button::new(text).fill(...))in every helper.add_sizedwraps the widget inallocate_ui_with_layout(size, Layout::centered_and_justified(main_dir))—
horizontal_align=Center, text centers inside the fill rect.Important:
ui.add_sizedtreats its size arg as a MAX, but egui'shorizontal layout expands
frame_sizetomax(desired, available)innext_frame_ignore_wrap, so long labels overflow upward instead ofclipping. Proven by the new kittest
button_sizingsuite.refactor(ui): migrate direct Button callsites to ComponentStyles helpers—Every primary CTA, tab switcher, and toolbar button that was
constructing
Button::new(...).fill(...).min_size(...)manually nowdelegates to the appropriate
ComponentStyleshelper. All directcallsites now delegate to
ComponentStyleshelpers; no.min_size(...)constants remain in the migrated files. Custom widths (120×28 tabs,
160×36 send, 150×36 toolbar) collapse to the shared
DIALOG_BUTTON_MIN_SIZE(96×36) floor. Callsites that carry abespoke glass-panel look (the 4
*_subscreen_chooser_panel.rsfilesplus
grovestark_screen.rsmode tabs) keep their directButtonconstruction but drop
.min_size(...)entirely — buttons size tocontent there.
Changes
Commit 1 —
src/ui/theme.rs,src/ui/wallets/wallets_screen/dialogs.rsadd_primary_button,add_secondary_button,add_danger_button,add_toolbar_button,add_primary_button_enabledall go throughui.add_sized(DIALOG_BUTTON_MIN_SIZE, btn).dialogs.rsthat dispatched viaui.add_enabled(...)switches toui.add_enabled_ui(..., |ui| ui.add_sized(DIALOG_BUTTON_MIN_SIZE, btn)).Commit 2 — migration to helpers (17 files, -214 lines net)
Primary CTA (
add_primary_button/add_primary_button_enabled):dashpay/mod.rs,my_tokens.rs(Import/Refresh),token_creator.rs(Load Identity / Create Token / Register Token Contract / View JSON),
top_up by_platform_address.rs,send_screen.rs(2 sends),single_key_send_screen.rs,withdraw_screen.rs,network_chooser_screen.rs(Connect).Danger (
add_danger_button):network_chooser_screen.rs(Disconnect).Tabs (primary/secondary swap for active/inactive):
contacts_list.rs(My Contacts / Requests),
contact_requests.rs(Incoming / Outgoing),transfer_screen.rs(Identity / Platform Address).Bucket 7 — keep direct Button, drop
.min_size(...)andadd_sized,use plain
ui.add: the 4*_subscreen_chooser_panel.rsfiles(DashPay, DPNS, Tokens, Tools) and
grovestark_screen.rs(Generate /Verify tabs). Custom glass-panel styling doesn't map to any existing
helper; buttons now content-size.
New tests —
tests/kittest/button_sizing.rs(5 tests)Locks in the correct semantics of the helpers:
"Remove From Local Database") grow past the 96px floor.
add_primary_button,add_primary_button_enabled,add_secondary_button,add_danger_button.Test plan
cargo +nightly fmt --all— cleancargo clippy --all-features --all-targets -- -D warnings— cleancargo test --test kittest --all-features— 77/77 pass (including5 new
button_sizingtests)— text centered in fill.
Address, Generate/Verify Proof) — text centered.
now size to content (previously fixed 150×28); confirm no visual
regression.
Summary by CodeRabbit
Bug Fixes
Refactor
Tests