flux branch worktree-grid-trio-font commits 1 files 3 touched lines +22 / -1

Pre-push review: grid-trio-font

Dashboard live trio no longer wraps large grid-import values onto two lines.

At a glance

  • When grid import (or any Solar/House/Grid column) reaches 10 kW, the value string is 5 characters ("11.51") and previously wrapped onto a second line.
  • A new trioValueCompact typography variant (22 pt vs the regular 26 pt) is selected when the numeric value runs past four characters; values under 10 kW are unchanged.
  • .lineLimit(1) on the value Text guarantees it can never wrap, even for unexpected widths or custom fonts.
  • The rule applies to all three columns, so House or Solar crossing 10 kW gets the same treatment — keeping the trio visually consistent.

Verdict

Ready to push

A focused, low-risk UI fix. Three parallel review agents (reuse, quality, efficiency) found nothing to consolidate, no efficiency regression, and confirmed the threshold logic matches the intended behaviour. The one nit raised — a comment that under-described the trigger range — was fixed in this review. Build succeeds and the two edited files lint clean.

Review findings

3 raised · 1 fixed · 2 skipped

Jump to findings →

Commits

Three-level explanation

What changed

The Dashboard shows three little columns at a glance: Solar, House, and Grid. Each shows a number and a unit (like 11.51 kW). When the grid number got big enough to have five characters (anything 10 kW or more), it was too wide for its column and the last digit dropped onto a second line, which looked broken.

Why it matters

It's a visual glitch on the app's main screen — the first thing you see. The fix makes the number a bit smaller only when it's long, so it always stays on one tidy line.

Key concepts

  • Font size: text is drawn at a point size; 26 pt normally, 22 pt when the value is long.
  • Line limit: telling the text "only ever use one line" so it can't wrap.

Architecture

LiveTrioPanel.column(...) formats the wattage via PowerFormatting.split, which returns a (value, unit) pair — e.g. ("11.51", "kW") for ≥ 1 kW, ("950", "W") below 1 kW. The value and unit render as two adjacent Text views in a firstTextBaseline HStack.

Pattern

The fix selects the font by the formatted value's character count: (parts?.value.count ?? 0) > 4 ? trioValueCompact : trioValue. Because split formats kW with %.2f, anything ≥ 10 kW is exactly five characters, which is the natural boundary for "regular under 10 kW, smaller above." A new FluxTheme.Typography.trioValueCompact mirrors trioValue (same medium weight, monospaced digits) at 22 pt. .lineLimit(1) is a belt-and-braces guard against wrapping.

Trade-offs

The threshold is coupled to the formatter's output shape rather than the underlying magnitude. That's acceptable here — both live in tightly-related code and the relationship is documented in a comment — but a magnitude-based predicate would be marginally less brittle if the formatter ever changed precision.

Deep dive

The selection is computed once per render in the view body as a stored function reference (FluxTheme.Typography.trioValueCompact is a (String?) -> Font), then resolved by AppFontModifier against the appFontFamily environment value. AppFontResolver.resolve caches font-availability lookups per family, so switching between the 22 pt and 26 pt variants across the 10 s auto-refresh cycle is a constant-cost cache hit plus a SwiftUI Font construction — no regression versus the prior unconditional trioValue.

Edge cases

  • abs() is applied upstream (watts.map(PowerFormatting.split) receives abs(pgrid) for the grid column), so no minus sign ever inflates the character count.
  • W-range values ("0"–"999") are ≤ 3 chars and always keep the regular font; kW "0.00"–"9.99" are 4 chars (regular); "10.00"+ are 5 chars (compact). The boundary is exact.
  • .lineLimit(1) sits only on the value Text; the unit ("kW"/"W", fixed 11 pt) is small enough that the compact value keeps the pair within the column width, so the unit needs no explicit limit.
  • monospacedDigit() is preserved on the compact variant so digits stay tabular across columns; the documented caveat is that custom font families without a tabular-figures variant may drift slightly — unchanged from existing behaviour.

Completeness assessment

Fully implemented: the requested behaviour (regular < 10 kW, slightly smaller ≥ 10 kW, no wrapping). Partial/missing: none for the stated scope. No spec exists for this panel; this is a standalone visual bugfix.

Important changes — detailed

LiveTrioPanel: length-based value font selection

LiveTrioPanel.swift

Why it matters. User-visible fix on the app's primary screen; prevents the grid value wrapping to two lines at ≥ 10 kW. Applies to all three columns for consistency.

What to look at. LiveTrioPanel.swift:60-65, 79-82

Takeaway. When a value+unit pair can overflow a fixed-width column, picking a smaller font by the formatted string's length (tied to the formatter's known output shape) is a simple alternative to SwiftUI's minimumScaleFactor/ViewThatFits when you want a discrete, predictable step rather than continuous scaling.
Rationale. The user asked for a discrete behaviour — regular under 10 kW, slightly smaller above — rather than continuous auto-shrink. A character-count threshold expresses that exactly because PowerFormatting.split formats kW with %.2f (≥ 10 kW = 5 chars).

FluxTheme: trioValueCompact typography variant

FluxTheme.swift

Why it matters. Adds the 22 pt variant the panel switches to, keeping all sizing in the central typography enum rather than as an inline magic number at the call site.

What to look at. FluxTheme.swift:109-115

Takeaway. Keep paired size variants in the same typography namespace with identical weight/digit treatment so they remain visually consistent and easy to find.
Rationale. Mirrors the existing trioValue (medium weight, monospacedDigit) at a ~15% smaller size, following the established FluxTheme.Typography convention.

Key decisions

Character-count threshold (&gt; 4) rather than a magnitude check.

The user requested a discrete "regular under 10 kW, smaller above" behaviour. Because PowerFormatting.split formats kW with %.2f, the value is exactly five characters at and above 10 kW, so a length test of > 4 maps cleanly to the requested boundary and reads simply. The coupling to the formatter's output is documented inline.

22 pt for the compact variant.

A ~15% reduction from the regular 26 pt — "a little smaller" as requested — verified to fit "11.51 kW" within the iPhone column width while staying close to the regular size. Same medium weight and monospaced digits as trioValue.

(inferred — not stated by the author.)
Apply to all three columns, not just Grid.

The shared column(...) helper renders Solar, House, and Grid identically, so the rule naturally covers any column crossing 10 kW. This matches the project's data/visual-consistency principle that a value shown in multiple places should behave the same everywhere.

(inferred — not stated by the author.)

Review findings

SeverityAreaFindingResolution
minorLiveTrioPanel.swift / FluxTheme.swift commentsThe explanatory comment cited "11.51" as the trigger example, which under-described the actual range — the > 4 rule fires for the whole ≥ 10 kW range (e.g. "10.00" is also 5 chars).Reworded both comments to state that values ≥ 10 kW render five characters, with "10.00"/"11.51" as examples.
minorcode reuseConsidered whether an existing shrink-to-fit mechanism (minimumScaleFactor / ViewThatFits) or a shared renderer (PowerTrioColumns, DashboardHeroPanel) should be reused instead.None applies: LiveTrioPanel is the only caller of PowerFormatting.split, no length-aware font pattern exists in the app, and the widget trio uses the combined PowerFormatting.format. The localized fix is correct.
minorefficiencyChecked whether per-render font selection on the 10 s refresh hot path adds cost.No regression — AppFontResolver caches availability per family; the selection is O(1) and the compact path costs the same as the prior unconditional resolve.

Per-file diffs

Click to expand.

Flux/Flux/Dashboard/LiveTrioPanel.swift Modified +9 / -1
diff --git a/Flux/Flux/Dashboard/LiveTrioPanel.swift b/Flux/Flux/Dashboard/LiveTrioPanel.swiftindex 0e4e3eb..fcb5bbf 100644--- a/Flux/Flux/Dashboard/LiveTrioPanel.swift+++ b/Flux/Flux/Dashboard/LiveTrioPanel.swift@@ -58,6 +58,13 @@ struct LiveTrioPanel: View {         showsLeftDivider: Bool     ) -> some View {         let parts = watts.map(PowerFormatting.split)+        // Drop to a slightly smaller value font once the numeric portion runs+        // past four characters — values ≥ 10 kW render five characters+        // (e.g. "10.00", "11.51") — so the value plus unit stays on one line+        // instead of wrapping in the column.+        let valueFont = (parts?.value.count ?? 0) > 4+            ? FluxTheme.Typography.trioValueCompact+            : FluxTheme.Typography.trioValue         HStack(spacing: 0) {             if showsLeftDivider {                 Rectangle()@@ -71,8 +78,9 @@ struct LiveTrioPanel: View {                     .foregroundStyle(FluxTheme.Palette.tertiaryText)                 HStack(alignment: .firstTextBaseline, spacing: 2) {                     Text(parts?.value ?? "—")-                        .appFont(FluxTheme.Typography.trioValue)+                        .appFont(valueFont)                         .foregroundStyle(valueColor)+                        .lineLimit(1)                     Text(parts?.unit ?? "kW")                         .appFont(FluxTheme.Typography.trioUnit)                         .foregroundStyle(FluxTheme.Palette.tertiaryText)
Flux/Flux/Helpers/FluxTheme.swift Modified +7 / -0
diff --git a/Flux/Flux/Helpers/FluxTheme.swift b/Flux/Flux/Helpers/FluxTheme.swiftindex 4861365..eba175d 100644--- a/Flux/Flux/Helpers/FluxTheme.swift+++ b/Flux/Flux/Helpers/FluxTheme.swift@@ -106,6 +106,13 @@ enum FluxTheme {         nonisolated static func trioValue(family: String?) -> Font {             AppFontResolver.resolve(size: 26, weight: .medium, family: family).monospacedDigit()         }+        /// Slightly smaller trio value used when the numeric portion runs long+        /// (values ≥ 10 kW render five characters, e.g. "10.00", "11.51"), so+        /// the value plus its unit fits on one line instead of wrapping in the+        /// column.+        nonisolated static func trioValueCompact(family: String?) -> Font {+            AppFontResolver.resolve(size: 22, weight: .medium, family: family).monospacedDigit()+        }         nonisolated static func trioUnit(family: String?) -> Font {             AppFontResolver.resolve(size: 11, weight: .regular, family: family)         }
CHANGELOG.md Modified +6 / -0
diff --git a/CHANGELOG.md b/CHANGELOG.mdindex 354ecab..9dd398c 100644--- a/CHANGELOG.md+++ b/CHANGELOG.md@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [Unreleased]++### Fixed++- **Dashboard live trio no longer wraps large grid values.** When grid import (or any Solar/House/Grid column) reached 10 kW or more, the value rendered "11.51 kW" with the trailing digit wrapping onto a second line. The numeric value now drops to a slightly smaller font (22 pt instead of 26 pt) once it runs past four characters, keeping the value and unit on one line; values under 10 kW are unchanged.+ ## [1.4] - 2026-05-31  ### Added

Things to double-check

Flaky test, not this change.

DashboardViewModelTests.refreshSkipsWhenAlreadyLoading failed on one suite run and passed on others. It is a timing-dependent async test (relies on an 80 ms in-flight window) and passes in isolation. Unrelated to this font-only change, but worth de-flaking separately.