Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 92 additions & 8 deletions .claude/agents/DEVELOPMENT-PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,86 @@ component to our component:
- ListEmptyState - ListEmptyState
- tablelist-masternodekeys - TableListMasternodeKeyRow

## Dark Mode Compatibility

Every new screen, dialog, and component must work in both light and dark mode. The app uses `Theme.AppCompat.DayNight` which activates `values-night/` resource qualifiers automatically.

### Compose: The Golden Rule

**One call to `LocalDashColors.current` per composable, at the top. Never use `MyTheme.Colors.*` directly.**

```kotlin
@Composable
fun MyComponent(...) {
val colors = LocalDashColors.current // ← always this, nothing else
Column(modifier = Modifier.background(colors.backgroundPrimary)) {
Text("Hello", color = colors.textPrimary)
}
}
```

**Root entry point wrapping** — The topmost composable in a Fragment's `setContent { }` block must be wrapped in `DashWalletTheme`:

```kotlin
composeView.setContent {
DashWalletTheme { // ← selects light or dark colors once
MyScreen(...)
}
}
```

**Rules:**
- `DashWalletTheme` wraps the root once. Child composables never call `isSystemInDarkTheme()`.
- `MyTheme.Colors` (light) and `MyTheme.DarkColors` (dark) are the source-of-truth instances; `LocalDashColors.current` resolves to the correct one automatically.
- Always wrap previews in `DashWalletTheme`. For dark previews add `uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES`:

```kotlin
@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
@Composable
fun MyPreviewDark() {
DashWalletTheme { MyComponent(...) }
}
```

### ColorScheme Fields Reference

Key fields from `MyTheme.ColorScheme` (use via `LocalDashColors.current`):

| Field | Light | Dark | Use for |
|-------|-------|------|---------|
| `backgroundPrimary` | `#F5F6F7` | `#10151F` | Page background |
| `backgroundSecondary` | `#FFFFFF` | `#1D2532` | Cards, sheets, panels |
| `textPrimary` | `#191C1F` | `#FFFFFF` | Primary text |
| `textSecondary` | `#6E757C` | `#92929C` | Secondary / helper text |
| `textTertiary` | `#75808A` | `#75808A` | Tertiary / label text |
| `dashBlue` | `#008DE4` | `#008DE4` | Brand accent, links |
| `dividerColor` | `#EDF0F2` | `#2C3748` | Dividers, borders |
| `disabledButtonBg` | `#EEEEEE` | `#3C3C3C` | Disabled button background |
| `contentDisabled` | `#92929C` | `#92929C` | Disabled text / icon |

### XML Layouts: Semantic Color Tokens

Use semantic tokens from `values/colors.xml` — they have night overrides in `values-night/colors.xml`. Never use `@android:color/white` or literal hex values for surfaces or text.

| Purpose | Token |
|---------|-------|
| Page background | `@color/background_primary` |
| Card / sheet surface | `@color/background_secondary` |
| Primary text | `@color/content_primary` |
| Dividers / borders | `@color/divider_color` |

### XML Drawables: Adaptive Colors

- **Shape drawables** (cards, panels): fill with `@color/background_secondary`, not `@android:color/white`
- **Vector icons**: set `android:tint="@color/content_primary"` on the `<vector>` element instead of a hardcoded fill
- **Color state lists** (`<selector>`): the default (last) item must use `@color/content_primary`, not a hardcoded hex

### DashButton Disabled State

`DashButton` handles the disabled state automatically via `colors.disabledButtonBg` and `colors.contentDisabled`. Do not set `alpha` or override colors manually for disabled buttons — just pass `isEnabled = false`.

---

## NavBar / TopNavBase (Figma: NavBar)

The navigation bar lives in:
Expand Down Expand Up @@ -329,7 +409,7 @@ ListItem(
Icon(
painter = painterResource(R.drawable.ic_dash_blue_filled),
contentDescription = null,
tint = MyTheme.Colors.dashBlue,
tint = LocalDashColors.current.dashBlue,
modifier = Modifier.size(32.dp)
)
}
Expand Down Expand Up @@ -370,7 +450,7 @@ ListItem(
Icon(
painter = painterResource(R.drawable.ic_menu_row_arrow),
contentDescription = null,
tint = MyTheme.Colors.textTertiary,
tint = LocalDashColors.current.textTertiary,
modifier = Modifier.size(16.dp)
)
}
Expand Down Expand Up @@ -408,7 +488,7 @@ ListEmptyState(
Icon(
painter = painterResource(R.drawable.ic_dash_blue_filled),
contentDescription = null,
tint = MyTheme.Colors.dashBlue,
tint = LocalDashColors.current.dashBlue,
modifier = Modifier.size(48.dp)
)
},
Expand Down Expand Up @@ -561,7 +641,7 @@ FeatureItemNumber(number = "1")

**Visual Specs**:
- Size: 20dp circle
- Background: MyTheme.Colors.dashBlue
- Background: `colors.dashBlue` (via `LocalDashColors.current`)
- Border radius: 8dp
- Text: 12sp, white, centered

Expand Down Expand Up @@ -846,18 +926,21 @@ When implementing designs from Figma, use the following typography mappings. All
### Usage Examples

```kotlin
// Always read colors at the top of the composable:
val colors = LocalDashColors.current

// Dialog title - Use Headline S Bold
Text(
text = stringResource(R.string.upgrade_pin_title),
style = MyTheme.Typography.HeadlineSmallBold,
color = MyTheme.Colors.textPrimary
color = colors.textPrimary
)

// Dialog description - Use Body M (Regular)
Text(
text = stringResource(R.string.upgrade_pin_description),
style = MyTheme.Typography.BodyMedium,
color = MyTheme.Colors.textSecondary
color = colors.textSecondary
)

// List item title - Use Title M Semibold
Expand All @@ -870,7 +953,7 @@ Text(
Text(
text = "2 hours ago",
style = MyTheme.Typography.LabelMedium,
color = MyTheme.Colors.textTertiary
color = colors.textTertiary
)

// Button text - Use Label L Semibold (handled by DashButton)
Expand Down Expand Up @@ -986,10 +1069,11 @@ private fun SettingsScreenContent(
else -> stringResource(statusId)
}

val colors = LocalDashColors.current
Column(
modifier = Modifier
.fillMaxSize()
.background(MyTheme.Colors.backgroundPrimary)
.background(colors.backgroundPrimary)
) {
// Top Navigation
NavBarBack(onBackClick = onBackClick)
Expand Down
99 changes: 87 additions & 12 deletions .claude/agents/figma-to-compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,20 @@ See `development-patterns` for full `NavBarBack`/`NavBarBackTitle`/`TopIntro` us

#### Color Mapping

| Figma Token | MyTheme Reference | Hex |
|-------------|-------------------|-----|
| `text/primary` | `MyTheme.Colors.textPrimary` | `#191C1F` |
| `text/secondary` | `MyTheme.Colors.textSecondary` | `#6E757C` |
| `text/tertiary` | `MyTheme.Colors.textTertiary` | `#75808A` |
| `background/primary` | `MyTheme.Colors.backgroundPrimary` | `#F5F6F7` |
| `background/secondary` | `MyTheme.Colors.backgroundSecondary` | `#FFFFFF` |
| `colors/dash-blue` | `MyTheme.Colors.dashBlue` | `#008DE4` |
| `colors/orange` | `MyTheme.Colors.orange` | `#FA9269` |
| `colors/red` | `MyTheme.Colors.red` | `#EA3943` |
| `colors/green` | `MyTheme.Colors.green` | `#3CB878` |
| `colors/gray` | `MyTheme.Colors.gray` | `#B0B6BC` |
**Important:** Never reference `MyTheme.Colors.*` directly in composables. Always read the current theme colors via `val colors = LocalDashColors.current` at the top of each composable, then use `colors.*`. This ensures correct values in both light and dark mode.

| Figma Token | `colors.*` field | Light hex | Dark hex |
|-------------|-----------------|-----------|----------|
| `text/primary` | `colors.textPrimary` | `#191C1F` | `#FFFFFF` |
| `text/secondary` | `colors.textSecondary` | `#6E757C` | `#92929C` |
| `text/tertiary` | `colors.textTertiary` | `#75808A` | `#75808A` |
| `background/primary` | `colors.backgroundPrimary` | `#F5F6F7` | `#10151F` |
| `background/secondary` | `colors.backgroundSecondary` | `#FFFFFF` | `#1D2532` |
| `colors/dash-blue` | `colors.dashBlue` | `#008DE4` | `#008DE4` |
| `colors/orange` | `colors.orange` | `#FA9269` | `#FA9269` |
| `colors/red` | `colors.red` | `#EA3943` | `#EA3943` |
| `colors/green` | `colors.green` | `#3CB878` | `#3CB878` |
| `colors/gray` | `colors.gray` | `#B0B6BC` | `#B0B6BC` |

### 4. Handle Icons and Image Assets

Expand Down Expand Up @@ -294,6 +296,79 @@ After implementation:
3. Verify all `R.string.*` references have entries in strings.xml
4. Confirm the nav graph `tools:layout` attribute is removed if the fragment uses ComposeView

## Dark Mode Compatibility

Every new screen, dialog, and component must work correctly in both light and dark mode. Follow these rules without exception.

### Compose: Theme Color Access

**Root entry point** — The composable entry point called from a Fragment's `ComposeView.setContent { }` must be wrapped in `DashWalletTheme`:

```kotlin
// In Fragment.onCreateView or onViewCreated:
composeView.setContent {
DashWalletTheme {
MyScreen(...)
}
}
```

**Inside every composable** — read current colors once at the top:

```kotlin
@Composable
fun MyComponent(...) {
val colors = LocalDashColors.current
// Use colors.textPrimary, colors.backgroundSecondary, etc.
// NEVER use MyTheme.Colors.* directly here
}
```

**Rules:**
- Never call `isSystemInDarkTheme()` inside individual components or screens — `DashWalletTheme` handles this once at the root
- Never use `MyTheme.Colors.*` directly in a composable body — it always returns light-mode values
- `MyTheme.Colors` and `MyTheme.DarkColors` are the source-of-truth data class instances; `LocalDashColors.current` selects the right one automatically

### Compose: Preview Dark Mode

Always include both a light and dark preview for new components:

```kotlin
@Composable
@Preview(name = "Light")
fun MyComponentPreviewLight() {
DashWalletTheme { MyComponent(...) }
}

@Composable
@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
fun MyComponentPreviewDark() {
DashWalletTheme { MyComponent(...) }
}
```

### XML Layouts: Semantic Color Tokens

Use semantic color names that have night-mode overrides in `values-night/colors.xml`. Never use raw `@android:color/white` or literal hex values for surfaces or text.

| Purpose | Use this color token |
|---------|---------------------|
| Page background | `@color/background_primary` |
| Card / sheet surface | `@color/background_secondary` |
| Primary text | `@color/content_primary` |
| Secondary text | `@color/content_secondary` |
| Dividers / borders | `@color/divider_color` |

### XML Drawables: Adaptive Colors

- **Shape drawables** (cards, panels): fill with `@color/background_secondary`, not `@android:color/white`
- **Vector icons**: add `android:tint="@color/content_primary"` to the `<vector>` element instead of hardcoding a fill color
- **Color state lists** (selectors): default state should use `@color/content_primary`, not a hardcoded dark hex

### DashButton Disabled State

`DashButton` handles disabled appearance automatically through `colors.disabledButtonBg` and `colors.contentDisabled` from `LocalDashColors`. Do not override button colors manually for the disabled case.

## Common Pitfalls

### Kotlin Overload Resolution with Trailing Lambdas
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import org.dash.wallet.common.R

@Deprecated(message = "use DashButton instead")
object ButtonStyles {
@Composable
fun whiteWithBlueText() = ButtonDefaults.buttonColors(
Expand Down
Loading