Files
floret-kit/CHANGELOG.md
T

422 lines
24 KiB
Markdown

# Changelog
All notable changes to floret-kit are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/).
floret-kit is consumed **from source** — each app embeds it as a git submodule
and builds it through a Gradle composite build, pinning a specific commit. There
are no binary releases; "version" tracks the shared `version` in the root build
(currently `0.7.0`). Entries are grouped by module.
## [Unreleased]
### Fixed
- **`core-locale`** — picking a language did nothing on Android 13+. AppCompat's
`setApplicationLocales` only reaches the platform through a live
`AppCompatActivity`, and every app uses `ComponentActivity`. `AppLanguage`
now calls `LocaleManager` directly there: `apply(context, tag)` and
`currentTag(context[, supported])`. The context-free versions are deprecated.
`LanguagePickerRow` uses the new ones; apps that call `AppLanguage` themselves
(Agendula's settings row) need to pass a context.
### Changed
- **Build** — dependencies to the newest stable releases: AGP 9.4.1, Kotlin
2.4.20, Gradle 9.8.0, Compose BOM 2026.09.00, core-ktx 1.19.1, appcompat
1.8.0, JUnit 6.1.3; material3 to `1.5.0-alpha29` (still no stable 1.5 with the
Expressive APIs). AGP must match across a composite build, so a consuming app
re-pinning to this needs AGP 9.4.1 itself.
### Added
- **`components`** — picker-row helpers lifted from Calendula's pickers and
settings, all additive. `GroupedRow` takes `checked` (default `false`): it
implies `selected` and fills an empty trailing slot with `SelectedCheck`,
replacing the hand-written `if (selected) { { SelectedCheck() } } else null`.
`OptionPicker` takes `dismissOnSelect` (default `true`, today's behaviour) for
preview pickers that stay open, and its rows are public as `OptionRows` for a
picker that composes its own `FullScreenPicker`. `CustomExpandRow` is the
"Custom…" row of a presets-plus-custom picker, revealing its editor
(typically `CustomAmountEditor`) as one connected container.
`GroupedSwitchRow` is the settings toggle row where the whole row flips the
switch. `GroupedHint` is the muted text beside the cards (settings hints,
picker descriptions), and `ExpandIcon` the turning chevron, now also used by
`InstructionSteps`.
- **`components`** — `IconInfoCard`, the tonal icon-plus-content card behind
Calendula's event detail and edit screens (optionally clickable, a top-aligned
icon for multi-line fields, a `header` line the icon centres on).
- **`components`** — `OnboardingScaffold` takes `headline` and `supporting`,
the centred title and muted explanation most steps open `body` with; `body`
now defaults to empty. `OnboardingPrimaryButton` is the full-width 56dp
call-to-action, with an optional trailing icon. Existing calls are unchanged.
- **`components`** — `LanguagePickerRow` takes `header` and `predictiveBack`,
forwarded to its `OptionPicker`, so an app with a "help translate" row above
the languages can drop its own copy of the row. Both default to today's
behaviour.
- **`core-locale`** — `relativeTimeAgo(epochMillis, nowMillis, minResolution)`,
the platform-localised "5 minutes ago" Calendula's backup and special-dates
status lines build through `DateUtils.getRelativeTimeSpanString`.
- **`identity`** — a pure-black (AMOLED) dark theme. `ColorScheme.toPureBlack()`
sets `background`, `surface`, `surfaceDim` and `surfaceContainerLowest` to
#000 and pulls the other containers down towards black, keeping their hue
tint, so grid tiles drawn as containers over `surface` still show their
gaps. `FloretExpressiveTheme` gains `pureBlack` (default `false`), which
applies it to the dark scheme, dynamic or app-supplied.
- **`core-prefs`** — `ThemeMode.BLACK`, dark with pure-black surfaces.
`isDark` treats it as dark, and the new `ThemeMode.isPureBlack` is what an
app passes to the theme's `pureBlack`. It's stored as `"BLACK"` under the
same `theme_mode` key; a build without it reads that as `SYSTEM`.
- **`glance`** (new module) — the family's shared home-screen-widget plumbing
and M3 Expressive widget vocabulary, built on Jetpack Glance:
`FloretGlanceTheme` (dynamic colour on API 31+, app-supplied fallback
schemes otherwise, a per-widget `WidgetThemeMode` override), `WidgetSurface`
(solid/tinted/transparent root backgrounds with the system corner radius),
`WidgetHeader` + `WidgetActionButton` (a 48dp tonal/filled pill, extendable
with a label), `WidgetGroupedRow` (the Glance counterpart of `components`'
`GroupedSurface`, built from four position-shaped drawables since Glance's
`cornerRadius` has no per-corner overload), `WidgetMessage`, and the
`WidgetRollover` + `FloretWidgetUpdateReceiver` midnight-rollover plumbing.
Converged from Agendula's widget package, written for the M3 Expressive
widget pass (calendula#71); Calendula adopts it when its own widget overhaul
lands.
## [0.7.0] - 2026-09-12
### Added
- **`components`** — `CollapsingScaffold` takes `floatingActionButton` and
`floatingActionButtonPosition`, forwarded to the underlying `Scaffold`. Both
default to the bare scaffold, so every existing call site in every app is
unchanged, and they sit after `snackbarHost` so no positional call shifts.
The driver is Clockula's alarms tab: three of its four tabs need an "add"
affordance on a scaffold the kit owns, and the alternative — a `Box` wrapping
the scaffold with a bottom-end FAB — re-derives the window-inset and
position maths `Scaffold` already does, once per tab and again in every
sibling app.
## [0.6.0] - 2026-09-11
### Changed
- **`components`** — `CollapsingScaffold`'s `onBack` is now nullable and
defaults to null. A null `onBack` draws no navigation icon and wires no
predictive-back handler; every existing call site, which passes a lambda,
is unchanged. The driver is Clockula's four tabs: a top-level destination
wants exactly this scaffold — the large title that shrinks into the bar over
a scrolling column — and nothing behind it to go back to, and a back arrow
that pops nothing is worse than no arrow at all. Cheaper and truer than each
app hand-rolling a second, arrow-less scaffold beside this one.
## [0.5.0] - 2026-09-11
### Added
- **`core-prefs`** — the family's typed DataStore layer, extracted now that
Clockula is the third app that would have written it again. `Pref<T>` is one
preference as data — its on-disk key name, its default and its codec — built
by `booleanPref`/`intPref`/`longPref`/`floatPref`/`stringPref`/`stringSetPref`,
the nullable pair (`nullableStringPref`/`nullableLongPref`, where writing
`null` *removes* the key rather than storing a sentinel), `enumPref`, and
`map()` for a view of the same slot as another type. `PrefStore` wraps a
`DataStore<Preferences>` with `get`/`set`/`update`/`clear`/`clearAll`, a raw
`edit` escape hatch, and `flow(pref)` — which is `distinctUntilChanged`, so an
unrelated key's write no longer re-emits every screen's setting, the bug both
siblings' hand-rolled `data.map { … }` carried. `String?.toEnum(default)` is
the read path for every enum-valued preference: unknown, empty and wrong-case
values degrade to the default instead of throwing. Also the shared appearance
slice — `ThemeMode` (with `isDark(systemDark)`, the `when` block that was
duplicated in both siblings' `MainActivity`), `Appearance` and
`AppearancePrefs`. The stored key names are byte-identical to what Calendula
and Agendula already write (`theme_mode`, `dynamic_color`), so adopting the
module is a code change with **no data migration**. The store's *name* and its
construction stay with the app. Consumed by Clockula.
- **`core-di`** — the family's shared DI vocabulary: the `@IoDispatcher`,
`@DefaultDispatcher` and `@ApplicationScope` qualifiers and the
`CoroutinesModule` that provides them (the application scope on a
`SupervisorJob`, so one failing collector can't take its siblings down). Every
app in the family had re-declared the same qualifier and the same provider.
The kit's only module that knows about Dagger, and its only one carrying an
annotation processor — a `@Module @InstallIn(...)` reaches a consuming app's
component only if the library itself is run through `hilt-compiler`. Consumed
by Clockula.
## [0.4.0] - 2026-09-24
### Added
- **`components`** — `GroupedSectionHeader`, the label above a run of
`GroupedRow`s: small, `primary`, inset to `GroupedListInset` so it shares the
cards' left margin. Converged from Calendula's settings hub, where the recipe
was written as a private `SectionHeader`, and now Agendula's, which had grown
a flat run of a dozen rows with nothing separating how the app behaves from
what it is.
- **`components`** — `GroupedRow` takes a `contentColor`, the pair to the
existing `container`: a row given a tint now names the `on*` role that goes
with it instead of leaving its text on `onSurface`, which only happens to
read on a container the theme did not pick. Wanted by a status row that
colours itself by outcome (Agendula's export receipt: `secondaryContainer` on
success, `errorContainer` on failure).
- **`components`** — `ScaledViewPreview` and `ViewPreviewFrame`, the preview
picker's two halves: the first measures real screen composables at a phone
viewport and scales the finished layout down (swallowing touches and
collapsing semantics, so a preview reads as a picture rather than a control),
the second is the tonal plate it sits on, cross-fading between options and
taking `GroupedListInset` and the grouped rows' outer radius so preview and
options read as one stack. Converged from Calendula's view/month-style
choosers, where the pattern was invented, and now Agendula's onboarding step
for the quick-add bar; the viewport height the app scales from is a parameter
rather than a calendar-shaped constant.
- **`components`** — `InstructionSteps`, a numbered, connected list for a
procedure the user carries out somewhere else. Collapsible, so it can come
back as a reference once it has had a screen of its own.
- **`components`** — `InlineTextField` takes a `visualTransformation`, so a
password can be masked. `KeyboardType.Password` only drops IME suggestions;
without this the field rendered the secret in the clear.
- **`components`** — `ReorderableColumn` takes a `rowHeight` (default
`ReorderableRowHeight`), so rows keep their normal height while reordering.
The drag shadow now follows the card's shape, and a row resists being dragged
past either end.
- **`core-time`** — `formatTime`, `formatDateTime` and `formatDateTimeCompact`
take an optional `is24Hour`, for an app-level 12/24-hour setting. `null`
keeps following the system.
### Fixed
- **`identity`** — A committed predictive back no longer snaps the surface back
to full size before it leaves. `Modifier.predictiveBack()` now holds the
preview transform after `onBack`, and the new `predictiveBackExit()` exit
fades and keeps shrinking from there; pair them on the surface's
`AnimatedVisibility` instead of a full-size slide-out. Pass that
`AnimatedVisibility`'s `visible` to `predictiveBack(visible = …)` (or
`CollapsingScaffold(visible = …)`) so the preview is dropped as soon as the
surface is shown again — reopened mid-exit it no longer appears shrunk — and
settles back when a back leaves it in place. Without it the preview is held
for a fixed second.
- **`core-crash`** — The prefilled crash issue is filed in English again. Its
title and body came from string resources, so a crash on a French phone
opened the tracker with a French issue — unreadable to whoever has to triage
it. That text is now fixed English in `CrashReportSubmit`, out of resources
altogether, and `CrashConfig` no longer takes an `issueTitle` (apps drop the
argument, and their own `crash_report_issue_title` / `crash_report_body_*`
strings with it). Everything the reporter *sees* — the dialog, the toasts,
the clipboard label — stays translated.
## [0.3.0] - 2026-08-10
### Added
- **`components`** — `OnboardingScaffold`, the shared shell for a first-run
flow: hero, body and a call-to-action stack, so the family's onboarding
screens read as one family while keeping their own mark. A multi-step flow
passes `progress`, which `OnboardingProgress` draws as one capsule per step
with the caller's own localised label, and `navigationIcon` for a step that
can be stepped back from — the navigation slot's width is reserved whether or
not it is filled and mirrored on the other side, so the progress stays centred
as steps come and go. `topSpacing` gives a step whose hero is a large picture
rather than a mark room to breathe; `contentPadding` is the scrolling column's
horizontal inset, which a step built from grouped rows passes 0 for since
those carry their own; and `scrollingActions` moves the actions off the bottom
bar onto the end of the content, for a step taller than the screen where a
pinned bar eats the room the content needs and hides that there is more below.
`BenefitRow` is the icon/title/body line those screens list their points with.
- **`components`** — `SnackChip`, the family's compact confirmation: a
content-sized pill sliding up from the bottom with an optional trailing action,
for the case where the surface behind the confirmation *is* the confirmation
(a list that just lost a row, a calendar that just moved an event) and a
full-width snackbar would cover it. Placement and timing stay with the caller —
the bottom inset to clear differs per screen, and what an expiring undo window
commits is the app's business. Converged from Agendula's swipe-delete undo chip
and Calendula's drag-to-reschedule confirmation; the shared version keeps the
polite live region so the chip is announced, takes a `maxWidth` so a message
can't run into a bottom-end FAB, and takes `color`/`contentColor` so an app can
put it on the FAB's own fill. A message replaced in place crossfades and the
pill resizes with it, so an answered confirmation ("Moved" → "Move undone")
reads as one chip changing rather than two chips swapping.
- **`components`** — `GroupedRow` takes an `AnnotatedString` title and summary
as well as plain `String`s, so a row can carry its own emphasis: marking the
run a search matched, without every caller having to rebuild the row. The
`String` overload delegates to it, so existing callers are unchanged.
- **`components`** — `GroupedSurface`/`GroupedRow` take an `onLongClick` (with
the platform long-press haptic), for a list that enters a selection mode from
a row.
- **`components`** — `GroupedListInset`, the 16dp a grouped list keeps from the
screen edge, exported. It was a private literal, so everything sitting outside
the cards but belonging to them — section headers, hints, picker descriptions —
repeated the number and had drifted to 24dp in places, leaving pages with two
ragged left margins.
- **`components`** — `OptionPicker` takes a per-option summary, put under the
label, for a row that names something it doesn't spell out (a range's dates, a
reminder's firing time, what "follow the system" resolves to today). Every
option then shows its effect at once and stays comparable, without the cost of
a live preview. The KDoc states the three rungs of picker richness — plain
rows / per-option summary / live preview — and the rule that a preview picker
must not close on selection, since closing hides the thing it exists to show.
- **`components`** — `SelectedCheck`, the trailing check that marks a chosen row
in a full-screen picker, made public. It was private to `OptionPicker`, so
every consumer hand-rolled a copy — Calendula alone carried five, all drawing
a different check than the kit's.
- **`components`** — `AboutCard` takes a `Position` and renders through
`GroupedSurface`, so it can be the top of a group with rows continuing
underneath rather than only standing alone. Defaults to `Position.Alone`.
### Fixed
- **`core-time`** — all-day values rendered a day early west of UTC. All-day
items are date-only in iCalendar and stored as UTC midnight of their calendar
date, but `formatDate`/`formatDateTime`/`formatDateTimeCompact` resolved every
instant against `ZoneId.systemDefault()`. `formatDate` now takes an `allDay`
flag (default `false`, so existing call sites are unaffected) and resolves
those against UTC; callers must pass it at the call site.
- **`components`** — full-screen picker status/navigation-bar icons matched the
XML theme rather than the running one. A Compose `Dialog` owns its own window,
seeded from the XML theme, so a picker drawing under the bars showed dark icons
on a dark surface. `FloretExpressiveTheme` now exposes its resolved light/dark
decision as `LocalFloretDarkTheme` and the dialog window's bar appearance
follows it, tracking the in-app choice even when it diverges from the system
night qualifier.
## [0.2.1] - 2026-07-17
### Added
- **`components`** — `CollapsingScaffold` takes a `scrollable` flag (default
`true`, so every existing caller is unchanged). Set it to `false` when the
content brings its own scrolling container: the scaffold's own
`verticalScroll` composes each row up front and can't host a `LazyColumn`
(nesting a same-axis scrollable throws), which ruled out any surface with more
options than fit an eager pass. Opting out hands the content a plain column and
leaves the insets, IME padding and top-bar collapse as they were.
`FullScreenPicker` forwards the flag. Consumed by Calendula's ~600-entry
time-zone picker from 2.16.0.
## [0.2.0] - 2026-07-17
### Added
- **`components`** — `CustomAmountEditor`, the amount + unit editor behind an
"Custom…" picker option (Calendula's custom snooze duration). Consumed by
Calendula since 2.15.0.
- **`core-locale`** — the locale-aware formatting half of the module, so apps
stop hand-rolling `"$day. $month $year"` templates that bake one language's
field order (and its separators, and its ordinal dot) into every language they
ship:
- `localizedDateFormatter(locale, skeleton)` — a `DateTimeFormatter` for a
field skeleton ("LLLLy", "dMMMy", "EEEdMMM") laid out in the locale's own
order, via Android's best-pattern matching. Drop a field from the skeleton to
drop it from the output — the locale still supplies the arrangement.
- `currentLocale()` — the display locale, observed from `LocalConfiguration`.
Since `AppLanguage.apply()` changes the language inside a running process, a
locale captured once goes stale; this keys recomposition to the change.
### Changed
- **`core-locale`** now carries Compose (runtime + ui only) for `currentLocale()`.
The module previously declared itself Compose-free; applying a language
mid-process is its own API, so the hook that makes the UI notice belongs beside
it rather than in a Compose module depending back on this one. `core-crash` is
the precedent for a `core-*` module carrying Compose.
### Known gaps
- `core-time`'s `Instant.formatDate()` family now overlaps `core-locale`. Its
private `dateNoYearFormatter` strips the year by regex-ing the pattern string,
which `localizedDateFormatter(locale, "dMMM")` does properly; and its
formatters resolve `Locale.getDefault()` once at class-init ("locale changes
within a running process are rare enough to ignore"), which `AppLanguage.apply()`
makes untrue. Left alone for now — it is unused by Calendula but Agendula-visible.
Reconcile when Agendula next re-pins.
## [0.1.0] - 2026-07-07
### Added
- **`core-locale`** (Android library) — per-app language plumbing (`AppLanguage`):
read the shipped languages from the app's `res/xml/locales_config.xml`, get/set
the applied language via `AppCompatDelegate`, render each language's autonym.
The app passes its own `locales_config` resource id; the system-default choice
is `null`. _(Shipped in 0.1.0 but omitted from this changelog at the time;
recorded here retroactively.)_
- **`core-time`** (JVM library) — pure-Kotlin date/time helpers, no Android and
no third-party dependencies:
- `DayWindow.today()` — local-day boundaries for smart-list logic.
- `Instant.formatDate()` / `formatTime()` / `formatDateTime()` — locale- and
zone-aware display formatting over `kotlin.time.Instant`.
- `TimeBridge` — `Long` epoch-millis ↔ `Instant`, for content-provider I/O.
- `LocalDate.isoWeekNumber()` — ISO-8601 week-of-week-based-year, the
calendar-week number for week/month grids (one scheme, shared).
- **`core-crash`** (Android library) — privacy-respecting, on-device crash
capture and reporting. Writes an allowlist-only report (app/Android/device
version, locale, time, stack trace — nothing else) to private storage and
chains to the platform handler; uploads nothing. Includes startup crash-loop
detection, a report dialog that shows the full text before it leaves the
device, and an issue-tracker hand-off. Per-app specifics (display name, issue
URLs) come from a `CrashConfig` supplied at `install()`; each app keeps its
own thin themed `CrashReportActivity`.
- **`identity`** (Android library) — the family's Material 3 Expressive theme
factory `FloretExpressiveTheme(lightScheme, darkScheme, …)` (dynamic colour on
API 31+, system light/dark fallback, the standard motion scheme) plus the
shared motion helpers — `rememberNavSlideSpec()`, the content transitions,
`Modifier.predictiveBack()` and `Modifier.animateContentSizeMotion()` (eases a
block's own height changes, e.g. a text field wrapping to a new line), each
reduced-motion-aware. Each app passes its own seed-derived colour schemes
and typography — the mechanics are shared, the look is not.
- **`components`** (Android library) — the shared Compose component vocabulary:
- `GroupedSurface` / `GroupedRow` + `Position` / `positionOf` — the canonical
press-morphing tonal grouped-row primitive (the Android-15 settings pattern).
- `InlineTextField` — the borderless tonal text input (capitalization is a
parameter).
- `OptionCard` — the sanctioned selection-dialog pick.
- `CollapsingScaffold` — the collapsing settings/sub-screen scaffold; set
`largeTopBar = false` for a pinned single-line bar (the picker variant).
- `FullScreenPicker` / `OptionPicker` — the full-screen single-select picker,
now over a pinned single-line bar (a picker is a short list, so no tall
collapsing header to scroll past). `FullScreenPicker` takes an optional
`actions` slot (app-bar trailing items) for pickers that carry a commit or
extra action — a custom-value **Add**/**OK**, a **Reset**.
- `ReorderableColumn` — dependency-free drag-to-reorder for the short, fixed
grouped-card Settings lists; pairs with `GroupedRow(gapBelow = false)`, which
hands the column uniform control of row spacing.
- `DebugRibbon` — a stark, un-themed "DEBUG" corner ribbon; gate on
`BuildConfig.DEBUG` at the call site.
- `InlineTextField` gained `enabled` (a disabled field dims to a locked value).
- `pastelize()` — softens a raw provider colour to a theme-fitting pastel.
App-agnostic by design: apps compose their own screens and keep their own
identity chips/icons. String resources here (e.g. `back`) are fallbacks an app
can override.
- **`core-reminders`** (JVM library) — the app-agnostic reminder-override model:
`ReminderOverride` (`Inherit` / `None` / `Minutes`), where `Minutes` carries a
**list** of lead-times so a target can hold several reminders at once (an app
that offers a single reminder just uses a one-element list). Map helpers
(`reminderOverrideFor`, `reminderLeadsFor`, `applyReminderOverride`,
`reminderOverrideForMinutes`, `normalizeReminders`) plus `ReminderOverrideCodec`
(three configurable separators; single legacy values round-trip byte-identically)
and the lead-time unit model (`ReminderUnit`, `decomposeReminderMinutes`). The
labels/presets and the alarm scheduler stay per-app.
### Conventions
- **No `foojay` toolchain resolver** anywhere — it can fetch a JDK at build time,
which reproducible / official F-Droid builds reject. Modules set `jvmTarget`
directly. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
- AGP 9.x provides built-in Kotlin compilation, so Android modules apply only the
Android library + Compose plugins (no separate `kotlin.android`).