Merge remote-tracking branch 'origin/main'

# Conflicts:
#	README.md
This commit is contained in:
2026-06-28 19:20:30 +02:00
7 changed files with 419 additions and 8 deletions

54
CHANGELOG.md Normal file
View File

@@ -0,0 +1,54 @@
# 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.1.0`). Entries are grouped by module.
## [Unreleased]
### Added
- **`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.
- **`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
`rememberNavSlideSpec()`. 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.
- `FullScreenPicker` / `OptionPicker` — the full-screen single-select picker.
- `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.
### 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`).

77
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,77 @@
# Contributing to floret-kit
floret-kit is the shared Material 3 Expressive design system + plumbing for the
Floret app family. Before changing it, skim
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) (how it's built and consumed) and
[`docs/ROADMAP.md`](docs/ROADMAP.md) (what's in scope). This file is the
practical how.
## The rules
1. **Share mechanics, not look.** Extract the machinery; leave seed colours,
palettes, identity chips, and wording to the apps. If a piece can't be made
app-agnostic without contortion, it doesn't belong here yet.
2. **No app coupling.** A kit module never imports an app package, an app's `R`,
or a domain type. App-specific values arrive as **constructor/config
parameters** or **callbacks** (see `core-crash`'s `CrashConfig`,
`InlineTextField`'s `capitalization`).
3. **String resources are fallbacks.** Kit strings are English defaults an app
overrides via resource merge. Don't put app-specific or per-app-divergent
wording in the kit.
4. **Bake divergence into parameters, don't pick a silent winner.** Where two
apps differ, expose the difference. When a shared primitive adopts one app's
version as canonical, that's a deliberate, documented convergence.
5. **No `foojay` toolchain resolver** — not in any `*.gradle.kts`, not even as a
token in a comment. It breaks reproducible / official F-Droid builds. Set
`jvmTarget` directly. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) §5.
## Prerequisites
- JDK 17
- Android SDK (compileSdk 37) for the Android modules — set `ANDROID_HOME` or add
a gitignored `local.properties` with `sdk.dir`. Pure-JVM modules need neither.
## Build & test
```sh
./gradlew :core-time:test # JVM module (JUnit 5 + Truth)
./gradlew :core-crash:testDebugUnitTest
./gradlew :identity:assembleDebug
./gradlew build # everything
```
In practice the kit is exercised through a consuming app's composite build (e.g.
`agendula`'s `./gradlew assembleDebug`), which compiles the included modules from
source.
## Adding a module
1. Create `:<module>` and add `include(":<module>")` to
[`settings.gradle.kts`](settings.gradle.kts).
2. Add a `<module>/build.gradle.kts`:
- **JVM:** `plugins { alias(libs.plugins.kotlin.jvm) }`, a `kotlin { compilerOptions { jvmTarget = JVM_17 } }` block, JUnit 5 test deps.
- **Android:** `plugins { alias(libs.plugins.android.library); alias(libs.plugins.kotlin.compose) }` (no `kotlin.android` — AGP 9.x is built-in), `namespace = "de.jeanlucmakiola.floret.<area>"`, `compileSdk 37` / `minSdk 29`, `jvmTarget = JVM_17`.
3. Put sources under `src/main/kotlin/de/jeanlucmakiola/floret/<area>/`. The root
build's `subprojects {}` sets the shared `group`/`version`, so no per-module
coordinates are needed.
4. Add an entry to [`CHANGELOG.md`](CHANGELOG.md), the README module table, and
(if status-worthy) [`docs/ROADMAP.md`](docs/ROADMAP.md).
## Adopting the kit in an app
1. `git submodule add <kit-url> floret-kit`.
2. `includeBuild("floret-kit")` in the app's `settings.gradle.kts`.
3. `implementation("de.jeanlucmakiola.floret:<module>")` in the app module.
4. Ensure CI checks out submodules (`submodules: recursive`) and the included
build can find the SDK (`ANDROID_HOME`). For official F-Droid reproducible
builds, add `submodules: true` to the fdroiddata recipe and have the repro
guard scan the submodule.
5. The app pins a kit commit via the submodule; bump it deliberately to adopt kit
changes.
## Validate before pushing
- `python3 -c "import yaml; yaml.safe_load(open('<workflow>.yml'))"` after any
workflow edit — indentation bites.
- A consuming app's `lintDebug` + `testDebugUnitTest` + `assembleDebug` should be
green after a kit change you mean to land.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Jean-Luc Makiola
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -25,20 +25,38 @@ implementation("de.jeanlucmakiola.floret:core-time")
```
Gradle's dependency substitution maps `de.jeanlucmakiola.floret:<module>` to the
local source module — edit the kit and the app picks it up immediately.
local source module — edit the kit and the app picks it up immediately. Android
modules need an SDK location for the included build: set `ANDROID_HOME` (CI) or
add a gitignored `<app>/floret-kit/local.properties` with `sdk.dir` (locally).
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full consumption model.
## Modules
| Module | What it holds |
|--------|---------------|
| `core-time` | Pure-Kotlin date/time helpers: local-day windows (`DayWindow`) for smart-list logic and locale/zone-aware display formatting (`Instant.formatDate()` / `formatTime()` / `formatDateTime()`). No Android, no deps. |
| `core-reminders` | Pure-Kotlin reminder-lead plumbing: the lead-time unit model (`ReminderUnit`, `decomposeReminderMinutes`), the per-target override model (`ReminderOverride` + `reminderLeadFor` / `applyReminderOverride`) and the stored-format codec (`ReminderOverrideCodec`, separators configurable per app). No Android, no deps. Each app layers its own DataStore + string labels on top. |
| `core-locale` | 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`, and render each language's autonym. The app passes its own `locales_config` resource id; appcompat only, no Compose. |
| Module | Type | What it holds |
|--------|------|---------------|
| `core-time` | JVM | Date/time helpers: `DayWindow` local-day windows, locale/zone-aware `Instant` formatting, `TimeBridge` (millis ↔ `Instant`). No Android, no deps. |
| `core-reminders` | JVM | Reminder-lead plumbing: the lead-time unit model (`ReminderUnit`, `decomposeReminderMinutes`), the per-target override model (`ReminderOverride` + `reminderLeadFor` / `applyReminderOverride`) and the stored-format codec (`ReminderOverrideCodec`, separators configurable per app). Each app layers its own DataStore + string labels on top. |
| `core-locale` | Android | 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`; appcompat only, no Compose. |
| `core-crash` | Android | Privacy-respecting on-device crash capture + report dialog + issue-tracker hand-off. Parameterized per app via `CrashConfig`. |
| `identity` | Android | The M3 Expressive theme factory `FloretExpressiveTheme(…)`, `rememberNavSlideSpec()`, the content transitions (`expandEnter`/`collapseExit`/`itemEnter`/…) and the `predictiveBack` peek. Each app supplies its own seed/palette. |
| `components` | Android | Shared Compose vocabulary + recipes: `GroupedSurface`/`GroupedRow`, `InlineTextField`, `OptionCard`, `CollapsingScaffold`, `OptionPicker`, `pastelize()`, `DialogControls`, `OnboardingScaffold`, `OptionalFormSection`, `AboutCard`, `LanguagePickerRow`. |
_More modules (identity/theme, components, screen recipes, provider/prefs/crash plumbing) land as they're extracted from the apps._
_The principle: the **mechanics** are shared, the **look** stays per-app. See
[`docs/ROADMAP.md`](docs/ROADMAP.md) for what's extracted, deferred, and
deliberately not shared._
## Docs
See [`docs/`](docs/) — start with [`docs/README.md`](docs/README.md)
([ARCHITECTURE](docs/ARCHITECTURE.md), [ROADMAP](docs/ROADMAP.md)) and
[`CONTRIBUTING.md`](CONTRIBUTING.md) to add or change a module.
[`CHANGELOG.md`](CHANGELOG.md) tracks what landed.
>>>>>>> origin/main
## Build
```sh
./gradlew :core-time:test
./gradlew :core-time:test # JVM module tests
./gradlew :core-crash:testDebugUnitTest
./gradlew build # everything (Android modules need an SDK; see above)
```

147
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,147 @@
# floret-kit — architecture
How the kit is structured, built, and consumed **as it stands today**. For what
is and isn't extracted, see [`ROADMAP.md`](ROADMAP.md); for the practical how of
adding a module, see [`../CONTRIBUTING.md`](../CONTRIBUTING.md).
---
## 1. The thesis in one sentence
floret-kit is a Gradle build of small, focused modules — plumbing and a Material
3 Expressive design layer — that the family's apps draw from instead of
re-deriving the same code. The whole design hangs off one rule:
> Share the **mechanics**, keep the **look** per-app.
Theme machinery, content-provider plumbing, and component primitives are shared;
seed colours, palettes, identity chips, domain types, and app-specific wording
stay in each app. The kit never knows about a specific app's domain.
---
## 2. How an app consumes the kit
Each app embeds this repo as a **git submodule** and wires it in as a Gradle
**composite build**. The kit is therefore always built **from source** — there
are no published binaries — which keeps every consuming app reproducible (a
requirement for official F-Droid).
```kotlin
// <app>/settings.gradle.kts
includeBuild("floret-kit")
```
```kotlin
// <app>/app/build.gradle.kts
implementation("de.jeanlucmakiola.floret:core-time")
implementation("de.jeanlucmakiola.floret:components")
```
Every module sets `group = "de.jeanlucmakiola.floret"` (via the root build's
`subprojects {}`), so Gradle's dependency substitution maps
`de.jeanlucmakiola.floret:<module>` to the local `project(":<module>")` of the
included build. Editing the kit is picked up by the app's next build with no
publish step.
Each app **pins a specific kit commit** through its submodule gitlink. So a
breaking change in the kit can't reach an app until that app deliberately
re-pins — apps stay on independent cadences (important while Calendula ships and
Agendula moves fast).
### SDK location for the included build
An included Android build needs to locate the Android SDK independently of the
host app. Provide it via:
- **CI:** an `ANDROID_HOME` env var (the apps set it at the job level), or
- **locally:** a gitignored `<app>/floret-kit/local.properties` with `sdk.dir`.
Pure-JVM modules (`core-time`) need neither.
### Submodules in CI / F-Droid
Consuming apps must check out submodules (`submodules: recursive` in the
checkout step) so `floret-kit/` isn't empty. For an app published via **official
F-Droid reproducible builds** (Calendula), the fdroiddata recipe additionally
needs `submodules: true` on the build entry, and the app's reproducibility guard
should scan the submodule's Gradle scripts (see §5).
---
## 3. Module layering
| Layer | Modules | Notes |
|---|---|---|
| Plumbing | `core-time` (JVM) | Pure logic; no Android, no deps. |
| Plumbing | `core-crash` (Android) | Self-contained subsystem; parameterized per app. |
| Design — identity | `identity` (Android) | Theme factory + motion. App supplies seed/palette. |
| Design — components | `components` (Android) | Shared Compose primitives. App composes screens. |
Dependencies between modules are kept minimal: `core-time` depends on nothing in
the kit; `identity` and `components` depend only on Compose + (for components)
`material-icons-core`. There is no app-level or cross-domain coupling.
Each module's package is `de.jeanlucmakiola.floret.<area>` (e.g.
`de.jeanlucmakiola.floret.components`).
---
## 4. Module types & the build
Two kinds of module:
- **JVM library** (`core-time`) — `org.jetbrains.kotlin.jvm`, `jvmTarget = 17`,
JUnit 5 + Truth tests. The right home for pure logic; cheapest to build and
test.
- **Android library** (`core-crash`, `identity`, `components`) —
`com.android.library` + the Compose plugin, `compileSdk 37` / `minSdk 29`.
> **AGP 9.x provides built-in Kotlin compilation.** Android modules therefore
> apply only `com.android.library` and `org.jetbrains.kotlin.plugin.compose` —
> **not** a separate `org.jetbrains.kotlin.android` plugin (applying it errors).
> They set `jvmTarget` directly in a `kotlin { compilerOptions { … } }` block.
Versions live in [`gradle/libs.versions.toml`](../gradle/libs.versions.toml),
kept in lockstep with the apps' catalogs.
---
## 5. Reproducibility rules
Because consuming apps build the kit from source — and at least one publishes via
official F-Droid reproducible builds — the kit follows the same invariants:
1. **No `foojay` toolchain resolver**, anywhere (not even as a token in a
comment). It can fetch a JDK over the network at build time, which the offline
F-Droid source scanner rejects. Modules set `jvmTarget` directly instead of
using a Java toolchain block that would need a resolver.
2. **No published artifacts.** Built from source, pinned by commit.
3. Android library modules emit no APK-level metadata of their own (no
`buildConfig`); the consuming app controls APK-level reproducibility
(`vcsInfo`/`dependenciesInfo`).
A consuming app's reproducibility guard should scan the submodule's `*.gradle.kts`
for the resolver too, since the included build is part of the from-source graph.
---
## 6. The design-divergence contract
The design modules (`identity`, `components`) carry the family's look, so they
extract **mechanics** and leave **identity** to the app:
- `identity` exposes a theme **factory**; each app passes its own seed-derived
`lightScheme`/`darkScheme` and typography. Seed colours and hand-tuned
fallbacks are never shared constants.
- `components` are app-agnostic primitives. Where two apps differ, the
difference is an explicit **parameter** (e.g. `InlineTextField`'s
`capitalization`) or a **callback**, never a silently-picked winner. Apps keep
their own identity chips/icons and compose their own screens.
- **String resources** in the kit are English **fallbacks**; an app overrides
them (with its localized values) via resource merge. App-specific wording
stays in the app.
When a primitive is shared by adopting one app's version as canonical (e.g.
`components`' `GroupedSurface` follows Agendula's cleaner abstraction), that is a
deliberate, signed-off convergence — the other app's surfaces change when it
re-pins, reviewable at that point.

26
docs/README.md Normal file
View File

@@ -0,0 +1,26 @@
# floret-kit — documentation
floret-kit is the shared **Material 3 Expressive design system + plumbing** for
the Floret family of Android apps
([Calendula](https://gitea.jeanlucmakiola.de/makiolaj/calendula),
[Agendula](https://gitea.jeanlucmakiola.de/makiolaj/agendula), and future
siblings). It is consumed from source as a git submodule + Gradle composite
build. See the top-level [`../README.md`](../README.md) for the pitch.
## Index
| Doc | What it covers |
|---|---|
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | How the kit is built and **consumed** — the module layering, JVM vs Android-library modules, the submodule + composite-build model, and the reproducibility rules. Start here. |
| [`ROADMAP.md`](ROADMAP.md) | **What's extracted, what's deferred, and what's deliberately not shared** — the module status and the divergence decisions from the cross-app survey. |
| [`../CONTRIBUTING.md`](../CONTRIBUTING.md) | The practical how — adding a module, the no-app-coupling rules, build/test, and how an app adopts the kit. |
| [`../CHANGELOG.md`](../CHANGELOG.md) | Keep a Changelog; what each module added. |
## The one principle
> Share the **mechanics**, keep the **look** per-app.
Theme machinery, plumbing, and component primitives are shared; seed colours,
palettes, identity chips, and app-specific wording stay in each app. This is what
lets the family read as one design language without homogenising apps that are
meant to have distinct personalities.

68
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,68 @@
# floret-kit — roadmap
What's extracted, what's deferred, and what's deliberately **not** shared. The
defer/skip calls come from a cross-app survey of both Calendula and Agendula —
the guiding rule is to share only what is genuinely common, and to keep
divergent or app-specific code where it lives.
---
## Extracted (done)
| Module | Type | Consumers |
|---|---|---|
| `core-time` | JVM | Agendula (and Calendula's pending branch) |
| `core-crash` | Android | Agendula |
| `identity` | Android | Agendula |
| `components` | Android | Agendula |
See [`../CHANGELOG.md`](../CHANGELOG.md) for the contents of each.
---
## Deferred / not yet worth it
- **`core-prefs`** (DataStore base: `ThemeMode`, `dynamicColor`, the typed
wrapper, `toEnum()`) and **`core-di`** (`@IoDispatcher` + provider) — genuinely
shared but **low value**: pure relocation, no new capability, the shared
surface is tiny once the store name and bindings stay app-local. Sensible to
defer until a third app makes the duplication hurt.
- **`ReminderFormatting`** (`ReminderUnit` + `reminderLeadTimeLabel` +
`decomposeReminder`) — the labels are `@Composable` over app-specific
`R.plurals`, and the presets differ per app. Extract later behind a
**label/preset callback API**, not before.
## Deferred — needs migration first
- **`core-provider`** (the ContentProvider seam: `ColumnReader`, failures,
observer→Flow) — the two apps' `ColumnReader`s diverged (Calendula index-based,
Agendula name-based + null-safe). True sharing means migrating Calendula to the
better name-based design first (~a few hundred LOC), so it waits until Calendula
is actively on the kit.
## Not shared (by design)
- **A generic Settings scaffold** — the two apps' settings *domains* are too
divergent for a config-driven `SettingsScaffold` to stay readable. The win is
captured instead at the **component** level (shared `GroupedRow`/section
primitives + `CollapsingScaffold` + `OptionPicker`); each app composes its own
settings from those.
- **`core-reminders`** (unified) — Agendula self-schedules alarms (pull),
Calendula is provider-event-driven (push). Opposite architectures over
incompatible data models; forcing a shared abstraction would be leaky. A tiny
`core-notification` (idempotent channel setup, `POST_NOTIFICATIONS` checks)
*might* land later; schedulers stay app-local.
- Domain-specific UI (date/time fields tied to a domain, calendar/timeline
widgets, recurrence text, domain pickers) and all string wording — these encode
each app's model and resist parameterization.
---
## Consumers & cadence
- **Agendula** consumes the kit on `main` (active development).
- **Calendula** (ships via official F-Droid reproducible builds) has a pending
adoption branch. Bringing it onto the kit is where the canonical-`GroupedSurface`
convergence becomes visible, and where the F-Droid reproducibility checklist
(`submodules: true` in the recipe; the repro guard scanning the submodule)
applies — see [`ARCHITECTURE.md`](ARCHITECTURE.md) §2 and §5.