Skip to content

<goma-competition-filter> ​

A host-fed horizontal filter pill bar: a row of single-selection pills with two optional edge triggers (a configurable icon / text / colour pill pinned flush to the left and/or right edge). Fully host-driven: no WAMP, no RPC, no data fetching of any kind. Use it for any "nav pills" strip (categories, competitions, filters) where the host already has the list of options in hand.

Live demo ​

Open in the playground →: toggle Filter bar in the widget rail. Edit items, leftTrigger and rightTrigger in the inspector's JSON tab.

Element tag ​

html
<goma-competition-filter></goma-competition-filter>

Installation ​

@gomagaming/competition-filter is a scoped private package. See Installation for the one-time .npmrc setup.

bash
npm install @gomagaming/competition-filter

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

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

Configuration ​

el.config = { locale, theme, messages, debug }: the standard widget config bag. This widget makes no network calls, so there is no apiBaseUrl / socketUrl.

KeyTypeDescription
localestringActive i18n locale. Only the error boundary's retry button uses it. Defaults to 'en'.
themeRecord<string,string>--goma-* token overrides: see Theming.
messagesobjectOptional i18n phrase overrides.
debugbooleanDefaults to false.

Data sources ​

There is exactly one path in: the host sets items (and, optionally, leftTrigger / rightTrigger) as JS properties or JSON-encoded HTML attributes. Nothing is fetched or subscribed to.

js
const bar = document.getElementById('bar')
bar.items = [
  { id: 'popular', label: 'Popular' },
  { id: 'upcoming', label: 'Upcoming' },
  { id: 'top', label: 'Top Competitions' },
  // A pill may carry an optional leading icon (inline SVG, uses currentColor).
  { id: 'all', label: 'All Competitions', icon: '<svg …>…</svg>' },
]
bar.selectedId = 'top'

// Optional edge triggers (omit / null to hide).
bar.leftTrigger  = { icon: '<svg …>…</svg>', text: 'Tennis', color: '#29f2ff', textColor: '#062225', caret: true }
bar.rightTrigger = { icon: '<svg …>…</svg>' }

Props / attributes ​

All keys can be set as JS properties, or as kebab-cased HTML attributes (object / array attributes are JSON-encoded).

PropertyAttributeTypeDefaultPurpose
itemsitemsArray<{ id, label, icon?, iconFallback?, outlined? }>[]The pills. icon is an optional inline-SVG string (uses currentColor) shown before the label; it may instead name a bundled icon, with iconFallback as its stand-in (see Trigger). outlined: true draws the pill as an unfilled 2px ring (--goma-textSecondary, --goma-highlightPrimary when selected) for a pill that should read differently from its siblings (e.g. "All Competitions" with a funnel icon); its vertical padding follows pillBorderWidth, so it stays as tall as the plain pills; plain pills keep their --goma-backgroundCards fill.
selectedIdselected-idstring | number''The active pill's id. Updated on tap (two-way).
leftTriggerleft-triggerTrigger | nullnullOptional pill flush to the left edge. null hides it.
rightTriggerright-triggerTrigger | nullnullOptional pill flush to the right edge. null hides it.
spaceBetweenspace-betweennumber8Pixel gap between pills.

Trigger: { icon?, iconFallback?, text?, color?, textColor?, caret? }:

FieldTypePurpose
iconstringInline-SVG string (uses currentColor), or the name of a bundled icon such as icons/sports/dark_mode/1.svg. A name the bundled set does not carry is requested from the host (iconBaseUrl); a sport icon that fails there too draws that set's noicon, whether it is named icons/sports/<mode>/…, sports/<mode>/… or assets/icons/sports/<mode>/….
iconFallbackstringName of a bundled icon to draw when icon names one that neither the bundled set nor the host can serve, such as the sport's family glyph. Tried before the implied noicon.
textstringLabel shown next to the icon.
colorstringPill background colour (any CSS colour). Defaults to the backgroundOdds token.
textColorstringIcon + label colour. Defaults to textPrimary.
caretbooleanDraws an up/down caret after the label, marking the trigger as opening a picker. Takes its colour from textColor.

The caret is the widget's own glyph rather than something you pass in, so every bar that opens a picker shows the same affordance. Feed icon for the content of the pill; set caret for the hint that tapping it opens something.

Behavior ​

Selecting a pill scrolls it to the centre of the pills strip (and a pre-set selectedId centres once the items paint), so the active pill is always visible even when the row overflows. Only the pills container scrolls: the host page's scroll position is never touched. Tapping the pill that is already selected is a no-op: no event fires and no scroll happens.

Events ​

Every event is composed: true and bubbles: true.

EventdetailWhen
goma:filter-select{ id, label, previousId }A pill was tapped, or the host set selectedId. The widget also flips selectedId. No-op if it was already selected; never emitted on mount. See Driving a selection from the host.
goma:filter-left{ icon?, text?, color?, textColor?, caret? }The left edge trigger was tapped. Echoes the leftTrigger object.
goma:filter-right{ icon?, text?, color?, textColor?, caret? }The right edge trigger was tapped. Echoes the rightTrigger object.
ready{}Widget mounted and initial render complete.
goma:error{ message, code, component? }Fires on 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. The connection-layer path other widgets document (code: 'API_INIT_ERROR') does not exist here: the bar is purely host-fed, with no connection that can fail.
goma:retry{}User clicked Retry on the render-error banner.

The edge triggers only emit: the widget does not open anything itself; the host wires whatever it needs (open a sport picker, a settings modal, navigate, …).

This widget does not emit goma:competition-select, despite its name: that event is emitted by <goma-competitions-list> and <goma-top-competitions-pills>. See the cross-widget wiring guide for the full emitter/listener matrix.

Empty / loading / error states ​

There is no loading state: nothing is fetched. With items: [] (the default) the pills strip renders empty inside the bar; there's no dedicated empty-state message. A Vue render error is caught by the app's error boundary and shown as a role="alert" banner with a Retry button (emits goma:retry and clears the banner); the banner also auto-clears after 8 seconds.

Theming ​

All colours map to existing --goma-* tokens; host-fed Trigger.color / Trigger.textColor win over the token defaults for that trigger.

TokenRoleDefault
--goma-navPillsBar background#161732
--goma-backgroundCardsPill background#1f2147
--goma-backgroundOddsEdge trigger default background#434799
--goma-textPrimaryPill / trigger text#ffffff
--goma-textSecondaryActive (pressed) pill text#939dff
--goma-highlightPrimarySelected pill border#ff6600
--goma-highlightSecondaryHover / active pill border#404cff
--goma-alertErrorRender-error banner background#f9554d
--goma-highlightPrimaryContrastRender-error banner Retry button text/background#ffffff

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.

KeyCSS variableDefaultAffects
pillHeight--goma-cfPillHeightauto (38px from padding and border)Height of every pill, plain and outlined.
pillPaddingX--goma-cfPillPaddingX20pxSide padding of the plain pills (outlined pills keep 12px).
pillBorderWidth--goma-cfPillBorderWidth1pxRing width of the plain pills: transparent when idle, highlightPrimary when selected (outlined pills keep 2px).
barPaddingY--goma-cfBarPaddingY12pxVertical padding of the pill strip; with pillHeight it sets the bar height.

i18n / message overrides ​

KeyDefault (en)Used by
retry"Retry"Render-error banner's retry button (shared key).

Pill labels and edge-trigger texts are host-fed, not localised by the widget: pre-localise them before passing items / leftTrigger / rightTrigger in.

Embedding examples ​

Vanilla / CDN ​

html
<goma-competition-filter id="bar"></goma-competition-filter>
<script type="module">
  import '@gomagaming/competition-filter'

  const bar = document.getElementById('bar')
  bar.items = [
    { id: 'popular', label: 'Popular' },
    { id: 'upcoming', label: 'Upcoming' },
    { id: 'top', label: 'Top Competitions' },
    { id: 'all', label: 'All Competitions', icon: '<svg …>…</svg>' },
  ]
  bar.selectedId = 'top'
  bar.rightTrigger = { icon: '<svg …>…</svg>' }

  bar.addEventListener('goma:filter-select', (e) => console.log(e.detail)) // { id, label, previousId }
  bar.addEventListener('goma:filter-left',  () => openSportPicker())
  bar.addEventListener('goma:filter-right', () => openSettings())
</script>

React ​

jsx
// items / triggers are object/array props → set them via a ref.
import { useEffect, useRef } from 'react'
import '@gomagaming/competition-filter'

function FilterBar() {
  const ref = useRef(null)
  useEffect(() => {
    const el = ref.current
    if (!el) return
    el.items = [
      { id: 'popular', label: 'Popular' },
      { id: 'upcoming', label: 'Upcoming' },
      { id: 'top', label: 'Top Competitions' },
    ]
    el.selectedId = 'top'
    el.rightTrigger = { icon: '<svg …>…</svg>' }
    const onSelect = (e) => console.log(e.detail) // { id, label, previousId }
    el.addEventListener('goma:filter-select', onSelect)
    return () => el.removeEventListener('goma:filter-select', onSelect)
  }, [])
  return <goma-competition-filter ref={ref} />
}

Vue ​

vue
<script setup>
import { ref, onMounted } from 'vue'
import '@gomagaming/competition-filter'

const bar = ref(null)
onMounted(() => {
  bar.value.items = [
    { id: 'popular', label: 'Popular' },
    { id: 'upcoming', label: 'Upcoming' },
    { id: 'top', label: 'Top Competitions' },
  ]
  bar.value.selectedId = 'top'
  bar.value.rightTrigger = { icon: '<svg …>…</svg>' }
})
</script>

<template>
  <goma-competition-filter
    ref="bar"
    @goma:filter-select="(e) => console.log(e.detail)"
    @goma:filter-right="openSettings"
  />
</template>

Accessibility ​

ElementRoleAttributes
Pills containertablist-
Each pilltabaria-selected (true for the active pill), focus-visible ring
Edge triggersbuttonaria-label (the trigger's text, falling back to "left" / "right"), focus-visible ring

Integration Caveats ​

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

  • Tokens/Theming: --goma-textSecondary/--goma-highlightSecondary (used and documented) plus the render-error banner's --goma-alertError/--goma-highlightPrimaryContrast are now registered in themeTokens.js and 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.4
    • SbIcon: a sport icon with no bundled artwork now falls back to the sport set's noicon glyph however its path is spelled. Before, only names starting icons/sports/<mode>/ got the placeholder. A host that named the icon sports/<mode>/<id>.svg (or assets/icons/sports/…, or icons/sportradar/…) got an empty slot instead, for example the sport pill of <goma-competition-filter> with Pool or Arena of Valor selected. Icons that have artwork are unaffected, and the host's own file is still requested first.
0.37.0
  • Pills and edge triggers accept an iconFallback: the name of a bundled icon to draw when icon names one that neither the bundled set nor the host can serve. A sport pill fed a category whose artwork does not exist yet now shows its family's glyph, or the sport set's noicon, instead of an empty slot. An icon that resolves is unaffected.
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.36.0
  • Pills accept an optional outlined: true item flag: the pill renders unfilled with a 2px ring (textSecondary at rest, highlightPrimary when selected) for a pill that should read differently from its siblings (for example an "All competitions" entry with a funnel icon). Its vertical padding follows the plain pills' border width, so it stays exactly as tall as the pills beside it. Items without the flag render exactly as before.
  • <goma-competition-filter> gains an appearance object (structure only): pillHeight, pillPaddingX, pillBorderWidth (plain pills) and barPaddingY. Unset, the pills keep their 38px height, 20px sides and 1px ring, and the strip its 12px padding, so the default look is unchanged. @gomagaming/core is bumped for the new utilities in its compiled sheet.
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.

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.30.0
  • <goma-competition-filter>'s edge triggers take a new optional caret field: leftTrigger/rightTrigger accept { …, caret: true } to draw an up/down caret after the label, marking the trigger as one that opens a picker. The glyph belongs to the widget rather than being passed in, so every bar that opens a picker shows the same affordance, and it inherits the trigger's textColor. Existing triggers are unaffected: with caret unset nothing renders.

    The edge triggers are also sized to the design rather than stretching to the bar's full height: both now sit at 38px, matching the pills between them, and the settings trigger is 38px wide around its glyph.

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's createWidgetElement() factory, and live-chat's and messenger's own element classes, now guard against importing a widget package in a non-browser context (SSR): the HTMLElement subclass was previously defined at module top level in every widget package, so importing any of them under server-side rendering threw a ReferenceError immediately.

    Per-package fixes:

    • favorites: added src/themeTokens.js and a :host stylesheet (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's dataSource away from rpc/subscribe never released the open topic.
    • filters-settings: fixed a dead :host{display:inline-block} CSS rule (the shared element factory's inline display:block always won), so the widget always rendered full-width instead of the intended compact inline trigger.
    • game-details: added a min-height fallback for the inert/no-eventId state 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. Corrected themeTokens.js registries that had drifted from actual token usage, added missing goma:retry Events-table rows, and other small doc/registry corrections.
    • region-filter: added a missing typeof window !== 'undefined' guard on a window listener.
    • live-chat: corrected custom-elements.json's members array (missing theme/config accessors, 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 in docs/guide/integration-caveats.md as needing human visual verification before release.

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.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.fonts API so widgets paint the intended typeface inside the shadow root.
    • Game-card market pill colours corrected (live separatorLine, prelive highlightPrimary); 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.
0.14.0
  • <goma-competition-filter> now scrolls the selected pill into the centre of the pills strip on selection (and when the item set changes with a pre-set selectedId), mirroring <goma-sports-navigation-horizontal>'s click-to-centre. Only the pills container scrolls: the host page's scroll position is untouched.
0.12.0
  • BREAKING: <goma-competition-filter> is overhauled into a generic, host-fed filter pill bar. It no longer makes any RPC / WAMP calls.

    • Pills are host-fed via items ([{ id, label, icon? }], icon = inline SVG), single-selection via selectedId; a tap emits goma:filter-select{ id, label, previousId }.
    • Two optional edge triggers: leftTrigger / rightTrigger, each { icon?, text?, color?, textColor? }. Render a pill flush to the bar edge and emit goma:filter-left / goma:filter-right on tap (the host wires the action).
    • Removed: dataSource, sportId, regionId, followSportPicker, followRegionPicker, showLabels/showIcons/skeletonCount, the goma:competition-select event, the useCompetitionsSource usage, and the @gomagaming/sports-domain dependency (now core-only).
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-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.

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