<goma-pros-choice>
A single "The Pros' Choice" promo card: a host-fed artwork strip, the competition line (favourite star, sport glyph, region flag, name) over a fading hairline, the promoted market's name opposite the kick-off, the fixture as a small pill bled flush to the card's left edge, and a row of odds buttons labelled with outcome names. Player names for a scorer market, not Home / Draw / Away. An optional "See other markets" link closes the card.
One card per element: the host owns the rail. To render a row of picks, loop your own array and assign one object to each element's card, the same way <goma-suggested-bet> is used. The widget ships no carousel, no arrows, no dots and no section header.
Live demo
Open in the playground →: toggle Pros' choice in the widget rail. The bundled mock source renders a scorer-market card whose three prices drift every 3 s, so the odds-tick animation is visible; 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-pros-choice></goma-pros-choice>Installation
bash
pnpm add @gomagaming/pros-choice vue pinia vue-router vue-i18n @vueuse/core autobahn-browserThe framework packages are peer dependencies installed once at the app level; bundlers dedup them across every @gomagaming/* widget. This widget declares no swiper peer: it renders no carousel. autobahn-browser is declared, as it is by every package that depends on @gomagaming/sports-domain: the shared package's socket adapter chunk references it. This widget never loads that chunk (it opens no socket), but the specifier still has to resolve at bundle time. 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 card is fully host-fed and makes no network calls. The auto modes ('rpc' / 'subscribe') additionally need socketUrl: see below.
| Key | Type | Description |
|---|---|---|
locale | string | Active i18n locale ('en' / 'fr' / 'pt'). Defaults to 'en'. |
favoritesApiBaseUrl | string | Favourites 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. |
theme | Record<string,string> | --goma-* token overrides. |
messages | object | Optional i18n phrase overrides. |
debug | boolean | Defaults to false. |
socketUrl | string | Auto mode only. WAMP endpoint. Required by dataSource: 'rpc' | 'subscribe'; without it the widget warns once and degrades to the host-fed card. |
socketRealm | string | Auto mode only. WAMP realm. |
ucsOperatorId | string | Auto mode only. Operator id used by the feed. |
Data sources
The card property (a single object, not an array) is always the entry point. It accepts either a live object (JS property) or a JSON string (HTML attribute / the playground's JSON control); a malformed string, an array, or an object with nothing renderable in it renders nothing rather than a broken card.
What fills the card in depends on dataSource:
'prop'(default): fully host-fed. Everything. Artwork, copy, market name, outcome labels and prices. Arrives from the host, and no socket is opened.'subscribe': auto, live. Feed{ eventId, marketId / defaultMarket, image }(the event is looked up by id, so nosportIdis needed to resolve it) and the widget resolves competition, flag, fixture, kick-off, market name, outcome labels and ticking odds from the live feed.'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.
Both auto modes need config.socketUrl; without one the widget warns once and degrades to the host-fed card. Because this card's premise is a non-main market (an anytime-scorer line, say), matchOdds defaults to 'auto' here: the by-id events feed only streams ~5 main markets per event, so the per-match {eventId}/match-odds feed is the only route to the market the card names.
Precedence in the auto modes: the host wins for anything a human wrote (team names, competition, market name, kick-off), the feed wins for identity and price (marketId, bettingTypeId, eventPartId, eventId, isLive and every outcomes[] field), and presentation (image, cta, flags) is host-only and never touched. Until the event lands the card shows a skeleton, or the host's own display fields, if it supplied any, and a 15 s watchdog stops it waiting forever.
js
document.querySelector('goma-pros-choice').card = {
id: 'pick-scorer-bar-ray',
image: 'https://cdn.example.com/promo/scorer.jpg',
imageAlt: 'Anytime scorer promo',
eventId: '2214800000000123456',
sportId: 1,
competitionId: '9100000000000000007',
competitionName: 'La Liga',
venueId: 26,
marketName: 'Goaler',
marketId: '9900000000000000042',
bettingTypeId: '128',
eventPartId: '1',
homeTeam: 'FC Barcelona',
awayTeam: 'Rayo Vallecano',
startTime: 1791225000000,
outcomes: [
{ outcomeId: 'o1', bettingOfferId: 'bo1', label: 'Bellingham, Jude', odds: 1.97 },
{ outcomeId: 'o2', bettingOfferId: 'bo2', label: 'Mbappé, Kylian', odds: 2.15 },
{ outcomeId: 'o3', bettingOfferId: 'bo3', label: 'Júnior, Vinícius', odds: 2.40 },
],
cta: { label: 'See other markets', url: '/event/2214800000000123456' },
}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 promotes a non-main market (a scorer market, say), so with matchOdds: 'auto' (the default) it opens a per-match odds feed for it. How you name the market decides how much that feed downloads:
defaultMarket form | Example | What the card downloads |
|---|---|---|
{bettingTypeId}_{eventPartId} (recommended) | '30_3' | Only that market's feed, {eventId}/match-odds/30-3 (tens of KB) |
bettingTypeId only | '30' | Every market of the match (several MB per match on a real operator), because the period is unknown |
The card shows the first three priced outcomes of the market, in the order the feed sends them. Outcomes the provider lists but never prices (a scorer market's "None", say) are left out.
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-pros-choice] 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), ...
With matchOdds on, this means the event does not offer the market, or offers it under another period. Fix the card's defaultMarket (pick one the warning lists). Prefer that over marketFallback: 'first', which shows a market the CMS did not choose.
One record shape, two widgets
<goma-pros-choice> and <goma-highlight-card> 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/awayTeamLogoare highlight-card-only. That card draws 19×19 team crests beside its two team rows; this one draws a fixture pill and ignores both fields. They are the only fields in the record this widget does not read.fixtureLabelis read by both but rendered by only one: this one. It is the fixture pill's copy here;<goma-highlight-card>draws two stacked team rows 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 GomaPromoCard object, field by field
This is the contract a CMS should be built against. The CMS-owned column says whether an editor would author the value (artwork and copy) or whether it is provider data an integration would copy across from the sportsbook feed. Every field below is read by <goma-highlight-card> too.
| Field | Type | Required | CMS-owned | Notes |
|---|---|---|---|---|
id | string | yes | provider/editor | Stable identity, echoed back on every emitted detail. Not an event id: it is the card's own slot id. |
image | string (URL) | no | editor | The hero artwork. No provider field carries it: it is the reason this widget is host-fed. Absent renders the strip as flat card colour. |
imageAlt | string | no | editor | Empty (the default) keeps the strip decorative (aria-hidden). Supply one only when the artwork carries meaning the copy does not. |
eventId | string | no, but see notes | provider | String, never Number(): provider ids are 18-digit snowflakes past Number.MAX_SAFE_INTEGER. Gates goma:event-navigate, is the favourite star's target (without it the star is not rendered), and is copied onto every betslip selection: a card without it produces picks that cannot be placed. |
sportId | number | string | no | provider | Resolves the sport glyph (see iconBaseUrl / sportIconUrl). |
sportIconUrl | string (URL) | no | editor | Per-card override. Beats iconBaseUrl; both blank falls back to the flat glyph bundled in @gomagaming/core. |
competitionId | string | no | provider | Rides onto the synthesized event so it reaches the betslip leg. Nothing on the card renders it. |
competitionName | string | no | provider | Competition row copy, e.g. "La Liga". |
venueId | string | number | no | provider | Region-flag lookup: ${flagBaseUrl}{venueId}.png. |
flagUrl | string (URL) | no | editor | Per-card override of the venueId lookup, for hosts whose flags don't follow the CDN naming scheme. |
marketName | string | no | provider | The card's headline, e.g. "Goaler". Truncates rather than wrapping. |
marketId | string | no | provider | Copied onto betslip selections and onto goma:pros-choice-select. |
bettingTypeId | string | number | no, but see notes | provider | Load-bearing. The provider rejects a bet whose leg carries no betting type. Omit it and the card still renders, but its picks are unplaceable. |
eventPartId | string | number | no | provider | bettingTypeId's pair: together they key a market. Rides onto goma:outcome-select. |
startTime | epoch ms number, or an ISO 8601 string | no | provider | Rendered as the two-line Today / 14:30 block, localised. |
dateLabel | string | no | editor | Explicit override that bypasses startTime date formatting. |
timeLabel | string | no | editor | Explicit override that bypasses startTime time formatting. |
homeTeam | string | no | provider | Feeds goma:event-navigate and the fixture pill. |
awayTeam | string | no | provider | Same. |
fixtureLabel | string | no | editor | Pill copy override. Default is `${homeTeam} - ${awayTeam}`, falling back to whichever single name exists. No label and no team names → no pill. Read by <goma-highlight-card> too, where it names the synthesized event rather than painting a pill. |
isFavorite | boolean | no | provider | One-way seed for the favourite star: true merges eventId into the shared goma:favorites:v1 envelope once, and only when the player is signed in. It can only add, never remove, so it cannot clobber what <goma-favorites>' server sync resolved, and it never invents a favourite for a signed-out visitor. Identical behaviour in <goma-highlight-card>. |
isLive | boolean | no | provider | Recorded on the synthesized event so a betslip leg is flagged live. Nothing on the card changes: there is no live badge, score or clock in the design. |
outcomes | Outcome[] | no | mixed | The odds buttons: the promoted market's outcomes, in the editor's order. Empty or absent → no odds row (the rest of the card still renders). Not selections: in this codebase a selection is a leg on a betslip, and reusing that word for market outcomes was actively confusing. |
cta | Cta | null | no (default null) | editor | null renders no CTA band. |
ctaUrl | string (URL) | no | editor | Top-level shorthand for cta.url, accepted for the same record to work in both widgets. Declaring either one paints the band; cta.url wins when both are set. |
Outcome
| Field | Type | Required | Notes |
|---|---|---|---|
label | string | yes in practice | The button's top line: the outcome name ("Bellingham, Jude"). name / outcomeName are accepted synonyms. |
odds | number | yes in practice | The button's bottom line, as decimal odds. odd is an accepted synonym. Missing renders the button's loading spinner, not 0.00. |
outcomeId | string | no | The stable abstract outcome id. id is an accepted synonym. Falls back to bettingOfferId, then to a synthetic ${marketId}-o{index}. Keys betslip selection state, so supply the real one. |
bettingOfferId | string | no | The price-tied offer id. offerId is an accepted synonym. Both this and outcomeId reach the place-bet payload, and it is never fabricated: omit it and the leg carries null plus the betslip's own warning, because the provider rejects a leg carrying an offer id it never issued. |
isAvailable | boolean | no (default true) | Fails open: only an explicit false locks the button (padlock glyph, not tappable). |
Cta
| Field | Type | Required | Notes |
|---|---|---|---|
label | string | no | Overrides the widget-level ctaLabel. |
url | string | no | Carried in the emitted detail. The widget never follows the URL: routing to it is the host's job. It does, separately, emit goma:event-navigate on the same tap when the card carries an eventId, which is what opens <goma-game-details>. |
Accepted synonyms
The canonical spelling is the one documented above. Four fields also accept alternatives, kept because they cost nothing and make a hand-written CMS record forgiving: both widgets accept exactly this set:
| Canonical | Also accepted |
|---|---|
outcomes[].odds | odd |
outcomes[].label | name, outcomeName |
outcomes[].outcomeId | id |
outcomes[].bettingOfferId | offerId |
cta.url | ctaUrl (top-level shorthand) |
Fields this widget ignores
homeTeamLogo and awayTeamLogo are on the shared record but are read only by <goma-highlight-card>, which is the only card that draws team crests. They are harmless here: the same record can carry them for the other widget's benefit.
Only the dark "See other markets" link variant is built. The design's second, promotional gradient CTA is not implemented, which is why there is no type / variant field here.
Props / attributes
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
card | card (JSON) | GomaPromoCard | null | null | The single host-fed card object: the same record <goma-highlight-card> takes. |
dataSource | data-source | 'prop' | 'rpc' | 'subscribe' | 'prop' | Resolution 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. Any unsupported value logs one console warning and falls back to 'prop'. |
sportEventLimit | sport-event-limit | number | 20 | Deprecated, 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. |
marketFallback | market-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; 'first' falls back to the leading main market, which on this card is almost always the wrong thing, since the whole premise is one curated non-main pick. |
matchOdds | match-odds | 'auto' | boolean | 'auto' | Auto mode only. Whether to also open the per-match {eventId}/match-odds feed, which carries the FULL market set. 'auto' (the default here) opens it whenever the card names a market: load-bearing on this widget, because a scorer market is NOT a main market, and the events feed streams only about 5 main markets per event, while the match-odds feed carries the rest. 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). |
ctaLabel | cta-label | string | 'See other markets' | Default CTA copy. A per-card cta.label wins. |
showCta | show-cta | boolean | true | Element-level switch for the "See other markets" band. The card must also declare a cta (or the ctaUrl shorthand) before it paints: this suppresses the band across a whole rail without editing every card. |
showFavorite | show-favorite | boolean | true | Favourite star. Hidden regardless when the card has no eventId. |
showBorder | show-border | boolean | true | The 1 px gradient hairline around the card. One design palette hides it; the layout does not shift either way. |
emptyMessage | empty-message | string | '' | Message rendered when there is no card worth painting. Blank → the widget renders nothing at all, so a host page never gets a card-shaped hole. |
heroHeight | hero-height | number | 100 | Hero strip height in px. |
heroObjectPosition | hero-object-position | string | 'center 34%' | CSS object-position for the hero crop. The default reproduces the design's centre-biased crop of a ~1.73 : 1 source. |
flagBaseUrl | flag-base-url | string | '' | Region-flag base: ${flagBaseUrl}{venueId}.png. Blank falls back to the shared flag CDN. |
iconBaseUrl | icon-base-url | string | '' | Sport-icon base: ${iconBaseUrl}/icons/sports/dark_mode/{sportId}.svg. Blank falls back to the glyph bundled in @gomagaming/core. |
Events
All goma:* events bubble and are composed: true (they cross the Shadow DOM boundary).
| Event | detail | Notes |
|---|---|---|
goma:pros-choice-select | { id, eventId, marketId, marketName } | Card body tapped. Always fires, including on a card with no eventId (in which case eventId is null). Host-only: no shipped widget listens for it. |
goma:pros-choice-cta | { id, eventId, ctaType: 'markets', label, url } | "See other markets" tapped. url is null when the card supplies none. ctaType is a constant today; it is carried so that a second CTA variant would be an added value rather than a changed payload. Host-only. |
goma:event-navigate | { eventId, homeTeam, awayTeam, competitionName } | Emitted in addition to either event above, but only when the card carries an eventId. |
goma:outcome-select | { bettingOfferId, outcomeId, outcomeName, odds, eventId, marketId, bettingTypeId, eventPartId, line } | Odds button tapped. Dispatched by the shared RegularOdds button, identical to every other card family. |
goma:outcome-deselect | { bettingOfferId, outcomeId } | A selected odds button tapped again. |
goma:favorite-toggle | { type: 'event', id, eventId, isFavorite } | Star toggled while signed in. id and eventId carry the same value. |
goma:login-required | { reason: 'favorite', type: 'event', id } | Star tapped while signed out. The widget does not write; the host opens its login flow. |
goma:seo-data | { widget: 'pros-choice', 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 | {} | User clicked Retry on the render-error banner. |
An odds tap never also activates the card, and a CTA tap never also emits goma:pros-choice-select: both are stopped at the source, for pointer and keyboard alike.
Integrates with
The card listens for nothing: it has no follow* prop, because there is no picker whose selection would re-scope a single host-curated card.
It cooperates downstream through two shared envelopes:
goma:betslip:v4: odds taps write selections throughuseSharedBetslipSync({ subscribeToState: false }): identity fields only, opting out of slip-level state exactly as<goma-events-horizontal>and<goma-hero-card>do. Drop a<goma-betslip-floating>or<goma-betslip-sidebar>anywhere on the page and a tap here lands in it with no host glue.goma:favorites:v1: the star writes theeventsarray. A fixture starred here lights up in<goma-favorites>, in<goma-highlight-card>and in every game card on the page, and vice-versa.
goma:event-navigate is followed by <goma-game-details> (its followEventNavigate is on by default), so both a card-body tap and the "See other markets" link open the full event with no host code: provided the card carries an eventId.
See Cross-widget wiring and Shared state for the full contracts.
Favourite-star scope: a documented judgement call
The star is drawn beside the competition name, on a card that promotes a market on one fixture, and the design does not say which of the two it favourites. This widget treats it as an event favourite: what a player is starring is the fixture in front of them, which is what the sibling <goma-highlight-card> and every game card store. Scoping it to the competition instead made one tap light up every card in the same league, which is not what the gesture means. A card with no eventId therefore renders no star at all rather than a control that silently writes nothing.
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. None of the states below are designed (the Figma source has no empty, loading, error, suspended, odds-ticking or live treatment for this card) so each is a deliberate, conservative choice:
- Nothing rendered:
cardisnull(the default), an array, an unparseable JSON string, or an object carrying noimage,marketName,competitionName, team names, fixture label oroutcomes. The element occupies no visual space rather than drawing an empty shell.<goma-highlight-card>asks this of the same fields, so one record is "empty" in both or in neither. - Partial card. A card with some fields renders the parts it has: no
outcomesdrops the odds row (and the whole bottom band when there is also nocta), no fixture names drop the pill, noimageleaves a flat card-coloured strip. - Locked outcome: an outcome with
isAvailable: falserenders the shared padlock treatment and is not tappable. Availability fails open: a card that never mentions it is fully tappable. - Odds tick: when a host replaces
cardwith a new object carrying new prices, the sharedRegularOddsbutton runs its standard green/red change animation for 3000 ms. The widget has no feed of its own, so a host that wants ticking odds must re-assigncard. - Named market not found (auto modes). No odds row, and one console warning after 5 s. See Choosing the market.
- Render error: an
onErrorCapturedboundary shows arole="alert"banner (ERROR: <message>) with a Retry button (clears the error and emitsgoma:retry) and a dismiss×, auto-clearing after 8000 ms. - Unimplemented
dataSource: logs exactly oneconsole.warnand renders the card in'prop'mode.
Theming
Every colour maps to an existing --goma-* token: this widget introduces none:
| Token | Role |
|---|---|
--goma-backgroundCards | Card fill; also the fixture pill's inner surface (so the pill reads as transparent while masking its gradient edge) |
--goma-backgroundSecondary | "See other markets" link background |
--goma-textPrimary | Market headline, kick-off time |
--goma-textSecondary | Competition name, kick-off date, CTA label |
--goma-iconPrimary | Sport glyph |
--goma-iconSecondary | CTA chevron |
--goma-favorites | Favourite star |
--goma-allWhite | Region-flag ring |
--goma-highlightPrimary | Divider mid-stop, fixture-pill text, every focus ring |
--goma-backgroundOdds / --goma-textOdds | Odds buttons at rest |
--goma-buttonBackgroundPrimary / --goma-buttonTextPrimary | Selected odds button |
--goma-backgroundDisabledOdds / --goma-textDisabledOdds | Suspended odds button |
--goma-separatorLine | CTA link border |
--goma-cardBorderLineGradient1 / 2 / 3 | The card's 1 px hairline and the fixture pill's edge: the pill paints the same three stops in the reverse direction, which is what the design draws |
--goma-scroll | The odds row's scrollbar when it overflows (see below) |
--goma-backgroundBorder | Loading skeleton bars |
The widget root is transparent: the host owns the surrounding panel and the rail background. The card is fluid: it fills its container's width, with a 100 px hero strip and an 8 px radius. See Theming for the complete reference.
No font is bundled. GomaPulse widgets ship no typeface and register no @font-face inside the Shadow DOM (unreliable on Firefox), so the card renders in the shared :host stack from @gomagaming/core ('Open Sans' followed by the system fallbacks) which resolves to whatever the page has actually loaded. The design's Exo 2 is a host concern; load it on the page and override font-family on the element to use it.
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.
| Key | CSS variable | Default | Affects |
|---|---|---|---|
oddsLabelFontSize | --goma-oddsLabelFontSize | 0.62rem | Outcome name above the price on each odds button. It stays one line with an ellipsis. |
oddsLabelLineHeight | --goma-oddsLabelLineHeight | 1.25 | Line height of that outcome name. |
The same two keys exist on <goma-highlight-card>: both cards draw the shared odds button.
i18n / message overrides
Almost all copy on this card is host-fed, because it is a CMS surface: the market name, the outcome labels, the competition name, the fixture and the CTA label all come from card (or from ctaLabel, whose default is the plain English string "See other markets"). The widget adds no new i18n keys. The bundled keys its tree does reach are all pre-existing shared ones:
| Key | Default (en) | Used by |
|---|---|---|
today | "Today" | Kick-off date line for a same-day fixture. |
tomorrow | "Tomorrow" | Kick-off date line for the next day. |
add_to_favorites | "Add to favorites" | Star aria-label when not favourited. |
remove_from_favorites | "Remove from favorites" | Star aria-label when favourited. |
outcome_locked | "Outcome locked" | aria-label of an unavailable odds button. |
odds_increased | "Odds increased to {value}" | Live-region announcement on an upward odds change. |
odds_decreased | "Odds decreased to {value}" | Live-region announcement on a downward odds change. |
retry | "Retry" | Render-error banner's retry button. |
Embedding examples
Vanilla / CDN
The host owns the rail: loop your own array of picks and give each element one card:
html
<div class="pros-choice-rail"></div>
<script type="module">
import '@gomagaming/pros-choice'
const picks = await fetch('/api/cms/pros-choice').then((r) => r.json())
const rail = document.querySelector('.pros-choice-rail')
for (const pick of picks) {
const el = document.createElement('goma-pros-choice')
el.config = { locale: 'en' }
el.card = pick
el.addEventListener('goma:pros-choice-cta', (e) => {
// e.detail = { id, eventId, ctaType, label, url }
if (e.detail.url) window.location.href = e.detail.url
})
rail.appendChild(el)
}
</script>css
/* The rail is yours: the design lays the cards out with an 8 px gap. */
.pros-choice-rail {
display: flex;
gap: 8px;
overflow-x: auto;
}
.pros-choice-rail > goma-pros-choice {
flex: 0 0 auto;
width: 348px;
}React
jsx
import '@gomagaming/pros-choice'
import { useEffect, useRef } from 'react'
function ProsChoiceCard({ 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:pros-choice-cta', handler)
return () => el.removeEventListener('goma:pros-choice-cta', handler)
}, [card, onCta])
return <goma-pros-choice ref={ref} />
}
export function ProsChoiceRail({ picks, onCta }) {
return (
<div style={{ display: 'flex', gap: 8, overflowX: 'auto' }}>
{picks.map((card) => (
<ProsChoiceCard key={card.id} card={card} onCta={onCta} />
))}
</div>
)
}Vue
vue
<script setup>
import '@gomagaming/pros-choice'
import { ref, watchEffect } from 'vue'
const props = defineProps({ picks: { type: Array, default: () => [] } })
const els = ref([])
watchEffect(() => {
els.value.forEach((el, i) => {
if (!el) return
el.config = { locale: 'en' }
el.card = props.picks[i]
})
})
</script>
<template>
<div class="rail">
<goma-pros-choice
v-for="(pick, i) in picks"
:key="pick.id"
:ref="(el) => (els[i] = el)"
@goma:pros-choice-cta="$emit('cta', $event.detail)"
/>
</div>
</template>Accessibility
- The card is a
div role="button"withtabindex="0"and anaria-labelbuilt from the market name, fixture and competition; not a native<button>, because it nests other interactive controls. Enter and Space activate it. - Every nested control stops both click and keydown from reaching the card, so the star, an odds button and the CTA each fire exactly one intent.
- The favourite star is a native
<button>carryingaria-pressedand a state-dependentaria-label. - Odds buttons carry
aria-pressedfor selection state,aria-disabledwhen the offer is unavailable, anaria-labelof"<outcome>, <odds>", and anaria-live="polite"region announcing odds changes. - The hero image is
aria-hiddenunless the host suppliesimageAlt; the sport glyph, region flag and gradient divider are decorative and hidden from assistive technology. - Card, star, odds buttons and CTA all carry a visible
focus-visiblering (--goma-highlightPrimary).
What's deferred
- Outcome-level resolution from
bettingOfferIdalone. The auto modes resolve a card from{ eventId, marketId }: see Data sources. What is not built is the<goma-suggested-bet>-style path that takes a bareoutcomes: [{ bettingOfferId }]and walks the betting store's global entity maps back to its outcome and market. A card that wants specific outcomes still names its market and takes the market's own outcomes. - The promotional CTA variant. The design's second
ctaCardvariant (a green gradient band with a lightning glyph and a brand wordmark) is not built. Only the dark "See other markets" link ships, which is whyctahas notypefield and the widget introduces no gradient tokens. - The rail. No carousel, arrows, dots, edge fades, peek or section title are inside the widget; the design source has none either (its rail frame is three cards in a plain row). The host lays them out.
- A refresh / cycle glyph. Described in early briefs but absent from the design source: the competition row ends at the name. It is not built and no
goma:pros-choice-refreshevent is emitted. Should it be confirmed, the widget would still only report the tap: it has no alternative-pick source, so cycling is the host's job (swap the object incard). - Live-match treatment. No LIVE badge, score or clock.
card.isLiveis recorded on the betslip selection but changes nothing visually, because nothing is designed for it. - A designed empty / loading / error state. See Empty / loading / error states: every state there is a conservative fallback, not a design.
- More or fewer than three outcomes. The design draws exactly three full-width buttons and specifies nothing else. In the auto modes the card shows the first three priced outcomes of the market, in the order the feed sends them (never re-sorted). An outcome the provider lists but never prices, such as a scorer market's "None", is left out rather than shown with a price that never arrives; it appears in its feed position if an offer is created for it later. Fewer than three priced outcomes stretch to fill the row. A host-fed card shows every outcome the host supplies: four or more hold an 88 px minimum and the row scrolls horizontally rather than crushing the labels.
Source layout
| Layer | File |
|---|---|
| Element class | packages/pros-choice/src/ProsChoiceElement.js |
| App wrapper | packages/pros-choice/src/ProsChoiceApp.vue |
| Card | packages/pros-choice/src/components/ProsChoice/ProsChoiceCard.vue |
| Market headline / fixture pill / odds row | packages/pros-choice/src/components/ProsChoice/{ProsChoiceMarketHeadline,ProsChoiceFixturePill,ProsChoiceOddsRow}.vue |
| View-model composable | packages/pros-choice/src/composables/useProsChoiceCard.js |
| Per-widget CSS | packages/pros-choice/src/styles/card.css |
| Shared card pieces | packages/ui/src/components/PromoCards/{PromoCardShell,PromoCardHero,PromoCardCompetitionRow,PromoCardKickoffTime,PromoCardCtaLink}.vue |
| Shared odds button | packages/ui/src/components/GameCards/RegularOdds.vue |
| Shared region flag | packages/ui/src/components/common/CountryFlag.vue |
| Cross-widget storage sync | packages/sports-domain/src/shared-state/{useSharedBetslipSync,useSharedFavoritesSync}.js |
| Theme-token registry | packages/pros-choice/src/themeTokens.js |
| Playground entry | packages/pros-choice/src/playground.manifest.js + playground/src/registry.ts |
| Tests | packages/pros-choice/tests/{ProsChoice,useProsChoiceCard,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 differentlocalevalues 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, awaitwindow.__gomaCdn.readyfirst, 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 whenfavoritesApiBaseUrlis 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:subscribeToMatchOddsAdditiveandbannerEvents.subscribeForMatchOddsacceptbettingTypeIdandeventPartId. With both, they open that one market's feed,{matchId}/match-odds/{bettingTypeId}-{eventPartId}, instead of every market of the match.subscribeForMatchOddsno 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.unregisterGraceMssets the window, and0restores the immediate release.@gomagaming/suggested-bet: inrecsysmode 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-cardand@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
eventIdnow 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 asportIdto resolve an event. ThesportEventLimitproperty and thesport-event-limitattribute 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. SetshowMarketNameon 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.orderMarketsByMainMarketstakes an optional{ isLive }andresolveMarketSelectionan optionalisLiveoption for this. A feed that declares only one list ranks every event by that list, as before.<goma-suggested-bet>:custom-elements.jsonnow lists the defaults for theicon-base-urlattribute ('') and theflagBaseUrlproperty (the built-in flag CDN).- Playground theme-token lists (
./playgroundexport) 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, and1Mfor 999,999) instead of keeping a trailing.0(25.0K).
Game cards: a suspended odds button now paints
backgroundDisabledOdds. The restingbackgroundOddsclass 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 readstextDisabledOdds, the same token as the suspended odds button, instead ofinputTextTitleDisabled(this reverses the token choice of a separate change). The suspended button's padlock is now the design's plain 14px padlock (core'slock.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
- The odds button's outcome name (the line above the price) reads its size and line height from
--goma-oddsLabelFontSizeand--goma-oddsLabelLineHeight, which fall back to the old0.62remand1.25.<goma-pros-choice>gains anappearanceobject and, like<goma-highlight-card>, acceptsoddsLabelFontSizeandoddsLabelLineHeight. The name stays one line with an ellipsis. Without these keys every odds button renders exactly as before.@gomagaming/coreis 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/quickAddAmountsJsonare nowplaceBetHeaders/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
highlightedImagesstring default was indexed character by character and requested bad image URLs on every load. The missions-hubemptyStateImagesillustrations now show.
No widget runtime behaviour changes.
- The betslips'
0.36.0
Moved every shared UI component out of
@gomagaming/sports-domaininto a new@gomagaming/uipackage, which depends on it:The game cards:
RegularOddsand its live/prelive/outright/market variants,SkeletonCardandCountryFlag. Used by every widget that lists events.The betslip family:
BetslipPaneland its selection, summary and bonus pieces, and theGomaDialogmodal 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.
MyBetsPanelandMyBetsCardmoved 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/uiis installed automatically with the widgets that use it.@gomagaming/sports-domainno longer exports any component. It keeps the stores, API clients, composables and the promo-card auto-mode layer, and additionally exportsuseBetslipSystem,useBonusSessionGate,bonusRowTitle,convertOdd,formatRatePercent,formattedDate,formattedTimeandresolveDateLocale. A host importing one of the moved components from@gomagaming/sports-domaindirectly 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:truewas submitted asACCEPT_HIGHER, which accepts a rise. Its replacement,oddsChangeBehavior, says which ofaccept-any,accept-higherandnever-acceptapplies, 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 asACCEPT_HIGHER. Nothing about those bets changes. - A betslip with
acceptOddsChange: falsewas being submitted asACCEPT_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 exportedBETSLIP_STORAGE_KEYrather than a literal. Widget code that sets the old boolean keeps working: it is still available, derived from the policy.- A betslip with
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.outcomesand the market reaches the card with its two sides the other way round.sortOutcomesonly ranks outcomes carrying aheaderNameKey, which a handicap outcome does not, so every pair compared equal andArray#sort's stability handed the layout to arrival order. For a two-sided market the comparator now falls through to the provider's outcomecode: numbered codes (#1/#2) compare numerically, so#10does not sort before#2, and the provider's named pairs (#Y/#N,#O/#U,#Odd/#Even) are ranked the waygetOutcomeOrderalready 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.sortOutcomesreturns 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>callsortOutcomeson their own market lists and render the shared odds button, so the new ordering and the newlineongoma:outcome-selectreach them too.The betslip.
RegularOddsrecords the market's line (paramFloat1) on the selection at tap time, and it now survives both cross-widget bridges: thegoma:betslip:v4envelope carries it as an identity field, andgoma:outcome-selectcarries it in its detail. Without that it never reached the widget that reads it.useBetslipOddsSynccompares 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, anduseBetslipLogicrefuses 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: truebesideisAvailable: 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) andgoma:bet-failedlists those legs under a newmovedLineSelectionskey rather than folding them intounavailableSelections, so a host does not word a live match as dead.placeBetBlockedReasongains'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 newplace_bet_blocked_line_movedkey 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.
SbIconresolves 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 toopacity: 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
childrenIdsnames 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 neutralnoiconglyph as before. The newsportIconNameshelper in@gomagaming/coreis 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}.svgwhen one is set, and the origin-relative/icons/sports/dark_mode/{iconId}.svgwhen it is not.iconBaseUrlhas 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
SbIconinherits 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/subscribemodeuseSportsSourcemirrors 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 asstate.sportsCatalog, andsportIconLookupList(new in@gomagaming/core) is what the tiles, the trigger pill and the settings row read.mapSportalso refresheschildrenIdson 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,autoplayIntervaland 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 sport1and 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 theconfigobject 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 ausernameand 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
loginWithCeSessioncall) now has a 5s deadline, tunable with the newsessionStepTimeoutMskey (0disables it). A timed-out StartInfo request is aborted and retried. A timed-outloginWithCeSessiondrops 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
errorevent alongsidegoma:error.Every factory-built widget's app-level Vue error handler dispatched two events for the same failure:
goma:error, and an un-namespacederror. The bare one is gone;goma:erroris unchanged and still carries the same{ message, code, component? }detail.Like every GomaPulse event it was
composedand bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had onwindow.addEventListener('error', …): as aCustomEventwithmessage,errorandfilenameallundefined, since those areErrorEventfields aCustomEventdoes 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
errorCapturedwalk at the throwing component's parent, so theonErrorCapturedboundary 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 emitgoma:retry.Hosts listening for
goma:errorneed no change. A host listening for bareerroron a widget element should move togoma: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-choicealso 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), andcasino-games. The payload is{ widget, type, items }, wheretypeis'sports-event'or'casino-game'. The event bubbles and is composed, so a single listener ondocumentcatches every widget on the page.Emission fires once when data resolves and again only when the data's identity changes:
markets,scoreandclockare excluded from the identity signature, so odds ticks do not re-fire it. An emptyitemsarray 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.
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 onel.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, aneventIdand adefaultMarket) and the widget resolves teams, competition, kick-off, market name, outcome labels and odds from the feed, keeping prices ticking. Auto mode needsconfig.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 }]andcta: { 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/awayTeamLogoare read only by<goma-highlight-card>, the only card that draws team crests; andfixtureLabelis 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:
oddforodds,name/outcomeNameforlabel,idforoutcomeId,offerIdforbettingOfferId, and a top-levelctaUrlshorthand forcta.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. Emitsgoma:outcome-select/goma:outcome-deselect(identical to every other odds button, so both betslips pick it up with no wiring),goma:event-navigateon a body tap (so a sibling<goma-game-details>follows it) alongsidegoma:highlight-select,goma:highlight-ctafrom the link pill, andgoma:favorite-toggle/goma:login-requiredfrom the star. Writesgoma:betslip:v3and theeventsarray ofgoma: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). Emitsgoma:outcome-select/goma:outcome-deselect,goma:pros-choice-selecton a body tap plusgoma:event-navigatewhen the card carries aneventId,goma:pros-choice-ctafrom the link, andgoma:favorite-toggle/goma:login-requiredfrom 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 theeventsarray ofgoma:favorites:v1, and it is hidden entirely on a card with noeventId.@gomagaming/sports-domaingains the presentational layer both cards compose:PromoCardShell,PromoCardHero,PromoCardCompetitionRow,PromoCardKickoffTimeandPromoCardCtaLink, 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.CountryFlagalso takes a newsrcprop that wins over thevenueIdCDN lookup, for hosts whose per-item flag URL does not follow the CDN naming scheme.<goma-highlight-card>'s "See other markets" link now emitsgoma:event-navigatealongsidegoma: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.