Skip to content

Sports navigation — deployment scenarios

goma-sports-navigation-horizontal and goma-sports-navigation-dialog are interchangeable: identical config object, identical props, identical goma:sport-select event payload. That parity unlocks three deployment patterns — pick whichever matches the host page's layout budget without rewriting any data wiring.

ScenarioWhen to reach for it
Horizontal-onlyDesktop / wide tablet — there's room above the fold for a horizontal swiper.
Dialog-onlyMobile / narrow surfaces — vertical space is precious; a single trigger pill is all that fits.
Both togetherResponsive sites that show the swiper on desktop and collapse to the pill on mobile.

The goma:sport-select event is bubbles + composed, so cross-widget wiring (e.g. goma-events-horizontal's followSportPicker) reacts to whichever picker is mounted. No data-flow rewiring is needed when swapping pickers — see Cross-widget coordination.


Horizontal-only

The default desktop pattern. The widget claims a single horizontal row above the events feed and never opens a modal.

html
<goma-sports-navigation-horizontal id="nav"></goma-sports-navigation-horizontal>
<goma-events-horizontal id="ev" followSportPicker></goma-events-horizontal>
<script type="module">
  import '@gomagaming/sports-navigation-horizontal'
  import '@gomagaming/events-horizontal'

  const config = {
    socketUrl: 'wss://api.example.com/v2',
    socketRealm: 'www.example.com',
    apiBaseUrl: 'https://api.example.com',
    bettingApiBaseUrl: 'https://sports-api.example.com',
    ucsOperatorId: '4313',
    locale: 'en',
  }
  const nav = document.getElementById('nav')
  nav.dataSource = 'subscribe'
  nav.liveStatus = 'LIVE'
  nav.config = config

  const events = document.getElementById('ev')
  events.config = config
  events.topic = '/topic/{sportId=44}/...'
</script>

Notes

  • One config object, applied to both widgets. Bundlers dedup vue / pinia / vue-router / vue-i18n / @vueuse/core because they're declared as peerDependencies in every @gomagaming/* package.
  • The horizontal widget centres the selected tile on first paint and on every selection change — keep ~80 px of vertical space for the row including the live-count badges.

Dialog-only

The default mobile pattern. The page shows just a small pill — tapping it opens a full-screen sport picker. Vertical real estate is preserved for content underneath.

html
<goma-sports-navigation-dialog id="picker"></goma-sports-navigation-dialog>
<goma-events-horizontal id="ev" followSportPicker></goma-events-horizontal>
<script type="module">
  import '@gomagaming/sports-navigation-dialog'
  import '@gomagaming/events-horizontal'

  const config = {
    socketUrl: 'wss://api.example.com/v2',
    socketRealm: 'www.example.com',
    apiBaseUrl: 'https://api.example.com',
    bettingApiBaseUrl: 'https://sports-api.example.com',
    ucsOperatorId: '4313',
    locale: 'en',
    // The pill should be transparent — the trigger paints its own
    // surface, the host element shouldn't add a second one.
    theme: { backgroundPrimary: 'transparent' },
  }
  const picker = document.getElementById('picker')
  picker.dataSource = 'subscribe'
  picker.liveStatus = 'LIVE'
  picker.config = config

  const events = document.getElementById('ev')
  events.config = config
</script>

Notes

  • The dialog opens via the browser's native <dialog> top layer — no custom z-index plumbing or stacking-context elevation is required even when the host page has heavy z-indexed siblings.
  • On viewports below mobileBreakpoint (default 'sm' = 640 px) the panel renders as a bottom-sheet; above, it renders as a centred modal. Tune via el.mobileBreakpoint = 'md' (768 px) or any raw px value.
  • el.showSearch = false removes the search input when the catalogue is small enough that scrolling beats searching.

Both together (responsive)

Mount both widgets on the same page, hide one or the other with CSS at the breakpoint of your choice. Because the widgets share the same goma:sport-select event contract, the downstream consumer sees one stream of selections regardless of which picker emitted them.

html
<style>
  /* Show the swiper above 768 px, the pill below. */
  .picker-strip   { display: block; }
  .picker-pill    { display: none; }
  @media (max-width: 768px) {
    .picker-strip { display: none; }
    .picker-pill  { display: block; }
  }
</style>

<goma-sports-navigation-horizontal id="strip" class="picker-strip"></goma-sports-navigation-horizontal>
<goma-sports-navigation-dialog     id="pill"  class="picker-pill"></goma-sports-navigation-dialog>
<goma-events-horizontal id="ev" followSportPicker></goma-events-horizontal>

<script type="module">
  import '@gomagaming/sports-navigation-horizontal'
  import '@gomagaming/sports-navigation-dialog'
  import '@gomagaming/events-horizontal'

  // Same object, both pickers, no copy/paste of wiring.
  const config = { /* … same as above … */ }
  for (const el of [strip, pill]) {
    el.dataSource = 'subscribe'
    el.liveStatus = 'LIVE'
    el.config = config
  }
  document.getElementById('ev').config = config

  // Optional: keep the two pickers in sync so a desktop user who
  // resizes down (and vice-versa) lands on the same selection.
  function syncSelection(e) {
    const next = e.detail.sportId
    if (strip.selectedSportId !== next) strip.selectedSportId = next
    if (pill.selectedSportId  !== next) pill.selectedSportId  = next
  }
  strip.addEventListener('goma:sport-select', syncSelection)
  pill .addEventListener('goma:sport-select', syncSelection)
</script>

Notes

  • Both widgets share useSportsSource upstream, so each one fetches/subscribes independently. If you'd rather pay for one WAMP subscription, mount the dialog with dataSource = 'prop' and feed it from the horizontal widget's selection updates — or vice-versa.
  • Display: none / display: block is enough — the widgets unmount cleanly when removed and remount instantly. CSS-toggling the visibility keeps both lists warm.

Cross-widget coordination (followSportPicker)

<goma-events-horizontal> exposes a followSportPicker flag (with optional sportPickerSelector) that auto-rewires its WAMP topic whenever any sports picker on the page emits goma:sport-select. Both widgets work as picker — the events-horizontal listener doesn't care which one fired, only that the detail shape matches.

html
<!-- Single picker (either widget works) -->
<goma-sports-navigation-dialog id="picker"></goma-sports-navigation-dialog>
<goma-events-horizontal followSportPicker></goma-events-horizontal>

<!-- Multiple pickers — point the events feed at one explicitly -->
<goma-sports-navigation-horizontal id="desktop"></goma-sports-navigation-horizontal>
<goma-sports-navigation-dialog     id="mobile"></goma-sports-navigation-dialog>
<goma-events-horizontal
  followSportPicker
  sportPickerSelector="#desktop"
></goma-events-horizontal>

See <goma-events-horizontal> § follow-sport-picker for the full automation contract — it doesn't change between scenarios.


Picking config keys per scenario

ScenarioRequired keysOptional
Horizontal-only prop modeiconBaseUrl, localetheme, messages, debug
Horizontal-only rpc / subscribe modesocketUrl, socketRealm, apiBaseUrl, ucsOperatorId, localebettingApiBaseUrl, iconBaseUrl, theme, messages, debug
Dialog-onlySame as horizontal — exact same keys, same parsing.+ theme.backgroundPrimary = 'transparent' (host pill on a styled page).
Both togetherThe union — declare once, assign to both widgets.theme.dialogBackground to keep the dialog opaque while the host pill is transparent.

The widgets are strictly additive in the same config object — supplying socketUrl to a prop-mode picker is harmless; the picker only reads what it needs. This is why the same config drives both widgets without runtime checks.


Migration notes

Both widgets are first-class shipped packages — there is no pre-existing public release to migrate from. The notes below cover the behavioural differences a consumer would hit when switching between the two pickers in an existing host page.

From horizontal to dialog (or vice-versa)

ConcernMigration step
Element tagRename <goma-sports-navigation-horizontal><goma-sports-navigation-dialog>. Custom-element registration is a side-effect of the package import.
Package importSwap import '@gomagaming/sports-navigation-horizontal' for import '@gomagaming/sports-navigation-dialog'. Same peer deps.
Config objectNo change. Same keys, same parsing.
PropsNo change for the shared contract (sports, selectedSportId, dataSource, liveStatus, showLabels, skeletonCount).
Dialog-only propsThe dialog adds open, showSearch, gridColumns, mobileBreakpoint. The horizontal widget adds spaceBetween (gap between tiles). Neither is required.
EventsNo change for goma:sport-select. The dialog also emits goma:dialog-open / goma:dialog-close — extra signals, not breaking ones.
ThemingThe dialog uses --goma-dialogBackground for the modal panel surface and is the only widget that reads it. Horizontal-only deployments can safely ignore the token.

Host stylesheet

When mounting the dialog where the horizontal widget previously lived, set theme.backgroundPrimary = 'transparent' so the host element doesn't paint a wide dark band behind the trigger pill. The horizontal widget needs backgroundPrimary opaque for its fade overlays — keep the override scoped to the dialog instance.