Skip to content

<goma-sports-navigation-dialog> ​

Self-contained sport-picker dialog: a trigger pill that, on tap, opens a modal grid of sport tiles with an optional search input. Sister widget to goma-sports-navigation-horizontal: same data sources (prop / rpc / subscribe), same selection state, same goma:sport-select event, just rendered as a popover dialog instead of a horizontal swiper. Designed primarily for mobile layouts where the horizontal strip wouldn't fit, but the contract is identical so the two widgets are interchangeable wherever a single-selection sports picker is needed.

Live demo ​

Open in the playground →: toggle Sports navigation (dialog) in the widget rail, then tap the trigger pill on the stage to open the modal. The playground's mock source (a fourth option layered on top of the widget's own prop / rpc / subscribe for demo purposes) drifts live-count badges on a 4-second cadence so you can verify the dialog stays responsive while open.

Element tag ​

html
<goma-sports-navigation-dialog></goma-sports-navigation-dialog>

Installation ​

@gomagaming/sports-navigation-dialog is a scoped private package. See Installation for the one-time .npmrc setup required to consume it.

bash
npm install @gomagaming/sports-navigation-dialog

You also need the shared peer dependencies installed at your app level:

bash
npm install vue pinia vue-router vue-i18n @vueuse/core autobahn-browser

vue, pinia, vue-router, vue-i18n, @vueuse/core, and autobahn-browser are declared as peerDependencies so a consumer with multiple @gomagaming/* widgets gets exactly one copy of each.

Configuration ​

js
const el = document.getElementById('snd')
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',
}

The shape is identical to the horizontal sibling: see its config table. All keys are optional in prop mode (no socket is opened).

Data sources ​

The dataSource property selects where the sport list comes from: defaults to 'prop', and the default sports list is [], so an unconfigured widget renders an empty grid until the host feeds data (via el.sports, or by switching to 'rpc' / 'subscribe'). Every example on this page sets sports explicitly or switches to 'subscribe' with a real socket config for this reason. The mechanism is shared with the horizontal widget via the same useSportsSource composable.

ModeBehaviour
'prop' (default)Host feeds el.sports = […]. No socket.
'rpc'One-shot $wcall on connect populates the list once.
'subscribe'Live WAMP subscription streams updates as games go live.

See Sports Navigation Horizontal § Data sources for the full breakdown of liveStatus mappings, RPC method names, and subscription topic URLs.

Props / attributes ​

Inherited from the shared sports-picker contract (identical to the horizontal sibling):

PropertyAttributeDefaultDescription
dataSourcedata-source'prop''prop' / 'rpc' / 'subscribe'.
liveStatuslive-status'LIVE''LIVE' / 'NOT_LIVE' / 'BOTH'.
sportssports[]Array of GomaSport.
selectedSportIdselected-sport-idnullCurrently selected sport id. Two-way.
showLabelsshow-labelstrueRender the sport name under each tile icon.
showLiveCountshow-live-counttrueRender the live-event count badge on each sport tile. Set false to hide it.
skeletonCountskeleton-count12Placeholder tile count until the first non-empty sports assignment.

Dialog-specific knobs:

PropertyAttributeDefaultDescription
openopenfalseProgrammatic open/close. Two-way: tapping the trigger flips it to true; selecting a sport flips it back to false.
showSearchshow-searchtrueRender the search input inside the modal. Set false to hide.
showTriggershow-triggertrueRender the built-in trigger pill. Set false when the dialog is opened externally (host control or the goma:sports-dialog-open event).
showBackButtonshow-back-buttonfalseShow a Back button (header left) that emits goma:dialog-back and closes: for returning to a previous view.
showApplyButtonshow-apply-buttonfalseShow an Apply footer. When on, tapping a tile only highlights it (no auto-close); Apply commits the highlighted sport (goma:sport-select) and closes.
gridColumnsgrid-columns0Force a fixed column count; 0 = auto (currently 4).
mobileBreakpointmobile-breakpoint'sm'Viewport width below which the dialog renders as a bottom-sheet (full-width, anchored to viewport bottom, slide-up animation) instead of a centred modal. Accepts a Tailwind token ('sm' 640 / 'md' 768 / 'lg' 1024 / 'xl' 1280 / '2xl' 1536) or a raw pixel value (number 480 or string '480px').

spaceBetween from the horizontal sibling is not applicable here: the grid uses CSS gap from the panel layout.

Bottom-sheet vs centred modal ​

Below mobileBreakpoint the panel renders as a bottom-sheet: full-width, anchored to the viewport bottom, rounded only at the top, with a small drag-handle pill above the header (purely visual, the sheet isn't draggable yet) and a slide-up entry/exit. At or above the breakpoint, the panel is a classic centred modal with rounded corners on all sides and a scale + fade entry/exit. The transition is live: resizing the viewport across the breakpoint while the dialog is open swaps the layout without re-mounting.

GomaSport shape ​

Identical to the horizontal sibling: see GomaSport.

Behavior ​

The widget renders only the trigger pill in its closed state: its host element shrinks to that footprint. Tapping the pill opens the modal.

Open / close ​

  • Tapping the trigger pill sets el.open = true and opens the modal.
  • Selecting a sport tile dispatches goma:sport-select, mutates state.selectedSportId, and auto-closes the dialog (unless showApplyButton is on, see below).
  • Tapping the Cancel button (top-right), tapping the backdrop, or pressing Escape closes the dialog.
  • Hosts can drive open/close programmatically: el.open = true / el.open = false. Each transition emits goma:dialog-open / goma:dialog-close.

Sort order ​

Live-first partition: same as the horizontal widget. Sports with numberOfLiveEvents > 0 float to the top, sorted by live count desc; the rest preserve original order.

When showSearch is true (default), the modal renders an input with a magnifying-glass icon and a localised placeholder (sports_navigation_dialog_search_placeholder). The query filters the sport list by case-insensitive substring against sport.name. An empty match shows a localised "no results" message (sports_navigation_dialog_no_results). The search query resets every time the dialog reopens.

Trigger pill customisation ​

The trigger pill is rendered inside the widget's Shadow DOM, so it is not stylable from the outer document. All customisation flows through the public surface: CSS variables for visuals, message overrides for text, and the selectedSportId property for the icon and announced selection.

Visual ​

The pill displays the selected sport's icon and name on an accent-coloured background; it is rounded on the right only. Before a sport is selected it shows a placeholder label instead of a name, and long sport names truncate with an ellipsis to keep the pill compact. Driven by --goma-highlightPrimary (pill background + focus ring) and --goma-highlightPrimaryContrast (icon glyph + name text): see Theming below.

Text / accessibility ​

The pill's aria-label is built from sports_navigation_dialog_title plus the currently-selected sport name. Override the base label per locale via config.messages:

js
el.config = {
  /* … */
  messages: {
    en: { sports_navigation_dialog_title: 'Pick a sport' },
    fr: { sports_navigation_dialog_title: 'Choisir un sport' },
  },
}
// → aria-label="Pick a sport: Football" once a sport is selected.

The pill's icon and name come from the currently selected sport: feed sports and selectedSportId to control which sport appears. The icon resolves from the selected sport's iconId. Until a sport is selected (e.g. while the sport list is still loading in rpc/subscribe mode) the pill shows the noicon glyph with a placeholder label (sports_navigation_dialog_placeholder, default Sport) so it is never empty. A sport whose iconId has no artwork in the bundled set falls back to its family's glyph. An e-sports category draws the e-sports icon. On the pill and on the tiles alike, with the neutral noicon glyph as the last resort when the list names no family. Neither ever 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 asked for first either way: ${iconBaseUrl}/icons/… when a base URL is set, the origin-relative /icons/… when it is not, and a fallback appears only once that request has failed.

Hide the search bar with a single switch ​

js
el.showSearch = false                   // hides the search input on every open
el.setAttribute('show-search', 'false') // attribute equivalent

The placeholder defaults to Search sports... (and the localised equivalents in fr / pt). Override it per locale via config.messages: the same string also feeds the input's sr-only <label>, so both move together:

js
el.config = {
  /* … */
  messages: {
    en: { sports_navigation_dialog_search_placeholder: 'Find a sport…' },
    fr: { sports_navigation_dialog_search_placeholder: 'Trouver un sport…' },
    pt: { sports_navigation_dialog_search_placeholder: 'Encontrar um desporto…' },
  },
}

Events ​

StatusEventDetailEmitted when
Canonicalready{}Widget mounted, initial render done.
Canonicalgoma:sport-select{ sportId, sportName, sportIconId, previousSportId }User tapped (or activated via Enter/Space) a sport tile, or the host set selectedSportId: never on mount, and never when re-assigned the value already held (re-tapping the active tile still closes the dialog, it just announces nothing). In showApplyButton mode the selection is PENDING: tapping only highlights, and the event is emitted when Apply is pressed, reporting the sport the dialog opened on as previousSportId. See Driving a selection from the host.
Canonicalgoma:dialog-open{}state.open transitioned from false → true.
Canonicalgoma:dialog-close{}state.open transitioned from true → false.
Canonicalgoma:dialog-back{}The Back button (showBackButton) was pressed; the dialog closes.
Canonicalgoma:retry{}The error banner's Retry button is pressed.
Canonicalgoma: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.

goma:sport-select is bubbles + composed, so it crosses the shadow-DOM boundary and is observable from a window.addEventListener('goma:sport-select', …) listener: the dialog and the horizontal picker are drop-in interchangeable for any consumer wiring that expected the horizontal picker (e.g. <goma-events-horizontal>'s followSportPicker rewiring keeps working).

Opening via an event. Any control on the page can open the dialog by dispatching a composed goma:sports-dialog-open event: window.dispatchEvent(new CustomEvent('goma:sports-dialog-open', { bubbles: true, composed: true })). Paired with showTrigger: false, this lets another widget (e.g. <goma-filters-settings>'s "Filters applied to:" card) act as the trigger.

Integrates with ​

Emits goma:sport-select (the same payload shape as <goma-sports-navigation-horizontal>) so any downstream widget with a followSportPicker prop (e.g. <goma-events-horizontal>, <goma-region-filter>, <goma-competitions-list>) re-scopes when either sports picker is on the page; the two are interchangeable from a listener's point of view.

Listens for a goma:sports-dialog-open command on window: a composed event no shipped widget currently dispatches on its own. It's the documented open hook for a host-driven trigger (e.g. <goma-filters-settings>'s "Filters applied to:" row) that wants to reuse this widget's picker UI with showTrigger: false instead of building its own.

This widget writes no shared-storage envelope (goma:betslip:v4, goma:favorites:v1, etc.): selection state lives only in state.selectedSportId and the goma:sport-select event. See Cross-widget wiring and Shared state for the full contracts.

Empty / loading / error states ​

StateTriggerRender
SkeletonFirst open before the host has fed sportsPulsing placeholder circles (count = skeletonCount). Gated once hasReceivedData flips true it never reverts, even if sports is later reassigned to [].
EmptyHost fed sports = [] after at least one assignmentLocalised sports_navigation_empty message.
No resultsSearch query has no matchLocalised sports_navigation_dialog_no_results message.
Render errorAn error is thrown while rendering (or state._forceError is set)An inline error banner with a Retry button (retry) that clears the error and emits goma:retry; auto-clears after 8 seconds if not dismissed.

Theming ​

Override CSS variables via el.theme = { … }: the prefix --goma- is added automatically.

js
el.theme = {
  // The dialog is a popover, so its host element should usually be transparent.
  // Override backgroundPrimary if you want a backdrop band behind the trigger pill.
  backgroundPrimary: 'transparent',
  // Modal panel background: decoupled from backgroundPrimary so consumers can
  // paint a transparent host while keeping the modal opaque.
  dialogBackground: '#03061b',
  // Selected-tile ring + pill/Apply-button accent
  highlightPrimary: '#ff6600',
  // Live-count badge
  liveCountBackground: '#ed4f63',
  liveCountText: '#ffffff',
  textPrimary: '#ffffff',
  textSecondary: '#939dff',
}

Tokens used by this widget ​

TokenElement
--goma-backgroundPrimaryHost element background (consumers usually set this to transparent for popover usage).
--goma-dialogBackgroundModal panel background. Specific to this widget: defaults to #03061b in main.css.
--goma-backgroundCardsUnselected sport-tile circle background; search-input background; skeleton placeholders.
--goma-highlightPrimarySelected-tile ring; trigger-pill and Apply-button background; Back/Cancel button text.
--goma-highlightPrimaryContrastTrigger-pill icon + name text, Apply-button text, and the error-banner Retry-button text.
--goma-textPrimaryTitle, selected label, body text.
--goma-textSecondaryUnselected labels; placeholder text.
--goma-separatorLinePanel border, search-input border.
--goma-liveCountBackgroundLive-count badge background.
--goma-liveCountTextLive-count badge text.
--goma-alertErrorRender-error banner background.
--goma-iconPrimaryDeclared on this widget's token surface, but not currently wired to anything: SportItem.vue renders its <sb-icon> with no color class or style applied, so the token has no path to the SVG. (The mechanism itself works elsewhere: the bundled sport SVGs use fill="currentColor", and e.g. <goma-suggested-bet> recolors an <sb-icon> via a text-iconPrimary wrapper class, this widget's own markup just never opts in.) Setting this token has no observable effect today.

See the Theming guide for the complete token reference.

i18n / message overrides ​

KeyDefault (en)Used in
sports_navigation_dialog_titleSports availableModal header title; base of the trigger pill's aria-label
sports_navigation_dialog_placeholderSportTrigger-pill label shown when no sport is selected yet
sports_navigation_dialog_search_placeholderSearch sports...Search input placeholder + associated <label>
sports_navigation_dialog_no_resultsNo sports match your searchEmpty-search-result message
sports_navigation_ariaSports navigationGrid aria-label (shared with horizontal)
sports_navigation_emptyNo sports availableEmpty-list message (shared with horizontal)
backBackHeader Back button (showBackButton)
cancelCancelHeader dismiss button (top-right)
applyApplyApply-footer button (showApplyButton)
retryRetryError-banner button

Override with config.messages:

js
el.config = {
  /* … */
  messages: {
    en: { sports_navigation_dialog_title: 'Pick a sport' },
    fr: { sports_navigation_dialog_title: 'Choisir un sport' },
  },
}

Embedding examples ​

Vanilla HTML ​

html
<goma-sports-navigation-dialog id="snd"></goma-sports-navigation-dialog>
<script type="module">
  import '@gomagaming/sports-navigation-dialog'

  const el = document.getElementById('snd')
  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-dialog'
import { useEffect, useRef } from 'react'

function SportsPicker({ config }) {
  const ref = useRef(null)
  useEffect(() => {
    if (!ref.current) return
    ref.current.dataSource = 'subscribe'
    ref.current.liveStatus = 'LIVE'
    ref.current.config = config
  }, [config])
  return (
    <goma-sports-navigation-dialog
      ref={ref}
      onGoma:sport-select={(e) => console.log(e.detail)}
    />
  )
}

Vue 3 ​

vue
<script setup>
import '@gomagaming/sports-navigation-dialog'
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
})
</script>

<template>
  <goma-sports-navigation-dialog
    ref="el"
    @goma:sport-select="(e) => console.log(e.detail)"
  />
</template>

Prop mode (no backend) ​

js
el.dataSource = 'prop'
el.sports = await fetch('/api/my-sports').then(r => r.json())
el.selectedSportId = '3'

Accessibility ​

ElementRoleAttributes
Trigger pillbuttonaria-haspopup="dialog", aria-expanded, aria-label (announces current selection)
Modal panelnative <dialog>aria-labelledby → title id; inert while closed (removed just before showModal()): showModal() supplies modal semantics natively, no explicit aria-modal attribute is authored
Sport tiletabtabindex="0", aria-selected, aria-label="{name}"
Live-count badge-aria-label="N live event(s)"
Search input-associated sr-only <label> whose text matches the placeholder

Keyboard: Tab cycles focus inside the modal; Shift+Tab cycles backward; Escape closes; Enter/Space activates the focused tile. Focus returns to the trigger pill on close.

Focus trap: provided by the browser. The widget uses a native <dialog> opened via showModal(), which puts the dialog into the browser's top layer and traps Tab/Shift+Tab inside automatically. The inert attribute on the dialog wrapper keeps its descendants out of the tab order while the dialog is closed.

iOS Safari notes ​

An early prototype tried <div role="dialog"> with user-space sentinels, a custom focus-trap, and a host-elevation workaround to handle iOS Safari's quirks around shadow-rooted overlays. This widget shipped with a different architecture, native <dialog> + showModal(), which lets the browser's top layer escape those quirks. The table below maps each iOS-specific symptom to its current solution:

Symptom on iOSSolution in this widget
Inputs under 16 px trigger an auto-zoomSearch input has inline font-size: 16px.
Native <dialog> focus-trap leaks when used as a styled box in light DOMThe dialog opens via showModal() into the browser's top layer: the trap is browser-managed, not user-space, and works because the dialog tree is no longer anchored to the host's stacking context.
showModal() doesn't scroll-lock the page on iOSuseScrollLock from @gomagaming/core applied on every open (and released on close/unmount).
Shadow-rooted z-index doesn't always cover sibling widgetsNative <dialog> top layer escapes every ancestor stacking context: the dialog paints above sibling widgets without any host-elevation workaround.
Keyboard focus tracking edge casesBrowser-managed focus trap inside the top-layer dialog handles these: no custom keydown tracking needed.
Layout flex/grid quirks on the dialog/panel rootThe dialog itself is a transparent full-viewport stage (display: grid; place-items: center) and the visible panel uses flex flex-col: layout lives on inner wrappers, not on the panel root.

What's deferred ​

The mobile bottom-sheet's drag-handle pill is currently decorative only: the sheet is not swipe-to-dismiss draggable yet; closing on mobile still requires tapping Cancel, the backdrop, or Escape.

Integration Caveats ​

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

  • Tokens/Theming: the docs previously claimed --goma-iconPrimary drives icon color via <sb-icon> here, which is now corrected. This widget's own SportItem.vue never applies a color class to its <sb-icon> usage, so the token has no effect in this widget specifically. (The mechanism itself works: bundled sport SVGs use fill="currentColor", and other widgets, e.g. <goma-suggested-bet>, do recolor an <sb-icon> this way.) The render-error banner's --goma-alertError token is now listed in the Theming table above.

  • Initialization: this widget shares @gomagaming/core's i18n singleton with every other widget on the page; mounting two widgets with different locale values on one page is not currently isolated per instance (a known constraint, not fixed in this pass).

  • Configuring before the definition loads. Assigning config (or any property) while the element is still an unknown element is safe: the value is replayed through the widget when it upgrades. That matters when loading from the CDN, whose loader imports widget modules asynchronously, so your script usually runs first. On release sets at 0.34.0 and earlier the value was silently discarded and the widget mounted with no configuration and a clean console: there, await window.__gomaCdn.ready first, or configure through attributes. See Setting a property before the widget's definition has loaded.

See Integration Caveats for the cross-widget picture.

Changelog ​

Current release: 0.37.11. Every GomaPulse package shares one version number and they are published together, so a release that changes nothing here still bumps this widget. Only the releases that changed it are listed below.

Every release that changed this widget, newest first. The latest is open; the rest expand.

0.37.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, in rpc and subscribe modes. Pickers set to 'LIVE' or 'BOTH' are unchanged, and sports outside the pre-live list still get their names and icons.
0.36.1
  • Playground manifests (@gomagaming/<widget>/playground) now offer a control for every setting each widget really has. Settings that cannot be a form field (raw provider data, read-only state mirrors, props a widget never reads) are recorded with a reason in the repo instead of being silently missing.

    Controls that never reached the widget are fixed:

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

    No widget runtime behaviour changes.

0.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 a follow* prop kept its previous filter, so the highlighted pill and the data underneath it disagreed. The only workaround was to forge the widget's own event on window, which breaks as soon as a *Selector prop is set.

    All seven pickers now emit from a watcher on the selection itself, so a tap, an internal reset and a host write are indistinguishable: <goma-region-filter>, <goma-top-competitions>, <goma-top-competitions-pills>, <goma-competitions-list>, <goma-competition-filter>, <goma-sports-navigation-horizontal> and <goma-sports-navigation-dialog>. @gomagaming/core gains the shared useSelectionEmit composable they all use, so a picker added later inherits the behaviour rather than reimplementing it.

    Three rules hold across all of them. Nothing is emitted on mount, so a value seeded from markup (<goma-region-filter selected-region-id="19">) opens on that scope without firing the chain during boot. Re-assigning the value already held emits nothing. And ids are the contract while names are best-effort: a selection applied before the widget's list has loaded still emits immediately with the right id and null descriptors, so deep links never have to wait for data.

    Two deliberate exceptions: <goma-competitions-list> stays silent when the selection is CLEARED, since closing its drill-in detail has its own event and an empty selection would scope a following feed to nothing; and <goma-sports-navigation-dialog> in showApplyButton mode treats the selection as pending, emitting when Apply is pressed.

    Two behaviour changes come with the single emit point. Re-tapping the item already active no longer emits (it is not a change): in the dialog that still closes it, it just announces nothing. And <goma-top-competitions>'s isFeatured is now derived from highlightedImages and the active variant rather than passed in from the tap, so a host write reports it as truthfully as a click.

    <goma-top-competitions-pills> also gains resetCompetitionOnSportChange (default true), matching resetRegionOnSportChange on <goma-region-filter>: it clears the selection on a sportId change, and with it on no write order lets one navigation action set a sport and a competition together.

  • A sport with no bundled icon now falls back to a real glyph instead of the browser's broken-image icon.

    SbIcon resolves 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 to opacity: 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 childrenIds names 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 neutral noicon glyph as before. The new sportIconNames helper in @gomagaming/core is 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}.svg when one is set, and the origin-relative /icons/sports/dark_mode/{iconId}.svg when it is not. iconBaseUrl has 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 SbIcon inherits 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/subscribe mode useSportsSource mirrors 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 as state.sportsCatalog, and sportIconLookupList (new in @gomagaming/core) is what the tiles, the trigger pill and the settings row read. mapSport also refreshes childrenIds on 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.

  • <goma-sports-navigation-dialog>: the "Sports available" sheet's search box now reads Search sports... instead of Filter by country.... The input filters the sport tiles by name (it has never filtered by country) so the old placeholder described a filter the widget does not have. The fr and pt locales are corrected alongside en (Rechercher un sport... / Procurar desporto...).

    The string lives in @gomagaming/core's locale bundles under sports_navigation_dialog_search_placeholder, and the dialog reuses it for the input's sr-only <label>, so the accessible name moves with the visible placeholder. Hosts that already overrode the key via config.messages are unaffected.

    The same wrong-entity caption survived in French on two more keys: competitions_filter_search_placeholder read Filtrer par pays... and search_field_competitions Filtrez par pays..., where en and pt name a competition in both. Both are orphaned today (no widget references them), so nothing rendered them; they are corrected here so the next widget to adopt one does not ship the bug again. The test is keyed to the competition-captioned family rather than to a single key, so a third instance cannot hide behind a guard that only watches the one someone noticed.

0.34.0
  • Stop a region list fetched for the wrong sport from overwriting the right one, and parse numeric strings assigned to numeric widget properties instead of silently replacing them with the widget's default

    A <goma-region-filter> configured for any sport other than Football issued two location requests instead of one: its own, correctly scoped to the configured sport, and a second one (scoped to Football) that nothing had asked for. Both wrote the same place, so the pills the widget rendered were whichever response happened to come back last. The same race reached <goma-competitions-list>, which groups its competitions by the same region data.

    The unrequested request came from the per-widget boot routine, which fetched locations with no sport at all and so fell back to sport 1. Locations are scoped to a sport on the server, and boot has no sport to scope them by, so it no longer fetches them: the only readers of that data are the widgets above, and each of them already fetches it for the sport it is showing.

    That removes the collision at its source. As a second line of defence, a locations payload now records which sport it was fetched for, and a widget ignores one belonging to a sport it is not showing. This matters whenever two scoped fetches can overlap (switching sport while a request is still in flight, for instance) and it is deliberately narrow: a payload with no recorded sport is still rendered, so the guard can only ever suppress data it can positively identify as belonging to somewhere else.

    Separately, every numeric property on every widget (sportId, skeletonCount, pageSize, spaceBetween, autoplayInterval and the rest) used to accept a JavaScript number and nothing else. A numeric string was not parsed; it was discarded and quietly replaced by the property's default value. el.sportId = '8' therefore left the widget on sport 1 and fetched the wrong data, with nothing logged to say so. Three ordinary integration paths hand over a string: a router parameter, a value read out of a CMS or JSON configuration, and a framework re-render that stringifies its props. Numeric strings are now parsed. Values that are not a finite number and not a numeric string (null, undefined, '', booleans, objects, NaN, Infinity) still fall back to the default, as before. HTML attributes (sport-id="8") and the config object were never affected by this and are unchanged.

0.32.0
  • Widgets no longer dispatch a bare error event alongside goma:error.

    Every factory-built widget's app-level Vue error handler dispatched two events for the same failure: goma:error, and an un-namespaced error. The bare one is gone; goma:error is unchanged and still carries the same { message, code, component? } detail.

    Like every GomaPulse event it was composed and bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had on window.addEventListener('error', …): as a CustomEvent with message, error and filename all undefined, since those are ErrorEvent fields a CustomEvent does not have. A host with error reporting installed received a malformed error record it could neither read nor attribute.

    That had been written off as harmless on the grounds that the handler behind it could never fire. It can. Vue begins its errorCaptured walk at the throwing component's parent, so the onErrorCaptured boundary each widget installs in its own root component cannot see that component's own errors, and being the root, there is nothing above it to catch them either. Anything raised in a widget's root scope (its setup, its render, its lifecycle hooks, its watchers) skips the boundary and reaches the app-level handler every time. Only errors from child components are caught by the boundary, which is what it is for: those show the in-shadow banner and emit goma:retry.

    Hosts listening for goma:error need no change. A host listening for bare error on a widget element should move to goma:error; it was documented as unreachable, so in practice it delivered nothing until now.

0.26.0
  • Stop publishing source maps and package source.

    dist/*.js.map was emitted by the shared Vite preset and swept into every tarball by files: ["dist/", "src/"], with the original source inlined in sourcesContent. src/ shipped the same source again, in the clear. Both are one URL away from a browser once a package is served from a CDN, so the preset now builds with sourcemap: false and files no longer carries src/.

    @gomagaming/core keeps the two paths its export map actually points at: src/styles/main.css and src/types/index.d.ts. Its tarball drops from 1741 files to 576, which also brings it back under the 900-file limit a per-request unpacker can handle.

    Deep imports of @gomagaming/<pkg>/src/* from an installed package no longer resolve. Nothing documented used them; imports inside the monorepo are unaffected.

0.12.0
  • <goma-sports-navigation-dialog>: tighten and complete the selected-sport trigger pill. Icon (and its placeholder) sized to 16px, pill padding 5px 10px, and the label now always renders (truncating long sport names with an ellipsis so the pill stays compact), falling back to a "Sport" placeholder (new sports_navigation_dialog_placeholder i18n key in en/fr/pt) while the sport list loads instead of showing an empty pill.
0.11.0
  • <goma-sports-navigation-dialog>: redesign the picker (60px-circle grid, "Filter by country…" search placeholder) and add composition props: showTrigger (hide the built-in pill), showBackButton (Back button → goma:dialog-back), and showApplyButton (Apply footer: tapping a tile highlights, Apply commits goma:sport-select + closes). It can now be opened externally via a composed goma:sports-dialog-open event. Panel height is now a definite 90dvh/92dvh so it matches the <goma-filters-settings> settings modal exactly (they swap in place when embedded).
0.9.2
  • The dialog trigger pill is redesigned: it now shows the selected sport's icon and name on the accent (highlightPrimary) background, rounded on the right only, without the up/down caret arrows.
  • goma-sports-navigation-horizontal and goma-sports-navigation-dialog gain a showLiveCount prop / show-live-count attribute (default true). Set it to false to hide the live-event count badge on each sport tile.
0.9.0
  • New widget <goma-competitions-list>: vertical list of competitions grouped by region, scoped per sport.

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

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

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

  • New widget <goma-multi-banners>. A swipeable carousel of injectable banner cards, three types in one widget: sport events, casino game promos, and info banners.

    All data is host-fed via the banners array (no CMS). For a sport banner, setting eventId (live or prelive) makes the widget resolve teams, start time, score and odds itself; without it the host supplies those fields inline. The background image is always host-fed.

    • Sport cards adapt to event state: live cards show the running clock/part and score (home - away), prelive cards show the kickoff date + time.
    • Carousel chrome: prev/next arrows, pagination dots, swipe/drag, opt-in autoplay (off by default). The track is clipped + isolated so nothing bleeds over neighbouring widgets.
    • Emits goma:banner-select (every banner), goma:event-navigate (sport-card body tap → game-details), and goma:outcome-select (sport odds tap → betslip, same shape as events-horizontal).
    • No new theme tokens; all colours map to existing --goma-* tokens.

    @gomagaming/sports-domain additions (additive, no change to existing paths):

    • New action group sports.bannerEvents (createBannerEventsActions): a multi-event subscription manager that accumulates events into bettingStore.events (skipStoreCleanup) so several event cards keyed by id coexist with live odds, unlike the singular-game matchDetails path.
    • Public exports for useGameScoresTallOdds and useGamePartsAndTimesTallOdds so other widgets can render live scores/clocks from a store event.
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-select with the full payload (competitionId, competitionName, sportSportId, competitionVenueId, previousCompetitionId).

    • Three data sources via dataSource: 'prop' (host-fed), 'rpc' (one-shot popularTournaments call 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 off flagBaseUrl ({venueId}.png). Same conventions as the rest of the filter family.
    • New composable useTopCompetitionsSource in @gomagaming/sports-domain wraps the existing oddsApi.betting.getTopCompetitions RPC.

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

0.7.0
  • Playground scenario presets, live theme-token editor, and family-wide picker-coupling defaults.

    Widget runtime defaults

    • events-horizontal: followSportPicker, followRegionPicker, followCompetitionPicker, followTimePicker all flip from false to true in 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 to false (or the matching HTML attribute follow-sport-picker="false", etc.).
    • region-filter: followSportPicker default flips to true in the element class + manifest. The widget will re-fetch the region list whenever a sibling sports picker emits goma:sport-select.
    • competition-filter: followSportPicker and followRegionPicker defaults both flip to true. Drops the manual wiring step from the picker-chain composition.
    • region-filter / competition-filter: playground manifest's source default flips from 'mock' to 'rpc'. Aligns the manifest with the element class's own dataSource: '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*Picker explicitly will now see them auto-react to each other's selection events. The old isolated behaviour is restored with follow*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), and dark-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.
    • 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 (sparse Record<--goma-key, hex>), which Stage's fast-path watcher pushes via el.theme = … to every mounted element. _applyTheme() writes host.style.setProperty(--goma-X) reactively, so token edits repaint with no Vue remount.
    • Share URL: v3 envelope additively carries themeTokens and activeScenario. Default values are dropped from the encoded payload (size optimisation, hydrate re-applies defaults). Foreign payloads with poisoned themeTokens (non-object, arrays, non-string values) are normalised back to {} in hydrate().
    • dark-mint theme renamed to dark-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 #5b56ff to 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 pin theme: 'dark-mint' fall back to THEMES.default after hydrate.

    Tests

    • New tests/playground-scenarios.test.js: 19 vitest cases covering applyScenario, setThemeToken + activeScenario Custom-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.
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 off iconBaseUrl (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> } via goma:competition-select: maps straight to the matches-aggregator topic's literal all no-filter tournamentId segment.
    • Opt-in followSportPicker (re-fetch on sport-select) and followRegionPicker (client-side filter on venueId via the picked regionId; 'all' disables). Both reset selectedCompetitionId to 'all' and emit goma:competition-select on context change so downstream listeners clear their tournamentId filter in the same tick.

    @gomagaming/events-horizontal additions

    • New followCompetitionPicker + competitionPickerSelector opt-in props. Third in the picker chain: listens for goma:competition-select at the window and rewrites segment index 2 (tournamentId) of the matches-aggregator topic. The existing rewriteTopicSegment(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 followCompetitionPicker toggle under "advanced".

    @gomagaming/sports-domain additions

    • useCompetitionsSource(state): public composable. Watches state.sportId + state.regionId + state.dataSource; calls oddsApi.betting.getCompetitions({ sportId }) on connect and mirrors a venueId-filtered slice of bettingStore.competitions into state.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 as subscribeToLocations). Backend topic for live competition updates is deferred.

    @gomagaming/core additions

    • 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-filter joins the registry, types WidgetId union, store ALL_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.exclude entry in demo/.

    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*Picker flags and the topic chain {sportId}/{locationId}/{tournamentId} updates segment-by-segment with no host glue and no WAMP reconnect.

  • Drop the unused operatorId field from the public widget config surface. The field was declared in @gomagaming/core defaults + TypeScript types but never read anywhere on the data path: ucsOperatorId is the only operator identifier the EveryMatrix adapter (and useBetslipLogic) consume. Setting config.operatorId on a widget element now logs an "unknown config key" warning in debug: true mode instead of being silently accepted.

    Migration: consumers using config.operatorId should rename to config.ucsOperatorId. No behavioural change for consumers who already only set ucsOperatorId (which the docs have always recommended).

    @gomagaming/core changes:

    • createWidgetElement.js: COMMON_DEFAULTS no longer seeds operatorId; the knownTopLevelKeys validator set drops it.
    • types/index.d.ts: removed operatorId?: string from the GomaWidgetConfig interface.

    @gomagaming/betslip-sidebar docstring tweak:

    • custom-elements.json example endpoint placeholder changed from /place-bet/4313/v2/bets to /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 operatorId input row; share-URL envelope, persistence, types, and preset filler all scrubbed in lockstep so older share URLs that carried operatorId are silently ignored on hydrate (the field falls out of STATE_DEFAULTS and the validator's stringFields list).
    • 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, realm www.demo1-stage.everymatrix.com, ucsOperatorId 2838).

    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.com placeholders), example ucsOperatorId literal updated to 2838. 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 placeholder subscribe mode that currently falls back to RPC until the backend exposes a live-update topic for locations.

    What's new

    • New package @gomagaming/region-filter published 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' } via goma:region-select (canonical "no filter" sentinel, matches the matches-aggregator topic format where locationId='all').
    • Flag glyphs rendered from a configurable CDN: default flagBaseUrl is the upstream Glastcoper flag pack, resolved as ${flagBaseUrl}{regionId}.png. Falls back to a globe glyph when a flag fails to load.
    • Opt-in followSportPicker automation (mirrors <goma-events-horizontal>): when enabled, the widget listens at the window for goma:sport-select events emitted by sibling sports-navigation widgets and rewrites its sportId so the region list refetches automatically. Optional sportPickerSelector scopes the listener to a specific picker for multi-instance disambiguation.

    @gomagaming/events-horizontal additions

    • New followRegionPicker + regionPickerSelector opt-in props. Mirror the existing followSportPicker plumbing but listen for goma:region-select and rewrite the locationId segment (index 1) of the matches-aggregator topic. The region-filter's hardcoded "All" pill emits regionId: 'all' which lines up exactly with the matches-aggregator topic's literal all no-filter segment: no special-casing needed. Drop a <goma-region-filter> alongside an events-horizontal feed and set follow-region-picker on the latter; selections flow through with no host glue and no WAMP reconnect (the existing useTopicSubscription watcher rebinds the topic in place).

    @gomagaming/sports-domain additions

    • useLocationsSource(state): public composable that drives state.regions from the WAMP API. Watches state.dataSource, state.sportId, and the apiContainer's connection; on connect, calls oddsApi.betting.getLocations({ sportId }) and mirrors bettingStore.locations into 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-shot getAllLocations until 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/core additions

    • Three new bundled i18n keys across en.json, fr.json, and pt.json: region_filter_aria, region_filter_empty.

    gomawt playground

    • region-filter joins the playground registry, store ALL_WIDGETS, types WidgetId union, and VitePress theme side-effect imports. The embedded /playground route can mount it alongside the other six widgets.
  • 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 setupApp plumbing on the element, no shared composable in @gomagaming/sports-domain. Just an intervals prop, a selectedTimeValue selection state, and a goma:time-select event.

    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 }, …]. label is rendered verbatim (hosts pre-localise, only the hardcoded "All" pill is locale-driven, via the bundled time_filter_all_label key). value matches the matches-aggregator topic's hoursInterval segment ('0-1', '0-24', '24-48', etc.). Malformed entries drop silently; duplicates by value keep the first occurrence.
    • The hardcoded "All" pill emits { timeValue: 'all', timeLabel: <localised>, previousTimeValue } via goma:time-select: maps straight to the matches-aggregator topic's literal all no-filter hoursInterval segment.

    @gomagaming/events-horizontal additions

    • New followTimePicker + timePickerSelector opt-in props. Fourth in the picker chain. Listens for goma:time-select at the window and rewrites segment index 3 (hoursInterval) of the matches-aggregator topic with detail.timeValue. Reuses the existing rewriteTopicSegment(topic, idx, value) helper (with its canonical-prefix fallback so mock-mode + pre-WAMP-connect rewrites still land).
    • Playground manifest gains a followTimePicker toggle under "advanced".

    @gomagaming/core additions

    • Three new bundled i18n keys per locale (en/fr/pt): time_filter_all_label, time_filter_aria, time_filter_empty.

    gomawt playground

    • time-filter joins the registry, types WidgetId union, store ALL_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.exclude entry in demo/.

    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*Picker flags and the topic chain {sportId}/{locationId}/{tournamentId}/{hoursInterval} updates segment-by-segment with no host glue and no WAMP reconnect.

0.5.2
  • packages: wire up the now-live gomawt.gomagaming.com docs subdomain across npm-facing surfaces. Adds a homepage field to each of the eight publishable package.json files so the npm sidebar shows a "Homepage" link again (widget packages point at their per-widget docs page under /widgets/<name>; @gomagaming/core and @gomagaming/sports-domain point at the site root). Replaces the two _coming soon_ Documentation bullets in each README with real links to gomawt.gomagaming.com/widgets/<name> (full reference) and gomawt.gomagaming.com/playground (live playground). No code change.
0.5.1
  • events-horizontal: flip followSportPicker default to true in 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 via config.followSportPicker or the HTML attribute still get the old behaviour.

  • Playground + maintenance polish:

    • @gomagaming/events-horizontal now exports its own package.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 drifts liveCount every 4 seconds, matching the horizontal sister. Previously its subscribe() 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, and bugs fields from all eight publishable package.json files: 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_TOKEN auth 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 single homepage field add per package.json once 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 existing goma:event-navigate event (opt-out via config.followEventNavigate: false).

    What's new

    • New package @gomagaming/game-details published in lockstep alongside the existing widgets. Element: <goma-game-details>. Reference docs: docs/widgets/game-details.md.
    • New per-component subscription pattern (.subscription.js files) port the source post-a separate change architecture: each internal component (Header / MarketsList) owns its own WAMP subscription with onScopeDispose cleanup. Multi-instance safe: each <goma-game-details> mount gets its own useGameDetails() state and createMatchDetailsActions() closure.
    • Five new theme tokens with CSS-level fallbacks to existing tokens: --goma-tabPillBackground, --goma-tabPillBackgroundActive, --goma-tabPillText, --goma-tabPillTextActive, --goma-marketCardHeaderBackground. Documented in docs/guide/theming.md → Game details.
    • New marketsListHeight config knob (mirrored as the markets-list-height HTML 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-domain additions

    • actions.sports.matchDetails.*: six new WAMP actions lifted from the source repo's actions tree: subscribeToMatch / unSubscribeToMatch, subscribeToMarketGroups / unRegisterToMarketGroups, subscribeToMarketsByMarketGroupKey / unSubscribeToMarketsByMarketGroupKey. Per-instance unregister closure via createMatchDetailsActions(): 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 over apiContainer and read oddsApi from apiContainer.oddsApi.value directly instead of useOddsApi() / inject(). Required because action methods are invoked from watcher callbacks that fire outside Vue's setup context (e.g., when a WAMP onInitialDump cascades through a reactive store patch into a watcher), where inject() returns undefined. The closure path is reliable regardless of where the callback runs. Backwards-compatible: existing adapters that ignore the second arg keep working.
    • useLoader loader store gains matchDetailsMarketGroupMarkets flag (used by the markets-by-group action's afterInitialDump callback).

    @gomagaming/core additions

    • Bundled i18n keys for game-details added across en.json, fr.json, and pt.json: empty_state_match_not_available_*, empty_state_match_no_markets_*, market_categories, collapse.
    • createWidgetElement's COMMON_DEFAULTS now declare the provider / connection fields (provider, socketUrl, socketRealm, bettingApiBaseUrl, operatorId, ucsOperatorId) so every widget's reactive state bridge auto-tracks them. Setting them via el.config = { socketUrl: '…', … } propagates into the widget's _state: the new connectSportsApiReactive helper watches that and reconnects on runtime change.

    Connection-reactive widgets (all five publishable widgets + game-details)

    • Every shipped widget now uses connectSportsApiReactive instead of the one-shot connectSportsApi(...) call in setupApp. Hosts can change socketUrl (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:connection events).
    • 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 subscribe mode now auto-builds the WAMP topic from the manifest's sportId + eventLimit when the user hasn't set topic explicitly.

    Follow-up

    • BetBoost tab, share button, favorite toggle, stats / lineups / H2H / commentary are explicit v2+ surfaces (recorded in docs/widgets/game-details.md → What's deferred).
  • Playground manifest schema additively extended (Phase 4 inspector parity with demo/). @gomagaming/core exports new control types (slider, textarea, json), two new tag values ('transport', 'mock'), an optional per-control dependsOn: { key, oneOf } field for conditional visibility, an optional top-level themeTokens per-widget token registry, and an optional topicBuilder function 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.js will be expanded in subsequent commits on this branch with the full per-widget configuration surface previously only exposed in the internal demo/ 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 respect dependsOn filtering; the per-widget manifests catch up in Block D.

0.4.0
  • Phase 2 retrofit for the betslip widgets: @gomagaming/betslip-floating and @gomagaming/betslip-sidebar now ship the same ./playground sub-export contract as the other widgets. Each has a src/playground.manifest.js validated by PlaygroundManifestSchema in @gomagaming/core, a dist/playground.js emitted alongside the main bundle (via the extraEntries option in @gomagaming/vite-preset), a ./playground entry in package.json exports, and a per-package Vitest test asserting schema conformance. Unlike events-horizontal / sports-navigation-*, the betslip widgets ship no mock() 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.include is extended to pre-bundle all five main entries plus their ./playground sub-exports.

0.3.0
  • Adds the playground manifest contract: every widget package now exports a ./playground sub-export carrying a declarative manifest (validated by PlaygroundManifestSchema in @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 core and sports-domain exports to drop the ./src/* escape hatch, and promote widget-consumed targets into main entries. Multi-widget consumers now resolve a single copy of vue, 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-horizontal 1.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/core and @gomagaming/sports-domain package.json exports. 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 exports LiveRegularOdds, PreLiveRegularOdds, OutrightRegularOdds, MarketRegularOdds, SkeletonCard, useTopicSubscription, useSportsSource, sortOutcomes, and normaliseTopicPrefix. @gomagaming/sports-navigation-dialog ships for the first time at 0.2.0 (no prior 0.1.x publish).

All notable changes to this package will be documented in this file. Auto-generated by Changesets; do not edit by hand.