Appearance
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.
| Scenario | When to reach for it |
|---|---|
| Horizontal-only | Desktop / wide tablet — there's room above the fold for a horizontal swiper. |
| Dialog-only | Mobile / narrow surfaces — vertical space is precious; a single trigger pill is all that fits. |
| Both together | Responsive 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/corebecause they're declared aspeerDependenciesin 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 viael.mobileBreakpoint = 'md'(768 px) or any raw px value. el.showSearch = falseremoves 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
useSportsSourceupstream, so each one fetches/subscribes independently. If you'd rather pay for one WAMP subscription, mount the dialog withdataSource = '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
| Scenario | Required keys | Optional |
|---|---|---|
Horizontal-only prop mode | iconBaseUrl, locale | theme, messages, debug |
Horizontal-only rpc / subscribe mode | socketUrl, socketRealm, apiBaseUrl, ucsOperatorId, locale | bettingApiBaseUrl, iconBaseUrl, theme, messages, debug |
| Dialog-only | Same as horizontal — exact same keys, same parsing. | + theme.backgroundPrimary = 'transparent' (host pill on a styled page). |
| Both together | The 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)
| Concern | Migration step |
|---|---|
| Element tag | Rename <goma-sports-navigation-horizontal> → <goma-sports-navigation-dialog>. Custom-element registration is a side-effect of the package import. |
| Package import | Swap import '@gomagaming/sports-navigation-horizontal' for import '@gomagaming/sports-navigation-dialog'. Same peer deps. |
| Config object | No change. Same keys, same parsing. |
| Props | No change for the shared contract (sports, selectedSportId, dataSource, liveStatus, showLabels, skeletonCount). |
| Dialog-only props | The dialog adds open, showSearch, gridColumns, mobileBreakpoint. The horizontal widget adds spaceBetween (gap between tiles). Neither is required. |
| Events | No change for goma:sport-select. The dialog also emits goma:dialog-open / goma:dialog-close — extra signals, not breaking ones. |
| Theming | The 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.