A discount-themed card variant inside a horizontally scrolling carousel — hero image, percent-off badge, and label.
Carousel Card rather than a separate component. Today's 3 variants collapse into Carousel Card props: variant: default | with-icon | discount, violator?: string, isLoading: bool.Discount Card appears in horizontally-scrolling voucher rails — GDeals, Voucher Pocket, "For You" promotions. Violator tag calls out freshness (New, Ending Soon, Limited).
Add label here
Add label here
PHP 200.00
_space_12 layer acts as a 12 px spacer via an invisible rectangle rather than a gap token.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | type=default | Banner + perforate + label + value. No violator. |
| With violator | Yes | Yes | type=with violator | Same layout with a blue tag anchored to the banner's top-right corner. Text is hardcoded "New". |
| Skeleton (loading) | Yes | Yes | type=skeleton loader | Flat 140 × 152 placeholder fill for the banner; rounded rectangles for title (27 h) and amount (10 h × 101 w). |
| Pressed | N/A | N/A | Not built | Cards tap through to voucher detail — needs a pressed state (subtle dim or scale) for feedback. |
- Duplicates Carousel Card's anatomy. Same 140-wide frame, same banner + text + skeleton composition. Ships as a second component with its own variants and tokens instead of a
variant=discounton the shared card. C1 · Layer Structure & Naming -
typeconflates layout and state.defaultandwith violatorare layout variants;skeleton loaderis a loading state. Packing them on one enum forces mutually-exclusive combinations that shouldn't be — a violator card can also be loading. C2 · Variant & Property Naming - Violator label hardcoded. The "New" string is baked into the variant — consumers can't show "Ending Soon", "Limited", or localized copy without detaching. C2 · Variant & Property Naming
- Perforated voucher edge baked into the banner image. The serrated bottom is part of a raster PNG, not a vector overlay. Ties every "discount card" to voucher visuals even when the use case is a plain promo card. C4 · Native Mappability
- Banner is a raster PNG with mask layers.
replace-this-asset+ mask intersect + overflow-clip is fragile on native (iOSAsyncImage/ ComposeAsyncImagedon't need any of that). Also blocks the image from being sized/cropped consistently. C6 · Asset & Icon Quality -
_space_12invisible rectangle used as a spacer. A 10.305 px tall#0500ffrectangle withopacity:0sits between label and value as a spacing hack. Should be agap/space-*token on the auto-layout. C1 · Layer Structure & Naming - No pressed state. Card is tappable (opens voucher detail) but no pressed/active appearance is modeled. C5 · Interaction State Coverage
- Code Connect mappings not registered. Blocked until the consolidation into Carousel Card is decided. C7 · Code Connect Linkability
- Consolidate into Carousel Card with
variant=discount. The entire Carousel family (Carousel Card, Carousel - Discount Card, Carousel - Item, Carousel Item - Center, Carousel Item - Side) shares a 140-wide frame and banner + text + skeleton composition. Merge the three "card" siblings into oneCarousel Cardwithvariant: default | with-icon | discount. Preserves every existing layout; eliminates redundant components. Family - Split
typeinto independent props. On the consolidatedCarousel Card:variant: default | with-icon | discount(layout),violator?: string(optional slot — any text, any variant),isLoading: bool(orthogonal state). 3 × 2 × 2 visual cases from 3 clean props instead of conflated enums. Property - Make the violator a named slot. Adopt Figma Slots for the top-right corner overlay. Accepts Badge instance or custom text — maps to
@ViewBuilder(SwiftUI) /@Composableslot (Compose) via Code Connect. Slot - Replace the perforate edge with a vector overlay. Today it's baked into the banner raster. Extract as a vector SVG rendered on top of the banner when
variant=discount. Token-bindable fill + crisp at any scale. Asset - Remove the
_space_12placeholder rectangle. Usegap: 12on the content auto-layout frame instead. Invisible elements used as spacers are a C1 anti-pattern — they clutter the layer tree and break native handoff. Property - Rename the value slot to match Figma's token. The peso amount binds to
main/carousel/color/valuebut renders as a standalone text. Expose asamount: Stringon the proposed Carousel Card so Code Connect can target it directly. Rename - Add a pressed state on the consolidated card. Subtle scale (0.98) or overlay tint when tapped. One state that covers all three variants on the merged component. State
- Banner should accept an Image instance, not a mask layer. Replace the
Asset Placeholder+replace-this-asset+ mask stack with a single image-fill slot on the banner frame. Cleaner handoff toAsyncImageon both platforms. Slot
Voucher card with perforated banner image, two-line label, and peso-value line.
Add label here
Add label here
PHP 200.00
Discount carousel card with label + brand-blue value pair on a white surface.
| Role | Token | Default |
|---|---|---|
| Label | carousel/color/label | #0A2757 |
| Discount | carousel/color/value | #2340A9 |
| Surface | bg/color-bg-main | #FFFFFF |
| Inverse text | text/color-text-inverse | #FFFFFF |
| Active dot | bg/color-bg-primary | #005CE5 |
Adds a blue violator tag anchored to the banner's top-right corner. Text is hardcoded "New" today — should be a parameterized slot.
Add label here
Add label here
PHP 200.00
Same default surface as Card 1, plus a red violator chip overlaying the image area.
| Role | Token | Default |
|---|---|---|
| Surface bg | main/discount-card/bg | #FFFFFF |
| Violator bg | main/discount-card/violator/bg | #D81E1E |
| Violator label | main/discount-card/violator/label | #FFFFFF |
| Title | main/discount-card/title | #0A2757 |
Loading pattern: flat banner fill, rounded title rectangle, shorter amount rectangle. Centered column (differs from the left-aligned default).
Loading state — every content slot becomes a rounded grey rectangle.
| Role | Token | Default |
|---|---|---|
| Skeleton bg | main/skeleton/bg | #EEF2F9 |
| Surface bg | main/discount-card/bg | #FFFFFF |
| Figma Property | SwiftUI | Compose |
|---|---|---|
type=default | variant: discount | .ebVariant(.discount) |
type=with violator | violator?: String (slot) | violator: String? / violatorSlot: (()->Badge)? |
type=skeleton loader | isLoading: Bool | loading: Bool |
| (hardcoded 2-line label) | label: String (2-line auto-wrap) | label: String |
| (hardcoded "PHP 200.00") | amount: String | amount: String |
| (mask + raster asset) | banner: Image (slot) | banner: Image |
| (baked perforate PNG) | (auto-rendered vector when variant=discount) | — |
(_space_12 invisible rect) | (auto-layout gap token) | — |
| (not modeled) | onTap?: () -> Void | onTap: (() -> Void)? |
| Requirement | iOS | Android |
|---|---|---|
| Card as a button | Wrap in Button; accessibilityLabel combines violator + label + amount. | Modifier.clickable { onTap() }.semantics(mergeDescendants = true) on the card. |
| Combined announcement | "New, 2% off GCrypto Bitcoin purchase, PHP 200.00" | Same reading order via TalkBack. |
| Loading state | Announce "Loading voucher" once on mount; suppress placeholder reads. | contentDescription = "Loading voucher" on the skeleton container. |
| Min touch target | 223 pt card height ≫ 44 pt ✓ | 223 dp ≫ 48 dp ✓ |
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Needs Refinement | _space_12 invisible rectangle acts as a spacer; Asset Placeholder + replace-this-asset leak authoring affordances. |
| C2 | Variant & Property Naming | Needs Refinement | type conflates layout and loading state; violator text hardcoded. |
| C3 | Token Coverage | Ready | All colors bound to main/carousel/color/*, bg/*, text/*. Typography via Primary/Multi-line Label/Small + Primary/Label/Fine. |
| C4 | Native Mappability | Needs Refinement | Perforate edge baked into the banner raster; mask-intersect image composition doesn't translate cleanly to AsyncImage. |
| C5 | Interaction State Coverage | Needs Refinement | Default + skeleton built. Missing pressed for a tappable card. |
| C6 | Asset & Icon Quality | Needs Refinement | Banner + perforate ship as raster PNGs. |
| C7 | Code Connect Linkability | Not Mapped | Blocked until consolidation into Carousel Card is decided. |
type (3) = 3 variants. Single-axis enum conflates layout (default, with violator) with loading state (skeleton loader) — should split into variant + violator? + isLoading on consolidation.
| type | Node | Dimensions | Notes |
|---|---|---|---|
| default | 18543:2762 | 140 × 223.48 | Banner + label + value. Left-aligned. |
| with violator | 18543:2770 | 140 × 223.48 | Adds blue "New" tag top-right of banner. |
| skeleton loader | 18543:2782 | 140 × 211 | Flat banner fill + 2 rounded rect placeholders. Centered column. |
variant=discount + violator? slot + isLoading. Anatomy duplicates Carousel Card. Open_space_12 invisible spacer — Replace with gap: 12 on auto-layout. Opentype conflates layout + state — Split into variant, violator?, isLoading on consolidated Carousel Card. Parameterize violator text. Open