East Blue
KeepNeeds Refinement
Countdown - Promo Component link

A promotional card built around a countdown — a line of copy, the timer, a full-width action and a dismiss control. Two levels of emphasis.

Keep — the recipe the last review asked for
All four DS Health traits pass. This card used to live inside Countdown as a fourth "style", which is what made that component hard to reason about — a promo with copy, a button and a close icon is not a sibling of a timer layout. It is now its own component composing the timer, with the button and the dismiss control as real slots rather than baked instances.
In Context

Runs full-width on the dashboard and on Discover, above the fold, for a campaign with a deadline. High emphasis for a headline promotion, Low for one that sits alongside other cards without shouting.

Live Preview
Properties
Emphasis
headerText
hasCTA
hasCloseButton
DS Health
Reusable
Pass
One card serves any campaign with a deadline. The copy, the action and the dismiss control are all supplied by whoever uses it.
Self-contained
Pass
Carries its own surface, radius, spacing and both emphasis levels, and picks the countdown treatment that contrasts with its own card.
Consistent
Pass
Both versions read Container → ⤷ ActionSlot → ⤷ CloseSlot, using the slot naming the rest of the system uses.
Composable
Pass
Composes Countdown for the timer and takes a button and a close icon through slots, so none of the three is baked in.
Behavior
State iOS Android Figma Property Notes
Emphasis=High N/A N/A #1972F9 card Blue card, white copy, and a pale countdown so the timer reads against it.
Emphasis=Low N/A N/A #FFFFFF card White card, dark copy, and a gradient blue countdown to carry the emphasis instead.
⤷ ActionSlot N/A N/A full-width bar Holds a Button - XSmall stretched edge to edge, which keeps the button's padding and label size.
⤷ CloseSlot N/A N/A 16 × 16 Dismiss control, top right.
Issues
  • The action bar is a Button pushed out of shape. The ⤷ ActionSlot holds a Button - XSmall overridden away from what Button is: 24px tall against Button’s 26, square against its 99px pill, filling 360 where Button hugs, and on the High card painted #2340A9 — Button’s pressed fill — as a resting state. A developer handed action: EBButton? would render a 26px enabled-blue pill, not the full-bleed bar the design shows, so the slot cannot be mapped as it stands. Two ways out, and it needs a decision rather than a fix: either Button gains a full-bleed appearance that this can use, or Promo’s action stops being a Button and becomes its own treatment. C4 · Native Mappability
  • The promo is its own component. It used to be a fourth value on Countdown's Style setting, sitting alongside three timer layouts as though a card with copy and a button were the same kind of thing. Splitting it is what let both components get simple. C1 · Layer Structure & Naming
  • The copy, button and close are no longer baked in. The old card hard-coded its header text, its call to action and its close icon, so any campaign that needed different words meant detaching. #title is a text layer, and the other two are ⤷ ActionSlot and ⤷ CloseSlot. C1 · Layer Structure & Naming
  • The emphasis setting says what it controls. It was Theme = Light | Dark, then briefly Color = Light | Blue. Neither fitted: the card is not following a system theme, and naming a value after its hue is the pattern the guidelines rule out — and the one this very component fixed when White and Blue stopped being Style values. It is now Emphasis = High | Low, which survives a palette change. C2 · Variant & Property Naming
  • The dismiss control is visible enough to find. The close icon sat at roughly 1.9:1 against the white card — about half what an interactive control needs. Both versions were re-coloured and now clear 3:1. C3 · Token Coverage
  • Slot and wrapper names match the system. Action Slot and Icon Slot became ⤷ ActionSlot and ⤷ CloseSlot, and container became Container — the same vocabulary as Ad Space, Tooltip and the Date Picker family. C1 · Layer Structure & Naming
Recommendations
  • Replace the placeholder copy before handoff. #title currently holds "Hurry up! ⏎Sale ends in:" — a manual line break and a double space. It is a stand-in while the content model is undecided, but a forced break will not survive translation, so the native side should let the string wrap on its own. Docs
  • Document that the countdown inside is deliberately inverted. The High card holds a pale timer and the Low card a bold one, so each reads against its own surface. It looks backwards in the layers panel and is worth stating plainly, or someone will "fix" it. Docs
  • Give dismissal somewhere to go. The card can be dismissed but nothing says what that means — gone for the session, gone permanently, or gone until the campaign changes. Define it alongside the timer contract so every surface behaves the same way. Docs
  • Announce the card as one region. Copy, timer, action and dismiss read separately is four unrelated fragments. Group them so a screen reader announces the promotion, the time remaining and the action together, with the dismiss control reachable but not first. A11y
Emphasis
High
Properties
headerText
hasCTA
hasCloseButton
Properties
Emphasis High
headerText Hurry up! Sale ends in:
⤷ ActionSlot (slot) Button - XSmall · 2 items
⤷ CloseSlot (slot) Close · 2 items
hasCTA true
hasCloseButton true
Countdown inside Style=One · Surface=onColor · resized to 173 × 44 with 8px padding and an 8px gap
Versions 2
Colors
Card #1972F9
#title #FFFFFF
ActionSlot bar #2340A9
ActionSlot #label #FFFFFF
CloseSlot icon #F6F9FD @ 80%
Typography
#title Primary/Label/Base
ActionSlot #label Primary/Label/Small
Layout
Height 92px — 68px content row + 24px action bar
Width 360px
Radius 0px — the card is square
Padding H 20px left · 24px right, in the content row
Padding V 12px in the content row · 5px in the action bar
Gap Auto — the title and the timer sit at opposite ends; 0 between the content row and the action bar
Alignment Center
Colors by Emphasis

The five roles Promo paints itself. Everything inside the timer belongs to Countdown and carries its tokens: text/color-text-primary for the numbers and labels, text/color-text-inverse-weak for the label on the brand fill, bg/color-bg-info-weak and border/color-border-info for the separator on the onLight and onColor surfaces. Promo swaps which Countdown surface it places, not those colours.

Role Token HighLow
Card bg/color-bg-brand · bg/color-bg-main #1972F9 #FFFFFF
#title text/color-text-inverse · text/color-text-info-strongest #FFFFFF #071969
ActionSlot bar bg/color-bg-primary-hover · bg/color-bg-primary #2340A9 #005CE5
ActionSlot #label text/color-text-inverse #FFFFFF #FFFFFF
CloseSlot icon border/color-border-inverse-weak · border/color-border-stronger #F6F9FD @ 80% #7E96BE
Countdown inside placed as Style=One, Surface follows the emphasis onColor — white on the brand card onLight — the brand gradient on white
Low
Properties
headerText
hasCTA
hasCloseButton
Properties
Emphasis Low
headerText Hurry up! Sale ends in:
⤷ ActionSlot (slot) Button - XSmall · 2 items
⤷ CloseSlot (slot) Close · 2 items
hasCTA true
hasCloseButton true
Countdown inside Style=One · Surface=onLight · resized to 173 × 44 with 8px padding and an 8px gap
Versions 2
Colors
Card #1972F9
#title #FFFFFF
ActionSlot bar #2340A9
ActionSlot #label #FFFFFF
CloseSlot icon #F6F9FD @ 80%
Typography
#title Primary/Label/Base
ActionSlot #label Primary/Label/Small
Layout
Height 92px — 68px content row + 24px action bar
Width 360px
Radius 0px — the card is square
Padding H 20px left · 24px right, in the content row
Padding V 12px in the content row · 5px in the action bar
Gap Auto — the title and the timer sit at opposite ends; 0 between the content row and the action bar
Alignment Center
Installation Planned API

iOS — Swift Package Manager

.package(url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0")

Android — Gradle (Kotlin DSL)

implementation("com.eastblue.ds:countdown:1.0.0")

Import

import EastBlueDS
import com.eastblue.ds.countdown.*

Planned API — the native library does not exist yet. The artifact is the Countdown family, not this component: Countdown, Unit and Promo all ship in <code>com.eastblue.ds:countdown</code> and import <code>com.eastblue.ds.countdown.*</code>.

Property Mapping

Six properties, in the order the Figma property panel lists them. The deadline is not among them: Figma places a static Countdown instance showing 00:00:00:00, so nothing in the file states when the promotion ends — natively that is until, and the platform derives the timer from it. Each slot keeps its boolean alongside it rather than being inferred from a nil parameter, so every Figma property traces 1:1 for Code Connect.

Figma PropertySwiftUICompose
Emphasis — High, Low emphasis: EBCountdownPromoEmphasis emphasis: EBCountdownPromoEmphasis
headerText — free text headerText: String headerText: String
hasCTA — true, false hasCTA: Bool = true hasCTA: Boolean = true
hasCloseButton — true, false hasCloseButton: Bool = true hasCloseButton: Boolean = true
⤷ ActionSlot (slot) action: EBButton? — shown when hasCTA action: @Composable (() -> Unit)? — shown when hasCTA
⤷ CloseSlot (slot) onDismiss: (() -> Void)? — shown when hasCloseButton onDismiss: (() -> Unit)? — shown when hasCloseButton
Usage Snippets Planned API
High — a headline promotion
EBCountdownPromo(
    headerText: campaign.headline,
    until: campaign.endsAt,
    emphasis: .high
)
EBCountdownPromo(
    headerText = campaign.headline,
    until = campaign.endsAt,
    emphasis = EBCountdownPromoEmphasis.High
)
Low — dismissible, alongside other cards
EBCountdownPromo(
    headerText: voucher.headline,
    until: voucher.expiresAt,
    emphasis: .low,
    onDismiss: { feed.dismiss(voucher) }
)
EBCountdownPromo(
    headerText = voucher.headline,
    until = voucher.expiresAt,
    emphasis = EBCountdownPromoEmphasis.Low,
    onDismiss = { feed.dismiss(voucher) }
)
Accessibility
RequirementiOSAndroid
The card is one region .accessibilityElement(children: .contain) with the title as its label Modifier.semantics(mergeDescendants = true)
Time remaining is spoken, not spelled out The countdown supplies one formatted duration Same, via the countdown's contentDescription
Dismiss is reachable but not first Place it last in the accessibility order with a clear label Same, via traversal order and onClickLabel
The action is a button .accessibilityAddTraits(.isButton) on the bar Role.Button on the bar
Usage Guidelines

Do

Let the card pick the countdown treatment that contrasts with it — pale timer on the blue card, bold timer on the white one.

Don't

Don't match the timer to the card; it will disappear into its own background.

Do

Use High for one headline promotion per surface.

Don't

Don't stack two High cards — competing urgency reads as neither.

Do

Give the dismiss control a defined lifetime.

Don't

Don't ship a close button that brings the card back on the next launch.

Criteria Scorecard
ID Criterion Status Notes
C1 Layer Structure & Naming Ready Container → ⤷ ActionSlot → ⤷ CloseSlot in both versions, matching the system's slot vocabulary.
C2 Variant & Property Naming Ready Emphasis = High | Low — semantic, and survives a palette change.
C3 Token Coverage Ready Card, copy, action and dismiss all bound. The close icon now clears 3:1 on both versions.
C4 Native Mappability Needs Refinement Five parameters, no web-only pattern — but the ⤷ ActionSlot does not map as it stands. It holds a Button - XSmall overridden away from what Button is: 24px tall against Button’s 26, square against its 99px pill, filling 360 against its hug, and on the High card painted #2340A9 — Button’s pressed fill — as a resting state. A developer given action: EBButton? would render a 26px enabled-blue pill, not the full-bleed bar the design shows. Either Button gains a full-bleed appearance or Promo’s action becomes its own treatment; the slot maps once that is decided.
C5 Interaction State Coverage Not Applicable The card has no states of its own — pressed behaviour belongs to the button and the dismiss control.
C6 Asset & Icon Quality Ready The close icon is a vector instance in a slot.
Variants Inventory (2 total)

1 Emphasis setting × 2 values. The countdown inside always runs opposite to the card.

EmphasisCardCountdown insideNode
High#1972F9Style=One, Surface=onColor5630:36045
Low#FFFFFFStyle=One, Surface=onLight5630:36046
1.0.2 — September 2026Patch
The action-slot mapping gap is on the record — node 5630:36047
C4 was flagged with nothing behind it. The scorecard had read Needs Refinement since the reassessment, and the reason sat only in its own note — so the page showed an amber badge that nothing in Open Issues explained. The finding is now where a reader meets it: the ⤷ ActionSlot holds a Button - XSmall overridden away from what Button is — 24px tall against 26, square against the 99px pill, filling 360 where Button hugs, and painted with Button’s pressed fill as a resting state. It needs a decision rather than a fix: either Button gains a full-bleed appearance, or Promo’s action stops being a Button.
C4 open
1.0.1 — September 2026Patch
Style and Code tabs rebuilt, three properties documented — node 5630:36047
Three of the four properties were undocumented. The page described Emphasis; the Figma panel also carries headerText, hasCTA and hasCloseButton. Nothing changed in Figma — they were always there. get_node_info cannot read property definitions, so this is the third component in the family where the panel held more than the node tree could show.
C2 resolved
One card per Emphasis value. High and Low, in the panel’s order, with the three content properties as controls on both rather than as extra cards.
Docs
Layout was derived from bounding boxes; it is now read from the panel. All seven keys: 20px left and 24px right in the content row, 12px vertical, 5px in the action bar, gap Auto, radius 0. Four of the old values were labelled "(derived)" and three of those were wrong.
Docs
The nested Countdown is overridden three ways. The instance is resized from 237 to 173 wide, its padding cut from 16/16 to 8/8 and its gap from 16 to 8, so copy can sit beside it. Recorded on the Properties row of both cards; Style=One cannot express a narrow timer on its own.
Docs
Preview geometry and typeface corrected. The container drew no padding, the timer inherited Countdown’s full 237px width and overflowed the card, the action bar had none of its 5×8 padding, the card carried a radius it does not have, and the whole preview rendered in the documentation font instead of Proxima Soft.
Docs
#title renders its line break again. Figma sets the placeholder as Hurry up! then a break then Sale ends in:. A text input cannot hold a newline, so the control shows the flattened string and the break is restored while it is untouched.
Docs
Three of the four tabs rendered nothing. Two surplus </div> in livePreviewHtml closed [data-tab-group] two levels early, which left the Style, Code and Changelog panels outside the root switchTab queries — clicking a tab deactivated Overview and had no panel to activate. The markup and the previews were correct throughout; only the nesting was wrong, and both the build and the lint passed. Measured across all 95 components afterwards: no other page is affected.
Docs
The install block pointed at coordinates that will never exist. gcash/east-blue-ios and com.gcash.eastblue:components:1.0.0, with no Import line. It now cites the Countdown family artifact com.eastblue.ds:countdown:1.0.0 and imports com.eastblue.ds.countdown.* — the third and last member of the family onto the family artifact.
Docs
Property Mapping was missing two properties and misnamed a third. hasCTA and hasCloseButton had no rows; #title named the layer rather than the headerText property; and a Countdown row mapped something that is not a property at all. Six rows now, one per property. The deadline is named in the description instead: Figma places a static instance showing 00:00:00:00, so nothing in the file says when a promotion ends.
Docs
The Code tab contradicted the Style tab. Usage Snippets called title: and emphasis = High where the Style tab’s DEV code calls headerText: and EBCountdownPromoEmphasis.Low. Both now use one API, and the two snippets are keyed to the Emphasis values rather than to placement.
Docs
The DEV snippet split its own string literal. getSnippet interpolated the placeholder’s real line break straight into the Swift literal, so switching language tabs printed a string broken across two lines. It escapes to \n now, the way a developer would have to write it.
Docs
Code Connect emptied. Three rows describing readiness for a registration that cannot happen while the native library does not exist.
Docs
C4 held open: the action slot holds a button that is no longer a button. ⤷ ActionSlot takes a Button - XSmall overridden four ways — 24px tall against Button’s 26, square against its 99px pill, filling 360 against its hug, and on the High card painted #2340A9, which is Button’s pressed fill, as a resting state. A developer given action: EBButton? would render a 26px enabled-blue pill, not the full-bleed bar the design shows. Native Readiness moves from Ready to Needs Refinement; the Overview issue and the fix are a separate pass.
C4 open
1.0.0 — August 2026Initial
Split out of Countdown · node 5630:36047
Extracted from Countdown. The promo card was a fourth value on that component's Style setting, sitting alongside three timer layouts.
Family
C1 — copy, action and dismiss are no longer baked in. #title is a text layer; the button and close icon are slots.
C1 resolved
C2 — Theme became Emphasis = High | Low, after a pass through Color = Light | Blue, which repeated the hue-as-value pattern the guidelines rule out.
C2 resolved
C3 — the dismiss control was re-coloured on both versions, from roughly 1.9:1 to about 3.0:1 on the white card and 3.2:1 on the blue.
C3 resolved
C1 — slot and wrapper names aligned: ⤷ ActionSlot, ⤷ CloseSlot, Container.
C1 resolved
#title still holds placeholder copy with a manual line break, kept while the content model is undecided.
Docs