Skip to content

<goma-multi-banners> ​

A swipeable hero carousel of injectable banner cards. One widget renders three card types side by side, sport (a sporting event, resolved live or supplied inline), casino (a game promo), and info (a generic promo), with prev/next arrows, pagination dots, swipe/drag, and opt-in autoplay.

Live demo ​

Open in the playground →: toggle Multi banners in the widget rail, then drive the banners array, autoplay, and the multi-card layout controls from the inspector's Form / JSON / Snippet tabs. Use Share setup to capture a ?s=… URL for a support ticket or repro.

Element tag ​

html
<goma-multi-banners></goma-multi-banners>

Installation ​

@gomagaming/multi-banners is a scoped private package. See Installation for the one-time .npmrc setup required to consume it.

bash
pnpm add @gomagaming/multi-banners

You also need the shared peer dependencies installed at your app level:

bash
pnpm add vue pinia vue-router vue-i18n @vueuse/core autobahn-browser swiper

autobahn-browser is only exercised when at least one sport banner uses eventId (the widget opens a WAMP connection to resolve it); casino, info, and inline sport banners never touch the socket. All seven packages are declared as peerDependencies, so a consumer with multiple @gomagaming/* widgets gets exactly one copy of each.

Configuration ​

Assign a config object to the element's config property. Only the connection fields are specific to this widget: everything else is the shared config surface every widget accepts.

js
el.config = {
  locale: 'en',
  // Required only for feed-backed sport banners (see Data sources):
  socketUrl: 'wss://…',
  socketRealm: 'sportsbook',
  ucsOperatorId: '2838',
  bettingApiBaseUrl: 'https://…',
  // Optional restyle:
  theme: { '--goma-buttonBackgroundPrimary': '#ff6600' },
}
KeyTypeRequiredDescription
socketUrlstringOnly for feed-backed bannersWAMP socket URL used to subscribe to the banners' events by id.
socketRealmstringOnly for feed-backed bannersWAMP realm.
ucsOperatorIdstringOnly for feed-backed bannersOperator ID.
bettingApiBaseUrlstringNoREST base URL for betting offers.
localestringNoi18n locale. Defaults to 'en'.
favoritesApiBaseUrlstringNoFavourites 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.
themeobjectNoCSS variable overrides: see Theming.
debugbooleanNoSurfaces verbose console logging, including the id set useBannerEvents subscribes to. Defaults to false.

Casino and info banners, and any sport banner pinned to dataSource: 'prop' or carrying no eventId, need no connection config at all.

Data sources ​

Every banner runs in one of three modes, chosen per entry:

ModeWhat it means
'subscribe'The widget subscribes to the event by id (live or prelive) over WAMP and fills in teams, competition, start time and odds itself. Odds tick live. Needs eventId (+ config.socketUrl).
'rpc'The same resolution, released once every feed-backed banner has resolved: a one-shot snapshot that then stops ticking.
'prop'Fully host-fed: the entry supplies its own display fields and odds. No socket involved.

Sport entries take the mode from their own dataSource, falling back to the element-level dataSource (default 'subscribe'). Two clauses override that:

  • an entry whose type is 'casino' or 'info' is always 'prop': there is no event to resolve;
  • an entry with no eventId is always 'prop': there is nothing to resolve from.

The feeds follow the banners, in both directions. Switching a mounted element's dataSource to 'prop', or removing the last feed-backed banner, releases the open feeds rather than holding them until the element is removed from the page, which matters because every widget on the page shares one socket. Moving banners to a different sport releases the sport that dropped out and opens the new one; adding a sport only opens what it adds, leaving the existing feeds alone. Switching back to a live mode rebuilds what it needs.

The mode matrix ​

typeeventIdentry dataSourceEffective modeRenders from
sportsetunsetelement default: 'subscribe'the live feed
sportset'subscribe''subscribe'the live feed
sportset'rpc''rpc'the feed, then frozen
sportset'prop''prop'the host's own fields: new, see below
sportunsetany'prop'the host's own fields
casino / infoanyany'prop'the host's own fields

This is additive. Before the mode existed the behaviour was implicit and per entry (an entry with an eventId resolved from the feed, one without did not) and every row above except the fourth reproduces exactly that. Leaving dataSource alone everywhere changes nothing.

The fourth row is the new capability: dataSource: 'prop' on an entry that also carries an eventId. That combination was impossible before (an eventId always won outright), so no existing embed can depend on the old behaviour. It lets a CMS store the real event id, so a card tap emits a goma:event-navigate that opens the right fixture, while pinning curated odds that do not tick.

js
el.dataSource = 'subscribe'      // element-level DEFAULT for sport entries

el.banners = [
  // Resolves itself from the feed (the element default).
  { id: 'b1', type: 'sport', eventId: '900987654321098765', sportId: 1, image: '…' },
  // Same event, but the odds are the host's and stay put.
  { id: 'b2', type: 'sport', dataSource: 'prop', eventId: '900987654321098766',
    homeTeam: 'Sporting C.P', awayTeam: 'Torreense', marketName: '1X2',
    bettingTypeId: 219, eventPartId: 0,
    outcomes: [{ outcomeId: 'o1', label: 'Sporting C.P', odds: 1.92, bettingOfferId: 'bo-1' }] },
  // No event at all: always host-fed.
  { id: 'b3', type: 'info', title: 'Welcome Bonus', cta: { label: 'Claim', url: '/promo' } },
]

An unsupported dataSource value logs one console.warn naming it and falls back to the element-level default; it never throws.

'rpc' releases all-or-nothing

One by-id topic set feeds every banner, and the shared subscription manager has no per-event release. So the widget drops its topic only once every feed-backed entry is in 'rpc' mode and has resolved (event found, market selected, every rendered outcome priced). A single 'subscribe' entry anywhere in the array keeps the feed live: a banner promised live odds never silently stops ticking.

Inside 'prop': two host-fed shapes ​

A 'prop' sport entry is resolved in this order: unchanged, and identical to <goma-hero-card>'s hostEvent-over-raw-props rule:

  1. event set: the host-fed GomaHostEvent path, run through normalizeHostEvent. Wins over the flat fields below.
  2. neither, the flat inline fields (homeTeam, outcomes, …), unchanged and still fully supported, this is not a deprecated path.
js
el.banners = [
  {
    id: 's1',
    type: 'sport',
    backgroundImage: 'https://cdn.example.com/banner.jpg',
    ctaUrl: '/sports/event/900',
    event: {
      id: '900987654321098765',
      homeTeam: 'Real Madrid',
      awayTeam: 'Fenerbahce',
      competition: 'EuroLeague',
      sportId: 3,
      isLive: true,
      score: { home: 58, away: 52 },
      clock: { minute: 27, part: '3rd Quarter' },
      markets: [
        {
          id: 'm-winner',
          name: 'Match Winner',
          bettingTypeId: 219,
          eventPartId: 0,
          outcomes: [
            { id: 'o-home', label: 'Real Madrid', odds: 1.65, bettingOfferId: 'bo-h-1' },
            { id: 'o-away', label: 'Fenerbahce', odds: 2.20, bettingOfferId: 'bo-a-1' },
          ],
        },
      ],
    },
  },
]

See the Host-fed data guide for the full GomaHostEvent field reference: everything documented there applies to this banner's event key one for one.

Feed-resolution note. Events are resolved by id: the widget asks the provider for exactly the events your banners name, live or prelive, so any event the provider knows resolves. There is no sport feed and no event window to fall outside of, and the banner needs no sportId to resolve. Until the event arrives the card's odds row shows a loading skeleton (see Empty / loading / error states). The widget opens one subscription for the whole banners array, and opens it again only when the set of event ids changes. For full control, pin the entry to dataSource: 'prop' and supply the fields yourself.

One record, four widgets ​

A type: 'sport' entry is the GomaPromoCard record that the single-card promo widgets (<goma-highlight-card> and <goma-pros-choice>) take on their card property. banners is GomaPromoCard[] plus the two event-less types (casino, info) and the per-entry dataSource. One CMS shape feeds all of them, with no rename step.

This widget shipped first and with its own spelling for a handful of fields, so both vocabularies are accepted everywhere. Nothing has been renamed or deprecated: where an entry carries both spellings, the banner one wins, so an existing embed renders byte-for-byte as it always has.

ConceptBanner spelling (wins)Canonical GomaPromoCard spelling
artworkbackgroundImageimage
artwork alt text-imageAlt
competition namecompetitioncompetitionName
competition id-competitionId (rides onto the betslip leg)
home / away teamhomeTeam / awayTeam (also home / away)same
fixture as one string-fixtureLabel (names the synthesized event)
sport glyph URLsportIconsportIconUrl
region flag URL-flagUrl (wins over the venueId lookup)
CTA label / URLctaLabel / ctaUrlcta: { label, url }
market selectormarketIddefaultMarket
outcome idoutcomes[].idoutcomes[].outcomeId
outcome labeloutcomes[].labeloutcomes[].name, outcomes[].outcomeName
outcome priceoutcomes[].oddsoutcomes[].odd
outcome offer idoutcomes[].bettingOfferIdoutcomes[].offerId

Fields with no banner spelling (imageAlt, competitionId, fixtureLabel, flagUrl) are new and additive: omitting them behaves exactly as before.

Not every GomaPromoCard field has a job here: this widget draws no team crests (homeTeamLogo / awayTeamLogo) and no favourite star (isFavorite), so those are read by the single-card widgets only and ignored here.

Choosing which market a banner promotes ​

A sport banner renders one market. defaultMarket picks it, per entry, so a rail can promote 1X2 on one card and Over/Under on the next:

js
el.banners = [
  { id: 'b1', type: 'sport', eventId: '9009…765', defaultMarket: 219 },  // Match Winner
  { id: 'b2', type: 'sport', eventId: '9009…766', defaultMarket: 225 },  // Over/Under
]
  • The recommended value is a bettingTypeId: the same value <goma-events-horizontal>, <goma-favorites> and <goma-competitions-list> take under the same prop name, and the same value <goma-filters-settings> puts on goma:filters-apply. The per-event market instance id and the provider's displayKey also resolve.
  • marketId is accepted as an alias, because it is the already-published record key on a banner entry and on a GomaPromoCard. defaultMarket wins if both are set.
  • It applies in every mode: against the live event's markets in 'subscribe' / 'rpc', and against event.markets in 'prop'. A flat inline entry has only one market, so there is nothing to select.
  • A market the event does not carry falls back to the leading market, so a banner never loses its odds row over a stale CMS value.
  • A bare bettingTypeId / eventPartId on the entry is not a selector: those two stay the inline passthrough that puts the market key on the betslip leg, exactly as before. Ask for a market with defaultMarket.

Why the default changed which market renders

With no selector the widget used to read markets[0]. By position. The matches-aggregator delivers a market suspend/re-offer as a DELETE followed by a CREATE and appends the re-created market to the tail of the list, so a live banner reshuffled its own headline market as markets came and went. The leading market is now derived from the provider's canonical main-market ranking instead, which is requested once per sport and is stable. The events feed itself carries no such ranking, so after the first events arrive the widget makes one small extra request for each sport it shows and keeps the result. Until that answer arrives, the order of the markets in the feed is used. If the request fails, it stays that way. A live event is ranked by the live list and a pre-match event by its pre-match list, because the two can put different markets first. Host-fed ('prop') entries have no such ranking and no such churn, so there the leading market is still the first one you listed.

Live vs prelive sport cards ​

A sport card adapts to the event's state, mirroring the live/prelive game cards elsewhere in the family:

  • Live (isLive true): the right-hand block shows the live clock / match part (e.g. 27' - 3rd Quarter) and the running score (home - away). For feed-backed cards ('subscribe' / 'rpc') the score and clock tick live off the same feed the odds use; for event-backed cards they come from the event.score / event.clock you supplied (a genuine snapshot, not ticking on its own, push a new event object to update it). The flat inline fields (no eventId, no event) have no score / clock fields at all, so they show no score, the same as a live match the feed sends no score for: use an event snapshot, or leave the entry feed-backed, for anything that needs a real score. See Host-fed data for the full comparison against <goma-hero-card>.
  • Prelive: the right-hand block shows the start date + time (e.g. Today / 14:30), from event.startTime / startTime, or the explicit dateLabel / timeLabel fields in flat inline mode.

The odds row adapts its column count to the number of outcomes (1, 2 or 3, outcomes beyond the third are not rendered), so a two-way market (e.g. an Asian-handicap line) fills the card width rather than leaving an empty third column.

Host-fed sport banners (event or flat inline): supply bettingTypeId

In 'subscribe' / 'rpc' mode the widget resolves the market itself, so odds taps carry the full market identity. A 'prop' entry: event or flat inline (has only what you put in the banner) and a betslip leg without a bettingTypeId is rejected by the provider at place-bet ("Invalid or missing Betting Type Id on placed bet").

  • In flat inline mode, bettingTypeId (and its pair eventPartId) sit directly on the banner item, alongside marketId / marketName / outcomes.
  • In event mode, they've moved inside event.markets[].bettingTypeId / event.markets[].eventPartId: per market, not per banner, since one event can carry several markets. normalizeHostEvent (the mapper that expands event) emits a named console.warn for any market that omits bettingTypeId, at the moment the payload enters, so a CMS gets a pointed warning during content review instead of a silent rejection at place-bet.

Whichever widget on the page is running the shared betslip sync (this one included) also logs one console.warn naming the outcome the first time a goma:outcome-select arrives without a bettingTypeId: the tap and the local selection still go through, only the warning is emitted. See Host-fed data for the full mechanics.

The banners array ​

ts
type MultiBannerItem =
  | {
      id: string                    // CMS slot key: NOT an event id
      type: 'sport'
      dataSource?: 'prop' | 'rpc' | 'subscribe'   // per-entry mode; defaults to the
                                    // element-level dataSource ('subscribe')
      showMarketName?: boolean      // per-entry override of the element-level
                                    // showMarketName; wins when set
      backgroundImage?: string      // alias: image
      imageAlt?: string             // blank keeps the artwork decorative
      ctaUrl?: string               // alias: cta.url
      // Feed resolution ('subscribe' / 'rpc'):
      eventId?: string | number     // string end-to-end; absent forces 'prop'
      sportId?: number              // default 1: not needed to resolve an event
                                    // (it is looked up by id); picks the sport icon
      defaultMarket?: string | number   // which market this banner promotes;
                                        // a bettingTypeId. Alias: marketId
      // 'prop' + event: wins over the flat fields below:
      event?: GomaHostEvent         // one host-fed event; see /guide/host-fed-data.
                                    // Expanded via normalizeHostEvent. Unlocks a
                                    // real score + clock.
      // 'prop' + flat inline fields: legacy but fully supported, not deprecated:
      competition?: string          // alias: competitionName
      competitionId?: string        // rides onto the betslip leg; nothing renders it
      sportIcon?: string            // icon URL (optional: the header otherwise
                                    // auto-resolves the sport glyph from sportId).
                                    // alias: sportIconUrl
      venueId?: string | number     // region/country flag lookup (via flagBaseUrl);
                                    // globe fallback when unresolved
      flagUrl?: string              // explicit flag URL; wins over venueId
      homeTeam?: string             // alias: home
      awayTeam?: string             // alias: away
      fixtureLabel?: string         // "{home} - {away}" by default
      isLive?: boolean
      startTime?: number            // epoch ms (or use dateLabel/timeLabel)
      dateLabel?: string            // e.g. "Today"
      timeLabel?: string            // e.g. "14:30"
      marketId?: string             // the synthesized market's id in flat inline
                                    // mode; also read as a defaultMarket alias
      marketName?: string           // label of the synthesized market; the pill text
                                    // when showMarketName is on
      bettingTypeId?: string | number   // market identity: see the note above.
                                        // NOT a market selector: use defaultMarket.
                                        // In event mode this lives per-market at
                                        // event.markets[].bettingTypeId instead.
      eventPartId?: string | number     // same relocation as bettingTypeId above
      cta?: { label?: string; url?: string }   // alias for ctaLabel / ctaUrl
      outcomes?: Array<{
        id?: string                 // alias: outcomeId
        label: string               // aliases: name, outcomeName
        odds: number                // alias: odd
        bettingOfferId?: string     // alias: offerId. Never fabricated by the widget
                                    // in feed mode: absent means null
        isAvailable?: boolean
      }>
    }
  | {
      id: string
      type: 'casino'
      backgroundImage?: string      // alias: image
      imageAlt?: string
      ctaUrl?: string               // alias: cta.url
      gameId?: string
      category?: string
      categoryIcon?: string         // icon URL
      title?: string
      description?: string
      ctaLabel?: string             // CTA only renders when set. alias: cta.label
    }
  | {
      id: string
      type: 'info'
      backgroundImage?: string      // alias: image
      imageAlt?: string
      ctaUrl?: string               // alias: cta.url
      title?: string
      description?: string
      ctaLabel?: string             // alias: cta.label
    }

GomaHostEvent above is the same type documented in full on the Host-fed data guide: read it there for every field's exact semantics (score/clock null-handling, the bettingTypeId warning, id synthesis, …); this page only covers what's specific to how a sport banner consumes it.

Market-name pill ​

Sport banners can show the name of the market they promote (for example "Match Winner") in a small pill under the team names. It is off by default, so a banner looks the same until you turn it on.

js
el.showMarketName = true            // every sport banner
el.banners = [
  { id: 'b1', type: 'sport', eventId: '123', showMarketName: false },  // this one stays off
]
  • Precedence. A boolean showMarketName on a sport entry wins over the element-level value. Without it on the entry, the element-level value applies.
  • Label. The market the card renders: its bettingTypeName, else its name. In inline mode that name is the entry's marketName. In eventId mode it comes from the live feed, so the pill appears once the event resolves.
  • No pill when the setting is off, or when the card has no market or the market has no name.
  • Look. A transparent, fully rounded pill with a 1px border and a one-line label that ends with an ellipsis when too long. A 28px gradient line, from the border colour to transparent, follows it. Colours: --goma-bannerMarketPillBorder and --goma-bannerMarketPillText. Sizes: the marketPillFontSize and marketPillLineWidth appearance keys.

Multi-card layout ​

By default the carousel shows one card centred at a time. Set slidesPerViewMobile and/or slidesPerViewDesktop to switch on the multi-card peek layout: several full cards side by side with a fixed strip of the next card bleeding at the edge, so it reads as a partial-bleed carousel.

js
el.slidesPerViewDesktop = 2   // 2 full cards on wide containers
el.slidesPerViewMobile = 1    // 1 full card on narrow containers
el.peek = 24                  // ~24px of the neighbour peeking (default)
html
<goma-multi-banners
  slides-per-view-desktop="2"
  slides-per-view-mobile="1"
  peek="24"
></goma-multi-banners>
  • Container-based breakpoint. The mobile/desktop switch is measured against the widget's own container width, not the viewport: a widget dropped in a narrow sidebar column shows the mobile count even on a wide screen. multiBreakpoint (default 768) is the container width at/above which slidesPerViewDesktop applies.
  • Logical pages. Dots, arrows and swipe advance a full page (the visible card count); the pagination dots stay 1:1 with those pages.
  • Desktop hover arrows. On hover-capable pointers the prev/next arrows stay hidden until you hover (or keyboard-focus) the section, then fade in without shifting the layout. On touch devices the arrows never appear: swipe and dots drive navigation. (Still gated by showArrows.)
  • Opt-in / no regression. Leaving both slidesPerView* unset keeps the original single centred-card engine exactly as before, so existing embeds are unaffected. centeredSlides / centeredSlidesBounds apply to the legacy layout only.

Props / attributes ​

PropertyAttributeTypeDefaultDescription
bannersbannersMultiBannerItem[][]The banner list (JSON when set as an attribute). A type: 'sport' entry is the same GomaPromoCard record <goma-highlight-card> / <goma-pros-choice> take: see One record, four widgets.
dataSourcedata-source'prop' | 'rpc' | 'subscribe''subscribe'Default mode for type: 'sport' entries; each entry overrides it with its own dataSource. The default reproduces the widget's original implicit behaviour: see the mode matrix. Casino and info entries are always 'prop'.
showMarketNameshow-market-namebooleanfalseShow the market name in a pill under the team names of sport banners (see Market-name pill). A sport entry's own showMarketName wins over this value.
selectedOutcomesselected-outcomesstring[][]Two-way mirror of selected odds ids (sport cards).
showArrowsshow-arrowsbooleantrueShow the prev/next arrows.
showDotsshow-dotsbooleantrueShow pagination dots. The dots stay hidden while every banner fits in the rail and it cannot move.
centeredSlidescentered-slidesbooleantrueCentre the active slide (legacy single-card layout only).
centeredSlidesBoundscentered-slides-boundsbooleantrueWith centeredSlides, pin the first slide to the left edge and the last to the right (only in-between slides centre): the natural hero behaviour. Legacy layout only.
looploopbooleantrueRewind to the start past the last slide (auto-off for one banner).
enableSwipeenable-swipebooleantrueAllow touch/drag swiping.
autoplayautoplaybooleanfalseAuto-advance on an interval.
autoplayIntervalautoplay-intervalnumber5000Autoplay delay (ms).
spaceBetweenspace-betweennumber16Gap between slides (px).
slidesPerViewMobileslides-per-view-mobilenumber | nullnullOpt-in multi-card layout. Full cards shown below multiBreakpoint. null keeps the legacy single centred-card layout.
slidesPerViewDesktopslides-per-view-desktopnumber | nullnullOpt-in multi-card layout. Full cards shown at/above multiBreakpoint (measured against the widget's container width). null keeps the legacy layout.
peekpeeknumber24Multi-card layout: pixels of the neighbouring card that peek at the edge.
multiBreakpointmulti-breakpointnumber768Multi-card layout: container width (px) at/above which slidesPerViewDesktop applies.
sportEventLimitsport-event-limitnumber50Deprecated, no effect. Events are resolved by id, so there is no sport feed to cap. Still accepted so existing pages do not break.
flagBaseUrlflag-base-urlstring'https://static.glastcoper.com/omfe-widgets/s/assets/1.10.2/om1/icons/flag/'Base URL for country-flag PNGs on sport banners. The sport header resolves ${flagBaseUrl}{venueId}.png; a globe glyph is the fallback. Defaults to the same OM flag CDN <goma-events-horizontal> uses, so flags resolve out of the box.
localelocalestring'en'i18n locale.
themethemeobject{}--goma-* overrides.
appearanceappearanceobject{}Structure-only sizes and spacing: see Appearance.

Events ​

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

EventdetailFired when
goma:banner-select{ type, id, ctaUrl?, eventId?, gameId? }Any banner is activated (CTA tap on casino/info, or sport-card body tap). eventId is only present on sport banners, gameId only on casino banners; on a sport banner it's the resolved event's real id whenever the entry declares an eventId (in any mode) or carries an event snapshot, and null for a flat inline entry with no event to identify.
goma:event-navigate{ eventId, homeTeam, awayTeam, competitionName }A sport card's body is tapped: drives <goma-game-details>. eventId is the resolved event's real id whenever the entry declares an eventId (in any mode) or carries an event snapshot; a flat inline entry with neither falls back to the banner's own id.
goma:outcome-select{ bettingOfferId, outcomeId, outcomeName, odds, eventId, marketId, bettingTypeId, eventPartId, line }An odds button on a sport card is selected (same shape as <goma-events-horizontal>, a betslip widget picks it up).
goma:outcome-deselect{ bettingOfferId, outcomeId }An already-selected odds button on a sport card is tapped again to remove it. Emitted by the shared odds-button component.
goma:favorite-toggle{ type: 'event', id, eventId, isFavorite }The favourite star on a sport card is tapped while signed in. The write lands in the shared goma:favorites:v1 envelope, so every other widget on the page reflects it immediately.
goma:login-required{ reason: 'favorite', type: 'event', id }The favourite star is tapped while signed out. Nothing is written and the star does not fill: your page decides how to prompt for sign-in.
goma:seo-data{ widget: 'multi-banners', type: 'sports-event', items: object[] }Fired once one or more sport banners' events resolve, and again only when an event's identity changes: never on an odds tick. Casino and info banners never contribute an item. See SEO data for the payload contract.
goma:error{ message, code, component? }Two paths. Connection layer: the WAMP adapter (needed to resolve eventId sport banners) 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 error-boundary banner.
ready{}Widget mounted.

Integrates with ​

<goma-multi-banners> has no follow* props and listens for no goma:* events itself: it is a pure host-fed leaf, so nothing upstream drives it automatically.

It commonly pairs downstream with <goma-betslip-floating> / <goma-betslip-sidebar> (odds taps on sport banners flow into them via the shared betslip storage) and <goma-game-details> (a sport-card body tap drives it through goma:event-navigate).

Writes selections into goma:betslip:v4: identity fields only (useSharedBetslipSync({ subscribeToState: false })), the same opt-out <goma-events-horizontal> uses. See Cross-widget wiring and Shared state for the full contracts.

Empty / loading / error states ​

  • Empty: banners: [] (the default) renders the carousel container with no slides, no cards, and no pagination dots; arrows still render if showArrows is on but sit disabled since there is nothing to navigate to. There is no dedicated "no banners" message.
  • Loading (per feed-backed sport card): until the target event arrives in the sport's subscribed feed, the card's team names and competition render blank and the odds row shows three pulsing skeleton bars instead of odds buttons.
  • Error: a Vue render error thrown anywhere in the widget's tree is caught by an onErrorCaptured boundary in MultiBannersApp: a role="alert" banner reading ERROR: <message> appears with a Retry button (emits goma:retry) and a dismiss ×. It self-clears automatically after 8000 ms even without user interaction.

Theming ​

All colours are --goma-* tokens (see the theming reference):

TokenUsed for
--goma-backgroundPrimarySection background behind the cards
--goma-backgroundCardsCard shell background
--goma-backgroundBorderCard shell border
--goma-backgroundOddsOdds-button background
--goma-buttonBackgroundPrimary / --goma-buttonTextPrimaryCTA button
--goma-highlightPrimaryFocus rings, Retry button background
--goma-highlightPrimaryContrastRetry button text on the render-error banner
--goma-textPrimaryText and active pagination dot
--goma-allWhiteNav arrow disc, painted at 60% opacity
--goma-bannerMarketPillBorderOptional market-name pill border, also the start colour of the line after it. Unset (default), it is --goma-highlightPrimary.
--goma-bannerMarketPillTextOptional market-name pill label colour. Unset (default), it is --goma-textPrimary.
--goma-navArrowBackgroundOptional nav arrow disc override. Unset (default), the disc is --goma-allWhite at 60%. For a palette where that does not pair with the caret, e.g. a light theme that wants a dark disc.
--goma-navArrowIconOptional nav arrow caret override. Unset (default), the caret is --goma-highlightSecondaryContrast.
--goma-highlightSecondaryContrastNav arrow caret
--goma-separatorLineInactive pagination dots
--goma-alertErrorRender-error banner background
--goma-widgetInsetHorizontal gutter: see Widget gutter

The card background image is host-fed; a fixed dark gradient over it keeps text legible regardless of the image.

--goma-widgetInset sets the HORIZONTAL gutter between the rail's content and the edge of the element: 8px here. Widgets differ, so a page stacking several of them can set the token once to line them all up, or 0 to hand the gutter to the page. It does not touch vertical padding, and leaving it unset keeps the value above. See Widget gutter.

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
railPaddingY--goma-bannerRailPaddingY12pxTop and bottom padding of the rail. The nav arrows follow it, so they stay centred on the cards.
headerFontSize--goma-bannerHeaderFontSize0.75rem (12px)Card header label: the sport card's competition name and the casino card's category.
titleLineHeight--goma-bannerTitleLineHeight1.25 (20px at 16px)Line height of the sport card's team names and the casino / info card titles.
teamGap--goma-bannerTeamGap0.25rem (4px)Space between the home and away team names.
dateFontSize--goma-bannerDateFontSize0.75rem (12px)Sport card's start-date line (prelive) and match clock (live).
dateFontWeight--goma-bannerDateFontWeight400Weight of that same line.
ctaPaddingY--goma-bannerCtaPaddingY0.375rem (6px)Casino / info CTA button top and bottom padding.
ctaPaddingX--goma-bannerCtaPaddingX1.25rem (20px)CTA button side padding.
ctaLineHeight--goma-bannerCtaLineHeight1.25rem (20px)CTA label line height (with the padding, sets the button height: 32px by default).
ctaGap--goma-bannerCtaGap0.375rem (6px)Space between the casino CTA label and its play glyph.
dotGap--goma-bannerDotGap6pxSpace between pagination dots.
prevArrowDisplay--goma-bannerPrevArrowDisplayinline-flexDisplay of the previous arrow. none leaves only the next arrow, for a forward-only strip; swipe and the dots still go back. Touch devices hide both arrows either way.
edgeFadeWidth--goma-bannerEdgeFadeWidth0pxWidth of a fade over the cards' right edge, from transparent to backgroundPrimary, under the next arrow. 0px draws none.
marketPillFontSize--goma-bannerMarketPillFontSize10pxLabel size of the market-name pill.
marketPillLineWidth--goma-bannerMarketPillLineWidth28pxWidth of the 1px gradient line after the market-name pill.

i18n / message overrides ​

Keyen defaultUsed for
liveLiveThe live badge on a live sport card's header.
retryRetryThe error-boundary banner's Retry button.
outcome_lockedOutcome lockedOdds-button aria-label when the outcome is unavailable: from the shared odds-button component.
odds_increasedOdds increased to {value}Screen-reader announcement when an odds value ticks up.
odds_decreasedOdds decreased to {value}Screen-reader announcement when an odds value ticks down.

Embedding examples ​

Vanilla / CDN ​

html
<goma-multi-banners id="b"></goma-multi-banners>
<script type="module">
  import '@gomagaming/multi-banners'

  const el = document.getElementById('b')
  el.config = { socketUrl: 'wss://…', ucsOperatorId: '2838', locale: 'en' }
  el.banners = [
    { id: 's1', type: 'sport', eventId: '123456', sportId: 1, backgroundImage: 'https://…/match.jpg' },
    { id: 'c1', type: 'casino', backgroundImage: 'https://…/slot.jpg', category: 'Slots', title: 'Gates of Olympus', description: 'Spin to win', ctaLabel: 'Play', ctaUrl: 'https://…' },
    { id: 'i1', type: 'info', backgroundImage: 'https://…/promo.jpg', title: 'Welcome Bonus', description: 'Up to €100', ctaLabel: 'Claim', ctaUrl: 'https://…' },
  ]
  el.addEventListener('goma:banner-select', (e) => console.log(e.detail))
</script>

React ​

jsx
import '@gomagaming/multi-banners'

export function Banners({ banners, config }) {
  return (
    <goma-multi-banners
      ref={(el) => {
        if (!el) return
        el.config = config
        el.banners = banners
      }}
    />
  )
}

Vue ​

vue
<script setup>
import '@gomagaming/multi-banners'
import { ref, watchEffect } from 'vue'
const el = ref(null)
const props = defineProps(['banners', 'config'])
watchEffect(() => {
  if (!el.value) return
  el.value.config = props.config
  el.value.banners = props.banners
})
</script>

<template>
  <goma-multi-banners ref="el" @goma:banner-select="$emit('select', $event.detail)" />
</template>

Accessibility ​

  • A clickable sport card's body is a div role="button" (not a native <button>, since it nests the odds buttons) with tabindex="0", an aria-label built from the competition and team names, and Enter/Space keydown activation.
  • Odds buttons (the shared RegularOdds component) carry role="button", tabindex="0", aria-pressed (selected state), aria-disabled (unavailable), an aria-label combining outcome name and price (or the outcome_locked message when locked), Enter/Space activation, and an aria-live="polite" screen-reader-only region announcing odds changes.
  • Casino/info CTA buttons are native <button> elements with visible text.
  • Pagination dots sit in a role="tablist" container; each dot is a <button> with an aria-label (Go to page N / Go to banner N) and aria-selected.
  • Prev/next arrows carry aria-label="Previous" / "Next".
  • Focus rings on every interactive element use the --goma-highlightPrimary token via focus-visible.

Integration Caveats ​

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

  • Tokens/Theming: --goma-backgroundCards/--goma-backgroundBorder (banner card shell) and --goma-alertError/--goma-highlightPrimaryContrast (render-error banner) are now in the Theming table above; a --goma-textSecondary entry that was declared but never actually used was removed from themeTokens.js and the docs.

  • 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.6
  • Read the live score in any feed language. The whole-match score row is now found by the part id of the match, and tennis sets and games by their scoring unit, not by the translated part name. In the game cards, <goma-game-details>, <goma-hero-card> and <goma-multi-banners>, a match on a language with no known part name shows its score. In the game cards and <goma-game-details>, live tennis on a Spanish feed also shows the current game points in place of the sets won. A match with no whole-match row, such as e-Tennis, shows the broadest part still running. Spanish set, game, quarter and inning names now fill the part columns.

    The live game card and the <goma-game-details> header now read one shared score, so they show the same thing. For a live match the feed sends no score for, they show the last score seen for that match. With none, they draw no score box, at any point in the match, and show the current part. A real 0 0 still draws. A live <goma-multi-banners> sport card with no score now shows none, where it showed 0 - 0. A host that feeds events with no homeScore or awayScore gets no score box in place of 0 0: send both as 0 to keep it. A set sport with no whole-match row now shows the sets won on the card, not the points of the running set. Live cards now show the feed's current part, such as 7th Game (2nd Set), where they showed the status, such as In Progress. In <goma-game-details>, goma:game-ended now carries homeScore and awayScore for a match with no whole-match row, such as e-Tennis, where it carried null.

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-multi-banners>: the pagination dots are now hidden when every banner already fits in the rail (for example two banners in a wide desktop column), since the carousel cannot move there. They come back as soon as the rail can scroll again, such as after a resize or when more banners arrive.

  • 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
  • <goma-multi-banners> and <goma-casino-games> take an appearance object. On multi-banners it sets the rail's top and bottom padding, the card header and start-date type, the team-name line height and spacing, the CTA button's padding, line height and icon gap, and the gap between pagination dots. On casino-games it sets the pagination dots' size, gap and the active dot's width, so a design can draw the active dot as a circle instead of the default pill. Every key is optional and an empty object keeps today's look. @gomagaming/core is bumped for the new utilities in its compiled sheet.

  • <goma-multi-banners> gains two appearance keys: prevArrowDisplay (none keeps only the next arrow) and edgeFadeWidth (a fade over the cards' right edge into the page background, none by default). <goma-casino-games> gains a maxDots property (max-dots attribute) that caps the pagination row: with more snap positions than maxDots, that many dots stand for the whole track, the active one at the current position's proportional place, and a tap jumps to the matching position. The default, 0, keeps one dot per snap position. Both widgets look and behave as before unless a host sets the new options.

  • Casino games and time filter: the error bar's dismiss × is now a button named "Close", so keyboard and screen-reader users can reach it, and its "ERROR:" prefix comes from the new error_prefix copy key and follows the widget's locale. In casino games, the carousel arrows' names and the dots' names ("Go to page 1") also come from copy keys (previous, next, and the new go_to_page). clearError() now removes an error bar that forceError() showed, and forcing the same message again after a dismiss shows the bar again.

    Casino games: a heading of spaces only now shows the default title, like a blank one. custom-elements.json now declares the goma:seo-data and goma:retry events the widget already emitted, and types a tapped game's id and name as possibly null. The theme token list now names the flame's own token (iconPrimary) and the error bar's, and the two optional arrow tokens (navArrowBackground, navArrowIcon) now list a default, also in multi-banners and sports navigation (horizontal).

    Core: the Portuguese "Next" label read "Prximo"; it now reads "Próximo".

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
  • The carousel navigation arrow accepts two optional theme tokens: --goma-navArrowBackground (the disc) and --goma-navArrowIcon (the caret). Unset, the arrow looks exactly as before (an allWhite disc at 60% with a highlightSecondaryContrast caret) so only a palette where those two do not pair (for example a light theme that wants a dark disc) needs to set them.

  • 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.

  • Match dates read day-first in English, so one match is no longer written two different ways across the suite.

    An event card writes the kick-off as a compact 11/10; <goma-game-details>'s header, <goma-hero-card>, <goma-multi-banners> and <goma-search>'s result rows write it in words through toLocaleDateString. With locale: 'en' (the bare tag, which Intl resolves to American ordering) that long form came out as October 11 next to the card's 11/10. English was the only shipped locale that disagreed with its own cards (fr and pt are already day-first, and the shared time formatter has always been pinned to en-GB), so the long form now resolves a bare en to en-GB. A host that explicitly configures en-US keeps American ordering.

    Two surfaces build their own Intl.DateTimeFormat rather than going through the shared formatters, and both showed the same inversion for the same match: the My Bets card's leg date (which also printed 07 October where the cards print 7 October, and sat directly under a hand-built dd/mm/yyyy "placed" line) and the live-chat bet modal's per-leg date. Both now resolve the locale the same way.

    Every package listed here renders a date that changes, which is why the note reaches all of their pages rather than only the two whose source moved: including the two betslips and <goma-messenger>, which render the same leg date through the <goma-my-bets> widget they embed.

    Not covered, and deliberately: the missions surfaces format challenge dates with the browser's locale rather than the widget's, and, in the same file, render the same instant as a deliberately locale-independent DD/MM/YY two functions below, so one view contradicts itself regardless of locale. That is a different defect from this one and wants its own ticket.

  • 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.

  • The event-resolution drivers now release their WAMP feeds when they stop needing them, instead of holding them until the element unmounts.

    Both drivers stopped doing further work when a widget left a live state, but every guard that stopped them returned before any teardown. The feeds stayed open: still streaming into a store the widget no longer read, on a socket now shared by the whole page.

    usePromoCardEvents (<goma-highlight-card>, <goma-pros-choice>) and useBannerEvents (<goma-multi-banners>) each leaked on two paths:

    • dataSource changes to a non-live mode on a mounted widget. A host going fully host-fed kept the previous feeds open.
    • The last resolvable record goes away: cards cleared, or the last feed-backed banner removed. Nothing was left to resolve, and nothing was released.

    <goma-multi-banners> additionally never released a sport that dropped out of its banner set: moving banners from one sport to another opened the new sport's feeds and left the old ones open indefinitely. It now releases and rebuilds when the set shrinks, while a pure addition still opens only what it adds: banners spanning several sports are what this driver's accumulating behaviour is for.

    Releasing when nothing is open is a no-op, and a widget that never subscribed does not report a teardown it never performed. The subscription manager is created per element, so one widget's release can never reach another's feeds, and returning to rpc or subscribe rebuilds the feeds as before.

0.31.0
  • Added --goma-widgetInset, one CSS custom property that settles how far a widget's content sits from the edge of its element.

    Widgets had each grown their own horizontal padding (8px around the multi-banners and casino-games rails, none on the sports-navigation strip or the event feeds) so stacking them in a column started content at different offsets, and the only way to line them up was to cancel the difference with negative margins on the host side.

    Set --goma-widgetInset once and every widget that has a gutter adopts it; set it to 0 and they all run flush to their element, handing the gutter back to the page. Widgets that were already flush are unaffected: the token does not add padding where there was none.

    It governs horizontal padding only. Vertical padding is the space between sections, which a host already controls with margins, so folding it into the same token would mean setting the gutter to 8px silently added 8px of top padding to rails that had none.

    Panel widgets are deliberately not governed by it: <goma-competitions-list> and <goma-top-competitions> (list variant) keep a fixed 16px interior, because that padding is the inside of a rounded box the widget paints rather than a page gutter.

    Nothing changes for existing embeds. There is no global default: each widget falls back to exactly what it renders today, so the token only takes effect once a host sets it.

0.30.0
  • Favourite stars now animate: favouriting fills the star from its base to its tip, and un-favouriting reverses the same wipe. The star on a <goma-multi-banners> sport card is now an actual control: it was drawn as decoration, so tapping it opened the event instead of favouriting it. The favourites empty state gains its illustration and its final copy.

    Three competition surfaces now switch geometry at the 768px breakpoint instead of painting one fixed size at every width: the collapsible competition top bar and the drill-in detail header grow on desktop, and the region accordion shrinks on mobile. Where its oversized title had been truncating region names that now fit.

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.

  • Integration caveats: fixes from a cross-widget review of every shipped widget from an integrator's point of view.

    Cross-cutting fixes:

    • @gomagaming/core's createWidgetElement() factory, and live-chat's and messenger's own element classes, now guard against importing a widget package in a non-browser context (SSR): the HTMLElement subclass was previously defined at module top level in every widget package, so importing any of them under server-side rendering threw a ReferenceError immediately.

    Per-package fixes:

    • favorites: added src/themeTokens.js and a :host stylesheet (previously the package had neither, so it silently rendered core's base theme instead of its documented dark-navy/cyan/amber theme).
    • suggested-bet. Fixed a leaked WAMP subscription: switching a mounted widget's dataSource away from rpc/subscribe never released the open topic.
    • filters-settings: fixed a dead :host{display:inline-block} CSS rule (the shared element factory's inline display:block always won), so the widget always rendered full-width instead of the intended compact inline trigger.
    • game-details: added a min-height fallback for the inert/no-eventId state in an unsized container.
    • betslip-sidebar, my-bets, competition-filter, suggested-bet, events-horizontal, top-competitions, top-competitions-pills, competitions-list, multi-banners, casino-games, sports-navigation-horizontal. Corrected themeTokens.js registries that had drifted from actual token usage, added missing goma:retry Events-table rows, and other small doc/registry corrections.
    • region-filter: added a missing typeof window !== 'undefined' guard on a window listener.
    • live-chat: corrected custom-elements.json's members array (missing theme/config accessors, and an inaccurate description).
    • sports-domain: corrected a stale code comment that seeded a documentation error in two Missions widgets.

    Three fixes (favorites, game-details, filters-settings) change real rendering and are flagged in docs/guide/integration-caveats.md as needing human visual verification before release.

  • 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
  • <goma-events-horizontal>, <goma-favorites> and <goma-competitions-list>: add a size prop (size attribute, 'default' | 'large', default 'default') that switches the whole event rail between the baseline card geometry and an enlarged one, and bring both geometries back onto the design's heights.

    Behaviour change. The default card is 8px shorter. The game card had drifted 8px taller than the design and has been brought back onto it: the main card is now 156px tall at size="default" (was 164px) and 188px at size="large" (was 196px). This is not opt-in and there is no flag to keep the old height: it changes the default rendering of every shipped embed the moment it is upgraded. A host will see each event card, loading skeleton, empty-state block and See All tile lose 8px of height, so an events rail gets 8px shorter per row and any surrounding layout that was sized around the old height (a fixed-height container, a hard-coded calc() offset, a screenshot-diff baseline) shifts by that much. Nothing is clipped (the content stack still fits inside the card at both sizes) and the design is the authority on the number.

    Behaviour change: every game card and every odds button is restyled. The card's internal geometry had drifted off the design frame in thirteen measured places, and all thirteen are corrected here. This is not opt-in, there is no flag to keep the old look, and it applies at both size variants: the design's size=default and size=large frames carry identical card chrome, so each value below is the same number in both.

    What a host actually sees on the game card:

    wasnow
    Odds row height48px40px
    Odds button padding8px sides / 4px top+bottom8px all round
    Odds button corner6px4px
    Card padding, top / bottom4px / 12px8px / 10px
    Breadcrumb row height24px16px
    Competition name weight300500
    Team name weight600700
    Team name line height20px16px
    Gap between the two team rows4px6px (default only)
    Market badge height14px16px
    Market badge side padding12px8px

    The visible result is a tighter, more deliberate card: the team block reads as one unit instead of three loosely spaced lines, the competition name is legible instead of hairline, and the odds row is a 40px band of 4px-cornered buttons rather than a 48px band of 6px-cornered ones. Nothing is clipped or truncated that was not clipped before, and no card's content reaches its painted edge at either size: the shortest remaining bottom gutter across every card type, size and state is 10px.

    The odds button is shared, so this restyle reaches further than the event feeds. RegularOdds is the only odds button in the product. Its 8px padding and 4px corner therefore also change the odds buttons inside <goma-game-details>, <goma-hero-card> and <goma-multi-banners>, which render it directly. Those widgets already sized the button at 40px, so only its padding and corner move; their own layouts are untouched. Any host screenshot baseline that includes an odds button anywhere will shift.

    The card corner stays on the 8px radius token. The frame reports 8.9376px, but that is the only non-integer value in the whole frame, the paddings (8/12/10/12), the row heights (16/40) and the odds button's own 4px corner are all exact integers, so it reads as a corner artefact of a resized instance rather than a designed value. A sub-pixel radius difference is not visible, and honouring it would mean carrying a matching arbitrary radius on the gradient-border wrapper of every card component to stop the border mis-rounding.

    The outright card is left on its own geometry. It is a different component with a different box (145 / 177px), no team rows, no market badge and no odds grid (it renders a single full-width action button, not RegularOdds) and none of the thirteen values above were measured against an outright frame. Changing it would be a guess, so it keeps the padding, breadcrumb and corner it has today.

    Where the 8px of drift came from. The odds row being 48px instead of 40px is the origin of the earlier 164-vs-156 card height: the card had been padded to fit oversized buttons. When the card was brought back to the design's 156px while the buttons stayed at 48px, the pre-live card's content ran the full height of the box and its bottom gutter went to zero. Correcting the row height hands that back: the pre-live card now carries a 15px gutter at default.

    Three follow-on adjustments come with it:

    • The secondary market card's inner column follows the card height by the same relationship as before, re-derived from the new padding: cardHeight − 46px (top padding 8 + bottom padding 10 + 2 borders + a 26px breadcrumb budget), so 112px → 110px at default and 144px → 142px at large. Its odds row is the last child of a fixed-height justify-between column, so the row keeps its bottom edge and simply becomes 8px shorter.
    • The suspended / no-odds block takes the odds row's new 40px height and its 4px corner, so the card does not change shape when a market closes. The pre-live card's copy also takes the same top margin as the odds grid it replaces, matching what the live card has always done.
    • The outright card keeps its own 145 / 177px geometry. It was already a separate, shorter box; the change narrows the gap to the main card rather than widening it.

    'large' is the design system's size=large variant of the game card and the secondary market card:

    • Main game card: team names 16px → 18px, the internal vertical gaps opened up (the design's 7px → 12px container gap and 6px → 18px gap between the two team rows), and the card 32px taller.
    • Secondary market card: market title 14px → 16px, team names 10px → 14px, and the same 32px height step so a mixed rail stays level.
    • Outright card and the See All tile take the same 32px step, which is what keeps a rail carrying several card types from going ragged. The design system has no dedicated large outright card; this is the consistent extrapolation.

    The loading skeleton and the feed's empty-state block take the same 32px step, so the placeholder a feed paints while its first dump is in flight is exactly as tall as the card that replaces it. Without that, a large feed rendered 156px skeletons and jumped 32px on every load: most visible on a teaser that reloads whenever a sport picker changes. Skeleton height and card height are now pinned to each other by tests in all three widgets.

    The loading skeleton mirrors the card's new chrome as well as its height (the same 8px/10px card padding and the same 40px, 4px-cornered odds placeholders) so the placeholder a feed paints while its first dump is in flight is the same shape as the card that replaces it, not just the same height. Tests pin both.

    size is set on the feed widget and threads down to the cards, the same shape defaultMarket and oddsFilter already use: the cards themselves gain a size prop and stay presentational. All three widgets render the event rail from byte-identical copies of the same component, so the prop lands on all three together; a root test now asserts those three copies stay identical, since a prop reaching one copy and not the others is silent when it happens.

    @gomagaming/core is republished because it compiles the single stylesheet adopted into every widget's shadow root from the workspace's sources: both card geometries' utilities live in that bundle, not in the widget packages.

  • Give <goma-multi-banners> an explicit per-entry data mode, let its banners array accept the shared promo-card record, and let each entry choose which market it promotes.

    Everything here is additive. No prop, event, payload or banner field was renamed, removed or given a new default, and every combination that was expressible before renders exactly as it did.

    • Per-entry dataSource ('prop' | 'rpc' | 'subscribe'), with a new element-level dataSource supplying the default. That default is 'subscribe', which (together with "an entry with no eventId is 'prop'" and "casino/info entries are always 'prop'") reproduces case for case the implicit behaviour the widget has always had. The mode was previously invisible and unoverridable; it is now declared and per entry, because the array is genuinely heterogeneous. The one newly-expressible combination is dataSource: 'prop' on an entry that also carries an eventId (impossible before, since an eventId always won outright) which lets a CMS store the real event id for navigation while pinning curated odds. 'rpc' is a one-shot snapshot; it releases the widget's topics only once every feed-backed entry is in 'rpc' mode and has resolved, since the shared subscription manager has no per-topic release.

    • The shared promo-card record. A type: 'sport' entry is now the same GomaPromoCard the single-card promo widgets take, so one CMS shape feeds all of them. The canonical spellings are accepted as aliases alongside the banner ones: image/backgroundImage, competitionName/competition, sportIconUrl/sportIcon, cta: { label, url }/ctaLabel+ctaUrl, outcomes[].outcomeId/id, .name/.outcomeName/.label, .odd/.odds, .offerId/.bettingOfferId. With the banner spelling winning where both are present. imageAlt, competitionId, fixtureLabel and flagUrl are newly read; omitting them behaves as before.

    • Per-entry defaultMarket (a bettingTypeId; marketId accepted as an alias) chooses which market a banner promotes, so one rail can lead with 1X2 on one card and Over/Under on the next. A market the event does not carry falls back to the leading market, which is this widget's shipped behaviour.

    Behaviour change to be aware of on a live feed: with no selector the widget used to take markets[0]: by position. The matches-aggregator delivers a market suspend/re-offer as a DELETE followed by a CREATE and appends the re-created market to the tail of the list, so a live banner reshuffled its own headline market as markets came and went. The leading market is now derived from the feed's canonical main-market ranking instead. Which market renders can therefore differ from before on a live feed: toward the stable, intended one. Host-fed entries have no such ranking and no such churn, so there the leading market is still the first one listed.

    The gzip/brotli bundle budgets move from 11 kB / 9.5 kB to 12.5 kB / 11 kB to cover the new mode resolution, alias readers and market selection (11.94 kB gzipped as shipped).

  • Quieten the paging arrows on both scrolling strips. The solid disc with a drop shadow read as a primary control competing with the content beneath it; the arrow is chrome, so it is now a translucent scrim (a color-mix wash with a blurred backdrop and no shadow) and the caret stays the same token so it is legible against it.

    Both widgets derive scrim and caret from --goma-textPrimary: one token for both halves, so a host theming the widget cannot pull them apart. The <goma-sports-navigation-horizontal> arrow previously mixed a background token (--goma-buttonBackgroundTertiary) for the scrim with a text token for the caret; that token is no longer read by the widget.

    Both widgets also now carry a plain rgba() declaration ahead of the color-mix() one, so engines without color-mix() still paint a scrim instead of a fully transparent disc. <goma-sports-navigation-horizontal>'s arrow moved off an inline style object into src/styles/nav.css (fed to the shadow root via extraStyles) to make that two-declaration fallback expressible at all.

0.26.0
  • Stop publishing source maps and package source.

    dist/*.js.map was emitted by the shared Vite preset and swept into every tarball by files: ["dist/", "src/"], with the original source inlined in sourcesContent. src/ shipped the same source again, in the clear. Both are one URL away from a browser once a package is served from a CDN, so the preset now builds with sourcemap: false and files no longer carries src/.

    @gomagaming/core keeps the two paths its export map actually points at: src/styles/main.css and src/types/index.d.ts. Its tarball drops from 1741 files to 576, which also brings it back under the 900-file limit a per-request unpacker can handle.

    Deep imports of @gomagaming/<pkg>/src/* from an installed package no longer resolve. Nothing documented used them; imports inside the monorepo are unaffected.

0.25.0
  • A type: 'sport' banner can now carry an event key holding one GomaHostEvent: the same host-fed event shape <goma-hero-card> accepts via hostEvent. Set it and the banner shows a real, ticking live score and clock instead of the static 0 - 0 placeholder that inline fields alone could never produce, plus resolves teams, competition, start time and up to several markets in one object (the card still renders the first one). A CMS driving both widgets can now write one event shape for each.

    Precedence across a sport banner's three data modes: eventId (live subscription) wins over event (host-fed snapshot), which wins over the existing flat inline fields. Existing flat-field banners are unaffected.

0.19.0
  • Fix place-bet rejected with "Invalid or missing Betting Type Id on placed bet"

    Every leg was serialised with bettingTypeId: null, so the provider rejected the whole slip. The odds card captured the field correctly: it was lost in transit to the betslip widget.

    Tapping odds does two things in the same tick: it writes a full selection to the source widget's own store, then dispatches goma:outcome-select. That event is synchronous, so a betslip widget's window listener inserted its row from the event detail alone, which carried no bettingTypeId, a tick before the complete shared-storage snapshot arrived. The inbound sync then skipped the row because its outcomeId was already present, so the partial first writer won permanently.

    Fixed on both sides:

    • goma:outcome-select now carries bettingTypeId and eventPartId (it already carried outcomeId, previously undocumented). Additive: existing listeners are unaffected.
    • The shared-betslip inbound sync backfills identity fields that are missing from the local copy, repairing any selection that entered through a lossier path. Absent, undefined and null all count as missing, so the repair does not depend on how a producer spells "unknown"; a null on the wire never overwrites an id already held locally. Live fields (odd / isAvailable / isLive) stay owned by each widget's own feed and are never backfilled.
    • When a goma:outcome-select detail arrives without a bettingTypeId (a host driving the betslip from its own UI, where no richer snapshot exists to repair the row) the betslip logs a console.warn naming the outcome, once per outcome. The first symptom used to be the provider's HTTP 409, which names neither the leg nor the missing field.
    • <goma-multi-banners> inline sport banners forward bettingTypeId / eventPartId from the banner to the odds button. The eventId path already resolved them from the feed; inline banners had no way to supply them, so their legs were rejected at place-bet.

    Affects every odds button in every widget: they all render the shared RegularOdds component.

0.16.0
  • GomaPulse UX review (round 3):

    • Sport icons: dedicated glyphs for the eSports titles (FIFA, NBA2k, Counter-Strike, DOTA 2, Overwatch) plus a shared outline-controller default for every other eSports category, replacing the legacy generic gamepad; corrected Beach Volleyball and Hurling glyphs.
    • Casino card: nav-arrow disc uses backgroundBorder; restyled Play pill (8px radius, 14px, weight 600, buttonTextPrimary, outline play icon); title gains a soft white glow on hover; added the design's top-edge line + radial top glow (driven by highlightPrimary); removed the hover lift.
    • Multi-banners: favourite star now matches the app's outline star.
    • Country flag ring (shared): uses the theme-fixed allWhite token so the ring stays white under any consumer theme.
    • tabPill pressed state: added the 80%-opacity pressed treatment on the My Bets and Game Details pills.
    • Sports selector: the widget host now carries a default 8px corner radius.
    • Suggested-bet leg: sport-icon colour uses iconPrimary.
0.15.2
  • GomaPulse UX-review round 2: visual/token fixes:

    • core: official flat sports-icon set (46 mainstream/virtual/eSports-category glyphs across dark + light modes, keyed by sport id). Inherited app-wide via SbIcon; new casino_games_play i18n key (en/fr/pt).
    • casino-games: official flame glyph, hover "Play" overlay on tiles, title→tiles / tiles→dots spacing.
    • multi-banners: header now self-resolves the sport glyph from the event's sport id, renders the country flag via the shared CountryFlag component (new flagBaseUrl option, defaults to the OM flag CDN), and gains a 1px card border.
    • sports-navigation-horizontal: the rounded container now clips its content so the 8px corners render.
    • suggested-bet: leg sport icons use the flat bundled icon set.
    • game-details / missions-hub / sports-domain (My Bets): tabPill hover + pressed states (violet ring); My Bets ticket icon sizing (12px sport + flag) and centered totals summary.
0.15.0
  • UI fixes from the a separate change design review (Sports Pages, items 1.1–1.19):

    • Suspended odds pill text uses inputTextTitleDisabled (legible on lightened themes) and game-card team/player names render at 16px.
    • Brand font: Open Sans is bundled and registered via the document.fonts API so widgets paint the intended typeface inside the shadow root.
    • Game-card market pill colours corrected (live separatorLine, prelive highlightPrimary); sports-navigation tile sizing + rounded container; multi-banners overlay gradient; casino-games flame colour + tile gradient border; filters-settings radio-row divider.
    • competition-filter: edge-trigger icon size, pill padding, and hover/pressed states.
    • competitions-list: inset row divider (original colour) + corrected accordion caret orientation.
    • game-details: header sport-icon/flag + rounded top corners; market-tab horizontal paging arrows with click-to-centre; long-market "See All" cap; corrected collapse-caret orientation.
    • top-competitions: white sport icon + featured-card neon divider moved to the card bottom and thickened.
    • suggested-bet: Bet / Total Odds / Winnings summary centre-aligned.
0.13.0
  • <goma-multi-banners>: add an opt-in responsive multi-card peek layout. Set slidesPerViewMobile / slidesPerViewDesktop to show several full cards side by side with a fixed peek (~24px) of the next card bleeding at the edge; the mobile/desktop switch is measured against the widget's own container width (multiBreakpoint, default 768px). Dots, arrows and swipe advance a full logical page. On desktop the prev/next arrows now hover-reveal on the section (no layout shift); on touch they stay hidden and swipe + dots drive navigation. Leaving both slidesPerView* unset keeps the original single centred-card layout unchanged. No new theme tokens.
0.9.1
  • Support host-driven sessions and richer hero/betslip presentation, surfaced while building the GomaPulse client POC:

    core

    • The injected sportsbookConfig is now reactive and stays in sync when el.config is assigned after mount, so post-mount connection keys that aren't mirrored into _state (playerApiBaseUrl, betsApiBaseUrl, gameLaunchBaseUrl, apiBaseUrl) now reach inject('sportsbookConfig') consumers (useSession / useCeLogin / useMyBets). Previously the session/login flow only worked when config was set before the element connected.
    • The Vue mount wrapper gets height: 100% so a host that sizes the element (definite height / flex item) propagates down to the app root: fixes the sidebar betslip's internal selections scroll.

    sports-domain / betslip-sidebar / betslip-floating

    • New myBetsTab prop ('auto' default | 'always' | 'never') controls the MyBets header tab independent of sessionId. The MyBets empty pane now reuses the host emptyStateImage illustration, and its Open/Closed/Won sub-tabs are hidden for anonymous embeds.
    • New requireSession prop (default false): when true, the Place Bet button stays disabled until a sessionId is set (with a "Sign in to place a bet" hint). Only enforced in api mode; a no-op in event mode.

    sports-domain

    • Fixed selected-odds text colour: the resting text-textOdds is no longer a static class, so the selected buttonTextPrimary actually wins. Previously a selected odds button kept its resting (light) text on a light buttonBackgroundPrimary, leaving the label unreadable. Affects every widget that renders RegularOdds.
    • Fixed swapped live-card backgrounds: the main live card now uses backgroundDrop and its secondary market cards use backgroundSecondary (previously reversed). Affects every widget that renders live game cards.

    multi-banners

    • New centeredSlides and centeredSlidesBounds props (first banner pins left, last pins right, middle centres); pagination dots now render one-per-banner driven by the snap index; and the carousel stays on the first banner until the user interacts.
    • Sport banner odds row now adapts its column count to the number of outcomes (1/2/3) so a 2-way market fills the card width instead of leaving an empty third column.
0.9.0
  • New widget <goma-multi-banners>. A swipeable carousel of injectable banner cards, three types in one widget: sport events, casino game promos, and info banners.

    All data is host-fed via the banners array (no CMS). For a sport banner, setting eventId (live or prelive) makes the widget resolve teams, start time, score and odds itself; without it the host supplies those fields inline. The background image is always host-fed.

    • Sport cards adapt to event state: live cards show the running clock/part and score (home - away), prelive cards show the kickoff date + time.
    • Carousel chrome: prev/next arrows, pagination dots, swipe/drag, opt-in autoplay (off by default). The track is clipped + isolated so nothing bleeds over neighbouring widgets.
    • Emits goma:banner-select (every banner), goma:event-navigate (sport-card body tap → game-details), and goma:outcome-select (sport odds tap → betslip, same shape as events-horizontal).
    • No new theme tokens; all colours map to existing --goma-* tokens.

    @gomagaming/sports-domain additions (additive, no change to existing paths):

    • New action group sports.bannerEvents (createBannerEventsActions): a multi-event subscription manager that accumulates events into bettingStore.events (skipStoreCleanup) so several event cards keyed by id coexist with live odds, unlike the singular-game matchDetails path.
    • Public exports for useGameScoresTallOdds and useGamePartsAndTimesTallOdds so other widgets can render live scores/clocks from a store event.