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>
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>
CalendarRepositoryImpl gained a SettingsPrefs parameter, but this
instrumented smoke test still called the three-arg constructor — so the
whole androidTest source set failed to compile. Pass a SettingsPrefs
built on the same DataStore.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
groupAgendaDays keyed each instance by its start day alone, so a
multi-day event surfaced only on its first day and vanished from the
rest of its span in both the Agenda screen and the agenda widget.
Expand each instance across every day from its start (clamped to the
anchor for ongoing events) through its last occupied day, bounded by the
visible window end. An event ending exactly at midnight — including the
exclusive next-midnight all-day events end at — does not reach that
boundary day.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
java.util gave abbreviations only for zones in the display locale's region:
an en-DE phone saw "CEST" for Berlin but "GMT-4" for New York, and an
en-US phone the reverse — ICU only surfaces the short names commonly used
where the reader is. Verified on-device (en-DE): US zones fell back to the
offset, EU ones resolved.
Ask ICU in the display language but the *zone's* region instead — New York
in en-US, Berlin in en-DE — using android.icu's zone→region map. On-device
that lights up EDT/PDT/CDT, plus BST, AEST, IST, JST that showed only the
offset before. Where a region still has no name (Athens in en-GR) the
device locale sometimes does, so fall back to it, then to the offset.
The region lookup needs android.icu, which domain/ can't import, so it's
injected: timeZoneOptions takes a regionOf lambda (default none, keeping
the module pure and JVM-tested), and the UI passes icuTimeZoneRegion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The abbreviation came from java.time's "zzz" formatter, which on Android
has no short specific-zone names and silently degrades every zone to a
"GMT-4" form — so on device New York showed "GMT-04:00" instead of "EDT".
Desktop couldn't catch it: there "zzz" and java.util.TimeZone agree, and
that agreement is the whole trap.
Resolve through java.util.TimeZone.getDisplayName(inDst, SHORT, locale)
instead — ICU-backed, so it returns the real abbreviation on both Android
and the JVM. Zones with no named abbreviation still fall back to a GMT
form, which zoneDescriptor already collapses to a single token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Central European Time (Europe/Berlin)" is too wide — it made the zone
field wrap and stretch. Lead with the id ("Europe/Berlin") and follow it
with the abbreviation instead: "CEST · GMT+02:00" in the picker and edit
card, "CET · 8:00 AM – 9:00 AM" on the detail card (abbreviation + the
event's own-zone time).
The abbreviation is resolved DST-aware at the same instant as the offset
(CET vs CEST) via "zzz"; zones with no named abbreviation fall back to a
"GMT+05:30" form, and zoneDescriptor drops the separate offset in that
case so it isn't stated twice. The long localized name is kept on the
option for search only — typing "pacific" still works — but no longer
shown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The When card reads date-large, time-small-beneath. The zone card led
with the original time at titleMedium and dropped the zone label to
bodyMedium, inverting that — which made the foreign time the loudest
thing on the screen and pulled attention off the local time the reader
actually acts on.
Put the label back on top and the original time small beneath it, so both
cards read the same way and the original stays available without
competing. The label already names the zone, so the range drops its "in
New York" tail (and the string with it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two review fixes.
The zone picker's search box was a raw Material OutlinedTextField — the
only one left in the app, and against the convention DialogControls
states outright ("the family's InlineTextField over a tonal surface, not
Material's outlined field"). Rebuild it on InlineTextField over a tonal
surface, with the clear button inside the surface since the picker's top
bar is the title rather than a search field.
Showing a pinned event only in its own zone answered "what was it set
to?" while dropping "when is it for me?" — the user had to do the offset
arithmetic. Show both whenever they differ:
- the edit form keeps editing the event in its own zone (that's the time
it was set at) and captions it with the local equivalent;
- the detail screen keeps local times primary and now leads the zone card
with the original ("8:00 AM – 9:00 AM in New York") instead of naming
the zone and nothing else.
EventForm.timesIn is pure, so the conversion — including crossing the
date line and each zone's own DST, which don't move together — is a
plain JUnit test rather than something only reviewable on a phone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the zone as an optional form field, right after the time fields
it qualifies: hidden on ordinary events, revealed automatically when the
event already carries a foreign zone, and withheld entirely while all-day
is on (a date-anchored event has no zone to show).
The picker is a full-screen one per the app's convention, but it can't
reuse OptionPicker: that composes every option eagerly, and ~600 zones
would all compose on open. It drives its own LazyColumn instead, which
needs the kit's new `scrollable = false` — the scaffold's own
verticalScroll would otherwise throw on a nested same-axis scrollable.
The device zone and recently-picked zones pin to the top so the common
case needs no typing; search is accent- and case-insensitive.
Recents persist in DataStore, capped at five, dropping ids the tz
database no longer knows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every write sampled ZoneId.systemDefault() and stamped it into
EVENT_TIMEZONE, so the column was real but only ever held the device's
zone: an event synced from elsewhere could be read in its zone, never
authored in one.
Give EventForm a nullable `timezone`, where null keeps meaning "the
device zone at save time" — so every existing call site behaves exactly
as before — and a non-null value pins the event to a zone it then tracks
across DST. toWriteTimes resolves the form's zone ahead of the device's;
toEditForm pins only when the stored zone differs from the device's, and
prefills such an event in its own zone so the form shows the wall-clock
the event actually means.
Two provider-contract bugs fall out of this:
- Editing the time of a foreign-zone event rewrote EVENT_TIMEZONE to the
device's. The instants stayed right, so nothing looked wrong, but the
event silently stopped tracking its zone and would drift an hour at the
next DST boundary. Only the timesChanged gate spared title-only edits.
- A zone change with an untouched wall-clock is still a time change (the
same 09:00 elsewhere is a different instant), so it now trips
timesChanged and rewrites DTSTART instead of being dropped.
All-day events keep carrying no zone at all: they're date-anchored, and
the UTC midnights they normalise to are an anchor rather than a location.
TimeZoneCatalog is pure JVM so the search ranking and DST-aware offsets
stay plain JUnit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Locale-aware calendar titles (#60): Month, Week and Day move onto the shared
formatter Agenda already used, the year drops out while you're in the current
one, and the Week title names its month rather than restating the day numbers
printed directly below it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Month, Week and Day each hand-rolled their title from a German-style template —
"$weekday, ${date.day}. $monthName ${date.year}" — so every language got a
trailing ordinal dot and day-before-month order, including ones that write the
month first. English read "Fri, 17. Jul 2026" where it should read
"Fri, Jul 17, 2026".
396a561 already fixed exactly this, but only for Agenda: it added
localizedDateFormatter and migrated the four agenda files, leaving Month, Week,
Day and MonthWidget on their originals. Day and Agenda therefore disagreed about
the same date. All four now route through formatCalendarTitle, so there is one
place left that decides how a title reads.
Two behaviour changes fall out of the reporter's point that the bar wastes space:
The year is dropped while you are in the current year. The title sits above a
grid that already says which year it is; the year's absence is itself the signal
that you are in the current one, and it reappears when you page out — the moment
it starts carrying information. Since a skeleton is a field list, this is just
appending "y", and the locale still places it.
The Week title names a month instead of a day range. "24. Jun – 31. Jun" restated
the day numbers printed in the column headers directly below, in the widest
string in the bar. Naming weekStart's month keeps a straddling week on the
outgoing month until it is fully gone — a week is seven contiguous days, so the
earlier month has a day in it exactly while weekStart is inside it. No straddle
conditional, and the title depends on nothing but weekStart, so it cannot drift
with the direction you paged in from.
currentLocale/localizedDateFormatter move to floret-kit's core-locale (neither is
calendar-specific); LocaleSupport.kt goes away and its 11 callers repoint.
MonthWidget keeps Locale.getDefault() — Glance has no LocalConfiguration and the
widget re-renders on a configuration change, matching AgendaWidget.
Does not touch the FAB stack the issue opens with: the buttons overlaying content
is intentional and matches Google Calendar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the Unreleased changelog under [2.15.0] — 2026-07-15, bump
versionName/versionCode to 2.15.0/21500, and sync the F-Droid
per-version changelog.
Milestone 2.15.0 (all integrated): #21#35#36#39#40#46#52.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Soften calendar colours" setting (Settings → Design, default on).
Turning it off paints calendar and event colours raw, exactly as the sync
source (DAVx5/CalDAV) publishes them, instead of the theme-fitting pastels
(#36).
Event/calendar colours now flow through shared eventFill()/eventInk()
helpers gated by a LocalSoftenColors composition local (widgets read the
pref directly). Event titles pick black or white text by the fill's WCAG
relative luminance, so a dark colour stays legible whether softened or raw
(#21) — previously always near-black.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both landed as community translations through Weblate (French ~33%, Polish
~57%), above the bar already shipped for zh-CN. Add them to locales_config.xml —
the single source of truth for the in-app language picker and Android's per-app
language settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The day view's 48dp edge-to-edge hour gutter centred its labels at 24dp, 4dp
left of the hamburger. Give the gutter content the same 8dp start inset as the
week view so the labels centre on the hamburger (4dp bar inset + 24dp half icon).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The month grid was inset 4dp while its weekday header (and the loading grid) used
8dp, so the week-number column — and the day cells under their labels — sat 4dp
left of where they should. Bring the grid to 8dp: the gutter centre now lands on
the hamburger (4dp bar inset + 24dp half icon), and day cells sit under their
weekday labels.
The week view's header badge and hour labels had the same drift (a 48dp edge-to-
edge gutter centres its content at 24dp, not 28dp). Its top section background
bleeds full-width when scrolled, so instead of insetting the whole content, give
just the gutter content an 8dp start inset to centre it on the hamburger too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agenda widget's event rows are plain (a colour stripe + text, no card), so
the rounded "No more events today" surface looked out of place. Render it as a
muted line indented to the event titles instead, matching the widget's style.
The in-app agenda keeps its coffee-cup card, where event rows are cards too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>