Action-list row with a 32px leading icon, label, trailing chevron, and a trailing Counter pill.
EBCounter to the base transaction row. That should be a trailing slot (or a counter: Int? parameter that swaps in a Counter) on the base component — not a second component with a duplicated 2 × 3 density/state matrix. Shipping as a sibling doubles maintenance cost on every token or layout change, and the same anti-pattern will repeat for the "with Description" sibling.Used where a row needs to surface a pending count alongside the action — inbox folders, notification categories, or settings entries with outstanding items.
Density is PascalCase; State is PascalCase here but state (lowercase) on Counter — inconsistent casing across the family. Loading skeleton is a generic strip instead of a trailing pill shape. C2| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State=Default | Brand-blue label, brand-blue chevron, filled Counter pill (#072592 on #EEF2F9). |
| Disabled | Yes | Yes | State=Disabled | Muted label (#C2CFE5), muted chevron (#9BC5FD), empty Counter pill. |
| Loading | Yes | Yes | State=Loading | Avatar circle, long label skeleton, trailing 46 × 16 strip. The strip doesn't actually match the Counter pill shape — see open issue. |
| Pressed / Focused | N/A | N/A | Not modeled | No pressed / focused variants on the row. Inherited gap — same issue as base Transaction row. |
- Duplicated variant matrix. This sibling recreates the base row's 2 × 3 (
Density×State) matrix just to bolt on a trailing Counter. Every future change to the base row (radius, padding, token rename) has to be mirrored here. Fold into the base row via atrailingslot or acounter: Int?parameter. C1 · Layer Structure & Naming - Property casing is inconsistent across the family.
DensityandStateuse PascalCase, while the composedCounterchild uses lowercasestate. Pick one (recommend lowercase) and apply across every List Item and Counter property. C2 · Variant & Property Naming - Loading skeleton doesn't match the trailing Counter shape. The 46 × 16 strip is the generic "trailing icon" skeleton used on the base row — it doesn't look like a 24 × 24 pill. Either shape the skeleton to match or drop the Counter entirely in Loading (and let the skeleton stand in). C4 · Native Mappability
- No pressed / focused states. Action rows are interactive targets; they need pressed and focused visuals for native parity. Missing on the base row too — fix once at the base. C5 · Interaction State Coverage
- Code Connect mappings not registered. Blocked until the consolidation lands — wiring this sibling directly would entrench the duplication. C7 · Code Connect Linkability
- Consolidate into one
Action Rowcomponent. This file becomes atrailing=counterconfiguration of the unifiedAction Row(with acounter: Intvalue prop), alongside the "with Description" sibling mapped tosubtitle: String?. Three components collapse into one with clean slot-based composition. Family - Adopt a Figma Slot for the trailing area. With the slot, this file's 6 variants disappear; consumers drop an existing
Counterinstance (node18482:71321) into the trailing slot of the base row. Maps cleanly to@ViewBuilder trailing(SwiftUI) /trailing: @Composable () -> Unit(Compose) for Code Connect. Slot - Normalize property casing. Rename
Density→densityandState→stateso the whole Transaction family (base, with Counter, with Description) matches the lowercase convention used on Counter and most of the DS. Rename - Shape the Loading skeleton or omit the trailing. If the base row's Loading skeleton stays, match the trailing skeleton to a 24 × 24 pill so consumers see the actual footprint. Easier path: drop the trailing skeleton and let the 46 × 16 strip stand in for all trailing content. State
- Add pressed / focused states at the base row. Action rows are tappable; native parity requires pressed + focused visuals. Fix on the base Transaction row once, and every "with X" sibling (or slotted consumer) inherits it. State
- Document the migration path. When the base row gets a trailing slot, deprecate this component and link consumers to the base row with Counter composition. Otherwise teams keep instancing the sibling and the duplication doesn't go away. Docs
360 × 56. 32 px icon, brand-blue label, chevron, trailing 24 × 24 filled Counter pill.
| Role | Token | Default | Disabled | Loading |
|---|---|---|---|---|
| Row bg | main/action-list/color/default/bg | #FFFFFF | #FFFFFF | #FFFFFF |
| Label | main/action-list/color/default/label-brand | #005CE5 | – | – |
| Label (disabled) | main/action-list/color/disabled/label | – | #C2CFE5 | – |
| Chevron | main/action-list/color/default/chevron | #005CE5 | #9BC5FD | – |
| Counter bg | main/counter/color/filled/bg | #EEF2F9 | #EEF2F9 | – |
| Counter label (filled) | main/counter/color/filled/label | #072592 | – | – |
| Counter label (empty) | main/counter/color/empty/label | – | #C2CFE5 | – |
| Skeleton bar | bg/color-bg-strong | – | – | #EEF2F9 |
| Row shadow | Depth/D0 | drop-shadow(0 1 3 0 #E8EEF2C9) |
360 × 64. Same composition; 15 px vertical padding vs 11 px on Compact.
| Role | Token | Value |
|---|---|---|
| Row width | — | 360px |
| Row height (Compact) | — | 56px |
| Row height (Expanded) | — | 64px |
| Row padding H | space/space-12 | 12px |
| Row padding V (Compact) | — | 11px |
| Row padding V (Expanded) | — | 15px |
| Icon → label gap | space/space-12 | 12px |
| Label → trailing gap | space/space-16 | 16px |
| Icon size | — | 32 × 32 |
| Chevron size | — | 32 × 32 |
| Counter size | — | 24 × 24 (min) · hugs digits |
| Counter pad H | space/space-8 | 8px |
| Counter radius | radius/radius-round | pill (99999) |
| Row radius | radius/radius-2 | 6px |
Muted label, muted chevron, empty Counter pill.
| Role | Token | Spec |
|---|---|---|
| Label | Primary/Label/Large | Proxima Soft Bold · 18 / 18 · +0.25 |
| Counter | Primary/Label/Small | Proxima Soft Bold · 14 / 14 · +0.25 |
Expanded height + Disabled tokens.
Avatar circle + label line + 46 × 16 trailing strip. Strip shape doesn't match the Counter pill.
Same skeleton with 16 px padding.
iOS — Swift Package Manager
// In Xcode: File → Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios" import EBDesignSystem
Android — Gradle (Kotlin DSL)
dependencies { implementation("com.eastblue.ds:list:1.0.0") } import com.eastblue.ds.components.EBListItemTransaction
| Figma Property | SwiftUI | Compose |
|---|---|---|
| (separate component) | trailing Slot on base row | @ViewBuilder trailing |
| Density | density (renamed) | density: .compact | .expanded |
| State | state (renamed) | state: .default | .disabled | .loading |
| icon (bool) | leading Slot | @ViewBuilder leading |
| label | label | label: String |
| chevron (bool) | chevron | chevron: Bool = true |
| counter (bool) | derived from trailing slot | — |
// Default — Compact density, trailing Counter EBListItemTransaction(label: "Notifications") { EBAvatar(initials: "N") } trailing: { EBCounter(count: unreadCount) } // Expanded density EBListItemTransaction(label: "Promos") { EBAvatar(initials: "P") } trailing: { EBCounter(count: 2) } .density(.expanded) // Disabled row — counter stays in empty state EBListItemTransaction(label: "Archive") { EBAvatar(initials: "A") } trailing: { EBCounter(count: 0) } .disabled(true) // Loading — trailing slot omitted; skeleton fills EBListItemTransaction.loading()
// Default — Compact density, trailing Counter EBListItemTransaction( label = "Notifications", leading = { EBAvatar(initials = "N") }, trailing = { EBCounter(count = unreadCount) } ) // Expanded density EBListItemTransaction( label = "Promos", density = EBListDensity.Expanded, leading = { EBAvatar(initials = "P") }, trailing = { EBCounter(count = 2) } ) // Disabled row EBListItemTransaction( label = "Archive", enabled = false, leading = { EBAvatar(initials = "A") }, trailing = { EBCounter(count = 0) } ) // Loading — trailing slot omitted; skeleton fills EBListItemTransaction(state = EBListState.Loading)
| Requirement | iOS | Android |
|---|---|---|
| Row role | Wrap in Button / NavigationLink for tappable semantics | Apply Modifier.clickable(...) + Role.Button |
| Counter label | Compose accessibility label: "Notifications, 5 unread" — don't let VoiceOver read the digit alone | Merge into row: contentDescription = "Notifications, 5 unread" |
| Disabled | .disabled(true) — drops from hit-testing + dims label/chevron/counter | enabled = false on clickable modifier |
| Loading | Announce "Loading"; hide skeleton children from a11y tree | Modifier.semantics { liveRegion = Polite } + hide skeletons |
| Chevron | Decorative — .accessibilityHidden(true) | contentDescription = null |
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Requires Rework | Sibling duplicates the base Transaction row matrix. Consolidate via trailing slot. |
| C2 | Variant & Property Naming | Needs Refinement | Density/State PascalCase mismatches lowercase Counter and most of DS. |
| C3 | Token Coverage | Ready | All colors + spacing bound. Uses main/action-list/* + main/counter/*. |
| C4 | Native Mappability | Needs Refinement | HStack/Row maps cleanly. Loading skeleton's 46 × 16 trailing strip doesn't match a Counter pill. |
| C5 | Interaction State Coverage | Needs Refinement | No pressed / focused variants — inherited from base row. |
| C6 | Asset & Icon Quality | Ready | Chevron is a vector instance; icon is a swap placeholder — same pattern as other List Items. |
| C7 | Code Connect Linkability | Not Mapped | Do not wire this sibling — map the base row with trailing slot after consolidation. |
Density (2) × State (3) = 6 variants. Identical matrix to the base Transaction row.
| # | Density | State | Node ID | Dimensions |
|---|---|---|---|---|
| 1 | Compact | Default | 18577:14638 | 360 × 56 |
| 2 | Expanded | Default | 18577:14647 | 360 × 64 |
| 3 | Compact | Disabled | 18577:14656 | 360 × 56 |
| 4 | Expanded | Disabled | 18577:14665 | 360 × 64 |
| 5 | Compact | Loading | 18577:14674 | 360 × 56 |
| 6 | Expanded | Loading | 18577:14679 | 360 × 64 |
Density/State mismatch lowercase state on composed Counter. Open