ART-379 · Form Bubbles · mobile + desktop — scroll for the full spec ↓

Form Bubbles

Η πρόσφατη φόρμα μιας ομάδας: κύκλοι που επικαλύπτονται, με το γράμμα του αποτελέσματος μέσα (Ν / Ι / Η) και ? για τον επόμενο αγώνα. Δύο παραλλαγές — desktop (hero, popover ανά bubble) και mobile (Βαθμολογία, modal με όλους τους αγώνες). Κάνε κλικ στα bubbles και στις δύο.

desktop

data-variant="desktop"

Σκούρα επιφάνεια (hero banner). Hover, click ή focus ανοίγει το .fpop popover του συγκεκριμένου αγώνα: έδρα · αντίπαλος, σκορ, αποτέλεσμα + κάρτες, mini 0–90′ timeline.

mobile

data-variant="mobile"

Φωτεινή επιφάνεια (πίνακας βαθμολογίας). Tap σε οποιοδήποτε bubble ανοίγει το .fdmodal με όλους τους αγώνες της ομάδας — όχι popover σε hover.

Design → Dev Handover

Component form-bubbles v1.0.0 · ART-379 · βιβλιοθήκη foxbet-components/components/form-bubbles/. Port από foxbet-responsive.html. Όπου το κείμενο και το HTML διαφωνούν, ισχύει το HTML.

Assets

Αυτό το component δεν χρησιμοποιεί εικόνες. Τα bubbles είναι καθαρό CSS + ελληνικά γράμματα, οπότε το assets στο component.json είναι σκόπιμα κενό: [].

ΤΟ ΜΟΝΟ ΓΡΑΦΙΚΟ — ΤΟ ΔΙΝΕΙ Ο CONSUMER

Το έμβλημα στην κεφαλίδα του modal δεν ανήκει στο component: το περνά η σελίδα μέσω data-crest στο .formdots. Το component απλώς το βάζει σε <img>. Αν λείπει, η κεφαλίδα δουλεύει κανονικά χωρίς εικόνα.

Καμία εξάρτηση από αρχεία — αντιγράφεις component.css + component.js και δουλεύει.

01Οι δύο παραλλαγές

Η παραλλαγή δηλώνεται με data-variant στο .formdots.

desktopmobile
Μοντέλοbubbles του hero bannerbubbles της Βαθμολογίας
Επιφάνειασκούρα → υπογράμμιση #fffφωτεινή → υπογράμμιση var(--ink)
Δείκτηςcursor:pointer + hover translateY(-3px):active{transform:scale(.85)}
Λεπτομέρεια.fpop popover, ανά bubble.fdmodal scrim + κάρτα, όλοι οι αγώνες
Ανοίγει μεclick · hover ποντικιού · focustap
Κλείνει μεpointerleave · blur · pointerdown έξω · scrolltap στο scrim · .fdmodal-x
Στοίχισηflex defaultjustify-content:center
⚠️
Η δηλωμένη παραλλαγή αποφασίζει — ΟΧΙ το πλάτος του viewport. Το data-variant="mobile" ανοίγει modal σε οποιοδήποτε πλάτος· το data-variant="desktop" κρατά popover σε οποιοδήποτε πλάτος. Το matchMedia('(max-width:1199.98px)') είναι μόνο fallback όταν δεν δηλώνεται data-variant (legacy responsive σελίδες). Στο prototype μία responsive σελίδα άλλαζε συμπεριφορά στο breakpoint· εδώ ο consumer δηλώνει τι θέλει.

02Ανατομία & γεωμετρία

Η γεωμετρία είναι κοινή στις δύο παραλλαγές — γράφεται μία φορά.

ΣτοιχείοΤιμήΣημείωση
Διάμετρος25 × 25px+ 2px λευκό border → 29px εξωτερικά
Επικάλυψηmargin-left:-8pxσε κάθε bubble εκτός του πρώτου· gap:0
Radius50%κύκλος
ΓράμμαRoboto Mono 12px / 700var(--num), λευκό
Υπογράμμιση2.5px, left/right 18%, bottom -5pxradius 4px
Tap feedback:active{transform:scale(.85)}transition .12s ease
⚠️
Η υπογράμμιση δείχνει τον τελευταίο ΠΑΙΓΜΕΝΟ αγώνα. Ένα .fq στο τέλος δεν είναι αποτέλεσμα: το .formdots .fd.fq:last-child::after{display:none} το ακυρώνει και το .formdots:has(.fq) .fd:nth-last-child(2)::after βάζει την υπογράμμιση στο προηγούμενο. Μη «απλοποιήσεις» αυτούς τους δύο κανόνες σε έναν.

03Χρώματα αποτελέσματος

ΚατάστασηΚλάσηΓράμμαFill
Νίκη.fwΝlinear-gradient(135deg,#039855,var(--win))
Ισοπαλία.fdrΙ#999999
Ήττα.flΗlinear-gradient(135deg,var(--err),var(--errdark))
Επόμενος.fq?var(--fill) + κείμενο #808080

04Συμβόλαιο δεδομένων

Κανονική είσοδος: data-fm JSON σε κάθε παιγμένο bubble.

ΠεδίοΤύποςΣημασία
v"H" / "A"έδρα — εντός / εκτός· παραλείπεται σε ουδέτερη έδρα
ostringαντίπαλος
dtstringημερομηνία εμφάνισης, π.χ. "25 Μαρ"
gf / gaintγκολ υπέρ / κατά (πάντα από τη σκοπιά της ομάδας)
r"W"/"D"/"L"αποτέλεσμα
y / rdintκίτρινες / κόκκινες κάρτες
earrayσυμβάντα [[λεπτό, "g"|"y", "us"|"them"], …] για το timeline

ΣΤΟ .formdots

AttributeΡόλος
data-variant"mobile" ή "desktop"υποχρεωτικό στη βιβλιοθήκη
data-teamόνομα ομάδας για την κεφαλίδα του modal
data-crestURL εμβλήματος· προαιρετικό

ΣΤΟ ? BUBBLE · ΚΑΙ LEGACY

AttributeΜορφή
data-fmnext"<ετικέτα>: vs <αντίπαλος> · <πότε>" — split στο πρώτο :, μετά στο ·· τελικό HH:MM = ώρα έναρξης
data-tiplegacy"Νίκη 3-0 vs Κόστα Ρίκα · 11 Ιουν". Το init() το μετατρέπει αυτόματα σε data-fm. Μη γράφεις νέο markup με αυτό.
⚠️
Το prototype έψαχνε το DOM της σελίδας (.standwrap<tr>.tm .full) για το όνομα και το έμβλημα, και το modal γινόταν appendChild στο .phone. Στη βιβλιοθήκη αυτά αντικαταστάθηκαν από data-team/data-crest και από opts.mount. Η παλιά αναζήτηση προγόνου μένει μόνο ως fallback, ώστε οι υπάρχουσες σελίδες να δουλεύουν χωρίς αλλαγή markup. Το hook για πίνακες με οριζόντιο scroll είναι τώρα [data-fb-scroll], όχι το .standwrap.

05API

Ένα global: window.FormBubbles. Το init() είναι idempotent (κάθε bubble δένεται μία φορά, μέσω data-fb-bound).

ΜέλοςΠεριγραφή
init(opts)opts.root (default document), opts.mount (default document.body — πού μπαίνει το modal)
MOBILE_QUERY'(max-width:1199.98px)' — μόνο fallback χωρίς data-variant
parseTip(tip)legacy string → data-fm payload· null αν δεν ταιριάζει
teamOf(group){nm, fl} από data-team/data-crest, με fallback προγόνου
// μετά το markup, μία φορά
FormBubbles.init();

// ή με ρητό mount point (π.χ. μέσα σε device frame)
FormBubbles.init({ root: document.querySelector('.page'), mount: document.querySelector('.phone') });

06Καταστάσεις

Κατάστασηdesktopmobile
defaultκύκλος 25px, γράμμα λευκό
hovertranslateY(-3px) + z-index:8
focusfocus ring μέσω tabindex="0"· στο desktop ανοίγει το popover
pressedscale(.85)
open.fpop ανοιχτό.fdmodal ανοιχτό
reduced motion@media (prefers-reduced-motion:reduce)transition:none

07Design tokens

Το component δεν δηλώνει tokens — μόνο τα χρησιμοποιεί. Πηγή: tokens/tokens.css.

TokenΤιμήΧρήση
--ink#1A1A1Aυπογράμμιση σε φωτεινή επιφάνεια, κείμενο
--mut#666666ημερομηνίες, δευτερεύον κείμενο
--line#E6E6E6borders, timeline
--fill#F2F2F2.fq fill, κεφαλίδα modal
--acc#CB2F01score chip, ετικέτα «Επόμενος αγώνας»
--win#027A48τέλος gradient νίκης
--err#D92D20αρχή gradient ήττας
--errdark#912018τέλος gradient ήττας
--num'Roboto Mono'ΟΛΟΙ οι αριθμοί και τα γράμματα των bubbles
ℹ️
Τα #039855 (αρχή gradient νίκης), #999999 (ισοπαλία), #808080, #FDB022 (κίτρινη κάρτα) και #FFEBE6/#FFD8CC (score chip) μένουν σκόπιμα literals — είναι χειροκίνητα επιλεγμένα, όπως στο odds-cta.

08Προσβασιμότητα

  • Κάθε .fd έχει tabindex="0" — προσβάσιμο από πληκτρολόγιο.
  • Το popover έχει role="tooltip"· το κουμπί κλεισίματος του modal aria-label="Κλείσιμο".
  • prefers-reduced-motion απενεργοποιεί το transition.

ΑΠΟΔΕΚΤΟ ΧΡΕΟΣ WCAG — ΜΕΤΡΗΜΕΝΟ 2026-07-29

Τα γράμματα είναι 12px/700. Το bold 12px δεν είναι «large text» κατά WCAG, άρα ισχύει το όριο 4.5:1.

ΚατάστασηΚείμενο σεΛόγοςAA 4.5
Νίκη — αρχή gradient#FFFFFF σε #0398553.73
Ισοπαλία#FFFFFF σε #9999992.85(και AA-large ✗)
Επόμενος .fq#808080 σε #F2F2F23.53
Νίκη — τέλος gradient#FFFFFF σε #027A485.41
Ήττα — αρχή / τέλος#FFFFFF σε #D92D20 / #9120184.83 / 8.66
⚠️
Τρεις από τις τέσσερις καταστάσεις αποτυγχάνουν στο AA. Προϋπήρχε στο prototype. Απόφαση 2026-07-29: port ως έχει, καταγραφή ως χρέος στο contrastKnownFail του component.json, χωρίς αλλαγή χρωμάτων. Τα bubbles δεν βασίζονται μόνο στο χρώμα — το γράμμα Ν/Ι/Η μεταφέρει το αποτέλεσμα, που καλύπτει το SC 1.4.1 (Use of Colour). Το ανοιχτό θέμα είναι το contrast, όχι η κωδικοποίηση.

09Markup — copy/paste

MOBILE — ΒΑΘΜΟΛΟΓΙΑ (5 ΑΓΩΝΕΣ)

<div class="formdots" data-variant="mobile"
     data-team="Ολυμπιακός" data-crest="/img/teams/olympiacos.svg">
  <span class="fd fw"  data-fm='{"v":"H","o":"ΑΕΚ","dt":"24 Αυγ","gf":2,"ga":0,"r":"W","y":0,"rd":0,"e":[[23,"g","us"],[71,"g","us"]]}'>Ν</span>
  <span class="fd fw"  data-fm='{"v":"A","o":"ΠΑΟΚ","dt":"31 Αυγ","gf":3,"ga":1,"r":"W","y":1,"rd":0,"e":[]}'>Ν</span>
  <span class="fd fdr" data-fm='{"v":"H","o":"Παναθηναϊκός","dt":"14 Σεπ","gf":1,"ga":1,"r":"D","y":2,"rd":0,"e":[]}'>Ι</span>
  <span class="fd fw"  data-fm='{"v":"A","o":"Άρης","dt":"21 Σεπ","gf":2,"ga":1,"r":"W","y":1,"rd":0,"e":[]}'>Ν</span>
  <span class="fd fl"  data-fm='{"v":"H","o":"ΟΦΗ","dt":"28 Σεπ","gf":0,"ga":1,"r":"L","y":1,"rd":1,"e":[]}'>Η</span>
</div>

DESKTOP — HERO, ΜΕ ΕΠΟΜΕΝΟ ΑΓΩΝΑ

<div class="formdots" data-variant="desktop" data-team="Ολλανδία">
  <span class="fd fw" data-fm='{"v":"H","o":"Αυστρία","dt":"25 Μαρ","gf":3,"ga":0,"r":"W","y":1,"rd":0,"e":[[12,"g","us"]]}'>Ν</span>
  <span class="fd fq" data-fmnext="Επόμενος: vs Ιαπωνία · Σάβ 14 Ιουν 23:00">?</span>
</div>

ΠΙΝΑΚΑΣ ΜΕ ΟΡΙΖΟΝΤΙΟ SCROLL

<!-- βάλε data-fb-scroll στο wrapper: το popover γίνεται position:fixed -->
<div class="standwrap" data-fb-scroll> <table>…</table> </div>

10Τι ΔΕΝ περιλαμβάνεται

ΕκτόςΛόγος
.fdacc κέλυφος, drag handle, @keyframes fdaccOpen, .fdacc-hd, .fdacc-xΤο accordion του hero σε mobile. Το mobile ακολουθεί τη Βαθμολογία → modal. Οι κλάσεις γραμμής (.fdacc-row/-date/-match/-score) περιλαμβάνονται — το modal τις χρησιμοποιεί.
#formtipΕξυπηρετούσε μόνο legacy data-tip hover· μετά την αναβάθμιση σε data-fm το .fpop το καλύπτει.
.formbtn, .fcol-early, .stand.full.formfirstΤο quick-toggle της στήλης ΦΟΡΜΑ — συμπεριφορά του πίνακα, όχι των bubbles.
Μεγέθη 22px / 20px.tcard-hd και .pr-fx-score. Ζητήθηκαν δύο παραλλαγές, όχι κλίμακα μεγεθών.
.form / .formcolΚάρτες «Πρόσφατη φόρμα» — layout δοχείου, ξεχωριστό component αν χρειαστεί.
ℹ️
Το πρόθεμα fdacc- στις κλάσεις γραμμής είναι πλέον παραπλανητικό (το accordion δεν περιλαμβάνεται) αλλά δεν μετονομάζεται — ο κανόνας verbatim ονομάτων ισχύει, ώστε να μη χρειαστεί αλλαγή σε καμία σελίδα.

11Build checklist

  • Αντίγραψε component.css και component.js. Καμία εξάρτηση, κανένα asset.
  • Βεβαιώσου ότι τα --win / --err / --errdark υπάρχουν στα tokens.
  • Κάθε .formdots πρέπει να δηλώνει data-variant.
  • Δώσε data-team (και data-crest αν υπάρχει) σε κάθε ομάδα — αλλιώς η κεφαλίδα του modal πέφτει στο fallback.
  • Το backend βγάζει data-fm JSON. Μη γράφεις νέο data-tip.
  • data-fb-scroll σε κάθε wrapper πίνακα με οριζόντιο scroll.
  • Κάλεσε FormBubbles.init() μία φορά μετά το render. Ξανακάλεσέ το μετά από AJAX — είναι idempotent.
  • Έλεγξε: υπογράμμιση στον τελευταίο παιγμένο αγώνα, και στις δύο περιπτώσεις (με και χωρίς ?).
91 αυτόματα tests στο tools/test_build.py καλύπτουν το contract, το scope των κλάσεων, τα tokens και τα contrast ratios. Τρέξε python -m unittest discover -s tools -p "test_*.py" από το foxbet-components/.
21ΕΕΕΠ | 21+ | ΠΑΙΞΕ ΥΠΕΥΘΥΝΑ