<goma-game-details>
Full details for a single sporting event: header (teams, score, clock), market-category tabs, and a lazy-loaded list of collapsible market cards. Page-shaped: it fills its parent and the host owns presentation context (their own route slot, drawer, modal, or sized column); <goma-game-details> owns the contents.
Live demo
Open in the playground →: toggle Game details in the widget rail and pick the mock source in the inspector to render a self-contained game with drifting odds and a live clock. Mount Events horizontal alongside it to verify the goma:event-navigate hand-off: tapping a card swaps the details view.
Element tag
html
<goma-game-details></goma-game-details>The widget fills its parent (:host { display: block; width: 100% }): give it a sized container (route slot, drawer, sidebar column, modal body). It stays inert until eventId is set, and that inert state has no real content to size itself against; a built-in min-height: 360px fallback on the widget's root keeps it from collapsing to near-zero height in an unsized container while waiting for eventId. It's a safety net for that inert/loading state, not a substitute for sizing the container.
Installation
bash
pnpm add @gomagaming/game-details vue pinia vue-router vue-i18n @vueuse/core autobahn-browserSee Installation for the one-time .npmrc token setup. vue, pinia, vue-router, vue-i18n, @vueuse/core, and autobahn-browser are peerDependencies: installed once at the app level and deduped across every @gomagaming/* widget.
Configuration
js
const el = document.getElementById('gd')
el.config = {
// WAMP socket for live match + market subscriptions
socketUrl: 'wss://sports-api.example.com/v2',
socketRealm: 'www.example.com',
// REST base URL for the sports API
apiBaseUrl: 'https://sports-api.example.com',
// The event to render. Equivalent to setting el.eventId directly.
eventId: '12345',
// Optional: locale propagates to bundled i18n
locale: 'en',
// Optional: country-flag CDN base for the header's competition row
flagBaseUrl: 'https://static.example.com/flags/',
}| Key | Type | Required | Description |
|---|---|---|---|
socketUrl | string | For live data | WAMP socket URL for the match + market-group + markets subscriptions. |
socketRealm | string | For live data | WAMP realm. |
apiBaseUrl | string | Yes | REST base URL for the sports API. |
eventId | string | No | The match to render. Equivalent to setting el.eventId directly. |
locale | string | No | i18n locale. Defaults to 'en'. |
favoritesApiBaseUrl | string | No | Favourites REST base URL. When set, star taps on this widget are saved to the player's favourites, also on a page with no <goma-favorites>, when a signed-in session is available on the page. The operator is ucsOperatorId. |
flagBaseUrl | string | No | Country-flag CDN base for the header's competition breadcrumb (resolved as ${flagBaseUrl}{venueId}.png). Config-only: there's no matching flagBaseUrl JS property. |
theme | object | No | CSS variable overrides: see Theming. |
messages | object | No | i18n overrides: see i18n / message overrides. |
debug | boolean | No | Verbose console logging, including the followEventNavigate rebind log. Defaults to false. |
The batching, header-surface, and cross-widget-follow knobs (initialBatchSize, batchSize, showBackButton, …) are plain element properties: see Props / attributes below. They're tracked state, so they can also be set through this same config object.
Data sources
WAMP subscription: once eventId (and a connected socketUrl) are set, three independent subscriptions run. All three are owned by GameDetailsApp itself, not by the header/markets-list components that read their data: those components live behind the not-found and ended branches, so a subscription rooted inside either one would tear down (and never re-fire) the moment that branch renders:
| Subscription | Composable | Fires on |
|---|---|---|
subscribeToMatch(eventId) | useGameDetailsHeaderSubscription | eventId change |
subscribeToMarketGroups(eventId) | useGameDetailsHeaderSubscription | eventId change |
subscribeToMarketsByMarketGroupKey({ matchId, groupKey }) | useGameDetailsMarketsListSubscription | (eventId, activeTab) change |
Each composable cleans up its own subscription via onScopeDispose. Switching eventId tears down the previous subscriptions before registering new ones. Switching the active market-category tab swaps only the markets-by-group subscription: the match + market-groups subscriptions stay attached.
Default tab auto-selection runs once per eventId when the market groups arrive: see Default market tab for which group it picks. If the user (or a prior interaction) has moved the tab off 'allMarkets', the auto-select respects the current choice and just marks the eventId resolved.
Direct data: bypass the socket and drive the widget entirely host-side:
js
el.game = { id: '12345', homeShortParticipantName: 'PAC', /* … */ }
el.marketGroups = [{ groupKey: 'goals', position: 0, translatedName: 'Goals' }]
el.markets = [{ id: 'm1', outcomeIds: ['o1', 'o2'], bettingTypeName: '1X2' }]
el.outcomes = [{ id: 'o1', translatedName: '1' }]
el.bettingOffers = [{ outcomeId: 'o1', odds: 1.85, isAvailable: true }]The internal subscription composables still run with no socketUrl open: with nothing to subscribe to, they no-op, and only the direct-data values render. This is how the playground's mock source drives the widget.
A bulk expand/collapse control sits above the market list. Market cards open expanded, so it starts as Close all; tapping it collapses every card and the label flips to Expand all, which re-expands them. The state is persistent rather than a one-off action, so market cards that are lazily mounted later (as you scroll) seed their initial expanded/collapsed state from it: collapsed after Close all, expanded after Expand all. Tapping an individual card's caret still works, and the next bulk tap re-syncs every card. Switching market-category tab resets the control to its expanded default. It uses the close_all and expand_all message keys.
Props / attributes
| HTML attribute | JS property | Type | Default | Notes |
|---|---|---|---|---|
event-id | eventId | string | null | null | Primary contract. Changing it tears down the previous subscriptions and re-registers against the new id (no element re-mount). |
locale | locale | string | 'en' | Mirrored. |
| - | config | GameDetailsConfig | {} | JS-only; full config object. |
| - | theme | Record<string, string> | {} | Standard --goma-* token bag. |
follow-event-navigate | followEventNavigate | boolean | true | Toggle the navigate watcher: see Integrates with. |
event-navigate-selector | eventNavigateSelector | string | '' | CSS selector scoping the navigate watcher to one emitter, matched against the event's retargeted host. Empty follows any emitter. |
initial-batch-size | initialBatchSize | number | 5 | Markets in the first batch. |
batch-size | batchSize | number | 10 | Markets added per sentinel hit. |
intersection-root-margin | intersectionRootMargin | string | '200px' | Passed to the batching IntersectionObserver. |
markets-list-height | marketsListHeight | string | '480px' | CSS length for the markets-list scroll viewport. |
show-back-button | showBackButton | boolean | true | Toggle the header back affordance. |
show-competition | showCompetition | boolean | true | Toggle the competition row. |
show-live-clock | showLiveClock | boolean | true | Toggle the live clock (effective only for live events). |
team-name-format | teamNameFormat | string | 'short' | Team names in the header: 'short' (e.g. "Man City") or 'full' (e.g. "Manchester City"). Either one falls back to the other when the feed leaves it empty. The value ignores case and surrounding spaces ('Full' works); any other value reads as 'short' and logs one console warning. Emitted event payloads keep the short name. |
game | game | object | null | null | Direct-data mode: host-supplied match object (patched into the betting store). |
market-groups | marketGroups | array | [] | Direct-data mode: host-supplied market-group tabs. |
markets | markets | array | [] | Direct-data mode: host-supplied markets. |
outcomes | outcomes | array | [] | Direct-data mode: host-supplied outcomes. |
betting-offers | bettingOffers | array | [] | Direct-data mode: host-supplied betting offers (prices). |
The HTML-attribute forms of the direct-data props (game, market-groups, markets, outcomes, betting-offers) are parsed with JSON.parse; in practice you set these as JS properties.
Events
All custom events use { bubbles: true, composed: true }.
| Event | Detail | When |
|---|---|---|
ready | {} | Widget mounted, initial render complete. |
goma:error | { message: string, code: string, component?: string } | Two paths. Connection layer: the WAMP adapter exhausts its 5 retry attempts (code: 'API_INIT_ERROR'), alongside goma:connection { state: 'failed' }. Widget root scope: an error raised in the widget's own outermost component (its setup, its render, its lifecycle hooks or its watchers), where code is the error's name, e.g. 'TypeError' ('RUNTIME_ERROR' when it has none). Errors raised inside the widget's child components do not reach this event: the in-shadow error boundary catches those and shows the retry banner instead, emitting only goma:retry. See Empty / loading / error states. |
goma:back | { eventId: string } | User taps the back affordance in the header. Host decides what "back" means in its routing. |
goma:tab-select | { eventId, groupKey, groupName } | User changes the active market-group tab, or the auto-select default fires. |
goma:event-not-found | { eventId: string } | Match subscription resolves and the event is missing or empty. Fires once per eventId. |
goma:game-ended | { eventId, homeTeam, awayTeam, homeScore, awayScore } | A match the widget saw live reaches a terminal status: ended, cancelled, walkover, abandoned or retired (isTerminalEvent from @gomagaming/sports-domain). An interrupted match does not fire it: that status clears isLive too but the match can resume, so the latch stays armed. Fires once per eventId, carrying the final team names and the last live score. Two fallbacks: a host feeding no statusId at all falls back to a plain live→not-live transition, and a match that disappears from the feed entirely fires once its absence survives a 2.5s grace window (long enough to absorb a re-subscribe's transient null). homeScore and awayScore are null when the match has no score rows at the end, also when the header still shows a last score. Only fires for a match the widget actually observed live in-session: one already finished on arrival renders the ended state instead and never emits this event. |
goma:outcome-select | { bettingOfferId, outcomeId, outcomeName, odds, eventId, marketId, bettingTypeId, eventPartId, line } | User taps an outcome to add to the betslip (reused from the shared RegularOdds button). |
goma:outcome-deselect | { bettingOfferId, outcomeId } | User taps an active outcome to remove. |
goma:seo-data | { widget: 'game-details', type: 'sports-event', items: object[] } | Fired once the displayed event resolves, and again only when its identity changes: never on an odds tick. See SEO data for the payload contract. |
goma:retry | {} | User taps the retry button on the error boundary. |
Integrates with
Listens for goma:event-navigate (gated by followEventNavigate, default true) and patches its own eventId: a no-op if the incoming id already matches the current one. This is the widget's canonical downstream role: <goma-events-horizontal> (every card type plus its wrapper), <goma-favorites>'s event cards, <goma-hero-card>, <goma-multi-banners>'s sport banner card, and <goma-competitions-list>'s drill-in detail view all emit this event today, so dropping any of them alongside <goma-game-details> wires the hand-off with no glue code. Set config.followEventNavigate = false to opt out and drive eventId manually, or call event.stopPropagation() on goma:event-navigate at a higher DOM level to mediate (e.g. route through your own URL before switching).
Because that list of emitters is long and the watcher is on by default, a page carrying two card widgets, or two <goma-game-details>, has every panel jumping to every card tap anywhere. Set eventNavigateSelector to a CSS selector for the emitter that should drive this panel; it is matched against the event's retargeted host, the same guard <goma-events-horizontal> and the picker followers carry:
html
<goma-events-horizontal id="feed"></goma-events-horizontal>
<goma-game-details event-navigate-selector="#feed"></goma-game-details>Writes odds selections into goma:betslip:v4 (identity fields only, useSharedBetslipSync({ subscribeToState: false }), the same opt-out <goma-events-horizontal> uses), so any <goma-betslip-*> widget on the page picks up a tap here and vice versa.
See Cross-widget wiring and Shared state for the full contracts.
Empty / loading / error states
- Inert. No
eventIdset: nothing renders. - Loading.
eventIdset, subscription in flight: the header shows a spinner in place of the team/score rows, and the markets list showsinitialBatchSize(default5) skeleton placeholder cards. - Not found. Subscription resolved, no event in the store: empty state with the
empty_state_match_not_available_title/_descriptionstrings, plus agoma:event-not-foundemit (once pereventId). - Ended. The event has reached a terminal state (ended, cancelled, walkover, abandoned, or retired): the header stays, so the teams and competition remain readable, but its live furniture (score, match clock, LIVE badge) and the kickoff date/time are all dropped. A finished match is neither running nor upcoming. The market tabs and markets list are replaced by an empty state with the
empty_state_match_ended_title/_descriptionstrings. Interrupted matches are not treated as ended: they can resume. A match that transitions into this state after the widget saw it live also firesgoma:game-ended: see Events.goma-live-chatconsumes that event to close its chat room, though the host has to relay it: the chat widget listens for nothing on its own. - Live with no score. The feed does not carry a score for every live match, for example some lower-tier tennis tournaments, or the first minutes of an e-Tennis match. With no score, the header draws no score block, at any point in the match, so the current part (
7th Game (2nd Set), in the feed's language) tells the progress. A match that had a score keeps the last one while it stays on screen. A page reload or a move to another match clears it. Withshow-live-clockoff, a match with no score shows no progress at all. - No markets. Match loaded but the active tab has no markets: empty state with the
empty_state_match_no_markets_titlestring. - Render error. Caught by the in-shadow error boundary: a red banner with a retry button that emits
goma:retry; auto-clears after 8 seconds even without a retry tap.
Loading is a lifecycle fact, not an inference from the data: the widget tracks whether the match feed and the markets feed have answered for the current event and tab. An initial dump landing counts as an answer even when it carries nothing: that's what makes the ended, not-found and no-markets states reachable at all. If a subscribed feed never answers (the topic doesn't exist, the provider goes silent, the dump is lost), the widget stops waiting after 55 seconds (long enough for the connection layer to ask for the data again) and falls through to the matching terminal state rather than holding skeletons indefinitely. Switching event or market tab starts a fresh cycle, and so does a subscription that begins late (a host that sets config after eventId), so a slow start is never mistaken for a missing match.
The timeout applies only to feeds the widget actually subscribed to. If it never gets that far (no socketUrl, or the adapter fails to initialise) there's no subscription to time out, and the widget holds its loading state rather than claiming the match doesn't exist.
Default market tab
Market groups are a provider-side split of a match's markets, each with its own feed. A group counts as usable only if the provider reports numberOfMarkets greater than zero and supplies a groupKey: the key is the feed's address, and a group's id is not a substitute for it. Groups that fail either test are hidden from the tab strip entirely; if none passes, the strip doesn't render.
Among the usable groups, the widget activates the one the provider flags isDefault (the leftmost, by position, if several are), and otherwise the leftmost one. A group that omits position sorts last, so it can't take the default slot by accident.
If no group is usable (common on sports with sparse market metadata, where operators advertise the full set of groups but populate none of them) the widget stays on the catch-all allMarkets feed, which carries every market the match has. This is load-bearing: moving to a group the provider reports as empty swaps a feed that has markets for one that answers with nothing, and the tab strip offers no way back.
Theming
Most surfaces map to the shared --goma-* token set: see Theming for the complete reference. The tokens this widget's own components actually read:
- Backgrounds: page on
--goma-backgroundPrimary, header on--goma-gameHeader, market-card body and the score "main score" chip on--goma-backgroundCards, the market-tab strip's row on--goma-navPills, loading skeletons on--goma-backgroundBorder. - Text:
--goma-textPrimary/--goma-textSecondarythroughout;--goma-buttonTextSecondaryon the red-card score badge. - Live: the header's LIVE badge fills with
--goma-liveTag; its text/icon, and the error-banner retry button's text, sit on--goma-highlightPrimaryContrast. - Highlights & interaction:
--goma-highlightPrimarydrives the active tab border, the section divider, the "See All" toggle, focus rings, and the error-banner background;--goma-highlightSecondaryis the tab-pill hover/active border. - Market-tab pills: fill
--goma-tabPillBackground(falls back to--goma-backgroundCards) and idle label--goma-tabPillText(falls back to--goma-textSecondary). The active pill keeps its--goma-highlightPrimaryborder and--goma-textPrimarylabel. Set the two tab tokens to restyle the tabs without recolouring the market cards. - Alerts:
--goma-alertErrorbacks the render-error banner and the red-card score badge. - Card frame. Market cards use the same gradient-border recipe as
<goma-events-horizontal>'s main card:--goma-cardBorderLineGradient1/2/3. - Icons:
--goma-iconPrimarycolors the back arrow, sport glyph, paging arrows, and market-card collapse caret.
Outcome buttons are the shared RegularOdds component, so they reuse the same odds-button tokens as every other widget's game cards: see Theming rather than a widget-local list.
js
el.theme = {
gameHeader: '#161732',
backgroundCards: '#1f2147',
highlightPrimary: '#ff6600',
buttonBackgroundPrimary: '#ff6600',
buttonTextPrimary: '#ffffff',
}i18n / message overrides
The widget bundles its strings via @gomagaming/core's shared messages. Pass config.messages to override per-locale phrases:
js
el.config = {
apiBaseUrl: '…',
messages: {
en: {
empty_state_match_not_available_title: 'Match unavailable',
empty_state_match_no_markets_title: 'No markets yet',
market_categories: 'Categories',
},
},
}Keys used by the widget:
back,live,expand,collapse,retryclose_all/expand_all(the two labels of the bulk expand/collapse-every-market-card control)market_categories(the ARIA label of the tablist)previous/next(ARIA labels on the tab strip's paging arrows)market_group_<key>(per-category labels, fall back to the server-providedtranslatedNamewhen present)see_all/show_less(a market card's outcome-list toggle, shown once a market has more than 9 outcomes)empty_state_match_not_available_title/empty_state_match_not_available_descriptionempty_state_match_no_markets_title(the no-markets empty state has no description string)empty_state_match_ended_title/empty_state_match_ended_description: realendefaults'This game has ended'/'Betting is closed for this match.'
Accessibility
- The market-tab strip is an ARIA
tablist; each pill is atabwitharia-selected; the markets list is the correspondingtabpanelwitharia-labelledbypointing to the active tab. - The tab strip's paging arrows carry
aria-labels (previous/next) and only render in the direction there's more to reveal. - The back affordance is a
<button>(not an anchor): the widget doesn't know the host's route. - Each market card's collapse toggle and its "See All" outcome toggle both expose
aria-expandedandaria-controls. - Outcome buttons (shared
RegularOddscomponent) carryaria-pressedfor the selected state andaria-disabledwhen suspended.
What's deferred
These are recognised next-version surfaces, not omissions:
- BetBoost tab: a
bet_boostsynthetic tab. The markets-list subscription already special-cases and skips this tab key; the widget UI for it hasn't landed. - Share button + a share event contract: no share affordance or event exists yet.
- Favorite toggle in the header.
- Stats / lineups / H2H / commentary, not implemented.
- A modal/dialog twin: a variant for mobile-overlay UX patterns. Will land if a real product need shows up.
Integration Caveats
SSR/webview: importing this package outside a browser previously threw immediately; fixed with a guard on the shared element factory.
Sizing, added a
min-heightfallback (matching the mechanism sibling widgets use, a floor on an inner wrapper class, not:hostalone) for the inert/no-eventIdstate, which previously had no floor in an unsized container.Tokens/Theming:
themeTokens.jswas out of sync (a stalebackgroundSecondaryentry, six real tokens missing); corrected to match the Theming table 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.9
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.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
- 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.3
- Read the whole-match score from a Spanish feed. A live football match on a Spanish-language operator showed no score in the match header, because the whole-match part name in Spanish was not recognised and that row is the only score of a football match.
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 market-category tab pills read
--goma-tabPillBackground(pill fill) and--goma-tabPillText(idle label). Both fall back to the tokens the pills used before (backgroundCards,textSecondary), so the default look is unchanged; a host can now restyle the tabs without recolouring the market cards. The theming guide lists these two instead of the five tab/header tokens it documented but no component read.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.Match dates read day-first in English, so one match is no longer written two different ways across the suite.
An event card writes the kick-off as a compact
11/10;<goma-game-details>'s header,<goma-hero-card>,<goma-multi-banners>and<goma-search>'s result rows write it in words throughtoLocaleDateString. Withlocale: 'en'(the bare tag, whichIntlresolves to American ordering) that long form came out asOctober 11next to the card's11/10. English was the only shipped locale that disagreed with its own cards (frandptare already day-first, and the shared time formatter has always been pinned toen-GB), so the long form now resolves a bareentoen-GB. A host that explicitly configuresen-USkeeps American ordering.Two surfaces build their own
Intl.DateTimeFormatrather than going through the shared formatters, and both showed the same inversion for the same match: the My Bets card's leg date (which also printed07 Octoberwhere the cards print7 October, and sat directly under a hand-builtdd/mm/yyyy"placed" line) and the live-chat bet modal's per-leg date. Both now resolve the locale the same way.Every package listed here renders a date that changes, which is why the note reaches all of their pages rather than only the two whose source moved: including the two betslips and
<goma-messenger>, which render the same leg date through the<goma-my-bets>widget they embed.Not covered, and deliberately: the missions surfaces format challenge dates with the browser's locale rather than the widget's, and, in the same file, render the same instant as a deliberately locale-independent
DD/MM/YYtwo functions below, so one view contradicts itself regardless of locale. That is a different defect from this one and wants its own ticket.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.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.Each widget now formats dates and config values using its own locale.
Previously a single shared i18n instance inside
@gomagaming/corewas rewritten by every widget as it mounted, so a page hosting two widgets with two differentlocalevalues had the later one decide the language for shared formatting helpers used by both. That instance is gone; the helpers take the calling widget's i18n context directly.The undocumented
i18nConfigexport has been removed from@gomagaming/core. Nothing in the widget catalogue used it.@gomagaming/sports-domain's publicformattedUnixDateexport is also affected: called directly, without passing an i18n context, itstoday/tomorrowtext used to fall back to the shared instance's bundled English and now returns the raw translation key (e.g.today) instead. Callers outside the widget catalogue that relied on the old English fallback need to start passing{ t, locale }themselves.
0.30.0
- The floating betslip button draws the ticket glyph from the design instead of a receipt outline, with the two rules that were a hair apart (2.16 against 2.17325) evened up: that mismatch was what made the icon look distorted at small sizes. The game-details market list takes 16px of inline padding on mobile, where the widget is laid out edge-to-edge and the cards were butting against the screen.
0.29.0
Add
goma:seo-data: widgets emit their resolved event data so hosts can build their own meta tags and structured data.Eight widgets emit the event:
game-details,events-horizontal,competitions-list,hero-card,highlight-card,pros-choice,multi-banners(sport banners only), andcasino-games. The payload is{ widget, type, items }, wheretypeis'sports-event'or'casino-game'. The event bubbles and is composed, so a single listener ondocumentcatches every widget on the page.Emission fires once when data resolves and again only when the data's identity changes:
markets,scoreandclockare excluded from the identity signature, so odds ticks do not re-fire it. An emptyitemsarray is never emitted. Hosts must replace their structured data on each event rather than append.Widgets carrying a signed-in user's own data, private content, or page chrome deliberately do not emit.
Integration caveats: fixes from a cross-widget review of every shipped widget from an integrator's point of view.
Cross-cutting fixes:
@gomagaming/core'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
Event-contract audit: six fixes across shipped widgets. Two of them change behaviour for existing embeds: read the last two entries before upgrading.
The outright card's competition star now has a login gate: the star on
OutrightRegularOdds(shown for_type === 'TOURNAMENT'events, and so in<goma-events-horizontal>,<goma-favorites>and<goma-competitions-list>) was the only star in the family that skippedguardedFavoriteToggle. Tapped while signed out it wrote and emitted anyway, contradicting the promise in the cross-widget wiring guide that a signed-out star does not write. It now emitsgoma:login-requiredwhen signed out, and writes the sharedgoma:favorites:v1envelope (instead of reading a widget-local store slice) when signed in, so it agrees with every other competition star on the page. Itsgoma:favorite-togglepayload gainstypeandidand dropseventId, which held a competition id: a host resolving that value as an event id was looking up the wrong entity.type: 'competition'+idcarry the same information under honest names, and match what the other competition stars have always emitted.<goma-filters-settings>now moves the feed. Applying a time window emitted onlygoma:filters-apply, which no feed widget listens for, so the mobile filter funnel left every sibling feed exactly where it was, while the identical window picked in<goma-time-filter>moved it.applyFilters()now also emitsgoma:time-select({ timeValue, timeLabel, previousTimeValue }, the same payload<goma-time-filter>emits) when the window actually changed, immediately before the unchangedgoma:filters-applysummary event.<goma-game-details>gainseventNavigateSelector(event-navigate-selector, default'').goma:event-navigatehas about a dozen emitters andfollowEventNavigateis on by default, so two game-details panels on one page both jumped to every card tap anywhere. The new prop is the same CSS-selector scope guard<goma-events-horizontal>and the picker followers already carry, matched against the event's retargeted host. Empty (the default) keeps today's follow-everything behaviour.goma:outcome-deselectno longer puts an outcome id inbettingOfferId. Removing a row from the betslip panel emitted{ bettingOfferId: <outcomeId> }, while the card-side odds button sends a real offer id in that field: a host deduping ondetail.bettingOfferIdgot mismatched keys from the two surfaces. The panel now emits{ bettingOfferId: null, outcomeId }, the card emitsbettingOfferId: nullrather thanundefinedwhen there is no offer, and theGomaEventMaptype widens to{ bettingOfferId?: string | null; outcomeId: string }. Dedupe onoutcomeId. Both surfaces always carry it.Behaviour change:
<goma-hero-card>'s favourite star now writes shared state. The star was localrefstate emittinggoma:favorite-select, an event name no other widget uses, so a hero card and an events-horizontal card for the same match disagreed on the same page. It now goes throughguardedFavoriteToggleand the sharedgoma:favorites:v1envelope, and emits the canonicalgoma:favorite-toggle. If your page mounts<goma-favorites>or otherwise persists that envelope, a tap here is now persisted where it previously was not. TheisFavoriteprop becomes a seed (mirrored into the envelope once, while signed in) rather than a parallel source of truth.goma:favorite-selectis deprecated: still emitted alongsidegoma:favorite-togglefor one release so existing listeners keep working; migrate togoma:favorite-toggle, it will be removed next.Behaviour change:
<goma-top-competitions-pills>now follows the sport picker by default.followSportPickerflips fromfalsetotrue, matching<goma-region-filter>,<goma-competitions-list>and<goma-events-horizontal>, so swapping one of those for this widget no longer silently loses sport-following. An existing embed that mounts this widget alongside a sports picker and relied on the pill row staying put will now re-fetch on every sport change. Setfollow-sport-picker="false"(orel.followSportPicker = false) to keep the old behaviour.<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.24.0
- game-details: emit a new
goma:game-endedevent when a match the widget saw live reaches a state it never returns from. Detection is driven by the event's status (ended,cancelled,walkover,abandonedorretired) because the provider keeps a finished event in the feed and moves its status rather than removing the event. An interrupted match deliberately does not fire it: play can resume, and ending there would permanently close a sibling chat on a match that comes back. Two fallbacks cover the rest: a host feeding nostatusIdfalls back to a live→not-live transition, and an event that does disappear from the feed fires once the removal outlives a short grace window (so a re-subscribe blipping the slot can't raise a false end). The payload carries the final teams + derived match score ({ eventId, homeTeam, awayTeam, homeScore, awayScore }), snapshotted on every live tick so it survives the event's removal, and latches once per event. A sibling<goma-live-chat>(or any host) can listen to close the chat. Matches already finished on arrival were never observed live and render the ended state instead of emitting.
0.23.0
Finished matches leave the feed, and game details can no longer skeleton forever
Tapping a card whose odds all showed
-opened a game-details view that sat on skeletons indefinitely: no markets, no empty state, no way out. Pulling that thread turned up several independent causes, plus a set of match-lifecycle behaviours the widgets had never carried over from the source sportsbook.Nothing handled a match ending. The provider deletes an ended event's betting offers but keeps the event itself in the aggregator for a while, so the feed kept rendering a card with
-for every odd, and that card still opened a details page with nothing left to bet on. Events that have reached a terminal state (ended, cancelled, walkover, abandoned, retired) are now filtered out of<goma-events-horizontal>, and<goma-game-details>renders a dedicated "this game has ended" state: the header stays so the teams and competition remain readable, but the score, match clock, LIVE badge and kickoff time all go. Interrupted matches are untouched: they can resume. Host-fed feeds can mark an event by settingstatusIdon it.isLivewas decided once, at dump time, and never revisited. A match that kicked off mid-session kept rendering as prematch until the next full dump, and one that finished kept its live styling. Match updates now recompute it: on the live list it up-flips at kick-off and never down-flips (only a server delete takes an event out of that list) while on the single-match surface behind<goma-game-details>it tracks the status in both directions.extraTimeis cleared on status and match-part transitions.A match delete arriving mid-load flashed not-found over a match that was about to appear. Deletes for the previous subscription are ignored while a match-details dump is still in flight.
Odds went missing from cards whose offer arrived in an earlier dump. On feeds that accumulate (favourites, competition detail, banners, paginated lists) only new entities are mapped, but they were being matched against the new offers alone. An outcome whose offer was already in the store found nothing and rendered
-for a price that was present and ticking. Outcomes are now mapped against the full offer pool on every accumulating feed. This is a second, independent source of the-cards above.Loading was inferred from a flag that only the initial dump could clear. Game details gated its skeleton, its empty state and its not-found state on loader flags that ship as
trueand are cleared solely by the WAMPafterInitialDumpcallback. Every path where that dump never lands (a rejected initial-dump RPC, a topic that doesn't exist, a subscribe that bailed before the socket was ready) therefore pinned the widget on skeletons AND made both terminal states unreachable, because each is a!loadingbranch. "Answered" is now a lifecycle fact per event and per market tab, with a 15-second watchdog, so a feed that never replies falls through to the right terminal state instead of waiting forever. A subscription that starts late (a host that setsconfigaftereventId) also counts as a fresh cycle, so a slow start is not mistaken for a missing match. The subscribe actions clear their own flag when they bail out or when the provider module swallows a failure.The terminal states could latch permanently. The match-details subscriptions were owned by the header and markets-list components, which sit behind the ended and not-found branches, so entering either state unmounted the very watchers that would have re-subscribed, and an in-flight dump was discarded with nothing left to retry it. Both now live at widget scope, as they do in the source sportsbook, and the markets feed is released explicitly when a match ends rather than as a side effect of a component unmounting.
The default market tab could point at an empty feed. The active tab was picked as the first market group by position, falling back to the group's
idwhen it had nogroupKey: on sports with sparse market metadata that selected a group with no markets, or built a topic that does not exist. Groups the provider does not confirm as carrying markets, and groups with nogroupKey, are now hidden from the tab strip and skipped by the auto-select; among the rest theisDefaultflag wins, then the leftmost. With no usable group the widget stays on the catch-allallMarketsfeed, which carries every market the match has.Match-part names matched fewer languages than the source. The part-name patterns are now the union of both sets, adding the Finnish, Romanian and Estonian forms, the reverse word order (
Set 2as well as2nd Set) and whitespace-free variants (2.Set). Part names decide which bucket a score row lands in, so this affects score rendering wherever those markets appear.@gomagaming/sports-domaingains four exports for the above:isTerminalEvent,extractEventStatus,EVENT_STATUSESandTERMINAL_EVENT_STATUS_IDS. Mapped feed events and host-fed events both now carrystatusId.
0.21.0
game-details: make the market list's bulk collapse control persistent and two-way
The "Close all" control broadcast a one-shot signal that only the market cards mounted at that instant could hear. Because the list mounts cards lazily in batches as you scroll, every card revealed after the tap came in expanded, so "close all" visibly failed to close all. It was also collapse-only, with no way to re-open the cards in bulk.
The list now owns a persistent expanded/collapsed state that cards seed from at mount and follow thereafter, and the control toggles between Close all and Expand all (the
expand_allmessage key was already bundled in all locales). Late-mounted cards match the current state in both directions. Switching market-category tab resets the control to its expanded default.
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.17.0
- Missions Finished-tab history card fixes: the forfeited outcome now uses the shared
alertErrortoken consistently. The Forfeit pill, the per-level ✕ circle, and the "Forfeited" status text all paint--goma-alertError(replacing a maroonmyTicketsLostpill and a hardcoded#be170fred). The token default is corrected to its design-system value#f9554d. The "X/Y levels" count now counts only levels the player actually completed, so a fully-forfeited challenge reads0/4instead of4/4.
0.16.0
GomaPulse UX review (round 3):
- Sport icons: dedicated glyphs for the eSports titles (FIFA, NBA2k, Counter-Strike, DOTA 2, Overwatch) plus a shared outline-controller default for every other eSports category, replacing the legacy generic gamepad; corrected Beach Volleyball and Hurling glyphs.
- Casino card: nav-arrow disc uses
backgroundBorder; restyled Play pill (8px radius, 14px, weight 600,buttonTextPrimary, outline play icon); title gains a soft white glow on hover; added the design's top-edge line + radial top glow (driven byhighlightPrimary); removed the hover lift. - Multi-banners: favourite star now matches the app's outline star.
- Country flag ring (shared): uses the theme-fixed
allWhitetoken so the ring stays white under any consumer theme. - tabPill pressed state: added the 80%-opacity pressed treatment on the My Bets and Game Details pills.
- Sports selector: the widget host now carries a default 8px corner radius.
- Suggested-bet leg: sport-icon colour uses
iconPrimary.
0.15.2
GomaPulse UX-review round 2: visual/token fixes:
- core: official flat sports-icon set (46 mainstream/virtual/eSports-category glyphs across dark + light modes, keyed by sport id). Inherited app-wide via
SbIcon; newcasino_games_playi18n key (en/fr/pt). - casino-games: official flame glyph, hover "Play" overlay on tiles, title→tiles / tiles→dots spacing.
- multi-banners: header now self-resolves the sport glyph from the event's sport id, renders the country flag via the shared
CountryFlagcomponent (newflagBaseUrloption, defaults to the OM flag CDN), and gains a 1px card border. - sports-navigation-horizontal: the rounded container now clips its content so the 8px corners render.
- suggested-bet: leg sport icons use the flat bundled icon set.
- game-details / missions-hub / sports-domain (My Bets): tabPill hover + pressed states (violet ring); My Bets ticket icon sizing (12px sport + flag) and centered totals summary.
- core: official flat sports-icon set (46 mainstream/virtual/eSports-category glyphs across dark + light modes, keyed by sport id). Inherited app-wide via
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
Fix: game-details now participates in the cross-widget betslip sync (
goma:betslip:v3). Selecting/deselecting an odd propagates to the betslip widgets, and a removal in the betslip deselects the odd. Also, a selection's market name now resolves frombettingTypeName/translatedName(used by game-details markets) instead of falling back to the "Market" placeholder.game-details design polish:
- Header uses the
gameHeadersurface; the competition-breadcrumb separator is a cyan glow. - Market-category pills are outlined when active (cyan border) on a
navPillsbar, instead of a solid fill. - Market cards use the cyan gradient border (matching the events-horizontal card) and the market title no longer has a separate background.
- New Close all button collapses every market card (adds the
close_alllocale key to@gomagaming/core).
- Header uses the
Live badge + live-card surface polish:
- The live game card (
LiveRegularOdds) live badge is now a cyanLIVEtab pinned flush to the card's top-right edge (rounded on the left, a record-dot glyph), and the card surface reads thebackgroundPrimarytoken instead ofbackgroundDrop. - The
game-detailsheader live badge matches the same cyan flush-tab treatment.
- The live game card (
0.9.1
Widget fixes surfaced while building the GomaPulse client POC desktop Sports page.
sports-navigation-horizontal
- New
showArrowsprop (defaulttrue): circular prev/next paging arrows at the strip edges, shown only when the content overflows and there's room to scroll that way. Each arrow auto-hides at its end; tapping pages the strip by ~80% of the visible width.
game-details
- Fixed a race where setting
eventId+configtogether on mount left the widget stuck on skeletons forever: the match / market-group / markets subscriptions fired before the WAMP adapter finished connecting and never retried (eventId never changed again). They now also depend on the actions container'sreadysignal and read actions lazily, so they re-fire the moment the adapter connects. Previously only worked when the event was selected after the widget had connected (e.g. a card tap). - Fixed the header back-button glyph pointing the wrong way (a rotated CSS border corner); it's now a left-pointing chevron.
- New
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
0.7.0
Playground scenario presets, live theme-token editor, and family-wide picker-coupling defaults.
Widget runtime defaults
- events-horizontal:
followSportPicker,followRegionPicker,followCompetitionPicker,followTimePickerall flip fromfalsetotruein the element-class defaults and the playground manifest. Drop a sibling picker or filter on the page and the events feed auto-couples without any explicit config. Consumers that don't want the coupling set the flag tofalse(or the matching HTML attributefollow-sport-picker="false", etc.). - region-filter:
followSportPickerdefault flips totruein the element class + manifest. The widget will re-fetch the region list whenever a sibling sports picker emitsgoma:sport-select. - competition-filter:
followSportPickerandfollowRegionPickerdefaults both flip totrue. Drops the manual wiring step from the picker-chain composition. - region-filter / competition-filter: playground manifest's
sourcedefault flips from'mock'to'rpc'. Aligns the manifest with the element class's owndataSource: 'rpc'default; consumers of the published widgets are unaffected (mock was a playground-only convenience).
Breaking-ish note: a host page that mounts multiple of these widgets side-by-side without setting
follow*Pickerexplicitly will now see them auto-react to each other's selection events. The old isolated behaviour is restored withfollow*Picker: false.Playground SPA additions (not published; bundled into the docs site at
/playground)- Scenario presets: a named scenario is an ordered set of widgets
- per-instance configs + theme + token overrides + locale. Three built-ins ship:
full-betting(filters row + sports nav + events + game details + sidebar betslip),mobile-layout(dialog picker + events + floating betslip), anddark-neon-blue-showcase. TopBar gains a Scenario<select>that replaces widgets / configs / theme in one mutation; the dropdown auto-flips to "Custom" the moment the user diverges from the preset.
- per-instance configs + theme + token overrides + locale. Three built-ins ship:
- Live theme-token editor: new Inspector "Theme" tab grouped by prefix (Backgrounds, Text, Icons, Highlights, …). Edits write through
store.setThemeToken('--goma-X', '#hex')to the global override layer (sparseRecord<--goma-key, hex>), which Stage's fast-path watcher pushes viael.theme = …to every mounted element._applyTheme()writeshost.style.setProperty(--goma-X)reactively, so token edits repaint with no Vue remount. - Share URL: v3 envelope additively carries
themeTokensandactiveScenario. Default values are dropped from the encoded payload (size optimisation, hydrate re-applies defaults). Foreign payloads with poisonedthemeTokens(non-object, arrays, non-string values) are normalised back to{}inhydrate(). dark-minttheme renamed todark-neon-blue: palette pulled verbatim from the new Figma tokens export (Uniclub Dark Neon Blue). Highlights / live tags / button-primary flip from mint green (#5cffc0,#00ffbc) to neon cyan (#29f2ff); secondary accent flips from indigo#5b56ffto purple#8750ff. Backgrounds, text, icons, separators, dialog background are unchanged. New tokens added: inputs, alerts, betslip, game header, hero card, tickets, stats, shadows. Persisted state or share URLs that pintheme: 'dark-mint'fall back toTHEMES.defaultafter hydrate.
Tests
- New
tests/playground-scenarios.test.js: 19 vitest cases coveringapplyScenario,setThemeToken+activeScenarioCustom-flip,resetThemeTokens, share URL round-trip with the new envelope fields, hydrate normalisation of malformed payloads. - New
tests/integration/playground-scenarios.spec.ts. 4 Playwright cases: scenario → DOM mount, dark-neon-blue palette assertion (backgroundPrimary+highlightPrimary), live token edit without remount (element identity preserved), share URL round-trip across pages. tests/integration/playground-inspector.spec.ts: tab list extended with the new "Theme" tab.
- events-horizontal:
0.6.0
New widget:
<goma-competition-filter>. Horizontal pill row of competitions (tournaments, leagues) with a hardcoded "All" pill on the leftmost slot, single-selection state, and optional competition icons resolved officonBaseUrl(same convention as sport-navigation,competitions/{id}.svg). Sister widget to<goma-region-filter>; same data-source surface, same picker-chain pattern, plus an additional client-side filter wired to the region picker.What's new
- New package
@gomagaming/competition-filter. Element:<goma-competition-filter>. Reference docs:docs/widgets/competition-filter.md. - Hardcoded "All" pill emits
{ competitionId: 'all', competitionName: <localised> }viagoma:competition-select: maps straight to the matches-aggregator topic's literalallno-filter tournamentId segment. - Opt-in
followSportPicker(re-fetch on sport-select) andfollowRegionPicker(client-side filter onvenueIdvia the picked regionId;'all'disables). Both resetselectedCompetitionIdto'all'and emitgoma:competition-selecton context change so downstream listeners clear their tournamentId filter in the same tick.
@gomagaming/events-horizontaladditions- New
followCompetitionPicker+competitionPickerSelectoropt-in props. Third in the picker chain: listens forgoma:competition-selectat the window and rewrites segment index 2 (tournamentId) of the matches-aggregator topic. The existingrewriteTopicSegment(topic, idx, value)helper does the lifting: adapter-resolved prefix when available, canonical'custom-matches-aggregator/'fallback otherwise (works in mock mode + pre-WAMP-connect). - Playground manifest gains a
followCompetitionPickertoggle under "advanced".
@gomagaming/sports-domainadditionsuseCompetitionsSource(state): public composable. Watchesstate.sportId+state.regionId+state.dataSource; callsoddsApi.betting.getCompetitions({ sportId })on connect and mirrors avenueId-filtered slice ofbettingStore.competitionsintostate.competitions. The region filter runs client-side without requiring a re-fetch.subscribeToCompetitions({ sportId, afterInitialDump })stub on the everymatrix betting module (warn-once + RPC fallback, same shape assubscribeToLocations). Backend topic for live competition updates is deferred.
@gomagaming/coreadditions- Four new bundled i18n keys per locale (en/fr/pt):
competition_filter_all_label,competition_filter_aria,competition_filter_empty,competition_filter_live_events_count(vue-i18n 3-form plural for the live-count badge).
gomawt playground
competition-filterjoins the registry, typesWidgetIdunion, storeALL_WIDGETS, VitePress theme side-effect import, Stage event-log allow-list (goma:competition-select), and EventLogPanel category classifier.- demo entry-file imports added to playground/isolation/stress/ soak entries; workspace dep +
optimizeDeps.excludeentry indemo/.
Three-layer chain ready out of the box: drop a sports navigation picker, a region filter, a competition filter, and an events-horizontal feed on the same page; turn on the relevant
follow*Pickerflags and the topic chain{sportId}/{locationId}/{tournamentId}updates segment-by-segment with no host glue and no WAMP reconnect.- New package
Drop the unused
operatorIdfield from the public widget config surface. The field was declared in@gomagaming/coredefaults + TypeScript types but never read anywhere on the data path:ucsOperatorIdis the only operator identifier the EveryMatrix adapter (anduseBetslipLogic) consume. Settingconfig.operatorIdon a widget element now logs an "unknown config key" warning indebug: truemode instead of being silently accepted.Migration: consumers using
config.operatorIdshould rename toconfig.ucsOperatorId. No behavioural change for consumers who already only setucsOperatorId(which the docs have always recommended).@gomagaming/corechanges:createWidgetElement.js:COMMON_DEFAULTSno longer seedsoperatorId; theknownTopLevelKeysvalidator set drops it.types/index.d.ts: removedoperatorId?: stringfrom theGomaWidgetConfiginterface.
@gomagaming/betslip-sidebardocstring tweak:custom-elements.jsonexample endpoint placeholder changed from/place-bet/4313/v2/betsto/place-bet/2838/v2/bets(matches the staging operator id used by the playground EM demo preset). No behavioural change.
gomawt playground:
- The "Connection" panel drops the
operatorIdinput row; share-URL envelope, persistence, types, and preset filler all scrubbed in lockstep so older share URLs that carriedoperatorIdare silently ignored on hydrate (the field falls out ofSTATE_DEFAULTSand the validator'sstringFieldslist). - New mobile UI: ≤760 px viewport gets a bottom tab bar (Widgets / Stage / Inspect) routing the body panels; the topbar folds Mode/Theme/Locale/API URL behind a
⋮disclosure; Connection panel grid collapses to a single column; tab targets meet 44 px touch-target guidance and inputs use 16 px font to suppress iOS focus-zoom. - EM demo preset refreshed to the NDW staging endpoint (
wss://api-ndwstage.everymatrix.com/v2, realmwww.demo1-stage.everymatrix.com, ucsOperatorId2838).
VitePress docs site:
- Locked to dark mode (
appearance: 'force-dark'): the appearance toggle is hidden from the nav. The playground's dark--bg-*tokens no longer two-tone against a light VitePress chrome. - Homepage, introduction, publishing, and widget reference docs refreshed to current state: nine widgets shipped, eleven publishable packages in the lockstep group, example URLs scrubbed of the previous client-specific staging hostnames (now use generic
api.example.com/sports-api.example.complaceholders), exampleucsOperatorIdliteral updated to2838. Three new widget docs (region/competition/time-filter) added to the homepage feature sections.
New widget:
<goma-region-filter>. Horizontal pill row of regions (countries) with a hardcoded "All" pill on the leftmost slot, single-selection state, and an optional flag glyph per pill. Sources its region list from a one-shot WAMP RPC call on connect, with a placeholdersubscribemode that currently falls back to RPC until the backend exposes a live-update topic for locations.What's new
- New package
@gomagaming/region-filterpublished in lockstep with the existing widgets. Element:<goma-region-filter>. Reference docs:docs/widgets/region-filter.md. - Hardcoded "All" pill emits
{ regionId: 'all', regionName: 'All' }viagoma:region-select(canonical "no filter" sentinel, matches the matches-aggregator topic format wherelocationId='all'). - Flag glyphs rendered from a configurable CDN: default
flagBaseUrlis the upstream Glastcoper flag pack, resolved as${flagBaseUrl}{regionId}.png. Falls back to a globe glyph when a flag fails to load. - Opt-in
followSportPickerautomation (mirrors<goma-events-horizontal>): when enabled, the widget listens at the window forgoma:sport-selectevents emitted by sibling sports-navigation widgets and rewrites itssportIdso the region list refetches automatically. OptionalsportPickerSelectorscopes the listener to a specific picker for multi-instance disambiguation.
@gomagaming/events-horizontaladditions- New
followRegionPicker+regionPickerSelectoropt-in props. Mirror the existingfollowSportPickerplumbing but listen forgoma:region-selectand rewrite the locationId segment (index 1) of the matches-aggregator topic. The region-filter's hardcoded "All" pill emitsregionId: 'all'which lines up exactly with the matches-aggregator topic's literalallno-filter segment: no special-casing needed. Drop a<goma-region-filter>alongside an events-horizontal feed and setfollow-region-pickeron the latter; selections flow through with no host glue and no WAMP reconnect (the existinguseTopicSubscriptionwatcher rebinds the topic in place).
@gomagaming/sports-domainadditionsuseLocationsSource(state): public composable that drivesstate.regionsfrom the WAMP API. Watchesstate.dataSource,state.sportId, and the apiContainer's connection; on connect, callsoddsApi.betting.getLocations({ sportId })and mirrorsbettingStore.locationsinto the widget's reactive state.subscribeToLocations({ sportId, afterInitialDump }): new stub method on the Everymatrix betting module. Logs a one-time warning and falls back to one-shotgetAllLocationsuntil the live topic is wired backend-side. Returns a no-op unregister so the composable's cleanup contract stays uniform with the other subscribe methods.
@gomagaming/coreadditions- Three new bundled i18n keys across
en.json,fr.json, andpt.json:region_filter_aria,region_filter_empty.
gomawt playground
region-filterjoins the playground registry, storeALL_WIDGETS, typesWidgetIdunion, and VitePress theme side-effect imports. The embedded/playgroundroute can mount it alongside the other six widgets.
- New package
New widget:
<goma-time-filter>. Horizontal pill row of time-interval filters with a hardcoded "All" pill on the leftmost slot. Pure host-fed; no WAMP transport, no sport-scoping, no API container.Lighter than the rest of the filter family: no
setupAppplumbing on the element, no shared composable in@gomagaming/sports-domain. Just anintervalsprop, aselectedTimeValueselection state, and agoma:time-selectevent.What's new
- New package
@gomagaming/time-filter. Element:<goma-time-filter>. Reference docs:docs/widgets/time-filter.md. - The host feeds
el.intervals = [{ label, value }, …].labelis rendered verbatim (hosts pre-localise, only the hardcoded "All" pill is locale-driven, via the bundledtime_filter_all_labelkey).valuematches the matches-aggregator topic's hoursInterval segment ('0-1','0-24','24-48', etc.). Malformed entries drop silently; duplicates byvaluekeep the first occurrence. - The hardcoded "All" pill emits
{ timeValue: 'all', timeLabel: <localised>, previousTimeValue }viagoma:time-select: maps straight to the matches-aggregator topic's literalallno-filter hoursInterval segment.
@gomagaming/events-horizontaladditions- New
followTimePicker+timePickerSelectoropt-in props. Fourth in the picker chain. Listens forgoma:time-selectat the window and rewrites segment index 3 (hoursInterval) of the matches-aggregator topic withdetail.timeValue. Reuses the existingrewriteTopicSegment(topic, idx, value)helper (with its canonical-prefix fallback so mock-mode + pre-WAMP-connect rewrites still land). - Playground manifest gains a
followTimePickertoggle under "advanced".
@gomagaming/coreadditions- Three new bundled i18n keys per locale (en/fr/pt):
time_filter_all_label,time_filter_aria,time_filter_empty.
gomawt playground
time-filterjoins the registry, typesWidgetIdunion, storeALL_WIDGETS, VitePress theme side-effect import, Stage event-log allow-list (goma:time-select), and EventLogPanel category classifier.- demo entry-file imports added across playground/isolation/stress/ soak; workspace dep +
optimizeDeps.excludeentry indemo/.
Four-segment chain ready out of the box: drop a sports navigation picker, a region filter, a competition filter, a time filter, and an events-horizontal feed on the same page; turn on the relevant
follow*Pickerflags and the topic chain{sportId}/{locationId}/{tournamentId}/{hoursInterval}updates segment-by-segment with no host glue and no WAMP reconnect.- New package
0.5.2
- packages: wire up the now-live
gomawt.gomagaming.comdocs subdomain across npm-facing surfaces. Adds ahomepagefield to each of the eight publishablepackage.jsonfiles so the npm sidebar shows a "Homepage" link again (widget packages point at their per-widget docs page under/widgets/<name>;@gomagaming/coreand@gomagaming/sports-domainpoint at the site root). Replaces the two_coming soon_Documentation bullets in each README with real links togomawt.gomagaming.com/widgets/<name>(full reference) andgomawt.gomagaming.com/playground(live playground). No code change.
0.5.1
events-horizontal: flip
followSportPickerdefault totruein the playground manifest so integrators landing on the playground see the events feed auto-follow any sports picker they enable. The shipped widget's element-class default (followSportPicker: false) is unchanged: consumers who don't opt in viaconfig.followSportPickeror the HTML attribute still get the old behaviour.Playground + maintenance polish:
@gomagaming/events-horizontalnow exports its ownpackage.json("./package.json": "./package.json"in the exports map). Lets consumers read the package version without import-resolution errors; the embedded playground uses this to render its lockstep version badge.@gomagaming/sports-navigation-dialog's playground mock fixture now driftsliveCountevery 4 seconds, matching the horizontal sister. Previously itssubscribe()was a no-op; the dialog's badge UX now exercises while the dialog is open. Initial dataset unchanged.- Source comments scrubbed of stale internal references throughout the eight published packages. No behaviour change.
packages: scrub private-repo URLs from npm-facing metadata and rewrite client-facing READMEs. Removes
repository,homepage, andbugsfields from all eight publishablepackage.jsonfiles: the gomawt GitHub repo is private, so these fields were leaking a 404-on-anonymous link into the npm sidebar. Rewrites the six widget READMEs and the two internal-package READMEs (@gomagaming/core,@gomagaming/sports-domain) with a client-dev-targeted template: blockquote one-liner, framework-agnostic note, install snippet with.npmrc+NPM_TOKENauth blurb, widget-specific quick-start, and placeholder Documentation section (the canonical docs subdomain and public playground URL are pending DevOps; they'll fill in via find-and-replace on_coming soon_and a singlehomepagefield add perpackage.jsononce available). No code change.
0.5.0
New widget:
<goma-game-details>. Page-shaped Web Component that renders the full details for one sporting event. Header (teams, score, clock) + market-category tabs + fixed-height scrollable list of collapsible market cards with IntersectionObserver-driven batched rendering. Fills its parent; the host owns presentation context (route slot, drawer, modal, sized column, their choice). a separate change.Cross-widget cooperation works out of the box: drop a
<goma-events-horizontal>and a<goma-game-details>on the same page and card taps drive the details widget via the existinggoma:event-navigateevent (opt-out viaconfig.followEventNavigate: false).What's new
- New package
@gomagaming/game-detailspublished in lockstep alongside the existing widgets. Element:<goma-game-details>. Reference docs:docs/widgets/game-details.md. - New per-component subscription pattern (
.subscription.jsfiles) port the source post-a separate change architecture: each internal component (Header / MarketsList) owns its own WAMP subscription withonScopeDisposecleanup. Multi-instance safe: each<goma-game-details>mount gets its ownuseGameDetails()state andcreateMatchDetailsActions()closure. - Five new theme tokens with CSS-level fallbacks to existing tokens:
--goma-tabPillBackground,--goma-tabPillBackgroundActive,--goma-tabPillText,--goma-tabPillTextActive,--goma-marketCardHeaderBackground. Documented indocs/guide/theming.md → Game details. - New
marketsListHeightconfig knob (mirrored as themarkets-list-heightHTML attribute). Accepts any CSS length ('480px'default,'70vh','calc(100vh - 200px)', …): bounds the markets-list scroll viewport so the widget doesn't sprawl in hosts that don't constrain it.
@gomagaming/sports-domainadditionsactions.sports.matchDetails.*: six new WAMP actions lifted from the source repo's actions tree:subscribeToMatch/unSubscribeToMatch,subscribeToMarketGroups/unRegisterToMarketGroups,subscribeToMarketsByMarketGroupKey/unSubscribeToMarketsByMarketGroupKey. Per-instanceunregisterclosure viacreateMatchDetailsActions(): co-mounted widgets don't share WAMP handles.- New public exports:
useOddsApiActions,useActionsContainer,useLoader,useGameCardsTallOdds,RegularOdds,formattedUnixDate,formattedUnixTime,connectSportsApiReactive. These were already used internally; opening them up so external widgets (incl. this one) can compose without deep imports. adapter.createActions(router, apiContainer): adapter signature widened. Action factories close overapiContainerand readoddsApifromapiContainer.oddsApi.valuedirectly instead ofuseOddsApi()/inject(). Required because action methods are invoked from watcher callbacks that fire outside Vue's setup context (e.g., when a WAMPonInitialDumpcascades through a reactive store patch into a watcher), whereinject()returnsundefined. The closure path is reliable regardless of where the callback runs. Backwards-compatible: existing adapters that ignore the second arg keep working.useLoaderloader store gainsmatchDetailsMarketGroupMarketsflag (used by the markets-by-group action'safterInitialDumpcallback).
@gomagaming/coreadditions- Bundled i18n keys for game-details added across
en.json,fr.json, andpt.json:empty_state_match_not_available_*,empty_state_match_no_markets_*,market_categories,collapse. createWidgetElement'sCOMMON_DEFAULTSnow declare the provider / connection fields (provider,socketUrl,socketRealm,bettingApiBaseUrl,operatorId,ucsOperatorId) so every widget's reactive state bridge auto-tracks them. Setting them viael.config = { socketUrl: '…', … }propagates into the widget's_state: the newconnectSportsApiReactivehelper watches that and reconnects on runtime change.
Connection-reactive widgets (all five publishable widgets + game-details)
- Every shipped widget now uses
connectSportsApiReactiveinstead of the one-shotconnectSportsApi(...)call insetupApp. Hosts can changesocketUrl(and the other connection fields) at any time after mount and the widget tears down the old WAMP handle and re-establishes against the new params. Use case: the gomawt playground's "Apply" button for the Connection panel; any host that rotates credentials / switches environments without remounting the widget. - New public export from
@gomagaming/sports-domain:connectSportsApiReactive({ app, host, router, apiContainer, actionsContainer, name }).
gomawt playground
- New "Connection" disclosure in the TopBar with six inputs (
provider,socketUrl,socketRealm,bettingApiBaseUrl,operatorId,ucsOperatorId), Apply / Revert buttons, share-URL- localStorage persistence.
- Live connection-state badges on each widget card (driven by
goma:connectionevents). - Generic mock-fixture plumbing: fixtures can now declare arbitrary top-level keys (not just
events/sports); the Stage applies each as a JS property on the element. - Game-details added to the rail with a mock fixture (Arsenal vs Chelsea, four market groups, six markets, drifting odds + live clock).
- Events-horizontal's
subscribemode now auto-builds the WAMP topic from the manifest'ssportId+eventLimitwhen the user hasn't settopicexplicitly.
Follow-up
BetBoosttab, share button, favorite toggle, stats / lineups / H2H / commentary are explicit v2+ surfaces (recorded indocs/widgets/game-details.md → What's deferred).
- New package
Playground manifest schema additively extended (Phase 4 inspector parity with
demo/).@gomagaming/coreexports new control types (slider,textarea,json), two new tag values ('transport','mock'), an optional per-controldependsOn: { key, oneOf }field for conditional visibility, an optional top-levelthemeTokensper-widget token registry, and an optionaltopicBuilderfunction field for read-only WAMP topic previews. Phase 2 manifests continue to validate without modification (regression guard in the schema test suite).Every shipped widget's
playground.manifest.jswill be expanded in subsequent commits on this branch with the full per-widget configuration surface previously only exposed in the internaldemo/sandbox (transport block, mock-streaming knobs, full theme tokens, place-bet JSON fields where applicable). After this block the playground SPA can render the new control types and respectdependsOnfiltering; the per-widget manifests catch up in Block D.