<goma-top-competitions>
"Top Competitions": an optional section title above a set of competition entries (sport icon + region flag + name), with single-selection state. Emits goma:top-competition-select when the selection changes: a tap on an entry that is not already selected, or a host write of a different value after mount.
Two presentations of the same feed, picked with the variant property:
'list'(default): sidebar column of rows inside a rounded panel, plus a host-fedhighlightedImagesmap that flips matching rows into a banner-topped featured card.'tiles'. Mobile-first: a trophy title band above a horizontally scrollable rail of fixed-width tiles.
The data plumbing is identical either way: same RPC, same competitions array, same selection state, same emitted event. Only the front end changes.
Live demo
Open in the playground →: toggle Top Competitions in the widget rail.
Element tag
html
<goma-top-competitions></goma-top-competitions>Installation
@gomagaming/top-competitions is a scoped private package. See Installation for the one-time .npmrc setup.
bash
npm install @gomagaming/top-competitionsYou also need the shared peer dependencies installed at your app level:
bash
npm install vue pinia vue-router vue-i18n @vueuse/core autobahn-browserConfiguration
Assign a config object to the element's config property:
js
const el = document.getElementById('tc')
el.config = {
socketUrl: 'wss://sportsapi.example.com/v2',
socketRealm: 'www.example.com',
locale: 'en',
}| Key | Type | Required | Description |
|---|---|---|---|
socketUrl | string | When dataSource !== 'prop' | WAMP socket URL. Without it (and without socketRealm) the connect step is skipped and the widget stays in its skeleton state. |
socketRealm | string | When dataSource !== 'prop' | WAMP realm. |
sportsNamespace | string | No | Root WAMP namespace the popularTournaments RPC is built against ({sportsNamespace}#popularTournaments). Defaults to '/sports'. |
network | object | No | Network 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. A failed or timed-out try is logged. The list keeps its rows on the same sport, and ends empty after a sport change or on a first load. |
locale | string | No | i18n locale; also sent as the RPC's lang kwarg. Defaults to 'en'. |
theme | object | No | CSS variable overrides: see Theming. |
appearance | object | No | Structure overrides (spacing, radii, sizes): see Appearance. |
messages | object | No | i18n overrides per locale. |
debug | boolean | No | Verbose console logging. |
apiBaseUrl, bettingApiBaseUrl, and ucsOperatorId are accepted on the shared config shape but this widget's RPC path never reads them: it only needs the WAMP socket to reach popularTournaments.
Data sources
'prop': host-fed
The widget renders whatever the host writes to el.competitions. Use this when the competition list comes from a REST endpoint or a CMS layer you already own.
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 apiSession.$wcall(popularTournaments, { lang, liveStatus: 'BOTH', sortByPopularity: true, maxResults, sportId? }) once the WAMP connection is up, with maxResults larger than numberOfCompetitions (see Which competitions are listed). sportId is optional on the backend:
- Cross-sport (default,
sportId: null):sportIdis omitted from the kwargs entirely. Backend returns top tournaments aggregated across every sport. - Sport-scoped (
sportId: 1etc.): a positivesportIdis included. 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 list ends empty.
js
// Cross-sport (default: no sportId)
el.dataSource = 'rpc'
el.config = { /* socket creds */ }
// Sport-scoped, rows leading to a prelive-only feed
el.dataSource = 'rpc'
el.sportId = 1
el.numberOfCompetitions = 5
el.contextLiveStatus = 'NOT_LIVE'
el.config = { /* socket creds */ }Which competitions are listed
A row is only shown when its competition has games of the kind the page it leads to lists, so no row opens on an empty feed. Set contextLiveStatus to the live status of that page's feed:
contextLiveStatus | A 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 row leads to a page that also lists outrights. - The rule reads the count fields of each
popularTournamentsrecord. 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
numberOfCompetitionsand the list is cut after filtering, so the list stays full. - A
selectedCompetitionIdthat names a competition left out by this rule highlights no row. - Changing
contextLiveStatusre-filters the loaded list without a new RPC call. 'prop'mode is not filtered: a host-fed list is shown as given.highlightedImagesonly applies to competitions that are still listed.
'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, logging a one-time console warning. Once the topic ships, the fallback flips automatically.
Props / attributes
| Property | Attribute | Default | Description |
|---|---|---|---|
variant | variant | 'list' | 'list' / 'tiles': presentation only, see Variants. Any unrecognised value renders 'list'. |
dataSource | data-source | 'rpc' | 'prop' / 'rpc' / 'subscribe'. |
sportId | sport-id | null | Sport id passed to popularTournaments. null / 0 / missing (default) → cross-sport RPC. A real number → sport-scoped RPC. |
numberOfCompetitions | number-of-competitions | 5 | Max number of rows shown. The RPC is asked for more, see Which competitions are listed. |
contextLiveStatus | context-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 shown; 'ANY' keeps every competition. Not applied in 'prop' mode. |
competitions | - | [] | Array of GomaCompetition objects. Mirrored from bettingStore.topCompetitions when dataSource !== 'prop', and null during a sport change until the new sport answers, unless its list is already loaded. |
selectedCompetitionId | selected-competition-id | null | Id of the currently-selected row. Two-way: clicks update this value. It belongs to this element only, see Keeping the selection across pages. |
showTitle | show-title | true | Render the "Top Competitions" header. In 'list' it is a plain heading, in 'tiles' it is the trophy band. |
skeletonCount | skeleton-count | 5 | Number of placeholder rows ('list') / tiles ('tiles') shown until the first competitions assignment, and again on a sport change with a socket source. |
flagBaseUrl | flag-base-url | 'https://static.glastcoper.com/omfe-widgets/s/assets/1.10.2/om1/icons/flag/' | Base URL for country-flag PNG assets. Flags resolve to ${flagBaseUrl}{venueId}.png. |
iconBaseUrl | icon-base-url | '' | Accepted and threaded through to both row components, but not used to build the sport-icon URL: the glyph always resolves from the bundled SbIcon set (icons/sports/dark_mode/{sportId}.svg); the value only appears as a watch dependency that resets flag-load-failure state. |
highlightedImages | highlighted-images (JSON) | {} | 'list' variant only. Map of competition-id → banner image URL. Every competition in the list whose id appears here renders as the featured card (banner + gradient divider + row). Ignored by 'tiles'. |
GomaCompetition shape
ts
interface GomaCompetition {
id: number
name: string
translatedName?: string
shortTranslatedName?: string
sportId: number
sportName?: string
venueId?: number // country/region id: used to resolve the flag PNG
venueName?: string
}Variants
variant swaps the widget's front end without touching anything below it. The feed, the competitions array, selectedCompetitionId and the goma:top-competition-select payload are identical in both: a host can flip the property at runtime (a media query, a route change) and its listener never notices.
js
el.variant = 'tiles'html
<goma-top-competitions variant="tiles"></goma-top-competitions>Any value other than 'tiles' renders 'list', so a typo degrades to the default rather than blanking the widget.
'list': sidebar column (default)
Rounded backgroundSecondary panel, a bold section heading, and 38px-tall rows stacked vertically. Supports featured cards. This is the shipped rendering and it is unchanged.
'tiles': horizontal rail
Mobile-first. A trophy title band (backgroundCards, 4px radius) sits above a horizontally scrollable rail of 140px-wide tiles. Each tile pairs a 20px sport glyph with a 20px circular country flag above the competition name, which clamps to two lines before ellipsis.
Differences from 'list':
- No outer panel. The root is a bare flex column (12px gap), so the tiles sit directly on the host's page background. Give the widget a full-bleed slot: the rail supplies its own 16px side gutters and scrolls edge to edge.
- No featured cards.
highlightedImagesis ignored, andisFeaturedin the emitted payload is therefore alwaysfalse(the field describes what was rendered). - Tiles stretch to match. A name that wraps to two lines grows its tile past the design's 88px, and every sibling matches it, so the rail stays a clean band. To keep every tile at a fixed height instead, see
appearance. iconBaseUrlis unused. The sport glyph always comes from the bundledSbIconset.
html
<section style="padding: 0">
<goma-top-competitions id="tc" variant="tiles" number-of-competitions="8"></goma-top-competitions>
</section>Featured cards via highlightedImages
Pass an object map of competition-id → banner image URL to mark specific competitions as featured. Every competition in the list whose id appears in the map renders with the banner image on top + a cyan→purple gradient divider + the same info row underneath; the rest render as plain rows. The featured cards keep the natural feed order: the map only flips the row variant, it does not reorder. An empty map (the default) renders the entire list as plain rows.
js
el.highlightedImages = {
'106': 'https://photobucket.com/comp/ucl.png',
'999': 'https://photobucket.com/comp/europa.png',
}html
<goma-top-competitions
highlighted-images='{"106":"https://photobucket.com/comp/ucl.png"}'
></goma-top-competitions>The selection event detail includes isFeatured: boolean so hosts can fork their handler on whether the tapped row was the banner variant.
Events
| Status | Event | Detail | Emitted when |
|---|---|---|---|
| Canonical | ready | {} | Widget mounted. |
| Canonical | goma:connection | { state, … } | Connection state transition. |
| Canonical | 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. |
| Canonical | goma:retry | {} | The error banner's Retry button is pressed. |
| Canonical | goma:top-competition-select | { competitionId, competitionName, competitionSportId, competitionVenueId, isFeatured, previousCompetitionId } | User taps (or Enter/Space-activates) a competition row or tile that is not already selected. A tap on the selected entry changes nothing, so it emits nothing. The widget also mutates selectedCompetitionId so the entry flips immediately. isFeatured: true when the row RENDERED as the featured card: derived from highlightedImages and the active variant, so it is reported the same way for a host write; always false in the 'tiles' variant, which has no featured rendering. Also emitted when the host sets the property after the element has mounted (a menu link, a deep link, a refresh restore), so the whole follow* chain keeps up without extra wiring; never on mount (a value set before the element mounts is applied silently), and never when re-assigned the value already held. See Driving a selection from the host. |
| Canonical | goma: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 list ends empty. Informational: use it to show your own message or to reload. sportId is null when the list is cross-sport. |
The payload is emitted from one place for both variants (src/composables/useCompetitionSelection.js), so it cannot drift between them.
No shipped widget listens for goma:top-competition-select: it's a distinct event from the goma:competition-select emitted by <goma-competitions-list> and <goma-top-competitions-pills>, which <goma-events-horizontal> does follow. See the full event catalogue for the current listener matrix.
Keeping the selection across pages
The selection lives on one element instance. A host that renders the list inside each routed page creates a new element on every page change, and the new element starts with nothing selected.
| Host need | Do |
|---|---|
| The tapped row stays selected after the page change it caused | Keep one element mounted across pages (render it outside the routed view), or write selectedCompetitionId from the host's address when each new element is created and on every route change |
| A tap on a row always navigates | Clear selectedCompetitionId (null) on pages that are not that competition's. A tap on the entry already selected emits nothing, so a row left selected after the player has left its page does nothing when tapped again |
| A restore does not navigate again | A write after mount emits goma:top-competition-select like a tap. Ignore an event whose competitionId is the competition the address already shows. Clearing the selection emits competitionId: null: ignore that too |
js
// The competition the current address shows, as a string, or null.
const addressCompetition = () => competitionIdFor(router.currentRoute.value) ?? null
// Call it when a new element is created, and on every route change.
function syncSelection(el) {
el.selectedCompetitionId = addressCompetition()
}
// One page-level listener also hears the elements created later.
window.addEventListener('goma:top-competition-select', (e) => {
const { competitionId } = e.detail
// A cleared selection.
if (competitionId == null) return
// The echo of syncSelection: the page already shows that competition.
if (String(competitionId) === String(addressCompetition())) return
router.push(competitionPageFor(e.detail))
})Empty / loading / error states
- Loading (skeleton): until
state.competitionsfirst receives an array (from eitherel.competitions =in'prop'mode or the mirroredbettingStore.topCompetitionsin'rpc'/'subscribe'mode), the widget rendersskeletonCountpulsing placeholder rows / tiles (aria-hidden="true"). Ifcompetitionsis already non-empty when the widget mounts. A host that assigns it before the element connects, or a runtimevariantswap that remounts the tree. The skeleton is skipped outright. A sport change in a socket mode shows the skeleton again until the new sport answers. - Loaded, zero results: once
state.competitionshas received an array, the skeleton is replaced even if that array is empty; an empty RPC response renders an emptyrole="group"container with no rows. - RPC failure: a failed or timed-out
getTopCompetitionstry is logged to the console ([goma-top-competitions] RPC failed:) 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 list keeps its rows on the same sport and otherwise ends empty (after a sport change or on a first load), andgoma:feed-load-failedis emitted once. - Render error: a Vue render error caught by the App wrapper's error boundary shows an inline
role="alert"banner with a Retry button (dispatchesgoma:retry) and a dismiss control; it self-clears after 8 seconds.
Theming
Override via el.theme = { … }.
Both variants paint with existing --goma-* tokens: the 'tiles' variant introduced none.
In 'list', the outer panel paints with backgroundSecondary; the competition rows, the featured card, and the loading skeleton paint with backgroundPrimary, so the rows read as recessed against the panel. In 'tiles' there is no panel: the title band and the tiles are both backgroundCards surfaces on the host's own background.
| Token | Element |
|---|---|
--goma-backgroundSecondary | 'list': outer panel background |
--goma-backgroundPrimary | 'list' (row / featured-card / skeleton background; 'tiles') disc behind a flag that has not loaded |
--goma-backgroundCards | 'tiles': title band, tile and skeleton-tile background |
--goma-backgroundBorder | 'tiles': 1px tile border |
--goma-textPrimary | Row / tile label + title |
--goma-textSecondary | Flag fallback glyph |
--goma-iconPrimary | Sport icon colour; 'tiles': also the trophy glyph and the flag hairline |
--goma-highlightPrimary | Selected row/tile ring + focus ring; Retry button background |
--goma-highlightGradientStart | 'list': featured-card divider gradient start (cyan default) |
--goma-highlightGradientEnd | 'list': featured-card divider gradient end (purple default) |
--goma-alertError | Render-error banner background |
--goma-highlightPrimaryContrast | Retry button text on the render-error banner |
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.
'list' variant:
| Key | CSS variable | Default | Affects |
|---|---|---|---|
panelPaddingY | --goma-tcPanelPaddingY | 16px | Top and bottom padding inside the panel. |
panelPaddingX | --goma-tcPanelPaddingX | 16px | Side padding inside the panel. |
panelRadius | --goma-tcPanelRadius | 1rem (16px) | Panel corner radius. |
rowSpacing | --goma-tcRowSpacing | 0.5rem (8px) | Vertical space between rows, featured cards and skeleton rows. |
rowPaddingX | --goma-tcRowPaddingX | 0.75rem (12px) | Side padding of a row and of a featured card's name row. |
rowGap | --goma-tcRowGap | 0.5rem (8px) | Gap between the sport icon, the flag and the name in a plain row. |
rowRadius | --goma-tcRowRadius | 0.5rem (8px) | Corner radius of a plain row and a skeleton row. The featured card keeps 8px. |
rowFontWeight | --goma-tcRowFontWeight | 500 | Name weight in a plain row. Featured names are always 600. |
featuredRowHeight | --goma-tcFeaturedRowHeight | 38px | Height of the featured card's name row. The 56px banner and the 4px gradient divider come on top of it. |
'tiles' variant:
| Key | CSS variable | Default | Affects |
|---|---|---|---|
headHeight | --goma-tctHeadHeight | auto | Title band height, padding included. Unset, the band fits its content. |
titleFontSize | --goma-tctTitleFontSize | 16px | Title size. |
titleFontWeight | --goma-tctTitleFontWeight | 800 | Title weight. |
tileHeight | --goma-tctTileHeight | auto (at least 88px) | Set, every tile is exactly this tall and no longer grows to fit a two-line name. Pair it with labelLines: 1 and a small tileGap. |
tileGap | --goma-tctTileGap | 8px | Minimum space between the icons and the name. An 88px tile fits a one-line name only at 2px. |
labelLines | --goma-tctLabelLines | 2 | Lines the name shows before the ellipsis. |
js
// 88px tiles with a single-line name.
el.appearance = { tileHeight: '88px', tileGap: '2px', labelLines: 1 }i18n / message overrides
| Key | en default |
|---|---|
top_competitions | "Top Competitions" |
retry | "Retry" |
Embedding examples
Vanilla HTML
html
<goma-top-competitions id="tc" sport-id="1" number-of-competitions="5"></goma-top-competitions>
<script type="module">
import '@gomagaming/top-competitions'
document.getElementById('tc').config = {
socketUrl: 'wss://sportsapi.example.com/v2',
socketRealm: 'www.example.com',
locale: 'en',
}
document.getElementById('tc').addEventListener('goma:top-competition-select', (e) => {
console.log('selected', e.detail.competitionId, e.detail.competitionName)
})
</script>Host-fed
html
<goma-top-competitions id="tc" data-source="prop"></goma-top-competitions>
<script type="module">
import '@gomagaming/top-competitions'
const el = document.getElementById('tc')
el.competitions = await fetch('/api/top-competitions').then((r) => r.json())
</script>React
jsx
import '@gomagaming/top-competitions'
import { useEffect, useRef } from 'react'
function TopCompetitions({ 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
ref={ref}
onGoma:top-competition-select={(e) => console.log(e.detail)}
/>
)
}Vue 3
vue
<script setup>
import '@gomagaming/top-competitions'
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 ref="el" @goma:top-competition-select="onCompetitionSelect" />
</template>Accessibility
role="group"(labelled by the section title) wraps the loaded rows / tiles;role="button"+tabindex="0"+aria-current="true"on the selected row / tile.- Keyboard parity: Enter / Space activate selection.
- Row / tile
aria-labelis the competition name. - Skeleton placeholders are
aria-hidden="true". - The
'tiles'trophy glyph isaria-hidden: the band's<h2>carries the name. 'tiles'disables its skeleton pulse and tile transitions underprefers-reduced-motion: reduce.
Source layout
| Layer | File |
|---|---|
| Element class | packages/top-competitions/src/TopCompetitionsElement.js |
| App wrapper (variant switch) | packages/top-competitions/src/TopCompetitionsApp.vue |
'list' variant | packages/top-competitions/src/components/TopCompetitions/{TopCompetitionsList,TopCompetitionItem,TopCompetitionFeaturedItem}.vue |
'tiles' variant | packages/top-competitions/src/components/TopCompetitionTiles/{TopCompetitionTilesRail,TopCompetitionTile,TrophyIcon}.vue |
'tiles' chrome (plain CSS) | packages/top-competitions/src/styles/tiles.css |
| Shared selection + event | packages/top-competitions/src/composables/useCompetitionSelection.js |
| Tile derivations | packages/top-competitions/src/composables/useCompetitionTile.js |
| Source composable | packages/sports-domain/src/composables/ui/useTopCompetitionsSource.js |
| RPC | packages/sports-domain/src/api/everymatrix/modules/betting.js (getTopCompetitions) |
What's deferred
- Real subscribe path. Backend topic for live top-competition updates not yet known;
dataSource: 'subscribe'falls back to one-shot RPC.
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-backgroundSecondary/--goma-alertError/--goma-highlightPrimaryContrastare now registered inthemeTokens.js(already correctly documented in the Theming table above).Events:
goma:retrywas 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 differentlocalevalues on one page is not currently isolated per instance (a known constraint, not fixed in this pass).Configuring before the definition loads. Assigning
config(or any property) while the element is still an unknown element is safe: the value is replayed through the widget when it upgrades. That matters when loading from the CDN, whose loader imports widget modules asynchronously, so your script usually runs first. On release sets at 0.34.0 and earlier the value was silently discarded and the widget mounted with no configuration and a clean console: there, awaitwindow.__gomaCdn.readyfirst, or configure through attributes. See Setting a property before the widget's definition has loaded.
See Integration Caveats for the cross-widget picture.
Changelog
Current release: 0.37.11. Every GomaPulse package shares one version number and they are published together, so a release that changes nothing here still bumps this widget. Only the releases that changed it are listed below.
Every release that changed this widget, newest first. The latest is open; the rest expand.
0.37.9
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
competitionsreadsnulluntil the new sport answers. If no answer lands withinnetwork.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.rpcTimeoutMsbounds each try. A provider error now counts as a failed try:getCompetitions,getTopCompetitionsandgetAllLocationsreject 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 eventgoma: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
<goma-suggested-bet>:custom-elements.jsonnow lists the defaults for theicon-base-urlattribute ('') and theflagBaseUrlproperty (the built-in flag CDN).- Playground theme-token lists (
./playgroundexport) now match what each widget paints:<goma-hero-card>now also lists--goma-favorites(the favourite star), alongside--goma-backgroundOdds(the odds buttons, painted by the shared odds button);<goma-top-competitions>drops--goma-iconSecondary, which it never paints (the sport glyph uses--goma-iconPrimary);<goma-highlight-card>and<goma-pros-choice>add--goma-backgroundBorder, the colour of their loading skeleton bars. <goma-live-chat>: on a browser that cannot format compact numbers, the participant count now reads like the normal compact output (25K,2M, and1Mfor 999,999) instead of keeping a trailing.0(25.0K).
- Top competitions lists only show competitions that have games. The new
contextLiveStatusprop ('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 fillsnumberOfCompetitions: 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.competitionHasGamesis exported from@gomagaming/sports-domain. <goma-top-competitions>: thecustom-elements.jsondescription ofgoma:top-competition-selectnow says when the event fires: a tap on an entry that is not already selected, or a host write of a different value after mount. A tap on the selected entry emits nothing, and a value set before mount is applied silently. Clearing the selection emitscompetitionId: null, so the event type now readsstring|number|null. No behaviour change. The docs page gains "Keeping the selection across pages", for hosts that render the list inside each routed page.
0.37.0
<goma-top-competitions>gains anappearanceobject (structure only). For the'list'variant:panelPaddingY,panelPaddingX,panelRadius,rowSpacing,rowPaddingX,rowGap,rowRadius,rowFontWeightandfeaturedRowHeight. For the'tiles'variant:headHeight,titleFontSize,titleFontWeight,tileHeight,tileGapandlabelLines, which together allow fixed-height tiles with a single-line name. Unset, each keeps the current value, so the default look of both variants is unchanged.
0.36.1
Playground manifests (
@gomagaming/<widget>/playground) now offer a control for every setting each widget really has. Settings that cannot be a form field (raw provider data, read-only state mirrors, props a widget never reads) are recorded with a reason in the repo instead of being silently missing.Controls that never reached the widget are fixed:
- The betslips'
placeBetHeadersJson/placeBetBodyJson/quickAddAmountsJsonare nowplaceBetHeaders/placeBetBody/quickAddAmounts. - The time-filter and filters-settings ticks editors set
ticks. - live-chat's layout control writes
position, the config key the element reads. - The sports-navigation widgets drop the unread
sportsJson. - JSON-typed defaults are real objects and arrays instead of serialized strings. The top-competitions
highlightedImagesstring default was indexed character by character and requested bad image URLs on every load. The missions-hubemptyStateImagesillustrations now show.
No widget runtime behaviour changes.
- The betslips'
0.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 afollow*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 onwindow, which breaks as soon as a*Selectorprop 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/coregains the shareduseSelectionEmitcomposable 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 andnulldescriptors, 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>inshowApplyButtonmode 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>'sisFeaturedis now derived fromhighlightedImagesand 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 gainsresetCompetitionOnSportChange(defaulttrue), matchingresetRegionOnSportChangeon<goma-region-filter>: it clears the selection on asportIdchange, 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.
deleteon a non-configurable own property throws, and module code is always strict, so a host that usedObject.defineProperty(el, 'config', { value })before the widget loaded (legal, andconfigurabledefaults tofalse) madeconnectedCallbackthrow. The element never mounted at all: an uncaughtTypeErrorand 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-selectcarried two "unknown" sentinels.top-competitions-pillsdescribed an unresolved name as''whilecompetitions-listandtop-competitionsusednull. Three widgets emit that one event and a host wires a single listener for it, so the difference landed on every consumer.nullthroughout now.isFeaturedwas derived in two places. The predicate lived in bothuseCompetitionSelectionandTopCompetitionsList.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.
SbIconresolves a bundled SVG by name and otherwise falls through to an<img>. For a sport id the feed ships before its artwork exists (e-Volleyball, in the sports carousel and in the "Sports available" list) that<img>404s, and the old error handler only faded it toopacity: 0.2, which leaves the broken-image glyph on screen at 20%. A failed<img>is now dropped from the render instead.What stands in is the sport's family glyph: every e-sports category falls back to the e-sports icon, which is a real icon rather than "no icon". The family is read from the sport list the widget was already fed (the parent whose
childrenIdsnames the category) so no id list has to be kept in step with the provider, and a flat feed that names no parent simply falls back to the neutralnoiconglyph as before. The newsportIconNameshelper in@gomagaming/coreis what the sport tiles, the picker's trigger pill and the settings modal's sport row all resolve through.A host serving its own sport artwork keeps priority, and keeps it whether or not it configured a base URL:
${iconBaseUrl}/icons/sports/dark_mode/{iconId}.svgwhen one is set, and the origin-relative/icons/sports/dark_mode/{iconId}.svgwhen it is not.iconBaseUrlhas no value that means "my own root" (''and'/'both read as unset), so a host serving icons from its root keeps working with no configuration at all. A stand-in is considered only once that request has failed.Every widget that draws a sport glyph through
SbIconinherits the change, which is why they are all listed here: the two sports pickers, the settings modal, both top-competitions widgets, the sport banners, the game-details header, the suggested-bet legs, the My Bets cards, the two betslips that embed them, the highlight card and the Pros' Choice card.The family is looked up across the whole sport list the feed sent rather than the slice on screen. In
rpc/subscribemodeuseSportsSourcemirrors the betting store filtered to the current context (LIVE vs popular), and a parent category carries no events of its own, so it is routinely missing from that slice: every e-sports child would have fallen back to the neutral glyph. The unfiltered list is mirrored alongside it asstate.sportsCatalog, andsportIconLookupList(new in@gomagaming/core) is what the tiles, the trigger pill and the settings row read.mapSportalso refresheschildrenIdson an existing entry: the list was written only when the entry was first created, so a family relation that arrived on a later dump, or a child added to one. Never reached the widget.
0.34.0
Stop a region list fetched for the wrong sport from overwriting the right one, and parse numeric strings assigned to numeric widget properties instead of silently replacing them with the widget's default
A
<goma-region-filter>configured for any sport other than Football issued two location requests instead of one: its own, correctly scoped to the configured sport, and a second one (scoped to Football) that nothing had asked for. Both wrote the same place, so the pills the widget rendered were whichever response happened to come back last. The same race reached<goma-competitions-list>, which groups its competitions by the same region data.The unrequested request came from the per-widget boot routine, which fetched locations with no sport at all and so fell back to sport
1. Locations are scoped to a sport on the server, and boot has no sport to scope them by, so it no longer fetches them: the only readers of that data are the widgets above, and each of them already fetches it for the sport it is showing.That removes the collision at its source. As a second line of defence, a locations payload now records which sport it was fetched for, and a widget ignores one belonging to a sport it is not showing. This matters whenever two scoped fetches can overlap (switching sport while a request is still in flight, for instance) and it is deliberately narrow: a payload with no recorded sport is still rendered, so the guard can only ever suppress data it can positively identify as belonging to somewhere else.
Separately, every numeric property on every widget (
sportId,skeletonCount,pageSize,spaceBetween,autoplayIntervaland the rest) used to accept a JavaScript number and nothing else. A numeric string was not parsed; it was discarded and quietly replaced by the property's default value.el.sportId = '8'therefore left the widget on sport1and fetched the wrong data, with nothing logged to say so. Three ordinary integration paths hand over a string: a router parameter, a value read out of a CMS or JSON configuration, and a framework re-render that stringifies its props. Numeric strings are now parsed. Values that are not a finite number and not a numeric string (null,undefined,'', booleans, objects,NaN,Infinity) still fall back to the default, as before. HTML attributes (sport-id="8") and theconfigobject were never affected by this and are unchanged.
0.32.0
Widgets no longer dispatch a bare
errorevent alongsidegoma:error.Every factory-built widget's app-level Vue error handler dispatched two events for the same failure:
goma:error, and an un-namespacederror. The bare one is gone;goma:erroris unchanged and still carries the same{ message, code, component? }detail.Like every GomaPulse event it was
composedand bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had onwindow.addEventListener('error', …): as aCustomEventwithmessage,errorandfilenameallundefined, since those areErrorEventfields aCustomEventdoes not have. A host with error reporting installed received a malformed error record it could neither read nor attribute.That had been written off as harmless on the grounds that the handler behind it could never fire. It can. Vue begins its
errorCapturedwalk at the throwing component's parent, so theonErrorCapturedboundary each widget installs in its own root component cannot see that component's own errors, and being the root, there is nothing above it to catch them either. Anything raised in a widget's root scope (its setup, its render, its lifecycle hooks, its watchers) skips the boundary and reaches the app-level handler every time. Only errors from child components are caught by the boundary, which is what it is for: those show the in-shadow banner and emitgoma:retry.Hosts listening for
goma:errorneed no change. A host listening for bareerroron a widget element should move togoma:error; it was documented as unreachable, so in practice it delivered nothing until now.
0.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'screateWidgetElement()factory, andlive-chat's andmessenger's own element classes, now guard against importing a widget package in a non-browser context (SSR): theHTMLElementsubclass was previously defined at module top level in every widget package, so importing any of them under server-side rendering threw aReferenceErrorimmediately.
Per-package fixes:
favorites: addedsrc/themeTokens.jsand a:hoststylesheet (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'sdataSourceaway fromrpc/subscribenever released the open topic.filters-settings: fixed a dead:host{display:inline-block}CSS rule (the shared element factory's inlinedisplay:blockalways won), so the widget always rendered full-width instead of the intended compact inline trigger.game-details: added amin-heightfallback for the inert/no-eventIdstate 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. CorrectedthemeTokens.jsregistries that had drifted from actual token usage, added missinggoma:retryEvents-table rows, and other small doc/registry corrections.region-filter: added a missingtypeof window !== 'undefined'guard on a window listener.live-chat: correctedcustom-elements.json'smembersarray (missingtheme/configaccessors, 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 indocs/guide/integration-caveats.mdas needing human visual verification before release.Visual corrections across the game card, the casino rail and every carousel arrow.
- Game cards. Team names on the large card variant were clipped: an 18px font was set on a 16px line box, which sheared the ascenders and the descenders of names like
Puerto Rico (W). The line box now fits the font's ink, and the home row's bottom margin is pulled in so the painted distance between the two team names is unchanged. Affects every widget rendering the shared game card, plus<goma-highlight-card>'s team rows. <goma-casino-games>: the'simplified'tile now matches its design source: a 23:30 portrait tile, and the name/provider block and play glyph laid out as one centred stack so the gap between them no longer changes with the number of lines the game name wraps to.- Navigation arrows:
<goma-casino-games>,<goma-multi-banners>and<goma-sports-navigation-horizontal>now draw one identical arrow: a 24px disc of--goma-allWhiteat 60% carrying a full-strength--goma-highlightSecondaryContrastcaret on a 1px stroke. Previously the three diverged in disc colour, glyph size (14–18px) and stroke weight. Hosts that restyled these arrows through--goma-textPrimaryor--goma-backgroundBordershould move to the two tokens above; both are declared in each widget's theme-token registry and documented in its theming table. <goma-top-competitions>: the list header's type now matches the section headers around it instead of running a size larger at a lighter weight.
- Game cards. Team names on the large card variant were clipped: an 18px font was set on a 16px line box, which sheared the ascenders and the descenders of names like
0.28.0
<goma-top-competitions>: add atilesvisual variant.Purely additive. A new
variantprop (variantattribute) picks the presentation:'list'(the shipped sidebar column of rows, unchanged and still the default) or'tiles', a mobile-first trophy title band above a horizontally scrollable rail of 140px competition tiles (sport glyph + circular country flag over a two-line-clamped name).The data plumbing is untouched and shared: same
popularTournamentsRPC, samecompetitionsarray, sameselectedCompetitionId, and the samegoma:top-competition-selectpayload. Now emitted from a single composable so it cannot drift between the two front ends. Any unrecognisedvariantvalue renders'list', so existing embeds are unaffected whether or not they set it.Notes for the
tilesvariant: it renders no outer panel (the host owns the page background and should give it a full-bleed slot, the rail supplies its own 16px gutters), it has no featured-card rendering sohighlightedImagesis ignored andisFeaturedis alwaysfalse, and it adds no new theme tokens.Also fixes a latent skeleton bug in both variants: a host that assigned
el.competitionsbefore the element connected would have been stranded on pulsing placeholders, because the loaded-state watch only fired on a subsequent change. It is now seeded from the state present at mount.
0.26.0
Stop publishing source maps and package source.
dist/*.js.mapwas emitted by the shared Vite preset and swept into every tarball byfiles: ["dist/", "src/"], with the original source inlined insourcesContent.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 withsourcemap: falseandfilesno longer carriessrc/.@gomagaming/corekeeps the two paths its export map actually points at:src/styles/main.cssandsrc/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.15.0
UI fixes from the a separate change design review (Sports Pages, items 1.1–1.19):
- Suspended odds pill text uses
inputTextTitleDisabled(legible on lightened themes) and game-card team/player names render at 16px. - Brand font: Open Sans is bundled and registered via the
document.fontsAPI so widgets paint the intended typeface inside the shadow root. - Game-card market pill colours corrected (live
separatorLine, prelivehighlightPrimary); 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.
- Suspended odds pill text uses
0.9.2
- Surface colours: the widget panel now uses
backgroundSecondaryand the competition rows (plus the featured card and loading skeleton) usebackgroundPrimary, so the rows read as recessed on the panel.
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 onvenueIdto build the per-region groups. - Three data sources via
dataSource:'prop'(host-fed viael.competitions/el.regions),'rpc'(one-shotgetCompetitions+getLocationson connect; default),'subscribe'(placeholder, falls back to RPC). sportIdis mandatory (defaults to 1 = Football). Pair with the sports picker viafollowSportPickerto re-fetch on sport change.- Emits
goma:competition-selectwith the same payload as<goma-competition-filter>: downstream<goma-events-horizontal>withfollowCompetitionPicker: truecooperates 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.
- Reuses
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
bannersarray (no CMS). For a sport banner, settingeventId(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), andgoma:outcome-select(sport odds tap → betslip, same shape as events-horizontal). - No new theme tokens; all colours map to existing
--goma-*tokens.
@gomagaming/sports-domainadditions (additive, no change to existing paths):- New action group
sports.bannerEvents(createBannerEventsActions): a multi-event subscription manager that accumulates events intobettingStore.events(skipStoreCleanup) so several event cards keyed by id coexist with live odds, unlike the singular-gamematchDetailspath. - Public exports for
useGameScoresTallOddsanduseGamePartsAndTimesTallOddsso other widgets can render live scores/clocks from a store event.
- Sport cards adapt to event state: live cards show the running clock/part and score (
0.8.0
New widget
<goma-top-competitions>: sidebar "Top Competitions" list.A vertical list of competition rows (sport icon + region flag + name) with a section title above. Selecting a row emits
goma:competition-selectwith the full payload (competitionId, competitionName, sportSportId, competitionVenueId, previousCompetitionId).- Three data sources via
dataSource:'prop'(host-fed),'rpc'(one-shotpopularTournamentscall on connect; default),'subscribe'(placeholder, falls back to RPC). - Sport scope set once via
config.sportId; no picker-chain wiring. - Sport icons resolved off
iconBaseUrl(/icons/sports/dark_mode/{sportId}.svg), flags offflagBaseUrl({venueId}.png). Same conventions as the rest of the filter family. - New composable
useTopCompetitionsSourcein@gomagaming/sports-domainwraps the existingoddsApi.betting.getTopCompetitionsRPC.
Released alongside the rest of the lockstep family at a minor bump.
- Three data sources via