Files
floret-kit/CHANGELOG.md
T
makiolaj f1ded81c03 feat(identity): pure-black (AMOLED) dark theme
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.
2026-10-14 17:19:31 +02:00

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'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

  • 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 GroupedRows: 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 Strings, 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.
  • AGP 9.x provides built-in Kotlin compilation, so Android modules apply only the Android library + Compose plugins (no separate kotlin.android).