Skip to main content
skill CX Codex

compose-expert — Claude Code skill: what it does, how to install

Skill outlining shared Jetpack Compose rules in Amethyst: Navigation 3, adaptive shell layouts, and UI performance conventions.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add vitorpamplona/amethyst --skill compose-expert -a claude-code
Open on GitHub

What compose-expert does

This skill guides development inside commonsUI/ for the Amethyst client. It governs shared screens, theme tokens, and the adaptive AppShellLayout. Window tiers rely on ScreenLayoutSpec read via LocalScreenLayout.current, avoiding direct platform checks for Android or desktop environments.

Navigation relies on Navigation 3 without a NavController or navigation graph. The back stack persists as snapshot state with kotlinx-serialization. Shared composables follow strict conventions: UI-state collections must use kotlinx.collections.immutable, text comes from Res.string, and rich text is processed through CachedRichTextParser.

The skill explicitly delegates out-of-scope work. It redirects Android manifest work to android-expert, window chrome to desktop-expert, and coroutine logic to kotlin-coroutines.

What it helps with

  • register shared routes with Navigation 3 builders
  • adapt layouts to screen sizes using ScreenLayoutSpec
  • review public Composable APIs for ImmutableList compliance
  • replace platform string references with Res.string

Example prompts for the agent

  • Review this new composable in commonsUI against Amethyst immutable collection conventions.
  • Register Route.Home in NavDestinations for a shared screen layout.
  • Check whether this shared navigation code avoids direct platform checks.

How to install compose-expert

1Skills CLI (Vercel)

npx skills add vitorpamplona/amethyst --skill compose-expert -a claude-code

2Manually: put the files in place

~/.claude/skills/compose-expert/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/compose-expert/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about compose-expert

How to install compose-expert in Claude Code?

Run npx skills add vitorpamplona/amethyst --skill compose-expert -a claude-code. The skill file is placed at ~/.claude/skills/compose-expert/SKILL.md.

What does compose-expert do?

It provides architecture rules and conventions for shared Compose UI in the Amethyst repository. It covers Navigation 3 back stacks, adaptive layouts, and composable API reviews.

What data collection rules does compose-expert enforce?

Composable state models must not use standard List. They require kotlinx.collections.immutable collections like ImmutableList or ImmutableMap to guarantee immutability across recompositions.

Source text

SKILL.md · MIT
---
name: compose-expert
description: Shared Compose UI in `commonsUI/`: screens, components, the nav shell (`ScreenLayoutSpec`, `AppShellLayout`, Navigation 3 back stacks), component catalog, `RichTextParser`, Compose API/perf review. Use when writing or reviewing shared UI.
---

# compose-expert

Where shared Compose UI lives in Amethyst and the house rules for it. What goes in
`commons` vs `commonsUI` vs a platform shim is in `AGENTS.md` ("Sharing Philosophy",
"Feature Workflow") and `commonsUI/ARCHITECTURE.md`; this skill does not repeat it.

Delegate: Activity / manifest / flavors -> `android-expert`; Window, tray, menu bar ->
`desktop-expert`; source-set placement, expect/actual, extracting an Android composable ->
`kotlin-multiplatform`; feeds and `Account` state -> `account-state`; Flow and coroutine
scopes -> `kotlin-coroutines`.

## Where things live

All paths below are under
`commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/`.

| What | Where |
|------|-------|
| Screens | `ui/screen/loggedIn/<feature>/` (and `ui/screen/` for shared feed views) |
| Reusable components | `ui/components/` (catalog: `references/shared-composables-catalog.md`) |
| Feed scaffolding | `ui/feeds/` (`RenderFeedContentState`, `RefresheableBox`, `FeedEmpty`, `LoadingFeed`) |
| Note rendering | `ui/note/` (`NoteCompose`), rich text in `ui/richtext/` + `ui/components/RichTextViewer.kt` |
| Theme | `ui/theme/` (`AmethystColorScheme.kt` palettes + `ColorScheme.*` tokens, `ColorScheme.isLight`, `Shape.kt`, `Type.kt`) |
| Window tiers | `ui/layouts/ScreenLayout.kt` (`ScreenLayoutSpec`, `LocalScreenLayout`, `FeedContentMaxWidth`) |
| Shell + nav | `ui/app/AmethystApp.kt`, `ui/app/AppRoot.kt`, `ui/navigation/` (see below) |
| Platform ports for UI | `ui/platform/AppPlatform.kt` (`LocalAppPlatform`), `ui/components/PlatformPermission.kt` |
| Icons | `icons/` (Amethyst glyph font) and `icons/symbols/` (`MaterialSymbols`, `AmethystIcons`) |
| Robohash avatars | `robohash/` (`CachedRobohash`, `RobohashAssembler`, `parts/`) |
| Strings | `commonsUI/src/commonMain/composeResources/values/strings.xml` -> `Res.string.*` (rules: `AGENTS.md` "Strings") |

The Android app's `AmethystTheme` wrapper is still in
`amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt`; the palettes it
applies are shared.

## The shell and window tiers

One shell for every window, chosen by size, not by platform (`AGENTS.md` "Navigation Shell"):

- `AmethystApp` (`ui/app/AmethystApp.kt`) is the root both front ends call. Android's
  `MainActivity` passes `AndroidAppRoot`; the desktop window (`AmethystDesktop.kt`) passes a
  `DesktopAppRoot`. `AppRoot`
  (`ui/app/AppRoot.kt`) is the port each platform implements: `createAccountViewModel`,
  `rememberScreenLayoutSpec`, `registerDestinations`, `ShellOverlay`, `NavigationEffects`, ...
- `ScreenLayoutSpec.navigationStyle` is `BOTTOM_BAR`, `NAV_RAIL` or `PERMANENT_DRAWER`;
  `hasRoomForNotificationPanel` docks the notification panel. `isLargeScreen` is "not the
  bottom bar". Read it with `LocalScreenLayout.current`, never with a platform check.
- `AppShellLayout` (`ui/navigation/shell/`) lays out the bar / rail / drawer;
  `ui/navigation/bottombars/` holds `AppBottomBar` and `AppNavigationRail`;
  `ui/navigation/deck/DeckArea.kt` adds the opt-in deck columns on windows of 1,200 dp+.

## Navigation 3

The app owns its back stack as snapshot state; `NavDisplay` only renders it. No
`NavController`, no graph. Full rules in `references/navigation.md`.

- Routes: `@Serializable sealed class Route` in
  `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt`.
  The back stack is saved through kotlinx-serialization, so every route argument must be
  serializable.
- `NavBackStacks` (same package) holds `stack` (never empty, starts at `Route.Home`) and
  `savedTabs` (the root entry of each bottom-bar tab you left, so its ViewModels and scroll
  survive). A `NavStackEntry`'s `contentKey` is `"nav-$id"`: saved state and ViewModels key
  on it, so the same route opened twice is two screens.
- `Nav` (`ui/navigation/navs/Nav.kt`) implements `INav`: `nav`, `navDrawer`, `newStack`,
  `navBottomBar`, `popBack`, `popUpTo`, each after the keyboard settles (`ImeSettler`).
- Register a screen once with a builder from `ui/navigation/host/NavDestinations.kt`. Shared
  screens go in `NavDestinations.sharedDestinations` (`ui/navigation/host/SharedDestinations.kt`);
  Android-only ones in `androidDestinations` in
  `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt`. The builder
  picks the motion and the reading-column cap:

```kotlin
// from SharedDestinations.kt
composableCapped<Route.Home> { HomeScreen(accountViewModel, nav) }
composable<Route.Message> { MessagesScreen(accountViewModel, nav) }
composableFromEnd<Route.Polls> { PollsScreen(accountViewModel, nav) }
composableFromEnd<Route.Browser>(capWidth = false) { BrowserScreen(accountViewModel, nav) }
composableFromBottomArgs<Route.AwardBadge> { AwardBadgeScreen(it.kind, it.pubKeyHex, it.dTag, accountViewModel, nav) }
```

A route nobody registered fails the first time it is opened (the desktop installs an
`unavailable` screen instead). Gate a menu item on `canOpen(route)` when a front end may lack it.

## Composable conventions that differ from the default

- **Strings come from `Res`**, read with `stringRes(Res.string.x)`; no literal user-visible text
  in a shared composable, no Android `R.string`. Default label parameters are `StringResource`
  (see `AddButton` in `ui/components/ActionButtons.kt`).
- **UI-state collections are `kotlinx.collections.immutable`** (`ImmutableList`,
  `ImmutableMap`, `persistentListOf`), an `api` dependency of `commonsUI`. The parser output
  (`RichTextViewerState`, `ParagraphState`) and the platform ports already use them; pass those,
  not `List`, into composables.
- **`@Immutable` / `@Stable`** mark the state classes and port interfaces handed to composables
  (`ScreenLayoutSpec`, `AppPlatform`, `PlatformPermission`). Only mark a class you can keep
  truly immutable.
- **Theme-dependent values come from the color scheme**: `MaterialTheme.colorScheme.isLight`
  (memoized in `AmethystColorScheme.kt`), not a hand-rolled luminance check.
- **Icons** follow `AGENTS.md` "Icons": `icons/*.kt` compile into `amethyst_icons.ttf` and
  draw through `AmethystIconGlyph`; Material icons are `MaterialSymbols` from a subset font, so
  a new codepoint needs `tools/material-symbols-subset/subset.sh`. Robohash is the one
  ImageVector-built asset: `references/icon-assets.md`.
- **Parse rich text through `CachedRichTextParser`**, never per recomposition:
  `references/rich-text-parsing.md`.
- **Permissions in shared UI** go through an `expect fun rememberXPermission(): PlatformPermission`
  (see `ui/components/LocationPermission.kt`; the Android actual wraps Accompanist).

## Reviewing a component's public API

For slot parameters, `modifier` placement, and the other API-shape checks, use
`references/api-style.md`.

## Performance: measure first

Feed jank in this app has been measured as GC-bound and RenderThread-bound, with composition a
small share of frame time. Stability annotations, `derivedStateOf` and deferred reads only help
when a trace shows composition is the cost. Profile a release-like build (the `benchmark`
build type in `amethyst/build.gradle.kts` is minified and profileable) before you apply them, and
measure again afterwards.

Compose compiler reports are configured only for `:amethyst` (`composeCompiler { reportsDestination
... }` near line 551 of `amethyst/build.gradle.kts`, written to `amethyst/build/compose_compiler/`).
They cover only composables compiled in that module. Shared composables are compiled in
`:commonsUI`, which has no such block. To inspect their skippability, add the block to
`commonsUI/build.gradle.kts` temporarily, and don't commit it.

## See it running

A screen change is not done until you've looked at it: `tools/desktop-run/run-headless.sh start`
runs the desktop app, which is this same UI, on a virtual display and screenshots it.
`AMETHYST_SCREEN=480x900x24` shows the phone tier. See `tools/desktop-run/README.md` and
`desktop-expert`.

## References

- `references/navigation.md`: back-stack operations, rendering, transitions, intents, tests
- `references/shared-composables-catalog.md`: the reusable components and their signatures
- `references/rich-text-parsing.md`: `RichTextParser`, segments, `CachedRichTextParser`, NIP-92 imeta
- `references/icon-assets.md`: robohash avatars
- `references/api-style.md`: component API review checklist (slots, modifiers)

© vitorpamplona · MIT · vitorpamplona/amethyst

Similar skills

Everything for Frontend and UI →

From the same set2

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor