mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
170
.codex/skills/compose-skill/references/ui-ux.md
Normal file
170
.codex/skills/compose-skill/references/ui-ux.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# UI/UX Patterns for Utility Apps
|
||||
|
||||
## Core Principles
|
||||
|
||||
Utility apps are trust products. The UI must feel: stable, immediate, precise, reversible, non-destructive.
|
||||
|
||||
## Loading States
|
||||
|
||||
### Decision Rule
|
||||
|
||||
| Situation | Best default |
|
||||
|---|---|
|
||||
| First load, known result card layout | skeleton |
|
||||
| Small inline refresh of one section | keep content + small inline indicator |
|
||||
| Whole-screen blocking startup with no known structure | spinner, but rare |
|
||||
| Recalculating quote while old result exists | keep old result + "updating" affordance |
|
||||
| Empty but idle state | empty-state hint, not spinner |
|
||||
|
||||
### Default Recommendation
|
||||
|
||||
- **Skeleton**: default for known layout with missing data
|
||||
- **Subtle shimmer over skeleton**: optional polish, not the primary strategy
|
||||
- **Spinner**: only for small unknown-layout operations or blocking tasks with no stable placeholder shape
|
||||
|
||||
### Stable Layout During Loading
|
||||
|
||||
Never wipe content during refresh. Never cause height jumps, flicker, or lost context.
|
||||
|
||||
## Inline Validation
|
||||
|
||||
Default behavior:
|
||||
|
||||
- Validate format/range as user edits for fields where feedback is obvious
|
||||
- Avoid screaming errors on untouched fields
|
||||
- Show errors inline, next to the field they belong to
|
||||
- Do not collapse layout when error appears/disappears
|
||||
- Disable submit when impossible, but also explain why
|
||||
|
||||
### Good inline validation behavior
|
||||
|
||||
- Field keeps its value during error
|
||||
- Error appears under field
|
||||
- Submit remains disabled only when necessary
|
||||
- No modal dialog for every invalid keystroke
|
||||
- No full-form red error wall
|
||||
|
||||
## Disabled States
|
||||
|
||||
Disabled is fine only when:
|
||||
|
||||
- The reason is obvious from nearby context
|
||||
- The screen is still readable
|
||||
- User input is preserved
|
||||
|
||||
Bad disabled state: button disabled with no visible reason, form cleared during loading, entire screen grayed out for a small refresh.
|
||||
|
||||
## Preserving User Input
|
||||
|
||||
Non-negotiable rules:
|
||||
|
||||
- **Never clear edited fields on refresh**
|
||||
- **Never clear last good result while fetching a new one**
|
||||
- **Never wipe the screen because one request failed**
|
||||
|
||||
## Progressive Disclosure
|
||||
|
||||
For dense forms:
|
||||
|
||||
- Hide advanced options by default
|
||||
- Keep main path obvious
|
||||
- Reveal secondary controls progressively
|
||||
- Do not split trivial forms into too many steps
|
||||
|
||||
## Partial Results
|
||||
|
||||
Good pattern:
|
||||
|
||||
- Compute instant local estimate from current draft
|
||||
- Show local estimate immediately
|
||||
- Fetch remote refinement in background
|
||||
- Keep old refined quote until new one arrives
|
||||
- Label refreshed state clearly
|
||||
|
||||
## Perceived Performance
|
||||
|
||||
For form-heavy screens:
|
||||
|
||||
- Apply local field state changes instantly
|
||||
- Recalculate cheap deterministic outputs immediately
|
||||
- Debounce only expensive async work
|
||||
- Keep layout stable
|
||||
- Animate only meaningful content changes
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Error messages must be text, not color only
|
||||
- Loading indicators should not hide context unnecessarily
|
||||
- Support logical keyboard/focus order
|
||||
- Avoid rapid flashing/sweeping shimmer
|
||||
- Keep controls large enough for data-entry reliability
|
||||
|
||||
## Code Examples
|
||||
|
||||
### BAD: disappearing content and layout jumps
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun QuoteSection(quote: QuoteUi?, isLoading: Boolean) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator()
|
||||
} else if (quote != null) {
|
||||
QuoteContent(quote = quote, refreshing = false)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GOOD: stable layout with old content preserved
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun QuoteSection(quote: QuoteUi?, isLoading: Boolean) {
|
||||
ResultCardSlot {
|
||||
when {
|
||||
quote != null -> QuoteContent(quote = quote, refreshing = isLoading)
|
||||
isLoading -> QuoteCardSkeleton()
|
||||
else -> QuoteEmptyState()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GOOD: stable placeholder slot
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ResultCardSlot(content: @Composable BoxScope.() -> Unit) {
|
||||
Box(modifier = Modifier.fillMaxWidth().heightIn(min = 180.dp)) { content() }
|
||||
}
|
||||
```
|
||||
|
||||
### GOOD: skeleton with shimmer
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun QuoteCardSkeleton(modifier: Modifier = Modifier) {
|
||||
val alpha by rememberInfiniteTransition(label = "skeleton").animateFloat(
|
||||
initialValue = 0.35f,
|
||||
targetValue = 0.60f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 800),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "alpha",
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 180.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha))
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth(0.4f).height(20.dp).clip(RoundedCornerShape(8.dp)).background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)))
|
||||
Box(Modifier.fillMaxWidth().height(36.dp).clip(RoundedCornerShape(12.dp)).background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)))
|
||||
Box(Modifier.fillMaxWidth(0.7f).height(20.dp).clip(RoundedCornerShape(8.dp)).background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)))
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user