Skip to content

<goma-top-competitions-pills> ​

Horizontal single-selection pill row of top competitions (tournaments): a region flag (with a 🌐 fallback glyph) + competition name per pill, the active pill wearing a highlightPrimary ring. The horizontal-pill sibling of <goma-top-competitions> (vertical list) and <goma-competition-filter> (host-fed bar): same data + theming family, different presentation.

Live demo ​

Open in the playground →: toggle Top competitions (pills) in the widget rail.

Element tag ​

html
<goma-top-competitions-pills></goma-top-competitions-pills>

Installation ​

@gomagaming/top-competitions-pills is a scoped private package. See Installation for the one-time .npmrc setup.

bash
npm install @gomagaming/top-competitions-pills

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

bash
npm install vue pinia vue-router vue-i18n @vueuse/core autobahn-browser

Configuration ​

js
const el = document.getElementById('tcp')
el.config = {
  socketUrl: 'wss://sportsapi.example.com/v2',
  socketRealm: 'www.example.com',
  apiBaseUrl: 'https://api.example.com',
  ucsOperatorId: '2838',
  locale: 'en',
}
KeyTypeRequiredDescription
socketUrlstringWhen dataSource !== 'prop'WAMP socket URL. The widget hits popularTournaments over this transport.
socketRealmstringWhen dataSource !== 'prop'WAMP realm.
apiBaseUrlstringAdvisoryREST base URL. Not read by this widget's own data path, but every widget carries it as a standard config key: debug: true warns if it's missing.
ucsOperatorIdstringNoOperator ID. Tracked as part of the common config surface; not read directly by this widget's own composable.
localestringNoi18n locale. Defaults to 'en'.
themeobjectNoCSS variable overrides: see Theming.
messagesobjectNoi18n overrides per locale.
networkobjectNoNetwork policy; network.rpcTimeoutMs bounds each try of the getTopCompetitions call (up to 3 tries, with a 2 s and a 5 s wait between them, so the worst case is 3 x rpcTimeoutMs + 7 s, about 52 s by default) and, after a sport change, the wait for a socket that is down.
debugbooleanNoVerbose console logging (RPC failures, followSportPicker rewrites).

Data sources ​

'prop': host-fed ​

The widget renders whatever the host writes to el.competitions.

js
el.dataSource = 'prop'
el.competitions = [
  { id: 106, name: 'UEFA Champions League', sportId: 1, venueId: 9  },
  { id: 102, name: 'La Liga',                sportId: 1, venueId: 11 },
  { id: 110, name: 'Primeira Liga',          sportId: 1, venueId: 16 },
]

'rpc': one-shot WAMP call (default) ​

The widget calls popularTournaments once the WAMP connection is up. sportId is optional on the backend:

  • Cross-sport (default, sportId: null): omit sportId. Backend returns top tournaments aggregated across every sport.
  • Sport-scoped (sportId: 1 etc.): pass sportId. Backend returns the top N for that sport.

Re-runs whenever sportId or numberOfCompetitions changes. On a sport change the widget shows its skeleton, and competitions reads null, until the new sport answers, unless its list is already loaded. An answer for an earlier sport is ignored. If no answer lands, the row ends empty.

js
// Cross-sport (default: no sportId)
el.dataSource = 'rpc'
el.config = { /* socket creds */ }

// Sport-scoped, paired with a prelive-only feed
el.dataSource = 'rpc'
el.sportId = 1
el.numberOfCompetitions = 12
el.contextLiveStatus = 'NOT_LIVE'
el.config = { /* socket creds */ }

Which competitions are listed ​

A pill is only shown when its competition has games of the kind the paired feed lists, so no pill opens on an empty feed. Set contextLiveStatus to the live status of that feed:

contextLiveStatusA competition is shown when
'BOTH' (default)numberOfUpcomingMatches + numberOfLiveEvents > 0
'NOT_LIVE'numberOfUpcomingMatches > 0
'LIVE'numberOfLiveEvents > 0
'ANY'always: the filter is off
  • A competition with only outright markets has no games, so it is not shown, except with 'ANY'. Use 'ANY' when a pill leads to a page that also lists outrights.
  • The rule reads the count fields of each popularTournaments record. A record that leaves out every field the status reads is kept: an absent count is unknown, not zero.
  • The RPC is asked for more competitions than numberOfCompetitions and the list is cut after filtering, so the row stays full.
  • Changing contextLiveStatus re-filters the loaded list without a new RPC call.
  • A selectedCompetitionId that names a competition left out by this rule is replaced by the first pill when autoSelectFirst is on, the same as an id that is not in the list.
  • 'prop' mode is not filtered: a host-fed list is shown as given.

'subscribe': live WAMP subscription (placeholder) ​

Note: the backend topic for live top-competition updates is not yet available. The widget accepts dataSource: 'subscribe' but internally falls back to the one-shot RPC (warns once via console.warn). Once the topic ships, the fallback flips automatically.

Props / attributes ​

PropertyAttributeDefaultDescription
dataSourcedata-source'rpc''prop' / 'rpc' / 'subscribe'.
sportIdsport-idnullSport id. null / missing (default) → cross-sport. A real number → sport-scoped.
numberOfCompetitionsnumber-of-competitions12Max number of pills shown. The RPC is asked for more, see Which competitions are listed.
contextLiveStatuscontext-live-status'BOTH''BOTH' / 'NOT_LIVE' / 'LIVE' / 'ANY': which events the paired feed lists. A competition with no games of that kind is not shown; 'ANY' keeps every competition. Not applied in 'prop' mode.
competitions-[]Array of GomaCompetition objects. Populated by the WAMP composable when dataSource !== 'prop', and null during a sport change until the new sport answers, unless its list is already loaded.
selectedCompetitionIdselected-competition-idnullId of the currently-selected pill. Two-way: taps update this value.
skeletonCountskeleton-count4Number of placeholder pills shown until the first competitions assignment, and again on a sport change with a socket source.
flagBaseUrlflag-base-url'https://static.glastcoper.com/omfe-widgets/s/assets/1.10.2/om1/icons/flag/'Base URL for country-flag PNGs. Flags resolve to ${flagBaseUrl}{venueId}.png; a 🌐 glyph shows on a missing/broken PNG.
iconBaseUrlicon-base-url''Base URL for sport-icon SVGs (only used when showSportIcon is on). Sport icons resolve to ${iconBaseUrl}/icons/sports/dark_mode/{sportId}.svg.
showSportIconshow-sport-iconfalsePrepend the sport icon before the flag on each pill.
autoSelectFirstauto-select-firsttrueOnce a list arrives, pre-select (and emit) a pill so a paired feed always has a scope. Honours a valid selectedCompetitionId first, else the first pill.
followSportPickerfollow-sport-pickertrueWhen on, rewrites sportId whenever any sports-navigation widget on the page emits goma:sport-select, re-fetching the list. Matches the default on <goma-region-filter>, <goma-competitions-list> and <goma-events-horizontal>. Set follow-sport-picker="false" for a standalone pill row.
sportPickerSelectorsport-picker-selectornullOptional CSS selector to target a specific sport picker when followSportPicker is on.
resetCompetitionOnSportChangereset-competition-on-sport-changetrueClear selectedCompetitionId whenever sportId changes, since competitions are sport-scoped. Set false when one navigation action picks a sport AND a competition: see Integration Caveats.

GomaCompetition shape ​

ts
interface GomaCompetition {
  id: number | string
  name: string
  translatedName?: string
  shortTranslatedName?: string
  sportId: number
  sportName?: string
  venueId?: number      // country/region id: used to resolve the flag PNG
  venueName?: string
}

Events ​

StatusEventDetailEmitted when
Canonicalready{}Widget mounted.
Canonicalgoma:connection{ state, … }Connection state transition.
Canonicalgoma:error{ message, code, component? }Two paths. Connection layer: the popularTournaments WAMP adapter exhausts its 5 retry attempts (code: 'API_INIT_ERROR'), alongside goma:connection { state: 'failed' }. Widget root scope: an error raised in the widget's own outermost component (its setup, its render, its lifecycle hooks or its watchers), where code is the error's name, e.g. 'TypeError' ('RUNTIME_ERROR' when it has none). Errors raised inside the widget's child components do not reach this event: the in-shadow error boundary catches those and shows the retry banner instead, emitting only goma:retry.
Canonicalgoma:retry{}User clicked Retry on the render-error banner.
Canonicalgoma:competition-select{ competitionId, competitionName, competitionSportId, previousCompetitionId }User taps a pill (or activates it via Enter / Space), or autoSelectFirst picks the first pill on load. The widget also mutates selectedCompetitionId so the pill flips immediately. Also emitted when the host sets the property (a menu link, a deep link, a refresh restore), so the whole follow* chain keeps up without extra wiring; never on mount, and never when re-assigned the value already held. See Driving a selection from the host.
Canonicalgoma:feed-load-failed{ source: 'top-competitions', sportId }The top competitions call failed on all 3 tries (each bounded by network.rpcTimeoutMs, 15 s by default, with a 2 s and a 5 s wait between them). The row ends empty. Informational: use it to show your own message or to reload. sportId is null when the row is cross-sport.

goma:competition-select, not goma:top-competition-select (the event its vertical sibling <goma-top-competitions> emits), is deliberate: this payload matches <goma-competition-filter> and <goma-competitions-list>, so any downstream <goma-events-horizontal> with followCompetitionPicker re-scopes its feed with no extra wiring.

Integrates with ​

Listens for goma:sport-select, gated by followSportPicker (default true) with an optional sportPickerSelector to target one specific picker. Commonly pairs with <goma-sports-navigation-horizontal> / <goma-sports-navigation-dialog> upstream (feeding sportId), and emits goma:competition-select for a downstream <goma-events-horizontal followCompetitionPicker> to re-scope its feed. See Cross-widget wiring for the full rewrite behavior and emitter/listener matrix.

This widget reads and writes no shared-state envelope (goma:betslip:v4, goma:favorites:v1, etc.): see Shared state for the widgets that do.

Empty / loading / error states ​

  • Loading (skeleton): until the widget's first competitions assignment (even an empty array), the row renders skeletonCount (default 4) pulsing placeholder pills instead of real ones.
  • Loaded, zero results: once competitions holds an array, the skeleton is gone until the next sport change, even if the array is empty. An empty array renders an empty row: there is no dedicated "no competitions" message.
  • RPC failure: a failed popularTournaments try (or a timeout past the configured network.rpcTimeoutMs) is logged via console.error and tried again, up to 3 tries in all, with a 2 s wait before the second and 5 s before the third. The skeleton stays while a try is left. When all 3 failed, the widget keeps its rows on the same sport and otherwise ends empty (after a sport change or on a first load), and goma:feed-load-failed is emitted once.
  • Render error: a Vue render error is caught by the widget's own error boundary and shown as an inline alert banner with a Retry button (emits goma:retry) and a dismiss (×); the banner auto-clears after 8 seconds.

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
pillHeight--goma-tcpPillHeightat least 40pxPill height. Set, the pill is exactly this tall.
pillPaddingLeft--goma-tcpPillPaddingLeft12pxPill left padding.
pillPaddingRight--goma-tcpPillPaddingRight16pxPill right padding.
pillPaddingY--goma-tcpPillPaddingY8pxPill vertical padding.
pillGap--goma-tcpPillGap8pxGap between the flag and the label.
pillLineHeight--goma-tcpPillLineHeight1.25rem (20px)Label line height.
flagSize--goma-tcpFlagSize20pxCountry-flag disc size.

Theming ​

Override via el.theme = { … }.

TokenElement
--goma-backgroundPrimaryOuter container + flag disc background
--goma-backgroundCardsPill background
--goma-separatorLineIdle pill border
--goma-textPrimaryPill label
--goma-textSecondaryFlag fallback glyph
--goma-iconSecondarySport icon colour (when showSportIcon)
--goma-highlightPrimarySelected pill ring
--goma-alertErrorRender-error banner background
--goma-highlightPrimaryContrastRetry button text on the render-error banner

No new theme tokens: the pill row paints existing --goma-* tokens only.

i18n / message overrides ​

Keyen defaultUsed for
top_competitionsTop CompetitionsThe pill row's aria-label.
retryRetryThe retry button inside the render-error banner.

Override via el.config = { messages: { en: { top_competitions: '...' } } }.

Embedding examples ​

Vanilla HTML ​

html
<goma-top-competitions-pills id="tcp" sport-id="1" number-of-competitions="12"></goma-top-competitions-pills>
<script type="module">
  import '@gomagaming/top-competitions-pills'
  const el = document.getElementById('tcp')
  el.config = {
    socketUrl: 'wss://sportsapi.example.com/v2',
    socketRealm: 'www.example.com',
    apiBaseUrl: 'https://api.example.com',
    ucsOperatorId: '2838',
    locale: 'en',
  }
  el.addEventListener('goma:competition-select', (e) => {
    console.log('scope →', e.detail.competitionId, e.detail.competitionName)
  })
</script>

Paired with a feed (zero-wiring filter) ​

html
<goma-top-competitions-pills sport-id="1"></goma-top-competitions-pills>
<goma-events-horizontal follow-competition-picker></goma-events-horizontal>
<script type="module">
  import '@gomagaming/top-competitions-pills'
  import '@gomagaming/events-horizontal'
  // Both share the same config; each pill tap re-scopes the feed.
</script>

Host-fed ​

html
<goma-top-competitions-pills id="tcp" data-source="prop"></goma-top-competitions-pills>
<script type="module">
  import '@gomagaming/top-competitions-pills'
  const el = document.getElementById('tcp')
  el.competitions = await fetch('/api/top-competitions').then((r) => r.json())
</script>

React ​

jsx
import '@gomagaming/top-competitions-pills'
import { useEffect, useRef } from 'react'

function TopCompetitionsPills({ config, sportId = 1 }) {
  const ref = useRef(null)
  useEffect(() => {
    if (!ref.current) return
    ref.current.sportId = sportId
    ref.current.config = config
  }, [config, sportId])
  return (
    <goma-top-competitions-pills
      ref={ref}
      onGoma:competition-select={(e) => console.log(e.detail)}
    />
  )
}

Vue 3 ​

vue
<script setup>
import '@gomagaming/top-competitions-pills'
import { ref, onMounted } from 'vue'

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

onMounted(() => {
  el.value.sportId = 1
  el.value.config = props.config
})
</script>

<template>
  <goma-top-competitions-pills ref="el" @goma:competition-select="onCompetitionSelect" />
</template>

Accessibility ​

  • role="tablist" on the row, role="tab" + aria-selected on each pill.
  • Keyboard parity: Enter / Space activate selection; pills are natively focusable <button>s.
  • Each pill's aria-label is the competition name; the row's aria-label is the localised "Top Competitions".
  • Comfortable tap targets (≥40 px pill height).

What's deferred ​

  • Real subscribe path. Backend topic for live top-competition updates not yet known; dataSource: 'subscribe' falls back to one-shot RPC.
  • Featured pill variant. A banner-image pill (like the vertical list's highlightedImages) is out of scope for v1: plain pills only.

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-alertError/--goma-highlightPrimaryContrast (used by the render-error banner) are now registered in themeTokens.js and the Theming table above.

  • Events: goma:retry was emitted but had no dedicated Events-table row (the goma:error row's own text referenced it with nothing following); added.

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

  • Setting sport and competition together: a sportId change clears selectedCompetitionId, because a competition from the previous sport is not in the new sport's list. That clearing runs in a watcher, which observes state after both of your writes have landed, so with it on no write order lets a menu link set a sport and a competition at once. Set resetCompetitionOnSportChange (attribute reset-competition-on-sport-change) to false and clear the competition yourself for sport-only navigation. See Driving a selection from the host.

  • 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
  • The competition lists now stay on the selected sport. An answer for a sport the player has already left is ignored, so it can no longer replace the list on screen or select another sport's competition. During a sport change the widgets show their skeleton in place of the old rows, and competitions reads null until the new sport answers. If no answer lands within network.rpcTimeoutMs, the list ends empty and not on the previous sport. This also holds in 'subscribe' mode on <goma-competitions-list>, and while the socket is down, where the list loads when the socket comes back. A pick back to a sport whose list is already loaded shows that list again at once, also while the socket is down. A failed first call ends empty too, not on a skeleton. A 'prop' list is not affected.

  • The event lists and the competition lists now wait for a slow or failing provider before they show an empty state. An event list keeps its loading state for about 55 s, long enough for the connection layer to ask for the data again, and the watchdog reports a failure only for a topic that was subscribed. The competition, top competition and region calls make up to 3 tries in 'rpc' mode (top competitions also in its 'subscribe' fallback), with a wait of 2 s before the second and 5 s before the third. network.rpcTimeoutMs bounds each try. A provider error now counts as a failed try: getCompetitions, getTopCompetitions and getAllLocations reject instead of resolving with nothing. A late answer of an earlier try that fills the list ends the tries. When every try has failed, the widget shows its empty state (the region filter drops its skeleton) and emits the new informational event goma:feed-load-failed ({ source, sportId?, topic? }), so a host can show its own message or reload. A sport change during the wait cancels the old tries, and the region call now ignores an answer for a sport the player has already left. The connection layer only gains a comment that links its retry timings to the event-list wait.

    The event list waits 55 s only after a request for data was sent, and 15 s when nothing could be requested. Load more now waits the same 55 s before its spinner clears, and the match page (game details) waits for the retries before it shows its not-found or empty state.

    The suggested bet card (dataSource: 'recsys') waits up to 55 s for the first answer once it has subscribed to the recommended events, instead of showing its empty state after 15 s. After an answer that still leaves no priced combo, or when no subscribe could be sent, it gives up after 15 s.

0.37.4
  • Top competitions lists only show competitions that have games. The new contextLiveStatus prop ('BOTH' by default, or 'NOT_LIVE' / 'LIVE') says which events the paired feed lists, and a competition with none of them is left out, so a pill or row never opens on an empty feed. Outright-only competitions are no longer listed by default; contextLiveStatus: 'ANY' turns the filter off and lists them again. A competition whose record carries none of the count fields the status reads is kept: an absent count is unknown, not zero. The list still fills numberOfCompetitions: the RPC is asked for more and the list is cut after filtering. A competition fetched again now gets its new counts instead of keeping the first ones. 'prop' mode is not filtered. competitionHasGames is exported from @gomagaming/sports-domain.
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
  • <goma-top-competitions-pills> gains an appearance object (structure only): pillHeight, pillPaddingLeft, pillPaddingRight, pillPaddingY, pillGap, pillLineHeight and flagSize. Unset, each keeps the current value (at least 40px tall, 12/16px sides, 8px gap, 20px flag), so the default look is unchanged. @gomagaming/core is bumped for the new utilities in its compiled sheet.
0.35.0
  • Every picker now announces a host-driven selection, not just a tap

    Setting a picker's selection from the page (el.selectedSportId = '8' for a menu entry, a deep link, state restored after a refresh) used to change the highlighted item silently, because the event was emitted from inside the click handler. Anything wired up with a follow* prop kept its previous filter, so the highlighted pill and the data underneath it disagreed. The only workaround was to forge the widget's own event on window, which breaks as soon as a *Selector prop is set.

    All seven pickers now emit from a watcher on the selection itself, so a tap, an internal reset and a host write are indistinguishable: <goma-region-filter>, <goma-top-competitions>, <goma-top-competitions-pills>, <goma-competitions-list>, <goma-competition-filter>, <goma-sports-navigation-horizontal> and <goma-sports-navigation-dialog>. @gomagaming/core gains the shared useSelectionEmit composable they all use, so a picker added later inherits the behaviour rather than reimplementing it.

    Three rules hold across all of them. Nothing is emitted on mount, so a value seeded from markup (<goma-region-filter selected-region-id="19">) opens on that scope without firing the chain during boot. Re-assigning the value already held emits nothing. And ids are the contract while names are best-effort: a selection applied before the widget's list has loaded still emits immediately with the right id and null descriptors, so deep links never have to wait for data.

    Two deliberate exceptions: <goma-competitions-list> stays silent when the selection is CLEARED, since closing its drill-in detail has its own event and an empty selection would scope a following feed to nothing; and <goma-sports-navigation-dialog> in showApplyButton mode treats the selection as pending, emitting when Apply is pressed.

    Two behaviour changes come with the single emit point. Re-tapping the item already active no longer emits (it is not a change): in the dialog that still closes it, it just announces nothing. And <goma-top-competitions>'s isFeatured is now derived from highlightedImages and the active variant rather than passed in from the tap, so a host write reports it as truthfully as a click.

    <goma-top-competitions-pills> also gains resetCompetitionOnSportChange (default true), matching resetRegionOnSportChange on <goma-region-filter>: it clears the selection on a sportId change, and with it on no write order lets one navigation action set a sport and a competition together.

  • Review fixes: a throwing property rescue, a split event sentinel, a duplicated rule

    The pre-upgrade rescue could stop a widget mounting. delete on a non-configurable own property throws, and module code is always strict, so a host that used Object.defineProperty(el, 'config', { value }) before the widget loaded (legal, and configurable defaults to false) made connectedCallback throw. The element never mounted at all: an uncaught TypeError and no widget, which is worse than the silent drop the rescue exists to prevent, and it lands in the same window. Such a property can never be rescued (the own definition shadows the accessor for the element's life), so it is now skipped with a warning naming the key, and the rest of the element's properties still apply.

    goma:competition-select carried two "unknown" sentinels.top-competitions-pills described an unresolved name as '' while competitions-list and top-competitions used null. Three widgets emit that one event and a host wires a single listener for it, so the difference landed on every consumer. null throughout now.

    isFeatured was derived in two places. The predicate lived in both useCompetitionSelection and TopCompetitionsList.vue. They agreed, but two copies agreeing is not the same as agreeing: if one moved, the payload would claim a rendering the user never saw. The exact failure deriving it was meant to prevent. The composable now exports the predicate and the template renders from it, so agreement is structural.

    <goma-search> landed on main while this was open and emits the same event with the old '', so it is aligned here too: otherwise the split it fixes would return the moment this merges.

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

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

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

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

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

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

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

0.28.0
  • Event-contract audit: six fixes across shipped widgets. Two of them change behaviour for existing embeds: read the last two entries before upgrading.

    The outright card's competition star now has a login gate: the star on OutrightRegularOdds (shown for _type === 'TOURNAMENT' events, and so in <goma-events-horizontal>, <goma-favorites> and <goma-competitions-list>) was the only star in the family that skipped guardedFavoriteToggle. Tapped while signed out it wrote and emitted anyway, contradicting the promise in the cross-widget wiring guide that a signed-out star does not write. It now emits goma:login-required when signed out, and writes the shared goma:favorites:v1 envelope (instead of reading a widget-local store slice) when signed in, so it agrees with every other competition star on the page. Its goma:favorite-toggle payload gains type and id and drops eventId, which held a competition id: a host resolving that value as an event id was looking up the wrong entity. type: 'competition' + id carry the same information under honest names, and match what the other competition stars have always emitted.

    <goma-filters-settings> now moves the feed. Applying a time window emitted only goma:filters-apply, which no feed widget listens for, so the mobile filter funnel left every sibling feed exactly where it was, while the identical window picked in <goma-time-filter> moved it. applyFilters() now also emits goma:time-select ({ timeValue, timeLabel, previousTimeValue }, the same payload <goma-time-filter> emits) when the window actually changed, immediately before the unchanged goma:filters-apply summary event.

    <goma-game-details> gains eventNavigateSelector (event-navigate-selector, default ''). goma:event-navigate has about a dozen emitters and followEventNavigate is on by default, so two game-details panels on one page both jumped to every card tap anywhere. The new prop is the same CSS-selector scope guard <goma-events-horizontal> and the picker followers already carry, matched against the event's retargeted host. Empty (the default) keeps today's follow-everything behaviour.

    goma:outcome-deselect no longer puts an outcome id in bettingOfferId. Removing a row from the betslip panel emitted { bettingOfferId: <outcomeId> }, while the card-side odds button sends a real offer id in that field: a host deduping on detail.bettingOfferId got mismatched keys from the two surfaces. The panel now emits { bettingOfferId: null, outcomeId }, the card emits bettingOfferId: null rather than undefined when there is no offer, and the GomaEventMap type widens to { bettingOfferId?: string | null; outcomeId: string }. Dedupe on outcomeId. Both surfaces always carry it.

    Behaviour change: <goma-hero-card>'s favourite star now writes shared state. The star was local ref state emitting goma:favorite-select, an event name no other widget uses, so a hero card and an events-horizontal card for the same match disagreed on the same page. It now goes through guardedFavoriteToggle and the shared goma:favorites:v1 envelope, and emits the canonical goma:favorite-toggle. If your page mounts <goma-favorites> or otherwise persists that envelope, a tap here is now persisted where it previously was not. The isFavorite prop becomes a seed (mirrored into the envelope once, while signed in) rather than a parallel source of truth. goma:favorite-select is deprecated: still emitted alongside goma:favorite-toggle for one release so existing listeners keep working; migrate to goma:favorite-toggle, it will be removed next.

    Behaviour change: <goma-top-competitions-pills> now follows the sport picker by default. followSportPicker flips from false to true, matching <goma-region-filter>, <goma-competitions-list> and <goma-events-horizontal>, so swapping one of those for this widget no longer silently loses sport-following. An existing embed that mounts this widget alongside a sports picker and relied on the pill row staying put will now re-fetch on every sport change. Set follow-sport-picker="false" (or el.followSportPicker = false) to keep the old behaviour.

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.14.0
  • Add <goma-top-competitions-pills>: a horizontal, single-selection pill row of top competitions (region flag + name per pill, active pill wearing a highlightPrimary ring). The horizontal-pill sibling of <goma-top-competitions> (vertical list). Reuses useTopCompetitionsSource + the popularTournaments RPC (prop / rpc / subscribe→RPC-fallback), with sportId optional (cross-sport when omitted), numberOfCompetitions cap, autoSelectFirst (default on), and an opt-in followSportPicker. Emits goma:competition-select: the same payload as <goma-competition-filter> / <goma-competitions-list>, so a paired <goma-events-horizontal follow-competition-picker> re-scopes its feed with no extra wiring.