The marker could still be caught fading in on the outgoing page. Paging changes
the selection immediately but the state only once the new month has loaded, so
until then the content key — the state's month — is unchanged and AnimatedContent
updates the page *in place*: the old grid really was told the next month's 1st
was selected, and it shows that date among its trailing days.
A page now marks only the days its own month owns. Selection always lands inside
the month it belongs to, so nothing legitimate is lost, and the phantom goes
without timing the fade against the page transition — a delay would have moved
it rather than removed it, and slowed selecting a day within a page besides.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three things still moved badly while paging.
The selection marker was thrown across the outgoing grid. `selected` was read
from outside the AnimatedContent, so both pages rendered the *incoming*
selection — and the new month's 1st is a day the old grid still shows among its
trailing days, so the outline jumped there (bottom right) before the new page
slid in and put it where it belonged (top left). The selection now travels with
the state, so each page keeps its own and the marker only ever appears where its
month has it.
Moving the selection within a month also jumped, outline blinking out of one
cell and into another. Each cell now fades its own outline, so it reads as one
mark crossing the grid. Snapped under reduced motion.
And the day pane swapped its rows outright, popping one list out and another in.
It now cross-fades, the incoming list rising as it arrives — the same entrance
the rest of the family uses for resolved content — with the skeleton handing
over the same way. Rows within a day also animate their placement, so an event
arriving from a sync nudges its neighbours instead of teleporting them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paging between months, weeks and days looked rough, and the transition was the
reason: a full-width slide with no fade at all. AnimatedContent stacks the two
pages and both were fully opaque, so what you saw was one grid racing across
another and clipping at the viewport edge — the movement was carrying the entire
transition, over the longest distance available.
It is now M3's shared-axis X. The offset only hints the direction — a fifth of
the width — while a cross-fade does the swapping, position springs and opacity
eases, and the size transform no longer clips. All three calendar views share
this transition, so all three settle.
Also lifts the month view's swipe threshold from 6dp to the 24dp the week and
day views already used. Six is inside the distance a tap wanders, so brushing
the grid turned the page — which reads as the animation firing at random rather
than as an over-eager gesture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two things made a swipe between months feel rough, both from the grid and the
pane disagreeing about what was happening.
The grid sized itself to its month, 4 to 6 rows, so every swipe shunted the pane
up or down by a row while the grid content swapped underneath with no transition
at all. It now always reserves six rows, padding a short month with blank ones —
which both holds the pane still and makes the paged style's directional slide
safe to use here, since there is no longer a height change to animate under it.
The pane also flashed "Nothing scheduled" mid-page. Paging moves the selection
to the new month before that month's events have arrived, and the pane was
handed `instancesByDay[selected].orEmpty()` — so a day that simply wasn't loaded
yet was indistinguishable from a day with nothing on it, and got reported as
free. It now takes a nullable list and shows skeleton rows for the gap, saying
"still looking" rather than making a claim about the day.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The selected cell also swapped its fill to primaryContainer, which lightened the
whole cell — a second state stacked on the day rather than a mark on it, and one
that fought today's circle when the two landed together. The outline and the day
number carry it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rule between the compact grid and the day pane earns its place in the
Continuous header, where it closes a section off; here it only draws a line
between two halves the layout already separates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opening Continuous stayed slow, and the slowness tracked the number of enabled
calendars rather than the window size — which pointed past the provider query to
the layout behind it.
layoutCalendarWeek opens by filtering the instances it is handed down to the
ones touching its seven days. For a single month's grid that is nothing; for the
scrolling styles it was a scan of every instance in an eleven-month window,
repeated for each of a hundred-odd rows, with a time-zone conversion per check.
The work grew with events × rows, so switching every calendar on multiplied it.
- DayIndex buckets the window's events by the dates they cover, once, and each
row takes the handful on its own seven days. Membership is decided by
coversDay itself rather than re-derived from the timestamps — the all-day and
timed cases have enough edge cases between them (UTC anchoring, exclusive
ends, zero-length events at midnight) that a second implementation would
drift. DayIndexTest pins row-for-row equality with the old full scan, in a
zone east of UTC.
- Only the style on screen is laid out. Adding Dense had quietly doubled the
work, since both layouts were built from every load and one was always thrown
away.
- The paged flow no longer queries under a scrolling style, mirroring the gate
the scrolling flow already had.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opening Continuous made the first frame wait for nine months of recurrence
expansion when only one was about to be looked at — a provider Instances query
expands across its whole range, so the window's generosity was paid for up
front, every time.
The window now starts at one month either side of the visible one — about what
the paged style costs — and each completed load reaches two months further in
both directions until it spans eleven. The months a scroll can reach arrive
while the first one is already on screen, and because a widening is triggered by
the previous load landing rather than by a timer, the ladder can never outrun
the provider.
- The paged and split styles no longer run this query at all. The screen
collects the flow whatever the style is set to, so until now every Month view
opening paid for a window it would never draw.
- The reload trigger is derived from the current pad instead of being fixed, and
is held strictly inside it: a trigger at or beyond the pad would re-fire the
moment its own reload landed.
- The scrolling styles get their own skeleton — the layout they are about to
become, at the same measurements, so arriving months replace it in place. It
and the per-month placeholders now breathe, so a slow load reads as work
rather than as an empty grid. Held still under reduced motion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Continuous's month blocks make the boundaries legible, but the seams are exactly
what someone who wants a dense calendar doesn't want. Both are now offered:
- **Dense** is the old flowing layout, kept rather than replaced — one
uninterrupted stream of weeks with months running into each other, the 1st
naming its month, and the top bar keeping the full month title (there is no
sticky header to defer to).
- Continuous gains a **rule under its month label**, closing the header off
against the grid. On trial: it comes out again if it doesn't earn its place.
One provider query serves both. `ContinuousMonthUiState.Success` now carries the
same loaded window laid out twice — `monthsByIndex` clipped into blocks,
`weeksByIndex` left whole — rather than standing up a second flow and querying
the same range again. Both styles report *months* as they scroll, so the window
hysteresis is shared; `weekWindowFor` maps a month window onto the Dense rows it
covers, widened a week each side so boundary rows don't flicker.
The list state is keyed on style *and* week start: Dense indexes by week, which
the week-start preference moves, while Continuous indexes by month, which it
doesn't — carrying an offset between the two would land somewhere arbitrary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The continuous grid failed outright with "Could not read the calendar" the
moment it opened.
Before the first layout pass LazyListState reports no visible items, and the
screen defaulted that to item 0 — claiming January 1900 was on screen. The load
window widened around it, its pad ran past the epoch to index -4, and
yearMonthForIndex indexed Month.entries out of bounds; the flow's catch turned
the crash into the generic provider failure. Week indexing never showed this:
index 0 was a real week, so a bogus report only cost one wasted query.
- The visible-range report is skipped entirely while the list has no items,
rather than standing in a default for them.
- clampMonthWindow holds any window inside 1900–2100, so a pad at either end
can't produce an index with no month behind it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The continuous style streamed weeks with no boundaries at all, which left it
hard to tell where one month ended: the only marker was a "Jul 1" label on the
1st, and every boundary week mixed two months' days into one row.
It is now a vertical stack of self-contained month blocks. Each block shows only
its own days — the boundary week keeps its seven columns so nothing shifts
sideways, but the neighbour month's cells are blank rather than filled with
duplicates of days shown again a block later — under a sticky month header with
whitespace either side. Scrolling stays continuous; only the reading changes.
- The coordinate space moves from absolute week index to absolute month index
(two LazyColumn items per month: header, then block). Unlike week indices, it
doesn't depend on the week-start preference, so changing that reflows the rows
inside a block without moving the block or losing the scroll position.
- The sliding data window now loads months rather than weeks, widened to whole
grid weeks at both ends so a bar reaching into a block from a clipped-off day
still renders.
- `clipWeekToMonth` is the pure seam: it drops the neighbour month's pills and
counts and cuts spanning bars back to the month's own columns, keeping a flat
cap on the cut side so a bar reads as continuing past the block.
- The top bar carries the year in this style — the block's own header names the
month, so repeating it two lines up was pure duplication.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first version of this picker invented its own visual language — free-
standing bordered cards with a hand-rolled check — which matched nothing else
in the app. It is now the family's standard shape: FullScreenPicker with
PickerDescription and connected GroupedRows, tonal highlight and the shared
SelectedCheck.
Above the rows sits a live, scaled-down Month view that changes as you pick a
style. It renders the *real* grid composables rather than a drawing of them, so
the preview cannot drift from what it depicts: MonthGrid, ContinuousMonthGrid,
SplitMonthGrid and SplitDayPane are now internal rather than private, and the
sample month runs through the same layoutMonthWeeks/layoutCalendarWeek the live
views use. The month, today's position, week start, colour softening and clock
format are all real; only the events are stand-ins, since a settings screen has
no business querying the provider for a thumbnail.
Selecting applies immediately and leaves the picker open — closing on tap would
hide the very thing the screen is for. Back exits, as in the App name picker.
Scaling note worth keeping: Modifier.requiredSize looks like the way to force a
full-viewport measurement, but it *centres* content that overflows the incoming
constraints, which left only the grid's bottom-right corner inside the clip. A
layout modifier that measures at Constraints.fixed and reports the scaled size
has no overflow to align and no dependence on parent alignment.
PickerDescription is internal now rather than duplicated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Filed under Unreleased, matching where the agenda widget fix landed after the
2.16.0 section was cut. Fold into 2.16.0 if that section reopens before it
merges to main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds MonthViewStyle.Split: the month compressed to day numbers and event dots
over a list of the selected day's events.
- The app's first selected-day concept. Every other view drills straight into a
date; here a tap selects, because the pane below is already the answer to
"what's on this day" and opening a whole screen would defeat the layout. The
full Day view stays one tap away on the pane's date header, matching the Week
and Agenda headers (#37).
- The pane reuses the agenda's row vocabulary (extracted in the groundwork
commit) rather than growing a parallel set, so the two surfaces read as one
app. It needs no extra provider query: instancesByDay is already covered by
the month grid range.
- Selection follows the month — today when the new month holds it, else the 1st
— so the pane never lists a day the grid isn't showing. Tapping a leading or
trailing day follows it to its own month.
- Selection and today are different signals (tinted, outlined cell vs. the
filled circle the other views use for today), so both read when they land on
the same day.
- The swipe stays on the grid alone; the pane scrolls and is full of tappable
rows.
MonthScreen gains onEventClick, wired in CalendarHost alongside the other three
views, and MonthUiState.Success now carries its zone for the same reason the
agenda's does.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds MonthViewStyle.Continuous: one uninterrupted vertical stream of weeks
instead of month pages.
The layout unit is the ISO week, not the month. A vertical stack of month grids
would still repeat a boundary week at the end of one month and the start of the
next, which is precisely the duplication the issue asks to be rid of. Streaming
weeks means every date appears exactly once.
- Weeks are addressed by an absolute index (epoch 1900, so indices stay
non-negative and map 1:1 onto LazyColumn item indices), giving the list one
stable, gap-free coordinate space to scroll and key by.
- The view model loads a sliding window of weeks around the visible range.
nextLoadWindow() holds the hysteresis: the window only moves once the visible
range comes within four weeks of a loaded edge, so scrolling re-queries the
provider occasionally rather than every frame. Unloaded rows render a
same-height skeleton, so nothing jumps when the window catches up.
- No dimmed "other month" days — every day in the stream belongs to a month
equally. Instead the 1st names its month, which is the only marker needed to
tell one month from the next, and the top bar title tracks the month the
viewport mostly sits in.
- The horizontal swipe detector and the paged AnimatedContent are both off in
this style; vertical scrolling owns the gesture. Today and drawer jump-to-date
animate the list instead of swapping months. The view opens positioned on the
current month.
MonthWeekRow now takes inMonth as a predicate rather than a YearMonth, which is
what lets the same row serve a stream that has no enclosing month.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces MonthViewStyle{Paged, Continuous, Split} and the setting that
chooses it. Only Paged is wired up so far — the other two land next; this
commit is the pref, the state plumbing and the chooser.
One picker rather than two switches ("vertical scrolling" + "month with
agenda"): independent toggles would multiply into four combinations, most of
which nobody asked for. Split does not disable the Agenda view — that stays a
forward multi-day window, while the split pane lists one selected day.
The chooser lives on the Views settings screen (per-view layout belongs with
the other view configuration) and is a hand-rolled FullScreenPicker rather than
OptionPicker, which cannot render previews: the three options differ in shape,
which a word like "Continuous" does not convey. Each card carries a schematic
drawn from theme tokens, and selection reads three ways over — border weight,
container tint and a check — so it never rests on colour alone. The cards are a
selectableGroup with Role.RadioButton for screen readers.
Folded into the ViewCustomization holder, which had spare arity; the outer
settings combine is still at its five-flow limit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Groundwork for the month view style setting (#38, #53); no behaviour change.
- Split layoutCalendarWeek out of layoutMonthWeeks so the continuous style,
which streams weeks and has no enclosing YearMonth to slice by, lays rows
out identically. layoutMonthWeeks (also used by the month widget) keeps its
signature and becomes a loop over it.
- Add MonthUiState.Success.instancesByDay: the grid's events keyed by date and
uncapped, so the split style's day pane can list a date without a second
provider query — the month grid range already covers it.
- Move AgendaDayHeader / AgendaEmptyDayRow / AgendaEventRow and their label
helpers into AgendaRows.kt as internal, so the split pane reuses the agenda's
row vocabulary instead of growing a parallel one. AgendaEmptyDayRow takes its
text as a parameter now that it serves more than "nothing left today".
- Add the month package's first JVM tests, covering week counts, span
continuation across row boundaries, lane stacking and instancesByDay.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
floret-kit!2 (public SelectedCheck) is merged, so the submodule no longer
needs to track the feature branch. Same tree, so nothing rebuilds — this
only stops Calendula pinning a branch that is now deletable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three inconsistencies the recurrence-picker review surfaced, all of the
same kind: the redesigned picker had quietly diverged from the family.
SelectedCheck existed as five copies app-side plus a private one in the
kit, and they had drifted — the kit drew Icons.Rounded.Check, every app
copy drew Icons.Default.Check. Delete all five against the kit's now
public primitive (floret-kit!2), which settles the glyph on Rounded.
The recurrence picker was the only full-screen picker whose selected row
carried no check, relying on the tonal highlight alone. Add it to all six
rows, matching OptionPicker, reminder, agenda range, timezone, calendar
and Settings.
Its two new AnimatedVisibility blocks were the only ones in the app using
Compose's bare defaults rather than expandEnter()/collapseExit(). Those
helpers honour rememberReduceMotion(), so the weekday and count cards
were ignoring the system "remove animations" setting — an accessibility
regression, not just a style drift.
Bumps the floret-kit pointer; needs floret-kit!2 merged first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups to the picker redesign, plus the bugs it exposed.
The live read-out now renders customResult itself — the exact string OK
would save — instead of rebuilding a parallel rule with `interval ?: 1`
and `end ?: Never` fallbacks, which let it confidently describe a rule
that differed from the selected controls whenever a field was invalid.
Shrink the invalid space behind it: a blank amount field reads as its
visible placeholder (1 / 10) rather than as an error, and backing out of
the date picker falls back to "never" instead of stranding a dateless
"on a date". Only an out-of-range 0 remains invalid, and that now says so
rather than greying out OK with no cause.
UNTIL displayed the day after the one picked for zones behind UTC:
toRRule deliberately writes the end of the chosen *local* day expressed
in UTC (the provider applies UNTIL coarsely), so the read side must
convert back before taking the date. Fixes the detail screen too, and
untilLocalDate is extracted so it can be tested.
Also: hoist a remember() out of a conditional (a slot that appears and
disappears breaks positional memoisation), match GroupedSurface's 22dp
corners instead of a drifted local 20dp copy, move the cards onto
GroupedSurface, drop the segmented row's icon slot so longer unit labels
fit, and reserve two lines so the stack stops shifting as the phrase
grows with each weekday.
Extract SettingsPrefs.firstDayOfWeek(scope), replacing three copies that
had drifted onto different initialValues.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to 30fcbfa, fixing eight issues found in review:
- Cap the agenda row list at 100. SizeMode.Exact asks Glance for one
RemoteViews per host size where SizeMode.Single produced exactly one,
roughly doubling the payload; with the range reaching
AgendaRange.MAX_CUSTOM_DAYS (365) an uncapped list could push past the
binder transaction limit and the host would show "Problem loading
widget". A trailing day header stranded by the cut is dropped.
- Loosen the height cap so it only catches genuinely squashed widgets.
It previously required ~320dp of height for LARGE, so widening a widget
without also making it unusually tall — the exact resize #51 reports —
stayed REGULAR or COMPACT and the feature was near a no-op for it.
Thresholds now work on height minus header chrome.
- Lock the event stripe to the system font scale. It is a Dp beside sp
text, so at large accessibility settings the text outgrew it and it
under-ran the row it marks.
- Route the day-header and placeholder padding through the metrics table
so vertical rhythm holds at the larger tiers, and derive the text
indent from the row constants instead of a hardcoded 19dp.
- Share the bucketing as widget/WidgetScale.kt so MonthWidget (already
SizeMode.Exact) can adopt one rule rather than growing a parallel copy.
- Anchor the type ramp to Material 3 type-scale roles per CLAUDE.md, with
the two off-scale values marked and justified inline. COMPACT is
unchanged, so a default-sized widget still looks exactly as before.
- Tie the "default size unchanged" test to the provider XML's declared
3-cell band rather than one measured 222dp point.
- Hang the metrics off an ordinal-indexed table so lookup allocates
nothing per recomposition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correctness:
- Recurring writes: move the series DTSTART by the *wall-clock* shift applied
to the edited occurrence and re-resolve it in the event's zone, instead of by
a millisecond delta. The old delta baked in whichever UTC offset applied on
the edited occurrence's date, so pinning a recurring event to another zone —
or editing an occurrence on the far side of a DST boundary from the series
anchor — shifted the whole series by an hour. Also snaps the anchor to a UTC
midnight when the event becomes all-day.
- Detail card and edit form now resolve a pinned zone's abbreviation/offset at
the *event's* instant, not at "now", so a July event no longer reads
"CET · 10:00 AM" when opened in January.
- Agenda: the zone used to label multi-day rows now travels on
AgendaUiState.Success rather than a process-lifetime file-level constant, so
labelling can't disagree with the grouping after a device time-zone change.
- Week title: spell out the year when the week straddles New Year, via a new
forceYear flag on formatCalendarTitle.
Performance:
- Build the ~600-entry zone catalogue off the main thread (produceState +
Dispatchers.Default); resolve the device row's summary on its own so it still
renders complete on the first frame.
- Pre-normalize each TimeZoneOption's search keys at construction, turning
~2400 NFD normalizations per keystroke into plain prefix/substring checks.
Hoist the combining-mark Regex out of the hot path.
- Key the edit form's local-time line on the fields it reads instead of
recomputing it on every keystroke.
- Move LauncherNameManager's PackageManager binder calls off the main thread.
Cleanup:
- Drop a duplicate Public icon import and the unused
event_edit_timezone_clear string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The weekday toggles now order by the app's "Week starts on" setting
(via WeekStartPref.resolveFirstDay), matching the month/week/agenda
views, instead of always following the device locale. Threaded through
a new EventEditViewModel.firstDayOfWeek flow.
- Carry the 16dp section gaps on each block rather than a parent
spacedBy, so the weekday card's gap collapses together with the card
under AnimatedVisibility — removing the end-of-animation jump in the
ends section when switching away from Weekly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Upcoming" agenda widget used Glance's default SizeMode.Single: it was
composed once at the minimum size and the launcher stretched that single
RemoteViews when enlarged, so the text stayed small-widget-sized no matter how
big the widget grew. Reported as a "font size" request (#51), but it's really a
missing size-response.
Switch to SizeMode.Exact (like MonthWidget) and read LocalSize.current to pick
one of four tiers (COMPACT/REGULAR/LARGE/XLARGE), scaling type and row metrics.
Exact over Responsive so the ~30-day LazyColumn isn't replicated per tier.
Width picks the tier, height can only lower it. Width governs how much of a
title fits on a row, so it's what should drive type size; height only decides
how many rows are visible, so a tall narrow widget shows more events rather than
bigger text. Height does act as a cap, though, or a squashed widget would keep
the large type its width earned in a sliver of space. Thresholds are spread over
the width range a phone actually produces (measured on a Pixel/Nova: a compact
widget is 222dp wide, a large one 378dp) rather than a theoretical range, so the
tiers are reachable in practice; XLARGE is reserved for tablets/foldables.
COMPACT reproduces the original constants verbatim, so an existing widget is
visually unchanged. The tier logic lives in a pure, Glance-free AgendaScale.kt
(compose.ui.unit only) and is JVM-tested: the COMPACT baseline, the width
buckets, the height cap stepping a squashed widget down, and that height never
raises the tier.
No new setting: the widget follows the size the launcher/user already chose.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the single current-state hero + option rows with two selectable
launcher-mark cards (Calendula / Calendar), so the user sees what each choice
would look like, not just the current name. Tapping a card applies immediately
and highlights it (primary border + tinted container + check); the picker stays
open so the change is visible, and back exits — which also fixes the earlier
"hero doesn't update until reopened" gap.
Built on FullScreenPicker directly (the component OptionPicker wraps) to render
the custom card row; material-3 tokens throughout (surfaceContainerHigh /
primary / primaryContainer / outlineVariant, 24dp corners, 8dp-grid spacing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a preview hero to the App name picker header: the app's launcher mark over
the current name on a tonal surface card, the explanatory hint below it, then
generous space before the choices. Gives the picker the visual "display" the
inline hint lacked and separates the text from the options.
Follows the material-3 guidance — tonal surfaceContainerHigh card (no shadow),
28dp corner, 8dp-grid spacing, onSurface/onSurfaceVariant role pairing — and
reuses the onboarding BrandHero's squircle reconstruction of the adaptive icon
so it renders identically everywhere.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the inline App name switch with a GroupedRow that opens a full-screen
OptionPicker (Calendula / Calendar), matching the app's other "choose one"
settings and the ReFra pattern the user preferred. The row shows the current
name; the picker's header carries the explanatory + icon-may-move hint. Leaves
room for more launcher names later without redesigning the row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild the custom step of the recurrence picker so complex-but-supported
rules (e.g. "every 2 weeks on Mon+Tue") are discoverable and legible,
rather than buried in a ragged stack of mismatched controls.
- Add a live, human-readable summary of the rule as it is built, via the
existing recurrenceText humanizer.
- Group interval + frequency into one tonal card; the frequency is now a
SingleChoiceSegmentedButtonRow (all four units visible) instead of a
dropdown.
- Reveal the weekday picks with AnimatedVisibility and house them, the
"every" controls, and the ends group in consistent 16dp-inset grouped
cards; the count field folds into the bottom of the ends run.
No behaviour, domain, or RRULE changes: parse/render logic and the set of
expressible rules are unchanged. The EXDATE half of #42 is deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Settings → Appearance toggle that switches the app's launcher label
between "Calendula" and "Calendar", for users on launchers that can't rename
apps themselves.
The launcher entry moves off MainActivity onto two <activity-alias> components
(DefaultNameAlias / CalendarNameAlias); exactly one is enabled at a time via
PackageManager.setComponentEnabledSetting. MainActivity keeps every other intent
filter; the android.app.shortcuts meta-data moves onto both aliases so the
long-press shortcut still publishes. Component-enabled state is the single source
of truth — no persisted preference.
The ComponentName uses the applicationId for the package (carrying the
.debug/.releasetest suffix) and the namespace for the class, since manifest
".Alias" names resolve against the namespace; switching to Calendar enables the
target alias before disabling the other to avoid a zero-entry launcher transient.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The agenda window starts at the anchor's local midnight, which east of UTC
is the previous day's 22:00 UTC. All-day events live at UTC midnights with an
exclusive next-midnight end, so yesterday's all-day event (a birthday, say)
still overlaps today's window start and is returned by the provider.
groupAgendaDays then clamped its first day up to the anchor and, via a trailing
coerceAtLeast(firstDay) on the last day, pulled it onto the anchor's "today"
section — the multi-day fix only stopped the forward leak on interior days.
Drop instances whose true last day (resolved in UTC for all-day events) falls
before the anchor, or whose first day falls past the window end: they occupy no
visible day and must not be clamped onto an edge. Adds eastern-zone regression
tests for both edges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the today icon-button block that was copy-pasted into all four
calendar top bars (Week/Month/Day/Agenda) into one TodayAction composable in
ui/common. No behaviour change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Today button in toolbar" appearance toggle (default off). When on,
each calendar view (month/week/day/agenda) shows a persistent go-to-today
icon button in its top bar instead of the fade-in extended FAB, and the
FAB pill is suppressed. Same jump action, just always present.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-review follow-up.
- The multi-day expansion in groupAgendaDays is shared with the agenda
widget, but only the screen's summary was made day-aware — so the
widget rendered the raw "start – end" on every spanned day, the very
bug the screen fix cured. Hoist a pure agendaTimeLabel(event, day,
zone) into the shared agenda layer and resolve strings from it in both
the screen and the widget, so they label identically. (findings 1, 2)
- groupAgendaDays could silently drop an instance whose clamped span was
empty (firstDay > lastDay); floor lastDay at firstDay so a returned
instance always surfaces on at least its first visible day. (finding 3)
- agendaTimeLabel resolves the span days once instead of the summary
recomputing them 2–3× per row. (finding 5)
Finding 4 (within-day sort) needs no change: sorting by absolute start
already places a still-running multi-day event at the top of each day it
continues into, which is chronologically correct (it is ongoing from
that day's midnight), and at its real start time on its first day.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-up. spanFirstDay/spanLastDay resolved every event in the
device zone, but all-day events live at UTC midnights with an exclusive
end — east of UTC (e.g. Europe/Berlin) that pushed spanLastDay onto the
next day, so a single-day all-day event reported spansMultipleDays and
leaked onto a second agenda day. Resolve all-day dates in UTC, matching
the Week view and detail card. Adds eastern-zone regression tests that
the prior UTC-only tests could not catch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "→" arrow convention read as unclear. Spell each day out instead:
the first day names the start ("Starts 14:00"), the last day the end
("Ends 10:00"), and whole days in between read as "All day". All-day
multi-day events stay "All day" on every day. Single-day rows unchanged.
Adds agenda_span_starts / agenda_span_ends (owes Weblate backfill).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-day event repeated the same "start – end" on every day it spans
("14:00 – 14:00"), which reads as meaningless. Show only the part
relevant to each day, with a "→" marking that it carries past the day's
boundary: the first day shows the start ("14:00 →"), the last day the
end ("→ 10:00"), and whole days in between an all-day span arriving from
and continuing into their neighbours ("→ All day →"). Single-day rows are
unchanged.
Factors the span first/last-day resolution into shared EventInstance
helpers reused by groupAgendaDays and the label.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-day event now appears under every day it spans, so keying its
row by instanceId alone repeated the key across days and crashed the
LazyColumn ("Key already used") on scroll. Scope the key by day.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Search matched city, id, and the long localized name but not the
abbreviation, so typing "CEST" found nothing. Match it too: an exact
abbreviation hit ranks just under a city prefix, so typing an abbreviation
gathers every zone that shows it (all the CEST zones at once). It matches
the region-resolved abbreviation — i.e. exactly what the row displays.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>