summaryrefslogtreecommitdiff
path: root/docs/superpowers
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-13 09:05:17 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-16 02:39:57 +0000
commit9d4b4ead6ee139fd0f2ba1508bc513e1a5fafc57 (patch)
treed1fc09c19181dc5aec26835fa7e2ae3866116de7 /docs/superpowers
parent44abf42ed45aa8f285e7ce031cbb9ef1ade667ea (diff)
docs: add design for widget text-size setting + tomorrow-section fix
Covers the root cause for the missing TOMORROW section (grid bounds skewed by tomorrow's events) and the configurable header/content text size+weight preference.
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/specs/2026-07-13-widget-text-size-and-tomorrow-fix-design.md78
1 files changed, 78 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-07-13-widget-text-size-and-tomorrow-fix-design.md b/docs/superpowers/specs/2026-07-13-widget-text-size-and-tomorrow-fix-design.md
new file mode 100644
index 0000000..6b567cd
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-13-widget-text-size-and-tomorrow-fix-design.md
@@ -0,0 +1,78 @@
+# Widget Text Size + Tomorrow-Section Fix — Design
+
+## Context
+
+Two small, related requests against `android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt`:
+
+1. Widget text is too hard to read; wants it larger/heavier, and configurable.
+2. The TOMORROW section (added in an earlier cycle) never appears, even when items are scheduled for tomorrow.
+
+Both are scoped to the same file and are being shipped together.
+
+## Part 1: Tomorrow-section bug (root cause + fix)
+
+**Root cause:** `calcGridStart`/`calcGridEnd` (lines 472-488) compute today's hour-grid bounds from `scheduledEvents`, a list that contains both today's and tomorrow's items. Both functions only inspect `.hour` on the parsed `Instant`, discarding the date — so a tomorrow event's hour-of-day skews today's grid range even though it doesn't belong in it.
+
+Confirmed against the live `/api/widget` response: with `now` at 22:54 and two calendar events tomorrow (10:00–14:30 and 17:35–...), the buggy `gridStart` calculation pulls the "earliest hour" down to 9 (from tomorrow's 10am event) instead of the correct ~21 (based on `nowHour`, since nothing is scheduled *today* this late). That manufactures ~12 empty hour rows between the real content and the widget's bottom edge. Because the widget root is a plain (non-lazy, non-scrolling) `Column`, anything past the widget's visible height is simply clipped — the TOMORROW section is being rendered, just permanently below the fold.
+
+**Fix:** filter to today-only events before computing grid bounds:
+
+```kotlin
+val todayScheduledEvents = scheduledEvents.filter { Instant.parse(it.start!!) < tomorrowStart }
+val gridStart = calcGridStart(todayScheduledEvents, nowZoned.hour)
+val gridEnd = calcGridEnd(todayScheduledEvents, nowZoned.hour)
+```
+
+No other call site changes: the `HourRow` loop keeps receiving the full `scheduledEvents` list (it already filters by real `Instant` per hour, which is date-correct), and `TomorrowSection`'s own filtering is unaffected.
+
+**Testing:** `calcGridStart`/`calcGridEnd` are currently `private` top-level functions, making them untestable from `app/src/test`. Change visibility to `internal` and add `DootWidgetGridTest.kt` with a case reproducing the live scenario (today empty, one event tomorrow at an early hour, `nowHour` late) asserting `calcGridStart` returns a value derived from `nowHour`, not from the tomorrow event's hour.
+
+## Part 2: Configurable text size
+
+**Preference:** new enum, stored as a string preference (`Keys.TEXT_SIZE`) in the same widget DataStore used for server URL/token:
+
+```kotlin
+enum class WidgetTextSize(
+ val headerScale: Float, val headerWeightBump: Int,
+ val contentScale: Float, val contentWeightBump: Int
+) {
+ SMALL(1.0f, 0, 1.0f, 0), // reproduces today's exact appearance
+ NORMAL(1.15f, 1, 1.15f, 1), // new default
+ LARGE(1.3f, 2, 1.3f, 2)
+}
+```
+
+Header and content are separate fields (currently identical numbers) so either can be retuned later without affecting the other. Default is `NORMAL` — for both new installs and existing installs with no saved preference (i.e. missing/unparseable pref value falls back to `NORMAL`, not `SMALL`).
+
+**Weight ladder:** both weight-bump fields walk the same three-step ladder, capped at the top:
+
+```kotlin
+private val weightLadder = listOf(FontWeight.Normal, FontWeight.Medium, FontWeight.Bold)
+
+fun WidgetTextSize.scaledHeaderSize(baseSp: Int): TextUnit = (baseSp * headerScale).sp
+fun WidgetTextSize.scaledHeaderWeight(base: FontWeight): FontWeight = bumpWeight(base, headerWeightBump)
+fun WidgetTextSize.scaledContentSize(baseSp: Int): TextUnit = (baseSp * contentScale).sp
+fun WidgetTextSize.scaledContentWeight(base: FontWeight): FontWeight = bumpWeight(base, contentWeightBump)
+
+private fun bumpWeight(base: FontWeight, bump: Int): FontWeight {
+ val idx = weightLadder.indexOf(base).coerceAtLeast(0)
+ return weightLadder[(idx + bump).coerceAtMost(weightLadder.lastIndex)]
+}
+```
+
+**Call-site mapping** — every `Text(..., style = TextStyle(fontSize = Xsp, fontWeight = Y))` in `DootWidget.kt` switches to `textSize.scaledHeaderSize(X)`/`scaledHeaderWeight(Y)` or the content equivalent, per this split:
+
+- Header (chrome): TODAY/TOMORROW section titles, `hourLabel` text in `HourRow`, the "TODAY" sub-header inside `TaskFragmentBlock`, the time label in `TomorrowEventRow`.
+- Content (readability): `AllDayRow` title, `EventBlock` title, `TaskRow` title, `TomorrowEventRow` title.
+
+**Propagation:** `WidgetTextSize` is read once in `DootWidget.provideGlance()` alongside the other prefs (parse the stored string, default `NORMAL` on missing/invalid), then threaded as an explicit `textSize: WidgetTextSize` parameter through the composable call chain (`WidgetRoot` → `AllDayRow`/`HourRow`/`EventBlock`/`TaskFragmentBlock`/`TaskRow`/`TomorrowSection`/`TomorrowEventRow`) — matching the file's existing explicit-parameter style; no `CompositionLocal` needed for this few call sites.
+
+**Settings UI:** `SettingsActivity`/`SettingsScreen` gains a labeled 3-option row (Small / Normal / Large) below the existing Server URL / Token fields. Selection is read from and written to `Keys.TEXT_SIZE` the same way `savedUrl`/`savedToken` are today, and saved via the existing `onSave` path — which already calls `RefreshWorker.runOnce()`, and `RefreshWorker.doWork()` unconditionally calls `DootWidget().updateAll()` regardless of whether the server fetch changed anything, so the new size takes effect immediately without extra plumbing.
+
+**Testing:** `scaledHeaderSize`/`scaledContentSize`/`bumpWeight` are pure functions — add unit tests in `app/src/test` covering the ladder-capping edge case (`Bold` + any positive bump stays `Bold`) and the scale math for each tier.
+
+## Out of scope
+
+- Icon sizes (refresh/quick-add/checkbox glyphs) are unaffected — only text scales.
+- No per-element (as opposed to header/content-category) customization.
+- No live preview in the settings screen; user judges the result on the actual home-screen widget after saving.