docs: add V1 design spec for calendar app
Initial design document for the Material 3 Expressive calendar app. Covers scope (V1 read-only MVP, variant "B"), tech stack (Kotlin + Compose + Material3 Expressive, minSdk 29), architecture, data flow over CalendarContract, screens/menus, the mandatory Loading/Failure/ Success state pattern per screen, error handling, i18n, accessibility, testing approach, and CI/CD adaptation from HouseHoldKeaper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
392
docs/superpowers/specs/2026-06-08-calendar-app-design.md
Normal file
392
docs/superpowers/specs/2026-06-08-calendar-app-design.md
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
# Calendar App - V1 Design Spec
|
||||||
|
|
||||||
|
**Date:** 2026-06-08
|
||||||
|
**Status:** Draft for review
|
||||||
|
**Author:** Jean-Luc Makiola (with Claude)
|
||||||
|
|
||||||
|
## 1. Motivation & Goal
|
||||||
|
|
||||||
|
Eine Android-native, Open-Source-Kalender-App im Material 3 Expressive Design.
|
||||||
|
Schließt die Lücke, dass es derzeit keinen optisch zeitgemäßen Kalender abseits
|
||||||
|
von Google Calendar gibt - speziell mit dem 2025er Expressive-Design.
|
||||||
|
|
||||||
|
**Was die App NICHT macht:**
|
||||||
|
- Eigene CalDAV/iCal-Synchronisation - das übernimmt der Android `CalendarContract`
|
||||||
|
bzw. Drittsoftware wie DAVx5
|
||||||
|
- Eigene lokale Event-DB - alle Daten leben im `CalendarContract`
|
||||||
|
|
||||||
|
**Was die App macht:**
|
||||||
|
- Schöne, moderne UI über Android-Bordmittel
|
||||||
|
- Liest aus `CalendarContract` (alle Quellen: Nextcloud/CalDAV via DAVx5,
|
||||||
|
Google, lokal, WebCal-Subscriptions)
|
||||||
|
- V1 ist read-only; Schreibrechte kommen in V2
|
||||||
|
|
||||||
|
## 2. Scope - V1 MVP (Variante "B")
|
||||||
|
|
||||||
|
### In-Scope
|
||||||
|
- 3 Hauptansichten: Monat, Woche, Tag
|
||||||
|
- Event-Detail-Sheet (read-only Detailansicht)
|
||||||
|
- Multi-Kalender-Toggle (Sichtbarkeit pro Kalender)
|
||||||
|
- Heute-Button + Jump-to-Date
|
||||||
|
- Settings-Screen (Theme, Dynamic Color, Wochenstart, Sprache)
|
||||||
|
- Permission-Flow für `READ_CALENDAR`
|
||||||
|
- Empty-States und Error-Recovery
|
||||||
|
- DE + EN Lokalisierung
|
||||||
|
- Tests + CI ab Tag 1
|
||||||
|
|
||||||
|
### Out-of-Scope (V2+)
|
||||||
|
- Event-Create/Edit/Delete (V2)
|
||||||
|
- Home-Screen-Widget
|
||||||
|
- Volltextsuche
|
||||||
|
- Quick-Add
|
||||||
|
- Notifications/Reminders (System macht das schon, nicht doppeln)
|
||||||
|
- Tablet-/Foldable-spezifische Layouts
|
||||||
|
- iOS (Kotlin-Native ist explizit Android-only)
|
||||||
|
|
||||||
|
## 3. Tech Stack
|
||||||
|
|
||||||
|
| Layer | Wahl | Begründung |
|
||||||
|
|---|---|---|
|
||||||
|
| Sprache | Kotlin 2.0+ | Android-Native-Standard |
|
||||||
|
| UI | Jetpack Compose + Material3 Expressive (1.5+) | Echter M3 Expressive Support |
|
||||||
|
| Min SDK | 29 (Android 10) | Modern, keine Compat-Pfade |
|
||||||
|
| Target SDK | 36 (Android 16) | Aktuell, wie HouseHoldKeaper CI |
|
||||||
|
| DI | Hilt | Industriestandard |
|
||||||
|
| Persistenz Prefs | DataStore (Preferences) | Theme, Wochenstart, Filter-State |
|
||||||
|
| Persistenz Daten | keine | Source of Truth bleibt `CalendarContract` |
|
||||||
|
| Datum/Zeit | `kotlinx.datetime` (Domain), `java.time` an Provider-Grenze | Saubere API |
|
||||||
|
| Navigation | Compose Navigation, Single-Activity | Standard |
|
||||||
|
| Lokalisierung | Android Resources (`strings.xml`) + Plurals | DE + EN ab V1 |
|
||||||
|
| Tests | JUnit5, Truth, Turbine, Compose UI Test | JVM-first, Instrumented nur für ContentResolver-Integration |
|
||||||
|
| Build | Gradle Kotlin DSL + Version Catalog | Lesbar, typsicher |
|
||||||
|
| CI | Gitea Workflows (adaptiert von HouseHoldKeaper) | Gleiche Konvention wie restliche Projekte |
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
- `READ_CALENDAR` (einzige Runtime-Permission)
|
||||||
|
- `android.permission.QUERY_ALL_PACKAGES` **nicht** nötig (Maps-Intent geht ohne)
|
||||||
|
|
||||||
|
### App-Identifier
|
||||||
|
- Package: `de.jeanlucmakiola.cal` (Name "cal" als Platzhalter, finalisierung später)
|
||||||
|
- Convention identisch zu `HouseHoldKeaper`: `de.jeanlucmakiola.<app_name>`
|
||||||
|
|
||||||
|
## 4. Architektur
|
||||||
|
|
||||||
|
### Modul-Struktur
|
||||||
|
Single Gradle module `:app` für V1. Feature-Split (`:core`, `:feature-*`) erst
|
||||||
|
wenn nötig - YAGNI.
|
||||||
|
|
||||||
|
### Package-Layout
|
||||||
|
```
|
||||||
|
de.jeanlucmakiola.cal/
|
||||||
|
├── App.kt + MainActivity.kt
|
||||||
|
├── data/ ContentResolver-Wrapper, Repositories
|
||||||
|
├── domain/ Pure-Kotlin Models (Event, CalendarSource)
|
||||||
|
└── ui/
|
||||||
|
├── theme/ M3 Expressive Theme, Dynamic Color
|
||||||
|
├── month/ Monatsansicht (Composable + ViewModel + State)
|
||||||
|
├── week/ Wochenansicht
|
||||||
|
├── day/ Tagesansicht
|
||||||
|
├── detail/ Event-Detail-Sheet
|
||||||
|
├── filter/ Kalender-Filter-Sheet
|
||||||
|
├── settings/ Settings-Screen
|
||||||
|
├── permission/ Permission-Request-Screen
|
||||||
|
└── common/ Geteilte Composables (LoadingScreen-Helper, FailureScreen-Helper)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer-Verantwortlichkeiten
|
||||||
|
- **data/**: Nur Layer der Android-Klassen kennt (`ContentResolver`, `Cursor`,
|
||||||
|
`CalendarContract`). Mapped auf Domain-Modelle.
|
||||||
|
- **domain/**: Pure Kotlin. Keine Android-Imports.
|
||||||
|
- **ui/**: Compose-Code, ViewModels. Hängt von domain ab, niemals direkt an data.
|
||||||
|
|
||||||
|
## 5. Datenfluss & Domain-Modell
|
||||||
|
|
||||||
|
### Domain-Modelle (pure Kotlin)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class CalendarSource(
|
||||||
|
val id: Long,
|
||||||
|
val displayName: String,
|
||||||
|
val accountName: String, // z.B. "jlmak@nextcloud.example.com"
|
||||||
|
val accountType: String, // z.B. "at.bitfire.davdroid" (DAVx5), "com.google" (Google), "LOCAL"
|
||||||
|
val color: Int,
|
||||||
|
val isVisibleInSystem: Boolean, // CalendarContract.Calendars.VISIBLE
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EventInstance(
|
||||||
|
val instanceId: Long, // Instances._ID (eindeutig pro Vorkommen)
|
||||||
|
val eventId: Long, // Events._ID (gleich für alle Vorkommen)
|
||||||
|
val calendarId: Long,
|
||||||
|
val title: String,
|
||||||
|
val start: Instant,
|
||||||
|
val end: Instant,
|
||||||
|
val isAllDay: Boolean,
|
||||||
|
val color: Int, // Effektiv: Event.color ?: Calendar.color
|
||||||
|
val location: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EventDetail(
|
||||||
|
val instance: EventInstance,
|
||||||
|
val description: String?,
|
||||||
|
val organizer: String?,
|
||||||
|
val attendees: List<Attendee>,
|
||||||
|
val rrule: String?, // Read-only: nur "wiederkehrt wöchentlich" anzeigen
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Attendee(val name: String, val email: String?, val status: AttendeeStatus)
|
||||||
|
enum class AttendeeStatus { Accepted, Declined, Tentative, NeedsAction, Unknown }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
interface CalendarRepository {
|
||||||
|
fun calendars(): Flow<List<CalendarSource>>
|
||||||
|
fun instances(range: ClosedRange<Instant>): Flow<List<EventInstance>>
|
||||||
|
suspend fun eventDetail(eventId: Long): EventDetail
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation-Details:**
|
||||||
|
- Hält `ContentObserver` auf `CalendarContract.CONTENT_URI` registriert
|
||||||
|
- Bei Observer-Trigger: re-query, emit neuer Wert via SharedFlow
|
||||||
|
- Queries laufen auf `Dispatchers.IO`
|
||||||
|
- `instances(range)` nutzt `CalendarContract.Instances.CONTENT_BY_DAY_URI`
|
||||||
|
oder `CONTENT_URI` mit Time-Range - Recurrence-Expansion macht der Provider
|
||||||
|
- App-eigene "ausgeblendete Kalender-IDs" leben in DataStore als `Set<Long>`,
|
||||||
|
kombinieren via `combine()` mit Calendar-Flow
|
||||||
|
|
||||||
|
### ViewModel-Pattern
|
||||||
|
|
||||||
|
Ein ViewModel pro Top-Level-Screen. State immer als sealed interface
|
||||||
|
(siehe Section 7 - Loading/Failure/Success).
|
||||||
|
|
||||||
|
ViewModel kennt aktuelle "Cursor"-Position (welcher Monat/Woche/Tag) und
|
||||||
|
fragt Repository nur für sichtbaren Range an - kein "alle Events der Welt".
|
||||||
|
|
||||||
|
## 6. Screens & Menüs
|
||||||
|
|
||||||
|
### Hauptscreens
|
||||||
|
|
||||||
|
**S1 - Monatsansicht**
|
||||||
|
- Zeigt einen Monat im Überblick
|
||||||
|
- Pro Tag erkennbar: hat-Events / keine-Events, ggf. Andeutung über Anzahl/Farbe
|
||||||
|
- Navigation: vorwärts/zurück zwischen Monaten
|
||||||
|
- Tap auf Tag → Tagesansicht für diesen Tag
|
||||||
|
- Heute deutlich markiert
|
||||||
|
|
||||||
|
**S2 - Wochenansicht**
|
||||||
|
- Zeigt eine Woche mit Zeitschiene
|
||||||
|
- Events auf ihrer Uhrzeit, Calendar-Farbe
|
||||||
|
- Overlap-Events: nebeneinander aufgelöst
|
||||||
|
- All-Day-Events extra dargestellt
|
||||||
|
- Navigation: vorwärts/zurück zwischen Wochen
|
||||||
|
- Tap Event → Event-Detail-Sheet
|
||||||
|
|
||||||
|
**S3 - Tagesansicht**
|
||||||
|
- Eine Spalte, mehr Detail pro Event als Wochenansicht
|
||||||
|
- All-Day-Events extra
|
||||||
|
- Navigation: vorwärts/zurück zwischen Tagen
|
||||||
|
- Tap Event → Event-Detail-Sheet
|
||||||
|
|
||||||
|
**S4 - Event-Detail-Sheet (Bottom-Sheet, ModalBottomSheet)**
|
||||||
|
- Pflicht-Inhalte: Titel, Start/Ende oder "Ganztägig", Kalender-Zugehörigkeit
|
||||||
|
- Konditional: Ort (Tap → Maps-Intent), Beschreibung, Teilnehmer, RRULE-Hinweis
|
||||||
|
- Dismissable via Drag oder Back-Geste
|
||||||
|
|
||||||
|
### Menüs
|
||||||
|
|
||||||
|
**M1 - View-Switcher**
|
||||||
|
- Wechsel zwischen Monat / Woche / Tag
|
||||||
|
- Immer erreichbar von allen Hauptansichten
|
||||||
|
- State persistent (zuletzt aktive Ansicht)
|
||||||
|
|
||||||
|
**M2 - Heute / Springe-zu-Datum**
|
||||||
|
- Schnell zurück zu "heute"
|
||||||
|
- Springe zu beliebigem Datum via Datum-Picker
|
||||||
|
- Erreichbar von allen Hauptansichten
|
||||||
|
|
||||||
|
**M3 - Kalender-Filter (Bottom-Sheet)**
|
||||||
|
- Sichtbare Kalender ein-/ausblenden
|
||||||
|
- Gruppiert pro Account (Nextcloud / Local / Google / …)
|
||||||
|
- Pro Eintrag: Name, Calendar-Farbe
|
||||||
|
- Persistiert in DataStore
|
||||||
|
- Erreichbar von allen Hauptansichten
|
||||||
|
|
||||||
|
**M4 - Settings**
|
||||||
|
- Theme: System / Light / Dark
|
||||||
|
- Dynamic Color: an/aus (auto disabled wenn API < 31)
|
||||||
|
- Wochenstart: Auto (aus Locale) / Mo / So
|
||||||
|
- Sprache: Auto / DE / EN
|
||||||
|
- About: Version, Lizenz, Link zum Quellcode (Gitea)
|
||||||
|
|
||||||
|
### Spezial-Flows
|
||||||
|
|
||||||
|
**F1 - Erst-Start / Permission-Flow**
|
||||||
|
- Beim ersten App-Start: `READ_CALENDAR`-Request
|
||||||
|
- Erklärungs-Text: "Wir lesen nur deinen Gerätekalender - keine Daten verlassen das Gerät"
|
||||||
|
- Bei Denial: friendlicher Recovery-Screen mit Re-Request-Button + Link zu System-Settings
|
||||||
|
|
||||||
|
**F2 - Empty-State (keine Kalender / keine Events)**
|
||||||
|
- Keine Kalender konfiguriert: Hinweis "Füge in DAVx5 oder System-Settings einen Kalender hinzu" mit Intent-Link zu System-Calendar-Settings
|
||||||
|
- Kalender da, aber aktuelle Ansicht leer: dezent, kein nerviger Placeholder
|
||||||
|
|
||||||
|
**F3 - Reaktion auf externe Änderungen**
|
||||||
|
- DAVx5/System-Calendar ändert sich → App aktualisiert sich automatisch via ContentObserver
|
||||||
|
- Kein manueller Pull-to-Refresh
|
||||||
|
|
||||||
|
## 7. UI-State-Modell: Loading / Failure / Success
|
||||||
|
|
||||||
|
**Pflicht-Pattern für jeden Screen.** Keine Ausnahmen.
|
||||||
|
|
||||||
|
### ViewModel-State
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
sealed interface MonthUiState {
|
||||||
|
data object Loading : MonthUiState
|
||||||
|
data class Failure(val reason: FailureReason) : MonthUiState
|
||||||
|
data class Success(
|
||||||
|
val month: YearMonth,
|
||||||
|
val eventsPerDay: Map<LocalDate, List<EventInstance>>,
|
||||||
|
val visibleCalendars: List<CalendarSource>,
|
||||||
|
) : MonthUiState
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class FailureReason {
|
||||||
|
PermissionRevoked, // → Re-Request-Screen
|
||||||
|
NoCalendarsConfigured, // → Empty-State mit Intent zu System-Settings
|
||||||
|
ProviderUnavailable, // → Retry-Screen
|
||||||
|
EventNotFound, // → nur für Event-Detail-Sheet
|
||||||
|
Unknown, // → Fallback
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Composable-Dispatch
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Composable
|
||||||
|
fun MonthScreen(viewModel: MonthViewModel) {
|
||||||
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
when (val s = state) {
|
||||||
|
is MonthUiState.Loading -> MonthLoadingScreen()
|
||||||
|
is MonthUiState.Failure -> MonthFailureScreen(s.reason, onRetry = viewModel::retry)
|
||||||
|
is MonthUiState.Success -> MonthSuccessScreen(s, ...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pflicht-Composables pro Screen
|
||||||
|
|
||||||
|
| Screen | Loading | Failure-Varianten | Success |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Monat | Skelett-Grid (Shimmer) | Permission, NoCalendars, Provider | Grid mit Events |
|
||||||
|
| Woche | Skelett-Schiene | Permission, NoCalendars, Provider | Schiene mit Events |
|
||||||
|
| Tag | Skelett-Schiene | Permission, NoCalendars, Provider | Schiene mit Events |
|
||||||
|
| Event-Detail | kompakter Skelett-Sheet | EventNotFound, Provider | Voll-Detail |
|
||||||
|
| Kalender-Filter | Skelett-Liste | Provider | Liste |
|
||||||
|
| Settings | sofort (DataStore ist instant) | - | direkt |
|
||||||
|
|
||||||
|
**Regeln:**
|
||||||
|
- Loading ist ein bewusst gestalteter Screen (Skeleton + Shimmer), kein loser Spinner
|
||||||
|
- Failure ist ein eigener Screen mit Erklärung + Recovery-Action, kein Toast
|
||||||
|
- Beim UI-Design später: alle drei Varianten pro Screen skizzieren, nie nur Success
|
||||||
|
- Tests: pro Screen mindestens `renders_loading`, `renders_failure`, `renders_success`
|
||||||
|
|
||||||
|
## 8. Error Handling & Edge Cases
|
||||||
|
|
||||||
|
### Philosophie
|
||||||
|
Calendar-Apps dürfen niemals an leeren/malformen Daten crashen. Defensive
|
||||||
|
Validierung im Repository, kaputte Instanzen still droppen + via `Log.w` loggen.
|
||||||
|
|
||||||
|
### Konkrete Fehlerfälle
|
||||||
|
| Fall | Verhalten |
|
||||||
|
|---|---|
|
||||||
|
| ContentResolver-Query wirft (Permission revoked zur Laufzeit) | State → `Failure(PermissionRevoked)`, UI zeigt Re-Request |
|
||||||
|
| Calendar `displayName` null | Fallback "(Unbenannter Kalender)" |
|
||||||
|
| Event `title` null/leer | Fallback "(Ohne Titel)" |
|
||||||
|
| `dtend < dtstart` | Event droppen, Warn-Log |
|
||||||
|
| `dtstart` vor Unix-Epoch | Event droppen, Warn-Log |
|
||||||
|
| Maps-Intent fehlt | SnackBar "Keine Karten-App installiert" |
|
||||||
|
| DataStore-IO-Fehler | Defaults verwenden, weiter, Warn-Log |
|
||||||
|
|
||||||
|
### Edge Cases im UI
|
||||||
|
- **All-Day-Events über mehrere Tage:** in Wochen-/Tagesansicht über mehrere
|
||||||
|
Tage gespannter All-Day-Strip
|
||||||
|
- **Events über Mitternacht:** in Wochen-/Tagesansicht am Folgetag fortsetzen
|
||||||
|
- **Instant Events (start == end):** Mindesthöhe rendern für Tap-Target
|
||||||
|
- **Viele Events an einem Tag:** in Monatsansicht "+N more" statt Overflow
|
||||||
|
- **Timezones:** alle Berechnungen in Geräte-Local-TZ, außer all-day = floating
|
||||||
|
|
||||||
|
## 9. i18n & Accessibility
|
||||||
|
|
||||||
|
### i18n
|
||||||
|
- `res/values/strings.xml` = englische Master-Strings
|
||||||
|
- `res/values-de/strings.xml` = deutsche Übersetzungen
|
||||||
|
- Alle Strings extrahiert, auch Plurals (`<plurals>` für "1 Event" / "N Events")
|
||||||
|
- Wochentags-/Monatsnamen via `java.time.format.DateTimeFormatter` mit aktiver
|
||||||
|
Locale (kein Hardcoding)
|
||||||
|
- Sprach-Override aus Settings via `AppCompatDelegate.setApplicationLocales`
|
||||||
|
|
||||||
|
### Accessibility (V1-Minimum)
|
||||||
|
- Alle interaktiven Elemente: `contentDescription`, Tap-Target ≥ 48dp
|
||||||
|
- Event-Items: semantisches Label "Titel, Start-Zeit, Dauer, Kalender X"
|
||||||
|
- Calendar-Color **immer** mit Label/Form kombiniert (nie nur-Farbe-Information)
|
||||||
|
- Dynamic-Text-Size respektiert (keine fixen sp-Werte für Text)
|
||||||
|
- Hoher-Kontrast: M3-Theme reagiert automatisch
|
||||||
|
- TalkBack-Smoke-Tests im UI-Test-Plan
|
||||||
|
|
||||||
|
## 10. Testing
|
||||||
|
|
||||||
|
Best Practices, kein Diskussionsbedarf:
|
||||||
|
- **Unit-Tests:** JUnit5 + Truth + Turbine. Repository, ViewModels, Date/Time-Helpers,
|
||||||
|
ContentResolver-Wrapper (mit Mock-Cursor)
|
||||||
|
- **UI-Tests:** Compose UI Test pro Screen, mindestens `renders_loading` /
|
||||||
|
`renders_failure` / `renders_success` + 1-2 Interaktions-Tests
|
||||||
|
- **Coverage-Ziel:** pragmatisch ~70% lines, 100% für Repository + Date-Logik.
|
||||||
|
Kein Coverage-Gate in CI, aber Pflicht-Run
|
||||||
|
- **Instrumented-Tests:** nur für ContentResolver-Integration (echte
|
||||||
|
CalendarContract-Queries auf Emulator)
|
||||||
|
|
||||||
|
## 11. CI/CD
|
||||||
|
|
||||||
|
Adaption der `HouseHoldKeaper`-Pipeline, nur Flutter-Steps durch Gradle ersetzt.
|
||||||
|
|
||||||
|
### `.gitea/workflows/ci.yaml` (push + PR)
|
||||||
|
- Setup Java 17 + Android SDK 36
|
||||||
|
- `./gradlew lint`
|
||||||
|
- `./gradlew test`
|
||||||
|
- `./gradlew assembleDebug`
|
||||||
|
- Trivy Filesystem-Scan (HIGH/CRITICAL, `continue-on-error` wie HHK)
|
||||||
|
|
||||||
|
### `.gitea/workflows/release.yaml` (auf Git-Tags)
|
||||||
|
- Alles aus `ci.yaml`
|
||||||
|
- Version aus Git-Tag in `app/build.gradle.kts`:
|
||||||
|
- `versionName = "${tag#v}"`
|
||||||
|
- `versionCode = MAJOR*10000 + MINOR*100 + PATCH` (HHK-Konvention)
|
||||||
|
- Keystore aus Gitea Secrets (`KEYSTORE_BASE64`, `KEY_PASSWORD`, `KEY_ALIAS`)
|
||||||
|
- `./gradlew assembleRelease`
|
||||||
|
- F-Droid-Pipeline 1:1 wie HHK: Hetzner-Sync, `fdroid update -c`, Re-Upload
|
||||||
|
|
||||||
|
### Repo-Konventionen
|
||||||
|
- `CHANGELOG.md` wird beim Taggen gepflegt (patch/minor/major)
|
||||||
|
- `fdroid-metadata/de.jeanlucmakiola.cal/` Verzeichnis-Struktur
|
||||||
|
- `LICENSE` = MIT, Jean-Luc Makiola, 2026
|
||||||
|
- `.planning/` mit `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md`
|
||||||
|
|
||||||
|
## 12. Open Decisions
|
||||||
|
|
||||||
|
| Punkt | Status |
|
||||||
|
|---|---|
|
||||||
|
| Finaler App-Name (Platzhalter `cal`) | offen, später |
|
||||||
|
| Konkretes UI-Layout pro Screen (Mockups, Komponenten-Wahl) | offen, eigene Design-Iteration nach Spec-Approval |
|
||||||
|
| Theme-Seed-Color (Hex) für Fallback wenn kein Dynamic Color | offen, später beim Design |
|
||||||
|
| Icon (Adaptive Launcher + Foreground) | offen, später beim Design |
|
||||||
|
|
||||||
|
## 13. Nächste Schritte nach Spec-Approval
|
||||||
|
|
||||||
|
1. Implementation-Plan via `writing-plans`-Skill aus diesem Spec ableiten
|
||||||
|
2. Initiales Gradle-Projekt-Scaffolding
|
||||||
|
3. Tooling: Lint-Config, Detekt o.ä., CI-Workflows initial
|
||||||
|
4. Iterative UI-Design-Phase (Mockups pro Screen, alle drei States,
|
||||||
|
bevor implementiert wird)
|
||||||
|
5. Feature-by-Feature-Implementation gegen den Plan
|
||||||
Reference in New Issue
Block a user