Skip to content

<goma-competitions-list> ​

Vertical list of competitions grouped by region: one collapsible card per region (flag, region name, caret) over competition rows separated by dividers, scoped to a single sport. An opt-in drill-in mode swaps a tapped row for an in-widget detail view of that competition's live events instead of only emitting a selection event.

Live demo ​

Open in the playground →: toggle Competitions list in the widget rail.

Element tag ​

html
<goma-competitions-list></goma-competitions-list>

Installation ​

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

bash
npm install @gomagaming/competitions-list

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 swiper

Configuration ​

js
const el = document.getElementById('comps')
el.config = {
  socketUrl: 'wss://sportsapi.example.com/v2',
  socketRealm: 'www.example.com',
  apiBaseUrl: 'https://api.example.com',
  locale: 'en',
}
KeyTypeRequiredDescription
socketUrlstringWhen dataSource !== 'prop'WAMP socket URL. The widget hits getCompetitions + getLocations over this transport (and, with drillInOnSelect, subscribes a competition-scoped matches-aggregator topic for the detail view).
socketRealmstringWhen dataSource !== 'prop'WAMP realm.
apiBaseUrlstringWhen dataSource !== 'prop'REST base URL.
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.
messagesobjectNoi18n overrides per locale.
networkobjectNoNetwork policy; network.rpcTimeoutMs bounds each try of the getCompetitions call (up to 3 tries in 'rpc' mode, 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.

This widget is read-only (no betting config): it does not consume the betting-only bettingApiBaseUrl / ucsOperatorId config keys.

Data sources ​

'prop': host-fed ​

The widget renders whatever the host writes to el.competitions and el.regions. Competitions are grouped by venueId; the matching entry in el.regions supplies the canonical region label (falls back to the venueName carried on the competition record when absent).

js
el.dataSource = 'prop'
el.regions = [
  { id: 1, name: 'International - Clubs', translatedName: 'International - Clubs' },
  { id: 41, name: 'France', translatedName: 'France' },
]
el.competitions = [
  { id: 106, name: 'Champions League', sportId: 1, venueId: 1 },
  { id: 107, name: 'Europe League',    sportId: 1, venueId: 1 },
  { id: 301, name: 'Ligue 1',          sportId: 1, venueId: 41 },
]
ts
type GomaCompetition = {
  id: string | number
  name: string
  translatedName?: string
  shortTranslatedName?: string
  sportId?: number
  sportName?: string
  venueId: number          // region id: required for grouping
  venueName?: string       // fallback region label if regions array is missing
}

type GomaRegion = {
  id: number
  name: string
  translatedName?: string
}

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

The widget calls getCompetitions({ sportId }) and getLocations({ sportId }) once the WAMP connection is up, then joins them client-side on venueId. Re-runs whenever sportId 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 list ends empty. The answer is filtered by contextLiveStatus: see Which competitions are listed.

js
el.dataSource = 'rpc'
el.sportId = 1
el.config = { /* socket + REST creds */ }

'subscribe': live WAMP subscription (placeholder) ​

Note: the backend topics for live competitions / locations updates are not yet wired. The widget accepts dataSource: 'subscribe' but internally falls back to the one-shot RPCs (warn-once). Unlike 'rpc' mode it makes one try only, with no retries: a lost answer ends the list after network.rpcTimeoutMs, and goma:feed-load-failed is not sent. Once the topics ship, the fallback flips automatically.

Which competitions are listed ​

A row is only listed when its competition has games of the kind the page it leads to lists, so no row opens an empty detail or feed. Set contextLiveStatus to the live status of that page:

contextLiveStatusA competition is listed 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 (for example a tennis Grand Slam a year ahead) has no games, so it is not listed, except with 'ANY'. A region whose competitions are all left out gets no card.
  • The rule reads the count fields of each tournaments record. A record that leaves out every field the status reads is kept: an absent count is unknown, not zero.
  • With drillInOnSelect, the detail lists live and upcoming games, so keep 'BOTH'. Use 'NOT_LIVE' or 'LIVE' for the status of a paired feed.
  • competitions holds the filtered list. A selectedCompetitionId that names a competition left out highlights no row and, with drillInOnSelect, opens no detail. With an id set after mount, goma:competition-select still goes out for that id, with competitionName, competitionSportId and competitionVenueId set to null, so a paired feed follows it. An id set before mount sends no event, so the feed does not follow.
  • Changing contextLiveStatus filters the loaded list again without a new call. When that leaves no open region listed, the first listed region opens.
  • The list loads once per sport, so a competition that loses its last game while the page stays open stays listed until the next load.
  • 'prop' mode is not filtered: a host-fed list is shown as given.

Props / attributes ​

PropertyAttributeDefaultDescription
sportIdsport-id1Sport id passed to the RPCs. Mandatory: the backend endpoints are sport-scoped.
competitions-[]Host-fed competitions array (when dataSource: 'prop'). With a socket source it reads null during a sport change, until the new sport answers, unless its list is already loaded.
regions-[]Host-fed regions array (when dataSource: 'prop').
selectedCompetitionIdselected-competition-idnullCurrently-selected row id. Two-way: clicks update it. With drillInOnSelect, setting it opens that competition's detail once the list has loaded (host deep-link / refresh restore), setting a different id while a detail is open switches to that competition, and clearing it closes the detail; back clears it. Clearing means the property set to null/'' or the attribute set to '': removing the attribute does not close it (a removed attribute is indistinguishable from one never set).
dataSourcedata-source'rpc''prop' | 'rpc' | 'subscribe'.
contextLiveStatuscontext-live-status'BOTH''BOTH' / 'NOT_LIVE' / 'LIVE' / 'ANY': which events the page a row leads to lists. A competition with no games of that kind is not listed; 'ANY' lists every competition. Not applied in 'prop' mode. See Which competitions are listed.
showTitleshow-titletrueRender the "Competitions" header above the list.
skeletonCountskeleton-count4Placeholder region cards shown until the first competitions assignment, and again on a sport change with a socket source.
showFadesshow-fadestrueRender the edge fades on each drill-in event card's market rail. The gradient fades to --goma-backgroundSecondary, so set false when the host paints a different surface behind the widget: otherwise the fade reads as a smear rather than a hint that the rail scrolls.
sizesize'default'Card size variant for the drill-in detail's event cards. 'default' is the design's baseline geometry: 160px-tall cards with 16px team names. 'large' opts into the enlarged card: roomier internal spacing, 18px team names on the main card, 16px market titles and 14px team names on the additional-market cards, and a 190px-tall rail. The loading skeletons take the same height, so a large rail never reflows when its first dump lands. The size prop itself is additive and opt-in, but the 'default' height is not: it moved from 164px to the design's 156px, and then to 160px for the reason given below. Every existing embed follows it with no way to pin an older one. The card's internal geometry was corrected onto the design in the same change and is likewise not opt-in: the odds row is 40px tall instead of 48px, the odds buttons carry 8px of padding on a 4px corner instead of 6px, the card is padded 8px/10px top/bottom instead of 4px/12px, the breadcrumb row is 16px instead of 24px, team names are weight 700 on a 16px line instead of weight 600 on a 20px line, the competition name is weight 500 instead of 300, and the market badge is 16px tall with 8px side padding instead of 14px with 12px. The two heights are EXACT, never minimums: a rail must never show cards of differing heights, so the "See all" tile, the loading skeleton and the empty state all carry the same number. They read 160px and 190px rather than the design frames' 156px and 188px because those frames set two 16px/700 team names in 16px line boxes: a line box equal to its font shears the ascenders on the web once the names are truncated, so the boxes are sized to their ink instead and design raised the card by the difference.
teamNameFormatteam-name-format'short'Team names on the drill-in detail's event cards. Requires drillInOnSelect. 'short' shows the participant short name (e.g. "Man City"); 'full' shows the full name (e.g. "Manchester City"). Either one falls back to the other when the feed leaves it empty, so a name is never blank. The value ignores case and surrounding spaces ('Full' works); any other value reads as 'short' and logs one console warning. The accessible card labels follow the same setting; emitted event payloads (goma:event-navigate's homeTeam / awayTeam) keep the short name.
cardWidthcard-width''Fixed width for every event card in the drill-in detail: any CSS length (e.g. '340px'), or a number read as px (340, '340'), measured over the card's border. The loading skeletons take the same width. Empty, or a value that is neither, keeps the clamped 260-400px width that follows the viewport. Requires drillInOnSelect.
flagBaseUrlflag-base-url'https://static.glastcoper.com/omfe-widgets/s/assets/1.10.2/om1/icons/flag/'Country flag CDN base. Flags resolve to ${flagBaseUrl}{venueId}.png.
initialOpenRegionIdsinitial-open-region-ids'first'Initial expanded state: 'first' | 'all' | 'none' | an array of region ids (venueIds) to open exactly those. The HTML attribute accepts a JSON-encoded array. When a list leaves no open region listed, the first listed region opens. When every card is closed ('none', or the player closed them), they stay closed.
drillInOnSelectdrill-in-on-selectfalseWhen true, tapping a competition row opens an in-widget detail view instead of only emitting goma:competition-select. Off by default: existing emit-only behaviour is unchanged.
pageSizepage-size0Events added per Load more tap inside the detail view. 0 fetches the whole competition in one dump. Requires drillInOnSelect; the region accordion itself is never paginated. See Competition detail view.
followSportPickerfollow-sport-pickertrueListen at window for goma:sport-select and rewrite sportId. See Integrates with.
sportPickerSelectorsport-picker-selectornullOptional CSS selector scoping followSportPicker to one element.

The event cards are the same components <goma-events-horizontal> renders, so a card with nothing to price shows the Suspended label across its odds row rather than a bare -: see <goma-events-horizontal> → Empty and unpriced cards for the rule.

Competition detail view ​

When drillInOnSelect is enabled, tapping a competition row opens an in-widget detail view instead of only emitting goma:competition-select. The detail shows a header (back arrow, region flag, competition name, and a login-gated favourite star) above that competition's live event cards. The cards are the same market-rich tall-odds cards <goma-events-horizontal> renders, with the market swiper: tapping a card's body emits goma:event-navigate (drive a linked <goma-game-details>), tapping odds emits goma:outcome-select and adds the selection to the shared betslip. Tapping the back arrow returns to the list, which stays mounted underneath (v-show, not v-if) so its scroll / expanded-region state is preserved across a drill-in round trip; the detail view itself unmounts on back, tearing down its subscription.

Within each card, the markets behind the swiper are ordered by identity against the canonical market list the feed declares, not by their position in the event's markets array. This keeps the leftmost (headline) market stable on live feeds, where a suspended-and-re-offered market would otherwise land at the end of the array and reshuffle the row. See <goma-events-horizontal> → Market ordering within a row for the full rule.

The view transition is announced via goma:competition-detail-open / goma:competition-detail-close, so a host can promote the detail to its main content area (hiding sibling widgets) while it is open and restore the normal layout on back.

Setting selectedCompetitionId also opens that competition's detail (when drillInOnSelect is on) as soon as the matching record is present in competitions, so a host with its own router can deep-link or restore a detail after a page refresh by feeding the id in on mount; if the id is set before the list has resolved, the widget retries once competitions updates. Tapping back clears selectedCompetitionId. The reverse also holds, so a host router can drive the view both ways: clearing selectedCompetitionId closes an open detail (announced with goma:competition-detail-close, exactly as the back arrow), and a different id while a detail is open switches to it (announced as goma:competition-detail-close for the outgoing competition then goma:competition-detail-open for the incoming one, so the two events stay paired). This is what lets the browser's Back button take a host out of a detail it had pushed onto its own history.

Clearing it means setting the property to null or '', or setting the attribute to ''. Removing the attribute does not close the detail: a removed attribute arrives as null, which the element cannot tell apart from an attribute that was never set, so it is ignored. An id that is not in competitions closes an open detail rather than leaving the previous one on screen; when the list has not resolved yet (a cold deep link) there is nothing open to close and the widget still opens that id once the list arrives.

Paginating the detail ​

By default the detail fetches the whole competition in a single dump (an eventLimit of 100 on its own competition-scoped matches-aggregator topic). The topic sorts by UPCOMING, so it lists every upcoming and live match of the competition, not only the ones the provider marks as popular. Set pageSize to a positive number to page it instead: the detail seeds the topic at that limit and renders a Load more button underneath which grows eventLimit by another pageSize on each tap and re-subscribes. Mechanically the same technique as <goma-events-horizontal>'s pageSize. The already-rendered cards stay on screen across the refetch; there is no flicker and no scroll jump.

js
el.drillInOnSelect = true
el.pageSize = 10          // 10 events per page; 0 (default) = whole list in one dump

Two behaviours worth knowing:

  • End of list is inferred from a short page. Ask for 10, get 6 back, and the button disappears because that was everything. The cost is one wasted round-trip to discover the end; the aggregator dump carries no total, so there is nothing better to read.
  • A page that never arrives clears itself after 55s, long enough for the connection layer to ask for the page again. If the socket drops mid-request the spinner stops and the button becomes tappable again rather than latching forever. The list is not marked exhausted in that case: "we never heard back" is not "that was the last page".

pageSize only affects the detail view. The region accordion itself is never paginated: its getCompetitions RPC returns every competition for the sport in one call.

Events ​

NameDetailNotes
ready{}Mounted + initial render complete.
goma:error{ message, code, component? }Two paths. Connection layer: the WAMP adapter exhausts its 5 retry attempts (code: 'API_INIT_ERROR'), alongside goma:connection { state: 'failed' }. Widget root scope: an error raised in the widget's own outermost component (its setup, its render, its lifecycle hooks or its watchers), where code is the error's name, e.g. 'TypeError' ('RUNTIME_ERROR' when it has none). Errors raised inside the widget's child components do not reach this event: the in-shadow error boundary catches those and shows the retry banner instead, emitting only goma:retry. See Empty / loading / error states.
goma:retry{}User clicked Retry on the render-error banner.
goma:competition-select{ competitionId, competitionName, competitionSportId, competitionVenueId, previousCompetitionId }User tapped a competition row, or the host set selectedCompetitionId (a deep link or refresh restore): never on mount, and never when re-assigned the value already held. CLEARING the id is silent: closing the drill-in detail has its own goma:competition-detail-close, and an empty selection would tell a following feed to scope to nothing. See Driving a selection from the host. Identical payload to <goma-competition-filter> and <goma-top-competitions-pills>: a downstream <goma-events-horizontal> with followCompetitionPicker: true cooperates with no changes, regardless of which of the three emitted it.
goma:region-toggle{ regionId, regionName, isOpen }User toggled a region card. Informational.
goma:competition-detail-open{ competitionId, competitionName, competitionSportId, competitionVenueId }Drill-in only: the detail view just opened (a row was tapped, or selectedCompetitionId resolved, with drillInOnSelect on). A host can use this to promote the widget to its main content area (hide sibling widgets) while the detail is shown.
goma:competition-detail-close{ competitionId }Drill-in only: the detail view closed, through the back arrow, the host clearing selectedCompetitionId, the host switching to another competition (this fires for the outgoing one), or an id the list cannot resolve. Pairs with goma:competition-detail-open so the host can restore its normal layout. competitionId is the competition that was open.
goma:favorite-toggle{ type: 'competition', id, isFavorite }Detail view only: header favourite star tapped while logged in. The widget does not persist by itself; the star's own composable writes into the shared cache (see Integrates with).
goma:login-required{ reason: 'favorite', type: 'competition', id }Detail view only: header favourite star tapped while logged out. The widget does not favourite: the host should open its login flow.
goma:event-navigate{ eventId, homeTeam, awayTeam, competitionName }Detail view only: a detail-view event card's body (or its "See all" markets button) was tapped.
goma:outcome-select{ bettingOfferId, outcomeId, outcomeName, odds, eventId, marketId, bettingTypeId, eventPartId, line }Detail view only: odds button tapped on a detail-view event card. Adds to the shared betslip.
goma:seo-data{ widget: 'competitions-list', type: 'sports-event', items: object[] }Detail view only: fired once that competition's fixtures resolve, and again only when an event's identity changes, never on an odds tick. Closing and reopening the same competition's detail re-emits the same payload by design: see SEO data.
goma:feed-load-failed{ source, sportId?, topic? }A list call failed on all 3 tries ('rpc' mode only), or the drill-in detail feed got no answer after every retry. The widget shows its empty state when it had no rows; after Load more or a reconnect the earlier rows stay. Informational: use it to show your own message or to reload. source is 'competitions' or 'locations' (with sportId) for the region list, or 'events' (with topic) for the drill-in detail feed. With source: 'locations' the rows still render, from each competition's venueName.

All custom events are bubbles: true, composed: true so they cross the Shadow DOM boundary.

Integrates with ​

Listens for goma:sport-select (gated by followSportPicker, default true) and rewrites sportId in place: both source composables re-run their RPCs against the new sport, and the region accordion re-seeds to initialOpenRegionIds for the new list. Scope the listener to one emitter via sportPickerSelector (default null = match any). Commonly paired upstream with <goma-sports-navigation-horizontal> / <goma-sports-navigation-dialog>.

Emits goma:competition-select on every row tap (the same payload <goma-competition-filter> and <goma-top-competitions-pills> emit), so a downstream <goma-events-horizontal> with followCompetitionPicker: true re-scopes its feed with no extra wiring.

In the drill-in detail view, event cards write betslip selections into the shared goma:betslip:v4 envelope (identity fields only, useSharedBetslipSync({ subscribeToState: false }), mirroring <goma-events-horizontal>), and card taps emit goma:event-navigate, which <goma-game-details> follows by default. The detail header's favourite star reads and writes the shared goma:favorites:v1 cache, so a star tapped here lights up the matching card in <goma-events-horizontal>, <goma-game-details>, and <goma-favorites>, and vice versa.

See Cross-widget wiring and Shared state for the full rewrite mechanics and envelope field-by-field contracts.

Empty / loading / error states ​

  • List, loading. Skeleton region cards (skeletonCount, default 4) render until state.competitions first becomes an array, including an RPC or host assignment that resolves to []. A sport change shows them again until the new sport answers, unless its list is already loaded.
  • List: empty. Once data has arrived, an empty list renders a localised "No competitions available" message instead of skeletons. This includes a sport whose competitions all have no games for contextLiveStatus.
  • List: loaded. Region cards render, grouped by venueId, in locations-RPC order (with any straggler region present only in competitions appended at the end).
  • Detail (loading. Driven by the detail's own topic subscription's loaded lifecycle flag, not by an event count) a competition with genuinely zero events resolves to the empty state instead of an endless skeleton. If the subscription never answers (socket down, provider silent), useTopicSubscription's own watchdog releases loaded so the view falls through rather than spinning forever.
  • Detail: empty. Zero events for the competition renders a localised "No events available" message.
  • Detail: error (render). A render error inside the widget shows a themed error banner for 8 seconds with a dismiss button and a Retry button (emits goma:retry).

Theming ​

The widget paints with --goma-* CSS variables. Override via el.theme = { … } or the host page's CSS. See Theming for the full token catalogue.

TokenDefaultUsed for
--goma-backgroundPrimary#03061bOuter container background
--goma-backgroundCards#1f2147Region card background, detail header background
--goma-backgroundSecondary#161732Detail-view event-card scroll fade, empty-state background
--goma-backgroundTertiary#1f2147Detail-view "See all" button border
--goma-backgroundBorder#2a2d60Detail-view "See all" button hover background
--goma-textPrimary#ffffffRegion + competition labels
--goma-textSecondary#939dffEmpty-state + flag fallback
--goma-iconPrimary#ffffffCaret stroke
--goma-separatorLineSecondary#393d83Row separator line
--goma-highlightPrimary#ff6600Focus rings, Load more button, retry-button background, inactive favourite star
--goma-highlightPrimaryContrast#ffffffRetry-button text + focus ring
--goma-favorites#fac125Active favourite star
--goma-alertError#f9554dRender-error banner background

i18n / message overrides ​

Override any bundled translation key via config.messages. Keys this widget's own components reference directly:

KeyDefault (en)Used in
competitions_list_titleCompetitionsHeader above the list (showTitle)
competitions_list_ariaCompetitions listaria-label on the region-card group
competitions_list_emptyNo competitions availableList empty state
competitions_list_region_expand_ariaExpand {region} competitionsRegion header aria-label when collapsed
competitions_list_region_collapse_ariaCollapse {region} competitionsRegion header aria-label when expanded
backBackDetail header back button
add_to_favoritesAdd to favoritesDetail header favourite star (inactive)
remove_from_favoritesRemove from favoritesDetail header favourite star (active)
loadingLoadingDetail-view skeleton / load-more spinner
load_moreLoad moreDetail-view pagination button
no_events_availableNo events availableDetail-view empty state
retryRetryError banner retry button

Embedding examples ​

Vanilla HTML ​

html
<goma-competitions-list id="comps" sport-id="1"></goma-competitions-list>

<script type="module">
  import '@gomagaming/competitions-list'

  const el = document.getElementById('comps')
  el.config = {
    socketUrl: 'wss://sportsapi.example.com/v2',
    socketRealm: 'www.example.com',
    apiBaseUrl: 'https://api.example.com',
    locale: 'en',
  }
  el.addEventListener('goma:competition-select', (e) => {
    console.log('selected', e.detail)
  })
</script>

React ​

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

export function CompetitionsList({ sportId, config }) {
  const ref = useRef(null)
  useEffect(() => {
    if (!ref.current) return
    ref.current.config = config
    ref.current.sportId = sportId
  }, [config, sportId])
  return <goma-competitions-list ref={ref} />
}

Vue 3 ​

vue
<template>
  <goma-competitions-list ref="el" :sport-id="sportId" />
</template>

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

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

onMounted(() => { if (el.value) el.value.config = props.config })
watch(() => props.config, (next) => { if (el.value) el.value.config = next })
</script>

Accessibility ​

  • Each region header is a <button> with aria-expanded and aria-controls pointing at the body it toggles.
  • The body region renders with role="region" and aria-label="{region name}".
  • Competition rows have role="button", tabindex="0", and respond to Enter / Space in addition to mouse clicks. Selection sets aria-current="true".
  • The detail header's back button and favourite star are native <button> elements with aria-label; the favourite star also sets aria-pressed.
  • In a detail event row, a card or odds button that takes focus outside the visible part of the row slides into view, and the row's edge fades stay at its edges.

What's deferred ​

  • Live updates. dataSource: 'subscribe' falls back to one-shot RPCs until backend topics for competitions / locations are wired.
  • Drag-to-reorder regions. Region order today is fixed by the locations RPC response.
  • Per-region competition counts. The design ships labels only; counts can be added when the data carries them.

Integration Caveats ​

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

  • Tokens/Theming, themeTokens.js was synced to the 13 tokens this widget (list + detail view) actually paints with, previously only 7 were registered, though the Theming table above was already accurate.

  • Events: goma:retry was emitted but had no dedicated Events-table row; 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).

  • 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.10
  • A competition's match list now shows every upcoming and live match. Before, the competition detail in <goma-competitions-list> and a favourite competition's section in <goma-favorites> asked only for the popular matches, so a small competition opened empty and a bigger one showed only some of its matches.
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.

  • 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
  • The competitions list only lists competitions that have games, so a row never opens an empty detail or feed. The new contextLiveStatus prop ('BOTH' by default, or 'NOT_LIVE' / 'LIVE') says which events the paired page lists, and a competition with none of them is left out. 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. A deep link to a competition that is left out opens no detail. When a list leaves no open region listed, the first listed region opens, in every data source; when every card is closed, they stay closed. 'prop' mode is not filtered. useCompetitionsSource now applies the same filter to the list it writes.

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

  • New teamNameFormat setting ('short' | 'full', default 'short') on <goma-events-horizontal>, <goma-favorites>, <goma-competitions-list> and <goma-game-details>. It picks the team names the event cards and the Game Details header show: the short name (e.g. "Man City", the current look) or the full name (e.g. "Manchester City"). Either one falls back to the other when the feed leaves it empty. The value ignores case and surrounding spaces; any other value reads as 'short' and logs one console warning. The cards' accessible labels follow the setting; emitted event payloads keep the short name. @gomagaming/sports-domain exports the participantNames(event, format) helper the cards use.

0.37.4
  • <goma-favorites> and <goma-competitions-list> gain a cardWidth setting (card-width attribute) that pins every event card to a fixed width, as <goma-events-horizontal> already allows. Empty keeps the default width.
  • Keyboard focus in horizontal rails now stays on screen. <goma-sports-navigation-horizontal> slides a tile reached with Tab to the centre of the strip, and the event rows in <goma-events-horizontal>, <goma-favorites> and the <goma-competitions-list> detail view slide a card or odds button reached with Tab into view. The event rows also no longer scroll natively under the carousel, which moved their edge fades into the row and left the carousel out of step with what was shown.
  • 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.
  • 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-events-horizontal> gains seven opt-in card geometry options: cardWidth (a fixed width for every rail card and loading skeleton, border included, instead of the clamped 260–400px; a CSS length, or a number read as px), oddsGap (space between the main card's odds buttons), rowGap (space between event rows), preliveCardStyle: 'compact' (an 11px/600 date line on the pre-live card), and, for the live main card, liveOddsHeight, liveTeamNameSize and liveBadgeLeftRadius (odds row height and team name size at any size, and the LIVE tab's left corner). Unset, each keeps the current value (clamped width, 8px, 16px, 12px/500, 40px / 34px compact, 16px / 18px large, pill / 8px compact LIVE tab), so the default look is unchanged. The game cards in @gomagaming/ui take the matching cardWidth, oddsGap, oddsHeight, teamNameSize, badgeLeftRadius and preliveCardStyle props. <goma-favorites> and <goma-competitions-list> carry the same row component, so they are bumped with it. @gomagaming/core is bumped for the new utilities in its compiled sheet.
0.36.1
  • Playground manifests (@gomagaming/<widget>/playground) now offer a control for every setting each widget really has. Settings that cannot be a form field (raw provider data, read-only state mirrors, props a widget never reads) are recorded with a reason in the repo instead of being silently missing.

    Controls that never reached the widget are fixed:

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

    No widget runtime behaviour changes.

0.36.0
  • The flat 1px frame of the secondary game cards (additional-market, pre-live and outright) reads an optional --goma-secondaryCardBorder token, the same one for all three card types. Unset, it falls back to cardBorderLineGradient2, the token it used before, so the default look is unchanged; a host can frame those cards without moving the main card's gradient mid-stop. @gomagaming/core is bumped for the new utility in its compiled sheet.

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

  • <goma-competitions-list>: selectedCompetitionId now drives the drill-in detail both ways. Clearing it closes an open detail (announced with goma:competition-detail-close, as the back arrow does), and setting a different id while a detail is open switches to that competition (announced with goma:competition-detail-open). Switching announces the outgoing competition's close before the incoming one's open, and an id the list cannot resolve closes an open detail instead of leaving the previous one on screen. Clearing means the property set to null/'' or the attribute set to '': removing the attribute is ignored, as it is indistinguishable from an attribute that was never set.

    A host that keeps the open detail in its own router can now let the browser's Back button take the widget out of the detail; before, only the in-widget back arrow could. Minor rather than patch: selectedCompetitionId = null used to be a no-op, so an existing embed that clears the id for unrelated reasons now closes the detail with it.

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

  • Live cards keep their odds in step with the event page through a re-price. The provider models a re-price as a new betting-offer id rather than a change to the old one, and two things in the card's path mishandled that pair:

    • detachBettingOfferFromOutcome keyed on the OUTCOME rather than on the offer, so a re-price arriving as CREATE(new) + DELETE(old) (an order the provider is free to use) made the DELETE tear off the offer the CREATE had just attached. The card was left with no price until the next full dump. Order-dependent, which is why it read as the card being "sometimes slower, sometimes faster". It now takes the offer id and ignores a delete whose offer has already been replaced.
    • attachBettingOfferToOutcome re-attached whatever offer an update named, so a close-out on the offer a re-price had just replaced put the retired price back on the card while the event page (keyed on the offer id) stayed on the new one. A refresh now lands only on an outcome already holding that same offer. That also covers the case where the outcome holds nothing: a retired offer is never evicted from the pool, so without it a withdrawal followed by any late tick repainted a dead price as tappable, and a bet placed on it would be rejected by the provider.
    • bettingOfferId is re-pointed alongside the object it names, so the two cannot disagree. This is invariant hygiene rather than a visible fix: no surface resolves a price through that field on a live feed today.

    Separately, BETTING_OFFER records now reach both store mirrors whichever WAMP topic carried them: the shared pool the cards read and the singular game slot <goma-game-details> reads. create previously wrote exactly one of the two and never the other. The cross-writes are non-creating: an offer for an outcome a mirror does not already hold is left alone, so the event page's markets never seed the pool the cards read.

    Affects every widget that renders odds from a live feed.

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

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.30.2
  • Event cards: line the odds row up across a rail, and settle the market badge

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

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

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

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

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

0.28.0
  • <goma-events-horizontal>, <goma-favorites> and <goma-competitions-list>: add a showFades prop (show-fades attribute, default true) that toggles the edge fades on each event's market rail.

    The fades are painted as a gradient from --goma-backgroundSecondary, so they only read correctly when the host paints that exact surface behind the widget. On any other background the fade showed as a smear across the rail edges rather than a hint that it scrolls, and there was no way to turn it off. Default stays true, so existing embeds are unchanged.

    All three widgets render the same market rail from byte-identical copies of the same component, so the prop lands on all three together: <goma-favorites> applies it to its favourite-event cards and <goma-competitions-list> to the event cards in its opt-in drill-in detail view.

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

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.22.0
  • Fix <goma-events-horizontal> showing skeleton cards forever when a feed legitimately returns zero events.

    Loading was inferred from store content (events.length === 0 plus a flag that only ever flipped on a non-empty list), which cannot distinguish "an empty dump landed" from "no dump yet", so an events-less topic never left the loading state and its empty state was unreachable. It surfaced on a cold load into a sport with no live games.

    useTopicSubscription now owns the feed-load lifecycle and returns a loaded ref: true once an initial dump lands (whether or not it carries events), reset on every topic change, and released by a 15s watchdog for the dump that never arrives at all. A subscribe that was a no-op because no session existed yet, or a provider that went silent. Neither case can ever call afterInitialDump, so no dump-based signal alone can cover them. The existing onLoaded callback is unchanged and still means strictly "a dump landed".

    Consumers should render loading from loaded, never from an event count. In host-fed mode an explicit events assignment (including []) is the answer.

    <goma-competitions-list>'s drill-in detail adopts the same ref. Its own flag already handled the empty dump, but nothing released it when no dump arrived at all, so a detail opened before the socket finished connecting kept its skeleton indefinitely. Pagination still settles on dumps only: a watchdog expiry must not count as a short page, or Load more would be retired for good.

    Also raises events-horizontal's bundle budget to 11.5 kB gzip / 10.5 kB brotli; the fix needed ~480 B and the previous 11 kB cap left only ~180 B of headroom.

0.21.0
  • Add opt-in load-more pagination to <goma-competitions-list>'s competition-detail view. A new pageSize prop (default 0 = off, whole list in one dump as before) renders a "Load more" button under the detail's event cards; each click grows the detail topic's eventLimit segment by one page and re-subscribes. Exhaustion is inferred from a short page, and a request that never lands clears its spinner after 15s rather than latching the button. Only affects the drill-in detail: the region accordion is not paginated.
0.20.0
  • Stop event cards reshuffling their markets on live feeds

    Each card's row of markets was selected by array position: markets[0] for the headline market, slice(1, n) for the rest. On a live feed a suspended market is re-offered as a delete followed by a create, and the re-created market lands at the end of the event's markets array, so the headline market and every card behind it reshuffled continuously.

    Markets are now ordered by identity (bettingTypeId) against the canonical market list the feed declares, matching how the reference sportsbook selects them. Host-fed embedding is unaffected: with no canonical list to rank against, the order declared in each event's markets array is preserved.

    Adds orderMarketsByMainMarkets to @gomagaming/sports-domain.

0.19.0
  • Remove the unused vue-toastification peer dependency

    useGameCardsTallOdds called useToast() and discarded the result. Because that is a call expression rather than a bare import, Rollup could not tree-shake it, so the built sports-domain chunk carried a real vue-toastification import: every consumer rendering a game card loaded the library at runtime for a variable nothing read.

    The dead call is gone and vue-toastification has been dropped from the peerDependencies of all 19 packages that declared it. Consumers no longer need to install it. No element tag, prop, config key or event changed.

    Three neighbouring declarations were corrected at the same time:

    • sports-domain no longer declares a swiper peer it never imported.
    • competitions-list declares swiper as a peer rather than a direct dependency, matching every other swiper-using package.
    • core now declares the peers its useSwiper stub actually imports: @vueuse/core, plus swiper as an optional peer so consumers of carousel-free widgets are not warned about a package they never load.

    Action required for @gomagaming/competitions-list consumers: swiper@^11.1.0 is now a peer dependency rather than a bundled dependency. Package managers that auto-install peers (pnpm 8+, npm 7+) handle this transparently. Package managers that do not (yarn classic, yarn berry, or npm run with --legacy-peer-deps) will stop installing swiper, and your build will fail to resolve swiper/vue. Install swiper@^11.1.0 alongside the widget.

    This release is a minor rather than a patch for exactly that reason: adding a peer requirement can break a consumer, and a patch bump is not a strong enough signal. Every other change here only relaxes requirements.

  • 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.15.1
  • GomaPulse: Fortuna Juegos UI review fixes:

    • Suspended/locked outcome labels are now legible: they read the dedicated "suspended odds text" token instead of a divider-line colour.
    • A selected outcome keeps its highlight and readable text when its odds change; the green/red change border and triangle still overlay.
    • Secondary (additional-market) cards render a single market icon in the icon-secondary colour instead of two mis-coloured glyphs.
    • The My-Bets leg sport icon is sized to match the adjacent country flag.
    • Competitions-list rows and region headers no longer clip text descenders.
0.15.0
  • <goma-competitions-list> gains an opt-in drillInOnSelect prop: tapping a competition opens an in-widget detail view (header with back button, region flag, name, and login-gated favourite star, over that competition's live event cards) instead of only emitting goma:competition-select. Default off, so existing integrations are unchanged. New events: goma:favorite-toggle (header star tapped while logged in), goma:login-required (star tapped while logged out), goma:event-navigate (detail card body tapped), goma:outcome-select (detail card odds tapped). All scoped to the detail view, plus goma:competition-detail-open / goma:competition-detail-close, which announce the view transitions so a host can promote the detail to its main content area while it is open.

  • 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.9.0
  • New widget <goma-competitions-list>: vertical list of competitions grouped by region, scoped per sport.

    Each region renders as a collapsible card (flag + region name + caret in the header, competition rows with separator lines in the expanded body), matching the design. Multi-open accordion; the first region card is expanded by default, controlled via initialOpenRegionIds.

    • Reuses useCompetitionsSource + useLocationsSource; joins them client-side on venueId to build the per-region groups.
    • Three data sources via dataSource: 'prop' (host-fed via el.competitions / el.regions), 'rpc' (one-shot getCompetitions + getLocations on connect; default), 'subscribe' (placeholder, falls back to RPC).
    • sportId is mandatory (defaults to 1 = Football). Pair with the sports picker via followSportPicker to re-fetch on sport change.
    • Emits goma:competition-select with the same payload as <goma-competition-filter>: downstream <goma-events-horizontal> with followCompetitionPicker: true cooperates with no changes.
    • Also emits goma:region-toggle (informational) when a region accordion card is opened or closed.

    Released alongside the rest of the lockstep family at a minor bump.

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