Skip to content

<goma-highlight-card> ​

A single highlight card for one sporting event: a hero image band over a compact event summary (competition header with a favourite star, two team rows with optional crests, a kick-off block, a market-type pill and an odds row) plus an optional "See other markets" link pill. It is the CMS-driven "what's on" card: every field is host-fed, nothing is read from a socket.

One card per element: the host owns the rail. The widget renders exactly one card and carries no carousel, dots, arrows, peek or section header. A host that wants a rail loops its own array and assigns one object per element, the same shape <goma-suggested-bet> uses.

Live demo ​

Open in the playground →: toggle Highlight card in the widget rail. The bundled mock source renders a fully populated card; switch the inspector's source to prop and paste your own object into the JSON tab to try a CMS record.

Element tag ​

html
<goma-highlight-card></goma-highlight-card>

Installation ​

bash
pnpm add @gomagaming/highlight-card vue pinia vue-router vue-i18n @vueuse/core autobahn-browser

The framework packages are peer dependencies installed once at the app level; bundlers dedup them across every @gomagaming/* widget. autobahn-browser is declared because the shared @gomagaming/sports-domain code the card reuses imports it: this widget never opens a socket itself. See Installation for the one-time .npmrc token setup.

Configuration ​

el.config = { locale, theme, messages, debug }: the standard widget config bag. In the default dataSource: 'prop' mode the widget makes no network calls, so there is nothing else to supply. The auto modes ('rpc' / 'subscribe') additionally need socketUrl: see below.

KeyTypeDescription
localestringActive i18n locale ('en' / 'fr' / 'pt'). Defaults to 'en'.
favoritesApiBaseUrlstringFavourites REST base URL. When set, star taps on this widget are saved to the player's favourites, also on a page with no <goma-favorites>, when a signed-in session is available on the page. The operator is ucsOperatorId.
themeRecord<string,string>--goma-* token overrides.
messagesobjectOptional i18n phrase overrides.
debugbooleanDefaults to false.
socketUrlstringAuto mode only. WAMP endpoint. Required by dataSource: 'rpc' | 'subscribe'; without it the widget warns once and degrades to the host-fed card.
socketRealmstringAuto mode only. WAMP realm.
ucsOperatorIdstringAuto mode only. Operator id used by the feed.

Data sources ​

The card property is always the entry point. What fills it in depends on dataSource:

  • 'prop' (default): fully host-fed. Every field arrives on el.card and no socket is opened.
  • 'subscribe': auto, live. The host supplies only what a CMS owns (image, eventId, defaultMarket / marketId) and the widget looks the event up by id, so any live or prelive event resolves. It fills in teams, competition, kick-off, market name, outcome labels and odds from the live feed, keeping the odds ticking.
  • 'rpc': auto, one-shot. Same resolution, but the topics are released once a complete priced snapshot has landed.

Switching a mounted card's dataSource to 'prop', or clearing its card, releases the topics it had open rather than holding them until the element leaves the page, which matters because every widget on the page shares one socket. Switching back to a live mode reopens them.

Precedence in the auto modes: the host wins for anything a human wrote (team names, competition, market name, kick-off), and the feed wins for identity and price (marketId, bettingTypeId, eventPartId, eventId, isLive and every outcomes[] field). Those go on the place-bet wire, where a stale CMS value is rejected. Presentation (image, cta, crests, flags) is host-only and never touched. Until the event lands the card shows a skeleton; if the host supplied display fields it shows those instead, and a 15 s watchdog stops it waiting forever.

card takes a live object (JS property) or a JSON string (HTML attribute / the playground's JSON control). A malformed JSON string, a non-object, or an empty object is treated as "no card" and renders the empty state.

Choosing the market (auto modes) ​

The host is responsible for the market config. defaultMarket (or marketId) must name a market that the card's event carries. The widget does not pick another market for you (unless you set marketFallback: 'first').

This card's feed carries the event's ~5 main markets (Home Draw Away, Over/Under, Both Teams To Score and similar). For one of those, name it any way below: it costs no extra download. For a non-main market (a scorer market, say), also set matchOdds: 'auto', and name the market with its period:

defaultMarket formExampleWhat the card downloads for the market
{bettingTypeId}_{eventPartId} (recommended)'69_3'A main market: nothing extra. A non-main market with matchOdds: 'auto': only that market's feed, {eventId}/match-odds/69-3 (tens of KB)
bettingTypeId only'69'A main market: nothing extra. A non-main market with matchOdds: 'auto': every market of the match (several MB per match on a real operator)

When the market is not found, the card renders with no odds row. Once it has stayed that way for 5 s, the widget logs one console warning per event and market, listing the markets the card can see for that event:

[goma-highlight-card] market "30" is not on event 317502630930751488, so the card shows no odds. Markets this card can see for it: 69_3 (Home Draw Away), 47_3 (Over/Under), 76_3 (Both Teams To Score), ...

Fix the card's defaultMarket (pick one the warning lists), or set matchOdds: 'auto' for a non-main market. Prefer that over marketFallback: 'first', which shows a market the CMS did not choose.

One record shape, two widgets ​

<goma-highlight-card> and <goma-pros-choice> take the same host-fed record: the GomaPromoCard below. The two ship together, compose the same shared card pieces and are meant to be authored from one CMS record type, so a client builds one mapping table, not two. Feed the same object to either element and it renders; feed it to both and they produce identical betslip legs.

Two caveats, and only two:

  • homeTeamLogo / awayTeamLogo are highlight-card-only. This is the only card that draws team crests. <goma-pros-choice> ignores them.
  • fixtureLabel is read by both but rendered by only one.<goma-pros-choice> paints it as its fixture pill; this card draws two stacked team rows instead and uses the value only to name the synthesized event (which is what reaches the betslip leg). Its precedence (explicit value, else `${homeTeam} - ${awayTeam}`, else whichever single name exists) is identical in both.

Everything else is shared, means the same thing on both, and is spelled the same way.

The host-fed card contract ​

This is the contract a CMS would eventually populate. Everything is optional except id: the card degrades field by field rather than failing, so a partially-populated CMS record still renders.

ts
interface GomaPromoCard {
  // ── Identity ──────────────────────────────────────────────────────────
  /** Stable slot key for this card. Echoed on goma:highlight-select /
   *  goma:highlight-cta. It is NOT an event id and is never advertised as
   *  one: goma:event-navigate carries `eventId` or does not fire.
   *  CMS-populated. */
  id: string

  /** The provider's event id, when the CMS record is bound to a real
   *  fixture. Kept as a STRING end-to-end: provider ids are 18-digit
   *  snowflakes that exceed Number.MAX_SAFE_INTEGER. It GATES
   *  goma:event-navigate: without it neither a card-body tap nor the "See
   *  other markets" link emits one, so nothing drives <goma-game-details>:
   *  and it is what makes the favourite star addressable. CMS-populated (as
   *  an editorial pick). */
  eventId?: string

  // ── Presentation: never resolvable from a feed ───────────────────────
  /** Hero image URL. Rendered as a full-width 100 px band, object-fit:
   *  cover, cropped at `center 25.7%`. Without it the band paints
   *  --goma-backgroundCards. Always CMS-populated. */
  image?: string
  /** Blank (the default) keeps the band decorative: the shared hero marks it
   *  aria-hidden. Supply one only when the artwork carries meaning the
   *  surrounding copy does not. */
  imageAlt?: string
  /** Shorthand for `cta.url`: the destination of a card-body or "See other
   *  markets" tap. Reported on goma:highlight-select (as `ctaUrl`) and
   *  goma:highlight-cta (as `url`); the widget never follows it itself, and
   *  separately emits goma:event-navigate when the card carries an `eventId`.
   *  `cta.url` wins when both are set. CMS-populated. */
  ctaUrl?: string

  // ── Event summary ─────────────────────────────────────────────────────
  /** Competition name in the header row, e.g. "Premier League". */
  competitionName?: string
  /** The competition's provider id. Nothing on the card renders it: it
   *  rides onto the synthesized event so it reaches the betslip leg. */
  competitionId?: string
  /** Resolves the sport glyph: `${iconBaseUrl}/icons/sports/dark_mode/
   *  {sportId}.svg`, or the flat glyph bundled in @gomagaming/core when no
   *  `iconBaseUrl` is set. */
  sportId?: string | number
  /** Explicit sport-icon URL. Wins over both the `iconBaseUrl` convention
   *  and the bundled glyph. */
  sportIconUrl?: string
  /** Region id for the flag lookup: `${flagBaseUrl}{venueId}.png`.
   *  Falls back to a 🌐 glyph when absent or the image 404s. */
  venueId?: string | number
  /** Explicit flag image URL. Wins over the venueId lookup. */
  flagUrl?: string

  homeTeam?: string
  awayTeam?: string
  /** SHARED, but rendered only by <goma-pros-choice> (as its fixture pill).
   *  Here it names the synthesized event, which is what reaches the betslip
   *  leg. Defaults to `${homeTeam} - ${awayTeam}`, falling back to whichever
   *  single name exists. Same precedence in both widgets. */
  fixtureLabel?: string
  /** HIGHLIGHT-CARD ONLY: 19x19 team crests. <goma-pros-choice> draws no
   *  crests and ignores these. Host-fed and optional: no provider field is known
   *  to carry crest artwork, so there is no CDN convention to infer. When
   *  neither is supplied both crest slots are dropped and the names sit
   *  flush; when one is supplied both slots are reserved so the two rows
   *  stay aligned. */
  homeTeamLogo?: string
  awayTeamLogo?: string

  /** Epoch ms (or anything `new Date()` parses). Formatted into the
   *  today/tomorrow date line + HH:mm time by the shared kick-off block. */
  startTime?: number | string
  /** Pre-localised overrides. When set they win outright over `startTime`,
   *  so a host that has already formatted its copy is never re-formatted. */
  dateLabel?: string
  timeLabel?: string

  // ── Market + odds ─────────────────────────────────────────────────────
  /** Market id. Rides through to the betslip leg. Defaults to a synthetic
   *  `{eventId|id}-m` when omitted. */
  marketId?: string
  /** The promoted market's name, e.g. "1X2 TR": the provider's own term for
   *  it. Copy for the market-type pill here, the market headline in
   *  <goma-pros-choice>; the pill is hidden when it is blank. Also used as
   *  the betslip leg's market name. */
  marketName?: string
  /** REQUIRED for a placeable betslip leg. The provider rejects a slip with
   *  "Invalid or missing Betting Type Id" without it, and a host-fed card is
   *  the only place it can come from. */
  bettingTypeId?: string | number
  /** Its pair: the two together key a market. Same warning applies. */
  eventPartId?: string | number
  /** The market's outcomes, in render order: Home / Draw / Away here,
   *  player names on a scorer market. THIS card caps the row at 3 (the
   *  design draws three columns); <goma-pros-choice> keeps every entry and
   *  scrolls. Two outcomes render as two full-width buttons (see Odds row
   *  below).
   *
   *  (Not "selections": in this codebase a selection is a leg on a betslip.
   *  These are market outcomes, which is also the provider's term.) */
  outcomes?: Array<{
    /** Abstract outcome id: what the betslip keys selections by.
     *  `id` is accepted as a synonym. */
    outcomeId?: string
    /** Button's top line, e.g. "Liverpool F.C" / "Draw". `name` and
     *  `outcomeName` are accepted as synonyms. */
    label: string
    /** Decimal odds. `odd` is accepted as a synonym. Missing renders the
     *  button's loading spinner, not `0.00`. */
    odds: number
    /** Price-tied offer id. Required on the place-bet payload alongside
     *  the outcome id, and NEVER fabricated: omit it and the leg carries
     *  `null` plus the betslip's own warning. `offerId` is accepted as a
     *  synonym. */
    bettingOfferId?: string
    /** Defaults to true. `false` renders the suspended (padlock) state. */
    isAvailable?: boolean
  }>

  /** Recorded on the synthesized event so a betslip leg is flagged live.
   *  NOTHING on the card changes: the design has no live variant (no badge,
   *  score or clock), but a leg mislabelled prelive is priced wrongly. */
  isLive?: boolean

  // ── Chrome ────────────────────────────────────────────────────────────
  /** One-way seed for the favourite star: when `true`, and `eventId` is set
   *  AND the player is signed in: the id is merged into the shared
   *  `goma:favorites:v1` envelope once. It can only add, never remove, so it
   *  cannot clobber what <goma-favorites>' server sync has already resolved,
   *  and it never invents a favourite for a signed-out visitor. The session
   *  resolves asynchronously, so the seed lands when the session arrives. */
  isFavorite?: boolean

  /** The canonical CTA shape for the "See other markets" link pill. Both
   *  fields are optional, and declaring `cta` at all (even `{}`) is what
   *  turns the pill on: together with the element-level `showCta`. The
   *  `ctaUrl` shorthand above also turns it on. CMS-populated. */
  cta?: {
    /** Label override. Falls back to the element's `ctaLabel`, then to the
     *  English default "See other markets". */
    label?: string
    /** URL reported on goma:highlight-cta. Falls back to `ctaUrl`. */
    url?: string
  }
}

Accepted synonyms. The canonical spelling is the one documented above. Three fields also accept alternatives, kept because they cost nothing and make a hand-written CMS record forgiving: both widgets accept exactly this set:

CanonicalAlso accepted
outcomes[].oddsodd
outcomes[].labelname, outcomeName
outcomes[].outcomeIdid
outcomes[].bettingOfferIdofferId
cta.urlctaUrl (top-level shorthand)

Which fields the auto modes resolve ​

In dataSource: 'rpc' / 'subscribe' these are the fields the feed fills in; the rest stay host-fed forever. A host value still wins for the DISPLAY rows (an editor's chosen wording survives the feed) while the feed wins outright for identity and price, which go on the place-bet wire.

FieldFeed-resolved?
homeTeam / awayTeam / fixtureLabel / competitionNameYes
sportId → sport glyph, venueId → flagYes
startTime (and therefore the date/time lines)Yes
marketName, outcomes, outcomes[].oddsYes, and in 'subscribe' the odds tick live
bettingTypeId / eventPartIdYes: taken from the resolved market; must be host-fed in 'prop' mode
isFavoritePartly: seeded by the host, then owned by the shared cache
image, imageAlt, ctaUrl, cta.*No: always host-fed CMS content
homeTeamLogo / awayTeamLogoNo: no known feed field carries crests

The same split holds for <goma-pros-choice>, which shares the record: the presentation fields (image, imageAlt, cta) are permanently CMS-owned in both, and everything else is provider data an integration could one day resolve from eventId instead of copying across.

Props / attributes ​

PropertyAttributeTypeDefaultDescription
cardcard (JSON)GomaPromoCard | nullnullThe single host-fed card object: the same record <goma-pros-choice> takes.
dataSourcedata-source'prop' | 'rpc' | 'subscribe''prop'Data mode. 'prop' is fully host-fed. 'subscribe' resolves the event and the named market from the live feed and keeps the odds ticking; 'rpc' resolves a one-shot priced snapshot and then releases its topics. Both auto modes need config.socketUrl: without one the widget warns once and degrades to the host-fed card. An unsupported value warns once and falls back to 'prop'.
sportEventLimitsport-event-limitnumber20Deprecated, no effect. The card resolves its event by id, so there is no sport feed to cap. Still accepted so existing pages do not break.
marketFallbackmarket-fallback'none' | 'first''none'Auto mode only. What to do when the resolved event does not carry the market the card named. 'none' renders the card with no odds row: a promo card silently promoting a market the CMS did not choose is worse than one showing no prices. 'first' falls back to the event's leading main market.
matchOddsmatch-odds'auto' | booleanfalseAuto mode only. Whether to also open the per-match {eventId}/match-odds feed, which carries the FULL market set. false here because this card promotes a MAIN market, which the by-id events feed already streams inside its ~5-main-market window. Set 'auto' when the card promotes a non-main market; true always opens it. Costs one extra topic per card. When defaultMarket (or marketId) names the market as {bettingTypeId}_{eventPartId} (for example 30_3), only that market's feed is opened, {eventId}/match-odds/{bettingTypeId}-{eventPartId} (tens of KB); a bare bettingTypeId opens every market of the match (several MB on a real operator).
showFavoriteshow-favoritebooleantrueFavourite star in the competition header.
showCtashow-ctabooleantrueThe "See other markets" link pill.
showBordershow-borderbooleantrueThe 1 px gradient hairline around the card.
ctaLabelcta-labelstring''CTA copy for this element; blank → card.cta.label, then the English default "See other markets".
emptyMessageempty-messagestring''Message rendered when there is no card. Blank → the widget renders nothing.
heroHeighthero-heightnumber100Hero band height in px.
heroObjectPositionhero-object-positionstring'center 25.7%'CSS object-position for the hero crop. The default reproduces the design's crop.
flagBaseUrlflag-base-urlstring''Base URL for {venueId}.png flags; blank falls back to the shared flag CDN.
iconBaseUrlicon-base-urlstring''Sport-icon base: ${iconBaseUrl}/icons/sports/dark_mode/{sportId}.svg. Blank falls back to the glyph bundled in @gomagaming/core; a per-card sportIconUrl beats both.
localelocalestring'en'Active locale.
themetheme (JSON)Record<string,string>{}--goma-* overrides.

Odds row ​

The odds buttons are the shared RegularOdds component every other gomawt card uses: unchanged, so the betslip legs it produces are byte-identical to the ones <goma-events-horizontal> produces. The card synthesizes the provider-shaped event / market / outcome objects the button expects from the host-fed fields. The synthesized event's id is card.eventId or null: never card.id, because that value is copied straight onto the betslip selection and onto goma:outcome-select, where a CMS slot key would reach place-bet looking like a real event id. A card with no eventId still renders and still prices; its legs simply cannot claim an event.

The design draws three columns. The row is a flex row of fill children, so a 2-outcome market (Asian handicap, tennis) spreads across the full width instead of leaving a hole where a third column would be. More than three outcomes are capped at three; zero outcomes hides the row entirely.

Selected, suspended and odds-tick states are inherited from RegularOdds unmodified: --goma-buttonBackgroundPrimary when selected, --goma-backgroundDisabledOdds + a padlock when isAvailable: false.

Events ​

All events bubble and are composed: true (they cross the Shadow DOM boundary).

EventdetailNotes
goma:outcome-select{ bettingOfferId, outcomeId, outcomeName, odds, eventId, marketId, bettingTypeId, eventPartId, line }An odds button was selected. Identical shape to every other odds button, so both betslips pick it up with no wiring.
goma:outcome-deselect{ bettingOfferId, outcomeId }An already-selected odds button was tapped again.
goma:event-navigate{ eventId, homeTeam, awayTeam, competitionName }The card body or the "See other markets" link was tapped. Emitted only when card.eventId is set: the card's own id is a CMS slot key and is never advertised as an event id, so a card without eventId emits nothing here. Drives <goma-game-details> through its default-on followEventNavigate.
goma:highlight-select{ id, eventId: string | null, ctaUrl: string | null }The card was activated as a whole, fired alongside goma:event-navigate (which itself only fires when eventId is set). ctaUrl is the resolved destination: card.cta.url, else the card.ctaUrl shorthand, else null. Host-only: no shipped widget listens for it.
goma:highlight-cta{ id, eventId: string | null, label: string, url: string | null }The "See other markets" link was tapped. Kept separate from goma:highlight-select because it means something different. Host-only, but not inert: goma:event-navigate fires alongside it whenever the card carries an eventId, so the link opens <goma-game-details> with no host code. Exactly as the sibling <goma-pros-choice> link does. url is reported for hosts that route somewhere of their own.
goma:favorite-toggle{ type: 'event', id, eventId, isFavorite }The star was tapped while signed in. Also written into goma:favorites:v1.
goma:login-required{ reason: 'favorite', type: 'event', id }The star was tapped while signed out. The star does not write: the host drives login.
goma:seo-data{ widget: 'highlight-card', type: 'sports-event', items: object[] }Fired once the card's event resolves, and again only when its identity changes: never on an odds tick. Suppressed until the resolved event carries a real team name: covers both auto mode's resolving state and the post-watchdog empty fallback, which would otherwise leave the same display-less record. See SEO data for the payload contract.
ready{}Widget mounted.
goma:error{ message, code, component? }Two paths. Connection layer: in an AUTO mode (subscribe / rpc), the WAMP adapter exhausts its 5 retry attempts (code: 'API_INIT_ERROR'), alongside goma:connection { state: 'failed' }. Widget root scope: an error raised in the widget's own outermost component (its setup, its render, its lifecycle hooks or its watchers), where code is the error's name, e.g. 'TypeError' ('RUNTIME_ERROR' when it has none). Errors raised inside the widget's child components do not reach this event: the in-shadow error boundary catches those and shows the retry banner instead, emitting only goma:retry.
goma:retry{}The render-error banner's Retry button was pressed.

The CTA deliberately does not emit goma:outcome-select: unlike <goma-suggested-bet>'s "Bet the same" it carries no legs. It reports the tap (goma:highlight-cta) and navigates (goma:event-navigate); it never touches the betslip.

Integrates with ​

The widget has no follow* props and listens for no goma:* events: it is a leaf, the same position <goma-multi-banners> takes. In the auto modes it resolves its own event from the feed rather than being driven by a sibling.

Downstream it commonly pairs with <goma-betslip-floating> / <goma-betslip-sidebar> (odds taps) and <goma-game-details> (card-body taps and "See other markets" taps, both via goma:event-navigate, provided the card carries an eventId).

Shared-state envelopes:

  • goma:betslip:v4: written on every odds tap, identity fields only (useSharedBetslipSync({ subscribeToState: false }), the same opt-out <goma-events-horizontal> takes: a card feed has no business owning slip-level state such as stake or bet type).
  • goma:favorites:v1: read and written by the star, so a card favourited here lights up in <goma-favorites> and vice versa.
  • goma:session:v1: read only, to decide whether a star tap toggles or emits goma:login-required.

See Cross-widget wiring and Shared state for the full contracts.

Empty / loading / error states ​

There is no loading state: card is host-fed and synchronous, so the widget renders its final state on the first tick.

  • Empty: no card, a non-object, an array, an unparseable JSON string, or an object with nothing renderable in it (no image, competitionName, homeTeam, awayTeam, fixtureLabel, marketName and no outcomes, a CMS slot that carries only its own key). <goma-pros-choice> asks this of the same fields, so one record is "empty" in both or in neither. Renders emptyMessage if the host set one; otherwise renders nothing at all, so a host page never gets a card-shaped hole where an absent highlight would be.
  • Partially populated. Each region hides independently: no image leaves the hero band painted --goma-backgroundCards, a blank marketName hides the pill, and no outcomes hides the odds row.
  • Named market not found (auto modes). No odds row, and one console warning after 5 s. See Choosing the market.
  • Render error: an onErrorCaptured boundary in HighlightCardApp catches a Vue render/runtime error, shows a role="alert" banner (ERROR: <message>) with a Retry button (clears the error, emits goma:retry), and auto-clears after 8000 ms.

Theming ​

All colours map to tokens that already ship in @gomagaming/core: this widget introduces no new tokens. See Theming for the full reference.

TokenRole
--goma-backgroundCardsCard fill; also the hero band's fallback fill
--goma-cardBorderLineGradient1/2/3The 1 px gradient hairline around the card
--goma-textPrimaryTeam names, kick-off time
--goma-textSecondaryCompetition name, date line, market-pill label, CTA label
--goma-iconPrimarySport glyph
--goma-iconSecondaryCTA chevron
--goma-favoritesFavourite star
--goma-separatorLineSecondaryMarket-pill ring and its trailing rule
--goma-separatorLineCTA pill border
--goma-backgroundSecondaryCTA pill fill (a recessed panel inside the card)
--goma-backgroundOdds / --goma-textOddsOdds buttons at rest
--goma-buttonBackgroundPrimary / --goma-buttonTextPrimarySelected odds button
--goma-backgroundDisabledOdds / --goma-textDisabledOddsSuspended odds button
--goma-highlightPrimaryHeader hairline accent and every focus ring
--goma-allWhiteRegion-flag ring
--goma-backgroundBorderLoading skeleton bars

Fonts. The reference design uses Exo 2, which is not bundled: Shadow DOM cannot use @font-face reliably in Firefox, so the widget inherits the core type stack (Open Sans, registered through the document.fonts API) rather than shipping a face of its own. Weights and sizes match the design; the family does not.

Appearance (styling object) ​

appearance carries structure only. Each key is a single value (identical in light and dark) that maps to one --goma-* CSS custom property on the host. Set it via el.appearance = {...} or the appearance attribute (a JSON string); unset keys keep the default, so an empty {} (the default) reproduces the current look. Colours stay in theme.

KeyCSS variableDefaultAffects
teamNameLineHeight--goma-highlightCardTeamNameLineHeight1.375 (22px)Team-name line height. Pass a length (16px), not a unitless number.
oddsLabelFontSize--goma-oddsLabelFontSize0.62remOutcome name above the price on each odds button. It stays one line with an ellipsis.
oddsLabelLineHeight--goma-oddsLabelLineHeight1.25Line height of that outcome name.

A line height under the default keeps the glyphs unclipped: the name bleeds the difference back as padding inside its ellipsis clip box and as negative margin outside it, so the row is laid out at the requested height. Below 19px the team crests bleed the same way (equal negative block margin, still drawn at 19 x 19 and centred on the name), so a crest never sets the row height: at 16px the rows are 16px with or without crests and the two names stay 6px apart (38px column).

i18n / message overrides ​

The card's own copy is host-fed, not keyed: it is a CMS widget, so its strings belong to the CMS. Only three pre-existing shared keys are referenced, all of them from components the card reuses:

KeyDefault (en)Used by
add_to_favorites / remove_from_favorites"Add to favorites" / "Remove from favorites"The star's aria-label.
today / tomorrow"Today" / "Tomorrow"The kick-off date line, when it is derived from startTime rather than a host-fed dateLabel.
outcome_locked"Outcome locked"A suspended odds button's aria-label.
odds_increased / odds_decreased"Odds increased to {value}" / "Odds decreased to {value}"The odds buttons' screen-reader announcement.
retry"Retry"The render-error banner's retry button.

The CTA's fallback label, "See other markets", is an English string constant rather than a locale key: override it per element with ctaLabel or per card with card.cta.label.

Embedding examples ​

Vanilla / CDN ​

html
<goma-highlight-card id="hl"></goma-highlight-card>
<script type="module">
  import '@gomagaming/highlight-card'

  const el = document.getElementById('hl')
  el.config = { locale: 'en' }
  el.card = {
    id: 'hl-1',
    eventId: '1234567890123456789',
    image: 'https://…/liverpool-united.jpg',
    competitionName: 'Premier League',
    sportId: 1,
    venueId: 15,
    homeTeam: 'Liverpool F.C',
    awayTeam: 'Manchester United F.C',
    dateLabel: 'Today',
    timeLabel: '14:30',
    marketName: '1X2 TR',
    bettingTypeId: 1,
    eventPartId: 1,
    outcomes: [
      { outcomeId: 'o-home', label: 'Liverpool F.C',     odds: 1.97, bettingOfferId: 'bo-home' },
      { outcomeId: 'o-draw', label: 'Draw',              odds: 3.11, bettingOfferId: 'bo-draw' },
      { outcomeId: 'o-away', label: 'Manchester United', odds: 1.21, bettingOfferId: 'bo-away' },
    ],
    cta: { label: 'See other markets', url: 'https://…/event/1234567890123456789' },
  }

  el.addEventListener('goma:highlight-cta', (e) => {
    window.location.href = e.detail.url ?? `/event/${e.detail.eventId}`
  })
</script>

A rail is the host's own loop: one element per card:

js
for (const card of cards) {
  const el = document.createElement('goma-highlight-card')
  el.config = { locale: 'en' }
  el.card = card
  rail.appendChild(el)
}

React ​

jsx
import '@gomagaming/highlight-card'
import { useRef, useEffect } from 'react'

export function HighlightCard({ card, onCta }) {
  const ref = useRef(null)
  useEffect(() => {
    const el = ref.current
    if (!el) return
    el.config = { locale: 'en' }
    el.card = card
    const handler = (e) => onCta?.(e.detail)
    el.addEventListener('goma:highlight-cta', handler)
    return () => el.removeEventListener('goma:highlight-cta', handler)
  }, [card, onCta])
  return <goma-highlight-card ref={ref} />
}

Vue ​

vue
<script setup>
import '@gomagaming/highlight-card'
import { ref, watchEffect } from 'vue'

const props = defineProps(['card'])
const el = ref(null)

watchEffect(() => {
  if (!el.value) return
  el.value.config = { locale: 'en' }
  el.value.card = props.card
})
</script>

<template>
  <goma-highlight-card ref="el" @goma:highlight-cta="$emit('cta', $event.detail)" />
</template>

Accessibility ​

  • The card body is a div role="button" tabindex="0" with an aria-label built from the competition and the two team names, and Enter/Space both activate it. It is not a native <button> because it nests interactive children (odds buttons, the star, the CTA), which a <button> may not contain.
  • The favourite star is a native <button> with aria-pressed and an aria-label that flips between "Add to favorites" and "Remove from favorites".
  • The CTA is a native <button>; its chevron is aria-hidden="true".
  • Odds buttons carry role="button", aria-pressed (selected) and aria-disabled (suspended), an aria-label of "<outcome>, <odds>", and an aria-live="polite" region that announces odds movements.
  • The hero image, sport glyph, region flag, team crests and the market pill's trailing rule are all decorative and aria-hidden / empty-alt.
  • Every interactive element carries a visible focus-visible ring (--goma-highlightPrimary).

What's deferred ​

  • A live (eventId-resolved) data mode. The widget is dataSource: 'prop' only. The contract above is deliberately shaped so adding it is additive: eventId is already a first-class field, ids are already strings, outcomes already carry bettingOfferId, and every display value is read through a single internal event / market computed. The machinery it would use is sports.bannerEvents in @gomagaming/sports-domain, the same multi-event subscription <goma-multi-banners> uses for its eventId path.
  • The green promo CTA. The reference design carries a second, gradient "promo" CTA variant alongside the dark link pill. Only the link pill is built; there is no variant prop.
  • A live-event presentation. The card is prelive-only (date + time). There is no score/clock variant, so a live fixture renders with whatever date/time copy the host supplies.
  • Team crests from a feed. They are host-fed and optional. No provider field is known to carry crest artwork and no CDN naming convention is assumed.
  • RTL. The layout is LTR-only, in line with the rest of the widget set.

Source layout ​

LayerFile
Element classpackages/highlight-card/src/HighlightCardElement.js
App wrapperpackages/highlight-card/src/HighlightCardApp.vue
Cardpackages/highlight-card/src/components/HighlightCard/HighlightCard.vue
Team row / market pill / odds rowpackages/highlight-card/src/components/HighlightCard/{HighlightTeamRow,HighlightMarketPill,HighlightOddsRow}.vue
Empty statepackages/highlight-card/src/components/HighlightCard/EmptyState.vue
View-model composablepackages/highlight-card/src/composables/useHighlightCard.js
Shared card piecespackages/ui/src/components/PromoCards/{PromoCardShell,PromoCardHero,PromoCardCompetitionRow,PromoCardKickoffTime,PromoCardCtaLink}.vue
Shared odds buttonpackages/ui/src/components/GameCards/RegularOdds.vue
Shared region flagpackages/ui/src/components/common/CountryFlag.vue
Cross-widget storage syncpackages/sports-domain/src/shared-state/{useSharedBetslipSync,useSharedFavoritesSync}.js
Theme-token registrypackages/highlight-card/src/themeTokens.js
Playground entrypackages/highlight-card/src/playground.manifest.js + playground/src/registry.ts
Testspackages/highlight-card/tests/{HighlightCard,playground-manifest}.test.js

Integration Caveats ​

  • SSR/webview: importing this package outside a browser previously threw immediately; fixed with a guard on the shared element factory.

  • Initialization: this widget shares @gomagaming/core's i18n singleton with every other widget on the page; mounting two widgets with different locale values on one page is not currently isolated per instance (a known constraint, not fixed in this pass).

  • Configuring before the definition loads. Assigning config (or any property) while the element is still an unknown element is safe: the value is replayed through the widget when it upgrades. That matters when loading from the CDN, whose loader imports widget modules asynchronously, so your script usually runs first. On release sets at 0.34.0 and earlier the value was silently discarded and the widget mounted with no configuration and a clean console: there, await window.__gomaCdn.ready first, or configure through attributes. See Setting a property before the widget's definition has loaded.

See Integration Caveats for the cross-widget picture.

Changelog ​

Current release: 0.37.11. Every GomaPulse package shares one version number and they are published together, so a release that changes nothing here still bumps this widget. Only the releases that changed it are listed below.

Every release that changed this widget, newest first. The latest is open; the rest expand.

0.37.9
  • Favourites: the list always belongs to the player who is signed in now, quick star taps are no longer lost, only real star taps are sent to the server, a failed load is tried again and then shows an error with Retry, and <goma-favorites> no longer waits for the socket login. Star taps on game details, hero card, highlight card, pros' choice, competitions list and banners are now saved to the server when favoritesApiBaseUrl is set and a signed-in session is available on the page (on a page without <goma-favorites>, that means the socket login has finished). A late rejection of a session that already ended (replaced by a new login, or signed out) no longer signs out the current one; a rejection of a new session still does.
0.37.7
  • Faster pages with suggested-bet and promo cards on a real operator, with less data downloaded.

    • @gomagaming/sports-domain: mapping a market dump no longer slows down as the store grows. Outcomes find their offer, and markets find their outcomes, through a lookup built once per dump instead of a scan per item. The results are the same.
    • @gomagaming/sports-domain: subscribeToMatchOddsAdditive and bannerEvents.subscribeForMatchOdds accept bettingTypeId and eventPartId. With both, they open that one market's feed, {matchId}/match-odds/{bettingTypeId}-{eventPartId}, instead of every market of the match. subscribeForMatchOdds no longer re-opens a topic that is already open on the same session.
    • @gomagaming/network: a topic stays registered for 3 s after its last listener leaves, so a widget that closes and reopens it at once does not download its initial dump again. A listener that joins a topic whose initial dump failed, or has had no answer for 10 s, requests the dump again. unregisterGraceMs sets the window, and 0 restores the immediate release.
    • @gomagaming/suggested-bet: in recsys mode each leg opens only its recommended market's feed.
    • @gomagaming/pros-choice: a card that names its market as {bettingTypeId}_{eventPartId} opens only that market's feed. In the auto modes the card shows the first three priced outcomes in feed order, and leaves out outcomes the provider never prices instead of showing a price that never arrives.
    • @gomagaming/highlight-card and @gomagaming/pros-choice: a card whose named market is not on its event logs one console warning after 5 s, listing the markets the event does offer.
0.37.4
  • An event that a feed delivers again now keeps its markets. Before, when a favourite competition was collapsed and opened again, or its time window or sport changed, the event card in <goma-favorites> showed "Suspended" instead of its odds, because the markets were already stored and the event was built without them. The same fix applies to the banner, suggested bet, highlight and pros choice cards, which load their events the same way. An event only receives the stored markets that belong to it, and those markets take the state of the new data: a market that is open again is no longer shown as suspended, and an offer that the provider withdrew while the section was closed is no longer shown at its old price.

  • A card or banner that names an eventId now finds its event by id. Before, the widget subscribed to the whole feed of the event's sport and looked for the event inside it, so an event outside that feed (a far-future match, for example) never resolved and its card stayed empty. Now the widget asks the provider for the events it needs, live or prelive, so any event the provider knows resolves, and its score, clock, odds and live status keep updating. The events feed carries no main-market ranking, so for each sport shown the widget makes one small extra request for it, to keep a banner's headline market stable when a market is suspended and offered again.

    <goma-multi-banners>, <goma-highlight-card>, <goma-pros-choice> and <goma-suggested-bet> no longer need a sportId to resolve an event. The sportEventLimit property and the sport-event-limit attribute are deprecated and now do nothing. They are still accepted, so no existing page breaks.

    On the favourites page, live score, clock and status now update after the page loads. In the suggested bet recommendation mode the events also receive live match updates now; the card does not show a score, so nothing changes on it.

    Sport banners in <goma-multi-banners> can show the market name in a pill under the team names. Set showMarketName on the widget, or on one banner entry, to turn it on.

  • A live event's markets are now ranked by the feed's live main-market list. A feed that carries both live and pre-match events declares one list for each, and they can put different markets first: for basketball, the pre-match list leads with Over/Under and the live list leads with Home Away. Before, every event was ranked by whichever list came first in the feed, so a live game could lead with the pre-match main market. This applies to the event cards in <goma-events-horizontal>, <goma-favorites> and <goma-competitions-list>, and to the main market that <goma-multi-banners>, <goma-highlight-card> and <goma-pros-choice> pick when none is set. orderMarketsByMainMarkets takes an optional { isLive } and resolveMarketSelection an optional isLive option for this. A feed that declares only one list ranks every event by that list, as before.

    • <goma-suggested-bet>: custom-elements.json now lists the defaults for the icon-base-url attribute ('') and the flagBaseUrl property (the built-in flag CDN).
    • Playground theme-token lists (./playground export) now match what each widget paints: <goma-hero-card> now also lists --goma-favorites (the favourite star), alongside --goma-backgroundOdds (the odds buttons, painted by the shared odds button); <goma-top-competitions> drops --goma-iconSecondary, which it never paints (the sport glyph uses --goma-iconPrimary); <goma-highlight-card> and <goma-pros-choice> add --goma-backgroundBorder, the colour of their loading skeleton bars.
    • <goma-live-chat>: on a browser that cannot format compact numbers, the participant count now reads like the normal compact output (25K, 2M, and 1M for 999,999) instead of keeping a trailing .0 (25.0K).
  • Game cards: a suspended odds button now paints backgroundDisabledOdds. The resting backgroundOdds class used to stay on the button and won the cascade, so a theme's suspended background never showed. The "Suspended" label shown when a whole market is suspended now reads textDisabledOdds, the same token as the suspended odds button, instead of inputTextTitleDisabled (this reverses the token choice of a separate change). The suspended button's padlock is now the design's plain 14px padlock (core's lock.svg, its only user) instead of a padlock inside a refresh ring. Affects the game cards of events-horizontal, favorites, competitions-list, game-details, hero-card, multi-banners, highlight-card and pros-choice.

0.37.0
  • Adds an appearance object with teamNameLineHeight (--goma-highlightCardTeamNameLineHeight). The value must be a length (e.g. 16px), not a unitless number; unset, the team names keep their current line height. A tighter line height keeps the team names' glyphs unclipped. A card without appearance renders exactly as before. Below 19px the team crests bleed too, so with crests the rows stay at the requested height (16px rows, names 6px apart).
  • The odds button's outcome name (the line above the price) reads its size and line height from --goma-oddsLabelFontSize and --goma-oddsLabelLineHeight, which fall back to the old 0.62rem and 1.25. <goma-pros-choice> gains an appearance object and, like <goma-highlight-card>, accepts oddsLabelFontSize and oddsLabelLineHeight. The name stays one line with an ellipsis. Without these keys every odds button renders exactly as before. @gomagaming/core is bumped for the two new utilities in its compiled sheet.
0.36.1
  • Playground manifests (@gomagaming/<widget>/playground) now offer a control for every setting each widget really has. Settings that cannot be a form field (raw provider data, read-only state mirrors, props a widget never reads) are recorded with a reason in the repo instead of being silently missing.

    Controls that never reached the widget are fixed:

    • The betslips' placeBetHeadersJson / placeBetBodyJson / quickAddAmountsJson are now placeBetHeaders / placeBetBody / quickAddAmounts.
    • The time-filter and filters-settings ticks editors set ticks.
    • live-chat's layout control writes position, the config key the element reads.
    • The sports-navigation widgets drop the unread sportsJson.
    • JSON-typed defaults are real objects and arrays instead of serialized strings. The top-competitions highlightedImages string default was indexed character by character and requested bad image URLs on every load. The missions-hub emptyStateImages illustrations now show.

    No widget runtime behaviour changes.

0.36.0
  • Moved every shared UI component out of @gomagaming/sports-domain into a new @gomagaming/ui package, which depends on it:

    • The game cards: RegularOdds and its live/prelive/outright/market variants, SkeletonCard and CountryFlag. Used by every widget that lists events.

    • The betslip family: BetslipPanel and its selection, summary and bonus pieces, and the GomaDialog modal shell. Used by the betslip floating and sidebar widgets.

    • The missions family: the missions cards, level rows, tags, empty state and mission-detail panels. Used by the missions hub and mission details widgets.

    • The promo-card family: the promo card shell, hero, competition row, kickoff time, CTA link and skeleton. Used by the highlight card and pros' choice widgets.

    MyBetsPanel and MyBetsCard moved into @gomagaming/my-bets, their only consumer. The betslip widgets keep showing them through <goma-my-bets>.

    No widget's public surface, markup or behaviour changes, and each widget renders the same components as before. @gomagaming/ui is installed automatically with the widgets that use it.

    @gomagaming/sports-domain no longer exports any component. It keeps the stores, API clients, composables and the promo-card auto-mode layer, and additionally exports useBetslipSystem, useBonusSessionGate, bonusRowTitle, convertOdd, formatRatePercent, formattedDate, formattedTime and resolveDateLocale. A host importing one of the moved components from @gomagaming/sports-domain directly should import it from @gomagaming/ui (or, for the MyBets pieces, use <goma-my-bets>) instead.

0.35.0
  • The shared betslip moved to goma:betslip:v4, and the odds-change setting it carries is now a three-value policy.

    The envelope used to carry acceptOddsChange, a boolean that could express only two of the three settings the betting provider accepts, and that meant the opposite of what its name said: true was submitted as ACCEPT_HIGHER, which accepts a rise. Its replacement, oddsChangeBehavior, says which of accept-any, accept-higher and never-accept applies, and the last of those is a setting the boolean could not represent at all: a bet that is cancelled if the odds move in either direction.

    The storage version moved rather than the field changing meaning under the same key, so a page still running the previous release keeps reading the envelope it wrote.

    Nothing is required of an integrator. An existing betslip migrates on the first widget mount, carrying the slip, stake, tab and bonus selections across. Only the odds setting changes, and every existing betslip resolves to accept-higher:

    • A betslip with acceptOddsChange: true, the previous default, was already being submitted as ACCEPT_HIGHER. Nothing about those bets changes.
    • A betslip with acceptOddsChange: false was being submitted as ACCEPT_ANY, under a control reading "Approve higher odds", which is not what that value does: it also accepts a DROP in the odds. Those bets are now submitted as the label promised. A player who wants any change accepted can select it explicitly.

    An integrator reading the betslip out of storage should read oddsChangeBehavior, and address the key through the newly exported BETSLIP_STORAGE_KEY rather than a literal. Widget code that sets the old boolean keeps working: it is still available, derived from the policy.

  • Secondary market cards no longer swap their two sides mid-game, and a betslip row can no longer become a different bet without saying so.

    The card. A live market that suspends and re-offers arrives as DELETE+CREATE, so the re-created outcome lands on the tail of market.outcomes and the market reaches the card with its two sides the other way round. sortOutcomes only ranks outcomes carrying a headerNameKey, which a handicap outcome does not, so every pair compared equal and Array#sort's stability handed the layout to arrival order. For a two-sided market the comparator now falls through to the provider's outcome code: numbered codes (#1 / #2) compare numerically, so #10 does not sort before #2, and the provider's named pairs (#Y/#N, #O/#U, #Odd/#Even) are ranked the way getOutcomeOrder already ranks them. Yes before no, over before under, odd before even. A pair of codes the comparator does not recognise keeps the order the feed sent, and a market with more than two outcomes is never reordered. sortOutcomes returns a copy, so the call sites that hand it a store-backed market's own array no longer reorder the store from inside a computed.

    <goma-events-horizontal>, <goma-favorites> and <goma-competitions-list> render the shared secondary card and inherit the fix. <goma-game-details>, <goma-hero-card>, <goma-highlight-card>, <goma-multi-banners> and <goma-pros-choice> call sortOutcomes on their own market lists and render the shared odds button, so the new ordering and the new line on goma:outcome-select reach them too.

    The betslip. RegularOdds records the market's line (paramFloat1) on the selection at tap time, and it now survives both cross-widget bridges: the goma:betslip:v4 envelope carries it as an identity field, and goma:outcome-select carries it in its detail. Without that it never reached the widget that reads it. useBetslipOddsSync compares it against the market on every pass, in the game-details store slots as well as the list ones: when the line has moved the row is re-captioned and marked unavailable, and useBetslipLogic refuses to place a slip holding an unavailable leg, so the player picks the new line deliberately instead of having it accepted for them by the odds-change gate. The tapped line is never overwritten, so the verdict is re-asserted on every tick rather than being undone by the next price update, and a line that moves with no price change is caught too: a market-only rewrite never woke the odds watcher before. A wording-only change still just corrects the caption, and a selection saved before the line was recorded is never invalidated by the upgrade.

    Saying why. A blocked leg now carries lineMoved: true beside isAvailable: false, so the two reasons a leg cannot be placed stay apart. The betslip row reads "Line changed" (selection_line_moved, added to the en/fr/pt bundles) instead of "Selection unavailable" (the match is still on, it is the line that is gone) and goma:bet-failed lists those legs under a new movedLineSelections key rather than folding them into unavailableSelections, so a host does not word a live match as dead.

    placeBetBlockedReason gains 'line_moved', ahead of 'unavailable', so a host keying its wording off that token can say that the line moved rather than that the match is gone; the widget's own copy carries the new place_bet_blocked_line_moved key in all three locales. The flag itself is now written in both directions: a line that moves and comes back clears it, so a later genuine withdrawal reports as unavailable and not as a moved line.

  • A sport with no bundled icon now falls back to a real glyph instead of the browser's broken-image icon.

    SbIcon resolves a bundled SVG by name and otherwise falls through to an <img>. For a sport id the feed ships before its artwork exists (e-Volleyball, in the sports carousel and in the "Sports available" list) that <img> 404s, and the old error handler only faded it to opacity: 0.2, which leaves the broken-image glyph on screen at 20%. A failed <img> is now dropped from the render instead.

    What stands in is the sport's family glyph: every e-sports category falls back to the e-sports icon, which is a real icon rather than "no icon". The family is read from the sport list the widget was already fed (the parent whose childrenIds names the category) so no id list has to be kept in step with the provider, and a flat feed that names no parent simply falls back to the neutral noicon glyph as before. The new sportIconNames helper in @gomagaming/core is what the sport tiles, the picker's trigger pill and the settings modal's sport row all resolve through.

    A host serving its own sport artwork keeps priority, and keeps it whether or not it configured a base URL: ${iconBaseUrl}/icons/sports/dark_mode/{iconId}.svg when one is set, and the origin-relative /icons/sports/dark_mode/{iconId}.svg when it is not. iconBaseUrl has no value that means "my own root" ('' and '/' both read as unset), so a host serving icons from its root keeps working with no configuration at all. A stand-in is considered only once that request has failed.

    Every widget that draws a sport glyph through SbIcon inherits the change, which is why they are all listed here: the two sports pickers, the settings modal, both top-competitions widgets, the sport banners, the game-details header, the suggested-bet legs, the My Bets cards, the two betslips that embed them, the highlight card and the Pros' Choice card.

    The family is looked up across the whole sport list the feed sent rather than the slice on screen. In rpc/subscribe mode useSportsSource mirrors the betting store filtered to the current context (LIVE vs popular), and a parent category carries no events of its own, so it is routinely missing from that slice: every e-sports child would have fallen back to the neutral glyph. The unfiltered list is mirrored alongside it as state.sportsCatalog, and sportIconLookupList (new in @gomagaming/core) is what the tiles, the trigger pill and the settings row read. mapSport also refreshes childrenIds on an existing entry: the list was written only when the entry was first created, so a family relation that arrived on a later dump, or a child added to one. Never reached the widget.

0.34.0
  • Stop a region list fetched for the wrong sport from overwriting the right one, and parse numeric strings assigned to numeric widget properties instead of silently replacing them with the widget's default

    A <goma-region-filter> configured for any sport other than Football issued two location requests instead of one: its own, correctly scoped to the configured sport, and a second one (scoped to Football) that nothing had asked for. Both wrote the same place, so the pills the widget rendered were whichever response happened to come back last. The same race reached <goma-competitions-list>, which groups its competitions by the same region data.

    The unrequested request came from the per-widget boot routine, which fetched locations with no sport at all and so fell back to sport 1. Locations are scoped to a sport on the server, and boot has no sport to scope them by, so it no longer fetches them: the only readers of that data are the widgets above, and each of them already fetches it for the sport it is showing.

    That removes the collision at its source. As a second line of defence, a locations payload now records which sport it was fetched for, and a widget ignores one belonging to a sport it is not showing. This matters whenever two scoped fetches can overlap (switching sport while a request is still in flight, for instance) and it is deliberately narrow: a payload with no recorded sport is still rendered, so the guard can only ever suppress data it can positively identify as belonging to somewhere else.

    Separately, every numeric property on every widget (sportId, skeletonCount, pageSize, spaceBetween, autoplayInterval and the rest) used to accept a JavaScript number and nothing else. A numeric string was not parsed; it was discarded and quietly replaced by the property's default value. el.sportId = '8' therefore left the widget on sport 1 and fetched the wrong data, with nothing logged to say so. Three ordinary integration paths hand over a string: a router parameter, a value read out of a CMS or JSON configuration, and a framework re-render that stringifies its props. Numeric strings are now parsed. Values that are not a finite number and not a numeric string (null, undefined, '', booleans, objects, NaN, Infinity) still fall back to the default, as before. HTML attributes (sport-id="8") and the config object were never affected by this and are unchanged.

  • Signing in (<goma-betslip-sidebar>, <goma-betslip-floating>, <goma-favorites>) no longer stalls on a WAMP socket the provider never answers.

    The EveryMatrix gateway sometimes hands out a socket that completes the handshake and then answers no call at all, not the feeds, not /sports#loginWithCeSession. The CE-session exchange only retried once a step settled, so it stalled for good: the host's "session ready" wait ran out, and every bet went out without a username and came back rejected until the page was reloaded. Around a quarter of sign-ins hit it on stage.

    Each exchange step (the StartInfo request and the loginWithCeSession call) now has a 5s deadline, tunable with the new sessionStepTimeoutMs key (0 disables it). A timed-out StartInfo request is aborted and retried. A timed-out loginWithCeSession drops the shared connection instead of retrying into it (every widget reconnects and the exchange re-runs on the fresh socket) at most twice per session; a successful exchange restores that allowance. If no new connection follows a replacement, a console warning says the exchange is not running.

    <goma-multi-banners>, <goma-highlight-card> and <goma-pros-choice> now re-open their live topics after a reconnect. They skipped every sport they had already subscribed to, so after any socket loss, and now after a replaced login socket, their odds stopped updating until the element was unmounted.

0.32.0
  • Widgets no longer dispatch a bare error event alongside goma:error.

    Every factory-built widget's app-level Vue error handler dispatched two events for the same failure: goma:error, and an un-namespaced error. The bare one is gone; goma:error is unchanged and still carries the same { message, code, component? } detail.

    Like every GomaPulse event it was composed and bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had on window.addEventListener('error', …): as a CustomEvent with message, error and filename all undefined, since those are ErrorEvent fields a CustomEvent does not have. A host with error reporting installed received a malformed error record it could neither read nor attribute.

    That had been written off as harmless on the grounds that the handler behind it could never fire. It can. Vue begins its errorCaptured walk at the throwing component's parent, so the onErrorCaptured boundary each widget installs in its own root component cannot see that component's own errors, and being the root, there is nothing above it to catch them either. Anything raised in a widget's root scope (its setup, its render, its lifecycle hooks, its watchers) skips the boundary and reaches the app-level handler every time. Only errors from child components are caught by the boundary, which is what it is for: those show the in-shadow banner and emit goma:retry.

    Hosts listening for goma:error need no change. A host listening for bare error on a widget element should move to goma:error; it was documented as unreachable, so in practice it delivered nothing until now.

0.30.2
  • Event cards: line the odds row up across a rail, and settle the market badge

    Every card now distributes its rows in a flex column and pins the odds row to the bottom. Previously the main cards simply stacked their rows inside a fixed height panel: the stack ran taller than the panel, so the odds row spilled past the card and sat several pixels below the odds row of the secondary market cards beside it. The market badge is now centred in the gap between the last team name and the odds buttons, and the secondary card draws the same hairline under its top row that the main cards have always drawn.

    The card is also slightly taller: the default size goes from 156px to 160px and the large from 188px to 190px, and at large the gap above the team names comes down from 12px to 8px. The design frames set two 16px team names in 16px line boxes, which shears the glyphs on the web once the names are truncated, so the names are sized to their ink instead: height the frames never budgeted for. The heights stay EXACT rather than becoming minimums: a rail must never show cards of differing heights. The "See all" tile, the loading skeleton and the empty state move with them, being the same card.

    The suspended / no-odds block picks up the same transparent one pixel border the odds buttons carry, so the two occupy identical boxes rather than differing by a border width.

    Promo cards (goma-highlight-card, goma-pros-choice): the loading skeleton is now the same height as the card it stands in for, so a rail no longer jumps when the feed answers. goma-pros-choice also reserves the fixture pill's height when a card carries no fixture, so cards keep one height with or without it.

0.29.0
  • Add goma:seo-data: widgets emit their resolved event data so hosts can build their own meta tags and structured data.

    Eight widgets emit the event: game-details, events-horizontal, competitions-list, hero-card, highlight-card, pros-choice, multi-banners (sport banners only), and casino-games. The payload is { widget, type, items }, where type is 'sports-event' or 'casino-game'. The event bubbles and is composed, so a single listener on document catches every widget on the page.

    Emission fires once when data resolves and again only when the data's identity changes: markets, score and clock are excluded from the identity signature, so odds ticks do not re-fire it. An empty items array is never emitted. Hosts must replace their structured data on each event rather than append.

    Widgets carrying a signed-in user's own data, private content, or page chrome deliberately do not emit.

  • Visual corrections across the game card, the casino rail and every carousel arrow.

    • Game cards. Team names on the large card variant were clipped: an 18px font was set on a 16px line box, which sheared the ascenders and the descenders of names like Puerto Rico (W). The line box now fits the font's ink, and the home row's bottom margin is pulled in so the painted distance between the two team names is unchanged. Affects every widget rendering the shared game card, plus <goma-highlight-card>'s team rows.
    • <goma-casino-games>: the 'simplified' tile now matches its design source: a 23:30 portrait tile, and the name/provider block and play glyph laid out as one centred stack so the gap between them no longer changes with the number of lines the game name wraps to.
    • Navigation arrows: <goma-casino-games>, <goma-multi-banners> and <goma-sports-navigation-horizontal> now draw one identical arrow: a 24px disc of --goma-allWhite at 60% carrying a full-strength --goma-highlightSecondaryContrast caret on a 1px stroke. Previously the three diverged in disc colour, glyph size (14–18px) and stroke weight. Hosts that restyled these arrows through --goma-textPrimary or --goma-backgroundBorder should move to the two tokens above; both are declared in each widget's theme-token registry and documented in its theming table.
    • <goma-top-competitions>: the list header's type now matches the section headers around it instead of running a size larger at a lighter weight.
0.28.0
  • Add two single-card, fully host-fed promo widgets that take one and the same card record. Both render one card per element (no carousel, arrows, dots or section header) so a host that wants a rail loops its own array and assigns one object per element, the shape <goma-suggested-bet> already uses. Both are CMS-driven and ship two modes. In manual mode (dataSource: 'prop', the default) every field arrives on el.card (a live object or a JSON string) and nothing is read from a socket. In auto mode (dataSource: 'subscribe' for live, 'rpc' for a one-shot snapshot) the host supplies only what a CMS owns (the image, an eventId and a defaultMarket) and the widget resolves teams, competition, kick-off, market name, outcome labels and odds from the feed, keeping prices ticking. Auto mode needs config.socketUrl. Host-supplied display fields win over the feed; identity and price always come from the feed. Neither widget adds a theme token.

    One record shape for both. The two widgets ship together and are meant to be authored from a single CMS record type, so they take the same object field for field: id, eventId, image / imageAlt, competitionName / competitionId, sportId / sportIconUrl, venueId / flagUrl, homeTeam / awayTeam / fixtureLabel, startTime / dateLabel / timeLabel, marketName / marketId, bettingTypeId / eventPartId, isLive, isFavorite, outcomes: [{ outcomeId, label, odds, bettingOfferId, isAvailable }] and cta: { label, url }. Paste the same object into either element and it renders; paste it into both and an odds tap produces an identical betslip leg. There are exactly two exceptions, both by design: homeTeamLogo / awayTeamLogo are read only by <goma-highlight-card>, the only card that draws team crests; and fixtureLabel is read by both but painted only by <goma-pros-choice>, as its fixture pill. The highlight card draws two stacked team rows and uses the value to name the event instead. Integrators build one mapping table, not two.

    A short list of synonyms is accepted so a hand-written record is forgiving, identically in both widgets: odd for odds, name / outcomeName for label, id for outcomeId, offerId for bettingOfferId, and a top-level ctaUrl shorthand for cta.url. The canonical spelling is the one documented on each widget page; the synonyms are accepted, not recommended.

    <goma-highlight-card>. A highlight card for one sporting event: hero image band, competition header with a favourite star, two team rows with optional crests, a kick-off block, a market-type pill, an odds row, and an optional "See other markets" link pill. Emits goma:outcome-select / goma:outcome-deselect (identical to every other odds button, so both betslips pick it up with no wiring), goma:event-navigate on a body tap (so a sibling <goma-game-details> follows it) alongside goma:highlight-select, goma:highlight-cta from the link pill, and goma:favorite-toggle / goma:login-required from the star. Writes goma:betslip:v3 and the events array of goma:favorites:v1.

    <goma-pros-choice>. "The Pros' Choice" promo card, promoting a single market on a single fixture: artwork strip, competition row over a fading hairline, the promoted market's name opposite the kick-off, the fixture as a pill bled flush to the card's left edge, and odds buttons labelled with outcome names (player names for a scorer market, not Home / Draw / Away). Emits goma:outcome-select / goma:outcome-deselect, goma:pros-choice-select on a body tap plus goma:event-navigate when the card carries an eventId, goma:pros-choice-cta from the link, and goma:favorite-toggle / goma:login-required from the star. The star is event-scoped on both cards: what a player is starring is the fixture in front of them, not every fixture in the same league, so it writes the events array of goma:favorites:v1, and it is hidden entirely on a card with no eventId.

    @gomagaming/sports-domain gains the presentational layer both cards compose: PromoCardShell, PromoCardHero, PromoCardCompetitionRow, PromoCardKickoffTime and PromoCardCtaLink, all host-fed and store-free. They render what they are handed and report taps, while the owning widget keeps the feed, the betslip and the favourites gate. CountryFlag also takes a new src prop that wins over the venueId CDN lookup, for hosts whose per-item flag URL does not follow the CDN naming scheme.

    <goma-highlight-card>'s "See other markets" link now emits goma:event-navigate alongside goma:highlight-cta, so it opens the event with no host code: the composition rule <goma-pros-choice> already followed. It previously reported the tap and stopped, leaving the two widgets' identical-looking links behaving differently. Enter and Space on that link no longer also activate the card shell.