diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3da06d3 --- /dev/null +++ b/CHANGELOG.md @@ -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`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..041890b --- /dev/null +++ b/CONTRIBUTING.md @@ -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 `:` and add `include(":")` to + [`settings.gradle.kts`](settings.gradle.kts). +2. Add a `/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."`, `compileSdk 37` / `minSdk 29`, `jvmTarget = JVM_17`. +3. Put sources under `src/main/kotlin/de/jeanlucmakiola/floret//`. 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 floret-kit`. +2. `includeBuild("floret-kit")` in the app's `settings.gradle.kts`. +3. `implementation("de.jeanlucmakiola.floret:")` 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('.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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c6dd179 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index cc99fbb..65cb262 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,35 @@ implementation("de.jeanlucmakiola.floret:core-time") ``` Gradle's dependency substitution maps `de.jeanlucmakiola.floret:` 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 `/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. | +| 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-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()`. Each app supplies its own seed/palette. | +| `components` | Android | Shared Compose vocabulary: `GroupedSurface`/`GroupedRow`, `InlineTextField`, `OptionCard`, `CollapsingScaffold`, `OptionPicker`, `pastelize()`. | -_More modules (identity/theme, components, screen recipes, provider/prefs/reminders/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. ## 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) ``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..db85bef --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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 +// /settings.gradle.kts +includeBuild("floret-kit") +``` +```kotlin +// /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:` to the local `project(":")` 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 `/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.` (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. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..952ea18 --- /dev/null +++ b/docs/README.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..85a5cd3 --- /dev/null +++ b/docs/ROADMAP.md @@ -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.