ColorScheme.toPureBlack() sets background, surface, surfaceDim and surfaceContainerLowest to #000 and dims the other containers by scaling their sRGB channels, so their hue tint carries over and grid tiles drawn as containers over surface keep visible gaps. FloretExpressiveTheme takes pureBlack to apply it to the dark scheme, dynamic or app-supplied. core-prefs gains ThemeMode.BLACK (dark, pure black) and ThemeMode.isPureBlack, stored under the same theme_mode key.
22 KiB
Changelog
All notable changes to floret-kit are documented here. The format follows Keep a Changelog.
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'ssetApplicationLocalesonly reaches the platform through a liveAppCompatActivity, and every app usesComponentActivity.AppLanguagenow callsLocaleManagerdirectly there:apply(context, tag)andcurrentTag(context[, supported]). The context-free versions are deprecated.LanguagePickerRowuses the new ones; apps that callAppLanguagethemselves (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
-
identity— a pure-black (AMOLED) dark theme.ColorScheme.toPureBlack()setsbackground,surface,surfaceDimandsurfaceContainerLowestto #000 and pulls the other containers down towards black, keeping their hue tint, so grid tiles drawn as containers oversurfacestill show their gaps.FloretExpressiveThemegainspureBlack(defaultfalse), which applies it to the dark scheme, dynamic or app-supplied. -
core-prefs—ThemeMode.BLACK, dark with pure-black surfaces.isDarktreats it as dark, and the newThemeMode.isPureBlackis what an app passes to the theme'spureBlack. It's stored as"BLACK"under the sametheme_modekey; a build without it reads that asSYSTEM. -
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-widgetWidgetThemeModeoverride),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 ofcomponents'GroupedSurface, built from four position-shaped drawables since Glance'scornerRadiushas no per-corner overload),WidgetMessage, and theWidgetRollover+FloretWidgetUpdateReceivermidnight-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—CollapsingScaffoldtakesfloatingActionButtonandfloatingActionButtonPosition, forwarded to the underlyingScaffold. Both default to the bare scaffold, so every existing call site in every app is unchanged, and they sit aftersnackbarHostso 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 — aBoxwrapping the scaffold with a bottom-end FAB — re-derives the window-inset and position mathsScaffoldalready does, once per tab and again in every sibling app.
[0.6.0] - 2026-09-11
Changed
components—CollapsingScaffold'sonBackis now nullable and defaults to null. A nullonBackdraws 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 bybooleanPref/intPref/longPref/floatPref/stringPref/stringSetPref, the nullable pair (nullableStringPref/nullableLongPref, where writingnullremoves the key rather than storing a sentinel),enumPref, andmap()for a view of the same slot as another type.PrefStorewraps aDataStore<Preferences>withget/set/update/clear/clearAll, a raweditescape hatch, andflow(pref)— which isdistinctUntilChanged, so an unrelated key's write no longer re-emits every screen's setting, the bug both siblings' hand-rolleddata.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(withisDark(systemDark), thewhenblock that was duplicated in both siblings'MainActivity),AppearanceandAppearancePrefs. 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,@DefaultDispatcherand@ApplicationScopequalifiers and theCoroutinesModulethat provides them (the application scope on aSupervisorJob, 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 throughhilt-compiler. Consumed by Clockula.
[0.4.0] - 2026-09-24
Added
-
components—GroupedSectionHeader, the label above a run ofGroupedRows: small,primary, inset toGroupedListInsetso it shares the cards' left margin. Converged from Calendula's settings hub, where the recipe was written as a privateSectionHeader, 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—GroupedRowtakes acontentColor, the pair to the existingcontainer: a row given a tint now names theon*role that goes with it instead of leaving its text ononSurface, 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:secondaryContaineron success,errorContaineron failure). -
components—ScaledViewPreviewandViewPreviewFrame, 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 takingGroupedListInsetand 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—InlineTextFieldtakes avisualTransformation, so a password can be masked.KeyboardType.Passwordonly drops IME suggestions; without this the field rendered the secret in the clear. -
components—ReorderableColumntakes arowHeight(defaultReorderableRowHeight), 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,formatDateTimeandformatDateTimeCompacttake an optionalis24Hour, for an app-level 12/24-hour setting.nullkeeps 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 afteronBack, and the newpredictiveBackExit()exit fades and keeps shrinking from there; pair them on the surface'sAnimatedVisibilityinstead of a full-size slide-out. Pass thatAnimatedVisibility'svisibletopredictiveBack(visible = …)(orCollapsingScaffold(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 inCrashReportSubmit, out of resources altogether, andCrashConfigno longer takes anissueTitle(apps drop the argument, and their owncrash_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 passesprogress, whichOnboardingProgressdraws as one capsule per step with the caller's own localised label, andnavigationIconfor 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.topSpacinggives a step whose hero is a large picture rather than a mark room to breathe;contentPaddingis the scrolling column's horizontal inset, which a step built from grouped rows passes 0 for since those carry their own; andscrollingActionsmoves 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.BenefitRowis 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 amaxWidthso a message can't run into a bottom-end FAB, and takescolor/contentColorso 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—GroupedRowtakes anAnnotatedStringtitle and summary as well as plainStrings, so a row can carry its own emphasis: marking the run a search matched, without every caller having to rebuild the row. TheStringoverload delegates to it, so existing callers are unchanged. -
components—GroupedSurface/GroupedRowtake anonLongClick(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—OptionPickertakes 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 toOptionPicker, so every consumer hand-rolled a copy — Calendula alone carried five, all drawing a different check than the kit's. -
components—AboutCardtakes aPositionand renders throughGroupedSurface, so it can be the top of a group with rows continuing underneath rather than only standing alone. Defaults toPosition.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, butformatDate/formatDateTime/formatDateTimeCompactresolved every instant againstZoneId.systemDefault().formatDatenow takes anallDayflag (defaultfalse, 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 ComposeDialogowns its own window, seeded from the XML theme, so a picker drawing under the bars showed dark icons on a dark surface.FloretExpressiveThemenow exposes its resolved light/dark decision asLocalFloretDarkThemeand 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—CollapsingScaffoldtakes ascrollableflag (defaulttrue, so every existing caller is unchanged). Set it tofalsewhen the content brings its own scrolling container: the scaffold's ownverticalScrollcomposes each row up front and can't host aLazyColumn(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.FullScreenPickerforwards 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)— aDateTimeFormatterfor 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 fromLocalConfiguration. SinceAppLanguage.apply()changes the language inside a running process, a locale captured once goes stale; this keys recomposition to the change.
Changed
core-localenow carries Compose (runtime + ui only) forcurrentLocale(). 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-crashis the precedent for acore-*module carrying Compose.
Known gaps
core-time'sInstant.formatDate()family now overlapscore-locale. Its privatedateNoYearFormatterstrips the year by regex-ing the pattern string, whichlocalizedDateFormatter(locale, "dMMM")does properly; and its formatters resolveLocale.getDefault()once at class-init ("locale changes within a running process are rare enough to ignore"), whichAppLanguage.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'sres/xml/locales_config.xml, get/set the applied language viaAppCompatDelegate, render each language's autonym. The app passes its ownlocales_configresource id; the system-default choice isnull. (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 overkotlin.time.Instant.TimeBridge—Longepoch-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 aCrashConfigsupplied atinstall(); each app keeps its own thin themedCrashReportActivity. -
identity(Android library) — the family's Material 3 Expressive theme factoryFloretExpressiveTheme(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()andModifier.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; setlargeTopBar = falsefor 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).FullScreenPickertakes an optionalactionsslot (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 withGroupedRow(gapBelow = false), which hands the column uniform control of row spacing.DebugRibbon— a stark, un-themed "DEBUG" corner ribbon; gate onBuildConfig.DEBUGat the call site.InlineTextFieldgainedenabled(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), whereMinutescarries 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) plusReminderOverrideCodec(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
foojaytoolchain resolver anywhere — it can fetch a JDK at build time, which reproducible / official F-Droid builds reject. Modules setjvmTargetdirectly. Seedocs/ARCHITECTURE.md. - AGP 9.x provides built-in Kotlin compilation, so Android modules apply only the
Android library + Compose plugins (no separate
kotlin.android).