<goma-time-filter>
Dual-thumb range slider for picking a (start, end) time window. Both thumbs snap to host-supplied tick positions; the widget emits the snapped pair as '{startHours}-{endHours}', or the literal 'all' sentinel when both thumbs sit at the extremes. The lightest widget in the filter family: no WAMP transport, no sport-scoping, no API container: just a ticks array (shipped with a working default), a selection state, and a goma:time-select event.
Breaking change in 0.x
The intervals / intervalsJson props from an earlier pill-row version have been removed and replaced with ticks / ticksJson. The element no longer renders a pill row. The goma:time-select event payload and the selectedTimeValue timeValue segment format are unchanged.
Live demo
Open in the playground →: toggle Time filter in the widget rail. The playground pre-fills the inspector's "Ticks JSON" field with the widget's own built-in 8-tick set (Now / 1H / 3H / 6H / 12H / 24H / 48H / Everything) so you can see the shape before editing it.
Element tag
html
<goma-time-filter></goma-time-filter>Installation
bash
npm install @gomagaming/time-filterPlus the shared peer dependencies at your app level: see Installation for the one-time .npmrc token setup:
bash
npm install vue pinia vue-router vue-i18n @vueuse/coreConfiguration
No apiBaseUrl / socketUrl: there's no WAMP transport. Only the standard config bag applies:
| Key | Type | Description |
|---|---|---|
locale | string | Active i18n locale ('en' / 'fr' / 'pt'). Defaults to 'en'. |
theme | Record<string,string> | --goma-* token overrides. |
messages | object | Optional i18n phrase overrides. |
debug | boolean | Verbose console warnings for malformed ticks / ticksJson / selectedTimeValue. Defaults to false. |
js
const el = document.getElementById('tf')
el.config = { locale: 'en' }Data sources
Purely host-fed: there is no RPC/subscribe mode. Set el.ticks to an array of { hours, label } pairs, ordered ascending by hours. ticks is optional: the element ships a built-in default so the slider is usable out of the box.
js
// The widget's own default (packages/time-filter/src/TimeFilterElement.js)
[
{ hours: 0, label: 'Now' },
{ hours: 1, label: '1H' },
{ hours: 3, label: '3H' },
{ hours: 6, label: '6H' },
{ hours: 12, label: '12H' },
{ hours: 24, label: '24H' },
{ hours: 48, label: '48H' },
{ hours: 168, label: 'Everything' },
]Passing your own array overrides it entirely:
js
el.ticks = [
{ hours: 0, label: 'Now' },
{ hours: 1, label: '1H' },
{ hours: 6, label: '6H' },
{ hours: 24, label: '24H' },
{ hours: 168, label: 'Everything' },
]hoursis the canonical non-negative integer. A pair (ticks[i].hours,ticks[j].hours) wherei < jbecomes the'{a}-{b}'timeValuesegment of the matches-aggregator topic.labelis the visible text rendered in the inline header (e.g."Starts of events: Now – 24H"). Hosts pre-localise their labels: the widget never translates tick labels itself.- Entries with a missing / non-finite
hoursare dropped; duplicatehoursvalues are de-duped (first one kept); the remaining set is sorted ascending. Misconfigs warn underdebug: true. - The
'all'sentinel is the only no-filter path. Both thumbs sit at the extremes (left thumb atticks[0], right thumb at the last tick) andemitSelection()emits the literal stringtimeValue: 'all'instead of a'{a}-{b}'range. There is no separate "All" / reset control; moving either thumb away from an extreme immediately switches to the range format, and moving both back to the extremes re-emits'all'. - Only registered tick pairs are reachable: thumbs move index-by-index through
ticks, so a 5-tick array limits emissions to the 10 possible ascending(i, j)pairs plus'all'; there's no arbitrary'5-17'output. Thumbs hard-stop one tick apart and cannot cross.
selectedTimeValue accepts a host-driven range back: any '{a}-{b}' string must match an exact (ticks[i].hours, ticks[j].hours) pair or the widget falls back to 'all' (warning under debug).
Props / attributes
| Property | Attribute | Default | Description |
|---|---|---|---|
ticks | ticks (JSON) | The built-in 8-tick set | Array of { hours, label } pairs. Set via JS or a JSON-encoded HTML attribute. |
ticksJson | ticks-json | '' | Convenience JSON string. Empty / whitespace strings leave the existing ticks array untouched. |
selectedTimeValue | selected-time-value | 'all' | The currently-selected range. 'all' parks both thumbs at the extremes; a non-matching '{a}-{b}' falls back to 'all'. |
locale | locale | 'en' | Active i18n locale. |
theme | theme (JSON) | - | --goma-* token overrides. |
debug | - | false | Verbose console warnings. |
Events
| Event | detail | Emitted when |
|---|---|---|
ready | {} | Widget mounted. |
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: time-filter has no WAMP transport, so there is no connection that can fail. |
goma:retry | {} | User clicked Retry on the render-error banner. |
goma:time-select | { timeValue, timeLabel, previousTimeValue } | A thumb is released (pointer or keyboard) onto a new tick. timeValue is '{startHours}-{endHours}' or 'all'; timeLabel is '{leftLabel} – {rightLabel}' (e.g. 'Now – 24H'). The widget also mutates selectedTimeValue to match. Never fires on mount. |
Integrates with
Emits goma:time-select, which <goma-events-horizontal> listens for via its followTimePicker prop (default true) to rewrite the hoursInterval segment of its matches-aggregator topic in place: no reconnect. <goma-time-filter> itself listens for nothing and writes no shared-storage envelope. See Cross-widget wiring for the full rewrite contract.
Empty / loading / error states
- Fewer than two usable ticks (after dropping invalid entries and de-duping): the slider group doesn't render at all; only the inline header text shows, using the
time_filter_range_emptystring. No thumbs, no track. - Render error: a
role="alert"bar appears above the slider with theerror_prefixcopy ("ERROR:" in English), the caught error message, a retry button and a dismiss×button named "Close". The slider stays visible under it. The bar clears after 8 seconds, on dismiss, or on retry. A bar fromel.forceError(message)has no timer: it stays until dismissed or retried, or until the host callsel.clearError(). - There is no network round-trip, so there's no separate loading state: the widget renders synchronously from whatever
ticksit has (built-in default or host-supplied) on mount.
Theming
Six tokens carry the slider's paint surface. Override via el.theme = { … }: see Theming for the full token catalogue.
| Token | Element |
|---|---|
--goma-backgroundCards | Slider container background |
--goma-separatorLine | Out-of-range (dim) track |
--goma-highlightPrimary | In-range track + focus ring |
--goma-textPrimary | Thumbs, inner ticks, header text |
--goma-alertError | Render-error banner background |
--goma-highlightPrimaryContrast | Render-error banner Retry button text/background |
The full track paints --goma-separatorLine; the segment between the two thumbs paints --goma-highlightPrimary on top, so the active range reads as the accent colour against the dim base.
Appearance (styling object)
appearance carries structure only. Each key is a single value (identical in light and dark) that maps to one --goma-* CSS custom property on the host. Set it via el.appearance = {...} or the appearance attribute (a JSON string); unset keys keep the default, so an empty {} (the default) reproduces the current look. Colours stay in theme.
| Key | CSS variable | Default | Affects |
|---|---|---|---|
headerFontSize | --goma-timeFilterHeaderFontSize | 0.875rem (14px) | Header readout ("Starts of events: …") font size. |
headerLineHeight | --goma-timeFilterHeaderLineHeight | 1.25rem (20px) | Header readout line height. |
The separator between the two ends of the header comes from the time_filter_range_header translation, so a host that wants a different one (e.g. a hyphen) overrides that key through messages (see below).
i18n / message overrides
| Key | Default (en) | Used in |
|---|---|---|
time_filter_range_header | Starts of events: {left} – {right} | The inline header text; {left} / {right} are the currently snapped tick labels (live during drag). |
time_filter_range_empty | Starts of events: - | Rendered when fewer than two ticks are supplied. |
retry | Retry | The error bar's retry button (shared key). |
error_prefix | ERROR: | The error bar's prefix, before the message (shared key). |
close | Close | Accessible name of the error bar's dismiss × (shared key). |
The bundled dictionary also carries legacy time_filter_all_label / time_filter_aria / time_filter_empty / time_filter_label keys from a prior pill-row version; the current slider no longer references them.
Tick labels themselves are never localised by the widget: hosts pre-localise their ticks array before passing it in.
Embedding examples
Vanilla: drive an events-horizontal feed
html
<goma-time-filter id="times"></goma-time-filter>
<goma-events-horizontal
id="feed"
follow-time-picker
topic="custom-matches-aggregator/1/all/all/all/POPULAR/LIVE/10/5"
></goma-events-horizontal>
<script type="module">
import '@gomagaming/time-filter'
import '@gomagaming/events-horizontal'
document.getElementById('times').config = { locale: 'en' }
// el.ticks is optional: the built-in 8-tick default applies if omitted.
document.getElementById('feed').config = { /* socket + REST creds */ }
</script>Drag a thumb → events-horizontal rewrites segment 3 (hoursInterval) of its topic → the WAMP subscription rebinds transparently, no reconnect.
React
jsx
import '@gomagaming/time-filter'
import { useEffect, useRef } from 'react'
export function TimeFilter({ onSelect }) {
const ref = useRef(null)
useEffect(() => {
const el = ref.current
if (!el) return
el.config = { locale: 'en' }
const onTimeSelect = (e) => onSelect(e.detail)
el.addEventListener('goma:time-select', onTimeSelect)
return () => el.removeEventListener('goma:time-select', onTimeSelect)
}, [onSelect])
return <goma-time-filter ref={ref} />
}Vue
vue
<script setup>
import '@gomagaming/time-filter'
import { ref, onMounted } from 'vue'
const el = ref(null)
onMounted(() => { el.value.config = { locale: 'en' } })
</script>
<template>
<goma-time-filter ref="el" @goma:time-select="onTimeSelect" />
</template>Accessibility
| Element | Role | Attributes |
|---|---|---|
| Slider group | group | aria-labelledby pointing at the inline header span |
| Left thumb | slider | tabindex="0", aria-valuemin, aria-valuemax, aria-valuenow, aria-valuetext |
| Right thumb | slider | tabindex="0", aria-valuemin, aria-valuemax, aria-valuenow, aria-valuetext |
| Error banner | - | role="alert" for screen-reader announcement |
aria-valuemin / aria-valuemax reflect the hard-stop one tick away from the other thumb, so screen readers announce the valid range bounds. Each thumb's 16 px visual sits inside a transparent 44 px tap target via a ::before halo (WCAG 2.5.5).
Keyboard, per focused thumb:
- ArrowLeft / ArrowDown: back one tick.
- ArrowRight / ArrowUp: forward one tick.
- Home: left thumb jumps to the first tick; right thumb jumps to one tick past the left thumb.
- End: left thumb jumps to one tick before the right thumb; right thumb jumps to the last tick.
- PageDown / PageUp: ±2 ticks.
prefers-reduced-motion: reduce disables the thumb's settle animation.
What's deferred
- Per-tick label rendering below the track (the Figma reference shows tick gaps only; labels could be added in a follow-up).
- A
rangesprop to constrain emissions to an explicit subset of(ticks[i], ticks[j])pairs (current behaviour: anyi < jis emittable). - A native
<input type="range">fallback for non-pointer browsers (current behaviour:role="slider"divs with manual keyboard + pointer handling).
Integration Caveats
SSR/webview: importing this package outside a browser previously threw immediately; fixed with a guard on the shared element factory.
Misc hygiene: the render-error banner's
--goma-alertError/--goma-highlightPrimaryContrasttokens are now listed in the Theming table above.Initialization: this widget shares
@gomagaming/core's i18n singleton with every other widget on the page; mounting two widgets with differentlocalevalues on one page is not currently isolated per instance (a known constraint, not fixed in this pass).Configuring before the definition loads. Assigning
config(or any property) while the element is still an unknown element is safe: the value is replayed through the widget when it upgrades. That matters when loading from the CDN, whose loader imports widget modules asynchronously, so your script usually runs first. On release sets at 0.34.0 and earlier the value was silently discarded and the widget mounted with no configuration and a clean console: there, awaitwindow.__gomaCdn.readyfirst, or configure through attributes. See Setting a property before the widget's definition has loaded.
See Integration Caveats for the cross-widget picture.
Changelog
Current release: 0.37.11. Every GomaPulse package shares one version number and they are published together, so a release that changes nothing here still bumps this widget. Only the releases that changed it are listed below.
Every release that changed this widget, newest first. The latest is open; the rest expand.
0.37.0
Casino games and time filter: the error bar's dismiss
×is now a button named "Close", so keyboard and screen-reader users can reach it, and its "ERROR:" prefix comes from the newerror_prefixcopy key and follows the widget's locale. In casino games, the carousel arrows' names and the dots' names ("Go to page 1") also come from copy keys (previous,next, and the newgo_to_page).clearError()now removes an error bar thatforceError()showed, and forcing the same message again after a dismiss shows the bar again.Casino games: a heading of spaces only now shows the default title, like a blank one.
custom-elements.jsonnow declares thegoma:seo-dataandgoma:retryevents the widget already emitted, and types a tapped game'sidandnameas possiblynull. The theme token list now names the flame's own token (iconPrimary) and the error bar's, and the two optional arrow tokens (navArrowBackground,navArrowIcon) now list a default, also in multi-banners and sports navigation (horizontal).Core: the Portuguese "Next" label read "Prximo"; it now reads "Próximo".
The time filter's package README now describes the slider it ships (
ticks,goma:time-select), not the old pill row.custom-elements.jsonnow declaresgoma:retry, which the render-error bar's Retry button has always emitted. The playground's theme-token list now shows the two tokens the error bar paints (--goma-alertError,--goma-highlightPrimaryContrast) and the correct--goma-separatorLinedefault (#2a2d60). The error bar drops a monospace class that never took effect, so nothing on screen changes.
0.36.1
Playground manifests (
@gomagaming/<widget>/playground) now offer a control for every setting each widget really has. Settings that cannot be a form field (raw provider data, read-only state mirrors, props a widget never reads) are recorded with a reason in the repo instead of being silently missing.Controls that never reached the widget are fixed:
- The betslips'
placeBetHeadersJson/placeBetBodyJson/quickAddAmountsJsonare nowplaceBetHeaders/placeBetBody/quickAddAmounts. - The time-filter and filters-settings ticks editors set
ticks. - live-chat's layout control writes
position, the config key the element reads. - The sports-navigation widgets drop the unread
sportsJson. - JSON-typed defaults are real objects and arrays instead of serialized strings. The top-competitions
highlightedImagesstring default was indexed character by character and requested bad image URLs on every load. The missions-hubemptyStateImagesillustrations now show.
No widget runtime behaviour changes.
- The betslips'
0.36.0
<goma-time-filter>gains anappearanceobject (structure only) withheaderFontSizeandheaderLineHeightfor the "Starts of events: …" readout. Unset, the header stays at 14px / 20px, so the default look is unchanged. The separator between the two ends comes from thetime_filter_range_headertranslation; a host that wants a different one overrides that key throughmessages.@gomagaming/coreis bumped for the new utilities in its compiled sheet.
0.35.0
<goma-time-filter>and the settings modal's time slider: the header no longer resizes the track while the player drags. The header is a live readout of the selected window, and abovesmit shares a flex row with theflex-1track, so it changed width on every tick the thumb passed ("Starts of events: Now – Everything" is far wider than "Starts of events: 3H – 48H"), the track resized with it, and the bar and both thumbs visibly jumped under the pointer. The header column now reserves the widest reading its tick set can produce, via hidden sizers stacked on the live text in one grid cell, so the track's width is constant for a given tick set.No API change: same
tickscontract, samegoma:time-selectpayload, same markup belowsm(where the header is on its own line and drives nothing).
0.32.0
Widgets no longer dispatch a bare
errorevent alongsidegoma:error.Every factory-built widget's app-level Vue error handler dispatched two events for the same failure:
goma:error, and an un-namespacederror. The bare one is gone;goma:erroris unchanged and still carries the same{ message, code, component? }detail.Like every GomaPulse event it was
composedand bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had onwindow.addEventListener('error', …): as aCustomEventwithmessage,errorandfilenameallundefined, since those areErrorEventfields aCustomEventdoes not have. A host with error reporting installed received a malformed error record it could neither read nor attribute.That had been written off as harmless on the grounds that the handler behind it could never fire. It can. Vue begins its
errorCapturedwalk at the throwing component's parent, so theonErrorCapturedboundary each widget installs in its own root component cannot see that component's own errors, and being the root, there is nothing above it to catch them either. Anything raised in a widget's root scope (its setup, its render, its lifecycle hooks, its watchers) skips the boundary and reaches the app-level handler every time. Only errors from child components are caught by the boundary, which is what it is for: those show the in-shadow banner and emitgoma:retry.Hosts listening for
goma:errorneed no change. A host listening for bareerroron a widget element should move togoma:error; it was documented as unreachable, so in practice it delivered nothing until now.
0.26.0
Stop publishing source maps and package source.
dist/*.js.mapwas emitted by the shared Vite preset and swept into every tarball byfiles: ["dist/", "src/"], with the original source inlined insourcesContent.src/shipped the same source again, in the clear. Both are one URL away from a browser once a package is served from a CDN, so the preset now builds withsourcemap: falseandfilesno longer carriessrc/.@gomagaming/corekeeps the two paths its export map actually points at:src/styles/main.cssandsrc/types/index.d.ts. Its tarball drops from 1741 files to 576, which also brings it back under the 900-file limit a per-request unpacker can handle.Deep imports of
@gomagaming/<pkg>/src/*from an installed package no longer resolve. Nothing documented used them; imports inside the monorepo are unaffected.
0.9.2
- Fix a stale GPU repaint artifact (and pointer lag) when a slider thumb is dragged back and forth quickly: the thumb no longer animates its
lefttransition during an active pointer drag. It tracks the pointer 1:1. Keyboard / programmatic changes still animate.
0.9.0
New widget
<goma-competitions-list>: vertical list of competitions grouped by region, scoped per sport.Each region renders as a collapsible card (flag + region name + caret in the header, competition rows with separator lines in the expanded body), matching the design. Multi-open accordion; the first region card is expanded by default, controlled via
initialOpenRegionIds.- Reuses
useCompetitionsSource+useLocationsSource; joins them client-side onvenueIdto build the per-region groups. - Three data sources via
dataSource:'prop'(host-fed viael.competitions/el.regions),'rpc'(one-shotgetCompetitions+getLocationson connect; default),'subscribe'(placeholder, falls back to RPC). sportIdis mandatory (defaults to 1 = Football). Pair with the sports picker viafollowSportPickerto re-fetch on sport change.- Emits
goma:competition-selectwith the same payload as<goma-competition-filter>: downstream<goma-events-horizontal>withfollowCompetitionPicker: truecooperates with no changes. - Also emits
goma:region-toggle(informational) when a region accordion card is opened or closed.
Released alongside the rest of the lockstep family at a minor bump.
- Reuses
New widget
<goma-multi-banners>. A swipeable carousel of injectable banner cards, three types in one widget: sport events, casino game promos, and info banners.All data is host-fed via the
bannersarray (no CMS). For a sport banner, settingeventId(live or prelive) makes the widget resolve teams, start time, score and odds itself; without it the host supplies those fields inline. The background image is always host-fed.- Sport cards adapt to event state: live cards show the running clock/part and score (
home - away), prelive cards show the kickoff date + time. - Carousel chrome: prev/next arrows, pagination dots, swipe/drag, opt-in autoplay (off by default). The track is clipped + isolated so nothing bleeds over neighbouring widgets.
- Emits
goma:banner-select(every banner),goma:event-navigate(sport-card body tap → game-details), andgoma:outcome-select(sport odds tap → betslip, same shape as events-horizontal). - No new theme tokens; all colours map to existing
--goma-*tokens.
@gomagaming/sports-domainadditions (additive, no change to existing paths):- New action group
sports.bannerEvents(createBannerEventsActions): a multi-event subscription manager that accumulates events intobettingStore.events(skipStoreCleanup) so several event cards keyed by id coexist with live odds, unlike the singular-gamematchDetailspath. - Public exports for
useGameScoresTallOddsanduseGamePartsAndTimesTallOddsso other widgets can render live scores/clocks from a store event.
- Sport cards adapt to event state: live cards show the running clock/part and score (
0.8.0
New widget
<goma-top-competitions>: sidebar "Top Competitions" list.A vertical list of competition rows (sport icon + region flag + name) with a section title above. Selecting a row emits
goma:competition-selectwith the full payload (competitionId, competitionName, sportSportId, competitionVenueId, previousCompetitionId).- Three data sources via
dataSource:'prop'(host-fed),'rpc'(one-shotpopularTournamentscall on connect; default),'subscribe'(placeholder, falls back to RPC). - Sport scope set once via
config.sportId; no picker-chain wiring. - Sport icons resolved off
iconBaseUrl(/icons/sports/dark_mode/{sportId}.svg), flags offflagBaseUrl({venueId}.png). Same conventions as the rest of the filter family. - New composable
useTopCompetitionsSourcein@gomagaming/sports-domainwraps the existingoddsApi.betting.getTopCompetitionsRPC.
Released alongside the rest of the lockstep family at a minor bump.
- Three data sources via
0.7.0
Playground scenario presets, live theme-token editor, and family-wide picker-coupling defaults.
Widget runtime defaults
- events-horizontal:
followSportPicker,followRegionPicker,followCompetitionPicker,followTimePickerall flip fromfalsetotruein the element-class defaults and the playground manifest. Drop a sibling picker or filter on the page and the events feed auto-couples without any explicit config. Consumers that don't want the coupling set the flag tofalse(or the matching HTML attributefollow-sport-picker="false", etc.). - region-filter:
followSportPickerdefault flips totruein the element class + manifest. The widget will re-fetch the region list whenever a sibling sports picker emitsgoma:sport-select. - competition-filter:
followSportPickerandfollowRegionPickerdefaults both flip totrue. Drops the manual wiring step from the picker-chain composition. - region-filter / competition-filter: playground manifest's
sourcedefault flips from'mock'to'rpc'. Aligns the manifest with the element class's owndataSource: 'rpc'default; consumers of the published widgets are unaffected (mock was a playground-only convenience).
Breaking-ish note: a host page that mounts multiple of these widgets side-by-side without setting
follow*Pickerexplicitly will now see them auto-react to each other's selection events. The old isolated behaviour is restored withfollow*Picker: false.Playground SPA additions (not published; bundled into the docs site at
/playground)- Scenario presets: a named scenario is an ordered set of widgets
- per-instance configs + theme + token overrides + locale. Three built-ins ship:
full-betting(filters row + sports nav + events + game details + sidebar betslip),mobile-layout(dialog picker + events + floating betslip), anddark-neon-blue-showcase. TopBar gains a Scenario<select>that replaces widgets / configs / theme in one mutation; the dropdown auto-flips to "Custom" the moment the user diverges from the preset.
- per-instance configs + theme + token overrides + locale. Three built-ins ship:
- Live theme-token editor: new Inspector "Theme" tab grouped by prefix (Backgrounds, Text, Icons, Highlights, …). Edits write through
store.setThemeToken('--goma-X', '#hex')to the global override layer (sparseRecord<--goma-key, hex>), which Stage's fast-path watcher pushes viael.theme = …to every mounted element._applyTheme()writeshost.style.setProperty(--goma-X)reactively, so token edits repaint with no Vue remount. - Share URL: v3 envelope additively carries
themeTokensandactiveScenario. Default values are dropped from the encoded payload (size optimisation, hydrate re-applies defaults). Foreign payloads with poisonedthemeTokens(non-object, arrays, non-string values) are normalised back to{}inhydrate(). dark-minttheme renamed todark-neon-blue: palette pulled verbatim from the new Figma tokens export (Uniclub Dark Neon Blue). Highlights / live tags / button-primary flip from mint green (#5cffc0,#00ffbc) to neon cyan (#29f2ff); secondary accent flips from indigo#5b56ffto purple#8750ff. Backgrounds, text, icons, separators, dialog background are unchanged. New tokens added: inputs, alerts, betslip, game header, hero card, tickets, stats, shadows. Persisted state or share URLs that pintheme: 'dark-mint'fall back toTHEMES.defaultafter hydrate.
Tests
- New
tests/playground-scenarios.test.js: 19 vitest cases coveringapplyScenario,setThemeToken+activeScenarioCustom-flip,resetThemeTokens, share URL round-trip with the new envelope fields, hydrate normalisation of malformed payloads. - New
tests/integration/playground-scenarios.spec.ts. 4 Playwright cases: scenario → DOM mount, dark-neon-blue palette assertion (backgroundPrimary+highlightPrimary), live token edit without remount (element identity preserved), share URL round-trip across pages. tests/integration/playground-inspector.spec.ts: tab list extended with the new "Theme" tab.
- events-horizontal:
time-filter: pill row → dual-thumb range slider
<goma-time-filter>is restyled from a horizontal pill row to a dual-thumb range slider matching the UNICLUB design. The widget now renders an inline header ("Starts of events: Now – Everything") plus a single-track slider with two thumbs; the user drags either thumb to pick a(start, end)time window.Public surface: BREAKING
- Removed:
intervals,intervals-json,space-between,show-labelsprops (and their HTML attribute counterparts). - Added:
ticks,ticks-jsonprops.ticksis an array of{ hours: number, label: string }pairs ordered ascending byhours. The first tick is the "Now" anchor; the last is the "Everything" anchor. - Both thumbs are movable. Emissions snap to
(ticks[i].hours, ticks[j].hours)pairs withi < j: no arbitrary in-between values. Both thumbs at the extremes ((ticks[0], ticks[last])) is the only path totimeValue: 'all'.
Unchanged
goma:time-selectevent payload ({ timeValue, timeLabel, previousTimeValue }) andtimeValuesegment format ('{startHours}-{endHours}'or the literal'all'sentinel) are the same as the pill row. Downstream listeners (including<goma-events-horizontal followTimePicker>'s segment-3 topic rewrite) keep working without modification.selectedTimeValueis the same two-way controlled value.
Interaction
- Pointer: drag a thumb; releases snap to the nearest tick. Pointer capture handles
pointercancel/lostpointercapture(snap on interruption). - Keyboard: ArrowLeft / ArrowDown / ArrowRight / ArrowUp move the focused thumb ±1 tick; Home / End jump to that thumb's bound; PageDown / PageUp move ±2 ticks.
- Thumbs hard-stop one tick apart (no auto-swap).
- 16-px circular thumbs wrap a transparent 44-px tap target via a
::beforehalo (WCAG 2.5.5). prefers-reduced-motion: reducedisables the settle animation.
Theming
The slider paints with four existing tokens: no new tokens added:
--goma-backgroundCards: slider container background--goma-separatorLine: out-of-range (dim) track--goma-highlightPrimary: in-range track overlay + focus ring--goma-textPrimary: thumbs, inner ticks, header text
i18n (
@gomagaming/core)Two new bundled keys (en / fr / pt):
time_filter_range_header:"Starts of events: {left} – {right}"(named-interpolation; populated from the snapped tick labels, live during drag).time_filter_range_empty:"Starts of events: \u2014"(rendered when fewer than two ticks are supplied).
Legacy keys (
time_filter_all_label,time_filter_aria,time_filter_empty) remain in the bundled dictionary but are no longer referenced by the widget.Defaults
The built-in default
ticksset covers0 / 1 / 3 / 6 / 12 / 24 / 48 / 168hours (labelled Now / 1H / 3H / 6H / 12H / 24H / 48H / Everything).Hydration
selectedTimeValue: 'all'parks both thumbs at the extremes.'{a}-{b}'requires exacthoursmatches againstticks; any other value falls back to'all'and warns whendebug: true.
Defensive misconfiguration
ticks: []or fewer than two ticks: renders the empty header text only, no thumbs, no emission.- Duplicate
hours: dedupe (keep first) with debug warn. - Unsorted ticks: sort ascending defensively with debug warn.
Dependencies
Dropped
swiperfrompeerDependencies: the slider doesn't need it. Hosts that already had Swiper installed for other widgets are unaffected.- Removed:
0.6.0
Drop the unused
operatorIdfield from the public widget config surface. The field was declared in@gomagaming/coredefaults + TypeScript types but never read anywhere on the data path:ucsOperatorIdis the only operator identifier the EveryMatrix adapter (anduseBetslipLogic) consume. Settingconfig.operatorIdon a widget element now logs an "unknown config key" warning indebug: truemode instead of being silently accepted.Migration: consumers using
config.operatorIdshould rename toconfig.ucsOperatorId. No behavioural change for consumers who already only setucsOperatorId(which the docs have always recommended).@gomagaming/corechanges:createWidgetElement.js:COMMON_DEFAULTSno longer seedsoperatorId; theknownTopLevelKeysvalidator set drops it.types/index.d.ts: removedoperatorId?: stringfrom theGomaWidgetConfiginterface.
@gomagaming/betslip-sidebardocstring tweak:custom-elements.jsonexample endpoint placeholder changed from/place-bet/4313/v2/betsto/place-bet/2838/v2/bets(matches the staging operator id used by the playground EM demo preset). No behavioural change.
gomawt playground:
- The "Connection" panel drops the
operatorIdinput row; share-URL envelope, persistence, types, and preset filler all scrubbed in lockstep so older share URLs that carriedoperatorIdare silently ignored on hydrate (the field falls out ofSTATE_DEFAULTSand the validator'sstringFieldslist). - New mobile UI: ≤760 px viewport gets a bottom tab bar (Widgets / Stage / Inspect) routing the body panels; the topbar folds Mode/Theme/Locale/API URL behind a
⋮disclosure; Connection panel grid collapses to a single column; tab targets meet 44 px touch-target guidance and inputs use 16 px font to suppress iOS focus-zoom. - EM demo preset refreshed to the NDW staging endpoint (
wss://api-ndwstage.everymatrix.com/v2, realmwww.demo1-stage.everymatrix.com, ucsOperatorId2838).
VitePress docs site:
- Locked to dark mode (
appearance: 'force-dark'): the appearance toggle is hidden from the nav. The playground's dark--bg-*tokens no longer two-tone against a light VitePress chrome. - Homepage, introduction, publishing, and widget reference docs refreshed to current state: nine widgets shipped, eleven publishable packages in the lockstep group, example URLs scrubbed of the previous client-specific staging hostnames (now use generic
api.example.com/sports-api.example.complaceholders), exampleucsOperatorIdliteral updated to2838. Three new widget docs (region/competition/time-filter) added to the homepage feature sections.
New widget:
<goma-time-filter>. Horizontal pill row of time-interval filters with a hardcoded "All" pill on the leftmost slot. Pure host-fed; no WAMP transport, no sport-scoping, no API container.Lighter than the rest of the filter family: no
setupAppplumbing on the element, no shared composable in@gomagaming/sports-domain. Just anintervalsprop, aselectedTimeValueselection state, and agoma:time-selectevent.What's new
- New package
@gomagaming/time-filter. Element:<goma-time-filter>. Reference docs:docs/widgets/time-filter.md. - The host feeds
el.intervals = [{ label, value }, …].labelis rendered verbatim (hosts pre-localise, only the hardcoded "All" pill is locale-driven, via the bundledtime_filter_all_labelkey).valuematches the matches-aggregator topic's hoursInterval segment ('0-1','0-24','24-48', etc.). Malformed entries drop silently; duplicates byvaluekeep the first occurrence. - The hardcoded "All" pill emits
{ timeValue: 'all', timeLabel: <localised>, previousTimeValue }viagoma:time-select: maps straight to the matches-aggregator topic's literalallno-filter hoursInterval segment.
@gomagaming/events-horizontaladditions- New
followTimePicker+timePickerSelectoropt-in props. Fourth in the picker chain. Listens forgoma:time-selectat the window and rewrites segment index 3 (hoursInterval) of the matches-aggregator topic withdetail.timeValue. Reuses the existingrewriteTopicSegment(topic, idx, value)helper (with its canonical-prefix fallback so mock-mode + pre-WAMP-connect rewrites still land). - Playground manifest gains a
followTimePickertoggle under "advanced".
@gomagaming/coreadditions- Three new bundled i18n keys per locale (en/fr/pt):
time_filter_all_label,time_filter_aria,time_filter_empty.
gomawt playground
time-filterjoins the registry, typesWidgetIdunion, storeALL_WIDGETS, VitePress theme side-effect import, Stage event-log allow-list (goma:time-select), and EventLogPanel category classifier.- demo entry-file imports added across playground/isolation/stress/ soak; workspace dep +
optimizeDeps.excludeentry indemo/.
Four-segment chain ready out of the box: drop a sports navigation picker, a region filter, a competition filter, a time filter, and an events-horizontal feed on the same page; turn on the relevant
follow*Pickerflags and the topic chain{sportId}/{locationId}/{tournamentId}/{hoursInterval}updates segment-by-segment with no host glue and no WAMP reconnect.- New package
All notable changes to this package will be documented in this file. Auto-generated by Changesets; do not edit by hand.