mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-13 05:49:57 +02:00
Add the VniDrop-specific Compose architecture skill, unify invitation and targeted transfer drafts, and promote Saved devices to an adaptive first-class destination.
145 lines
9.5 KiB
Markdown
145 lines
9.5 KiB
Markdown
---
|
|
name: compose-skill
|
|
description: VniDrop-specific Compose Multiplatform UI and Kotlin presentation architecture. Use when designing, implementing, refactoring, or reviewing code under shared/ for Android, Windows, or Linux: screens, ViewModels, routes, navigation, adaptive layouts, platform adapters, icons, resources, accessibility, and UI tests.
|
|
---
|
|
|
|
# VniDrop KMP UI
|
|
|
|
Build VniDrop's Android and desktop UI without weakening its domain model, platform identity, or Rust streaming invariants.
|
|
|
|
## Start here
|
|
|
|
1. Read the root `AGENTS.md` and `shared/AGENTS.md` completely.
|
|
2. Read `CONTEXT.md` and only the ADRs relevant to the feature.
|
|
3. Inspect the nearby feature, its tests, and its Android/JVM adapters before designing.
|
|
4. Identify the module, interface, seam, and adapters. Prefer a deep module: small interface, substantial hidden behavior, one test surface.
|
|
5. Model state and platform behavior before drawing pixels.
|
|
6. Implement the smallest complete product flow; add regressions at the lowest useful layer.
|
|
7. Run `make test-shared`; run `make check-shared` for a production UI handoff.
|
|
|
|
## Scope and ownership
|
|
|
|
- `shared/commonMain` owns shared domain-facing presentation state, feature behavior, semantic UI structure, and reusable visual primitives.
|
|
- `androidMain` owns Android pickers, SAF, MediaStore, system surfaces, and Android-native presentation where needed.
|
|
- `jvmMain` owns Windows/Linux filesystem, desktop integration, and platform-native presentation where needed.
|
|
- Apple uses the native SwiftUI app under `apple/`. Do not move Apple presentation into KMP.
|
|
- Rust owns transfer payload streaming, authorization, durable lifecycle, and transfer persistence. Kotlin must not become the payload path.
|
|
|
|
## Architecture
|
|
|
|
Use VniDrop's MVVM-style modules:
|
|
|
|
- Immutable `*State` exposed through `StateFlow`.
|
|
- Named ViewModel methods for user actions. Do not introduce a generic `onEvent` hierarchy.
|
|
- Route: obtain/collect state, invoke platform adapters, collect effects, and perform navigation.
|
|
- Screen: render state and emit explicit callbacks.
|
|
- Leaf composables: accept narrow state and callbacks; retain only visual-local state such as focus, scroll, or animation.
|
|
- `AppGraph` wires dependencies. Do not introduce Hilt, Koin, or a second graph.
|
|
|
|
Design for depth and locality:
|
|
|
|
- Put behavior behind a small interface used by callers and tests.
|
|
- Keep internal seams private. Do not add an interface until behavior genuinely varies.
|
|
- An Android/JVM/test adapter set is a real seam; a single implementation is not.
|
|
- Do not hide callback explosion in an `Actions` data class. That changes syntax, not depth.
|
|
- Do not add use-case classes or pass-through repositories around `CoreGateway`.
|
|
- Preserve `Invitation transfer`, `Targeted transfer`, `Transfer draft`, `Saved device`, and `Device relationship` as distinct terms from `CONTEXT.md`.
|
|
|
|
For structural work, read [references/architecture.md](references/architecture.md). Open no other reference in the same turn unless the task changes materially.
|
|
|
|
## Platform-native experience
|
|
|
|
Every supported platform should feel native. Sharing implementation is a means, not the goal.
|
|
|
|
- Prefer shared behavior and state, but allow repeated Android, Windows, and Linux presentation implementations when native interaction, layout, menus, dialogs, shortcuts, density, or system integration differ.
|
|
- Do not force the lowest-common-denominator UI merely to maximize `commonMain` code.
|
|
- Keep duplicated platform presentation thin and semantic; do not duplicate domain rules or transfer state machines.
|
|
- Use `expect`/`actual`, platform source sets, or injected adapters only at genuine seams.
|
|
- Android should follow Material interaction and navigation conventions.
|
|
- Windows should use Fluent iconography and desktop interaction conventions.
|
|
- Linux/Desktop should use the existing Lucide family and desktop conventions.
|
|
|
|
### Native icons
|
|
|
|
- Use semantic `AppIcon` values rendered through `PlatformIcon`.
|
|
- Android resolves Material icons, Windows resolves Fluent icons, and Linux/Desktop resolves Lucide icons.
|
|
- When adding an icon, provide the appropriate resource for every supported family. Do not reuse one platform's asset everywhere because it is convenient.
|
|
- Prefer the native system icon or platform icon family when a platform exposes a stronger convention. A platform-specific implementation is acceptable.
|
|
- Give actionable icons a localized content description; decorative icons use `null`.
|
|
- Do not inline arbitrary Material icons or hard-code drawable selection in feature composables.
|
|
|
|
For platform-specific UI decisions, read [references/platform-native-ui.md](references/platform-native-ui.md). Open no other reference in the same turn unless the task changes materially.
|
|
|
|
## VniDrop transfer invariants
|
|
|
|
- Invitation transfer and Targeted transfer may share a Transfer draft implementation, but never erase their distinct destination, authorization, lifecycle, or result types.
|
|
- Public Targeted operations remain transfer-ID-only. Never expose authorization material to Kotlin.
|
|
- A saved-device display name resolves as local label, then authenticated remote display name, then a localized unnamed fallback. Endpoint ID is secondary diagnostic identity.
|
|
- Android folder sharing expands a SAF tree into file descriptors plus safe relative names. Never pass an Android directory FD to Rust.
|
|
- Desktop may pass filesystem directories marked as directories for Rust traversal.
|
|
- Platform source adapters keep descriptors and leases alive for the complete core call and close them exactly once.
|
|
- App-owned picker copies are released on replacement, removal, explicit dismissal, or successful creation. Never delete original user sources.
|
|
- Picker cancellation and creation failure preserve the current valid Transfer draft.
|
|
|
|
### Transfer draft architecture
|
|
|
|
Use one deep, session-scoped composition module for Invitation and Targeted creation:
|
|
|
|
- Concrete MVVM module with `TransferDraftState`, named methods, and semantic outputs.
|
|
- Domain-specific `openInvitation` and `openTargeted`; Targeted receiver is locked for the session.
|
|
- Routes invoke file/folder picker adapters and navigate from semantic creation results.
|
|
- The module owns selection, opaque source IDs, automatic-name provenance, validation, retry, single-flight submission, destination revalidation, and temporary-copy lifecycle.
|
|
- One private platform source-adapter seam serves Android, JVM, and tests.
|
|
- Multiple files or one folder; do not add mixed file-plus-folder drafts without an explicit product decision.
|
|
- Targeted mode omits Invitation sender-name and access-policy controls.
|
|
- Operational failure preserves the draft; successful creation emits the correct domain identity for the host to open.
|
|
|
|
## UI system
|
|
|
|
- Use `LocalVniDropColors` and `VniDropThemeTokens`; do not hard-code product colors.
|
|
- Use existing `WindowClass`, `LocalUiPlatform`, `contentWindowClassFor`, and shell/navigation helpers.
|
|
- Prefer semantic feature modules over generic visual abstractions.
|
|
- Keep stable keys for device and transfer lists.
|
|
- Preserve minimum touch targets, keyboard access, focus order, readable contrast, and meaningful semantics.
|
|
- Treat phone, tablet/rail, Windows desktop, and Linux desktop as deliberate presentations—not scaled copies.
|
|
|
|
## Strings and resources
|
|
|
|
- `localization/strings.json` is the only source of truth for product strings.
|
|
- Run the localization generator after editing it.
|
|
- Never hand-edit generated Compose XML, Apple catalogs, or accessors.
|
|
- Use `Res.string.*` in `commonMain`; never Android `R` there.
|
|
- Do not synthesize English product copy in ViewModels, including automatic transfer names.
|
|
- Resolve semantic strings near presentation or inject a small formatter when behavior requires localized text.
|
|
|
|
## Dependencies
|
|
|
|
- Prefer existing dependencies and platform facilities.
|
|
- Before adding AndroidX/Jetpack to `commonMain`, verify coordinates, exact API shape, and every required KMP target using official documentation or artifact metadata.
|
|
- If verification is unavailable, stop and report the uncertainty. Do not add an unverified production dependency with a “check later” comment.
|
|
- Do not add navigation, DI, persistence, networking, or image-loading frameworks unless the requested feature proves the need.
|
|
|
|
## Testing
|
|
|
|
- `commonTest`: state machines and feature behavior through the module interface; use focused fakes.
|
|
- `jvmTest`: Compose interaction, semantics, keyboard behavior, and adaptive phone/desktop presentation.
|
|
- Platform tests: Android SAF/MediaStore and desktop filesystem/native integration.
|
|
- Test complete states where relevant: empty, loading, content, busy, error, confirmation, interruption, and terminal outcomes.
|
|
- Test both Invitation and Targeted modes through the shared Transfer draft interface.
|
|
- Assert native icon-family selection and accessibility semantics when adding platform actions.
|
|
- Prefer deterministic gates and virtual time; avoid fixed sleeps.
|
|
- Delete obsolete shallow tests after equivalent interface-level coverage exists.
|
|
|
|
## Anti-patterns
|
|
|
|
- Business rules, core calls, ticket parsing, or filesystem work in composables.
|
|
- A global mutable draft shared by Send and Saved devices.
|
|
- Payload bytes streamed through Kotlin.
|
|
- Android directory FDs.
|
|
- Raw endpoint IDs as primary saved-device names.
|
|
- Generic `onEvent`, forced MVI, Hilt/Koin migrations, or use-case-per-method architecture.
|
|
- One universal icon set or one platform's interaction model imposed on every platform.
|
|
- Duplicated domain behavior justified as “native UI.” Only presentation duplication is acceptable.
|
|
- Hand-edited generated localization outputs.
|
|
- New generic wrappers whose deletion merely moves calls to the caller.
|