<goma-sports-navigation-horizontal>
Horizontal swipeable strip of sports: circular icon + label per tile, single-selection state with a themed ring around the selected tile, optional live-event count badge. Sources its sport list from a host-fed array, a one-shot WAMP RPC call, or a live WAMP subscription.
Live demo
Open in the playground →: toggle Sports navigation (horizontal) in the widget rail, then switch between prop / rpc / subscribe sources in the inspector to see how each feed shape renders. Live-count badges drift on the mock source so you can verify the badge UX without a real WAMP feed.
Element tag
html
<goma-sports-navigation-horizontal id="nav"></goma-sports-navigation-horizontal>Installation
@gomagaming/sports-navigation-horizontal is a scoped private package. See Installation for the one-time .npmrc setup required to consume it.
bash
npm install @gomagaming/sports-navigation-horizontalYou also need the shared peer dependencies installed at your app level:
bash
npm install vue pinia vue-router vue-i18n @vueuse/core autobahn-browser swipervue, pinia, vue-router, vue-i18n, @vueuse/core, autobahn-browser, and swiper are declared as peerDependencies so a consumer with multiple @gomagaming/* widgets gets exactly one copy of each.
Configuration
Assign a config object to the element's config property:
js
const el = document.getElementById('nav')
el.config = {
socketUrl: 'wss://api-ndwstage.everymatrix.com/v2',
socketRealm: 'www.yes.dk',
apiBaseUrl: 'https://api-ndwstage.everymatrix.com',
bettingApiBaseUrl: 'https://sports-api.example.com',
ucsOperatorId: '2838',
locale: 'en',
iconBaseUrl: '/dist',
}| Key | Type | Required | Description |
|---|---|---|---|
socketUrl | string | Yes (rpc/subscribe) | WAMP socket URL. Only consulted when dataSource is 'rpc' or 'subscribe'. |
socketRealm | string | Yes (rpc/subscribe) | WAMP realm. |
apiBaseUrl | string | Yes (rpc/subscribe) | REST base URL. |
bettingApiBaseUrl | string | No | REST base URL for betting offers. |
ucsOperatorId | string | Yes (rpc/subscribe) | Operator ID: used to build the per-tenant subscription URL. |
locale | string | No | i18n locale. Defaults to 'en'. |
iconBaseUrl | string | No | Base URL where icon SVGs are served from (${iconBaseUrl}/icons/sports/dark_mode/{iconId}.svg). |
theme | object | No | CSS variable overrides: see Theming. |
appearance | object | No | Structure overrides (sizes, spacing): see Appearance. |
messages | object | No | i18n overrides per locale. |
debug | boolean | No | Verbose console logging (config-validation warnings, connection-retry messages, etc.). |
In prop mode only iconBaseUrl, locale, theme, messages, and debug are consulted: no socket is opened.
Data sources
The dataSource property selects where the sport list comes from: the widget doesn't combine them. Its default is 'prop' with sports: [], so left untouched the strip renders no tiles; pick 'rpc' or 'subscribe' (or feed el.sports yourself) to get a populated widget.
1. 'prop': host-fed array (default)
Drive the list from outside. The widget never opens a socket.
js
el.dataSource = 'prop'
el.sports = [
{ id: '1', name: 'Football', iconId: 1, numberOfLiveEvents: 12 },
{ id: '2', name: 'Basketball', iconId: 2, numberOfLiveEvents: 0 },
{ id: '3', name: 'Tennis', iconId: 3, numberOfLiveEvents: 64 },
]numberOfLiveEvents is optional but enables the live-count badge and the live-first ordering (see Sort order).
2. 'rpc': one-shot WAMP call on connect
The widget calls apiSession.$wcall('/sports#disciplines', …) once the WAMP connection is up, populates its internal store, then renders. No live updates after the dump: call el.config = el.config (or change liveStatus) to re-fetch.
js
el.dataSource = 'rpc'
el.liveStatus = 'LIVE' // 'LIVE' | 'NOT_LIVE' | 'BOTH'
el.config = {
socketUrl: '...',
socketRealm: '...',
apiBaseUrl: '...',
ucsOperatorId: '...',
}liveStatus | Underlying RPC method | Equivalent upstream call |
|---|---|---|
'LIVE' | getLiveSports() | $wcall('/sports#disciplines', { liveStatus: 'LIVE', virtualStatus: 'BOTH', sortByPopularity: true }) |
'NOT_LIVE' | getPreLiveSports() | $wcall('/sports#disciplines', { liveStatus: 'NOT_LIVE', virtualStatus: 'BOTH', sortByPopularity: true }) |
'BOTH' | getAllSports() | $wcall('/sports#disciplines', { liveStatus: 'BOTH', virtualStatus: 'BOTH', sortByPopularity: true }) |
3. 'subscribe': live WAMP subscription
The widget registers to the per-tenant sports topic and renders updates as they stream in. numberOfLiveEvents (and the badge / ordering that uses it) stay accurate as games go live.
js
el.dataSource = 'subscribe'
el.liveStatus = 'LIVE' // 'LIVE' | 'NOT_LIVE' (BOTH falls back to LIVE)
el.config = { /* same as rpc */ }liveStatus | Underlying subscription | Topic URL |
|---|---|---|
'LIVE' | subscribeToLiveSports({ afterInitialDump }) | /sports/{ucsOperatorId}/{lang}/sports/LIVE/BOTH |
'NOT_LIVE' | subscribeToPreLiveSports({ afterInitialDump }) | /sports/{ucsOperatorId}/{lang}/sports/NOT_LIVE/BOTH |
'BOTH' | Falls back to subscribeToLiveSports | /sports/{ucsOperatorId}/{lang}/sports/LIVE/BOTH (no dedicated BOTH topic exists upstream) |
Subscriptions are torn down and replayed automatically when dataSource or liveStatus change, or when the WAMP connection drops and reconnects.
Props / attributes
| Property | Attribute | Default | Description |
|---|---|---|---|
dataSource | data-source | 'prop' | 'prop' / 'rpc' / 'subscribe'. See Data sources. |
liveStatus | live-status | 'LIVE' | 'LIVE' / 'NOT_LIVE' / 'BOTH'. Selects which RPC method or subscription URL to use. |
sports | sports | [] | Array of GomaSport objects. Driven by the host in 'prop' mode; populated from the WAMP API in 'rpc'/'subscribe' mode. |
selectedSportId | selected-sport-id | null | Id of the currently-selected sport. Two-way: a tile click also updates this value. |
spaceBetween | space-between | 12 | Pixel gap between tiles. |
showLabels | show-labels | true | Render the sport name beneath each icon. Turning it off is worth a thought where the family fallback applies: a category with no artwork of its own draws its family's glyph, so with labels off a parent tile and its children are pixel-identical. The name survives in aria-label and the hover tooltip. |
showLiveCount | show-live-count | true | Render the live-event count badge on each sport tile. Set false to hide it. |
skeletonCount | skeleton-count | 8 | Number of placeholder circles shown until the first sports assignment. |
showArrows | show-arrows | true | Show circular prev/next paging arrows at the strip edges when the content overflows. Each arrow auto-hides at its end; tapping pages the strip by ~80% of the visible width. Set to false to rely on swipe/drag only. |
appearance | appearance | {} | Structure overrides: tile width and label type. See Appearance. |
GomaSport shape
ts
interface GomaSport {
id: string | number
name: string
iconId?: string | number // resolves to /icons/sports/dark_mode/{iconId}.svg
numberOfEvents?: number
numberOfLiveEvents?: number // > 0 → renders the live-count badge
isVirtual?: boolean
isTopSport?: boolean
}When the WAMP modes populate the list, the entries are produced by mapSport() and include the full set of optional fields. The widget only reads id, name, iconId, and numberOfLiveEvents.
A sport whose iconId has no artwork in the bundled set (a category the feed ships before its glyph exists) falls back to its family's glyph: an e-sports category draws the e-sports icon rather than a generic placeholder. The family is read from the list you feed (the parent whose childrenIds names the sport); with no family in the list, the neutral noicon glyph stands in. Either way the tile never shows the browser's broken-image glyph. In the WAMP modes the family is looked up across the whole sport list the feed sent, not only the sports currently on screen: a parent category carries no events of its own, so it is routinely absent from the live slice, and the stand-in would otherwise degrade to the neutral glyph for every child.
Your own artwork is always asked for first: ${iconBaseUrl}/icons/sports/dark_mode/{iconId}.svg when a base URL is set, and the origin-relative /icons/sports/dark_mode/{iconId}.svg when it is not. iconBaseUrl has no value meaning "my own root" ('' and '/' both read as unset), so serving them from the root needs no configuration. A fallback appears only once that request has failed, which in a browser means the 404 you would expect for a sport nobody has drawn yet.
Sort order
Tiles are partitioned at render time:
- Sports with
numberOfLiveEvents > 0: sorted descending by live count. - All other sports: preserve their original feed order at the tail.
This applies in all three data-source modes. If the host sorts its sports array a particular way for prop mode, that order is preserved within the non-live group.
Click-to-centre scroll
Selecting a tile (whether by user click, keyboard activation, or host setting el.selectedSportId = X) triggers a smooth 300ms scroll that centres the selected tile in the visible swiper viewport. The scroll also runs once on first paint if the host pre-selects a sport before mount.
The animation clamps to the swiper edges, so a selection at the very start or end of the list never over-scrolls past the first/last tile. No-op if the selected sport isn't currently in the rendered list (e.g. during a liveStatus switch where the sport hasn't arrived yet) or if the swiper hasn't initialised.
Sport-name tooltip
Hovering a tile shows the sport's full name in a tooltip centred under it, 10px below the strip, whether or not the label beneath the icon is cut off. The label itself stays on one line and truncates with an ellipsis at the tile width. The tooltip takes the label's type (the labelFontSize, labelFontWeight and labelLineHeight appearance keys), 4px × 8px padding and an 8px radius, painted with --goma-backgroundSecondary and --goma-textPrimary. It hides when the pointer leaves the tile and when the strip slides or the page scrolls.
It renders in the browser's top layer (the Popover API), so no host container clips it, even one with overflow: hidden. In a browser without the Popover API there is no custom tooltip, and the label's native title shows the name instead. The tooltip is hidden from assistive technology; the tile's aria-label already carries the name.
Touch layout
A screen with no hover ((hover: none), so phones and most tablets) cannot open the tooltip, so the label itself shows the full name:
- The label wraps to at most two lines, breaking between words only, and is always two lines tall, so every tile has the same height and the row stays level. A name too long for two lines ends with an ellipsis on line 2.
- There is no native
title.
Only the label changes. The tile width, spaceBetween, showArrows, the side inset and the label type stay what the host set, so a client whose mobile design differs (a narrower tile, a smaller gap, no arrows) sets those in its own theme for (hover: none). Pick an itemWidth wide enough for the longest single word at the label size: a word wider than the tile cannot break, so it is cut at the edge with no ellipsis.
The switch follows the media query, not the viewport width, so a narrow desktop window keeps the one-line label and the tooltip.
Live-count badge
When a sport has numberOfLiveEvents > 0, a small pill renders on the top-right of the icon circle showing the count (capped at 99+), wrapped in a 2px ring in --goma-backgroundPrimary so it pops cleanly off the tile behind it. Toggle it with showLiveCount (default true). See Theming for the badge's dedicated tokens.
Events
| Status | Event | Detail | Emitted when |
|---|---|---|---|
| Canonical | ready | {} | Widget mounted, initial render done. |
| Canonical | goma:connection | { state, attempt?, nextRetryMs?, lastError?, attempts? } | Connection state transition ('rpc' / 'subscribe' modes only). |
| Canonical | goma:error | { message, code, component? } | Two paths. Connection layer: the WAMP adapter exhausts its 5 retry attempts (code: 'API_INIT_ERROR'), alongside goma:connection { state: 'failed' }. Widget root scope: an error raised in the widget's own outermost component (its setup, its render, its lifecycle hooks or its watchers), where code is the error's name, e.g. 'TypeError' ('RUNTIME_ERROR' when it has none). Errors raised inside the widget's child components do not reach this event: the in-shadow error boundary catches those and shows the retry banner instead, emitting only goma:retry. |
| Canonical | goma:retry | {} | The error banner's Retry button is pressed. |
| Canonical | goma:sport-select | { sportId, sportName, sportIconId, previousSportId } | User taps (or activates via Enter/Space) a sport tile. The widget also mutates state.selectedSportId so the ring follows immediately. Also emitted when the host sets the property (a menu link, a deep link, a refresh restore), so the whole follow* chain keeps up without extra wiring; never on mount, and never when re-assigned the value already held. See Driving a selection from the host. |
| Alias | goma:connected | {} | Dispatched once when the API connection is first established. Prefer goma:connection with state === 'connected'. |
Reconnection behavior
In 'rpc' and 'subscribe' modes, the widget connects to the WAMP socket on mount. Same exponential-backoff schedule as goma-events-horizontal (1s, 2s, 4s, 8s, 16s, 30s cap; 5 attempts before goma:connection { state: 'failed' }), and the same reconnect when the page returns from more than network.resumeReconnectAfterMs in the background: see Coming back from sleep. On reconnect:
'rpc'mode re-runs thegetXxxSports()call automatically.'subscribe'mode re-registers the topic; the initial dump from the new subscription rehydrates the list, and version-based deduplication in the WAMP client prevents double-processing.
Toggling dataSource or liveStatus at runtime cleanly unregisters the previous subscription / cancels the previous RPC before issuing the new one: no leaks.
Integrates with
Emits goma:sport-select ({ sportId, sportName, sportIconId, previousSportId }): the canonical upstream event for the sports-picker family. <goma-sports-navigation-dialog> emits the identical event as an alternate (modal) picker UI; the two are interchangeable upstream sources for everything below.
Four downstream widgets follow it, each gated by its own followSportPicker prop:
| Downstream widget | followSportPicker default | On select |
|---|---|---|
<goma-events-horizontal> | true | Rewrites segment 0 (sportId) of its feed topic. |
<goma-region-filter> | true | Sets its own sportId and re-fetches the region list. |
<goma-competitions-list> | true | Sets its own sportId and re-fetches the competition list. |
<goma-top-competitions-pills> | false | Sets its own sportId and re-fetches. |
<goma-favorites> | false | Narrows rendered favourites to the selected sport (also follows goma:filters-apply). |
This widget itself listens for nothing and reads/writes no shared-storage envelope: it's the root of the picker chain, not a follower. See Cross-widget wiring and Shared state for the full rewrite/re-fetch contracts and the dataSource: 'prop' pitfall (an unpopulated picker has nothing for a follower to react to).
Empty / loading / error states
- Skeleton: pulsing placeholder circles render from mount until the first
sportsassignment. Even an explicit empty array counts as "checked, nothing to show," not "not fed yet." Count is configurable viaskeletonCount(default8). Once data has arrived, the widget never falls back to skeleton: even if the host later clears the list. - Empty. Once data has been received and the list is empty, a centred localised "No sports available" message is shown (key:
sports_navigation_empty). - Error: render errors surface in a themed banner for 8 seconds with a Retry button (emits
goma:retry). Connection failures after the retry budget is exhausted emitgoma:errorto the host.
Theming
Override any CSS variable by passing a theme object. All tokens are namespaced with --goma-* and applied as inline styles on the host element. The prefix is added automatically: pass just the token name:
js
el.theme = {
backgroundPrimary: '#03061b',
backgroundCards: '#1f2147',
highlightPrimary: '#ff6600', // selected-tile ring colour
liveCountBackground: '#ef4444', // live-count pill
liveCountText: '#ffffff',
textPrimary: '#ffffff', // selected label colour
textSecondary: '#939dff', // unselected label colour
}Tokens used by this widget
| Token | Element |
|---|---|
--goma-backgroundPrimary | Container background and the live-count badge's ring offset |
--goma-backgroundCards | Unselected icon circle background |
--goma-highlightPrimary | Selected-tile ring and keyboard :focus-visible ring |
--goma-textPrimary | Selected label colour and sport-name tooltip text |
--goma-backgroundSecondary | Sport-name tooltip background |
--goma-allWhite | Nav arrow disc, painted at 60% opacity |
--goma-navArrowBackground | Optional nav arrow disc override. Unset (default), the disc is --goma-allWhite at 60%. For a palette where that does not pair with the caret, e.g. a light theme that wants a dark disc. |
--goma-navArrowIcon | Optional nav arrow caret override. Unset (default), the caret is --goma-highlightSecondaryContrast. |
--goma-highlightSecondaryContrast | Nav arrow caret |
--goma-textSecondary | Unselected label colour |
--goma-liveCountBackground | Live-count badge background |
--goma-liveCountText | Live-count badge text |
--goma-alertError | Render-error banner background |
--goma-highlightPrimaryContrast | Render-error banner Retry button text/background |
--goma-widgetInset | Horizontal gutter: see Widget gutter |
See the Theming guide for the complete token reference.
--goma-widgetInset sets the HORIZONTAL gutter between the strip's content and the edge of the element: 0 here. Widgets differ, so a page stacking several of them can set the token once to line them all up, or 0 to hand the gutter to the page. It does not touch vertical padding, and leaving it unset keeps the value above. See Widget gutter.
Appearance (styling object)
appearance carries structure only. Each key is a single value (identical in light and dark) that maps to one --goma-* CSS custom property on the host. Set it via el.appearance = {...} or the appearance attribute (a JSON string); unset keys keep the default, so an empty {} (the default) reproduces the current look. Colours stay in theme.
| Key | CSS variable | Default | Affects |
|---|---|---|---|
itemWidth | --goma-sportsNavItemWidth | 72px | Width of each sport tile (and its skeleton). The label truncates, or wraps in the touch layout, to this width. |
labelFontSize | --goma-sportsNavLabelFontSize | 0.75rem (12px) | Sport label font size. |
labelFontWeight | --goma-sportsNavLabelFontWeight | 500 | Sport label font weight. |
labelLineHeight | --goma-sportsNavLabelLineHeight | 1.25 | Sport label line height. |
The gap between tiles is not an appearance key: it is the spaceBetween prop (a number of pixels), because the swiper lays the tiles out with it.
js
el.spaceBetween = 24
el.appearance = { itemWidth: '45px', labelFontSize: '11px', labelFontWeight: '400', labelLineHeight: '15px' }i18n / message overrides
| Key | Default (en) | Used in |
|---|---|---|
sports_navigation_aria | Sports navigation | <swiper aria-label> |
sports_navigation_empty | No sports available | Empty state message |
retry | Retry | Error banner button |
previous | Previous | Left paging-arrow aria-label |
next | Next | Right paging-arrow aria-label |
Override with config.messages:
js
el.config = {
/* … */
messages: {
en: { sports_navigation_empty: 'Nothing live right now' },
fr: { sports_navigation_empty: 'Rien en direct' },
},
}Embedding examples
Vanilla HTML
html
<goma-sports-navigation-horizontal id="nav"></goma-sports-navigation-horizontal>
<script type="module">
import '@gomagaming/sports-navigation-horizontal'
const el = document.getElementById('nav')
el.dataSource = 'subscribe'
el.liveStatus = 'LIVE'
el.config = {
socketUrl: 'wss://example.com/v2',
socketRealm: 'www.example.com',
apiBaseUrl: 'https://api.example.com',
bettingApiBaseUrl: 'https://sports-api.example.com',
ucsOperatorId: '2838',
locale: 'en',
}
el.addEventListener('goma:sport-select', (e) => {
console.log('Selected:', e.detail.sportName, e.detail.sportId)
})
</script>React
jsx
import '@gomagaming/sports-navigation-horizontal'
import { useEffect, useRef } from 'react'
function SportsNav({ config }) {
const ref = useRef(null)
useEffect(() => {
const el = ref.current
if (!el) return
el.dataSource = 'subscribe'
el.liveStatus = 'LIVE'
el.config = config
const onSelect = (e) => console.log(e.detail.sportName, e.detail.sportId)
el.addEventListener('goma:sport-select', onSelect)
return () => el.removeEventListener('goma:sport-select', onSelect)
}, [config])
return <goma-sports-navigation-horizontal ref={ref} />
}Vue 3
vue
<script setup>
import '@gomagaming/sports-navigation-horizontal'
import { ref, onMounted } from 'vue'
const el = ref(null)
const props = defineProps(['config'])
onMounted(() => {
el.value.dataSource = 'subscribe'
el.value.liveStatus = 'LIVE'
el.value.config = props.config
})
function onSportSelect(e) {
console.log('Selected:', e.detail.sportId)
}
</script>
<template>
<goma-sports-navigation-horizontal
ref="el"
@goma:sport-select="onSportSelect"
/>
</template>Prop mode (no backend)
For storefronts, design previews, or hosts that already have sport data in scope. Note the explicit el.sports assignment below: without it, dataSource: 'prop' (the default) renders an empty strip with nothing to tap:
js
el.dataSource = 'prop'
el.sports = await fetch('/api/my-sports').then(r => r.json())
el.selectedSportId = '3'TypeScript
The shared widget type declarations (including GomaSport and the GomaSportsNavigationHorizontalElement class) are published by @gomagaming/core at src/types/index.d.ts:
ts
import type { GomaSport } from '@gomagaming/core'
const el = document.querySelector('goma-sports-navigation-horizontal')!
el.dataSource = 'subscribe' // 'prop' | 'rpc' | 'subscribe'
el.liveStatus = 'LIVE' // 'LIVE' | 'NOT_LIVE' | 'BOTH'
el.theme = { highlightPrimary: '#dc2626' }
el.sports = [] satisfies GomaSport[]Accessibility
| Element | Role | Attributes |
|---|---|---|
| Swiper container | tablist | aria-label="Sports navigation" (i18n key sports_navigation_aria) |
| Sport tile | tab | tabindex="0", aria-selected, aria-label="{name}" |
| Live-count badge | - | aria-label="N live event(s)" |
| Error banner | - | role="alert" for screen-reader announcement |
Keyboard: Tab to focus tiles, Enter / Space to select. A tile that takes focus outside the visible part of the strip slides to the centre, so the focused tile is always on screen; the paging arrows stay at the strip edges. :focus-visible ring uses the --goma-highlightPrimary token.
Integration Caveats
SSR/webview: importing this package outside a browser (e.g. server-side rendering) previously threw immediately; fixed with a guard on the shared element factory.
Misc hygiene: the render-error banner's
--goma-alertError/--goma-highlightPrimaryContrasttokens are now listed in the Theming table above; a stray comment referencing the deprecateddemo/app insrc/playground/mock.jswas reworded.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.6
- On a screen with no hover (phones, most tablets), the sport label now wraps to two lines between words (always two lines tall, an ellipsis only past line 2) instead of one truncated line, and drops the native title, so the full name shows where there is no hover tooltip. Tile width,
spaceBetween,showArrows, the inset and the label type stay what the host set. Screens that can hover are unchanged.
0.37.4
- Keyboard focus in horizontal rails now stays on screen.
<goma-sports-navigation-horizontal>slides a tile reached with Tab to the centre of the strip, and the event rows in<goma-events-horizontal>,<goma-favorites>and the<goma-competitions-list>detail view slide a card or odds button reached with Tab into view. The event rows also no longer scroll natively under the carousel, which moved their edge fades into the row and left the carousel out of step with what was shown. - Hovering a sport tile now shows the sport's full name in a tooltip under the strip, whether or not the label is cut off. The label keeps truncating with an ellipsis at the tile width. The tooltip uses
--goma-backgroundSecondaryand--goma-textPrimaryand the label's type, renders in the browser's top layer so no host container clips it, and falls back to the label's nativetitlewhere the Popover API is missing. <goma-sports-navigation-horizontal>: the paging arrows now stay at the edges of the strip. Before, the strip could also scroll natively (for example with the keyboard arrow keys after clicking a sport), which moved the next arrow into the middle of the tiles and pushed the previous arrow out of view.
0.37.1
- A sports picker set to
liveStatus: 'NOT_LIVE'(sports navigation horizontal and dialog, filters settings) no longer lists sports that only have live events. The full sports list the widget loads on connect could land after the widget's own pre-live list and mark every sport as pre-live; it now carries no such mark, so the pre-live list stays as the source sent it, inrpcandsubscribemodes. Pickers set to'LIVE'or'BOTH'are unchanged, and sports outside the pre-live list still get their names and icons.
0.37.0
<goma-hero-card>gains anappearanceobject (structure only):borderWidth,borderRadius,minHeight,paddingTop,paddingX,competitionFontSize,competitionFontWeight,marketPillPaddingY,marketPillPaddingX,marketPillLineHeightandoddsGap.<goma-sports-navigation-horizontal>gains one too:itemWidth,labelFontSize,labelFontWeightandlabelLineHeight. Unset, every key keeps today's value, so the default look of both widgets is unchanged.@gomagaming/coreis bumped for the new utilities in its compiled sheet.Casino games and time filter: the error bar's dismiss
×is now a button named "Close", so keyboard and screen-reader users can reach it, and its "ERROR:" prefix comes from the newerror_prefixcopy key and follows the widget's locale. In casino games, the carousel arrows' names and the dots' names ("Go to page 1") also come from copy keys (previous,next, and the newgo_to_page).clearError()now removes an error bar thatforceError()showed, and forcing the same message again after a dismiss shows the bar again.Casino games: a heading of spaces only now shows the default title, like a blank one.
custom-elements.jsonnow declares thegoma:seo-dataandgoma:retryevents the widget already emitted, and types a tapped game'sidandnameas possiblynull. The theme token list now names the flame's own token (iconPrimary) and the error bar's, and the two optional arrow tokens (navArrowBackground,navArrowIcon) now list a default, also in multi-banners and sports navigation (horizontal).Core: the Portuguese "Next" label read "Prximo"; it now reads "Próximo".
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 carousel navigation arrow accepts two optional theme tokens:
--goma-navArrowBackground(the disc) and--goma-navArrowIcon(the caret). Unset, the arrow looks exactly as before (anallWhitedisc at 60% with ahighlightSecondaryContrastcaret) so only a palette where those two do not pair (for example a light theme that wants a dark disc) needs to set them.
0.35.0
Every picker now announces a host-driven selection, not just a tap
Setting a picker's selection from the page (
el.selectedSportId = '8'for a menu entry, a deep link, state restored after a refresh) used to change the highlighted item silently, because the event was emitted from inside the click handler. Anything wired up with afollow*prop kept its previous filter, so the highlighted pill and the data underneath it disagreed. The only workaround was to forge the widget's own event onwindow, which breaks as soon as a*Selectorprop is set.All seven pickers now emit from a watcher on the selection itself, so a tap, an internal reset and a host write are indistinguishable:
<goma-region-filter>,<goma-top-competitions>,<goma-top-competitions-pills>,<goma-competitions-list>,<goma-competition-filter>,<goma-sports-navigation-horizontal>and<goma-sports-navigation-dialog>.@gomagaming/coregains the shareduseSelectionEmitcomposable they all use, so a picker added later inherits the behaviour rather than reimplementing it.Three rules hold across all of them. Nothing is emitted on mount, so a value seeded from markup (
<goma-region-filter selected-region-id="19">) opens on that scope without firing the chain during boot. Re-assigning the value already held emits nothing. And ids are the contract while names are best-effort: a selection applied before the widget's list has loaded still emits immediately with the right id andnulldescriptors, so deep links never have to wait for data.Two deliberate exceptions:
<goma-competitions-list>stays silent when the selection is CLEARED, since closing its drill-in detail has its own event and an empty selection would scope a following feed to nothing; and<goma-sports-navigation-dialog>inshowApplyButtonmode treats the selection as pending, emitting when Apply is pressed.Two behaviour changes come with the single emit point. Re-tapping the item already active no longer emits (it is not a change): in the dialog that still closes it, it just announces nothing. And
<goma-top-competitions>'sisFeaturedis now derived fromhighlightedImagesand the active variant rather than passed in from the tap, so a host write reports it as truthfully as a click.<goma-top-competitions-pills>also gainsresetCompetitionOnSportChange(defaulttrue), matchingresetRegionOnSportChangeon<goma-region-filter>: it clears the selection on asportIdchange, and with it on no write order lets one navigation action set a sport and a competition together.A sport with no bundled icon now falls back to a real glyph instead of the browser's broken-image icon.
SbIconresolves a bundled SVG by name and otherwise falls through to an<img>. For a sport id the feed ships before its artwork exists (e-Volleyball, in the sports carousel and in the "Sports available" list) that<img>404s, and the old error handler only faded it toopacity: 0.2, which leaves the broken-image glyph on screen at 20%. A failed<img>is now dropped from the render instead.What stands in is the sport's family glyph: every e-sports category falls back to the e-sports icon, which is a real icon rather than "no icon". The family is read from the sport list the widget was already fed (the parent whose
childrenIdsnames the category) so no id list has to be kept in step with the provider, and a flat feed that names no parent simply falls back to the neutralnoiconglyph as before. The newsportIconNameshelper in@gomagaming/coreis what the sport tiles, the picker's trigger pill and the settings modal's sport row all resolve through.A host serving its own sport artwork keeps priority, and keeps it whether or not it configured a base URL:
${iconBaseUrl}/icons/sports/dark_mode/{iconId}.svgwhen one is set, and the origin-relative/icons/sports/dark_mode/{iconId}.svgwhen it is not.iconBaseUrlhas no value that means "my own root" (''and'/'both read as unset), so a host serving icons from its root keeps working with no configuration at all. A stand-in is considered only once that request has failed.Every widget that draws a sport glyph through
SbIconinherits the change, which is why they are all listed here: the two sports pickers, the settings modal, both top-competitions widgets, the sport banners, the game-details header, the suggested-bet legs, the My Bets cards, the two betslips that embed them, the highlight card and the Pros' Choice card.The family is looked up across the whole sport list the feed sent rather than the slice on screen. In
rpc/subscribemodeuseSportsSourcemirrors the betting store filtered to the current context (LIVE vs popular), and a parent category carries no events of its own, so it is routinely missing from that slice: every e-sports child would have fallen back to the neutral glyph. The unfiltered list is mirrored alongside it asstate.sportsCatalog, andsportIconLookupList(new in@gomagaming/core) is what the tiles, the trigger pill and the settings row read.mapSportalso refresheschildrenIdson an existing entry: the list was written only when the entry was first created, so a family relation that arrived on a later dump, or a child added to one. Never reached the widget.
0.34.0
Stop a region list fetched for the wrong sport from overwriting the right one, and parse numeric strings assigned to numeric widget properties instead of silently replacing them with the widget's default
A
<goma-region-filter>configured for any sport other than Football issued two location requests instead of one: its own, correctly scoped to the configured sport, and a second one (scoped to Football) that nothing had asked for. Both wrote the same place, so the pills the widget rendered were whichever response happened to come back last. The same race reached<goma-competitions-list>, which groups its competitions by the same region data.The unrequested request came from the per-widget boot routine, which fetched locations with no sport at all and so fell back to sport
1. Locations are scoped to a sport on the server, and boot has no sport to scope them by, so it no longer fetches them: the only readers of that data are the widgets above, and each of them already fetches it for the sport it is showing.That removes the collision at its source. As a second line of defence, a locations payload now records which sport it was fetched for, and a widget ignores one belonging to a sport it is not showing. This matters whenever two scoped fetches can overlap (switching sport while a request is still in flight, for instance) and it is deliberately narrow: a payload with no recorded sport is still rendered, so the guard can only ever suppress data it can positively identify as belonging to somewhere else.
Separately, every numeric property on every widget (
sportId,skeletonCount,pageSize,spaceBetween,autoplayIntervaland the rest) used to accept a JavaScript number and nothing else. A numeric string was not parsed; it was discarded and quietly replaced by the property's default value.el.sportId = '8'therefore left the widget on sport1and fetched the wrong data, with nothing logged to say so. Three ordinary integration paths hand over a string: a router parameter, a value read out of a CMS or JSON configuration, and a framework re-render that stringifies its props. Numeric strings are now parsed. Values that are not a finite number and not a numeric string (null,undefined,'', booleans, objects,NaN,Infinity) still fall back to the default, as before. HTML attributes (sport-id="8") and theconfigobject were never affected by this and are unchanged.
0.32.0
Widgets no longer dispatch a bare
errorevent alongsidegoma:error.Every factory-built widget's app-level Vue error handler dispatched two events for the same failure:
goma:error, and an un-namespacederror. The bare one is gone;goma:erroris unchanged and still carries the same{ message, code, component? }detail.Like every GomaPulse event it was
composedand bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had onwindow.addEventListener('error', …): as aCustomEventwithmessage,errorandfilenameallundefined, since those areErrorEventfields aCustomEventdoes not have. A host with error reporting installed received a malformed error record it could neither read nor attribute.That had been written off as harmless on the grounds that the handler behind it could never fire. It can. Vue begins its
errorCapturedwalk at the throwing component's parent, so theonErrorCapturedboundary each widget installs in its own root component cannot see that component's own errors, and being the root, there is nothing above it to catch them either. Anything raised in a widget's root scope (its setup, its render, its lifecycle hooks, its watchers) skips the boundary and reaches the app-level handler every time. Only errors from child components are caught by the boundary, which is what it is for: those show the in-shadow banner and emitgoma:retry.Hosts listening for
goma:errorneed no change. A host listening for bareerroron a widget element should move togoma:error; it was documented as unreachable, so in practice it delivered nothing until now.
0.31.0
Added
--goma-widgetInset, one CSS custom property that settles how far a widget's content sits from the edge of its element.Widgets had each grown their own horizontal padding (8px around the multi-banners and casino-games rails, none on the sports-navigation strip or the event feeds) so stacking them in a column started content at different offsets, and the only way to line them up was to cancel the difference with negative margins on the host side.
Set
--goma-widgetInsetonce and every widget that has a gutter adopts it; set it to0and they all run flush to their element, handing the gutter back to the page. Widgets that were already flush are unaffected: the token does not add padding where there was none.It governs horizontal padding only. Vertical padding is the space between sections, which a host already controls with margins, so folding it into the same token would mean setting the gutter to
8pxsilently added8pxof top padding to rails that had none.Panel widgets are deliberately not governed by it:
<goma-competitions-list>and<goma-top-competitions>(list variant) keep a fixed 16px interior, because that padding is the inside of a rounded box the widget paints rather than a page gutter.Nothing changes for existing embeds. There is no global default: each widget falls back to exactly what it renders today, so the token only takes effect once a host sets it.
0.29.0
Integration caveats: fixes from a cross-widget review of every shipped widget from an integrator's point of view.
Cross-cutting fixes:
@gomagaming/core'screateWidgetElement()factory, andlive-chat's andmessenger's own element classes, now guard against importing a widget package in a non-browser context (SSR): theHTMLElementsubclass was previously defined at module top level in every widget package, so importing any of them under server-side rendering threw aReferenceErrorimmediately.
Per-package fixes:
favorites: addedsrc/themeTokens.jsand a:hoststylesheet (previously the package had neither, so it silently rendered core's base theme instead of its documented dark-navy/cyan/amber theme).suggested-bet. Fixed a leaked WAMP subscription: switching a mounted widget'sdataSourceaway fromrpc/subscribenever released the open topic.filters-settings: fixed a dead:host{display:inline-block}CSS rule (the shared element factory's inlinedisplay:blockalways won), so the widget always rendered full-width instead of the intended compact inline trigger.game-details: added amin-heightfallback for the inert/no-eventIdstate in an unsized container.betslip-sidebar,my-bets,competition-filter,suggested-bet,events-horizontal,top-competitions,top-competitions-pills,competitions-list,multi-banners,casino-games,sports-navigation-horizontal. CorrectedthemeTokens.jsregistries that had drifted from actual token usage, added missinggoma:retryEvents-table rows, and other small doc/registry corrections.region-filter: added a missingtypeof window !== 'undefined'guard on a window listener.live-chat: correctedcustom-elements.json'smembersarray (missingtheme/configaccessors, and an inaccurate description).sports-domain: corrected a stale code comment that seeded a documentation error in two Missions widgets.
Three fixes (
favorites,game-details,filters-settings) change real rendering and are flagged indocs/guide/integration-caveats.mdas needing human visual verification before release.Visual corrections across the game card, the casino rail and every carousel arrow.
- Game cards. Team names on the large card variant were clipped: an 18px font was set on a 16px line box, which sheared the ascenders and the descenders of names like
Puerto Rico (W). The line box now fits the font's ink, and the home row's bottom margin is pulled in so the painted distance between the two team names is unchanged. Affects every widget rendering the shared game card, plus<goma-highlight-card>'s team rows. <goma-casino-games>: the'simplified'tile now matches its design source: a 23:30 portrait tile, and the name/provider block and play glyph laid out as one centred stack so the gap between them no longer changes with the number of lines the game name wraps to.- Navigation arrows:
<goma-casino-games>,<goma-multi-banners>and<goma-sports-navigation-horizontal>now draw one identical arrow: a 24px disc of--goma-allWhiteat 60% carrying a full-strength--goma-highlightSecondaryContrastcaret on a 1px stroke. Previously the three diverged in disc colour, glyph size (14–18px) and stroke weight. Hosts that restyled these arrows through--goma-textPrimaryor--goma-backgroundBordershould move to the two tokens above; both are declared in each widget's theme-token registry and documented in its theming table. <goma-top-competitions>: the list header's type now matches the section headers around it instead of running a size larger at a lighter weight.
- Game cards. Team names on the large card variant were clipped: an 18px font was set on a 16px line box, which sheared the ascenders and the descenders of names like
0.28.0
Quieten the paging arrows on both scrolling strips. The solid disc with a drop shadow read as a primary control competing with the content beneath it; the arrow is chrome, so it is now a translucent scrim (a
color-mixwash with a blurred backdrop and no shadow) and the caret stays the same token so it is legible against it.Both widgets derive scrim and caret from
--goma-textPrimary: one token for both halves, so a host theming the widget cannot pull them apart. The<goma-sports-navigation-horizontal>arrow previously mixed a background token (--goma-buttonBackgroundTertiary) for the scrim with a text token for the caret; that token is no longer read by the widget.Both widgets also now carry a plain
rgba()declaration ahead of thecolor-mix()one, so engines withoutcolor-mix()still paint a scrim instead of a fully transparent disc.<goma-sports-navigation-horizontal>'s arrow moved off an inline style object intosrc/styles/nav.css(fed to the shadow root viaextraStyles) to make that two-declaration fallback expressible at all.
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.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
goma-sports-navigation-horizontalandgoma-sports-navigation-dialoggain ashowLiveCountprop /show-live-countattribute (defaulttrue). Set it tofalseto hide the live-event count badge on each sport tile.
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.
0.4.0
Phase 2 retrofit for the betslip widgets:
@gomagaming/betslip-floatingand@gomagaming/betslip-sidebarnow ship the same./playgroundsub-export contract as the other widgets. Each has asrc/playground.manifest.jsvalidated byPlaygroundManifestSchemain@gomagaming/core, adist/playground.jsemitted alongside the main bundle (via theextraEntriesoption in@gomagaming/vite-preset), a./playgroundentry inpackage.jsonexports, and a per-package Vitest test asserting schema conformance. Unlike events-horizontal / sports-navigation-*, the betslip widgets ship nomock()fixture: they read selections from cross-widget shared storage (goma:betslip:v3) populated by events-horizontal, so the playground demos them by enabling events-horizontal alongside and tapping outcomes.The betslip widgets also join the fixed lockstep group in
.changeset/config.json, so all 7 publishable packages now version together. This fixes the version-skew introduced when betslip widgets were added at 0.2.0 in the a separate change PR without lockstep membership.The playground SPA gains all five widgets in its registry, store, Stage element map, and snippet PKG_NAMES; vite.config.ts'
optimizeDeps.includeis extended to pre-bundle all five main entries plus their./playgroundsub-exports.
0.3.0
Adds the playground manifest contract: every widget package now exports a
./playgroundsub-export carrying a declarative manifest (validated byPlaygroundManifestSchemain @gomagaming/core) plus an optional lazy-loaded mock fixture. Powers the new client-facing playground at /playground/ on the docs site (replaces the workspace-internal demo/ snapshot).Schema lives in @gomagaming/core; consumers can
import { validateManifest } from '@gomagaming/core'to assert manifest conformance. zod is now a runtime dep of @gomagaming/core (tree-shaken away for widget bundles that don't reference the schema).
0.2.0
Dedup architecture: externalize peers + workspace deps in widget builds, narrow
coreandsports-domainexportsto drop the./src/*escape hatch, and promote widget-consumed targets into main entries. Multi-widget consumers now resolve a single copy ofvue,pinia,vue-router,vue-i18n,@vueuse/core,autobahn-browser, swiper,@gomagaming/core, and@gomagaming/sports-domain: measured ~17% reduction in two-widget consumer bundle gzip total. Per-widget published artifacts shrink dramatically (e.g.events-horizontal1.7 MB → 6.75 KB gzip, 99.6% smaller). Sport-icon dedup remains per-widget and is tracked as a follow-up.Breaking: removes
./src/*from@gomagaming/coreand@gomagaming/sports-domainpackage.jsonexports. Any consumer that imported via@gomagaming/<pkg>/src/...paths must switch to the main-entry import (import { X } from '@gomagaming/<pkg>').sports-domain's main entry now exportsLiveRegularOdds,PreLiveRegularOdds,OutrightRegularOdds,MarketRegularOdds,SkeletonCard,useTopicSubscription,useSportsSource,sortOutcomes, andnormaliseTopicPrefix.@gomagaming/sports-navigation-dialogships for the first time at0.2.0(no prior 0.1.x publish).
0.1.1
Fix bundler dedup: each widget's Vite library build now externalizes its peerDependencies and workspace
@gomagaming/*dependencies, so the consumer's bundler resolves Vue, Pinia, autobahn, sport icons, and the shared@gomagaming/core+@gomagaming/sports-domainpackages exactly once across all installed widgets.Affected: per-widget published dist sizes drop dramatically (events-horizontal shrinks from ~6 MB to ~24 kB). Consumers installing both widgets no longer ship duplicate copies of Vue/Pinia/etc.
0.1.0
- Promote 0.1.0-rc.0 to 0.1.0 GA.
0.1.0-rc.0
Initial release.
Per-widget package layout extracted from the gomawt monorepo. See
docs/superpowers/specs/2026-04-28-monorepo-widgets-design.mdand the follow-on Plan 1 / Plan 2 / Plan 3 specs and plans for the architectural and release-engineering rationale.Public surface in 0.1.0:
<goma-events-horizontal>Web Component<goma-sports-navigation-horizontal>Web Component- Shared framework spine in
@gomagaming/core - Shared sports primitives in
@gomagaming/sports-domain
All notable changes to this package will be documented in this file. Auto-generated by Changesets; do not edit by hand.