<goma-favorites>
Favourites page widget: a lazy accordion of the signed-in user's favourited competitions and events, with market-rich event cards. Favourite stars are server-authoritative (persisted through the EM user-data-service) and mirrored into a cross-widget shared cache, so a star tapped here or on any other favourites-aware widget on the page stays in sync without extra wiring.
Element tag
html
<goma-favorites></goma-favorites>Give it a sized container: this widget has no min-height fallback of its own (unlike <goma-betslip-sidebar>/<goma-my-bets>, which ship an explicit 360px floor for the same "fills its parent" contract), so an unsized host renders it collapsed.
Installation
@gomagaming/favorites is a scoped private package. See Installation for the one-time .npmrc setup required to consume it.
bash
npm install @gomagaming/favoritesYou also need the shared peer dependencies installed at your app level:
bash
npm install vue pinia vue-router vue-i18n @vueuse/core autobahn-browser swipervue, pinia, vue-router, vue-i18n, @vueuse/core, autobahn-browser, and swiper are declared as peerDependencies so a consumer with multiple @gomagaming/* widgets gets exactly one copy of each.
Configuration
Assign a config object to the element's config property:
js
const el = document.getElementById('fav')
el.config = {
socketUrl: 'wss://api.example.com/v2',
socketRealm: 'www.example.com',
favoritesApiBaseUrl: '/em-bets/user-data-service',
locale: 'en',
}| Key | Type | Description |
|---|---|---|
socketUrl | string | WAMP socket URL. Needed both to resolve live odds on event cards and (when the widget is fed a raw sessionId) to bind the loginWithCeSession exchange. |
socketRealm | string | WAMP realm. |
favoritesApiBaseUrl | string | Base URL of the user-data-service that persists favourites. Can also be set as the favoritesApiBaseUrl JS property / favorites-api-base-url attribute. |
playerApiBaseUrl | string | Base URL for the REST session probe (GET /v1/player/session/player). Falls back to apiBaseUrl if unset. Only needed on the raw-sessionId path: omitting it leaves the session probe unable to run and the widget stuck in loading until the 15s resolve timeout drops it to logged-out. |
gameLaunchBaseUrl | string | Base URL for the gamelaunch StartInfo REST call that exchanges the REST sessionId for a WAMP ceSession. Only needed on the raw-sessionId path. |
ucsOperatorId | string | Operator id. Fallback for the operatorId prop (either surface works) used both for the CE-login exchange and as the default operatorId fed to the favourites REST client. |
locale | string | i18n locale (e.g. en, fr, pt). Defaults to 'en'. |
theme | object | CSS variable overrides: see Theming. |
messages | object | i18n overrides per locale: see i18n / message overrides. |
sessionMaxAgeMs | number | Max age (ms) before the cached shared session is proactively re-exchanged. Defaults to 1800000 (30 min). |
sessionRetryBackoffMs | number | Base backoff (ms) for the CE-session exchange retry (exponential over 3 attempts). Defaults to 500; 0 disables the wait. |
sessionStepTimeoutMs | number | How long (ms) one CE-session exchange step (the StartInfo request or the loginWithCeSession call) may stay unanswered. A StartInfo timeout is retried; a loginWithCeSession timeout replaces the shared socket (at most twice per session). Defaults to 5000; 0 disables the deadline. |
debug | boolean | Verbose console logging. Defaults to false. |
Data sources
The widget resolves its own authenticated identity, then reads two data paths in parallel once a session is available.
Session
Two ways to give the widget a session, both set as JS properties (recommended) or HTML attributes:
Raw session id: feed
sessionIdand the widget resolves the authenticated identity itself: a REST session probe (GET {playerApiBaseUrl}/v1/player/session/player) followed by the gamelaunchStartInfo+ WAMPloginWithCeSessionexchange, on its own socket. Exactly like the betslip widgets. It derives theuserIdand the long-form REST session token internally.jsel.sessionId = 'abc123' el.operatorId = '2838'Pre-resolved identity: if the host already holds the resolved values, feed them directly to skip the exchange. Direct props win over the derived values.
jsel.userId = '12345' el.operatorId = '2838' el.sessionToken = 'Bearer …'
With a raw sessionId, the widget is signed in once the player REST check confirms it. It does not wait for the socket login. This applies only where <goma-favorites> is on the page: it is the widget that runs that REST check for favourites. On a page without it, the star widgets (events list, game details, hero card, highlight card, pros' choice, competitions list, banners) save star taps only once the socket login has finished in the shared session, for example through the betslip. Each favourites request carries the headers x-operatorid, userid and x-sessionid, plus x-sessiontoken only when a socket token exists for that same session id.
With no session input at all the widget renders the logged-out state. While a fed sessionId is still being exchanged it renders loading: a 15s resolve timeout falls back to logged-out if the exchange never completes (e.g. a stale sessionId).
The resolved session (userId + long-form token) is cached in the cross-widget goma:session:v1 envelope, so one exchange per origin serves every @gomagaming/* widget on the page. See Shared state for the full envelope contract and Integrates with below for this widget's participation in the session-death contract.
Favourites REST + server persistence
On a resolved session the widget calls the user-data-service (favoritesApiBaseUrl) to fetch the user's favourite tournament ids and event ids, seeding the shared goma:favorites:v1 cache. Persistence is a tab-level singleton, not tied to this widget instance: the underlying sync arms once (first valid arm wins) and pushes add/remove deltas for the whole tab for every star tap (a change to the shared cache that is not a tap, such as one made by another tab or by a player change, is never sent), so a star toggled by any favourites-aware widget on the page (even one with no <goma-favorites> mounted) persists to the server.
The list is loaded in two requests, one after the other: the favourite tournaments first, then the favourite events. If a load fails, the widget tries again after 1 s, 3 s and 10 s. After the last failure it shows the error state with a Retry button.
Star changes go to the server one at a time. A tap made while an earlier one is still saving is sent after it. A tap made while the list is loading is kept.
Ids are kept as strings end-to-end. EM tournament/event ids are 18-digit snowflakes that exceed Number.MAX_SAFE_INTEGER; a host or integration that runs Number() on one silently corrupts it. Every id in the shared cache, the REST payloads, and the emitted event details is a string.
WAMP live resolution
Once the socket connects, the widget subscribes to the matches-aggregator feed for each favourited-competition section that's expanded (lazy, one subscription per open section, released on collapse), and to a single batched subscription covering every favourited event (capped internally at ~200 ids per batch). Live odds tick into the cards as the feed pushes updates, and the cards' live score, clock and status update live too. Without socketUrl the widget still renders: events show their last-fetched odds and the loading skeleton resolves as soon as the REST seed completes.
A competition section's feed sorts by UPCOMING, so it carries every upcoming and live match of the competition, not only the ones the provider marks as popular (still capped by maxEventsPerCompetition).
A competition section shows every event its own feed delivers, including events played in one of the competition's sub-tournaments (a group, a league phase). If you favourite both a competition and one of its sub-tournaments, the shared event shows in both sections.
A favourite event is listed first inside the favourite competition that contains it, even when it is played in one of that competition's sub-tournaments, and it then gets no section of its own. An event that no favourite competition contains gets its own section, titled with its tournament.
The two subscriptions do not always carry the same markets for one event. A favourited-competition section uses the matches-aggregator feed, and the favourited-events list uses the user-events-aggregator feed. Each feed chooses its own set of markets for an event, so the same match can show a different headline market in the two places. For example, a pre-match basketball game can lead with Over/Under in its competition section, while its card in the favourited-events list leads with Home Away because that feed does not send the Over/Under market for it. Both cards follow the same market ranking (see Market ordering within a row), including the separate rankings for live and pre-match events. Only the markets they receive are different.
Props / attributes
| Property | Attribute | Default | Description |
|---|---|---|---|
sessionId | session-id | '' | Raw REST session id. See Session. |
userId | user-id | null | Authenticated user ID. Pre-resolved override; skips the exchange. |
sessionToken | session-token | '' | Long-form REST session token. Pre-resolved override; skips the exchange. |
operatorId | operator-id | null | Operator identifier. Falls back to config.ucsOperatorId when unset. |
favoritesApiBaseUrl | favorites-api-base-url | '' | Base URL of the user-data-service. |
flagBaseUrl | flag-base-url | the OM CDN path | CDN base for country flag images. Resolved as {base}{venueId}.png. |
emptyStateImage | empty-state-image | '' | Illustration URL shown when the user has no favourites. |
maxEventsPerCompetition | max-events-per-competition | 10 | Maximum event cards per competition section (pinned favourites + aggregator fill). |
showFades | show-fades | true | Render the edge fades on each event'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. |
size | size | 'default' | Card size variant for the favourite 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. |
teamNameFormat | team-name-format | 'short' | Team names on the favourite event cards. '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. |
cardWidth | card-width | '' | Fixed width for every favourite event card: 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. |
followSportPicker | follow-sport-picker | false | Opt-in. Narrows the accordion to the sport selected by a page sport picker: see Integrates with. |
sportPickerSelector | sport-picker-selector | '' | Optional CSS selector scoping followSportPicker to a specific picker element (matched against the composed event's host). Empty → follow any picker. |
followFilters | follow-filters | false | Opt-in. Applies the page's time / default-market / odds-range filters to the favourite event cards: see Integrates with. |
Common props (all @gomagaming/* widgets):
| Property | Attribute | Default | Description |
|---|---|---|---|
config | - | {} | Full runtime config object (JS property only). |
locale | locale | 'en' | i18n locale code. |
theme | theme | {} | CSS variable overrides (JS object or JSON attribute). |
debug | - | false | Verbose console logging (JS property only, set via el.debug = true; there is no debug attribute). |
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.
Events
All events use bubbles: true and composed: true to cross the Shadow DOM boundary.
| Event | Detail | Emitted when |
|---|---|---|
ready | {} | Widget mounted, initial render done. |
goma:favorite-toggle | { type: 'competition' | 'event', id: string, isFavorite: boolean, eventId?: string } | A favourite star was toggled. type: 'competition' fires from a section header star (FavoriteCompetitionSection); type: 'event' fires from the star built into each event card (shared with <goma-events-horizontal>'s game-card star) and additionally carries eventId (same value as id). id/eventId are always strings: see the ids note in Data sources. |
goma:login-required | { reason: 'favorite', type: 'competition' | 'event', id: string } | A favourite star is tapped while logged out; the widget does not change the favourite: the host should open its login flow. |
goma:favorites-browse | { target: 'upcoming' } | The upcoming events link in the empty state was clicked. The widget owns no navigation, so it reports the intent and leaves the destination to you: point it at whatever your page uses to show upcoming fixtures. |
goma:event-navigate | { eventId: string, homeTeam: string, awayTeam: string, competitionName: string } | User taps an event card's See all control to navigate to the event detail view. |
goma:outcome-select | { bettingOfferId: string, outcomeId: string, outcomeName: string, odds: number, eventId: string, marketId: string, bettingTypeId: string|number, eventPartId: string|number, line } | User taps an odds button to add the outcome to the betslip. |
goma:session-ready | { userId: string | null, username: string | null } | The session finished resolving into an identity that can actually place a bet. Fires once per resolved token per mount, and again whenever a new token is minted (max-age refresh, WAMP reconnect): handlers must be idempotent. |
goma:session-expired | { reason: string, invalidationId: number } | The player's session stopped working: a 401/403 on a favourites read/write, the session probe, the CE-login exchange, or a rejection raised by any other session-aware widget on the page. Only the host can fix it (re-authenticate, set a fresh sessionId). Dedupe on detail.invalidationId. |
goma:error | { message: string, code: string, component?: string } | Two paths. Connection layer: the widget's own WAMP session/socket 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. Its Retry button clears the error and emits goma:retry, below. |
Integrates with
Listens for, gated by two opt-in follow* props: both default false:
followSportPicker(defaultfalse): listens forgoma:sport-selectandgoma:filters-apply(readingdetail.sportIdoff either), folding the selected sport into a post-fetch, client-side filter that narrows which accordion sections render. Off by default because favorites is a page widget and shouldn't silently narrow just because an unrelated picker sits elsewhere on the page. Scope it to one picker withsportPickerSelector.followFilters(defaultfalse): listens forgoma:time-selectandgoma:filters-apply, applying the time window, default-market swap, and odds-range filter to the favourite event cards (event-level, not section-level).
Commonly pairs with <goma-sports-navigation-horizontal> / <goma-sports-navigation-dialog> (sport), <goma-time-filter> (time), and <goma-filters-settings> (the mobile settings-modal funnel for both).
The star on a card's odds row writes selections into the shared betslip envelope goma:betslip:v4 via useSharedBetslipSync({ subscribeToState: false }): identity fields only, so favourite selections reach any <goma-betslip-floating> / <goma-betslip-sidebar> on the page with full market data (not just the lossy goma:outcome-select window event).
Reads and writes goma:favorites:v1 ({ competitions: string[], events: string[] }): both the competition-section star and the per-event card star share this envelope with <goma-events-horizontal> (showFavorite / showTopBar), so a star tapped on either widget lights up the matching card everywhere. Resolves its own identity into goma:session:v1 (see Session) and participates fully in the session-death contract: a 401/403 on a favourites call marks the shared session invalid and this widget falls back to its own logged-out state along with every other session-aware widget on the page.
See Cross-widget wiring and Shared state for the full rewrite mechanics and envelope field-by-field contracts.
Empty / loading / error states
Five states are managed internally:
- Logged-out: no session input at all (no
sessionId, no direct identity pair), a fedsessionIdthat failed to resolve within 15s, or the shared session was authoritatively rejected. Renders afavorites_logged_out_title/favorites_logged_out_bodyprompt. - Loading: a
sessionIdis being exchanged, or a resolved session is awaiting its first server seed. Renders thefavorites_loadingtext. Shown instead of logged-out during the mount grace window and while a session input is pending and unresolved, so a signed-in user never sees the sign-in prompt flash during resolution. - Empty: the server seed completed and the user has no favourited competitions or events. Renders a bordered card with a star glyph above the
favorites_empty_title/favorites_empty_bodytext. The host-fedemptyStateImageproperty is an optional override: when set, it replaces the glyph with your own illustration. - Ready: at least one favourited competition or event. Renders the accordion.
- Error: the favourites could not be loaded after three retries. The widget shows a message (
favorites_load_error) and a Retry button. Retry also emitsgoma:retry.
Within the ready state, a favourited competition whose tournament has been removed or expired from the feed (no metadata, no pinned events) is dropped as an orphan rather than rendered as a nameless row; a section still resolving is kept in place so a real one never flickers out. When followSportPicker is active and every resolved section excludes the active sport, the accordion shows a favorites_sport_empty inline message instead of the whole-account empty state.
A render error shows a themed banner for 8 seconds with a dismiss button and a Retry button (retry key) that clears the error and emits goma:retry.
Theming
Override any CSS variable by passing a theme object. All tokens are namespaced with --goma-* and applied as inline styles on the host element. The prefix is added automatically: pass just the token name:
js
el.theme = {
backgroundPrimary: '#0f111a',
highlightPrimary: '#f59e0b',
favorites: '#f59e0b',
}See the Theming guide for the complete token reference, default values, and full theme examples.
Tokens used by this widget
| Token | Default | Used for |
|---|---|---|
--goma-backgroundPrimary | #15162a | Page / widget background |
--goma-backgroundCards | #2a2f3e | Event card + competition-section header background |
--goma-backgroundSecondary | #15162a | Empty-events box background + "See all" card fill + event-rail edge-fade target colour |
--goma-backgroundTertiary | #3d425a | "See all" card border |
--goma-backgroundOdds | #1f2236 | Odds button background |
--goma-backgroundBorder | #3d425a | Card and section borders |
--goma-textPrimary | #ffffff | Primary text |
--goma-textSecondary | #ade4ff | Muted / secondary text |
--goma-textOdds | #ffffff | Odds value text |
--goma-highlightPrimary | #29f2ff | Accent ring / active odds |
--goma-highlightPrimaryContrast | #062225 | Text on the accent: the render-error banner's retry button |
--goma-favorites | #f59e0b | Favourite star fill |
--goma-alertError | #ed4f63 | Render-error banner background |
--goma-backgroundSecondaryis a judgment call, not a settled fact. It serves two conflicting roles in this widget: the empty-events box's own fill, and the market-rail edge-fade's target colour (see theshowFadesconfig row above). A fade only reads as "the rail scrolls" and not as a visible band if it matches whatever surface is actually painted behind the widget.FavoritesApp.vue's root paints--goma-backgroundPrimary(#15162a), so the default here was set to match that: prioritizing the fade working over the empty-box having a shade visually distinct from the page background. Whoever visually verifies this widget should confirm the edge fade actually disappears against a real host background and that the empty-events box still reads as a distinct surface; overridebackgroundSecondarylocally if either doesn't hold for your host chrome.
Partial-palette hand-off, not full coverage. The tokens above cover this widget's own chrome, but each event card is rendered by
@gomagaming/ui's shared card components (PreLiveRegularOdds/LiveRegularOdds/OutrightRegularOdds/MarketRegularOdds, wired in viaEventHorizontalWrapper.vue): the same cards<goma-events-horizontal>renders. Those consume roughly 30 more--goma-*tokens this widget does not override, so they keep core's base orange/blue values unless a host sets them explicitly. Most visibly:--goma-buttonBackgroundPrimary(#ff6600) is the selected-odds-button fill. Tapping an odds button on a favourite event card will show core's orange, not this widget's cyan, until a host overrides it. Also unstyled:--goma-liveTag,--goma-cardBorderLineGradient1/2/3,--goma-liveBorder1/2/3,--goma-separatorLine,--goma-iconSecondary, and--goma-backgroundDisabledOdds/--goma-textDisabledOdds. This mirrors<goma-hero-card>'s documented partial-palette pattern (8 tokens shipped, not full coverage): it is not a bug, but a visual reviewer expecting a fully cyan-themed odds button will see orange and should not read that as the fix being broken.
i18n / message overrides
Override any bundled translation key via config.messages. The fallback chain is: custom key → bundled default → fallback locale (en) → literal key string.
js
el.config = {
favoritesApiBaseUrl: '…',
messages: {
en: {
favorites_loading: 'Loading your favourites…',
favorites_empty_title: 'Nothing starred yet',
},
},
}Keys used by this widget
| Key | Default (en) | Used in |
|---|---|---|
favorites_loading | Loading favorites | Loading state |
favorites_load_error | We could not load your favorites. | Error state |
favorites_empty_title | No events on your radar... | Empty state heading |
favorites_empty_body | Check upcoming events and save your favorites. | Empty state body |
favorites_logged_out_title | Log in to see your favorites | Logged-out state heading |
favorites_logged_out_body | Sign in to follow competitions and events. | Logged-out state body |
favorites_sport_empty | No favourites for the selected sport. | Sport-filtered accordion, no matches |
favorites_title | Favorites | Fallback section title when a tournament's name hasn't resolved yet |
add_to_favorites / remove_from_favorites | Add to favorites / Remove from favorites | Competition-section star aria-label |
add_favorite / remove_favorite | Add to favorites / Remove from favorites | Per-event card star aria-label (shared PreLiveRegularOdds / LiveRegularOdds cards) |
collapse / expand | Collapse / Expand | Section caret aria-label |
retry | Retry | Error banner retry button |
loading | Loading | Section skeleton aria-label |
no_events_available | No events available | A section with zero cards after filtering |
see_all | See All | Event card's See-all control |
x_markets | No Markets | 1 Market | {count} Markets | See-all control's market count |
Accessibility
- The competition-section star is a
<button>witharia-pressedreflecting favourite state and anaria-labelfromadd_to_favorites/remove_from_favorites; the caret button carriesaria-expanded+expand/collapse. - The per-event card star (shared with
<goma-events-horizontal>) carries anaria-labelcombiningadd_favorite/remove_favoritewith the matchup, and responds to Enter in addition to click. - Loading skeletons and the empty-events message use
role="status"with anaria-label/sr-onlytext so screen readers announce the state change; the render-error banner usesrole="alert". - Interactive controls carry
focus-visible:ring-2styling keyed to--goma-highlightPrimary. - In an 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.
Integration Caveats
SSR/webview: importing this package outside a browser previously threw immediately; fixed with a guard on the shared element factory.
Tokens/Theming: this was the only one of the 25 shipped widgets with no
src/themeTokens.jsand no CSS file at all, so it silently rendered core's base theme instead of its documented dark-navy/cyan/amber theme. Created both, matching the documented defaults. One token's default (--goma-backgroundSecondary) serves two conflicting roles in this widget; resolved in favor of the widget's own documented edge-fade behavior.Sizing: documented the missing
min-heightfallback (see above).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.11
- A session-aware widget that is mounted again later in the same page load (for example favourites after the player opens an event and comes back) now keeps the valid session. Before, when the host set
sessionIdafter the mount, the widget skipped the session check and stayed signed out, and favourites showed "Log in" after 15 s.
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
- Favourites: the list always belongs to the player who is signed in now, quick star taps are no longer lost, only real star taps are sent to the server, a failed load is tried again and then shows an error with Retry, and
<goma-favorites>no longer waits for the socket login. Star taps on game details, hero card, highlight card, pros' choice, competitions list and banners are now saved to the server whenfavoritesApiBaseUrlis set and a signed-in session is available on the page (on a page without<goma-favorites>, that means the socket login has finished). A late rejection of a session that already ended (replaced by a new login, or signed out) no longer signs out the current one; a rejection of a new session still does.
0.37.6
Read the live score in any feed language. The whole-match score row is now found by the part id of the match, and tennis sets and games by their scoring unit, not by the translated part name. In the game cards,
<goma-game-details>,<goma-hero-card>and<goma-multi-banners>, a match on a language with no known part name shows its score. In the game cards and<goma-game-details>, live tennis on a Spanish feed also shows the current game points in place of the sets won. A match with no whole-match row, such as e-Tennis, shows the broadest part still running. Spanish set, game, quarter and inning names now fill the part columns.The live game card and the
<goma-game-details>header now read one shared score, so they show the same thing. For a live match the feed sends no score for, they show the last score seen for that match. With none, they draw no score box, at any point in the match, and show the current part. A real0 0still draws. A live<goma-multi-banners>sport card with no score now shows none, where it showed0 - 0. A host that feedseventswith nohomeScoreorawayScoregets no score box in place of0 0: send both as0to 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 as7th Game (2nd Set), where they showed the status, such asIn Progress. In<goma-game-details>,goma:game-endednow carrieshomeScoreandawayScorefor a match with no whole-match row, such as e-Tennis, where it carriednull.New
teamNameFormatsetting ('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-domainexports theparticipantNames(event, format)helper the cards use.
0.37.4
<goma-favorites>and<goma-competitions-list>gain acardWidthsetting (card-widthattribute) that pins every event card to a fixed width, as<goma-events-horizontal>already allows. Empty keeps the default width.An event that a feed delivers again now keeps its markets. Before, when a favourite competition was collapsed and opened again, or its time window or sport changed, the event card in
<goma-favorites>showed "Suspended" instead of its odds, because the markets were already stored and the event was built without them. The same fix applies to the banner, suggested bet, highlight and pros choice cards, which load their events the same way. An event only receives the stored markets that belong to it, and those markets take the state of the new data: a market that is open again is no longer shown as suspended, and an offer that the provider withdrew while the section was closed is no longer shown at its old price.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 card or banner that names an
eventIdnow finds its event by id. Before, the widget subscribed to the whole feed of the event's sport and looked for the event inside it, so an event outside that feed (a far-future match, for example) never resolved and its card stayed empty. Now the widget asks the provider for the events it needs, live or prelive, so any event the provider knows resolves, and its score, clock, odds and live status keep updating. The events feed carries no main-market ranking, so for each sport shown the widget makes one small extra request for it, to keep a banner's headline market stable when a market is suspended and offered again.<goma-multi-banners>,<goma-highlight-card>,<goma-pros-choice>and<goma-suggested-bet>no longer need asportIdto resolve an event. ThesportEventLimitproperty and thesport-event-limitattribute are deprecated and now do nothing. They are still accepted, so no existing page breaks.On the favourites page, live score, clock and status now update after the page loads. In the suggested bet recommendation mode the events also receive live match updates now; the card does not show a score, so nothing changes on it.
Sport banners in
<goma-multi-banners>can show the market name in a pill under the team names. SetshowMarketNameon the widget, or on one banner entry, to turn it on.A favourited competition in
<goma-favorites>now shows its events when they are played in one of its sub-tournaments (a group, a league phase). Before, such a section showed "No events available", and collapsing it left those events behind. A competition and one of its sub-tournaments can both be favourited, and an event then shows in both sections. A favourite event played in a sub-tournament of a favourited competition now shows once, at the top of that competition's section, instead of also getting a second section of its own.getTournamentsByIdsin@gomagaming/sports-domainnow returns each tournament'sparentId.A live event's markets are now ranked by the feed's live main-market list. A feed that carries both live and pre-match events declares one list for each, and they can put different markets first: for basketball, the pre-match list leads with Over/Under and the live list leads with Home Away. Before, every event was ranked by whichever list came first in the feed, so a live game could lead with the pre-match main market. This applies to the event cards in
<goma-events-horizontal>,<goma-favorites>and<goma-competitions-list>, and to the main market that<goma-multi-banners>,<goma-highlight-card>and<goma-pros-choice>pick when none is set.orderMarketsByMainMarketstakes an optional{ isLive }andresolveMarketSelectionan optionalisLiveoption for this. A feed that declares only one list ranks every event by that list, as before.Game cards: a suspended odds button now paints
backgroundDisabledOdds. The restingbackgroundOddsclass used to stay on the button and won the cascade, so a theme's suspended background never showed. The "Suspended" label shown when a whole market is suspended now readstextDisabledOdds, the same token as the suspended odds button, instead ofinputTextTitleDisabled(this reverses the token choice of a separate change). The suspended button's padlock is now the design's plain 14px padlock (core'slock.svg, its only user) instead of a padlock inside a refresh ring. Affects the game cards of events-horizontal, favorites, competitions-list, game-details, hero-card, multi-banners, highlight-card and pros-choice.
0.37.0
<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,liveTeamNameSizeandliveBadgeLeftRadius(odds row height and team name size at anysize, 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/uitake the matchingcardWidth,oddsGap,oddsHeight,teamNameSize,badgeLeftRadiusandpreliveCardStyleprops.<goma-favorites>and<goma-competitions-list>carry the same row component, so they are bumped with it.@gomagaming/coreis 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/quickAddAmountsJsonare nowplaceBetHeaders/placeBetBody/quickAddAmounts. - The time-filter and filters-settings ticks editors set
ticks. - live-chat's layout control writes
position, the config key the element reads. - The sports-navigation widgets drop the unread
sportsJson. - JSON-typed defaults are real objects and arrays instead of serialized strings. The top-competitions
highlightedImagesstring default was indexed character by character and requested bad image URLs on every load. The missions-hubemptyStateImagesillustrations now show.
No widget runtime behaviour changes.
- The betslips'
0.36.0
The flat 1px frame of the secondary game cards (additional-market, pre-live and outright) reads an optional
--goma-secondaryCardBordertoken, the same one for all three card types. Unset, it falls back tocardBorderLineGradient2, 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/coreis bumped for the new utility in its compiled sheet.<goma-suggested-bet>gains arecsysdata source that fills the card from the recommendation service, with popular recommendations for a signed-out visitor and personalised ones for a signed-in player. In this mode the card also gets a loading skeleton and an empty state (see below). Multiplerecsyscards on the same page now automatically divide up the recommendation pool instead of racing each other onto the same fixtures: each card claims the events its combo settles on, and a sibling card sharing the same{recsysBaseUrl, recsysOperatorId, sessionId}combination skips whatever is already claimed and fills its own combo from what's left, falling back to its empty state if too little is left over.recsysCandidatePoolSizenow counts candidates a card can actually use, reaching as deep into the ranked list as it needs to after excluding a sibling's claims, never the service's literal first N, so a rail of several cards no longer starves the later ones while each card still opens at mostrecsysCandidatePoolSizebatched-events subscriptions. No host configuration is involved. While therecsyspipeline (fetch → shortlist → resolve → subscribe) is still working, the card now shows a whole-card loading skeleton sized to match the finished card instead of leaving a bare gap in the rail;prop,rpcandsubscribeare unchanged and still render nothing until the host feeds abet, since in those modes the host (not the widget) owns whether a card exists at all.@gomagaming/sports-domaingains the REST client and the combo-assembly policy behind it, plus a generic events-by-id subscription (sports.eventsById) that both the new recsys driver and@gomagaming/favoritesnow share;favorites's own favourite-events feed was migrated onto it with no change in behaviour.@gomagaming/coregains theno_suggested_betlocale key the new empty state renders.<goma-suggested-bet>also gains two element properties,rankandstake, read as a fallback whenever the active bet does not already carry that field. A host-fedbet.rank/bet.stakealways wins. This closes the gap therecsysmode above opens: the recommendation service supplies neither, so without a host-fed fallback arecsyscard shows no rank badge and an empty placeholder for Bet and Winnings. The fallback is general rather than scoped torecsys: it equally patches a partial host-fed bet inprop,rpcorsubscribemode. The widget never derivesrankitself; a host supplies its card's own position in the rail.stakestays display-only in every mode, exactly like a host-fedbet.stake: the "Bet the same" CTA still never writes it into the shared betslip. Leaving both unset reproduces today's behaviour exactly.The
recsyscombo's per-match odds subscription now tracks the legs actually displayed, not the whole candidate pool. Each{matchId}/match-oddstopic carries the full market set for one match, so opening one per pool candidate to ever showrecsysLegCountof them cost far more live-odds traffic than the card put on screen, and a rail of several cards made this add up fast. The driver now walks the pool in the service's own rank order and opens a per-match feed only for the candidates it is currently trying to display: a candidate whose market already resolves off the batched events feed needs no subscription at all, and a candidate that is live but unpriced gets exactly one fair shot before the driver moves on to the next unclaimed candidate and releases the one that didn't pay off. The search stays exactly as deep asrecsysCandidatePoolSizeallows; only the number of open per-match feeds shrank, down torecsysLegCountat most. No behaviour change to what a combo displays, and no new configuration.Moved every shared UI component out of
@gomagaming/sports-domaininto a new@gomagaming/uipackage, which depends on it:The game cards:
RegularOddsand its live/prelive/outright/market variants,SkeletonCardandCountryFlag. Used by every widget that lists events.The betslip family:
BetslipPaneland its selection, summary and bonus pieces, and theGomaDialogmodal shell. Used by the betslip floating and sidebar widgets.The missions family: the missions cards, level rows, tags, empty state and mission-detail panels. Used by the missions hub and mission details widgets.
The promo-card family: the promo card shell, hero, competition row, kickoff time, CTA link and skeleton. Used by the highlight card and pros' choice widgets.
MyBetsPanelandMyBetsCardmoved into@gomagaming/my-bets, their only consumer. The betslip widgets keep showing them through<goma-my-bets>.No widget's public surface, markup or behaviour changes, and each widget renders the same components as before.
@gomagaming/uiis installed automatically with the widgets that use it.@gomagaming/sports-domainno longer exports any component. It keeps the stores, API clients, composables and the promo-card auto-mode layer, and additionally exportsuseBetslipSystem,useBonusSessionGate,bonusRowTitle,convertOdd,formatRatePercent,formattedDate,formattedTimeandresolveDateLocale. A host importing one of the moved components from@gomagaming/sports-domaindirectly should import it from@gomagaming/ui(or, for the MyBets pieces, use<goma-my-bets>) instead.
0.35.0
The shared betslip moved to
goma:betslip:v4, and the odds-change setting it carries is now a three-value policy.The envelope used to carry
acceptOddsChange, a boolean that could express only two of the three settings the betting provider accepts, and that meant the opposite of what its name said:truewas submitted asACCEPT_HIGHER, which accepts a rise. Its replacement,oddsChangeBehavior, says which ofaccept-any,accept-higherandnever-acceptapplies, and the last of those is a setting the boolean could not represent at all: a bet that is cancelled if the odds move in either direction.The storage version moved rather than the field changing meaning under the same key, so a page still running the previous release keeps reading the envelope it wrote.
Nothing is required of an integrator. An existing betslip migrates on the first widget mount, carrying the slip, stake, tab and bonus selections across. Only the odds setting changes, and every existing betslip resolves to
accept-higher:- A betslip with
acceptOddsChange: true, the previous default, was already being submitted asACCEPT_HIGHER. Nothing about those bets changes. - A betslip with
acceptOddsChange: falsewas being submitted asACCEPT_ANY, under a control reading "Approve higher odds", which is not what that value does: it also accepts a DROP in the odds. Those bets are now submitted as the label promised. A player who wants any change accepted can select it explicitly.
An integrator reading the betslip out of storage should read
oddsChangeBehavior, and address the key through the newly exportedBETSLIP_STORAGE_KEYrather than a literal. Widget code that sets the old boolean keeps working: it is still available, derived from the policy.- A betslip with
Stop the game-card composable from setting the host page's colour scheme
Rendering a game card changed the page it was embedded in.
useGameCardsTallOddscalled VueUse'suseDark(), and that helper's defaults areselector: 'html',attribute: 'class'andstorageKey: 'vueuse-color-scheme', so the widget wrote a class onto the host document's<html>element and a key into the host'slocalStorage, neither of which is a widget's to touch.The effect was worst for a host that manages its own dark mode through a class on
<html>, which is the common Tailwind arrangement: the widget resolved the visitor's OS preference and wrote its own answer over the host's, so a deliberately dark page could turn light because someone's laptop was set that way.Nothing consumed the value. The composable returned it as
isDarkand no component, template or composable in the suite ever read it, so the call was pure side effect. It is gone, along with the last@vueuse/coreimport in@gomagaming/sports-domain.<goma-favorites>,<goma-game-details>and<goma-hero-card>are the widgets that pull this composable in. Their own appearance is unchanged: every widget already carries its dark treatment inside its own shadow root and never depended on the class being written outside it.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:
detachBettingOfferFromOutcomekeyed on the OUTCOME rather than on the offer, so a re-price arriving asCREATE(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.attachBettingOfferToOutcomere-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.bettingOfferIdis 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_OFFERrecords now reach both store mirrors whichever WAMP topic carried them: the shared pool the cards read and the singulargameslot<goma-game-details>reads.createpreviously 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.outcomesand the market reaches the card with its two sides the other way round.sortOutcomesonly ranks outcomes carrying aheaderNameKey, which a handicap outcome does not, so every pair compared equal andArray#sort's stability handed the layout to arrival order. For a two-sided market the comparator now falls through to the provider's outcomecode: numbered codes (#1/#2) compare numerically, so#10does not sort before#2, and the provider's named pairs (#Y/#N,#O/#U,#Odd/#Even) are ranked the waygetOutcomeOrderalready ranks them. Yes before no, over before under, odd before even. A pair of codes the comparator does not recognise keeps the order the feed sent, and a market with more than two outcomes is never reordered.sortOutcomesreturns a copy, so the call sites that hand it a store-backed market's own array no longer reorder the store from inside a computed.<goma-events-horizontal>,<goma-favorites>and<goma-competitions-list>render the shared secondary card and inherit the fix.<goma-game-details>,<goma-hero-card>,<goma-highlight-card>,<goma-multi-banners>and<goma-pros-choice>callsortOutcomeson their own market lists and render the shared odds button, so the new ordering and the newlineongoma:outcome-selectreach them too.The betslip.
RegularOddsrecords the market's line (paramFloat1) on the selection at tap time, and it now survives both cross-widget bridges: thegoma:betslip:v4envelope carries it as an identity field, andgoma:outcome-selectcarries it in its detail. Without that it never reached the widget that reads it.useBetslipOddsSynccompares it against the market on every pass, in the game-details store slots as well as the list ones: when the line has moved the row is re-captioned and marked unavailable, anduseBetslipLogicrefuses to place a slip holding an unavailable leg, so the player picks the new line deliberately instead of having it accepted for them by the odds-change gate. The tapped line is never overwritten, so the verdict is re-asserted on every tick rather than being undone by the next price update, and a line that moves with no price change is caught too: a market-only rewrite never woke the odds watcher before. A wording-only change still just corrects the caption, and a selection saved before the line was recorded is never invalidated by the upgrade.Saying why. A blocked leg now carries
lineMoved: truebesideisAvailable: false, so the two reasons a leg cannot be placed stay apart. The betslip row reads "Line changed" (selection_line_moved, added to the en/fr/pt bundles) instead of "Selection unavailable" (the match is still on, it is the line that is gone) andgoma:bet-failedlists those legs under a newmovedLineSelectionskey rather than folding them intounavailableSelections, so a host does not word a live match as dead.placeBetBlockedReasongains'line_moved', ahead of'unavailable', so a host keying its wording off that token can say that the line moved rather than that the match is gone; the widget's own copy carries the newplace_bet_blocked_line_movedkey in all three locales. The flag itself is now written in both directions: a line that moves and comes back clears it, so a later genuine withdrawal reports as unavailable and not as a moved line.
0.34.0
Stop a region list fetched for the wrong sport from overwriting the right one, and parse numeric strings assigned to numeric widget properties instead of silently replacing them with the widget's default
A
<goma-region-filter>configured for any sport other than Football issued two location requests instead of one: its own, correctly scoped to the configured sport, and a second one (scoped to Football) that nothing had asked for. Both wrote the same place, so the pills the widget rendered were whichever response happened to come back last. The same race reached<goma-competitions-list>, which groups its competitions by the same region data.The unrequested request came from the per-widget boot routine, which fetched locations with no sport at all and so fell back to sport
1. Locations are scoped to a sport on the server, and boot has no sport to scope them by, so it no longer fetches them: the only readers of that data are the widgets above, and each of them already fetches it for the sport it is showing.That removes the collision at its source. As a second line of defence, a locations payload now records which sport it was fetched for, and a widget ignores one belonging to a sport it is not showing. This matters whenever two scoped fetches can overlap (switching sport while a request is still in flight, for instance) and it is deliberately narrow: a payload with no recorded sport is still rendered, so the guard can only ever suppress data it can positively identify as belonging to somewhere else.
Separately, every numeric property on every widget (
sportId,skeletonCount,pageSize,spaceBetween,autoplayIntervaland the rest) used to accept a JavaScript number and nothing else. A numeric string was not parsed; it was discarded and quietly replaced by the property's default value.el.sportId = '8'therefore left the widget on sport1and fetched the wrong data, with nothing logged to say so. Three ordinary integration paths hand over a string: a router parameter, a value read out of a CMS or JSON configuration, and a framework re-render that stringifies its props. Numeric strings are now parsed. Values that are not a finite number and not a numeric string (null,undefined,'', booleans, objects,NaN,Infinity) still fall back to the default, as before. HTML attributes (sport-id="8") and theconfigobject were never affected by this and are unchanged.Signing in (
<goma-betslip-sidebar>,<goma-betslip-floating>,<goma-favorites>) no longer stalls on a WAMP socket the provider never answers.The EveryMatrix gateway sometimes hands out a socket that completes the handshake and then answers no call at all, not the feeds, not
/sports#loginWithCeSession. The CE-session exchange only retried once a step settled, so it stalled for good: the host's "session ready" wait ran out, and every bet went out without ausernameand came back rejected until the page was reloaded. Around a quarter of sign-ins hit it on stage.Each exchange step (the StartInfo request and the
loginWithCeSessioncall) now has a 5s deadline, tunable with the newsessionStepTimeoutMskey (0disables it). A timed-out StartInfo request is aborted and retried. A timed-outloginWithCeSessiondrops the shared connection instead of retrying into it (every widget reconnects and the exchange re-runs on the fresh socket) at most twice per session; a successful exchange restores that allowance. If no new connection follows a replacement, a console warning says the exchange is not running.<goma-multi-banners>,<goma-highlight-card>and<goma-pros-choice>now re-open their live topics after a reconnect. They skipped every sport they had already subscribed to, so after any socket loss, and now after a replaced login socket, their odds stopped updating until the element was unmounted.
0.32.0
Widgets no longer dispatch a bare
errorevent alongsidegoma:error.Every factory-built widget's app-level Vue error handler dispatched two events for the same failure:
goma:error, and an un-namespacederror. The bare one is gone;goma:erroris unchanged and still carries the same{ message, code, component? }detail.Like every GomaPulse event it was
composedand bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had onwindow.addEventListener('error', …): as aCustomEventwithmessage,errorandfilenameallundefined, since those areErrorEventfields aCustomEventdoes not have. A host with error reporting installed received a malformed error record it could neither read nor attribute.That had been written off as harmless on the grounds that the handler behind it could never fire. It can. Vue begins its
errorCapturedwalk at the throwing component's parent, so theonErrorCapturedboundary each widget installs in its own root component cannot see that component's own errors, and being the root, there is nothing above it to catch them either. Anything raised in a widget's root scope (its setup, its render, its lifecycle hooks, its watchers) skips the boundary and reaches the app-level handler every time. Only errors from child components are caught by the boundary, which is what it is for: those show the in-shadow banner and emitgoma:retry.Hosts listening for
goma:errorneed no change. A host listening for bareerroron a widget element should move togoma:error; it was documented as unreachable, so in practice it delivered nothing until now.
0.30.2
Event cards: line the odds row up across a rail, and settle the market badge
Every card now distributes its rows in a flex column and pins the odds row to the bottom. Previously the main cards simply stacked their rows inside a fixed height panel: the stack ran taller than the panel, so the odds row spilled past the card and sat several pixels below the odds row of the secondary market cards beside it. The market badge is now centred in the gap between the last team name and the odds buttons, and the secondary card draws the same hairline under its top row that the main cards have always drawn.
The card is also slightly taller: the default size goes from 156px to 160px and the large from 188px to 190px, and at large the gap above the team names comes down from 12px to 8px. The design frames set two 16px team names in 16px line boxes, which shears the glyphs on the web once the names are truncated, so the names are sized to their ink instead: height the frames never budgeted for. The heights stay EXACT rather than becoming minimums: a rail must never show cards of differing heights. The "See all" tile, the loading skeleton and the empty state move with them, being the same card.
The suspended / no-odds block picks up the same transparent one pixel border the odds buttons carry, so the two occupy identical boxes rather than differing by a border width.
Promo cards (
goma-highlight-card,goma-pros-choice): the loading skeleton is now the same height as the card it stands in for, so a rail no longer jumps when the feed answers.goma-pros-choicealso reserves the fixture pill's height when a card carries no fixture, so cards keep one height with or without it.
0.30.1
- The competition headers in
<goma-favorites>switch to their desktop sizing at 768px (flag 28px, title 18px, star and caret 24px) instead of painting the mobile geometry at every width. Brings them in line with the equivalent headers in<goma-events-horizontal>and<goma-competitions-list>.
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
Corrected the
<goma-favorites>and<goma-hero-card>documentation pages: no source change.<goma-favorites>'s Props / attributes table showeddebugbacked by adebugHTML attribute; only the JS property (el.debug = true) actually works, so the row now shows it as JS-property-only. Both pages listedmessagesas a documented prop;messagesis only reachable throughel.config = { messages }(noel.messagesaccessor exists), so the phantom prop row has been removed: the config-key documentation in each page's Configuration section was already correct and is unchanged.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.
0.28.0
<goma-events-horizontal>,<goma-favorites>and<goma-competitions-list>: add ashowFadesprop (show-fadesattribute, defaulttrue) 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 staystrue, 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 asizeprop (sizeattribute,'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 atsize="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-codedcalc()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
sizevariants: the design'ssize=defaultandsize=largeframes carry identical card chrome, so each value below is the same number in both.What a host actually sees on the game card:
was now Odds row height 48px 40px Odds button padding 8px sides / 4px top+bottom 8px all round Odds button corner 6px 4px Card padding, top / bottom 4px / 12px 8px / 10px Breadcrumb row height 24px 16px Competition name weight 300 500 Team name weight 600 700 Team name line height 20px 16px Gap between the two team rows 4px 6px ( defaultonly)Market badge height 14px 16px Market badge side padding 12px 8px 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.
RegularOddsis 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 atdefaultand 144px → 142px atlarge. Its odds row is the last child of a fixed-heightjustify-betweencolumn, 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'ssize=largevariant 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.
sizeis set on the feed widget and threads down to the cards, the same shapedefaultMarketandoddsFilteralready use: the cards themselves gain asizeprop 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/coreis 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.- The secondary market card's inner column follows the card height by the same relationship as before, re-derived from the new padding:
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.22.0
Session: new
goma:session-readyevent. The host can finally tell when a session is usableA host knows when it issued a
sessionId. It has had no way to know when that id became something the player can bet with, and those are seconds apart: the REST probe resolves theuserId, then the CE-login exchange fetches aceSessionand binds the WAMP socket, and only then does the place-bet payload carry theusernamethe provider rejects a bet without.A host that reveals its betting UI on its own login response therefore hands the player a Place Bet button whose first submission is guaranteed to fail: tap once, get an error, tap again, it works.
goma:session-readycloses that gap, as the counterpart to the existinggoma:session-expired:jswindow.addEventListener("goma:session-ready", ({ detail }) => { // detail: { userId, username } closeLoginModal(); });Emitted by every session-aware widget (both betslips, favourites, my-bets) once per resolved token, and again whenever a new token is minted (max-age refresh, WAMP reconnect), so handlers must be idempotent.
There is no host-side alternative: the shared
goma:session:v1envelope is public, butstorageevents fire only in other tabs, never the writer's, so a host could only have polled it.Session: a player who signs in without reloading can bet again
Signing in during a page's lifetime left the player half-authenticated: the place-bet payload went out with no
usernameand the provider refused it, and only a page reload fixed it. Two defects, each of which was hiding the other.A refused CE-login was committed as a session. A successful
loginWithCeSessionanswers{ username, sessionToken, message }; a refused one answers a single-key payload, which is still truthy, so the guard let it through. That wrotestatus: 'valid'with a nullusernameand a nullsessionToken: an envelope every widget reads as resolved, that can never place a bet, and that nothing re-triggers. A response missing either credential is now treated as a failed attempt and retried.An unresolved session was mistaken for a logout. While the exchange had committed nothing, the shared envelope stayed
idle, and the mirror read that as "the player signed out" and wiped theuserIdthe REST probe had already established, so the bet went out with no identity at all and came back401.idlenow only tears the identity down when the host has actually dropped itssessionId, which is what a real logout does.Fixing either alone made things worse, because the first defect's bogus commit was what kept the second from firing.
0.20.1
Fix: a 401 no longer signs the player out of the whole page
0.20.0 made a dead session sign the player out, but treated any
401/403from place-bet or favourites as proof the session had died, and applied the logout page-wide regardless of which credential actually failed. In practice that made a signed-in session unusable: place a bet, get signed out.A bare 401 is no longer read as a dead session. It means "this request wasn't authorised", which equally covers a gateway rejection, a misrouted call, or a request issued with a credential the host has already replaced. Place-bet and favourites now require the provider's explicit
InvalidSessionmarker before condemning the session, matching the production sportsbook'shandleSessionExpiration. Anything else fails that one request and leaves the session alone. The session probe (GET /v1/player/session/player) and the balance stream'sSESSION_EXPIRATIONevent remain the authorities on whether the player is still signed in.A rejection is scoped to the credential it names.
markInvalid()now records whichsessionIdfailed, and the logout +goma:session-expiredonly apply to a widget still running that one. Previously the death was stamped onto whatever the envelope currently held, which, after a re-login, is the new token, so a late401from the old one killed the session that had just replaced it. Requests can't be recalled: a/balancecall, a favourites poll, or an SSE stream replaying its captured headers on backoff can all outlive the token they were issued with.commit()no longer blanks fields it wasn't given.sessionIdused??, so an empty string overwrote a real id: hosts feeding a pre-resolved identity (userId+sessionToken, no rawsessionId) wiped the id every other widget had recorded, after whichisValidForcould never match and the CE-session exchange re-ran indefinitely.usernamehad the same flaw: only the CE-login exchange learns it (the REST probe never returns one), so any other committer nulled it, andusernamethen silently vanished from every subsequent place-bet payload. Both are preserved now; a real logout still clears them viaclear().A spent rejection left over from a previous
sessionIdis deliberately left in the envelope rather than cleared:clear()resets it to'idle', whose mirror branch wipes the identity the probe just established. Trading one logout bug for another. It is inert, since every consumer scopes on the id it names.
0.20.0
A dead session now signs the player out and notifies the host
A place-bet rejected with
401used to paint the literal stringHTTP 401into the betslip panel: the transport fallback, because the provider sends no body on a gateway rejection and HTTP/2 carries nostatusText. Above it sat a "your session is no longer valid" banner, and underneath, nothing recovered: the invalidation cleared the shared token but no resolver watched for it, the stale token stayed on the user store, and Place Bet stayed enabled. Every retry re-failed identically until a full page reload.gomawt cannot re-authenticate (the REST
sessionIdis minted by the host's login and the provider exposes no refresh path) so the fix hands the problem to the party that can solve it.A dead session is now a logout, not a warning. The identity is cleared, so balance, My Bets and favourites all fall back to their guest states. The warning banner and its
session_invalid_warningmessage key are removed: leaving the UI presenting a signed-in player who couldn't do anything was the worst of both. A place-bet401/403also renders no error text.goma:bet-failedstill carries the fullstatus/bodyfor telemetry.goma:session-expiredis the session-death contract. Every authoritative rejection funnels throughuseSharedSession().markInvalid(reason)and reaches the host: place-bet401/403, favourites401/403, the session probe, the CE-login exchange, and the SSE expiry that already fired it.detail.reasonnames the source. It fires once per invalidation per mounted session-aware widget, and again on a retry, so handlers must be idempotent.Migrating an existing host
This changes what the widget shows, so a host upgrading from an earlier version should check both of these:
- The session-invalid banner is gone. If you relied on the widget painting "Your session is no longer valid" as the only signal to the player, there is now no widget-side surface at all: a dead session silently signs them out. Listen for
goma:session-expiredand show your own message. - Place-bet errors no longer include session failures. A
401/403writes nothing to the panel. If you were reading the rendered error text, switch to thegoma:bet-failedevent, which still carriesstatusandbody.
jslet lastHandled = null; el.addEventListener("goma:session-expired", async (e) => { const { reason, invalidationId } = e.detail; if (invalidationId === lastHandled) return; // a sibling widget already reported it lastHandled = invalidationId; el.sessionId = await myApp.reauthenticate(); });Hosts using
placeBetMode: 'event'are unaffected: they submit through their own backend and already own auth.detail.invalidationIdis a monotonic counter: every mounted session-aware widget reports the same id for one death, a retry reports a higher one. Dedupe on it rather than on your own logged-in flag, which would swallow the retry.A page load re-verifies the session instead of trusting the cache. The
goma:session:v1envelope records what gomawt last resolved, never that the provider still honours it, so a session killed server-side stayed "valid" across reloads for the whole max-age window. The firstsessionIdof each page load now always runs the REST probe and always re-runs the CE-session exchange (the cached token was bound to the previous socket). Both are de-duped page-wide (one probe plus one exchange per load however many session-aware widgets are mounted) after which the normalsessionMaxAgeMsreuse window resumes.requireSessionnow gates on a usable session, not just a non-empty string. A stalesessionIdrestored from a host cookie satisfied the old check, so Place Bet stayed enabled and the submit went out to a guaranteed401. The widget can't clear the host-ownedsessionIdprop, so this is also what stops a player re-tapping into the same failure while the host gets around to signing them off. The gate is scoped to the id the rejection was recorded against, so a freshly-issued token isn't blocked by the previous one's death.Transient failures change nothing. A network blip, a 5xx, or a missing
playerApiBaseUrlleaves the session exactly as it was (no logout, no page-wide invalidation, no event) so a flaky connection can't sign the player out. Only an authoritative rejection does.- The session-invalid banner is gone. If you relied on the widget painting "Your session is no longer valid" as the only signal to the player, there is now no widget-side surface at all: a dead session silently signs them out. Listen for
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'smarketsarray, 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'smarketsarray is preserved.Adds
orderMarketsByMainMarketsto@gomagaming/sports-domain.
0.19.0
Fix place-bet rejected with "Invalid or missing Betting Type Id on placed bet"
Every leg was serialised with
bettingTypeId: null, so the provider rejected the whole slip. The odds card captured the field correctly: it was lost in transit to the betslip widget.Tapping odds does two things in the same tick: it writes a full selection to the source widget's own store, then dispatches
goma:outcome-select. That event is synchronous, so a betslip widget's window listener inserted its row from the eventdetailalone, which carried nobettingTypeId, a tick before the complete shared-storage snapshot arrived. The inbound sync then skipped the row because itsoutcomeIdwas already present, so the partial first writer won permanently.Fixed on both sides:
goma:outcome-selectnow carriesbettingTypeIdandeventPartId(it already carriedoutcomeId, 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,
undefinedandnullall count as missing, so the repair does not depend on how a producer spells "unknown"; anullon 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-selectdetail arrives without abettingTypeId(a host driving the betslip from its own UI, where no richer snapshot exists to repair the row) the betslip logs aconsole.warnnaming 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 forwardbettingTypeId/eventPartIdfrom the banner to the odds button. TheeventIdpath 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
RegularOddscomponent.
0.15.0
favorites: the widget can now follow the page's filter controls. Two opt-in flags (both off by default):
followSportPicker(+ optionalsportPickerSelector) narrows the accordion to the sport selected by a sports picker, listening forgoma:sport-selectandgoma:filters-apply. It's a post-fetch, section-level filter on the resolved sport; sections whose sport is still resolving are hidden until they resolve. When a sport has favourites but none match, an inline "No favourites for the selected sport" message shows (distinct from the empty state).followFiltersapplies the time / default-market / odds-range filters to the favourite event cards, listening for a standalone<goma-time-filter>(goma:time-select) and the<goma-filters-settings>modal (goma:filters-apply). Default market swaps each card's main market; odds range and the time window gate the events within each section (an emptied section keeps its header). Unknown markets / start times are never hidden.
The accordion now keeps one visible section open after any structural change (load, orphan drop, sport switch) while respecting a manual collapse.
sports-domain: new shared
eventPassesTimeWindow(event, timeValue, nowMs)helper (parses the time filter's'{startHours}-{endHours}'window against an event'sstartTime; unix ms/s auto-detected).Favourite star toggles now persist to the server from any favourites-aware widget, not only the favourites page.
useFavoritesServerSyncis now a tab-level singleton: it reads the resolved identity from the shared session and is armed once with the favourites REST config (favoritesApiBaseUrl+ operator), then seeds and pushes add/remove deltas for the whole tab. Any widget can arm it viauseFavoritesPersistence(state, config):<goma-events-horizontal>does so whenfavoritesApiBaseUrl(orconfig.favoritesApiBaseUrl) is set, so a star tapped on a page with no<goma-favorites>still persists (and no longer gets silently discarded by the favourites page's next server seed). No-op when no favourites REST config is supplied, so existing integrations are unchanged.favorites: new
<goma-favorites>page widget. A lazy accordion of favourited competitions and events with market-rich cards, server-persisted star toggles (user-data-service REST) and a cross-widget shared cache.sports-domain:
createFavoritesApiREST client,useFavoritesServerSync, extendeduseSharedFavoritesSync(events), and an additive multi-match subscription (subscribeToMatchAdditive+sports.favoriteEvents) so many favourite games resolve by id and tick live at once.core: typed
goma:favorite-toggledetail ({ type, id, isFavorite }).events-horizontal: emit the typed
goma:favorite-toggleshape from the top-bar star.Favourite stars are now login-gated. When a favourite star is tapped while no session is present, the widget does not write the favourite; instead, it emits a new
goma:login-requiredevent ({ reason: 'favorite', type: 'competition' | 'event', id: string }) so the host can open its login flow. A sharedguardedFavoriteTogglehelper and auseSharedSession().isLoggedInsignal back the gate. Applies to the shared game-card star (events-horizontal / favorites), the events-horizontal top-bar competition star, the favorites section star, and hero-card's host-owned favourite toggle.favorites: harden the accordion. A section's live WAMP feed is now dropped when it's collapsed or filtered out of the sport view, so only on-screen sections stream odds in the background. The first section auto-opens only once every competition's metadata has settled (with a stall-timeout failsafe), so it lands on the true top section instead of whichever resolved first; a deliberate collapse-all is respected through background rebuilds. A transient
getTournamentInfofailure now retries so a competition can't be left permanently hidden under a sport filter.sports-domain:
useSharedStoragecloses itsBroadcastChannelon scope dispose (fixes the leaked channel + its teardown "Channel closed" noise). The betslip's shared-session handle is created once in setup rather than per place-bet 401 (no more per-failure channel leak). An explicitsessionMaxAgeMsof0(force-re-exchange) is honoured instead of falling back to the default.events-horizontal: fix the competition top bar staying on its loading skeleton for the first-selected competition. The
getTournamentInfoRPC can race WAMP connect and come back empty, and nothing re-attempted it. It now retries for the active competition and shows the loading skeleton while the authoritative name resolves (rather than flashing the unreliable feed name). Corrects thegoma:favorite-togglepayload shape incustom-elements.jsonto{ type, id, isFavorite }.